Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ API_PORT=8000
AI_INFERENCE_HOST=127.0.0.1
AI_INFERENCE_PORT=8001
REDIS_URL=redis://localhost:6379/0
DATABASE_URL=mysql+pymysql://labsafe:change-me@localhost:3306/labsafe
wsq/i-01b-database-foundation
DATABASE_URL_FILE=.local-secrets/database-url
DATABASE_SCHEMA=labsafe_dev_local

AI_MODE=mock
AI_TOKEN_FILE=.local-secrets/ai-token
AI_ALLOWED_TENANTS=11111111-1111-4111-8111-111111111111
Expand Down
41 changes: 38 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ jobs:
- run: python -m pip check
- run: python -m unittest discover -s tests/business -t . -v
- run: python -m unittest discover -s tests/protocol -t . -v
- run: ruff check apps packages tests
- run: ruff format --check apps packages tests

- run: ruff check apps packages tests tools/database
- run: ruff format --check apps packages tests tools/database


ai-python311:
runs-on: ubuntu-latest
Expand All @@ -46,7 +48,9 @@ jobs:
- run: python -m pip check
- name: AI environment must not install business or model dependencies
run: |
python -c "import importlib.util; names=['celery','redis','sqlalchemy','torch','paddle','onnxruntime']; assert all(importlib.util.find_spec(n) is None for n in names)"

python -c "import importlib.util; names=['celery','redis','sqlalchemy','alembic','pymysql','argon2','torch','paddle','onnxruntime']; assert all(importlib.util.find_spec(n) is None for n in names)"

- run: python -m unittest discover -s tests/ai -t . -v
- run: python -m unittest discover -s tests/protocol -t . -v

Expand All @@ -63,6 +67,37 @@ jobs:
- run: python -B tools/design/validate_specs.py
- run: git diff --exit-code


persistence-mysql:
runs-on: ubuntu-latest
services:
mysql:
image: mysql:8.0.33
env:
# Disposable CI instance only; never use this test credential for deployments.
MYSQL_ROOT_PASSWORD: ${{ github.run_id }}-${{ github.run_attempt }}-mysql-test-only
MYSQL_ROOT_HOST: '%'
ports:
- 3306/tcp
options: >-
--health-cmd="mysqladmin ping -h 127.0.0.1 --silent"
--health-interval=5s --health-timeout=5s --health-retries=30
env:
APP_ENV: test
CI: 'true'
LABSAFE_MYSQL_INTEGRATION: '1'
LABSAFE_CI_MYSQL_PASSWORD: ${{ github.run_id }}-${{ github.run_attempt }}-mysql-test-only
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: python -m pip install -r requirements/dev.txt
- run: python tools/database/run_mysql_tests.py --ci
env:
LABSAFE_CI_MYSQL_PORT: ${{ job.services.mysql.ports[3306] }}


web:
runs-on: ubuntu-latest
defaults:
Expand Down
77 changes: 70 additions & 7 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
# LabSafe 开发指南(I-01A)

# LabSafe 开发指南(I-01A / I-01B)


## 1. 本阶段边界

所有 Python 服务统一使用 Python 3.11.x;业务与 AI 使用独立虚拟环境和独立进程。
依据见 [运行时决策](docs/02-architecture/07-python-runtime.md)。前端 CI 使用 Node.js 20。

本阶段提供真实 FastAPI/Celery 入口、同协议开发 fixture、配置保护和 CI。
不包含数据库迁移、用户会话、业务 API、持久任务、真实图像处理、训练权重或生产部署。

已提供真实 FastAPI/Celery 入口、同协议开发 fixture、配置保护,以及 I-01B 初始数据库迁移、租户初始化和真实 MySQL 测试。
不包含用户会话、RBAC/仓储、业务 API、持久任务、真实图像处理、训练权重或生产部署。

API 的 /ready 只检查 I-01A 进程配置,不代表数据库或完整系统已就绪。
所有后端进程和前端构建均拒绝 production;AI_MODE 只接受 mock。

Expand Down Expand Up @@ -112,15 +116,66 @@ npm run dev
两个环境变量必须相等且为 dev/test。开发页面始终显示“开发环境,结果不用于安全判断”。
构建命令是 npm run build;production 或不一致环境必须失败。


### 3.5 数据库迁移与初始化(I-01B)

先在独立的开发 MySQL 8.0.16+ 实例创建专用空库 labsafe_dev_local。
由数据库管理员提供仅对该库具有 SELECT/INSERT/UPDATE/DELETE/CREATE/ALTER/DROP/INDEX/REFERENCES 权限的迁移/初始化账户;不要给 API/Worker 或 AI 使用该管理账户。
本阶段只允许本机回环连接;远端 TLS、应用最小权限账户和生产发布不在 I-01B 范围内。

将管理员提供的 mysql+pymysql 连接 URL 保存到 .local-secrets/database-url,包含用户名、URL 编码后的密码、回环地址、端口和准确的库名。
URL 不允许 query 参数;程序只读取 DATABASE_URL_FILE,不读取明文 DATABASE_URL,也不自动加载 .env。
该文件必须仅当前用户可读;不要把真实连接串放在命令行、终端历史、日志、PR 或文档中。

~~~powershell
$env:APP_ENV = 'dev'
$env:DATABASE_SCHEMA = 'labsafe_dev_local'
$env:DATABASE_URL_FILE = (Resolve-Path .local-secrets/database-url).Path
.\.venv-business\Scripts\python.exe -m packages.persistence.cli upgrade
.\.venv-business\Scripts\python.exe -m packages.persistence.cli verify
.\.venv-business\Scripts\python.exe -m packages.persistence.cli bootstrap-tenant --code my_org --name '机构名称' --timezone Asia/Shanghai --username admin --display-name '初始管理员' --reason '首次初始化'
~~~

最后一步交互式输入并确认 12–128 字符密码,不提供项目默认密码。
非交互环境使用 --password-file 指向一次性 UTF-8 秘密文件:内容就是密码,不要额外添加 BOM 或结尾换行,程序不会 trim/规范化密码;成功后由提供该文件的操作者安全移除。
用户名执行 NFKC+casefold;租户、六角色、租户级 safety_admin 授权与脱敏审计在一个事务中提交。
重复初始化同一租户返回错误,绝不重置已有密码;本命令不是登录接口,也不是管理员重置工具。
upgrade 成功不等于结构正确,必须继续 verify;JSON 中 errors 应为空,退出码为 0。
CLI 退出码:0=成功,2=输入/保护检查失败,1=数据库/迁移/结构检查失败;错误不输出连接串或密码。

初始迁移仅允许空库(可以已有 Alembic 版本表);不使用 stamp 绕过保护。
MySQL DDL 非整体事务:中途失败可能遗留部分表,不能承诺自动回滚。
此时保留失败证据,并由操作者确认后丢弃整个专用测试库重建;不要对已有业务库执行重建。
降级会删除全部 43 张业务表,只能在 APP_ENV=test 且 DATABASE_SCHEMA 为 labsafe_test_ 前缀的可丢弃库执行:

~~~powershell
# 仅在已确认的专用测试库配置中执行,绝不能指向业务/开发数据库。
$env:LABSAFE_ALLOW_DESTRUCTIVE_DOWNGRADE = $env:DATABASE_SCHEMA
.\.venv-business\Scripts\python.exe -m packages.persistence.cli downgrade
Remove-Item Env:LABSAFE_ALLOW_DESTRUCTIVE_DOWNGRADE
~~~

本机自动验证不需要配置或连接已有库:

~~~powershell
.\.venv-business\Scripts\python.exe tools/database/run_mysql_tests.py --mysqld 'D:/SQL/MySQL/MySQL Server 8.0/bin/mysqld.exe'
~~~

把路径替换为实际 mysqld 可执行文件。脚本创建临时 data directory、随机回环端口和随机凭据,核对实例身份后才建库;结束后只关闭本次子进程并清理临时数据。
不注册或修改 Windows 服务。Linux 本机模式需已安装 mysqld,并以允许运行 mysqld 的非 root 用户执行;CI 使用隔离 MySQL 容器模式,不依赖本机服务。

## 4. 验证

根目录、业务环境执行:

~~~powershell
.\.venv-business\Scripts\python.exe -m unittest discover -s tests/business -t . -v
.\.venv-business\Scripts\python.exe -m unittest discover -s tests/protocol -t . -v
.\.venv-business\Scripts\python.exe -m ruff check apps packages tests
.\.venv-business\Scripts\python.exe -m ruff format --check apps packages tests

.\.venv-business\Scripts\python.exe -m pytest tests/persistence/test_unit.py --tb=short
.\.venv-business\Scripts\python.exe -m ruff check apps packages tests tools/database
.\.venv-business\Scripts\python.exe -m ruff format --check apps packages tests tools/database

~~~

AI 环境执行:
Expand All @@ -130,8 +185,11 @@ AI 环境执行:
.\.venv-ai\Scripts\python.exe -m unittest discover -s tests/protocol -t . -v
~~~

测试自行在临时目录生成令牌和合成请求,不要求预设 APP_ENV,也不连接 Redis/MySQL/对象存储。

I-01A 的业务/AI/协议测试自行在临时目录生成令牌和合成请求,不要求预设 APP_ENV,也不连接 Redis/MySQL/对象存储。
包含实际 API/AI 子进程 HTTP smoke,不只是 import 非空检查。
I-01B 的真实数据库测试必须运行 3.5 的隔离启动器;直接 pytest tests/persistence 会跳过未配置的 MySQL 用例,不能把跳过算作数据库通过。


设计校验使用独立工具环境,安装 tools/design/requirements.txt 后执行:

Expand All @@ -148,4 +206,9 @@ CI 必须先 --check,不能先生成来掩盖漂移。设计校验不是应用

遵循 [Git 协作规范](CONTRIBUTING.md)。提交特性分支、创建 PR;不直接推送 main,不自行合并或代替独立审查。
不提交虚拟环境、.local-secrets、真实图片、权重或 .env。
下一阶段从 I-01B 专用空库迁移开始;真实模型主线可按 I-ML-01 并行,但不扩大本 PR 验收范围。

I-01B 的实际结果与后续边界见 [验收记录](docs/08-delivery/08-i01b-acceptance.md)。
I-01A PR #5 已合并;I-01B 在 wsq/i-01b-database-foundation 上通过 PR #7 提交,目标为最新 main。数据库/安全变更仍需按 CONTRIBUTING 独立评审;不直接推送 main。
PR #7 的远程 CI 必须运行其自身的 persistence-mysql、业务、AI、设计和 Web 检查,不能用 PR #5 的成绩代替。
下一阶段 I-01C 是仓储、会话/RBAC、幂等与租户事务;真实模型主线可按 I-ML-01 推进,但不以数据库通过替代模型或生产验收。

3 changes: 3 additions & 0 deletions alembic.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[alembic]
script_location = %(here)s/packages/persistence/migrations
prepend_sys_path = .
2 changes: 1 addition & 1 deletion contracts/database-design.sql
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ CREATE TABLE `user_roles` (
`role_id` CHAR(36) NOT NULL,
`laboratory_id` CHAR(36) NULL,
`scope_kind` ENUM('tenant','laboratory') NOT NULL DEFAULT 'tenant',
`scope_key` CHAR(36) GENERATED ALWAYS AS (COALESCE(laboratory_id,'00000000-0000-0000-0000-000000000000')) STORED,
`scope_key` CHAR(36) GENERATED ALWAYS AS (COALESCE(laboratory_id,'00000000-0000-0000-0000-000000000000')) STORED NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_user_roles_0` (`tenant_id`, `id`),
UNIQUE KEY `uq_user_roles_1` (`tenant_id`, `user_id`, `role_id`, `scope_key`),
Expand Down
4 changes: 4 additions & 0 deletions docs/02-architecture/06-data-persistence.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,7 @@ IRR-04:completeUpload用API内部HEAD取得准确versionId/大小/MIME,HEAD
## 6. 初始化和迁移门禁

先 tenants/roles/users,再组织/模板/词典/模型/规则/activation;循环current pointers初始NULL。角色固定六种;bootstrap管理员由一次性CLI输入密码,不将演示账号写进DDL。数据库管理账户只用于migration;API/worker按表权限分配,AI无账户。DDL脚本不能直接在非空生产schema执行;迁移必须可备份、演练且有前向修复方案。

2026-09-29 实现说明:I-01B 已提供 packages/persistence 的 Alembic 0001_initial、只读结构核对和原子租户/角色/管理员初始化。操作与失败恢复见 [开发指南](../../DEVELOPMENT.md),真实 MySQL 证据见 [I-01B 验收](../08-delivery/08-i01b-acceptance.md)。本阶段迁移/初始化凭据只供离线管理 CLI;不代表 API/Worker 已获得业务数据库访问能力。

真实库核对发现 DDL 生成器遗漏了 user_roles.scope_key 的 NOT NULL;生成源与未发布的初始迁移快照已补齐,以保持原数据字典定义,不改变角色 scope 规则。迁移发布后不得修改历史快照;后续设计变更必须新增 revision,并更新当前 head 的核对基准。
4 changes: 4 additions & 0 deletions docs/08-delivery/07-i01a-acceptance.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@

## 3. 明确未完成的后续阶段


以下保留 2026-09-28 的历史边界;2026-09-29 后续实现状态见 [I-01B 验收](08-i01b-acceptance.md),不回写当时的验收结论。


- I-01B/C:专用 MySQL、43 表 Alembic 迁移、仓储、会话、RBAC、幂等与租户事务。
- I-02/I-03:实际巡检命令、Outbox/inbox、Worker→AI→DB、租约/fencing/恢复。
- I-03:真实 spawn 计算子进程、硬超时 kill/join 与故障重建;I-01A仅有可取消的合成延迟。
Expand Down
70 changes: 70 additions & 0 deletions docs/08-delivery/08-i01b-acceptance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# 08.8 I-01B 数据库基础验收记录

日期:2026-09-29。范围:初始迁移、租户/角色/初始管理员 CLI、真实 MySQL 结构与约束核对。
本机验证通过不等于协作者批准、远程 CI 通过或生产可用。

## 1. 分支与前置依赖

本地分支 wsq/i-01b-database-foundation,基于已合并的 I-01A PR #5(main 提交 3a92e09)。
I-01B PR #7 已推送并以 main 为目标;本页保留分支依赖关系,数据库/安全变更仍需两名协作者共同确认。
远程 CI 已运行并通过 business-python311、ai-python311、design-contracts、persistence-mysql 和 web 检查;通过 CI 不替代代码评审。

## 2. 已交付

| 交付项 | 代码入口与约束 |
|---|---|
| 初始 Alembic 迁移 | packages/persistence/migrations/versions/0001_initial.py;43 张业务表、115 个 FK,另有 Alembic 版本表 |
| 不可变迁移输入 | migrations/snapshots/0001_initial.sql/json;执行时不读取可变的 contracts;wheel 包含快照和 revision |
| 连接保护 | database.py;Python 3.11、显式 dev/test、同环境前缀 schema、秘密文件、回环地址、MySQL 8.x 且不低于 8.0.16 |
| 迁移保护 | 非空初始库/视图和 offline 模式拒绝;迁移持有 schema 级命名锁;downgrade 仅允许显式确认的可丢弃 test 库 |
| 初始化 | cli.py / bootstrap.py;租户、固定六角色、租户级 safety_admin、脱敏审计同事务提交;并发初始化串行化,重复租户不重置密码 |
| 密码与账号 | 交互 getpass 或秘密文件;密码 12–128 字符,不 trim/规范化;Argon2id m=65536 KiB、t=3、p=1、salt=16 bytes、hash=32 bytes;用户名 NFKC+casefold |
| 结构核对 | schema.py;表/列顺序、类型、nullability、默认值、生成列、PK/unique/index/FK/check、collation、UTC/strict/FK/unique session 设置 |
| 测试与 CI | tests/persistence、tools/database/run_mysql_tests.py;新增独立 MySQL CI job,不给 AI 安装数据库或密码散列依赖 |

结构比较只规范化 MySQL 的表示差异,不忽略 NOT NULL、CHECK 优先级或 ENUM 字符串的大小写差异。
六角色是 safety_admin、lab_manager、inspector、remediator、viewer、rule_expert;只为初始管理员创建 safety_admin 授权,不自动授予其他角色。
初始化 CLI 不是登录、会话或密码重置实现。

## 3. 本机实际验证

Windows、Python 3.11.4、MySQL Community Server 8.0.33。数据库测试使用新的临时目录、随机回环端口、随机凭据与专用随机 schema。
启动器使用 --no-defaults,并在任何写操作前核对该实例的 datadir/port;没有使用、注册、修改或停止已有 MySQL 服务。
测试结束已删除本次临时 schema/data directory 并关闭本次子进程。

| 检查 | 实际结果 |
|---|---|
| I-01B 测试套件 | 47 项通过:26 项真实 MySQL 用例 + 21 项配置/安全/规范化/快照单元用例 |
| 迁移周期 | upgrade → 核对 → downgrade → re-upgrade → 再核对;43 表、115 FK、head=0001_initial、差异为空 |
| 保护反例 | 非空库 sentinel 保留;未确认 downgrade 拒绝且原结构不变;生产/远端连接/offline/错误 schema/缺密码/额外 URL 参数拒绝 |
| 数据约束 | 跨 tenant、错 item current 指针、重复 scope、owner 同空/同非空、重复 crop、错 run/parent/evidence、非法 attempt/state、受引用父项删除均拒绝;合法路径通过 |
| 拒绝错误码 | FK=1452;父记录 RESTRICT=1451;唯一键=1062;CHECK=3819;严格模式非法 ENUM=1265 |
| 结构校验反例 | 实际修改列长度、默认值、唯一索引、FK、CHECK enforcement、额外索引六类漂移均被发现,恢复后再次无差异 |
| 初始化事务 | 注入 audit 写入失败后所有记录回滚;并发同租户只生成一个管理员;重复调用不改变密码;密码/hash 不进入输出或审计 changes |
| I-01A 回归 | 6 项业务 + 4 项协议 + 15 项 AI 测试通过;协议在独立 AI 环境另行复验通过,不重复计数 |
| 环境与静态检查 | 两环境 pip check 通过;AI 无 SQLAlchemy/Alembic/PyMySQL/Argon2/Celery/Redis/模型依赖;Ruff check/format 通过(50 个 Python 文件) |
| 发布包 | 使用声明的隔离构建依赖构建 wheel,安装到新临时目录后可找到两份快照和 Alembic head,不依赖源码目录 |
| 设计检查 | build_specs --check、148 项 IRR 合成检查、validate_specs(含 41 发布/39 D-FINE 合成检查)通过 |
| CI 配置 | YAML 结构校验通过;PR #7 的新 MySQL job 已在远程运行并通过,未复用 PR #5 的成绩 |

MySQL 8.0.33 只是本次本机/CI 兼容性测试基线,不是生产镜像或安全补丁选型结论。
设计工具仍声明未运行应用/数据库测试,是该工具自身的证据范围;本页单独记录真实数据库结果,二者不能互相替代。

## 4. 本轮发现并关闭的差异

真实 introspection 发现设计字典要求 user_roles.scope_key 非空,但 DDL 生成器遗漏 NOT NULL。
已修复 tools/design/data_model.py、重新生成 contracts/database-design.sql,并同步 0001_initial 快照;数据字典和角色规则没有变更。
MySQL 8 的默认认证所需 RSA 依赖通过 PyMySQL[rsa] 补齐;Argon2id 和时区依赖仅加入业务侧。

## 5. 操作、回滚与后续边界

完整环境准备、upgrade/verify/bootstrap-tenant、秘密文件、退出码、失败处理和命令见 [开发指南](../../DEVELOPMENT.md) 第 3.5 节。
正常代码回滚不授权删除业务库。首迁移 DDL 中途失败可能留下部分表;保留失败证据,仅在操作者确认的可丢弃库重建。
downgrade 删除 43 张业务表,必须 APP_ENV=test 且 LABSAFE_ALLOW_DESTRUCTIVE_DOWNGRADE 与 DATABASE_SCHEMA 完全一致;不是生产恢复方案。
历史迁移发布后不得就地修改快照;新增 revision 并维护对应 head 的核对基准。

- I-01C:业务仓储、session/RBAC、幂等、租户事务、最后管理员保护及角色/实验室授权守卫。
- I-02/I-03:跨资源/JSON 语义一致性、状态机、持久任务、旧租约 fencing 回写拒绝、Worker→AI→DB。
- I-ML/I-05/R:真实 D-FINE-N/CUDA/图片评测、远端 TLS 与生产账户最小权限、备份恢复、部署和人工批准。

以上后续事项未在 I-01B 实现,不能用 FK 检查、schema 通过、开发 fixture 或既有 CI 结果代替验收。
3 changes: 3 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

## 阅读顺序

实现进度:I-01A 工程骨架与 I-01B 数据库基础已在特性分支实现;审批状态与本机测试不能互相替代。
运行入口见 [开发指南](../DEVELOPMENT.md),本轮证据见 [I-01B 验收](08-delivery/08-i01b-acceptance.md)。

1. [文档控制](00-document-control.md):规范边界和变更要求。
2. [产品](01-requirements/01-product-requirements.md)与[范围](01-requirements/01-scope-and-acceptance.md)。
3. [编码入口](02-architecture/04-coding-baseline.md)→[状态机](02-architecture/02-domain-model.md)→[AI进程](02-architecture/03-ai-inference-process.md)→[持久任务](02-architecture/05-durable-jobs.md)→[数据库](02-architecture/06-data-persistence.md)。
Expand Down
Loading
Loading