test(freeze): 增长冻结改盯耦合,行数降为粗护栏
Home() useState/useRef 与 JyotishAPIHandler 类方法/__new__ 不得增长。行数改为开工实测基线加余量。产品授权删除参数式 hook 零 React hook 规则。
This commit is contained in:
@@ -92,8 +92,10 @@
|
||||
|
||||
## 6. 代码增长冻结
|
||||
|
||||
- `scripts/jyotish_api_server.py` must not grow. New endpoints and features go in dedicated modules under `scripts/` and are thinly registered from the main file. Do not add new handler bodies, workflows, or feature branches to this file. The cap is enforced by `tests/test_api_server_growth_contract.py`(冻结时行数 + 300 行 bugfix 余量)。
|
||||
- `frontend/src/app/page.tsx` 已从 4,766 行拆到 2,000 行以下,**不得再增长**:新逻辑进 `frontend/src/hooks/`、`frontend/src/lib/` 或组件;参数式 hook 内部保持 0 个 React hook 的既定模式。
|
||||
行数是耦合的影子:盯行数时,`Home()` 的状态数和 `JyotishAPIHandler` 的类方法数仍在涨。主门换成耦合指标,行数只作粗护栏。
|
||||
|
||||
- `scripts/jyotish_api_server.py` must not grow. New endpoints and features go in dedicated modules under `scripts/` and are thinly registered from the main file. Do not add new handler bodies, workflows, or feature branches to this file. The live gate is `tests/test_api_server_growth_contract.py`:**类方法数不得增长、`JyotishAPIHandler.__new__` 伪造点不得增长**;行数是粗护栏(开工实测基线 + 300)。
|
||||
- `frontend/src/app/page.tsx`:`Home()` 的 `useState` / `useRef` 数不得增长。新逻辑进 `frontend/src/hooks/`、`frontend/src/lib/` 或组件;抽出去的 hook 与子组件应当持有自己的状态,`page.tsx` 只做装配。行数是粗护栏(开工实测基线 + 150),由 `frontend/tests/home-shell-growth-contract.test.ts` 执行。
|
||||
- 不得再手写第二个聊天输入框(一律 `ChatComposer`,自有草稿走 `value`)、第二套滚动跟随(一律 `useConversationScrollAnchor` + `JumpToLatestButton`)、第二套加载动画(揭幕后不得出现 spinner / 骨架 / "正在加载",流式生成中除外)。
|
||||
|
||||
## 7. 前端红线
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
# PROGRESS · 增长冻结改盯耦合(2026-09-16)
|
||||
|
||||
工作树:`.worktrees/freeze-metric-change-20260915`
|
||||
分支:`codex/freeze-metric-change-20260915`
|
||||
任务书基线写 `6b3248bf`;开工时 `origin/staging` = **`51a65d92`**(含 BUG-732)。
|
||||
本机 Windows。无 Docker。本单不占 BUG 号,不改 `CHANGELOG.md` / `BUG_HISTORY.md` / `docs/tasks/README.md`。
|
||||
`page.tsx` 与 `scripts/jyotish_api_server.py` **一行未动**(反向验证后已 `git checkout` 还原)。
|
||||
|
||||
## 开工实测基线(2026-09-16,`51a65d92`)
|
||||
|
||||
禁止抄任务书 1951 / 66 / 41 / 11334 / 225。下列数字全部本机现测。
|
||||
|
||||
| 组 | 指标 | 数法 | 实测 | 新门 |
|
||||
| --- | --- | --- | ---: | --- |
|
||||
| 1 | `frontend/src/app/page.tsx` 行数 | `Path.read_bytes().count(b"\n")`,与 `(source.match(/\n/g) ?? []).length` 相同 | **1951** | 粗护栏 ≤ 1951 + 150 = 2101 |
|
||||
| 2a | `Home()` `useState` | `\buseState[<(]`(含 `useState<Type>(`) | **66** | 主门,不得增长 |
|
||||
| 2b | `Home()` `useRef` | `\buseRef[<(]` | **41** | 主门,不得增长 |
|
||||
| 3 | `scripts/jyotish_api_server.py` 行数 | `Path.read_bytes().count(b"\n")` | **11291** | 粗护栏 ≤ 11291 + 300 = 11591 |
|
||||
| 4a | 四空格 `def`(任务书同一种数法) | `^ (?:async )?def \w+` 扫全文件 | **225** | 主门,不得增长 |
|
||||
| 4b | `JyotishAPIHandler.__new__` | `scripts/` + `tests/` 的 `*.py`,排除合同测试自身 | **33** | 主门,不得增长 |
|
||||
|
||||
说明:
|
||||
|
||||
- 任务书表格写 api 行数 11,334;本机 `51a65d92` 实测 **11291**(未抄)。
|
||||
- 任务书表格写 `__new__` = 4:那是 **scripts/ 生产伪造点**(`consultation_workflow_service.py` ×2、`capture_report_blocked_repairs_golden.py` ×1、`local_accuracy_report.py` ×1)。任务书要求扫 `scripts/` 与 `tests/`,实测合计 **33**(scripts 4 + tests 29)。主门按扫描范围合计冻结,只许降不许升。失败信息会列出命中文件。
|
||||
- 全文件四空格 `def` = 225,其中 `JyotishAPIHandler` 类体 222、类前嵌套 3。冻结用任务书指定的全文件数法。
|
||||
- 只按 `useState(` 会计到 26,漏掉泛型调用;合同用 `\buseState[<(]`。
|
||||
|
||||
`frontend/AGENTS.md` 只有 Next.js 版本提示,没有 §6 重复表述,本单未改。
|
||||
|
||||
## 做了什么
|
||||
|
||||
1. `tests/test_api_server_growth_contract.py`:主门改成类方法数 + `__new__` 计数;行数 rebaseline 为 11291 + 300。`must not grow` / `thinly registered` / `CORE_PYTEST_TARGETS` 三条原样仍绿。
|
||||
2. 新建 `frontend/tests/home-shell-growth-contract.test.ts`:`Home()` `useState` / `useRef` 不得增长,行数粗护栏 1951 + 150。
|
||||
3. `frontend/tests/chart-view-route.test.ts`:删除行数断言,保留 `doesNotMatch(pageSource, /chart-page|ChartPageView|\/api\/chart-view/)`,注明搬到 home-shell 合同。
|
||||
4. `AGENTS.md` §6:主门改耦合指标;删掉「参数式 hook 内部保持 0 个 React hook」;第三条三个「不得再手写第二套」原样保留;仍含 `must not grow` 与 `thinly registered`。
|
||||
|
||||
## 既有断言改动
|
||||
|
||||
| 文件 | 原值 | 新值 | 原因 |
|
||||
| --- | --- | --- | --- |
|
||||
| `chart-view-route.test.ts` `page.tsx does not grow to host the chart page` | `(pageSource.match(/\n/g) ?? []).length <= 1951` 且 `doesNotMatch(...)` | 测试改名为 `page.tsx does not host the chart page`,只留 `doesNotMatch` | 行数冻结与星盘页无关,搬到专用合同 |
|
||||
| `test_api_server_growth_contract.py` 行数 baseline | 11063(2026-09-02)+ 300 | **11291**(2026-09-16 实测)+ 300 | 任务书要求开工实测 rebaseline,不得抄旧数 |
|
||||
|
||||
## 反向验证
|
||||
|
||||
### 1. `page.tsx` 人为加一个 `useState`
|
||||
|
||||
在 `Home()` 里插入 `const [_growthContractProbe, _setGrowthContractProbe] = useState(false);`,然后:
|
||||
|
||||
```
|
||||
npx tsx --test tests/home-shell-growth-contract.test.ts
|
||||
not ok 1 - Home() useState count must not grow
|
||||
error: 'Home() has 67 useState calls; cap is 66. ...'
|
||||
# tests 3 / pass 2 / fail 1
|
||||
```
|
||||
|
||||
`useRef` 与行数粗护栏仍绿。随后 `git checkout -- frontend/src/app/page.tsx`,文件回到 `51a65d92`。
|
||||
|
||||
### 2. `jyotish_api_server.py` 人为加一个类方法
|
||||
|
||||
在 `JyotishAPIHandler` 里插入 `def _growth_contract_probe(self): return None`,然后:
|
||||
|
||||
```
|
||||
pytest tests/test_api_server_growth_contract.py
|
||||
FAILED test_jyotish_api_handler_method_count_must_not_grow
|
||||
AssertionError: ... has 226 four-space def methods; cap is 225.
|
||||
1 failed, 4 passed
|
||||
```
|
||||
|
||||
行数粗护栏、`__new__`、AGENTS 措辞、`CORE_PYTEST_TARGETS` 仍绿——尺子动的是耦合门。随后 `git checkout -- scripts/jyotish_api_server.py`。
|
||||
|
||||
## 测试
|
||||
|
||||
本 worktree 的 `.venv` 是已跟踪的指针文件(内容 `/workspace/Jyotisha/.venv`),Windows 上不能当目录用,未覆盖。合同测试用本机 `C:\Users\74082\anaconda3\python.exe`(pytest 9.1.1)。
|
||||
|
||||
| 命令 | 结果 |
|
||||
| --- | --- |
|
||||
| `python -m pytest tests/test_api_server_growth_contract.py -v` | **5 passed**(含原 `must not grow` / `thinly registered` / `CORE_PYTEST_TARGETS` 三条) |
|
||||
| `npx tsx --test tests/home-shell-growth-contract.test.ts tests/chart-view-route.test.ts` | **16 pass / 0 fail**(home-shell 3 + chart-view 13;chart-view 无行数断言) |
|
||||
| `./node_modules/.bin/tsc --noEmit` | 0 错 |
|
||||
| `npm run lint` | **0 error** / 119 warning(既有,未动) |
|
||||
| `npx tsx --test tests/*.test.ts` | 3196 tests / 3112 pass / **69 fail** / 15 skip。本单 3 条 + `page.tsx does not host the chart page` 均 `ok`。失败为无 Docker、Windows `SKILL.md` symlink `EPERM`、bash 部署脚本,与近期同机清单同类 |
|
||||
| `run_quality_gate.py --profile quick --skip-frontend-runtime` | **blocked**:`interpretation_source_inventory_gate.py` → `ModuleNotFoundError: No module named 'mcp'`。停在 pytest 之前,不是本单合同红。未写成通过 |
|
||||
|
||||
同 SHA 近期进度 `PROGRESS-consultation-session-capacity-20260915.md` 记 `tests/*.test.ts` 为 3089 / 3001 / **73** fail。本机这次 3196 / 3112 / **69** fail(PowerShell glob 与计数差一截,fail 未增加)。本单新增 3 条全绿,不把 69 写成通过。
|
||||
|
||||
未跑 `next build`:无 UI 改动;质量门在 Python 预检就停了。
|
||||
|
||||
## 未做(属范围外)
|
||||
|
||||
- 真的搬 `Home()` 状态或拆 `JyotishAPIHandler`(后续两单)
|
||||
- `__new__` 收到 0
|
||||
- 推 `origin HEAD:staging`
|
||||
@@ -296,8 +296,9 @@ test("the golden chart-view envelope stays inside the page contract", () => {
|
||||
assert.match(COORDINATE_BOUNDARY.qizheng, /角宿/);
|
||||
});
|
||||
|
||||
test("page.tsx does not grow to host the chart page", () => {
|
||||
assert.ok((pageSource.match(/\n/g) ?? []).length <= 1951);
|
||||
test("page.tsx does not host the chart page", () => {
|
||||
// Line-count freeze moved to tests/home-shell-growth-contract.test.ts
|
||||
// (Home() useState / useRef caps + coarse line-count guardrail).
|
||||
assert.doesNotMatch(pageSource, /chart-page|ChartPageView|\/api\/chart-view/);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
const pageSource = readFileSync(new URL("../src/app/page.tsx", import.meta.url), "utf8");
|
||||
|
||||
// Measured 2026-09-16 on origin/staging @ 51a65d92.
|
||||
// Line count: (pageSource.match(/\n/g) ?? []).length, same as `wc -l` = 1951.
|
||||
// Hook counts use \buseState[<(] / \buseRef[<(] so `useState<Type>(` is not missed.
|
||||
// Former line-count assertion lived in tests/chart-view-route.test.ts
|
||||
// (`page.tsx does not grow to host the chart page`).
|
||||
const PAGE_LINE_COUNT_BASELINE = 1951;
|
||||
const PAGE_LINE_COUNT_CAP = PAGE_LINE_COUNT_BASELINE + 150;
|
||||
const HOME_USE_STATE_CAP = 66;
|
||||
const HOME_USE_REF_CAP = 41;
|
||||
|
||||
const USE_STATE_RE = /\buseState[<(]/g;
|
||||
const USE_REF_RE = /\buseRef[<(]/g;
|
||||
|
||||
function homeSource(source: string): string {
|
||||
const marker = "export default function Home(";
|
||||
const start = source.indexOf(marker);
|
||||
assert.notEqual(start, -1, "page.tsx must export default function Home(");
|
||||
return source.slice(start);
|
||||
}
|
||||
|
||||
function countMatches(source: string, pattern: RegExp): number {
|
||||
return source.match(new RegExp(pattern.source, "g"))?.length ?? 0;
|
||||
}
|
||||
|
||||
test("Home() useState count must not grow", () => {
|
||||
const n = countMatches(homeSource(pageSource), USE_STATE_RE);
|
||||
assert.ok(
|
||||
n <= HOME_USE_STATE_CAP,
|
||||
`Home() has ${n} useState calls; cap is ${HOME_USE_STATE_CAP}. Extracted hooks and child components should own their own state.`,
|
||||
);
|
||||
});
|
||||
|
||||
test("Home() useRef count must not grow", () => {
|
||||
const n = countMatches(homeSource(pageSource), USE_REF_RE);
|
||||
assert.ok(
|
||||
n <= HOME_USE_REF_CAP,
|
||||
`Home() has ${n} useRef calls; cap is ${HOME_USE_REF_CAP}. Do not rewrite state as refs to dodge the useState freeze.`,
|
||||
);
|
||||
});
|
||||
|
||||
test("page.tsx line count stays within the coarse guardrail", () => {
|
||||
const n = (pageSource.match(/\n/g) ?? []).length;
|
||||
assert.ok(
|
||||
n <= PAGE_LINE_COUNT_CAP,
|
||||
`page.tsx has ${n} lines; cap is ${PAGE_LINE_COUNT_CAP} (${PAGE_LINE_COUNT_BASELINE} baseline + 150).`,
|
||||
);
|
||||
});
|
||||
@@ -1,30 +1,81 @@
|
||||
"""Freeze scripts/jyotish_api_server.py growth.
|
||||
"""Freeze coupling in scripts/jyotish_api_server.py.
|
||||
|
||||
New endpoints and features must live in new modules and be thinly registered
|
||||
from the main file. This cap is the live line count at freeze (11063 on
|
||||
2026-09-02, via `wc -l`) plus 300 lines of bugfix slack.
|
||||
from the main file. Line count is only a coarse guardrail; the live gates are
|
||||
JyotishAPIHandler method count and JyotishAPIHandler.__new__ forgery sites.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from collections import Counter
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
API_SERVER = ROOT / "scripts" / "jyotish_api_server.py"
|
||||
AGENTS = ROOT / "AGENTS.md"
|
||||
|
||||
# Live `wc -l scripts/jyotish_api_server.py` at freeze. New features must not
|
||||
# consume this budget; open a module instead.
|
||||
JYOTISH_API_SERVER_LINE_COUNT_BASELINE = 11063
|
||||
# Live `wc -l scripts/jyotish_api_server.py` equivalent:
|
||||
# Path.read_bytes().count(b"\n"), measured 2026-09-16 on origin/staging @ 51a65d92.
|
||||
# New features must not consume this budget; open a module instead.
|
||||
JYOTISH_API_SERVER_LINE_COUNT_BASELINE = 11291
|
||||
JYOTISH_API_SERVER_LINE_COUNT_CAP = JYOTISH_API_SERVER_LINE_COUNT_BASELINE + 300
|
||||
|
||||
# Whole-file indent match `^ (?:async )?def \w+`, same count as
|
||||
# TASK-freeze-metric-change-20260915 §1. Measured 2026-09-16 @ 51a65d92.
|
||||
JYOTISH_API_HANDLER_METHOD_COUNT_BASELINE = 225
|
||||
|
||||
# `JyotishAPIHandler.__new__` in scripts/ and tests/ `*.py`, excluding this file.
|
||||
# Measured 2026-09-16 @ 51a65d92: scripts/ production forgeries = 4, tests/ = 29.
|
||||
JYOTISH_API_HANDLER_NEW_COUNT_BASELINE = 33
|
||||
|
||||
HANDLER_METHOD_RE = re.compile(r"^ (?:async )?def \w+", re.MULTILINE)
|
||||
NEW_MARKER = "JyotishAPIHandler.__new__"
|
||||
|
||||
|
||||
def _handler_method_count(source: str) -> int:
|
||||
return len(HANDLER_METHOD_RE.findall(source))
|
||||
|
||||
|
||||
def _new_hits() -> Counter[str]:
|
||||
hits: Counter[str] = Counter()
|
||||
skip = Path(__file__).resolve()
|
||||
for folder in (ROOT / "scripts", ROOT / "tests"):
|
||||
for path in sorted(folder.rglob("*.py")):
|
||||
if path.resolve() == skip:
|
||||
continue
|
||||
count = path.read_text(encoding="utf-8").count(NEW_MARKER)
|
||||
if count:
|
||||
hits[path.relative_to(ROOT).as_posix()] = count
|
||||
return hits
|
||||
|
||||
|
||||
def test_jyotish_api_handler_method_count_must_not_grow() -> None:
|
||||
source = API_SERVER.read_text(encoding="utf-8")
|
||||
count = _handler_method_count(source)
|
||||
assert count <= JYOTISH_API_HANDLER_METHOD_COUNT_BASELINE, (
|
||||
f"{API_SERVER.as_posix()} has {count} four-space def methods; "
|
||||
f"cap is {JYOTISH_API_HANDLER_METHOD_COUNT_BASELINE}. Move behaviour "
|
||||
"into a dedicated module; thinly registered from this file."
|
||||
)
|
||||
|
||||
|
||||
def test_jyotish_api_handler_new_count_must_not_grow() -> None:
|
||||
hits = _new_hits()
|
||||
total = sum(hits.values())
|
||||
listed = ", ".join(f"{path}:{count}" for path, count in sorted(hits.items()))
|
||||
assert total <= JYOTISH_API_HANDLER_NEW_COUNT_BASELINE, (
|
||||
f"{NEW_MARKER} appears {total} times under scripts/ and tests/; "
|
||||
f"cap is {JYOTISH_API_HANDLER_NEW_COUNT_BASELINE}. Hits: {listed}"
|
||||
)
|
||||
|
||||
|
||||
def test_jyotish_api_server_must_not_grow_beyond_bugfix_slack() -> None:
|
||||
line_count = API_SERVER.read_bytes().count(b"\n")
|
||||
assert line_count <= JYOTISH_API_SERVER_LINE_COUNT_CAP, (
|
||||
f"{API_SERVER.as_posix()} has {line_count} lines; cap is "
|
||||
f"{JYOTISH_API_SERVER_LINE_COUNT_CAP} ({JYOTISH_API_SERVER_LINE_COUNT_BASELINE} "
|
||||
"baseline + 300 bugfix slack). New endpoints and features must be new "
|
||||
"baseline + 300 coarse guardrail). New endpoints and features must be new "
|
||||
"modules, thinly registered from this file."
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user