49 KiB
Performance implementation plan
Status: implementation in progress; Stages 1–7 are complete, Stages 8–11 have not started.
Date: 2026-10-09.
Scope and authority
Implement PF-01 through PF-06 from the performance roadmap. Read this entire plan before starting and complete the numbered stages in order. Each stage is a separate Luna implementation assignment and must leave the repository buildable. The roadmap owns the target and selected OD-1/OD-2 approaches; this plan owns execution choices, sequencing, and implementation evidence.
Follow the architecture policy, accepted ADRs, agent policy, testing policy, and documentation policy. Public Go declarations and the CLI reference own existing contracts. Private names proposed below identify implementation responsibilities, not new public APIs or durable internal interfaces.
This file replaces the completed ReleaseKit plan. Release tooling is already implemented and is outside this sprint. Stage 11 repairs navigation that still identifies this file as the ReleaseKit plan. The previous record is available through Git history; do not restore its assignments here.
No required product or architecture questions remain open. Measurements assess the selected improvements; they do not authorize substituting persistent mutable backends or segmented YAML delivery for the user's choices.
Baseline and implementation choices
The planning baseline is 6612b54, with a clean worktree. Production code still
matches the efficiency review at 54cfbd8; intervening changes are documentation.
The root facade, internal Cobra adapter, inspection families, and release tooling
are implemented. No performance benchmarks were found in the indexed code, and
this planning pass has not executed tests, benchmarks, or profiles. Reinspect
current state before implementation and record the actual measurement revisions.
Use the following concrete choices:
- Render PromptKit's requested format in its handler, then pass completed bytes to a shared delivery helper. Preserve the existing report DTOs and serializers.
- Index backend commands directly to neutral declarations. Walk Cobra's existing parent links inside the adapter to reconstruct only the selected path.
- Scan YAML opening lines with byte offsets. Return complete framed bytes plus whether the native document has directives, so the aggregate writer can use one analysis. Retain the full framed buffer selected in OD-2.
- Prepare and clone pending command registrations before updating the existing registry. Keep batch validation atomic and preserve owned declaration storage.
- Lazily cache one neutral bridge tree per
App, invalidating it after a successful nonempty registration. Construct fresh mutable backend state for each invocation, then reuse it for parsing, help, and usage within that invocation, as selected in OD-1. - Represent registered configuration metadata with a private generic prepared suite containing normalized declarations, ordered scopes, name lookup, and discovery inventory. Use name/short-name maps for selector duplicate checks. Keep scope selection in preflight and handlers as inexpensive indexed passes. Selected-scope memoization and additional selector-membership indexes are deferred; their mutable bookkeeping is unnecessary for removing the observed quadratic scope searches.
PF-05 and the bounded PF-06 changes above are included in the sprint despite their lower priority. Implement the straightforward algorithms and measure their cost; do not silently omit either coverage ID. Escalate concrete correctness conflicts or material regressions rather than add a new consumer restriction or change the selected scope.
Instructions for every stage
- Start with developer orientation, inspect Git state, and read the assigned sources and prior stage evidence. Prefer knowledge-graph tools for code discovery. Preserve unrelated work and read applicable policies.
- Implement the assigned stage and necessary supporting tests/documentation.
Preserve public signatures and the fields of value structs such as
Invocation,CommandSpec, andConfigSuite; do not add execution state to them. TheApphandle may gain private implementation fields. Cobra/pflag objects and errors stay inside their adapter. - Preserve grammar, conversion of every supplied singleton occurrence, explicit presence, collection ownership, callback lifecycle, supported error identity, diagnostic delivery, and report semantics. Reuse existing coverage; add only missing protection for the change's realistic risks. Do not assert private helper counts or fixed timings in behavioral tests.
- Format changed Go files. Run the stage's focused validation and measurements. Dependency preparation, if needed, is separate from offline measurement. Apply the measurement protocol's workspace, local-toolchain, and offline environment settings to Go validation commands as well. Tests and benchmarks added by this plan are development evidence, not new release gates. Leave the opt-in ReleaseKit publication exercise unselected.
- Record actual commands, results, fixture dimensions, relevant comparisons, and unresolved issues in the stage's evidence field. Never replace pending evidence with an expected result. Investigate a regression before proceeding; absence of a timing speedup is not permission to invent a faster contract.
- Review staged changes and commit each completed stage under the agent policy. Report its commit, checks, and remaining work. Proceed only when its completion conditions hold. Update the roadmap's overall status when implementation begins and ends; detailed execution results belong here.
No concurrency guarantee, public freeze step, new parser, provider cache, library version change, or release publication is needed. Application callbacks retain their effects. Do not forbid registration from callbacks to simplify caching; refresh metadata-dependent presentation when successful registration has made the invocation's prepared metadata stale, without repeating input callbacks.
Measurement protocol
Use standard Go benchmarks named with prefix BenchmarkPerformance. Use the
same fixtures and measurement scope before and after a change. Record Go version,
GOOS/GOARCH, CPU/machine description, source revision, and fixture dimensions.
Separate app construction plus first execution from execution on an existing
app. Exclude fixture construction from repeated-execution measurements and include
it only when explicitly measuring construction/registration.
Use in-memory provider fixtures and io.Discard for ordinary delivery
measurements. Observe outcomes so invalid fixtures cannot appear as fast success.
Actual ConfigKit/PromptKit loading is covered by existing integration tests;
these measurements isolate CommandKit overhead and do not contact providers.
Keep a few representative shapes rather than a Cartesian product of dimensions.
With module caches prepared, the common measurement command is:
GOWORK=off GOTOOLCHAIN=local GOPROXY=off GONOPROXY=none GOSUMDB=off go test -mod=readonly -run '^$' -bench '^BenchmarkPerformance' -benchmem -benchtime=200ms -count=5 . ./internal/cobra
Narrow -bench and the package list for each stage. Save raw output outside the
worktree and summarize representative ns/op, B/op, and allocs/op results here,
including variation or inconclusive differences. When measurements span a
different machine or Go version, repeat the baseline in an isolated checkout
with the same benchmark sources before making a timing claim. Profile CPU or
allocations only when needed to explain a remaining cost or regression. No new
benchmark-analysis dependency or permanent numeric pass threshold is selected.
Stage inventory
| Stage | Deliverable | Roadmap coverage |
|---|---|---|
| 1 | Dispatch and registration baseline benchmarks | PF-02, PF-03, PF-05 |
| 2 | Inspection and scope baseline benchmarks | PF-01, PF-04, PF-06 |
| 3 | PromptKit rendering for the selected format | PF-01 |
| 4 | Direct declaration lookup and ancestry traversal | PF-02 |
| 5 | YAML preamble analysis with complete framed delivery | PF-04, OD-2 |
| 6 | Registration without copying existing entries | PF-05 |
| 7 | Cached immutable bridge metadata | PF-03, OD-1 |
| 8 | An invocation-owned adapter session | PF-03, OD-1 |
| 9 | Root dispatch, help, and usage integration | PF-03, OD-1 |
| 10 | Prepared configuration suite metadata | PF-06 |
| 11 | Final behavioral/performance audit and documentation | PF-01–06 |
Stage 1: establish dispatch and registration measurements
Status: complete. Prerequisite: orientation and this plan.
Files: new performance_dispatch_test.go,
internal/cobra/performance_test.go, and evidence here. Production code stays
unchanged in this stage.
- Create small test-only command fixtures: a small flat registry with 8 custom commands, a wide registry with 128 custom commands, and a nested path of depth 24 with sibling commands. Add one flag-heavy leaf case with 24 flags. These dimensions are benchmark inputs, not supported-size limits.
- Add root benchmarks for repeated success, local
--help, explicithelp COMMAND, parser usage failure, and handler-returned usage. Use the small fixture for the full path set and the wider/deeper fixtures for success and help. Add construction plus first-execution cases for small and wide apps. Use real routing and bindings with a cheap typed handler and stable streams. - Add adapter
Parsebenchmarks for shallow/wide and deep neutral trees to isolate PF-02 from root report work. Keep the neutral declarations outside the timed region; include backend construction in theParsemeasurement. - Add registration benchmarks comparing individual and batch registration of
8 and 128 preconstructed declarations. Include fresh
Newplus registration in both timed paths so the comparison has the same construction overhead. - Run and record the baseline under the measurement protocol. Preserve benchmark inputs for subsequent stages. Verify setup and expected outcome classes; fix the fixture if it unexpectedly measures an error path.
Validation: compile both packages with go test -mod=readonly -run '^$' . ./internal/cobra, then execute the new benchmarks. No timing assertions,
production instrumentation, or full dependency test matrix is needed.
Completion: reproducible baseline results distinguish registration, cold execution, repeated execution, parsing, and help/usage costs.
Evidence: Benchmark fixtures were measured from source revision 1274ae7
with the new benchmark files in the working tree (committed with this evidence).
Environment: Go 1.26.8, linux/amd64, Linux 6.17.0-41-generic, Intel Core
i5-7260U CPU @ 2.20 GHz. The required compile-only command passed:
GOWORK=off GOTOOLCHAIN=local GOPROXY=off GONOPROXY=none GOSUMDB=off go test -mod=readonly -run '^$' . ./internal/cobra.
The five-sample measurement command also passed:
GOWORK=off GOTOOLCHAIN=local GOPROXY=off GONOPROXY=none GOSUMDB=off go test -mod=readonly -run '^$' -bench '^BenchmarkPerformance' -benchmem -benchtime=200ms -count=5 . ./internal/cobra.
Raw output is in /tmp/commandkit-performance-baseline.txt; focused repeat output
for noisy cases is in /tmp/commandkit-performance-baseline-repeat.txt.
Fixtures: root dispatch used 8 and 128 flat commands, a 24-level path with
siblings at each parent, and a leaf with 24 flags. Dispatch covered success,
local and explicit help, parser and handler usage, cold construction plus first
execution, and repeated execution. Registration used 8 and 128 prebuilt
declarations with fresh New in individual and batch paths. Adapter parsing
used 128 shallow siblings and the same depth-24 path. All repeated app and
adapter fixtures were built outside the timed loops; benchmark outcomes were
checked for their intended classes.
Representative five-sample ranges (ns/op; B/op; allocs/op): small repeated success 31,066–41,041; 33,209–33,210; 287. Small local help 103,022–116,183; 78,176–78,177; 810. Parser usage 62,596–74,475; 66,363–66,364; 591. Wide success 218,073–253,010; 269,063–269,070; 1,898. Deep success 119,958–139,625; 139,787–139,791; 1,074. Wide help 491,172–622,188; 549,923–549,940; 4,033. Explicit help measured 101,998–111,895 ns/op, 78,152–78,153 B/op, and 815 allocs/op; handler usage measured 61,878–72,264 ns/op, 66,211–66,212 B/op, and 583 allocs/op. Cold construction plus first execution measured 38,152–50,235 ns/op, 38,275–38,276 B/op, and 339 allocs/op for the small app, and 251,945–315,276 ns/op, 291,822–291,827 B/op, and 2,184 allocs/op for the wide app. The 24-flag leaf focused repeat was 43,662–56,492 ns/op, 40,027–40,028 B/op, and 401 allocs/op. Deep help was noisy in the full run (429,873–1,634,363 ns/op); a focused repeat was 300,155–326,094 ns/op, 294,882–294,896 B/op, and 2,408 allocs/op. The initial flag-heavy timing also varied widely before the focused range reported above. Registration at 8 declarations measured individual 7,141–7,645 ns/op, 5,497 B/op, 64 allocs/op versus batch 4,159–5,190 ns/op, 3,456 B/op, 48 allocs/op. At 128 declarations, individual measured 414,618–492,261 ns/op, 405,011–405,018 B/op, 784 allocs/op versus batch 33,985–36,207 ns/op, 23,013–23,014 B/op, 288 allocs/op. Adapter parse measured shallow/wide 150,918–171,598 ns/op, 242,936 B/op, 1,705 allocs/op, and deep 86,282–109,697 ns/op, 119,920 B/op, 827 allocs/op. These are baselines, not speedup claims; the large initial deep-help and flag-heavy timing variation shows why later comparisons should report run variation.
Stage 2: establish report and scope measurements
Status: complete. Prerequisite: Stage 1.
Files: new performance_reports_test.go and evidence here. Production code
stays unchanged in this stage.
- Use direct
PromptKitSuitecallbacks returning fixed inspection records. Measure prompt text/YAML with 4 and 128 inputs, and profile text/YAML with small and larger nestedExtraParamsmaps. Construct fixtures outside the timed loop; useApp.Executeso parsing and delivery are represented. - Add isolated framing and aggregate
config show --scope allbenchmarks with two native document fixtures: approximately 4 KiB and 1 MiB with many short lines. Keep native content and provider callbacks fixed. Include an ordinary preamble and a directive-bearing preamble, without multiplying every size by every marker variation. Measure single-scope display as a reference case. - Measure
config scopes, named selection, and aggregate validation with 2 and 32 scopes. Give every aggregate fixture the requested capability and use cheap acquisition callbacks. Add suite registration with 4 and 64 distinct selectors to expose duplicate-check cost without using duplicate declarations. - Run and record these baselines before handler or scope changes. Label isolated helper results separately from complete command execution. Adjust internal benchmark calls after later signature refactors only as necessary to measure the same logical operation and fixtures.
Validation: compile the root package and run the new report/scope benchmarks under the common protocol. Confirm the fixtures produce the intended formats and success outcomes outside timed setup; existing integration coverage remains the owner of actual library loading semantics.
Completion: PF-01, PF-04, and PF-06 have comparable baseline measurements.
Evidence: Fixtures were measured from source revision aed4c59 with
performance_reports_test.go in the working tree (committed with this evidence).
Environment matched the first baseline: Go 1.26.8, linux/amd64, Linux
6.17.0-41-generic, Intel Core i5-7260U CPU @ 2.20 GHz. Root package compilation
passed with GOWORK=off GOTOOLCHAIN=local GOPROXY=off GONOPROXY=none GOSUMDB=off go test -mod=readonly -run '^$' .. The five-sample benchmark command passed:
GOWORK=off GOTOOLCHAIN=local GOPROXY=off GONOPROXY=none GOSUMDB=off go test -mod=readonly -run '^$' -bench '^BenchmarkPerformance(PromptReports|YAMLFraming|ConfigScopes|ConfigRegistration)$' -benchmem -benchtime=200ms -count=5 ..
Raw output is in /tmp/commandkit-performance-reports-baseline.txt.
Fixtures: PromptKit callbacks returned prebuilt inspection records directly;
prompt reports used 4 and 128 input records, while profile ExtraParams used
2 groups with 3 nested values each or 24 groups with 8 nested values each and a
nested sequence. Each was measured as text and YAML through App.Execute.
Native YAML fixtures contain unique short mapping keys and are 4,096 bytes with
an ordinary comment preamble and 1,048,591 bytes with a %YAML directive
preamble. Framing was measured directly; aggregate config show --scope all
measured two documents of each selected fixture size/style, and single-scope
display used the 4 KiB ordinary document. Scope operations used 2 or 32 scopes
with the requested show/validate capabilities and cheap in-memory acquisition
callbacks. Suite registration used 4 or 64 distinct selectors and a fresh app.
The scope counts also covered discovery, named validation, and aggregate
validation. Setup verified success and report shape outside timed loops; native
fixtures, records, suites, and apps were prepared outside repeated loops.
Representative five-sample ranges (ns/op; B/op; allocs/op): prompt, 4 inputs, text 41,287–52,115; 26,160–26,161; 294, and YAML 88,991–104,671; 74,730–74,733; 557. Prompt, 128 inputs, text 169,155–198,281; 145,524–145,531; 1,171, and YAML 1,418,692–1,490,181; 1,393,508–1,393,568; 6,039–6,040. Profile, small extra map, text 74,109–89,337; 70,914–70,918; 462, and YAML 161,470–368,813; 162,688–162,717; 755. Profile, large extra map, text 1,277,396–2,583,950; 836,141–836,296; 3,027–3,028, and YAML 1,756,571–2,377,499; 1,586,728–1,586,849; 5,878–5,879. Larger profile timings varied, while allocation counts and bytes were stable.
Direct framing measured 4 KiB ordinary at 5,883–6,842 ns/op, 11,056 B/op, 3 allocs/op, and 1 MiB directive at 1,208,678–1,502,417 ns/op, 2,539,520–2,539,523 B/op, 2 allocs/op. Aggregate show measured 4 KiB documents at 44,395–48,161 ns/op, 55,537–55,538 B/op, 211 allocs/op, and 1 MiB documents at 4,818,907–6,009,877 ns/op, 8,695,182–8,695,197 B/op, 215–216 allocs/op. Single-scope 4 KiB display measured 20,229–21,791 ns/op, 17,935–17,936 B/op, 183 allocs/op. With 2 scopes, discovery measured 23,840–30,007 ns/op, 21,161–21,162 B/op, 223 allocs/op; named validation 24,841–26,340 ns/op, 21,817 B/op, 228 allocs/op; aggregate validation 25,231–31,126 ns/op, 22,361–22,362 B/op, 237 allocs/op. With 32 scopes, discovery measured 31,439–37,167 ns/op, 28,141–28,142 B/op, 257 allocs/op; named validation 29,123–74,374 ns/op, 22,775–22,778 B/op, 228 allocs/op; aggregate validation 45,548–63,471 ns/op, 29,781–29,782 B/op, 269 allocs/op. Registration with 4 selectors measured 12,573–14,789 ns/op, 8,911 B/op, 104 allocs/op; with 64 it measured 91,675–105,548 ns/op, 82,817–82,818 B/op, 485 allocs/op. Large-report cost includes full output framing/delivery; no segmented-delivery comparison was selected. These measurements establish baselines and show timing variation; they are not fixed thresholds or speedup claims.
Stage 3: render only the selected PromptKit format
Status: complete. Prerequisite: Stage 2.
Files: promptkit.go, promptkit_test.go, relevant report benchmarks, and evidence here.
- In
runPromptKitPromptandrunPromptKitProfile, select YAML or text before calling either text formatter. Preserve report DTO construction and all acquisition, cancellation, nil-result, and diagnostic handling. - For YAML, encode into a
bytes.Bufferand use its bytes directly. For text, format once and convert the completed string to bytes without copying it through an additional builder. Preserve profile text-formatting errors. - Reduce
writePromptKitReportto shared delivery of completed bytes plus diagnostics. Keep the pre-delivery cancellation check, checked writer, and incomplete-report outcome handling. Both formats finish rendering before this delivery step. No lazy public formatter API is needed. - Reuse the real inspection tests for report fields, input order, nil parameter
maps, nested
ExtraParams, cancellation, and output failures. Add focused missing cases rather than collaborator-call assertions or broad snapshots. - Rerun prompt/profile measurements and compare them with Stage 2. Explain any text-path regression or unexpectedly unchanged YAML allocation result.
Validation: root PromptKit inspection and cancellation tests, followed by
the applicable report benchmarks. Use go test -mod=readonly -count=1 . for
root behavioral validation. Preserve public error identity and existing
report confidentiality; this change introduces no redaction policy.
Completion: YAML requests do not construct text reports, and text delivery has no redundant intermediate builder.
Evidence: go test -mod=readonly -count=1 . passed with the workspace,
local toolchain, and offline environment settings from the measurement protocol.
The same five-sample report fixtures passed with:
GOWORK=off GOTOOLCHAIN=local GOPROXY=off GONOPROXY=none GOSUMDB=off go test -mod=readonly -run '^$' -bench '^BenchmarkPerformancePromptReports$' -benchmem -benchtime=200ms -count=5 ..
The environment matched Stage 2: Go 1.26.8, linux/amd64, Linux
6.17.0-41-generic, Intel Core i5-7260U CPU @ 2.20 GHz. The measured source was
revision a431540 plus the promptkit.go working-tree change committed with
this evidence. Raw output is in /tmp/commandkit-performance-prompt-after.txt.
Every five-sample report case reduced allocation count. Prompt, 4 inputs, text changed from 41,287–52,115 ns/op, 26,160–26,161 B/op, 294 allocs/op to 29,898–35,259 ns/op, 25,280–25,282 B/op, 291 allocs/op; YAML changed from 88,991–104,671 ns/op, 74,730–74,733 B/op, 557 allocs/op to 83,454–91,286 ns/op, 71,853–71,855 B/op, 505 allocs/op. Prompt, 128 inputs, text changed from 169,155–198,281 ns/op, 145,524–145,531 B/op, 1,171 allocs/op to 161,185–185,023 ns/op, 128,960–128,966 B/op, 1,168 allocs/op; YAML changed from 1,418,692–1,490,181 ns/op, 1,393,508–1,393,568 B/op, 6,039–6,040 allocs/op to 1,255,623–1,360,926 ns/op, 1,264,474–1,264,486 B/op, 5,104 allocs/op.
Profile, small extra map, text changed from 74,109–89,337 ns/op, 70,914–70,918 B/op, 462 allocs/op to 75,818–88,197 ns/op, 69,955–69,959 B/op, 459 allocs/op; the timing ranges overlap. YAML changed from 161,470–368,813 ns/op, 162,688–162,717 B/op, 755 allocs/op to 107,244–127,744 ns/op, 114,793–114,795 B/op, 533 allocs/op. Profile, large extra map, text changed from 1,277,396–2,583,950 ns/op, 836,141–836,296 B/op, 3,027–3,028 allocs/op to 839,692–936,229 ns/op, 826,407–826,448 B/op, 3,024 allocs/op; YAML changed from 1,756,571–2,377,499 ns/op, 1,586,728–1,586,849 B/op, 5,878–5,879 allocs/op to 796,779–976,685 ns/op, 780,391–780,436 B/op, 3,086 allocs/op. These results confirm that YAML requests avoid text-report construction; text requests retain their report semantics and formatting errors.
Stage 4: index backend declarations and traverse parents
Status: complete. Prerequisite: Stage 3.
Files: adapter, adapter tests, relevant benchmarks, and evidence here.
- Change the construction index to map backend command pointers directly to
neutral declarations. Update
Parse,RenderHelp, andUsageLineto use direct lookup instead of scanning declaration-to-backend entries. - Reconstruct a selected path by walking the backend's
Parent()links and reading each mapped declaration's name, excluding the root. Reverse the collected names once. Remove recursive root-based parent discovery. - Keep neutral failures for missing trees/unmapped selections and exact-path verification. Do not rely on Cobra prefix matching to define accepted paths.
- Extend adapter coverage only where needed for a deep path, wide siblings, unknown paths, root selection, and help/usage path resolution. Existing parsing and flag-state tests remain applicable.
- Compare adapter and complete dispatch benchmarks with Stage 1. The tree still rebuilds here; distinguish reduced path-search cost from that remaining cost.
Validation: go test -mod=readonly -count=1 ./internal/cobra ., then the applicable
parse and dispatch benchmarks.
Completion: declaration lookup uses the index and ancestry traversal is proportional to selected depth after backend construction.
Evidence: go test -mod=readonly -count=1 ./internal/cobra . passed with
the common offline environment settings. The focused test covers a 24-level
path, 128 siblings, root selection, unknown help/usage paths, and exact help and
usage resolution. Existing exact-path and parse failure tests remain in place.
Measurements used Go 1.26.8, linux/amd64, Linux 6.17.0-41-generic, Intel Core
i5-7260U CPU @ 2.20 GHz, based on source revision 628dc78 with this stage's
changes in the working tree. The five-sample adapter command was
GOWORK=off GOTOOLCHAIN=local GOPROXY=off GONOPROXY=none GOSUMDB=off go test -mod=readonly -run '^$' -bench '^BenchmarkPerformanceParse$' -benchmem -benchtime=200ms -count=5 ./internal/cobra;
the dispatch command used the same flags with -bench '^BenchmarkPerformanceDispatch$' .. Both passed. Raw outputs are in
/tmp/commandkit-performance-parse-after-stage4.txt and
/tmp/commandkit-performance-dispatch-after-stage4.txt.
Adapter shallow/wide Parse measured 149,426–171,199 ns/op, 242,936 B/op, and 1,705 allocs/op, compared with baseline 150,918–171,598 ns/op, 242,936 B/op, and 1,705 allocs/op. Deep Parse measured 90,608–104,770 ns/op, 119,536 B/op, and 826 allocs/op, compared with baseline 86,282–109,697 ns/op, 119,920 B/op, and 827 allocs/op. Timings overlap; deep parsing saves 384 B and one allocation. End-to-end dispatch allocations remain effectively unchanged across the measured cases. Timing variation, including deep-help outliers, is substantial, so these runs do not support an end-to-end speedup claim. The comparison isolates the selected-path lookup change while backend tree construction remains in each parse.
Stage 5: scan YAML preambles without splitting documents
Status: complete. Prerequisite: Stage 4.
Files: show.go, show_test.go, configkit_test.go, framing benchmarks, and evidence.
- Introduce a private preamble analysis using
bytes.IndexByteto locate line boundaries and byte trimming for classification. Track marker positions, preamble end, directives, and inline marker content. Stop when the opening framing decision is complete. Preserve Unicode whitespace behavior and accepted space/tab marker forms; do not add a general YAML parser or limits. - Have
frameEffectiveYAMLreturn complete framed bytes and a directive flag from that analysis. Update its internal callers/tests for the result pair. Remove the independent full-document split inhasYAMLDirectiveand use the same analysis for aggregate completion-marker decisions. - Retain the complete native buffer and framed buffer per document. Preserve placement of labels around inline content/block scalars, authored comments, directives, native byte slices, quoted scope labels, and boundary newlines. Keep the existing aggregate document-end marker where directives require it.
- Leave acquisition-before-output, direct single-scope writing, cancellation boundaries, and write-error outcomes intact. Do not introduce segmented native delivery, indentation wrappers, or reserialization of provider documents.
- Reuse
TestEffectiveYAMLFramingPreservesOneNativeDocumentand aggregate acquisition/delivery tests. Fill meaningful gaps for absent trailing newline, CRLF, directive preambles, and control characters in labels. Verify native content preservation as well as decoded values and document count. - Compare framing/aggregate measurements with Stage 2, emphasizing allocation growth with line count. Report the full framed copy as an intentional retained cost under OD-2.
Validation: root framing/configuration behavior with
go test -mod=readonly -count=1 ., then the relevant report benchmarks.
Completion: each document has one opening analysis, no document-wide line index, and the selected complete-buffer delivery model.
Evidence: GOWORK=off GOTOOLCHAIN=local GOPROXY=off GONOPROXY=none GOSUMDB=off go test -mod=readonly -count=1 . passed. Existing aggregate tests
continue to cover acquisition-before-output, directive document separators,
decoded values, diagnostics, and writer failures. New framing cases cover a
missing final newline, CRLF, a directive preamble, a control character in the
scope label, unchanged native bytes, decoded values, and one-document output.
Measurements used Go 1.26.8, linux/amd64, Linux 6.17.0-41-generic, Intel Core
i5-7260U CPU @ 2.20 GHz, based on source revision ae656d8 with this stage's
changes in the working tree. The five-sample command passed:
GOWORK=off GOTOOLCHAIN=local GOPROXY=off GONOPROXY=none GOSUMDB=off go test -mod=readonly -run '^$' -bench '^BenchmarkPerformanceYAMLFraming$' -benchmem -benchtime=200ms -count=5 ..
Raw output is in /tmp/commandkit-performance-yaml-after-stage5.txt.
Direct 4 KiB ordinary framing measured 1,135–1,455 ns/op, 4,864 B/op, and one allocation, compared with 5,883–6,842 ns/op, 11,056 B/op, and three allocations at baseline. Direct 1 MiB directive framing measured 189,303–359,655 ns/op, 1,056,768–1,056,769 B/op, and one allocation, compared with 1,208,678–1,502,417 ns/op, 2,539,520–2,539,523 B/op, and two allocations. Aggregate 4 KiB show measured 29,951–49,108 ns/op, 36,999–37,001 B/op, and 206 allocations, compared with 44,395–48,161 ns/op, 55,537–55,538 B/op, and 211 allocations. Aggregate 1 MiB show measured 1,326,104–1,557,880 ns/op, 4,246,730–4,246,767 B/op, and 211 allocations, compared with 4,818,907–6,009,877 ns/op, 8,695,182–8,695,197 B/op, and 215–216 allocations. Single-scope 4 KiB display remained at 17,935–17,936 B/op and 183 allocations; timing variation overlaps the baseline. The complete framed output still requires a full-size copy under OD-2. The allocation reduction comes from eliminating document-wide split/index storage and reusing the opening analysis for framing and aggregate directive handling.
Stage 6: install validated registrations without copying the registry
Status: complete. Prerequisite: Stage 5.
Files: registry.go, registry_test.go, registration benchmarks, and evidence.
- Preserve the nil-app check. Validate each declaration and check its name against both existing registrations and pending entries. Clone declarations into private pending storage before modifying the app.
- After the whole batch succeeds, initialize the app's map if needed and add the pending entries to it. Leave existing entries untouched. An empty batch remains a successful no-op on a non-nil app.
- Preserve batch failure behavior for invalid later declarations, duplicates within a batch, existing paths, and reserved help/version paths. Keep existing cloning boundaries rather than sharing caller-owned child slices.
- Reuse atomicity and callback-free registration tests. Add a missing late-failure or within-batch collision case only if existing coverage is insufficient.
- Compare individual/batch registration with Stage 1. Sorting and metadata preparation remain separate until Stage 7.
Validation: registry/declaration behavior with
go test -mod=readonly -count=1 ., then registration benchmarks.
Completion: registration copies only pending declarations and preserves all-or-nothing validation without rebuilding the existing registry map.
Evidence: GOWORK=off GOTOOLCHAIN=local GOPROXY=off GONOPROXY=none GOSUMDB=off go test -mod=readonly -count=1 . passed. Existing tests cover
atomic rollback, reserved paths, callback lifecycle, and ownership. Added a
duplicate-within-batch case and confirmed that it leaves the registry unchanged.
Measurements used Go 1.26.8, linux/amd64, Linux 6.17.0-41-generic, Intel Core
i5-7260U CPU @ 2.20 GHz, based on source revision 3f8841f with this stage's
changes in the working tree. The five-sample command passed:
GOWORK=off GOTOOLCHAIN=local GOPROXY=off GONOPROXY=none GOSUMDB=off go test -mod=readonly -run '^$' -bench '^BenchmarkPerformanceRegistration$' -benchmem -benchtime=200ms -count=5 ..
Raw output is in /tmp/commandkit-performance-registration-after-stage6.txt.
At 8 declarations, individual registration measured 4,873–5,401 ns/op, 3,152 B/op, and 45 allocations; batch registration measured 4,404–10,155 ns/op, 3,152 B/op, and 45 allocations (the timing range includes noisy outliers). At 128 declarations, individual registration measured 55,274–64,107 ns/op, 29,399–29,400 B/op, and 293 allocations, compared with the Stage 1 baseline 414,618–492,261 ns/op, 405,011–405,018 B/op, and 784 allocations. Batch registration measured 47,867–53,858 ns/op, 35,969–35,970 B/op, and 296 allocations, compared with baseline 33,985–36,207 ns/op, 23,013–23,014 B/op, and 288 allocations. A slice-only pending check reduced the batch allocation cost to about 30.5 KB but was slower (about 61–66 μs) because it scans pending names quadratically; the map-based pending index is retained for linear-time duplicate validation. The batch regression remains measurable and reflects growing the existing map plus maintaining pending entries; restoring a full registry rebuild would contradict this stage's selected change. Individual registration has a large allocation and latency reduction. No correctness issues remain open.
Stage 7: cache immutable bridge metadata per app
Status: complete. Prerequisite: Stage 6.
Files: app.go, parser.go, registry invalidation, root parser/registry tests, dispatch benchmarks, and evidence.
- Add a private cached neutral tree pointer to
App. Split bridge translation into a cache accessor and an uncached builder. Build lazily on first use; sort root names and child lists once per registry state while creating owned flag/operand metadata. The cached neutral tree is read-only thereafter. - Invalidate the pointer at successful nonempty registration's completion point. Failed and empty registrations retain valid cached metadata. Built-in construction remains before first use; do not create metadata eagerly for each individual command registration.
- Keep this cache free of contexts, streams, parsed values, provider views, and Cobra/pflag objects. Adapter construction reads the neutral tree without mutating it. A pointer to the neutral tree identifies a prepared metadata generation for Stage 9, avoiding a separate counter.
- Protect observable behavior after success, failure, and empty registration following earlier execution/help. Check that newly installed configuration and PromptKit families appear after an earlier help request without provider loading. Preserve owned declarations and sorted metadata.
- Compare cold and repeated execution with Stage 1. Account for the one retained neutral tree per app and confirm rebuilding occurs only when needed through source review and measurements, not public allocation/count assertions.
Validation: root registry/parser/help tests and dispatch benchmarks. Each
invocation still creates fresh backend values at this stage. Use
go test -mod=readonly -count=1 . for root behavioral validation.
Completion: unchanged registrations reuse translated metadata; successful registration is visible to later parsing and help.
Evidence: GOWORK=off GOTOOLCHAIN=local GOPROXY=off GONOPROXY=none GOSUMDB=off go test -mod=readonly -count=1 . passed. The new behavioral test
builds help metadata, registers a command and both standard command families,
then confirms later help and parsing see them without invoking provider
callbacks. It also checks visibility after failed and empty registration.
Source review confirms the tree is built lazily, contains only neutral
declarations/flag/operand metadata, and is cleared only after successful
nonempty registration. Root and child ordering is performed by the uncached
builder once per metadata generation.
Measurements used Go 1.26.8, linux/amd64, Linux 6.17.0-41-generic, Intel Core
i5-7260U CPU @ 2.20 GHz, based on source revision 5816dd3 with this stage's
changes in the working tree. The five-sample command passed:
GOWORK=off GOTOOLCHAIN=local GOPROXY=off GONOPROXY=none GOSUMDB=off go test -mod=readonly -run '^$' -bench '^BenchmarkPerformanceDispatch$' -benchmem -benchtime=200ms -count=5 ..
Raw output is in /tmp/commandkit-performance-dispatch-after-stage7.txt.
Repeated small success changed from 31,066–41,041 ns/op, 33,209–33,210 B/op, and 287 allocations to 26,008–31,223 ns/op, 29,408–29,409 B/op, and 267 allocations. Wide success changed from 218,073–253,010 ns/op, 269,063–269,070 B/op, and 1,898 allocations to 182,431–194,243 ns/op, 247,626–247,635 B/op, and 1,756 allocations. Deep success changed from 119,958–139,625 ns/op, 139,787–139,791 B/op, and 1,074 allocations to 103,208–124,879 ns/op, 124,240–124,242 B/op, and 879 allocations. Help and usage paths also reduced allocations and bytes. Cold construction plus first execution remained similar for the small app (38,152–50,235 ns/op, 38,275–38,276 B/op, 339 allocations baseline; 39,158–47,365 ns/op, 38,243 B/op, 338 allocations now). The wide cold case changed from 251,945–315,276 ns/op, 291,822–291,827 B/op, and 2,184 allocations to 271,403–321,746 ns/op, 305,046–305,051 B/op, and 2,194 allocations. Its timing range overlaps baseline; the extra cold storage is the retained neutral tree. The measured tradeoff is lower repeated-execution cost with one tree allocation retained per app metadata generation.
Stage 8: introduce an invocation-owned adapter session
Status: not started. Prerequisite: Stage 7.
Files: the adapter, its tests/benchmarks, and evidence. Root execution remains functional through the existing package-level adapter helpers during this stage.
- Add an internal
Sessionhandle with private backend root, neutral metadata, and reverse declaration index. UsePrepare(*Command, ParseOptions) *Session; construction normalizes options and builds a fresh mutable tree. A missing tree is represented by a session whose operations return neutral failures. Methods areParse([]string) Result,RenderHelp([]string) (string, *Failure), andUsageLine([]string) (string, *Failure). They expose no backend types. - Move existing parse/help/usage logic into those methods. Reuse the prepared tree and Stage 4's index/path traversal. Help renders to a private buffer; temporarily redirect and restore the selected backend's output/error streams so generated bytes remain under root delivery control.
- Keep package-level helpers as thin one-session conveniences for current root calls and focused adapter benchmarks/tests. They must delegate to the same methods, not maintain parallel parsing or help implementations.
- Keep malformed/nil-tree results neutral. Metadata remains read-only; parsed values belong to the session. Test parse followed by help and usage on one session, and fresh values/streams on separately prepared sessions. Do not add a public session API, pool, persistent backend cache, or reset framework.
- Add a small parse-plus-help session benchmark using an existing fixture. Label it as a new internal measurement; existing end-to-end benchmarks are still the before/after evidence for root integration in Stage 9.
Validation: go test -mod=readonly -count=1 ./internal/cobra ., then adapter benchmarks.
Completion: the adapter can perform parsing and metadata presentation on one prepared invocation tree while legacy root entry points still compile.
Evidence: pending.
Stage 9: reuse the adapter session throughout root execution
Status: not started. Prerequisite: Stage 8.
Files: execute.go, builtins.go, private parser coordination, execute_external_test.go, dispatch benchmarks, and evidence.
- Introduce private execution coordination local to
App.Execute, retaining the app, parse options, neutral tree pointer, and session. Prepare after the existing context/stream checks. Keep early nil-app/context outcomes and generic usage fallbacks functional without a usable session. - Route parse, root/group/local help, usage after parse/input failures, and
usage returned by handlers through that local coordination. Keep the existing
input preparation and callback order, including supplied-value validation
for help. Normal domain handlers still receive the public
Invocation. - Handle the reserved top-level
helpcommand explicitly in the dispatcher after its ordinary typed preparation/checks: use its privatehelpInput, resolve the target withhelpCommandTarget, and render through the local session. Share target/render logic with its registered built-in handler. A one-off private convenience may prepare a session for direct internal helper calls; theExecutepath must use its existing session. Preserve runnable help metadata and the semantics ofhelp --helpand unknown targets. - Do not store the active session on
App, add it to publicInvocation, or put it in caller contexts. For help/usage presentation, compare the current cached metadata pointer with the session's pointer. If successful registration by a callback changed metadata, prepare presentation against the new tree without rerunning parsing, factories, checks, or handlers. This preserves registration freedom without caching stale presentation. - Preserve outcome normalization, cause/error identity, diagnostic ownership, delivery errors, and incomplete-report reporting. Remove obsolete duplicate preparation paths once all root execution branches are integrated.
- Reuse the existing help-entry and fresh-invocation tests. Fill gaps for a successful flag-bearing invocation followed by help and omitted/false/empty values, distinct streams/contexts, handler usage, and registration followed by metadata presentation. Test contracts, not exact session-construction counts.
- Compare complete help/error/repeated/cold execution with Stage 1. Stable
metadata should need one backend preparation through each ordinary invocation,
including explicit
help; verify structurally and through measurements.
Validation: root dispatcher/help/outcome/cancellation tests, adapter tests,
with go test -mod=readonly -count=1 . ./internal/cobra, then complete dispatch
benchmarks. Use existing facade boundary coverage/review
to confirm that signatures and backend isolation are unchanged.
Completion: every normal Execute path uses the selected OD-1 lifecycle and
all existing public handler and presentation semantics remain intact.
Evidence: pending.
Stage 10: prepare configuration scope metadata once
Status: not started. Prerequisite: Stage 9.
Files: config.go, selectors.go,
configuration handler files, configuration/cancellation tests, scope benchmarks,
and evidence. Keep public ConfigSuite/ConfigScope unchanged.
- Introduce private
preparedConfigSuite[C]with the normalized public suite, ordered scope slice,map[string]ConfigScope[C]lookup, and discovery DTO. Prepare it once inRegisterConfigafter normalization. Use the existing ordering comparator:app,promptkit, then other identifiers alphabetically. Normalize/copy metadata before preparation so it remains instance-owned. - Replace the previous-selector loop in
normalizeConfigSuitewith seen-name and seen-short maps, retaining validation order, scope-reference deduplication, and the same collision categories. Preserve all existing naming rules. - Pass the private prepared suite into standard command constructors and preflight/run helpers. Use its ordered slice for aggregate selection and its map for named lookup, capability preflight, and acquisition. Update validate/show/sources/explain together; no public integration signature changes.
- Construct the discovery inventory from declared/installed operations during
registration and retain the private DTO. Encode text/YAML at invocation time.
This preparation must not call
NewContext, acquisition, input checking, native writers, or other runtime callbacks. - Keep selection validation in preflight and handlers, now using prepared metadata. Keep selector applicability scans and fresh context projection. Do not add mutable selected-scope memoization or public/internal input hooks solely to avoid a second inexpensive validation pass.
- Reuse discovery ordering/capability, selector projection/requiredness, unknown scope, provider-error, and cancellation coverage. Ensure aggregate operations check every selected capability before acquisition; discovery remains metadata-only. Add only missing post-registration ownership/collision cases.
- Compare scope execution and suite registration with Stage 2, including the normal two-scope case. Record the retained slice/map/DTO cost and any timing tradeoff; extra metadata is private to the registered suite.
Validation: root configuration and cancellation tests plus scope/selector
benchmarks. Use go test -mod=readonly -count=1 . for root behavioral validation.
Preserve native reporting and per-operation output timing.
Completion: scope sorting/discovery preparation occurs at registration, scope lookup is indexed, and duplicate selector checks are linear in selector count plus declared scope references.
Evidence: pending.
Stage 11: audit the completed target and reconcile documentation
Status: not started. Prerequisite: Stages 1–10.
Files: this plan, performance.md,
internal overview, developer/README navigation,
and stale references to the retired ReleaseKit plan. Focused corrections only
where the audit proves an incomplete target.
-
Audit PF-01–06 against source and observed behavior: selected formatting, indexed ancestry, prefix-only YAML analysis, full framed buffering, pending registrations, immutable cache invalidation, invocation sessions across all help/usage paths, and prepared configuration metadata. Identify authoritative evidence for each target rather than infer completion from stage statuses.
-
Run the default suite once for the complete sprint:
GOWORK=off GOTOOLCHAIN=local GOPROXY=off GONOPROXY=none GOSUMDB=off go test -mod=readonly -count=1 ./...Keep live services, the optional release publication exercise, and additional race/platform/coverage gates unselected. Review production imports and exported declarations using existing boundary checks and source evidence.
-
Rerun representative benchmarks with unchanged fixtures and comparable environment. Summarize before/after data by PF ID, separate cold/repeated and isolated/whole-command costs, and record remaining full-tree/full-buffer work. Reuse a recent identical measurement when source and environment are unchanged. Explain material regressions or collect more evidence before declaring success.
-
Update internal documentation to describe implemented cache/session ownership and prepared metadata. Current CLI/GoDoc contracts should remain unchanged; update them only for an actual clarification without inventing a new promise.
-
Update README and
docs/development.mdso current implementation navigation points to this performance plan and roadmap. Inreleasekit.md,initial.md, andremediation.md, remove or replace obsolete ReleaseKit-plan references with the owning completed roadmap/runbook as appropriate. Remove its obsolete final-stage fragment references. Do not preserve the retired stage assignments or restart completed release work. -
Check affected document links/fragments, prose status, and whitespace. Mark the roadmap implemented only when every selected target has evidence. Record the stage commits, measurement revisions, check results, and any qualifications here, then commit the audit and leave a clean workspace.
Validation: the default suite, comparative measurements, facade/import review, and affected documentation/link checks described above. Reuse unchanged stage evidence where it directly proves the final target.
Completion: all six target improvements and both selected approaches are implemented with relevant behavioral and efficiency evidence; maintained docs identify their current owners. No performance guarantee or additional release gate has been introduced.
Evidence: pending.