1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
|
# AGENTS.md — working on bokf
`bokf` is a self-hosted Swedish bookkeeping system in C11: one daemon
(`bokfd`) owning a SQLite database, one JSON protocol, three clients
(`bokfctl`, `bokftui`, agents). GPL-3.0-or-later.
## Build and test
```sh
make -j$(nproc) # bokfd, bokfctl, bokftui
make test # server/protocol test suite — must be green before you stop
```
No new dependencies without asking. SQLite, yyjson, SHA-256 and Argon2 are
vendored in `vendor/` and pinned; the only system library is libncursesw.
## Layout
| Path | What |
|---|---|
| `src/` | daemon, protocol, ledger, reports, SIE, seed, formula |
| `clients/` | `bokfctl.c`, `bokftui.c`, shared `client.c` |
| `docs/` | PROTOCOL.md, SCHEMA.md, COMPLIANCE.md, TUI-GUIDELINES.md |
| `tests/` | in-process protocol/ledger tests (`test_core.c`) |
| `data/` | BAS charts (third-party, see THIRD_PARTY_NOTICES.md) |
## Invariants — do not break these
- **Protocol first.** All state changes go through commands in `PROTOCOL.md`;
clients never touch the database or bypass the daemon.
- **Amounts are integer öre**, dates `YYYY-MM-DD`, account numbers strings.
- **Append-only ledger.** Vouchers/rows/audit are immutable; corrections are
new vouchers. Never add an UPDATE/DELETE path for them.
- **Everything is audited** via `audit_append`; secrets never reach the log.
- **Tenant isolation**: every tenant table carries `org_id`, references are
composite FKs. New tables follow the same pattern.
- **Hash chains**: if you change what a voucher stores, check the canonical
encodings in `SCHEMA.md` §7.1/§9.1 and `ledger.c`.
## Changing the schema
Bump `BOKF_SCHEMA_VERSION` in `db.h`, add a forward migration in `db.c`, update
`SCHEMA.md`, and make sure an existing test database upgrades (the test suite
opens a fresh DB; migration is exercised by running the daemon on the demo DB).
Never rewrite ledger rows in a migration.
## Adding a command
1. Handler in the matching domain file `src/cmd_<domain>.c` (shared helpers
live in `src/cmd_util.[ch]`), using the `fail()/failf()` helpers and stable
error codes from `PROTOCOL.md` §5.3.
2. Entry in that file's `g_cmd_<domain>[]` table with permission, `need_org`,
`mutating`, `dry_run` and a `CMD_ARGS(args_x)` name/type/required/default
schema; the dispatcher validates present arguments before the handler runs
and `describe` emits the schema. A new file's table is registered in
`g_command_tables[]` in `src/commands.c`; `make check` scans all of them.
3. Audit (`audit_append`) for mutations, after success.
4. Update `docs/PROTOCOL.md`; add tests in `tests/test_core.c`. `make check`
(part of `make test`) fails when a command or error code is missing from
PROTOCOL.md.
5. `agent.instructions` in `commands.c` if agents need to know about it.
## Server code style
C11, no comments unless the reason is non-obvious, static helpers, no VLAs.
Bounded buffers with `snprintf`, check every `sqlite3_step`, free what you
allocate with a clear owner. Warnings must be zero (`-Wall -Wextra …`).
User-visible server messages are English; the TUI is Swedish.
## TUI work
Read `docs/STATE.md` first for status, open decisions and the prioritized
backlog (settled decisions are archived in `docs/DECISIONS.md`; consult it
only when you need the history behind a rule). Then read `docs/TUI-GUIDELINES.md` and follow it. Summary: one shared line editor,
universal keys (Ctrl+N new, F5 refresh, Esc/q back, digits jump, `g` goto),
forms behave the same everywhere, hints always visible, errors shown as
`CODE: message`. Screens call only the widget layer (`clients/tui.[ch]`:
`tui_menu`, `tui_select_list`, `tui_prompt_into`, `tui_edit_field`,
`tui_pad_field`, `tui_message`, `show_error`) — never ncurses directly and
never a local one-off loop. If no widget fits, extend the widget layer
first (spec in `TUI-GUIDELINES.md` + tests in `tests/test_tui.c`).
Views live one file per group in `clients/screens_<group>.c`; shared UI
helpers and `struct app` are in `clients/ui.[ch]`; the scene registry
(`struct scene SCENES[]` and `open_scene`) is in `clients/bokftui.c` with
`main`, login and the Ctrl+R reload.
## Verifying
- Server changes: `make test` (runs `make check`, the server suite and the TUI
unit tests). UI changes: also run `make test-pty`, which drives the real TUI
over a pty against a throwaway `/tmp` daemon and asserts on screen text.
- Iterate on one `test_core` section with `./build/test_core --only <name>`
(runs that test's dependencies first); `./build/test_core --list` prints the
names and an unknown name exits 2.
- Sanitizers: `make test-asan` (ASan) and `make test-ubsan` (UBSan) build
`test_core` in `build-asan`/`build-ubsan`. Run them after touching
allocation or arithmetic paths.
- Before you stop: `make gate` — a clean rebuild with `WERROR=1`, the full
suite and `test-asan`, in its own `build-gate/` (it never touches
`build/`). Run `sh scripts/install-hooks.sh` once per checkout: it points
`core.hooksPath` at `.githooks/` so the pre-push hook runs `make gate`
automatically.
- Never run `bokftui` for tests without `scripts/tui-sandbox.sh`: it
isolates `XDG_CONFIG_HOME`/`XDG_CACHE_HOME` so a run cannot overwrite the
human's `~/.config/bokf/tui.conf`, `~/.cache/bokf/tui.log` or bw session.
The daemon/test database must likewise live in `/tmp`, never in
`~/bokf-demo` or the live NAS volume.
- Never commit unless the human asks. When asked, keep the message short and
in the repo's style.
## Parallel work
Anything that touches more than one domain file goes on an `eff/*` (or
`feat/*`) branch in a git worktree under `/tmp/opencode/wt-*`
(`git worktree add /tmp/opencode/wt-<name> -b eff/<name> main`), never in
the main checkout. Verify the branch with `make gate`; the lead merges it
after review. Protocol and docs updates (`PROTOCOL.md`, `SCHEMA.md`,
`STATE.md`) stay in the same branch as the code they describe.
|