From 9dc87ae996bbf68c2bbf09389de59aa3b85a4674 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Mon, 2 Nov 2026 03:36:53 +0000 Subject: [PATCH] Record deferred resource assessments --- docs/roadmap/implementation.md | 48 +++++++- .../validation/file_size_benchmark_test.go | 109 ++++++++++++++++++ 2 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 internal/validation/file_size_benchmark_test.go diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index de53c83..88f6914 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -53,7 +53,7 @@ the agent policy; an intermediate compilation dependency is recorded accurately. | 15 | A13 | Complete — validation ordering, scope, tree lifecycle, diagnostics, and public callback checks passed. | | 16 | A15 | Complete — the full CLI command suite passed after workflow extraction. | | 17 | Remaining A19, A20 | Complete — Git, identity, and Markdown tests passed; delivery benchmark ran for fresh uploads and matching retries. | -| 18 | A11, A17: assessment only | Pending | +| 18 | A11, A17: assessment only | Assessment complete; production JSON limits and exclusion indexing remain deferred. Evidence follows below. | | 19 | Integrated verification and current documentation | Pending | ## Stage 1: Correct the owned tag push @@ -536,6 +536,52 @@ production behavior remains as defined in [audit.md](audit.md). Evidence: both deferred items have a concrete disposition and useful evidence without inventing unapproved protocol limits or performance thresholds. +### Assessment evidence + +**A11 — observed local sizes, not limits.** The Gitea tests construct repository, +release, tag, and attachment responses in code; the public delivery fixture also +constructs release and attachment lists with variable URLs, note bodies, and +asset counts. Representative encoded fixture responses, including the JSON +encoder's trailing newline, measured 81 bytes for repository identity, 136 +bytes with permissions, 125 bytes for a sparse release response, 144 bytes for +a populated release response, 73 bytes for a one-item attachment list, and 62 +bytes for an attachment upload response. The four literal release-ID fixtures +in `internal/gitea/backend_test.go` are 21, 28, 29, and 29 bytes without a +newline. Empty, malformed, and missing-field responses are also represented; +generated list and note lengths vary with the fixture inputs. + +The eight checked-in release notes measured: `v0.1.0` 3,400 bytes, `v0.2.0` +6,086, `v0.3.0` 6,006, `v0.4.0` 7,899, `v0.4.1` 1,235, `v0.4.2` 1,826, +`v0.5.0` 3,179, and `v0.6.0` 7,297. These local examples do not establish +deployment or protocol maxima. JSON size/time caps and new configuration remain +deferred; existing cancellation and ambiguous-write handling remain in force. + +**A17 — benchmark-only index.** On Linux/amd64, Go 1.26.8, Intel Core +i5-7260U, the following command ran 64 combinations, three samples each: + +```sh +PATH=/home/eric/sdk/go1.26.8/bin:$PATH TMPDIR=/var/tmp \ + GOCACHE=/tmp/releasekit-go-cache GOTOOLCHAIN=local GOPROXY=off GOSUMDB=off \ + go test ./internal/validation -run '^$' -bench '^BenchmarkFileSizeExclusions$' \ + -benchtime=100ms -count=3 +``` + +Each operation normalizes exclusions and, for the prototype, builds one index. +Paths and fixture strings are prepared outside the timed interval. Median +times and allocations for representative cases were: + +| Paths / exclusions / case | Scan | Index | +| --- | --- | --- | +| 1,000 / 1 / shallow early match | 24.5 µs, 16 B, 1 allocation | 44.0 µs, 16 B, 1 allocation | +| 1,000 / 1 / deep nonmatch | 25.7 µs, 16 B, 1 allocation | 119.0 µs, 16 B, 1 allocation | +| 10,000 / 1,000 / shallow early match | 270 µs, 16,384 B, 1 allocation | 564 µs, 70,992 B, 6 allocations | +| 10,000 / 1,000 / shallow nonmatch | 227.0 ms, 16,384 B, 1 allocation | 521 µs, 70,992 B, 6 allocations | +| 10,000 / 1,000 / deep nonmatch | 233.4 ms, 16,384 B, 1 allocation | 1.36 ms, 70,992 B, 6 allocations | + +The prototype is slower for small lists and early matches but substantially +faster for the large nonmatching list, at a higher allocation cost. Production +matching remains unchanged; these observations support a later adoption decision. + ## Stage 19: Verify the complete target and update current documentation 1. Inspect the combined implementation against all A01–A20 outcomes and the diff --git a/internal/validation/file_size_benchmark_test.go b/internal/validation/file_size_benchmark_test.go new file mode 100644 index 0000000..d4a77bc --- /dev/null +++ b/internal/validation/file_size_benchmark_test.go @@ -0,0 +1,109 @@ +package validation + +import ( + "fmt" + "strings" + "testing" +) + +// benchmarkExclusionIndex is an assessment-only alternative, not production logic. +func benchmarkExclusionIndex(exclusions []string) map[string]struct{} { + index := make(map[string]struct{}, len(exclusions)) + for _, exclusion := range normalizedExclusions(exclusions) { + index[exclusion] = struct{}{} + } + return index +} + +func benchmarkExcluded(filePath string, index map[string]struct{}) bool { + if _, ok := index["."]; ok { + return true + } + if _, ok := index[filePath]; ok { + return true + } + for pos := 0; pos < len(filePath); pos++ { + if filePath[pos] == '/' { + if _, ok := index[filePath[:pos]]; ok { + return true + } + } + } + return false +} + +var benchmarkExclusionMatches int + +func BenchmarkFileSizeExclusions(b *testing.B) { + for _, pathCount := range []int{1000, 10000} { + for _, exclusionCount := range []int{1, 10, 100, 1000} { + for _, depth := range []string{"shallow", "deep"} { + for _, match := range []bool{true, false} { + exclusions := make([]string, exclusionCount) + for i := range exclusions { + exclusions[i] = fmt.Sprintf("excluded-%04d", i) + } + paths := make([]string, pathCount) + prefix := "other" + if match { + prefix = exclusions[0] + } + if depth == "deep" { + prefix += "/nested/one/two/three/four" + } + for i := range paths { + paths[i] = fmt.Sprintf("%s/file-%05d", prefix, i) + } + name := fmt.Sprintf("paths=%d/exclusions=%d/%s/match=%t", pathCount, exclusionCount, depth, match) + for _, indexed := range []bool{false, true} { + method := "scan" + if indexed { + method = "index" + } + b.Run(name+"/"+method, func(b *testing.B) { + normalized := normalizedExclusions(exclusions) + index := benchmarkExclusionIndex(exclusions) + for _, filePath := range paths { + if excludedByPath(filePath, normalized) != benchmarkExcluded(filePath, index) { + b.Fatalf("index disagrees for %q", filePath) + } + } + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + count := 0 + if indexed { + index := benchmarkExclusionIndex(exclusions) + for _, filePath := range paths { + if benchmarkExcluded(filePath, index) { + count++ + } + } + } else { + normalized := normalizedExclusions(exclusions) + for _, filePath := range paths { + if excludedByPath(filePath, normalized) { + count++ + } + } + } + benchmarkExclusionMatches = count + } + }) + } + } + } + } + } +} + +func TestBenchmarkExclusionIndexPreservesLiteralMatching(t *testing.T) { + for _, exclusions := range [][]string{{"."}, {"/"}, {"a//b", "../x", "", "a/b"}, {"a", "a/b"}} { + index := benchmarkExclusionIndex(exclusions) + for _, filePath := range []string{"a", "a/b", "a/b/c", "ab/c", "/", "//file", "../x/file", "other"} { + if got, want := benchmarkExcluded(filePath, index), excludedByPath(filePath, normalizedExclusions(exclusions)); got != want { + t.Errorf("path %q exclusions %s: indexed %t, scan %t", filePath, strings.Join(exclusions, ","), got, want) + } + } + } +}