Complete the registration API design mockups
This commit is contained in:
@@ -118,7 +118,9 @@ state. Under the current generic sketch, their loader accepts an unused
|
|||||||
`struct{}` context argument and their suite uses `struct{}` with no `NewContext`
|
`struct{}` context argument and their suite uses `struct{}` with no `NewContext`
|
||||||
callback. This makes the remaining type plumbing visible rather than hiding it
|
callback. This makes the remaining type plumbing visible rather than hiding it
|
||||||
behind an `AppInput` type or helper. Whether to offer a context-free loader form
|
behind an `AppInput` type or helper. Whether to offer a context-free loader form
|
||||||
is still open; these examples do not settle that API.
|
is evaluated below, with a focused convenience function recommended for Option A.
|
||||||
|
The generic sketches remain visible for comparison; they are not the recommended
|
||||||
|
shortest setup for that case.
|
||||||
|
|
||||||
In the composed examples, both loaders accept `*RunState` and all provider and
|
In the composed examples, both loaders accept `*RunState` and all provider and
|
||||||
suite assembly is shown inline. The illustrative `newRunState` callback is
|
suite assembly is shown inline. The illustrative `newRunState` callback is
|
||||||
@@ -347,8 +349,9 @@ func Command[C any](suite Suite[C]) commandkit.Command
|
|||||||
A returned command is an uninstalled, potentially invalid declaration. `New` or
|
A returned command is an uninstalled, potentially invalid declaration. `New` or
|
||||||
`Register` prepares and validates the complete batch before committing it. The
|
`Register` prepares and validates the complete batch before committing it. The
|
||||||
core needs a supported mechanism for integration-owned validation and assembly
|
core needs a supported mechanism for integration-owned validation and assembly
|
||||||
without importing integration types or exposing backend machinery. Its exact
|
without importing integration types or exposing backend machinery. The concrete
|
||||||
shape remains open; the shorter consumer syntax does not make that work disappear.
|
`DeferredGroup` proposal below makes that cost visible; the shorter consumer
|
||||||
|
syntax does not make that work disappear.
|
||||||
|
|
||||||
Preparation uses only declarations and metadata. It does not acquire providers
|
Preparation uses only declarations and metadata. It does not acquire providers
|
||||||
or call application context factories. Installation errors include both invalid
|
or call application context factories. Installation errors include both invalid
|
||||||
@@ -419,25 +422,51 @@ No guarantee extends across separate calls.
|
|||||||
|
|
||||||
## Ownership, collisions, and the core boundary
|
## Ownership, collisions, and the core boundary
|
||||||
|
|
||||||
None of the alternatives needs to expose registry maps, an installed-config boolean,
|
None of the alternatives needs to expose registry maps, an installed-config
|
||||||
individual handler constructors, or backend objects. Ordinary registration can
|
boolean, individual handler constructors, or backend objects. Ordinary
|
||||||
reject an occupied root path and commit a fully validated batch without merging
|
registration can reject an occupied root path and commit a fully validated batch
|
||||||
children. That protects installed-family ownership for all three designs.
|
without merging children. That protects installed-family ownership for all three designs.
|
||||||
|
|
||||||
Evaluate any additional reservation requirements against the accepted ownership
|
The accepted ownership rule is clear for installed standard families: application
|
||||||
decision, not prototype flags or incidental checks. If a reserved namespace must
|
registration cannot replace them or add private operations underneath them.
|
||||||
be unavailable to ordinary application declarations before its integration is
|
[ADR 0001](../adr/0001-command-dispatch-and-inputs.md) also refers to reserved
|
||||||
installed, group immutability and duplicate checks alone do not express that
|
standard families, while the [architecture policy](../policy/architecture.md)
|
||||||
rule. A narrow core installation distinction may then be justified. All designs
|
explicitly describes an installed `promptkit` family. Neither provides a uniform,
|
||||||
can accommodate it, and the final proposal should identify the exact rule it
|
explicit lifetime rule for every uninstalled optional family. That interpretation
|
||||||
serves before selecting its public signature. Do not invent package-origin
|
must be decided deliberately rather than inferred from prototype flags.
|
||||||
authentication or a general plugin framework to solve registration ownership.
|
|
||||||
|
|
||||||
An explicit `Family` handle with separate creation and installation is also
|
**Recommendation:** help/version are reserved by core construction; an optional
|
||||||
possible. It would distinguish standard families from domain groups, but adds a
|
standard family claims its complete root namespace when installed. Before that,
|
||||||
second public declaration model. Adopt it only if a concrete reservation or
|
a domain group can occupy the name, but a subsequent integration installation
|
||||||
composition responsibility requires that distinction; module extraction alone
|
then fails with a collision. Neither registration order can replace a winner.
|
||||||
does not. This is a conditional extension to the alternatives, not a prerequisite.
|
This avoids a core registry of every possible future integration namespace and
|
||||||
|
does not prohibit core-only applications from using an otherwise absent family
|
||||||
|
name. It preserves ownership throughout an installed family's lifetime.
|
||||||
|
|
||||||
|
**Alternative:** reserve the known standard names before integration installation.
|
||||||
|
This catches domain misuse earlier but constrains applications that never install
|
||||||
|
those integrations and requires the core to recognize the reserved names and an
|
||||||
|
installation distinction. If selected, a narrow standard-group constructor can
|
||||||
|
mark integration declarations; do not expose private registry state.
|
||||||
|
|
||||||
|
Before implementation, align the roadmap, policy, and ADR interpretation with
|
||||||
|
the selected lifetime rule. If it changes an accepted decision, record that
|
||||||
|
change through the ADR process; this mockup does not authorize weakening it.
|
||||||
|
|
||||||
|
Option A assembles and registers its group at the root internally. B and C expose
|
||||||
|
handles, so they additionally need enforceable root placement. For B, a proposed
|
||||||
|
`commandkit.RootGroup(name, summary, children...) (Command, error)` returns an
|
||||||
|
opaque group declaration that normal `New`/`Register` accepts at the root and
|
||||||
|
rejects as a child of another group. It adds one public core constructor for a
|
||||||
|
concrete placement need, without exposing its handlers or creating a second
|
||||||
|
`Family` type. C's deferred constructor carries the same root placement metadata.
|
||||||
|
Moving a returned standard declaration under `tools`, for example, is an assembly
|
||||||
|
error rather than silently producing `app tools config show`.
|
||||||
|
|
||||||
|
This public distinction represents placement, not proof of a caller's package
|
||||||
|
identity. Do not invent package-origin authentication. An explicit `Family` type
|
||||||
|
remains viable if a concrete responsibility eventually warrants a separate
|
||||||
|
model, but it is not needed merely to relocate packages.
|
||||||
|
|
||||||
## Validation and declaration ownership
|
## Validation and declaration ownership
|
||||||
|
|
||||||
@@ -455,15 +484,13 @@ Recommended ownership behavior: successful installation captures the declaration
|
|||||||
metadata it needs, so later edits to suite fields or declaration slices do not
|
metadata it needs, so later edits to suite fields or declaration slices do not
|
||||||
change installed commands. Application callbacks and the runtime objects they
|
change installed commands. Application callbacks and the runtime objects they
|
||||||
capture retain their documented application-owned lifecycle; this is not a
|
capture retain their documented application-owned lifecycle; this is not a
|
||||||
promise to deep-copy arbitrary application state. The precise capture boundary
|
promise to deep-copy arbitrary application state. For B and C, the proposal below
|
||||||
for B and C, before installation, remains an open decision below.
|
makes capture happen when a declaration factory is called; installation does not
|
||||||
|
read mutable caller-owned declaration slices.
|
||||||
|
|
||||||
Empty declarations also need an explicit choice. A suite with no definition
|
The empty-suite proposal below distinguishes a configuration suite with no
|
||||||
capabilities could reasonably install nothing, but B and C then need a way to
|
scopes from a definition suite with no capabilities. B and C use an explicit
|
||||||
represent that result without registering an empty standard namespace or treating
|
no-op declaration for the latter; an invalid zero-value command remains an error.
|
||||||
every invalid zero-value command as a no-op. An empty configuration suite may
|
|
||||||
have different semantics because scope discovery is itself an operation. These
|
|
||||||
cases remain open rather than inheriting a choice from illustrative code.
|
|
||||||
|
|
||||||
## Comparison
|
## Comparison
|
||||||
|
|
||||||
@@ -476,13 +503,14 @@ cases remain open rather than inheriting a choice from illustrative code.
|
|||||||
| Atomicity | Each integration registration call | Each core registration batch | Each core registration batch, including preparation |
|
| Atomicity | Each integration registration call | Each core registration batch | Each core registration batch, including preparation |
|
||||||
| Standard assembly visible to callers | Integration-owned | Complete group handle exposed | Deferred group handle exposed |
|
| Standard assembly visible to callers | Integration-owned | Complete group handle exposed | Deferred group handle exposed |
|
||||||
| Pre-assembly of mixed command collections | Separate domain declarations and family calls | Directly supported | Directly supported |
|
| Pre-assembly of mixed command collections | Separate domain declarations and family calls | Directly supported | Directly supported |
|
||||||
| Additional core mechanism | Only for demonstrated ownership needs | Same | Deferred preparation, plus any ownership mechanism |
|
| Additional core mechanism | Ordinary groups/registration under the recommended reservation rule | Root-placement declaration and explicit no-op | Deferred preparation, root placement, and explicit no-op |
|
||||||
| Dependency isolation and lazy acquisition | Supported | Supported | Supported |
|
| Dependency isolation and lazy acquisition | Supported | Supported | Supported |
|
||||||
|
|
||||||
## Recommendation
|
## Recommendation
|
||||||
|
|
||||||
Prefer **Option A: integration-owned `Register` functions with typed suites and
|
Prefer **Option A: integration-owned `Register` functions with typed suites and
|
||||||
declaration-only adapters**. Rank Option C second and Option B third.
|
declaration-only adapters, plus the focused `RegisterApp` convenience below**.
|
||||||
|
Rank Option C second and Option B third.
|
||||||
The representative consumers benefit from installing standard behavior directly;
|
The representative consumers benefit from installing standard behavior directly;
|
||||||
they do not demonstrate a need to collect standard command handles or roll back
|
they do not demonstrate a need to collect standard command handles or roll back
|
||||||
all family registrations together. Per-call atomicity and discarding a failed
|
all family registrations together. Per-call atomicity and discarding a failed
|
||||||
@@ -490,8 +518,9 @@ application setup are sufficient for these sketches.
|
|||||||
|
|
||||||
Use ordinary group construction and registry collision handling wherever they
|
Use ordinary group construction and registry collision handling wherever they
|
||||||
satisfy the selected ownership rules. Remove redundant prototype bookkeeping.
|
satisfy the selected ownership rules. Remove redundant prototype bookkeeping.
|
||||||
Resolve the reservation distinction above before finalizing core signatures;
|
Adopt the proposed installed-family reservation rule only after its deliberate
|
||||||
add only the capability its actual responsibility requires.
|
policy/ADR reconciliation. With that rule, A needs no extra core family or
|
||||||
|
preparation abstraction. Add only capabilities serving a selected responsibility.
|
||||||
|
|
||||||
Do not ship both `Register` and `Command` merely to cover every possible assembly
|
Do not ship both `Register` and `Command` merely to cover every possible assembly
|
||||||
style. Options B and C are credible alternatives if pre-assembly or batch
|
style. Options B and C are credible alternatives if pre-assembly or batch
|
||||||
@@ -506,40 +535,334 @@ plan can then specify exact declarations and validation without asking the codin
|
|||||||
agent to invent a family framework. Prototype-compatible names, intermediate
|
agent to invent a family framework. Prototype-compatible names, intermediate
|
||||||
buildability, and mechanical file relocation are not selection criteria.
|
buildability, and mechanical file relocation are not selection criteria.
|
||||||
|
|
||||||
## Remaining decisions and incomplete areas
|
## Additional design work and proposed resolutions
|
||||||
|
|
||||||
1. **Select the registration alternative.** A remains the recommendation; C and
|
These proposals address the requested follow-up areas. They are recommendations
|
||||||
B remain proposals. Evaluate the full setup shown above rather than counting
|
for review, not new accepted contracts. Select their semantics before producing a
|
||||||
only the final installation calls. No registration shape is selected here.
|
decision-complete implementation plan.
|
||||||
2. **Settle the context-free case.** The simple examples still require
|
|
||||||
`Suite[struct{}]`, a scope slice, and an unused loader argument. Decide whether
|
### 1. Registration choice and the full setup cost
|
||||||
that is acceptable or whether a focused convenience entry point should accept
|
|
||||||
a loader without application context. Show its complete call site and avoid
|
Retain the ranking **A, C, B**. All three generic sketches include real adapter,
|
||||||
creating a parallel configuration language just to remove a few lines.
|
scope, and suite assembly. A's common case improves further with the focused
|
||||||
3. **Specify empty-suite behavior.** Decide independently for configuration and
|
entry point below. C has the shortest mixed declaration expression but needs a
|
||||||
definition suites whether zero capabilities mean no installation, discovery
|
new public preparation contract, root-placement handling, and no-op representation.
|
||||||
only, or a declaration error. If selecting B or C, define the representation
|
B exposes early validation and batch composition at the cost of more intermediate
|
||||||
of an intentional no-op and distinguish it from an invalid command.
|
handles and error checks. None needs a transaction spanning separate calls.
|
||||||
4. **Resolve pre-installation namespace reservations.** Identify the exact
|
|
||||||
accepted rule for names before an integration is installed, reconcile ADR
|
### 2. A context-free application configuration entry point
|
||||||
and architecture-policy language where necessary, and distinguish it from
|
|
||||||
protecting an already installed family. For B or C, also specify how standard
|
**Recommendation for A:** add a single focused convenience function:
|
||||||
root placement is retained when declarations are passed through ordinary
|
|
||||||
group APIs. Add a core distinction only for the selected rule.
|
```go
|
||||||
5. **Finalize declaration capture and reuse.** Confirm the proposed separation
|
// In ckconfig:
|
||||||
of captured metadata and application-owned callback state. For B and C,
|
func RegisterApp[T any](
|
||||||
decide whether capture happens at factory invocation or installation, and
|
app *commandkit.App,
|
||||||
whether the returned declaration can be installed in multiple applications.
|
load func(context.Context, Selection) (*configkit.Result[T], error),
|
||||||
Do not infer concurrent-dispatch guarantees from declaration reuse.
|
) error
|
||||||
6. **Complete validation details.** Define how declaration-only adapters retain
|
```
|
||||||
invalid wiring for the eventual validation boundary, how errors identify the
|
|
||||||
affected family or scope, and how nil loaders differ from deliberately absent
|
Complete registration call site, assuming `greet` is already declared:
|
||||||
capabilities. If selecting C, specify the public preparation mechanism and
|
|
||||||
show that it validates the whole batch before registry mutation without
|
```go
|
||||||
invoking providers or application context factories.
|
app, err := commandkit.New(identity, greet)
|
||||||
7. **Exercise typed customization.** Add one realistic selector/override example
|
if err != nil {
|
||||||
showing explicit presence and scope applicability. Specify absent
|
return err
|
||||||
`NewContext` behavior, particularly for pointer context types, and keep the
|
}
|
||||||
configuration and definition selector contracts appropriately distinct.
|
if err := ckconfig.RegisterApp(app, loadAppConfig); err != nil {
|
||||||
These details should support the chosen registration model without becoming
|
return err
|
||||||
a universal integration framework.
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here `loadAppConfig` accepts only `context.Context` and `ckconfig.Selection`.
|
||||||
|
`RegisterApp` adapts it internally, declares the standard `app` scope, and uses
|
||||||
|
the same validation and installation path as `Register`. A nil original loader
|
||||||
|
remains a declaration error. The default scope summary can be a plain application
|
||||||
|
configuration description; callers needing a different summary, additional
|
||||||
|
scopes, selectors, or state use `Register` with a typed suite.
|
||||||
|
|
||||||
|
This is an alternative entry point for a demonstrated common case, not an options
|
||||||
|
language or a second scope model. It does not install a suite to which more
|
||||||
|
scopes are appended later. An application needing composition selects the full
|
||||||
|
suite entry point from the start.
|
||||||
|
|
||||||
|
**Alternative:** keep only generic `Register`. A context-free loader can use the
|
||||||
|
following explicit wrapper and declaration, with no additional export:
|
||||||
|
|
||||||
|
```go
|
||||||
|
if err := ckconfig.Register(app, ckconfig.Suite[struct{}]{
|
||||||
|
Scopes: []ckconfig.Scope[struct{}]{
|
||||||
|
ckconfig.FromConfigKit("app", "Application settings.",
|
||||||
|
func(ctx context.Context, selection ckconfig.Selection, _ struct{}) (*configkit.Result[AppConfig], error) {
|
||||||
|
return loadAppConfig(ctx, selection)
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
That is viable but makes simple consumers repeat plumbing unrelated to their
|
||||||
|
application. Prefer the one focused export; do not automatically add analogous
|
||||||
|
conveniences for every provider or family. If B or C is chosen, evaluate an
|
||||||
|
`AppCommand` factory for this same use case instead of adding A's registration
|
||||||
|
entry point to a factory-based design.
|
||||||
|
|
||||||
|
### 3. Empty suites and intentional no-op declarations
|
||||||
|
|
||||||
|
**Recommendation:** distinguish absence of resources from absence of an optional
|
||||||
|
family's operations, and validate supplied declarations before treating them as
|
||||||
|
empty.
|
||||||
|
|
||||||
|
| Declaration | Proposed result |
|
||||||
|
| --- | --- |
|
||||||
|
| Config suite with no scopes | Declaration error: there is no configuration resource to inspect |
|
||||||
|
| Named config scopes with no inspection operations | Install `config scopes`; report those scopes and their actual empty capability sets |
|
||||||
|
| Config scopes with some supplied operations | Install discovery and the operations supplied by at least one scope |
|
||||||
|
| Definition suite with no capabilities and otherwise valid wiring | Successful no-op; no `promptkit` namespace or help entry installed |
|
||||||
|
| Adapter constructed with a nil loader | Declaration error, even if its wrappers or selected capabilities would otherwise look empty |
|
||||||
|
|
||||||
|
Not calling a registration function remains the simplest way to omit a family.
|
||||||
|
The no-op case supports capability-driven assembly without requiring a special
|
||||||
|
conditional in every consuming application. It does not reserve an absent
|
||||||
|
namespace under the proposed installed-family rule.
|
||||||
|
|
||||||
|
For B and C only, add an explicit proposed `commandkit.NoCommand() Command`
|
||||||
|
representation. Core `New`/`Register` skips it. A zero-value `Command` remains
|
||||||
|
invalid. B returns the explicit no-op after validating an empty definition suite;
|
||||||
|
C's preparation returns it after validation. Invalid wiring is never swallowed
|
||||||
|
as a no-op. A needs neither a public no-op handle nor that extra export.
|
||||||
|
|
||||||
|
**Alternatives:** omit all empty suites successfully, or install configuration
|
||||||
|
discovery even with no scopes. Both avoid the configuration error, but may hide
|
||||||
|
missing resource wiring or expose a namespace with nothing to discover. Requiring
|
||||||
|
at least one definition capability is also viable, but adds conditional assembly
|
||||||
|
for an intentionally optional family. These are product choices, not necessities
|
||||||
|
inherited from the prototype.
|
||||||
|
|
||||||
|
### 4. Namespace reservation and placement
|
||||||
|
|
||||||
|
Prefer the installed-family lifetime rule described above. It protects complete
|
||||||
|
standard groups while keeping unused names available to core-only consumers.
|
||||||
|
Confirm or revise the relevant policy/ADR interpretation explicitly before
|
||||||
|
implementation. Pre-installation reservation remains a viable alternative; its
|
||||||
|
cost and required installation distinction are described above.
|
||||||
|
|
||||||
|
For B and C, enforce standard-root placement in the core declaration metadata,
|
||||||
|
not only in documentation. An ordinary wrapper group must reject a standard
|
||||||
|
root-only child during assembly/registration. A keeps standard handles private
|
||||||
|
and performs its complete root registration inside the integration function.
|
||||||
|
|
||||||
|
### 5. Declaration capture, installation, and reuse
|
||||||
|
|
||||||
|
**Recommendation:** A captures registration metadata when `Register` is called;
|
||||||
|
B and C capture it when their `Command` factory is called. Capture includes
|
||||||
|
scope names and descriptions, capability lists, selector applicability, bindings,
|
||||||
|
and callback references. Copy declaration collections and nested metadata
|
||||||
|
collections needed for these semantics, rather than retaining the caller's
|
||||||
|
mutable slices. Application callback closures and the state they reference stay
|
||||||
|
application-owned; this is not a deep-copy guarantee for arbitrary values.
|
||||||
|
|
||||||
|
After `declaration := ckconfig.Command(suite)`, changing `suite.Scopes` or a scope's
|
||||||
|
operation list does not change that declaration. C's deferred preparation reads
|
||||||
|
its captured declaration, not the later contents of `suite`. Invalid recorded
|
||||||
|
wiring also stays attached to that captured declaration.
|
||||||
|
|
||||||
|
B and C declarations may be installed in more than one application. Each registry
|
||||||
|
captures its own installed metadata and each invocation receives its own parsed
|
||||||
|
state. Callback references may remain shared, and consumer closures may share
|
||||||
|
state deliberately. This does not promise concurrent dispatch or make caller
|
||||||
|
state safe for concurrent use. A can likewise receive the same suite in separate
|
||||||
|
registration calls; each call captures the suite as it exists at that time.
|
||||||
|
|
||||||
|
**Alternative:** C could capture at installation. That permits editing a suite
|
||||||
|
through retained references until installation but makes a command handle's
|
||||||
|
meaning depend on subsequent mutations and installation timing. Factory-time
|
||||||
|
capture provides a clearer declaration model without freezing application state.
|
||||||
|
|
||||||
|
### 6. Declaration validation and Option C preparation
|
||||||
|
|
||||||
|
**Recommendation:** declaration-only adapters retain the original loader or a
|
||||||
|
private construction issue in their owning integration declarations. Validators
|
||||||
|
inspect that information before using generated wrappers. A non-nil wrapper must
|
||||||
|
not make an originally nil loader appear valid. No public invalid-loader flag,
|
||||||
|
root-private accessor, or provider invocation is needed.
|
||||||
|
|
||||||
|
For example, `ckprompt.FromEngine(nilLoader)` records invalid wiring; it is not
|
||||||
|
an empty suite. A deliberately absent capability is an absent callback in a
|
||||||
|
manually composed, otherwise valid suite, or an explicitly supported capability
|
||||||
|
selection. A zero definition suite expresses intentional absence. An upstream
|
||||||
|
configuration or named lookup error from a valid callback remains an invocation
|
||||||
|
result; it is not a registration error.
|
||||||
|
|
||||||
|
Errors identify the family and, where applicable, the scope, capability, or flag.
|
||||||
|
For example, `config scope "app": loader is nil` and
|
||||||
|
`promptkit: engine loader is nil` are illustrative diagnostic content, not exact
|
||||||
|
string contracts. Preserve wrapped error identity where a fallible declaration
|
||||||
|
operation supplies an error. Validate scope uniqueness, supported capability
|
||||||
|
wiring, selector declarations, and reserved-flag collisions from metadata.
|
||||||
|
Integrations validate their own provider and scope metadata; core command/binding
|
||||||
|
construction validates opaque binding handles and command/flag conflicts. Wrap
|
||||||
|
those errors with the integration context rather than inspecting private flag
|
||||||
|
fields or duplicating parser validation. This does not require a new public
|
||||||
|
registry inspection or binding bridge just for declaration validation.
|
||||||
|
Do not validate files, credentials, or application configuration during assembly.
|
||||||
|
|
||||||
|
If C is selected, its concrete proposed public preparation mechanism is:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// In commandkit; needed only for Option C:
|
||||||
|
func DeferredGroup(
|
||||||
|
name, summary string,
|
||||||
|
prepare func() (Command, error),
|
||||||
|
) Command
|
||||||
|
```
|
||||||
|
|
||||||
|
This is a root-only deferred command declaration, not a startup hook. Its opaque
|
||||||
|
handle carries root identity and the preparation callback. Integration factories
|
||||||
|
capture the typed suite and supply their own metadata-only preparation function:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func Command[C any](suite Suite[C]) commandkit.Command {
|
||||||
|
captured := captureSuiteDeclaration(suite) // Integration-private metadata copy.
|
||||||
|
return commandkit.DeferredGroup("config", "Inspect configuration.",
|
||||||
|
func() (commandkit.Command, error) {
|
||||||
|
// Validate captured wiring and assemble the complete standard group.
|
||||||
|
// No acquisition callback or NewContext callback is invoked here.
|
||||||
|
return prepareConfigGroup(captured)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`captureSuiteDeclaration` and `prepareConfigGroup` identify integration-owned
|
||||||
|
responsibilities; they are not additional public exports. Capturing metadata can
|
||||||
|
record an invalid declaration without returning an error. Preparation reports
|
||||||
|
that error at installation.
|
||||||
|
|
||||||
|
Core `New`/`Register` prepares the entire supplied batch before mutation, checks
|
||||||
|
resolved declarations and placement against each other and the existing registry,
|
||||||
|
and commits only after success. A nil preparation callback or a prepared group's
|
||||||
|
root differing from its declared root is an assembly error. A deliberate no-op
|
||||||
|
is permitted; malformed declarations are not. Failure leaves the registry intact,
|
||||||
|
but cannot undo arbitrary effects inside a caller-supplied callback. Standard
|
||||||
|
integration preparation functions themselves are responsible for remaining
|
||||||
|
metadata-only. Registering the same declaration in another app may prepare it
|
||||||
|
again; no cross-application preparation cache or exactly-once promise is needed.
|
||||||
|
|
||||||
|
**Alternative for C:** store a captured wiring error and an already assembled
|
||||||
|
neutral group in a command declaration, reporting the error only at installation.
|
||||||
|
This can avoid a general preparation callback if all necessary assembly can be
|
||||||
|
completed without error-returning work. Its viability depends on the final core
|
||||||
|
constructors; it must not discard validation failures or expose an untyped payload.
|
||||||
|
C's callback form is the concrete mockup here, not a requirement to choose C.
|
||||||
|
|
||||||
|
### 7. Typed customization and absent context factories
|
||||||
|
|
||||||
|
**Recommendation:** an omitted `NewContext` supplies the zero value of `C` for
|
||||||
|
the selected invocation. For a struct, that is a fresh zero-valued struct; for
|
||||||
|
`*RunState`, it is nil. Do not silently allocate arbitrary pointer contexts or
|
||||||
|
reject every pointer-context declaration. A loader may intentionally accept nil.
|
||||||
|
Applications whose binding callbacks or loaders require allocated state supply
|
||||||
|
`NewContext`. A supplied factory returning nil has the same caller-owned meaning;
|
||||||
|
this is not a promise to inspect or repair arbitrary callback state.
|
||||||
|
|
||||||
|
The following realistic A sketch uses a value input with a shared cache pointer.
|
||||||
|
All integration assembly is visible. `strconv.Atoi`, the string parser, and core
|
||||||
|
bindings are illustrative public binding primitives; their final names do not
|
||||||
|
constrain the registration choice.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type RunInput struct {
|
||||||
|
Campaign commandkit.Supplied[string]
|
||||||
|
Limit commandkit.Supplied[int]
|
||||||
|
Cache *RunCache
|
||||||
|
}
|
||||||
|
|
||||||
|
campaignFlag := commandkit.ValueFlag[RunInput, string](
|
||||||
|
commandkit.FlagMeta{Name: "campaign", Usage: "Select campaign inputs."},
|
||||||
|
nil,
|
||||||
|
func(raw string) (string, error) { return raw, nil },
|
||||||
|
func(input *RunInput, value commandkit.Supplied[string]) {
|
||||||
|
input.Campaign = value
|
||||||
|
},
|
||||||
|
)
|
||||||
|
limitFlag := commandkit.ValueFlag[RunInput, int](
|
||||||
|
commandkit.FlagMeta{Name: "limit", Usage: "Override the application's item limit."},
|
||||||
|
nil, strconv.Atoi,
|
||||||
|
func(input *RunInput, value commandkit.Supplied[int]) {
|
||||||
|
input.Limit = value
|
||||||
|
},
|
||||||
|
)
|
||||||
|
newInput := func() RunInput {
|
||||||
|
return RunInput{Cache: newRunCache()}
|
||||||
|
}
|
||||||
|
|
||||||
|
app, err := commandkit.New(identity, sessionGroup)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ckconfig.Register(app, ckconfig.Suite[RunInput]{
|
||||||
|
NewContext: newInput,
|
||||||
|
Selectors: []ckconfig.Selector[RunInput]{
|
||||||
|
{Binding: campaignFlag, Scopes: []string{"app", "promptkit"}},
|
||||||
|
{Binding: limitFlag, Scopes: []string{"app"}},
|
||||||
|
},
|
||||||
|
Scopes: []ckconfig.Scope[RunInput]{
|
||||||
|
ckconfig.FromConfigKit("app", "Application settings.", loadAppConfig),
|
||||||
|
ckprompt.ConfigScope(loadEngine),
|
||||||
|
},
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
definitions := ckprompt.FromEngine(loadEngine)
|
||||||
|
definitions.NewContext = newInput
|
||||||
|
definitions.Selectors = []commandkit.Flag[RunInput]{campaignFlag}
|
||||||
|
if err := ckprompt.Register(app, definitions); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Configuration selectors carry configuration-scope applicability. Definition
|
||||||
|
selectors are plain typed flags for that family's input; they do not need a
|
||||||
|
list containing a fictitious configuration scope. Each loader in this sketch
|
||||||
|
receives `RunInput`; registration and help call neither loader nor `newInput`.
|
||||||
|
|
||||||
|
For `app config show --scope all --campaign spring --limit=0`, both scopes receive
|
||||||
|
the campaign selector, but only the `app` scope receives the limit override.
|
||||||
|
Its loader checks `input.Limit.Set` before applying `input.Limit.Value`; explicit
|
||||||
|
zero overrides an inherited limit, while omission leaves it unchanged. An explicit
|
||||||
|
`--campaign=` similarly remains supplied with an empty value. The bindings retain
|
||||||
|
presence rather than inferring it from nonzero values.
|
||||||
|
|
||||||
|
Scope filtering uses binding metadata so a selector not applicable to a scope
|
||||||
|
is not copied into that scope's input. It does not erase shared acquisition state
|
||||||
|
such as `Cache`, or require deep-copying application state. Selecting only a scope
|
||||||
|
to which a supplied selector does not apply does not add a new rejection rule:
|
||||||
|
the selector simply is not delivered to that scope. If a stricter unused-selector
|
||||||
|
policy is desired, decide it separately rather than inventing it in registration.
|
||||||
|
|
||||||
|
For `app promptkit show profile briefing --campaign spring`, definition inspection
|
||||||
|
receives the campaign input and fresh invocation state; it does not receive the
|
||||||
|
configuration-only limit flag or invoke the application configuration scope.
|
||||||
|
|
||||||
|
**Alternative:** require a context factory for every stateful suite. That makes
|
||||||
|
initialization explicit but adds boilerplate for value inputs that work correctly
|
||||||
|
with zero values. Prefer the zero-value default plus an explicit factory when
|
||||||
|
actual application state needs one.
|
||||||
|
|
||||||
|
## Decisions still requiring approval
|
||||||
|
|
||||||
|
The requested design work is now concrete, but these proposals remain unapproved:
|
||||||
|
|
||||||
|
1. Choose A with `RegisterApp`, C, or B.
|
||||||
|
2. Confirm empty configuration errors, discovery-only named scopes, and empty
|
||||||
|
definition no-ops; B/C additionally need explicit no-op declarations.
|
||||||
|
3. Confirm installed-family namespace reservation and reconcile policy/ADR wording,
|
||||||
|
or choose pre-installation reservation and its narrow core distinction.
|
||||||
|
4. Confirm declaration capture timing and reuse, and the proposed separation from
|
||||||
|
caller-owned state and concurrent-dispatch guarantees.
|
||||||
|
5. Confirm the zero-value context default and the typed customization contracts.
|
||||||
|
|
||||||
|
If C is chosen, additionally select its preparation mechanism; if B is chosen,
|
||||||
|
select the root-placement constructor. Those extra exports are conditional and
|
||||||
|
are not additions to the recommended A API.
|
||||||
|
|||||||
Reference in New Issue
Block a user