docs(阶段0): 更正存量盘点数字,ground truth 改为可独立核验

设计文档 §4 原写「端点 122、DEPRECATED 16、前端调用 78、疑似无人调用约
35%」四项全错:前三项来自漏抓 5 个端点的提取器和只数字面量的前端统计,
35% 是从 (122−78)/122 推的。改为实测值 127 / 17 / 148 条 method+path
(104 条不同路径)/ 23 个无调用 = 18%。减法空间是 18% 不是三分之一,
后续阶段按此排期。

实施计划里的 ground truth 原本就是那个有 bug 的脚本自己产出的,自检退化成
「脚本必须复现自己的 bug」。改为 127 / 17 / 104 并写明独立核验方式。

另:§7 引导语两处改三处;7.3 把「启动时一次性 loadDict」提成衍生约束小节,
附实测数据(200 词逐条 39.4ms vs 一次性 0.98ms,且 loadDict 语义确认为累加)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-06 20:08:53 -06:00
parent 9069b1cdb7
commit e3fe9e1ab9
2 changed files with 51 additions and 18 deletions

View File

@@ -13,8 +13,21 @@
- **不修改 `OnlineJudge/``ojnext/` 任何文件。** 两个旧仓库全程冻结,回滚路径依赖于此。阶段 0 的"砍"是决策层面的,产物是清单不是 diff。 - **不修改 `OnlineJudge/``ojnext/` 任何文件。** 两个旧仓库全程冻结,回滚路径依赖于此。阶段 0 的"砍"是决策层面的,产物是清单不是 diff。
- **不写测试。** 项目既定策略(根 `CLAUDE.md`Do not write new tests。本计划用"跑脚本核对输出数字"替代测试环节。 - **不写测试。** 项目既定策略(根 `CLAUDE.md`Do not write new tests。本计划用"跑脚本核对输出数字"替代测试环节。
- **本机无 PostgreSQL / Redis / Docker。** 任何需要数据库连接的操作只能在服务器上做。 - **本机无 PostgreSQL / Redis / Docker。** 任何需要数据库连接的操作只能在服务器上做。
- 后端端点 ground truth**122oj 74 / admin 48**,其中 **16 个**已由人工标注 `# DEPRECATED: 前端未调用`。任何提取脚本的输出必须与这两个数字吻合,不吻合就是脚本有 bug。 - 后端端点 ground truth**127oj 77 / admin 50**,其中 **17 个**已由人工标注 `# DEPRECATED: 前端未调用`。任何提取脚本的输出必须与这两个数字吻合,不吻合就是脚本有 bug。
- 前端调用路径基线:**78 条**(用字面量 `get("...")` 形式统计,未含模板字符串,实际应 ≥ 78
> **这两个数字不是提取脚本自己产出的**,否则自检就退化成"脚本必须复现自己的 bug"——本计划初稿写的 122 / 74 / 48 / 16 正是这么来的,脚本漏抓了 `tutorial/urls/tutorial.py` 与 `utils/urls.py` 两个文件共 5 个端点ground truth 跟着一起错。
>
> 独立核验方式(不经过任何提取脚本):
>
> ```bash
> cd /home/xuyue/Projects/OJ/OnlineJudge
> cat */urls/*.py utils/urls.py | grep -c "path(" # → 127
> ```
>
> oj/admin 的拆分靠与 `OnlineJudge/oj/urls.py` 的 include 清单逐条对齐核验:该文件共 26 条 `include(...)`,挂载前缀 `api/` 的归 oj、`api/admin/` 的归 admin把每条 include 指向的文件的 `path(` 计数按前缀分别累加 → oj 77、admin 50。注意其中两条不符合"`<app>/urls/{oj,admin}.py`"的命名惯例:`tutorial.urls.tutorial`(目录里但文件名不叫 oj与 `utils.urls`(模块文件,没有 `urls/` 目录)。**提取器必须以 `oj/urls.py` 为唯一入口,不得按文件名白名单猜。**
- 前端调用路径基线:**148 条 `method + path` / 104 条不同路径**(含模板字符串,`${...}` 归一化为 `:param`)。计划初稿写的 78 是只数字面量、不含模板串和泛型 `get<T>(...)` 的旧口径,已作废。
- 反向对账基线:前端调用路径全部能在后端端点全集里找到对应,**orphan 应为 0**。非 0 说明提取器又漏了 urls 文件,或前端有调用死路径的代码。
- 工作目录统一为 `OJ2/docs/spikes/`,脚本用绝对路径接收 `OnlineJudge` / `ojnext` 位置。 - 工作目录统一为 `OJ2/docs/spikes/`,脚本用绝对路径接收 `OnlineJudge` / `ojnext` 位置。
--- ---
@@ -52,10 +65,11 @@
} }
``` ```
实现中踩过的个坑,改脚本时别踩回去: 实现中踩过的个坑,改脚本时别踩回去:
1. 18 处 `path(` 参数换行写,按行扫会漏 → 用括号深度扫描找完整片段。 1. 18 处 `path(` 参数换行写,按行扫会漏 → 用括号深度扫描找完整片段。
2. 行尾注释在 `.as_view()` 的右括号之后,切片到第一个 `)` 会截断 → 片段要延伸到该行行尾。 2. 行尾注释在 `.as_view()` 的右括号之后,切片到第一个 `)` 会截断 → 片段要延伸到该行行尾。
3. 注释可能出现在 `path(` 后、字符串后、逗号后任意位置 → 匹配前先 `replace(/#[^\n]*/g, "")` 剥掉,判 `DEPRECATED` 时仍用原文。 3. 注释可能出现在 `path(` 后、字符串后、逗号后任意位置 → 匹配前先 `replace(/#[^\n]*/g, "")` 剥掉,判 `DEPRECATED` 时仍用原文。
4. **不要按文件名白名单(`oj.py` / `admin.py`)扫 `<app>/urls/` 目录。** 初版这么写,静默漏掉 5 个端点(其中 4 个前端在用):`tutorial/urls/tutorial.py` 文件名不在白名单里,`utils/urls.py` 根本没有 `urls/` 目录、被 `statSync` 的 catch 直接吞掉。改为解析 `OnlineJudge/oj/urls.py` 的 26 条 `include(...)``side` 与路径前缀直接取挂载前缀,`app` 取 Python 模块名首段(不能用目录层数推,`utils.urls` 只有两段)。
- [ ] **Step 1: 确认脚本输出与 ground truth 一致** - [ ] **Step 1: 确认脚本输出与 ground truth 一致**
@@ -64,10 +78,11 @@ cd /home/xuyue/Projects/OJ/OJ2/docs/spikes
bun run extract-endpoints.ts /home/xuyue/Projects/OJ/OnlineJudge bun run extract-endpoints.ts /home/xuyue/Projects/OJ/OnlineJudge
``` ```
预期输出,个数字必须完全一致: 预期输出,个数字必须完全一致:
``` ```
后端端点合计 122 (oj 74 / admin 48) 挂载点 26 个(来自 oj/urls.py 的 include
其中已标 DEPRECATED: 16 后端端点合计 127 (oj 77 / admin 50)
其中已标 DEPRECATED: 17
→ endpoints-backend.json → endpoints-backend.json
``` ```
@@ -161,7 +176,7 @@ cd /home/xuyue/Projects/OJ/OJ2/docs/spikes
bun run extract-frontend-calls.ts /home/xuyue/Projects/OJ/ojnext bun run extract-frontend-calls.ts /home/xuyue/Projects/OJ/ojnext
``` ```
预期:去重后条数 **≥ 78**78 是只数字面量的基线,加上模板串应当更多)。若明显低于 78,说明正则漏了写法,检查 `src/utils/http.ts` 里 http 客户端的实际调用形式再修。 预期:`前端调用点 154 处,去重后 148 条`148 是 `method + path` 去重;只按 path 去重是 104 条)。若明显低于此数,说明正则漏了写法,检查 `src/utils/http.ts` 里 http 客户端的实际调用形式再修 —— 泛型 `get<T>("x")` 是重灾区,不吃泛型会漏掉三分之一
- [ ] **Step 3: 抽查 3 条结果** - [ ] **Step 3: 抽查 3 条结果**
@@ -198,6 +213,8 @@ git commit -m "chore(阶段0): 前端 API 调用提取器"
- [ ] **Step 1: 写对账脚本** - [ ] **Step 1: 写对账脚本**
> 下面是初稿。**以仓库里的 `docs/spikes/reconcile.ts` 为准**,它比初稿多两处必要修正:`key()` 不能剥掉 `admin/` 段(初稿的 `(admin\/)?` 分组会把全部 admin 端点误判成 REVIEW以及新增了反向对账前端调用了但后端查无此端点
```typescript ```typescript
#!/usr/bin/env bun #!/usr/bin/env bun
// 对账后端端点全集与前端调用全集,产出三态清单 // 对账后端端点全集与前端调用全集,产出三态清单
@@ -260,7 +277,7 @@ cd /home/xuyue/Projects/OJ/OJ2/docs/spikes
bun run reconcile.ts bun run reconcile.ts
``` ```
预期:三态之和等于 122。REVIEW 数量若超过 40说明 `key()` 归一化不够,多半是后端 `pattern` 里还有没处理的占位符写法 —— 先抽查几个 REVIEW 行确认是真需人工判还是归一化没做对。 预期:`KEEP 104 / CUT 17 / REVIEW 6 合计 127`,且**不出现** `⚠ 前端调用无对应后端端点` 这行反向对账告警。REVIEW 数量若超过 40说明 `key()` 归一化不够,多半是后端 `pattern` 里还有没处理的占位符写法 —— 先抽查几个 REVIEW 行确认是真需人工判还是归一化没做对。反向告警若非 0先查提取器是不是又漏了 urls 文件,再考虑是不是前端留了死调用。
- [ ] **Step 3: 抽查归一化质量** - [ ] **Step 3: 抽查归一化质量**

View File

@@ -9,7 +9,7 @@
| 仓库 | 角色 | 栈 | 规模 | | 仓库 | 角色 | 栈 | 规模 |
|---|---|---|---| |---|---|---|---|
| `OnlineJudge/` | 后端 REST API + WebSocket | Django 6 + DRF + PostgreSQL + Redis + Dramatiq | 17k 行 Python26 model118 migration122 端点 | | `OnlineJudge/` | 后端 REST API + WebSocket | Django 6 + DRF + PostgreSQL + Redis + Dramatiq | 17k 行 Python26 model118 migration127 端点 |
| `ojnext/` | 前端 SPA | Vue 3 + TypeScript + Vite + Naive UI + Pinia | 37k 行217 文件 | | `ojnext/` | 前端 SPA | Vue 3 + TypeScript + Vite + Naive UI + Pinia | 37k 行217 文件 |
重写的驱动力有三条,均为交付性诉求,非兴趣驱动: 重写的驱动力有三条,均为交付性诉求,非兴趣驱动:
@@ -50,14 +50,17 @@
| 项 | 数值 | | 项 | 数值 |
|---|---| |---|---|
| 后端端点合计 | 122`oj` 74 + `admin` 48 | | 后端端点合计 | 127`oj` 77 + `admin` 50 |
| 其中已由人工标注 `# DEPRECATED: 前端未调用` | 16 | | 其中已由人工标注 `# DEPRECATED: 前端未调用` | 17 |
| 前端实际调用的路径 | 78 | | 前端实际调用的路径 | 148 条 `method + path`104 条不同路径 |
| **疑似无人调用** | **约 35%** | | **疑似无人调用** | **23 个18%** |
| 空 app | `course/``comment/`(均 0 行) | | 空 app | `course/``comment/`(均 0 行) |
| 全站不用的分支 | OI 赛制(所有比赛均为 ACM | | 全站不用的分支 | OI 赛制(所有比赛均为 ACM |
| Python 生态锁定 | 仅 2 处:`jieba``flowchart/views/admin.py` 单文件)、`tree-sitter``ast_checker/`177 行) | | Python 生态锁定 | 仅 2 处:`jieba``flowchart/views/admin.py` 单文件)、`tree-sitter``ast_checker/`177 行) |
> 更正2026-08-06 阶段 0 重跑后):本表原写「端点合计 122oj 74 / admin 48、DEPRECATED 16、前端调用 78、疑似无人调用约 35%」,四项全错。前三项来自一版漏抓了 `tutorial/urls/tutorial.py` 与 `utils/urls.py` 的提取脚本(共漏 5 个端点,其中 4 个前端在用)与一版只数字面量、不含模板串的前端统计;「约 35%」是从 `(12278)/122` 推出来的,两个输入都错。现表为 `docs/spikes/` 三个脚本重跑的实测值,独立核验:`cd OnlineJudge && cat */urls/*.py utils/urls.py | grep -c "path("` → 127。
> **减法空间只有 18%,不是三分之一。** 后续阶段按 18% 排期。
前端网络层集中度高,改动面小: 前端网络层集中度高,改动面小:
| 文件 | 规模 | 处置 | | 文件 | 规模 | 处置 |
@@ -131,7 +134,7 @@ Projects/OJ/
## 7. 已验证的技术假设 ## 7. 已验证的技术假设
处高风险假设已在 Bun 1.3.11 上实测通过spike 代码见 `docs/spikes/` 处高风险假设已在 Bun 1.3.11 上实测通过spike 代码见 `docs/spikes/`依赖清单与 lockfile 已随 spike 源码入库(`docs/spikes/package.json``bun.lock``cd docs/spikes && bun install` 后三个脚本均可直接重跑。
### 7.1 Django 密码哈希兼容(`docs/spikes/pbkdf2-spike.ts` ### 7.1 Django 密码哈希兼容(`docs/spikes/pbkdf2-spike.ts`
@@ -197,7 +200,20 @@ C 与 Python 两套 grammar 均正常。`.wasm` 文件随 npm 包分发(`tree-
jieba.loadDict(Buffer.from("两个整数 9999\n")) jieba.loadDict(Buffer.from("两个整数 9999\n"))
``` ```
**采用方案**:新后端用 `@node-rs/jieba``CUSTOM_WORDS` 列表在启动时拼成一份词典缓冲区一次性 `loadDict`,替代原来的逐词 `add_word` 循环 **采用方案**:新后端用 `@node-rs/jieba`
**衍生约束**`loadDict` 语义已实测确认为**累加**(连续两次 `loadDict` 后先后加入的词都仍然成词),不是整份替换;但它每次调用都要重新解析并合并一遍词典,单条调用的固定开销远大于词条本身。实测 200 个自定义词:
```
逐条 loadDict : 39.4 ms
一次性 loadDict: 0.98 ms 40 倍)
```
因此给后续阶段的实现者:
1. `CUSTOM_WORDS` 必须在**启动时**拼成一份完整的词典缓冲区,**一次性** `loadDict`,不得把 Python 那边的 `for w in CUSTOM_WORDS: jieba.add_word(w)` 逐词循环直译成逐条 `loadDict`
2. 缓冲区格式与 Python jieba 用户词典一致:每行 `"词 词频"`,词频沿用现有的 `9999`
3. 不要在请求路径上调 `loadDict`。词表变更走重建缓冲区 + 重启(或重建整个 `Jieba` 实例)。
## 8. 数据层策略 ## 8. 数据层策略
@@ -233,11 +249,11 @@ jieba.loadDict(Buffer.from("两个整数 9999\n"))
| 阶段 | 内容 | 出口标准 | | 阶段 | 内容 | 出口标准 |
|---|---|---| |---|---|---|
| **0 减法与探路** | 对照 78 个前端实际调用筛查 122 个后端端点,砍掉无人调用者;删除 `course`/`comment` 空壳与 OI 分支;验证 `@node-rs/jieba` | 产出端点清单,长度比 122 少约三分之一 | | **0 减法与探路** | 对照 104 条前端实际调用路径筛查 127 个后端端点,砍掉无人调用者;删除 `course`/`comment` 空壳与 OI 分支;验证 `@node-rs/jieba` | 产出端点清单,REVIEW 归零、每个端点有 KEEP/CUT 裁决(机器初判:可砍 17 个,疑似无人调用 23 个 = 18% |
| **1 骨架** | 建仓、Bun workspaces、`drizzle-kit pull` 拿 26 张表并剪除 `django_*`、拷贝 ojnext 进 `apps/web` | `bun dev` 可启动,能从真实库读出一道题 | | **1 骨架** | 建仓、Bun workspaces、`drizzle-kit pull` 拿 26 张表并剪除 `django_*`、拷贝 ojnext 进 `apps/web` | `bun dev` 可启动,能从真实库读出一道题 |
| **2 判题竖线**(关键) | 最小 auth + 读题 + 提交 → BullMQ → JudgeServer → Bun WS 推回前端;前端仅改对应几个 api 函数 | 一名学生能登录、看题、提交、看到实时判题结果 | | **2 判题竖线**(关键) | 最小 auth + 读题 + 提交 → BullMQ → JudgeServer → Bun WS 推回前端;前端仅改对应几个 api 函数 | 一名学生能登录、看题、提交、看到实时判题结果 |
| **3 铺开** | 74`oj` 端点逐个搬运,搬一个换一个前端 api 函数 | 用户侧全部功能运行在新后端上 | | **3 铺开** | 77`oj` 端点逐个搬运,搬一个换一个前端 api 函数 | 用户侧全部功能运行在新后端上 |
| **4 后台** | 48`admin` 端点,约占全程 40% 工作量 | 后台可用 | | **4 后台** | 50`admin` 端点,约占全程 40% 工作量 | 后台可用 |
| **5 切换演练** | `docker/` 三套 compose单二进制镜像用生产库快照完整演练 | 演练 30 分钟内完成,回滚路径已验证 | | **5 切换演练** | `docker/` 三套 compose单二进制镜像用生产库快照完整演练 | 演练 30 分钟内完成,回滚路径已验证 |
**阶段顺序的理由** **阶段顺序的理由**