Make mystery importer seed notes and tolerate sparse manifests

Add a notes[] array to the manifest (note exhibits with presentation),
so the Scene 6 phone-note board is reproducible from source instead of
hand-authored. Also default documents/folders/brief/subtitle so
narrative-only mysteries (e.g. barricelli-files) import cleanly.

Captures mysteries/barricelli-phone-note/mystery.json from the authored
template (note: "PHONE FOR GLITCH HUNTER / Call: 5550100").

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-23 00:43:47 +02:00
co-authored by Claude Opus 4.8
parent ce30321532
commit f6fbeb39cf
2 changed files with 37 additions and 13 deletions
+17 -13
View File
@@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto'
import { readFile } from 'node:fs/promises'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import type { CaseDocument, CaseState, ClaimExhibit, DocumentCaptureKind, PartyKind, SourceFileType } from '../src/types.js'
import type { CaseDocument, CaseState, ClaimExhibit, DocumentCaptureKind, NoteExhibit, NotePresentation, PartyKind, SourceFileType } from '../src/types.js'
type MysteryDocument = {
key: string
@@ -41,12 +41,13 @@ type MysteryManifest = {
slug: string
name: string
title: string
subtitle: string
subtitle?: string
timelineRange?: { start: string; end: string }
brief: { body: string; concepts: { label: string; context: string; expectedPartyKind: PartyKind }[] }
documents: MysteryDocument[]
folders: { key: string; title: string; content: string; x: number; y: number; width: number; members: string[] }[]
brief?: { body: string; concepts: { label: string; context: string; expectedPartyKind: PartyKind }[] }
documents?: MysteryDocument[]
folders?: { key: string; title: string; content: string; x: number; y: number; width: number; members: string[] }[]
claims?: { key:string;statement:string;x:number;y:number;width?:number;height?:number }[]
notes?: { title?:string;content:string;presentation?:NotePresentation;x:number;y:number;width?:number;height?:number }[]
report?: { title?:string;requiredForCompletion?:boolean }
goals?: MysteryGoal[]
evidenceMatchRules?: MysteryEvidenceMatchRule[]
@@ -84,14 +85,15 @@ export async function importMysteryTemplate(manifestPath: string, baseUrl = 'htt
const headers = { 'content-type': 'application/json', ...(authorization ? { authorization } : {}) }
const createdResponse = await requireOk(await fetch(`${baseUrl}/api/levels`, {
method: 'POST', headers,
body: JSON.stringify({ id: authoringId, title: manifest.title, subtitle: manifest.subtitle }),
body: JSON.stringify({ id: authoringId, title: manifest.title, subtitle: manifest.subtitle || '' }),
}), 'Create authoring level')
const state = await createdResponse.json() as CaseState
const documentPositions = new Map<string, { x: number; y: number }>()
manifest.folders.forEach((folder, folderIndex) => folder.members.forEach((key, memberIndex) => documentPositions.set(key, { x: folder.x + 70 + memberIndex * 205, y: folder.y + 230 + folderIndex * 35 })))
const manifestFolders = manifest.folders || []
manifestFolders.forEach((folder, folderIndex) => folder.members.forEach((key, memberIndex) => documentPositions.set(key, { x: folder.x + 70 + memberIndex * 205, y: folder.y + 230 + folderIndex * 35 })))
const documents = new Map<string, CaseDocument>()
for (const source of manifest.documents) {
for (const source of manifest.documents || []) {
const uploaded = await uploadAsset(baseUrl, state.id, manifestDir, source, authorization)
documents.set(source.key, {
id: uploaded?.id || randomUUID(), type: 'document', title: source.title, publishedAt: source.publishedAt,
@@ -103,17 +105,19 @@ export async function importMysteryTemplate(manifestPath: string, baseUrl = 'htt
})
}
const folderIds = new Map(manifest.folders.map(folder => [folder.key, randomUUID()]))
state.brief = { body: manifest.brief.body, concepts: manifest.brief.concepts.map(concept => ({ id: randomUUID(), ...concept })) }
const folderIds = new Map(manifestFolders.map(folder => [folder.key, randomUUID()]))
if (manifest.brief) state.brief = { body: manifest.brief.body, concepts: manifest.brief.concepts.map(concept => ({ id: randomUUID(), ...concept })) }
state.views = state.views.map(view => view.type === 'timeline' ? { ...view, rangeMode: manifest.timelineRange ? 'fixed' : 'auto', range: manifest.timelineRange } : view)
const folders = manifest.folders.map(folder => ({
const folders = manifestFolders.map(folder => ({
id: folderIds.get(folder.key)!, type: 'folder', title: folder.title, content: folder.content,
x: folder.x, y: folder.y, width: folder.width, height: 166, rotation: 0, zIndex: 1, hidden: false, isOpen: false,
} as const))
const claims:ClaimExhibit[]=(manifest.claims || []).map(claim => ({ id:randomUUID(),type:'claim',title:claim.statement,statement:claim.statement,
x:claim.x,y:claim.y,width:claim.width || 310,height:claim.height || 180,rotation:0,zIndex:2,hidden:false }))
state.exhibits = [...documents.values(), ...folders,...claims]
state.relations = manifest.folders.flatMap(folder => folder.members.map((key, memberIndex) => {
const notes:NoteExhibit[]=(manifest.notes || []).map(note => ({ id:randomUUID(),type:'note',title:note.title || 'NOTE',content:note.content,
presentation:note.presentation || 'luggage',x:note.x,y:note.y,width:note.width || 230,height:note.height || 180,rotation:0,zIndex:2,hidden:false }))
state.exhibits = [...documents.values(), ...folders,...claims,...notes]
state.relations = manifestFolders.flatMap(folder => folder.members.map((key, memberIndex) => {
const document = documents.get(key)
if (!document) throw new Error(`Folder ${folder.key} refers to unknown document ${key}`)
return {