"""端到端验证:因子参数化(暴露真实参数、可编辑、且真生效、真冻结)(2026-10)。
为什么单独一条契约脚本:因子参数写错**不会报错**,只会让策略按另一个口径选股
(「窗口 20」悄悄算成「窗口 90」= 另一个因子的结果)。所以这里从界面一路查到引擎:
1. `/factors`:目录暴露 template/params/param_specs(中文名含参数、允许范围 2~500、
方向二选一);展开行给出精确引擎键与来源;页面不再是「不可修改」的旧说法。
2. `/factors`:用界面新建参数化因子(窗口 90、越低越好)—— 预览键必须与后端规范键
逐字符一致;创建成功后 API 里出现该行,中文名/回看/方向全部由引擎投影。
3. `/factors`:越界参数(窗口 999)在界面上被拒,且**没有**留下任何垃圾行。
4. `/factors`:停用/启用真的写库,且停用后仍能被引擎解析(历史不变义)。
5. `/strategies`:条件字段下拉里出现该参数化因子(因子目录并入字段库),
因子下拉显示中文名(含参数)而不是又长又生的引擎键。
6. 375px:页面无横向溢出、几何检查全过。
7. 收尾:删掉验证期间创建的临时因子(目录没有删除接口,直接按主键清),回到 11 条内置。
需要浏览器可用的 Web(:3000)与 API(:8000)。复用 verify_ui_alignment 的 CDP 管线
(系统 Chrome,无需 playwright)。
"""
from __future__ import annotations
import json
import os
import subprocess
import sys
import tempfile
import time
import urllib.error
import urllib.request
sys.path.insert(0, "/Users/summer/project/qlib/scripts")
from verify_ui_alignment import CDP, check_geometry, find_chrome, free_port, probe # noqa: E402
WEB = "http://127.0.0.1:3000"
API = "http://127.0.0.1:8000"
TMP_KEY = "momentum(window=90,direction=lower_is_better)"
BAD_KEY = "momentum(window=999,direction=lower_is_better)"
FACTORS_READ = r"""
(() => {
const t = (el) => (el ? el.innerText.replace(/\s+/g, " ").trim() : null);
const rows = [...document.querySelectorAll("table.tbl tbody tr")];
const find = (name) => rows.find((r) => (r.innerText || "").includes(name));
const mv = find("momentum_60");
const mvCells = mv ? [...mv.querySelectorAll("td")].map((td) => t(td)) : [];
const probe = find("__PROBE__");
const probeCells = probe ? [...probe.querySelectorAll("td")].map((td) => t(td)) : [];
const probeBtn = probe
? [...probe.querySelectorAll("button")].map((b) => t(b))
: [];
return JSON.stringify({
rowCount: rows.length,
headers: [...document.querySelectorAll("table.tbl thead th")].map((th) => t(th)),
momentumCells: mvCells,
probeCells,
probeButtons: probeBtn,
// 展开行的文本:主行的下一个兄弟 tr(class=expand-cell)就是展开内容
probeExpandedText: probe ? t(probe.nextElementSibling) : null,
bodyText: t(document.body),
});
})()
"""
PREVIEW_READ = r"""
(() => {
const t = (el) => (el ? el.innerText.replace(/\s+/g, " ").trim() : null);
const w = document.querySelector("#factor-new-window");
const d = document.querySelector("#factor-new-direction");
const all = t(document.body) || "";
// 预览键 = 模板名(参数=值,…)。**不要**扫整页文本:
// 页面说明里也有一个示例键,扫文本会拿示例当预览(假通过)。
const previewKeys = [
...new Set(
[...document.querySelectorAll("b.mono")]
.map((el) => t(el))
.filter((x) => x && /^[a-z_][a-z0-9_]*\(/.test(x)),
),
];
return JSON.stringify({
windowValue: w ? w.value : null,
windowMin: w ? w.min : null,
windowMax: w ? w.max : null,
directionOptions: d ? [...d.options].map((o) => o.textContent.trim()) : [],
directionValue: d ? d.value : null,
templateOptions: [...(document.querySelectorAll("#factor-new-template option") || [])]
.map((o) => o.textContent.trim()),
previewKeys,
bodyText: all,
});
})()
"""
STRATEGY_FORM_READ = r"""
(() => {
const t = (el) => (el ? el.innerText.replace(/\s+/g, " ").trim() : null);
const factorSel = document.querySelector('select[aria-label="第 1 个因子的名称"]');
const condSel = document.querySelector('select[aria-label="第 1 个条件的字段"]');
// 注意: