diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index f181811..24506be 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -48,7 +48,11 @@ "ghcr.io/dhoeric/features/act:1": {}, "ghcr.io/devcontainers-community/features/direnv:1": {}, "ghcr.io/rocker-org/devcontainer-features/apt-packages:1": { - "packages": "git-filter-repo" + // The libs after git-filter-repo are what Playwright's chromium links against + // (the ubuntu24.04 chromium list from playwright-core's nativeDeps); the + // screenshot tool in .claude/skills/mock-shot cannot launch without them. + // Keep this in step with the base image: the t64 names are noble-specific. + "packages": "git-filter-repo libasound2t64 libatk-bridge2.0-0t64 libatk1.0-0t64 libatspi2.0-0t64 libcairo2 libcups2t64 libdbus-1-3 libdrm2 libgbm1 libglib2.0-0t64 libnspr4 libnss3 libpango-1.0-0 libx11-6 libxcb1 libxcomposite1 libxdamage1 libxext6 libxfixes3 libxkbcommon0 libxrandr2" } }, "postAttachCommand": "/bin/sh .devcontainer/postAttachCommand.sh", diff --git a/.devcontainer/postCreateCommand.sh b/.devcontainer/postCreateCommand.sh index aad0236..6dc4b28 100755 --- a/.devcontainer/postCreateCommand.sh +++ b/.devcontainer/postCreateCommand.sh @@ -29,6 +29,16 @@ if [ -f package.json ]; then fi fi +# Playwright's browser for the screenshot tool (.claude/skills/mock-shot). +# The shared libraries it links against are declared in devcontainer.json's +# apt-packages feature — without them the browser dies at launch with +# "libglib-2.0.so.0: cannot open shared object file". +# The installs above pass --ignore-scripts, so package.json's postinstall does not +# fire here — call it explicitly. +if [ -x node_modules/.bin/playwright ]; then + bun run postinstall || echo "[postCreate] browser download failed — run 'bun run postinstall'" +fi + # Apply migrations and load the scenarios into the local D1 database. # Both run against .wrangler/state, so no network and no database container. if [ -n "$(ls -A db/migrations 2>/dev/null)" ]; then diff --git a/.gitignore b/.gitignore index 28e15a2..3b7b1d8 100644 --- a/.gitignore +++ b/.gitignore @@ -257,6 +257,7 @@ vite.config.*.timestamp-* /db/seeds/*.local.* # db:seed が生成する投入用SQL。中身は db/scenarios/*.yaml から再生成できる。 /db/seed.sql +/db/tts-speakers.sql ### Cloudflare Workers ### # wrangler のローカル状態(DO/KVのエミュレーションデータなど) diff --git a/.vscode/settings.json b/.vscode/settings.json index 3f68bf3..e62c32a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -32,20 +32,37 @@ "**/.turbo": true, "**/.cache": true, "**/coverage": true, - "**/.tanstack": true + "**/.tanstack": true, + "**/.vite": true, + "**/.wrangler": true, + "**/*.tsbuildinfo": true, + "storybook-static": true, + "screenshots": true, + "db/seed.sql": true }, "files.watcherExclude": { "**/.git/objects/**": true, "**/node_modules/**": true, - "**/dist/**": true + "**/dist/**": true, + "**/.tanstack/**": true, + "**/.vite/**": true, + "**/.wrangler/**": true, + "storybook-static/**": true, + "screenshots/**": true }, "search.exclude": { "**/node_modules": true, "**/dist": true, "**/coverage": true, + "**/.tanstack": true, + "**/.wrangler": true, "**/*.gen.ts": true, + "**/*.tsbuildinfo": true, "**/bun.lock": true, - "**/db/migrations/**": true + "**/db/migrations/**": true, + "storybook-static": true, + "screenshots": true, + "db/seed.sql": true }, // ───────────────────────────────────────────────────────────── diff --git a/README.md b/README.md index 544d999..b8acb36 100644 --- a/README.md +++ b/README.md @@ -225,7 +225,7 @@ Hono + Vite + React を Cloudflare Workers 上で動かし、状態は D1 / Dura ### LLM -**AlibAIはLLMプロバイダを固定しません。** 役割ごとに選び、混成もできます。 +**AlibAIはLLMを役割で使い分けます。** 宛先は `OPENAI_URL` が指す OpenAI互換サーバ1つで、役割ごとに当てるモデルを選びます。 | 役割 | 何をするか | 求められる性質 | | --- | --- | --- | @@ -233,17 +233,17 @@ Hono + Vite + React を Cloudflare Workers 上で動かし、状態は D1 / Dura | **Judge** | 証拠開示・矛盾検出・質問候補生成 | 構造化出力、安価、高速、並列実行できる | | **Author** | シナリオ作成支援と整合性チェック | 頻度は低い。長文脈と推論力 | -役割ごとの既定モデルは以下。3社どれでも組めます。 +役割ごとの既定モデルは以下(`db/llm-catalog.ts` の `LLM_DEFAULT_MODELS`)。 -| 役割 | Anthropic | OpenAI | Google | -| --- | --- | --- | --- | -| Actor | `claude-sonnet-5` | `gpt-5.6-terra` | `gemini-3.5-flash` | -| Judge | `claude-haiku-4-5` | `gpt-5.6-luna` | `gemini-3.1-flash-lite` | -| Author | `claude-opus-5` | `gpt-5.6-sol` | `gemini-3.1-pro` | +| 役割 | 既定モデル | +| --- | --- | +| Actor | `gpt-5.6-terra` | +| Judge | `gpt-5.6-luna` | +| Author | `gpt-5.6-sol` | -> **モデルIDと料金は各社とも改定が速いです。** -> 上の表は既定値であって正典ではありません。採用前に各社の公式ドキュメントで確認してください。 -> 特にOpenAI / Googleの型番は二次情報を元にしているので、実装前の検証が必須です。 +> **これは env にも設定にも何も無いときの最後の拠りどころであって、正典ではありません。** +> 互換サーバがこのIDを持っているとは限らないので、構成に合わせて `LLM_ACTOR_MODEL` などで上書きしてください。 +> 実際に選べるモデルの一覧は、互換サーバの `/v1/models` から取ります(手で書いた表は持ちません)。 **選び方の目安** @@ -255,28 +255,33 @@ Hono + Vite + React を Cloudflare Workers 上で動かし、状態は D1 / Dura 切り替えは環境変数だけで済みます。 ```bash -LLM_ACTOR_PROVIDER=anthropic # anthropic | openai | google -LLM_JUDGE_PROVIDER=google # 役割ごとに別プロバイダでよい -LLM_AUTHOR_PROVIDER=openai +OPENAI_URL=https://example.internal/v1 # 末尾は /v1 +OPENAI_API_KEY= # 鍵の要らないサーバでも何か入れる + +LLM_ACTOR_MODEL= # 未設定なら既定表。互換サーバ独自の名前も書ける +LLM_JUDGE_MODEL= +LLM_AUTHOR_MODEL= ``` -実装は [Vercel AI SDK](https://sdk.vercel.ai/) で3社を吸収します。 -`src/server/llm/provider.ts` が役割 → モデルの解決だけを担当し、 -呼び出し側(`actor.ts` / `judge.ts`)はプロバイダを知りません。 +`OPENAI_API_KEY` は鍵を要求しないサーバでも必要です。SDK が Authorization ヘッダを無条件に組み立てるためで、 +未設定だと組み立てに失敗します。 + +実装は [Vercel AI SDK](https://sdk.vercel.ai/) の `@ai-sdk/openai` 1本です。 +`src/server/llm/provider.ts` が役割 → モデルIDの解決(`chooseLlm`)とクライアントの生成(`resolveModel`)に分かれ、 +呼び出し側(`actor.ts` / `judge.ts`)はモデルIDだけを知ります。 ```ts -export const resolveModel = (role: LlmRole): LanguageModel => { - const config = PROVIDER_CONFIG[role] - const modelId = config.model === undefined ? DEFAULT_MODELS[config.provider][role] : config.model - - switch (config.provider) { - case 'anthropic': return anthropic(modelId) - case 'openai': return openai(modelId) - case 'google': return google(modelId) - } +export const chooseLlm = (env: Env, role: LlmRole, override?: LlmOverride): string => { + // 優先順位は override(画面での選択)→ env → 既定表 } + +export const resolveModel = (env: Env, modelId: string): LanguageModel => + createOpenAI({ apiKey: env.OPENAI_API_KEY, baseURL: env.OPENAI_URL }).chat(modelId) ``` +`.chat` を明示するのは、省くと `@ai-sdk/openai` v2 が Responses API(`/responses`)へ行くためです。 +互換を名乗るサーバが出しているのは大抵 `/chat/completions` だけで、既定のままだと 404 になります。 + Actorは必ずストリーミングで返します。 スマホで10分の体験なので、返答が丸ごと届くまで待たせると体感が死にます。 @@ -299,55 +304,38 @@ export const judgementSchema = z.object({ 20ターンの尋問なら、同じキャラクターシートを20回送ることになります。 キャッシュが効けばこの部分が大幅に安くなり、効かなければ全額です。 -**ここだけはプロバイダを抽象化の裏に隠せません。** 挙動が3社で違うからです。 +**明示指定は持っていません。** -| プロバイダ | 方式 | 実装側でやること | -| --- | --- | --- | -| Anthropic | 明示的。`cache_control` をブロックに付ける | **付け忘れると効かない。** ブレークポイント設計が必要 | -| OpenAI | 自動。共通プレフィックスがあれば勝手に効く | 設定不要。プレフィックスを壊さないことだけ意識する | -| Google | 暗黙キャッシュが自動。明示キャッシュは別API | 基本は自動任せ。強く効かせたいなら明示キャッシュを別管理 | +以前は「ブロックに `cache_control` を付ける」方式のモデル向けに `cacheHint(role)` という関数を表に出し、 +そのモデルを選んだときだけ明示指定を差し込んでいました。宛先を互換API1本にした時点で外しています。 +`createOpenAI` で組んだモデルに `providerOptions` でベンダ固有の指定を渡しても、 +AI SDK 側で誰も読まないため黙って捨てられるからです。エラーは出ず、請求だけが変わる—— +動かない指定をコードに残すほうが害が大きい、という判断です。 -抽象レイヤの裏に隠すと、プロバイダを切り替えた瞬間にコストが跳ねます。 -だから `cacheHint(role)` として関数を表に出し、Anthropic選択時だけ明示指定を差し込みます。 +いま頼っているのは、**共通プレフィックスがあれば自動で効くタイプのキャッシュだけ**です。 +互換サーバがそもそもキャッシュを持たない可能性もあります。 -**3社共通で守るべき設計ルール** +**守るべき設計ルール** 1. **system promptを凍結する。** 「現在のターン数」「経過時間」「発見済み証拠リスト」をsystemに埋め込まないこと。 これらは毎ターン変わるので、先頭に入れた瞬間にキャッシュミスします。 動的な状態は必ずメッセージ列の後方に置きます。 - どのプロバイダもプレフィックス一致なので、これは共通です。 + プレフィックス一致で効くタイプしか使っていないので、ここが唯一のレバーです。 -2. **ブレークポイントは安定性の境界に置く(Anthropic)。** +2. **前置きは安定している順に並べる。** - 1つ目: 全シナリオ共通のゲームルール(最も安定) - 2つ目: NPCのキャラクターシート(そのNPCとの会話中は不変) - - 3つ目: 直近ターンの最終コンテンツブロック(会話履歴を累積キャッシュ) + - 3つ目以降: 探偵、会話履歴 - ブレークポイントは1リクエスト最大4つまでです。 - -3. **最小キャッシュ長に注意する(Anthropic)。** - これを下回ると、`cache_control` を付けてもエラーにならず静かにキャッシュされません。 - - | モデル | 最小キャッシュ長 | - | --- | --- | - | `claude-opus-5` | 512 tokens | - | `claude-sonnet-5` | 1,024 tokens | - | `claude-haiku-4-5` | 4,096 tokens | - - Judge用のプロンプトは4,096トークン未満になりがちです。 - キャッシュしたいなら共通の判定ルールを厚めに書いて閾値を超えさせるか、割り切って諦めます。 - -4. **キャッシュはモデル単位・NPC単位で分かれる。** +3. **キャッシュはモデル単位・NPC単位で分かれる。** 同一シナリオでもNPCが違えばキャッシュは別物です。 1プレイ中に何度も同じNPCへ聞き直す設計は、キャッシュ効率の面でも有利になります。 -5. **必ず計測する。** +4. **必ず計測する。** キャッシュ読み込みトークンが0のまま増えないなら、どこかに破壊要因があります。 タイムスタンプ、UUID、JSONのキー順不定、条件分岐によるsystem文の差分などを疑ってください。 - -Anthropicの既定TTLは5分です。1回のプレイが約10分なので、 -テンポの良いプレイなら5分で十分回りますが、じっくり考えるプレイヤーは会話間隔が空きます。 -1時間TTLは書き込みコストが上がるので、実データでヒット率を見てから判断します。 + 0が続くなら、実装より先に互換サーバ側を確かめてください。 ### Frontend @@ -389,10 +377,11 @@ scenario_truths 真相(サーバー限定。APIレスポンスに絶対含 characters 登場人物(人格・知識・秘密・目的・嘘・時系列) evidences 証拠(開示条件、発見トリガー) play_sessions プレイセッション(匿名可、進行状態、経過時間) -messages 会話ログ(NPC別、トークン使用量も記録) +messages 会話ログ(NPC別) discoveries 発見済み証拠・指摘済み矛盾 results 結果(解決時間、質問回数、正解率、順位) reports UGC通報 +llm_usages LLM呼び出しごとのトークン消費(役割・モデル別) ``` `scenario_truths` を別テーブルに切るのが重要です。 @@ -424,14 +413,14 @@ LLMを使うサービスは、コスト可視化を後回しにすると必ず | エラー監視 | Sentry | | LLMトレース | Langfuse(または Braintrust) | | プロダクト分析 | PostHog | -| コスト集計 | 全リクエストのトークン使用量を記録し、シナリオ別・NPC別・プロバイダ別に集計 | +| コスト集計 | 全リクエストのトークン使用量を `llm_usages` に記録し、シナリオ別・NPC別・モデル別に集計 | 記録すべきは通常の入出力トークンに加えて、**キャッシュ書き込み量とキャッシュ読み込み量**です。 入力トークンの総量は「非キャッシュ入力 + キャッシュ書き込み + キャッシュ読み込み」であって、 非キャッシュ入力の単体ではありません。 ここを取り違えると、キャッシュが効いているのに「トークンが少ない」と誤読します。 -プロバイダ別に集計しておくと、乗り換え判断が数字でできるようになります。 +役割別・モデル別に集計しておくと、モデルの入れ替え判断が数字でできるようになります。 1プレイあたりのコストは「ターン数 × (キャッシュ読み込み + 新規入力 + 出力)」で見積もり、 ローンチ前に必ず実測してください。キャッシュ設計が効いているかどうかで一桁変わります。 @@ -450,9 +439,9 @@ LLMを使うサービスは、コスト可視化を後回しにすると必ず インジェクションテストは**CIに入れること**。 プロンプトを1行変えただけで防御が崩れることがあるので、人力レビューでは守り切れません。 -プロバイダを切り替え可能にした以上、**評価は3社分回す必要があります。** -Anthropicで漏れなかったプロンプトがGeminiで漏れることは普通にあります。 -乗り換えを検討するときは、まずこの評価スイートを通してから判断してください。 +モデルを差し替え可能にした以上、**評価は使う候補モデルごとに回す必要があります。** +あるモデルで漏れなかったプロンプトが別のモデルで漏れることは普通にあります。 +互換サーバに載せるモデルを入れ替えるときは、まずこの評価スイートを通してから判断してください。 ### CI / CD @@ -497,14 +486,13 @@ Anthropicで漏れなかったプロンプトがGeminiで漏れることは普 ```json "remoteEnv": { - "ANTHROPIC_API_KEY": "${localEnv:ANTHROPIC_API_KEY}", "OPENAI_API_KEY": "${localEnv:OPENAI_API_KEY}", - "GOOGLE_GENERATIVE_AI_API_KEY": "${localEnv:GOOGLE_GENERATIVE_AI_API_KEY}" + "OPENAI_URL": "${localEnv:OPENAI_URL}" } ``` ホスト側にキーが無ければ空文字が入るだけで、コンテナは問題なく起動します。 -使わないプロバイダのキーは設定しなくて構いません。 +LLMの宛先は互換サーバ1つなので、渡すのはこの2つだけです。 初回作成時に `.env.example` から `.env` が生成されます。 **既存の `.env` は上書きしません。** 実キーを消す事故を防ぐためです。 diff --git a/__tests__/api/validation.test.ts b/__tests__/api/validation.test.ts index 77807d8..e5808f1 100644 --- a/__tests__/api/validation.test.ts +++ b/__tests__/api/validation.test.ts @@ -237,27 +237,17 @@ describe('POST /api/sessions/:id/ask のモデル指定', () => { llm, }) - test('提供元が列挙の外なら 400(3値で安定しているので厳格に見る)', async () => { - const res = await postJson( - `/api/sessions/${SESSION_ID}/ask`, - askBody({ - actor: { provider: 'nope' }, - }), - ) - - expect(res.status).toBe(400) - }) - /* - 知らないモデルIDでは弾かない。localStorage に古い設定が残っているだけの - プレイヤーを事件の途中で締め出すことになるので、サーバ側で既定へ落とす。 + 知らないモデルIDでは弾かない。突き合わせる許可リストはもう無く、 + localStorage に古い設定が残っているだけのプレイヤーを事件の途中で + 締め出すことにもなるため、そのまま通して互換サーバに任せる。 ここでは「400 にはならない」ことだけを確かめる(先へ進むとバインディングに触る)。 */ test('知らないモデルIDでは 400 にしない', async () => { const res = await postJson( `/api/sessions/${SESSION_ID}/ask`, askBody({ - actor: { provider: 'openai', model: 'gpt-imaginary-9' }, + actor: { model: 'gpt-imaginary-9' }, }), ) diff --git a/__tests__/client/detective-store.test.ts b/__tests__/client/detective-store.test.ts index 5f0b1be..50030b4 100644 --- a/__tests__/client/detective-store.test.ts +++ b/__tests__/client/detective-store.test.ts @@ -3,11 +3,15 @@ import { activeDetective, clearActiveDetective, type DetectiveStore, + detectiveRoster, EMPTY_STORE, + isPresetDetective, + PRESET_DETECTIVES, parseDetectiveStore, removeDetective, type StoredDetective, setActiveDetective, + TACHIBANA_SHERRY, toDetective, upsertDetective, } from '@/client/lib/detective-store' @@ -18,6 +22,7 @@ const profile = (id: string, name: string): StoredDetective => ({ ageGroup: 'young', gender: 'female', appearance: 'くたびれたコート', + speech: '', }) const akari = profile('a', '日下部 灯') @@ -109,6 +114,39 @@ describe('activeDetective', () => { }) }) +describe('備え付けの探偵', () => { + test('1人も作っていなくても名簿には並ぶ', () => { + expect(detectiveRoster(EMPTY_STORE)).toEqual(PRESET_DETECTIVES) + }) + + test('作った探偵より前に並ぶ', () => { + const store: DetectiveStore = { profiles: [akari], activeId: undefined } + + expect(detectiveRoster(store).map((p) => p.id)).toEqual([TACHIBANA_SHERRY.id, 'a']) + }) + + test('選べる', () => { + const selected = setActiveDetective(EMPTY_STORE, TACHIBANA_SHERRY.id) + + expect(activeDetective(selected)?.name).toBe('橘シェリー') + }) + + test('選んでも保管庫には書かれない', () => { + expect(setActiveDetective(EMPTY_STORE, TACHIBANA_SHERRY.id).profiles).toEqual([]) + }) + + test('選択中のまま読み直しても、選択が外れない', () => { + const parsed = parseDetectiveStore({ profiles: [], activeId: TACHIBANA_SHERRY.id }) + + expect(parsed.store.activeId).toBe(TACHIBANA_SHERRY.id) + }) + + test('備え付けかどうかを見分けられる', () => { + expect(isPresetDetective(TACHIBANA_SHERRY.id)).toBe(true) + expect(isPresetDetective('a')).toBe(false) + }) +}) + describe('clearActiveDetective', () => { test('選択だけ外し、保存済みの探偵は消さない', () => { const store: DetectiveStore = { profiles: [akari, tsubaki], activeId: 'a' } @@ -126,6 +164,7 @@ describe('toDetective', () => { ageGroup: 'young', gender: 'female', appearance: 'くたびれたコート', + speech: '', }) }) }) @@ -150,8 +189,9 @@ describe('parseDetectiveStore', () => { ageGroup: 'young', gender: 'female', appearance: 'くたびれたコート', + speech: '', }, - { id: 'b', name: '八千代 椿', ageGroup: 'elder', gender: 'male', appearance: '' }, + { id: 'b', name: '八千代 椿', ageGroup: 'elder', gender: 'male', appearance: '', speech: '' }, ]) expect(parsed.store.activeId).toBe('a') }) @@ -189,6 +229,15 @@ describe('parseDetectiveStore', () => { expect(parsed.store.activeId).toBeUndefined() }) + test('口調の欄が無かった頃の探偵を、消さずに空の口調で読み替える', () => { + const { speech, ...speechless } = akari + const parsed = parseDetectiveStore({ profiles: [speechless], activeId: 'a' }) + + expect(parsed.migrated).toBe(true) + expect(parsed.store.profiles).toEqual([{ ...akari, speech: '' }]) + expect(parsed.store.activeId).toBe('a') + }) + test('今の形はそのまま通り、書き戻しも起こさない', () => { const parsed = parseDetectiveStore({ profiles: [akari], activeId: 'a' }) diff --git a/__tests__/client/format.test.ts b/__tests__/client/format.test.ts index edca938..deca71d 100644 --- a/__tests__/client/format.test.ts +++ b/__tests__/client/format.test.ts @@ -4,16 +4,21 @@ import { formatClock, formatSeconds } from '@/client/lib/format' describe('formatSeconds', () => { test('分と秒に分ける', () => { - expect(formatSeconds(65)).toBe('1:05') + expect(formatSeconds(65)).toBe('01:05') }) test('1分未満は0分として出す', () => { - expect(formatSeconds(9)).toBe('0:09') + expect(formatSeconds(9)).toBe('00:09') }) - test('秒はゼロ詰めする', () => { + test('分も秒もゼロ詰めする', () => { expect(formatSeconds(600)).toBe('10:00') }) + + // 計器として等幅で並ぶので、桁が増えても左へ伸びるだけで秒の位置は動かない。 + test('百分を超えても桁を落とさない', () => { + expect(formatSeconds(6_000)).toBe('100:00') + }) }) describe('formatClock', () => { diff --git a/__tests__/client/restore.test.ts b/__tests__/client/restore.test.ts index 00d31fb..4fb10a7 100644 --- a/__tests__/client/restore.test.ts +++ b/__tests__/client/restore.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from 'bun:test' import { restoreConversations } from '@/client/lib/restore' import type { SessionHistory } from '@/client/lib/schemas' +import { sessionHistorySchema } from '@/client/lib/schemas' const history = (histories: SessionHistory['histories']): SessionHistory => ({ sessionId: '8571c162-a7d4-4be9-a14c-2d4ea2780d4f', @@ -145,3 +146,34 @@ describe('restoreConversations', () => { expect(Object.keys(result)).toEqual(['a']) }) }) + +/* + * 相手のIDは三種類ある。uuid の人物、決め打ちの `victim`、そして作者が書いた場所の + * ローカルID。履歴は聞き込みの画面へ入った瞬間に読むので、ここで受けそこねると + * 一手も打たないうちに画面ごと落ちる——実際、場所を足したときに `victim` までしか + * 許しておらず、場所を持つ事件が開いた時点で必ず落ちた。 + */ +describe('sessionHistorySchema の相手ID', () => { + const body = (characterId: string) => ({ + sessionId: '8571c162-a7d4-4be9-a14c-2d4ea2780d4f', + histories: [{ characterId, exchanges: [] }], + }) + + test('人物の uuid を受ける', () => { + expect( + sessionHistorySchema.safeParse(body('7f97837b-ef8f-46ff-a199-377926e8fb75')).success, + ).toBe(true) + }) + + test('遺体の victim を受ける', () => { + expect(sessionHistorySchema.safeParse(body('victim')).success).toBe(true) + }) + + test('場所のローカルIDを受ける', () => { + expect(sessionHistorySchema.safeParse(body('choba')).success).toBe(true) + }) + + test('三者のどれでもない文字列は弾く', () => { + expect(sessionHistorySchema.safeParse(body('帳場')).success).toBe(false) + }) +}) diff --git a/__tests__/client/settings-store.test.ts b/__tests__/client/settings-store.test.ts index 442ccaf..d3ec982 100644 --- a/__tests__/client/settings-store.test.ts +++ b/__tests__/client/settings-store.test.ts @@ -20,59 +20,73 @@ describe('parseSettings: 読めない入力', () => { }) describe('parseSettings: モデルの選択', () => { - test('カタログにある組み合わせはそのまま通る', () => { + test('モデルIDはそのまま通る', () => { const parsed = parseSettings({ - llm: { actor: { provider: 'openai', model: 'gpt-5.6-terra' } }, + llm: { actor: { model: 'gpt-5.6-terra' } }, limits: DEFAULT_SETTINGS.limits, + judge: DEFAULT_SETTINGS.judge, }) - expect(parsed.settings.llm.actor).toEqual({ provider: 'openai', model: 'gpt-5.6-terra' }) + expect(parsed.settings.llm.actor).toEqual({ model: 'gpt-5.6-terra' }) expect(parsed.migrated).toBe(false) }) /* - カタログからモデルが消えても、提供元の選択まで巻き添えで消さない。 - まるごと消えると、プレイヤーには「なぜか設定が戻った」としか見えず直しようがない。 + 提供元を選ばせていた頃の保管庫が、いま遊んでいる人の端末に残っている。 + ここでモデルまで捨てると、プレイヤーには「なぜか設定が戻った」としか見えず、 + しかも気づくのは次に何か操作して上書きされた後になる。 */ - test('消えたモデルIDは落とすが、提供元の選択は残す', () => { + test('提供元付きの古い形は、モデルを残して読み替える', () => { const parsed = parseSettings({ - llm: { actor: { provider: 'openai', model: 'gpt-4-retired' } }, + llm: { actor: { provider: 'anthropic', model: 'claude-sonnet-5' } }, limits: DEFAULT_SETTINGS.limits, }) - expect(parsed.settings.llm.actor).toEqual({ provider: 'openai', model: undefined }) + expect(parsed.settings.llm.actor).toEqual({ model: 'claude-sonnet-5' }) + // 古い形のまま置いておくと provider が消えないので、その場で書き戻させる。 expect(parsed.migrated).toBe(true) }) - test('提供元が読めない役割は落とす', () => { + /* + 互換サーバが何を載せているかは、この端末からは分からない。 + 知らないIDでも落とさず、通らなければサーバ側でエラーになるのに任せる。 + */ + test('見覚えのないモデルIDでも落とさない', () => { + const parsed = parseSettings({ + llm: { actor: { model: 'gpt-4-retired' } }, + limits: DEFAULT_SETTINGS.limits, + }) + + expect(parsed.settings.llm.actor).toEqual({ model: 'gpt-4-retired' }) + }) + + test('モデルが読めない役割は落とす', () => { const parsed = parseSettings({ - llm: { actor: { provider: 'nope', model: 'gpt-5.6-terra' } }, + llm: { actor: { model: 42 }, judge: { provider: 'anthropic' } }, limits: DEFAULT_SETTINGS.limits, }) expect(parsed.settings.llm.actor).toBeUndefined() + expect(parsed.settings.llm.judge).toBeUndefined() expect(parsed.migrated).toBe(true) }) test('壊れた役割があっても、他の役割は残す', () => { const parsed = parseSettings({ llm: { - actor: { provider: 'nope' }, - judge: { provider: 'anthropic', model: 'claude-haiku-4-5' }, + actor: { model: '' }, + judge: { model: 'claude-haiku-4-5' }, }, limits: DEFAULT_SETTINGS.limits, }) expect(parsed.settings.llm.actor).toBeUndefined() - expect(parsed.settings.llm.judge).toEqual({ - provider: 'anthropic', - model: 'claude-haiku-4-5', - }) + expect(parsed.settings.llm.judge).toEqual({ model: 'claude-haiku-4-5' }) }) test('設定できない役割(author など)は拾わない', () => { const parsed = parseSettings({ - llm: { author: { provider: 'openai', model: 'gpt-5.6-sol' } }, + llm: { author: { model: 'gpt-5.6-sol' } }, limits: DEFAULT_SETTINGS.limits, }) @@ -140,3 +154,53 @@ describe('parseSettings: 進行の数値', () => { expect(parsed.settings.limits).toEqual(DEFAULT_SETTINGS.limits) }) }) + +describe('parseSettings: 判定の直し', () => { + test('保管庫に無ければ全部オフ', () => { + const parsed = parseSettings({ llm: {}, limits: DEFAULT_SETTINGS.limits }) + + expect(parsed.settings.judge).toEqual(DEFAULT_SETTINGS.judge) + // この区画より前の保管庫なので、その場で書き戻させて形を揃える。 + expect(parsed.migrated).toBe(true) + }) + + test('入れてある切り替えはそのまま通る', () => { + const parsed = parseSettings({ + llm: {}, + limits: DEFAULT_SETTINGS.limits, + judge: { ...DEFAULT_SETTINGS.judge, checkEvidenceIds: true, retryOnce: true }, + }) + + expect(parsed.settings.judge.checkEvidenceIds).toBe(true) + expect(parsed.settings.judge.retryOnce).toBe(true) + expect(parsed.settings.judge.fixedTemperature).toBe(false) + expect(parsed.migrated).toBe(false) + }) + + /* + 切り替えを増やしたとき、保存済みの端末には新しい鍵が無い。丸ごと捨てると + 既に入れてあったぶんまでオフに戻る。 + */ + test('鍵が欠けていても、他の切り替えは残す', () => { + const parsed = parseSettings({ + llm: {}, + limits: DEFAULT_SETTINGS.limits, + judge: { checkEvidenceIds: true }, + }) + + expect(parsed.settings.judge.checkEvidenceIds).toBe(true) + expect(parsed.settings.judge.retryOnce).toBe(false) + expect(parsed.migrated).toBe(true) + }) + + test('真偽値でない値はオフに落とす', () => { + const parsed = parseSettings({ + llm: {}, + limits: DEFAULT_SETTINGS.limits, + judge: { ...DEFAULT_SETTINGS.judge, checkEvidenceIds: 'はい' }, + }) + + expect(parsed.settings.judge.checkEvidenceIds).toBe(false) + expect(parsed.migrated).toBe(true) + }) +}) diff --git a/__tests__/client/voice.test.ts b/__tests__/client/voice.test.ts new file mode 100644 index 0000000..e174fd8 --- /dev/null +++ b/__tests__/client/voice.test.ts @@ -0,0 +1,61 @@ +import { afterEach, describe, expect, test } from 'bun:test' +import { DEFAULT_VOICE, loadVoiceSetting, saveVoiceSetting } from '@/client/lib/voice' + +/** + * localStorage は「使えない環境がある」前提で触っている。 + * bun のテストには最初から生えていないので、必要なテストだけ差し込む。 + */ +const useStorage = (initial: Record = {}) => { + const store = new Map(Object.entries(initial)) + + Object.defineProperty(globalThis, 'localStorage', { + configurable: true, + value: { + getItem: (key: string) => { + const value = store.get(key) + + return value === undefined ? null : value + }, + setItem: (key: string, value: string) => { + store.set(key, value) + }, + }, + }) +} + +afterEach(() => { + Reflect.deleteProperty(globalThis, 'localStorage') +}) + +describe('loadVoiceSetting', () => { + test('保存されていなければ読み上げる', () => { + useStorage() + + expect(loadVoiceSetting()).toBe(DEFAULT_VOICE) + expect(DEFAULT_VOICE).toBe('on') + }) + + test('保存した値を読み戻せる', () => { + useStorage() + saveVoiceSetting('off') + + expect(loadVoiceSetting()).toBe('off') + }) + + test('知らない値は既定に落とす', () => { + useStorage({ 'alibai:voice': 'loud' }) + + expect(loadVoiceSetting()).toBe(DEFAULT_VOICE) + }) + + /** localStorage ごと無い環境。声が出ないだけで、プレイは続けられる。 */ + test('localStorage が無くても既定を返す', () => { + expect(loadVoiceSetting()).toBe(DEFAULT_VOICE) + }) + + test('打鍵音とは別の鍵に住む', () => { + useStorage({ 'alibai:sound': 'off' }) + + expect(loadVoiceSetting()).toBe('on') + }) +}) diff --git a/__tests__/db/compile-scenario.test.ts b/__tests__/db/compile-scenario.test.ts index 3d3cf56..b8477f1 100644 --- a/__tests__/db/compile-scenario.test.ts +++ b/__tests__/db/compile-scenario.test.ts @@ -393,6 +393,68 @@ describe('compileScenario: 列挙の訳し分け', () => { expect(alpha.personality).toBe('淡々としている。\n\n- ベータ: 同僚(距離を置いている)') }) + test('年ごろと性別は書かれていなければ unknown になる', () => { + expect(alpha.ageGroup).toBe('unknown') + expect(alpha.gender).toBe('unknown') + }) + + test('書かれた年ごろと性別はそのまま列に入る', () => { + const definition = makeMinimal() + const written = compileOrThrow({ + ...definition, + characters: definition.characters.map((character) => + character.id === 'alpha' + ? { ...character, ageGroup: 'senior', gender: 'female' } + : character, + ), + }).characters[0] + + if (written === undefined) throw new Error('characters[0] がありません') + + expect(written.ageGroup).toBe('senior') + expect(written.gender).toBe('female') + }) + + test('声が書かれていなければ両方 null になる', () => { + expect(alpha.voiceSeed).toBeNull() + expect(alpha.voiceCaption).toBeNull() + }) + + test('書かれた声はそのまま列に入る', () => { + const definition = makeMinimal() + const written = compileOrThrow({ + ...definition, + characters: definition.characters.map((character) => + character.id === 'alpha' + ? { ...character, voiceSeed: 1234, voiceCaption: '落ち着いた女性の声。' } + : character, + ), + }).characters[0] + + if (written === undefined) throw new Error('characters[0] がありません') + + expect(written.voiceSeed).toBe(1234) + expect(written.voiceCaption).toBe('落ち着いた女性の声。') + }) + + test('シードを書き忘れた声は検証で落ちる', () => { + const definition = makeMinimal() + const result = compileScenario( + { + ...definition, + characters: definition.characters.map((character) => + character.id === 'alpha' + ? { ...character, voiceCaption: '落ち着いた女性の声。' } + : character, + ), + }, + { isPublished: true, newId: sequentialIds() }, + ) + + expect(result.ok).toBe(false) + expect(result.ok ? [] : result.issues.join('\n')).toContain('voiceSeed') + }) + test('態度が無ければ括弧ごと省く', () => { const beta = minimal.characters[1] expect(beta?.personality).toBe('よく喋る。\n\n- アルファ: 同僚') diff --git a/__tests__/db/scenario-current-authoring.test.ts b/__tests__/db/scenario-current-authoring.test.ts index 1d25a59..d68d03c 100644 --- a/__tests__/db/scenario-current-authoring.test.ts +++ b/__tests__/db/scenario-current-authoring.test.ts @@ -329,6 +329,22 @@ describe('scenario current authoring guide', () => { expect(violations).toEqual([]) }) + test('月見荘で遺体から死亡推定を開くなら、体温と硬直の所見を遺体側にも持つ', async () => { + const tsukimisou = (await scenarios()).find(({ file }) => file === 'tsukimisou.yaml')?.scenario + + expect(tsukimisou).toBeDefined() + if (tsukimisou === undefined) return + + const postmortem = tsukimisou.evidences.find((evidence) => evidence.id === 'postmortem-signs') + const findings = + tsukimisou.victim?.findings.map((finding) => finding.statement).join('\n') ?? '' + + expect(postmortem?.revealsDeathTime).toBe(true) + expect(postmortem?.sources.some((source) => source.type === 'victim')).toBe(true) + expect(findings).toMatch(/温か|体温/) + expect(findings).toContain('硬直') + }) + test('精読で場所調査が有効と判断した事件には、空振りしない調査場所を置く', async () => { const violations: string[] = [] diff --git a/__tests__/llm/detective.test.ts b/__tests__/llm/detective.test.ts index 691b4b0..1a939a0 100644 --- a/__tests__/llm/detective.test.ts +++ b/__tests__/llm/detective.test.ts @@ -7,6 +7,7 @@ const detective = (patch: Partial): Detective => ({ ageGroup: 'young', gender: 'female', appearance: '', + speech: '', ...patch, }) diff --git a/__tests__/llm/judge.test.ts b/__tests__/llm/judge.test.ts index 8dc737b..a4145fc 100644 --- a/__tests__/llm/judge.test.ts +++ b/__tests__/llm/judge.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from 'bun:test' +import { loadJudgeRubric } from '@/server/cache/scenario' import { judgementSchema } from '@/server/llm/judge' const validJudgement = { @@ -9,6 +10,33 @@ const validJudgement = { suggestedQuestions: [], } +describe('loadJudgeRubric', () => { + test('質問しただけでは証拠を開かず、返答で事実が確認できたときだけ開示するよう審判に要求する', async () => { + const kv = { + get: async () => null, + put: async () => undefined, + } as unknown as KVNamespace + const db = { + select: () => ({ + from: () => ({ + where: async () => [ + { + id: 'postmortem-signs', + revealCondition: '死後硬直と体温を確認できたら開示する。', + }, + ], + }), + }), + } as never + + const rubric = await loadJudgeRubric(kv, db, 'scenario-id') + + expect(rubric).toContain('質問しただけ') + expect(rubric).toMatch(/返答.*確認/) + expect(rubric).toMatch(/分からない|確認できない/) + }) +}) + describe('judgementSchema', () => { test('Revelation解禁IDを構造化出力として受け取れる', () => { const parsed = judgementSchema.parse({ diff --git a/__tests__/llm/provider.test.ts b/__tests__/llm/provider.test.ts index 005a7b2..1ecc69f 100644 --- a/__tests__/llm/provider.test.ts +++ b/__tests__/llm/provider.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from 'bun:test' import type { Env } from '@/server/env' -import { cacheHint, chooseLlm, hasApiKey } from '@/server/llm/provider' +import { chooseLlm, chooseLlms } from '@/server/llm/provider' import { toUsageRow } from '@/server/llm/usage' import { LLM_DEFAULT_MODELS } from '~/db/llm-catalog' @@ -9,18 +9,11 @@ import { LLM_DEFAULT_MODELS } from '~/db/llm-catalog' * 残りは型を満たすだけの値で埋める。 */ const makeEnv = (overrides: Partial): Env => ({ - LLM_ACTOR_PROVIDER: 'anthropic', - LLM_JUDGE_PROVIDER: 'anthropic', - LLM_AUTHOR_PROVIDER: 'anthropic', LLM_ACTOR_MODEL: undefined, LLM_JUDGE_MODEL: undefined, LLM_AUTHOR_MODEL: undefined, - ANTHROPIC_API_KEY: 'key-anthropic', OPENAI_API_KEY: 'key-openai', - GOOGLE_GENERATIVE_AI_API_KEY: undefined, - ANTHROPIC_BASE_URL: undefined, - OPENAI_BASE_URL: undefined, - GOOGLE_GENERATIVE_AI_BASE_URL: undefined, + OPENAI_URL: undefined, MAX_TURNS: 5, QUESTIONS_PER_TURN: 1, RATE_LIMIT_MAX_CALLS: 420, @@ -29,109 +22,127 @@ const makeEnv = (overrides: Partial): Env => ({ ...overrides, }) -describe('chooseLlm: 優先順位', () => { - test('指定が無ければ env のプロバイダと既定表のモデル', () => { - expect(chooseLlm(makeEnv({}), 'actor')).toEqual({ - provider: 'anthropic', - modelId: LLM_DEFAULT_MODELS.anthropic.actor, - }) - }) +/** 一覧を返す互換サーバの代わり。テストからネットワークを切る。 */ +const lists = + (...ids: string[]) => + async () => + ids - test('env のモデル指定があればそれを使う', () => { - const env = makeEnv({ LLM_ACTOR_MODEL: 'claude-opus-5' }) +const configured = makeEnv({ OPENAI_URL: 'https://gateway.example.internal/v1' }) - expect(chooseLlm(env, 'actor').modelId).toBe('claude-opus-5') +describe('chooseLlm: 優先順位', () => { + test('env のモデル指定があればそれを使う', async () => { + const env = makeEnv({ LLM_ACTOR_MODEL: 'my-local-model' }) + + expect(await chooseLlm(env, 'actor', undefined, lists('other'))).toBe('my-local-model') }) - test('プレイヤーの指定は env より優先される', () => { - const choice = chooseLlm(makeEnv({}), 'actor', { - provider: 'openai', - model: 'gpt-5.6-luna', - }) + test('プレイヤーの指定は env より優先される', async () => { + const env = makeEnv({ LLM_ACTOR_MODEL: 'my-local-model' }) - expect(choice).toEqual({ provider: 'openai', modelId: 'gpt-5.6-luna' }) + expect(await chooseLlm(env, 'actor', { model: 'another-model' }, lists())).toBe('another-model') }) /* - ここが一番静かに壊れるところ。env の LLM_ACTOR_MODEL は anthropic 向けの値なので、 - プロバイダだけ openai に変えて引き継ぐと openai に claude のIDを投げることになる。 + 突き合わせる許可リストはもう無い。互換サーバに何が生えているかはここからは + 分からないので、見慣れないIDでも既定へ落とさずそのまま通す。 + 通らないIDなら互換サーバがエラーを返す。 */ - test('プロバイダを変えたら env のモデルIDは引き継がず、既定表から引き直す', () => { - const env = makeEnv({ LLM_ACTOR_MODEL: 'claude-opus-5' }) - const choice = chooseLlm(env, 'actor', { provider: 'openai' }) - - expect(choice).toEqual({ provider: 'openai', modelId: LLM_DEFAULT_MODELS.openai.actor }) + test('見慣れないモデルIDでも捨てずに通す', async () => { + expect(await chooseLlm(makeEnv({}), 'actor', { model: 'who-knows-9' }, lists())).toBe( + 'who-knows-9', + ) }) +}) - test('役割ごとに既定のモデルが違う', () => { - const env = makeEnv({}) +describe('chooseLlm: 一覧から既定を選ぶ', () => { + test('既定表と同じ名前が一覧にあればそれを採る', async () => { + const chosen = await chooseLlm( + configured, + 'actor', + undefined, + lists('something-else', LLM_DEFAULT_MODELS.actor), + ) - expect(chooseLlm(env, 'actor').modelId).not.toBe(chooseLlm(env, 'judge').modelId) + expect(chosen).toBe(LLM_DEFAULT_MODELS.actor) }) -}) -describe('chooseLlm: 信用しない入力', () => { - test('カタログに無いモデルIDは黙って捨て、既定へ落とす', () => { - const choice = chooseLlm(makeEnv({}), 'actor', { - provider: 'anthropic', - model: 'claude-imaginary-9', - }) + /* + 接頭辞で経由先を表すゲートウェイがあり、素の名前は一つも生えていない。 + 末尾が一致するIDを拾えないと、必ず存在しないモデルを指名することになる。 + */ + test('接頭辞付きのIDでも末尾が一致すれば拾う', async () => { + const chosen = await chooseLlm( + configured, + 'actor', + undefined, + lists('alpha,other-model', `alpha,${LLM_DEFAULT_MODELS.actor}`), + ) + + expect(chosen).toBe(`alpha,${LLM_DEFAULT_MODELS.actor}`) + }) - expect(choice.modelId).toBe(LLM_DEFAULT_MODELS.anthropic.actor) + test('希望が一つも無ければ一覧の先頭に落ちる', async () => { + expect(await chooseLlm(configured, 'actor', undefined, lists('only-this'))).toBe('only-this') }) /* - キーの無いプロバイダを通すと、応答を流し始めてから SDK の中で落ちる。 - 一番後味の悪い壊れ方なので、選ばれても env のプロバイダのまま進む。 + 一覧が引けないときに既定表をそのまま返すのは、動かすためではなく、 + モデル名がエラーに出るぶん原因が分かるようにするため。 */ - test('APIキーが無いプロバイダの指定は無視する', () => { - const choice = chooseLlm(makeEnv({}), 'actor', { provider: 'google' }) - - expect(choice.provider).toBe('anthropic') + test('一覧が空なら既定表をそのまま返す', async () => { + expect(await chooseLlm(configured, 'actor', undefined, lists())).toBe(LLM_DEFAULT_MODELS.actor) }) - test('キーが有れば同じ指定が通る', () => { - const env = makeEnv({ GOOGLE_GENERATIVE_AI_API_KEY: 'key-google' }) + test('鍵が無ければ一覧を引きに行かない', async () => { + const asked = { count: 0 } - expect(chooseLlm(env, 'actor', { provider: 'google' }).provider).toBe('google') - }) -}) + await chooseLlm(makeEnv({ OPENAI_API_KEY: undefined }), 'actor', undefined, async () => { + asked.count += 1 -describe('hasApiKey', () => { - test('鍵の有無だけを見る', () => { - const env = makeEnv({}) + return [] + }) - expect(hasApiKey(env, 'anthropic')).toBe(true) - expect(hasApiKey(env, 'google')).toBe(false) + expect(asked.count).toBe(0) }) }) -/* - cacheHint が env と role ではなく決定済みの choice を受けるのは、 - 「モデルは openai なのに anthropic のキャッシュ指定が付く」ズレを型で書けなくするため。 -*/ -describe('cacheHint', () => { - test('anthropic のときだけキャッシュ指定を返す', () => { - expect(cacheHint({ provider: 'anthropic', modelId: 'claude-sonnet-5' })).toEqual({ - anthropic: { cacheControl: { type: 'ephemeral' } }, +describe('chooseLlms', () => { + test('役割ごとに別のモデルを返す', async () => { + const chosen = await chooseLlms( + configured, + {}, + lists(`x,${LLM_DEFAULT_MODELS.actor}`, `x,${LLM_DEFAULT_MODELS.judge}`), + ) + + expect(chosen).toEqual({ + actor: `x,${LLM_DEFAULT_MODELS.actor}`, + judge: `x,${LLM_DEFAULT_MODELS.judge}`, }) }) - test('他のプロバイダでは空', () => { - expect(cacheHint({ provider: 'openai', modelId: 'gpt-5.6-terra' })).toEqual({}) - expect(cacheHint({ provider: 'google', modelId: 'gemini-3.5-flash' })).toEqual({}) + /* 両方とも決まっているなら、一覧を引く理由が無い。 */ + test('両方指定されていれば一覧を引かない', async () => { + const asked = { count: 0 } + const env = makeEnv({ LLM_ACTOR_MODEL: 'a', LLM_JUDGE_MODEL: 'b' }) + const chosen = await chooseLlms(env, {}, async () => { + asked.count += 1 + + return [] + }) + + expect(chosen).toEqual({ actor: 'a', judge: 'b' }) + expect(asked.count).toBe(0) }) }) describe('toUsageRow', () => { /* - 使用量の provider 列は env ではなく実際に使った choice から取る。 - env から引き直すと、プレイヤーがプロバイダを差し替えたセッションの記録が - 静かに嘘になり、コストの内訳が追えなくなる。 + model は設定値ではなく応答が名乗ったIDを入れる。設定から引き直すと、 + 互換サーバが別名へ振り替えたときに記録が静かに嘘になる。 */ - test('env ではなく実際に使ったプロバイダを記録する', () => { + test('実際に応答したモデルを記録する', () => { const row = toUsageRow({ - choice: { provider: 'openai', modelId: 'gpt-5.6-terra' }, role: 'actor', model: 'gpt-5.6-terra', usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 }, @@ -140,16 +151,15 @@ describe('toUsageRow', () => { scenarioId: 'scenario-1', }) - expect(row.provider).toBe('openai') + expect(row.model).toBe('gpt-5.6-terra') expect(row.role).toBe('actor') expect(row.inputTokens).toBe(10) }) test('未報告のトークン数は0として数える', () => { const row = toUsageRow({ - choice: { provider: 'anthropic', modelId: 'claude-sonnet-5' }, role: 'judge', - model: 'claude-sonnet-5', + model: 'gpt-5.6-luna', usage: { inputTokens: undefined, outputTokens: undefined, totalTokens: undefined }, providerMetadata: undefined, sessionId: 'session-1', diff --git a/__tests__/server/judge-tuning.test.ts b/__tests__/server/judge-tuning.test.ts new file mode 100644 index 0000000..c8ffd46 --- /dev/null +++ b/__tests__/server/judge-tuning.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, test } from 'bun:test' +import { acceptRevealedEvidenceIds } from '@/server/game/discovery-state' + +/* + 証拠の突き合わせは、Revelation 側の acceptRevealedRevelationIds と同じ役目を + 証拠に付けたもの(__tests__/server/revelations.test.ts と対になる)。 +*/ + +const scenarioEvidenceIds = ['glass', 'letter', 'key'] + +describe('acceptRevealedEvidenceIds', () => { + test('シナリオに無いIDは採用しない', () => { + expect(acceptRevealedEvidenceIds(scenarioEvidenceIds, [], ['glass', 'hallucinated'])).toEqual([ + 'glass', + ]) + }) + + test('すでに見つけている証拠は採用しない', () => { + expect(acceptRevealedEvidenceIds(scenarioEvidenceIds, ['glass'], ['glass', 'letter'])).toEqual([ + 'letter', + ]) + }) + + test('同じIDを二度返されても一度だけ採る', () => { + expect(acceptRevealedEvidenceIds(scenarioEvidenceIds, [], ['key', 'key'])).toEqual(['key']) + }) + + test('採るものが無ければ空', () => { + expect(acceptRevealedEvidenceIds(scenarioEvidenceIds, ['glass'], ['glass'])).toEqual([]) + }) +}) diff --git a/__tests__/server/llm-settings.test.ts b/__tests__/server/llm-settings.test.ts index 7ab62bc..1265ef4 100644 --- a/__tests__/server/llm-settings.test.ts +++ b/__tests__/server/llm-settings.test.ts @@ -9,20 +9,15 @@ import { LIMIT_CEILINGS } from '@/shared/turns' * 鍵の入った env を直接渡して組み立てる。HTTP 越しに確かめようとすると、 * バインディングの無いテスト環境では withEnv が先に落ちて 500 になり、 * 「本文に鍵が無い」がただの空振りになってしまう。 + * + * 一覧の取得も差し替える。互換サーバへ本当に出ていくと、テストが手元の構成に依存する。 */ const envWithKeys: Env = { - LLM_ACTOR_PROVIDER: 'openai', - LLM_JUDGE_PROVIDER: 'openai', - LLM_AUTHOR_PROVIDER: 'openai', LLM_ACTOR_MODEL: undefined, LLM_JUDGE_MODEL: undefined, LLM_AUTHOR_MODEL: undefined, - ANTHROPIC_API_KEY: 'sk-ant-secret-value', OPENAI_API_KEY: 'sk-openai-secret-value', - GOOGLE_GENERATIVE_AI_API_KEY: undefined, - ANTHROPIC_BASE_URL: 'https://gateway.example.internal/v1', - OPENAI_BASE_URL: 'https://gateway.example.internal/v1', - GOOGLE_GENERATIVE_AI_BASE_URL: undefined, + OPENAI_URL: 'https://gateway.example.internal/v1', MAX_TURNS: 5, QUESTIONS_PER_TURN: 1, RATE_LIMIT_MAX_CALLS: 420, @@ -30,48 +25,68 @@ const envWithKeys: Env = { RETENTION_DAYS: 90, } +const listsTwo = async () => ['model-alpha', 'model-beta'] + describe('buildLlmSettings', () => { - test('鍵の有無だけを真偽値で伝える', () => { - const payload = buildLlmSettings(envWithKeys) - const availability = Object.fromEntries( - payload.providers.map((provider) => [provider.id, provider.available]), + test('互換サーバが返したモデルをそのまま並べる', async () => { + const payload = await buildLlmSettings(envWithKeys, listsTwo) + + expect(payload.models).toEqual([ + { id: 'model-alpha', label: 'model-alpha' }, + { id: 'model-beta', label: 'model-beta' }, + ]) + }) + + /* + 鍵か向き先が無ければ聞きに行かない。ここで空配列を返せるかどうかが、 + 未設定のまま設定画面を開いたときに待たされないことの担保になる。 + */ + test('鍵が無ければ聞きに行かず空で返す', async () => { + const asked = { count: 0 } + const payload = await buildLlmSettings( + { ...envWithKeys, OPENAI_API_KEY: undefined }, + async () => { + asked.count += 1 + + return ['model-alpha'] + }, ) - expect(availability).toEqual({ anthropic: true, openai: true, google: false }) + expect(payload.models).toEqual([]) + expect(asked.count).toBe(0) + }) + + test('向き先が無ければ聞きに行かず空で返す', async () => { + const payload = await buildLlmSettings({ ...envWithKeys, OPENAI_URL: undefined }, listsTwo) + + expect(payload.models).toEqual([]) }) /* - 漏洩の回帰テスト。鍵そのものも、ゲートウェイの向き先も応答に載ってはいけない。 - ベースURLは設定できない方針なので、存在すら漏らさない——漏らせば、 + 漏洩の回帰テスト。鍵そのものも、互換サーバの向き先も応答に載ってはいけない。 + 向き先は設定できない方針なので、存在すら漏らさない——漏らせば、 どこを狙えばよいかを教えることになる。 */ - test('鍵の値もゲートウェイの向き先も本文に現れない', () => { - const text = JSON.stringify(buildLlmSettings(envWithKeys)) + test('鍵の値も互換サーバの向き先も本文に現れない', async () => { + const text = JSON.stringify(await buildLlmSettings(envWithKeys, listsTwo)) - expect(text).not.toContain('sk-ant-secret-value') expect(text).not.toContain('sk-openai-secret-value') expect(text).not.toContain('gateway.example.internal') expect(text).not.toContain('http') expect(text).not.toContain('API_KEY') - expect(text).not.toContain('BASE_URL') + expect(text).not.toContain('OPENAI_URL') }) - test('選ばせる役割は会話と判定の2つだけ(author は出さない)', () => { - const payload = buildLlmSettings(envWithKeys) + test('選ばせる役割は会話と判定の2つだけ(author は出さない)', async () => { + const payload = await buildLlmSettings(envWithKeys, listsTwo) expect(payload.roles.map((role) => role.id)).toEqual(['actor', 'judge']) }) - test('画面の入力欄が読む上限を返す', () => { - const payload = buildLlmSettings(envWithKeys) + test('画面の入力欄が読む上限を返す', async () => { + const payload = await buildLlmSettings(envWithKeys, listsTwo) expect(payload.limits.maxTurns).toEqual({ value: 5, max: LIMIT_CEILINGS.maxTurns }) expect(payload.limits.totalQuestions.max).toBe(LIMIT_CEILINGS.totalQuestions) }) - - test('各プロバイダに選べるモデルが1つ以上ある', () => { - for (const provider of buildLlmSettings(envWithKeys).providers) { - expect(provider.models.length).toBeGreaterThan(0) - } - }) }) diff --git a/__tests__/server/scenario-cache.test.ts b/__tests__/server/scenario-cache.test.ts index cf7d655..c0ae8cd 100644 --- a/__tests__/server/scenario-cache.test.ts +++ b/__tests__/server/scenario-cache.test.ts @@ -1,27 +1,54 @@ import { describe, expect, test } from 'bun:test' import { buildCharacterSheet } from '@/server/cache/scenario' +const row = (overrides: Partial[0]> = {}) => ({ + id: '00000000-0000-4000-8000-000000000001', + scenarioId: '00000000-0000-4000-8000-000000000002', + name: '証人A', + shortName: '証人', + ageGroup: 'unknown' as const, + gender: 'unknown' as const, + publicIntroduction: '受付担当。', + personality: '落ち着いている。', + knowledge: '公開された範囲のことを知る。', + secrets: '秘密は話さない。', + goals: '仕事を続ける。', + lies: 'なし', + lieRefs: [], + memories: '事件前の記憶。', + ...overrides, +}) + +const BRIEFING = 'この施設では毎年、関係者だけの催しが開かれていた。' + describe('buildCharacterSheet', () => { test('NPC はキャラクター固有情報に加えて事件の公開記録も共有する', () => { - const sheet = buildCharacterSheet( - { - id: '00000000-0000-4000-8000-000000000001', - scenarioId: '00000000-0000-4000-8000-000000000002', - name: '証人A', - publicIntroduction: '受付担当。', - personality: '落ち着いている。', - knowledge: '公開された範囲のことを知る。', - secrets: '秘密は話さない。', - goals: '仕事を続ける。', - lies: 'なし', - memories: '事件前の記憶。', - }, - 'この施設では毎年、関係者だけの催しが開かれていた。', - ) + const sheet = buildCharacterSheet(row(), BRIEFING) expect(sheet).toContain('## 事件の公開記録') - expect(sheet).toContain('この施設では毎年、関係者だけの催しが開かれていた。') + expect(sheet).toContain(BRIEFING) expect(sheet).toContain('## 人物像') expect(sheet).toContain('落ち着いている。') }) + + test('年ごろと性別は人物像の一行目に入る', () => { + const sheet = buildCharacterSheet(row({ ageGroup: 'teen', gender: 'female' }), BRIEFING) + + expect(sheet).toContain( + '## 人物像\n年ごろは十代(13〜19歳ごろ)。性別は女性。\n落ち着いている。', + ) + }) + + test('unknown の側は落ちる', () => { + const sheet = buildCharacterSheet(row({ ageGroup: 'elder' }), BRIEFING) + + expect(sheet).toContain('## 人物像\n年ごろは老齢(70代以上)。\n落ち着いている。') + expect(sheet).not.toContain('性別') + }) + + test('どちらも書かれていなければ人物像は文章だけ', () => { + const sheet = buildCharacterSheet(row(), BRIEFING) + + expect(sheet).toContain('## 人物像\n落ち着いている。') + }) }) diff --git a/__tests__/server/speakers.test.ts b/__tests__/server/speakers.test.ts new file mode 100644 index 0000000..6894c18 --- /dev/null +++ b/__tests__/server/speakers.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, test } from 'bun:test' +import { assignSpeakers, speakerFor } from '@/server/tts/irodori' + +const pool = (prefix: string, count: number) => + Array.from({ length: count }, (_value, index) => ({ id: `${prefix}-${index}` })) + +/** 束は「年ごろまで合う声」「性別だけ合う声」「全員」の順を想定している。 */ +const person = (id: string, tiers: { id: string }[][]) => ({ id, tiers }) + +describe('assignSpeakers', () => { + test('同じ束から取る二人に、同じ声を当てない', () => { + const same = pool('young-female', 4) + const assigned = assignSpeakers([person('a', [same]), person('b', [same])]) + + expect(assigned.get('a')?.id).not.toBe(assigned.get('b')?.id) + }) + + test('年ごろまで合う声が一体しか無いときは、二人目が年ごろを譲る', () => { + const senior = pool('senior', 1) + const sameGender = [...senior, ...pool('adult', 3)] + const assigned = assignSpeakers([ + person('a', [senior, sameGender]), + person('b', [senior, sameGender]), + ]) + + // 一人目は年ごろまで合う一体を取り、二人目は同じ性別の別の声へ落ちる。 + expect(new Set([assigned.get('a')?.id, assigned.get('b')?.id]).size).toBe(2) + expect([assigned.get('a')?.id, assigned.get('b')?.id]).toContain('senior-0') + }) + + test('性別の合う声が無ければ、最後の束(全員)から取る', () => { + const everyone = pool('any', 3) + const assigned = assignSpeakers([person('a', [[], [], everyone])]) + + expect(assigned.get('a')?.id).toStartWith('any-') + }) + + test('どの束も空なら声を当てない', () => { + expect(assignSpeakers([person('a', [[], [], []])]).size).toBe(0) + }) + + test('全部塞がったら重なりを許す。声が出ないより似ているほうを採る', () => { + const only = pool('only', 1) + const assigned = assignSpeakers([person('a', [only]), person('b', [only])]) + + expect(assigned.get('a')?.id).toBe('only-0') + expect(assigned.get('b')?.id).toBe('only-0') + }) + + test('渡す順を変えても同じ割り当てになる(IDの昇順で配る)', () => { + const same = pool('same', 5) + const cast = [person('c', [same]), person('a', [same]), person('b', [same])] + const forward = assignSpeakers(cast) + const backward = assignSpeakers([...cast].reverse()) + + for (const id of ['a', 'b', 'c']) { + expect(forward.get(id)?.id).toBe(backward.get(id)?.id) + } + }) +}) + +describe('speakerFor', () => { + test('同じ人物には毎回同じ声が当たる', () => { + const candidates = pool('voice', 7) + + expect(speakerFor(candidates, 'person-1')?.id).toBe(speakerFor(candidates, 'person-1')?.id) + }) + + test('候補が空なら決まらない', () => { + expect(speakerFor([], 'person-1')).toBeUndefined() + }) +}) diff --git a/__tests__/client/paragraphs.test.ts b/__tests__/shared/paragraphs.test.ts similarity index 74% rename from __tests__/client/paragraphs.test.ts rename to __tests__/shared/paragraphs.test.ts index ef79e94..91fc06f 100644 --- a/__tests__/client/paragraphs.test.ts +++ b/__tests__/shared/paragraphs.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'bun:test' -import { splitParagraphs } from '@/client/lib/paragraphs' +import { messageParagraphs, splitParagraphs } from '@/shared/paragraphs' describe('splitParagraphs', () => { test('空行1つで区切られた段落を分割する', () => { @@ -79,3 +79,29 @@ describe('splitParagraphs(長い段落の分割)', () => { ]) }) }) + +/** + * 読み上げは「この発言の N 行目」で頼むので、画面とサーバがここを同じに割る。 + * ずれると、出ている行と読まれる行が食い違う。 + */ +describe('messageParagraphs', () => { + const sentence = (chars: number) => `${'あ'.repeat(chars - 1)}。` + + test('単なる改行も段落の切れ目として扱う', () => { + expect(messageParagraphs(`${sentence(10)}\n${sentence(10)}`)).toEqual([ + sentence(10), + sentence(10), + ]) + }) + + test('連続した改行はひとつの切れ目', () => { + expect(messageParagraphs(`${sentence(10)}\n\n\n${sentence(10)}`)).toHaveLength(2) + }) + + test('長い段落は文の切れ目で割れ、添字がそのまま行番号になる', () => { + const said = messageParagraphs(`${sentence(40)}${sentence(40)}`) + + expect(said).toHaveLength(2) + expect(said[1]).toBe(sentence(40)) + }) +}) diff --git a/bun.lock b/bun.lock index 9fc3f96..504a1dd 100644 --- a/bun.lock +++ b/bun.lock @@ -5,23 +5,21 @@ "": { "name": "alibai", "dependencies": { - "@ai-sdk/anthropic": "^2", - "@ai-sdk/google": "^2", - "@ai-sdk/openai": "^2", - "@ai-sdk/provider-utils": "^3", + "@ai-sdk/openai": "^2.0.126", + "@ai-sdk/provider-utils": "^3.0.36", "@commitlint/config-conventional": "^21.2.2", - "@tanstack/react-router": "^1.170.32", - "@tanstack/react-start": "^1.168.49", + "@tanstack/react-router": "^1.170.33", + "@tanstack/react-start": "^1.168.50", "@vitejs/plugin-react": "^6.1.1", - "ai": "^5", + "ai": "^5.0.254", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "commitlint": "^21.2.2", "conventional-changelog-conventionalcommits": "^10.4.0", "dayjs": "^1.11.23", "drizzle-orm": "^0.45.2", - "hono": "^4.13.5", - "lucide-react": "^1.38.0", + "hono": "^4.13.7", + "lucide-react": "^1.43.0", "radix-ui": "^1.6.7", "react": "^19.2.8", "react-dom": "^19.2.8", @@ -29,22 +27,22 @@ "zod": "^4.5.4", }, "devDependencies": { - "@biomejs/biome": "^2.5.11", - "@cloudflare/vite-plugin": "^1.54.2", - "@cloudflare/workers-types": "^5.20260831.1", + "@biomejs/biome": "^2.5.12", + "@cloudflare/vite-plugin": "^1.54.6", + "@cloudflare/workers-types": "^5.20260908.1", "@storybook/react-vite": "10.5.10", "@tailwindcss/vite": "^4.3.3", - "@types/bun": "^1.4.0", - "@types/node": "^26.4.0", + "@types/bun": "^1.4.2", + "@types/node": "^26.5.0", "@types/react": "^19.2.18", - "@types/react-dom": "^19.2.5", + "@types/react-dom": "^19.2.7", "drizzle-kit": "^0.31.10", - "playwright": "^1.62.1", + "playwright": "^1.63.0", "storybook": "10.5.10", "tailwindcss": "^4.3.3", "typescript": "^7.0.2", "vite": "^8.2.2", - "wrangler": "^4.127.1", + "wrangler": "^4.130.0", "yaml": "^2.9.0", }, }, @@ -52,13 +50,9 @@ "packages": { "@adobe/css-tools": ["@adobe/css-tools@4.5.0", "", {}, "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q=="], - "@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.101", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.36" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-jLAtSFPiB9mftqZyIbZ/FXIB/jBwgbrbVM9M6jDGbSapYCsqUlfbiWszjkEiX2nHK+BxgeQOPqKbWyuzFJPBqA=="], + "@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.148", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.36", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-j6ObDCcvDNUtEUzPLiBVB6EsJNbeIAGtWd4cxW0MQvQldllrLGPRvEDJNfYP7MbSmRDDjfsNnDnH5jmRwXngVw=="], - "@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.145", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.36", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-yDnrdxwKBF9kYHpS99c7GreufknlNU9ecXqYSjigq3Pv5OUFNVeEGwfbL7ninYNU13p4SlXoRmRlmymWPLZMVQ=="], - - "@ai-sdk/google": ["@ai-sdk/google@2.0.95", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.36" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-nE45RIjdpJRMK4vFbGL1xvOBsE7jsC3niDW+UrHLBf4T3YTm7gjuX+COycsmLR6opKpZcqHwF7SY/pN9mqHfwQ=="], - - "@ai-sdk/openai": ["@ai-sdk/openai@2.0.123", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.36" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-KB9KY1yxTGkeYE7lskYydS7AkAvEdTSALJfsSsq8oDgTj4zJMfKWS+K5yJVp7ErWZ+tRyMqx3aepnxPsv9fWeA=="], + "@ai-sdk/openai": ["@ai-sdk/openai@2.0.126", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.36" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-LNNz+yUEVxIKmTCBJhzk1k7fKij4gGJCGsPUK98vBpv5pEKp1FQ9Um2vcm+XbaankRw+jeXuk9GMOoMVrDdfFQ=="], "@ai-sdk/provider": ["@ai-sdk/provider@2.0.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-h88OPkavHTiN9tMn2l5awAznGB0lXzjcLhgR1/rvjB2zlLprsNxbM2tt6OJsHUxduLC3klq0/eqaSf6fX5XVww=="], @@ -98,41 +92,41 @@ "@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], - "@biomejs/biome": ["@biomejs/biome@2.5.11", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.11", "@biomejs/cli-darwin-x64": "2.5.11", "@biomejs/cli-linux-arm64": "2.5.11", "@biomejs/cli-linux-arm64-musl": "2.5.11", "@biomejs/cli-linux-x64": "2.5.11", "@biomejs/cli-linux-x64-musl": "2.5.11", "@biomejs/cli-win32-arm64": "2.5.11", "@biomejs/cli-win32-x64": "2.5.11" }, "bin": { "biome": "bin/biome" } }, "sha512-Tj0dnkLPdW0ASjHfj2D/ZkkvPU2wrFmnE1jWTD2xzV1ycapV1DutbYXk4NDnR3rYTi1ZCbNFD4G2gRMEY65WaA=="], + "@biomejs/biome": ["@biomejs/biome@2.5.12", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.12", "@biomejs/cli-darwin-x64": "2.5.12", "@biomejs/cli-linux-arm64": "2.5.12", "@biomejs/cli-linux-arm64-musl": "2.5.12", "@biomejs/cli-linux-x64": "2.5.12", "@biomejs/cli-linux-x64-musl": "2.5.12", "@biomejs/cli-win32-arm64": "2.5.12", "@biomejs/cli-win32-x64": "2.5.12" }, "bin": { "biome": "bin/biome" } }, "sha512-Lw4VHZRebrReBBnlHa12JQjnIBm3JJAA55PDB9LbBVBF0q4RYphm6KfmIjqtPhf61MxZ5Q9KoK8R8x+7per5Aw=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-6SGZxoKbXvUjMn1t6A98HqWISPnGNbYs0R/Rt2JarmXBSev+lva4QxUMWEBX9lX1Wo1XTJ78uk5xVDtG58SRZg=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.5.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lCRY1rwgNeWNgTr4DI/u6ZwXTRwRLHAvbaio1YLLGS+4r1nhvB2ssyPqIpfUSmRveNfv0fn/N58C7CAdK2XVrg=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-nYkXY7tLBEgnGbYapDKAyKzgt44ZEyG+AKalvTXtCWKYgepI9dw327q+cVgedxm+Udi1ZzHKUyZrIusHi/KQbw=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.5.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-vhPgwnh+6tN3ArdAXuET99xaNbFt7CG82Bqn+omHVLC5xdVx45JsYjGPmUIGNzjDek5XdNCP1HKksK7fn8+3bQ=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-3PVLSTD9RR73rvVPt5G3T1gc+ycggWEGfTD7RvzzbtcDPD27NxgxBbAFfpm7DXJKW6VLHWE1lLMGvFt2Qxjcow=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.5.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-2gp8aVwXYKdAtmBfRFCUuyDMcfN1ahHqUkGfLYrZlNRFmryMATLVvJgWKvyA8wu4Rwn5OSxM1UcUmOuOFNGeBQ=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-qhyZUMyCbWYFV2bAwRNVvfMVZ+hv7WYl6mossGrxC+uiQQXhvsuWWU8zz6jYX0mChZd9MgQZbm4vozTmG/5iGw=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.5.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-couHYjFLL5uuI8ne6zhT7KwEsXo5YP7ry/2xmEqah7qanu0YmfDi3mwJg47YXSuv/NpZj22CZzcRH/5c4gjPSQ=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.11", "", { "os": "linux", "cpu": "x64" }, "sha512-JOytptlsgM33B2MMFUg8iBrb4IKpbD5JnJrSeYiaFEeAj4vuXx0iQSQZ4qK7sqyMtfjZxxPdNdMZZVL4y/mFyA=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.5.12", "", { "os": "linux", "cpu": "x64" }, "sha512-SnvOs3TSTiuia4SQOUNe1aWC9RT4+YkjcKnOhL/nsKOV0k5ycgBkDzF0lUxKn1V7Q8CLTRq6iV23ZAivHomRoA=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.11", "", { "os": "linux", "cpu": "x64" }, "sha512-oRRlrchG5EfrEL/EmtT1qUjSNHk3/5LGeZhQqADBBAJF1b1ET6964xEKe7aGlGARzDfza8H/seEsFJl7S6Ql9w=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.5.12", "", { "os": "linux", "cpu": "x64" }, "sha512-8A0oDW58/w9f/PQNYuq0sGUZtGtGrkNF4Z6n0PUoXpLCshi85vtKTv1XSznQawhdE4MXJ8ufpzHXyLFe87M/+w=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-e49E6K9hzH/ohJNx8Y26mY8HaV4I4ZViIeoqhKsmoXLKHhQnMeBAVqCgsGf2Wa3lXlS7RkporDXMHHWkzvZzFw=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.5.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-b9vtoZFsuZt1pdjNwJvXl0f+BpayRzV008uS2+JpmwIKdSE2qdu4A/l04FESwLoou5g2E/Qlec0xwJydZplH+A=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.11", "", { "os": "win32", "cpu": "x64" }, "sha512-QSQr/KjOgXA7OzXJUWS+oguKyAZ3Q0l/lnlDGbu397eKo83atuWUjBPJrsqbKNF6CARGw8XXJLGzpHC8Ryhd4Q=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.12", "", { "os": "win32", "cpu": "x64" }, "sha512-B1R/l+CwEpKFSuqiwePzPNRk1EiJN8kc0UhdafNz6MZN9v5OFP9HYP1irptvWzHrwVI4blVNGMbxc5zt70m3IA=="], "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.5.0", "", {}, "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg=="], "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw=="], - "@cloudflare/vite-plugin": ["@cloudflare/vite-plugin@1.54.2", "", { "dependencies": { "@cloudflare/unenv-preset": "2.16.1", "miniflare": "5.20260828.0-alpha", "unenv": "2.0.0-rc.24", "workerd": "1.20260828.1", "wrangler": "4.127.1", "ws": "8.21.0" }, "peerDependencies": { "vite": "^6.1.0 || ^7.0.0 || ^8.0.0" }, "bin": { "cf-vite": "bin/cf-vite" } }, "sha512-15mH5ARHTkNZAqa5GhedjLAt/MCiGBEo09Hgf82EiWuIm/5yOaMVL6ABSun/W8C3Vbw1clzW/2i9IfH5zT/Kvg=="], + "@cloudflare/vite-plugin": ["@cloudflare/vite-plugin@1.54.6", "", { "dependencies": { "@cloudflare/unenv-preset": "2.16.1", "miniflare": "5.20260908.0-alpha", "unenv": "2.0.0-rc.24", "workerd": "1.20260908.1", "wrangler": "4.130.0", "ws": "8.21.0" }, "peerDependencies": { "vite": "^6.1.0 || ^7.0.0 || ^8.0.0" }, "bin": { "cf-vite": "bin/cf-vite" } }, "sha512-7G85rvY9gaLYOLA/0Q1rQPx6l7wrY763b67FEVXyWoFUriiLljemBkbsFsnVYjrJyZ+yqHQ6gNCRX//hAzSc0g=="], - "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260828.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-CVd+xPhqUESg8Xhq09TZx0wl4FSirfJGOzvbPz2yHhBIvmNHFFQkSN3rkd7wEwnhQQk37Xi0/aD6ykPLJbmGiQ=="], + "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260908.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-t3juyCXFn12OklBL0S7UC98py3nLEmuioBOUOazeyeVsLUu8fv+pfJrR+XzwSH2Uh6rCXZNogRh+LtV0YpzMcQ=="], - "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260828.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5HDPXRM152vU5JveByGFk34X57TVyIsfp4cabepAf45DC0MKvm52ucJqAjW1h8bvW4X+zRw9GU35OHF9FEC9Ww=="], + "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260908.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-I1nwA4qm/fUNSKhPSO76YA6JAhcA0KU63yFcYHN6AiDowGbDyfjDPH9KCVopT5rI1LY4GfbdAi5b9gODqZsAkQ=="], - "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260828.1", "", { "os": "linux", "cpu": "x64" }, "sha512-MQ1Ll9P7F72HHUKizbb7BlDfbY8fRoNMpbIpZoU6uKsSkneFICWSKv6UlgU9EQZ+w0i7TMa12iUgJ8l29eRI9A=="], + "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260908.1", "", { "os": "linux", "cpu": "x64" }, "sha512-s/h5uSW1UC6dGeVKSqrPLpTu+vo0fKJZCNEokW1Ol1qcCB2oN6WCRHhoyzo4Y69oONAXRgLfLBYaHWe7z51Vnw=="], - "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260828.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-FBTaUQ1xcU9jcp4OyBPcH8x0QiFvc1iuZL2GkD8zp2q1WyTVHYOptRDQUU+cuHjt0rQ2EIKVPBjahPxfa0joBw=="], + "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260908.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-PP/nUKl0R6colwfocggL8dctO/dFMqMft12unzFUkoAJr0aXQeT+ia0JuNmOUWXe6EfvyCSmAbijEsTKQ5t/ew=="], - "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260828.1", "", { "os": "win32", "cpu": "x64" }, "sha512-yvr77hC7dUbvK5K+SCg062kkPq3sx+drV1PcgHslzHDYcJBtT0V3X80qLE49LW1vq2svaeNmsVQS+vHsqWu8cQ=="], + "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260908.1", "", { "os": "win32", "cpu": "x64" }, "sha512-jkaS5EKKTvzAdIlbMfOYrHoTucWVRwYBwIig+xRsjXQv5BXTLA2Llg+iM8djlKtk0CjkztZhXmuxuqoqWKZmFw=="], - "@cloudflare/workers-types": ["@cloudflare/workers-types@5.20260831.1", "", {}, "sha512-yXg4pwfYjhsDH9rYc3qZ3K+z62DCSvO/aj7GiZo6AyDeWGZpyFRpPMYcQ6LF/zfaf1x0Ngw2gSqL8JjuUtMGlA=="], + "@cloudflare/workers-types": ["@cloudflare/workers-types@5.20260908.1", "", {}, "sha512-cILmYEd/YtL+Hlwytc5n+QVV8F+E5doQOtc6QiBtPb51kK+5fkk909db+G8dxeUsMd2ytJgdpAt/lyciUEobcw=="], "@commitlint/cli": ["@commitlint/cli@21.2.2", "", { "dependencies": { "@commitlint/config-conventional": "^21.2.2", "@commitlint/format": "^21.2.2", "@commitlint/lint": "^21.2.2", "@commitlint/load": "^21.2.2", "@commitlint/read": "^21.2.1", "@commitlint/types": "^21.2.0", "tinyexec": "^1.0.0", "yargs": "^18.0.0" }, "bin": { "commitlint": "cli.js" } }, "sha512-a+6hQxIxnpdvSvS2apvttPNbEliYsVC3PqFYDiiB2kjbwIsQsj1urvQ4Tkf70pKYozPalKAuRQmm/GHwndduqA=="], @@ -532,35 +526,35 @@ "@radix-ui/rect": ["@radix-ui/rect@1.1.3", "", {}, "sha512-JtyZR+mqgBibTo8xea3B6ZRmzZiM/YeVBtUkas6zMuXjAlfIFIW2FgqeM9eLyvEaYX66vr6DJMK+4U6LV0KhNw=="], - "@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.6", "", { "os": "android", "cpu": "arm" }, "sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ=="], + "@rolldown/binding-android-arm-eabi": ["@rolldown/binding-android-arm-eabi@1.2.8", "", { "os": "android", "cpu": "arm" }, "sha512-tN5aztYkKCte4i5SIrrz5yK/HMjEuCqCSCJa418jOV8tZ1cBY3YF2otxB1ktPxzsLA1BeTqwapK0bfjxNvHJVw=="], - "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.6", "", { "os": "android", "cpu": "arm64" }, "sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.2.8", "", { "os": "android", "cpu": "arm64" }, "sha512-dIYTWl9XprMUiQFoc55KUyk/oS8SKYH3zFl0LTR7RT0Xj4hgSVyuJcroH8JUu8RcpF8fTB6E0aOwCkZoYPcDSQ=="], - "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA=="], + "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.2.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-PCSDQGXD2IyTEFrcgPyBM8jJuGmrbCMuoIOXdbEGVemruKACXoLQJrb+A45Z0L5t1RQkdfJprAYPkikbh7dzdA=="], - "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q=="], + "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.2.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-Uk7lRsGhPFHVX/sAUC6D5H9Ol30dFHd6iquokll2th3LpdJ3F5CzQB+7DHn0Ri2mG+U7k2zXiPHDrwZenXhwSA=="], - "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA=="], + "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.2.8", "", { "os": "freebsd", "cpu": "x64" }, "sha512-DjszaTEVogPqA5bYzsEeqDCQxbcp2fexQwKcRspYji2yzR68fCf+e4fx6kBSRDwX5/brZaHw/hWS9+A/+/w9sQ=="], - "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.6", "", { "os": "linux", "cpu": "arm" }, "sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w=="], + "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.2.8", "", { "os": "linux", "cpu": "arm" }, "sha512-zmwa7FTmdzB6aaEEuuls18H6Ap5JmJPSoPTuXixeJZV6tG40SyLkApQtz1g8ptZtiEKqj9OM0oNLPh1AgvE31Q=="], - "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg=="], + "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.2.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-KdYQDPHwJVnbFwdTGMgxsI9SqblBlz6STGM+w1We/d5B8OWWidYH0MwkU/uA1wM5fIpO2MkOVxXrNzzuZhw9ew=="], - "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw=="], + "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.2.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-jFJTifHnNPY+yzOoNZQfSIysrVyXzEQPhPnOUjmD1bcQGHH6s7c8cViKWar8YplQImE5N9JRqMCLrM2CdxOrZA=="], - "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.6", "", { "os": "linux", "cpu": "ppc64" }, "sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ=="], + "@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.2.8", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FhiOziBDWPBjbcmRzfLyIJnaP7AVMFXT7YCXPjXxj7wKU3vx24RjrCNN/zjvVa+N2vVoHJwCoUBvsrN/DG3zIA=="], - "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.6", "", { "os": "linux", "cpu": "s390x" }, "sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA=="], + "@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.2.8", "", { "os": "linux", "cpu": "s390x" }, "sha512-WnHfADMzOV2Y55wlx1hzzQnar/wDt/VdvWSD99r18Mz9ylNieIGOkRx3UV21h7m/eJvjySYJkO26VvGNFkwsIQ=="], - "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w=="], + "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.2.8", "", { "os": "linux", "cpu": "x64" }, "sha512-H9tRr5ibfXFVLxbPOseVewewFpl28zcEdjRDt2FTUZU7odxP0gEv1ki4/kGmcGOh78oRwZuuQllGLZ9zTJp84g=="], - "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ=="], + "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.2.8", "", { "os": "linux", "cpu": "x64" }, "sha512-UefiqfM3D6IVNlZ8tSGs9+Ejjud2T+oxO0IHADU45Y+lyEjD2dVFyZHbkfX0LUb5Zugo/oIv1eCO/KVYhgYJYA=="], - "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.6", "", { "os": "none", "cpu": "arm64" }, "sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg=="], + "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.2.8", "", { "os": "none", "cpu": "arm64" }, "sha512-637Ke4kWSy6rp9cxQ9gMOXlxPgIw/c1beASV4M//3+9I4uwBVOOl74G+e3zyU3u19U7RkRl/HuewixZ/Z6+Rjg=="], - "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A=="], + "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.2.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-xWBkPOF1Q9k/Gv1nQXnVdLxKu74jXppuOM4Z3mnypVUJJJwLsMl7hNJGRAUJoG8A5MgOI1ACKM+wBFxSJzKy4A=="], - "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.6", "", { "os": "win32", "cpu": "x64" }, "sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ=="], + "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.2.8", "", { "os": "win32", "cpu": "x64" }, "sha512-uz2ZvfgXbxqNwijjjbxrnvALwpyODDcgc1T1N8N3rf/DXKQmaFwmB4LX4yyjggpwN2obdQLb2rgirX5ffCWYng=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="], @@ -620,37 +614,37 @@ "@tailwindcss/vite": ["@tailwindcss/vite@4.3.3", "", { "dependencies": { "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw=="], - "@tanstack/history": ["@tanstack/history@1.162.1", "", {}, "sha512-DR9t6lfLVdrjgCwpglrR9DR7Ok8/HlXjcOE+goWXF3zyuLUO/ug7vMbSFxTqrQTtbRghJfyhmIZ0S6LhPIy44w=="], + "@tanstack/history": ["@tanstack/history@1.162.2", "", {}, "sha512-Lemp3DJbzNqcin/nZpWxycDaEqySDbnIshDbyHJMMCapD4ZQMe57szRpBXOfzfP6fyWAtHNrLrcBUyANJ6Vlow=="], - "@tanstack/react-router": ["@tanstack/react-router@1.170.32", "", { "dependencies": { "@tanstack/history": "1.162.1", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.171.27", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-SIpxvaTKco100a5ZR3ePmArbhtm3XOx+w1dpGYY9gxHDta4iXSKDdQuhLonwJbIMkVJsU1rwXf0UDHMrF/1snw=="], + "@tanstack/react-router": ["@tanstack/react-router@1.170.33", "", { "dependencies": { "@tanstack/history": "1.162.2", "@tanstack/react-store": "^0.9.3", "@tanstack/router-core": "1.171.28", "isbot": "^5.1.22" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-iNnI98vH3kO/V4dy6YM0CInhqwWBddU0G5wZK5jiMvr3HsK2avDQSRx3RY/y6v+6zQAqb2kD6hUPHIenrJBTSw=="], - "@tanstack/react-start": ["@tanstack/react-start@1.168.49", "", { "dependencies": { "@tanstack/react-router": "1.170.32", "@tanstack/react-start-client": "1.168.30", "@tanstack/react-start-rsc": "0.1.48", "@tanstack/react-start-server": "1.167.37", "@tanstack/router-utils": "1.162.2", "@tanstack/start-client-core": "1.170.27", "@tanstack/start-plugin-core": "1.171.39", "@tanstack/start-server-core": "1.169.31", "pathe": "^2.0.3" }, "peerDependencies": { "@rsbuild/core": "^2.0.0", "@vitejs/plugin-rsc": "*", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "vite": ">=7.0.0" }, "optionalPeers": ["@rsbuild/core", "@vitejs/plugin-rsc", "vite"] }, "sha512-iQb1ZoEHqvZMGLR4G7v3tTtgL06yv/bwxvGP3waVHxVn7bRpyopM44YbOluaGkcJzc13ZTvdLKWYNAN3bxMX/Q=="], + "@tanstack/react-start": ["@tanstack/react-start@1.168.50", "", { "dependencies": { "@tanstack/react-router": "1.170.33", "@tanstack/react-start-client": "1.168.31", "@tanstack/react-start-rsc": "0.1.49", "@tanstack/react-start-server": "1.167.38", "@tanstack/router-utils": "1.162.2", "@tanstack/start-client-core": "1.170.28", "@tanstack/start-plugin-core": "1.171.40", "@tanstack/start-server-core": "1.169.32", "pathe": "^2.0.3" }, "peerDependencies": { "@rsbuild/core": "^2.0.0", "@vitejs/plugin-rsc": "*", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "vite": ">=7.0.0" }, "optionalPeers": ["@rsbuild/core", "@vitejs/plugin-rsc", "vite"] }, "sha512-QtC9gSEQBE3Nnx24ZpbhSO+agNR9Wv/fkW03yORFLtDxMenqNYISrsBPEH2GpRElGofATaLWkCShlUwilaYksw=="], - "@tanstack/react-start-client": ["@tanstack/react-start-client@1.168.30", "", { "dependencies": { "@tanstack/react-router": "1.170.32", "@tanstack/router-core": "1.171.27", "@tanstack/start-client-core": "1.170.27" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-qsZuykUl1EF0/rc1bin1RtjFzz07YMOTBzhOstSDzbOVm/WKf1QKFTN+qAZi74xlbXSWNuT2MiFRyg4RKwj8iw=="], + "@tanstack/react-start-client": ["@tanstack/react-start-client@1.168.31", "", { "dependencies": { "@tanstack/react-router": "1.170.33", "@tanstack/router-core": "1.171.28", "@tanstack/start-client-core": "1.170.28" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-zpvlHd4Bq75tCc86Yz5GYWrO/6pWu4KL4q2q9RkBLAZE56epBuSvRBVZaMclaN9/pXIuW8R73fejKrSVNtJSbw=="], - "@tanstack/react-start-rsc": ["@tanstack/react-start-rsc@0.1.48", "", { "dependencies": { "@tanstack/react-router": "1.170.32", "@tanstack/router-core": "1.171.27", "@tanstack/router-utils": "1.162.2", "@tanstack/start-client-core": "1.170.27", "@tanstack/start-fn-stubs": "1.162.0", "@tanstack/start-plugin-core": "1.171.39", "@tanstack/start-storage-context": "1.167.29", "pathe": "^2.0.3" }, "peerDependencies": { "@rspack/core": ">=2.0.0-0", "@vitejs/plugin-rsc": ">=0.5.30", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "react-server-dom-rspack": ">=0.0.2" }, "optionalPeers": ["@rspack/core", "@vitejs/plugin-rsc", "react-server-dom-rspack"] }, "sha512-UglRdTMuF3c4dvzL/gh4dMVbMWHsPy8ZgTQdT2qpTlyt1b/3m+R40tBFdsfTgw76VTXivW+qV/ih0PN9000XTw=="], + "@tanstack/react-start-rsc": ["@tanstack/react-start-rsc@0.1.49", "", { "dependencies": { "@tanstack/react-router": "1.170.33", "@tanstack/router-core": "1.171.28", "@tanstack/router-utils": "1.162.2", "@tanstack/start-client-core": "1.170.28", "@tanstack/start-fn-stubs": "1.162.0", "@tanstack/start-plugin-core": "1.171.40", "@tanstack/start-storage-context": "1.167.30", "pathe": "^2.0.3" }, "peerDependencies": { "@rspack/core": ">=2.0.0-0", "@vitejs/plugin-rsc": ">=0.5.30", "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0", "react-server-dom-rspack": ">=0.0.2" }, "optionalPeers": ["@rspack/core", "@vitejs/plugin-rsc", "react-server-dom-rspack"] }, "sha512-2c/k2pT2gCvEpymug9sBqJ8klKGNnazxqbfdEMQi9rI5FXoB1W6TBt3XEQa3V5FvTxk7p7xKmgOqVhzR+bDKdw=="], - "@tanstack/react-start-server": ["@tanstack/react-start-server@1.167.37", "", { "dependencies": { "@tanstack/react-router": "1.170.32", "@tanstack/router-core": "1.171.27", "@tanstack/start-server-core": "1.169.31" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-cODHpFU8vIm7AdHii9W3NEuwyruNmT5wLDZjRsJLT9Jp+7FACnfJrRbvxnp1ldSv/9mxcHKi/OgwbdHQeMZcAQ=="], + "@tanstack/react-start-server": ["@tanstack/react-start-server@1.167.38", "", { "dependencies": { "@tanstack/react-router": "1.170.33", "@tanstack/router-core": "1.171.28", "@tanstack/start-server-core": "1.169.32" }, "peerDependencies": { "react": ">=18.0.0 || >=19.0.0", "react-dom": ">=18.0.0 || >=19.0.0" } }, "sha512-7Txo84AYNlSW0kLfd4HDccD5mgI/Atc0z29AvRMafS5KOjEb2j9tyOGDI0KRmTMAg/3zk9EIgBe8w7yDrysMEg=="], "@tanstack/react-store": ["@tanstack/react-store@0.9.3", "", { "dependencies": { "@tanstack/store": "0.9.3", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-y2iHd/N9OkoQbFJLUX1T9vbc2O9tjH0pQRgTcx1/Nz4IlwLvkgpuglXUx+mXt0g5ZDFrEeDnONPqkbfxXJKwRg=="], - "@tanstack/router-core": ["@tanstack/router-core@1.171.27", "", { "dependencies": { "@tanstack/history": "1.162.1", "cookie-es": "^3.0.0", "seroval": "^1.6.2", "seroval-plugins": "^1.6.2" } }, "sha512-wDwSLvoLwIaNcnx9UNcN9Mb7Y8QwCYq1U1RQZwyN186gnkIoIYI2SOxy8VqH1vFigbkHkk4FmwMAQlghPgDK2g=="], + "@tanstack/router-core": ["@tanstack/router-core@1.171.28", "", { "dependencies": { "@tanstack/history": "1.162.2", "cookie-es": "^3.0.0", "seroval": "^1.6.2", "seroval-plugins": "^1.6.2" } }, "sha512-PvPWSklhw6i9b0rzScVh0btQsK5u/gBYN3mBHyDzhC/U4LrB3WzPXPkUunQUKvQOGXCp16UEb7Htc/ITGm5DkQ=="], - "@tanstack/router-generator": ["@tanstack/router-generator@1.167.33", "", { "dependencies": { "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.27", "@tanstack/router-utils": "1.162.2", "@tanstack/virtual-file-routes": "1.162.0", "jiti": "^2.7.0", "magic-string": "^0.30.21", "prettier": "^3.5.0", "zod": "^4.4.3" } }, "sha512-Z3lCWIPuRUMPmuI8Mm48x/s49TxmHOaFVZ52j1W1QKYrsFHyT6U/h9bqfHJDxfQ8kz7y9q+W1YPKZ15Ee7yuCA=="], + "@tanstack/router-generator": ["@tanstack/router-generator@1.167.34", "", { "dependencies": { "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.28", "@tanstack/router-utils": "1.162.2", "@tanstack/virtual-file-routes": "1.162.0", "jiti": "^2.7.0", "magic-string": "^0.30.21", "prettier": "^3.5.0", "zod": "^4.4.3" } }, "sha512-sbCQrpYd2MGQXVXltUyPNLQFl924kLXBYp6Gq4keb1GPuDKfxH+yfcI3Qv9RQxI3ZErjgDZE7sOhRDXIf7xzxQ=="], - "@tanstack/router-plugin": ["@tanstack/router-plugin@1.168.35", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.27", "@tanstack/router-generator": "1.167.33", "@tanstack/router-utils": "1.162.2", "chokidar": "^5.0.0", "unplugin": "^3.0.0", "zod": "^4.4.3" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2 || ^2.0.0", "@tanstack/react-router": "^1.170.32", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0", "vite-plugin-solid": "^2.11.10 || ^3.0.0-0", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-foDAZKFqHXae+oFbIgcsSvy2QCVRn7XdS3nhwcRvD+ed6JrKPUP/1lQMsZLJqWycgR1vkZF7gs955KGa0NZQ0w=="], + "@tanstack/router-plugin": ["@tanstack/router-plugin@1.168.36", "", { "dependencies": { "@babel/core": "^7.28.5", "@babel/template": "^7.27.2", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.28", "@tanstack/router-generator": "1.167.34", "@tanstack/router-utils": "1.162.2", "chokidar": "^5.0.0", "unplugin": "^3.0.0", "zod": "^4.4.3" }, "peerDependencies": { "@rsbuild/core": ">=1.0.2 || ^2.0.0", "@tanstack/react-router": "^1.170.33", "vite": ">=5.0.0 || >=6.0.0 || >=7.0.0 || >=8.0.0", "vite-plugin-solid": "^2.11.10 || ^3.0.0-0", "webpack": ">=5.92.0" }, "optionalPeers": ["@rsbuild/core", "@tanstack/react-router", "vite", "vite-plugin-solid", "webpack"] }, "sha512-A/Ama19yfgbgC3QZgQGwC+wceK8cYiqqFgRjTuNxhPCrrrdUrP1SKnCKvLvHISkx6xgLnDDMl2wJyP9PMgrjYg=="], "@tanstack/router-utils": ["@tanstack/router-utils@1.162.2", "", { "dependencies": { "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ansis": "^4.1.0", "babel-dead-code-elimination": "^1.0.12", "diff": "^8.0.2", "pathe": "^2.0.3", "tinyglobby": "^0.2.15" } }, "sha512-hTWqJtqIFFdvuCl8WXNyrodp2L9zo2G37xKRrcVmVRWpAB2h+U1LuRAfS4tsFTiWOIoE/B+WDVFB8JpoEdw6jQ=="], - "@tanstack/start-client-core": ["@tanstack/start-client-core@1.170.27", "", { "dependencies": { "@tanstack/router-core": "1.171.27", "@tanstack/start-fn-stubs": "1.162.0", "@tanstack/start-storage-context": "1.167.29", "seroval": "^1.6.2" } }, "sha512-Ro6ZSM0NgYKDMxM0e8qyU4mBfnld2Zb74BA/9f4i35C0Y3IAA8Zxs/DIPfOo9VRYWp5c5n5Q8dRBn2qn0vtPhQ=="], + "@tanstack/start-client-core": ["@tanstack/start-client-core@1.170.28", "", { "dependencies": { "@tanstack/router-core": "1.171.28", "@tanstack/start-fn-stubs": "1.162.0", "@tanstack/start-storage-context": "1.167.30", "seroval": "^1.6.2" } }, "sha512-WbMnrvYzsE4TtqotX0HeRnBI8wyMbFbg/uLzcY23Omk5BiduwlyIWEnBZsac3AjdK8C0BP1XTafp2p4kwhhNeA=="], "@tanstack/start-fn-stubs": ["@tanstack/start-fn-stubs@1.162.0", "", {}, "sha512-QWfUZ3Yo923tdQn38LyKMU8rcTw69zc+T4dAvgTWV4O56SqFRsGfS0lSWIMhJRwXIx/bvdi7nTUBDdZtTHtpTQ=="], - "@tanstack/start-plugin-core": ["@tanstack/start-plugin-core@1.171.39", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.28.5", "@babel/types": "^7.28.5", "@tanstack/router-core": "1.171.27", "@tanstack/router-generator": "1.167.33", "@tanstack/router-plugin": "1.168.35", "@tanstack/router-utils": "1.162.2", "@tanstack/start-server-core": "1.169.31", "exsolve": "^1.0.7", "lightningcss": "^1.32.0", "pathe": "^2.0.3", "picomatch": "^4.0.3", "seroval": "^1.6.2", "source-map": "^0.7.6", "srvx": "^0.11.9", "tinyglobby": "^0.2.15", "ufo": "^1.5.4", "vitefu": "^1.1.1", "xmlbuilder2": "^4.0.3", "zod": "^4.4.3" }, "peerDependencies": { "@rsbuild/core": "^2.0.0", "vite": ">=7.0.0" }, "optionalPeers": ["@rsbuild/core", "vite"] }, "sha512-Zyj6G4MDFLXcHYhPavhewCBo8dsxi3qvPk31zl/QtTzYzOY9rJHDDBGarKsJq20ziChmkGn/sttYqb4vGCxJDA=="], + "@tanstack/start-plugin-core": ["@tanstack/start-plugin-core@1.171.40", "", { "dependencies": { "@babel/code-frame": "7.27.1", "@babel/core": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/remapping": "^2.3.5", "@tanstack/router-core": "1.171.28", "@tanstack/router-generator": "1.167.34", "@tanstack/router-plugin": "1.168.36", "@tanstack/router-utils": "1.162.2", "@tanstack/start-server-core": "1.169.32", "exsolve": "^1.0.7", "lightningcss": "^1.32.0", "pathe": "^2.0.3", "picomatch": "^4.0.3", "seroval": "^1.6.2", "source-map": "^0.7.6", "srvx": "^0.11.9", "tinyglobby": "^0.2.15", "ufo": "^1.5.4", "vitefu": "^1.1.1", "xmlbuilder2": "^4.0.3", "zod": "^4.4.3" }, "peerDependencies": { "@rsbuild/core": "^2.0.0", "vite": ">=7.0.0" }, "optionalPeers": ["@rsbuild/core", "vite"] }, "sha512-AWKf7Lr14qd+G24K5AJTHsxKrgzmYnk0a521O80ZMAkVWXeNZZ8A2L4qyNTL+eVk9i5vssMhPoSgulJqPEvUHg=="], - "@tanstack/start-server-core": ["@tanstack/start-server-core@1.169.31", "", { "dependencies": { "@tanstack/history": "1.162.1", "@tanstack/router-core": "1.171.27", "@tanstack/start-client-core": "1.170.27", "@tanstack/start-storage-context": "1.167.29", "fetchdts": "^0.1.6", "h3-v2": "npm:h3@2.0.1-rc.20", "seroval": "^1.6.2" } }, "sha512-56w8l+Fao01YCrmv0hzNxL3b3FRmqLRGdE11izZNQsW+1CcSYuehAM5khWKABuI1DI/UIBLGV7BwL4Dlg0eHCw=="], + "@tanstack/start-server-core": ["@tanstack/start-server-core@1.169.32", "", { "dependencies": { "@tanstack/history": "1.162.2", "@tanstack/router-core": "1.171.28", "@tanstack/start-client-core": "1.170.28", "@tanstack/start-storage-context": "1.167.30", "fetchdts": "^0.1.6", "h3-v2": "npm:h3@2.0.1-rc.20", "seroval": "^1.6.2" } }, "sha512-/r10WTBAncF1llVqxc7r+a5Y3Lkd685gMbFCl4udV+9022bW40or3K4mmNI5q+LceZgI/HFdHF4+j54wikFrNw=="], - "@tanstack/start-storage-context": ["@tanstack/start-storage-context@1.167.29", "", { "dependencies": { "@tanstack/router-core": "1.171.27" } }, "sha512-8qfprC5774XMRDQlMogkfiGpFLiBf0xDG4bMFUbfkSzpCAQwpLbgGY4Zwft22O9rKYq2vUXusKAdVjvJTUEquQ=="], + "@tanstack/start-storage-context": ["@tanstack/start-storage-context@1.167.30", "", { "dependencies": { "@tanstack/router-core": "1.171.28" } }, "sha512-NR4AmF7PjnOyQ47pMc9pC1IpOKGmLMjTOKkGzRzipluzLE8gfEsrapbef/rSRGAYyGU0oHDGm3Fz4itFnK1eUQ=="], "@tanstack/store": ["@tanstack/store@0.9.3", "", {}, "sha512-8reSzl/qGWGGVKhBoxXPMWzATSbZLZFWhwBAFO9NAyp0TxzfBP0mIrGb8CP8KrQTmvzXlR/vFPPUrHTLBGyFyw=="], @@ -660,7 +654,7 @@ "@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="], - "@testing-library/user-event": ["@testing-library/user-event@14.6.6", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-Jbs9FpkkIDw8FgSc6kOVsOv8JuuqGAL7J4X1oot77JxAoDlkNn2GRkd0aYRVuQ+pVQAiHWVkE4rX/dkF5fBiCw=="], + "@testing-library/user-event": ["@testing-library/user-event@14.6.7", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-MPCpX8bxe8zS+JmmTwLp8jd0dy1rAm60Te/SL8JrQM3qvQJcBOs1d7IefJMyZzqM3EWBrDn/LWDt1BCGu4ASfg=="], "@tybys/wasm-util": ["@tybys/wasm-util@0.10.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg=="], @@ -674,7 +668,7 @@ "@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="], - "@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="], + "@types/bun": ["@types/bun@1.4.2", "", { "dependencies": { "bun-types": "1.4.2" } }, "sha512-GimotNn7+ZV0uVArItBbriZsR1oNf0+WTzPkdcFrzShI7k2norL0uzEaJT8T33dWr7O/c9ZDuAFQrctKCi72oQ=="], "@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], @@ -684,11 +678,11 @@ "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], - "@types/node": ["@types/node@26.4.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-faiGnoIrLH/V8cibOMEAZ8pMw6oXqSukl29ra4mN8GdaB2ZewzeaLj+INpV5N+Z1eKWzY+IzaIZH2EIR6YZRNQ=="], + "@types/node": ["@types/node@26.5.0", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-dVSGpriSoCgz8WnDNTuSSuSv1PC/ALXihO4ulRZt7Md8k9mlbdin3lGOcDE8SnWOgf513ByWlXd7BK4azmyg/A=="], "@types/react": ["@types/react@19.2.18", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w=="], - "@types/react-dom": ["@types/react-dom@19.2.5", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg=="], + "@types/react-dom": ["@types/react-dom@19.2.7", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-I8bPpDLcHBv1qiIiXDCy71Rt8eQDKJP0sMSWJphDdAcdqiJ1sGpZamavoEIRZmYzjia9LuEb2HlYdDpmoENpvQ=="], "@types/resolve": ["@types/resolve@1.20.6", "", {}, "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ=="], @@ -748,7 +742,7 @@ "acorn": ["acorn@8.18.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ=="], - "ai": ["ai@5.0.251", "", { "dependencies": { "@ai-sdk/gateway": "2.0.145", "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.36", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-d4mwmJ10uvyFqq5nOYrIgRtuadDc6ipjiN9X0HKRpM/z96zt9crQemuOXUaaa9XWufyXbYTP1kEAfjnTn+Ru+w=="], + "ai": ["ai@5.0.254", "", { "dependencies": { "@ai-sdk/gateway": "2.0.148", "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.36", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-RLetELlXNhg1s+u/7UwO5PAF8NiG/zMdSz7ZX/5hTfb4xYVWzOenLu3uRVrc8iKgC2Fkskn+LGdqUc7el0GvMw=="], "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], @@ -760,7 +754,7 @@ "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - "argue-cli": ["argue-cli@3.1.0", "", {}, "sha512-DhBpBfXL4SS2uC0N922MMajKR3CdrTG0u2or1PNYgXMsrSzViJrbtvT0nCLlLGUI0plam/ZZCs7aAauHtW9thw=="], + "argue-cli": ["argue-cli@3.2.0", "", {}, "sha512-VipTB0gXgGIFO2Rg9yEVN5wLt2AurJcZqDbgmYSwPwsykhLrQhs240/bfceev4w68lI8JshoOJIk9uW7tFzROw=="], "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], @@ -774,17 +768,17 @@ "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - "baseline-browser-mapping": ["baseline-browser-mapping@2.11.20", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw=="], + "baseline-browser-mapping": ["baseline-browser-mapping@2.11.21", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ=="], "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], "brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="], - "browserslist": ["browserslist@4.28.8", "", { "dependencies": { "baseline-browser-mapping": "^2.11.12", "caniuse-lite": "^1.0.30001809", "electron-to-chromium": "^1.5.402", "node-releases": "^2.0.53", "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA=="], + "browserslist": ["browserslist@4.28.9", "", { "dependencies": { "baseline-browser-mapping": "^2.11.20", "caniuse-lite": "^1.0.30001810", "electron-to-chromium": "^1.5.420", "node-releases": "^2.0.54", "update-browserslist-db": "^1.3.2" }, "bin": { "browserslist": "cli.js" } }, "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg=="], "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], - "bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="], + "bun-types": ["bun-types@1.4.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-bxV1FgK7yBIzjRe5zBozIM4Bem11ZJcCXSrjWRG3YWLt8yFDePu4cLjpebO8OvPeIE9trbyPF4fuj3Cia4Fj3w=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], @@ -854,7 +848,7 @@ "drizzle-orm": ["drizzle-orm@0.45.2", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "prisma": "*", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "prisma", "sql.js", "sqlite3"] }, "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q=="], - "electron-to-chromium": ["electron-to-chromium@1.5.417", "", {}, "sha512-4T+DTDWuMPM4aHlHwWdAVCVWwp7LDilnhzkj+c/Lbj91XSQrLuOmZSLtS9Q4iIqjlPUbPOnC624zDVVHCHaolQ=="], + "electron-to-chromium": ["electron-to-chromium@1.5.425", "", {}, "sha512-QvPtl41EUOnuT1HBvMKgxXRIaHNcagBPs50u7VULzhZXaGfqTbZyE16LQsctZ/RQHlGu+FOWeDTR4mY6YbeF1g=="], "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], @@ -888,13 +882,13 @@ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - "fast-uri": ["fast-uri@3.1.6", "", {}, "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q=="], + "fast-uri": ["fast-uri@3.1.7", "", {}, "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fetchdts": ["fetchdts@0.1.7", "", {}, "sha512-YoZjBdafyLIop9lSxXVI33oLD5kN31q4Td+CasofLLYeLXRFeOsuOw0Uo+XNRi9PZlbfdlN2GmRtm4tCEQ9/KA=="], - "fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -918,7 +912,7 @@ "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], - "hono": ["hono@4.13.5", "", {}, "sha512-O6+/eCYRkzzzy0rPWwKLiGBR1nFuUPZynnwjxN1MBA62NNqbT0wQEzQyK2gSO5yDIDB336sXQleAhOHrzlYyKw=="], + "hono": ["hono@4.13.7", "", {}, "sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ=="], "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], @@ -990,7 +984,7 @@ "lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="], - "lucide-react": ["lucide-react@1.38.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-xZCyBd/wiVUDactoCc+42TjL0aB7EBOXsuX+tjz+W/sGzw2KhHpL1NOH3FIaVUcpimvUBpIYfz34Ofj9S5JEzQ=="], + "lucide-react": ["lucide-react@1.43.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-ubtnda1fVK5ky0PNEpOmB0wiwhpZUyJiol4K14KCm+QKvuCkfUu54Et/rIjyupJpwiJ5Hg+BLQE86/GEsS+IgQ=="], "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], @@ -998,7 +992,7 @@ "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], - "miniflare": ["miniflare@5.20260828.0-alpha", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.35.2", "undici": "7.29.0", "workerd": "1.20260828.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" } }, "sha512-6nbxhZEcz/UET3Y1OnYPsrAUjUmuFoib3ynUqteRdn1YnDxsLg8cwgZJZCk9QmtOmGzXwzXzgE/d/C0dJAPtVw=="], + "miniflare": ["miniflare@5.20260908.0-alpha", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.35.2", "undici": "7.29.0", "workerd": "1.20260908.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" } }, "sha512-BHIknb0u6vLIvb+R8PsUAzgoWWk3OlW85DrV2SG0EydfSpIba28YT0rH6xZThjnSwDxzNuW3FDRNSWvbiI/DVw=="], "minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], @@ -1010,7 +1004,7 @@ "nanoid": ["nanoid@3.3.18", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="], - "node-releases": ["node-releases@2.0.54", "", {}, "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ=="], + "node-releases": ["node-releases@2.0.55", "", {}, "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ=="], "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], @@ -1036,11 +1030,11 @@ "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], - "playwright": ["playwright@1.62.1", "", { "dependencies": { "playwright-core": "1.62.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg=="], + "playwright": ["playwright@1.63.0", "", { "dependencies": { "playwright-core": "1.63.0" }, "bin": { "playwright": "cli.js" } }, "sha512-+7ziBLidS4NaNCdt57SUDT+wYmmd5fmiQejUic/kb+YsYSCPyOOE9sebzMjNmQrsnNpDJqd4WHvV/8lfKfUDUg=="], - "playwright-core": ["playwright-core@1.62.1", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw=="], + "playwright-core": ["playwright-core@1.63.0", "", { "bin": { "playwright-core": "cli.js" } }, "sha512-rYCsBF/M5HjUch52bbtVONEFjv6Xu8sm8h72dNlR5bzIE1fvC/bxgspzkjSfU+MweEMmPM8KJebG6nnyxo5mCg=="], - "postcss": ["postcss@8.5.26", "", { "dependencies": { "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ=="], + "postcss": ["postcss@8.5.28", "", { "dependencies": { "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A=="], "prettier": ["prettier@3.9.6", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g=="], @@ -1078,7 +1072,7 @@ "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], - "rolldown": ["rolldown@1.2.6", "", { "dependencies": { "@oxc-project/types": "=0.147.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.6", "@rolldown/binding-android-arm64": "1.2.6", "@rolldown/binding-darwin-arm64": "1.2.6", "@rolldown/binding-darwin-x64": "1.2.6", "@rolldown/binding-freebsd-x64": "1.2.6", "@rolldown/binding-linux-arm-gnueabihf": "1.2.6", "@rolldown/binding-linux-arm64-gnu": "1.2.6", "@rolldown/binding-linux-arm64-musl": "1.2.6", "@rolldown/binding-linux-ppc64-gnu": "1.2.6", "@rolldown/binding-linux-s390x-gnu": "1.2.6", "@rolldown/binding-linux-x64-gnu": "1.2.6", "@rolldown/binding-linux-x64-musl": "1.2.6", "@rolldown/binding-openharmony-arm64": "1.2.6", "@rolldown/binding-win32-arm64-msvc": "1.2.6", "@rolldown/binding-win32-x64-msvc": "1.2.6" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA=="], + "rolldown": ["rolldown@1.2.8", "", { "dependencies": { "@oxc-project/types": "=0.149.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.8", "@rolldown/binding-android-arm64": "1.2.8", "@rolldown/binding-darwin-arm64": "1.2.8", "@rolldown/binding-darwin-x64": "1.2.8", "@rolldown/binding-freebsd-x64": "1.2.8", "@rolldown/binding-linux-arm-gnueabihf": "1.2.8", "@rolldown/binding-linux-arm64-gnu": "1.2.8", "@rolldown/binding-linux-arm64-musl": "1.2.8", "@rolldown/binding-linux-ppc64-gnu": "1.2.8", "@rolldown/binding-linux-s390x-gnu": "1.2.8", "@rolldown/binding-linux-x64-gnu": "1.2.8", "@rolldown/binding-linux-x64-musl": "1.2.8", "@rolldown/binding-openharmony-arm64": "1.2.8", "@rolldown/binding-win32-arm64-msvc": "1.2.8", "@rolldown/binding-win32-x64-msvc": "1.2.8" }, "bin": { "rolldown": "./bin/cli.mjs" } }, "sha512-Z67nTmhZe7anqnM/EjI392w5i/ANUinjip7QYsOyN37oayduxt3ksdX0hf5OOamkAd53BiIHfbfSzfUmzKFQqQ=="], "rou3": ["rou3@0.8.1", "", {}, "sha512-ePa+XGk00/3HuCqrEnK3LxJW7I0SdNg6EFzKUJG73hMAdDcOUC/i/aSz7LSDwLrGr33kal/rqOGydzwl6U7zBA=="], @@ -1088,9 +1082,9 @@ "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - "seroval": ["seroval@1.6.4", "", {}, "sha512-LErWMNS2RRFdu2RMA5u/PA59/IWs0XsikyEXGQ2/36iEWFrdG0ABmg17E17cikrv76891kOAMq3TkTFXpwAHXw=="], + "seroval": ["seroval@1.6.7", "", {}, "sha512-AeDcLh0yO2SFm9W71essgnSzLV9DI8ZH0x0knXn2DMnUZj728mpLbxjlbB6IqKCmqh8JA3cEqRyGoNkt584JcQ=="], - "seroval-plugins": ["seroval-plugins@1.6.4", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-R0f1U9hmn38+dFMz6b6ab8lwucmw4AtiY7St+JPWudy1dm+Bs3g884nyrsH9Cy6rKpZKLYayXuMda9GZ/fl8JQ=="], + "seroval-plugins": ["seroval-plugins@1.6.7", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-4Nk35ttD3DTDJW4hgw5StsVAPeU6qnDFnULAouw6tQ7oLTV/ICXrWpsXo2EE52eSP2joUMazbVf52mFEcADqRw=="], "sharp": ["sharp@0.35.2", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.4" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.2", "@img/sharp-darwin-x64": "0.35.2", "@img/sharp-freebsd-wasm32": "0.35.2", "@img/sharp-libvips-darwin-arm64": "1.3.1", "@img/sharp-libvips-darwin-x64": "1.3.1", "@img/sharp-libvips-linux-arm": "1.3.1", "@img/sharp-libvips-linux-arm64": "1.3.1", "@img/sharp-libvips-linux-ppc64": "1.3.1", "@img/sharp-libvips-linux-riscv64": "1.3.1", "@img/sharp-libvips-linux-s390x": "1.3.1", "@img/sharp-libvips-linux-x64": "1.3.1", "@img/sharp-libvips-linuxmusl-arm64": "1.3.1", "@img/sharp-libvips-linuxmusl-x64": "1.3.1", "@img/sharp-linux-arm": "0.35.2", "@img/sharp-linux-arm64": "0.35.2", "@img/sharp-linux-ppc64": "0.35.2", "@img/sharp-linux-riscv64": "0.35.2", "@img/sharp-linux-s390x": "0.35.2", "@img/sharp-linux-x64": "0.35.2", "@img/sharp-linuxmusl-arm64": "0.35.2", "@img/sharp-linuxmusl-x64": "0.35.2", "@img/sharp-webcontainers-wasm32": "0.35.2", "@img/sharp-win32-arm64": "0.35.2", "@img/sharp-win32-ia32": "0.35.2", "@img/sharp-win32-x64": "0.35.2" } }, "sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w=="], @@ -1124,13 +1118,13 @@ "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], - "tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="], + "tinyexec": ["tinyexec@1.3.1", "", {}, "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA=="], "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], - "tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="], + "tinyspy": ["tinyspy@4.0.6", "", {}, "sha512-u8KszXvGfU68hVcZpRHKG28T0krMuv2G5nDhiHaMLen/gIuFEgIJhaJuO69qjnXg5paSrbPMFfx3brNuN8eVSg=="], "ts-dedent": ["ts-dedent@2.3.0", "", {}, "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg=="], @@ -1146,7 +1140,7 @@ "undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], - "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + "undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="], "unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="], @@ -1166,9 +1160,9 @@ "webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="], - "workerd": ["workerd@1.20260828.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260828.1", "@cloudflare/workerd-darwin-arm64": "1.20260828.1", "@cloudflare/workerd-linux-64": "1.20260828.1", "@cloudflare/workerd-linux-arm64": "1.20260828.1", "@cloudflare/workerd-windows-64": "1.20260828.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-pB9yvt0kkwZDAGZHmpY59r0o3hM0DzdW6BJERqwZOhunZ3ssOyDSgQxOQer2cSZW4YCFeOTIQYN1qwhK5wv/Cw=="], + "workerd": ["workerd@1.20260908.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260908.1", "@cloudflare/workerd-darwin-arm64": "1.20260908.1", "@cloudflare/workerd-linux-64": "1.20260908.1", "@cloudflare/workerd-linux-arm64": "1.20260908.1", "@cloudflare/workerd-windows-64": "1.20260908.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-rYhpW6NWHD++p34ej+VXWzi5Pdnmd7ncdbB/ux4s+i3mf7IeOpW3O4roqClQ+Z8ernvyMCknBLCb76z1rA+a7Q=="], - "wrangler": ["wrangler@4.127.1", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "5.20260828.0-alpha", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260828.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^5.20260828.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js", "cf-wrangler": "bin/cf-wrangler.js" } }, "sha512-OzsiNgaI8i681L/+KnAKc+uEZ5D57xK5JuNvCOpRKICF4/5Q3Cu1oTGuUiT/f3GDUqQb3gzXNT0tfOHGMEtknw=="], + "wrangler": ["wrangler@4.130.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.1", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "5.20260908.0-alpha", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260908.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^5.20260908.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js", "cf-wrangler": "bin/cf-wrangler.js" } }, "sha512-fzNjnTzyZl31PGJOFXbiLeZqEIToQ9KOzkkvGdQz4wlB+BoHnl3BRwCR5xg8AqYyzVunvvMHlMzvlbThQ7rrAA=="], "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], @@ -1248,7 +1242,7 @@ "redent/strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], - "rolldown/@oxc-project/types": ["@oxc-project/types@0.147.0", "", {}, "sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg=="], + "rolldown/@oxc-project/types": ["@oxc-project/types@0.149.0", "", {}, "sha512-Efcc+iF0j3Bf67YjEqIqWXbX5XddXoK/Mw4K1/JuXwRCZ8N16VR7iT23nlCc9XrveFVh/E5Rqs2StT0V8v9LdA=="], "storybook/esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="], @@ -1258,14 +1252,8 @@ "tsx/esbuild": ["esbuild@0.28.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.2", "@esbuild/android-arm": "0.28.2", "@esbuild/android-arm64": "0.28.2", "@esbuild/android-x64": "0.28.2", "@esbuild/darwin-arm64": "0.28.2", "@esbuild/darwin-x64": "0.28.2", "@esbuild/freebsd-arm64": "0.28.2", "@esbuild/freebsd-x64": "0.28.2", "@esbuild/linux-arm": "0.28.2", "@esbuild/linux-arm64": "0.28.2", "@esbuild/linux-ia32": "0.28.2", "@esbuild/linux-loong64": "0.28.2", "@esbuild/linux-mips64el": "0.28.2", "@esbuild/linux-ppc64": "0.28.2", "@esbuild/linux-riscv64": "0.28.2", "@esbuild/linux-s390x": "0.28.2", "@esbuild/linux-x64": "0.28.2", "@esbuild/netbsd-arm64": "0.28.2", "@esbuild/netbsd-x64": "0.28.2", "@esbuild/openbsd-arm64": "0.28.2", "@esbuild/openbsd-x64": "0.28.2", "@esbuild/openharmony-arm64": "0.28.2", "@esbuild/sunos-x64": "0.28.2", "@esbuild/win32-arm64": "0.28.2", "@esbuild/win32-ia32": "0.28.2", "@esbuild/win32-x64": "0.28.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA=="], - "tsx/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - - "vite/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - "wrangler/esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], - "wrangler/fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], diff --git a/db/compile-scenario.ts b/db/compile-scenario.ts index 8b94ea3..1d88f0a 100644 --- a/db/compile-scenario.ts +++ b/db/compile-scenario.ts @@ -152,6 +152,12 @@ const compileDefinition = ( id: characterUuid(character.id), scenarioId, name: character.name, + // 書かれていなければフルネームをそのまま。帯が窮屈になるだけで、表示は壊れない。 + shortName: character.shortName === undefined ? character.name : character.shortName, + ageGroup: character.ageGroup, + gender: character.gender, + voiceSeed: character.voiceSeed === undefined ? null : character.voiceSeed, + voiceCaption: character.voiceCaption === undefined ? null : character.voiceCaption, publicIntroduction: character.publicIntroduction, /* 関係は「事実」ではなく「相手への態度」なので、知っていることに混ぜると diff --git a/db/detective.ts b/db/detective.ts index 6a14c7c..f2aa191 100644 --- a/db/detective.ts +++ b/db/detective.ts @@ -1,80 +1,32 @@ import { z } from 'zod' +import { AGE_GROUP_LABELS, ageGroupSchema, GENDER_LABELS, genderSchema } from './person' /** * プレイヤーが演じる探偵。 * * この形の正典はここ1箇所。サーバ・クライアント・DBの型付けが同じ定義を読む。 - * - * 名前と詳細(自由記述)以外を列挙にしているのは、この値がそのまま NPC の - * プロンプトに入るため。年齢を自由記述にすると「28」「三十路」「アラサー」と - * 書き方が割れ、NPC の呼びかけ方を組み立てる側が文字列を解釈する羽目になる。 - * 選択肢に閉じておけば、年ごろと性別から呼称と態度を確実に引ける。 + * 年ごろと性別の列挙は登場人物と共通で、db/person.ts が正典。 * * このファイルが drizzle-orm を import していないのは意図的。 * クライアントからも読むので、ORM をブラウザのバンドルへ持ち込みたくない * (db/floor-plan.ts と同じ理由)。 */ -export const ageGroupSchema = z.enum([ - 'child', - 'teen', - 'young', - 'adult', - 'senior', - 'elder', - 'unknown', -]) - -export type AgeGroup = z.infer - -/** 画面の選択肢に並べる順。若い順に並べ、「不詳」を末尾に置く。 */ -export const AGE_GROUPS = ageGroupSchema.options - -export const AGE_GROUP_LABELS: Record = { - child: '子供', - teen: '十代', - young: '若者', - adult: '壮年', - senior: '初老', - elder: '老齢', - unknown: '年齢不詳', -} - -/** ラベルだけでは幅が広すぎるので、目安の年齢を添える。プロンプトにも同じ文を渡す。 */ -export const AGE_GROUP_NOTES: Record = { - child: '12歳ごろまで', - teen: '13〜19歳ごろ', - young: '20代', - adult: '30〜40代', - senior: '50〜60代', - elder: '70代以上', - unknown: '見た目からは測れない', -} - -export const genderSchema = z.enum(['male', 'female', 'other', 'unknown']) - -export type Gender = z.infer - -export const GENDERS = genderSchema.options - -export const GENDER_LABELS: Record = { - male: '男性', - female: '女性', - other: 'どちらでもない', - unknown: '明かさない', -} - /** * 探偵そのもの。名乗らずに始めることもできるので、持たないセッションもある。 * - * appearance だけが自由記述。NPC のプロンプトにそのまま入る=そのままトークン数に + * appearance と speech が自由記述。NPC のプロンプトにそのまま入る=そのままトークン数に * なるので、上限を切る。空でも構わない(書かなければプロンプトにも出さない)。 + * + * speech が appearance より短いのは、口調は例を一つ二つ挙げれば足りるため。 + * 長く書けるようにすると人物設定をここへ書き始めてしまい、appearance と役が重なる。 */ export const detectiveSchema = z.object({ name: z.string().nonempty().max(40), ageGroup: ageGroupSchema, gender: genderSchema, appearance: z.string().max(200), + speech: z.string().max(100), }) export type Detective = z.infer diff --git a/db/generate-scenario.ts b/db/generate-scenario.ts index 92c36b0..a9925ee 100644 --- a/db/generate-scenario.ts +++ b/db/generate-scenario.ts @@ -1,7 +1,7 @@ import { generateObject, jsonSchema } from 'ai' import { z } from 'zod' import { parseEnv } from '@/server/env' -import { cacheHint, chooseLlm, resolveModel } from '@/server/llm/provider' +import { chooseLlm, resolveModel } from '@/server/llm/provider' import { type AuthorGenerate, describeIssues, runAuthor } from './author' import { scenarioDefinitionShapeSchema } from './scenario-definition' import { toScenarioYaml } from './scenario-file' @@ -43,7 +43,7 @@ if (premise === undefined || premise.trim() === '') { } const env = parseEnv(process.env) -const choice = chooseLlm(env, 'author') +const choice = await chooseLlm(env, 'author') const model = resolveModel(env, choice) /* @@ -75,7 +75,6 @@ const generate: AuthorGenerate = async (request) => { schema: outputSchema, system: SYSTEM_PROMPT, prompt, - providerOptions: cacheHint(choice), }) console.log(` トークン: 入力 ${result.usage.inputTokens} / 出力 ${result.usage.outputTokens}`) @@ -84,7 +83,7 @@ const generate: AuthorGenerate = async (request) => { } console.log(`題材: ${premise}`) -console.log(`モデル: ${choice.provider} / ${choice.modelId}`) +console.log(`モデル: ${choice}`) const result = await runAuthor({ premise, generate, maxAttempts: MAX_ATTEMPTS }) diff --git a/db/judge-tuning.ts b/db/judge-tuning.ts new file mode 100644 index 0000000..f366009 --- /dev/null +++ b/db/judge-tuning.ts @@ -0,0 +1,71 @@ +import { z } from 'zod' + +/** + * 判定役の振る舞いを、一つずつ入れたり切ったりするための語彙。 + * + * 正典はここ1箇所。サーバ・クライアントのどこからでも同じものを読む + * (`db/game-mode.ts` や `db/detective.ts` と同じ立ち位置)。 + * + * 四つとも「判定の精度を上げるはずの直し」で、既定は全部オフ——つまり今までの挙動。 + * 入れた回と切った回を混ぜて遊び、`analytics_turns` に残る記録を後から見比べて + * 採否を決めるためのもの。良くなると分かっているなら切り替えは要らないので、 + * 決着がついたら勝った側を既定に畳んで、この語彙ごと消す。 + */ + +export const judgeTuningSchema = z.object({ + /** + * 判定が返した証拠IDを、そのシナリオに実在する未発見の証拠と突き合わせて弾く。 + * + * Revelation 側は最初からこの形(`src/server/game/revelations.ts`)。証拠だけが + * 素通しで、実在しないIDでも既に出ているIDでもそのまま記録に入る。 + */ + checkEvidenceIds: z.boolean(), + /** + * 矛盾の指摘を判定するときに、それまでのやり取りも渡す。 + * + * `contradictionPointedOut` は「過去の発言との矛盾を指摘できたか」を訊いているのに、 + * 判定へ渡しているのはその回のやり取りだけ。突き合わせる相手が無い。 + */ + contradictionNeedsHistory: z.boolean(), + /** + * 判定の温度を 0 に固定する。 + * + * 指定しないと提供元の既定に従うので、同じやり取りでも回ごとに判定が揺れる。 + */ + fixedTemperature: z.boolean(), + /** 判定の呼び出しが落ちたとき、一度だけやり直す。 */ + retryOnce: z.boolean(), +}) + +export type JudgeTuning = z.infer + +/** 画面に並べる順。増減したときに拾い漏らさないよう、鍵の一覧もここで持つ。 */ +export const JUDGE_TUNING_KEYS = [ + 'checkEvidenceIds', + 'contradictionNeedsHistory', + 'fixedTemperature', + 'retryOnce', +] as const satisfies readonly (keyof JudgeTuning)[] + +/** 何も入れていない状態。今までの挙動と同じ。 */ +export const DEFAULT_JUDGE_TUNING: JudgeTuning = { + checkEvidenceIds: false, + contradictionNeedsHistory: false, + fixedTemperature: false, + retryOnce: false, +} + +/** 画面に出す名前。内部の名前をそのまま見せない。 */ +export const JUDGE_TUNING_LABELS: Record = { + checkEvidenceIds: '証拠のIDを確かめる', + contradictionNeedsHistory: '矛盾の判定に前のやり取りを渡す', + fixedTemperature: '判定の揺れを抑える', + retryOnce: '判定が落ちたらやり直す', +} + +export const JUDGE_TUNING_NOTES: Record = { + checkEvidenceIds: '実在しない証拠や、すでに見つけた証拠を弾く', + contradictionNeedsHistory: '前の発言と食い違っているかを、実際に読み比べて決める', + fixedTemperature: '同じやり取りなら毎回同じ判定になるようにする', + retryOnce: '一度だけやり直す。落ちた回は発見が丸ごと消えるため', +} diff --git a/db/llm-catalog.ts b/db/llm-catalog.ts index fec88e7..a51a5e0 100644 --- a/db/llm-catalog.ts +++ b/db/llm-catalog.ts @@ -1,31 +1,16 @@ import { z } from 'zod' /** - * 選べる LLM の一覧と、役割ごとの既定。 + * 役割ごとの LLM 設定の語彙。 * * 正典はここ1箇所。サーバ・クライアント・シードのどこからでも同じものを読む * (`db/game-mode.ts` や `db/detective.ts` と同じ立ち位置)。 * - * ここが「許可リスト」を兼ねる。設定画面は認証を持たないので、モデルIDを自由入力に - * すると通りすがりの誰でも最上位の推論モデルを指名できてしまう。選択肢をこの表に - * 閉じ込めておけば、増やすときに必ず人が1行足すことになる。 - * - * NOTE: モデルIDと料金は各社とも改定が速い。ここは「既定値」であって正典ではないので、 - * 採用前に必ず各社の公式ドキュメントで確認すること。 + * 選べるモデルの一覧はここには無い。宛先が `OPENAI_URL` の互換サーバ1つになったので、 + * 実在するモデルはそのサーバに聞く(`src/server/llm/models.ts`)。手で書いた表を持つと、 + * サーバの構成を変えるたびに実態とずれ、しかもずれたことに気づけない。 */ -export const llmProviderSchema = z.enum(['anthropic', 'openai', 'google']) - -export type LlmProvider = z.infer - -export const LLM_PROVIDERS = llmProviderSchema.options - -export const LLM_PROVIDER_LABELS: Record = { - anthropic: 'Anthropic', - openai: 'OpenAI', - google: 'Google', -} - /** * 画面から選ばせる役割。 * @@ -55,67 +40,31 @@ export const LLM_ROLE_NOTES: Record = { } /** - * 役割ごとの既定モデル。 + * 役割ごとの既定モデル。env にも設定にも何も無いときの最後の拠りどころ。 * - * `author` も残してあるのは、CLI がこの表を引くため。画面には出ない。 + * 互換サーバがこのIDを持っているとは限らないので、構成に合わせて + * `LLM_ACTOR_MODEL` などで上書きすること。`author` も残してあるのは CLI が引くため。 */ export const LLM_DEFAULT_MODELS = { - anthropic: { - actor: 'claude-sonnet-5', - judge: 'claude-haiku-4-5', - author: 'claude-opus-5', - }, - openai: { - actor: 'gpt-5.6-terra', - judge: 'gpt-5.6-luna', - author: 'gpt-5.6-sol', - }, - google: { - actor: 'gemini-3.5-flash', - judge: 'gemini-3.1-flash-lite', - // pro 系は preview 付きのIDでしか公開されていない(`gemini-3.1-pro` は存在しない)。 - // models API で実在を確認した上でこの値にしてある。 - author: 'gemini-3.1-pro-preview', - }, + actor: 'gpt-5.6-terra', + judge: 'gpt-5.6-luna', + author: 'gpt-5.6-sol', } -/** - * 画面に並べる選択肢。 - * - * 既定表に載っているものを、そのプロバイダで選べるモデルとして開く。 - * 役割の既定でないモデルも選べる(judge に重いモデルを当てる、等を試せるようにする)。 - */ -export const LLM_CATALOG: Record = { - anthropic: [ - { id: 'claude-opus-5', label: 'Claude Opus 5' }, - { id: 'claude-sonnet-5', label: 'Claude Sonnet 5' }, - { id: 'claude-haiku-4-5', label: 'Claude Haiku 4.5' }, - ], - openai: [ - { id: 'gpt-5.6-sol', label: 'GPT-5.6 Sol' }, - { id: 'gpt-5.6-terra', label: 'GPT-5.6 Terra' }, - { id: 'gpt-5.6-luna', label: 'GPT-5.6 Luna' }, - ], - google: [ - { id: 'gemini-3.1-pro-preview', label: 'Gemini 3.1 Pro (preview)' }, - { id: 'gemini-3.5-flash', label: 'Gemini 3.5 Flash' }, - { id: 'gemini-3.1-flash-lite', label: 'Gemini 3.1 Flash Lite' }, - ], -} - -export const isKnownModel = (provider: LlmProvider, modelId: string): boolean => - LLM_CATALOG[provider].some((model) => model.id === modelId) - /** * クライアントが送ってくる希望。 * - * model を素通しの文字列で受けるのは、ここで弾かないため。長さだけ見て通し、 - * 表に無いIDは `chooseLlm` が黙って捨てて既定へ落とす。400 を返すと、 - * localStorage に古いIDが残っているだけのプレイヤーを事件の途中で締め出すことになる。 - * provider だけは enum で厳格に見る(3値で安定していて、増減が設定の意味を変えるため)。 + * model を素通しの文字列で受ける。以前はここが許可リストを兼ねていたが、選べる一覧が + * 互換サーバ由来になったので、突き合わせる表がもう無い。長さだけ見て通す。 + * + * 400 を返さないのは、localStorage に古いIDが残っているだけのプレイヤーを + * 事件の途中で締め出すことになるため。互換サーバが知らないIDならそこでエラーになる。 + * + * NOTE: この画面は認証を持たないので、互換サーバに載せたモデルは誰でも指名できる。 + * 高いモデルを載せるなら、レート制限(RATE_LIMIT_MAX_CALLS)で殴られる量が + * 上限になることを踏まえて決めること。 */ export const llmOverrideSchema = z.object({ - provider: llmProviderSchema.optional(), model: z.string().nonempty().max(80).optional(), }) @@ -125,5 +74,3 @@ export const llmOverridesSchema = z.object({ actor: llmOverrideSchema.optional(), judge: llmOverrideSchema.optional(), }) - -export type LlmOverrides = z.infer diff --git a/db/migrations/0023_analytics-tables.sql b/db/migrations/0023_analytics-tables.sql new file mode 100644 index 0000000..31229d5 --- /dev/null +++ b/db/migrations/0023_analytics-tables.sql @@ -0,0 +1,54 @@ +-- 分析用の控え。play_sessions への外部キーを張らないのは意図で、 +-- 保持期間の削除に巻き込まれないことがこの2表の存在理由そのもの(db/schema.ts 参照)。 +-- 連番は drizzle が journal から採るので 0023 だが、手書きの 0025 より後に書いたもの。 +-- 互いに独立した変更なので、どちらの順で適用しても結果は変わらない。 +CREATE TABLE `analytics_sessions` ( + `session_id` text PRIMARY KEY NOT NULL, + `scenario_id` text NOT NULL, + `mode` text NOT NULL, + `detective` text, + `max_turns` integer NOT NULL, + `questions_per_turn` integer NOT NULL, + `exchanges_per_topic` integer NOT NULL, + `started_at` integer DEFAULT (unixepoch()) NOT NULL, + `finished_at` integer, + `culprit_character_id` text, + `culprit_correct` integer, + `method_correct` integer, + `motive_correct` integer, + `reasoning` text, + `method` text, + `motive` text, + `method_comment` text, + `motive_comment` text, + `solved_seconds` integer, + `question_count` integer, + `evidence_found` integer, + `evidence_total` integer, + `contradiction_count` integer, + `accuracy_percent` integer +); +--> statement-breakpoint +CREATE INDEX `analytics_sessions_scenario_id_idx` ON `analytics_sessions` (`scenario_id`);--> statement-breakpoint +CREATE INDEX `analytics_sessions_started_at_idx` ON `analytics_sessions` (`started_at`);--> statement-breakpoint +CREATE TABLE `analytics_turns` ( + `id` text PRIMARY KEY NOT NULL, + `session_id` text NOT NULL, + `scenario_id` text NOT NULL, + `mode` text NOT NULL, + `subject_kind` text NOT NULL, + `subject_id` text NOT NULL, + `turn_index` integer, + `question_count` integer, + `topic` text NOT NULL, + `exchanges` text NOT NULL, + `revealed_evidence_ids` text, + `revealed_revelation_ids` text, + `contradiction_pointed_out` integer, + `npc_lied` integer, + `created_at` integer DEFAULT (unixepoch()) NOT NULL +); +--> statement-breakpoint +CREATE INDEX `analytics_turns_session_id_idx` ON `analytics_turns` (`session_id`);--> statement-breakpoint +CREATE INDEX `analytics_turns_scenario_id_idx` ON `analytics_turns` (`scenario_id`);--> statement-breakpoint +CREATE INDEX `analytics_turns_created_at_idx` ON `analytics_turns` (`created_at`); \ No newline at end of file diff --git a/db/migrations/0024_character-short-name.sql b/db/migrations/0024_character-short-name.sql new file mode 100644 index 0000000..e8381c3 --- /dev/null +++ b/db/migrations/0024_character-short-name.sql @@ -0,0 +1,5 @@ +ALTER TABLE `characters` ADD `short_name` text DEFAULT '' NOT NULL;--> statement-breakpoint +-- 既存の行にはまだ短い名前が無い。空のままにすると帯から名前が消えるので、 +-- フルネームを写して埋める。窮屈なのは直っていないが、表示は壊れない。 +-- 短い名前そのものは、あとから yaml へ書いて焼き直す。 +UPDATE `characters` SET `short_name` = `name` WHERE trim(`short_name`) = ''; diff --git a/db/migrations/0025_tsukimisou-death-estimate-consistency.sql b/db/migrations/0025_tsukimisou-death-estimate-consistency.sql new file mode 100644 index 0000000..a309fec --- /dev/null +++ b/db/migrations/0025_tsukimisou-death-estimate-consistency.sql @@ -0,0 +1,27 @@ +-- 月見荘: 遺体検分の返答と死亡推定の開示条件を一致させる。 +-- 既存の victim_findings(前提 Evidence UUID を含む)は保持し、所見だけを追記する。 +UPDATE scenario_truths +SET victim_findings = CASE + WHEN EXISTS ( + SELECT 1 + FROM json_each(scenario_truths.victim_findings) + WHERE json_extract(value, '$.id') = 'postmortem-state' + ) THEN victim_findings + ELSE json_insert( + victim_findings, + '$[#]', + json('{"id":"postmortem-state","statement":"発見時、体にはまだ温かさが残っており、死後硬直も始まりかけた段階にとどまっている。","requires":{"revelations":[],"evidences":[]}}') + ) +END +WHERE scenario_id = ( + SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1 +); + +UPDATE evidences +SET + description = '唇のまわりと指先のしびれの跡に加え、発見時には体にまだ温かさが残り、死後硬直も始まりかけた段階にとどまっている。発見時刻と合わせると、事切れたのは20時15分ごろと推定できる。', + reveal_condition = 'プレイヤーが遺体を調べ、検分の返答で「体にまだ温かさが残り、死後硬直も始まりかけている」という所見を実際に確認し、その所見を根拠に死亡時刻を検討したら開示する。' +WHERE scenario_id = ( + SELECT id FROM scenarios WHERE victim_name = '高瀬涼子' LIMIT 1 +) +AND label = '遺体に残る中毒の徴候と、その進み具合'; diff --git a/db/migrations/0026_character-age-gender.sql b/db/migrations/0026_character-age-gender.sql new file mode 100644 index 0000000..788c400 --- /dev/null +++ b/db/migrations/0026_character-age-gender.sql @@ -0,0 +1,6 @@ +ALTER TABLE `characters` ADD `age_group` text DEFAULT 'unknown' NOT NULL;--> statement-breakpoint +ALTER TABLE `characters` ADD `gender` text DEFAULT 'unknown' NOT NULL; +-- 既存の行を 'unknown' のままにするのは、書き換えるべき正しい値がどこにも無いため。 +-- 年ごろと性別は今のところ personality の文章にしか書かれておらず、そこから機械的に +-- 引き当てると取り違える。'unknown' の行はキャラクターシートに出ないので、 +-- これまでと同じく人物像の文章が読まれる。値は yaml へ書いて焼き直したときに入る。 diff --git a/db/migrations/0027_character-voice.sql b/db/migrations/0027_character-voice.sql new file mode 100644 index 0000000..7134c3b --- /dev/null +++ b/db/migrations/0027_character-voice.sql @@ -0,0 +1,5 @@ +ALTER TABLE `characters` ADD `voice_seed` integer;--> statement-breakpoint +ALTER TABLE `characters` ADD `voice_caption` text; +-- 既定を置かず null のままにするのは、シードに無難な既定値が無いため。 +-- 0 も正当なシードなので、埋めた値と「まだ決めていない」を区別できなくなる。 +-- 両方 null の人物は喋らない。声は yaml へ書いて焼き直したときに入る。 diff --git a/db/migrations/0028_llm-usage-drop-provider.sql b/db/migrations/0028_llm-usage-drop-provider.sql new file mode 100644 index 0000000..719684d --- /dev/null +++ b/db/migrations/0028_llm-usage-drop-provider.sql @@ -0,0 +1,4 @@ +-- 宛先が OpenAI互換サーバ1つになり、anthropic / openai / google の区分が +-- 経路も設定も分けなくなったので落とす。課金の内訳はモデルID(model 列)で追える。 +-- 既存行の provider 値は戻らない。列にインデックスも外部キーも無いため再構築は要らない。 +ALTER TABLE `llm_usages` DROP COLUMN `provider`; diff --git a/db/migrations/0029_tts-speakers.sql b/db/migrations/0029_tts-speakers.sql new file mode 100644 index 0000000..2309603 --- /dev/null +++ b/db/migrations/0029_tts-speakers.sql @@ -0,0 +1,6 @@ +CREATE TABLE `tts_speakers` ( + `id` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `age_group` text DEFAULT 'unknown' NOT NULL, + `gender` text DEFAULT 'unknown' NOT NULL +); diff --git a/db/migrations/0030_messages-drop-character-fk.sql b/db/migrations/0030_messages-drop-character-fk.sql new file mode 100644 index 0000000..51a8602 --- /dev/null +++ b/db/migrations/0030_messages-drop-character-fk.sql @@ -0,0 +1,17 @@ +PRAGMA foreign_keys=OFF;--> statement-breakpoint +CREATE TABLE `__new_messages` ( + `id` text PRIMARY KEY NOT NULL, + `session_id` text NOT NULL, + `character_id` text NOT NULL, + `role` text NOT NULL, + `content` text NOT NULL, + `created_at` integer DEFAULT (unixepoch()) NOT NULL, + FOREIGN KEY (`session_id`) REFERENCES `play_sessions`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +INSERT INTO `__new_messages`("id", "session_id", "character_id", "role", "content", "created_at") SELECT "id", "session_id", "character_id", "role", "content", "created_at" FROM `messages`;--> statement-breakpoint +DROP TABLE `messages`;--> statement-breakpoint +ALTER TABLE `__new_messages` RENAME TO `messages`;--> statement-breakpoint +PRAGMA foreign_keys=ON;--> statement-breakpoint +CREATE INDEX `messages_session_id_idx` ON `messages` (`session_id`);--> statement-breakpoint +CREATE INDEX `messages_character_id_idx` ON `messages` (`character_id`); \ No newline at end of file diff --git a/db/migrations/0031_skinny_pet_avengers.sql b/db/migrations/0031_skinny_pet_avengers.sql new file mode 100644 index 0000000..b0cd038 --- /dev/null +++ b/db/migrations/0031_skinny_pet_avengers.sql @@ -0,0 +1 @@ +ALTER TABLE `analytics_turns` ADD `judge_tuning` text; \ No newline at end of file diff --git a/db/migrations/meta/0023_snapshot.json b/db/migrations/meta/0023_snapshot.json new file mode 100644 index 0000000..064add5 --- /dev/null +++ b/db/migrations/meta/0023_snapshot.json @@ -0,0 +1,1522 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "1f5ead39-3cf1-403e-9a67-e4382532478a", + "prevId": "1a711748-a372-470e-957e-00db96cda2e8", + "tables": { + "analytics_sessions": { + "name": "analytics_sessions", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "detective": { + "name": "detective", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max_turns": { + "name": "max_turns", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "questions_per_turn": { + "name": "questions_per_turn", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "exchanges_per_topic": { + "name": "exchanges_per_topic", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "culprit_character_id": { + "name": "culprit_character_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "culprit_correct": { + "name": "culprit_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "method_correct": { + "name": "method_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_correct": { + "name": "motive_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reasoning": { + "name": "reasoning", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive": { + "name": "motive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "method_comment": { + "name": "method_comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_comment": { + "name": "motive_comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "solved_seconds": { + "name": "solved_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "evidence_found": { + "name": "evidence_found", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "evidence_total": { + "name": "evidence_total", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "contradiction_count": { + "name": "contradiction_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accuracy_percent": { + "name": "accuracy_percent", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "analytics_sessions_scenario_id_idx": { + "name": "analytics_sessions_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "analytics_sessions_started_at_idx": { + "name": "analytics_sessions_started_at_idx", + "columns": [ + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "analytics_turns": { + "name": "analytics_turns", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_kind": { + "name": "subject_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "turn_index": { + "name": "turn_index", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "topic": { + "name": "topic", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "exchanges": { + "name": "exchanges", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revealed_evidence_ids": { + "name": "revealed_evidence_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revealed_revelation_ids": { + "name": "revealed_revelation_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "contradiction_pointed_out": { + "name": "contradiction_pointed_out", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "npc_lied": { + "name": "npc_lied", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "analytics_turns_session_id_idx": { + "name": "analytics_turns_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "analytics_turns_scenario_id_idx": { + "name": "analytics_turns_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "analytics_turns_created_at_idx": { + "name": "analytics_turns_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "characters": { + "name": "characters", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "public_introduction": { + "name": "public_introduction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "personality": { + "name": "personality", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "knowledge": { + "name": "knowledge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "secrets": { + "name": "secrets", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "goals": { + "name": "goals", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lies": { + "name": "lies", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lie_refs": { + "name": "lie_refs", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "memories": { + "name": "memories", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "characters_scenario_id_idx": { + "name": "characters_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "characters_scenario_id_scenarios_id_fk": { + "name": "characters_scenario_id_scenarios_id_fk", + "tableFrom": "characters", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "discoveries": { + "name": "discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_id": { + "name": "evidence_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "discoveries_evidence_id_idx": { + "name": "discoveries_evidence_id_idx", + "columns": [ + "evidence_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "discoveries_session_id_play_sessions_id_fk": { + "name": "discoveries_session_id_play_sessions_id_fk", + "tableFrom": "discoveries", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "discoveries_evidence_id_evidences_id_fk": { + "name": "discoveries_evidence_id_evidences_id_fk", + "tableFrom": "discoveries", + "tableTo": "evidences", + "columnsFrom": [ + "evidence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "discoveries_session_id_evidence_id_pk": { + "columns": [ + "session_id", + "evidence_id" + ], + "name": "discoveries_session_id_evidence_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "evidences": { + "name": "evidences", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reveal_condition": { + "name": "reveal_condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "supports": { + "name": "supports", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "contradicts": { + "name": "contradicts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "reveals_death_time": { + "name": "reveals_death_time", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "evidences_scenario_id_idx": { + "name": "evidences_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "evidences_scenario_id_scenarios_id_fk": { + "name": "evidences_scenario_id_scenarios_id_fk", + "tableFrom": "evidences", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "llm_usages": { + "name": "llm_usages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "llm_usages_created_at_idx": { + "name": "llm_usages_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "llm_usages_session_id_idx": { + "name": "llm_usages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "character_id": { + "name": "character_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "messages_session_id_idx": { + "name": "messages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "messages_character_id_idx": { + "name": "messages_character_id_idx", + "columns": [ + "character_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "messages_session_id_play_sessions_id_fk": { + "name": "messages_session_id_play_sessions_id_fk", + "tableFrom": "messages", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "messages_character_id_characters_id_fk": { + "name": "messages_character_id_characters_id_fk", + "tableFrom": "messages", + "tableTo": "characters", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "play_sessions": { + "name": "play_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detective": { + "name": "detective", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "play_sessions_scenario_id_idx": { + "name": "play_sessions_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "play_sessions_started_at_idx": { + "name": "play_sessions_started_at_idx", + "columns": [ + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "play_sessions_scenario_id_scenarios_id_fk": { + "name": "play_sessions_scenario_id_scenarios_id_fk", + "tableFrom": "play_sessions", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reports": { + "name": "reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reported_at": { + "name": "reported_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "reports_scenario_id_idx": { + "name": "reports_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "reports_scenario_id_scenarios_id_fk": { + "name": "reports_scenario_id_scenarios_id_fk", + "tableFrom": "reports", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "results": { + "name": "results", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "solved_seconds": { + "name": "solved_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_found": { + "name": "evidence_found", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contradiction_count": { + "name": "contradiction_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accuracy_percent": { + "name": "accuracy_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method_correct": { + "name": "method_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_correct": { + "name": "motive_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deduction": { + "name": "deduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "results_session_id_play_sessions_id_fk": { + "name": "results_session_id_play_sessions_id_fk", + "tableFrom": "results", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelation_discoveries": { + "name": "revelation_discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revelation_id": { + "name": "revelation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "revelation_discoveries_revelation_id_idx": { + "name": "revelation_discoveries_revelation_id_idx", + "columns": [ + "revelation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelation_discoveries_session_id_play_sessions_id_fk": { + "name": "revelation_discoveries_session_id_play_sessions_id_fk", + "tableFrom": "revelation_discoveries", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "revelation_discoveries_revelation_id_revelations_id_fk": { + "name": "revelation_discoveries_revelation_id_revelations_id_fk", + "tableFrom": "revelation_discoveries", + "tableTo": "revelations", + "columnsFrom": [ + "revelation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "revelation_discoveries_session_id_revelation_id_pk": { + "columns": [ + "session_id", + "revelation_id" + ], + "name": "revelation_discoveries_session_id_revelation_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelations": { + "name": "revelations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "related_facts": { + "name": "related_facts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "revelations_scenario_id_idx": { + "name": "revelations_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelations_scenario_id_scenarios_id_fk": { + "name": "revelations_scenario_id_scenarios_id_fk", + "tableFrom": "revelations", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenario_truths": { + "name": "scenario_truths", + "columns": { + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "culprit_character_id": { + "name": "culprit_character_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truth": { + "name": "truth", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive": { + "name": "motive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timeline": { + "name": "timeline", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timeline_events": { + "name": "timeline_events", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "victim_cause_of_death": { + "name": "victim_cause_of_death", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_findings": { + "name": "victim_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "place_findings": { + "name": "place_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "secret_keywords": { + "name": "secret_keywords", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "scenario_truths_scenario_id_scenarios_id_fk": { + "name": "scenario_truths_scenario_id_scenarios_id_fk", + "tableFrom": "scenario_truths", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenarios": { + "name": "scenarios", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "synopsis": { + "name": "synopsis", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "briefing": { + "name": "briefing", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "floor_plan": { + "name": "floor_plan", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "time_start": { + "name": "time_start", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_end": { + "name": "time_end", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_name": { + "name": "victim_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_introduction": { + "name": "victim_introduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_at": { + "name": "victim_found_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_in": { + "name": "victim_found_in", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_investigable": { + "name": "victim_investigable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "victim_estimated_death_at": { + "name": "victim_estimated_death_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "places": { + "name": "places", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_published": { + "name": "is_published", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "estimated_minutes": { + "name": "estimated_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/db/migrations/meta/0024_snapshot.json b/db/migrations/meta/0024_snapshot.json new file mode 100644 index 0000000..aa08c8d --- /dev/null +++ b/db/migrations/meta/0024_snapshot.json @@ -0,0 +1,1530 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "d34b9fdc-9abd-47a8-991f-786fb35f50c1", + "prevId": "1f5ead39-3cf1-403e-9a67-e4382532478a", + "tables": { + "analytics_sessions": { + "name": "analytics_sessions", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "detective": { + "name": "detective", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max_turns": { + "name": "max_turns", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "questions_per_turn": { + "name": "questions_per_turn", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "exchanges_per_topic": { + "name": "exchanges_per_topic", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "culprit_character_id": { + "name": "culprit_character_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "culprit_correct": { + "name": "culprit_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "method_correct": { + "name": "method_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_correct": { + "name": "motive_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reasoning": { + "name": "reasoning", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive": { + "name": "motive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "method_comment": { + "name": "method_comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_comment": { + "name": "motive_comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "solved_seconds": { + "name": "solved_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "evidence_found": { + "name": "evidence_found", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "evidence_total": { + "name": "evidence_total", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "contradiction_count": { + "name": "contradiction_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accuracy_percent": { + "name": "accuracy_percent", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "analytics_sessions_scenario_id_idx": { + "name": "analytics_sessions_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "analytics_sessions_started_at_idx": { + "name": "analytics_sessions_started_at_idx", + "columns": [ + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "analytics_turns": { + "name": "analytics_turns", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_kind": { + "name": "subject_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "turn_index": { + "name": "turn_index", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "topic": { + "name": "topic", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "exchanges": { + "name": "exchanges", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revealed_evidence_ids": { + "name": "revealed_evidence_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revealed_revelation_ids": { + "name": "revealed_revelation_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "contradiction_pointed_out": { + "name": "contradiction_pointed_out", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "npc_lied": { + "name": "npc_lied", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "analytics_turns_session_id_idx": { + "name": "analytics_turns_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "analytics_turns_scenario_id_idx": { + "name": "analytics_turns_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "analytics_turns_created_at_idx": { + "name": "analytics_turns_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "characters": { + "name": "characters", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "short_name": { + "name": "short_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "public_introduction": { + "name": "public_introduction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "personality": { + "name": "personality", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "knowledge": { + "name": "knowledge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "secrets": { + "name": "secrets", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "goals": { + "name": "goals", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lies": { + "name": "lies", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lie_refs": { + "name": "lie_refs", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "memories": { + "name": "memories", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "characters_scenario_id_idx": { + "name": "characters_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "characters_scenario_id_scenarios_id_fk": { + "name": "characters_scenario_id_scenarios_id_fk", + "tableFrom": "characters", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "discoveries": { + "name": "discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_id": { + "name": "evidence_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "discoveries_evidence_id_idx": { + "name": "discoveries_evidence_id_idx", + "columns": [ + "evidence_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "discoveries_session_id_play_sessions_id_fk": { + "name": "discoveries_session_id_play_sessions_id_fk", + "tableFrom": "discoveries", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "discoveries_evidence_id_evidences_id_fk": { + "name": "discoveries_evidence_id_evidences_id_fk", + "tableFrom": "discoveries", + "tableTo": "evidences", + "columnsFrom": [ + "evidence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "discoveries_session_id_evidence_id_pk": { + "columns": [ + "session_id", + "evidence_id" + ], + "name": "discoveries_session_id_evidence_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "evidences": { + "name": "evidences", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reveal_condition": { + "name": "reveal_condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "supports": { + "name": "supports", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "contradicts": { + "name": "contradicts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "reveals_death_time": { + "name": "reveals_death_time", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "evidences_scenario_id_idx": { + "name": "evidences_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "evidences_scenario_id_scenarios_id_fk": { + "name": "evidences_scenario_id_scenarios_id_fk", + "tableFrom": "evidences", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "llm_usages": { + "name": "llm_usages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "llm_usages_created_at_idx": { + "name": "llm_usages_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "llm_usages_session_id_idx": { + "name": "llm_usages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "character_id": { + "name": "character_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "messages_session_id_idx": { + "name": "messages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "messages_character_id_idx": { + "name": "messages_character_id_idx", + "columns": [ + "character_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "messages_session_id_play_sessions_id_fk": { + "name": "messages_session_id_play_sessions_id_fk", + "tableFrom": "messages", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "messages_character_id_characters_id_fk": { + "name": "messages_character_id_characters_id_fk", + "tableFrom": "messages", + "tableTo": "characters", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "play_sessions": { + "name": "play_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detective": { + "name": "detective", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "play_sessions_scenario_id_idx": { + "name": "play_sessions_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "play_sessions_started_at_idx": { + "name": "play_sessions_started_at_idx", + "columns": [ + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "play_sessions_scenario_id_scenarios_id_fk": { + "name": "play_sessions_scenario_id_scenarios_id_fk", + "tableFrom": "play_sessions", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reports": { + "name": "reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reported_at": { + "name": "reported_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "reports_scenario_id_idx": { + "name": "reports_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "reports_scenario_id_scenarios_id_fk": { + "name": "reports_scenario_id_scenarios_id_fk", + "tableFrom": "reports", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "results": { + "name": "results", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "solved_seconds": { + "name": "solved_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_found": { + "name": "evidence_found", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contradiction_count": { + "name": "contradiction_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accuracy_percent": { + "name": "accuracy_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method_correct": { + "name": "method_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_correct": { + "name": "motive_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deduction": { + "name": "deduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "results_session_id_play_sessions_id_fk": { + "name": "results_session_id_play_sessions_id_fk", + "tableFrom": "results", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelation_discoveries": { + "name": "revelation_discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revelation_id": { + "name": "revelation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "revelation_discoveries_revelation_id_idx": { + "name": "revelation_discoveries_revelation_id_idx", + "columns": [ + "revelation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelation_discoveries_session_id_play_sessions_id_fk": { + "name": "revelation_discoveries_session_id_play_sessions_id_fk", + "tableFrom": "revelation_discoveries", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "revelation_discoveries_revelation_id_revelations_id_fk": { + "name": "revelation_discoveries_revelation_id_revelations_id_fk", + "tableFrom": "revelation_discoveries", + "tableTo": "revelations", + "columnsFrom": [ + "revelation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "revelation_discoveries_session_id_revelation_id_pk": { + "columns": [ + "session_id", + "revelation_id" + ], + "name": "revelation_discoveries_session_id_revelation_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelations": { + "name": "revelations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "related_facts": { + "name": "related_facts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "revelations_scenario_id_idx": { + "name": "revelations_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelations_scenario_id_scenarios_id_fk": { + "name": "revelations_scenario_id_scenarios_id_fk", + "tableFrom": "revelations", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenario_truths": { + "name": "scenario_truths", + "columns": { + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "culprit_character_id": { + "name": "culprit_character_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truth": { + "name": "truth", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive": { + "name": "motive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timeline": { + "name": "timeline", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timeline_events": { + "name": "timeline_events", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "victim_cause_of_death": { + "name": "victim_cause_of_death", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_findings": { + "name": "victim_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "place_findings": { + "name": "place_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "secret_keywords": { + "name": "secret_keywords", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "scenario_truths_scenario_id_scenarios_id_fk": { + "name": "scenario_truths_scenario_id_scenarios_id_fk", + "tableFrom": "scenario_truths", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenarios": { + "name": "scenarios", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "synopsis": { + "name": "synopsis", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "briefing": { + "name": "briefing", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "floor_plan": { + "name": "floor_plan", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "time_start": { + "name": "time_start", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_end": { + "name": "time_end", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_name": { + "name": "victim_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_introduction": { + "name": "victim_introduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_at": { + "name": "victim_found_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_in": { + "name": "victim_found_in", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_investigable": { + "name": "victim_investigable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "victim_estimated_death_at": { + "name": "victim_estimated_death_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "places": { + "name": "places", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_published": { + "name": "is_published", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "estimated_minutes": { + "name": "estimated_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/db/migrations/meta/0026_snapshot.json b/db/migrations/meta/0026_snapshot.json new file mode 100644 index 0000000..7d4a4f8 --- /dev/null +++ b/db/migrations/meta/0026_snapshot.json @@ -0,0 +1,1546 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "2f0bf734-edde-4eb3-ab25-6885267077b7", + "prevId": "d34b9fdc-9abd-47a8-991f-786fb35f50c1", + "tables": { + "analytics_sessions": { + "name": "analytics_sessions", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "detective": { + "name": "detective", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max_turns": { + "name": "max_turns", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "questions_per_turn": { + "name": "questions_per_turn", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "exchanges_per_topic": { + "name": "exchanges_per_topic", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "culprit_character_id": { + "name": "culprit_character_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "culprit_correct": { + "name": "culprit_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "method_correct": { + "name": "method_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_correct": { + "name": "motive_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reasoning": { + "name": "reasoning", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive": { + "name": "motive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "method_comment": { + "name": "method_comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_comment": { + "name": "motive_comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "solved_seconds": { + "name": "solved_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "evidence_found": { + "name": "evidence_found", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "evidence_total": { + "name": "evidence_total", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "contradiction_count": { + "name": "contradiction_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accuracy_percent": { + "name": "accuracy_percent", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "analytics_sessions_scenario_id_idx": { + "name": "analytics_sessions_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "analytics_sessions_started_at_idx": { + "name": "analytics_sessions_started_at_idx", + "columns": [ + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "analytics_turns": { + "name": "analytics_turns", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_kind": { + "name": "subject_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "turn_index": { + "name": "turn_index", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "topic": { + "name": "topic", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "exchanges": { + "name": "exchanges", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revealed_evidence_ids": { + "name": "revealed_evidence_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revealed_revelation_ids": { + "name": "revealed_revelation_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "contradiction_pointed_out": { + "name": "contradiction_pointed_out", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "npc_lied": { + "name": "npc_lied", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "analytics_turns_session_id_idx": { + "name": "analytics_turns_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "analytics_turns_scenario_id_idx": { + "name": "analytics_turns_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "analytics_turns_created_at_idx": { + "name": "analytics_turns_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "characters": { + "name": "characters", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "short_name": { + "name": "short_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "age_group": { + "name": "age_group", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "gender": { + "name": "gender", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "public_introduction": { + "name": "public_introduction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "personality": { + "name": "personality", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "knowledge": { + "name": "knowledge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "secrets": { + "name": "secrets", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "goals": { + "name": "goals", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lies": { + "name": "lies", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lie_refs": { + "name": "lie_refs", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "memories": { + "name": "memories", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "characters_scenario_id_idx": { + "name": "characters_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "characters_scenario_id_scenarios_id_fk": { + "name": "characters_scenario_id_scenarios_id_fk", + "tableFrom": "characters", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "discoveries": { + "name": "discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_id": { + "name": "evidence_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "discoveries_evidence_id_idx": { + "name": "discoveries_evidence_id_idx", + "columns": [ + "evidence_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "discoveries_session_id_play_sessions_id_fk": { + "name": "discoveries_session_id_play_sessions_id_fk", + "tableFrom": "discoveries", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "discoveries_evidence_id_evidences_id_fk": { + "name": "discoveries_evidence_id_evidences_id_fk", + "tableFrom": "discoveries", + "tableTo": "evidences", + "columnsFrom": [ + "evidence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "discoveries_session_id_evidence_id_pk": { + "columns": [ + "session_id", + "evidence_id" + ], + "name": "discoveries_session_id_evidence_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "evidences": { + "name": "evidences", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reveal_condition": { + "name": "reveal_condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "supports": { + "name": "supports", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "contradicts": { + "name": "contradicts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "reveals_death_time": { + "name": "reveals_death_time", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "evidences_scenario_id_idx": { + "name": "evidences_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "evidences_scenario_id_scenarios_id_fk": { + "name": "evidences_scenario_id_scenarios_id_fk", + "tableFrom": "evidences", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "llm_usages": { + "name": "llm_usages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "llm_usages_created_at_idx": { + "name": "llm_usages_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "llm_usages_session_id_idx": { + "name": "llm_usages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "character_id": { + "name": "character_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "messages_session_id_idx": { + "name": "messages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "messages_character_id_idx": { + "name": "messages_character_id_idx", + "columns": [ + "character_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "messages_session_id_play_sessions_id_fk": { + "name": "messages_session_id_play_sessions_id_fk", + "tableFrom": "messages", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "messages_character_id_characters_id_fk": { + "name": "messages_character_id_characters_id_fk", + "tableFrom": "messages", + "tableTo": "characters", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "play_sessions": { + "name": "play_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detective": { + "name": "detective", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "play_sessions_scenario_id_idx": { + "name": "play_sessions_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "play_sessions_started_at_idx": { + "name": "play_sessions_started_at_idx", + "columns": [ + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "play_sessions_scenario_id_scenarios_id_fk": { + "name": "play_sessions_scenario_id_scenarios_id_fk", + "tableFrom": "play_sessions", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reports": { + "name": "reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reported_at": { + "name": "reported_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "reports_scenario_id_idx": { + "name": "reports_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "reports_scenario_id_scenarios_id_fk": { + "name": "reports_scenario_id_scenarios_id_fk", + "tableFrom": "reports", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "results": { + "name": "results", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "solved_seconds": { + "name": "solved_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_found": { + "name": "evidence_found", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contradiction_count": { + "name": "contradiction_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accuracy_percent": { + "name": "accuracy_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method_correct": { + "name": "method_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_correct": { + "name": "motive_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deduction": { + "name": "deduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "results_session_id_play_sessions_id_fk": { + "name": "results_session_id_play_sessions_id_fk", + "tableFrom": "results", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelation_discoveries": { + "name": "revelation_discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revelation_id": { + "name": "revelation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "revelation_discoveries_revelation_id_idx": { + "name": "revelation_discoveries_revelation_id_idx", + "columns": [ + "revelation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelation_discoveries_session_id_play_sessions_id_fk": { + "name": "revelation_discoveries_session_id_play_sessions_id_fk", + "tableFrom": "revelation_discoveries", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "revelation_discoveries_revelation_id_revelations_id_fk": { + "name": "revelation_discoveries_revelation_id_revelations_id_fk", + "tableFrom": "revelation_discoveries", + "tableTo": "revelations", + "columnsFrom": [ + "revelation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "revelation_discoveries_session_id_revelation_id_pk": { + "columns": [ + "session_id", + "revelation_id" + ], + "name": "revelation_discoveries_session_id_revelation_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelations": { + "name": "revelations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "related_facts": { + "name": "related_facts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "revelations_scenario_id_idx": { + "name": "revelations_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelations_scenario_id_scenarios_id_fk": { + "name": "revelations_scenario_id_scenarios_id_fk", + "tableFrom": "revelations", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenario_truths": { + "name": "scenario_truths", + "columns": { + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "culprit_character_id": { + "name": "culprit_character_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truth": { + "name": "truth", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive": { + "name": "motive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timeline": { + "name": "timeline", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timeline_events": { + "name": "timeline_events", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "victim_cause_of_death": { + "name": "victim_cause_of_death", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_findings": { + "name": "victim_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "place_findings": { + "name": "place_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "secret_keywords": { + "name": "secret_keywords", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "scenario_truths_scenario_id_scenarios_id_fk": { + "name": "scenario_truths_scenario_id_scenarios_id_fk", + "tableFrom": "scenario_truths", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenarios": { + "name": "scenarios", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "synopsis": { + "name": "synopsis", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "briefing": { + "name": "briefing", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "floor_plan": { + "name": "floor_plan", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "time_start": { + "name": "time_start", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_end": { + "name": "time_end", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_name": { + "name": "victim_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_introduction": { + "name": "victim_introduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_at": { + "name": "victim_found_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_in": { + "name": "victim_found_in", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_investigable": { + "name": "victim_investigable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "victim_estimated_death_at": { + "name": "victim_estimated_death_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "places": { + "name": "places", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_published": { + "name": "is_published", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "estimated_minutes": { + "name": "estimated_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/db/migrations/meta/0027_snapshot.json b/db/migrations/meta/0027_snapshot.json new file mode 100644 index 0000000..baacc40 --- /dev/null +++ b/db/migrations/meta/0027_snapshot.json @@ -0,0 +1,1560 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "73124777-d458-4905-b666-c6e2b52506db", + "prevId": "2f0bf734-edde-4eb3-ab25-6885267077b7", + "tables": { + "analytics_sessions": { + "name": "analytics_sessions", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "detective": { + "name": "detective", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max_turns": { + "name": "max_turns", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "questions_per_turn": { + "name": "questions_per_turn", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "exchanges_per_topic": { + "name": "exchanges_per_topic", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "culprit_character_id": { + "name": "culprit_character_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "culprit_correct": { + "name": "culprit_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "method_correct": { + "name": "method_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_correct": { + "name": "motive_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reasoning": { + "name": "reasoning", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive": { + "name": "motive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "method_comment": { + "name": "method_comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_comment": { + "name": "motive_comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "solved_seconds": { + "name": "solved_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "evidence_found": { + "name": "evidence_found", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "evidence_total": { + "name": "evidence_total", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "contradiction_count": { + "name": "contradiction_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accuracy_percent": { + "name": "accuracy_percent", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "analytics_sessions_scenario_id_idx": { + "name": "analytics_sessions_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "analytics_sessions_started_at_idx": { + "name": "analytics_sessions_started_at_idx", + "columns": [ + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "analytics_turns": { + "name": "analytics_turns", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_kind": { + "name": "subject_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "turn_index": { + "name": "turn_index", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "topic": { + "name": "topic", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "exchanges": { + "name": "exchanges", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revealed_evidence_ids": { + "name": "revealed_evidence_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revealed_revelation_ids": { + "name": "revealed_revelation_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "contradiction_pointed_out": { + "name": "contradiction_pointed_out", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "npc_lied": { + "name": "npc_lied", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "analytics_turns_session_id_idx": { + "name": "analytics_turns_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "analytics_turns_scenario_id_idx": { + "name": "analytics_turns_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "analytics_turns_created_at_idx": { + "name": "analytics_turns_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "characters": { + "name": "characters", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "short_name": { + "name": "short_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "age_group": { + "name": "age_group", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "gender": { + "name": "gender", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "voice_seed": { + "name": "voice_seed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "voice_caption": { + "name": "voice_caption", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_introduction": { + "name": "public_introduction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "personality": { + "name": "personality", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "knowledge": { + "name": "knowledge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "secrets": { + "name": "secrets", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "goals": { + "name": "goals", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lies": { + "name": "lies", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lie_refs": { + "name": "lie_refs", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "memories": { + "name": "memories", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "characters_scenario_id_idx": { + "name": "characters_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "characters_scenario_id_scenarios_id_fk": { + "name": "characters_scenario_id_scenarios_id_fk", + "tableFrom": "characters", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "discoveries": { + "name": "discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_id": { + "name": "evidence_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "discoveries_evidence_id_idx": { + "name": "discoveries_evidence_id_idx", + "columns": [ + "evidence_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "discoveries_session_id_play_sessions_id_fk": { + "name": "discoveries_session_id_play_sessions_id_fk", + "tableFrom": "discoveries", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "discoveries_evidence_id_evidences_id_fk": { + "name": "discoveries_evidence_id_evidences_id_fk", + "tableFrom": "discoveries", + "tableTo": "evidences", + "columnsFrom": [ + "evidence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "discoveries_session_id_evidence_id_pk": { + "columns": [ + "session_id", + "evidence_id" + ], + "name": "discoveries_session_id_evidence_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "evidences": { + "name": "evidences", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reveal_condition": { + "name": "reveal_condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "supports": { + "name": "supports", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "contradicts": { + "name": "contradicts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "reveals_death_time": { + "name": "reveals_death_time", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "evidences_scenario_id_idx": { + "name": "evidences_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "evidences_scenario_id_scenarios_id_fk": { + "name": "evidences_scenario_id_scenarios_id_fk", + "tableFrom": "evidences", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "llm_usages": { + "name": "llm_usages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "llm_usages_created_at_idx": { + "name": "llm_usages_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "llm_usages_session_id_idx": { + "name": "llm_usages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "character_id": { + "name": "character_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "messages_session_id_idx": { + "name": "messages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "messages_character_id_idx": { + "name": "messages_character_id_idx", + "columns": [ + "character_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "messages_session_id_play_sessions_id_fk": { + "name": "messages_session_id_play_sessions_id_fk", + "tableFrom": "messages", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "messages_character_id_characters_id_fk": { + "name": "messages_character_id_characters_id_fk", + "tableFrom": "messages", + "tableTo": "characters", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "play_sessions": { + "name": "play_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detective": { + "name": "detective", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "play_sessions_scenario_id_idx": { + "name": "play_sessions_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "play_sessions_started_at_idx": { + "name": "play_sessions_started_at_idx", + "columns": [ + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "play_sessions_scenario_id_scenarios_id_fk": { + "name": "play_sessions_scenario_id_scenarios_id_fk", + "tableFrom": "play_sessions", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reports": { + "name": "reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reported_at": { + "name": "reported_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "reports_scenario_id_idx": { + "name": "reports_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "reports_scenario_id_scenarios_id_fk": { + "name": "reports_scenario_id_scenarios_id_fk", + "tableFrom": "reports", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "results": { + "name": "results", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "solved_seconds": { + "name": "solved_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_found": { + "name": "evidence_found", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contradiction_count": { + "name": "contradiction_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accuracy_percent": { + "name": "accuracy_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method_correct": { + "name": "method_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_correct": { + "name": "motive_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deduction": { + "name": "deduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "results_session_id_play_sessions_id_fk": { + "name": "results_session_id_play_sessions_id_fk", + "tableFrom": "results", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelation_discoveries": { + "name": "revelation_discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revelation_id": { + "name": "revelation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "revelation_discoveries_revelation_id_idx": { + "name": "revelation_discoveries_revelation_id_idx", + "columns": [ + "revelation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelation_discoveries_session_id_play_sessions_id_fk": { + "name": "revelation_discoveries_session_id_play_sessions_id_fk", + "tableFrom": "revelation_discoveries", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "revelation_discoveries_revelation_id_revelations_id_fk": { + "name": "revelation_discoveries_revelation_id_revelations_id_fk", + "tableFrom": "revelation_discoveries", + "tableTo": "revelations", + "columnsFrom": [ + "revelation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "revelation_discoveries_session_id_revelation_id_pk": { + "columns": [ + "session_id", + "revelation_id" + ], + "name": "revelation_discoveries_session_id_revelation_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelations": { + "name": "revelations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "related_facts": { + "name": "related_facts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "revelations_scenario_id_idx": { + "name": "revelations_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelations_scenario_id_scenarios_id_fk": { + "name": "revelations_scenario_id_scenarios_id_fk", + "tableFrom": "revelations", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenario_truths": { + "name": "scenario_truths", + "columns": { + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "culprit_character_id": { + "name": "culprit_character_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truth": { + "name": "truth", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive": { + "name": "motive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timeline": { + "name": "timeline", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timeline_events": { + "name": "timeline_events", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "victim_cause_of_death": { + "name": "victim_cause_of_death", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_findings": { + "name": "victim_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "place_findings": { + "name": "place_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "secret_keywords": { + "name": "secret_keywords", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "scenario_truths_scenario_id_scenarios_id_fk": { + "name": "scenario_truths_scenario_id_scenarios_id_fk", + "tableFrom": "scenario_truths", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenarios": { + "name": "scenarios", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "synopsis": { + "name": "synopsis", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "briefing": { + "name": "briefing", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "floor_plan": { + "name": "floor_plan", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "time_start": { + "name": "time_start", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_end": { + "name": "time_end", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_name": { + "name": "victim_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_introduction": { + "name": "victim_introduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_at": { + "name": "victim_found_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_in": { + "name": "victim_found_in", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_investigable": { + "name": "victim_investigable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "victim_estimated_death_at": { + "name": "victim_estimated_death_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "places": { + "name": "places", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_published": { + "name": "is_published", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "estimated_minutes": { + "name": "estimated_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/db/migrations/meta/0028_snapshot.json b/db/migrations/meta/0028_snapshot.json new file mode 100644 index 0000000..66eab37 --- /dev/null +++ b/db/migrations/meta/0028_snapshot.json @@ -0,0 +1,1553 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "a4d4ac81-17e6-4235-972e-6a800b527a6d", + "prevId": "73124777-d458-4905-b666-c6e2b52506db", + "tables": { + "analytics_sessions": { + "name": "analytics_sessions", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "detective": { + "name": "detective", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max_turns": { + "name": "max_turns", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "questions_per_turn": { + "name": "questions_per_turn", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "exchanges_per_topic": { + "name": "exchanges_per_topic", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "culprit_character_id": { + "name": "culprit_character_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "culprit_correct": { + "name": "culprit_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "method_correct": { + "name": "method_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_correct": { + "name": "motive_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reasoning": { + "name": "reasoning", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive": { + "name": "motive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "method_comment": { + "name": "method_comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_comment": { + "name": "motive_comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "solved_seconds": { + "name": "solved_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "evidence_found": { + "name": "evidence_found", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "evidence_total": { + "name": "evidence_total", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "contradiction_count": { + "name": "contradiction_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accuracy_percent": { + "name": "accuracy_percent", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "analytics_sessions_scenario_id_idx": { + "name": "analytics_sessions_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "analytics_sessions_started_at_idx": { + "name": "analytics_sessions_started_at_idx", + "columns": [ + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "analytics_turns": { + "name": "analytics_turns", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_kind": { + "name": "subject_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "turn_index": { + "name": "turn_index", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "topic": { + "name": "topic", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "exchanges": { + "name": "exchanges", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revealed_evidence_ids": { + "name": "revealed_evidence_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revealed_revelation_ids": { + "name": "revealed_revelation_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "contradiction_pointed_out": { + "name": "contradiction_pointed_out", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "npc_lied": { + "name": "npc_lied", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "analytics_turns_session_id_idx": { + "name": "analytics_turns_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "analytics_turns_scenario_id_idx": { + "name": "analytics_turns_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "analytics_turns_created_at_idx": { + "name": "analytics_turns_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "characters": { + "name": "characters", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "short_name": { + "name": "short_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "age_group": { + "name": "age_group", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "gender": { + "name": "gender", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "voice_seed": { + "name": "voice_seed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "voice_caption": { + "name": "voice_caption", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_introduction": { + "name": "public_introduction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "personality": { + "name": "personality", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "knowledge": { + "name": "knowledge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "secrets": { + "name": "secrets", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "goals": { + "name": "goals", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lies": { + "name": "lies", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lie_refs": { + "name": "lie_refs", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "memories": { + "name": "memories", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "characters_scenario_id_idx": { + "name": "characters_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "characters_scenario_id_scenarios_id_fk": { + "name": "characters_scenario_id_scenarios_id_fk", + "tableFrom": "characters", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "discoveries": { + "name": "discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_id": { + "name": "evidence_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "discoveries_evidence_id_idx": { + "name": "discoveries_evidence_id_idx", + "columns": [ + "evidence_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "discoveries_session_id_play_sessions_id_fk": { + "name": "discoveries_session_id_play_sessions_id_fk", + "tableFrom": "discoveries", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "discoveries_evidence_id_evidences_id_fk": { + "name": "discoveries_evidence_id_evidences_id_fk", + "tableFrom": "discoveries", + "tableTo": "evidences", + "columnsFrom": [ + "evidence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "discoveries_session_id_evidence_id_pk": { + "columns": [ + "session_id", + "evidence_id" + ], + "name": "discoveries_session_id_evidence_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "evidences": { + "name": "evidences", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reveal_condition": { + "name": "reveal_condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "supports": { + "name": "supports", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "contradicts": { + "name": "contradicts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "reveals_death_time": { + "name": "reveals_death_time", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "evidences_scenario_id_idx": { + "name": "evidences_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "evidences_scenario_id_scenarios_id_fk": { + "name": "evidences_scenario_id_scenarios_id_fk", + "tableFrom": "evidences", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "llm_usages": { + "name": "llm_usages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "llm_usages_created_at_idx": { + "name": "llm_usages_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "llm_usages_session_id_idx": { + "name": "llm_usages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "character_id": { + "name": "character_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "messages_session_id_idx": { + "name": "messages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "messages_character_id_idx": { + "name": "messages_character_id_idx", + "columns": [ + "character_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "messages_session_id_play_sessions_id_fk": { + "name": "messages_session_id_play_sessions_id_fk", + "tableFrom": "messages", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "messages_character_id_characters_id_fk": { + "name": "messages_character_id_characters_id_fk", + "tableFrom": "messages", + "tableTo": "characters", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "play_sessions": { + "name": "play_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detective": { + "name": "detective", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "play_sessions_scenario_id_idx": { + "name": "play_sessions_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "play_sessions_started_at_idx": { + "name": "play_sessions_started_at_idx", + "columns": [ + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "play_sessions_scenario_id_scenarios_id_fk": { + "name": "play_sessions_scenario_id_scenarios_id_fk", + "tableFrom": "play_sessions", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reports": { + "name": "reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reported_at": { + "name": "reported_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "reports_scenario_id_idx": { + "name": "reports_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "reports_scenario_id_scenarios_id_fk": { + "name": "reports_scenario_id_scenarios_id_fk", + "tableFrom": "reports", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "results": { + "name": "results", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "solved_seconds": { + "name": "solved_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_found": { + "name": "evidence_found", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contradiction_count": { + "name": "contradiction_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accuracy_percent": { + "name": "accuracy_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method_correct": { + "name": "method_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_correct": { + "name": "motive_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deduction": { + "name": "deduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "results_session_id_play_sessions_id_fk": { + "name": "results_session_id_play_sessions_id_fk", + "tableFrom": "results", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelation_discoveries": { + "name": "revelation_discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revelation_id": { + "name": "revelation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "revelation_discoveries_revelation_id_idx": { + "name": "revelation_discoveries_revelation_id_idx", + "columns": [ + "revelation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelation_discoveries_session_id_play_sessions_id_fk": { + "name": "revelation_discoveries_session_id_play_sessions_id_fk", + "tableFrom": "revelation_discoveries", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "revelation_discoveries_revelation_id_revelations_id_fk": { + "name": "revelation_discoveries_revelation_id_revelations_id_fk", + "tableFrom": "revelation_discoveries", + "tableTo": "revelations", + "columnsFrom": [ + "revelation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "revelation_discoveries_session_id_revelation_id_pk": { + "columns": [ + "session_id", + "revelation_id" + ], + "name": "revelation_discoveries_session_id_revelation_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelations": { + "name": "revelations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "related_facts": { + "name": "related_facts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "revelations_scenario_id_idx": { + "name": "revelations_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelations_scenario_id_scenarios_id_fk": { + "name": "revelations_scenario_id_scenarios_id_fk", + "tableFrom": "revelations", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenario_truths": { + "name": "scenario_truths", + "columns": { + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "culprit_character_id": { + "name": "culprit_character_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truth": { + "name": "truth", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive": { + "name": "motive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timeline": { + "name": "timeline", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timeline_events": { + "name": "timeline_events", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "victim_cause_of_death": { + "name": "victim_cause_of_death", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_findings": { + "name": "victim_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "place_findings": { + "name": "place_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "secret_keywords": { + "name": "secret_keywords", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "scenario_truths_scenario_id_scenarios_id_fk": { + "name": "scenario_truths_scenario_id_scenarios_id_fk", + "tableFrom": "scenario_truths", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenarios": { + "name": "scenarios", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "synopsis": { + "name": "synopsis", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "briefing": { + "name": "briefing", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "floor_plan": { + "name": "floor_plan", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "time_start": { + "name": "time_start", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_end": { + "name": "time_end", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_name": { + "name": "victim_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_introduction": { + "name": "victim_introduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_at": { + "name": "victim_found_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_in": { + "name": "victim_found_in", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_investigable": { + "name": "victim_investigable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "victim_estimated_death_at": { + "name": "victim_estimated_death_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "places": { + "name": "places", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_published": { + "name": "is_published", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "estimated_minutes": { + "name": "estimated_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/db/migrations/meta/0029_snapshot.json b/db/migrations/meta/0029_snapshot.json new file mode 100644 index 0000000..5a4c27f --- /dev/null +++ b/db/migrations/meta/0029_snapshot.json @@ -0,0 +1,1593 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "1a38c7d6-fe72-48c7-823b-ef2fda11faca", + "prevId": "a4d4ac81-17e6-4235-972e-6a800b527a6d", + "tables": { + "analytics_sessions": { + "name": "analytics_sessions", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "detective": { + "name": "detective", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max_turns": { + "name": "max_turns", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "questions_per_turn": { + "name": "questions_per_turn", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "exchanges_per_topic": { + "name": "exchanges_per_topic", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "culprit_character_id": { + "name": "culprit_character_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "culprit_correct": { + "name": "culprit_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "method_correct": { + "name": "method_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_correct": { + "name": "motive_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reasoning": { + "name": "reasoning", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive": { + "name": "motive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "method_comment": { + "name": "method_comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_comment": { + "name": "motive_comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "solved_seconds": { + "name": "solved_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "evidence_found": { + "name": "evidence_found", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "evidence_total": { + "name": "evidence_total", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "contradiction_count": { + "name": "contradiction_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accuracy_percent": { + "name": "accuracy_percent", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "analytics_sessions_scenario_id_idx": { + "name": "analytics_sessions_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "analytics_sessions_started_at_idx": { + "name": "analytics_sessions_started_at_idx", + "columns": [ + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "analytics_turns": { + "name": "analytics_turns", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_kind": { + "name": "subject_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "turn_index": { + "name": "turn_index", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "topic": { + "name": "topic", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "exchanges": { + "name": "exchanges", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revealed_evidence_ids": { + "name": "revealed_evidence_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revealed_revelation_ids": { + "name": "revealed_revelation_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "contradiction_pointed_out": { + "name": "contradiction_pointed_out", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "npc_lied": { + "name": "npc_lied", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "analytics_turns_session_id_idx": { + "name": "analytics_turns_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "analytics_turns_scenario_id_idx": { + "name": "analytics_turns_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "analytics_turns_created_at_idx": { + "name": "analytics_turns_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "characters": { + "name": "characters", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "short_name": { + "name": "short_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "age_group": { + "name": "age_group", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "gender": { + "name": "gender", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "voice_seed": { + "name": "voice_seed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "voice_caption": { + "name": "voice_caption", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_introduction": { + "name": "public_introduction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "personality": { + "name": "personality", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "knowledge": { + "name": "knowledge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "secrets": { + "name": "secrets", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "goals": { + "name": "goals", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lies": { + "name": "lies", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lie_refs": { + "name": "lie_refs", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "memories": { + "name": "memories", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "characters_scenario_id_idx": { + "name": "characters_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "characters_scenario_id_scenarios_id_fk": { + "name": "characters_scenario_id_scenarios_id_fk", + "tableFrom": "characters", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "discoveries": { + "name": "discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_id": { + "name": "evidence_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "discoveries_evidence_id_idx": { + "name": "discoveries_evidence_id_idx", + "columns": [ + "evidence_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "discoveries_session_id_play_sessions_id_fk": { + "name": "discoveries_session_id_play_sessions_id_fk", + "tableFrom": "discoveries", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "discoveries_evidence_id_evidences_id_fk": { + "name": "discoveries_evidence_id_evidences_id_fk", + "tableFrom": "discoveries", + "tableTo": "evidences", + "columnsFrom": [ + "evidence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "discoveries_session_id_evidence_id_pk": { + "columns": [ + "session_id", + "evidence_id" + ], + "name": "discoveries_session_id_evidence_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "evidences": { + "name": "evidences", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reveal_condition": { + "name": "reveal_condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "supports": { + "name": "supports", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "contradicts": { + "name": "contradicts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "reveals_death_time": { + "name": "reveals_death_time", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "evidences_scenario_id_idx": { + "name": "evidences_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "evidences_scenario_id_scenarios_id_fk": { + "name": "evidences_scenario_id_scenarios_id_fk", + "tableFrom": "evidences", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "llm_usages": { + "name": "llm_usages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "llm_usages_created_at_idx": { + "name": "llm_usages_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "llm_usages_session_id_idx": { + "name": "llm_usages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "character_id": { + "name": "character_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "messages_session_id_idx": { + "name": "messages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "messages_character_id_idx": { + "name": "messages_character_id_idx", + "columns": [ + "character_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "messages_session_id_play_sessions_id_fk": { + "name": "messages_session_id_play_sessions_id_fk", + "tableFrom": "messages", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "messages_character_id_characters_id_fk": { + "name": "messages_character_id_characters_id_fk", + "tableFrom": "messages", + "tableTo": "characters", + "columnsFrom": [ + "character_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "play_sessions": { + "name": "play_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detective": { + "name": "detective", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "play_sessions_scenario_id_idx": { + "name": "play_sessions_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "play_sessions_started_at_idx": { + "name": "play_sessions_started_at_idx", + "columns": [ + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "play_sessions_scenario_id_scenarios_id_fk": { + "name": "play_sessions_scenario_id_scenarios_id_fk", + "tableFrom": "play_sessions", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reports": { + "name": "reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reported_at": { + "name": "reported_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "reports_scenario_id_idx": { + "name": "reports_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "reports_scenario_id_scenarios_id_fk": { + "name": "reports_scenario_id_scenarios_id_fk", + "tableFrom": "reports", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "results": { + "name": "results", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "solved_seconds": { + "name": "solved_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_found": { + "name": "evidence_found", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contradiction_count": { + "name": "contradiction_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accuracy_percent": { + "name": "accuracy_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method_correct": { + "name": "method_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_correct": { + "name": "motive_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deduction": { + "name": "deduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "results_session_id_play_sessions_id_fk": { + "name": "results_session_id_play_sessions_id_fk", + "tableFrom": "results", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelation_discoveries": { + "name": "revelation_discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revelation_id": { + "name": "revelation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "revelation_discoveries_revelation_id_idx": { + "name": "revelation_discoveries_revelation_id_idx", + "columns": [ + "revelation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelation_discoveries_session_id_play_sessions_id_fk": { + "name": "revelation_discoveries_session_id_play_sessions_id_fk", + "tableFrom": "revelation_discoveries", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "revelation_discoveries_revelation_id_revelations_id_fk": { + "name": "revelation_discoveries_revelation_id_revelations_id_fk", + "tableFrom": "revelation_discoveries", + "tableTo": "revelations", + "columnsFrom": [ + "revelation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "revelation_discoveries_session_id_revelation_id_pk": { + "columns": [ + "session_id", + "revelation_id" + ], + "name": "revelation_discoveries_session_id_revelation_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelations": { + "name": "revelations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "related_facts": { + "name": "related_facts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "revelations_scenario_id_idx": { + "name": "revelations_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelations_scenario_id_scenarios_id_fk": { + "name": "revelations_scenario_id_scenarios_id_fk", + "tableFrom": "revelations", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenario_truths": { + "name": "scenario_truths", + "columns": { + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "culprit_character_id": { + "name": "culprit_character_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truth": { + "name": "truth", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive": { + "name": "motive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timeline": { + "name": "timeline", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timeline_events": { + "name": "timeline_events", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "victim_cause_of_death": { + "name": "victim_cause_of_death", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_findings": { + "name": "victim_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "place_findings": { + "name": "place_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "secret_keywords": { + "name": "secret_keywords", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "scenario_truths_scenario_id_scenarios_id_fk": { + "name": "scenario_truths_scenario_id_scenarios_id_fk", + "tableFrom": "scenario_truths", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenarios": { + "name": "scenarios", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "synopsis": { + "name": "synopsis", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "briefing": { + "name": "briefing", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "floor_plan": { + "name": "floor_plan", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "time_start": { + "name": "time_start", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_end": { + "name": "time_end", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_name": { + "name": "victim_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_introduction": { + "name": "victim_introduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_at": { + "name": "victim_found_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_in": { + "name": "victim_found_in", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_investigable": { + "name": "victim_investigable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "victim_estimated_death_at": { + "name": "victim_estimated_death_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "places": { + "name": "places", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_published": { + "name": "is_published", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "estimated_minutes": { + "name": "estimated_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tts_speakers": { + "name": "tts_speakers", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "age_group": { + "name": "age_group", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "gender": { + "name": "gender", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/db/migrations/meta/0030_snapshot.json b/db/migrations/meta/0030_snapshot.json new file mode 100644 index 0000000..c1262ed --- /dev/null +++ b/db/migrations/meta/0030_snapshot.json @@ -0,0 +1,1580 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "6e6c8bca-ef92-4a53-8cbb-793a4c0c0a9c", + "prevId": "1a38c7d6-fe72-48c7-823b-ef2fda11faca", + "tables": { + "analytics_sessions": { + "name": "analytics_sessions", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "detective": { + "name": "detective", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max_turns": { + "name": "max_turns", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "questions_per_turn": { + "name": "questions_per_turn", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "exchanges_per_topic": { + "name": "exchanges_per_topic", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "culprit_character_id": { + "name": "culprit_character_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "culprit_correct": { + "name": "culprit_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "method_correct": { + "name": "method_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_correct": { + "name": "motive_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reasoning": { + "name": "reasoning", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive": { + "name": "motive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "method_comment": { + "name": "method_comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_comment": { + "name": "motive_comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "solved_seconds": { + "name": "solved_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "evidence_found": { + "name": "evidence_found", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "evidence_total": { + "name": "evidence_total", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "contradiction_count": { + "name": "contradiction_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accuracy_percent": { + "name": "accuracy_percent", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "analytics_sessions_scenario_id_idx": { + "name": "analytics_sessions_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "analytics_sessions_started_at_idx": { + "name": "analytics_sessions_started_at_idx", + "columns": [ + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "analytics_turns": { + "name": "analytics_turns", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_kind": { + "name": "subject_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "turn_index": { + "name": "turn_index", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "topic": { + "name": "topic", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "exchanges": { + "name": "exchanges", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revealed_evidence_ids": { + "name": "revealed_evidence_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revealed_revelation_ids": { + "name": "revealed_revelation_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "contradiction_pointed_out": { + "name": "contradiction_pointed_out", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "npc_lied": { + "name": "npc_lied", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "analytics_turns_session_id_idx": { + "name": "analytics_turns_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "analytics_turns_scenario_id_idx": { + "name": "analytics_turns_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "analytics_turns_created_at_idx": { + "name": "analytics_turns_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "characters": { + "name": "characters", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "short_name": { + "name": "short_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "age_group": { + "name": "age_group", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "gender": { + "name": "gender", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "voice_seed": { + "name": "voice_seed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "voice_caption": { + "name": "voice_caption", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_introduction": { + "name": "public_introduction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "personality": { + "name": "personality", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "knowledge": { + "name": "knowledge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "secrets": { + "name": "secrets", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "goals": { + "name": "goals", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lies": { + "name": "lies", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lie_refs": { + "name": "lie_refs", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "memories": { + "name": "memories", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "characters_scenario_id_idx": { + "name": "characters_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "characters_scenario_id_scenarios_id_fk": { + "name": "characters_scenario_id_scenarios_id_fk", + "tableFrom": "characters", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "discoveries": { + "name": "discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_id": { + "name": "evidence_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "discoveries_evidence_id_idx": { + "name": "discoveries_evidence_id_idx", + "columns": [ + "evidence_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "discoveries_session_id_play_sessions_id_fk": { + "name": "discoveries_session_id_play_sessions_id_fk", + "tableFrom": "discoveries", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "discoveries_evidence_id_evidences_id_fk": { + "name": "discoveries_evidence_id_evidences_id_fk", + "tableFrom": "discoveries", + "tableTo": "evidences", + "columnsFrom": [ + "evidence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "discoveries_session_id_evidence_id_pk": { + "columns": [ + "session_id", + "evidence_id" + ], + "name": "discoveries_session_id_evidence_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "evidences": { + "name": "evidences", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reveal_condition": { + "name": "reveal_condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "supports": { + "name": "supports", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "contradicts": { + "name": "contradicts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "reveals_death_time": { + "name": "reveals_death_time", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "evidences_scenario_id_idx": { + "name": "evidences_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "evidences_scenario_id_scenarios_id_fk": { + "name": "evidences_scenario_id_scenarios_id_fk", + "tableFrom": "evidences", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "llm_usages": { + "name": "llm_usages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "llm_usages_created_at_idx": { + "name": "llm_usages_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "llm_usages_session_id_idx": { + "name": "llm_usages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "character_id": { + "name": "character_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "messages_session_id_idx": { + "name": "messages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "messages_character_id_idx": { + "name": "messages_character_id_idx", + "columns": [ + "character_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "messages_session_id_play_sessions_id_fk": { + "name": "messages_session_id_play_sessions_id_fk", + "tableFrom": "messages", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "play_sessions": { + "name": "play_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detective": { + "name": "detective", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "play_sessions_scenario_id_idx": { + "name": "play_sessions_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "play_sessions_started_at_idx": { + "name": "play_sessions_started_at_idx", + "columns": [ + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "play_sessions_scenario_id_scenarios_id_fk": { + "name": "play_sessions_scenario_id_scenarios_id_fk", + "tableFrom": "play_sessions", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reports": { + "name": "reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reported_at": { + "name": "reported_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "reports_scenario_id_idx": { + "name": "reports_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "reports_scenario_id_scenarios_id_fk": { + "name": "reports_scenario_id_scenarios_id_fk", + "tableFrom": "reports", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "results": { + "name": "results", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "solved_seconds": { + "name": "solved_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_found": { + "name": "evidence_found", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contradiction_count": { + "name": "contradiction_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accuracy_percent": { + "name": "accuracy_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method_correct": { + "name": "method_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_correct": { + "name": "motive_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deduction": { + "name": "deduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "results_session_id_play_sessions_id_fk": { + "name": "results_session_id_play_sessions_id_fk", + "tableFrom": "results", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelation_discoveries": { + "name": "revelation_discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revelation_id": { + "name": "revelation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "revelation_discoveries_revelation_id_idx": { + "name": "revelation_discoveries_revelation_id_idx", + "columns": [ + "revelation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelation_discoveries_session_id_play_sessions_id_fk": { + "name": "revelation_discoveries_session_id_play_sessions_id_fk", + "tableFrom": "revelation_discoveries", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "revelation_discoveries_revelation_id_revelations_id_fk": { + "name": "revelation_discoveries_revelation_id_revelations_id_fk", + "tableFrom": "revelation_discoveries", + "tableTo": "revelations", + "columnsFrom": [ + "revelation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "revelation_discoveries_session_id_revelation_id_pk": { + "columns": [ + "session_id", + "revelation_id" + ], + "name": "revelation_discoveries_session_id_revelation_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelations": { + "name": "revelations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "related_facts": { + "name": "related_facts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "revelations_scenario_id_idx": { + "name": "revelations_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelations_scenario_id_scenarios_id_fk": { + "name": "revelations_scenario_id_scenarios_id_fk", + "tableFrom": "revelations", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenario_truths": { + "name": "scenario_truths", + "columns": { + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "culprit_character_id": { + "name": "culprit_character_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truth": { + "name": "truth", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive": { + "name": "motive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timeline": { + "name": "timeline", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timeline_events": { + "name": "timeline_events", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "victim_cause_of_death": { + "name": "victim_cause_of_death", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_findings": { + "name": "victim_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "place_findings": { + "name": "place_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "secret_keywords": { + "name": "secret_keywords", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "scenario_truths_scenario_id_scenarios_id_fk": { + "name": "scenario_truths_scenario_id_scenarios_id_fk", + "tableFrom": "scenario_truths", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenarios": { + "name": "scenarios", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "synopsis": { + "name": "synopsis", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "briefing": { + "name": "briefing", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "floor_plan": { + "name": "floor_plan", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "time_start": { + "name": "time_start", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_end": { + "name": "time_end", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_name": { + "name": "victim_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_introduction": { + "name": "victim_introduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_at": { + "name": "victim_found_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_in": { + "name": "victim_found_in", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_investigable": { + "name": "victim_investigable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "victim_estimated_death_at": { + "name": "victim_estimated_death_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "places": { + "name": "places", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_published": { + "name": "is_published", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "estimated_minutes": { + "name": "estimated_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tts_speakers": { + "name": "tts_speakers", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "age_group": { + "name": "age_group", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "gender": { + "name": "gender", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/db/migrations/meta/0031_snapshot.json b/db/migrations/meta/0031_snapshot.json new file mode 100644 index 0000000..eee51bb --- /dev/null +++ b/db/migrations/meta/0031_snapshot.json @@ -0,0 +1,1587 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "1c206771-7d75-4b20-9e11-c0f901e0d636", + "prevId": "6e6c8bca-ef92-4a53-8cbb-793a4c0c0a9c", + "tables": { + "analytics_sessions": { + "name": "analytics_sessions", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "detective": { + "name": "detective", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "max_turns": { + "name": "max_turns", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "questions_per_turn": { + "name": "questions_per_turn", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "exchanges_per_topic": { + "name": "exchanges_per_topic", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "culprit_character_id": { + "name": "culprit_character_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "culprit_correct": { + "name": "culprit_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "method_correct": { + "name": "method_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_correct": { + "name": "motive_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reasoning": { + "name": "reasoning", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive": { + "name": "motive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "method_comment": { + "name": "method_comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_comment": { + "name": "motive_comment", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "solved_seconds": { + "name": "solved_seconds", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "evidence_found": { + "name": "evidence_found", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "evidence_total": { + "name": "evidence_total", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "contradiction_count": { + "name": "contradiction_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "accuracy_percent": { + "name": "accuracy_percent", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "analytics_sessions_scenario_id_idx": { + "name": "analytics_sessions_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "analytics_sessions_started_at_idx": { + "name": "analytics_sessions_started_at_idx", + "columns": [ + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "analytics_turns": { + "name": "analytics_turns", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_kind": { + "name": "subject_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "turn_index": { + "name": "turn_index", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "topic": { + "name": "topic", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "exchanges": { + "name": "exchanges", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revealed_evidence_ids": { + "name": "revealed_evidence_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "revealed_revelation_ids": { + "name": "revealed_revelation_ids", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "contradiction_pointed_out": { + "name": "contradiction_pointed_out", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "npc_lied": { + "name": "npc_lied", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "judge_tuning": { + "name": "judge_tuning", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "analytics_turns_session_id_idx": { + "name": "analytics_turns_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "analytics_turns_scenario_id_idx": { + "name": "analytics_turns_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "analytics_turns_created_at_idx": { + "name": "analytics_turns_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "characters": { + "name": "characters", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "short_name": { + "name": "short_name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "age_group": { + "name": "age_group", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "gender": { + "name": "gender", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "voice_seed": { + "name": "voice_seed", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "voice_caption": { + "name": "voice_caption", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "public_introduction": { + "name": "public_introduction", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "personality": { + "name": "personality", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "knowledge": { + "name": "knowledge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "secrets": { + "name": "secrets", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "goals": { + "name": "goals", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lies": { + "name": "lies", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lie_refs": { + "name": "lie_refs", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "memories": { + "name": "memories", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "characters_scenario_id_idx": { + "name": "characters_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "characters_scenario_id_scenarios_id_fk": { + "name": "characters_scenario_id_scenarios_id_fk", + "tableFrom": "characters", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "discoveries": { + "name": "discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_id": { + "name": "evidence_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "discoveries_evidence_id_idx": { + "name": "discoveries_evidence_id_idx", + "columns": [ + "evidence_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "discoveries_session_id_play_sessions_id_fk": { + "name": "discoveries_session_id_play_sessions_id_fk", + "tableFrom": "discoveries", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "discoveries_evidence_id_evidences_id_fk": { + "name": "discoveries_evidence_id_evidences_id_fk", + "tableFrom": "discoveries", + "tableTo": "evidences", + "columnsFrom": [ + "evidence_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "discoveries_session_id_evidence_id_pk": { + "columns": [ + "session_id", + "evidence_id" + ], + "name": "discoveries_session_id_evidence_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "evidences": { + "name": "evidences", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reveal_condition": { + "name": "reveal_condition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "supports": { + "name": "supports", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "contradicts": { + "name": "contradicts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "reveals_death_time": { + "name": "reveals_death_time", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "evidences_scenario_id_idx": { + "name": "evidences_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "evidences_scenario_id_scenarios_id_fk": { + "name": "evidences_scenario_id_scenarios_id_fk", + "tableFrom": "evidences", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "llm_usages": { + "name": "llm_usages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "reasoning_tokens": { + "name": "reasoning_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "llm_usages_created_at_idx": { + "name": "llm_usages_created_at_idx", + "columns": [ + "created_at" + ], + "isUnique": false + }, + "llm_usages_session_id_idx": { + "name": "llm_usages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "character_id": { + "name": "character_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "messages_session_id_idx": { + "name": "messages_session_id_idx", + "columns": [ + "session_id" + ], + "isUnique": false + }, + "messages_character_id_idx": { + "name": "messages_character_id_idx", + "columns": [ + "character_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "messages_session_id_play_sessions_id_fk": { + "name": "messages_session_id_play_sessions_id_fk", + "tableFrom": "messages", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "play_sessions": { + "name": "play_sessions", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "detective": { + "name": "detective", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "started_at": { + "name": "started_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "finished_at": { + "name": "finished_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "play_sessions_scenario_id_idx": { + "name": "play_sessions_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + }, + "play_sessions_started_at_idx": { + "name": "play_sessions_started_at_idx", + "columns": [ + "started_at" + ], + "isUnique": false + } + }, + "foreignKeys": { + "play_sessions_scenario_id_scenarios_id_fk": { + "name": "play_sessions_scenario_id_scenarios_id_fk", + "tableFrom": "play_sessions", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "reports": { + "name": "reports", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "reported_at": { + "name": "reported_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "reports_scenario_id_idx": { + "name": "reports_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "reports_scenario_id_scenarios_id_fk": { + "name": "reports_scenario_id_scenarios_id_fk", + "tableFrom": "reports", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "results": { + "name": "results", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "solved_seconds": { + "name": "solved_seconds", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "question_count": { + "name": "question_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "evidence_found": { + "name": "evidence_found", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "contradiction_count": { + "name": "contradiction_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "accuracy_percent": { + "name": "accuracy_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method_correct": { + "name": "method_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive_correct": { + "name": "motive_correct", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "deduction": { + "name": "deduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "results_session_id_play_sessions_id_fk": { + "name": "results_session_id_play_sessions_id_fk", + "tableFrom": "results", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelation_discoveries": { + "name": "revelation_discoveries", + "columns": { + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "revelation_id": { + "name": "revelation_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "discovered_at": { + "name": "discovered_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": { + "revelation_discoveries_revelation_id_idx": { + "name": "revelation_discoveries_revelation_id_idx", + "columns": [ + "revelation_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelation_discoveries_session_id_play_sessions_id_fk": { + "name": "revelation_discoveries_session_id_play_sessions_id_fk", + "tableFrom": "revelation_discoveries", + "tableTo": "play_sessions", + "columnsFrom": [ + "session_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "revelation_discoveries_revelation_id_revelations_id_fk": { + "name": "revelation_discoveries_revelation_id_revelations_id_fk", + "tableFrom": "revelation_discoveries", + "tableTo": "revelations", + "columnsFrom": [ + "revelation_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "revelation_discoveries_session_id_revelation_id_pk": { + "columns": [ + "session_id", + "revelation_id" + ], + "name": "revelation_discoveries_session_id_revelation_id_pk" + } + }, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "revelations": { + "name": "revelations", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_type": { + "name": "subject_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "subject_id": { + "name": "subject_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "sources": { + "name": "sources", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "related_facts": { + "name": "related_facts", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "revelations_scenario_id_idx": { + "name": "revelations_scenario_id_idx", + "columns": [ + "scenario_id" + ], + "isUnique": false + } + }, + "foreignKeys": { + "revelations_scenario_id_scenarios_id_fk": { + "name": "revelations_scenario_id_scenarios_id_fk", + "tableFrom": "revelations", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenario_truths": { + "name": "scenario_truths", + "columns": { + "scenario_id": { + "name": "scenario_id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "culprit_character_id": { + "name": "culprit_character_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "truth": { + "name": "truth", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "method": { + "name": "method", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "motive": { + "name": "motive", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "timeline": { + "name": "timeline", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timeline_events": { + "name": "timeline_events", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "victim_cause_of_death": { + "name": "victim_cause_of_death", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_findings": { + "name": "victim_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "place_findings": { + "name": "place_findings", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "secret_keywords": { + "name": "secret_keywords", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "scenario_truths_scenario_id_scenarios_id_fk": { + "name": "scenario_truths_scenario_id_scenarios_id_fk", + "tableFrom": "scenario_truths", + "tableTo": "scenarios", + "columnsFrom": [ + "scenario_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "scenarios": { + "name": "scenarios", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "synopsis": { + "name": "synopsis", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "briefing": { + "name": "briefing", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "floor_plan": { + "name": "floor_plan", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category": { + "name": "category", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "''" + }, + "time_start": { + "name": "time_start", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "time_end": { + "name": "time_end", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_name": { + "name": "victim_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_introduction": { + "name": "victim_introduction", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_at": { + "name": "victim_found_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_found_in": { + "name": "victim_found_in", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "victim_investigable": { + "name": "victim_investigable", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "victim_estimated_death_at": { + "name": "victim_estimated_death_at", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "places": { + "name": "places", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'[]'" + }, + "author_id": { + "name": "author_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_published": { + "name": "is_published", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "difficulty": { + "name": "difficulty", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 3 + }, + "estimated_minutes": { + "name": "estimated_minutes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 10 + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "tts_speakers": { + "name": "tts_speakers", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "age_group": { + "name": "age_group", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + }, + "gender": { + "name": "gender", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'unknown'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/db/migrations/meta/_journal.json b/db/migrations/meta/_journal.json index 482e363..bb85f5f 100644 --- a/db/migrations/meta/_journal.json +++ b/db/migrations/meta/_journal.json @@ -162,6 +162,62 @@ "when": 1788379031994, "tag": "0022_scenario-place-public-copy-safety", "breakpoints": true + }, + { + "idx": 23, + "version": "6", + "when": 1788790609383, + "tag": "0023_analytics-tables", + "breakpoints": true + }, + { + "idx": 24, + "version": "6", + "when": 1788830884765, + "tag": "0024_character-short-name", + "breakpoints": true + }, + { + "idx": 26, + "version": "6", + "when": 1788834040283, + "tag": "0026_character-age-gender", + "breakpoints": true + }, + { + "idx": 27, + "version": "6", + "when": 1788845199807, + "tag": "0027_character-voice", + "breakpoints": true + }, + { + "idx": 28, + "version": "6", + "when": 1788865661202, + "tag": "0028_llm-usage-drop-provider", + "breakpoints": true + }, + { + "idx": 29, + "version": "6", + "when": 1788937520736, + "tag": "0029_tts-speakers", + "breakpoints": true + }, + { + "idx": 30, + "version": "6", + "when": 1788940795359, + "tag": "0030_messages-drop-character-fk", + "breakpoints": true + }, + { + "idx": 31, + "version": "6", + "when": 1788975525922, + "tag": "0031_skinny_pet_avengers", + "breakpoints": true } ] } \ No newline at end of file diff --git a/db/person.ts b/db/person.ts new file mode 100644 index 0000000..b1d6f09 --- /dev/null +++ b/db/person.ts @@ -0,0 +1,67 @@ +import { z } from 'zod' + +/** + * 探偵と登場人物に共通する、人の年ごろと性別。 + * + * 自由記述にしないのは、この値がそのまま NPC のプロンプトに入るため。年齢を + * 自由記述にすると「28」「三十路」「アラサー」と書き方が割れ、呼びかけ方を + * 組み立てる側が文字列を解釈する羽目になる。選択肢に閉じておけば、年ごろと + * 性別から呼称と態度を確実に引ける。 + * + * 職業はここに置かない。「投影技師」「売店責任者」のように列挙に収まらず、 + * 登場人物の側では publicIntroduction と personality の文章が既にその役を + * 果たしている。探偵の側は常に探偵なので、列を持たせても同じ値が並ぶだけ。 + * + * このファイルが drizzle-orm を import していないのは意図的。 + * クライアントからも読むので、ORM をブラウザのバンドルへ持ち込みたくない + * (db/floor-plan.ts と同じ理由)。 + */ + +export const ageGroupSchema = z.enum([ + 'child', + 'teen', + 'young', + 'adult', + 'senior', + 'elder', + 'unknown', +]) + +export type AgeGroup = z.infer + +/** 画面の選択肢に並べる順。若い順に並べ、「不詳」を末尾に置く。 */ +export const AGE_GROUPS = ageGroupSchema.options + +export const AGE_GROUP_LABELS: Record = { + child: '子供', + teen: '十代', + young: '若者', + adult: '壮年', + senior: '初老', + elder: '老齢', + unknown: '年齢不詳', +} + +/** ラベルだけでは幅が広すぎるので、目安の年齢を添える。プロンプトにも同じ文を渡す。 */ +export const AGE_GROUP_NOTES: Record = { + child: '12歳ごろまで', + teen: '13〜19歳ごろ', + young: '20代', + adult: '30〜40代', + senior: '50〜60代', + elder: '70代以上', + unknown: '見た目からは測れない', +} + +export const genderSchema = z.enum(['male', 'female', 'other', 'unknown']) + +export type Gender = z.infer + +export const GENDERS = genderSchema.options + +export const GENDER_LABELS: Record = { + male: '男性', + female: '女性', + other: 'どちらでもない', + unknown: '明かさない', +} diff --git a/db/scenario-definition.ts b/db/scenario-definition.ts index 0b8e003..bdee003 100644 --- a/db/scenario-definition.ts +++ b/db/scenario-definition.ts @@ -1,5 +1,6 @@ import { z } from 'zod' import { floorPlanSchema } from './floor-plan' +import { ageGroupSchema, genderSchema } from './person' const scenarioIdSchema = z.string().regex(/^[a-z0-9][a-z0-9-]{2,63}$/) const localIdSchema = z.string().nonempty().max(100) @@ -194,6 +195,36 @@ export const scenarioRelationshipSchema = z.object({ export const scenarioCharacterSchema = z.object({ id: localIdSchema, name: nonemptyTextSchema.max(100), + /** + * 帯や名札のように幅の狭いところへ出す短い名前。姓だけ、渾名だけ。 + * + * 場所の `shortName` と同じ役目だが、こちらは省略できる。書かなければ `name` が + * そのまま入る(db/compile-scenario.ts)。姓を機械的に切り出す案は採らない + * ——一文字姓や外国名では切る位置が決まらないので、書く人が決める。 + */ + shortName: nonemptyTextSchema.max(8).optional(), + /** + * 年ごろと性別。探偵と同じ列挙を読む(db/person.ts)。 + * + * 既定が `unknown` なのは、この項目より前に書かれた yaml がそのまま通るようにするため。 + * 書かなければキャラクターシートにも出ないので、人物像の文章に任せたままにできる。 + * 職業の項目は無い。列挙に収まらず、publicIntroduction と personality が既にその役をしている。 + */ + ageGroup: ageGroupSchema.default('unknown'), + gender: genderSchema.default('unknown'), + /** + * Irodori-TTS へ渡す声の指定。どちらも任意で、書かなければ喋らない。 + * + * `voiceSeed` は合成のシード。固定しないと台詞ごとに別人の声になるので、 + * 声を与えるなら必ず要る(caption だけ書いた場合は下の superRefine が弾く)。 + * + * `voiceCaption` はそのキャラの声質を書いた一文。「落ち着いた女性の声で、 + * 近い距離感でやわらかく」のような自由記述で、モデルはこの日本語文から声を作る。 + * 場面ごとの感情はここに書かない——喋らせる側が実行時に足して繋ぐ。 + * ここに書くと、どの台詞も同じ感情で読まれる。 + */ + voiceSeed: z.int().min(0).optional(), + voiceCaption: nonemptyTextSchema.max(200).optional(), publicIntroduction: nonemptyTextSchema.max(300), personality: nonemptyTextSchema, goals: z.array(nonemptyTextSchema), @@ -549,6 +580,19 @@ export const ScenarioDefinitionSchema = scenarioDefinitionShapeSchema.superRefin }) } }) + + /* + シードの無い声は台詞ごとに別人になる。合成してみるまで気づけず、 + テストでも落ちないので、書き忘れをここで止める。 + */ + if (character.voiceCaption !== undefined && character.voiceSeed === undefined) { + ctx.addIssue({ + code: 'custom', + path: ['characters', characterIndex, 'voiceSeed'], + message: + 'voiceCaption を書いたら voiceSeed も必要です(固定しないと台詞ごとに声が変わります)。', + }) + } }) scenario.timeline.forEach((event, eventIndex) => { diff --git a/db/scenarios/avalanche-lodge-phantom-room.yaml b/db/scenarios/avalanche-lodge-phantom-room.yaml index 62db420..7ad489d 100644 --- a/db/scenarios/avalanche-lodge-phantom-room.yaml +++ b/db/scenarios/avalanche-lodge-phantom-room.yaml @@ -135,6 +135,8 @@ characters: - id: natsume name: 夏目悠 publicIntroduction: "丁寧で機転が利く夜勤責任者。" + ageGroup: adult + gender: male personality: 丁寧で機転が利く夜勤責任者。館内事情には誰より詳しいが、会計の話になると慎重に言葉を選ぶ。早瀬には長年世話になった一方、翌朝の監査を強く恐れていた。 goals: - 二一四号室を探していた時間を自分のアリバイとして成立させたい @@ -173,6 +175,8 @@ characters: - id: oda name: 小田真紀 publicIntroduction: "几帳面で、人の出入りをよく覚えている客室係。" + ageGroup: adult + gender: female personality: 几帳面で、人の出入りをよく覚えている客室係。自分の備品管理ミスを隠しているため、管理記録を調べられることには抵抗がある。 goals: - リネン破損の記録改ざんを隠したい @@ -196,6 +200,8 @@ characters: - id: fujino name: 藤野修平 publicIntroduction: "気さくで話好きな料理人。" + ageGroup: adult + gender: male personality: 気さくで話好きな料理人。細かな時刻には弱いが、厨房前で誰と話したかはよく覚えている。持ち帰ろうとした料理用ワインの件だけは触れられたくない。 goals: - ワインの持ち帰りを隠したい diff --git a/db/scenarios/avalanche-monastery-bell-window.yaml b/db/scenarios/avalanche-monastery-bell-window.yaml index 1e84994..8ab6a2d 100644 --- a/db/scenarios/avalanche-monastery-bell-window.yaml +++ b/db/scenarios/avalanche-monastery-bell-window.yaml @@ -132,6 +132,8 @@ characters: - id: mizuki name: 水城奈央 publicIntroduction: "修復設計士。" + ageGroup: adult + gender: female personality: 理屈が明快な修復設計士で、建物の構造と工事手順に詳しい。鐘は高瀬が鳴らしたという前提を強調し、自分だけが仮設試験線を熟知していることには触れたがらない。 goals: - 架空の追加工事請求を隠したい @@ -168,6 +170,8 @@ characters: - id: kurokawa name: 黒川玲 publicIntroduction: "文献には厳密だが建築設備には疎い研究者。" + ageGroup: adult + gender: female personality: 文献には厳密だが建築設備には疎い研究者。鐘を聞いたことは確かだが、音が鳴る仕組みまでは知らない。撮影禁止史料を写したことを隠している。 goals: - 禁止史料の無断撮影を隠したい @@ -194,6 +198,8 @@ characters: - id: genda name: 玄田修 publicIntroduction: "長年勤める管理人。" + ageGroup: senior + gender: male personality: 長年勤める管理人。鍵の扱いにはうるさいが、工事中の仮設設備には疎い。無断で予備鍵を作ったため、鍵の話題になると妙に防御的になる。 goals: - 無許可の予備鍵を隠したい diff --git a/db/scenarios/blizzard-lodge-seat-score-alibi.yaml b/db/scenarios/blizzard-lodge-seat-score-alibi.yaml index f6a50a0..c870a38 100644 --- a/db/scenarios/blizzard-lodge-seat-score-alibi.yaml +++ b/db/scenarios/blizzard-lodge-seat-score-alibi.yaml @@ -142,6 +142,8 @@ characters: - id: katase name: 片瀬真央 publicIntroduction: "話術に長けた企画担当。" + ageGroup: adult + gender: female personality: 話術に長けた企画担当。得点表に自分の名前があることを「ずっとその場にいた証明」として強調する。講師費の精算内容を詳しく聞かれると話題を変えたがる。 goals: - 架空の講師費を隠したい @@ -183,6 +185,8 @@ characters: - id: todo name: 藤堂凛 publicIntroduction: "言葉の細部をよく覚える講師。" + ageGroup: adult + gender: female personality: 言葉の細部をよく覚える講師。片瀬が戻ってきた場面は覚えているが、研修資料の無断転用を隠したいので自分への追及には敏感。 goals: - 資料の無断転用を隠したい @@ -212,6 +216,8 @@ characters: - id: yoshioka name: 吉岡蓮 publicIntroduction: "研修の事務を担当する職員。" + ageGroup: young + gender: male personality: 数字を整えて記録するのが好きな事務職員。得点表はラウンドごとに後から清書しており、誰が席にいたかまでは記録していない。私用交通費の混入を隠している。 goals: - 私用交通費の精算を隠したい @@ -241,6 +247,8 @@ characters: - id: mamiya name: 間宮葵 publicIntroduction: "頼まれると断れない設備係。" + ageGroup: young + gender: female personality: 頼まれると断れない設備係。片瀬に少しだけ代わってと言われ、深く考えず青席へ座った。空き客室を私物置き場に使っていることを隠したい。 goals: - 空き客室の私的利用を隠したい diff --git a/db/scenarios/cave-lab-locator-cart.yaml b/db/scenarios/cave-lab-locator-cart.yaml index 700ab96..fcaccf6 100644 --- a/db/scenarios/cave-lab-locator-cart.yaml +++ b/db/scenarios/cave-lab-locator-cart.yaml @@ -159,6 +159,8 @@ characters: - id: kagawa name: 香川紗英 publicIntroduction: "洞窟測量を担当する技術者。" + ageGroup: adult + gender: female personality: 位置情報と測量値を絶対視する技術者。数字で説明できることには強気だが、成果の正確さを疑われると攻撃的になる。位置タグの連続軌跡を自分の在席証明として繰り返し示す。 goals: - 試料採取地点の記録を偽ったことを隠したい @@ -201,6 +203,8 @@ characters: - id: shindo name: 新堂匠 publicIntroduction: "研究成果への競争心が強い地質学者。" + ageGroup: adult + gender: male personality: 研究成果への競争心が強い地質学者。共同試料を個人ケースへ移したことを隠すため保管区画の話を避けるが、位置タグの仕組みには詳しくない。 goals: - 共同試料を個人研究用に移したことを隠したい @@ -224,6 +228,8 @@ characters: - id: yuki name: 結城真 publicIntroduction: "実務優先の設備担当。" + ageGroup: adult + gender: male personality: 実務優先の設備担当。換気センサーを無効化した手順違反を隠したいが、安全用位置タグの受信設備も保守しており、タグが人そのものを認識しているわけではないと知っている。 goals: - 換気センサーの手順外無効化を隠したい @@ -244,6 +250,8 @@ characters: - id: tono name: 遠野澪 publicIntroduction: "調査記録を担当する職員。" + ageGroup: young + gender: female personality: 観察した順番を細かく記憶する記録担当。夕方の巡回時刻を書き直したことを隠したいが、21時07分の連絡通路で見た香川の姿は鮮明に覚えている。 goals: - 夕方の巡回時刻を後から修正したことを隠したい diff --git a/db/scenarios/coldcase-lodge-borrowed-memory.yaml b/db/scenarios/coldcase-lodge-borrowed-memory.yaml index 833fccf..e9e0036 100644 --- a/db/scenarios/coldcase-lodge-borrowed-memory.yaml +++ b/db/scenarios/coldcase-lodge-borrowed-memory.yaml @@ -131,6 +131,8 @@ characters: - id: maki name: 高瀬真紀 publicIntroduction: "2026年現在は穏やかな元教師。" + ageGroup: senior + gender: female personality: 2026年現在は穏やかな元教師。47年間「22時30分に暖炉前で野上を見た」と繰り返してきたため、本人にもそれが固い記憶になっている。だが事件当時の現金窃盗は今でも恥じている。 goals: - 1979年の現金窃盗を隠したい @@ -163,6 +165,8 @@ characters: - id: tatsuya name: 藤村達也 publicIntroduction: "2026年現在は慎重な会社員OB。" + ageGroup: senior + gender: male personality: 2026年現在は慎重な会社員OB。今は自分も暖炉前の野上を見た気がしているが、古い供述を読むと自信を失う。若いころの秘密の交際だけは話したくない。 goals: - 当時の秘密の交際を隠したい @@ -191,6 +195,8 @@ characters: - id: megumi name: 倉田恵 publicIntroduction: "2026年現在は物静かな元経理職。" + ageGroup: senior + gender: female personality: 2026年現在は物静かな元経理職。事件当夜は22時15分から眠っていたという最初の供述を強調する。仕入れ代の水増しを問われると防御的になる。 goals: - 仕入れ代の水増しを隠したい diff --git a/db/scenarios/deepsea-habitat-batch-signature.yaml b/db/scenarios/deepsea-habitat-batch-signature.yaml index c75e4d5..cc70e09 100644 --- a/db/scenarios/deepsea-habitat-batch-signature.yaml +++ b/db/scenarios/deepsea-habitat-batch-signature.yaml @@ -140,6 +140,8 @@ characters: - id: sagisawa name: 鷺沢怜 publicIntroduction: "生命維持設備を担当する技術者。" + ageGroup: adult + gender: female personality: 数字と手順を盾にする生命維持技術者。異常時でも声を荒らげないが、自分の整備判断を否定されると頑なになる。点検画面に署名が並んでいることを強調する。 goals: - 点検記録の改ざんを隠したい @@ -179,6 +181,8 @@ characters: - id: narumi name: 鳴海俊 publicIntroduction: "好奇心旺盛で研究成果への執着が強い地質学者。" + ageGroup: adult + gender: male personality: 好奇心旺盛で研究成果への執着が強い地質学者。規約違反の試料取り置きを知られたくないため試料庫の話を避ける。 goals: - 個人研究用に試料を取り置いたことを隠したい @@ -199,6 +203,8 @@ characters: - id: kohinata name: 小日向茜 publicIntroduction: "通信担当者。" + ageGroup: adult + gender: female personality: 時刻と通信記録に几帳面な担当者。自分の手順ミスを消したことには後ろめたさがあるが、中央通路で見た人物と時計表示は正確に覚えている。 goals: - 支援船との通信一件を削除したことを隠したい @@ -219,6 +225,8 @@ characters: - id: toki name: 土岐誠 publicIntroduction: "経験則を重んじる機械整備担当。" + ageGroup: senior + gender: male personality: 経験則を重んじる機械整備担当。期限切れ設備への応急処置を隠すが、点検システムの画面仕様は保守教育で知っている。 goals: - 昇降機構を手順外で使い続けていたことを隠したい diff --git a/db/scenarios/ferry-fog-passenger-count.yaml b/db/scenarios/ferry-fog-passenger-count.yaml index b169118..5529198 100644 --- a/db/scenarios/ferry-fog-passenger-count.yaml +++ b/db/scenarios/ferry-fog-passenger-count.yaml @@ -137,6 +137,8 @@ characters: - id: kanda name: 神田美奈 publicIntroduction: "手際がよく、乗客対応にも慣れた事務長。" + ageGroup: adult + gender: female personality: 手際がよく、乗客対応にも慣れた事務長。数字と帳簿を扱う仕事に自信を持ち、記録さえ整っていれば説明は通ると考えがち。柴田の監査には以前から強い緊張を感じていた。 goals: - 船内売上金の抜き取りを隠したい @@ -176,6 +178,8 @@ characters: - id: ogiwara name: 荻原陸 publicIntroduction: "明るく人懐っこい若い甲板員。" + ageGroup: young + gender: male personality: 明るく人懐っこい若い甲板員。乗客へのサービス精神が強すぎて、出航前に知人を乗員通路へ入れた規則違反を隠したい。船内で見た人の移動はよく覚えている。 goals: - 知人を乗員通路へ入れたことを隠したい @@ -199,6 +203,8 @@ characters: - id: fujiwara name: 藤原奈緒 publicIntroduction: "粘り強い地方紙記者。" + ageGroup: adult + gender: female personality: 粘り強い地方紙記者。情報源を守る意識が強く、柴田との秘密の面会を簡単には認めない。会社の安全管理問題を追っていたため、監査役との接触自体が取材先へ漏れることを恐れている。 goals: - 柴田から内部資料を受け取ったことを隠したい diff --git a/db/scenarios/festival-lantern-blackout.yaml b/db/scenarios/festival-lantern-blackout.yaml index 15a6e1b..deb95d7 100644 --- a/db/scenarios/festival-lantern-blackout.yaml +++ b/db/scenarios/festival-lantern-blackout.yaml @@ -145,6 +145,8 @@ characters: - id: tozuka name: 戸塚岳 publicIntroduction: "現場慣れした電気技師で、トラブル時ほど落ち着いて話す。" + ageGroup: adult + gender: male personality: 現場慣れした電気技師で、トラブル時ほど落ち着いて話す。専門知識への自負が強く、素人には設備の細部まで分からないと思っている。神谷とは発注費を巡って関係が悪化していた。 goals: - 発注額の水増しと謝礼の受領を隠したい @@ -178,6 +180,8 @@ characters: - id: makabe name: 真壁聡 publicIntroduction: "真面目で融通の利かない禰宜。" + ageGroup: adult + gender: male personality: 真面目で融通の利かない禰宜。神社を守る責任感は強いが、修繕費の不足から寄付金を一時流用したことを深く後悔している。鍵の管理について聞かれると必要以上に神経質になる。 goals: - 寄付金の流用を隠したい @@ -200,6 +204,8 @@ characters: - id: aihara name: 相原夏帆 publicIntroduction: "活発で世話焼きな屋台会のまとめ役。" + ageGroup: adult + gender: female personality: 活発で世話焼きな屋台会のまとめ役。祭りの成功を優先するあまり、申請前の知人を出店させた後ろめたさがある。暗闇で見た人物について断定するのをためらうが、服装と歩き方には自信がある。 goals: - 無許可に近い状態で知人の屋台を出したことを隠したい diff --git a/db/scenarios/flood-archive-self-locking-vault.yaml b/db/scenarios/flood-archive-self-locking-vault.yaml index c3b5a1e..865021c 100644 --- a/db/scenarios/flood-archive-self-locking-vault.yaml +++ b/db/scenarios/flood-archive-self-locking-vault.yaml @@ -163,6 +163,8 @@ characters: - id: yagami name: 八神琴子 publicIntroduction: "手仕事への誇りが高く、修復結果を批判されることに強く反発する修復士。" + ageGroup: adult + gender: female personality: 手仕事への誇りが高く、修復結果を批判されることに強く反発する修復士。鍵が今泉のポケットにあったことを何度も強調し、資料庫へ入れるはずがないという印象を作ろうとする。 goals: - 修復失敗と処置記録の改ざんを隠したい @@ -204,6 +206,8 @@ characters: - id: kuga name: 久我遼 publicIntroduction: "細かな動線を覚えている学芸員。" + ageGroup: young + gender: male personality: 細かな動線を覚えている学芸員。資料を傷めたことを隠したいが、21時14分ごろ八神が開いた資料庫へ入った場面ははっきり見ている。 goals: - 資料を傷めて報告しなかったことを隠したい @@ -224,6 +228,8 @@ characters: - id: tozuka name: 戸塚誠 publicIntroduction: "文書館の警備担当。" + ageGroup: adult + gender: male personality: 規則には詳しい警備担当。閲覧室のカメラ停止を隠したいが、希少資料庫の扉が閉まるだけで自動施錠される仕様は日常業務として知っている。 goals: - 閲覧室の監視カメラを私用のため止めたことを隠したい @@ -244,6 +250,8 @@ characters: - id: majima name: 真島香苗 publicIntroduction: "自信家の外部鑑定人。" + ageGroup: adult + gender: female personality: 自信家の外部鑑定人。来歴写真の差し替えを隠したいので今泉との鑑定上の衝突を小さく見せるが、21時台前半は別の資料室で作業していた。 goals: - 鑑定写真を差し替えたことを隠したい diff --git a/db/scenarios/flood-courthouse-unlogged-door.yaml b/db/scenarios/flood-courthouse-unlogged-door.yaml index a1168ae..b0df4b3 100644 --- a/db/scenarios/flood-courthouse-unlogged-door.yaml +++ b/db/scenarios/flood-courthouse-unlogged-door.yaml @@ -134,6 +134,8 @@ characters: - id: yabuki name: 矢吹梓 publicIntroduction: "裁判所の書記官。" + ageGroup: adult + gender: female personality: 論理的で、規則やシステムの仕様を根拠に話す書記官。記録上の証拠を重視するため、自分に有利なログだけを強調する傾向がある。 goals: - 解錠履歴がないことを、自分が保存記録室へ入っていない証明として通したい @@ -172,6 +174,8 @@ characters: - id: mitamura name: 三田村悟 publicIntroduction: "設備仕様には詳しいが、書類仕事が雑な技術職員。" + ageGroup: adult + gender: male personality: 設備仕様には詳しいが、書類仕事が雑な技術職員。点検の一項目を飛ばしたことを隠したい一方、電子錠が何を記録するかは正確に説明できる。 goals: - 点検項目を飛ばしたことを隠したい @@ -203,6 +207,8 @@ characters: - id: kise name: 木瀬亮 publicIntroduction: "寡黙な警備員。" + ageGroup: adult + gender: male personality: 寡黙な警備員。人の歩き方や服装をよく覚えているが、勤務中に古い事件資料を私的に読んでいたことを知られたくない。 goals: - 私的に事件資料を読んでいたことを隠したい diff --git a/db/scenarios/gallery-blue-frame.yaml b/db/scenarios/gallery-blue-frame.yaml index f6eb439..d06b91b 100644 --- a/db/scenarios/gallery-blue-frame.yaml +++ b/db/scenarios/gallery-blue-frame.yaml @@ -157,6 +157,8 @@ characters: - id: sakaki name: 榊玲 publicIntroduction: "物静かで職人気質の修復士。" + ageGroup: adult + gender: female personality: 物静かで職人気質の修復士。作品の状態については細部まで説明するが、自分の判断を疑われることを極端に嫌う。鳥羽には技術を評価されていた一方、管理の厳しさを窮屈に感じていた。 goals: - 作品のすり替えを隠したい @@ -192,6 +194,8 @@ characters: - id: mido name: 御堂和也 publicIntroduction: "洗練された物腰の画商。" + ageGroup: adult + gender: male personality: 洗練された物腰の画商。作品の価値と人間関係を同じように交渉材料として見る癖がある。鳥羽に不適切な提案を拒絶されたため、その面会自体を消したがっている。 goals: - 鳥羽へ謝礼を持ちかけたことを隠したい @@ -212,6 +216,8 @@ characters: - id: enomoto name: 榎本駿 publicIntroduction: "口数の少ない警備員。" + ageGroup: adult + gender: male personality: 口数の少ない警備員。巡回経路と時刻はよく覚えているが、短時間居眠りしたことを処分の対象にされるのを恐れている。見た人物を必要以上に断定しない慎重さがある。 goals: - 居眠りを隠したい diff --git a/db/scenarios/generation-ship-staggered-dawn.yaml b/db/scenarios/generation-ship-staggered-dawn.yaml index ceec302..639630a 100644 --- a/db/scenarios/generation-ship-staggered-dawn.yaml +++ b/db/scenarios/generation-ship-staggered-dawn.yaml @@ -126,6 +126,8 @@ characters: - id: sera name: セラ・イワノフ publicIntroduction: "有能な農業主任。" + ageGroup: adult + gender: female personality: 有能な農業主任。自分の区画時間で物事を話す癖があり、「六時過ぎには農業区にいた」と繰り返す。希少種子の管理を問われると強く反発する。 goals: - 希少種子の流用を隠したい @@ -169,6 +171,8 @@ characters: - id: dario name: ダリオ・ケイン publicIntroduction: "電力管制官。" + ageGroup: adult + gender: male personality: 船内標準時で考える電力管制官。区画時間を標準時へ換算するのが習慣になっている。個人工作室への電力流用は隠したい。 goals: - 個人工作室への規定外電力を隠したい @@ -204,6 +208,8 @@ characters: - id: yuna name: ユナ・パク publicIntroduction: "穏やかな医療技師。" + ageGroup: adult + gender: female personality: 穏やかな医療技師。患者の生活周期に合わせて医療区時間で話す。友人へ医療用品を融通した件は隠したい。 goals: - 手続き外の医療用品融通を隠したい diff --git a/db/scenarios/glasshouse-dawn-irrigation.yaml b/db/scenarios/glasshouse-dawn-irrigation.yaml index 9c1a666..4fd81a4 100644 --- a/db/scenarios/glasshouse-dawn-irrigation.yaml +++ b/db/scenarios/glasshouse-dawn-irrigation.yaml @@ -130,6 +130,8 @@ characters: - id: narahara name: 楢原彩 publicIntroduction: "植物園の研究助手。" + ageGroup: young + gender: female personality: 植物の扱いは繊細で、記録にも几帳面な研究助手。希少種への愛着は強いが、自分の持ち出しを「余った挿し穂の有効利用」と正当化している。作業手順を細かく話して信用を得ようとする。 goals: - 希少植物の無断販売を隠したい @@ -165,6 +167,8 @@ characters: - id: mizusawa name: 水沢浩司 publicIntroduction: "丁寧な言葉を崩さない企業担当者。" + ageGroup: adult + gender: male personality: 丁寧な言葉を崩さない企業担当者。寄付を交渉材料に使ったことを表へ出したくなく、木島との関係を必要以上に穏便だったように話す。 goals: - 命名権を巡って木島へ圧力をかけたことを隠したい @@ -185,6 +189,8 @@ characters: - id: onishi name: 大西蒼太 publicIntroduction: "朝が早い仕事に慣れた園芸員。" + ageGroup: young + gender: male personality: 朝が早い仕事に慣れた園芸員。設備の数値には正確だが、自分の残業申請の水増しが見つかるのを恐れている。人間関係の揉め事には首を突っ込みたくない。 goals: - 残業時間の水増し申請を隠したい diff --git a/db/scenarios/ink-stained-contract.yaml b/db/scenarios/ink-stained-contract.yaml index 74ce02c..180ef92 100644 --- a/db/scenarios/ink-stained-contract.yaml +++ b/db/scenarios/ink-stained-contract.yaml @@ -136,6 +136,8 @@ characters: - id: kawase name: 川瀬梨奈 publicIntroduction: "交渉上手で感情を表に出さない著作権エージェント。" + ageGroup: adult + gender: female personality: 交渉上手で感情を表に出さない著作権エージェント。契約書や入金記録の細部まで把握しているが、自分の管理する口座について尋ねられると話を一般論へそらす。石橋とは長く仕事をしてきた。 goals: - 海外版権収入の不正を隠したい @@ -171,6 +173,8 @@ characters: - id: fujimoto name: 藤本圭 publicIntroduction: "几帳面で締切に厳しい編集者。" + ageGroup: adult + gender: male personality: 几帳面で締切に厳しい編集者。話題作りのため未発表原稿を流したことを強く後悔しており、端末ログを詳しく調べられるのを嫌がる。人の出入りにはよく気づく。 goals: - 未発表原稿の漏洩を隠したい @@ -196,6 +200,8 @@ characters: - id: shido name: 志堂透 publicIntroduction: "評判を気にする中堅作家。" + ageGroup: adult + gender: male personality: 評判を気にする中堅作家。自尊心が高く、盗用疑惑を認めるくらいなら石橋との口論自体を隠したいと思っている。感情的にはなるが、事件後は露骨に怯えている。 goals: - 新作の盗用を隠したい diff --git a/db/scenarios/landslide-clock-museum-eleven-minutes.yaml b/db/scenarios/landslide-clock-museum-eleven-minutes.yaml index 114946e..23fbfbe 100644 --- a/db/scenarios/landslide-clock-museum-eleven-minutes.yaml +++ b/db/scenarios/landslide-clock-museum-eleven-minutes.yaml @@ -162,6 +162,8 @@ characters: - id: shiba name: 志波沙月 publicIntroduction: "古時計の修復技師。" + ageGroup: adult + gender: female personality: 古時計への知識と誇りが強い修復技師。細部をよく覚えているように話すが、時刻については館内の鐘を意図的に基準にする。倉橋に記録改ざんを見抜かれたことを隠したい。 goals: - 展示時計の来歴を偽ったことを隠したい @@ -202,6 +204,8 @@ characters: - id: asakura name: 朝倉真紀 publicIntroduction: "事務処理に強い受付責任者。" + ageGroup: adult + gender: female personality: 事務処理に強い受付責任者。売上金の集計ミスを隠したいので閉館後の帳簿作業を曖昧にするが、人とすれ違った順番はよく覚えている。時刻は大時計の鐘を信用していた。 goals: - 売上金不足が自分の集計ミスだったことを隠したい @@ -225,6 +229,8 @@ characters: - id: hoshina name: 保科悠人 publicIntroduction: "研究熱心だが承認欲求の強い若手学芸員。" + ageGroup: young + gender: male personality: 研究熱心だが承認欲求の強い若手学芸員。未公開写真を外部へ送ったことが露見するのを恐れ、収蔵庫周辺の行動を隠す。倉橋とは展示方針で衝突していた。 goals: - 未公開収蔵品の写真を外部へ送ったことを隠したい @@ -250,6 +256,8 @@ characters: - id: kido name: 城戸篤 publicIntroduction: "時計博物館の設備技術者。" + ageGroup: adult + gender: male personality: 機器の仕組みを平易に説明できる設備技術者。夕方の同期試験ミスを隠したいので最初は時計の精度に問題はなかったと言い張るが、防災端末と親時計が別系統であることは知っている。 goals: - 親時計を十一分進ませたままにした作業ミスを隠したい diff --git a/db/scenarios/landslide-hotel-frosted-silhouette.yaml b/db/scenarios/landslide-hotel-frosted-silhouette.yaml index fc4b9ea..bff2dab 100644 --- a/db/scenarios/landslide-hotel-frosted-silhouette.yaml +++ b/db/scenarios/landslide-hotel-frosted-silhouette.yaml @@ -128,6 +128,8 @@ characters: - id: ayase name: 綾瀬環 publicIntroduction: "愛想がよく館内の細かな要望にもすぐ対応するフロント主任。" + ageGroup: adult + gender: female personality: 愛想がよく館内の細かな要望にもすぐ対応するフロント主任。備品の位置や客の動線を熟知している。請求書の話になると説明を曖昧にし、21時半の人影を長峰本人だったと強く主張する。 goals: - 改装費の流用を隠したい @@ -166,6 +168,8 @@ characters: - id: kanda name: 神田理一 publicIntroduction: "ホテルに宿泊していた作家。" + ageGroup: senior + gender: male personality: 観察したことを文章にする癖がある作家だが、一度意味づけした光景を事実そのものと思い込みやすい。人影を見たことには自信があるが、顔も声も確認していない。 goals: - 無断で回想録を複写したことを隠したい @@ -192,6 +196,8 @@ characters: - id: hoshino name: 星野結 publicIntroduction: "光や構図に敏感な写真家。" + ageGroup: young + gender: female personality: 光や構図に敏感な写真家。物の位置が変わるとすぐ気づく。契約外の写真を売ろうとした後ろめたさがあり、その話題では歯切れが悪い。 goals: - 契約外写真の売却計画を隠したい diff --git a/db/scenarios/last-train-delay-certificate.yaml b/db/scenarios/last-train-delay-certificate.yaml index 25b22f2..ec116e5 100644 --- a/db/scenarios/last-train-delay-certificate.yaml +++ b/db/scenarios/last-train-delay-certificate.yaml @@ -130,6 +130,8 @@ characters: - id: arima name: 有馬結衣 publicIntroduction: "接客が丁寧で、遅延時にも落ち着いて対応できる助役。" + ageGroup: adult + gender: female personality: 接客が丁寧で、遅延時にも落ち着いて対応できる助役。記録と手順を重視する一方、少額の現金差なら見過ごされるという甘さがあった。藤崎には仕事を評価されていたため、不正を見抜かれたことへの動揺が大きい。 goals: - 返金処理を利用した現金抜き取りを隠したい @@ -165,6 +167,8 @@ characters: - id: komori name: 小森拓 publicIntroduction: "話好きな売店責任者で、ホームや改札の様子をよく見ている。" + ageGroup: adult + gender: male personality: 話好きな売店責任者で、ホームや改札の様子をよく見ている。廃棄商品の横流しが会社へ知られるのを恐れており、帳簿の話を向けられると急に口数が減る。 goals: - 廃棄商品の帳簿外販売を隠したい @@ -188,6 +192,8 @@ characters: - id: sawada name: 沢田亮 publicIntroduction: "機械好きで、古い鉄道設備を見ると仕事の範囲を越えて確認したくなる点検員。" + ageGroup: adult + gender: male personality: 機械好きで、古い鉄道設備を見ると仕事の範囲を越えて確認したくなる点検員。無許可で機器室へ入ったことを隠したいが、通路で会った人の時刻は点検時計と結びつけて覚えている。 goals: - 正式な指示なしに機器室へ入ったことを隠したい diff --git a/db/scenarios/mars-outpost-delayed-reply.yaml b/db/scenarios/mars-outpost-delayed-reply.yaml index dc80e38..3796126 100644 --- a/db/scenarios/mars-outpost-delayed-reply.yaml +++ b/db/scenarios/mars-outpost-delayed-reply.yaml @@ -120,6 +120,8 @@ characters: - id: noah name: ノア・チェン publicIntroduction: "火星基地の副主任。" + ageGroup: adult + gender: male personality: 冷静で数字に強い副主任。地球からの返信が21時38分に届いたことを強調し、それを自分が通信室にいた証明として扱わせようとする。予算の細部には神経質。 goals: - 予算流用を隠したい @@ -158,6 +160,8 @@ characters: - id: amira name: アミラ・サイード publicIntroduction: "火星基地の通信士。" + ageGroup: adult + gender: female personality: 理屈を重んじる通信士。送受信時刻と会話の因果関係を厳密に区別する。私的通信の帯域利用は隠したい。 goals: - 家族との規則外通信を隠したい @@ -184,6 +188,8 @@ characters: - id: luis name: ルイス・オルテガ publicIntroduction: "現場主義の整備主任。" + ageGroup: adult + gender: male personality: 現場主義の整備主任。規定より稼働を優先して無登録部品を使うことがある。通信には疎いが、廊下で見たノアについては自信がある。 goals: - 無登録部品の交換を隠したい diff --git a/db/scenarios/midnight-radio-rerun.yaml b/db/scenarios/midnight-radio-rerun.yaml index c9224e6..4436439 100644 --- a/db/scenarios/midnight-radio-rerun.yaml +++ b/db/scenarios/midnight-radio-rerun.yaml @@ -154,6 +154,8 @@ characters: - id: minobe name: 美濃部沙耶 publicIntroduction: "判断が速く、番組進行の乱れを何より嫌うプロデューサー。" + ageGroup: adult + gender: female personality: 判断が速く、番組進行の乱れを何より嫌うプロデューサー。普段は冷静だが、制作費や報告書の話になると妙に説明が細かくなる。大門とは長年組んできた。 goals: - 放送実績報告の不正を隠し通したい @@ -191,6 +193,8 @@ characters: - id: kuze name: 久世直人 publicIntroduction: "ラジオ局の音響技師。" + ageGroup: adult + gender: male personality: 機材の話になると饒舌だが、人間関係には深入りしたがらない音響技師。自分の小さな規則違反を大事にされるのを恐れている。 goals: - 私用録音をしていたことを隠したい @@ -223,6 +227,8 @@ characters: - id: natsume name: 夏目亮介 publicIntroduction: "愛想のよい営業担当だが、契約の話になると押しが強い。" + ageGroup: adult + gender: male personality: 愛想のよい営業担当だが、契約の話になると押しが強い。会社に知られたくない交渉を抱えており、自分が大門と揉めていた事実を必死に軽く見せようとする。 goals: - 大門へ個人的な謝礼を持ちかけたことを隠したい diff --git a/db/scenarios/polar-station-shared-clock.yaml b/db/scenarios/polar-station-shared-clock.yaml index 4a7e14e..9d5da8a 100644 --- a/db/scenarios/polar-station-shared-clock.yaml +++ b/db/scenarios/polar-station-shared-clock.yaml @@ -141,6 +141,8 @@ characters: - id: shinomiya name: 篠宮怜 publicIntroduction: "理詰めで話す観測員。" + ageGroup: adult + gender: female personality: 理詰めで話す観測員。記録が複数一致していることを強く主張し、個人の目撃よりシステムログを信用すべきだと言う。研究成果を失うことへの焦りが強い。 goals: - 四種類の時刻記録を独立したアリバイ証拠に見せたい @@ -181,6 +183,8 @@ characters: - id: higuchi name: 樋口海 publicIntroduction: "極地観測基地の通信担当。" + ageGroup: young + gender: male personality: 神経質な通信担当。システム障害を自分の失態と考え、最初は同期ずれを小さく見せようとする。技術仕様については正確に説明できる。 goals: - 同期障害の共有が遅れたことを隠したい @@ -215,6 +219,8 @@ characters: - id: sawatari name: 沢渡直人 publicIntroduction: "現場感覚を重視する整備員。" + ageGroup: adult + gender: male personality: 現場感覚を重視する整備員。基地システムとは別の携帯時計を使う習慣があり、自分の見た時刻に自信がある。私物機器の無断使用だけは隠したい。 goals: - 私物機器の無断使用を隠したい diff --git a/db/scenarios/quarantine-clinic-borrowed-badge.yaml b/db/scenarios/quarantine-clinic-borrowed-badge.yaml index 29cfa4c..d919c81 100644 --- a/db/scenarios/quarantine-clinic-borrowed-badge.yaml +++ b/db/scenarios/quarantine-clinic-borrowed-badge.yaml @@ -126,6 +126,8 @@ characters: - id: sera name: 世良美冬 publicIntroduction: "冷静で判断が早い看護主任。" + ageGroup: adult + gender: female personality: 冷静で判断が早い看護主任。隔離手順を熟知し、自分の説明には必ず記録上の根拠を添える。業務効率のために手順を独断変更したことを星名に問題視されていた。 goals: - 21時18分の橙色バッジ記録を星名の生存証明として扱わせたい @@ -164,6 +166,8 @@ characters: - id: kuga name: 久我夏樹 publicIntroduction: "診療所の検査技師。" + ageGroup: young + gender: male personality: 技術には詳しいが規則にはやや無頓着な検査技師。自分が管理バッジを借りたことを重大な規則違反だと思い込み、最初は隠そうとする。 goals: - 星名の管理バッジを借りたことを隠したい @@ -197,6 +201,8 @@ characters: - id: aihara name: 相原誠 publicIntroduction: "診療所の警備担当。" + ageGroup: adult + gender: male personality: 真面目な警備員で、制服やバッジの色を人物識別に使う癖がある。防護服姿の顔は見分けられなかったことを認める一方、私用電話で席を外したことは隠したい。 goals: - 監視席を離れたことを隠したい diff --git a/db/scenarios/rainy-bookstore-receipt.yaml b/db/scenarios/rainy-bookstore-receipt.yaml index fb8952d..30e52d3 100644 --- a/db/scenarios/rainy-bookstore-receipt.yaml +++ b/db/scenarios/rainy-bookstore-receipt.yaml @@ -183,6 +183,9 @@ timeline: characters: - id: makino name: 牧野千尋 + shortName: 牧野 + ageGroup: young + gender: female publicIntroduction: "丁寧で几帳面な古書店員。" personality: 丁寧で几帳面な古書店員。書誌や発送手順には強いが、金銭の話を向けられると防御的になる。水野には仕事を教わった恩がある一方、給料の低さには不満を抱えていた。 goals: @@ -214,6 +217,9 @@ characters: attitude: 値切り方が強引で苦手 - id: kuroda name: 黒田征司 + shortName: 黒田 + ageGroup: senior + gender: male publicIntroduction: "希少本の知識を誇る収集家。" personality: 希少本の知識を誇る収集家。金で解決できると思いがちで、体面を傷つけられることを嫌う。事件そのものより、裏取引の提案が会社へ知られることを恐れている。 goals: @@ -244,6 +250,9 @@ characters: attitude: 本を見る目は信用している - id: sena name: 瀬名真琴 + shortName: 瀬名 + ageGroup: adult + gender: unknown publicIntroduction: "気さくな喫茶店主で、商店街の人間関係には詳しい。" personality: 気さくな喫茶店主で、商店街の人間関係には詳しい。借金のことだけは強い引け目があり、水野との金銭関係を聞かれると曖昧になる。 goals: diff --git a/db/scenarios/shanghai-warehouse-carbon-copy.yaml b/db/scenarios/shanghai-warehouse-carbon-copy.yaml index b0d453c..cafb589 100644 --- a/db/scenarios/shanghai-warehouse-carbon-copy.yaml +++ b/db/scenarios/shanghai-warehouse-carbon-copy.yaml @@ -115,6 +115,8 @@ characters: - id: lin name: 林雪梅 publicIntroduction: "冷静で計算高い番頭。" + ageGroup: adult + gender: female personality: 冷静で計算高い番頭。書類の扱いに慣れており、署名入り伝票を強い生存証明として押し出そうとする。帳簿外の荷について問われると警戒する。 goals: - 帳簿外の絹荷を隠したい @@ -151,6 +153,8 @@ characters: - id: chen name: 陳伯安 publicIntroduction: "細部に強い通関書記。" + ageGroup: adult + gender: male personality: 細部に強い通関書記。紙の筆圧や控えの違いに敏感だが、自分の不正な謝礼については口を濁す。 goals: - 役人への不正な謝礼を隠したい @@ -179,6 +183,8 @@ characters: - id: wang name: 王世傑 publicIntroduction: "豪胆な荷役監督。" + ageGroup: adult + gender: male personality: 豪胆な荷役監督。細かな帳簿は苦手だが、物品の状態には目が利く。破損品の持ち出しだけは隠したい。 goals: - 破損品の持ち出しを隠したい diff --git a/db/scenarios/snow-gallery-erased-tracks.yaml b/db/scenarios/snow-gallery-erased-tracks.yaml index 7a003c9..d6c552f 100644 --- a/db/scenarios/snow-gallery-erased-tracks.yaml +++ b/db/scenarios/snow-gallery-erased-tracks.yaml @@ -125,6 +125,8 @@ characters: - id: kurata name: 倉田真帆 publicIntroduction: "彫刻館の学芸員。" + ageGroup: adult + gender: female personality: 落ち着いた学芸員で、展示記録や時系列を細かく説明する。雪面の状態を物理的証拠として強調する一方、来歴記録の問題を青沼に見つけられていた。 goals: - 発見時に足跡がないことを、20時10分以降誰も離れへ行っていない証拠として通したい @@ -159,6 +161,8 @@ characters: - id: anzai name: 安西雄 publicIntroduction: "彫刻館の施設員。" + ageGroup: senior + gender: male personality: 無愛想だが作業時刻には几帳面な施設員。雪害センサーを壊した失敗を隠したい。通路を掃いたことで、発見時の雪面が犯行時の雪面ではないことを知っている。 goals: - 雪害センサーの破損を隠したい @@ -184,6 +188,8 @@ characters: - id: enami name: 江波涼 publicIntroduction: "感覚派の彫刻家。" + ageGroup: adult + gender: male personality: 感覚派の彫刻家。時刻には曖昧だが展示室の状態にはよく気づく。試作品の破損を隠しているため、青沼との口論を知られたくない。 goals: - 試作品を壊したことを隠したい diff --git a/db/scenarios/snow-observatory-last-photo.yaml b/db/scenarios/snow-observatory-last-photo.yaml index d85d356..fcfa67b 100644 --- a/db/scenarios/snow-observatory-last-photo.yaml +++ b/db/scenarios/snow-observatory-last-photo.yaml @@ -135,6 +135,8 @@ characters: - id: hiyama name: 日山澪 publicIntroduction: "寡黙で観測に没頭する若い研究助手。" + ageGroup: young + gender: female personality: 寡黙で観測に没頭する若い研究助手。数字には強い自信を持つが、研究成果を失うことへの恐怖が強い。神崎を師として尊敬していた分、見放されたと思うと感情が揺れる。 goals: - 観測データの不正補正を隠したい @@ -174,6 +176,8 @@ characters: - id: kurose name: 黒瀬俊介 publicIntroduction: "自信家で競争心の強い研究者。" + ageGroup: adult + gender: male personality: 自信家で競争心の強い研究者。神崎とは研究方針でしばしば衝突する。未公開データを無断で持ち出そうとしたため、事件と無関係でも解析室への出入りを隠したい。 goals: - 未公開データを無断コピーしたことを隠したい @@ -199,6 +203,8 @@ characters: - id: muroi name: 室井邦彦 publicIntroduction: "無口で実務的な施設管理員。" + ageGroup: senior + gender: male personality: 無口で実務的な施設管理員。研究内容には疎いが、誰がどの廊下を通ったかはよく覚えている。規則違反の私物ヒーターを見つけられるのを恐れている。 goals: - 私物ヒーターの使用を隠したい diff --git a/db/scenarios/snowbound-farm-morning-chores.yaml b/db/scenarios/snowbound-farm-morning-chores.yaml index 38775db..13ab36a 100644 --- a/db/scenarios/snowbound-farm-morning-chores.yaml +++ b/db/scenarios/snowbound-farm-morning-chores.yaml @@ -133,6 +133,8 @@ characters: - id: fuyuki name: 冬木紬 publicIntroduction: "実務に強く、農園の細かな段取りを誰より把握している主任。" + ageGroup: adult + gender: female personality: 実務に強く、農園の細かな段取りを誰より把握している主任。佐久間への恩義を語る一方、経営の数字を突かれると急に防御的になる。朝の作業は佐久間本人がしたという前提を崩されたくない。 goals: - 出荷代金の流用を隠したい @@ -171,6 +173,8 @@ characters: - id: nozomi name: 佐久間希 publicIntroduction: "被害者の姪。" + ageGroup: young + gender: female personality: 被害者の姪。几帳面で数字に強いが、個人的な借金を抱えていることを恥じている。朝に台所の灯りを見たため、叔父はその時点で生きていたと強く思い込んでいる。 goals: - 叔父から金を借りていたことを隠したい @@ -197,6 +201,8 @@ characters: - id: kuze name: 久世圭太 publicIntroduction: "農園の設備担当。" + ageGroup: adult + gender: male personality: 設備の状態を数字で覚える寡黙な技術者。予備発電機を私用した規則違反を隠しているが、人の動線については余計な推測をせず見たまま話す。 goals: - 予備発電機の私的利用を隠したい diff --git a/db/scenarios/snowbound-gallery-wrong-crime-scene.yaml b/db/scenarios/snowbound-gallery-wrong-crime-scene.yaml index c88841f..fda8617 100644 --- a/db/scenarios/snowbound-gallery-wrong-crime-scene.yaml +++ b/db/scenarios/snowbound-gallery-wrong-crime-scene.yaml @@ -143,6 +143,8 @@ characters: - id: kosaka name: 香坂澪 publicIntroduction: "手先が器用で作品を丁寧に扱う修復士。" + ageGroup: adult + gender: female personality: 手先が器用で作品を丁寧に扱う修復士。保存庫が発見時に閉鎖されていたことを強調し、「中で事件が起きたなら自分には不可能」と話す。修復費の細部を追及されると苛立つ。 goals: - 修復費の水増しを隠したい @@ -190,6 +192,8 @@ characters: - id: asakura name: 朝倉凪 publicIntroduction: "作品の所在を細かく管理する学芸員。" + ageGroup: adult + gender: female personality: 作品の所在を細かく管理する学芸員。保存庫で発見したため当初はそこが事件現場だと思い込んだ。所蔵作品の無断貸し出しを隠している。 goals: - 所蔵作品を無断で貸したことを隠したい @@ -222,6 +226,8 @@ characters: - id: tada name: 多田圭 publicIntroduction: "画廊の警備担当。" + ageGroup: adult + gender: male personality: 規則を重んじる警備担当だが、自分の休憩延長だけは記録から外している。保存庫の閉鎖時刻は機械記録と照合して話せる。 goals: - 無断で休憩を延ばしたことを隠したい diff --git a/db/scenarios/snowbound-studio-roomtone-loop.yaml b/db/scenarios/snowbound-studio-roomtone-loop.yaml index cf2bb46..13cab25 100644 --- a/db/scenarios/snowbound-studio-roomtone-loop.yaml +++ b/db/scenarios/snowbound-studio-roomtone-loop.yaml @@ -158,6 +158,8 @@ characters: - id: manabe name: 真鍋伊織 publicIntroduction: "録音所の録音技師。" + ageGroup: adult + gender: male personality: 音の差異に異常なほど敏感で、自分の技術に強い自負を持つ録音技師。説明は専門的だが、連続した収録ファイルを在席の客観証拠として押し出し、入力経路の話になると曖昧になる。 goals: - 未公開音源を外部へ渡したことを隠したい @@ -202,6 +204,8 @@ characters: - id: shirase name: 白瀬環 publicIntroduction: "感覚的に物事を捉える歌手。" + ageGroup: young + gender: female personality: 感覚的に物事を捉える歌手。公開前情報を知人へ話したことを隠したいが、廊下で聞いた音については率直に答える。音が聞こえたため最初は真鍋が中にいると思い込んでいる。 goals: - 新曲情報を知人へ漏らしたことを隠したい @@ -225,6 +229,8 @@ characters: - id: takano name: 鷹野徹 publicIntroduction: "録音所の制作進行。" + ageGroup: adult + gender: male personality: 現実的で数字に強い制作進行。契約処理の付け替えが露見するのを恐れているため冬木との事務的な衝突を小さく見せるが、音響機器の細部には疎い。 goals: - 制作費の付け替えを隠したい @@ -248,6 +254,8 @@ characters: - id: makimura name: 牧村葉月 publicIntroduction: "配線と機材配置を記憶するのが得意な機材担当。" + ageGroup: young + gender: female personality: 配線と機材配置を記憶するのが得意な機材担当。高価なマイクを傷つけたことを隠したいが、パッチ盤の入力経路と廊下で見た真鍋については正確に話せる。 goals: - マイクを落として傷つけたことを隠したい diff --git a/db/scenarios/snowbound-theater-auto-cues.yaml b/db/scenarios/snowbound-theater-auto-cues.yaml index 16337a0..2744360 100644 --- a/db/scenarios/snowbound-theater-auto-cues.yaml +++ b/db/scenarios/snowbound-theater-auto-cues.yaml @@ -149,6 +149,8 @@ characters: - id: kunieda name: 国枝美冬 publicIntroduction: "段取りに厳しく、舞台上の出来事を秒単位で管理する舞台監督。" + ageGroup: adult + gender: female personality: 段取りに厳しく、舞台上の出来事を秒単位で管理する舞台監督。照明が予定どおり進んだ事実を自分の操作記録のように語り、自動進行機能について尋ねられると話をそらす。 goals: - 架空スタッフ名義の残業費請求を隠したい @@ -198,6 +200,8 @@ characters: - id: hiiragi name: 柊真琴 publicIntroduction: "感情表現の大きい主演俳優。" + ageGroup: adult + gender: male personality: 感情表現の大きい主演俳優。改稿台本を知人へ送ったことを隠したいが、舞台機器の仕組みには詳しくない。瀬尾と配役を巡って口論していた。 goals: - 公開前の台本を外部へ送ったことを隠したい @@ -218,6 +222,8 @@ characters: - id: toba name: 鳥羽香 publicIntroduction: "細部を見る衣装担当。" + ageGroup: adult + gender: female personality: 細部を見る衣装担当。衣装の損傷を隠したいが、客席から照明が二十分間切り替わった事実は正確に話す。ただし誰が調光卓を操作していたかは見えていない。 goals: - 衣装を傷めて隠したことを秘密にしたい @@ -244,6 +250,8 @@ characters: - id: sasai name: 笹井徹 publicIntroduction: "舞台機構の挙動に詳しい技術者。" + ageGroup: adult + gender: male personality: 舞台機構の挙動に詳しい技術者。点検省略を隠したいが、22時10分の国枝の目撃と調光卓の自動進行機能は正確に説明できる。 goals: - 舞台機構の定期点検を一項目省略したことを隠したい diff --git a/db/scenarios/storm-aquarium-feeding-lamp.yaml b/db/scenarios/storm-aquarium-feeding-lamp.yaml index 85fad9e..999d90a 100644 --- a/db/scenarios/storm-aquarium-feeding-lamp.yaml +++ b/db/scenarios/storm-aquarium-feeding-lamp.yaml @@ -141,6 +141,8 @@ characters: - id: morishita name: 森下莉央 publicIntroduction: "生き物の状態変化には鋭いが、責任を問われると防御的になる飼育員。" + ageGroup: young + gender: female personality: 生き物の状態変化には鋭いが、責任を問われると防御的になる飼育員。担当展示への愛着が強く、給餌灯を自分のアリバイとして強調する。 goals: - 飼育記録を改ざんしたことを隠したい @@ -187,6 +189,8 @@ characters: - id: sakakibara name: 榊原直 publicIntroduction: "几帳面で理屈っぽい獣医師。" + ageGroup: adult + gender: unknown personality: 几帳面で理屈っぽい獣医師。在庫管理ミスが表に出るのを恐れているが、給餌設備の基本仕様は把握している。 goals: - 在庫記録を後から修正したことを隠したい @@ -213,6 +217,8 @@ characters: - id: mikoshiba name: 御子柴徹 publicIntroduction: "豪放で口数の多い潜水設備担当。" + ageGroup: adult + gender: male personality: 豪放で口数の多い潜水設備担当。無断の私物カメラを隠したいが、給餌装置の整備も担当しており機械の挙動には詳しい。 goals: - 私物カメラの無断設置を隠したい @@ -239,6 +245,8 @@ characters: - id: sagara name: 相良芳江 publicIntroduction: "落ち着いた夜間警備員。" + ageGroup: senior + gender: female personality: 落ち着いた夜間警備員。巡回を一回省略したことだけは隠したいが、零時すぎにバックヤードで森下とすれ違った記憶は鮮明。 goals: - 西展示区画の巡回を省略したことを隠したい diff --git a/db/scenarios/storm-hydropower-rising-walkway.yaml b/db/scenarios/storm-hydropower-rising-walkway.yaml index f4bbc0d..a059e64 100644 --- a/db/scenarios/storm-hydropower-rising-walkway.yaml +++ b/db/scenarios/storm-hydropower-rising-walkway.yaml @@ -154,6 +154,8 @@ characters: - id: kido name: 城戸真琴 publicIntroduction: "古い設備の癖まで覚えている保守主任。" + ageGroup: adult + gender: unknown personality: 古い設備の癖まで覚えている保守主任。発見時の歩廊が通れなかった事実を強調し、「あの部屋へは誰も入れない」と言い張る。点検記録の話には神経質になる。 goals: - 点検記録の改ざんを隠したい @@ -202,6 +204,8 @@ characters: - id: saionji name: 西園寺悠 publicIntroduction: "発電所の安全監査員。" + ageGroup: adult + gender: unknown personality: 記録を一つずつ照合する監査員。発見時の現場を見て「密室」と判断したが、その状態がいつ成立したかまでは考えていなかった。報告書漏洩を隠している。 goals: - 監査報告の漏洩を隠したい @@ -234,6 +238,8 @@ characters: - id: taniguchi name: 谷口航 publicIntroduction: "数値監視を担当する若い運転員。" + ageGroup: young + gender: male personality: 数値監視を担当する若い運転員。警報の扱いで規則違反をしたためログの話題を避けるが、水位の上昇時刻については正確に答えられる。 goals: - 警報を非表示にした規則違反を隠したい diff --git a/db/scenarios/storm-island-scheduled-mail.yaml b/db/scenarios/storm-island-scheduled-mail.yaml index aa2de48..2181392 100644 --- a/db/scenarios/storm-island-scheduled-mail.yaml +++ b/db/scenarios/storm-island-scheduled-mail.yaml @@ -153,6 +153,8 @@ characters: - id: aizawa name: 相沢奈緒 publicIntroduction: "予定と文書管理に几帳面で、榊原の癖もよく知る秘書。" + ageGroup: adult + gender: female personality: 予定と文書管理に几帳面で、榊原の癖もよく知る秘書。落ち着いて時系列を整理するように話すが、21時42分のメールを本人の生存証明として意図的に強調する。 goals: - 財団資金を私的に流用したことを隠したい @@ -207,6 +209,8 @@ characters: - id: mikage name: 御影崇 publicIntroduction: "財団の会計担当。" + ageGroup: adult + gender: male personality: 数字に細かく、他人の経費処理にも厳しい会計担当。自分も予算項目を付け替えていたため監査の話題を嫌うが、メールの送信情報を見る知識はある。 goals: - 予算項目の付け替えを隠したい @@ -230,6 +234,8 @@ characters: - id: hatori name: 羽鳥栞 publicIntroduction: "話し方が明快な外部講師。" + ageGroup: adult + gender: female personality: 話し方が明快な外部講師。未公開教材を別講座で使ったことは隠したいが、21時35分から55分まで相沢と一緒に準備していたことは正確に証言する。 goals: - 未公開教材を別講座で先に使ったことを隠したい @@ -256,6 +262,8 @@ characters: - id: horie name: 堀江充 publicIntroduction: "施設内の人の出入りをよく見ている管理担当。" + ageGroup: adult + gender: male personality: 施設内の人の出入りをよく見ている管理担当。空き客室を私物置き場にしていたことを隠したいが、21時29分に執務室前で見た相沢については時刻も場所も覚えている。 goals: - 空き客室を私物の荷物置き場にしていたことを隠したい diff --git a/db/scenarios/storm-lighthouse-relief-slate.yaml b/db/scenarios/storm-lighthouse-relief-slate.yaml index 9f9089f..e62b3ab 100644 --- a/db/scenarios/storm-lighthouse-relief-slate.yaml +++ b/db/scenarios/storm-lighthouse-relief-slate.yaml @@ -134,6 +134,8 @@ characters: - id: torigoe name: 鳥越玲 publicIntroduction: "実直に見える設備当直員。" + ageGroup: adult + gender: female personality: 実直に見える設備当直員。手順や記録を重視する話し方をし、数字を示して自分の説明を固めようとする。記録ミスを倉橋に見つけられたことを強く恐れている。 goals: - 当直板と濡れた外套を西側巡回の証拠として通したい @@ -174,6 +176,8 @@ characters: - id: hashiba name: 橋場圭 publicIntroduction: "灯台の通信担当。" + ageGroup: adult + gender: male personality: 観察力のある通信担当。時刻は通信時計で確認する癖がある一方、規定外の個人的な受信をしていたため通信記録を調べられるのを嫌がる。 goals: - 個人的な無線受信を隠したい @@ -197,6 +201,8 @@ characters: - id: morishita name: 森下透 publicIntroduction: "若い補助員。" + ageGroup: young + gender: male personality: 若い補助員。素直だが自分の勤務態度に自信がなく、居眠りを知られるのを恐れている。設備の封鎖確認には橋場より詳しい。 goals: - 発電室で眠っていたことを隠したい diff --git a/db/scenarios/storm-mountain-inn-echoed-cane.yaml b/db/scenarios/storm-mountain-inn-echoed-cane.yaml index 66f6223..3108397 100644 --- a/db/scenarios/storm-mountain-inn-echoed-cane.yaml +++ b/db/scenarios/storm-mountain-inn-echoed-cane.yaml @@ -142,6 +142,8 @@ characters: - id: akiwa name: 秋庭澄 publicIntroduction: "穏やかな料理長で、客の好みをよく覚えている。" + ageGroup: adult + gender: unknown personality: 穏やかな料理長で、客の好みをよく覚えている。厨房と旧階段を自由に使える。九時二十五分の杖音を桐谷本人の行動と断定し、仕入れの話を避けようとする。 goals: - 業者から謝礼を受け取っていたことを隠したい @@ -182,6 +184,8 @@ characters: - id: morisaki name: 森崎透 publicIntroduction: "山宿の常連客。" + ageGroup: senior + gender: male personality: 桐谷と長い付き合いがあり、杖の音を何度も聞いてきた常連客。その親しさゆえに、特徴的な音を聞いた瞬間に桐谷本人だと決めつけた。宿泊代の未払いを隠している。 goals: - 宿泊代の未払いを隠したい @@ -208,6 +212,8 @@ characters: - id: sakakibara name: 榊原蓮 publicIntroduction: "古い木造建築の癖を体で覚えている大工。" + ageGroup: adult + gender: male personality: 古い木造建築の癖を体で覚えている大工。余剰材料を持ち帰ろうとした後ろめたさはあるが、音の伝わり方については具体的に説明できる。 goals: - 余剰木材の持ち出し計画を隠したい diff --git a/db/scenarios/storm-planetarium-reflected-witness.yaml b/db/scenarios/storm-planetarium-reflected-witness.yaml index 8d63211..4d78744 100644 --- a/db/scenarios/storm-planetarium-reflected-witness.yaml +++ b/db/scenarios/storm-planetarium-reflected-witness.yaml @@ -129,6 +129,8 @@ characters: - id: onodera name: 小野寺莉香 publicIntroduction: "空間演出に詳しく、人の視線や展示の見え方を意識する企画担当。" + ageGroup: adult + gender: female personality: 空間演出に詳しく、人の視線や展示の見え方を意識する企画担当。落ち着いて松田の目撃を肯定するが、ガラスの反射条件を誰より理解している。 goals: - 21時35分の白い人物像を犬塚の生存証明として成立させたい @@ -169,6 +171,8 @@ characters: - id: matsuda name: 松田圭介 publicIntroduction: "天象館の投影技師。" + ageGroup: adult + gender: male personality: 機械には強いが、視覚的な思い込みには無自覚な投影技師。白い上着を見て犬塚だと判断したが、顔までは確認していない。機器テストを省略したことを隠したい。 goals: - 投影機器の事前テストを省略したことを隠したい @@ -196,6 +200,8 @@ characters: - id: asakura name: 朝倉葉月 publicIntroduction: "現実的な売店責任者で、人の服装をよく覚えている。" + ageGroup: adult + gender: female personality: 現実的な売店責任者で、人の服装をよく覚えている。閉館作業中に小野寺が白い展示用上着を着ていたことを見ているが、在庫不足を隠している。 goals: - 売店在庫の不足を隠したい diff --git a/db/scenarios/storm-research-vessel-missing-launch.yaml b/db/scenarios/storm-research-vessel-missing-launch.yaml index 1685113..3e85ac5 100644 --- a/db/scenarios/storm-research-vessel-missing-launch.yaml +++ b/db/scenarios/storm-research-vessel-missing-launch.yaml @@ -134,6 +134,8 @@ characters: - id: kariya name: 狩谷琴音 publicIntroduction: "頭の回転が速く、可能性を広げて話す研究員。" + ageGroup: young + gender: female personality: 頭の回転が速く、可能性を広げて話す研究員。作業艇の消失を強調し、船内の三人に限定せず外部犯人の可能性を残そうとする。自分の研究上の不正を瀬尾に見つけられていた。 goals: - 消えた作業艇を外部犯人の逃走手段と思わせたい @@ -174,6 +176,8 @@ characters: - id: mizuki name: 水城明 publicIntroduction: "現場優先の機関担当。" + ageGroup: adult + gender: male personality: 現場優先の機関担当。設備点検を延期した件を隠したいが、作業艇が失われた時刻と狩谷の署名については記録通り話す。 goals: - 設備点検の延期を隠したい @@ -207,6 +211,8 @@ characters: - id: nonomura name: 野々村岳 publicIntroduction: "調査船の通信担当。" + ageGroup: adult + gender: male personality: 正確な通信記録を好む担当者。勤務中の私的通信を知られたくない。19時56分に瀬尾本人と話したことには自信がある。 goals: - 業務外の個人的な通信を隠したい diff --git a/db/scenarios/storm-seminar-echoed-testimony.yaml b/db/scenarios/storm-seminar-echoed-testimony.yaml index 9da83ff..c90706b 100644 --- a/db/scenarios/storm-seminar-echoed-testimony.yaml +++ b/db/scenarios/storm-seminar-echoed-testimony.yaml @@ -140,6 +140,8 @@ characters: - id: kuga name: 久我真帆 publicIntroduction: "自信が強く議論の組み立てが速い研究員。" + ageGroup: young + gender: female personality: 自信が強く議論の組み立てが速い研究員。自分の証言だけでなく「成瀬も白石も同じ認識だった」と一致の数を強調する。未発表成果の扱いを問われると攻撃的になる。 goals: - 未発表成果を無断投稿しようとしたことを隠したい @@ -185,6 +187,8 @@ characters: - id: naruse name: 成瀬灯 publicIntroduction: "情報整理が得意な編集者。" + ageGroup: adult + gender: female personality: 情報整理が得意な編集者。ただし、誰から聞いた情報かを省いて「分かっていること」として話す癖がある。公開前情報の漏洩を隠している。 goals: - 論文集の内容を漏らしたことを隠したい @@ -217,6 +221,8 @@ characters: - id: shiraishi name: 白石慧 publicIntroduction: "研究会に参加していた大学院生。" + ageGroup: young + gender: male personality: 慎重な大学院生で、人の発言を時刻と一緒に覚えている。参加者データを無断利用した秘密はあるが、21時台の廊下で見た久我については具体的に話せる。 goals: - 参加者データの無断流用を隠したい diff --git a/db/scenarios/thunder-cablecar-occupied-cabin.yaml b/db/scenarios/thunder-cablecar-occupied-cabin.yaml index b2230f7..fdd2df2 100644 --- a/db/scenarios/thunder-cablecar-occupied-cabin.yaml +++ b/db/scenarios/thunder-cablecar-occupied-cabin.yaml @@ -160,6 +160,8 @@ characters: - id: sudo name: 須藤拓海 publicIntroduction: "ロープウェイの運転主任。" + ageGroup: adult + gender: male personality: 安全規程を熟知し、自信を持って機器表示を説明する運転主任。自分の点検記録の不備を隠し、監視盤の『乗員1』を人物の在席記録であるかのように語る。 goals: - 制動点検記録の虚偽を隠したい @@ -209,6 +211,8 @@ characters: - id: enomoto name: 榎本澄 publicIntroduction: "丁寧で記憶力のよい案内係。" + ageGroup: adult + gender: unknown personality: 丁寧で記憶力のよい案内係。拾った財布を正式処理しなかったことを隠したいが、21時18分に職員通路で須藤とすれ違ったことは明確に覚えている。 goals: - 拾得物を自分のロッカーへ入れたことを隠したい @@ -235,6 +239,8 @@ characters: - id: nagamine name: 長峰礼 publicIntroduction: "機械の仕様を優先して考える設備担当。" + ageGroup: adult + gender: unknown personality: 機械の仕様を優先して考える設備担当。融雪設備の手順違反を隠したいが、非常用搬器の乗員表示が単なる荷重判定だと知っている。 goals: - 融雪ヒーターの警報を無効化したことを隠したい @@ -265,6 +271,8 @@ characters: - id: orihara name: 折原壮 publicIntroduction: "山頂駅にいた山岳写真家。" + ageGroup: adult + gender: male personality: 好奇心が強く規則を軽視しがちな山岳写真家。立入禁止の保守デッキへ出たことを隠すため行動をぼかすが、職員同士の事情には詳しくない。 goals: - 保守デッキへ無断で出たことを隠したい diff --git a/db/scenarios/tsukimisou.yaml b/db/scenarios/tsukimisou.yaml index 362f3b2..2f0a299 100644 --- a/db/scenarios/tsukimisou.yaml +++ b/db/scenarios/tsukimisou.yaml @@ -41,6 +41,8 @@ victim: statement: 争った跡が無い。着衣も髪も乱れておらず、文机の上も片付いたままになっている。 - id: numbness-signs statement: 唇のまわりと指先に、しびれが出たときの跡が残っている。 + - id: postmortem-state + statement: 発見時、体にはまだ温かさが残っており、死後硬直も始まりかけた段階にとどまっている。 - id: single-glass statement: 文机に、飲みかけのグラスが一つだけ置かれている。誰かと酌み交わした跡は無い。 # 後継者指定を知ってから読むと意味が変わる草案。順序を作るために前提を置く。 @@ -420,6 +422,8 @@ characters: - id: fukagawa name: 深川誠也 publicIntroduction: "気弱で愛想笑いが多い税理士。" + ageGroup: senior + gender: male personality: 気弱で愛想笑いが多い税理士。人当たりは柔らかいが、追い詰められるとしどろもどろになり目が泳ぐ。涼子には昔から頭が上がらない。 goals: - 横領が誰にもバレないまま今夜をやり過ごしたい @@ -453,6 +457,8 @@ characters: - id: mizuki name: 早坂美月 publicIntroduction: "明るく気配り上手で場の空気をよく読む。" + ageGroup: adult + gender: female personality: 明るく気配り上手で場の空気をよく読む。涼子の親戚の中でも一番可愛がられてきた自覚がある。内心は打算的で、追い詰められると笑顔の下で早口になる。 goals: - 月見荘と遺産を確実に手に入れたい @@ -486,6 +492,8 @@ characters: - id: kiryu name: 桐生涼 publicIntroduction: "落ち着いた物腰で観察力が鋭い医師。" + ageGroup: senior + gender: male personality: 落ち着いた物腰で観察力が鋭い医師。淡々と話すが、涼子への想いだけは昔から変わらず深い。 goals: - 涼子への想いを誰にも知られたくない @@ -635,9 +643,9 @@ evidences: # 探偵自身の検死。遺体を調べる一手を使った人だけが、刻限を自分の目で確かめられる。 - id: postmortem-signs label: 遺体に残る中毒の徴候と、その進み具合 - description: 唇のまわりと指先のしびれの跡、体の冷え方と硬直の出方。口にしてから絶命までの時間と合わせると、事切れたのは20時15分ごろになる。 + description: 唇のまわりと指先のしびれの跡に加え、発見時には体にまだ温かさが残り、死後硬直も始まりかけた段階にとどまっている。発見時刻と合わせると、事切れたのは20時15分ごろと推定できる。 reveal: - condition: プレイヤーが遺体を調べ、探偵が唇や指先の跡、体の冷え方や硬直といった死後の変化に触れたら開示する。 + condition: プレイヤーが遺体を調べ、検分の返答で「体にまだ温かさが残り、死後硬直も始まりかけている」という所見を実際に確認し、その所見を根拠に死亡時刻を検討したら開示する。 sources: - type: victim id: victim diff --git a/db/scenarios/typhoon-datacenter-maintenance-window.yaml b/db/scenarios/typhoon-datacenter-maintenance-window.yaml index a669692..c4e9275 100644 --- a/db/scenarios/typhoon-datacenter-maintenance-window.yaml +++ b/db/scenarios/typhoon-datacenter-maintenance-window.yaml @@ -145,6 +145,8 @@ characters: - id: kuze name: 久世秋穂 publicIntroduction: "データセンターのシステム技術者。" + ageGroup: adult + gender: female personality: 冷静で手順を重んじるシステム技術者。ログを強い根拠として話す一方、自分の不正を追及されると急に言葉数が減る。 goals: - 保守費の水増しを隠したい @@ -184,6 +186,8 @@ characters: - id: koda name: 甲田修 publicIntroduction: "口が達者な外部技術者。" + ageGroup: adult + gender: male personality: 口が達者な外部技術者。契約外の設定ファイルを持ち出していたため、自分の作業内容を細かく話したがらない。 goals: - 顧客設定ファイルの無断複製を隠したい @@ -204,6 +208,8 @@ characters: - id: hatano name: 波多野結 publicIntroduction: "現場優先で判断の速い設備技術者。" + ageGroup: adult + gender: female personality: 現場優先で判断の速い設備技術者。警報を勝手に止めたことには後ろめたさがあるが、部品庫の扉の状態はよく覚えている。 goals: - 温度警報を無断停止したことを隠したい @@ -224,6 +230,8 @@ characters: - id: makabe name: 真壁徹 publicIntroduction: "データセンターの警備員。" + ageGroup: adult + gender: male personality: 規則に厳しい警備員。長時間勤務で居眠りしたことを隠したいが、廊下ですれ違った人物の記憶には自信がある。 goals: - 監視席で居眠りしたことを隠したい diff --git a/db/scenarios/typhoon-offshore-drone-route.yaml b/db/scenarios/typhoon-offshore-drone-route.yaml index 67449e7..25a89b1 100644 --- a/db/scenarios/typhoon-offshore-drone-route.yaml +++ b/db/scenarios/typhoon-offshore-drone-route.yaml @@ -148,6 +148,8 @@ characters: - id: hiiragi name: 柊木慧 publicIntroduction: "洋上基地の点検技師。" + ageGroup: adult + gender: male personality: 機器の性能を熟知し、点検記録を客観的な証拠として扱う技師。自分の作業効率に自信があり、不正を「現場の合理化」と言い換えがち。飛行軌跡と写真を自分の在席証明として押し出す。 goals: - 過去写真を使った点検不正を隠したい @@ -190,6 +192,8 @@ characters: - id: saegusa name: 三枝千尋 publicIntroduction: "保護装置の理屈に厳しい電気主任。" + ageGroup: adult + gender: female personality: 保護装置の理屈に厳しい電気主任。無断リセットを隠したいが、01時08分に連絡階段で柊木を見た記憶は明確。ドローンが自動運転できることも知っている。 goals: - 保護装置を無断リセットしたことを隠したい @@ -210,6 +214,8 @@ characters: - id: nami name: 波木恵 publicIntroduction: "洋上基地の気象担当。" + ageGroup: adult + gender: female personality: 数字に慎重な気象担当。風速記録の書き直しを隠したいが、点検ドローンが事前経路を自律飛行する運用は日常的に見ている。 goals: - 風速の手書き記録を後から修正したことを隠したい @@ -233,6 +239,8 @@ characters: - id: kokubu name: 国分透 publicIntroduction: "洋上基地の資材担当。" + ageGroup: adult + gender: male personality: 在庫と数字に敏感な資材担当。棚卸し不足を隠すため部品の付け替えをしており、芳賀に追及されていた。事件を自分の不正と結びつけられたくない。 goals: - 予備部品の在庫付け替えを隠したい diff --git a/db/scenarios/venetian-lazaretto-sunset-bell.yaml b/db/scenarios/venetian-lazaretto-sunset-bell.yaml index ecc256f..5490ea1 100644 --- a/db/scenarios/venetian-lazaretto-sunset-bell.yaml +++ b/db/scenarios/venetian-lazaretto-sunset-bell.yaml @@ -117,6 +117,8 @@ characters: - id: marta name: マルタ・ベッリーニ publicIntroduction: "実務に強く落ち着いた薬剤係。" + ageGroup: adult + gender: female personality: 実務に強く落ち着いた薬剤係。時刻を尋ねられると時計ではなく鐘を基準に話す癖がある。不足した薬剤の話題では急に言葉が硬くなる。 goals: - 薬剤の横流しを隠したい @@ -155,6 +157,8 @@ characters: - id: nicolo name: ニコロ・フェッリ publicIntroduction: "几帳面な書記。" + ageGroup: adult + gender: male personality: 几帳面な書記。紙の記録を重視し、人の記憶には懐疑的。配給帳を書き換えていたため帳簿全体を調べられるのは困るが、廊下で見たマルタについては正確に話す。 goals: - 配給帳の改ざんを隠したい @@ -181,6 +185,8 @@ characters: - id: pietro name: ピエトロ・サルヴィ publicIntroduction: "潟の天候と潮を読むことに長けた舟番。" + ageGroup: senior + gender: male personality: 潟の天候と潮を読むことに長けた舟番。時計より空の明るさや鐘で時間を捉える。私信を運んだ規則違反だけは隠したい。 goals: - 私信を密かに舟へ載せたことを隠したい diff --git a/db/scenarios/victorian-underground-last-telegram.yaml b/db/scenarios/victorian-underground-last-telegram.yaml index 7b3b9ca..19bb397 100644 --- a/db/scenarios/victorian-underground-last-telegram.yaml +++ b/db/scenarios/victorian-underground-last-telegram.yaml @@ -142,6 +142,8 @@ characters: - id: bell name: アーサー・ベル publicIntroduction: "実務能力の高い施工監督。" + ageGroup: adult + gender: male personality: 実務能力の高い施工監督。工期と費用の話になると強気だが、帳簿の細部を問われると苛立つ。電信の符号がヘイル本人の生存証明だと言い張りたい。 goals: - 資材費の水増しを隠したい @@ -180,6 +182,8 @@ characters: - id: clara name: クララ・ウェッブ publicIntroduction: "正確さを誇る若い電信係。" + ageGroup: young + gender: female personality: 正確さを誇る若い電信係。受信した文字列については自信がある一方、私的な通信を写していた規則違反は隠したい。送信者を実際に見てはいない。 goals: - 無断で私信を写していたことを隠したい @@ -206,6 +210,8 @@ characters: - id: thomas name: トーマス・リード publicIntroduction: "無骨な機関助手。" + ageGroup: adult + gender: male personality: 無骨な機関助手。工事用石炭の横流しをしていたため帳簿調査を恐れている。坑道で見たベルの位置については妙に具体的に覚えている。 goals: - 石炭の横流しを隠したい diff --git a/db/schema.ts b/db/schema.ts index 0311d2f..a18cc29 100644 --- a/db/schema.ts +++ b/db/schema.ts @@ -2,6 +2,8 @@ import { sql } from 'drizzle-orm' import { index, integer, primaryKey, sqliteTable, text } from 'drizzle-orm/sqlite-core' import type { Detective } from './detective' import type { FloorPlanInput } from './floor-plan' +import type { JudgeTuning } from './judge-tuning' +import type { AgeGroup, Gender } from './person' import type { InvestigablePlace, PlaceFindings } from './place' import type { ScenarioEvidenceSource, ScenarioRevelationSource } from './scenario-definition' import type { TimelineEvent } from './timeline-event' @@ -9,7 +11,8 @@ import type { VictimFinding } from './victim-finding' /** * プレイヤーが演じる探偵の形と検証は db/detective.ts が正典。 - * 年ごろと性別は列挙で、NPCの呼びかけ方はそこから引く。 + * 年ごろと性別の列挙は db/person.ts にあり、探偵と登場人物が同じものを読む。 + * NPCの呼びかけ方はそこから引く。 */ export type { Detective } from './detective' /** @@ -209,6 +212,29 @@ export const characters = sqliteTable( .notNull() .references(() => scenarios.id, { onDelete: 'cascade' }), name: text('name').notNull(), + /** + * 幅の狭いところへ出す短い名前。アリバイ表の帯がこれを読む。 + * + * 既定が空なのは、この列より前に焼かれた行があるため。移行では `name` を写して埋め、 + * コンパイラも書かれていなければ `name` を入れるので、空のまま残る行は本来無い。 + */ + shortName: text('short_name').notNull().default(''), + /** + * 年ごろと性別。探偵と同じ列挙で、正典は db/person.ts。 + * + * `unknown` は「書かれていない」も兼ねる。キャラクターシートはそのとき行ごと出さないので、 + * 年ごろも性別も人物像の文章に委ねたままにできる。職業の列は持たない。 + */ + ageGroup: text('age_group').$type().notNull().default('unknown'), + gender: text('gender').$type().notNull().default('unknown'), + /** + * Irodori-TTS へ渡す声の指定。両方 null なら、その人物は喋らない。 + * + * キャラクターシートには入れない。これはNPCへ読ませる情報ではなく、 + * 合成を呼ぶ側が読むもので、プロンプトに混ぜると声の指示を台詞として喋り出す。 + */ + voiceSeed: integer('voice_seed'), + voiceCaption: text('voice_caption'), /** プレイヤーへ最初から見せてよい、完全公開の人物紹介。 */ publicIntroduction: text('public_introduction').notNull().default(''), personality: text('personality').notNull(), @@ -353,9 +379,14 @@ export const messages = sqliteTable( sessionId: text('session_id') .notNull() .references(() => playSessions.id, { onDelete: 'cascade' }), - characterId: text('character_id') - .notNull() - .references(() => characters.id, { onDelete: 'cascade' }), + /** + * 話しかけた(あるいは調べた)相手。 + * + * `characters` への外部キーは張らない。遺体は `victim` 固定、場所はシナリオの + * JSON 列が持つ ID で、どちらも `characters` に行が無いため。外部キーがあった頃は + * 検分の往復だけ記録ごと落としていた。 + */ + characterId: text('character_id').notNull(), role: text('role').notNull(), content: text('content').notNull(), createdAt: createdTimestamp('created_at'), @@ -464,7 +495,6 @@ export const llmUsages = sqliteTable( scenarioId: text('scenario_id'), /** provider.ts の LlmRole。actor / judge / author。 */ role: text('role').notNull(), - provider: text('provider').notNull(), model: text('model').notNull(), inputTokens: integer('input_tokens').notNull().default(0), outputTokens: integer('output_tokens').notNull().default(0), @@ -481,3 +511,153 @@ export const llmUsages = sqliteTable( index('llm_usages_session_id_idx').on(table.sessionId), ], ) + +/** + * 分析用の控え(analytics_sessions / analytics_turns)。 + * + * この2表は llm_usages と同じ理由で play_sessions への外部キーを張らない。 + * 会話ログは保持期間で消す方針のままにしつつ、プロンプトと難易度を後から + * 調整するための材料——プレイヤーが何を打ち、NPCが何を返し、その回に何が + * 出たか——だけを残すのがこの2表の存在理由なので、cascade で一緒に消えては + * 目的がそのまま失われる。したがって session_id / scenario_id は参照の切れた + * 履歴上の値であり、play_sessions と JOIN できることを前提にしてはいけない + * (この2表どうしの JOIN は、両方とも消えないので成立する)。 + * + * 集計に使う値は列に開き、読み返すだけの本文は JSON 列に置く。 + * 「モード別に何ターンで解けたか」を数えるのは列、「そのとき何を打ったか」を + * 読むのは JSON、という住み分け。 + */ + +/** analytics_turns.exchanges の1往復。探偵の質問とNPCの返答。 */ +export type LoggedExchange = { + question: string + answer: string +} + +/** + * 1プレイセッション1行。開始時に入れ、告発時に結果列を埋める。 + * + * 告発まで至らなかった行は結果列が NULL のまま残る。これは欠損ではなく、 + * 「どこで諦めたか」がこの表の一番知りたいことの一つなので、 + * 完走した行だけを入れる作りにはしない。 + */ +export const analyticsSessions = sqliteTable( + 'analytics_sessions', + { + /** play_sessions.id と同じ値。ただし外部キーではない(上のコメント参照)。 */ + sessionId: text('session_id').primaryKey(), + scenarioId: text('scenario_id').notNull(), + /** 難易度モード。db/game-mode.ts の列挙。 */ + mode: text('mode').notNull(), + /** 名乗らずに始められるので nullable。名前と容姿は自由記述で、そのまま入る。 */ + detective: text('detective', { mode: 'json' }).$type(), + /** + * 進行の上限。難易度そのものを動かす値なので、モードとは別に持つ。 + * 同じ nohope でも上限が違えば別の難しさになり、混ぜると読み違える。 + */ + maxTurns: integer('max_turns').notNull(), + questionsPerTurn: integer('questions_per_turn').notNull(), + exchangesPerTopic: integer('exchanges_per_topic').notNull(), + startedAt: createdTimestamp('started_at'), + /** ここから下は告発時に埋まる。埋まっていない=最後まで行っていない。 */ + finishedAt: integer('finished_at', { mode: 'timestamp' }), + culpritCharacterId: text('culprit_character_id'), + culpritCorrect: integer('culprit_correct', { mode: 'boolean' }), + methodCorrect: integer('method_correct', { mode: 'boolean' }), + motiveCorrect: integer('motive_correct', { mode: 'boolean' }), + /** プレイヤーが書いた推理の本文。 */ + reasoning: text('reasoning'), + method: text('method'), + motive: text('motive'), + /** 採点者の短評。プレイヤーの記述をどう読んだかが分かる。 */ + methodComment: text('method_comment'), + motiveComment: text('motive_comment'), + solvedSeconds: integer('solved_seconds'), + questionCount: integer('question_count'), + evidenceFound: integer('evidence_found'), + /** 発見数だけでは難易度を読めないので、母数も一緒に残す。 */ + evidenceTotal: integer('evidence_total'), + contradictionCount: integer('contradiction_count'), + accuracyPercent: integer('accuracy_percent'), + }, + (table) => [ + index('analytics_sessions_scenario_id_idx').on(table.scenarioId), + index('analytics_sessions_started_at_idx').on(table.startedAt), + ], +) + +/** + * ask 1回(=プレイヤーが話題を1つ投げた回)1行。 + * + * 聞き込みと検分を同じ形で受ける。messages が検分を落としているのは + * character_id が characters への外部キーで、場所も遺体も入らないため。 + * こちらは外部キーを持たないので、subject_kind で区別して両方入る。 + */ +export const analyticsTurns = sqliteTable( + 'analytics_turns', + { + id: uuidPrimaryKey('id'), + sessionId: text('session_id').notNull(), + scenarioId: text('scenario_id').notNull(), + /** + * モードは analytics_sessions から JOIN で引けるが、ここにも置く。 + * 開始時の行の書き込みが落ちた回でも、ターン単体で難易度が読めるようにするため。 + */ + mode: text('mode').notNull(), + /** character / victim / place。sessions.ts の sourceTypeOf と同じ区別。 */ + subjectKind: text('subject_kind').notNull(), + /** 人物ID、VICTIM_ID、場所ID のいずれか。 */ + subjectId: text('subject_id').notNull(), + /** + * 何ターン目か(DO の round)と、その時点の累計質問数。 + * DO への記録が落ちた回は取れないので nullable。 + */ + turnIndex: integer('turn_index'), + questionCount: integer('question_count'), + /** プレイヤーが打った文そのもの。この列がこの表の主目的。 */ + topic: text('topic').notNull(), + /** 探偵の質問とNPCの返答。プロンプト調整はここを読む。 */ + exchanges: text('exchanges', { mode: 'json' }).$type().notNull(), + /** + * その回の判定。Judge が落ちた回は判定そのものが無いので nullable。 + * 落ちた回でもプレイヤーの入力は残したいので、行ごと捨てることはしない。 + */ + revealedEvidenceIds: text('revealed_evidence_ids', { mode: 'json' }).$type(), + revealedRevelationIds: text('revealed_revelation_ids', { mode: 'json' }).$type(), + contradictionPointedOut: integer('contradiction_pointed_out', { mode: 'boolean' }), + npcLied: integer('npc_lied', { mode: 'boolean' }), + /** + * その回に入れてあった判定の直し(`db/judge-tuning.ts`)。判定が落ちた回は無い。 + * + * 列を四つに割らず1つのJSONで持つのは、切り替え自体が試すためのもので、 + * 増えたり減ったり、決着がついたら丸ごと消える見込みのため。 + */ + judgeTuning: text('judge_tuning', { mode: 'json' }).$type(), + createdAt: createdTimestamp('created_at'), + }, + (table) => [ + index('analytics_turns_session_id_idx').on(table.sessionId), + index('analytics_turns_scenario_id_idx').on(table.scenarioId), + index('analytics_turns_created_at_idx').on(table.createdAt), + ], +) + +/** + * Irodori-TTS の登録話者に、年ごろと性別を付けた表。 + * + * TTS 側の `/speakers` は uuid と名前しか返さないので、キャラクターの `age_group` / + * `gender` と突き合わせるための情報がここにしか無い。値は `db/tts-speakers.ts` が + * Author LLM に話者名から推定させて入れる。 + * + * **開発中の仮のもの。** 登録話者は既存作品の登場人物で、公開する作品には乗せられない + * (`src/server/tts/irodori.ts` の但し書きを参照)。声の作り分けを画面で確かめるための + * 足場であって、外へ出すときはこの表ごと落とす。 + */ +export const ttsSpeakers = sqliteTable('tts_speakers', { + /** TTS 側の話者 UUID。こちらでは採番しない。 */ + id: text('id').primaryKey(), + /** 突き合わせには使わない。推定を人が見直すときの手がかり。 */ + name: text('name').notNull(), + ageGroup: text('age_group').$type().notNull().default('unknown'), + gender: text('gender').$type().notNull().default('unknown'), +}) diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 492823c..b6df198 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -10,7 +10,7 @@ | --- | --- | | [runtime.md](./runtime.md) | 実行基盤・APIレイヤ・ビルドパイプライン | | [data.md](./data.md) | 永続化・キャッシュ・状態管理の役割分担 | -| [llm.md](./llm.md) | LLMの役割分割、プロバイダ抽象、プロンプトキャッシュ | +| [llm.md](./llm.md) | LLMの役割分割、OpenAI互換サーバへの一本化、プロンプトキャッシュ | | [scenario-format.md](./scenario-format.md) | シナリオ定義形式、YAML/Structured Output、検証・コンパイル仕様 | | [cost.md](./cost.md) | 1プレイあたりのトークン消費、各社の無料枠、推奨構成 | | [development.md](./development.md) | 開発環境・コード品質・CI/CD・テスト | @@ -53,7 +53,7 @@ | ORM | Drizzle ORM / drizzle-kit | `^0.44` / `^0.31` | | セッション状態・レート制限 | Durable Objects (SQLite backend) | — | | 読みキャッシュ | Workers KV | — | -| LLM | Vercel AI SDK (Anthropic / OpenAI / Google) | `ai@^5` | +| LLM | Vercel AI SDK(OpenAI互換サーバ1つへ) | `ai@^5` / `@ai-sdk/openai@^2` | | バリデーション | Zod | `^4` | | Lint / Format | Biome + biome-plugins (GritQL) | `^2` | diff --git a/docs/architecture/cost.md b/docs/architecture/cost.md index b56666c..0506bf9 100644 --- a/docs/architecture/cost.md +++ b/docs/architecture/cost.md @@ -1,6 +1,8 @@ # コストと無料枠 > **調査時点: 2026年8月。** 各社の枠と価格は改定が速く、Gemini は2025年12月に無料枠を大幅削減しています。ここに書いた数字は見積もりの出発点であって正典ではありません。採用前に各社の公式ダッシュボードで実数を確認してください。 +> +> **各社の無料枠と価格を並べた節は、いまや参考資料です。** アプリは `OPENAI_URL` の OpenAI互換サーバ1つへ投げる形になり、どのベンダをどう束ねるかは互換サーバ側の話になりました([llm.md](./llm.md))。以下の枠と単価がそのまま効くのは、その互換サーバが当該ベンダを素通しで前段に置いている場合だけです。**削らずに残してあるのは、「1プレイのトークン量」と「回数で切られるかトークン量で切られるか」という見方が、宛先が変わっても効くからです。** 数字ではなく、この見方のほうを持ち帰ってください。 ## 1プレイあたりのトークン消費 @@ -23,9 +25,9 @@ Actor の入力が伸びるのは会話履歴が累積するためです。同じNPCに聞き続けるほど1リクエストが重くなります。逆にNPCを切り替えると履歴がリセットされるので入力は軽くなりますが、キャッシュも別物になります([llm.md](./llm.md) 参照)。 -**リクエスト回数は1プレイ24回**です(Actor 12 + Judge 12)。後述しますが、この回数が Gemini では致命的に効きます。 +**リクエスト回数は1プレイ24回**です(Actor 12 + Judge 12)。後述の参考節で見るとおり、トークン量ではなく回数で切られる枠に当たると、この回数が致命的に効きます。 -## OpenAI: データ共有プログラム +## OpenAI: データ共有プログラム(参考) 入出力を OpenAI と共有することに同意した組織に、日次の無料トークンが付与されます。 @@ -53,7 +55,7 @@ Terra と Luna が枠を共有するか個別かで倍近く変わります。 Sol(Author)の100万はプレイでは一切消費しません。シナリオ作成専用です。1本あたり5万トークンと見積もっても**1日20シナリオ**は作れるため、UGC 立ち上げ期には十分な枠です。 -## Google: Gemini 無料枠 +## Google: Gemini 無料枠(参考) **トークン量ではなく回数で切られる**のが決定的な違いです。 @@ -89,9 +91,9 @@ Sol(Author)の100万はプレイでは一切消費しません。シナリ SNS で共有されて瞬間的にアクセスが来る、という AlibAI が狙っている流入の形と正面から衝突します。 -## 推奨構成 +## 推奨構成(参考) -無料枠で回す前提なら、**トークン量を OpenAI 側に寄せる**のが最適です。 +無料枠で回す前提なら、**トークン量を OpenAI 側に寄せる**のが最適でした。 | 優先度 | 構成 | 1日のプレイ数 | | --- | --- | --- | @@ -99,9 +101,9 @@ SNS で共有されて瞬間的にアクセスが来る、という AlibAI が | 2 | 上記を使い切ったら Gemini Flash 系へフォールバック | 約420 | | 3 | Gemini のみ | 約125 | -Gemini を単独で使うより、**OpenAI の枠を使い切ってからのフォールバック先**として位置づけるほうが総量が伸びます。 +回数で切られる枠を単独で使うより、**トークン量で切られる枠を使い切ってからのフォールバック先**として位置づけるほうが総量が伸びる、というのがここの読みどころです。 -ただしフォールバック機構は現在のコードに入っていません。`resolveModel` は env の設定を返すだけなので、実装するなら `resolveModel` の外側に「枠を使い切ったら別プロバイダで再試行する」層が必要です。使用量の判断材料は `messages.usage` に揃っています。 +**アプリ側にフォールバック機構は入っていません。** `chooseLlm` は override → env → 既定表の順に引くだけで、使い切ったかどうかを見ません。実装するならその外側に「枠を使い切ったら別のモデルで再試行する」層が要ります。ただし宛先は互換サーバ1つなので、複数ベンダにまたがる振り分けを実装する場所としては、Worker より互換サーバ側のほうが素直です。使用量の判断材料は `llm_usages`(`role` / `model` ごとのトークン消費)に揃っています。 ### Judge を毎ターン呼ばない選択肢 @@ -118,13 +120,13 @@ Gemini を単独で使うより、**OpenAI の枠を使い切ってからのフ プロンプトキャッシュが下げるのは**単価**であって**トークン量**ではありません。無料枠が数えているのは量のほうです。キャッシュ読み込みトークンも量として計上されるなら、「1日300プレイ」という上限はキャッシュ設計をどれだけ丁寧にやっても動きません。 -つまり、[llm.md](./llm.md) に書いたブレークポイント設計は、**無料枠で回している間は体感ゼロ**です。効き始めるのは課金に移行した瞬間からです。 +つまり、[llm.md](./llm.md) に書いたプレフィックス設計は、**無料枠で回している間は体感ゼロ**です。効き始めるのは課金に移行した瞬間からです(そもそも互換サーバがキャッシュを持たなければ、課金後も効きません)。 無料枠のフェーズで追うべき指標は、キャッシュヒット率ではなく**総トークン量**です。ここを取り違えると「キャッシュを効かせたのにプレイ可能数が増えない」という誤った悩み方をします。見るべき数字がフェーズで変わる、と覚えておいてください。 ## 課金移行後の見積もり -参考として、GPT-5.6 の価格です(2026年7月30日の値下げ後、100万トークンあたり)。 +参考として、GPT-5.6 の価格です(2026年7月30日の値下げ後、100万トークンあたり)。**この表は積み上げの桁感をつかむための参考値で、実際の請求は互換サーバの課金体系に従います。** | モデル | 入力 | 出力 | | --- | --- | --- | diff --git a/docs/architecture/data.md b/docs/architecture/data.md index 9649eca..f781cef 100644 --- a/docs/architecture/data.md +++ b/docs/architecture/data.md @@ -51,10 +51,13 @@ scenario_truths 真相(サーバー限定。APIレスポンスに絶対含 characters 登場人物(公開紹介・人格・知識・秘密・目的・嘘・記憶) evidences 証拠(Judgeが開示を判定するための条件文) play_sessions プレイセッション(匿名可。プレイヤーが演じる探偵を持つ) -messages 会話ログ(NPC別、トークン使用量・プロバイダ・モデルも記録) +messages 会話ログ(NPC別。トークン使用量は持たない) discoveries 発見済み証拠(session_id + evidence_id の複合主キー) results 結果(解決時間、質問回数、正解率) reports UGC通報 +llm_usages LLM呼び出しごとのトークン消費(役割・モデル別。保持期間の削除対象外) +analytics_sessions 分析用の控え・1セッション1行(保持期間の削除対象外) +analytics_turns 分析用の控え・ask 1回1行(保持期間の削除対象外) ``` 設計上の要点が3つあります。 @@ -65,10 +68,14 @@ reports UGC通報 **`play_sessions.user_id` が nullable。** 匿名プレイを一級市民として扱うためです。「URLから即プレイ」を掲げる以上、ログイン壁は致命的になります。 -**`messages.usage` にトークン使用量を記録。** LLMを使うサービスはコスト可視化を後回しにすると事故ります。`provider` / `model` も併記し、プロバイダ別・シナリオ別に集計できるようにしています。 +**トークン使用量は `llm_usages` に持つ。** LLMを使うサービスはコスト可視化を後回しにすると事故ります。会話ログ(`messages`)側には持たせません。1回の話題で Interviewer・Actor・Judge が複数回モデルを呼ぶので、会話の行と呼び出しは1対1になりませんし、`messages` は保持期間を過ぎたら消える一方でコストの履歴は残す必要があります。`llm_usages` は `role` と `model` を持ち、役割別・モデル別・シナリオ別に集計できます。宛先が互換サーバ1つになったので `provider` 列は落としました(マイグレーション 0028)。どのモデルを叩いたかは `model` だけで一意に決まります。 **`scenarios.briefing` と `scenarios.floor_plan` は一覧に載せない。** 前者はゲームマスターが読み上げる事件の記録(空行区切りの段落)、後者は UI が SVG で描くための論理座標です。どちらも `GET /api/scenarios/:id` でだけ返します。選ぶ画面に長文と図が並ぶと、遊び始める前に読み疲れるためです。一覧が返すのはタイトル・カテゴリ・登場人物数・難易度・所要時間だけです。 +**`analytics_*` は `play_sessions` への外部キーを張らない。** `llm_usages` と同じ理由です。会話ログ(`messages`)と結果(`results`)は保持期間を過ぎたら消しますが、プロンプトと難易度を後から調整するための材料——プレイヤーが何を打ち、NPC が何を返し、その回に何が出たか——は残す必要があります。外部キーを張ると cascade で一緒に消え、この2表の存在理由がそのまま失われます。したがって `session_id` / `scenario_id` は参照の切れた履歴上の値で、`play_sessions` と JOIN できることを前提にしてはいけません(`analytics_sessions` と `analytics_turns` どうしの JOIN は、両方とも消えないので成立します)。 + +**検分の記録は `analytics_turns` にしかない。** `messages.character_id` は `characters` への外部キーなので、場所も遺体も入りません(`src/server/routes/sessions.ts` の ask で検分だけ `messages` への insert を飛ばしています)。`analytics_turns` は外部キーを持たず `subject_kind` で区別するため、聞き込みと検分が同じ形で入ります。 + **`play_sessions.detective` は開始時に決めたら変えない。** プレイヤーが演じる探偵(名前・年ごろ・性別・容姿)で、Actor のプロンプトに入ります。年ごろと性別は `db/detective.ts` の列挙が正典で、自由記述ではありません。NPC の呼びかけ(老人が十代の少女に「お嬢さん」と話しかける類)をこの2つから引くため、「28」「三十路」と書き方が割れると引けなくなります。会話の途中で変わるとキャッシュのプレフィックスが崩れるうえ、NPC から見て相手が別人になります。名乗らずに始めることもできるので nullable です。 ## Durable Objects diff --git a/docs/architecture/development.md b/docs/architecture/development.md index 0651a50..46b8984 100644 --- a/docs/architecture/development.md +++ b/docs/architecture/development.md @@ -44,13 +44,12 @@ ```json "remoteEnv": { - "ANTHROPIC_API_KEY": "${localEnv:ANTHROPIC_API_KEY}", "OPENAI_API_KEY": "${localEnv:OPENAI_API_KEY}", - "GOOGLE_GENERATIVE_AI_API_KEY": "${localEnv:GOOGLE_GENERATIVE_AI_API_KEY}" + "OPENAI_URL": "${localEnv:OPENAI_URL}" } ``` -ホスト側にキーが無ければ空文字が入るだけで、コンテナは問題なく起動します。使わないプロバイダのキーは設定しなくて構いません。 +ホスト側にキーが無ければ空文字が入るだけで、コンテナは問題なく起動します(空文字は「未設定」として扱われます)。LLM の宛先は `OPENAI_URL` の互換サーバ1つなので、鍵もこの1本だけです。 `~/.aws` `~/.ssh` `~/.config/gh` は読み取り専用でバインドマウントされます。 @@ -137,7 +136,7 @@ test('GET /api/health responds ok', async () => { 全ジョブが `~/.bun/install/cache` をキャッシュし、`bun install --frozen-lockfile --ignore-scripts` で入れます。 -デプロイに必要な GitHub Secrets は `CLOUDFLARE_API_TOKEN` と `CLOUDFLARE_ACCOUNT_ID` です。**LLM の APIキーは渡しません。** `wrangler secret put ANTHROPIC_API_KEY` で登録済みである前提です。`wrangler.jsonc` の `vars` にはプロバイダ選択とレート制限値だけを置き、シークレットは入れません。 +デプロイに必要な GitHub Secrets は `CLOUDFLARE_API_TOKEN` と `CLOUDFLARE_ACCOUNT_ID` です。**LLM の APIキーは渡しません。** `wrangler secret put OPENAI_API_KEY` で登録済みである前提です。向き先(`OPENAI_URL`)も secret 側に置きます。互換サーバの所在を公開設定に晒さないためです。`wrangler.jsonc` の `vars` に置くのはターン数・レート制限・保持日数だけです。 コミットメッセージは Conventional Commits です(`.commitlintrc.yaml`)。ローカルで CI を動かしたいときは Dev Container に入っている `act` が使えます(`.actrc` あり)。 @@ -169,4 +168,4 @@ LLMを使うサービスは、コスト可視化を後回しにすると必ず | LLMトレース | Langfuse または Braintrust | | プロダクト分析 | PostHog | -現状 Workers の `observability.enabled: true` によるログのみで、`messages.usage` にトークン使用量を記録する土台だけが入っています。 +現状 Workers の `observability.enabled: true` によるログのみで、`llm_usages` に呼び出しごとのトークン使用量を記録する土台だけが入っています。 diff --git a/docs/architecture/llm.md b/docs/architecture/llm.md index a63829b..1da32d2 100644 --- a/docs/architecture/llm.md +++ b/docs/architecture/llm.md @@ -12,52 +12,62 @@ AlibAI は LLM を1つの用途で使いません。求められる性質が違 Actor と Judge を分けるのが要点です。1つのモデルに「キャラを演じつつゲーム進行も管理して」と頼むと、演技が崩れるか判定が曖昧になります。役者は役者に徹してもらい、進行管理はゲームエンジンが構造化出力で受け取ります。 -この3つはプロバイダ設定の単位でもあります。これとは別に、探偵の質問を組み立てる **Interviewer**(`src/server/llm/interviewer.ts`)がありますが、求められる性質が Actor と同じなので `actor` ロールの設定に相乗りします。設定の単位ではなく、プロンプトの立場だけが違うものとして扱ってください。 +この3つはモデル設定の単位でもあります。これとは別に、探偵の質問を組み立てる **Interviewer**(`src/server/llm/interviewer.ts`)がありますが、求められる性質が Actor と同じなので `actor` ロールの設定に相乗りします。設定の単位ではなく、プロンプトの立場だけが違うものとして扱ってください。 -## Vercel AI SDK によるプロバイダ抽象 +## Vercel AI SDK と OpenAI互換サーバ -`ai@^5` と各社アダプタ(`@ai-sdk/anthropic` / `@ai-sdk/openai` / `@ai-sdk/google`)で3社を吸収します。`src/server/llm/provider.ts` が「役割 → モデル」の解決だけを担当し、呼び出し側(`actor.ts` / `judge.ts`)はプロバイダを知りません。 +`ai@^5` と `@ai-sdk/openai` を使います。宛先は `OPENAI_URL` が指す OpenAI互換サーバ1つです。以前は3社(Anthropic / OpenAI / Google)のネイティブSDKを env で選び分けていましたが、その分岐は残っていません。**「プロバイダ」という概念は型からも env からも画面からも消えています。** 残っているのは役割とモデルIDだけです。 + +`src/server/llm/provider.ts` が2段に分かれています。`chooseLlm` が「役割 → モデルID」を決め、`resolveModel` が「モデルID → SDKクライアント」を組みます。分けてあるのは、プレイヤーが画面からモデルを差し替えられるので、決定を**リクエストごとに一度だけ**行って以降は同じ値を配り回すためです。役割から二度引くと、片方だけ差し替わって食い違います。 ```typescript -export const resolveModel = (env: Env, role: LlmRole): LanguageModel => { - const config = configOf(env, role) - const modelId = config.model === undefined ? DEFAULT_MODELS[config.provider][role] : config.model - - switch (config.provider) { - case 'anthropic': return createAnthropic({ apiKey: env.ANTHROPIC_API_KEY })(modelId) - case 'openai': return createOpenAI({ apiKey: env.OPENAI_API_KEY })(modelId) - case 'google': return createGoogleGenerativeAI({ apiKey: env.GOOGLE_GENERATIVE_AI_API_KEY })(modelId) - } -} +export const resolveModel = (env: Env, modelId: string): LanguageModel => + createOpenAI({ apiKey: env.OPENAI_API_KEY, baseURL: env.OPENAI_URL }).chat(modelId) ``` +`OPENAI_API_KEY` は鍵を要求しないサーバでも入れてください。SDK が Authorization ヘッダを無条件に組み立てるので、未設定だとそこで失敗します。 + +向き先はクライアントから差し替えられません。公開された画面から変えられると、攻撃者が自分のサーバを指定するだけで、Worker がそこへ APIキーを添えて送ってしまいます。 + +`.chat` を明示しています。省くと `@ai-sdk/openai` v2 は Responses API(`/responses`)へ行きますが、OpenAI互換を名乗るサーバが出しているのは大抵 `/chat/completions` だけで、既定のままだと 404 になります。 + `env` を引数で受け取るのが重要です。Workers の isolate はグローバルスコープにシークレットを持たないため、モジュールのトップレベルで env を読んだりクライアントを組み立てたりすると、デプロイした瞬間に起動しなくなります。全てリクエストスコープに降ろしています。 -プロバイダのクライアントはただのファクトリなので、リクエストごとに作っても実質コストはありません。isolate をまたいで使い回そうとするより、毎回作るほうが安全で読みやすいという判断です。 +クライアントはただのファクトリなので、リクエストごとに作っても実質コストはありません。isolate をまたいで使い回そうとするより、毎回作るほうが安全で読みやすいという判断です。 ### 役割ごとの既定モデル -| 役割 | Anthropic | OpenAI | Google | -| --- | --- | --- | --- | -| Actor | `claude-sonnet-5` | `gpt-5.6-terra` | `gemini-3.5-flash` | -| Judge | `claude-haiku-4-5` | `gpt-5.6-luna` | `gemini-3.1-flash-lite` | -| Author | `claude-opus-5` | `gpt-5.6-sol` | `gemini-3.1-pro` | +| 役割 | 既定モデル | +| --- | --- | +| Actor | `gpt-5.6-terra` | +| Judge | `gpt-5.6-luna` | +| Author | `gpt-5.6-sol` | + +> `db/llm-catalog.ts` の `LLM_DEFAULT_MODELS` です。env にも設定にも何も無いときの最後の拠りどころであって、正典ではありません。互換サーバがこのIDを持っている保証はないので、構成に合わせて上書きしてください。 + +**選べるモデルの一覧は手で持ちません。** 以前は `db/llm-catalog.ts` にプロバイダごとの選択可能モデルを並べた表(`LLM_CATALOG`)がありましたが、宛先が互換サーバ1つになった時点で捨てました。実在するモデルはそのサーバに聞きます(`src/server/llm/models.ts` が `/v1/models` を引き、`GET /api/settings/llm` が設定画面へ渡します)。手で書いた表を持つと、サーバの構成を変えるたびに実態とずれ、しかもずれたことに気づけません。 -> モデルIDと料金は各社とも改定が速いです。上表は `DEFAULT_MODELS` の既定値であって正典ではありません。採用前に各社の公式ドキュメントで確認してください。特に OpenAI / Google の型番は二次情報を元にしているため、実装前の検証が必須です。 +そのため、プレイヤーが指定したモデルIDを突き合わせる許可リストもありません。長さだけ見て通し、互換サーバが知らないIDならそこでエラーになります。400 を返さないのは、`localStorage` に古いIDが残っているだけのプレイヤーを事件の途中で締め出すことになるためです。 + +> この画面は認証を持たないので、互換サーバに載せたモデルは誰でも指名できます。高いモデルを載せるなら、レート制限(`RATE_LIMIT_MAX_CALLS`)で殴られる量が上限になることを踏まえて決めてください。 ### 切り替え -環境変数だけで済みます。役割ごとに別プロバイダを選べるので、混成構成も設定だけで組めます。 +環境変数だけで済みます。役割ごとに別のモデルを当てられるので、「会話は賢いモデル、判定は安いモデル」といった組み合わせが設定だけで組めます。ただし宛先は `OPENAI_URL` 一つなので、選んだモデルがその互換サーバに生えていることが前提です。 ```bash -LLM_ACTOR_PROVIDER=anthropic # anthropic | openai | google -LLM_JUDGE_PROVIDER=google # actorはClaude、judgeはGeminiのFlash系、も可 -LLM_AUTHOR_PROVIDER=openai +OPENAI_URL=https://example.internal/v1 # 末尾は /v1 +OPENAI_API_KEY= # 鍵の要らないサーバでも何か入れる -# 個別にモデルIDを上書きしたい場合 +# 役割ごとのモデル。未設定なら上の既定表。突き合わせる表を持たないので、 +# 互換サーバ独自のモデル名もそのまま書けます。 LLM_ACTOR_MODEL= +LLM_JUDGE_MODEL= +LLM_AUTHOR_MODEL= ``` +画面(設定)から選ばせるのは `actor`(会話)と `judge`(判定)の2つだけです。`author` は Worker からの呼び出しが一つも無く、使うのはオフラインの `db/generate-scenario.ts` だけなので、ブラウザで切り替えても何も起きません。 + ### 選び方の目安 - **Actor のコストが総額の大半を占めます。** ここを1段安いモデルにできるかが最大のレバーです。ただしキャラ崩れは体験に直撃するので、安易に下げず必ずキャラ一貫性の評価にかけてください。 @@ -70,14 +80,9 @@ LLM_ACTOR_MODEL= ```typescript streamText({ - model: resolveModel(env, 'actor'), - messages: [ - { role: 'system', content: gameRules, providerOptions: cacheHint(env, 'actor') }, - { role: 'system', content: characterSheet, providerOptions: cacheHint(env, 'actor') }, - ...buildDetectiveMessages(detective), - ...history, - { role: 'user', content: question }, - ], + model: resolveModel(env, modelId), + system: [gameRules, characterSheet, ...detectiveBlocks(detective)].join('\n\n'), + messages: [...history, { role: 'user', content: question }], maxOutputTokens: 1024, }) ``` @@ -98,7 +103,7 @@ streamText({ ### 探偵の人物像 -プレイヤーが演じる探偵(`src/server/llm/detective.ts`)を、キャラクターシートの直後・履歴の前に置きます。セッション開始時に決まったら変わらないので、プレフィックスを壊しません。`cacheHint` は付けません。数行しかなく最小キャッシュ長に届かないので、ブレークポイントを1つ捨てるだけになります。 +プレイヤーが演じる探偵(`src/server/llm/detective.ts`)を、キャラクターシートの直後・履歴の前に置きます。セッション開始時に決まったら変わらないので、プレフィックスを壊しません。 ここには人物像に加えて**呼びかけの候補**を書きます。設定を並べるだけでは、モデルはどの相手にも同じ調子で喋るからです。 @@ -165,60 +170,34 @@ Judge は往復ごとではなく**話題ごとに1回**呼びます。往復ご 会話型ゲームは、毎ターン同じNPC定義(人格・知識・秘密・記憶)を送り直す構造です。20ターンの尋問なら、同じキャラクターシートを20回送ることになります。キャッシュが効けばこの部分が大幅に安くなり、効かなければ全額です。 -### ここだけは抽象化の裏に隠せない +### 明示指定は持っていません -挙動が3社で違います。 +以前は「ブロックに `cache_control` を付ける」方式のモデル向けに、`cacheHint` という関数でその指定を差し込んでいました。互換API一本にした時点で外しています。`createOpenAI` で組んだモデルにベンダ固有の `providerOptions` を渡しても、AI SDK 側で誰も読まないため黙って捨てられるからです。エラーは出ず、請求だけが変わる——動かない指定をコードに残すほうが害が大きいと判断しました。 -| プロバイダ | 方式 | 実装側でやること | -| --- | --- | --- | -| Anthropic | 明示的。`cache_control` をブロックに付ける | **付け忘れると効かない。** ブレークポイント設計が必要 | -| OpenAI | 自動。共通プレフィックスがあれば効く | 設定不要。プレフィックスを壊さないことだけ意識する | -| Google | 暗黙キャッシュが自動。明示キャッシュは別API | 基本は自動任せ | +いま頼っているのは、共通プレフィックスがあれば自動で効くタイプのキャッシュだけです。互換サーバが裏でどのベンダを叩いていても、明示的なキャッシュ指定を通せるかどうかはそのサーバ次第で、ここからは指定できません。**明示指定が要るモデルへ寄せるなら、そのモデルだけネイティブSDKに戻す判断が要ります。** -抽象レイヤの裏に隠すと、プロバイダを切り替えた瞬間にコストが跳ねます。そのため `cacheHint(env, role)` を関数として表に出し、Anthropic 選択時だけ明示指定を差し込みます。 - -```typescript -export const cacheHint = (env: Env, role: LlmRole): ProviderOptions => - providerOf(env, role) === 'anthropic' - ? { anthropic: { cacheControl: { type: 'ephemeral' } } } - : {} -``` - -### 3社共通で守るべき設計ルール +### 守るべき設計ルール **1. system prompt を凍結する。** -「現在のターン数」「経過時間」「発見済み証拠リスト」を system に埋め込まないこと。これらは毎ターン変わるので、先頭に入れた瞬間にキャッシュミスします。動的な状態は必ずメッセージ列の後方に置きます。どのプロバイダもプレフィックス一致なので、これは共通です。 +「現在のターン数」「経過時間」「発見済み証拠リスト」を system に埋め込まないこと。これらは毎ターン変わるので、先頭に入れた瞬間にキャッシュミスします。動的な状態は必ずメッセージ列の後方に置きます。プレフィックス一致で効くタイプのキャッシュしか使っていないので、ここが唯一のレバーです。 -**2. ブレークポイントは安定性の境界に置く(Anthropic)。** +**2. 前置きは安定している順に並べる。** - 1つ目: 全シナリオ共通のゲームルール(最も安定) - 2つ目: NPCのキャラクターシート(そのNPCとの会話中は不変) -- 3つ目: 直近ターンの最終コンテンツブロック(会話履歴を累積キャッシュ) - -ブレークポイントは1リクエスト最大4つまでです。 +- 3つ目以降: 探偵、会話履歴 -**3. 最小キャッシュ長に注意する(Anthropic)。** -これを下回ると、`cache_control` を付けてもエラーにならず静かにキャッシュされません。 +前置きは system オプションに一本の文字列としてまとめます。以前は messages の先頭に system ロールで積んでいましたが、互換サーバが Anthropic へ中継する構成だと 400 になります(あちらは system を最上位でしか受けません)。ブロックに分けていた理由(`cache_control` をブロック単位で打つため)はもう無いので、順序さえ保てば足ります。 -| モデル | 最小キャッシュ長 | -| --- | --- | -| `claude-opus-5` | 512 tokens | -| `claude-sonnet-5` | 1,024 tokens | -| `claude-haiku-4-5` | 4,096 tokens | - -Judge 用のプロンプトは 4,096 トークン未満になりがちです。キャッシュしたいなら共通の判定ルールを厚めに書いて閾値を超えさせるか、割り切って諦めます。現状 `judge.ts` は `cacheHint` を使っていません。 - -**4. キャッシュはモデル単位・NPC単位で分かれる。** +**3. キャッシュはモデル単位・NPC単位で分かれる。** 同一シナリオでもNPCが違えばキャッシュは別物です。1プレイ中に何度も同じNPCへ聞き直す設計は、キャッシュ効率の面でも有利になります。 -**5. 必ず計測する。** -キャッシュ読み込みトークンが0のまま増えないなら、どこかに破壊要因があります。タイムスタンプ、UUID、JSONのキー順不定、条件分岐による system 文の差分などを疑ってください。 - -Anthropic の既定 TTL は5分です。1回のプレイが約10分なので、テンポの良いプレイなら5分で十分回りますが、じっくり考えるプレイヤーは会話間隔が空きます。1時間TTLは書き込みコストが上がるので、実データでヒット率を見てから判断します。 +**4. 必ず計測する。** +キャッシュ読み込みトークンが0のまま増えないなら、どこかに破壊要因があります。タイムスタンプ、UUID、JSONのキー順不定、条件分岐による system 文の差分などを疑ってください。互換サーバがそもそもキャッシュを持たない可能性もあるので、0が続くなら実装より先にサーバ側を確かめてください。 ### コスト集計 -`messages.usage` に記録するのは通常の入出力トークンだけでなく、**キャッシュ書き込み量とキャッシュ読み込み量**です。入力トークンの総量は「非キャッシュ入力 + キャッシュ書き込み + キャッシュ読み込み」であって、非キャッシュ入力の単体ではありません。ここを取り違えると、キャッシュが効いているのに「トークンが少ない」と誤読します。 +トークン消費は `llm_usages` に1呼び出し1行で記録します(`messages` 側には持ちません)。記録するのは通常の入出力トークンだけでなく、**キャッシュ書き込み量とキャッシュ読み込み量**です。入力トークンの総量は「非キャッシュ入力 + キャッシュ書き込み + キャッシュ読み込み」であって、非キャッシュ入力の単体ではありません。ここを取り違えると、キャッシュが効いているのに「トークンが少ない」と誤読します。 1プレイあたりのコストは「ターン数 × (キャッシュ読み込み + 新規入力 + 出力)」で見積もり、ローンチ前に必ず実測してください。キャッシュ設計が効いているかどうかで一桁変わります。実際の積み上げ計算は [cost.md](./cost.md) にあります。 @@ -258,7 +237,7 @@ Anthropic の既定 TTL は5分です。1回のプレイが約10分なので、 最大の防御は「そもそも真相を Actor に渡さない」ことです。ここで単語を並べて守りを固めたつもりになるより、 ゲームが壊れないことを優先します。 -インジェクションテストは CI に入れてください。プロンプトを1行変えただけで防御が崩れることがあるので、人力レビューでは守り切れません。**プロバイダを切り替え可能にした以上、評価は3社分回す必要があります。** Anthropic で漏れなかったプロンプトが Gemini で漏れることは普通にあります。 +インジェクションテストは CI に入れてください。プロンプトを1行変えただけで防御が崩れることがあるので、人力レビューでは守り切れません。**モデルを差し替え可能にした以上、評価は使う候補モデルごとに回す必要があります。** あるモデルで漏れなかったプロンプトが別のモデルで漏れることは普通にあります。互換サーバに載せるモデルを入れ替えるときも同じです。 ## 入力の制約 diff --git a/docs/architecture/runtime.md b/docs/architecture/runtime.md index 9fb1e1e..a6fb528 100644 --- a/docs/architecture/runtime.md +++ b/docs/architecture/runtime.md @@ -80,7 +80,7 @@ const app = new Hono<{ Bindings: Bindings; Variables: { env: Env } }>() `src/server/env.ts` が Zod スキーマで環境変数を検証します。バインディングは isolate の中で不変なので、`src/server/middleware/env.ts` が一度検証した結果を使い回します。リクエストごとに parse をやり直すと、10分の体験の中で何十回も同じ検証を繰り返すことになります。 -APIキーは全て `optional` です。3社のうち使うプロバイダの分だけあればよく、未使用プロバイダのキーが無くても起動します。 +LLM 関係の項目(`OPENAI_URL` / `OPENAI_API_KEY` / `LLM_*_MODEL`)は全て `optional` です。設定が無くても起動し、聞き込みに入って初めて足りないと分かる形にしています。設定画面は鍵と向き先が揃っているときだけモデルの一覧を出し、揃っていなければ空配列を返して「選択肢が出ない」と説明します。 ## ストリーミング @@ -115,7 +115,7 @@ return streamSSE(c, async (stream) => { 画面のルーティングは TanStack Start(`@tanstack/react-start`)が担い、`src/routes/` のディレクトリ構造から `src/routeTree.gen.ts` を生成します。生成物は手で触らず、`biome.json` の `!**/*.gen.ts` で検査からも外してあります。アセットに一致しないパスは Worker に落ちて SSR で応答するので、`not_found_handling`(SPAフォールバック)は使いません。 -dev サーバは `.env` を読んでシークレットとして Worker に渡します。未使用のプロバイダのキーを `KEY=` と空文字で残しても落ちないよう、`env.ts` 側で空文字は未設定として扱っています。 +dev サーバは `.env` を読んでシークレットとして Worker に渡します。使わない項目を `KEY=` と空文字で残しても落ちないよう、`env.ts` 側で空文字は未設定として扱っています。 ### TypeScript diff --git a/docs/architecture/scenario-format.md b/docs/architecture/scenario-format.md index 6fa1e97..6e31585 100644 --- a/docs/architecture/scenario-format.md +++ b/docs/architecture/scenario-format.md @@ -313,6 +313,36 @@ characters: `publicIntroduction` はプレイヤーへ最初から公開する短い人物紹介です。職業・立場・表向きの性格だけを書き、秘密・動機・未公開の目撃・アリバイ・嘘・違反行為など、聞き込みで判明すべき情報を含めません。`personality` と `relationships` はActor専用の非公開情報です。 +### `ageGroup` / `gender` + +任意です。探偵と同じ列挙を使い、正典は `db/person.ts` にあります。 + +- `ageGroup`: `child` / `teen` / `young` / `adult` / `senior` / `elder` / `unknown` +- `gender`: `male` / `female` / `other` / `unknown` + +```yaml +ageGroup: senior +gender: female +``` + +書かなければ `unknown` になり、キャラクターシートにも出ません。そのときは今までどおり `personality` の文章が年ごろと性別を伝えます。職業の項目はありません。「投影技師」「売店責任者」のように列挙へ収まらず、`publicIntroduction` と `personality` が既にその役をしているためです。 + +### `voiceSeed` / `voiceCaption` + +任意です。[Irodori-TTS](https://github.com/Aratako/Irodori-TTS) へ渡す声の指定で、両方書かなければその人物は喋りません。 + +```yaml +voiceSeed: 1234 +voiceCaption: 落ち着いた女性の声で、近い距離感でやわらかく話す。 +``` + +- `voiceSeed`: 合成のシード(0以上の整数)。固定しないと台詞ごとに別人の声になります。`voiceCaption` を書いて `voiceSeed` を書かないと検証で落ちます。 +- `voiceCaption`: そのキャラの**声質**を書いた一文(200字まで)。モデルはこの日本語文から声を作ります。 + +`voiceCaption` に場面ごとの感情を書かないでください。「声が震えており、悲痛なトーンで」のような指定は喋らせる側が実行時に足して繋ぐもので、ここへ書くとどの台詞も同じ感情で読まれます。 + +この2つはキャラクターシート(Actorへ渡すプロンプト)には入りません。合成を呼ぶ側だけが読みます。 + `knowledge` は原則 `facts[].id` の参照です。 ### `secrets` diff --git a/docs/scenario-authoring.md b/docs/scenario-authoring.md index d954151..68e9a9d 100644 --- a/docs/scenario-authoring.md +++ b/docs/scenario-authoring.md @@ -248,6 +248,7 @@ evidences: characters: - id: fukagawa name: 深川誠也 + shortName: 深川 # 省略可。8字まで。アリバイ表の帯に出る publicIntroduction: 月見荘の経理を長年任されている税理士。 # プレイヤーへ最初から公開 personality: 気弱で愛想笑いが多い税理士。追い詰められると目が泳ぐ。 # NPC内部用、非公開 goals: @@ -318,6 +319,10 @@ evidences: 場所を調べるのも質問1回ぶんを使います。三択になるぶん、場所を増やしすぎると聞き込みが痩せるので、 事件の芯に関わるものだけに絞ってください。遺体と同じで、場所は**逃げも黙秘もしない情報源**です。 +`shortName` は幅の狭いところ——アリバイ表の帯や名札——へ出す短い名前です。姓だけ、渾名だけ。 +省略できます(書かなければ `name` がそのまま入ります)。姓を機械的に切り出すことはしません。 +一文字姓や外国名では切る位置が決まらないので、書く人が決めてください。 + ### `publicIntroduction` と `personality` `publicIntroduction` は**プレイヤーへ事件開始前から見せる人物紹介**です。名前と一緒に概要画面・聞き込み画面へ表示されます。 diff --git a/mocks/desktop/_desk.css b/mocks/desktop/_desk.css index 94d6a7a..6ec8382 100644 --- a/mocks/desktop/_desk.css +++ b/mocks/desktop/_desk.css @@ -704,19 +704,79 @@ select { grid-template-columns: repeat(2, 1fr); gap: 18px; } + +/* ---- 押せるものが触れられたとき ---- */ +/* + * 行と同じことをボタンにも言う。ただし枠を持っているので、左に一本足すと + * 枠の内側にもう一本増えて濁る。ボタンでは枠と字のほうを起こす。 + * 地を上げる幅は行と揃えて --sumi-2 まで。 + * + * 朱の一手だけ色を替えない。取り消せない一手が、触れただけで他の操作と + * 同じ顔になると、押す前の身構えがひとつ抜ける。 + * + * 字だけの操作(戻る・やり直す)には起こすものが字しかないので、字だけ上げる。 + */ +@media (hover: hover) { + .go:not(.off):hover { + background: var(--sumi-2); + border-color: var(--kinari); + } + .go.final:hover { + border-color: var(--shu); + } + .desk > .top .act:hover, + .askbar .btn:hover, + .res .foot span:hover { + background: var(--sumi-2); + border-color: var(--nezumi); + color: var(--kinari); + } + .desk > .top .back:hover, + .foot .again:hover, + .brief .skip:hover { + color: var(--kinari); + } +} .foot .pair .go { letter-spacing: 0.12em; font-size: 14px; } +/* ---- 押せる行の左端に立つ一本 ---- */ +/* + * 机の上では指が触れないので、押せる行かどうかを静止画から読み取れない。 + * 触れているあいだだけ左端に一本立てて、そこが押せることを言う。 + * + * 選ばれた行の線と同じ位置・同じ太さで引き、明るさだけを変える—— + * 迷っているあいだの線と、決めたあとの線が別物に見えると、 + * 触れて動いた一本が「選べた」のか「選んだ」のか分からなくなる。 + * + * 枠ではなく内側の影で引く。枠だと線が出た瞬間に行の字が右へずれる。 + * 行の側に左の余白を用意しておくのも同じ理由で、線と字を重ねないため。 + * 顔料を持つ行(人物・場所)はその人の色で、持たない行は鼠で引く。 + * + * 地も一段だけ起こす。線だけだと行のどこまでが一枚なのかが分からず、 + * 二列に畳んだ名簿では隣の列の行を触っているように見える。値は聞き込み中の列 + * (.chart .col.now)と同じ --sumi-2 で、これ以上は上げない——濃くすると + * 触れた行が箱になり、罫線で区切ってきた画面のなかで一行だけ札になる。 + * + * 指の台では hover が押したあと張り付くので、ポインタのある台だけに出す。 + */ + /* ---- 名簿(登場人物) ---- */ .cast .r { display: flex; align-items: center; gap: 12px; - padding: 10px 0; + padding: 10px 0 10px 12px; border-bottom: 1px solid var(--keisen); } +@media (hover: hover) { + .cast .r:not([disabled]):hover { + box-shadow: inset 2px 0 0 currentColor; + background: var(--sumi-2); + } +} .cast .r:first-child { border-top: 1px solid var(--keisen); } diff --git a/mocks/desktop/accusation.html b/mocks/desktop/accusation.html index d65166d..0d053b7 100644 --- a/mocks/desktop/accusation.html +++ b/mocks/desktop/accusation.html @@ -32,6 +32,11 @@ .suspects button + button { border-left: 1px solid var(--keisen); } /* 選ばれた一人だけ、下辺を朱に替える。塗り足しも枠も足さない。 */ .suspects button.on { border-bottom-color: var(--shu); } + /* 触れているあいだは左端にその人の顔料で一本(語彙は _desk.css)。 + 指名の印は下辺なので、軸が違って喧嘩しない。 */ + @media (hover: hover) { + .suspects button:hover { box-shadow: inset 2px 0 0 currentColor; background: var(--sumi-2); } + } .suspects .face { width: 30px; height: 30px; border-radius: 50%; flex: none; display: grid; place-items: center; background: var(--sumi-2); diff --git a/mocks/desktop/detective.html b/mocks/desktop/detective.html index 3058063..2a89141 100644 --- a/mocks/desktop/detective.html +++ b/mocks/desktop/detective.html @@ -60,9 +60,18 @@ /* 選択は塗らずに左端の一本で示す。行の頭は全行そろえたいので、枠ではなく 内側の影で引く——枠だと選ばれた行だけ字が右へずれる。 + 触れているあいだも同じ位置に一本立てるが、そちらは鼠。探偵は顔料を + 持たないので、明るさだけで「選べる」と「選んだ」を分ける。 */ .roster .r { align-items: flex-start; padding-left: 14px; } .roster .r.on { box-shadow: inset 2px 0 0 var(--kinari); } + @media (hover: hover) { + .roster .r:not(.on):hover { box-shadow: inset 2px 0 0 var(--nezumi); background: var(--sumi-2); } + /* 行の中の小さな操作と、字だけの操作。起こすものが字しかない。 */ + .roster .acts span:hover, .add:hover, .mirror .edit:hover, .det > .lt > .back:hover { + color: var(--kinari); + } + } .roster .pick { display: flex; flex-direction: column; min-width: 0; background: none; border: 0; padding: 0; text-align: left; diff --git a/mocks/desktop/scenario-select.html b/mocks/desktop/scenario-select.html index 5ccf392..7095b17 100644 --- a/mocks/desktop/scenario-select.html +++ b/mocks/desktop/scenario-select.html @@ -45,10 +45,16 @@ } .case { display: flex; flex-direction: column; gap: 2px; align-items: flex-start; - padding: 13px 0; text-align: left; + padding: 13px 0 13px 12px; text-align: left; background: none; border: 0; border-bottom: 1px solid var(--keisen); color: inherit; font: inherit; text-decoration: none; } + /* 触れている一件だけ左端に線を立てる(語彙は _desk.css の「押せる行の左端に立つ一本」)。 + 事件は顔料を持たないので鼠で引く。 */ + @media (hover: hover) { + .case:hover { box-shadow: inset 2px 0 0 var(--nezumi); background: var(--sumi-2); } + .masthead .rgt .set:hover { color: var(--kinari); } + } .case .cat { font-size: 10px; color: var(--nezumi-dim); letter-spacing: 0.16em; line-height: 1.5; } .case .ttl { font-family: var(--mincho); font-weight: 500; font-size: 15px; line-height: 1.5; letter-spacing: 0.03em; } .case .meta { font-size: 11px; color: var(--nezumi-dim); line-height: 1.5; } diff --git a/mocks/desktop/settings.html b/mocks/desktop/settings.html index 94a95b3..5adc7bf 100644 --- a/mocks/desktop/settings.html +++ b/mocks/desktop/settings.html @@ -16,6 +16,9 @@ /* 数は打ち替えられる。枠と高さは _desk.css の .num のまま借りる。 */ .set .num { background: none; width: 100%; } .set .back { text-decoration: none; } + @media (hover: hover) { + .set .back:hover { color: var(--kinari); } + } @@ -31,7 +34,7 @@

設定

使うモデル
-

Google は APIキーが未設定のため選べません。

+

@@ -51,6 +54,17 @@

設定

+ +
+ 判定の調整 +

判定役の振る舞いを一つずつ試せます。既定はすべてオフで、入れたものだけ判定の出方が変わります。

+
+
+ +
+ 聞き込み +
+
+
読み上げ
+
読み終わるまで次の行を待ちます
+
+
+
+

読み上げないときは、これまで通り一定の間で流れます。

+
@@ -83,16 +114,27 @@

設定

;(function () { // 設定は物語の外の値なので、事件データではなくここに素で置く。 var unset = Mock.str('unset', '0') === '1' + /* + 互換サーバが返すモデルIDは「提供元,モデル」のカンマ区切り。 + 欄を二つに割るのは、一覧が全提供元ぶん一列に並ぶと二十行を超えるため。 + 並べる値はここで割ったもので、保存されるのは連結し直した一つの文字列。 + */ + var nomodels = Mock.str('nomodels', '0') === '1' + var chosen = unset || nomodels ? null : 'claude-code,claude-opus-5' var roles = [ { name: '会話', note: 'NPCの受け答えと、探偵が組み立てる質問', - provider: unset ? null : 'Anthropic', - model: unset ? null : 'Claude Sonnet 5', + provider: chosen && chosen.split(',')[0], + model: chosen && chosen.split(',')[1], }, { name: '判定', note: '証拠の開示と、推理の採点', provider: null, model: null }, ] + document.getElementById('nomodels').textContent = nomodels + ? 'モデルの一覧が空です。APIキーが未設定か、モデルサーバに繋がっていません。' + : '' + // 提供元が決まるまでモデルは触れない。枠を地に沈めて、押せないことを枠で言う。 var cell = function (value, locked, id) { var cls = value ? 'sel2' : locked ? 'sel2 locked' : 'sel2 unset' @@ -183,16 +225,70 @@

設定

} choices('brief', [{ key: 'typewriter', name: '一段落ずつ' }, { key: 'crawl', name: 'せり上がる' }], brief, true) choices('sound', [{ key: 'on', name: '鳴らす' }, { key: 'off', name: '鳴らさない' }], sound, pickable) + choices( + 'voice', + [{ key: 'on', name: '読み上げる' }, { key: 'off', name: '読み上げない' }], + Mock.str('voice', 'on'), + true, + ) document.getElementById('brieffine').textContent = pickable ? '一段落ずつは、読む速さをあなたが握ります。' : 'せり上がるは速さが決まっているぶん、音は鳴りません。' + /* + 判定の直しは四つとも入切だけ。名前と説明の出どころは db/judge-tuning.ts で、 + ここはその写し——増減したときは向こうを直してからここへ写す。 + */ + var JUDGE = [ + { key: 'checkEvidenceIds', name: '証拠のIDを確かめる', note: '実在しない証拠や、すでに見つけた証拠を弾く' }, + { + key: 'contradictionNeedsHistory', + name: '矛盾の判定に前のやり取りを渡す', + note: '前の発言と食い違っているかを、実際に読み比べて決める', + }, + { key: 'fixedTemperature', name: '判定の揺れを抑える', note: '同じやり取りなら毎回同じ判定になるようにする' }, + { key: 'retryOnce', name: '判定が落ちたらやり直す', note: '一度だけやり直す。落ちた回は発見が丸ごと消えるため' }, + ] + + document.getElementById('judge').innerHTML = JUDGE.map(function (t) { + return ( + '
' + + '
' + + Mock.esc(t.name) + + '
' + + Mock.esc(t.note) + + '
' + + '
' + ) + }).join('') + + JUDGE.forEach(function (t) { + choices( + 'judge-' + t.key, + [{ key: 'on', name: '入れる' }, { key: 'off', name: '入れない' }], + Mock.str(t.key, 'off'), + true, + ) + }) + Array.prototype.forEach.call(document.querySelectorAll('[data-set]'), function (el) { el.addEventListener('click', function () { var kv = el.dataset.set.split('=') - location.hash = - 'brief=' + (kv[0] === 'brief' ? kv[1] : brief) + '&sound=' + (kv[0] === 'sound' ? kv[1] : sound) + // 判定の欄は id が judge- 付き。ハッシュへ書くときは接頭辞を外す。 + var name = kv[0].indexOf('judge-') === 0 ? kv[0].slice(6) : kv[0] + var state = { brief: brief, sound: sound } + JUDGE.forEach(function (t) { + state[t.key] = Mock.str(t.key, 'off') + }) + state[name] = kv[1] + location.hash = Object.keys(state) + .map(function (k) { + return k + '=' + state[k] + }) + .join('&') location.reload() }) }) diff --git a/mocks/mobile/settings.html b/mocks/mobile/settings.html index b640d58..a635b27 100644 --- a/mocks/mobile/settings.html +++ b/mocks/mobile/settings.html @@ -31,7 +31,7 @@

設定

使うモデル
-

Google は APIキーが未設定のため選べません。

+

@@ -56,6 +56,16 @@

設定

+ +
+
判定の調整
+

判定役の振る舞いを一つずつ試せます。既定はすべてオフで、入れたものだけ判定の出方が変わります。

+
+
+
事件の記録
@@ -70,6 +80,17 @@

設定

+ + +
+
聞き込み
+
+
読み上げ
+
読み終わるまで次の行を待ちます
+
+
+

読み上げないときは、これまで通り一定の間で流れます。

+
@@ -81,16 +102,27 @@

設定

// 選んだ状態とを、見た目でも分けられるかを確かめるための切り替え。 var unset = Mock.str('unset', '0') === '1' + /* + 互換サーバが返すモデルIDは「提供元,モデル」のカンマ区切り。欄を二つに割るのは、 + 一覧が全提供元ぶん一列に並ぶと二十行を超えるため。並べる値はここで割ったもので、 + 保存されるのは連結し直した一つの文字列。 + */ + var nomodels = Mock.str('nomodels', '0') === '1' + var chosen = unset || nomodels ? null : 'claude-code,claude-opus-5' var roles = [ { nm: '会話', ps: 'NPCの受け答えと、探偵が組み立てる質問', - vendor: unset ? null : 'Anthropic', - model: unset ? null : 'Claude Sonnet 5', + vendor: chosen && chosen.split(',')[0], + model: chosen && chosen.split(',')[1], }, { nm: '判定', ps: '証拠の開示と、推理の採点', vendor: null, model: null }, ] + document.getElementById('nomodels').textContent = nomodels + ? 'モデルの一覧が空です。APIキーが未設定か、モデルサーバに繋がっていません。' + : '' + document.getElementById('roles').innerHTML = roles .map(function (r) { var vendor = r.vendor @@ -173,19 +205,78 @@

設定

sound, pickable, ) + choices( + 'voice', + [ + { key: 'on', name: '読み上げる' }, + { key: 'off', name: '読み上げない' }, + ], + Mock.str('voice', 'on'), + true, + ) document.getElementById('brieffine').textContent = pickable ? '一段落ずつは、読む速さをあなたが握ります。' : 'せり上がるは速さが決まっているぶん、音は鳴りません。' + /* + 判定の直しは四つとも入切だけ。名前と説明の出どころは db/judge-tuning.ts で、 + ここはその写し——増減したときは向こうを直してからここへ写す。 + + 狭い画面では名前・説明・入切を縦に積む。打鍵音の行と同じ組みで、 + 説明が操作の直前に来るぶん、名前に貼り付けると一かたまりに見える。 + */ + var JUDGE = [ + { key: 'checkEvidenceIds', name: '証拠のIDを確かめる', note: '実在しない証拠や、すでに見つけた証拠を弾く' }, + { + key: 'contradictionNeedsHistory', + name: '矛盾の判定に前のやり取りを渡す', + note: '前の発言と食い違っているかを、実際に読み比べて決める', + }, + { key: 'fixedTemperature', name: '判定の揺れを抑える', note: '同じやり取りなら毎回同じ判定になるようにする' }, + { key: 'retryOnce', name: '判定が落ちたらやり直す', note: '一度だけやり直す。落ちた回は発見が丸ごと消えるため' }, + ] + + document.getElementById('judge').innerHTML = JUDGE.map(function (t) { + return ( + '
' + + '
' + + Mock.esc(t.name) + + '
' + + Mock.esc(t.note) + + '
' + ) + }).join('') + + JUDGE.forEach(function (t) { + choices( + 'judge-' + t.key, + [ + { key: 'on', name: '入れる' }, + { key: 'off', name: '入れない' }, + ], + Mock.str(t.key, 'off'), + true, + ) + }) + Array.prototype.forEach.call(document.querySelectorAll('[data-set]'), function (el) { el.addEventListener('click', function () { var kv = el.dataset.set.split('=') - location.hash = - 'brief=' + - (kv[0] === 'brief' ? kv[1] : brief) + - '&sound=' + - (kv[0] === 'sound' ? kv[1] : sound) + // 判定の欄は id が judge- 付き。ハッシュへ書くときは接頭辞を外す。 + var name = kv[0].indexOf('judge-') === 0 ? kv[0].slice(6) : kv[0] + var state = { brief: brief, sound: sound } + JUDGE.forEach(function (t) { + state[t.key] = Mock.str(t.key, 'off') + }) + state[name] = kv[1] + location.hash = Object.keys(state) + .map(function (k) { + return k + '=' + state[k] + }) + .join('&') location.reload() }) }) diff --git a/package.json b/package.json index 42f05be..5846842 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "private": true, "type": "module", "scripts": { + "postinstall": "playwright install --only-shell chromium", "dev": "vite --host", "build": "tsc --noEmit && vite build", "preview": "vite preview", @@ -18,29 +19,29 @@ "db:migrate:remote": "wrangler d1 migrations apply alibai --remote", "db:seed": "bun run db/seed.ts", "db:seed:apply": "wrangler d1 execute alibai --local --file=db/seed.sql", + "db:speakers:apply": "wrangler d1 execute alibai --local --file=db/tts-speakers.sql", "db:seed:apply:remote": "wrangler d1 execute alibai --remote --file=db/seed.sql", + "db:speakers:apply:remote": "wrangler d1 execute alibai --remote --file=db/tts-speakers.sql", "db:author": "bun run db/generate-scenario.ts", "se:fetch": "bun run scripts/fetch-se.ts", "deploy": "bun run se:fetch && bun run build && bun run db:migrate:remote && wrangler deploy" }, "dependencies": { - "@ai-sdk/anthropic": "^2", - "@ai-sdk/google": "^2", - "@ai-sdk/openai": "^2", - "@ai-sdk/provider-utils": "^3", + "@ai-sdk/openai": "^2.0.126", + "@ai-sdk/provider-utils": "^3.0.36", "@commitlint/config-conventional": "^21.2.2", - "@tanstack/react-router": "^1.170.32", - "@tanstack/react-start": "^1.168.49", + "@tanstack/react-router": "^1.170.33", + "@tanstack/react-start": "^1.168.50", "@vitejs/plugin-react": "^6.1.1", - "ai": "^5", + "ai": "^5.0.254", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "commitlint": "^21.2.2", "conventional-changelog-conventionalcommits": "^10.4.0", "dayjs": "^1.11.23", "drizzle-orm": "^0.45.2", - "hono": "^4.13.5", - "lucide-react": "^1.38.0", + "hono": "^4.13.7", + "lucide-react": "^1.43.0", "radix-ui": "^1.6.7", "react": "^19.2.8", "react-dom": "^19.2.8", @@ -48,22 +49,22 @@ "zod": "^4.5.4" }, "devDependencies": { - "@biomejs/biome": "^2.5.11", - "@cloudflare/vite-plugin": "^1.54.2", - "@cloudflare/workers-types": "^5.20260831.1", + "@biomejs/biome": "^2.5.12", + "@cloudflare/vite-plugin": "^1.54.6", + "@cloudflare/workers-types": "^5.20260908.1", "@storybook/react-vite": "10.5.10", "@tailwindcss/vite": "^4.3.3", - "@types/bun": "^1.4.0", - "@types/node": "^26.4.0", + "@types/bun": "^1.4.2", + "@types/node": "^26.5.0", "@types/react": "^19.2.18", - "@types/react-dom": "^19.2.5", + "@types/react-dom": "^19.2.7", "drizzle-kit": "^0.31.10", - "playwright": "^1.62.1", + "playwright": "^1.63.0", "storybook": "10.5.10", "tailwindcss": "^4.3.3", "typescript": "^7.0.2", "vite": "^8.2.2", - "wrangler": "^4.127.1", + "wrangler": "^4.130.0", "yaml": "^2.9.0" } } diff --git a/scripts/compare-int-crop.mjs b/scripts/compare-int-crop.mjs new file mode 100644 index 0000000..7e96b0a --- /dev/null +++ b/scripts/compare-int-crop.mjs @@ -0,0 +1,78 @@ +/* + * 聞き込みのモックと実装を、同じ切り取りで上下に並べて撮る。 + * 三枚並びでは字が潰れて読めないので、細部を見るときだけ使う。 + * + * 使い方: bun scripts/compare-int-crop.mjs [x,y,w,h] + */ +import { createServer } from 'node:http' +import { existsSync, mkdirSync, readFileSync } from 'node:fs' +import { extname, join } from 'node:path' +import { chromium } from 'playwright' + +const root = process.cwd() +const dir = join(root, 'storybook-static/intstate') +const mime = { + '.html': 'text/html', + '.js': 'text/javascript', + '.mjs': 'text/javascript', + '.css': 'text/css', + '.json': 'application/json', + '.png': 'image/png', + '.svg': 'image/svg+xml', + '.woff2': 'font/woff2', +} +const { server, url } = await new Promise((resolve) => { + const s = createServer((req, res) => { + const p = decodeURIComponent(req.url.split('?')[0]) + const f = join(dir, p === '/' ? '/index.html' : p) + if (!f.startsWith(dir) || !existsSync(f)) return res.writeHead(404).end('x') + res.writeHead(200, { 'content-type': mime[extname(f)] ?? 'application/octet-stream' }) + res.end(readFileSync(f)) + }) + s.listen(0, '127.0.0.1', () => resolve({ server: s, url: `http://127.0.0.1:${s.address().port}` })) +}) + +const viewName = process.argv[2] ?? 'desktop' +const storyId = process.argv[3] +const hash = process.argv[4] ?? '' +const clipArg = process.argv[5] ?? '' +const out2 = process.argv[6] ?? 'crop.png' +const view = + viewName === 'mobile' + ? { w: 390, h: 844, dir: 'mocks/mobile' } + : { w: 1440, h: 900, dir: 'mocks/desktop' } +const clip = clipArg + ? (([x, y, w, h]) => ({ x, y, width: w, height: h }))(clipArg.split(',').map(Number)) + : undefined + +const browser = await chromium.launch() +const out = join(root, 'screenshots/match') +mkdirSync(out, { recursive: true }) + +const page = await browser.newPage({ + viewport: { width: view.w, height: view.h }, + deviceScaleFactor: 2, + reducedMotion: 'reduce', +}) +await page.goto(`file://${join(root, view.dir, 'interrogation.html')}${hash ? `#${hash}` : ''}`, { + waitUntil: 'networkidle', +}) +await page.evaluate(() => document.fonts.ready) +await page.waitForTimeout(400) +const a = (await page.screenshot({ clip })).toString('base64') + +await page.goto(`${url}/iframe.html?id=${storyId}&viewMode=story`, { waitUntil: 'networkidle' }) +await page.evaluate(() => document.fonts.ready) +await page.waitForFunction(() => document.querySelector('#storybook-root')?.childElementCount > 0) +await page.waitForTimeout(700) +const b = (await page.screenshot({ clip })).toString('base64') + +const sheet = await browser.newPage({ viewport: { width: 3400, height: 3400 }, deviceScaleFactor: 1 }) +await sheet.setContent(` +
MOCK
IMPL
`) +await sheet.waitForTimeout(200) +await sheet.locator('.row').screenshot({ path: join(out, out2) }) +await browser.close() +server.close() +console.log(join(out, out2)) diff --git a/.tmp-crop.mjs b/scripts/compare-mock-impl.mjs similarity index 100% rename from .tmp-crop.mjs rename to scripts/compare-mock-impl.mjs diff --git a/.crop.mjs b/scripts/crop-image.mjs similarity index 100% rename from .crop.mjs rename to scripts/crop-image.mjs diff --git a/.tmp-crop-int.mjs b/scripts/crop-interrogation-columns.mjs similarity index 100% rename from .tmp-crop-int.mjs rename to scripts/crop-interrogation-columns.mjs diff --git a/.tmp-crop-int2.mjs b/scripts/crop-interrogation-last.mjs similarity index 100% rename from .tmp-crop-int2.mjs rename to scripts/crop-interrogation-last.mjs diff --git a/.tmp-crop3.mjs b/scripts/crop-zoom-onpin.mjs similarity index 100% rename from .tmp-crop3.mjs rename to scripts/crop-zoom-onpin.mjs diff --git a/.tmp-crop4.mjs b/scripts/crop-zoom2-onpin.mjs similarity index 100% rename from .tmp-crop4.mjs rename to scripts/crop-zoom2-onpin.mjs diff --git a/.tmp-measure.mjs b/scripts/measure-gutters.mjs similarity index 100% rename from .tmp-measure.mjs rename to scripts/measure-gutters.mjs diff --git a/.tmp-font.mjs b/scripts/probe-fonts.mjs similarity index 100% rename from .tmp-font.mjs rename to scripts/probe-fonts.mjs diff --git a/.tmp-mockshot.mjs b/scripts/shoot-mock.mjs similarity index 100% rename from .tmp-mockshot.mjs rename to scripts/shoot-mock.mjs diff --git a/scripts/stack-panels.mjs b/scripts/stack-panels.mjs new file mode 100644 index 0000000..e73f3fb --- /dev/null +++ b/scripts/stack-panels.mjs @@ -0,0 +1,26 @@ +/** + * 三枚並びの絵から、モック側と実装側の同じ矩形を切り出して上下に積む。 + * 横に並んだままだと同じ場所を見比べにくいので、縦へ積み替えるためだけの使い捨て。 + * + * bun scripts/stack-panels.mjs [zoom] + */ +import { readFileSync } from 'node:fs' +import { chromium } from 'playwright' + +const [, , src, out, panelW, gap, top, x, y, w, h, zoom] = process.argv +const n = (v) => Number(v) +const z = zoom === undefined ? 1 : n(zoom) +const b64 = readFileSync(src).toString('base64') +const cell = (index) => ` +
+ +
` + +const browser = await chromium.launch() +const page = await browser.newPage({ viewport: { width: n(w) * z, height: n(h) * z * 2 + 8 } }) +await page.setContent( + `${cell(0)}${cell(1)}`, +) +await page.screenshot({ path: out }) +await browser.close() diff --git a/scripts/tmp-ovw-crop.mjs b/scripts/tmp-ovw-crop.mjs new file mode 100644 index 0000000..0f9d3f3 --- /dev/null +++ b/scripts/tmp-ovw-crop.mjs @@ -0,0 +1,40 @@ +/* 使い捨て。モックと story を同寸で撮り、指定の区画だけ上下に並べて見る。 */ +import { mkdirSync } from 'node:fs' +import { chromium } from 'playwright' + +const [id, view, ...rects] = process.argv.slice(2) +const w = view === 'desktop' ? 1440 : 390 +const h = view === 'desktop' ? 900 : 844 +const dir = view === 'desktop' ? 'desktop' : 'mobile' + +const browser = await chromium.launch() +const shoot = async (url) => { + const p = await browser.newPage({ viewport: { width: w, height: h }, reducedMotion: 'reduce' }) + await p.goto(url, { waitUntil: 'networkidle' }) + await p.evaluate(() => document.fonts.ready) + if (url.includes('iframe.html')) { + await p.waitForFunction(() => document.querySelector('#storybook-root')?.childElementCount > 0) + } + await p.waitForTimeout(900) + const s = (await p.screenshot()).toString('base64') + await p.close() + return s +} +const mock = await shoot(`file:///home/vscode/app/mocks/${dir}/case-overview.html#mode=nohope`) +const impl = await shoot(`http://127.0.0.1:6099/iframe.html?id=${id}&viewMode=story`) + +mkdirSync('screenshots/crop', { recursive: true }) +for (const rect of rects) { + const [x, y, cw, ch] = rect.split(',').map(Number) + const page = await browser.newPage({ viewport: { width: cw + 20, height: ch * 2 + 60 }, deviceScaleFactor: 2 }) + await page.setContent(` + +
mock
+
impl
`) + await page.waitForTimeout(300) + await page.screenshot({ path: `screenshots/crop/${view}-${rect.replace(/,/g, '_')}.png` }) + await page.close() +} +await browser.close() diff --git a/src/client/components/CaseNote.tsx b/src/client/components/CaseNote.tsx index 187e404..3b5ddef 100644 --- a/src/client/components/CaseNote.tsx +++ b/src/client/components/CaseNote.tsx @@ -6,7 +6,7 @@ import { DialogTitle, DialogTrigger, } from '@/client/components/ui/dialog' -import { splitParagraphs } from '@/client/lib/paragraphs' +import { splitParagraphs } from '@/shared/paragraphs' type Props = { briefing: string diff --git a/src/client/components/ChatLog.tsx b/src/client/components/ChatLog.tsx index 182e8d7..1e78a20 100644 --- a/src/client/components/ChatLog.tsx +++ b/src/client/components/ChatLog.tsx @@ -1,7 +1,7 @@ import { edgeOf, inkOf } from '@/client/components/CharacterAvatar' import type { ChatTurn } from '@/client/hooks/useInterrogation' import { usePacedReveal } from '@/client/hooks/usePacedReveal' -import { settledSentences } from '@/client/lib/paragraphs' +import { settledSentences } from '@/shared/paragraphs' type Props = { turns: ChatTurn[] diff --git a/src/client/components/Motions.stories.tsx b/src/client/components/Motions.stories.tsx index 01d9d5c..1bb1055 100644 --- a/src/client/components/Motions.stories.tsx +++ b/src/client/components/Motions.stories.tsx @@ -200,16 +200,18 @@ const MotionCatalogue = () => (
窓口の受付は午後七時八分でした。レシートも残っています。 - - 新事実 - - 牧野は午後六時三十五分に店を出たと述べた + + + 新事実 + + 牧野は午後六時三十五分に店を出たと述べた +
diff --git a/src/client/components/NewFactBand.tsx b/src/client/components/NewFactBand.tsx index 14bb67f..0e8b978 100644 --- a/src/client/components/NewFactBand.tsx +++ b/src/client/components/NewFactBand.tsx @@ -12,9 +12,15 @@ const VISIBLE_MS = 2600 * 新しく分かったことを、会話の上に帯で被せて知らせる。 * * 事件の記録は聞き込み中に画面上に無いので、増えたことは被せて伝えるしかない。 - * 箱にはせず、二本の罫線と薄い覆いだけ。操作は塞がない(pointer-events-none) + * 箱にはせず、二本の罫線だけ。操作は塞がない(pointer-events-none) * ——テンポよく次を訊きたい人の指を、演出で止める理由がない。 * + * 地は透かさない。端末は幅が狭く帯が会話の行を丸ごと覆うので、わずかでも透けると + * 後ろの字と帯の字が重なって、どちらも読めなくなる。 + * + * 机(lg)では被せず、名札の下・会話の上に流れの中で置く。机には縦の余りがあるので、 + * 読んでいる行を隠してまで割り込む理由がない。 + * * 祝わない。増えたのは事実であって、手柄ではないので。 * * 出し直しは呼び出し側が key を差し替えて行う(TurnAnnounce と同じ約束)。 @@ -35,11 +41,11 @@ export const NewFactBand = ({ text }: Props) => { return (
-
+
新事実 - {text} + {text}
) diff --git a/src/client/components/RouteStatus.tsx b/src/client/components/RouteStatus.tsx index d676a9e..33a9534 100644 --- a/src/client/components/RouteStatus.tsx +++ b/src/client/components/RouteStatus.tsx @@ -1,4 +1,4 @@ -import { Link } from '@tanstack/react-router' +import { type ErrorComponentProps, Link } from '@tanstack/react-router' /** * ルータが直接描く3つの状態(読み込み中・見つからない・エラー)。 @@ -26,11 +26,11 @@ export const RouteNotFound = () => (
) -export const RouteError = ({ error }: { error: Error }) => ( +export const RouteError = ({ error }: ErrorComponentProps) => (

うまく開けませんでした

{/* 何が起きたかは出す。黙って戻す画面は、同じ操作をもう一度させるだけになる。 */} -

{error.message}

+

{error instanceof Error ? error.message : String(error)}

最初から diff --git a/src/client/components/ui/button.tsx b/src/client/components/ui/button.tsx index c3d5041..58cff60 100644 --- a/src/client/components/ui/button.tsx +++ b/src/client/components/ui/button.tsx @@ -16,15 +16,27 @@ const buttonVariants = cva( { variants: { variant: { - /** 画面の主たる操作。事件を始める・推理を出す、といった一段目のボタン。 */ + /** + * 画面の主たる操作。事件を始める・推理を出す、といった一段目のボタン。 + * + * 触れたら地を一段だけ起こし、枠を生成りまで上げる。地を上げる幅は + * 押せる行と同じ sumi-2 で止める——濃くすると、罫線で組んだ画面のなかで + * ここだけ塗った札になる。 + */ default: - 'border border-nezumi-dim font-semibold tracking-widest text-kinari hover:border-nezumi', - /** 取り返しのつかない操作。朱が出るのはこの variant だけ。 */ - destructive: 'border border-shu tracking-widest text-shu hover:border-shu', + 'border border-nezumi-dim font-semibold tracking-widest text-kinari hover:border-kinari hover:bg-sumi-2', + /** + * 取り返しのつかない操作。朱が出るのはこの variant だけ。 + * + * 触れても枠は朱のまま動かさない。取り消せない一手が、触れただけで + * 他の操作と同じ顔になると、押す前の身構えがひとつ抜ける。 + */ + destructive: 'border border-shu tracking-widest text-shu hover:bg-sumi-2', /** 沈めた枠。選ばれていない選択肢や、副次的な入口。 */ - outline: 'border border-sumi-3 text-nezumi-dim hover:border-nezumi-dim hover:text-nezumi', + outline: + 'border border-sumi-3 text-nezumi-dim hover:border-nezumi hover:bg-sumi-2 hover:text-kinari', /** 丸いアイコンボタン(記・図・推)。 */ - icon: 'rounded-full border border-keisen text-nezumi hover:border-nezumi-dim hover:text-kinari', + icon: 'rounded-full border border-keisen text-nezumi hover:border-nezumi hover:bg-sumi-2 hover:text-kinari', /** 枠なし。列に並ぶ項目そのものを押させるとき。 */ ghost: 'text-nezumi hover:text-kinari', /** 文中の細いリンク。 */ diff --git a/src/client/hooks/useInterrogation.ts b/src/client/hooks/useInterrogation.ts index c1ecebb..245ecdc 100644 --- a/src/client/hooks/useInterrogation.ts +++ b/src/client/hooks/useInterrogation.ts @@ -37,6 +37,14 @@ export type ChatTurn = { * 投げた時点では分からず、判定が返ってきて初めて決まるので、後から立てる。 */ notable?: boolean + /** + * サーバに記録されたこの発言の行のID。読み上げを頼む宛先。 + * + * 立つのは配信の途中で、その時点で本文は確定している。裏を返すと、これが無いのは + * 「まだ書いている途中」か「記録に失敗した」かのどちらかで、どちらも声は付かない。 + * 開き直して復元した会話にも無い——読み直しに読み上げは要らないので、取りに行かない。 + */ + messageId?: string } /** @@ -160,6 +168,34 @@ export const useInterrogation = (seed: InterrogationSeed) => { }) } + /** + * 記録された発言のIDを、その行に立てる。 + * + * 宛先は末尾からの位置で指す(`appendQuestionDelta` と同じ手)。質問のIDが届く時点で + * 末尾は返答待ちの空の行なので、質問はその1つ手前。返答のIDは末尾に立つ。 + */ + const markMessageId = (characterId: string, fromEnd: number, messageId: string) => { + setConversations((prev) => { + const current = prev[characterId] + const turns = current === undefined ? [] : current + const index = turns.length - fromEnd + const target = turns[index] + + if (target === undefined) { + return prev + } + + return { + ...prev, + [characterId]: [ + ...turns.slice(0, index), + { ...target, messageId }, + ...turns.slice(index + 1), + ], + } + }) + } + /** * その話題が何かを引き出したことを、会話ログの上でも印にする。 * @@ -215,7 +251,9 @@ export const useInterrogation = (seed: InterrogationSeed) => { { role: 'assistant', text: '', askedAt }, ]), onQuestion: (chunk) => appendQuestionDelta(params.characterId, chunk), + onQuestionId: (id) => markMessageId(params.characterId, 2, id), onDelta: (chunk) => appendAssistantDelta(params.characterId, chunk), + onAnswerId: (id) => markMessageId(params.characterId, 1, id), onJudgement: (judgement) => { if (judgement.revealedEvidences.length > 0 || judgement.revealedRevelations.length > 0) { markTopicNotable(params.characterId, askedAt) diff --git a/src/client/hooks/useVoicedReveal.ts b/src/client/hooks/useVoicedReveal.ts new file mode 100644 index 0000000..f5a1b81 --- /dev/null +++ b/src/client/hooks/useVoicedReveal.ts @@ -0,0 +1,221 @@ +import { useEffect, useRef, useState } from 'react' +import { loadVoiceSetting } from '@/client/lib/voice' + +/** + * 声が付かない行の間。 + * + * 返答は文の単位で届くが、モデルが速いと二文がほぼ同時に着地して、 + * 読んでいる最中に次が積まれる。届いた順はそのままに、通す間隔にだけ下限を設ける。 + */ +const SILENT_INTERVAL_MS = 800 + +/** + * `shown` は画面に出してよい行数、`spoken` は読み終わった行数。 + * + * 二つ持つのは、**読んでいる行が画面に出ている**ようにするため。出す前に鳴らすと、 + * まだ無い行を読み上げることになる。順は「出す → 読む → 次を出す」で、 + * 読んでいる最中は `shown === spoken + 1`——最後に出た一行がいま読まれている行。 + */ +type State = { shown: number; spoken: number; armed: boolean } + +/** + * 鳴らせる形にして返す。取れなければ undefined——呼ぶ側は時間送りへ落とす。 + * + * Blob に落としてから要素を作る。URL を要素に渡して読み込ませると、失敗を + * `error` イベントで待つことになり、待ちの管理がもう一つ増える。ここで待てば + * 「取れた/取れない」が返り値だけで決まる。 + */ +const load = async (url: string): Promise => { + try { + const response = await fetch(url) + + return response.ok ? new Audio(URL.createObjectURL(await response.blob())) : undefined + } catch { + return undefined + } +} + +/** + * 積まれた行を、読み上げに合わせて一つずつ通す。 + * 返すのは「今いくつまで出してよいか」と「まだ読んでいる最中か」。 + * + * 一行を出して読み上げ、読み終わってから次の行を出す。声の付かない行——書いている途中、 + * 記録に失敗した行、読み上げを切っているとき、合成に失敗したとき——は + * これまで通り一定の間で通す。**声が出ないことで会話が止まらない**のが要点で、 + * どの失敗も時間送りへ落ちる。 + * + * 間を置くのは `live`(返答が流れている)あいだに増えた分だけ。履歴の読み込みや + * 画面の復帰で一気に増えたときは全部そのまま出す——読み直しに間も声も要らない。 + * 流れ終わっても、溜まっているぶんは間を置いて出し切る。 + */ +export const useVoicedReveal = ( + /** + * 通す順に並んだ行の、音を取りに行く先。声が付かない行は undefined。 + * + * 毎回作り直される配列なので、依存には入れない(入れると返答が伸びるたびに + * 鳴っている途中の音が止まる)。読むのは行が入れ替わった瞬間だけなので、 + * 参照で持ち回る。 + */ + urls: (string | undefined)[], + live: boolean, +): { shown: number; speaking: boolean } => { + const total = urls.length + const [state, setState] = useState({ shown: total, spoken: total, armed: false }) + /* + * 読み上げの入切は聞き始めに一度だけ見る。行ごとに localStorage を読むと、 + * 一行ごとに同期の読み取りが挟まる(`useReadOut` の打鍵音と同じ理由)。 + */ + const [voiceOn] = useState(() => loadVoiceSetting() === 'on') + const urlsRef = useRef(urls) + urlsRef.current = urls + /** + * 合成を頼んである音。宛先ごとに1つ。 + * + * 合成には一行あたり数秒かかるので、順番が来てから頼むと、行と行のあいだが + * そのぶん空く。鳴らす順を待たずに、宛先が分かった端から並行で頼んでおく。 + * + * 読み終えても表からは落とさない。落とすと、次の行が届いて頼み直すときに + * 読み終えた行まで作り直してしまう。音そのもの(blob)は鳴らし終えた時点で + * 手放すので、残るのは空になった要素だけ。 + */ + const pending = useRef(new Map>()) + /** いま鳴っているもの。画面を離れるときに止める。 */ + const playing = useRef(undefined) + + /* + 宛先が分かった端から合成を頼む。宛先が立つのはサーバが発言を記録した時点なので、 + 探偵の質問を読んでいるあいだに返答の合成が進む。 + */ + const known = urls.join('|') + + useEffect(() => { + if (!voiceOn) { + return + } + + // 声の付かない行はここで空になる。宛先に区切りの字は入らない(問い合わせは ?line=N)。 + for (const url of known.split('|')) { + if (url.length > 0 && !pending.current.has(url)) { + pending.current.set(url, load(url)) + } + } + }, [known, voiceOn]) + + /* 読み切らずに離れたぶん。blob は明示的に手放さないと残る。 */ + useEffect(() => { + const held = pending.current + + return () => { + for (const audio of held.values()) { + void audio.then((element) => { + if (element !== undefined) { + URL.revokeObjectURL(element.src) + } + }) + } + + held.clear() + } + }, []) + + /* + 次の一行を出す。訊いた瞬間に「間を置く相手」になり、読み終えてこちらの番に + 戻ったら外れる。外れているあいだは届いたぶんをそのまま出す。 + + 出すのは前の行を読み終えてから(`shown === spoken`)。読んでいる最中に + 次を出すと、読み上げより先に字が進む。 + */ + useEffect(() => { + if (live && !state.armed) { + setState({ shown: state.shown, spoken: state.spoken, armed: true }) + + return + } + + if (!state.armed) { + if (state.shown !== total || state.spoken !== total) { + setState({ shown: total, spoken: total, armed: false }) + } + + return + } + + if (state.spoken >= total && !live) { + setState({ shown: state.shown, spoken: state.spoken, armed: false }) + + return + } + + if (state.shown === state.spoken && state.shown < total) { + setState({ shown: state.shown + 1, spoken: state.spoken, armed: true }) + } + }, [total, live, state]) + + /** いま読んでいる最中か。出ているのに読み終わっていない行があるあいだ。 */ + const reading = state.armed && state.spoken < state.shown + + /* + 出ている一行を読み上げる。依存に total を入れないのは、返答が伸びるたびに + 作り直されると、鳴っている途中の音が止まって頭から鳴り直すため。 + */ + useEffect(() => { + if (!reading) { + return + } + + const url = urlsRef.current[state.spoken] + const cancelled = { value: false } + /* + 畳んだ後は進めない。鳴らし終える前に離れた回に、要素へ残った ended が + 後から届いて、もう居ない行の位置を立て直すのを防ぐ。 + */ + const done = () => { + if (!cancelled.value) { + setState((prev) => ({ shown: prev.shown, spoken: prev.spoken + 1, armed: prev.armed })) + } + } + + if (!voiceOn || url === undefined) { + const timer = setTimeout(done, SILENT_INTERVAL_MS) + + return () => clearTimeout(timer) + } + + // 先に頼んである音。まだ頼んでいなければ(切から入に変わった直後など)ここで頼む。 + const requested = pending.current.get(url) + const audio = requested === undefined ? load(url) : requested + + void audio.then((element) => { + if (cancelled.value) { + return + } + + // 取れなかった・鳴らせなかったときは待たずに次へ。無音のまま止まるより出す。 + if (element === undefined) { + done() + + return + } + + playing.current = element + element.addEventListener('ended', done, { once: true }) + element.addEventListener('error', done, { once: true }) + void element.play().catch(done) + }) + + return () => { + cancelled.value = true + + const element = playing.current + + if (element !== undefined) { + element.pause() + // 音は行ごとに使い捨て。放っておくと、聞き込みのあいだ blob が積み上がる。 + URL.revokeObjectURL(element.src) + playing.current = undefined + } + } + }, [reading, state.spoken, voiceOn]) + + return { shown: state.shown, speaking: reading } +} diff --git a/src/client/index.css b/src/client/index.css index 6ddacf9..3f96265 100644 --- a/src/client/index.css +++ b/src/client/index.css @@ -297,22 +297,27 @@ body { * 箱にはせず、二本の罫線と薄い覆いだけ。操作は塞がず 2.6 秒で引く。 * 祝わない——増えたのは事実であって、手柄ではないので。 */ +/* + * 縦の中央寄せはここに書かない。器の側(-translate-y-1/2)が持つ。 + * 動きを切ってある人には下の @media でこの animation ごと none になるので、 + * ここが中央寄せを兼ねていると、その人にだけ帯が半分ぶん下へずれる。 + */ @keyframes band { 0% { opacity: 0; - transform: translateY(-50%) scaleY(0.86); + transform: scaleY(0.86); } 11% { opacity: 1; - transform: translateY(-50%) scaleY(1); + transform: scaleY(1); } 84% { opacity: 1; - transform: translateY(-50%) scaleY(1); + transform: scaleY(1); } 100% { opacity: 0; - transform: translateY(-50%) scaleY(1); + transform: scaleY(1); } } diff --git a/src/client/lib/api.ts b/src/client/lib/api.ts index b81e91b..6a0bf14 100644 --- a/src/client/lib/api.ts +++ b/src/client/lib/api.ts @@ -25,8 +25,14 @@ import { 画面から引数で渡して回すと、設定と関係のない関数にまで引数が生えるので、 送る直前にここで読む。サーバはこの値を信用せず、許可リストと上限で検め直す。 */ -import { loadSettings, settingsLimits, toLlmOverrides } from '@/client/lib/settings-store' +import { + loadSettings, + settingsLimits, + toJudgeTuning, + toLlmOverrides, +} from '@/client/lib/settings-store' import { parseSseStream } from '@/client/lib/sse' +import type { JudgeTuning } from '~/db/judge-tuning' /** * HTTPエラーとレスポンス形状の不一致を1つの型にまとめる。 @@ -101,6 +107,19 @@ export const fetchScenarios = (): Promise => export const fetchScenarioDetail = (scenarioId: string): Promise => requestJson(`/api/scenarios/${scenarioId}`, scenarioDetailSchema, undefined) +/** + * リクエストに載せるプレイヤー側の設定。 + * + * モデルの指名と判定の切り替えは、どちらも「その回に何を使ったか」なので一緒に載せる。 + * 保管庫を毎回読み直すのは、設定画面で切り替えた直後の話題から効かせるため + * (読み込み時に一度だけ読むと、遊びながら入れ替えて比べるという使い方ができない)。 + */ +const playerSettings = (): { llm: ReturnType; judge: JudgeTuning } => { + const settings = loadSettings() + + return { llm: toLlmOverrides(settings), judge: toJudgeTuning(settings) } +} + /** * セッション開始。ここで計時が始まるので、事件の記録を読み終えてから呼ぶ。 * 探偵は名乗らずに始めることもできるので undefined を許す。 @@ -149,6 +168,8 @@ export const submitAccusation = (params: { requestJson( `/api/sessions/${params.sessionId}/accuse`, accuseResultSchema, + // 判定の直しは載せない。推理の採点は別の呼び出し(`deduction.ts`)で、 + // 話題ごとの判定とは通る道が違う。 jsonInit('POST', { ...params, llm: toLlmOverrides(loadSettings()) }), ) @@ -160,8 +181,16 @@ export type AskCallbacks = { onQuestionStart: () => void /** 探偵の質問の断片。直前の `onQuestionStart` で始まった往復の質問として継ぎ足す。 */ onQuestion: (chunk: string) => void + /** + * 質問が記録された合図。届く `id` は messages の行のIDで、読み上げを頼む宛先になる。 + * ここまで来たら本文は確定している——以後この質問に断片は継ぎ足されない。 + * 記録に失敗した回は届かない(声が付かないだけで、会話は続く)。 + */ + onQuestionId: (id: string) => void /** NPCの返答の断片。直前の `onQuestion` で始まった往復の答えとして継ぎ足す。 */ onDelta: (chunk: string) => void + /** 返答が記録された合図。`onQuestionId` と同じ扱い。 */ + onAnswerId: (id: string) => void onJudgement: (judgement: Judgement) => void onDone: () => void } @@ -179,7 +208,7 @@ export const askTopic = async ( ): Promise => { const res = await fetch( `/api/sessions/${params.sessionId}/ask`, - jsonInit('POST', { ...params, llm: toLlmOverrides(loadSettings()) }), + jsonInit('POST', { ...params, ...playerSettings() }), ) if (!res.ok) { @@ -206,10 +235,18 @@ export const askTopic = async ( callbacks.onQuestion(event.data) } + if (event.event === 'question-id') { + callbacks.onQuestionId(event.data) + } + if (event.event === 'delta') { callbacks.onDelta(event.data) } + if (event.event === 'answer-id') { + callbacks.onAnswerId(event.data) + } + if (event.event === 'judgement') { const parsedJson: unknown = JSON.parse(event.data) callbacks.onJudgement(decode(judgementSchema, parsedJson)) @@ -221,6 +258,15 @@ export const askTopic = async ( } } +/** + * 発言の1行を読み上げてもらう先。 + * + * 文そのものは渡さない。サーバが messages から読んで同じ割り方で切り出す + * (`src/server/routes/voice.ts`)。声の付かない行はここを呼ばない。 + */ +export const voiceUrl = (sessionId: string, messageId: string, line: number): string => + `/api/sessions/${sessionId}/messages/${messageId}/voice?line=${line}` + /** catch (error: unknown) から表示用の一言を取り出す。 */ export const describeError = (error: unknown): string => { if (error instanceof ApiError) { diff --git a/src/client/lib/detective-store.ts b/src/client/lib/detective-store.ts index bf03e7b..6b29656 100644 --- a/src/client/lib/detective-store.ts +++ b/src/client/lib/detective-store.ts @@ -26,6 +26,40 @@ export type DetectiveStore = z.infer export const EMPTY_STORE: DetectiveStore = { profiles: [], activeId: undefined } +/** + * 備え付けの探偵。作らなくても、この一体を選べばすぐ事件に向かえる。 + * + * localStorage には書かない。保管庫は「プレイヤーが作ったもの」だけを持ち、 + * 備え付けは名簿に並べるときに前へ足す。書き込むと、消せない行が保存され続けるうえ、 + * 中身を直したときに古い写しが各人の端末に残る。 + * + * 【外に出す前に必ず外すこと】 + * 「橘シェリー」は既存作品の登場人物で、声も実在の声優に紐づく登録話者を借りている。 + * src/server/tts/irodori.ts の但し書きと同じ扱いで、公開に向かうときは + * DETECTIVE_SPEAKER_ID と一緒にこの定数ごと落とす。 + */ +export const TACHIBANA_SHERRY: StoredDetective = { + id: 'preset:tachibana-sherry', + name: '橘シェリー', + ageGroup: 'teen', + gender: 'female', + appearance: + '水色の髪を三つ編みでまとめた団子頭に、オレンジの瞳。右の横髪に金と黒の小さなリボン。紺と青と水色のチェックのシャーロックハットとスカート、くすんだ水色のウェストコートの上に紺のインバネスコート。左足だけ黒いタイツ。どんなときもにこやかで、楽しそうなことには迷わず飛びつく。気になったことはとことん追いかけ、周りを驚かせることもある。', + speech: 'いつもですます口調。明るく弾んだ調子で、面白がっているのがそのまま声に出る。', +} + +export const PRESET_DETECTIVES: StoredDetective[] = [TACHIBANA_SHERRY] + +/** 備え付けかどうか。編集と削除を出すかがここで決まる。 */ +export const isPresetDetective = (id: string): boolean => + PRESET_DETECTIVES.some((preset) => preset.id === id) + +/** 名簿。備え付けが先、そのあとに作った順で並ぶ。 */ +export const detectiveRoster = (store: DetectiveStore): StoredDetective[] => [ + ...PRESET_DETECTIVES, + ...store.profiles, +] + const STORAGE_KEY = 'alibai:detectives' /** @@ -47,6 +81,14 @@ const legacyStoredDetectiveSchema = z.object({ appearance: z.string().max(200), }) +/** + * 口調の欄が無かった頃の形。空の口調として読み替える。 + * + * 足したばかりの欄なので、保管庫にある探偵はまだ誰も持っていない。ここを飛ばすと + * 全員が今の形にも旧い形にも当てはまらなくなり、次に開いた瞬間まとめて消える。 + */ +const speechlessStoredDetectiveSchema = storedDetectiveSchema.omit({ speech: true }) + /** 中身の検証は1人ずつやるので、ここでは器の形だけ見る。 */ const looseStoreSchema = z.object({ profiles: z.array(z.unknown()), @@ -119,6 +161,12 @@ const recoverProfile = (raw: unknown): StoredDetective[] => { return [current.data] } + const speechless = speechlessStoredDetectiveSchema.safeParse(raw) + + if (speechless.success) { + return [{ ...speechless.data, speech: '' }] + } + const legacy = legacyStoredDetectiveSchema.safeParse(raw) if (!legacy.success) { @@ -132,6 +180,7 @@ const recoverProfile = (raw: unknown): StoredDetective[] => { ageGroup: toAgeGroup(legacy.data.age), gender: toGender(legacy.data.gender), appearance: legacy.data.appearance, + speech: '', }, ] } @@ -161,7 +210,9 @@ export const parseDetectiveStore = (raw: unknown): ParsedStore => { const profiles = loose.data.profiles.flatMap(recoverProfile) // 選択中だった探偵が復元できなかったなら、選択は外す。 // 居ない相手を選んだままにすると、そのまま事件に向かえてしまう。 - const activeId = profiles.some((profile) => profile.id === loose.data.activeId) + const activeId = detectiveRoster({ profiles, activeId: undefined }).some( + (profile) => profile.id === loose.data.activeId, + ) ? loose.data.activeId : undefined @@ -176,7 +227,7 @@ export const parseDetectiveStore = (raw: unknown): ParsedStore => { * プレイが始まってしまう。 */ export const activeDetective = (store: DetectiveStore): StoredDetective | undefined => - store.profiles.find((profile) => profile.id === store.activeId) + detectiveRoster(store).find((profile) => profile.id === store.activeId) /** * 追加または更新。同じ id があれば置き換え、無ければ末尾に足す。 @@ -207,7 +258,7 @@ export const removeDetective = (store: DetectiveStore, id: string): DetectiveSto /** 選択の切り替え。存在しない id を渡された場合は何も変えない。 */ export const setActiveDetective = (store: DetectiveStore, id: string): DetectiveStore => - store.profiles.some((profile) => profile.id === id) ? { ...store, activeId: id } : store + detectiveRoster(store).some((profile) => profile.id === id) ? { ...store, activeId: id } : store /** 名乗らずに始めるときのために、選択を外す。 */ export const clearActiveDetective = (store: DetectiveStore): DetectiveStore => ({ @@ -221,6 +272,7 @@ export const toDetective = (stored: StoredDetective): Detective => ({ ageGroup: stored.ageGroup, gender: stored.gender, appearance: stored.appearance, + speech: stored.speech, }) export const newDetectiveId = (): string => crypto.randomUUID() diff --git a/src/client/lib/format.ts b/src/client/lib/format.ts index c7d86ec..0848100 100644 --- a/src/client/lib/format.ts +++ b/src/client/lib/format.ts @@ -1,12 +1,16 @@ import dayjs from 'dayjs' -/** 経過秒数を `分:秒` に整形する。タイマー表示とリザルトの解決タイムの両方で使う。 */ +/** + * 経過秒数を `分:秒` に整形する。タイマー表示とリザルトの解決タイムの両方で使う。 + * + * 分も0詰めする。この数字は等幅で計器として出るので、桁が増減すると + * 隣の字が横へ動く。`3:21` と `03:21` が入れ替わるだけで目盛りが揺れて見える。 + */ export const formatSeconds = (totalSeconds: number): string => { const minutes = Math.floor(totalSeconds / 60) const seconds = totalSeconds % 60 - const paddedSeconds = seconds < 10 ? `0${seconds}` : `${seconds}` - return `${minutes}:${paddedSeconds}` + return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}` } /** diff --git a/src/client/lib/schemas.ts b/src/client/lib/schemas.ts index 87bf28f..9bb6979 100644 --- a/src/client/lib/schemas.ts +++ b/src/client/lib/schemas.ts @@ -2,9 +2,9 @@ import { z } from 'zod' import { detectiveSchema } from '~/db/detective' import { floorPlanSchema } from '~/db/floor-plan' import { gameModeSchema, hintSchema } from '~/db/game-mode' -import { llmProviderSchema, settableLlmRoleSchema } from '~/db/llm-catalog' +import { settableLlmRoleSchema } from '~/db/llm-catalog' import { investigablePlaceSchema } from '~/db/place' -import { VICTIM_ID } from '~/db/scenario-definition' +import { placeIdSchema, VICTIM_ID } from '~/db/scenario-definition' /** * サーバのレスポンスは fetch の時点では unknown。 @@ -31,6 +31,13 @@ export const scenarioListSchema = z.array(scenarioSummarySchema) export const characterSchema = z.object({ id: z.uuid(), name: z.string().nonempty(), + /** + * 幅の狭いところへ出す短い名前。アリバイ表の帯がこれを読む。 + * + * サーバは書かれていない行にも `name` を写して返すので、常に値がある。 + * 画面側で姓を切り出さないこと——一文字姓や外国名で切る位置が決まらない。 + */ + shortName: z.string().nonempty(), publicIntroduction: z.string().nonempty(), }) @@ -303,10 +310,15 @@ export const historyExchangeSchema = z.object({ /** * 話しかけた相手のID。 * - * 登場人物は uuid だが、被害者だけは決め打ちの `victim`(採番する先が一人しか無い)。 - * ここを uuid で縛ると、遺体を調べたセッションが復元できずに画面ごと落ちる。 + * 登場人物は uuid、被害者は決め打ちの `victim`、場所は作者が書いたローカルID。 + * 三者は形で見分けられるので、どれを指しているかは常に決まる(`placeIdSchema`)。 + * + * ここを狭く縛ると、その相手を含むセッションが復元できずに**画面ごと落ちる**。 + * 履歴は聞き込みの画面へ入った瞬間に読むので、一手も打たないうちに落ちる。 + * 実際、場所を足したとき `victim` までしか許しておらず、場所を持つ事件が + * 開いた時点で必ず落ちた。相手を増やしたらここも必ず増やすこと。 */ -const subjectIdSchema = z.union([z.uuid(), z.literal(VICTIM_ID)]) +const subjectIdSchema = z.union([z.uuid(), z.literal(VICTIM_ID), placeIdSchema]) export const sessionHistorySchema = z.object({ sessionId: z.uuid(), @@ -321,20 +333,14 @@ export const sessionHistorySchema = z.object({ /** * 設定画面が選択肢を組み立てるための材料(GET /api/settings/llm)。 * - * available はキーが設定されているかの真偽値だけ。鍵も、その長さも、 - * ゲートウェイの向き先も返ってこない。 + * 鍵も、その長さも、互換サーバの向き先も返ってこない。models が空なら + * 「鍵が未設定」か「互換サーバに繋がらなかった」のどちらかで、画面から区別は付かない。 */ const limitBoundSchema = z.object({ value: z.int().positive().optional(), max: z.int().positive() }) export const llmSettingsResponseSchema = z.object({ - providers: z.array( - z.object({ - id: llmProviderSchema, - label: z.string().nonempty(), - available: z.boolean(), - models: z.array(z.object({ id: z.string().nonempty(), label: z.string().nonempty() })), - }), - ), + /** 互換サーバの `/v1/models` 由来。手で書いた表ではないので、ID を列挙で縛らない。 */ + models: z.array(z.object({ id: z.string().nonempty(), label: z.string().nonempty() })), roles: z.array( z.object({ id: settableLlmRoleSchema, @@ -358,8 +364,9 @@ export const apiErrorSchema = z.object({ export type ScenarioSummary = z.infer export type CharacterSheet = z.infer -export type { AgeGroup, Detective, Gender } from '~/db/detective' +export type { Detective } from '~/db/detective' export type { FloorPlan, Room } from '~/db/floor-plan' +export type { AgeGroup, Gender } from '~/db/person' export type ScenarioDetail = z.infer export type CreateSessionResponse = z.infer export type Discovery = z.infer @@ -373,10 +380,5 @@ export type AccuseResult = z.infer export type SessionHistory = z.infer export type { GameMode, Hint, SubjectCount } from '~/db/game-mode' export type LlmSettingsResponse = z.infer -// 選択肢の正典は db/llm-catalog.ts。ここで並べ直すと画面とAPIの受け入れ値がずれる。 -export { - LLM_CATALOG, - LLM_PROVIDER_LABELS, - type LlmProvider, - type SettableLlmRole, -} from '~/db/llm-catalog' +// 役割の正典は db/llm-catalog.ts。ここで並べ直すと画面とAPIの受け入れ値がずれる。 +export type { SettableLlmRole } from '~/db/llm-catalog' diff --git a/src/client/lib/settings-store.ts b/src/client/lib/settings-store.ts index 799516b..17fb486 100644 --- a/src/client/lib/settings-store.ts +++ b/src/client/lib/settings-store.ts @@ -1,35 +1,37 @@ import { z } from 'zod' import { clampLimits, EXCHANGES_PER_TOPIC, type SessionLimits } from '@/shared/turns' -import { - isKnownModel, - llmProviderSchema, - type SettableLlmRole, - settableLlmRoleSchema, -} from '~/db/llm-catalog' +import { DEFAULT_JUDGE_TUNING, type JudgeTuning, judgeTuningSchema } from '~/db/judge-tuning' +import { type SettableLlmRole, settableLlmRoleSchema } from '~/db/llm-catalog' /** * プレイヤーがこのブラウザで選んだ設定。 * * サーバには保存しない。自分のプレイにだけ効くもので、他の人には影響しない。 - * リクエストのたびに載せて送るが、**サーバはこれを信用しない**——モデルIDは - * 許可リストと突き合わせ、数値は上限で切り詰めてから使う。ここでの検証は - * 「画面に変な値を出さない」ためのもので、防御の本体はサーバ側にある。 + * リクエストのたびに載せて送るが、**サーバはこれを信用しない**——数値は上限で + * 切り詰めてから使う。ここでの検証は「画面に変な値を出さない」ためのもので、 + * 防御の本体はサーバ側にある。 */ -const roleSettingSchema = z.object({ - provider: llmProviderSchema.optional(), - model: z.string().nonempty().max(80).optional(), -}) +/** 長さだけ見て通す。突き合わせる許可リストはもう無い(`db/llm-catalog.ts`)。 */ +const modelField = z.string().nonempty().max(80).optional() + +const roleSettingSchema = z.object({ model: modelField }) export type RoleSetting = z.infer const settingsSchema = z.object({ - llm: z.partialRecord(settableLlmRoleSchema, roleSettingSchema), + /* + 知らないキーを許さないのは、書き戻しの判断(parseSettings の `current`)に使うため。 + 通常の object は provider を黙って捨てて「現行の形で読めた」と答えるので、 + 提供元を選んでいた頃の保管庫がいつまでも書き換わらない。 + */ + llm: z.partialRecord(settableLlmRoleSchema, z.strictObject({ model: modelField })), limits: z.object({ maxTurns: z.int().positive(), questionsPerTurn: z.int().positive(), exchangesPerTopic: z.int().positive(), }), + judge: judgeTuningSchema, }) export type Settings = z.infer @@ -54,41 +56,41 @@ export const DEFAULT_SETTINGS: Settings = { questionsPerTurn: 2, exchangesPerTopic: EXCHANGES_PER_TOPIC, }, + /* + * 判定の直しは既定で全部オフ。今までの挙動のまま始まり、入れたときだけ変わる。 + * 入れた回と切った回を見比べたいので、既定を良いほうに寄せない。 + */ + judge: DEFAULT_JUDGE_TUNING, } /** 器だけを見るための緩いスキーマ。中身の妥当性は要素ごとに判断する。 */ const looseSettingsSchema = z.object({ llm: z.record(z.string().nonempty(), z.unknown()).optional(), limits: z.record(z.string().nonempty(), z.unknown()).optional(), + judge: z.record(z.string().nonempty(), z.unknown()).optional(), }) /** * 1役割ぶんの読み替え。 * - * プロバイダが読めなければ、その役割ごと落とす(モデルだけ残しても、 - * どのプロバイダのモデルか決まらないため)。モデルだけが表から消えている場合は、 - * プロバイダの選択は活かしてモデルだけ落とす——カタログの更新で - * プレイヤーの選択がまるごと消えるのは、直しようがなくて困る。 + * 提供元を選んでいた頃の `{provider, model}` がそのまま残っている端末がある。 + * zod は知らないキーを黙って落とすので、provider は消えて model だけが残る + * ——ここで役割ごと捨てると、プレイヤーには「なぜか設定が戻った」としか見えない。 + * + * 突き合わせる許可リストはもう無い(宛先が互換サーバ1つになり、実在するモデルは + * サーバに聞くもの)。長さだけ見て通し、知らないIDならサーバ側でエラーになる。 + * + * モデルが読めなければ役割ごと落とす。model しか持たない今、モデルの無い役割は + * 「未選択」と同じものなので、空の器を残す意味が無い。 */ const recoverRole = (raw: unknown): RoleSetting | undefined => { const parsed = roleSettingSchema.safeParse(raw) - if (!parsed.success) { + if (!parsed.success || parsed.data.model === undefined) { return undefined } - const provider = parsed.data.provider - - if (provider === undefined) { - return undefined - } - - const model = parsed.data.model - - return { - provider, - model: model !== undefined && isKnownModel(provider, model) ? model : undefined, - } + return { model: parsed.data.model } } export type ParsedSettings = { @@ -130,7 +132,7 @@ export const parseSettings = (raw: unknown): ParsedSettings => { DEFAULT_SETTINGS.limits, ) - const settings: Settings = { llm, limits } + const settings: Settings = { llm, limits, judge: recoverJudge(loose.data.judge) } // 現行スキーマで読めて、かつ切り詰めも取りこぼしも起きていなければ、書き戻す必要はない。 return { settings, migrated: !current.success || !isSameSettings(current.data, settings) } @@ -139,6 +141,27 @@ export const parseSettings = (raw: unknown): ParsedSettings => { const numberOf = (value: unknown): number | undefined => typeof value === 'number' && Number.isFinite(value) ? value : undefined +const judgeFlag = (raw: Record | undefined, key: keyof JudgeTuning): boolean => { + const parsed = z.boolean().safeParse(raw?.[key]) + + return parsed.success ? parsed.data : DEFAULT_JUDGE_TUNING[key] +} + +/** + * 判定の直しの読み替え。鍵ごとに見て、真偽値でないものは既定(オフ)に落とす。 + * + * 器ごと捨てないのは他の項目と同じ理由だが、ここはもう一つある——切り替えを増やしたとき、 + * 保存済みの端末には新しい鍵が無い。丸ごと捨てると、既に入れてあったぶんまでオフに戻る。 + * + * 鍵を並べて書いてあるのは、増やしたときにここを直し忘れると型が通らないため。 + */ +const recoverJudge = (raw: Record | undefined): JudgeTuning => ({ + checkEvidenceIds: judgeFlag(raw, 'checkEvidenceIds'), + contradictionNeedsHistory: judgeFlag(raw, 'contradictionNeedsHistory'), + fixedTemperature: judgeFlag(raw, 'fixedTemperature'), + retryOnce: judgeFlag(raw, 'retryOnce'), +}) + const isSameSettings = (a: Settings, b: Settings): boolean => JSON.stringify(a) === JSON.stringify(b) @@ -148,6 +171,15 @@ export const toLlmOverrides = (settings: Settings): Partial settings.limits +/** + * サーバへ載せる形。limits と同じで、常に四つとも載る。 + * + * llm のような「未選択」を作らないのは、切り替えの既定がサーバ側にも同じ形で + * 置いてあるため(`DEFAULT_JUDGE_TUNING`)。載せないと既定に落ちるだけなので、 + * 「送っていない」と「オフを送った」を区別する意味が無い。 + */ +export const toJudgeTuning = (settings: Settings): JudgeTuning => settings.judge + /** * localStorage は「使えない環境がある」前提で触る。 * 読めなければ既定から始めればよく、設定の都合でプレイが始まらないほうが困る。 diff --git a/src/client/lib/voice.ts b/src/client/lib/voice.ts new file mode 100644 index 0000000..8b9cd46 --- /dev/null +++ b/src/client/lib/voice.ts @@ -0,0 +1,35 @@ +import { z } from 'zod' + +/** + * 聞き込みの読み上げ。 + * + * 効果音(`sound.ts`)とは別に持つ。あちらは打鍵と場面の音で、こちらは人の声。 + * 声だけ切りたい人と、音だけ切りたい人が別々に居る。 + * + * 切ってあるあいだ、会話は読み上げが無かった頃と同じ時間送りで出る。 + */ + +const STORAGE_KEY = 'alibai:voice' +const voiceSettingSchema = z.enum(['on', 'off']) + +export type VoiceSetting = z.infer + +export const DEFAULT_VOICE: VoiceSetting = 'on' + +export const loadVoiceSetting = (): VoiceSetting => { + try { + const parsed = voiceSettingSchema.safeParse(localStorage.getItem(STORAGE_KEY)) + + return parsed.success ? parsed.data : DEFAULT_VOICE + } catch { + return DEFAULT_VOICE + } +} + +export const saveVoiceSetting = (setting: VoiceSetting): void => { + try { + localStorage.setItem(STORAGE_KEY, setting) + } catch { + // 保存できなくても今回のプレイには影響しない。 + } +} diff --git a/src/client/screens/AccusationScreen.stories.tsx b/src/client/screens/AccusationScreen.stories.tsx index 5682612..dd8e00c 100644 --- a/src/client/screens/AccusationScreen.stories.tsx +++ b/src/client/screens/AccusationScreen.stories.tsx @@ -103,6 +103,44 @@ type Story = StoryObj export const Default: Story = { render: () => , play: ({ canvasElement }) => { - canvasElement.querySelector('input')?.click() + pickFirstSuspect(canvasElement) }, } + +/** + * 書き上げたところ。指名も二つの欄も埋まり、提出だけが残っている。 + * + * 中身は真相そのもの——モックが `#filled=1` で入れているのと同じ文面にしてある。 + * 当てずっぽうの下書きを入れると、欄の長さも改行位置もモックと違う絵になり、 + * 突き合わせる意味が薄れる。 + */ +export const Filled: Story = { + render: () => , + play: ({ canvasElement }) => { + pickFirstSuspect(canvasElement) + + const areas = canvasElement.querySelectorAll('textarea') + + write(areas[0], '帳場の奥で、書架の支柱で殴打した') + write(areas[1], '初版本のすり替えを水野に気づかれ、問い詰められたため') + }, +} + +/** 先頭の一人を指す。素のラジオを直に押すので、状態は本物と同じ道を通る。 */ +const pickFirstSuspect = (canvasElement: HTMLElement) => { + canvasElement.querySelector('input')?.click() +} + +/** + * 欄に文字を入れる。 + * React は value を自前の記述子で握っているので、`node.value = …` では onChange が起きない。 + * prototype 側の setter を通してから input を投げると、本物の入力と同じ経路になる。 + */ +const write = (node: HTMLTextAreaElement | undefined, text: string) => { + if (node === undefined) { + return + } + + Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, 'value')?.set?.call(node, text) + node.dispatchEvent(new Event('input', { bubbles: true })) +} diff --git a/src/client/screens/AccusationScreen.tsx b/src/client/screens/AccusationScreen.tsx index 8e44810..cf234c8 100644 --- a/src/client/screens/AccusationScreen.tsx +++ b/src/client/screens/AccusationScreen.tsx @@ -1,4 +1,4 @@ -import { useId, useState } from 'react' +import { type ReactNode, useId, useState } from 'react' import { AlibiChart, type AlibiPerson, type AlibiSegment } from '@/client/components/AlibiChart' import { CharacterAvatar, inkOf, surfaceOf } from '@/client/components/CharacterAvatar' import { Button } from '@/client/components/ui/button' @@ -118,17 +118,69 @@ export const AccusationScreen = ({ ]), ] - /** 帯のなかでの位置。端末側は幅が端末に依るので、px ではなく % で置く。 */ - const ratio = (at: string): string => { + /** 帯のなかでの位置(%)。端末側は幅が端末に依るので、px ではなく % で置く。 */ + const ratioNum = (at: string): number => { if (timeWindow === null) { - return '0%' + return 0 } const from = toMinutes(timeWindow.start) const length = toMinutes(timeWindow.end) - from - return `${(((toMinutes(at) - from) / length) * 100).toFixed(1)}%` + return ((toMinutes(at) - from) / length) * 100 } + const ratio = (at: string): string => `${ratioNum(at).toFixed(1)}%` + const leftOf = (pct: number): string => `${pct.toFixed(1)}%` + /** 端に寄った札は文字が画面の外へ出ないよう内側へ折り返す(机の DeadlineLabel と同じ判断)。 */ + const railLabelAlign = (pct: number): string => + pct < 24 ? '' : pct >= 72 ? '-translate-x-full text-right' : '-translate-x-1/2' + + /* + * 端末の帯に置く「遺体発見・死亡推定」の印。机の AlibiChart(DeadlineMarks)と同じ規則を + * 横向きに言い換えたもの——遺体発見は常に実線、死亡推定は手に入れた確度で描き分ける。 + * この画面だけの帯なので、机の縦向きの実装をそのまま流用できない。 + */ + const deathInfo = deadlineOf(scenario.victim, interrogation.estimatedDeathAt) + + /** 一点を指す印。裏の取れていない見立てだけ点線にする(実線=盤面が保証した情報)。 */ + const RailTick = ({ pct, dotted }: { pct: number; dotted: boolean }) => ( +
- {/* 事件の幅が長いと表は画面より背が高くなる。縮めずにここで送る(聞き込みと同じ)。 */} -
+ {/* + 事件の幅が長いと表は画面より背が高くなる。縮めずにここで送る(聞き込みと同じ)。 + 伸びはしない(flex-1 を持たない)——余りを飲ませると、短い事件で凡例が + 表から離れて画面の下端まで落ちる。溢れたときだけ縮んで、中を送る。 + */} +
+
実線 diff --git a/src/client/screens/DetectiveSetupScreen.stories.tsx b/src/client/screens/DetectiveSetupScreen.stories.tsx index aa10b92..0bf1744 100644 --- a/src/client/screens/DetectiveSetupScreen.stories.tsx +++ b/src/client/screens/DetectiveSetupScreen.stories.tsx @@ -6,7 +6,7 @@ import { DETECTIVES, SCENARIO } from '@/client/stories/fixtures' /** * ALI_DET — 探偵を決める。 * readStore と initialDraft を差し替えられるように作られているので、 - * localStorage を仕込まずに名簿・つくる・編集する・空の4状態を出せる。 + * localStorage を仕込まずに名簿・つくる・編集する・作る前の4状態を出せる。 */ const meta: Meta = { title: 'Screens/DET 探偵を決める', @@ -37,6 +37,7 @@ const EDIT_DRAFT: Draft = { ageGroup: 'adult', gender: 'female', appearance: '背の低い痩身。読みかけの文庫をいつも外套の右に入れている。', + speech: '丁寧語だが素っ気ない。相手を名字で呼ぶ。', } /** 探偵を編集する。1人目の値が入っていて、保存が押せる。 */ @@ -44,7 +45,10 @@ export const Edit: Story = { args: { initialDraft: EDIT_DRAFT }, } -/** 名簿が空。「まだ探偵がいません」と出て、事件に向かうは押せない。 */ -export const Empty: Story = { +/** + * まだ1人も作っていない状態。備え付けの橘シェリーだけが並ぶ。 + * 選ぶまで「事件に向かう」は押せず、その一体には編集も削除も出ない。 + */ +export const PresetOnly: Story = { args: { readStore: () => EMPTY_STORE }, } diff --git a/src/client/screens/DetectiveSetupScreen.tsx b/src/client/screens/DetectiveSetupScreen.tsx index 0eef460..63f9000 100644 --- a/src/client/screens/DetectiveSetupScreen.tsx +++ b/src/client/screens/DetectiveSetupScreen.tsx @@ -5,6 +5,8 @@ import { activeDetective, clearActiveDetective, type DetectiveStore, + detectiveRoster, + isPresetDetective, loadDetectiveStore, newDetectiveId, removeDetective, @@ -15,17 +17,16 @@ import { } from '@/client/lib/detective-store' import type { ScenarioDetail } from '@/client/lib/schemas' import { playSe } from '@/client/lib/sound' +import { type Detective, describeDetective } from '~/db/detective' import { AGE_GROUP_LABELS, AGE_GROUP_NOTES, AGE_GROUPS, type AgeGroup, - type Detective, - describeDetective, GENDER_LABELS, GENDERS, type Gender, -} from '~/db/detective' +} from '~/db/person' /** * 節の見出しと欄の名前。等幅なのは書式であって時刻ではないので、値には使わない。 @@ -65,6 +66,7 @@ export const emptyDraft = (): Draft => ({ ageGroup: 'unknown', gender: 'unknown', appearance: '', + speech: '', }) /** @@ -90,6 +92,7 @@ export const DetectiveSetupScreen = ({ }: Props) => { const nameId = useId() const appearanceId = useId() + const speechId = useId() const [store, setStore] = useState(readStore) const [draft, setDraft] = useState(initialDraft) @@ -121,6 +124,7 @@ export const DetectiveSetupScreen = ({ ageGroup: draft.ageGroup, gender: draft.gender, appearance: draft.appearance, + speech: draft.speech, } update(upsertDetective(store, saved)) @@ -128,6 +132,7 @@ export const DetectiveSetupScreen = ({ } const selected = activeDetective(store) + const roster = detectiveRoster(store) const canSave = draft !== undefined && draft.name.trim().length > 0 // 名簿は端末では draft が無いあいだだけ、机では常に見せる。 const rosterVisibleOnPhone = draft === undefined @@ -146,7 +151,7 @@ export const DetectiveSetupScreen = ({ @@ -165,75 +170,82 @@ export const DetectiveSetupScreen = ({
探偵 - {store.profiles.length === 0 ? ( -

- まだ探偵がいません。作るか、名乗らずに始めることもできます。 -

- ) : ( -
    - {store.profiles.map((profile) => { - const isActive = profile.id === store.activeId + {/* 備え付けの一体が常に居るので、名簿が空になることはない。 */} +
      + {roster.map((profile) => { + const isActive = profile.id === store.activeId + const isPreset = isPresetDetective(profile.id) - return ( -
    • - {/* + return ( +
    • + {/* 行そのものが押す場所なので、ここは素のボタンのまま。名前と説明を 積む形は Button の一行に詰める組みとは噛み合わない。 */} - + )} + - {/* 消すほうは一段沈める。並べて置くと押し間違える。 */} + {/* + 消すほうは一段沈める。並べて置くと押し間違える。 + 備え付けは直しも消しもできないので、行ごと出さない。 + */} + {!isPreset && (
      -
    • - ) - })} -
    - )} + )} + + ) + })} +
@@ -297,13 +309,21 @@ export const DetectiveSetupScreen = ({ > {selected.appearance.length > 0 ? selected.appearance : '容姿は書かれていません。'}

- + {/* 書かれていないときは行ごと出さない。容姿と違って、無いなら黙っていてよい。 */} + {selected.speech.length > 0 && ( +

+ {selected.speech} +

+ )} + {!isPresetDetective(selected.id) && ( + + )}
) ) : ( @@ -364,6 +384,23 @@ export const DetectiveSetupScreen = ({ className="field-sizing-fixed resize-none leading-relaxed" /> + + {/* + 容姿より短い欄にしてある。口調は例を一つ二つ挙げれば足りて、 + 長く書けるようにすると人物設定をこちらへ書き始めてしまう。 + */} +