fix: record an experience application only when it touches the same knowledge
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
@@ -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))))
|
||||
|
||||
Reference in New Issue
Block a user