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:
@@ -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);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user