docs: split context into layered, agent-oriented files

Replace the single file-structure.md with a stratified layout designed
for AI/agent skill consumption: tables and concrete identifiers over
prose, files loaded on demand, content separated by churn rate.

Layers:
- architecture/  stable structural reference (overview, source-map,
                 test-map, data-flow)
- patterns/      memory, commands, autocomplete, events, xmpp,
                 encryption, ui, plugins
- testing/       unit-tests, stubs, functional-tests, bench
- build/         local, docker, ci
- playbooks/     add-command, add-test, add-autocomplete,
                 add-event-handler, add-encryption
- gotchas.md     append-only dated entries (seven seed entries)
- wip/           branch-specific notes; deleted on merge to master

Stable layers describe cproof on master only. In-flight feature
branches (currently feat/ai) get a single file under wip/.

INDEX.md is the entry map with churn labels; SKILL.md is the
always-loaded skill hint pointing to it.
This commit is contained in:
2026-04-30 20:52:06 +03:00
parent bea6ab6df5
commit 22977846a3
30 changed files with 2351 additions and 112 deletions

View File

@@ -0,0 +1,119 @@
# Playbook: add autocomplete to a command
Pre-req: command already exists in `cmd_defs.c`. If not, see
`playbooks/add-command.md`.
## Choose the flavour
| Token set | Use |
|---|---|
| Static, known at startup. | `autocomplete_param_with_ac` + a static `Autocomplete`. |
| Dynamic — depends on roster, accounts, DB query, etc. | `autocomplete_param_with_func` + a stateless callback. |
## Static token set
`src/command/cmd_ac.c`:
```c
// 1. Declare the AC at file scope:
static Autocomplete foo_ac;
// 2. Add it to the static array near the top of the file (the free-list
// that cmd_ac_uninit walks at shutdown):
static Autocomplete* all_acs[] = {
// ... existing entries ...
&foo_ac,
};
// 3. Initialise in cmd_ac_init():
foo_ac = autocomplete_new();
autocomplete_add(foo_ac, "alpha");
autocomplete_add(foo_ac, "beta");
// 4. Implement the dispatcher:
static char*
_foo_autocomplete(ProfWin* window, const char* const input, gboolean previous)
{
return autocomplete_param_with_ac(input, "/foo", foo_ac, TRUE, previous);
}
// 5. Register:
g_hash_table_insert(ac_funcs, "/foo", _foo_autocomplete);
```
## Dynamic — function callback
If suggestions come from runtime state (roster, accounts, providers list):
```c
// In a domain module (e.g. src/foo/foo.c), implement a stateless callback:
char*
foo_suggestions(const char* const search_str, gboolean previous, void* context)
{
// Compute and return a freshly-allocated char* (caller frees), or NULL.
// No module-level "_last_match" globals — keep state on the stack /
// derive deterministically from search_str and previous.
}
```
Wire into `cmd_ac.c`:
```c
static char*
_foo_autocomplete(ProfWin* window, const char* const input, gboolean previous)
{
return autocomplete_param_with_func(input, "/foo", foo_suggestions, previous, NULL);
}
g_hash_table_insert(ac_funcs, "/foo", _foo_autocomplete);
```
A clean canonical example is `roster_contact_autocomplete` in
`src/xmpp/roster_list.c` — it delegates to `autocomplete_complete` against a
roster-owned `Autocomplete` and keeps no callback-local state. **Do not
copy older callbacks that keep state in a file-static `_last_match`
variable** — those have known issues with shift-tab cycling and concurrent
completers.
## Subcommand autocompletion
If `/foo` has subcommands, branch inside `_foo_autocomplete`:
```c
static char*
_foo_autocomplete(ProfWin* window, const char* const input, gboolean previous)
{
char* result = NULL;
// First the subcommand list itself:
result = autocomplete_param_with_ac(input, "/foo", foo_subcommands_ac, TRUE, previous);
if (result) return result;
// Then per-subcommand argument completion:
if (g_str_has_prefix(input, "/foo set ")) {
result = autocomplete_param_with_func(input, "/foo set", foo_set_options, previous, NULL);
if (result) return result;
}
return NULL;
}
```
`/roster` (`src/command/cmd_ac.c`, search for `_roster_autocomplete`) is a
current example of this layered subcommand layout.
## Unit-test the callback
Stateless callbacks are easy to unit-test directly:
```c
void
test_foo_suggestions_returns_first_match(void** state)
{
char* r = foo_suggestions("al", FALSE, NULL);
assert_string_equal(r, "alpha");
free(r);
}
```
Add the test pair to `tests/unittests/` (see `playbooks/add-test.md`).

161
playbooks/add-command.md Normal file
View File

@@ -0,0 +1,161 @@
# Playbook: add a `/command`
End-to-end recipe for adding a new user-facing command. Example: `/foo`.
## 1. Define the handler
`src/command/cmd_funcs.h`:
```c
gboolean cmd_foo(ProfWin* window, const char* const command, gchar** args);
```
`src/command/cmd_funcs.c`:
```c
gboolean
cmd_foo(ProfWin* window, const char* const command, gchar** args)
{
if (connection_get_status() != JABBER_CONNECTED) {
cons_show("You are not currently connected.");
return TRUE;
}
if (!args[0]) {
cons_bad_cmd_usage(command);
return TRUE;
}
// ... do the thing ...
return TRUE;
}
```
Keep the handler thin: validate, then delegate to a domain module.
## 2. Register the command
`src/command/cmd_defs.c` — add an entry to the static `Command` array:
```c
{ CMD_PREAMBLE("/foo",
parse_args, 0, 1, NULL)
CMD_MAINFUNC(cmd_foo)
CMD_TAGS(CMD_TAG_CHAT)
CMD_SYN("/foo [<arg>]")
CMD_DESC("Do the foo thing.")
CMD_ARGS(
{ "<arg>", "Optional argument to pass." })
CMD_EXAMPLES("/foo bar")
}
```
For a command with subcommands, swap `CMD_MAINFUNC(cmd_foo)` for
`CMD_SUBFUNCS({"sub", cmd_foo_sub}, ...)`.
## 3. Autocompletion (optional)
`src/command/cmd_ac.c`:
a. (If a static token list) declare and initialise an `Autocomplete`:
```c
static Autocomplete foo_ac;
// in cmd_ac_init():
foo_ac = autocomplete_new();
autocomplete_add(foo_ac, "bar");
autocomplete_add(foo_ac, "baz");
// add &foo_ac to the static free-list near top of file
```
b. Implement the per-command dispatcher:
```c
static char*
_foo_autocomplete(ProfWin* window, const char* const input, gboolean previous)
{
return autocomplete_param_with_ac(input, "/foo", foo_ac, TRUE, previous);
}
```
c. Register it:
```c
g_hash_table_insert(ac_funcs, "/foo", _foo_autocomplete);
```
For dynamic suggestions, use `autocomplete_param_with_func` and a stateless
callback. See `patterns/autocomplete.md`.
## 4. Unit test
Create `tests/unittests/test_cmd_foo.c` and `test_cmd_foo.h`:
`test_cmd_foo.h`:
```c
void test_cmd_foo_when_disconnected_shows_message(void** state);
void test_cmd_foo_when_no_arg_shows_usage(void** state);
void test_cmd_foo_happy_path(void** state);
```
`test_cmd_foo.c`:
```c
#include "config.h"
#include "prof_cmocka.h"
#include "test_cmd_foo.h"
// ... includes for stubs and the unit ...
void
test_cmd_foo_when_disconnected_shows_message(void** state)
{
will_return(connection_get_status, JABBER_DISCONNECTED);
expect_string(cons_show, msg, "You are not currently connected.");
gchar* args[] = { NULL };
assert_true(cmd_foo(NULL, "/foo", args));
}
```
## 5. Register the test
`tests/unittests/unittests.c`:
```c
#include "test_cmd_foo.h"
// ... inside the tests[] array ...
cmocka_unit_test(test_cmd_foo_when_disconnected_shows_message),
cmocka_unit_test(test_cmd_foo_when_no_arg_shows_usage),
cmocka_unit_test(test_cmd_foo_happy_path),
```
## 6. Stubs
If `cmd_foo` calls a function that is not yet stubbed, add the stub. See
`testing/stubs.md`.
## 7. Wire into Make
- `cmd_defs.c`, `cmd_funcs.c`, `cmd_ac.c` are already in the build.
- New test file: add `tests/unittests/test_cmd_foo.c` to the unittests
sources in `tests/unittests/Makefile.am` (or whatever wires it).
- New stub file (if any): add to the same Makefile.
## 8. Build & check
Inside Docker (`build/docker.md`):
```sh
./autogen.sh && ./configure && make -j$(nproc) && make check
```
## 9. Help text
`/help foo` should now produce the synopsis / description / args from the
`Command` entry. No separate help file to update.
## 10. Commit
Single commit, conventional-commit style, English. No AI-attribution
trailer.

103
playbooks/add-encryption.md Normal file
View File

@@ -0,0 +1,103 @@
# Playbook: touch encryption modules safely
Encryption stacks (OMEMO, OTR, PGP/OX) are independent and each is gated
behind a `configure` option. Edits must respect three things: the gating
macro, the bridge file split, and the matching unit-test stubs.
See `patterns/encryption.md` for the architectural split.
## 1. Identify the bridge
| Stack | Module dir | Bridge file (XMPP side) |
|---|---|---|
| OMEMO | `src/omemo/` | `src/xmpp/omemo.c` |
| OTR | `src/otr/` | (none — in-stream on `src/xmpp/message.c`) |
| PGP / OX | `src/pgp/` | `src/xmpp/ox.c` |
Outbound calls (cproof → encryption) usually live in the bridge file.
Inbound (decrypted message → cproof) usually surfaces in the bridge or
directly in `src/xmpp/message.c`.
## 2. Identify the gating macro
```sh
grep -n "HAVE_OMEMO\|HAVE_LIBOTR\|HAVE_LIBGPGME" src/xmpp/<bridge>.c
```
Every call into the encryption module is wrapped:
```c
#ifdef HAVE_OMEMO
omemo_encrypt_message(jid, plaintext, &ciphertext);
#else
// graceful fallback or skip
#endif
```
When you add a new call, copy the existing guard. Do **not** drop the
fallback — the build must succeed with the feature disabled.
## 3. Update the public surface (header)
If you add or change an exported function in `src/omemo/omemo.h` /
`src/otr/otr.h` / `src/pgp/gpg.h` / `src/pgp/ox.h`, update **two** places:
1. The header itself.
2. The corresponding stub: `tests/unittests/{omemo,otr,pgp}/stub_*.c`.
Keep the stub signature byte-for-byte identical to the header.
## 4. Stubs
Stubs exist regardless of whether a stack is enabled at runtime — they
satisfy the unit-test linker. Three flavours, see `testing/stubs.md`.
If a unit test needs to drive the new function, switch the stub to `mock()`
and queue with `will_return`. If a unit test needs to assert arguments,
switch to `check_expected` and queue with `expect_string` / `expect_value`.
## 5. `test_forced_encryption`
`tests/unittests/test_forced_encryption.{c,h}` exercises the policy that
refuses plaintext sends when a session is encryption-locked. **Run it after
any encryption-policy change.**
## 6. Encryption-aware command tests
| Test file | Covers |
|---|---|
| `test_cmd_otr.c` / `.h` | `/otr` command surface. |
| `test_cmd_pgp.c` / `.h` | `/pgp` command surface. |
(No `test_cmd_omemo.c` exists at the time of writing — confirm with `ls
tests/unittests/test_cmd_*.c` before assuming.)
## 7. Build matrix
After changes, build with each encryption flag in turn:
```sh
./configure --disable-omemo
make check
./configure --disable-otr
make check
./configure --disable-pgp
make check
```
(Inside Docker — see `build/docker.md`.)
CI does not necessarily run every disable combination; verify locally if
your change affects gating logic.
## Conventions
- Never strip the `#ifdef HAVE_*` guard "for clarity". The disabled-build
must still compile.
- Encryption keys / fingerprints persist via the module's own store (`src/
omemo/store.c`, libotr's keystore, gpgme's keyring). Do not invent
parallel storage.
- Outbound encryption sits between message build and stanza send — see
`patterns/xmpp.md` for the flow.

View File

@@ -0,0 +1,109 @@
# Playbook: add an event handler
`src/event/` mediates between `src/xmpp/` (and locally-triggered actions)
and the rest of the codebase. Two directions:
- **Server events** (`sv_ev_*`) — triggered by inbound stanzas.
- **Client events** (`cl_ev_*`) — triggered by local actions (UI, command).
See `patterns/events.md` for the conceptual split.
## Decide direction
| Trigger | Direction | File |
|---|---|---|
| Inbound stanza parsed in `src/xmpp/`. | Server. | `src/event/server_events.c/h` |
| User typed a command, UI key, or internal timer. | Client. | `src/event/client_events.c/h` |
## Add the handler — server example
`src/event/server_events.h`:
```c
void sv_ev_foo_received(const char* const from, const char* const payload);
```
`src/event/server_events.c`:
```c
void
sv_ev_foo_received(const char* const from, const char* const payload)
{
// 1. Update persistent state.
chatlog_msg_in(from, payload);
// 2. Push UI update.
ProfChatWin* win = wins_get_chat(from);
if (win) {
chatwin_incoming_msg(win, payload, NULL, FALSE);
} else {
cons_show_incoming_message(from, payload);
}
// 3. Plugin callbacks last — state must be consistent.
plugins_post_chat_message_received(from, payload);
}
```
## Wire the trigger site
For a server event, the matching parser in `src/xmpp/` calls the new
handler. For instance, if `<foo>` stanzas are parsed in
`src/xmpp/message.c`:
```c
// ... after parsing the stanza ...
sv_ev_foo_received(from, payload);
```
For a client event, call from `cmd_funcs.c` (or wherever the user-facing
trigger lives).
## Stub the handler in tests
`tests/unittests/event/stub_*.c` — add a stub for the new function so units
that exercise the trigger site link cleanly:
```c
void
sv_ev_foo_received(const char* const from, const char* const payload)
{
// pass-through (or check_expected, depending on test needs)
}
```
If a test needs to verify the handler is called with specific args, switch
the stub to `check_expected` and the test queues `expect_string` / `expect_value`.
See `testing/stubs.md`.
## Plugin hook (if applicable)
If the event should fire a plugin callback, add a dispatcher in
`src/plugins/plugins.c/h` (e.g. `plugins_post_foo_received`) and call it
**last** in the handler — after state and UI are consistent. Keep C and
Python plugin APIs in sync (`c_api.h` / `python_api.h`).
## Unit test the handler
`tests/unittests/test_server_events.c` (or `test_client_events.c`) holds
event-side tests. Pattern:
```c
void
test_sv_ev_foo_received_writes_chatlog(void** state)
{
expect_string(chatlog_msg_in, jid, "alice@example.com");
expect_string(chatlog_msg_in, msg, "hello");
sv_ev_foo_received("alice@example.com", "hello");
}
```
## Conventions
- One handler, one event. Don't pile multiple unrelated events into the same
function.
- Order inside the handler: state → UI → plugins. Never call plugins before
state is committed.
- `sv_ev_*` and `cl_ev_*` should be straight-line — no XMPP protocol parsing,
no UI rendering. They orchestrate; they don't implement.

97
playbooks/add-test.md Normal file
View File

@@ -0,0 +1,97 @@
# Playbook: add a unit test file
Recipe for a new `tests/unittests/test_<topic>.c` + `.h` pair.
## 1. Create the header
`tests/unittests/test_<topic>.h`:
```c
#ifndef TEST_<TOPIC>_H
#define TEST_<TOPIC>_H
void test_<topic>_<scenario_1>(void** state);
void test_<topic>_<scenario_2>(void** state);
#endif
```
One declaration per test function. Headers exist solely so `unittests.c` can
include them and reference each function symbol.
## 2. Create the source
`tests/unittests/test_<topic>.c`:
```c
#include "config.h"
#include "prof_cmocka.h"
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
#include "test_<topic>.h"
// ... includes for the unit under test and any types it needs ...
void
test_<topic>_<scenario_1>(void** state)
{
// Arrange — `will_return`, `expect_string`, etc.
// Act — call the unit
// Assert — `assert_*`
}
```
## 3. Register in `unittests.c`
```c
#include "test_<topic>.h"
// inside the tests[] array passed to cmocka_run_group_tests:
cmocka_unit_test(test_<topic>_<scenario_1>),
cmocka_unit_test(test_<topic>_<scenario_2>),
```
If multiple tests share fixture, add a setup / teardown pair and use
`cmocka_unit_test_setup_teardown` (or define a per-topic macro near the top
of `unittests.c`, like `muc_unit_test`).
## 4. Stubs
For each external function the unit calls, ensure a stub exists in the
matching `tests/unittests/<module>/stub_*.c`. Three flavours:
- **Pass-through** — no observation needed.
- **`mock()` / `will_return`** — test injects return values.
- **`check_expected()` / `expect_*`** — test asserts arguments.
See `testing/stubs.md`.
## 5. Wire into Make
`tests/unittests/Makefile.am` (or the active wiring file): add
`test_<topic>.c` to the `unittests_SOURCES` (or equivalent) list. Same for
any new stub file.
## 6. Build & run
Inside Docker:
```sh
./autogen.sh && ./configure && make -j$(nproc) check
```
Diagnose failures via `tests/unittests/unittests.log` and the cmocka stderr
output.
## 7. Conventions
- Test functions: `test_<topic>_<scenario>` — never `test_topic1`,
`test_topic2`. Names should describe the scenario.
- One assertion focus per test (multiple `assert_*` calls are fine; multiple
*unrelated* assertions are not).
- Don't reuse stubs across topic suites unless the call truly is uniform —
diverging behaviour is a strong sign you want a dedicated stub.
- No I/O in unit tests. Filesystem, network, ncurses are all stubbed.