Skip to content

chore: release v0.4.0 - #19

Merged
tkgstrator merged 58 commits into
masterfrom
develop
Sep 2, 2026
Merged

chore: release v0.4.0#19
tkgstrator merged 58 commits into
masterfrom
develop

Conversation

@tkgstrator

Copy link
Copy Markdown
Contributor

Promotes develop to master. Merging deploys to production and applies migrations
00190022 to the production D1.

What ships

The board stops knowing things the player has not earned. The alibi table used to draw
a death estimate from the first turn, before anyone had examined the body or asked the
doctor. Now the discovery time — public, since the case record states it — is drawn solid
from the start, and the estimate stays a dotted window with a question mark until evidence
marked revealsDeathTime is found. Cases that never name an estimate draw no mark at all,
rather than inviting a search for something nobody wrote.

Three separate leaks of the same time were closed along the way: the chart drawing it, the
scenario detail API shipping it to the browser regardless, and the phone's enlarged rail
printing it as an axis label.

Places can be investigated. A third kind of subject beside people and the body — the
counter, the back room — reachable from both the roster and the interrogation screen, with
their own findings feeding the same evidence path. 17 of 43 cases carry them.

Topics, not questions. The player names a topic and the interviewer model composes the
actual questions, streaming them as they are written. Topics that yield evidence are marked
in the log so a case can be read back.

The switcher offers only what matches what you are doing — people while questioning,
the body and places while examining. Asking and examining each cost a turn; sliding between
them from the corner of the screen blurred where one act ended.

Also: a month of previously unpushed work (PWA viewport lock, sentence-paced chat log,
alibi clash lines, the scenario definition rework), the dependency update held back from
the AI SDK majors, and every screen matched against its mock.

Migrations

0019 places columns · 0020 the disclosure flag · 0021 place and disclosure data for
seeded rows · 0022 spoiler-neutral public copy for place entries. All additive or
in-place updates; none drop or delete.

After merge

The local seed will be applied to the production D1 by hand. This deletes and re-inserts
all 43 scenarios, which cascades to play_sessions — 7 sessions and 49 messages currently
in production will be lost. Confirmed as acceptable.

Test plan

  • Integration green on develop (build, code check, typecheck, test, commitlint)
  • bun run test — 721 pass / 0 fail
  • all 43 scenarios compile; regenerating seed.sql leaves it unchanged
  • migration columns checked against the seed's inserts
  • after deploy: confirm 00190022 applied to production D1

🤖 Generated with Claude Code

tkgstrator and others added 30 commits August 31, 2026 16:36
Pinch and double-tap zoom broke the fixed layout on phones, so the

viewport meta now pins the scale. iOS ignores user-scalable=no, so body

also opts into touch-action: manipulation.

bump version to 0.2.4

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…imits

- add usePacedReveal hook and paragraphs helper to split assistant replies by sentence, holding back the trailing unfinished one while streaming
- raise question limits for 3-5 person cases (30 -> 45) with a new product cap
- sort characters deterministically for public introductions
- fetch and cache sound effects during deployment
- add direction mockups for desktop review

Co-Authored-By: Claude <noreply@anthropic.com>
Adds mocks/effects.html cataloging the ten UI motions with replay controls, wires it into mocks/storyboard.html as a new section, and includes a reference screenshot.

Co-Authored-By: Claude <noreply@anthropic.com>
…line events

Add timeline_events to scenario_truths and supports to evidences so the
compiled scenario carries a structured timeline the alibi chart can read
without touching the existing narrative timeline. Server computes per-
character alibi segments from discovered revelations/evidence and returns
them from the session endpoint; segments end at the next known event for
that person rather than leaking undiscovered timing.

Add a NewFactBand component and wire interrogation/result screens to
surface newly revealed facts. Reorganize the effects mock catalog into
mocks/effects/* per-effect pages and refresh desktop/mobile mocks to match.

Co-Authored-By: Claude <noreply@anthropic.com>
…tions drive the alibi table

Explain that `kind` on facts decides solid vs. dashed lines, that `participants`/`location` on
timeline events are required for a row to appear, and that `supports`/`relatedFacts` are what
make discovered clues draw new segments.

Co-Authored-By: Claude <noreply@anthropic.com>
…chema

Introduce victim cause-of-death, findings, and found location fields, and
derive an investigable flag so the client can decide whether the body can
be examined without consulting the truth table.

Co-Authored-By: Claude <noreply@anthropic.com>
Add a victim examination flow: compiled scenarios remap victim
findings' evidence/revelation requirements to their scenario uuids so
discovery gates actually resolve, a new server-side sheet builder
exposes only unlocked findings without leaking that hidden ones exist,
and the interrogation screen adds the victim as a special subject
("所見") alongside characters when the case allows examining the body.

Co-Authored-By: Claude <noreply@anthropic.com>
…ation

Pass the character (or victim) picked on the overview screen through the
route search param so interrogation resumes with that target instead of
always starting from the top of the character list.

Co-Authored-By: Claude <noreply@anthropic.com>
… examinations

Examining the victim previously reused the interview question-generator, which
produced lines addressed to the deceased ("涼子さん、〜はありますか?"). Add
composeExaminationFocus/EXAMINATION_INTENT_RULES so examinations get a
self-directed "what to check next" line instead of a question, and route
sessions.ts to it when the target is under examination.

Co-Authored-By: Claude <noreply@anthropic.com>
Add a `pickable` flag to AlibiChart so the victim's column header only
becomes clickable when the scenario allows examining them, and wire the
interrogation screen's victim role label and target-switching through it.

Co-Authored-By: Claude <noreply@anthropic.com>
Carries the target picked from the character list back into the
interrogation screen when resuming, and consumes the `first` search
param once so navigating away and back doesn't snap the conversation
back to it.

Co-Authored-By: Claude <noreply@anthropic.com>
Wrap AlibiChart in a scrollable container across all four screens that
render it, so long time windows scroll within the frame at their real
1min/10px scale instead of shrinking the tick spacing or overflowing
the viewport.

Co-Authored-By: Claude <noreply@anthropic.com>
… for all scenarios

Add victim.foundAt/foundIn/causeOfDeath/findings and victim-sourced motive clues to every scenario, backed by migration 0011 and a new test enforcing the current-authoring guide.

Co-Authored-By: Claude <noreply@anthropic.com>
Let name lists and interrogation screens treat an investigable victim as
a selectable target labeled "調べる" instead of a silent, disabled
"被害者" row. Adds a mobile target-swap control since the phone layout
has no column headers, and switches the mobile chat background to
opaque so overlapping text stays legible on narrow screens.

Co-Authored-By: Claude <noreply@anthropic.com>
Add an optional `who` param to `Mock.play` so callers can pin the
current speaker; without it, repeated calls (e.g. device switching)
could silently reassign the previously chosen target.

Co-Authored-By: Claude <noreply@anthropic.com>
Timeline location may reference floorPlan.rooms[].id instead of a
place name when the case has a floor plan. Resolve it to the room's
label so the compiled table doesn't show a raw room ID; fall back to
the raw value when it isn't a known room id.

Co-Authored-By: Claude <noreply@anthropic.com>
…tion

Co-Authored-By: Claude <noreply@anthropic.com>
Deriving a UUIDv5 from the filename and threading it through compile so re-seeding
deletes the old row by id. Deleting by title left duplicate rows behind when a
scenario's title changed between runs.

Co-Authored-By: Claude <noreply@anthropic.com>
…ions guidance

Bumps judgeRubricKey to v2 so cached rubrics don't serve stale instructions for up to an hour after deploy, and tightens suggestedQuestions prompt guidance to phrase suggestions as player questions rather than instructions or disclosure-condition paraphrases.

Co-Authored-By: Claude <noreply@anthropic.com>
The tsukimisou scenario combined Kiryu passing Mizuki in the corridor with
Mizuki entering the study into one event, which made the alibi table place
Kiryu in the study too. Split into two events by actual location and update
the authoring guide to require this split, plus adjust the timeline length
test.

Co-Authored-By: Claude <noreply@anthropic.com>
Note that all 43 scenarios now populate location, and describe the
compile-time room-id-to-name resolution for scenarios with floor plans.

Co-Authored-By: Claude <noreply@anthropic.com>
- drop legacy fields (tags, secret, role, about, reveal.mode, requiredFacts, quality) from the authoring format
- add migrations 0012-0018 for evidence detail, death time, clash materials, and required records
- require estimatedDeathAt for time-deception cases and record backing for physical-evidence timeline events
- add authoringWarnings to flag evidence contradictions with no timeline anchor
- retitle scenarios, dropping duplicate and legacy titles
- update interrogation UI, alibi chart, and mobile safe-area mocks

Co-Authored-By: Claude <noreply@anthropic.com>
…of hardcoded px

Store clash participants/time in C.clash and resolve column indices at
render time so the line and end pins track cast/column changes instead
of drifting when layout shifts. Add pin-lift-x for the alibi table's
vertical time axis, mirroring pin-lift's horizontal counterpart, and
sync the direction doc and interrogation mock accordingly.

Co-Authored-By: Claude <noreply@anthropic.com>
Track who told each lie and which character's evidence broke it, so
clashOf can return the pair the dashed line connects instead of just
a timestamp. AlibiChart now derives the line's column endpoints and
width from actual column layout instead of hardcoded pixels.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…scenario

Co-Authored-By: Claude <noreply@anthropic.com>
The clashOf logic in src/server/game/alibi.ts draws a clash line between
two people's columns, so it also needs the evidence that breaks a lie to
come from a character other than the liar. authoringWarnings only checked
timeline/facts linkage, so scenarios with clash-eligible lies but
same-source evidence silently produced no clash line. Update the warning
condition and docs accordingly.

Co-Authored-By: Claude <noreply@anthropic.com>
The catalogue drew motion three on a horizontal rail with pin-lift, but
the alibi chart runs vertically, so its pins are horizontal bars that
stretch along their own length via pin-lift-x. The card no longer showed
what the app actually does, which defeats the catalogue's purpose as a
comparison surface. Redraw it as two columns with the line drawn between
them and both end pins lifting once the line settles, matching AlibiChart.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Screenshots belong in screenshots/, which .gitignore excludes, so this one
should never have been tracked at the root. The file stays on disk for
local reference; only the tracked copy goes away.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The two conditions for the clash mark are easy to satisfy on paper and
still produce nothing on screen, so authors need to see what they are
aiming for. Track the screenshot under docs/images/ and reference it from
the lies section, where both conditions are explained.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
tkgstrator and others added 27 commits September 1, 2026 23:56
The catalogue pages still drew motion three on a horizontal rail with
pin-lift, while the screen mock, AlibiChart and the Storybook catalogue
all show the vertical table where a pin is a horizontal bar stretched by
pin-lift-x. Anyone looking up the motion got a different picture depending
on where they looked, and the rail form hid why the vocabulary needs both
orientations. Redraw both catalogue pages as two columns with the line
drawn between them and the end pins lifting once it settles.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…nterrogation

Introduces a separate "places" group in case-overview so scenes/bodies can be
examined distinctly from cast members, unifies the row-rendering logic for
cast/victim/place entries, and updates interrogation.html to reflect
examine-mode copy and headline text when a place or the victim is selected.

Co-Authored-By: Claude <noreply@anthropic.com>
…har count

Switch the interrogation log auto-scroll from tracking accumulated text length to a ResizeObserver on the log content element, since text length changes before paced-reveal actually renders new lines, leaving the view short of the bottom. Also fix the hint element's [hidden] override in the phone mock CSS to scope the display:none to .hint instead of all elements.

Co-Authored-By: Claude <noreply@anthropic.com>
Redrawing motion three in the vertical form left pin-lift with no user:
the alibi chart is vertical, so only pin-lift-x is ever drawn. The pair is
worth keeping because it shows that a pin's orientation follows its time
axis, but an unused rule sitting silently is the very thing that made the
motion vocabulary drift out of step with the app in the first place. Note
the reason at each of the four copies.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Went through all eight screens against mocks/ and closed the gaps the
comparison turned up. Two were structural rather than cosmetic: the case
overview had no "where to investigate" section at all, and the result screen
was missing the whole accused-time comparison on mobile. The rest is line
work — rules, spacing, leading, and the pigment assignments.

Shared parts moved too: the block button takes the mincho face and wider
tracking the mocks use for it, the accusation outline goes to full shu, the
select arrow becomes a filled triangle, the textarea border moves to keisen,
and avatar discs go neutral so only the letter carries the character's colour.

Interrogation also gained the examine state the mocks already described:
places sit alongside people, and the wording turns from asking to examining.

Co-Authored-By: Claude <noreply@anthropic.com>
develop already carries the pinch-zoom fix as #11, squashed. This branch still
has the original commit for it, so the same change arrived twice and index.css
conflicted. Kept this branch's body block, which is a superset — it has the
touch-action line develop brought plus the font smoothing and safe-area padding
added since.

Co-Authored-By: Claude <noreply@anthropic.com>
…scenario schema, and the mock pass (#13)

A month of unpushed work: PWA viewport lock, sentence-paced chat log, alibi
segments and clash lines, the examination step, the scenario definition schema
rework, the mock catalogue, and finally the pass that matched all eight screens
to their mocks.
…erver

Extract the 500-char topic limit into a single MAX_TOPIC_CHARS constant in src/shared/turns.ts, reduce it to 140, and use it from both the input field and the server-side schema so the two can't drift apart.

Co-Authored-By: Claude <noreply@anthropic.com>
…ve's question

Ports the two pieces of #4 that develop never picked up. The rest of that
branch — the interviewer, the topic flow, the DO's topic persistence — had
already landed here by another route, which is why the branch would not merge.

Yielded topics: the session DO now keeps a list of which rounds produced
evidence or a revelation, the history reports it, and the chat log lifts those
topic lines to the main text colour. No new pigment — the palette belongs to
the characters, and a topic is nobody's. Reading back a case, the lines that
mattered are the ones that stand out.

Streamed questions: the interviewer now streams, so the detective's question
appears as it is written rather than arriving whole after a silence. The
question-start event is only sent once the first chunk lands, so a round where
the model returns nothing leaves no empty line behind. Examination keeps its
one-shot phrasing — that line is short enough that streaming it shows nothing.

Not ported: RevealAnnounce. It announces new clues in the centre of the screen,
which NewFactBand already does here, and the band is what the mocks specify.

Co-Authored-By: Claude <noreply@anthropic.com>
…ve's question (#14)

Ports the two pieces of #4 that develop never picked up: the shared topic
length constant, the yielded-topic mark through DO/history/restore/log, and the
streamed interviewer question. RevealAnnounce is deliberately left out —
NewFactBand already covers it and matches the mocks.
The board asserted a death estimate the player could not yet know. In the
Tsukimisou case the record says the body was found around 20:30, but the chart
drew 20:15 "death estimate" from the first turn, before anyone had examined
anything. The data already separated foundAt from estimatedDeathAt; only the
drawing collapsed them.

So the deadline becomes a window rather than a line. The discovery time is
public and always drawn solid. The estimate is drawn by how it was come by:

1. fixed    solid, the time as it is
2. range    a span mark with end caps, both ends solid
3. unknown  dotted across the whole known window, with ? instead of a time
4. claimed  dotted, ? beside the time, and whose word it rests on, in their ink

Splitting 1 from 4 is the point. The same 18:50 means something different when
the detective examined the body than when a bystander says so, and if that
bystander is the culprit, the two will not agree once the examination lands.

Nothing is filled — the window is a line with returns at each end. A shaded
band would compete with the suspects' own bars.

Desktop only for now; the phone chart still draws the old single line.
Reasoning is written up in docs/design/deadline-window.md.

Co-Authored-By: Claude <noreply@anthropic.com>
Switching #death= one at a time shows each state but never lets you compare
them, and comparing is the whole point: the difference between a time the
detective established and the same time a bystander asserted is only legible
next to each other.

The marks come from Mock.deathMarks, the same call the alibi table makes, so
the sheet cannot drift from what the screens actually draw. The stage keeps the
table's scale for the same reason — a comparison at a different scale is a
comparison of something else.

Co-Authored-By: Claude <noreply@anthropic.com>
The phone lists places in its swap nav; the desk had no way in at all, because
switching there is done by the alibi table's column headings and places hold no
column. The mock said as much, with a "still" in it — a note left for later
rather than a decision.

The consequence was that the same case played differently by device: on a phone
you step from the counter to the back room where you stand, on a desk you go
back to the prep screen first.

So the desk gets its own way in, beside the nameplate rather than in the table.
Grey, no pigment: places do not answer, and the colours belong to those who do.
The one you are already looking at is left out — an entry that does nothing when
pressed is worse than no entry.

Co-Authored-By: Claude <noreply@anthropic.com>
Asking and examining are two different moves, each costing a turn. Listing them
together let you slip from a conversation into a search from the corner of the
screen, which blurs where one act ends and the next begins.

So the switcher now offers only what matches what you are already doing: people
while questioning, the body and the places while examining. Crossing between the
two goes back through the prep roster, which is where you chose in the first
place.

On the desk this means the place list appears only while examining; people were
never in that corner anyway, since the table's column headings have always been
the way to them, and they stay reachable throughout.

Co-Authored-By: Claude <noreply@anthropic.com>
- Introduce places as a first-class target for `ask`, alongside characters and the victim: schema, migration, compile-scenario split into public/truth findings, and location-typed evidence sources now resolve against places too.
- Add a deadline window derived from victim discovery/estimated-death times, revealed only after the body has been examined, and render it across AlibiChart, CaseOverviewScreen, InterrogationScreen, and ResultScreen.
- Update scenario YAMLs, fixtures, stories, and docs to cover the new places and deadline data.

Co-Authored-By: Claude <noreply@anthropic.com>
The schema, the server and the screens could all handle places, but nothing
joined them: only Storybook ever passed any, so in the running app the section
never appeared no matter how far the case progressed. The two routes now pass
what the API already returns.

The nameplate takes `situation` rather than `introduction`. The latter is the
roster blurb — "the ground floor, the till and the ledger" — which describes the
place rather than what is in front of you. What belongs under the name while you
are standing there is the other line: the clearing-up stopped halfway.

The authoring guide gains a places section: the shape, why findings are
mandatory (a place on the roster is always worth examining), the ID rules and
why they exist, matching room IDs when there is a floor plan, and the line
between `situation` and `findings` — the first is public and keyword-checked,
the second is earned. Also a reminder that a place with nothing sourced from it
is a turn spent on nothing.

Co-Authored-By: Claude <noreply@anthropic.com>
…tcher

In easy mode every character carries a remaining count in the alibi table's
column heading, and every room on the floor plan carries one too. Places fall
between: they hold no column, and a place that is not also a drawn room appears
on no plan either. The count the server computes for them reached nothing.

So it goes beside the name in the switcher, which is the only surface a place
has. People and the body deliberately do not get one there — their number
already stands in the column heading, and the same figure in two places reads
as one of them being stale.

Co-Authored-By: Claude <noreply@anthropic.com>
The desk drew the window; the phone was still drawing one line. Same vocabulary
now, turned ninety degrees onto the rail.

Two things had to change rather than shrink. Labels cannot sit beside their mark
at 390px — discovery stands at 92% of the axis, so its label would fall off the
edge; they go on a row beneath instead, a point-mark's label right-aligned to it
and a window's centred on its span. And the accusation's shu tick, which defaults
to 18:50, was covering the deadline mark outright; it now stops at the row above,
so solid and dotted are told apart below it.

The enlarged rail also loses the 18:50 tick it used to print in the middle. That
was the board naming the estimate before anyone had earned it — the same fault
this whole change exists to remove.

Co-Authored-By: Claude <noreply@anthropic.com>
The estimate used to appear once the body had been examined. That was a
placeholder standing in for the real rule, and it closed the other door: a case
with a doctor in it should let you reach the same time by asking them.

Evidence now carries `revealsDeathTime`. Hold one such piece and the board's
dotted window resolves to a solid line; hold none and it stays unknown to the
end. The mark sits on evidence alone rather than also on findings, because the
server can tell whether a piece of evidence has been discovered — a finding only
records whether it may be shown, so a mark there could never be read back.

The server decides. Sending the client a table of which evidence opens the
estimate would hand the board a key to its own answer, so it computes the state
and ships the resolved time or null.

While here: `read/scenarios.ts` was putting `estimatedDeathAt` into the case
detail. The board drew "unknown" while the browser already held 20:15 — the
whole point of hiding it, undone one layer down. Removed.

Tsukimisou gets two ways in, the examination and the doctor's account, so that
choosing one route does not lock the deadline away for the rest of the case.

Co-Authored-By: Claude <noreply@anthropic.com>
…re that opens them (#15)

The board no longer asserts a death estimate the player has not earned: the
discovery time is public and solid, the estimate stays a dotted window until a
piece of evidence marked revealsDeathTime is found. Places become a third kind
of subject alongside people and the body, reaching the screens for the first
time. The switcher offers only the kind you are already working with.
Lets an evidence entry mark that it discloses the time of death, feeding the interrogation deadline disclosure logic.

Co-Authored-By: Claude <noreply@anthropic.com>
…mate

Add hasEstimatedDeathAt so the board can tell "not yet found" apart from
"this case has no death estimate at all", instead of showing the
searching-dashes-and-? mark for cases that never had one.

Co-Authored-By: Claude <noreply@anthropic.com>
Two cases the guide did not cover, both easy to get wrong and neither caught by
the validator.

Leaving `estimatedDeathAt` out is now a decision, not an omission: the board
draws no estimate mark at all. Dotted-and-question-mark means "there is
something here to find", so showing it where nothing was ever written asks the
player to search for what does not exist.

And writing the time without marking any evidence with `revealsDeathTime` is the
worst of the three: the answer exists, no route reaches it, and the board keeps
inviting a search that cannot end. Both halves validate fine on their own, so
nothing will tell the author but a playthrough.

Co-Authored-By: Claude <noreply@anthropic.com>
…ship the missing migration (#16)

The 0020 migration was left untracked when #15 was staged, so develop described
a column nothing created. And deadlineOf folded "no estimate in the scenario"
into "not discovered yet", drawing a dotted window with a question mark for 33
cases where no time was ever written — an invitation to search for nothing.
…e death estimate (#17)

Scenario data for the machinery #15 and #16 shipped. Places go from 2 cases to
17, and every case that names a death estimate now has evidence marked to
disclose it — the nine that had a time written and no route to it are gone.

Two migrations carry it to databases already seeded. The second rewrites only
the public half of each place entry, leaving the private findings alone: what
the roster and the nameplate show is public, what examining turns up is earned.
master sits at 0.3.0 because the dependency update went straight there without
passing through develop, so develop's 0.2.4 is behind the released version while
carrying a month more work. 0.4.0 clears both.
The dependency update went to master directly, so develop never received it and
the release PR would not merge. This brings it back: the held-back AI SDK pins,
the Biome 2.5 config, and the devcontainer Node bump.

The auto-merge resolved package.json to master's 0.3.0, which would have
released the version that is already out. Set back to 0.4.0.

Co-Authored-By: Claude <noreply@anthropic.com>
The dependency update went to master directly and never came back, so the
release PR would not merge. Git's auto-resolution also took master's 0.3.0 for
package.json, which would have released the version already out; restored to
0.4.0.
@tkgstrator
tkgstrator merged commit 8557993 into master Sep 2, 2026
9 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant