Implement Experience V1 reflect, retrieval boost, and product metrics.

Add rule-based reflect() to aggregate task outcomes, environment-aware search scoring, experience_saved/recalled events, and structured injection blocks per the V1 schema.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
john
2026-09-02 14:30:59 +08:00
parent 6d48480b1c
commit 2ec6fdbe11
11 changed files with 675 additions and 58 deletions
+70 -1
View File
@@ -58,7 +58,7 @@ function createMockPool() {
if (sql.includes('FROM h5_experience') && sql.includes('WHERE scope = ?')) {
const [scope] = params;
const matched = rows
.filter((r) => r.scope === scope && r.status === 'active')
.filter((r) => r.scope === scope && (sql.includes("status = 'active'") ? r.status === 'active' : true))
.sort((a, b) => b.updated_at - a.updated_at);
return [matched.map((r) => ({ ...r }))];
}
@@ -69,6 +69,18 @@ function createMockPool() {
}
return [{ affectedRows: params.length }];
}
if (sql.includes("SET status = 'archived'")) {
const [parentId, updatedAt, ...ids] = params;
for (const id of ids) {
const row = rows.find((r) => r.id === id);
if (row) {
row.status = 'archived';
row.parent_experience_id = parentId;
row.updated_at = updatedAt;
}
}
return [{ affectedRows: ids.length }];
}
throw new Error(`unexpected SQL: ${sql}`);
};
return { pool: { query }, rows };
@@ -176,3 +188,60 @@ test('search bumps use_count for returned rows', async () => {
await svc.search('caddy 灰度');
assert.equal(rows[0].use_count, 1);
});
test('search boosts environment component matches', async () => {
const { pool } = createMockPool();
const svc = createExperienceService(pool, { now: () => 10_000 });
await svc.record({
title: '通用标题',
body: '正文',
environment: { runtime: 'goosed', components: ['docker'] },
});
await svc.record({
title: '另一标题',
body: '正文',
problem: 'goosed docker 部署',
});
const hits = await svc.search('goosed docker', { limit: 1 });
assert.equal(hits.length, 1);
assert.equal(hits[0].title, '通用标题');
});
test('reflect aggregates repeated task outcomes into execution_pattern', async () => {
const { pool, rows } = createMockPool();
const svc = createExperienceService(pool, { now: () => 9000 });
for (let i = 0; i < 3; i += 1) {
await svc.record({
kind: 'task_outcome',
title: `SSE 修复 ${i}`,
body: `body ${i}`,
problem: 'SSE connection instability',
result: 'success',
confidence: 0.8,
});
}
const result = await svc.reflect({ minGroupSize: 3 });
assert.equal(result.ok, true);
assert.equal(result.created, 1);
assert.equal(result.archived, 3);
assert.equal(rows.filter((row) => row.kind === 'execution_pattern').length, 1);
assert.equal(rows.filter((row) => row.status === 'archived').length, 3);
});
test('reflect dryRun reports eligible groups without writing', async () => {
const { pool, rows } = createMockPool();
const svc = createExperienceService(pool, { now: () => 11_000 });
for (let i = 0; i < 3; i += 1) {
await svc.record({
kind: 'task_outcome',
title: `Deploy ${i}`,
body: `body ${i}`,
problem: 'headscale deploy',
result: 'success',
});
}
const preview = await svc.reflect({ minGroupSize: 3, dryRun: true });
assert.equal(preview.dryRun, true);
assert.equal(preview.groupsEligible, 1);
assert.equal(rows.filter((row) => row.kind === 'execution_pattern').length, 0);
});