fix: record an experience application only when it touches the same knowledge

This commit is contained in:
mesalogo
2026-10-02 19:08:05 +08:00
parent c3fa262832
commit 4d1ce38dad
3 changed files with 30 additions and 6 deletions
@@ -23,13 +23,18 @@ async function fixture() {
cleanups.push(async () => { sql.close(); db.close(); await rm(directory, { recursive: true, force: true }) })
const project = db.listProjects()[0]!
// Publishes events of one project in time order, then places them all in one feature story.
const publish = async (items: Array<[string, Kind]>, day: number, story = 'Review scheduling') => {
// Every event mentions the entity `topic`; an application must share an entity with the formation.
const publish = async (items: Array<[string, Kind]>, day: number, story = 'Review scheduling', topic = 'Long jobs') => {
const request = { trigger: 'manual' as const, scope: { kind: 'projects' as const, projectIds: [project.id] }, timeRange: { from: '2026-09-01T00:00:00Z', to: '2026-10-01T00:00:00Z' } }
const candidates = db.listSupervisionCandidates(request)
const known = candidates.find(entity => entity.label === topic)
db.saveSupervisionResult({
request: { trigger: 'manual', scope: { kind: 'projects', projectIds: [project.id] }, timeRange: { from: '2026-09-01T00:00:00Z', to: '2026-10-01T00:00:00Z' } },
request, candidates,
evidence: items.map(([title], index) => ({ id: `s${index}`, sourceType: 'conversation' as const, sourceId: 'c', title, content: title,
occurredAt: `2026-09-${day}T0${index}:00:00.000Z`, locator: { source: `message:${randomUUID()}`, revision: 'r', projectId: project.id, start: 0, end: title.length } })),
output: { summary: 'x', changeDigest: '', openItems: [], entities: [], entityChanges: [], relations: [],
events: items.map(([title, eventType], index) => ({ title, description: title, occurredAt: `2026-09-${day}T0${index}:00:00.000Z`, eventType, entityIds: [], sourceReferenceIds: [`s${index}`] })) }
output: { summary: 'x', changeDigest: '', openItems: [], entityChanges: [], relations: [],
entities: [{ id: 'topic', label: topic, description: topic, sourceReferenceIds: ['s0'], ...(known ? { persistedId: known.id } : {}) }],
events: items.map(([title, eventType], index) => ({ title, description: title, occurredAt: `2026-09-${day}T0${index}:00:00.000Z`, eventType, entityIds: ['topic'], sourceReferenceIds: [`s${index}`] })) }
})
const existing = db.supervisionStories().candidates(project.id, false).find(row => row.name === story)
await assignStories(db.supervisionStories(), async prompt => {
@@ -98,6 +103,12 @@ it('rejects invented evidence and saves nothing from a rejected answer; drops ap
await f.publish([['D', 'decision']], 19)
await extractExperiences(f.store, async () => JSON.stringify({ applications: [{ experience: 'x_1', event: 'e_1' }] }), options)
expect(f.store.list()[0]!.events.map(event => [event.role, event.title])).toEqual([['formed', 'B'], ['applied', 'C']])
// A later event about unrelated knowledge is not recorded as an application.
await f.publish([['Unrelated decision', 'decision']], 25, 'Review scheduling', 'Billing')
await expect(extractExperiences(f.store, async prompt => {
const ref = prompt.split('\n').find(line => line.includes('Unrelated decision'))!.match(/"ref":"(e_\d+)"/)![1]
return JSON.stringify({ applications: [{ experience: 'x_1', event: ref, note: 'loosely related' }] })
}, options)).resolves.toMatchObject({ applied: 0 })
})
it('protects edits, keeps removed experiences removed and undoes adjustments', async () => {
+12 -1
View File
@@ -139,9 +139,20 @@ export class SupervisionExperienceStore {
}
// An application must come after the experience formed; the real model sometimes cites earlier events. Those
// citations are dropped, the rest of the answer is kept.
// An application must also touch the same knowledge as the formation: share an entity with one of its
// formation events. The real model otherwise records loosely related events as applications.
const entitiesOf = this.db.prepare('SELECT entity_id FROM supervision_event_entities WHERE event_id = ?')
const formedEvents = new Map<string, string[]>()
for (const item of output.experiences) formedEvents.set(item.key, item.formed.map(ref => refs.get(ref)!.id))
const formedOf = this.db.prepare("SELECT event_id FROM supervision_experience_events WHERE experience_id = ? AND role = 'formed'")
for (const [ref, row] of xRefs) formedEvents.set(ref, formedOf.all(String(row.id)).map(item => String(item.event_id)))
const related = (experience: string, eventId: string) => {
const formed = new Set((formedEvents.get(experience) ?? []).flatMap(id => entitiesOf.all(id).map(item => String(item.entity_id))))
return entitiesOf.all(eventId).some(item => formed.has(String(item.entity_id)))
}
const timely = output.applications.filter(application => {
const since = formedAt.get(application.experience)
return !since || refs.get(application.event)!.at > since
return (!since || refs.get(application.event)!.at > since) && related(application.experience, refs.get(application.event)!.id)
})
const removed = this.db.prepare("SELECT id, statement FROM supervision_experiences WHERE status = 'removed'").all()
const removedStatements = new Set(removed.map(row => statementKey(String(row.statement))))