This is the ordered implementation roadmap following the accepted exhibit model. Work generally proceeds from top to bottom. It is not a substitute for migrations or implementation issues.
The narrative layer — campaigns, NPC cutscenes, the admin authoring panel, and the LLM cognitive shim that keeps players oriented through complex real-world scam cases — is tracked separately in [narrative-todo.md](narrative-todo.md). It depends on Milestone 5 (Case Report / Claims) for the assistant's read-only view of the player's reasoning.
- [x] Add one browser smoke test: open an isolated level, drag an exhibit, pan, board-zoom, open a folder, move a file, reload, and verify persistence.
The purpose of this milestone is the gameplay loop, not a knowledge graph: the player connects two exhibits, explains that one specific thread with a luggage-tag Claim, and later discovers that their accumulated explanations have become a nearly complete case report.
### 5.1 Lock the gameplay and temporal rules
- [ ] Define a Claim as an entity owned by exactly one investigative thread; a thread has zero or one Claim. (The text associated with a claim can prove multiple points, handed by the text.
- [ ] Keep untagged threads as ordinary connections that do not appear in the report.
- [ ] Use the same Claim text on the luggage tag and in the report. Editing either presentation updates the same database value.
- [ ] Derive the Claim date from the earliest non-null temporal date of its two endpoint exhibits; never use `created_at` or the current time.
- [ ] Use Event `occurred_at` and a Document's primary timeline date as direct endpoint dates. For a Folder, use the earliest dated contained Document. Leave a Claim undated when neither endpoint supplies a date.
- [ ] Place undated Claims after dated Claims in the initial report order while keeping them fully editable and reorderable.
- [ ] Treat the derived date as the initial chronological suggestion only. Manual report ordering must not rewrite exhibit or Claim dates.
### 5.2 Add normalized persistence
- [ ] Add a `claims` table with a unique foreign key to `exhibit_connections`, text, tag style, position percentage, lateral offset, and timestamps.
- [ ] Move luggage-tag-specific text and placement fields out of `exhibit_connections`; retain curve tightness and endpoints on the connection.
- [ ] Add one level-owned `case_report` and normalized `case_report_claims` rows with explicit `sort_order`.
- [ ] Assign stable, level-local display numbers to cite exhibits as `Exhibit 3` independently of board position, z-index, or report order.
- [ ] Enforce same-board ownership for the Claim's connection, both endpoint exhibits, report, and report membership.
- [ ] Delete a Claim and its report membership transactionally when its luggage tag is removed, while retaining the now-untagged thread.
- [ ] Delete both the Claim and connection when the thread itself is removed.
- [ ] Clone board-owned Claims with fresh IDs during template creation and instantiation; rebuild level report membership against the cloned Claim IDs.
- [ ] Make reset discard player-created Claims and restore exactly the Claims present in the source template version.
- [ ] Keep uploaded binary evidence in MinIO. Reports and Claims reference Document exhibits and asset metadata; they never duplicate or embed asset bytes.
### 5.3 Expose a focused API contract
- [ ] Add a typed Claim DTO to an investigative connection instead of exposing a free-form connection `label`.
- [ ] Return each Claim's derived date and its two stable exhibit citations in the report response.
- [ ] Add granular operations to create, edit, and remove a Claim without replacing the complete board state.
- [ ] Add a report endpoint that returns ordered Claim rows and an atomic reorder operation.
- [ ] Reject empty Claim text, invalid connection ownership, duplicate Claims on one connection, and report orders containing foreign or duplicate Claim IDs.
- [ ] Protect Claim and report writes with the board revision so concurrent saves cannot silently overwrite reasoning.
### 5.4 Turn luggage tags into Claim widgets
- [ ] Change the thread prompt from “Add tag” to “Explain this connection,” with a secondary option to leave the thread untagged.
- [ ] Create the Claim and its luggage-tag presentation in one interaction, preserving the current tightening animation.
- [ ] Keep both `LUGGAGE` and `COMPACT` as visual presentations of the same Claim entity while playtesting them.
- [ ] Preserve draggable percentage and tension-constrained lateral offset as Claim presentation state.
- [ ] Rename tag-oriented frontend types and commands to Claim terminology without changing the established visual design.
- [ ] Give a newly created Claim a subtle “added to report” ink animation or badge without interrupting board work.
### 5.5 Build the typewriter case report
The end goal is that a case report is prepopulated by the claims the player made during the investigation so that a skeleon of the case solution is present. The player must simply edit the case report and submit it.
- [ ] Add **Case Report** as a primary menu item and implement it as a persistent board view, separate from exhibits and the timeline.
- [ ] Provide an empty state that explains that explaining red threads will create the report, without revealing a solution or forcing a tutorial.
- [ ] Initially arrange Claim rows chronologically by derived date, using the order parameters on the claim as tie-breaker and undated Claims last, below a horizontal rule that says (missing date)
- [ ] Render each row as a typewritten Claim with its date and endpoint citations, for example: `14.10.1987 — Maria Voss redirected the shipment. Exhibits 4 and 7.`
- [ ] It needs to be possible to add free text before and after the claims. Coloured inline text (use span elements) have a specific class and id can be edited
- [ ] -Make Claim text editable inline. Persist through the Claim API so the luggage tag updates immediately.
- [ ] Support pointer and keyboard reordering of Claim rows and persist the resulting explicit report order into order column of the claim.
- [ ] Clicking a Claim must minimize the report as appropriate, center its thread, and briefly illuminate the curve and luggage tag.
- [ ] Clicking an exhibit citation must locate and highlight that exhibit using the existing tray/board locator treatment.
- [ ] Ensure the report is legible and operable on portrait mobile layouts as well as desktop.
- [ ] Add restrained typewriter, paper, and ink feedback while respecting reduced-motion preferences.
### 5.6 Test the reasoning loop
- [ ] Unit-test endpoint date resolution for Event, Document, Folder, partially dated, fully undated, and invalid-date cases.
- [ ] Migration-test the one-Claim-per-thread constraint, cascading behavior, same-board enforcement, and report ordering constraints.
- [ ] Integration-test create/edit/remove Claim, two-way text synchronization, report reorder, reload persistence, reset, and template cloning.
- [ ] Verify that Claims citing uploaded Document exhibits survive cloning while their immutable assets remain shared through MinIO.
- [ ] Add a browser test that creates several connections out of chronological order, explains them, opens the report, edits and reorders the Claims, then returns to each highlighted thread.
- [ ] Play the Glass Harbor mystery using Claims as the primary reasoning mechanism and record whether the generated report makes the conclusion emerge naturally.
- [ ] Revise prompts, animation, initial ordering, and report typography based on that playtest before adding automated evaluation.
### 5.7 Deliberately deferred
- [ ] Design a hidden, template-versioned solution rubric only after the deterministic Claim/report loop is enjoyable and dependable.
- [ ] Add LLM report evaluation as a later milestone with structured citations, calibrated uncertainty, and reproducible evaluator output.
- [ ] Do not add general-purpose semantic edge roles, Claim hubs, Claim-to-Claim links, or a knowledge-graph ontology as part of this milestone.
### Milestone 5 definition of done
A player can connect exhibits, explain each connection with a luggage-tag Claim, discover those exact words in a chronological typewriter report, improve and reorder the Claims, follow every citation back to the board, reload without loss, and reset safely to the template. No LLM is required for this experience to work.