docs: 阶段 0/1/2 出口标准核验 + 阶段 3 覆盖率对账
核验为逐条实跑,不采信文档声称。阶段 0/1/2 出口标准全部通过, 其中阶段 2 判题链路端到端实测(注册→登录→提交→JudgeServer→WS 推送)。 覆盖率对账:oj 侧 65 条已全部实现,admin 侧 45 条一条未做。 新旧路径不同名(API 已重新设计),对照关系为人工按语义逐条比对。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
87
docs/specs/phase012-verification.md
Normal file
87
docs/specs/phase012-verification.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# 阶段 0 / 1 / 2 出口标准核验
|
||||
|
||||
日期:2026-08-07
|
||||
方法:**逐条实跑,不采信文档声称**。阶段 1 后半段至阶段 3 的实现由外部 agent(Codex)完成,
|
||||
本次核验按外部代码对待,独立验证。
|
||||
|
||||
## 结论
|
||||
|
||||
| 阶段 | 出口标准条数 | 通过 | 结果 |
|
||||
|---|---|---|---|
|
||||
| 0 存量盘点 | 4 | 4 | ✅ |
|
||||
| 1 骨架 | 5 | 5 | ✅ |
|
||||
| 2 判题竖线 | 1(端到端) | 1 | ✅ |
|
||||
|
||||
## 阶段 0
|
||||
|
||||
| 标准 | 实测 |
|
||||
|---|---|
|
||||
| `endpoint-inventory.md` 的 REVIEW 计数为 0 | `grep -c '^| REVIEW |'` → **0** |
|
||||
| 清单顶部记录最终数字 | 「合计 127 —— KEEP 110、CUT 17、REVIEW 0;新后端需实现 110 个」 |
|
||||
| jieba 结论写回设计文档 7.3 | `@node-rs/jieba` 在文档中出现 6 处,7.3 节为实测结论 |
|
||||
| `schema.sql` 入库 | `git ls-files` 命中 |
|
||||
|
||||
## 阶段 1
|
||||
|
||||
| 标准 | 实测 |
|
||||
|---|---|
|
||||
| `@oj2/contract` 为符号链接 | `contract -> ../../packages/contract`(另有 api、web 两个 workspace 链接) |
|
||||
| `schema.ts` 27 张表且无框架表 | `grep -c 'pgTable('` → **27**;`django_*` / `auth_*` 匹配为空 |
|
||||
| `/api/problems` 返回真实数据 | HTTP 200,返回中文标题含 emoji(如「🎮好耶!是大冒险!」),字段完整 |
|
||||
| `apps/web` 构建成功 | `bun run build` 退出码 **0** |
|
||||
| `/dev-problems` 显示 20 道题 | `GET /api/dev/problems` → 200,题目数 **20** |
|
||||
|
||||
### 旧 Chrome 兼容约束(硬约束)专项核验
|
||||
|
||||
学校机房浏览器版本低,`mermaid-legacy` 等 fallback 依赖与 `legacy()` 插件不可动。实测:
|
||||
|
||||
- `apps/web/vite.config.ts` 与 `ojnext/vite.config.ts` 差异 **17 行,全部是新增代理**
|
||||
(`/api2`、`/ws2`、`/public/avatar`),未触碰 `legacy()` 插件与 Chrome 90 兼容配置
|
||||
- 依赖 38 → 39,**新增仅 `@oj2/contract`,删除 0 个**,`mermaid-legacy` 仍在
|
||||
|
||||
约束守住。
|
||||
|
||||
## 阶段 2
|
||||
|
||||
出口标准:一名学生能登录、读公开题、提交代码,并通过 WebSocket 看到真实 JudgeServer 的判题结果。
|
||||
|
||||
实测流程(全部真实执行,非模拟):
|
||||
|
||||
1. **注册** `POST /api/users` → 201
|
||||
2. **登录** `POST /api/auth/login` → 200,`Set-Cookie: oj2_session=<32 字节随机令牌>`
|
||||
3. **会话** `GET /api/me` → 200,返回用户身份
|
||||
4. **读公开题** `GET /api/problems/1004` → 200
|
||||
5. **提交** `POST /api/submissions` → 201,返回 `submissionId`
|
||||
6. **判题** 真实 JudgeServer 执行,返回 `result: 0`(ACCEPTED),两个测试点均带
|
||||
`memory` / `cpu_time` / `output_md5`
|
||||
7. **WebSocket 推送** `ws://localhost:3000/ws/submissions`,订阅后收到三条:
|
||||
|
||||
```
|
||||
{"type":"submission_update","result":6,"status":"pending"}
|
||||
{"type":"submission_update","result":7,"status":"judging"}
|
||||
{"type":"submission_update","result":0,"status":"finished","time_cost":0,"memory_cost":1470464,"score":0}
|
||||
```
|
||||
|
||||
8. **统计回写** 题目 `myStatus` 变为 0(已通过)
|
||||
|
||||
### 附带核验:公开题详情无泄露
|
||||
|
||||
`GET /api/problems/1004` 的响应字段中**不含** `testCaseId`、`testCaseScore`、`answers`、
|
||||
`astRules` —— 学生侧看不到测试点与答案。
|
||||
|
||||
## 遗留待处理项(不影响上述判定)
|
||||
|
||||
1. **`GET /api/dev/problems` 与 `apps/web/src/oj/dev-problems.vue` 仍在。** 二者是阶段 1 的
|
||||
临时链路验证物,阶段 1 计划正文写明「阶段 3 会删」。`apps/web/src/routes.ts` 里的
|
||||
`dev-problems` 路由同样待删。
|
||||
2. **占位用户 `devadmin` 无法登录。** 其 `password` 列是字面量 `'unusable'`(非有效哈希),
|
||||
`raw_password` 为 `'devonly'`。这是阶段 1 为满足 `problem.created_by_id` 外键而造的占位记录,
|
||||
设计如此,不是缺陷。需要管理员账号做本地验证时得另建。
|
||||
3. **判题容器健康状态依赖后端。** `oj2-judge` 的 healthcheck 需要能连上后端心跳端点;
|
||||
API 未启动时 `docker compose ps` 会显示 unhealthy,属预期行为,非故障。
|
||||
|
||||
## 未覆盖
|
||||
|
||||
本次只核验出口标准,**未做代码质量与安全审查**。阶段 2、3 的实现均未经过任务级评审
|
||||
(阶段 0、1 的评审曾逮到 `_id`/`id` 静默碰撞、端点漏抓 5 个、`key()` 误剥前缀等缺陷),
|
||||
相关审查另行进行,见 `phase3-review-authz.md` 与 `phase3-review-leakage.md`。
|
||||
123
docs/specs/phase3-coverage.md
Normal file
123
docs/specs/phase3-coverage.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# 阶段 3 覆盖率对账
|
||||
|
||||
日期:2026-08-07
|
||||
基准:`docs/specs/endpoint-inventory.md` 的 110 条 KEEP 端点
|
||||
对象:`apps/api/src/routes/*.ts` 的 79 个路由 handler(含未提交工作树)
|
||||
|
||||
## 结论
|
||||
|
||||
| | 旧端点 KEEP | 新后端已实现 | 缺口 |
|
||||
|---|---|---|---|
|
||||
| **oj 侧** | 65 | 65(另有 4 条新增) | **0** |
|
||||
| **admin 侧** | 45 | **0** | **45** |
|
||||
| 合计 | 110 | 65 | 45 |
|
||||
|
||||
**oj 侧已全部覆盖,admin 侧一条未做。** 这与设计文档第 11 节的阶段划分一致 —— admin 本就排在阶段 4。
|
||||
|
||||
> 对账方法说明:阶段 0 已定案 API 重新设计,新旧路径不同名(如旧 `/api/problem` → 新 `/api/problems`、
|
||||
> 旧 `/api/pickone` → 新 `/api/problems/random`),**无法按字符串自动匹配**。本表由人工按语义逐条比对,
|
||||
> 判断依据是路由文件归属 + 路径语义 + HTTP 动词。个别条目的对应关系带主观判断,下表逐条列出以便复核。
|
||||
|
||||
## oj 侧逐条对照(65 条)
|
||||
|
||||
| 旧 app | 旧端点 | 新路由 |
|
||||
|---|---|---|
|
||||
| account | `login` | `POST auth/login` |
|
||||
| account | `logout` | `DELETE auth/session` |
|
||||
| account | `register` | `POST users` |
|
||||
| account | `profile` | `GET me` / `GET profiles/:username` / `PUT me/profile` |
|
||||
| account | `profile/fresh_display_id` | `POST me/problem-display-ids/refresh` |
|
||||
| account | `metrics` | `GET users/:id/metrics` |
|
||||
| account | `upload_avatar` | `POST me/avatar` |
|
||||
| account | `user_rank` | `GET rankings/users` |
|
||||
| account | `user_activity_rank` | `GET rankings/activity` |
|
||||
| account | `user_problem_rank` | `GET problems/:displayId/rank` |
|
||||
| achievement | `achievements` | `GET achievements` |
|
||||
| achievement | `achievements/summary` | `GET achievements/summary` |
|
||||
| achievement | `achievements/pending` | `GET achievements/pending` |
|
||||
| ai | `ai/detail` | `GET ai/detail` |
|
||||
| ai | `ai/duration` | `GET ai/duration` |
|
||||
| ai | `ai/heatmap` | `GET ai/heatmap` |
|
||||
| ai | `ai/login_summary` | `GET ai/login-summary` |
|
||||
| ai | `ai/pinned` | `GET ai/pinned` |
|
||||
| ai | `ai/analysis` | `POST ai/analysis` |
|
||||
| ai | `ai/hint` | `POST ai/hint` |
|
||||
| ai | `ai/class_pk` | `POST ai/class-pk-analysis` |
|
||||
| ai | `ai/class_single` | `POST ai/class-analysis` |
|
||||
| announcement | `announcement` | `GET announcements` / `GET announcements/:id` |
|
||||
| class_pk | `class_rank` | `GET rankings/classes` |
|
||||
| class_pk | `user_class_rank` | `GET me/class-rank` |
|
||||
| class_pk | `class_pk` | `POST classes/comparison` |
|
||||
| conf | `website` | `GET site` |
|
||||
| conf | `hitokoto` | `GET quotes/random` |
|
||||
| conf | `class_usernames` | `GET classes/:className/usernames` |
|
||||
| conf | `judge_server_heartbeat/` | `POST judge-server/heartbeat` |
|
||||
| contest | `contests` | `GET contests` |
|
||||
| contest | `contest` | `GET contests/:id` |
|
||||
| contest | `contest/password` | `POST contests/:id/access` |
|
||||
| contest | `contest/access` | `GET contests/:id/access` |
|
||||
| contest | `contest_rank` | `GET contests/:id/rank` |
|
||||
| flowchart | `flowchart/submission`(POST) | `POST flowcharts` |
|
||||
| flowchart | `flowchart/submissions` | `GET flowcharts` |
|
||||
| flowchart | `flowchart/submission/retry` | `POST flowcharts/:id/retry` |
|
||||
| flowchart | `flowchart/submission/detail` | `GET flowcharts/:id` |
|
||||
| flowchart | `flowchart/submission/current` | `GET problems/:id/flowchart/current` |
|
||||
| message | `message` | `GET messages` / `POST messages` |
|
||||
| problem | `problem/tags` | `GET problem-tags` |
|
||||
| problem | `problem` | `GET problems/:displayId` |
|
||||
| problem | `problem/beat_count` | `GET problems/:id/beat-count` |
|
||||
| problem | `problem/similar` | `GET problems/:displayId/similar` |
|
||||
| problem | `problem/author` | `GET problem-authors` |
|
||||
| problem | `problem/yearly_ac` | `GET problems/:displayId/yearly-ac` |
|
||||
| problem | `pickone` | `GET problems/random` |
|
||||
| problem | `contest/problem` | `GET contests/:id/problems` + `GET contests/:id/problems/:displayId` |
|
||||
| problemset | `problemset` | `GET problem-sets` |
|
||||
| problemset | `problemset/<id>` | `GET problem-sets/:id` |
|
||||
| problemset | `problemset/<id>/problems` | `GET problem-sets/:id/problems` |
|
||||
| problemset | `problemset/progress` | `POST problem-set-progress` / `PUT problem-set-progress` |
|
||||
| problemset | `user/badges` | `GET users/:username/badges` |
|
||||
| problemset | `problemset/<id>/badges` | `GET problem-sets/:id/badges` |
|
||||
| problemset | `problemset/<id>/users_progress` | `GET problem-sets/:id/user-progress` |
|
||||
| reaction | `reaction` | `GET problems/:id/reaction` / `POST problems/:id/reaction` |
|
||||
| submission | `submission` | `GET submissions/:id` |
|
||||
| submission | `submissions` | `GET submissions` |
|
||||
| submission | `submissions/today_count` | `GET submissions/today-count` |
|
||||
| submission | `format_code` | `POST code/format` |
|
||||
| submission | `contest_submissions` | `GET contests/:contestId/submissions` |
|
||||
| tutorial | `tutorial` | `GET tutorials/:id` |
|
||||
| tutorial | `tutorials` | `GET tutorials` |
|
||||
| tutorial | `exercises` | `GET tutorials/:id/exercises` |
|
||||
|
||||
### 新增的 4 条(旧后端没有对应)
|
||||
|
||||
| 新路由 | 说明 |
|
||||
|---|---|
|
||||
| `POST submissions` | 旧后端提交走 `POST /api/submission`,与 `GET submission` 同路径不同动词,拆开后成独立条目 |
|
||||
| `PUT submissions/:id` | 判题结果写回 |
|
||||
| `POST achievements/pending/read` | 成就已读标记 |
|
||||
| `GET problems/:id/flowchart/history` | 流程图历史 |
|
||||
| `GET dev/problems` | **阶段 1 的临时验证端点,应删除** |
|
||||
|
||||
## admin 侧缺口(45 条,按 app)
|
||||
|
||||
| app | 条数 |
|
||||
|---|---|
|
||||
| problem | 14 |
|
||||
| problemset | 10 |
|
||||
| conf | 5 |
|
||||
| contest | 3 |
|
||||
| tutorial | 3 |
|
||||
| account | 2 |
|
||||
| achievement | 2 |
|
||||
| submission | 2 |
|
||||
| ai | 1 |
|
||||
| announcement | 1 |
|
||||
| flowchart | 1 |
|
||||
| utils | 1 |
|
||||
|
||||
`problem` 与 `problemset` 两块占了 24 条,超过 admin 缺口的一半 —— 排期时应作为主体。
|
||||
|
||||
## 待处理项
|
||||
|
||||
1. **`GET dev/problems` 是阶段 1 的临时验证端点**,与 `apps/web` 里的临时验证页 `dev-problems.vue` 配套,两者都应在本阶段收尾时删除。
|
||||
2. 本报告的对照关系带人工判断成分,若某条对应有异议,以实际业务行为为准。
|
||||
Reference in New Issue
Block a user