diff --git a/docs/plans/2026-08-06-phase0-endpoint-inventory.md b/docs/plans/2026-08-06-phase0-endpoint-inventory.md index 93530bc..e4d8514 100644 --- a/docs/plans/2026-08-06-phase0-endpoint-inventory.md +++ b/docs/plans/2026-08-06-phase0-endpoint-inventory.md @@ -13,8 +13,21 @@ - **不修改 `OnlineJudge/` 和 `ojnext/` 任何文件。** 两个旧仓库全程冻结,回滚路径依赖于此。阶段 0 的"砍"是决策层面的,产物是清单不是 diff。 - **不写测试。** 项目既定策略(根 `CLAUDE.md`:Do not write new tests)。本计划用"跑脚本核对输出数字"替代测试环节。 - **本机无 PostgreSQL / Redis / Docker。** 任何需要数据库连接的操作只能在服务器上做。 -- 后端端点 ground truth:**122 个(oj 74 / admin 48)**,其中 **16 个**已由人工标注 `# DEPRECATED: 前端未调用`。任何提取脚本的输出必须与这两个数字吻合,不吻合就是脚本有 bug。 -- 前端调用路径基线:**78 条**(用字面量 `get("...")` 形式统计,未含模板字符串,实际应 ≥ 78)。 +- 后端端点 ground truth:**127 个(oj 77 / admin 50)**,其中 **17 个**已由人工标注 `# DEPRECATED: 前端未调用`。任何提取脚本的输出必须与这两个数字吻合,不吻合就是脚本有 bug。 + + > **这两个数字不是提取脚本自己产出的**,否则自检就退化成"脚本必须复现自己的 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。注意其中两条不符合"`/urls/{oj,admin}.py`"的命名惯例:`tutorial.urls.tutorial`(目录里但文件名不叫 oj)与 `utils.urls`(模块文件,没有 `urls/` 目录)。**提取器必须以 `oj/urls.py` 为唯一入口,不得按文件名白名单猜。** + +- 前端调用路径基线:**148 条 `method + path` / 104 条不同路径**(含模板字符串,`${...}` 归一化为 `:param`)。计划初稿写的 78 是只数字面量、不含模板串和泛型 `get(...)` 的旧口径,已作废。 +- 反向对账基线:前端调用路径全部能在后端端点全集里找到对应,**orphan 应为 0**。非 0 说明提取器又漏了 urls 文件,或前端有调用死路径的代码。 - 工作目录统一为 `OJ2/docs/spikes/`,脚本用绝对路径接收 `OnlineJudge` / `ojnext` 位置。 --- @@ -52,10 +65,11 @@ } ``` -实现中踩过的三个坑,改脚本时别踩回去: +实现中踩过的四个坑,改脚本时别踩回去: 1. 18 处 `path(` 参数换行写,按行扫会漏 → 用括号深度扫描找完整片段。 2. 行尾注释在 `.as_view()` 的右括号之后,切片到第一个 `)` 会截断 → 片段要延伸到该行行尾。 3. 注释可能出现在 `path(` 后、字符串后、逗号后任意位置 → 匹配前先 `replace(/#[^\n]*/g, "")` 剥掉,判 `DEPRECATED` 时仍用原文。 +4. **不要按文件名白名单(`oj.py` / `admin.py`)扫 `/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 一致** @@ -64,10 +78,11 @@ cd /home/xuyue/Projects/OJ/OJ2/docs/spikes bun run extract-endpoints.ts /home/xuyue/Projects/OJ/OnlineJudge ``` -预期输出,两个数字必须完全一致: +预期输出,三个数字必须完全一致: ``` -后端端点合计 122 (oj 74 / admin 48) -其中已标 DEPRECATED: 16 +挂载点 26 个(来自 oj/urls.py 的 include) +后端端点合计 127 (oj 77 / admin 50) +其中已标 DEPRECATED: 17 → 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 ``` -预期:去重后条数 **≥ 78**(78 是只数字面量的基线,加上模板串应当更多)。若明显低于 78,说明正则漏了写法,检查 `src/utils/http.ts` 里 http 客户端的实际调用形式再修。 +预期:`前端调用点 154 处,去重后 148 条`(148 是 `method + path` 去重;只按 path 去重是 104 条)。若明显低于此数,说明正则漏了写法,检查 `src/utils/http.ts` 里 http 客户端的实际调用形式再修 —— 泛型 `get("x")` 是重灾区,不吃泛型会漏掉三分之一。 - [ ] **Step 3: 抽查 3 条结果** @@ -198,6 +213,8 @@ git commit -m "chore(阶段0): 前端 API 调用提取器" - [ ] **Step 1: 写对账脚本** +> 下面是初稿。**以仓库里的 `docs/spikes/reconcile.ts` 为准**,它比初稿多两处必要修正:`key()` 不能剥掉 `admin/` 段(初稿的 `(admin\/)?` 分组会把全部 admin 端点误判成 REVIEW),以及新增了反向对账(前端调用了但后端查无此端点)。 + ```typescript #!/usr/bin/env bun // 对账后端端点全集与前端调用全集,产出三态清单 @@ -260,7 +277,7 @@ cd /home/xuyue/Projects/OJ/OJ2/docs/spikes 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: 抽查归一化质量** diff --git a/docs/specs/2026-08-06-bun-backend-rewrite-design.md b/docs/specs/2026-08-06-bun-backend-rewrite-design.md index 21bc75a..c800a9e 100644 --- a/docs/specs/2026-08-06-bun-backend-rewrite-design.md +++ b/docs/specs/2026-08-06-bun-backend-rewrite-design.md @@ -9,7 +9,7 @@ | 仓库 | 角色 | 栈 | 规模 | |---|---|---|---| -| `OnlineJudge/` | 后端 REST API + WebSocket | Django 6 + DRF + PostgreSQL + Redis + Dramatiq | 17k 行 Python,26 model,118 migration,122 端点 | +| `OnlineJudge/` | 后端 REST API + WebSocket | Django 6 + DRF + PostgreSQL + Redis + Dramatiq | 17k 行 Python,26 model,118 migration,127 端点 | | `ojnext/` | 前端 SPA | Vue 3 + TypeScript + Vite + Naive UI + Pinia | 37k 行,217 文件 | 重写的驱动力有三条,均为交付性诉求,非兴趣驱动: @@ -50,14 +50,17 @@ | 项 | 数值 | |---|---| -| 后端端点合计 | 122(`oj` 74 + `admin` 48) | -| 其中已由人工标注 `# DEPRECATED: 前端未调用` | 16 | -| 前端实际调用的路径 | 78 | -| **疑似无人调用** | **约 35%** | +| 后端端点合计 | 127(`oj` 77 + `admin` 50) | +| 其中已由人工标注 `# DEPRECATED: 前端未调用` | 17 | +| 前端实际调用的路径 | 148 条 `method + path`/104 条不同路径 | +| **疑似无人调用** | **23 个,18%** | | 空 app | `course/`、`comment/`(均 0 行) | | 全站不用的分支 | OI 赛制(所有比赛均为 ACM) | | Python 生态锁定 | 仅 2 处:`jieba`(`flowchart/views/admin.py` 单文件)、`tree-sitter`(`ast_checker/`,177 行) | +> 更正(2026-08-06 阶段 0 重跑后):本表原写「端点合计 122(oj 74 / admin 48)、DEPRECATED 16、前端调用 78、疑似无人调用约 35%」,四项全错。前三项来自一版漏抓了 `tutorial/urls/tutorial.py` 与 `utils/urls.py` 的提取脚本(共漏 5 个端点,其中 4 个前端在用)与一版只数字面量、不含模板串的前端统计;「约 35%」是从 `(122−78)/122` 推出来的,两个输入都错。现表为 `docs/spikes/` 三个脚本重跑的实测值,独立核验:`cd OnlineJudge && cat */urls/*.py utils/urls.py | grep -c "path("` → 127。 +> **减法空间只有 18%,不是三分之一。** 后续阶段按 18% 排期。 + 前端网络层集中度高,改动面小: | 文件 | 规模 | 处置 | @@ -131,7 +134,7 @@ Projects/OJ/ ## 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`) @@ -197,7 +200,20 @@ C 与 Python 两套 grammar 均正常。`.wasm` 文件随 npm 包分发(`tree- 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. 数据层策略 @@ -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` 可启动,能从真实库读出一道题 | | **2 判题竖线**(关键) | 最小 auth + 读题 + 提交 → BullMQ → JudgeServer → Bun WS 推回前端;前端仅改对应几个 api 函数 | 一名学生能登录、看题、提交、看到实时判题结果 | -| **3 铺开** | 74 个 `oj` 端点逐个搬运,搬一个换一个前端 api 函数 | 用户侧全部功能运行在新后端上 | -| **4 后台** | 48 个 `admin` 端点,约占全程 40% 工作量 | 后台可用 | +| **3 铺开** | 77 个 `oj` 端点逐个搬运,搬一个换一个前端 api 函数 | 用户侧全部功能运行在新后端上 | +| **4 后台** | 50 个 `admin` 端点,约占全程 40% 工作量 | 后台可用 | | **5 切换演练** | `docker/` 三套 compose;单二进制镜像;用生产库快照完整演练 | 演练 30 分钟内完成,回滚路径已验证 | **阶段顺序的理由**: