Files
commandkit/docs/roadmap/configkit.md

25 KiB

Adopt ConfigKit v0.7.1 and expose native configuration comparison

Status: proposed feature roadmap; dependency adoption and config diff are not implemented. The targeted delivery includes both, as requested by the user. This document defines the selected end state; a subsequent staged implementation plan will define execution order.

Purpose and authority

Adopt ConfigKit's current loading and inspection improvements and expose its native effective-configuration comparison through CommandKit. Operators should be able to compare two selections of one configuration scope, including the effect of a selected overlay, without applications writing duplicate CLI or comparison infrastructure.

The revised ADR 0008 owns native comparison and paired safe disclosure. ADR 0009 owns scope selection and independent capabilities; ADR 0011 owns captured declarations, typed input, and shared state, refined for comparison pairs by ADR 0012. The architecture policy controls ownership and dependency direction. The current ADR alignment edits remain uncommitted for user review; this roadmap follows their agreed comparison direction.

The repository is being developed as a pre-release alpha. Choose the simplest coherent final API without preserving prototype shapes through compatibility wrappers. Existing deliberately selected contracts still control; changes to published releases follow the release policy.

This work consumes ConfigKit's supported public API. No upstream extension is required for the selected comparison goal. ConfigKit owns comparison and safe disclosure; CommandKit owns selection, coordination, adaptation, and presentation.

Current baseline

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 records the target. Native semantics, selection syntax (OD-1), provider representation (OD-2), and the YAML-only report (OD-3) are accepted.

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.

ConfigKit v0.7.1 identifies source commit 6fffde4af8f31f99d3ee295c426eb3234cb0fb77. Relative to the selected v0.5.0:

  • v0.6.0 reduces repeated loading, resolution, metadata, and inspection work. Its Go 1.26.0 minimum is already met by CommandKit.
  • v0.7.0 adds native frozen effective-value comparison.
  • v0.7.1 changes the upstream maintainer tool to ReleaseKit v0.4.1, with no 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 release notes and comparison contract.

Targeted end state

Dependencies and module boundaries

Both integration modules 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.

ConfigKit's declared tool requirement brings ReleaseKit v0.4.1 into the integration module graphs. Reconcile that selection through normal Go module resolution and accurate attribution. A combined development workspace can also select that version. This does not by itself change the core module's separately declared release tool or authorize release-launcher changes; assess core through its own workspace-off graph.

Independent module versions remain independent of upstream ConfigKit's version. Release preparation uses appropriate unused versions under the existing policy, without moving tags or claiming unpublished dependencies are available.

Ownership

Owner Target responsibility
ConfigKit Frozen effective-value equality, changed-region selection/order, sensitivity union, paired safe snapshots, hidden-change detection, and load semantics.
ckconfig Comparison capability and provider contracts; typed ConfigKit adapter; config diff input preparation, acquisition coordination, reports, and upstream diagnostic adaptation.
Core Existing declaration installation, flag conversion/presence, help, dispatch, cancellation context, and outcome/delivery mechanics. No comparison model or engine.
ckprompt Continue existing integration behavior with the upgraded shared dependency and ckconfig API. Do not claim native PromptKit comparison support through this upgrade.
Consuming application Configuration definitions, acquisition callback internals, loader/cache choices, domain interpretation, startup, and process exit.

ConfigKit-backed comparison

The config family exposes diff when at least one scope declares the capability. Help and config scopes report actual availability. A scope can support comparison independently of other operations; enabling comparison elsewhere does not require every scope to support it. Unsupported selections fail as usage errors before acquisition.

config diff selects one scope, defaults to app, and does not accept --scope all, as selected by ADR 0009. It compares earlier/left and later/right effective configurations. Both file comparison and base-versus-overlay comparison are supported through ordinary application loading choices. Exact side selection and inheritance rules are accepted in OD-1 below.

ConfigKit-backed adapters use the same supplied typed loader for existing inspection and both comparison selections. They delegate equality and paired disclosure to configkit.Diff; no rendered-YAML comparison or assertion on mutable Result.Config is introduced. Convenience wiring remains simple: AppCommand and FromConfigKit should provide native comparison alongside their existing supported operations, with ordinary capability narrowing available. The provider representation is accepted in OD-2.

Declaration capture, metadata discovery, and help leave suite factories and acquisition/comparison callbacks uninvoked. Invocation preparation uses original typed binding identities and converted values, preserving explicit empty, false, and zero selections. Supplied values are converted through core once, including help paths; applying a value to both comparison inputs does not parse it twice. Input/state timing for the pair is part of OD-2, rather than an implicit extension of the existing one-selection lifecycle.

Comparison meaning and disclosure

Retain ConfigKit's native changed-region records at the provider boundary, including semantic paths, left/right Present and Redacted states, safe YAML values, and HiddenChanged. The default report deliberately presents a reduced view under OD-3 rather than exposing all native metadata. The native contract owns detailed comparison semantics; the important operator consequences are:

  • Changes reflect frozen effective load values, including defaults and recorded domain resolution. Provenance or source-layout differences alone are not effective changes.
  • Compare final values, not source presence alone. Omitted and explicit-null inputs may resolve equally for a typed field; an absent map key and a present null-valued key can remain distinct in the final configuration. Nil and empty containers also remain distinct. Mappings compare recursively; sequences report atomic changed regions.
  • Both sides receive the union of sensitivity from both loads. Structural replacements can mask an entire region.
  • A difference can exist even when its safe left/right YAML is identical. HiddenChanged reports an actual concealed change; redaction alone does not.
  • An empty successful diff supports equality within ConfigKit's inspected value model. Typed secret references compare by configured name; credential rotation behind an unchanged reference is outside that comparison.

Reports use safe changed regions without inventing a coverage-gap inventory, decoding redaction-marker text as metadata, identifying concealed members beyond upstream disclosure, or adding separately rendered full configurations as if they had received paired protection. Full paired document context and a sensitivity audit are separate potential capabilities, not prerequisites here.

Reports, diagnostics, and outcomes

Default config diff to one YAML document with additions, changes, and deletions, as selected in OD-3. Use existing output streams. YAML is the only supported report format, with explicit --format yaml accepted. The view distinguishes deletion from null but omits separate redaction and concealed- change metadata. Move the implemented report contract to the CLI reference. ADR 0008 records this reduced view while retaining native metadata at the provider boundary. Other commands retain their existing format defaults.

Successful comparisons return success whether they find differences, concealed changes, or none. No assertion status or separate exit code is added. Load and comparison failures retain operational classification and supported error identity; selection/input errors retain usage classification. Output failures use existing delivery and incomplete-report semantics.

The ConfigKit adapter retains diagnostics from the acquired results, including warnings and any load failure encountered. Present them through existing core diagnostic delivery; preserve meaningful left/right context when needed to identify the failing selection without losing wrapped error identity. Retain left-side warnings if the right load fails, and diagnostics from both successful loads if native comparison fails. Finish acquisition and native comparison before writing a successful diff report. Cancellation is checked at ordinary operation boundaries; do not promise interruption inside a synchronous upstream call.

Work needed from the baseline

These are work areas, not numbered implementation stages. Use the accepted choices below to write the staged implementation plan.

  • 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. Do not fabricate future checksums or turn migration checks into permanent gates.

Focused comparison coverage should protect the CommandKit boundary: capability availability before callbacks, side selection/presence, chosen input/state lifetime, native public and concealed changes, absent versus null values, and empty successful comparisons. Include representative load/comparison errors, diagnostics, cancellation, and output failure. Specifically cover left-side warnings followed by a right-side load failure, and diagnostics from both loads followed by a comparison failure. Verify that selection-aware caching does not return the left result for the right selection. Include the three report categories, explicit null versus deletion, concealed-only changes, and an unchanged masked value inside a changed sequence. Existing inspection and composed PromptKit behavior must continue to work. Reuse native collaborators and test CommandKit's adaptation/reporting; do not duplicate ConfigKit's entire comparator suite or impose performance thresholds.

Scope limits and completion

This work delivers ConfigKit adoption and ConfigKit-backed config diff. PromptKit upgrade/comparison/catalogs, JSON reports, per-element sequence matching, resolved credential comparison, full paired document context, assertion mode, provider caching, and core backend changes are outside this target.

Completion means upgraded integration dependencies, usable native comparison through declared scopes and convenience wiring, selected public API/CLI/report contracts in their current owners, relevant maintained examples, accurate notices, and sufficient boundary validation. No open choices may be silently converted into contracts by implementation. Publication follows the existing release procedure and is not performed by this planning pass.

Selected report design (OD-3)

Selected: the default operator-facing output of config diff is a single YAML document with three primary keys: additions, changes, and deletions. Additions and changes preserve configuration hierarchy and contain only safe right-side values; deletions are canonical JSON Pointer paths. For example:

additions:
  server:
    health_check: true
changes:
  server:
    port: 9090
  logging:
    destination: null
deletions:
  - /server/legacy_timeout

Classify each native difference by its presence fields: absent-left/present-right is an addition, present-left/absent-right is a deletion, and present on both sides is a change. Explicit null remains a value; do not encode deletion as null. Changed sequences remain whole sequences, and native structural replacements remain whole reported regions. Do not derive a second set of changes by comparing rendered values or expanding atomic regions.

Assemble additions and changes from ConfigKit's paired-safe right snapshots, preserving YAML value types and semantic path components. Parsing and assembly serve presentation only. Do not read unredacted configuration values or append independently rendered full configurations. The result is a partial inspection view, not a complete configuration or an automatically applicable patch.

Omit separate Redacted and HiddenChanged metadata from this default view. Every reported region changed, but a masked value within it need not have changed: a public field in an atomic sequence may change while a secret remains unchanged. A concealed-only change still appears in its native reported region even if the safe snapshots are identical. Preserve native masks without interpreting marker text; a literal public string resembling [REDACTED] remains an ordinary value. The default report intentionally does not expose every distinction available in the native records. A detailed metadata format can be considered separately if needed; it is not required for this delivery.

Accepted edge-case treatment: when a nonempty root mapping becomes an empty mapping, report the native removed-key paths under deletions. The reduced report need not add special metadata or a separate envelope solely to distinguish exceptional whole-root empty/null transitions from an empty comparison. Ambiguity in that case is acceptable to the user and is not a planning blocker. Native comparison records retain their distinctions; ordinary field-level null values and deletions retain the selected reporting rules.

Accepted format and empty-category treatment: YAML is the only report format for this delivery; a separate plain text report adds no needed operator value. Always include all three keys in the order additions, changes, deletions. Empty additions and changes render as {}; empty deletions render as []. An empty comparison therefore produces:

additions: {}
changes: {}
deletions: []

This stable shape supports automated consumption without missing-key handling and retains the accepted tolerance for exceptional whole-root ambiguity. Other commands keep their existing formats.

ADR 0008 records this selected presentation. Selection syntax and provider shape are accepted below. No significant open decisions remain for this roadmap.

Alternatives considered: a full difference envelope with left/right snapshots and explicit presence, redaction, and hidden-change fields preserves all native metadata, but is more verbose than the chosen operator view. Embedding parsed values in that envelope avoids YAML strings but retains the record-oriented layout. A right-side-only view without deletion paths cannot represent removals unambiguously. The selected three-category document balances configuration-shaped output with explicit removal reporting.

Omitting empty categories would shorten the report but require consumers to handle missing keys. A separate text report could label changes by path or group them into sections, but YAML already provides readable operator output; another renderer and presentation contract are unnecessary for this delivery.

Accepted Decisions

OD-1: How operators select the two sides

Accepted: use existing --config and --overlay for the left side and add --against-config and --against-overlay overrides for the right side. Omitted right fields inherit the left field, including its supplied state; explicit right values, including an empty string, replace it. Use the scope's applicable typed domain selectors on both sides. This supports two files and base-versus-overlay comparison while reusing familiar syntax and preserving provider-owned defaults. Allow equal selections; they can produce a legitimate 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:

app config diff --config before.yml --against-config after.yml
app config diff --config base.yml --against-overlay candidate
app config diff --config base.yml --overlay current --against-overlay ""

Right-side inheritance is field-by-field. Overriding only the configuration still inherits the left overlay. For either selection field:

Left field Right override Effective right field
Omitted Omitted Omitted, preserving provider defaults
Supplied value Omitted Same value and supplied state as left
Explicit empty string Omitted Explicit empty string, still supplied
Any value/state Supplied value Right value, supplied
Any value/state Explicit empty string Empty string, supplied

The provider continues to interpret an explicitly empty overlay through its ordinary loading contract; CommandKit does not define empty as disabling overlays. Shared domain selector values are the initial scope; independent per-side domain selectors can be considered with a concrete consumer need, without precluding them as future API work.

Alternative considered: expose symmetric --left-config/--right-config and --left-overlay/--right-overlay selections, resolving each side independently. This makes two independent selections explicit and avoids inheritance rules. It adds vocabulary beside existing configuration selectors and repeats flags for the common single-base overlay workflow. It remains viable if symmetry is valued more than reuse of familiar inspection syntax.

OD-2: Comparison provider and input/state lifetime

Accepted: add a narrow optional scope comparison callback receiving the pair's selections, separately prepared left/right typed inputs, and one shared state value. Group each side's selection and input in a request value to avoid several positional left/right arguments. 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:

type ComparisonSide[I any] struct {
    Selection Selection
    Input     I
}

type ComparisonRequest[I any] struct {
    Left  ComparisonSide[I]
    Right ComparisonSide[I]
}

// Accepted target 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.

The typed FromConfigKit adapter can retain its T, invoke the supplied loader for each reached side, and call configkit.Diff. Existing single-view acquisition remains responsible for ordinary operations. A comparison-only scope can supply the pair callback without an unrelated Acquire callback. Declared capabilities determine which callbacks must be wired.

For a reached comparison, initialize each selection's I separately, apply the same applicable captured selector values, and check it with that side's Selection. Finish both input checks before initializing S once and invoking the pair provider. Omitted factories still supply zero values, including nil pointers; separate calls do not guarantee distinct caller objects. Later factories or loads need not run after failure. Help/discovery remain free of these calls. 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.

This keeps generic result handling inside the owning adapter and preserves typed binding composition 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.

Alternative considered: acquire a comparison-capable facet with each ordinary View, then compare the two acquired facets. This shares single-side acquisition between all operations and may suit providers already retaining typed results behind a stable comparison handle. It requires a public way to establish compatibility between two heterogeneous facets, and can lead to type assertions or extra opaque-handle machinery. Consider it if representative non-ConfigKit consumers demonstrate enough benefit to justify that additional contract.