Add authorable per-level briefing checklist

The case briefing showed a wall of body text. Add a checklist of short,
player-tickable steps authored per level: a new `checklist` on the level brief
(table osint.level_brief_checklist_items), read/written with the board, cloned
on template freeze/instantiate, and carried through the mystery importer and the
mystery:pull exporter. Player tick state is guidance-only, persisted locally per
level rather than on the shared board. Convert the Barricelli inventor-proof
brief from prose into a short intro plus a six-step checklist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-23 22:50:50 +02:00
co-authored by Claude Opus 4.8
parent 95b17e945c
commit e37dc651f3
9 changed files with 53 additions and 12 deletions
+6 -2
View File
@@ -360,7 +360,7 @@ export function createLevelRepository(pool: Pool, editingEnabled: boolean, objec
// a template board that no player has instantiated.
async function assembleBoard(level: LevelRow, authorMode = false): Promise<CaseState> {
const [exhibitsResult, blocksResult, regionsResult, membershipsResult, connectionsResult, metadataResult, eventEvidenceResult,
aliasesResult, partyEvidenceResult, briefResult, conceptsResult, viewsResult, requirementsResult, flagsResult, seenResult] = await Promise.all([
aliasesResult, partyEvidenceResult, briefResult, conceptsResult, viewsResult, requirementsResult, flagsResult, seenResult, checklistResult] = await Promise.all([
pool.query<ExhibitRow>(`SELECT e.id,e.exhibit_type_id,e.xpos,e.ypos,e.width,e.height,e.rotation,e.z_index,e.hidden,
COALESCE(f.title, d.title, n.title, ev.title, p.display_name, claim.statement, '') AS title,
COALESCE(f.label_text, n.note_text, ev.narrative_text, p.summary, '') AS content,
@@ -419,6 +419,7 @@ export function createLevelRepository(pool: Pool, editingEnabled: boolean, objec
'SELECT document_exhibit_id,flag_key FROM osint.document_flag_requirements WHERE board_id=$1 ORDER BY document_exhibit_id,flag_key', [level.board_id]),
pool.query<{ flag_key: string }>('SELECT flag_key FROM osint.level_flags WHERE level_id=$1 ORDER BY flag_key', [level.id]),
pool.query<{ document_exhibit_id: string }>('SELECT document_exhibit_id FROM osint.level_seen_documents WHERE level_id=$1', [level.id]),
pool.query<{ text: string }>('SELECT text FROM osint.level_brief_checklist_items WHERE board_id=$1 ORDER BY sort_order,id', [level.board_id]),
])
const blocks = new Map<string, string[]>()
@@ -480,7 +481,7 @@ export function createLevelRepository(pool: Pool, editingEnabled: boolean, objec
tagPosition: row.tag_position_percent, tagOffset: row.tag_lateral_offset })),
viewport: { x: level.viewport_x, y: level.viewport_y, zoom: level.viewport_zoom }, updatedAt: level.updated_at.toISOString(),
views, revision: Number(level.revision),
brief: { body: briefResult.rows[0]?.body || '', concepts }, goals, report, levelStatus: level.status, editingAllowed: editingEnabled && authorMode,
brief: { body: briefResult.rows[0]?.body || '', concepts, checklist: checklistResult.rows.map(row => row.text) }, goals, report, levelStatus: level.status, editingAllowed: editingEnabled && authorMode,
sourceTemplateVersionId: level.source_template_version_id || undefined }
return filterLevelVisibility(fullState, flagsResult.rows.map(row => row.flag_key), seenResult.rows.map(row => row.document_exhibit_id), authorMode)
}
@@ -520,6 +521,7 @@ export function createLevelRepository(pool: Pool, editingEnabled: boolean, objec
await client.query('DELETE FROM osint.board_views WHERE board_id=$1', [level.board_id])
await client.query('DELETE FROM osint.brief_concepts WHERE board_id=$1', [level.board_id])
await client.query('DELETE FROM osint.level_briefs WHERE board_id=$1', [level.board_id])
await client.query('DELETE FROM osint.level_brief_checklist_items WHERE board_id=$1', [level.board_id])
await client.query('DELETE FROM osint.exhibit_sources WHERE exhibit_id IN (SELECT id FROM osint.exhibits WHERE board_id=$1)', [level.board_id])
await client.query('DELETE FROM osint.metadata_fields WHERE board_id=$1', [level.board_id])
await client.query('DELETE FROM osint.document_flag_requirements WHERE board_id=$1',[level.board_id])
@@ -657,6 +659,8 @@ export function createLevelRepository(pool: Pool, editingEnabled: boolean, objec
}
const brief = state.brief || { body: '', concepts: [] }
await client.query('INSERT INTO osint.level_briefs (board_id,body) VALUES ($1,$2)', [level.board_id, brief.body || ''])
for (const [sortOrder, item] of (brief.checklist || []).map(text => text.trim()).filter(Boolean).entries())
await client.query('INSERT INTO osint.level_brief_checklist_items (id,board_id,sort_order,text) VALUES ($1,$2,$3,$4)', [randomUUID(), level.board_id, sortOrder, item])
for (const [sortOrder, concept] of brief.concepts.entries()) {
requireUuid(concept.id, 'Brief concept id')
if (concept.resolvedPartyExhibitId && !evidenceIds.has(concept.resolvedPartyExhibitId)) throw new Error('Concept resolution references an unknown party')