产品 2026-09-15 三项拍板,落成两份新单与两处既有单的修订: - 新增 TASK-freeze-metric-change-20260915(无 BUG 号,后面两单的前置): 两条冻结余量已用完(page.tsx 1951/1951 余 0;api server 11334/11363 余 29), 冻结从「逼新代码往外走」退化成拦路。实证:page.tsx 行数砍 59% 但 Home() 的 useState 从 56 涨到 66;api server 225 个类方法只有 12 处真碰 HTTP。 主门换成耦合指标,行数降为粗护栏;同时推翻 §6「参数式 hook 内部保持 0 个 React hook」——那正是状态搬不走的原因。 - 新增 TASK-home-state-lowering-20260915(无 BUG 号):先搬 rectification* 那 15 个 state 进已经是 dynamic 子树的校正面,Home() useState 66 → ≤53。 零行为变化;串行在 freeze-metric-change + C2 + R3 之后。 - 修订 TASK-consultation-external-evidence-cache-20260915:依赖反转,C1 排在 API server 拆解之前(它动模块级函数,拆解动类方法);补「不得新增类方法、 行数余量仅 29」的硬红线。 - 修订 TASK-api-server-decomposition-20260916:串行依赖加 C1 与 freeze-metric-change;__new__ 计数按 grep 的 4 计(原文 3 是文件数); 阶段 4 收尾口径改写;基线 11,314 → 11,334。 纯文档推送,不触发门禁、不发布镜像、不部署。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JUei7K13cYxLHE3Axe4A45
9.9 KiB
TASK · 把两条增长冻结从「数行数」换成「数耦合」
- 日期:2026-09-15
- 基线 commit:
origin/staging@6b3248bf - 执行分支:
codex/freeze-metric-change-20260915 - 落点:
AGENTS.md§6、tests/test_api_server_growth_contract.py、新建frontend/tests/home-shell-growth-contract.test.ts、frontend/tests/chart-view-route.test.ts(挪走一条断言) - 这一单是后面两单的前置:
TASK-home-state-lowering-20260915与 API server 拆解都需要新口径先生效,否则它们做的事会被旧门禁判红 - 与七条在飞分支无文件重叠(它们都不碰这两个合同测试,也都声明「
page.tsx一行不许动」)
1. 为什么要换
两条冻结的余量都用完了:
| 文件 | 当前 | 上限 | 余量 |
|---|---|---|---|
frontend/src/app/page.tsx |
1,951 行 | 1,951 | 0 |
scripts/jyotish_api_server.py |
11,334 行 | 11,363 | 29 |
冻结当初的用意是逼新代码往 hooks / lib / 独立模块里走,第一阶段确实起了作用(page.tsx 从 4,766 砍到 1,951)。但顶到线之后它的作用反转:任何一轮正常改动只要需要在这两个文件里加几行接线就会被打红,这一轮于是被迫去做一件与本次目标无关的搬运。上一轮的 TASK-rectification-p0-fix-20260915 就是这么来的——BUG-705 的十来行接线让 1,951 变 1,964,逻辑本身早就在 lib 里了,超的只是接线。
而行数从来不是维护成本的来源。两个文件各有一个能直接表达耦合的数字:
page.tsx
| 指标 | BUG-249 当时 | 今天 |
|---|---|---|
| 文件行数 | 4,766 | 1,951 |
useState |
56 | 66 |
useEffect |
18 | 22 |
useRef |
— | 41 |
useCallback / useMemo |
0 / 0 | 0 / 0 |
行数砍掉 59%,状态反而从 56 涨到 66。 抽出去的 hook 是参数式的(§6 现有那句「参数式 hook 内部保持 0 个 React hook」),useSessionManagement(params) 开头要解构约 40 个参数——代码搬走了,状态所有权一个都没搬。
scripts/jyotish_api_server.py
| 项 | 数量 |
|---|---|
JyotishAPIHandler 方法 |
225(8,219 行,占全文件 72%) |
| 模块级函数 | 102(2,981 行) |
do_POST / do_GET 路径分支 |
78(do_POST 单个方法 263 行) |
self.headers / self.wfile / self.rfile / self.path 全文件出现次数 |
12 |
JyotishAPIHandler.__new__ 伪造点 |
4 |
225 个方法,只有 12 处真的碰到 HTTP 上下文。 其余是披着 self 外衣的纯函数,这正是那 4 处 __new__ 后门的成因。
2. 根因
冻结盯的是「文件有多大」,而维护成本来自「谁依赖谁」。行数是耦合的影子:影子被按住了,本体照长不误——page.tsx 的状态数、api server 的类方法数在冻结期内都是增长的。
3. 决策记录
产品 2026-09-15 拍板:两条冻结的口径都要换,并明确以下三点。
- 主门换成耦合指标,行数降级为粗护栏。 不是取消行数限制,而是把它 rebaseline 到有余量的位置,让它只拦住「整块新功能塞进来」这种明显情况;真正的门是下面两组数。
- 推翻
AGENTS.md§6 现有的「参数式 hook 内部保持 0 个 React hook 的既定模式」。 这条正是状态搬不走的原因:它要求抽出去的 hook 不持有 React 状态,于是状态只能留在Home()。产品明确授权改掉它——新口径下,抽出去的 hook 与子组件应当持有自己的状态。执行方不得以「AGENTS 有这条」为由拒改;本节就是那条红线的推翻记录。 __new__计数这一轮只要求「不得增长」,不要求为 0。 现在是 4 处,收到 0 是 API server 拆解单的验收标准,不是本单的。本单只负责把尺子立起来。
4. 硬红线
- 本单不改任何业务代码。 只改
AGENTS.md§6、两个合同测试,以及把一条断言从它现在寄居的文件挪到专用文件。page.tsx与jyotish_api_server.py一行不许动。 - 不得放宽既有的其它冻结条款。 §6 第三条(不得再手写第二个聊天输入框 / 第二套滚动跟随 / 第二套加载动画)原样保留。
- 行数 rebaseline 的新基线必须取开工当时的实测值,并在测试注释里写明取值日期与
wc -l的结果,不得抄本任务书里的数字(七条在飞分支合并后这些数会变)。 - 新的耦合指标基线同理:
useState/useRef/ 类方法数 /__new__计数都以开工当时实测为准,只许降不许升。 - 不得顺手升级依赖、不得顺手修不在本单里的 warning。
5. 任务分解
5.1 page.tsx:新建专用合同测试
现在这条断言寄居在 frontend/tests/chart-view-route.test.ts 的 page.tsx does not grow to host the chart page 里(assert.ok((pageSource.match(/\n/g) ?? []).length <= 1951))——它和星盘页没有关系,只是当时顺手放在那儿。新建 frontend/tests/home-shell-growth-contract.test.ts,把增长约束集中过去:
| 门 | 断言 | 今天的值 |
|---|---|---|
| 主门 | Home() 里的 useState 数不得增长 |
66 |
| 主门 | Home() 里的 useRef 数不得增长(防止把 state 改写成 ref 绕过上面那条) |
41 |
| 粗护栏 | 文件行数 ≤ 实测基线 + 150 | 1,951 |
chart-view-route.test.ts 里保留与星盘页真正相关的那半条(assert.doesNotMatch(pageSource, /chart-page|ChartPageView|\/api\/chart-view/)),行数断言删除并注明搬到了哪里。
- 验收:新测试在当前代码上绿;人为在
page.tsx加一个useState后必须红(执行方在进度记录里贴出这次反向验证,证明尺子会动,不是恒为真)。 - 验收:计数方式要能区分
useState(与useState<Type>((今天 66 这个数就是按\buseState[<(]数出来的;只按useState(数会漏掉一半)。 - 验收:
npx tsx --test tests/chart-view-route.test.ts仍绿,且该文件不再包含行数断言。
5.2 jyotish_api_server.py:改 tests/test_api_server_growth_contract.py
现在是 JYOTISH_API_SERVER_LINE_COUNT_BASELINE = 11063 + 300。改成:
| 门 | 断言 | 今天的值 |
|---|---|---|
| 主门 | JyotishAPIHandler 的方法数不得增长 |
225 |
| 主门 | 全仓 JyotishAPIHandler.__new__ 出现次数不得增长 |
4 |
| 粗护栏 | 文件行数 ≤ 实测基线 + 300 | 11,334 |
方法数用缩进匹配(^ (?:async )?def \w+)即可,和本任务书 §1 那张表同一种数法。__new__ 计数扫 scripts/ 与 tests/,把命中文件列进断言失败信息,方便下次一眼看到是谁又开了后门。
- 验收:新断言在当前代码上绿;人为加一个类方法后必须红(同样贴反向验证)。
- 验收:该文件现有的另外三条断言(
AGENTS.md里必须出现must not grow/thinly registered/ 该测试必须在CORE_PYTEST_TARGETS与快速门里)一条不改仍绿。 - 验收:
.venv/bin/python -m pytest tests/test_api_server_growth_contract.py通过。
5.3 改 AGENTS.md §6
两条改写,逐字说明新口径:
- 第一条(api server):把「冻结时行数 + 300 行 bugfix 余量」换成「类方法数不得增长、
__new__伪造点不得增长,行数是粗护栏」;保留「新端点进独立模块、主文件只做薄注册」这句话——新口径正是在奖励它。 - 第二条(
page.tsx):把「不得再增长」换成「Home()的useState/useRef数不得增长」;删掉「参数式 hook 内部保持 0 个 React hook 的既定模式」,改成「抽出去的 hook 与子组件应当持有自己的状态;page.tsx只做装配」。
同时在 §6 里点明这次换口径的理由一句话(行数是耦合的影子),免得下一轮有人以为是放水。
- 验收:
tests/test_api_server_growth_contract.py里那条「AGENTS.md必须包含 must not grow / thinly registered」的断言仍绿(措辞改写时不要把这两个短语弄没了)。 - 验收:
frontend/AGENTS.md若有重复表述,同轮对齐。
5.4 记录
本单不产生 Bug 记录(改的是规则,不是缺陷),也不进 CHANGELOG.md(无用户可感知变化)。进度记录里必须写清楚:四组基线的实测值、取值日期、以及两次反向验证的结果。
6. 让步顺序
- 5.2(api server 口径)最先做——它直接决定 C1 那一单还要不要为 29 行余量拧巴。
- 5.1 次之。
- 5.3 必须和前两条同轮(规则和门禁不许分家,否则下一轮有人按旧 AGENTS 拒改)。
- 5.4 不得砍。
7. 开工前置命令
git fetch origin --prune
git worktree add -b codex/freeze-metric-change-20260915 \
.worktrees/freeze-metric-change-20260915 origin/staging
cd .worktrees/freeze-metric-change-20260915
git status -sb | head -1
# 取当时实测基线,不要抄任务书里的数
wc -l scripts/jyotish_api_server.py frontend/src/app/page.tsx
grep -cE '^ (async )?def ' scripts/jyotish_api_server.py
grep -rc 'JyotishAPIHandler.__new__' scripts/ tests/ | grep -v ':0'
grep -cE '\buseState[<(]' frontend/src/app/page.tsx
grep -cE '\buseRef[<(]' frontend/src/app/page.tsx
验收命令:
.venv/bin/python -m pytest tests/test_api_server_growth_contract.py
.venv/bin/python scripts/run_quality_gate.py --profile quick
cd frontend && npx tsx --test tests/home-shell-growth-contract.test.ts tests/chart-view-route.test.ts
npx tsx --test tests/*.test.ts # 与基线逐条比对失败清单
8. BUG 编号起点
本单不占 BUG 号。基线 6b3248bf 上最大号 BUG-720,721–732 已被两轮审计七单预占。
9. 不在本单范围
- 真的去搬状态或搬方法(见
TASK-home-state-lowering-20260915.md与 API server 拆解单) __new__收到 0(拆解单的验收标准)- §6 第三条的三个「不得再手写第二套」条款