From e834e7d80631a741e6b31412227f07cb5f9a4fff Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Thu, 5 Nov 2026 14:06:09 +0000 Subject: [PATCH] Add bounded diagnostic prefix encoding --- docs/roadmap/implementation.md | 5 +++ internal/usecase/repair_prefix.go | 48 ++++++++++++++++++++++++++ internal/usecase/repair_prefix_test.go | 33 ++++++++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 internal/usecase/repair_prefix.go create mode 100644 internal/usecase/repair_prefix_test.go diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index b5561c3..bcba66e 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -492,6 +492,11 @@ invalid-byte runs, empty/small budgets, and truncation boundaries. Wiring into the public repair workflow follows in Stage 19; no interim alternative formatter is exposed or new contract created for a helper's exact allocation count. +Completion: bounded UTF-8 normalization and JSON-prefix encoding retain only +the useful candidate prefix and report complete versus truncated results. +Focused encoding and existing formatter tests pass on Go 1.27.3; formatter +wiring remains in the next unit. + ## Stage 19: Construct Repair Feedback Without Whole-Input Serialization **Catalog:** 17, completion. **Files:** `formatRepairDiagnostics` and repair tests. diff --git a/internal/usecase/repair_prefix.go b/internal/usecase/repair_prefix.go new file mode 100644 index 0000000..b2adc69 --- /dev/null +++ b/internal/usecase/repair_prefix.go @@ -0,0 +1,48 @@ +package usecase + +import ( + "encoding/json" + "strings" + "unicode/utf8" +) + +// boundedDiagnosticJSON returns a JSON string prefix and whether it represents +// the complete normalized input. It retains at most the useful prefix. +func boundedDiagnosticJSON(value string, maxBytes int) ([]byte, bool) { + if maxBytes < 2 { + return nil, false + } + limit := maxBytes - 2 + var prefix strings.Builder + used := 0 + more := false + for i := 0; i < len(value); { + r, size := utf8.DecodeRuneInString(value[i:]) + piece := value[i : i+size] + if r == utf8.RuneError && size == 1 { + piece = "\uFFFD" + i++ + for i < len(value) { + r, size = utf8.DecodeRuneInString(value[i:]) + if r != utf8.RuneError || size != 1 { + break + } + i++ + } + } else { + i += size + } + if used+len(piece) > limit { + more = true + break + } + prefix.WriteString(piece) + used += len(piece) + } + normalized := prefix.String() + encoded, _ := json.Marshal(normalized) + if len(encoded) > maxBytes { + return truncateDiagnosticJSONValue(normalized, maxBytes), false + } + return encoded, !more +} diff --git a/internal/usecase/repair_prefix_test.go b/internal/usecase/repair_prefix_test.go new file mode 100644 index 0000000..44ecf80 --- /dev/null +++ b/internal/usecase/repair_prefix_test.go @@ -0,0 +1,33 @@ +package usecase + +import ( + "encoding/json" + "strings" + "testing" + "unicode/utf8" +) + +func TestBoundedDiagnosticJSON(t *testing.T) { + for _, input := range []string{"", "plain", "<&>\n\"", "雪☃", "a\xff\xfe\xffb", "\xff\xfe" + strings.Repeat("z", 100)} { + for budget := 0; budget <= 30; budget++ { + encoded, complete := boundedDiagnosticJSON(input, budget) + if budget < 2 { + if encoded != nil || complete { + t.Fatalf("budget %d: %q %v", budget, encoded, complete) + } + continue + } + if len(encoded) > budget || !utf8.Valid(encoded) { + t.Fatalf("budget %d: %q", budget, encoded) + } + var decoded string + if err := json.Unmarshal(encoded, &decoded); err != nil { + t.Fatal(err) + } + normalized := strings.ToValidUTF8(input, "\uFFFD") + if !strings.HasPrefix(normalized, decoded) || complete != (decoded == normalized) { + t.Fatalf("input %q budget %d: %q complete=%v normalized=%q", input, budget, decoded, complete, normalized) + } + } + } +}