Todo's packages are licensed as Learn's (decided 2026-10-19): the server under the SSPL, the client and the app under the AGPL. Its AGENTS.md and READMEs describe the shell it is now; the root's README, lint note and licence table follow. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
87 lines
18 KiB
Markdown
87 lines
18 KiB
Markdown
# The Halkyon project
|
|
|
|
Halkyon's apps and the packages they share, in one repository and one pub workspace (ADR 0015, decided 2026-10-19). This file holds what applies to every app and package; each app's own `AGENTS.md` holds what is its alone, and is read before working in it: [Halkyon Learn](apps/learn/AGENTS.md), [Halkyon Todo](apps/todo/AGENTS.md).
|
|
|
|
- **Working notes**: if `NEXT.md` exists at the repository's root, read it first. It holds the current step, the plan and the reasoning behind each decision. It is local and never committed.
|
|
|
|
## Layout
|
|
|
|
- **`apps/<app>/`**: each app's packages, as `serverpod create` lays them out (`halkyon_<app>_server`, `_client`, `_flutter`), plus what is the app's alone (Learn's curator, the scripts writing Learn's assets in `apps/learn/tool/`, run from `apps/learn`). Learn is the app that runs; Todo is still Serverpod's scaffold; Train and Meditate are to come.
|
|
- **`packages/`**: what the apps share, each with its README and licence, which the apps take by path (`path: ../../../packages/<package>`), so a change to a shared package and to the apps using it is one commit:
|
|
- `halkyon_ui`: Halkyon's look, the theme around cobalt and copper, Noto Sans bundled, the window-size classes and the widgets every screen is built of, with texts of its own (English, German).
|
|
- `halkyon_base_server`, `halkyon_base_client`: the Serverpod module every Halkyon server includes (ADR 0014): signing in (anonymous, GitHub, email codes), an account's ways in and its deletion with a hook for each app's own data, the pruning, the instance's basics.
|
|
- `halkyon_app`: every app's shell on the module's client (ADR 0014): the instances a device keeps accounts on and the first start, signing in and switching person, the gates building an app's dependencies per instance and per person, the sync service and where it stands, with texts that name the app.
|
|
- `halkyon_core` (low-level primitives in pure Dart: ids, a logger, `SerialQueue`, `CoalescingRunner`) and `halkyon_interop` (contracts between apps, pure Dart).
|
|
- **`deploy/`**: the instance deploy at the root, learn.halkyon.eu's today (Learn's `AGENTS.md`, *CI and deploys*), where other services' compose files are to join Learn's. **`snap/`**: Learn's snap, built from the root, which its build needs whole. **`docs/adr/`**: the decision records of every app and package, one numbering. **`tool/`**: the repository's tools (below).
|
|
- **One workspace, one lock**: the root `pubspec.yaml` lists every package, `pubspec.lock` holds one version of each dependency for all of them, and its `dependency_overrides` hold for every app (the offline sync fork, Learn's `AGENTS.md`; `cli_util`, below). **Serverpod's version moves as one**: every package pins Serverpod's packages to the same version (4.0.4), the module's and Todo's too, together with the CLI version CI generates code with (`SERVERPOD_VERSION` in `.forgejo/workflows/ci.yaml`) and the sync packages Learn's server depends on.
|
|
|
|
## How work is done here
|
|
|
|
- **The agent implements; the developer decides.** Put every decision to the developer before building (design choices, alternatives, framework defaults kept or changed). The first time a new pattern appears (e.g. the first repository/gateway pair, the first synced model, the first endpoint with its denial tests, the first Serverpod module), walk through it in detail with the developer; afterwards, implement exceedingly similar cases directly and point out only deviations.
|
|
- **Tests come first** and state the specification. Every access rule gets a test that asserts **denial**, not only permission.
|
|
- **Explain every migration's SQL.**
|
|
- **Before keeping a framework default, name the alternatives** and recommend one, especially where a choice becomes expensive once users or data exist.
|
|
- Every decision carries its reason (in `NEXT.md` or an ADR under `docs/adr/`).
|
|
- **Commits** are GPG-signed and carry a DCO `Signed-off-by` trailer (`git commit -s`; a local `prepare-commit-msg` hook adds it automatically), and **keep their type prefix** (`feat`, `fix`, `perf`, `refactor`, `docs`, `build`, `chore`, a `!` or a `BREAKING CHANGE:` line for what breaks): releases work out their versions from them (Learn's `AGENTS.md`, *Releases*).
|
|
- **Servers are named in prose by the domain they serve** (learn.halkyon.eu's server, the halkyon.app server), which says what a reader needs; the SSH aliases and the CI runner's label keep the servers' own names (`sahaquiel-deploy`, `ramiel-fdroid`, `sahaquiel:host`).
|
|
- **Pushes** pass the checks first, through a local `pre-push` hook (in a fresh clone: `ln -s ../../tool/pre-push .git/hooks/pre-push`): `tool/pre-push` first refuses a push to `main` that brings a commit not signed by a key in `deploy/allowed-signers` (`tool/check_signatures.sh`, the deploy's own rule), so it fails on the laptop rather than after CI, then runs `tool/check.dart`. CI runs the checks again, and a green `main` deploys learn.halkyon.eu (see *CI and deploys*), so an unsigned commit on `main` blocks every deploy after it.
|
|
- **Lints**: LeanCode's strict set (`leancode_lint`), configured once in the root `analysis_options.yaml`, which also enables its analyzer plugin. Plugins can only be enabled at the root of a pub workspace and only reach packages whose options include that file: Learn's and Todo's servers and apps, Learn's curator and every shared package include it; the generated client packages keep their own. Every `// ignore:` gets a comment saying why (`document_ignores`). Each server package switches off the four rules for Dart v3.13's constructor syntax (`use_primary_constructors`, `use_declaring_parameters`, `empty_container_bodies`, `unnecessary_type_name_in_constructor`): Serverpod's code generator (checked with v4.0.3) can't parse that syntax in endpoint files yet.
|
|
|
|
## Flutter and Serverpod
|
|
|
|
Each app is a Flutter app (frontend) backed by a Serverpod server (backend). Always build an app's backend with Serverpod. Build for multiple users, with Serverpod's built-in authentication, which a server gets from `halkyon_base` (Learn's `server.dart` configures the module; Todo's scaffold still has Serverpod's own email sign-in).
|
|
|
|
The user starts an app's server and Flutter app with `serverpod start` in that app's server package (Learn's: `apps/learn/halkyon_learn_server`). There is no need to check if the server is running: make the changes and call the `serverpod` MCP tools as needed. If the server is not running, an informative error message will be received from the MCP server. Then STOP and ask the user to start it. NEVER start the server yourself. The Flutter app is started along with it, or can be launched from the MCP tool `spawn_flutter_app`.
|
|
|
|
While running, `serverpod start` watches for file changes to run incremental code generation and hot reload both the server and the Flutter app. Calling `serverpod generate` directly is not needed, but might be useful to troubleshoot when an incremental generation fails.
|
|
|
|
ALWAYS use the MCP server instead of the command line. Use the MCP server to:
|
|
|
|
- `create_migration` and `apply_migrations` for database (after you change data models).
|
|
- `create_repair_migration` if the database has drifted out of sync with the migrations.
|
|
- `tail_server_logs` to read logs from the server.
|
|
- `tail_flutter_logs` to read the raw stdout/stderr of the Flutter app.
|
|
- `hot_reload` / `hot_restart` to reload or restart the server and the Flutter app. ALWAYS call `hot_restart` after doing changes in the Flutter app that may not work with normal hot reload (which is automatically applied).
|
|
- `spawn_flutter_app` to start a Flutter app declared under `serverpod: flutter_apps:` in the server `pubspec.yaml`.
|
|
- `get_flutter_app_dtd` (Dart tooling daemon) for connecting to the app through the `dart` MCP.
|
|
|
|
The `serverpod` MCP tools come from `.mcp.json`: `serverpod mcp-server`, a bridge to the running `serverpod start` of one app's server, the one its `--server-dir` names (each app's `AGENTS.md` gives its own). They load when a session starts; if they are missing, the server wasn't running or the session predates `.mcp.json`. Like all agent configuration, `.mcp.json` is git-ignored. A session works with one app's server at a time; work on another app's server package with the CLI below, in a worktree if that app's `serverpod start` runs.
|
|
|
|
NEVER edit generated code. Each server's `lib/src/generated/` directory and its whole client package (`apps/<app>/halkyon_<app>_client`, `packages/halkyon_base_client`) are rewritten by the code generator. Change the `.spy.yaml` models, the endpoints, or the server's `lib/server.dart` instead.
|
|
|
|
Migrations are a narrow exception: the `migration.sql` of a generated migration MAY be edited by hand when the generated SQL would lose data — to add a data transformation, or to reach a destructive change through non-destructive steps. Never touch the other files in the migration directory, and keep the schema the SQL ends up with identical to `definition.sql` — new databases are created from that file and never run `migration.sql`. A module's tables are part of each server's migrations that includes it; moving a table into a module unchanged needs no SQL but the version rows (`create-migration --empty`, ADR 0014).
|
|
|
|
**Pass `--no-analytics` to every `serverpod` call** (before the command: `serverpod --no-analytics generate`). Otherwise the CLI reports the commands run and the Serverpod features the project uses to PostHog; there is no setting or environment variable to switch that off. The VS Code task, `.mcp.json` and Melos' `generate` script pass it already.
|
|
|
|
Only when the server cannot be started at all, or the MCP tools serve another app's server or checkout, fall back to the CLI in the server package:
|
|
|
|
- `serverpod generate` to regenerate the client and the generated server code (`dart run melos run generate` for every server package at once).
|
|
- `serverpod create-migration` after changing a model with a `table` (add `--force` for destructive changes). It only writes the migration; `serverpod start` applies pending migrations when it boots the server.
|
|
|
|
Tests need no Docker. Each server's `config/test.yaml` sets `database.dataPath`, so Serverpod starts and manages the test database (an embedded PostgreSQL) itself. Just run `dart test` in the server package, which needs the test passwords in its git-ignored `config/passwords.yaml` (throwaway values for the module's and Todo's tests; Learn's real file for Learn's) or from the environment as CI gives them.
|
|
|
|
Tooling traps: rules of `leancode_lint`'s analyzer plugin are ignored with its prefix (`// ignore: leancode_lint/missing_equatable_props`); `dart fix --apply` moves `flutter_driver` into an app's `dependencies` (as `any`) on every run, so restore it as a dev dependency (`sdk: flutter`) afterwards; never `dart format` generated code, only the hand-written paths, as `tool/check.dart` does (a server's `test/integration/test_tools/` is generated too, so format test files one by one, never the whole `test/` folder); a UUID table's `id` column takes only a set typed `Set<UuidValue>` in `inSet` and `notInSet` (`_uuids` in Learn's `sync_tag_gateway.dart`): it is nullable as declared, so a set literal in its place becomes a set of nullable ids, which Serverpod (v4.0.4) refuses at run time with a type error fake gateways never meet (found 2026-10-17: taking a tag off anything had failed on devices since v0.1.0); and never rebase or check out other commits while `serverpod start` runs: its watcher regenerates code for each intermediate state, leaves stale generated files behind and hot-reloads them. Stop it first, or work in a separate worktree.
|
|
|
|
If the user asks you to test an app:
|
|
|
|
1. Use `get_flutter_app_dtd` (`serverpod` MCP) to get the Flutter app's DTD
|
|
2. Pass the DTD to `connect_dart_tooling_daemon` (`dart` MCP) to connect to the app
|
|
3. Use `flutter_driver` (`dart` MCP) to navigate through the app (each app's `AGENTS.md` says where it starts the driver extension)
|
|
|
|
## Checks, Melos and tools
|
|
|
|
Checklist after doing changes, in this order:
|
|
|
|
- `dart format` (CLI) on the files you changed
|
|
- `create_migration` and `apply_migrations` (MCP - only if necessary)
|
|
- `dart run tool/check.dart` (CLI): the packages the lock names (`flutter pub get --enforce-lockfile`, so a merge never leaves the resolution behind), format check of our own Dart files, `dart analyze --fatal-infos` over the whole workspace, the tools' own tests, and every package's tests through Melos. With `--changed-since=origin/main` it tests only the packages changed since then and those depending on them. The pre-push hook runs it in full, so a push only goes through when it passes.
|
|
- Do `serverpod` MCP `hot_restart` if required (hot reload is done automatically). Will also hot restart Flutter app
|
|
- Check `serverpod` MCP `tail_server_logs` and `tail_flutter_logs` for any issues.
|
|
|
|
- **Melos** (decided 2026-10-19, ADR 0015) runs scripts across the workspace's packages: a dev dependency of the root (`dart run melos …`, its version in the lock), its scripts under `melos:` in the root `pubspec.yaml`: `analyze`; `test:dart` (the packages without Flutter, one at a time, as each server's tests start a PostgreSQL of their own on the same port); `test:flutter` (`flutter test -x requires-server`, one package at a time); `generate` (Serverpod's code generation in every package with a `config/generator.yaml`, the CLI on the PATH); `check` (`tool/check.dart`). Every script takes Melos' filters: `dart run melos run test:flutter --scope=halkyon_app`, or `--diff=origin/main --include-dependents` for what changed and what depends on it; `dart run melos list` lists the packages. The root overrides `cli_util` to 0.5.2 for it: Learn's `flutter_launcher_icons` (0.14.4, its latest) asks for ^0.4.1 and uses only the logger, which 0.5 keeps; drop the override once that package allows 0.5.
|
|
- **Tools are Dart where they hold logic** (decided 2026-10-19): conditions, parsing, parallel work, anything worth a test, as `tool/check.dart` (its file selection in `tool/src/check_files.dart`, tested in the root's `test/`). **Bash for glue**, short and linear (`tool/pre-push`, `tool/check_signatures.sh`, `tool/workspace_only.sh`, which narrows the root pubspec's workspace to the packages an image builds), **and for everything run on the production host**, which has no Dart SDK and gets none (`deploy/redeploy.sh`, `deploy/backup.sh`). **Python only where a library exists only there** (Learn's icons with Pillow, its Anki fixtures with Anki itself); Learn's data scripts and `packages/halkyon_app/tool/subdivision_names.py` need nothing but Python's standard library and may move to Dart when they next change. **Releases come from `tool/release.dart`** (decided 2026-10-19, ADR 0015), shared by the apps, each configured in Dart beside it (`apps/<id>/release.dart`, typed and commented, registered in `tool/src/release/apps.dart`): `dart run tool/release.dart learn [major | minor | patch] [--dry-run]`, on a developer's laptop, never in CI. Tags are per app (`learn-v0.19.0`; an app's earlier tags, Learn's `v…`, count as its history), and only commits touching the app decide its version: its folder, the shared packages its packages use by path (transitively), the root pubspec and lock, its release files (key pins, F-Droid listing, snap recipe) and where its packages were before a move. `--dry-run` reads everything a release would (the version, the commits that count and what each touches, the steps, the notes it would publish), changes nothing, asks for no secret, and lists what a release would refuse. Its pure logic is in `tool/src/release/`, tested in the root's `test/release/`. `tool/fdroid_repo.sh` stays Bash (glue for fdroidserver). `tool/release.sh` releases build 21 alone, the release going with ADR 0014's breaking deploy, by the script it was proven with; then it goes.
|
|
|
|
## CI and deploys
|
|
|
|
- **CI** (`.forgejo/workflows/ci.yaml`) runs `dart run tool/check.dart` on every push of a branch (not of a tag: a release pushes its tag right after `main`, on the same commit), on the Forgejo runner on learn.halkyon.eu's server (`deploy/runner/`), except for a commit that passed before: the job marks passed commits in the runner's home (`checked/`, kept 90 days), so a branch's commit fast-forwarded into `main` goes straight to its deploy. It checks every package, whichever a push changed. Jobs run directly on the host as the unprivileged user `runner`, never in containers: Docker access would be root on the production server. The runner's systemd unit keeps jobs away from production (no `/home`, no Docker socket, Docker's container networks unreachable, memory and CPU capped); keep it that way when changing it. `runner` has user ID 61000 and must never share one with a container's processes (PostgreSQL and Learn's server run as 999, not remapped): the same user ID can read their environment and files. The job's first step (*Sandbox*) fails if any of this leaks. Flutter is pinned in the workflow (`FLUTTER_VERSION`): move it together with the local one. Every server package's tests get throwaway passwords from the environment (`SERVERPOD_…`, new for every job), since no `passwords.yaml` is ever committed; without them they exit with code 1 and no message (Serverpod's own template does the same). CI also runs `dart run melos run generate` with the Serverpod CLI at `SERVERPOD_VERSION` and fails if the committed generated code of any server differs from its output. So never run `dart format` over a server's `lib/` or `test/` as a whole: it reformats the generated code (`lib/src/generated/`, `serverpod_test_tools.dart`); format the files you wrote. CI runs on pushes only: a pull request from a fork would run its code on the production server.
|
|
- **A green `main` deploys learn.halkyon.eu**: this repository's `main`, whichever app or package a push changed, through `deploy/redeploy.sh` (Learn's `AGENTS.md`, *CI and deploys*, with the rules a deploy checks, the sidecars, the backups and the releases).
|