6.5 KiB
Plan: validate-docmap subcommand (Issue #141)
Problem
CI has no way to verify that doc-map.yml is kept up to date. When a developer adds a new
module/directory, they may forget to add a paths: entry. When a design doc is deleted or
moved, the docs: entry becomes stale. Both failures are silent — the AI reviewer just gets
no docs injected, and nobody notices.
This is a pure static check: no AI, no VCS API. Just YAML parsing + glob matching + os.Stat.
Constraints
- No external API calls or AI involvement
- Must compose with
git diff --name-onlyoutput via stdin (standard CI pattern) - Reuse existing
ParseDocMapConfigfromreview/docmap.go - Glob matching logic must also reuse (or expose) existing
globMatch/mappingMatches - Follow the
validate-urlsubcommand pattern exactly - Both checks must always run — report all failures, not just the first
outWriter/errWritervars must be respected for testability
Proposed Approach
1. Export a glob-coverage helper from review/docmap.go
Add one new exported function:
// FileCoveredByDocMap returns true if any paths: glob in cfg matches the given file.
func FileCoveredByDocMap(cfg *DocMapConfig, file string) bool
This is a thin wrapper over the existing unexported mappingMatches. It lets the cmd/ layer
call into the review package without duplicating glob logic.
Alternative considered: Duplicate the loop in cmd/. Rejected — duplication of non-trivial
glob matching is a maintenance hazard. Exporting one function is cleaner.
2. New file: cmd/review-bot/validatedocmap.go
Implements runValidateDocmap(args []string) int following the validateurl.go pattern.
Flag parsing (use flag.NewFlagSet — NOT global flag, to avoid polluting main.go's flag state):
--docmap (required) path to YAML file
--repo-root (optional, default ".") base for resolving docs: paths
Step 1: Parse flags. Validate --docmap is set. Exit 2 on error.
Step 2: ParseDocMapConfig(docmapPath) → exit 2 on parse error
Step 3: Read stdin lines → changedFiles []string
Step 4: Coverage check — for each file in changedFiles:
if !FileCoveredByDocMap(cfg, file) → record as uncovered
Step 5: Stale-docs check — for each unique docs: entry across all mappings:
if os.Stat(filepath.Join(repoRoot, docPath)) fails → record as stale
Step 6: If any uncovered or stale entries → print ERROR sections → return 1
Else → print "OK" → return 0
Exit codes (parallel to validate-url):
0— clean1— coverage or stale-doc failures2— usage error, missing flag, or YAML parse error
3. Wire into main.go
Add case "validate-docmap": to the existing os.Args[1] switch.
4. Tests: cmd/review-bot/validatedocmap_test.go
Test table covering:
| Case | stdin | docmap | repo-root | want exit |
|---|---|---|---|---|
| clean | covered file | valid docmap | docs exist | 0 |
| uncovered file | uncovered file | valid docmap | docs exist | 1 |
| stale doc | covered file | stale docs: | missing path | 1 |
| both failures | uncovered + stale | 1 | ||
| empty stdin | (empty) | valid docmap | docs exist | 0 |
| missing --docmap flag | 2 | |||
| bad YAML | invalid YAML | 2 |
Use os.MkdirTemp + os.WriteFile to create real temp directories for the stale-docs check.
5. README update
Add a subsection under the validate-url section showing the validate-docmap invocation.
State/Data Model
No persistent state. All inputs are flags + stdin + local filesystem.
Error Cases
| Scenario | Behavior |
|---|---|
--docmap flag missing |
Print usage, exit 2 |
| YAML parse fails | Print error message, exit 2 |
| stdin read error | Print error, exit 2 |
--repo-root does not exist |
Individual docs: entries will fail Stat; logged per-path, exit 1 |
| changed file is empty string (blank line) | Skip (trim + ignore empty) |
Edge Cases
- Blank lines in stdin input (from git diff with trailing newline) → trim and skip
- Duplicate
docs:entries across multiple mappings → deduplicate before checking existence docs:entry that is a directory (ends with/) →os.Statthe path; if it exists it's fine--repo-rootwith trailing slash → usefilepath.Joinwhich normalizes it- Changed files with
../or absolute paths → check only (no traversal needed here since we're just callingFileCoveredByDocMap, which is pure string matching)
Testing Strategy
- Unit tests with real temp files for stale-doc check (no mocking needed for
os.Stat) outWriter/errWritercapture pattern (same asvalidateurl_test.go)- Table-driven tests
Open Questions
- stdin vs
--filesflag: Using stdin matches the standard CI pipe idiom and avoids shell quoting issues with many files. Confirmed by Aaron's clarification. - Empty stdin coverage: Aaron said empty stdin = no coverage failures. This means
"no changed files, no problem" — vacuously true. Makes sense for
git diffon unchanged branches. - Directory docs: entries:
os.Statis sufficient — if the directory exists, it's valid. We don't recursively verify it has.mdfiles. Kept simple. --repo-rootvs always cwd: Default to cwd but allow override. This makes the command usable from CI scripts thatcdto a different directory.
Completion Checklist (generated for this task)
FileCoveredByDocMapexported and covers the all-mappings, any-glob-matches logic correctly?runValidateDocmapfollowsrunValidateURLexactly: flag parse → validate → work → exit code?- Both checks always run (no early exit after first failure section)?
- Empty stdin treated as clean (exit 0, no coverage errors)?
- All
docs:entries deduplicated before stale check? outWriter/errWriterused (notfmt.Printlndirectly), so tests can capture output?case "validate-docmap":added tomain.godispatch switch?- Tests cover all 7 cases in the table above?
- README updated with usage example?
go test ./...passes with no new failures?
Implementation Phases
Phase 1: Export helper in review/docmap.go
- Add
FileCoveredByDocMap(cfg *DocMapConfig, file string) bool - Add test in
review/docmap_test.go
Phase 2: cmd/review-bot/validatedocmap.go
- Full
runValidateDocmapimplementation
Phase 3: Wire into main.go + tests
case "validate-docmap":dispatchvalidatedocmap_test.gowith full table
Phase 4: README + final
- Update README
go test ./...