Plan shared debug binding and reporting mode access

This commit is contained in:
2026-11-08 13:52:48 +00:00
parent 69257066e2
commit ec0e58efff

View File

@@ -0,0 +1,233 @@
# Shared debug binding and reporting-mode access
Status: agreed capability direction; not implemented. Public binding details
remain open below. Baseline: `69257066e21c12ea3492237ebee072598adea60c`.
## Purpose and authority
Applications should be able to receive the boolean value of CommandKit's
`--debug` flag through normal typed input binding. Applications that need the
resolved reporting mode should also have a read-only invocation API.
The [Audita request](audita-feature-request.md) motivated this work. Subsequent
discussion selected a broader distinction: application debug behavior and CLI
reporting visibility are separate choices. Audita's original requirement that
capture follow reporting mode is an application policy, not a CommandKit
requirement. This roadmap records the agreed direction from that discussion;
it does not treat every proposal in the original request as accepted.
The [architecture policy](../policy/architecture.md#ownership-boundaries) and
[ADR 0006](../adr/0006-invocation-reporting.md) already assign CLI mechanics to
core and domain behavior to applications. This work fits those boundaries and
does not require another ADR. Implemented behavior remains documented in
[Go declarations](../../command.go) and the [CLI reference](../cli.md#reporting)
until this work lands.
## Current baseline
| Area | Current implementation | Work needed |
| --- | --- | --- |
| Typed binding | `BoolFlag` in [binding.go](../../binding.go) converts supplied occurrences and applies the last boolean with `Supplied[bool]` presence. | Let a binding receive the existing core debug flag's input. |
| Registration | `FlagMeta` and `validateBindings` in [command.go](../../command.go) reserve every reporting name, including `debug`. | Recognize the selected shared-binding form without treating it as an independent competing flag. |
| Parser metadata | [commandmeta](../../internal/commandmeta/metadata.go) separates `CoreFlags` from consumer-indexed `Flags`; [parser.go](../../parser.go) supplies core flags to each command. | Preserve consumer binding identity and indexes while sharing one parser declaration. |
| Backend | [internal/cobra/parse.go](../../internal/cobra/parse.go) registers both lists, records occurrences, and extracts indexed values only for consumer flags. | Supply shared debug values and spellings to the participating binding without duplicate backend registration. |
| Reporting | `reduceReportingOccurrences` in [parser.go](../../parser.go) resolves the mutually exclusive mode from ordered occurrences. | Preserve that reduction independently of the bound debug boolean. |
| Invocation | [execute.go](../../execute.go) applies reporting selection before input preparation and execution. [reporter.go](../../reporter.go) stores the mode privately; [Invocation](../../identity.go) exposes no mode query. | Expose the already resolved selection through a CommandKit-owned public type. |
Simply permitting the name in registration is insufficient: the backend would
otherwise receive two declarations for `debug`. Existing occurrence capture is
the starting point; applications should not scan or reparse arguments.
## Targeted end state
### Bind the debug boolean independently
An application opts into receiving `--debug` in its command input through a
`Flag[I]` and the existing typed setter model. Exact construction and metadata
rules are subject to OD-1 and OD-2. Commands without such a binding retain the
standard core flag and reporting behavior.
The ordinary shared binding receives `Supplied[bool]`: omission produces
`Value=false, Set=false`; an explicitly supplied false produces
`Value=false, Set=true`. Repeated debug occurrences follow ordinary boolean
binding semantics: every supplied value must be valid, and the last value wins.
Other reporting flags do not rewrite the bound debug value.
The following examples appear after a complete command path. They show the
ordinary false-default binding and the existing mode-selection rules together:
| Arguments | Debug value | Supplied | Resolved reporting mode |
| --- | --- | --- | --- |
| None | false | false | Ordinary |
| `--debug` | true | true | Debug |
| `--debug --quiet` | true | true | Quiet |
| `--quiet --debug` | true | true | Debug |
| `--debug --verbose` | true | true | Verbose |
| `--debug --debug=false` | false | true | Ordinary |
| `--verbose --debug --debug=false` | false | true | Ordinary |
| `--debug --quiet=false` | true | true | Debug |
| `--debug --progress=false` | true | true | Debug |
Aliases use the same canonical debug input. Tokens consumed as other flag
values or appearing after `--` keep their existing meaning. Invalid reporting
booleans remain usage failures without handler execution. Existing placement,
help, and input-preparation timing continue to apply.
Application code can use the bound value for diagnostic artifacts even when
quiet mode suppresses ordinary CLI output. Core continues to own reporting
visibility; applications own capture, redaction, file lifecycle, and any other
domain effects. No capture policy is imposed on Audita or other applications.
### Query the resolved reporting mode
Add a CommandKit-owned `ReportingMode` type with ordinary, quiet, verbose, and
debug values, and one canonical accessor:
```go
func (inv Invocation) ReportingMode() ReportingMode
```
Use `ReportingModeOrdinary`, `ReportingModeQuiet`, `ReportingModeVerbose`, and
`ReportingModeDebug` as the planned constant names, with ordinary as zero.
The accessor reads the existing selection without output or mutation. The mode
is final when the handler receives the invocation and remains its selection
after execution finishes. A manually constructed zero-value invocation returns
ordinary. Reads are compatible with concurrent reporting on that invocation;
this does not add concurrent dispatch support or extend writer lifetimes.
An application deliberately following reporting visibility can use:
```go
captureArtifacts := inv.ReportingMode() == commandkit.ReportingModeDebug
```
This reports the selected mode, not whether output was delivered successfully.
Progress and report format remain separate selections. Keep the public type
independent of internal/backend types and avoid a second mutable copy of the
resolved mode. A second accessor on `Reporter` is unnecessary for this scope.
## Implementation work
1. **Settle binding construction and metadata.** Resolve OD-1 and OD-2 before
making the registration exception. Document which declaration attaches to
the core flag and how its application metadata is handled. Preserve ordinary
validation for unrelated names and bindings; do not broadly disable reserved
name or collision checks.
2. **Connect shared parsing to typed input.** Update declaration validation,
metadata bridging, and backend value extraction together. Register one
effective debug flag, retain canonical occurrence order and supplied
spellings, and deliver its values through normal input preparation. Keep
reporting reduction based on the same parsed occurrences, including for
commands that have no application debug binding. Reuse current mechanisms;
a general observer registry, alternate parser, or subscription lifecycle is
not needed for this feature.
3. **Preserve binding composition.** Review `CaptureFlags`, `FlagValues.ApplyTo`,
and `ApplySuppliedTo` in [flag_values.go](../../flag_values.go). A shared
binding should participate through the existing identity and presence
machinery. Review `RenameFlag` if a special binding marker is introduced:
renaming to an ordinary application name must not accidentally keep reading
core debug input. Avoid adding separate storage or application callbacks just
to carry this boolean.
4. **Expose the mode query.** Reuse reporter state and its synchronization as
appropriate; document the zero-value, invocation lifetime, and concurrent-read
behavior. Do not expose backend objects or add public mutation APIs.
5. **Update canonical documentation when implemented.** Update binding and
invocation GoDoc, the reserved-name wording in `FlagMeta`, and the
[CLI reference](../cli.md#placement-and-aliases). Add a short example to the
[consumer guide](../consumers/guide.md#invocation-reporting) contrasting a
bound debug boolean with an explicit mode query. Explain application effects
in application help using the metadata choice from OD-2. Record the core API
addition in the applicable release note. Retire this roadmap through the
normal documentation lifecycle when complete.
This is core work. It does not require ConfigKit or PromptKit API additions or
adoption of Audita's artifact implementation. Binding other core flags, adding
mode setters, or creating a general flag-sharing framework is not part of this
sprint; that scope limit is not a permanent prohibition on later capabilities.
## Validation and completion checklist
Follow the [testing policy](../policy/testing.md): test the public behavior and
reuse existing parser and reporting coverage rather than duplicate it wholesale.
The table above supplies representative boundary expectations, not a request
for separate copies of the same cases at every layer.
- [ ] A registered application command receives the shared debug boolean and
presence through normal typed input preparation, including when quiet or
verbose becomes the resolved reporting mode.
- [ ] The public query returns the selected mode without changing visibility;
cover zero value, retained invocation, and fresh selection on subsequent runs.
- [ ] Focused coverage establishes safe mode reads during existing concurrent
reporter use, without asserting concurrent dispatch support.
- [ ] Exercise aliases, explicit false, repeated values, invalid booleans, and
option-looking operands/values where they establish the new binding boundary.
Preserve existing failure precedence and no-handler-on-parse-failure behavior.
- [ ] A command with both shared debug and ordinary flags receives the correct
values; help and completion describe one effective debug option. Exercise
representative capture/application composition and any affected rename path.
- [ ] Test only the metadata/collision decisions actually selected below;
replace obsolete blanket rejection assertions for the allowed binding form.
- [ ] Run focused binding/parser/reporter tests, then the core suite and aligned
integration suites using the [developer guide](../development.md). The shared
parser change justifies checking that standard integration commands still
register and execute. No live providers or Audita processing tests are needed.
- [ ] GoDoc and consumer guidance distinguish boolean input from reporting
mode, and canonical docs describe the implemented registration exception.
For this documentation pass, review baseline accuracy, scope, links, and
whitespace only. Implementation and the unchecked items above remain future work.
## Open Decisions
### OD-1: How does an application declare the shared binding?
**Recommendation:** recognize a normal `BoolFlag` with `FlagMeta.Name` set to
`debug` as attachment to the existing core flag. It returns the same `Flag[I]`,
uses `Supplied[bool]`, and occupies the normal command binding list. This most
directly meets the request that using debug should feel like using other flags
and avoids another public constructor. The cost is special handling of a
core-owned name, particularly the metadata rules in OD-2. Do not interpret an
arbitrary string-valued or custom-parser flag named `debug` as equivalent merely
because its name matches.
**Viable alternative:** add a dedicated constructor such as
`DebugFlag[I](set func(*I, Supplied[bool])) Flag[I]`. This still uses the ordinary
typed binding and composition path, but makes attachment explicit and avoids
exposing conflicting declaration options. It adds public API vocabulary and
makes debug construction different from other booleans. Prefer this if ordinary
`BoolFlag` reuse needs substantial metadata reconciliation or confusing exceptions.
The example constructor name is illustrative; choose one binding form rather
than adding both entry points without a demonstrated need.
### OD-2: Which application metadata can accompany a shared flag?
**Recommendation:** retain the core boolean grammar and false default. Allow
command-local usage text to explain additional application effects alongside
core reporting help, and allow a local short spelling through existing alias
and collision rules. Keep one displayed option. Continue using core boolean
completion. Under the ordinary `BoolFlag` approach, report conflicting default,
requiredness, or completion overrides as declaration errors rather than silently
ignoring them. A dedicated constructor could instead omit those options. These
are proposed rules for the shared attachment only, not accepted new restrictions
on ordinary application flags.
This preserves consistent debug input across applications and avoids help that
claims a default or requirement different from the shared flag. Its tradeoff is
that a normal-looking declaration would accept fewer metadata choices.
**Viable alternative:** let application defaults and requiredness affect only
the receiving binding, using ordinary binding semantics, while core reporting
continues to follow explicit occurrences. That gives applications more familiar
flexibility: for example, an application default of true could enable its own
diagnostics without selecting debug reporting. However, omission would no longer
necessarily mean a false bound value, and help would need to distinguish an
application default from the core reporting default. If selected, revise the
omission contract above and explain this distinction explicitly; do not silently
allow local metadata to change core reporting selection.
**Simpler help alternative:** retain core flag help unchanged and put application
effects in the command description, with short spellings supplied through the
existing `FlagAliasFor` mechanism. This avoids help merging, at the cost of less
convenient flag-local documentation. It combines naturally with the dedicated
constructor in OD-1. There is no need for a general metadata-merging framework.