case0 / src /case_zero /api /public_view.py
HusseinEid's picture
Case Zero - initial public release (fully local: Qwen2.5-1.5B via llama.cpp + Supertonic, custom pixel-noir SPA via gradio.Server)
414dc55
"""The PUBLIC wire contract: the exact case shape the Preact client receives.
This mirrors the prototype's ``prototype/js/case.jsx`` (the data-model-by-example) in
camelCase. It is the ONLY case representation that ever reaches the browser before the
verdict. The sealed solution - killer, true motive, key-evidence chain - and every
per-question/per-evidence suspicion *delta* and scripted *answer* are deliberately absent:
those are produced live, server-side (the deltas alone would reveal the killer).
"""
from __future__ import annotations
from pydantic import BaseModel, ConfigDict, Field
from pydantic.alias_generators import to_camel
class _Wire(BaseModel):
"""camelCase on the wire; snake_case in Python. Frozen - a view is never mutated."""
model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True, frozen=True)
class PublicVictim(_Wire):
name: str
role: str
age: int
sprite: str
bio: str
class SuggestedQuestion(_Wire):
id: str
q: str
class PublicSuspect(_Wire):
id: str
name: str
role: str
age: int
sprite: str
gender: str = "male"
tag: str
baseline_suspicion: int
motive: str # apparent motive shown in the dossier - NOT proof
alibi: str
quote: str
greet: str
suggested_questions: tuple[SuggestedQuestion, ...]
class ThreadMessage(_Wire):
from_: str = Field(alias="from")
who: str
t: str
m: str
class PublicEvidence(_Wire):
id: str
name: str
type: str # PHONE | PAPER | IMAGE | AUDIO | DATA
icon: str
time: str
found: str
summary: str
# Exactly one of these display payloads is populated, per type.
thread: tuple[ThreadMessage, ...] | None = None
detail: str | None = None
transcript: str | None = None
dur: str | None = None
rows: tuple[tuple[str, ...], ...] | None = None
class TimelineBeat(_Wire):
time: str
label: str
locked: bool = False
ev: str | None = None
conflict: bool = False
class FlashbackAccount(_Wire):
who: str
scene: str
lines: tuple[str, ...]
flags: tuple[int, ...] = ()
class PublicFlashback(_Wire):
title: str
a: FlashbackAccount
b: FlashbackAccount
class PublicMotive(_Wire):
id: str
text: str
class StoryBeat(_Wire):
scene: str
kicker: str
title: str
text: str
class PublicCase(_Wire):
id: str
city: str
district: str
title: str
tagline: str
weather: str
victim: PublicVictim
scene: str
tod: str
found: str
cause: str
facts: tuple[tuple[str, str], ...]
boot_lines: tuple[str, ...]
story_beats: tuple[StoryBeat, ...]
suspects: tuple[PublicSuspect, ...]
evidence: tuple[PublicEvidence, ...]
timeline: tuple[TimelineBeat, ...]
flashback: PublicFlashback
motives: tuple[PublicMotive, ...]
def golden_to_public(data: dict) -> PublicCase:
"""Project a sealed golden/stored case dict into the PUBLIC view.
Strips the ``sealed`` block and, per suspect, the scripted ``answer``/``delta`` and
the ``present``/``default`` reply tables - leaving only question text the player sees.
"""
suspects = tuple(
PublicSuspect(
id=s["id"],
name=s["name"],
role=s["role"],
age=s["age"],
sprite=s["sprite"],
gender=s.get("gender", "male"),
tag=s["tag"],
baseline_suspicion=s["suspicion"],
motive=s["motive"],
alibi=s["alibi"],
quote=s["quote"],
greet=s["greet"],
suggested_questions=tuple(
SuggestedQuestion(id=q["id"], q=q["q"]) for q in s["questions"]
),
)
for s in data["suspects"]
)
return PublicCase(
id=data["id"],
city=data["city"],
district=data["district"],
title=data["title"],
tagline=data["tagline"],
weather=data["weather"],
victim=PublicVictim(**data["victim"]),
scene=data["scene"],
tod=data["tod"],
found=data["found"],
cause=data["cause"],
facts=tuple(tuple(pair) for pair in data["facts"]),
boot_lines=tuple(data["bootLines"]),
story_beats=tuple(StoryBeat(**b) for b in data.get("storyBeats", [])),
suspects=suspects,
evidence=tuple(PublicEvidence(**e) for e in data["evidence"]),
timeline=tuple(TimelineBeat(**b) for b in data["timeline"]),
flashback=PublicFlashback(
title=data["flashback"]["title"],
a=FlashbackAccount(**data["flashback"]["a"]),
b=FlashbackAccount(**data["flashback"]["b"]),
),
motives=tuple(PublicMotive(**m) for m in data["motives"]),
)