Imported from azusachino/felicia (
AGENTS.md). Install upstream withnpx skills add azusachino/felicia. Copyright stays with the author.
AGENTS.md — felicia
Single source of truth for humans and agents working in this repo.
Project Overview
felicia is a map-based travel journal (modeled on liuaaron.com,
"Aaron's Waypoints"). Each journey is drawn on a dark world map as an orange route line;
along it sit mementos — the objects that anchor a memory (an admission ticket, but equally
a souvenir, a goods, a receipt, a stamp), each rendered as a collectible stub. Clicking a
memento animates it open into an essay and a photo gallery. The map is the index; the
mementos are the stories. (kind-tagged; physical tickets are dying, so stubs are rendered
from data — see docs/research/mementos-not-tickets.md.)
North star: docs/direction.md (direction: personal now,
product-ready). Earlier design/spec drafts are parked in docs/archive/.
Status: implementation stage (research trail continues), unhurried (~6-month horizon).
Delivery status lives in docs/roadmap.md; the selected end-to-end
journey and its per-stage status live in
docs/roadmap/user-journey.md.
Tech Stack & Architecture
- Backend: Go 1.27 — API, runtime, provider, and core modules in one
go.workworkspace. - DB: SQLite is the only v1 persistence contract (ADR-0032). The PostgreSQL/PostGIS provider stays in the tree as frozen, explicitly non-v1 work, deferred to v1.1/v1.2.
- Object storage: S3-compatible interface; R2 backend (MinIO/B2 swappable by config).
- Frontend: Vite + MapLibre GL SPAs — public site, private reader, and admin authoring app (bun workspace).
- Locales: static system UI catalogs support Japanese, English, and Chinese. Authored content has no translation sidecar and is rendered exactly as entered.
- Host: self-hosted container deployment; Cloudflare Tunnel is an optional ingress.
- Ingestion sources (self-hosted): Immich (photos/ticket stubs, via API) + Dawarich (passive iPhone GPS track, via API); joined on timestamp. Vision-LLM (Claude) pre-fills ticket metadata for confirmation.
Authoring model (A+E): an auto-ingest pipeline seeds ingested fields; an admin UI is where you author essays / photo curation / animation. The importer is field-scoped and never overwrites authored fields — re-import is always safe (see design §5).
Current layout
apps/{felicia-core,felicia-runtime,felicia-providers,felicia-publication,
felicia-server,felicia-cli,felicia-admin,felicia-web,felicia-public-site}/
packages/{felicia-model,felicia-runtime,felicia-components,felicia-renderers,
felicia-reader}/ contracts/ ops/ scripts/ docs/
The ownership map and dependency direction are defined in
docs/development/layout.md and
ADR-0034.
felicia-core is the pure domain and port layer (no I/O). felicia-runtime
owns use cases, felicia-providers owns persistence implementations,
felicia-publication owns the public contract, and apps/felicia-server/CLI adapters compose
runtime and publication ports. felicia-reader owns the public reader facade,
named design registry, and concrete compositions. felicia-model owns reader
data/public contracts; felicia-runtime, felicia-components, and
felicia-renderers are reusable package boundaries. The admin, private reader,
and public site remain separate hosts.
The root Go module has been retired; all Go code is built through go.work.
Build, Run & Test
All daily operations go through make <target>. Tools: Go, Bun, uv, Prettier,
golangci-lint, goose, and sqlc come from the checked-in mise configuration. PostgreSQL
18 + PostGIS remain disposable container infrastructure.
| Target | Does |
|---|---|
make fmt |
format Go |
make vet |
go vet ./... |
make lint |
golangci-lint run (mise) |
make test |
go test -race -cover ./... |
make check |
fmt + vet + lint + test + feature contracts — before commit |
make build |
build all binaries |
make validate |
check + build + public/admin/private frontend checks — before PR |
make migrate |
goose up (needs DATABASE_DSN) |
make admin |
local admin GUI: authoring API + felicia-admin on 0.0.0.0 for Tailscale access |
Coding Conventions
- Conventional commits (
feat:,fix:,chore:,deploy:) — no emojis. - Go: standard
gofmt/goimports; errors wrapped with context; small interfaces at seams. - 2-space indent for config files (YAML/TOML/JSON).
- Test-first for the importer core; pure functions + fixtures, no network in unit tests.
- Keep it simple — avoid speculative abstraction.
Key Files & Entry Points
docs/direction.md— research-stage north star: the idea + personal-now / product-ready direction.docs/research/— exploration trail (workflows, liuaaron teardown, product-vs-personal, mementos-not-tickets, notion-prototype, notion-to-stack, source-connectors, transit-tickets, authoring-publish-flow, ux-restyle, memento-arrangement, reader-admin-surfaces, adventurelog teardown). Backend core:backend-stack.md(stack + decisions D1–D9),data-model.md(stable schema),memento-templates.md(declarative kind-template registry).docs/archive/— parked design/spec/plan drafts (premature lock-in); detail, not binding.- asobi graph
felicia:*— decisions (ADRs), session state. Runasobicommands.
Quality Standards
make check must pass before every commit; make validate before every PR (both
hook-enforced). No --no-verify. Don't commit or push without explicit confirmation.
Development-Flow Constraints
These are cheap rules that would have caught defects this repo actually shipped. Each one names the failure it prevents, so it can be retired if the failure stops being possible.
-
Provider intent is explicit, and mis-selection fails loudly. ADR-0021 already forbids implicit provider changes, but the config contract has a hole: a PostgreSQL DSN with no
DATABASE_DRIVERsilently starts SQLite.ops/compose.yamldoes exactly this, so its API ran on a throwaway in-container SQLite file while Postgres, PostGIS, and every migration sat unused — with nothing in the logs to say so. Configuring a DSN for a provider you did not select must be a startup error, never a silent default. -
A v1 schema change lands in SQLite only; touching both providers ships a parity check. ADR-0017 required conformance tests "to prevent SQLite and PostgreSQL behavior from drifting", and
apps/felicia-providers/contractdelivers that — for behavior. Schema shape is unguarded, and the two DDLs have already diverged (tb_journalinapps/felicia-server/migrations/,tb_journalsinapps/felicia-providers/sqlite/schema.sql). Under ADR-0032 that drift is now a frozen deferred-provider snapshot, not an active obligation: v1 schema changes are not duplicated into PostgreSQL, and doing so by reflex re-opens the parity surface the deferral closed. A change that does touch both — v1.1 re-entry work — asserts shape parity in a test, not in review. -
Every user-facing surface has exactly one documented
maketarget. The admin GUI — the primary authoring surface — had no launcher, so the documented flow could not be started from the documentation. If a person is meant to open it,make helpnames it. -
The local authoring stack may bind the tailnet, and packaging must not publish it.
make adminbinds the API, site preview, and admin GUI to0.0.0.0so the author can use them from a Tailscale client; useFELICIA_HOST=127.0.0.1for a host-only session. The admin API remains unauthenticated by design, so the host firewall/Tailscale policy must be the access boundary. Deployment packaging never publishes the admin port. -
Superseding an ADR answers the costs the superseded one enumerated. ADR-0008 rejected a second database engine and named the three costs it was avoiding: duplicate DDL migrations, repository translation layers, and custom Go-memory spatial logic. ADR-0017 reversed it five days later on local-setup ergonomics without disputing those costs — and all three arrived (a second DDL that has drifted, ~1.3k lines of second provider,
SnapToRoute/GetDisplayRoutereimplemented in Go). A superseding ADR states how each named cost will be contained, or records that it is accepted. -
Private authoring data never sits on a committable path. The original journal is the artifact ADR-0025 says must not leave the machine. Private workspaces, databases, and originals live under
.felicia/. Sanitized, explicitly published journey inputs may be committed only underpublication/journeys/;.gitignorecovers every SQLite spelling the tooling can emit.
Docs-Sync Discipline (per PR)
Every PR that changes system behavior, capability, or delivery progress must update the matching status docs in English, in the same PR, before merge:
- the README "Status & roadmap" summary, if the overall picture changed;
docs/roadmap.md/ the active epic doc, if milestone or epic progress changed;docs/roadmap/user-journey.md— the per-stage status of the selected end-to-end journey (collection → intake → authoring → publish → deploy).
This is checked at the PR gate alongside make validate; individual commits are not
required to carry doc updates. GitHub is the single ledger for issue state — do not
recreate local issue mirrors (the old drafts are archived under
docs/archive/github-issues/); scripted issue lookups use a GITHUB_TOKEN
environment variable, not interactive gh auth login.
Issue Convention
One classification standard for the open ledger. Three competing title schemes
(M0 —, [FELICIA-PAGES-01.x], P0:) and an unlabelled backlog previously made
"what is next" unanswerable without reading every issue.
-
Title:
R<n>: <lowercase summary>, whereR0–R5is the roadmap milestone fromdocs/roadmap.md. No other prefix. Priority and type live in labels, never in the title. -
Milestone: every issue carries its
R<n>milestone — this is the coarse ordering, and it is what makes milestone completion percentages true. -
Type label (exactly one):
type:epic(milestone umbrella),type:feature,type:defect(shipped behavior contradicts a binding contract),type:decision(needs an ADR or a joint call first). -
Priority label (exactly one):
prio:P0breaks a stated invariant or destroys data ·prio:P1blocks the documented end-to-end journey ·prio:P2real gap with a workaround ·prio:P3correctness/polish, nobody blocked. -
Work order: milestone ascending, then priority ascending. Query it, don't guess:
gh issue list --state open --milestone "R4 — Ingestion and route enrichment" --label prio:P0 -
Closed issues keep their historical titles:
[FELICIA-PAGES-01.x]is the doc↔issue trace inpages-v1-epic.md. They carry milestones but are not retitled. Epic-localM1–M4numbering stays scoped to its epic doc and never appears in an issue title. -
A
type:defectbody cites the contract it violates (ADR,docs/contracts/*, or AGENTS.md) plusfile:lineevidence. Without that citation it is atype:feature.