From 64e2fc8f76db2896563352caf5d94aaca04bacc1 Mon Sep 17 00:00:00 2001 From: Haoyu Xi Date: Thu, 15 Oct 2026 23:49:03 +0800 Subject: [PATCH 1/4] =?UTF-8?q?docs(review):=20=E4=B8=93=E5=AE=B6=E5=9B=A2?= =?UTF-8?q?=E9=98=9F=E8=AF=84=E4=BC=B0=EF=BC=88=E9=9D=9E=E5=9C=A8=E9=80=94?= =?UTF-8?q?=E9=9D=A2=EF=BC=89=E2=80=94=E2=80=94=202=20P0=20+=205=20P1=20+?= =?UTF-8?q?=2010=20P2=EF=BC=8C=E5=90=AB=E8=B4=A8=E7=96=91=E5=A4=8D?= =?UTF-8?q?=E6=A0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 席位:dba-optimizer / sre-resilience / code-reviewer 三席并行;跳过 frontend-architect(在途前端已排除、余前端无改动窗口)、perf-profiler(无新增热路径)、product-ux(无旅程变更),均附 artifact 证据。 - P0-1(CONFIRMED,已复现):/api/backup/restore 静默清空 listen_trials 与 hard_sentence_trials —— _BACKUP_TABLES 10 表 vs RestoreReq 8 字段 + pydantic extra=ignore + _replace_tables 无条件 DELETE ⇒ 删光零回灌且返回 ok。与 database.py:1492-1496 修过的同类 bug 是第二次发生。 - P0-2(CONFIRMED,机制经质疑席纠正):退出挂死 + 假成功 —— 真因是 start.py:180 thread.join() 无超时 + 非 daemon uvicorn 线程 + 丢弃 shutdown() 返回值 + 无条件打印「服务已停止」+ 裸调 main() 无 SystemExit;不是席位原报的 Server.shutdown() 无限等待。 - P1-1(CONFIRMED,两席合并):备份 JSON 带走 wb.pair.v1(长期 wb_sync_key + 内网 host),后端白名单被 localStorage 侧门绕过;还原侧还会写回本机。修法需同步改 tests/test_server.py:1031(它把原样透传钉成了契约)。 - 质疑席推翻 1 条:原 P1「桌面壳 os._exit 导致正常退出也报 exit 1」不成立(仅在超时分支硬杀)⇒ 降级 P2。 - 3 条 sre P1 未做对抗复核,标记 UNVERIFIED 并给出补证命令,不得据此下阻塞结论。 - 本评估在 master 新分支 chore/swarm-audit-master 上做,不触碰在途分支。 --- docs/reviews/2026-10-15-swarm-audit-master.md | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 docs/reviews/2026-10-15-swarm-audit-master.md diff --git a/docs/reviews/2026-10-15-swarm-audit-master.md b/docs/reviews/2026-10-15-swarm-audit-master.md new file mode 100644 index 00000000..0424e5a8 --- /dev/null +++ b/docs/reviews/2026-10-15-swarm-audit-master.md @@ -0,0 +1,95 @@ +# 🏛️ Multi-Expert Swarm Audit Report: DeLector「非在途面」全量评估 + +**Date**: 2026-10-15 +**Target Scope**: 仓库**除在途改动面之外**的全部代码(分支 `chore/swarm-audit-master` @ `9d6ce78`) +**Seats Dispatched**: `dba-optimizer`(数据库与存储)· `sre-resilience`(SRE 与可用性)· `code-reviewer`(代码质量与密钥) +**Seats Skipped(artifact 证据,不是"不相关")**: +- `frontend-architect`:本轮目标里唯一有在途改动的前端(`static/js/writer.js` 与 ai-polish 契约)已被显式排除,剩余静态前端在 master 上无改动窗口;3 席位上限内优先持久层与可用性面。 +- `perf-profiler`:目标内无新增热路径 / 内存池 / 锁级并发改动;`asyncio.to_thread` 与信号量由 sre 与 dba(锁范围)覆盖。 +- `product-ux`:目标内无用户旅程 / CLI 变更面(在途旅程改动全在 ai-polish,已排除)。 + +**Findings**: **16** 条(合并后)← 原始 **35** 条(dba 15 / sre 13 / code-reviewer 7)—— 16 < 35,Collapse 成立 +**Read scope**: 见文末「读过的面」;席位的越界发现一律点名移交,未私自判决。 + +--- + +## 🚨 Critical Findings (P0) + +### P0-1 · `/api/backup/restore` 静默清空听力与长难句训练记录(**丢数据 + 零提示**) +- **[dba-optimizer]** [CONFIRMED]:失败场景 = 用户导出备份(数据在文件里)→ 还原 → `listen_trials` 与 `hard_sentence_trials` **永久归零**,接口仍返回 `{"status":"ok"}`、UI 弹「备份还原成功」。 +- 机制(质疑席复核确认):`_BACKUP_TABLES` 现读为 **10 张表**(含 `database.py:1573` `listen_trials`、`:1598` `hard_sentence_trials`),而 `RestoreReq`(`routes/main.py:1747-1768`)只声明 **8 张主库表**字段;全仓 0 处 `ConfigDict` ⇒ pydantic v2 默认 `extra=ignore` ⇒ 前端整份 POST 的这两键被丢弃;`_replace_tables`(`database.py:1895-1909`)对清单里**每张表无条件 `DELETE FROM`**(`database.py:1901`),随后 `payload.get(name) or []` 为空 ⇒ `continue` ⇒ **删光零回灌**。全仓这两张表只有 record/list 两处写入者,**无第二回灌路径**。席位已用真实 `_replace_tables` 在临时库复现(还原前 1/1 → 还原后 0/0)。 +- *Impact*:任何一次还原(换机、误操作回滚)都不可逆抹掉训练记录;与 `database.py:1492-1496` 修过的「清单外 ⇒ 往返静默打回默认」是**同一类 bug 第二次发生** ⇒ 缺的是「清单 ↔ 还原模型」一致性守卫。 +- *Verification*:CONFIRMED(质疑席逐环读码 + 复现)。 +- 最小修法(两处都要):① `RestoreReq` 补 `listen_trials` / `hard_sentence_trials` 两字段;② `_replace_tables` 加前向兼容闸 `if name not in payload: continue`,让"新增备份表忘了加字段"退化成"原样保留"而非"清空";③ 契约测试断言 `set(_BACKUP_TABLES) - set(RestoreReq.model_fields) == set()`。 + +### P0-2 · 退出路径挂死 + 假成功日志 ⇒ 端口被占、下次启动连到旧实例 +- **[sre-resilience]** [CONFIRMED,**机制经质疑席纠正**]:席位原报的根因是 `Server.shutdown()` 里 `wait_for(timeout=None)` 无限等 —— 质疑席核实:本仓 `server_lifecycle.py:65-76` 的 `shutdown()` **根本没调** `Server.shutdown()`,它是 `should_exit=True` + `thread.join(5.0)`,**有界且如实返回 False**。真正的挂死在 `start.py`: + 1. `start.py:180` `thread.join()` **无超时**; + 2. `server_lifecycle.py:60` 起的 uvicorn 线程是 **`daemon=False`**(属实); + 3. `start.py:186` **丢弃** `shutdown()` 返回值,`:187` 却无条件 `print("[提示] 服务已停止。")`; + 4. `start.py:232` 裸调 `main()`、**无 `SystemExit`** ⇒ 解释器退出时 `threading._shutdown()` 再 join 那个非 daemon 线程 ⇒ **「已停止」已打印、进程仍挂、端口 8000 仍占**。下次启动 `start.py:204 _handle_existing_port` 经 `probe_identity` 命中旧实例 → 打印「DeLector 已在运行」并打开**旧实例**(与本仓历史「孤儿 uvicorn 让门禁假绿」同形)。 + ⚠️ `server_lifecycle.py:50` 未设 `timeout_graceful_shutdown` 属实(uvicorn 默认语义无法在本工作区核实 site-packages,但结论不依赖它)。 +- *Impact*:静默残留 + 假成功 + 下次连到旧实例;退出瞬间存在 >5s 在途请求即可触发(TTS 最坏 ~78s、`/api/backup/restore` 持写锁数秒~数十秒、抓取最多 6 跳 × 15s)。 +- *Verification*:CONFIRMED(质疑席给了纠正后的机制与关键行)。 +- 最小修法:① `build_server` 加 `timeout_graceful_shutdown=5`;② `start.py:186-187` 改成 `if not shutdown(server, thread): print("[错误] 服务未能干净退出…", file=sys.stderr); sys.exit(1)`;③ `start.py:180` 的 `thread.join()` 不许裸等。 + +--- + +## ⚠️ Critical Findings (P1) + +### P1-1 · 备份 JSON 会带走 `wb.pair.v1`(长期同步密钥 + 内网地址),绕过后端白名单 +- **[code-reviewer + dba-optimizer]**(**同一缺陷,已合并**)[CONFIRMED] +- 链路(两席各自读码、质疑席复核确认):`database.py:806` `wb_sync_key` = `secrets.token_hex(16)`、**不轮换**,只有手动撤销才失效 → `static/german/workbench.html:1410-1419` 写进 `localStorage["wb.pair.v1"] = {host, key}`(`static/js/encounter.js:1295` 的 `if (p && p.host && p.key)` 反证该键**确含长期密钥**)→ `static/js/cards.js:1017` 按 `wb.` 前缀**全扫**且无 exclude(唯一的排除表 `BACKUP_LS_TRANSIENT` 只作用于 `delector_` 前缀)→ `main.py:1636` `payload["local_storage"] = req.local_storage or {}` **原样透传落盘**(`PrepareBackupReq` 是裸 `Dict[str, Any]`)→ 而 `database.py:1444` 的 `BACKUP_SETTINGS_WHITELIST` **明确排除** `wb_sync_key`,注释写明「备份 JSON 是用户会分享/上传网盘的文件」。还原侧 `cards.js:1075-1076` 还会把备份里的 `wb.pair.v1` **写回本机**(换机还原 = 密钥迁移,也可能被指向他人主机)。 +- *Impact*:长期凭据外泄(拿到文件即可对 `http://:8000` 发 `PUT /api/wb/state` 覆盖学习镜像、投 RTC 信令);后端白名单设计再严也挡不住这条 localStorage 侧门。 +- *Verification*:CONFIRMED。 +- 最小修法(纵深两道):① `cards.js` 的 `backupLocalStorageKeys()` 加 `BACKUP_LS_EXCLUDE = {"wb.pair.v1"}`,**导出与还原两侧都过滤**;② `main.py:1636` 后端按键名黑名单过滤(权威闸必须在后端,前端可被绕过);③ 契约守卫:带 `wb.pair.v1` 打 `/api/backup/prepare` ⇒ 断言下载体不含该键且 `delector_voice` 仍在(**同时必须改 `tests/test_server.py:1031`,它把"原样透传"钉成了契约**)。 + +### P1-2 · 备份不是跨表一致快照 +- **[dba-optimizer]** [CONFIRMED]:`build_backup_payload`(`database.py:1734-1765`)是裸 `conn.execute("SELECT * …")` 循环;`database.py:163/174` 的 `sqlite3.connect(path)` 未传 `isolation_level`(默认 legacy `""`),全模块 **0 处 `BEGIN`** ⇒ 每条 SELECT 各自读"执行瞬间的最新提交";主库与 progress 库还是两个独立连接。与 `database.py:1811-1817` 自己写下的「文件拷贝会拿到不一致快照,故用 backup API」标准**直接冲突**——JSON 这条用户真在用的路径没有同等保证。 +- *Impact*:极端情况下导出/还原得到跨表时间不一致的混合体;无外键约束 ⇒ 孤儿行会原样往返。质疑席补充:**窗口是毫秒级、严重性低**。 +- 最小修法:`build_backup_payload` 内 `BEGIN DEFERRED` → 读完 `COMMIT`(每库内一致;**两库之间仍不可能原子**,这点应写进注释而非假装解决)。 + +### P1-3 ~ P1-5 · sre 席位自报的三条(**未做对抗复核,标记 UNVERIFIED**) +> 协议要求 P1 必须过质疑席;本轮质疑预算只覆盖了 5 条(A–E),以下三条**未验证**,不得据此下阻塞结论,但需尽快补证: +- **[sre-resilience]** [UNVERIFIED] **P1-3**:`core/security.py:177-237` 出网抓取全程**无 `try/except httpx.HTTPError`**,且 `delector/` 全树 **0 处 `exception_handler`** ⇒ DNS/连接/超时一律笼统 500(`routes/encounter.py:380-389` 有 502 人话,两处口径不一致);`timeout=15` 是**每操作**而非总截止 ⇒ 最多 6 跳 ≈ 90s,慢滴服务器可让请求无限期存活。 +- **[sre-resilience]** [UNVERIFIED] **P1-4**:`agent/internal/pythonsvc/supervisor.go:245-267` + `:279` —— `MaxRestarts=5` 是**单次 episode** 配额,`supervise` 每轮崩溃后 `tries` 归零 ⇒ **重启次数无累积上界**、跨 episode **零退避** ⇒ crash-loop 以最快速度反复拉起 uvicorn,且 `Failures()` 永不上报(静默)。 +- **[sre-resilience]** [UNVERIFIED] **P1-5**:`agent/internal/llm/deepseek.go:76 + :160 + :96-106` —— 单条 `Complete` 最坏 `4×120s + 退避 ≈ 483s`;退避是**纯确定性** `500ms<1000 篇才值);`import_encounter_pack` 的 SELECT-then-INSERT 竞态;`/api/search` 全表扫(>1000 篇才值)。 +10. **[sre-resilience]** [UNVERIFIED] 其它:`workbench.html` 轮询无 jitter、`_pollTimer` 从不 `clearInterval`、fetch 无 `AbortSignal.timeout`;`GET /api/wb/state` 无大小闸(与 rtc/sync 的 32KB 不一致);`supervisor.go:390-399` kill 后 `<-done` **无第二超时**;`cmd.Stdout` 直连父 stdout **无轮转**;长写事务期间 `/api/health` 会因 `busy_timeout=5000` 误报 503(Go 探针会据此判启动失败)。 + +--- + +## 💡 Consensus Action Plan(有序) + +1. **先修 P0-1(丢数据)**:`RestoreReq` 补两字段 + `_replace_tables` 前向兼容闸 + 契约测试。这是唯一"用户数据不可逆丢失且零提示"的缺陷,优先级最高。 +2. **再修 P0-2(退出挂死)**:`timeout_graceful_shutdown=5` + `start.py` 尊重 `shutdown()` 返回值 + `thread.join()` 加超时。**注意按质疑席纠正的机制修,不要照席位原报的 `Server.shutdown()` 说法改。** +3. **P1-1(密钥进备份)**:前端 `wb.pair.v1` 双侧过滤 + 后端键名黑名单(权威闸)+ 契约守卫;**同时改 `tests/test_server.py:1031`**,否则新守卫与旧契约对撞。 +4. **P1-2(快照一致性)**:`build_backup_payload` 内 `BEGIN DEFERRED`;注释写明"跨库仍非原子"。 +5. **补证 P1-3~P1-5**(未验证的三条):按席位给的精确命令实跑(抓取 500 vs 502、Go supervisor crash-loop、DeepSeek 退避 jitter),验证后决定是否升/降级。 +6. **P2 批处理**:静默吞错(3 处)→ 密钥扫描进 CI → 两处测试问题 → 持久层那组按阈值"先登记、到规模再动"。 + +## ♻️ Reuse Ladder Check +- 全部修法均**不需新依赖、不需新模块**:复用 `database.py` 既有白名单常量位置与 `_BACKUP_TABLES` 清单、`cards.js` 既有的 `backupLocalStorageKeys()` 单一收集点、`test_audit_hardening.py` 既有契约测试文件、`core/security.py` 既有的 2MB 大小闸(给 encounter 抓取复用)、`asyncio.timeout`(3.11 stdlib)与 `secrets.compare_digest`(已在用)。 +- 唯一"新增"是两处测试断言与一处常量集合,均为仓内既有模式的复制,非新抽象。 + +--- + +## 读过的面(席位自报,节选) +`core/database.py`(全文 + AST 取 `_BACKUP_TABLES` 键集)· `routes/main.py`(backup/restore/settings/TTS/health/wb 段)· `core/security.py`、`core/server_lifecycle.py`、`core/data_dir_bootstrap.py`、`core/vocab_pool.py`、`core/wb_auth.py` · `services/tts.py`、`services/search.py` · `routes/update.py`、`encounter.py`、`sync.py`、`rtc.py`、`search.py`、`listen.py`、`syntax_hard.py`(部分)· `desktop.py`、`start.py`、`server.py` · `static/js/cards.js`、`encounter.js`、`core.js` 及 `static/js/*.js` 全量检索 · `static/german/workbench.html`(wbsync/RTC/TTS/导入导出段)· `agent/internal/{llm/deepseek.go, job/runner.go, pythonsvc/supervisor.go}` · `.gitignore`、`.githooks/pre-commit`、`DeLector.spec`、`package_windows.py`、三份 workflow · 真实库 `delector.db`/`progress.db`(**只读 `EXPLAIN QUERY PLAN`**)+ 临时构造库(20k 卡 / 2k 篇)做规模测量。 + +**越界移交(未判)**:`services/tts.py` 的协议伪装(伪造 Chromium Cookie/Origin)→ security 席位;`WbStateReq.payload` 裸 `Dict` 导致的 `fsrs_s` 漏计 → data-correctness;`X-WB-Key` 明文过 LAN 的信任模型(现有设计注释自述为"单用户自用"取舍)→ 产品/安全决策。 From bb70c5f3ff131aad5a796a43f390d9408141d054 Mon Sep 17 00:00:00 2001 From: Haoyu Xi Date: Fri, 16 Oct 2026 00:06:02 +0800 Subject: [PATCH 2/4] =?UTF-8?q?docs(review):=20=E9=87=8D=E6=9E=84=E5=80=99?= =?UTF-8?q?=E9=80=89=E8=AF=84=E4=BC=B0=EF=BC=88LOC=20=E5=AE=9E=E6=B5=8B=20?= =?UTF-8?q?+=20=E4=B8=A4=E5=B8=AD=E5=AE=9A=E6=80=A7=E6=8E=92=E5=BA=8F?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 核心结论:最大的文件恰恰最不该动 —— core_dict_ext(3978)/official_vocab(3064)/official_vocab_rich(3049)/encounter_seed_dict(1493)/prep_dict(933) 全是 DATA-BLOB(0 def、auto-generated 已标注);拆数据需同步 3 个交付配置面 + tests/test_server.py 的 data_dict_modules 硬编码守卫(漏登记 = 本地全绿、打包后 ModuleNotFoundError)。 - 真正该动:core/database.py(2809 / 86 def / 9 组职责 / 32 处被 import,且与 nlp_engine/processor 有已知双向耦合)⇒ 但须在 P0-1 修完并补契约测试后再切。 - 前端最高价值且低风险:cards.js 的备份+LS 键扫描 与 workbench.html:4347-4472 是同一关注点的两套实现、两边各自硬编码 wb. 前缀 ⇒ 正是 P1-1(密钥经前端备份绕过后端白名单)的结构性成因 ⇒ 键集合单点化(不是拆文件)。 - 前端最痛但别先动:workbench.html(4730 单文件 PWA,塞了种子数据/g2p/存储/wbsync/WebRTC/IndexedDB/TTS/FSRS/短码同步/导入导出/PWA)—— 拆文件会让 4 个测试文件全红(含整文件位置断言与按行号钉 wbsync.push 的探针)⇒ 先把探针改成按段读入。 - 在途冲突:routes/main.py(3115/70 routes)、services/writing.py(1424)、writer.js(ai-polish 段) 一律等各自计划落地。 - 不建议动还含:encounter.js/reader.js(内聚)、listen-lab.js/hard-sentences.js(被模块图守卫三重钉死 + 有意为之的复制)、a1_cards.js(拆标记=重写渲染)、core.js(是收敛目标)。 --- .../reviews/2026-10-15-refactor-candidates.md | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 docs/reviews/2026-10-15-refactor-candidates.md diff --git a/docs/reviews/2026-10-15-refactor-candidates.md b/docs/reviews/2026-10-15-refactor-candidates.md new file mode 100644 index 00000000..8495354e --- /dev/null +++ b/docs/reviews/2026-10-15-refactor-candidates.md @@ -0,0 +1,82 @@ +# 重构候选评估(DeLector,非在途面) + +**Date**: 2026-10-15 · 分支 `chore/swarm-audit-master` @ `64e2fc8`(不碰在途分支) +**方法**:LOC 实测(`wc -l`)+ 两个侦察席位按「职责数 / 耦合 / 重复 / 在途冲突」定性排序 +**核心结论**:**最大的文件恰恰最不该动**(前三大都是纯数据),真正该动的是 `core/database.py`(2,809 行 / 86 def / 9 组职责 / 32 处被 import)与前端**备份键集合的双实现**(与 P1-1 同源)。 + +--- + +## 0. 先纠正指标:LOC 大 ≠ 该重构 + +| 文件 | LOC | 判定 | 处置 | +|---|---|---|---| +| `delector/data/core_dict_ext.py` | 3,978 | **DATA-BLOB**(0 def、1 个 `CORE_VOCAB_EXT`、auto-generated 已标注) | 不动 | +| `delector/data/official_vocab.py` | 3,064 | **DATA-BLOB**(文档明写纯数据/零函数/零副作用) | 不动 | +| `delector/data/official_vocab_rich.py` | 3,049 | **DATA-BLOB**(富字段 side-car) | 不动 | +| `delector/data/encounter_seed_dict.py` | 1,493 | **DATA-BLOB**(真源与重建命令已写明) | 不动 | +| `delector/data/prep_dict.py` | 933 | **DATA-BLOB**(0 def) | 不动 | + +**隐藏成本(为什么不拆数据)**:`tests/test_server.py` 的 `data_dict_modules` 是**硬编码集合**(当前 16 个,`:4582-4611`);每新增一个数据分片 MUST 同步 **3 个交付配置面**(Windows PyInstaller / Linux+macOS workflow / Android 拷贝打包)+ 这一个守卫集合;该测试自述「本地全绿、打包后 ModuleNotFoundError」(`:4570-4577`)。⇒ **纯数据文件不得仅因 LOC 大就拆**。 + +--- + +## 1. 后端候选榜(值不值得动,降序) + +| # | 文件 | LOC | 类型 | 职责 | 被 import | 在途冲突 | 一句话建议 | +|---:|---|---:|---|---|---:|---|---| +| 1 | `delector/core/database.py` | 2,809 | LOGIC-GOD-FILE | 9 组 / **86 def** | **32** | 否(API 被在途面用) | **最值**:按 连接/迁移 → 领域仓储 → 备份 → 导出/种子 切,保留兼容 façade | +| 2 | `tests/test_server.py` | 7,271 | TEST-BLOB | 14 组 / ≥301 用例 | 0 | ⚠️ 含 ai-polish 测试段 | 值:**按被测面原样搬用例**(不改用例本身),润色段等在途落地 | +| 3 | `delector/nlp_engine/processor.py` | 661 | MIXED | 4 组 / 11 符号 | 6 | 间接 | 值:切「模型生命周期 / CEFR 策略数据 / 文本流水线」,先冻结加载接口 | +| 4 | `tests/test_german_workbench.py` | 3,913 | TEST-BLOB | 8 组 / 105 用例 | 0 | 否 | 值:按 TTS / 核心词 / 队列 / 迁移 / 同步 / rich 回填 切 | +| 5 | `delector/nlp_engine/syntax_tree.py` | 1,545 | MIXED | 4 组 / 11 符号 | 7 | 否 | 值:先分 分类表 / 拓扑场 / Clause AST;模型加载与 `processor` 统一边界 | +| 6 | `delector/nlp_engine/linguistics.py` | 1,543 | MIXED | 4 组 / 13 符号 | 6 | 间接 | 值:按 不规则动词 / 复合词 / 扩展词典 / 介词矩阵 切(先数据后算法) | +| 7 | `tests/test_desktop_shell.py` | 2,664 | TEST-BLOB | 8 组 / 89 用例 | 0 | 否 | 值(独立批次):按 日志/退出/托盘/GUI 启动/server-only/AppID 切 | +| 8 | `delector/routes/main.py` | 3,115 | LOGIC-GOD-FILE | 12 组 / **70 routes** | 3 | ⚠️ **直接重叠** | **等在途落地后**:按领域 router 切;当前严禁碰 ai-polish/SSE 窗口 | +| 9 | `delector/services/writing.py` | 1,424 | MIXED | 4 组 / 27 def | 7 | ⚠️ **直接重叠** | **等裁判集落地后**:按 语法规则族 / 聚合器 / A1 格式裁判 切 | +| 10 | `delector/data/a1_hoeren_dict.py` | 1,547 | MIXED | 4 组 / 3 def + 数据表 | 3 | 否 | 等第三套同类引擎或下次改判分:抽与 Lesen **完全同构**的查询/脱敏/判分(`:1423/:1436/:1475` vs Lesen `:1103/:1116/:1148`),**数据不动** | + +**候选 #1 `database.py` 的证据**:86 个顶层函数横跨 —— 路径/连接与双库建表迁移(`:46-581`)· 设置与鉴权(`:585-843`)· 文章与 encounter 种子(`:846-1209`)· Anki 与音频缓存(`:1212-1432`)· 备份/还原(`:1432-1910`)· 考试/训练记录(`:1928-2196`)· 词汇契约与富字段 …… 且与 `nlp_engine/processor.py` 存在**已知双向耦合**(`core/database.py:23` ⇄ `nlp_engine/processor.py:16`)。 + +--- + +## 2. 前端候选榜 + +| # | 文件 | LOC | 类型 | 职责 | 重复/风险 | 在途冲突 | 一句话建议 | +|---:|---|---:|---|---|---|---|---| +| 1 | `static/js/cards.js` | 1,324 | 页面脚本 | 42 函数 / 23 onclick / 3 段 | **备份 + LS 键扫描「第二实现」** | 无 | **最值**:键集合单点化(不是拆文件) | +| 2 | `static/index.html` | 3,158 | 页面壳(0 JS) | 9 view / **214 处 onclick** | 全局函数名与 markup 硬耦合 | 无 | 中风险:按 view 拆标记,**必须保留** `main.js` 的 window 接线面 | +| 3 | `static/js/companion.js` | 782 | 页面脚本 | 12 处裸 localStorage | 键名硬编码 10 个 | 无 | **纯收益**:键名收进常量表 + 注册进备份白名单 | +| 4 | `static/js/main.js` | 1,203 | 共享/接线层 | 25 函数 / 1 个 `Object.assign(window,…)`(`:958`) | 214 onclick 的**唯一接线点** | 无 | 先加守卫再动:exposer 是全站交互单点故障 | +| 5 | `static/german/workbench.html` | 4,730 | **单文件 PWA** | 1 内联 script(822-4620)+ 内嵌 SW;115 函数 | 备份第 2 处、TTS、api 裸 fetch 20 处 | 无 | **最痛但别先动**(见下) | +| 6 | `static/js/writer.js` | 1,657 | 页面脚本 | 47 函数 / 5 段 | — | ⚠️ **ai-polish 段 1089-1487** | 等 `a1-judge-expansion` 落定 | +| 7 | `static/js/encounter.js` | 1,485 | 页面脚本 | 61 函数 / 10 段 | 少 | 无 | 不值:边界已清 | +| 8 | `static/js/reader.js` | 1,403 | 页面脚本 | 40 函数 / 10 段 | TTS 第 4 处(ShadowPlayer) | 无 | 不值:段内聚;TTS 是**合并**不是拆文件 | + +**候选 #1 的真实价值(与 P1-1 同源)**:`cards.js:1005-1008` 硬编码 `BACKUP_LS_PREFIX="delector_"` / `BACKUP_LS_WORKBENCH_PREFIX="wb."` / `BACKUP_LS_TRANSIENT`,`:1010` 手扫 localStorage;而 `workbench.html:4347/4362/4382/4386/4441` 是**同一关注点的第二套实现**,两边**各自硬编码 `wb.` 前缀** ⇒ 任一侧加键,另一侧静默漏备份。这正是 P1-1(密钥 `wb.pair.v1` 经前端备份绕过后端白名单)的结构性成因 ⇒ **修 P1-1 时顺手把键集合单点化**,收益最大、风险最低。 + +**候选 #5 为什么别先动**:`workbench.html` 单文件内塞了种子词数据(825-905)· g2p IPA 引擎(906-984)· 存储与键表(1031-1040)· 归一迁移(1041-1350)· wbsync(1359-1451)· WebRTC/RTC(1452-1686)· IndexedDB(1687-1850)· TTS 三层(1936-2284)· FSRS-6(2285-2372)· 复习/自测/统计/词库 · LAN 短码同步(3863-4093)· 导入导出(4094-4503)· PWA(4635-4729)—— **但拆文件会让 4 个测试文件全红**:`test_german_workbench.py`(28 处引用,含「`index("const tryServer") < index("const tryOnline")`」这类**全文位置断言**)、`test_workbench_tokens.py`(20)、`test_a1_workbench_source.py`(7)、`test_frontend_module_graph.py:401-433`(按**行号**钉 `saveXxx` 挂 `wbsync.push()`)。⇒ **先做:把整文件/行级探针改成按段读入**,再谈切分。 + +--- + +## 3. 明确不建议动 + +**后端**:五个纯数据模块(见 §0)· `a1_lesen_dict.py`(应与 Hoeren **配对**抽公共逻辑,不单独拆)· `tests/test_ci_hardening.py`(1,380,等 >2,000 或新增第三类门禁)· `tools/verify_windows_portable.py`(924,职责单一)· `agent/internal/job/encounter.go`、`pythonsvc/supervisor.go`(458/449,不因 Python 重构顺带动 Go 门禁)。 + +**前端**:`encounter.js` / `reader.js`(段内聚,拆只增跳转成本)· `listen-lab.js`、`hard-sentences.js`(被 `test_frontend_module_graph.py:503-573` **三重钉死**:非孤岛 / 导出齐全 / 无环;且 `listen-lab.js:13-16` 明写「自管播放队列、不重构 ShadowPlayer」是**有意为之**的复制)· `a1_cards.js`(38 处 onclick 全靠 innerHTML,拆标记=重写渲染,收益为负)· `core.js`(它是**收敛目标**,不是切分对象)。 + +--- + +## 4. 建议的重构顺序(与在途面 / P0-P1 协调) + +1. **现在就做(与 P0/P1 同批,纯收益)**:前端**备份键集合单点化**(`cards.js` ↔ `workbench.html` 合一处)+ `companion.js` 键名常量表 —— 直接消掉 P1-1 的结构性成因。 +2. **P0-1 修完之后**:`core/database.py` 按职责切(**切之前先补契约测试**:`_BACKUP_TABLES` ↔ `RestoreReq` 一致性、备份往返、白名单),否则切模块会顺手丢掉不变量。 +3. **在途落地之后**:`routes/main.py` 按领域 router 切(避开 ai-polish/SSE 窗口)· `services/writing.py` 按规则族切(等裁判集第二轮)。 +4. **独立批次**:测试文件按被测面切(`test_server.py` / `test_german_workbench.py` / `test_desktop_shell.py`)—— **只搬归属、不改用例**。 +5. **等条件**:`workbench.html`(先把整文件/行级探针改按段读入)· `a1_hoeren/lesen_dict` 的公共 exam dataset engine(等第三套同类或下次同时改判分)。 +6. **永不**:纯数据模块(除非生成耗时/多人冲突成为**实测**问题)。 + +## 5. 打包面结论(重构前必读) + +- **前端拆文件**:`static` 整目录 add-data(`package_windows.py:324`、`build-release.yml:107/206`)⇒ 打包**零改动**;`APP_NEEDLES` 不含前端 ⇒ 零影响;会红的是 `test_frontend_module_graph.py`(新模块须被 import、不得成环、不得空)。 +- **后端拆/新增模块**(尤其 `delector/data/*`):MUST 同步 **3 个交付配置面 + 1 个守卫集合**(`tests/test_server.py` 的 `data_dict_modules`),否则「本地全绿、打包后 ModuleNotFoundError」。 +- `index.html` 动标记时**别碰** `:40` 的版本串与 `sw.js:3` 的 `CACHE_NAME`(`test_writer_mobile.py` 断言二者一致)。 From d93089bcff06257f4120e51194a7ecaaa20b4b63 Mon Sep 17 00:00:00 2001 From: Haoyu Xi Date: Fri, 16 Oct 2026 01:09:57 +0800 Subject: [PATCH 3/4] =?UTF-8?q?docs(review):=20=E9=A1=B9=E7=9B=AE=E7=BB=93?= =?UTF-8?q?=E6=9E=84=E4=B8=8E=E6=96=87=E6=A1=A3=E8=B4=A8=E9=87=8F=E8=AF=84?= =?UTF-8?q?=E4=BC=B0=EF=BC=884=20P1=20+=209=20P2=EF=BC=8C=E6=97=A0=20P0?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 两席并行(code-explorer 结构 + code-reviewer 文档),只读未改码;18 条原始发现合并为 13 条。 P1-1 测试库写进仓库根:conftest.py:30-31 与 tests/test_get_endpoints_with_writes.py:37-38 用裸相对文件名 ⇒ 落 CWD;而 db_cleanup.py:72 的 ROOT_DB_NAMES 只比 delector.db/progress.db ⇒ test_*.db 在守卫视野外(*.db 被 ignore ⇒ 可无限期无人发现),且根目录有库会让 test_save_wb_state_cost.py 拒绝运行 ⇒ 本地红/CI 绿的迁移闸假象。 P1-2 docs/agents/ops.md:53-57 数据目录口径与 v5.17.0 事实相悖(且与本文件 47-51 自相矛盾)⇒ agent 会被误导在仓库里跑 start.py,触发 bootstrap_data_dir 把真实 delector.db 改名 .bak 并复制走。 P1-3 architecture.md:202/:93-94 宣称 POST /api/settings 无鉴权,实测 routes/main.py:1500-1502 第一行就是 _require_localhost ⇒ 会造成重复加固或误报安全结论。 P1-4 门禁基线停在 2026-08/09(352 / 719 / 10 探针 vs 实测 122 文件 / 1986 条 / 29 探针)+ 前端模块数三处三个数(README 11 / architecture 10 / 实测 23)。 P2 含:CLAUDE.md 是缺路由短桩(AGENTS.md:3 自称同形态但 GEMINI.md 才指 AGENTS)、Inbox 路径两套写法(99-Inbox vs 08-Inbox 四文件两地址)、archive 4 份文件违反命名规则、design-system.md 全仓零入口且 7 token/5 色值与 CSS 逐个 grep 零命中、README Python badge 3.10 无支撑、根目录/包结构描述缺项、scratch 50MB 无修剪、agent/ 一词三义、WORKMEMORY INDEX:16 vs :39 矛盾、README Tests 徽章口径未定义。 合理部分(两席共识):README 版本面最健康且有机械闸、单一真相源纪律到位、docs/README.md 是单一契约、.gitignore 分层设防带理由、未发版功能诚实用限定表述、测试隔离基础设施方向正确(唯一漏口即 P1-1)。 --- ...026-10-16-repo-structure-and-docs-audit.md | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 docs/reviews/2026-10-16-repo-structure-and-docs-audit.md diff --git a/docs/reviews/2026-10-16-repo-structure-and-docs-audit.md b/docs/reviews/2026-10-16-repo-structure-and-docs-audit.md new file mode 100644 index 00000000..1b8176b2 --- /dev/null +++ b/docs/reviews/2026-10-16-repo-structure-and-docs-audit.md @@ -0,0 +1,66 @@ +# 项目结构与文档质量评估(DeLector,master @ `bb70c5f` 之前) + +**Date**: 2026-10-16 · 分支 `chore/swarm-audit-master`(评估专用,不碰在途分支) +**席位**:`code-explorer`(结构清点)+ `code-reviewer`(文档/README 质量)。两席均只读、未改任何文件。 +**Findings**:**13** 条(合并后)← 原始 **18** 条(结构 4 / 文档 14)。**无 P0**:入口文档的版本面与 `APP_VERSION`/git tag 一致且有机械闸守护。 + +--- + +## 一、P1(4 条,都会把后续 agent/用户引向错误动作) + +### P1-1 · 测试库写进仓库根:相对路径副作用 + 会话守卫看不见 `test_*.db` +**[code-explorer]** 证据:`conftest.py:30-31` `os.environ.setdefault("DATABASE_PATH", "test_conftest_default.db")`(相对路径 ⇒ 解析到 CWD);`tests/test_get_endpoints_with_writes.py:37-38` 同样裸文件名;而 `tests/db_cleanup.py:72` 的 `ROOT_DB_NAMES = ("delector.db","progress.db")` 与 `tests/conftest.py:38-56` 的会话守卫**只比这两个名字** ⇒ `test_*.db` 完全在守卫视野外,而 `*.db` 被 `.gitignore` 挡住 ⇒ 泄漏可无限期无人发现。 +失败场景:① `delector.db`/`progress.db` 落根是**已知旧落点**(`core/data_dir_bootstrap.py:108-115` `legacy_data_dir()`,首启会被搬去 `%LOCALAPPDATA%`),但 `test_*.db` **没有任何设计理由**落在根;② 仓库根有库会让 `test_save_wb_state_cost.py` **拒绝运行**(`tests/conftest.py:16-19` 明写)⇒ **本地红 / CI 绿**的迁移闸假象。 +修法:`conftest.py:30-31` 与 `test_get_endpoints_with_writes.py:37-38` 改 `tmp_path_factory`/显式临时目录**绝对路径**;`db_cleanup.ROOT_DB_NAMES` 扩为含 `test_*.db` 让会话守卫看住。 + +### P1-2 · `docs/agents/ops.md:53-57` 数据目录口径与 v5.17.0 事实相悖(且与本文件 47-51 自相矛盾) +**[code-reviewer]** 漂移:`ops.md:54-55` 写「桌面端 `DATA_DIR` = 仓库根、桌面端不设该变量」;但同文件 `:47-51` 写着「真实跑 `start.py` 会触发数据外置迁移,把仓库根的库改名 `.bak-<时间戳>` 并复制到 `%LOCALAPPDATA%\DeLector`」,`README.md:101-105` 亦同。 +失败场景:agent 按 `:54` 判断"桌面数据在仓库根"→ 在仓库里跑 `start.py` 做 dev 验证 → 触发 `bootstrap_data_dir` 把**真实 `delector.db` 改名成 `.bak`** 并复制走 ⇒ 工作区被污染 + 用户侧看到"数据丢了"(正是 `:47-51` 自己警告过、且"已多次在任务实跑时发生"的事故)。 +修法:改为「桌面默认 `%LOCALAPPDATA%\DeLector`;`DELECTOR_PORTABLE=1` 或 `DELECTOR_DATA_DIR` 才回落程序目录」,Docker 段保留。 + +### P1-3 · `docs/agents/architecture.md:202/:93-94` 宣称 `POST /api/settings` **无鉴权** —— 与代码相反 +**[code-reviewer]** 实测 `routes/main.py:1500-1502`:`update_app_settings` 第一行就是 `_require_localhost(request)`;`ops.md:26-29` 与 `PROJECT_OVERVIEW.md:47`(红线 7)也都写「仅 127.0.0.1 可写、局域网 403」。 +失败场景:agent 以 `:202` 为架构事实 → 给一个**已经有闸**的端点重复加鉴权,或反向推断「API Key 可被局域网改写」而做多余加固/误报安全结论。 +修法:两处改为「`POST /api/settings` 与 `/api/settings/test-key` 挂 `_require_localhost`(v4.4.0 起);`GET /api/settings` 保持可读」。 + +### P1-4 · 测试与探针基线停在 2026-08/09;前端模块清单三处漂移 +**[code-reviewer]** ① `architecture.md:26`「17 个测试文件共 **352**(2026-08-29)」、`ops.md:41`「pytest **719 passed**」、`ops.md:42`「`tools/*.mjs` **10 个**,发布闸要求 10/10」—— 实测 **122** 个测试文件 / `--collect-only` **1986** 条 / 探针 **29** 个(`README.md:23` 与 `CHANGELOG.md:35` 都写 29/29)。失败场景:agent 拿 `ops.md` 当门禁基线 → 只验 10 个探针就宣称「发布闸绿」,漏掉 19 个。 +② 前端模块数三处三个数:`README.md:137`「11 模块」/ `architecture.md:19`「10 个模块」(括号里其实列了 11 个名字)/ 实测 `static/js/*.js` = **23**,漏掉 `listen-lab`、`hard-sentences`、`enc-i1`、`search`、`update` 等整块。失败场景:agent 建立前端心智模型时不知道这些文件存在 → 新建第二模块或改错文件(`architecture.md:339-341` 自己记载过「两份逐字相同的错误实现」同族事故)。 +修法:数字一律改为「以 `pytest --collect-only -q | tail -1` / `ls static/js | wc -l` 为准」或直接删枚举只留目录指引,**别再手抄**。 + +--- + +## 二、P2(9 条) + +| # | 发现 | 证据 | 修法 | +|---|---|---|---| +| P2-1 | `CLAUDE.md` 是「缺路由的短桩」——15 行只有 vault 路径 + WORKMEMORY 读序,**无一处提到 `AGENTS.md`**,也无 architecture/ops 入口;而 `AGENTS.md:3` 自称「同 CLAUDE.md 形态」、`GEMINI.md:11` 却明确「读 AGENTS.md」。Claude Code 会话只自动加载 CLAUDE.md ⇒ 全程不知道 architecture/ops 存在 | 两席独立命中(**已合并**) | CLAUDE.md 加一行「文档路由见 `AGENTS.md` 的表格」,**别造第三份** | +| P2-2 | **Inbox 路径两套写法**:`CLAUDE.md:9` 与 `GEMINI.md:17` 写 `d:\Obsidian\Coding\99-Inbox\…`,而 `.cursorrules:7` 与 `.agent-rules.md:7` 写 `…\08-Inbox\…` —— 同一件事四个文件两个地址,必有一错 | 结构席实测 | 统一成一个值,其余改为引用 | +| P2-3 | `docs/plans/archive/` 4 份文件违反命名规则(无 `<日期>-` 前缀,而 `docs/README.md:20` 规定 `plans/<日期>-<主题>.md`):`workbench-scope-control-and-live-settings.md`、`workbench-progress-server-sync.md`、`workbench-core-words-mode.md`、`plan-companion-mascot.md`。归档判据「只进不出」本身是明示取舍(**不是疏漏**),但命名应一致 | 结构席 | 改名或在该节加豁免说明 | +| P2-4 | `docs/design-system.md`(606 行)被 `docs/README.md:16` 列为**顶层常青规范**,但 `AGENTS.md` 路由表与 `README.md:39-50` 导航都**没有它** ⇒ 全仓零入口;且其 §4 token 表与 CSS 脱节:`--bone`、`--ink-blue`、`--paper-dark`、`--paper-dark-box`、`--rule-dashed`、`--shadow-exhibit`、`--reveal-delay` 七个 token 及 `#F7F1DE`/`#DDD2B6`/`#15140F`/`#2A528A`/`#C8BFA9` 五个色值,在 `static/style.css`、`static/css/tokens.css`、`static/german/` 里**逐个 grep 零命中** | 文档席逐个 grep 实测 | 要么补入口并同步 token 表现在值,要么把状态改成「历史设计稿(v3.0 · 2026-08,未随 tokens.css 同步)」——**不能继续既无人可达、又看起来是现行规范** | +| P2-5 | `README.md:5` Python badge「3.10 丨 3.11」无任何事实支撑:`pyproject.toml:20`/`Dockerfile:1`/CI 6 处全钉 **3.11**,且 `FEATURES.md:295` 已明文点名过 README 的版本数字会漂 | 文档席 | 确认 3.10 是否真支持(无证据);不支持就改成 3.11 | +| P2-6 | 根目录/包结构描述缺项:`architecture.md:12` 漏 `desktop.py`/`start.bat`/`requirements-desktop.txt`;`:18` 写 `start.py` 86 行(实测 **232**);`README.md:136` 的 `delector/` 分层漏 `delector/tools/`(正是 `polish_tools.py` 所在、最近两批 PR 主战场);`README.md:141` 的 `docs/` 说明漏 `adr/` 与 `reviews/`(而 `:50` 自己又指向 `docs/adr/`) | 文档席 | 按 `ls` 实况补三处;行数类数字改为不写或标注量取日 | +| P2-7 | `scratch/` 50MB 纯本地堆积、无修剪约定(CI 输出快照/复现探针/PR 正文草稿/变异夹具/陈旧 `wr_old.py`);`agent/`(Go agent runtime)与 `AGENTS.md`/`.agent-rules.md`(AI agent 规范)**同一个词三种指涉** | 结构席 | scratch 加「超 N 天/超 X MB 即清」本地约定;`README` 或 `architecture` 用一句话说明 `agent/` 是 Go runtime、与 AI agent 规范无关 | +| P2-8 | `WORKMEMORY/INDEX.md:16` 说 `handoff_*.md`「仅当存在活跃交接时才存在」vs `:39` 把 `handoff_interactive-agent-loop.md` 标 closed 却仍留着 ⇒ 按 `:16` 语义下一个 agent 会以为存在的 handoff 都是活的 | 文档席 | 关闭的 handoff 移入 `WORKMEMORY/archive/`(协议 §4 允许),或补一条清理事件 | +| P2-9 | `README.md:10` Tests 徽章「1768/1770」是 v5.17.0 发版实测,而 master 在 PR #136/#137 后已是 **1984+2**(本地 collect 1986)——徽章被 `README.md:81` 当成**当前**套件规模读,口径未定义 | 文档席 | 定口径(跟发版 or 跟 master);若跟 master,按 `ops.md:152-153`「测试数变化须同批更新版本面」它已欠 218 条 | + +--- + +## 三、合理、别动的部分(两席共识,值得保留) + +1. **README 版本面是全仓最健康的一块**:`:4` badge、`:32-35` 下载表、`:23-25` 摘要全部等于 `APP_VERSION` v5.17.0,与 `static/index.html:40`、`static/sw.js:3`、`CHANGELOG.md:7`、git tag 一致,且有 `tests/test_writer_mobile.py:61`(四处落点钉版本)与 `:127`(v4.9.0「下载表指向 v4.8.3」事故的产物)**机械守护**。 +2. **单一真相源纪律执行到位**:`README.md:153-155` 明言「完整版本历史的正主是 CHANGELOG,此处不再复制第二份」;`docs/README.md` 把发版历史钉在根 CHANGELOG 并记录了 2026-10-02 清理 `release-v2.1.md` 的理由。**未发现同一件事两处描述会互相漂移的重复文档**(ADR 件 vs 同名实施计划已比对:是决策件与计划件,不是副本)。 +3. **`docs/README.md` 就是「文档该放哪」的单一契约**(分层表 + 放置规则 + 与别处的关系 + 历史合并记录),AGENTS.md 无死链(逐个校验其指向均存在)。 +4. **`.gitignore` 分层设防、几乎每条带理由**(`*.db*` 四件套 / 迁移备份 / 签名材料「丢失=包名永久无法升级」/ 本地 agent 工具态),并显式声明 `WORKMEMORY/` 不许忽略。 +5. **诚实表述**:`PROJECT_OVERVIEW.md:28-30` 与 `FEATURES.md:283-288` 对未发版功能均用限定表述,没有把未完成写成已完成(§4.3 纪律守住)。 +6. **测试隔离基础设施方向正确**:根 `conftest.py` 只做 sys.path + env setdefault(尊重 CI 显式值),重活在 `tests/conftest.py` 与 `tests/db_cleanup.py`(失败必抛、绝不被吞);唯一漏口就是 P1-1 的 `test_*.db`。 + +--- + +## 四、建议顺序 + +1. **P1-1/P1-2 优先**(两条都会造成**真实数据/工作区事故**,且 P1-2 是文档误导 agent 去踩已知事故)。 +2. **P1-3/P1-4 同批**(文档与代码/实测对齐,把"手抄数字"改成"现读口径")。 +3. P2 里 **P2-1/P2-2** 顺手修(入口桩与路径统一,纯收益);**P2-4**(design-system 无人可达 + token 脱节)建议单独一次定夺;其余登记待办。 + +**未核(UNVERIFIED)**:vault 侧文件(`01-Rules/*`、ADR-0023 正式件)本机不可达;`docs/design-system.md` 其余 500 行版式/组件章节未逐条比对 CSS;`android/` 与 `agent/` 的内部互相引用;`build/`/`dist/` 实际内容;archive 内各文件最后修改时间(判断是否仍在更新)。 From 8303c99c4fb59713161eda665f279ed206283432 Mon Sep 17 00:00:00 2001 From: Haoyu Xi Date: Fri, 16 Oct 2026 01:31:02 +0800 Subject: [PATCH 4/4] =?UTF-8?q?docs(review):=20commit=20=E8=8A=82=E5=A5=8F?= =?UTF-8?q?=E4=B8=8E=20git=20=E7=AE=A1=E7=90=86=E8=AF=84=E4=BC=B0=EF=BC=88?= =?UTF-8?q?2=20P1=20+=208=20P2=EF=BC=8C=E6=97=A0=20P0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 两席 code-reviewer 并行(commit 节奏与历史纪律 / git 管理与仓库卫生),只读未写。 节奏结论:健康 —— 447 提交/30 天 ≈ 3 PR/天 × PR 内任务级原子提交(平均 2.3/PR)+ 协议驱动的簿记事件;空泛消息 0;90 个 merge 全为 --no-ff 且合并信息带 PR 号与验收数据;版本面六件套 + tag 硬闸;规则生效后(2026-10-14 起)直推 master 的 20 条只碰 WORKMEMORY/ 与 docs/。 P1-1 分支纪律无机械闸:2026-10-09~10-14 有 25 个 feat/fix/ci/test/perf 提交直推 master(含生产代码与 CI 定义),已有实发事故 f49d359(直推把 master 推红后回头补);规则 2026-10-14 才进可见文档,此前 agent 读不到。修法:pre-push hook 或 CI job 断言 master 非 merge 提交改动面 ⊆ 簿记/文档白名单。 P1-2 ignore 覆盖缺口(编排者已独立复核):launch.log / launch.log.1 / lan-address.txt 均 NOT-IGNORED,确由 desktop.py:46/51/58 产生,便携模式(data_dir_bootstrap.py:118-129 return program_dir())下落在仓库根 ⇒ 一次 git add -A 会把运行态日志(启动堆栈/端口冲突)与内网 IP 提交进历史,正是 .gitignore:21-24 自述的威胁模型;pre-commit 文件名黑名单也不含 .log/.txt。历史从未进过这三个文件。修法 3 行根级忽略规则。 P2 含:实现与簿记混装 7 个提交、批量子提交多事揉一团(59ac353 11 文件/+1448)、行号锚点缺席、.superpowers/ 缺根级兜底、松散对象 49.10 MiB、9 个文件工作区行尾 w/mixed、CRLF 纪律依赖本机 core.autocrlf 而非 .gitattributes 契约、.env.example 注释与代码矛盾(与结构评估 P1-2 同源)、gradle-wrapper.jar 未入库待确认。 合理部分:PR 内任务级原子提交 + 每提交带回归锁与变异自证;--no-ff 可读合并信息;簿记一个事件一提交;密钥与历史卫生干净(HEAD 高熵扫描 0 命中);.gitignore 按类别+理由分层;DeLector.spec 由显式 skip 钉住非死绿。 --- ...-commit-rhythm-and-git-management-audit.md | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 docs/reviews/2026-10-16-commit-rhythm-and-git-management-audit.md diff --git a/docs/reviews/2026-10-16-commit-rhythm-and-git-management-audit.md b/docs/reviews/2026-10-16-commit-rhythm-and-git-management-audit.md new file mode 100644 index 00000000..dd897ecb --- /dev/null +++ b/docs/reviews/2026-10-16-commit-rhythm-and-git-management-audit.md @@ -0,0 +1,86 @@ +# commit 节奏与 git 管理评估(DeLector,master @ `d93089b`) + +**Date**: 2026-10-16 · 分支 `chore/swarm-audit-master`(评估专用) +**席位**:`code-reviewer` ×2(**commit 节奏与历史纪律** / **git 管理与仓库卫生**),均只读、只跑只读 git 命令 +**Findings**:**10** 条(合并后)← 原始 **14** 条。**无 P0**(密钥与历史卫生干净)。2 P1 + 8 P2。 + +--- + +## 一、commit 节奏:**健康,不是噪声** + +**一句话结论**:447 提交/30 天 ≈ 15/天,是「**PR 内任务级原子提交**」与「**协议驱动的簿记事件**」两股叠加,不是过碎。 + +| 证据 | 数据 | +|---|---| +| PR 合并节奏 | 90 个 merge / 30 天 ≈ **3 PR/天** | +| PR 内提交数 | 4~14 个、平均约 2.3(`ae8c3fd`=14、`d919d42`=13、`a4984c2`=10、`6024fb0`=4) | +| 空泛消息 | `wip`/`fix`/`update` 命中数 = **0** | +| 合并形态 | 90 个 merge 全为 `Merge pull request #N from …`(`--no-ff`),合并信息带 PR 号 + 分支名 + 合并不变量 | +| 分支拓扑 | 仅 4 个分支;唯一未合分支是活跃在途的 `feat/judge-gaming-experiment`(12 提交,最后活动 = 今天) | +| 簿记节律 | `work.log` 被改 83 次 = NOTE/WORK_END/DECISION 事件协议(一个事件一提交)——**不应为压低提交数而合并** | +| 版本面纪律 | 发版稳定六件套(CHANGELOG/README/PROJECT_OVERVIEW/gradle/index.html/sw.js,v5.17.0 起加 `core/version.py` + `test_app_version_source.py`);`40a9f88` 四处版本串一致 + `assert_tag_matches_app_version` 构建期硬闸 | + +**规则生效后 100% 合规**:`--since=2026-10-14 --first-parent --no-merges master` 的 20 条直推**只碰 `WORKMEMORY/` + `docs/`**(代码/CI/打包命中数 = 0)。 + +--- + +## 二、P1(2 条) + +### P1-1 · 分支纪律**没有机械闸**;规则落地前 25 个实现类提交直推 master +**[code-reviewer]** 证据:`docs/agents/ops.md:202-216` 自述「2026-10-13『去控制台 + 桌面化观感』那批…全程直推 `master`、零 PR」,并承认「分支纪律没有机械闸」。实测复核:`--since=2026-10-09 --until=2026-10-14 --first-parent --no-merges master` 中 `feat/fix/ci/test/perf` 前缀共 **25 条**,含生产代码与 CI 定义(例:`a2c4621` 改 `desktop.py`+55 与 `tests/test_desktop_shell.py`+132;`41cb430` 改 CI;`f66d203` 改发布链路)。 +**失败场景**:直推不经 PR ⇒ 无 review、无 PR 级 CI 门禁;**已有实发事故** —— `f49d359 fix(ci): 修 master push 上 CI 变红(打包面检测 job 缺 checkout)`,即一次直推把 master 推红后才回头补。规则 2026-10-14 才写进可见文档(`fbeca12`/`4897a6f`/`46487ac`),此前 agent 按 `AGENTS.md` bootstrap **根本读不到这条规矩**(ops.md:212-214 自述根因)。 +**修法**:加一条廉价机械闸 —— pre-push hook 或 CI job 断言「`master` 上非 merge 提交的改动面 ⊆ {WORKMEMORY/, docs/, README.md, CHANGELOG.md, static/index.html, static/sw.js, android/app/build.gradle, delector/core/version.py},否则 fail」(与已有 tag 闸同族,`ops.md:215` 自己指出了这个对照)。 + +### P1-2 · `.gitignore` 覆盖缺口:`launch.log` / `launch.log.1` / `lan-address.txt` 未忽略,且默认可落仓库根 +**[code-reviewer]**(**编排者已独立复核**:`git check-ignore` 三者均 **NOT-IGNORED**;产物确由 `desktop.py:46/51/58` 产生;**历史从未进过这三个文件**) +证据链:`desktop.py:126` `resolve_log_path() = resolve_data_dir(env) + LOG_FILE_NAME`;`core/data_dir_bootstrap.py:118-129` `resolve_data_dir`:**便携开关命中时 `return program_dir()`(= 仓库根)**;`.env.example:5` 附近注释还背书「留空(不设)= 仓库根,即桌面端默认」。 +**失败场景**:开发者从源码跑桌面壳(`DELECTOR_PORTABLE=1 python desktop.py` 或 `DELECTOR_DATA_DIR=.`)⇒ 三个文件生成在仓库根,`git status` 不标 `!!` ⇒ 一次 `git add -A` 把**运行态日志(启动堆栈 / 端口冲突详情)与内网 IP 地址**提交进历史 —— 与 `.gitignore:21-24` 自己写下的理由完全同类(「一次 `git add -A` 就会把用户数据提交进历史」);`.githooks/pre-commit:55-68` 的文件名黑名单也不含 `.log`/`.txt`,钩子拦不住。 +**修法**(3 行): +```gitignore +/launch.log +/launch.log.* +/lan-address.txt +``` + +--- + +## 三、P2(8 条) + +| # | 发现 | 证据 | 修法 | +|---|---|---|---| +| P2-1 | **实现与簿记混装**(7 个提交) | `8f07820`/`6a3aca4`/`24b7c2e`/`ad556a1`/`949061a`/`7ce6191`/`fbeca12` 同时改 `WORKMEMORY/*` 与实现/测试 ⇒ `git log -- WORKMEMORY` 与 `git log -- src` 边界糊掉,无法单独回滚一段记忆或单独 cherry-pick 一个修复 | PR 内拆出 `docs(workmemory):` 提交(**PR #137 的 `55e0f0d`/`555cd4a` 已是范式**,照抄即可) | +| P2-2 | **批量子提交把多件事揉进一个 commit** | `59ac353 perf(data): 数据面成本账 P1-5~8`(11 文件 / +1448,含 4 项互不相关的优化)⇒ bisect 命中后仍需在 1448 行里人工二分;单项回归无法单独 revert | 一个 PR 可以,提交按「一项一提交」拆(`eae739f→76fdccf→0cba260→c108124→e42f3cd` 的 T1~T5 是范式) | +| P2-3 | **行号锚点基本缺席**(符号级锚点 ~98 处) | 符号锚在重命名/搬移后静默失效;行号至少能暴露「代码已不在原处」。附带:`dbe0e70` 消息里混入一个俄语词「почему」 | 保持符号锚(对重构更稳),**接受现状**;仅记录「声称的行号锚点未全面落实」 | +| P2-4 | `.superpowers/` **缺根级兜底** | `.gitignore:30-33` 为 `.mypy_cache/`/`.ruff_cache/` 加了根级兜底并写明理由;而 `.superpowers/` 靠嵌套 `.superpowers/sdd/.gitignore`(内容 `*`)自忽略 ⇒ 工具升级/重建时 `git status` 干净、`git add -A` 把整棵工具缓存提交 | 补一行 `.superpowers/`,与 `.mypy_cache/` 同级 | +| P2-5 | **松散对象堆积** | `git count-objects -vH`:count 7181 / size 49.10 MiB;`gc.auto` 疑似未生效(未查证) | `git gc` 一次;查 `gc.auto`/`gc.autoPackLimit` 配置 | +| P2-6 | **9 个跟踪文件工作区行尾 `w/mixed`** | `git ls-files --eol` 中 9 个(`.agent-rules.md`、`.cursorrules`、3 份 work-2026-09-1x.log、`linguistics.py`、`syntax_score.py`、`hard-sentences.js`、`test_desktop_data_dir_bootstrap.py`),blob 均 `i/lf`(**历史干净,问题只在工作区**) | 潜伏的口径不一致(非正在发生的错误);`git add --renormalize` 一次性收口 | +| P2-7 | **CRLF 纪律实质依赖个人配置而非仓库契约** | `.gitattributes` 有 4 组规则且**实测生效**(`agent/**/*.go` 全 `attr/text eol=lf` + `w/lf`,与 gofmt 假阳性事故闭环);但 `.py/.md/.js` 无 attr,实测 `attr/` 空 + `w/crlf` —— 只因为本机 `core.autocrlf=true`(**系统级** Git 配置)。Linux/macOS clone 拿到 LF ⇒ 同一文件在不同机器上格式不同 | `.gitattributes` 增加 `* text=auto eol=crlf`(置于 `*.go text eol=lf` **之前**,让具体规则覆盖通用规则),把 CRLF 从「本机副作用」升级为「仓库契约」 | +| P2-8 | `.env.example` 注释与代码矛盾(**与结构评估 P1-2 同源**) | `.env.example:5` 说「留空 = 仓库根,即桌面端默认」;实际 `data_dir_bootstrap.py:125-129` Windows 默认 `%LOCALAPPDATA%\DeLector`,只有 `DELECTOR_PORTABLE` 才落程序目录。它正是 P1-2 的**认知放大器** | 改为「Windows 默认 `%LOCALAPPDATA%\DeLector`;设 `DELECTOR_PORTABLE=1` 才落到程序所在目录」 | + +**另附(P2,待确认)**:`android/gradle/wrapper/gradle-wrapper.jar` 未入库(`gradle-wrapper.properties` 在)⇒ 干净 checkout 上 `./gradlew` 缺 jar;若 CI 有缓存则不暴露。修法:确认 CI 是否依赖;若故意排除请在 `android/build_apk_guide.md` 写明理由。 + +--- + +## 四、合理、保持的部分 + +1. **PR 内任务级原子提交 + 每条提交自带回归锁与变异自证**:`598983d`(P0 死锁)、`dbe0e70`(发版门守卫)、`80c3aa0`(超时护栏反转)三条消息都写「症状/根因/修复/回归锁/门禁实测」,是「为什么」而非「做了什么」的范本;这类密度在非合并提交里是主流。 +2. **`--no-ff` + 可读合并信息**:90 个 merge 均带 PR 号、分支名、合并方式变更原因与验收数据(如 `ae8c3fd` 记「服务端合并被 OAuth scope 拒绝 ⇒ 本地 `--no-ff`」)。 +3. **簿记节律与协议对齐**:一个事件一个提交,不应合并;`be40ffd`/`58985af` 还有事件头自查纠错。 +4. **密钥与历史卫生干净**:HEAD 树高熵正则扫描 0 命中(`sk-`/`Bearer `);历史上从未新增 env/keystore/credential 文件;被删文件里无敏感文件;`.githooks/pre-commit` 有自述的全量扫描记录(2026-08-19)。 +5. **`.gitignore` 按「类别 + 理由」分层**(85 行),注释解释*为什么*;`.gitattributes` 对 go/bat/二进制生效(go 那组有事故闭环);`DeLector.spec` 本地产物边界由 `test_server.py:4562` 的**显式 skip** 钉住(不是静默 `if`,不会成为死绿测试)。 + +--- + +## 五、建议顺序 + +1. **P1-2 先修**(3 行 `.gitignore`,消掉「运行态日志 + 内网 IP 进历史」的真实风险,且成本为零)。 +2. **P1-1 加机械闸**(pre-push hook 或 CI job;这是本仓已实发过事故的纪律缺口,规则本身 2026-10-14 才落地)。 +3. P2 里 **P2-1/P2-7/P2-8** 同批(提交拆分习惯 + `.gitattributes` 收口 CRLF 契约 + 注释与代码对齐);**P2-5**(`git gc`)随手;**P2-4/P2-6/P2-3** 登记待办。 + +## ⚠️ UNVERIFIED(按纪律未跑,需更重遍历) + +1. **全历史逐 blob 密钥扫描未做** —— 「历史无密钥」建立在 HEAD 树扫描 0 命中 + `.githooks/pre-commit:5-7` 的**自述**(2026-08-19 全量扫过可达提交与 fsck 悬空对象)之上,该声明**未能独立验证**。本应跑:`git rev-list --all | xargs -n 50 git grep -I -E 'sk-…|AKIA…|ghp_…|-----BEGIN …PRIVATE KEY-----'` + `git fsck --lost-found --unreachable` 后对悬空 blob 同样扫描。 +2. **squash/rebase 是否混用** —— 只抽样 5/90 个 merge 做 `^1..^2` 差集,其余 85 个未验。 +3. **每个提交自身是否绿灯落地** —— 只读了消息自报的门禁数字(如 1984 passed),未在对应 sha 上复跑。 +4. **`gc.auto` 配置未查证**(`git config --get gc.auto` 即可,属轻量但本轮未跑)。 +5. **`static/index.html` 23 次改动是否每次都有对应版本面动作** —— 只抽查 4 对(均同日闭环)。