From 1d29cdd0291f315641eac92d8c8e57039dc6df7d Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Mon, 26 Oct 2026 01:25:11 +0000 Subject: [PATCH] Clarify provider diagnostics and reporting edge cases --- docs/roadmap/reporting.md | 99 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 96 insertions(+), 3 deletions(-) diff --git a/docs/roadmap/reporting.md b/docs/roadmap/reporting.md index 24ed52c..afa3e37 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 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 and presentation-result model below are -also selected. Architectural preparation is complete: +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](../adr/0014-invocation-reporting.md) is accepted, and the [architecture policy](../policy/architecture.md) is aligned. Implementation has not started; implementation, validation, and maintained consumer examples remain. @@ -167,6 +168,8 @@ 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, @@ -544,6 +547,21 @@ 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. @@ -651,6 +669,26 @@ The named renderers above have signature `func(io.Writer) error` and preserve th 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: @@ -672,6 +710,42 @@ 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: + +```go +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 ```go @@ -786,7 +860,9 @@ rules. terminal failure with an explicit explanation and no Go error cause. Include simple and structured failure construction without early emission, optional cause transport independent of operator text, and an integration - returning both a substantive report and a failure. + returning both a substantive report and a failure. Include provider-returned + independent warnings and terminal context, unexplained failures receiving the + fallback, and rendering helpers using only the supplied callback writer. 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`. @@ -796,6 +872,10 @@ rules. 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. + Adapt provider-returned `Acquired[T]`, `Diagnostic`, and `Severity` contracts + to independent nonterminal messages and separate terminal explanations. + Replace whole-batch severity promotion with deliberate separation of failure + context and unrelated messages. Providers remain independent of the reporter. 5. Verify classification, flag selection, suppression, terminal explanations, ordering, duplicate prevention, terminal Error/non-zero-outcome consistency, debug visibility, and write-failure handling with focused behavioral tests and @@ -833,6 +913,19 @@ rules. processing and subsequent presentation failures, including wrapped errors inspected with `errors.Is` and `errors.As`. Verify causes alone do not produce messages or change statuses or visibility. + Verify provider-returned data remains unpresented until submitted by the + integration, including data returned alongside an error. Cover mixed batches + where terminal context remains visible in quiet mode and unrelated warnings + remain suppressed. Verify generic fallback presentation and retention for + failed outcomes with no message or non-nil detail renderer, with and without + a cause. Check unchanged classifications, statuses, and causes when fallback + delivery succeeds, and existing presentation-failure rules when it fails. + Cover native details with an empty heading, including a renderer that writes + no bytes, without adding a fallback or replaying callbacks to inspect content. + Exercise callback helpers using the supplied writer, messages submitted before + and after rendering, and ordinary concurrent submissions serialized around a + complete rendering operation. Document the callback boundary without requiring + runtime detection of recursive reporter use. 6. Update GoDoc, the CLI reference, consumer guidance, and release documentation when implemented. Exact reporting APIs and flag syntax remain API/CLI contracts.