Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
13 commits
Select commit Hold shift + click to select a range
2ae5527
docs(plan): A1 裁判集扩充计划按双席位评审修复清单定稿(8 任务)
ROM4n2 Oct 15, 2026
33d6bb4
docs(plan): RED#2 修法修正为分层承担(运行期 fail-open + 声明/门禁层 fail-closed)
ROM4n2 Oct 15, 2026
88856ab
feat(judge): A1 裁判扩到语法层并把能力同批送进生产链路(Task 1+2 原子变更)
ROM4n2 Oct 15, 2026
37294b5
docs(plan): 补 Task 7 的前端降级提示生产者 + Fog Unknown F(Android 本地加载未验证)
ROM4n2 Oct 15, 2026
5ddd1cc
docs(plan): 勾选 Task 1/2 的 Step Breakdown(随台账 complete 同步)
ROM4n2 Oct 15, 2026
deede44
feat(loop): 内核接缝加固 —— dispatch 超时 + 同步裁判移出事件循环 + 修不可达 except(Task 8)
ROM4n2 Oct 15, 2026
55e9090
docs(workmemory): 裁判集扩充计划推进至 3/8 + 双席位评审结论与两次反裁决
ROM4n2 Oct 15, 2026
39e6d79
feat(judge): 三个新语法维度(主谓一致/动词支配格/主句 V2)+ 加宽后的永久硬门(Task 3+4+5)
ROM4n2 Oct 15, 2026
209cc24
test(judge): FP 标定工具(情态框架语序 / 过去锚时态一致性)—— Task 6 的 AFK 半
ROM4n2 Oct 15, 2026
55e0f0d
docs(plan+workmemory): Task 6 HITL 裁决落档 —— A 维不做、B 维接线为 warning(含 51 …
ROM4n2 Oct 15, 2026
cf9ea76
feat(judge): B 维接线为 warning + 闸门收窄到任意德语文本 + 提示词参数化(Task 6 收尾 + Task 7)
ROM4n2 Oct 15, 2026
a5a7c1e
docs(plan+workmemory): §4.3 解除判据表(8 条实测)+ 勾选 Task 3~8 步骤 + 本批交接事件
ROM4n2 Oct 15, 2026
555cd4a
docs(plan+workmemory): §4.3 签批落档 —— 限定表述(无条件「已实现」继续禁用)+ 回退语义保持
ROM4n2 Oct 15, 2026
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
2 changes: 2 additions & 0 deletions .github/workflows/build-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ jobs:
--hidden-import=delector.data.core_dict_ext \
--hidden-import=delector.data.prep_dict \
--hidden-import=delector.data.a1_dict \
--hidden-import=delector.data.a1_verb_case \
--hidden-import=delector.data.a1_writing_dict \
--hidden-import=delector.data.a1_hoeren_dict \
--hidden-import=delector.data.a1_lesen_dict \
Expand Down Expand Up @@ -218,6 +219,7 @@ jobs:
--hidden-import=delector.data.core_dict_ext \
--hidden-import=delector.data.prep_dict \
--hidden-import=delector.data.a1_dict \
--hidden-import=delector.data.a1_verb_case \
--hidden-import=delector.data.a1_writing_dict \
--hidden-import=delector.data.a1_hoeren_dict \
--hidden-import=delector.data.a1_lesen_dict \
Expand Down
34 changes: 34 additions & 0 deletions WORKMEMORY/work.log
Original file line number Diff line number Diff line change
Expand Up @@ -519,3 +519,37 @@ Caps: filesystem-read, filesystem-write, shell-exec
- **Why 现在不做**:三态递进需先有「第二个实现」才有内容(现仅一个工具面、一个裁判)——提前定接口正是 §2 反模式;且与本子计划(抬 §4.3 门槛)正交。
- **已登记**:写进新计划 `docs/plans/2026-10-15-a1-judge-expansion.md` 的 `Out of Scope`(本计划一切改动 MUST NOT 依赖该重构)+ `Fog of War [Unknown E]`(含四个子目标与将来启动的前置顺序)。
- **不阻塞**:三张 RED(接线缝 / fail-closed / 同步调用超时)**均可走"端点注入缝"修复,不需要这次重构**(这正是两个席位给出的"不改内核结构、允许经注入缝改变裁判语义"路径)。

### 2026-10-15 15:10 | codebuddy | NOTE
Caps: filesystem-read, filesystem-write, shell-exec
事件:ADR-0023 §4.3 续作(A1 裁判集扩充,计划 `docs/plans/2026-10-15-a1-judge-expansion.md`,8 任务)在分支 `feat/a1-judge-expansion` 上推进到 **3/8**(Task 1+2 合并交付 `88856ab`;Task 8 `deede44`)。
- **开工前的双席位评审**(用户指示,按 `01-Rules/LAYERED-ARCHITECTURE` + `AGENT-SDK-PATTERNS`):两席位均判「局部偏离」,核心是 **RED#1 接线缝缺失** —— 计划 7 个任务没写"谁把 `nlp` 交给裁判",而 `_run_polish` 从不传 `judge=`、Out-of-Scope 又禁改内核 ⇒ **语法裁判在生产恒不生效而 7 任务测试可全绿**(dead-green),§4.3 判据表还能被打勾解除。据此把 Task 1/2 合并为**原子变更**(能力与接缝同批落地)。
- **我对评审的两处反裁决(都有实测依据,已写进计划)**:① RED#2 的"降级即禁止收敛"**不采用** —— 实测 `processor.py:192-198` Android 显式 `nlp=None`,若禁止收敛则 `argmin` 回退返回**原文** = ADR §10.3 的功能报废回归;改为**运行期 fail-open + 显式标记 / 声明门禁层 fail-closed**。② maker 一度把 `_a1_violations_if_letter` 提前改成按规则族分流 —— 那是 **Task 7 的 HITL 行为变更**(语法违规开始计数后,模型修不动的作文会回退原文),已退回并加一条**非真空钉子**钉住「本批不改该行为」。
- **Task 8(接缝加固)**:dispatch 加 `min(per-call, 剩余 deadline)` 超时(调用前重算剩余)、同步 `judge` 移 `asyncio.to_thread`(否则长裁判阻塞 `/api/health` 且 `task.cancel()` 掐不断)、修 `_polish_event_stream` 那个**永不命中**的 `except llm_client.LLMError`(`_run_polish` 早已把它转成 HTTPException)⇒ 改为捕 HTTPException 发 error 帧,`e.detail` 人话才到得了用户。
- **基础设施坑(两次)**:两次 subagent 派单被掐(10003 / 10101),第一次留了半成品 + 一条取消用例跑不完(备份 `/tmp/task8-partial.diff` 后丢弃、重派成功)。**教训:给 maker 的反挂死纪律必须写进 prompt**(所有 sleep 要有 wait_for/固定次数兜底、禁 join 线程、禁无界 while、每条用例贴实测耗时)。Task 8 的 flaky 黄卡(心跳窗口余量趋零)由我按 CRV 开方改 1 行(跨调用取 max)并 20 连跑自证 —— 偏离 maker-checker,已如实记录在提交信息。
- **门禁**:5 套件 411 passed;`ruff check .` 全绿;`mypy --strict delector` 66 files 0;契约闸 PASS;CRLF。
- **未做**:Task 3/4/5(三个新维度规则)、Task 6(FP 标定,HITL)、Task 7(闸门收窄 + §4.3 判据表,HITL,含前端降级提示)。**Pi 式分层重构已由用户拍板暂缓**(`docs/agents/architecture.md` 已登记)。

### 2026-10-15 16:40 | codebuddy | DECISION
Caps: filesystem-read, filesystem-write, shell-exec
事件:**Task 6 的 HITL 裁决落地**(用户拍板):A 维**不做**、B 维**接线为 warning**。
- **A 维(情态动词框架语序)不做**:实测负样本 6/7(「实词夹在情态动词与末位动词之间」整族不可见 = 结构性 FN),且样本外追补的 3 句**正确**句(逗号并列主句)全部误报 ⇒ FP 与 FN 同源、两侧都破。标定产物(`tools/bench_judge_fp.py`)保留作记录、不接线。**Why 这条要记**:本计划第一个"测了之后选择不做"的维度 —— 防止后来者看到 bench 脚本以为它只是"还没接线"。
- **B 维(时间副词 ↔ 时态一致性)接线为 warning**:正样本 FP 0/35、负样本 7/7、对照 0/6,两档阈值均成立;样本偏小(7 错 + 6 对照)⇒ 先 advisory 不阻断收敛,积累真实用法后再议升 error。可见性靠响应体新增 `judge_advisories`(只增不改)+ Task 7 的前端渲染。
- 更正一条**未实测口径**:提交 `39e6d79` 的消息里写「COMMON_CORRECT_SENTENCES 47 句」是我未实测的转述,**实测为 51 句** —— 以实测为准(本事件与 PR 正文为准)。

### 2026-10-15 18:20 | codebuddy | NOTE
Caps: filesystem-read, filesystem-write, shell-exec
事件:**A1 裁判集扩充计划(`docs/plans/2026-10-15-a1-judge-expansion.md`,8 任务)技术面全部落地,台账 8/8**(分支 `feat/a1-judge-expansion`,待开 PR)。
- **交付面**:`88856ab` Task 1+2(违规形状统一 + 接线既有两件算力 + 端点注入缝 + 官方范文零违规门)· `39e6d79` Task 3/4/5(`numerus` / `rektion`+29 条人工词表 / `wortstellung` + 加宽后的 51 句永久硬门)· `209cc24` Task 6(FP 标定工具,冻结记录)· `cf9ea76` Task 6 收尾 + Task 7(B 维 warning/advisory + 闸门按规则族分流 + 提示词参数化 + 前端非阻断提示)· `deede44` Task 8(dispatch 超时 / 同步裁判移出事件循环 / 修不可达 except)。
- **HITL 裁决(用户)**:A 维(情态框架语序)**不做** —— 实测负样本 6/7 + 样本外 3 句正确句全误报 ⇒ FP/FN 同源;B 维(时态一致性)**接线为 warning** —— 正样本 FP 0/35、负样本 7/7、对照 0/6。
- **本计划最有价值的产出不是规则,而是三次"误报类"返工**:① `öffnen/bezahlen/schmecken` 被当严格 Acc ⇒ 合法自由第三格被报;② V2 把**主语 NP 内修饰**(`Meine sehr große Familie…`)当独立前置成分;③ V2 漏豁免**焦点小词**(`Auch…`/`Nur…`)+ rektion 把自由第三格当违规。三次都**先写成用例跑红再修**,并把硬门从"官方 10 篇"扩到 **51 句 + 类覆盖清单**(编排者另做 30 句广度扫雷 FP=0 后定稿)。**Why 要记**:官方范文门放过了这三类 —— "正样本不够宽"本身就是一类系统性风险。
- **判据面**:§4.3 解除判据表已入计划(8 条判据:维度覆盖**部分满足**、官方范文零误报 ✅、51 句硬门 ✅、30 句扫雷 ✅、负样本召回 ✅、非书信可判 ✅、降级可见性 ✅、**"浅裁判是否被模型针对"的对照实验未做**(阻塞于 `DEEPSEEK_API_KEY`)⇒ 建议**只允许限定表述**,不建议解禁 §4.3 原文所禁的无条件"已实现")。
- **基础设施坑(本会话共 5 次子代理中断:10003×2 / 10006×3 / 10101)**:Task 8 首次留半成品 + 挂死用例(备份后丢弃重派);Task 6 前两次零落盘;最后一次死在报告前但改动已落盘 ⇒ 由我逐条核到行号并复跑门禁后代提交。**两条实操教训**:⑴ 给 maker 的 prompt MUST 写反挂死纪律(所有 sleep 要有 wait_for/固定次数兜底、禁 join 线程、禁无界 while、每条用例贴实测耗时);⑵ 提交信息**禁用 `python -c "...'''...'''"` 形式**(反引号会被 bash 当命令替换吞掉正文 —— Task 6 那次就丢了正文,已用 heredoc 重建并在消息尾部注明)。
- **未做**:Android 真机点检(含新 API 的 WebView 表现)、有 Key 后的真实轮次分布与真 token 计数回测、Pi 式分层重构(用户已拍板暂缓,登记在 `docs/agents/architecture.md`)。

### 2026-10-15 19:05 | codebuddy | DECISION
Caps: filesystem-read, filesystem-write, shell-exec
事件:**§4.3 门槛的宣称方式与回退语义由用户签批**(计划 `docs/plans/2026-10-15-a1-judge-expansion.md` 的 §4.3 判据表尾已记)。
- **宣称方式 = 限定表述**:允许对外写「裁判已扩到 A1 语法维度:动词位置 + 格的使用三规则 error 级、时态一致性 advisory;语序经实测评审判定不做」;**无条件「已实现验证驱动 agent 循环」继续禁用** —— 判据表第 8 条("浅裁判是否真被模型针对"的对照实验)因缺 `DEEPSEEK_API_KEY` 未做,故 §4.3 的门槛只算**部分抬升**。
- **回退语义 = 保持**:非书信文本模型修不动 ⇒ `no_improvement` ⇒ argmin 回退**原文**(与循环语义一致,断言 + 三处 docstring + 前端人话原因码已钉住)。
- **Why 记**:这是"测出来之后**不做**"与"允许**限定**宣称"两个反直觉结论 —— 防止后来者把 §4.3 读成"已经解禁"或把 bench 脚本里的 A 维当成"待接线"。
63 changes: 63 additions & 0 deletions delector/data/a1_verb_case.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
"""A1 高频动词 → 支配格(Rektion)**人工登记表**。

**这是事实数据,不是生成物**:每条都按「歌德 A1 词表(`delector/data/a1_dict.py::GOETHE_A1_VOCAB`)
+ 教科书级支配格常识」逐条确认,禁止用 LLM 批量生成。收录标准 = **少而准**(FP 是敌人):

- 只收**严格单格**支配的动词(`Dat` 或 `Acc`)——检测器仅在「有冠词的宾语 NP + 单数」时才报;
- 标 `Dat+Acc` 的动词(人三格物四格 / 自由第三格 freier Dativ / 格支配两可)**登记但不判**
—— 这些动词的宾语格位依句式而定,硬判必误报;
- 可分前缀动词(an|rufen)/ 反身动词(sich freuen)/ 名词化动词一律**不入表**
(检测器另有守卫,见 `delector/services/writing.py::detect_verb_case_government` 的 docstring);
- 动词同时可接介词搭配(如 `helfen bei`、`danken für`)不影响本表 —— 那是
`detect_preposition_case` / 固定搭配表的领域。

来源注释(按组给出,每组均为教科书级支配格常识 + 歌德 A1 词表在册动词):
- `Dat` 组:`jemandem helfen / danken / gratulieren`(人三格)、`Das gefällt / gehört /
passt / fehlt mir`(A1 教材固定搭配)。
- `Acc` 组:A1 教科书典型句式中只接四格宾语的及物动词(`etwas sehen / brauchen / suchen /
finden / lieben / grüßen / kennen / lesen / trinken / essen / hören / verstehen /
wiederholen / unterschreiben`)。**凡 A1 教科书级出现自由第三格(人三格附加宾)或双宾
用法的动词一律不进本组**:`jdm. die Tür öffnen` / `jdm. etw. bezahlen` 是 A1 教科书级
搭配 ⇒ `öffnen` / `bezahlen` 移入 `Dat+Acc`;`schmecken` 两可(`Das schmeckt mir.`
三格 / `Ich schmecke den Wein.` 四格)⇒ 同样移入 `Dat+Acc`。
- `Dat+Acc` 组(不判):双宾「人三物四」(`jemandem etwas geben / bringen / zeigen /
schreiben / kaufen`)+ 自由第三格(`jdm. die Tür öffnen` / `jdm. etw. bezahlen`)+
两可支配(`schmecken`)。
"""

A1_VERB_CASES: dict[str, str] = {
# ── Dat(严格三格;`helfen / danken / gratulieren` = 人三格,A1 教材固定搭配)────────
"helfen": "Dat",
"danken": "Dat",
"gratulieren": "Dat",
# Dat(`Das gefällt / gehört / passt / fehlt mir.` 句式)─────────────────────────────
"gefallen": "Dat",
"gehören": "Dat",
"passen": "Dat",
"fehlen": "Dat",
# ── Acc(严格四格;A1 教材典型句式只接四格宾语的及物动词)──────────────────────────
"sehen": "Acc",
"brauchen": "Acc",
"suchen": "Acc",
"finden": "Acc",
"lieben": "Acc",
"grüßen": "Acc",
"kennen": "Acc",
"lesen": "Acc",
"trinken": "Acc",
"essen": "Acc",
"hören": "Acc",
"verstehen": "Acc",
"wiederholen": "Acc",
"unterschreiben": "Acc",
# ── Dat+Acc(双宾 / 自由第三格 / 两可支配 ⇒ 登记但**不判**)────────────────────────
"geben": "Dat+Acc",
"bringen": "Dat+Acc",
"zeigen": "Dat+Acc",
"schreiben": "Dat+Acc",
"kaufen": "Dat+Acc",
"öffnen": "Dat+Acc",
"bezahlen": "Dat+Acc",
"schmecken": "Dat+Acc",
}
49 changes: 49 additions & 0 deletions delector/nlp_engine/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,55 @@ def _resolve_nlp_model() -> Any:
return _nlp_model


# ── 只读 / 无下载加载助手(Task 1:给产品层「拿一个可用的 nlp」但不付下载成本)────────
# 说明:`_resolve_nlp_model()` 是「首用即加载」且**带 `spacy.cli.download` 兜底**(出网 + 起 pip
# 子进程、无时限)。产品层的润色循环不能付这个成本,于是拆成两件更窄的只读/无下载算力。
def get_loaded_nlp() -> Any:
"""只读**已加载**的 spaCy 模型:仅当此前成功解析过(`_nlp_resolved` 且 `_nlp_model` 非 None)才返回。

绝不触发加载、绝不 `spacy.cli.download` —— 这是给产品层「先看有没有现成模型」的纯读探针。
与 `_resolve_nlp_model()` 的分工:后者是首用即加载(可能起下载),本函数是**零副作用读取**。
"""
if _nlp_resolved and _nlp_model is not None:
return _nlp_model
return None


def try_load_nlp_no_download() -> Any:
"""按 `SPACY_MODEL_CANDIDATES` **只做本地加载**:能装就装(写回既有缓存),装不上就返回 None。

**为什么需要这个中间态**(设计理由,勿删):
- 只用 `get_loaded_nlp()` 会让**桌面首次润色**也落在 `rules_only`(模型是惰性加载的,别处
还没触发过)⇒ 生产上引擎标记长期假降级,与真实能力不符;
- 直接调 `_resolve_nlp_model()` 会走 `spacy.cli.download`(出网 + 起 pip 子进程,无时限);
- 本函数取中间态:**本地能加载就加载、加载不上就诚实返回 None,绝不下载**。

成功 ⇒ 写入既有缓存(`_nlp_model` / `_nlp_resolved=True`)并刷新 `NLP_ENGINE` / `NLP_ENGINE_DETAIL`;
失败 ⇒ **不写任何全局**(不改 `_nlp_resolved` / `_nlp_error`),返回 None。

同步函数(内部不 await),可在 `asyncio.to_thread` 里跑,避免阻塞事件循环。并发安全:
沿用 `_model_lock` + 双检,保证与 `_resolve_nlp_model()` 共用同一份缓存、不重复加载。
"""
global _nlp_model, _nlp_resolved, NLP_ENGINE, NLP_ENGINE_DETAIL
if _nlp_resolved and _nlp_model is not None:
return _nlp_model
with _model_lock:
if _nlp_resolved and _nlp_model is not None:
return _nlp_model
for candidate in SPACY_MODEL_CANDIDATES:
try:
model, how = _load_spacy_model(candidate)
except Exception:
# 该候选本地加载失败 ⇒ 试下一个;**绝不**退到 spacy.cli.download(见 docstring)。
continue
_nlp_model = model
_nlp_resolved = True
NLP_ENGINE = "spacy"
NLP_ENGINE_DETAIL = f"spaCy {getattr(spacy, '__version__', 'unknown')} + {how}"
return _nlp_model
return None


class _LazyNlp:
"""惰性 spaCy 模型代理:首次**调用**时才真正加载(ADR-0018 §7.6 O0)。

Expand Down
Loading
Loading