diff --git a/.conform.yaml b/.conform.yaml index 24f4539..15c8fb3 100644 --- a/.conform.yaml +++ b/.conform.yaml @@ -4,17 +4,30 @@ structure: directories: required: - - cmd/worker - - internal/platform - - internal/parser - - internal/agent - - internal/pipeline - - internal/config - - pkg/version + - backend + - backend/cmd/worker + - backend/internal/agent + - backend/internal/config + - backend/internal/editor + - backend/internal/parser + - backend/internal/pipeline + - backend/internal/platform + - backend/internal/runtime + - backend/internal/schema + - backend/pkg/version + - frontend + - deploy + - docs + - .skills files: required: - README.md - Makefile - - go.mod - - cmd/worker/main.go + - .golangci.yml + - .conform.yaml + - backend/go.mod + - backend/go.sum + - backend/cmd/worker/main.go + - frontend/README.md + - deploy/README.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2633176..df42d68 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,8 @@ # code-bee · 持续集成工作流 # # 核心功能: -# 1. 在代码推送或 PR 提交时自动运行编译、测试与静态分析 -# 2. 复用 Makefile 中已有的 build / test / lint 目标,保持本地与 CI 行为一致 +# 1. 对提交信息、目录结构、后端静态分析、单元测试、race 检测和构建进行分阶段门禁 +# 2. 复用 Makefile 中的统一命令入口,保证本地、CI、发布前校验使用完全一致的执行口径 # # 触发时机: # - push 到 main / master 分支 @@ -10,6 +10,7 @@ # # 维护: AI Assistant # 创建时间: 2026-07-04 +# 更新时间: 2026-07-06 name: CI on: @@ -28,10 +29,55 @@ concurrency: cancel-in-progress: true jobs: - check: - name: Build & Test & Lint + commits: + name: Check Commits runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 5 + + steps: + - name: Checkout 代码 + uses: actions/checkout@v5 + with: + fetch-depth: 2 + + - name: 校验 PR 标题 + if: github.event_name == 'pull_request' + run: ./scripts/check_commit_message.sh --message "${{ github.event.pull_request.title }}" + + - name: 校验最新提交信息 + if: github.event_name == 'push' + run: ./scripts/check_commit_message.sh --head + + structure: + name: Check Structure + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: Checkout 代码 + uses: actions/checkout@v5 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.23' + cache: false + + - name: 安装 conform + run: | + go install github.com/siderolabs/conform/cmd/conform@latest + echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" + + - name: 校验项目目录结构 + run: make check-structure + + backend-lint: + name: Backend Lint + runs-on: ubuntu-latest + timeout-minutes: 10 + needs: + - commits + - structure steps: - name: Checkout 代码 @@ -42,12 +88,19 @@ jobs: with: go-version: '1.23' cache: true + cache-dependency-path: backend/go.sum - name: 下载依赖 - run: go mod download + run: cd backend && go mod download + + - name: 安装 golangci-lint + run: | + go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" - name: 校验 go.mod 同步 run: | + cd backend go mod tidy if [ -n "$(git status --porcelain go.mod go.sum)" ]; then echo "::error::go.mod 或 go.sum 不同步,请在本地运行 go mod tidy 后提交" @@ -55,14 +108,84 @@ jobs: exit 1 fi - - name: 编译 - run: make build + - name: 静态分析 + run: make lint-backend - - name: 单元测试 - run: make test + backend-unit: + name: Backend Unit Test + runs-on: ubuntu-latest + timeout-minutes: 15 + needs: + - commits + - structure - - name: 静态分析 - uses: golangci/golangci-lint-action@v6 + steps: + - name: Checkout 代码 + uses: actions/checkout@v5 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.23' + cache: true + cache-dependency-path: backend/go.sum + + - name: 运行后端单元测试 + run: make test-backend-unit + + backend-race: + name: Backend Race Test + runs-on: ubuntu-latest + timeout-minutes: 20 + needs: + - commits + - structure + + steps: + - name: Checkout 代码 + uses: actions/checkout@v5 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.23' + cache: true + cache-dependency-path: backend/go.sum + + - name: 安装 conform + run: | + go install github.com/siderolabs/conform/cmd/conform@latest + go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" + + - name: 运行后端 race 测试 + run: make test-backend-race + + backend-build: + name: Backend Build + runs-on: ubuntu-latest + timeout-minutes: 10 + needs: + - backend-lint + - backend-unit + - backend-race + + steps: + - name: Checkout 代码 + uses: actions/checkout@v5 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.23' + cache: true + cache-dependency-path: backend/go.sum + + - name: 编译后端 + run: make build-backend + + - name: 上传二进制产物 + uses: actions/upload-artifact@v4 with: - version: latest - args: --config .golangci.yml + name: code-bee-backend-binary + path: bin/code-bee diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ec39a3c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,70 @@ +# code-bee · 发布流水线 +# +# 核心功能: +# 1. 在 tag 发布或手动触发时执行发布前总校验与最小冒烟校验,防止未通过门禁的代码进入发布阶段 +# 2. 构建后端二进制并在 tag 场景下创建 GitHub Release,确保发布动作与主干质量门禁绑定 +# +# 维护: AI Assistant +# 创建时间: 2026-07-06 +name: Release + +permissions: + contents: write + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +jobs: + release: + name: Release Check + runs-on: ubuntu-latest + timeout-minutes: 20 + + steps: + - name: Checkout 代码 + uses: actions/checkout@v5 + with: + fetch-depth: 0 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.23' + cache: true + cache-dependency-path: backend/go.sum + + - name: 安装发布校验工具 + run: | + go install github.com/siderolabs/conform/cmd/conform@latest + go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" + + - name: 校验 tag 格式 + if: startsWith(github.ref, 'refs/tags/') + run: | + TAG_NAME="${GITHUB_REF#refs/tags/}" + if [[ ! "$TAG_NAME" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "::error::Tag 格式不符合 Semantic Versioning: $TAG_NAME" + exit 1 + fi + + - name: 发布前总校验 + run: make release-check + + - name: 发布前最小冒烟校验 + run: make release-smoke + + - name: 上传发布二进制 + uses: actions/upload-artifact@v4 + with: + name: code-bee-release-binary + path: bin/code-bee + + - name: 创建 GitHub Release + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v2 + with: + files: bin/code-bee diff --git a/.gitignore b/.gitignore index fd6bdf4..4aa258f 100644 --- a/.gitignore +++ b/.gitignore @@ -20,5 +20,6 @@ bin/ # Build output /worker - +frontend/node_modules/ +frontend/dist/ .trae/ diff --git a/Makefile b/Makefile index b49c664..ff61d59 100644 --- a/Makefile +++ b/Makefile @@ -1,39 +1,75 @@ # code-bee · 项目统一操作入口 +# +# 核心功能: +# 1. 提供 backend / frontend / deploy 分层后的统一命令入口,避免开发者记忆各子目录的具体命令 +# 2. 将“提交前检查、CI 校验、发布前校验”收口为可复用目标,保证本地与流水线执行口径一致 +# +# 开发维护: AI Assistant +# 创建时间: 2026-07-04 +# 更新时间: 2026-07-06 -.PHONY: help build test lint check-structure check-all clean +.PHONY: \ + help \ + build build-backend \ + test test-backend-unit test-backend-race test-ci \ + lint lint-backend \ + check-structure check-commits check-all \ + install-hooks release-check release-smoke \ + clean # 默认目标 help: @echo "code-bee · Issue-driven AI coding scheduler" @echo "" - @echo " make build 编译二进制" - @echo " make test 运行测试" - @echo " make lint Go 代码静态分析" - @echo " make check-structure 项目目录结构校验" - @echo " make check-all 全部校验" - @echo " make clean 清理编译产物" + @echo " make build 编译后端二进制" + @echo " make test 运行后端 race 测试" + @echo " make lint 运行后端静态分析" + @echo " make test-backend-unit 运行后端单元测试" + @echo " make test-backend-race 运行后端 race 测试" + @echo " make check-structure 项目目录结构校验" + @echo " make check-commits 当前提交信息格式校验" + @echo " make check-all 提交前静态门禁" + @echo " make test-ci CI 级测试入口" + @echo " make install-hooks 安装本地 Git hooks" + @echo " make release-check 发布前总校验" + @echo " make release-smoke 发布前最小冒烟校验" + @echo " make clean 清理编译产物" @echo "" # ============================================================ # 编译 # ============================================================ -build: +build: build-backend + +build-backend: @echo "→ 编译 code-bee..." - go build -o bin/code-bee ./cmd/worker/ + cd backend && go build -o ../bin/code-bee ./cmd/worker/ # ============================================================ # 测试 # ============================================================ -test: +test: test-backend-race + +test-backend-unit: + @echo "→ 运行后端单元测试..." + cd backend && go test -v ./... + +test-backend-race: @echo "→ 运行测试..." - go test -race -v ./... + cd backend && go test -race -v ./... + +test-ci: test-backend-unit test-backend-race + @echo "" + @echo "✅ CI 级测试通过" # ============================================================ # 静态分析 # ============================================================ -lint: +lint: lint-backend + +lint-backend: @echo "→ golangci-lint 代码检查..." - golangci-lint run -c .golangci.yml ./... + cd backend && golangci-lint run -c ../.golangci.yml ./... # ============================================================ # 目录结构校验 @@ -42,13 +78,38 @@ check-structure: @echo "→ conform 目录结构校验..." conform enforce +check-commits: + @echo "→ 校验当前提交信息格式..." + ./scripts/check_commit_message.sh --head + # ============================================================ # 全部校验 # ============================================================ -check-all: lint check-structure +check-all: lint-backend check-structure check-commits @echo "" @echo "✅ 全部校验通过" +# ============================================================ +# Git hooks +# ============================================================ +install-hooks: + @echo "→ 安装本地 Git hooks..." + ./scripts/install_hooks.sh + +# ============================================================ +# 发布前校验 +# ============================================================ +release-check: check-all test-ci build-backend + @echo "" + @echo "✅ 发布前总校验通过" + +release-smoke: + @echo "→ 执行发布前最小冒烟校验..." + test -f bin/code-bee + # 使用 version 子命令做无副作用冒烟校验,避免帮助输出路径返回非零退出码。 + ./bin/code-bee -version >/dev/null + @echo "✅ 冒烟校验通过" + # ============================================================ # 清理 # ============================================================ diff --git a/README.md b/README.md index 9b82b1a..8687646 100644 --- a/README.md +++ b/README.md @@ -302,19 +302,19 @@ code-bee --repo your-org/your-repo --issue 42 --workflow ./my-workflow.yaml ``` . -├── cmd/worker/ # CLI 入口,加载 workflow 并启动调度 -├── internal/ -│ ├── agent/ # 编码智能体执行器(runner) -│ ├── config/ # 运行时配置(Repo/IssueNumber/Token) -│ ├── parser/ # Issue 解析 -│ ├── pipeline/ # 文件契约 + runtime.Engine 薄封装 + ArtifactResolver 适配层 -│ ├── platform/ # 平台适配(GitHub 等) -│ ├── runtime/ # 可编排执行引擎(Engine/Tool/Executor/PromptBuilder + 默认 workflow) -│ └── schema/ # 三层 JSON Schema + YAML 加载器 + 语义校验 -├── pkg/version/ # 版本信息 -├── Makefile # 统一操作入口 -├── .conform.yaml # 目录结构校验 -└── .golangci.yml # 代码规范检查 +├── backend/ # Go 后端工程 +│ ├── cmd/worker/ # CLI 入口 +│ ├── internal/ # 私有代码(agent/config/editor/parser/pipeline/platform/runtime/schema) +│ ├── pkg/version/ # 版本信息 +│ ├── go.mod +│ └── go.sum +├── frontend/ # React 前端工程(预留) +├── deploy/ # 部署资产(预留) +├── docs/ # 项目文档与设计图 +├── .skills/ # 智能体技能资产 +├── Makefile # 仓库级统一操作入口 +├── .conform.yaml # 目录结构校验 +└── .golangci.yml # Go 代码规范检查 ``` --- @@ -389,9 +389,9 @@ prompt 模板中的角色展示名(如 `@{{.DefaultAgent}}`)从 `workflow.To ### 更多细节 -- [Schema 定义](internal/schema/schemas/v1/README.md) —— 三层 JSON Schema 与校验机制 -- [默认 workflow](internal/runtime/default_workflow.yaml) —— 内置配置参考 -- [Prompt 模板](internal/runtime/prompts/) —— 5 个内置模板文件 +- [Schema 定义](backend/internal/schema/schemas/v1/README.md) —— 三层 JSON Schema 与校验机制 +- [默认 workflow](backend/internal/runtime/default_workflow.yaml) —— 内置配置参考 +- [Prompt 模板](backend/internal/runtime/prompts/) —— 5 个内置模板文件 --- @@ -425,11 +425,14 @@ code-bee 的场景驱动调度已落地,未来计划扩展的方向包括: ### 开发命令 ```bash -make build # 编译 -make test # 运行测试 -make lint # 代码检查 -make check-structure # 目录结构校验 -make check-all # 全部校验 +make build # 编译后端二进制 +make test-backend-unit # 后端单元测试 +make test-backend-race # 后端 race 测试 +make lint-backend # 后端代码检查 +make check-structure # 目录结构校验 +make check-commits # 提交信息校验 +make check-all # 提交前静态门禁 +make release-check # 发布前总校验 ``` --- diff --git a/cmd/worker/main.go b/backend/cmd/worker/main.go similarity index 100% rename from cmd/worker/main.go rename to backend/cmd/worker/main.go diff --git a/cmd/worker/main_test.go b/backend/cmd/worker/main_test.go similarity index 100% rename from cmd/worker/main_test.go rename to backend/cmd/worker/main_test.go diff --git a/go.mod b/backend/go.mod similarity index 100% rename from go.mod rename to backend/go.mod diff --git a/go.sum b/backend/go.sum similarity index 100% rename from go.sum rename to backend/go.sum diff --git a/internal/agent/runner.go b/backend/internal/agent/runner.go similarity index 100% rename from internal/agent/runner.go rename to backend/internal/agent/runner.go diff --git a/internal/agent/runner_test.go b/backend/internal/agent/runner_test.go similarity index 100% rename from internal/agent/runner_test.go rename to backend/internal/agent/runner_test.go diff --git a/internal/config/config.go b/backend/internal/config/config.go similarity index 100% rename from internal/config/config.go rename to backend/internal/config/config.go diff --git a/internal/config/config_test.go b/backend/internal/config/config_test.go similarity index 100% rename from internal/config/config_test.go rename to backend/internal/config/config_test.go diff --git a/backend/internal/editor/schema/codec.go b/backend/internal/editor/schema/codec.go new file mode 100644 index 0000000..77c2920 --- /dev/null +++ b/backend/internal/editor/schema/codec.go @@ -0,0 +1,151 @@ +// Package schema 提供标准 YAML Schema 的编解码能力。 +// +// 核心功能: +// 1. 将标准 YAML 文本解析为编辑器侧可消费的 Document 结构 +// 2. 将内存中的 Document 重新组装为稳定、可读的 YAML 文本 +// +// 开发维护: AI Assistant +// 创建时间: 2026-07-06 +// 更新时间: 2026-07-06 +package schema + +import ( + "bytes" + "fmt" + "os" + "strings" + + "gopkg.in/yaml.v3" +) + +const schemaFilePermission = 0o600 + +// DecodeYAML 将标准 YAML 文本解析为 Document,并执行静态校验。 +// +// 输入参数: +// - content: 标准 YAML 文件的原始字节内容 +// +// 返回值: +// - *Document: 解析成功后的文档对象 +// - error: 当 YAML 为空、字段未知、格式非法或结构校验失败时返回错误 +// +// 核心实现逻辑: +// - 使用 yaml.Decoder 的 KnownFields 模式阻断未知字段,避免拼写错误静默漏掉 +// - 解析完成后统一调用 Validate,保证所有入口的校验规则一致 +// +// 调用注意事项: +// - 该函数只面向编辑器标准 YAML,不应用于自由格式 YAML 的宽松解析 +func DecodeYAML(content []byte) (*Document, error) { + trimmedContent := strings.TrimSpace(string(content)) + if trimmedContent == "" { + return nil, fmt.Errorf("empty schema yaml content") + } + + var doc Document + decoder := yaml.NewDecoder(bytes.NewReader([]byte(trimmedContent))) + decoder.KnownFields(true) + + if err := decoder.Decode(&doc); err != nil { + return nil, fmt.Errorf("decode schema yaml: %w", err) + } + + if err := Validate(&doc); err != nil { + return nil, fmt.Errorf("decode schema yaml: %w", err) + } + + return &doc, nil +} + +// EncodeYAML 将 Document 组装为稳定的 YAML 文本。 +// +// 输入参数: +// - doc: 待组装的文档对象 +// +// 返回值: +// - []byte: 组装完成的 YAML 文本 +// - error: 当文档为空、校验失败或编码失败时返回错误 +// +// 核心实现逻辑: +// - 先调用 Validate,确保不会把非法文档落盘为 YAML 文件 +// - 使用 2 空格缩进输出,保证编辑器保存结果稳定、可读 +// +// 调用注意事项: +// - 当前输出不追求保留原始注释和字段顺序以外的格式细节,只保证结构语义稳定 +func EncodeYAML(doc *Document) ([]byte, error) { + if err := Validate(doc); err != nil { + return nil, fmt.Errorf("encode schema yaml: %w", err) + } + + var buffer bytes.Buffer + encoder := yaml.NewEncoder(&buffer) + encoder.SetIndent(2) + + // 编码器需要显式关闭,以确保内部缓冲区完整 flush 到外层 buffer。 + if err := encoder.Encode(doc); err != nil { + _ = encoder.Close() + return nil, fmt.Errorf("encode schema yaml: %w", err) + } + + if err := encoder.Close(); err != nil { + return nil, fmt.Errorf("encode schema yaml: %w", err) + } + + return buffer.Bytes(), nil +} + +// LoadFile 从磁盘读取并解析一份标准 YAML Schema 文件。 +// +// 输入参数: +// - filePath: YAML 文件路径 +// +// 返回值: +// - *Document: 读取并校验后的文档对象 +// - error: 当读文件失败、YAML 非法或结构不合法时返回错误 +// +// 核心实现逻辑: +// - 先读文件,再统一复用 DecodeYAML 完成解析与校验 +// - 将文件路径包进错误上下文,便于调用方定位坏文件 +// +// 调用注意事项: +// - filePath 指向的文件内容必须是标准 YAML Schema,而不是任意 YAML +func LoadFile(filePath string) (*Document, error) { + content, err := os.ReadFile(filePath) + if err != nil { + return nil, fmt.Errorf("load schema file %s: %w", filePath, err) + } + + doc, err := DecodeYAML(content) + if err != nil { + return nil, fmt.Errorf("load schema file %s: %w", filePath, err) + } + + return doc, nil +} + +// SaveFile 将 Document 保存为标准 YAML Schema 文件。 +// +// 输入参数: +// - filePath: YAML 文件输出路径 +// - doc: 待保存的文档对象 +// +// 返回值: +// - error: 当文档非法、编码失败或写文件失败时返回错误 +// +// 核心实现逻辑: +// - 先复用 EncodeYAML 统一完成校验与编码 +// - 使用受限文件权限写盘,避免默认权限过宽 +// +// 调用注意事项: +// - 当前函数不负责创建父目录,调用方应提前保证目录存在 +func SaveFile(filePath string, doc *Document) error { + content, err := EncodeYAML(doc) + if err != nil { + return fmt.Errorf("save schema file %s: %w", filePath, err) + } + + if err := os.WriteFile(filePath, content, schemaFilePermission); err != nil { + return fmt.Errorf("save schema file %s: %w", filePath, err) + } + + return nil +} diff --git a/backend/internal/editor/schema/defaults.go b/backend/internal/editor/schema/defaults.go new file mode 100644 index 0000000..ff4b244 --- /dev/null +++ b/backend/internal/editor/schema/defaults.go @@ -0,0 +1,353 @@ +// Package schema 提供标准 YAML Schema 的默认文档构造能力。 +// +// 核心功能: +// 1. 将当前 CodeB 已稳定下来的默认编排事实收口为一份标准文档对象 +// 2. 为后续 editor state 与 UI 层提供统一、可复用的默认输入,而不是散落在测试中的字符串样例 +// +// 开发维护: AI Assistant +// 创建时间: 2026-07-06 +// 更新时间: 2026-07-06 +package schema + +const ( + // DefaultDocumentName 是默认编排文档的稳定名称。 + DefaultDocumentName = "default-issue-flow" + + // DefaultDocumentDescription 是默认编排文档的稳定说明。 + DefaultDocumentDescription = "CodeB 当前固定四阶段流程与 coder-reviewer loop" + + // DefaultStartNodeID 是默认编排图的起始节点 ID。 + DefaultStartNodeID = "issue_handling" + + // DefaultIssueHandlingNodeID 是 Issue 处理节点的稳定 ID。 + DefaultIssueHandlingNodeID = "issue_handling" + + // DefaultIssuePostIntakeNodeID 是接单回帖节点的稳定 ID。 + DefaultIssuePostIntakeNodeID = "issue_post_intake" + + // DefaultCodingNodeID 是编码主节点的稳定 ID。 + DefaultCodingNodeID = "coding_main" + + // DefaultReviewNodeID 是审查主节点的稳定 ID。 + DefaultReviewNodeID = "review_main" + + // DefaultIssuePostCompletionNodeID 是完成回帖节点的稳定 ID。 + DefaultIssuePostCompletionNodeID = "issue_post_completion" + + // DefaultLoopJudgeNodeID 是价值评估节点的稳定 ID。 + DefaultLoopJudgeNodeID = "loop_judge_main" + + // DefaultIssueHandlingArtifact 是 Issue 处理结果文件名。 + DefaultIssueHandlingArtifact = "issue_intake_result.json" + + // DefaultCodingArtifact 是编码结果文件名。 + DefaultCodingArtifact = "coding_result.json" + + // DefaultReviewArtifact 是审查结果文件名。 + DefaultReviewArtifact = "review_result.json" + + // DefaultIssuePostIntakeArtifact 是接单回帖结果文件名。 + DefaultIssuePostIntakeArtifact = "issue_post_result_intake.json" + + // DefaultIssuePostCompletionArtifact 是完成回帖结果文件名。 + DefaultIssuePostCompletionArtifact = "issue_post_result_completion.json" + + // DefaultIssuePostPatternArtifact 是通用 Issue 提交结果文件命名模式。 + DefaultIssuePostPatternArtifact = "issue_post_result_{purpose}.json" + + // DefaultLoopJudgeArtifact 是价值评估结果文件名。 + DefaultLoopJudgeArtifact = "loop_judge_result.json" + + // DefaultLoopHistoryArtifact 是循环历史聚合文件名。 + DefaultLoopHistoryArtifact = "loop_history.json" + + // DefaultMaxCodingReviewRounds 是默认 coder-reviewer 最大轮数。 + DefaultMaxCodingReviewRounds = 3 + + // DefaultMaxConsecutiveUnknownReview 是默认连续 UNKNOWN 阈值。 + DefaultMaxConsecutiveUnknownReview = 2 + + // DefaultMaxIssuePostAttempts 是默认 Issue 提交最大重试次数。 + DefaultMaxIssuePostAttempts = 2 + + // DefaultLoopJudgeStartRound 是默认价值评估触发起始轮。 + DefaultLoopJudgeStartRound = 0 + + // DefaultIssueHandlingPositionX 是 Issue 处理节点的默认画布横坐标。 + DefaultIssueHandlingPositionX = 120 + + // DefaultIssueHandlingPositionY 是 Issue 处理节点的默认画布纵坐标。 + DefaultIssueHandlingPositionY = 120 + + // DefaultIssuePostIntakePositionX 是接单回帖节点的默认画布横坐标。 + DefaultIssuePostIntakePositionX = 340 + + // DefaultIssuePostIntakePositionY 是接单回帖节点的默认画布纵坐标。 + DefaultIssuePostIntakePositionY = 120 + + // DefaultCodingPositionX 是编码节点的默认画布横坐标。 + DefaultCodingPositionX = 620 + + // DefaultCodingPositionY 是编码节点的默认画布纵坐标。 + DefaultCodingPositionY = 120 + + // DefaultReviewPositionX 是审查节点的默认画布横坐标。 + DefaultReviewPositionX = 900 + + // DefaultReviewPositionY 是审查节点的默认画布纵坐标。 + DefaultReviewPositionY = 120 + + // DefaultIssuePostCompletionPositionX 是完成回帖节点的默认画布横坐标。 + DefaultIssuePostCompletionPositionX = 1180 + + // DefaultIssuePostCompletionPositionY 是完成回帖节点的默认画布纵坐标。 + DefaultIssuePostCompletionPositionY = 120 + + // DefaultLoopJudgePositionX 是价值评估节点的默认画布横坐标。 + DefaultLoopJudgePositionX = 900 + + // DefaultLoopJudgePositionY 是价值评估节点的默认画布纵坐标。 + DefaultLoopJudgePositionY = 320 +) + +// BuildDefaultDocument 构造一份可直接用于编辑器初始化的标准默认文档。 +// +// 输入参数: +// - 无;该函数内部直接使用当前源码映射稿中已经明确的默认节点图、角色、工件命名和 runtime 参数 +// +// 返回值: +// - *Document: 一份可直接参与 Validate、EncodeYAML、DecodeYAML、SaveFile/LoadFile 闭环的默认文档 +// +// 核心实现逻辑: +// - 顶层结构固定为 codebee/v1 + Orchestration +// - workflow、runtime、roles、artifacts、ui 分别由独立 helper 构造,避免默认事实散落 +// - 所有默认值严格对齐当前源码映射稿,不额外发明运行时语义 +// +// 调用注意事项: +// - 返回对象仅作为编辑器的默认输入,不代表当前运行时会直接消费该文档 +func BuildDefaultDocument() *Document { + return &Document{ + APIVersion: APIVersion, + Kind: Kind, + Metadata: Metadata{ + Name: DefaultDocumentName, + Description: DefaultDocumentDescription, + }, + Workflow: buildDefaultWorkflow(), + Runtime: buildDefaultRuntime(), + Roles: buildDefaultRoles(), + Artifacts: buildDefaultArtifacts(), + UI: buildDefaultUI(), + } +} + +// buildDefaultWorkflow 构造默认节点图主体。 +func buildDefaultWorkflow() Workflow { + return Workflow{ + StartNodeID: DefaultStartNodeID, + Nodes: buildDefaultNodes(), + Edges: buildDefaultEdges(), + } +} + +// buildDefaultNodes 构造默认编排中的固定节点列表。 +func buildDefaultNodes() []Node { + return []Node{ + { + ID: DefaultIssueHandlingNodeID, + Type: NodeTypeIssueHandling, + Name: "Issue 处理", + RoleRef: RoleRefIssuePostAgent, + PromptTemplate: PromptTemplateIssueHandling, + ResultContract: ResultContractIssueHandling, + Outputs: []string{DefaultIssueHandlingArtifact}, + }, + { + ID: DefaultIssuePostIntakeNodeID, + Type: NodeTypeIssuePost, + Name: "接单回帖", + RoleRef: RoleRefIssuePostAgent, + PromptTemplate: PromptTemplateIssuePost, + ResultContract: ResultContractIssuePost, + Inputs: []string{DefaultIssueHandlingArtifact}, + Outputs: []string{DefaultIssuePostIntakeArtifact}, + }, + { + ID: DefaultCodingNodeID, + Type: NodeTypeCoding, + Name: "编码实现", + RoleRef: RoleRefDefaultAgent, + PromptTemplate: PromptTemplateCoding, + ResultContract: ResultContractCoding, + Inputs: []string{DefaultIssueHandlingArtifact, DefaultReviewArtifact}, + Outputs: []string{DefaultCodingArtifact}, + }, + { + ID: DefaultReviewNodeID, + Type: NodeTypeReview, + Name: "结果审查", + RoleRef: RoleRefReviewerAgent, + PromptTemplate: PromptTemplateReview, + ResultContract: ResultContractReview, + Inputs: []string{DefaultIssueHandlingArtifact, DefaultCodingArtifact}, + Outputs: []string{DefaultReviewArtifact}, + }, + { + ID: DefaultIssuePostCompletionNodeID, + Type: NodeTypeIssuePost, + Name: "完成回帖", + RoleRef: RoleRefIssuePostAgent, + PromptTemplate: PromptTemplateIssuePost, + ResultContract: ResultContractIssuePost, + Inputs: []string{DefaultReviewArtifact}, + Outputs: []string{DefaultIssuePostCompletionArtifact}, + }, + { + ID: DefaultLoopJudgeNodeID, + Type: NodeTypeLoopJudge, + Name: "价值评估", + RoleRef: RoleRefLoopJudgeAgent, + PromptTemplate: PromptTemplateLoopJudge, + ResultContract: ResultContractLoopJudge, + Inputs: []string{DefaultLoopHistoryArtifact}, + Outputs: []string{DefaultLoopJudgeArtifact}, + }, + } +} + +// buildDefaultEdges 构造默认节点图中的固定边列表。 +func buildDefaultEdges() []Edge { + return []Edge{ + { + From: DefaultIssueHandlingNodeID, + To: DefaultIssuePostIntakeNodeID, + }, + { + From: DefaultIssuePostIntakeNodeID, + To: DefaultCodingNodeID, + When: EdgeCondition{ + IssuePostStatus: IssuePostStatusPosted, + }, + }, + { + From: DefaultCodingNodeID, + To: DefaultReviewNodeID, + When: EdgeCondition{ + CodingStatus: CodingStatusDone, + }, + }, + { + From: DefaultReviewNodeID, + To: DefaultIssuePostCompletionNodeID, + When: EdgeCondition{ + ReviewStatus: ReviewStatusPass, + }, + }, + { + From: DefaultReviewNodeID, + To: DefaultCodingNodeID, + When: EdgeCondition{ + ReviewStatus: ReviewStatusFail, + }, + }, + { + From: DefaultReviewNodeID, + To: DefaultCodingNodeID, + When: EdgeCondition{ + ReviewStatus: ReviewStatusUnknown, + }, + }, + { + From: DefaultReviewNodeID, + To: DefaultLoopJudgeNodeID, + When: EdgeCondition{ + ReviewStatus: ReviewStatusFail, + }, + }, + { + From: DefaultReviewNodeID, + To: DefaultLoopJudgeNodeID, + When: EdgeCondition{ + ReviewStatus: ReviewStatusUnknown, + }, + }, + { + From: DefaultLoopJudgeNodeID, + To: DefaultCodingNodeID, + When: EdgeCondition{ + LoopJudgeDecision: LoopJudgeDecisionContinue, + }, + }, + { + From: DefaultLoopJudgeNodeID, + To: DefaultCodingNodeID, + When: EdgeCondition{ + LoopJudgeDecision: LoopJudgeDecisionShrinkTask, + }, + }, + } +} + +// buildDefaultRuntime 构造默认 runtime 参数块。 +func buildDefaultRuntime() Runtime { + return Runtime{ + MaxCodingReviewRounds: DefaultMaxCodingReviewRounds, + MaxConsecutiveUnknownReview: DefaultMaxConsecutiveUnknownReview, + MaxIssuePostAttempts: DefaultMaxIssuePostAttempts, + LoopJudgeStartRound: DefaultLoopJudgeStartRound, + } +} + +// buildDefaultRoles 构造默认角色配置块。 +func buildDefaultRoles() Roles { + return Roles{ + DefaultAgent: "开发者", + ReviewerAgent: "QA负责人", + IssuePostAgent: "产品经理", + LoopJudgeAgent: "技术负责人", + } +} + +// buildDefaultArtifacts 构造默认工件命名配置块。 +func buildDefaultArtifacts() Artifacts { + return Artifacts{ + IssueHandlingResult: DefaultIssueHandlingArtifact, + CodingResult: DefaultCodingArtifact, + ReviewResult: DefaultReviewArtifact, + IssuePostResult: DefaultIssuePostPatternArtifact, + LoopJudgeResult: DefaultLoopJudgeArtifact, + LoopHistory: DefaultLoopHistoryArtifact, + } +} + +// buildDefaultUI 构造默认画布元数据。 +func buildDefaultUI() UISettings { + return UISettings{ + Nodes: map[string]NodeUI{ + DefaultIssueHandlingNodeID: { + X: DefaultIssueHandlingPositionX, + Y: DefaultIssueHandlingPositionY, + }, + DefaultIssuePostIntakeNodeID: { + X: DefaultIssuePostIntakePositionX, + Y: DefaultIssuePostIntakePositionY, + }, + DefaultCodingNodeID: { + X: DefaultCodingPositionX, + Y: DefaultCodingPositionY, + }, + DefaultReviewNodeID: { + X: DefaultReviewPositionX, + Y: DefaultReviewPositionY, + }, + DefaultIssuePostCompletionNodeID: { + X: DefaultIssuePostCompletionPositionX, + Y: DefaultIssuePostCompletionPositionY, + }, + DefaultLoopJudgeNodeID: { + X: DefaultLoopJudgePositionX, + Y: DefaultLoopJudgePositionY, + }, + }, + } +} diff --git a/backend/internal/editor/schema/defaults_test.go b/backend/internal/editor/schema/defaults_test.go new file mode 100644 index 0000000..ed1f8be --- /dev/null +++ b/backend/internal/editor/schema/defaults_test.go @@ -0,0 +1,72 @@ +// Package schema 覆盖默认标准文档构造能力。 +// +// 核心功能: +// 1. 验证 BuildDefaultDocument 生成的默认文档与当前源码映射稿保持一致 +// 2. 验证默认文档可以稳定通过 Validate、EncodeYAML、DecodeYAML 闭环 +// +// 开发维护: AI Assistant +// 创建时间: 2026-07-06 +// 更新时间: 2026-07-06 +package schema + +import "testing" + +// TestBuildDefaultDocumentReturnsValidDocument 验证默认文档构造器直接返回一份合法文档。 +func TestBuildDefaultDocumentReturnsValidDocument(t *testing.T) { + doc := BuildDefaultDocument() + + if err := Validate(doc); err != nil { + t.Fatalf("Validate(BuildDefaultDocument()) unexpected error: %v", err) + } + + if doc.APIVersion != APIVersion { + t.Fatalf("APIVersion = %q, want %q", doc.APIVersion, APIVersion) + } + + if doc.Kind != Kind { + t.Fatalf("Kind = %q, want %q", doc.Kind, Kind) + } + + if doc.Metadata.Name != DefaultDocumentName { + t.Fatalf("Metadata.Name = %q, want %q", doc.Metadata.Name, DefaultDocumentName) + } +} + +// TestBuildDefaultDocumentRoundTrip 验证默认文档可稳定完成 YAML round-trip。 +func TestBuildDefaultDocumentRoundTrip(t *testing.T) { + doc := BuildDefaultDocument() + + content, err := EncodeYAML(doc) + if err != nil { + t.Fatalf("EncodeYAML(BuildDefaultDocument()) unexpected error: %v", err) + } + + decodedDoc, err := DecodeYAML(content) + if err != nil { + t.Fatalf("DecodeYAML(EncodeYAML(BuildDefaultDocument())) unexpected error: %v", err) + } + + if decodedDoc.APIVersion != APIVersion { + t.Fatalf("decoded APIVersion = %q, want %q", decodedDoc.APIVersion, APIVersion) + } + + if decodedDoc.Kind != Kind { + t.Fatalf("decoded Kind = %q, want %q", decodedDoc.Kind, Kind) + } + + if decodedDoc.Metadata.Name != DefaultDocumentName { + t.Fatalf("decoded Metadata.Name = %q, want %q", decodedDoc.Metadata.Name, DefaultDocumentName) + } + + if decodedDoc.Workflow.StartNodeID != DefaultStartNodeID { + t.Fatalf("decoded Workflow.StartNodeID = %q, want %q", decodedDoc.Workflow.StartNodeID, DefaultStartNodeID) + } + + if len(decodedDoc.Workflow.Nodes) != len(doc.Workflow.Nodes) { + t.Fatalf("len(decoded Workflow.Nodes) = %d, want %d", len(decodedDoc.Workflow.Nodes), len(doc.Workflow.Nodes)) + } + + if len(decodedDoc.Workflow.Edges) != len(doc.Workflow.Edges) { + t.Fatalf("len(decoded Workflow.Edges) = %d, want %d", len(decodedDoc.Workflow.Edges), len(doc.Workflow.Edges)) + } +} diff --git a/backend/internal/editor/schema/schema.go b/backend/internal/editor/schema/schema.go new file mode 100644 index 0000000..6640abe --- /dev/null +++ b/backend/internal/editor/schema/schema.go @@ -0,0 +1,232 @@ +// Package schema 定义编排编辑器使用的标准 YAML Schema 数据模型。 +// +// 核心功能: +// 1. 提供标准 YAML Schema 在 Go 内存中的稳定结构,作为“YAML <-> 编辑器状态”之间的中间表达 +// 2. 将源码中已经稳定存在的编排事实,抽象为编辑器可解析、可编辑、可重新组装的结构体 +// +// 开发维护: AI Assistant +// 创建时间: 2026-07-06 +// 更新时间: 2026-07-06 +package schema + +const ( + // APIVersion 是当前标准 YAML Schema 的固定版本号。 + APIVersion = "codebee/v1" + + // Kind 是当前标准 YAML Schema 的固定种类标识。 + Kind = "Orchestration" + + // NodeTypeIssueHandling 表示 Issue 处理节点。 + NodeTypeIssueHandling = "issue-handling" + + // NodeTypeCoding 表示编码节点。 + NodeTypeCoding = "coding" + + // NodeTypeReview 表示审查节点。 + NodeTypeReview = "review" + + // NodeTypeIssuePost 表示 Issue 评论提交节点。 + NodeTypeIssuePost = "issue-post" + + // NodeTypeLoopJudge 表示价值评估节点。 + NodeTypeLoopJudge = "loop-judge" + + // PromptTemplateIssueHandling 表示 Issue 处理阶段的提示词模板标识。 + PromptTemplateIssueHandling = "issue-handling" + + // PromptTemplateCoding 表示编码阶段的提示词模板标识。 + PromptTemplateCoding = "coding" + + // PromptTemplateReview 表示审查阶段的提示词模板标识。 + PromptTemplateReview = "review" + + // PromptTemplateIssuePost 表示 Issue 提交阶段的提示词模板标识。 + PromptTemplateIssuePost = "issue-post" + + // PromptTemplateLoopJudge 表示价值评估阶段的提示词模板标识。 + PromptTemplateLoopJudge = "loop-judge" + + // ResultContractIssueHandling 表示 Issue 处理结果契约标识。 + ResultContractIssueHandling = "issue-handling-result" + + // ResultContractCoding 表示编码结果契约标识。 + ResultContractCoding = "coding-result" + + // ResultContractReview 表示审查结果契约标识。 + ResultContractReview = "review-result" + + // ResultContractIssuePost 表示 Issue 提交结果契约标识。 + ResultContractIssuePost = "issue-post-result" + + // ResultContractLoopJudge 表示价值评估结果契约标识。 + ResultContractLoopJudge = "loop-judge-result" + + // RoleRefDefaultAgent 表示编码主角色引用键。 + RoleRefDefaultAgent = "defaultAgent" + + // RoleRefReviewerAgent 表示审查角色引用键。 + RoleRefReviewerAgent = "reviewerAgent" + + // RoleRefIssuePostAgent 表示 Issue 提交角色引用键。 + RoleRefIssuePostAgent = "issuePostAgent" + + // RoleRefLoopJudgeAgent 表示价值评估角色引用键。 + RoleRefLoopJudgeAgent = "loopJudgeAgent" + + // IssueStatusReady 表示当前 Issue 已准备好进入开发。 + IssueStatusReady = "READY" + + // IssueStatusBlocked 表示当前 Issue 因外部条件不足而阻塞。 + IssueStatusBlocked = "BLOCKED" + + // CodingStatusDone 表示本轮编码工作已准备好接受审查。 + CodingStatusDone = "DONE" + + // CodingStatusInProgress 表示本轮编码还未结束。 + CodingStatusInProgress = "IN_PROGRESS" + + // CodingStatusBlocked 表示本轮编码因外部条件不足而阻塞。 + CodingStatusBlocked = "BLOCKED" + + // ReviewStatusPass 表示审查通过。 + ReviewStatusPass = "PASS" + + // ReviewStatusFail 表示审查明确失败。 + ReviewStatusFail = "FAIL" + + // ReviewStatusUnknown 表示审查暂时无法判断。 + ReviewStatusUnknown = "UNKNOWN" + + // ReviewStatusBlocked 表示审查因外部条件不足而阻塞。 + ReviewStatusBlocked = "BLOCKED" + + // IssuePostStatusPosted 表示 Issue 评论已成功提交。 + IssuePostStatusPosted = "POSTED" + + // IssuePostStatusRejected 表示 Issue 评论内容不合格,被拒绝提交。 + IssuePostStatusRejected = "REJECTED" + + // IssuePostStatusBlocked 表示 Issue 提交受外部环境阻塞。 + IssuePostStatusBlocked = "BLOCKED" + + // LoopJudgeDecisionContinue 表示自动循环继续执行。 + LoopJudgeDecisionContinue = "CONTINUE" + + // LoopJudgeDecisionShrinkTask 表示自动循环继续,但下一轮应收缩任务。 + LoopJudgeDecisionShrinkTask = "SHRINK_TASK" + + // LoopJudgeDecisionStopManual 表示应立即停止自动循环,转人工接管。 + LoopJudgeDecisionStopManual = "STOP_MANUAL" + + // LoopJudgeDecisionStopBlocked 表示应立即停止自动循环,转阻塞状态。 + LoopJudgeDecisionStopBlocked = "STOP_BLOCKED" +) + +// Document 表示标准 YAML Schema 的完整文档结构。 +// +// 输入参数: +// - 无;该结构体由 YAML 解码产生,或由调用方在内存中组装 +// +// 返回值: +// - 无;作为顶层数据容器承载 metadata、workflow、runtime、roles、artifacts、ui 六块信息 +// +// 核心实现逻辑: +// - workflow 保存节点图主体 +// - runtime 保存运行参数 +// - roles / artifacts 保存编辑器需要展示和引用的受控配置 +// - ui 保存纯展示元数据,禁止承载运行时语义 +// +// 调用注意事项: +// - Document 只用于编辑器 YAML 解析与组装,不应用于现有调度主流程 +type Document struct { + APIVersion string `yaml:"apiVersion"` + Kind string `yaml:"kind"` + Metadata Metadata `yaml:"metadata"` + Workflow Workflow `yaml:"workflow"` + Runtime Runtime `yaml:"runtime"` + Roles Roles `yaml:"roles"` + Artifacts Artifacts `yaml:"artifacts"` + UI UISettings `yaml:"ui,omitempty"` +} + +// Metadata 表示编排文档的元信息。 +type Metadata struct { + Name string `yaml:"name"` + Description string `yaml:"description,omitempty"` +} + +// Workflow 表示节点图主体。 +type Workflow struct { + StartNodeID string `yaml:"startNodeId"` + Nodes []Node `yaml:"nodes"` + Edges []Edge `yaml:"edges"` +} + +// Node 表示单个编排节点。 +type Node struct { + ID string `yaml:"id"` + Type string `yaml:"type"` + Name string `yaml:"name"` + RoleRef string `yaml:"roleRef"` + PromptTemplate string `yaml:"promptTemplate"` + ResultContract string `yaml:"resultContract"` + Inputs []string `yaml:"inputs,omitempty"` + Outputs []string `yaml:"outputs"` +} + +// Edge 表示节点之间的有向连线。 +type Edge struct { + From string `yaml:"from"` + To string `yaml:"to"` + When EdgeCondition `yaml:"when,omitempty"` +} + +// EdgeCondition 表示受控边条件。 +// +// 设计说明: +// - 当前只允许 5 类受控条件字段,禁止引入任意表达式 +// - 一个条件块内最多只允许设置 1 个字段,避免语义歧义 +type EdgeCondition struct { + IssueStatus string `yaml:"issueStatus,omitempty"` + CodingStatus string `yaml:"codingStatus,omitempty"` + ReviewStatus string `yaml:"reviewStatus,omitempty"` + IssuePostStatus string `yaml:"issuePostStatus,omitempty"` + LoopJudgeDecision string `yaml:"loopJudgeDecision,omitempty"` +} + +// Runtime 表示与循环相关的运行参数块。 +type Runtime struct { + MaxCodingReviewRounds int `yaml:"maxCodingReviewRounds"` + MaxConsecutiveUnknownReview int `yaml:"maxConsecutiveUnknownReview"` + MaxIssuePostAttempts int `yaml:"maxIssuePostAttempts"` + LoopJudgeStartRound int `yaml:"loopJudgeStartRound"` +} + +// Roles 表示标准 YAML 中固定暴露给编辑器的角色配置。 +type Roles struct { + DefaultAgent string `yaml:"defaultAgent"` + ReviewerAgent string `yaml:"reviewerAgent"` + IssuePostAgent string `yaml:"issuePostAgent"` + LoopJudgeAgent string `yaml:"loopJudgeAgent"` +} + +// Artifacts 表示编辑器展示和组装时需要引用的工件命名约定。 +type Artifacts struct { + IssueHandlingResult string `yaml:"issueHandlingResult"` + CodingResult string `yaml:"codingResult"` + ReviewResult string `yaml:"reviewResult"` + IssuePostResult string `yaml:"issuePostResultPattern"` + LoopJudgeResult string `yaml:"loopJudgeResult"` + LoopHistory string `yaml:"loopHistory"` +} + +// UISettings 表示编辑器专用的 UI 元数据块。 +type UISettings struct { + Nodes map[string]NodeUI `yaml:"nodes,omitempty"` +} + +// NodeUI 表示单个节点在画布中的展示位置。 +type NodeUI struct { + X int `yaml:"x"` + Y int `yaml:"y"` +} diff --git a/backend/internal/editor/schema/schema_test.go b/backend/internal/editor/schema/schema_test.go new file mode 100644 index 0000000..5ad0f30 --- /dev/null +++ b/backend/internal/editor/schema/schema_test.go @@ -0,0 +1,245 @@ +// Package schema 覆盖标准 YAML Schema 的解析、组装与静态校验逻辑。 +// +// 核心功能: +// 1. 验证标准 YAML 能稳定解析为 Document,并能重新编码回合法 YAML +// 2. 验证非法枚举、非法引用、非法条件会在解析或编码阶段被及时阻断 +// +// 开发维护: AI Assistant +// 创建时间: 2026-07-06 +// 更新时间: 2026-07-06 +package schema + +import ( + "path/filepath" + "testing" +) + +// TestDecodeYAMLAndEncodeYAMLRoundTrip 验证标准 YAML 能稳定完成解析与组装闭环。 +func TestDecodeYAMLAndEncodeYAMLRoundTrip(t *testing.T) { + doc, err := DecodeYAML(mustEncodeDefaultSchemaYAML(t)) + if err != nil { + t.Fatalf("DecodeYAML() unexpected error: %v", err) + } + + content, err := EncodeYAML(doc) + if err != nil { + t.Fatalf("EncodeYAML() unexpected error: %v", err) + } + + decodedAgain, err := DecodeYAML(content) + if err != nil { + t.Fatalf("DecodeYAML(encoded content) unexpected error: %v", err) + } + + if decodedAgain.Workflow.StartNodeID != "issue_handling" { + t.Fatalf("Workflow.StartNodeID = %q, want %q", decodedAgain.Workflow.StartNodeID, "issue_handling") + } + + if len(decodedAgain.Workflow.Nodes) != 6 { + t.Fatalf("len(Workflow.Nodes) = %d, want %d", len(decodedAgain.Workflow.Nodes), 6) + } +} + +// TestDecodeYAMLRejectsUnknownField 验证未知字段会被严格解析器阻断。 +func TestDecodeYAMLRejectsUnknownField(t *testing.T) { + content := `apiVersion: codebee/v1 +kind: Orchestration +unknownField: true +metadata: + name: default-issue-flow +workflow: + startNodeId: issue_handling + nodes: [] + edges: [] +runtime: + maxCodingReviewRounds: 3 + maxConsecutiveUnknownReview: 2 + maxIssuePostAttempts: 2 + loopJudgeStartRound: 0 +roles: + defaultAgent: 开发者 + reviewerAgent: QA负责人 + issuePostAgent: 产品经理 + loopJudgeAgent: 技术负责人 +artifacts: + issueHandlingResult: issue_intake_result.json + codingResult: coding_result.json + reviewResult: review_result.json + issuePostResultPattern: issue_post_result_{purpose}.json + loopJudgeResult: loop_judge_result.json + loopHistory: loop_history.json +` + + if _, err := DecodeYAML([]byte(content)); err == nil { + t.Fatal("DecodeYAML() error = nil, want error for unknown field") + } +} + +// TestDecodeYAMLRejectsInvalidRoleRef 验证非法角色引用会被静态校验阻断。 +func TestDecodeYAMLRejectsInvalidRoleRef(t *testing.T) { + doc := BuildDefaultDocument() + doc.Workflow.Nodes[2].RoleRef = "randomAgent" + + if _, err := EncodeYAML(doc); err == nil { + t.Fatal("DecodeYAML() error = nil, want error for invalid roleRef") + } +} + +// TestDecodeYAMLRejectsMultipleEdgeConditions 验证单条边不能同时声明多个条件字段。 +func TestDecodeYAMLRejectsMultipleEdgeConditions(t *testing.T) { + doc := BuildDefaultDocument() + doc.Workflow.Edges[1].When.ReviewStatus = ReviewStatusPass + + if _, err := EncodeYAML(doc); err == nil { + t.Fatal("EncodeYAML() error = nil, want error for multiple edge conditions") + } +} + +// TestLoadFileAndSaveFile 验证标准 YAML 文件可稳定保存并重新读取。 +func TestLoadFileAndSaveFile(t *testing.T) { + filePath := filepath.Join(t.TempDir(), "orchestration.yaml") + + doc, err := DecodeYAML(mustEncodeDefaultSchemaYAML(t)) + if err != nil { + t.Fatalf("DecodeYAML() unexpected error: %v", err) + } + + if err := SaveFile(filePath, doc); err != nil { + t.Fatalf("SaveFile() unexpected error: %v", err) + } + + loadedDoc, err := LoadFile(filePath) + if err != nil { + t.Fatalf("LoadFile() unexpected error: %v", err) + } + + if loadedDoc.Metadata.Name != "default-issue-flow" { + t.Fatalf("Metadata.Name = %q, want %q", loadedDoc.Metadata.Name, "default-issue-flow") + } +} + +// TestEncodeYAMLRejectsInvalidDocument 验证非法文档在编码前会被阻断。 +func TestEncodeYAMLRejectsInvalidDocument(t *testing.T) { + doc, err := DecodeYAML(mustEncodeDefaultSchemaYAML(t)) + if err != nil { + t.Fatalf("DecodeYAML() unexpected error: %v", err) + } + + doc.Workflow.Nodes[0].Outputs = nil + + if _, err := EncodeYAML(doc); err == nil { + t.Fatal("EncodeYAML() error = nil, want error for invalid document") + } +} + +// TestValidateRejectsInvalidAPIVersion 验证非法 apiVersion 会被静态校验阻断。 +func TestValidateRejectsInvalidAPIVersion(t *testing.T) { + doc := BuildDefaultDocument() + doc.APIVersion = "codebee/v0" + + if err := Validate(doc); err == nil { + t.Fatal("Validate() error = nil, want error for invalid apiVersion") + } +} + +// TestValidateRejectsInvalidKind 验证非法 kind 会被静态校验阻断。 +func TestValidateRejectsInvalidKind(t *testing.T) { + doc := BuildDefaultDocument() + doc.Kind = "Workflow" + + if err := Validate(doc); err == nil { + t.Fatal("Validate() error = nil, want error for invalid kind") + } +} + +// TestValidateRejectsEmptyMetadataName 验证 metadata.name 不能为空。 +func TestValidateRejectsEmptyMetadataName(t *testing.T) { + doc := BuildDefaultDocument() + doc.Metadata.Name = "" + + if err := Validate(doc); err == nil { + t.Fatal("Validate() error = nil, want error for empty metadata.name") + } +} + +// TestValidateRejectsEmptyStartNodeID 验证 startNodeId 不能为空。 +func TestValidateRejectsEmptyStartNodeID(t *testing.T) { + doc := BuildDefaultDocument() + doc.Workflow.StartNodeID = "" + + if err := Validate(doc); err == nil { + t.Fatal("Validate() error = nil, want error for empty workflow.startNodeId") + } +} + +// TestValidateRejectsUnknownStartNodeID 验证 startNodeId 指向未知节点时会被阻断。 +func TestValidateRejectsUnknownStartNodeID(t *testing.T) { + doc := BuildDefaultDocument() + doc.Workflow.StartNodeID = "unknown" + + if err := Validate(doc); err == nil { + t.Fatal("Validate() error = nil, want error for unknown workflow.startNodeId") + } +} + +// TestValidateRejectsEmptyWorkflowNodes 验证节点列表不能为空。 +func TestValidateRejectsEmptyWorkflowNodes(t *testing.T) { + doc := BuildDefaultDocument() + doc.Workflow.Nodes = nil + + if err := Validate(doc); err == nil { + t.Fatal("Validate() error = nil, want error for empty workflow.nodes") + } +} + +// TestValidateRejectsDuplicateNodeID 验证重复节点 ID 会被阻断。 +func TestValidateRejectsDuplicateNodeID(t *testing.T) { + doc := BuildDefaultDocument() + doc.Workflow.Nodes[1].ID = doc.Workflow.Nodes[0].ID + + if err := Validate(doc); err == nil { + t.Fatal("Validate() error = nil, want error for duplicate node id") + } +} + +// TestValidateRejectsUnknownEdgeNode 验证边引用未知节点会被阻断。 +func TestValidateRejectsUnknownEdgeNode(t *testing.T) { + doc := BuildDefaultDocument() + doc.Workflow.Edges[0].To = "unknown" + + if err := Validate(doc); err == nil { + t.Fatal("Validate() error = nil, want error for unknown edge node") + } +} + +// TestValidateRejectsInvalidReviewStatus 验证非法状态枚举会被阻断。 +func TestValidateRejectsInvalidReviewStatus(t *testing.T) { + doc := BuildDefaultDocument() + doc.Workflow.Edges[3].When.ReviewStatus = "MAYBE" + + if err := Validate(doc); err == nil { + t.Fatal("Validate() error = nil, want error for invalid review status") + } +} + +// TestValidateRejectsUnknownUINodeReference 验证 UI 元数据引用未知节点会被阻断。 +func TestValidateRejectsUnknownUINodeReference(t *testing.T) { + doc := BuildDefaultDocument() + doc.UI.Nodes["ghost"] = NodeUI{X: 1, Y: 2} + + if err := Validate(doc); err == nil { + t.Fatal("Validate() error = nil, want error for unknown UI node reference") + } +} + +// mustEncodeDefaultSchemaYAML 将默认文档编码为标准 YAML,供 round-trip 测试复用。 +func mustEncodeDefaultSchemaYAML(t *testing.T) []byte { + t.Helper() + + content, err := EncodeYAML(BuildDefaultDocument()) + if err != nil { + t.Fatalf("EncodeYAML(BuildDefaultDocument()) unexpected error: %v", err) + } + + return content +} diff --git a/backend/internal/editor/schema/validate.go b/backend/internal/editor/schema/validate.go new file mode 100644 index 0000000..ef5194d --- /dev/null +++ b/backend/internal/editor/schema/validate.go @@ -0,0 +1,425 @@ +// Package schema 提供标准 YAML Schema 的静态校验逻辑。 +// +// 核心功能: +// 1. 在编辑器加载 YAML 时尽早发现结构问题、枚举值错误和引用关系错误 +// 2. 在编辑器保存 YAML 前阻断非法文档落盘,保证输出内容稳定、可预期 +// +// 开发维护: AI Assistant +// 创建时间: 2026-07-06 +// 更新时间: 2026-07-06 +package schema + +import ( + "fmt" + "strings" +) + +// Validate 校验一份标准 YAML Schema 文档的最小完整性。 +// +// 输入参数: +// - doc: 待校验的标准 YAML 文档对象 +// +// 返回值: +// - error: 当顶层字段、节点、边、枚举、引用关系任一不合法时返回详细错误 +// +// 核心实现逻辑: +// - 先校验顶层与 runtime、roles、artifacts 等稳定块 +// - 再校验 workflow 节点图,确保 startNodeId、节点唯一性和边引用完整 +// - 最后校验 UI 元数据,仅允许引用已存在节点 +// +// 调用注意事项: +// - Validate 只做静态结构校验,不执行任何运行时行为 +func Validate(doc *Document) error { + if doc == nil { + return fmt.Errorf("nil schema document") + } + + if doc.APIVersion != APIVersion { + return fmt.Errorf("invalid apiVersion %q", doc.APIVersion) + } + + if doc.Kind != Kind { + return fmt.Errorf("invalid kind %q", doc.Kind) + } + + if strings.TrimSpace(doc.Metadata.Name) == "" { + return fmt.Errorf("empty metadata.name") + } + + if err := validateRuntime(doc.Runtime); err != nil { + return err + } + + if err := validateRoles(doc.Roles); err != nil { + return err + } + + if err := validateArtifacts(doc.Artifacts); err != nil { + return err + } + + nodeIndex, err := validateWorkflow(doc.Workflow) + if err != nil { + return err + } + + if err := validateUI(doc.UI, nodeIndex); err != nil { + return err + } + + return nil +} + +// validateRuntime 校验 runtime 配置块。 +func validateRuntime(runtime Runtime) error { + // 这些阈值都直接影响编辑器输出的 YAML 是否仍处于可用区间,因此统一在这里收口。 + if runtime.MaxCodingReviewRounds <= 0 { + return fmt.Errorf("invalid runtime.maxCodingReviewRounds %d", runtime.MaxCodingReviewRounds) + } + + if runtime.MaxConsecutiveUnknownReview <= 0 { + return fmt.Errorf( + "invalid runtime.maxConsecutiveUnknownReview %d", + runtime.MaxConsecutiveUnknownReview, + ) + } + + if runtime.MaxIssuePostAttempts <= 0 { + return fmt.Errorf("invalid runtime.maxIssuePostAttempts %d", runtime.MaxIssuePostAttempts) + } + + if runtime.LoopJudgeStartRound < 0 { + return fmt.Errorf("invalid runtime.loopJudgeStartRound %d", runtime.LoopJudgeStartRound) + } + + return nil +} + +// validateRoles 校验 roles 配置块。 +func validateRoles(roles Roles) error { + if strings.TrimSpace(roles.DefaultAgent) == "" { + return fmt.Errorf("empty roles.defaultAgent") + } + + if strings.TrimSpace(roles.ReviewerAgent) == "" { + return fmt.Errorf("empty roles.reviewerAgent") + } + + if strings.TrimSpace(roles.IssuePostAgent) == "" { + return fmt.Errorf("empty roles.issuePostAgent") + } + + if strings.TrimSpace(roles.LoopJudgeAgent) == "" { + return fmt.Errorf("empty roles.loopJudgeAgent") + } + + return nil +} + +// validateArtifacts 校验 artifacts 配置块。 +func validateArtifacts(artifacts Artifacts) error { + if strings.TrimSpace(artifacts.IssueHandlingResult) == "" { + return fmt.Errorf("empty artifacts.issueHandlingResult") + } + + if strings.TrimSpace(artifacts.CodingResult) == "" { + return fmt.Errorf("empty artifacts.codingResult") + } + + if strings.TrimSpace(artifacts.ReviewResult) == "" { + return fmt.Errorf("empty artifacts.reviewResult") + } + + if strings.TrimSpace(artifacts.IssuePostResult) == "" { + return fmt.Errorf("empty artifacts.issuePostResultPattern") + } + + if strings.TrimSpace(artifacts.LoopJudgeResult) == "" { + return fmt.Errorf("empty artifacts.loopJudgeResult") + } + + if strings.TrimSpace(artifacts.LoopHistory) == "" { + return fmt.Errorf("empty artifacts.loopHistory") + } + + return nil +} + +// validateWorkflow 校验节点图主体,并返回节点索引用于后续边和 UI 校验。 +func validateWorkflow(workflow Workflow) (map[string]struct{}, error) { + if strings.TrimSpace(workflow.StartNodeID) == "" { + return nil, fmt.Errorf("empty workflow.startNodeId") + } + + if len(workflow.Nodes) == 0 { + return nil, fmt.Errorf("empty workflow.nodes") + } + + nodeIndex := make(map[string]struct{}, len(workflow.Nodes)) + for nodeIndexValue, node := range workflow.Nodes { + if err := validateNode(node, nodeIndexValue); err != nil { + return nil, err + } + + if _, exists := nodeIndex[node.ID]; exists { + return nil, fmt.Errorf("duplicate workflow node id %q", node.ID) + } + + nodeIndex[node.ID] = struct{}{} + } + + if _, exists := nodeIndex[workflow.StartNodeID]; !exists { + return nil, fmt.Errorf("unknown workflow.startNodeId %q", workflow.StartNodeID) + } + + for edgeIndex, edge := range workflow.Edges { + if err := validateEdge(edge, edgeIndex, nodeIndex); err != nil { + return nil, err + } + } + + return nodeIndex, nil +} + +// validateNode 校验单个节点的结构完整性。 +func validateNode(node Node, nodeIndex int) error { + if strings.TrimSpace(node.ID) == "" { + return fmt.Errorf("empty workflow.nodes[%d].id", nodeIndex) + } + + if strings.TrimSpace(node.Name) == "" { + return fmt.Errorf("empty workflow.nodes[%d].name", nodeIndex) + } + + if !isValidNodeType(node.Type) { + return fmt.Errorf("invalid workflow.nodes[%d].type %q", nodeIndex, node.Type) + } + + if !isValidRoleRef(node.RoleRef) { + return fmt.Errorf("invalid workflow.nodes[%d].roleRef %q", nodeIndex, node.RoleRef) + } + + if !isValidPromptTemplate(node.PromptTemplate) { + return fmt.Errorf( + "invalid workflow.nodes[%d].promptTemplate %q", + nodeIndex, + node.PromptTemplate, + ) + } + + if !isValidResultContract(node.ResultContract) { + return fmt.Errorf( + "invalid workflow.nodes[%d].resultContract %q", + nodeIndex, + node.ResultContract, + ) + } + + if err := validateArtifactsList(node.ID, "inputs", node.Inputs); err != nil { + return err + } + + if len(node.Outputs) == 0 { + return fmt.Errorf("empty workflow node %q outputs", node.ID) + } + + if err := validateArtifactsList(node.ID, "outputs", node.Outputs); err != nil { + return err + } + + return nil +} + +// validateArtifactsList 校验节点的输入或输出工件列表。 +func validateArtifactsList(nodeID string, fieldName string, artifacts []string) error { + for artifactIndex, artifact := range artifacts { + if strings.TrimSpace(artifact) == "" { + return fmt.Errorf( + "empty workflow node %q %s[%d]", + nodeID, + fieldName, + artifactIndex, + ) + } + } + + return nil +} + +// validateEdge 校验单条边及其条件块。 +func validateEdge(edge Edge, edgeIndex int, nodeIndex map[string]struct{}) error { + if strings.TrimSpace(edge.From) == "" { + return fmt.Errorf("empty workflow.edges[%d].from", edgeIndex) + } + + if strings.TrimSpace(edge.To) == "" { + return fmt.Errorf("empty workflow.edges[%d].to", edgeIndex) + } + + if _, exists := nodeIndex[edge.From]; !exists { + return fmt.Errorf("unknown workflow.edges[%d].from %q", edgeIndex, edge.From) + } + + if _, exists := nodeIndex[edge.To]; !exists { + return fmt.Errorf("unknown workflow.edges[%d].to %q", edgeIndex, edge.To) + } + + if err := validateEdgeCondition(edge.When, edgeIndex); err != nil { + return err + } + + return nil +} + +// validateEdgeCondition 校验受控边条件块。 +func validateEdgeCondition(condition EdgeCondition, edgeIndex int) error { + setCount := 0 + + if strings.TrimSpace(condition.IssueStatus) != "" { + setCount++ + if !isOneOf(condition.IssueStatus, IssueStatusReady, IssueStatusBlocked) { + return fmt.Errorf( + "invalid workflow.edges[%d].when.issueStatus %q", + edgeIndex, + condition.IssueStatus, + ) + } + } + + if strings.TrimSpace(condition.CodingStatus) != "" { + setCount++ + if !isOneOf(condition.CodingStatus, CodingStatusDone, CodingStatusInProgress, CodingStatusBlocked) { + return fmt.Errorf( + "invalid workflow.edges[%d].when.codingStatus %q", + edgeIndex, + condition.CodingStatus, + ) + } + } + + if strings.TrimSpace(condition.ReviewStatus) != "" { + setCount++ + if !isOneOf( + condition.ReviewStatus, + ReviewStatusPass, + ReviewStatusFail, + ReviewStatusUnknown, + ReviewStatusBlocked, + ) { + return fmt.Errorf( + "invalid workflow.edges[%d].when.reviewStatus %q", + edgeIndex, + condition.ReviewStatus, + ) + } + } + + if strings.TrimSpace(condition.IssuePostStatus) != "" { + setCount++ + if !isOneOf( + condition.IssuePostStatus, + IssuePostStatusPosted, + IssuePostStatusRejected, + IssuePostStatusBlocked, + ) { + return fmt.Errorf( + "invalid workflow.edges[%d].when.issuePostStatus %q", + edgeIndex, + condition.IssuePostStatus, + ) + } + } + + if strings.TrimSpace(condition.LoopJudgeDecision) != "" { + setCount++ + if !isOneOf( + condition.LoopJudgeDecision, + LoopJudgeDecisionContinue, + LoopJudgeDecisionShrinkTask, + LoopJudgeDecisionStopManual, + LoopJudgeDecisionStopBlocked, + ) { + return fmt.Errorf( + "invalid workflow.edges[%d].when.loopJudgeDecision %q", + edgeIndex, + condition.LoopJudgeDecision, + ) + } + } + + // 允许无条件边;但一旦写了条件,就必须保持“单条件字段”约束,避免出现多条件混写。 + if setCount > 1 { + return fmt.Errorf("workflow.edges[%d].when has multiple condition fields", edgeIndex) + } + + return nil +} + +// validateUI 校验 UI 元数据块,确保它只引用已存在节点。 +func validateUI(ui UISettings, nodeIndex map[string]struct{}) error { + for nodeID := range ui.Nodes { + if _, exists := nodeIndex[nodeID]; !exists { + return fmt.Errorf("ui.nodes references unknown node %q", nodeID) + } + } + + return nil +} + +// isValidNodeType 判断节点类型是否合法。 +func isValidNodeType(value string) bool { + return isOneOf( + value, + NodeTypeIssueHandling, + NodeTypeCoding, + NodeTypeReview, + NodeTypeIssuePost, + NodeTypeLoopJudge, + ) +} + +// isValidRoleRef 判断角色引用键是否合法。 +func isValidRoleRef(value string) bool { + return isOneOf( + value, + RoleRefDefaultAgent, + RoleRefReviewerAgent, + RoleRefIssuePostAgent, + RoleRefLoopJudgeAgent, + ) +} + +// isValidPromptTemplate 判断提示词模板标识是否合法。 +func isValidPromptTemplate(value string) bool { + return isOneOf( + value, + PromptTemplateIssueHandling, + PromptTemplateCoding, + PromptTemplateReview, + PromptTemplateIssuePost, + PromptTemplateLoopJudge, + ) +} + +// isValidResultContract 判断结果契约标识是否合法。 +func isValidResultContract(value string) bool { + return isOneOf( + value, + ResultContractIssueHandling, + ResultContractCoding, + ResultContractReview, + ResultContractIssuePost, + ResultContractLoopJudge, + ) +} + +// isOneOf 判断目标值是否命中受控枚举。 +func isOneOf(target string, allowed ...string) bool { + for _, candidate := range allowed { + if target == candidate { + return true + } + } + + return false +} diff --git a/backend/internal/editor/state/mapper.go b/backend/internal/editor/state/mapper.go new file mode 100644 index 0000000..cdb119e --- /dev/null +++ b/backend/internal/editor/state/mapper.go @@ -0,0 +1,217 @@ +// Package state 提供 schema 与 editor state 之间的双向映射逻辑。 +// +// 核心功能: +// 1. 将标准 YAML Schema 映射为画布和属性面板更容易直接消费的内存状态 +// 2. 将编辑器状态重新组装为标准 YAML Schema,供保存和后续模块复用 +// +// 开发维护: AI Assistant +// 创建时间: 2026-07-06 +// 更新时间: 2026-07-06 +package state + +import ( + "fmt" + + "github.com/JiGuangWorker/code-bee/internal/editor/schema" +) + +// FromSchema 将标准 YAML Schema 文档映射为编辑器状态。 +// +// 输入参数: +// - doc: 已通过 schema.Validate 的标准文档对象 +// +// 返回值: +// - *State: 可直接供编辑器画布和属性面板消费的最小状态对象 +// - error: 当输入文档为空或 schema 校验失败时返回错误 +// +// 核心实现逻辑: +// - 先复用 schema.Validate 兜底结构合法性 +// - 将 workflow 节点和 ui 节点位置合并到单个 NodeState 中 +// - 边、runtime、roles、artifacts 保持一一映射,不新增额外运行语义 +// +// 调用注意事项: +// - 若 schema 文档未提供某个节点的 UI 坐标,则默认使用零值位置 +func FromSchema(doc *schema.Document) (*State, error) { + if err := schema.Validate(doc); err != nil { + return nil, fmt.Errorf("map schema to editor state: %w", err) + } + + state := &State{ + APIVersion: doc.APIVersion, + Kind: doc.Kind, + Metadata: MetadataState{ + Name: doc.Metadata.Name, + Description: doc.Metadata.Description, + }, + StartNodeID: doc.Workflow.StartNodeID, + Nodes: make([]NodeState, 0, len(doc.Workflow.Nodes)), + Edges: make([]EdgeState, 0, len(doc.Workflow.Edges)), + Runtime: doc.Runtime, + Roles: doc.Roles, + Artifacts: doc.Artifacts, + } + + for _, node := range doc.Workflow.Nodes { + position := NodePosition{} + if nodeUI, exists := doc.UI.Nodes[node.ID]; exists { + position = NodePosition{ + X: nodeUI.X, + Y: nodeUI.Y, + } + } + + state.Nodes = append(state.Nodes, NodeState{ + ID: node.ID, + Type: node.Type, + Name: node.Name, + RoleRef: node.RoleRef, + PromptTemplate: node.PromptTemplate, + ResultContract: node.ResultContract, + Inputs: cloneStrings(node.Inputs), + Outputs: cloneStrings(node.Outputs), + Position: position, + }) + } + + for _, edge := range doc.Workflow.Edges { + state.Edges = append(state.Edges, EdgeState{ + From: edge.From, + To: edge.To, + Condition: edge.When, + }) + } + + return state, nil +} + +// ToSchema 将编辑器状态重新组装为标准 YAML Schema 文档。 +// +// 输入参数: +// - editorState: 编辑器侧的最小状态对象 +// +// 返回值: +// - *schema.Document: 可直接用于 schema.Validate、EncodeYAML、SaveFile 的标准文档 +// - error: 当状态为空、节点定义非法或组装后 schema 校验失败时返回错误 +// +// 核心实现逻辑: +// - 先校验状态的最小完整性,尽早阻断明显错误 +// - 将 NodeState 中的 Position 拆回 schema.UI,并把节点语义部分拆回 workflow.nodes +// - 最终复用 schema.Validate,保证输出一定仍符合标准 YAML 契约 +// +// 调用注意事项: +// - ToSchema 不会对节点和边重新排序,保持调用方当前状态顺序 +func ToSchema(editorState *State) (*schema.Document, error) { + if err := validateState(editorState); err != nil { + return nil, fmt.Errorf("map editor state to schema: %w", err) + } + + doc := &schema.Document{ + APIVersion: editorState.APIVersion, + Kind: editorState.Kind, + Metadata: schema.Metadata{ + Name: editorState.Metadata.Name, + Description: editorState.Metadata.Description, + }, + Workflow: schema.Workflow{ + StartNodeID: editorState.StartNodeID, + Nodes: make([]schema.Node, 0, len(editorState.Nodes)), + Edges: make([]schema.Edge, 0, len(editorState.Edges)), + }, + Runtime: editorState.Runtime, + Roles: editorState.Roles, + Artifacts: editorState.Artifacts, + UI: schema.UISettings{ + Nodes: make(map[string]schema.NodeUI, len(editorState.Nodes)), + }, + } + + for _, node := range editorState.Nodes { + doc.Workflow.Nodes = append(doc.Workflow.Nodes, schema.Node{ + ID: node.ID, + Type: node.Type, + Name: node.Name, + RoleRef: node.RoleRef, + PromptTemplate: node.PromptTemplate, + ResultContract: node.ResultContract, + Inputs: cloneStrings(node.Inputs), + Outputs: cloneStrings(node.Outputs), + }) + + doc.UI.Nodes[node.ID] = schema.NodeUI{ + X: node.Position.X, + Y: node.Position.Y, + } + } + + for _, edge := range editorState.Edges { + doc.Workflow.Edges = append(doc.Workflow.Edges, schema.Edge{ + From: edge.From, + To: edge.To, + When: edge.Condition, + }) + } + + if err := schema.Validate(doc); err != nil { + return nil, fmt.Errorf("map editor state to schema: %w", err) + } + + return doc, nil +} + +// validateState 校验 editor state 的最小完整性。 +func validateState(editorState *State) error { + if editorState == nil { + return fmt.Errorf("nil editor state") + } + + if editorState.APIVersion == "" { + return fmt.Errorf("empty state apiVersion") + } + + if editorState.Kind == "" { + return fmt.Errorf("empty state kind") + } + + if editorState.Metadata.Name == "" { + return fmt.Errorf("empty state metadata.name") + } + + if editorState.StartNodeID == "" { + return fmt.Errorf("empty state startNodeId") + } + + if len(editorState.Nodes) == 0 { + return fmt.Errorf("empty state nodes") + } + + nodeIndex := make(map[string]struct{}, len(editorState.Nodes)) + for nodeIndexValue, node := range editorState.Nodes { + if node.ID == "" { + return fmt.Errorf("empty state nodes[%d].id", nodeIndexValue) + } + + if _, exists := nodeIndex[node.ID]; exists { + return fmt.Errorf("duplicate state node id %q", node.ID) + } + + nodeIndex[node.ID] = struct{}{} + } + + if _, exists := nodeIndex[editorState.StartNodeID]; !exists { + return fmt.Errorf("unknown state startNodeId %q", editorState.StartNodeID) + } + + return nil +} + +// cloneStrings 复制字符串切片,避免上游状态和下游 schema 共享底层数组。 +func cloneStrings(values []string) []string { + if len(values) == 0 { + return nil + } + + clonedValues := make([]string, len(values)) + copy(clonedValues, values) + + return clonedValues +} diff --git a/backend/internal/editor/state/mapper_test.go b/backend/internal/editor/state/mapper_test.go new file mode 100644 index 0000000..627d574 --- /dev/null +++ b/backend/internal/editor/state/mapper_test.go @@ -0,0 +1,162 @@ +// Package state 覆盖 schema 与 editor state 的双向映射逻辑。 +// +// 核心功能: +// 1. 验证标准 schema 能稳定映射为编辑器状态,并可重新组装回合法 schema +// 2. 验证 UI 坐标等展示元数据与节点语义边界清晰,避免互相污染 +// +// 开发维护: AI Assistant +// 创建时间: 2026-07-06 +// 更新时间: 2026-07-06 +package state + +import ( + "testing" + + "github.com/JiGuangWorker/code-bee/internal/editor/schema" +) + +// TestFromSchemaAndToSchemaRoundTrip 验证默认 schema 能稳定完成 state 双向 round-trip。 +func TestFromSchemaAndToSchemaRoundTrip(t *testing.T) { + sourceDocument := schema.BuildDefaultDocument() + + editorState, err := FromSchema(sourceDocument) + if err != nil { + t.Fatalf("FromSchema() unexpected error: %v", err) + } + + rebuiltDocument, err := ToSchema(editorState) + if err != nil { + t.Fatalf("ToSchema() unexpected error: %v", err) + } + + if err := schema.Validate(rebuiltDocument); err != nil { + t.Fatalf("schema.Validate(ToSchema(FromSchema(...))) unexpected error: %v", err) + } + + if rebuiltDocument.Workflow.StartNodeID != sourceDocument.Workflow.StartNodeID { + t.Fatalf( + "Workflow.StartNodeID = %q, want %q", + rebuiltDocument.Workflow.StartNodeID, + sourceDocument.Workflow.StartNodeID, + ) + } + + if len(rebuiltDocument.Workflow.Nodes) != len(sourceDocument.Workflow.Nodes) { + t.Fatalf( + "len(Workflow.Nodes) = %d, want %d", + len(rebuiltDocument.Workflow.Nodes), + len(sourceDocument.Workflow.Nodes), + ) + } + + if len(rebuiltDocument.Workflow.Edges) != len(sourceDocument.Workflow.Edges) { + t.Fatalf( + "len(Workflow.Edges) = %d, want %d", + len(rebuiltDocument.Workflow.Edges), + len(sourceDocument.Workflow.Edges), + ) + } +} + +// TestFromSchemaMergesNodeUIIntoPosition 验证 schema.UI 中的坐标会合并进节点状态。 +func TestFromSchemaMergesNodeUIIntoPosition(t *testing.T) { + sourceDocument := schema.BuildDefaultDocument() + + editorState, err := FromSchema(sourceDocument) + if err != nil { + t.Fatalf("FromSchema() unexpected error: %v", err) + } + + for _, node := range editorState.Nodes { + if node.ID == schema.DefaultCodingNodeID { + if node.Position.X != 620 || node.Position.Y != 120 { + t.Fatalf("coding_main position = (%d,%d), want (620,120)", node.Position.X, node.Position.Y) + } + return + } + } + + t.Fatal("coding_main node not found in editor state") +} + +// TestToSchemaWritesNodePositionBackToUI 验证节点位置修改后只回写到 schema.UI。 +func TestToSchemaWritesNodePositionBackToUI(t *testing.T) { + editorState, err := FromSchema(schema.BuildDefaultDocument()) + if err != nil { + t.Fatalf("FromSchema() unexpected error: %v", err) + } + + for nodeIndex := range editorState.Nodes { + if editorState.Nodes[nodeIndex].ID == schema.DefaultReviewNodeID { + editorState.Nodes[nodeIndex].Position = NodePosition{X: 1000, Y: 480} + break + } + } + + rebuiltDocument, err := ToSchema(editorState) + if err != nil { + t.Fatalf("ToSchema() unexpected error: %v", err) + } + + reviewUI, exists := rebuiltDocument.UI.Nodes[schema.DefaultReviewNodeID] + if !exists { + t.Fatalf("UI.Nodes[%q] not found", schema.DefaultReviewNodeID) + } + + if reviewUI.X != 1000 || reviewUI.Y != 480 { + t.Fatalf("UI position = (%d,%d), want (1000,480)", reviewUI.X, reviewUI.Y) + } + + for _, node := range rebuiltDocument.Workflow.Nodes { + if node.ID == schema.DefaultReviewNodeID { + if node.Type != schema.NodeTypeReview { + t.Fatalf("review node type = %q, want %q", node.Type, schema.NodeTypeReview) + } + return + } + } + + t.Fatal("review_main node not found in rebuilt schema") +} + +// TestToSchemaRejectsUnknownStartNodeID 验证非法 startNodeId 会在状态层被阻断。 +func TestToSchemaRejectsUnknownStartNodeID(t *testing.T) { + editorState, err := FromSchema(schema.BuildDefaultDocument()) + if err != nil { + t.Fatalf("FromSchema() unexpected error: %v", err) + } + + editorState.StartNodeID = "ghost" + + if _, err := ToSchema(editorState); err == nil { + t.Fatal("ToSchema() error = nil, want error for unknown start node") + } +} + +// TestToSchemaRejectsDuplicateNodeID 验证重复节点 ID 会在状态层被阻断。 +func TestToSchemaRejectsDuplicateNodeID(t *testing.T) { + editorState, err := FromSchema(schema.BuildDefaultDocument()) + if err != nil { + t.Fatalf("FromSchema() unexpected error: %v", err) + } + + editorState.Nodes[1].ID = editorState.Nodes[0].ID + + if _, err := ToSchema(editorState); err == nil { + t.Fatal("ToSchema() error = nil, want error for duplicate node id") + } +} + +// TestToSchemaRejectsInvalidNodeSemanticField 验证节点语义字段仍会通过 schema.Validate 被阻断。 +func TestToSchemaRejectsInvalidNodeSemanticField(t *testing.T) { + editorState, err := FromSchema(schema.BuildDefaultDocument()) + if err != nil { + t.Fatalf("FromSchema() unexpected error: %v", err) + } + + editorState.Nodes[0].RoleRef = "randomAgent" + + if _, err := ToSchema(editorState); err == nil { + t.Fatal("ToSchema() error = nil, want error for invalid node semantic field") + } +} diff --git a/backend/internal/editor/state/state.go b/backend/internal/editor/state/state.go new file mode 100644 index 0000000..b8a9185 --- /dev/null +++ b/backend/internal/editor/state/state.go @@ -0,0 +1,74 @@ +// Package state 定义编排编辑器使用的内存状态模型。 +// +// 核心功能: +// 1. 为编辑器提供比标准 YAML Schema 更直接的内存结构,便于画布、属性面板和保存操作消费 +// 2. 在不引入运行时语义扩张的前提下,将 UI 坐标等展示元数据合并到节点状态中,降低上层调用复杂度 +// +// 开发维护: AI Assistant +// 创建时间: 2026-07-06 +// 更新时间: 2026-07-06 +package state + +import "github.com/JiGuangWorker/code-bee/internal/editor/schema" + +// State 表示编排编辑器在 Go 侧使用的最小状态结构。 +// +// 输入参数: +// - 无;该结构由 FromSchema 生成,或由调用方在内存中编辑后交给 ToSchema 重新组装 +// +// 返回值: +// - 无;作为编辑器状态容器承载元信息、节点、边、运行参数、角色与工件命名 +// +// 核心实现逻辑: +// - 将 schema.Document 中分散在 workflow 与 ui 的信息拍平成更易编辑的结构 +// - 节点列表直接携带位置坐标,避免上层每次都去 workflow / ui 两处拼装 +// +// 调用注意事项: +// - 该结构只服务编辑器状态转换,不应用于现有调度主流程 +type State struct { + APIVersion string + Kind string + Metadata MetadataState + StartNodeID string + Nodes []NodeState + Edges []EdgeState + Runtime schema.Runtime + Roles schema.Roles + Artifacts schema.Artifacts +} + +// MetadataState 表示编辑器状态中的元信息块。 +type MetadataState struct { + Name string + Description string +} + +// NodeState 表示编辑器中的单个节点状态。 +// +// 设计说明: +// - 运行语义字段继续保持与 schema.Node 对齐 +// - Position 将原本独立放在 schema.UI 中的坐标信息收口到节点级,便于画布层直接消费 +type NodeState struct { + ID string + Type string + Name string + RoleRef string + PromptTemplate string + ResultContract string + Inputs []string + Outputs []string + Position NodePosition +} + +// NodePosition 表示节点在编辑器画布中的位置。 +type NodePosition struct { + X int + Y int +} + +// EdgeState 表示编辑器中的单条边状态。 +type EdgeState struct { + From string + To string + Condition schema.EdgeCondition +} diff --git a/internal/parser/doc.go b/backend/internal/parser/doc.go similarity index 100% rename from internal/parser/doc.go rename to backend/internal/parser/doc.go diff --git a/internal/pipeline/artifacts.go b/backend/internal/pipeline/artifacts.go similarity index 100% rename from internal/pipeline/artifacts.go rename to backend/internal/pipeline/artifacts.go diff --git a/internal/pipeline/contracts.go b/backend/internal/pipeline/contracts.go similarity index 100% rename from internal/pipeline/contracts.go rename to backend/internal/pipeline/contracts.go diff --git a/internal/pipeline/coverage_test.go b/backend/internal/pipeline/coverage_test.go similarity index 100% rename from internal/pipeline/coverage_test.go rename to backend/internal/pipeline/coverage_test.go diff --git a/internal/pipeline/dispatch.go b/backend/internal/pipeline/dispatch.go similarity index 100% rename from internal/pipeline/dispatch.go rename to backend/internal/pipeline/dispatch.go diff --git a/internal/pipeline/dispatch_test.go b/backend/internal/pipeline/dispatch_test.go similarity index 100% rename from internal/pipeline/dispatch_test.go rename to backend/internal/pipeline/dispatch_test.go diff --git a/internal/pipeline/history.go b/backend/internal/pipeline/history.go similarity index 100% rename from internal/pipeline/history.go rename to backend/internal/pipeline/history.go diff --git a/internal/pipeline/loop_judge_test.go b/backend/internal/pipeline/loop_judge_test.go similarity index 100% rename from internal/pipeline/loop_judge_test.go rename to backend/internal/pipeline/loop_judge_test.go diff --git a/internal/pipeline/runtime_adapter.go b/backend/internal/pipeline/runtime_adapter.go similarity index 100% rename from internal/pipeline/runtime_adapter.go rename to backend/internal/pipeline/runtime_adapter.go diff --git a/internal/pipeline/runtime_adapter_test.go b/backend/internal/pipeline/runtime_adapter_test.go similarity index 100% rename from internal/pipeline/runtime_adapter_test.go rename to backend/internal/pipeline/runtime_adapter_test.go diff --git a/internal/platform/client.go b/backend/internal/platform/client.go similarity index 100% rename from internal/platform/client.go rename to backend/internal/platform/client.go diff --git a/internal/platform/github/client.go b/backend/internal/platform/github/client.go similarity index 100% rename from internal/platform/github/client.go rename to backend/internal/platform/github/client.go diff --git a/internal/platform/github/client_test.go b/backend/internal/platform/github/client_test.go similarity index 100% rename from internal/platform/github/client_test.go rename to backend/internal/platform/github/client_test.go diff --git a/internal/runtime/agent_tool.go b/backend/internal/runtime/agent_tool.go similarity index 100% rename from internal/runtime/agent_tool.go rename to backend/internal/runtime/agent_tool.go diff --git a/internal/runtime/command_tool.go b/backend/internal/runtime/command_tool.go similarity index 100% rename from internal/runtime/command_tool.go rename to backend/internal/runtime/command_tool.go diff --git a/internal/runtime/context.go b/backend/internal/runtime/context.go similarity index 100% rename from internal/runtime/context.go rename to backend/internal/runtime/context.go diff --git a/internal/runtime/context_test.go b/backend/internal/runtime/context_test.go similarity index 100% rename from internal/runtime/context_test.go rename to backend/internal/runtime/context_test.go diff --git a/internal/runtime/default_workflow.go b/backend/internal/runtime/default_workflow.go similarity index 100% rename from internal/runtime/default_workflow.go rename to backend/internal/runtime/default_workflow.go diff --git a/internal/runtime/default_workflow.yaml b/backend/internal/runtime/default_workflow.yaml similarity index 100% rename from internal/runtime/default_workflow.yaml rename to backend/internal/runtime/default_workflow.yaml diff --git a/internal/runtime/doc.go b/backend/internal/runtime/doc.go similarity index 100% rename from internal/runtime/doc.go rename to backend/internal/runtime/doc.go diff --git a/internal/runtime/engine.go b/backend/internal/runtime/engine.go similarity index 100% rename from internal/runtime/engine.go rename to backend/internal/runtime/engine.go diff --git a/internal/runtime/engine_test.go b/backend/internal/runtime/engine_test.go similarity index 100% rename from internal/runtime/engine_test.go rename to backend/internal/runtime/engine_test.go diff --git a/internal/runtime/executor.go b/backend/internal/runtime/executor.go similarity index 100% rename from internal/runtime/executor.go rename to backend/internal/runtime/executor.go diff --git a/internal/runtime/expression.go b/backend/internal/runtime/expression.go similarity index 100% rename from internal/runtime/expression.go rename to backend/internal/runtime/expression.go diff --git a/internal/runtime/expression_test.go b/backend/internal/runtime/expression_test.go similarity index 100% rename from internal/runtime/expression_test.go rename to backend/internal/runtime/expression_test.go diff --git a/internal/runtime/function_tool.go b/backend/internal/runtime/function_tool.go similarity index 100% rename from internal/runtime/function_tool.go rename to backend/internal/runtime/function_tool.go diff --git a/internal/runtime/loop_executor.go b/backend/internal/runtime/loop_executor.go similarity index 100% rename from internal/runtime/loop_executor.go rename to backend/internal/runtime/loop_executor.go diff --git a/internal/runtime/loop_executor_test.go b/backend/internal/runtime/loop_executor_test.go similarity index 100% rename from internal/runtime/loop_executor_test.go rename to backend/internal/runtime/loop_executor_test.go diff --git a/internal/runtime/parallel_executor.go b/backend/internal/runtime/parallel_executor.go similarity index 100% rename from internal/runtime/parallel_executor.go rename to backend/internal/runtime/parallel_executor.go diff --git a/internal/runtime/parallel_executor_test.go b/backend/internal/runtime/parallel_executor_test.go similarity index 100% rename from internal/runtime/parallel_executor_test.go rename to backend/internal/runtime/parallel_executor_test.go diff --git a/internal/runtime/prompt_builder.go b/backend/internal/runtime/prompt_builder.go similarity index 100% rename from internal/runtime/prompt_builder.go rename to backend/internal/runtime/prompt_builder.go diff --git a/internal/runtime/prompt_builder_test.go b/backend/internal/runtime/prompt_builder_test.go similarity index 100% rename from internal/runtime/prompt_builder_test.go rename to backend/internal/runtime/prompt_builder_test.go diff --git a/internal/runtime/prompts/coding.md b/backend/internal/runtime/prompts/coding.md similarity index 100% rename from internal/runtime/prompts/coding.md rename to backend/internal/runtime/prompts/coding.md diff --git a/internal/runtime/prompts/issue-handling.md b/backend/internal/runtime/prompts/issue-handling.md similarity index 100% rename from internal/runtime/prompts/issue-handling.md rename to backend/internal/runtime/prompts/issue-handling.md diff --git a/internal/runtime/prompts/issue-post.md b/backend/internal/runtime/prompts/issue-post.md similarity index 100% rename from internal/runtime/prompts/issue-post.md rename to backend/internal/runtime/prompts/issue-post.md diff --git a/internal/runtime/prompts/loop-judge.md b/backend/internal/runtime/prompts/loop-judge.md similarity index 100% rename from internal/runtime/prompts/loop-judge.md rename to backend/internal/runtime/prompts/loop-judge.md diff --git a/internal/runtime/prompts/review.md b/backend/internal/runtime/prompts/review.md similarity index 100% rename from internal/runtime/prompts/review.md rename to backend/internal/runtime/prompts/review.md diff --git a/internal/runtime/stage_executor.go b/backend/internal/runtime/stage_executor.go similarity index 100% rename from internal/runtime/stage_executor.go rename to backend/internal/runtime/stage_executor.go diff --git a/internal/runtime/stage_executor_test.go b/backend/internal/runtime/stage_executor_test.go similarity index 100% rename from internal/runtime/stage_executor_test.go rename to backend/internal/runtime/stage_executor_test.go diff --git a/internal/runtime/tool.go b/backend/internal/runtime/tool.go similarity index 100% rename from internal/runtime/tool.go rename to backend/internal/runtime/tool.go diff --git a/internal/runtime/tool_registry.go b/backend/internal/runtime/tool_registry.go similarity index 100% rename from internal/runtime/tool_registry.go rename to backend/internal/runtime/tool_registry.go diff --git a/internal/runtime/tool_test.go b/backend/internal/runtime/tool_test.go similarity index 100% rename from internal/runtime/tool_test.go rename to backend/internal/runtime/tool_test.go diff --git a/internal/schema/errors.go b/backend/internal/schema/errors.go similarity index 100% rename from internal/schema/errors.go rename to backend/internal/schema/errors.go diff --git a/internal/schema/loader.go b/backend/internal/schema/loader.go similarity index 100% rename from internal/schema/loader.go rename to backend/internal/schema/loader.go diff --git a/internal/schema/loader_test.go b/backend/internal/schema/loader_test.go similarity index 100% rename from internal/schema/loader_test.go rename to backend/internal/schema/loader_test.go diff --git a/internal/schema/schemas/v1/README.md b/backend/internal/schema/schemas/v1/README.md similarity index 100% rename from internal/schema/schemas/v1/README.md rename to backend/internal/schema/schemas/v1/README.md diff --git a/internal/schema/schemas/v1/condition.yaml b/backend/internal/schema/schemas/v1/condition.yaml similarity index 100% rename from internal/schema/schemas/v1/condition.yaml rename to backend/internal/schema/schemas/v1/condition.yaml diff --git a/internal/schema/schemas/v1/exit_condition.yaml b/backend/internal/schema/schemas/v1/exit_condition.yaml similarity index 100% rename from internal/schema/schemas/v1/exit_condition.yaml rename to backend/internal/schema/schemas/v1/exit_condition.yaml diff --git a/internal/schema/schemas/v1/loop.yaml b/backend/internal/schema/schemas/v1/loop.yaml similarity index 100% rename from internal/schema/schemas/v1/loop.yaml rename to backend/internal/schema/schemas/v1/loop.yaml diff --git a/internal/schema/schemas/v1/loop_judge.yaml b/backend/internal/schema/schemas/v1/loop_judge.yaml similarity index 100% rename from internal/schema/schemas/v1/loop_judge.yaml rename to backend/internal/schema/schemas/v1/loop_judge.yaml diff --git a/internal/schema/schemas/v1/pipeline_step.yaml b/backend/internal/schema/schemas/v1/pipeline_step.yaml similarity index 100% rename from internal/schema/schemas/v1/pipeline_step.yaml rename to backend/internal/schema/schemas/v1/pipeline_step.yaml diff --git a/internal/schema/schemas/v1/result_contract.yaml b/backend/internal/schema/schemas/v1/result_contract.yaml similarity index 100% rename from internal/schema/schemas/v1/result_contract.yaml rename to backend/internal/schema/schemas/v1/result_contract.yaml diff --git a/internal/schema/schemas/v1/stage.yaml b/backend/internal/schema/schemas/v1/stage.yaml similarity index 100% rename from internal/schema/schemas/v1/stage.yaml rename to backend/internal/schema/schemas/v1/stage.yaml diff --git a/internal/schema/schemas/v1/tool.yaml b/backend/internal/schema/schemas/v1/tool.yaml similarity index 100% rename from internal/schema/schemas/v1/tool.yaml rename to backend/internal/schema/schemas/v1/tool.yaml diff --git a/internal/schema/schemas/v1/workflow.yaml b/backend/internal/schema/schemas/v1/workflow.yaml similarity index 100% rename from internal/schema/schemas/v1/workflow.yaml rename to backend/internal/schema/schemas/v1/workflow.yaml diff --git a/internal/schema/testdata/invalid/agent_missing_skill.yaml b/backend/internal/schema/testdata/invalid/agent_missing_skill.yaml similarity index 100% rename from internal/schema/testdata/invalid/agent_missing_skill.yaml rename to backend/internal/schema/testdata/invalid/agent_missing_skill.yaml diff --git a/internal/schema/testdata/invalid/bad_tool_name.yaml b/backend/internal/schema/testdata/invalid/bad_tool_name.yaml similarity index 100% rename from internal/schema/testdata/invalid/bad_tool_name.yaml rename to backend/internal/schema/testdata/invalid/bad_tool_name.yaml diff --git a/internal/schema/testdata/invalid/command_missing_run.yaml b/backend/internal/schema/testdata/invalid/command_missing_run.yaml similarity index 100% rename from internal/schema/testdata/invalid/command_missing_run.yaml rename to backend/internal/schema/testdata/invalid/command_missing_run.yaml diff --git a/internal/schema/testdata/invalid/empty_aliases.yaml b/backend/internal/schema/testdata/invalid/empty_aliases.yaml similarity index 100% rename from internal/schema/testdata/invalid/empty_aliases.yaml rename to backend/internal/schema/testdata/invalid/empty_aliases.yaml diff --git a/internal/schema/testdata/invalid/empty_config.yaml b/backend/internal/schema/testdata/invalid/empty_config.yaml similarity index 100% rename from internal/schema/testdata/invalid/empty_config.yaml rename to backend/internal/schema/testdata/invalid/empty_config.yaml diff --git a/internal/schema/testdata/invalid/exit_condition_bad_operator.yaml b/backend/internal/schema/testdata/invalid/exit_condition_bad_operator.yaml similarity index 100% rename from internal/schema/testdata/invalid/exit_condition_bad_operator.yaml rename to backend/internal/schema/testdata/invalid/exit_condition_bad_operator.yaml diff --git a/internal/schema/testdata/invalid/loop_max_iterations.yaml b/backend/internal/schema/testdata/invalid/loop_max_iterations.yaml similarity index 100% rename from internal/schema/testdata/invalid/loop_max_iterations.yaml rename to backend/internal/schema/testdata/invalid/loop_max_iterations.yaml diff --git a/internal/schema/testdata/invalid/loop_missing_body.yaml b/backend/internal/schema/testdata/invalid/loop_missing_body.yaml similarity index 100% rename from internal/schema/testdata/invalid/loop_missing_body.yaml rename to backend/internal/schema/testdata/invalid/loop_missing_body.yaml diff --git a/internal/schema/testdata/invalid/missing_required_fields.yaml b/backend/internal/schema/testdata/invalid/missing_required_fields.yaml similarity index 100% rename from internal/schema/testdata/invalid/missing_required_fields.yaml rename to backend/internal/schema/testdata/invalid/missing_required_fields.yaml diff --git a/internal/schema/testdata/invalid/parallel_single_step.yaml b/backend/internal/schema/testdata/invalid/parallel_single_step.yaml similarity index 100% rename from internal/schema/testdata/invalid/parallel_single_step.yaml rename to backend/internal/schema/testdata/invalid/parallel_single_step.yaml diff --git a/internal/schema/testdata/invalid/pipeline_step_unknown_key.yaml b/backend/internal/schema/testdata/invalid/pipeline_step_unknown_key.yaml similarity index 100% rename from internal/schema/testdata/invalid/pipeline_step_unknown_key.yaml rename to backend/internal/schema/testdata/invalid/pipeline_step_unknown_key.yaml diff --git a/internal/schema/testdata/invalid/wrong_version.yaml b/backend/internal/schema/testdata/invalid/wrong_version.yaml similarity index 100% rename from internal/schema/testdata/invalid/wrong_version.yaml rename to backend/internal/schema/testdata/invalid/wrong_version.yaml diff --git a/internal/schema/testdata/valid/command_tool.yaml b/backend/internal/schema/testdata/valid/command_tool.yaml similarity index 100% rename from internal/schema/testdata/valid/command_tool.yaml rename to backend/internal/schema/testdata/valid/command_tool.yaml diff --git a/internal/schema/testdata/valid/function_tool.yaml b/backend/internal/schema/testdata/valid/function_tool.yaml similarity index 100% rename from internal/schema/testdata/valid/function_tool.yaml rename to backend/internal/schema/testdata/valid/function_tool.yaml diff --git a/internal/schema/testdata/valid/loop_with_judge.yaml b/backend/internal/schema/testdata/valid/loop_with_judge.yaml similarity index 100% rename from internal/schema/testdata/valid/loop_with_judge.yaml rename to backend/internal/schema/testdata/valid/loop_with_judge.yaml diff --git a/internal/schema/testdata/valid/minimal.yaml b/backend/internal/schema/testdata/valid/minimal.yaml similarity index 100% rename from internal/schema/testdata/valid/minimal.yaml rename to backend/internal/schema/testdata/valid/minimal.yaml diff --git a/internal/schema/testdata/valid/nested_loop.yaml b/backend/internal/schema/testdata/valid/nested_loop.yaml similarity index 100% rename from internal/schema/testdata/valid/nested_loop.yaml rename to backend/internal/schema/testdata/valid/nested_loop.yaml diff --git a/internal/schema/testdata/valid/parallel_pipeline.yaml b/backend/internal/schema/testdata/valid/parallel_pipeline.yaml similarity index 100% rename from internal/schema/testdata/valid/parallel_pipeline.yaml rename to backend/internal/schema/testdata/valid/parallel_pipeline.yaml diff --git a/internal/schema/testdata/valid/tool_with_alias.yaml b/backend/internal/schema/testdata/valid/tool_with_alias.yaml similarity index 100% rename from internal/schema/testdata/valid/tool_with_alias.yaml rename to backend/internal/schema/testdata/valid/tool_with_alias.yaml diff --git a/internal/schema/types.go b/backend/internal/schema/types.go similarity index 100% rename from internal/schema/types.go rename to backend/internal/schema/types.go diff --git a/internal/schema/validator.go b/backend/internal/schema/validator.go similarity index 100% rename from internal/schema/validator.go rename to backend/internal/schema/validator.go diff --git a/internal/schema/validator_test.go b/backend/internal/schema/validator_test.go similarity index 100% rename from internal/schema/validator_test.go rename to backend/internal/schema/validator_test.go diff --git a/pkg/version/version.go b/backend/pkg/version/version.go similarity index 100% rename from pkg/version/version.go rename to backend/pkg/version/version.go diff --git a/pkg/version/version_test.go b/backend/pkg/version/version_test.go similarity index 100% rename from pkg/version/version_test.go rename to backend/pkg/version/version_test.go diff --git a/deploy/README.md b/deploy/README.md new file mode 100644 index 0000000..be751c9 --- /dev/null +++ b/deploy/README.md @@ -0,0 +1,11 @@ +# deploy + +该目录用于承载自动化部署相关资产。 + +当前状态: +- 目录已预留并纳入仓库级结构校验 +- 尚未接入具体 Dockerfile、Kubernetes 清单或其他发布脚本 + +约束说明: +- 所有部署相关文件统一收口到 `deploy/` +- CI / 发布流水线中的部署动作应当引用本目录资产,而不是散落在其他目录中 diff --git a/docs/design/orchestration-yaml-schema-from-source.md b/docs/design/orchestration-yaml-schema-from-source.md new file mode 100644 index 0000000..96e4e9e --- /dev/null +++ b/docs/design/orchestration-yaml-schema-from-source.md @@ -0,0 +1,724 @@ +# CodeB 编排 YAML Schema(源码映射稿) + +## 1. 目的 + +本文档不是重新发明一套运行时协议,而是把当前 `code-bee` 已经写死在源码里的编排事实,整理成一份可供编辑器解析、展示、编辑、再组装回 YAML 的标准结构。 + +这份 Schema 的定位只有一个: + +- 给前端编辑器作为标准输入输出格式 + +当前明确**不包含**: + +- 不改 `dispatch` +- 不改 scheduler +- 不让运行时直接读取这份 YAML 执行 +- 不把 CodeB 变成通用工作流引擎 + +## 2. 来源边界 + +这份 Schema 只允许映射当前源码里已经稳定存在的事实,主要来自以下文件: + +- [`internal/pipeline/dispatch.go`](file:///Users/lihongyang/Desktop/code-bee-tools/internal/pipeline/dispatch.go) +- [`internal/pipeline/contracts.go`](file:///Users/lihongyang/Desktop/code-bee-tools/internal/pipeline/contracts.go) +- [`internal/pipeline/artifacts.go`](file:///Users/lihongyang/Desktop/code-bee-tools/internal/pipeline/artifacts.go) +- [`internal/pipeline/history.go`](file:///Users/lihongyang/Desktop/code-bee-tools/internal/pipeline/history.go) +- [`internal/pipeline/prompts.go`](file:///Users/lihongyang/Desktop/code-bee-tools/internal/pipeline/prompts.go) +- [`internal/config/config.go`](file:///Users/lihongyang/Desktop/code-bee-tools/internal/config/config.go) + +因此它能稳定表达的只有: + +- 固定阶段 +- 固定角色 +- 固定结果契约 +- 固定边条件 +- 固定 runtime 参数 +- 固定工件命名 + +它**不能**从现有源码中稳定推导的内容包括: + +- 画布坐标 +- 节点分组/颜色/折叠状态 +- 任意自定义节点类型 +- 通用表达式引擎 +- 任意脚本型边条件 + +所以,编辑器可以增加自己的 UI 元数据,但那必须放在独立字段里,不能污染这份核心 Schema。 + +## 3. 总体原则 + +这份 YAML Schema 必须满足 4 条原则: + +1. 只表达当前源码已有事实,不补脑 +2. 能被前端稳定解析成节点图 +3. 能从前端编辑态稳定组装回 YAML +4. 不要求当前运行时直接消费它 + +## 4. 顶层结构 + +标准顶层结构建议如下: + +```yaml +apiVersion: codebee/v1 +kind: Orchestration +metadata: + name: default-issue-flow + description: CodeB 当前固定四阶段流程 + +workflow: + startNodeId: issue_handling + nodes: [] + edges: [] + +runtime: + maxCodingReviewRounds: 3 + maxConsecutiveUnknownReview: 2 + maxIssuePostAttempts: 2 + loopJudgeStartRound: 0 + +roles: + defaultAgent: 开发者 + reviewerAgent: QA负责人 + issuePostAgent: 产品经理 + loopJudgeAgent: 技术负责人 + +artifacts: + issueHandlingResult: issue_intake_result.json + codingResult: coding_result.json + reviewResult: review_result.json + issuePostResultPattern: issue_post_result_{purpose}.json + loopJudgeResult: loop_judge_result.json + loopHistory: loop_history.json + +ui: + nodes: {} +``` + +## 5. 顶层字段定义 + +### 5.1 `apiVersion` + +固定值: + +```yaml +apiVersion: codebee/v1 +``` + +说明: + +- 表示这是给 CodeB 编排编辑器使用的 YAML 表达版本 +- 这里的版本是编辑器 Schema 版本,不是运行时协议版本 + +### 5.2 `kind` + +固定值: + +```yaml +kind: Orchestration +``` + +### 5.3 `metadata` + +```yaml +metadata: + name: default-issue-flow + description: CodeB 当前固定四阶段流程 +``` + +字段说明: + +- `name`: 编排名称,必填 +- `description`: 编排说明,选填 + +### 5.4 `workflow` + +```yaml +workflow: + startNodeId: issue_handling + nodes: [] + edges: [] +``` + +字段说明: + +- `startNodeId`: 起始节点 ID,必填 +- `nodes`: 节点列表,必填 +- `edges`: 边列表,必填 + +### 5.5 `runtime` + +```yaml +runtime: + maxCodingReviewRounds: 3 + maxConsecutiveUnknownReview: 2 + maxIssuePostAttempts: 2 + loopJudgeStartRound: 0 +``` + +字段说明: + +- `maxCodingReviewRounds`: coder-reviewer 最大轮数,当前默认值来自 [`dispatch.go:L25`](file:///Users/lihongyang/Desktop/code-bee-tools/internal/pipeline/dispatch.go#L25) +- `maxConsecutiveUnknownReview`: 连续 `UNKNOWN` 阈值,当前默认值来自 [`dispatch.go:L26`](file:///Users/lihongyang/Desktop/code-bee-tools/internal/pipeline/dispatch.go#L26) +- `maxIssuePostAttempts`: Issue 提交最大重试次数,当前默认值来自 [`dispatch.go:L27`](file:///Users/lihongyang/Desktop/code-bee-tools/internal/pipeline/dispatch.go#L27) +- `loopJudgeStartRound`: 从第几轮开始触发价值评估员,默认值来自 [`config.go:L13-L15`](file:///Users/lihongyang/Desktop/code-bee-tools/internal/config/config.go#L13-L15) + +### 5.6 `roles` + +```yaml +roles: + defaultAgent: 开发者 + reviewerAgent: QA负责人 + issuePostAgent: 产品经理 + loopJudgeAgent: 技术负责人 +``` + +字段说明: + +- `defaultAgent`: 编码主角色 +- `reviewerAgent`: 审查角色 +- `issuePostAgent`: Issue 提交角色 +- `loopJudgeAgent`: 价值评估角色 + +默认值来自 [`config.go:L104-L110`](file:///Users/lihongyang/Desktop/code-bee-tools/internal/config/config.go#L104-L110)。 + +### 5.7 `artifacts` + +```yaml +artifacts: + issueHandlingResult: issue_intake_result.json + codingResult: coding_result.json + reviewResult: review_result.json + issuePostResultPattern: issue_post_result_{purpose}.json + loopJudgeResult: loop_judge_result.json + loopHistory: loop_history.json +``` + +字段说明: + +- 用于给编辑器展示每个节点对应的工件命名 +- 来源于 [`artifacts.go`](file:///Users/lihongyang/Desktop/code-bee-tools/internal/pipeline/artifacts.go) + +### 5.8 `ui` + +```yaml +ui: + nodes: + issue_handling: + x: 100 + y: 120 +``` + +说明: + +- `ui` 是编辑器专用元数据区 +- 只允许放展示相关字段,如坐标、折叠状态、颜色 +- 不允许放运行语义 + +## 6. `workflow.nodes` 结构 + +单个节点结构如下: + +```yaml +- id: issue_handling + type: issue-handling + name: Issue 处理 + roleRef: issuePostAgent + promptTemplate: issue-handling + resultContract: issue-handling-result + outputs: + - issue_intake_result.json +``` + +字段定义: + +- `id`: 节点唯一标识 +- `type`: 节点类型 +- `name`: 节点显示名称 +- `roleRef`: 角色引用,引用 `roles` 中的 key +- `promptTemplate`: 提示词模板标识 +- `resultContract`: 结果契约标识 +- `inputs`: 输入工件列表,可选 +- `outputs`: 输出工件列表,必填 + +## 7. 标准节点类型 + +当前只能从源码里稳定提取这 5 类节点: + +```yaml +issue-handling +coding +review +issue-post +loop-judge +``` + +对应来源: + +- `issue-handling`: [`runIssueHandling`](file:///Users/lihongyang/Desktop/code-bee-tools/internal/pipeline/dispatch.go#L111-L158) +- `coding`: [`runCodingRound`](file:///Users/lihongyang/Desktop/code-bee-tools/internal/pipeline/dispatch.go#L363-L401) +- `review`: [`runReviewRound`](file:///Users/lihongyang/Desktop/code-bee-tools/internal/pipeline/dispatch.go#L403-L442) +- `issue-post`: [`runIssuePostWithRetry`](file:///Users/lihongyang/Desktop/code-bee-tools/internal/pipeline/dispatch.go#L444-L503) +- `loop-judge`: [`runLoopJudgeIfNeeded`](file:///Users/lihongyang/Desktop/code-bee-tools/internal/pipeline/dispatch.go#L305-L339) + +## 8. 推荐默认节点清单 + +基于当前源码,推荐固定为这 6 个节点: + +```yaml +workflow: + startNodeId: issue_handling + nodes: + - id: issue_handling + type: issue-handling + name: Issue 处理 + roleRef: issuePostAgent + promptTemplate: issue-handling + resultContract: issue-handling-result + outputs: + - issue_intake_result.json + + - id: issue_post_intake + type: issue-post + name: 接单回帖 + roleRef: issuePostAgent + promptTemplate: issue-post + resultContract: issue-post-result + inputs: + - issue_intake_result.json + outputs: + - issue_post_result_intake.json + + - id: coding_main + type: coding + name: 编码实现 + roleRef: defaultAgent + promptTemplate: coding + resultContract: coding-result + inputs: + - issue_intake_result.json + - review_result.json + outputs: + - coding_result.json + + - id: review_main + type: review + name: 结果审查 + roleRef: reviewerAgent + promptTemplate: review + resultContract: review-result + inputs: + - issue_intake_result.json + - coding_result.json + outputs: + - review_result.json + + - id: issue_post_completion + type: issue-post + name: 完成回帖 + roleRef: issuePostAgent + promptTemplate: issue-post + resultContract: issue-post-result + inputs: + - review_result.json + outputs: + - issue_post_result_completion.json + + - id: loop_judge_main + type: loop-judge + name: 价值评估 + roleRef: loopJudgeAgent + promptTemplate: loop-judge + resultContract: loop-judge-result + inputs: + - loop_history.json + outputs: + - loop_judge_result.json +``` + +## 9. `workflow.edges` 结构 + +单条边结构如下: + +```yaml +- from: review_main + to: coding_main + when: + reviewStatus: FAIL +``` + +字段定义: + +- `from`: 起点节点 ID +- `to`: 终点节点 ID +- `when`: 迁移条件 + +`when` 当前只允许使用受控条件字段,不能写任意表达式。 + +## 10. 标准边条件字段 + +当前建议只允许这 5 类条件字段: + +```yaml +issueStatus +codingStatus +reviewStatus +issuePostStatus +loopJudgeDecision +``` + +### 10.1 `issueStatus` + +取值来自 [`contracts.go:L20-L21`](file:///Users/lihongyang/Desktop/code-bee-tools/internal/pipeline/contracts.go#L20-L21): + +```yaml +READY +BLOCKED +``` + +### 10.2 `codingStatus` + +取值来自 [`contracts.go:L23-L25`](file:///Users/lihongyang/Desktop/code-bee-tools/internal/pipeline/contracts.go#L23-L25): + +```yaml +DONE +IN_PROGRESS +BLOCKED +``` + +### 10.3 `reviewStatus` + +取值来自 [`contracts.go:L27-L30`](file:///Users/lihongyang/Desktop/code-bee-tools/internal/pipeline/contracts.go#L27-L30): + +```yaml +PASS +FAIL +UNKNOWN +BLOCKED +``` + +### 10.4 `issuePostStatus` + +取值来自 [`contracts.go:L32-L34`](file:///Users/lihongyang/Desktop/code-bee-tools/internal/pipeline/contracts.go#L32-L34): + +```yaml +POSTED +REJECTED +BLOCKED +``` + +### 10.5 `loopJudgeDecision` + +取值来自 [`contracts.go:L181-L184`](file:///Users/lihongyang/Desktop/code-bee-tools/internal/pipeline/contracts.go#L181-L184): + +```yaml +CONTINUE +SHRINK_TASK +STOP_MANUAL +STOP_BLOCKED +``` + +## 11. 推荐默认边 + +当前固定流程可整理为以下边: + +```yaml +workflow: + edges: + - from: issue_handling + to: issue_post_intake + + - from: issue_post_intake + to: coding_main + when: + issuePostStatus: POSTED + + - from: coding_main + to: review_main + when: + codingStatus: DONE + + - from: review_main + to: issue_post_completion + when: + reviewStatus: PASS + + - from: review_main + to: coding_main + when: + reviewStatus: FAIL + + - from: review_main + to: coding_main + when: + reviewStatus: UNKNOWN + + - from: review_main + to: loop_judge_main + when: + reviewStatus: FAIL + + - from: review_main + to: loop_judge_main + when: + reviewStatus: UNKNOWN + + - from: loop_judge_main + to: coding_main + when: + loopJudgeDecision: CONTINUE + + - from: loop_judge_main + to: coding_main + when: + loopJudgeDecision: SHRINK_TASK +``` + +说明: + +- `STOP_MANUAL` 和 `STOP_BLOCKED` 是终止决策,不再回流到 `coding_main` +- `issue_handling -> issue_post_intake` 可以没有 `when`,因为这一步是固定串联 + +## 12. 标准结果契约标识 + +编辑器里不需要展开每个 JSON 字段定义到节点内部,但需要知道节点绑定的是哪种结果契约。 + +推荐使用以下标识: + +```yaml +issue-handling-result +coding-result +review-result +issue-post-result +loop-judge-result +``` + +它们分别映射到源码中的结构体: + +- `issue-handling-result` -> [`IssueHandlingResult`](file:///Users/lihongyang/Desktop/code-bee-tools/internal/pipeline/contracts.go#L47-L66) +- `coding-result` -> [`CodingResult`](file:///Users/lihongyang/Desktop/code-bee-tools/internal/pipeline/contracts.go#L80-L96) +- `review-result` -> [`ReviewResult`](file:///Users/lihongyang/Desktop/code-bee-tools/internal/pipeline/contracts.go#L108-L129) +- `issue-post-result` -> [`IssuePostResult`](file:///Users/lihongyang/Desktop/code-bee-tools/internal/pipeline/contracts.go#L149-L161) +- `loop-judge-result` -> [`LoopJudgeResult`](file:///Users/lihongyang/Desktop/code-bee-tools/internal/pipeline/contracts.go#L187-L201) + +## 13. 标准提示词模板标识 + +为避免把整段长 prompt 直接写进 YAML,建议只保留模板标识: + +```yaml +issue-handling +coding +review +issue-post +loop-judge +``` + +这些模板对应当前源码中的构造函数: + +- `issue-handling` -> [`buildIssueHandlingPrompt`](file:///Users/lihongyang/Desktop/code-bee-tools/internal/pipeline/prompts.go#L14-L84) +- `coding` -> [`buildCodingPrompt`](file:///Users/lihongyang/Desktop/code-bee-tools/internal/pipeline/prompts.go#L86-L114) +- `review` -> [`buildReviewPrompt`](file:///Users/lihongyang/Desktop/code-bee-tools/internal/pipeline/prompts.go#L181-L220) +- `issue-post` -> [`buildIssuePostPrompt`](file:///Users/lihongyang/Desktop/code-bee-tools/internal/pipeline/prompts.go#L264-L323) +- `loop-judge` -> [`buildLoopJudgePrompt`](file:///Users/lihongyang/Desktop/code-bee-tools/internal/pipeline/prompts.go#L325-L389) + +## 14. 完整 YAML 示例 + +```yaml +apiVersion: codebee/v1 +kind: Orchestration + +metadata: + name: default-issue-flow + description: CodeB 当前固定四阶段流程与 coder-reviewer loop + +workflow: + startNodeId: issue_handling + nodes: + - id: issue_handling + type: issue-handling + name: Issue 处理 + roleRef: issuePostAgent + promptTemplate: issue-handling + resultContract: issue-handling-result + outputs: + - issue_intake_result.json + + - id: issue_post_intake + type: issue-post + name: 接单回帖 + roleRef: issuePostAgent + promptTemplate: issue-post + resultContract: issue-post-result + inputs: + - issue_intake_result.json + outputs: + - issue_post_result_intake.json + + - id: coding_main + type: coding + name: 编码实现 + roleRef: defaultAgent + promptTemplate: coding + resultContract: coding-result + inputs: + - issue_intake_result.json + - review_result.json + outputs: + - coding_result.json + + - id: review_main + type: review + name: 结果审查 + roleRef: reviewerAgent + promptTemplate: review + resultContract: review-result + inputs: + - issue_intake_result.json + - coding_result.json + outputs: + - review_result.json + + - id: issue_post_completion + type: issue-post + name: 完成回帖 + roleRef: issuePostAgent + promptTemplate: issue-post + resultContract: issue-post-result + inputs: + - review_result.json + outputs: + - issue_post_result_completion.json + + - id: loop_judge_main + type: loop-judge + name: 价值评估 + roleRef: loopJudgeAgent + promptTemplate: loop-judge + resultContract: loop-judge-result + inputs: + - loop_history.json + outputs: + - loop_judge_result.json + + edges: + - from: issue_handling + to: issue_post_intake + + - from: issue_post_intake + to: coding_main + when: + issuePostStatus: POSTED + + - from: coding_main + to: review_main + when: + codingStatus: DONE + + - from: review_main + to: issue_post_completion + when: + reviewStatus: PASS + + - from: review_main + to: coding_main + when: + reviewStatus: FAIL + + - from: review_main + to: coding_main + when: + reviewStatus: UNKNOWN + + - from: review_main + to: loop_judge_main + when: + reviewStatus: FAIL + + - from: review_main + to: loop_judge_main + when: + reviewStatus: UNKNOWN + + - from: loop_judge_main + to: coding_main + when: + loopJudgeDecision: CONTINUE + + - from: loop_judge_main + to: coding_main + when: + loopJudgeDecision: SHRINK_TASK + +runtime: + maxCodingReviewRounds: 3 + maxConsecutiveUnknownReview: 2 + maxIssuePostAttempts: 2 + loopJudgeStartRound: 0 + +roles: + defaultAgent: 开发者 + reviewerAgent: QA负责人 + issuePostAgent: 产品经理 + loopJudgeAgent: 技术负责人 + +artifacts: + issueHandlingResult: issue_intake_result.json + codingResult: coding_result.json + reviewResult: review_result.json + issuePostResultPattern: issue_post_result_{purpose}.json + loopJudgeResult: loop_judge_result.json + loopHistory: loop_history.json + +ui: + nodes: + issue_handling: + x: 120 + y: 120 + issue_post_intake: + x: 340 + y: 120 + coding_main: + x: 620 + y: 120 + review_main: + x: 900 + y: 120 + issue_post_completion: + x: 1180 + y: 120 + loop_judge_main: + x: 900 + y: 320 +``` + +## 15. 编辑器实现时的边界 + +编辑器下一步只应该做这些事: + +1. 解析这份 YAML +2. 转成前端节点/边状态 +3. 编辑后重新组装成这份 YAML +4. 对枚举和值域做静态校验 + +编辑器当前不应该做这些事: + +1. 不驱动运行时执行 +2. 不替换 `dispatch.go` +3. 不把任意表达式塞进 `when` +4. 不引入超出当前源码事实的新节点类型 + +## 16. 结论 + +从现有源码可以稳定反推出一份标准 YAML Schema,而且这份 Schema 足够支撑一个编排编辑器。 + +但它的身份必须保持清楚: + +- 它是**源码事实的外部表达** +- 不是新的调度内核 +- 不是新的运行时协议 +- 更不是通用工作流平台 DSL + +所以,后续最合理的实现顺序是: + +1. 基于本文档实现 YAML 解析/组装 +2. 基于解析结果实现前端编辑态 +3. 最后再做画布和属性面板 diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..52ae749 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,11 @@ +# frontend + +该目录预留给符合项目规范的 React 前端工程。 + +当前状态: +- 目录已预留并纳入仓库级结构校验 +- 尚未接入实际前端实现 + +约束说明: +- 所有前端代码必须收敛到 `frontend/` 下,禁止再把 Node / Vite / React 资产散落到仓库根目录 +- 目录结构、测试入口和发布门禁后续需对齐《项目目录结构规范》和《自动化测试规范》 diff --git a/scripts/check_commit_message.sh b/scripts/check_commit_message.sh new file mode 100755 index 0000000..1617fcf --- /dev/null +++ b/scripts/check_commit_message.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# 核心功能: +# 1. 校验 Git commit message 或 PR 标题是否符合仓库约定的 Conventional Commits 格式 +# 2. 作为本地 commit-msg hook、Makefile 检查入口和 CI 校验脚本复用,避免规则散落 +# +# 开发维护: AI Assistant +# 创建时间: 2026-07-06 +# 更新时间: 2026-07-06 + +set -euo pipefail + +readonly COMMIT_MESSAGE_PATTERN='^(feat|fix|refactor|perf|test|docs|chore|style)(\([a-z0-9-]+\))?: .{1,50}$' + +print_usage() { + cat <<'EOF' +用法: + ./scripts/check_commit_message.sh --file + ./scripts/check_commit_message.sh --message "" + ./scripts/check_commit_message.sh --head +EOF +} + +validate_message() { + local message="$1" + + if [[ ! "$message" =~ $COMMIT_MESSAGE_PATTERN ]]; then + echo "提交信息格式不合规: $message" >&2 + echo "正确格式: (): " >&2 + echo "示例: feat(ci): add release workflow" >&2 + exit 1 + fi +} + +if [[ $# -eq 0 ]]; then + print_usage + exit 1 +fi + +case "${1:-}" in + --file) + if [[ $# -ne 2 ]]; then + print_usage + exit 1 + fi + validate_message "$(head -n 1 "$2" | tr -d '\r')" + ;; + --message) + if [[ $# -ne 2 ]]; then + print_usage + exit 1 + fi + validate_message "$2" + ;; + --head) + if [[ $# -ne 1 ]]; then + print_usage + exit 1 + fi + validate_message "$(git log -1 --pretty=%s)" + ;; + *) + print_usage + exit 1 + ;; +esac diff --git a/scripts/githooks/commit-msg b/scripts/githooks/commit-msg new file mode 100755 index 0000000..2824b51 --- /dev/null +++ b/scripts/githooks/commit-msg @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# 提交信息阶段执行 commit message 规范校验。 + +set -euo pipefail + +./scripts/check_commit_message.sh --file "$1" diff --git a/scripts/githooks/pre-commit b/scripts/githooks/pre-commit new file mode 100755 index 0000000..94aceae --- /dev/null +++ b/scripts/githooks/pre-commit @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# 提交前执行轻量静态门禁,尽早在本地阻断明显违规。 + +set -euo pipefail + +make lint-backend +make check-structure diff --git a/scripts/githooks/pre-push b/scripts/githooks/pre-push new file mode 100755 index 0000000..424d33d --- /dev/null +++ b/scripts/githooks/pre-push @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# 推送前执行最小可接受测试,避免明显失败的代码进入远端流水线。 + +set -euo pipefail + +make test-backend-unit diff --git a/scripts/install_hooks.sh b/scripts/install_hooks.sh new file mode 100755 index 0000000..0ccb74d --- /dev/null +++ b/scripts/install_hooks.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# 核心功能: +# 1. 将仓库内维护的 Git hooks 安装到当前本地仓库,统一提交前和推送前门禁 +# 2. 避免开发者手工复制脚本导致本地检查行为不一致 +# +# 开发维护: AI Assistant +# 创建时间: 2026-07-06 +# 更新时间: 2026-07-06 + +set -euo pipefail + +readonly GIT_HOOKS_DIR=".git/hooks" +readonly SOURCE_HOOKS_DIR="scripts/githooks" + +mkdir -p "$GIT_HOOKS_DIR" + +for hook_name in pre-commit commit-msg pre-push; do + cp "$SOURCE_HOOKS_DIR/$hook_name" "$GIT_HOOKS_DIR/$hook_name" + chmod +x "$GIT_HOOKS_DIR/$hook_name" +done + +echo "Git hooks 安装完成"