diff --git a/docs/roadmap/reporting.md b/docs/roadmap/reporting.md index 4e07f50..169f84c 100644 --- a/docs/roadmap/reporting.md +++ b/docs/roadmap/reporting.md @@ -7,8 +7,9 @@ OD-2's unified reporting direction, the terminal failure model, and application ownership of error handling 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. Exact API names, signatures, and representations remain to be -finalized. Implementation has not started. +selected. The consumer API signatures and presentation-result model below are +also selected. Implementation has not started; architectural contract +reconciliation and maintained implementation examples remain work to complete. ## Purpose @@ -158,7 +159,7 @@ 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. Exact API names and signatures remain to be finalized. +error. The selected API below provides failure constructors for this purpose. Output streamed before the failure remains visible according to its selected mode; terminal failure does not withdraw earlier bytes. Deferred messages, @@ -189,7 +190,7 @@ 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 concrete API must express this distinction for both simple and structured +The selected API expresses this distinction for both simple and structured explanations. For example: | Handler action | Presentation | @@ -473,26 +474,14 @@ 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 direction +## 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 facility exposes interfaces appropriate to each need: - -| Need | Selected direction | Illustrative shape | -| --- | --- | --- | -| Substantive results | An ordinary `io.Writer`, usable by encoders and native report writers. | `inv.Reporter().Output()` | -| Immediate messages | Categorized methods applying visibility and presentation automatically. | `Info`, `Warn`, `Debug`, `Progress` | -| End-of-run diagnostics | An explicit deferred view sharing the invocation's reporting state. | `report.AtEnd().Warn(message)` | -| Rich upstream diagnostics | Typed submission preserving severity, context, and native rendering. | Exact API remains open. | - -These method names illustrate the selected direction; exact names and signatures -remain to be finalized. Progress is streaming output, distinct from deferred -informational summaries. Applications need not implement categories they do not -use or check verbosity before reporting. +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 @@ -501,26 +490,224 @@ 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. -The returned outcome owns the operation result: success, failure, or an -application domain classification. CommandKit presents a failed outcome through -the same reporting facility as the terminal step. Error explanations belong to -that returned failure, including rich upstream explanations. Typed diagnostic -submission must preserve this relationship instead of permitting independently -emitted Error messages during normal execution. Application failures carry -explicit status and presentation content; they do not require a Go error cause. - Use the reporter as the common diagnostic submission path, replacing the earlier proposal to split submission between a reporter and `Outcome.Diagnostics`. -The returned outcome carries terminal explanations, with other retention limited -to the presentation needs described above. Caller-selected destinations belong -at the execution boundary; the handler-facing API routes output through the -reporter instead of requiring a parallel raw-stream API. +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. -Immediate methods expose delivery errors and the reporter retains delivery -failure state for automatic finalization, as selected in OD-4. Standard writer -operations remain compatible with `io.Writer`. Exact API names, signatures, and -outcome representations will express these behaviors through the selected -consumer-facing structure. +### Invocation reporter and structured detail + +```go +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. + +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: + +```go +_ = 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 + +```go +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: + +```go +_ = 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: + +```go +type Explanation struct { + Message string + Details []Detail +} + +type Outcome struct { + Kind OutcomeKind + Status int + Domain string + 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 +``` + +`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` preserves an application-defined classification and status. +`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: + +```go +return commandkit.Failure("Unable to export the requested report.") +``` + +```go +return commandkit.Failure("", writeNativeFailure) +``` + +```go +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. + +The target outcome has no application `Cause` field. Applications supply their +chosen explanation; CommandKit records its own presentation failures separately. +The returned outcome retains the terminal explanation, with other retention +limited to the presentation needs described above. + +### Presentation observations and closed reporters + +```go +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, verbosity checks, or error-conversion helpers. + +### Application example + +This target-API handler uses an application-owned `exportData` operation that +returns a JSON-encodable result with a `UsedDefaults` field: + +```go +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 @@ -561,10 +748,10 @@ rules. 2. Review current diagnostic batching, parsing, help, outcome presentation, and writer ownership to identify the smallest shared changes. Include invocation failures that occur before a handler receives its `Invocation`. -3. Finalize the unified reporting API and flag contracts, including interactions - with command and flag aliases, following OD-1 through OD-4 above. Replace split - handler submission paths and adapt core and integration callers. Account for - flag collisions and document changed public contracts. +3. Implement the selected unified reporting API and flag contracts, including + interactions with command and flag aliases, following OD-1 through OD-4 above. + Replace split handler submission paths and adapt core and integration callers. + Account for flag collisions and document changed public contracts. 4. Implement the selected behavior across core and the integrations. Preserve upstream rendering and semantics while applying the chosen visibility policy. 5. Verify classification, flag selection, suppression, terminal explanations, @@ -593,6 +780,10 @@ rules. that fail without writing and continued terminal delivery through a healthy diagnostic destination. Cover usage-status preservation on presentation failure, all category destinations, and usage guidance under quiet mode. + Verify the selected API's empty-heading native rendering, ordered details, + deferred submission errors, replacement by `WithExplanation`, and delivery + states for each destination. Check bounded first-error observations, rendering + through `RenderOutput`, and `ErrReporterClosed` for retained handles in quiet mode. 6. Before changing architectural contracts, reconcile the selected error-handling boundary with ADR 0005's existing outcome and error-identity commitments and the corresponding commitments in the [architecture policy](../policy/architecture.md). @@ -600,10 +791,8 @@ rules. the CLI reference, consumer guidance, and release documentation when implemented. Exact reporting APIs and flag syntax remain API/CLI contracts. -## Remaining design work +## Remaining preparation -OD-1 through OD-4 are resolved. Before implementation, finalize the concrete -reporter and outcome API, including simple and structured terminal explanations, -deferred submission, and delivery-failure observations. Review complete examples -against the selected behavior and reconcile the architectural contracts noted -above. +OD-1 through OD-4 and the consumer API signatures are selected. Reconcile the +architectural contracts noted above before changing them, and turn the target-API +examples into maintained, executable consumers as implementation proceeds.