kb --debug — Design
Status (2026-07-27): Decisions confirmed. See debug-logging-plan.md for the phased plan.
References: - harvey/debuglog.go — the
original nil-safe JSONL DebugLog pattern. -
github.com/caltechlibrary/clasm’s
internal/debuglog (macmini-rd.local,
~/WorkLab/clasm) — the same pattern applied to AWS SDK
calls, explicitly modeled on harvey’s per its own
DESIGN.md.
Motivation, and a correction
You asked for a --debug option that emits enough detail
to debug the TUI, “more detail since we’re using huh and bubbletea,”
citing clasm as a precedent that already worked. Worth
stating plainly since it changes the starting point:
clasm does not actually log bubbletea/huh
event-loop detail anywhere — checked internal/tui,
internal/ui, and every .go file under
internal/workflow for any logging call.
clasm’s -debug is scoped entirely to
internal/awsclient’s Wrap* decorators, which
log every AWS SDK call (method, region, params, duration, output/error)
— the “successful precedent” is the architecture (nil-safe
JSONL DebugLog, -debug flag, path announced
once at startup), not an existing example of TUI-level tracing to copy.
This design applies that same architecture to two things
clasm doesn’t cover: our own
knowledge.KnowledgeBase calls, and bubbletea’s
Update loop itself.
Confirmed in conversation: --debug applies to
both the TUI and every CLI verb (not TUI-only); the log
file is ./kb-debug-<timestamp>.jsonl in the current
directory, matching clasm’s convention exactly (not
<db-dir>/).
Decisions
New file
cmd/kb/debuglog.go, same shape asclasm’sinternal/debuglog(itself modeled onharvey/debuglog.go): a nil-safeDebugLogstruct,New(path string) (*DebugLog, error),DefaultPath() string("kb-debug-" + timestamp + ".jsonl"),(*DebugLog) Log(event string, fields map[string]any),Path(),Close(). Every method nil-safe (a nil*DebugLogis exactly what--debug=falseproduces), so noif debugconditional appears anywhere outside the one place--debugis parsed. Scoped tocmd/kb, not theknowledgepackage itself — this is a CLI/TUI concern, not something other consumers of the package need.--debugis a third global flag, alongside--db/--json, parsed inparseGlobalFlagsthe same way. Opened once inmainRun(skipped entirely forhelp/no-op paths, same as opening the database is); its path is printed to stderr once at startup, matchingclasm’s “so the operator knows where totail -fit.”verbFuncgains adl *DebugLogparameter, threaded the same wayjsonOut boolalready is — every verb handler (cmdProject,cmdProjectAdd,cmdObservation, … all ~20 handler functions across 7 files) picks up one more parameter. Rejected: a package-levelvar currentDebugLog *DebugLog— avoids touching every signature, but introduces mutable global state for something test code also needs to control per-test; an explicit parameter is more work to wire but stays consistent with howjsonOutalready flows through the same functions, and keeps every handler’s dependencies visible in its own signature.Knowledge-base calls are logged via an explicit generic helper, not an interface decorator.
clasm’sWrap*pattern works becauseEC2API/SSMAPI/etc. are already narrow, fake-able interfaces by design (for testing against fakes, unrelated to debug logging).*knowledge.KnowledgeBaseis a concrete struct with ~24 public methods — defining a parallel interface just to wrap it in a decorator would duplicate nearly the entire public API as a second abstraction, for a single first-party consumer, for one purpose (logging). Instead:// logKBCall runs call, logs one "kb_call" record to dl (method, params, // duration_ms, and either result or error), and returns call's result // unchanged. A nil dl makes this call's own overhead one nil check. func logKBCall[T any](dl *DebugLog, method string, params any, call func() (T, error)) (T, error) { start := time.Now() result, err := call() fields := map[string]any{ "method": method, "params": params, "duration_ms": time.Since(start).Milliseconds(), } if err != nil { fields["error"] = err.Error() } else { fields["result"] = result } dl.Log("kb_call", fields) return result, err }Applied at each of the ~34 real call sites across
project.go,observation.go,concept.go,link.go,source.go,search.go, andtui_model.go(merge.go’s calls are to package-level functions, not*KnowledgeBasemethods — covered separately, decision 6).The TUI additionally logs every
tea.Msgand every view-state transition — the actual “more detail… since we’re using bubbletea” ask, and the piece with no existing precedent anywhere to follow.- Every message
Updatereceives:event="tui_msg",msg_type(Go type name via%T), and fortea.KeyMsgspecifically, the key string (msg.String()) — this is the concrete, high-value case for “why didn’t my keypress do what I expected.” - Every view-state change (
viewProjects→viewObservations, etc.):event="tui_state_change",from,to. - Errors already stored in
m.erralso get an explicitevent="tui_error"record at the point they’re set, not just silently shown inView().
- Every message
mergegetsdltoo, logging its own already-distinct events (event="merge_collision",event="merge_reconciled",event="merge_summary") at the same points its existing progress text is written — not routed throughlogKBCall, sinceCollisionReport/ReconcileCollisions/MergeKnowledgeBasesare package-level functions operating on file paths, not*KnowledgeBasemethods.
Testing
Mirrors clasm’s own debuglog_test.go almost
exactly: TestLog_WritesOneJSONObjectPerLine (open a
temp-file-backed DebugLog, log two events, close, re-read
the file, assert line count and field values),
TestLog_NilReceiverIsSafe (every method on a nil
*DebugLog is a no-op). Plus:
TestLogKBCall_LogsMethodParamsAndResult,
TestLogKBCall_LogsErrorInsteadOfResult, and TUI-side tests
sending a tea.KeyMsg/state-changing key to a model
constructed with a real temp-file DebugLog, then reading
the file back to confirm the expected
tui_msg/tui_state_change records appear —
following the same “real file, real JSONL, read it back” pattern rather
than mocking Log itself.
What this does not cover
- Any change to
harvey’s own--debug/DebugLog— untouched, separate repo, separate concern. - Redacting sensitive fields (
clasm’s one exception:CreateKeyPair’s private key material never reaches its log) — nothing inknowledge.KnowledgeBase’s API returns comparably sensitive data today, so no redaction logic is being added preemptively; revisit if that changes.