diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 8a3207b..8c10325 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -17,10 +17,21 @@ jobs: - name: Install pyyaml run: pip install pyyaml - - name: Validate SKILL.md frontmatter + required files - run: python3 scripts/validate-skill.py . + - name: Validate SKILL.md + routing + self-check (strict) + run: python3 scripts/validate-skill.py --strict . - name: Smoke test build run: | python3 scripts/build-skill.py /tmp/dist test -f /tmp/dist/gospec.skill + + - name: Smoke test install.sh in clean dir + run: | + mkdir -p /tmp/install-test + cd /tmp/install-test + SKILL_DIR=$GITHUB_WORKSPACE bash $GITHUB_WORKSPACE/scripts/install.sh + test -f AGENTS.md + test -f .cursor/rules/gospec.mdc + # 校验 cursor 规则的 frontmatter(globs / alwaysApply) + head -5 .cursor/rules/gospec.mdc | grep -q "^globs:" + head -5 .cursor/rules/gospec.mdc | grep -q "^alwaysApply:" diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ee1f89..cb60053 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- **SKILL.md `description` 大幅缩短**(~1024 字符 → ~250 字符),提升 agent 激活匹配准确率。 +- **软化 3 条过严的"强制约束"**,与 Go 生态实际一致: + - `init()` 改为"仅允许做注册(pprof / collector / driver),禁止 IO 或可能 panic" + - `interface{}` 改为"避免出现在公共 API 边界,解码 / SDK 适配等不可避免场景就近注释" + - `utils/` 改为"仅作最后兜底,优先按职责拆 `mathx/` / `strx/`" +- 三处同步更新:`spec/spec.md` / `spec/05-coding/README.md` / `docs/templates/project-agents-template.md`。 +- **CI 切到 `--strict` 模式**,路由表完整性 + 自查清单缺失视为失败,防 spec 漂移。 + +### Added (Cursor 体验) + +- **`docs/templates/cursor-rule-template.mdc`**:Cursor 单文件规则模板,自带 `globs: [**/*.go, **/*.proto, ...]` + `alwaysApply: false`,让 Cursor 在编辑相关文件时自动附加,**避免每次让用户手动选择规则**。 +- **`scripts/install.sh` 同时落 `.cursor/rules/gospec.mdc`**,可通过 `NO_CURSOR=1` 跳过;本地落后远端时自动提示更新命令。 + +### Added (校验强化) + +- **路由表完整性校验**:`scripts/validate-skill.py` 检查 `spec/spec.md` 路由表里引用的所有 spec 子文件是否真实存在,防 broken link。 +- **自查清单存在校验**:每个 spec 子文件须有 `## 自查` / `## Checklist` 标题小节,否则在 `--strict` 模式下报错。已为缺失的两个文件补上(`spec/09-documentation.md`、`spec/01-requirement/lifecycle.md`);`spec/07-code-review.md` 整体即为 PR 自查清单,列入例外。 +- **install.sh 端到端冒烟测试**:CI 模拟在干净目录运行 install.sh,校验 `AGENTS.md` 和 `.cursor/rules/gospec.mdc` 落盘 + frontmatter 正确。 + ### Added - **SDLC spec 骨架**(`spec/`):覆盖从需求到运维的 13 个阶段,每阶段按主题拆分为子文件,通过 `spec/spec.md` 的任务路由表按需加载。 diff --git a/README.md b/README.md index 1e53b3d..937a6f8 100644 --- a/README.md +++ b/README.md @@ -169,21 +169,24 @@ npx skills add singchia/gospec -g # 安装到 ~/.claude/skills/gospec/ 之后 Claude Code 等支持 SKILL.md 的 agent 会在你写 / 审查 / 重构 Go 代码时自动激活 gospec,并在首次激活时主动询问是否在项目根创建 `AGENTS.md`。 -### 方式二:`install.sh`(一行命令,自动落 AGENTS.md) +### 方式二:`install.sh`(一行命令,自动落 AGENTS.md + Cursor 规则) -如果你想立刻把 `AGENTS.md` 落到项目根(不等 agent 提示),或者你的 agent 不读 SKILL.md 必须靠 AGENTS.md 触发: +如果你想立刻把规则落到项目根(不等 agent 提示),或者你的 agent 不读 SKILL.md 必须靠 AGENTS.md / Cursor rules 触发: ```bash cd your-go-project bash <(curl -sSL https://raw.githubusercontent.com/singchia/gospec/main/scripts/install.sh) ``` -这一行会做两件事: +这一行会做三件事: 1. **安装 gospec skill** 到 `~/.claude/skills/gospec/`(如果还没装) -2. **在当前目录创建 `AGENTS.md`**,让任何 AI agent 打开本项目就知道遵循 gospec +2. **在项目根创建 `AGENTS.md`**(Codex / Cline / 通用 agent 入口) +3. **在项目根创建 `.cursor/rules/gospec.mdc`**(Cursor 单文件规则,自带 `globs`,编辑 `.go` / `.proto` / `Dockerfile` / migration 时自动附加,**避免 Cursor 每次让用户手动选择**) -之后任何 agent(Claude Code、Cursor、Cline、Codex、Gemini CLI、GitHub Copilot)打开你的项目,第一眼看到 `AGENTS.md` 就被引导到 gospec 的任务路由表 + 核心约束。 +跳过 Cursor 落盘:`NO_CURSOR=1 bash <(curl ...)` + +之后任何 agent(Claude Code / Cursor / Cline / Codex / Gemini CLI / GitHub Copilot)打开你的项目,都会自动加载 gospec 的任务路由表 + 核心约束。 > **AGENTS.md vs SKILL.md 的区别**: > - `SKILL.md` 是 Claude Code 通过 [skills.sh](https://skills.sh) 协议自动加载的入口,作用域是整个 skill diff --git a/SKILL.md b/SKILL.md index c1fe203..40fe555 100644 --- a/SKILL.md +++ b/SKILL.md @@ -1,6 +1,6 @@ --- name: gospec -description: "Go backend SDLC spec skill (Chinese content). Covers 13 stages: requirements (Issue/RFC/PRD/Epic), architecture (layering + monorepo), API (proto/gRPC/HTTP), data models (MySQL/Redis/ClickHouse/InfluxDB), coding (10 design patterns), testing, code review, CI/CD, docs, observability (SLO/metrics/traces), security (authN/Z/threat-modeling/supply-chain), operations (incidents/runbooks/postmortems), and database migration. Use when writing or reviewing Go code, designing APIs or data schemas, writing tests, configuring CI/CD, setting up SLO and monitoring, handling security/secrets/compliance, writing deployment or incident plans, writing DB migrations, or drafting PRD/RFC/ADR/HLD docs. Go 后端项目 SDLC 全流程中文规范。覆盖需求/架构/API/数据模型(MySQL Redis ClickHouse InfluxDB)/编码/测试/代码审查/交付/文档/可观测性/安全/运维/数据库迁移 13 阶段。当编写或审查 Go 代码、设计 API 或数据模型、写测试、配 CI/CD、处理监控告警 SLO、安全 密钥 合规、写部署或事故响应方案、写数据库 migration、撰写 PRD RFC ADR HLD 文档时使用。" +description: "Go 后端 SDLC 全流程中文规范(Kratos / gRPC / GORM / MySQL / Redis / ClickHouse / InfluxDB)。写或审查 Go 代码、设计 API / 数据模型 / 架构、写测试、配 CI/CD、做日志指标追踪 SLO、处理认证密钥安全、写部署或事故方案、写数据库 migration、起草 PRD / RFC / ADR / HLD 时按需加载。Go backend SDLC spec skill — load on demand for coding, API/schema design, testing, CI/CD, observability, security, ops, DB migration, and PRD/RFC/ADR/HLD docs." license: MIT --- diff --git a/docs/templates/cursor-rule-template.mdc b/docs/templates/cursor-rule-template.mdc new file mode 100644 index 0000000..7b6b170 --- /dev/null +++ b/docs/templates/cursor-rule-template.mdc @@ -0,0 +1,89 @@ +--- +description: gospec — Go 后端 SDLC 规范,编写/审查 Go / proto / migration / Dockerfile 时自动加载 +globs: ["**/*.go", "**/*.proto", "go.mod", "go.sum", "**/Dockerfile", "**/*.sql", "docs/rfc/**/*.md", "docs/adr/**/*.md", "docs/requirements/**/*.md"] +alwaysApply: false +--- + +# gospec —— Go 后端 SDLC 规范(Cursor 单文件入口) + +> 本文件由 [gospec](https://github.com/singchia/gospec) 自动生成,作为 **Cursor 的唯一入口**。 +> 完整规范在 `~/.claude/skills/gospec/spec/` 或 `.claude/skills/gospec/spec/`。 +> Cursor 已通过 `globs` 自动附加,无需用户每次选择。 + +## 任务路由(先看这个表,再决定要不要展开 spec/) + +| 任务 | 最少必读 | +|------|---------| +| 写 HTTP / gRPC handler | `spec/03-api/proto.md` + `spec/03-api/http.md` + `spec/05-coding/errors.md` | +| 写业务逻辑 / Service | `spec/02-architecture/layering.md` + `spec/05-coding/errors.md` | +| 写 goroutine / 用锁 / 传 context | `spec/05-coding/concurrency.md` | +| 写 MySQL 模型 / DAO | `spec/04-data-model/mysql.md` | +| 写 Redis 缓存 / 分布式锁 | `spec/04-data-model/redis.md` | +| 写测试 | `spec/06-testing/unit.md` | +| 写 migration | `spec/13-database-migration/migration.md` | +| 加日志 / 指标 / 追踪 | `spec/10-observability/{logging,metrics,tracing}.md` | +| 实现登录 / 鉴权 | `spec/11-security/auth.md` | +| 写 PRD / RFC / ADR / HLD | `docs/templates/` 对应模板 | +| PR 自查 | `spec/07-code-review.md` | + +## 核心约束(不可违反,无需打开 spec 也要遵守) + +### 架构 +- 单服务:`cmd → web → controlplane → repo → model`,禁止跨层 +- monorepo:`internal/` 之间禁止直接 import,走 API / 事件 / `internal/shared/` +- 接口在消费方定义,禁止循环依赖 +- 依赖通过构造函数注入 + +### 编码 +- 禁止 `_ = fn()` 忽略错误 +- 共享状态必须加锁,测试必须 `-race` +- 错误用 `%w` 包装;不重复记录 +- 涉及 IO 的函数第一个参数 `context.Context` +- `init()` 仅做注册(pprof / collector / driver),禁止 IO 或可能 panic +- 避免 `any` / `interface{}` 出现在公共 API 边界 + +### API +- 所有变更先改 `.proto`,禁止改生成代码 +- Handler 必须有 Swagger 注释(`@Summary` / `@Router` / `@Success`) +- 响应统一 `{code, message, data}` +- 破坏性变更走新版本 + +### 测试 +- 新功能必须有单元测试 +- CI 强制 `-race` +- E2E 必须清理数据 + +### Git +- 提交格式:`(): ` +- 禁止提交敏感信息 +- 禁止 force push main/master + +### 可观测性 +- 暴露 `/healthz` `/readyz` `/metrics` +- 日志结构化 + `trace_id` +- 高基数字段(user_id / email / url)禁止做 Prometheus label +- 敏感字段禁止明文入日志 + +### 安全 +- 密码 bcrypt / argon2id,禁止 MD5 / SHA1 +- SQL 全部参数化 +- 密钥禁止进代码 / 镜像 / 日志 +- 容器以非 root 运行 +- 多租户接口强制 `tenant_id` 过滤 + +### 运维 +- 任何变更必须有回滚方案 +- 告警必须配 Runbook 链接 +- 高风险变更走金丝雀 / feature flag +- P0 / P1 事故必须产 blameless postmortem + +### 数据存储 +- **MySQL**:schema 变更走 migration;大表用在线 DDL;变更兼容滚动发布 +- **Redis**:所有 key 必须 TTL;禁止大 key(value > 10KB / 集合 > 5000);分布式锁有 owner 校验 +- **ClickHouse**:必须 Replicated;写入批量;ORDER BY 低基数到高基数 +- **InfluxDB**:tag 低基数;bucket 必须 retention +- PII 字段加密存储 + +## 输出语言 + +默认中文(代码注释 / 文档 / commit message)。 diff --git a/docs/templates/project-agents-template.md b/docs/templates/project-agents-template.md index ab69d96..33fbfe3 100644 --- a/docs/templates/project-agents-template.md +++ b/docs/templates/project-agents-template.md @@ -46,13 +46,13 @@ - 每个目录都被 CODEOWNERS 覆盖 ### 编码 -- 禁止 `_ = fn()` 忽略错误 +- 禁止 `_ = fn()` 忽略错误(确实想丢弃必须注释说明) - 共享状态必须加锁,测试必须带 `-race` - 错误用 `%w` 包装;不重复记录(要么处理要么传播) - 所有涉及 IO 的函数第一个参数为 `context.Context` -- 禁止 `init()` 函数(除 pprof 注册等特殊场景) -- 禁止全局可变变量 -- 禁止裸 `interface{}` +- `init()` 仅允许做注册(pprof / metrics collector / driver),禁止做 IO 或可能 panic +- 禁止全局可变变量(只读单例 / collector 除外) +- 避免 `any` / `interface{}` 出现在公共 API 边界(解码 / SDK 适配等不可避免时就近注释) ### API - 所有 API 变更先更新 `.proto`,禁止改生成代码 diff --git a/scripts/install.sh b/scripts/install.sh index 477e694..1d7b52b 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1,9 +1,12 @@ #!/usr/bin/env bash # install.sh — 在你的 Go 项目根目录运行,一行命令安装并激活 gospec # -# 它做两件事: +# 它做几件事: # 1. 安装 gospec skill 到 ~/.claude/skills/gospec/(如果还没装) -# 2. 在当前目录创建 AGENTS.md,让任何 AI agent 打开本项目都能识别 gospec +# 2. 在当前目录创建 AGENTS.md(Codex / Cline / Cursor 等通用入口) +# 3. 检测 Cursor,自动落 .cursor/rules/gospec.mdc +# —— 让 Cursor 通过 globs 自动附加,避免每次让用户挑选 +# 4. 检测 Claude Code 项目级 .claude/,可选项目级 skill 软链 # # 用法(远程,一行命令): # cd your-go-project @@ -15,13 +18,19 @@ # # 项目级安装(仅对当前项目生效): # SKILL_DIR=.claude/skills/gospec bash <(curl -sSL .../install.sh) +# +# 跳过 Cursor 落盘: +# NO_CURSOR=1 bash <(curl -sSL .../install.sh) set -euo pipefail REPO_URL="https://github.com/singchia/gospec.git" SKILL_DIR="${SKILL_DIR:-$HOME/.claude/skills/gospec}" TEMPLATE_REL="docs/templates/project-agents-template.md" -TARGET="./AGENTS.md" +CURSOR_TEMPLATE_REL="docs/templates/cursor-rule-template.mdc" +TARGET_AGENTS="./AGENTS.md" +TARGET_CURSOR_DIR="./.cursor/rules" +TARGET_CURSOR="$TARGET_CURSOR_DIR/gospec.mdc" echo "📦 gospec 安装" echo "" @@ -31,6 +40,16 @@ echo "" # ──────────────────────────────────────────────────────── if [[ -f "$SKILL_DIR/SKILL.md" ]]; then echo "✓ gospec skill 已安装在 $SKILL_DIR" + # 可选:检测远端有更新提示用户 update(不强制) + if command -v git >/dev/null 2>&1 && [[ -d "$SKILL_DIR/.git" ]]; then + if git -C "$SKILL_DIR" fetch --quiet origin 2>/dev/null; then + BEHIND=$(git -C "$SKILL_DIR" rev-list --count HEAD..origin/main 2>/dev/null || echo 0) + if [[ "$BEHIND" != "0" ]]; then + echo "⚠ 本地 gospec 落后远端 $BEHIND 个 commit,更新:" + echo " git -C $SKILL_DIR pull --ff-only" + fi + fi + fi else if ! command -v git >/dev/null 2>&1; then echo "❌ git 未安装,无法继续" @@ -46,8 +65,9 @@ fi # Step 2: 校验模板存在 # ──────────────────────────────────────────────────────── TEMPLATE_FULL="$SKILL_DIR/$TEMPLATE_REL" +CURSOR_TEMPLATE_FULL="$SKILL_DIR/$CURSOR_TEMPLATE_REL" if [[ ! -f "$TEMPLATE_FULL" ]]; then - echo "❌ 模板未找到:$TEMPLATE_FULL" + echo "❌ AGENTS 模板未找到:$TEMPLATE_FULL" echo " gospec 安装可能损坏或版本过旧,尝试:" echo " rm -rf $SKILL_DIR && bash $0" exit 1 @@ -56,22 +76,50 @@ fi # ──────────────────────────────────────────────────────── # Step 3: 在当前目录创建 AGENTS.md # ──────────────────────────────────────────────────────── -if [[ -f "$TARGET" ]]; then - BACKUP="$TARGET.bak.$(date +%Y%m%d%H%M%S)" - echo "⚠ $TARGET 已存在,备份为 $BACKUP" - cp "$TARGET" "$BACKUP" +if [[ -f "$TARGET_AGENTS" ]]; then + BACKUP="$TARGET_AGENTS.bak.$(date +%Y%m%d%H%M%S)" + echo "⚠ $TARGET_AGENTS 已存在,备份为 $BACKUP" + cp "$TARGET_AGENTS" "$BACKUP" +fi +cp "$TEMPLATE_FULL" "$TARGET_AGENTS" +echo "✓ AGENTS.md 已创建:$(pwd)/AGENTS.md(Codex / Cline / 通用 agent 入口)" + +# ──────────────────────────────────────────────────────── +# Step 4: 为 Cursor 落 .cursor/rules/gospec.mdc +# (单文件 + globs 自动附加,避免 Cursor 每次让用户选择) +# ──────────────────────────────────────────────────────── +if [[ "${NO_CURSOR:-0}" == "1" ]]; then + echo "⏭ 跳过 Cursor 规则(NO_CURSOR=1)" +elif [[ ! -f "$CURSOR_TEMPLATE_FULL" ]]; then + echo "⚠ Cursor 模板未找到,跳过:$CURSOR_TEMPLATE_FULL" +else + mkdir -p "$TARGET_CURSOR_DIR" + if [[ -f "$TARGET_CURSOR" ]]; then + BACKUP="$TARGET_CURSOR.bak.$(date +%Y%m%d%H%M%S)" + echo "⚠ $TARGET_CURSOR 已存在,备份为 $BACKUP" + cp "$TARGET_CURSOR" "$BACKUP" + fi + cp "$CURSOR_TEMPLATE_FULL" "$TARGET_CURSOR" + echo "✓ Cursor 规则已创建:$TARGET_CURSOR" + echo " (globs 自动附加 .go / .proto / Dockerfile / migration,无需每次手动选)" fi -cp "$TEMPLATE_FULL" "$TARGET" +# ──────────────────────────────────────────────────────── +# Step 5: 提示 Claude Code 项目级安装 +# ──────────────────────────────────────────────────────── +if [[ -d ".claude" && ! -d ".claude/skills/gospec" ]]; then + echo "" + echo "💡 检测到 .claude/,如需项目级安装(不影响其他项目):" + echo " SKILL_DIR=.claude/skills/gospec bash $0" +fi -echo "✓ AGENTS.md 已创建:$(pwd)/AGENTS.md" echo "" echo "───────────────────────────────────────────────" echo "下一步:" -echo " 1. 检查 AGENTS.md 内容,按需调整核心约束" +echo " 1. 检查 AGENTS.md / .cursor/rules/gospec.mdc 内容,按需调整" echo " 2. 提交到 git:" -echo " git add AGENTS.md" -echo " git commit -m 'chore: add gospec AGENTS.md'" -echo " 3. 任何 AI agent(Claude Code / Cursor / Cline / Codex)" -echo " 打开本项目都会读到 AGENTS.md 并加载 gospec 规范" +echo " git add AGENTS.md .cursor/rules/gospec.mdc" +echo " git commit -m 'chore: add gospec rules'" +echo " 3. 任何 AI agent(Claude Code / Cursor / Cline / Codex / Gemini CLI)" +echo " 打开本项目都会自动加载 gospec 规范" echo "───────────────────────────────────────────────" diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh new file mode 100755 index 0000000..a937ea7 --- /dev/null +++ b/scripts/uninstall.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# uninstall.sh — 移除 gospec skill 和当前项目里的 gospec 落盘文件 +# +# 用法(在 Go 项目根目录运行): +# bash <(curl -sSL https://raw.githubusercontent.com/singchia/gospec/main/scripts/uninstall.sh) +# +# 或本地: +# ~/.claude/skills/gospec/scripts/uninstall.sh +# +# 默认移除: +# - 当前目录的 ./AGENTS.md(先备份) +# - 当前目录的 ./.cursor/rules/gospec.mdc +# 不会自动删 ~/.claude/skills/gospec/,需要加 KEEP_SKILL=0 才会删 +# +# 环境变量: +# KEEP_SKILL=0 一并删 ~/.claude/skills/gospec/(默认保留) +# SKILL_DIR=... 自定义 skill 目录(默认 ~/.claude/skills/gospec) + +set -euo pipefail + +SKILL_DIR="${SKILL_DIR:-$HOME/.claude/skills/gospec}" +KEEP_SKILL="${KEEP_SKILL:-1}" +TARGET_AGENTS="./AGENTS.md" +TARGET_CURSOR="./.cursor/rules/gospec.mdc" + +echo "🧹 gospec 卸载" +echo "" + +# ──────────────────────────────────────────────────────── +# Step 1: 项目根 AGENTS.md +# ──────────────────────────────────────────────────────── +if [[ -f "${TARGET_AGENTS}" ]]; then + if grep -q "gospec" "${TARGET_AGENTS}" 2>/dev/null; then + BACKUP="${TARGET_AGENTS}.bak.$(date +%Y%m%d%H%M%S)" + cp "${TARGET_AGENTS}" "${BACKUP}" + rm "${TARGET_AGENTS}" + echo "✓ 已删除 ${TARGET_AGENTS}(备份为 ${BACKUP})" + else + echo "⏭ ${TARGET_AGENTS} 不像是 gospec 生成的(不含 'gospec' 字样),保留不动" + fi +else + echo "⏭ ${TARGET_AGENTS} 不存在,跳过" +fi + +# ──────────────────────────────────────────────────────── +# Step 2: Cursor 规则 +# ──────────────────────────────────────────────────────── +if [[ -f "${TARGET_CURSOR}" ]]; then + rm "${TARGET_CURSOR}" + echo "✓ 已删除 ${TARGET_CURSOR}" + # 如 .cursor/rules/ 已空,连带删 + if [[ -d "./.cursor/rules" ]] && [[ -z "$(ls -A ./.cursor/rules)" ]]; then + rmdir ./.cursor/rules + if [[ -d "./.cursor" ]] && [[ -z "$(ls -A ./.cursor)" ]]; then + rmdir ./.cursor + fi + fi +else + echo "⏭ ${TARGET_CURSOR} 不存在,跳过" +fi + +# ──────────────────────────────────────────────────────── +# Step 3: 全局 skill 目录 +# ──────────────────────────────────────────────────────── +if [[ "${KEEP_SKILL}" == "0" ]]; then + if [[ -d "${SKILL_DIR}" ]]; then + rm -rf "${SKILL_DIR}" + echo "✓ 已删除 ${SKILL_DIR}" + else + echo "⏭ ${SKILL_DIR} 不存在,跳过" + fi +else + if [[ -d "${SKILL_DIR}" ]]; then + echo "ℹ 保留 ${SKILL_DIR}(其他项目可能仍在用)" + echo " 如要一并删除:KEEP_SKILL=0 bash $0" + fi +fi + +echo "" +echo "✅ 卸载完成" diff --git a/scripts/validate-skill.py b/scripts/validate-skill.py index e60ed11..00989cf 100755 --- a/scripts/validate-skill.py +++ b/scripts/validate-skill.py @@ -7,6 +7,7 @@ 用法: python3 scripts/validate-skill.py # 校验当前目录 python3 scripts/validate-skill.py /path/to/dir # 校验指定目录 + python3 scripts/validate-skill.py --strict . # 自查清单缺失视为失败 退出码:0 = 通过,1 = 失败。 """ @@ -43,9 +44,33 @@ "docs/templates/technical-rfc-template.md", "docs/templates/architecture-decision-record-template.md", "docs/templates/high-level-design-template.md", + "docs/templates/cursor-rule-template.mdc", "scripts/install.sh", ] +# 路由表里的 spec 子文件必须真实存在 +ROUTING_LINK_RE = re.compile(r"`(\d{2}-[\w-]+/[\w.-]+\.md|\d{2}-[\w-]+\.md|spec\.md)`") + +# 每个 spec 子文件应有"自查"小节(标题),否则 agent 完成任务后无法对照。 +# 仓库现行约定是 `## 自查`(最常用);保留 `## 自查清单` / `## Checklist` 兼容。 +# 例外:纯路由 / 索引文件 +SELF_CHECK_PATTERN = re.compile(r"^##\s+(自查清单|自查|Checklist|checklist|Self-check)\b", re.MULTILINE) +SELF_CHECK_EXEMPT = { + "spec/spec.md", # 入口路由 + "spec/07-code-review.md", # 整个文件就是 PR 自查清单 + "spec/05-coding/README.md", # 二级路由 + "spec/01-requirement/README.md", + "spec/02-architecture/README.md", + "spec/03-api/README.md", + "spec/04-data-model/README.md", + "spec/06-testing/README.md", + "spec/08-delivery/README.md", + "spec/10-observability/README.md", + "spec/11-security/README.md", + "spec/12-operations/README.md", + "spec/13-database-migration/README.md", +} + def validate_frontmatter(skill_md: Path) -> tuple[bool, str]: content = skill_md.read_text() @@ -110,30 +135,86 @@ def validate_files(skill_dir: Path) -> tuple[bool, str]: return True, "" -def validate(skill_dir: str) -> tuple[bool, str]: +def validate_routing_links(skill_dir: Path) -> tuple[bool, str]: + """spec/spec.md 路由表里引用的所有 spec 子文件必须存在。""" + spec_md = skill_dir / "spec" / "spec.md" + text = spec_md.read_text() + spec_root = skill_dir / "spec" + broken = [] + for match in ROUTING_LINK_RE.findall(text): + # 跳过 spec.md 自引用 + if match == "spec.md": + continue + target = spec_root / match + if not target.exists(): + broken.append(match) + if broken: + unique = sorted(set(broken)) + return False, "spec/spec.md 路由表引用了不存在的文件:\n " + "\n ".join(unique) + return True, "" + + +def validate_self_check(skill_dir: Path) -> tuple[bool, str]: + """每个 spec/*.md 子文件(非索引)应有 `## 自查` / `## Checklist` 等标题小节。""" + spec_root = skill_dir / "spec" + missing = [] + for md in spec_root.rglob("*.md"): + rel = md.relative_to(skill_dir).as_posix() + if rel in SELF_CHECK_EXEMPT: + continue + body = md.read_text() + if not SELF_CHECK_PATTERN.search(body): + missing.append(rel) + if missing: + return False, "以下 spec 子文件缺少 `## 自查` / `## Checklist` 标题小节:\n " + "\n ".join(sorted(missing)) + return True, "" + + +def validate(skill_dir: str, strict: bool = False) -> tuple[bool, str, list[str]]: + """返回 (ok, summary_msg, warnings)。warnings 只在非 strict 模式下不阻塞。""" skill_dir = Path(skill_dir).resolve() + warnings: list[str] = [] if not skill_dir.is_dir(): - return False, f"目录不存在: {skill_dir}" + return False, f"目录不存在: {skill_dir}", warnings skill_md = skill_dir / "SKILL.md" if not skill_md.exists(): - return False, f"SKILL.md 不存在: {skill_md}" + return False, f"SKILL.md 不存在: {skill_md}", warnings ok, msg = validate_frontmatter(skill_md) if not ok: - return False, msg + return False, msg, warnings name = msg ok, msg = validate_files(skill_dir) if not ok: - return False, msg + return False, msg, warnings + + ok, msg = validate_routing_links(skill_dir) + if not ok: + return False, msg, warnings + + ok, msg = validate_self_check(skill_dir) + if not ok: + if strict: + return False, msg, warnings + warnings.append(msg) - return True, f"skill '{name}' 通过校验(frontmatter + {len(GOSPEC_REQUIRED_FILES)} 个必备文件)" + return True, ( + f"skill '{name}' 通过校验" + f"(frontmatter + {len(GOSPEC_REQUIRED_FILES)} 必备文件 + 路由表完整性" + f"{' + 自查清单' if not warnings else ''})" + ), warnings if __name__ == "__main__": - target = sys.argv[1] if len(sys.argv) > 1 else "." - ok, msg = validate(target) + args = sys.argv[1:] + strict = "--strict" in args + args = [a for a in args if a != "--strict"] + target = args[0] if args else "." + ok, msg, warnings = validate(target, strict=strict) + for w in warnings: + print("⚠️ " + w) print(("✅ " if ok else "❌ ") + msg) sys.exit(0 if ok else 1) diff --git a/spec/01-requirement/lifecycle.md b/spec/01-requirement/lifecycle.md index 8968227..dfc9148 100644 --- a/spec/01-requirement/lifecycle.md +++ b/spec/01-requirement/lifecycle.md @@ -170,3 +170,14 @@ ↓ [季度回顾 → 改进流程] ``` + +## 自查 + +发起 / 推进 / 关闭任何载体时对照: + +- [ ] 选对了载体(依 `README.md` 决策树,没有把 issue 写成 PRD) +- [ ] 状态在"已确认 / 已接受"前不开始编码(门禁) +- [ ] 变更走了对应载体的更新流程,不是直接改主分支文档 +- [ ] 上线后按要求做了复盘(PRD / Epic 强制;P0/P1 issue 走 Postmortem) +- [ ] 关闭时正文末尾给出沉淀文档指针(→ ADR / RFC / Postmortem 链接) +- [ ] 沉淀文档反链回原 issue / PRD / RFC,便于后续追溯 diff --git a/spec/05-coding/README.md b/spec/05-coding/README.md index e8fd9e0..425db8a 100644 --- a/spec/05-coding/README.md +++ b/spec/05-coding/README.md @@ -12,27 +12,31 @@ | 设计构造函数、组合 / 装饰 / 适配 / 重试 / 并发模式 | `patterns.md` | | import 顺序、写注释、设计 struct 字段、结构体生命周期 | `style.md` | -## 技术栈 - -| 领域 | 选型 | -|------|------| -| 语言 | Go 1.21+ | -| Web 框架 | go-kratos/kratos v2 | -| HTTP 路由 | gorilla/mux | -| ORM | gorm.io/gorm + gorm.io/driver/mysql | -| API 协议 | Protocol Buffers v3 + gRPC + HTTP/REST | -| 认证 | Casdoor + JWT (golang-jwt/jwt v5) | -| 配置 | gopkg.in/yaml.v2 | -| 日志 | `log/slog`(推荐,Go 1.21+)/ `zap`;存量项目兼容 `klog` | -| 指标 | prometheus/client_golang(详见 `10-observability/metrics.md`) | -| 追踪 | OpenTelemetry(详见 `10-observability/tracing.md`) | -| 测试 | testing + stretchr/testify | -| API 文档 | swaggo/swag | -| 进程管理 | armorigo/sigaction | -| 日志轮转 | gopkg.in/natefinch/lumberjack.v2 | -| 构建 | Go Modules + Makefile | -| 容器化 | Docker + docker-compose | -| CI/CD | GitHub Actions | +## 推荐技术栈(示例 / 非强制) + +> 以下是参考栈(出自 liaison / liaison-cloud 项目实战)。**项目可按团队约定替换**—— +> 例如 Web 框架可换 gin / echo,认证可换 Auth0 / Keycloak / 自研。 +> **强制的是"分层 / 错误处理 / 测试 / 安全"等约束**,不是具体库选型。 + +| 领域 | 推荐 | 可替换 | +|------|------|-------| +| 语言 | Go 1.21+ | — | +| Web 框架 | go-kratos/kratos v2 | gin / echo / chi | +| HTTP 路由 | gorilla/mux | net/http ServeMux(Go 1.22+)/ chi | +| ORM | gorm.io/gorm + gorm.io/driver/mysql | sqlx / ent / sqlc | +| API 协议 | Protocol Buffers v3 + gRPC + HTTP/REST | OpenAPI + REST | +| 认证 | Casdoor + JWT (golang-jwt/jwt v5) | Auth0 / Keycloak / Hydra / 自研 | +| 配置 | gopkg.in/yaml.v2 | viper / koanf / envconfig | +| 日志 | `log/slog`(推荐,Go 1.21+) | `zap` / 存量项目兼容 `klog` | +| 指标 | prometheus/client_golang | OpenTelemetry metrics | +| 追踪 | OpenTelemetry | Jaeger SDK(已弃用) | +| 测试 | testing + stretchr/testify | gocheck | +| API 文档 | swaggo/swag | grpc-gateway openapi | +| 进程管理 | armorigo/sigaction | 标准库 signal.NotifyContext | +| 日志轮转 | gopkg.in/natefinch/lumberjack.v2 | 系统 logrotate | +| 构建 | Go Modules + Makefile | Bazel / Earthly | +| 容器化 | Docker + docker-compose | podman / k8s | +| CI/CD | GitHub Actions | GitLab CI / Drone / Buildkite | ## 依赖引入原则 @@ -44,13 +48,13 @@ ## 强制约束(不可违反) -- 禁止 `_ = fn()` 忽略错误 +- 禁止 `_ = fn()` 忽略错误(确实想丢弃必须注释说明) - 共享状态必须加锁,测试必须带 `-race` - 错误不重复记录:要么处理,要么向上传播 - 所有涉及 IO 的函数第一个参数为 `context.Context` -- 禁止 `init()` 函数(除 pprof 注册等特殊场景,须注释说明) -- 禁止全局可变变量(配置通过依赖注入传递) -- 禁止裸 `interface{}`(用具体类型或 `any` + 类型断言) +- `init()` 仅允许做注册(pprof / metrics collector / database driver 等纯静态注册),禁止做 IO 或可能 panic 的逻辑 +- 禁止全局可变变量(配置通过依赖注入传递;Prometheus collector 等只读单例除外) +- 避免 `any` / `interface{}` 出现在公共 API 边界(解码 / SDK 适配 / 通用容器等不可避免场景必须就近注释说明并尽快用类型断言收敛) - 禁止忽略 `Close()` 返回的错误 - 禁止 goroutine 中访问未加锁的共享状态 - 禁止硬编码字符串(URL、密钥、端口提取为配置项) diff --git a/spec/09-documentation.md b/spec/09-documentation.md index c95ab43..bfe83cc 100644 --- a/spec/09-documentation.md +++ b/spec/09-documentation.md @@ -131,9 +131,9 @@ Rules: --- -## PR 文档检查项 +## 自查 -提交 PR 时确认: +提交 PR 前对照(与 `07-code-review.md` 的"文档"小节互补): - [ ] 新功能有对应 PRD 或 issue 关联 - [ ] 架构变更有 ADR @@ -141,3 +141,5 @@ Rules: - [ ] `README.md` 如有影响已同步更新 - [ ] `.env.example` 如有新配置项已同步更新 - [ ] 部署配置如有变更已同步更新 +- [ ] 文档归位(PRD/RFC 在 `docs/requirements/` 或 `docs/rfc/`,ADR 在 `docs/adr/`,不散落) +- [ ] 文档之间相互引用而非复制(避免 spec 漂移) diff --git a/spec/10-observability/slo-alerting.md b/spec/10-observability/slo-alerting.md index 15dd008..521c07d 100644 --- a/spec/10-observability/slo-alerting.md +++ b/spec/10-observability/slo-alerting.md @@ -13,6 +13,32 @@ | 数据库 | 查询成功率、P99 延迟 | | 第三方依赖 | 调用成功率、超时率 | +### 可用性目标 → 错误预算速查表 + +直接照抄到 `docs/slo/.md`,避免重复算: + +| 可用性目标 | 月度错误预算 | 周错误预算 | 说明 | +|-----------|-------------|-----------|------| +| 99% | ~7h 18min | ~1h 41min | MVP / 内部工具 | +| 99.5% | ~3h 39min | ~50min | 普通业务后台 | +| 99.9% | ~43min 12s | ~10min 5s | 一般对外服务(推荐起步) | +| 99.95% | ~21min 36s | ~5min 2s | 核心对外服务 | +| 99.99% | ~4min 19s | ~1min | 支付 / 关键链路 | +| 99.999% | ~26s | ~6s | 极少需要,成本陡增 | + +### Burn Rate 告警阈值速查表 + +按 [Google SRE Workbook](https://sre.google/workbook/alerting-on-slos/) 的多窗口多 burn-rate 方案: + +| 检测窗口 | Burn Rate | 多久耗尽 | 告警级别 | 触发例(99.9% SLO) | +|---------|-----------|---------|---------|-------------------| +| 5min + 1h | 14.4× | 2 天 | P0 | 1h 错误率 > 1.44% | +| 30min + 6h | 6× | 5 天 | P1 | 6h 错误率 > 0.6% | +| 2h + 24h | 3× | 10 天 | P2 | 24h 错误率 > 0.3% | +| 6h + 3d | 1× | 30 天 | P3(仅记录) | 3d 错误率 > 0.1% | + +**双窗口要求**:长短窗口都越界才告警,避免单点抖动误报。 + ### SLO 模板 每个对外服务在 `docs/slo/.md` 维护: diff --git a/spec/spec.md b/spec/spec.md index 9c62ba0..c5f3a8f 100644 --- a/spec/spec.md +++ b/spec/spec.md @@ -162,6 +162,26 @@ spec/ --- +## 项目阶段裁剪(按团队 / 流量规模可跳过) + +不是所有规则在所有阶段都强制。Agent 评估是否提议某项规则前,先看团队规模: + +| 规则 | < 5 人 / MVP | 5-20 人 / 成长期 | > 20 人 / 成熟期 | +|------|-------------|-----------------|----------------| +| ADR / RFC / PRD 全套 | issue 即可 | RFC + 简化 PRD | 全套 | +| 完整 Postmortem 模板 | 简短复盘 | 完整 | 完整 + 季度回顾 | +| 多服务 monorepo + CODEOWNERS | 单仓库 | 按需 | 强制 | +| feature flag / 金丝雀 | 直接发布 | 高风险变更走 | 强制 | +| ClickHouse / InfluxDB | 暂不引入 | 按需 | 按需 | +| OpenTelemetry tracing | logging 优先 | 加 trace | 全链路 trace | +| SLO 文档 + burn rate 告警 | 简单可用率告警 | 定 SLO | 多窗口 burn rate | +| 威胁建模 | 安全 checklist | 关键模块做 STRIDE | 全模块 | +| 变更评审 + on-call rotation | PR review 即可 | 引入 on-call | 24/7 on-call | + +**核心约束(下方"核心约束"小节)任何阶段都要遵守**——这些是红线,不随规模放宽。 + +--- + ## Agent 行为约定 1. **开始任何任务前**:查上面的路由表,确定本次需要读哪些文件 @@ -188,10 +208,13 @@ spec/ - 每个目录都被 CODEOWNERS 覆盖 ### 编码 -- 禁止 `_ = fn()` 忽略错误 +- 禁止 `_ = fn()` 忽略错误(确实想丢弃错误必须有注释说明) - 共享状态必须加锁,测试必须带 `-race` - 错误不重复记录:要么处理,要么向上传播 - 所有涉及 IO 的函数第一个参数为 `context.Context` +- `init()` 仅允许做注册(pprof / metrics collector / driver 等),禁止做 IO 或可能 panic 的逻辑 +- 避免 `any` / `interface{}` 出现在公共 API 边界(解码、SDK 适配等不可避免场景除外) +- `utils/` 仅作最后兜底,优先按职责拆 `mathx/`、`strx/` 等 ### API - 所有 API 变更先更新 `.proto`,不直接修改生成代码