Migrate openapiv3 to shared annotations, fix cross-file error suppression, confirm serialization consistency, and run final verification.

Purpose: This completes Phase 2 by migrating the last generator (openapiv3) and addressing the two remaining requirements: FOUND-04 (serialization audit) and cross-file annotation resolution error handling. After this plan, all duplicated annotation code is eliminated.

Output: All 4 generators use shared annotations, error suppression fixed, serialization audit confirmed, full test suite green.

<execution_context> @/Users/sebastienmelki/.claude/get-shit-done/workflows/execute-plan.md @/Users/sebastienmelki/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/02-shared-annotations/02-CONTEXT.md @.planning/phases/02-shared-annotations/02-RESEARCH.md @.planning/phases/02-shared-annotations/02-03-SUMMARY.md @internal/openapiv3/http_annotations.go @internal/openapiv3/http_annotations_test.go @internal/openapiv3/types.go @internal/openapiv3/generator.go @internal/httpgen/unwrap.go Task 1: Migrate openapiv3 to shared annotations and delete old files internal/openapiv3/http_annotations.go internal/openapiv3/http_annotations_test.go internal/openapiv3/types.go internal/openapiv3/generator.go **Step 1: Move OpenAPI-specific functions to types.go BEFORE deleting http_annotations.go**

Move the following from http_annotations.go to openapiv3/types.go (types.go is 298 lines -- adding ~80 lines of these functions keeps it well under 400):

  • convertHeadersToParameters function (~30 lines)
  • mapHeaderTypeToOpenAPI function (~20 lines)
  • Header type constants: headerTypeString, headerTypeInt32, headerTypeInt64, headerTypeInteger, headerTypeNumber, headerTypeFloat, headerTypeDouble

These are OpenAPI-specific type conversion functions, NOT annotation parsing. They stay in the openapiv3 package.

Also move TestMapHeaderTypeToOpenAPI and BenchmarkMapHeaderTypeToOpenAPI from http_annotations_test.go to an existing test file in the openapiv3 package (e.g., internal/openapiv3/exhaustive_golden_test.go if it has unit tests, or internal/openapiv3/types_test.go if it exists, or create internal/openapiv3/types_test.go). Check which test files exist and pick the most appropriate one.

The HTTP method lowercase constants (httpMethodGet, etc.) are NO LONGER needed -- openapiv3 will use strings.ToLower(cfg.Method) instead. Do not move them.

Step 2: Update openapiv3/generator.go

Add "github.com/SebastienMelki/sebuf/internal/annotations" import. Replace ALL annotation function calls:

  • getServiceHTTPConfig(service) at line ~327 -> replace with annotations.GetServiceBasePath(service). The current code does:

    serviceConfig := getServiceHTTPConfig(service)

    and later accesses serviceConfig.BasePath. Replace with servicePath := annotations.GetServiceBasePath(service).

  • getMethodHTTPConfig(method) at line ~328 -> annotations.GetMethodHTTPConfig(method) CRITICAL: openapiv3's getMethodHTTPConfig returns lowercase methods (via its own httpMethodToString). The shared GetMethodHTTPConfig returns UPPERCASE methods (via HTTPMethodToString). The openapiv3 generator uses the method string in addOperationToPath where lowercase is required for OpenAPI. Fix this by lowercasing at the usage site: after calling annotations.GetMethodHTTPConfig(method), use strings.ToLower(cfg.Method) where the lowercase method is needed (line ~346 area in addOperationToPath and line ~353 where it's set on the path item).

  • buildHTTPPath(servicePath, methodPath) at line ~346 -> annotations.BuildHTTPPath(servicePath, methodPath)

  • getQueryParams(method.Input) at line ~382 -> annotations.GetQueryParams(method.Input) openapiv3's QueryParam had FieldName, ParamName, Required, Field. The shared QueryParam has all these plus extras. Verify access patterns: .FieldName, .ParamName, .Required, .Field -- all still valid.

  • combineHeaders(getServiceHeaders(service), getMethodHeaders(method)) at line ~475 -> annotations.CombineHeaders(annotations.GetServiceHeaders(service), annotations.GetMethodHeaders(method))

  • hasUnwrapAnnotation(field) at line ~216 -> annotations.HasUnwrapAnnotation(field) (in generator.go's schema generation)

  • getUnwrapField(valueField.Message) at line ~238 -> annotations.FindUnwrapField(valueField.Message) (openapiv3 uses the simple version that returns *protogen.Field, not the full *UnwrapFieldInfo. The shared FindUnwrapField matches this signature exactly.)

Step 3: Update openapiv3/types.go

Add "github.com/SebastienMelki/sebuf/internal/annotations" import. Replace:

  • getFieldExamples(field) at line ~122 -> annotations.GetFieldExamples(field)
  • getUnwrapField(valueField.Message) at line ~202 -> annotations.FindUnwrapField(valueField.Message) (returns *protogen.Field, used in getMapValueSchema)
  • Remove getUnwrapField function definition (lines ~242-249) -- replaced by annotations.FindUnwrapField
  • Remove hasUnwrapAnnotation function definition (lines ~252-271) -- replaced by annotations.HasUnwrapAnnotation
  • Remove getFieldExamples function definition (lines ~273-298) -- replaced by annotations.GetFieldExamples

Keep convertField, convertScalarField, convertEnumField, convertMapField, getMapValueSchema, getMapValueField, createUnwrapArraySchema -- these are OpenAPI-specific type conversion logic, not annotation parsing.

Step 4: Delete openapiv3/http_annotations.go

Delete internal/openapiv3/http_annotations.go (406 lines). At this point, all reusable functions have been migrated to shared annotations, and OpenAPI-specific functions have been moved to types.go in Step 1.

Step 5: Delete openapiv3/http_annotations_test.go

Delete internal/openapiv3/http_annotations_test.go (390 lines). Tests for pure shared functions were moved to the shared package in plan 02-01. TestMapHeaderTypeToOpenAPI and BenchmarkMapHeaderTypeToOpenAPI were moved in Step 1. The remaining struct tests (TestHTTPConfig_Struct, TestQueryParam_Struct, TestServiceHTTPConfig_Struct) tested local types that no longer exist and have equivalents in the shared package.

Step 6: Verify

  1. go build ./internal/openapiv3/
  2. go build ./cmd/protoc-gen-openapiv3/
  3. go test ./internal/openapiv3/...
  4. go test -v -run TestExhaustiveGoldenFiles ./internal/openapiv3/... -- MUST pass with ZERO golden file changes
  5. make lint-fix

Do NOT update golden files. If they need updating, the migration is wrong. go build ./cmd/protoc-gen-openapiv3/ succeeds. go test ./internal/openapiv3/... all tests pass. Golden file tests pass WITHOUT updating golden files. ls internal/openapiv3/http_annotations.go returns "No such file or directory". ls internal/openapiv3/http_annotations_test.go returns "No such file or directory". make lint-fix clean. openapiv3/http_annotations.go (406 lines) deleted. openapiv3/http_annotations_test.go deleted. convertHeadersToParameters and mapHeaderTypeToOpenAPI preserved in openapiv3/types.go. All golden file tests pass unchanged.

Task 2: Fix cross-file error suppression in httpgen/unwrap.go internal/httpgen/unwrap.go In `internal/httpgen/unwrap.go`, there are two places where annotation errors are silently suppressed (the phase context decision says "fail hard on cross-file annotation resolution failures -- no partial output, generation stops with a clear error"):
  1. collectFileUnwrapFields (line ~88-101): Currently does continue on error:

    info, err := annotations.GetUnwrapField(msg)
    if err != nil {
        // Log error but continue
        continue
    }

    Change to: Return the error. This requires changing the function signature from func collectFileUnwrapFields(messages []*protogen.Message, global *GlobalUnwrapInfo) to func collectFileUnwrapFields(messages []*protogen.Message, global *GlobalUnwrapInfo) error. On error, return a descriptive error wrapping the original: return fmt.Errorf("collecting unwrap fields: %w", err). The caller collectGlobalUnwrapInfo must propagate this error.

  2. collectUnwrapFieldsRecursive (line ~134-148): Same pattern -- continue on error. Change to return error. Update signature and propagate. The caller collectAllUnwrapFields must also return an error and its callers must handle it.

Trace the call chain upward to ensure errors propagate to the point where generation can be stopped:

  • collectGlobalUnwrapInfo -> called from Generator's main generation function
  • collectAllUnwrapFields -> called from collectUnwrapContext
  • collectUnwrapContext -> called from Generator

Make sure the error reaches the generator's Run or Generate method and causes it to return an error to protoc (which stops code generation with an error message).

Every error message MUST include the proto file/message/field name. The UnwrapValidationError already has MessageName and FieldName. When wrapping, preserve this context.

After making changes:

  1. go build ./internal/httpgen/
  2. go build ./cmd/protoc-gen-go-http/
  3. go test ./internal/httpgen/...
  4. make lint-fix
Error suppression fixed: `grep -n "continue" internal/httpgen/unwrap.go` does not show error-suppressing continues in collectFileUnwrapFields or collectUnwrapFieldsRecursive. `go build ./cmd/protoc-gen-go-http/` succeeds. `go test ./internal/httpgen/...` all tests pass. `make lint-fix` clean. Cross-file error suppression fixed. collectFileUnwrapFields and collectUnwrapFieldsRecursive return errors on annotation failures. Errors propagate up to the generator and stop code generation with clear messages including proto file/message/field context.
Task 3: Serialization audit and final full-suite verification **Part 1: Serialization audit confirmation (verification-only, no code changes expected)**

The research already confirmed that encoding/json usage in httpgen is CORRECT (used for json.Marshaler/json.Unmarshaler interface checks on unwrap types, not for proto message serialization). Verify by grep:

  1. grep -rn "encoding/json" internal/httpgen/*.go -- confirm it appears only in unwrap.go for json.Marshaler/json.Unmarshaler interface checks and in binding-related generated code templates
  2. grep -rn "protojson" internal/httpgen/*.go -- confirm protojson is used for proto message serialization
  3. grep -rn "json.Marshal\|json.Unmarshal" internal/httpgen/*.go -- confirm these are NOT used with proto.Message arguments (they should only appear in generated code templates for unwrap types that implement json.Marshaler/Unmarshaler)

If all three checks confirm correct usage: no code changes needed. This is intentionally verification-only.

Part 2: Final full-suite verification

Run the complete test suite to confirm all 4 generators work correctly:

  1. go build ./cmd/protoc-gen-go-http/ ./cmd/protoc-gen-go-client/ ./cmd/protoc-gen-ts-client/ ./cmd/protoc-gen-openapiv3/
  2. ./scripts/run_tests.sh (full run with coverage, not --fast)
  3. Verify no golden file changes across any generator
  4. make lint-fix

Part 3: Verify duplication is eliminated

  • wc -l internal/httpgen/annotations.go internal/clientgen/annotations.go internal/tsclientgen/annotations.go internal/openapiv3/http_annotations.go 2>/dev/null should show all 4 files missing
  • wc -l internal/annotations/*.go should show the shared package exists
  • grep -r "proto.GetExtension" internal/httpgen/ internal/clientgen/ internal/tsclientgen/ internal/openapiv3/ should return zero results (all extension access is in the shared package now) go build ./cmd/protoc-gen-go-http/ ./cmd/protoc-gen-go-client/ ./cmd/protoc-gen-ts-client/ ./cmd/protoc-gen-openapiv3/ all succeed. ./scripts/run_tests.sh full suite passes with coverage above 85%. make lint-fix clean. Zero golden file changes across all generators. grep -r "proto.GetExtension" internal/httpgen/ internal/clientgen/ internal/tsclientgen/ internal/openapiv3/ returns zero results. All 4 old annotation files confirmed deleted. Serialization audit confirms encoding/json used correctly (interface checks only, not proto message serialization). Serialization audit confirmed correct (no fixes needed). All 4 generators use shared annotations package. ~1,289 lines of duplicated code eliminated. Full test suite passes. Zero behavior change.
1. All 4 generators build: `go build ./cmd/protoc-gen-go-http/ ./cmd/protoc-gen-go-client/ ./cmd/protoc-gen-ts-client/ ./cmd/protoc-gen-openapiv3/` 2. Full test suite: `./scripts/run_tests.sh` passes with coverage >= 85% 3. Zero golden file changes across all generators 4. All 4 old annotation files deleted 5. `grep -r "proto.GetExtension" internal/httpgen/ internal/clientgen/ internal/tsclientgen/ internal/openapiv3/` returns nothing 6. `grep -r "annotations\." internal/httpgen/ internal/clientgen/ internal/tsclientgen/ internal/openapiv3/` shows shared package usage in all 4 7. Error handling: collectFileUnwrapFields and collectUnwrapFieldsRecursive propagate errors 8. Serialization: no encoding/json used for proto message serialization in httpgen 9. convertHeadersToParameters and mapHeaderTypeToOpenAPI live in openapiv3/types.go 10. `make lint-fix` clean

<success_criteria>

  • Phase 2 success criteria 1: internal/annotations package exists, all 4 generators import it
  • Phase 2 success criteria 2: All golden file tests pass without changes
  • Phase 2 success criteria 3: ~1,289 lines of duplicated annotation parsing removed
  • Phase 2 success criteria 4: protojson used exclusively for proto message serialization (confirmed by audit)
  • Phase 2 success criteria 5: Cross-file annotation resolution never silently suppresses errors
  • Full test suite green with coverage >= 85% </success_criteria>
After completion, create `.planning/phases/02-shared-annotations/02-04-SUMMARY.md`