Document configuration comparison behavior

This commit is contained in:
2026-10-11 22:00:22 +00:00
parent f0853d1000
commit 8b46e12da7
11 changed files with 170 additions and 123 deletions

View File

@@ -230,6 +230,63 @@ environment-variable name; effective and historical secret fragments remain
redacted. Empty location, inheritance, history, reason, and dependency lists are
serialized as `[]`.
### Compare configuration selections
`config diff` compares two independently loaded selections for one scope that
declares comparison. The default scope is `app`; `--scope all` is unsupported.
The PromptKit engine scope does not currently declare comparison, so selecting
`--scope promptkit` is a usage error.
`--config` and `--overlay` select the left side. `--against-config` and
`--against-overlay` independently override the right side. When a right-side
field is omitted, it inherits the corresponding left value and supplied state;
an explicit empty string overrides it and remains supplied. Equal selections are
allowed and both reached sides are loaded.
YAML is the only format and the default. Explicit `--format yaml` is accepted;
`text` and other formats are usage errors. For example:
```text
example config diff --config before.yml --against-config after.yml
example config diff --config base.yml --against-overlay candidate
example config diff --config base.yml --overlay current --against-overlay ""
```
ConfigKit compares frozen effective typed values after defaults and resolution;
source or provenance differences alone do not create a change. The report is one
YAML document with `additions`, `changes`, and `deletions` in that order.
Additions and changes preserve semantic paths and contain safe right-side values.
Deletions contain canonical JSON Pointer paths. Explicit null is a value; it is
not a deletion. Changed sequences and native structural replacement regions stay
atomic.
ConfigKit pairs sensitivity across both loads. A concealed change can therefore
appear even when its two safe snapshots are equal. The report omits separate
redaction and hidden-change metadata: a reported masked region changed, but that
does not mean every member inside it changed. Marker-looking public strings stay
ordinary YAML values. An empty comparison succeeds with this shape:
```yaml
additions: {}
changes: {}
deletions: []
```
The reduced report is a partial safe view, not a reloadable configuration or an
automatically applicable patch. Exceptional whole-root empty/null transitions
can be ambiguous because the report has no root envelope; ordinary field-level
null values and deletions retain their distinction.
Successful comparisons return status `0`, including concealed or empty results.
Unsupported scopes and format errors are usage errors; input checks retain the
existing usage/operational classification. Load and native comparison failures
are operational and preserve supported error identity. Comparison
diagnostics identify their left/right side and source legend. The adapter loads
left before right, retains left diagnostics if right loading fails, and gathers
both loads' diagnostics before native comparison. The complete report is
prepared before writing. Cancellation and preparation failures leave report
output empty; an output error or short write uses the existing incomplete-report
delivery outcome.
## PromptKit definition inspection
Applications can install prompt and profile inspection independently with

View File

@@ -103,18 +103,21 @@ values.
Use `ckconfig.Command(ckconfig.Suite[I, S]{...})` when the application needs
multiple named scopes, selector flags, scope-aware input checks, or shared
acquisition state. Input is initialized separately for each selected scope;
state is shared by selected acquisitions. Factory results remain caller-owned,
so separate initialization calls do not guarantee distinct objects. See
acquisition state or a custom comparison provider. A declared `Diff` capability
uses `Scope.Compare`; `ckconfig.FromConfigKit` and `AppCommand` provide the
ConfigKit-backed implementation. Each comparison side gets its own initialized
and checked input, while state is shared by the invocation. Factory results
remain caller-owned, so separate initialization calls do not guarantee distinct
objects. See
[ckconfig.Suite's GoDoc](../../integrations/ckconfig/contracts.go) for factory
timing, omitted-factory zero values (including nil pointers), and ownership.
The application owns the state type and what it caches. `ckconfig.FromConfigKit`
adapts a typed ConfigKit loader without calling it during construction.
The `config` family provides scope discovery, validation, native effective YAML,
source inventory, and path explanation for the capabilities declared by its
scopes. The [CLI reference](../cli.md#configuration-scopes) owns operator
syntax and report details.
source inventory, path explanation, and ConfigKit-backed comparison for the
capabilities declared by its scopes. The [CLI reference](../cli.md#configuration-scopes)
owns operator syntax and report details.
## PromptKit configuration and definitions

View File

@@ -134,7 +134,9 @@ external services:
```sh
go run ./examples/core workspace greet --count=0 --tag=first --tag second
(cd integrations/ckconfig/examples/simple && go run . config show)
(cd integrations/ckconfig/examples/simple && go run . config diff --config fixtures/before.yml --against-config fixtures/after.yml)
(cd integrations/ckprompt/examples/composed && go run . config validate --scope all --campaign spring --limit=0)
(cd integrations/ckprompt/examples/composed && go run . config diff --scope app --config fixtures/before.yml --against-config fixtures/after.yml)
(cd integrations/ckprompt/examples/composed && go run . promptkit show prompt briefing --campaign spring --format yaml)
(cd integrations/ckprompt/examples/composed && go run . session restore session-42 --campaign spring)
```

View File

@@ -40,10 +40,11 @@ Cobra and pflag imports remain inside this package.
## Optional modules
`integrations/ckconfig` owns ConfigKit adaptation, scope capture, the standard
`config` command family, and native validate/show/sources/explain reports. It
depends on the public core facade and ConfigKit; it does not depend on PromptKit.
Each selected scope receives an independently initialized input, while one
suite-owned state value is shared by selected acquisitions in that invocation.
`config` command family, and native validate/show/sources/explain/diff behavior.
It depends on the public core facade and ConfigKit; it does not depend on
PromptKit. Each selected scope or comparison side receives an independently
initialized input, while one suite-owned state value is shared by an invocation's
acquisitions or comparison callback.
`integrations/ckprompt` owns PromptKit engine loading, configuration-scope
adaptation, and optional named prompt/profile inspection. It uses public core

View File

@@ -1,8 +1,8 @@
# Adopt ConfigKit v0.7.1 and expose native configuration comparison
Status: end-state decisions selected; dependency adoption and `config diff` are
not implemented. This roadmap defines the selected target and remaining gap;
the [implementation plan](implementation.md) owns stages and execution order.
Status: ConfigKit v0.7.1 adoption and ConfigKit-backed `config diff` are
implemented. Distribution reconciliation and final validation remain in the
[implementation plan](implementation.md), which records the evidence and order.
## Purpose and authority
@@ -34,14 +34,15 @@ disclosure; CommandKit owns selection, coordination, adaptation, and presentatio
| Owner | Current state |
| --- | --- |
| Core module | Go 1.26.0; captured commands, typed bindings, dispatch, outcomes, help, and version. No ConfigKit dependency. ReleaseKit v0.4.0 is the declared maintainer tool. |
| `integrations/ckconfig` | Requires ConfigKit v0.5.0. `Scope`, `Suite`, `FromConfigKit`, and `AppCommand` expose scopes/validate/show/sources/explain. No comparison operation or provider exists. |
| `integrations/ckprompt` | Directly requires ConfigKit v0.5.0 and PromptKit v0.12.0. Depends on ckconfig's public scope contracts and adapts engine configuration plus named definitions. |
| Deferred comparison | [DIFF-01](deferred.md#diff-01-semantic-configuration-comparison) records the target. Native semantics, selection syntax, provider representation, and the YAML-only report are selected; implementation remains pending. |
| `integrations/ckconfig` | Requires ConfigKit v0.7.1. `Scope`, `Suite`, `FromConfigKit`, and `AppCommand` expose independently declared validate/show/sources/explain/diff capabilities. |
| `integrations/ckprompt` | Requires ConfigKit v0.7.1 and PromptKit v0.12.0. It depends on ckconfig's public scope contracts and adapts engine configuration plus named definitions; its ConfigKit scope does not declare comparison. |
| Comparison status | ConfigKit-backed `config diff` is implemented. The [CLI reference](../cli.md#compare-configuration-selections), integration GoDoc, and [implementation record](implementation.md) own current contracts and evidence. PromptKit comparison remains deferred. |
The existing ConfigKit adapter acquires a typed `*configkit.Result[T]` and exposes
its effective, source, and explanation facets through `View`. That generic
adapter is a useful place to retain typed results for comparison. `View` alone
does not supply two results of the same `T` to `configkit.Diff`.
The ConfigKit adapter acquires typed `*configkit.Result[T]` values and exposes
effective, source, and explanation facets through `View` for ordinary operations.
Its separate comparison callback retains `T`, loads both prepared selections,
and returns the native records from `configkit.Diff` without routing through
`View`.
ConfigKit v0.7.1 identifies source commit
`6fffde4af8f31f99d3ee295c426eb3234cb0fb77`. Relative to the selected v0.5.0:
@@ -53,15 +54,14 @@ ConfigKit v0.7.1 identifies source commit
runtime/API change from v0.7.0.
The YAML dependency remains `go.yaml.in/yaml/v4 v4.0.0-rc.6`. Existing public
loading and inspection signatures are unchanged. The review found no apparent
adapter migration requirement for existing operations; that is static review
evidence, not a completed upgrade validation. See the pinned
loading and inspection signatures are unchanged, and the integration suites pass
with the adopted dependency. See the pinned
[release notes](https://gitea.maximumdirect.net/eric/ConfigKit/src/commit/6fffde4af8f31f99d3ee295c426eb3234cb0fb77/docs/releases/v0.7.1.md)
and [comparison contract](https://gitea.maximumdirect.net/eric/ConfigKit/src/commit/6fffde4af8f31f99d3ee295c426eb3234cb0fb77/docs/config.md#effective-configuration-diffs).
## Dependency adoption and ownership
Both integration modules select ConfigKit v0.7.1 and carry normal dependency
Both integration modules now select ConfigKit v0.7.1 and carry normal dependency
metadata for their actual graphs. Consumer Go minimums remain 1.26.0, the selected
release toolchain remains 1.26.8, and PromptKit remains v0.12.0 for this work.
Core continues to have no ConfigKit or optional-integration dependency.
@@ -115,7 +115,7 @@ empty diff without an invented requirement that an override be supplied. Do not
short-circuit acquisition merely because the selections are equal: caller-owned
loading may produce different effective results.
For example, these show the accepted target syntax, not current commands:
For example, these are current invocations:
```text
app config diff --config before.yml --against-config after.yml
@@ -255,12 +255,12 @@ boundaries; do not promise interruption inside a synchronous upstream call.
### Comparison provider and ConfigKit adapters
Add a narrow optional comparison callback to `ckconfig.Scope[I, S]`. Its request
`ckconfig.Scope[I, S]` exposes a narrow optional comparison callback. Its request
groups each side's selection with its separately prepared typed input and supplies
one shared state value separately. Return
`commandkit.Acquired[[]configkit.Difference]` plus an error, following the existing
acquisition convention. Reuse upstream public record types rather than duplicate
them solely to rename fields. The selected target shape in ckconfig is:
acquisition convention. It reuses upstream public record types rather than
duplicating them solely to rename fields. The current API shape is:
```go
type ComparisonSide[I any] struct {
@@ -273,13 +273,13 @@ type ComparisonRequest[I any] struct {
Right ComparisonSide[I]
}
// Accepted target field on Scope[I, S]:
// Current field on Scope[I, S]:
// Compare func(context.Context, ComparisonRequest[I], S) (commandkit.Acquired[[]configkit.Difference], error)
```
These names and the callback shape are selected for implementation; they are not
implemented exports. ADR 0012 records paired provider ownership and input/state
lifetimes; this roadmap owns the target declarations until the Go API is implemented.
These are the implemented exports in `integrations/ckconfig/contracts.go`.
ADR 0012 records paired provider ownership and input/state lifetimes; GoDoc owns
the exact public declarations.
`AppCommand` and `FromConfigKit` provide native comparison alongside their existing
supported operations, with ordinary capability narrowing available. They use the
@@ -306,19 +306,18 @@ or loads need not run after failure. Declaration construction, installation, hel
version, and scope discovery leave input/state factories and acquisition/comparison
callbacks uninvoked.
ADR 0012 records this two-selection specialization of ADR 0011's suite lifecycle,
and the architecture policy reflects it. Document the exact implemented API
contract in Go declarations and GoDoc when it lands.
and the architecture policy reflects it. The exact API contract is in the Go
declarations and GoDoc.
### Application-owned state and caches
Generic result handling stays inside the owning adapter, without reflection,
private-core access, or result type assertions. Shared state supports consumer
caches whose keys distinguish the effective selections and any typed-input values
that affect loading. A cache
that stores only one configuration can incorrectly reuse the left result for the
right and report equality. Demonstrate correct keys in the maintained example;
CommandKit neither supplies nor repairs application caches. Cache policy and
factory objects remain caller-owned.
that affect loading. A cache that stores only one configuration can incorrectly
reuse the left result for the right and report equality. The composed example
demonstrates selection- and input-aware keys; CommandKit neither supplies nor
repairs application caches. Cache policy and factory objects remain caller-owned.
## Design tradeoffs
@@ -340,46 +339,15 @@ values while representing removals explicitly. Omitting empty categories would
shorten output but require missing-key handling; a separate text renderer adds no
needed operator value for this delivery.
## Work needed from the baseline
## Implementation record
These work areas close the gap from the current implementation to the selected
end state above. The [implementation plan](implementation.md) defines their
execution order and completion evidence.
- **Dependency adoption:** upgrade both integration requirements, reconcile sums
and indirect/tool dependencies, and assess PromptKit v0.12.0 against the newly
selected ConfigKit version. Preserve distributed manifests without sibling
replacements or committed workspace files.
- **Capability and provider API:** extend ckconfig's operation/capture/validation
model for the selected comparison provider. Preserve retained adapter errors,
capability narrowing, metadata-only scopes, and validation only of bindings
used by generated commands. Do not require an ordinary acquisition provider
for a comparison-only scope if its selected pair provider is sufficient.
- **Typed preparation:** capture comparison selection metadata and applicable
selector lists once. Prepare/check the selected pair through core bindings,
with state and callback timing from the chosen lifecycle. Keep caller-owned
objects and aliases under existing ownership rules.
- **Native adaptation:** extend the typed ConfigKit adapter/convenience factory
to load the selected pair and call `configkit.Diff`. Reuse diagnostic adaptation
and ordinary error handling without introducing a generic provider framework,
global cache, or untyped result transport.
- **Command and reports:** generate the conditional `config diff` declaration,
integrate it with discovery/help, and render the chosen report contract using
native safe values. Preserve existing operation/report behavior.
- **Documentation and distribution:** update GoDoc, CLI/consumer guidance,
maintained examples, affected module notices and pinned ConfigKit links, and
relevant release notes as implementation lands. Include a maintained comparison
example that loads genuinely different selections through shared state, with
cache keys accounting for the selection and typed-input values used by its
loader. Review transitive tool notices for the actual graph. Move DIFF-01's
implemented portion out of deferred work; retain unresolved future PromptKit
comparison and unrelated proposals.
- **Validation evidence:** run each affected module's deterministic offline suite
and representative consumers. Check module/package boundaries independently
with workspace mode off. If CommandKit prerequisite versions remain unpublished,
use disposable alternate module files for local-source wiring evidence, as
described in the [development guide](../development.md#documentation-validation).
Do not fabricate future checksums or turn migration checks into permanent gates.
Dependency adoption, capability and typed-pair preparation, native adaptation,
YAML reporting, end-to-end execution, and both maintained comparison examples
are implemented. Current API and operator contracts belong to GoDoc, the
[CLI reference](../cli.md#compare-configuration-selections), the
[consumer guide](../consumers/guide.md#configkit-scopes), and the example
READMEs. The [implementation plan](implementation.md) records stage-by-stage
changes and validation; distribution reconciliation and final validation remain.
### Focused behavioral evidence

View File

@@ -18,29 +18,20 @@ an idea here does not select its syntax, API, priority, or delivery date.
## Deferred capability targets
### DIFF-01: semantic configuration comparison
### PromptKit configuration comparison
Expose comparison of two independently resolved selections of one scope using
owning-library public comparison APIs. [ADR 0008](../adr/0008-semantic-configuration-diff.md)
owns native comparison, paired safe disclosure, and the reduced default report.
The target includes a YAML-only report of safe changed regions; completed
comparisons succeed whether or not they find differences.
ConfigKit-backed `config diff` is implemented by `ckconfig` for scopes that
declare that capability. `ckprompt.ConfigScope` does not declare comparison.
Reassess PromptKit's supported public comparison capabilities before planning a
PromptKit comparison. Do not reach into ConfigKit internals or infer comparison
semantics from rendered YAML.
The initial review of ConfigKit v0.5.0 and PromptKit v0.12.0 did not find the
public comparison support needed for that target. ConfigKit v0.7.0 introduced
`configkit.Diff`, retained in v0.7.1, which supplies native comparison and paired
safe changed-region snapshots. CommandKit still uses ConfigKit v0.5.0; dependency
adoption and comparison delivery are targeted by the
[ConfigKit roadmap](configkit.md). Assess PromptKit's supported
public comparison capabilities separately. Rendered YAML and sibling internals
are not substitutes for the owning library's public comparison contract.
### Comparison extensions
The ConfigKit roadmap records accepted selection syntax and the paired provider
API, with lifecycle recorded in
[ADR 0012](../adr/0012-typed-configuration-comparison-pairs.md). The selected report
shape is recorded in that roadmap. Independent per-side domain selectors and
an optional assertion mode remain outside that delivery. Add public types and
handlers with their implementation rather than exposing placeholders.
Independent per-side domain selectors and an optional assertion mode remain
unselected extensions to the delivered ConfigKit comparison. Consider them only
for a concrete consumer need and a deliberate API/behavior decision under the
relevant ADRs.
### CAT-01: PromptKit catalogs

View File

@@ -1,6 +1,6 @@
# Implement ConfigKit adoption and configuration comparison
Status: implementation in progress; Stages 1–10 are complete.
Status: implementation in progress; Stages 1–11 are complete.
## Scope and authority
@@ -545,9 +545,20 @@ canonical owners, while future/deferred work stays clearly separate.
symbols. Keep accepted ADR decision text unchanged; presentation details
remain in CLI/API owners rather than returning to ADRs.
**Completion evidence:** current operator/API/consumer owners agree with the
implemented behavior; no stale four-operation convenience claims, text-diff
promises, or deferred markers for delivered ConfigKit comparison.
**Completion evidence — complete:** ckconfig GoDoc now describes all five
operations, paired input lifetimes, shared state, native records, diagnostics,
and supported error identity. The ckconfig README, CLI reference, consumer guide,
internal overview, development examples, and maintained example READMEs describe
current ConfigKit comparison behavior. The CLI owner covers independent right
inheritance and supplied presence, native effective-value semantics, reduced
safe YAML categories, null/deletion, concealed regions, empty output, root
ambiguity, diagnostics, and outcome delivery. The ConfigKit feature roadmap now
records implementation status; the deferred record no longer carries ConfigKit
comparison as pending work and retains PromptKit comparison, independent per-side
domain selectors, catalogs, and other unselected ideas. The accepted ADRs were
left unchanged. Local Markdown targets/fragments and copied commands were
reviewed; a local link check and `git diff --check -- '*.md'` passed. `go doc`
was inspected for the exported comparison API.
## Stage 12: Reconcile manifests, notices, and pending integration release notes

View File

@@ -24,9 +24,12 @@ app, err := commandkit.New(identity, ckconfig.AppCommand(loadAppConfig))
Use `ckconfig.Command(ckconfig.Suite[Input, State]{...})` to define named
scopes, selectors, input checks, and state shared by the selected acquisitions
in a config invocation. `ckconfig.FromConfigKit` adapts a typed ConfigKit loader.
The `config` command family exposes scope discovery and the validate, show,
sources, and explain operations supported by the declared scopes.
or comparison in a config invocation. `ckconfig.FromConfigKit` adapts a typed
ConfigKit loader. The `config` command family exposes scope discovery and the
validate, show, sources, explain, and diff operations supported by the declared
scopes. Comparison returns ConfigKit's native records to the YAML report path;
the [CLI reference](https://gitea.maximumdirect.net/eric/CommandKit/src/branch/main/docs/cli.md#compare-configuration-selections)
owns its operator details.
The maintained [simple example](examples/simple) shows a complete consumer.
The [CommandKit CLI reference](https://gitea.maximumdirect.net/eric/CommandKit/src/branch/main/docs/cli.md#configuration-scopes)

View File

@@ -14,9 +14,10 @@ import (
// FromConfigKit adapts a typed ConfigKit loader to all five native
// configuration operations. The loader receives the operation's explicit
// Selection, initialized input, and shared invocation state, and runs only when
// an installed operation selects this scope. Construction and help do not call
// it. A nil original loader is retained as a construction issue, even if callers
// later narrow the scope's operation list.
// an installed operation selects this scope. Comparison invokes it separately
// for each reached side. Construction and help do not call it. A nil original
// loader is retained as a construction issue, even if callers later narrow the
// scope's operation list.
func FromConfigKit[I, S, T any](
name, summary string,
load func(context.Context, Selection, I, S) (*configkit.Result[T], error),

View File

@@ -17,7 +17,7 @@ type Selection struct {
Overlay commandkit.Supplied[string]
}
// Operation identifies a supported configuration inspection operation.
// Operation identifies a supported standard configuration operation.
type Operation string
const (
@@ -42,7 +42,8 @@ type Request struct {
}
// ComparisonSide groups one configuration selection with its independently
// prepared typed input.
// initialized and checked typed input. The supplied-state fields of Selection
// are preserved for the scope's loader.
type ComparisonSide[I any] struct {
// Selection carries the explicit config and overlay inputs for this side.
Selection Selection
@@ -51,7 +52,8 @@ type ComparisonSide[I any] struct {
}
// ComparisonRequest carries the earlier and later selections supplied to a
// scope's comparison callback.
// scope's comparison callback. Each side has a separately prepared input; the
// callback receives shared invocation state separately.
type ComparisonRequest[I any] struct {
// Left is the earlier configuration selection.
Left ComparisonSide[I]
@@ -99,7 +101,9 @@ type Scope[I, S any] struct {
// invocation.
Acquire func(context.Context, Request, I, S) (commandkit.Acquired[View], error)
// Compare compares two selections for this scope using one shared state.
// It is required when Diff is declared in Operations.
// Its Acquired result carries native difference records and diagnostics;
// returned errors retain their supported cause identity. It is required when
// Diff is declared in Operations.
Compare func(context.Context, ComparisonRequest[I], S) (commandkit.Acquired[[]configkit.Difference], error)
issues []error
@@ -116,24 +120,28 @@ type Selector[I any] struct {
// Suite declares the typed input, shared state, selectors, and configuration
// scopes used to assemble configuration commands.
//
// Input initialization is separate for each selected scope; state is shared
// by that invocation's acquisitions. Factory results and callback effects remain
// Input initialization is separate for each selected ordinary scope and each
// reached comparison side; state is shared by the invocation's acquisitions or
// comparison callback. Factory results and callback effects remain
// caller-owned: Command does not deep-copy arbitrary objects or repair aliases
// returned by factories. Core-owned repeated flag collections are copied when
// applied to separate inputs. Factories are not invoked during construction,
// installation, help, version, or scope discovery, and need not run after failure.
type Suite[I, S any] struct {
// NewInput is called once for each selected scope reached after operation
// preflight, before applying its selectors and calling CheckInput.
// NewInput is called once for each selected ordinary scope, or once for each
// reached side of a comparison, after operation preflight and before applying
// selectors and calling CheckInput.
// If nil, input is the zero value of I, including nil for pointer types.
NewInput func() I
// NewState is called once when acquisition is reached, after all selected
// inputs pass their checks. The result is shared by selected acquisitions.
// NewState is called once when provider execution is reached, after all
// selected inputs pass their checks. The result is shared by selected
// acquisitions or the comparison callback.
// If nil, state is the zero value of S, including nil for pointer types.
NewState func() S
// Selectors declares typed flags and their applicable scopes.
Selectors []Selector[I]
// CheckInput validates one initialized scope input after applicable values are applied.
// CheckInput validates one initialized scope input after applicable values
// are applied. Comparison sides are checked separately with their selection.
CheckInput func(scope string, selection Selection, input I) error
// Scopes declares the configuration scopes assembled by Command.
Scopes []Scope[I, S]

View File

@@ -43,9 +43,10 @@ type preparedSuite[I, S any] struct {
// Command eagerly captures a configuration suite as a root-only config family.
// It copies suite slices, retains callback references, and returns a reusable
// declaration; installation does not invoke factories or providers. During an
// inspection, it initializes input separately for each selected scope and
// validates those inputs before acquisition. Suite documents factory timing,
// zero values, shared state, and caller ownership of returned objects.
// inspection, it initializes input separately for each selected scope. A
// comparison initializes and checks both sides before creating shared state
// and calling the pair provider. Suite documents factory timing, zero values,
// shared state, and caller ownership of returned objects.
func Command[I, S any](suite Suite[I, S]) commandkit.Command {
prepared := captureSuite(suite)
if len(prepared.scopes) == 0 {
@@ -67,7 +68,8 @@ func Command[I, S any](suite Suite[I, S]) commandkit.Command {
// AppCommand builds the state-free, input-free convenience suite for application
// configuration. The application consumer supplies only its ConfigKit loader,
// which runs only for a selected operation after input preflight.
// which runs for ordinary operations and separately for each reached comparison
// side after input preflight.
func AppCommand[T any](load func(context.Context, Selection) (*configkit.Result[T], error)) commandkit.Command {
var adapterLoad func(context.Context, Selection, struct{}, struct{}) (*configkit.Result[T], error)
if load != nil {