diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index f9fcd6b..1af761a 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Performance implementation plan -Status: implementation in progress; Stages 1–2 are complete, Stages 3–11 have not started. +Status: implementation in progress; Stages 1–3 are complete, Stages 4–11 have not started. Date: 2026-10-09. @@ -312,7 +312,7 @@ they are not fixed thresholds or speedup claims. ## Stage 3: render only the selected PromptKit format -**Status:** not started. **Prerequisite:** Stage 2. +**Status:** complete. **Prerequisite:** Stage 2. **Files:** [promptkit.go](../../promptkit.go), [promptkit_test.go](../../promptkit_test.go), relevant report benchmarks, and @@ -342,7 +342,37 @@ 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:** pending. +**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 diff --git a/promptkit.go b/promptkit.go index 2efadca..a813b82 100644 --- a/promptkit.go +++ b/promptkit.go @@ -1,10 +1,10 @@ package commandkit import ( + "bytes" "context" "errors" "fmt" - "io" "strconv" "strings" @@ -290,7 +290,17 @@ func runPromptKitPrompt[C any](invocation Invocation, suite PromptKitSuite[C], i return promptKitFailure(fmt.Errorf("PromptKit prompt inspection returned no result"), acquired.Diagnostics) } report := promptReportFromInspection(acquired.Value) - return writePromptKitReport(invocation, input.Format, report, formatPromptText(report), acquired.Diagnostics) + var output []byte + if input.Format == "yaml" { + var buffer bytes.Buffer + if err := encodeYAML(&buffer, report); err != nil { + return promptKitFailure(err, acquired.Diagnostics) + } + output = buffer.Bytes() + } else { + output = []byte(formatPromptText(report)) + } + return writePromptKitReport(invocation, output, acquired.Diagnostics) } func runPromptKitProfile[C any](invocation Invocation, suite PromptKitSuite[C], input promptKitInput[C]) Outcome { @@ -312,11 +322,21 @@ func runPromptKitProfile[C any](invocation Invocation, suite PromptKitSuite[C], return promptKitFailure(fmt.Errorf("PromptKit profile inspection returned no result"), acquired.Diagnostics) } report := profileReportFromInspection(acquired.Value) - textReport, err := formatProfileText(report) - if err != nil { - return promptKitFailure(err, acquired.Diagnostics) + var output []byte + if input.Format == "yaml" { + var buffer bytes.Buffer + if err := encodeYAML(&buffer, report); err != nil { + return promptKitFailure(err, acquired.Diagnostics) + } + output = buffer.Bytes() + } else { + textReport, err := formatProfileText(report) + if err != nil { + return promptKitFailure(err, acquired.Diagnostics) + } + output = []byte(textReport) } - return writePromptKitReport(invocation, input.Format, report, textReport, acquired.Diagnostics) + return writePromptKitReport(invocation, output, acquired.Diagnostics) } func promptKitFailure(err error, diagnostics []Diagnostic) Outcome { @@ -325,19 +345,11 @@ func promptKitFailure(err error, diagnostics []Diagnostic) Outcome { return outcome } -func writePromptKitReport(invocation Invocation, format string, report any, textReport string, diagnostics []Diagnostic) Outcome { - var output strings.Builder - if format == "yaml" { - if err := encodeYAML(&output, report); err != nil { - return promptKitFailure(err, diagnostics) - } - } else if _, err := io.WriteString(&output, textReport); err != nil { - return promptKitFailure(err, diagnostics) - } +func writePromptKitReport(invocation Invocation, output []byte, diagnostics []Diagnostic) Outcome { if err := invocation.Context.Err(); err != nil { return promptKitFailure(err, diagnostics) } - if err := writeReport(invocation.Streams.Out, []byte(output.String())); err != nil { + if err := writeReport(invocation.Streams.Out, output); err != nil { outcome := incompleteReport(err) outcome.Diagnostics = diagnostics return outcome