diff --git a/README.md b/README.md index 7769dea..f40a630 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,17 @@ modules, so applications choose which upstream libraries they add. The core captures reusable command declarations, validates installation batches before changing an application's registry, converts typed inputs before constructing them, and returns outcomes without exiting the process. Its -[consumer guide](docs/consumers/guide.md) covers declaration and invocation. +[consumer guide](docs/consumers/guide.md) covers declaration, invocation +reporting, and migration. Each handler receives one invocation reporter for +substantive output, categorized messages, and terminal explanations; `Execute` +returns delivery observations with the outcome. The [CLI reference](docs/cli.md) +defines the shared visibility policy. ## Modules | Module | Package | Provides | | --- | --- | --- | -| `gitea.maximumdirect.net/eric/CommandKit` | `commandkit` | Typed commands, bindings, execution, outcomes, help, and version. | +| `gitea.maximumdirect.net/eric/CommandKit` | `commandkit` | Typed commands, bindings, execution reporting and outcomes, help, and version. | | `gitea.maximumdirect.net/eric/CommandKit/integrations/ckconfig` | `ckconfig` | ConfigKit-backed `config` scopes, including configuration source evidence. | | `gitea.maximumdirect.net/eric/CommandKit/integrations/ckprompt` | `ckprompt` | PromptKit engine configuration/comparison, five catalogs, named inspection, requirements discovery, concrete request validation, and native smoke tests. | diff --git a/doc.go b/doc.go index a28ee5e..7b56c48 100644 --- a/doc.go +++ b/doc.go @@ -1,6 +1,7 @@ // Package commandkit provides the core instance-owned CLI dispatcher, typed // command declarations, application-owned command and flag aliases, input -// bindings, and invocation outcomes. New and App.Register accept canonical +// bindings, invocation-scoped reporting, and outcomes with separate operation +// causes and presentation observations. New and App.Register accept canonical // Command declarations and RoutingRule declarations together. Optional // integration modules provide ConfigKit and PromptKit-specific commands; // importing those modules does not install their commands. diff --git a/docs/cli.md b/docs/cli.md index ad68d8f..b9ee2c0 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -66,6 +66,93 @@ version: dev revision: "" ``` +## Reporting + +CommandKit applies one shared reporting policy to application handlers, +integrations, help, version, and CommandKit's own invocation diagnostics. The +caller passes result and diagnostic writers to `App.Execute`; substantive +results go to `Streams.Out`, and categorized messages, terminal explanations, +and usage guidance go to `Streams.Err`. + +### Modes and visibility + +Each invocation starts in ordinary mode. `--quiet`, `--verbose`, and `--debug` +are mutually exclusive; the last enabled occurrence selects the mode. They +accept presence or an explicit boolean value. A `false` occurrence clears the +selected mode only when it names the current mode, returning to ordinary mode; +it does not restore an earlier selection. Invalid boolean values are usage +errors, even if a later flag would select another mode. + +| Category | `--quiet` | No mode flag | `--verbose` | `--debug` | +| --- | --- | --- | --- | --- | +| Substantive result | Suppressed | Shown | Shown | Shown | +| Terminal failure | Shown | Shown | Shown | Shown | +| Warning | Suppressed | Suppressed | Shown | Shown | +| Info | Suppressed | Suppressed | Shown | Shown | +| Debug | Suppressed | Suppressed | Suppressed | Shown | +| Progress, unless enabled explicitly | Suppressed | Suppressed | Suppressed | Shown | + +`--progress` independently enables Progress; `--progress=false` disables it. +Both accept the existing boolean syntax, and the last occurrence wins. An +explicit setting overrides the mode default, except quiet always suppresses +Progress. Without an explicit setting, debug enables Progress; ordinary and +verbose modes leave it disabled. Progress does not enable Info, Warning, or +Debug. + +### Placement and aliases + +Reporting flags are available on application commands, the root, canonical +groups, help, version, and alias-only discovery help. Place them after the +complete command path; they may be interspersed with operands after that path. +For example: + +```text +example export --quiet +example config show --verbose +example promptkit test all --debug --progress=false +example help config show --quiet +example version --quiet +``` + +Reporting flags do not go before or between command-path components. Tokens +consumed as values and tokens after `--` retain their normal meaning. + +The reserved long spellings are `--quiet`, `--verbose`, `--debug`, and +`--progress`; application declarations cannot reuse them. There are no built-in +short spellings. Applications may define aliases such as `-q → --quiet`, +`-v → --verbose`, and `-d → --debug`; canonical and aliased occurrences share +the same ordering. Thus `example export --verbose -q` selects quiet mode. + +### Channels and messages + +Substantive reports use the result destination. Warnings, Info, Progress, +Debug, terminal explanations, and usage guidance use the diagnostic destination. +Simple Warning, Debug, and terminal failure messages begin with `WARN:`, +`DEBUG:`, and `ERROR:` respectively. Info, Progress, and substantive results +have no prefix. For multiline messages, the prefix appears on the opening line; +native structured details keep their own formatting without duplicate labels. +Progress is streamed as unprefixed lines; terminal redraw and progress bars are +not provided. A failed outcome with no message or detail uses the fallback +`ERROR: Command failed.`. + +Quiet mode suppresses successful help and version output, as it does other +substantive results. Usage guidance and routing, parsing, and operational +failures remain visible in every mode. A recovered problem is reported only if +the application chooses to submit a Warning; an error cause by itself does not +create a message. + +### Presentation failures + +CommandKit continues rendering substantive output under quiet mode through a +discard destination. This suppresses writes to the caller's result writer while +still detecting report preparation, encoding, or rendering failures. An +intentional suppression is not a delivery failure. A failure while rendering or +writing requested content is retained in the returned `Outcome` and makes an +otherwise successful or usage invocation operational with status `1`. A domain +classification and its nonzero status are preserved; a zero-status domain +outcome becomes status `1` when presentation fails. Earlier bytes already +written remain visible. + ## Registered command syntax Options are parsed after the complete command path and may be interspersed with @@ -223,8 +310,8 @@ interrupt the native value. Existing native markers and leading comments are retained; documents without a marker receive one. Scope labels containing line control characters are quoted to keep them on one comment line. A provider failure leaves the output stream empty. A rendering or output failure may leave -an aggregate prefix, and the outcome reports -incomplete report delivery. +an aggregate prefix; `Outcome.Presentation.Output` records incomplete delivery +and the destination or rendering error. ### List configuration sources @@ -390,8 +477,9 @@ again. The complete report is prepared before writing. Report preparation failures and cancellation observed before report delivery starts leave CommandKit's report output empty. Once writing starts, CommandKit does not interrupt the supplied -writer or roll back delivered bytes. A write error or short write uses the -existing incomplete-report delivery outcome and can leave partial output. +writer or roll back delivered bytes. A write error or short write is recorded +as incomplete delivery in `Outcome.Presentation.Output` and can leave partial +output. ## PromptKit definition inspection @@ -607,8 +695,9 @@ execution. See PromptKit's Builder, engine-loading, and native validation errors use the operational path (status `1`) with supported error identity and acquired diagnostics retained. Malformed syntax, missing operands, and unsupported formats use usage status -`2`. Successful diagnostics and report-delivery failures follow the common -delivery contract. +`2`. Successful diagnostics use the common reporting policy. Report delivery +failures are recorded in `Outcome.Presentation.Output`, separately from the +operation cause. ### PromptKit catalogs @@ -802,8 +891,8 @@ outcome. Operation errors use operational status `1`; aggregate runs continue after individual errors, retain their reports and ordered error text, and join causes for supported `errors.Is`/`errors.As` access. Cancellation stops later operations and preserves results already acquired. Report encoding or delivery -failures use the common incomplete-report delivery outcome and retain operation -causes where applicable. Syntax, operand, unsupported format, and conflicting +failures are recorded in `Outcome.Presentation.Output`, separately from +operation causes. Syntax, operand, unsupported format, and conflicting namespace selection are usage status `2`. Native configuration, selection, and diagnostic meanings remain owned by @@ -816,4 +905,9 @@ and [engine API](https://gitea.maximumdirect.net/eric/promptkit/src/commit/9178f CommandKit usage failures use status `2`; operational failures use status `1`. Successful operations use status `0`. Application handlers may return a domain classification and status of their choice, which `Execute` preserves. The caller -chooses how to map an outcome to process exit. +chooses how to map an outcome to process exit. A rendering or delivery failure +changes a successful or usage outcome to operational status `1`. A domain +outcome keeps its classification and nonzero status; a zero-status domain +outcome becomes `1` if presentation fails. The returned `Outcome.Presentation` +records output and diagnostic delivery observations separately from its +operation `Cause`. diff --git a/docs/consumers/guide.md b/docs/consumers/guide.md index c0eaf13..35119c6 100644 --- a/docs/consumers/guide.md +++ b/docs/consumers/guide.md @@ -64,7 +64,9 @@ func Greeting() commandkit.Command { ), }, Run: func(invocation commandkit.Invocation, input GreetInput) commandkit.Outcome { - _, _ = fmt.Fprintln(invocation.Streams.Out, input.Count.Value) + if _, err := fmt.Fprintln(invocation.Reporter().Output(), input.Count.Value); err != nil { + return commandkit.Failure("Could not write the greeting.").WithCause(err) + } return commandkit.Success() }, }) @@ -85,6 +87,49 @@ values convert before input construction. A command's `NewInput` creates input only when execution reaches it; help does not construct caller input or run provider callbacks. +## Invocation reporting + +`Invocation.Reporter()` supplies the reporter for the current handler. Use +`Output()` or `RenderOutput()` for substantive results; use `Warn`, `Info`, +`Progress`, and `Debug` for categorized messages. Queue a summary with +`AtEnd().Info` when it belongs after the handler returns. The +[CLI reference](../cli.md#reporting) owns flag grammar, visibility, prefixes, +and status behavior. + +Return terminal text with `Failure(message, details...)`, `Usage(message, +details...)`, or `WithExplanation` on a domain outcome. Attach an optional Go +error separately with `WithCause`. A cause is preserved for inspection, but it +does not choose the outcome or become user-facing text. Rendering callbacks +write to their supplied writer only while the callback runs; they must not +submit another message or start another rendering operation through the same +reporter. + +```go +Run: func(invocation commandkit.Invocation, input GreetInput) commandkit.Outcome { + if _, err := fmt.Fprintln(invocation.Reporter().Output(), input.Count.Value); err != nil { + return commandkit.Failure("Could not write the greeting.").WithCause(err) + } + return commandkit.Success() +} +``` + +After `App.Execute`, inspect the returned `Outcome`. Its `Cause` retains the +operation error; `Presentation.Output` and `Presentation.Diagnostics` report +delivery state and the first rendering or write errors for their destinations. +CommandKit does not retain a message transcript. Reporter operations for one +invocation are safe concurrently, but concurrent submission order is not +specified. The caller owns the writers passed to `Execute`; CommandKit does not +close them. + +### Migrating from the previous reporting API + +| Previous API | Current API | +| --- | --- | +| `invocation.Streams.Out` / `.Err` | `invocation.Reporter().Output()` and categorized reporter methods. `Streams` remains the argument to `App.Execute`. | +| `Failure(err)` / `Usage(err)` | `Failure("operator-facing explanation").WithCause(err)` / `Usage("usage explanation").WithCause(err)`. Choose the explanation deliberately; the cause alone is not printed. | +| `Outcome.Diagnostics` or terminal `SeverityError` | Submit independent messages through reporter methods. Put failure context in `Explanation`; provider `Acquired.Diagnostics` remains passive data for the owning integration to submit. | +| `ReportIncomplete`, `DiagnosticDelivery`, or `DeliveryError` | Inspect `Outcome.Presentation.Output` and `.Diagnostics`, including each `DeliveryResult.State`, `RenderError`, and `WriteError`. | + ## Application-owned aliases Pass commands and aliases together as declarations. Integration factories can @@ -257,10 +302,11 @@ application-owned `session restore` command. ## Outcomes and ownership Core classifies usage and operational failures while preserving application -domain outcomes and supported error identity. Report and diagnostic delivery -errors remain inspectable separately from the operation cause. The caller owns -process startup, signal handling, and exit mapping; `Execute` never exits the -process. +domain outcomes and supported error identity. Applications choose domain +classification and status. A terminal explanation is presented separately from +the optional operation cause, while report and diagnostic delivery observations +are available in `Outcome.Presentation`. The caller owns process startup, +signal handling, and exit mapping; `Execute` never exits the process. Go declarations and GoDoc own exact exported contracts. ConfigKit owns configuration decoding, validation, and provenance. PromptKit owns prompt diff --git a/docs/development.md b/docs/development.md index a249a1e..b491bd6 100644 --- a/docs/development.md +++ b/docs/development.md @@ -2,8 +2,9 @@ CommandKit is distributed as a core Go module and two optional integration modules. Core owns captured declarations, typed input preparation, installation, -dispatch, outcomes, help, and version. `integrations/ckconfig` owns ConfigKit -scope adaptation and reports; `integrations/ckprompt` owns PromptKit engine, +dispatch, invocation reporting and outcomes, help, and version. +`integrations/ckconfig` owns ConfigKit scope adaptation and reports; +`integrations/ckprompt` owns PromptKit engine, definition inspection, requirements discovery, and concrete request validation. The maintained [consumer guide](consumers/guide.md) links to the three maintained consumers. @@ -53,6 +54,7 @@ For application-owned aliases, use the | Task | Read | | --- | --- | | Implemented CLI behavior | [CLI reference](cli.md) | +| Invocation reporting | [CLI reporting contract](cli.md#reporting), [consumer guide](consumers/guide.md#invocation-reporting), core GoDoc, [reporting roadmap](roadmap/reporting.md), and [ADR 0014](adr/0014-invocation-reporting.md) | | Application-owned aliases | [CLI reference](cli.md#application-owned-aliases), [Go declarations and GoDoc](../command.go), [consumer guide](consumers/guide.md#application-owned-aliases), and [ADR 0013](adr/0013-application-alias-routing.md) | | Public API usage and standard-suite wiring | [Consumer guide](consumers/guide.md), [ckconfig package](../integrations/ckconfig/doc.go), [ckprompt package](../integrations/ckprompt/doc.go), and applicable ADRs | | Dispatcher, registration, or typed inputs | [Command declarations](../command.go), [bindings](../binding.go), [invocation](../execute.go), and [ADR 0001](adr/0001-command-dispatch-and-inputs.md), refined by [ADR 0011](adr/0011-captured-declarations-and-installation.md) | diff --git a/docs/internal/overview.md b/docs/internal/overview.md index b5e28c3..b7a951f 100644 --- a/docs/internal/overview.md +++ b/docs/internal/overview.md @@ -10,7 +10,8 @@ this page inventories the implemented owners. The root facade owns captured canonical command declarations, application-owned routing rules, binding conversion, invocation coordination, the per-App -registries, standard help/version commands, and outcome/diagnostic delivery. +registries, standard help/version commands, invocation reporting, and outcome +finalization. Its dependency graph has no ConfigKit or PromptKit integration dependency. Applications provide typed callbacks and install routing rules alongside the complete canonical command tree through the same batch API. @@ -25,6 +26,30 @@ successful registration invalidates the snapshot for a later invocation. The root package owns app input and callback execution, not provider configuration semantics. +Each execution creates one reporter backed by the caller's result and diagnostic +writers. The Cobra adapter returns parsed values, flag spellings, and reporting +flag occurrences. Core reduces those occurrences in entered order after route +resolution, applies the final mode and progress selection, and presents any +collected parser events through that selection. The selected mode therefore +applies to command execution, help, and version; routing and parsing failures +remain visible in every mode. + +The reporter owns invocation-scoped visibility, message formatting, managed +report rendering, delivery observations, and finalization. It presents queued +messages after the handler returns, then a failed outcome's explanation, and +closes its handles before `Execute` returns. It records rendering and write +errors separately for result and diagnostic destinations; the returned +`Outcome.Presentation` exposes those observations independently from the +operation cause. The reporter does not retain a transcript and does not close +or flush caller-owned writers. + +Internally, one reporting-state mutex guards visibility, queued messages, +destination state, and delivery errors while serializing reporter operations +and writes. Rendering callbacks execute within that coordination boundary, so +they must not recursively submit or render through the same reporter. This is +an implementation detail supporting the callback contract; the mutex does not +coordinate writes made outside the reporter. + ## `internal/alias` and `internal/commandpath` `internal/commandpath` provides shared path parsing and token operations used by diff --git a/docs/releases/v0.3.0.md b/docs/releases/v0.3.0.md index 9fc6106..beddb1e 100644 --- a/docs/releases/v0.3.0.md +++ b/docs/releases/v0.3.0.md @@ -2,9 +2,11 @@ ## Summary -CommandKit v0.3.0 adds application-owned command and flag aliases. This -pre-1.0 minor release also updates the installation declaration type and -Invocation struct shape; review the migration guidance before upgrading. +CommandKit v0.3.0 adds application-owned command and flag aliases and shared +invocation reporting. This pre-1.0 minor release also updates the installation +declaration type and Invocation struct shape, replaces the error-first outcome +and diagnostic-delivery APIs, and reserves reporting flag names; review the +migration guidance before upgrading. ## Changes @@ -17,6 +19,14 @@ Invocation struct shape; review the migration guidance before upgrading. `EnteredPath` and `CanonicalPath`. - Interpret a canonical short value flag such as `-c=` as an explicitly supplied empty value, consistent with `--color=` and `-c ""`. +- Obtain an invocation-scoped `Reporter` from each handler's `Invocation` for + substantive output, Warning/Info/Progress/Debug messages, deferred messages, + and managed report rendering. The shared `--quiet`, `--verbose`, `--debug`, + and `--progress` flags apply across core and integration commands. +- Return terminal explanation text with `Failure`, `Usage`, or + `Outcome.WithExplanation`; attach an optional operation error separately with + `Outcome.WithCause`. Returned `Outcome.Presentation` records output and + diagnostic delivery observations without changing the operation cause. ## Compatibility and migration @@ -29,10 +39,26 @@ values whose parameter type is `...Command`, including wrappers assigned from `New` or `Register`, must update their signature to `...Declaration`. Adding private route fields to `Invocation` means positional struct literals no -longer compile. Use keyed fields, for example -`commandkit.Invocation{Context: ctx, Streams: streams}`, when constructing an -Invocation outside command execution. CommandKit populates the route fields for -executed handlers. +longer compile. `Invocation` no longer contains `Streams`; `App.Execute` supplies +the reporter and route paths to handlers. `Streams` remains the argument to +`App.Execute`, where the caller owns the result and diagnostic writers. + +Handlers that used `invocation.Streams.Out` or `.Err` should use +`invocation.Reporter().Output()` or the categorized reporting methods. Replace +`Failure(err)` and `Usage(err)` with an operator-facing explanation and an +optional `.WithCause(err)`; causes alone are not rendered. Replace +`Outcome.Diagnostics` submissions and terminal `SeverityError` records with +reporter messages and an `Outcome` explanation. Provider-returned +`Acquired.Diagnostics` remain passive data for the owning integration to +submit. Replace `ReportIncomplete`, `DiagnosticDelivery`, and `DeliveryError` +with `Outcome.Presentation.Output` and `.Diagnostics`, whose `DeliveryResult` +contains a state plus first render and write errors. + +The long flags `--quiet`, `--verbose`, `--debug`, and `--progress` are reserved +for CommandKit reporting and cannot be declared by applications. Application +aliases may target them. See the current [CLI reporting contract](../cli.md#reporting) +and [consumer migration guide](../consumers/guide.md#invocation-reporting) for +their behavior and API examples. The consumer minimum remains Go 1.26.0 and the maintainer release toolchain remains Go 1.26.8. Core v0.3.0 must be published before preparing dependent diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 11f4d4a..bbb290e 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Invocation reporting implementation plan -Status: Stages 1–18 complete. Stage 19 is next. +Status: Stages 1–19 complete. Stage 20 is next. ## Objective and authority @@ -719,7 +719,7 @@ on either optional integration module. ## Stage 19: Update canonical documentation and compatibility guidance -Status: pending. +Status: complete. 1. Update public Go declarations/GoDoc for exact reporter, outcome, provider, lifecycle, cause, and presentation-observation contracts. Describe callback @@ -749,6 +749,15 @@ Validation: review prose against implemented behavior, check local links and fragments, and run the Markdown whitespace checks from the developer guide. Current contract documentation must no longer present the removed APIs as valid. +Completion: updated exported GoDoc, the CLI reference, consumer migration +guidance, README, integration package documentation, developer guide, internal +overview, and the unpublished core v0.3.0 note. Current contracts direct callers +to the invocation reporter and keep operation causes separate from explanations +and presentation observations. Reviewed the prose against the implementation +and selected reporting policy; `go doc` checks, local Markdown link and fragment +checks, and both Markdown whitespace checks pass. Historical integration release +notes, accepted ADRs, and deferred-work records remain unchanged. + ## Stage 20: Validate the complete end state and finish Status: pending. diff --git a/docs/roadmap/reporting.md b/docs/roadmap/reporting.md index 1acce3f..c7aa64f 100644 --- a/docs/roadmap/reporting.md +++ b/docs/roadmap/reporting.md @@ -12,8 +12,8 @@ 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. -The [implementation plan](implementation.md) owns stage order and execution +started. Stages 1–19 are complete; final validation remains. The +[implementation plan](implementation.md) owns stage order and execution instructions. ## Purpose diff --git a/execute.go b/execute.go index 6f588ab..983bc81 100644 --- a/execute.go +++ b/execute.go @@ -14,10 +14,11 @@ import ( // Execute parses and dispatches one invocation. args excludes the executable // name. Supplied values are converted before command input construction; help -// does not construct caller inputs or run acquisition callbacks. CommandKit -// writes diagnostics to streams.Err and leaves process exit to the caller. A -// nil context returns an operational failure, and nil streams are replaced with -// io.Discard. +// does not construct caller inputs or run acquisition callbacks. It applies the +// parsed reporting selection, finalizes invocation presentation, and returns +// the resulting Outcome without exiting the process. Supplied streams remain +// caller-owned; nil destinations are replaced with io.Discard. A nil context +// returns an operational failure. func (a *App) Execute(ctx context.Context, args []string, streams Streams) Outcome { current := newExecution(a, ctx, streams) if a == nil { diff --git a/identity.go b/identity.go index bf300c8..15e0be9 100644 --- a/identity.go +++ b/identity.go @@ -18,8 +18,11 @@ type Identity struct { Revision string } -// Streams contains the output destinations associated with a command -// invocation. +// Streams contains the caller-owned output destinations associated with an +// invocation. Out receives substantive results; Err receives messages, +// terminal explanations, usage guidance, and CommandKit diagnostics. Nil +// destinations are replaced with io.Discard. CommandKit does not close or flush +// supplied writers. type Streams struct { // Out receives help and command reports. Out io.Writer @@ -37,10 +40,11 @@ func normalizeStreams(streams Streams) Streams { return streams } -// Invocation provides execution context and reporting to a command handler. -// For command aliases, EnteredPath and CanonicalPath expose the route the -// caller used and the selected canonical command path. Both omit the -// executable name, flags, and operands. +// Invocation provides execution context and invocation-scoped reporting to a +// command handler. Execute finalizes its reporter before returning. For command +// aliases, EnteredPath and CanonicalPath expose the route the caller used and +// the selected canonical command path. Both omit the executable name, flags, +// and operands. type Invocation struct { // Context carries caller cancellation and request-scoped values. Context context.Context @@ -50,7 +54,8 @@ type Invocation struct { canonicalPath string } -// Reporter returns the reporting facility for this invocation. +// Reporter returns the reporting facility supplied for this invocation. A +// manually constructed Invocation has no reporter. func (inv Invocation) Reporter() *Reporter { return inv.reporter } // EnteredPath returns the command path used to invoke the selected command, diff --git a/integrations/ckconfig/doc.go b/integrations/ckconfig/doc.go index 2d3b01c..464d244 100644 --- a/integrations/ckconfig/doc.go +++ b/integrations/ckconfig/doc.go @@ -3,5 +3,7 @@ // through the core commandkit package. Applications can install CommandKit // RoutingRule declarations alongside it through commandkit.New or // App.Register. Source reports preserve ConfigKit's destination, placement, -// and version-check evidence; importing ckconfig alone installs no commands. +// and version-check evidence. Invocation reporting applies CommandKit's shared +// visibility and delivery policy; provider warnings remain independent of +// failure explanations. Importing ckconfig alone installs no commands. package ckconfig diff --git a/integrations/ckprompt/doc.go b/integrations/ckprompt/doc.go index 36fe3a1..63472ee 100644 --- a/integrations/ckprompt/doc.go +++ b/integrations/ckprompt/doc.go @@ -4,5 +4,7 @@ // canonical commandkit.Command declarations. Applications can install // commandkit.RoutingRule aliases alongside them through commandkit.New or // App.Register. Importing ckprompt installs neither family; callers compose its -// declarations with core and ckconfig explicitly. +// declarations with core and ckconfig explicitly. Provider diagnostics remain +// passive until the owning command submits them through the invocation +// reporter, separately from any terminal explanation. package ckprompt diff --git a/outcome.go b/outcome.go index d55f2ff..78f7da7 100644 --- a/outcome.go +++ b/outcome.go @@ -6,7 +6,8 @@ import ( "strings" ) -// Severity classifies a nonterminal diagnostic. +// Severity classifies a nonterminal provider diagnostic. Terminal failure +// context belongs in an Outcome explanation instead. type Severity string const ( @@ -48,13 +49,18 @@ const ( DeliveryIncomplete ) -// ErrReporterClosed is returned when a reporting handle is used after invocation finalization. +// ErrReporterClosed is returned when a reporting handle is used after +// invocation finalization. var ErrReporterClosed = errors.New("reporter is closed") -// Detail renders optional content to the supplied writer. +// Detail renders optional content to the writer supplied by the reporter. +// Use that writer only during the callback. A callback must not submit another +// message, queue deferred work, or start another rendering operation through +// the same invocation's reporter. type Detail func(io.Writer) error -// Explanation contains terminal content for a failed outcome. +// Explanation contains terminal content for a failed outcome. CommandKit +// presents it during invocation finalization, including in quiet mode. type Explanation struct { // Message is the literal opening text. Message string @@ -62,7 +68,9 @@ type Explanation struct { Details []Detail } -// PresentationResult records delivery observations for the two output channels. +// PresentationResult records output and diagnostic delivery observations +// populated by CommandKit during invocation finalization. It is not a message +// submission path or a transcript. type PresentationResult struct { // Output records delivery to the result destination. Output DeliveryResult @@ -71,6 +79,9 @@ type PresentationResult struct { } // DeliveryResult records rendering and writing errors for one destination. +// The state distinguishes no submitted content, intentionally suppressed +// content, completed delivery, and incomplete delivery. RenderError and +// WriteError retain the first error of their kind for that destination. type DeliveryResult struct { // State summarizes content delivery to the destination. State DeliveryState @@ -80,7 +91,9 @@ type DeliveryResult struct { WriteError error } -// Diagnostic is passive nonterminal message data returned by a provider. +// Diagnostic is passive nonterminal message data returned by a provider. It +// does not emit output or select an invocation outcome; the owning integration +// submits it through the invocation reporter. // // Write renders the diagnostic to a supplied writer when the owning integration // submits it through the invocation reporter. A nil Write function has no detail. @@ -91,7 +104,9 @@ type Diagnostic struct { Write Detail } -// Acquired pairs a provider value with independent diagnostics and failure context. +// Acquired pairs a provider value with independent diagnostics and failure +// context. The owning command chooses how to submit each diagnostic and whether +// to attach the explanation to a failed outcome. type Acquired[T any] struct { // Value is the value returned by the acquisition callback. Value T @@ -101,11 +116,16 @@ type Acquired[T any] struct { Explanation Explanation } -// Outcome is the classified result of an operation. +// Outcome is the classified result of an operation. Constructing or returning +// an outcome does not itself write output; Execute presents its explanation +// during finalization. // // Cause records an optional operation error without selecting the outcome or // generating operator-facing text. Presentation records delivery observations -// separately. Invocation coordination treats a zero Outcome as success with status 0. +// separately. Invocation coordination treats a zero Outcome as success with +// status 0. Presentation failures change a success or usage outcome to +// operational status 1; a domain outcome keeps its classification and nonzero +// status, while a zero domain status becomes 1. type Outcome struct { // Kind classifies the result. Kind OutcomeKind @@ -126,28 +146,38 @@ func Success() Outcome { return Outcome{Kind: OutcomeSuccess} } -// Failure returns an operational outcome with status 1 and a terminal explanation. +// Failure returns an operational outcome with status 1 and a terminal +// explanation. The message and details are presented once when the outcome is +// returned from an invocation; an optional operation error can be attached with +// WithCause and is not rendered automatically. func Failure(message string, details ...Detail) Outcome { return Outcome{Kind: OutcomeOperational, Status: 1, Explanation: Explanation{Message: message, Details: cloneDetails(details)}} } -// Usage returns a usage outcome with status 2 and a terminal explanation. +// Usage returns a usage outcome with status 2 and a terminal explanation. The +// message and details are presented when the outcome is returned from an +// invocation; an optional cause is preserved separately with WithCause. func Usage(message string, details ...Detail) Outcome { return Outcome{Kind: OutcomeUsage, Status: 2, Explanation: Explanation{Message: message, Details: cloneDetails(details)}} } -// Domain returns an application-domain outcome with the supplied name and status. +// Domain returns an application-domain outcome with the supplied name and +// status. The application owns the classification and status decision. func Domain(name string, status int) Outcome { return Outcome{Kind: OutcomeDomain, Status: status, Domain: name} } -// WithExplanation returns a copy of the outcome with the supplied terminal explanation. +// WithExplanation returns a copy of the outcome with the supplied terminal +// explanation, replacing any existing explanation. CommandKit presents it once +// during finalization, including in quiet mode. func (o Outcome) WithExplanation(message string, details ...Detail) Outcome { o.Explanation = Explanation{Message: message, Details: cloneDetails(details)} return o } -// WithCause returns a copy of the outcome with the supplied optional cause. +// WithCause returns a copy of the outcome with the supplied optional operation +// cause. The cause is preserved for caller inspection and does not select an +// outcome or generate operator-facing text. func (o Outcome) WithCause(cause error) Outcome { o.Cause = cause return o diff --git a/reporter.go b/reporter.go index 395415a..5c680d4 100644 --- a/reporter.go +++ b/reporter.go @@ -60,16 +60,24 @@ type deferredMessage struct { details []Detail } -// Reporter coordinates presentation and delivery for one invocation. +// Reporter coordinates presentation and delivery for one invocation. The +// invocation supplies it; consumers do not construct or finalize it. Reporting +// operations on one reporter are safe concurrently, although concurrent +// submissions have no predetermined order. Rendering callbacks must not +// reenter the same reporter, and their supplied writer is valid only while the +// callback runs. // -// The invocation supplies the reporter. Consumers use Output for substantive -// command results and the reporting methods for categorized messages. +// Consumers use Output for substantive command results and the reporting +// methods for categorized messages. Delivery and rendering failures are +// retained for Outcome.Presentation even when a caller ignores an immediate +// method's returned error. type Reporter struct { state *reportingState } // DeferredReporter queues messages for delivery during invocation finalization. -// Copies share the same invocation reporting state. +// Copies share the same invocation reporting state. Submissions made after +// finalization return ErrReporterClosed. type DeferredReporter struct { state *reportingState } @@ -126,7 +134,10 @@ func (r *Reporter) applySelection(selection commandmeta.ReportingSelection) { state.progressSet = selection.ProgressSet } -// Output returns a managed writer for substantive command results. +// Output returns a managed writer for substantive command results. Writes are +// serialized with other operations through this reporter, and failures are +// recorded in Outcome.Presentation. The writer returns ErrReporterClosed after +// invocation finalization. func (r *Reporter) Output() io.Writer { if r == nil || r.state == nil { return managedOutputWriter{} @@ -142,6 +153,10 @@ func (w managedOutputWriter) Write(p []byte) (int, error) { } // RenderOutput coordinates a complete substantive report rendering operation. +// The callback receives a writer usable only during the callback and must not +// reenter this reporter. In quiet mode, the callback still runs against a +// discard destination so rendering failures are retained without writing to +// the caller's output destination. func (r *Reporter) RenderOutput(write func(io.Writer) error) error { if r == nil || r.state == nil { return ErrReporterClosed @@ -168,27 +183,33 @@ func (r *Reporter) RenderOutput(write func(io.Writer) error) error { return finishRenderLocked(state, destinationOutput, renderErr, writer.writeError) } -// Info submits an informational message. +// Info submits an informational message to the diagnostic destination. Its +// visibility follows the selected reporting mode. func (r *Reporter) Info(message string, details ...Detail) error { return r.submitMessage(categoryInfo, message, details) } -// Warn submits a warning message. +// Warn submits a warning message to the diagnostic destination. Its visibility +// follows the selected reporting mode. func (r *Reporter) Warn(message string, details ...Detail) error { return r.submitMessage(categoryWarning, message, details) } -// Debug submits a debug message. +// Debug submits a debug message to the diagnostic destination. Its visibility +// follows the selected reporting mode. func (r *Reporter) Debug(message string, details ...Detail) error { return r.submitMessage(categoryDebug, message, details) } -// Progress submits a progress message. +// Progress submits a progress message to the diagnostic destination. Its +// visibility follows the selected mode and explicit progress setting. func (r *Reporter) Progress(message string, details ...Detail) error { return r.submitMessage(categoryProgress, message, details) } -// AtEnd returns a view for messages delivered after the handler returns. +// AtEnd returns a view for messages delivered after the handler returns and +// before terminal explanation delivery. The deferred view shares this +// invocation's state. func (r *Reporter) AtEnd() DeferredReporter { if r == nil { return DeferredReporter{} @@ -196,17 +217,23 @@ func (r *Reporter) AtEnd() DeferredReporter { return DeferredReporter{state: r.state} } -// Info queues an informational message for invocation finalization. +// Info queues an informational message for invocation finalization. The +// submission error is returned immediately; rendering and delivery errors are +// reflected in the final outcome. func (r DeferredReporter) Info(message string, details ...Detail) error { return r.enqueue(categoryInfo, message, details) } -// Warn queues a warning message for invocation finalization. +// Warn queues a warning message for invocation finalization. The submission +// error is returned immediately; rendering and delivery errors are reflected in +// the final outcome. func (r DeferredReporter) Warn(message string, details ...Detail) error { return r.enqueue(categoryWarning, message, details) } -// Debug queues a debug message for invocation finalization. +// Debug queues a debug message for invocation finalization. The submission +// error is returned immediately; rendering and delivery errors are reflected in +// the final outcome. func (r DeferredReporter) Debug(message string, details ...Detail) error { return r.enqueue(categoryDebug, message, details) }