Skip to content
Merged
Show file tree
Hide file tree
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
18 changes: 9 additions & 9 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
# Copie para .env. O .env não é commitado.

# Compose (Postgres local em localhost:5433)
POSTGRES_USER=mansao
POSTGRES_PASSWORD=mansao
POSTGRES_DB=mansao
POSTGRES_USER=firenze
POSTGRES_PASSWORD=firenze
POSTGRES_DB=firenze

# API — prefixo MANSAO_ (ver apps/api/src/mansao/config.py)
MANSAO_ENVIRONMENT=dev
MANSAO_DATABASE_URL=postgresql+psycopg://mansao:mansao@localhost:5433/mansao
MANSAO_REDIS_URL=redis://localhost:6379/0
# API — prefixo FIRENZE_ (ver apps/api/src/firenze/config.py)
FIRENZE_ENVIRONMENT=dev
FIRENZE_DATABASE_URL=postgresql+psycopg://firenze:firenze@localhost:5433/firenze
FIRENZE_REDIS_URL=redis://localhost:6379/0

# Provedor de LLM. Nunca commitar valor real.
# O SDK lê ANTHROPIC_API_KEY sozinho — sem prefixo MANSAO_.
# O SDK lê ANTHROPIC_API_KEY sozinho — sem prefixo FIRENZE_.
# ANTHROPIC_API_KEY=

# Qual modelo escreve o verniz. É o botão que move o custo por caso.
MANSAO_VENEER_MODEL=claude-haiku-4-5
FIRENZE_VENEER_MODEL=claude-haiku-4-5
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Mansão
# Firenze

Jogo de mistério investigativo com NPCs agênticos.
Next.js (front) + FastAPI/LangGraph (core de IA) + Postgres com pgvector.
Expand Down
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,16 @@ logs: ## segue os logs do compose
$(COMPOSE) logs -f

psql: ## abre psql no banco local
$(COMPOSE) exec db psql -U mansao -d mansao
$(COMPOSE) exec db psql -U firenze -d firenze

install: ## sincroniza o venv de apps/api a partir do uv.lock
cd $(API) && uv sync --extra dev

api: ## roda a API local com reload (sem container)
cd $(API) && uv run uvicorn mansao.main:app --reload --port 8000
cd $(API) && uv run uvicorn firenze.main:app --reload --port 8000

case: ## gera um caso (make case SEED=42 [LOCALE=en] [REVEAL=1] [VENEER=1])
cd $(API) && uv run mansao generate --seed $(or $(SEED),1) --locale $(or $(LOCALE),pt-BR) $(if $(REVEAL),--reveal,) $(if $(VENEER),--veneer,)
cd $(API) && uv run firenze generate --seed $(or $(SEED),1) --locale $(or $(LOCALE),pt-BR) $(if $(REVEAL),--reveal,) $(if $(VENEER),--veneer,)

openapi: ## regenera apps/api/openapi.json
cd $(API) && uv run python scripts/dump_openapi.py
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Mansão
# Firenze

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.
Expand Down Expand Up @@ -70,7 +70,7 @@ 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`
[`generation/solver.py`](apps/api/src/firenze/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
Expand All @@ -83,7 +83,7 @@ regenerated.
"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)).
([`domain/models.py`](apps/api/src/firenze/domain/models.py)).

```mermaid
flowchart LR
Expand Down Expand Up @@ -138,9 +138,9 @@ curl localhost:8000/health

| 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 |
| [`apps/api/src/firenze/domain/`](apps/api/src/firenze/domain/) | Entities. Structure only — no prose, no rendered sentence |
| [`apps/api/src/firenze/generation/`](apps/api/src/firenze/generation/) | Generator, solver, and the invariant checks |
| [`apps/api/src/firenze/i18n/`](apps/api/src/firenze/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 |

Expand Down
2 changes: 1 addition & 1 deletion apps/api/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"info": {
"description": "Owns the whole deterministic domain and every call to a model (ADR-0003). The front end never talks to the model provider or to the database.",
"summary": "AI core and domain of the mystery game.",
"title": "Mansão API",
"title": "Firenze API",
"version": "0.1.0"
},
"openapi": "3.1.0",
Expand Down
8 changes: 4 additions & 4 deletions apps/api/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[project]
name = "mansao-api"
name = "firenze-api"
version = "0.1.0"
description = "Core de IA e domínio do Mansão"
description = "Core de IA e domínio do Firenze"
requires-python = ">=3.13"
dependencies = [
"fastapi>=0.115",
Expand All @@ -11,7 +11,7 @@ dependencies = [
]

[project.scripts]
mansao = "mansao.cli:main"
firenze = "firenze.cli:main"

[project.optional-dependencies]
dev = [
Expand All @@ -26,7 +26,7 @@ requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/mansao"]
packages = ["src/firenze"]

[tool.ruff]
line-length = 100
Expand Down
2 changes: 1 addition & 1 deletion apps/api/scripts/dump_openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import json
from pathlib import Path

from mansao.main import app
from firenze.main import app

DESTINO = Path(__file__).resolve().parents[1] / "openapi.json"

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
from importlib.metadata import version

__version__ = version("mansao-api")
__version__ = version("firenze-api")
14 changes: 7 additions & 7 deletions apps/api/src/mansao/cli.py → apps/api/src/firenze/cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Case generator CLI.

`mansao generate --seed 42` prints the case as a player would receive it. The
`firenze generate --seed 42` prints the case as a player would receive it. The
solution only shows with `--reveal`, and that is not decoration: if printing the
culprit were the default, running the command while developing would spoil every
case you meant to play.
Expand All @@ -14,11 +14,11 @@
import sys
from collections.abc import Sequence

from mansao.config import settings
from mansao.domain import CaseWithSolution, FactKind, Role
from mansao.generation import UnsolvableCase, generate, solve
from mansao.i18n import DEFAULT_LOCALE, Catalog, UnknownLocale, available_locales, load
from mansao.veneer import CaseVeneer, VeneerRejected, VeneerUnavailable, write
from firenze.config import settings
from firenze.domain import CaseWithSolution, FactKind, Role
from firenze.generation import UnsolvableCase, generate, solve
from firenze.i18n import DEFAULT_LOCALE, Catalog, UnknownLocale, available_locales, load
from firenze.veneer import CaseVeneer, VeneerRejected, VeneerUnavailable, write


def _briefing(
Expand Down Expand Up @@ -80,7 +80,7 @@ def _briefing(


def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser(prog="mansao", description="Mansão case generator.")
parser = argparse.ArgumentParser(prog="firenze", description="Firenze case generator.")
sub = parser.add_subparsers(dest="command", required=True)

gen = sub.add_parser("generate", help="generate a case from a seed")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@


class Settings(BaseSettings):
"""Process configuration. MANSAO_ prefix so nothing collides."""
"""Process configuration. FIRENZE_ prefix so nothing collides."""

model_config = SettingsConfigDict(env_prefix="MANSAO_", env_file=".env", extra="ignore")
model_config = SettingsConfigDict(env_prefix="FIRENZE_", env_file=".env", extra="ignore")

environment: Environment = "dev"
database_url: str = "postgresql+psycopg://mansao:mansao@localhost:5433/mansao"
database_url: str = "postgresql+psycopg://firenze:firenze@localhost:5433/firenze"
redis_url: str = "redis://localhost:6379/0"
veneer_model: str = "claude-haiku-4-5"
"""Which model writes the veneer. Env-switchable because it is the one
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Domain model. Vocabulary in docs/01-dominio.md."""

from mansao.domain.models import (
from firenze.domain.models import (
Case,
CaseWithSolution,
Character,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""Case generation, validation and the deducibility gate."""

from mansao.generation.generator import GENERATOR_VERSION, UnsolvableCase, generate
from mansao.generation.solver import SolverResult, solve
from mansao.generation.validation import InvalidCase, validate
from firenze.generation.generator import GENERATOR_VERSION, UnsolvableCase, generate
from firenze.generation.solver import SolverResult, solve
from firenze.generation.validation import InvalidCase, validate

__all__ = [
"GENERATOR_VERSION",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
import random
from itertools import count

from mansao.domain import (
from firenze.domain import (
Case,
CaseWithSolution,
Character,
Expand All @@ -23,8 +23,8 @@
Scope,
Solution,
)
from mansao.generation.solver import solve
from mansao.generation.validation import validate
from firenze.generation.solver import solve
from firenze.generation.validation import validate

GENERATOR_VERSION = "2"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

from pydantic import BaseModel, ConfigDict

from mansao.domain import Case, Fact, FactKind
from firenze.domain import Case, Fact, FactKind


class SolverResult(BaseModel):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
something when the generator changes, or when a case arrives from elsewhere.
"""

from mansao.domain import CaseWithSolution, FactKind, Role
from firenze.domain import CaseWithSolution, FactKind, Role


class InvalidCase(ValueError):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Locale catalogs: structure in, prose out."""

from mansao.i18n.catalog import (
from firenze.i18n.catalog import (
DEFAULT_LOCALE,
Catalog,
MissingMessage,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
from pathlib import Path
from typing import Any

from mansao.domain import Case, Fact
from firenze.domain import Case, Fact

MESSAGES_DIR = Path(__file__).parent / "messages"
DEFAULT_LOCALE = "pt-BR"
Expand Down
6 changes: 3 additions & 3 deletions apps/api/src/mansao/main.py → apps/api/src/firenze/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from fastapi import FastAPI
from pydantic import BaseModel

from mansao import __version__
from mansao.config import Environment, settings
from firenze import __version__
from firenze.config import Environment, settings


class Health(BaseModel):
Expand All @@ -17,7 +17,7 @@ class Health(BaseModel):

def create_app() -> FastAPI:
app = FastAPI(
title="Mansão API",
title="Firenze API",
version=__version__,
summary="AI core and domain of the mystery game.",
description=(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""Prose over an approved structure. The model writes; it decides nothing."""

from mansao.veneer.models import CaseVeneer, CharacterVeneer, VeneerDraft
from mansao.veneer.validation import VeneerRejected, check
from mansao.veneer.writer import PROMPT_VERSION, VeneerUnavailable, load_prompt, write
from firenze.veneer.models import CaseVeneer, CharacterVeneer, VeneerDraft
from firenze.veneer.validation import VeneerRejected, check
from firenze.veneer.writer import PROMPT_VERSION, VeneerUnavailable, load_prompt, write

__all__ = [
"PROMPT_VERSION",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@

import re

from mansao.domain import Case
from mansao.veneer.models import VeneerDraft
from firenze.domain import Case
from firenze.veneer.models import VeneerDraft


class VeneerRejected(ValueError):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@
from pathlib import Path
from typing import Any, Protocol, cast

from mansao.domain import Case
from mansao.i18n import Catalog
from mansao.veneer.models import CaseVeneer, VeneerDraft
from mansao.veneer.validation import check
from firenze.domain import Case
from firenze.i18n import Catalog
from firenze.veneer.models import CaseVeneer, VeneerDraft
from firenze.veneer.validation import check

PROMPT_VERSION = "v1"
# Haiku: the veneer is short, structured, and its failure modes are caught by
Expand All @@ -49,8 +49,8 @@ def messages(self) -> _Parseable: ...


def prompts_dir() -> Path:
"""Repository `prompts/`, or wherever `MANSAO_PROMPTS_DIR` points."""
override = os.environ.get("MANSAO_PROMPTS_DIR")
"""Repository `prompts/`, or wherever `FIRENZE_PROMPTS_DIR` points."""
override = os.environ.get("FIRENZE_PROMPTS_DIR")
if override:
return Path(override)
return Path(__file__).resolve().parents[5] / "prompts"
Expand Down
2 changes: 1 addition & 1 deletion apps/api/tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import pytest

from mansao.cli import main
from firenze.cli import main

Capture = pytest.CaptureFixture[str]

Expand Down
8 changes: 4 additions & 4 deletions apps/api/tests/test_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@

import pytest

from mansao.domain import CaseWithSolution, FactKind, Role
from mansao.generation import InvalidCase, generate, solve, validate
from mansao.generation.solver import reachable_facts
from mansao.generation.validation import rn_004_no_overlap
from firenze.domain import CaseWithSolution, FactKind, Role
from firenze.generation import InvalidCase, generate, solve, validate
from firenze.generation.solver import reachable_facts
from firenze.generation.validation import rn_004_no_overlap

SEEDS = range(1, 41)

Expand Down
6 changes: 3 additions & 3 deletions apps/api/tests/test_health.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from fastapi.testclient import TestClient

from mansao import __version__
from mansao.main import app
from firenze import __version__
from firenze.main import app

cliente = TestClient(app)

Expand All @@ -16,5 +16,5 @@ def test_health_responde_ok() -> None:
def test_openapi_e_servido() -> None:
esquema = cliente.get("/openapi.json").json()

assert esquema["info"]["title"] == "Mansão API"
assert esquema["info"]["title"] == "Firenze API"
assert "/health" in esquema["paths"]
8 changes: 4 additions & 4 deletions apps/api/tests/test_i18n.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@

import pytest

from mansao.domain import FactKind
from mansao.generation import generate
from mansao.generation.generator import MEANS_KEYS, MOTIVE_KEYS, ROOMS, SECRET_KEYS
from mansao.i18n import UnknownLocale, available_locales, load
from firenze.domain import FactKind
from firenze.generation import generate
from firenze.generation.generator import MEANS_KEYS, MOTIVE_KEYS, ROOMS, SECRET_KEYS
from firenze.i18n import UnknownLocale, available_locales, load

LOCALES = available_locales()

Expand Down
Loading
Loading