Define int64_encoding, enum_encoding, and enum_value proto annotations and create shared annotation parsing functions in internal/annotations.

Purpose: Establish the annotation infrastructure that all 4 generators will use to control JSON encoding of int64/uint64 and enum fields. This is the foundation for the rest of Phase 4.

Output: Proto extensions for int64_encoding, enum_encoding, and enum_value annotations. Shared Go functions GetInt64Encoding, GetEnumEncoding, GetEnumValueMapping, and HasConflictingEnumAnnotations in internal/annotations package.

<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/STATE.md @.planning/phases/04-json-primitive-encoding/04-CONTEXT.md @.planning/phases/04-json-primitive-encoding/04-RESEARCH.md

Existing annotation patterns to follow

@internal/annotations/unwrap.go @internal/annotations/http_config.go @proto/sebuf/http/annotations.proto

Task 1: Add encoding annotations to proto/sebuf/http/annotations.proto proto/sebuf/http/annotations.proto Add the following proto definitions after the existing FieldOptions extensions:
  1. Int64Encoding enum:
// Int64Encoding controls how int64/uint64 fields serialize to JSON
enum Int64Encoding {
  // Follow protojson default (serialize as string)
  INT64_ENCODING_UNSPECIFIED = 0;
  // Explicit string encoding: "12345"
  INT64_ENCODING_STRING = 1;
  // Numeric encoding: 12345 (precision warning for values > 2^53)
  INT64_ENCODING_NUMBER = 2;
}
  1. EnumEncoding enum:
// EnumEncoding controls how enum fields serialize to JSON
enum EnumEncoding {
  // Follow protojson default (serialize as proto name string)
  ENUM_ENCODING_UNSPECIFIED = 0;
  // Explicit string encoding: "STATUS_ACTIVE"
  ENUM_ENCODING_STRING = 1;
  // Numeric encoding: 1
  ENUM_ENCODING_NUMBER = 2;
}
  1. Add to existing FieldOptions extension block:
// Controls int64/uint64 JSON encoding for this field
optional Int64Encoding int64_encoding = 50010;
 
// Controls enum JSON encoding for this field (only valid on enum fields)
optional EnumEncoding enum_encoding = 50011;
  1. Add EnumValueOptions extension:
// Extension for enum value options
extend google.protobuf.EnumValueOptions {
  // Custom JSON string for this enum value (e.g., "active" instead of "STATUS_ACTIVE")
  optional string enum_value = 50012;
}

Use extension numbers 50010, 50011, 50012 (continuing from existing 50009 for unwrap). Run buf build or protoc to verify the proto compiles without errors:

cd proto && buf build

Regenerate Go code:

cd proto && buf generate

Verify the generated Go code compiles:

go build ./http/...
- Int64Encoding and EnumEncoding enums exist in annotations.proto - int64_encoding, enum_encoding field extensions exist - enum_value EnumValueOptions extension exists - Generated Go code in http/ package compiles - E_Int64Encoding, E_EnumEncoding, E_EnumValue extension descriptors accessible
Task 2: Create internal/annotations/int64_encoding.go and enum_encoding.go internal/annotations/int64_encoding.go internal/annotations/enum_encoding.go internal/annotations/annotations_test.go Create int64_encoding.go following the pattern in unwrap.go:
package annotations
 
import (
    "google.golang.org/protobuf/compiler/protogen"
    "google.golang.org/protobuf/proto"
    "google.golang.org/protobuf/types/descriptorpb"
 
    "github.com/SebastienMelki/sebuf/http"
)
 
// GetInt64Encoding returns the int64 encoding for a field.
// Returns INT64_ENCODING_UNSPECIFIED if not set (callers should use protojson default: STRING).
// This annotation is valid on int64, sint64, sfixed64, uint64, and fixed64 fields.
func GetInt64Encoding(field *protogen.Field) http.Int64Encoding {
    options := field.Desc.Options()
    if options == nil {
        return http.Int64Encoding_INT64_ENCODING_UNSPECIFIED
    }
 
    fieldOptions, ok := options.(*descriptorpb.FieldOptions)
    if !ok {
        return http.Int64Encoding_INT64_ENCODING_UNSPECIFIED
    }
 
    ext := proto.GetExtension(fieldOptions, http.E_Int64Encoding)
    if ext == nil {
        return http.Int64Encoding_INT64_ENCODING_UNSPECIFIED
    }
 
    encoding, ok := ext.(http.Int64Encoding)
    if !ok {
        return http.Int64Encoding_INT64_ENCODING_UNSPECIFIED
    }
 
    return encoding
}
 
// IsInt64NumberEncoding returns true if the field should encode int64/uint64 as JSON number.
// Returns false for UNSPECIFIED or STRING (both use protojson default string encoding).
func IsInt64NumberEncoding(field *protogen.Field) bool {
    return GetInt64Encoding(field) == http.Int64Encoding_INT64_ENCODING_NUMBER
}

Create enum_encoding.go:

package annotations
 
import (
    "google.golang.org/protobuf/compiler/protogen"
    "google.golang.org/protobuf/proto"
    "google.golang.org/protobuf/types/descriptorpb"
 
    "github.com/SebastienMelki/sebuf/http"
)
 
// GetEnumEncoding returns the enum encoding for a field.
// Returns ENUM_ENCODING_UNSPECIFIED if not set (callers should use protojson default: STRING names).
// This annotation is only valid on enum fields.
func GetEnumEncoding(field *protogen.Field) http.EnumEncoding {
    options := field.Desc.Options()
    if options == nil {
        return http.EnumEncoding_ENUM_ENCODING_UNSPECIFIED
    }
 
    fieldOptions, ok := options.(*descriptorpb.FieldOptions)
    if !ok {
        return http.EnumEncoding_ENUM_ENCODING_UNSPECIFIED
    }
 
    ext := proto.GetExtension(fieldOptions, http.E_EnumEncoding)
    if ext == nil {
        return http.EnumEncoding_ENUM_ENCODING_UNSPECIFIED
    }
 
    encoding, ok := ext.(http.EnumEncoding)
    if !ok {
        return http.EnumEncoding_ENUM_ENCODING_UNSPECIFIED
    }
 
    return encoding
}
 
// GetEnumValueMapping returns the custom JSON value for an enum value, or empty string if not set.
// When set, this value should be used instead of the proto name for JSON serialization.
func GetEnumValueMapping(value *protogen.EnumValue) string {
    options := value.Desc.Options()
    if options == nil {
        return ""
    }
 
    enumValueOptions, ok := options.(*descriptorpb.EnumValueOptions)
    if !ok {
        return ""
    }
 
    ext := proto.GetExtension(enumValueOptions, http.E_EnumValue)
    if ext == nil {
        return ""
    }
 
    customValue, ok := ext.(string)
    if !ok {
        return ""
    }
 
    return customValue
}
 
// HasAnyEnumValueMapping returns true if any value in the enum has a custom JSON mapping.
func HasAnyEnumValueMapping(enum *protogen.Enum) bool {
    for _, value := range enum.Values {
        if GetEnumValueMapping(value) != "" {
            return true
        }
    }
    return false
}
 
// HasConflictingEnumAnnotations checks if a field has both enum_encoding=NUMBER and
// enum_value annotations on its enum values, which is an error per CONTEXT.md.
func HasConflictingEnumAnnotations(field *protogen.Field) bool {
    if field.Enum == nil {
        return false
    }
 
    encoding := GetEnumEncoding(field)
    if encoding != http.EnumEncoding_ENUM_ENCODING_NUMBER {
        return false
    }
 
    return HasAnyEnumValueMapping(field.Enum)
}

Add unit tests to annotations_test.go for the new functions (test with mock protogen types where possible, or pure function tests for helper functions).

go build ./internal/annotations/...
go test -v ./internal/annotations/...
make lint-fix

All tests pass, lint clean.

  • GetInt64Encoding function exists and returns correct encoding enum
  • IsInt64NumberEncoding helper exists
  • GetEnumEncoding function exists and returns correct encoding enum
  • GetEnumValueMapping function exists and returns custom JSON values
  • HasAnyEnumValueMapping helper exists
  • HasConflictingEnumAnnotations validation function exists
  • All functions follow existing annotation package patterns (protogen types, nil safety)
  • Unit tests cover all functions
  • Lint passes
1. Proto annotations compile: `cd proto && buf build && buf generate` 2. Go package compiles: `go build ./http/... ./internal/annotations/...` 3. Tests pass: `go test -v ./internal/annotations/...` 4. Lint clean: `make lint-fix` reports no issues 5. Verify extension numbers don't conflict with existing (50007=field_examples, 50008=query, 50009=unwrap)

<success_criteria>

  • Proto files can import and use int64_encoding, enum_encoding, and enum_value annotations
  • GetInt64Encoding(field) returns the configured Int64Encoding enum value
  • GetEnumEncoding(field) returns the configured EnumEncoding enum value
  • GetEnumValueMapping(value) returns custom JSON string for enum values
  • HasConflictingEnumAnnotations detects invalid annotation combinations
  • All existing tests continue to pass (zero regression) </success_criteria>
After completion, create `.planning/phases/04-json-primitive-encoding/04-01-SUMMARY.md`