Files
commandkit/docs/roadmap/reporting.md

43 KiB
Raw Blame History

Output policy and invocation reporting

Status: behavioral decisions selected through OD-4. Message categories, all four output modes, uniform treatment of help and version, message prefixes, and OD-1's flag grammar and scope (including independent Progress control) are selected. OD-2's unified reporting direction, the terminal failure model, and application ownership of domain-specific error decisions are selected. OD-3's failure presentation and retention boundaries are selected. OD-4's delivery lifecycle, write-failure handling, and quiet-mode rendering are selected. The consumer API signatures, presentation-result model, provider diagnostic boundary, fallback explanations, and rendering callback boundary below are also selected. Architectural preparation is complete: ADR 0014 is accepted, and the architecture policy is aligned. Implementation has started. Stages 1–19 are complete; final validation remains. The implementation plan owns stage order and execution instructions.

Purpose

Provide consistent handling of --quiet, --verbose, --debug, and --progress across application commands and optional integrations. Reduce repeated output classification, diagnostic presentation, and delivery bookkeeping where CommandKit can provide a meaningful benefit.

Ordinary Go writers and formatting functions already make application output straightforward. Design reporting conveniences around the shared output policy and invocation lifecycle. Applications continue to own their report content, domain behavior, and operation outcomes.

CLI presentation and error-handling ownership

Applications decide how to respond to domain-specific errors: recover, retry, continue silently, report a Warning, or end the invocation with a failure. CommandKit may provide shared helpers for carrying, inspecting, and presenting errors while leaving those decisions with the application. A Go error alone does not determine a CLI message category. Warnings may describe issues with or without an underlying Go error, and handling an error does not require emitting a message.

CommandKit handles the presentation the application requests: message category, visibility, formatting, timing, and delivery. For terminal application failures, the application supplies a non-zero outcome and an operator-facing explanation. It may also supply an optional Go error cause for the execution caller to inspect. CommandKit preserves that cause and its existing errors.Is/errors.As behavior. Supplying a cause alone does not select an outcome, generate an operator-facing message, or change visibility.

Applications can explicitly present err.Error() as a Warning through the ordinary message API, illustrated by report.Warn(err.Error()). Defer a separate WarnError convenience helper pending demonstrated value; this conversion already requires little code.

CommandKit retains responsibility for handling its own errors, including routing, parsing, and reporting failures. Integration-owned commands similarly adapt their upstream APIs into deliberate command outcomes and presentations. Ordinary Go error returns from CommandKit APIs and writers remain available where appropriate.

Current baseline

Streams and Invocation expose caller-selected output and diagnostic writers. Outcome and Diagnostic carry operation causes, diagnostic writers, and delivery state. Handlers can return Failure(err) or attach diagnostics to an outcome for presentation after the handler returns. MessageDiagnostic currently writes the supplied message with a newline; severity does not add a label automatically.

CommandKit coordinates final diagnostic delivery and retains delivery failures separately from the operation cause. Custom handlers can also write directly to the supplied streams and retain responsibility for their output. The existing outcome and channel ownership decisions are recorded in ADR 0005.

Integration adapters already translate upstream diagnostic records into CommandKit diagnostics while preserving native rendering and source context. Some adapters wrap multiple records in one diagnostic writer and classify the batch by its highest severity. This matters when designing selective visibility.

There is currently no shared verbosity policy or invocation reporter API.

Selected output policy

Distinguish substantive application output from diagnostic message categories:

  • Substantive output (Normal): The requested report or command result.
  • Error: Terminal presentation of an unrecoverable invocation failure, resulting in a non-zero status code.
  • Warning: Something went wrong or deserves attention, but the invocation can still succeed. The application chooses whether to report such an issue.
  • Info: Informational context and useful explanation, such as which profile was selected or a final operation summary.
  • Progress: Streaming updates about work underway, such as items processed.
  • Debug: Troubleshooting detail, including intermediate decisions and execution-path information.

Debug mode adds troubleshooting detail to verbose output:

Output category --quiet No flag --verbose --debug
Substantive application output Suppressed Shown Shown Shown
Error Shown Shown Shown Shown
Warning Suppressed Suppressed Shown Shown
Info Suppressed Suppressed Shown Shown
Debug Suppressed Suppressed Suppressed Shown
Progress, without an explicit setting Suppressed Suppressed Suppressed Shown

Info belongs in verbose mode; debug includes verbose messages. Normal output remains a substantive result category, separate from diagnostic severity. The category of a message determines its visibility; stream destination alone cannot distinguish a warning from an error.

Progress is distinct from Info and is controlled separately as specified below. Applications and integrations are not required to generate messages in any category. Enabling a category adds no output when there are no messages in it; other output remains subject to its own visibility rules.

Error presentation follows the terminal failure model below. An application may report a recovered problem as a Warning or continue without a message.

Apply the same policy to help and version. There are no command-specific exceptions for successful output:

app help --quiet       # succeeds silently
app version --quiet    # succeeds silently

Usage and operational failures still report their errors in quiet mode. The feature must distinguish successful output suppression from failure diagnostic delivery.

Message prefixes and channels

Substantive output uses the result destination, conventionally stdout. Warning, Info, Progress, Debug, and terminal explanations use the diagnostic destination, conventionally stderr. Callers supply both destinations.

Use WARN:, DEBUG:, and ERROR: prefixes for simple warning, debug, and error messages respectively. Substantive output and Info messages have no prefix. For multiline diagnostics, prefix the opening line and preserve the structured detail. Integration adapters coordinate native diagnostic rendering to avoid duplicate severity labels or loss of upstream formatting.

Usage guidance belongs to the terminal usage explanation and remains visible in quiet mode. Successful help output follows the substantive-output policy.

Initially, Progress messages are unprefixed streamed lines. The diagnostic destination keeps them separate from substantive output such as JSON or YAML. Terminal redraw and progress-bar behavior are outside this implementation.

Terminal failure model

During execution, applications can produce substantive output, Progress, Info, Debug, and Warnings. An unrecoverable failure ends the handler by returning a failed outcome. There is no standalone report.Error(...) method. Detailed error explanations accompany the returned failure and are presented as part of that terminal step.

CommandKit finalizes reporting, presents the terminal failure explanation, and returns the non-zero outcome to the application entry point. It accepts no further normal output for that invocation after terminal error presentation. The same boundary applies to core routing and parsing failures that prevent handler execution. The application entry point retains ownership of process exit; CommandKit ends the invocation and returns its outcome.

For example, if an export operation fails and the application decides it cannot continue, the handler returns a non-zero outcome with an explicit explanation such as "Unable to export the requested report." The application can choose to include detail from a Go error in that explanation. CommandKit presents the supplied explanation without automatically deriving text from an application error. The selected API below provides failure constructors for this purpose. When a failed outcome supplies no explanation, CommandKit supplies the generic fallback defined under terminal outcomes below.

Output streamed before the failure remains visible according to its selected mode; terminal failure does not withdraw earlier bytes. Deferred messages, cleanup writes, and delivery failures must respect the end of normal reporting. Their finalization and late-write behavior follow OD-4 below. A terminal failure may have multiple explanatory records, all belonging to that failure.

Failure explanations and presentation ownership (OD-3)

Make a non-zero outcome with an explicit message the convenient failure path. Support structured explanations, including multiple records, through the same presentation facility for richer application and integration reports. Neither form requires an underlying Go error.

The application or integration explicitly identifies the context belonging to a terminal explanation. Present that context with the failure in every mode, including quiet. Independent warnings and other messages retain their own visibility rules. For mixed upstream batches, adapters preserve useful native rendering while distinguishing terminal explanatory context from independent messages. Core does not infer those relationships from error chains or text.

Assign each submitted message one presentation path. Returning a terminal explanation schedules its presentation once; core does not append another message derived from an application error cause. Native renderers can supply already-labelled content without receiving duplicate prefixes. Avoid text-based deduplication: separate events may legitimately produce identical messages.

Constructing a failure result attaches explanatory content without emitting or independently scheduling an Error. Returning that result hands the explanation to CommandKit for terminal delivery through the invocation's reporting facility. The selected API expresses this distinction for both simple and structured explanations. For example:

Handler action Presentation
Construct a non-zero result with "Unable to export the requested report," then return it. Construction emits nothing. CommandKit presents the message once during terminal delivery.
Return a failure with native diagnostic records and their source context, submitting independent warnings separately. CommandKit presents the structured terminal explanation in every mode. Independent warnings follow their own visibility policy.

Retention boundary (OD-3)

CommandKit provides invocation-scoped CLI presentation. Logging and message history collection remain application concerns. Keep terminal explanations and any supplied cause with the returned outcome, and keep the temporary state needed for deferred delivery, parser events awaiting mode selection, and delivery bookkeeping. The reporting facility does not collect a transcript or retain other emitted or suppressed messages for later inspection. No optional message-history facility is planned for this implementation.

Selected flag grammar and scope

Mode selection and resets

Model quiet, ordinary, verbose, and debug as mutually exclusive presentation modes. Each invocation starts in ordinary mode. Process flag occurrences in invocation order; the last enabled mode wins. Repetition does not increment verbosity, and debug is selected explicitly.

Accept presence and the existing explicit boolean syntax, including --quiet=true and --quiet=false. A false occurrence clears its mode only if that mode is currently selected, returning to ordinary mode. It does not restore an earlier selection or disable a different mode. Every occurrence is converted; invalid boolean values remain usage errors even when a later flag would replace the selection.

Flag sequence Final mode
--quiet --verbose Verbose
--debug --quiet Quiet
--verbose --verbose Verbose
--verbose --verbose=false Ordinary
--quiet --verbose=false Quiet
--verbose --quiet --quiet=false Ordinary
--quiet=false --debug Debug

Keep verbosity a presentation choice: quiet preserves the requested operation's semantics and rendering failures. Delivery failures depend on the output actually requested; suppressing a destination write can avoid a delivery failure. Apply the policy through CommandKit-managed reporting and streams. Output written directly to process streams or external destinations remains application-owned.

Progress selection

--progress enables Progress without enabling Info, Warning, or Debug messages. --progress=false explicitly disables it. Accept the existing boolean syntax; repeated canonical or aliased progress settings use the last value. Retain whether a setting was explicitly supplied, separately from its boolean value.

Determine effective Progress visibility from the final presentation mode and explicit progress setting:

  1. Quiet mode always suppresses Progress.
  2. Otherwise, an explicit progress setting controls visibility.
  3. Without an explicit setting, debug enables Progress; ordinary and verbose modes leave it disabled.

An explicit progress setting overrides the mode's default regardless of its position relative to mode flags. It does not select or reset the presentation mode. Debug includes verbose detail and defaults Progress on; verbose alone does not enable Progress.

Flags Result
--verbose Warning and Info, without Progress.
--verbose --progress Warning, Info, and Progress.
--debug Warning, Info, Debug, and Progress.
--debug --progress=false or --progress=false --debug Debug detail without Progress.
--quiet --progress or --progress --quiet Only failure diagnostics.
--debug --verbose Verbose detail without Progress, absent an explicit progress setting.

Passing --progress to an application that emits no Progress messages adds no visible output. It does not suppress that application's substantive results or require a progress implementation.

Placement, availability, and aliases

Core supplies --quiet, --verbose, --debug, and --progress for application commands, integrations, the root, canonical groups, and alias-only discovery help. Consumers do not register them individually. Preserve the existing placement grammar: flags follow the complete command path and may be interspersed with operands after that path. For example:

app config show --quiet
app export destination --verbose
app help config show --quiet
app version --quiet

Options before or between command-path components are outside this grammar. Tokens consumed as values and tokens following -- retain their ordinary meaning.

Reserve the four long spellings. Applications may add -q, -v, or -d through the alias API; no built-in short spellings are selected. An application flag declaration that defines a reserved long name fails installation with a collision error. Aliases may target these flags but may not overload canonical spellings. Apply the existing collision and atomic installation rules, including later registration.

Aliased and canonical occurrences participate in the same ordering. With -q → --quiet, app export --verbose -q selects quiet mode. Document the newly reserved canonical names as a compatibility change.

Routing and parsing failures

Determine the mode through the normal parser before execution or successful help/report presentation. Do not add a separate verbosity scanner. This keeps token interpretation consistent and avoids reinterpreting values or operands.

Routing and parsing failures report their errors regardless of verbosity. For example, app export --quiet --unknown still reports the unknown flag. Error visibility follows the selected output policy independently of how the mode is determined; avoiding a separate scanner is an implementation simplification.

CommandKit's own diagnostics

Use the shared reporting system for CommandKit's invocation diagnostics so parser, integration, and application messages follow the same visibility and delivery policy. Installation errors occur before an invocation exists and continue to be returned to the application without automatic printing.

Supported repetition and last-occurrence overrides are routine decisions. Repeated singleton flags, including --verbose --verbose, may produce debug detail; repetition alone does not warrant a warning. Repeatable flags retain their intended accumulation behavior. Add warnings for concrete concerns an operator can act on, such as a deprecated spelling if deprecation is introduced. Unknown flags, invalid values, and invalid command paths remain invocation failures with error diagnostics.

Start debug instrumentation with routing, alias resolution, verbosity selection, and handler completion. Other useful candidates can be added where they explain a consequential decision:

Event Useful detail
Command-alias resolution Entered path, intermediate redirects, and final canonical path.
Flag-alias resolution Entered spelling and canonical flag name.
Scoped alias selection A command-scoped mapping took precedence over an application-wide mapping.
Verbosity selection A later occurrence selected a different mode; for --verbose --debug, debug wins because it appears last.
Help-route selection A canonical command or alias-only discovery context was selected.
Repeated singleton flag A later occurrence replaced an earlier occurrence, without printing their values.
Input preparation Canonical input validation completed and the handler is starting.
Handler completion Canonical command and the handler's returned classification and status, before final delivery.

The handler-completion event describes the handler's result. The final returned invocation outcome also includes delivery failures and remains authoritative. Present this event before terminal failure presentation; do not emit a completion message afterward.

Use the final selected mode to present earlier parser decisions. Collect the relevant events during parsing and apply visibility after mode selection: --verbose --debug shows the selection explanation, while --debug --quiet suppresses it. This collection of parser events does not require whole-run output buffering.

Debug messages should explain operator-relevant decisions independently of incidental implementation steps. Use command paths, flag names, and decision descriptions rather than raw argument or configuration-value dumps. Upstream libraries retain ownership of their diagnostic detail and disclosure rules.

Selected delivery lifecycle and quiet execution (OD-4)

Ordering and finalization

Use synchronous delivery. Present immediate messages and substantive writes in submission order. After the handler returns:

  1. Deliver eligible deferred messages in queue order, without sorting by severity.
  2. Finish ordinary reporting, including any handler-completion debug message and flushing any CommandKit-owned buffers for ordinary output. Account for failures discovered during this delivery before preparing the terminal explanation.
  3. Present any terminal failure explanation and complete its delivery, including any CommandKit-owned buffering of that explanation.
  4. Close the invocation's reporting facility and return the final outcome.

Deferred messages remain eligible when the operation fails, subject to verbosity. Deferral controls timing; applications queue success summaries only after the relevant operation succeeds. Ordering governs CommandKit's writes; separate stdout and stderr destinations may be buffered or displayed differently by the caller.

CommandKit finalizes automatically. Consumers do not need a separate flush or finish call. Handler cleanup, including Go defer functions, runs before the handler returns and participates in normal reporting. Applications complete any reporting goroutines before returning. After finalization, writes and message submissions return a closed-reporter error and emit nothing.

Reporter operations within an invocation are safe to call concurrently. Serialize access to reporting state and delivery so individual messages do not interleave. Concurrent submissions have no predetermined order; deferred messages follow the order in which they enter the queue. Multiple writes composing one application report still require application coordination if they must remain together. This contract covers operations through one invocation's reporter; coordination with writes outside that reporter remains caller-owned.

Finalize any buffers owned by CommandKit. Caller-supplied writers remain caller-owned; CommandKit does not automatically close them or invoke arbitrary flushing interfaces.

Visibility applies when output is emitted. A verbose or debug run may show informational detail before subsequently failing. Progress and substantive output may likewise be partially delivered before a failure. Previously written bytes cannot be withdrawn. Whole-run buffering solely to make failed runs contain only error output is outside this implementation.

Keep errors understandable in every mode, including source locations and related explanatory context. Unrelated warnings and informational messages remain subject to the selected mode. Apply the selected retention boundary without accumulating a message history.

Rendering failures, write failures, and execution flow

Distinguish who invokes a renderer and whether its destination failed:

Failure source Responsibility and handling
An encoder or renderer called directly by the application The application handles its returned error and chooses the outcome and explanation.
A rendering callback invoked by CommandKit CommandKit handles its returned error as a presentation failure and reflects it in the invocation outcome.
A destination rejects a write CommandKit records the delivery failure and stops further writes to that destination.

A rendering failure may occur before any write or while the destination remains healthy. It does not by itself establish a failed destination. CommandKit can explain its rendering failure through a working diagnostic destination. These responsibilities also apply when rendering into a discard destination under quiet mode.

Failure to deliver requested output makes an otherwise successful invocation unsuccessful. Apply this rule to substantive output, Warning, Info, Progress, Debug, and terminal explanations. Intentional suppression requests no delivery and is not a delivery failure.

Standard writer operations return write errors normally. Immediate reporting methods also expose delivery errors. CommandKit records bounded delivery-failure information so an ignored write error cannot yield a successful invocation. Deferred delivery failures appear in the final outcome.

Apply ADR 0005's outcome rules when presentation fails. For standard outcomes, return operational failure with status 1, including when the original outcome was a usage failure. Preserve the original explanation and any supplied cause; record the presentation failure separately in Presentation.

Application-defined domain outcomes retain their domain classification. Preserve an already non-zero domain status. If the domain status was 0, change the suggested status to 1 so presentation failure cannot yield a successful invocation. The application entry point retains control of process exit.

Outcome before presentation fails Final outcome
Success, status 0 Operational failure, status 1.
Usage failure, status 2 Operational failure, status 1.
Operational failure, status 1 Operational failure, status 1.
Domain outcome, non-zero status Same domain classification and status.
Domain outcome, status 0 Same domain classification, status 1.

These rules apply to both destination-write failures and rendering failures from callbacks invoked by CommandKit. Reporting failures remain CommandKit's own error-handling responsibility.

Stop writing to a destination after it fails. A substantive-output failure can receive a terminal explanation through the diagnostic destination if it still works. If diagnostic delivery fails, return that information to the caller. Do not redirect diagnostics into substantive output or recursively attempt to report the reporting failure.

A failed write does not stop application code. The handler can inspect the error and return immediately; otherwise, CommandKit records the failure and finalizes when the handler returns. CommandKit does not use process exit or panic to interrupt execution. Terminal failure presentation ends normal output.

Quiet-mode rendering and suppression

Initially, run substantive report generation normally with its managed writer discarding output in quiet mode. This preserves report generation, validation, and serialization failures while changing presentation. For example, a report encoder failure still fails the invocation under --quiet; the handler remains responsible for handling errors returned by an encoder it calls.

Simple suppressed messages need no formatting or destination writes. For richer report renderers, initially render into a discard destination. Rendering may be skipped later where doing so preserves the command's operation and failure semantics.

Treat suppression as intentional suppression, never as failed or incomplete delivery. Do not retain suppressed content. Delivery needs only the deferred queue, reporting lifecycle state, and bounded delivery-failure information, alongside the temporary parser state and returned explanations described above.

Selected consumer reporting API

Provide one reporting facility per invocation for substantive output, immediate messages, and end-of-run diagnostics. The consumer API is chosen for its intended long-term design; with no current consumers, preserving existing API shapes carries no weight in this decision.

The following declarations specify the target API and are not yet implemented.

The reporter owns presentation: output visibility, progress settings, prefixes, ordering, and delivery bookkeeping. Its views share invocation state, allowing already-emitted messages, deferred messages, suppression, and write failures to be coordinated in one place. Helpers may receive the reporter or a narrow application-defined interface. Domain libraries can continue returning diagnostic data for the handler to submit, independently of CommandKit.

Use the reporter as the common diagnostic submission path, replacing the earlier proposal to split submission between a reporter and Outcome.Diagnostics. Terminal explanations accompany returned outcomes and are delivered through the same facility. Caller-selected destinations remain at App.Execute; handlers use Invocation.Reporter() in place of raw invocation streams.

Invocation reporter and structured detail

func (inv Invocation) Reporter() *Reporter

type Reporter struct {
    // Private invocation state.
}

func (r *Reporter) Output() io.Writer
func (r *Reporter) RenderOutput(write func(io.Writer) error) error

func (r *Reporter) Info(message string, details ...Detail) error
func (r *Reporter) Warn(message string, details ...Detail) error
func (r *Reporter) Debug(message string, details ...Detail) error
func (r *Reporter) Progress(message string, details ...Detail) error

func (r *Reporter) AtEnd() DeferredReporter

type Detail func(io.Writer) error

Output() supports ordinary writers and encoders. RenderOutput() coordinates an entire rendering operation, captures its rendering error, and prevents other reporter operations from interleaving with it. Rendering callbacks write through their supplied writer. Each complete categorized message, including its details, is likewise coordinated as one reporter operation.

Rendering callbacks use their supplied writer only during the callback. They may call rendering helpers that write through it. While a callback is running, it must not recursively submit messages, queue deferred messages, start another rendering operation, or write through a separate Output() handle belonging to the same invocation's reporter. Make additional reporter submissions before or after the callback. A callback must also avoid waiting for another goroutine to submit through that reporter, since the submission may be waiting for the callback to finish.

This boundary applies to RenderOutput, immediate and deferred message details, and terminal explanation details. Ordinary concurrent reporter calls remain supported and serialized. Keeping each rendering operation together motivates the boundary; it does not prescribe a lock implementation or promise runtime detection of recursive calls.

Message strings are literal; consumers use fmt.Sprintf when formatting is needed. Initially omit parallel Infof, Warnf, and similar methods. CommandKit formats the opening message with its category's prefix and a terminating newline. Optional details follow in order with their formatting preserved. An empty opening message produces no heading, prefix, or blank opening line, supporting native renderers that supply a complete, labelled diagnostic:

_ = report.Warn("Using the default profile")
_ = report.Warn("Some configuration settings need attention.", writeNativeWarnings)
_ = report.Warn("", writeNativeWarnings)

These are alternative calls; writeNativeWarnings represents an application- or integration-supplied function with signature func(io.Writer) error. No generic logging method or severity enumeration is needed to submit messages. Applications need not check verbosity before reporting or implement categories they do not use.

Immediate methods return rendering or delivery errors, and CommandKit records the corresponding presentation failure even if the caller ignores the error. A caller can inspect the returned error and end its handler immediately.

Deferred messages

type DeferredReporter struct {
    // Private reference to the same invocation state.
}

func (r DeferredReporter) Info(message string, details ...Detail) error
func (r DeferredReporter) Warn(message string, details ...Detail) error
func (r DeferredReporter) Debug(message string, details ...Detail) error

For example:

_ = report.AtEnd().Warn("Three optional files were unavailable.")

Copies of the deferred view share invocation state. Its methods return submission errors, such as attempting to use a closed reporter. Errors encountered during deferred rendering appear in the final invocation outcome. Progress remains an immediate streaming operation; substantive output uses the main reporter.

Copy callback lists when accepting a submission. Callers retain ownership of data captured by rendering callbacks and keep that data valid until presentation.

Terminal outcomes

Outcome construction requires no invocation state and emits nothing:

type Explanation struct {
    Message string
    Details []Detail
}

type Outcome struct {
    Kind         OutcomeKind
    Status       int
    Domain       string
    Cause        error
    Explanation  Explanation
    Presentation PresentationResult
}

func Success() Outcome
func Failure(message string, details ...Detail) Outcome
func Usage(message string, details ...Detail) Outcome
func Domain(name string, status int) Outcome

func (o Outcome) WithExplanation(message string, details ...Detail) Outcome
func (o Outcome) WithCause(cause error) Outcome

Success returns a successful outcome with status 0. Failure produces an operational failure with status 1; Usage produces a usage failure with status 2. Domain constructs an outcome with the supplied application-defined classification and status. Subsequent presentation failures follow OD-4's rules: domain classification is preserved, and a zero status becomes 1 if presentation fails. OutcomeKind retains the success, usage, operational, and domain classifications.

WithExplanation returns a modified outcome value and replaces its terminal explanation. Explanations belong to failed outcomes; successful summaries use Info or substantive output. Ordinary application failure, native failure rendering, and a custom domain failure respectively can be expressed as:

return commandkit.Failure("Unable to export the requested report.")
return commandkit.Failure("", writeNativeFailure)
return commandkit.Domain("validation", 3).
    WithExplanation("The document failed validation.", writeFindings)

The named renderers above have signature func(io.Writer) error and preserve the owning library's safe diagnostic formatting. They run during terminal delivery, not when constructing the outcome.

Before terminal delivery, supply the explanation Command failed. when a non-zero outcome has an empty Explanation.Message and no non-nil callback in Explanation.Details. This produces ERROR: Command failed. in every mode, including quiet, and becomes the explanation retained in the returned outcome. Adding the fallback leaves classification, status, and cause unchanged; presentation failures still follow OD-4's rules. The fallback is based on the failed outcome and absent explanation, without deriving text from Cause.Error().

An empty heading with a non-nil detail renderer is an explicitly supplied explanation. Trust that renderer to provide its content. Do not buffer or replay it to decide whether the explanation is sufficiently informative, and do not add the fallback merely because it writes no bytes.

Returned outcome Terminal presentation
Failure("") Generic fallback, operational classification and status 1.
Domain("validation", 3) Generic fallback, domain classification and status 3.
Failure("").WithCause(err) Generic fallback; the supplied cause remains available without automatic rendering.
Failure("", writeNativeFailure) Native explanation without an added generic heading.

Cause is optional. WithCause returns a modified outcome value, replacing the cause without changing the classification, status, or explanation. For example:

return commandkit.Failure("Unable to open the configuration file.").
    WithCause(err)

CommandKit presents the supplied explanation and carries the original error through to the execution caller, which can inspect it with expressions such as errors.Is(outcome.Cause, fs.ErrNotExist). It does not automatically present Cause.Error() or use the cause to decide success, failure, or message category. Providing a cause is independent of choosing the terminal explanation; neither is inferred from the other. Warnings do not require supplying or retaining causes.

Outcome processing preserves supplied causes, including their existing Go error identity, when a subsequent rendering or delivery failure occurs. Presentation failures are recorded separately in Presentation. The returned outcome retains its terminal explanation and optional cause, with other retention limited to the presentation needs described above.

Provider-returned diagnostic data

Providers return values and diagnostic data without needing a reporter parameter. Retain Acquired[T] with separate fields for independent messages and failure explanations:

type Acquired[T any] struct {
    Value       T
    Diagnostics []Diagnostic
    Explanation Explanation
}

Diagnostics carries independent, nonterminal messages. Diagnostic and Severity remain passive data types for those messages, with each diagnostic carrying its Severity and a Write func(io.Writer) error renderer. Adapt their contracts to the nonterminal reporting categories. Integrations submit the records through the corresponding reporter methods, including the deferred view where appropriate. Removing Outcome.Diagnostics removes a submission path; provider-returned records remain available for the integration to submit.

Explanation carries context to attach when the integration returns a failed outcome. Terminal Error content uses this field rather than an independently submitted diagnostic. An upstream warning that helps explain the failure may belong in the explanation; unrelated warnings remain in Diagnostics and follow their ordinary visibility rules. Adapters preserve this distinction and useful native formatting when translating mixed upstream batches, instead of assigning the whole batch its highest severity.

Providers may return acquired data alongside a Go error. The owning integration chooses the command outcome, attaches any terminal explanation, and decides which cause to preserve. Returning diagnostic data does not itself emit messages or select the invocation outcome. Diagnostic renderers follow the same callback boundary as other details when invoked through the reporter.

Presentation observations and closed reporters

type PresentationResult struct {
    Output      DeliveryResult
    Diagnostics DeliveryResult
}

type DeliveryResult struct {
    State       DeliveryState
    RenderError error
    WriteError  error
}

type DeliveryState uint8

const (
    DeliveryNone DeliveryState = iota
    DeliverySuppressed
    DeliveryComplete
    DeliveryIncomplete
)

var ErrReporterClosed = errors.New("reporter is closed")
State Meaning
DeliveryNone No content was submitted for this destination.
DeliverySuppressed All submitted content was intentionally suppressed, and rendering succeeded.
DeliveryComplete All requested delivery completed; other content may have been suppressed.
DeliveryIncomplete Rendering or destination writing failed.

Retain the first rendering failure and first destination-write failure for each destination. This provides bounded evidence without collecting an event history. Outcome.Presentation is populated by CommandKit during execution and finalization; it is observation data, not another submission API.

The invocation supplies the reporter. Consumers neither construct nor finalize it. After closure, message submissions and writer operations return ErrReporterClosed, including in quiet mode. The initial API needs no explicit flush method, transcript accessor, or verbosity checks. Error-conversion helpers are deferred pending demonstrated value.

Application example

This target-API handler uses an application-owned exportData operation that returns a JSON-encodable result with a UsedDefaults field:

func runExport(inv commandkit.Invocation) commandkit.Outcome {
    report := inv.Reporter()

    result, err := exportData(inv.Context)
    if err != nil {
        return commandkit.Failure("Unable to export the requested data.")
    }

    if result.UsedDefaults {
        _ = report.Warn("Some optional settings used defaults.")
    }

    if err := report.RenderOutput(func(w io.Writer) error {
        return json.NewEncoder(w).Encode(result)
    }); err != nil {
        return commandkit.Failure("Unable to present the export result.")
    }

    _ = report.AtEnd().Info("Export completed.")
    return commandkit.Success()
}

Ignored immediate reporting errors are still recorded and can make the final invocation unsuccessful. The renderer runs in quiet mode against the discard destination, preserving serialization failures. The deferred summary follows the selected verbosity policy and describes the completed export operation; the final invocation outcome also accounts for presentation failures.

Upstream diagnostic ownership

ConfigKit and PromptKit remain authoritative for their diagnostic semantics, severity, source context, and disclosure rules. Integration adapters expose those capabilities through the shared CommandKit presentation policy.

Support consistent reporting of ConfigKit errors and warnings as its adopted public API evolves. This roadmap does not assume that a future warning API already exists or prescribe its upstream shape. Integration-owned commands select the outcome and submit useful native diagnostic detail for presentation. Core presents those records under the shared policy. Applications using upstream libraries directly choose how to respond to domain-specific errors and which outcomes and messages to supply, as for their other operations.

An integration may receive both a useful substantive report and an operation failure, as PromptKit smoke testing supports. Preserve the report under the ordinary substantive-output policy, then present the terminal explanation. Quiet mode suppresses the report while retaining the explanation. Having a report does not imply a successful outcome, and returning a failure does not discard substantive output already prepared for delivery before the terminal step.

Debug output follows the same upstream disclosure and safe-rendering contracts. Enabling debug does not authorize secret disclosure or bypass owning-library rules.

Delivery scope

The completed feature includes the selected reporter and outcome API, ordered mode/progress selection through normal parsing, shared core presentation, and both optional integration migrations. Passive provider diagnostics distinguish independent messages from terminal explanations while preserving native evidence and rendering. Existing command behavior remains unchanged outside the selected reporting policy.

Maintained consumers demonstrate ordinary output, immediate/deferred messages, silent recovery, explicit warnings, failure after partial output, optional cause transport, and structured terminal explanations. Integration consumers include native diagnostic context and useful reports returned alongside failures.

Behavioral coverage establishes CommandKit-owned parsing, visibility, ordering, concurrent reporter coordination, closure, presentation observations, and failure handling. It preserves existing alias, input/state, discovery, native-report, and upstream-adapter protections without testing upstream domain correctness. GoDoc, CLI documentation, consumer guidance, internal documentation, and release notes describe the implemented boundary under their canonical ownership.

The implementation plan defines the concrete migration steps, focused validation, and completion criteria. This roadmap defines the target behavior and selected policy choices.

Architectural preparation completed

  • Review and accept ADR 0014 as an extension for shared reporting, retaining ADR 0005's outcome and supplied-error preservation decisions.
  • Align the architecture policy with the reporting ownership model, including application ownership of domain-specific error decisions and scope for useful shared error helpers.

OD-1 through OD-4 and the consumer API signatures are selected. The target end state includes maintained, executable consumers using those APIs; the implementation plan owns their delivery sequence.