summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-20 09:32:29 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-20 09:35:41 +0200
commitb6aeaa8995ae81562a4bf11937959b09027116ff (patch)
tree90da3cbdb9afa383293544e506a180bea38f9915
parent26d800749f5d4bb150c86215b0df4444f01ed2f6 (diff)
downloadbokf-b6aeaa8995ae81562a4bf11937959b09027116ff.tar.gz
bokf-b6aeaa8995ae81562a4bf11937959b09027116ff.zip
scripts: check command and error-code drift against the docs
-rw-r--r--docs/PROTOCOL.md10
-rwxr-xr-xscripts/check-consistency.sh196
2 files changed, 206 insertions, 0 deletions
diff --git a/docs/PROTOCOL.md b/docs/PROTOCOL.md
index 47f6f26..16fd2c5 100644
--- a/docs/PROTOCOL.md
+++ b/docs/PROTOCOL.md
@@ -202,6 +202,15 @@ Cursors are opaque.
## 6. Discovery
+The discovery commands themselves:
+
+| Command | Auth | Result |
+|---|---|---|
+| `health` | public | `{"status":"ok"}` |
+| `meta` | public | server version, protocol version `1`, features, limits, `tcp_enabled`, `time` |
+| `describe` | authenticated | full command catalogue; `{"cmd":"voucher.post"}` returns one entry |
+| `agent.instructions` | authenticated | Markdown workflow rules (see 6.3) |
+
### 6.1 `meta` / `health` (unauthenticated)
`meta` returns server version, protocol version `1`, enabled features
@@ -256,6 +265,7 @@ Arguments are shown abbreviated; `describe` is authoritative.
| `session.open` | `method`, `username`, `password`, `token` | `session`, `user`, `orgs[]`, `active_org` |
| `session.close` | — | `{}` |
| `session.whoami` | — | `user`, `active_org`, `role`, `scopes` |
+| `session.list_orgs` | — | `items[{id,name,role}]` |
| `session.use_org` | `org` | `active_org`, `role` |
| `org.create` | `name`, `org_nr?`, `fiscal_year_start_month?`, `moms_period?`, `framework?` | `org` |
| `org.get` / `org.list` | `org?` | `org` / `items[]` |
diff --git a/scripts/check-consistency.sh b/scripts/check-consistency.sh
new file mode 100755
index 0000000..f67695f
--- /dev/null
+++ b/scripts/check-consistency.sh
@@ -0,0 +1,196 @@
+#!/bin/sh
+# Check that the daemon source and docs/PROTOCOL.md have not drifted apart:
+#
+# 1. every command in g_commands[] (src/commands.c) is listed in a
+# PROTOCOL.md table whose first column is "Command", and vice versa;
+# 2. every error code passed to fail()/failf()/add_error() in src/*.c is
+# documented in PROTOCOL.md section 5.3, and vice versa (documented but
+# unused codes are warnings only, some are reserved).
+#
+# Run from the repo root: sh scripts/check-consistency.sh
+# Exits 0 when there is no drift, 1 when drift is found, 2 on parse errors.
+#
+# Environment overrides (used by the self-tests):
+# REPO_ROOT repository root, default "." (run from the repo root)
+# COMMANDS_C command table source, default $REPO_ROOT/src/commands.c
+# PROTOCOL_MD protocol document, default $REPO_ROOT/docs/PROTOCOL.md
+# SRC_GLOB shell glob of sources scanned for error codes,
+# default $REPO_ROOT/src/*.c
+#
+# Commands deliberately kept out of the PROTOCOL.md tables. Prefer documenting
+# the command in a table over adding it here. Keep sorted, space separated.
+ALLOW_UNDOCUMENTED_COMMANDS=""
+
+set -eu
+export LC_ALL=C
+
+REPO_ROOT=${REPO_ROOT:-.}
+COMMANDS_C=${COMMANDS_C:-$REPO_ROOT/src/commands.c}
+PROTOCOL_MD=${PROTOCOL_MD:-$REPO_ROOT/docs/PROTOCOL.md}
+SRC_GLOB=${SRC_GLOB:-$REPO_ROOT/src/*.c}
+
+if [ ! -f "$COMMANDS_C" ]; then
+ echo "check-consistency: command source not found: $COMMANDS_C" >&2
+ exit 2
+fi
+if [ ! -f "$PROTOCOL_MD" ]; then
+ echo "check-consistency: protocol document not found: $PROTOCOL_MD" >&2
+ exit 2
+fi
+
+tmp=$(mktemp -d "${TMPDIR:-/tmp}/bokf-consistency-XXXXXX")
+trap 'rm -rf "$tmp"' EXIT HUP INT TERM
+
+# --- extract what the code implements --------------------------------------
+
+awk '
+ /g_commands[[:space:]]*\[[[:space:]]*\][[:space:]]*=/ { in_table = 1; next }
+ in_table && /^[[:space:]]*\};/ { in_table = 0 }
+ in_table && /^[[:space:]]*\{[[:space:]]*"/ {
+ line = $0
+ sub(/^[^{]*\{[[:space:]]*"/, "", line)
+ sub(/".*/, "", line)
+ if (line != "")
+ print line
+ }
+' "$COMMANDS_C" | sort -u > "$tmp/impl_commands"
+
+if [ ! -s "$tmp/impl_commands" ]; then
+ echo "check-consistency: no commands parsed from $COMMANDS_C" >&2
+ exit 2
+fi
+
+# --- extract what the docs promise -----------------------------------------
+
+# Only rows of tables whose header first cell is "Command" count; prose,
+# argument names and other tables must not produce command names.
+awk '
+ /^\|/ {
+ n = split($0, cells, "|")
+ cell = cells[2]
+ gsub(/^[[:space:]]+/, "", cell)
+ gsub(/[[:space:]]+$/, "", cell)
+ if (cell == "Command") { in_table = 1; next }
+ if (in_table) {
+ rest = cell
+ while (match(rest, /`[a-z][a-z0-9_.]*`/)) {
+ print substr(rest, RSTART + 1, RLENGTH - 2)
+ rest = substr(rest, RSTART + RLENGTH)
+ }
+ }
+ next
+ }
+ { in_table = 0 }
+' "$PROTOCOL_MD" | sort -u > "$tmp/doc_commands"
+
+if [ ! -s "$tmp/doc_commands" ]; then
+ echo "check-consistency: no command tables parsed from $PROTOCOL_MD" >&2
+ exit 2
+fi
+
+# Error codes are the backticked ALL_CAPS words inside section 5.3.
+awk '
+ /^###[[:space:]]+5\.3/ { in_section = 1; next }
+ in_section && /^###/ { in_section = 0 }
+ in_section {
+ rest = $0
+ while (match(rest, /`[A-Z][A-Z0-9_]+`/)) {
+ print substr(rest, RSTART + 1, RLENGTH - 2)
+ rest = substr(rest, RSTART + RLENGTH)
+ }
+ }
+' "$PROTOCOL_MD" | sort -u > "$tmp/doc_codes"
+
+if [ ! -s "$tmp/doc_codes" ]; then
+ echo "check-consistency: no error codes parsed from $PROTOCOL_MD 5.3" >&2
+ exit 2
+fi
+
+# --- compare ----------------------------------------------------------------
+
+: > "$tmp/allow_commands"
+for name in $ALLOW_UNDOCUMENTED_COMMANDS; do
+ printf '%s\n' "$name" >> "$tmp/allow_commands"
+done
+sort -u -o "$tmp/allow_commands" "$tmp/allow_commands"
+cat "$tmp/doc_commands" "$tmp/allow_commands" | sort -u > "$tmp/known_commands"
+
+comm -23 "$tmp/impl_commands" "$tmp/known_commands" > "$tmp/cmd_undocumented"
+comm -13 "$tmp/impl_commands" "$tmp/doc_commands" > "$tmp/cmd_unimplemented"
+
+for f in $SRC_GLOB; do
+ [ -f "$f" ] || continue
+ grep -E '(^|[^A-Za-z0-9_])(fail|failf|add_error)[[:space:]]*\(' "$f" |
+ grep -oE '"[A-Z][A-Z0-9_]{1,}"' || true
+done | tr -d '"' | sort -u > "$tmp/used_codes"
+
+if [ ! -s "$tmp/used_codes" ]; then
+ echo "check-consistency: no error-code call sites parsed from $SRC_GLOB" >&2
+ exit 2
+fi
+
+comm -23 "$tmp/used_codes" "$tmp/doc_codes" > "$tmp/code_undocumented"
+comm -13 "$tmp/used_codes" "$tmp/doc_codes" > "$tmp/code_unused"
+
+# --- report -----------------------------------------------------------------
+
+status=0
+problems=0
+warnings=0
+
+section() {
+ echo
+ echo "== $1 =="
+}
+
+section "commands: implemented in $COMMANDS_C but not documented in $PROTOCOL_MD"
+if [ -s "$tmp/cmd_undocumented" ]; then
+ while IFS= read -r name; do
+ echo "command: undocumented: $name"
+ problems=$((problems + 1))
+ done < "$tmp/cmd_undocumented"
+ status=1
+else
+ echo "(none)"
+fi
+
+section "commands: documented in $PROTOCOL_MD but not implemented"
+if [ -s "$tmp/cmd_unimplemented" ]; then
+ while IFS= read -r name; do
+ echo "command: documented but unimplemented: $name"
+ problems=$((problems + 1))
+ done < "$tmp/cmd_unimplemented"
+ status=1
+else
+ echo "(none)"
+fi
+
+section "error codes: used in src but not documented in $PROTOCOL_MD 5.3"
+if [ -s "$tmp/code_undocumented" ]; then
+ while IFS= read -r code; do
+ echo "error-code: undocumented: $code"
+ problems=$((problems + 1))
+ done < "$tmp/code_undocumented"
+ status=1
+else
+ echo "(none)"
+fi
+
+section "warnings (not failures)"
+if [ -s "$tmp/code_unused" ]; then
+ while IFS= read -r code; do
+ echo "warning: documented but unused in src: $code"
+ warnings=$((warnings + 1))
+ done < "$tmp/code_unused"
+ echo "(documented codes may be reserved for future use)"
+else
+ echo "(none)"
+fi
+
+echo
+if [ "$status" -eq 0 ]; then
+ echo "consistency: ok ($problems problems, $warnings warnings)"
+else
+ echo "consistency: FAILED ($problems problems, $warnings warnings)"
+fi
+exit "$status"