Reevaluate existing evidence when rules change
This commit is contained in:
@@ -250,6 +250,37 @@ suite('normalized level persistence API', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('evaluates existing documents when an evidence fingerprint is added later', async () => {
|
||||||
|
const created = await adminFetch(`${baseUrl}/api/levels`, {
|
||||||
|
method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({ id:'late-evidence-rule',title:'Late evidence rule' }),
|
||||||
|
})
|
||||||
|
expect(created.status).toBe(201)
|
||||||
|
const level = await created.json() as CaseState
|
||||||
|
const upload = new FormData()
|
||||||
|
upload.append('file',new Blob(['Archive patent 93585 names Niels Aall Baricelli and describes a Koffert-kommode.'],{ type:'text/plain' }),'patent.txt')
|
||||||
|
const uploaded = await (await fetch(`${baseUrl}/api/levels/${level.id}/documents`,{ method:'POST',body:upload })).json() as DocumentExhibit & {
|
||||||
|
analysis:{ matchedFlags:string[] }
|
||||||
|
}
|
||||||
|
expect(uploaded.analysis.matchedFlags).toEqual([])
|
||||||
|
|
||||||
|
const rule = await adminFetch(`${baseUrl}/api/levels/${level.id}/evidence-match-rules`,{
|
||||||
|
method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({
|
||||||
|
name:'Late National Library fingerprint',flagKey:'late.patent-recognized',minimumAnchorMatches:2,
|
||||||
|
anchors:[
|
||||||
|
{ phrase:'Archive patent 93585 names Niels Aall Baricelli',minimumSimilarity:.7 },
|
||||||
|
{ phrase:'describes a Koffert-kommode',minimumSimilarity:.7 },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
expect(rule.status).toBe(201)
|
||||||
|
expect(await (await adminFetch(`${baseUrl}/api/levels/${level.id}/flags`)).json()).toEqual([
|
||||||
|
expect.objectContaining({ key:'late.patent-recognized',earnedAt:expect.any(String) }),
|
||||||
|
])
|
||||||
|
const evaluation = await appPool.query<{ matched:boolean;matched_anchor_count:number }>(
|
||||||
|
'SELECT matched,matched_anchor_count FROM osint.evidence_match_evaluations WHERE document_exhibit_id=$1',[uploaded.id])
|
||||||
|
expect(evaluation.rows).toEqual([{ matched:true,matched_anchor_count:2 }])
|
||||||
|
})
|
||||||
|
|
||||||
it('imports the data-defined Scene 7 template with its private recognition rules', async () => {
|
it('imports the data-defined Scene 7 template with its private recognition rules', async () => {
|
||||||
const { importMysteryTemplate } = await import('../scripts/importMysteryTemplate.js')
|
const { importMysteryTemplate } = await import('../scripts/importMysteryTemplate.js')
|
||||||
const manifestPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'mysteries', 'barricelli-scene-7', 'mystery.json')
|
const manifestPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'mysteries', 'barricelli-scene-7', 'mystery.json')
|
||||||
|
|||||||
@@ -215,8 +215,41 @@ export function createLevelRepository(pool: Pool, editingEnabled: boolean, objec
|
|||||||
for (const [sortOrder, anchor] of input.anchors.entries()) await client.query(`INSERT INTO osint.evidence_match_anchors
|
for (const [sortOrder, anchor] of input.anchors.entries()) await client.query(`INSERT INTO osint.evidence_match_anchors
|
||||||
(id,rule_id,phrase_text,minimum_similarity,sort_order) VALUES ($1,$2,$3,$4,$5)`,
|
(id,rule_id,phrase_text,minimum_similarity,sort_order) VALUES ($1,$2,$3,$4,$5)`,
|
||||||
[randomUUID(), ruleId, anchor.phrase, anchor.minimumSimilarity, sortOrder])
|
[randomUUID(), ruleId, anchor.phrase, anchor.minimumSimilarity, sortOrder])
|
||||||
|
const rule = (await evidenceMatchRules(client, level.board_id, true)).find(candidate => candidate.id === ruleId) || null
|
||||||
|
if (rule) await evaluateRuleForExistingDocuments(client, level, rule)
|
||||||
await client.query('UPDATE osint.levels SET updated_at=NOW() WHERE id=$1', [level.id])
|
await client.query('UPDATE osint.levels SET updated_at=NOW() WHERE id=$1', [level.id])
|
||||||
return (await evidenceMatchRules(client, level.board_id, true)).find(rule => rule.id === ruleId) || null
|
return rule
|
||||||
|
}
|
||||||
|
|
||||||
|
async function evaluateRuleForExistingDocuments(client: PoolClient, level: LevelRow, rule: EvidenceMatchRuleDefinition) {
|
||||||
|
if (!rule.enabled) return
|
||||||
|
const documents = await client.query<{ document_exhibit_id:string;extraction_id:string;extracted_text:string }>(`
|
||||||
|
SELECT DISTINCT ON (document.exhibit_id) document.exhibit_id AS document_exhibit_id,
|
||||||
|
extraction.id AS extraction_id,extraction.extracted_text
|
||||||
|
FROM osint.document_exhibits document
|
||||||
|
JOIN osint.exhibits exhibit ON exhibit.id=document.exhibit_id AND exhibit.board_id=$1
|
||||||
|
JOIN osint.asset_text_extractions extraction ON extraction.asset_id=document.asset_id
|
||||||
|
AND extraction.status='succeeded' AND BTRIM(extraction.extracted_text)<>''
|
||||||
|
ORDER BY document.exhibit_id,extraction.updated_at DESC,extraction.id DESC`, [level.board_id])
|
||||||
|
for (const document of documents.rows) {
|
||||||
|
const evaluation = evaluateEvidenceRules(document.extracted_text, [rule as EvidenceMatchRule])[0]
|
||||||
|
const persisted = await client.query<{ id:string }>(`INSERT INTO osint.evidence_match_evaluations
|
||||||
|
(id,level_id,board_id,document_exhibit_id,extraction_id,rule_id,matched,matched_anchor_count,score)
|
||||||
|
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)
|
||||||
|
ON CONFLICT (level_id,document_exhibit_id,rule_id) DO UPDATE SET
|
||||||
|
extraction_id=EXCLUDED.extraction_id,matched=EXCLUDED.matched,
|
||||||
|
matched_anchor_count=EXCLUDED.matched_anchor_count,score=EXCLUDED.score,evaluated_at=NOW()
|
||||||
|
RETURNING id`, [randomUUID(),level.id,level.board_id,document.document_exhibit_id,document.extraction_id,
|
||||||
|
rule.id,evaluation.matched,evaluation.matchedAnchorCount,evaluation.score])
|
||||||
|
const evaluationId = persisted.rows[0].id
|
||||||
|
await client.query('DELETE FROM osint.evidence_match_anchor_evaluations WHERE evaluation_id=$1', [evaluationId])
|
||||||
|
for (const anchor of evaluation.anchors) await client.query(`INSERT INTO osint.evidence_match_anchor_evaluations
|
||||||
|
(evaluation_id,anchor_id,similarity,matched,matched_text) VALUES ($1,$2,$3,$4,$5)`,
|
||||||
|
[evaluationId,anchor.anchorId,anchor.similarity,anchor.matched,anchor.matchedText])
|
||||||
|
if (evaluation.matched) await client.query(`INSERT INTO osint.level_flags
|
||||||
|
(level_id,board_id,flag_key,awarded_by_evidence_match_id) VALUES ($1,$2,$3,$4)
|
||||||
|
ON CONFLICT (level_id,flag_key) DO NOTHING`, [level.id,level.board_id,evaluation.flagKey,evaluationId])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function levelGoalStates(
|
async function levelGoalStates(
|
||||||
|
|||||||
Reference in New Issue
Block a user