贡献者指南:分支、提交、代码风格、测试、数据库迁移、文档同步、spec-as-test 工作流。
本篇是开发流程的唯一规范文档。其他 docs/ 下的文档只描述项目本身(API、内部架构、运维),不重复这里的内容。
| 分支 | 角色 | 推送方式 |
|---|---|---|
main |
稳定发布分支 | 只能 PR 合入 |
dev |
当前迭代集成分支 | 只能 PR 合入 |
feature/<topic> |
新功能 | 开发者推 |
refactor/<topic> |
重构(不改变行为) | 开发者推 |
chore/<topic> |
依赖、工具、CI | 开发者推 |
命名约定:
- 用
/分类型,主题用 kebab-case。 - 主题描述结果,不是过程:
feature/pdf-async-image-upload✅ /feature/fix-pdf-bug❌。 - 一个分支只做一件事。
格式:<类型>(<可选范围>): <动作> <对象>
| 类型 | 用途 |
|---|---|
feat |
新功能 |
fix |
bug 修复 |
refactor |
重构 |
docs |
文档变更 |
test |
测试变更 |
chore |
依赖、工具、CI |
perf |
性能优化 |
要点:
- 主题 ≤ 70 字符,正文按需补"为什么"。
- 中文 / 英文皆可,单仓库内保持一致。
- 一次提交一个原子改动。
# 1. 起分支
git checkout dev && git pull
git checkout -b feature/<topic>
# 2. 开发、小步提交
# 3. 同步上游
git fetch && git rebase dev
# 4. 自检(见 1.4)
# 5. gh pr create → base = dev合并方式由仓库设置决定,不绕过设置。合并后删除本地与远程分支。
-
black src tests/isort src tests通过 -
mypy src无新增报错 -
pytest tests/unit全部通过 - 改动覆盖了对应测试
- 改
src/models/**.py或migrations/db.sql必须配 Alembic 迁移(见 §五) - 触发同步规则的改动已同步对应文档(见 §六)
- 无未使用依赖
- ❌ 直推
main/dev - ❌ PR 中夹带不相关改动
- ❌
--force推已被他人 review 的分支 - ❌
--no-verify跳过 pre-commit hook - ❌ 提交未通过单测的代码
配置集中在 pyproject.toml:
| 工具 | 用途 | 关键配置 |
|---|---|---|
black |
格式化 | line-length=100, target=py310 |
isort |
import 排序 | profile=black |
mypy |
静态类型 | 渐进启用 |
提交前顺序:isort → black → mypy → pytest。
最低 3.10。允许 match、int | str、typing.Self。避免 3.11+ 才有的 LiteralString、tomllib。
| 场景 | 要求 |
|---|---|
| 公共函数 / 方法签名 | ✅ 必须 |
| Pydantic 模型字段 | ✅ 必须 |
| 私有 helper / 局部变量 | ⬜ 推荐 |
| 简单 lambda / 短闭包 | ⬜ 可省 |
返回 None 也显式写出 -> None。
- 不写
except:或except Exception:然后吞掉——必须重新抛出或转成业务异常。 - 模块异常封装为模块自定义类,参考 src/core/mq/exceptions.py。
- 失败时记录足够上下文(task_id、外部资源 key 等)。
- 默认
async def;阻塞调用必须run_in_executor或换异步库。 - 数据库用
aiomysql,HTTP 用httpx,Kafka 用aiokafka。
- 模块:snake_case
- 类:PascalCase
- 函数 / 变量:snake_case
- 常量:UPPER_SNAKE_CASE
- 私有:
_internal
详细规则见 internals/naming_conventions.md。
| 层级 | 目录 | 默认运行 | 外部依赖 |
|---|---|---|---|
| 单元 | tests/unit/ |
✅ | Mock 隔离 |
| 集成 | tests/integration/ |
❌ 需 --run-integration |
真实 MySQL/MQ/向量库 |
| 连通性 | tests/integration/test_connectivity.py |
❌ 需 marker | 仅做 ping |
tests/integration/ 下所有测试由 conftest.py 自动加 @pytest.mark.integration。
配置见 pyproject.toml [tool.pytest.ini_options]。
| Marker | 何时打 |
|---|---|
unit |
默认,可不显式 |
integration |
放 tests/integration/ 即可(自动加) |
connectivity |
仅 ping 类检查 |
real_env |
触及真实 .env 配置;需显式 -m real_env 才跑 |
pytest # 仅 unit
pytest tests/unit/api # 指定子目录
pytest --run-integration tests/integration # 含集成
pytest --run-integration -m real_env # 真实环境禁止:
- ❌ 真实 HTTP(含 LLM、MinerU、内部 API)
- ❌ 真实数据库连接(MySQL、Redis、Qdrant、ES)
- ❌ 真实 MQ producer/consumer
- ❌ 真实文件系统写非临时目录
应当:
- ✅
unittest.mock/pytest-mock替换外部依赖 - ✅ HTTP 用
respx/httpx.MockTransport - ✅ 文件 IO 用
tmp_pathfixture
pyproject.toml 配置 asyncio_mode = "auto",async def test_xxx() 自动识别,无需装饰器。
- 选对层级(unit/integration)
- 文件名
test_<module>.py或test_<behavior>_integration.py - 单元测试无任何真实外部调用
- 关键 mock 有
assert_called_with校验 - 覆盖成功路径 + 至少一个失败路径
- 不依赖测试执行顺序
- 改
src/models/**.py→ PR 必须包含新的migrations/versions/*.py(同步规则db-migration-required,error 拦截)。 migrations/db.sql是 0001 baseline 冻结快照,禁止改动(同步规则baseline-sql-frozen,error 拦截)。
ORM 变更 (src/models/*.py) + migrations/versions/NNNN_*.py
│
│ alembic upgrade head
▼
MySQL.alembic_version 表
每次 alembic upgrade head 是幂等的:读 alembic_version → 应用未运行的 revision → 更新版本号。
# 1. 改 ORM 模型(不改 migrations/db.sql)
vim src/models/parse_task.py
# 2. autogen 生成骨架
alembic revision --autogenerate -m "add retry_count to parsed_log"
# 3. 必须 review 生成的脚本
# - autogen 把 rename 识别为 drop+add(丢数据!)必须手工改为 alter_column(..., new_column_name=...)
# - 索引名变化、CHECK 约束、字符集变化、server_default 可能丢失
# 4. 本地验证
alembic upgrade head
alembic downgrade -1
alembic upgrade head
# 5. 跑测试 → 提 PR
pytest tests/unit -q没有现成 DB 时起临时 docker:
docker run --rm -d --name mig-test -e MYSQL_ROOT_PASSWORD=root \
-e MYSQL_DATABASE=tolink_rag_db -p 3307:3306 mysql:8.0
mysql -h 127.0.0.1 -P 3307 -uroot -proot tolink_rag_db < migrations/db.sql
export ALEMBIC_DATABASE_URL="mysql+pymysql://root:root@127.0.0.1:3307/tolink_rag_db"
alembic stamp 0001NNNN_YYYYMMDD_slug.py
└─┬─┘ └──┬───┘ └─┬┘
│ │ └─ 简短描述(add_retry_count)
│ └───────── 创建日期
└─────────────── 4 位递增编号,写到 revision="NNNN"
禁止分叉:down_revision 必须指向当前 head。两个 PR 并发时,后合入的 rebase 时改 down_revision。
| 命令 | 用途 |
|---|---|
alembic current |
当前库 revision |
alembic history |
完整版本链 |
alembic heads |
列所有 head(正常 1 个) |
alembic upgrade head |
升到最新 |
alembic downgrade -1 |
回退一个 |
alembic stamp <rev> |
仅标记不执行 SQL |
alembic revision -m "msg" |
手写骨架 |
alembic revision --autogenerate |
autogen diff |
- Alembic 必须用同步 driver:
mysql+pymysql://。 - DB URL 优先级:
ALEMBIC_DATABASE_URL环境变量 >src.config.settings.DATABASE_URL。 server_default="0"才会落到 DDL(default=0只在 ORM 层,不进表)。- migrations/db.sql 是 0001 baseline,不是"最新 schema 文档";想看最新完整结构看
scripts/db/init.sql,或跑alembic upgrade head后 mysqldump。
- autogen 把 rename 识别为 drop+add:手工改为
op.alter_column(..., new_column_name=...)。 - "Target database is not up to date":库的
alembic_version≠ head;先alembic upgrade head或alembic stamp head。 Duplicate column name:同时改了 migrations/db.sql 和 migration → 撤掉 migrations/db.sql 的改动(这是baseline-sql-frozen规则要防的事)。
| Workflow | 触发 | 内容 |
|---|---|---|
| migrations-check.yml | PR/push dev,main | ephemeral MySQL → migrations/db.sql → stamp 0001 → upgrade head → 再 upgrade(验幂等) |
| docs-sync.yml | PR/push dev,main | 见 §六 |
机器执行版本:scripts/doc-sync-rules.yaml。本表是人读视图。
| 改动 | 同步位置 | 级别 |
|---|---|---|
src/models/**.py |
docs/api/schemas/mysql.md | ❌ error |
src/models/**.py |
新增 migrations/versions/*.py |
❌ error |
migrations/db.sql |
禁止改动 | ❌ error |
src/core/mq/messages/** |
docs/api/mq_contracts.md + docs/internals/mq.md | ❌ error |
src/core/pipeline/parse_task/** |
docs/internals/parse_task_pipeline.md | ❌ error |
仅保留 error 级规则。内部模块文档同步由 PR 评审兜底,不由 hook 强制。
- error:阻止 commit / merge。
scripts/check_docs_sync.py在 pre-commit 和 CI 上拦截。 - warning:本项目目前不使用 warning 级规则(避免"假阻拦")。如果发现失同步是普遍问题,应升级为 error 并加规则,而不是加 warning。
scripts/doc-sync-rules.yaml # 规则定义
scripts/check_docs_sync.py # 检测脚本
.pre-commit-config.yaml # 本地 hook
.github/workflows/docs-sync.yml # CI 检查
| 时点 | 触发 | 行为 |
|---|---|---|
git commit 前 |
pre-commit hook | error 阻止 commit |
| PR / push | GitHub Actions | error 阻止 merge |
| 手动 | python scripts/check_docs_sync.py --staged |
输出违规清单 |
python scripts/check_docs_sync.py --staged # 检查暂存区
python scripts/check_docs_sync.py --working # 检查工作区
python scripts/check_docs_sync.py --base origin/dev # 检查相对分支
python scripts/check_docs_sync.py --self-check # 仅验证 yaml 合法只在出现新的"代码改动 → 文档失同步会引发集成 bug"关系时新增。流程:
- 编辑
scripts/doc-sync-rules.yaml,加规则(参考已有格式)。 python scripts/check_docs_sync.py --self-check验证。- 同步本节 §5.1 的人读表。
需要绕过时(如 P0 hotfix):本地 git commit --no-verify。事后必须补 follow-up PR 同步文档。
涉及一个新功能从想法到合入的全流程时使用。短小改动(一行 bugfix、一处配置)直接 PR 即可,不必走全流程。
每个 feature 在 .specs/ 下建一个目录,依次产出:
| 文件 | 角色 | 由谁/何时产出 |
|---|---|---|
brief.md |
需求理解 + 待确认项 | 接到需求 → 开发者初稿 → 与提需者迭代到冻结 |
acceptance.feature |
Gherkin 验收契约(机器可消费) | brief 冻结后 |
technical_design.md |
技术方案 | acceptance 冻结后 |
implementation_report.md |
实施记录、决策、遗留 | 开发完成后 |
.specs/ 整目录已 git-ignored——这些文件只活在本地工作目录,不进版本控制。
PR 合并前,把 .specs/<feature>/ 里有长期价值的东西搬出去:
来自 .specs/ |
沉淀到 |
|---|---|
| 关键设计决策、风险、权衡 | PR 描述 |
| 可执行的 Gherkin 场景 | tests/acceptance/features/<name>.feature + tests/acceptance/test_<name>.py + step 实现 |
| 新模块或边界变化 | docs/internals/<module>.md |
| 新对外契约 | docs/api/ 对应文件 |
| 新配置项 | docs/ops/configure.md |
| 数据库表变化 | docs/api/schemas/mysql.md + Alembic 迁移 |
合并后必须 rm -rf .specs/<feature>/。brief.md / technical_design.md / implementation_report.md 是一次性产物,长期留着就是误导信息。
需要查阅历史时:
git log --all --oneline -- '.specs/<feature>/'
git show <commit-sha>:.specs/<feature>/brief.md.ai/skills/ 下有对应自动化 skill:brief-generator / acceptance-generator / technical-design / implementation-execution。在 Agent 模式下按顺序触发即可。
详见 .specs/README.md。
各文档目录的职责划分与按角色的导航入口,以 docs/README.md 为单一来源,不在此重复。本节只讲修改 docs/ 时必须遵守的规则。
每个事实只在一处正式描述。其他位置只放链接,不复制内容。当文档与代码冲突,以代码为准修文档。
- 设计草稿、PRD、迭代计划 →
.specs/ - 会议记录、决策讨论 → PR 描述 / issue
- 临时运维手册 → 运维系统
- 代码实现细节(算法步骤)→ docstring
- 文件名 snake_case,全小写。
- 操作类用动词或场景名词(
deploy.md/configure.md)。 - 契约类直接用名词(
http_contracts.md/mq_contracts.md)。
- ❌ 创建空目录 + 只放一份 README(等于"这里有东西"但实际没有)
- ❌ 同一字段表复制到多个文档(必然漏改)
- ❌ 文档替代代码 docstring(公共接口必须有 docstring)
- ❌ 把 internals 写成教程(教程归 ops 或 brief)
- 项目入口:CLAUDE.md / AGENTS.md(同一份文件的 symlink)
- 用户介绍:README.md
- 文档导航:docs/README.md
- spec-as-test 工作流:.specs/README.md