Protoc Plugin Engineering
Keep go-sphere's protoc-gen-* plugins structurally consistent so they share one
reading path, one calling convention, and one maintenance model — without forcing
their domain logic to be identical.
If the change touches generated output, state up front whether it is a semantic
or a presentational change. Do not bundle both in one commit.
Scope
| Plugin |
Kind |
protoc-gen-sphere |
new-file generator (service → interfaces + helpers) |
protoc-gen-route |
new-file generator (service + HTTP rules → routes) |
protoc-gen-sphere-errors |
new-file generator (enum → error definitions) |
protoc-gen-sphere-binding |
in-place AST rewriter (retags protoc-gen-go output) |
Non-Negotiable Principles
- Consistent structure and contract, not consistent business logic. Same
entrypoint, config validation, generator lifecycle, and test entry. Domain
logic may differ. Do not invent abstractions just to make filenames match.
- Immutable config, file-local state. Flags are parsed and validated once at
startup; the generator holds its own snapshot. Per-file state (imports,
services, enums) stays in the current call — never in package-level variables.
- Same input, same output. No map iteration order, wall-clock time,
randomness, machine paths, or state left over from the previous file.
- Default templates are read-only. Embedded templates are package-level
read-only resources. Custom templates load into a separate renderer instance.
No
ReplaceTemplateIfNeed-style package-level mutation.
- Generated files are a stable interface. They land in version control, code
review, and downstream builds. Do not create large golden diffs without cause.
Required Reading
- references/plugin-conventions.md — directory layout, entrypoint, config, generator contract, templates, output rules, AST-rewriter specifics, compatibility
- references/plugin-testing.md — the three test layers, fixture management, golden discipline, and the pitfalls that bite every plugin suite
Load the testing reference whenever you add or change tests, touch testdata/,
or update golden files.
Working Order for a New Plugin
- Decide: new-file generator or in-place rewriter.
- Create the standard directory and a minimal
Config.
- Implement
Validate, NewGenerator, and no-output semantics first.
- Then the domain model and generation logic.
- Introduce an immutable
Renderer only if the plugin needs templates.
- Add unit, descriptor, golden, CLI, and isolation tests.
- Confirm output determinism and API compatibility.
- Pass the full local quality gate before release.
Local Quality Gate
Run inside each plugin module before delivering:
find . -type f -name '*.go' -not -path './vendor/*' -exec gofmt -w {} +
git diff --check
GOWORK=off go mod tidy -diff
GOWORK=off go test -race ./...
GOWORK=off go vet ./...
golangci-lint run --no-config
nilaway ./...
GOWORK=off proves the module builds independently of the local workspace.
go mod tidy -diff checks without mutating. Golden updates run as a separate
explicit command, followed by a full test rerun. A project Makefile may wrap
these, but must not weaken their semantics — see the go-sphere-makefiles skill
for the target contract (test, lint, check, update-golden, generate).
Review Checklist
Structure
Config and state
Output
Compatibility
Verification
Reporting
State: plugin and kind; whether output changed and whether that change is
semantic or presentational; compatibility impact on exported API/flags/template
data; which test layers were exercised; golden files updated and reviewed; the
quality-gate commands actually run, with any skipped step named explicitly.
If a plugin must deviate from these conventions, record the deviation, the
reason, its blast radius, and the condition for returning to the common shape in
that plugin's README.
1---2name: protoc-plugin-engineering3description: Write, refactor, or review Go protoc-gen-* plugins in the go-sphere organization. Use when adding a new plugin, changing generated output, reworking plugin config or templates, hardening plugin tests, or reviewing a plugin PR for structure, determinism, immutability, and golden-file discipline. Covers both new-file generators (protoc-gen-sphere, protoc-gen-route, protoc-gen-sphere-errors) and in-place AST rewriters (protoc-gen-sphere-binding). Do not use for authoring .proto API contracts — that is proto-api-generator.4---56# Protoc Plugin Engineering78Keep go-sphere's `protoc-gen-*` plugins structurally consistent so they share one9reading path, one calling convention, and one maintenance model — without forcing10their domain logic to be identical.1112<HARD-GATE>13Before writing or changing plugin code, confirm:14- Which plugin, and whether it is a **new-file generator** or an **in-place AST rewriter** (they differ in no-output semantics and header handling)15- Whether the change alters **generated output** (semantic vs. presentational), **exported API**, **flags**, or **template data** — each has a compatibility cost16- Whether `workspace/docs/PROTOC_PLUGIN_GUIDELINES.md` and `TESTING.md` are available in this checkout; if so, they are authoritative and this skill is the summary1718If the change touches generated output, state up front whether it is a semantic19or a presentational change. Do not bundle both in one commit.20</HARD-GATE>2122## Scope2324| Plugin | Kind |25|--------|------|26| `protoc-gen-sphere` | new-file generator (service → interfaces + helpers) |27| `protoc-gen-route` | new-file generator (service + HTTP rules → routes) |28| `protoc-gen-sphere-errors` | new-file generator (enum → error definitions) |29| `protoc-gen-sphere-binding` | in-place AST rewriter (retags `protoc-gen-go` output) |3031## Non-Negotiable Principles32331. **Consistent structure and contract, not consistent business logic.** Same34 entrypoint, config validation, generator lifecycle, and test entry. Domain35 logic may differ. Do not invent abstractions just to make filenames match.362. **Immutable config, file-local state.** Flags are parsed and validated once at37 startup; the generator holds its own snapshot. Per-file state (imports,38 services, enums) stays in the current call — never in package-level variables.393. **Same input, same output.** No map iteration order, wall-clock time,40 randomness, machine paths, or state left over from the previous file.414. **Default templates are read-only.** Embedded templates are package-level42 read-only resources. Custom templates load into a separate renderer instance.43 No `ReplaceTemplateIfNeed`-style package-level mutation.445. **Generated files are a stable interface.** They land in version control, code45 review, and downstream builds. Do not create large golden diffs without cause.4647## Required Reading48491. **[references/plugin-conventions.md](references/plugin-conventions.md)** — directory layout, entrypoint, config, generator contract, templates, output rules, AST-rewriter specifics, compatibility502. **[references/plugin-testing.md](references/plugin-testing.md)** — the three test layers, fixture management, golden discipline, and the pitfalls that bite every plugin suite5152Load the testing reference whenever you add or change tests, touch `testdata/`,53or update golden files.5455## Working Order for a New Plugin56571. Decide: new-file generator or in-place rewriter.582. Create the standard directory and a minimal `Config`.593. Implement `Validate`, `NewGenerator`, and no-output semantics **first**.604. Then the domain model and generation logic.615. Introduce an immutable `Renderer` only if the plugin needs templates.626. Add unit, descriptor, golden, CLI, and isolation tests.637. Confirm output determinism and API compatibility.648. Pass the full local quality gate before release.6566## Local Quality Gate6768Run inside each plugin module before delivering:6970```sh71find . -type f -name '*.go' -not -path './vendor/*' -exec gofmt -w {} +72git diff --check73GOWORK=off go mod tidy -diff74GOWORK=off go test -race ./...75GOWORK=off go vet ./...76golangci-lint run --no-config77nilaway ./...78```7980`GOWORK=off` proves the module builds independently of the local workspace.81`go mod tidy -diff` checks without mutating. Golden updates run as a separate82explicit command, followed by a full test rerun. A project Makefile may wrap83these, but must not weaken their semantics — see the `go-sphere-makefiles` skill84for the target contract (`test`, `lint`, `check`, `update-golden`, `generate`).8586## Review Checklist8788**Structure**89- [ ] `main.go` only adapts the protoc protocol and assembles objects90- [ ] `Config`, `Generator`, template, and domain logic have clear boundaries91- [ ] Files named by responsibility; no mixed `utils.go` / `common.go` / `helper.go`92- [ ] Deviations in special plugins have a stated justification9394**Config and state**95- [ ] `DefaultConfig()` equals the real CLI defaults and returns independent objects96- [ ] `Validate` is nil-safe and its error text is order-stable97- [ ] Generator deep-copies reference-typed config98- [ ] No mutable package-level templates or cross-file state99- [ ] A custom template affects only its own instance100101**Output**102- [ ] Non-applicable files produce no output — never a header-only shell103- [ ] Filename, header, package, and imports are stable104- [ ] Map-derived output is sorted; repeated lists deduped and ordered105- [ ] Dynamic strings pass through `strconv.Quote`106- [ ] The generated diff contains only what this change requires107- [ ] AST rewrites are idempotent and preserve non-target content108109**Compatibility**110- [ ] Changes to exported API, flags, defaults, or template data are identified111- [ ] Compatible old APIs keep a `Deprecated:` wrapper112- [ ] Breaking changes have migration notes and a release plan113114**Verification**115- [ ] Unit, descriptor, golden, and CLI tests cover this change116- [ ] Golden diffs were reviewed by a human, not just made green117- [ ] `go test -race`, `go vet`, lint, and nilaway pass118- [ ] Module verifies independently under `GOWORK=off`119120## Reporting121122State: plugin and kind; whether output changed and whether that change is123semantic or presentational; compatibility impact on exported API/flags/template124data; which test layers were exercised; golden files updated and reviewed; the125quality-gate commands actually run, with any skipped step named explicitly.126127If a plugin must deviate from these conventions, record the deviation, the128reason, its blast radius, and the condition for returning to the common shape in129that plugin's README.