docs+chore: 同步操作说明与端到端自检(字段库/单位换算、因子参数化)

- docs/USAGE.md:
  · 因子层一行改为「代码注册表投影 + 参数化实例,参数写在名字里以冻结口径」;
  · 新增 `GET/POST/PATCH /api/factors`、`GET /api/factors/templates` 与
    `/api/condition-fields`、`/fields` 的说明;
  · 新增「参数化因子(2026-10)」块:受控范围、键必须写全参数(缺项就靠可改的
    默认值兜底 = 追溯改义,所以拒绝)、口径文案按代码收敛、停用 ≠ 删除、
    参数化因子也能当过滤条件;
  · 自检清单一并更新(pytest 500 条;verify_strategy_workspace 145 项 skip-job;
    verify_ui_alignment 8 页 160 项;新增 verify_unit_conversion、verify_factor_params)。
- scripts/:新增 verify_unit_conversion.py(单位只能在给定范围里选 + 界面单位⇄
  基准单位换算)、verify_factor_params.py(参数暴露/界面新建/越界拒绝/停用语义,
  跑完自动清掉临时因子);verify_strategy_workspace.py 加 [5.7b] 因子参数化一节,
  临时因子的清理挪进 finally(断言中途失败也不给真人库留垃圾)。
- .gitignore:docs/screenshots/ 是临时验证证据,不入库(文件留在磁盘)。
This commit is contained in:
Simon
2026-10-01 16:38:54 +08:00
parent f13b34c59e
commit 36fe018075
6 changed files with 1317 additions and 62 deletions
+11 -2
View File
@@ -56,7 +56,7 @@ import urllib.request
WEB = os.environ.get("QLIB_WEB", "http://127.0.0.1:3000")
DEFAULT_PAGES = ["/", "/backtest", "/strategies", "/selection", "/experiments", "/stocks", "/factors"]
DEFAULT_PAGES = ["/", "/backtest", "/strategies", "/selection", "/fields", "/experiments", "/stocks", "/factors"]
CHROME_CANDIDATES = [
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
@@ -216,6 +216,8 @@ PROBE = r"""
// 多行文本域:高度随行数变化,是「内容决定高度」的控件,
// 不参与「同排等高」(它本来就该比单行输入高)与「高度取值归一」比较
isTextarea: e.tagName === "TEXTAREA",
// 卡片式多选项(整张卡可点):高度由文案决定,不计入「控件高度归一」
cardPick: !!(e.closest && e.closest(".strategy-pick__item")),
x: r.x, y: r.y, w: r.w, h: r.h, fs: parseFloat(s.fontSize),
radius: s.borderRadius, padL: parseFloat(s.paddingLeft), padR: parseFloat(s.paddingRight),
placeholder: txt(e.getAttribute("placeholder")).slice(0, 30),
@@ -230,9 +232,15 @@ PROBE = r"""
required: e.hasAttribute("required"), disabled: e.hasAttribute("disabled") || e.disabled === true,
// 勾选/单选控件的真实点击目标是外层 label(.check / .radio-row),
// 16px 的方块永远达不到 28px;量 label 才是量「用户能不能点到」。
// 但「卡片式」多选 label(.strategy-pick__item:内含名称+说明+因子,
// 高度由文案决定,可达上百 px)不是「控件高度」概念,排除出归一比较。
hitH: (() => {
if (e.type !== "checkbox" && e.type !== "radio") return r.h;
const lab = e.closest("label");
if (lab && lab.classList.contains("strategy-pick__item")) {
// 整张策略卡都是热区:用卡片高度(远大于 28px),而非 16px 方块
return +lab.getBoundingClientRect().height.toFixed(1);
}
return lab ? +lab.getBoundingClientRect().height.toFixed(1) : r.h;
})(),
// pill / chip 是刻意的胶囊形控件,不参与圆角统一
@@ -332,7 +340,8 @@ def check_geometry(g: dict) -> list[tuple[bool, str, str]]:
out.append((not mixed, "同排控件等高", detail or f"{len(rows)} 行全部等高"))
# 2 高度取值归一
uniq = sorted({round(c["hitH"], 1) for c in ctrls if not c["optionRow"] and not c["isTextarea"]})
uniq = sorted({round(c["hitH"], 1) for c in ctrls
if not c["optionRow"] and not c["isTextarea"] and not c.get("cardPick")})
out.append((len(uniq) <= 3, "控件高度取值归一(≤3 种)", f"实际 {len(uniq)} 种:{uniq}"))
# 3 点击目标