@stardustlc/dsh-dream 0.5.1 → 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 +13 -0
- package/README.en.md +29 -12
- package/README.md +33 -16
- package/lib/bridge.d.ts +205 -2
- package/lib/bridge.js +1053 -6
- package/lib/client.js +515 -26
- package/lib/evidence.d.ts +15 -0
- package/lib/evidence.js +123 -0
- package/lib/knowledge-store.d.ts +21 -5
- package/lib/knowledge-store.js +271 -41
- package/lib/knowledge.d.ts +29 -0
- package/lib/knowledge.js +29 -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.js +376 -72
- package/lib/web.d.ts +48 -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);
|
|
@@ -536,12 +574,20 @@ export class KnowledgeStore {
|
|
|
536
574
|
});
|
|
537
575
|
}
|
|
538
576
|
/** 审阅经验:accepted → usable,rejected → rejected,unreviewed → candidate。 */
|
|
539
|
-
reviewLesson(id, decision, expectedRevision, idempotencyKey, actor) {
|
|
577
|
+
reviewLesson(id, decision, expectedRevision, idempotencyKey, actor, note, resolution) {
|
|
540
578
|
assertIdempotencyKey(idempotencyKey);
|
|
541
579
|
if (!isEnum(decision, REVIEW_DECISIONS))
|
|
542
580
|
throw new KnowledgeError('invalid', 'review decision 取值不合法');
|
|
543
581
|
const maskedActor = actor === undefined ? undefined : this.mask ? maskSecrets(actor) : actor;
|
|
544
|
-
const
|
|
582
|
+
const maskedNote = note === undefined ? undefined : this.mask ? maskSecrets(note) : note;
|
|
583
|
+
const requestHash = this.hashRequest({
|
|
584
|
+
operation: 'lesson.review',
|
|
585
|
+
lessonId: id,
|
|
586
|
+
decision,
|
|
587
|
+
actor: maskedActor === undefined ? null : maskedActor,
|
|
588
|
+
note: maskedNote === undefined ? null : maskedNote,
|
|
589
|
+
resolution: resolution === undefined ? null : resolution,
|
|
590
|
+
});
|
|
545
591
|
return this.withLock((touch) => {
|
|
546
592
|
const state = this.loadCompleteState(touch);
|
|
547
593
|
const cached = state.idempotency.get(idempotencyKey);
|
|
@@ -567,18 +613,27 @@ export class KnowledgeStore {
|
|
|
567
613
|
throw new KnowledgeError('invalid', '不允许的审阅迁移:' + lesson.state + ' → ' + nextState, { lessonId: id });
|
|
568
614
|
}
|
|
569
615
|
const now = new Date().toISOString();
|
|
616
|
+
const nextReview = { decision, at: now };
|
|
617
|
+
if (maskedActor !== undefined)
|
|
618
|
+
nextReview.actor = maskedActor;
|
|
619
|
+
if (maskedNote !== undefined)
|
|
620
|
+
nextReview.note = maskedNote;
|
|
621
|
+
if (resolution !== undefined)
|
|
622
|
+
nextReview.resolution = resolution;
|
|
570
623
|
const merged = validateLesson({
|
|
571
624
|
...lesson,
|
|
572
625
|
state: nextState,
|
|
573
|
-
review:
|
|
574
|
-
? { decision, at: now }
|
|
575
|
-
: { decision, actor: maskedActor, at: now },
|
|
626
|
+
review: nextReview,
|
|
576
627
|
revision: lesson.revision + 1,
|
|
577
628
|
updatedAt: now,
|
|
578
629
|
});
|
|
579
630
|
const payload = { decision, state: nextState };
|
|
580
631
|
if (maskedActor !== undefined)
|
|
581
632
|
payload.actor = maskedActor;
|
|
633
|
+
if (maskedNote !== undefined)
|
|
634
|
+
payload.note = maskedNote;
|
|
635
|
+
if (resolution !== undefined)
|
|
636
|
+
payload.resolution = resolution;
|
|
582
637
|
const event = {
|
|
583
638
|
schemaVersion: 1,
|
|
584
639
|
id: 'evt_' + randomUUID(),
|
|
@@ -599,6 +654,97 @@ export class KnowledgeStore {
|
|
|
599
654
|
return clone(merged);
|
|
600
655
|
});
|
|
601
656
|
}
|
|
657
|
+
/**
|
|
658
|
+
* 批量原子更新(M2-②):一个事件承载全部变更;先统一校验,
|
|
659
|
+
* 任一条不通过 → 零写入并抛 KnowledgeError(details.failedId)。
|
|
660
|
+
*/
|
|
661
|
+
updateLessonsBatch(updates, idempotencyKey) {
|
|
662
|
+
assertIdempotencyKey(idempotencyKey);
|
|
663
|
+
if (!Array.isArray(updates) || updates.length === 0)
|
|
664
|
+
throw new KnowledgeError('invalid', 'updates 必须是非空数组');
|
|
665
|
+
for (const update of updates) {
|
|
666
|
+
if (!isRecord(update) || typeof update.id !== 'string' || update.id.trim() === '' || !isRecord(update.patch)) {
|
|
667
|
+
throw new KnowledgeError('invalid', 'updates 每项必须包含 id/patch/expectedRevision');
|
|
668
|
+
}
|
|
669
|
+
if (typeof update.expectedRevision !== 'number' || !Number.isInteger(update.expectedRevision)) {
|
|
670
|
+
throw new KnowledgeError('invalid', 'expectedRevision 必须是整数', { failedId: update.id });
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
const requestHash = this.hashRequest({
|
|
674
|
+
operation: 'lesson.batch',
|
|
675
|
+
updates: updates.map((update) => ({ id: update.id, expectedRevision: update.expectedRevision, patch: this.hashablePatch(update.patch) })),
|
|
676
|
+
});
|
|
677
|
+
return this.withLock((touch) => {
|
|
678
|
+
const state = this.loadCompleteState(touch);
|
|
679
|
+
const cached = state.idempotency.get(idempotencyKey);
|
|
680
|
+
if (cached !== undefined) {
|
|
681
|
+
this.assertSameRequest(cached, requestHash, idempotencyKey);
|
|
682
|
+
const batch = this.resolveIdempotentBatch(state, idempotencyKey);
|
|
683
|
+
if (batch === undefined)
|
|
684
|
+
throw new KnowledgeError('io', '无法恢复批量幂等键的历史结果:' + idempotencyKey, { idempotencyKey });
|
|
685
|
+
return batch;
|
|
686
|
+
}
|
|
687
|
+
const now = new Date().toISOString();
|
|
688
|
+
const planned = [];
|
|
689
|
+
for (const update of updates) {
|
|
690
|
+
const previous = state.lessons.get(update.id);
|
|
691
|
+
if (previous === undefined)
|
|
692
|
+
throw new KnowledgeError('invalid', '经验不存在:' + update.id, { failedId: update.id });
|
|
693
|
+
if (previous.revision !== update.expectedRevision) {
|
|
694
|
+
throw new KnowledgeError('revision', '经验 ' + update.id + ' 期望 revision ' + update.expectedRevision + ',实际 ' + previous.revision, {
|
|
695
|
+
failedId: update.id,
|
|
696
|
+
expectedRevision: update.expectedRevision,
|
|
697
|
+
currentRevision: previous.revision,
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
const clean = this.cleanPatch(update.patch);
|
|
701
|
+
if (clean.state !== undefined && clean.state !== previous.state && !canTransition(previous.state, clean.state)) {
|
|
702
|
+
throw new KnowledgeError('invalid', '不允许的状态迁移:' + previous.state + ' → ' + clean.state, { failedId: update.id });
|
|
703
|
+
}
|
|
704
|
+
if (clean.evidenceIds !== undefined) {
|
|
705
|
+
clean.independentSupportCount = mergeEvidenceSupport({ evidenceIds: clean.evidenceIds }, Array.from(state.evidence.values()));
|
|
706
|
+
}
|
|
707
|
+
const merged = this.maskLesson(validateLesson({
|
|
708
|
+
...previous,
|
|
709
|
+
...clean,
|
|
710
|
+
id: previous.id,
|
|
711
|
+
schemaVersion: 1,
|
|
712
|
+
revision: previous.revision + 1,
|
|
713
|
+
updatedAt: now,
|
|
714
|
+
}));
|
|
715
|
+
planned.push({ previous, merged, patch: clean });
|
|
716
|
+
}
|
|
717
|
+
const event = {
|
|
718
|
+
schemaVersion: 1,
|
|
719
|
+
id: 'evt_' + randomUUID(),
|
|
720
|
+
at: now,
|
|
721
|
+
kind: 'lesson.batch',
|
|
722
|
+
idempotencyKey,
|
|
723
|
+
requestHash,
|
|
724
|
+
payload: {
|
|
725
|
+
updates: planned.map(({ previous, merged, patch: clean }) => {
|
|
726
|
+
const patch = {};
|
|
727
|
+
for (const key of Object.keys(clean)) {
|
|
728
|
+
;
|
|
729
|
+
patch[key] = merged[key];
|
|
730
|
+
}
|
|
731
|
+
return { lessonId: previous.id, revision: merged.revision, patch };
|
|
732
|
+
}),
|
|
733
|
+
},
|
|
734
|
+
};
|
|
735
|
+
this.appendEvent(state, event);
|
|
736
|
+
const applied = [];
|
|
737
|
+
for (const { merged } of planned) {
|
|
738
|
+
state.lessons.set(merged.id, merged);
|
|
739
|
+
applied.push(merged);
|
|
740
|
+
}
|
|
741
|
+
if (!state.idempotency.has(idempotencyKey)) {
|
|
742
|
+
state.idempotency.set(idempotencyKey, { lessonId: applied[0].id, requestHash, batch: applied, lessonIds: applied.map((lesson) => lesson.id) });
|
|
743
|
+
}
|
|
744
|
+
this.writeIndex(state, touch);
|
|
745
|
+
return clone(applied);
|
|
746
|
+
});
|
|
747
|
+
}
|
|
602
748
|
/** 状态迁移;不允许的迁移抛 KnowledgeError('invalid') 且不写。 */
|
|
603
749
|
applyTransition(id, to, expectedRevision, idempotencyKey) {
|
|
604
750
|
assertIdempotencyKey(idempotencyKey);
|
|
@@ -653,10 +799,11 @@ export class KnowledgeStore {
|
|
|
653
799
|
badLines: state.badLines,
|
|
654
800
|
orphanEvents: state.orphanEvents,
|
|
655
801
|
unsupportedVersions: state.unsupportedVersions,
|
|
802
|
+
skippedEvents: state.skippedEvents,
|
|
656
803
|
firstReplayedEventId: state.firstReplayedEventId,
|
|
657
804
|
replayMode: state.replayMode,
|
|
658
805
|
replayedEvents: state.replayedEvents,
|
|
659
|
-
|
|
806
|
+
unreplayedEvents: Math.max(0, state.events - state.replayedEvents),
|
|
660
807
|
};
|
|
661
808
|
}
|
|
662
809
|
/** 只读诊断:锁、文件、计数与最近一次派生索引写失败;绝不获取锁、绝不写盘。 */
|
|
@@ -703,6 +850,7 @@ export class KnowledgeStore {
|
|
|
703
850
|
badLines: state.badLines,
|
|
704
851
|
orphanEvents: state.orphanEvents,
|
|
705
852
|
unsupportedVersions: state.unsupportedVersions,
|
|
853
|
+
skippedEvents: state.skippedEvents,
|
|
706
854
|
truncated: state.events > this.maxReplayEvents,
|
|
707
855
|
byState,
|
|
708
856
|
},
|
|
@@ -789,11 +937,29 @@ export class KnowledgeStore {
|
|
|
789
937
|
if (entry.lesson !== undefined)
|
|
790
938
|
return clone(entry.lesson);
|
|
791
939
|
const exact = this.replayUntilIdempotencyKey(idempotencyKey);
|
|
792
|
-
if (exact !== undefined)
|
|
793
|
-
return clone(exact);
|
|
940
|
+
if (exact !== undefined && exact.lesson !== undefined)
|
|
941
|
+
return clone(exact.lesson);
|
|
794
942
|
const current = state.lessons.get(entry.lessonId);
|
|
795
943
|
return current === undefined ? undefined : clone(current);
|
|
796
944
|
}
|
|
945
|
+
resolveIdempotentBatch(state, idempotencyKey) {
|
|
946
|
+
const entry = state.idempotency.get(idempotencyKey);
|
|
947
|
+
if (entry === undefined)
|
|
948
|
+
return undefined;
|
|
949
|
+
if (entry.batch !== undefined)
|
|
950
|
+
return clone(entry.batch);
|
|
951
|
+
const exact = this.replayUntilIdempotencyKey(idempotencyKey);
|
|
952
|
+
if (exact !== undefined && exact.batch !== undefined)
|
|
953
|
+
return clone(exact.batch);
|
|
954
|
+
const ids = entry.lessonIds !== undefined ? entry.lessonIds : [entry.lessonId];
|
|
955
|
+
const out = [];
|
|
956
|
+
for (const id of ids) {
|
|
957
|
+
const lesson = state.lessons.get(id);
|
|
958
|
+
if (lesson !== undefined)
|
|
959
|
+
out.push(clone(lesson));
|
|
960
|
+
}
|
|
961
|
+
return out.length === 0 ? undefined : out;
|
|
962
|
+
}
|
|
797
963
|
replayUntilIdempotencyKey(idempotencyKey) {
|
|
798
964
|
const state = createEmptyReplayState();
|
|
799
965
|
let found;
|
|
@@ -802,8 +968,8 @@ export class KnowledgeStore {
|
|
|
802
968
|
state.events++;
|
|
803
969
|
this.applyEventLine(state, line);
|
|
804
970
|
const hit = state.idempotency.get(idempotencyKey);
|
|
805
|
-
if (hit !== undefined && hit.lesson !== undefined) {
|
|
806
|
-
found = hit
|
|
971
|
+
if (hit !== undefined && (hit.lesson !== undefined || hit.batch !== undefined)) {
|
|
972
|
+
found = hit;
|
|
807
973
|
return false;
|
|
808
974
|
}
|
|
809
975
|
return true;
|
|
@@ -887,6 +1053,7 @@ export class KnowledgeStore {
|
|
|
887
1053
|
state.badLines = snapshot.checkpoint.counters.badLines;
|
|
888
1054
|
state.orphanEvents = snapshot.checkpoint.counters.orphanEvents;
|
|
889
1055
|
state.unsupportedVersions = snapshot.checkpoint.counters.unsupportedVersions;
|
|
1056
|
+
state.skippedEvents = snapshot.checkpoint.counters.skippedEvents;
|
|
890
1057
|
state.snapshotEvidenceCount = snapshot.checkpoint.counters.evidenceCount;
|
|
891
1058
|
state.idempotencyComplete = snapshot.checkpoint.idempotencyComplete === true;
|
|
892
1059
|
state.lastEventId = snapshot.checkpoint.lastEventId;
|
|
@@ -995,6 +1162,11 @@ export class KnowledgeStore {
|
|
|
995
1162
|
state.unsupportedVersions++;
|
|
996
1163
|
return;
|
|
997
1164
|
}
|
|
1165
|
+
const kind = raw.kind;
|
|
1166
|
+
if (typeof kind !== 'string' || !KNOWN_EVENT_KINDS.has(kind)) {
|
|
1167
|
+
state.skippedEvents++;
|
|
1168
|
+
return;
|
|
1169
|
+
}
|
|
998
1170
|
if (!isEventShape(raw)) {
|
|
999
1171
|
state.badLines++;
|
|
1000
1172
|
return;
|
|
@@ -1049,9 +1221,13 @@ export class KnowledgeStore {
|
|
|
1049
1221
|
state.orphanEvents++;
|
|
1050
1222
|
return;
|
|
1051
1223
|
}
|
|
1052
|
-
const review = event.payload.
|
|
1053
|
-
|
|
1054
|
-
|
|
1224
|
+
const review = { decision: event.payload.decision, at: event.at };
|
|
1225
|
+
if (event.payload.actor !== undefined)
|
|
1226
|
+
review.actor = event.payload.actor;
|
|
1227
|
+
if (event.payload.note !== undefined)
|
|
1228
|
+
review.note = event.payload.note;
|
|
1229
|
+
if (event.payload.resolution !== undefined)
|
|
1230
|
+
review.resolution = event.payload.resolution;
|
|
1055
1231
|
const merged = validateLesson({
|
|
1056
1232
|
...previous,
|
|
1057
1233
|
review,
|
|
@@ -1073,6 +1249,40 @@ export class KnowledgeStore {
|
|
|
1073
1249
|
}
|
|
1074
1250
|
return;
|
|
1075
1251
|
}
|
|
1252
|
+
case 'lesson.batch': {
|
|
1253
|
+
const applied = [];
|
|
1254
|
+
const payload = event.payload;
|
|
1255
|
+
for (const item of payload.updates) {
|
|
1256
|
+
const previous = state.lessons.get(item.lessonId);
|
|
1257
|
+
if (previous === undefined || item.revision <= previous.revision) {
|
|
1258
|
+
state.orphanEvents++;
|
|
1259
|
+
continue;
|
|
1260
|
+
}
|
|
1261
|
+
const merged = validateLesson({
|
|
1262
|
+
...previous,
|
|
1263
|
+
...item.patch,
|
|
1264
|
+
id: previous.id,
|
|
1265
|
+
revision: item.revision,
|
|
1266
|
+
updatedAt: event.at,
|
|
1267
|
+
});
|
|
1268
|
+
if (merged.state !== previous.state && !canTransition(previous.state, merged.state)) {
|
|
1269
|
+
state.orphanEvents++;
|
|
1270
|
+
continue;
|
|
1271
|
+
}
|
|
1272
|
+
state.lessons.set(merged.id, merged);
|
|
1273
|
+
applied.push(merged);
|
|
1274
|
+
}
|
|
1275
|
+
if (!state.idempotency.has(event.idempotencyKey)) {
|
|
1276
|
+
const first = applied[0];
|
|
1277
|
+
const record = first === undefined
|
|
1278
|
+
? { lessonId: payload.updates[0] === undefined ? '' : payload.updates[0].lessonId }
|
|
1279
|
+
: { lessonId: first.id, batch: applied, lessonIds: applied.map((lesson) => lesson.id) };
|
|
1280
|
+
if (event.requestHash !== undefined)
|
|
1281
|
+
record.requestHash = event.requestHash;
|
|
1282
|
+
state.idempotency.set(event.idempotencyKey, record);
|
|
1283
|
+
}
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1076
1286
|
case 'evidence.add': {
|
|
1077
1287
|
const evidence = validateEvidence(event.payload);
|
|
1078
1288
|
state.evidence.set(evidence.id, evidence);
|
|
@@ -1106,6 +1316,8 @@ export class KnowledgeStore {
|
|
|
1106
1316
|
if (sessionKey === undefined && hash === undefined)
|
|
1107
1317
|
return undefined;
|
|
1108
1318
|
for (const item of state.evidence.values()) {
|
|
1319
|
+
if (input.verification === 'claimed' && item.verification === 'read')
|
|
1320
|
+
continue;
|
|
1109
1321
|
if (sessionKey !== undefined && evidenceSessionKey(item) === sessionKey)
|
|
1110
1322
|
return item;
|
|
1111
1323
|
if (hash !== undefined && item.sourceHash === hash)
|
|
@@ -1118,12 +1330,12 @@ export class KnowledgeStore {
|
|
|
1118
1330
|
if (!isRecord(input))
|
|
1119
1331
|
throw new KnowledgeError('invalid', '证据输入必须是对象');
|
|
1120
1332
|
const existing = this.findEvidenceByNaturalKey(state, input);
|
|
1121
|
-
if (existing !== undefined)
|
|
1333
|
+
if (existing !== undefined && !(existing.verification === 'claimed' && input.verification === 'read'))
|
|
1122
1334
|
return existing;
|
|
1123
1335
|
const now = new Date().toISOString();
|
|
1124
1336
|
const raw = {
|
|
1125
1337
|
schemaVersion: 1,
|
|
1126
|
-
id: 'evd_' + randomUUID(),
|
|
1338
|
+
id: existing?.id ?? 'evd_' + randomUUID(),
|
|
1127
1339
|
kind: input.kind,
|
|
1128
1340
|
observedAt: typeof input.observedAt === 'string' ? input.observedAt : now,
|
|
1129
1341
|
summary: input.summary,
|
|
@@ -1137,6 +1349,8 @@ export class KnowledgeStore {
|
|
|
1137
1349
|
raw.projectId = input.projectId;
|
|
1138
1350
|
if (input.sourceHash !== undefined)
|
|
1139
1351
|
raw.sourceHash = input.sourceHash;
|
|
1352
|
+
if (input.verificationReason !== undefined)
|
|
1353
|
+
raw.verificationReason = input.verificationReason;
|
|
1140
1354
|
const evidence = this.maskEvidence(validateEvidence(raw));
|
|
1141
1355
|
const natural = evidenceNaturalKey(evidence);
|
|
1142
1356
|
const event = {
|
|
@@ -1194,13 +1408,18 @@ export class KnowledgeStore {
|
|
|
1194
1408
|
maskLesson(lesson) {
|
|
1195
1409
|
if (!this.mask)
|
|
1196
1410
|
return lesson;
|
|
1197
|
-
|
|
1411
|
+
const masked = {
|
|
1198
1412
|
...lesson,
|
|
1199
1413
|
title: maskSecrets(lesson.title),
|
|
1200
1414
|
action: maskSecrets(lesson.action),
|
|
1201
1415
|
when: maskSecrets(lesson.when),
|
|
1202
1416
|
exceptions: lesson.exceptions.map((item) => maskSecrets(item)),
|
|
1203
1417
|
};
|
|
1418
|
+
if (lesson.review.note !== undefined) {
|
|
1419
|
+
const refs = [lesson.id, ...(lesson.review.resolution?.affectedIds ?? []), ...(lesson.review.resolution?.targetId ? [lesson.review.resolution.targetId] : [])];
|
|
1420
|
+
masked.review = { ...lesson.review, note: maskSecrets(lesson.review.note, refs) };
|
|
1421
|
+
}
|
|
1422
|
+
return masked;
|
|
1204
1423
|
}
|
|
1205
1424
|
maskEvidence(evidence) {
|
|
1206
1425
|
if (!this.mask)
|
|
@@ -1295,6 +1514,7 @@ export class KnowledgeStore {
|
|
|
1295
1514
|
badLines: counterNumber('badLines'),
|
|
1296
1515
|
orphanEvents: counterNumber('orphanEvents'),
|
|
1297
1516
|
unsupportedVersions: counterNumber('unsupportedVersions'),
|
|
1517
|
+
skippedEvents: counterNumber('skippedEvents'),
|
|
1298
1518
|
evidenceCount: counterNumber('evidenceCount'),
|
|
1299
1519
|
};
|
|
1300
1520
|
const lessonsRaw = raw.lessons;
|
|
@@ -1340,14 +1560,20 @@ export class KnowledgeStore {
|
|
|
1340
1560
|
const record = { lessonId: value.lessonId };
|
|
1341
1561
|
if (typeof value.requestHash === 'string')
|
|
1342
1562
|
record.requestHash = value.requestHash;
|
|
1563
|
+
if (Array.isArray(value.lessonIds)) {
|
|
1564
|
+
record.lessonIds = value.lessonIds.filter((id) => typeof id === 'string');
|
|
1565
|
+
}
|
|
1343
1566
|
idempotency.set(key, record);
|
|
1344
1567
|
}
|
|
1345
1568
|
}
|
|
1346
1569
|
const serialized = {};
|
|
1347
1570
|
for (const [key, entry] of idempotency) {
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1571
|
+
const record = { lessonId: entry.lessonId };
|
|
1572
|
+
if (entry.requestHash !== undefined)
|
|
1573
|
+
record.requestHash = entry.requestHash;
|
|
1574
|
+
if (entry.lessonIds !== undefined)
|
|
1575
|
+
record.lessonIds = entry.lessonIds;
|
|
1576
|
+
serialized[key] = record;
|
|
1351
1577
|
}
|
|
1352
1578
|
return {
|
|
1353
1579
|
lessons,
|
|
@@ -1374,9 +1600,12 @@ export class KnowledgeStore {
|
|
|
1374
1600
|
const prefixBytes = Math.min(size, PREFIX_HASH_BYTES);
|
|
1375
1601
|
const idempotency = {};
|
|
1376
1602
|
for (const [key, entry] of state.idempotency) {
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1603
|
+
const record = { lessonId: entry.lessonId };
|
|
1604
|
+
if (entry.requestHash !== undefined)
|
|
1605
|
+
record.requestHash = entry.requestHash;
|
|
1606
|
+
if (entry.lessonIds !== undefined)
|
|
1607
|
+
record.lessonIds = entry.lessonIds;
|
|
1608
|
+
idempotency[key] = record;
|
|
1380
1609
|
}
|
|
1381
1610
|
return {
|
|
1382
1611
|
offset: size,
|
|
@@ -1389,6 +1618,7 @@ export class KnowledgeStore {
|
|
|
1389
1618
|
badLines: state.badLines,
|
|
1390
1619
|
orphanEvents: state.orphanEvents,
|
|
1391
1620
|
unsupportedVersions: state.unsupportedVersions,
|
|
1621
|
+
skippedEvents: state.skippedEvents,
|
|
1392
1622
|
evidenceCount: state.evidence.size,
|
|
1393
1623
|
},
|
|
1394
1624
|
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,6 +45,8 @@ export interface Lesson {
|
|
|
36
45
|
decision: ReviewDecision;
|
|
37
46
|
actor?: string;
|
|
38
47
|
at?: string;
|
|
48
|
+
note?: string;
|
|
49
|
+
resolution?: LessonReviewResolution;
|
|
39
50
|
};
|
|
40
51
|
createdAt: string;
|
|
41
52
|
updatedAt: string;
|
|
@@ -58,6 +69,8 @@ export interface Evidence {
|
|
|
58
69
|
/** read=插件读到原记录;claimed=仅模型声称。 */
|
|
59
70
|
verification: 'read' | 'claimed';
|
|
60
71
|
sourceHash?: string;
|
|
72
|
+
/** 插件生成的核验结果;不是模型给出的保证。 */
|
|
73
|
+
verificationReason?: string;
|
|
61
74
|
}
|
|
62
75
|
/** dream_learn / 迁移的输入。 */
|
|
63
76
|
export interface LessonInput {
|
|
@@ -110,6 +123,8 @@ export type KnowledgeEvent = {
|
|
|
110
123
|
decision: ReviewDecision;
|
|
111
124
|
actor?: string;
|
|
112
125
|
state: LessonState;
|
|
126
|
+
note?: string;
|
|
127
|
+
resolution?: LessonReviewResolution;
|
|
113
128
|
};
|
|
114
129
|
} | {
|
|
115
130
|
schemaVersion: 1;
|
|
@@ -119,6 +134,20 @@ export type KnowledgeEvent = {
|
|
|
119
134
|
idempotencyKey: string;
|
|
120
135
|
requestHash?: string;
|
|
121
136
|
payload: Evidence;
|
|
137
|
+
} | {
|
|
138
|
+
schemaVersion: 1;
|
|
139
|
+
id: string;
|
|
140
|
+
at: string;
|
|
141
|
+
kind: 'lesson.batch';
|
|
142
|
+
idempotencyKey: string;
|
|
143
|
+
requestHash?: string;
|
|
144
|
+
payload: {
|
|
145
|
+
updates: Array<{
|
|
146
|
+
lessonId: string;
|
|
147
|
+
revision: number;
|
|
148
|
+
patch: Partial<Lesson>;
|
|
149
|
+
}>;
|
|
150
|
+
};
|
|
122
151
|
};
|
|
123
152
|
/** 知识层错误码:invalid=数据不合法;revision=乐观锁不符;duplicate=重复;io=读写/锁失败。 */
|
|
124
153
|
export declare class KnowledgeError extends Error {
|
package/lib/knowledge.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* @module dsh-dream/knowledge
|
|
8
8
|
*/
|
|
9
9
|
import { createHash } from 'node:crypto';
|
|
10
|
+
/** 审阅备注最大长度;超出截断而不是报错。 */
|
|
11
|
+
export const REVIEW_NOTE_MAX_LENGTH = 500;
|
|
10
12
|
/** 知识层错误码:invalid=数据不合法;revision=乐观锁不符;duplicate=重复;io=读写/锁失败。 */
|
|
11
13
|
export class KnowledgeError extends Error {
|
|
12
14
|
code;
|
|
@@ -118,6 +120,21 @@ function readApplicability(value) {
|
|
|
118
120
|
}
|
|
119
121
|
return out;
|
|
120
122
|
}
|
|
123
|
+
function readResolution(value) {
|
|
124
|
+
if (value === undefined || value === null)
|
|
125
|
+
return undefined;
|
|
126
|
+
if (!isRecord(value))
|
|
127
|
+
fail('review.resolution', '必须是对象');
|
|
128
|
+
const kind = readString(value.kind, 'review.resolution.kind', { required: true, nonEmpty: true });
|
|
129
|
+
const targetId = readString(value.targetId, 'review.resolution.targetId', { nonEmpty: true });
|
|
130
|
+
const affectedIds = readStringArray(value.affectedIds, 'review.resolution.affectedIds');
|
|
131
|
+
const resolution = { kind };
|
|
132
|
+
if (targetId !== undefined)
|
|
133
|
+
resolution.targetId = targetId;
|
|
134
|
+
if (affectedIds !== undefined)
|
|
135
|
+
resolution.affectedIds = affectedIds;
|
|
136
|
+
return resolution;
|
|
137
|
+
}
|
|
121
138
|
function readReview(value) {
|
|
122
139
|
if (value === undefined || value === null)
|
|
123
140
|
return { decision: 'unreviewed' };
|
|
@@ -133,6 +150,15 @@ function readReview(value) {
|
|
|
133
150
|
review.actor = actor;
|
|
134
151
|
if (at !== undefined)
|
|
135
152
|
review.at = at;
|
|
153
|
+
const noteRaw = readString(value.note, 'review.note');
|
|
154
|
+
if (noteRaw !== undefined) {
|
|
155
|
+
const trimmed = noteRaw.trim();
|
|
156
|
+
if (trimmed !== '')
|
|
157
|
+
review.note = trimmed.length > REVIEW_NOTE_MAX_LENGTH ? trimmed.slice(0, REVIEW_NOTE_MAX_LENGTH) : trimmed;
|
|
158
|
+
}
|
|
159
|
+
const resolution = readResolution(value.resolution);
|
|
160
|
+
if (resolution !== undefined)
|
|
161
|
+
review.resolution = resolution;
|
|
136
162
|
return review;
|
|
137
163
|
}
|
|
138
164
|
/** 当前内存模型 / 写盘的 lesson schemaVersion。 */
|
|
@@ -241,6 +267,7 @@ export function validateEvidence(value) {
|
|
|
241
267
|
const sessionId = readString(value.sessionId, 'evidence.sessionId', { nonEmpty: true });
|
|
242
268
|
const projectId = readString(value.projectId, 'evidence.projectId', { nonEmpty: true });
|
|
243
269
|
const sourceHash = readString(value.sourceHash, 'evidence.sourceHash', { nonEmpty: true });
|
|
270
|
+
const verificationReason = readString(value.verificationReason, 'evidence.verificationReason', { nonEmpty: true });
|
|
244
271
|
const recordSeqRaw = value.recordSeq;
|
|
245
272
|
let recordSeq;
|
|
246
273
|
if (recordSeqRaw !== undefined && recordSeqRaw !== null) {
|
|
@@ -267,6 +294,8 @@ export function validateEvidence(value) {
|
|
|
267
294
|
evidence.projectId = projectId;
|
|
268
295
|
if (sourceHash !== undefined)
|
|
269
296
|
evidence.sourceHash = sourceHash;
|
|
297
|
+
if (verificationReason !== undefined)
|
|
298
|
+
evidence.verificationReason = verificationReason;
|
|
270
299
|
return evidence;
|
|
271
300
|
}
|
|
272
301
|
/**
|
package/lib/mask.d.ts
CHANGED
|
@@ -10,4 +10,6 @@ export declare const SECRET_PATTERNS: Array<{
|
|
|
10
10
|
label: string;
|
|
11
11
|
}>;
|
|
12
12
|
/** 对文本做脱敏;无命中时原样返回。 */
|
|
13
|
-
export declare function maskSecrets(text: string): string;
|
|
13
|
+
export declare function maskSecrets(text: string, publicReferences?: readonly string[]): string;
|
|
14
|
+
/** 只认可插件生成的引用形状;不得把任意 *Id / *Hash 字段当作脱敏豁免。 */
|
|
15
|
+
export declare function isDreamReference(value: string): boolean;
|