feat: add doctor command (#151)#193
Conversation
Adds Store.TruncateEntitiesTx(ctx, tx) which deletes all rows from entities_current within a caller-supplied transaction. Called exclusively by Bus.TruncateAndReplay during projection rebuilds. Tests verify committed truncate empties the table and a rolled-back truncate restores rows.
closes #159 Add Bus.TruncateAndReplay(ctx) (int, error) that rebuilds entities_current from the full event log without writing new events. Implementation: - applyEventProjectionOnlyTx: new dispatch switch identical to applyEventTx except EntityReparentedEvent uses handleEntityReparentedProjectionOnlyTx (skips propagatePathChangesTx) and EntityPathChangedEvent is applied directly via handleEntityPathChanged — no event-log writes in either case - handleEntityReparentedProjectionOnlyTx: recomputes entity path via ComputeEntityPathTx without fetching descendants or calling propagate - writeEvent now accepts an applyFn parameter; Dispatch passes applyEventTx - TruncateAndReplay: loads all events before opening the write transaction (read-then-write), truncates entities_current via TruncateEntitiesTx, then replays each event through applyEventProjectionOnlyTx in event_id ASC order - Returns count of non-EntityPathChangedEvent events applied Also fixes pre-existing DoctorKind lint warning from #158. ADR-0009 documents the projection-only rebuild pattern. Issue #191 tracks the equivalent ReplayEvent/import fix. Integration test seeds a reparent scenario (produces EntityPathChangedEvent rows in the log), calls TruncateAndReplay, and asserts: correct returned count, correct projection state, and event log length unchanged.
Add CheckProjectionConsistency(ctx) to *App in internal/app. Performs a logical diff between the event log and entities_current projection without mutating state. Algorithm: - Derive expected-present set: entity.created minus entity.removed events - Phantom check: projection rows not in expected-present set - Missing check: expected-present entities absent from projection - Stale check: projection rows whose LastEventID != max event_id for entity Implementation is split across two unexported helpers: - buildProjectionSets: single-pass scan over all events; builds expected-present map and max event_id per entity - checkProjectionRows: set-difference checks against the full projection Also adds Store.ListAllEntities to internal/store/entities.go — same as ListEntities but includes removed rows, needed to detect phantoms. Tests cover: clean state, phantom row, missing row, stale LastEventID. All use the openTestAppWithStore helper pattern for direct store access. Closes #160
Add cmd/doctor/ following the established command pattern with a
doctorApp interface covering ValidateEventLog, CheckProjectionConsistency,
and TruncateAndReplay.
Also adds OutputWriter.Issue(kind, description) to internal/cli — renders
'[kind] description' in warning orange; JSON mode emits {kind, description}.
Always shown regardless of quiet mode.
Run order:
1. Config file validation (config.Check) + DB path resolution — aborts if
any config issue found
2. ValidateEventLog — issues printed, continues to projection check
3. CheckProjectionConsistency — issues printed
4. --rebuild: TruncateAndReplay if clean, or with --force (-f)
Exits non-zero if any issue found; prints 'OK' when all clear.
Registered in cmd/root.go after status; comment marks man as the first
hidden internal command.
GitHub issue #192 tracks aligning cmd/config check output to the same
[kind] description convention.
Closes #162
Add --json flag to cmd/doctor emitting a single JSON object:
{"healthy": bool, "issue_count": int, "issues": [...], "rebuilt": int|omitted}
- rebuilt is omitted when --rebuild was not passed or rebuild was skipped
- healthy is false and exit is non-zero when any issue is present
- event_id is null when the issue is not tied to a specific event
Refactors runDoctor to accumulate all issues before rendering, enabling
the single-document output. Extracts collectIssues and emitResult helpers
to keep cognitive complexity within limits.
Also relaxes the config-check abort rule from #162: config issues no
longer block DB checks. All checks run unless their infrastructure
prerequisite failed (DB cannot be opened). Config issues are collected
and passed into runDoctor alongside app-layer issues.
Adds jsonDoctorResult/jsonIssue test structs so JSON round-trip tests
assert integer fields as integers, not float64.
Adds ADR-0010 (doctor check abort rule), ADR-0011 (--json output shape),
and CONTEXT.md Healthy glossary entry.
Closes #163
Code reviewFound 2 issues:
wherehouse/internal/app/doctor.go Lines 136 to 146 in 3494c24
The comment at line 96 reads "Returns true if any config issues were found" — the function signature was changed to return a slice but this comment block was not updated. The duplicate comment block immediately below (lines 97–98) is the correct version. wherehouse/cmd/doctor/doctor.go Lines 95 to 99 in 3494c24 🤖 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
Closes #151
Implements the full
doctorcommand across the storage, eventbus, app, and command layers.What's included
internal/store—TruncateEntitiesTx(#157)Transactional DELETE of all
entities_currentrows. Called exclusively byTruncateAndReplayduring projection rebuilds.internal/eventbus—TruncateAndReplay(#159)Rebuilds
entities_currentfrom the full event log without writing new events. Uses a separateapplyEventProjectionOnlyTxdispatch path to avoid growing the event log on reparent replay (see ADR-0009).internal/app— doctor methods (#158, #160, #161)ValidateEventLog— checks each event for known type, unmarshalable payload, non-emptyEntityID, and validentity.createdfieldsCheckProjectionConsistency— logical diff between event log and projection: phantom rows, missing rows, stalelast_event_idTruncateAndReplay— thin delegation tobus.TruncateAndReplaycmd/doctor— command layer (#162, #163)Follows the established
NewDefaultXxxCmd/NewXxxCmd(app interface)/buildXxxCmd/runXxxpattern.Run order:
ValidateEventLogCheckProjectionConsistency--rebuild:TruncateAndReplayif clean, or with--force(-f)Flags:
--rebuild,--force/-f,--jsonKey behaviours:
OKwhen all clear--rebuildskipped when issues present unless--forceis set; exit is still non-zero when issues were found regardless of rebuild outcome--jsonemits a single document:{"healthy": bool, "issue_count": int, "issues": [...], "rebuilt": int|omitted}(ADR-0011)internal/cli—OutputWriter.IssueNew method rendering
[kind] descriptionin warning orange. Always shown regardless of quiet mode. JSON mode emits{"kind": ..., "description": ...}. Issue #192 tracks aligningconfig checkto the same convention.Docs
--jsonoutput shapeCONTEXT.md:Healthyglossary entry