Documentation
¶
Overview ¶
Package diagnostics surfaces unwired forge scaffolds at runtime.
Why this exists ¶
Forge scaffolds Tier-1 stubs that compile cleanly but return `connect.CodeUnimplemented` / `ErrNotImplemented`, and wire_gen.go constructs Deps fields as typed-zero values when no producer matched (see `internal/codegen/wire_gen.go` for the producer-resolution rules). Both are legitimate during a migration: the user can ship a scaffold that hasn't been filled in yet and still have a passing build. Both also caused a real production outage (kalshi-trader, 2026-06-03) when an operator was unaware that the on-disk YAML config wasn't being loaded — the stubbed loader silently returned `ErrNotImplemented`, the caller fell back to a Go-literal default with looser knobs, and a cron worker no-op'd for ~24h before anyone noticed.
`forge lint --wire-coverage` catches the nil-dep half at develop time. `forge lint --scaffolds` catches `FORGE_SCAFFOLD:` markers at commit time. Neither is visible to the operator watching boot logs in production. This package is the runtime third leg of that stool: a structured boot-time warn surface that emits one log line per unwired scaffold plus a roll-up summary, every time the binary starts.
Shape ¶
Codegen emits one `pkg/app/diagnostics_gen.go` per project. Its `init()` calls `diagnostics.Default.RegisterStub(...)` and `diagnostics.Default.RegisterNilDep(...)` for every scaffold the codegen detected at generate time. Bootstrap calls `Default.Boot(e)` after `Setup()` returns, where `e` is a `LogEmitter` wrapping the project's `*slog.Logger`. Each diagnostic produces one structured log line at warn level with the stable event name `forge.scaffold.unwired`; the roll-up summary uses `forge.scaffold.unwired.summary`.
Strict mode ¶
When `forge.yaml: features.strict_wiring: true`, Bootstrap wraps the base emitter with `StrictEmitter`, which calls `log.Fatalf` (and thus exits non-zero) after the summary if any diagnostic was emitted. Default-off: migration paths legitimately ship partial scaffolds, and forcing strict mode on every project would block the very scaffolding the package is designed to make safer to ship.
Acknowledged-stub opt-out ¶
Users can mark a Deps field with `// forge:stub-ok reason=...` to suppress the corresponding diagnostic. The marker lives in user-owned code (handlers/<svc>/service.go), so regen never wipes the acknowledgement. Distinct from `// forge:optional-dep`, which also gates validateDeps; `forge:stub-ok` only suppresses the unwired-scaffold diagnostic.
Index ¶
Constants ¶
const EventName = "forge.scaffold.unwired"
EventName is the stable slog event-name attribute every LogEmitter log line carries. Operators grep this; dashboards count it. Do not rename without a deprecation cycle.
const SummaryEventName = "forge.scaffold.unwired.summary"
SummaryEventName is the stable slog event-name for the roll-up line. Distinct from EventName so dashboards can chart the summary count separately from per-diagnostic occurrences.
Variables ¶
var Default = NewRegistry()
Default is the process-wide Registry that codegen-emitted diagnostics_gen.go's init() targets. Tests that want isolation can construct their own Registry via NewRegistry.
Functions ¶
This section is empty.
Types ¶
type Diagnostic ¶
type Diagnostic struct {
// Kind is one of the KindXxx constants above.
Kind Kind `json:"kind"`
// Symbol is the canonical Go package.identifier the diagnostic
// names. For KindStubImpl this is "<pkg>.<Func>" (e.g.
// "botconfig.LoadFromYAML"). For KindNilDep it's
// "<wireFunc>.<DepField>" (e.g.
// "wireWorkerCalibratorRefitDeps.PgUnsettled").
Symbol string `json:"symbol"`
// File is the project-relative path of the codegen-emitted source
// (forward slashes regardless of OS). Matches the path convention
// used by `forge audit --json` and `forge lint`.
File string `json:"file"`
// Line is the 1-indexed line number of the marker in File.
Line int `json:"line"`
// Component is the enclosing wire_gen function name for
// KindNilDep diagnostics (e.g.
// "wireWorkerCalibratorRefitDeps"). Empty for KindStubImpl —
// stubs have no wiring component.
Component string `json:"component,omitempty"`
// DepName is the Deps field name for KindNilDep diagnostics
// (e.g. "PgUnsettled"). Empty for KindStubImpl.
DepName string `json:"dep_name,omitempty"`
// Message is the one-line human-readable summary the LogEmitter
// writes as the slog log message. Stable across regenerates so
// log search queries don't break.
Message string `json:"message"`
// Severity is the planned emit severity. The LogEmitter ignores
// this field (it always writes warn); StrictEmitter only triggers
// a fatal exit when at least one diagnostic was emitted, not on
// per-diagnostic severity. The field is informational for
// audit-JSON consumers.
Severity Severity `json:"severity"`
}
Diagnostic is one registered unwired-scaffold record.
Construction is private to the Registry's Register* methods; do not build Diagnostic literals from user code. The JSON shape is part of the forge audit / lint contract.
type Emitter ¶
type Emitter interface {
// Emit writes one diagnostic. Implementations should not retain
// the diagnostic beyond the call — the Registry owns the
// canonical copy.
Emit(d Diagnostic)
// Summary writes the roll-up. May be called with an empty slice,
// in which case implementations should write a "clean" line (or
// nothing) — never a misleading "0 entries" warning.
Summary(ds []Diagnostic)
}
Emitter is the runtime sink for diagnostics.
Implementations must be safe for concurrent calls (Bootstrap calls Boot serially, but composed emitters may fan out to goroutines). Boot calls Emit once per diagnostic, then Summary once with the full slice in stable order.
type Kind ¶
type Kind string
Kind classifies an unwired scaffold by detection rule.
Two kinds today, matching the two backlog rules (FORGE_BACKLOG.md 2026-06-03 entry):
KindStubImpl: a generated function body whose only statement is `return ..., ErrNotImplemented` (or a configurable sentinel error). Detected at codegen time via the `// forge:gen unwired-stub` marker emitted by the handler template.
KindNilDep: a wire_gen.go DI site where a Deps field is constructed as `nil` (or typed zero) with no `forge:stub-ok` opt-out. Detected at codegen time by `internal/codegen/wire_gen.go`'s UnresolvedFields tracking.
Additional kinds can land additively without breaking existing consumers — the JSON tag and the audit-category contract both allow unknown enum values.
const ( // KindStubImpl is a Tier-1 stub whose body is the // ErrNotImplemented sentinel only. The user is expected to fill // in the body and remove the FORGE_SCAFFOLD: marker; until they // do, every boot emits one diagnostic per occurrence. KindStubImpl Kind = "stub-impl" // KindNilDep is a wire_gen.go Deps field constructed as nil with // no acknowledged-stub opt-out. The user should either extend // *App with a matching field and assign it in setup.go, or mark // the Deps field `// forge:stub-ok reason=...`. KindNilDep Kind = "nil-dep" )
type LogEmitter ¶
LogEmitter writes structured warn-level slog lines.
Logger may be nil — Emit falls back to slog.Default(), so projects that haven't wired a custom logger still get output. Most callers pass the per-process *slog.Logger from cmd/server.go.
func NewLogEmitter ¶
func NewLogEmitter(logger *slog.Logger) LogEmitter
NewLogEmitter returns a LogEmitter wrapping logger. Nil logger is fine; Emit will fall through to slog.Default().
func (LogEmitter) Emit ¶
func (l LogEmitter) Emit(d Diagnostic)
Emit writes one diagnostic at warn level with the stable EventName attribute. Attribute keys (kind, symbol, file, line, component, dep_name) are stable across forge versions.
func (LogEmitter) Summary ¶
func (l LogEmitter) Summary(ds []Diagnostic)
Summary writes the roll-up line. Empty slice → no output (Bootstrap callers don't want a "0 unwired scaffolds" warn at every boot of a clean project). Non-empty slice → one warn line with the count and a bracket-list of symbols, so a single log line answers "what's unwired?" without paging through the per-diagnostic lines.
type MultiEmitter ¶
type MultiEmitter struct {
Emitters []Emitter
}
MultiEmitter fans Emit and Summary calls out to every base emitter in order. Used to compose LogEmitter with a future MetricsEmitter so the same wiring gap surfaces on both stdout and an OTel dashboard.
Construct with NewMultiEmitter so the zero value is unambiguous — an empty MultiEmitter is a NopEmitter equivalent, which is rarely what the caller wants.
func NewMultiEmitter ¶
func NewMultiEmitter(emitters ...Emitter) MultiEmitter
NewMultiEmitter returns a MultiEmitter whose Emit/Summary calls fan out to each supplied emitter in order. Pass at least one emitter; an empty MultiEmitter is a NopEmitter equivalent.
func (MultiEmitter) Emit ¶
func (m MultiEmitter) Emit(d Diagnostic)
Emit fans the diagnostic out to every base emitter. Errors in any one emitter must not stop the others — implementations are expected to never panic, and we don't recover here.
func (MultiEmitter) Summary ¶
func (m MultiEmitter) Summary(ds []Diagnostic)
Summary fans the roll-up out to every base emitter.
type NopEmitter ¶
type NopEmitter struct{}
NopEmitter drops every Emit and Summary call. Used by tests that don't care about output, and as the fallback when a caller passes nil to Registry.Boot.
func (NopEmitter) Emit ¶
func (NopEmitter) Emit(Diagnostic)
func (NopEmitter) Summary ¶
func (NopEmitter) Summary([]Diagnostic)
type Registry ¶
type Registry struct {
// contains filtered or unexported fields
}
Registry holds the set of diagnostics for one process.
func NewRegistry ¶
func NewRegistry() *Registry
NewRegistry returns a new empty Registry. Most callers use the package-level Default instead — there's one process-wide Registry and codegen's init() targets it directly.
func (*Registry) Boot ¶
func (r *Registry) Boot(e Emitter) []Diagnostic
Boot emits every registered diagnostic through the supplied Emitter, then a single Summary call. Returns the full slice (in stable order) so callers — typically Bootstrap — can roll the data up into `forge audit --json` without re-walking the registry.
A nil Emitter is replaced by a NopEmitter; the slice is still returned so consumers that just want the data don't have to wire a logger.
Boot is idempotent in the sense that repeated calls emit the same data; it does not clear entries. Tests that want isolation should use a per-test Registry rather than calling Boot on Default twice.
func (*Registry) Len ¶
Len returns the number of registered diagnostics. Useful for tests and for the Bootstrap caller that wants to short-circuit when zero.
func (*Registry) RegisterNilDep ¶
RegisterNilDep records a wire_gen.go DI site where a Deps field is constructed as nil (or typed zero) with no acknowledged-stub opt-out.
Called from generated `diagnostics_gen.go::init()`. Component is the wire_gen function name (e.g. "wireWorkerCalibratorRefitDeps"); depName is the Deps field; file is project-relative; line is the 1-indexed line of the `// TODO: wire <field>` marker.
Empty component or depName is a no-op.
func (*Registry) RegisterStub ¶
RegisterStub records a Tier-1 stub whose body is solely `return ..., ErrNotImplemented` (or the configured sentinel).
Called from generated `diagnostics_gen.go::init()`. The symbol argument is the canonical `<pkg>.<Func>` identifier; file is project-relative; line is the 1-indexed line of the `// forge:gen unwired-stub` marker.
Empty symbol is a no-op — codegen should never emit one, but the guard avoids a noisy log line if it does.
type Severity ¶
type Severity string
Severity classifies a diagnostic by emit policy.
The default emitter writes warn-level slog lines. StrictEmitter upgrades the summary into a fatal exit when any diagnostic was emitted. The Severity field on Diagnostic itself is informational — it tells consumers what level the runtime emitter is going to use — and is not used to dispatch.
type StrictEmitter ¶
type StrictEmitter struct {
Base Emitter
}
StrictEmitter wraps a base Emitter and terminates the process via osExit(1) after Summary when at least one diagnostic was emitted.
The base emitter sees every Emit and Summary call first, so the fatal line is the LAST log output — operators see the full list before the exit. Empty Summary slice is a clean exit (no termination), matching the LogEmitter "no output for zero diagnostics" convention.
func NewStrictEmitter ¶
func NewStrictEmitter(base Emitter) StrictEmitter
NewStrictEmitter wraps base in a StrictEmitter. A nil base falls back to NopEmitter — strict mode with no logging surface is rare but legal (e.g. when the caller wants the exit-on-unwired behavior without writing log lines).
func (StrictEmitter) Emit ¶
func (s StrictEmitter) Emit(d Diagnostic)
Emit forwards to the base emitter. StrictEmitter does not terminate on per-diagnostic Emit — the exit decision is made at Summary so the operator sees every line before the process dies.
func (StrictEmitter) Summary ¶
func (s StrictEmitter) Summary(ds []Diagnostic)
Summary forwards to the base emitter, then terminates the process via osExit(1) if any diagnostic was emitted. The format of the final stderr line is intentionally plain (not slog-formatted) so it's visible even when slog handlers buffer or drop output.