Files
commandkit/docs/roadmap/audit/01-correctness.md

219 lines
13 KiB
Markdown
Raw Normal View History

# Functional correctness audit
Status: six open findings: four P2 library findings and two P3 example/validation
fixture findings. Reviewed source:
`7b2dc2f2c975abe2708945097f5dd4802313215e`.
## Scope and architecture
The review follows the [architecture policy](../../policy/architecture.md),
[CLI contract](../../cli.md), public GoDoc, and accepted decisions. The
[internal overview](../../internal/overview.md) inventories the current owners:
- Core owns captured declarations, atomic registration, command and flag alias
resolution, typed conversion and composition, completion, help/version,
shared report rendering, and invocation outcomes. Neutral metadata separates
those responsibilities from the internal Cobra/pflag adapter.
- ckconfig owns configuration scope selection, independent comparison inputs,
opt-in overrides, acquisition coordination, and configuration reports. ConfigKit
owns configuration resolution, validation, metadata, safe snapshots, and diffs.
- ckprompt owns PromptKit command assembly, reusable request bindings, namespace
forwarding, inspection/requirements/preparation adapters, and smoke aggregation.
PromptKit owns the underlying domain operations. Applications own loaders,
builders, execution dependencies, and executable lifecycle.
- Release launchers and module profiles select ReleaseKit's validation and
publication facilities; they are separate from library runtime behavior.
The audit examined ordinary and error paths, declaration capture and registration
state, routing and parsing boundaries, presence/default handling, captured input
replay, report conversions, reporting finalization, cancellation, provider result
adaptation, scope/comparison preparation, request forwarding, smoke aggregation,
maintained examples, and completion/release wiring. Upstream domain algorithms
were treated as upstream-owned. Findings concern CommandKit-owned behavior;
they do not request arbitrary object copying or concurrent App dispatch.
## Validation evidence
Validation used Go 1.27.0 and the existing dependency cache, without modifying
production code or repository tests:
- All three independent module default suites passed with
`GOWORK=off GOTOOLCHAIN=local go test -mod=readonly ./...`, run separately from
the root, ckconfig, and ckprompt module directories.
- The aligned workspace suites passed with
`GOTOOLCHAIN=local go test -mod=readonly ./... ./integrations/ckconfig/... ./integrations/ckprompt/...`.
- Temporary external-consumer probes, using local replacements for all three
CommandKit modules, reproduced C1–C4. Their assertions fail against the
reviewed implementation. C2 reproduced in three additional consecutive runs.
- Running the maintained core example reproduced C5.
- The opt-in backend Bash completion checks passed. The opt-in consumer Bash
checks failed before interactive completion, as described in C6. Zsh and Fish
are not installed, so their interactive checks were not run.
The completed findings in [remediation.md](../remediation.md) remain historical
records of the preceding reviews. This audit records additional findings against
the corrected source, rather than reopening those issues. Independent integration
modules still select released CommandKit dependencies; workspace tests and the
external probes exercised the combined current source.
## C1 — P2: CLI provider-parameter objects leak mutations between requests
**Code:** [request_flags.go](../../../integrations/ckprompt/request_flags.go),
NewExecutionFlags/ExtraParams and executionFlag, lines 107–115;
[smoke_command.go](../../../integrations/ckprompt/smoke_command.go),
runSmokeAggregate and smokeRequestForKind, particularly lines 227–240.
**Trigger and expected behavior:** supply `--llm-extra-params` containing an
object, then customize a smoke request in a builder/callback. Each aggregate
operation should begin with the operator's original seed, with its own kind.
Similarly, applying captured reusable execution flags to separately initialized
inputs should not let one destination rewrite the CommandKit-parsed JSON values
subsequently applied to another. Shared application input/state is a separate,
deliberate contract.
**Actual behavior:** ExtraParams assigns the captured map directly, and
smokeRequestForKind copies the execution struct and scalar pointers without
copying that map. The map and nested maps/arrays remain shared. In a probe using
`{"top":"original","nested":{"value":"original"}}`, mutations made by the
reachability callback appeared in authentication, text inference, and structured
output requests. The command still returned status 0. A second probe applied
the public ExtraParams handle through CaptureFlags/ApplyTo, mutated a nested
value, then used ApplySuppliedTo on an independent destination; it received the
mutated value instead of the supplied JSON.
**Potential remediation:** copy the library-parsed JSON object and its nested
objects/arrays when applying ExtraParams and when preparing each aggregate seed.
Retain nil versus an explicitly empty object, numeric types, and authoritative
builder replacements. Keep this localized to the known CLI JSON representation;
do not add generic deep-copy behavior to ValueFlag or arbitrary application
objects. Regression coverage should exercise both smoke aggregation and public
captured binding replay with nested objects/arrays.
## C2 — P2: Finalization can discard a successfully queued deferred message
**Code:** [reporter.go](../../../reporter.go), DeferredReporter.enqueue lines
254–279 and Reporter.finalize lines 282–325.
**Trigger and expected behavior:** a goroutine submits a visible deferred
message while Execute is flushing the invocation's existing deferred messages.
Reporter operations are documented as safe concurrently, and deferred submission
returns its acceptance error immediately. The submission should either be
retained for delivery or rejected because the deferred queue has closed.
**Actual behavior:** finalize detaches the current queue under the mutex, then
unlocks between deliveries. Until the final closed flag is set, enqueue can
append to the newly empty state.deferred and return nil. Finalization never
drains that new queue and clears it when closing. The accepted message disappears.
A probe used a diagnostic writer that briefly pauses delivery, with another
goroutine calling AtEnd().Warn during that flush. Submission returned nil, the
message was absent, and Execute returned success with diagnostic delivery marked
complete. This does not require concurrent App dispatch or reentering a reporter
from a rendering callback.
**Potential remediation:** make the deferred queue's acceptance/finalization
transition atomic. For example, stop accepting deferred submissions when the
queue is detached and return ErrReporterClosed to later submissions, while
allowing the internal finalization messages to finish. Alternatively, ensure
every accepted message is drained before closure. Preserve existing visibility,
message ordering, and callback non-reentrancy rules.
## C3 — P2: Prepared-request inspection reports success without a result
**Code:** [preparation.go](../../../integrations/ckprompt/preparation.go),
runRequestPreparation lines 95–104; the PrepareRequest callback contract in
[adapters.go](../../../integrations/ckprompt/adapters.go).
**Trigger and expected behavior:** a supplied Suite.PrepareRequest callback
returns a zero Acquired value and nil error. Its contract supplies a native
prepared-request report. Absence of that report should produce an operational
provider/wiring failure, consistent with named resource inspection, requirements
inspection, and smoke commands.
**Actual behavior:** the handler checks the error and context but never checks
acquired.Value. `promptkit show prepared example --format=json` with such a
callback writes `null` and returns status 0 with no explanation. A missing
prepared request is therefore indistinguishable from successful preparation in
the invocation outcome. The neighboring inspection handlers explicitly reject
missing results; smoke handling rejects a missing report without an error.
**Potential remediation:** reject a nil prepared result after acquisition and
cancellation handling, preserving independent diagnostics and any supplied
explanation. Leave core's support for deliberate null reports unchanged; this
check belongs to the prepared-request command. Cover the empty-success callback
alongside valid reports and report-plus-error behavior.
## C4 — P2: Show preparation labels ordinary cancellation as a rendering error
**Code:** [show.go](../../../integrations/ckconfig/show.go), runConfigShow lines
69–95, interacting with Reporter.RenderOutput in [reporter.go](../../../reporter.go).
**Trigger and expected behavior:** context cancellation becomes visible between
buffered effective-document conversions, before report delivery starts. The
handler should stop preparation and return context.Canceled as the operation
cause. Cancellation alone should not manufacture an encoder or output failure.
The same distinction is protected for buffered configuration reports in
[report_cancellation_test.go](../../../integrations/ckconfig/report_cancellation_test.go).
**Actual behavior:** the loop's context checkpoint returns the cancellation
error from inside RenderOutput. Core records it as Output.RenderError. The
subsequent configFailure preserves the cancellation cause but cannot undo that
false presentation evidence. A two-scope `config show --scope=all` probe acquired
both scopes successfully; the first native writer emitted valid safe YAML and
canceled the context while returning nil. The second writer was not called and
no report bytes were delivered, yet the outcome contained both context.Canceled
and an incomplete output delivery with context.Canceled as RenderError.
**Potential remediation:** carry preparation cancellation separately from the
rendering callback's error, following the cancellation checkpoint approach in
renderPreparedConfigReport. Retain real native-writing, decoding, and delivery
errors independently; preserve cancellation identity and the existing rule that
an incomplete aggregate show emits no report.
## C5 — P3: The core example overwrites its optional scenario default
**Code:** [examples/core/main.go](../../../examples/core/main.go), report command
input factory and optional argument setter, lines 70–84.
**Trigger and expected behavior:** invoke the example's `report` command without
a scenario. The operand is optional and NewInput explicitly initializes Scenario
to `demo`, so the default reporting demonstration should run.
**Actual behavior:** ValueArgument applies an unsupplied zero string, and the
example's setter assigns it unconditionally. CheckInput then rejects the empty
scenario. Running `go run ./examples/core report` prints
`ERROR: unknown reporting scenario ""` and exits with the command's usage status
2. Existing example cases always supply a scenario and miss this path.
**Potential remediation:** only replace Scenario when the argument's Set field
is true. This preserves the intended default while still letting an explicitly
supplied empty string reach the ordinary validation path. Do not change core's
documented argument/default application semantics to accommodate the example.
## C6 — P3: The interactive completion consumer cannot construct its App
**Code:** [testdata/completion/main.go](../../../testdata/completion/main.go),
lines 28–44; [completion_shell_test.go](../../../completion_shell_test.go),
TestCompletionConsumerInShells; core's reserved-name check in
[command.go](../../../command.go), validateBindings.
**Trigger and expected behavior:** enable the documented optional completion
consumer checks, or run the fixture's completion command. The fixture must
construct the application and generate the requested script before its shell
dispatch/completion behavior can be exercised.
**Actual behavior:** the fixture still declares its own `format` value flag.
Core reserves that spelling for universal report format selection, so New
rejects the declaration. The check
`COMMANDKIT_COMPLETION_SHELLS=1 go test -run '^TestCompletionConsumerInShells$/bash/' .`
fails while generating the Bash script with
`flag name "format" is reserved for reporting`. This precedes the interactive
shell step and affects every shell using this fixture. The ordinary suite skips
the opt-in check, explaining why its passing result does not cover this failure.
The separate backend Bash script checks pass.
**Potential remediation:** adapt the fixture to the core format flag, or rename
its application-specific choice flag and update the consumer cases accordingly.
Then rerun the consumer checks with the available shells. This is a fixture and
validation-path defect; the observed construction error does not establish that
all generated completion scripts are broken.