@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/CHANGELOG.md +22 -0
- package/README.en.md +35 -13
- package/README.md +39 -17
- package/lib/bridge.d.ts +205 -2
- package/lib/bridge.js +1053 -6
- package/lib/client.js +564 -27
- package/lib/evidence.d.ts +15 -0
- package/lib/evidence.js +123 -0
- package/lib/journal.d.ts +15 -0
- package/lib/journal.js +32 -11
- package/lib/knowledge-store.d.ts +111 -10
- package/lib/knowledge-store.js +1056 -198
- package/lib/knowledge.d.ts +55 -3
- package/lib/knowledge.js +65 -5
- package/lib/mask.d.ts +3 -1
- package/lib/mask.js +8 -2
- package/lib/retrieval.d.ts +47 -7
- package/lib/retrieval.js +196 -19
- package/lib/tools.js +406 -60
- package/lib/web.d.ts +52 -2
- package/lib/web.js +155 -14
- package/package.json +2 -2
- package/skills/dream-protocol/SKILL.md +15 -6
package/lib/tools.js
CHANGED
|
@@ -5,15 +5,16 @@
|
|
|
5
5
|
* @module dsh-dream/tools
|
|
6
6
|
*/
|
|
7
7
|
import { createHash } from 'node:crypto';
|
|
8
|
-
import { existsSync } from 'node:fs';
|
|
9
|
-
import { join } from 'node:path';
|
|
8
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
9
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
10
10
|
import { maskSecrets } from './mask.js';
|
|
11
11
|
import { dreamStats, readDreams, saveDream, searchDreams } from './journal.js';
|
|
12
|
-
import { bridgeDreams } from './bridge.js';
|
|
12
|
+
import { applyBridge, bridgeDreams, previewBridge, rollbackBridge, selectBridgeLessons, } from './bridge.js';
|
|
13
13
|
import { digestSessionFile, listSessionFiles } from './sessions.js';
|
|
14
14
|
import { KnowledgeError, lessonFingerprint, } from './knowledge.js';
|
|
15
15
|
import { KnowledgeStore } from './knowledge-store.js';
|
|
16
|
-
import {
|
|
16
|
+
import { verifyEvidence } from './evidence.js';
|
|
17
|
+
import { queryScopeLabel, resolveRetrievalBudget, retrieveLessons, retrievedLessonChars, taskPlatform, } from './retrieval.js';
|
|
17
18
|
/** 编译一个值 schema 节点;支持 enum、对象数组 items 与嵌套对象(宿主 schema 子集要求对象显式 additionalProperties)。 */
|
|
18
19
|
function compileValueNode(prop) {
|
|
19
20
|
const node = {};
|
|
@@ -129,6 +130,40 @@ function knowledgeDirOf(cfg) {
|
|
|
129
130
|
function knowledgeStoreOf(cfg) {
|
|
130
131
|
return new KnowledgeStore(knowledgeDirOf(cfg), { maskSecrets: cfg.maskSecrets });
|
|
131
132
|
}
|
|
133
|
+
function execCwdOf(exec) {
|
|
134
|
+
const root = asRecord(exec);
|
|
135
|
+
const candidates = [asRecord(asRecord(root.agent).session), asRecord(root.session), root];
|
|
136
|
+
for (const candidate of candidates) {
|
|
137
|
+
const cwd = candidate.cwd;
|
|
138
|
+
if (typeof cwd === 'string' && cwd.trim() !== '')
|
|
139
|
+
return cwd.trim();
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
/** 软链/junction 场景:跟随链接后的真实写入路径(与 bridge resolveBridgeTarget 同口径)。 */
|
|
144
|
+
function resolvedBridgePath(projectRoot, explicitPath) {
|
|
145
|
+
try {
|
|
146
|
+
const lexical = explicitPath !== undefined ? resolve(projectRoot, explicitPath) : join(projectRoot, 'AGENTS.md');
|
|
147
|
+
if (existsSync(lexical))
|
|
148
|
+
return realpathSync(lexical);
|
|
149
|
+
return join(realpathSync(dirname(lexical)), basename(lexical));
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/** 合并 bridge 自身 skipped 与工具层前置校验 skipped(按 lessonId 去重,bridge 结果优先)。 */
|
|
156
|
+
function mergeSkipped(primary, extra) {
|
|
157
|
+
const out = [...primary];
|
|
158
|
+
const seen = new Set(primary.map((entry) => entry.lessonId));
|
|
159
|
+
for (const entry of extra) {
|
|
160
|
+
if (seen.has(entry.lessonId))
|
|
161
|
+
continue;
|
|
162
|
+
seen.add(entry.lessonId);
|
|
163
|
+
out.push(entry);
|
|
164
|
+
}
|
|
165
|
+
return out;
|
|
166
|
+
}
|
|
132
167
|
function sha256Short(value) {
|
|
133
168
|
return createHash('sha256').update(value, 'utf8').digest('hex').slice(0, 32);
|
|
134
169
|
}
|
|
@@ -173,6 +208,11 @@ function parseEvidenceArgs(args, key, projectId) {
|
|
|
173
208
|
entry.recordSeq = recordSeq;
|
|
174
209
|
if (projectId !== undefined)
|
|
175
210
|
entry.projectId = projectId;
|
|
211
|
+
for (const key of ['quote', 'sourceHash', 'artifactPath']) {
|
|
212
|
+
const value = optionalString(item, key);
|
|
213
|
+
if (value !== undefined)
|
|
214
|
+
entry[key] = value;
|
|
215
|
+
}
|
|
176
216
|
out.push(entry);
|
|
177
217
|
});
|
|
178
218
|
return out;
|
|
@@ -210,11 +250,31 @@ function evidenceItemsSpec() {
|
|
|
210
250
|
sessionId: { type: 'string', description: '会话 ID(session 证据建议提供)。' },
|
|
211
251
|
recordSeq: { oneOf: [{ type: 'string' }, { type: 'integer' }], description: '记录序号(数字或字符串)。' },
|
|
212
252
|
summary: { type: 'string', description: '已脱敏的一句话摘要。' },
|
|
213
|
-
|
|
253
|
+
quote: { type: 'string', description: '来源可见正文中的原文片段;摘要为转述时必填。引用原文只用于核验,不持久化。' },
|
|
254
|
+
sourceHash: { type: 'string', description: '可选的来源 SHA-256;插件自行计算并核对,不能只凭此值证明来源。' },
|
|
255
|
+
artifactPath: { type: 'string', description: 'local-artifact 的文本路径;只能读取宿主当前工作区内文件。' },
|
|
256
|
+
verification: { type: 'string', enum: ['read', 'claimed'], description: 'read=请求插件核验引用;找不到或不匹配自动降为 claimed。读取不代表用户采纳。' },
|
|
214
257
|
},
|
|
215
258
|
required: ['kind', 'summary', 'verification'],
|
|
216
259
|
};
|
|
217
260
|
}
|
|
261
|
+
/** 冲突组校验:primary + conflictWith 全部存在、无自身/重复;任何写前完成。 */
|
|
262
|
+
function requireConflictGroup(store, lessonId, primary, conflictWith) {
|
|
263
|
+
const group = [lessonId];
|
|
264
|
+
for (const id of conflictWith) {
|
|
265
|
+
if (id === lessonId)
|
|
266
|
+
throw new KnowledgeError('invalid', 'conflictWith 不得包含 lessonId 自身:' + id);
|
|
267
|
+
if (group.includes(id))
|
|
268
|
+
throw new KnowledgeError('invalid', 'conflictWith 存在重复 id:' + id);
|
|
269
|
+
group.push(id);
|
|
270
|
+
}
|
|
271
|
+
return group.map((id) => {
|
|
272
|
+
const lesson = id === lessonId ? primary : store.getLesson(id);
|
|
273
|
+
if (lesson === undefined)
|
|
274
|
+
throw new KnowledgeError('invalid', '冲突对象不存在:' + id);
|
|
275
|
+
return { id, lesson };
|
|
276
|
+
});
|
|
277
|
+
}
|
|
218
278
|
const baseSchema = { type: 'object', additionalProperties: true };
|
|
219
279
|
/** 构建六个做梦工具。 */
|
|
220
280
|
export function buildDreamTools(config) {
|
|
@@ -378,28 +438,161 @@ export function buildDreamTools(config) {
|
|
|
378
438
|
};
|
|
379
439
|
const dreamBridge = {
|
|
380
440
|
name: 'dream_bridge',
|
|
381
|
-
description: '
|
|
441
|
+
description: '渡梦(M2):把已采纳且适用的经验桥接进项目 AGENTS.md 的 Dream 管理块(<!-- dsh-dream:start -->…<!-- dsh-dream:end -->),块外内容原样保留。mode:"preview" 只读看 diff;复核后 mode:"apply" 必须带 preview.expected.sha256(文件变了会 conflict 零写入);mode:"rollback" 带 backupId 恢复。缺省 mode 为 legacy 直写(已弃用,响应含 deprecation 提示)。资格:经验 state=usable,且 scope 与当前项目匹配(目标目录 realpath / 显式 projectId / global 且已采纳),频次不作准入。',
|
|
382
442
|
parameters: compileParameters({
|
|
383
|
-
|
|
384
|
-
|
|
443
|
+
mode: { type: 'string', enum: ['preview', 'apply', 'rollback'], description: 'preview=只读预览;apply=带 expectedSha256 落盘;rollback=按 backupId 恢复。缺省=legacy 直写(弃用)。' },
|
|
444
|
+
path: { type: 'string', description: '目标文件(可选;缺省为执行上下文 cwd 下的 AGENTS.md,取不到 cwd 会报错不猜)。' },
|
|
445
|
+
lessonIds: { type: 'array', items: { type: 'string' }, description: '参与桥接的经验 id(必须 usable 且 scope 匹配;缺省=按 store 顺序自动选取资格通过者)。' },
|
|
446
|
+
maxLessons: { type: 'integer', description: '经验上限:M2 模式默认 5、硬上限 20;legacy 模式默认 10、上限 30。' },
|
|
447
|
+
expectedSha256: { oneOf: [{ type: 'string' }, { type: 'null' }], description: 'apply 必填:preview.expected.sha256 原样回传;新文件用 null;不符则 conflict 零写入。' },
|
|
448
|
+
previewId: { type: 'string', description: 'apply 可带:preview.previewId,用于校验是同一份预览;块内容不一致 → preview-mismatch,需重新 preview。' },
|
|
449
|
+
backupId: { type: 'string', description: 'rollback 必填:apply 成功返回的 backupId。' },
|
|
450
|
+
projectId: { type: 'string', description: '当前项目 id(可选;用于匹配只带 projectId 的经验,纯增量)。' },
|
|
385
451
|
}),
|
|
386
452
|
output: {
|
|
387
453
|
schema: baseSchema,
|
|
388
454
|
render: (_args, value) => {
|
|
389
455
|
const rec = asRecord(value);
|
|
456
|
+
const mode = rec.mode;
|
|
457
|
+
if (rec.ok === false) {
|
|
458
|
+
const failure = asRecord(rec.error);
|
|
459
|
+
return [{ type: 'text', text: '桥接失败(' + (typeof mode === 'string' ? mode : 'legacy') + '):' + failure.code + ' ' + failure.message }];
|
|
460
|
+
}
|
|
461
|
+
if (mode === 'preview') {
|
|
462
|
+
const lessons = Array.isArray(rec.lessons) ? rec.lessons : [];
|
|
463
|
+
const skipped = Array.isArray(rec.skipped) ? rec.skipped : [];
|
|
464
|
+
const expected = asRecord(rec.expected);
|
|
465
|
+
const lines = ['桥接预览(' + rec.action + '):' + lessons.length + ' 条经验,当前 sha256=' + expected.sha256 + ',previewId=' + rec.previewId];
|
|
466
|
+
for (const item of lessons) {
|
|
467
|
+
const lesson = asRecord(item);
|
|
468
|
+
lines.push('- [' + lesson.lessonId + '@' + lesson.revision + '] ' + lesson.title);
|
|
469
|
+
}
|
|
470
|
+
for (const item of skipped) {
|
|
471
|
+
const entry = asRecord(item);
|
|
472
|
+
lines.push('· 跳过 ' + entry.lessonId + ':' + entry.reason);
|
|
473
|
+
}
|
|
474
|
+
const diff = typeof rec.diff === 'string' ? rec.diff : '';
|
|
475
|
+
if (diff !== '')
|
|
476
|
+
lines.push('', clip(diff, 2000));
|
|
477
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
478
|
+
}
|
|
479
|
+
if (mode === 'apply') {
|
|
480
|
+
if (rec.unchanged === true)
|
|
481
|
+
return [{ type: 'text', text: '桥接未变更:管理块内容已一致,字节未写。' }];
|
|
482
|
+
return [{ type: 'text', text: '桥接已应用:' + rec.path + ',backupId=' + rec.backupId + '。' + (typeof rec.warning === 'string' ? '\n' + rec.warning : '') }];
|
|
483
|
+
}
|
|
484
|
+
if (mode === 'rollback') {
|
|
485
|
+
return [{ type: 'text', text: '桥接已回滚:' + rec.path + ',backupId=' + rec.backupId + '。' }];
|
|
486
|
+
}
|
|
390
487
|
const actionText = rec.action === 'created' ? '新建了' : rec.action === 'replaced' ? '更新了' : '追加到了';
|
|
391
|
-
|
|
488
|
+
const deprecation = asRecord(rec.deprecation);
|
|
489
|
+
const deprecationText = typeof deprecation.message === 'string' && deprecation.message !== '' ? '(' + deprecation.message + ')' : '';
|
|
490
|
+
return [{ type: 'text', text: '已把 ' + rec.lessonsCount + ' 条梦境教训桥接:' + actionText + ' ' + rec.path + '。' + deprecationText }];
|
|
392
491
|
},
|
|
393
492
|
},
|
|
394
|
-
async execute(rawArgs) {
|
|
493
|
+
async execute(rawArgs, exec) {
|
|
395
494
|
const args = asRecord(rawArgs);
|
|
396
|
-
const
|
|
495
|
+
const mode = optionalString(args, 'mode');
|
|
496
|
+
if (mode !== undefined && mode !== 'preview' && mode !== 'apply' && mode !== 'rollback') {
|
|
497
|
+
throw new Error('mode 必须是 preview / apply / rollback(缺省为 legacy 直写)。');
|
|
498
|
+
}
|
|
499
|
+
const explicitPath = optionalString(args, 'path');
|
|
500
|
+
const cwd = execCwdOf(exec);
|
|
501
|
+
let projectRoot;
|
|
502
|
+
if (explicitPath !== undefined) {
|
|
503
|
+
projectRoot = cwd ?? dirname(resolve(explicitPath));
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
if (cwd === undefined) {
|
|
507
|
+
throw new Error('未提供 path 且无法从执行上下文取得 cwd:无法定位项目内 AGENTS.md,请显式传 path。');
|
|
508
|
+
}
|
|
509
|
+
projectRoot = cwd;
|
|
510
|
+
}
|
|
511
|
+
const resolvedPath = resolvedBridgePath(projectRoot, explicitPath);
|
|
397
512
|
const maxRaw = args.maxLessons;
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
513
|
+
const legacyMax = typeof maxRaw === 'number' && Number.isInteger(maxRaw) ? Math.min(30, Math.max(1, maxRaw)) : 10;
|
|
514
|
+
if (mode === undefined) {
|
|
515
|
+
const legacyTarget = explicitPath !== undefined ? resolve(explicitPath) : join(projectRoot, 'AGENTS.md');
|
|
516
|
+
const legacyResult = bridgeDreams(cfg.journalDir, legacyTarget, legacyMax);
|
|
517
|
+
return {
|
|
518
|
+
ok: true,
|
|
519
|
+
path: legacyTarget,
|
|
520
|
+
...(resolvedPath !== undefined ? { resolvedPath } : {}),
|
|
521
|
+
...legacyResult,
|
|
522
|
+
deprecation: {
|
|
523
|
+
code: 'legacy-write',
|
|
524
|
+
message: '缺省 mode 为 legacy 直写(已弃用);建议改用 mode:"preview" → 复核 diff → mode:"apply" + expectedSha256(并带同一 previewId)。',
|
|
525
|
+
},
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
const maxLessons = typeof maxRaw === 'number' && Number.isInteger(maxRaw) ? maxRaw : undefined;
|
|
529
|
+
const lessonIds = stringArray(args, 'lessonIds');
|
|
530
|
+
const projectId = optionalString(args, 'projectId');
|
|
531
|
+
const store = knowledgeStoreOf(cfg);
|
|
532
|
+
const lessons = store.listLessons();
|
|
533
|
+
if (mode === 'rollback') {
|
|
534
|
+
const backupId = requiredString(args, 'backupId', 'backupId');
|
|
535
|
+
const result = rollbackBridge({
|
|
536
|
+
journalDir: cfg.journalDir,
|
|
537
|
+
projectRoot,
|
|
538
|
+
...(explicitPath !== undefined ? { path: explicitPath } : {}),
|
|
539
|
+
backupId,
|
|
540
|
+
});
|
|
541
|
+
if (result.ok === true)
|
|
542
|
+
return { mode, ...result, resolvedPath: result.path };
|
|
543
|
+
return { mode, ...result, ...(resolvedPath !== undefined ? { resolvedPath } : {}) };
|
|
544
|
+
}
|
|
545
|
+
// 工具层前置资格校验 + 合并 skipped(bridge 层会再兜一次)。
|
|
546
|
+
const preSelection = selectBridgeLessons(lessons, { projectId, workspaceRoot: projectRoot }, {
|
|
547
|
+
...(lessonIds.length > 0 ? { lessonIds } : {}),
|
|
548
|
+
...(maxLessons !== undefined ? { maxLessons } : {}),
|
|
549
|
+
});
|
|
550
|
+
const bridgeOptions = {
|
|
551
|
+
journalDir: cfg.journalDir,
|
|
552
|
+
projectRoot,
|
|
553
|
+
...(explicitPath !== undefined ? { path: explicitPath } : {}),
|
|
554
|
+
lessons,
|
|
555
|
+
...(projectId !== undefined ? { projectId } : {}),
|
|
556
|
+
workspaceRoot: projectRoot,
|
|
557
|
+
...(lessonIds.length > 0 ? { lessonIds } : {}),
|
|
558
|
+
...(maxLessons !== undefined ? { maxLessons } : {}),
|
|
559
|
+
maskSecrets: cfg.maskSecrets,
|
|
560
|
+
};
|
|
561
|
+
if (mode === 'preview') {
|
|
562
|
+
const result = previewBridge(bridgeOptions);
|
|
563
|
+
return {
|
|
564
|
+
mode,
|
|
565
|
+
...result,
|
|
566
|
+
...(resolvedPath !== undefined ? { resolvedPath } : {}),
|
|
567
|
+
skipped: mergeSkipped(result.ok === true ? result.skipped : [], preSelection.skipped),
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
if (!Object.hasOwn(args, 'expectedSha256') || (args.expectedSha256 !== null && (typeof args.expectedSha256 !== 'string' || args.expectedSha256.trim() === ''))) {
|
|
571
|
+
throw new Error('apply 必须提供 expectedSha256:原样回传 preview.expected.sha256(文件不存在时为 null)。');
|
|
572
|
+
}
|
|
573
|
+
const expectedSha256 = args.expectedSha256;
|
|
574
|
+
const previewId = optionalString(args, 'previewId');
|
|
575
|
+
const result = applyBridge({
|
|
576
|
+
...bridgeOptions,
|
|
577
|
+
expectedSha256,
|
|
578
|
+
...(previewId !== undefined ? { previewId } : {}),
|
|
579
|
+
});
|
|
580
|
+
if (result.ok === true) {
|
|
581
|
+
return {
|
|
582
|
+
mode,
|
|
583
|
+
...result,
|
|
584
|
+
resolvedPath: result.path,
|
|
585
|
+
skipped: mergeSkipped(result.skipped, preSelection.skipped),
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
return {
|
|
589
|
+
mode,
|
|
590
|
+
...result,
|
|
591
|
+
...(resolvedPath !== undefined ? { resolvedPath } : {}),
|
|
592
|
+
skipped: mergeSkipped([], preSelection.skipped),
|
|
593
|
+
};
|
|
401
594
|
},
|
|
402
|
-
timeoutMs:
|
|
595
|
+
timeoutMs: 30000,
|
|
403
596
|
};
|
|
404
597
|
const dreamHealth = {
|
|
405
598
|
name: 'dream_health',
|
|
@@ -434,7 +627,7 @@ export function buildDreamTools(config) {
|
|
|
434
627
|
};
|
|
435
628
|
const dreamLearn = {
|
|
436
629
|
name: 'dream_learn',
|
|
437
|
-
description: '
|
|
630
|
+
description: '学梦:把一条可复用经验连同证据提交为候选。写清 title(一句话结论)、action(可执行动作)、when(适用条件)与 exceptions(例外);会话证据需 sessionId+recordSeq 和可匹配的 quote(省略时用 summary),本地产物需当前工作目录内的 artifactPath。verification=read 必须通过插件实际核验;失败降为 claimed 并说明原因,不计独立支持。新经验均先留在 candidate,读到来源不代表用户采纳。',
|
|
438
631
|
parameters: compileParameters({
|
|
439
632
|
kind: { type: 'string', required: true, enum: ['preference', 'procedure', 'pitfall', 'fact'], description: '经验类别。' },
|
|
440
633
|
title: { type: 'string', required: true, description: '一句话结论;脱离 when 不得单独使用。' },
|
|
@@ -456,7 +649,7 @@ export function buildDreamTools(config) {
|
|
|
456
649
|
},
|
|
457
650
|
description: '适用范围(可选)。',
|
|
458
651
|
},
|
|
459
|
-
evidence: { type: 'array',
|
|
652
|
+
evidence: { type: 'array', items: evidenceItemsSpec(), description: '证据数组(可选,缺省为空数组;无证据时只能保存为 candidate)。' },
|
|
460
653
|
}),
|
|
461
654
|
output: {
|
|
462
655
|
schema: baseSchema,
|
|
@@ -471,7 +664,7 @@ export function buildDreamTools(config) {
|
|
|
471
664
|
return [{ type: 'text', text: lines.join('\n') }];
|
|
472
665
|
},
|
|
473
666
|
},
|
|
474
|
-
async execute(rawArgs) {
|
|
667
|
+
async execute(rawArgs, exec) {
|
|
475
668
|
const args = asRecord(rawArgs);
|
|
476
669
|
const kind = parseLessonKind(args);
|
|
477
670
|
const title = maskIfNeeded(cfg, requiredString(args, 'title', '经验结论'));
|
|
@@ -488,6 +681,7 @@ export function buildDreamTools(config) {
|
|
|
488
681
|
...(entry.platform !== undefined ? { platform: maskIfNeeded(cfg, entry.platform) } : {}),
|
|
489
682
|
}));
|
|
490
683
|
const evidenceInputs = parseEvidenceArgs(args, 'evidence', projectId)
|
|
684
|
+
.map((entry) => verifyEvidence(entry, { sessionsRoot: cfg.sessionsRoot, projectId, workspaceRoot, cwd: execCwdOf(exec) }))
|
|
491
685
|
.map((entry) => ({ ...entry, summary: maskIfNeeded(cfg, entry.summary) }));
|
|
492
686
|
const scope = {
|
|
493
687
|
global: isGlobal,
|
|
@@ -518,15 +712,16 @@ export function buildDreamTools(config) {
|
|
|
518
712
|
recordSeq: entry.recordSeq ?? null,
|
|
519
713
|
summary: entry.summary,
|
|
520
714
|
verification: entry.verification,
|
|
715
|
+
sourceHash: entry.sourceHash ?? null,
|
|
521
716
|
})),
|
|
522
717
|
}));
|
|
523
718
|
const store = knowledgeStoreOf(cfg);
|
|
524
|
-
const { lesson, created } = store.createLesson(lessonInput, idempotencyKey);
|
|
719
|
+
const { lesson, created } = store.createLesson(lessonInput, idempotencyKey, { requireReview: true });
|
|
525
720
|
const byId = new Map(store.listEvidence().map((item) => [item.id, item]));
|
|
526
721
|
const evidence = lesson.evidenceIds
|
|
527
722
|
.map((id) => byId.get(id))
|
|
528
723
|
.filter((item) => item !== undefined)
|
|
529
|
-
.map((item) => ({ id: item.id, verification: item.verification }));
|
|
724
|
+
.map((item) => ({ id: item.id, verification: item.verification, ...(item.verificationReason ? { verificationReason: item.verificationReason } : {}) }));
|
|
530
725
|
const notes = [];
|
|
531
726
|
if (evidenceInputs.length === 0) {
|
|
532
727
|
notes.push('无证据:仅保存为 candidate,不会作为已证实结论注入任务建议。');
|
|
@@ -536,6 +731,10 @@ export function buildDreamTools(config) {
|
|
|
536
731
|
const claimedCount = evidence.filter((item) => item.verification === 'claimed').length;
|
|
537
732
|
notes.push('证据:' + readCount + ' 条已读(计入独立支持),' + claimedCount + ' 条仅声明(不计入独立支持)。');
|
|
538
733
|
}
|
|
734
|
+
if (evidenceInputs.some((item) => item.verification === 'claimed' && item.verificationReason !== 'model-claimed'))
|
|
735
|
+
notes.push('部分来源未能核验,已保留为仅声明;请补充真实 sessionId/recordSeq、原文 quote 或当前工作区内 artifactPath。');
|
|
736
|
+
if (created)
|
|
737
|
+
notes.push('来源读取不等于结论成立或用户采纳;新经验仍需审阅。');
|
|
539
738
|
if (!created)
|
|
540
739
|
notes.push('已存在同指纹经验(' + lesson.id + '):复用既有记录并合并新证据,未新增经验。');
|
|
541
740
|
if (!isGlobal && projectId === undefined && workspaceRoot === undefined) {
|
|
@@ -559,14 +758,17 @@ export function buildDreamTools(config) {
|
|
|
559
758
|
};
|
|
560
759
|
const dreamContext = {
|
|
561
760
|
name: 'dream_context',
|
|
562
|
-
description: '取梦:按当前任务返回少量真正适用的经验(只读,默认最多 5 条 / 3000 字符)。只返回 global 或与 projectId/workspaceRoot 精确匹配的经验;未知项目只返回 global,绝不跨项目扫描。rejected/stale/disputed
|
|
761
|
+
description: '取梦:按当前任务返回少量真正适用的经验(只读,默认最多 5 条 / 3000 字符)。只返回 global 或与 projectId/workspaceRoot 精确匹配的经验;未知项目只返回 global,绝不跨项目扫描。rejected/stale/disputed 默认排除;候选默认不注入(candidate-hold),无独立证据且非 usable 不注入(no-evidence),版本条件与包名不符不注入(version-mismatch),skipped 均给原因。未审阅候选不得当作已证实结论;只有明确审阅候选时才传 includeCandidates=true。when/exceptions 完整保留,元数据可按预算截断,预算放不下即停。',
|
|
563
762
|
parameters: compileParameters({
|
|
564
763
|
query: { type: 'string', required: true, description: '当前任务描述或检索关键词。' },
|
|
565
764
|
projectId: { type: 'string', description: '当前项目 ID(可选;未知时只返回 global)。' },
|
|
566
765
|
workspaceRoot: { type: 'string', description: '当前工作区根目录(可选)。' },
|
|
567
766
|
limit: { type: 'integer', description: '条数上限 1-20(默认 5)。' },
|
|
568
767
|
maxChars: { type: 'integer', description: '字符预算 1-20000(默认 3000)。' },
|
|
569
|
-
packageVersion: { type: 'string', description: '
|
|
768
|
+
packageVersion: { type: 'string', description: '当前相关依赖版本(可选;需配合包名被识别才参与版本判定)。' },
|
|
769
|
+
packageName: { type: 'string', description: '当前相关依赖包名(可选;与 query 文本一起识别 applicability.package)。' },
|
|
770
|
+
platform: { type: 'string', description: '任务目标平台(windows / linux / macos 等);缺省从任务文本识别,否则使用宿主系统。平台不符的经验不注入。' },
|
|
771
|
+
includeCandidates: { type: 'boolean', description: '是否把候选经验纳入任务建议(默认 false;仅明确审阅候选时传 true,无证据候选仍不注入)。' },
|
|
570
772
|
}),
|
|
571
773
|
output: {
|
|
572
774
|
schema: baseSchema,
|
|
@@ -575,12 +777,26 @@ export function buildDreamTools(config) {
|
|
|
575
777
|
const items = Array.isArray(rec.items) ? rec.items : [];
|
|
576
778
|
const budget = asRecord(rec.budget);
|
|
577
779
|
if (items.length === 0) {
|
|
578
|
-
|
|
780
|
+
const skipped = Array.isArray(rec.skipped) ? rec.skipped : [];
|
|
781
|
+
const countReason = (reason) => skipped.filter((entry) => asRecord(entry).reason === reason).length;
|
|
782
|
+
const hints = [];
|
|
783
|
+
const held = countReason('candidate-hold');
|
|
784
|
+
const noEvidence = countReason('no-evidence');
|
|
785
|
+
const mismatched = countReason('version-mismatch');
|
|
786
|
+
if (held > 0)
|
|
787
|
+
hints.push(held + ' 条候选待审阅(要审阅候选时显式传 includeCandidates=true)');
|
|
788
|
+
if (noEvidence > 0)
|
|
789
|
+
hints.push(noEvidence + ' 条无独立证据不注入');
|
|
790
|
+
if (mismatched > 0)
|
|
791
|
+
hints.push(mismatched + ' 条版本条件不匹配不注入');
|
|
792
|
+
const suffix = hints.length > 0 ? ';' + hints.join(';') : '';
|
|
793
|
+
return [{ type: 'text', text: '没有可注入的适用经验(范围:' + rec.scopeLabel + suffix + ')。' }];
|
|
579
794
|
}
|
|
580
795
|
const lines = ['适用经验 ' + items.length + ' 条(范围:' + rec.scopeLabel + ',字符 ' + budget.usedChars + '/' + budget.maxChars + '):'];
|
|
581
796
|
for (const item of items) {
|
|
582
797
|
const lesson = asRecord(item);
|
|
583
|
-
|
|
798
|
+
const truncation = lesson.truncated === true ? '|元数据截断' : '';
|
|
799
|
+
lines.push('- [' + lesson.state + '|' + lesson.scopeLabel + '|rev ' + lesson.revision + truncation + '] ' + lesson.title);
|
|
584
800
|
lines.push(' 何时:' + lesson.when);
|
|
585
801
|
lines.push(' 行动:' + lesson.action);
|
|
586
802
|
const exceptions = Array.isArray(lesson.exceptions) ? lesson.exceptions : [];
|
|
@@ -598,17 +814,23 @@ export function buildDreamTools(config) {
|
|
|
598
814
|
const projectId = optionalString(args, 'projectId');
|
|
599
815
|
const workspaceRoot = optionalString(args, 'workspaceRoot');
|
|
600
816
|
const packageVersion = optionalString(args, 'packageVersion');
|
|
817
|
+
const packageName = optionalString(args, 'packageName');
|
|
601
818
|
const { limit, maxChars } = resolveRetrievalBudget({
|
|
602
819
|
limit: typeof args.limit === 'number' ? args.limit : undefined,
|
|
603
820
|
maxChars: typeof args.maxChars === 'number' ? args.maxChars : undefined,
|
|
604
821
|
});
|
|
605
822
|
const retrievalQuery = {
|
|
606
823
|
query,
|
|
824
|
+
platform: taskPlatform(query, optionalString(args, 'platform'), process.platform),
|
|
607
825
|
limit,
|
|
608
826
|
maxChars,
|
|
827
|
+
// R1:默认不注入候选;claimed-only / 无证据且非 usable 的条目也不注入(R1 附带规则)。
|
|
828
|
+
includeCandidates: args.includeCandidates === true,
|
|
829
|
+
includeNoEvidence: false,
|
|
609
830
|
...(projectId !== undefined ? { projectId } : {}),
|
|
610
831
|
...(workspaceRoot !== undefined ? { workspaceRoot } : {}),
|
|
611
832
|
...(packageVersion !== undefined ? { packageVersion } : {}),
|
|
833
|
+
...(packageName !== undefined ? { packageName } : {}),
|
|
612
834
|
};
|
|
613
835
|
const scopeLabel = queryScopeLabel(retrievalQuery);
|
|
614
836
|
const dir = knowledgeDirOf(cfg);
|
|
@@ -622,17 +844,20 @@ export function buildDreamTools(config) {
|
|
|
622
844
|
},
|
|
623
845
|
timeoutMs: 15000,
|
|
624
846
|
};
|
|
625
|
-
const REVIEW_ACTIONS = ['accept', 'reject', 'mark-stale', 'mark-disputed', 'resolve-conflict', 'attach-evidence'];
|
|
847
|
+
const REVIEW_ACTIONS = ['accept', 'reject', 'reopen', 'mark-stale', 'mark-disputed', 'resolve-conflict', 'attach-evidence'];
|
|
626
848
|
const dreamReview = {
|
|
627
849
|
name: 'dream_review',
|
|
628
850
|
description: '审梦:采纳/驳回/标记冲突或过期/追加验证证据,必须带 expectedRevision(revision 不符会拒绝且不写入)。模型调用 actor 一律记为 model,不得冒充用户采纳;attach-evidence 只追加证据,不自动改变 state。',
|
|
629
851
|
parameters: compileParameters({
|
|
630
852
|
action: { type: 'string', required: true, enum: [...REVIEW_ACTIONS], description: '审阅动作。' },
|
|
631
|
-
lessonId: { type: 'string',
|
|
853
|
+
lessonId: { type: 'string', description: '经验 ID(accept/reject/mark-*/attach-evidence 必填;resolve-conflict 可用 targetId 替代)。' },
|
|
854
|
+
targetId: { type: 'string', description: 'resolve-conflict:目标经验 ID(优先于 lessonId)。' },
|
|
855
|
+
affectedIds: { type: 'array', items: { type: 'string' }, description: 'resolve-conflict:受影响/对方经验 ID(缺省则用 conflictWith)。' },
|
|
632
856
|
expectedRevision: { type: 'integer', required: true, description: '期望 revision(乐观锁,必须与当前一致)。' },
|
|
633
857
|
evidence: { type: 'array', items: evidenceItemsSpec(), description: 'attach-evidence 时使用的证据数组。' },
|
|
634
|
-
conflictWith: { type: 'array', items: { type: 'string' }, description: 'mark-disputed
|
|
635
|
-
|
|
858
|
+
conflictWith: { type: 'array', items: { type: 'string' }, description: 'mark-disputed:冲突对方 id 列表(双方互相登记且全部置 disputed);resolve-conflict:要一起解析的对方 id(必须与之互为 conflictIds)。' },
|
|
859
|
+
resolution: { type: 'string', enum: ['prefer', 'drop', 'merge'], description: 'resolve-conflict 必填:prefer=以 lessonId 为准;drop=放弃 lessonId;merge=并入 lessonId(对方 stale)。' },
|
|
860
|
+
note: { type: 'string', description: 'resolve-conflict 必填的解析依据;mark-disputed 可选。' },
|
|
636
861
|
}),
|
|
637
862
|
output: {
|
|
638
863
|
schema: baseSchema,
|
|
@@ -644,13 +869,18 @@ export function buildDreamTools(config) {
|
|
|
644
869
|
}];
|
|
645
870
|
},
|
|
646
871
|
},
|
|
647
|
-
async execute(rawArgs) {
|
|
872
|
+
async execute(rawArgs, exec) {
|
|
648
873
|
const args = asRecord(rawArgs);
|
|
649
874
|
const action = optionalString(args, 'action');
|
|
650
875
|
if (action === undefined || !REVIEW_ACTIONS.includes(action)) {
|
|
651
876
|
throw new Error('action 必须是 ' + REVIEW_ACTIONS.join(' / ') + '。');
|
|
652
877
|
}
|
|
653
|
-
const
|
|
878
|
+
const targetId = optionalString(args, 'targetId');
|
|
879
|
+
const lessonIdArg = optionalString(args, 'lessonId');
|
|
880
|
+
const lessonId = targetId ?? lessonIdArg;
|
|
881
|
+
if (lessonId === undefined) {
|
|
882
|
+
throw new KnowledgeError('invalid', '缺少 lessonId(resolve-conflict 可用 targetId)。');
|
|
883
|
+
}
|
|
654
884
|
const revisionRaw = args.expectedRevision;
|
|
655
885
|
if (typeof revisionRaw !== 'number' || !Number.isInteger(revisionRaw) || revisionRaw < 1) {
|
|
656
886
|
throw new Error('expectedRevision 必须是 >= 1 的整数。');
|
|
@@ -661,12 +891,24 @@ export function buildDreamTools(config) {
|
|
|
661
891
|
if (before === undefined)
|
|
662
892
|
throw new KnowledgeError('invalid', '经验不存在:' + lessonId);
|
|
663
893
|
if (before.revision !== expectedRevision) {
|
|
664
|
-
|
|
894
|
+
const error = new KnowledgeError('revision', '经验 ' + lessonId + ' 期望 revision ' + expectedRevision + ',实际 ' + before.revision);
|
|
895
|
+
error.currentRevision = before.revision;
|
|
896
|
+
error.details = { currentRevision: before.revision };
|
|
897
|
+
throw error;
|
|
665
898
|
}
|
|
666
|
-
const
|
|
899
|
+
const affectedIdsArg = stringArray(args, 'affectedIds');
|
|
900
|
+
const conflictWith = affectedIdsArg.length > 0 ? affectedIdsArg : stringArray(args, 'conflictWith');
|
|
667
901
|
const note = optionalString(args, 'note');
|
|
902
|
+
const maskedNote = note === undefined ? undefined : maskIfNeeded(cfg, note);
|
|
903
|
+
const resolutionArg = optionalString(args, 'resolution');
|
|
904
|
+
const evidenceInputs = action === 'attach-evidence'
|
|
905
|
+
? parseEvidenceArgs(args, 'evidence', before.scope.projectId).map(entry => verifyEvidence(entry, {
|
|
906
|
+
sessionsRoot: cfg.sessionsRoot, projectId: before.scope.projectId, workspaceRoot: before.scope.workspaceRoot, cwd: execCwdOf(exec),
|
|
907
|
+
})).map(entry => ({ ...entry, summary: maskIfNeeded(cfg, entry.summary) }))
|
|
908
|
+
: [];
|
|
668
909
|
const idempotencyKey = 'dream_review:' + sha256Short(JSON.stringify({
|
|
669
|
-
action, lessonId, expectedRevision, conflictWith,
|
|
910
|
+
action, lessonId, expectedRevision, conflictWith, resolution: resolutionArg ?? null, note: maskedNote ?? null,
|
|
911
|
+
evidence: evidenceInputs,
|
|
670
912
|
}));
|
|
671
913
|
// 模型调用永远记为 model;入参里的 actor 一律忽略(用户采纳只能来自宿主授权的真实用户指令)。
|
|
672
914
|
const actor = 'model';
|
|
@@ -678,50 +920,154 @@ export function buildDreamTools(config) {
|
|
|
678
920
|
case 'reject':
|
|
679
921
|
lesson = store.reviewLesson(lessonId, 'rejected', expectedRevision, idempotencyKey, actor);
|
|
680
922
|
break;
|
|
923
|
+
case 'reopen':
|
|
924
|
+
if (before.state !== 'rejected')
|
|
925
|
+
throw new KnowledgeError('invalid', 'reopen 只用于把已驳回经验重新送审。');
|
|
926
|
+
lesson = store.reviewLesson(lessonId, 'unreviewed', expectedRevision, idempotencyKey, actor);
|
|
927
|
+
break;
|
|
681
928
|
case 'mark-stale':
|
|
682
929
|
lesson = store.applyTransition(lessonId, 'stale', expectedRevision, idempotencyKey);
|
|
683
930
|
break;
|
|
931
|
+
case 'attach-evidence':
|
|
932
|
+
lesson = store.attachEvidence(lessonId, evidenceInputs, expectedRevision, idempotencyKey);
|
|
933
|
+
break;
|
|
684
934
|
case 'mark-disputed': {
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
935
|
+
// M2:primary 与所有 conflictWith 一律置 disputed,双方 conflictIds 互相登记(去重、可多对多)。
|
|
936
|
+
const members = requireConflictGroup(store, lessonId, before, conflictWith);
|
|
937
|
+
const group = members.map((member) => member.id);
|
|
938
|
+
const updates = [];
|
|
939
|
+
for (const member of members) {
|
|
940
|
+
const others = group.filter((id) => id !== member.id);
|
|
941
|
+
const conflictIds = [...member.lesson.conflictIds];
|
|
942
|
+
for (const id of others) {
|
|
691
943
|
if (!conflictIds.includes(id))
|
|
692
944
|
conflictIds.push(id);
|
|
693
945
|
}
|
|
694
|
-
|
|
946
|
+
const noteSatisfied = maskedNote === undefined || member.lesson.review.note === maskedNote;
|
|
947
|
+
const alreadyDisputed = member.lesson.state === 'disputed'
|
|
948
|
+
&& conflictIds.length === member.lesson.conflictIds.length
|
|
949
|
+
&& conflictIds.every((id) => member.lesson.conflictIds.includes(id))
|
|
950
|
+
&& noteSatisfied;
|
|
951
|
+
if (alreadyDisputed)
|
|
952
|
+
continue;
|
|
953
|
+
const patch = { state: 'disputed', conflictIds };
|
|
954
|
+
if (maskedNote !== undefined)
|
|
955
|
+
patch.review = { ...member.lesson.review, note: maskedNote };
|
|
956
|
+
updates.push({ id: member.id, patch, expectedRevision: member.lesson.revision });
|
|
695
957
|
}
|
|
958
|
+
lesson = updates.length === 0 ? before : store.updateLessonsBatch(updates, idempotencyKey).find(item => item.id === lessonId) ?? before;
|
|
696
959
|
break;
|
|
697
960
|
}
|
|
698
961
|
case 'resolve-conflict': {
|
|
699
|
-
|
|
700
|
-
|
|
962
|
+
const resolution = optionalString(args, 'resolution');
|
|
963
|
+
if (resolution !== 'prefer' && resolution !== 'drop' && resolution !== 'merge') {
|
|
964
|
+
throw new KnowledgeError('invalid', 'resolution 必须是 prefer / drop / merge。');
|
|
701
965
|
}
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
lesson = store.updateLesson(lessonId, { state: 'usable', conflictIds }, expectedRevision, idempotencyKey);
|
|
966
|
+
if (maskedNote === undefined) {
|
|
967
|
+
throw new KnowledgeError('invalid', 'resolve-conflict 必须带 note(解析依据),以便事件可回溯。');
|
|
705
968
|
}
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
const
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
const
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
969
|
+
if (conflictWith.length === 0) {
|
|
970
|
+
throw new KnowledgeError('invalid', 'resolve-conflict 必须带至少一个 affectedIds/conflictWith 目标 id。');
|
|
971
|
+
}
|
|
972
|
+
const members = requireConflictGroup(store, lessonId, before, conflictWith);
|
|
973
|
+
const group = members.map((member) => member.id);
|
|
974
|
+
// 校验:所有当事方必须 disputed,且互相登记 conflictIds;否则零写入。
|
|
975
|
+
for (const member of members) {
|
|
976
|
+
if (member.lesson.state !== 'disputed') {
|
|
977
|
+
throw new KnowledgeError('invalid', '冲突解析要求 ' + member.id + ' 处于 disputed(实际 ' + member.lesson.state + ')');
|
|
978
|
+
}
|
|
979
|
+
for (const other of group) {
|
|
980
|
+
if (other !== member.id && !member.lesson.conflictIds.includes(other)) {
|
|
981
|
+
throw new KnowledgeError('invalid', '冲突未双向登记:' + member.id + ' 的 conflictIds 缺少 ' + other);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
718
984
|
}
|
|
719
|
-
|
|
720
|
-
|
|
985
|
+
const now = new Date().toISOString();
|
|
986
|
+
const affectedIds = group.filter((id) => id !== lessonId);
|
|
987
|
+
const resolutionPayload = { kind: resolution, targetId: lessonId, affectedIds };
|
|
988
|
+
const remainingOf = (member) => member.lesson.conflictIds.filter((id) => !group.includes(id));
|
|
989
|
+
const planned = [];
|
|
990
|
+
if (resolution === 'prefer' || resolution === 'drop') {
|
|
991
|
+
for (const member of members) {
|
|
992
|
+
const isPrimary = member.id === lessonId;
|
|
993
|
+
const patch = { conflictIds: remainingOf(member) };
|
|
994
|
+
if (resolution === 'prefer') {
|
|
995
|
+
if (isPrimary) {
|
|
996
|
+
if (remainingOf(member).length === 0)
|
|
997
|
+
patch.state = 'usable';
|
|
998
|
+
patch.review = { ...member.lesson.review, at: now, note: maskedNote, resolution: resolutionPayload };
|
|
999
|
+
}
|
|
1000
|
+
else {
|
|
1001
|
+
patch.state = 'rejected';
|
|
1002
|
+
patch.review = { decision: 'rejected', actor: 'model', at: now, note: '冲突解析:败给 ' + lessonId, resolution: resolutionPayload };
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
else if (isPrimary) {
|
|
1006
|
+
patch.state = 'rejected';
|
|
1007
|
+
patch.review = { decision: 'rejected', actor: 'model', at: now, note: '冲突解析:放弃本条目', resolution: resolutionPayload };
|
|
1008
|
+
}
|
|
1009
|
+
else {
|
|
1010
|
+
if (remainingOf(member).length === 0)
|
|
1011
|
+
patch.state = 'usable';
|
|
1012
|
+
patch.review = { ...member.lesson.review, at: now, note: maskedNote, resolution: resolutionPayload };
|
|
1013
|
+
}
|
|
1014
|
+
planned.push({ id: member.id, isPrimary, patch });
|
|
1015
|
+
}
|
|
721
1016
|
}
|
|
722
1017
|
else {
|
|
723
|
-
|
|
1018
|
+
const primaryMember = members.find((member) => member.id === lessonId);
|
|
1019
|
+
const mergedEvidence = [...(primaryMember?.lesson.evidenceIds ?? [])];
|
|
1020
|
+
for (const member of members) {
|
|
1021
|
+
if (member.id === lessonId)
|
|
1022
|
+
continue;
|
|
1023
|
+
for (const id of member.lesson.evidenceIds) {
|
|
1024
|
+
if (!mergedEvidence.includes(id))
|
|
1025
|
+
mergedEvidence.push(id);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
for (const member of members) {
|
|
1029
|
+
const isPrimary = member.id === lessonId;
|
|
1030
|
+
const patch = { conflictIds: remainingOf(member) };
|
|
1031
|
+
if (isPrimary) {
|
|
1032
|
+
const supersedes = [...(member.lesson.supersedes ?? [])];
|
|
1033
|
+
for (const id of group) {
|
|
1034
|
+
if (id !== member.id && !supersedes.includes(id))
|
|
1035
|
+
supersedes.push(id);
|
|
1036
|
+
}
|
|
1037
|
+
if (remainingOf(member).length === 0)
|
|
1038
|
+
patch.state = 'usable';
|
|
1039
|
+
patch.supersedes = supersedes;
|
|
1040
|
+
patch.evidenceIds = mergedEvidence;
|
|
1041
|
+
patch.review = { ...member.lesson.review, at: now, note: '冲突解析:并入 ' + affectedIds.join('、'), resolution: resolutionPayload };
|
|
1042
|
+
}
|
|
1043
|
+
else {
|
|
1044
|
+
patch.state = 'stale';
|
|
1045
|
+
patch.review = { ...member.lesson.review, at: now, note: maskedNote, resolution: resolutionPayload };
|
|
1046
|
+
}
|
|
1047
|
+
planned.push({ id: member.id, isPrimary, patch });
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
const updates = [];
|
|
1051
|
+
for (const item of planned) {
|
|
1052
|
+
const member = members.find((entry) => entry.id === item.id);
|
|
1053
|
+
if (member === undefined)
|
|
1054
|
+
continue;
|
|
1055
|
+
const current = member.lesson;
|
|
1056
|
+
const desiredConflictIds = item.patch.conflictIds ?? [];
|
|
1057
|
+
const reviewPatch = item.patch.review;
|
|
1058
|
+
const evidencePatch = item.patch.evidenceIds;
|
|
1059
|
+
const desiredState = item.patch.state;
|
|
1060
|
+
const noOp = desiredState === current.state
|
|
1061
|
+
&& desiredConflictIds.length === current.conflictIds.length
|
|
1062
|
+
&& desiredConflictIds.every((id) => current.conflictIds.includes(id))
|
|
1063
|
+
&& (reviewPatch === undefined || (current.review.decision === reviewPatch.decision && current.review.note === reviewPatch.note))
|
|
1064
|
+
&& (evidencePatch === undefined
|
|
1065
|
+
|| (evidencePatch.length === current.evidenceIds.length && evidencePatch.every((id) => current.evidenceIds.includes(id))));
|
|
1066
|
+
if (noOp)
|
|
1067
|
+
continue;
|
|
1068
|
+
updates.push({ id: item.id, patch: item.patch, expectedRevision: current.revision });
|
|
724
1069
|
}
|
|
1070
|
+
lesson = updates.length === 0 ? before : store.updateLessonsBatch(updates, idempotencyKey).find(item => item.id === lessonId) ?? before;
|
|
725
1071
|
break;
|
|
726
1072
|
}
|
|
727
1073
|
default:
|