@stardustlc/dsh-dream 0.5.1 → 0.7.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 +39 -14
- package/README.md +43 -18
- package/lib/bridge.d.ts +205 -2
- package/lib/bridge.js +1053 -6
- package/lib/client.js +634 -27
- package/lib/evidence.d.ts +15 -0
- package/lib/evidence.js +123 -0
- package/lib/index.js +3 -1
- package/lib/knowledge-store.d.ts +23 -5
- package/lib/knowledge-store.js +311 -41
- package/lib/knowledge.d.ts +40 -0
- package/lib/knowledge.js +54 -0
- package/lib/mask.d.ts +3 -1
- package/lib/mask.js +8 -2
- package/lib/retrieval.d.ts +12 -0
- package/lib/retrieval.js +68 -3
- package/lib/tools.d.ts +2 -0
- package/lib/tools.js +436 -128
- package/lib/web-actions.d.ts +11 -0
- package/lib/web-actions.js +175 -0
- package/lib/web.d.ts +50 -1
- package/lib/web.js +153 -13
- package/package.json +2 -2
- package/skills/dream-protocol/SKILL.md +9 -4
package/lib/knowledge-store.js
CHANGED
|
@@ -214,6 +214,7 @@ function acquireLock(lockPath, options) {
|
|
|
214
214
|
sleepSync(25);
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
|
+
const KNOWN_EVENT_KINDS = new Set(['lesson.create', 'lesson.update', 'lesson.review', 'lesson.batch', 'evidence.add']);
|
|
217
218
|
/** 事件行形状检查(字段值再做 schema 校验)。 */
|
|
218
219
|
function isEventShape(value) {
|
|
219
220
|
if (!isRecord(value) || value.schemaVersion !== 1)
|
|
@@ -234,6 +235,16 @@ function isEventShape(value) {
|
|
|
234
235
|
typeof value.revision === 'number' &&
|
|
235
236
|
Number.isInteger(value.revision) &&
|
|
236
237
|
value.payload !== undefined);
|
|
238
|
+
case 'lesson.batch': {
|
|
239
|
+
if (!isRecord(value.payload) || !Array.isArray(value.payload.updates))
|
|
240
|
+
return false;
|
|
241
|
+
return value.payload.updates.every((item) => isRecord(item) &&
|
|
242
|
+
typeof item.lessonId === 'string' &&
|
|
243
|
+
item.lessonId !== '' &&
|
|
244
|
+
typeof item.revision === 'number' &&
|
|
245
|
+
Number.isInteger(item.revision) &&
|
|
246
|
+
isRecord(item.patch));
|
|
247
|
+
}
|
|
237
248
|
case 'evidence.add':
|
|
238
249
|
return value.payload !== undefined;
|
|
239
250
|
default:
|
|
@@ -268,6 +279,7 @@ function createEmptyReplayState() {
|
|
|
268
279
|
badLines: 0,
|
|
269
280
|
orphanEvents: 0,
|
|
270
281
|
unsupportedVersions: 0,
|
|
282
|
+
skippedEvents: 0,
|
|
271
283
|
replayMode: 'full',
|
|
272
284
|
idempotencyComplete: true,
|
|
273
285
|
entitiesFromSnapshot: false,
|
|
@@ -346,9 +358,9 @@ export class KnowledgeStore {
|
|
|
346
358
|
/**
|
|
347
359
|
* 创建候选经验。同 idempotencyKey + 同 requestHash 幂等;同指纹(文本 + 范围相同)确定性合并,
|
|
348
360
|
* 只把新证据并入已有经验,不新增经验。
|
|
349
|
-
*
|
|
361
|
+
* 经调用方核验的 read 来源可用于项目经验;来源读取不代表用户采纳,review 始终 unreviewed。
|
|
350
362
|
*/
|
|
351
|
-
createLesson(input, idempotencyKey) {
|
|
363
|
+
createLesson(input, idempotencyKey, options = {}) {
|
|
352
364
|
assertIdempotencyKey(idempotencyKey);
|
|
353
365
|
if (!isRecord(input))
|
|
354
366
|
throw new KnowledgeError('invalid', '经验输入必须是对象');
|
|
@@ -358,6 +370,7 @@ export class KnowledgeStore {
|
|
|
358
370
|
const evidenceInputs = input.evidence === undefined ? [] : input.evidence;
|
|
359
371
|
const requestHash = this.hashRequest({
|
|
360
372
|
operation: 'lesson.create',
|
|
373
|
+
...(options.requireReview === true ? { requireReview: true } : {}),
|
|
361
374
|
kind: input.kind === undefined ? null : input.kind,
|
|
362
375
|
title: input.title === undefined ? null : input.title,
|
|
363
376
|
action: input.action === undefined ? null : input.action,
|
|
@@ -424,27 +437,20 @@ export class KnowledgeStore {
|
|
|
424
437
|
.map((id) => state.evidence.get(id))
|
|
425
438
|
.filter((item) => item !== undefined);
|
|
426
439
|
const hasRead = mergedEvidence.some((item) => item.verification === 'read');
|
|
427
|
-
const hasUserCorrectionRead = mergedEvidence.some((item) => item.kind === 'user-correction' && item.verification === 'read');
|
|
428
440
|
const patch = {};
|
|
429
441
|
if (newIds.length > 0) {
|
|
430
442
|
patch.evidenceIds = mergedIds;
|
|
431
|
-
patch.independentSupportCount = mergeEvidenceSupport({ evidenceIds: mergedIds }, Array.from(state.evidence.values()));
|
|
432
443
|
}
|
|
444
|
+
const supportCount = mergeEvidenceSupport({ evidenceIds: mergedIds }, Array.from(state.evidence.values()));
|
|
445
|
+
if (supportCount !== existing.independentSupportCount)
|
|
446
|
+
patch.independentSupportCount = supportCount;
|
|
433
447
|
// R2′:候选经验遇到 read 证据可升为 usable;disputed/stale/rejected 状态保持不变。
|
|
434
|
-
if (existing.state === 'candidate' && hasRead) {
|
|
448
|
+
if (existing.state === 'candidate' && hasRead && !options.requireReview) {
|
|
435
449
|
patch.state = 'usable';
|
|
436
|
-
if (
|
|
437
|
-
patch.review = { decision: 'accepted', actor: 'user', at: now };
|
|
438
|
-
}
|
|
439
|
-
else if (existing.review.decision !== 'unreviewed') {
|
|
450
|
+
if (existing.review.decision !== 'unreviewed') {
|
|
440
451
|
patch.review = { decision: 'unreviewed' };
|
|
441
452
|
}
|
|
442
453
|
}
|
|
443
|
-
else if (existing.state === 'usable' &&
|
|
444
|
-
hasUserCorrectionRead &&
|
|
445
|
-
existing.review.decision !== 'accepted') {
|
|
446
|
-
patch.review = { decision: 'accepted', actor: 'user', at: now };
|
|
447
|
-
}
|
|
448
454
|
if (Object.keys(patch).length === 0)
|
|
449
455
|
return { lesson: clone(existing), created: false };
|
|
450
456
|
const merged = this.appendLessonUpdate(state, existing, patch, idempotencyKey, requestHash, touch);
|
|
@@ -455,11 +461,8 @@ export class KnowledgeStore {
|
|
|
455
461
|
.map((id) => state.evidence.get(id))
|
|
456
462
|
.filter((item) => item !== undefined);
|
|
457
463
|
const hasRead = allEvidence.some((item) => item.verification === 'read');
|
|
458
|
-
const
|
|
459
|
-
const
|
|
460
|
-
const initialReview = hasUserCorrectionRead
|
|
461
|
-
? { decision: 'accepted', actor: 'user', at: now }
|
|
462
|
-
: { decision: 'unreviewed' };
|
|
464
|
+
const initialState = hasRead && !options.requireReview ? 'usable' : 'candidate';
|
|
465
|
+
const initialReview = { decision: 'unreviewed' };
|
|
463
466
|
const lesson = validateLesson({
|
|
464
467
|
schemaVersion: 1,
|
|
465
468
|
id: 'lsn_' + randomUUID(),
|
|
@@ -499,6 +502,41 @@ export class KnowledgeStore {
|
|
|
499
502
|
return { lesson: clone(stored), created: true };
|
|
500
503
|
});
|
|
501
504
|
}
|
|
505
|
+
/** 追加证据与更新引用共用一把锁;先检查 revision 与全部输入,不改变审阅状态。 */
|
|
506
|
+
attachEvidence(id, inputs, expectedRevision, idempotencyKey) {
|
|
507
|
+
assertIdempotencyKey(idempotencyKey);
|
|
508
|
+
if (!Array.isArray(inputs) || inputs.length === 0)
|
|
509
|
+
throw new KnowledgeError('invalid', 'attach-evidence 需要非空 evidence 数组');
|
|
510
|
+
const requestHash = this.hashRequest({ operation: 'lesson.attach-evidence', id, inputs });
|
|
511
|
+
return this.withLock((touch) => {
|
|
512
|
+
const state = this.loadCompleteState(touch);
|
|
513
|
+
const cached = state.idempotency.get(idempotencyKey);
|
|
514
|
+
if (cached) {
|
|
515
|
+
this.assertSameRequest(cached, requestHash, idempotencyKey);
|
|
516
|
+
const lesson = this.resolveIdempotentLesson(state, idempotencyKey);
|
|
517
|
+
if (!lesson)
|
|
518
|
+
throw new KnowledgeError('io', '无法恢复追加证据结果');
|
|
519
|
+
return lesson;
|
|
520
|
+
}
|
|
521
|
+
const previous = state.lessons.get(id);
|
|
522
|
+
if (!previous)
|
|
523
|
+
throw new KnowledgeError('invalid', '经验不存在:' + id);
|
|
524
|
+
if (previous.revision !== expectedRevision)
|
|
525
|
+
throw new KnowledgeError('revision', '经验 revision 已变化', { currentRevision: previous.revision });
|
|
526
|
+
for (const item of inputs)
|
|
527
|
+
validateEvidence({ ...item, id: 'evd_probe', schemaVersion: 1, observedAt: item.observedAt ?? new Date().toISOString() });
|
|
528
|
+
const ids = [...previous.evidenceIds];
|
|
529
|
+
for (const item of inputs) {
|
|
530
|
+
const evidence = this.persistEvidenceLocked(item, state);
|
|
531
|
+
if (!ids.includes(evidence.id))
|
|
532
|
+
ids.push(evidence.id);
|
|
533
|
+
}
|
|
534
|
+
const count = mergeEvidenceSupport({ evidenceIds: ids }, [...state.evidence.values()]);
|
|
535
|
+
if (ids.length === previous.evidenceIds.length && count === previous.independentSupportCount)
|
|
536
|
+
return clone(previous);
|
|
537
|
+
return clone(this.appendLessonUpdate(state, previous, { evidenceIds: ids, independentSupportCount: count }, idempotencyKey, requestHash, touch));
|
|
538
|
+
});
|
|
539
|
+
}
|
|
502
540
|
/** 按 patch 更新经验;revision 不符抛 KnowledgeError('revision')(details 带 currentRevision)且不写。 */
|
|
503
541
|
updateLesson(id, patch, expectedRevision, idempotencyKey) {
|
|
504
542
|
assertIdempotencyKey(idempotencyKey);
|
|
@@ -535,13 +573,56 @@ export class KnowledgeStore {
|
|
|
535
573
|
return clone(this.appendLessonUpdate(state, lesson, clean, idempotencyKey, requestHash, touch));
|
|
536
574
|
});
|
|
537
575
|
}
|
|
576
|
+
/** 记录一次使用反馈,不改变审阅、状态、证据和最后核验时间。 */
|
|
577
|
+
recordFeedback(id, vote, note, expectedRevision, idempotencyKey) {
|
|
578
|
+
assertIdempotencyKey(idempotencyKey);
|
|
579
|
+
if (vote !== 'useful' && vote !== 'not-applicable')
|
|
580
|
+
throw new KnowledgeError('invalid', '反馈必须是 useful / not-applicable');
|
|
581
|
+
if (!Number.isInteger(expectedRevision) || expectedRevision < 1)
|
|
582
|
+
throw new KnowledgeError('invalid', '反馈必须带有效的经验版本');
|
|
583
|
+
if (note !== undefined && (typeof note !== 'string' || note.length > 500))
|
|
584
|
+
throw new KnowledgeError('invalid', '反馈备注最多 500 字符');
|
|
585
|
+
const cleanNote = note === undefined || note.trim() === '' ? undefined : this.mask ? maskSecrets(note.trim()) : note.trim();
|
|
586
|
+
const requestHash = this.hashRequest({ operation: 'lesson.feedback', id, vote, note: cleanNote ?? null, expectedRevision });
|
|
587
|
+
return this.withLock((touch) => {
|
|
588
|
+
const state = this.loadCompleteState(touch);
|
|
589
|
+
const cached = state.idempotency.get(idempotencyKey);
|
|
590
|
+
if (cached !== undefined) {
|
|
591
|
+
this.assertSameRequest(cached, requestHash, idempotencyKey);
|
|
592
|
+
const result = this.resolveIdempotentLesson(state, idempotencyKey);
|
|
593
|
+
if (result === undefined)
|
|
594
|
+
throw new KnowledgeError('io', '无法恢复反馈结果');
|
|
595
|
+
return result;
|
|
596
|
+
}
|
|
597
|
+
const previous = state.lessons.get(id);
|
|
598
|
+
if (previous === undefined)
|
|
599
|
+
throw new KnowledgeError('invalid', '经验不存在:' + id);
|
|
600
|
+
if (previous.revision !== expectedRevision)
|
|
601
|
+
throw new KnowledgeError('revision', '经验已更新,请刷新后再反馈', { currentRevision: previous.revision });
|
|
602
|
+
const old = previous.feedback ?? { useful: 0, notApplicable: 0, recent: [] };
|
|
603
|
+
const feedback = {
|
|
604
|
+
useful: old.useful + (vote === 'useful' ? 1 : 0),
|
|
605
|
+
notApplicable: old.notApplicable + (vote === 'not-applicable' ? 1 : 0),
|
|
606
|
+
recent: [...old.recent, { vote, at: new Date().toISOString(), ...(cleanNote === undefined ? {} : { note: cleanNote }) }].slice(-20),
|
|
607
|
+
};
|
|
608
|
+
return clone(this.appendLessonUpdate(state, previous, { feedback }, idempotencyKey, requestHash, touch));
|
|
609
|
+
});
|
|
610
|
+
}
|
|
538
611
|
/** 审阅经验:accepted → usable,rejected → rejected,unreviewed → candidate。 */
|
|
539
|
-
reviewLesson(id, decision, expectedRevision, idempotencyKey, actor) {
|
|
612
|
+
reviewLesson(id, decision, expectedRevision, idempotencyKey, actor, note, resolution) {
|
|
540
613
|
assertIdempotencyKey(idempotencyKey);
|
|
541
614
|
if (!isEnum(decision, REVIEW_DECISIONS))
|
|
542
615
|
throw new KnowledgeError('invalid', 'review decision 取值不合法');
|
|
543
616
|
const maskedActor = actor === undefined ? undefined : this.mask ? maskSecrets(actor) : actor;
|
|
544
|
-
const
|
|
617
|
+
const maskedNote = note === undefined ? undefined : this.mask ? maskSecrets(note) : note;
|
|
618
|
+
const requestHash = this.hashRequest({
|
|
619
|
+
operation: 'lesson.review',
|
|
620
|
+
lessonId: id,
|
|
621
|
+
decision,
|
|
622
|
+
actor: maskedActor === undefined ? null : maskedActor,
|
|
623
|
+
note: maskedNote === undefined ? null : maskedNote,
|
|
624
|
+
resolution: resolution === undefined ? null : resolution,
|
|
625
|
+
});
|
|
545
626
|
return this.withLock((touch) => {
|
|
546
627
|
const state = this.loadCompleteState(touch);
|
|
547
628
|
const cached = state.idempotency.get(idempotencyKey);
|
|
@@ -567,18 +648,27 @@ export class KnowledgeStore {
|
|
|
567
648
|
throw new KnowledgeError('invalid', '不允许的审阅迁移:' + lesson.state + ' → ' + nextState, { lessonId: id });
|
|
568
649
|
}
|
|
569
650
|
const now = new Date().toISOString();
|
|
651
|
+
const nextReview = { decision, at: now };
|
|
652
|
+
if (maskedActor !== undefined)
|
|
653
|
+
nextReview.actor = maskedActor;
|
|
654
|
+
if (maskedNote !== undefined)
|
|
655
|
+
nextReview.note = maskedNote;
|
|
656
|
+
if (resolution !== undefined)
|
|
657
|
+
nextReview.resolution = resolution;
|
|
570
658
|
const merged = validateLesson({
|
|
571
659
|
...lesson,
|
|
572
660
|
state: nextState,
|
|
573
|
-
review:
|
|
574
|
-
? { decision, at: now }
|
|
575
|
-
: { decision, actor: maskedActor, at: now },
|
|
661
|
+
review: nextReview,
|
|
576
662
|
revision: lesson.revision + 1,
|
|
577
663
|
updatedAt: now,
|
|
578
664
|
});
|
|
579
665
|
const payload = { decision, state: nextState };
|
|
580
666
|
if (maskedActor !== undefined)
|
|
581
667
|
payload.actor = maskedActor;
|
|
668
|
+
if (maskedNote !== undefined)
|
|
669
|
+
payload.note = maskedNote;
|
|
670
|
+
if (resolution !== undefined)
|
|
671
|
+
payload.resolution = resolution;
|
|
582
672
|
const event = {
|
|
583
673
|
schemaVersion: 1,
|
|
584
674
|
id: 'evt_' + randomUUID(),
|
|
@@ -599,6 +689,97 @@ export class KnowledgeStore {
|
|
|
599
689
|
return clone(merged);
|
|
600
690
|
});
|
|
601
691
|
}
|
|
692
|
+
/**
|
|
693
|
+
* 批量原子更新(M2-②):一个事件承载全部变更;先统一校验,
|
|
694
|
+
* 任一条不通过 → 零写入并抛 KnowledgeError(details.failedId)。
|
|
695
|
+
*/
|
|
696
|
+
updateLessonsBatch(updates, idempotencyKey) {
|
|
697
|
+
assertIdempotencyKey(idempotencyKey);
|
|
698
|
+
if (!Array.isArray(updates) || updates.length === 0)
|
|
699
|
+
throw new KnowledgeError('invalid', 'updates 必须是非空数组');
|
|
700
|
+
for (const update of updates) {
|
|
701
|
+
if (!isRecord(update) || typeof update.id !== 'string' || update.id.trim() === '' || !isRecord(update.patch)) {
|
|
702
|
+
throw new KnowledgeError('invalid', 'updates 每项必须包含 id/patch/expectedRevision');
|
|
703
|
+
}
|
|
704
|
+
if (typeof update.expectedRevision !== 'number' || !Number.isInteger(update.expectedRevision)) {
|
|
705
|
+
throw new KnowledgeError('invalid', 'expectedRevision 必须是整数', { failedId: update.id });
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
const requestHash = this.hashRequest({
|
|
709
|
+
operation: 'lesson.batch',
|
|
710
|
+
updates: updates.map((update) => ({ id: update.id, expectedRevision: update.expectedRevision, patch: this.hashablePatch(update.patch) })),
|
|
711
|
+
});
|
|
712
|
+
return this.withLock((touch) => {
|
|
713
|
+
const state = this.loadCompleteState(touch);
|
|
714
|
+
const cached = state.idempotency.get(idempotencyKey);
|
|
715
|
+
if (cached !== undefined) {
|
|
716
|
+
this.assertSameRequest(cached, requestHash, idempotencyKey);
|
|
717
|
+
const batch = this.resolveIdempotentBatch(state, idempotencyKey);
|
|
718
|
+
if (batch === undefined)
|
|
719
|
+
throw new KnowledgeError('io', '无法恢复批量幂等键的历史结果:' + idempotencyKey, { idempotencyKey });
|
|
720
|
+
return batch;
|
|
721
|
+
}
|
|
722
|
+
const now = new Date().toISOString();
|
|
723
|
+
const planned = [];
|
|
724
|
+
for (const update of updates) {
|
|
725
|
+
const previous = state.lessons.get(update.id);
|
|
726
|
+
if (previous === undefined)
|
|
727
|
+
throw new KnowledgeError('invalid', '经验不存在:' + update.id, { failedId: update.id });
|
|
728
|
+
if (previous.revision !== update.expectedRevision) {
|
|
729
|
+
throw new KnowledgeError('revision', '经验 ' + update.id + ' 期望 revision ' + update.expectedRevision + ',实际 ' + previous.revision, {
|
|
730
|
+
failedId: update.id,
|
|
731
|
+
expectedRevision: update.expectedRevision,
|
|
732
|
+
currentRevision: previous.revision,
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
const clean = this.cleanPatch(update.patch);
|
|
736
|
+
if (clean.state !== undefined && clean.state !== previous.state && !canTransition(previous.state, clean.state)) {
|
|
737
|
+
throw new KnowledgeError('invalid', '不允许的状态迁移:' + previous.state + ' → ' + clean.state, { failedId: update.id });
|
|
738
|
+
}
|
|
739
|
+
if (clean.evidenceIds !== undefined) {
|
|
740
|
+
clean.independentSupportCount = mergeEvidenceSupport({ evidenceIds: clean.evidenceIds }, Array.from(state.evidence.values()));
|
|
741
|
+
}
|
|
742
|
+
const merged = this.maskLesson(validateLesson({
|
|
743
|
+
...previous,
|
|
744
|
+
...clean,
|
|
745
|
+
id: previous.id,
|
|
746
|
+
schemaVersion: 1,
|
|
747
|
+
revision: previous.revision + 1,
|
|
748
|
+
updatedAt: now,
|
|
749
|
+
}));
|
|
750
|
+
planned.push({ previous, merged, patch: clean });
|
|
751
|
+
}
|
|
752
|
+
const event = {
|
|
753
|
+
schemaVersion: 1,
|
|
754
|
+
id: 'evt_' + randomUUID(),
|
|
755
|
+
at: now,
|
|
756
|
+
kind: 'lesson.batch',
|
|
757
|
+
idempotencyKey,
|
|
758
|
+
requestHash,
|
|
759
|
+
payload: {
|
|
760
|
+
updates: planned.map(({ previous, merged, patch: clean }) => {
|
|
761
|
+
const patch = {};
|
|
762
|
+
for (const key of Object.keys(clean)) {
|
|
763
|
+
;
|
|
764
|
+
patch[key] = merged[key];
|
|
765
|
+
}
|
|
766
|
+
return { lessonId: previous.id, revision: merged.revision, patch };
|
|
767
|
+
}),
|
|
768
|
+
},
|
|
769
|
+
};
|
|
770
|
+
this.appendEvent(state, event);
|
|
771
|
+
const applied = [];
|
|
772
|
+
for (const { merged } of planned) {
|
|
773
|
+
state.lessons.set(merged.id, merged);
|
|
774
|
+
applied.push(merged);
|
|
775
|
+
}
|
|
776
|
+
if (!state.idempotency.has(idempotencyKey)) {
|
|
777
|
+
state.idempotency.set(idempotencyKey, { lessonId: applied[0].id, requestHash, batch: applied, lessonIds: applied.map((lesson) => lesson.id) });
|
|
778
|
+
}
|
|
779
|
+
this.writeIndex(state, touch);
|
|
780
|
+
return clone(applied);
|
|
781
|
+
});
|
|
782
|
+
}
|
|
602
783
|
/** 状态迁移;不允许的迁移抛 KnowledgeError('invalid') 且不写。 */
|
|
603
784
|
applyTransition(id, to, expectedRevision, idempotencyKey) {
|
|
604
785
|
assertIdempotencyKey(idempotencyKey);
|
|
@@ -653,10 +834,11 @@ export class KnowledgeStore {
|
|
|
653
834
|
badLines: state.badLines,
|
|
654
835
|
orphanEvents: state.orphanEvents,
|
|
655
836
|
unsupportedVersions: state.unsupportedVersions,
|
|
837
|
+
skippedEvents: state.skippedEvents,
|
|
656
838
|
firstReplayedEventId: state.firstReplayedEventId,
|
|
657
839
|
replayMode: state.replayMode,
|
|
658
840
|
replayedEvents: state.replayedEvents,
|
|
659
|
-
|
|
841
|
+
unreplayedEvents: Math.max(0, state.events - state.replayedEvents),
|
|
660
842
|
};
|
|
661
843
|
}
|
|
662
844
|
/** 只读诊断:锁、文件、计数与最近一次派生索引写失败;绝不获取锁、绝不写盘。 */
|
|
@@ -703,6 +885,7 @@ export class KnowledgeStore {
|
|
|
703
885
|
badLines: state.badLines,
|
|
704
886
|
orphanEvents: state.orphanEvents,
|
|
705
887
|
unsupportedVersions: state.unsupportedVersions,
|
|
888
|
+
skippedEvents: state.skippedEvents,
|
|
706
889
|
truncated: state.events > this.maxReplayEvents,
|
|
707
890
|
byState,
|
|
708
891
|
},
|
|
@@ -789,11 +972,29 @@ export class KnowledgeStore {
|
|
|
789
972
|
if (entry.lesson !== undefined)
|
|
790
973
|
return clone(entry.lesson);
|
|
791
974
|
const exact = this.replayUntilIdempotencyKey(idempotencyKey);
|
|
792
|
-
if (exact !== undefined)
|
|
793
|
-
return clone(exact);
|
|
975
|
+
if (exact !== undefined && exact.lesson !== undefined)
|
|
976
|
+
return clone(exact.lesson);
|
|
794
977
|
const current = state.lessons.get(entry.lessonId);
|
|
795
978
|
return current === undefined ? undefined : clone(current);
|
|
796
979
|
}
|
|
980
|
+
resolveIdempotentBatch(state, idempotencyKey) {
|
|
981
|
+
const entry = state.idempotency.get(idempotencyKey);
|
|
982
|
+
if (entry === undefined)
|
|
983
|
+
return undefined;
|
|
984
|
+
if (entry.batch !== undefined)
|
|
985
|
+
return clone(entry.batch);
|
|
986
|
+
const exact = this.replayUntilIdempotencyKey(idempotencyKey);
|
|
987
|
+
if (exact !== undefined && exact.batch !== undefined)
|
|
988
|
+
return clone(exact.batch);
|
|
989
|
+
const ids = entry.lessonIds !== undefined ? entry.lessonIds : [entry.lessonId];
|
|
990
|
+
const out = [];
|
|
991
|
+
for (const id of ids) {
|
|
992
|
+
const lesson = state.lessons.get(id);
|
|
993
|
+
if (lesson !== undefined)
|
|
994
|
+
out.push(clone(lesson));
|
|
995
|
+
}
|
|
996
|
+
return out.length === 0 ? undefined : out;
|
|
997
|
+
}
|
|
797
998
|
replayUntilIdempotencyKey(idempotencyKey) {
|
|
798
999
|
const state = createEmptyReplayState();
|
|
799
1000
|
let found;
|
|
@@ -802,8 +1003,8 @@ export class KnowledgeStore {
|
|
|
802
1003
|
state.events++;
|
|
803
1004
|
this.applyEventLine(state, line);
|
|
804
1005
|
const hit = state.idempotency.get(idempotencyKey);
|
|
805
|
-
if (hit !== undefined && hit.lesson !== undefined) {
|
|
806
|
-
found = hit
|
|
1006
|
+
if (hit !== undefined && (hit.lesson !== undefined || hit.batch !== undefined)) {
|
|
1007
|
+
found = hit;
|
|
807
1008
|
return false;
|
|
808
1009
|
}
|
|
809
1010
|
return true;
|
|
@@ -887,6 +1088,7 @@ export class KnowledgeStore {
|
|
|
887
1088
|
state.badLines = snapshot.checkpoint.counters.badLines;
|
|
888
1089
|
state.orphanEvents = snapshot.checkpoint.counters.orphanEvents;
|
|
889
1090
|
state.unsupportedVersions = snapshot.checkpoint.counters.unsupportedVersions;
|
|
1091
|
+
state.skippedEvents = snapshot.checkpoint.counters.skippedEvents;
|
|
890
1092
|
state.snapshotEvidenceCount = snapshot.checkpoint.counters.evidenceCount;
|
|
891
1093
|
state.idempotencyComplete = snapshot.checkpoint.idempotencyComplete === true;
|
|
892
1094
|
state.lastEventId = snapshot.checkpoint.lastEventId;
|
|
@@ -995,6 +1197,11 @@ export class KnowledgeStore {
|
|
|
995
1197
|
state.unsupportedVersions++;
|
|
996
1198
|
return;
|
|
997
1199
|
}
|
|
1200
|
+
const kind = raw.kind;
|
|
1201
|
+
if (typeof kind !== 'string' || !KNOWN_EVENT_KINDS.has(kind)) {
|
|
1202
|
+
state.skippedEvents++;
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
998
1205
|
if (!isEventShape(raw)) {
|
|
999
1206
|
state.badLines++;
|
|
1000
1207
|
return;
|
|
@@ -1049,9 +1256,13 @@ export class KnowledgeStore {
|
|
|
1049
1256
|
state.orphanEvents++;
|
|
1050
1257
|
return;
|
|
1051
1258
|
}
|
|
1052
|
-
const review = event.payload.
|
|
1053
|
-
|
|
1054
|
-
|
|
1259
|
+
const review = { decision: event.payload.decision, at: event.at };
|
|
1260
|
+
if (event.payload.actor !== undefined)
|
|
1261
|
+
review.actor = event.payload.actor;
|
|
1262
|
+
if (event.payload.note !== undefined)
|
|
1263
|
+
review.note = event.payload.note;
|
|
1264
|
+
if (event.payload.resolution !== undefined)
|
|
1265
|
+
review.resolution = event.payload.resolution;
|
|
1055
1266
|
const merged = validateLesson({
|
|
1056
1267
|
...previous,
|
|
1057
1268
|
review,
|
|
@@ -1073,6 +1284,40 @@ export class KnowledgeStore {
|
|
|
1073
1284
|
}
|
|
1074
1285
|
return;
|
|
1075
1286
|
}
|
|
1287
|
+
case 'lesson.batch': {
|
|
1288
|
+
const applied = [];
|
|
1289
|
+
const payload = event.payload;
|
|
1290
|
+
for (const item of payload.updates) {
|
|
1291
|
+
const previous = state.lessons.get(item.lessonId);
|
|
1292
|
+
if (previous === undefined || item.revision <= previous.revision) {
|
|
1293
|
+
state.orphanEvents++;
|
|
1294
|
+
continue;
|
|
1295
|
+
}
|
|
1296
|
+
const merged = validateLesson({
|
|
1297
|
+
...previous,
|
|
1298
|
+
...item.patch,
|
|
1299
|
+
id: previous.id,
|
|
1300
|
+
revision: item.revision,
|
|
1301
|
+
updatedAt: event.at,
|
|
1302
|
+
});
|
|
1303
|
+
if (merged.state !== previous.state && !canTransition(previous.state, merged.state)) {
|
|
1304
|
+
state.orphanEvents++;
|
|
1305
|
+
continue;
|
|
1306
|
+
}
|
|
1307
|
+
state.lessons.set(merged.id, merged);
|
|
1308
|
+
applied.push(merged);
|
|
1309
|
+
}
|
|
1310
|
+
if (!state.idempotency.has(event.idempotencyKey)) {
|
|
1311
|
+
const first = applied[0];
|
|
1312
|
+
const record = first === undefined
|
|
1313
|
+
? { lessonId: payload.updates[0] === undefined ? '' : payload.updates[0].lessonId }
|
|
1314
|
+
: { lessonId: first.id, batch: applied, lessonIds: applied.map((lesson) => lesson.id) };
|
|
1315
|
+
if (event.requestHash !== undefined)
|
|
1316
|
+
record.requestHash = event.requestHash;
|
|
1317
|
+
state.idempotency.set(event.idempotencyKey, record);
|
|
1318
|
+
}
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1076
1321
|
case 'evidence.add': {
|
|
1077
1322
|
const evidence = validateEvidence(event.payload);
|
|
1078
1323
|
state.evidence.set(evidence.id, evidence);
|
|
@@ -1106,6 +1351,8 @@ export class KnowledgeStore {
|
|
|
1106
1351
|
if (sessionKey === undefined && hash === undefined)
|
|
1107
1352
|
return undefined;
|
|
1108
1353
|
for (const item of state.evidence.values()) {
|
|
1354
|
+
if (input.verification === 'claimed' && item.verification === 'read')
|
|
1355
|
+
continue;
|
|
1109
1356
|
if (sessionKey !== undefined && evidenceSessionKey(item) === sessionKey)
|
|
1110
1357
|
return item;
|
|
1111
1358
|
if (hash !== undefined && item.sourceHash === hash)
|
|
@@ -1118,12 +1365,12 @@ export class KnowledgeStore {
|
|
|
1118
1365
|
if (!isRecord(input))
|
|
1119
1366
|
throw new KnowledgeError('invalid', '证据输入必须是对象');
|
|
1120
1367
|
const existing = this.findEvidenceByNaturalKey(state, input);
|
|
1121
|
-
if (existing !== undefined)
|
|
1368
|
+
if (existing !== undefined && !(existing.verification === 'claimed' && input.verification === 'read'))
|
|
1122
1369
|
return existing;
|
|
1123
1370
|
const now = new Date().toISOString();
|
|
1124
1371
|
const raw = {
|
|
1125
1372
|
schemaVersion: 1,
|
|
1126
|
-
id: 'evd_' + randomUUID(),
|
|
1373
|
+
id: existing?.id ?? 'evd_' + randomUUID(),
|
|
1127
1374
|
kind: input.kind,
|
|
1128
1375
|
observedAt: typeof input.observedAt === 'string' ? input.observedAt : now,
|
|
1129
1376
|
summary: input.summary,
|
|
@@ -1137,6 +1384,8 @@ export class KnowledgeStore {
|
|
|
1137
1384
|
raw.projectId = input.projectId;
|
|
1138
1385
|
if (input.sourceHash !== undefined)
|
|
1139
1386
|
raw.sourceHash = input.sourceHash;
|
|
1387
|
+
if (input.verificationReason !== undefined)
|
|
1388
|
+
raw.verificationReason = input.verificationReason;
|
|
1140
1389
|
const evidence = this.maskEvidence(validateEvidence(raw));
|
|
1141
1390
|
const natural = evidenceNaturalKey(evidence);
|
|
1142
1391
|
const event = {
|
|
@@ -1194,13 +1443,23 @@ export class KnowledgeStore {
|
|
|
1194
1443
|
maskLesson(lesson) {
|
|
1195
1444
|
if (!this.mask)
|
|
1196
1445
|
return lesson;
|
|
1197
|
-
|
|
1446
|
+
const masked = {
|
|
1198
1447
|
...lesson,
|
|
1199
1448
|
title: maskSecrets(lesson.title),
|
|
1200
1449
|
action: maskSecrets(lesson.action),
|
|
1201
1450
|
when: maskSecrets(lesson.when),
|
|
1202
1451
|
exceptions: lesson.exceptions.map((item) => maskSecrets(item)),
|
|
1203
1452
|
};
|
|
1453
|
+
if (lesson.review.note !== undefined) {
|
|
1454
|
+
const refs = [lesson.id, ...(lesson.review.resolution?.affectedIds ?? []), ...(lesson.review.resolution?.targetId ? [lesson.review.resolution.targetId] : [])];
|
|
1455
|
+
masked.review = { ...lesson.review, note: maskSecrets(lesson.review.note, refs) };
|
|
1456
|
+
}
|
|
1457
|
+
if (lesson.feedback !== undefined)
|
|
1458
|
+
masked.feedback = {
|
|
1459
|
+
...lesson.feedback,
|
|
1460
|
+
recent: lesson.feedback.recent.map(item => ({ ...item, ...(item.note === undefined ? {} : { note: maskSecrets(item.note) }) })),
|
|
1461
|
+
};
|
|
1462
|
+
return masked;
|
|
1204
1463
|
}
|
|
1205
1464
|
maskEvidence(evidence) {
|
|
1206
1465
|
if (!this.mask)
|
|
@@ -1295,6 +1554,7 @@ export class KnowledgeStore {
|
|
|
1295
1554
|
badLines: counterNumber('badLines'),
|
|
1296
1555
|
orphanEvents: counterNumber('orphanEvents'),
|
|
1297
1556
|
unsupportedVersions: counterNumber('unsupportedVersions'),
|
|
1557
|
+
skippedEvents: counterNumber('skippedEvents'),
|
|
1298
1558
|
evidenceCount: counterNumber('evidenceCount'),
|
|
1299
1559
|
};
|
|
1300
1560
|
const lessonsRaw = raw.lessons;
|
|
@@ -1340,14 +1600,20 @@ export class KnowledgeStore {
|
|
|
1340
1600
|
const record = { lessonId: value.lessonId };
|
|
1341
1601
|
if (typeof value.requestHash === 'string')
|
|
1342
1602
|
record.requestHash = value.requestHash;
|
|
1603
|
+
if (Array.isArray(value.lessonIds)) {
|
|
1604
|
+
record.lessonIds = value.lessonIds.filter((id) => typeof id === 'string');
|
|
1605
|
+
}
|
|
1343
1606
|
idempotency.set(key, record);
|
|
1344
1607
|
}
|
|
1345
1608
|
}
|
|
1346
1609
|
const serialized = {};
|
|
1347
1610
|
for (const [key, entry] of idempotency) {
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1611
|
+
const record = { lessonId: entry.lessonId };
|
|
1612
|
+
if (entry.requestHash !== undefined)
|
|
1613
|
+
record.requestHash = entry.requestHash;
|
|
1614
|
+
if (entry.lessonIds !== undefined)
|
|
1615
|
+
record.lessonIds = entry.lessonIds;
|
|
1616
|
+
serialized[key] = record;
|
|
1351
1617
|
}
|
|
1352
1618
|
return {
|
|
1353
1619
|
lessons,
|
|
@@ -1374,9 +1640,12 @@ export class KnowledgeStore {
|
|
|
1374
1640
|
const prefixBytes = Math.min(size, PREFIX_HASH_BYTES);
|
|
1375
1641
|
const idempotency = {};
|
|
1376
1642
|
for (const [key, entry] of state.idempotency) {
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1643
|
+
const record = { lessonId: entry.lessonId };
|
|
1644
|
+
if (entry.requestHash !== undefined)
|
|
1645
|
+
record.requestHash = entry.requestHash;
|
|
1646
|
+
if (entry.lessonIds !== undefined)
|
|
1647
|
+
record.lessonIds = entry.lessonIds;
|
|
1648
|
+
idempotency[key] = record;
|
|
1380
1649
|
}
|
|
1381
1650
|
return {
|
|
1382
1651
|
offset: size,
|
|
@@ -1389,6 +1658,7 @@ export class KnowledgeStore {
|
|
|
1389
1658
|
badLines: state.badLines,
|
|
1390
1659
|
orphanEvents: state.orphanEvents,
|
|
1391
1660
|
unsupportedVersions: state.unsupportedVersions,
|
|
1661
|
+
skippedEvents: state.skippedEvents,
|
|
1392
1662
|
evidenceCount: state.evidence.size,
|
|
1393
1663
|
},
|
|
1394
1664
|
idempotency,
|
package/lib/knowledge.d.ts
CHANGED
|
@@ -4,6 +4,15 @@ export type LessonKind = 'preference' | 'procedure' | 'pitfall' | 'fact';
|
|
|
4
4
|
export type LessonState = 'candidate' | 'usable' | 'disputed' | 'stale' | 'rejected';
|
|
5
5
|
/** 审阅结论。 */
|
|
6
6
|
export type ReviewDecision = 'unreviewed' | 'accepted' | 'rejected';
|
|
7
|
+
/** 解析 / 冲突处理的可选痕迹(事件与经验都带得下;旧数据没有也合法)。 */
|
|
8
|
+
export interface LessonReviewResolution {
|
|
9
|
+
/** prefer | drop | merge 等;不强制枚举,由 tools 层约定。 */
|
|
10
|
+
kind: string;
|
|
11
|
+
targetId?: string;
|
|
12
|
+
affectedIds?: string[];
|
|
13
|
+
}
|
|
14
|
+
/** 审阅备注最大长度;超出截断而不是报错。 */
|
|
15
|
+
export declare const REVIEW_NOTE_MAX_LENGTH = 500;
|
|
7
16
|
/** 一条可复用经验。 */
|
|
8
17
|
export interface Lesson {
|
|
9
18
|
schemaVersion: 1;
|
|
@@ -36,7 +45,11 @@ export interface Lesson {
|
|
|
36
45
|
decision: ReviewDecision;
|
|
37
46
|
actor?: string;
|
|
38
47
|
at?: string;
|
|
48
|
+
note?: string;
|
|
49
|
+
resolution?: LessonReviewResolution;
|
|
39
50
|
};
|
|
51
|
+
/** 使用反馈独立于审阅和证据;只保留最近 20 次明细,事件流保留历史。 */
|
|
52
|
+
feedback?: LessonFeedback;
|
|
40
53
|
createdAt: string;
|
|
41
54
|
updatedAt: string;
|
|
42
55
|
lastValidatedAt?: string;
|
|
@@ -44,6 +57,15 @@ export interface Lesson {
|
|
|
44
57
|
supersedes?: string[];
|
|
45
58
|
conflictIds: string[];
|
|
46
59
|
}
|
|
60
|
+
export interface LessonFeedback {
|
|
61
|
+
useful: number;
|
|
62
|
+
notApplicable: number;
|
|
63
|
+
recent: Array<{
|
|
64
|
+
vote: 'useful' | 'not-applicable';
|
|
65
|
+
at: string;
|
|
66
|
+
note?: string;
|
|
67
|
+
}>;
|
|
68
|
+
}
|
|
47
69
|
/** 一条证据:只存脱敏摘要 + 定位信息。 */
|
|
48
70
|
export interface Evidence {
|
|
49
71
|
schemaVersion: 1;
|
|
@@ -58,6 +80,8 @@ export interface Evidence {
|
|
|
58
80
|
/** read=插件读到原记录;claimed=仅模型声称。 */
|
|
59
81
|
verification: 'read' | 'claimed';
|
|
60
82
|
sourceHash?: string;
|
|
83
|
+
/** 插件生成的核验结果;不是模型给出的保证。 */
|
|
84
|
+
verificationReason?: string;
|
|
61
85
|
}
|
|
62
86
|
/** dream_learn / 迁移的输入。 */
|
|
63
87
|
export interface LessonInput {
|
|
@@ -110,6 +134,8 @@ export type KnowledgeEvent = {
|
|
|
110
134
|
decision: ReviewDecision;
|
|
111
135
|
actor?: string;
|
|
112
136
|
state: LessonState;
|
|
137
|
+
note?: string;
|
|
138
|
+
resolution?: LessonReviewResolution;
|
|
113
139
|
};
|
|
114
140
|
} | {
|
|
115
141
|
schemaVersion: 1;
|
|
@@ -119,6 +145,20 @@ export type KnowledgeEvent = {
|
|
|
119
145
|
idempotencyKey: string;
|
|
120
146
|
requestHash?: string;
|
|
121
147
|
payload: Evidence;
|
|
148
|
+
} | {
|
|
149
|
+
schemaVersion: 1;
|
|
150
|
+
id: string;
|
|
151
|
+
at: string;
|
|
152
|
+
kind: 'lesson.batch';
|
|
153
|
+
idempotencyKey: string;
|
|
154
|
+
requestHash?: string;
|
|
155
|
+
payload: {
|
|
156
|
+
updates: Array<{
|
|
157
|
+
lessonId: string;
|
|
158
|
+
revision: number;
|
|
159
|
+
patch: Partial<Lesson>;
|
|
160
|
+
}>;
|
|
161
|
+
};
|
|
122
162
|
};
|
|
123
163
|
/** 知识层错误码:invalid=数据不合法;revision=乐观锁不符;duplicate=重复;io=读写/锁失败。 */
|
|
124
164
|
export declare class KnowledgeError extends Error {
|