Phase 06 Plan 01: Formatter Interface + TableFormatter Refactor Summary
Established the output.Formatter interface, a name-keyed registry, and TTY-aware color detection, then refactored the existing colored-table output to implement Formatter and register itself as "table". This is the foundation every other Phase 6 formatter (JSON, SARIF, CSV) will plug into.
ColorsEnabled(io.Writer) bool — returns false when NO_COLOR is set, when writer is not an *os.File, or when the file is not a TTY
Promoted github.com/mattn/go-isatty v0.0.20 from indirect (via lipgloss) to a direct dependency in go.mod
3. TableFormatter refactor (pkg/output/table.go)
TableFormatter struct{} implements Formatter, registered under "table" in init()
Writes to the caller-supplied io.Writer (no more hardcoded os.Stdout)
New tableStyles bundle + newTableStyles(colored bool) factory — when colored==false every style is a zero lipgloss.Style, which renders text verbatim with zero ANSI escapes
Preserves the exact layout from Phase 5: PROVIDER/KEY/CONFIDENCE/SOURCE/LINE columns plus optional VERIFY column, indented sorted metadata line, footer "N key(s) found."
Respects Options.Unmask to toggle between KeyMasked and KeyValue
PrintFindings(findings, unmask) retained as a deprecated thin wrapper that delegates to TableFormatter{}.Format(..., os.Stdout, Options{Unmask: unmask}) — keeps cmd/scan.go compiling untouched until Plan 06.
Tests
All green on go test ./pkg/output/... -count=1:
formatter_test.go: Register/Get round-trip, Get unknown → ErrUnknownFormat via errors.Is, Names() sort ordering, Options zero-value defaults.
colors_test.go: ColorsEnabled(&bytes.Buffer{})==false, NO_COLOR=1 forces false, typed-nil writer does not panic, IsTTY(nil)==false.
table_test.go (new):
Empty slice → exact "No API keys found.\n"
Two findings (one verified) written to bytes.Buffer → output contains no \x1b[ escapes, includes VERIFY column and "2 key(s) found." footer
VerifyMetadata {z:1, a:2} renders a: 2 before z: 1
Get("table") returns a TableFormatter
table_test.go (legacy): PrintFindings stdout-capture tests still pass (backward compat preserved).
Full project suite (go test ./...) green: cmd, engine, engine/sources, legal, output, providers, storage, verify.
Key Decisions
Registry is unguarded. All Register calls happen from package init() before main, which Go runs sequentially. Adding a mutex would be dead weight.
Plain styles via zero lipgloss.Style. Instead of conditionally calling different render functions, newTableStyles(false) returns lipgloss.NewStyle() for every field. A zero lipgloss.Style passes text through verbatim, so the same fmt.Fprintf(... style.header.Render("PROVIDER") ...) code path produces colored output on a TTY and plain output to a bytes.Buffer.
Backward-compat wrapper.PrintFindings stays so cmd/scan.go compiles without edits. Plan 06 will replace it with a registry lookup when wiring the --output flag. Deprecated comment in place.
NO_COLOR precedence.ColorsEnabled checks NO_COLOR before the TTY check, matching the https://no-color.org/ spec ("should check for a NO_COLOR environment variable that, when present (regardless of its value), prevents the addition of ANSI color").
Plan 02 (JSON + SARIF): create pkg/output/json.go and pkg/output/sarif.go, each with an init() calling Register. Implement Format against []engine.Finding. Use Options.ToolName/Options.ToolVersion for SARIF tool.driver metadata.
Plan 03 (CSV): identical pattern with encoding/csv.
Plan 06 (scan wiring): replace output.PrintFindings(findings, unmask) in cmd/scan.go with output.Get(flag) + f.Format(findings, os.Stdout, Options{Unmask: unmask, ToolName: "keyhunter", ToolVersion: version}).
Self-Check: PASSED
pkg/output/formatter.go — FOUND
pkg/output/formatter_test.go — FOUND
pkg/output/colors.go — FOUND
pkg/output/colors_test.go — FOUND
pkg/output/table.go — modified (TableFormatter present, Register("table", ...) in init)
pkg/output/table_test.go — modified (TableFormatter test block present)
go.mod — github.com/mattn/go-isatty v0.0.20 in direct require block