Create the shared `internal/annotations` package with all canonical types and functions extracted from the 4 generators.

Purpose: This is the foundation for Phase 2. All 4 generators will import this package instead of their duplicated annotation parsing code. The package must expose a convention-based API where each annotation type lives in its own file with GetXxx() / ParseXxx() function signatures.

Output: A fully compiled, tested internal/annotations package ready for generators to import.

<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/02-shared-annotations/02-CONTEXT.md @.planning/phases/02-shared-annotations/02-RESEARCH.md @internal/httpgen/annotations.go @internal/clientgen/annotations.go @internal/tsclientgen/annotations.go @internal/openapiv3/http_annotations.go @internal/openapiv3/types.go @internal/tsclientgen/types.go @internal/tsclientgen/helpers.go @internal/httpgen/annotations_test.go @internal/openapiv3/http_annotations_test.go Task 1: Create internal/annotations package with all shared types and functions internal/annotations/doc.go internal/annotations/http_config.go internal/annotations/headers.go internal/annotations/query.go internal/annotations/unwrap.go internal/annotations/field_examples.go internal/annotations/path.go internal/annotations/method.go internal/annotations/helpers.go Create `internal/annotations/` directory and the following files. All functions are EXPORTED (uppercase). All structs are transparent (exported fields). Accept protogen types (`*protogen.Method`, `*protogen.Service`, `*protogen.Message`, `*protogen.Field`), not protoreflect types.

doc.go: Package documentation explaining convention-based extensibility pattern. Each file = one annotation concept, each with GetXxx() functions.

http_config.go:

  • HTTPConfig struct: Path string, Method string, PathParams []string (same as existing but exported)
  • ServiceConfig struct: BasePath string (replaces both ServiceConfigImpl and ServiceHTTPConfig names)
  • GetMethodHTTPConfig(method *protogen.Method) *HTTPConfig -- extracts HTTP config from method options. Uses HTTPMethodToString() for the Method field (uppercase). Identical logic to existing getMethodHTTPConfig across all 4 generators.
  • GetServiceBasePath(service *protogen.Service) string -- extracts base path from service options. Returns empty string if not configured. Simpler API than returning a struct with one field.

headers.go:

  • GetServiceHeaders(service *protogen.Service) []*http.Header -- identical to existing
  • GetMethodHeaders(method *protogen.Method) []*http.Header -- identical to existing
  • CombineHeaders(serviceHeaders, methodHeaders []*http.Header) []*http.Header -- merges with method precedence, sorted by name. Extracted from openapiv3. Use sort.Strings instead of bubble sort.

query.go:

  • QueryParam struct: ALL fields from all generators combined:
    • FieldName string (proto field name)
    • FieldGoName string (Go field name)
    • FieldJSONName string (JSON field name / camelCase)
    • ParamName string (query parameter name)
    • Required bool
    • FieldKind string (proto field kind: string, int32, bool, etc.)
    • Field *protogen.Field (raw protogen field reference)
  • GetQueryParams(message *protogen.Message) []QueryParam -- populates ALL fields for each param. Use field.GoName for FieldGoName, field.Desc.JSONName() for FieldJSONName, field.Desc.Kind().String() for FieldKind, and pass field directly for the Field reference.

unwrap.go:

  • UnwrapFieldInfo struct: same as httpgen's (Field, ElementType, IsRootUnwrap, IsMapField)
  • UnwrapValidationError struct: same as httpgen's (MessageName, FieldName, Reason) with Error() method
  • HasUnwrapAnnotation(field *protogen.Field) bool -- identical to existing
  • GetUnwrapField(message *protogen.Message) (*UnwrapFieldInfo, error) -- full validation version from httpgen (validates non-repeated/non-map, multiple unwrap fields, map-without-root-unwrap). Return nil, nil for no unwrap field (with nolint comment).
  • FindUnwrapField(message *protogen.Message) *protogen.Field -- simple version from tsclientgen/openapiv3: returns the unwrap-annotated repeated field or nil. No validation, no error.
  • IsRootUnwrap(message *protogen.Message) bool -- from tsclientgen: single field with unwrap=true

field_examples.go:

  • GetFieldExamples(field *protogen.Field) []string -- identical to existing

path.go:

  • Unexported pathParamRegex (package-level compiled regex, same as existing)
  • ExtractPathParams(path string) []string -- identical to existing
  • BuildHTTPPath(servicePath, methodPath string) string -- from openapiv3's buildHTTPPath
  • EnsureLeadingSlash(path string) string -- from openapiv3's ensureLeadingSlash (export it since generators may need it)

method.go:

  • HTTPMethodToString(m http.HttpMethod) string -- returns UPPERCASE ("GET", "POST", etc.). Default POST for unspecified.
  • HTTPMethodToLower(m http.HttpMethod) string -- returns lowercase ("get", "post", etc.) for OpenAPI. Uses strings.ToLower(HTTPMethodToString(m)) for DRY.

helpers.go:

  • LowerFirst(s string) string -- converts "FooBar" to "fooBar". Identical to existing 3 copies.

IMPORTANT: Do NOT import any generator package (httpgen, clientgen, tsclientgen, openapiv3). Only import: stdlib, protogen, proto, descriptorpb, sebuf/http.

After creating files, run go build ./internal/annotations/ to verify compilation, then run make lint-fix to catch any linting issues. go build ./internal/annotations/ succeeds with zero errors. make lint-fix produces no errors. go vet ./internal/annotations/ produces no warnings. All 9 files exist in internal/annotations/, package compiles, no import cycles.

Task 2: Write unit tests for the shared annotations package internal/annotations/annotations_test.go Create `internal/annotations/annotations_test.go` with tests covering all pure functions (functions that don't require protogen mocks). Consolidate and improve the tests from: - `internal/httpgen/annotations_test.go` (TestHttpMethodToString, TestExtractPathParams, benchmarks) - `internal/openapiv3/http_annotations_test.go` (TestBuildHTTPPath, TestEnsureLeadingSlash, TestCombineHeaders, TestMapHeaderTypeToOpenAPI, benchmarks)

Tests to write:

  1. TestHTTPMethodToString -- all 5 methods + UNSPECIFIED + unknown values. Verify UPPERCASE output.
  2. TestHTTPMethodToLower -- all 5 methods + UNSPECIFIED + unknown values. Verify lowercase output.
  3. TestExtractPathParams -- single param, multiple params, no params, edge cases (empty braces, unclosed brace, hyphenated params). Reuse test cases from httpgen.
  4. TestBuildHTTPPath -- both paths, service only, method only, empty, slash handling, complex paths. Reuse test cases from openapiv3.
  5. TestEnsureLeadingSlash -- with slash, without slash, empty, just slash. Reuse from openapiv3.
  6. TestCombineHeaders -- service only, method only, override, sorted merge, empty names skipped. Reuse from openapiv3.
  7. TestCombineHeaders_MethodOverridesService -- verifies description and required field override.
  8. TestLowerFirst -- empty string, single char, normal cases.
  9. TestHTTPConfig_Struct -- struct field verification.
  10. TestQueryParam_Struct -- verify all fields including new FieldJSONName, FieldKind, Field.
  11. TestServiceConfig_Struct -- struct field verification.
  12. Benchmark tests: BenchmarkExtractPathParams, BenchmarkBuildHTTPPath, BenchmarkCombineHeaders, BenchmarkHTTPMethodToString.

After writing tests, run go test ./internal/annotations/ to verify all pass, then run make lint-fix. go test -v ./internal/annotations/ shows all tests passing. go test -bench=. ./internal/annotations/ runs benchmarks successfully. make lint-fix produces no errors. All unit tests pass for pure functions in the shared annotations package. Coverage includes all exported functions that can be tested without protogen mocks.

1. `go build ./internal/annotations/` compiles successfully 2. `go test -v ./internal/annotations/` all tests pass 3. `go vet ./internal/annotations/` no warnings 4. `make lint-fix` clean 5. No generator code modified yet (generators still use their own annotations) 6. Verify no import of httpgen/clientgen/tsclientgen/openapiv3 in the annotations package

<success_criteria>

  • internal/annotations package exists with 9 source files
  • Package compiles and passes all unit tests
  • Exported API covers: HTTPConfig, ServiceConfig, QueryParam, UnwrapFieldInfo, UnwrapValidationError, GetMethodHTTPConfig, GetServiceBasePath, GetServiceHeaders, GetMethodHeaders, CombineHeaders, GetQueryParams, HasUnwrapAnnotation, GetUnwrapField, FindUnwrapField, IsRootUnwrap, GetFieldExamples, ExtractPathParams, BuildHTTPPath, EnsureLeadingSlash, HTTPMethodToString, HTTPMethodToLower, LowerFirst
  • Convention-based pattern is clear: one file per annotation concept, GetXxx function signatures </success_criteria>
After completion, create `.planning/phases/02-shared-annotations/02-01-SUMMARY.md`