Migrate clientgen and tsclientgen to use the shared `internal/annotations` package, then delete their old annotation code.

Purpose: These two generators have similar complexity (~250 lines each of annotation code) and straightforward migration paths. Batching them together is efficient since neither depends on the other. After this plan, 3 of 4 generators use the shared package.

Output: clientgen and tsclientgen fully migrated, old annotation files deleted, all tests passing.

<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-02-SUMMARY.md @internal/clientgen/annotations.go @internal/clientgen/generator.go @internal/tsclientgen/annotations.go @internal/tsclientgen/generator.go @internal/tsclientgen/types.go @internal/tsclientgen/helpers.go Task 1: Migrate clientgen to shared annotations and delete annotations.go internal/clientgen/annotations.go internal/clientgen/generator.go **Step 1: Update clientgen/generator.go**

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

  • getMethodHTTPConfig(method) -> annotations.GetMethodHTTPConfig(method) (lines ~76, ~102, ~403)
    • The returned *HTTPConfig becomes *annotations.HTTPConfig. Update any type references.
  • getQueryParams(method.Input) -> annotations.GetQueryParams(method.Input) (lines ~88, ~442)
    • clientgen used QueryParam with FieldName, FieldGoName, ParamName, Required, FieldKind. The shared struct has all these fields. Verify every field access pattern in generator.go.
  • getServiceHeaders(service) -> annotations.GetServiceHeaders(service) (line ~313)
  • getMethodHeaders(method) -> annotations.GetMethodHeaders(method) (line ~320)
  • getServiceHTTPConfig(service) -> replace the pattern:
    serviceConfig := getServiceHTTPConfig(service)
    basePath := ""
    if serviceConfig != nil {
        basePath = serviceConfig.BasePath
    }
    With: basePath := annotations.GetServiceBasePath(service)
  • lowerFirst(...) -> annotations.LowerFirst(...) -- replace all occurrences in generator.go (line ~691 area is the function definition, line ~437 area is usage). Remove the lowerFirst function definition.
  • Also remove snakeToUpperCamel only if it's NOT used anywhere else in clientgen (check with grep). If it IS used, keep it -- it's generator-specific naming logic, not annotation-related.
  • Remove HTTP method constants (httpMethodGET, etc.) if they were referenced from annotations.go and are no longer needed.

Replace inline path building with BuildHTTPPath:

The inline path building logic at lines ~425-433:

fullPath := httpPath
if basePath != "" {
    basePath = strings.TrimSuffix(basePath, "/")
    if !strings.HasPrefix(httpPath, "/") {
        httpPath = "/" + httpPath
    }
    fullPath = basePath + httpPath
}

Replace with: fullPath := annotations.BuildHTTPPath(basePath, httpPath)

This is safe because the empty-path divergence (BuildHTTPPath("", "") returns "/" while inline returns "") is UNREACHABLE in clientgen: httpPath is always initialized to "/" + lowerFirst(methodName) at line ~405 before this code runs, so httpPath is never empty. When basePath is empty and httpPath is non-empty, BuildHTTPPath("", "/someMethod") returns "/someMethod" via ensureLeadingSlash -- identical to the inline result of fullPath := httpPath. When both are non-empty, both implementations trim trailing slash on basePath, ensure leading slash on methodPath, and concatenate.

Step 2: Delete clientgen/annotations.go

Delete internal/clientgen/annotations.go (241 lines).

Step 3: Verify

Run:

  1. go build ./internal/clientgen/
  2. go build ./cmd/protoc-gen-go-client/
  3. go test ./internal/clientgen/...
  4. make lint-fix

If golden file tests exist for clientgen, run them and verify ZERO golden file changes. go build ./cmd/protoc-gen-go-client/ succeeds. go test ./internal/clientgen/... all tests pass. ls internal/clientgen/annotations.go returns "No such file or directory". make lint-fix clean. clientgen/annotations.go (241 lines) deleted. clientgen/generator.go imports and uses shared annotations including BuildHTTPPath for path construction. All clientgen tests pass with zero behavior change.

Task 2: Migrate tsclientgen to shared annotations and delete annotations.go internal/tsclientgen/annotations.go internal/tsclientgen/generator.go internal/tsclientgen/types.go internal/tsclientgen/helpers.go **Step 1: Update tsclientgen/generator.go**

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

  • getServiceHeaders(service) -> annotations.GetServiceHeaders(service) (lines ~152, ~171, ~232, ~380)
  • getMethodHeaders(method) -> annotations.GetMethodHeaders(method) (lines ~180, ~388)
  • getMethodHTTPConfig(method) -> annotations.GetMethodHTTPConfig(method) (line ~260)
  • getServiceHTTPConfig(service) -> replace pattern with basePath := annotations.GetServiceBasePath(service) (line ~275)
  • getQueryParams(method.Input) -> annotations.GetQueryParams(method.Input) (line ~296)
    • tsclientgen's QueryParam used FieldName, FieldJSONName, ParamName, Required, FieldKind. The shared struct has all these fields. Verify every field access.
  • isRootUnwrap(msg) -> annotations.IsRootUnwrap(msg) (line ~332)
  • lowerFirst(...) -> annotations.LowerFirst(...) -- used in generator.go

Replace inline path building with BuildHTTPPath:

Replace inline path building (lines ~281-288) with annotations.BuildHTTPPath(basePath, httpPath).

This is safe for the same reason as clientgen: httpPath is always initialized to "/" + lowerFirst(methodName) at line ~262 before this code runs, so httpPath is never empty. The empty-path divergence is unreachable. Both implementations produce identical output for all reachable input combinations.

Step 2: Update tsclientgen/types.go

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

  • findUnwrapField(valueField.Message) -> annotations.FindUnwrapField(valueField.Message) (lines ~168, ~238)
  • hasUnwrapAnnotation(field) -> annotations.HasUnwrapAnnotation(field) -- but hasUnwrapAnnotation is called from types.go's findUnwrapField and isRootUnwrap. Since those functions are being deleted (they're now in the shared package), the only remaining calls to hasUnwrapAnnotation would be if there are other callers. Check: the only callers are findUnwrapField (line ~213) and isRootUnwrap (line ~225), both of which are being moved to the shared package. So hasUnwrapAnnotation calls in types.go go away when findUnwrapField and isRootUnwrap are removed.
  • Remove findUnwrapField function definition (lines ~210-218) -- now in shared package as annotations.FindUnwrapField
  • Remove isRootUnwrap function definition (lines ~220-226) -- now in shared package as annotations.IsRootUnwrap

Step 3: Update tsclientgen/helpers.go

Remove lowerFirst function (lines 8-13) -- now in shared package as annotations.LowerFirst. Keep snakeToLowerCamel and headerNameToPropertyName -- these are tsclientgen-specific naming helpers, not annotation-related.

Step 4: Delete tsclientgen/annotations.go

Delete internal/tsclientgen/annotations.go (250 lines).

Step 5: Verify

Run:

  1. go build ./internal/tsclientgen/
  2. go build ./cmd/protoc-gen-ts-client/
  3. go test ./internal/tsclientgen/...
  4. Run golden file tests: go test -v -run TestGolden ./internal/tsclientgen/... -- MUST pass with ZERO golden file changes
  5. ./scripts/run_tests.sh --fast for full test suite
  6. make lint-fix

Do NOT update golden files. If golden files need updating, the migration is wrong. go build ./cmd/protoc-gen-ts-client/ succeeds. go test ./internal/tsclientgen/... all tests pass. Golden file tests pass WITHOUT updating golden files. ls internal/tsclientgen/annotations.go returns "No such file or directory". ./scripts/run_tests.sh --fast all tests pass. make lint-fix clean. tsclientgen/annotations.go (250 lines) deleted. findUnwrapField and isRootUnwrap removed from types.go. lowerFirst removed from helpers.go. All tsclientgen tests including golden file tests pass with zero behavior change.

1. `go build ./cmd/protoc-gen-go-client/` succeeds 2. `go build ./cmd/protoc-gen-ts-client/` succeeds 3. `go test ./internal/clientgen/... ./internal/tsclientgen/...` all pass 4. Golden file tests pass with zero changes 5. `./scripts/run_tests.sh --fast` passes 6. `internal/clientgen/annotations.go` does not exist 7. `internal/tsclientgen/annotations.go` does not exist 8. `grep -r "annotations\." internal/clientgen/ internal/tsclientgen/` shows shared package usage 9. `make lint-fix` clean

<success_criteria>

  • clientgen/annotations.go deleted (241 lines removed)
  • tsclientgen/annotations.go deleted (250 lines removed)
  • tsclientgen/types.go: findUnwrapField and isRootUnwrap removed
  • tsclientgen/helpers.go: lowerFirst removed
  • Both generators use annotations.BuildHTTPPath for path construction (no inline path logic)
  • 3 of 4 generators now use shared annotations
  • All golden file tests pass unchanged
  • Full test suite passes </success_criteria>
After completion, create `.planning/phases/02-shared-annotations/02-03-SUMMARY.md`