Files

7.2 KiB

phase, plan, subsystem, tags, requirements, dependency-graph, tech-stack, key-files, decisions, metrics
phase plan subsystem tags requirements dependency-graph tech-stack key-files decisions metrics
06-output-reporting 01 pkg/output
output
formatter
registry
tty
colors
refactor
OUT-01
OUT-06
requires provides affects
pkg/engine.Finding
output.Formatter interface
output.Options struct
output.Registry (Register/Get/Names/ErrUnknownFormat)
output.TableFormatter
output.IsTTY / output.ColorsEnabled
cmd/scan.go PrintFindings wrapper
added patterns
github.com/mattn/go-isatty (promoted from indirect to direct)
Registry via init()-registered formatters
TTY-aware color stripping using zero-value lipgloss.Style
Deprecated wrapper (PrintFindings) delegating to new interface
created modified
pkg/output/formatter.go
pkg/output/formatter_test.go
pkg/output/colors.go
pkg/output/colors_test.go
pkg/output/table.go
pkg/output/table_test.go
go.mod
Registry is unguarded: all registration happens at init() before main.
newTableStyles(false) returns zero lipgloss.Style values — lipgloss passes text through verbatim, guaranteeing no ANSI escapes on non-TTY writers.
PrintFindings(findings, unmask) kept as backward-compat wrapper so cmd/scan.go still compiles unchanged until Plan 06 (scan flag wiring).
ColorsEnabled honours NO_COLOR (https://no-color.org/) before the TTY check.
duration completed tasks commits
~8m 2026-04-05 2 3

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.

What Was Built

1. Formatter interface + registry (pkg/output/formatter.go)

  • Formatter interface: Format(findings []engine.Finding, w io.Writer, opts Options) error
  • Options struct: { Unmask, ToolName, ToolVersion }
  • Register(name, f) — called from init() functions
  • Get(name) — returns ErrUnknownFormat wrapped via fmt.Errorf("%w: %q", ...)
  • Names() — sorted list for --output help text
  • ErrUnknownFormat sentinel, discoverable via errors.Is

2. TTY detection (pkg/output/colors.go)

  • IsTTY(*os.File) bool — wraps isatty.IsTerminal + isatty.IsCygwinTerminal, nil-safe
  • 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
    • Unverified-only header does NOT contain VERIFY
    • Verified row shows live
    • Unmask=false renders KeyMasked; Unmask=true renders KeyValue
    • 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

  1. Registry is unguarded. All Register calls happen from package init() before main, which Go runs sequentially. Adding a mutex would be dead weight.

  2. 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.

  3. 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.

  4. 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").

Deviations from Plan

None — plan executed exactly as written.

Commits

Commit Type Message
291c97e feat add Formatter interface, Registry, and TTY color detection
8c37252 test add failing tests for TableFormatter refactor
8e4db5d feat refactor table output into TableFormatter

Foundation for Next Plans

  • 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
  • Commits 291c97e, 8c37252, 8e4db5d — FOUND in git log
  • go build ./... — succeeds
  • go test ./pkg/output/... -count=1 — PASS
  • go test ./... — PASS