Some changes to the narrative intergation

This commit is contained in:
2026-08-22 18:27:06 +02:00
parent af0ffe055e
commit 917bb0248e
5 changed files with 26 additions and 101 deletions
-92
View File
@@ -1,92 +0,0 @@
{
"slug": "barricelli-inventor-proof",
"name": "Scene 7 · Prove Barricelli was an inventor",
"title": "The Barricelli Files",
"subtitle": "Scene 7 · Demonstrate OSINT skill",
"brief": {
"body": "DEMONSTRATE OSINT SKILL\n\nProve that Nils Aall Barricelli was an inventor. Find a reliable source online, take a screenshot, and paste it directly onto this board. Connect the source to the authored claim with red thread, explain what the evidence proves, and submit the Case Report.",
"concepts": []
},
"documents": [],
"folders": [],
"claims": [
{
"key": "nils-inventor",
"statement": "Nils Aall Barricelli was an inventor.",
"x": 940,
"y": 360,
"width": 330,
"height": 190
}
],
"report": {
"title": "Barricelli Inventor Finding",
"requiredForCompletion": true
},
"goals": [
{
"key": "barricelli.inventor-proof",
"title": "Prove Nils Aall Barricelli was an inventor",
"instructions": "Paste a reliable screenshot, connect it to the claim, and submit a properly cited Case Report.",
"completionMessage": "CASE REPORT ACCEPTED — NILS AALL BARRICELLI: INVENTOR",
"requiredFlags": ["scene7.nils_inventor_proved"]
}
],
"evidenceMatchRules": [
{
"name": "Google Patents · GB695913A",
"sourceLabel": "Google Patents · GB695913A · Improved chest of drawers",
"sourceUri": "https://patents.google.com/patent/GB695913A/en",
"flagKey": "scene7.nils_inventor_proved",
"minimumAnchorMatches": 2,
"anchors": [
{
"phrase": "GB695913A Improved chest of drawers",
"minimumSimilarity": 0.7
},
{
"phrase": "Nils Aall Barricelli",
"minimumSimilarity": 0.72
},
{
"phrase": "695913 Chests of drawers BARRICELLI N A May 31 1951",
"minimumSimilarity": 0.68
}
]
},
{
"name": "Nasjonalbiblioteket · Patent 93585",
"sourceLabel": "Nasjonalbiblioteket · Patent 93585 · Koffert-kommode",
"sourceUri": "https://www.nb.no/items/921545b51acef0b05054cfc1f4666975?page=9&searchText=baricelli",
"flagKey": "scene7.nils_inventor_proved",
"minimumAnchorMatches": 2,
"anchors": [
{
"phrase": "Nr 75 348 Kl 33 b-9 Fra 31 mai 1948 93585 Koffert-kommode",
"minimumSimilarity": 0.68
},
{
"phrase": "Niels Aall Baricelli Oslo",
"minimumSimilarity": 0.72
},
{
"phrase": "Patentpaastand Kommode som er satt sammen av flere enkeltdeler som hver er utfort som koffert",
"minimumSimilarity": 0.62
}
]
}
],
"evidenceSemanticRules": [
{
"goalKey": "barricelli.inventor-proof",
"name": "Barricelli inventor claim",
"targetSubject": "Nils Aall Barricelli",
"relatedSubject": "Nils Aall Barricelli's father",
"assertion": "The source states or directly demonstrates that Nils Aall Barricelli was an inventor or a named patent applicant for an invention.",
"successFlagKey": "scene7.nils_inventor_proved",
"relatedFlagKey": "scene7.father_inventor_discovered",
"minimumConfidence": 0.88,
"evaluatorVersion": "barricelli_inventor_v1"
}
]
}
+2 -2
View File
@@ -16,8 +16,8 @@ type MysteryDocument = {
}
type MysteryGraph = {
entry: string
nodes: { key: string; type: 'cutscene' | 'dialogue' | 'level' | 'det_gate' | 'llm_gate'; label?: string; x: number; y: number
componentKey?: string; templateSlug?: string; version?: number
nodes: { key: string; type: 'cutscene' | 'dialogue' | 'level' | 'det_gate' | 'llm_gate' | 'merit' | 'phone'; label?: string; x: number; y: number
componentKey?: string; templateSlug?: string; version?: number; awardsFlag?: string
terminals?: { key: string; label?: string; to?: string | null }[]
utterances?: { npc?: string; pose?: string; text: string; utterer?: 'npc' | 'player' }[] }[]
}
+8
View File
@@ -9,6 +9,7 @@ import jwt from 'jsonwebtoken'
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
import type { CaseState, DocumentExhibit, EventExhibit, FolderExhibit, NoteExhibit, PartyExhibit, TimelineView } from '../src/types.js'
import { runMigrations } from './migrations.js'
import type { StoryGraphDto } from './storyGraphRepository.js'
const { Client } = pg
const baseDatabaseUrl = process.env.TEST_DATABASE_URL
@@ -252,6 +253,7 @@ suite('normalized level persistence API', () => {
const { importMysteryTemplate } = await import('../scripts/importMysteryTemplate.js')
const manifestPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'mysteries', 'barricelli-scene-7', 'mystery.json')
const imported = await importMysteryTemplate(manifestPath, baseUrl, adminAuthorization.replace(/^Bearer /, ''))
expect(imported.mystery).toEqual({ slug:'barricelli-inventor-proof' })
expect(imported.playableLevel).toMatchObject({ title:'The Barricelli Files',exhibits:[expect.objectContaining({ type:'claim',statement:'Nils Aall Barricelli was an inventor.' })],report:expect.objectContaining({
title:'Barricelli Inventor Finding',requiredForCompletion:true,status:'draft',
}),goals:[expect.objectContaining({
@@ -270,6 +272,12 @@ suite('normalized level persistence API', () => {
expect.objectContaining({ goalKey:'barricelli.inventor-proof',targetSubject:'Nils Aall Barricelli',
relatedFlagKey:'scene7.father_inventor_discovered' }),
])
const mysteries = await (await adminFetch(`${baseUrl}/api/admin/mysteries`)).json() as { id:string;slug:string }[]
const mysteryId = mysteries.find(mystery => mystery.slug === 'barricelli-inventor-proof')!.id
const graph = await (await adminFetch(`${baseUrl}/api/admin/mysteries/${mysteryId}/graph`)).json() as StoryGraphDto
expect(graph.nodes).toHaveLength(2)
expect(graph.nodes.find(node => node.nodeType === 'level')).toMatchObject({ label:'Demonstrate OSINT skill',levelTemplateVersionId:expect.any(String) })
expect(graph.nodes.find(node => node.nodeType === 'merit')).toMatchObject({ label:'The Barricelli Luggage',awardsFlag:'barricelli_luggage' })
const fixtureDir = path.join(path.dirname(manifestPath), 'fixtures')
const fatherUpload = new FormData()
+13 -4
View File
@@ -144,8 +144,12 @@ suite('narrative graph runtime', () => {
const mysteries = await (await authFetch(`${baseUrl}/api/admin/mysteries`, adminAuthorization)).json() as { id:string;slug:string }[]
const mysteryId = mysteries.find(mystery => mystery.slug === 'goal-mystery')!.id
expect((await authFetch(`${baseUrl}/api/admin/mysteries/${mysteryId}/graph`, adminAuthorization, { method:'POST',headers:json,
body:JSON.stringify({ entry:'level',nodes:[{ key:'level',type:'level',label:'Prove it',templateSlug:'goal-chapter',x:0,y:0,
terminals:[{ key:'continue',label:'Continue',to:null }] }] }) })).status).toBe(201)
body:JSON.stringify({ entry:'level',nodes:[
{ key:'level',type:'level',label:'Prove it',templateSlug:'goal-chapter',x:0,y:0,
terminals:[{ key:'continue',label:'Continue',to:'merit' }] },
{ key:'merit',type:'merit',label:'Inventor Merit',awardsFlag:'demo.inventor-merit',x:200,y:0,
terminals:[{ key:'continue',label:'Accept',to:null }] },
] }) })).status).toBe(201)
const created = await authFetch(`${baseUrl}/api/playthroughs`, undefined, { method:'POST',headers:json,body:JSON.stringify({ mystery:'goal-mystery' }) })
expect(created.status).toBe(201)
@@ -167,10 +171,15 @@ suite('narrative graph runtime', () => {
const completed = await authFetch(`${baseUrl}/api/playthroughs/${playthroughId}/advance`, undefined, { method:'POST',headers:json,body:'{}' })
expect(completed.status).toBe(200)
expect((await completed.json() as PlaythroughState).playthrough.status).toBe('finished')
expect(await (await authFetch(`${baseUrl}/api/playthroughs/${playthroughId}/achievements`, undefined)).json()).toContain('demo.inventor-proved')
expect(await completed.json()).toMatchObject({ playthrough:{ status:'active' },node:{ kind:'merit',label:'Inventor Merit',awardsFlag:'demo.inventor-merit' } })
expect(await (await authFetch(`${baseUrl}/api/playthroughs/${playthroughId}/achievements`, undefined)).json())
.toEqual(expect.arrayContaining(['demo.inventor-proved','demo.inventor-merit']))
const achievement = await appPool.query<{ awarded_by_node_id:string | null }>(
'SELECT awarded_by_node_id FROM osint.achievements WHERE playthrough_id=$1 AND flag_key=$2', [playthroughId,'demo.inventor-proved'])
expect(achievement.rows[0].awarded_by_node_id).toBe(atLevel.node!.id)
const finished = await authFetch(`${baseUrl}/api/playthroughs/${playthroughId}/advance`, undefined, { method:'POST',headers:json,body:'{}' })
expect(finished.status).toBe(200)
expect((await finished.json() as PlaythroughState).playthrough.status).toBe('finished')
})
})
+3 -3
View File
@@ -3,7 +3,7 @@ import type { Pool, PoolClient } from 'pg'
export type GraphSpecNode = {
key: string; type: StoryNodeType; label?: string; x: number; y: number
componentKey?: string; templateSlug?: string; version?: number
componentKey?: string; templateSlug?: string; version?: number; awardsFlag?: string
terminals?: { key: string; label?: string; to?: string | null }[]
utterances?: { npc?: string; pose?: string; text: string; utterer?: Utterer }[]
}
@@ -261,8 +261,8 @@ export function createStoryGraphRepository(pool: Pool): StoryGraphRepository {
versionId = version.rows[0]?.id ?? null
if (!versionId) throw new Error(`Graph node ${node.key}: unknown level template ${node.templateSlug}`)
}
await client.query('INSERT INTO osint.story_nodes (id,mystery_id,node_type,label,xpos,ypos,has_utterances,level_template_version_id,component_key) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9)',
[id, mysteryId, node.type, node.label || node.type, node.x, node.y, Boolean(node.utterances?.length), versionId, node.componentKey || null])
await client.query('INSERT INTO osint.story_nodes (id,mystery_id,node_type,label,xpos,ypos,has_utterances,level_template_version_id,component_key,awards_flag) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)',
[id, mysteryId, node.type, node.label || node.type, node.x, node.y, Boolean(node.utterances?.length), versionId, node.componentKey || null, node.awardsFlag?.trim() || null])
for (const [index, terminal] of (node.terminals || []).entries()) {
const terminalId = randomUUID(); terminalIds.set(`${node.key}:${terminal.key}`, terminalId)
await client.query('INSERT INTO osint.story_node_terminals (id,parent_node_id,terminal_key,label,sort_order) VALUES ($1,$2,$3,$4,$5)',