---
name: crud-proto-gen
description: PowerX Proto 生成规则（buf、go_package、Make 目标）。
---

# PowerX CRUD Proto Gen

## 步骤

1) 打开 `本文件内嵌规则`。
2) 按规则执行实现/校对。
3) 完成后按核对清单验收。

## 核对点

- 与 PowerX 当前代码结构、路径与命名一致。
- 仅在传输层/契约层做职责内改动，不跨层越界。

## 规则（内嵌）

### proto_gen.yaml

````yaml
kind: ruleset
name: proto_gen
version: 1.0.2
owner: powerx
status: stable

meta:
  intent: >
    统一 Proto 的组织与生成规范：使用 buf 管理、版本化与生成；go_package 与生成路径一致；
    Make 目标必须存在；生成物输出到 api/grpc/gen 下并纳入 .gitignore（或采用只读提交策略）。
  references:
    - crud_grpc.yaml
    - transport_grpc.yaml

scope:
  applies_to:
    - "api/grpc/**/*.proto"
    - "api/grpc/contracts/buf.yaml"
    - "api/grpc/contracts/buf.gen.yaml"
    - "buf.work.yaml"
    - "Makefile"
    - ".gitignore"

principles:
  - 目录规范：源 proto 放在 api/grpc/**；生成代码放在 api/grpc/gen/**。
  - 工具规范：使用 buf（lint/breaking/生成），不直接手写 protoc 命令（由 buf.gen.yaml 驱动）。
  - 包路径一致：每个 proto 必须设置 option go_package，且与生成前缀 github.com/ArtisanCloud/PowerX/api/grpc/gen 保持一致。
  - 生成产物可清理：必须提供 proto-gen / proto-lint / proto-clean 的 Make 目标。
  - 版本可控：api/grpc/contracts/buf.yaml 指定 lint/breaking 规则；api/grpc/contracts/buf.gen.yaml 锁定插件版本（尽量锁到 minor）。
  - 产物纳管：默认在 .gitignore 忽略 api/grpc/gen；如需提交产物，需锁定插件版本并允许提交。
  - Workspace：使用 buf.work.yaml 管理多目录输入，禁止在 buf.yaml 使用 build.roots 等 v0 字段。

checks:

  # ===== 基本文件存在 =====
  - id: proto.files.required
    level: error
    when: { glob: "api/grpc/**/*.proto" }
    assert:
      - must_exist_glob: "api/grpc/**/*.proto"

  - id: buf.configs.exist
    level: error
    when:
      any_of:
        - file: "api/grpc/contracts/buf.yaml"
        - file: "api/grpc/contracts/buf.gen.yaml"
    assert:
      - file_exists: "api/grpc/contracts/buf.yaml"
      - file_exists: "api/grpc/contracts/buf.gen.yaml"

  - id: buf.workspace.exists
    level: error
    when: { file: "buf.work.yaml" }
    assert:
      - file_exists: "buf.work.yaml"
      - must_contain_regex: "^version:\\s*v1\\b"
      - must_contain: "directories:"
    message: "必须使用 workspace（buf.work.yaml）管理多目录输入。"

  # ===== go_package 必须存在且与生成路径前缀一致 =====
  - id: proto.go_package.set
    level: error
    when: { glob: "api/grpc/**/*.proto" }
    assert:
      - must_contain_regex: "option\\s+go_package\\s*=\\s*\"[^\"]+\";"

  - id: proto.go_package.path_match
    level: warn
    when: { glob: "api/grpc/**/*.proto" }
    assert:
      - should_contain_regex: "option go_package = \"github.com/ArtisanCloud/PowerX/api/grpc/gen/.+;[a-zA-Z0-9_]+\";"
    message: "建议 go_package 使用 github.com/ArtisanCloud/PowerX/api/grpc/gen/<domain>;pkgname 前缀"

  # ===== buf.yaml 的 lint / breaking 规则（不再要求 build.*）=====
  - id: buf.yaml.rules
    level: error
    when: { file: "api/grpc/contracts/buf.yaml" }
    assert:
      - must_contain_regex: "^version:\\s*v1\\b"
      - must_contain: "lint:"
      - must_contain: "breaking:"
      - must_contain_any:
          - "use: [DEFAULT]"
          - "use:\n  - DEFAULT"
      - must_not_contain_regex: "\\bbuild\\s*:"
      - must_not_contain_regex: "\\bbuild\\.roots\\b"

  # ===== buf.gen.yaml 的 go/grpc 插件与输出路径 =====
  - id: buf.gen.outputs
    level: error
    when: { file: "api/grpc/contracts/buf.gen.yaml" }
    assert:
      - must_contain_regex: "plugin:\\s*buf.build/protocolbuffers/go(:|\\b)"
      - must_contain_regex: "plugin:\\s*buf.build/grpc/go(:|\\b)"
      - must_contain_regex: "out:\\s*api/grpc/gen"
      - must_contain: "opt:\n          - paths=source_relative"
      - must_contain: "managed:"
      - must_contain: "go_package_prefix:"
      - must_contain: "default: github.com/ArtisanCloud/PowerX/api/grpc/gen"

  # ===== Makefile 目标 =====
  - id: make.targets
    level: error
    when: { file: "Makefile" }
    assert:
      - must_contain_regex: "^proto-gen:"
      - must_contain_regex: "^proto-lint:"
      - must_contain_regex: "^proto-clean:"

  # ===== 产物纳管：默认忽略 api/grpc/gen =====
  - id: gitignore.gen
    level: warn
    when: { file: ".gitignore" }
    assert:
      - should_contain_any:
          - "/api/grpc/gen/"
          - "api/grpc/gen/"

acceptance:
  checklist:
    - "[ ] 源 proto 位于 api/grpc/**；api/grpc/contracts/buf.yaml 与 api/grpc/contracts/buf.gen.yaml 存在"
    - "[ ] 每个 proto 设置了 option go_package，且前缀为 github.com/ArtisanCloud/PowerX/api/grpc/gen/**"
    - "[ ] 使用 workspace：仓库根存在 buf.work.yaml，且目录收纳正确"
    - "[ ] buf.yaml 启用了 lint/breaking（DEFAULT），且未使用 build.*"
    - "[ ] buf.gen.yaml 输出到 api/grpc/gen，且 paths=source_relative；managed.go_package_prefix.default=github.com/ArtisanCloud/PowerX/api/grpc/gen"
    - "[ ] Makefile 提供 proto-gen / proto-lint / proto-clean"
    - "[ ] api/grpc/gen 已在 .gitignore（或锁定插件版本并允许提交产物）"

templates:

  buf_yaml: |
    # api/grpc/contracts/buf.yaml
    version: v1
    name: buf.build/powerx/core
    lint:
      use:
        - DEFAULT
    breaking:
      use:
        - FILE
      ignore_unstable_packages: true

  buf_gen_yaml: |
    # api/grpc/contracts/buf.gen.yaml
    version: v1
    plugins:
      - plugin: buf.build/protocolbuffers/go:v1.33.0
        out: api/grpc/gen
        opt:
          - paths=source_relative
      - plugin: buf.build/grpc/go:v1.3.0
        out: api/grpc/gen
        opt:
          - paths=source_relative
    managed:
      enabled: true
      go_package_prefix:
        default: github.com/ArtisanCloud/PowerX/api/grpc/gen
        except:
          - buf.build/googleapis/googleapis
          - buf.build/protocolbuffers/wellknowntypes

  makefile_targets: |
    # Makefile（片段）
    .PHONY: proto-gen proto-lint proto-clean

    # 建议在仓库根执行（buf 会向上查找 buf.work.yaml）
    proto-gen:
    \tbuf generate --template api/grpc/contracts/buf.gen.yaml --path api/grpc

    proto-lint:
    \tbuf lint --config api/grpc/contracts/buf.yaml && buf breaking --against '.git#branch=main' --config api/grpc/contracts/buf.yaml || true

    proto-clean:
    \trm -rf api/grpc/gen/*

  proto_example: |
    // api/grpc/media/asset.proto
    syntax = "proto3";
    package powerx.media.v1;
    option go_package = "github.com/ArtisanCloud/PowerX/api/grpc/gen/powerx/media/v1;mediav1";

    import "google/protobuf/empty.proto";
    import "google/protobuf/timestamp.proto";

    message Pagination {
      int64 total = 1;
      int32 page = 2;
      int32 page_size = 3;
      int32 pages = 4;
    }

    message MediaAsset {
      string id = 1;
      uint64 tenant_id = 2;
      string name = 3;
      string code = 4;
      string meta_json = 5;
      int32 status = 6;
      google.protobuf.Timestamp created_at = 7;
      google.protobuf.Timestamp updated_at = 8;
    }

    message GetMediaAssetRequest { string id = 1; }
    message MediaAssetResponse { MediaAsset data = 1; }

    service MediaAssetService {
      rpc GetMediaAsset(GetMediaAssetRequest) returns (MediaAssetResponse);
    }

  gitignore_snippet: |
    # .gitignore（片段）
    /api/grpc/gen/
````
