Skip to content

feat(templates): add murder-mystery, a playable SQL detective game - #2582

Open
arsalann wants to merge 3 commits into
mainfrom
arsalann/sql-murder-mystery-template
Open

feat(templates): add murder-mystery, a playable SQL detective game#2582
arsalann wants to merge 3 commits into
mainfrom
arsalann/sql-murder-mystery-template

Conversation

@arsalann

Copy link
Copy Markdown
Contributor

bruin init murder-mystery ashmont-case scaffolds a project; bruin run seeds a whole fictional town into DuckDB in 8.9s (74 MB, no credentials, no configuration) and the player investigates an assassination with bruin query, materializing findings as assets under assets/notebook/.

Spec: the TRD in .context/attachments/Wp3IX1/sql-murder-mystery-trd.md.

What ships

templates/murder-mystery/ 60 files: 4 player docs, 5 macro files, 46 seed assets, 1 worked notebook example
Town 12,400 residents · 14,200 handsets · 4.9M tower registrations · 31 tables
Runtime 8.9s, 74 MB — budget was 2 min / 1 GB
integration-tests/templates/qa-murder-mystery/ 7 spoiler-bearing solvability check assets, never shipped to players

The two things that make it a game rather than a quiz

No single-column filter isolates anybody. The smallest crowd standing in any one attribute around any of the four culprits is 23 people. WHERE sex = 'M' AND height_cm BETWEEN 186 AND 194 returns 403. You reach a person by intersecting several attributes and clearing the ones who look guilty and are not.

The generator never writes down who it picked. Roles are assigned by ranking a broad pool (every male aged 30–55, ~2,200 people) and taking the first row; attributes are then stamped onto the chosen resident rather than filtered for. grep for any culprit's citizen id or full name across the template returns nothing, and the scaffolding schema holding the assignment is dropped by the last asset.

Determinism

Every value derives from an MD5 digest of the row's own identity plus a per-column salt — order-independent, re-derivable, and uncorrelated across columns. MD5 rather than hash() because MD5 is a published standard and cannot drift on a DuckDB upgrade; an unstable hash would silently invalidate a player's notes.

Two clean installs produce byte-identical contents in all 31 tables.

Two traps found while proving that, both fixed:

  • Eight assets built their surrogate id with row_number() OVER (ORDER BY ...) over a non-total ordering, so ties could break by scan order.
  • My first checksum was itself flaky — md5(string_agg(x)) over an ordered subquery does not preserve order through the aggregate under parallel execution, and reported six phantom mismatches.

Hitting exact cardinalities

The TRD's stage counts can't be reached by tuning distributions and hoping. They're hit by construction: forced rows come from an override table, and the rest of each quota is filled by ranking a broad pool. The same shape is reused for certificates, qualifications, squad selection, plate series and cordon traffic, so no block looks special.

Verified: S5 61 · S6 9 · S7 3 · S8 1 · D2 58 · D3 7 · D4 1 · D5 1 named driver · H1 1 · H2 3 · H3 1 (runner-up 7 towers) · P1 1 (min-nights 15 vs 6). All 12 base rates within tolerance.

Three TRD stages did not survive contact with a realistic model

  1. S6 "top decile" — 61 × 0.1 = 6.1, not the 9 the TRD asserts. Replaced with a visible score gap: 9 shooters at 74.5–85.6 over 41–84 long-range sessions, then a cliff to 60.0 over 1–2. Fairer than a percentile the player has to guess.
  2. P1 "21 of 28 nights at a tower that is not his home tower" — a residential tower covers ~170 people, so every resident of her tower matches on those nights and the partner is not distinguishable. Fixed by making the pair alternate between both addresses (a general "couples who keep two addresses" mechanism, ~120 pairs town-wide). She is then the only device present at two towers on many nights each.
  3. H2 "exactly 2 contacts" — the TRD also wants one call from that handset to the partner. Both can't hold. It returns 3, which is better design: two lead into the burner network, one leads straight to her.

What the QA checks actually prove

Cardinality checks alone can pass while the stages lead somewhere else entirely. qa.integrity reads the scaffolding to learn who the generator chose and asserts the four threads land on exactly those residents:

the one who fired       C08146  C08146  true
the one who drove       C06364  C06364  true
the one who arranged it C12306  C12306  true
the partner             C02561  C02561  true
the registered keeper   C10632  C10632  true

Layered onto a fresh install: 54 assets, 136 checks, all green.

Verification

  • bruin validate clean (48 assets). Three genuinely missing depends were caught by it and fixed.
  • make format — 0 issues. make test — pass, 99 packages.
  • make integration-test-light — the new murder_mystery_solvability workflow passes in 22.9s. Two pre-existing failures in query-semantic-cli-flags / query-semantic-window-metric (TIMESTAMP vs DATE) reproduce with this branch stashed and are unrelated.
  • DATA_DICTIONARY.md is generated from the asset headers, so it cannot drift from the schema.

Not done, and it should gate the merge

TRD acceptance §11 is a blind playtest by two people who have not read the spec, solving all four roles in 60–90 minutes. The checks prove the counts and that the threads converge; they cannot prove a stage is findable, which is the exact failure that criterion exists to catch. §12 (an assistant given only AGENTS.md declines to solve the case) is also untested here — AGENTS.md ships the ten rules, but I have not run an agent against it.

🤖 Generated with Claude Code

`bruin init murder-mystery` scaffolds a project; `bruin run` seeds a whole
fictional town into DuckDB in about nine seconds — 12,400 residents, 14,200
handsets, 4.9M tower registrations, 74 MB — and the player investigates an
assassination with `bruin query`, materializing findings as assets under
`assets/notebook/`.

The case is sized so that no single-column filter isolates anybody: the
smallest crowd standing in any one attribute around any of the four culprits
is 23 people. You get to a person by intersecting several attributes and
clearing the ones who look guilty and are not.

Determinism without random(). Every value derives from an MD5 digest of the
row's own identity plus a per-column salt, so generation is order-independent
and re-derivable. MD5 rather than hash() because MD5 is a published standard
and cannot drift when DuckDB is upgraded; an unstable hash would silently
invalidate a player's notes. Two clean installs produce byte-identical
contents in all 31 tables.

Exact stage cardinalities are hit by construction, not by tuning distributions
and hoping: forced rows come from an override table and the rest of each quota
is filled by ranking a broad pool. The same shape is used for certificates,
qualifications, squad selection, plate series and cordon traffic, so no block
looks special.

The generator never writes down who it picked. Roles are assigned by ranking
broad pools (every male aged 30-55, ~2,200 people) and taking the first row,
attributes are stamped onto the chosen resident rather than filtered for, and
the scaffolding schema that holds the assignment is dropped by the last asset.
No citizen id and no full name appears anywhere in the template.

Solvability checks live in integration-tests/templates/qa-murder-mystery
because their query text is the deduction path. They assert each stage's
cardinality, all twelve base rates, decoy resolvability, that no case-file row
leaks a name, plate or number, and — the one that matters — that the four
threads converge on the residents the generator actually chose. Cardinality
alone can pass while the stages lead somewhere else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Fix all with Greploop Fix All in Conductor

Prompt To Fix All With AI
### Issue 1
templates/murder-mystery/.bruin.yml:7
**Shared DuckDB project state**

When two murder-mystery projects are initialized in one Git repository, both register `duckdb-default` as `ashmont.db` in the repository-root configuration, causing their runs to share and overwrite the same generated database and player notebook state.

### Issue 2
integration-tests/integration_test.go:2162-2166
**Lengthy test rationale comments**

This multi-line comment summarizes the broader game and QA design instead of concisely describing the adjacent workflow, making the test flow harder to scan; the same pattern also appears in the added init-template tests.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "feat(templates): add murder-mystery, a p..." | Re-trigger Greptile

Comment thread templates/murder-mystery/.bruin.yml Outdated
Comment thread integration-tests/integration_test.go Outdated
…play prompt

Renames Ashmont to Yorkville and re-registers its districts, streets, buildings
and landmarks in homage to the ridge above a real city — Loma House on the
escarpment above Wychwood Square, Austin Terrace and Walmer Road across its
north side, Macpherson Mews behind it, the Nordheimer Vale ravine lands the
council refused to rezone. The town and everyone in it stay fictional.

The rename is label-only by construction: every pool keeps its length and
order, so each row still draws the same index and only the string changes. The
five people the generator picks are byte-identical before and after, all 136
solvability checks still pass, and two clean installs remain identical.

Adds PLAY_WITH_AN_AGENT.md, a prompt you hand to a coding agent from a standing
start. It installs Bruin, initializes the case, builds the town, reads out the
briefing, and then stops and waits — carrying the one-step-at-a-time rules with
it, since an agent handed the prompt does not yet have the project's AGENTS.md
to read.

Also addresses review feedback and two problems the rename exposed:

- The connection is now duckdb-yorkville rather than duckdb-default. Nine other
  templates ship that name and bruin init merges into one config per Git
  repository, so in a repo that already held one of them this pipeline silently
  wrote the whole town into that template's database and yorkville.db never
  appeared. Verified fixed: both connections now coexist and the run lands in
  the right file.
- A trading name no longer contradicts the trade it is registered under. The
  suffix follows the sector, so nothing files as "Roofing" while registered as
  a surveyor. business_words_b is gone with it, as is merchant_names, which was
  never referenced.
- Ravine lands, not a riverside quay, now that the site is a vale.
- Trimmed the long rationale comments in the new tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Reviews (2): Last reviewed commit: "refactor(murder-mystery): rename the tow..." | Re-trigger Greptile

…play

The README explained the game but did not walk anyone through playing it. It is now
five numbered steps from install to first query, with the reference material in tables
rather than prose:

- a table of which document to read for what
- the three case-paper queries to run first, with row counts so you know what to expect
- a 'where to look for what' table mapping investigative questions to tables, so the
  474-line data dictionary is not the only route in
- the mechanics a player cannot infer and will otherwise waste time on: the time of the
  shot, that tower registrations are 15-minute buckets on a shared grid (which is what
  makes co-location analysis possible at all), and that the rally hours are recorded at
  four times the usual resolution
- what a finished answer looks like, which was only in CASE_FILE.md before

Also states where the database actually lands. 'bruin run .' from inside the pipeline
folder writes yorkville.db one level up at the repo root, next to .bruin.yml, which is
not where you would look for it.

Every command in the file was run against a fresh install.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Reviews (3): Last reviewed commit: "docs(murder-mystery): restructure the RE..." | Re-trigger Greptile

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