Files
FabledGrimoire/docs/development.md
T
bvandeusenandClaude Opus 5.5 0f5ef0be63
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 12s
CI & Build / TypeScript typecheck (push) Successful in 56s
CI & Build / integration (push) Successful in 1m4s
CI & Build / Python tests (push) Successful in 1m55s
CI & Build / Build & push image (push) Successful in 44s
rename(547): the server is grimoire — package, image, DB defaults, backups
Step 2 of the Scribe -> Grimoire rename.

- src/scribe -> src/grimoire, and every import, patch target, module path,
  hypercorn entry point and `python -m grimoire.restore` with it; pyproject,
  uv.lock and the frontend package are named grimoire.
- Image git.fabledsword.com/bvandeusen/fabledgrimoire; CI's Postgres service
  runs as grimoire / grimoire_test.
- Compose files default the DB user, database and (prod) volume to grimoire,
  each overridable from .env, so an install created as Scribe keeps its data
  by setting POSTGRES_USER, POSTGRES_DB and PGDATA_VOLUME.
- Backups are written as grimoire-<ts>.tar with format grimoire-backup-tar.
  Archives named scribe-* with the scribe-backup-tar manifest still restore
  and are still counted at the destination (folder and S3), with tests.
- Exports, the MCP auth realm, the ASGI scope key and the persistent logger
  carry the new name.

Kept on purpose: the secret-sealing key label stays "scribe.secrets.v1",
because every stored secret was derived under it.

Plugin identity (plugin name, hooks, env vars, .scribe marker, skills) is
step 3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 10:50:45 -04:00

4.2 KiB

Development

Running it

docker compose up --build          # app + PostgreSQL at http://localhost:5000
docker compose up --build app      # rebuild after a change
docker compose down -v             # stop and wipe the database

The image builds the frontend, so a frontend change needs a rebuild too. For quicker frontend work, run Vite against the running app; it proxies /api to port 5000:

cd frontend && npm ci && npm run dev   # http://localhost:5173

Checks

CI runs these on every push; locally they need uv sync --extra dev and cd frontend && npm ci.

make lint        # ruff check src/
make typecheck   # vue-tsc --noEmit
make test        # pytest tests/
make check       # all three

Unit tests run without a database. Tests marked integration need a real PostgreSQL and run in CI's integration job, against a schema built by the real migrations.

Database migrations

The container runs alembic upgrade head on every start, after waiting for the database to accept connections.

docker compose exec app alembic revision -m "short_description"
  • Number revisions sequentially (0123_...). Revision ids must fit in 32 characters, the width of alembic_version.version_num.
  • Write both upgrade() and downgrade().
  • Adding a value to a CHECK-constrained column means dropping and re-adding the constraint in the same migration.
  • Keep migrations to schema changes. Filling a derived table from a large one belongs in a background backfill after startup, where its cost doesn't hold up the boot.
  • A migration that depends on application code (a key derivation, a serialiser) copies what it needs rather than importing it, so a later edit can't change what an old migration does.

The plugin

plugin/ ships straight from git: installs fetch it through .claude-plugin/marketplace.json, and there is no build step. After changing anything under plugin/ or .claude-plugin/, run make mint-plugin before committing. Claude Code only refreshes an installed plugin when its version changes, and CI fails a plugin change that didn't bump it.

The hooks must run on a bare machine: POSIX shell, awk, sed and the usual coreutils, plus git and curl. They fail open, so a slow or unreachable Grimoire never blocks Claude Code.

CI and releases

CI runs on Gitea Actions (workflow in .forgejo/workflows/ci.yml) in the shared ci-python:3.14 image (ci-requirements.md lists what it relies on). Six jobs: TypeScript typecheck, plugin hooks, Python lint (plus design-token and dangling-style checks), unit tests, integration tests, and the image build, which waits on all of them except the plugin job.

Push Image tags
dev :dev, :<sha>
main :latest, :<sha>
a v* tag :latest, :<version>, :<sha>

:<sha> never moves, so it is the rollback point. Images publish to git.fabledsword.com/bvandeusen/fabledscribe, with a registry layer cache at :cache. The build needs the REGISTRY_USER and REGISTRY_TOKEN (a token with write:packages) repository secrets.

Work happens on dev. A release merges dev into main with a plain merge commit; a dated vYYYY.MM.DD.HHMM tag, matching the version the image reports, can mark it.

Conventions

Backend

  • Services open their own session: async with async_session() as session: (from grimoire.models).
  • Routes and MCP tools stay thin; the logic lives in services/, shared by both.
  • Access checks go through services/access.py, never inline ownership checks.
  • Every wait on something outside the process has a deadline.

Frontend

  • API calls go through the typed helpers in frontend/src/api/client.ts.
  • Pinia stores for shared state, ref() for state local to a component; composables for reusable behaviour.
  • Colours, spacing and type come from the design system's tokens in theme.css, which is generated, so CI rejects an unknown var().

Commits

type(scope): what the change does

Why, and anything a reader tracing this change later needs.

Types: feat, fix, refactor, docs, chore, test. Comments in the code describe how it works today; the history of a change belongs in its commit message.