dsh-lite-memory 0.18.33 → 0.18.35
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/README.md +4 -0
- package/bench/bench.mjs +26 -4
- package/docs/BENCH.md +13 -12
- package/docs/RELEASE-0.18.34.md +11 -0
- package/docs/RELEASE-0.18.35.md +11 -0
- package/index.js +173 -24
- package/learning.js +34 -8
- package/package.json +1 -1
- package/store.js +46 -30
- package/test/contract.test.mjs +15 -11
- package/test/crossing.test.mjs +35 -4
- package/test/layer-read.test.mjs +2 -2
- package/test/learning.test.mjs +87 -0
- package/test/recall.test.mjs +243 -2
- package/test/runtime-context.test.mjs +4 -0
- package/test/store.test.mjs +20 -4
- package/view.js +52 -11
package/store.js
CHANGED
|
@@ -2595,16 +2595,16 @@ export function routeScope(draft, options = {}) {
|
|
|
2595
2595
|
requiredConditions: [{ key: 'machine', value: '' }, { key: 'runtime', value: '' }],
|
|
2596
2596
|
};
|
|
2597
2597
|
};
|
|
2598
|
-
/** 人明确点过「就存全局」才算数;模型自己提的
|
|
2598
|
+
/** 人明确点过「就存全局」才算数;模型自己提的 scope 只是提案。 */
|
|
2599
2599
|
const humanConfirmed = options.humanScope === true || source.scopeConfirmed === true;
|
|
2600
|
+
/** 实验/自动整理的产物永远留在项目层:它们没有「用户要求跨项目」这层依据。 */
|
|
2601
|
+
const autoOrigin = source.origin === 'project_evolution' || source.origin === 'model_drafted';
|
|
2600
2602
|
/** 「本项目里…」是项目事实的措辞,类别写成偏好/流程也不能这样进通用层。 */
|
|
2601
2603
|
const projectBound = projectBoundText(source);
|
|
2602
2604
|
if (durability === 'task') return { ...base, needsScope: true, reasonCode: 'task_state' };
|
|
2603
|
-
if (explicit) {
|
|
2605
|
+
if (explicit && (humanConfirmed || !autoOrigin)) {
|
|
2604
2606
|
/**
|
|
2605
|
-
*
|
|
2606
|
-
* 所以不能再按模型自荐降级,只有本机痕迹和项目事实两种硬冲突能改落点。
|
|
2607
|
-
* 没显式 scope 时才用 memoryClass 提案,缺可搬依据的流程留在项目层。
|
|
2607
|
+
* 只有人确认过的 scope 才代表「用户这个消息里要的那一层」;模型写进参数的 scope 只是提案。
|
|
2608
2608
|
*/
|
|
2609
2609
|
if (explicit === 'global' && !humanConfirmed) {
|
|
2610
2610
|
if (machineConflict) return fallbackToProject('machine_bound_scope');
|
|
@@ -2631,21 +2631,28 @@ export function routeScope(draft, options = {}) {
|
|
|
2631
2631
|
}
|
|
2632
2632
|
return { ...base, chosenScope: 'project', reasonCode: 'class_project_fact' };
|
|
2633
2633
|
}
|
|
2634
|
+
/**
|
|
2635
|
+
* 自动归类只能决定「它像哪类」,不能决定跨项目;默认落点一律是当前项目。
|
|
2636
|
+
* 具体冲突(本机痕迹、「本项目」措辞)仍按原原因报告,其余全局结论统一降级。
|
|
2637
|
+
*/
|
|
2638
|
+
const globalOutcome = (reasonCode) => (humanConfirmed
|
|
2639
|
+
? { ...base, chosenScope: 'global', reasonCode }
|
|
2640
|
+
: fallbackToProject('model_class_project'));
|
|
2634
2641
|
if (cls === 'personal_preference') {
|
|
2635
2642
|
/** 偏好也分「我的习惯」和「这台机器上的习惯」:后者绑机器,不进通用层。 */
|
|
2636
2643
|
if (machineConflict) return fallbackToProject('machine_bound_preference');
|
|
2637
2644
|
/** 说了「本项目」的偏好其实是项目约定,类别写错也不能跨项目。 */
|
|
2638
2645
|
if (projectBound) return fallbackToProject('project_bound_preference');
|
|
2639
|
-
return
|
|
2646
|
+
return globalOutcome('class_personal_preference');
|
|
2640
2647
|
}
|
|
2641
2648
|
if (cls === 'portable_procedure') {
|
|
2642
2649
|
if (machineConflict) return fallbackToProject('machine_bound_procedure');
|
|
2643
2650
|
if (projectBound) return fallbackToProject('project_bound_procedure');
|
|
2644
2651
|
/** 带适用环境条件的本机流程:条件成立时才用,换台机器自然不生效。 */
|
|
2645
|
-
if (envScoped) return
|
|
2652
|
+
if (envScoped) return globalOutcome('machine_scoped_procedure');
|
|
2646
2653
|
/** 说能搬去别的项目,就要有能搬的依据;没有就先留在本项目。 */
|
|
2647
2654
|
if (!evidence) return fallbackToProject('unbound_evidence');
|
|
2648
|
-
return
|
|
2655
|
+
return globalOutcome('class_portable_procedure');
|
|
2649
2656
|
}
|
|
2650
2657
|
if (projectBound) return fallbackToProject('project_bound_environment');
|
|
2651
2658
|
const conditions = view.normalizeConditions(source.conditions) || [];
|
|
@@ -2658,7 +2665,7 @@ export function routeScope(draft, options = {}) {
|
|
|
2658
2665
|
requiredConditions: [{ key: 'machine', value: '' }, { key: 'runtime', value: '' }],
|
|
2659
2666
|
};
|
|
2660
2667
|
}
|
|
2661
|
-
return
|
|
2668
|
+
return globalOutcome('class_environment_fact');
|
|
2662
2669
|
}
|
|
2663
2670
|
|
|
2664
2671
|
function locateInLayers(projectScope, id) {
|
|
@@ -2738,7 +2745,7 @@ function bindSameTopic(projectScope, ref) {
|
|
|
2738
2745
|
const CONFLICT_LIMIT = 20;
|
|
2739
2746
|
|
|
2740
2747
|
/** 待办里留一份可重放的草稿:面板能直接另存,不用用户回忆刚才想记什么。 */
|
|
2741
|
-
const CONFLICT_DRAFT_KEYS = ['title', 'summary', 'body', 'when', 'kind', 'tags', 'conditions', 'applicability', 'topicKey', 'source', 'origin'];
|
|
2748
|
+
const CONFLICT_DRAFT_KEYS = ['title', 'summary', 'body', 'when', 'kind', 'tags', 'conditions', 'applicability', 'selection', 'triggers', 'links', 'topicKey', 'source', 'origin'];
|
|
2742
2749
|
function conflictDraftOf(draft) {
|
|
2743
2750
|
const out = {};
|
|
2744
2751
|
if (!draft || typeof draft !== 'object') return out;
|
|
@@ -2754,6 +2761,10 @@ function conflictDraftOf(draft) {
|
|
|
2754
2761
|
else if (key === 'conditions') out.conditions = Array.isArray(value) ? value.slice(0, 8) : undefined;
|
|
2755
2762
|
else if (key === 'applicability') out.applicability = value;
|
|
2756
2763
|
else if (key === 'topicKey') out.topicKey = normalizeText(value, 80);
|
|
2764
|
+
/** 适用范围、召回触发词与关联整份重放:经过待裁决不能改变语义。 */
|
|
2765
|
+
else if (key === 'selection') out.selection = value && typeof value === 'object' && !Array.isArray(value) ? { ...value } : null;
|
|
2766
|
+
else if (key === 'triggers') out.triggers = Array.isArray(value) ? value.slice(0, 8).map((item) => normalizeText(item, 40)).filter(Boolean) : [];
|
|
2767
|
+
else if (key === 'links') out.links = Array.isArray(value) ? value.slice(0, 3).map((item) => (item && typeof item === 'object' ? { ...item } : null)).filter(Boolean) : [];
|
|
2757
2768
|
else if (key === 'source') out.source = value;
|
|
2758
2769
|
else if (key === 'origin') out.origin = value;
|
|
2759
2770
|
}
|
|
@@ -2821,30 +2832,35 @@ function staleDraft(row, ref) {
|
|
|
2821
2832
|
|
|
2822
2833
|
/** 确认另存:用队列里的草稿按 distinct 重放一次,成功后按 id 出队。 */
|
|
2823
2834
|
export function resolveConflict(scope, ref, options = {}) {
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2835
|
+
/** 版本检查、另存和出队必须在同一临界区:中间被新反馈改写时,旧面板不能批准新草稿。 */
|
|
2836
|
+
return withLocks([scope, globalScope()], () => {
|
|
2837
|
+
const row = conflictRowLocked(scope, ref);
|
|
2838
|
+
if (!row) return { ok: false, reason: '这条待裁决已经不在了,刷新面板看看' };
|
|
2839
|
+
if (staleDraft(row, ref)) return { ok: false, reasonCode: 'stale_draft', reason: '这条待裁决又更新了,刷新后再处理' };
|
|
2840
|
+
const draft = { ...(row.draft || {}), resolution: 'distinct', writeMode: options.writeMode === 'append' ? 'append' : 'patch' };
|
|
2841
|
+
if (scope.key === 'global') draft.scope = 'global';
|
|
2842
|
+
else draft.scope = 'project';
|
|
2843
|
+
if (!draft.title) draft.title = row.title;
|
|
2844
|
+
const result = saveEntry(scope, draft);
|
|
2845
|
+
if (result && result.needsResolution) return { ok: false, reason: '还是相近:换一个标题或等确认', result };
|
|
2846
|
+
if (result && result.needsScope) {
|
|
2847
|
+
const reason = result.reasonCode === 'memory_disabled' ? '这层记忆已关闭' : '这条记忆的适用范围还没定,先说明当前环境';
|
|
2848
|
+
return { ok: false, reason, result };
|
|
2849
|
+
}
|
|
2850
|
+
clearConflictLocked(scope, { id: row.id });
|
|
2851
|
+
return { ok: true, result, record: result && result.record };
|
|
2852
|
+
});
|
|
2839
2853
|
}
|
|
2840
2854
|
|
|
2841
2855
|
/** 删掉待裁决:用户决定不再另存。 */
|
|
2842
2856
|
export function dropConflict(scope, ref) {
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2857
|
+
return withLocks([scope, globalScope()], () => {
|
|
2858
|
+
const row = conflictRowLocked(scope, ref);
|
|
2859
|
+
if (!row) return { ok: false, reason: '这条待裁决已经不在了,刷新面板看看' };
|
|
2860
|
+
if (staleDraft(row, ref)) return { ok: false, reasonCode: 'stale_draft', reason: '这条待裁决又更新了,刷新后再处理' };
|
|
2861
|
+
clearConflictLocked(scope, { id: row.id });
|
|
2862
|
+
return { ok: true };
|
|
2863
|
+
});
|
|
2848
2864
|
}
|
|
2849
2865
|
|
|
2850
2866
|
/** 当前层还有几条没处理的冲突。 */
|
package/test/contract.test.mjs
CHANGED
|
@@ -377,10 +377,15 @@ assert.equal((await call('/lite-memory/unknown', { cwd })).status, 404);
|
|
|
377
377
|
assert.equal((await call('/lite-memory/keep', {}, { method: 'GET' })).status, 405);
|
|
378
378
|
|
|
379
379
|
// ── global layer ─────────────────────────────────────────────────────────────
|
|
380
|
-
const
|
|
381
|
-
assert.equal(
|
|
382
|
-
assert.
|
|
383
|
-
|
|
380
|
+
const proposedGlobal = await exec('memory_save', { title: '回复语言模型提案', summary: '模型建议回复语言用中文', scope: 'global' });
|
|
381
|
+
assert.equal(proposedGlobal.scope, 'project', '模型自填 scope 只是提案,默认落项目层');
|
|
382
|
+
assert.match(proposedGlobal.scopeNote, /模型归类只是提案/, '降级要能解释');
|
|
383
|
+
const global = store.saveEntry(store.globalScope(), {
|
|
384
|
+
title: '回复用中文', summary: '用户要求回复用中文', body: '回复默认用中文', scopeConfirmed: true,
|
|
385
|
+
});
|
|
386
|
+
assert.ok(global.record, '人明确要全局时才落全局层');
|
|
387
|
+
assert.equal(global.chosenScope, 'global');
|
|
388
|
+
assert.equal(exec === null, false);
|
|
384
389
|
|
|
385
390
|
// 显式写 global 也不再被悄无声息地改层:本机痕迹和项目事实各有一条能解释的降级。
|
|
386
391
|
const machineGlobal = await exec('memory_save', {
|
|
@@ -402,7 +407,7 @@ const factGlobal = await exec('memory_save', {
|
|
|
402
407
|
assert.equal(factGlobal.scope, 'project');
|
|
403
408
|
assert.match(factGlobal.scopeNote, /按项目事实留在项目层/);
|
|
404
409
|
const globalSearch = await exec('memory_search', { query: '回复用中文' });
|
|
405
|
-
assert.
|
|
410
|
+
assert.ok(globalSearch.count >= 1, '全局层条目能被搜到');
|
|
406
411
|
assert.match(render('memory_search', {}, globalSearch), /\[global\] 「/);
|
|
407
412
|
assert.equal(typeof globalThis.__liteMemoryStats, 'function');
|
|
408
413
|
const stats = globalThis.__liteMemoryStats(cwd);
|
|
@@ -1050,10 +1055,9 @@ await defs.get('memory_save').execute(
|
|
|
1050
1055
|
{ title: '发布窗口', summary: '只在周四晚上发版', body: '周四晚上再发版。', when: '周四晚上' },
|
|
1051
1056
|
{ agent: ovAgent },
|
|
1052
1057
|
);
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
);
|
|
1058
|
+
store.saveEntry(store.globalScope(), {
|
|
1059
|
+
title: '观察全局', summary: '跨项目通用说明', body: '全局正文。', scopeConfirmed: true,
|
|
1060
|
+
});
|
|
1057
1061
|
const ovOther = await defs.get('memory_save').execute(
|
|
1058
1062
|
{ title: '别的项目', summary: '只在别的项目里', body: '另一层的正文。', tags: ['别的'] },
|
|
1059
1063
|
{ agent: { id: 'session-overview-other', cwd: ovOtherCwd, session: { id: 'session-overview-other', header: { cwd: ovOtherCwd } } } },
|
|
@@ -1215,7 +1219,7 @@ const cfCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'lite-memory-contract-confli
|
|
|
1215
1219
|
sessionCwds.set('session-conflict-panel', cfCwd);
|
|
1216
1220
|
sessionCwds.set('session-conflict-other', fs.mkdtempSync(path.join(os.tmpdir(), 'lite-memory-contract-conflict-other-')));
|
|
1217
1221
|
const cfAgent = { id: 'session-conflict-panel', cwd: cfCwd, session: { id: 'session-conflict-panel', header: { cwd: cfCwd } } };
|
|
1218
|
-
|
|
1222
|
+
store.saveEntry(store.globalScope(), { title: '发布窗口', summary: '只在周四晚上发版', body: '周四晚上再发版。', scopeConfirmed: true });
|
|
1219
1223
|
const cfSave = await defs.get('memory_save').execute({ title: '发布窗口', summary: '本项目周五发版', body: '周五发版。' }, { agent: cfAgent });
|
|
1220
1224
|
assert.equal(cfSave.needsResolution, true);
|
|
1221
1225
|
assert.equal(store.loadIndex(store.scopeOf(cfCwd)).entries.length, 0, '没确认前不落库');
|
|
@@ -1276,7 +1280,7 @@ const cfgCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'lite-memory-contract-confl
|
|
|
1276
1280
|
sessionCwds.set('session-conflict-global', cfgCwd);
|
|
1277
1281
|
const cfgAgent = { id: 'session-conflict-global', cwd: cfgCwd, session: { id: 'session-conflict-global', header: { cwd: cfgCwd } } };
|
|
1278
1282
|
await defs.get('memory_save').execute({ title: '命名规则', summary: '项目里用短横线', body: '短横线。' }, { agent: cfgAgent });
|
|
1279
|
-
const cfgSave =
|
|
1283
|
+
const cfgSave = store.saveEntry(store.scopeOf(cfgCwd), { title: '命名规则', summary: '所有项目用下划线', body: '下划线。', scope: 'global', scopeConfirmed: true });
|
|
1280
1284
|
assert.equal(cfgSave.needsResolution, true);
|
|
1281
1285
|
const cfgView = await call('/lite-memory/overview', { sessionId: 'session-conflict-global', scopeKey: 'global' });
|
|
1282
1286
|
assert.equal(cfgView.body.selected.conflicts.length, 1, '全局层也要有这条待裁决');
|
package/test/crossing.test.mjs
CHANGED
|
@@ -44,12 +44,18 @@ const idsOfPrev = (scope) => {
|
|
|
44
44
|
};
|
|
45
45
|
|
|
46
46
|
// ── S03 范围路由 ────────────────────────────────────────────────────────────
|
|
47
|
-
await test('S03
|
|
47
|
+
await test('S03 个人偏好:模型自动归类留项目层,人明确要全局才落全局', () => {
|
|
48
48
|
const scope = fresh('route-pref');
|
|
49
|
-
const
|
|
49
|
+
const automatic = saveEntry(scope, { title: '回答用中文(自动)', summary: '回复语言偏好', body: '中文、简短(自动)', memoryClass: 'personal_preference' });
|
|
50
|
+
assert.equal(automatic.chosenScope, 'project', '模型归类只是提案,不能自己跨项目');
|
|
51
|
+
assert.equal(automatic.reasonCode, 'model_class_project');
|
|
52
|
+
assert.equal(idsOf(scope).length, 1);
|
|
53
|
+
const saved = saveEntry(scope, {
|
|
54
|
+
title: '回答用中文', summary: '回复语言偏好', body: '中文、简短',
|
|
55
|
+
memoryClass: 'personal_preference', scopeConfirmed: true, resolution: 'distinct',
|
|
56
|
+
});
|
|
50
57
|
assert.equal(saved.chosenScope, 'global');
|
|
51
58
|
assert.equal(saved.reasonCode, 'class_personal_preference');
|
|
52
|
-
assert.equal(idsOf(scope).length, 0);
|
|
53
59
|
assert.equal(stateOf(globalScope()).entries.filter((row) => row.current.title === '回答用中文').length, 1);
|
|
54
60
|
});
|
|
55
61
|
|
|
@@ -100,7 +106,7 @@ await test('S03 设备事实没给设备条件就返回 needsScope,不落盘',
|
|
|
100
106
|
await test('S03 设备事实带条件进全局层且是条件适用', () => {
|
|
101
107
|
const scope = fresh('route-env-ok');
|
|
102
108
|
const saved = saveEntry(scope, {
|
|
103
|
-
title: '本机 Node', summary: '本机 Node 版本', body: 'v24', memoryClass: 'environment_fact',
|
|
109
|
+
title: '本机 Node', summary: '本机 Node 版本', body: 'v24', memoryClass: 'environment_fact', scopeConfirmed: true,
|
|
104
110
|
conditions: [{ key: 'machine', value: 'mac-mini' }],
|
|
105
111
|
});
|
|
106
112
|
assert.equal(saved.chosenScope, 'global');
|
|
@@ -434,7 +440,32 @@ await test('S12 待裁决留可重放草稿,面板确认另存后才落库', (
|
|
|
434
440
|
assert.equal(pendingConflictsOf(scope), 0);
|
|
435
441
|
assert.equal(loadIndex(scope).entries.length, 1);
|
|
436
442
|
assert.equal(loadIndex(scope).entries[0].body, '项目补充');
|
|
443
|
+
assert.equal(loadIndex(scope).entries[0].when, '本仓库', 'when 经过待裁决不能丢');
|
|
437
444
|
assert.equal(dropConflict(scope, '不存在').ok, false);
|
|
438
445
|
});
|
|
439
446
|
|
|
447
|
+
await test('S12 经过待裁决另存不改变适用范围、触发词和关联', () => {
|
|
448
|
+
const scope = fresh('conflict-semantics');
|
|
449
|
+
const globalOne = saveEntry(globalScope(), { title: '发票抬头规则', summary: '开票信息固定', body: '全局版本', resolution: 'distinct' });
|
|
450
|
+
const linked = saveEntry(globalScope(), { title: '生效核验顺序', summary: '先验规则再验页面', body: '先验规则', resolution: 'distinct' });
|
|
451
|
+
const near = saveEntry(scope, {
|
|
452
|
+
title: '发票抬头规则', summary: '本项目发票只认这套抬头', body: '项目版本',
|
|
453
|
+
selection: { mode: 'complete', artifactKey: 'artifact-invoice-v1' },
|
|
454
|
+
triggers: ['artifact-hookword'],
|
|
455
|
+
links: [{ type: 'related', id: linked.record.id }],
|
|
456
|
+
topicKey: 'invoice',
|
|
457
|
+
});
|
|
458
|
+
assert.equal(near.needsResolution, true);
|
|
459
|
+
const [row] = conflictsOf(scope);
|
|
460
|
+
assert.deepEqual(row.draft.selection, { mode: 'complete', artifactKey: 'artifact-invoice-v1', items: [] });
|
|
461
|
+
assert.deepEqual(row.draft.triggers, ['artifact-hookword']);
|
|
462
|
+
assert.deepEqual(row.draft.links, [{ type: 'related', id: linked.record.id }]);
|
|
463
|
+
const kept = resolveConflict(scope, { id: row.id, expectedRevision: row.revision });
|
|
464
|
+
assert.equal(kept.ok, true, JSON.stringify(kept));
|
|
465
|
+
const [record] = loadIndex(scope).entries;
|
|
466
|
+
assert.deepEqual(record.selection, { mode: 'complete', artifactKey: 'artifact-invoice-v1', items: [] });
|
|
467
|
+
assert.deepEqual(record.triggers, ['artifact-hookword']);
|
|
468
|
+
assert.deepEqual(record.links, [{ type: 'related', id: linked.record.id }]);
|
|
469
|
+
});
|
|
470
|
+
|
|
440
471
|
console.log(`crossing.test.mjs: ${passed} passed`);
|
package/test/layer-read.test.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { fileURLToPath } from 'node:url'
|
|
1
|
+
import { fileURLToPath } from 'node:url'
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
@@ -172,7 +172,7 @@ const globalDraft = store.saveEntry(store.scopeOf(projectB), {
|
|
|
172
172
|
title: 'npm 发布凭据取用与发布后验版',
|
|
173
173
|
summary: 'npm 发布用 account regrevia,发布后轮询 dist-tags 验版',
|
|
174
174
|
body: '发布用 regrevia 身份;发完轮询 dist-tags.latest 判上线。',
|
|
175
|
-
memoryClass: 'personal_preference',
|
|
175
|
+
memoryClass: 'personal_preference', scope: 'global', scopeConfirmed: true,
|
|
176
176
|
});
|
|
177
177
|
assert.equal(globalDraft.chosenScope, 'global');
|
|
178
178
|
const near = await exec('memory_save', {
|
package/test/learning.test.mjs
CHANGED
|
@@ -1036,6 +1036,54 @@ test('L03d2 代码任务学到的规则不会在数据库任务里生效', () =>
|
|
|
1036
1036
|
assert.equal(db.reason, 'condition_mismatch');
|
|
1037
1037
|
});
|
|
1038
1038
|
|
|
1039
|
+
test('L03d3 空项目的第一条反馈能带上真实任务范围保存', () => {
|
|
1040
|
+
const parsed = learning.parseOutput(JSON.stringify({
|
|
1041
|
+
changes: [{
|
|
1042
|
+
target: 'new', feedbackType: 'spec_change', topicKey: '前端边框',
|
|
1043
|
+
currentText: '前端页面默认采用细边框', applicability: 'all_in_scope', evidence: ['u0'],
|
|
1044
|
+
}],
|
|
1045
|
+
}), {
|
|
1046
|
+
finish: { kind: 'stop' }, refs: { feedback: { u0: 'm1' }, rules: {} }, rules: [],
|
|
1047
|
+
ranges: { taskKinds: [], series: [], projects: ['p'] },
|
|
1048
|
+
expectedTarget: { scope: 'episode', projectId: 'p', episodeId: 'ep1', taskKind: 'code', phase: 'brief' },
|
|
1049
|
+
});
|
|
1050
|
+
assert.equal(parsed.ok, true, parsed.reason);
|
|
1051
|
+
assert.equal(parsed.changes[0].applicability, 'conditional');
|
|
1052
|
+
assert.deepEqual(parsed.changes[0].conditions, [{ key: 'taskKind', value: 'code' }], '采集阶段不该锁死长期适用范围');
|
|
1053
|
+
});
|
|
1054
|
+
|
|
1055
|
+
test('L03d4 模型条件与采集任务范围冲突时拒绝,不生成永不生效的规则', () => {
|
|
1056
|
+
const parsed = learning.parseOutput(JSON.stringify({
|
|
1057
|
+
changes: [{
|
|
1058
|
+
target: 'new', feedbackType: 'spec_change', topicKey: '迁移',
|
|
1059
|
+
currentText: '先跑迁移再改代码', applicability: 'conditional',
|
|
1060
|
+
conditions: [{ key: 'taskKind', value: 'database' }], evidence: ['u0'],
|
|
1061
|
+
}],
|
|
1062
|
+
}), {
|
|
1063
|
+
finish: { kind: 'stop' }, refs: { feedback: { u0: 'm1' }, rules: {} }, rules: [],
|
|
1064
|
+
ranges: { ...RANGES, taskKinds: ['code', 'database'] },
|
|
1065
|
+
expectedTarget: { scope: 'episode', projectId: 'p', episodeId: 'ep1', taskKind: 'code' },
|
|
1066
|
+
});
|
|
1067
|
+
assert.equal(parsed.ok, false);
|
|
1068
|
+
assert.match(parsed.reason, /冲突/);
|
|
1069
|
+
});
|
|
1070
|
+
|
|
1071
|
+
test('L03d5 同一条件键的两个值不能都留下', () => {
|
|
1072
|
+
const parsed = learning.parseOutput(JSON.stringify({
|
|
1073
|
+
changes: [{
|
|
1074
|
+
target: 'new', feedbackType: 'spec_change', topicKey: '阶段冲突',
|
|
1075
|
+
currentText: '只在脚本阶段生效', applicability: 'conditional',
|
|
1076
|
+
conditions: [{ key: 'phase', value: 'script' }, { key: 'phase', value: 'brief' }], evidence: ['u0'],
|
|
1077
|
+
}],
|
|
1078
|
+
}), {
|
|
1079
|
+
finish: { kind: 'stop' }, refs: { feedback: { u0: 'm1' }, rules: {} }, rules: [],
|
|
1080
|
+
ranges: { ...RANGES },
|
|
1081
|
+
expectedTarget: { scope: 'episode', projectId: 'p', episodeId: 'ep1', taskKind: 'code', phase: 'brief' },
|
|
1082
|
+
});
|
|
1083
|
+
assert.equal(parsed.ok, false);
|
|
1084
|
+
assert.match(parsed.reason, /条件/);
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1039
1087
|
test('L03e 先按相关性排序再截断,输入顺序不挤掉真正相关的旧规则', () => {
|
|
1040
1088
|
const rules = [];
|
|
1041
1089
|
for (let index = 0; index < 32; index += 1) {
|
|
@@ -1360,4 +1408,43 @@ test('L10 整理器规则先稳定排序再取 8', () => {
|
|
|
1360
1408
|
assert.equal(learning.orderOrganizerRules([], { limit: 3 }).length, 0);
|
|
1361
1409
|
});
|
|
1362
1410
|
|
|
1411
|
+
/* 收敛:连续纠正同一主题只留一条现行规则;换栏目不适用;关掉再开还是同一现行版。 */
|
|
1412
|
+
test('B30 连续纠正同一规则收敛到一条现行版', () => {
|
|
1413
|
+
const scope = fresh('converge');
|
|
1414
|
+
enable(scope);
|
|
1415
|
+
const firstEpisode = seedEpisode(scope, { sessionId: 'conv1', messageIds: ['cm1'] });
|
|
1416
|
+
const firstStart = begin(scope, firstEpisode);
|
|
1417
|
+
assert.equal(commitNew(scope, firstEpisode, firstStart, NEW_CHANGE).ok, true);
|
|
1418
|
+
const id = ruleRows(scope)[0].id;
|
|
1419
|
+
const bodies = ['开头两句内给结论。', '开头一句内给结论。'];
|
|
1420
|
+
bodies.forEach((body, index) => {
|
|
1421
|
+
const episode = seedEpisode(scope, { sessionId: `conv${index + 2}`, messageIds: [`cm${index + 2}`] });
|
|
1422
|
+
const started = begin(scope, episode);
|
|
1423
|
+
assert.equal(started.ok, true, started.reason);
|
|
1424
|
+
const result = commitExisting(scope, episode, started, id, {
|
|
1425
|
+
target: 'r0', feedbackType: 'spec_change', currentText: body, evidence: ['u0'],
|
|
1426
|
+
});
|
|
1427
|
+
assert.equal(result.ok, true, result.reason);
|
|
1428
|
+
});
|
|
1429
|
+
const rows = ruleRows(scope);
|
|
1430
|
+
assert.equal(rows.length, 1, '同一主题不堆多份现行规则');
|
|
1431
|
+
const current = rows[0];
|
|
1432
|
+
assert.equal(current.id, id);
|
|
1433
|
+
assert.match(current.body, /一句内给结论/);
|
|
1434
|
+
assert.ok((current.versions || []).length <= 2, '旧版有上限');
|
|
1435
|
+
const wrongColumn = view.eligibility(current, {
|
|
1436
|
+
projectId: scope.key,
|
|
1437
|
+
context: { seriesId: '字幕' },
|
|
1438
|
+
task: { taskKind: 'video', seriesId: '字幕' },
|
|
1439
|
+
}, 'auto');
|
|
1440
|
+
assert.equal(wrongColumn.ok, false, '换栏目不适用');
|
|
1441
|
+
store.setMemoryEnabled(scope, false, { operationId: 'conv-close' });
|
|
1442
|
+
assert.equal(ruleRows(scope).length, 1, '关闭不清成果');
|
|
1443
|
+
store.setMemoryEnabled(scope, true, { operationId: 'conv-open' });
|
|
1444
|
+
const reopened = ruleOf(scope, id);
|
|
1445
|
+
assert.equal(reopened.body, current.body);
|
|
1446
|
+
assert.equal(reopened.revision, current.revision);
|
|
1447
|
+
assert.equal(reopened.learning.state, current.learning.state);
|
|
1448
|
+
});
|
|
1449
|
+
|
|
1363
1450
|
console.log(`learning.test.mjs: ${passed} passed`);
|