Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 164 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,44 +1,181 @@
# Mansão

Jogo de mistério investigativo onde os suspeitos são NPCs com LLM. Você
interroga em texto livre, confronta com provas e acusa. A parte difícil não é o
chat — é gerar casos que sejam de fato dedutíveis e impedir que um NPC conte o
que ele não deveria saber.
An investigative mystery game where the suspects are LLM-driven NPCs. You
question them in free text, confront them with evidence, and name a culprit.

**Estado:** fase 0 (fundação). Ainda não é jogável.
The chat is the easy part. The hard part is generating mysteries that are
actually solvable, and keeping a language model from telling you something its
character has no way of knowing.

## Rodar local
![CI](https://github.com/Madeuss/firenze/actions/workflows/ci.yml/badge.svg)

Requisitos: Docker, [uv](https://docs.astral.sh/uv/) e `make`. O uv cuida do
Python — não precisa instalar a 3.13 na mão.
> **Status: phase 1 of 8.** The case generator and its solver work. There are no
> NPCs yet — the next phase gives one suspect a voice. Not playable.

```bash
cp .env.example .env
make dev # sobe Postgres 16 + pgvector, Redis e a API
curl localhost:8000/health
## See it work

```console
$ make case SEED=42 LOCALE=en

Case 42 (generator v2, en)

CAST
victim Rodolfo Andrade (victim)
sus-1 Vitória Belmiro
sus-2 Clarice Antunes
sus-3 Teodoro Mainz
sus-4 Godofredo Alves
sus-5 Ilma Prado
sus-6 Nazareno Cruz

WHAT IS KNOWN
F-001 Rodolfo Andrade's body was found in the conservatory, around 9:30 pm.

DOSSIERS (what each suspect knows)
sus-1 — Vitória Belmiro: 7 facts
sus-2 — Clarice Antunes: 4 facts
sus-3 — Teodoro Mainz: 10 facts
sus-4 — Godofredo Alves: 5 facts
sus-5 — Ilma Prado: 8 facts
sus-6 — Nazareno Cruz: 8 facts

Solution withheld. Use --reveal to see it.
```

Six dossiers, six different sizes. Nobody sees the same night, and nobody sees
the answer — including the code that builds their context.

Add `REVEAL=1` and the solver shows its work:

```console
SOLVER
deducible: True
deduced: sus-1
chain: F-001, F-005, F-006, F-007, F-008, F-009, F-022

SECRETS (why the innocent lie)
sus-2: Clarice Antunes was keeping objects that did not belong to them — and was in the wine cellar at 9:00 pm.
sus-3: Teodoro Mainz forged a signature in the ledger — and was in the study at 10:30 pm.
sus-5: Ilma Prado was hiding gambling debts — and was in the study at 10:00 pm.
```

A API sobe em `localhost:8000` (docs em `/docs`), o Postgres em `localhost:5433`
— porta 5433 de propósito, para não conflitar com um Postgres instalado na
máquina.
Every innocent has a secret that has nothing to do with the murder. Without
that, the game collapses into *whoever looks nervous did it*.

## What is hard here

### A mystery nobody can solve is a broken game

Sem Docker, dá para rodar só a API contra serviços seus:
Roughly half of the mysteries you get by sampling constraints at random are
unsolvable, and they *look* fine until a player wastes an hour on one. So a case
is not publishable until an automated solver proves a deduction path exists.

[`generation/solver.py`](apps/api/src/mansao/generation/solver.py) takes `Case`
and never `CaseWithSolution` — it cannot read the answer, and the type signature
is what guarantees that rather than the discipline of whoever writes the next
function. It starts from the public facts, adds everything a suspect would
reveal if asked, and only approves when exactly one suspect is left without an
alibi and physical evidence points at them. A case that fails is discarded and
regenerated.

### Isolation is a data boundary, not a prompt instruction

"Do not reveal the solution" in a system prompt is a wish. The solution is a
separate entity from the case, so the function that assembles an NPC's context
takes a type that has no path to the culprit
([`domain/models.py`](apps/api/src/mansao/domain/models.py)).

```mermaid
flowchart LR
S([seed]) --> G[generator]
G --> C[Case<br/>facts · scopes · timeline]
G --> X[Solution<br/>culprit · means · motive]
C --> V{solver<br/>deducible?}
V -- no --> G
V -- yes --> P([playable case])
C --> N[NPC context]
X -. never .-> N
```

Every secret fact also carries a canary token. A canary appearing in model
output is a critical failure: the response is discarded and the incident logged.
The CI gate for leakage is 0% and it blocks merges.

### The model narrates; it never decides

Verdict, score and contradiction detection are deterministic code comparing
structured fields. The model receives a finished outcome and writes it up. A
test with a mocked model proves the result does not depend on the model at all.

A side effect worth naming: because nothing in the deduction path parses prose,
the whole thing is language-independent for free
([ADR-0005](docs/adr/0005-locale-is-a-property-of-the-match.md)).

### Determinism is what makes evaluation possible

Same seed, same generator version, same case — down to the canary tokens. Every
eval runs five times because temperature > 0 makes a single run inconclusive; if
the mystery also varied per run, a prompt regression would be indistinguishable
from a harder case.

## Run it

Requirements: [uv](https://docs.astral.sh/uv/), `make`, and Docker if you want
the database.

```bash
make install # uv sync a partir do uv.lock
make api # uvicorn com reload
make install # sync the environment from uv.lock
make case SEED=42 REVEAL=1 # generate a case and see the solver's chain
make check # lint, typecheck, tests — what CI enforces

make dev # Postgres 16 + pgvector, Redis, the API
curl localhost:8000/health
```

## O que ler primeiro
`make` on its own lists every target.

| Documento | Para quê |
## Layout

| Path | What lives there |
|---|---|
| [`apps/api/src/mansao/domain/`](apps/api/src/mansao/domain/) | Entities. Structure only — no prose, no rendered sentence |
| [`apps/api/src/mansao/generation/`](apps/api/src/mansao/generation/) | Generator, solver, and the invariant checks |
| [`apps/api/src/mansao/i18n/`](apps/api/src/mansao/i18n/) | Message catalogs. Grammar lives here, not in the domain |
| [`docs/adr/`](docs/adr/) | Architecture decisions, with their downsides written down |
| [`infra/compose/`](infra/compose/) | Local Postgres with pgvector, Redis, API |

## Documentation

The design documents are in Brazilian Portuguese; the code and the ADRs are in
English ([ADR-0006](docs/adr/0006-english-in-code-portuguese-in-the-product.md)).
The glossary maps both vocabularies.

| Document | For |
|---|---|
| [`docs/00-plano-de-projeto.md`](docs/00-plano-de-projeto.md) | escopo, roadmap, método |
| [`docs/01-dominio.md`](docs/01-dominio.md) | glossário e modelo de domínio |
| [`docs/02-regras-de-negocio.md`](docs/02-regras-de-negocio.md) | RN-001 a RN-042 |
| [`docs/adr/`](docs/adr/) | as decisões e o porquê delas |
| [`CONTRIBUTING.md`](CONTRIBUTING.md) | fluxo de branch, commit e PR |
| [`docs/00-plano-de-projeto.md`](docs/00-plano-de-projeto.md) | Scope, roadmap, method |
| [`docs/01-dominio.md`](docs/01-dominio.md) | Glossary, domain model, state machines |
| [`docs/02-regras-de-negocio.md`](docs/02-regras-de-negocio.md) | RN-001 to RN-042, each with where it is enforced |
| [`docs/adr/`](docs/adr/) | Why things are the way they are |
| [`CONTRIBUTING.md`](CONTRIBUTING.md) | Branch, commit and PR flow |

Business rules are referenced by number from the code (`# RN-012`) and never
transcribed — duplicated text drifts.

## Roadmap

| Phase | Scope | Status |
|---|---|---|
| 0 | Foundation — repo, docs, local stack | done |
| 1 | Case generator and deducibility solver | in progress |
| 2 | A single NPC: isolated dossier, structured output, streaming | |
| 3 | Security: canary, input classifier, output filter, CI gates | |
| 4 | Full game: six NPCs, evidence, confrontation, verdict | |
| 5–7 | Front end, observability, production | |

Built with Python and FastAPI, on Postgres with pgvector for both game state
and NPC memory. LangGraph and Next.js arrive with the phases that need them —
the roadmap above is a record of what exists, not of what is planned to.

## Licença
## License

MIT — veja [`LICENSE`](LICENSE).
MIT — see [`LICENSE`](LICENSE).
Loading