3 Commits

Author SHA256 Message Date
8a627f5bf5 docs: stage AGENTS.md draft for cproof
Project-level agent instructions destined for the cproof repo root.
Staged here while the layered context bundle settles; will move to
cproof in a follow-up.

Pins the cproof-context bundle to commit 22977846 and tells the agent
the load order: SKILL.md -> INDEX.md -> on-demand -> gotchas.md, plus
wip/<branch>.md when working on a listed feature branch.
2026-04-30 21:08:31 +03:00
22977846a3 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.
2026-04-30 20:52:06 +03:00
bea6ab6df5 Add file structure 2026-04-29 12:15:57 +00:00
33 changed files with 2416 additions and 847 deletions

65
AGENTS.md Normal file
View File

@@ -0,0 +1,65 @@
# AGENTS
> **Note:** this file is staged in `cproof-context` for review. Its
> destination is the **cproof** repo root, not this one. It is **not**
> agent instructions for the `cproof-context` repository itself.
Project-level agent instructions for cproof (terminal XMPP client, fork of
Profanity).
## Agent context bundle
Detailed architecture, patterns, testing, build, playbooks, and known
pitfalls for this project live in a separate repository:
- **Repo:** https://git.jabber.space/devs/cproof-context
- **Pinned commit:** `22977846a3b2c1727ae6aeeaa1de70971bc0136e46b131febac7591df7d72e75`
(branch `draft/split-context`)
Bump the pinned commit explicitly when the context bundle is updated. Do
not track a moving branch.
## Load order
On session start (before doing meaningful work in this repo):
1. Read `SKILL.md` from the pinned commit — the always-loaded entry hint.
2. Read `INDEX.md` — map of every context file with churn labels and
"When to load" guidance.
3. Pull individual files from the bundle on demand, driven by the task
and the `INDEX.md` "When to load" column. Do **not** preload the whole
bundle.
4. Always scan `gotchas.md` before completing a non-trivial task.
5. If working on a feature branch listed under `wip/` (currently
`wip/feat-ai.md` for branch `feat/ai`), load that file as well — the
stable bundle layers describe `master` only.
## Hard rules for this project
- **Build inside Docker.** Use `Dockerfile.debian` at the repo root. Do
not run `./autogen.sh`, `./configure`, or `make` on the host shell. See
`build/docker.md` in the context bundle.
- **`make check` is the only routine test entry point.** Functional tests
(`tests/functionaltests/`) and benches (`tests/bench/`) are not part of
CI; run them locally inside the Docker image when needed.
- **Treat path / identifier references in the context bundle as claims
to verify.** They were authored against a specific cproof commit; grep
the live tree before relying on them.
- **`master` is the canonical baseline.** Feature-branch surface (e.g.
`src/ai/` on `feat/ai`) is documented only under `wip/<branch>.md` in
the context bundle. Do not assume branch-specific files exist on
`master`.
## Where agent context does NOT live
- This file is the only project-level agent instruction. There is no
parallel `CLAUDE.md`, `.cursorrules`, or per-tool variant.
- User-specific preferences (language, commit-trailer policy, identity)
belong in the agent runtime's user-memory, not in this file.
## Updating the bundle
Edits to architectural / pattern / test / build documentation land in the
`cproof-context` repo, not here. After a context-bundle PR merges,
update the pinned commit hash above in the same PR cycle that consumes
the new content.

77
INDEX.md Normal file
View File

@@ -0,0 +1,77 @@
# INDEX
One-page map of every context file. Churn labels: **low** (years), **med**
(months), **high** (weeks).
## Top level
| File | Churn | When to load |
|---|---|---|
| `README.md` | low | Never — repo metadata for humans. |
| `SKILL.md` | low | Always (entry hint). |
| `INDEX.md` | low | Always (this file). |
## architecture/ — structural reference
| File | Churn | When to load |
|---|---|---|
| `overview.md` | low | First load on any cproof task. |
| `source-map.md` | low | When choosing where to put new code, or locating an unfamiliar module. |
| `test-map.md` | low | When writing or running tests. |
| `data-flow.md` | low | When reasoning about input → output paths (commands, events, UI updates). |
## patterns/ — conventions and idioms
| File | Churn | When to load |
|---|---|---|
| `memory.md` | low | Any C edit that allocates / frees. |
| `commands.md` | med | Adding, changing, or understanding a `/command`. |
| `autocomplete.md` | med | Anything that touches `cmd_ac.c` or registers an `Autocomplete`. |
| `events.md` | med | Touching `src/event/`. |
| `xmpp.md` | med | Touching `src/xmpp/` or any XMPP-gated code path. |
| `encryption.md` | med | Touching `src/omemo/`, `src/otr/`, or `src/pgp/`. |
| `ui.md` | med | Touching `src/ui/`. |
| `plugins.md` | med | Touching `src/plugins/`, plugin C/Python API. |
## testing/ — how tests work
| File | Churn | When to load |
|---|---|---|
| `unit-tests.md` | med | Writing or extending unit tests. |
| `stubs.md` | med | Adding a function whose callers are unit-tested, or wiring a new stub. |
| `functional-tests.md` | med | Touching `tests/functionaltests/`. |
| `bench.md` | low | Touching `tests/bench/` or interpreting bench output. |
## build/ — how to build
| File | Churn | When to load |
|---|---|---|
| `local.md` | low | Reference for `autogen.sh` / `configure` / `make` flags. |
| `docker.md` | low | Always when building — canonical environment. |
| `ci.md` | med | Diagnosing CI failures, reading CI logs. |
## playbooks/ — step-by-step recipes
| File | Churn | When to load |
|---|---|---|
| `add-command.md` | med | Adding a new `/command`. |
| `add-test.md` | med | Adding a new unit test file. |
| `add-autocomplete.md` | med | Adding autocomplete for an existing command. |
| `add-event-handler.md` | med | Adding a server- or client-event handler. |
| `add-encryption.md` | med | Touching encryption modules safely. |
## gotchas.md
| File | Churn | When to load |
|---|---|---|
| `gotchas.md` | high (append-only) | Always scan before finishing a task. |
## wip/ — branch-specific notes
Files describing in-flight feature branches. Each file is **deleted** when
its branch merges to `master`; the surviving content (if any) is folded into
the stable layers above.
| File | Churn | When to load |
|---|---|---|
| `wip/feat-ai.md` | high (deleted on merge) | Working on `feat/ai`, or trying to understand AI-related code that does not yet exist on `master`. |

View File

@@ -1,2 +1,68 @@
# cproof-context # cproof-context
Agent-oriented context documentation for the [cproof](https://git.jabber.space/devs/cproof)
project (a fork of Profanity, a terminal-based XMPP client written in C).
This repository is **not** human-onboarding documentation. It is structured for
consumption by AI/agent skills — tables over prose, concrete identifiers over
descriptions, layered files loaded on demand.
## Layout
| Path | Churn | Purpose |
|---|---|---|
| `SKILL.md` | low | Always-loaded entry hint. Points to `INDEX.md`. |
| `INDEX.md` | low | One-page map of every doc with churn label. |
| `architecture/` | low | Stable structural reference: source map, test map, data flow. |
| `patterns/` | medium | Codified conventions: memory, commands, autocomplete, events, etc. |
| `testing/` | medium | How tests are organised, written, and stubbed. |
| `build/` | medium | Local, Docker, and CI build flows. |
| `playbooks/` | medium | Step-by-step recipes for common tasks. |
| `gotchas.md` | append-only | Dated entries: one per known pitfall. |
| `wip/` | high | Branch-specific notes for in-flight features. Deleted on merge. |
See `INDEX.md` for the full file list.
## Consumer
Consumed by an agent skill that operates on the cproof codebase. The skill
should pin this repo to a specific SHA (not a branch) and treat the loaded
files as authoritative for their stated scope.
## Update flow
1. Edits land via PRs against `master`.
2. Drift checks (path / identifier existence in cproof HEAD) run in CI before
merge — see `architecture/test-map.md` and `build/ci.md` for details once
wired up.
3. After major cproof refactors, the skill consumer bumps the pinned SHA to a
commit known to pass drift checks.
## Out of scope
- User-specific agent preferences (language, commit-trailer policy, etc.) —
belong in agent memory, not here.
- Git history snapshots, changelogs, PR summaries — derivable from `git log` /
`gh pr`.
- Aspirational guidance — this repo describes cproof as it is, not as it
should be.
## Baseline and WIP
The stable layers (`architecture/`, `patterns/`, `testing/`, `build/`,
`playbooks/`, `gotchas.md`) describe **cproof on `master`**. They do not
mention symbols, files, or commits that exist only on a feature branch.
In-flight feature branches that materially change the surface (new module,
new commands, new stubs) get a single file under `wip/<branch-slug>.md`. On
merge to `master`, that file is deleted and any surviving content is folded
into the stable layers.
Current WIP: `wip/feat-ai.md` (cproof branch `feat/ai`).
## Status
Layout is being authored on `draft/split-context`. The single
`file-structure.md` (still present on `feat/add-context`) is being decomposed
into the structure above; once the split is complete, `file-structure.md` is
deleted.

35
SKILL.md Normal file
View File

@@ -0,0 +1,35 @@
---
name: cproof-context
description: Architectural and conventional context for the cproof XMPP terminal client. Load INDEX.md first; pull individual files on demand based on the task at hand.
---
# cproof skill entry
cproof is a terminal-based XMPP client in C, fork of Profanity (renaming WIP).
Build: autotools + GLib. Tests: cmocka (unit) + custom runner (functional) +
custom benches.
## How to use this context
1. Read `INDEX.md` for the full file map and churn labels.
2. For any non-trivial task, pull only the files relevant to it:
- Touching commands? → `patterns/commands.md`, `patterns/autocomplete.md`,
`playbooks/add-command.md`.
- Touching tests? → `testing/unit-tests.md`, `testing/stubs.md`,
`playbooks/add-test.md`.
- Touching XMPP? → `patterns/xmpp.md`.
- Building? → `build/docker.md` (canonical) or `build/local.md`.
3. Always consult `gotchas.md` before finishing — it lists known pitfalls.
4. If the task is on a feature branch listed under `wip/`, load the matching
WIP file as well — the stable layers describe `master` only.
5. Treat path/identifier references in this context as claims to verify
against cproof HEAD before acting on them.
## Hard rules
- Build inside Docker (`Dockerfile.debian`). Do not run `autogen.sh` /
`./configure` / `make` on the host.
- Do not mix architecture, patterns, status, and gotchas in a single doc — each
has its own home.
- Do not duplicate what `ls` / `grep` would answer cheaper. This context exists
for invariants, decisions, and "why".

76
architecture/data-flow.md Normal file
View File

@@ -0,0 +1,76 @@
# Data flow
High-level paths through the codebase. Use this when reasoning about where a
change should land or where a bug originates.
## User input → action
```
keypress
→ src/ui/inputwin.c (line editor, key dispatch)
→ src/command/cmd_ac.c (autocomplete on Tab)
→ src/command/cmd_defs.c (parse / lookup)
→ src/command/cmd_funcs.c (handler: cmd_<name>)
→ side effects: src/xmpp/, src/config/, src/ui/, src/event/
```
- Tab triggers autocomplete via `cmd_ac.c` lookup in `ac_funcs` hash table
(key: `/cmd`, value: per-command `_<cmd>_autocomplete` function).
- Enter parses input via `parse_args()` (`src/tools/parser.c`), looks up the
`Command*` via `cmd_get()` (`src/command/cmd_defs.c`), invokes the handler
declared with `CMD_MAINFUNC` or one of the `CMD_SUBFUNCS`.
## Outgoing chat message
```
cmd_msg / typing-into-chatwin
→ src/xmpp/message.c (build message stanza)
→ src/xmpp/stanza.c (assemble XML)
→ libstrophe (send)
→ src/chatlog.c (persist locally)
→ src/ui/chatwin.c (echo into window)
```
Encryption injects between message build and send:
- OMEMO via `src/xmpp/omemo.c``src/omemo/omemo.c` (libsignal-protocol).
- OTR via `src/otr/otr.c` (libotr).
- PGP / OX via `src/pgp/gpg.c` / `src/pgp/ox.c``src/xmpp/ox.c`.
## Incoming stanza
```
libstrophe callback
→ src/xmpp/{message,presence,iq,muc,roster}.c (parse, validate)
→ src/event/server_events.c (sv_ev_*)
→ src/ui/{chatwin,mucwin,console,...}.c (render)
→ src/chatlog.c (persist)
```
`server_events.c` is the chokepoint — every inbound XMPP event flows through a
`sv_ev_*` function. UI updates and persistence fan out from there.
## Client-initiated events
`src/event/client_events.c` mirrors `server_events.c` for events that
originate locally (UI actions, command-driven state changes) but need the
same downstream fan-out.
## Connection state
- `src/xmpp/connection.c` owns the `jabber_conn_status_t` (declared in
`connection.h`).
- `connection_get_status()` returns the current state. Many code paths gate
on `JABBER_CONNECTED` — see `patterns/xmpp.md`.
- Tests must `will_return(connection_get_status, JABBER_CONNECTED)` (or the
desired state) before exercising any code path that consults it. See
`gotchas.md`.
## Plugin hook points
Plugin callbacks (`src/plugins/callbacks.c`) sit on:
- Pre/post message send and receive (around `src/xmpp/message.c` flow).
- Connection lifecycle (around `src/xmpp/session.c`).
- Window-switch events (around `src/ui/window_list.c`).
See `patterns/plugins.md`.

59
architecture/overview.md Normal file
View File

@@ -0,0 +1,59 @@
# Overview
## What cproof is
- Terminal-based XMPP (chat) client written in C.
- Fork of [Profanity](https://github.com/profanity-im/profanity); rename to
cproof is **work in progress** — many internal symbols, copyright headers,
and identifiers still say "Profanity".
- Heavy GLib usage (strings, lists, hash tables, key files).
- Optional integrations: OMEMO (libsignal-protocol), OTR (libotr), PGP/OX
(gpgme), Python plugins, libnotify, libgcrypt, libreadline, ncurses.
## Build system
- Autotools: `configure.ac` + a top-level `Makefile.am`. No CMake.
- `autogen.sh` regenerates the configure script.
- `configure-debug` is a thin helper that invokes `./configure` with
debug-friendly flags.
- Canonical build environment: Docker (`Dockerfile.debian` at repo root).
See `build/docker.md`.
## Test framework
- **Unit tests:** cmocka, run via `make check`. Live in `tests/unittests/`.
- **Functional tests:** custom runner in `tests/functionaltests/` against a
real running cproof instance.
- **Benches:** custom runners in `tests/bench/<runner>/` for database /
flatfile / long-message workloads.
- See `architecture/test-map.md` and the `testing/` directory for detail.
## High-level module split
Source lives under `src/` in module directories — see
`architecture/source-map.md` for the full table. Major chunks:
- **Command layer** (`src/command/`) — `/foo`-style commands the user types.
- **XMPP layer** (`src/xmpp/`) — protocol, sessions, stanza building.
- **UI layer** (`src/ui/`) — windows, buffers, status bar, input.
- **Event layer** (`src/event/`) — bridges between XMPP and UI / commands.
- **Encryption** (`src/omemo/`, `src/otr/`, `src/pgp/`) — three independent
encryption stacks.
- **Plugins** (`src/plugins/`) — C and Python plugin APIs.
- **Tools** (`src/tools/`) — generic utilities (parser, autocomplete, HTTP,
clipboard, editor).
Active feature branches (e.g. `feat/ai`, which adds `src/ai/`) are not
described in the stable architecture files. See `wip/` for branch-specific
notes.
## Entry points
| File | Role |
|---|---|
| `src/main.c` | Process entry. |
| `src/profanity.c` | Application init/shutdown lifecycle. |
| `src/common.h` | Project-wide macros (`auto_*`, `FREE_SET_NULL`, `ARRAY_SIZE`). |
| `src/database.c` / `src/database.h` | Database (SQLite + flatfile) abstraction. |
| `src/chatlog.c` | Chat history persistence. |
| `src/log.c` | Application log. |

View File

@@ -0,0 +1,38 @@
# Source map
## `src/` module directories
| Path | Purpose | Key files |
|---|---|---|
| `src/command/` | `/command` definition, dispatch, autocompletion. | `cmd_defs.c/h`, `cmd_funcs.c/h`, `cmd_ac.c/h` |
| `src/config/` | Persistent configuration: accounts, prefs, themes, colors, CA file, scripts. | `accounts.c/h`, `preferences.c/h`, `theme.c/h`, `color.c/h`, `cafile.c/h`, `scripts.c/h`, `tlscerts.c/h`, `conflists.c/h`, `files.c/h`, `account.c/h` |
| `src/event/` | Mediates between XMPP layer and UI/commands. | `client_events.c/h`, `server_events.c/h`, `common.c/h` |
| `src/omemo/` | OMEMO encryption (libsignal-protocol). | `omemo.c/h`, `crypto.c/h`, `store.c/h` |
| `src/otr/` | OTR encryption (libotr v4). | `otr.c/h`, `otrlib.h`, `otrlibv4.c` |
| `src/pgp/` | OpenPGP / OX (gpgme). | `gpg.c/h`, `ox.c/h` |
| `src/plugins/` | C and Python plugin support. | `plugins.c/h`, `c_plugins.c/h`, `python_plugins.c/h`, `c_api.c/h`, `python_api.c/h`, `api.c/h`, `profapi.c/h`, `callbacks.c/h`, `autocompleters.c/h`, `disco.c/h`, `settings.c/h`, `themes.c/h` |
| `src/tools/` | Generic utilities. | `parser.c/h`, `autocomplete.c/h`, `clipboard.c/h`, `editor.c/h`, `http_*.c/h`, `aesgcm_download.c/h`, `bookmark_ignore.c/h`, `plugin_download.c/h` |
| `src/ui/` | Windows, buffers, status bar, input. | `window.c/h`, `window_list.c/h`, `chatwin.c`, `mucwin.c`, `console.c`, `confwin.c`, `privwin.c`, `xmlwin.c`, `vcardwin.c`, `rosterwin.c`, `occupantswin.c`, `inputwin.c/h`, `statusbar.c/h`, `titlebar.c/h`, `tray.c/h`, `screen.c/h`, `notifier.c`, `core.c`, `buffer.c/h`, `ui.h`, `win_types.h` |
| `src/xmpp/` | XMPP protocol layer. | `connection.c/h`, `session.c/h`, `roster.c/h`, `roster_list.c/h`, `muc.c/h`, `presence.c/h`, `message.c/h`, `iq.c/h`, `stanza.c/h`, `vcard.c/h`, `vcard_funcs.h`, `jid.c/h`, `capabilities.c/h`, `omemo.c/h`, `ox.c/h`, `blocking.c/h`, `bookmark.c/h`, `chat_session.c/h`, `chat_state.c/h`, `avatar.c/h`, `contact.c/h`, `form.c/h`, `resource.c/h` |
## Top-level files in `src/`
| File | Role |
|---|---|
| `main.c` | Process entry. |
| `profanity.c` / `profanity.h` | Application init/shutdown. |
| `common.h` / `common.c` | Project-wide macros and small helpers. |
| `database.c` / `database.h` | Database facade (SQLite + flatfile). |
| `chatlog.c` / `chatlog.h` | Chat log writer/reader. |
| `log.c` / `log.h` | Application log. |
| `config.h.in` | autoconf-generated config header (do not edit `config.h`). |
| `gitversion.h.in` | Generated `gitversion.h` carrying build-time SHA. |
## Naming notes
- The fork-rename (Profanity → cproof) is incomplete. Many symbols, file
paths, and headers still carry "Profanity" / "prof_" prefixes. Treat them as
current and authoritative; do not rename opportunistically.
- Encryption modules under `src/omemo/`, `src/otr/`, `src/pgp/` use module-
internal helpers; the XMPP-side bridges live in `src/xmpp/omemo.c` and
`src/xmpp/ox.c` (separate files with the same names — be precise about path).

69
architecture/test-map.md Normal file
View File

@@ -0,0 +1,69 @@
# Test map
## Top-level test directories
| Path | Runner | Purpose |
|---|---|---|
| `tests/unittests/` | cmocka via `make check` | Unit tests with stubbed dependencies. |
| `tests/functionaltests/` | custom binary, runs against a real cproof | End-to-end behaviour tests. |
| `tests/bench/` | custom benches, one binary per runner | Database / flatfile / long-message benchmarks. |
| `tests/prof_cmocka.h` | — | Thin cmocka wrapper used by all unit tests. |
## `tests/unittests/` layout
- `unittests.c` — single test runner. Registers every test via
`cmocka_unit_test()` / `cmocka_unit_test_setup_teardown()`.
- `helpers.c` / `helpers.h` — shared test utilities.
- `test_<topic>.c` + `test_<topic>.h` — paired test files. The header
declares each test function; the runner includes the header to register it.
- Per-module subdirectories carry **stubs** for that module's public symbols:
| Stub directory | Stubs for |
|---|---|
| `tests/unittests/chatlog/` | `src/chatlog.c` |
| `tests/unittests/command/` | `src/command/` |
| `tests/unittests/config/` | `src/config/` (accounts, cafile, etc.) |
| `tests/unittests/database/` | `src/database.c` |
| `tests/unittests/event/` | `src/event/` |
| `tests/unittests/log/` | `src/log.c` |
| `tests/unittests/omemo/` | `src/omemo/` |
| `tests/unittests/otr/` | `src/otr/` |
| `tests/unittests/pgp/` | `src/pgp/` |
| `tests/unittests/plugins/` | `src/plugins/` |
| `tests/unittests/tools/` | `src/tools/` |
| `tests/unittests/ui/` | `src/ui/` |
| `tests/unittests/xmpp/` | `src/xmpp/` |
| `tests/unittests/unittests/` | top-level utility stubs |
Branch-specific stubs (e.g. `tests/unittests/ai/` on `feat/ai`) are not
listed here — see `wip/` for branch notes.
Each stub file is named `stub_<module>.c`. See `testing/stubs.md` for when
to add or extend a stub.
## `tests/functionaltests/` layout
- `functionaltests.c` — runner.
- `proftest.c` / `proftest.h` — fixture: spawns cproof under PTY, talks XMPP.
- `test_<feature>.c` — feature suites: connect, disconnect, message, presence,
carbons, roster, history, ping, autoping, lastactivity, software, disco,
receipts, rooms, muc, chat_session, export_import.
Functional tests require:
- A test XMPP server reachable from the test harness.
- A pre-baked `.profrc` (see recent perf work).
- See `testing/functional-tests.md` for prerequisites and runner flags.
## `tests/bench/` layout
Each runner is a separate directory + binary:
| Runner | Measures |
|---|---|
| `bench_runner` | Combined runner for general bench scenarios. |
| `bench_export_import` | Database export/import throughput. |
| `bench_failure_modes` | Flatfile parser failure / recovery paths. |
| `bench_long_messages` | Flatfile parser with very long messages. |
| `gen_history` | Generates synthetic chat history for benches. |
See `testing/bench.md` for invocation and interpretation.

55
build/ci.md Normal file
View File

@@ -0,0 +1,55 @@
# CI
## Workflows
`.github/workflows/`:
| Workflow | Purpose |
|---|---|
| `ci-code.yml` | Build + unit tests across the Linux distro matrix. |
| `ci-api-docs.yml` | API/plugin docs build check. |
Plus an OpenBSD pipeline at `.builds/openbsd.yml` (sourcehut-style builds).
## Distro matrix
The code CI runs across the Dockerfiles at repo root: Debian, Ubuntu,
Fedora, Tumbleweed, Arch. Each job builds the matching image, mounts the
checkout, and runs `autogen.sh && configure && make && make check`.
## Log naming
CI logs in this project are named:
```
ci-code-Linux (arch)-NNNN.log
```
— where `arch` is the distro flavour (debian, ubuntu, fedora, tumbleweed,
arch) and `NNNN` is the run number. Use the distro name to pick the right
log when triaging matrix failures.
## Known matrix-specific notes
- **Arch (`Dockerfile.arch`):** Pikaur in the image was hit by a duplicated-flag
failure (cf. cproof commit `0722dc9e3`). If `Dockerfile.arch` changes,
re-check the Pikaur invocation.
- **OpenBSD (`.builds/openbsd.yml`):** runs on sourcehut, not GitHub. Failures
there don't appear in the GitHub PR check list.
## Reproducing a CI failure locally
1. Identify the failing distro from the log name.
2. Build the matching Dockerfile locally — see `build/docker.md`.
3. Run the same `autogen && configure && make && make check` chain inside the
container.
4. Compare versions of any drifting dependency between the image and the CI
image (`apt list --installed` or equivalent inside both).
## What CI does not run
- Functional tests (`tests/functionaltests/`) — require a test XMPP server.
- Benches (`tests/bench/`) — not part of CI.
If you need either, run them locally inside the Docker image — see
`testing/functional-tests.md` and `testing/bench.md`.

75
build/docker.md Normal file
View File

@@ -0,0 +1,75 @@
# Docker build
Canonical build environment. **Use this; do not build on the host.**
## Dockerfiles
All at repo root:
| File | Base | Purpose |
|---|---|---|
| `Dockerfile.debian` | `debian:testing` | **Canonical.** Mirrors CI for Linux. |
| `Dockerfile.ubuntu` | `ubuntu` | Alternate Linux for compatibility checks. |
| `Dockerfile.fedora` | `fedora` | Fedora compatibility. |
| `Dockerfile.tumbleweed` | `opensuse/tumbleweed` | openSUSE compatibility. |
| `Dockerfile.arch` | `archlinux` | Arch CI matrix. |
Default: `Dockerfile.debian` for local dev, build, and `make check`.
## Build the image
```sh
docker build -t cproof-debian -f Dockerfile.debian .
```
`Dockerfile.debian` installs the autotools toolchain plus all optional
dependencies (cmocka, libsignal-protocol-c, libotr5, gpgme, libcurl,
libmicrohttpd, libnotify, ncurses, GLib, libgcrypt, libreadline, libssl,
Python). Treat its `apt-get install` block as the authoritative dependency
list.
## Build cproof inside the container
Bind-mount the working tree and run the autotools sequence:
```sh
docker run --rm -it \
-v "$PWD":/src -w /src \
cproof-debian \
bash -c './autogen.sh && ./configure && make -j$(nproc)'
```
For tests:
```sh
docker run --rm -it \
-v "$PWD":/src -w /src \
cproof-debian \
bash -c './autogen.sh && ./configure && make -j$(nproc) check'
```
`ccache` is enabled in the image (`CC="ccache gcc"`) — share a ccache volume
between runs for faster rebuilds:
```sh
-v "$HOME/.ccache":/root/.ccache
```
## Why Docker
- Pinned OS + package set; the same toolchain CI uses.
- Host glib / openssl / libstrophe drift cannot mask or invent failures.
- Encryption-stack dependencies (libsignal-protocol-c especially) are
awkward on host package managers; the image already has them.
## When to use a non-Debian image
- Reproducing a CI failure that only triggers on Arch / Fedora / Ubuntu /
Tumbleweed. Use the matching Dockerfile, same bind-mount pattern.
- Otherwise: stick with Debian.
## Hard rule
Do not run `./autogen.sh`, `./configure`, or `make` on the host shell unless
you have an explicit reason and have verified the host toolchain matches the
image. CI is the source of truth; CI uses Debian.

68
build/local.md Normal file
View File

@@ -0,0 +1,68 @@
# Local build
Reference for `autogen.sh` / `configure` / `make` flags. **Do not run these
on the host as a default workflow** — see `build/docker.md`. This file
documents what the canonical Docker build does internally.
## Toolchain
- autotools: `autoconf`, `automake`, `libtool`, `autoconf-archive`.
- C compiler: `gcc` (CI) or `clang`.
- GLib 2, ncursesw, OpenSSL, libstrophe, libreadline.
- Optional: libcmocka (tests), libsignal-protocol-c (OMEMO), libotr5 (OTR),
gpgme (PGP), libcurl (HTTP), libmicrohttpd (HTTP server features),
libnotify, libgcrypt, Python (plugins).
The full Debian package list is in `Dockerfile.debian` — treat that as the
authoritative dependency list.
## Sequence
```sh
./autogen.sh # generate configure from configure.ac
./configure [opts]
make -jN
```
`autogen.sh` runs `aclocal`, `autoheader`, `automake`, `autoconf`, `libtoolize`.
## Useful `./configure` flags
| Flag | Effect |
|---|---|
| `--enable-debug` | Debug build (the `configure-debug` script wraps this). |
| `--disable-omemo` / `--disable-otr` / `--disable-pgp` | Drop encryption stack. |
| `--disable-python-plugins` | C-only plugins. |
| `--disable-notifications` | No libnotify. |
| `--prefix=<path>` | Install prefix. |
`./configure --help` prints the full set; flag names track upstream Profanity
closely.
## `configure-debug`
Convenience script at repo root. Roughly:
```sh
./autogen.sh
./configure --enable-debug --prefix=$(pwd)/install
```
Tweak in-script if you want different debug knobs.
## `make` targets
| Target | What |
|---|---|
| `make` | Builds the `profanity` binary (and `cproof` symlink, if rename has progressed to it). |
| `make check` | Builds and runs the unit-test suite. |
| `make clean` | Wipes object files. |
| `make distclean` | Wipes generated configure artefacts too. |
## Why this is host-unfriendly
- Library version drift between the host (a workstation) and the canonical
Debian build means the host build can succeed while CI fails (or vice
versa) — bugs that don't reproduce in either env are very expensive.
- `Dockerfile.debian` pins the OS and package set used by CI. Mirror it
locally instead of fighting the host.

114
gotchas.md Normal file
View File

@@ -0,0 +1,114 @@
# Gotchas
Append-only log of known pitfalls. One entry per pitfall. Format:
```
## YYYY-MM-DD — <short title>
<body — what bites, why, what to do instead>
```
Scan this file before finishing any non-trivial task.
---
## 2026-04-30 — `g_strsplit` result must use `g_strfreev`
`g_strsplit` (and `g_strdupv`) return a `gchar**` whose elements are
individually GLib-allocated. Free with `g_strfreev`, **not** `g_free`. The
latter compiles fine and silently leaks the inner strings.
The cleanup macro is `auto_gcharv` (declared in `src/common.h`):
```c
auto_gcharv gchar** parts = g_strsplit(line, " ", -1);
```
Mixing `g_free` on a `gchar**` is the most common allocator-mismatch bug in
this codebase.
---
## 2026-04-30 — `JABBER_CONNECTED` must be queued in unit tests
Most XMPP-touching code paths gate on `connection_get_status()`. The unit-
test stub returns `mock()`, so every test reaching that gate must:
```c
will_return(connection_get_status, JABBER_CONNECTED);
// or
will_return(connection_get_status, JABBER_DISCONNECTED);
```
Forgetting this aborts cmocka with a "missing mock value" error. If you see
that error, it's almost always `connection_get_status` you forgot.
---
## 2026-04-30 — Autocomplete callbacks must be stateless
Some older callbacks keep "last match" state in a file-static `_last_*_match`
variable. Do not copy that pattern. A clean canonical example is
`roster_contact_autocomplete` (`src/xmpp/roster_list.c`) — it delegates
straight to `autocomplete_complete` against a roster-owned `Autocomplete`
object and keeps no callback-local state.
Stateful callbacks misbehave under shift-tab cycling and break when two
completers run concurrently in a layered command (e.g. `/foo set bar <tab>`
followed by `/foo set baz <tab>`).
---
## 2026-04-30 — CWE-134 format-string discipline
Never pass user-controlled or arbitrary strings as the format argument to
`printf`-family or GLib formatted-print functions. A recent compiler-
hardening pass (cf. cproof commit `9ec01fa8c`) audited and tightened these
sites; do not regress.
Right:
```c
cons_show("%s", user_text);
```
Wrong:
```c
cons_show(user_text);
```
The compiler does not always warn, especially across helper layers. Enable
and respect `-Wformat -Wformat-security` warnings in any build you trust.
---
## 2026-04-30 — Build inside Docker, not on the host
Library version drift between a developer host and `Dockerfile.debian`
(GLib, OpenSSL, libstrophe in particular) can mask or invent failures. CI
is the source of truth; CI builds inside the Debian image. See
`build/docker.md`.
If you absolutely must build on the host, mirror the package set in
`Dockerfile.debian` exactly and pin versions — you will spend more time
chasing version drift than the Docker build would have cost.
---
## 2026-04-30 — Beware of "Profanity" vs "cproof" naming
The fork-rename is incomplete. Many files, symbols, copyright headers, and
config keys still say `Profanity` / `prof_` / `profanity`. These are
authoritative as-is; do **not** rename them opportunistically alongside
unrelated changes. Rename work, when it happens, will be a dedicated PR.
---
## 2026-04-30 — Encryption disable builds must compile
Each encryption stack (`HAVE_OMEMO`, `HAVE_LIBOTR`, `HAVE_LIBGPGME`) can be
turned off at `configure` time. New code that calls into these modules
must wrap calls in the matching `#ifdef` and provide a graceful fallback
in the `#else`. CI does not always test every disable combination — verify
locally with at least the off-build for each stack you touched.

97
patterns/autocomplete.md Normal file
View File

@@ -0,0 +1,97 @@
# Autocomplete
## The `Autocomplete` type
`src/tools/autocomplete.h` defines an opaque `Autocomplete` (GList-backed
sorted set with a stateful "current iterator" for cycling through matches).
| Function | Purpose |
|---|---|
| `autocomplete_new()` | Create empty AC. |
| `autocomplete_clear(ac)` | Drop all entries (keep object). |
| `autocomplete_free(ac)` | Destroy. |
| `autocomplete_add(ac, item)` | Insert sorted. |
| `autocomplete_add_unsorted(ac, item, reversed)` | Append, no sort. |
| `autocomplete_add_all(ac, items)` | Bulk add from `char**`. |
| `autocomplete_update(ac, items)` | Clear + add_all. |
| `autocomplete_remove(ac, item)` | Drop one. |
| `autocomplete_remove_all(ac, items)` | Drop many. |
| `autocomplete_complete(ac, search, quote, previous)` | Cycle to next/previous match. |
| `autocomplete_reset(ac)` | Reset cycle position. |
| `autocomplete_contains(ac, value)` | Membership. |
## Two completion flavours used in commands
In `src/command/cmd_ac.c`:
```c
autocomplete_param_with_ac(input, "/cmd sub", static_ac, quote, previous);
autocomplete_param_with_func(input, "/cmd sub", callback, previous, ctx);
```
| When to use | Choose |
|---|---|
| Fixed token set known at startup. | `autocomplete_param_with_ac` + a static `Autocomplete` (allocated in `cmd_ac_init`, freed in `cmd_ac_uninit`). |
| Token set is dynamic / queried from state. | `autocomplete_param_with_func` + a callback `char* fn(const char* search, gboolean previous, void* ctx)`. |
## Static Autocompletes
Pattern (from `cmd_ac.c`):
```c
// File-scope:
static Autocomplete account_ac;
// In cmd_ac_init():
account_ac = autocomplete_new();
autocomplete_add(account_ac, "list");
autocomplete_add(account_ac, "show");
autocomplete_add(account_ac, "add");
// ...
// Registered with:
g_hash_table_insert(ac_funcs, "/account", _account_autocomplete);
```
Static `Autocomplete` instances are added to a file-scope free-list array
near the top of `cmd_ac.c` so `cmd_ac_uninit` can call `autocomplete_free`
on each at shutdown. **Add new static ACs to that free-list.**
## Function-callback completers
```c
char* roster_contact_autocomplete(const char* const search_str,
gboolean previous, void* context);
```
- Stateless across invocations — no module-level `_last_match` globals.
- Returns a freshly-allocated `char*` (caller frees), or `NULL` when no more
matches.
- `previous` cycles backward (Shift+Tab).
- `context` is whatever was passed in `autocomplete_param_with_func`'s last
arg (often `NULL`).
`roster_contact_autocomplete` (`src/xmpp/roster_list.c`) is a clean
canonical example: it delegates straight to `autocomplete_complete` against
a roster-owned `Autocomplete` object — no callback-local state.
**Avoid the legacy "store previous match in a static" pattern.** Some older
completers do it; do not copy them. Keeping callbacks stateless makes
shift-tab cycling and concurrent completers safe.
## Per-command dispatcher
Every command's autocomplete is a single static function in `cmd_ac.c`:
```c
static char* _account_autocomplete(ProfWin* window, const char* const input, gboolean previous);
```
Registered in `cmd_ac_init`:
```c
g_hash_table_insert(ac_funcs, "/account", _account_autocomplete);
```
The dispatcher inspects the input prefix and picks the right
`autocomplete_param_with_*` call.

109
patterns/commands.md Normal file
View File

@@ -0,0 +1,109 @@
# Commands
## Three files, three concerns
| File | Holds |
|---|---|
| `src/command/cmd_defs.c` (`cmd_defs.h`) | The big static array of `Command` records — every `/foo` is one entry. |
| `src/command/cmd_funcs.c` (`cmd_funcs.h`) | Handler implementations: `gboolean cmd_<name>(ProfWin*, const char* const, gchar**)`. |
| `src/command/cmd_ac.c` (`cmd_ac.h`) | Per-command autocomplete logic and registration. |
## Anatomy of a command record
Defined inline in `cmd_defs.c` using uppercase macros (declared in `cmd_defs.c`):
```c
{ CMD_PREAMBLE("/caps",
parse_args, 0, 1, NULL)
CMD_MAINFUNC(cmd_caps)
CMD_TAGS(
CMD_TAG_DISCOVERY,
CMD_TAG_CHAT,
CMD_TAG_GROUPCHAT)
CMD_SYN(
"/caps",
"/caps <fulljid>|<nick>")
CMD_DESC("...")
CMD_ARGS(
{ "<fulljid>", "..." },
{ "<nick>", "..." })
CMD_EXAMPLES(
"/caps user@host/res")
}
```
| Macro | Role |
|---|---|
| `CMD_PREAMBLE(name, parse_fn, min, max, pre_hook)` | Command name, parser, arg count bounds, optional pre-hook. |
| `CMD_MAINFUNC(fn)` | Single handler for the whole command. |
| `CMD_SUBFUNCS({"sub", fn}, ...)` | Dispatch by first argument. Use **instead of** `CMD_MAINFUNC` when the command has subcommands (e.g. `/status get`, `/status set`). |
| `CMD_TAGS(...)` | One or more `CMD_TAG_*` (UI grouping, search). |
| `CMD_SYN(...)` | Synopsis lines for `/help <cmd>`. |
| `CMD_DESC(...)` | Long description. |
| `CMD_ARGS({"name", "desc"}, ...)` | Argument table for help. |
| `CMD_EXAMPLES(...)` | Examples shown in help. |
The full set of `CMD_*` macro definitions is at the top of `cmd_defs.c`
read them when authoring a new command.
## Argument parsing
`parse_args()` lives in `src/tools/parser.c`:
```c
gchar** parse_args(const char* const inp, int min, int max, gboolean* result);
```
- Splits on whitespace, honours quoted segments.
- Returns `NULL` on parse failure (sets `*result = FALSE`).
- The handler receives the result as `gchar** args` — free is owned by the
caller in `cmd_defs.c`, the handler does **not** free.
Parser variants used in `CMD_PREAMBLE`:
- `parse_args` — standard quoted-aware split.
- `parse_args_with_freetext` — last argument is the rest of the line, no
splitting (used by `/msg`, `/me`, etc.).
- `parse_args_as_one` — concatenates remaining tokens into a single string.
## Handler signature
```c
gboolean cmd_foo(ProfWin* window, const char* const command, gchar** args);
```
Return `TRUE` on success, `FALSE` to print bad-usage. `command` is the literal
`/foo` typed (useful when the same handler is registered under multiple names).
## Connection-state gates
Most commands that touch XMPP must verify connection state first:
```c
if (connection_get_status() != JABBER_CONNECTED) {
cons_show("You are not currently connected.");
return TRUE;
}
```
`cons_bad_cmd_usage(cmd)` is the canonical "wrong arguments" path; pass the
literal command name.
## Adding a new command
See `playbooks/add-command.md` for the full walkthrough. Short version:
1. New entry in `cmd_defs.c` with `CMD_PREAMBLE`/`CMD_MAINFUNC`/...
2. Implement `cmd_<name>()` in `cmd_funcs.c` (declare in `cmd_funcs.h`).
3. (If autocompletion needed) implement `_<name>_autocomplete()` in
`cmd_ac.c` and register it via `g_hash_table_insert(ac_funcs, "/name", ...)`.
4. Add a unit test pair `tests/unittests/test_cmd_<name>.{c,h}` and register
tests in `tests/unittests/unittests.c`.
## Conventions
- `cmd_<name>` for handlers; `_<name>_autocomplete` (file-static) for the
autocomplete callback.
- Subcommand dispatch via `CMD_SUBFUNCS` is preferred over `if/else` chains
inside `cmd_<name>` when possible.
- Keep handlers in `cmd_funcs.c` thin: parse & validate args, then delegate
to a domain module (`xmpp/`, `config/`, `ui/`).

71
patterns/encryption.md Normal file
View File

@@ -0,0 +1,71 @@
# Encryption
Three independent stacks, all optional at build time (`./configure` flags):
| Module | Backend | XMPP bridge |
|---|---|---|
| `src/omemo/` | libsignal-protocol-c | `src/xmpp/omemo.c` |
| `src/otr/` | libotr v4 | (in-stream, no separate xmpp bridge) |
| `src/pgp/` | gpgme | `src/xmpp/ox.c` (for OX/XEP-0373) |
## Module layouts
### OMEMO (`src/omemo/`)
| File | Role |
|---|---|
| `omemo.c/h` | Public surface: session setup, encrypt/decrypt, key publish/fetch. |
| `crypto.c/h` | AES-GCM payload encryption; libsignal-protocol crypto provider. |
| `store.c/h` | Persistent identity / pre-key / signed pre-key / session storage. |
`src/xmpp/omemo.c` is the bridge — it talks to `src/omemo/omemo.c` and to
libstrophe stanza building.
### OTR (`src/otr/`)
| File | Role |
|---|---|
| `otr.c/h` | Public surface: start/end/secure messaging, fingerprint mgmt. |
| `otrlib.h` | Compatibility shim across libotr versions. |
| `otrlibv4.c` | libotr v4-specific implementation. |
OTR runs in-band on the message stream, no separate XMPP bridge file.
### PGP / OX (`src/pgp/`)
| File | Role |
|---|---|
| `gpg.c/h` | Classic PGP signing/encrypting via gpgme. |
| `ox.c/h` | XEP-0373 / XEP-0374 (OX): OpenPGP for XMPP. |
`src/xmpp/ox.c` is the OX bridge; `gpg.c` is invoked directly from message
handlers.
## Conditional compilation
Each module is gated by `HAVE_OMEMO`, `HAVE_LIBOTR`, `HAVE_LIBGPGME` (set by
`configure.ac`). Code that calls into a module guards with `#ifdef
HAVE_<X>` and supplies a graceful fallback. When editing:
- Search for the existing `#ifdef` use of the symbol you are touching to
confirm the gating macro.
- Stubs in `tests/unittests/{omemo,otr,pgp}/stub_*.c` exist regardless — they
satisfy the test linker even when the feature is "off" at runtime.
## Encryption-aware tests
`tests/unittests/test_forced_encryption.{c,h}` exercises the policy where
sending plain text is refused if a session is OMEMO/OTR/PGP-locked. Touching
encryption-policy code → check this test does not regress.
## Adding or modifying calls into encryption
1. Locate the bridge: outgoing call → typically `src/xmpp/omemo.c` /
`src/xmpp/ox.c` / `src/otr/otr.c`.
2. Confirm the `#ifdef HAVE_*` guard at the call site.
3. If the module's public surface (header) changes, update the corresponding
stub in `tests/unittests/{omemo,otr,pgp}/stub_*.c`.
4. Run the relevant unit tests (`test_forced_encryption`, plus anything in
`test_cmd_otr.c`, `test_cmd_pgp.c`).
See `playbooks/add-encryption.md`.

59
patterns/events.md Normal file
View File

@@ -0,0 +1,59 @@
# Events
`src/event/` mediates between the XMPP protocol layer and consumers (UI,
config, chatlog). Two main directions plus one common module.
## Files
| File | Purpose |
|---|---|
| `server_events.c/h` | `sv_ev_*` — events triggered by inbound stanzas. |
| `client_events.c/h` | `cl_ev_*` — events triggered locally (UI / commands). |
| `common.c/h` | Shared helpers used by both. |
## Server events (`sv_ev_*`)
Called from `src/xmpp/` parsers (`message.c`, `presence.c`, `iq.c`, `muc.c`,
`roster.c`) once a stanza has been validated and decomposed. Each `sv_ev_*`:
1. Updates persistent state (chatlog, roster, accounts).
2. Pushes UI updates (`chatwin_*`, `mucwin_*`, `cons_*`, `console_*`).
3. May invoke plugin callbacks (`src/plugins/callbacks.c`).
Examples (current symbols, grep `sv_ev_` in `server_events.c`):
- `sv_ev_incoming_message` — chat message arrived.
- `sv_ev_room_message` — MUC message.
- `sv_ev_presence_update` — presence change.
- `sv_ev_roster_received` — roster pushed by server.
## Client events (`cl_ev_*`)
Called from `cmd_funcs.c` and UI code when the user does something locally
that needs the same downstream fan-out as a server event.
Examples:
- `cl_ev_send_msg` — outgoing chat message.
- `cl_ev_send_presence` — presence update from `/status` or autoaway.
- `cl_ev_disconnect``/disconnect` initiated locally.
## Adding an event handler
1. Decide direction: inbound (server) or outbound (client).
2. Add `sv_ev_<name>` or `cl_ev_<name>` in the matching `.c` and declare in
the matching `.h`.
3. Call it from the trigger site (a parser in `src/xmpp/` for server events;
a command handler or UI input handler for client events).
4. Inside, perform the fan-out: state update → UI → plugin callback.
5. If unit-tested, add a stub for it in `tests/unittests/event/stub_*.c`
(or extend the existing stub).
See `playbooks/add-event-handler.md`.
## Conventions
- Keep XMPP-protocol concerns in `src/xmpp/`. The event module is a
*dispatcher*, not a parser.
- Keep UI rendering decisions in `src/ui/`. The event module *invokes* UI
functions but does not draw.
- Plugin callbacks are the **last** step — state must already be consistent
before they run, since plugins may re-enter the codebase.

65
patterns/memory.md Normal file
View File

@@ -0,0 +1,65 @@
# Memory management
## `auto_*` macros
GCC/Clang `__cleanup__` attribute, declared in `src/common.h` (one exception:
`auto_jid` lives in `src/xmpp/jid.h` because it needs the `Jid` type).
| Macro | Type | Cleanup | Use for |
|---|---|---|---|
| `auto_gchar` | `gchar*` | `auto_free_gchar()` | GLib strings (`g_strdup`, `g_strdup_printf`, etc.) |
| `auto_gcharv` | `gchar**` | `auto_free_gcharv()` | GLib string arrays (`g_strsplit`, `g_strjoinv` arg) |
| `auto_char` | `char*` | `auto_free_char()` | C strings (`strdup`, `malloc`'d) |
| `auto_guchar` | `guchar*` | `auto_free_guchar()` | Unsigned char buffers (e.g. `g_base64_decode`) |
| `auto_gfd` | `gint` | `auto_close_gfd()` | File descriptors held in a `gint` |
| `auto_FILE` | `FILE*` | `auto_close_FILE()` | `FILE*` from `fopen` |
| `auto_jid` | `Jid*` | `jid_auto_destroy()` | `Jid` structs (XMPP) |
Place the macro **before** the type, like `gboolean`-style attribute:
```c
auto_gchar gchar* msg = g_strdup_printf("hello %s", name);
auto_gcharv gchar** parts = g_strsplit(line, " ", -1);
```
The cleanup runs at scope exit, including early returns. Do **not** call the
matching `g_free`/`g_strfreev`/`fclose` manually — that double-frees.
## Manual cleanup helpers
In `common.h`:
| Macro | Behaviour |
|---|---|
| `FREE_SET_NULL(ptr)` | `free(ptr); ptr = NULL;` (use for `malloc`'d) |
| `GFREE_SET_NULL(ptr)` | `g_free(ptr); ptr = NULL;` (use for GLib-allocated) |
Use these when the pointer is a struct field that must remain accessible
after the free (so a later `if (x->p)` is safe).
## GLib free-function reference
| Allocator | Free with |
|---|---|
| `g_strdup`, `g_strdup_printf`, `g_strconcat`, ... | `g_free` (or `auto_gchar`) |
| `g_strsplit`, `g_strdupv` | `g_strfreev` (or `auto_gcharv`) |
| `g_list_*` of allocated items | `g_list_free_full(list, free_fn)` |
| `g_hash_table_new[_full]` | `g_hash_table_destroy` (uses key/value destroy fns if supplied) |
| `g_base64_decode` | `g_free` (or `auto_guchar`) |
| `g_key_file_*` | `g_key_file_free` |
**Common pitfall:** mixing `g_free` and `free`. GLib uses its own allocator
shim; never cross the boundary. If you got the buffer from a GLib function,
free it with the matching GLib function.
**Common pitfall:** `g_strsplit` returns `gchar**` — free with `g_strfreev`,
not `g_free`. (See `gotchas.md`.)
## Adding a new `auto_*`
1. Declare cleanup function: `void auto_close_foo(Foo** p);`
2. Define the macro: `#define auto_foo __attribute__((__cleanup__(auto_close_foo)))`
3. Place both in the header that owns the type — `common.h` for project-wide,
the type's own header otherwise.
4. The cleanup must tolerate `NULL` and idempotent re-entry; assign `*p = NULL`
inside if you keep the variable accessible after free.

67
patterns/plugins.md Normal file
View File

@@ -0,0 +1,67 @@
# Plugins
`src/plugins/` exposes both a C plugin ABI and a Python plugin runtime. Both
funnel through a shared core that fires callbacks on cproof events.
## Files
| File | Role |
|---|---|
| `plugins.c/h` | Public surface: load/unload, list, dispatch hooks. |
| `c_plugins.c/h` | C plugin loader (dlopen). |
| `python_plugins.c/h` | Python plugin loader (embedded interpreter). |
| `c_api.c/h` | C-side API exposed **to** plugins. |
| `python_api.c/h` | Python-side API exposed **to** plugins. |
| `api.c/h` | Shared API helpers. |
| `profapi.c/h` | The `prof` Python module surface. |
| `callbacks.c/h` | Hooks: pre/post message, pre/post presence, etc. |
| `autocompleters.c/h` | Plugin-registered autocompleters. |
| `disco.c/h` | Service-discovery feature contributions from plugins. |
| `settings.c/h` | Per-plugin settings storage. |
| `themes.c/h` | Plugin-supplied themes. |
## Hook points
Plugins fire on events around (cf. `src/plugins/callbacks.c`):
- Pre/post message send, pre/post message receive.
- Pre/post chat-message-display.
- Pre/post connect, disconnect.
- Pre/post quit.
- Window-switch, room-join, room-leave.
The callback names follow `prof_pre_*` / `prof_post_*` in the plugin API
surface; in cproof internals they are dispatched via
`plugins_pre_chat_message_send()` and similar.
## API surface for plugins
C API (`c_api.h`) and Python API (`python_api.h`) are kept in sync — any
new function in one should land in the other unless explicitly C-only or
Python-only.
The Python wrapper is in `profapi.c/h` and is exposed as the `prof` module
inside plugins.
## Adding a plugin hook point
1. Define dispatcher in `plugins.c/h`:
`void plugins_<verb>_<event>(<args>);`
2. Wire callback storage in `callbacks.c/h`.
3. Expose to plugins via `c_api.c/h` and `python_api.c/h` (matching names).
4. Call the dispatcher at the appropriate site (usually in `src/event/` or
`src/xmpp/`).
5. Document the hook in cproof's plugin docs (out of scope for this repo).
## Testing
`tests/unittests/test_plugins_disco.{c,h}` covers plugin-disco feature
contribution; broader plugin-system testing is light. Stubs:
`tests/unittests/plugins/stub_*.c`.
## Pitfalls
- Plugin callbacks may re-enter cproof via the C/Python API. Ensure state is
consistent **before** firing a callback — see `patterns/events.md`.
- The Python interpreter is global; plugins share state. Don't assume
isolation.
- Plugin loading is not sandboxed; treat plugins as trusted code.

63
patterns/ui.md Normal file
View File

@@ -0,0 +1,63 @@
# UI
`src/ui/` is built on ncurses (wide-char). It owns windows, the input line,
status/title bars, and tray notifications.
## Window types
| File | Window | Notes |
|---|---|---|
| `chatwin.c` | One-on-one chat | One per peer JID. |
| `mucwin.c` | MUC room | Tracks occupants list, subject, role. |
| `privwin.c` | MUC private chat | Per-occupant private window inside a room. |
| `console.c` | Console | Always present, index 1; system messages. |
| `confwin.c` | Form / config | Used for ad-hoc commands and data forms. |
| `xmlwin.c` | XML console | Raw stanza tracer. |
| `vcardwin.c` | vCard view | Read-only viewer. |
`win_types.h` declares the window-type enum used to discriminate at runtime.
`window.c/h` is the base type; `window_list.c/h` manages the open-windows
collection.
## Buffers
`buffer.c/h` — append-only ring of rendered lines per window. UI rendering
reads from the buffer; chatlog persists separately.
## Bars and chrome
| File | Role |
|---|---|
| `statusbar.c/h` | Bottom status line (window list, unread counts). |
| `titlebar.c/h` | Top bar (current window title, encryption state). |
| `tray.c/h` | System-tray notifications via libnotify. |
| `screen.c/h` | Screen-level helpers (resize, refresh). |
| `notifier.c` | Sound and visual notifications dispatch. |
| `inputwin.c/h` | Input line: line editor, history, completion trigger. |
| `core.c` | UI lifecycle (init / shutdown / refresh loop). |
## Roster panel
`rosterwin.c` and `occupantswin.c` render the side panels (roster and MUC
occupants). They subscribe to roster / muc state changes via the event layer.
## Input → completion
`inputwin.c` calls into `cmd_ac.c` on Tab. See `patterns/autocomplete.md` for
how that flow continues.
## Conventions
- Public UI calls used from outside `src/ui/`:
- `cons_show("...")` — formatted line into the console.
- `cons_bad_cmd_usage(cmd)` — print canonical "wrong usage" line.
- `chatwin_*`, `mucwin_*`, `privwin_*` — write into a specific window type.
- UI code does not block on XMPP. State queries go through getters in
`src/xmpp/` and `src/config/`.
- Color / theme lookups go through `src/config/color.c` and
`src/config/theme.c` — do not hardcode ncurses attribute pairs.
## Testing
UI is heavily stubbed in unit tests — see `tests/unittests/ui/stub_ui.c` and
sibling stubs. Real ncurses calls do not run under `make check`.

95
patterns/xmpp.md Normal file
View File

@@ -0,0 +1,95 @@
# XMPP layer
`src/xmpp/` wraps libstrophe and exposes higher-level operations to the
command, event, and UI layers.
## Connection lifecycle
`src/xmpp/connection.c` owns the connection state machine. Public surface in
`connection.h`:
- `jabber_conn_status_t connection_get_status(void);`
- `jabber_conn_status_t connection_connect(const char* fulljid, const char* passwd, ...);`
- `jabber_conn_status_t connection_register(...);`
- `void connection_disconnect(void);`
`jabber_conn_status_t` values:
| Value | Meaning |
|---|---|
| `JABBER_DISCONNECTED` | Not connected. |
| `JABBER_CONNECTING` | Connect in progress. |
| `JABBER_CONNECTED` | Live session. |
| `JABBER_DISCONNECTING` | Tearing down. |
**Most code paths gate on `JABBER_CONNECTED`.** A canonical command-handler
guard:
```c
if (connection_get_status() != JABBER_CONNECTED) {
cons_show("You are not currently connected.");
return TRUE;
}
```
In tests, `will_return(connection_get_status, JABBER_CONNECTED)` (or
`JABBER_DISCONNECTED`) before the call. See `gotchas.md`.
## Stanza building
`src/xmpp/stanza.c` is the assembler. Helpers like
`stanza_create_message(...)`, `stanza_create_presence(...)`, plus low-level
`xmpp_stanza_*` from libstrophe.
Outgoing flow:
- `cmd_funcs.c` or `client_events.c``xmpp/message.c` (or `presence.c`,
`iq.c`) → `stanza.c` to build → libstrophe to send.
Incoming flow:
- libstrophe handler → `xmpp/message.c` (or sibling) parses → `event/server_events.c`
fans out → `ui/`, `chatlog.c`, plugin callbacks.
## JIDs
`src/xmpp/jid.h` defines `Jid` and the `auto_jid` cleanup macro. Use
`auto_jid` rather than manual `jid_destroy()` whenever practical:
```c
auto_jid Jid* parsed = jid_create(input);
if (!parsed || !parsed->barejid) {
return FALSE;
}
```
`Jid` exposes `barejid`, `fulljid`, `localpart`, `domainpart`, `resourcepart`.
## Sessions, MUC, presence, roster
| File | Responsibility |
|---|---|
| `session.c` | Logical XMPP session: account, fulljid, presence, settings. |
| `muc.c` | Multi-user chat rooms (joins, occupants, configuration). |
| `presence.c` | Presence stanzas, subscription requests. |
| `roster.c` / `roster_list.c` | Roster fetch, push, contact lookup. |
| `chat_session.c` | Per-conversation state (carbons, OTR channel, etc.). |
| `chat_state.c` | Composing / paused / inactive notifications (XEP-0085). |
| `bookmark.c` | Bookmarked rooms (XEP-0048). |
| `blocking.c` | Block list (XEP-0191). |
| `vcard.c` / `vcard_funcs.h` | vCard fetch / publish. |
| `avatar.c` | Avatar publish / fetch (XEP-0084). |
| `iq.c` | Generic IQ handling and pending-IQ map. |
| `capabilities.c` | Entity capabilities cache (XEP-0115). |
## Encryption bridges
`src/xmpp/omemo.c` and `src/xmpp/ox.c` are *bridges* into the encryption
modules — they live alongside same-named files in `src/omemo/` and
`src/pgp/`. Be precise about which path you mean.
## Testing
When unit-testing code that calls into `xmpp/`, expect to:
- Mock `connection_get_status` with `will_return(...)`.
- Stub session getters (`session_get_account_name`) and roster lookups.
- Use stubs in `tests/unittests/xmpp/stub_*.c`. See `testing/stubs.md`.

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.

View File

@@ -1,304 +0,0 @@
---
name: command-system
description: Use when writing, modifying, or debugging CProof command functions (cmd_funcs.c), command definitions (cmd_defs.c), or autocomplete (cmd_ac.c). Documents the command dispatch architecture, critical argument indexing rules (args[0] contains subcommand name), and common patterns.
---
# CProof Command System
## When to Use This Skill
Use this skill when:
- Writing new command handler functions in `cmd_funcs.c`
- Adding/modifying command definitions in `cmd_defs.c`
- Implementing autocomplete for commands in `cmd_ac.c`
- Debugging command argument parsing issues
## Core Architecture
### Command Structure ([`Command`](src/command/cmd_defs.h:64))
```c
typedef struct cmd_t {
gchar* cmd; // e.g., "/ai"
gchar** (*parser)(const char*, int, int, gboolean*); // parse_args, parse_args_with_freetext, parse_args_as_one
int min_args; int max_args;
void (*setting_func)(void); // Shows current settings in console (or NULL)
struct { const char* cmd; gboolean (*func)(ProfWin*, const char*, gchar**); } sub_funcs[50];
gboolean (*func)(ProfWin*, const char*, gchar**); // Main handler (or NULL)
CommandHelp help; // Tags, synopsis, desc, args, examples
} Command;
```
### Help Structure ([`CommandHelp`](src/command/cmd_funcs.h:43))
```c
typedef struct cmd_help_t {
gchar* tags[20]; // Command categories (NULL-terminated)
gchar* synopsis[50]; // Usage examples (NULL-terminated)
gchar* desc; // Description text
gchar* args[128][2]; // Argument names and descriptions ({NULL,NULL}-terminated)
gchar* examples[20]; // Example commands (NULL-terminated)
} CommandHelp;
```
### Available Tags ([`cmd_defs.c:82-89`](src/command/cmd_defs.c:82))
| Tag | Description |
|-----|-------------|
| `CMD_TAG_CHAT` | Chat commands |
| `CMD_TAG_GROUPCHAT` | Group chat commands |
| `CMD_TAG_ROSTER` | Roster management |
| `CMD_TAG_PRESENCE` | Presence commands |
| `CMD_TAG_CONNECTION` | Connection management |
| `CMD_TAG_DISCOVERY` | Discovery commands |
| `CMD_TAG_UI` | UI display commands |
| `CMD_TAG_PLUGINS` | Plugin commands |
### Dispatch Flow ([`_cmd_execute()`](src/command/cmd_funcs.c:8261))
```
User: /ai set provider openai https://api.openai.com
|
v
parse_args("set provider openai https://...", 0, 5) → {"set","provider","openai","https://...",NULL}
|
v
args[0]="set" matches cmd->sub_funcs[0].cmd="set"
|
v
cmd_ai_set(window, "/ai", args) ← args[0] is "set", NOT "provider"!
```
**CRITICAL: Subcommand functions receive the FULL args array. args[0] contains the subcommand name itself. All parameter accesses must be shifted +1.**
## Argument Indexing Reference
| User Input | args[0] | args[1] | args[2] | args[3] |
|------------|---------|---------|---------|---------|
| `/ai` | NULL | - | - | - |
| `/ai set provider openai url` | `"set"` | `"provider"` | `"openai"` | `"url"` |
| `/ai set token openai sk-xxx` | `"set"` | `"token"` | `"openai"` | `"sk-xxx"` |
| `/ai start openai/gpt-4o` | `"start"` | `"openai/gpt-4o"` | - | - |
| `/ai remove provider foo` | `"remove"` | `"provider"` | `"foo"` | - |
| `/tls trust` | `"trust"` | - | - | - |
| `/connect account server foo` | `"account"` | `"server"` | `"foo"` | - |
## Correct Subcommand Function Pattern
```c
gboolean cmd_ai_set(ProfWin* window, const char* const command, gchar** args)
{
// args[0] = "set" (subcommand name)
// args[1] = next subcommand (provider/token/org)
if (args[1] == NULL) { cons_bad_cmd_usage(command); return TRUE; }
if (g_strcmp0(args[1], "provider") == 0) {
// args[2] = name, args[3] = url
if (g_strv_length(args) < 4) { cons_bad_cmd_usage(command); return TRUE; }
ai_add_provider(args[2], args[3], NULL);
cons_show("Provider '%s' configured with URL: %s", args[2], args[3]);
} else if (g_strcmp0(args[1], "token") == 0) {
// args[2] = provider, args[3] = token
if (g_strv_length(args) < 4) { cons_bad_cmd_usage(command); return TRUE; }
ai_set_provider_key(args[2], args[3]);
} else if (g_strcmp0(args[1], "org") == 0) {
// args[2] = provider, args[3] = org_id
if (g_strv_length(args) < 4) { cons_bad_cmd_usage(command); return TRUE; }
AIProvider* p = ai_get_provider(args[2]);
if (p) { g_free(p->org_id); p->org_id = g_strdup(args[3]); }
} else {
cons_bad_cmd_usage(command); return TRUE;
}
cons_show("");
return TRUE;
}
```
### Common Bug (WRONG)
```c
// BUG: args[0] is "set", not "provider"
if (g_strcmp0(args[0], "provider") == 0) { ... }
// BUG: args[1] is "provider", not the name
ai_add_provider(args[1], args[2], NULL);
```
## Command Definition Pattern ([`cmd_defs.c`](src/command/cmd_defs.c))
### Macros ([`cmd_defs.c:91-98`](src/command/cmd_defs.c:91))
```c
#define CMD_PREAMBLE(c, p, min, max, set) .cmd = c, .parser = p, .min_args = min, .max_args = max, .setting_func = set,
#define CMD_MAINFUNC(f) .func = f,
#define CMD_SUBFUNCS(...) .sub_funcs = { __VA_ARGS__, { NULL, NULL } },
#define CMD_TAGS(...) .help.tags = { __VA_ARGS__, NULL },
#define CMD_SYN(...) .help.synopsis = { __VA_ARGS__, NULL },
#define CMD_DESC(d) .help.desc = d,
#define CMD_ARGS(...) .help.args = { __VA_ARGS__, { NULL, NULL } },
#define CMD_EXAMPLES(...) .help.examples = { __VA_ARGS__, NULL }
```
### Example Definition
```c
{ CMD_PREAMBLE("/ai", parse_args, 0, 5, NULL)
CMD_SUBFUNCS({ "set", cmd_ai_set }, { "remove", cmd_ai_remove },
{ "start", cmd_ai_start }, { "clear", cmd_ai_clear },
{ "correct", cmd_ai_correct }, { "providers", cmd_ai_providers })
CMD_MAINFUNC(cmd_ai)
CMD_TAGS(CMD_TAG_CHAT)
CMD_SYN("/ai", "/ai set provider <name> <url>", "/ai set token <provider> <token>")
CMD_DESC("Interact with AI models via OpenAI-compatible APIs.")
CMD_ARGS({ "", "Display current AI settings" },
{ "set provider <name> <url>", "Add or update a provider" })
CMD_EXAMPLES("/ai", "/ai set token openai sk-xxx", "/ai start openai/gpt-4o")
},
```
## Autocomplete Pattern ([`cmd_ac.c`](src/command/cmd_ac.c))
### Initialization
```c
// In cmd_ac_init():
autocomplete_add(ai_subcommands_ac, "set");
autocomplete_add(ai_subcommands_ac, "remove");
autocomplete_add(ai_set_subcommands_ac, "provider");
autocomplete_add(ai_set_subcommands_ac, "token");
autocomplete_add(ai_set_subcommands_ac, "org");
g_hash_table_insert(ac_funcs, "/ai", _ai_autocomplete);
```
### Callback
```c
static char* _ai_autocomplete(ProfWin* window, const char* const input, gboolean previous)
{
char* result = NULL;
result = autocomplete_param_with_ac(input, "/ai", ai_subcommands_ac, TRUE, previous);
if (result) return result;
result = autocomplete_param_with_ac(input, "/ai set", ai_set_subcommands_ac, TRUE, previous);
if (result) return result;
// Dynamic: provider names from ai_list_providers()
return NULL;
}
```
## Common Patterns
### Window Type Access
```c
ProfAiWin* aiwin = (window && window->type == WIN_AI) ? (ProfAiWin*)window : wins_get_ai();
// Or switch:
switch (window->type) {
case WIN_CHAT: { ProfChatWin* w = (ProfChatWin*)window; /* w->barejid */ break; }
case WIN_MUC: { ProfMucWin* w = (ProfMucWin*)window; /* w->roomjid */ break; }
}
```
### Connection Check
```c
if (connection_get_status() != JABBER_CONNECTED) {
cons_show("You are not currently connected.");
return TRUE;
}
```
### Options Parsing
```c
gchar* opt_keys[] = { "server", "port", "tls", NULL };
GHashTable* opts = parse_options(&args[args[0] ? 1 : 0], opt_keys, &parsed);
// &args[args[0] ? 1 : 0] skips args[0] if present
```
### Memory Management
```c
auto_gcharv gchar** args = cmd->parser(inp, min, max, &result); // Auto-freed
auto_gchar gchar* path = prefs_get_string(PREF_SOME_PREF); // Auto-freed
```
## Argument Parsers ([`parser.c`](src/tools/parser.c))
| Function | Description |
|----------|-------------|
| `parse_args()` | Standard argument parsing, splits on spaces |
| `parse_args_with_freetext()` | Last argument captures everything after `max` tokens |
| `parse_args_as_one()` | Everything after first space becomes single argument |
## Console Output Functions ([`ui.h`](src/ui/ui.h))
| Function | Description |
|----------|-------------|
| `cons_show(const char* msg, ...)` | Print formatted message to console |
| `cons_show_error(const char* cmd, ...)` | Print error message |
| `cons_bad_cmd_usage(const char* cmd)` | Print usage error |
## Adding a New Command: Step-by-Step
### Step 1: Declare Function in `cmd_funcs.h`
```c
gboolean cmd_mycommand(ProfWin* window, const char* const command, gchar** args);
```
### Step 2: Implement Function in `cmd_funcs.c`
```c
gboolean
cmd_mycommand(ProfWin* window, const char* const command, gchar** args)
{
// args[0] = first argument (or NULL if none)
if (args[0] == NULL) {
cons_show("No argument provided.");
return TRUE;
}
cons_show("Got: %s", args[0]);
return TRUE;
}
```
### Step 3: Add Definition to `command_defs[]` in `cmd_defs.c`
Find the end of the array (look for `};` after the last entry) and add:
```c
{ CMD_PREAMBLE("/mycommand",
parse_args, 0, 1, NULL)
CMD_MAINFUNC(cmd_mycommand)
CMD_TAGS(CMD_TAG_CHAT)
CMD_SYN("/mycommand [<arg>]")
CMD_DESC("My new command description.")
CMD_ARGS({ "<arg>", "An argument" })
CMD_EXAMPLES("/mycommand hello")
},
```
### Step 4: Add Autocomplete
In `cmd_ac.c`:
1. Add static function declaration at top:
```c
static char* _mycommand_autocomplete(ProfWin* window, const char* const input, gboolean previous);
```
2. Register in `cmd_ac_init()`:
```c
g_hash_table_insert(ac_funcs, "/mycommand", _mycommand_autocomplete);
```
3. Implement the function:
```c
static char*
_mycommand_autocomplete(ProfWin* window, const char* const input, gboolean previous)
{
return NULL; // No autocomplete
}
```
## Key Files
- [`src/command/cmd_defs.c`](src/command/cmd_defs.c) - Command definitions
- [`src/command/cmd_defs.h`](src/command/cmd_defs.h) - Type definitions
- [`src/command/cmd_funcs.c`](src/command/cmd_funcs.c) - Handler implementations
- [`src/command/cmd_funcs.h`](src/command/cmd_funcs.h) - Function declarations
- [`src/command/cmd_ac.c`](src/command/cmd_ac.c) - Autocomplete
- [`src/tools/parser.c`](src/tools/parser.c) - Argument parsing
- [`src/ui/ui.h`](src/ui/ui.h) - Console output functions

View File

@@ -1,129 +0,0 @@
---
name: cproof-structure
description: Always use this skill before editing CProof program. It contains file structure and expected code style.
---
# Cproof Structure
## CProof Project Structure (for skill context)
**CProof** is a terminal-based XMPP (chat) client written in C. It uses autotools for building and cmocka for unit testing. CProof is a fork of Profanity, renaming is WIP.
### Build System
- `configure.ac` / `Makefile.am` - Autotools build configuration
- `autogen.sh` - Script to generate configure scripts
- `configure-debug` - Debug build helper
- `tests/prof_cmocka.h` - Test framework header (cmocka wrapper)
### Source Directory (`src/`)
| Directory | Purpose |
|-----------|---------|
| `src/ai/` | AI client implementation (`ai_client.c/h`) - OpenAI/Perplexity provider support |
| `src/command/` | Command handling (`cmd_ac.c/h` - autocomplete, `cmd_defs.c/h` - command definitions, `cmd_funcs.c/h` - command implementations) |
| `src/config/` | Configuration management (accounts, preferences, themes, colors, CA certs, scripts) |
| `src/event/` | Event handling (client events, server events, common) |
| `src/omemo/` | OMEMO encryption (crypto, store) |
| `src/otr/` | OTR encryption (`otr.c/h`, `otrlib.c/h`, `otrlibv4.c`) |
| `src/pgp/` | PGP/OpenPGP integration (`gpg.c/h`, `ox.c/h`) |
| `src/plugins/` | Plugin system (C API, Python API, autocompleters, callbacks, disco, settings, themes) |
| `src/tools/` | Utility functions (autocomplete, parser, clipboard, editor, HTTP download/upload, bookmark ignore) |
| `src/ui/` | User interface (buffers, windows, input, chat/muc/console windows, statusbar, tray, window management) |
| `src/xmpp/` | XMPP protocol layer (connection, roster, muc, presence, message, stanza, vcard, session, JID, capabilities, OMEMO, Ox, blocking, bookmark, chat state, avatar) |
**Key files at `src/` root:**
- `main.c` - Entry point
- `profanity.c` - Application initialization/shutdown
- `common.h` - Common types/macros (including `auto_gchar` for memory management)
- `database.c` - Database layer
- `chatlog.c` - Chat log handling
- `log.c` - Logging
### Tests Directory (`tests/`)
| Directory | Purpose |
|-----------|---------|
| `tests/functionaltests/` | Integration tests (requires running CProof instance) |
| `tests/unittests/` | Unit tests (cmocka-based, run via `make check`) |
**Unit test structure:**
- `tests/unittests/unittests.c` - Test runner (registers all tests)
- `tests/unittests/test_*.c/h` - Test files paired with headers
- `tests/unittests/helpers.c/h` - Test utilities
- `tests/unittests/<module>/stub_*.c` - Stub implementations for dependencies
**Stub directories:**
- `unittests/ai/stub_ai_client.c` - AI client stubs
- `unittests/config/stub_accounts.c`, `stub_cafile.c`
- `unittests/database/stub_database.c`
- `unittests/log/stub_log.c`
- `unittests/omemo/stub_omemo.c`
- `unittests/otr/stub_otr.c`
- `unittests/pgp/stub_gpg.c`, `stub_ox.c`
- `unittests/tools/stub_*.c`
- `unittests/ui/stub_ai.c`, `stub_ui.c`, `stub_vcardwin.c`
- `unittests/xmpp/stub_*.c`
### Key Patterns
**Memory Management:**
- Uses `auto_gchar` macro (from `common.h`) for automatic GString/GString cleanup
- `g_free()`, `g_list_free_full()`, `g_hash_table_destroy()` for manual cleanup
- `FREE_SET_NULL()` / `GFREE_SET_NULL()` macros
**Autocomplete System:**
- `Autocomplete` type (GList-based) defined in `tools/autocomplete.h`
- Functions: `autocomplete_new()`, `autocomplete_add()`, `autocomplete_complete()`, `autocomplete_free()`
- `autocomplete_param_with_ac()` - Complete using an Autocomplete object
- `autocomplete_param_with_func()` - Complete using a callback function
- `_autocomplete_param_common()` - Internal helper
**Command System:**
- Commands defined in `cmd_defs.c` with `CMD_MAINFUNC()` and `CMD_TAGS()` macros
- Autocomplete registered in `cmd_ac.c` via `g_hash_table_insert(ac_funcs, "/cmd", _cmd_autocomplete)`
- `parse_args()` from `tools/parser.c` splits input into args array
**Testing Patterns:**
- Use `will_return(func, value)` before calling `func()` to mock return values
- Use `expect_string()` for struct field verification
- Setup/teardown via `cmocka_unit_test_setup_teardown(test_func, setup, teardown)`, centralized in unittests.c
- Mock `connection_get_status()` with `will_return(connection_get_status, JABBER_CONNECTED)` if connection is needed
### Current Work Context
The `/ai` command autocomplete was recently added with:
- `ai_subcommands_ac` - Top-level subcommands: set, remove, start, clear, correct, providers
- `ai_set_subcommands_ac` - Set subcommands: provider, token, org
- `ai_remove_subcommands_ac` - Remove subcommands: provider
- `ai_providers_find()` - Provider name autocomplete callback (stateless, no `_last_provider_match`)
- Tests in `tests/unittests/test_ai_client.c` and `tests/unittests/test_ai_client.h`
## Full List of `auto_` Macros (Automatic Memory Management)
These macros use GCC's `__cleanup__` attribute to automatically free/close resources when they go out of scope.
| Macro | Type | Cleanup Function | Description |
|-------|------|-----------------|-------------|
| `auto_gchar` | `gchar*` | `auto_free_gchar()` | Automatically frees a GLib character string |
| `auto_gcharv` | `gchar**` | `auto_free_gcharv()` | Automatically frees a GLib character string array (e.g., from `g_strsplit`) |
| `auto_char` | `char*` | `auto_free_char()` | Automatically frees a standard C string (from `strdup`) |
| `auto_guchar` | `guchar*` | `auto_free_guchar()` | Automatically frees a GLib unsigned char string (e.g., base64 decoded) |
| `auto_gfd` | `gint*` | `auto_close_gfd()` | Automatically closes a GFileDescriptor |
| `auto_FILE` | `FILE*` | `auto_close_FILE()` | Automatically closes a FILE stream |
| `auto_jid` | `Jid*` | `jid_auto_destroy()` | Automatically frees a Jid struct (defined only in `src/xmpp/jid.h`) |
**Example usage:**
```c
auto_gchar gchar* myString = g_strdup("Hello, world!");
// myString is automatically freed when scope exits
```
## License declaration
License declaration must be concise, such as one below. Or, even better, not present in new files: commentary about file details is much more important.
```
* Copyright (C) 2026 CProof Developers
*
* SPDX-License-Identifier: GPL-3.0-or-later WITH OpenSSL-exception
```

View File

@@ -1,414 +0,0 @@
---
name: writing-prs-issues-commits
description: Guidelines for writing effective PR descriptions, GitHub issues, and git commit messages following the inverted pyramid style. Use only when you work on them.
---
# Writing Effective PRs, Issues, and Git Commit Messages
## Core Principle: Inverted Pyramid
All three artifacts (PRs, issues, commit messages) should follow the **inverted pyramid** style: the most important information comes first, with progressively finer details below.
> When scrolling through a commit history or PR list, readers need to quickly determine relevance. The "what" and "why" must be immediately visible.
---
## 1. Git Commit Messages
### Structure
```
<type>: <short summary (50 chars max)>
<blank line>
<one paragraph describing the change and its motivation>
<optional: implementation details, debugging process, architecture notes>
```
### Rules
1. **Subject line**: imperative mood, present tense, 50 characters or less
- Good: `feat(ai): add AI client with multi-provider support`
- Bad: `fixed the bug with the ai client that was causing issues`
2. **First paragraph**: state WHAT was changed and WHY in 2-3 sentences. No code references.
3. **Optional "How" section**: implementation details, debugging process, architecture decisions. This is "extra-credit" reading.
4. **Never bury the lead**: if the commit changes one file, the subject line should say what it does. Don't make readers scroll to find out.
### Anti-Patterns (from David Thompson's "Favorite Commit")
| Anti-Pattern | Example | Fix |
|---|---|---|
| Burying the change | 6 paragraphs before showing the diff | Put the summary first |
| Unexplained problem | "I introduced some tests..." without saying what broke | State the problem explicitly |
| Unlinked code references | "removing the .with_content() matchers" | Name the file or link to it |
### Example
```
feat(ai): add AI client with multi-provider support
Add an AI client module that integrates with OpenAI-compatible API
providers (OpenAI, Perplexity, and custom providers) to provide
AI-assisted responses within the CProof client.
The implementation includes provider management, session handling,
async HTTP request handling via libcurl, and a dedicated AI window
for displaying conversations.
Architecture:
- Async design: HTTP requests run on a separate thread to avoid
blocking the main UI loop
- Reference counting: Both AIProvider and AISession use ref counting
for safe shared ownership
- Response size limit: 10MB cap on HTTP responses to prevent OOM
```
---
## 2. GitHub Issues
### Structure
```markdown
# <type>: <issue title>
## Motivation
<Why does this matter? Who benefits?>
## Proposed Solution
<What should happen? Keep it implementation-agnostic.>
## Acceptance Criteria
- [ ] AC-1: <verifiable condition>
- [ ] AC-2: <verifiable condition>
- [ ] ...
## Open Questions
<Unresolved decisions that need input>
## Related
<Links to related issues, docs, or external resources>
```
### Rules
1. **Title**: clear, specific, action-oriented. Use conventional commit prefixes where appropriate (`feat:`, `fix:`, `docs:`, etc.)
2. **Motivation first**: explain WHY this matters before describing WHAT to build. Focus on user value, not technical details.
3. **Acceptance criteria**: verifiable, testable conditions. Use function names or command syntax where helpful, but avoid implementation specifics (no "use GHashTable" or "add this function signature").
4. **Keep it general**: describe the feature, not the implementation. The "how" is for the PR/commit, not the issue.
5. **Commands over code**: when describing user-facing behavior, use command syntax (`/ai start`) rather than function names (`ai_window_create()`).
### Anti-Patterns
| Anti-Pattern | Example | Fix |
|---|---|---|
| Implementation details in issue | "Add a GHashTable in ai_client.c" | Describe the feature: "Users can manage multiple AI providers" |
| Vague acceptance criteria | "Make it work" | "Running `/ai start` opens a dedicated AI conversation window" |
| No motivation | Jumping straight to the solution | Explain WHY this matters to users |
### Example
```markdown
# feat(ai): AI Client with Multi-Provider Support
## Motivation
CProof users want AI-assisted responses without leaving the chat client.
This feature enables users to query AI models directly from within CProof
for quick answers, message drafting, or knowledge retrieval.
### Privacy First
- No telemetry: No data is sent to CProof or any third party except
the configured AI provider
- Local-first: Any OpenAI-compatible API endpoint works, including
local servers (Ollama, LM Studio, vLLM)
- User-controlled keys: API keys are stored per-provider in the
preferences file, never hardcoded
## Proposed Feature
Add an AI client module to CProof that lets users interact with AI
providers from within the chat interface.
A user should be able to:
1. Start an AI session with `/ai start`
2. See a dedicated AI window open for the conversation
3. Send messages and receive AI responses
4. Switch between providers and models
5. Use tab-completion for provider names (`/ai s<tab>`)
6. Have providers and API keys persist across CProof sessions
## Acceptance Criteria
### Core Functionality
- [ ] AC-1: Running `/ai start` opens a dedicated AI conversation window
- [ ] AC-2: The AI window displays the user's prompt and the AI's response
- [ ] AC-3: AI responses are properly formatted, including multiline responses
- [ ] AC-4: The conversation maintains history within a session
- [ ] AC-5: Running `/ai stop` closes the AI window and ends the session
### Provider Management
- [ ] AC-6: Default providers are available out of the box
- [ ] AC-7: Users can add custom providers with `/ai provider add <name> <url>`
- [ ] AC-8: Users can list available providers with `/ai provider list`
### API Key Management
- [ ] AC-9: Users can set an API key for a provider with `/ai key <provider> <key>`
- [ ] AC-10: API keys persist across CProof sessions
- [ ] AC-11: API keys are not displayed in plain text when listed
### Autocomplete
- [ ] AC-12: `/ai s<tab>` autocompletes to available providers
- [ ] AC-13: Autocomplete works for custom providers added by the user
### Error Handling
- [ ] AC-14: If the API key is invalid, the user sees a clear error message
- [ ] AC-15: If the provider is unreachable, the user sees a clear error message
## Open Questions
1. Should we support streaming responses (displaying text as it arrives)?
2. Should there be a rate limit or timeout for AI responses?
## Related
- [OpenAI Responses API](https://platform.openai.com/docs/api-reference/responses)
- [Ollama](https://ollama.com/) (local server)
```
---
## 3. Pull Request Descriptions
### Structure
```markdown
# <type>: <PR title>
## Introduced change
<2-3 sentences: what this PR does and why it matters>
### Capabilities
<Bullet list of key features/capabilities>
## Reasoning behind the change
<Motivation, design decisions, trade-offs considered>
## Implementation details
<Architecture overview, key implementation details, code snippets if helpful>
## Testing
<How to run tests, what was tested>
## Screenshots (if UI changes)
<Before/after images>
Resolves #<issue-number>
```
### Rules
1. **"Introduced change"**: the first section should answer "what" without requiring the reader to scroll. Use bullet points for capabilities.
2. **"Reasoning behind the change"**: explain design decisions, trade-offs, and alternatives considered. This is where you justify your approach.
3. **"Implementation details"**: architecture diagrams, code snippets, data structures. This is "extra-credit" reading for reviewers who want to understand the internals.
4. **No git diff stat**: don't include "X files changed, +Y lines". That's visible in the PR UI.
5. **Commands over code**: when describing user-facing behavior, use command syntax rather than function names.
6. **Implicit sections**: the sections flow naturally — "Introduced change" covers what, "Reasoning" covers why, "Implementation details" covers how. Don't label them explicitly as "What/Why/How".
### Anti-Patterns
| Anti-Pattern | Example | Fix |
|---|---|---|
| Git diff stat | "20 files changed, +2400 insertions" | Remove — visible in PR UI |
| Implementation in "What" | "Added ai_client.c with 843 lines" | Describe the feature: "Core AI client with provider management" |
| No "Why" section | Just listing changes | Explain design decisions and trade-offs |
### Example
```markdown
# feat(ai): add AI client with multi-provider support and UI
## Introduced change
An AI client module that integrates with OpenAI-compatible API providers
to deliver AI-assisted responses within the CProof chat client. Users can
create AI sessions, send prompts, and view responses in a dedicated AI
window — all from within the terminal UI.
### Capabilities
- **Multi-provider support**: Ships with OpenAI and Perplexity as defaults;
add custom providers via `/ai provider add`
- **Per-provider API keys**: Each provider's API key is stored in the
preferences system
- **Conversation history**: Sessions maintain message history (user/assistant
turns) for context-aware responses
- **Async requests**: HTTP calls run on a background thread; responses and
errors invoke callbacks on the main UI thread
- **Model selection**: Switch between models per session (e.g., `gpt-4`, `sonar`)
- **Provider autocomplete**: Tab-completion for provider names in commands
## Reasoning behind the change
CProof is an XMPP client focused on privacy and usability. Adding AI support
gives users a way to get quick answers, message drafting assistance, or
knowledge retrieval without leaving the chat client.
The design prioritizes:
1. **Privacy**: API keys are stored per-provider in preferences, not hardcoded.
Users control which providers they use. No data is shared with us, local
OpenAI-compatible servers are also supported.
2. **Extensibility**: The provider abstraction (`AIProvider` struct with name,
URL, org_id) makes it trivial to add new providers.
3. **Non-blocking UI**: Async HTTP ensures the terminal UI remains responsive
during API calls.
4. **Safety**: Response size capped at 10MB; reference counting prevents
use-after-free on shared objects.
## Implementation details
### Architecture Overview
```
┌─────────────────────────────────────────────────────────────┐
│ UI Layer │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────────┐ │
│ │ ProfAiWin │ │ /ai command │ │ Provider autocomplete│ │
│ │ (window.c) │ │ (cmd_funcs) │ │ (cmd_ac.c) │ │
│ └──────┬───────┘ └──────┬───────┘ └────────┬──────────┘ │
│ │ │ │ │
│ ▼ ▼ │ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ AI Client (ai_client.c) │ │
│ │ ┌────────────┐ ┌────────────┐ ┌───────────┐ │ │
│ │ │ Providers │ │ Sessions │ │ curl HTTP│ │ │
│ │ │ (GHashTable)│ │(ref-counted)│ │(async) │ │ │
│ │ └────────────┘ └────────────┘ └───────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Preferences (config/preferences.c) │ │
│ │ Stores: api_keys[provider_name] │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### Key Implementation Details
**Provider Management** — Providers are stored in a `GHashTable` keyed by name.
Each provider has a reference count for safe shared ownership:
```c
typedef struct ai_provider_t {
gchar* name;
gchar* api_url;
gchar* org_id;
GList* models;
guint ref_count;
} AIProvider;
```
Default providers are registered during `ai_client_init()`:
| Provider | URL |
|---|---|
| `openai` | `https://api.openai.com/v1/responses` |
| `perplexity` | `https://api.perplexity.ai/v1/responses` |
**Session Lifecycle** — Sessions track conversation history and are reference-counted:
```c
typedef struct ai_session_t {
gchar* provider_name;
AIProvider* provider;
gchar* model;
gchar* api_key;
GList* history; // GList of AIMessage*
guint ref_count;
} AISession;
```
#### Async HTTP Request
`ai_send_prompt()` spawns a GThread that:
1. Builds a JSON body from the session's conversation history
2. Sends a POST request via libcurl with the provider's API key
3. Invokes the user's callback on the main thread with the response or error
Response size is capped at 10MB to prevent OOM conditions.
#### UI Integration
A new `ProfAiWin` window type displays AI conversations. Responses are streamed
line-by-line; errors are displayed in red.
#### Testing
Unit tests cover:
- Provider CRUD (add, remove, list, update)
- Session lifecycle (create, ref/unref, message history, clear)
- API key get/set
- JSON string escaping (special chars, backslashes, percent signs)
- Provider autocomplete (forward, backward, partial match, case sensitivity)
Resolves #110
```
---
## Quick Reference
### Conventional Commits Prefixes
| Prefix | Meaning |
|---|---|
| `feat` | New feature |
| `fix` | Bug fix |
| `docs` | Documentation changes |
| `style` | Code style changes (formatting, semicolons, etc.) |
| `refactor` | Code changes that neither fix a bug nor add a feature |
| `test` | Adding or modifying tests |
| `chore` | Build process, auxiliary tools, CI changes |
| `perf` | Performance improvements |
| `ci` | CI configuration changes |
| `build` | Build system or dependency changes |
### File Naming
- CProof (not Profanity) — this is a fork
- Use snake_case for files: `ai_client.c`, `ai_client.h`
- Use snake_case for functions: `ai_session_create()`
- Use PascalCase for types: `AIProvider`, `AISession`

49
testing/bench.md Normal file
View File

@@ -0,0 +1,49 @@
# Benchmarks
`tests/bench/` holds performance benches for storage / parser hot paths. Each
runner is a separate binary built into its own subdirectory.
## Runners
| Runner | Path | Measures |
|---|---|---|
| `bench_runner` | `tests/bench/bench_runner/` | General-purpose runner; orchestrates other benches. |
| `bench_export_import` | `tests/bench/bench_export_import/` | Database export/import throughput (SQLite + flatfile). |
| `bench_failure_modes` | `tests/bench/bench_failure_modes/` | Flatfile parser error/recovery paths. |
| `bench_long_messages` | `tests/bench/bench_long_messages/` | Flatfile parser on very long messages. |
| `gen_history` | `tests/bench/gen_history/` | Generates synthetic chat history fixtures used by other benches. |
Each runner pulls in a small set of `src/*.c` files (`common.c`,
`database.c`, `database_flatfile*.c`, etc.) — see the per-runner Makefile or
top-level `Makefile.am` for exact dependencies.
## Stubs
Benches use their own minimal stubs (typically `bench_stubs.c`,
`bench_common.c`, `bench_csv.c` per runner) — separate from
`tests/unittests/` stubs to keep the bench builds small.
## Running
Benches are not part of `make check`. Build and invoke each runner directly
once configured:
```sh
make tests/bench/bench_runner/bench_runner
./tests/bench/bench_runner/bench_runner [args]
```
(Argument shape per-runner; check `--help`.)
## Output
Runners typically emit CSV (`bench_csv.c`) for downstream tooling. Treat
output as machine-readable; do not parse the human header lines.
## When to add a bench
- A storage / parser hot path is sensitive to regression.
- A perf claim in a PR needs to be reproducible.
If it's just a one-off measurement, run it locally and don't commit. The
bench runners are for sustained, comparable measurement.

View File

@@ -0,0 +1,62 @@
# Functional tests
`tests/functionaltests/` exercises a real cproof binary under a PTY against a
real (test) XMPP server.
## Files
| File | Role |
|---|---|
| `functionaltests.c` | Runner (entry point). |
| `proftest.c/h` | Fixture: spawn cproof under PTY, drive XMPP server, assertions. |
| `test_<feature>.c` | Per-feature suites. |
Existing suites (grep for `test_*.c`):
- Connection / disconnection: `test_connect.c`, `test_disconnect.c`,
`test_autoping.c`, `test_ping.c`.
- Messaging: `test_message.c`, `test_chat_session.c`, `test_carbons.c`,
`test_receipts.c`, `test_history.c`.
- Presence and roster: `test_presence.c`, `test_roster.c`,
`test_lastactivity.c`.
- Discovery: `test_disco.c`, `test_software.c`.
- MUC: `test_muc.c`, `test_rooms.c`.
- Storage: `test_export_import.c`.
## Prerequisites
- A test XMPP server reachable from the harness (typically a local Prosody
instance configured for the tests).
- A pre-baked `.profrc` (recent perf work pre-bakes this for speed — see
cproof commit `f84ed1bf6` for context).
- PTY availability (`/dev/ptmx` on Linux).
## Running
Functional tests are not part of `make check` (which runs unit tests only).
They are built and invoked via a separate target in `tests/functionaltests/`
— inspect `tests/functionaltests/Makefile.am` for the current target name and
arguments.
Recent perf work (commit `f84ed1bf6`) added:
- Profrc pre-baking — fixture writes a known-good `.profrc` once, not per-test.
- pty-close shutdown — fixture closes the PTY to terminate cproof faster than
signalling.
- Parallel port pools — separate XMPP servers / ports per parallel suite.
## When to add functional vs unit
| Test what | Where |
|---|---|
| Pure logic, internal API, given a state. | Unit. |
| Stanza serialization / parsing round-trip with real libstrophe. | Functional. |
| Connection state machine across multiple network events. | Functional. |
| UI rendering (visible output). | **Neither** (UI stubbed in unit; not asserted in functional). |
## Failure-mode notes
- A leaked / orphan cproof process between suites poisons later runs. The
fixture aggressively kills children — but if you abort a test by hand,
`pkill -f tests/functionaltests` first.
- Port-pool exhaustion under parallelism: bump the pool size, do not lower
`-jN`.

113
testing/stubs.md Normal file
View File

@@ -0,0 +1,113 @@
# Stubs
Unit tests link against the unit-under-test plus a parallel set of "stub"
implementations of every dependency. Stubs are organised by module under
`tests/unittests/<module>/`.
## Layout
| Stub directory | Replaces |
|---|---|
| `tests/unittests/chatlog/` | `src/chatlog.c` |
| `tests/unittests/command/` | parts of `src/command/` |
| `tests/unittests/config/` | `src/config/` (accounts, cafile, ...) |
| `tests/unittests/database/` | `src/database.c` |
| `tests/unittests/event/` | `src/event/` |
| `tests/unittests/log/` | `src/log.c` |
| `tests/unittests/omemo/` | `src/omemo/` |
| `tests/unittests/otr/` | `src/otr/` |
| `tests/unittests/pgp/` | `src/pgp/` |
| `tests/unittests/plugins/` | `src/plugins/` |
| `tests/unittests/tools/` | `src/tools/` |
| `tests/unittests/ui/` | `src/ui/` |
| `tests/unittests/xmpp/` | `src/xmpp/` |
| `tests/unittests/unittests/` | top-level utility stubs |
Files are named `stub_<module>.c` (sometimes split, e.g.
`tests/unittests/config/stub_accounts.c`, `stub_cafile.c`).
## Two stub flavours
### Pass-through default
Returns a benign default (NULL, FALSE, 0). Used when the test does not care
about the call.
```c
void
chatlog_msg_in(...)
{
// no-op
}
```
### `will_return`-driven
The stub pops a value queued by the test:
```c
jabber_conn_status_t
connection_get_status(void)
{
return (jabber_conn_status_t)mock();
}
```
Test side:
```c
will_return(connection_get_status, JABBER_CONNECTED);
```
If the stub is `will_return`-driven, **every test** that exercises a code
path through it must queue a value, or cmocka aborts.
### `expect_*`-driven
The stub asserts argument values against expectations:
```c
void
cons_bad_cmd_usage(const char* const cmd)
{
check_expected(cmd);
}
```
Test side:
```c
expect_string(cons_bad_cmd_usage, cmd, CMD_ACCOUNT);
```
## When to add a stub
1. You call a new function from production code, and the call site is
reachable from a unit test.
2. `make check` link fails with an undefined-symbol error.
Procedure:
- Locate the matching stub module by callee path: `src/foo/bar.c`
`tests/unittests/foo/stub_bar.c`. Create the stub file if absent.
- Stub the new function with the right flavour:
- Pass-through if tests don't need to observe / drive it.
- `mock()` if a test needs to inject a return value.
- `check_expected()` if a test needs to assert an argument.
- Wire the new stub `.c` into the build. See `tests/unittests/Makefile.am`
(or whatever wires the stubs into the `unittests` target).
## When to extend an existing stub
If a stub already pass-through and a test now needs to drive it, **change the
stub to `mock()`**. Then every test (existing and new) reaching that stub
must `will_return` — be ready to update unrelated tests.
## Convention notes
- Stubs do **not** include real headers from `src/<module>/` if doing so
would pull in the very symbol they replace. Forward-declare instead.
- `tests/unittests/ui/stub_ai.c` is a UI-side stub for the AI client surface
consumed by `src/ui/`.
- The `tests/unittests/unittests/` directory holds top-level catch-alls that
do not fit a single module.

96
testing/unit-tests.md Normal file
View File

@@ -0,0 +1,96 @@
# Unit tests
Framework: cmocka, accessed via `tests/prof_cmocka.h` (a thin compatibility
wrapper). Run via `make check`.
## File layout
- One test file per topic: `tests/unittests/test_<topic>.c` + matching
`test_<topic>.h`.
- Header declares each test function (one per `cmocka_unit_test*`).
- Runner: `tests/unittests/unittests.c``#include`s every header, calls
`cmocka_run_group_tests()` over a single big array.
## Registration
Inside `unittests.c`, every test name appears in the array passed to
`cmocka_run_group_tests`:
```c
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_caps_show_basic),
cmocka_unit_test_setup_teardown(test_join_room, muc_before_test, muc_after_test),
// ...
};
```
Convenience macros are defined near the top, e.g.:
```c
#define muc_unit_test(f) cmocka_unit_test_setup_teardown(f, muc_before_test, muc_after_test)
```
## Test function shape
```c
void
test_account_set_jid_succeeds(void** state)
{
// Arrange
will_return(connection_get_status, JABBER_CONNECTED);
will_return(session_get_account_name, "alice@example.com");
ProfAccount* account = account_new("alice@example.com", ...);
will_return(accounts_get_account, account);
// Act
gchar* args[] = { "set", "alice@example.com", "jid", "alice@example.org", NULL };
gboolean ok = cmd_account(NULL, CMD_ACCOUNT, args);
// Assert
assert_true(ok);
}
```
## cmocka primitives in heavy use
| Primitive | When |
|---|---|
| `will_return(fn, value)` | Queue a return value for a stubbed function. Each call to the stub pops one. |
| `will_return_count(fn, value, n)` | Queue the same value `n` times. |
| `expect_string(fn, param, "...")` | Stub asserts that the named argument equals this string. |
| `expect_value(fn, param, value)` | Stub asserts equality on a non-string. |
| `expect_any(fn, param)` | Stub accepts any value (still pops the queue). |
| `assert_true / assert_false / assert_string_equal / assert_int_equal` | Assertions inside the test. |
| `cmocka_unit_test(fn)` | Plain registration. |
| `cmocka_unit_test_setup_teardown(fn, setup, teardown)` | With per-test fixture. |
`expect_*` queues are checked at test end — missing calls fail the test.
## Group setup / teardown
For sets of tests sharing fixture: define a `_setup` and `_teardown` and
register via `cmocka_unit_test_setup_teardown`. Examples in `unittests.c`:
`muc_before_test`, `muc_after_test`, `keyhandlers_setup`, `keyhandlers_teardown`.
## What gets stubbed
Tests run against the production `.c` files of the unit under test, with
stubs replacing all dependencies. See `testing/stubs.md` for the layout.
Standard idiom: every external function the unit calls must have a stub
returning either a fixed value or a `will_return`-driven one.
## Running a single test
`make check` runs everything. To narrow:
```sh
./tests/unittests/unittests --filter <test_name_pattern>
```
(Confirm via `unittests --help`; cmocka filter support depends on version.)
## See also
- `testing/stubs.md` — adding/extending stubs.
- `playbooks/add-test.md` — adding a new test file end-to-end.

84
wip/feat-ai.md Normal file
View File

@@ -0,0 +1,84 @@
# WIP: `feat/ai` — AI assistant client
**Branch:** `feat/ai` in cproof.
**Status:** in flight at the time of writing. Delete this file when the branch
merges to `master`; fold the relevant bits into the stable layer.
This file describes the delta `feat/ai` adds on top of `master`. Load it only
when working on the `feat/ai` branch.
## What the branch adds
A new module: `src/ai/`. A multi-provider AI assistant accessible via the
`/ai` command (OpenAI, Perplexity).
## New files
| Path | Role |
|---|---|
| `src/ai/ai_client.c` | Provider abstraction, request/response, provider registry. |
| `src/ai/ai_client.h` | Public surface. Notable: `char* ai_providers_find(const char*, gboolean, void*);` (stateless autocomplete callback). |
| `tests/unittests/test_ai_client.c` | Unit tests for the AI client. |
| `tests/unittests/test_ai_client.h` | Header pair for the above. |
| `tests/unittests/ai/stub_ai_client.c` | Stubs for `ai_client.*` consumers in other unit tests. |
| `tests/unittests/ui/stub_ai.c` | UI-side stub for the AI surface consumed by `src/ui/`. |
Plus added entries in:
| File | Addition |
|---|---|
| `src/command/cmd_defs.c` | `/ai` command record. |
| `src/command/cmd_funcs.{c,h}` | `cmd_ai` handler. |
| `src/command/cmd_ac.c` | `_ai_autocomplete` dispatcher; static `ai_subcommands_ac`, `ai_set_subcommands_ac`, `ai_remove_subcommands_ac`. |
| `tests/unittests/unittests.c` | Test registrations for `test_ai_*`. |
## Subcommand layout
```
/ai set {provider|token|org} <provider-name> <value>
/ai remove provider <provider-name>
/ai start <provider-name>
/ai clear <provider-name>
/ai correct <provider-name>
/ai providers
```
## Autocomplete
- `_ai_autocomplete` in `cmd_ac.c` is the per-command dispatcher.
- Top-level subcommand list: `ai_subcommands_ac``set`, `remove`, `start`,
`clear`, `correct`, `providers`.
- Set subcommands: `ai_set_subcommands_ac``provider`, `token`, `org`.
- Remove subcommands: `ai_remove_subcommands_ac``provider`.
- Provider-name completion: `ai_providers_find()` (`src/ai/ai_client.c`) — a
**stateless** callback (no `_last_match` global). On the `feat/ai`
branch, this is the most current canonical example of a stateless
autocomplete callback.
## CI deltas
`feat/ai` also brings (unrelated to AI itself):
- `0feacbc9d ci: simulate Pikaur flag duplication in Arch Linux CI` — adds a
CI step that simulates the Pikaur duplicated-flag failure mode in the
Arch Linux job. Survives independently of the AI feature.
If the AI work is merged but the CI delta is rebased separately, update
`build/ci.md` to mention this commit alongside `0722dc9e3`.
## On merge — what to fold into stable layers
When `feat/ai` lands on `master`, edit:
| File | Change |
|---|---|
| `architecture/source-map.md` | Add `src/ai/` row to the module table. |
| `architecture/overview.md` | Add AI to the optional integrations / module list; drop the "active feature branches" caveat. |
| `architecture/test-map.md` | Add `tests/unittests/ai/` stub directory; mention `test_ai_client.{c,h}`. |
| `patterns/autocomplete.md` | Optionally add `ai_providers_find` as a second canonical stateless-callback example (or leave `roster_contact_autocomplete` as the sole example). |
| `playbooks/add-autocomplete.md` | Same. |
| `gotchas.md` | Same. |
| `build/ci.md` | Mention the Pikaur CI simulation commit alongside the original Pikaur fix. |
Then **delete this file**. Update `INDEX.md` to drop the `wip/` row if no
other WIP entries remain.