# 变更记录(CHANGELOG) 本文件记录 `dsh-tradewatcher` 的历次变更、口径修订与实测依据。 - 项目说明、安装与使用方式、当前生效的口径规范 → 见 [`README.md`](README.md) - 待开发清单与「动手前必须探活的源」 → 见 [`docs/ROADMAP.md`](docs/ROADMAP.md) 约定:版本号取自 `package.json.version`(单一来源,构建期注入客户端并校验产物含该版本号)。 日期为本地(`Asia/Shanghai`)日期。**口径修订单独列出**——评分/概率/排序这类逻辑一旦改动, 升级前后的数字不可直接比较,必须能被追溯到"从哪天开始不一样"。 --- ## v0.183.0 — 全仓审查:宿主 5 条 P1、客户端 2 条 P1、测试补强 27 条、3 个生产缺陷 `npm run check` **1484 / 1484 × 2 时区**(1446 → 1484)+ `ALL HOST CHECKS PASSED` + build(73 片段全过)。 三份审查报告:`docs/AUDIT-HOST-1030.md`、`docs/AUDIT-CLIENT-1030.md`、`docs/AUDIT-TESTS-1030.md`。 ### 宿主层 P1(5 条,已修) | # | 位置 | 问题 | 修法 | | --- | --- | --- | --- | | H-01 | `detailTencent.ts:43` | 正则只认 `sh/sz/bj`,美股 `v_usAAPL=` 永远解析失败 | 补 `us[A-Za-z.]+` | | H-02 | `detailTencent.ts:57` | 美股 `[37]` 是美元却按万元 ⇒ 成交额 **×10 000** | 按 `105/106/107` 前缀判美股:`×1` | | H-03 | `detailTencent.ts:100` | 量纲判不出 ⇒ `vol:null`,`missing[]` 无条目 | 补「成交量」缺失原因 | | H-04 | `em.ts:3383` | 补今日柱后返回 `lmt+1` 根 ⇒ 切窗错位 | 抽 `withLiveBar()` 统一截断 | | H-05 | `rescue.ts:2238` | 6 只 ETF 超大单兜底串行 `await` ⇒ +1.2 s/拍 | 循环前 `Promise.all` 预取 | ### 客户端层 P1(2 条,已修) | # | 位置 | 问题 | 修法 | | --- | --- | --- | --- | | C-01 | `GlobalClockPage.tsx:166` | effect 依赖 `refreshSec`,设置解析后重复请求 `/quotes`;且 `clockRefreshMs` / `api.prefs()` 是死代码 | 删死代码,依赖收窄为 `[load]`,tick 补 `shouldPollQuotes` 闸门 | | C-02 | `RescuePanel.tsx:454` | `activeIntervalSec` 缺失 ⇒ `NaN` ⇒ **0 ms 轮询** | 新增 `rescuePollSec()`(`rescueView.ts:190`):非有限值回落 60 s,夹紧 `[30, 600]` | ### 测试层发现的 3 个生产缺陷(已修) | # | 缺陷 | 修法 | | --- | --- | --- | | T-01 | `batchesOf(codes, 0)` **死循环**(`i += 0`) | 空数组返回 `[]`;`size` 必填,`Math.floor` 后非有限或 `<1` ⇒ `RangeError`;6 个非法输入各断言且 <1 s 返回 | | T-02 | `bucketIndexOf` ±10% 边界方向相反 | 统一左闭右开:`+10.00 → >10`、`−10.00 → ≤−10`、`−7.00 → −7~−5`;对称性 `idx(10) === 10 − idx(−10)` | | T-03 | `dedupeSecids` 强制大写改写 `usAAPL` / `hkHSI` | 新增 `secidKeyOf()`:只对数字码归一,字母市场码原样;返回首次出现的原始串 | ### 测试层补强 | 项 | 数量 | | --- | --- | | 脆弱断言(只扫源码标识符) | 72 条 / 30 文件(登记) | | 无反例注入 | 100 / 205 文件(登记) | | 覆盖缺口 | 26 处,补 27 条断言(`hostFetch.ts` 此前零测试) | | 3 条最脆弱改行为断言 | `prismDay`、`distCard`、`chipsView` | ### 验证 `npm run check` **1484 / 1484 × 2 时区** + build(73 项);新增断言均带反例注入(改坏必败)。 审查过程实录:22:33 出现 1 条红(`clockView.test.ts:114` 源码正则与新 ticker 实现冲突),属并发编辑的旧断言,已随重构更新。 ### 未确认 - 31 个真网络测试在 `--test-concurrency=8` 下是否会偶发 429 —— 未长跑验证。 - 34 条路由未逐条人工复核,只回归 `/kline` 与 `/detail`。 ## v0.182.0 — 护盘卡片补量比/超大单净额/占成交额;刷新降为 5 分钟 `npm run check` **1446 / 1446 × 2 时区**(1440 → 1446)+ `ALL HOST CHECKS PASSED` + build(73 片段全过)。 ### 1) 三项数据补齐 | 字段 | 数据源 | 说明 | | --- | --- | --- | | 量比 | 腾讯 `qt.gtimg.cn` **`f49`** | 实测确认:股票与 ETF **同一位置**(600519 = 0.90 / 510300 = 0.44 / 159915 = 0.92 / 588000 = 0.88) | | 超大单净额 | 新浪 `MoneyFlow.ssi_ssfx_flzjtj`,`r0_in − r0_out` | 东财不可达时的替代;元为单位,展示换算亿元;60s 缓存,失败缓存 `null` 不抛 | | 占成交额 | `superShareOf(net, amount) = net / amount` | 两者都有才算 | 六只 ETF 实测(14:5x): | ETF | 量比 | 超大单净额 | 占成交额 | | --- | --- | --- | --- | | 510300 沪深300ETF华泰柏瑞 | 0.44 | **+1.024 亿** | +2.70% | | 510050 上证50ETF华夏 | 0.67 | −0.496 亿 | −2.57% | | 510500 中证500ETF南方 | 0.80 | −0.390 亿 | −1.06% | | 512100 中证1000ETF | 1.11 | **+1.234 亿** | +2.91% | | 588000 科创50ETF华夏 | 0.88 | **−6.688 亿** | −8.00% | | 159915 创业板ETF易方达 | 0.91 | +0.422 亿 | +0.41% | ### 2) 护盘刷新 60s → 300s - `RESCUE_DEFAULT_SEC` 与 `DEFAULT_PREFS.rescue.intervalSec`:**60 → 300**,范围仍 `[30, 600]`(`clampRescueSec(15)=30`、`(900)=600`)。 - 仍走 `shouldPollRescue`:盘后(15:05 / 20:00)与周六 `false`,仅「立即采样」可手动触发。 - **每 5 分钟请求数**:改前 5 拍 ≈ **35 条**;改后 1 拍 ≈ **7 条** ⇒ **−80%**。东财 `f66` 不可达时的额外新浪大单请求:30 条/5min → **6 条/5min**。 ### 验证 `npm run check` **1446 / 1446 × 2 时区** + build(73 项)。 新增 `etfFlow.test.ts` 5 条(含注入必败):① 六卡量比非空(`f49` 清空 ⇒ 全 null ⇒ 败)|② 超大单净额与占比同源(用 `netamount` 全部单冒充 ⇒ 败)|③ 缺数据 `—` 不是 0(写 0 ⇒ 败)|④ 默认 300s、范围 [30,600](60 ⇒ 败)|⑤ 盘后不轮询(盘后 true ⇒ 败)。 `pollGate.test.ts` 的旧「默认 60」断言同步改为 300(用户要求,非放宽)。 ### 未验证 `breaker` 三主机状态来自**沙箱同进程实探**(`open=false / fails=0 / trips=0`),不是运行中的 `dsh web` 实例历史累计 —— 重启后以实机为准。 ## v0.181.0 — 成交量量纲修正:K 线统一「手」、详情头统一「股」 `npm run check` **1440 / 1440 × 2 时区**(1434 → 1440)+ `ALL HOST CHECKS PASSED` + build(73 片段全过)。 ### 现象 日K 今日量柱比历史柱高 5–6 倍;详情头「成交量 290.95 亿」与「成交额 127.58 亿」同量级。 ### 根因 `1.688825` 的成交量在不同上游单位不同: | 源 | 单位 | 原始值 | | --- | --- | --- | | 日线(腾讯 `fqkline`) | **手** | 10-28 = 2,957,315 | | 快照 `[36]` | **股** | 295,731,483 | 上一轮把「股」当「手」硬 ×100 ⇒ K 线柱 ×100、详情头 `295.7 亿股`。**以成交额为锚**:`295,731,483 股 × 44.29 元 = 131.0 亿元` vs 成交额 `129.7 亿元`,误差 **1.0%**,证明「股」正确;按手则误差 **9900%**。 ### 修法 新增 `src/host/volUnit.ts`: | 函数 | 用途 | | --- | --- | | `volUnitOf(raw, price, amount)` | 用成交额反推单位(股 / 手 / 判不出) | | `volToLots(v)` | → 手(K 线用) | | `volToShares(v)` | → 股(详情头用) | - **K 线**:`liveDayBarOf` 入柱前按 `volUnitOf` 换算成手;判不出 ⇒ 该柱 `vol: null`(宁缺不错 100 倍)。 - **详情头**:`parseTencentDetail` 去掉硬编码 ×100,改为按锚判单位后统一成股;`QuoteDrawer` 用 `volumeCellOf`,量 × 均价与成交额差 >5% ⇒ 显示 `—` + `title`「量纲不一致:…相差 X%」。 - **单位标签**:`volLabelOfShares` —— ≥1e8 用「亿股」,否则「万股 / 股」。 ### 取证(`1.688825`,14:0x) | 项 | 值 | | --- | --- | | 历史 K 线末三根(手) | 3,255,307 / 3,444,443 / **2,957,315** | | 近 20 日中位数(手) | 2,870,817 | | 修前今日柱 | 295,731,483(把股当手) | | 修后今日柱 | **2,957,315 手**,比值 **1.03** | | 快照 `[36]` / `[37]` | 295,731,483 股 / 1,297,007 万元(129.7 亿元) | | 修前详情头 | `295.7 亿股`(×100 错位) | | 修后详情头 | **2.96 亿股** | ### 验证 `npm run check` **1440 / 1440 × 2 时区** + build(73 项)。 新增 `volUnit.test.ts` 4 条(含注入必败):① 今日柱 / 近 20 日中位数 ∈ [0.2, 5](实测 1.03;股当手 ⇒ 103 倍 ⇒ 败)|② 今日柱与历史柱同单位(按手入柱 ⇒ ×100 ⇒ 败)|③ 量 × 均价与成交额误差 ≤5%(实测 1.0%;×100 ⇒ 9900% ⇒ 败)|④ 单位标签自洽(`volLabelOfShares(295,731,483) = "2.96 亿股"`;5000 万股必须 `"5000.0 万股"`,写「0.50 亿股」⇒ 败)。 ### 连带修正 上一轮 v0.178.0 报告的「茅台成交量 2,201,200 股(22,012 手 × 100)」同样多乘了 100,实际为 **2,201,200 股**(快照原值即为股,无需换算)。本次修正后该类标的详情头不再 ×100。 ## v0.180.0 — 日K 缺今日柱:盘中缓存改 5 分钟 + 实时快照补动态柱 `npm run check` **1434 / 1434 × 2 时区**(1429 → 1434)+ `ALL HOST CHECKS PASSED` + build(73 片段全过)。 ### 现象 页头写 `截至 10-28 10:09`,但日K 最后一根是 **10-27**,X 轴末尾 `2026-10-27`。用户报「看不到当天柱子」。 ### 根因(两层叠加) 1. **归档层**:09:20 首次取数时上游序列只到 10-27,而 `ttlCache(全量, 1h)` 让这份「昨日为止」的缓存活到 10:20 ⇒ 10:09 的页面只有 10-27。 2. **上游 qfq 在 10:09 前也未吐出今日柱**(东财 `push2his` 本机不可达;生产链路本就是腾讯优先、东财备用)。 映射层没有丢数据。 ### 修法(仅 `src/host/em.ts`) | 项 | 内容 | | --- | --- | | 新增 | `klineTtlOf`(**盘中 5 分钟**、收盘后全量 1h / 增量 2min)、`shouldAppendLiveBar`、`liveDayBarOf` | | 今日柱 | `fetchKline` 回包末尾按需追加**今日动态柱**:`liveTodayBarOf` 先读共用行情槽(**零请求**),槽空才补一次单标的行情 | | 落盘 | 今日柱**只进回包、不落盘**(归档保持上游真值,下次回源按日期覆盖),`barOpen` 自动 `true` | ### 三段末三根对照(`1.600519`,klt=101 fqt=1) | 层 | 末三根 | 结论 | | --- | --- | --- | | 东财原始 `push2his` | 本机不可达(`fetch failed`,两次探测) | 生产链路腾讯优先,非必经层 | | 归档(按 09:20 写入形态复现) | 10-23 / 10-26 / **10-27** | **丢在这一层** | | 最终回包(归档 + 上游全熔断复现) | 10-26 / 10-27 / **10-28** ✅ | 实时快照补齐,`barOpen: true` | ### 今日 bar 实测(`1.600519`,13:45) **O 1354.98 / H 1354.98 / L 1323.11 / C 1323.38 / vol 25 829 手**(`barOpen: true`)。 ### 验证 `npm run check` **1434 / 1434 × 2 时区** + build(73 项)。 新增断言(含注入必败):① 盘中 10:09 最后一日 = 今日(不补 ⇒ 止于昨日 ⇒ 败)|② 09:20 的缓存在盘中必须失效(`klineTtlOf({inSession:true}) = 300_000`,旧 1h ⇒ 败)|③ 15:05 今日 OHLC 与收盘价一致(`close` 写成 `open` ⇒ 败)|④ 前复权今日与前日连续(实测 `|1323.38/1355.90 − 1| = 2.40% ≤ 5%`;重复复权 ⇒ 8.2% ⇒ 败)|⑤ 缺价不造柱(快照缺任一字段 ⇒ `null`;周六与 09:15 前不补)。 ## v0.179.0 — 日K 信息框翻边:今日柱与右侧价格轴不再被遮 `npm run check` **1429 / 1429 × 2 时区**(1423 → 1429)+ `ALL HOST CHECKS PASSED` + build(73 片段全过)。 ### 现象 日K 右侧最新一根(今日)被悬浮信息框压住;右侧价格刻度(`60.00 / 50.00 / 40.0…`)被遮;X 轴末尾日期截成 `2026-10-`。 ### 修法 | # | 内容 | | --- | --- | | 1 | `tipPlacement`(「右侧放不下才翻边」)换成 **`klineTipPlacement`**(「光标在右半就翻边」,阈值 `0.55 × 绘图区`),信息框夹进绘图区 | | 2 | 传入**最新 3 柱包围盒**做 `keepOut`;翻边后仍相交 ⇒ 退化**左上角** | | 3 | 画布右侧改为 **`AXIS_W = 56` + `TAIL_GAP = 28`**,末柱不再贴边 | | 4 | `TipCard` 加 `pointerEvents: 'none'` | 新增 `klineLayoutOf`(`PAD_X 8 / AXIS_W 56 / TAIL_GAP 28` 单一来源)、`overlapAreaOf`。 ### 几何实测(670px 宽) | 项 | 值 | | --- | --- | | 光标在右半(x≈493) | 翻边 `left = 493 − 12 − 152 = 329`,右缘 481 ≤ 485 ✓ | | 末柱中心 | 583.59;翻边框右缘 571.59 与最新 3 柱框 `[570.5, 585.9]` 相交 1.1px ⇒ **退化左上角**,交集 0 | | 尾距 | 120 根 ⇒ 30.41px;240 根 ⇒ 29.20px(≥28) | | 右轴 | `plotRight = 670 − 56 = 614`,末日期 `text-anchor=end` 零溢出 | ### 行为说明(按规格,不是 bug) 光标压**最后一根**时必然走左上角分支:翻边只退 12px,而最新 3 根柱自身宽约 14px ⇒ 仍相交 1px,触发退化。渲染图即此形态:信息框左上、今日柱与右轴、末日期全部无遮挡。 ### 验证 `npm run check` **1429 / 1429 × 2 时区** + build(73 项)。 新增 `klineTip.test.ts` 5 条(含注入必败):① 右半必须左翻(仍画右侧 ⇒ 败)|② 与最新 3 柱交集面积 = 0(贴右缘 ⇒ 败)|③ 尾距 ≥28px(取消 ⇒ 败)|④ 轴宽 ≥56px、末标签不裁剪(轴宽 0 ⇒ 败)|⑤ `pointer-events:none`(改 auto ⇒ 败)。 渲染核对:`/home/hu/kline-670.png` —— 信息框左上,`2026-10-28` 与 `80.00/70.00/60.00` 完整可见。 ### 未做 - 分时图的老 `tipPlacement` 未动(分时无「最新柱」概念,改动风险大于收益)。 ## v0.178.0 — 详情头加腾讯兜底:东财不可达时不再「详情暂不可用」 `npm run check` **1423 / 1423 × 2 时区**(1417 → 1423)+ `ALL HOST CHECKS PASSED` + build(73 片段全过)。 ### 现象 分时图上方详情头 `详情暂不可用`:`所属板块 / 今开 / 昨收 / 换手率 / 最高 / 最低 / 成交量 / 成交额 / 市盈率 / 总市值` 全 `—`。 ### 根因 `/tradewatcher/detail` 只有东财 `push2` 一条链(`f57/f46/f44/f43/f47/f48/f162/f116/f127`),东财连接失败 ⇒ 全空。 ### 修法 `/detail` 改为 **东财 → 腾讯** 两级: | 项 | 内容 | | --- | --- | | 新增 | `src/host/detailTencent.ts`(`parseTencentDetail` 纯解析 + `fetchTencentDetail` 走 `fetchTextHost` 过熔断器 + `detailWithFallback` 可注入编排) | | 新增 | `src/client/detailFallback.ts`(`industryCellOf`) | | 接线 | `/detail` 走 `detailWithFallback`,回包新增 `source` 与恒存在 `missing[]` | | 页面 | `QuoteDrawer` 记录 `detailSource`,行业格按来源给 `title` | 东财可用时行为完全不变,且**一条腾讯请求都不发**。 ### 真实标的兜底实测(`1.600519`,2026-10-28 11:5x) | 字段 | 值 | | --- | --- | | 名称 | **贵州茅台** | | 今开 / 昨收 | 1354.98 / 1355.90 | | 最高 / 最低 | 1354.98 / 1330.17 | | 成交量 | **2,201,200 股**(22,012 手 × 100) | | 成交额 | **2,951,090,000 元**(295,109 万元,29.51 亿) | | 换手率 | 0.18% | | 市盈率(TTM) | 20.44 | | 总市值 | **16,644.21 亿元** | | `missing` | `["量比","涨跌家数"]`(腾讯单票没有,如实列出) | ### 两处按数据纠正了施工单 1. **市值下标是反的**:施工单写 `[44] 总 / [45] 流通`;实测工商银行 `f44 = 22,944 亿 < f45 = 30,330 亿` ⇒ **`f44` = 流通、`f45` = 总市值**。照施工单抄会让工行总市值少 25%。 2. **中文名必须按 GBK 解**:`fetchTextHost` 默认 UTF-8 会把「贵州茅台」解成乱码 ⇒ 新增可选 `charset?: 'utf-8' | 'gbk'`,腾讯调用传 `gbk`。 ### 验证 `npm run check` **1423 / 1423 × 2 时区** + build(73 项)。 新增 `detailTencent.test.ts` 断言:① 10 项至少 8 项非空(抹空 4 项 ⇒ 败)|② 成交量 ×100、成交额 ×1e4、市值 ×1e8(按手当股 ⇒ 败)|③ 东财可用 ⇒ `source='eastmoney'` 且腾讯 0 请求(要求 1 次 ⇒ 败)|④ 东财失败 ⇒ `source='tencent'` 且 detail 非空;「详情暂不可用」只在 `detail===null` 后(挪到守卫前 ⇒ 败)|⑤ 行业缺失 ⇒ `—` + title 原因(空串/0 ⇒ 败)。 ### 一处测试口径调整 `routes.test.ts` 的「上游熔断 ⇒ /detail 200 + 原因」用例:原来只熔断东财,现在兜底会真取到数据 ⇒ 同时熔断 `qt.gtimg.cn` 模拟「全线不可达」,并补 `source===null` 断言;「东财挂、腾讯活」场景由 `detailTencent.test.ts ④` 覆盖。 ### 未做 - `量比`、`涨跌家数` 腾讯单票不提供,仍在 `missing` 中列出。 ## v0.177.0 — 涨跌分布重绘 + 涨跌趋势五格接上聚合数据 `npm run check` **1417 / 1417 × 2 时区**(1410 → 1417)+ `ALL HOST CHECKS PASSED` + build(73 片段全过)。 ### 现象 实机里分布卡只有散落数字、**没有柱体**;「北交所未接入」压住 X 档名;左侧 5 格上涨/平盘/下跌 `—`、涨停/跌停 `0`。 ### 根因 1. 旧柱是行内 ``、无宽度 ⇒ 只有数字没有柱。 2. 「北交所」注记写在柱区,占 X 标签行。 3. 5 格未接聚合回包的 `up/flat/down/limitUp/limitDown`。 4. 涨跌停在东财链上不可反推,取不到时写了 0。 ### 修法 | # | 修法 | | --- | --- | | 1 | 新增 `distCard.ts`(纯几何:`distBarsOf` / `distYTicksOf` / `distConsistencyOf` / `trendCellsOf`)+ `distCardRender.tsx`(唯一柱状图实现,两处布局共用):柱按 counts 归一、最高柱满格、柱顶家数、柱下 9 个档名、柱宽均分间距 6px、Y 轴三档 + 网格 | | 2 | 「北交所 未接入」移到**卡头右侧** 12px 次要色,完整说明进 `title` | | 3 | 5 格直接取 `distribution.up/flat/down/limitUp/limitDown`;缺分布才退回旧链;**缺失显示 `—` + 原因,不写 0** | | 4 | `up + flat + down` 与 9 档合计不一致时,卡头与柱区各出一行 `口径不一致:5 格合计 X / 分布合计 Y` | ### 验证 `npm run check` **1417 / 1417 × 2 时区** + build(73 项)。 新增 `distCard.test.ts` 7 条(含注入必败):① 9 柱高度按 counts 单调、最高柱 100%(全等高 ⇒ 败)|② 柱顶家数 9 个逐项相等(错位 ⇒ 败)|③ 档名 9 个顺序正确(反转 ⇒ 败)|④ 「北交所」在卡头、渲染层不得出现(塞回柱区 ⇒ 败)|⑤ 5 格 = `[2082,144,2990,59,28]`(写死 0 ⇒ 败)|⑥ 合计一致 ⇒ 无 warn,不一致 ⇒ 文案含两个数(吞 warn ⇒ 败)|⑦ Y 轴 ≥3 档且最高档 = 最大计数(全 0 数据给 `[0,1,2]` 不崩)。 渲染核对:`/home/hu/distui-render.png`(1200×250),柱/家数/档名/Y 轴/卡头注记均无叠字。 ### 两处测试口径更新(有注释) 1. 分布图标记抽到 `distCardRender.tsx` 后,`distLayout.test.ts` / `impl78.test.ts` / `marketV1.test.ts` 的源码扫描改为**同时读渲染层**,结构断言不变。 2. `marketV1 ⑥` 的柱顶家数判据接受 `data-role=dist-count`(旧类名随旧实现删除)。 ## v0.176.0 — 涨跌分布脱离东财单点:新浪代码表 + 腾讯批量报价聚合 `npm run check` **1410 / 1410 × 2 时区**(1404 → 1410)+ selftest + build(73 片段全过)。 ### 背景 东财 `push2*` 连接层不可达(`000`),涨跌分布长期 `—`。改用「新浪代码表 + 腾讯批量报价」本地聚合。 ### 实现 | 项 | 内容 | | --- | --- | | 代码表 | 新浪 `Market_Center.getHQNodeData?node=hs_a`,**56 页 / 5 576 只 / 0 重复**,`Referer` + 页间 200ms,**当日缓存** | | 报价 | 腾讯 `qt.gtimg.cn/q=`,≤600/批、4 并发、批间 120ms、GBK;取 `[32]` 涨跌幅、`[47]/[48]` 涨跌停价 | | 聚合 | 11 桶 → **映射到现有 9 档**(只合并两个开口边界档);`DIST_LABELS` 未动 | | 接线 | `/breadth?dist=1`:东财 `emDistribution()` 优先 → `counts===null` 切 `tencentDistribution()`;回包新增 `source / coverage{codes,quoted,suspended} / partial / scannedBatches / totalBatches / warn` | | UI | 标题 `9 档 · 覆盖 N 只`(不足 ⇒ `已扫 k/N 批`);柱下常驻 `北交所 未接入(本轮只统计沪深A股)`;柱色改按**档名**判(旧 `i<5` 硬编码在 9 档下会把涨档涂成跌色) | ### 红线 - **H1**:新增 `src/host/hostFetch.ts` 的 `fetchTextHost`,内部 `withHostBreaker(host)`,熔断打开抛 `breaker-open: `;`fix3Highmed.test.ts` 白名单 **+1 条**(现 2 条),并新增「熔断打开 ⇒ `calls===0` 且抛 `breaker-open`」断言。裸 fetch 仅剩 `em.ts:96`(原有)与 `hostFetch.ts:46`(新增)。 - **R5-⑤**:日期用 `dayOf(now)`,无本机构造。 - **repoHygiene**:两个新模块均被 `routes.ts` 真实引用;顺手删除两个零引用导出。 ### 真实扫描读数(2026-10-26 21:4x,收盘后) | 项 | 值 | | --- | --- | | codes(沪深 A 股) | **5 223**(新浪 5 576 − 北交所 353) | | quoted | **5 223** | | suspended(单列) | **7** | | 涨停 / 跌停 | **59 / 28** | | 上涨 / 下跌 / 平盘 | **2 082 / 2 990 / 144**(144 + 7 = 151,与上轮互证) | | 批次 | **9/9,0 失败** | | 耗时 | 冷启动 **35.4 s**(56 页代码表 + 9 批报价);同进程第二次 **0.59 s**(代码表当日缓存) | | 9 档 | `[110, 68, 226, 1678, 144, 2039, 563, 217, 171]`,合计 **5 216 = 5 223 − 7** | ### 验证 `npm run check` **1410 / 1410 × 2 时区** + `ALL HOST CHECKS PASSED` + build(73 项)。 新增 `breadthScan.test.ts` 10 例(合计守恒、停牌不计平盘、单批失败不废整轮、缓存 0 请求、批间 120ms、并发 ≤4、11→9 映射后合计不变等)。 ### 一处判据改动(已在测试注释说明) `marketV1.test.ts ⑥` 原要求页面含字面量 `distCounts.length !== DIST_BUCKETS`,而 `DIST_BUCKETS` 仍是定稿的 **11**;本轮宿主只回 **9 档**,照旧写会永远落进「未取到」。改为 `!== distLabels.length`(档数 == 档名数),**`DIST_BUCKETS=11` 与 11 列布局均未动**。 ### 未做 - 11 档真实分档(当前 9 档映射)——另单。 - 北交所 353 只未接入(接口 20 条/页,未翻页)。 - 冷启动 35.4 s 偏长(代码表 56 页),后续可降到每日一次预热。 ## v0.175.0 — 大盘页:图表坐标系、左列紧凑、右列自适应 `npm run check` **1398 / 1398 × 2 时区**(1395 → 1398)+ selftest + build(73 片段全过)。 | # | 问题 | 修法 | | --- | --- | --- | | 1 | 两张图缺 Y 轴刻度与网格;空态只剩一条孤立基线 | `marketBlocks.ts` 新增 `X_TICKS`(09:30/10:06/10:42/11:18/13:24/14:00/14:36)、`yTicksOf`、`gridYsOf`;两图各 **4 条网格 + 4 个 Y 刻度 + 7 个 X 刻度** + 左边框 + 底轴;空态同一坐标系 + **图内居中** `—(原因)` | | 2 | 左列三卡过高、留白多 | `.tw-mk-col-l .tw-mk-card{padding:10px}`、`.tw-mk-grid2{gap:12px}`、`.tw-mk-card{min-height:0}`、标签行距 `margin-top:0` | | 3 | 右列两卡撑满、大块空白 | `.tw-mk-col-r .tw-mk-card{height:auto;min-height:0}`、`.tw-mk-col-r .tw-mk-empty{max-height:220px}` | 分钟成交额的柱改为 SVG `rect`,240 根等分。 ### 验证 `npm run check` **1398 / 1398 × 2 时区** + selftest + build(73 项)。 新增 `marketPolish2.test.ts` 3 条(含注入):① X 刻度 7 个、`yTicksOf`/`gridYsOf` 各 ≥3、两图都渲染 `gy*`/`yt*`、空态原因为图内居中 `text`(无轴 ⇒ 败)|② `padding:10px` / `gap:12px` / `min-height:0` / `margin-top:0`(写死高度 ⇒ 败)|③ `height:auto` + 空态 `max-height:220px` + 两列容器类(撑满 ⇒ 败)。 另更新 2 条被 DOM 变更取代的旧断言(`H=160` 常量、分钟卡 aria-label),判定不变。 ### 未交付 1200px 渲染图未出(上下文耗尽)。 ## v0.174.0 — 大盘页:隐藏重复的旧汇总块(最小改动) `npm run check` **1395 / 1395 × 2 时区**(1394 → 1395)+ selftest + build(73 片段全过)。 ### 问题 大盘页顶部旧汇总块(`涨跌趋势 · 沪深两市` + 成交额 + 两融一行 + 右侧涨跌分布/日内家数)与新版两列块**重复**,页面上出现两套同名标题。 ### 做法(一行级改动) 旧块根节点加三件套: ```js React.createElement('div', { className: 'tw-panel', hidden: true, style: { display: 'none' }, 'data-legacy': 'market-summary' }, ``` - **不删任何 JSX / helper / 常量**(`tw-dist-cols` / `item()` / `legendRow()` / 四个 `*_REASON` / `forecastCell` 全部原样),因此 1394 条既有断言继续通过。 - 新版两列块在旧块之后渲染 ⇒ 页面只剩一套可见标题。 ### 验证 `npm run check` **1395 / 1395 × 2 时区** + selftest + build(73 项)。 新增 `marketLegacyHidden.test.ts` 1 条:① 源码含 `'data-legacy': 'market-summary'`;② 同一 `createElement` 行内同时有 `hidden: true` 与 `display: 'none'`;③ `tw-mk-grid2` 索引 > 旧块索引;④ 去掉 `hidden: true` ⇒ 判据必败。 ### 已知限制 这是**隐藏**而非删除。旧块代码仍在源码中;完整的「行为迁移 + 删除旧块 + 重写 8 条脆性断言」需要具备 DOM 测试栈的环境,另单排期。 ## v0.173.0 — 大盘页细节打磨:短句、等分小格、图表尺寸、重试入口 `npm run check` **1394 / 1394 × 2 时区**(1388 → 1394)+ selftest + build(73 片段全过)。 | # | 差异 | 修法 | | --- | --- | --- | | 1 | 信号时间线是长段落;底部留着整段「口径说明」 | 新增 `shortSignalOf(e)`:`14:32 · 疑似护盘 · 3族 · 72分 · 量能放大 2.46…`,`|` 替换为空格、硬截 40 字;**删除「口径说明:…」整段**;缺数据统一 `—(短原因)` | | 2 | 小格未等分、内容贴边 | `.tw-mk-grid5/4/3` 改 `repeat(5/4/3, minmax(0,1fr))` + `align-items:stretch`;`cell()` 顺序改为**标签在上、数值在下**;数值 22px、标签 12px、`padding 8`、圆角 8 | | 3 | 图表过小或空白 | 成交额分时 `H=180` 并标 `X 09:30–15:00 · Y 亿元`;分钟量能高度 160,空态改为 **160px 容器 + SVG 坐标系(基线/纵轴 + 时间刻度)+ 一句原因** | | 4 | 大块 `—` 无自动尝试 | 四块共用 `mkRetry` 令牌,`breadth` / `marketExtras` / `amtTrend` 的 effect 依赖它;每张卡头右侧「重试」按钮(`tw-tab` 12px) | ### 验证 `npm run check` **1394 / 1394 × 2 时区** + selftest + build(73 项)。 新增 `marketPolish.test.ts` 6 条:① 短句 ≤40 字、无 `|`、`e.reason` 不再直出(恢复长段落 ⇒ 败)|② 页面 `口径说明:` grep 0(出现 ⇒ 败)|③ 三容器 `repeat(5/4/3, …)` 且标签在数值前(拉伸 / auto-fit ⇒ 败)|④ `H=180` 与 `height:160` + X/Y 轴说明(无轴 ⇒ 败)|⑤ 空态有坐标系 + 原因(空白 ⇒ 败)|⑥ 「重试」按钮与令牌存在(缺 ⇒ 败)。 ### 未完成 - **1200px 渲染图**未出(本轮上下文耗尽)。 - 成交额分时的**空态分支**仍是早退(`noAmountReason` 在 492 行 return),尚未换成坐标系;分钟卡已改。 ## v0.172.0 — 大盘页收口:单一 ticker、工具页卡片重排、去掉大盘多余模块 `npm run check` **1388 / 1388 × 2 时区**(1381 → 1388)+ selftest + build(73 片段全过)。 ### 1) 报价轮询合并为单一 ticker - 新增 `src/client/ticker.ts`:`subscribeTicker(fn)` 返回退订函数,`TICK_MS = 60_000`,订阅为 0 时自动 `clearInterval`。 - `useQuotes` / `mini` / `PortfolioPage` / `index.tsx` / `GlobalClockPage`(行情段)全部改为订阅;卸载退订。 - 回调仍先过 `shouldPollQuotes`(护盘过 `shouldPollRescue`)。 - **报价相关 `setInterval` 字面量:5 → 1**(时钟页 1 秒时钟、`useNow`、`PrismPage` 日期、`RescuePanel` 采样在白名单内)。 ### 2) 工具页「全球期货交易时钟」卡片重排 - 主要指数 **6 张一行**、期货自选 **5 张一行**(22px、`padding 8`、圆角 8);窄屏 6→2→1、5→2。 - 该改动属**工具页**,不回退。 ### 3) 大盘页去掉多余模块(用户裁定) - 大盘页**不渲染**「主要指数」与「期货自选」;`MarketPage.tsx` 确认无这两块与 `GlobalClockPage` 引用。 - `docs/mockups/market-v1.html` 删除这两块,模块清单改为 `涨跌家数 → 成交额 → 两融余额 → 护盘信号`。 ### 验证 `npm run check` **1388 / 1388 × 2 时区** + selftest + build(73 项)。 新增断言:`ticker.test.ts` 4 条(6/5 列与两档断点、容器接线、22px、报价 `setInterval` = 1、退订后订阅数 0、回调走闸门)|`marketModules.test.ts` 3 条(设计稿与 `MarketPage` 均不含 `主要指数` / `期货自选` / `GlobalClockPage`;`tw-mk-grid2` 在 `RescuePanel` 之前)。 `clockView.test.ts` ⑦ 有意反转为「行情走统一 ticker」。 ## v0.171.0 — 大盘页 V1 改版;指数分时换源;轮询降频与单一报价源 报告 `docs/AUDIT-MARKET-UI.md`、`docs/AUDIT-BACKOFF.md`。`npm run check` **1381 / 1381 × 2 时区**(1349 → 1381)+ selftest + build(73 片段全过)。 ### 1) 大盘页 V1(设计稿 `docs/mockups/market-v1.html`,A 单列) - **护盘信号删除展开/缩回**,全部区块常驻;六张 ETF 卡同容器 **6 等分**(<1100 三列、<700 两列);**因子明细 / 分时量能左右并排**;再往下 今日信号时间线(6 条)→ 5 分钟抽样(47 根)→ 历史回顾(30 天)。 - 新增「涨跌 / 成交额 / 两融」两列块:左 = 涨跌趋势 5 格 → 成交量 4 格 → 两融 3 格 → 成交额分时(今日/昨日/预测三线);右 = 涨跌分布 **11 档**(柱顶家数)→ 分钟成交额较昨日增减 **240 根**。 - 小格 22px / `padding 8` / 圆角 8 / 格距 6 / 卡头 3px 主色竖条;缺数据 `—(原因)`;**无 `⚠`、无免责文案**。 - 偏差:标签 11.5px → **12px**(`uiuxDiscipline` 字号阶梯)。 ### 2) 指数分时换源(③④ 的数据前提) `src/host/em.ts` 指数分时优先级改为 **trends2 → 新浪 1 分钟 → 新浪 5 分钟兜底**: | secid | source | 点数 | amount | | --- | --- | --- | --- | | `1.000001` | `local-stitch`(今段 sina-minute) | **239** | 239/239 | | `1.000300` | `sina-minute` | **239** | 239/239 | | `0.399001` | `local-stitch` | **239** | 239/239 | 5 分钟兜底标 `noAmount:'该源无成交额、48 根/日'`,③④ 显示 `—`,**不插值**。沪指由 48 根/日 → **239 根/日**。 ### 3) 「昨日分钟额」从本地归档拼 `ndays>1` 回包从 `trends//` 取昨日完整日(≥200 根且带 `amount`),附 `prev` + `prevSource:'archive'` + `prevDay`;不足则不附。**本机归档当前只有今天 ⇒ ④ 仍 `—(昨日分钟额不可得)`,明日归档有昨日后自动成形。** ### 4) 轮询降频与单一报价源 - 新 `src/shared/pollGate.ts`:`QUOTE_POLL_MS = 60s`、`RESCUE_DEFAULT_SEC = 60`([30,600])、`shouldPollQuotes`(午休/盘后/非交易日 false)、`shouldPollRescue`、`pollableSecids`(熔断 open ⇒ 仅保留有腾讯/新浪映射的)、`dedupeSecids`。 - 新 `src/client/quotePoll.ts`:`planQuotePoll` = 时段 → 去重 → 熔断过滤;`breakerOpenCached()` 读 `/health.breaker.open`,**60s 内只读一次**。 - `useQuotes` 成为**唯一发起报价请求**的 60s 轮询源;`mini` / `PortfolioPage` / `index.tsx` 回调首行加 `shouldPollQuotes` 闸门(非时段直接 return);时钟页 1s 时钟未动。 ### 每 5 分钟请求数(估算) | 场景 | 改前 | 改后 | | --- | --- | --- | | 护盘(15s→60s) | 20 | **5** | | 行情(5 页各自轮询) | 15–30 | **5** | | 午休 / 盘后 | 15–30 | **0** | | 熔断期·纯东财 | 每轮撞墙 | **0** | ### 验证 `npm run check` **1381 / 1381 × 2 时区** + selftest + build(73 项)。 新增断言合计 **32 条**:market V1 8 条(无折叠、6 等分、左右并排、22/8/8、240 根、11 档、`—(原因)`、无 `⚠`)|指数源 4 条(首选 trends2、1 分钟在 5 分钟前、`<200 根` 判失败、`noAmount` 不插值、三 source 互斥)|归档 3 条|降频 6 条|接线 7 条(唯一报价定时器、15:05 与周末 0 请求、午休 0 与 13:01 恢复、熔断退避、去重、恢复)。 ### 已知差距 `setInterval` 字面量仍 **5 个**(useQuotes / mini / PortfolioPage / index / GlobalClock);其中**只有 `useQuotes` 会发报价请求**,其余已加闸门不发。若要「字面量只剩 1 个」,需改为共享 ticker 订阅(下一轮)。 ## v0.170.0 — 做T 网格:间距按当日振幅收紧、右端不再压轴 `npm run check` **1349 / 1349 × 2 时区**(1344 → 1349)+ selftest + build(73 片段全过);只改 `src/client/**`。 | 项 | 修前 | 修后 | | --- | --- | --- | | 间距 | `ATR% ÷ 4`(ATR 8.15% ⇒ 2.04%,8 档只有 3 个可见) | `min(ATR% ÷ 4, 当日振幅% ÷ 4)`;半跨度再硬压到 `≤ 振幅%`(越界时 `间距 = 振幅% ÷ (档数/2)`) | | 振幅口径 | — | `(当日最高 − 当日最低) ÷ 昨收 × 100`;取不到退回 `ATR%/4` 并写 `ampReason` | | 右侧 | 虚线顶到轴、与刻度重叠 | 虚线 `PAD_X → PAD_X + innerW − 8`;标签宽度夹在该线内;`twr-*` 右侧档位文字 grep 0 命中;Y 刻度右移 4px | | 8 档可见性 | 大部分档位在画布外 | 标签 y 排序后强制 ≥14px 行距并整组夹回绘图区 | ### 验证 `npm run check` **1349 / 1349 × 2 时区** + selftest + build(73 项)。 新增 `tWindowFit.test.ts` 5 条:① `spacing === min(8.15/4, 3.6/4) = 0.9`(写死 `ATR%/4 = 2.0375` ⇒ 败)|② 半跨度 ≤ 振幅(三组 8.15/3.6、12/1.2、2.2/0.6;不压间距 ⇒ 越界 ⇒ 败)|③ 线与标签都在 `[plotLeft, plotRight−8]`(标签到 `plotRight−10` ⇒ 败)|④ 8 档标签数 = 8、相邻 y ≥ 13px、全在绘图区(只画 3 个 ⇒ 败)|⑤ `x > plotRight−8` 无任何做T 图元、tick 在 +4(端点改回 `innerW` ⇒ 败)。 参考稿:`docs/mockups/t-window-fit-8bands.html` + `.png`(ATR 8.15% / 振幅 3.60% / 间距 0.900% / 8 档)。 ## v0.169.0 — 做T 网格标签移到左侧,右侧 Y 轴不再重叠 `npm run check` **1344 / 1344 × 2 时区**(1339 → 1344)+ selftest + build(73 片段全过);只改 `src/client/**`。 | 项 | 修前 | 修后 | | --- | --- | --- | | 档位标签 | 画在右侧,与 Y 轴刻度重叠 | 统一画在**绘图区左内侧**:`x = plotLeft + 6`、`text-anchor:start`、垂直居中、白底 2px 描边 halo;折线经过时按 `0 / −8 / +8 / −12 / +12` 轻推 ≤8px | | 横虚线 | 不变 | 仍 `PAD_X → PAD_X + innerW` 贯穿全宽;外两档 1.5px、内档 1px | | 右侧 Y 轴 | 叠加做T 文字 | **只留刻度**(删除 `twr-*` 节点) | | 当前价气泡 | 右侧轴外 | 保持原样 | ### 验证 `npm run check` **1344 / 1344 × 2 时区** + selftest + build(73 项)。 新增 `tWindowLabels.test.ts` 5 条:① 标签 `x ∈ [plotLeft, plotLeft+80]` 且右端 ≤ `plotRight`(放到右侧轴 ⇒ 败)|② 右侧轴区无做T 文字(注入加回 ⇒ 败)|③ 标签数 == 档数(1.5%/3%/5% 三档各测,少画 ⇒ 败)|④ 标签矩形 ∩ 折线包围盒面积 = 0(构造折线穿过最上档,轻推后 0;不轻推 ⇒ 败)|⑤ 每条虚线 `x1: PAD_X → x2: PAD_X + innerW`(缩短或出现 `innerW / 2` ⇒ 败)。 ### 参考稿 `docs/mockups/t-window-left-labels.html` + `.png`(同一份几何函数渲染,均价 17.06 / ATR 3.20% / 间距 0.80% / 6 档)。 ## v0.168.0 — 分时均价(VWAP)补齐:逐点累计,缺成交额时近似并标注 报告 `docs/AUDIT-VWAP.md`。`npm run check` **1339 / 1339 × 2 时区**(1332 → 1339)+ selftest + build(73 片段全过)。 ### 根因 主源每分钟带 `amount`(可算精确 VWAP);备用新浪分钟此前只解析 `volume`,`avg` 为空 ⇒ 均价线缺失 ⇒ 做T 网格不可用。实现只取「载荷最后一点的 `avg`」,未自行累计。 ### 修复 新增 `src/shared/trendVwap.ts`:`backfillTrendAvg(points, { lotSize })` + `lastAvgOf(points)`: | 条件 | 口径 | 标记 | | --- | --- | --- | | 有 `amount` | `Σamount ÷ Σvolume` | `avgApprox = false`(精确) | | 无 `amount` | `Σ(典型价 × volume) ÷ Σvolume`,典型价 `(H+L+C)/3` | `avgApprox = true`(近似) | | `volume = 0` | 沿用前值;首点无前值 ⇒ `null` | 不写 0 | 接入 `em.ts` 两条备用链:`sinaMinuteTrend`(A股/ETF,`lotSize:100`)、`sinaGlobalMinuteTrend`。`QuoteDrawer` 用 `lastAvgOf` 作网格中心;近似时在**既有口径行**追加 `· 均价为近似(缺成交额)`。 **单位坑(实测踩到)**:A股/ETF 分钟量是**手**、额是**元** ⇒ 必须 `lotSize:100`;漏乘会让均价差 100×,被「均价须落在价格区间」判据丢成 `null`。 ### 真实末点 avg(2026-10-26 14:0x) | secid | 源 | 末点价 | 末点 avg | 标记 | | --- | --- | --- | --- | --- | | `1.600519` | sina-minute | 1341.01 | **1335.639** | 精确 | | `0.000636` | sina-minute | 37.35 | **38.295** | 精确 | | `1.510300` | sina-minute | 4.16 | **4.147** | 精确 | 实测确认新浪 CN 分钟**带 `amount`**(226 803 266 元 / 545 889 手)⇒ 精确口径;近似分支留给真正缺成交额的源。 ### 验证 `npm run check` **1339 / 1339 × 2 时区** + selftest + build(73 项)。 `trendVwap.test.ts` 7 条:① vol 100/200/300、amount 1000/2200/3600 ⇒ 末点 `11.333`(单点算 =12 ⇒ 败)|② 无 amount ⇒ `avg≈11.33` 且 `avgApprox=true`(写死 close=11 ⇒ 败)|③ `vol=0` 沿用前值、首点为 null(写 0 ⇒ 败)|④ 有 amount ⇒ `avgApprox=false`(误标 ⇒ 败)|⑤ 全无 avg ⇒ 做T 给原因;部分有 ⇒ 正常(任一点缺就禁用 ⇒ 败)|⑥ `lotSize=100`:546k 手 / 2.27 亿元 ⇒ 均价 4.15 量级(漏乘 ⇒ 415.5 ⇒ 败)。 ## v0.167.0 — 分时图「做T窗口」(可开关、按 ATR 自适应网格) 报告 `docs/AUDIT-TWINDOW.md`;设计稿 `docs/mockups/t-window.html`。`npm run check` **1332 / 1332 × 2 时区**(1323 → 1332)+ selftest + build(73 片段全过)。 ### 功能 - 分时卡右上角开关 `做T`,**按标的记忆**(`prefs.tWindow[secid]`);关闭 ⇒ `tWin=null`,图内**不添加任何节点**。 - 基准 = 当日均价;间距 = `ATR% ÷ 4`(`ATR% = ATR14 ÷ 收盘 × 100`,新增 `src/host/atr.ts`,Wilder TR 的 14 根简单平均)。 - 档数:`≤2% ⇒ 4`、`≤3.5% ⇒ 6`、`>3.5% ⇒ 8`,上下对称,半跨度 = `间距 × 档数/2`。 - **不可手动调整**:`tWindowOf` 只接受 `atrPct / avg`,无档数/间距入参,无拖拽/输入/重设。 - 不画成交点、不涉及棱镜。 - 标签:左 `偏离% + 价格`(右对齐、间距 4px、`tabular-nums`);右价格距线 6px;当前价气泡 `rx6` + 1px 边框;外两档 1.5px、内档 1px;买 `#4C6EF5`、卖 `#E8590C`;虚线 `4 3`。 - 徽标:`ATR x.xx% · n 档`。 ### 实测(ATR 取自本机日 K 缓存) | secid | close | ATR14 | ATR% | 间距 | 档数 | 半跨度 | | --- | --- | --- | --- | --- | --- | --- | | `1.601398` | 8.32 | 0.186 | **2.23%** | 0.558% | **6** | 1.67% | | `1.510300` | 4.178 | 0.078 | **1.86%** | 0.466% | **4** | 0.93% | | `0.000636` | 40.15 | 2.768 | **6.89%** | 1.723% | **8** | 6.89% | | `1.600938` | 33.72 | 1.046 | **3.10%** | 0.775% | **6** | 2.33% | ### 验证 `npm run check` **1332 / 1332 × 2 时区** + selftest + build(73 项)。 `tWindow.test.ts` 9 条:档数边界 `2.0/2.1/3.5/3.6 ⇒ 4/6/6/8`(边界写错 ⇒ 败)|间距 = `ATR%/4`(固定 0.5% ⇒ 败)|均价 +1% ⇒ 全部档价 ×1.01(网格固定 ⇒ 败)|ATR/均价缺失 ⇒ `reason`(默认 2% ⇒ 败)|关闭 ⇒ 无网格节点(去掉 `null` 短路 ⇒ 败)|无 drag/input/reset/成交点|标签 `17.05 +0.62%` 格式(缺偏离 ⇒ 败)|徽标模板与两色值|ATR14 纯函数(15 根 ⇒ ATR=2 / close=24)。 ### 两处必要偏差 1. 设计稿 11px ⇒ **实现 12px**(`uiuxDiscipline` 锁死详情页字号阶梯 12/12.5/13/13.5/14,11px 判失败)。 2. 新增 `/tradewatcher/atr`(仅开关打开时请求)⇒ **路由账 33 → 34(core 16 → 17)**,README、设计文档、两处断言已同步。 ### 已知边界 **均价来自最近一次分时载荷**(切页签或 5 分钟缓存刷新才更新);网格每个行情节拍重算,但中心基准要等载荷刷新 —— 实时报价里没有 VWAP 字段。 ## v0.166.0 — 筹码分布:删除冗长口径注解(纯删除,无替代文案) `npm run check` **1323 / 1323 × 2 时区**(1319 → 1323)+ selftest + build(73 片段全过)。 ### 删除 | 位置 | 内容 | | --- | --- | | `chipsView.ts` | `caliberDeviates` 整体下线(`只取到近 N 日(不足 M 日)——历史越长越准` 及后附整段) | | `chips.ts` | 载荷 `bucketNote` 措辞改为「回包大小上限 / 档数 A → B」,**卡片不再渲染** | | `klineUnits.ts` | 改为「⇒ 无法判定量纲 ⇒ 不改数(只陈述数字,不推断单位)」(不显示在卡片) | | 容器 | `role:'note'` 的 `tw-chips-narrow` 整块 + 标题悬停里的 `⚠ ${bucketNote}` + `methodology` 里的 `⚠` 拼接 | **保留**:`▾ 筹码分布`、四格(平均成本 / 支撑 / 压力 / 盈利比例)、`YYYY-MM-DD · 样本 N 天`、百分位列表与图例。 **未加任何替代文案**:卡片内 `量纲` / `未确认` / `⚠` 均为 **0 处**。 ### 验证 `npm run check` **1323 / 1323 × 2 时区** + selftest + build(73 项)。 新增 `chipsNoteRemoval.test.ts` 4 条:① 面板不含 `回包体积预算` / `384 档降到 242` / `判不了` / `只报数` / `历史越长越准` / `量纲` / `未确认` / `⚠`(旧句放回 ⇒ 败)|② 四格仍在(删「盈利比例」⇒ 败)|③ `样本 ${day.samples} 天` 模板仍在(删样本行 ⇒ 败)|④ 可选行表达式由 `(deviate?1:0)+(narrow?1:0)` 变 `d.narrow?1:0`、无 `role:'note'`、`tw-chips-narrow` 仅 1 处、绘图区变高(旧表达式 ⇒ 败)。 `build.mjs` 未改动:锁定片段 `该标的不支持前复权(指数/期货/板块)⇒ 不提供筹码分布` 原在被删函数里,已移回「不支持前复权」分支,构建恢复 73 项。 **有意更新既有断言 3 处**:`chipsView.test.ts`(删 `caliberDeviates` 用例)、`chips.adaptive.test.ts` ⑤(改为「面板不得引用 `bucketNote`」)、`fix7Host.test.ts`(文案「判不了/未改动」→「无法判定/不改数」,语义不变)。 ### 备注 整个客户端包仍有 **21 处 `⚠`**、**1 处 `量纲`**(来自其它组件),本轮未动。 ## v0.165.0 — 订正历史拆单佣金(退回 99.05 元) 报告 `docs/AUDIT-FEE-CORRECTION.md`。`npm run check` **1319 / 1319 × 2 时区**(1316 → 1319)+ selftest + build(73 片段全过)。 ### 数据订正(用户批准) | 项 | 数值 | | --- | --- | | 同组实际 fills | **24 笔**(23 × 5 062 @8.60 + 1 × 3 574 @7.54)——界面按 bar 合并显示为 26 行统计口径不同,磁盘实际 24 笔 | | 旧费用 | **205.34** | | 重算应收 | **106.29**(佣金 21.09 + 印花税 84.36 + 过户费 0.84) | | **订正金额** | **99.05** | | 现金 | 263 795.28 → **263 894.33**(+99.05) | | 已实现盈亏 | −43 773.36 → **−43 674.31**(+99.05) | `feeCorrections` 新增 1 条:`{secid:'1.588170', orderIds:['o-1792984516628-1.588170-sell-0.703-120000'], beforeFee:205.34, afterFee:106.29, credit:99.05, reason:'拆单重复收取最低佣金'}`。 **原始 fills 未改**;`prism-orders.json` 与备份 `deepEqual`;备份在 `/tmp/prism-{account,orders}-feecredit-20261026-115236.json`。 ### 未按 223.60 硬编码 用户口径为 `26 × 8.60 = 223.60`,但磁盘同组只有 24 笔、旧费用 205.34。若按 223.60 加钱会**多退 18.26**,因此按实际 `beforeFee` 计算。 ### 验证 `npm run check` **1319 / 1319 × 2 时区** + selftest + build(73 项)。 新增 `prismFeeCorrection.test.ts`:夹具 26 笔 ⇒ `beforeFee 223.60 / afterFee 116.58 / credit 107.02`(硬编码 125 ⇒ 败)|现金与 realized 增量各 = credit(不等 ⇒ 败)|原始 fills `deepEqual`(改动 ⇒ 败)|重复执行 `duplicate=true`、`credit=0`、`feeCorrections.length=1`、现金不变(二次加钱 ⇒ 败)|无匹配 fills ⇒ 拒绝且不写账。 ### 生效条件 live `prism-account.json` 已写;**宿主内存需重启读盘后体现**。 ## v0.164.0 — 棱镜:拆单根因(量能按 bar 累计、成交合并显示、最低佣金只收一次) `npm run check` **1316 / 1316 × 2 时区**(1312 → 1316)+ selftest + build(73 片段全过)。 ### 现象 成交表出现 **26 笔同一时间(11:16:00)、同一价格(0.70)** 的细碎成交,每笔 5 062 股、费用 8.60。 ### 三条根因 | # | 根因 | 修法 | | --- | --- | --- | | 1 | **量能上限按 tick 重置**:每个 tick 都从 `volume × rate` 重新算,同一分钟 bar 的量被反复使用 | 按 `secid + barTime` 汇总 `filledInBar`,每 tick 上限 = `volume × rate − filledInBar`;订单记 `lastBarTime`,同一 bar 只处理一次 | | 2 | **UI 逐笔渲染** | 按 `(day, barTime, secid, side, price)` 合并为一行;数量与费用合计;明细仍保留在 `fills` 与磁盘 | | 3 | **最低佣金逐笔重复**:`feesOf` 对每笔成交各取 `max(5, 金额×费率)` | 按同一委托**累计金额**算应收佣金,再减已收;最低 5 元只收一次;`PrismFill.commission` 留痕 | ### 验证 `npm run check` **1316 / 1316 × 2 时区** + selftest + build(73 项)。 新增断言:bar `volume=50620`、rate 10% ⇒ 首次成交 5 062 股、同 bar 第二次 **0**、累计仍 5 062(每 tick 重置 ⇒ 26×5 062 ⇒ 败)|同一 bar 处理两次 ⇒ 第二次 0|同一委托跨两根 bar ⇒ 佣金合计 **5 元**(首笔 5、次笔 0,而非 10)|26 笔同 bar 同价 ⇒ 合并 **1 行**、数量 **131 612 股**、费用 **223.60**,原始数组仍 26 条。 ### 未追溯修正 历史 26 笔的**费用按原记录保留**(合计 223.60,其中含重复收取的最低佣金约 **125 元**);**新成交**不再重复收费。是否订正历史账本待用户决定。 ## v0.163.0 — 棱镜:成交页分段控件被压扁 `npm run check` **1312 / 1312 × 2 时区**(1309 → 1312)+ selftest + build(73 片段全过);只改 `src/client/**`。 ### 根因 1. `.tw-prism-seg` 在左栏纵向 flex 中**没设 `flex-shrink:0`**,长成交表把它压到约 **8px**。 2. `.tw-prism-tablecard` 没有独立滚动区,靠压缩兄弟元素腾空间。 3. `.tw-prism-left` 缺 `min-height:0`,子项无法正确滚动。 ### 修法 - `.tw-prism-seg`:`flex: 0 0 auto; min-height: 32px; align-self: flex-start`。 - `.tw-prism-tablecard`:`flex: 1 1 auto; min-height: 0; overflow: auto`。 - `.tw-prism-left`:`min-width:0; min-height:0; overflow:hidden`。 ### 读数 | 成交行数 | 分段高度 | | --- | --- | | 0 | **32px** | | 26 | **32px** | | 200 | **32px** | 分段三项宽度之和 **147px**(3 × 49px),不随行数变化。 ### 验证 `npm run check` **1312 / 1312 × 2 时区** + selftest + build(73 项);正常 + UTC 两轮干净复跑。 新增 `prismSegFix.test.ts` 3 条:0/26/200 行高度均 32px(删 `flex:0 0 auto` ⇒ 200 行变 8px ⇒ 败)|宽度和 147px(`min-width:49px` 改 0 ⇒ 败)|表格容器同时含 `flex:1 1 auto` / `min-height:0` / `overflow:auto`(删任一 ⇒ 败)。 ## v0.162.0 — 棱镜:挂单/成交只显示当天(磁盘保留历史) `npm run check` **1309 / 1309 × 2 时区**(1304 → 1309)+ selftest + build(73 片段全过);只改 `src/client/**`。 | # | 项 | 实现 | | --- | --- | --- | | 1 | 按北京日期过滤 | 新增 `beijingDayOf` / `todayBeijing`(基于 `shDayText`,**不用 UTC 日**);挂单按 `o.day`(缺失用 `decidedAt`)、成交按 `f.at` 的北京日期;页面每 **30 秒**重算 `today`,跨零点自动切换 | | 2 | 表头计数 | `挂单(今日 N 笔)` / `成交(今日 N 笔)` | | 3 | 空态 | `—(今日无挂单)` / `—(今日无成交)`;不显示昨日记录,也不显示「共 N 笔历史」 | | 4 | 数据保留 | **只在渲染层过滤**,不删除、不写 `prism-orders.json`、不 `splice`;历史仍用于累计盈亏、净值与复盘 | ### 验证 `npm run check` **1309 / 1309 × 2 时区** + selftest + build(73 项);正常 + UTC 两轮干净复跑。 新增 `prismDay.test.ts` 5 条:① 3 笔昨日 + 2 笔今日 ⇒ 只 2 行且标题 `今日 2 笔`(5 行 ⇒ 败)|② 成交同理(显示昨日 ⇒ 败)|③ 北京 23:59 算当天、00:01 算次日(用 UTC 日 ⇒ 败)|④ 空态文案固定、页面不含 `暂无数据` 或历史计数(⇒ 败)|⑤ 过滤前后源数组 `deepEqual` 不变、页面不含写入或 `splice`(误删 ⇒ 败)。 ## v0.161.0 — 全球期货交易时钟:周日弧、指数映射、缺失分类、JM 补位、区间列与倒计时 `npm run check` **1304 / 1304 × 2 时区**(1294 → 1304)+ selftest + build(73 片段全过)。 ### 首轮实机对比的 4 处差异 + 2 处数据错误 | # | 问题 | 根因 | 修法 | | --- | --- | --- | --- | | 1 | 表盘完全没有时段弧 | 画弧用「按 `days` 过滤后的当天窗口」,10-25 周日 ⇒ 黄金组 0 条 | 新增 `sessionWheelSpans()`:只按当地墙钟折算轮盘分钟、**不看 `days`**;`sessionStatus` 只管状态。周日照画 **6 条弧 + 6 个标签** | | 2 | 深证成指 / 创业板指无数据 | 深市 secid 前缀写成 `1.` | 改 `0.399001` / `0.399006`,新浪兜底生效 | | 3 | 缺失文案把指数写成「期货代码」 | 缺失清单未分类 | 新增 `missingNote()`:`缺数据:指数 2 个(399001/399006)· 期货 0 个` | | 4 | 期货卡少 JM 焦煤 | 规格表漏 `112.JM0` | 补 AU/AG/CU/**JM**/LC 五张 + GC 第六张 | | 5 | 「时段区间」列渲染成当前时刻;倒计时出现 `24:22:21` | 渲染夹具写错(页面取窗口起点);`nextStartSeconds()` 跨周末会 >24h | 新增 `sessionRangeText()`(配置 `start–end` + `次日`);倒计时按**轮盘分钟取模**,恒在 `[0, 86400)` | | 6 | 全部卡片涨跌% 都是 `+0.86%`;`GC` 显示 2 412.50 | **渲染夹具**给所有行塞同一 `pct`、GC 价格是编的(页面本身逐卡取 `items[secid]`) | 夹具改为真实值;新增断言锁死「逐卡取自己的 `pct`/`source`/`at`」 | | 7 | 表盘在卡片内偏小(约 60%) | `max-width:440px` | 去掉 ⇒ 实测 **564/590 ≈ 95%** | **黄金组时段改为设计稿的北京窗口**(21:00–02:30 / 09:00–15:00 / 06:00–17:00 / 15:00–23:00 / 20:00–02:30);原油与股指仍走交易所时区 + DST。 ### 验证 `npm run check` **1304 / 1304 × 2 时区** + selftest + build(73 项)。 新增 5 条断言(各配反例必败):① 6 张指数 `pct` 两两不同且逐卡等于夹具(全 +0.86 ⇒ 败)→ 实得 `+0.89 / +2.79 / +4.16 / +1.71 / +2.39 / +2.38`|② 期货同(全同值 ⇒ 败)→ `−0.87 / −1.05 / +0.99 / 0.00 / 0.00 / −1.29`,GC `4 007.91`|③ 区间列等于配置 `start–end`(等于当前时刻 ⇒ 败)|④ 倒计时 `0 ≤ v < 86400`(`24:22:21` ⇒ 败)|⑤ 每卡 `source`/`at` 取自自己那行(复用第一张 ⇒ 败)。 ### 渲染核对(670 px 视口) 弧 6 条、金线固定 12 点、表盘 95% 卡宽、区间列 5 行正确(含两处「次日」)、倒计时全 < 24 小时、6 组指数与 6 组期货涨跌各不相同、GC `4 007.91`。 ### 已知小瑕疵 顶部「上金所·AU9999」与「上期所·AU」两个弧内标签轻微压字(弧带已分开,文字重叠),留作下一轮打磨。 ## v0.160.0 — 新增【工具】「全球期货交易时钟」 报告 `docs/AUDIT-GLOBAL-CLOCK.md`;设计稿 `docs/mockups/global-clock.html`。`npm run check` **1294 / 1294 × 2 时区**(1286 → 1294)+ selftest + build(73 片段全过)。 ### 新增 | 文件 | 内容 | | --- | --- | | `src/shared/clockSessions.ts` | 三组时段(黄金 / 原油 / 主要股指)+ `Intl` 时区与夏令时换算 + 状态 / 倒计时 / 轮盘角 / 几何常量 | | `src/client/clockView.ts` | 卡片模型、时区选项、圆心自检 | | `src/client/tools/globalClock.ts` | 工具注册 `name: '全球期货交易时钟'` | | `src/client/GlobalClockPage.tsx` | 页面(表盘 + 时段表 + 指数 / 期货卡) | | `src/client/clockView.test.ts` | 8 项断言 | 改动:`toolRegistry.ts`(第三项 + `page` 判别位 `'leverage' | 'globalClock'`)、`ToolsPage.tsx`、`styles.ts`(`.tw-clock-*`)、`shared/model.ts` + `host/store.ts`(`clockRefreshSec`,复用 `/tradewatcher/prefs`)。**无新路由**(只消费既有 `/quotes` 与 `/prefs`)。 ### 行为 - **表盘**:`wheelAngle = −(分钟 × 0.25°)`,整盘逆时针;**金线固定 12 点**(不带 transform、在旋转组外);刻度与标签反向自转保持水平;弧宽 **12**、半径 **160/140/120/100**、净空隙 **8**、圆头端点、halo 4px;中心圆盘 `r=64` 与三行文字共用环心。 - **时钟**:1 秒更新;`立即校时` 用服务端时间差校正。 - **时区**:默认北京,可切本地 / 常用时区;**只影响显示,不改弧角度**。 - **数据**:复用报价层(东财优先、失败切新浪);缺数据 `—` + 原因,每卡带 `source` 与 `at`。 - **刷新**:行情 **120 秒**(`clockRefreshSec`,30–600 收敛),页面显示上次刷新时刻。 - 设计稿收尾:缺失代码压成一行小字;期货 5 张卡改为有数据,另留 1 张无数据示例。 ### 验证 `npm run check` **1294 / 1294 × 2 时区** + selftest + build(73 项);本地与 `TZ=UTC` 各跑一遍。 新增 8 条断言各配反例注入:① 旋转量负且等于 `−(分钟×0.25)`(正转 / 固定 ⇒ 败)|② 金线 `x1==x2==环心x`、`y2==外圈顶`、`transform===null` 且在旋转组外(随盘转 ⇒ 败)|③ 弧宽 12 / 半径差 20 / 净空隙 8(8.5 / 24 ⇒ 败)|④ 文字中心与环心 ≤1.5(`margin-top:24px` ⇒ 败)|⑤ `20:37` 时 XAU 与 AU9999「交易中」、上期所夜盘「时段外」、倒计时 1380s(全「交易中」⇒ 败)|⑥ 缺数据 `—` + 原因且不含 `0.00`(0 占位 ⇒ 败)|⑦ 刷新 120000ms(1000ms ⇒ 败)|⑧ 切时区角度不变(角度随当地分钟 ⇒ 败)。 ### 未做 / 边界 - **未做真实浏览器可视化验收**(本轮不重启)。 - 分道用「时间重叠贪心」,最多 4 道;极端情况同品种日夜盘共道。 - 期货 5 个主连只有东财源,熔断即 `—` + 原因。 - 节假日与临时休市未建模(只按周历)。 - 为容纳第三张工具卡,改了两处硬编码断言(2 → 3)。 ## v0.159.0 — 新增新浪期货/指数报价源(nf_ / hf_ / sh|sz) 报告 `docs/PROBE-SINA-FUTURES.md` + `docs/AUDIT-SINA-FUTURES.md`。`npm run check` **1286 / 1286 × 2 时区**(1279 → 1286)+ selftest + build(73 片段全过)。 ### 为什么加 上期所主连(`113.aum/agm/cum/jmm/lcm`)、`101.GC`、`100.N225`、`118.AU9999` **只有东财一个源**,东财熔断时全部无数据。新浪 `nf_`(国内期货)/ `hf_`(外盘)/ `sh|sz`(指数)可补。 ### 侦察(`hq.sinajs.cn`,2026-10-25 20:30–20:45;必须带 `Referer`) | 代码 | 名称 | 现价 | 上游时间 | 量 | | --- | --- | --- | --- | --- | | `nf_AU0` | 黄金连续 | 861.620 | 02:30:00 @10-24 | 142 413 手 | | `nf_AG0` | 白银连续 | 13 116.000 | 02:30:00 | 289 441 | | `nf_CU0` | 铜连续 | 108 370.000 | 01:00:00 | 39 890 | | `nf_JM0` | 焦煤连续 | 1 410.000 | 23:00:00 @10-23 | 265 613 | | `nf_LC0` | 碳酸锂连续 | 97 280.000 | 15:01:07 @10-23 | 459 010 | | `hf_GC` / `hf_SI` / `hf_CL` / `hf_NG` / `hf_HG` | 纽约金/银/油/气/铜 | 4007.910 / 54.035 / 103.493 / 3.502 / 650.085 | 04:59:5x @10-24 | 无 | | `hf_XAU` / `hf_XAG` | 伦敦金/银 | 3997.79 / 53.91 | 04:54:00 | 无 | | `sh000001` 等 6 条指数 | 沪深指数 | 与东财一致 | 15:00–15:35 @10-23 | 有 | ### 三族字段(实测,互不相同) - `nf_`:价 8、开 2、高 3、低 4、昨收 5(**为 0 时用昨结算 10**)、量 14、时间 17+1。 - `hf_`:价 0、昨收 7、买 2、卖 3、高 4、低 5、时间 6、**今开 8**、日期 12、名称 13。 - 指数:开 1、昨收 2、**价 3**、高 4、低 5、量 8、额 9、时间 30+31。 ### 实现 - 走既有报价层(`withHostBreaker('hq.sinajs.cn')` + `fetchSinaQuotes` 5 条一批 + 行情槽有效期缓存),`source` 标 `sina-futures` / `sina-index` + `at`;东财在链上、失败自动切新浪;两家都失败 ⇒ 无行 + 「已试:东财、新浪」。 - 映射:`112.AU0/AG0/CU0/LC0` → `nf_*`;`101.GC00Y/CL00Y/NG00Y` → `hf_GC/CL/NG`;`122.XAG` → `hf_XAG`;6 条指数**逐条登记**(不给 A 股加通配)。 - **顺带修两个真实解析错误**:`hf_` 的「今开」一直读的是卖价(下标 3);`nf_` 昨收常为 0 导致涨跌幅恒缺。 ### 验证 `npm run check` **1286 / 1286 × 2 时区** + selftest + build(73 项)。 新增 **7 条**断言:三族各用真实回包单测(`nf_AU0` 861.62、昨收 0 → 昨结算 869.2、量 142 413;`hf_GC` 今开 4042.2 而非卖价 4010;`sh000001` 3690.4753 / 额 8.99e11 / 15:35:30)|东财失败 + 新浪成功 ⇒ `source` 正确且 `missing` 为空(仍报缺失 ⇒ 败)|两家都失败 ⇒ 无行 + 「已试:东财、新浪」(0 价占位 ⇒ 败)|兜底行 `source` + `at` 必存在。 **实测**:东财整体不可达时,14/14 有价、0 missing、448ms。 ### 未做 / 风险 - 国内期货**分钟线不接**:实测末点写 `2026-10-26 00:00:00`(未来时刻),与快照最后成交 10-24 02:30 矛盾 ⇒ 时间戳不可信,宁可没有。 - 指数分钟线、报价时刻常驻标注另案。 ## v0.158.0 — 外盘分时覆盖扩到 8 个品种;出处与陈旧标注补齐 报告 `docs/AUDIT-INTL-MINUTE.md` §5。`npm run check` **1279 / 1279 × 2 时区**(1273 → 1279)+ selftest + build(73 片段全过)。 ### 侦察实测(新浪全球期货分钟,20:05–20:10) | symbol | secid | 条数 | 末点 | 有量 | | --- | --- | --- | --- | --- | | `CL` | `101.CL` 原油 | 1378 | 10-24 04:59 | 否 | | `GC` | `101.GC` 黄金 | 1379 | 10-24 04:59 | 否 | | `SI` | `101.SI` 白银 | 1365 | 10-24 04:59 | 否 | | `HG` | `101.HG00Y` 铜 | 1321 | 10-24 04:59 | 否 | | `NG` | `101.NG` 天然气 | 1121 | 10-24 04:59 | 否 | | `OIL` | `112.B00Y` 布伦特 | 1270 | 10-24 05:59 | **是** | | `NK` | `100.N225` 日经 | 673 | 10-24 05:03 | **是** | | `XAG` | `122.XAG` 伦敦银 | 1373 | 10-24 04:53 | 否 | | `KS200` 等 13 个候选 | `100.KOSPI200` | **0** | — | — | ### 采用方案 - `101.CL/GC/SI/HG/NG`:`sina-global-min`(代码表 `SINA_GLOBAL_MIN`,去掉 `00Y` 后缀再查)。 - `112.B00Y` → `OIL`、`100.N225` → `NK`:**代理**,口径行自曝(ICE 布伦特连续 / SGX 日经期货,**不是该合约与现货本身**)。 - `106.FRO` / `107.VIXY`:改走同一条 `trendWithFallback` loader,带 `source: sina-us-min` + `at`。 - 兜底快照:`source` 缺省 `lkg-snapshot`;`nonSession.reason` 写「当日分时源不可用,最后交易日 YYYY-MM-DD(显示本地兜底快照)」。 - `annotate()` 抽为模块级 `annotateTrendPayload()`,**休市定稿短路复用也走同一套标注**(原来直接返回 ⇒ 旧图无 `stale`、无日期、`source` 为空)。 ### 仍未取到 - **`100.KOSPI200`:无任何源**(实测 KS200 / KOSPI / KOSPI200 / K200 / KS / KRX / NKD / NIY / SPI 全空)⇒ `trend:null` + `missing{why:'no-source'}`,note 点名「已试:东财 trends2、新浪全球期货分钟(无对应代码)、腾讯分钟(仅沪/深/港)、本地兜底快照」。 - **未接**:`100.HSI`、`100.SPX/NDX/DJI`、`100.FCHI/FTSE/SX5E/GDAXI`(本轮未扩;欧洲指数无源)。 ### 验证 `npm run check` **1279 / 1279 × 2 时区** + selftest + build(73 项)。 新增 **6 条**断言:`CL/GC/SI` 各有可用源且末点距取数 ≤18h(不接新源 ⇒ 败)|`100.KOSPI200` 回 `trend:null`+`missing` 含「已试:」(只回 `{trend:null}` ⇒ 败)|`122.XAU` / `101.CL` / `106.FRO` 三条路径必带 `source`+`at`(删掉 ⇒ 败)|`100.N225` 兜底必带 `stale:true`+「最后交易日 2026-10-13」(旧文案无日期 ⇒ 败)|落后 14h 必给 `lagMinutes/lagReason`、只差 2 分钟必不给|美股不得 early-return。 **有意更新既有断言 1 处**:`usChart.test.ts` ②b 由「真实多日源不标 source」反转为 `source === 'sina-us-min'`(与「国际回包必须带出处」一致)。 ## v0.157.0 — 国际商品分时换源:伦敦金现接新浪全球期货分钟,滞后自曝 报告 `docs/AUDIT-INTL-MINUTE.md`。`npm run check` **1273 / 1273 × 2 时区**(1263 → 1273)+ selftest + build(73 片段全过)。 ### 根因 `122.XAU`(伦敦金现)的当日分时链**只有东财 `trends2` 一条源**(`em.ts:1831-1857`);新浪 `sinaSymbol` 只认 sh/sz、腾讯 `tencentCode` 只认 0/1/116 ⇒ 备用源全为 `null`。东财对该标的不回数据时,整链落到 `trends-lkg` 的 **10-21 快照**(与实时价相差 3 天)。 ### 第一步实测(19:14–19:35 真实回包;**今天是周六**,现货金最后成交 04:54 后休市) | 源 | 条数 | 最后一点 | 距 19:14 | | --- | --- | --- | --- | | 东财 `trends2`(push2his / push2delay) | — | 整体不可达(CN 对照 `1.600519` 同样失败) | — | | 腾讯 minute(`hf_XAU` 等) | 0 | `code param error` | — | | 腾讯 mkline | 0 | `param error` / 301 | — | | 腾讯 / 新浪报价 | 快照 | tick **10-24 04:54**(3997.79 / 昨收 4024.26) | ≈14h20m | | 新浪 CN / 美股分钟 | 0 | `null` / `x(null)` | — | | **新浪全球期货分时 `symbol=XAU`** | **1374** | **10-24 04:53(3996.80)** | **≈14h21m** | ⇒ 19:14 仍能给「最近交易时段」数据的**只有新浪全球期货分时**。 ### 采用方案 - 新源接入既有数据层:`sinaText` → `withHostBreaker('stock2.finance.sina.com.cn')` → `ttlCache(60s)` → `normalizeTrendSeries`,`source:'sina-global-min'`。 - **只对 `122.*` 换链**(`fetchTrend` 按市场选源);CN「当日链与五日链同源同序」不变式未动。 - **滞后由宿主判定**:`TREND_LAG_WARN_MINUTES=15` 单一定义;沪深/港股只在盘中、美股只在美盘、国际商品恒判;随包给 `at` / `lagMinutes` / `lagReason`。 - 抽屉口径行追加「· 数据滞后 X 小时(分钟源:… · 报价源:… · 原因:…)」;`122.*` 缺失原因改写。 ### 改后实测 `fetchTrend('122.XAU',1)` ⇒ `source: sina-global-min`、**1373 点**、首 10-23 06:01、**末 10-24 04:53(3996.80)**、`prePrice 4024.26`(与 `hf_XAU` 昨收一致)、`sessionDay 2026-10-24`、`at` 有值、`lagMinutes 871`、`lagReason "上游此刻没有更新的分钟线(本次取数成功)"`。旧实现同一时刻给 **10-21 14:08**。 ### 验证 `npm run check` **1273 / 1273 × 2 时区** + selftest + build(73 项);`build.mjs` **无需改锁**。 新增 **10 条**断言:末点 = now − 2 分钟 ⇒ 差 2 分钟(仍走旧链 ⇒ 错差 4000 分钟 ⇒ 败)|`source`/`at` 必存在、不滞后不给 `lagMinutes`|now = 末点 + 120 分钟 ⇒ `lagMinutes 120` + 原因(硬置 null ⇒ 败)|边界 15 不告警、16 告警|无源 ⇒ `fetchTrend === null` + 点名(旧曲线冒充 ⇒ 败)|只有 10-21 旧数据 ⇒ `nonSession` + 日期 + 滞后(去掉 `nonSession` ⇒ 败)|界面口径行五项齐全、滞后格式(45 分钟 / 1.5 小时 / 5 小时)、接线判据。 **有意更新既有断言 1 处**:`chart-source.test.ts` 的 `122.*` 措辞(腾讯 → 新浪全球期货分钟;旧措辞已与事实不符)。 ### 未做 / 权衡 - `122.*` 仍先试东财(唯一给 VWAP + 每分钟成交额的源,缓存 30–60s);五日档对 `122.*` 仍本地拼接(新浪这条只有 `minLine_1d`)。 - 滞后原因不写「休市」(无国际交易日历,只陈述「上游此刻没有更新的分钟线」)。 - `100.` / `112` / `113` / `114` 未改(各有可用源)。 ## v0.156.0 — 棱镜:决策理由与上限依据改为卡片内完整换行(去掉省略号) `npm run check` **1263 / 1263 × 2 时区**(1262 → 1263)+ selftest + build(73 片段全过);只改 `src/client/**`。 | # | 修前 | 修后 | | --- | --- | --- | | 1 | 「决策理由」用 `-webkit-line-clamp:3` + 省略号 + `title` 截断 | **移除截断**,完整多行显示,卡片高度随内容增长 | | 2 | 文本未换行、向右溢出到右栏 | `.tw-prism-left` / `.tw-prism-decisioncard` `min-width:0; overflow:hidden`;`.tw-prism-decision` `table-layout:fixed; width:100%`;单元格 `white-space:normal; overflow-wrap:anywhere; word-break:break-word`;两个字段**无 `nowrap`**(「本次夹紧」只保留等宽字体) | | 3 | 「仓位上限依据」三块中「本次夹紧」禁止换行 | 三块均可多行,块间 2px;缺数据仍写 `—(原因)` | ### 验证 `npm run check` **1263 / 1263 × 2 时区** + selftest + build(73 项);正常 + UTC 两轮干净复跑。 新增 5 条断言(`prismBatch3Ui.test.ts`):① 长文本 ≥200 字 ⇒ `lines ≥ 2` 且 `truncated === false`(省略号或单行 ⇒ 败)|② 渲染宽度 ≤ 内容宽度(越界 1px ⇒ 败)|③ 左栏仍 62%(63% ⇒ 败)|④ 卡片高度随内容增长(不变 ⇒ 败)|⑤ 两字段 CSS 不含 `nowrap` / `line-clamp` / `ellipsis`,页面不含 `title={r.value}`(出现任一 ⇒ 败)。 ## v0.155.0 — 棱镜:决策理由不越界、删除版本指纹行 `npm run check` **1262 / 1262 × 2 时区**(1260 → 1262)+ selftest + build(73 片段全过);只改 `src/client/**`。 ### 1) 决策理由穿透右栏 - `.tw-prism-left` / `.tw-prism-decisioncard`:`min-width:0; overflow:hidden`;决策表 `table-layout:fixed`;单元格 `white-space:normal; overflow-wrap:anywhere; word-break:break-word`。 - 「决策理由」`-webkit-line-clamp:3` + ellipsis,全文放 `title`;「仓位上限依据」仍为 3 个单行块,仅「本次夹紧」用等宽 + `nowrap`。 - 新增纯函数 `decisionReasonLayoutOf` / `decisionReasonFits`,页面与测试共用。 ### 2) 删除版本指纹行 - 删除研判卡下方整个 `
` 与 `versionSummaryOf` UI helper;界面不再渲染 `版本 —` / `指纹 —` / `旧记录无版本绑定`。 - **数据保留**:`PrismRun.codeVersion` / `mandateHash` 与 run 回包不变,`prismPipeline.ts` 仍写入。 ### 验证 `npm run check` **1262 / 1262 × 2 时区** + selftest + build(73 项);正常 + UTC 两轮干净复跑。 新增 2 条断言:① 长文本在左栏宽 300 时 `width ≤ 300`、`lines ≤ 3`、`truncated=true`(渲染 301 / `maxLines=5` ⇒ 败);CSS 含 `overflow-wrap:anywhere` 且长文本不含 `nowrap`(注入 nowrap ⇒ 败)。② 页面不含 `版本 —` / `指纹 —` / `旧记录无版本绑定` / `tw-prism-verinfo`(注入一行 ⇒ 败);`src/shared/prism.ts` 仍含两字段(删数据 ⇒ 败)。 ## v0.154.0 — 回填 10-22 净值点 capital;回撤闸门首次可用 留痕 `docs/AUDIT-PRISM-EQUITY-BACKFILL.md`。`npm run check` **1260 / 1260 × 2 时区**(1259 → 1260)+ selftest + build(73 片段全过)。 ### 数据订正(用户批准) `2026-10-22` 净值点补 `capital = 1500000`(反推依据 `unrealized = equity − capitalLimit` ⇒ `1 500 000.00 = 1 281 801.66 − (−218 198.34)`);`benchmarkClose` 保持 `null`(当时无基准,**未编造**)。改前备份 `/tmp/prism-equity-backfill-20261024-111312.json`;`2026-10-23` 点未修改。 ### 回填后读数 | 项 | 值 | | --- | --- | | 评估 `n`(独立窗口) | **1** | | 超额(含费 / 不含费) | `null` / `null`,原因「基准 1.000300 收盘缺失」 | | 回撤(单日 / 累计) | **−11.02% / −11.02%** | | 闸门状态 | **已拒新买单**;目标总仓 ≤40%、现金 ≥25% | | 命中率 | `—(样本不足 n=1 < 30)` | ### 验证 `npm run check` **1260 / 1260 × 2 时区** + selftest + build(73 项);正常 + UTC 两轮干净复跑。 新增 1 条断言(5 个反例注入必败):`capital === 1500000`(写 2000000 ⇒ 败)|`benchmarkClose === null`(编造 ⇒ 败)|闸门返回数字且 `reason:null`(返回 `—` ⇒ 败)|超额 `null` 且原因含「基准 1.000300」(给数字 ⇒ 败)|`10-23` 点与原始对象 `deepEqual`(改到它 ⇒ 败)。 ### 注意(行为变化) 累计回撤 −11.02% 已越过 −10% 阈值 ⇒ **新的买入委托会被拒绝**,直到回撤收窄或闸门复位。卖出不受影响。 ## v0.153.0 — 棱镜:分段控件间隙、决策卡依据三块、渲染级断言 `npm run check` **1259 / 1259 × 2 时区**(1257 → 1259)+ selftest + build(73 片段全过);只改 `src/client/**`。 | # | 修前 | 修后 | | --- | --- | --- | | 1 | 分段控件三个标签挤成一串 | 容器 `padding:2px` + `gap:4px`,每项 `min-width:49px`;选中 `rgba(94,106,210,.18)` + `#2f3aa0` + `700`,未选中 `transparent` + `--tw-dim` + `400` | | 2 | 决策卡「仓位上限依据」是一整段自动换行 | 拆成 **3 个独立块**(各 `white-space:nowrap`、块间 2px、次要色,夹紧块等宽数字) | ### 断言改为渲染级(旧的 CSS 数值断言抓不到) - ① `segmentLayoutOf` 计算实际间隙:`gaps = 4 / 4px`、`min-width = 49px`(注入 `gap:0` ⇒ 必败)。 - ② `basisBlocksOf` **只接受 3 块**,2 块或 4 块直接抛错;每块 `\n` 压成空格(合成一段 ⇒ 必败)。 - ③ `segmentPaintOf(true)` 与 `(false)` 背景必须不同(同色 ⇒ 必败)。 CSS 对拍仍 **68 项 / 0 不一致**。 ## v0.152.0 — 棱镜第二批:评估口径、回撤闸门、行业上限;CSS 对拍 68 项全等 报告 `docs/AUDIT-PRISM-BATCH2.md`;对拍表 `docs/CSS-PARITY-PRISM.md`;设计稿 `docs/mockups/prism-v3.html`。`npm run check` **1257 / 1257 × 2 时区**(1250 → 1257)+ selftest + build(73 片段全过)。 ### P-03 / P-02 评估口径 - `src/host/prismEval.ts`:`EVAL_MIN_SAMPLE=30`、`EMBARGO_DAYS=1`、脚注 `非重叠样本 · 不含 embargo 区间`;`nonOverlappingWindowsOf` 起点间隔 = 持有期,**`n` 为独立窗口数**。 - 指标:超额(**含费 / 不含费双值**)、自高点回撤、命中率;**`n<30` 一律 `null` + 原因**(`—(样本不足 n=X < 30)`),**不写 0**。 - 基准用 P-01 的 `benchmarkClose`(1.000300);费用按日汇总既有 `fills`。 - **UI(C)**:右栏净值卡之后、版本信息之前;三行 + 每行 `n` + **迷你超额曲线** + 固定脚注。 ### P-20 回撤闸门(R12:只提示 + 拒新买单) - `drawdownGateOf` 只用**资本调整后**序列:单日 ≤ −3% ⇒ 目标总仓 ≤60%;自高点 ≤ −10% ⇒ ≤40% 且现金 ≥25%;**连续两轮触发 ⇒ 冻结新开仓一轮**。 - `applyDrawdownGate` **只过滤买单**,卖单原样保留;**不新建委托、不自动卖出**。 - `PrismRun.drawdown` 落盘;**决策卡新增「回撤闸门」行(B)**:`单日 −3% / 累计 −10% / 状态`,含当前值、目标总仓、现金下限、样本 `n`。 ### P-11 行业上限(UI A) - `settings.industryLimits` 持久化;`POST {action:'industryLimits'}` 校验 0–100 后保存。 - `runPrismForSlot` 把 settings 行业上限并入 `limits.byIndustry`,沿用 P-08 链 **单票 → 行业 → 总仓 → 现金**(整手向下、夹掉不重分配、行业缺失不夹、卖单不裁)。 - **行业卡「上限」列改为可编辑 input**(Enter / 失焦保存);`未穿透`、`现金` 无 input。 ### CSS 对拍(本次专项) - `prismDesignParity.ts` 扩到 **68 项**,覆盖颜色(含 tonal / 悬停 / 按下 / 涨跌)、圆角与边框、卡片与浮层阴影、深色阴影、四格 / 标题 / 表格 / 决策 / 图例 / 评估字号、padding 与 gap、分隔条、分段控件、按钮、输入框、堆叠条、图表线宽与标签、行业上限单元格。 - **改前 40 项不一致 / 66 项 ⇒ 改后 0 项不一致 / 68 项**。 - 两处受仓库全局 12px 下限限制无法取设计原值(净值坐标 11px、图例与评估 `n` 11.5px ⇒ 实现 12px),已在 `docs/CSS-PARITY-PRISM.md` 注明。 ### 验证 `npm run check` **1257 / 1257 × 2 时区** + selftest + build(73 项);正常 + UTC 两轮干净复跑。 新增用例:100 个重叠事件 / 持有 5 日 ⇒ **`n=19`**(用事件数 ⇒ 败)|`n<30` ⇒ `null` + 原因(返回 0 ⇒ 败)|超额双值(单值 ⇒ 败)|回撤只用调整后序列(用未调整 ⇒ 败)|触发只拒买不拒卖(拒卖 ⇒ 败)|连续两次冻结(未冻结 ⇒ 败)|半导体 18% ⇒ 2000 股夹到 1800 股|缺行业不夹|全文无「预测能力」|评估卡顺序 / 三行 / `n` / 曲线 / 脚注|行业上限 input 与固定行不可写|CSS 68 项全等 + 7 类注入必败(圆角 0、分段背景删、表格 13px、分隔条 0、阴影 none、深色阴影放大、图例 11px)。 ### 未做 - P-03 不做历史信号重放;P-20 不自动卖出、不按目标仓位生成卖单;P-11 不做 ETF 成分穿透。 - 深色无独立设计 token(按既有 hybrid 深色值对拍)。 - **未在活宿主重启后做浏览器截图验收**。 ## v0.151.0 — 棱镜:版本信息行加字段名,去掉重复文案 报告 `docs/AUDIT-PRISM-BATCH1.md`。`npm run check` **1250 / 1250 × 2 时区**(1249 → 1250)+ selftest + build(73 片段全过)。 ### 修复 新增 `versionSummaryOf(codeVersion, mandateHash)`(`src/client/prismView.ts`): - 两字段缺值:`版本 —(旧记录无版本绑定) · 指纹 —(旧记录无版本绑定)`(此前印成 `—(…) · —(…)`,无字段名且重复)。 - 有值:`版本 v0.151.0 · 指纹 ab12cd34ef56…`;展开详情同样带字段名,不再显示裸 hash。 ### 验证 `npm run check` **1250 / 1250 × 2 时区** + selftest + build(73 项);正常 + UTC 两轮干净复跑。 新增 1 条断言:两字段均缺时该文案出现 **=2** 次且分别匹配 `版本 —(…)` 与 `· 指纹 —(…)`(无字段名重复 ⇒ 败);有值时匹配 `版本 v…`(丢字段名 ⇒ 败)。 ## v0.150.0 — 棱镜首批补修:行业卡常驻、净值点同源、版本与达成度文案 报告 `docs/AUDIT-PRISM-BATCH1.md`。`npm run check` **1249 / 1249 × 2 时区**(1246 → 1249)+ selftest + build(73 片段全过)。 ### 1) 行业暴露卡整卡消失 - 根因:`trading.exposure` 为 `null` 时前端**不渲染整卡**,违反「缺数据必须显示 `—` + 原因」。 - 修复:后端 `exposure` **始终存在**;不可用时 `{state:'unavailable', reason, rows:[]}`。前端**永远渲染**行业卡,不可用时表体显示 `—(原因)`;`normalizeTradingState` 对旧包也补 `unavailable`。 - **当前 live 根因**:宿主 breaker 对 `push2*.eastmoney.com` 报 `fetch failed` ⇒ `fetchIndustryOverview` 取不到 `f127` ⇒ 非宽基持仓全部落到「未穿透」。此前空 map 被当成正常数据。 ### 2) 净值点数两处不一致 - `GET` 的 `trading.equity`、`state.equity`、页面 `equityPoints` 统一来自 `PrismTradingStore.equity()`;旧包 `trading.equity` 为空而 `state.equity` 有数据时回退到非空数组。 ### 3) 版本信息空值 - `codeVersion` / `mandateHash` 缺值固定显示 `—(旧记录无版本绑定)`,不再出现 `«——»` 或空串。 ### 4) 达成度 `—` 补原因 - `exposure` 不可用 ⇒ `达成度 —(缺行业数据)`;可用但无行业上限 ⇒ `达成度 —(未配置行业上限)`。 ### 验证 `npm run check` **1249 / 1249 × 2 时区** + selftest + build(73 项);正常 + UTC 两轮干净复跑。 新增/覆盖 5 条断言:`exposure` 非 null 且不可用给 `unavailable`|前端在 `unavailable` 时仍渲染卡(不渲染 ⇒ 败)|`trading`/`state` 点数一致(页面 `equityPoints=[]` ⇒ 败)|版本缺值固定文案(空串/横线 ⇒ 败)|达成度 `—` 必带原因(只写 `—` ⇒ 败)。 ### 注意 升级后行业卡会**始终出现**;在东财 `f127` 恢复前,卡内大部分市值会显示在「未穿透」,这是如实反映,不是新 bug。 ## v0.149.0 — 棱镜首批五项:净值口径、版本绑定、量能撮合、仓位裁剪、行业暴露 报告 `docs/AUDIT-PRISM-BATCH1.md`;设计定稿 `docs/mockups/prism-v2-final.html`。`npm run check` **1246 / 1246 × 2 时区**(1234 → 1246)+ selftest + build(73 片段全过)。 ### P-01 权益口径与现金流 - 净值点新增 `capital` / `benchmarkClose` / `warnings`;`benchmarkClose` 走既有 `em.fetchKline('1.000300',101,1)`,取不到写 `null`。 - **资本调整后收益** = `(equity_t − capital_t) / (equity_0 − capital_0) − 1`;缺 `capital` 的点给 `null` + 原因,**不用当前 capital 回填**(否则 10-22 收益 −14.55% 会变 −35.91%)。 - `dayStartEquity` 与前一交易日收盘差 **> 1 元 ⇒ 告警**(`warnings` + `console.warn`);当日盈亏改为相邻净值点 `(equity − capital)` 差分。 - `unrealized` 数值不变,UI 标注「相对本金总盈亏」。 - **UI**:净值卡移到右栏;卡头「名义 / 调整后」开关,**默认调整后**。 ### P-04 版本绑定 - `PrismRun.codeVersion` 来自构建注入 `__TW_VERSION__`(测试回落 `dev`,不手写)。 - `mandateHash` = 骨架 + settings 参数(**剔除 `lastFired`**)的**全 sha256**,复用 `backup.ts` 的 `canonical()`。 - **UI**:版本信息在净值卡之后,默认折叠 `
`。 ### P-05 量能约束 - `MinuteBar.volume`:新浪补 `r['volume']`、腾讯 m1 补 `r[5]`(注释标「股」口径);**缺失为 `null`,不是 0**。 - 参与率进 settings,默认 **10%**;成交 ≤ `volume × rate`;不足则**部分成交**,剩余仍 `pending`。 - 独立原因码:`vol-missing` / `vol-zero` / `trend-no-1min` / `transient-source`,全部进 `rejects` 与挂单「原因」列。 - **修既有 bug**:`source === null` 不再置 `rejected`,保留 `pending` 下个 tick 重试。 ### P-08 仓位上限真正执行 - 纯函数 `clampDecision`:固定链 **单票 → 行业 → 总仓 → 现金**;分母统一「现金 + 持仓市值」;整手向下;`<100` 不成交并留痕;**卖单不裁**;缺价格/行业/上限不夹;**夹掉不重分配**。 - `ClampEvent{at,secid,dimension,before,after,basis}` 写入 decision,**裁剪早于 `placeDecision`**;只裁已有订单、不新建委托(R12);允许撤销未成交的买卖单。 - **UI**:不单独占卡,写进决策卡「仓位上限依据」三行(`资金上限 · 单票 · 总仓` / `达成度` / `本次夹紧 前 → 后`,无则 `—`)。 ### P-10 行业暴露 - `industryExposureOf`:`市值 ÷ (现金 + 持仓市值)`;行业取 `fetchIndustryOverview(f127)`;目录内宽基去重为「宽基 ETF」;族级用 `RESCUE_COVERAGE_FAMILY_OF_INDEX`;**未穿透与现金独立成行**。 - 暴露写进决策输入,GET 注入 `trading.exposure`。 - **UI**:构成条之后新增行业卡 —— 堆叠条 + 图例 + 紧凑表(前 6 行 + `其余 N 行`);含 `未穿透`、`现金`、上限/状态;卡内写 `已归类 X% + 未归类 Y% = 100.00%`。 ### 验证 `npm run check` **1246 / 1246 × 2 时区** + selftest + build(73 项);两轮(正常 + UTC)干净复跑。 新增 **12 条**用例(`prismBatch1.test.ts`、`prismBatch1Ui.test.ts`):缺 capital ⇒ `—`|`benchmarkClose` 可空落盘|`dayStartEquity` 差 >1 告警|仅 `lastFired` 不同 ⇒ hash 相同、改 capital ⇒ 变化、64 位全摘要|`volume=null/0/trend/transient` 四码不同且都保持 `pending`|100/200 部分成交且余量 `pending`|40.92% 夹到 25%(先整手)|两个买单总仓不重分配|缺行业不夹|卖单不动|权重合计 100.00、宽基只一行、未穿透不并入其它|UI 默认调整后、行业卡 100.00%、移动端顺序。 ### 未做 - 后续批次(P-02/P-03/P-06/P-07/P-09/P-11 及以后)未动。 - P-05 未做真实逐笔回放(量能口径沿用既有「股」标定 + mock)。 - P-08 不自动卖出已有超限仓位(R12);行业裁剪仅在 `limits.byIndustry` 或 settings 行业上限存在时生效。 - P-10 不做 ETF 成分穿透。 - **未在活宿主重启后做浏览器截图验收**。 ## v0.148.0 — 棱镜:复制持仓自动提额(资金不足时补到刚好够) 报告 `docs/AUDIT-PRISM-COPY.md`。`npm run check` **1234 / 1234 × 2 时区**(1230 → 1234)+ selftest + build(73 片段全过)。 ### 根因(用户报「总市值与持仓股数对不上」) 8 只持仓按成本合计需 **1 907 130 元**,当时上限 **1 750 000 元** ⇒ 复制逻辑按行**部分复制**,长鑫科技从 5 000 股变成 **1 314 股**,界面无提示。 总资产本身自洽(现金 41.04 + 市值 1 555 039.90 = 1 555 080.94)。 ### 修复(方案:自动提额) `action:'copy'` 先按摊薄成本算 `required = Σ(round2(成本) × 数量)`(不含费用),向上取整到分: - `capitalAfter = max(capitalBefore, required)`;`raised = capitalAfter > capitalBefore`。 - 用提升后的上限执行 `copyAtCost`,**写** `account.capital` / `capitalLimit` 与 `settings.capital`。 - 复制后 `cash = capitalAfter − required`。 - 回包新增 `copy:{ requestedPositions, copiedPositions, required, capitalBefore, capitalAfter, raised }`,**UI 不显示该摘要**。 - 真实持仓为空 ⇒ `required=0`、`raised=false`、上限不变。连续复制不累加。 ### 真实数据读数(隔离目录,use live positions/ledger) ```json { "copy": { "requestedPositions": 8, "copiedPositions": 8, "required": 1907130, "capitalBefore": 1750000, "capitalAfter": 1907130, "raised": true }, "capital": 1907130, "cash": 0, "positions": 8 } ``` 8 只数量逐只相等:`600000 / 5000 / 15000 / 4000 / 65000 / 85000 / 2000 / 5000`;**长鑫科技 5 000 股**。 与我先前估的 `1 908 105` 差 **975 元**:当前 `dilutedCost` 取整口径不同(长鑫 42.6437→42.64 等),实现**不硬编码**该值。 ### 验证 `npm run check` **1234 / 1234 × 2 时区** + selftest + build(73 项);两轮干净复跑。 新增 `prismCopy.test.ts` **4 条**:① 上限不足 ⇒ `capital === required`、8 只全等、长鑫 5000(保留部分复制 ⇒ 败)|② `cash === capital − required`(负现金 ⇒ 败)|③ `raised` 与 `capitalBefore/After` 正确(不记录 ⇒ 败)|④ 上限 2 000 000 ⇒ `raised=false`、cash 92 020(无条件上调 ⇒ 败)|⑤ 连续两次 copy 不累加|⑥ 持仓空 ⇒ required 0 且不改上限。 ### 保留 GET 每次 `reloadOrders()`;`prism-orders.json` 为 orders/fills 唯一来源;`account.capital === settings.capital`;14:59:09 可下单、15:00:00 判盘后(用 `run.startedAt`)。 ### 生效条件 需**重启 `dsh web`**;重启后点一次「按成本价复制持仓」,上限会自动补到所需并完整复制。 ## v0.147.0 — 棱镜:orders/fills 每次重读、capital 对齐、恢复 4 笔成交、盘后边界 报告 `docs/AUDIT-PRISM-LEDGER.md`。`npm run check` **1230 / 1230 × 2 时区**(1227 → 1230)+ selftest + build(73 片段全过)。 ### A. orders / fills 读不出来 **根因**:`PrismTradingStore.init()` 只在启动时读一次 `prism-orders.json`;进程启动后文件才写入 4 笔,内存仍是 0。 **修复**:新增 `reloadOrders()`,**每次 GET 重读**;`prism-orders.json` 的 orders/fills **优先**,账户内嵌仅兜底。 断言:磁盘 2 笔胜过内嵌 1 笔|外部写入第 3 笔后 `reloadOrders()` 可见|GET 源码含 `reloadOrders`(只读内嵌 ⇒ 败)。 ### B. `account.capital` 为空 **根因**:账户历史字段只有 `capitalLimit`,没有对外 `capital`。 **修复**:`PrismAccount.capital` 作为 `settings.capital` 同值别名;`normalizeAccount` 兼容 `capital` / `capitalLimit`;GET 用 settings.capital 构造返回账户(不写盘);保存时同时写两者。 断言:`settings.capital === trading.account.capital`(留 null ⇒ 败)。 ### 恢复今天 4 笔成交(界面不标「重建」) | 代码 | 方向 | 数量 | 价格 | 成交额 | 费用 | | --- | --- | --- | --- | --- | --- | | `1.588170` | sell | 50 000 | 0.698 | 34 900.00 | 43.98 | | `1.560010` | sell | 20 000 | 2.657 | 53 140.00 | 66.96 | | `1.688035` | sell | 500 | 52.33 | 26 165.00 | 32.97 | | `1.518880` | sell | 1 000 | 8.187 | 8 187.00 | 13.27 | - 卖出净额:`34 900 + 53 140 + 26 165 + 8 187 − 157.18 = **122 234.82**`。 - 目标现金:`1 600 000 + 122 234.82 = **1 722 234.82**`(已写入 live `prism-account.json`,`capital` 与 `capitalLimit` 为 1 600 000)。 - 与订正前 `100 000.16` 的差额 **1 622 234.66** 来自历史买入/其他流水,**未硬凑**;相对 1 600 000 起点,现金变化**恰好**等于 4 笔卖出净额。 - 回包与 UI **不出现**「重建 / recovered / rebuilt」;`fills[].source` 均为 `1min`;追溯信息只写在文档。 ### 盘后边界(14:59 vs 15:00) 原判定用 `Date.now()` ⇒ 14:59:09 触发、分析跨过 15:00 就被误判盘后。 现改为传 **`run.startedAt`(研判触发时刻)**:**14:59:09 可下单,15:00:00 才判盘后**。测试覆盖两边界。 ### 验证 `npm run check` **1230 / 1230 × 2 时区** + selftest + build(73 项);两轮干净复跑。 新增 3 条用例(orders 唯一源、无「重建」标记、盘后边界)+ B 断言并入既有 cash 测试。 ### 生效条件 需**重启 `dsh web`** 装载本批 host 代码。 ## v0.146.0 — 棱镜:自动时点不落单、账本被清空(含恢复路径) 报告 `docs/AUDIT-PRISM-LEDGER.md`。`npm run check` **1227 / 1227 × 2 时区**(1223 → 1227)+ selftest + build(73 片段全过)。 ### Bug 1:自动时点研判的挂单从不落库 **根因**:`runPrismForSlot` 只写 `prism-runs.json`;`placeDecision` 只在**手动** `POST action:'run'` 的**路由层**调用。调度器直接调 `runPrismForSlot` ⇒ 自动时点的 4 / 1 / 2 / 1 笔挂单**从未进入账本**。 **修复**:把 `quoteLiteOf + placeDecision + store.replaceRun` 移入 `runPrismForSlot`(手动路由不再重复 place)。 每个 run 现在记录 `placement: { placed, skipped, rejects:[{secid,reason}], skippedReasons }`;重复委托不再静默 `continue`,计入 `skipped` 并写原因。 ### Bug 2:账本被清空 / 读取回落 **根因**:① `init()` 把 `{v:1, account}` 整包传给 `normalizeAccount`,未解包 ⇒ 重启后回落默认;② `ensureAccount` 在 GET/run/capital 前自动 `copyAtCost`,覆盖现金。 **修复**: - 读取兼容**三种**历史格式(裸 account / `{v,account}` / 含 `fills` 的包裹);fills 优先 `prism-orders.json`。 - GET / run / capital **不再自动复制**;只有显式 `action:'copy'`。 - **`accountWriteGuard`**:旧账户有 positions/fills、新账户 `positions=0` 且 `fills=0`、且非 `explicitReset` ⇒ **拒绝写盘并告警**。 - 新增只读 `POST {action:'recover'}`:返回重建的 fills/cash,**不写盘**。 ### 今天的数据:能恢复多少 当前磁盘 `prism-orders.json` 保留 **4 笔 filled 单 + 4 笔 fills**,`rebuildFromOrders` 可重建: `1.588170 sell 50000@0.698`、`1.560010 sell 20000@2.657`、`1.688035 sell 500@52.33`、`1.518880 sell 1000@8.187`。 - **10:12 的 4 笔成交可以恢复 fills**(订单文件保留成交价/费/时间)。 - 现金只能给参考值 **1 722 234.82**,**不能精确复原**(缺历史入金与未知买入)⇒ 以当前账户为准。 - **11:15 / 13:10 / 14:35 的 1 / 2 / 1 笔从未落单,没有成交价与时刻 ⇒ 不能伪造历史成交**,只能在新代码下重跑并重新挂单。 ### 验证 `npm run check` **1227 / 1227 × 2 时区** + selftest + build(73 项);两轮干净复跑。 新增 4 条用例:① 自动 `runPrismForSlot` ⇒ `run.placement` 存在且 `trading.orders` 增加(只写 runs ⇒ 败)|② 重复委托 ⇒ `skipped=1` 且带「重复」原因|③ 三种格式读取后 positions/fills 非空(读空 ⇒ 败)|④ 旧有数据写空账户 ⇒ `accountWriteGuard` 拒绝且磁盘保留旧持仓,`explicitReset` 才允许|⑤ `rebuildFromOrders` 2 笔买入 ⇒ fills=2、cash=699.9、note 含「重建」(重建为空 ⇒ 败)。 ### 生效条件 需**重启 `dsh web`** 装载新 host 代码;重启后 `POST {action:'recover'}` 可查看可重建的成交。 ## v0.145.0 — 棱镜:修复资金上限无法保存、现金余额反复横跳 报告 `docs/AUDIT-PRISM-CASH.md`。`npm run check` **1223 / 1223 × 2 时区**(1219 → 1223)+ selftest + build(73 片段全过)。 ### Bug 1:资金上限无法保存(三个根因) 1. **只写账户未写 settings**:`prism-settings.json` 没有 `capital` 字段,`PrismSettings` 也没定义。 2. **重启回落**:`PrismTradingStore.init()` 把 `{ v: 1, account }` 整包传给 `normalizeAccount`,**没解包 `account`** ⇒ 重启后 capital/cash 回落到 1500000、持仓回空。 3. 修复:新增 `PrismSettings.capital` 与 `PrismStore.saveCapital`;保存时**同时写 account + settings**并回包;初始化**兼容包裹格式与旧裸对象**。 ### Bug 2:现金余额反复横跳(两个根因) 1. **GET 自动复制**:`ensureAccount` 在每次 GET / run / capital 前调用,首次会 `copyAtCost` 覆盖现金。 2. **账户解包错误**(同上)⇒ 重启后读成空账户,随后又被自动复制 ⇒ 现金在两个值间来回。 修复:GET / run / capital **全部移除 `ensureAccount`**;只有 `action:'copy'` 才复制;`recordEquity` 只写 equity、不写 account;`state.account` 与 `trading.account` 共用同一个 `tradingWithDecision`。 ### curl 序列实测(隔离 route server,夹具 1 只 100 股 @1000) | 步骤 | capital | cash | copiedAt | | --- | --- | --- | --- | | POST copy | 1 500 000 | 1 400 000 | …070615 | | GET 基线 | 1 500 000 | 1 400 000 | …070615 | | POST capital 2 000 000 | **2 000 000** | **1 900 000** | …070615 | | GET ×3(间隔 2 s) | **2 000 000** | **1 900 000**(三次相同) | …070615 | | POST copy | 2 000 000 | 1 900 000 | **…076866**(只变一次) | | GET ×3 | 2 000 000 | 1 900 000(三次相同) | …076866 | ### 验证 `npm run check` **1223 / 1223 × 2 时区** + selftest + build(73 项);两轮干净复跑。 新增 `prismCash.test.ts` **4 条**:① capital 落 settings、3 次 GET 恒新值、**重启后 settings/account 均保留**(GET 里写回常量 ⇒ 败)|② GET 不写 account(cash、copiedAt、`JSON.stringify(account)` 三次一致;`state.account` 与 `trading.account` 逐字节相同)(GET 里重算 ⇒ 败)|③ `copiedAt` 非空不重复制、入金只改 capital/cash 一次且 positions/cost/copiedAt 不变、同值再存不重复加钱(入金触发重复制 ⇒ 败)|④ **源码 GET 分支不得含 `saveAccount`/`ensureAccount`/`copyAtCost`**(注入 `saveAccount` ⇒ 败)。 ### 升级注意 **GET 不再自动复制持仓**。升级后若账户仍为空,请点一次页面「按成本价复制持仓」重建账户。活宿主当前基线为 `capital 1 500 000 / cash 1.12`,语义与测试序列一致。 ## v0.144.0 — 日志降噪:跨批抑制、熔断期只记状态转换、原因折叠、限长 报告 `docs/AUDIT-LOG-NOISE.md`。`npm run check` **1219 / 1219 × 2 时区**(1212 → 1219)+ selftest + build(73 片段全过)。 ### 根因 聚合只做「**同一批内**」合并(400 ms 窗口一过就原样再刷),桶外没有任何状态 ⇒ 熔断期间每个 tick 都刷一遍同一三元组。 ### 五项修复(`src/host/degrade.ts`) | # | 修复 | | --- | --- | | 1 | **跨批同一签名抑制** `admitSignature` + `sigs` 注册表:状态未变时只在 **1 / 10 / 100 …** 输出(N 次事件上界 `1+floor(log10 N)`);**原因签名变了立刻再出一条** | | 2 | **熔断期降噪**:只在 `!prevOpen → open`(开启)、`noteHostProbe`(half-open 探测)、`noteHostSuccess`(恢复)各出一条;其余 tick 只累加 `noise.suppressed`;`noteDegraded` 新增可选 `hosts` —— **涉及主机全部 open 时该 tick 不产出** | | 3 | **原因折叠**:`shortReasonOf()`(截断限 22 字)+ `reasonTally()` 计数 ⇒ `未完成 bar 已超过 60s TTL ×27`;完整 `detail` 只在 `TW_DEBUG=1` | | 4 | **行长上限** `LOG_LINE_MAX = 200` + `clipLogLine()`(截断标记计入 200) | | 5 | **计数可核对**:`期间抑制 N 条`(熔断期未进桶)+ `聚合抑制 N 条`(进桶被折叠);另导出 `logNoiseStats()` | ### 改前 / 改后(真实形态构造,实测) | 场景 | 改前 | 改后 | | --- | --- | --- | | 熔断 10 分钟 × 每 10 秒失败(60 tick × 3 组) | **180 条** | **5 条**(3 首现 + 熔断开启 1 + 恢复 1;恢复那条写 `期间抑制 681 条`) | | K 线降级 27 个文件(原因尾巴逐文件不同) | 1 行 / **316 字** | 1 行 / **184 字**(`… ×27`) | ### 验证 `npm run check` **1219 / 1219 × 2 时区** + selftest + build(73 项)。 新增 `logNoise.test.ts` **7 条**:60 tick ⇒ **≤5 条**(逐 tick 输出 180 条 ⇒ 败)|同签名 100 次 ⇒ **3 条**且末条含 `聚合抑制 97 条`、`emitted+suppressed===100`(不抑制 ⇒ 败)|熔断期 30 tick 全静默、状态事实 3 条、`期间抑制 ≥30`(逐 tick 记 ⇒ 败)|单条 ≤200 且末尾 `(其余见 TW_DEBUG=1)`、折叠 `×9`(保留全文 316 字 ⇒ 败)|`TW_DEBUG=1` 可见 ≤2 条但 debug **12 条**(debug 也吞掉 ⇒ 败)|恢复后计数归零、第二轮第 1 次立刻输出(不清零 ⇒ 被门槛吃掉 ⇒ 败)|原因折叠纯函数边界。 ### 影响面 `flushDegraded` 行格式(最短原因 / 两种抑制数 / 限长);`noteDegraded` 多可选 `hosts`(不传行为不变);**主机状态转换现在会产出日志**(此前完全不产出)。既有 `degrade.test.ts` ⑥/⑥b/⑧ 全绿。 ### 未做 `logNoiseStats()` 未挂 `/health`(契约冻结);抑制账不落盘(重启即干净启动,保留「重启后首次故障可见」);只给主机级三条日志补 `hosts`;未改结构化日志。 ## v0.143.0 — 棱镜:分段回顶、四格 28px、持仓表拆代码/名称 报告 `docs/AUDIT-PRISM-REDESIGN.md`。`npm run check` **1212 / 1212 × 2 时区**(1209 → 1212)+ selftest + build(73 片段全过);只改 `src/client/**`。 | # | 修前 | 修后 | | --- | --- | --- | | 1 | 分段控件在四格之下 | **分段回到左栏第一块**(四格之上);左栏 DOM:分段 → 四格 → 资金上限行 → 交易表(含构成条)→ 决策 → 净值;移动端 order 同步 `1/2/3/4/5/6` | | 2 | 四格数值约 34px | `.tw-prism-v` 改 `font: 600 28px/1.15`(标题仍 12px、间距不变) | | 3 | 「标的」一格塞代码与名称 | 表头拆为 **8 列**:`代码 | 名称 | 数量 | 摊薄成本 | 现价 | 市值 | 仓位占比 | 浮盈`;代码 `tw-prism-mono` 等宽,名称左对齐 + `ellipsis` + 最大宽 180px | ### 验证 `npm run check` **1212 / 1212 × 2 时区** + selftest + build(73 项);两轮干净复跑。 新增 3 条用例:① 左栏分段索引 < 四格索引、CSS order 1/2/3(放回四格之后 ⇒ 败)|② 解析 CSS 字号 == 28 且 ≥15 展示级(改回 34 ⇒ 败)|③ 表头严格 8 项、代码/名称相邻、数据行 `` 数与表头一致(合并名称列或删一格 ⇒ 败)。 ## v0.142.0 — 棱镜:决策卡移到左栏(净值曲线之前) 报告 `docs/AUDIT-PRISM-REDESIGN.md`。`npm run check` **1209 / 1209 × 2 时区**(1203 → 1209)+ selftest + build(73 片段全过);只改 `src/client/**`。 ### 布局调整 | 栏 | 顺序 | | --- | --- | | **左** | 四格资产 → 分段(持仓 / 挂单 / 成交)→ 当前分段内容 → **决策卡** → **净值曲线卡** | | **右** | 研判(汇总 + 四份分报告)→ 时点配置 → 模型配置 | 移动端 `<900px` 顺序同步:资产 1 → 工具 2 → 分段 3 → 交易表 4 → **决策 5** → **净值 6** → 研判。 两栏仍 `62fr / 6px / 38fr`,拖拽夹取 40–75,默认 62。 ### 验证 `npm run check` **1209 / 1209 × 2 时区** + selftest + build(73 项);两轮干净复跑。 新增 `prismDecisionMove.test.ts` **6 条**:① 决策卡在左栏、右栏不含(注入到右栏 ⇒ 败)|② 决策索引 < 净值索引且两卡间无其他卡片(交换顺序 ⇒ 败)|③ 右栏扫描无 `tw-prism-decision`(双渲染 ⇒ 败)|④ 七行齐全(删任一行 ⇒ 败)|⑤ CSS order `decision:5 < equity:6`(反转 ⇒ 败)|⑥ `PRISM_SPLIT_DEFAULT=62`、`clampSplit(20)=40`、`clampSplit(90)=75`、网格仍 `${split}fr 6px ${100-split}fr`。 原有 `prismRedesign` ④ 同步改为「分段在四格之后、交易内容之前」,移动端回归断言改为决策 5 / 净值 6。 ## v0.141.0 — 棱镜:强制双侧决策(买/卖)、单票上限荒谬值、已跑标签位置 报告 `docs/AUDIT-PRISM-ACCOUNT.md`。`npm run check` **1203 / 1203 × 2 时区**(1200 → 1203)+ selftest + build(73 片段全过)。 ### 1) 汇总 prompt 强制双侧思考(此前只评估买入) 现象:重跑 09:35 时,账户满仓(仓位 100%、现金 1.12 元)、A股与黄金同时破位,但 `orders=[]`、`cancel=[]`,决策理由只说「暂不新增交易」——**卖出/减仓侧完全没被评估**。 修复(硬要求,不是建议): - 必须先判断**是否需要降低风险暴露**(减仓 / 止损 / 清仓);不减必须写明「为什么满仓合理」。 - `orders` 明确允许 `side:'sell'`,字段 `{secid, side:'sell', price, shares, reason}`。 - `orders=[]` 时 `note` 必须分两段:「**不买的理由**」+「**不卖的理由**」,缺一不合格。 - `decisionProblems` 升级:`orders=[]` 且 `note` 不含卖/减仓/止损字样 ⇒ 报错。 ### 2) `单票上限 0.00009%` 这种荒谬值 可买额度 1.12 元 ÷ 总资产 1 246 531.82 会算出 `0.00009%`(数学正确但无意义)。 新增 `singleLimitText`:**可买额度 < 最低现价 × 100** ⇒ 显示 `—(可买额度不足一手)`;否则保留百分比。`仓位上限依据` 仍显示原始计算。 ### 3) 「已跑 09:35」标签 从独立行移入标题行右侧(`.tw-prism-notice{margin-left:auto}`),不再挤占分段控件行。 ### 验证 `npm run check` **1203 / 1203 × 2 时区** + selftest + build(73 项);两轮干净复跑。 新增 3 条用例:捕获汇总请求体含 `减仓 / 止损 / 清仓 / 不买 / 不卖`(删掉 ⇒ 败)|构造一笔 `sell` ⇒ 解析成功并随 run 落库|`orders=[]` 且 note 只讲买入侧 ⇒ 必败|低现金单票文案与充足现金百分比两分支|notice 在标题行内、分段行不含(回独立行 ⇒ 败)。 ### 备注 09:35 旧轮次本身无订单属正常;旧记录 `note` 缺双侧说明会显示 `—`,**下一轮起**按新提示词生成。 ## v0.140.0 — 棱镜:研判注入账户快照、决策带理由、账户字段统一 报告 `docs/AUDIT-PRISM-ACCOUNT.md`。`npm run check` **1200 / 1200 × 2 时区**(1194 → 1200)+ selftest + build(73 片段全过)。 ### 缺陷 1:账户数据没喂给模型 `decision.limits` 长期为 `null`,模型原话「材料未提供账户总资产、风险预算与仓位约束」。 修复:`PrismInputs.account` + `accountSnapshotOf`,4 路分析与汇总的请求体**全部**带上 7 项: `总资产` / `现金` / `持仓市值` / `当前仓位占比` / `可买额度` / `当日已成交笔数` / `未成交挂单`。 汇总 prompt 要求**依据快照计算** `singlePct/totalPct`;确实无法算才 null 并在 `limits.note` 说明。页面决策表新增「**仓位上限依据**」行。 ### 缺陷 2:决策没有「为什么不下单」 `PrismDecision.note` 新增(兼容旧落盘):`parseDecisionDetailed` 优先读顶层 `note`、兼容 `limits.note`、空串归一 null;汇总 JSON schema 增加 `"note":"本轮决策理由(一句话;orders 为空也必须写)"`;页面决策表新增「**决策理由**」行。 `decisionProblems` 对 `orders=[] && note===''` 返回错误。 ### 账户字段统一 `state.account` 与 `trading.account` 同一对象(同一 `tradingWithDecision` 展开),实测 `cash=1.12`、`positions=6`、`copiedAt` 均有值;客户端新增 `normalizeTradingState`(`trading.account.positions` 为空时落回 `state.account`)。 ### 验证 `npm run check` **1200 / 1200 × 2 时区** + selftest + build(73 项);两轮干净复跑。 本轮新增 7 条用例:7 项齐全且 5 个请求体全含(少一项 ⇒ 败)|正常 `20/50` 回包解析非 null|`orders=[]` + 空 note ⇒ 败|`state.account` 与 `trading.account` 同源|`trading.account` 空时客户端兜底(cash 1.12 / 1 持仓)|决策表新增两行且空值显示 `—`。 ## v0.139.0 — 竞价格式解耦东财;删除护盘「备用源」归因面板 报告 `docs/AUDIT-AUCTION-DECOUPLE.md`、`docs/AUDIT-RESCUE-PANEL.md`。`npm run check` **1194 / 1194 × 2 时区**(1181 → 1194)+ selftest + build(73 片段全过)。 ### 一、竞价不再依赖东财(新增独立出口) **症状**:竞价原嵌在 `/tradewatcher/detail` 里,而详情**只有东财一个上游** ⇒ 东财熔断时,本地 37 份采样**一条也读不到**(用户截图的「—」即此)。 **修复(方案 A + B)** 1. **新增 `GET /tradewatcher/auction?secid=`**:**永远 200**、`auction` 必有;不可用时 `state:'unavailable'` + 原因 + `missing[]`(沪深 `transient` / 非沪深 `no-source`)。**路由账 32 → 33(core 15 → 16)**,归 **core**(唯一消费方是详情抽屉)。 2. `/detail` 仍带顶层 `auction`(`detail.auction` 保留仅兼容)。 3. 数据源**只用本地采样 + 腾讯 m1**:昨收不再依赖东财 `f18`,改取**同包 `qt[4]`**(实测 `[…1317.87, 1318.49, 1311.84…]` 与分钟首根 `0930 1311.84 1049` 互证);**缺昨收只缺溢价那一项**,不再整行退化成 `—`。 4. **腾讯分钟链补同源三台回退**:实测 09:31 `web.ifzq` 又回 501、`ifzq` / `proxy` 200 ⇒ 否则四项会跟着分钟链一起死。 **实测(东财被 stub 掉并抛错,真实腾讯)** | secid | state | 点数 | 价 / 量 / 占比 / 溢价 | | --- | --- | --- | --- | | `1.600900` | ok | **60** | 29.70 / 25 790 手 / 14.42% / −0.40% | | `1.510300` | ok | **60** | 4.08 / 127 974 手 / 5.34% / −0.66% | | `1.600519` | ok | 0(本机未采样该只) | 1311.84 / 1049 手 / 20.69% / −0.50% | | `116.00700` | unavailable(仅沪深) | 0 | — | 请求主机:`web.ifzq×3`(501)+ `ifzq×3`(200),**东财 0 条**。 ### 二、删除护盘「备用源」归因面板 - 整块删除:容器 + `备用源` 标签 + **17 行**归因(`护盘信号|…|评分 …/100` … `阈值来源 …`)+ 其 CSS。 - **保留**:标题、等级胶囊、`0 族 · 6/6`、评分/当日峰/分位、`因子 n/4`、采样状态与操作按钮、六张通道卡、「共振」。 - 删后**无空盒、无占位、无额外间距**。 - `build.mjs` 锁定片段核实:本面板字面量命中 **0**;`备用源` 仅命中 2 条**不相关**既有文案(`腾讯备用源兜底`、`板块涨跌来自腾讯备用源`),未触发停手。 ### 验证 `npm run check` **1194 / 1194 × 2 时区** + selftest + build(73 项)。 本轮新增 **13 条**用例(解耦 9 + 面板删除 4),逐条注入必败: 解耦:东财抛错 + 腾讯首台 501 ⇒ `/auction` 200 且 `state=ok`、四项齐、序列非空(与 detail 一起置空 ⇒ 败)|`seriesDay === today`(写 null ⇒ 败)|源码 5 个禁词 0 命中且 `push2*` 请求计数 0(塞东财进请求列表 ⇒ 败)|客户端 `detail===null` 两分支都渲染竞价行与曲线(收进 `detail!==null` ⇒ 败)|路由账 33=core 16(用旧账目 ⇒ 败)|`/auction` 归 core(错登记进 market ⇒ 败)|`/detail` 东财挂时顶层 `auction.state=ok`(去掉顶层 ⇒ 败)。 面板删除:源码不含 `备用源` / `评分 26/100` / `已剔除…` / `阈值来源` / `tw-rescue-summary` / `snapshot.summary`(加回任一行 ⇒ 败)|CSS 无 `.tw-rescue-summary` 且不留空 `
`(只清文字 ⇒ 败)|标题/操作/通道卡/共振仍在(连带删 ⇒ 败)|`.tw-rescue` 仍 `gap:6px` 且无 `min-height`(撑高 ⇒ 败)。 ## v0.138.0 — 棱镜收尾:净值卡语义拆分、时点去 emoji、四格留白 报告 `docs/AUDIT-PRISM-REDESIGN.md`。`npm run check` **1181 / 1181 × 2 时区**(1175 → 1181)+ selftest + build(73 片段全过);只改 `src/client/**`。 | # | 修前 | 修后 | | --- | --- | --- | | 1 | 净值卡混两种语义(净值折线 + 持仓构成堆叠条,图例只列 3 个持仓) | 净值卡**只留折线 + `总资产` 单项图例**;持仓构成条移到**持仓表下方**(权重 = 市值 ÷ 总资产,现金为条底留白),图例 **`min(持仓数, 6)`**(>6 列前 5 + `其他`) | | 2 | 时点配置用 emoji(`⏱` `✕`) | 改**文字按钮**(`重跑` / `删除`);源码不含 `⏱ / ⏰ / ⏳ / ✕` | | 3 | 四格右侧大片留白 | 宽屏 `repeat(4, minmax(0,1fr))`、`<1100px` 2×2、`<640px` 单列;数值 26px 左对齐,次要说明 `margin-left:auto` 贴右 | ### limits 核实 11:20 轮 `decision.limits = {singlePct:null, totalPct:null, note:"材料未提供组合总资产、现金与风险预算,无法计算仓位上限"}`、`parseError: null` ⇒ **模型本轮没给 limits,页面 `—` 正确**;14:40 轮 `singlePct=20` 说明解析链路正常。**不是解析丢失。** ### 验证 `npm run check` **1181 / 1181 × 2 时区** + selftest + build(73 项);BO/BP 两轮干净复跑。 新增 6 条用例:① `equityLegendItems()` 只返回 `['总资产']`(注入第二个持仓名 ⇒ 败)|② `compositionItems` 3→3 / 6→6 / 7→6(写死 4 ⇒ 败)|③ 构成条在持仓卡内、净值卡内不含 `tw-prism-composition`(合并回一卡 ⇒ 败)|④ 时点源码无 emoji 码位(恢复 ⇒ 败)|⑤ `.tw-prism-sub` 含 `margin-left:auto`(删掉 ⇒ 败)|⑥ limits 为 null 时返回 `—` 且不产生 `parseError`。 ## v0.137.0 — 棱镜视觉收口:剥 Markdown、四格一行四列、阴影按最终生效值、分段加深、决策表并入 rejects 报告 `docs/AUDIT-PRISM-REDESIGN.md`。`npm run check` **1175 / 1175 × 2 时区**(1169 → 1175)+ selftest + build(73 片段全过);只改 `src/client/**`。 | # | 修前 | 修后 | | --- | --- | --- | | 1 | 汇总直接显示 `**总体偏弱、防御占优**` | `stripMarkdown` 剥离 `**` / `*` / 反引号;`summaryBullets`、`analysisText`、`decisionErrorText` 全走它;页面无连续两个星号 | | 2 | 四格 2×2 且极宽留白 | 宽屏 **`repeat(4, minmax(0,1fr))`**;`<1100px` 两列;`<640px` 单列;数值 26px | | 3 | 卡片阴影看不见(被后续同选择器覆盖) | 最终规则直接写设计稿值;新增 `finalCardShadow` 取**同选择器最后一条**并解析 `var`,后写覆盖会被抓到 | | 4 | 分段选中态太淡 | `rgba(94,106,210,.18)` + `#2f3aa0` + **700**;容器 1px 边框 + 2px 内边距;深色 `.22` + `#dde0ff` | | 5 | `rejects: 无` 纯文本 | 并入**决策表格行**;`orders`/`cancel` 为 0 时 `.zero` 次要色;整表 `tabular-nums` | ### 时点核实 `PRISM_DEFAULT_TIMES = ['09:35','11:20','13:10','14:40']`(测试断言含 `14:40`);活文件 `prism-settings.json` 为 `14:35` 且 `lastFired` 记录过 `14:40` ⇒ **属用户手动编辑,未改**。 ### 验证 `npm run check` **1175 / 1175 × 2 时区** + selftest + build(73 项);BM/BN 两轮干净复跑。 新增 6 条用例:① 页面无 `**`(恢复星号 ⇒ 败)|② 宽屏 4 列 + 两档媒体查询(写回 2 列 ⇒ 败)|③ `finalCardShadow` 等于设计稿值(追加 `.tw-prism-card{box-shadow:none}` ⇒ 败)|④ 选中背景/文字/700(删背景 ⇒ 败)|⑤ `decisionRows` 含 `rejects` 行且 zero 次要色(改回纯文本 ⇒ 败)|⑥ 默认常量含 `14:40`。 ### 验收通过的功能读数(活宿主,curl) 自检 `ok:true / OK / 1376ms`;账户 `现金 1.12 / 6 个持仓 / copiedAt 有值`;金十 `ready / 10 页 / 471 条`;研判 2 次(`day`、`at`、4 份报告、`parseError=false`);挂单与成交 0;净值 1 点。 ## v0.136.0 — 棱镜:修复整页不能滚动、页头自检误报失败 报告 `docs/AUDIT-PRISM-REDESIGN.md`。`npm run check` **1169 / 1169 × 2 时区**(1163 → 1169)+ selftest + build(73 片段全过);只改 `src/client/**`。 ### 1) 底部内容被裁、无法下滑 **根因**:`.tw-prism` 是 flex 子项,但没有 `flex:1 / min-height:0 / overflow-y:auto`;父级是固定高度 ⇒ 整块内容被父容器裁掉,页面自身没有滚动容器。 **修复**: - `.tw-prism{flex:1 1 auto; min-height:0; overflow-y:auto; overflow-x:auto}`;**无** `overflow:hidden`、**无**固定 `height`。 - `.tw-prism-body{min-height:0}`;右栏**不独立滚动**(整页统一滚动)。 - `<900px` 折叠容器同样 `min-height:0`,不写固定高度。 ### 2) 页头自检误报「自检失败 · HTTP 200」 **根因**:页面走通用 `requestWithTimeout`;HTTP 200 但 body 非 JSON 时它抛 `响应解析失败 (HTTP 200)`,页面把这句话当成失败原因。 **修复**:新增专用 `runSelftest` + `parseSelftestBody(status, raw)`: - 200 + 可解析 ⇒ 按 `body.ok` 给「自检通过 · {elapsedMs}ms · {text}」或「自检失败 · {error}」; - 200 + 非 JSON ⇒ 「自检失败 · 响应非 JSON(前 60 字:…)」; - 网络/超时 ⇒ 「自检失败 · 超时/网络错误…」; - 页面源码**不再出现** `HTTP ${status}` 作为失败文案。 ### 验证 `npm run check` **1169 / 1169 × 2 时区** + selftest + build(73 项);BK/BL 两轮干净复跑。 新增 6 条用例:滚动 3 条(根 `overflow-y:auto` 且拒绝 `hidden`/固定高度;body `min-height:0`;移动端无固定高度)|自检 3 条(`{ok:true,text:'OK',elapsedMs:12}` ⇒ 含「通过」「12ms」「OK」;非 JSON ⇒ 含「响应非 JSON」且带前 60 字;源码扫描 `HTTP ${status}` 且注入恢复必败)。 ### 待验证 重启后确认「能滚到底」与「自检显示通过」。 ## v0.135.0 — 棱镜视觉补齐并与设计稿数值对拍(分段控件 / 卡片层级 / 右栏卡片化) 报告 `docs/AUDIT-PRISM-REDESIGN.md`。`npm run check` **1163 / 1163 × 2 时区**(1156 → 1163)+ selftest + build(73 片段全过);只改 `src/client/**`。 ### 修了七项视觉差异 | # | 修前 | 修后 | | --- | --- | --- | | 1 | 分段控件是三段纯文本 | **MD SegmentedButton**:容器 `padding:2px` + `gap:4px` + `border-radius:8px` + 灰 tonal 底;按钮高 28px(总高 32px);选中 `rgba(94,106,210,.14)` + 字重 600 + 文字 `#3a44a8` | | 2 | 卡片扁平、无层级 | `.tw-prism-card` 白底 + **圆角 10px** + 1px 边框 + `0 1px 2px rgba(0,0,0,.06), 0 1px 3px rgba(0,0,0,.04)`;页面底 `#f5f6f8`;四格改 **2×2**(避免 28px 数值被挤压) | | 3 | 卡内偏挤 | padding **12px**;标题 12px 次要色;数值 **28px** 半粗;标题与数值间距 4px | | 4 | 数字不齐 | 表头/单元格 **`font-variant-numeric: tabular-nums`**;表头 12px、行高 26px、1px 分隔 | | 5 | 右栏 439 条文本 + **原始 JSON** 直出 | 顶部一行 `研判 · 14:40 · 日期 · token 用量`;**汇总**最多 5 条(line-clamp 2,剥掉 ```json 块);**决策小表**(单票上限 / 总仓位上限 / orders / cancel)+ rejects 单行;**四份分报告**可折叠(左侧 3px 系列色条);**时点配置**与**模型配置**各自成卡 | | 6 | 分隔条看不见 | 6px 轨道 + 2px 竖线(上下各留 16px),hover/拖拽变强调色、`cursor:col-resize`、键盘 ±2 | | 7 | 净值卡样式不统一 | 空态文案保留;与其它卡同圆角、同 elevation-1、同 padding | ### 验收方式改为「与设计稿数值对拍」 新增 `prismDesignParity.ts`:从 `docs/mockups/md-hybrid.html` 抽基准值,与实现 CSS **逐项比对**;不一致即失败。 | 项 | 设计稿 | 实现 | | --- | --- | --- | | 页面底色 | `#f5f6f8` | `#f5f6f8` | | 卡片圆角 | `10px` | `10px` | | 卡片阴影 | `0 1px 2px rgba(16,24,40,.05)` | 同 | | 卡片内边距 | `12px` | `12px` | | 分段圆角 / 内边距 | `8px` / `2px` | `8px` / `2px` | | 分段选中背景 | `rgba(94,106,210,.14)` | 同 | | 表格行高 / tabular | `26px` / `tabular-nums` | 同 | | 分隔条宽度 | `6px` | `6px` | ### 验证 `npm run check` **1163 / 1163 × 2 时区** + selftest + build(73 项);BI/BJ 两轮干净复跑。 新增 7 条用例(对拍 6 + 汇总 JSON 剥离 1);注入必败:卡片圆角改 0|删分段选中背景|分隔条宽度 0|去掉 `tabular-nums`|页面恢复 `{"limits"` 原始 JSON。 原有 10 条重设计断言保留(拖拽夹取、`<900` 折叠、左栏分段、8 系列色、红涨绿跌、ripple、elevation、深色、功能项)。 ### 未做 重启后的真实截图复核(需刷新/重启);当前为数值对拍 + 构建 + 静态断言。 ## v0.134.0 — 棱镜页 UI/UX 重设计(hybrid + MD 结构,功能零回退) 报告 `docs/AUDIT-PRISM-REDESIGN.md`;设计稿 `docs/mockups/{md-hybrid,md-tonal,variants-q9,q12,q13}.html`。`npm run check` **1156 / 1156 × 2 时区**(1146 → 1156)+ selftest + build(73 片段全过);只改 `src/client/**`。 ### 用户选型 | 项 | 选择 | | --- | --- | | 配色 | **hybrid** —— 底色/表面/边框/文字**完全沿用现有 token**,叠加 MD 强调色与 tonal 填充 | | 分段控件 | **左栏顶部**(`持仓 / 挂单 / 成交`),右栏无分段 | | 两栏划分 | 左 = 四格资产 + 分段 + 净值曲线;右 = 研判 + 时点配置 + 模型配置 | | 图表 | **MD dataviz 8 系列色**(表格涨跌仍红涨绿跌) | ### 实现 - **两栏 62 : 38**,中间 6px 分隔条:`role="separator"` + `aria-valuenow`,拖拽/键盘(←/→ ±2)夹在 **40–75**,**<900px 折叠上下**(order:资产 → 工具 → 分段 → 交易表 → 净值 → 研判)。 - **块间距只允许 0 / 8 / 16**;表格 12px、行高 26px、1px 行分隔(`spacingProblems` / `tableRhythmProblems` 判据)。 - **MD 三层 elevation**:页面底 0 / 卡片 1 / 分段与图例 3;深色 surface 三级 `#16181b / #1b1d21 / #23252a`,**阴影仍 0 1px 级**(不放大)。 - **state layer** 悬停 8% / 按下 12%(`::after` + `opacity`);**ripple 仅按钮与分段**(100×100、`transform: scale` + `opacity`、`data-idle` 移除节点、reduced-motion 关闭)。 - **图表**:`PRISM_SERIES` 8 色(去重校验)、细网格 `tw-prism-gridline`、点标记 `tw-prism-dot`、数据标签、图例右上 `top:8px; right:8px`。 - 表格涨跌/盈亏仍绑定 `--tw-up / --tw-down`(**不被系列色污染**)。 ### 功能零回退(逐项断言) 四格数值、**摊薄成本列**、**仓位占比列**、**模型配置**、**页头自检文案**、时点增删与补跑、四份分报告、挂单/成交/净值、缺数据 `—` + 原因、**无声明性文案** —— 十项全部有源码断言,缺任一项或出现免责文案 ⇒ 测试失败。 ### 验证 `npm run check` **1156 / 1156 × 2 时区** + selftest + build(73 项);BA/BB 两轮干净复跑。 新增 `prismRedesign.test.ts` **10 条**断言,每条配反例注入必败:宽度夹取(允许 20% 或固定 50% ⇒ 败)|`<900px` 折叠(仍两栏 ⇒ 败)|间距只 0/8/16(`gap:12px` ⇒ 败)|分段在左栏顶部(右栏加第二段 ⇒ 败)|8 系列色 + 网格/点/图例(单色 ⇒ 败)|表格涨跌用 token(改用系列色 ⇒ 败)|ripple 仅按钮分段(表格行加 ⇒ 败)|三层 elevation(删分段 e3 ⇒ 败)|深色不放大阴影(`0 8px 24px` ⇒ 败)|功能十项不回退。 ### 未做 / 待验证 - **活宿主视觉走查未做**(需重启装载)。 - 设计稿里提示的取舍仍在:左栏三表合并为 tab 后,左栏内容比右栏短(约 810px vs 1126px),左下留白 —— 未处理(等用户裁决)。 ## v0.133.0 — 棱镜:摊薄成本、仓位占比列、模型配置、页头自检修复 报告 `docs/AUDIT-PRISM-B34.md`。`npm run check` **1146 / 1146 × 2 时区**(1139 → 1146)+ selftest + build(73 片段全过)。 ### 1) 成本改摊薄口径 - 复制持仓取主插件的 **`PositionRow.dilutedCost`**(仅当摊薄为 null 才回退 `avgCost`)。 - 模拟盘自身卖出后重算:`PrismPosition.netCost` —— 买入 `+= amount + fee`、卖出 `-= amount − fee`、`cost = netCost / qty`。 - 浮盈仍 `(现价 − cost) × 数量`,与主插件摊薄口径一致。 - **样例**:买 100@10(无费)→ 卖 50@12(无费)⇒ `netCost = 1 000 − 600 = 400`、`cost = 400/50 = **8.00**`(移动加权会仍为 10.00 ⇒ 断言必败)。 ### 2) 持仓表新增「仓位占比」 - 位置:市值之后、浮盈之前。 - 口径:`市值 ÷ (现金 + 持仓市值) × 100`,2 位小数;总资产为 0 或缺价 ⇒ `—`。 - 实测:两行市值 60/40、现金 0 ⇒ **60.00% / 40.00%**;现金 100、总资产 200 ⇒ **30.00% / 20.00%**(若用持仓内部占比会得 60/40 ⇒ 必败)。 ### 3) 模型配置 - `prism-settings.json` 增 `model:{provider, model}`;页面顶部两个自由输入 + 宿主 `listProviders()/listModels()` 的 datalist 候选(取不到则退化为自由输入,**不写死**)。 - `resolvePrismModel(get, selected)`:**配置优先**;研判与自检都使用配置值。 - 保存**未知 provider ⇒ 400**「未知 provider「x」(可用:…)」,不写入、不回退默认。 ### 4) 页头自检文案修复 - 此前把 HTTP 200 当失败、且固定 0ms;现为:`ok:true ⇒ 自检通过 · {elapsedMs}ms · {text}`|`ok:false ⇒ 自检失败 · {error}`。 ### 验证 `npm run check` **1146 / 1146 × 2 时区** + selftest + build(73 项);AU/AV 两轮干净复跑。 新增 7 条用例:摊薄成本(买 100@10 → 卖 50@12 ⇒ 8.00)|仓位占比(两组实测)|页头自检文案(成功/失败)|模型配置(5 次请求的 provider/model 全等于所选;自检按配置;未知 provider 400 且 `settings.model` 仍为 null)|首次 GET 的持仓成本来自摊薄口径。 ### 边界 - 活宿主内**新模型配置与摊薄复制待重启复验**。 - 模型候选依赖宿主 `listModels`,取不到时页面为自由输入。 - 摊薄成本为 null 的持仓回退 `avgCost`(主插件该行本身显示 `—`)。 ## v0.132.0 — 棱镜落账补齐:账户自动初始化、时间戳、四份报告、JSON 解析留痕、盘后拒绝、净值 报告 `docs/AUDIT-PRISM-B34.md`。`npm run check` **1139 / 1139 × 2 时区**(1134 → 1139)+ selftest + build(73 片段全过)。 ### 活宿主实测发现的五处缺失(已修) | # | 问题 | 修复 | | --- | --- | --- | | 1 | 账户是 `{}`(无现金/持仓) | `ensureAccount` 在**首次 GET / 首次研判 / 改资金前**执行:按真实 `avgCost` 复制,`cash = 150 万 − Σ(实际 qty×cost)`;不足则**按行部分复制**(`take = min(qty, floor(剩余/cost))`)并写 `copyLogs.note/skipped` | | 2 | `run.day` / `run.at` 为 `null` | 写北京日期与 ISO 时刻(`2026-10-22T17:12:53+08:00`);旧 runs 读盘自动补齐 | | 3 | `run.reports` 为 `[]` | `reports:[{role,text,error}]` **四份逐份落盘**并随回包返回;页面优先读 `reports` | | 4 | 挂单 0 笔且无原因 | 新增 `decision.parseError`:无 JSON / JSON 非法 / `orders` 被丢弃都会**带原文或丢弃计数**,**不再静默成 0 笔**;提示词要求严格 JSON | | 5 | `equity` 0 点 | GET、每次研判、复制持仓后各 `recordEquity`(总资产 = 现金 + 持仓市值,缺价按成本),**同日覆盖、每日一点** | ### 盘后下单:明确拒绝并留痕 引擎拒绝 `<09:30` 或 `≥15:00` 的决策单 ⇒ `rejected`,原因 `盘后不下单(HH:MM,A股 15:00 收盘)`,同时写 `decision.rejects` 并落盘。**不会伪装成「模型没动作」。** ### 查实一条:那次 0 笔不是解析失败 活宿主 `14:40` 那轮实际解析成功:`limits.singlePct=20`、`orders=[]`、`cancel=[]` 是**模型的真实选择**。 ### 验证 `npm run check` **1139 / 1139 × 2 时区** + selftest + build(73 项);AQ/AR 两轮干净复跑。 新增 5 条用例:① 首次 GET 后 `copiedAt` 非空、`cost` 正确、`cash` 正确、`equity=1`(另测资金不足 ⇒ 只复制部分 + 留痕)|② `day`/`at` 非空且 `+08:00`|③ `reports.length=4` 且文本非空、落盘可读|④ 汇总无 JSON ⇒ `parseError` 含原文、`decision` 非 null|⑤ 17:05 决策 ⇒ `rejected` + `rejects[0].reason` 含「盘后不下单」|⑥ 同日再 GET 仍 1 点。 ### 待验证 活宿主内复验(需重启):首次 GET 后账户/净值应有值;17:0x 补跑应出现 4 份 `reports` + `rejects`。 ## v0.131.1 — 修复棱镜自检「模型返回空文本」(content 必须传块数组) 报告 `docs/AUDIT-PRISM-SELFTEST-FIX.md`。`npm run check` **1134 / 1134 × 2 时区**(1132 → 1134)+ selftest + build(73 片段全过)。 ### 根因 `GenerateOptions.messages` 的真实契约是 `readonly ContentBlock[]`(`dsh-llm/lib/types/types.d.ts`:`MessageBase.content: readonly ContentBlock[]`),而 selftest 传了**字符串**。pi-ai 适配器对字符串调 `message.content.some(...)` 抛 `TypeError`,运行时把它转成 `finish(kind=error)` chunk;旧消费代码**只取 `text-delta`、丢掉 `finish`** ⇒ 0 chunk / **0 ms** / 空文本。 ⇒ 观察到 `elapsedMs: 0` 正是「流立即以 error 结束」的特征。 ### 修复 1. `messages` 改发 `[{type:'text', text}]`。 2. selftest 消费端**解析 `finish`**:`finish.reason.failure{code,message}` ⇒ `error = 模型流结束于 error [CODE]:<上游 message>`,并返回**前 3 个 chunk 的 `type` 与键名**便于定位。 3. 去掉 `maxTokens: 50`(reasoning 模型可能把预算全给推理 ⇒ 空文本);提示本身仍 ≤50 token。 4. 失败仍 **200**;空流 `error` 含「空文本(0 个 text-delta,reasoning-delta N 个…)」。 ### 三种可能原因的排除结论 | 可能原因 | 结论 | 依据 | | --- | --- | --- | | 消费方式错 | **部分成立,已修** | 旧代码忽略 `finish` 错误 chunk | | 缺必填 option | **不成立** | 必填只有 `provider` / `model` / `messages`;`system`、`maxTokens` 可选 | | provider 参数不同 | **不成立** | 与 `dsh headless` 同 provider/model(`workbuddy / cn:deepseek-v4.1-flash`),headless 能回 `OK`;差异只在 headless 走 agent loop、发的是块数组 | ### selftest 返回形状 成功:`{ok:true, error:null, model, elapsedMs, text:"OK", chunks:[{type:"text-delta", keys:[...]}, …]}` 失败:HTTP **200**,`error` 为原文,`chunks` 给前 3 个 chunk 的 `type` 与键名。 ### 验证 `npm run check` **1134 / 1134 × 2 时区** + selftest + build(73 项);AO/AP 两轮干净复跑。 新增 2 条用例(总 4 条):两个 `text-delta` "O"+"K" ⇒ `text:"OK"` 且断言 `messages[0].content === [{type:'text',text:'…'}]`(传字符串 ⇒ 必败)|空流 ⇒ `ok:false` + 「空文本」+ `chunks:[]`|抛错 ⇒ 200 + 原文|`finish(error)` ⇒ `ok:false` + 上游 message + `chunks[0]` 为 `finish`。 ### 待验证 **活宿主内的 `curl /tradewatcher/prism/selftest` 未复验**(需重启装载后执行)。 ## v0.131.0 — 棱镜批次 3+4:模拟盘引擎、完整页面、运行时自检、现金硬校验 报告 `docs/AUDIT-PRISM-B34.md`。`npm run check` **1132 / 1132 × 2 时区**(1110 → 1132)+ selftest + build(73 片段全过);未新增依赖;路由账 **32**(新增 selftest)。 ### 模拟盘引擎 | 口径 | 实现 | | --- | --- | | 资金 | 初始 **1 500 000**;POST 可改上限(入金)并同步现金;仓位上限由 agent 写入 `decision.limits` | | 起点持仓 | `copyAtCost` 按 **`avgCost` 复制**;`copyLogs` 记时间与前后差异;`cash = 上限 − Σ(qty×cost)` | | 挂单 | `PrismOrder.day` 当日有效;跨日 `cancelled` + 「当日未成交,次日失效」;后续研判可撤销并留 `cancelledAt` | | 撮合 | `fetchOneMinuteBars`(新浪 scale=1 → 腾讯 m1 → 既有分时);买看 `low`、卖看 `high`,**成交价恒为挂单价**;`source` 记 `1min` / `trend` | | 费用 | 佣金万 2.5(最低 5 元)+ 卖出印花税千 1 + 过户费万 0.1;**不计滑点** | | A股约束 | `rejectReasonOf` 四类(T+1 / 涨跌停 / 停牌 / 整手),`rejected` 落库**不静默** | | 账本 | `prism-account.json` / `prism-orders.json` / `prism-equity.json`;全部 `updateJsonLocked` + `ReadGuard`,坏文件 `quarantineFile` | **成交前硬校验(发布前补的真缺陷)**:`fillRejectReasonOf` 在写成交前校验 —— 买入须「成交金额 + 费用 ≤ 现金」,卖出须「可卖数量(已扣当日买入)≥ 委托量」;不足 ⇒ `rejected` + 原因,现金与持仓不变;`applyFill` 再加兜底抛错,**现金不会被写成负数**。 ### 完整链路实测 | 步骤 | 数值 | | --- | --- | | 初始 | 现金 1 500 000.00 | | 买 100@10(low 9.9) | 成交 10;费用 **5.01**;现金 **1 498 994.99**;成本 **10.05** | | 次日卖 100@10.8(high 10.9) | 成交 10.8;费用 **6.09**(含印花税 1.08);现金 **1 500 068.90**;已实现 **+68.91** | | 净值 | 10-22 **1 500 044.99** → 10-23 **1 500 068.90** | | 现金不足(1 000 买 100@20,需 2 005.02) | `rejected`「现金不足(需 2005.02,可用 1000.00)」,现金仍 1 000 | | 刚好够(1 000 买 100@9.9,需 995.01) | `filled`,现金 **4.99** | | 卖出超量(持 100 卖 200) | `rejected`,持仓仍 100 | ### 页面 顶部四格(总资产 / 现金 / 当日盈亏(缺基准 `—` + 原因)/ 累计盈亏)、持仓表(等宽数字)、挂单表(状态含「不可成交 + 原因」)、成交记录(时间 / 价格 / 数量 / 费用 / 来源)、净值迷你折线(无坐标文字)+ 单行口径、研判区(四份分报告 `
` + 汇总 + limits / 挂单 / 撤单清单 + token 用量 `—`)。**无声明性文案**;缺数据 `—` + 一句原因;参数常驻可见。 ### 运行时自检 `GET /tradewatcher/prism/selftest`(**永远 200**):真实调用一次 `ctx.llm.stream`(≤50 token),返回 `{ok, error, model, elapsedMs}`;失败也 200 且 `error` 给原文。用于重启后确认**插件内**模型调用是否可用。 ### 验证 `npm run check` **1132 / 1132 × 2 时区** + selftest + build(73 项);AM/AN 两轮干净复跑。 新增 22 条用例:`prismAccount.test.ts`(12 引擎 + 4 现金/持仓校验)、`prismPage4.test.ts`(4 页面)、`prismSelftest.test.ts`(2 自检:成功 `ok=true`;`stream` 抛 `boom-原文` 仍 200 且 error 带原文)。每条均配反例注入必败。 ### 未做 / 待验证(如实) - **活宿主内 `curl /tradewatcher/prism/selftest` 未执行**(需重启装载)。 - **1 分钟数据源未真机联网实测**(测试注入 bar)。 - 涨跌停判定为**近似**:前收 × 板块幅度,ST 依名称判断。 - token 用量上游不提供 ⇒ 显示 `—`。 ## v0.130.0 — 新页签【棱镜】批次 1+2:注册面 / 调度 / 骨架 / 金十 / 研判管线 / MD 页面 报告 `docs/AUDIT-PRISM-B12.md`、`docs/PROBE-PRISM2.md`。`npm run check` **1110 / 1110 × 2 时区**(1096 → 1110)+ selftest + build(73 片段全过);未新增依赖。 ### 批次 1:注册面与调度 - 新组件行 `tradewatcher-prism`(`cordis.patch.yml` 第 9 行)、新页签 **`棱镜` 最右**(第 8 个);单条路由 `/tradewatcher/prism`,**路由账 31**;`README` **8 页签 / 9 行**;`locale/rows/prism`、两条 `exports`、五处镜像与测试期望同步。 - `src/index.ts` 的 `inject` 加 **`llm`**;所有 `ctx.get` 包 try/catch(宿主 ctx 是 Proxy,未 inject 访问会抛)。 - 调度:`prismScheduler.ts` 的 `dueSlots`(默认 `09:35 / 11:20 / 13:10 / 14:40`,可增删,上限 50);**非交易日不跑**;自动只追 10 分钟内、更早**手动补跑**(POST `{action:'run', slot}` 只标该时点);水位落 `prism-settings.json`(`updateJsonLocked` + `ReadGuard`);30 秒节拍 + `unref` + 测试进程不启动。 - 交易骨架:`prism-skeleton.md`,首次打开写默认;页面多行编辑 + 保存(≤20 000 字)。 ### 批次 2:金十与研判 - 金十 `flash-api.jin10.com/get_flash_list`,带 `x-app-id` / `x-version`(否则 502),经 `withHostBreaker`;实测 **20 条/页**,最新 `15:41:51`;首次回填 **≤10 页、页间 ≥3 秒**、未完成标「回填中(已 N 页)」、之后 `lastSeenId` **增量**;保留 24 小时。 - 研判 `prismPipeline.ts`:**4 路并行**(A股宏观 / 外盘 / 大宗 / 信息面)+ **1 次汇总**;输入 = 真实持仓(`assemblePortfolio`,只读)+ 骨架 + 金十 24h + 既有 `fetchQuotes` 行情摘要;结果 `prism-runs.json`(保留 20 次);单路失败留痕、**不重试**;四路全空则汇总跳过。 - 页面:时点增删、骨架编辑、新闻状态与同步、最近研判、补跑按钮;**无免责文案**;缺数据 `—` + 一句原因。 ### Material Design(只本页) - elevation 六档 token `--tw-elev-0/1/3/6/8/12`(浅/深各一套)+ state layer(`::after` + `opacity`)。 - ripple 固定 **100×100**,关键帧只 `transform: scale` + `opacity`;`data-idle` 转真时 JS 移除节点;`prefers-reduced-motion` 关闭。 - 字号只用 **12 / 14**;无闪烁动画。 ### 两处偏离(如实记录) 1. **未用 `ctx.schedule`**:它必须绑定 `SessionId`,且投递会**开启 agent turn**(`dsh-schedule/lib/index.js:1596` 调 `agent.followup`)。改用已在用的 **30 秒节拍 + 水位**,行为等价且不产生额外会话。 2. **插件 fiber 内的 `ctx.llm.stream` 未实跑**:`inject` 只在装载时生效,不重启 `dsh web` 就无法装载新行。已用 `dsh headless '只回复 OK'` 验证同 profile 模型链路**真实可用(输出 `OK`,无错误)**;插件内路径用假 stream 单测覆盖(4+1、失败留痕、不重试)。 ### 验证 `npm run check` **1110 / 1110 × 2 时区** + selftest + build(73 项);两轮(AB/AC)干净复跑。 新增 14 条用例(六个测试文件),逐条注入必败:页签最右 + 路由账 31|`inject` 含 `llm` 且有 try/catch|时点落盘/可增删/周末不跑/补跑单点|骨架默认与读写|金十两头齐备|回填 ≤10 页 + 3 秒 + 增量停在水位|4 并行 + 1 汇总且失败不重试|MD(ripple transform / 字号 / elevation token)|无免责文案|`—` + 原因。 ### 未做 / 待验证 - **插件内模型调用未验证**:需重启 `dsh web` 后触发一次实跑。 - 无真实浏览器截图(不能重启服务)。 - 新闻同步在交易日每 15 分钟增量;首次回填由页面打开触发。 - **批次 3(模拟盘引擎)与批次 4(页面完整化)尚未开始**。 ## v0.129.1 — 修复「有日线却聚合不出周/月/年」(四个真实原因) 报告 `docs/AUDIT-BK-AGG.md` §F。`npm run check` **1096 / 1096 × 2 时区**(1091 → 1096)+ selftest + build(73 片段全过)。 ### 四个根因(逐条已实测) 1. **只读磁盘缓存**:v0.129.0 的兜底只认「同 fqt 的本地日线缓存」,**内存里已取到的日线看不到**。 2. **落盘不 await**:`void saveKlineCache(...)` ⇒ 日K 刚取到、文件还没写完时,判成「没有日线」。 3. **fqt 严格匹配是真缺陷**:本机 `1.601398` 只有旧命名(不复权)日线 **811 根**,用户默认前复权 ⇒ 按 fqt 过滤直接判「无日线」,**811 根被丢掉**。 4. **只在本次实时取到**(进程刚重启/缓存被裁/保存失败)时完全够不着。 ### 修复 - 聚合源改**四级**:内存 → 磁盘 → **其它 fqt**(`[0,1,2]`)→ **本次实时取日线**(与日K 同一套 TTL 缓存 ⇒ 第二次零请求)。 - 同口径优先;**不匹配也照用**,但回包 `fqt` 改成**日线实际那份**,口径句追加「日线口径:不复权(请求为前复权)」。 - 闸门仍是「原生优先 + 日线 ≥10 根」;缺失原因按**实际尝试**说(「本地与本次实时都没有取到日线」/「日线只有 N 根」)。 - 仍只走既有 `aggregated` 口径行(**不新增浮层**)。 ### 实测前 / 后(同一份真实缓存副本,上游全挂) | 标的 / 档 | 修前 | 修后 | | --- | --- | --- | | `0.399001` 日K | 240 根(2025-10-24 → 2026-10-21) | 同 | | `0.399001` **周K** | **null**,文案「…主机熔断冷却中(未发出上游请求)」← 与用户截图同款 | **172 根**(2023-06-19 → 2026-10-19) | | `0.399001` 月K / 年K | null / null | **41 根** / **4 根** | | `1.601398` 前复权周K(仅有不复权日线 811 根) | **null** | **172 根**,`fqt` 回包 = 0,口径句含「日线口径:不复权(请求为前复权)」 | 口径句示例:`由日线聚合(非上游原生周线/月线/年线)(周K)· 日线取自本地缓存 808 根(截至 10-22 09:31)`;实时取数那支写「日线为本次实时取数(240 根)」。 ### 验证 `npm run check` **1096 / 1096 × 2 时区** + selftest + build(73 项)。 新增 5 条用例,逐条注入必败:⑥① 内存有日线(先实时取 240 根再**删磁盘文件**)⇒ 周K 非空(只读磁盘 ⇒ 败)|⑥② 磁盘与内存都空 ⇒ 走实时取数且口径句写「本次实时取数(240 根)」(要求必须读盘 ⇒ 败)|⑥③ 只有磁盘缓存 ⇒ 成功且零 `fqkline` 请求(删兜底 ⇒ 败)|⑥④ fqt 不匹配 ⇒ 非空 + `fqt=0` + 口径写明两边(严格 fqt 过滤 ⇒ 败)|⑥⑤ 口径句单行且「由日线聚合」恰好 1 次(多一行 ⇒ 败)。 ### 未能定位的一项 用户截图那只标的**未能逐字定位**(本机 179 个日线缓存里没有 `slice(-240)` 首根 = 2026-04-24 的),复现用同类指数 `0.399001`。 另需注意:**当时安装版可能仍是 v0.128.0**(无聚合兜底)⇒ 截图那一刻的行为属旧版本。 ## v0.129.0 — K线:同源主机回退 + 周月年聚合兜底;移除键盘提示框;护盘标注精简 `npm run check` **1091 / 1091 × 2 时区** + selftest + build(73 片段全过);未新增依赖。 ### 一、K 线链路:同源主机回退(修「熔断冷却中」的真正来源) 实测:腾讯三台同源主机中 **`web.ifzq.gtimg.cn` 会阶段性回 WAF 501**(14:26 那刻另两台正常,14:41 已自愈)。 旧代码写死一台 ⇒ 窗口期内所有 A股/港股 K 线「没数据」,撞上东财熔断(真实 `/health`:`push2` 三台 trips 3/3/4 全 open)⇒ 用户看到的「主机熔断冷却中」。 - 回退顺序固定:`web.ifzq` → `ifzq` → `proxy.finance`(`proxy` 走 `/ifzqgtimg` 前缀,**参数逐字不变**)。 - 成功时只打一台:`1.600519` 周K 232 ms|`1.000001` 周K 103 ms|`116.00700` 日K 141 ms(各 1 条请求)。 - **501 记为「主机失败」**(`HTTP 501 from …`),**绝不当数据**;被兄弟机接住的那台在 `/health` 标 `servedBy` + 「降级(同源 X 接住)」,**不判死**;三台全挂才抛错(带逐台 `tries`),理由写「主机不可用(三台都不可用)」。 - `qfqday` 在 `ifzq` 与 `proxy` **逐字相同**(整包差异只在 `qt`/`mx_price` 时间戳,解析不取)。 ### 二、周 / 月 / 年:由本地日线缓存聚合兜底(零新增请求) - 通用聚合器 `src/host/klineAggregate.ts`(`usAggregate` 改为别名,**单份实现**);原生周期一条都没拿到时,用**同 fqt 的本地日线缓存**聚合,回包带 `aggregated`。 - 实测(真实 `1.000001` 日线 809 根,上游全离线):月K **20 根**、年K **4 根**;2026 年 O 3986.97 / H 4258.86 / L 3649.86 / C 3658.04、pct **−7.83**。 - `vol` / `amount`:**桶内任一缺失 ⇒ 该桶 `null`**(不 0 相加;指数日线无成交额 ⇒ `amount=null`)。 - 口径行(已有状态行,**无新浮层**):`由日线聚合(非上游原生周线/月线/年线)(月K)· 日线取自本地缓存 809 根(截至 10-22 14:41)`。 - 闸门:原生优先;日线 <10 根不聚合;**聚合结果有意不落盘**(避免派生数据混进原生缓存)。 ### 三、板块指数:原因改准确,且只渲染一次 板块指数**没有可用日线源**(腾讯不认 `90.BK*`;换腾讯板块码 `pt01801731` 只回 1 根;东财 `push2*` 零请求;本机 207 个缓存 `90.*` **0 个**)⇒ 仍「未取到」,但文案改为: `本地确实没有该周期的缓存数据;板块指数没有可用的日线源:腾讯 K 线不覆盖板块码(90.BK*),东财是唯一链路而它这次也没给出数据(本机长期连不通);本地也没有它的日线缓存,聚合不出周/月/年 —— 稍后自动重试` **不再出现「熔断冷却」**;同一句 note **只渲染一次**(此前出现两遍)。 ### 四、K 线左下角提示框:移除 - 键盘提示整段(`←/→ 平移`、`Shift 加速`、`Enter 显示`、`Esc 隐藏`、`+/- 缩放`、`Home/End`)连同**原生 `title` 提示层**一起删除。 - `区间内 B/S 笔数 · 净买入 · 买入/卖出均价` 搬进**同一条**状态行(`klineStats.ts` 纯函数 + `onRangeChange`),不新增第二行或浮层。 - `build.mjs`:锁定片段 `'键盘:←/→ 平移'` **替换**为 `'区间内 B '`(该字面量确已删除),片段总数仍 **73**。 ### 五、护盘标注精简(用户任务,Lead 校验后合并) Host 侧把摘要改为**多行**(`wrapItems()` 按 40 字换行)、`styles.ts` 用 `white-space: pre-wrap`;客户端不再各自硬编码措辞(`(满分权重)`/`(脉冲打 0.6 折)`/`阈值来源…` 改由宿主摘要统一给出)。 **校验结论**:评分与等级判定**未改**(`level` 只出现在文案拼接);关键事实仍在(`阈值来源`、`自建分位`、`脉冲`、`0.6`、`满分权重` 均在宿主侧命中);属文案重排与单一来源收敛。 顺手删除误留在源码目录的 `src/host/rescue.ts.bak-20261022`(156 KB)。 ### 验证 `npm run check` **1091 / 1091 × 2 时区** + selftest + build(73 项)。 新增 19 条用例: - A 主机回退 4 条:① 首台 501 + 第二台 200 ⇒ 成功且逐台记账(不回退 ⇒ 败)|② 三台全失败 ⇒ 每台 `fails+1`,理由含「主机不可用」「都不可用」且**不含**「上游没有这份数据」「熔断冷却中」(旧文案 ⇒ 败)|③ 解析逐字相同(换主机顺手去 `qfq` ⇒ 拿不到)。 - B 聚合 6 条:klt 102/103/104 都有数据且**零** fqkline 请求(旧路径 ⇒ null ⇒ 败)|无日线 ⇒ null + 准确原因(保留「熔断冷却」⇒ 败)|跨周 5 日两桶逐项(首末取反 ⇒ 败)|任一日缺失 ⇒ 桶 `null`(0 相加 ⇒ 败)|口径句单行含来源/根数/截至(删标注 ⇒ 败)。 - C 文案 3 条:同一句 note 只 1 处(恢复第二处 ⇒ 败)|`aggregated.note` 只 1 处且客户端不自造(多写一处 ⇒ 败)。 - K线提示框 6 条:三处源码无键盘关键词(加回 ⇒ 败)|无 `fixed/absolute` 提示浮层且容器无 `title`(恢复 ⇒ 败)|三行统计仍在状态行(删除 ⇒ 败)|状态行仍一行(拆两行 ⇒ 败)|细节卡仍在(改回浮层 ⇒ 败)。 ### 未做(如实) - 分时链(`fetchTencentMinutes`)未加同源回退(多处断言依赖其请求条数)。 - `rescue.ts::fetchAvgAmount20` 的 `fqkline` 未按同一 helper 收口(该文件同批被改,留待下一轮)。 - 有月K 时仍走既有 `resampleYearly`(两套年K 口径未逐根对比)。 - 美股不从本地日线缓存聚合(已有新浪路径)。 - selftest 会真实取数并刷新 2 个 kline 缓存文件(既有行为,非本轮新增)。 ## v0.128.0 — 竞价参考价曲线(9:15–9:25 采样 + 落盘 + 抽屉迷你图) 报告 `docs/AUDIT-AUCTION.md` §7、`docs/PROBE-AUCTION-LIVE.md`。`npm run check` **1072 / 1072 × 2 时区**(1060 → 1072)+ selftest + build(73 片段全过);**未新增路由**(路由账仍 30)。 ### 采样(宿主) - 新 `src/host/auctionSeries.ts`:窗口 `09:15:00–09:25:30`(Asia/Shanghai,含 09:25:30),每 **10 秒**一拍。 - 交易日判定用**行情源时间** `f30` 的日期(不猜节假日);`f30` 按北京墙上时间显式解析(`TZ=UTC` 不漂)。 - 取腾讯 `f9`(买一价)/`f10`(买一量)/`f19`(卖一价);**仅当 `f9 === f19` 且 > 0、`f10 > 0`** 才记录 `{t, ref, matched}`;`f9 !== f19`(已进入连续竞价)该拍**丢弃**。 - 标的:**自选 ∪ 持仓**,去重、自选顺序优先、上限 **60**(超限留痕)。 - 定时器 `unref`;测试进程(`NODE_TEST_CONTEXT`)不启动真实定时器。 ### 落盘 - `/dsh-tradewatcher/auction/.json`:根 `{day, points[]}`(今天)+ `history[]`(最近 4 日)⇒ **合计 5 个交易日**;单日 **≤120 点**,超限丢最旧,按 `t` 去重。 - 走既有 `updateJsonLocked` + `ReadGuard`;坏文件先 `quarantineFile` 再重建;重启不丢(历史直接读盘)。 - 当天未采到 ⇒ `series: []`。 ### 展示 - 详情回包 `StockDetail.auction` 增加 `series` / `seriesDay`(**无新路由**)。 - 抽屉竞价行下方 `AuctionSpark`:`ref` 折线(`--tw-accent`)+ 下部 `matched` 细柱(`--tw-muted`),高 **44px**,**无 ``、无网格、无浮标**,`aria-hidden`。 - 行尾 `auctionSeriesText` ⇒ `1332.68 · 1330.75`(最高 · 最低,2 位小数);无点 ⇒ `—`。 ### 干跑实测(构造 63 点,09:15:00–09:25:20) `auctionSeriesText = 1332.68 · 1330.75`;几何高 44;量柱 63 根;落盘 6 天后 ⇒ `{day: 2026-10-22, history: [10-21, 10-20, 10-17, 10-16]}`(**5 日、最旧被丢**)。 ### 同日实盘探针的更正(`docs/PROBE-AUCTION-LIVE.md`) 09:15:50–09:26:01 共 **61 次**:`f10 > 0` **61/61**;`f9 === f19` **55/61**(不是 61/61 —— 我此前说"去重集合完全一致"过于绝对);出现 **23 个**不同参考价。 **09:25:10 起 `f9 ≠ f19`** 且 `f6`(成交量)从 0 跳到 **836 手** ⇒ 与「进入连续竞价即不记录」的规则一致。 ### 验证 `npm run check` **1072 / 1072 × 2 时区** + selftest + build(73 项);S/T 两轮干净复跑。 新增 12 条用例(宿主 8 + 展示 4),逐条注入必败:窗口边界(09:14:59 / 09:25:31 不采;09:15:00 / 09:25:00 采;全天都算 ⇒ 败)|`f9 ≠ f19` 不入库(照收 ⇒ 败)|单日 ≤120、5 日、丢最旧(不裁剪 ⇒ 败)|上限 60(不截断 ⇒ 败)|曲线无文字/网格/浮框且高 ≤48(加 `text` ⇒ 败)|回包带 `series` 且**无竞价路由**(源码出现竞价路由 ⇒ 败)|无点 ⇒ `—`(写 `0.00 · 0.00` ⇒ 败)。 ### 未做 / 待验证 - **本实现的端到端真实采样尚未跑过**(今日 9:25 已过)⇒ 待明日 09:15–09:25 验证。 - 非沪深不采样(`series: []`);窗口内 6/61 次 `f9 ≠ f19` 按规则跳过;**不做历史补采**。 ## v0.127.0 — 行情抽屉新增「竞价」行(沪深,9:25 撮合价 + 竞价量) 报告 `docs/AUDIT-AUCTION.md`。`npm run check` **1060 / 1060 × 2 时区**(1052 → 1060)+ selftest + build(73 片段全过);未新增路由、页签、卡片行或数据源。 ### 展示 个股详情抽屉中,紧跟「今开」新增一行: > `竞价 1331.88 · 516.00 手 · 1.54% · +0.00%` 四格含义:竞价价(=当日开盘价)· 竞价量(手)· 占当日总量比 · 相对昨收溢价(带符号)。 **无口径、无解释、不进悬停**(按用户既有要求)。 ### 实现 - 契约:`shared/model.ts` 新增 `AuctionInfo`(`ok` / `not-open` / `unavailable` + 四项 + `reason`),`StockDetail.auction`。 - 计算:`host/auction.ts` 的 `auctionOfMinutePoints`(**只认 `dayOf(ts) === 今天`**;首根价与量、末根累计量、`prev` 算溢价)+ `fetchAuction`(15 s 缓存、**永不抛**;非沪深给「仅沪深提供竞价」)。 - 接线:`host/em.ts` 的 `fetchStockDetail` 在拿到 `prev` 后计算,随详情回包下发(**无新路由**)。 - 展示:`client/auctionView.ts` 的 `auctionText` 固定格式;`QuoteDrawer.tsx` 用 `kv('竞价', …)`。 ### 实测(`1.600519`,2026-10-21 收盘后) | 项 | 值 | 依据 | | --- | --- | --- | | m1 当日根 | **241 根**(09:30 → 15:00) | `fetchTencentMinutes` | | 竞价价 | **1331.88** | m1 首根 `price` = 日开(腾讯/新浪一致) | | 竞价量 | **516.00 手** | 首根 `cumVol` | | 当日总量 | **33 514 手** | 末根 `cumVol` = 腾讯行情 `vol` | | 占全日量比 | **1.54%** | 516 ÷ 33 514 | | 昨收 | **1331.88** | 腾讯日 K + 新浪 quote 双源一致 | | 溢价 | **+0.00%**(平开) | 1331.88 ÷ 1331.88 − 1 | ### 边界 - **未开盘**(`m1` 无今日根,例如 09:15–09:26)⇒ `竞价 —`,**不得**用昨日数据顶替。 - `m1` 不可用 ⇒ `竞价 —(原因)`。 - 非沪深市场 ⇒ `竞价 —(仅沪深提供竞价)`(**不猜**其它市场的竞价语义)。 ### 验证 `npm run check` **1060 / 1060 × 2 时区** + selftest + build(73 项);Q/R 两轮干净复跑。 新增 8 条用例(宿主 5 + 展示 3),逐条注入必败:四项齐全且格式逐字(少一项 ⇒ 败)|无今日根 ⇒ `—`(用昨日顶替或写 0 ⇒ 败)|占比分母必须是**当日**总量(用昨日 ⇒ 败)|溢价分母必须是**昨收**(用今收 ⇒ 败)|抽屉无解释文字且该行无 `title`(加说明 ⇒ 败)。 ### 未做 - **开盘竞价时段(09:15–09:26)实盘验证**:已安排定时任务在 2026-10-22 09:15:40 取样到 09:26,判据是腾讯 `f10 > 0` 且 `f9 == f19` 且随撮合变化 ⇒ 竞价期间逐点参考价可得;否则该路线关闭。 - 港股/美股/指数/商品的竞价语义未验证(不猜)。 ## v0.126.0 — 移除【棱镜】组件(完整回滚 + 冗余文件清理) `npm run check` **1052 / 1052 × 2 时区**(1085 − 33 条棱镜用例)+ selftest + build(73 片段全过);`build.mjs` 未改;无新增依赖。 ### 删除文件(11) `src/shared/prism.ts`、`src/host/prism.ts`、`src/host/prismFeed.ts`、`src/host/prism.test.ts`、`src/client/PrismPage.tsx`、`src/client/prismView.ts`、`src/client/prismView.test.ts`、`locale/rows/prism/en.json`、`locale/rows/prism/zh.json`、`docs/AUDIT-PRISM.md`、`docs/PROBE-PRISM-FEED.md`。 ### 回改集成点(14) `src/index.ts`(`RowId` / `ROW_ROUTES` / `ROW_TOOLS` / `rowOf` / `all`)、`src/client/index.tsx`(`PageKey` / tabs / `KEEP_ALIVE`)、`src/client/api.ts`(方法与 DTO 导入)、`src/host/routes.ts`(`/tradewatcher/prism`)、`src/host/selftest.ts`(7 条棱镜探针)、`src/index.test.ts`、`src/rowOwnership.test.ts`、`src/client/impl78.test.ts`、`src/client/impl83.test.ts`、`src/client/toolsPage.test.ts`、`src/readmeCounts.test.ts`(仅回改棱镜计数,**保留** README 免责措辞判据)、`cordis.patch.yml`(**9 → 8 行**)、`docs/DESIGN-SUBPLUGINS.md`(**路由账 31 → 30**,§2.1/§2.2 删行)、`package.json`(删两条 `exports`)、`README.md`(**页签 8 → 7**、**行 9 → 8**、删棱镜整节)。 ### 保留(成果仍在代码里,文档不删) `docs/RECON-WORKBENCHES.md`、`docs/DESIGN-ADOPT-WORKBENCHES.md`、`docs/AUDIT-IMPL89.md`;`CHANGELOG.md` 的**历史条目不改写**。 ### 复核读数(脚本实测,非手算) | 项 | 读数 | | --- | --- | | `git ls-files \| grep -i prism` | **空** | | `grep -rn "棱镜\|prism" src/ docs/ cordis.patch.yml README.md` | **无命中** | | README 页签 / 代码 | 声明「七」、`PageKey` + tabs = **7** | | README 行 / 代码 | 声明「八」、`ROW_ROUTES + core` = **8**、`cordis.patch.yml` id = **8** | | 路由账 | **30** | ### 验证 `npm run check` **1052 / 1052 × 2 时区** + selftest + build(73 项);O/P 两轮干净复跑。 ## v0.125.0 — 棱镜:修复「六块全空」;默认标的与回退改到有源市场 报告 `docs/AUDIT-PRISM.md` §12。`npm run check` **1085 / 1085 × 2 时区**(1078 → 1085)+ selftest + build(73 片段全过)。 ### 根因 默认标的取了**自选首项**(港股 `116.00148`),而**港股没有分钟线源** ⇒ 六块全空;原因文案又只有笼统一句「腾讯 5 分钟回空」。 实测确认**没有第二个 bug**:`pairA=1.600519` 时六块全有值。 **修复前 / 修复后(真实请求)** | 请求 | bars | 六块 | | --- | --- | --- | | `pairA=116.00148`(修复前) | **0 根** | **全空** | | `pairA=1.600519`(对照) | **120 根**(`tencent-m5`) | 全有值:bayes p=0.2619、edge 「不下结论」EV=−14.23bps、stoikov s=1306.6(micro-price)、kelly 0、mc ruin 0 | | 默认(修复后) | 解析为 `1.600519`,**120 根** | **6/6**:spread z=0.754、stoikov s=1306.12 | | 手动选 `116.00148`(修复后) | 回退到 `1.600519`,120 根 | **6/6** | ### 修复逐条 1. **默认与回退统一在宿主**:`resolvePairA`(优先自选第一个 **A股**,尽量不等于配对 B,否则 `1.600519`);首次解析 + A 真取不到 bar 时再回退一次并继续算;payload 带 `fallback{requested,actual,reason}`。 2. **空态兜底**:A 无 bar ⇒ 回退后照算;B 无 bar ⇒ **只影响 Spread**,其余五块只看 A。 3. **原因按市场分列**:`116.*` ⇒ `港股无分钟线源(需授权行情)`;`100.KOSPI200` ⇒ `指数无分钟线`;`101.CL00Y` ⇒ `该品种无 5 分钟源`;美股失败 ⇒ `美股分钟线取数失败(原因)`。取数入口按市场直接短路,**不再打无意义的上游请求**。 4. **顶部数据行可判读**:`30 分钟 · 120 根` / `30 分钟 · 6 根(样本不足)`;未新增解释段落。 5. **显示实际标的**:`实际标的 1.600519(已从 116.00148 回退)`;客户端超时放宽到 25 s,给回退重试留时间。 6. **`secid` 解析核对**:`116.00148` → `rt_hk00148`、`116.00700` → `rt_hk00700`,**未截断成同值**。 ### 验证 `npm run check` **1085 / 1085 × 2 时区** + selftest + build(73 项);M/N 两轮干净复跑;棱镜 live 探针 7 条全过。 新增 7 条判据,逐条注入必败:默认 A股且首屏 ≥4 块有数值(默认改回港股 ⇒ 败)|港股原因逐字(写「腾讯 5 分钟回空」⇒ 败)|A 无 bar 自动回退(回退但参数框仍写请求值 ⇒ 败)|回退后参数框显示实际标的(删同步 ⇒ 败)|B 无 bar 只影响 Spread(整页清空 ⇒ 败)|两只港股 secid 不混(截断成 `00000` ⇒ 败)|顶部数据行看得出样本不足(去掉「(样本不足)」⇒ 败)。 ### 未做(如实) **港股 5 分钟 OHLC 仍无源**(腾讯 `mkline?param=hk*` 报 `param error`、新浪 `HK_MinKService` 不提供)⇒ 港股现在会被**自动回退到 A股**,而不是硬算。要真正支持港股六块,需授权行情源(港交所 BBO / 券商 L2 / 另一家港股分钟线)。 ## v0.124.0 — 棱镜接入美股分钟线;港股接盘口价;README 免责措辞清除 报告 `docs/AUDIT-PRISM.md` §10–§11、`docs/PROBE-PRISM-FEED.md`。`npm run check` **1078 / 1078 × 2 时区**(1070 → 1078)+ selftest + build(73 片段全过);未新增依赖。 ### 一、美股分钟线:源本就在仓库里,根因是「符号命名空间接错」 - 真源:新浪 `US_MinKService.getMinK?symbol=<裸小写 ticker>&type=1|5|30`(**实测 200、1023 根**,字段 `d/o/h/l/c/v/a`,`d` 为 **ET 收盘时刻**,`v` 单位**股**)。 - **「新浪 5 分钟回空」的根因已定位并复现**:`sinaSymbol('105.FRO')` 给的是快照符号 `gb_fro`,被喂进**沪深** K 线服务 ⇒ HTTP 200 回 4 字节 `null`。**不是缺源。** - 实现:`sinaUsTicker`(`105/106/107.X → x`)+ 独立解析 `parseUsMinuteText`(**未改**分时链的 `SinaUsMinBar`)+ `usWallToEpoch` 按 ET 收敛 + 本地 `slice(-240)` + `BAR_CACHE` 45 s;**单链**(美股无第二源),失败给原因。 | type | 根数 | 首根 ET | 末根 ET | 末根 UTC | | --- | --- | --- | --- | --- | | 1 | 240 | 2026-10-20 12:01 | 2026-10-20 16:00 | `20:00Z` | | 5 | 240 | 2026-10-15 15:35 | 2026-10-20 16:00 | `20:00Z` | | 30 | 240 | 2026-09-24 13:30 | 2026-10-20 16:00 | `20:00Z` | (`16:00 ET → 20:00Z` 是 EDT;若按 UTC 解析会偏 4 小时。) ### 二、港股:只接「盘口价」,用独立字段,不混形态 - 5 家公开源**都只给价、不给量**;腾讯港股 10 个量槽全 0,**且 `f9=f19=现价`(连两档价都不是真盘口)**、滞后 **15 分 06 秒** ⇒ **腾讯港股盘口整条弃用**。 - 采用新浪 `rt_hk00700` 的 `f9/f10`(买一/卖一价,实测**同一秒**新鲜度)。实测:`available:false`、`levels:0`、`microPrice:null`、**`quoteMid=400.70`**、`quoteAt=2026-10-21T05:41:09Z`。 - **契约(防下游混淆)**:`available:true` ⇔ `levels>0` + `microPrice≠null` + `quoteMid===null`;`available:false` ⇒ `microPrice===null`、`levels===0`,可带 `quoteMid` + `quoteSource` + `quoteAt`。 - `s` / `mid` 取数顺序:**micro-price → 盘口中点 → 原因**(不再拿收盘价凑);页面常驻显示输入名(`s:盘口中点(1 档价)`、`mid:A micro-price(5 档) / B 盘口中点(1 档价)`)。 - 回归:沪深 `600519` 仍 `levels=5`、`micro=1307.88`、`quoteMid=null`;美股 `AAPL` 盘口 5 档、`micro=338.15`。 ### 三、README 免责措辞清除(4 处) | 位置 | 处理 | | --- | --- | | 首段尾句「不构成投资建议」 | 删除 ⇒ 收尾「仅作盯盘参考。」 | | 棱镜段「免责类注解不上屏」 | 改为「**注解类文案不上屏**」并只列数据事实 | | 章节标题「已知边界与免责」 | 改为「**已知边界**」(该章 14 条未动) | | 清单「本插件不构成投资建议」 | 整条删除 | `grep` 复核:README 中 `不构成投资建议 / 投资建议 / 免责` **0 命中**;`docs/` 的审计记录保留。 新增断言:README 不得出现这三类措辞(3 条注入必败,含 `assert.throws`)。 ### 验证 `npm run check` **1078 / 1078 × 2 时区** + selftest + build(73 项);两轮(K/L)干净复跑。 新增 8 条用例,逐条注入必败:美股 5/1/30 各 ≥240 根(改回 `gb_` ⇒ 败)|`16:00 ET → 20:00Z`(按 UTC 解 ⇒ 败)|1023 根被 `slice(-240)`(不截 ⇒ 败)|港股契约(mid 塞进 `microPrice` ⇒ 败)|`mid`/`s` 优先级(跳过 `quoteMid` ⇒ 败)|输入名常驻(删 ⇒ 败)|沪深 5 档回归(编 0 ⇒ 败)。 live 探针(soft)三条本次全 PASS:美股三档 ≥240 根、沪深 5 档 micro、港股只有价。 ### 未做 / 边界(如实记录) - **港股 5 分钟 OHLC 仍无源**(腾讯 `mkline?param=hk00700,m5` 报 `param error`;新浪 `HK_MinKService` 报 `Service not valid`)⇒ 港股作配对 A 时六块因**没有 bar** 给原因;`quoteMid` 的 `s`/`mid` 优先级只在单测与「配对 B」路径成立。 - **港股 micro-price** 仍需授权源(港交所 BBO / 券商 L2);本轮只到「有价、无量」。 - 美股**无第二源**(单链为刻意设计,不假装兜底);挂单价 δ 按口径不提供。 - 侦察自曝一处纪律违规:`api.twelvedata.com` 的 robots 亦为 `Disallow: /`,它在同一批命令里先取 robots、紧接着发了 **1 次**数据请求(未等门禁),此后无请求;该源按「不可用」判定。 ## v0.123.0 — 新页签【棱镜】(`dsh-tradewatcher-prism`):六模型只读推演 报告 `docs/AUDIT-PRISM.md`。`npm run check` **1070 / 1070 × 2 时区**(基线 1051 + 19)+ selftest + build(73 片段全过);未新增依赖。 ### 页签与归属 - 新页签 **`棱镜`**,位置**最右**;组件行 `tradewatcher-prism`(`cordis.patch.yml` 第 9 行)。 - 归属镜像全部同步:`ROW_ROUTES.prism`、`ROW_TOOLS.prism`、`RowId`/`rowOf`、`FUNCTION_OWNED_PATHS`、`index.test.ts`/`rowOwnership.test.ts`、`DESIGN-SUBPLUGINS.md` §2.1/§2.2 与**路由账 31**、`README`(**八个页签 / 九行**)、两条 `exports`、`locale/rows/prism/{en,zh}.json`。 - 页签保活但**只在打开时取数**(`active` 门控,不因启动即回源)。 ### 六块(`GET /tradewatcher/prism`,**永远 200**,缺数据 `null` + 一句原因) | 块 | 实测(600519 / 000001,freq=5) | | --- | --- | | Bayesian | p = **0.419**(先验 0.5) | | Edge | EV = **−6.38bps** ⇒ **不下结论**、无目标价 | | Spread | z = −1.12、中位 4.742576(**5 档 micro-price**)、未越阈;**不给挂单价** | | Stoikov(模拟) | s = 1312.44、r = 1289.42、未触发 `T−t→0` 保护 | | Kelly(模拟) | 模拟值 = **0**(f* ≤ 0 ⇒ 不押) | | Monte Carlo | 10 000 × 240;MDD P50/P95 = **10.93% / 13.21%**;破产 **0**;四条假设逐条上屏 | - 降频链:样本不足 ⇒ `5 → 10 → 30` 逐档重试,足迹进 `downgrade.attempts`;三档都不够 ⇒ 六块给全量原因。 - ⚠ **降频不会救回「样本不足」**:聚合后根数只会更少(已写进审计,属实测结论)。 ### 五档盘口实测(决定 micro-price 能算到几档) | 市场 | 结果 | | --- | --- | | 沪深股票 / ETF | **5 档齐全**(`1.600519`、`0.159915`) | | **港股** | **0 档**(腾讯 `hk*` 量恒 0、时间戳停 `11:59:59`;13:07 / 13:40 两次复测一致) | | 指数 | 0 档(无盘口) | 初版把 `0.00 / 0` 的档当有效价会算出 0 ⇒ 已修为「**价 > 0 且量 > 0** 才计入」,全无效 ⇒ `available:false` + 原因。 ### 用户裁决的两处偏离(覆盖我此前的红线约束) 1. **允许给买卖/开仓/目标价**,但:目标价必须带**规则短标签**(`ATR×2` / `历史P95`)且可复算;Edge 不达标时**必须走「不下结论」**。 2. **界面不放免责类注解**:`推演结果,不构成投资建议`、`本页信号基于简化模型…` 两句及其常量**已从源码彻底删除**;断言**反转**为「免责类字样必须为零」(黑名单含 `不构成投资建议 / 推演结果 / 免责 / 风险提示 / 我方历史复核 / 可证明的预测力 / 本页信号基于简化模型`),并新增"源码里连注释都不许有"的断言。 - Kelly 卡文案同步改为中性口径「模拟值(f* × 分数系数)」,**「模拟」字样保留**。 - `README` 里全站既有的那句注解也已按用户要求删除(首段、章节标题与清单条目共 4 处;`readmeCounts.test.ts` ⑤ 断言全文不再出现该类措辞)。 ### 验证 `npm run check` **1071 / 1071 × 2 时区** + selftest + build(73 项);两轮干净复跑(1071 = 1070 + README 注解清理的新判据)。 新增 19 条用例(10 宿主 + 9 页面),逐条注入必败:页签最右(挪中间 ⇒ 败)|六块 + 原因(`samples=0` / `reason:null` / `NaN` / 假设 0 条 ⇒ 败)|**免责字样为零**(加回任一条 ⇒ 败)|`模拟` 字样(删 ⇒ 败)|参数常驻可见(只进 `title` ⇒ 败)|切频率只改参数(多一条路由 ⇒ 败)|聚合正确(取错首/末 ⇒ 败)|`T−t→0` 保护(不保护 ⇒ 败)|MC 假设全上屏(`slice(0,3)` ⇒ 败)|永远 200(上游 `throw` ⇒ 仍 200 且六块齐全)|EV 不达标必须「不下结论」(硬给方向 ⇒ 败)|目标价裸数字(⇒ 败)。 ### 未做 / 需新数据源(如实记录) - **美股分钟 OHLC** 未接(现只有新浪日线)⇒ 该市场给原因。 - **港股五档数量**结构性拿不到(腾讯量恒 0、新浪不覆盖)⇒ 需港交所 BBO / 券商 L2 / 另一家延迟行情(**未自行接新源**)。 - **挂单价 δ** 按口径不提供。 - 隐身档数字模糊未接(页面无金额字段)。 - 偏离记录:原计划「复用 `fetchKline` 的 5 分钟线」**不成立**(`fetchKline` 只有日/周/月/年;仓库里的 5 分钟线是分时链、只留 close/vol)⇒ 新增 `prismFeed.ts` 复用**同一上游与同一套时间解析**,未接新源。 ## v0.122.0 — 底部概率的区间口径声明(先测量、后上屏) 报告 `docs/AUDIT-IMPL89.md` §9。`npm run check` **1051 / 1051 × 2 时区** + selftest + build(73 片段全过)。 ### 为什么上屏:先测量,再决定 底部校准**没有独立落盘文件**(`stripSnapshot()` 不落 `bottom`;实测 `rescue-log.json` 的 `lastSnapshot.bottom` 为 `null`)⇒ 用**同一判据在同一份磁盘数据**上重算,并与宿主 `laneOutcomeStats()` **交叉校验 3/3 一致**(28=28 / 29=29 / 22=22)。 | 指标 | 读数 | | --- | --- | | 同日 ≥2 通道命中 ÷ 可评估日(≥2 通道) | **29 / 255 = 11.4%** | | 同日 ≥2 通道命中 ÷ 命中日 | **29 / 42 = 69.0%** | | 命中日的连续段 | 17 段;最长 **6 日**;**83.3% 的命中日落在 ≥2 日连续段内** | | Wilson 用的样本量 | `N_raw = 79` ⇒ 去横截面重复 **`N_date = 42`(−46.8%)** ⇒ 再去前向窗口重叠 **`N_greedy = 15`(−81.0%)** | | Wilson 95% 宽度(+1% 目标,p=68.4%) | **20.1pp → 27.0pp → 43.1pp** | **判定依据**:① 按「占全部可评估日」读落在 10–20 带(单看只能给建议),但**「去重后 N 下降 ≥20%」独立成立**(−46.8% / −81.0%)⇒ 按裁决的「或」关系支持上屏;另一侧证据同向(83.3% 命中日在连续段内)。 **敏感性**:加入第 4 个通道(缓存表头无 fqt)⇒ 12.2%、最长 9 日、85.4%、`N` 107→48→16 ⇒ **通道越多重复越高,11.4% 是下界**。 ### 声明文案(唯一来源 `shared/model.ts`) > `样本含同日多通道重复计入(相邻日的前向窗口也重叠)⇒ 区间偏窄` 链路:`calibratePooled` → `BottomCalibration.intervalNote` → `buildBottomLane` → `RescueBottomLane.calibration` → 大盘页「历史频率」的 **`title` + `aria-label`**(读屏也读得到)。 **不占常驻行、无任何数字、不给「偏窄多少」**(那会变成新的"准确率");**不进 `detailModel`**;**不改任何概率数值**。 ### 验证 `npm run check` **1051 / 1051 × 2 时区** + selftest + build(73 项)。 新增 `src/host/bottomInterval.test.ts` **5 条**,均验过必败: 1. **三个入口**(`calibratePooled` / `calibrateAcross` / `calibrateBottom`)都原样带上唯一来源那句。 2. **措辞纪律**:必须含「重复计入」「偏窄」;`/\d/` 必须为 0;不得含「准确率/命中率/精确/可靠」⇒ 注入「(约 80%)」数字判据红;换成「区间精确」⇒ 偏窄判据红。 3. **链路**:`buildBottomLane` 输出带着它(界面读这一份,不自算)。 4. **引用面恰好 3 个文件**(`shared/model.ts` · `host/bottom.ts` · `client/RescuePanel.tsx`);`toolRegistry.ts` / `ToolsPage.tsx` 不许出现 ⇒ 谁把它接进 `detailModel` 必红。 5. **源码锚**:大盘页「历史频率」的 title/aria-label 必须来自 `calibration.intervalNote` ⇒ 删掉必红。 ### 测量边界(如实记录) - 磁盘只有 **3 只 fqt=1 日线**(6 只里 3 只)⇒ 横截面重复率是**下界**。 - 东财缓存 ≠ 生产用的腾讯日线(单通道命中数会差几个点,但「同日多通道同向触发」属市场结构)。 - `N_greedy` 是**乐观上界**,真实有效样本量介于 `N_date` 与 `N_greedy` 之间。 - **未做显著性检验**,也**没有据此改动任何概率数字**。 ## v0.121.0 — 载荷闸门 + 出处 lint + 缓存上限 + 曲线上限;修「有规模无份额」无原因 报告 `docs/AUDIT-IMPL89.md`、`docs/DESIGN-ADOPT-WORKBENCHES.md`。`npm run check` **1046 / 1046 × 2 时区**(基线 1007 + 39)+ selftest + build(73 片段全过);未新增依赖。 ### R1 载荷闸门(`src/shared/payloadGate.ts`,纯函数) 三类检查:**出处**(有值必须有来源 / 没值必须有原因)、**数据时刻**(必须能答"这是几点的数据",答不出必须给原因)、**口径**(不变量用 `actual/expected/tolerance/why` 的**数据**表达,算不出必须有原因)。 - 适配器:蛋糕走 `cell` 档(每格本来有出处);杠杆走 `payload` 档,**走这档必须写 `sourceScopeWhy`**,例外条数锁定为 **1**。 - **拒绝运行时阻塞**(只做测试强制点):失败方向是「少给 + 写原因」。 - **闸门第一次跑真实载荷就抓到 2 行真缺口并已修**:资金蛋糕市场表里 **A股/港股 有规模、无份额、无原因**(份额依赖该市场自己的日线,而原因函数只覆盖"汇率缺")。修法:补第三类原因。**这是本轮唯一改到用户可见文案处,数字与口径未变。** ### R3 出处字段 lint `api.ts` 每个顶层 `*Payload`/`*Dto` 必须含 `asOf` + `missing`,或进**显式白名单**(**8 条**,每条带 `carrier` 指向真实字段);**条数锁定**,加一条必须显式改数字。 ### R4 客户端缓存上限(先测后定) | 测量 | 结果 | | --- | --- | | 每标的键数 | **14** | | 标的数 | **45**(自选 41 ∪ 持仓 9) | | K 线回包字节(n=207) | p50 70.1 KB / **p95 88.5 KB** / max 140.4 KB | | 分时回包字节(n=553) | p50 14.7 KB / p95 18.6 KB / max 54.7 KB | | 全量浏览上界 | 45 × 14 = **630 条 ≈ 48 MB** ⇒ 必须有上限 | 取值 **`CHART_CACHE_MAX_ENTRIES = 160`**(≈ **13.8 MB**;常量旁存测量证据)。淘汰:先清过期(**不计 `evicted`**)→ 仍超限按最早过期淘汰 → **绝不淘汰正在写入的那条**。断言把上限钉在 ≤16 MB,**同时**断言"全量上界必须 >16 MB" ⇒ 判据非恒真。 ### R5 曲线上限 序列 **≤6**、标记 **≤50**,判据在**纯模型层**;超限 ⇒ **清空 + 空态给原因**(含真实条数并解释"为什么不截断")。边界是 `>` 不是 `>=`(恰好 6/50 放行)。 ### 文档漂移(两处,已修 + 防复发断言) - `README` 页签数 六 → **七**(补复盘);行数 七 → **八**(补 `fupan`);行号引用改为**引用 `- insert:` 块**(不再写会漂的行号)。 - 新增 `src/readmeCounts.test.ts`(6 条)锁五处同源事实:README 页签数 == 自列名 == 客户端 tabs 数 == `PageKey` 成员数;README 行数 == 自列名 == `ROW_ROUTES`+core == `cordis.patch.yml` 的 id 行数;README 不许再写 `cordis.patch.yml:<行号>`。 ### 验证 `npm run check` **1046 / 1046 × 2 时区** + selftest + build(73 项)。 每条断言都验过「注入即红」:闸门必拦 12 种(0 顶替 / 有值无出处 / 缺值无原因 / 无 `sourceScopeWhy` / `asOf` 空且无 `why` / 97≠100 / 不变量算不出无原因 / 删真实载荷的 `source` / 抹 `asOf` / 份额改 97% / 截短序列 / 空载荷塞日期);**必放 4 份真实 fixture**(`cake-full`、`cake-empty`、`leverage-full`(asOf 2026-10-20,250 日,温度 85.2)、`leverage-empty`)并断言判据非空。 apiProvenance:注入 `GhostDto` / 只带 `asOf` 的 `HalfPayload` / 豁免 +1 ⇒ 三种必红。 chartCache:+5 条 ⇒ `entries=160`、`evicted=5`、最早两条被淘汰、最后写入的仍在;写满后过 31 s 再写 ⇒ `entries=1` 且 `evicted=0`(**清过期与淘汰分开计数**);超限下刷新失败 ⇒ 仍 `fallback:true` 且该条不被自己挤掉。 曲线:7 条 / 51 标记 ⇒ 空态含真实条数并解释不截断;**恰好 6/50 ⇒ 放行**;无数据骨架不得出现"上限"字样与数字。 文档:README 改回「六个页签」/「七行」/删 ` / fupan` ⇒ 三条各自必红。 ### 未做(有意) - **R2 暂缓**(按裁决):测量口径已写好 —— 先数 `bottom.json` 里「同日 ≥2 通道命中」的比例。 - **救援面板的闸门接入**:`RescuePanel.tsx` 把因子表与「历史频率」内联在 JSX 里,没有整块纯模型可套 ⇒ 第二批(先抽模型)。 - **闸门运行时告警**:只做测试强制点(运行时阻塞按裁决拒绝)。 - R4 的 160 是**预算**(测到的是 14 键 / 45 标的 / p95 字节);**解析后堆占用未测**(需浏览器内存分析)。 ## v0.120.0 — 杠杆温度计:基准改为「2024 整段落在 50」;全周期保留 报告 `docs/AUDIT-LEVERAGE-HOST.md` §9。`npm run check` **1007 / 1007 × 2 时区** + selftest + build(73 片段全过);契约零改动。 ### 口径(最终版,第 4 版) - 样本:**全历史 4018 行**(2010-03-31 起)——**不删 2024 年之前的年份**。 - `P` = 比率的全历史扩张窗口分位(无前视);`P0` = **2024 各日 `P[i]` 的中位** = **55.0**;`温度 = clamp(P − P0 + 50, 0, 100)`。 - ⇒ **2024 整段温度中位恰为 50.0**(基准线成立);当前温度 **85.4**。 **四版对照(留档)** | 版本 | 定义 | 温度 | 问题 | | --- | --- | --- | --- | | 纯分位 | 全历史分位 | 90.4 | 2024 无基准,曲线长期高位 | | 锚定删数据(废弃) | 只用 2024 后样本 | 89.5 | **删掉了 2024 前的历史**(用户否定) | | A(同日被取代) | `P0` = 2024 中位比例的全历史分位 | 91.9 | 2024 段中位 56.5,基准线不成立 | | **B(现行)** | `P0` = 2024 各日分位的中位 | **85.4** | — | **实测(真实落盘 4018 行,末行 2026-10-19)** | 档 | 点位 | 有效点 | null | 极差 | | --- | --- | --- | --- | --- | | `range=250` | 250 | 250 | 0 | 83.2…89.3 | | `range=750` | 750 | 750 | 0 | **22.2…89.3** | | `range=all` | **4018** | 3769 | **249(仅头部)** | **22.2…95** | 2024 段:n=242,**温度中位 50.0**(均值 54.01、极差 22.2…84.2)。备选 `first-day` 模式 `P0` = 78.9(未采用)。 ### 修正一处估算 上一轮我给的 B 估算值 **83.9** 有误:把「2024 段**温度**中位 56.5」当成了 `P0`。B 方案要求 `P0` 是 2024 段**分位**的中位(55.0),两者在温度轴上差 1.5。**以实测 85.4 为准**。 ### 验证 `npm run check` **1007 / 1007 × 2 时区**,**连跑三次** + selftest + build(73 项)。 断言 ② 改为「**2024 各日温度的中位 = 50(±0.5)**」,用**单调上升构造**(2024 段 `P[i]` 恒 100 ⇒ `P0 = 100`、段中位 50)使「改回旧 `P0`」的注入必然得到 ≈100 ⇒ **必败**(判据非恒真)。 其余 16 条保持:全周期无 `null`(仅头部 249)、无前视、全历史 <250 ⇒ `null`、契约不变、每日 1 次上游、落盘锁与幂等、`range` 切片、首次回填。 顺带验证边界:**空库首次端到端返回 200 + 全 null**(`P0` 也 null ⇒ 温度 null),不是 0、不是 5xx。 ### 待处理 - `CHANGELOG` 中更早条目的温度示例仍是历史读数(如 `90.4`)——**历史条目不改写**;本条目以 85.4 为准。 ## v0.119.0 — 杠杆温度计:口径改「锚定 2024-01-01」+ 标题/字号/日期小字调整 报告 `docs/AUDIT-LEVERAGE-HOST.md` §9、`docs/AUDIT-LEVERAGE-UI.md` §16–§21。`npm run check` **1005 / 1005 × 2 时区** + selftest + build(73 片段全过);契约字段与形状**零改动**。 ### 一、口径变更:温度改为「锚定基准年」重标定 - **温度 = `d ≥ 2024-01-01` 样本内的扩张窗口分位**(0–100,1 位小数,**无前视**:第 i 天只用该日及以前的锚定样本)。 - 锚定起点抽常量 `LEVERAGE_ANCHOR = '2024-01-01'`(**单一来源**)。 - 门槛改为**锚定样本** < 250 ⇒ `null`(锚定日之前的行**不凑数**、也不给读数)。 - 契约不变:`thresholds {p80:80, p95:95}` 仍固定在**温度轴**;比例等价值改为锚定样本计算(P80/P95 = **2.597% / 2.750%**,改前全历史是 2.400% / 2.974%;UI 不写,已留档)。 **改前 / 改后(同一份真实落盘 4018 行,末行 2026-10-19,`RZYEZB = 2.6547%`)** | 读数 | 改前(全历史 4018) | 改后(锚定 674) | | --- | --- | --- | | `temperature` | 90.4 | **89.5** | | `range=250` | 极差 88.2…94.3 | 极差 **81…100** | | `range=750` | 极差 27.2…94.3 | **425** 点(前 325 点 `null`),极差 **46.3…100** | | `range=all` | 3769 点(前 249 `null`) | **425** 点(前 **3593** `null`),极差 **46.3…100** | ⇒ 曲线**有形状**了(46.3–100),但**「全历史」档的左侧基本都是 `null`**(2025-01 之前锚定样本不足)——这是锚定口径的必然结果,属预期,不是缺陷。 ### 二、界面(三项) 1. **删除温度卡的日期小字**:`ASOF_PREFIX`、`leverageAsOfText()`、`LeverageCard.note`、页面小字锚点**整条渲染链删除**;页面代码里「截至」**0 处**。`asOf` 仍留在契约里(**不渲染**)。 2. **卡片值字号降档**:温度 **28px**、其余 **26px**(下限 26 写进判据);通用 `SUMMARY_TYPE.value`(34px)**未动**,不影响蛋糕工具。 3. **工具名带口径**:`杠杆温度计` → **`杠杆温度计(锚定 2024)`**(**全角**括号、单一来源 `tools/leverage.ts` 的 `name`);目录卡与详情页标题一起变,页面**零处写死**;新名字**未命中任何锁定片段**(`build.mjs` 未动)。 ### 验证 `npm run check` **1005 / 1005 × 2 时区** + selftest + build(73 项);两侧各跑两轮全绿。 宿主新增/加强 4 条:**①锚定分位**(构造 200×3.0+99×1.0+今天 2.0 ⇒ 锚定 **33.3** vs 全历史 **75.0**;注入改回全历史 ⇒ 必败)**②无前视**(逐点与"只喂到该日重算"一致;锚定日之前是 `null` 不是 0)**③锚定样本 <250 ⇒ `null`**(恰 250 ⇒ 必须给数)**④锚定日之前的行不参与**(两份样本锚定窗口相同、前置历史 0.5 vs 9.9 ⇒ 温度**一字相同**;注入把 2010 起算进去 ⇒ 必败)**⑤契约不变**(注入加字段/改名/删字段全被抓)。 客户端新增 5 条(无「截至」、8 卡回归、28/26 字号、标题唯一且前置、无口径文字)+ R5 三条(目录名逐字、标题同源、标题唯一),**12 条真源码注入全 RED**,另加一条**负对照**(名字写进注释时判据应通过)。 ### 已知后果(留档) - **「全历史」档左侧大面积 `null`**(2025-01 前锚定样本不足 250)⇒ 该档可读区间与「1 年」接近;如需「全历史」看起来完整,只能改回全历史分位或放宽门槛。 - **界面上不再有任何「数据截至哪一天」的提示**(按用户要求删除)⇒ 两融是 **T+1**,读者可能把上一个交易日的数字当成今天。 - `CHANGELOG` 的 v0.118.0 条目里温度示例仍是改口径前的 `90.4`(历史条目,不改写)。 ## v0.118.0 — 杠杆温度计:温度移入卡片行 + 新增四个卡片(两行 × 四列) 报告 `docs/AUDIT-LEVERAGE-UI.md` §12–§15。`npm run check` **996 / 996 × 2 时区** + selftest + build(73 片段全过);只在 `src/client/**`。 ### 布局(两行 × 四列,共 8 张卡) | 行 | 卡片(左→右) | 值 | | --- | --- | --- | | 一 | **温度** | `90.4` + `/ 100` + 小字 `截至 2026-10-19`(`asOf === ''` ⇒ 小字只 `—`,不出现「截至」) | | 一 | 显性融资 | `2.65%` | | 一 | 杠杆率 | `2.69%` | | 一 | 当日融资净买入 | `-63.31 亿` | | 二 | **融资余额** | `2.45 万亿`(余额不带符号) | | 二 | **融券余额** | `291.68 亿` | | 二 | **流通市值** | `92.42 万亿` | | 二 | **窗口累计净买入** | 见下三档 | - 原大字区**整块删除、没留空位**;窗口切换控件挪到顶行右侧(与「回目录」同一行)。 - 布局:`.tw-lev-cards` = `repeat(4, minmax(0,1fr))`;`@media (max-width: 760px)` 折**两列**(不折成长列表);间距走 `TOOL_SPACE.tileGap`,边框/字号走既有令牌;**本页仍零动画**。 ### 窗口累计净买入(实测三档,0 缺失) | 窗口 | 交易日 | 累计 | 界面 | | --- | --- | --- | --- | | 1 年 | 250 | 40 258 761 307 元 | **+402.59 亿** | | **3 年(默认)** | 750 | 969 148 374 180 元 | **+9691.48 亿** | | 全历史 | 4018 | 2 429 784 225 797 元 | **+2.43 万亿** | - 口径 `windowNetSum()`:窗口内**任一 `null` ⇒ 整列 `—`**(不静默少算)。 - **交叉印证**:3 年档 `+9691.48 亿` 与参考图 2 的「窗口累计净买入 +9691.5 亿」一致 ⇒ 我方口径与参考实现对齐。 ### 验证 `npm run check` **996 / 996 × 2 时区** + selftest + build(73 项)。 新增 `src/client/leverageCards.test.ts` **7 条**(8 张卡 + 键顺序|温度卡首位与 `/ 100`/`截至`|第二行顺序|金额格式|累计随窗口变|`null` ⇒ `—`|卡片行无口径文字),每条带用例内注入;另在**真源码**上跑 **9 条注入 + 7 条 R2 回归注入,全部 RED**。 ### 未做(已记录) - 余额类不带符号(符号只留在净买入类)。 - 窄屏折两列用媒体查询(`auto-fit` 在宽屏会变 8 列,与「两行四列」冲突)。 - 触屏无 hover ⇒ 读数框不出现(与上一轮一致)。 ## v0.117.0 — 杠杆温度计:按图1 复核动态图形结构(六项) 报告 `docs/AUDIT-LEVERAGE-UI.md` §8–§11。`npm run check` **989 / 989 × 2 时区** + selftest + build(73 片段全过);**口径一个字未改**。 ### 六项结构性修正 1. **主图占屏比**:新增 `LEVERAGE_PLOT_HEIGHT = 'min(58vh, 520px)'`(绘图区与加载骨架同一契约)⇒ 900px 视口下有效 **520px**(下限 480);通用 `PLOT.heightCss`(280–380)未动。 2. **阈值右端胶囊**:`capsules` 新增 `threshold-capsule`,圆角=半高、右缘对齐绘图区、色走 `--tw-imp-2/3` 语义色阶(**不跟涨跌翻转**);数字从「左轴旁」移进胶囊 ⇒ 与 `100` 刻度至少隔 **200px**(修掉截图里 `100 95` 叠字)。 3. **悬停读数框**:`leverageReadout()` + `onMouseMove/onMouseLeave` + `leverageIndexAt()`;复用既有 `.tw-chart-tip tw-glass`(**零新 CSS**),三行 = 温度 / 显性融资比例 / 净买入,缺值 `—`;**无十字光标、无 title 说明**。此条**推翻上一轮「删浮框」**的决定,以图1 为准。 4. **X 轴多刻度**:`xTickCount()` 一年 7 / 三年 8 / 全历史 9;**像素级等分**(相邻间距差 ≤0.5px),标签取最近某天的 `YYYY-MM`(步长自然落成月/季度/年)。 5. **水平网格线**:恢复 `grid` 角色 —— `[0,25,50,75,100]` 五条、虚线、`--tw-border`、**不挂任何文字**。 6. **柱区独立刻度**:新增 `bar-max`/`bar-min`(`fmtNetYi` 右对齐),柱宽 `min(6px, max(0.6px, 内宽/天数*0.66))` 自适应;左内边距 66→74px。**顺带修一个真实缺陷**:`all` 窗口最小比例是 2010 年的 `0.00%`,右轴减余量后曾冒出 `-0.56%` 的假刻度 ⇒ 下限夹到 0。 ### 温度线极差实测(我上一条判断错了) | 窗口 | 交易日 | min | max | 极差 | | --- | --- | --- | --- | --- | | `250`(1 年) | 250 | 88.2 | 94.3 | **6.1 点** | | **`750`(3 年,默认)** | 750 | 27.2 | 94.3 | **67.1 点** | | `all` | 4018 | 27.2 | 100 | **72.8 点** | ⇒ **3 年窗口极差 67.1 点,不是直线**。**截图当时停在「1 年」档**,1 年窗口确实挤在 88–94(6.1 点)。我据此得出的「温度线几乎水平」判断**不成立**,口径无需改动。 留档(供将来的口径决策,本轮未改):最近 750 天里温度落在 **80–100 带 378 天(50.4%)**、40–80 有 296 天、20–40 只有 9 天 —— **扩张窗口分位对单调上行的比例序列会自然饱和到高位**。要与图1 的「全幅波动」对齐需改**滚动窗口分位**(会改口径)。 ### 验证 `npm run check` **989 / 989 × 2 时区** + selftest + build(73 项)。 新增 `src/client/leverageChart.test.ts` **6 条**(高度 / 胶囊 / 读数框 / X 刻度 / 网格 / 柱区小轴),每条带用例内注入;另在**真源码**上跑 **17 条注入,全部 RED**:写死 300px|阈值回左轴|删读数框或加回十字光标|只给首末刻度|删网格或改实线或给网格加文字|删柱区小轴或柱宽写死 18px|(回归)改副标题、多写「截至」、空日期也写「截至 」、金额丢符号、`?? 0`、多参数、加口径说明。 ### 未做(已记录) - 读数框只在 hover 出现(触屏不显示,未做点击固定)。 - 读数框**不含「与阈值的差」**(属解释性口径,按「不写口径」处理)。 - `min(58vh, 520px)` 在视口矮于 828px 时实高会低于 480(`min()` 语义;判据按 900px 参考视口)。 - X 轴标签取最近日(刻度像素等分优先,标签可偏半个步长)。 - 未改口径 / 宿主 / 共享层 / `build.mjs`;未新增依赖。 ## v0.116.0 — 新工具【杠杆温度计】:A股融资杠杆热度(全历史分位) 报告 `docs/AUDIT-LEVERAGE-HOST.md`、`docs/AUDIT-LEVERAGE-UI.md`。`npm run check` **983 / 983 × 2 时区** + selftest + build(73 片段全过);`build.mjs` 零改动。 ### 工具与口径(界面按用户要求不写口径) - 工具目录新增第二张卡:`杠杆温度计` / 副标题 `A股融资杠杆热度`;**不新增顶部页签**。 - **温度 = 显性融资比例(融资余额 ÷ 全市场流通市值)的全历史分位**(0–100,1 位小数,**扩张窗口、无前视**);历史自 **2010-03-31** 起,**4018 个交易日**。 - 数据源:`RPTA_RZRQ_LSHJ`(`RZYE/RQYE/RZJME/LTSZ/RZYEZB`);`pageSize` 被上游截到 800 ⇒ **回填 7 个请求**(一次性),之后**每自然日最多 1 次**。 - 首个实盘读数:温度 **90.4**、显性融资 **2.65%**、杠杆率 **2.69%**、当日融资净买入 **−63.31 亿**;落盘 **610 KB**(4018 行)。 ### 页面(九条规格) 1. 大字 `91.2` + `/ 100` + **唯一说明文字** `截至 YYYY-MM-DD`(`asOf` 为空串时**只显示 `—`**,不出现「截至」二字)。 2. 三张卡:显性融资、杠杆率、当日融资净买入(元→亿/万亿,2 位小数,显式正负号);**没有第四张**。 3. 主图**双轴**(左 0–100 温度、右比例 %),两条线;**阈值线 80 / 95 画在温度轴上、只标数字**。 4. 底部**净买入柱**(正红负蓝,走既有色 token);**缺值不画柱、不加任何说明**。 5. 窗口 `1 年 / 3 年 / 全历史`(`range=250/750/all`),默认 **3 年**;切换只改同一接口的参数,**切回命中本地缓存零请求**。 6. **删**水平网格线与图上浮框;保留图例、右轴刻度、柱图零轴、摘要卡边框。 7. 缺失一律 `—`(`DASH` 唯一来源);**数值路径没有一处 `?? 0`**。 8. 复用既有令牌与字号阶梯;**本页零动画**(不可能闪烁);骨架占同一份高度(无跳变)。 ### 后端契约 `GET /tradewatcher/leverage?range=250|750|all`(默认 750)→ **永远 200**,缺数据全 `null`;`range` 非法回落 750。 返回:`asOf` / `temperature` / `ratioPct` / `ratioWithShortPct` / `marginBalance` / `shortBalance` / `floatMv` / `netBuy` / `thresholds` / 等长的 `dates`、`temp`、`ratio`、`net`。 落盘:`/dsh-tradewatcher/leverage.json`,**`ReadGuard` 读 + `updateJsonLocked` 锁内读改写**,坏文件先隔离再重建;`README.md` 数据文件表已补该文件。 ### 路由归属 `/tradewatcher/leverage` 宣告到 **`tools`** 行(唯一消费者 = `ToolsPage`);四处镜像同步(`ROW_ROUTES` / `FUNCTION_OWNED_PATHS` + 测试副本 + 精确列表 / 设计文档 §2.1+§2.2+路由账 / `cordis.patch.yml` 注释)。 **实测路由账 30** = core 15 + calendar 1 + watchlist 3 + portfolio 2 + market 4 + cloudmap 0 + **tools 2** + fupan 3(旧 29)。 ### 验证 `npm run check` **983 / 983 × 2 时区** + selftest + build(73 项);两侧各跑两轮全绿。 新增 **22 条**用例(宿主 13 + 客户端 9),每条带反例注入;客户端 **10 条真实源码注入 ⇒ 10/10 必败**(含「阈值标签带文字」「加回浮框」「缺值 `?? 0`」「加第二条路由」「页面加一段口径」)。 宿主侧注入:近 N 天分位、样本写 0、每次打上游、绕锁/重复追加、`all` 只给 750 —— 全部必败。 ### 已知偏差(按用户要求不进 UI,仅存档) 「全历史分位」跨越两个制度断点:**2014-09-22**(深交所余额起含调出标的)、**2023 年**北交所并入(892 vs 4018 个交易日,体量占 **0.32%**)。 **方向已自我纠正**:早期读数偏低 ⇒ 历史分布被压低 ⇒ **今天的分位略偏高(温度略偏热)**,幅度**不可量化**(上游无分项)。 处置:**不校正**(校正=编数)、**不改成近 3 年**(会让阈值跨版本漂移:P95 2.974% → 2.747%)。 ### 未做(已记录) - 全历史档 4018 根柱**未做像素级降采样**(会把「当日净买入」悄悄变成「几天合计」=改语义);默认 750 档实测无卡顿。 - 切窗口瞬间保留上一档画面(不得闪烁与「绝不清屏」的取舍)。 - 未在真实浏览器核验(纪律不允许重启 `dsh web`);渲染靠纯模型判据 + 源码接线断言两层。 ## v0.115.0 — 复盘页文案精简(五段冗余注释移除) 报告 `docs/AUDIT-IMPL88.md`。`npm run check` **961 / 961 × 2 时区** + selftest + build(73 片段全过);`build.mjs` 未改动。 用户要求「简洁有效:可标记可不标记一律不标」。常驻移除五段:① 数据时刻后的源串 `·local:fupan.json+calendar:trading-calendar.json`;② 交易日口径长段(含外部日历文件路径与「要精确请新建该文件」);③ 月历空态说明句;④ 右栏空态说明句;⑤ 底部上限说明段。 保留:图例一行(`● 笔记 ▲ 交易笔数`);**「法定调休/临时休市可能被判成交易日」搬进图例悬停**(口径披露不丢,只不占版面);失败态、`dropped` 文案、截断提示照旧。 新增 `src/client/impl88.test.ts` 4 条断言(各带反例注入):五段特征串不得常驻|图例悬停仍含口径|空态仍有短句|失败态与 `dropped` 仍在。`build.mjs` 未改(先 grep:五段文案均未被锁定片段引用)。 ## v0.114.0 — 复盘联动持仓交易记录(只读):月历笔数 + 当日逐笔 + 跳转高亮 报告 `docs/AUDIT-IMPL86/87.md`。`npm run check` **957 / 957 × 2 时区** + selftest + build(73 片段全过);`build.mjs` 未改动。 ### 一、宿主:两条只读接口 | 接口 | 内容 | | --- | --- | | `GET /fupan/month?ym=` | `dates[]` 增 **`trades: number \| null`**(`0` = 那天真的没有;`null` = 流水读不到) | | `GET /fupan/trades?day=` | 逐笔(`ledgerId/ts/time/name/side/direction/qty/price/fee`)+ 汇总 + 当日盈亏 | - **口径唯一实现**:`fupanTrades.ts:116 tradesByDayOfMonth()`(哪些 verb 算交易 + 算在哪个北京日),逐日与月历都从它取 ⇒ **不是两套判断**。 - 计入 verb:**`buy / sell / adjust`**;`create / remove / rename` 不计。 - 归属日 = `ts` 的**北京日**(构造 23:30Z / 00:30Z 两条落在不同北京日,注入用宿主时区即红)。 - **当月盈亏只对「北京今天」给值**;历史日 ⇒ `null` + 原因(不拿今天的核算值冒充历史)。 - 单日 >500 笔 ⇒ `truncated {shown,total}`;缓存 **60 秒**(月历与逐日同一槽),**流水写入即失效**。 - **零上游请求**(只读共用行情槽 + 本地快照)。 ### 二、客户端:月历与当日详情 - 月历:笔记 `●`、交易 `▲n`,并存并排,**不用颜色**;一行图例。 - **图例去掉旧形状**:原 `●` 表示「交易日」与现在的「笔记」冲突 ⇒ 改为纯词 `交易日 / 休市 / 交易日未知`(文字 + 虚线 + 颜色仍能区分)。 - 当日详情:逐笔只读 + 汇总 **「买入 N 笔 · 卖出 M 笔」**(源码里 `净买入|净卖出|净额` **0 命中**)+ 当日盈亏数值(**无来源标注**,按「简洁有效」)。 - 点某笔 ⇒ 切到持仓页 → 交易流水 → **静态高亮**;状态**只在内存**(注入 `pushState`/`hash`/`localStorage`/`setPrefs` 四种全红)。 - **不接隐身档**(三份源码 `viewMode/incognito/moneyMask` 0 处,注入读开关即红)。 ### 三、你选择的两处例外(已在文档注明) 1. **读取失败显示「0 笔」且月历不画三角** ⇒ 视觉上无法区分「真的没交易」与「读取失败」。回包仍保留 `readFailed` + 原因(数据层不撒谎)。 2. **复盘页不接隐身档** ⇒ 全局隐身开启时本页仍明文。 ### 四、验证 `npm run check` **957 / 957 × 2 时区** + selftest + build(73 项);两侧各跑两轮。 新增 **21 条**用例(宿主 13 + 客户端 11)。**交叉一致性**:30 天逐日调 `/fupan/trades?day=` 与月历笔数逐一比对 ⇒ **0 差异**。 注入全部变红:月历不填 `trades`(30 条缺)|把 `create/remove/rename` 算进笔数(`09-08` 变 3、`09-09` 变 2)|两套口径不一致|读失败写 `0`|为零上游判据发一次 fetch|逐笔重算盈亏(得 −5,真值 95)|盈亏写 0|不截断|无缓存/不失效|失败缺 `readFailed`|失败画 `▲0`|加净额|加来源标注|缺值写 0|静默截断|跳转写 URL/偏好|高亮加动画|读遮蔽开关。 ### 五、已知限制 - **跳转高亮**只覆盖最近 **1000 条**流水(`/ledger` 无 id/日期筛选);更旧的流水「弹窗照开但不高亮」,**不伪造邻近行**。彻底解决需宿主加筛选参数。 - **历史某天没有当日盈亏**(见上)。 - 整月一键预取不需要了(宿主已带笔数)。 ## v0.113.0 — 移除护盘「跨日信号收件箱」 用户判断该功能无用。报告 `docs/AUDIT-IMPL85.md`。`npm run check` **933 / 933 × 2 时区** + selftest + build(73 片段全过);`build.mjs` 未改动。 ### 移除(7 文件,+30 / −949) | 文件 | 内容 | | --- | --- | | `src/client/rescueInbox.ts` | **整文件删除**(−317):合并、排序、已读映射、空态文案、日期工具、`INBOX_*` 常量 | | `src/client/RescuePanel.tsx`(−196) | 收件箱 import 与类型、按天取数 effect、已读写入、整个渲染区块(标题、列表上方声明、「全部已读」、「还有 N 天」、未存盘提示、列表、空态、`rescue-inbox-empty`) | | `src/shared/model.ts`(−10) | `PortPrefs.rescueInboxRead` 与默认值 | | `src/host/store.ts`(−30) | 装载归一、`applyPrefsPatch` 分支、上限与归一函数 | | `src/client/styles.ts`(−4) | `.tw-rescue-inbox*` 三个类 | | `src/client/impl81.test.ts`(+27/−286) | **逐条甄别**:删掉只服务收件箱的 5 条与相关夹具;**保留**覆盖广度 4 条,并把第八条收窄到 `.tw-rescue-cov` / `rescueCoverage.ts`,另补 2 条反例注入(强度不降) | | `src/host/impl80.test.ts`(+2/−106) | 删掉只服务收件箱的 2 条与 `prefsHarness()`;第 13 条去收件箱夹具并补 `prefs.theme` 判据防空跑;①–⑩ 一字未改 | ### 保留(各有守门判据) - **今日信号时间线**:结构判据 + **真实数据链路**(钉时间到 2026-10-19 10:00 北京 + 种 `rescue-log.json` ⇒ `snapshot().today.length === 2`、`hhmm = ['09:55','09:45']`、`today[0].level === 2`;空日志反例必败)。 - **`?day=` 历史回看**链路。 - **覆盖广度徽标与悬停**(`rescueCoverage.ts` 未被触碰)。 - 等级、连续评分近 20 日分位、当日峰、口径入口 `data-role: rescue-caliber`、5 分钟抽样。 - 阶段一附加分恒 0、权重 0.717、零新增请求(impl80 ①–⑩ 未动)。 ### 验证 `npm run check` **933 / 933 × 2 时区** + selftest + build(73 项);两轮 exit 0。 **先查锁定片段**:`grep "收件箱|最近一次信号|只列疑似|未存盘|rescueInbox" build.mjs` ⇒ **未命中**,锁定表无需改。 新增 6 条守卫(客户端 4 + 宿主 2),**真源码注入全部变红**: - ① 源码 + 产物**零残留**(8 条特征串,扫 134 个源文件 + `lib/index.js` + `lib/client.js`)⇒ 命中 **0**;注入 A(往 `RescuePanel` 塞回「收件箱」)、D(往 `store.ts` 塞回归一)、E(**只**往产物 `lib/client.js` 塞)全部变红。 - ② 今日时间线结构 + 数据双判据;删标题/容器/渲染循环 ⇒ 红;空日志 ⇒ `today=0` 且必败。 - ③ 覆盖广度:删 `rescue-coverage-badge` + `CoverageMark` ⇒ 红。 - ④ **旧 prefs 键宽容**:老 `prefs.json` 带着死键 ⇒ init 不抛错、`setPrefs` 正常;老客户端带死键请求 ⇒ 不 reject 且新值生效;反例 `theme:'bogus'` ⇒ 必须 rejects(证明「不抛错」有来源)。 ### 未做(已记录) - 老 `prefs.json` 里的死键**不清理**:`prefsFrom` 用 `{...DEFAULT_PREFS, ...loaded}`,死键留在明文但不被读取。清它需在运行时源码点名该键(与「源码零残留」冲突),通用未知键裁剪又有前向兼容风险 ⇒ 按「宽容忽略」保留。 - 历史审计 `AUDIT-IMPL80/81` 与旧 CHANGELOG **不改写**(仍描述收件箱),指向本文件。 ## v0.112.0 — 修「当日分时显示昨日」:当日链补新浪源 + 非当日必须标注 + 缓存键带交易日 + 门控窗口修正 报告 `docs/AUDIT-IMPL84.md`。`npm run check` **934 / 934 × 2 时区** + selftest + build(73 片段全过);`build.mjs` 未改动。 ### 根因:两条链的**源集合不一致**,不是上游旧 | 链 | 修前的源集合 | | --- | --- | | **当日分时** | 东财 `trends2` → 腾讯分钟(`web.ifzq.gtimg.cn`) | | 五日分时 | **新浪 5min**(`quotes.sina.cn`)→ 腾讯 5min(`proxy.finance.qq.com`) | 真机现场(09:56–09:59):东财三主机**全熔断**(`/health` `breaker.allOpen=true`,`fetch failed` ×12/22/24)+ 腾讯分钟域名被 **WAF 501** + **新浪可用** ⇒ 当日只剩昨日 LKG,五日拿到当日 ⇒ 同一时刻两个日期。 **复现取证**:`ndays=1` ⇒ 242 点全 `2026-10-19`,`staleAt=2026-10-19 17:38:11`;同刻 `ndays=5` ⇒ 末点 `2026-10-20 10:00`。隔离环境可重放同一形状。 **附带两条同源缺陷**:内存缓存键 `trend::` **不含交易日**(跨日可命中);定稿门控从 **09:25** 起算 ⇒ 09:15–09:25 集合竞价把昨天判成「已定稿」并复用 300 秒。 ### 修法 1. **补源(同源同序)**:`em.ts:1653` 新增 `fetchSinaMinuteBars`(唯一取数/解析,显式 `+08:00`);`:1692` `sinaMinuteTrend`(新浪 1min → 当日分时,成交量股→手,`avg` 恒 `null` 不凑);`:1749` `minuteFallbackSources()` = 新浪 → 腾讯;`:1432` 备用段改为**按源表逐个试并记录每源失败原因**(旧:只问腾讯一家);`fetchSina5Min` 复用同一取数(顺带修掉无偏移 `Date.parse` 在 `TZ=UTC` 偏 8 小时)。 2. **诚实性兜底**:`em.ts:1463` `annotate()` —— 非当日 ⇒ `sessionDay` + `nonSession{day, reason, detail}`;LKG ⇒ `stale/asOf`。`model.ts:268/307/312/321` 新增 `TrendNonSession` / `asOf` / `stale`,`source` 扩为可逐条归因。`chartNote.ts:125` 日期改**全年份**、`:135` `nonSessionOf()`、`:160` **非当日提到脚注第一句 + 原因**;`QuoteDrawer.tsx:437` 图头改口(单日 `10-19 09:30 ~ 15:00`、多日「末行为 2026-10-19」)。 3. **缓存键带交易日**:`trendCacheKey(secid, ndays, now)` = `trend:::`,缓存/定稿/在飞合并全改用它。 4. **门控窗口**:`time.ts:183` 新增 `inMarketWindow`(工作日 **09:15–15:05**);`isSettledOffline` 改用它 ⇒ **交易窗口内永不判定稿**(顺带修好 quotes/kline 的同类门控);非交易时段行为不变。 ### 改前/改后(同标的、同一时刻,隔离) | 场景 | 改前 | 改后 | | --- | --- | --- | | 东财熔断 + 腾讯不可达 | 1 日 **10-19 09:30**,`staleAt` 10-19 17:38,**无** `source`/`nonSession` | 1 日 **10-20 10:07**,`source=sina-minute`,**脚注为空** | | 三源全灭 | 10-19,有 `sessionDay` **但无原因** | 10-19 + `nonSession{day, reason, detail}` + `stale=true`,脚注首句 **`· 非当日数据(2026-10-19):当日分时未取到,显示本地兜底快照`** | ### 验证 `npm run check` **934 / 934 × 2 时区** + selftest + build(73 项);新增 14 条(宿主 10 + 客户端 2 + 脚注 2)。 注入全部变红:缓存键去日期(跨日命中 10-19)|备用链只剩腾讯(1 日 = 10-19)|摘掉 `nonSession`/`sessionDay`|旧 09:25 门控(`settled=true`)|UI 恒 `slice(11)` + 「末行为今日」|脚注退回 `staleAt` 优先。 **唯一改动的既有断言**是收紧:`非当日数据(10-01)` → `(2026-10-01)`(改全年份)。 ### ⚠ 现场是间歇性的 10:04 腾讯偶发通过后宿主又恢复成 10-20(10:11 复读 1 日 = 10-20 10:05)。**结构性缺陷与诚实性缺口由测试与隔离复现钉住,不依赖现场运气**;真机复验需重启后看 `ndays=1` 的 `source` 是否为 `sina-minute` 或 `eastmoney-trends2`、末点为当日。 ### 未做(已记录) - 腾讯分钟换域名到 `proxy.finance.qq.com`(实测 200 当日;但护盘采样也用该域名,属另一条链行为变更)。 - `mini.ts` 的 90 秒缩略图缓存键仍无日期(无文字承载面)。 - `parseTrendRow` 的 `t` 无显式 `+08:00`(界面全走 `label`,未影响本问题)。 ## v0.111.0 — 新增第 8 行 `fupan`(复盘):月历 + 当日笔记 + 钉到未来日期 概念参考第三方插件 `dsh-fupan`(MIT),**自行实现**、不引其代码/样式。报告 `docs/AUDIT-IMPL82/83.md`。`npm run check` **920 / 920 × 2 时区** + selftest + build(73 片段全过);`build.mjs` 未改动。 ### 为什么不直接吸纳 该仓库 v0.1.0、单次提交、只有冒烟测试;它读本地 Markdown 时**没有「读失败 ≠ 没有」这层**(读取失败会被显示成「没有复盘」);构建管线独立,并进来会**绕过我们的 73 条锁定片段**拦截。⇒ 采纳概念,重写。 ### 一、第 8 行注册(五处一致) | 落点 | 位置 | | --- | --- | | 真表(唯一事实源) | `src/index.ts:80`:`fupan: ['/tradewatcher/fupan/month', '/tradewatcher/fupan/note']`;`ROW_TOOLS.fupan = []` | | patch 行注释 | `cordis.patch.yml:16` | | 文档 §2.1 | `DESIGN-SUBPLUGINS.md:73`;路由账 26 → **28** | | 测试副本 | `src/index.test.ts:346` | | manifest 行 | `cordis.patch.yml:81` | 连带:`EXPECTED_ROWS`、`FUNCTION_OWNED_PATHS`(并集 28)、`applies` 8 → **9**、`locale/rows/fupan/`、两条 exports、README 数据文件表。 **发现一个真坑**:patch 注释解析器会把 `` `/a|b` `` 里的 `b` 补成 `/tradewatcher/b` ⇒ **嵌套路径会被补错**;处置为两条路由写成**两个反引号 token**,并做成注入判据。 ### 二、存储(`/dsh-tradewatcher/fupan.json`) - 写入**只走文件锁**(锁内读最新 → 改 → 裁剪 → 原子写回);读取**只走读守卫**:**仅 ENOENT 算「本来没有」**,EACCES/EIO/EISDIR ⇒ 拒绝写回。 - 坏 JSON **隔离保留原字节**后从空开始。 - 有界:单条 **≤4000 字(超限拒绝、不截断)**、≤2000 条、≤512 KB;丢弃按「最久未更新」,**置顶的最后才丢**,逐条 `dropped` 留痕。 - 交易日**只由既有交易日历判定**;覆盖不足时 `calendarWhy` 如实披露(含「外部日历仅当月生效」这条既有怪癖)。 - **零新增上游请求**(含写入路径,实测 `fetch` 调用 0 次)。 ### 三、客户端:第 7 个页签「复盘」 - 页签在「工具」之后(`index.tsx:473`),`row:'fupan'` ⇒ **行停用时页签消失**;保活(`['calendar','tools','fupan']`),理由是**本页零轮询**,收益是月份/选中日/草稿不丢。 - 月历:周一起头、整周、裁空尾;**交易日 · 休市 · 未知三态**(未知用虚线 + `?`,并有图例);条数与钉住标记。 - 当日笔记:增 / 改 / 删 / **钉到未来日期**;在目标日标「**钉自 YYYY-MM-DD**」,在自己那天标「已钉到 …」。 - 失败与超限**都显示宿主原因**(实测文案:`新增失败(本次未写入,界面保留原内容)—— 笔记正文 4100 字 > 上限 4000 字…`);`dropped` 显示「宿主丢弃了 N 条最旧笔记(超出上限)」。 - 路由不可用(行未启用 404)⇒ **原因 + 重试**,状态机保证 `failed` **永远压过** `empty`(不把「取不到」显示成「没有笔记」)。 - 键盘:`role=grid/row/columnheader/gridcell` + roving tabindex + 方向键 + 回车/空格。 ### 验证 `npm run check` **920 / 920 × 2 时区** + selftest + build(73 项);两侧各连跑两轮全绿。 新增 **42 条**用例(宿主 30 + 客户端 12)。**注入(真实源码/副本)全部变红**:行注册只改一处|功能行写成裸包名|路由塞进 core/其它行|直接 `writeFile`、去掉 `assertWritable`|读失败当成空|`dropped` 清空|只信周末(10-01 会变交易日)|`calendarWhy` 写 `null/''/0/空白`|为接口发 fetch|删页签 / 页签插到「工具」前|默认全交易日|`catch(()=>{})` 静默|去掉「钉自」|静默丢 `dropped`|空列表冒充|只留 `onClick`。 并发与丢更新:两实例各写一条 ⇒ 磁盘**两条都在**;锁被活进程占住 ⇒ 150 毫秒有界失败且**文件一字节未变**。 产物核查:`复盘(月历 + 当日笔记)`、`钉自 `、`本月暂无笔记`、`交易日未知`、`宿主丢弃了 `、`fupan/month` 等字符串在 `lib/client.js` 全部命中。 ### 未做(已记录) - **`POST /fupan/import` 本批不注册**:客户端本批无导入 UI ⇒ 没有唯一消费点,声明进 `ROW_ROUTES` 会让归属断言红,不声明则 core 兜底注册、导致「复盘行关不掉」。骨架与安全口径(只读、读失败给原因、默认不允许任何目录)已就位,下次三处同批接上。 - 无真实浏览器渲染验证(本机不重启 `dsh web`、`node_modules` 无 react/react-dom)⇒ 首装后需人工过三态配色、窄屏堆叠、隐身档。 - 无草稿持久化(刷新即丢);无批量操作(整月导出 / 清空某日)。 - `at` 只有最后写入时刻 ⇒ 「最旧」= 最久未更新(不是最早创建)。 - `dropped` 只显示条数与原因,不逐条摊内部 id。 - 第 8 行的**真实装载**(`/rows` 含 `fupan`、关行 404)待首装验证。 ## v0.110.0 — 护盘「覆盖广度」(阶段一:只记录 + 展示)+ 跨日信号收件箱 报告 `docs/AUDIT-IMPL80/81.md`。`npm run check` **890 / 890 × 2 时区** + selftest + build(73 片段全过);`build.mjs` 未改动。 ### 一、覆盖广度:契约 7 键 + 原因 | 字段 | 含义 | 用途 | | --- | --- | --- | | `nEtf` | 触发通道数(量能 **z ≥ 2**,复用 `sameSlotZ`,**不另造阈值**) | 进评分 | | `nFamily` / `families[]` | 命中族数 / 族名(**大盘**=沪深300+上证50;**中小盘**=中证500+中证1000;**成长**=科创50+创业板指) | 族数进评分 | | `confirm1` | 沪深300 与 ETF 方向一致性:`strong` / `same` / `mixed` | 进评分(4 / 2 / 0) | | `nEtfWithData` | 有读数的通道数(显示分母) | 只展示 | | `confirm1N` | 与沪深300 同向的通道数 | 只展示 | | `confirm2` | **30 年国债 ETF 的反向读数**(`−(涨跌幅%)`);本次行情批次里没有 ⇒ `null` + 原因(**不写 0、不单独取数**) | 只展示 | | `coverageWhy[]` | 同级缺失原因 | 展示 | 落点:快照 `rescue.ts:2350`、事件 `:2283`、出分日志 `:2621`(与现有 60 天/240 条同寿命,无新存储)、LKG `:180`;族映射唯一来源 `model.ts:1626`。 ### 二、阶段一 = 附加分穷举恒 0 - 开关 `rescueCoverageEnabled` **默认 false**;快照回显 `coverageEnabled`;summary 写「附加 0(开关关闭:只记录不评分)」。 - 阶段二函数已实现但**不启用**:阶梯 **0/3/6** + 门槛 `nEtf ≥ 3 ∧ nFamily ≥ 2`;`confirm1` **2/4**;上限 **10**;**只做加法**进 `raw`,**四因子权重 0.717 逐项未动、无除法**。 - 端到端打桩:阶段一 `bonus = 0`;阶段二 `= 10` 时 `rawScore` 差值**恰好 10**,四因子 `id/weight/score` 逐项相同。 ### 三、客户端展示 - **常驻短标记** `${nFamily} 族 · ${confirm1N}/${nEtfWithData}`(例 `3 族 · 5/6`),紧随等级徽标;`nEtfWithData = 0` 时那半段显示 `—`;**整块缺失 ⇒ `—` + 原因**(「宿主未提供覆盖广度(阶段一字段)」),**无一处 0 顶替**。 - 悬停/读屏含:三族归属、`z ≥ 2` 与公式、两个确认档、以及「**覆盖广度当前未计入评分(阶段一)**」(开关打开则改口为「已开启附加分(阶段二)」,不说假话)。 - 同一实现用于主线、今日时间线、历史回看每条。 ### 四、跨日信号收件箱 - 保留 **30 天**;**只列等级 ≥ 2**(「资金异动」基率 100% ⇒ 不入箱,等级 0 也不入),列表上方有可见声明。 - **排序:等级优先,同级时间倒序**;**合并:固定 5 分钟窗(闭区间)+ 按(天, 等级)分组,得分取窗口内最大**。 - 已读存 `prefs.rescueInboxRead`(乐观更新 + 回读校验;宿主没接受 ⇒ 如实写「已读状态本次未存盘(宿主未接受该偏好)」,**不假装成功**)。 - **空态**:「最近一次信号在 X 天前(日期) · 本期(30 天):N 天有信号 / M 条(最高 …)」;天数不足则说「自采样起只有 N 天日志」;**历史还在路上时显示「取数中(还剩 N 天)」而不是空态**(避免假否定)。 - **不做**:推送、红点、闪烁、新依赖、新路由(复用现成 `?day=`,只取 `maxLevel ≥ 2` 的天)。 ### 五、顺带完成 prefs 白名单 `PortPrefs.rescueInboxRead`(`model.ts:726`)+ `applyPrefsPatch` 分支(`store.ts:1231`)+ 归一与上限 **2000**(`:1359-1360`)。实测:写入后 GET `/prefs` 原样读回;2500 键裁到 2000(按值最旧丢);非对象不 500。 ### 验证 `npm run check` **890 / 890 × 2 时区** + selftest + build(73 项);客户端连跑 **8 轮**全绿,最终两轮源码逐文件一致。 新增 **22 条**用例(宿主 13 + 客户端 9);**9 个真实源码注入全部变红**:开关 false 也加分|四因子归一化(0.333/0.273/0.181/0.213)|阶梯错一档|`confirm1N` 混进 `raw`|上限抬到 12|构造器里插 `fetch`|科创50 划进大盘|`confirm2` 缺数据写 0|附加分不进 `raw`。 客户端 7 组注入必败:整段口径塞回常驻|缺失写 0|把「资金异动」列入|纯时间倒序|取最新分(61 而非 72)|已读写进 `rescue-log`|空态只写「无信号」。 合并/排序实测(固定夹具):`L2 14:30–14:35 ×4 score=72`(最新 61 ⇒ 取最大生效;中间夹 L3 不切断窗口)、`L2 14:36 ×1 66`、`L3 14:31 ×1 90` ⇒ 排序 `L3 14:31 90 → L2 14:36 66 → L2 14:30 ×4 72`;全读完 `unread=0 / total=3`(**统计不因已读减少**)。 ### 未做(已记录) - `confirm2` 默认 `null`:要读数需把 30 年国债 ETF 加进「自定义通道」(零新增请求的直接代价)。 - **阶段二未打开**;开关的界面入口与「打开后等级分布差异」的真实日志回放留下一批。 - 开关打开且附加分满 10 时,连续评分理论上限 113.9 被既有 clamp 收在 100 —— 是否改分母属口径决策。 - `tradewatcher_rescue` **工具回包未加 coverage**(本批只做面板与落盘)。 - 推送/红点/闪烁按要求全部不做;阶段二开关未加客户端 UI(悬停只回显状态)。 ## v0.109.0 — 护盘面板:三段常驻口径披露收进「口径与边界」悬停入口(事实不删) 报告 `docs/AUDIT-IMPL79.md`。`npm run check` **868 / 868 × 2 时区** + selftest + build(73 片段全过);`build.mjs` 未改动。 ### 用户要求与处置 用户要求删除「底部位置 / 形态 / 触发后对照」表下方的**三段常驻文字**。这三段是**我们自己的红线披露**(口径与统计基础、Wilson 区间怎么读、「不是胜率/概率/预测」、形态不参与校准、「位置低 ≠ 见底」),而用户的既有要求是「诚实性默认可见、不得消失」。 ⇒ **处置:常驻删、事实留**。常驻区只留**一行** `口径与边界(悬停查看)`(14 字,`tabIndex:0` + `role="note"` + `data-role="rescue-caliber"` + `cursor:help`);三段原文进 `title` 与 `aria-label`,**16 条事实逐条仍在**。 ### 三段的新落点 | 段 | 落点 | | --- | --- | | ① 口径句(样本 163 / 基线 1530 / 收益中位数 0.34% / 回撤 −2.45%) | **宿主生成**的 `bottom.view.model`(`host/rescue.ts:2543`,本批未改宿主)→ 由 `caliberHoverTextOf(model)` 原样拼进入口 | | ② 口径说明(「历史同类情形的出现频率」、Wilson 区间、「不是胜率、概率或预测」) | `rescueView.ts:74` 的 `RESCUE_CALIBER_DISCLOSURE`(**唯一来源**) | | ③ 形态边界(形态不参与概率校准、「位置低 ≠ 见底」) | 同上(与 ② 同一常量) | 改动点:`rescueView.ts:71-96`、`RescuePanel.tsx:382`(计算唯一来源)、`:880-890`(一行入口替换原来的两个 `tw-hint` div)。 注:「历史频率」行里的 `期间最大回撤中位数 -2.45%` 是**数据单元格**,不属于被判读的三段,保留常驻。 ### 锁定字面量(两条,`build.mjs` 未动) - `历史同类情形的出现频率` → `RESCUE_CALIBER_DISCLOSURE` → `caliberEntryOf()` → 入口的 `title` 与 `aria-label`(唯一来源)。 - `95% 区间` → 同一常量 **+** 常驻「历史频率」行的 CI 文本(未动)⇒ 双保险。 **注入(真实构建,副本)**:删悬停里两条锁定字面量 ⇒ 构建失败(`缺少 UI 片段 —— 历史同类情形的出现频率`;`95% 区间` 仍由常驻 CI 兜住);再把常驻 CI 文本里的 `95% 区间` 也删 ⇒ 两条都失败。 ### 一处措辞调整(不是删事实) `**不是**胜率、概率或预测` → `不是胜率、概率或预测`(去 Markdown 星号):文案挪进 `.ts` 后触发仓库既有断言「用户可见字符串不许出现 `**`」(`host/markdownScan.test.ts`;星号在 `title` 里会原样显示)。语义不变;同时**必要同步** `rescueVisual.test.ts ②`:诚实边界/禁买卖结论/禁带数字胜率的判据改为**同时扫 `RescuePanel.tsx` + `rescueView.ts`**(文案搬到哪、判据扫到哪)。 ### 验证 `npm run check` **868 / 868 × 2 时区** + selftest + build(73 项);两轮 `PIPE_STATUS=0`,两轮前后源码 + `build.mjs` 哈希一致(`392db086…`)。 新增 `impl79.test.ts` **5 条**,每条带必败注入:① 把披露拼回常驻 ⇒ 抛错 ②(a) 只留宿主口径句、删两段红线 ⇒ 缺 10 条事实;(b) 只删「Wilson 区间」半句 ⇒ 缺该条 ③ model 为空 ⇒ **只给两段且不出现**「期间最大回撤中位数」(**不补编假口径**)④ 从常量删锁定字面量 ⇒ 抛错 ⑤ 去掉 `tabIndex`/`aria-label`(不可聚焦/读不出)⇒ 抛错。 ### 未做(已记录) - 宿主 `model` 句的措辞/精简(`host/rescue.ts:2543`,本批不动宿主)。 - 悬停入口的「点击展开」(触屏场景会重新占版面,**需用户确认后再做**)。 - 若用户要连悬停也彻底删除 ⇒ 等于移除「位置低 ≠ 见底」与「不是胜率」两条边界声明,需显式确认。 ## v0.108.0 — 队列收尾:LKG 装载 →5 ms、载荷 −80.7%、客户端 P2/P3 全处置 报告 `docs/AUDIT-IMPL77/78.md`。`npm run check` **863 / 863 × 2 时区** + selftest + build(73 片段全过)。 ### 一、LKG 装载 60–65 ms → **0.7–5.4 ms** 清单升 **v4**:最近 **8 条**热条内联(受 512 KB 预算约束),其余 **103 条冷条懒加载 + 后台预热**,读完补写一次清单。`lastGoodTrend` 改异步;写盘串行化(顺手修掉一次 `*.tmp` 竞态)。 契约全保住:单次写 ≤512 KB、`ensureAll()` 后**逐字节等价**、保留上限与 `dropped` 守恒不变。 **取舍**:清单从 7 KB 涨到 **120 KB**(换启动 −55 ms);要更小可把热条降到 4 条(≈60 KB)。 ### 二、载荷二级瘦身 −80.7% - `/kline`:`pct === null` 不再上线(`0` 是真实涨跌幅,照发)⇒ 24 333 → **21 693 B**。 - `/trend`:单日序列逐点 `label` 只留 `HH:mm`、日期上移 `labelDay` ⇒ 24 022 → **21 405 B**。 - 两项字段合计 **6 512 → 1 255 B(−80.7%)**(目标 ≥ −30%)。 - 读侧 `chartWire` 三件套在 `chartCache` 收口统一归一:**双编码兼容、缺省是 `null` 不是 0**。 ### 三、老备份 2.34 MB:校验通过才清理 写分片后**逐条读回解码 + 与迁移前指纹比对**,`verifiedShards === entries` 才删;清单写 `migratedFrom {v, ts, file, bytes, entries, sha256, verifiedShards, verified, reason, removedAt}`。真实副本实测 **111/111 校验后清理**;你 `~/.dsh` 那份留待**下次宿主重启**由这段代码处理(未手工写用户目录)。 ### 四、客户端 P2 十条 + P3 七条全部处置 | # | 内容 | | --- | --- | | P2-1 | **接口加 15 秒超时** + `finally` 无条件复位 `inFlight` ⇒ 一次挂起不再让行情轮询停摆(用「只监听 abort、永不 resolve」的桩真跑验证) | | P2-2 | 旧代响应按**代号**丢弃(`requestGen`) | | P2-3 | 视图切换失败**不再静默**,回滚并给原因 | | P2-4 | `const visible = true` 换成真实可见性(`IntersectionObserver` + 文档隐藏),面板藏起不再每 10 秒取数 | | P2-5 | 浮层加 `role=dialog`/`aria-modal` + 焦点归还 + Tab 陷阱;**Esc 只关栈顶**(收起的下拉不占栈位) | | P2-6 | 点击型元素全部键盘可达(`role=button` + Enter/Space) | | P2-7 | 六页签**先保活两页**(日历 + 工具),只挂「当前行被启用」的页 | | P2-8 | K 线 mousemove 走 **rAF 合并**(同帧 3 次 → 落 1 次,用假调度器断言) | | P2-9 | 动画属性 `width` → **`transform`**;断言扩到 transition(布局属性白名单) | | P2-10 | 截断数抽 `MAX_QUOTE_IDS`;**漂移断言**盯住宿主字面量与徽标 | | P3 | `--tw-warn` **补三档定义**(原来全项目从未定义)+ 11 处琥珀统一;删死字段;删编造的「+2%/日」表头;做T 截断改为明示;涨跌分布 `?? 0` 改 `—` + 原因、配色按档名;工具目录补 `role=tab`/`aria-selected` | 新增 **17 条**断言(4 条是**真跑**的行为断言:超时桩、假 rAF 调度器、层级栈、纯函数组合),每条配注入。 ### 验证 `npm run check` **863 / 863 × 2 时区** + selftest + build(73 项);两轮源码指纹一致;`--test-concurrency=8` 连跑三次无抖动。 注入(副本内注入 → 跑 → 恢复):懒加载漏条、N 调大去预算、退回旧编码、失败也删、先删再校验、超时失效、`stale` 恒 false、恒可见、`isTop` 恒 true、不合并 rAF、`transition:width`、删深色档 —— **全部按预期变红**。 ### 未做(已记录) - **P2-10 真·单一来源**:宿主 `routes.ts` 改 import 共享常量 + 锁片段 `已截断 160` → `已截断 `(需动 `build.mjs`,归 Lead)。当前以漂移断言防分叉。 - **六页签全量保活**:其余四页有高频取数,需先接 `active` 门控;本轮只两页验证。 - 流水/详情请求无序号(同类,建议下批用 `requestGen` 范式)。 - 抽屉「焦点归还触发元素」需上游传 ref。 - `shared/model.ts:454` 的「高(红)」注释(文件正被并行批改)。 ## v0.107.0 — 队列收口:年K/筹码量红线、客户端 8 条 P1、LKG 分片(−99.7%)、护盘快照 −42% 报告 `docs/AUDIT-IMPL74/75/76.md`。`npm run check` **840 / 840 × 2 时区** + selftest + build(73 片段全过)。 ### 一、A股年K 三处口径 + 筹码尾部量(红线) | 项 | 旧 | 新 | | --- | --- | --- | | 缺月量 | `?? 0` 相加 ⇒ 缺一个月仍给 **400** | **该年 `vol`/`amount` = `null`** | | 年K `date` | 每月覆盖 ⇒ 年末月 ⇒ 「当年未收盘」判定**永远为假** | **年内首个月K** + 年K 专用 `barOpen` | | 首根 `pct` | 拿年内开盘价当基数(**冒充同比、未披露**) | `pct = null` + `KlineData.pctNote` 口径句 | | `chips.lastBar` | 缺失与真 0 **不可区分**(都是 0) | 缺失 ⇒ `null` + 原因;真 0 ⇒ `0` + `zeroVolume: true` | 顺手修一处**类型谎话**:`em.lastBarOfKline` 声明 `vol: number` 而 `DayBar.vol` 可为 `null`(`strict: false` 掩盖),后果正是「缺失被读成零成交」。 **注入**:I1–I5 逐项改回旧写法各让对应断言立刻变红;复原 8/8 绿。 ### 二、客户端 8 条 P1 ① 悬浮图失败**给原因**(「重试无用」与「稍后自动重试」分开)② 搜索候选**键盘可选**(↑↓ Home/End Enter Esc + `combobox`/`listbox`/`option`/`aria-activedescendant`,行改 `onClick`)③ 买卖**不再用涨跌色、不随 `redUp` 翻转** ④ 三句「本插件未集成」改成事实(哪个源没取到 + 稍后重试 + 不是 0)⑤ 两处 `toTimeString()` 改北京时间(R5 回归)⑥ 历史回看失败**不再伪装「无信号」** ⑦ 重要性/护盘等级新增 **7 个独立语义色**,隐身档灰化 ⑧ 缩略图改称「**分时首笔/高/低**」并注明非交易所口径。 新增 13 条断言 + **16 处注入全部被抓住**。 ### 三、`trends-lkg` 分片:单次写 2.34 MB → 7 KB(−99.7%) - `trends-lkg.json` 变**清单 v3**;每条标的单独 `trends-lkg.d/.json`(只含 trend,`at`/`ts` 不进分片 ⇒ 只有 trend 变才重写)。 - **清单是提交点**(先分片后清单,崩溃下次重迁移、幂等);老单文件(真实 2.34 MB / 111 条)首次装载拆成 111 分片,原文件改名备份。 - 保留:`maxEntries 400 / maxBytes 4 MB / maxShardWriteBytes 512 KB`,超限按最旧丢并**逐条留痕 + 计数**;**守恒判据**(存活 + 丢弃 == 总数)。口径与客户端**共用同一常量**并进悬停。 | 指标 | 改前 | 改后 | | --- | --- | --- | | 单次写(一条标的刷新) | 2 342 741 B | **7 154 B** | | 单次写(trend 真变) | 2 342 741 B | **10 657 B** | | 单次写最大值 | 2.34 MB | **93 369 B**(≤512 KB) | | 装载 | 单文件 parse | 迁移 192.7 ms / 热 **60–65 ms** | ### 四、护盘快照瘦身 −42.4%(只裁空字段) `amount` 恒在(`null` = 该分钟无读数,不写 0)、空 `hhmm` 与 `null price` 不发;落盘与内存快照仍 dense(可回滚)。客户端归一函数同时吃新旧两种编码(缺省还原 `''`/`null`,**不把缺失当 0**)。 | 指标 | 改前 | 改后 | | --- | --- | --- | | `/rescue` 回包 | 78 676 B | **45 280 B(−42.4%)** | | `etfSeries` | 69 789 B | **36 839 B(−47.2%)** | | `/rescue` p50 / CPU | 0.74 / 0.95 ms | **0.56 / 0.74 ms** | ⚠ 实施期发现:`etfSeries` **目前没有任何消费方**(69.8 KB 此前只发不读)。按约定做「语义不变瘦身 + 双编码读法」,**没有删字段**。 ### 五、顺手修掉一处偶发假失败 `src/index.test.ts`「apply 幂等」用固定 `setTimeout(50ms)` 冒充 `store.init()` 已落定 ⇒ 8 并发满负载下偶现 `starts=0`(**这正是长期未定位的那类抖动**)。改成**有界等条件**,断言不变、真没落定时照样失败。 ### 验证 `npm run check` **840 / 840 × 2 时区** + selftest + build(73 项)。 新增 20 例(`impl74` 8、`impl75` 13、`trendLkgShards` 4、`rescueWire` 3,另有既有用例改写/加强)。 **注入(副本内注入 → 跑 → 恢复)**:年K/筹码 I1–I5、客户端 16 处、LKG A–C(退回整份 blob / 删分片 / 静默截断)、快照 D–F(dense 回包 / 只支持新编码 / amount 取整)—— 全部按预期变红。 **锁定片段已脱离注释**:`无行情源 N` 现在落在真实渲染字符串(`NO_SOURCE_BADGE_HINT`)上;把注释里的 `N` 去掉后构建仍 73 项通过 ⇒ `build.mjs` 无需改动。 ### 未做(已记录) - LKG 装载 60–65 ms 可压到约 5 ms(最近 N 条进清单、其余懒加载)—— 收益是一次性 50 ms。 - `/kline pct:null`(12%)与 `trend label`(25%)瘦身(涉及客户端解析)。 - 分片清理只覆盖「被保留策略淘汰的分片」;老备份 `trends-lkg.v2.json.bak`(2.3 MB)留待下一版删。 - `lastBar.partial` 恒 `false`(既有问题,全仓无消费者,本批只加注释)。 - 年K 的 `date` 只能是「年内首个月K」(上游月K 日期本身是月末)。 - 客户端其余 P2/P3 未做。 ## v0.106.0 — 性能批:门禁 −43%、筹码回包 −49~66%、归档写入 −99.9%;修 `trends-lkg` 从未更新 报告 `docs/AUDIT-IMPL73.md`、`docs/REVIEW-9-PERF.md`。`npm run check` **812 / 812 × 2 时区** + selftest + build(73 片段全过)。 ### 一、门禁耗时 58.35 s → **33.0 s(−43%)** `package.json:41` 只加两个参数:`NODE_COMPILE_CACHE=node_modules/.cache/node-compile`(放在已忽略目录,不新增 gitignore)+ `--test-concurrency=8`。 - 子进程启动 **167 → 71 ms**;单时区用户态 CPU **53.4 → 25.5 s**。 - 并发 8 实测验证到 **n=16 轮全绿**(6 次 `npm test` × 2 时区 + 4 次 `npm run check` × 2 时区);回退 4 只损失约 2 s/时区。 - 追加:`repoHygiene ④` 由「1264 导出 × 121 文件 = 14.7 万次全文正则」改成一遍分词索引 ⇒ **1688 → 68 ms(−1.62 s/时区)**,口径等价性逐条核对 1264 个名字 **0 差异**(含模板字符串 `$` 边界)。 | 指标 | 改前 | 改后 | | --- | --- | --- | | `npm test`(2 时区) | 58.35 s / user 105.0 s | **32.64–34.20 s(6 次,均值 33.0)/ user 51.4–53.4 s** | | 子进程启动 | 167 ms | 71 ms | | `repoHygiene ④` | 1688 ms | 68 ms | ### 二、`/chips` 回包 −49~66%、纯计算 −80% - 轴 `centers` **顶层只发一份**(`ChipDay.centers` 变可选;客户端 `day.centers ?? payload.centers`,混合版本给「轴缺失 ⇒ 刷新」提示,**不画假图**)。 - **非展示日只跑 `hist` 递推**,不再算 `total/norm/avg/support/resistance/profitRatio`(口径文案依赖的 `minFactor/peakSum/clamped` 仍全窗统计)。 | 标的 | 改前 | 改后 | | --- | --- | --- | | `0.000636` | 131 234 B | **44 744 B(−65.9%)** | | `1.603668` | 166 134 B | **84 268 B(−49.3%)** | | 路由 p50 / CPU | 3.58 / 4.95 ms | **2.47 / 3.40 ms(−31%)** | | `chipSeriesOf`(180/45/242) | 3.673 ms | **0.751 ms(−80%)** —— 大头是旧实现每展示日 242 次轴拷贝(10 646 次/请求) | ### 三、`/trend` 归档写入 −99.9% 归档按 `entry.points` **内容指纹**跳过(不含 `updatedAt`)+ 跳过前 1 次 stat 防外部删除 + 目录清单每轮只读一次 + prune 只在「真的多出来」时做。 12 次 `/trend` 写字节 **176 768 → 104 B**;p50 **1.449 → 0.666 ms**;零写入那轮 **0 次 readdir**。 ### 四、顺手修掉一个真 bug:`trends-lkg.json` 自 v0.91.0 起从未更新 两个顺序 bug(先清 `dirty` / 先推进 `lastWrite` 再判定)⇒ 判定永远「跳过」且无日志。实测:修前新 home 里 **4 次成功取数文件从未生成**(本机文件停在 10-16 10:03);修后首次即落盘、内容变化时才写(≤1 次/5 s)。新增 `trendLkgPersist` 端到端断言锁住。 ### 验证 `npm run check` **812 / 812 × 2 时区** + selftest + build(73 项);副本跑 **4 轮全 exit 0**。 新增 12 例:`chipsPayload` 5(轴唯一次数/体积/跨市场一致/统计天数 45/线性成本)、`trendArchiveCost` 4(零写入 + mtime 不变/写一次/按需 prune)、`trendLkgHygiene` +2、`trendLkgPersist` 1。 **注入(副本内注入 → 跑 → 恢复)**:A 逐日重复 `centers` ⇒ 3 条红;B 统计量算回非展示日 ⇒ 2 条红(计数实测 180);C 归档指纹含 `updatedAt` ⇒ 2 条红;D 复现 v0.91.0 顺序 bug ⇒ 红。 ### 未做(已记录) - 退避可注入(估 5–8 s/时区):要改生产取数层计时语义,风险中且已达标。 - L10 四文件并行 / L5 缩窗口(估 3–5 s/时区):需单独回归时序假设。 - 护盘快照丢空字段(−33 KB)与 `/kline pct:null`、`trend label` 瘦身:第二梯队。 - `trends-lkg` 单次写体量 2.34 MB **仍未降**(只修了「该不该写」,分片/追加留待下一批)。 ### 风险登记 并发 8 在更慢的机器上可能抖动(回退 4 只 +2 s/时区);新宿主 + 旧客户端需刷新一次页面(已做 `day.centers ?? payload.centers` 兜底);归档「内容未变跳过」依赖 stat 兜底(外部删除会补写)。 ## v0.105.0 — 全项目评审批次一:宿主 5 项 P0/P1 + 客户端 3 项 P0 与 3 项 UX 报告 `docs/REVIEW-7-HOST.md`、`docs/REVIEW-8-CLIENT-UX.md`、`docs/AUDIT-IMPL71.md`、`docs/AUDIT-IMPL72.md`。`npm run check` **800 / 800 × 2 时区** + selftest + build(73 片段全过);`build.mjs` 零改动。 ### 最重要的发现:这些缺陷没有一条被现有测试覆盖 宿主评审把 4 条「建议修法」**逐字注入**原实现 ⇒ **775 条测试依旧全绿**;对照组注入一个明显错误 ⇒ 立刻变红(证明测试有牙)。⇒ 本轮所有修复都补上「能抓到旧行为」的断言,并给出「改前全绿 / 改后必红」的逐条读数。 ### 宿主 D1–D5 | 项 | 问题 | 修法 | | --- | --- | --- | | **D1**(P0) | 量纲护栏的「同一天」检查**前提恒真**(参照日取自日线自己最后一根),且 20–100 倍仍无条件 ÷100 ⇒ **筹码换手率整体小 100 倍**,`decayReason` 仍 null(口径行看不出异常) | ① 参照日改用**报价自己的交易日**,不一致 ⇒ 不判;② 判据收紧为 `\|log10(ratio/100)\| ≤ 0.1`,越带 ⇒ 不缩放 + 写「判不了」 | | **D2**(P0,一行) | 美股日线 `vol: num(it.v) ?? 0` ⇒ 缺失/空串/`"0"` 都写成 0,v0.104 的「任一日缺失 ⇒ 该根 null」契约**从源头失效**;该解析器**全仓零单测** | `numField` + `> 0` 两道;`SinaBar.vol` 放宽为 `number \| null`;**首次覆盖**该解析器与聚合契约 | | **D3**(P1) | 宏观红线断言只查「谁 import 了门禁」,而 `marketExtras.ts` 有 **6 处裸请求**打同一批宏观主机 ⇒ 断言恒绿 | 5 条真宏观取数改走 `gatedJson` / `gatedSinaBody`(各自带预算);**两融与涨停池明确划出宏观圈**并登记 `DECISIONS`(豁免 ≠ 不过熔断);断言升级为**取数点级结构扫描** | | **D4**(P1) | 两处 0 顶替:两融读成「融资余额 **0 亿元**」;美债 10Y 历史出现 `value: 0` | 空串 ⇒ `null`;历史丢缺值点并把条数写进 `missing[]` | | **D5**(P1) | 文件锁把**活着的持有者**判「心跳断供」并回收 ⇒ 两个写者可能同时进临界区 | `pidAlive === true` 且断供 ≤ 3×staleMs ⇒ **否决回收**;超过 3× 判挂死并写明理由 | **注入读数**(`/tmp/fx7-base` 逐项改回旧写法):D1-② / D1-① / D2 / D3 / D4-a / D4-b / D5 **各让对应断言立刻变红**,其余保持绿;复原后 9/9 绿。 ### 客户端 P0 三件 + 三项 UX | 项 | 问题 | 修法 | | --- | --- | --- | | **P0-1** | 持仓页首屏失败时守卫漏一种组合 ⇒ **渲染期崩溃**(`strict:false` 让 tsc 放过) | 守卫改为 `view === null` 单一判据;失败分支只渲染原因 + 重试,**不读 `view`** | | **P0-2** | 汇总分母用**全部 28 张卡**(含已隐藏),而屏幕只渲染可见卡 ⇒ 永久「27/28 实时 · 1 缺失」,与旁边「无行情源 0」矛盾 | `QuoteState` 加 **`pending`**;新增 `summarizeStrip()` 只算屏幕卡;「屏幕 N 张 / 是否隐藏 M 张」进 `title`。**实算:隐藏 1 张 ⇒ `27/27 实时`、`noSourceOnScreen = 0`** | | **P0-3** | 做T 摘要显示「**胜率 X%**」⇒ **违反红线** | 删除;改为中性事实「做T N 次 · 净 ±X · 平均每笔 ±Y」+ 口径进悬停(「只统计已发生的交易,不做对将来的推断」) | | **U1** | 写失败的原因落在**弹窗遮罩后面**,且多处 `catch(()=>undefined)` 静默吞错 | 四条写路径(分组 / 加证券 / 日历 / 流水)改为 Promise + 弹窗内 `setErr`;三条写路径里已无吞错 | | **U2** | 自选首屏失败 ⇒ **永久骨架屏**、错误组件不可达 | 失败 ⇒ 原因 + 重试 | | **U3** | 4 条 effect 依赖 `quotes` **对象身份** ⇒ 每 10 秒重复取数 | 依赖改为 `quoteTs` + 新增 `pacing.ts`(纯函数);副作用写进 `title` | **调用次数实测**(10 分钟、一拍 10 秒的纯函数模型):industries **60 → 10**、breadth **60 → 10**、market-extras **60 → 20**、trend **60 → 10**(均达预算)。 ### 验证 `npm run check` **800 / 800 × 2 时区** + selftest + build(73 项);两轮 `sha` 一致。 新增 25 条用例(宿主 9 + 客户端 16),每条带反例注入必败:D1 含**端到端**(盘缓存停昨天 + 报价今天 ⇒ 盘上量一分不动);P0-3 全客户端正则 `(胜率|概率|预测)\s*[::]?\s*[0-9]` 命中 **0**(注入 `胜率 58%` ⇒ 必败);U1 用**语句级窗口**判「写调用点必须含 `setErr(`」(第一版用字符窗口曾假阴性)。 ### 一处需你知道的过程事件 锁定片段 `无行情源 N` 的唯一来源是 `quoteState.ts` 的一句**注释**(非压缩构建保留注释)。客户端重写注释时丢了 `N` ⇒ 构建校验失败过一次;处理是**改回原注释措辞**(该句仍成立),`build.mjs` 与片段表未动。**建议下一批把它换成真实字符串**(那时才需要改 `build.mjs`,由 Lead 执行)。 ### 未做(已记录) - 客户端 `REVIEW-8` 其余 P1(8 条)、P2(12 条)、P3(7 条)**全部未做**。 - 宿主 `REVIEW-7` 的 **D6–D12 未做**;其中 **D7(A股年K 把缺失月量按 0 求和)与 D8(筹码尾部量 0)与 D2 是同一条红线**,下一批优先。 - `D5` 未上 fencing token(代价:真挂死时后来者多等约 30 秒)。 - `D3` 预算每模块一份 ⇒ `hq.sinajs.cn` 实际两处各 ≤5 次/小时(未合并,避免改掉既有账本口径)。 - `U3` 未做浏览器 Network 实测(替代方案:纯函数模型 + 接线断言 + 反例注入)。 ## v0.104.0 — 美股周/月/年 K(日线聚合 + 拆股标注)+ 上游挂掉不再整页 503 报告 `docs/AUDIT-IMPL69.md`、`docs/AUDIT-IMPL70.md`。`npm run check` **775 / 775 × 2 时区** + selftest + build(73 片段全过);`build.mjs` 零改动。 ### 一、美股周 / 月 / 年 K(由日线聚合) - 分组:自然周(周一起)/ 月 / 年,按**美东交易日**分组,纯日历算术 ⇒ 与宿主 `TZ` 无关。 - 聚合根 `date` = 该周期**首个交易日**(稳定键;否则 `mergeBars` 会随周内推进把同一根堆成两根)。 - OHLC = 首根 open / 最高 / 最低 / 末根 close;量额 = **求和**;**任一日缺失 ⇒ 该根 `null`**(不当 0、不取平均)。 - **拆股断层如实标注、绝不改价**:相邻日跌幅 ≥ 25% **且**比值接近常见拆合股比(容差 8%)⇒ 记「**疑似**」;图上竖虚线 + 「拆」角标 + 悬停「4:1 拆股 · 2020-08-31(未复权原始价:这是拆股造成的断层,不是涨跌)」;口径行写「检测到 N 处疑似拆股/合股(未复权,价差不可比)」。**标记写进 K 线缓存**(否则休市定稿免回源那一轮会丢,实测踩到过)。 - **反向改动做全**:上一批的六处拦截(`tabUnavailableReason`、`disabled`、`aria-disabled`、禁用 CSS、宿主 `klt≠101 ⇒ null`、`klineMissingReason` 分支)与对应断言**全部反转**;注入「仍然禁用 ⇒ 必败」。 - **没有顺手放开复权**:`fqSupported(美股)=false`、筹码不提供、`positionRefusal` 仍拒绝位置(断言双钉 + 注入)。 - 实网检出与公开事实一致:`AAPL` **5 处**(1987 / 2000 / 2001 的 2:1、2007 的 1:3 合股、**2020-08-31 的 4:1**);`FRO` 2016-02-03 的 1:5 合股。缓存往返(第二次命中磁盘、零回源)标记与日期完全一致。 ### 二、上游挂掉不再整页 503(P0) **根因**(复现而非猜测):`/detail`、`/industry` 把上游异常原样抛给 `routes.ts:291` 的 `fail()`,`http.ts` 的 `UPSTREAM_RE` 把「熔断 / 上游暂时不可用」映射成 **503**;而行业归属与详情**只有东财一个上游** ⇒ 东财一挂整块报错,尽管同一只标的的价格腾讯明明能给。 - 复现:只拦东财、腾讯/新浪走真实上游 ⇒ 改前 **21 条路由里 2 条 503**;改后 **0 条 5xx**,两条都是 200 + `missing[{what,why,note}]`。 - `/quotes` 原来 `missing` 只有 id 列表、**没有原因** ⇒ 新增 `missingReasons[]`(**保留** `missing: string[]` 给既有客户端契约,改形状会让界面渲染成 `[object Object]`)。 - `/industries` 原来部分丢码时静默返回 `{}` ⇒ 现在 `partial{requested,succeeded,failed}` + 逐标原因。 - `/board` 只在**真·全失败**时 5xx,且消息强制含「东财 × 板块榜单 × 真实原因」(`TotalUpstreamError` 构造器强制)。 ### 三、来源优先级翻转为 腾讯 → 新浪 → 东财兜底(P1) - 新增能力表与 `quoteSourcePlan`(`em.ts:984-1052`);东财只对「仍无价」的下手。 - **实测收益**(单拍 `/quotes`,按主机计请求):6 只腾讯全覆盖 ⇒ `{tencent: 1}`、**东财 0 次**、222ms;混入 2 只东财独占品种 ⇒ 东财只为那 2 只打(4 次)。改前同一批 8 只是 `quotes batch failed pass 1 8 symbols`(8 只全打东财)+ ~1.18s。 - **一个必须记住的取舍**:`f8/f10/f62/f66/f104-106`(换手率 / 量比 / 主力净额 / 超大单净额 / 涨跌家数)**只有东财给**。无条件把东财降到最后会让**健康主机上的护盘超大单因子静默变全 null**、涨跌家数源 A 永久缺失。故加 `emFields: true` 显式声明,**只有 `rescue.ts`(护盘)与 `/breadth` 传它** ⇒ 东财可提前,且仅当它还有主机未熔断。 **「品种 × 来源」覆盖矩阵(实测,直接 import 插件源码)** | 品种 | 东财 | 腾讯 | 新浪 | | --- | --- | --- | --- | | A股个股/指数/ETF、深市指数、港股、国际指数 SPX/NDX/DJI/HSI | ✗ `UND_ERR_SOCKET` | ✓ | — 无映射 | | 国际指数 `N225`/`KOSPI200`/`FCHI`/`GDAXI` | ✗ | — 无映射 | — 无映射 | | 美股 `105/106/107` | ✗ | ✓ | ✓ `gb_*` | | 国内期货 / 外盘商品 / 贵金属 | ✗ | — | ✓ `nf_*` / `hf_*` | | 国债指数 `1.000012` | ✗ | ✓ | — | | **国债收益率 `171.*`** | ✗ | — | — | ⇒ 预设 28 只里腾讯/新浪能服务 **19 只**,只有 **9 只**必须等东财(`171.*` + 无腾讯映射的国际指数),逐标原因写「只有东财一个行情源…等东财恢复」。 ⚠ **纠正既有文档**:期货 / 商品**是有新浪兜底的**(`nf_RB0` / `hf_HG`)—— 上一版矩阵误用 `nf_rbm` / `hf_hg00y` 得出「空行」的结论,按插件真实映射复测后确认可用。 ### 四、熔断作用域(结论:本来就是按主机) `breaker.ts:109` `breakers = new Map()`、`:111` `breakerFor(host)`、`breakerFetch.ts:35-36` ⇒ **按主机**。但它仍会造成整路由 503:`fetchAny` 在整组冷却时抛**普通 Error**,被 `http.ts` 映射成 503 ⇒ 逐标路由连累别的数据。改法:熔断期不再让逐标路由走 `fail()`(P0);并把「熔断跳过」与「请求失败」**分开记账**(`noteHostSkipped` vs `noteHostFailure`),否则熔断期零请求会被记成「连续失败越堆越高」。 ### 五、日志噪声(P2) 「降级采用 K 线缓存」从每标的每轮一行 ⇒ **每轮每类 1 行**(`27 个` + 样例 + 「明细见 debug 级」),逐标的明细降到 debug。`AGG_FLUSH_MS = 400` 是经验值:极慢的一轮可能拆成两行 —— **宁可两行,也不要把两拍并成一行**(后者会让人以为只降级了一次)。 ### 六、可观测性 `/tradewatcher/health` 新增 `upstream`:逐主机 `lastOkAt / lastFailAt / consecutiveFails / open / minutesLeft / lastSkipReason / healthy` + 一行 `line`(如 `上游健康 20:15:03:qt.gtimg.cn ✓ 20:15:02 | push2delay.eastmoney.com ✗ 20:14:58(连续失败 3 次,fetch failed)`)。**零新增请求**(读既有记账,用请求计数器断言)。 ### 验证 `npm run check` **775 / 775 × 2 时区** + selftest + build(73 项)。 新增 `src/host/degrade.test.ts` **10 例**(上游全打桩),每条带反例注入必败:② 东财挂 + 腾讯可达 ⇒ 200 且三只有价(注入 503 / `priced: 0` ⇒ 必败)③ 东财独占品种 ⇒ 200 + 两条原因(注入 `why: 'no-source'` 把「等它恢复」说反 ⇒ 必败)④ 全源关闭 ⇒ 每标的 missing 且无 0 价/编价 ⑤ `sources` 与实际行数自洽(注入把失败标的计成功 ⇒ 必败)⑥ 同轮 27 个降级 ⇒ **1 行** ⑦ 东财熔断时 `hostsAllowed` 仍含腾讯 ⑧ 健康摘要读完**请求数不变** ⑨ 全失败 5xx 必含上游 + 范围 + 真实原因。 `routes.test.ts` 里既有用例「上游熔断 ⇒ 503 且带 `retry-after`」**按新契约改写** —— 它断言的正是被投诉的那条行为,留着就是绿不了的红灯。 新增 `usAggregate.test.ts` **6 例** + `usChart.test.ts` 反转 8 例(见上)。 ### 未做(已记录) - **客户端仍未显示 `missingReasons`**:只保证路由契约(agent 工具早有结构化 missing);要显示得改 `client/*`,本批避开并行改动。 - `/industry`、`/industries` 的 200 是**加法**(新增 `missing` / `partial`),老客户端忽略即可。 - `AGG_FLUSH_MS = 400` 是经验值。 - `emFields` 之外若将来有人要 `f8/f10`,必须显式传,否则字段静默为 null(已写进 `QuoteLoadOptions` 注释)。 - `TotalUpstreamError` 目前只挂在 `/board` 的 catch;将来新增「无部分结果」的上游接口应复用它。 - 拆股只给「疑似」(无公告上游,8% 容差是经验值,实测误差 3.4% / 3.6%);MA/MACD 跨拆股仍跳变(未复权的必然结果,已标注);半日市与停牌周不特判(`sessions` 如实给覆盖交易日数)。 ## v0.103.0 — 美股补「日线 K + 分时 + 五日」;复权不可得时拒绝计算筹码与位置 报告 `docs/AUDIT-IMPL68.md`。`npm run check` **759 / 759 × 2 时区** + selftest + build(73 片段全过);`build.mjs` 零改动。 ### 一、新源实测(先测后接) | 接口 | 结果 | | --- | --- | | 新浪美股**日线** `US_MinKService.getDailyK` | **全历史无上限**:`fro` 5649 根(2001-08-06 起)、`aapl` 10055 根(1984 起);字段 `d/o/h/l/c/v/a` | | 新浪美股**分钟** `getMinK` | **硬上限 1023 根**:`type=1` ⇒ 3 个交易日(390 根/日 09:31→16:00 ET)、`type=5` ⇒ 14 天、`type=15` ⇒ 40 天;`a` 是**逐根**成交额(非累计) | | 腾讯对照(**不采信**) | `minute/query?code=usFRO` ⇒ **1 个点**;`fqkline?param=usFRO,day,,,10,qfq` ⇒ 只有 `[2011-06-02, 2026-10-16]` **2 根** ⇒ 若采信,缓存里会混进「15 年凭空消失」的假序列 | **复权判定(决定红线处置)**:同日线接口 `AAPL 2020-08-28 close 499.23 → 2020-08-31 close 129.04`(4:1 拆股**未调整**)、`FRO 2016-02-03 1.93 → 10.0` ⇒ **新浪美股日线 = 未复权原始价**,且本插件没有第二个美股历史源 ⇒ **前复权不可得**。 ### 二、接线 - 原语:`sina.ts:385-429` 新增 `SinaUsMinBar` / `parseSinaUsMin` / `fetchSinaUsMin`;`SinaBar` 增 `amount`(`a="0"` ⇒ **null**,不 0 顶替)。 - 宿主:`em.ts:1760` `isUsSecid`;`:2311` `requestKlineFromSinaUs`(TTL 10 分钟缓存整段历史);`:2329-2345` `requestKlineRaw` 美股先新浪、**跳过腾讯**;`:2414` 美股 `klt ≠ 101` 直接 null;`:1599` `sinaUsTrend`;`:1480` 单日分时先新浪、失败落回东财;`:1641` 五日走 `type=5`。 - 时间:`time.ts:217/233/245/265/272/296`(`newYorkParts` / `usDayOf` / `usWallToEpoch` / `inUsSession` / `lastUsCloseBoundary` / `isUsSettledOffline`);`parseTencentUsStamp` 改为复用(不再两处抄)。 - 前端:`chartCache.ts:41` `tabUnavailableReason`;`QuoteDrawer.tsx:625-645` 禁用档 + 自动归位;`:520-535` 口径行用宿主 `caliber`;`styles.ts:206` 禁用态样式。 ### 三、不支持周期的处置(两道都拦) 美股只提供 **分时 / 五日 / 日K**。**周 / 月 / 年**:① 前端按钮 `disabled` + 可见原因(「上游只有日线,不用日线重采样冒充」),且旧档自动归位到日K;② 宿主 `fetchKline(…, klt ≥ 102)` 直接 `null` + `no-source` 原因 ⇒ **不会「点了才报错」**。 ### 四、红线(诚实性) - **筹码不提供**:美股 `fqSupported = false` ⇒ 筹码直接拒绝。 - **位置指标新增 `positionRefusal()`**(`position.ts:63`;`tools.ts:1047/1185` 命中即返回 `why` 而**不计算**)。理由:`positionOfBars` 在**拆股窗口**上确实能算出一个「看着正常」的百分比 —— 测试里正是用这条反例证明「照算」会发生,再证明拦住它的是这条判据。 - **文案拆开**:复权不可得不再说「该标的不适用复权」(那是指数/期货的说法),改为「不复权(上游只给原始价:拆股/分红未调整)」;YTD 单独说明「跨拆股会失真」;筹码文案把「不需要」与「上游没给」并列写清。 - **口径随数据回**:`TrendData` 新增 `timeZone` / `caliber`,口径行显示「来源:新浪美股分钟(1 分钟)· 时区:美东(ET)· 成交量单位:股 · 昨收=前一交易日最后一根分钟(非交易所官方收盘)」;K 线 `unitNote` 标「股而非手」。上游不给延迟标志 ⇒ 只说「截至 …」,**不说「实时」**。 ### 五、顺手修一条真缺陷 `isSettledOffline` 用**北京**日历日 + A股 15:05 判「定稿」⇒ **美股盘中(北京深夜)会被判成已定稿、整夜不回源**,日线永远停在昨天那根。已按市场分流(`isUsSettledOffline`,纽约 16:05),并有断言。 ### 验证 `npm run check` **759 / 759 × 2 时区** + selftest + build(73 项);最终树跑 **3 轮**。 新增 `src/host/usChart.test.ts` **8 例**(751 → 759),全带反例注入必败:① 日K 条数与上游一致(注入:删接线 ⇒ null 必败;采信腾讯 2 根 ⇒ `notEqual(2)` 必败)①b 空回包 ⇒ null + 原因说清**新浪** ② 分时 3 根 + 标签 + `timeZone`/`caliber`(注入:**返回 `points: []` 冒充** ⇒ 必败,且原因里不许出现「该标的没有分时」)②b 五日三天、`source ≠ local-stitch` ③ 美股只禁 `week/month/year`(注入:判据删掉或只在前端拦 ⇒ 必败)④ `fqSupported=false` + `positionRefusal` 非空,并**先证明「照算能算出数」**再证明拦住它的是这条判据 ⑤ 美东→UTC + DST 两端 ⑤b 定稿按纽约 16:05、盘中永不定稿。 **时区双跑**:默认与 `TZ=UTC` 都 759/759。 **实网复核**:`106.FRO` 日K 末根 `2026-10-16 52.49 / −0.10%`、分时 **388 点/1 天**、五日 **390 点/5 天**;`107.VIXY` 日K 末根 `17.73 / +8.11%`、分时 **390 点**、五日 **389 点/5 天**。 ### 裁决(命中 build.mjs 锁定片段) `build.mjs:184` 锁定字面量 `该标的不支持前复权(指数/期货/板块)⇒ 不提供筹码分布`。实现者把美股原因作为**括号补在后**(`includes()` 通过,73 项绿),**没有改 `build.mjs`**。 **Lead 裁决:维持现状**(选项 ①)。理由:该锁的作用是保证「不提供筹码」这句披露存在;前半句虽只列「指数/期货/板块」,但美股原因紧接其后,读者不会误解。**已知小债**:文案不完美,若将来改这段,需同时改 `build.mjs` 的锁定片段。 ### 未做(已记录) - 美股 **周 / 月 / 年 K 不提供**(上游只有日线,**不做重采样冒充**)。 - 美股**盘前 / 盘后**分时无源。 - 分时**均价线(VWAP)**新浪不给 ⇒ `avg: null`(图上没有均价线,口径也不声称有)。 - 昨收取「前一交易日分钟末根」,与官方收盘可能差 1 分(已写进口径)。 - **半日市**未特殊处理(只会多回源,不会造错数)。 - 东财对美股日线仍作第二上游保留但**未确认**(robots 零请求 + 本机长期不可达)。 ## v0.102.0 — 美股基金/期货类出价:补足 105/106/107 映射 + 新浪 `gb_*` 第二兜底 报告 `docs/AUDIT-IMPL67.md`、`docs/PROBE-US-QUOTES.md`。`npm run check` **751 / 751 × 2 时区** + selftest + build(73 片段全过);`build.mjs` / `package.json` / 依赖**零改动**。 ### 结论:不缺数据源,缺映射 `106.FRO`(纽交所)与 `107.VIXY`(美交所)此前显示「无行情源」。实测:**腾讯 `usFRO` / `usVIXY` 与新浪 `gb_fro` / `gb_vixy` 都能取到完整行情**,两源市值/成交额/份额逐位吻合。断在代码:`tencentQuoteCode()` 不认 `105/106/107`,`sinaSymbol()` 没有 `gb_*` 规则 ⇒ 东财一挂,美股行必然无源。 ### 六项改动 1. **美股映射**:`tencent.ts:91` 新增 `tencentUsStockQuoteCode()`(`105/106/107 → us`,形状 `^[A-Z][A-Z0-9.]{0,11}$`),接在 `tencentQuoteCode`(`:103`)第三档。**`tencentCode`(`:34`)一行没动**,并加断言 `tencentCode('106.FRO') === null` 把 K 线链护栏钉住。 2. **成交额量纲**(`tencent.ts:290-297`):`f[35]` 三连(A股)= 元原样;美股 `f[35] = 'USD'` ⇒ 回退 `f[37]`,**美股 ×1、A股 ×1e4**。 3. **空串不许变 0**:`tencent.ts:133` 新增 `numField`(`Number('') === 0` 的坑),`:302-303` 用它取 `f45/f44` 并加 `> 0` 闸门 ⇒ `107.VIXY` / `105.SPY` 的 `totalMv/floatMv` 是 **null**(不是 0),`106.FRO` 仍 11,685,475,000。 4. **时间戳**:`tencent.ts:181` 新增 `parseTencentUsStamp()` —— 用 `Intl.DateTimeFormat('America/New_York')` **反解**美东墙上时间、迭代纠正(DST 交给 Intl),**反解不收敛就给 null**;`parseTencentStamp` 一字未改(对 `-` 格式仍返回 null,断言钉住)。 5. **第二兜底 `gb_*`**:`sina.ts:88` 新增 `sinaUsSymbolOf()`(**规则**,不是静态表),`sinaSymbolOf`(`:99`)改为「静态表 ?? 规则」;`:423` 抽出 `gbRowOf()` 让既有 `parseSinaGb` 与新 `parseSinaHq` 的 `gb_` 分支**共用同一份下标表**;`SinaGbQuote` 扩 `prev/open/high/low/vol/amount/tradeTime`。下游 `em.ts:763-780` 把原来一律写 null 的 `vol/amount/time/totalMv` 填上(`floatMv` 仍 null —— 新浪无此列)。 6. **文案/注释**:`em.ts:409-433` 三分支覆盖「有兜底但两源都没价」⇒ 美股接线后自动落到「东财**与备用源**本次都未给出可用价格」(`transient` + 原因);改掉 4 处过时注释。 ### ⚠ 意外发现(重要,属红线级) **新浪对不存在的美股代码会回「全零空壳」** —— 实测 `gb_zzzz` ⇒ `ZZZZ,1.0000,0.00,…`。若不拦,界面会把「没有这只票」显示成 **1.00 / 0.00%**(**编造价格**)。已加占位行判据(`sina.ts:454`:价格 > 0 且至少一项真实活动痕迹 —— 昨收 / 量 / 额 / 当日高低),**停牌股(有昨收)不误杀**;实网验证 `105.ZZZZ` 现在进 `missing` 且文案正确。 ### 顺手修两条真缺陷 - **`selftest.ts:360-400` 周六必红**:stub 时钟固定到 2026-10-19(周一)10:00 北京,`try/finally` 还原,**断言没放宽**;另加反例「同一快照挪到周六 ⇒ `level` 必须 null 且 `nonTrading.reason='weekend'`」。现在任何时区、任何真实日期都绿。 - **两处老断言按新事实更新(不是放宽)**:`p0.test.ts` 把「仅东财源」的例证从 `107.SPY`(美股,现有兜底)换成 `113.rb2610`(月度合约),语义不变;`selftest.ts` 的 `hasQuoteFallback` 断言改为「美股有两档兜底、月度合约仍只有东财」。 ### 合规口径(新增决策) `docs/DECISIONS.md` 新增「决策:报价/分时链**显式豁免** `robotsGate`」: ① **豁免范围**:仅报价/快照/分时/K线/护盘同批行情字段;**不含**正文抓取、公告新闻、财报 PDF、归档与历史批量。 ② **理由**:只取公开行情数字、不抓正文不绕登录、腾讯与新浪两源互校。 ③ **与宏观链区别**:宏观走 `robotsGate` + 每主机 ≤5 次/小时;报价链是热连接高频,套 5 次/小时会让行情条不可用。 ④ **不许**扩大成「任何上游都不看 robots」。 断言:`robotsGate` 接入面必须**恰好**是 `['host/globalFlow.ts']`(往报价链加 ⇒ 必红;删宏观链 ⇒ 必红)。 ### 三标的实网实测(修后代码路径) | 标的 | 出价 | 时间戳 | 市值 | | --- | --- | --- | --- | | `106.FRO` | 52.49 / −0.10%,量 3,722,974,额 196,988,621 | `2026-10-16T20:04:53Z`(4:04:53 PM ET) | 总 11,685,475,000 / 流通 7,515,370,000 | | `107.VIXY` | 17.73 / +8.11%,量 15,329,796,额 270,828,663 | `2026-10-16T20:00:01Z`(4:00:01 PM ET) | **null**(ETF 无该字段,不是 0) | | `105.AAPL`(对照) | 343.46 / +0.43%,额 20,621,913,459 | `2026-10-16T20:00:02Z` | 5,012,517,063,000 / 5,009,401,495,000 | `missing = []`、`sources = {"tencent": 3}`。腾讯与新浪同刻一致:FRO 52.49/52.49、−0.10/−0.10(成交额差 0.024%);VIXY 17.73/17.73、+8.11/+8.11(差 0.075%);AAPL 差 0.037%。两源**时间戳口径不同**(腾讯 = 收盘后首笔、新浪 = 盘后最新),断言里明确**不比时间戳**。 ### 验证 `npm run check` **751 / 751 × 2 时区** + selftest + build(73 项);最终树跑了 **3 轮**,实现后另做 2 轮干净复跑。 新增 `src/host/usQuote.test.ts` **10 例**,每条带反例注入必败:② **乘 10000 必红** ③ **空串写 0 必红** ③b 把 A股也改掉必红 ④ **按东八区解释必红**(16:00Z / 08:00Z 判别性输入)+ DST 两端 ⑤ 未知 ticker ⇒ 腾讯不回行、**新浪占位行必须丢弃** ⑥ `price × shares == totalMv` 且两源价/涨跌**完全相等** ⑦ 端到端(东财挂 + 腾讯不回行 ⇒ 新浪补齐)⑧ 文案 ⑨ `robotsGate` 接入面恰好一处。 **时区双跑**:同一批真实回包在 `TZ=UTC` 与 `TZ=Asia/Shanghai` 下输出 **`diff` = 0 行**(73 行 JSON 逐字节相同,含三个美股时间戳)。 ### 未做(已记录) - **东财对 FRO/VIXY 自身的覆盖未确认**(`push2*` robots 全站禁止 ⇒ 零请求;本机东财整批不可用)。不影响结论:现有腾讯/新浪两档。 - **美股盘中时段未测**(当前休市,拿到的是收盘/盘后快照;盘中 `f[6]` 是否仍为「股」未验证)。 - 美股 **K 线与五日分时未开**(腾讯美股日线只回 1 根,有意不并进 `tencentCode`)。 - 新浪 `gb_*` 只实测 10 只标的(FRO / VIXY / SPY / AAPL / EWJ / UVXY / UNG / SLV / TLT / SQQQ)。 - 客户端 `ui.tsx:239` 文案未改(它说的是「腾讯/新浪都没有映射」的品种,美股已映射,文案仍成立)。 ## v0.101.0 — 筹码:修科创板量纲断层(股/手混用)+ 档数自适应价格区间 报告 `docs/AUDIT-IMPL65.md`、`docs/AUDIT-IMPL66.md`。`npm run check` **741 / 741 × 2 时区** + selftest + build(73 片段全过);`build.mjs` 未触碰。 ### 一、量纲断层(数据质量) **根因**(三方交叉,可复算):腾讯 `fqkline` 对**科创板(688)**返回的成交量单位是**股**,东财回落是**手**,插件全链路按**手**算换手率 ⇒ `mergeBars` 按日期拼接后留下 **≈100× 段内断层**。腾讯 `qfqday` 与 `day` 两次探针逐位相同 ⇒ 与复权键无关;东财三台 `robots` 全站 `Disallow: /` ⇒ 零请求。 | 标的 | 腾讯 kline 同日 vol | 快照 vol(手) | 比值 | | --- | --- | --- | --- | | `1.688035` 科创板 | 4 653 193 | 46 532 | **100.0×** ⇒ kline 是股 | | `1.688361` 科创板 | 7 448 573 | 74 486 | **100.0×** | | `1.600519` 主板 / `0.300285` 创业板 / `1.510300` ETF | — | — | **1.0×(手,不动)** | **影响面**:用户池 43 只里科创板 **3 只全部命中**;全库 126 个日线缓存文件中 ≈100× 段内断层 **4 处**(≈2429 根)。危害不是「略偏」——`t` 被放大 100× 直接顶 cap。 **修正前后(真实缓存 + 真 `chipSeriesOf`)**: | 标的 | 换手率(均值/中位) | cap 命中 | 盈利比例 | | --- | --- | --- | --- | | `688035` 前 | 24.69% / 25.00% | **784 天** | 0% | | `688035` 后 | **2.80% / 1.56%** | **0 天** | 0.37% | | `688361` 前 | 19.87% / 25.00% | **434 天** | 4.83% | | `688361` 后 | **0.71% / 0.29%** | **0 天** | 22.84% | | `688825`(阴性对照) | 前后一字未改 | 1 天 | 8.48% | 价格哈希 `sha256(date|open|close|high|low)` 前后**完全相同** ⇒ 只改了 vol。修法:**源侧 ÷100 + 读缓存时旧段对齐新段 + 读侧同日报价兜底**,落盘走 `updateJsonLocked`,回包与界面如实标注。新增 `src/host/klineUnits.ts`(+9 条测试)。**6 组突变全部被抓**(不归一 / 顺手改 close / 静默丢段 / 写 0 / 段归一失效 / 伪造 `updatedAt`);判据原式 `amount ÷ (vol × 100) ≈ price`(容差 5%)也在断言里。 ### 二、档数自适应价格区间 - 规则:**目标档宽 = 区间中间价 × 0.1%**,`档数 = clamp(round(区间 ÷ 目标), 48, 384)`,再受**回包体积预算 256 KB** 约束;**任何夹取都留痕**(界面元组 `窗口 N 天 · 档数 M · 档宽 X 元`)。 - ⚠ **它否掉了我建议的 0.3%–1%**,理由有数据:**现状默认**(45 天 / 96 档)的档宽本就是中间价的 **0.096%–0.128%**(茅台 1.247/1295;510300 0.0058/4.52);若取 0.5% 会让**默认视图先粗 5×**(茅台 1.25 → 6.5 元/档)⇒ 那是新回归。故取 **0.1%**、允许带 `[0.05%, 0.2%]`,判据是「**不许比现状默认更粗**」。 - 上限由三者中先撞上的一道决定:① 目标档宽 ② 档数 `[48, 384]`(384 只在展示天数 ≤28 时生效)③ **256 KB ÷ 每档·日 24 B** ⇒ `display=45` 时 **242 档**、`display=60` 时 **182 档**。今天 96 档实发 61–101 KB ⇒ 预算是今天的 **2.5–4×**。 - **显示层必须做合并**:384 档时回包 218–376 KB、渲染 46–355 行,而绘图区仅约 86px ⇒ 硬画会「末行被静默裁掉」。故按**可画行数合并相邻档**(权重相加、行中心取加权均价、悬停写明「显示按 N 档合并」)。 **代价表(实测,零上游请求)** | 样本 | 根数 | 48 档 | 96 档 | 192 档 | 384 档 | | --- | --- | --- | --- | --- | --- | | `1.510300` 耗时 / 字节 / 行数 | 805 | 8.8ms / 35KB / 16 | 11.4ms / 61KB / 31 | 15.3ms / 113KB / 68 | 28.5ms / 218KB / 137 | | `1.600519` | 801 | 5.7ms / 55KB / 46 | 3.6ms / 101KB / 80 | 6.3ms / 193KB / 168 | 13.9ms / 376KB / 355 | | `116.00700` | 2000 | 10.4ms / 37KB / 4 | 18.5ms / 65KB / 7 | 14.7ms / 120KB / 14 | 28.1ms / 229KB / 46 | **自适应后实测**(档数 / 档宽 / 占中间价 / 相对旧 96 档):510300@45 → 124 / 0.0045 / 0.100% / 细 1.29×;510300@805 → 242 / 0.0087 / 0.216% / **细 2.53×**;600519@45 → 92 / 1.3012 / 0.100% / 0.96×;600519@360 → 242 / 1.5983 / 0.117% / **细 2.52×**;00700@45 → 105 / 0.4343 / 0.100% / 1.09×;00700@805 → 242 / 1.7161 / 0.365% / **细 2.52×**。 **既有统计量差异**:P5/P95/均价差异 **≤ 2.04 元**(一律 ≤ 旧格子一个档宽 4.33 元,无系统偏移);`display=45`、最新日、`barAlignsWithDisplay`、权重守恒**逐位不变**。⚠ **盈利比例最大差 5.04pp**(港股 00700@360:5.86% → 10.90%)——不是口径变了,而是旧粗格子把「跨过现价的那一桶」整桶判给一侧的量化误差 ⇒ **新值更准**;发版说明需带这一句。 ### 验证 `npm run check` **741 / 741 × 2 时区** + selftest + build(73 项)。 新增断言 **+7 条**,全部带必败注入:① 档数随区间自适应 ② **档宽必须落在目标带**,越带只能是被夹住且留痕(钉死 96 ⇒ 长窗口 0.42% 越带且无留痕 ⇒ 必败)③ 上下限生效 ④ **回包字节 ≤ 256 KB**(放开到 384 ⇒ 超预算 ⇒ 必败)⑤ 夹取必须**界面可见**(静默夹取 ⇒ 必败)⑥ 既有语义回归(P5/P95/均价差异 ≤ 旧一个档宽;盈利比例两侧非 null 且 ≤5pp;逐日权重守恒)⑦ 客户端**渲染行数 ≤ 可画行数**(合并不守恒 ⇒ 必败)。 **它自己踩过一次红线**:`methodology` 文本里写了 Markdown 粗体 ⇒ `markdownScan.test.ts` 当场红 ⇒ 已改纯文本。 ### 未做 / 待决 - **窗口默认值与上限未动**(仍 `CHIPS_WINDOW=360 / MAX=400`)——扩窗到 800 是另一条线,等用户裁决。 - 未做真机复核:本轮改了客户端 ⇒ 构建已把新 `lib/client.js` 推给运行实例,**宿主侧需重启才生效**;期间处于「新客户端 + 旧宿主」,缺 `bucketWidth` 时界面显示 `—`(不写 0、不报错,是刻意降级路径)。 - 显示层合并的**观感未截图**(合并后柱更宽;要「看起来也细」需加绘图区高度,属 UI 决策)。 - 「东财 = 手」仍是**推断**(遵守 robots 零请求);`1.689*`(科创板存托凭证)未单独实测;整段皆股且无段内断层的文件靠下次取数自愈(筹码路径不受影响);路由报价闸缺集成用例。 - K 线缓存「按标的保留」**不值得**(只读结论):现状 199 文件 / 17.2 MB(上限 300),mtime-LRU 已等价于「最近看过」;要留余量把 `KLINE_CACHE_MAX` 300 → 600 即可。 ## v0.100.0 — 侧栏标签恒为「盯盘」(后缀从结构上删除) 报告 `docs/AUDIT-IMPL64.md`(v4 一节)。`npm run check` **724 / 724 × 2 时区** + selftest + build(73 片段全过);`build.mjs` 未触碰。 ### 用户要求 侧栏标签当时显示 `盯盘 · 数据 15:05:56 · 已收盘` ⇒ **只保留 `盯盘`**。 ### 改动 - **标签改为常量** `label: () => '盯盘'`(`src/client/index.tsx:135`,附「不许再做插值」注释)。 - 同时把 v0.99.0 那条「顺带修复」的 `BadgeState.stamp` **整个删掉**(`index.tsx:35/38-44` + `badgeView.ts` 删除 `stamp` 字段与辅助函数,4 个分支不再返回它)⇒ **结构上不可能再有后缀**(不是靠注释约定)。 - **信息不丢**:① 侧栏条目**悬停 `title`** = `当日盈亏 -1.6% · 数据 15:05:56 · 已收盘`(隐身档为 `隐身视图 · 点位 4134 · 数据 …`);② **面板内**本来就有(页面级数据时刻、护盘面板采样窗口与「已收盘」、来源/降级行)。空徽标档保持既有规则 `当日盈亏 —(无持仓行情)`(32 字上限内塞不下时刻,塞了必被宿主截断)。 - **锁片段先查后动**:`grep "盯盘" build.mjs` ⇒ 未命中;`grep "已收盘\|'数据\|label\|badgeState\|stamp" build.mjs` ⇒ 只有 4 条**别的** aria-label(自选排序 / 持仓排序 / 点击打开详情 / 复权口径)⇒ 无冲突,未停手。 - **文档同步**:`README.md:65`、`:437`(标签不带后缀;窄容器短式改指**悬停**)。 ### 验证 `npm run check` **724 / 724 × 2 时区** + selftest + build(73 项);**徽标形状断言 ①–⑤⑧–⑫ 一条未动**(按前批约定)。 新增/改写断言(12 → 13 条): - **⑥ 源码级**:正则取出 `label: () => …` 的**整段表达式**逐字比对必须恰为 `'盯盘'`,且该行不许出现 `${…}`。注入三组**全必败**:加回 `` `盯盘 · 数据 ${badgeState.stamp}` ``、加回常量拼接 `` `盯盘 · 已收盘` ``、接回 `` `盯盘 · ${badgeState.detail}` ``(最容易「顺手」的写法)。 - **⑥b 结构**:`Object.keys(badgeView(...))` 必须恰为 `['detail','text','tone']`;注入 `stamp: '数据 15:05:56 · 已收盘'` ⇒ 必败(证明「后缀无处可取」)。 - **⑦ 文本**:标签**恰为** `盯盘`,不含 `数据`/`已收盘`/`·`/`%`/`当日盈亏`;同时验**信息没丢**:`detail` 必须含 `· 数据 09:42:52`(北京字面量)与 `settled=true` 时的 `· 已收盘`;徽标 `-2.3%` / tone `down` 不变。 - `marketTimeDisplay.test.ts` 的 **R5-①** 对象从标签改为**悬停**(断言的仍是「北京时间、不跟浏览器时区走」)。 产物核对:`lib/client.js` 里是 `label: () => "\u76EF\u76D8"`,全文已无 `盯盘 · ${` 这类插值。 ### 未做 - 真机复核(不许重启):用户**刷新页面**后标签即 `盯盘`,同一屏悬停仍带时刻。 - 历史审计文档(如 `docs/FIX-2-TIMEZONE.md:34`)仍写着旧的标签形态 —— 按纪律**不改写历史快照**。 - v3 遗留两项仍未动:`/tradewatcher/badge` 回包仍带 `level`/`levelLabel`(宿主契约);「护」若要侧栏入口须**另立**。 ## v0.99.0 — 侧栏徽标恢复为当日涨跌幅(移除「护」优先分支);顺带修两个真缺陷 报告 `docs/AUDIT-IMPL64.md`(v3 一节)。`npm run check` **723 / 723 × 2 时区** + selftest + build(73 片段全过);`build.mjs` 未触碰。 ### 现象与追因 用户报告侧栏图标「变了」。**图标本身自 v0.24.0 未变**;真正的差异在**图标右下角 18px 的小徽标**:昨天显示**当日涨跌幅**(`-2.3%`),今天显示**「护」**。 追因结论: - 「护」是一条**等级优先分支**:`if (level >= 2) return { text: '护' }`,排在盈亏各档**之前**。 - 引入提交 **`036b1a3`(v0.25.0,2026-10-08)**,且**需求文档 `docs/ROADMAP.md:39` 的 P0-3 原文就写了「护」优先**(由 `9df8835` v0.23.0 写入,早于实现)⇒ **不是"谁手滑改代码",每一行都有出处**;根因是**同一个 18px 位置承载两种语义**,只有用户用眼睛才发现。 - **为什么今天才看见**(`rescue-log.json` 24 天实测):10-12/13/14 当天 `level ≥ 2` 采样点 **0 个**;10-15(昨天)只有 **14:11 一拍** L2 ⇒ 60 秒轮询大概率错过;10-16(今天)**5 拍** L2(14:00 起多次)⇒ 整段时间显示「护」。 - 另有一条**间接推手**:v0.94.0(`eb396f5`)把等级线在基线可用时钉成 35/55(早盘以前是 87.5/137.5、数学上不可达)⇒「疑似护盘」在**更宽时间窗**可达,那条分支更容易被点亮。 ### 改动 1. **移除「护」优先分支**;徽标回到**当日涨跌幅**:短标记 `-2.3%` / `+2.3%`、`≥10% ⇒ +10+%`、`0 ⇒ 0.0%`(flat);**算不出(`dayPnlPct === null`)⇒ 徽标为空 + 悬停给原因,不写 0 冒充**;隐身档 ⇒ 点位。 2. **结构性防线**:`BadgeInput` 删掉 `level` / `levelLabel`,`tone` 收敛为 `up | down | flat` ⇒ **入参里没有等级,就不可能再写出等级分支**。 3. **底色跟随 `prefs.redUp`**(模块级布尔通道,与既有 `isMoneyMasked` 同模式)。⚠ 改前徽标是**写死的红涨绿跌**、`redUp = false` 会画反 —— 这是本轮唯一的「行为新增」,可单点回退(`theme.ts` + `index.tsx` 两处 + 断言 ⑩)。 4. **顺带修掉同文件一处既有缺陷**:`setBadge` 比较 `stamp` 却**从未赋值**(`e89abb2` v0.53.0 引入)⇒ 侧栏标签一直只显示「盯盘」;补上赋值后标签才真是 `盯盘 · 数据 HH:mm:ss`(让既有契约真正生效;同样可单点回退)。 5. 「护」的**独立价值未删**:护盘面板与 `tradewatcher_rescue` 工具照旧;若要它在侧栏也有入口 ⇒ 按约定**另立入口**,**不占用徽标位**。 ### 验证 `npm run check` **723 / 723 × 2 时区** + selftest + build(73 项)。**先验证通过再回报**(上一批曾因漏更新锁定片段导致构建失败)。 断言由 6 条扩到 **12 条**(文件头写明「改这些形状断言 = 显式声明这是有意的」):① 形状判据(只许 `±x.x%` / `±10+%` / `0.0%` / 空;`护`、`护盘`、`等级`、`疑似` 一律不许)④ 整段文字换成 `'护'` ⇒ 必败(另测 `疑似护盘` / `L2` / `3963` / `—` 四种非涨跌幅文本)⑤ **源码级**(去注释后)不许出现 `b.level` / `levelLabel` / `text: '护'` ⑫ **用户报告的场景本身**:把实时抓下来的回包(`level: 2` / `疑似护盘` / `dayPnlPct: -2.33`)整体传入 ⇒ 必须得 `-2.3%` ⑩ 底色跟随 `redUp` 且色板不许再有 `rescue:` / `#d97706` ⑥ `setBadge` 必须写 `badgeState.stamp`。 实时坐实:`GET /tradewatcher/badge` → `{"level":2,"levelLabel":"疑似护盘","dayPnl":-34189,"dayPnlPct":-2.33}` ⇒ 用户的 `-2.3%` **一直在回包里**,只是被「护」挡住。 产物核对:`lib/client.js` 的 `badgeView` 已无等级分支,「护」在徽标函数里为 0(其余 18 处在护盘面板,正确保留)。 ### 同批「用户没要求但被改了」清单(审计 `036b1a3`,29 文件 / +2846 行) ① **徽标的「护」优先分支**(ROADMAP 有写、**用户现在否认**)← 唯一会顶掉用户原本要看的信息 ② 同批把侧栏 `label` 从 `'盯盘'` 改成 `盯盘 · `(无需求,后已收敛)③ 收盘后缀放进标题而非图标内 ④ `fxMode='none'` 总额只含 A 股(**用户可见行为变更**)⑤ 出分日志粒度(内部)⑥ P0-1/2/4…11 逐条对上 ROADMAP,无问题 ⑦ 图标字形未动。 ### 待裁决 / 未做 - **`/tradewatcher/badge` 回包仍带** `level` / `levelLabel`(宿主契约未动;护盘面板与工具共用同一份快照),客户端已不消费 ⇒ 是否从回包删除属**契约变更**(Lead 决定:**保留**,只在报告注明客户端不再消费)。 - **真机复核未做**(不许重启):用户刷新后徽标即 `-2.3%`,最后一眼由用户确认。 - `docs/ROADMAP.md:39/147` 与 CHANGELOG 的历史文本仍写着「护」优先(**未改历史文档**);若要把「徽标只表示我的盈亏」变成成文契约,下一批在那两处加一行即可。 - 未 commit(由 Lead 发布)。 ## v0.98.0 — 护盘面板批次:总分环形与阈值线、家数搭车、分时点六通道、每卡分钟序列;导航图标钉死 报告 `docs/AUDIT-IMPL62.md`、`docs/AUDIT-IMPL63.md`、`docs/AUDIT-IMPL64.md`。`npm run check` **717 / 717 × 2 时区** + selftest + build(73 片段)。 ### 一、总分可视化(客户端) - **环形进度条**(SVG,无依赖;参数来自纯函数 `ringDashOf`)+ **中性色阶**(灰 / 琥珀 / 橙)——**不用涨跌红绿**(红绿是方向语义)。刻度文案只有「异常程度 低 / 中等 / 偏高 / 未知」,环的 `aria-label` 同步。 - **阈值线**:接在卡片「同时点量能」条上,阈值取**宿主标定值** `f1.extreme`(P95 = 1.92×,条长按 2×extreme 归一 ⇒ 线在 50%);只有一个标定值就只画一条。 - **顺手修掉一处既有红线违规**:面板原有「形态 / **概率**」与「概率 +x% / N 日」标签 ⇒ 改为「**触发后对照(历史频率,非胜率/概率)**」,**功能不删只改措辞**(Wilson 区间与样本量说明保留)。 ### 二、家数搭车(宿主;决策 1) 事件带 `breadth{up,down,even,amount,asOf,source}` + `breadthWhy` + `breadthSource`。优先级:① **本拍行情里的指数 `f104/f105/f106`**(零成本、时刻最准)→ ② `/breadth` 最近成功结果(≤120s,路由内纯内存记录)→ ③ 都没有 ⇒ `null` + 原因(含「不为此单独发请求」),**绝不写 0**;缓存过期也算缺失。 ### 三、分时点六通道(宿主;决策 2) `intraday[].channels[]`(8 字段:secid/name/price/pct/amount/timeAdjMult/volumeZ/superNet),值**与同拍快照 `etfs` 逐个相等**(证明同一拍投影);上限 120 条 + 192KB。 ### 四、每卡分钟序列(宿主;追加) `snapshot.etfSeries[]`(六只宽基各一条当日分钟序列)。口径写进契约并原样展示:「每分钟成交额(元)·当日;腾讯链逐分钟值由累计额差分推导」。上限:**每卡 ≤242 点、整块 ≤128KB**,超出**从最旧丢且六卡同步裁**(长度一致);持久化同样带上(已裁剪 ⇒ 文件有界),沿用 60 秒写入节流;**缺数据 ⇒ `points: []` + 原因(不补 0)**。⚠ 上游**没有每分钟超大单**(只有当日累计)⇒ 卡内序列用**每分钟成交额**。 ### 五、导航图标钉死(`docs/AUDIT-IMPL64.md`) **追因结论:本仓库里没有这次图标改动** —— 侧栏字形自 **v0.24.0**(`314f7d2`)起就是折线 + 右上箭头,`git log -S` 只命中那一次;更早的版本是 `📈`(**同样是趋势语义**)。同类审计:**无意改动清单为空**。 ⇒ 本轮**不做「恢复」**(唯一历史替代品也是趋势字形,恢复等于猜着改),改为新增 `src/client/navIcon.test.ts`:侧栏两个 path 串必须存在、必须是 SVG(非 emoji);`⚙/▣/◐/☀/☾` 一并钉住。反例注入:换成光秃秃的上升箭头 ⇒ 必败;`⚙` → `⚙️` ⇒ 必败。文件头写明「改断言 = 显式声明这是有意的」。 ### 六、`build.mjs` 锁定片段同步(Lead) 客户端把锁定的字面量 `'底部位置 / 形态 / 概率'` 改成 `'底部位置 / 形态 / 触发后对照'`(红线要求去掉「概率」字样)⇒ 同步更新锁定片段(73 条不变)。 ### 验证 `npm run check` **717 / 717 × 2 时区** + selftest + build(73 片段)。 **零新增上游请求**(宿主批实测):打桩跑真采样统计全部 URL ⇒ `clist`/`breadth`/`f104=` 请求数 **0**;`ulist.np/get` **≤1 次**(六通道搭同一批,绝无每卡一次);另加结构性扫描(各段 0 个 `fetch(`)。 **注入 8/8 全抓**:P1 删家数字段|P2 缺失写 0|**P3 为家数单独取数(红:`不许为家数单独取数(实际 ["…/clist/get?pn=1"])`)**|P4 删六通道字段|P5 删六卡序列|P6 去上限(`每卡 ≤ 242 点(实际 400)`)|P7 缺数据补 0|P8 纯函数里出现 `fetch`。 客户端断言 3 条(环形数学 / 刻度文案与色阶 / 接线)全带反例必败;反向判据**只掐肯定式**(`不是胜率、概率或预测` 这类否定式诚实说明**必须保留**,第一版判据误杀过它们)。 ### 未做(已记录) - 卡内**迷你折线**的界面渲染(本批只交付数据面,界面在下一批)。 - 联动(③)、时间线过滤(⑤)、多周期 1/5/15(④)在下一批(共享「选中时刻 + 周期」状态形状)。 - 每分钟超大单:上游不具备(需新上游 = 红线)。 - 分时点仍是 5 分钟粒度(1 分钟可用性见 `docs/PROBE-RESCUE-UI.md`)。 - 图标字形待用户定夺(保持现状 / 回到 `📈` / 给新字形)。 ## v0.97.0 — 搜索下拉被祖先裁剪:改视口定位 + 贴底翻转 + 保底可见行数 用户澄清:**不是条数不够,是弹窗高度不足,下方结果显示不出来**。报告 `docs/AUDIT-IMPL61.md`。`npm run check` **706 / 706 × 2 时区** + build(73 片段全过)。 ### 高度链逐层实测(视口 900px、分组添加弹窗、17 条结果) | 层 | 规则 / 位置 | 改前 | 改后 | | --- | --- | --- | --- | | ① 遮罩 | `styles.ts:368` `fixed; inset:0; padding:56px 14px 20px` | 弹窗从 y=56 起,不裁 | 不变 | | ② **弹窗 `.tw-modal`** | `styles.ts:369` `max-height:82vh; **overflow:auto**` | **裁剪边界**:内容只 ≈200px ⇒ 盒子下沿 ≈256px;下拉本应长到 ≈500px ⇒ **只有 256→260 一行在盒内**(= 用户看到的计数行) | 弹窗保持自身滚动;**下拉不再住在它里面** ⇒ 裁不到 | | ③ 输入框包装 | `ui.tsx` `boxRef` `position:relative` | 定位参照 | 不变(仍用它量矩形) | | ④ 下拉 `.tw-suggest` | `styles.ts:366` 原 `max-height:**262px**` | 262px ≈ 9 行 ⇒ **不是瓶颈**(症状来自 ②) | 删掉写死高度;`position:fixed` + `top/left/width/maxHeight` 由纯几何函数按视口实测给出;`overflow:auto` 保留 | | ⑤ 实际可见 | —— | **≈1 行** | **≥5 行 / ≥160px**;上限 `min(320, 52vh)`(900px ⇒ 320px ⇒ 12 行) | - **贴底**:输入框 y=860–884 ⇒ 下方仅 8px、上方 852px ⇒ `below(8) < 最小列表(160)` 且 `above > below` ⇒ **向上展开**,`top` 夹在视口内。 - **窄侧栏**:260×760 ⇒ 上限 320 ⇒ 12 行 ✓。 ### 实现 - `src/client/suggestView.ts` 新增**纯几何**:`SUGGEST_ROW_PX=24` / `HEAD=21` / `COUNT=26` / `MIN_LIST=max(160, 5 行)` / `cap=min(320, 52vh)` / `suggestDropdownBoxOf({rect, viewportH, contentHeight})` / `visibleRowsOf()`。 - `ui.tsx` 的 `SuggestInput` 用量取的矩形算落点(打开时量一次 + `resize`/`scroll`(捕获) 跟随;渲染带 `data-flip`/`data-max-h` 便于复核)。 - `styles.ts` 删掉 `.tw-suggest` 里写死的 `262px`。 - **未改交互**(回车选排序第一条、点击添加、计数行仍来自 `countTextOf`);**未加依赖**(没用 portal:`react-dom` 不在打包 external 面里,动它会牵 `build.mjs`)。 ### 验证 `npm run check` **706 / 706 × 2 时区** + selftest + build。 新增 `src/client/suggestHeight.test.ts` **5 条**,全带反例必败:① 可用高度 ≥ max(160, 5 行)、不超上限、内容高于上限则内部滚动(**注入:上限压到 40px ⇒ 必败**)② 必须视口坐标 + 写死的 262px 必须已删(**注入:退回 `absolute` ⇒ 必败**)③ 贴底必须 `up=true` 且落点与下沿都在视口内(**注入:去掉翻转 ⇒ 浮层越出底边 ⇒ 必败**)④ 17 条 + 窄侧栏下可见 ≥5 行(**注入:按真实裁剪形态给 20px 可见高度 ⇒ 0 行 ⇒ 必败**)⑤ 回归:计数行 / 点击 / 回车 / 类名 / 滚动不变。 **它自己纠正了一个无效反例**:④ 最初的反例是「可用高度砍一半」,实测仍 9 行 ⇒ **反例不成立**,反而印证根因在**祖先裁剪**而非高度上限;已把反例改成「可见高度 = 20px」的真实形态。 ### 未做(已记录) - **未做真机截图**(本机无法渲染插件页面 + 不重启)⇒ 高度链是「CSS 实读 + 几何推导」。发布后请复核三处:17 条能否一次看到 ≥5 行、贴底是否向上展开、窄侧栏同样成立。 - 未用 portal(`fixed` 已足够,且不牵 `build.mjs`)。 - `.tw-modal{overflow:auto}` 保留不动(长弹窗需要它)。 - 极小视口(< 400px 高)下保底 5 行放不下时按视口给最大值,`top` 仍夹在视口内(不会整块消失)。 ## v0.96.0 — 搜索:列出全部匹配(去掉写死的 10 条截断 + 排序 + 分组 + 上限披露) 报告 `docs/AUDIT-IMPL60.md`。`npm run check` **697 / 697 × 2 时区** + build(73 片段全过)。 ### 上限来自三处,真正的元凶是第三处 | # | 位置 | 事实 | | --- | --- | --- | | ① | `src/host/routes.ts:918` | `/tradewatcher/suggest` 只有 `q`,**没有 count** ⇒ 调用方要不到更多 | | ② | `src/host/em.ts:2809` | 请求上游**写死** `count=14` | | ③ | **`src/host/em.ts:2847`** | 映射循环 **`if (out.length >= 10) break`** ⇒ 不管上游回多少只留 10 条 ← **元凶** | | ④ | `src/client/ui.tsx` | 客户端**没有**截断 ⇒ 不是客户端上限 | ### 改后实测(真实关键字) | 关键字 | 改前 | 改后 | 备注 | | --- | --- | --- | --- | | `中远` | **10** | **17** | A股 7 · 港股 5 · 其它 5 —— 正是被写死 10 卡住的那类 | | `中远海能` | 2 | **2** | ⚠ **上游本来就只给 2 条**(600026 A + 01138 H)⇒ 不是我们截断的 | | `600026` | 1 | 1 | 纯数字代码 ⇒ 完全匹配档 | | `zhongyuan` | — | 10 | 拼音由上游匹配(客户端不猜、也不据此过滤) | | `中国平安` | — | 9 | A股 1 · 港股 2 · 其它 6(期货合约) | | `a` | 10 | **50** | 触发上限 ⇒ `共 50 条 · 已显示全部 50 条(上游本次上限 50 条,可能有更多)` | ### 实现要点 - 宿主 `searchSymbols(query, limit=14)`:请求带 `count=cap`、`break` 改 `cap`、腾讯兜底同 cap、缓存键带 cap;**默认仍 14 ⇒ agent 工具行为不变**。 - 路由加 `count`(默认 50,夹 1..100)+ `requested` / `truncated`。 - 新增纯模块 `src/client/suggestView.ts`(去重 / 排序 / 分组 / 计数 / 分批,市场分类复用共享 `marketOf`)。 - 弹窗渲染计数行 + 分组头(`A股 · 7 条`)+ **原因优先于「无匹配」** + 「显示更多(还有 X 条)」+ 回车取排序后第一条。 - **长列表**:容器原有 `max-height:262px; overflow:auto`,新增**分批渲染**(40 行/批)⇒ DOM 行数有上界(不引虚拟化:不测行高、不加依赖)。 - **归属**:`/tradewatcher/suggest` 属 **core**(自选页 + 持仓页两个行消费 + agent 工具)⇒ 按判据**保持不动**,只改实现。 ### 上游能力实测(决定「取全」的边界) `count=14 → 14 条;count=30/50/100 → 18 条`,且 **`pn=2` 无效(返回同一批)⇒ 上游没有分页** ⇒ 「翻页取全」退化为「一次要够 + 拿满上限时如实披露」。 ### 验证 `npm run check` **697 / 697 × 2 时区** + selftest + build。 新增 `src/client/suggestAll.test.ts` **6 条**,每条配反例注入(必败):① 条数 = 上游去重总数(并锁「写死的 `>=10` 必须已删」)② 排序(完全 / 前缀 / 包含 + 同档市场与代码 + 大小写不敏感 + 拼音不假装能算)③ **截断必须出现「上游本次上限」**(删掉该文案 ⇒ 必败)④ **失败必须给原因且弹层保持打开**、渲染顺序原因在「无匹配」之前(失败即空数组 ⇒ 必败)⑤ 计数 = 行数、分批后「可见 + 隐藏 = 总数」⑥ 边界(空关键字两端都不发请求、>40 字给原因而非静默截断)。 ### 未做(已记录) - **上游无分页** ⇒ 只能「要够 + 披露上限」(若将来支持 `pn`,在 `searchSymbols` 加一层循环即可)。 - 虚拟化未做(分批已封顶,实测最多 50 条)。 - 未做真机复核(分组头与计数行在窄侧栏的观感、滚动)。 - `fetchTencentSuggest` 默认 limit 仍 10(只有本路径显式传 cap,其它调用方不受影响)。 - **「中远海能」只有 2 条是上游匹配集**;若期望「相关名也列出来」(中远海特 / 中远海控),那是**联想 / 相关度**的语义扩展,不是「列出所有匹配」⇒ 建议单独立项。 ## v0.95.0 — 工程四项:归属表同步断言、弱锁改强锁(18→1)、导出面归并、文档纪律 依据 `docs/REVIEW-6-ENGINEERING.md`;报告 `docs/AUDIT-IMPL58.md`。`npm run check` **691 / 691 × 2 时区** + build(73 片段全过)。 ### ① 归属表同步断言(顺手抓到 4 处既有漂移) - 真表 `ROW_ROUTES` / `ROW_TOOLS` 改为 `export`(`src/index.ts:32/:67`);`src/index.test.ts` 新增两条用例:三个解析器把**测试副本 / 文档 §2.1 表 / `cordis.patch.yml` 行注释**读成结构化数据,对真表**逐行 `deepEqual`**;另一条守路由账目(真表不许重复声明、`FUNCTION_OWNED_PATHS` 必须等于真表并集、文档账目必须等于扫 `routes.ts` 的实测)。 - **新断言第一次运行就红了 4 条真漂移**:文档 `watchlist` 行缺 `/industries`、`/anomaly`;`calendar` 行写法解析器读不到;`market` 行顺序与代码不同;**`FUNCTION_OWNED_PATHS` 漏了 `/tradewatcher/tools/global-flow`**(此前「core 会不会偷注册这条路由」根本没有断言)。 - D1–D4 同步补齐(含路由账 `24 → 26 = core 15 + calendar 1 + watchlist 3 + portfolio 2 + market 4 + cloudmap 0 + tools 1`)。 ### ② 弱锁片段改强锁(只换 needle;73 条不变、顺序不变) `git diff --stat build.mjs` = **17 insertions / 17 deletions**。**18 条弱锁修强 17 条**:`n≥4` 由 **18 → 1**,`n=1` 由 **29 → 46**。 | 旧 needle | 次数 | 新 needle | 次数 | | --- | --- | --- | --- | | `板块` | 29 | `概念板块` | 1 | | `隐身` | 16 | `视图:隐身(金额已模糊为 ¥••••、涨跌色转灰阶)` | 1 | | `不适用` | 12 | `该标的是指数/期货,价格本身没有除权除息,复权不适用` | 1 | | `前复权` | 9 | `该标的不支持前复权(指数/期货/板块)⇒ 不提供筹码分布` | 1 | | `数字模糊` | 8 | `数字模糊(价格/涨跌/盈亏/成交额;hover 该行显形)` | 1 | | `日分位` | 7 | `60日分位` | 1 | | …另有 11 条 | | 略(见报告对照表) | 1 | | **`振幅`** | **6** | **保留(无法唯一化 ⇒ 停手上报)** | 6 | **负例证明**(`/tmp/eng2-neg` 克隆,未动工作区):把源码里的 `概念板块` 改成 `概念XX` ⇒ **新 needle 让构建失败**;同一份源码退回旧 needle `板块` ⇒ **构建通过(73 项)** —— 同一处 UI 消失,旧锁放行、新锁拦住。 **勘误**:REVIEW-6 §4.1 的「20 条弱锁」计数有误(纯 ASCII needle 被重复计一次)⇒ 修正后为 **18 条**,已在文档加勘误块。 ### ③ 导出面归并 + 文档纪律 - 22 条多余 `export` 去掉(`tsc --noEmit` 通过 ⇒ 反证无外部引用);3 条死代码删除(`LimitCountsWire` / `macroHistory` / `macroReadFailures`);4 条测试钩子统一 `__` 前缀(旧名全仓零残留)。 - `AUDIT-IMPL35/45/46/47` + `AUDIT-HY4-1` 首行加「历史快照…不代表现状」页眉。 - `DECISIONS.md` 头部 4 行重复 H1 收敛为 1 行 + 说明(13+/4−,正文未动),并**新增决策**:「发版必须是原子动作 —— 升版本 → 跑门禁 → **确认产物含新版本号** → 再重启/发布」。 - 新增 `src/repoHygiene.test.ts` **6 条防线**:22 条不许再 `export` 回来 / 3 条死代码不许复活 / 4 条钩子必须 `__` 前缀 / **全仓不许新增「零引用导出」**(基线 49 条历史欠账,只减不增)/ 历史页眉 / `DECISIONS` 头部唯一 H1 + 原子发版条目。 ### ④ 权威性声明 `docs/DESIGN-SUBPLUGINS.md` §2.1 表头已写明「本表是 `src/index.ts` 的 `ROW_ROUTES`/`ROW_TOOLS` 的**镜像**,冲突以代码为准;三处镜像由 `src/index.test.ts` 的用例逐项 `deepEqual` 守住」;真表注释反向指回测试,形成双向指引。 ### 验证 `npm run check` **691 / 691 × 2 时区** + selftest + build(73 项)。 **文件级反例注入 13/13 全部被抓住**(A 组 6/6:真表加路由、文档删 market-extras、文档删 tools 行、patch 注释删 market-extras、文档账目改回 24、测试副本脱钩;B 组 7/7:export 回来、两条死代码复活、钩子旧名、删页眉、去掉原子发版条目、头部插重复 H1)。每条注入后立即还原、`sha256` 一致。**⑥ 的判据因注入而改强一次**:原写法可被「头部插一行 H1」绕过,改成「前 8 个非空行只允许文档 H1 与 `# 决策:…`」后必败。 ### 未做(已记录) - **`振幅` 那条弱锁未修强**(18 → 17),按约定停手上报(两页 tooltip 逐字相同 ⇒ 无法唯一化)。 - 导出面基线里仍 **49 条历史欠账**(`TrendCaliber` / `Z_95` / `QuoteEngine` 等),本批只冻结增量。 - 「零引用」判据是**文本级**(不解析 AST),极端写法可能漏判 ⇒ 已限定只对「新增」生效。 - 本批门禁读数**包含同期另一路在途文件**(差值 = 5 条用例)。 - 未做浏览器实机确认(本批全是结构/文档/常量级改动,无 UI 行为变化)。 ## v0.94.0 — 护盘信号:量能改用同刻度基线(早盘不再静默),时点系数降级为兜底 用户要求「不加新功能,但要能识别早盘大资金放量,且不得放宽系数」。报告 `docs/AUDIT-IMPL59.md`。`npm run check` **691 / 691 × 2 时区** + selftest + build(73 片段全过)。 ### 根因与修法 早盘静默的根因不是「没信号」,而是 **`时点系数 0.4` 在粗糙替代一个缺失的同刻度基线**:系数把等级线整体抬到 `35/0.4 = 87.5`、`55/0.4 = 137.5`,而四项上限只有 **71.7** ⇒ 数学上不可达。硬证据:全日志 09:30–10:29 的 65 条样本里 `rawScore × coef` 最大值 **23.5**,旧早盘线 **87.5**。 修法(**复用既有机制,无新面板/开关/推送**): - 量能因子改按**同 5 分钟 slot 的近 20 日 (μ, σ) 基线**的 `z` 归一;μ/σ 复用既有 `progressCurve` 抓的 5 分钟 K 线(实测 **126 天**历史),新增逐 slot σ = `progressSd`,**长度固定 49** ⇒ 天然有界,仍走既有文件锁与读守卫。 - **等级线保持 35 / 55 不变**(不放宽)。 - **时点系数降级为兜底**:仅当基线不足时参与,且归因里必须写明「同刻度基线不足(N/20 日)⇒ 本次用时点系数 X 替代」。口径走既有因子行(`z=…` / `…x`)与归因句。 ### 回放读数(本机日志只读副本;最近 9 个有样本日、321 条样本) | 口径 | 改前 | 改后 | | --- | --- | --- | | **早盘 09:30–10:29 触发率** | **0 / 65 = 0.0%** | **45 / 65 = 69.2%** | | 日级最高等级分布 | `{0:3, 1:3, 2:3}` | `{0:2, 1:5, 2:2}` | | 全天触发条数 | 156 | 210 | ### 合成用例(验收核心) 1. 早盘 09:40、旧系数 0.4、`volumeZ = 2.2`、n=20 ⇒ **出 L1**(raw 40.0;线 35/55)⇒ **早盘不再静默**。 2. 同一盘面把 `volumeZ` 换成 `0.1` ⇒ **L0**(raw ≈ 19.6)⇒ **不误报**;同一盘面 z=2.2 确实出等级 ⇒ 差异来自量能刻度,**不是门槛**。 3. 冷启动(`volumeZ = null`、days=6)⇒ 系数仍 0.4、线仍抬高、归因写明「用时点系数替代」⇒ **不静默**。 ### 验证 `npm run check` **691 / 691 × 2 时区** + selftest + build。 新增 `src/host/rescueSameSlot.test.ts`(`z` 判据 / 三个合成用例 / 结构性:`sameSlotZ` 单实现、采样路径必须传 `volumeZ` + `volumeZDays`、`coef = sameSlot ? 1 : coefInput`、基准仍是 `[35, 55]`、σ 必须持久化)。**注入 4/4 全抓**:E1 删基线(早盘 z=2.2 变 L0)|E2 用全局系数顶替 z|E3 冷启动静默回落(删掉替代声明)|E4 等级线改回缩放。 ### 已知边界(如实记录) - ⚠ **回放里的 σ 是声明过的假设值**(σ = 25% × μ、n 视为 20 天)—— 本机日志里**没有逐日同 slot 曲线**,所以真实 σ 现在还没有。σ 敏感度:早盘触发占比在 σ = 25% / 50% / 75% / 100% × μ 下分别是 **75.4% / 72.3% / 63.1% / 44.6%** ⇒ **绝对触发率取决于真实 σ**(由上游 126 天逐日曲线算出)。 - 本机现状:`progressDays = 126`、`progressSd` **尚不存在**(本批新增)⇒ **下一次每日标定写回 σ 之前,真机走兜底(行为与改前一致)**;写回后 n=126 ≥ 20 ⇒ **立刻生效,不用攒 20 天**。 - **尾盘比改前略严**:基线可用时系数完全不参与,旧口径 `coef = 1.1` 曾把线降低约 9%(更容易出等级),现在固定 35/55。这是「系数不参与」的直接后果;要保留尾盘旧宽松度需另立口径(**未擅自加**)。 - μ/σ 是**六通道按 slot 池化**(沿用 `fetchProgressCurve` 既有做法)。 - 仍**不做身份认定**,文案保持行为描述。 ## v0.93.0 — 补齐快照契约缺口:连续评分跨日分位进契约(客户端不现算) 报告 `docs/AUDIT-IMPL57.md`。`npm run check` **678 / 678 × 2 时区** + selftest + build(73 片段全过);`build.mjs` 零改动。 ### 缺口与修法 宿主**早已算出**「连续评分近 20 日分位」(`rescue.ts:693`),但**没进客户端快照契约** ⇒ 界面拿不到,客户端按「只许用宿主已算的」的规矩**既不现算也不顶替**,只能在悬停写明原因。 现在契约含 **4 个字段**: | 字段 | 类型 | 含义 | | --- | --- | --- | | `continuous` | `number \| null` | 连续评分 0–100 = `raw ÷ 权重合计 × 100`;非交易时段 ⇒ null | | `continuousPercentile` | `number \| null` | 近 20 日分位;样本 < 5 ⇒ null | | `continuousPercentileN` | `number` | 样本数(0 = 还没有跨日样本) | | `continuousPercentileWhy` | `string \| null` | 算不出来的原因(样本不足 / 冷启动 / 非交易时段 / **旧快照缺字段**) | - 映射**只有一处**(纯函数 `snapshotContractFields(scored)`);另三条非采样路径(尚未采样 / 复盘兜底 / 持久化兜底)也显式补齐 + 各自原因 ⇒ 冷启动拿到的是「样本不足」,**不是 `undefined`**。 - 持久化保留这 4 个字段 ⇒ 收盘后重启仍能显示分位。 ### 客户端(同一批) 可见行 = `${score}/100 · 当日峰 … · 分位 X%(N=…)`(缺数据显示 `分位 —`);悬停写契约里的 `continuousPercentileWhy`;**不引用宿主 `scorePercentile`、无任何自算式分位**。 ### 验证 `npm run check` **678 / 678 × 2 时区** + selftest + build。 新增 `src/host/rescueSnapshotContract.test.ts`:① 纯函数映射与 `scorePercentile` **逐点一致**、N 与样本数一致、样本不足 ⇒ null + 原因、非交易时段 ⇒ null + 原因、**并断言采样路径必须展开该映射**;①b 端到端 `mon.sampleNow()` 快照带四字段;② 冷启动 ⇒ null + N=0 + 原因(**不得 0**)。另 `rescueWeights.test.ts ⑫` 与 `rescuePanel.test.ts ⑥`。 **注入 5/5 全抓**:C1 映射删三字段(缺口复现)|**C1b 采样路径不再展开映射(专堵「字段悄悄丢回缺口」)**|C2 缺样本用 0 顶替|C3 客户端自算分位|C4 分位人为偏移 +1。 **更新一条旧断言**:`productBatch.test.ts ②` 原要求面板写明「当前快照契约里没有带出来」(缺口时期的规定)⇒ 缺口补上后改为「必须读契约字段 + 仍不许现算 + 可见部分带短标记」。 ### 未做(已记录) - 「真采样 → 真分位」的完整上游夹具(本批用离线桩 + 纯函数映射覆盖)。 - 工具输出结构未改(归因句里本来就带连续评分 / 分位 / N)。 - 分位窗口定义未变(最近 ≤20 个有 `continuous` 的跨日样本,跳过今天)。 ## v0.92.0 — 产品迭代批:字号与间距收敛(145 处 → 0)+ 护盘参照 + 可发现性 依据 `docs/REVIEW-5-PRODUCT.md`;报告 `docs/AUDIT-IMPL56.md`。`npm run check` **673 / 673 × 2 时区** + build(73 片段全过);`build.mjs` 零改动。 ### 一、口径变更(Lead 裁决) `FLOW_PAYLOAD_TTL_MS` **10 → 12 分钟**(`src/host/globalFlow.ts:169`),注释写明「12 = 60 / 预算上限 5 算出来的」⇒ 与「每主机 ≤5 次/小时」对齐,四块磁贴不再在第 6 次显示「未取到(预算用尽)」。 断言:**TTL / 预算比值 ≥12 分钟/次**,且上界 ≤30 分钟(防「缓存到看不见更新」)。注入:TTL 调回 10 分钟(每小时 6 次、预算必然用尽)⇒ 必败。 ### 二、字号与间距收敛(145 → 0) - **TSX 64 + CSS 81 = 145 处 → 0 处**;映射 `8/9/9.5/10/10.5/11/11.5 → 12`(展示级 ≥15 不动)。 - **审计面从 2 个文件扩到全客户端**,并加三个防退化阈值(文件数 ≥20 / TSX 字号 ≥60 / CSS 字号 ≥100)。 - **拥挤区逐处核对**:`.tw-tone` 字号 10→12 时行高顶满 ⇒ 同批 16/16(余量 4px);`.tw-chip` / `.tw-chart-tip-row` / `.tw-iconbtn` 余量 4/7px;`kline` 三处窗格标签基线 `+9/+10 → +12/+13`(否则 12px 字被上沿裁)。全量脚本复核:**行高余量 <2px 的规则 = 0**。 - 间距:三处越格(`gap:5`×2、`marginRight:3`)→ 4px。 - ⚠ **CSS 里 26 处 3/5px 改了又撤回**:先按「全部 3/5→4」改,撞掉 `splitDrag.test.ts:193` 的几何断言(`.tw-zoom{gap:5px}` 是 `CHART_CONTROLS_PX=66` 的输入)⇒ **撤回并列为显式例外**,依据写进断言。要收敛须同批改图表几何常量,建议单开。 ### 三、护盘面板参照 - 总分:`${score}/100 · 当日峰 X`;悬停补峰值时刻与阈值口径;**峰值算法上提为唯一实现**(原先下方 IIFE 又算一次)。 - 因子列:列头改「贡献/满分」,每行 `x/满分`、合计 `x/100`,公式进悬停。 - ⚠ **发现一个契约缺口(未越界改)**:跨日分位(连续评分近 ≤20 样本分位)在 `src/host/rescue.ts:693` **已算**,但**没进客户端快照契约**(`shared/model.ts` 只有位置类的 `percentile60`)⇒ 界面拿不到。按「只许用宿主已算的、不许客户端现算」的规则,**没有显示、也没有顶替**,只在悬停写明原因,并用契约里真有的当日峰值作参照。**建议宿主侧把 `percentile`(+ 样本数)加进快照**。 ### 四、可发现性 - 行情条新增**可见按钮** `视图·完整/紧凑/隐身`(悬停写明 `Alt+M`);切换逻辑抽成 `cycleViewMode` **唯一实现**(快捷键与按钮共用,断言锁「档位循环只允许一份」)。 - 大盘页补首次加载空态;日历页裸文案 → `EmptyHint`;工具目录顶部一行如实说明(判据挂真实 `TOOL_ENTRIES.length <= 2`)。 - **更正上一份评审**:云图**已有**「超时占位 + 域名 + 最后成功时间 + 重试」⇒ 本轮不动它。 ### 验证 `npm run check` **673 / 673 × 2 时区** + selftest + build。 全客户端扫描:**改前 145 → 改后 0**(TSX 0 / CSS 0;4 个测试文件内的字号是反例注入,扫描已显式排除)。 新增 7 条断言(`robotsGate` 1 + `uiuxDiscipline` 改写 1 + 新建 `productBatch.test.ts` 6),每条配反例注入(塞回 `11px` / 去掉可见参照 / 撤掉 `/满分` / 退回只有快捷键 / 日历退回裸 `div` / 删稀疏说明)⇒ 全部必败。 ### 未做(已记录) - 跨日分位显示:**卡在宿主契约**(`shared/model.ts` 缺 `percentile`)⇒ 待宿主补字段。 - CSS 26 处 3/5px 几何例外:需与图表几何常量同批改。 - `6px` / `8px` 混用未动(同刻度但语义未分级,全量改会挤动大量紧凑行,建议规则化后分批)。 - **未做真机复核**:字号 +1~3px 的观感、因子表与轴标签是否换行、新增文本按钮在窄侧栏是否换行 —— 需真实面板确认。 ## v0.91.0 — 修掉复核证伪的 5 条逃逸注入 + 门禁覆盖全部宏观链路 + 数据卫生 依据 `docs/REVIEW-4-RISK.md`。`npm run check` **666 / 666 × 2 时区** + selftest + build(73 片段全过);`build.mjs` 零改动。 ### 一、门禁覆盖全部宏观链路(「唯一入口」从措辞变成事实) - 实测证伪:`globalFlow.ts` 的**新浪实时主源**与**东财交叉核对**是裸 `fetch`(只走熔断,**没有 robots 与 ≤5 次/小时预算**),真机 `req` 账本里只有 treasury 与 ecb 两家。 - 修后:四条链路(新浪 / 美国财政部 / ECB / 东财)**全部走 `gatedFetch`**;断言「跑一次宏观取数后,`req` 账本里四家主机各 ≥1 次」。 - 措辞同步改成事实(`robotsGate.ts` 与报告)。 ### 二、补上 5 条逃逸注入的守卫 | 逃逸注入(改前全绿) | 现守卫 | | --- | --- | | 删 `budget.spend(host)` | 断言「成功一次 ⇒ 该主机 `req` +1」;用满后第 6 次 `ok:false` + 原因含「预算」+ **请求数不增**(零请求) | | 关掉 `$` 锚点 | **判别性输入**:`/private$` 拦 `/private`、不拦 `/private/x`,且两条路径结论必须不同 | | ECB 链路换裸 `fetch` | 宏观块**结构检查**:`globalFlow.ts` 不许有任何裸 `fetch(`;四家主机各自由 `gatedFetch(` 取数(解析调用点首参) | | 新增不过门禁的宏观源 | 宏观块里每个 URL 主机必须落在这四条内 | | README 覆盖扫描硬编码 8 文件 | 改为**扫描生成**(walk `src/host/**/*.ts` 取全部 `.json` 字面量逐条要求被 README 覆盖);途中另抓两个「假守」:占位符 `[^/]+` 会把新文件算成已覆盖、代码里原样写 `YYYY-MM-DD.json` 需归一 | **改前 → 改后读数**:I14 / I15 / I16 / I17 / I09 全部**全绿 → 红**(共 7/7 抓住,脚本 `/tmp/risk-inject.py`)。 ### 三、数据卫生 - `quotes-lkg.json`:**46/130 行缺 `source`** ⇒ 装载时按事实标注 `source:'lkg'`(**不编上游名**)并写回;日志报条数。 - `trends-lkg.json`:**5.19MB 无上限、每 5 秒整份重写**(写放大)⇒ 新增 `trimTrendPayload`(超 4MB 按 `at` 新→旧压到 80%)与 `trendWriteDecision`(不 dirty / 节流内 / **内容指纹未变** ⇒ 不写)。 ### 四、文案修正(P1) - `README.md` 的 `write-log.json` 改为事实:只覆盖**插件代写的日历新增**(真机无该文件属正常)。 - `docs/DESIGN-UIUX-ADOPT.md` 加追记:如实写「起初只扫硬编码 8 文件、I09 可绕过」,现已修。 - `docs/AUDIT-IMPL55.md` 加文件头「按批次累积 + 最新 = 666 测试 / v0.90.0」。 ### 五、一条已知边界(纪律的预期行为) 载荷缓存 10 分钟 ⇒ 一小时最多约 6 次宏观取数 ⇒**每主机 5 次/小时的预算会在第 6 次用满**,那一次四块磁贴如实显示「未取到 + 原因(预算用尽)」。这是设计内行为;若要常年不触发,需把宏观取数缓存拉到 ≥12 分钟或把上限调到 6(属口径变更)。 ### 未做(已记录) 宿主热重载(四条链路的 `req` 账本要重载后才变四家);REVIEW 里其余未点名的注入(I01–I13、I18–I21)处置权在 Lead;`writeLog` 是否覆盖持仓/流水/分组属产品口径,待定。 ## v0.90.0 — 磁贴去噪(解释进悬停)+ 宏观历史回填(不再等一周)+ 修两个真 bug 报告 `docs/AUDIT-IMPL55.md`(§批 4)。`npm run check` **661 / 661 × 2 时区** + selftest + build(73 片段全过);`build.mjs` 零改动;注入 **24/24 全抓**。 ### 一、常驻磁贴去噪(用户要求) 常驻只留 **值 + `较上日` 短标记**(`较上日 —` 或 `较上日 ±数字`);**括号解释与整句原因全部进 `title`/`aria-label`**。 | 位置 | 常驻 | 事实去哪 | | --- | --- | --- | | 蛋糕温度 | `较上日 —` / `±x` | 悬停:`本次「较上日」不可算:回放窗口只有一天(曲线序列不足两步)` | | 美国 10Y / 美元指数 | 同上 | 悬停:退化说明(`自采样不足 5 条 ⇒ 已退化为 1 日`)、历史来源(含代理标注)、交叉核对 | | 流动性支持度 | `未取到` + `较上日 —` | 悬停:整句原因(缺哪一侧、为何算不出、不用 0 顶替) | | **保留** | 曲线口径标记(圆点 = 实测日)、降级/陈旧/快照/作废标记、数据时刻、**边界声明** | 断言锁「不得消失」 | 断言:常驻不许出现 `(`、`曲线序列不足两步`、`自采样不足`、`未取到:`、`⇒`(新增单点曲线场景)。 ⚠ **诚实披露**:这条断言第一版遍历了**并不存在**的 `kpi.cells` ⇒ 判据是空的(注入 K23 一度没被抓到);改为按标签遍历后立即变红。 ### 二、宏观历史回填(把「等一周」改成「补历史」) - **美债 10Y**:美国财政部官方日频 CSV ⇒ **回填 197 天**(2026-01-02 → 2026-10-14)。实时值仍是新浪 `5.156`,**回填不覆盖实时**;5 日变化立刻可算(`-0.124`)。 - **美元**:**DXY 日频历史实测三条路全不可得** —— 东财 `push2his` 的 robots 禁止 `/`(零请求且本机不可达);新浪 `DINIW_D` 403;新浪外汇日线接口服务端报错 ⇒ 用 **ECB 欧元/美元日频代理 280 天**,`historySource` 写明「**与 DXY 口径不同,仅用于方向判定**」,悬停再补「代理值不是 DXY,不得当 DXY 用」。 - **蛋糕温度「较上日」**:365 个序列日**立刻可算**。 - 写入与自采样共用 `updateJsonLocked` 文件锁内读-改-写(同一天以自采样为准);刷新节流 7 天;只多两次官方请求,仍走 `robotsGate`(robots + 每主机 ≤5 次/小时)。 ### 三、两个真 bug(顺手修掉) 1. **温度「较上日」索引写反**:`idx+1` 取到了**未来** ⇒ 明明有 365 个序列日,却永远显示「不足两步」。 2. **断言遍历了不存在的字段**(`kpi.cells`)⇒ 判据实际是空的(见上)。 ### 四、真机现场(只读证据) 宿主**已重载** ⇒ 真机数据目录出现插件自己写的 `macro-log.json`:`dxy = 102.0652 | us10y = 5.156`,`diag` 四源全 ✓(新浪 ×2 + 财政部 + ECB),`req = {home.treasury.gov: 1, data-api.ecb.europa.eu: 1}` ⇒ **前一批的「三块未取到」已消失**。本次构建跑的是已提交的 v0.89.0 ⇒ **本批回填要等宿主再热重载一次**才会落盘(落盘后「较上日 / 5 日变化」立刻有值)。 ### 验证 `npm run check` **661 / 661 × 2 时区** + selftest + build。注入 **24/24 全抓**:K20 删回填|K21 代理写成 DXY|K22 日频覆盖实时|K23 括号解释塞回常驻|K24 整句原因回填常驻。 ### 未做(已记录) - 宿主热重载(不由实现者做)。 - **DXY 真实日频历史**:本机无源,只能用 ECB 代理并标注。 - 汇率三源未接(不在本批需求内)。 ## v0.89.0 — 宏观数据换源(Yahoo/FRED 移除)+ robots 门禁 + 自采样加锁;删除两处冗余文案 报告 `docs/AUDIT-IMPL55.md`(§批 2、§批 3)。`npm run check` **659 / 659 × 2 时区** + selftest + build(73 片段全过);`build.mjs` 零改动。 ### 一、宏观两档换源(依据 `docs/RECON-PUBLIC-SOURCES.md` 实测) - **Yahoo Finance 与 FRED 已从代码里彻底删除**:前者 403 且条款禁止自动化;后者链路未确认(代理 HTTP/2 错误 92、直连一次 200 一次 92)。 - 新链路:**新浪(实时)** + **美国财政部官方日频**(美债 10Y par yield,`robots = Allow: /`)+ **ECB Data API**(美元指数**同向参考**)。 - **BIS 只走后台归档**(`npm run macro:archive`,实测 25 秒级),**不进交互路径**;BIS 有效汇率的 dataflow id 实测三种键形 404 ⇒ **未确认,未写进代码**。 **换源后真机读数(隔离 `DSH_HOME`)**: ``` US10Y = 5.156 | 新浪 globalbd_us10yt(24 小时品种) cross = 美国财政部(官方日频 par yield)5.190(2026-10-14)—— 同口径,差 -3.4bp DXY = 102.061 | 新浪 DINIW cross = ECB 欧元/美元 1.12(2026-10-14)—— 与主源不是同一口径 ⇒ 只并列参考、不做等价核对 diag = [OK] 新浪×2 |[OK] 美国财政部 |[OK] ECB 预算落盘 = {"home.treasury.gov":1,"data-api.ecb.europa.eu":1} ``` ### 二、robots 门禁与请求预算(新增 `src/host/robotsGate.ts`) `gatedFetch` 是宏观请求的**唯一入口**: 1. 先读 `robots.txt`(24h 缓存;支持 UA 分组、`Allow`/`Disallow`、`*` 通配、`$` 锚点、**最长模式优先**)⇒ `Disallow` 路径**零请求**并把原因写进 `diag`。 2. **每主机 ≤5 次/小时**,超限零请求并说明;计数写进 `macro-log.json.req`,**进程重启继续算**。 3. robots 取不到 ⇒ 视为无声明限制但**留痕**。 交叉核对规则:美债 10Y 与官方日频**同口径**(容差 15bp)⇒ 不一致时**两个值都列出**;美元指数与 ECB **不同口径** ⇒ **只并列参考**(不把口径差异说成数据冲突)。变化量仍**只用自采样**;24 小时品种不套 A 股休市冻结;**独立熔断键**,宏观失败不牵连汇率与规模取数。 ### 三、顺手修掉一个真 bug 自采样写入改为**文件锁内的读-改-写**(`updateJsonLocked`)。实测发现 fire-and-forget 写会与其它写者互相覆盖(**历史曾被空 `days` 覆盖**)。 ### 四、删除两处冗余文案(用户要求) 删掉说明段的「自定口径 + 与参考版不可比」限定语与摘要卡的「规模只作展示,不进权重」;**两条事实保留在**资金蛋糕卡的 `noteTitle`(悬停/aria)与蛋糕温度/流动性支持度磁贴的 `source` 里(注入 K9/K10/K11 必败)。 ### 验证 `npm run check` **659 / 659 × 2 时区** + selftest + build。 **注入 19/19 全抓**:K9–K11(文案删除与保留位置)|K12 交叉不一致静默取一个|K15 Yahoo 接回|K16 FRED 接回|K17 绕过 robots|K18 预算失效|K19 BIS 进交互路径 —— 全部必败且失败原因指向对应断言。 新增 `robotsGate.test.ts`(解析/通配/最长模式/预算窗口滑动/反例);`globalFlow.test.ts` ⑤ 扩到 6 个场景(一致 / 口径不同必标注 / 一源不可达仍出值 / 全不可达 `null` + 原因 / 不一致两值都列 / robots 禁 ⇒ 零请求 / 预算满 ⇒ 零请求)+ ⑥「源纪律代码扫描」(Yahoo/FRED 不得出现、BIS 不得进交互路径、门禁与预算必须在链路上)。 ### 未做(已记录) - **真机三块仍是「未取到」**:根因是**宿主插件未重载**(`dsh web` 启动于 15:57;真机快照 `missing` 里没有宏观条目;同代码在进程外取数正常)⇒ **需一次宿主重载**。客户端现在会明说「宿主载荷缺字段(宿主插件尚未重载)」。 - BIS 有效汇率 dataflow id 未确认(要接先列 `…/api/v1/dataflow/BIS?format=jsondata`)。 - 财政部日频与实时盘中值有小时级时差(已标注日期,容差外一律标「不一致」)。 - 汇率三源(`Frankfurter` / ECB `eurofxref` / 中国货币网)本批未接(不在本次需求内,侦察已确认可达)。 ## v0.88.0 — 蛋糕工具 KPI 磁贴改为「蛋糕温度 / 流动性支持度 / 美国十年期国债收益率 / 广义美元指数」 报告 `docs/AUDIT-IMPL55.md`。`npm run check` **652 / 652 × 2 时区** + selftest + build(73 片段全过);`build.mjs` 零改动。 ### 一、移除常驻的「旧口径快照已作废」提示 常驻行只留 `数据时刻 …|<降级标记>`;作废句(含天数与日期示例)**保留在状态行 `title`/`aria-label`**。原四块(偏好榜首 / 五市场合计规模 / 近五日轮动 / 数据时刻)在同一悬停里以一行保留。断言:常驻行不得出现该句与被作废日期;悬停必须能读到同一事实。 ### 二、四块 KPI 磁贴(对齐参考版) | 磁贴 | 来源与口径 | | --- | --- | | **美国十年期国债收益率** | 新浪 `globalbd_us10yt` 实测 **5.156%**(GBK,需 Referer);东财 `171.US10Y` 本机不可达 ⇒ 只做**尽力而为的交叉核对**并如实记一条 `transient`,主源照常出值 | | **广义美元指数** | 新浪 `DINIW` 实测 **102.1181**(东财 `100.UDI` 同上,仅核对) | | **蛋糕温度** | 五市场**偏好分**(0–1)均值 × 100,**与权重同源**(55% × 60 日区间分位 + 30% × 5 日动量 + 15% × 20 日动量);状态词 `≥70 偏热 / 30–70 平衡 / ≤30 偏冷` | | **流动性支持度** | `5 − 2.5 × (clamp(Δ美元指数/1.5, −1, 1) + clamp(Δ美债10Y(pp)/0.25, −1, 1))`,0–10 越高越支持;变化量取 5 个采样交易日 | - 两块宏观磁贴按 **24 小时品种口径**,**不套 A 股休市冻结**(断言扫源码块 + 周六载荷仍给真值);**独立熔断键** `hq.sinajs.cn/macro` ⇒ 宏观失败不牵连汇率与规模取数。 - **压力是否已传导**判定:美元与美债**同步走强且蛋糕温度同期下降** ⇒ 「已传导」;可算但不同时满足 ⇒ 「暂未传导」;输入缺 ⇒ 「数据不足」+ 原因。 - 两处自定公式写在**代码注释 + 界面悬停 + 报告**三处,并写明**自定口径 / 与参考版不可比**(其公式未公开)。 - 工具声明标签**零阿拉伯数字**(红线)⇒ 声明写「美国十年期国债收益率」,带数字的 `10Y` 只出现在有数据的悬停里。 ### 三、变化量一律自采样(重要) 两个宏观源**都没有**可无歧义解读的「昨收」字段(`DINIW` 的 `[1]/[2]/[8]` 同值;`US10Y` 的 `[8]` 与东财 `f4` 对不上)⇒ 「较上日 / 5 日变化」**一律用自采样**,**绝不用来源不明的字段充当变化量**。新增 `macro-log.json`(每交易日一条、保留 120 天);落盘尽力而为、不阻塞载荷;读不到则**拒绝写回**(与快照同纪律)。README 数据文件表已补该文件。 ### 验证 `npm run check` **652 / 652 × 2 时区** + selftest + build。 **注入 8/8 全抓**:K1 把蛋糕温度写成上游符号|K2 把自定公式标成「上游口径」|K3 套 A 股休市冻结|K4 缺数据写 0|K5 旧四块消失|K6 作废事实两处皆无|K7 作废句回常驻行|K8 宿主层套交易时段。 真机跑(隔离 `DSH_HOME=/tmp/kpi-1`):拿到 `us10y = 5.156`、`dxy = 102.1181`、`macro-log.json` 落盘、东财核对如实记 `transient`。 ### 未做(已记录) - **「较上日」与「5 日变化」依赖自采样** ⇒ **首日只有真值,delta 显示「未取到 + 原因」**(约一周后 5 日变化才成立)。 - 东财交叉源本机不可达 ⇒ 只做核对,**不做**「两源平均」之类无依据的合成。 - 参考版公式未公开 ⇒ **数值不可比**(已写进三条界面文案与报告)。 - 浏览器实机未验证(未重启约束);磁贴悬停只有模型层与渲染源码断言。 ## v0.87.0 — 参考竞品 UI/UX 纪律与令牌:采纳 5 / 改造 3 / 拒绝 4;修一处文案与实现不一致 依据 `docs/RECON-52ETF.md`(只读侦察)与 `docs/DESIGN-UIUX-ADOPT.md`(采纳矩阵)。`npm run check` **646 / 646 × 2 时区** + build(73 片段全过);`build.mjs` 零改动;**新增主题令牌仅 5 个**(4 玻璃 + 1 缓动),两档均有值。 ### 采纳 5 1. **玻璃浮层做法**(半透明 + 背景模糊 + 饱和度)。 2. **动效四条纪律**:只动 `transform`/`opacity`;关闭即 `display:none`;切后台暂停;尊重 `prefers-reduced-motion`。 3. **1px 间距粒度**。 4. **自适应卡片** `minmax(148px,1fr)`。 5. **密集字号阶梯** `12 / 12.5 / 13 / 13.5 / 14`。 ### 改造采纳 3 1. **字号阶梯收敛**:详情页 17 处 10.5/11/11.5px 全部抬到 ≥12px;并划出「≥15px = 展示级,不受阶梯约束」的边界。 2. **玻璃令牌重命名 + 主题双档 + 单一来源**:`toolDetailTheme.GLASS` 是唯一来源,CSS 变量必须与它逐项一致。 3. **多形态只采纳约定**(「同一份数据、不新增取数路径」),形态本身不做(见未做)。 ### 拒绝 4(含两条要点) 1. **不接它的数据源**:东财 `robots.txt = Disallow: /` 是明确信号。 2. **不硬编码它的涨跌色/暗底/主色**:涨跌色必须跟随主题与 `prefs.redUp`;样式表出现 `#f22423`/`#00a000`/`#e3534f`/`#56b779`/`#262931`/`#3c7eff` 任一 ⇒ 测试红(注入实测红)。 3. 不抄它的窗口体系(需浏览器扩展能力,宿主没有这些权限)。 4. 不引 ECharts / D3(零新增依赖红线)。 ### 令牌映射(我们的令牌 ← 竞品事实值) | 令牌 | 竞品事实 | 深色档 | 浅色档 | | --- | --- | --- | --- | | `--tw-glass-bg` | `rgba(13,17,25,.92)` | 照录 | `rgba(255,255,255,.86)`(自定;浅色档抄深色 ⇒ 断言红) | | `--tw-glass-blur` | `20px` | `20px` | `20px` | | `--tw-glass-saturate` | `1.5` | `1.5` | `1.4` | | `--tw-glass-border` | `rgba(255,255,255,.09)` | 照录 | `rgba(15,23,42,.08)` | | `--tw-ease` | `cubic-bezier(.16,1,.3,1)` | 照录 | 照录 | 玻璃只用在**浮层**(图表悬停提示框、行情悬浮卡,均带 `data-open`);规则放样式表末尾(同特异性靠后写者胜,有顺序断言)。 ### 动效纪律落地 `@keyframes tw-breathe` 从动 `background`(每帧 paint)改为 `opacity .3→1`;`.tw-glass[data-open=false]{display:none}`;根节点 `data-idle`(复用宿主 `visible` + `document.visibilitychange`,纯函数三态可测)⇒ `[data-idle=true] * { animation-play-state: paused }`;`.tw-glass` 有专属 reduced-motion 块。**取数行为未动**。 ### 合规自查(竞品反面教材) 竞品隐私说明与实际清单**不一致**(说明称仅 `activeTab`、不用本地存储、不要账号;实际要 6 个权限、11 个域名、泛域名脚本,且有账号体系与本地令牌)。 我们对 9 条「README 文案 ↔ 代码行为」逐项核对,**发现并修掉 1 处不一致**:README 的「数据文件」表漏了 3 个真实落盘文件(`trading-calendar.json` / `global-flow.json` / `market-extras.json`)⇒ 补齐,并加**可持续断言**:宿主代码里每个 `*.json` 文件名必须出现在 README 表里(删行 ⇒ 测试红,已注入验证)。其余 8 条一致。 ### 验证 `npm run check` **646 / 646 × 2 时区** + selftest + build。 新增 `src/client/uiuxDiscipline.test.ts` 7 条(字号阶梯 / 间距刻度 / 自适应栅格 / 玻璃令牌 + 竞品色禁令 / 动效纪律 / 文案一致 / 接线),**文件级注入 16/16 全部被抓住**(真改源码 → 跑真测试 → 立即还原 + sha256 一致)。失败断言原文示例:`字号越出阶梯:ToolsPage.tsx 有 13.2px`、`TOOL_SPACE.sectionGap 必须落在 1px 刻度上`、`light 档缺少或写错令牌`、`@keyframes tw-breathe:动画只允许 transform / opacity(实得「background」)`、`玻璃浮层的关闭态必须是 display:none`、`页面切后台必须能暂停动画`、`README 的数据文件表漏了 global-flow.json`。 ### 未做(已记录) - **迷你窗 / 网页内玻璃浮窗形态不做**:需先确认宿主是否允许第二实例或浮层挂载点、状态同步走主机半区且不新增取数路径、整页开关归属;方案已写进 `DESIGN-UIUX-ADOPT.md` §4-1。 - 竞品两套主题色板只用做法不用值(我们的主题已更完整)。 - 玻璃观感的**像素级确认需用户看图**(浅色档 86% 透底是否够实)。 - 字号阶梯本轮只收敛**详情页**;插件其余界面仍混用 9–11.5px(全站收敛影响面大,建议另开一批)。 ## v0.86.0 — 护盘信号优化:删恒 0/不可用因子、删死等级、加交易时段闸门、连续评分 依据 `docs/DESIGN-RESCUE-VALIDITY.md`(效度复核)与 `docs/DESIGN-RESCUE-OPTIMIZE.md`(9 条来源);落地记录 `docs/AUDIT-IMPL54.md`。`npm run check` **639 / 639 × 2 时区** + selftest + build(73 片段全过);`build.mjs` 零改动。 ### 一、删掉两个「名存实亡」的因子 1. **`resonance`(全池共振)**:实测 **302/302 恒为 0**,权重 0.08 从未生效 ⇒ 删除,其余五项按原比例归一(当时合计仍 1)。 2. **`superflow`(超大单强度,F2)**:原始输入在宽基 ETF 上**几乎恒为 0**(305 样本 p90 = **0.01×** 20 日均额、max 0.08),而锚点要求 0.5–2.0× ⇒ 分数 max 仅 15、`≥70` 的样本 **0 个**。**根因是数据源**(东财分单对宽基 ETF 覆盖稀疏),与数据通路无关。 - 处置:**剔除因子但不归一**(四项权重保持原值,合计 **0.717**),并**如实标注**「已剔除『超大单强度』因子:宽基 ETF 分单数据稀疏(p90 = 0.01×20 日均额),该因子不可用」。**不得**用常数顶替其分值。 ### 二、删掉「强护盘」这个死等级(用户确认) - 实测:**302 采样点 0 次**,且当前配置下**数学上不可达**。 - 关键判断:**不能用「把等级线乘权重合计」来让它可达** —— 剔除 F2 后实际分布几乎没降,乘 0.717 等于**人为降低门槛**,回放显示 **8 天里 5 天等级上移一档、方向单向向上**(伪信号)。「只调 L3 一条线」也**数学上不成立**(会出现 L3 线低于 L2 线的自相矛盾)。 - 处置:**等级线固定历史值 35 / 55**、等级集降为**两级信号档**(平静 / 资金异动 / 疑似护盘),粒度交给**连续评分(`raw ÷ 0.717`,0–100)+ 近 20 日分位 + 因子 N/4**。缩放开关与整条缩放路径**整体删除**(不留死开关)。 - **新增结构性断言**:**任何被声明的等级,其线必须 ≤ 可达上限**(0.717×100 = 71.7)⇒ 以后谁再声明够不着的等级会被直接挡下(反证:把 L3 线 75 塞回 ⇒ 必红)。 ### 三、交易时段闸门 + 矛盾检测 - **闸门**:非交易时段 `level/coef/raw/continuous/percentile` **全置空** + 原因「非交易时段」;不进事件、不进抽样、不计入当日最高等级(此前 `sessionElapsed('21:56') ⇒ coef 1.1`,等于**夜里用最低门槛判等级**)。面板显示「非交易时段」+ 中性色。 - **P6 矛盾检测**:`量能倍数 ≥ P90(1.57×)` 且超大单读数 ≤ 0 ⇒ 标记「拆单/算法单/被动成交」,**只进摘要,不参与 raw/score/level**。 ### 验证 `npm run check` **639 / 639 × 2 时区** + selftest + build。 **真机回放(305 样本 / 8 天)改前改后逐日完全一致**:无闸门 `{0:2,1:3,2:3}` → 同;闸门后 `{1:2,2:3}` → 同。唯一差异样本 `2026-10-13 15:02`(F2 当次 2 分 ⇒ raw 31.985→31.419 由 L1 降 L0),方向**更保守**,且该时刻在交易时段外(闸门后本就不计)。 **注入 9/9 全抓**:塞回 F2 权重 / 权重归一 / 把线缩放塞回 / **声明够不着的等级** / 声明第三级不给线 / `ruleLevel` 冒出未声明档 / 闸门恒 true / 常数顶替 F2 / 删「已剔除」标注。 `ENGINE_VERSION` 0.13.0 → **0.15.0**;三代日志关系写进报告(0.13.0 五项权重 + 线 75;0.14.0 曾试线 ×0.717 并**废弃**;0.15.0 两级 + 连续评分 + 闸门)。 共享文案:`RESCUE_LEVEL_LABEL[3]` 改为「强护盘(已删除,不再出现)」,并写明**无任何路径会产生 3**。 ### 未做(已记录) - `forward` 可交易口径(第 5 项):实现不在宿主(`grep forward src/host/rescue.ts` 无命中),需先在客户端/工具层定位。 - 连续评分未按「因子可用度」归一(`persistence` 缺 73% 时会系统性偏低;归因已给「因子 N/4」)。 - 分位窗口 = 近 20 个自然日(冷启动 < 5 样本 ⇒ `null`)。 - **P1 申购代理不做**(实测 `f84` 不是份额:`f84 × 现价 ≈ 26.5 亿` vs 规模 1036.25 亿,差 39 倍)。 - **P7「信号后上涨概率」不做**(外部反证:主力净占比分档的价差在控制当日涨跌幅后基本消失)。 - P2 同刻度 Z 分数:排队。 - 浏览器实机未验证(未重启)。 ## v0.85.0 — 工具详情页贴近目标布局:横条替代表格 + 实心饼 + 播放控件 + 用户语标题 规格与证据:`docs/DESIGN-TOOL-PAGE-ALIGN.md` §13(差异评估)、§14(实现)。`npm run check` **631 / 631 × 2 时区** + build(73 片段全过);`build.mjs` 零改动。 ### 目标布局 5 条 1. **去掉内部区名**:`曲线区` / `饼图区` / `市场表` / `KPI 行` 全部换成用户语标题;曲线卡加**可见副标题**。 2. **饼图改实心**(扇形从圆心起画、无中心遮罩圆)+ 图例在饼下 + **播放/暂停按钮 + `N/30` 计数**(原先只有滑块)。 3. **市场信息由 8 列表格改为五行横条**:`市场名` + 「当前最多」徽标 → `当前相对份额 xx.x%` → 长条 → 右端「近5日」+ `升温/降温 x.x 个百分点`。**诚实信息未丢**:规模、偏好分、动量、横截面排名进 `tool-bar-detail` 与 `title`/`aria-label`;缺数据行仍给原因。**表格未删** —— 它仍是横条无数据时的回退路径。 4. **底部改为两段散文**:`当前偏好怎么判断` 与 `这份数据的边界`(`ul/li` 已去掉,且**不许复用会被隐藏的 `tw-hint`**)。 5. **段末一行结论**:`→ 近5日资金偏好显示:从 … 转向 …`。 ### 六条裁决 1. **默认档 365**:`cake.ts:44` `ranges.initial: 365`;曲线本就是 365 点本地切片 ⇒ **零新增上游请求**;断言「默认档必须来自工具声明」(注入写死 180 必败)。 2. **零数字红线恢复为整份模型零阿拉伯数字**:数字口径(55%/30%/15%/60 日)留在宿主 `FLOW_BOUNDARY`(有数据才随口径行出现);工具文件改「按固定比例加权的三项」「最近三十个交易日」;**白名单仍只有 `ranges` 的 90/180/365**。 3. **条数断言 → 事实并集**:新增 `src/client/toolFacts.ts`(15 条事实主题 + 关键词 + 等价说法),`>= 8` 换成并集核对(工具两段 + 宿主 `FLOW_BOUNDARY` ⇒ **15/15 命中**);**逐主题删除式注入 15/15 必败**。 4. **补两组断言**:`barProblems`(条高 [8,14]、圆角 [4,条高]、颜色必须 `var(--tw-)`、**不许用涨跌色画条长**、行距区间)与 `PIE.solid`(实心圆心起画)。 5. **内部名兜底**:`toolRegistry.ts:319` 起改为 `关键指标` / `份额曲线` / `最近三十个交易日` / `市场构成` / `数据边界`;断言兜底列表逐项不许出现内部名。 6. **保留项**:表格列下标断言未动(回退路径看守);播放语义写进断言(默认暂停、点击步进、**到最新即停、不循环、不发请求**)。 ### 验证 `npm run check` **631 / 631 × 2 时区** + selftest + build。 **文件级反例注入 23/23 全部被抓**(A 组目标布局 11 + B 组裁决 8 + C 组红线回归 4;真改文件 → 跑断言 → 立即还原 + sha256 一致),关键断言原文:`默认档必须是 365`、`详情页骨架里出现了数字(可能是编造的默认值)`、`事实主题缺了:11 —— 散文可以改句式,但不许删事实`、`兜底标题不许是内部名「KPI 行」`、`横条高度必须 ≥ 8px`、`横条填充色不许用涨跌色`、`饼图必须是实心饼`、`走到最新一步必须自动停下(不循环播放)`、`缺份额必须写「未取到」`、`散文段不许复用 tw-hint`。 产物复核:新增锚点 `tool-flow-bar`/`tool-bar-badge`/`tool-bar-track`/`tool-bar-delta`/`tool-bar-detail`/`tool-pie-day`/`tool-replay-toggle`/`tool-replay-count`/`tool-conclusion`/`tool-section-subtitle`/`tool-method`/`tool-legal-line` 全部命中;`initial: 365` 在、`initial: 180` **已不在**;内部名 `'KPI 行'`/`'曲线区'`/`'饼图区'` 在产物里**已无**。 ### 未做(已记录) - 浏览器实机观感未验证(未重启服务):横条宽度与饼图直径由容器宽度 + `clamp()` 决定,像素级观感需用户看图。 - 「当前最多」徽标只按**该日份额**判定;若目标还含动量或规模语义,需用户给口径。 - 横条右端「近5日」在**回放历史日**用曲线两点现算(`delta5Of`),与宿主最新日 `delta5pP` 同口径但算法路径不同(实测一致),以宿主为基准写在注释里。 - `toolFacts.ts` 只被测试引用 ⇒ 不进客户端产物(片段数不变)。 ## v0.84.0 — 折线图与饼图尺寸对齐参考版:图表卡改两列 + 折线高度有界 规格与证据:`docs/DESIGN-TOOL-PAGE-ALIGN.md` §12。`npm run check` **625 / 625 × 2 时区** + build(73 片段全过);`build.mjs` 零改动;**新增主题令牌 0 个**。 ### 病根(v0.83.0 引入) 上一版给绘图区设 `min-height: 420px` + **`aspect-ratio: 16/7`**。在整卡宽度(约 2000px)下,16/7 会算出**约 875px 高** ⇒ 折线图过高、几乎占满视口,且把饼图挤到下方。参考版是**两列**(左折线、右饼图 + 回放滑块),不是上下堆叠。 ### 改动 1. **图表卡改为两列**:`grid-template-columns: minmax(0, 1.9fr) minmax(220px, 1fr)` ⇒ 左折线、右饼图 + 回放滑块;**窄屏(≤900px)回落为单列堆叠**。 2. **折线图高度有界**:`height: clamp(280px, 26vh, 380px)`(不再使用 `aspect-ratio`)。1080p 取 280px,高视口取 380px。画布仍按 `ResizeObserver` 量到的真实像素绘制。 3. **饼图尺寸有界**:`clamp(220px, 22vh, 320px)`,正方形、居中、不随整列拉伸;画面直径 = 尺寸 × 2 × 0.46 = **202–294px**(与参考版 260–300 同量级);图例在饼下方。 ### 验证 `npm run check` **625 / 625 × 2 时区** + selftest + build。 新增 4 条断言 + 反例注入**全部必败**: - ① 折线高度区间 `[280, 420]`(注入「写死 160px」「回到整卡 `aspect-ratio`」「上限放到 600px」「只写 `flex:1`(历史 `.tw-chips-plot` 形态)」)。 - ② 两列存在且**折线在左、饼在右**(注入「左右互换」「塌成单列」)。 - ③ 饼直径 `[220, 320]` 且正方形(注入「改成 80px」「撑满整列 100%」「宽高不等」「删图例」)。 - ④ 窄屏回落堆叠(注入「删掉 900px 媒体查询」)。 同时复核上一版的动效与红线断言**仍全绿**(A 组 8/8、B 组 6/6、排版组 5/5),含「浮层缺值写 0」「reduced-motion 开关失效」「图例回到底部」「卡片重阴影」等;恢复后复跑 `exit=0`,sha256 一致。 产物复核:`tw-tool-split`、`tool-split-left`、`tool-split-right`、`tool-pie-legend`、两条 `clamp`、`max-width:900px` 全部命中;**旧契约 `16 / 7` 已不在产物里**。 ### 未做(已记录) - 浏览器实机仍未验证(未重启服务):`vh` 取视口高度,若宿主把面板塞进很矮的容器,实际高度会更贴近下限 280px —— 这正是「有界」的用意;要精确贴合容器需 `@container`,需先确认宿主结构与浏览器支持。 - 右列 `minmax(220px, 1fr)`:极窄列下饼图仍按 220px 下限显示,可能轻微溢出 —— 已由 900px 断点提前转堆叠规避。 - 悬停为鼠标交互(触屏与键盘用户可从市场表与 KPI 磁贴读到同样数值)。 - CSS 路径过渡仅在 Chrome / Firefox 97+ 生效,不支持时行为不变。 ## v0.83.0 — 工具详情页对齐参考版:排版/字号/颜色 + 绘图区填满 + 动效;回目录按钮移到左上角 规格与差异表:`docs/DESIGN-TOOL-PAGE-ALIGN.md`(262 行)。`npm run check` **622 / 622 × 2 时区** + build(73 片段全过);`build.mjs` 零改动;**新增主题令牌 0 个**(全部复用既有 `--tw-*`)。 ### 一、回目录按钮:居中整行 → 左上角圆角 Material 按钮 - 详情页**第一个子元素**(列向 flex + `alignSelf: flex-start` + `inline-flex` + `width: auto`)⇒ 贴左上、不居中、不占整行。 - 圆角 10px、内边距 `8px 12px`、图标与文字间距 8、字重 500、字号 12.5px、手型光标;小阴影(悬停升一级、按下下沉 1px 加强调色底作等效涟漪、focus 有 2px 可见环);`title`/`aria-label` 齐备。 - 新增 `src/client/toolBackButton.ts`(外观与判据的唯一来源,纯模块)。**只用内联样式** ⇒ 紧凑/隐身档不会被隐藏。 ### 二、排版对齐(用户列的 8 条全部落地) | 部位 | 规格(可检查) | | --- | --- | | Hero 标题 | 40–60px、字重 ≥ 700 | | Kicker | 由胶囊标签改为纯文字小标(大写、宽字距) | | 摘要卡 | 标签 12–13px 在上,数值 32–40px 在下,**字号差 ≥ 18** | | 卡片 | 1px 主题边框 + 圆角 ≥ 12 + **`box-shadow: none`** + 内边距 16–20(暗底靠边框分层) | | KPI 磁贴 | 新增**彩色方块色标**(≥6×6、圆角)+ 小标签 + 大数值 + 右端 delta | | 图例 | **移到图表上方居中**;色标宽高相等、`borderRadius: 50%` | | 区块间距 | 20–28px | | 常驻口径噪声 | 各区口径行移入 `title`/`aria-label`(呈现方式变化,**事实一个未删**) | ### 三、绘图区填满卡片(A 组 5 条) - 绘图区 `min-height ≥ 420px` **且必须有高度来源**(`aspect-ratio` + 按容器真实像素绘制,`ResizeObserver` 测量),消除"图与图例之间的大片空白带"。 - 渲染顺序契约:**图例在上、画布在下**。 - **历史事故防线**:`plotHeightProblems({flex: 1})`(`.tw-chips-plot` 塌成 1px 的形态)在单元断言里必败。 ### 四、动效(B 组 5 条) 1. **悬停十字线 + 提示框**:逐序列列出五个市场的真实值;**缺值写「未取到」,绝不写 0**(浮层值直接取自曲线序列)。 2. **切档过渡** 150–250ms。 3. **回放滑块拖动时路径经 CSS 平滑跟随**(120–250ms)。 4. **不新增依赖**(无图表库;用 CSS transition / SVG 路径动画)。 5. **尊重 reduced-motion**:`@media (prefers-reduced-motion: reduce)` + `[data-anim="off"]` 双开关,降级为无动画但功能不变。 ### 验证 `npm run check` **622 / 622 × 2 时区** + selftest + build(73 片段)。 **15 组反例注入全部被抓**(真改工作区文件 → 跑断言 → 立刻还原 + sha256 校验;脚本 `/tmp/align-inject.py`):A 组 4(写死 160px / 去掉 `aspect-ratio` / 拿掉真实像素测量 / 顺序颠倒)+ B 组 6(过渡 400ms / 关回放平滑 / 删 reduced-motion / 删 `data-anim` 开关 / 浮层缺项 / **浮层把缺值写成 0**)+ 排版组 5(标题缩回 17px / 摘要卡数值缩回 16px / 卡片改回重阴影 / 磁贴色标塌成 0 / 图例色标退回小矩形)。 产物复核(`lib/client.js`):`tw-tool-plot`、`tw-tool-tiles`、`tool-kpi-swatch`、`tool-hover-tip`、`tool-crosshair`、`tool-chart-legend`、`tool-legal-line`、`tool-pie-missing`、`tool-market-total`、`ResizeObserver`、`prefers-reduced-motion`、`data-anim`、`path("`、`AS OF` 全部命中。 **红线遵守**:降级/陈旧/缺失原因与边界声明**仍默认可见**;本次还把饼图缺块说明与份额合计从 `tw-hint` 改为内联样式 ⇒ 紧凑/隐身档也不会被隐藏(6 条 `tool-*-` 锚点的负向断言)。数字与数据来源未变。 ### 未做(已记录) - 浏览器实机观感需用户看图确认(约束:不重启服务)。 - CSS `d` 过渡仅在 Chrome / Firefox 97+ 生效;不支持时**行为不变**(逐帧切换、不闪)——设计内降级。 - 悬停是鼠标交互;触屏与键盘用户可从市场表与 KPI 磁贴读到同样数值(浮层不独占信息)。 - `tools/cake.ts` 未改(kicker 与说明段文案不变,排版全部由令牌承担;口径事实一个字未动)。 ## v0.82.0 — 五市场权重改「偏好分」口径 + 大盘页新数据源接线 + 删除【指数行情】 报告 `docs/AUDIT-IMPL53.md`、`docs/AUDIT-IMPL52.md`、`docs/AUDIT-IMPL51.md`。`npm run check` **611 / 611 × 2 时区** + build(73 片段全过);`build.mjs` 零改动。 ### 一、五市场权重:从「资产规模」改为「偏好分」(用户要求贴近参考版) **原因**:旧口径用宽基 ETF 资产规模 ⇒ SPY 约 8223 亿美元 vs 其余四家各 20–30 亿美元 ⇒ 权重天然 91% 对几 %,曲线被压平。参考版权重(日本 34.9% / 韩国 22.0% / 美国 20.8% / A股 11.7% / 港股 10.6%)明显不是规模口径。 **新口径**:贪婪值 = 自身 60 日区间分位;偏好分 = 55% × 贪婪值 + 30% × 5 日动量分位 + 15% × 20 日动量分位;**份额 = 偏好分 ÷ 有值之和**。规模仍在 KPI 磁贴与市场表展示,**不再进权重**。副作用(好的):分位与动量无量纲 ⇒ **权重不再依赖汇率**(汇率只影响规模折美元展示)。 **新旧对照与参考版差值**: | 市场 | 旧(规模口径) | 新(偏好分口径) | 参考版 | 差值 | | --- | --- | --- | --- | --- | | 美国 | 91.20% | **45.52%** | 20.8% | +24.72 | | 日本 | 2.62% | **29.44%** | 34.9% | −5.46 | | 韩国 | 2.61% | **15.84%** | 22.0% | −6.16 | | A股 | 1.77% | **9.20%** | 11.7% | −2.50 | | 港股 | 1.80% | 本轮未进分母(腾讯日线 501) | 10.6% | —— | **如实说明**:参考版只公开了「情绪与趋势」这半句,**它如何给出 34.9%/20.8% 没有公开依据**,我们的口径无法逐项复现;报告列了两组候选权重(A/B)与各自读数。**未按单一样本手调系数**(反过拟合红线)。 ### 二、大盘页新数据源接线 新增只读路由 `/tradewatcher/market-extras`(登记在 `market` 行;关该行即 404),把宿主三条能力接到界面: | 项 | 结果 | | --- | --- | | 涨停 / 跌停家数 | **真值**:涨停 **67** / 跌停 **8**(`push2ex` 池)。悬停带口径、来源、时刻、池内前 5 只样本 | | 昨日成交 / 较昨日变动 | 沿用本地快照 `prevAmount`;缺 ⇒ 未取到 + 原因(不得 0) | | 预测全天 | **只在盘中给值**;收盘后回落**实际值**并写明「不再给预测」;口径(曲线/样本天数/生成时间/区间/进度/相位)随值回 | | 两融余额 | 深交所官方 + 东财**双源**;不一致 ⇒ `mismatch` + 相对差 + **两个读数都显示**;本轮实测单源(深所 null / 东财 25252.96 亿)⇒ 如实显示单源 | | 日内家数 | 仍无源 ⇒ 保留「未取到 + 原因」 | 宿主侧 `src/host/marketExtras.ts`:全部走 `withHostBreaker` + 缺失四态;池接口 `rc != 0` / `data: null` 当「未取到」而**不是**「0 家涨停」;**不假设 `push2*` 长期可用**。live 顺带修掉两处口径错误:东财两融余额列是 **`RZYE`(元)**(`NEW` 不是余额,拿它交叉会得出 186% 的假不一致);**`push2ex` 的 `sort` 是必需参数**。 ### 三、删除【指数行情】模块(用户要求) 整块删除(标题 + 6 列表格 + 点行看详情接线 + `cnIndices`);`tsc --noEmit` 全绿 ⇒ 无无引用符号。归属复核:`MARKET_PAGE_SECIDS` 由 6 收窄为 2(符合该文件「不渲染就不轮询」原则)。删除后「上证/深成」两值在页内彻底不再出现(断言为负向,含回填式写法)。 ### 验证 `npm run check` **611 / 611 × 2 时区** + selftest + build(73 片段)。反例注入全败:权重退回规模口径 / 汇率影响权重 / 缺市场当 0 / 口径行删掉 / 预测缺口径 / 收盘后仍给预测 / 两融只显示一个源 / 缺昨日写 0 / 路由未登记 / 加回指数行情块 / 加回上证卡。 **两处过程记录(如实)**:① 宿主批第一版用 `toISOString()` 当日界 + 30% 的裸 `fetch` 是注释文本触发防线 ⇒ 两条均由**已有全仓防线**自动抓到并修;② 界面批第一版把**共用**样式类名当删除痕迹 ⇒ 误报,已改用该块独有标记。 ### 未做(已记录) - `marketExtras.ts` 缺「最后一次成功 / 降级」字段 ⇒ 界面看不出数据新鲜度(建议补 `lastOkAt` 或 `degraded`)。 - 两融「不一致」与预测「盘中」两条路径未能用真上游复现(此刻单源 + 收盘),只有夹具断言。 - 日内家数序列仍无源(宿主实测不可得),需自采样管道。 - 未做真页面复核(未重启)。 ## v0.81.0 — 界面两批:工具箱目录页精简 + 详情页三段式(对齐参考版);涨跌分布模块重排 报告 `docs/AUDIT-IMPL50.md`、`docs/AUDIT-IMPL51.md`。`npm run check` **590 / 590 × 2 时区** + build(73 片段全过);`build.mjs` 零改动。 ### 一、工具箱目录页:返回控件与搜索框**连根删除**(用户判为冗余) - `ToolsPage.tsx`:删页头整块(返回按钮 + `BackIcon` + `PAGE_BACK_ACTION`)、搜索框、`query` 状态、`props.onBack` ⇒ **组件不再接任何 props**。 - `toolText.ts`:删 `PAGE_BACK_ACTION`、`SEARCH_PLACEHOLDER`。 - `toolRegistry.ts`:删 `searchPlaceholder`、`searchTextOf()`、`matchesQuery()`、`catalogModel` 的 `query` 参数与过滤层。 - **删除文件** `src/client/toolsNav.ts`(只服务那个按钮)。 - `index.tsx`:删 `toolsNav` import、`TOOLS_PAGE`、`lastNonTools` ref、`backPage`、`onBack`(不删就留"传了没人用"的死管道)。 - **保留**详情页 `← 工具目录`(回目录唯一入口)。 - 断言分四层(源码含 aria/title、接线、文件与导出、模型)+ **5 条注入全败**。 ### 二、蛋糕工具详情页:Hero → 摘要卡 → 图表卡(对齐参考版) - **Hero**:kicker + 大标题(工具名逐字)+ 说明 + 右上角 `90 / 180 / 365` 分段控件。 - **摘要卡 ×2**:`资金蛋糕`(五市场合计规模 + 集中度)+ `当前偏好`(份额榜首 + 轮动)。 - **图表卡**:标题 + 常驻状态行 + `AS OF <日期>` + **四个 KPI 磁贴** + 回放滑块 + 主图(图例在下方、纵轴在右侧、缺数据点画断)。 - **KPI 磁贴全部来自真实数据**(规模榜首 / 五市场合计规模 + 两条汇率 / 近五日轮动 / 数据时刻);**未引入**参考版的「蛋糕温度」「流动性支持度」(断言禁用这些词,常量磁贴反例必败)。 - **时间范围**:宿主一次构建到 365(腾讯日线 260 → 425 条),客户端**本地切片** ⇒ 切档 **0 请求 0 重算**。真网络实测三档:90 点 `06-09→10-13`、180 点 `02-03→10-13`、365 点 `2025-05-19→10-13`,份额合计均 100.00%。档位集合与宿主 `FLOW_CURVE_WINDOWS` 有跨模块一致性断言(防两处漂移)。 - **常驻文案压缩**:只留「数据时刻 + 降级/陈旧标记」四类;取数方式、曲线构成、窗口、留痕全部进 `title`/`aria-label`。**边界声明仍默认可见**(`data-role=tool-legal-line`,不复用会被隐藏的样式类)。 ### 三、涨跌分布模块重排(对齐参考版) - 结构:`两列` ⇒ 左列三段(`涨跌趋势` / `成交量` / `两融走势`)+ 右列两块(`涨跌分布` 直方图 / `日内家数`);每段「小色条 + 标题」。 - `涨跌趋势`:**一行 5 个内联统计**(原大磁贴已删除);`上涨占比` 压成一行小字(口径与样本进悬停)。 - `成交量`:一行 4 个内联统计(变动值按 `redUp` 着色)。 - 直方图**补纵轴 + 虚线网格**(值域按数据取整到 1/2/5×10ⁿ,柱高按轴上界归一,与网格对齐)。 - **两张 `上证`/`深成` 大卡删除**(值在移出的指数表里仍可见 ⇒ 事实没丢);**指数表移出本模块**,作为同级独立区块(自带标题、6 列、点行看详情不变)。 - 红线:六处无源项仍为「未取到」+ 悬停原因;`上涨/平盘/下跌` 无数据为 `—`;**没有为对齐布局编任何数**。 - 断言:新建 `distLayout.test.ts`(8 条)+ **6 组注入必败**(加回上证卡 / 换回磁贴 / 把「预测全天」渲染成 `0` / 去掉纵轴 / 删而非移指数表 / 删轴样式)。 ### 未做(已记录) - KPI 磁贴未接美元指数与美债 10 年期(需新增两个上游 + 降级口径,属数据层扩容)。 - 「渲染存在」只到源码层:本仓库 `node_modules` 无 `react`/`react-dom`(客户端由宿主提供)⇒ 无真渲染测试;**需刷新页面由用户复核**。 - 两融段无图区(无数据源,画空框会误导);日内家数未画折线(宿主只按交易日存快照,真画线需先加日内采样管道)。 ## v0.80.0 — 交易日历改为可手工月度维护(外部覆盖文件 + 过期可见告警 + 校验入口) 用户定:**每月手工更新一次交易日数据**。报告 `docs/AUDIT-IMPL49.md`。`npm run check` **579 / 579 × 2 时区** + selftest + build(73 片段全过);`build.mjs` 零改动(唯一非源码改动:`package.json` 加 `calendar:check` 脚本)。 ### 文件与优先级 `/dsh-tradewatcher/trading-calendar.json`,优先级 **外部文件 > 内置假日表 > 周末规则**。 ```json { "v": 1, "source": "上交所 2026 年部分节假日休市安排(上证公告〔2025〕45号)", "updatedAt": "2026-10-08", "months": { "2026-10": ["2026-10-01","2026-10-02","2026-10-05","2026-10-06","2026-10-07"], "2026-11": [] } } ``` - 选**按月列休市日**:一个月法定休市只有 0–5 天,交易日有 18–23 天 ⇒ 写得更少、更不易错。周末不必写。 - **`"2026-11": []` = 本月无法定休市但已被覆盖**;**空数组 ≠ 缺键**,缺键 ⇒ 告警 + 退回内置。 ### 过期告警(3 处,不静默) 1. 快照 `summary` 追加 `|⚠ 交易日历未更新到本月(2026-10)…`(面板直接渲染)。 2. 面板摘要下方**橙色 `⚠` 独立一行**。 3. agent 工具输出新增 `tradingCalendar` 段(来源/文件/已覆盖月份/当月已覆盖/告警)。 `source` **如实写 `builtin`**(不许显示"外部文件")。文件**不存在**不算未更新(首次使用 `warning=null`)。 ### 优先级细则与一处隐患修复 - 覆盖当月 ⇒ **以文件为准**(内置表不参与 —— 用户可用"不写"纠正过期内置条目);未覆盖/缺失/非法 ⇒ 内置表;都没命中 ⇒ 按交易日(失败方向 = **多采一天**,不会漏采)。 - 顺手修掉一处隐患:`inTradingWindow` 此前**自己重写了一遍**星期/假日判断 ⇒"文件把 10-09 标成休市"在**采样窗口快速路径**上失效(定时器照采)。现在只调用 `tradingDayState`(一处实现)。 ### 校验入口 `npm run calendar:check`:打印文件路径/是否存在/生效来源/已覆盖月份/当月是否覆盖/本月休市日/被忽略的周末条目/告警/隔离去向 + **未来 10 天判定**(预览走真正生效的判定链,不在报告里重写规则)。 schema 校验:JSON 坏 / `v≠1` / 月份键非法 / 日期非 `YYYY-MM-DD` / **日期不在所属月份** / **重复** ⇒ **改名隔离 + `.reason.txt` 留痕**后退回内置;**周末条目**不隔离但如实计「已忽略 N 条」。 ### 验证 `src/host/tradingCalendar.test.ts` 8 条全绿 + 反例注入 2 组(把"未覆盖"报告成"已覆盖/无休市日" ⇒ 两条红;把损坏文件当有效 ⇒ 一条红),日志 `/tmp/tradingcal-audit/inject/`。 覆盖:覆盖且正确(含内置表没有的 10-14;空数组 = 覆盖、缺键 = 未覆盖);未覆盖 ⇒ `source=builtin` + 告警文案 + 退回后国庆仍休市 + 快照 summary 含 ⚠;5 个非法 case 逐个隔离留痕;冲突双向(文件把内置交易日标成休市 ⇒ 休市;文件覆盖当月却不写内置假日 ⇒ 按交易日)。 ### 文档 README 新增「交易日历维护(每月一次,5 分钟)」小节:位置 / 格式 / 样例 / 为什么列休市日 / `[]` vs 缺键 / 数据来源(上交所休市安排页)/ 自查命令 / 隔离行为 / 失败方向。 ### 未做(已记录) 自动抓公告(保持手工);「列交易日」格式(只在 `v` 升版本时才加,版本不认识不猜);半日市(A 股无);内置表跨年份(2027 起需更新 `A_SHARE_HOLIDAYS`);面板「下次采样」未为外部日历单列原因文案。 ## v0.79.0 — 护盘信号:接 A 股交易日历(休市日不采样)+ 早盘等级线按时点系数缩放 报告 `docs/AUDIT-IMPL48.md`(§1 审计现状 / §2 实现与证据)。`npm run check` **571 / 571 × 2 时区** + selftest + build(73 片段全过);`build.mjs` 零改动。 ### 审计结论(问题是真的) - **法定假日此前完全没判**(周末判了、假日没判);且非交易日照样出等级并写当日分 —— 本地真实日志里 **10-10 周六 21:44、10-11 周日 12:10 各落过一条 27~30 分「平静」记录**。 - **按需路径无门禁**:`sampleNow()`(`rescue.ts:1268`)不查窗口 ⇒ 面板/路由一刷新就在周末采样出分(上述两条就是这条路径写的)。 - 时点系数 `timeCoefficient(elapsedMin)` = `≤0→0.4 / <60→0.4 / <120→0.7 / <180→1.0 / else→1.1`(09:30 起算、午休不计);只用于护盘评分。 - 等级线改前为 `score >= 75/55/35`(**分数乘系数、线固定** ⇒ 系数与线两套机制并存)。 - 数据源实测:`/tradewatcher/calendar` **只有 ipo/earnings/dividend/macro 四类事件,无交易日字段** ⇒ 不可用;指数当日成交技术可得但**假日回包今日不可观测**;上交所休市安排页本机可达(HTML)。 ### 实现 - **主判据 = 周末 + 内置 2026 法定假日表**(来源:上证公告〔2025〕45 号;国庆 10/1–10/7、**10/8 开市**)—— **离线、零请求、确定性**;失败方向刻意选为"表过期只多采一天",**不会把真交易日误判成休市**。 - `inTradingWindow` 加假日判据;`samplingWindow` 增 `holiday` 原因且 `nextAt` **跳过假日**(原先只跳周末,会把下次采样指进假期);`sampleNow` 非交易日**短路**(零上游请求);`snapshot()` 门禁 ⇒ **`level: null`**(**不是 0 = 平静**)+ `nonTrading` + 「休市(非交易日)」文案;面板/路由/工具同步。 - **早盘等级线 = 按时点系数缩放的唯一一处时点调整**:`levelLines(coef) = {35,55,75} / coef` ⇒ 早盘 **87.5 / 137.5 / 187.5**(>100 ⇒ 早盘不可能上「疑似/强护盘」),尾盘 31.8 / 50 / 68.2;`score` 仅作展示。 ### 实测数字 **40 条真实早盘样本**:新旧口径等级 **0 条差异**(无回归)。**若去掉缩放** ⇒ `10-13 09:50 / 09:55 / 10:00`(原始分 58.8 / 58.8 / 56.16)**直接跳到「疑似护盘」** = 改前等级 2 vs 改后等级 0 —— 正是用户报的早盘假信号。 ### 验证 新增 `src/host/rescueTradingDay.test.ts`(8 条全绿)+ 反例注入 2 组(假日当交易日 ⇒ 三条红;去掉缩放 ⇒ 两条红),日志 `/tmp/rescue-audit/inject/`。 覆盖:假日/周末非交易日 + **10-09 反证** + **10-08 开市边界**;下次采样必须落 10-08;`sampleNow` 0 请求 / `level === null` / `nonTrading.label = '国庆节'` / `scoreLog` 长度 0;早盘 t2/t3 > 100、尾盘线低于基准、09:35 vs 14:00 判定不同、系数扫描等级单调不减;交易日收盘后仍是定稿口径。 **一处既有测试是按错误行为写的**:`p0.test.ts`「国庆假期周五收盘 ⇒ 下次 10-05」已改 10-08 并补普通周末对照。 ### 未做(已记录) - 候选判据「指数当日无成交」**未接线**(假阴性会丢真实信号,且假日回包今日不可观测 —— 复核命令已给)。 - 半日市(A 股无此情形);假日表自动更新(年限快照,抓 HTML 脆弱);`locale/` 的「法定假日休市」多语言条目;盘前 09:25–09:30 独立系数(无数据依据,按反过拟合红线不动)。 ## v0.78.0 — 全仓审查第四批:冗余清理与文档更正(收尾) 报告 `docs/FIX-4-REDUNDANCY-DOCS.md`。`npm run check` **563 / 563 × 2 时区** + selftest + build(73 项锁定片段全过);`build.mjs` 与 `package.json` 零改动。 ### 文档更正 - README「大盘云图」自绘整节(描述的是 **v0.62.0 已删除**的功能)→ 改为「只保留第三方站点」;删除 `:422` 的 `client/treemap.ts` 条目。 - **六行 → 七行**:`README.md:344/:346` + `DESIGN-SUBPLUGINS.md:3/:56/:113/:162`(补 `tools`、`core + 6 = 7`、七条 Loader)。 - **失效行号**:`cordis.patch.yml:38-56` → **`:53-75`**,6 处全改。 - `CHANGELOG.md:191`(v0.71.0 的 C1 声明)**更正而非删除**:加「⚠ 更正(v0.78.0 文档批)」,说明原句与事实不符、本批补做,**历史条目不改写**。 - 权威性收敛到 4 处(行清单=manifest、行归属=`ROW_ROUTES`、阈值=`rescue-thresholds`、历史=CHANGELOG),README 只留指针;报告 §8 列对照表。 - 三条新约定写入 `docs/DECISIONS.md`:熔断出口 `withHostBreaker`(含「HTTP 非 2xx 也算失败」与低层 helper 豁免条件)、行归属按调用方推导、断言强度扫描(`assertStrength.test.ts`)。 - 顺手:README 页签「五个→六个」、行情条「三行→四行」并补国债行、预设组统一含 `bond`、标定数字改 `1.19/1.57/1.92`。 ### 安全删除(删前逐个 `grep -c build.mjs` = 0;删后 73 条锁定片段全过) - **9 个真死导出**(`PasteDelimiter` / `invalidateChips` / `ChipsViewState` / `resetMountCounts` / `ChipsLineKind` / `FQ_NOTE` / `flowInflightKeys` / `heldLocks` / `removeLockFileSync`,后者的"说谎注释"随之消失)+ `scripts/rescue-thresholds.json`。 - `model.ts` 死字段 `marketState`/`showing` + `styles.ts` 4 条死 CSS(`.tw-chips-bars` 的说明搬到 `.tw-chips-plot`)。 - **阈值先核实再改**:标定 `f1 = {1.19, 1.57, 1.92}`(pooled p75/p90/p95)是对的;面板那个 **`1.63` 是更早一次标定的 P90**,而它用于**极端档**(`LEVEL_COLOR[3]`)配色 ⇒ 改为 `RESCUE_F1_EXTREME = 1.92`,并新增 `src/client/rescuePanel.test.ts` 跨文件同步断言(不许回退 1.63)。 ### 谨慎处理 - 9 簇重复逻辑:**只合并 2 簇**(删死表 `FQ_NOTE`;`fmtPct` 内联箭头改调 `pctArrow`)+ 1 处零行为改名(`tones.percentileOf` → `rankPctExclusive`,与 `breadth.ts` 同名不同义;测试数值未动)。 - 空文件头:补回**被别处引用的 3 个**(`marketTime.ts`、`sina.ts`、`tencent.ts`),内容按 git 创建提交复原**并更新到当前事实**;摆正 `sina.ts` 的 import 顺序。 - 过期注释逐条修正(`index.ts` 5 处失效行号改指函数/调用点、`DESIGN-CHIPS.md:52` 行号 `:123`→`:142`、`STE-REVIEW-UI.md` 抬头加「已执行完毕、勿逐条照做」)。 ### 未做(已记录) - 其余 6 簇重复(懒加载骨架 ×4、Tab 陷阱 ×2、北京时格式化 ×3、裸 `toLocaleTimeString` ×10、筹码线型类型、`fmtAmt`/`fmtBig`)—— 按「有风险的不动」;其中裸 `toLocaleTimeString` 改的是**显示行为**(非 UTC+8 浏览器会变)。 - 其余 26 个空文件头(可复原,但需逐个核对是否仍成立;**空块是"丢过文档"的可见信号**,未删)。 - `CHANGELOG` C2–C7;`DESIGN-DASHBOARD.md` 归 archive;`escapeUpper()` 冗余分支与 `'后复权'` 弱锁(都要动 `build.mjs`,本轮硬约束零改动)。 ## v0.77.0 — 全仓审查第三批:高 4 + 中 5(熔断收口、锁重入、假断言、行归属…) 报告 `docs/FIX-3-HIGHMED.md`。`npm run check` **562 / 562 × 2 时区** + selftest + build(73 项);`build.mjs` 零改动。 ### 高 1. **熔断绕过 14 站点 → 收口**:新出口 `host/breakerFetch.ts:34 withHostBreaker`(allow → claimProbe → 运行 → 成功/失败记账 → finally release);腾讯 5、新浪 3、日历 2、护盘 2、em 2 全部收口,K 线首选源复用同一出口。 **关键一条**:`HTTP 非 2xx 也记账**(`:45`)—— 调用方把 `if(!res.ok) throw` 写在包装外时,只判"没抛异常"会让 5xx 永不进账(**第一版正是这样,实测 `fails 0→0`,已修**)。低层 `fetchFromHost`(`em.ts:88`)保留裸 fetch 并进白名单(调用方只有两个已记账入口,理由写进注释);新增全仓回归防线禁止再出现无记账裸 fetch。 2. **锁重入判据 → `AsyncLocalStorage`**(`lock.ts:397`):只有**同一条 async 链**才算可重入,兄弟调用排队 ⇒ 复核实测 X6 现值 `n=2`(旧 `n=1`)。 3. **已跳过流水不再计入当日盈亏**:`store.ts:280/296/309` replay 回报 `appliedIds`;`portfolio.ts:68` 当日切片只统计被应用的流水(空仓不再报 `dayPnl=-50`,正常路径 +50 的对照保留)。 4. **日历降级落盘**:`CalFile.lastSources` 落盘(`:49/:683`)⇒ 重启不再洗白;`count===0` 记 **`empty` 档**(`:571`,不算成功、`stale=true`、进 `missing[]`);`syncedAt` 只在**所有被尝试源都成功**时推进(`:680`)。原测试"宏观空数组也算成功"的契约按本条**作废**并写明。 ### 中 5. **假断言 6 条 → 换判据**(依赖数组真含 retry / JS 不许硬编码 cursor / 面板必须**点访问**四字段 / 原因须由**调用点实参**传入 / 两份自比较改固定字面量)。新增 `src/assertStrength.test.ts` 三道扫描(`|| true` 兜底、字面恒真、自比较;按顶层逗号切分、括号引号感知):坏样本 8 例必中 + 好样本 6 例不误报 + 注释不计;全仓 0 命中。 6. **行归属测试改为按调用方推导**:新增 `src/rowOwnership.test.ts`,自解析 `api.ts` 函数→路由 + 客户端**调用点**(先剥注释)+ import 图反向可达性,与 `ROW_ROUTES` 对齐(消费行必须**恰好**等于声明行),自带双向对照;真实文件注入(给 `TopBar` 加 `api.board`)⇒ 两条都红。 7. **采样 TTL 与休市定稿**:`loadQuoteRowsShared(…,{minReuseMs})`(`em.ts:1024/903`)+ 采样器按 tick 传窗口(`rescue.ts:1229`,尾盘 11s / 盘中 22s)⇒ 采样拍内零额外上游而界面口径不变;`em.ts:459` 定稿行(休市 + 越过收盘)不再算 stale ⇒ 不再"已收盘"掉回"延迟/限流"(盘中 20 分钟没取到仍判 stale 的对照保留)。 8. **M8 空日线不再进 12h 正缓存**(`globalFlow.ts:748`:空/过短即抛,空 ≠ 成功)。 9. **`index.test.ts` flake 修根因**:判据原读**模块级** `__kernelStats.starts`(别的用例迟到的 init 会加到这一拍头上 ⇒ 11)⇒ 改为**内核级**计数(`index.ts:99/180`)+ 静默窗口,强度不变(本内核真起两次仍红)。 ### 验证 每条断言锚真实实现;每条配反例注入并贴证据后恢复(md5 校验)。**一处自查返工**:高① 第一版把 `if(!res.ok) throw` 留在包装外 ⇒ 5xx 不记账(实测 `fails 0→0`),已修并写进报告。 ### 未做(已记录) - L4(退出钩子不校验 token,低档且未确认)。 - 熔断键仍按 host(`web.ifzq.gtimg.cn` 同时服务 K 线与分时,会连带快速失败)—— 既有行为,按 (host, endpoint) 细分未做。 - M9/M10/M11/M14/M15/M16 等其余"中"档不在本批。 - 三条新约定(熔断出口 / 行归属推导 / 断言强度扫描)**未**写进 `docs/DECISIONS.md`(属第四批文档工作)。 ## v0.76.0 — 全仓审查红线批:时区 2 条(宿主偏 8 小时 + 客户端「数据时刻」按浏览器时区) 报告 `docs/FIX-2-TIMEZONE.md`。 ### ④ 宿主:市场时钟按宿主时区解析(UTC 宿主下偏 8 小时) 新增出口 `src/host/time.ts:89 parseMarketStamp`(`"YYYY-MM-DD HH:mm[:ss]"` 与 `"YYYYMMDDHHmm"` 一律按 `+08:00`;带 `Z`/`±hh:mm` 的原样认;垃圾 ⇒ NaN)+ `shDateTimeText/shClockText/shStampText`(`:124/132/137`,固定 `Asia/Shanghai`)。 两处解析收口:`rescue.ts:843`(护盘 trends2 + fflow)、`tencent.ts:404`(腾讯分时兜底)⇒ 分时兜底刻度不再变成 **17:30–23:00**,护盘采样回填环不再偏 8 小时。 同族**格式化**侧一并改:`tools.ts:70/417/699/891/1379`、`backup.ts:217/218`(原为无 `timeZone` 的 `toLocaleString`)。 **顺带抓到自己的一个坑**:`selftest.ts:725` 用 **UTC 日期**当 `rescue-log` 的日键,而监控按**北京自然日**查 ⇒ 北京 00:30 前后自检会假红/假绿;已改 `dayOf(Date.now())`。 ### ⑤ 客户端:「数据时刻」族按浏览器时区(5 处) 新增 `src/client/marketTime.ts:82/89/94/99/104`(`shDateTimeText/shClockText/shStampText/shDayText/shToday`,`timeZone` 写死)。 5 处红线站点:侧栏徽标 `badgeView.ts:56`、行情卡 `title` `quoteState.ts:66`、K 线脚注 `chartNote.ts:41`(+`format.ts:92/110`)、日历「今天」`CalendarPage.tsx:46/187`、页面级数据时刻 `MarketPage.tsx:21/445/456`/`PortfolioPage.tsx:338`/`WatchlistPage.tsx:202`/`QuoteDrawer.tsx:466/514`/`RescuePanel.tsx:438/468/476/479`。 排查另扫出三类同族并修掉:**用 UTC 日期当自然日**(备份文件名 `PortfolioPage.tsx:595`、CSV 名 `RescuePanel.tsx:101`、交易明细成交日 `QuoteDrawer.tsx:392`、K 线窗口起点 `kline.tsx:930`)。完整逐个结论见报告 §3(含两条白名单:`trendStitch.msToDay` 的日期串往返、测试子进程夹具)。 ### 两条**把缺陷当契约**的测试已改(报告 §4) - `src/client/badgeView.test.ts:69`:原来用同一个 `toLocaleTimeString` 推导期望值 ⇒ 任何时区都过(UTC 浏览器下它接受 `01:42:52`)。改为固定字面量 `'盯盘 · 数据 09:42:52'`。 - `src/client/chartNote.test.ts:56`:`new Date(2026,9,9,9,5)` 本地构造 + 本地渲染 ⇒ 同样恒真。改为固定 epoch + 固定字面量,并加北京 `00:05` 跨日边。 ### 验证 `npm run check` **EXIT=0**:**550 / 550 × 2 时区** + selftest + build(73 项);`build.mjs` 零改动。 新增 `src/host/marketClock.test.ts`(4 条,真 `fetchTencentMinutes`/`fetchMinuteSeries`,stub 上游)+ `src/client/marketTimeDisplay.test.ts`(5 条,含**全仓静态防线**:无 `timeZone` 的 `toLocale*`、`toISOString` 当日界、本地 `getHours` 一律零容忍)。全部断言在 `TZ=UTC` 轮通过;跨时区实跑 `TZ=UTC` 29/29、`TZ=America/New_York` 9/9。 **4 组反例注入在 `TZ=UTC` 下全部必败**:R4-A(退回无偏移解析 ⇒ 证据里直接打出 "17:31")、R5-A(退回浏览器时区 ⇒ 打出 "实际 数据 01:42:52")、R5-B(脚注退回本地 `getHours`)、R5-C(新塞一处无 `timeZone` 调用 ⇒ 被静态防线抓到)。日志 `/tmp/fix2/inject/*.log`,md5 校验还原。 ### 一处 flake(如实记录) 某轮 `npm run check` 里 `src/index.test.ts:491`「`rescue.start()` 只跑一次」出现一次 `11 !== 1`;单跑该文件 17/17 通过、整轮再跑两次 550/550 通过 ⇒ 判为**负载相关的计时器 flake**(该用例靠计时器计数),与本批改动无关(本批两个文件都不启动计时器)。**未动那条测试**;是否顺手加稳定性处理待定(已并入下一批)。 ### 未做(已记录) - 没有「跟随浏览器时区」的开关(按仓库既有规矩一律北京,写死是刻意的)。 - 锁/隔离件命名保持 UTC(非市场时间);非时间类 `toLocaleString`(金额千分位)保持。 - 界面文案未加「(北京时间)」标注。 - `sh*Text` 依赖 Node full-icu(当前环境成立,未做回退)。 ## v0.75.0 — 全仓审查后的红线批:数据安全与编造数字(5 条) 三路全仓审查(`docs/REVIEW-1-REDUNDANCY.md`、`REVIEW-2-CORRECTNESS.md`、`REVIEW-3-DOCS.md`)共发现 7 条红线。本版修前 5 条(数据安全与编造数字),时区 2 条留待下一批。报告 `docs/FIX-1-REDLINE.md`。 ### R1 「读不到」≠「没有」⇒ 覆盖用户数据(3 处同族) 新增 `src/host/readGuard.ts`:只有 `ENOENT` 算"本来就没有",其它 errno 记入守卫并在**写回前**抛 `UnreadableFileError`(`store.ts:415/458/498`、`calendar.ts:352/382`、`globalFlow.ts:604-612/669`),重读成功自动解封;坏 JSON 一律**隔离原件**(原字节留档);原因接进 `syncStatus().missing` / 工具 `provenance.missing` / portfolio 回包。 断言 4 条(含"ENOENT 仍可写"的反面对照);注入 3 组(`Missing expected rejection…` / `读不到时必须拒绝写回`)必失败。 ### R2 K 线失败进 1 小时正缓存 + 理由谎称"重试无效" 失败**不进正缓存**(`em.ts:2315` 让 `ttlCache` 不写槽),只写 **10s 负缓存**;新增 `klineLastAttempt()` 记录"本轮到底发没发请求",`klineMissingReason`(`:2253-2266`)按事实给 `transient`("本轮没有向上游发请求")或 `no-source`("确实请求过")。 断言:窗口内不发请求但理由如实 + **+11s 后必须重新回源**;注入(空数组当成功值缓存)必失败。 ### R3 做T 缺价按 0 价 ⇒ 编造盈亏 `dayTrade.ts:73-78` 缺价流水**不进配对**、进 `unmatched`;`:145-156` 有未计价流水时 `netPnl/avgNet/winRate` **一律 null** + `unpricedCount`。 断言:`rounds=0`/`netPnl=null`/`unmatched.price=null`,且**价格齐全仍算出 1000**(防修过头);注入(退回 `price ?? 0`)⇒ 旧实现按 0 价配出一次往返、`grossPnl=−10000`,必失败。 ### R6 已清仓且成本未知 ⇒ 合计按 0 + 披露被 `qty>0` 门控 三处 gate 收敛到 `model.ts:1146 unknownRealizedRows()`(**不看 `qty`**);合计不再把 null 当 0(`portfolio.ts:291/302/310/319`,分组 `:335`、总览 `:347-369` 有未知行即 **null** + `realizedUnknownCount`);工具在 `includeEmpty` 过滤**之前**取披露(`tools.ts:330/366`)并写"累计已实现:未取到(含已清仓的行)";面板同样去门控。 断言用真 `assemblePortfolio` + 真工具 `execute`/`output.render`;注入 ⇒ `累计已实现必须是 null(实际 200)`,必失败。 ### R7 组合构成 1:1 混币种 `composition.ts` 输入新增**必填** `mvCny`(人民币口径),分母与权重只用它,原币种 `mv` 照旧展示,新增 `excludedNonCny` 标注;`portfolio.ts:374-383` 与 `grand.totalMv` **同一份**折算规则。 断言:真路径 `composition.totalMv === grand.totalMv === 1100`(旧 69300)、港股 `weightPct=null` 但 `mv=64000`、`excludedNonCny=2`;注入(三者 1:1 相加)必失败。 ### 连带改动(类型必须同步,否则门禁红) `src/client/api.ts`(`netPnl: number | null` + `unpricedCount`)、`src/client/PortfolioPage.tsx`(净盈亏 null 时显示「未取到(N 笔缺成交价)」;`costUnknownRows` 去 `qty>0` 门控 + 合计 null 提示)。 ### 验证 `npm run check` **EXIT=0**:**541 / 541 × 2 时区** + selftest + build(73 项);`build.mjs` 零改动。 每条断言锚真实实现;每条配反例注入并贴证据后恢复(注入日志 `/tmp/fix1/inject/`,全部 md5 校验还原)。**未手工改 `~/.dsh` 任何文件。** ### 未做(已记录) - `missing[].why` 缺"本地故障"档(枚举只有 `transient`/`no-source`)⇒ R1 用 `transient` + 明确 note 承载「不会自动重试、修好权限后重启恢复」;要真正加档需改 `shared/model.ts` + 两端渲染(跨端,未做)。 - R2 同型的 M8(`globalFlow.ts` 的"成功但空"日线进 12h 正缓存)属"中"档,未动。 - U7(归档分组与 `grand` 口径不一致)意图不明,未裁决。 - 客户端组合构成渲染今日不渲染(复核 0 命中),本批只改数据层与类型,未加界面。 ## v0.74.0 — 市场成分改为 A股/港股/美国/日本/韩国;工具箱目录页三项界面改动 ### 一、口径变更:五市场成分 v2(用户指定,与需求 T-5 一致) | 市场 | 规模(实测) | 动量 | 币种 | | --- | --- | --- | --- | | A股 | 新浪 ETF 节点 `nmc` = 1063.50 亿元(与腾讯 `f45` 交叉一致) | 腾讯 `sh000300` | 人民币 | | 港股 | **腾讯 `hk02800` `f45` = 1262.18 亿港元** | 腾讯 `hkHSI` | 港元 | | 美国 | 新浪 `gb_spy` 总市值 | 腾讯 `us.INX` | 美元 | | 日本 | 新浪 `gb_ewj` | EWJ 自身日线 | 美元 | | 韩国 | **推导值**:腾讯 `usEWY` `f71`(股本)× 最新价 | EWY 自身日线 | 美元 | - **汇率**:`fx_susdcny` = 6.6941、`fx_susdhkd` = 7.8473(同日腾讯 `whUSDHKD` = 7.8474,两源差 0.003%)。缺任一汇率 ⇒ 该市场**不进分母** + **行级原因**,不得静默按 1 折算。 - **实测读数**(22:54 真网络):A股 1.76%|港股 1.78%|美国 **91.14%**|日本 2.63%|韩国 2.69%,合计 100.00%。冷启动 15 次请求 / ~2.0s,热命中 0 次。 - 口径标识 `v2:cn,hk,us,jp,kr`。 ### 二、快照兼容(两级校验 + 留痕上界面) 快照文件升 `v=2` + `scope`;**文件级 + 逐日级**两级校验,任一不符即作废**并留痕** `purged[]`(只增不删,落盘写回),留痕**上界面**(状态行「⚠ 另有 N 天旧口径快照已作废…」)。实测:手写 v1 快照(含 `eu`/`em` 键)⇒ `days={}`、`purged` 1 条、旧日期不进曲线;「v2 文件头 + 一条 v1 记录」的混合文件 ⇒ 只保留合规那天。 ### 三、三条已知取数限制(如实记录) 1. **港股规模单点依赖**腾讯 `hk02800`(新浪港股侧**没有任何规模字段**,6 个节点候选全空或 `s_auth FAILED`)。 2. **韩国规模是推导值**(新浪 `gb_ewy` 的市值与股本字段实测恒为 0)⇒ 用 `f71` 股本 × 最新价;`f71` 语义有独立证据(`usEWJ f71` 与新浪股本**逐位一致**;`usSPY f71` 与新浪差 0.06%)。**行级已标注推导**;新浪补字段后自动切回直接口径。 3. **KOSPI200 无日线源**(`us.KOSPI200`/`kr.KOSPI`/`hkKOSPI200`/`us.KOSPI` 全 `pv_none_match`,`fqkline` 0 根)⇒ **韩国动量用 EWY 自身**,写进边界声明。 ### 四、工具箱目录页三项界面改动 - **页头那一行删掉**:`工具目录` 标题与 `本插件自带 N 个工具` 整行删除;模型字段(`CatalogModel.title`/`subtitle`)与常量 `CATALOG_TITLE` 一并移除(不留无人渲染的死字段)。只留**最小返回控件**(内联 SVG 图标 + `返回`,成对有断言)。 - **工具名/chip 逐字锁定**:`全球资金流蛋糕模型` / `宏观 | 全球轮动`(两条逐字断言 + 改名反例)。 - **卡片**:`toolCardView.ts` 新模块(`CARD_TAG='button'`、`CARD_PROPS={type:'button',tabIndex:0}`、`CARD_STYLE` = `var(--tw-shadow-lg)` + `borderRadius:12`,其余走主题 token)⇒ 阴影圆角 + **整卡可点** + 键盘可达 + `aria-label` = 工具名;**页脚 `打开工具 →` 已删**(`ToolCard.action` 字段一并删除)。 ### 五、验证 `npm run check` **EXIT=0**:**532 / 532 × 2 时区** + selftest + build(73 项);`build.mjs` 与 `styles.ts` **零改动**。 **反例注入 24/24 被抓**:界面批 10 组(页标题加回、改名、改 chip、`boxShadow:'none'`、`borderRadius:0`、`CARD_TAG='div'`、`tabIndex:-1`、入口文案加回、不用 `CARD_STYLE`、不用 `CARD_PROPS`)+ 数据批 14 组(改名一字、边界残留旧成分、分母算入缺值、缺汇率按 1 折算、两级校验全关、只关逐日级、留痕不上界面、曲线少一条、饼图多切旧成分、汇率校验退回旧正则、港股读错字段、韩国来源失联)。每条均贴失败断言、还原后 sha256 校验并复跑全绿。 **两处第一轮没抓住(如实记录)**:`N5` 只关文件级校验时被逐日级兜住 ⇒ 改成两级全关 + 另立单测;`N12` 把韩国改回新浪时因兜底路径接管而通过(两条路等价,**不是**缺陷)⇒ 改成「来源整体失联」才锁住。 ### 六、未做 - `tools` 行需重启才装载(已在发布后的重启中生效)。 - 浏览器实机观感(阴影/圆角)只能由用户在界面确认。 - 旧名残留于 `host/routes.ts`、`host/globalFlow.ts`、`host/globalFlow.test.ts` 的注释(非用户可见)。 ## v0.73.0 — 修 Hy4 复核发现的问题(含两条假断言)+ 边界声明补全 跨模型对抗复核(`cn:hy4`,报告 `docs/AUDIT-HY4-1.md`)推翻 v0.72.0 的部分结论。本版逐条修复。 ### ① 红线:全缺场景出现 0 顶替 + **一条恒真断言** - 五市场全部失败且无快照时,脚注原显示「份额合计 = 0.00%」⇒ 改为「未取到:这一天没有一个市场拿到规模 ⇒ 份额合计无从谈起(分母里没有有值市场);缺口原因见下面每一行」。 - 顺带修同一场景的**第二个问题**:全缺时不再把当天登记成"实测日"(否则曲线上出现一个每值皆 null 却标"实测"的点,还会顶掉空态)⇒ 现在曲线 0 点、走「未取到 + 原因」空态。 - **旧断言用 `^0` 锚定 ⇒ 恒真、抓不到**。已换成「整串不许出现 `0.00%` + 必须含未取到 + 必须含原因」,并把判据下沉为单点实现 `toolText.looksLikeZeroStandIn()`,两个测试文件各配**自证非恒真**的控制断言(`0.00%`/`0.00 亿美元`/`-0.00` 必须被抓到;`未取到`/`+0.01pp`/`0.00pp` 不许误报)。 ### ② 行级原因 + 人民币原值 - 规模单元格:`1063.50 亿元 ≈ 158.87 亿美元`;缺汇率 ⇒ `1063.50 亿元(汇率未取到,未折算)`。列头由「规模(美元)」改为「规模」(避免"列名美元、格子人民币"的错读)。 - 新增 `rowReasonOf()`:缺汇率时该行说明写清「已取到人民币规模,但本次汇率未取到 ⇒ 不做美元折算、不进美元合计」。旧断言里的 `||`(让行级那一半永远不成立)拆成载荷/单元格/行级三条独立断言。 ### ③ 「实测日的洞不许用重建值补」补上断言锁 新断言:`buildCurve` 里"有快照的那天缺的市场必须是 null(真洞)",并配**对照组**证明"补洞"能力在实现里存在(同一天无快照时会被重建出来)。复核的原注入现在必败。 ### ④ 边界声明:补全 + 默认可见 + 去重 - **逐字**写入需求原文那句「这是风险偏好代理,不是真实跨境资金净流入」(`REQ-tools-component.md` T-5;断言按逐字匹配),放在工具文件 ⇒ **无数据时也在**。 - **可见性**:边界声明、状态行、逐行缺口原因**不再复用 `tw-hint`**(`.tw-root[data-view=compact|incognito] .tw-hint{display:none}`)⇒ 任何视图档默认可见;三个 `data-role` 各配一条"不许复用 tw-hint"注入。**`styles.ts` 零改动**(改用内联样式)。 - **去重**:工具声明改讲"模型是什么/不是什么",宿主 `FLOW_BOUNDARY` 只讲数字口径与数据源事实 ⇒ **12 条零重复**(原 13 条里 5 组近似重复)。 ### ⑤ 失败不进缓存 + provenance 修正 - 汇率/资金流的 loader 改为**抛错**、catch 搬到 `cached` 外层 ⇒ 失败不进 30 分钟 TTL;断言「上游恢复后必须重打」(旧行为读数:仍停在 1 次)。顺带把载荷 TTL 的时间基准改为传入的 `now`(此前注入 `now` 会永远命中缓存)。 - 新增 `source='fallback'`:回退态状态行说明「本次上游全部失败 ⇒ 下表与曲线来自最近一次本地快照(某日),不是实时值」;**无快照**时改为「上游本次全部失败,且本地还没有任何快照 ⇒ 这一屏没有数据」(旧文案原是不可达死分支)。 ### 附带两条(复核 §9) 自校告警(总市值 vs 最新价×股本 偏差 >5%)从死代码接出 ⇒ 有值也推进该行 `missing` 且界面可见;`|值| < 0.005` 归零,不再显示 `-0.00pp`。 ### 断言强度排查(Lead 要求) 扫 5 个测试文件共 **438 条 `assert.*`**:除复核发现的 `^0` 恒真外,**新发现 1 处同类** —— 0 顶替扫描用两端锚定 `/^[+-]?0(\.0+)?\s*%?$/` ⇒ 抓不到 `0.00 亿美元`/`0.00 亿元`(已换单点判据 + 自证);另补 2 处"空集合真空通过"的长度前置断言、1 处 `.some(...) === false` 的空集合前置断言。其余候选逐条复核后判为**非恒真**,保留不动。 ### 验证 `npm run check` **EXIT=0**:**523 / 523 × 2 时区** + selftest + build(73 项);`build.mjs` 与 `styles.ts` **零改动**。 **反例注入 12/12 被抓**(真改源码 → 跑真测试 → 立即还原 + `sha256sum -c` 6 文件全绿):A 脚注退回 0.00%|L 全缺仍登记实测日|B 不显示人民币原值|C 行级原因不消费汇率缺口|D 实测日补洞|E 去掉免责|F 边界声明重复|G 复用 `tw-hint`|H 汇率失败进缓存|I 回退谎称只读快照|J 自校告警回死代码|K 去掉负零归零。其中 **L 第一次注入 exit=0**(无断言看守)⇒ 补断言后同注入 exit=1,过程记入 §8。 **真网络复核**(22:27,隔离 `DSH_HOME`):14 次上游、`source=live`;A股 行 `1063.50 亿元 ≈ 158.87 亿美元`;脚注 `份额合计 = 100.00%(分母只含这一天有值的 5 个市场)`;去重后仍 12 条、免责在第一条。 ### 待确认(Lead 已定,记录在案) `REQ-tools-component.md` T-5 写「5 个市场(A股、港股、美国、日本、韩国)」,与任务书「用户已定:美股、欧股、日股、A股、新兴市场」**不一致**。实现按**任务书**执行(用户在本次对话中显式指定,晚于该文档)。**口径若要以文档为准 ⇒ 属口径变更,不是缺陷修复。** ### 未做 - `tools` 行需重启才装载(已在本次发布后的重启中生效)。 - 各区口径行/表脚注/曲线图例/回放说明仍随紧凑档隐藏(隐藏不等于误导;只有"来源与回退态、逐行缺口原因、免责声明"要求默认可见)。 - incognito 的金额模糊不作用于本工具的公开市场数据。 - 仍无浏览器像素级验证(可见性链条 = 源码层 + 接线断言 + 注入 + CSS 事实)。 ## v0.72.0 — 全球资金流蛋糕模型:数据层与详情页接通真数据(工具箱首个工具) ### 份额口径(用户已定;写进代码注释与 `docs/AUDIT-IMPL45.md` §2) - **份额 = 市场规模 ÷ 有值市场规模之和**。缺的市场不进分子也不进分母 ⇒ 有值部分恒 100%,该行显示「未取到 + 原因」,**不用 0 顶替**。 - 规模(AUM):美股 SPY / 欧股 VGK / 日股 EWJ / 新兴 EEM 取新浪 `gb_*` 的**总市值**(用户口中的「字段 13」是 1 起数 = 下标 12;**下标 13 实测恒 0,是市盈率**);A股取新浪 ETF 节点 `nmc`(实测 1063.50 亿元,与腾讯 f45 一致到 0.01%;同 ETF 的 `mktcap` 只有 824.69 亿,差 29%,**不是规模**)。 - **币种**:A股为人民币 ⇒ 按新浪 `fx_susdcny`(在岸价)折美元后进分母;**汇率取不到就不折算、单列、标注口径不一致**(不静默混算)。 - 动量 = 各市场**自己的**交易日 20/60/120 日收益率 + 横截面排名(并列取最靠前;样本不足 ⇒ null)。 - **A股净流入单列**(510300 的 20 日 `netamount` 累加,实测 15.56 亿元),不参与份额/规模计算。 - 曲线固定 180 个序列日;实测快照日 = 实测(`origin=observed`),其余 = `当前规模 × 日线收盘价之比` 重建(假定份额数量不变 ⇒ 只反映价格驱动的相对规模变化,不反映申赎)——**实测/重建在数据与界面上分开标注**;滑块回放最近 30 个交易日(本地快照,**零上游请求**)。 - 实测样例:美股 8222.73 亿美元 **89.17%** / 欧股 3.17% / 日股 2.58% / A股(折美元 158.87 亿)1.72% / 新兴 3.36%;轮动「从 新兴市场 转向 美股」。 - **口径形状(已写进边界声明)**:SPY 是全世界最大的宽基 ETF,≈ 其余四家之和的 8 倍 ⇒ 这个口径**天然偏美股(89%)**,不是错数。 ### 取数与效率(真网络实测) - 冷启动 **14 次上游 / 1.65s**;热命中 **0 次**;回放 **0 次**。并发 2 次同请求 ⇒ 上游只打一遍。 - 一次真网络查出的返工:规模/动量各自建键 ⇒ 同一条 ETF 日线拉两遍(17 次);按 (来源, 符号) 建键后 14 次(`AUDIT-IMPL45.md` §8.3)。 - 全程**不用东财 `push2*`**(本机 0 成功率);只用腾讯 + 新浪(`int_*` 族已停用,见 v0.71.0)。 ### 行归属(按 §2.2 判据更新) `ROW_ROUTES.tools = ['/tradewatcher/tools/global-flow']`(原为空数组;注释里预告过「有独占路由就写进来」)。 ### 验证 515 条测试 × 2 时区全绿(494 → 515,+21);`build.mjs` 零改动。 断言六项:① 合计 100%(全有值 & 缺值 & 曲线每一天)② 某市场失败 ⇒ 该行「未取到」+ 原因且**整表无 0**(含渲染层)③ 无轮动给「未见明确轮动」+ 判不了的第三态 ④ 只改净流入 ⇒ 份额/规模逐项不变 ⑤ 并发 2 次 = 一遍上游 ⑥ `source=snapshot` / 回放零上游(含路由层与客户端换帧)。 **反例注入 6/6 被抓**(真改源码 → 跑真测试 → 立即还原 + sha256 校验):归一化×1.01 / 缺市场渲染 0.00% / 永远给轮动 / 净流入混进规模 / 摘掉在飞合并 / 快照模式回源。 ### 未做(已记录) - **`tools` 行在运行中的实例未装载**(宿主行只在启动时按 manifest 装载;该实例 16:47 启动,第 7 行 20:46 才提交)⇒ **需重启 `dsh web` 后生效**。 - 浏览器实机渲染未验证(不能装 bundle/重启);替代证据 = 模型层 19 条断言 + 源码接线 6 条注入 + 产物片段核对。 - 回放历史日不重算动量/排名(界面显式「不适用(回放日)」);汇率只有现价(重建日等价于假定汇率不变);快照从上线当天起累积 ⇒ 首日曲线实测日只有 1 天。 ## v0.71.0 — 新增第 7 组件行「工具」工具箱骨架 + 修正国际指数兜底**错数**(红线) ### 一、新增组件行与工具箱骨架(`dsh-tradewatcher-tools`) - `cordis.patch.yml:73-75` 第 7 行(`name: 'dsh-tradewatcher/rows/tools'` + `config: { row: tools }`);真 YAML 解析器验过:`insert = 7 行`。注释含「行用途 + 按 id 关闭示例」。 - `locale/rows/tools/{en,zh}.json`(标题 `dsh-tradewatcher-tools` 纯 ASCII;描述中文)+ `package.json:27-28`(子路径导出 + locale 通配,用 Node 解析链实测可落到文件)。 - `src/index.ts:29,53-64,73,122,280`:`RowId` 加 `tools`;`ROW_ROUTES.tools = []`、`ROW_TOOLS.tools = []`,**带 §2.2 归属判据注释**(单一消费方才归该行;本轮一条都拿不走)。 - `src/client/index.tsx:189-413`:`PageKey` 加 `'tools'`;页签「工具」**紧跟「日历」之后**;`goPage` 收敛为单一换页入口;`backPage = backTarget(…, visibleTabs…)`。 - `src/client/ToolsPage.tsx`(新):页头返回按钮 + 目录页(`工具目录` / 副标题 / 分类标签行只有「全部」/ 搜索框占位 `搜索工具、市场或指标` / **三列卡片网格** / 卡片 chip·标题·说明·`打开工具 →`,**无星标**,跟随主题)+ 详情骨架(KPI 行 / 曲线区 / 饼图区 / 市场表 / 边界声明)。 - `src/client/toolRegistry.ts`(新,纯函数)+ `src/client/tools/cake.ts`(新,「一个工具一个文件」)+ `src/client/toolsNav.ts`(新,返回规则纯函数)。 - **红线**:详情页无数据一律「未取到」+ 原因;断言锁死**整份详情模型零数字**(不得写假数字)。 ### 二、红线修复:国际指数兜底是**错数** 探测发现(`docs/AUDIT-IMPL44.md`,实测对照): | 标的 | 插件原用的新浪 `int_*` | 正确值(腾讯) | 偏差 | | --- | --- | --- | --- | | 标普 500 | 6643.70 | `us.INX` **7811.54** | **15.0%** | | 道指 | 46247.29 | `us.DJI` **51654.95** | **10.5%** | | 纳指 | 22484.07 | `us.NDX` **30883.15** | **27.2%** | `int_*` 族**无时间戳** ⇒ 是陈旧快照;且**错值已落盘**(`quotes-lkg.json` 里 `100.SPX` = 6643.7)。 处置:① 映射改为腾讯 `us.INX`/`us.DJI`/`us.NDX`(`usSPX`/`us.SPX` 实测均 `pv_none_match`);② **`int_*` 整族停用**(映射表 + 解析器**双层**堵死);③ 落盘错值**按内容特征作废并留痕**(`v:2` + `purged[]`);④ 真数据端到端:`100.SPX` 6643.7 → **7811.54 source=tencent**。 **一处连带发现(只有真数据端到端能抓到)**:光改映射不够 —— `em.ts` 的 `tencentQuoteRows` 用只认沪深港的 `tencentCode` 过滤 ⇒ 国际指数**在发请求前就被丢掉**,端到端全无值;改用 `tencentQuoteCode` 后才拿到。K 线侧:`us.INX` 的 `fqkline` 只回 `day`(指数无除权 ⇒ 允许取 `day`),`mkline` 对 `us.INX` 返 `param error`(五日分时那条链不接指数)。 ### 三、有意降级(须知道) 日经/富时/德国(+韩国/法国/斯托克 50)在腾讯**也没有映射** ⇒ **可用兜底 22 → 18、新浪专供 15 → 8**。EM 不可达时这些卡**如实显示「无行情源 + 原因」**(宁缺错值)。自检阈值已同步并写明原因。 ### 四、验证 `npm run check` **EXIT=0**:**494 / 494 × 2 时区** + selftest + build(73 项片段);`build.mjs` 零改动。 反例注入:骨架批 6 组(文件级,6/6 被抓);红线批 3 条断言 + 2 组注入(恢复后 md5 校验)。其中骨架批第 4 组**第一次没抓住**(断言不敏感),补用例后才抓住 —— 过程如实记入 `docs/AUDIT-IMPL43.md` §4.1。 两处 `**` 残留(`selftest.ts:519/530`)由 `markdownScan.test.ts` 自己抓出并修掉,**未改扫描规则**。 ### 五、⚠ 工作流事实:宿主会热重载,脏工作区构建 = WIP 直接上线 本轮发现:宿主**热重载**插件。一次从**脏工作区**(含他人未完成改动)触发的 `build` 把 WIP 推给了正在运行的 `dsh web`,该实例随后按新规则把现网 `quotes-lkg.json` 的 6 条 `int_*` 旧值作废(现为 `v:2` + `purged[]`;`100.FCHI` 等无 source 的东财时代行未误伤)。 **结果正确,但流程有问题**:构建前必须确认工作区状态。已作为规则记下。 ### 六、未做 - 蛋糕工具的数据层与详情页数据(本批只有结构骨架,KPI/曲线/饼图/市场表全部「未取到」)。 - 浏览器实机验证(需构建 + 重启 `dsh web`)。 - 版本号 / `docs/DESIGN-SUBPLUGINS.md` §2.1 表格:本轮已在本次发布中一并更新。 - ⚠ **更正(v0.78.0 文档批)**:本条原写「README / DESIGN-SUBPLUGINS 已一并更新」,**与事实不符** —— 当时两份文档仍写「六行 / `cordis.patch.yml:38-56`」(第 7 行 `tools` 已入 manifest,文档未跟)。已在 v0.78.0 的文档批补做(行数与行号引用一次性改正);本行按历史事实保留并标注更正,不再声称当时已更新。 ## v0.70.0 — 移除自选页常驻提示块(降级事实移入悬停)+ 修一处时区缺陷 用户要求移除自选页红框内的两行常驻文字。 ### 处置(逐条判断「这话别处有没有」) | 红框内容 | 别处有没有 | 处置 | | --- | --- | --- | | `YTD 缺 1 项` | **有**:行内那一格已显示 `YTD —`(`ytdText()` 缺失态) | **直接删**(连带专用变量与 import 一并收掉) | | `板块涨跌与 α 本次未取到 · 17:38:06 · 上游暂时不可用(熔断中,约 11 分钟后自动重试)` | **别处看不到原因** | 面板整块删;**原因改挂到受影响的两格**(`title` + `aria-label`) | **发现一个坑**:行内 `板块 —` / `α —` 两格的 tooltip 原来写的是「见上方「板块涨跌与 α」一行」—— 那行一删,这两处就变成**悬空引用**。已改为直接带完整原因。 **降级事实一个没丢**(悬停层,两格共用):`板块涨跌与 α 本次未取到 · 17:38:06 · 上游暂时不可用(熔断中,约 11 分钟后自动重试);下一次行情轮询会自动重试`;没时刻 ⇒「时刻未知」,没原因 ⇒ 通用词。**常驻可见处不出现任何新增文案。** **没跟着删的两行**(非红框内容,且是另一件事):`YTD 本次未取到 · …(已取到的保留上一次结果)`(整条 YTD 请求失败)与 `YTD 单次上限 N 项…`(被上限截断)—— 删了会让这两类事实无处可读。 ### 改动 - `src/client/WatchlistPage.tsx`:删两个块(含 `indState.status === 'error'` 的渲染守卫);行级两格的 `title`/`aria-label` 改为带完整原因。 - 新增 `src/client/watchlistView.ts` 纯函数 `indReasonOf({at, error})`(唯一来源,两格复用)。 - 新增 `src/client/watchlistHints.test.ts`(2 条断言 + 2 处反例注入)。 ### 顺手修一处**真缺陷**(TZ 相关) 原来那行常驻文案用**宿主本地时区**格式化时刻 ⇒ 宿主机不在东八区时会把 `17:38:06` 显示成别的钟点。`indReasonOf` 写死 `timeZone: 'Asia/Shanghai'`(与 `marketTime.ts` 同规矩)⇒ **一律北京时间**。 这个 bug 是被**自己的断言**抓出来的:第一次 `npm run check` 默认时区绿、第二遍 `TZ=UTC` **红**(断言要求 `17:38:06`,UTC 下渲染成 `09:38:06`)。修完两遍都绿,断言里写明该口径。 ### 验证 478 条测试 × 2 时区全绿(476 → 478)。反例注入两处:① 把面板级那行加回渲染位置 ⇒ 判据非空 ⇒ `assert.throws`;② 去掉 `板块 —` 那格的 `title` ⇒ 接线计数由 4 变 3 ⇒ `assert.throws` 证明「四处都要有」。 断言 `permanentHintViolations(src)` 同时正面确认「另一件事」的两行仍在(不是靠删光了事)。`build.mjs` 零改动(73 项)。 ### 未做(已记录) 同类「宿主本地时区」写法在客户端**还有残留**(例如 `RescuePanel.tsx` 多处 `toLocaleTimeString('zh-CN', {hour12:false})` 未固定时区)—— 本轮只修了碰到的这处。**建议单开一次「客户端时刻一律北京时间」的收口。** ## v0.69.0 — 补齐 `klines/` 的并发端到端验证(本轮最后一块证据) v0.68.0 的加锁里,`klines/` 两处只有代码级复核、**没有并发端到端**。本版补上:`src/host/klineLock.test.ts`(2 条)+ `src/host/klineLock.child.ts`(真子进程,走真实 `fetchKline` → `refreshKline` → `saveKlineCache`;上游桩给定日期区间;子进程内钉北京 10:30 盘中并**硬断言 `DSH_HOME` 前缀 `/tmp`**,父进程再核一次)。 ### ① `saveKlineCache` 锁内 merge 两子进程**同一时刻**写同一个 `1.600519_101_1.json`:A 带 D0..D399、B 带 D300..D700(重叠 101 ⇒ 并集 **701**)。实测(复跑 3 次稳定):`bars.length = 701`、首 D0、末 D700、无重复日期 ⇒ **两组 bar 都在**。 **反例注入**(退回 `writeFileAtomic` 整份覆写)⇒ 断言红:`末根必须是最晚那组的最后一根(实际 2025-02-02)`。 ### ② `pruneKlineCache` 目录锁 + 清单过滤 配额 300,目录预置 **320 个缓存文件** + 两个"非缓存条目"哨兵(`.keepme` 点文件 / `keepme.txt`,**mtime 设为最旧**),两子进程同刻各落一次盘并触发裁剪。实测(复跑 3 次稳定):文件数 **300**;两哨兵**都在**;`.tmp` 残留 **0**;`.lock` 残留 **0**;两次裁剪的「清理 N 个」**之和 = 22 = 创建数 322 − 配额 300** ⇒ 证明两次裁剪**串行**(第二次看到第一次的结果),而不是各按旧清单各删一遍。 **反例注入**(去掉 `n.endsWith('.json') && !n.startsWith('.')`)⇒ 断言红:`点文件(.klines.lock 同属这一类)不得被裁剪删掉`。 ### 一处实现者**改进了我的要求**(记录并采纳) 我原先要求"直接断言 `.klines.lock` 还在"。实现者指出这**咬不住任何注入**:锁文件由 `withFileLock` 在裁剪**开始那一刻**创建、mtime 永远最新,按"最旧先删"永远轮不到它。 ⇒ 改用**同属点文件的 `.keepme` 哨兵**(注入下必被删 ⇒ 有咬合力),另加 `.lock` / `.tmp` 零残留断言。**这个替换比我的原要求更强**,理由写进报告 §10。 ### 验证 `npm run check` **EXIT=0**:**476 / 476 × 2 时区** + selftest + build(v0.69.0,客户端片段 73 项);`build.mjs` 零改动。 ## v0.68.0 — 持久化跨进程锁 + 锁内合并(修"多实例互相覆盖状态")+ 隐藏标的过滤 用户要求全修。本批 = **持久化**那一半(取数层见 v0.67.0)。起因是真实的:审计期间两个实例同时跑,`rescue-log.json` 的 `days['2026-10-12'].samples` 被另一实例的拍数混入、**无法干净剔除**。 根因不是"没写原子"(`rename` 只保证不写半截文件),而是**整文件覆写 + 无跨进程互斥**:后写者整体覆盖先写者。 ### 加锁覆盖清单 | 状态 | 处理 | | --- | --- | | `watch.json` / `positions.json` / `ledger.json` / `prefs.json` | 加锁,**锁内先刷新磁盘最新 → 改 → 写回**;`importFiles` 的四个文件整段在锁内(**显式覆盖不合并**) | | `rescue-log.json` | 加锁 + **计数合并语义**(`updateJsonLocked`) | | `klines/__.json` | 加锁 + 读-合并-写 | | `klines/` 的 prune 配额 | 目录级互斥(`/.klines.lock`) | **判定不需要加锁的(附理由)**:`quotes/trends/board-lkg.json`(已是原子写;"最新快照胜"、非读-改-写 ⇒ 加锁收益为零、只添陈旧锁风险)、`trends//.json`、`calendar.json`、`breadth` 缓存。 **判据**(写进报告 §2.3):**必须加锁 ⇔ 整文件覆写 + 写前读过 + 覆盖会不可恢复地丢数据**。 ### 顺带发现并修掉一个**此前没人提的旧缺陷** `writeFileAtomic` 的临时名**写死** `.tmp` ⇒ 两个进程同时落盘时,一个 `rename` 成功、另一个 `rename` 报 `ENOENT` ⇒ **整次落盘静默丢失**(在旧版 `rescue-log` 上实测到并留了日志)。现在这些路径都在锁内,不会再撞。 ### 「双写不丢更新」实测数字 | 实验 | 结果 | | --- | --- | | 两个真子进程各 `samples += 3` | **6**(一次没丢) | | 两个真进程各写 6 条自选 + 6 条持仓 | 自选 **12/12**、持仓 **12/12**、流水 **12/12**、分组 **2/2**;锁文件 0 遗留 | | 最小双写:有锁 / 无锁 | **0 → 2** / **0 → 1**(无锁那个子进程还读到半截文件,打印 `samples=-1`) | | **端到端真路径**(`rescue-log.json` 初值 371,两进程各采样一拍) | 新版 **373**(两轮都 373);旧版 `24936cd`(整份覆写)**372**(两轮都 372,丢一次) | | 覆盖证明 | 外部子进程持锁 500ms 时,`store` 四个文件的写入**各自实测等待 ≥300ms**(漏锁会立刻返回) | **反例注入**:把 `acquireLock` 退化成"只做进程内队列、不创建锁文件" ⇒ **L1 / L9 / L10 全部变红**;仓库源码不含该注入。 ### 隐藏标的过滤 `1.000012` 修后**不在**请求集合(23 张);修前的 `TW_ROWS` 全量实现**必命中**(反例注入)。 ### 按 Lead 决定放宽的一处(反例对照) L2/L3 的判据改为「**崩溃(含半截读/解析失败)或丢更新,任一出现即通过**」,不钉时序;文件头写明"反例为什么允许两种表现",正例(L1/L9/L10)**保持严格**。整套并行下连跑 3 次无抖动。 ### 验证 `npm run check` = **474 / 474 × 2 时区** + selftest + build(Lead 树级复跑,无写入者);`build.mjs` 零改动。报告 `docs/AUDIT-IMPL42.md`(520 行:协议 / API 契约 / 覆盖清单 / 合并语义 / 改动点行号 / 证据 / 复现命令)。 ### 未做(已记录) - `klines` 两处的**并发端到端**验证(按分工留给 Lead 与另一批)。 - `trends/.json` / `calendar.json` / `breadth` / `write-log.json` 未纳入(按 §2.3 判据只有 `write-log.json` 属"追加式该并集",但非用户数据、频率低)。 - **读路径跨进程新鲜度**:A 改了、B 的 GET 仍读内存旧值,直到 B 发生一次写 —— 本轮范围外,且是**既有**行为;可用现成 `fileMtime()` 做"mtime 变了就便宜刷新"的读侧补丁。 - 浏览器端到端观测(不驱动用户浏览器、未触碰 3080)。 ## v0.67.0 — 数据层去重与熔断收口(按两份审计全修,取数层) 用户要求全修。本批 = **取数层**全部已发现问题(另一批"持久化加锁"另发)。核心指标是**上游请求数**,全部实测(口径见下)。 ### 改前 → 改后(上游请求数) | 场景 | 改前 | 改后 | | --- | --- | --- | | 分时并发 8 个同 secid | **8** | **1** | | 一次 `/breadth?dist=1` | **112** | **56**(唯一 URL 数,已是下限) | | 分布扫描墙钟(上游注入 100ms 延迟) | 5855ms | **1246ms**(并发峰值 1 → 6) | | 稳态 10s 一拍 ×6 | 6 | **3**(1,0,1,0,1,0) | | 收盘后 A 股 ×6 | 6 | **1** | | 三台全熔断时一次 `/kline`(`114.lhm`) | **6** | **0**;冷却结束 **1 条探针 ⇒ 240 根**(未退化) | | EM 不可达时一次 `/kline` 的 `fails` | 0/0 不变 | **2/台**(记账生效) | | 连续 3 次 `/kline` | 6,6,6(trips 恒 0) | **6,3,0**(trips=1) | | 采样器 + 行情条共存 45s | 8 | **6**(采样器自身 3 → 1) | | 行业榜单标的 | 5 页 | **1 页**(名字在第 5 页时 5 → 5,不退化) | | 源B 失败后连续调用 | 每次 4 页 | 首调 4、第 2/3 调 **0**(总上游 27 → 19) | ### 修法要点 - **P0-1 分时缺 in-flight 合并**:回源段套进 `ttlCache`(并发 8 ⇒ 1)。 - **P0-2 分布同轮扫两遍**:`routes.ts` 的 dist 与源 B 扫同一份 clist ⇒ 给分页加短 TTL(112 ⇒ 56)。 - **P1-1 源 B 无负缓存** ⇒ 加负缓存(失败后不再空转 56 页循环,也不再抢半开探针名额)。 - **P1-2 分布串行扫描** ⇒ 有限并发(6),**红线保留**:页数上限、**没扫全 ⇒ `counts: null` + 原因**(两侧 `scanned=5513` 都扫全)。 - **P1-3 日K 绕过熔断器**(两份审计**独立命中**)⇒ 收口到熔断器:`allow()` + `recordFailure`;腾讯不覆盖的标的(商品主连、外盘指数)行为不变,只是受管辖。 - **稳态命中率 0%(根因)**:服务端槽 TTL **等于**客户端轮询间隔 ⇒ 改为 `quoteSlotTtlMs=12000 > quoteFreshnessMs=10000`(一拍内仍新鲜)。 - **收盘后回源**:行情加载加交易时段判断(沿用"休市定稿零回源"语义);外盘/商品仍轮询(设计如此)。 - **采样器绕过行情缓存** ⇒ 复用同一份行情缓存。 - **P2-1 行业榜**按需取页;**P2-2** 分时 peek 宽限口径统一。 ### 顺带落的前置项(加锁批次需要) `em.ts:21` 引入 `writeFileAtomic`,四处落盘点改 tmp + rename:`276`(`quotes-lkg.json`)、`734`(`board-lkg.json`)、`1054`(`trends-lkg.json`)、`1782`(`klines/__.json`)。序列化**逐字节不变**(compact)。验证:落盘后 `JSON.parse` 通过、目录内 **0 个 `.tmp` 残留**。 > 加锁本身仍未接(`rescue.persist` 合并语义 / `saveKlineCache` + `pruneKlineCache` 读-改-写 / 上述四处)—— 等另一批的精确补丁。 ### 测量口径(可复算) - 脚本 `/tmp/impl41/run.sh before|after`;**每次运行硬断言 `DSH_HOME` 前缀为 `/tmp/`**,本轮对用户 `~/.dsh` **零写入**。 - 计数 = 包一层 `globalThis.fetch` 逐条记录 host+path;**上游 = 合成 fixture**(5513 只 / 56 页、行业榜 496 项、242 点、800 根),**零真实网络**。 - 时钟钉 **2026-10-12 10:30(盘中)/ 15:09(收盘后)**;"改前" = `git archive HEAD` 的独立副本。 - **反例注入 12 组**(关掉修法 ⇒ 断言必须失败),日志留 `/tmp/impl41/inject/*.log`,注入产物已回滚。 ### 顺带修的一处**既有测试缺陷**(与本轮无关) `chips.test.ts:314`「缓存 40 根 + 末根未完成超期 + 回源失败 ⇒ samples ≥ 10」断言 `stale === true` 实际 `false`:**测试没钉时钟**,而收盘后生产代码判"休市定稿"⇒ 不回源 ⇒ `stale=false`(**这是正确行为**)。该用例**只在 09:25–15:05 通过** ⇒ **14:14 那次"全绿"是假绿**。 处置:**只改测试**(用例内钉北京时间 10:30,`finally` 恢复)—— 改前 16/1 挂、改后 **17/17 全绿**(且在**盘后 16 点**跑出的绿)。 **时刻敏感排查**:把时钟整体平移到"盘中/盘后/周六"跑全仓 **61 个测试文件** ⇒ 除已修这条外**无其它命中**(静态 grep 结论一致)。 ### 验证 `npm run check` **EXIT=0**:**473 / 473 × 2 时区** + selftest + build;`build.mjs` 零改动。新增 `src/host/fetchDedupe.test.ts`(20 条)。 ### 未做(已记录) - **P2-3 行情缺口重试 ×2**(不在清单)。 - **行业榜更优解**"按名字定位 ⇒ 稳态 1 页/30s"需东财恢复后用真实回包验证(今天整段不可达)。 - **按 (host, endpoint) 细分熔断键**:P1-3 收口后的已知副作用 —— clist 挂时同主机的 K 线也会被冷却挡住。需改 breaker 键语义 + `/health` 与横幅口径,属另一轮。 ## v0.66.0 — 修分时量能失效:腾讯兜底把每分钟成交额写成了 null(含一处红线违反) 用户报【护盘信号】分时量能长期失效(图区只有「分钟成交额未取到」)。 ### 我的诊断有一半是错的(记录在此) 我在任务卡里写「分时逐分钟序列**没有兜底**(grep `minute/query` 零命中)」—— **错**。`src/host/tencent.ts:266` 早就是 `.../appstock/app/minute/query?code=…`(导出 `fetchTencentMinutes`),已被 `em.ts` 的 `tencentTrend()` 与 `rescue.ts:856` 消费。 **错因**:我的 grep 命令带 `head -5`,命中被截断,我据截断结果下了结论。实现者没有照卡执行,而是**先自己 curl 验证**,据此纠正。**教训:截断的输出不能当证据。** ### 真正的两处缺陷 1. `tencentTrend()` 把每个点的 `amount` **写成 `null`**(上游明明给了额)⇒ 客户端 `PulseChart` 按 `amount > 0` 过滤后不足 5 点 ⇒ 只剩「分钟成交额未取到」。 2. `/trend` **只在整条 trend 为 `null` 时**才给 `missing` ⇒「有 193 个点、个个无额」时 `missing` 仍是 `null` ⇒ **违反红线(缺失必须给原因)**,与实测吻合。 ### 上游结构(实测,与卡片假设不同) 每行 **4 段**:`HHMM 价格 累计成交量(手) 累计成交额(元)`,另有 `data.date=20261012`。**第 4 段就是上游自算的累计成交额**,不是我们推的 —— 校验:`219031 手 ×100×4.382 = 95,979,384` vs 上游 `95,963,053`(差 0.017%,来自"末价 vs 分钟 VWAP")⇒ 所以**用上游累计额做差分**,而不是"价×手数×100"的估算。覆盖已验证:`sh510300`(ETF) / `sh000001`(指数) / `sz000001` / `sh600519` 都有 ~190 行 + `date`。 ### 改动 - `tencent.ts` 新增纯函数 `minuteTrendPointsOf()`:每分钟额 = **相邻累计额之差**、每分钟量 = 相邻累计量之差(`vol` 原是累计值,一并改成每分钟,与东财口径一致);每个带额的点挂 `amountDerived: true` + `amountNote`("腾讯逐分钟只给累计额 ⇒ 差分得到(**推导值,非上游直给**)");累计回退/缺字段 ⇒ 该点 `amount: null`(**不编、不用 0 顶替**)。 - `em.ts`:`tencentTrend()` 改调该纯函数;label 改用 `dayOf`+`hhmmOf`(**一律北京时间**)—— 原来是"UTC 日期 + 宿主本地时间"混拼,**宿主机不在东八区就会错**;新增纯函数 `trendAmountMissingReason()`。 - `routes.ts`:`/trend` 现在**有数据但量能用不了**时也追加 `missing[]`(三情形;整条为 null 时仍用 `trendMissingReason`,不重复)。 - `RescuePanel.tsx`:常驻仍是那一句「分钟成交额未取到」,**原因进 `title` + `aria-label`**;带 `amountDerived` 的序列常驻加一个 4 字标记「**量能推算**」,说明进 title/aria(**不新增常驻长句**)。护盘评分**一行未动**。 ### 真实数据端到端(注入"东财不可达"强制走兜底) ``` points = 193 带成交额 = 192 标推导 = 193 首点 09:30 vol 76758 amount 33650707 第2点 09:31 vol 142273 amount 62312346 ← 142273 = 219031−76758;62312346 = 95963053−33650707(差分对) 末点 14:11 vol 2444 amount 1065034.24 ``` **量级一致性**:逐分钟推导**成交量合计 4,673,155 手**,与同日腾讯日线 `vol=4,673,155`、东财日线 `vol=4,673,155` **逐位相等**(还反证了"差分不漏点");成交额合计 **20.4876 亿元**(截至 14:11),与 `4,673,155 手 ×100×4.384 元 ≈ 20.49 亿元` 相差 0.01%。**没有对不上的地方,也没有调参。** ### 验证 435 条测试 × 2 时区全绿(430 → 435,+5)。断言:① 纯映射(首点=累计、第2点=相邻差、每点带标记、**逐分钟额之和 == 末点累计额**(守恒)、回退 ⇒ null、全无额 ⇒ `allDerived=false`),反例(照搬累计 = 关掉兜底的样子)⇒ 必失败;② `<5` ⇒ 原因非空且**三情形文案互不相同**,反例(写回 null)⇒ 必失败;③ 三情形逐条(`transient` / `no-source` / 非采样时段);④ 客户端消费 `missing[0].note`(原因进 title+aria),反例(去掉 title / 把 `amountDerived` 改 false = 把推导值当上游直给)⇒ 判据非空 ⇒ 必失败。`build.mjs` 零改动(73 项)。 ### 未做 - `amountDerived` 目前是**运行期附加字段**(`TrendPoint` / `TrendData.source` 在 `shared/model.ts`,不在本任务范围);升为正式契约是两行改动(建议与客户端的 8 个消费者同批做)。 - `vol` 语义变更已 grep:唯一消费者(图表 / `trendView`)都按每分钟读;`rescue.ts` 用自己那份累计口径(未动),两者不混。 - 只覆盖沪/深(`tencentCode` 范围):港美股仍只有东财,取不到时原因会如实说明。 ## v0.65.0 — 大盘组件重构:删查询框 / A股全景 → 涨跌分布 / 护盘黄块改简洁 ### ① 移除「个股 / 基金查询」UI(含归属复核) 删除面板块(标题 + 搜索框 + 内联详情卡 + 说明句)、`SuggestInput` 引用、随之无用的 `detailLoading/detailError`。**详情没跟着丢**:`DetailModal` 仍在渲染、行点击仍走 `openDetail`。 归属复核(**`src/index.ts` 与 §2.1/§2.2 无需改**):`/tradewatcher/suggest` **仍有 2 个客户端消费者**(`PortfolioPage.tsx:728`、`WatchlistPage.tsx:554` 仍用 `SuggestInput`)⇒ 保留在 core;`tradewatcher_search` 工具仍在且仍用 `em.searchSymbols` ⇒ 搜索能力保留;**没有任何路由/工具因本次删 UI 失去消费者**。 ### ② 【A股全景】→【涨跌分布】,按参考布局重构(先审计数据源) **可得性表(逐格)** | 格子 | 数据源 | 结论 | | --- | --- | --- | | 上涨 / 平盘 / 下跌 | `/breadth` `current.up/even/down` | **可得**(红绿仍走 `dirClass`) | | 当日成交额 | `/breadth` `current.amount` | **可得** | | **昨日成交** | **本地** `breadth.json` 上一交易日快照 `amount` | **可得**(`/breadth` 新增 `prevAmount`;缺快照 ⇒ 未取到 + 原因) | | 较昨日变动 | 上面两项相减 | **可得**(派生;任一缺失 ⇒ 未取到,**不写 0**) | | 涨跌分布直方图 | 东财 clist `f3` 分页(`em.fetchClistPctPage`) | **可得**(本轮实现) | | 涨停 / 跌停 | 指数行情只有涨/跌/平家数,无涨停/跌停字段 | **未取到 + 原因**(不做,不是 0) | | 预测全天 | 无上游字段,且本插件无日内采样与校准基准 | **未取到 + 原因**("任何预测都是编的") | | 两融余额 / 融资净买 | 全部路由里**没有任何 margin 字段** | **未取到 + 原因**(需新路由 + 缓存 + 归属,另开) | | 日内上涨 / 下跌家数折线 | 宿主只按**交易日**存快照(一天一条) | **未取到 + 原因**(需新采样管道,另开) | 红线:未取到的格子常驻只有「未取到」三字,**原因进 `title`/`aria-label`**;**没有任何格子用 0 或编造数值顶替**。 **实现**:标题改为 `涨跌分布`;两列(新增 `.tw-dist-*` 样式)。直方图为**对称 9 档**(`≥7 / 5~7 / 3~5 / 0~3 / 平盘 / -3~0 / -5~-3 / -7~-5 / ≤-7`,纯函数 `distributionBinsOf`,柱上标数、柱宽用百分比);**无涨跌幅(停牌)不计入任何档**,单独记 `blank` —— 不许当 0(那会凭空多一只平盘)。 路由:`/breadth?dist=1` **显式开关**(其他调用方/工具/自检不付成本);60s 内存缓存;页数上限 60;**没扫全 ⇒ `counts: null` + 原因**(绝不拿扫到的部分冒充全市场)。未新增说明性长文案。 ### ③ 【护盘信号】黄块 → 紧凑标记(降级事实不丢) 提取纯函数 `breakerBadgeOf()`(新建 `src/client/rescueView.ts`)⇒ `{ marker, reason, fromAlt }`:常驻**只留 ≤6 字标记**(`备用源` / `熔断 N/M`);**完整那句一个字没删**,进 `aria-label` + `title`(含熔断明细与最后错误),元素 `tabIndex:0 role:note`(键盘/触屏可读)。 ### 验证 430 条测试 × 2 时区全绿(423 → 430,+7)。三处反例注入:① 查询块加回常驻 ⇒ 非空;② 把 `blank` 当平盘 ⇒ `assert.throws` 证明"blank 不许当 0"必失败;③ 给「预测全天」编数(`1.23 万亿`)⇒ `assert.throws` 证明"必须显示未取到"必失败;另 ③b 长句换到常驻位置 ⇒ 判据抓到 `badge.reason` ⇒ 必失败。 **真实上游验证(含当下故障)**:实测 `pz=500` 上游**只回 100 行**(`total: 5562`)⇒ 全市场 ≈ **56 页**(与上限 60 一致,注释写死这笔账)。收尾时东财 push2/push2delay **整段不可达**(连接重置)⇒ 扫描器得 `页数=0/失败=fetch failed` ⇒ 按实现返回 `counts: null` + 原因 ⇒ **这条"未取到 + 原因"的路径被真实故障验证,且没有把 0 当成"全市场 0 只涨"画出去**。happy path 因上游故障**未能当场观测**(报告如实标注;分档算法用 fixture 逐点断言)。上游恢复后可复核:`curl '…/tradewatcher/breadth?dist=1' | jq '.distribution'`。 ### 未做(各自"要做得先做什么"已记录) 涨停/跌停(需扩 clist 字段 + 判定口径);预测全天(需日内采样 + 校准);两融(需新路由 + 归属);日内家数折线(需新采样管道);直方图是否按幅度归一/对数(等用户看后再定)。 ## v0.64.1 — 可见句措辞由「不可信的」改为「未完成的」最后一根 `task-13` 的可见句是被截断的**首段**,而「未完成」原先只出现在被截断的括号里 ⇒ 读者看到「不可信的最后一根已丢弃」,**"不可信"太含糊**。改为「**未完成的**最后一根已丢弃」(`em.ts` 一处词 + 一行注释)。 | 层 | 文案 | 字数 | | --- | --- | --- | | 宿主整句(`kline.staleReason`) | `回源未取到新数据 ⇒ 改用本地缓存的 800 根已完成 bar;**未完成的**最后一根已丢弃(原因:未完成 bar 已超过 60s TTL ⇒ 回源重取;但文件里有 801 根,丢掉不可信的最后一根后仍有 800 根 ≥ 10 ⇒ 降级采用)` | 128 | | 客户端可见(首段) | `· 回源未取到新数据 ⇒ 改用本地缓存的 800 根已完成 bar;未完成的最后一根已丢弃` | 46 | ### 一处**故意保留**(Lead 已确认) 宿主整句括号内「丢掉**不可信**的最后一根后仍有 M 根」(`em.ts:1531` 的 `degradedReason`)**未改**:它同时覆盖 `v≠3` 那条路径 —— 那种情况下那根 bar 未必"未完成",只是**无法确认** ⇒ 中性词更准。 ### 验证 423 条测试 × 2 时区全绿(无新增断言,均为既有断言继续成立)。改词只**增加**了一处「未完成」⇒ `chips.test.ts` 的 e2e(查 `staleReason` 含"未完成" + "已完成 bar")不受影响;客户端 `chartNote.test.ts` 只查 `/最后一根/`、**不锁宿主措辞** ⇒ 这次改词没让客户端测试变红(当初放宽那条的用意正在此)。`build.mjs` 零改动(73 项)。 ## v0.64.0 — 图表脚注显示缓存降级的**具体原因**(替换通用那句,不新增文案) `task-11` 已在宿主侧备好 `staleReason`(完整 128 字),但客户端没读 ⇒ 只显示「· 上次成功数据(MM-DD HH:mm)」。本轮让客户端读它,**替换**那句通用说明(用户一直在要求减少提示)。 ### 实际渲染文案 ``` 宿主完整 staleReason(128 字,仍留在 payload 里) ① 有 staleReason ⇒ 客户端渲染(46 字,一行): · 回源未取到新数据 ⇒ 改用本地缓存的 800 根已完成 bar;不可信的最后一根已丢弃 ② 没有 staleReason(同一份数据)⇒ 与改动前逐字一致: · 上次成功数据(10-09) ``` ### 改动 - `src/client/chartNote.ts`:`NoteKlineLike` 增 `staleReason?`;新增 `staleReasonOf()`(只 K 线档取,空串/空白不算)+ `staleVisibleOf()`(取宿主那句的**首段**);`lastSuccessNote()` 有原因 ⇒ ` · <首段>`,没有 ⇒ 原样不动。 - `src/client/chartCache.ts` **未改** —— payload 本就整只塞宿主回包(`chartCache.ts:82`)⇒ `staleReason` 天然透传(实测确认)。 - **未新增第二处文案**:没有第二行,也没往 `title`/`aria-label` 加内容;完整原文留在 payload 里(接口/agent 可读)。 - `src/shared/model.ts` **未改**:`staleReason` 只声明在客户端消费侧的结构类型(结构兼容即可读);要提升为正式契约字段建议单开(会牵动 8 个消费者)。 ### 可见句取"首段"(Lead 已确认) 宿主整句 128 字里,括号内是**二级细节**(TTL 数值、文件根数、采纳下限)—— 常驻放不下,也违背"减少提示"。**括号内容没丢**,仍在 `kline.staleReason`。若要整句显示,`staleVisibleOf` 改成 `return reason` 一行即可。 ### 验证 423 条测试 × 2 时区全绿(421 → 423)。断言:有 `staleReason` ⇒ 渲染值 == ` · ` + 首段、**不含**「上次成功数据」、含"回源未取到新数据 / 已完成 bar / 最后一根"、**常驻一行(≤80 字)**、payload 完整原文**保持原样**、不出现「降级缓存」这类新增词;无 ⇒ 与以前逐字一致;分时档不取该字段。 **反例注入**:写一个"忽略 `staleReason`、永远给通用句"的假实现 ⇒ 与真实现不同 ⇒ `assert.throws` 证明断言必然失败;另有兜底断言确认真实现读的**就是** payload 那个字段(`staleReason:'甲(乙)'` ⇒ ` · 甲`)。 一处**故意放宽**:只断言 `/最后一根/`,不锁宿主原文,免得宿主微调措辞把客户端测试搞红。 ### 未做 筹码侧未透传降级说明(`/chips` 修好后不再报"样本不足",但不说"数据来自降级缓存");`staleReason` 未提升为正式契约字段。 ## v0.63.1 — 命名改为 `dsh-tradewatcher` / `dsh-tradewatcher-*` 用户要求:插件名显示 `dsh-tradewatcher`,组件名显示 `dsh-tradewatcher-*`(覆盖 v0.61.0 的英文友好名)。 ### 先查清头部标题的来源(卡里给的两条分支其实是同一条) | 位置 | 代码 | 取的是 | | --- | --- | --- | | `dsh-client-ui-plugin-manager/lib/client.js:615`(`packageText`;`:2334` 的 `PackageCard` 当**头部**) | `pkg.meta?.title ?? pkg.name` | **包级** meta | | `…/client.js:628`(`rowText`) | `row.meta?.title ?? row.moduleName` | **行级** meta | 包级 meta 来自 `dsh-plugin-manager/lib/index.js:1525` 的 `readPluginMeta(info.name ?? name, <包目录>/package.json)`;行级来自 `:1931` 的 `metaOf(row.name, base)`(同一个 `readPluginMeta`)。**core 行的 specifier 就是裸包名** ⇒ 用真函数、两个不同 base 实测:头部与 core 行**同源同值**。 ⇒ **不存在"头部取包清单名 ⇒ core 可写 `-core`"这条路**:要头部 = `dsh-tradewatcher`,core 行只能同值。反过来让 core 走子路径就是被实测否决的方案 B(裸包名一行不剩 ⇒ 浏览器半区整体消失)。 ### 改后实际显示 | 层 | 标题 | | --- | --- | | **头部(插件名)** | **`dsh-tradewatcher`** | | core(与包级同源) | `dsh-tradewatcher` | | calendar / watchlist / portfolio / market / cloudmap | `dsh-tradewatcher-calendar` / `-watchlist` / `-portfolio` / `-market` / `-cloudmap` | 描述行保持中文未动;13 个 locale 文件的 title **两语都改**(解析是 `text.zh ?? text.en`,只改一侧在中文环境失效)。 ### 断言(不放宽 + 新增一条关键闸门) 保留三条:允许集(ASCII + `·`)、不得含 CJK、描述必须含中文。 **新增 `^dsh-tradewatcher(-[a-z]+)?$`** —— 这是**唯一**能拦住"改回 `Tradewatcher · Calendar`"的闸门:那条既不含 CJK、也没有非法字符,前三条全拦不住。 另加 `core 标题 === package.json 的 name`、五个功能行 `=== <包名>-`(注释写明"core 与插件名同值"是**构造使然**,同一 specifier)。 **反例实测**(改真文件)⇒ `fail 2`,失败项点名 `title 不符合命名不变量 ^dsh-tradewatcher(-[a-z]+)?$:Tradewatcher · Calendar`;恢复后逐字一致、16/16 全绿。该注入已固化进用例(与 task-9 的三条共四条常驻)。 ### 并发写入的记录(实现者主动作废了一次绿) 它第一次 `EXIT=0` 时另一批(`task-11`)正在改 `src/host/em.ts`/`routes.ts`,写入落在其检查窗口内 ⇒ **该轮绿作废**;重跑撞上对方中间态;等对方停下、全程无写入后再跑才得到最终绿。**Lead 随后在无写入状态下复跑树级校验:421 / 421 × 2 时区、fail 0。** ### 验证 `npm run check` = 421/421 × 2 时区(Lead 树级复跑);片段校验 73 项。 ## v0.63.0 — 修「丢数据」:缓存被判不可用时不得整段丢弃 用户报「筹码分布突然样本不足」并问「历史数据是不是没落库」。**落库了**(盘上 v3、801 根),是我们的读侧把整段缓存判废后返回 0 根。 ### 事实(修前) `klines/1.560010_101_1.json` = v3 / `partial: true` / **801 根**(2023-06-19 ~ 2026-10-12)/ updatedAt 距今 41.6 分钟 ⇒ 被"partial 超 60s TTL"**整段判废** ⇒ `/kline` 回 0 根 ⇒ `/chips` `samples = 0` ⇒ 显示「样本不足」。 **手里有 800 根已完成历史却报"样本不足" —— 这是丢数据,不是缺数据。** ### 修法 - `em.ts`:新增 `KLINE_MIN_USABLE_BARS = 10`(注释写明与 `CHIPS_MIN_SAMPLES` 同值,两层阈值不许打架)+ 纯函数 **`klineCacheAdoptionOf()`**:过闸门 ⇒ 原样用;被拒 ⇒ **丢弃最后一根**,剩 ≥10 根就照用并给降级说明;不足 10 根 ⇒ 不采用(**不硬凑**)。`KlineEntry` 增 `fileBars/refusedReason/degradedReason/droppedLast` 记账;`loadKlineCache` 由"被拒就 continue"改为走该判定(丢掉未完成那根后 `partial` 跟着置 false)。 - `routes.ts`:`/kline` 注入 `klineCacheStatusOf(secid, klt===104?103:klt, em.normalizeFq(secid, fqt))`(键与内部一致:年K 走 103、复权过 `normalizeFq`)。 - `chips.ts` **未改** —— 它本来就"喂什么算什么";症状在筹码,**病灶在取数层**。 - **契约不变**:`missing`/`why`/`stale`/`asOf` 名字与含义都没动;只**增加**一个可选字段 `staleReason`(写在 `em.ts` 的交叉类型里,**未改 `src/shared/model.ts`**)。 ### 修后实测(真缓存文件 + 把上游整段挡掉) ``` /kline days = 360 stale = true staleReason = 回源未取到新数据 ⇒ 改用本地缓存的 800 根已完成 bar;不可信的最后一根已丢弃(原因:未完成 bar 已超过 60s TTL ⇒ 回源重取;文件里 801 根,丢掉最后一根后仍有 800 根 ≥ 10) 末两根 = 2026-10-08@2.975 , 2026-10-09@2.982 ← 10-12 那根盘中未完成 bar 已丢弃 /chips 样本数 = 360(原 0);平均成本 3.0944,支撑/压力 2.9702/3.2231 ``` ### note 三情形(不再用一句话概括) | 情形 | 修后 | | --- | --- | | ① 从未有缓存 | 「本地**确实**没有该周期的缓存数据;…」 | | ② **有缓存但被判不可用** | 「本地**有**该周期的缓存,**但被判不可用**(未完成 bar 已超过 60s TTL ⇒ 回源重取;文件里 801 根);…」 | | ③ 回源失败 | ②是事实半句、③是原因半句,可分辨;归因仍 `transient` | ### 一处经 Lead 确认的扩展 被拒的两类原因**都**走"降级采用(丢最后一根)":**(a) partial 超 TTL**(任务明说)与 **(b) 旧 schema `v≠3`**(实现者主动扩展)。理由:老规则"v2 一律不采用"会造成**同一类丢数据**(盘上 900 根、界面报样本不足)。**红线保留**:绝不与可能冻住的最后一根混用 ⇒ 丢最后一根、用其余,而不是整段放弃。 ### 验证 421 条测试 × 2 时区全绿(414 → 421,+7)。反例注入:把判定改回"老行为 `adopt:false`" ⇒ `assert.throws` 证明"必须采用"必然失败;5 根 ⇒ 如实不采用;TTL 内 ⇒ 一根不丢;`partial=false` 旧缓存不受 TTL 影响;降级说明只在"降级 + 回源失败"时给。端到端:临时 `DSH_HOME` + 真写 v3 缓存 + **`globalThis.fetch` 一律 reject**(不碰网络)⇒ 真 `fetchKline` 不再 null、`stale === true`、`days.length === 39`、喂真 `chipSeriesOf` ⇒ `samples ≥ 10`。 `build.mjs` 零改动(73 项)。 ### 未做(待派) - **客户端不读 `staleReason`**(`chartNote.ts` 对 `stale` 只给「· 上次成功数据(MM-DD HH:mm)」)⇒ 要让用户看到具体原因需客户端一行改动。host 侧字段与文案已就绪。 - `/chips` 自己的 `why` 未加"数据来自降级缓存"的说明(修后它不再出现"样本不足");要加需在 `chipSeriesOf` 透传 `staleReason`。 ### 复核命令 `curl '127.0.0.1:3080/tradewatcher/kline?secid=1.560010&klt=101&lmt=360' | jq '.kline.staleReason, (.kline.days|length)'` ## v0.62.0 — 云图只留第三方站点(删自绘)+ `/board` 归属随之改到 market 行 + 修一个先于本轮的测试竞态 ### 删除(用户要求:云图去掉自绘,只保留第三方云图) - 删 `src/client/IndustryHeatmap.tsx`(自绘行业热力:取数 `api.board('industry','amount',1)`、派生 `squarify`、口径段、空态/降级文案全在其中)。 - 删 `src/client/treemap.ts` + `treemap.test.ts`(自绘下线后**无生产消费者** ⇒ 死码清理,−4 条测试)。 - `CloudMap.tsx`:删 import / `source` 状态 / 整个内建分支 / 上下两处段控 + hint / 随之无用的 `useWidth`+`hostRef`+`width`;**保留**第三方分支、iframe、三态与 timeout 占位,**未新增任何文案**。 - **解锁 2 条构建片段**(Lead 执行):`build.mjs` 里锁定的 `行业热力(自绘)` 与 `面积=成交额` —— 片段表锁的正是"自绘视图存在",与下线目标直接冲突;其余 73 条未动(片段校验 = **73 项**)。 > 实现者在此**停手上报**并给出实测失败信息与"75 条里只有这 2 条无别处来源"的系统核对,没有用"把字面量藏到别处"之类办法绕过校验。 ### `/board` 归属(判据没变,变的是调用方集合) | 时点 | 消费方 | 归属 | | --- | --- | --- | | 删除前 | `MarketPage.tsx:84` + `IndustryHeatmap.tsx:30`(只被云图页渲染) | core(≥2 行消费) | | **删除后** | **只有 `MarketPage.tsx:84`** | **market 行** | 改动:`ROW_ROUTES.market` 收进 `/tradewatcher/board`(`cloudmap` 保持空数组);`DESIGN-SUBPLUGINS.md` §2.1 与 §2.2 表格同步(原"2 个消费方 ⇒ core"改为"1 个消费方 ⇒ market"),那段"关掉大盘行 ⇒ 云图页热力图 404"的动机**已作废**、改为历史说明。另核:"自绘是否还牵动别的路由/工具" ⇒ 其 API 面**只有** `api.board` 一处 ⇒ **无第二条要重定归属**。 ### 验证 414 条测试 × 2 时区全绿(连跑 3 次);条数净 0(删 treemap −4、加 cloudMap +2、加 index +2)。 **反例注入**:① 内建视图标题加回渲染位置 ⇒ 非空 + `assert.throws`;② 段控加回 ⇒ 非空;③ **把字样放进另一个实现文件(`src/index.ts`)⇒ 全仓扫描非空**(证明覆盖面真实);④ `boardOwnershipProblems` 从 `src/index.ts` 源码文本抽数组(不另抄)—— 从 market 撤掉 ⇒ 非空;塞回 `cloudmap: []` ⇒ 非空。运行期门控:只开 core ⇒ 不含 `/board`(404);core+market ⇒ 含;**core+cloudmap ⇒ 仍不含**(云图不再消费它)。 **浏览器实测**(真 `react-dom/client` + 真 CSS + 真事件/计时器):loading `加载中…`、loaded `已完成`(真加载 52etf.site)、timeout `加载超时` + 占位 `无法在 8 秒内加载 52etf.site | …最后成功加载:10-11 14:30` + iframe `display:none`;三态共同:`.tw-seg` = 0、`.tw-drawer-body svg` = 0、DOM 中任何 title/文本都不匹配 `行业热力|自绘`。 一处自查改正:第一版断言把 `tw-seg` 当"自绘专用类",实为**通用**段控类(日历/大盘/持仓/SortBar 仍在用)⇒ 改为"CloudMap 不许用 + 该类仍被别处使用 + 定义仍在"。 ### 顺带修掉一个**先于本轮存在**的测试竞态(不修则验收无法全绿) 现象:`task-6:六行装载` 断言 `__kernelStats.starts === 1` 偶发拿到 `0`。 **归因证据**:在干净 worktree(基线 `9dd132d`、不含本轮任何改动)跑同一套 ⇒ **3/3 全红**,失败项就是它;该文件单独跑时基线与改动后都失败 ⇒ **与本轮无关**。 **根因**:`store.init()` 是真异步(临时 `DSH_HOME` 上做文件 IO),测试却"固定睡 50ms 就当 init 落定" ⇒ 负载下假失败。 **修法(仅测试)**:加**有界**条件等待器 `waitFor(cond, ms=5000)`,只把那一处改成等 `starts === 1`。 **反例注入**:注入"采样器根本不启动"⇒ **仍然失败**(证明该等待器**不会把失败等成通过**)。 ### 未做(已记录) - **`iframe.onLoad` 在错误页上也会触发**(把域名整段阻断后浏览器仍派发 `load`)⇒ 状态徽标显示"已完成"不等于内容真的取到了。**既有行为,本轮按"三态不变"未改**,建议另开任务。 - `DESIGN-SUBPLUGINS.md:55` 那段"功能行(可关)"总览仍把 `/anomaly`/`/industries` 写在 market(task-60 之后即已过时;§2.1/§2.2 才是权威表)—— 建议单开一次小清理。 - `docs/DECISIONS.md` 与历史 `CHANGELOG` 条目为历史记录,未改;`ROADMAP.md` 的 P2-5 加了一行"v0.62.0 已下线"。 ## v0.61.0 — 组件标题用英文,中文只留在描述行 用户要求:插件名与各组件名用**英文**;中文解释**只出现在组件详情(描述行)**。 ### 改动(只动 locale 元数据) `locale/**`(13 个 json)的 **`title` 全部英文**,`description` 保持中文(`zh`)/ 英文(`en`): | 行 | `title` | 描述(中文环境) | | --- | --- | --- | | core | `Tradewatcher · Core` | 行情条 / 详情图 / 数据层与共享路由 | | calendar | `Tradewatcher · Calendar` | 财经日历:宏观数据 / 新股 / 财报 / 分红 | | watchlist | `Tradewatcher · Watchlist` | 自选分组、行业板块与异动队列 | | portfolio | `Tradewatcher · Portfolio` | 持仓盈亏、逐笔流水与费用 | | market | `Tradewatcher · Market` | 涨跌家数与护盘信号 | | cloudmap | `Tradewatcher · Cloudmap` | 大盘云图(行业热力,纯前端) | > **最容易漏的一条**:解析规则是 `text.zh ?? text.en` ⇒ **`zh.json` 的 `title` 也必须是英文**,只改 `en` 的话中文界面仍显示中文标题。两语都已写英文。 ### 我给的规格里有一处自相矛盾(实现者指出并正确处理) 我的断言写「`title` 匹配 `^[\x20-\x7E]+$`(纯 ASCII)」,但**同一张卡给的示例**用了 `Tradewatcher · Core`,其中 `·` 是 U+00B7 —— 不在 ASCII 可打印区间内。严格断言会把示例自己判红。 采纳的处理:允许集 = 可打印 ASCII **+ `·`**(注释写明原因),并**另加一条真正的本意断言 —— 标题不得含 CJK**(含 CJK 标点与全角)。若将来要严格 ASCII,把分隔符换成 `: ` 即可(一条 `sed`,已写进报告)。 ### 验证 414 条测试 × 2 时区全绿(413 → 414,EXIT=0);`build.mjs` / `cordis.patch.yml` / `package.json` / `src/index.ts` **零改动**(只动 13 个 locale 文件 + 测试 + 报告)。 断言设计成「真实文件必须返回空、注入必须非空」⇒ **非恒真是结构性保证**:抽出 `localeMetaViolations(entries)`(标题非空 + 允许集 + 不得含 CJK;描述非空;`zh` 描述必须含中文;`en` 描述不得含 CJK)。 **改真文件反例**(把 `locale/rows/cloudmap/zh.json` 的 `title` 写回中文)⇒ `fail 2`,失败信息**直接点名原因**("title 含非允许字符" + "title 含 CJK —— 标题必须是英文");恢复后逐字一致、14/14 全绿。另 3 条注入(title 写回中文 / zh 描述失去中文 / title 混入全角括号)固化进用例。 ### 结果 插件列表现在显示:六个**英文标题**(`Tradewatcher · Core / Calendar / Watchlist / Portfolio / Market / Cloudmap`)+ **全中文描述行**。若标题再出现中文,红的就是那条 CJK 断言。 ## v0.60.0 — 六个组件行可区分:core 留裸包名 + 五行子路径 + locale 元数据 用户反馈:插件列表里**六行标题都叫 `dsh-tradewatcher`**,分不清哪个是云图。 ### 标题的真实来源(先查清再改) 标题取自 `row.meta.title`,无 meta 才回退 `moduleShortName(name)`;meta 由 **`readPluginMeta(specifier, base)` 按 specifier 解析 `/locale/{en,zh}.json`**(源码注释原文:*"A subpath specifier never reads package.json"*)⇒ **子路径本来就是元数据主体**。 ### 我的方案被实测否决(记录在此,避免以后重犯) 我原提"六行全用子路径模块名"。实现者**按我的要求先做端到端实测再决定**,结论:**会静默干掉浏览器半区**。 `dsh-client-modules` 的 `exactPackageSpecifier()`(`:82-88`)对**非 scoped 且含 `/`** 的 specifier 返回 `void 0`,配合 `:746-747` ⇒ **子路径行根本不是 client source**(`resolveMeta → null`)⇒ 该包**一条 client source 都不剩**。 三态实测(临时实例 `dsh --profile web --port 0 --no-open`,用完即杀;用户 3080 服务未动),探针 = 首页 `__DSH_BOOT__` 里 `{"id":"dsh-tradewatcher"}` 出现次数: | 态 | 宿主 6 路由 | `/rows` | 日志 | 浏览器半区 | | --- | --- | --- | --- | --- | | A 基线 v0.59.1 | 全 200 | 六项 | 无 error | **1 次 ✅** | | **B 我的方案(六行全子路径)** | 全 200 | 六项 | 无 error | **0 次 ❌ 界面消失** | | **C 采纳方案** | 全 200 | 六项 | 无 error | **1 次 ✅** | 态 B 是最阴的一种失败:**日志一个字不报、六个路由全 200、`/rows` 照旧六项**,只有抠首页 HTML 才看得出 —— 与当初"插件服务异常"同款形态。 另:我原先担心的 composition error **不会发生**(冲突判据是解析后的**包名**,`:871-876`;子路径不构成第二个 source)—— 但代价是**这个包名一条 source 都不剩**,这点我没想到。 ### 改动(方案 C) - `cordis.patch.yml`:**core 行保持裸包名**(保住浏览器半区)+ 另五行用 `dsh-tradewatcher/rows/`,头部注释写明这条硬约束与实测依据。 - `package.json`:新增 5 个 `/rows/` 宿主入口、6 个 locale 映射、`files` 加 `locale`。 - 新增 `locale/**`(13 个 json:core + 五行,各 zh/en 标题)。 - `src/index.test.ts`:+1 条标题/locale 断言 + 3 条**裸包名不变量**注入。 - `src/index.ts` / `build.mjs` / `src/client/**` 零改动。 **实测标题对照**(直接调真实 `readPluginMeta`):`dsh-tradewatcher` → `盯盘 · 核心`;`/rows/calendar` → `盯盘 · 自选`… 六个互不相同,即 `盯盘 · 核心 / 日历 / 自选 / 持仓 / 大盘 / 云图`。 ### 验证 413 条测试 × 2 时区全绿(412 → 413);`build.mjs` 零改动。 **裸包名不变量**(本轮新增的回归闸门):core 行的 `name` 必须是裸包名 —— 断言 + 反例注入(改成子路径 ⇒ 必须失败)。 回滚命令(已演练):`git checkout HEAD -- cordis.patch.yml package.json src/index.test.ts && rm -rf locale`,随后重启 `dsh web`。 ### 未做 装机后确认(用户重启):六行标题可区分 **且** 浏览器半区仍在(面板正常渲染)。 ## v0.59.1 — 修正三处旧文本的错误归因(纯文字与注释,产物逐字不变) 六行方案已恢复(v0.59.0),但三处旧文本仍写着**错误归因**("同包多行会被整体拒绝")。 ### 改动 - `docs/DESIGN-SUBPLUGINS.md`:抬头改为「**已落地(v0.59.0 / a2066e1)**」;「回退记录(v0.50.0)」整节改写为「**归因纠正与恢复记录**」—— 三段结论 + `sourceKey` 归并的代码级证据表 + `ctx.config` 真凶一节 + 仍然成立/仍然未验证;§5 验收判据「未执行」→「静态已验、待装机」;§P0/§5.1 的"缺省 `core`" → **`all`**;指针 `:13-15` → 现行 `:38-56`。另顺手修三处同类过时"现状"(`:8`、§1.3 加快照说明、§7 标题)—— 不修则抬头说"已落地"、下面写"现状:单行",同屏自相矛盾。 - `README.md`「子组件与开关」:权威指针改指**本包 `cordis.patch.yml:38-56`**(列出六个 id);落地记录改指 `docs/AUDIT-IMPL35.md`;**新增**「怎么关一行」(profile 层 `- id: tradewatcher-` + `disabled: true` 片段)+「**必须重启 `dsh web`**」+「core 行别关」。 - `CHANGELOG.md`:三处历史陈述加**相邻纠正标记**(原文保留以存史,标记落在相邻行);并把 v0.44.0 那句歧义表述改写为"同一个包被**多个解析源** resolve 才属 composition error;**同一源的多个 row 不算**"。 - `src/index.ts:12` 注释:缺省 row 由 `core` 改为 **`all`(全量;不是 `core`)**,并写明"给 `core` 会把自选/持仓/大盘/日历的路由一起 404(v0.45.1 实测)"。 ### 验证 **零行为改动,有哈希证据**:改注释前后各 `npm run build` ⇒ `lib/index.js = 25bb8cba…`、`lib/client.js = 8fa44f32…`,**逐字一致**(esbuild 不带普通注释进产物)。 **行数中性(差点踩的坑)**:注释第一版拆成 3 行会让 `src/index.ts` 由 269 变 271,使 `AUDIT-IMPL35.md` 十几处行号锚点**集体漂移 +2**;已压成一行,锚点全部继续有效。 `npm run check` = **412 / 412** × 2 时区 + `ALL HOST CHECKS PASSED`;`build.mjs` 零改动。 全仓 grep 自查逐条结论记入 `docs/AUDIT-IMPL35.md` §10.2:保留且正确的 = 该文档 §1、`cordis.patch.yml:14-16`、`CHANGELOG.md:18-19`、`DESIGN-SUBPLUGINS.md:38`(英文原句引用,其"client 包级单实例"结论仍成立)、`src/index.ts:99`;加标记的历史段落 = `CHANGELOG.md:315/560/569`。 ### 未做(有意) `docs/STE-REVIEW-DOCS.md:15`(过时评审建议表)与 `docs/AUDIT-IMPL23.md:13/:20`(上一轮自述报告)**不改** —— 它们是**时间点记录**,按本项目惯例保留原样;权威表述以 `DESIGN-SUBPLUGINS.md` 与 `README.md` 为准。 ## v0.59.0 — 恢复组件分离:六行 manifest(含归因纠正,附代码级证据) ### 纠正我此前的一个错误结论 我曾判定"同包多活跃 Loader 源 = composition error ⇒ 六行方案不可行"。**这个结论是错的**,依据是源码注释里的一句话,而非实测。 真原文是 **"distinct active Loader *sources* for that package are a composition error"**。本轮的**代码级证据**: `dsh-client-modules/lib/index.js` 的 `processOne()` 把同名行按 **`sourceKey = baseUrl + "\0" + loaderName`** 收进 Map(`:833-840`,`:791-792`)⇒ **六行同名同 baseUrl ⇒ 只有一个 sourceKey ⇒ 永远走不到 `:874` 的 `sources.length > 1` 报错**。"多个 row"与"多个 source"是两件事。 **当年失败的真凶**(v0.45.3 才查到,且与行数无关):`apply` 读了 `ctx.config`,而 Cordis 的 ctx 是 Proxy —— `cordis/lib/index.js:672` 的 get trap、`:676` 的 `cannot get property "…" without inject`(`:736/:743` 的 mixin accessor 注册解释了为什么 `effect`/`get` 能用而 `config` 抛)。**这个 bug 会同等打死 1 行或 6 行 ⇒ 六行方案从未被真正验证过。** ### 改动 `cordis.patch.yml` 恢复六行(`tradewatcher-core / -calendar / -watchlist / -portfolio / -market / -cloudmap`),全部启用,每行 `config: { row: … }`,`name` 全为 `dsh-tradewatcher`;**代码零改动**(`rowOf` 已支持显式行;无 config 仍是 `all` 默认 ⇒ 向后兼容保留)。注释写明归因纠正。 YAML 用 **DSH 自带解析器**验过:`errors = 0 / warnings = 0`,结构 `[{insert: [6 行]}]`(语法错才是启动期硬失败;"目标行不存在"只是 warning)。 **不需要 install**:`profiles/web/package.json` 为 `link:`、`node_modules/dsh-tradewatcher` 是 symlink(**同 inode 4879983**)⇒ profile 看到的就是工作区那份。 ### 静态自查(锚**真实产物** `lib/index.js`) `inits = 1` / `starts = 1`(再注册一轮仍为 1);**跨行重复路径 `[]`**;**路由账 24 = core 16 + calendar 1 + watchlist 3 + portfolio 2 + market 2 + cloudmap 0**;**六行并集 == 单行全量**;**core 不含** `/watch`、`/calendar`、`/portfolio`、`/ledger`、`/breadth`、`/rescue`。工具同样逐行分开(core 5 / calendar 2 / watchlist 2 / portfolio 2 / market 1 / cloudmap 0)。 ### 反例证据(三组,各贴输出后恢复) 1. **代码级(点名的)**:`applyCore(ctx, true)` 当默认 ⇒ `pass 5 / fail 7`(含"calendar 行不该注册 `/tradewatcher/health`");`git checkout` 后 `cmp` 逐字一致。 2. **跨行重叠**:让 market 前缀多一段 `/tradewatcher/watch` ⇒ 报 `同一路径被两行各注册一次`。为此把"跨行零重复"排在"每行只注册自己那段"**之前** —— 两条不是同一条断言的重复。 3. **改真实文件**:大写 / 数字后缀 / 漏 `config` 三种 ⇒ 每次 `fail 2`;每次立刻还原,收尾 `sha256sum -c` 通过。 另:三种注入**固化进测试**(`checkManifest()` 对真实文件必须返空、对 5 个字符串注入必须非空)。 ### 回滚(已**演练**,非纸上步骤) 主命令 `git checkout HEAD -- cordis.patch.yml src/index.test.ts`。备用(不依赖 git)heredoc 写回单行:实测写入后 `tests 12 / pass 10 / fail 2`(红的正是两条锁六行的清单断言,设计如此),运行时断言全绿;还原六行后 `pass 12 / fail 0`、sha256 与基线一致。 ### 验收 `npm run check` = **412 / 412** × 2 时区 + `ALL HOST CHECKS PASSED`;**测试条数仍 412**(删 3 条锁单行的旧断言、加 3 条锁六行的新断言,净 0);`build.mjs` 零改动。 **最后一公里只能由装机验证**:重启 `dsh web` 后看 `plugin_manager list_plugins` 是否出现六行且 `fiberPhase=active`、`/tradewatcher/rows` 是否六项、关一行后是否 404。 **已知兼容点**:profile 里旧有的 `- id: tradewatcher`(`disabled: false`)重启后会变成 warning(absent target 只 warning、不挂启动),无需处理。 ## v0.58.0 — 逐日盈亏边界价:柱色与当日「盈利比例」同源(拖滑杆看历史日不再自相矛盾) ### 问题 宿主对**历史展示日**的 `profitRatio` 用「当天收盘价」,对**最新展示日**用 `priceUsed`;而筹码 DTO 没有逐日价格 ⇒ 客户端只能统一用顶层现价 ⇒ 拖滑杆看历史某天时,**柱色与该日盈利比例可能自相矛盾**(默认最新日两者一致,所以先前未暴露)。 ### 改动 **宿主**:`ChipDay` 增 `price: number | null`(注释写明口径:最新展示日 = 顶层 `priceUsed`;历史展示日 = 当日收盘价 `bars[i].close`;不可得 ⇒ `null`)。`days.push` 里复用的就是 `chips.ts:398` **那个已存在的变量**(与 `profitRatio` 同一个)⇒ **同源是构造上保证的**,不是两处各写一遍。顶层 `priceUsed`/`priceSource`/`fq`/`priceScale`/`missing`/`why` **一字未改**。 **客户端**:`ChipDayDto.price?`(**可选** = 向后兼容落点);新增 `chipsDayPriceOf({ dayPrice, topPriceUsed })` ⇒ 有逐日价就用、缺失/null/非有限 ⇒ **回退顶层**并给 `note`("未带 price(老宿主)"与"price 不可用(null)"**分开写**);都没有 ⇒ `null`(按"未取到",不猜价)。柱色改用 `dayPrice.price`;**「盈利比例」tooltip 的口径也一并修正** —— 历史日原来写"现价"是错的,现在写 `Σ(桶价 < 当日收盘价 …);当日收盘价 18.55(该展示日为历史日)`。 ### 验证 412 条测试 × 2 时区全绿(409 → 412)。**同源断言**:3 天 fixture(收盘 10/20/30)+ 现价 **25(刻意不等于任何一天收盘)** ⇒ 逐日 `price` 正确,且对**每一天**用该日 `price` 复算 `Σ(桶中心 < price 的权重)` **等于**该日 `profitRatio`(±0.01%);端到端(宿主 DTO → 客户端取价 → 柱色,10 天)**0 处不匹配**;本地真 `chipSeriesOf` 12 天 **0/12 不一致**。 **反例注入**:客户端改回统一用顶层 `priceUsed` ⇒ 第 2 天恰好一致、**第 1 天不同** ⇒ `assert.throws` 证明"第 1 天翻转点 = 其当日价"必然失败。 **向后兼容实测**(线上宿主仍是 v0.57.0、**没有** `days[].price`):客户端回退并留痕 —— `note: '该展示日未带 price(老宿主)⇒ 回退顶层现价 2.94:柱色与该日盈利比例可能不同源'`;顶层字段与 `missing` 形态都在 ⇒ **老宿主 + 新客户端行为与 v0.57.0 完全一致**,只多一条可追的 note。 装机后一行可核:`curl '127.0.0.1:3080/tradewatcher/chips?secid=…' | jq '.days[-1].price'` 应等于顶层 `priceUsed`。 ### 一个边界情况的处置(Lead 定:保持"同源"优先) 宿主原有规则未改:`priceUsed` 在"显示最后一根 ≠ 最新日线"(`barAligns === false`)时置 `null`。新增的 `days[last].price` **不跟着置 null** —— 它的定义是"**该日 `profitRatio` 用到的那个价**",而那时 `profitRatio` 确实是拿 `priceUsed` 算的;跟着置 null 会让柱色与那个数字再次分家(正是本轮要修的病)。tooltip 同时给出 `来源 ${priceSource}`(那时是 `close`),不会把"定格收盘价"说成实时现价。 ### 未做 真页面复核(GUI 需鉴权 URL,且线上宿主为旧版);`src/shared/model.ts` 未动(grep 确认该文件不含筹码 DTO)。 ## v0.57.0 — 筹码红绿倒置:同一条链上有**两处**错(方向 + 边界用错价) 用户报红绿倒置。定位后确认**两处**都要改 —— 用户口述的"平均成本线以上/以下"其实是**准确地描述了当时的错误行为**。 | # | 改前 | 错在哪 | | --- | --- | --- | | ① | `ChipsPanel.tsx:188` `dirClass(r.center - price, redUp)` | **方向反了**:中心高于现价 ⇒ 正号 ⇒ 红(该绿) | | ② | `ChipsPanel.tsx:61` `const price = day.avgCost` | **边界用错价**:盈亏边界应是"成本 vs **现价**",这里拿的是**平均成本** | ### 方向对照表(`redUp=true` 默认;`tw-up` 红 `#e5484d` / `tw-down` 绿 `#0e8f5c`) | 桶中心 vs 现价 | 语义 | 符号 | `redUp=true` | `redUp=false` | | --- | --- | --- | --- | --- | | **高于**现价 | 成本高于现价 ⇒ **套牢** | `priceUsed − center` < 0 | **绿** | 红 | | **低于**现价 | 成本低于现价 ⇒ **获利** | > 0 | **红** | 绿 | | 等于现价 / 现价不可得 | 不赚不亏 / 口径缺失 | 0 / null | 中性(不猜颜色) | 同 | 真值复算(`priceUsed 19.98` / `avgCost 19.072`):14.9 / 18 / **19.072** ⇒ 红(获利);19.98 ⇒ 中性;20.5 / 22.16 ⇒ 绿(套牢)。 ### 改动 新增**唯一着色入口** `chipsView.ts:251` `chipProfitDirOf({ center, priceUsed, redUp })` ⇒ `dirClass(priceUsed - center, redUp)` —— **仍走 `dirClass`**,`redUp` 继续统一生效,**没有写死红绿**;面板改用它并**删掉 `const price = day.avgCost`**。轮廓线(中性灰)与三条语义线(橙/紫/蓝)未触碰。 ### 边界必须用现价的举证(三条) 1. **判据本身**:套牢/获利 = 「我的成本」相对「现在的价」;平均成本是分布的一阶统计量(橙线回答"筹码堆在哪"),拿它当边界等于把统计量当价格。 2. **内部一致性**:头部 `盈利比例` 由宿主按**现价**算(`src/host/chips.ts:385-390`;字段注释 `chips.ts:118` 写明"实际用到的现价")⇒ 柱色若按平均成本分,就会与紧挨着的那个数字**自相矛盾**。 3. **外部举证**:参考实现的红绿分界在 ≈22.1 ≈ 其现价 22.07,而非其平均成本 18.57。 > 用户看到"平均成本线以上/以下"的原因:现价与平均成本接近时两种口径在图上几乎重合 —— 但 **②** 说明当时确实用的是平均成本,所以用户描述是准的。 ### 验证 409 条测试 × 2 时区全绿(408 → 409)。断言把"红/绿"钉在**真实 token 值**上(非文字):`{center:20, priceUsed:19}` ⇒ 绿、`{center:18}` ⇒ 红、`redUp=false` ⇒ 互换;**边界断言**用二分求翻转点(现价 19 ⇒ 19.000;现价 22.5 ⇒ 22.5,跟着现价走),并断言签名/实现里**都没有 `avgCost`**、面板传的是 `d.priceUsed`。 **浏览器决定性实验**:把 `avgCost` 放到窗口之外(20.50,高于所有桶)、现价放窗口内(18.60)⇒ 现价以上 14 根**全绿** `rgb(14,143,92)`、以下 7 根**全红** `rgb(229,72,77)`,翻转点落在 **18.67→18.59(= 现价 18.60)**,**不是** avgCost 20.50;若边界是平均成本则该全红(实测 14 根绿 ⇒ 排除)。 **反例注入**:假实现"方向改回 `center − priceUsed`" ⇒ 给出 `tw-up`(正是用户看到的错色)⇒ `assert.throws` 证明断言必然失败。 其它着色点核查:`dirClass` 全仓 36 处调用,**只有筹码柱这一处反了**;专搜"成本 vs 现价"型着色 ⇒ 空,无第二处。 ### 顺带发现(本轮未改,待 Lead 定) 宿主 `profitRatio` 对**历史展示日**用"当天收盘价"(`b.close`),对**最新展示日**用 `priceUsed`;而 `ChipDayDto` **没有逐日价格字段** ⇒ 客户端只能统一用顶层 `d.priceUsed` ⇒ **拖滑杆看历史某天**时,柱色按现价分、`盈利比例` 按那天收盘价算,两者可能不一致(默认最新日两者一致,不影响本次修复)。修法:`ChipDay` 加逐日 `price`(宿主已有该变量),客户端按展示日取。 ## v0.56.0 — 删两处提示条(K线区间 B/S 汇总 + 抽屉脚注);未丢口径,改为悬停可见 ### 处置原则:删之前先查"这话别处有没有" **① 上窗区间买卖图例(`kline.tsx` 原 714-733)** | 子信息 | 别处 | 处置 | | --- | --- | --- | | `区间内 B/S 笔数`、`净买入 X 份/股`、`买入均价/卖出均价` | 持仓页流水 + 读屏摘要 `aria-label` | 删(可见) | | `区间外另有 N 笔` | **没有别处**(依赖当前缩放窗口) | **搬进容器 `title`**(悬停可见) | | `区间内无买卖点` | 语义等价句在摘要里 | **搬进 `title`** | **② 抽屉脚注(`.tw-chartnote`)** | 子信息 | 别处 | 处置 | | --- | --- | --- | | `共 240 根`、`日期区间` | 滑杆行 `… · 120/120 根` | 删 | | `前复权` | 口径行 + 复权段控 | 删 | | **`(详情头为真实成交价)`** | **没有别处** | **搬到 B/S 标记的 `title` + `aria-label`** | | `MA5/10/30/60` | 上窗 MA 图例 | 删 | | `滚轮或拖动滑块缩放日期区间` | 容器 `title`(原有键盘说明)+ 控件自证 | 删 | | `本地缓存` | 实现细节(TTL 命中、数据没变) | 删常驻(字面量保留在 `chartNote.ts`) | | `B 买入 / S 卖出 笔数(来自持仓流水)` | 持仓页流水 | 删(`legend` 变量整块删) | | 分时档 `昨收 3.10 · 09:30 ~ 15:00` | **没有别处**(分时基线口径) | **保留**(不在红框内) | | `显示上次成功数据` / `非当日数据` / `休市定稿缓存` | 降级/陈旧告知,项目原则要求可见 | **保留常驻** | ### 改动与连带必改 - `kline.tsx:603` 新增唯一来源 `BS_PRICE_CALIBER` ⇒ `:613` 进 `detailText`;买/卖标记 `g` **同时**带 `aria-label` 与 ``。 - 图例文字改 `bsHint` 挂到图表容器 `title`;**可见 `.tw-ma-legend` 删除**。 - `QuoteDrawer.tsx:407-425` K 线脚注整条删除(分时那条保留);新增纯函数 `visibleProvenanceOf`(命中 TTL 缓存 ⇒ `''`;降级/非当日/定稿/带快照的本地缓存 ⇒ 保留)—— **诚实标注不回退**。 - `chartCache.ts` 删 `fqNoteOf`(唯一调用方是脚注;口径已搬到标记,避免两处同义文案漂移)。 - **连带必改**:`CHART_CONTROLS_PX` **85 → 66**(删图例少 19px:15 图例 + 4 间距,实测 65.75)。不改会把下窗多压 19px。逐段分项与"模块加载即校验"同步更新;浏览器复测缩放轴**仍 16/16px 全可见**、上窗 239→254、筹码块 130.75→140.25。 ### 验证 408 条测试 × 2 时区全绿(405 → 408)。判据是返回**违规清单**的纯函数 `visibleNoticeViolations(src)`(去注释后扫源码):真源码必须为空、反例必须非空 —— **三个反例各自 `assert.throws` 证明必然失败**(图例加回可见处、脚注加回、口径回到可见脚注)。 无头 Chromium 截图目视核对:MA 图例行下直接是缩放轴、筹码块下直接是抽屉底栏;可见叶子文本 0 处命中那 4 类字串;`.tw-chartnote` 不存在;`g[aria-label]` 与 `g>title` 都含 `… · 标记价格是真实成交价(详情头为真实成交价;前复权只改图形,不改成交价)`。 ### build.mjs 锁定片段(未改,按要求报告) 涉及 `区间内无买卖点`、`区间外另有`、`本地缓存`、`休市定稿缓存`、`显示上次成功数据` —— **一个都没改**:前两句从"常驻可见"搬到"悬停可见"(字面量原样),`本地缓存` 仍在 `chartNote.ts`(只是不再常驻显示),后两句照旧常驻。⇒ 未出现"必须改锁定字面量"的情况。 **若要求连 tooltip 里也不出现那两句,就必须改 `build.mjs` 的片段表**(Lead 范围)。 ### 未做 分时档脚注(`昨收 …`)保留;降级提示(上次成功数据/非当日/定稿)仍常驻(项目原则要求诚实标注,本轮只去掉"命中 TTL 缓存"一条);未做真页面截图(GUI 需鉴权 URL),改用真组件标记 + 真 CSS 的无头 Chromium。 ## v0.55.0 — 筹码区块死空白(真因:两处 max-height 钉死)+ 分布轮廓线(方案乙)+ 两个顺带挖出的真 bug ### ① 下方大片空白:空白属于 **body**,不是筹码块内部 高度账(真组件标记 + 真 CSS,抽屉 592px):head/tabs/foot `64.5/43/31.5` ⇒ **body clientHeight 453**(内容盒 435);口径行 25.75;**两窗格 `.tw-panes` 385** = 上窗 239 + 分割线 9 + 筹码块 **130.75**;空白 **−0.25px(=0)**。 真因:body 里**只有筹码块可伸缩**,而它被 `styles.ts:469/472` 的 `.tw-chips-body{max-height:160px}` + `.tw-chips-chart{max-height:140px}` **钉死在 184px**。旧空白模型 `空白 = 0.38×body − 267.25`;比例拖到 0.25 时 1000px 抽屉下约 **379px** 空白(对得上用户截图);抽屉 ≤842px 时表现为**内容溢出 + body 滚动条**。 修法:`.tw-panes{flex:1 1 auto;min-height:0}` 包住两窗格、body 改 flex 列、上窗 `flex:none`、`.tw-chips{flex:1 1 auto;min-height:0}`、**删掉两处 max-height**;可用高度改量 `.tw-panes`(量 body 会多算 65.25px);`maxBottom = 真实筹码块高度`;行高由 `chipsRowsLayoutOf(真实绘图区, 行数, gap)` 算 ⇒ 行**必装得下**。 修后实测(384/512/592/720 × 比例 0.25/0.35/0.5/0.62):空白全 **0**、bodyOverflow **0**。 ### ② 顺带抓到的两个真 bug 1. **末 12 行被静默裁掉**:行高按 `split.bottom`(172) 算,而绘图区实际只有 **86px**(被 max-height 钉住)⇒ 21 行占 168px、后 12 行不可见(`chartScrollHeight 55 ≠ clientHeight 86`)。这正是"可见柱只有 3–4 根"的一半原因。 2. **整幅分布上下镜像**:修前**顶行是 2.95 元**,而左轴顶部刻度是 **3.23**(柱按升序、刻度按降序)⇒ 柱与刻度、与平均成本/支撑/压力线**互相矛盾**。修后左轴顶 3.23 ↔ 首行 3.23、底 2.95 ↔ 末行 2.95(两个标的都核过)。**这是会误导判断的呈现错误**,不是美观问题。 ### ③ 连续性:选**乙**(用户拍板),并说明"空行"不是"空桶" 实测(`1.560010`,2026-10-12):窗口 2.9538–3.2272、行数 21、非零 **18(85.7%)**、最长连续空行段 **2**、覆盖 99.93%、单桶峰值 **44.98%**(轴顶 60%)。真因是 (a) 上述静默裁行 与 (b) **重尾**(一个桶 44.98%、另一个 0.002% ⇒ 条宽 0.03px 被 `min-width:1px` 兜成发丝,看着像空行)。**甲(合并桶)修不了重尾**且需改口径 ⇒ 不采用;丙改数据 ⇒ 不用。 实现:`chipsRowsOf()`(行序改价格降序)+ `chipsProfileOf()` —— 顶点 x = `widthOf(bucket)`(与柱宽**同一函数**)、y 与柱共用同一 rowH/gap;**每个桶都出顶点,空桶照实经过 0、不跳过不拉直**;颜色用新 token `--tw-chip-profile`(中性灰,≠ 橙/紫/蓝,也 ≠ 红绿)。 **UI 零描述(用户硬约束)**:轮廓 svg **无 title / 无 aria-label / 无图例**,仅 `aria-hidden`;语义只写在源码注释。浏览器实测 `title === null`、`ariaLabel === null`。 **乙不改变数据**:窗口区间/行数/非零数/最长空行段/权重和(1.000000)**一行不改**;变的只是"最长连续无任何视觉标记的行段"由 3(只数柱)→ **0**(折线经过每一行)。**视觉改善 ≠ 数据变密**。 ### 验证 405 条测试 × 2 时区全绿(402 → 405);`build.mjs` 零改动、75 项片段全保留;v0.54.0 的宽度契约与 `CHART_CONTROLS_PX` **未回退**(`splitDrag.ts` 零改动)。 断言锚真源码/真 CSS/真数据:① flex 契约 + **两处 max-height 必须消失** + "可用高度必须量 panes 而非 body" + 6 个抽屉高度空白==0(反例:筹码块钉 184px ⇒ 900px 下空白 45.9px、1080px 下 114.3px ⇒ `assert.throws` 证明必然失败);② 3 fixture × 4 高度逐项断言顶点数==行数、第 i 个顶点 x==`widthOf(第 i 桶)`、y 间距==rowH+gap、空桶 `x===0`、颜色 token 互不相等、**用户可见文案不许出现"轮廓/引导/连接线/折线/视觉"**(反例:跳过空桶 ⇒ 顶点 18≠21 失败;升序 ⇒ 与左轴刻度矛盾失败)。 ### 未做 / 待定 - 未改 `SPLIT_DEFAULT`:抽屉 ≤500px 时两窗格都不够用(上窗 57px、绘图区 6px 属退化)。取舍工具已有(拖分割线 / ↑↓,0.25–0.75)。 - `600519` 的 93 行需绘图区 ≥93px(抽屉 ≥700px 且比例 ≤0.5);装不下时按 1px 画并**如实报 `fits:false`**,界面**不加**提示(硬约束禁新增文案)。 - **待用户给 DevTools 定案**:① `.tw-panes` 的 Computed height(应随抽屉变);② `.tw-chips-body` 的 Computed height(应 >160 且随抽屉变;若恒 160 ⇒ 旧 CSS 仍在生效)。 ## v0.54.0 — 筹码条列宽度契约("算得对、看着被压"的真因)+ 日K 日期范围缩放轴被裁回(v0.48.0 回归) 两件事都在**本地 Chromium(真组件标记 + 真 `TW_CSS` + 真接口数据)**里量过,不是纸面推断。 ### ① 筹码横条被压到左侧:**条列没有宽度契约** 修复前:`.tw-chips-plot` **只有内联 `flex:1`**,CSS 层零契约;左轴 `.tw-chips-axis`(内容 + `min-width:34px`)**可被压缩**;右标签列 `.tw-chips-reflabel` 是**绝对定位覆盖层**,**不参与排版宽度**。 - **没有塌陷祖先**:链路实测 `642.33 → 692 → 692 → 696 → 720 → 1280`,全是 block/flex、`min-width:0`。 - **决定性反例(浏览器级)**:注入 `.tw-chips-plot{flex:none}` ⇒ 条列 **642.33px → 1px**、峰值条 **571.94px → 1px**,而**内联百分比一字未变(89.0432%)** ⇒ 这正是"数值算得对、屏幕看着被压"的机制。 - 修复:条列 `flex: 1 1 auto; min-width: 0`、左轴 `flex: none; max-width: 60px`、死规则 `.tw-chips-bars` 同步(`styles.ts:463-481`);新增 `chipsColumnsOf()` / `chipBarPxOf()` 做宽度账。 - 断言**从 CSS 文本里抽数**(不另造常量):契约 + 两组数值(抽屉 720 ⇒ 条列 642.33 / 峰值条 571.94;容器 940 ⇒ 条列 862.33),并用 `assert.throws` 证明 `flex:none` 反例必然失败。 - 顺带纠正题面口径:右标签列是绝对定位、**不参与**条列宽度扣减(按实测改,而不是按我给的 940/60/70 账法硬算)。 ### ② 日K 图丢失日期范围缩放轴:**被裁掉,不是被删(回归提交 `5d11d91` / v0.48.0)** `git log -S` 指向 `5d11d91`:它给上窗加了 `height: split.top; overflow: hidden`,而 `klineHeightsOf` 只扣 SVG **内部**开销(`CHART_CHROME 36`)+ 24,**没扣 SVG 之下的控件块**;`kline.tsx` 在该提交**零改动**。 - **Chromium 复现(修前)**:上窗 384px、内容 424.75px ⇒ 缩放轴**整块在裁剪线以下**(`trackVisiblePx = 0`、`barVisible = 0`)—— **DOM 里在、屏幕上没有、不报错**(这类"静默缺失"最费时间)。 - 修复:新增 `CHART_CONTROLS_PX = 6+15+4+15+2+43 = 85`(Chromium **逐段实测 84.75** ⇒ 向上取整),`klineHeightsOf` 多减这 85px;新增 `chartGeomOf()` 做纵向几何账(`clipped / overflow / overlap`)。`QuoteDrawer.tsx` 零改动(`chartHeight = kh.chartHeight` 自动跟随)。 - **修后浏览器复测**:4 种抽屉高度 × 有无 B/S 标记 ⇒ 轨道 **16/16px 全可见**、2 个手柄、5 个快捷按钮全在裁剪线内。 - **代价(如实记账)**:为给控件块腾位,绘图区 `mainH` 比修复前小约 **24px**(上窗 384 时 158→153)。要两全需改 `SPLIT_DEFAULT` 或压缩控件块 —— 未做。 ### 验证 402 条测试 × 2 时区全绿(400 → 402);`build.mjs` 零改动、75 项片段校验通过。 **未在真页面截图复核**(GUI 需 `dsh web` 打印的带鉴权 URL,本环境 401);改用"真组件标记 + 真 CSS + 真接口数据"的本地 Chromium。 **待用户定案的 DevTools 两个数**:`.tw-chips-plot` 的 Computed width(正常 **642.33px**,抽屉 720)、峰值条 `.tw-chips-bar` 的 Computed width(正常 **571.94px**)。若前者 < 600px ⇒ 条列仍被别的压住,请再给 `.tw-chips-chart` 的 computed width/display。 ### 未做 触屏 pointer 与拖拽跟手重绘(v0.48 起未做);`mainH` 少 24px 的两全方案;真页面截图复核。 ## v0.53.0 — 条宽单位约定写死 + 几何锚定断言;侧栏标签不再重复当日盈亏(含追因) ### 条宽"被压到左侧":**"单位混用"的推断被数字推翻** 真宿主实测(601872):`peak = 0.08560533`(分数)、`top = 0.1`(分数)、`widthOf(peak) = 85.60533%`;渲染层源码就是 `width: `${axis.widthOf(w)}%`` ⇒ 写进去的是 **"85.60533%"**,不是 "0.86%"。**当前工作区不存在"差 100 倍"的路径** —— 我此前那条推断不成立。 截图的两个可验证假设(**均标注为假设,未当结论**):① **前端 bundle 是旧的(最可能)** —— 客户端改动需刷新页面才生效,用户可能仍在跑 **task-69 之前**的 JS(那时条宽用 `w/max`、与轴顶不同尺度,正好呈现"极短条 + 轴到 10%");② 若刷新后复现,则是**布局宽度**问题(绘图区被左轴/语义线标签挤窄),此时需 DevTools 里 `.tw-chips-bars` 的 `clientWidth` 与某根条的 `width`。 **本批加固(未改行为)**:① 单位约定**写死在 `ChipAxis` 注释**(入参=分数;`ticks[].pct`/`widthOf` 返回=百分数;跨单位只在边界乘一次);② **几何锚定断言**(真实量级 fixture ⇒ `widthOf(peak) ∈ [80,90]%`、轴顶桶宽 **===100%**、`peak===top ⇒ 100%`、返回值必须是百分数、**渲染层不许再乘 100**);③ 核查 `axisTicks`/`bucketCenterTicks`:均为**价格(元)**、与 `centers` 同单位 ⇒ 无混用。 反例注入:`widthOf` 多乘一次 100 ⇒ `AssertionError: 峰值条宽 = peak/top(与刻度同尺度)`。 ### 侧栏标签重复当日盈亏(追因到位) `git log -S` 结果:标签写成 `` `盯盘 · ${badgeState.detail}` `` 的提交是 **`036b1a3`(v0.25.0)**;**`e9d4cb5`(v0.38.2「侧栏悬停提示砍回一行」)只删字、没有拆开标签与徽标** ⇒ 修复方向与病根错位,所以这次"又出现"。**不是 STE 文案批、也不是组件化批引入的。** 改动:① 标签只留数据时刻(新增 `BadgeState.stamp`)⇒ 改后 `盯盘 · 数据 09:42:52`;② 徽标口径统一为**百分比**:`-2.6` → **`-2.3%`**,与 tooltip **同一舍入**(零值 `0.0%`、`≥10%` 仍 `10+%` 但标量纲);③ 徽标本就不是金额 ⇒ 不构成"金额 vs 百分比"的重复;④ 未新增任何文案。 **一条自查**:本批第一版断言"自己拼 label 断言自己",反向注入**没抓到**;改为**锚在真实接线**(直接读 `index.tsx`,核对接的是 `stamp` 且不允许 `detail`)后才抓到 ⇒ `AssertionError: 真实接线必须是 stamp(标签只留数据时刻)`。 ### 验证 400 条测试 × 2 时区全绿(398 → 400);`build.mjs` 零改动、75 项片段校验通过。 ### 未做 / 待用户动作 - **截图症状的最终定因需浏览器**:未定位真实原因前**没有改数值路径**(改了会掩盖病根)。请用户先**硬刷新**(Ctrl/Cmd+Shift+R)再看;若复现,给出 DevTools 里 `.tw-chips-bars` 的 `clientWidth` 与条宽计算值。 - 徽标多一个 `%` 字符的宽度未实测(极窄侧栏观感待装机确认)。 - tooltip 里的「当日盈亏」字样保留 —— 那是**唯一一处**,删它属"删信息",需用户确认。 ## v0.52.0 — 三处实质缺陷:现价口径 / K 线缓存完整性 / 筹码图展示不全 三个值(盈利比例 94.47 / 85.95 / 79.77)追查后确认为**三个不同原因**,其中两个是真问题。 ### ① 现价与前复权桶价不在同一价格空间(口径) 筹码桶价来自 `fqt=1` 前复权序列,现价却取**未复权报价快照**。10-09 的换算因子 **1.02463**,最近 360 根里 **301 天因子≠1** ⇒ 盈利比例**低估约 5.7pp**(只影响 `profitRatio`;`avgCost`/`support`/`resistance` 不碰现价)。 修复:`priceUsed = priceRaw × (closeFq1 / closeFq0)`,两个 close 取**同一根**最新日线;日期不等则不换算。**暴露输入**:`priceUsed` / `priceSource`(`quote`|`close`)/ `fqt` / `priceScale` / `latestBarDate` / `barAlignsWithDisplay`。报价缺失 ⇒ 回落最新前复权收盘价并标 `close`;现价不可得 ⇒ 盈利比例 `未取到`(不是 0)。 反例注入:绕过换算 ⇒ `AssertionError: priceUsed = 原始价 × 因子`。 ### ② 磁盘缓存冻住了"盘中未完成的 bar"(数据完整性,影响全部四个数) `klines/1.601872_101_1.json` 的 mtime 是 **10-09 09:33(盘中)**,里面那根 `vol 222846 / close 21.93 / low 21.81`;真宿主内存同一天是 `vol 2115568 / close 22.07 / low 20.65`(≈10.5% 的成交量)⇒ **谁读磁盘缓存,谁就得到一套"看着正常但错"的筹码分布**(我们的红线)。这也正是"实现者本地那份 94.47 与真宿主不可比"的根源,现已**精确归因**。 修复:写侧 `isPartialBar`(交易日盘中抓到当日 bar ⇒ `partial: true`);读侧 **`v !== 3` 一律不采用**,`partial` 超 **60s TTL** ⇒ 回源重取;回包新增 `lastBar: {date, close, vol, partial}`;口径行写明盘中取数规则。 抽查:`klines/` **144 份全部是 v1/v2**(100% 将被忽略回源),其中约 **100 份**疑似盘中写入。旧文件不主动删,下次访问被忽略并覆盖为 v3(保留可回溯)。 反例注入:短路 TTL ⇒ `AssertionError: 未完成 bar 超 TTL ⇒ 回源重取`。 ### ③ 筹码图"展示不全"(呈现) 诊断(真宿主一帧):非零 77/96、sum 1.0、区间 5.4653→22.8187、展示日峰值 8.47%。 | 量 | 改前 | 改后 | | --- | --- | --- | | 窗口区间 | 15.3293→22.8187(覆盖 94.31%) | **10.5800→22.8187(覆盖 99.02%)** | | 布局行数 | **42**(左侧 54 个桶被阈值整段删掉) | **68 = 窗口覆盖桶数,一个不删** | | 刻度 ∈ 桶中心集合 | **[true,false,false,false,true]** ✗ | **[true,true,true,true,true]** ✅ | 修复三条:① **价格轴连续** —— 删除"权重 < 峰值 20% ⇒ 剔行",只保留质量覆盖缩窗口,窗口内**每个桶都占位**(空桶零长度、不留缝),行高 = 区间 ÷ 行数;② **刻度对齐桶中心** —— 新增 `bucketCenterTicks`(值必定是某行桶中心价),旧 `axisTicks` 标 deprecated;③ 同源保持(滑杆换日 ⇒ 轴顶与刻度一起变)。 反例注入:刻度改回两端等分 ⇒ `AssertionError: 刻度 17.2025 必须是某个桶的中心价`。 **对 Lead 推断的判定**:推断①部分成立但机制不同(旧代码从两端内移**吃掉左侧 54 个桶**,不是"96→5";"只剩 5 根粗条"是 42 行里 15 行零长度 + 每行被拉到 3px 的叠加效果);**推断②不成立** —— 轴顶与条宽**已同源**(同一 `axis.top`),截图的"轴 10%、可见条 2.5%"是同一坐标系下的正常读数。 ### 附带:日期敏感用例修绿(未放宽任何断言) `trend-fallback.test.ts` 的 P1-2 跨天后失败,根因是该用例会读到**真宿主当天写的 `trends/` 归档**。修法:顶部隔离 `DSH_HOME` 到临时目录、在**两个不同的固定假时间**下各跑一遍同一断言、每轮用全局唯一 secid ⇒ 输入完全由用例控制。 ### 验证 397 条测试 × 2 时区全绿(387 → 397);`build.mjs` 零改动、75 项片段校验通过。三处反例注入均如期失败后恢复。 ### 未做 - 多标的横截面重做参数标定(旧标定用未完成 bar,最优 k 从 ≈1.4 移到 ≈2.0;仍是单样本 ⇒ **默认值不变**:三角形 + k=1.5)。 - 仍是 2px/行(要更清楚需给分割线更多高度或缩小显示窗口)。 - 旧 `axisTicks` 未删(有旧断言引用,建议下批连旧断言一起清)。 - 三条语义线右端标签在窄高度下被裁 —— 独立问题,未处理。 ## v0.51.0 — 按 ASD-STE100 原则的团队 review 与修正(三路审计 + 三批改动) ### 审计(三路并行,只读,各出一份可执行清单) | 对象 | 产出 | 命中 | | --- | --- | --- | | UI 文案 | `docs/STE-REVIEW-UI.md` | 62 处(R1 14 / R7 10 / 冗余 16 / R19 14 / R8 2 / R17 3 / R11 9;R3·R9·R14 为 0) | | 文档与注释 | `docs/STE-REVIEW-DOCS.md` | 与代码不符 9 条、术语 2 组、冗长 4 类;R3 为 0 | | 模型契约文案 | `docs/STE-REVIEW-AGENT.md` | 28 条(歧义 6 / R1 7 / 冗余 6 / 其它 9) | ### 最重要的发现:不是用词,是契约缺口 `guidanceText()` 只列 **10** 个工具,实际注册 **12** 个 ⇒ `tradewatcher_position` 与 `tradewatcher_group_rank` **对模型不可见**。 修法**从根上消除漂移**:注入段一览改为**由工具定义生成**(`toolLine()`),短说明放 `TOOL_BLURB`(说明可省、清单不可省)⇒ "新增工具却忘写注入段"结构上不可能。 新增 `src/host/guidance.test.ts` 双向不变量:左侧取 `registerTools()` **实际注册到宿主工具表的名字**、右侧取 `prompt.section({text})` **真实注入段正文**里抓到的名字,断言双向相等(另加"至少 12 个"防"一个都没写")。**反例注入**(临时删 `position`)⇒ `AssertionError: 注入段漏列:tradewatcher_position`。 ### 文档收口(`task-73`) - **9 条与代码不符全部改完**:`DESIGN-CHIPS.md` 的窗口/桶数改为 **360 天累积 / 展示 45 天 / 96 桶**、筹码改为**六个页签都显示**、半衰期写死不可配、计算量改写(96×360≈10.4 万次,实测 ~19ms);`DESIGN-SUBPLUGINS.md` 抬头改为「**状态:未落地(方案记录)**」并把方案与现状分开、验收判据标「未执行」、**新增「回退记录」节**;三处重复事实收敛到 `cordis.patch.yml`。 - ⚠ **这一处「未落地 / 未执行 / 回退记录」已被 v0.59.0 取代**(此处保留原文以存史):`DESIGN-SUBPLUGINS.md` 现为「**已落地(v0.59.0)**」,验收判据按静态口径已验、回退记录已改写为「归因纠正与恢复记录」;六行的唯一权威处仍是 `cordis.patch.yml`(现行 `:38-56`)。 - **术语统一**:「顶栏」→「**行情条**」;「关注标的」→「**自选 + 持仓标的**」。 - **归档**:28 份一次性报告 `git mv` 进 `docs/archive/`(内容不改、历史保留),`docs/*.md` 由 41 份降到 13 份;`DECISIONS.md` 顶部加「历史记录,不适用 STE 规范」。 - **判定一处审计误报**:`README.md:97` 是 `rescue-log.json` 的数据字段说明段(无 `⚠`、无后果句),机械套 R16 属类型套错 ⇒ 不改,记入报告。 ### UI 与契约文案(`task-74` + 契约批) - R1 统一:`暂无`/`不可用`/`取不到`/`拿不到`/`缺失` → `未取到`;`结构性缺失` → `上游无此数据`(结构性那档**不并入** `本次失败`)。 - `README.md` 词表新增两行:**`不可算`**(输入不足,与 `未取到` 是两种状态,不得合并)与 **`加载中`**(界面瞬时态,不属 R19 的 10 个状态词,不得改成「进行中」)。 - 改法可复核:脚本按清单统一格式解析后套用,每条过三道闸 —— 现文**逐字命中**、该文件内**唯一**、命中处**紧邻 `aria-label`/`title` 一律跳过**。应用 20 条、同义多处替换 11 处、跳过 22 条(11 条"删整句/删括号"型需人工判定边界)。 - **主动否决并回退一处审计建议**:`CloudMap.tsx:151` 的第三态 `加载超时`→`失败` 会丢掉"超时"信息,且与同行按 `timeout` 显示的「重试」按钮语义不一致(枚举是 `loading | loaded | timeout`)⇒ 恢复原文案并写明理由。 ### 验证 387 条测试 × 2 时区(386 → 387);`build.mjs` 零改动、75 项片段校验通过。两条红线脚本核对:① `git diff` 中命中 `aria-label`/`title` 的建议被跳过、全仓无 `aria-label': ''`;② `git diff --stat src/client/styles.ts` **为空** ⇒ 两条 `data-blur=1` 清单未变、无选择器失效。 ### 未做(不扩大范围) task-74 跳过的 11 条「删整句/删括号」需人工判定删除边界(可能动到 `title`/`aria-label` 的完整原因);R5(名词串 ≤3)与 R15(列表代替长句)在清单里没有可执行替换 ⇒ 未动;R17 剩余处(真开放列举,补全等于新增内容)与两处最长段(拆段属结构性改写)⇒ 停手;审计里其余 356 条超长句未动。 ## v0.50.0 — 筹码图观感:价格轴裁到筹码密集区 + 条宽/刻度统一尺度(并修掉一处网格坐标 bug) ### 几何诊断(601872,先诊断后改动) | 量 | 改前 | | --- | --- | | 轴范围 | **5.47 → 22.82**(96 桶铺满 360 天全区间,17.35 元) | | 每行像素 | `max(1, floor(140/96))` = **1px**;可见行 ≈72/96 | | 单桶峰值 | 3.99%(缓存)/ 8.05%(live,差异在最新一根 bar) | | 条宽分母 | **= 峰值(同源)** ——"轴用 A、条宽用 B"的假设被查掉了 | | 轴刻度集 | 0/1/2/3/4%(轴顶 4.00%) | ### ① 价格轴裁到"筹码密集区" `massWindowOf`:先按**筹码质量 99% 覆盖**从两端内丢,再隐去**权重 < 窗口峰值 20%** 的稀疏行;被裁掉的质量如实计入 `dropped` 并写进 tooltip(不静默)。 **601872 实测:5.47–22.82 / 96 行 / 1px → 14.05–22.64 / 48 行 / 2px(覆盖 97.35%,轴外 2.65%)**(与券商 13.7–23.3 同量级)。 **没有**任何"按峰值归一/整体拉伸":权重数组原样使用、`sum = 1` 守恒断言仍在 —— 只是**少画**轴外与稀疏行。 ### ② "压扁在左侧"的真因:两套尺度(不是取错峰值) 条宽按 **peak** 归一(峰值 ⇒ 100% 宽),刻度按 **nice 取整后的 top**(8.05% ⇒ 10%)归一 ⇒ 同一个"100% 宽"含义不同:≥8% 的条顶满整宽、刻度却说是 10%,小条被读成"不到 2%"。 改为 `chipAxisOf` 单一来源 `{ peak, top, ticks, window, widthOf }`,条宽 `w / top × 100%`(**按轴顶**)⇒ 峰值条宽 = 3.99/4.00 = **99.8%**,与刻度同尺度;刻度覆盖到轴顶、从 0 起步;峰值为 0 ⇒ 不出刻度。 ### ③ 顺带修掉一处真实坐标 bug 竖网格位置写成 `calc(pct% − pct×44px)`(减掉标签宽度)⇒ 每条网格被拉左最多 **44px**,与刻度标签对不上。已改为 `left: ${pct}%`。 ### 验证与未做 386 条测试 × 2 时区(383 → 386,新增 `chipAxis.test.ts` 3 条:覆盖面与守恒、同源同尺度、自适应与极值)。`build.mjs` 零改动。 **未做**:20% 稀疏阈值是按观感定的经验值,未做跨标的校准(多峰/厚尾可能裁掉有意义长尾,`dropped` 已在 tooltip 给出);本地四数(avg 18.75 / profit 94.47)与 live(18.88 / 85.95)**有差异且未追因**(最可能是最新一根 bar 的取法)—— 列为待查项;语义线落在裁剪窗口外时只夹到 0–100%,**不额外提示"该线在轴外"**。 ## v0.49.1 — 断言补强:把两条"证明力不足"的判据换成有区分力的(并由代码级注入查出一个真缺口) 来自独立验证(`docs/AUDIT-VERIFY9/10.md`):**判据本身是假的**,这里修掉。 ### 补强 1:动态重绘 —— 旧判据在"假实现"上也通过 旧判据 `chartHeight − (mainH+volH+macdH) === 60` 只说明"开销没被缩放",**区分不了"按高度重绘"与"绘图区恒定"**。测试里已写出**假实现副本**(绘图区写死 `210/54/64`、无视高度)并证明**旧判据在它上面照样通过**。 换成三条有区分力的: - ① `mainH` 随高度变:真实现 `320→88`、`560→305`(互不相等且都 ≠ 假实现的 210); - ② **刻度条数随高度变**:`100→3 / 200→4 / 300→5`,且 100 与 300 的刻度集不同(不是把同一套刻度拉长); - ③ 对 `chartHeight` 注入 **364** 敏感(两高度互不相等且 ≠ 364)。 两条方向都覆盖:**真实现成立** + **假实现不成立**。 ### 补强 2:筹码依赖数组 —— 旧判据证明的是 loader 缓存 "请求数 = 1" 在 **60s TTL** 下会被缓存吸收(把 `tab` 加回依赖数组,TTL 内计数**仍是 1**,过期才变 2)⇒ 它证明的是缓存与并发合并。现在:计数断言保留但**测试名/注释写明它证明的是"60s 内不重复请求"**;新增锚在源码字面量的判据 —— `useChips` 依赖数组**逐项** deepEqual `['secid','enabled','refreshKey']`、`setChipsRefresh` **全仓仅 1 处调用且必须是 `onRetry`**。 ### 代码级反向注入查出一个真缺口 真改 props 签名 + 真加回页签门控分支(不是注释形态)后,**第一次注入全部测试通过** ⇒ 旧断言扫页签判断时用的是 `drawer + hook + loader`,**漏了面板自己**。补上"面板源码不许出现 `'5d'`、面板 props 不许声明 `tab`"两条后重做注入 ⇒ `AssertionError: 面板源码里不许出现 5d 门控`,恢复后 8/8。**旧抓不住 → 补强后抓得住**,两个结果都在报告里。 ### 验证 383 条测试 × 2 时区(382 → 383;动态高度那条是**替换**旧判据,条数不增但判据从"无区分力"变成"能抓住假实现")。`build.mjs` 零改动。未改任何产品行为。 ## v0.49.0 — 筹码呈现:96 桶(×3)+ 轴刻度与网格线 + 三条语义线 ### ① 分布精度 ×3 `CHIPS_BUCKETS` 32 → **96**(`MIN 16 / MAX 160`,`normalizeBuckets`:缺省不留痕、越界**夹取并留痕**、非数字回落并留痕;路由 `buckets` 参数保留)。 计算量:96 桶 × 360 根 ≈ **10.4 万次**浮点/标的,本机实测 **~19ms**(32 桶 ≈3.5 万次)。渲染行高 = `max(1, floor(可用/96))`(192px ⇒ 2px、96px ⇒ 1px、**40px 也按 1px 画**)—— **不静默丢桶、无 0 高度**。 为让 96 桶下权重守恒断言仍然紧,桶权重存储精度 6 → **8 位**(96 桶 × 6 位舍入会把总和漂到 ~2e-6)。 ### ② 左轴字体 + 横轴比例刻度 + 网格线 左轴价格刻度 9.5 → **11px**(同字体族不加粗)。横轴刻度 `ratioTicks(peak)`:**nice step**(1/2/2.5/5×10ⁿ 自适应)、小数位由步长决定(**不会出现 0.30000000004**)、3–6 个刻度、峰值为 0 不出刻度。 网格:横轴刻度 ⇒ 竖网格、纵轴刻度 ⇒ 横网格,颜色走新 token `--tw-chips-grid`(深浅两套),**层级在横条之下**(先网格后横条)。 ### ③ 三条语义线(平均成本 / 压力 P95 / 支撑 P5) `chipsLinesOf()` 坐标严格取输入值(**换一组输入坐标跟着变 ⇒ 不是硬编码样例**),虚线 + 右端数值标签。 配色合规:只用新 token `--tw-chip-avg`(橙)/ `--tw-chip-res`(紫)/ `--tw-chip-sup`(蓝),**不用红/绿**、不碰 `dirClass`/`toneClass`;值在 `styles.ts` 深浅两套变量块,**源码无硬编码色值**。 隐身档合规:新数值面登记进**两条** `data-blur=1` 清单 —— `.tw-chips-nums, .tw-chips-date, .tw-chips-bar, .tw-chips-reflabel, .tw-chips-xaxis`(5 类数字面,后两类是本轮新增;旧断言已同步)。 异常输入(缺值/非有限数/**P95 < P5**)⇒ 返回空数组:不出线、不抛错。 ### 验证与已知缺口 382 条测试 × 2 时区(378 → 382);`build.mjs` 零改动;反例注入(桶数改回 32)如期失败。 **未做**:横轴刻度是"占**总筹码**比例"(分布内归一化),**不是**真实流通盘占比(历史逐日股本不可得,tooltip 已写明口径);网格/语义线的视觉密度未做人眼校准;三条线标签**无避让**(极窄高度可能重叠)。 另:独立验证(`docs/AUDIT-VERIFY9/10.md`)对 v0.46.0 与 v0.48.0 均判**通过**,但指出**两条判据证明力不足**("固定开销恒为 60px"无法区分"重绘"与"绘图区恒定";"请求数=1"证明的是 60s 缓存而非依赖数组)—— 已排入断言补强批次。 ## v0.48.0 — 抽屉上下分区:切页签不再塌图 / 筹码六页签一致 / 比例可拖拽 三批合并(用户连续四条反馈)。 ### ① 切页签不再"整块刷新"(根因定位到具体一行) 根因:切页签时 `setPayload(prev => prev.tab === tab ? prev : null)` **把上一帧丢掉** ⇒ `chartBody()` 早退渲染整块骨架(`height: chartHeight` 的空盒)⇒ 图表区塌陷、看起来像整个侧栏在刷新。 改法:**不清空**(改为 `setLoading(true)`,上一帧留在 `payload`);骨架条件收窄为 `payload === null`(**只有首帧**才允许骨架);`.tw-chart-wrap` 内叠 `.tw-chart-loading`(**淡遮罩 + 2px 细进度条**,`pointer-events:none` 不挡 hover,文案"载入中 · 上图为上一周期"+`aria-busy`);新数据到达自动消失、不闪回空态;失败仍走既有 `NoteIcon`/`unavailable` 契约。纯函数 `chartFrameOf` 给出 `first / stale / fresh` 三态。 ### ② 筹码与页签彻底解耦(**我此前把它挂在页签上是设计错误**) 用户指出:**从没要求过五日档不显示筹码分布**,而且"如果上下独立,切页签就不该影响下层"。筹码分布是**标的的属性**(由日线筹码成本推出),与当前在看哪个周期无关。 删掉 `isFiveDay` 与整个五日档分支 ⇒ **分时/五日/日K/周K/月K/年K 六个页签一律显示**、同一数据与滑杆位置;那行"筹码分布只在分时/日K 及以上显示…"的文案(连注释)**已从源码删除**。 **保留**与标的/数据有关的原因:`fqSupported=false`、样本不足(<10 根)、口径偏离默认(港股回落半衰期、缺 `floatMv`)—— 正常标的**一个字都不显示**。 ### ③ 上下分区加可拖拽分割线(用户要求自由调节比例) - `.tw-split`:**9px 命中区(1px 细线 + 上下各 4px)**,`cursor: row-resize`,**无装饰把手**。 - 拖拽:起手**只挪线**(不重绘、不发请求、`preventDefault` + `user-select:none`)⇒ 松手才提交 ⇒ **按最终高度重绘一次全图**。 - 持久化:`prefs.splitRatio`(默认 **0.62**,范围 **[0.25, 0.75]**)+宿主 `setPrefs` 白名单 + `normalizeSplitRatio`;缺键/NaN/越界/字符串 ⇒ 默认并如实 `log`;装载路径同样归一。 - 边界:上窗 ≥160 / 下窗 ≥120,越界夹取;`available=0` ⇒ `{0,0}` 不抛错。 - 键盘/aria:`role=separator` + `aria-orientation` + `aria-valuenow/min/max` + `tabIndex=0`;`↑/↓` 2%、`Enter`/双击复位、`title`/`aria-label` 写明"双击或回车恢复默认"。 - 筹码区原 160px 上限**改由分割线决定**;折叠时**不渲染**分割线;**不新增数字展示面**。 **关键一条**:`chartHeight` 从**硬编码 364** 改为 `klineHeightsOf(split.top)`(`KlineChart` 的 `mainH/volH/macdH` 同源)⇒ 比例尺/轴刻度/横条位置**按新高度重算**,**没有任何 CSS 拉伸**(拉伸会把文字与线宽一起变形)。轴刻度条数随高度重算(<120px ⇒ 3 条、<240 ⇒ 4 条、≥240 ⇒ 5 条)。`KlineChart` 的 `key: tab` 原样保留,新增容器与分割线都不带 `key` ⇒ 拖动/折叠不会重挂图表或筹码。 ### 验证 378 条测试 × 2 时区(372 → 378)。`build.mjs` 零改动。三处**反例注入**均抓得住(都贴了证据后恢复):去掉夹取 ⇒ `AssertionError: 被夹住时标记 clamped`;几何改回硬编码 ⇒ `AssertionError: 图表几何必须来自 kh(不许硬编码)`;把 `isFiveDay` 加回面板 ⇒ `AssertionError: 面板不许再有 isFiveDay`。 ### 未做 / 待装机复核 拖动中**不跟手重绘**(松手后才重绘,规格允许的路线);未处理触屏 `pointer*`(键盘与 aria 已覆盖无鼠标场景);比例无常驻百分比显示(遵"不加数字面")。 装机复核:① 拖动后端**线跟手**、松手后图表按新高度重画(文字/线宽不变形);② `__twMounts()` 在拖动与折叠后仍为 `{drawer:1, chips:1}`;③ Tab 到分割线有可见焦点环、↑/↓ 各 2%、回车/双击回默认;④ 折叠筹码后分割线消失;⑤ 极矮窗口无负高度/重叠;⑥ 切页签:上半区换图叠载入态、下半区一动不动。 ## v0.47.0 — 筹码窗口 360d + 用券商样本标定(含一条"对不上"的如实结论)+ 切页签不重挂载的证据 ### A. 切页签"整块刷新":查清了,也要如实说 逐条查证:抽屉的 `key` 只绑 `secid`、组件都在模块作用域、筹码面板与图表是同一 body 的**兄弟**、五日档只换渲染内容不卸载(**筹码从未被重挂载**);`KlineChart` 上的 `key: tab` 是图表自己的身份,属预期。 **真正的观感来源**:切页签时 `setPayload(null)` 把**图表区**换成骨架(原设计:旧周期数据不能当新周期渲染)⇒ 看起来像整页刷新。 **证据**:新增挂载计数(`src/client/mounts.ts`,运行时 `__twMounts()` 可读)+ 结构断言;反例(把 `key: tab` 加回筹码面板)⇒ 断言失败,恢复后 6/6;上一轮"切 6 次页签请求数 = 1"的断言保留。 ### B. 窗口 360d + 按券商样本标定 参考样本:**1.601872(招商轮船)@ 2026-10-09** —— 现价 22.07、平均成本 18.57、支撑 20.92、压力 22.20、盈利比例 96.87%。 `CHIPS_WINDOW` 180 → **360**(上限 400),展示窗口仍近 45d、滑杆不变、`days=45` 老调用结构不破。数据全部取自**本机 kline 缓存**(802 根至 2026-10-09),未裸探上游。 网格:`k ∈ [0.5,3.0]` 步长 0.05 × 形状(三角形/均匀)× 换手率口径(`now` / price-scaled 历史代理)= 4×51 组。 **结论:三角 + k=1.5 + 口径 `now` 保持默认**(1.35 只把平均成本残差从 0.18 改善到 0.00 元,同一量级噪声,**单样本上不值得改默认**;price-scaled 只改善 0.03 元却要多背"股本不变"假设 ⇒ 实现但默认不启用): | 项 | 我们(k=1.5,now,三角) | 参考 | 残差 | | --- | --- | --- | --- | | 平均成本 | 18.75 | 18.57 | **0.18 元** | | 压力位 P95 | 21.83 | 22.20 | **0.37 元** | | 盈利比例 | 92.5% | 96.87% | **4.4 pp** | | 支撑位 P5 | **14.46** | **20.92** | **6.5 元 ✗ 对不上** | **"支撑位"这一项对不上,而且原因清楚了**:参考值 20.92 **高于**平均成本 18.57 —— 在"成本分布 + P5"的定义下**不可能**(P5 必然 ≤ 平均成本);整张网格里 P5 的**最大值只有 15.51**。⇒ 券商那个"支撑位"**大概率不是筹码分布的 P5**(更像技术位/均线,或完全不同的窗口与权重模型)。**我们没有改定义去凑这个数**:面板里它仍然是分布统计量,tooltip 写明"不是预测、不是买卖建议",文档标注"与参考值不可比"。 **诚实边界**(写进代码注释与报告):单一样本拟合、有过拟合风险;**没有任何按标的写死的分支**(无 `if secid === …`);券商算法无法验证,参考值只当拟合目标。 ### 验证 371 条测试 × 2 时区(369 → 371);`build.mjs` 零改动。 ### 未做 / 待装机复核 图表骨架导致的视觉跳动**本轮未改**(留待下一批:保留上一张图 + 叠加载入态)。装机后:控制台 `__twMounts()` 切 6 次页签前后都应是 `{drawer:1, chips:1}`;1.601872@2026-10-09 四个数人眼对照(预计平均成本≈18.6–18.8、压力≈21.8、盈利≈93%,**支撑位会明显偏低**);360d 后最早展示日是否还"全是获利盘"。 ## v0.46.0 — 筹码模块四条修正(切页签不刷新 / 去配置项 / 纵轴刻度 / 窗口 180d) ### ① 切 K 线页签不再让筹码刷新(用户点名) 新增 `src/client/chipsLoader.ts`:**入参只有 `secid`** —— 结构上不可能随页签变化(按 secid 缓存 60s + 同一 secid 并发合并);`useChips` 依赖数组改为 `[secid, enabled, refreshKey]`(`refreshKey` 是筹码自己的重试计数,图表 `retry` 不再参与);抽屉里筹码开关恒为 on(五日档只是不显示,数据保留 ⇒ 切回立即可见)。 **硬证据**(假 fetch 计数):切 6 次页签(1m/5d/day/week/month/year)后 `calls === 1`;换 secid ⇒ 2;同 secid 并发 3 次 ⇒ 1。图表数据路径未改(该重取仍重取)。 ### ② 去掉前台配置项(用户判定为过度设计) 删掉口径行与两个 select(`k=1.5`、`三角形`),默认**写死**:三角形分布 + k = 1.5。 **保留底线**:`caliberDeviates()` 只在**偏离默认**时给一行原因(港股回落固定半衰期、缺 `floatMv`、不复权标的、样本不足),正常 A 股一个字都不显示 —— 不能拿默认口径冒充实际情况。反向断言:面板源码(剥注释后)不许出现 `k=`/`三角形`/`select`。 ### ③ 筹码分布图补左侧纵轴价格刻度 `axisTicks()`:3–5 个均匀刻度、**含端点**(最低/最高桶价);与横条同一 flex 行 ⇒ **逐行对齐**;9.5px 次要色等宽不加粗。 ### ④ 累积窗口 180d、展示窗口 45d(修正"早期筹码全是获利盘") `CHIPS_WINDOW = 180`(累积)、`CHIPS_DISPLAY = 45`(展示,上限 60):**在整个 180 根上逐日递推,只为最近 45 天记录状态**。路由 `days` 语义改为累积窗口(默认 180),新增 `display`(默认 45);回包**同时**给 `window` 与 `displayWindow` ⇒ 老调用 `days=45` 结构不坏。样本规则按实际根数:`N<10` 样本不足,`10≤N<180` 照算并如实写"用了近 N 根(不足累积窗口 180 根)"。 **断言**(自造 180 根:前 135 根 10 元区 + 后 45 根 20 元区):`window=180` 与 `window=45` 在**同一天**算出的 buckets **不相等**(证明窗口真的生效);展示状态数 = 45;每天权重和 = 1(守恒)。 ### 验证 369 条测试 × 2 时区(363 → 369);`build.mjs` 零改动。 ### 未做 / 待装机复核 `prefs.chipsDecay/chipsSpread` 仍留在数据模型(默认值即写死值,宿主归一照旧;将来要再开放可调直接接 UI);缓存 TTL 60s 是经验值。装机复核:切页签时网络面板请求数应为 1、左轴对齐观感、正常 A 股只剩四个数字而港股出现一行回落原因、180d 累积后最早展示日是否仍有"全获利"观感。 ## v0.45.3 — 真因修复:真宿主的 `ctx` 是 Proxy,读未 inject 的属性会抛 v0.45.2 之后插件仍不可用(插件列表里 `include:tradewatcher` 的 `fiberPhase = failed`)。**真因**用"另起一个端口的临时实例"抓到了: ``` tradewatcher (dsh-tradewatcher): Error: cannot get property "config" without inject at apply (.../lib/index.js) ``` **Cordis 的 `ctx` 是 Proxy:访问未 `inject` 的属性会直接抛**(`ctx.config` 就是其中之一),所以 `ctx.config ?? …` 里的 `?.`/`??` 救不了 —— 属性访问本身就抛。我们的单元测试一直用**普通对象**当假 ctx,所以这个 bug 在被真宿主跑之前完全看不见。 ### 修复 - 新增 `ctxSafe(ctx, key)`:try/catch 读取可选属性;`apply` 只读**第二参数**的 config(不再读 `ctx.config`,读不到就走 `all` 默认)。 - `ctx.provide` / `ctx.get` 同样改为防御式读取(取不到就退回模块级 kernel,而不是让整行 fiber failed)。 - `servicesOf()`(`src/host/tools.ts`)不再直接 `ctx.get(...)`,逐项 try/catch:**取不到服务只是不注册工具,不该把插件整行弄挂**。 - **回归断言(关键)**:用 `Proxy` 造出"访问未声明属性即抛"的假 ctx,断言 `apply` 不抛、且缺省全量下 `/health`、`/rows`、`/calendar`、`/watch`、`/ledger`、`/breadth`、`/chips` 都注册上了。这条断言正是此前缺失的"真实宿主行为模型"。 ### 验证(真宿主实测,不是推演) 在**另一个端口**起了一次真实 profile 启动(`dsh --profile web --port 0 --no-open`,不影响用户的服务): ``` [tradewatcher] row「all」mounted [tradewatcher] store ready at /home/hu/.dsh/dsh-tradewatcher [tradewatcher] rescue monitor started interval 60s / tail 15s ``` ``` /health HTTP 200 {"ok":true,...} /rows HTTP 200 {"rows":["core","calendar","watchlist","portfolio","market","cloudmap"]} /calendar HTTP 200 {"events":[...]} /chips?secid=1.600519&days=45 HTTP 200 {"decay":"turnover","bucketCount":32,"window":45,...} /chips(缺 secid) HTTP 400 {"error":"secid 不合法(如 1.600519)"} ``` 日志里**没有** "tools service absent" ⇒ 真宿主上 `ctx.get('tools')` 正常,**agent 工具照常注册**(防御式改动没有把功能吃掉)。 363 条测试 × 2 时区全绿;`build.mjs` 零改动。 ### 结论与教训 - "代码 + 断言全绿"**不等于**"能在真宿主装载":假 ctx 的形状与真宿主不一致时,测试给的信心是假的。已用 Proxy 断言把这个缺口补上。 - 上一轮六行方案的回退(v0.45.2)仍然有效且必要:同包多活跃 Loader 源 + 带客户端半区会被 `dsh-client-modules` 判为 composition error。 - ⚠ **本条的归因已被 v0.59.0 推翻**(此处保留原文以存史):报错对象是**不同的加载源**(`sourceKey = baseUrl + "\0" + loaderName`),**不是同一加载源的多个 row**;同包六行并不触发它,已于 v0.59.0 恢复。正确表述见本文件 v0.59.0 条目与 `docs/DESIGN-SUBPLUGINS.md`「归因纠正与恢复记录」。 ## v0.45.2 — 紧急修复:v0.44/v0.45 的六行装载会让插件整体不工作(回退为单行 + 缺省全量) **用户重启后报"插件服务异常"。实测**:`/tradewatcher/health`、`/rows`、`/quotes` 全部 **404**,组合后的 profile 里**没有** tradewatcher 的任何行 —— 插件的宿主半区根本没装载。**这是我造成的**,两个原因叠加: 1. **我漏了安装步骤**:六行只写在**bundle 清单**(`cordis.patch.yml`)里,而 profile 的 patch(真正的组合真源)仍是当初单行安装时留下的那条 `- id: tradewatcher`(不带 `config.row`)。 2. **我改错了缺省语义**:P2 把"无 config"的缺省行从 `core` 改成了"只有 core 自己的部分"(当时以为这样才对),于是那条 legacy 行只注册内核那一段 —— **各功能行的前缀被 core 静态排除**,`/watch`、`/portfolio`、`/ledger`、`/calendar`、`/breadth` 等几乎全部 404。 3. 更正一处**设计假设错误**:DSH 的 `dsh-client-modules` 把"同一 package 出现多个**活跃 Loader 源**"视为 **composition error**,而本包**同时带客户端半区**(`dsh.client` + `exports['./client']`)⇒ 同包六行会被整体拒绝。要真做成可独立开关的子组件,得走"**每个子组件各自一个包**"(参考 `@linxin666/dsh-web-all` 的 `/market`、`/settings` 那样),不是同一个包开六行。 - ⚠ **本条(含"同包六行会被整体拒绝"与"得走每个子组件各自一个包"两个结论)已被 v0.59.0 推翻**(此处保留原文以存史):那句话里的 source 指**不同的加载源**,同包同基准的六行会被归并成一个 sourceKey,不会报错;当年真正的失败原因是 `apply` 读 `ctx.config`(Proxy 未 inject 即抛),**与行数无关**。六行已于 v0.59.0 恢复,**不需要拆成六个 npm 包**。 ### 修复 - **缺省行语义改回 `all`(全量)**:不带 config ⇒ 注册**全部**路由与工具(与拆分前逐字一致);显式 `row: core` 才是"只要内核那一段";认不出的值一律回退 `all`(**宁可多给,不可少给** —— 少给的表现就是用户看到的"插件服务异常")。 - **manifest 回退为单行** `- id: tradewatcher`(与 profile 里既有的 id 匹配),并加注释写明六行为何被回退、以及要重做需要换路线。 - 断言同步:缺省 `rowOf(...) === 'all'`;patch 断言改为"**只有一个 `id: tradewatcher`、不带 config、行级精确、数量守恒**"(保留 task-60 的防截断纪律)。 - 代码侧仍**保留**显式行模式(`row: core` / `row: <功能行>`)与 `apply` 幂等,供将来按"一包一行"路线复用。 ### 验证 362 条测试 × 2 时区全绿;`build.mjs` 零改动;profile 完好(`dsh-tradewatcher` 仍是指向工作区的 `link:`,其余 bundle 未受影响)。 **待用户重启一次 `dsh web` 验证恢复**:`/tradewatcher/health` 应 200、`/rows` 应返回全部行、面板正常。 ## v0.45.1 — 修正组件化归属(按实测调用方重定)与 patch 断言盲区 来自 P3+P4 的独立对抗验证(`docs/AUDIT-VERIFY8.md`,结论"有条件通过")。 ### 归属判据改成"看实测调用方",不是看名字 验证员自己解析 `api.ts` 的方法↔路径、再 grep 非测试调用点,查出三条路由的归属与真实消费者冲突: | 路由 | 原归属 | 实测调用方 | 现归属 | | --- | --- | --- | --- | | `/tradewatcher/board` | market | `MarketPage.tsx:84` **与** `IndustryHeatmap.tsx:30`(后者只被云图页 `CloudMap.tsx:125` 渲染) | **core**(≥2 行消费) | | `/tradewatcher/industries` | market | **只有** `WatchlistPage.tsx:89` | **watchlist** | | `/tradewatcher/anomaly` | market | `alerts.ts:69` ← 只被自选页引用 | **watchlist** | 原来的归属会让"关掉大盘行"**连带弄坏云图页热力图与自选页的行业列/告警扫描**(诚实 404,但不是我们想要的)。 判据已写进 `docs/DESIGN-SUBPLUGINS.md` §2.2:**按实测调用方定归属 —— 被 ≥2 个行消费 ⇒ core;单一消费方 ⇒ 该行**,并附含 file:line 的映射表(`market` 行因此缩到 `/breadth` + `/rescue`)。 新增断言逐条锁住"归属 == 调用方所在行",并给出"把 `/board` 加回 market ⇒ 断言失败"的证据。 ### patch↔一致性断言:字符类不够,还要行级精确 把 id 正则从 `[a-z]+` 放宽到 `[a-z0-9-]+` 并加数量守恒后,实现者发现**仍抓不到 `tradewatcher-portfolioX`**(大写被截断后数量与列表都不变)⇒ 再补**行级精确**断言(每个 `- id:` 行必须完整匹配)。三种注入(`portfolioX`、`market2`、全小写改名)现在都会失败,证据已入档。 ### 验证 362 条测试 × 2 时区(359 → 362)。`build.mjs` 零改动。 ### 待装机复核(用户重启后) ① `/tradewatcher/rows` 返回六行;② 关「大盘」⇒ `/breadth`、`/rescue` 404,而 `/board`(core)、`/industries`、`/anomaly`(watchlist)**仍 200**;③ 关「自选」⇒ `/industries`、`/anomaly` 404,大盘页 `/breadth`、`/board` 照常。 ## v0.45.0 — 筹码分布模块(45 日 · 换手率衰减)+「详情不可用」简化为圆形 icon ### 筹码分布(分时/K线页面上下切分,下方为筹码分布) **递推式(用户给定口径)**:`chips_t = chips_{t-1} × max(0, 1 − t_t × k) + new_t` - `t_t` = 当日换手率 = **成交额 ÷ 流通市值**(`min(t, 0.25)`)。我们拿不到流通股本,但拿得到流通市值 —— 两者等价(茅台实测 0.282%,量级正确)。 - `k` = **衰减系数**,默认 **1.5**、范围 [0.5, 3.0]:>1 时今天会"搬走"比实际换手率更多的旧筹码,用来**突出近期筹码**(已用"k 从 1.0 调到 2.0 ⇒ 近期筹码占比单调上升"的断言钉死语义)。 - 首日 `chips_1 = new_1`;`1 − t×k` 为负 ⇒ `max(0, …)` 夹住,并把**实际生效因子**与"出现过夹取"写进 `methodology`(口径与算法同源)。 - `new_t` 铺到当日 `[low, high]`:**三角形分布(默认,峰在 `(h+l+c)/3`)** 或 **均匀分布**,两者**总权重守恒**(有断言)。 **输出**:逐日 32 桶分布 + 平均成本 + **支撑位/压力位(筹码累积分布 P5/P95)** + 盈利比例;45 根 `fqt=1` 前复权;桶边界按位置线性分摊(别差一分钱换一桶);`N<10` ⇒ 样本不足;指数/期货/板块(`fqSupported=false`)⇒ **不提供该模块**并给一行原因。 **支撑/压力位的 tooltip 明写**:"这是分布统计量,不是支撑/压力的预测,也不是买卖建议";区间过窄时给一行"仅供参考"。 **港股回落固定半衰期 10 日**并写明原因:实测**港股 `vol/amount` 的缩放与 A 股不同(比值≈9926、单位未标定)**,用未标定的单位算换手率会得到一个"看着正常但错"的数 —— 宁可回落经验口径。缺 `floatMv` 同样回落(原因写清)。 **UI**:抽屉内 `.tw-drawer-body` 的可折叠区块(默认展开、160px 上限);**日期滑杆在分布图上方**;极简口径控件(口径行 `换手率衰减 k=1.5 · 三角形分布 · 近 45 日` + k/形状两个小 select,tooltip 写明 k 的语义);分布条走 `dirClass`(**跟随 `prefs.redUp`**);筹码数字与容器已登记进两条 `data-blur=1` 清单;五日档隐藏并给原因。 **手算对照**(逐位一致):3 根序列 `t = 0.01/0.02/0.03` ⇒ 因子 `max(0, 1−t×1.5) = 0.985/0.970/0.955`,逐日 `chips_t` 与平均成本均与手写递推吻合。 ### 「详情不可用」提示简化为圆形 icon 四类变体(限流 / no-source / 图表缺失 / 缺失明细)统一为**右上角小圆形感叹号**:有重试时用**原生 `<button>`**(可聚焦、回车/空格即触发,另补 keydown 便于断言),无重试时为可聚焦 `span[role="note"]`;`title` 与 `aria-label` 均带**完整原因**;点击 icon = 触发同一个 `setRetry` 自增。旧的可见长条与「重试」按钮**不再渲染**(断言核对)。 ### 验证 359 条测试 × 2 时区(344 → 359)。过程中实现者自己的三条断言抓到过三个真问题(旧按钮未移除、手算对照差 0.17、methodology 里一处 `**` 泄漏),均已修。`build.mjs` 零改动。 ### 未做(已记录) 港股换手率**仍未标定**(单位未知 ⇒ 回落半衰期,本轮不猜);历史逐日流通市值不可得 ⇒ 用当前 `floatMv` 近似(`methodology` 已写明);筹码未做成独立图表面板(本轮只做抽屉内区块)。 ## v0.44.0 — 组件化重构 P1–P4:bundle 内多行子组件(六行可独立开关) 用户要求把本插件重构成"聚合包 + 可单独开关的子组件"(参照 `@linxin666/dsh-web-all` 形态),并按 `docs/DESIGN-SUBPLUGINS.md` 分阶段推进、每阶段独立验证。 ### 机制:不需要拆包,而是**同一个 package 出现在多条 Loader 行** DSH 原生支持同包多行(官方 `dsh-base/cordis.patch.yml` 即如此),靠 `id` + `config.row` 区分,**行的 `disabled` 就是插件列表那个开关**;参考包的"子路径壳"是因为它的子组件是独立 npm 包,我们不需要。**客户端是包级单实例**(同一个包被**多个解析源** resolve 才属 composition error;**同一源的多个 row 不算**)⇒ 停用门控走 core 的活跃行端点,而不是按行拆 client bundle。 ### 六个行 | 行 | 独占路由 / 工具 | | --- | --- | | `core`(必需) | 数据层 / prefs / 行情条 / 详情抽屉 / **全部 Slot 注册** / `/rows` 端点 / 核心工具(quotes、search、position、ytd、trades、industry、undo…) | | `calendar` | `/tradewatcher/calendar*` + `calendar`/`calendar_add` | | `cloudmap` | **无 host 面**(纯前端 iframe)—— 该行就是**客户端 tab 开关**(查证后的诚实结论) | | `market` | `/breadth`、`/board`、`/industries`、`/rescue`、`/anomaly` + `rescue` | | `watchlist` | `/watch` + `watchlist`/`group_rank` | | `portfolio` | `/portfolio`、`/ledger` + `portfolio`/`ledger` | **共享面一律留 core**:`/tradewatcher/industry`(详情抽屉用)、`/tradewatcher/trades`(P1 曾误挂到自选行,P4 纠正回 core)。**停用某行 = 该行路由 404 + 该行工具消失 + 该 tab 消失**(不灰显、不留空壳)。 ### 关键实现要点 - **`apply` 幂等**(本轮头号风险):多行装载会调用 N 次 ⇒ `store.init()`/`rescue.start()` 收进 core 分支 + 模块级守卫,**只有 1 个采样器**(实测 `inits=1/starts=1`,副本注入证明断言真的敏感)。 - **`config.row`**:缺省 = `core`(老 profile 行为逐字一致);未知值/非字符串一律退回 core(不静默"无行")。 - **内核服务化**:core `provide('tradewatcher.core')`,功能行 `inject` 消费;取不到就拒绝注册(**禁止各自 `new DataStore()`** —— 多实例会并发写坏同一 JSON)。 - **`GET /tradewatcher/rows`** 由 core 持有、永远可用;客户端 `tabsGate` 三条语义:**端点不可达 ⇒ 失败放行**(避免抖动把界面弄没)、**全关 ⇒ 不渲染 tab 条**、**当前页被停用 ⇒ 回落首个可用**(自选/持仓互为回落对象,有断言)。 - **patch↔代码一致性断言**:六行与 `ROW_ROUTES`/`ROW_TOOLS` 逐行对应,防漂移(改 id/disabled 会失败)。 ### 验证 344 条测试 × 2 时区(331 → 342 → 343 → 344)。**P1+P2 已过独立对抗验证**(`docs/AUDIT-VERIFY7.md`):幂等、三处门控、tabsGate 三语义(含"可达但停用必须真的少 tab"的反例)、`rowOf` 十例健壮、patch 断言非恒真、**云图"没有 host 面"被独立确认**。P3+P4 的验证在 `docs/AUDIT-VERIFY8.md`(进行中)。过程中验证员还纠正了两处:幂等断言必须**重复 apply 同一行**才敏感(已修);`/trades` 的归属(已修)。 ### 待装机复核(本轮无法在本环境完成) **真实多行装载需要重启 `dsh web`**:跑着的宿主是旧进程(`/rows` 返回空、`/calendar` 仍 200)。重启后应确认:① `/tradewatcher/rows` 返回六行;② 逐个停用行 ⇒ 路由 404 + 工具消失 + tab 消失,且**其余 tab、行情条、详情抽屉(`/trades`、`/industry`)照常**;③ 停用自选/持仓后刷新不白屏、回落正确。 ## v0.43.0 — 五日的两个问题:渠道级实测 + 陈旧呈现诚实化 用户问:① 五日分时是否完全无法通过任何渠道获取?② 为什么"五日"在显示 17 天前的 09-24?本轮用**插件自己的路由与模块**实测回答(`docs/RESEARCH-5DAY-TREND.md`、`AUDIT-IMPL11.md`)。 ### 问题②:不是"缺数据",是**呈现会说谎** `122.XAU` 的归档里只有 09-24 一天(462 点),而口径行写「本地拼接 **1/5 天**(仅当日,从本版起累积)」、footer 只写「本地缓存」 ⇒ 读起来像"最近五日"。更细的成因也已查清:**今天确实成功取到了数**(归档文件 mtime 是今天 12:15),但拿回来的就是那份 09-24 的 LKG,而五日档在归档非空时不会再取当日 —— 于是 17 天前的数据成了"五日"。 改法(新纯函数 `shared/trendCaliber.ts` + 抽屉接线): - **滞后优先**:`数据滞后 17 天(最近一次成功快照 09-24 13:41)` 排在口径行最前;不足上限写 `仅 1 天可画`; - `1/5 天` → `归档 1 天(本插件无多日源,逐日累积)`;footer 补 `本地缓存(最近一次成功快照 09-24 13:41)`; - **陈旧判据**:自然日差 + **工作日近似**(跳周末、无节假日日历 ⇒ 口径里如实写明是近似)≥2 判陈旧,附一句「图表按真实点画,不插值、不延长」;**不因陈旧而不显示**(看历史快照是合理需求)。 ### 问题①:不是"任何渠道都拿不到",而是我们只给沪/深接了渠道 `fetchMultiDayTrend` **首行**就用 `sinaSymbol` 判空(它只认沪/深),腾讯那一档又复用同一个符号变量 ⇒ 港股/国际/商品/期货**结构上永远进不去**。README 原来那句"这些市场没有多日分钟源"混淆了"上游没有"与"我们没覆盖"。 - **解耦**后做了定向实验:**腾讯 5 分钟线对港股只给当日**(272 点、1 个交易日)⇒ 按 `isMultiDayTrend` 如实否掉,**没有为了"看起来支持"放宽判据**(已加断言钉住"272 点但只 1 天 ⇒ 非多日")。 - **东财 `trends2` 的 `ndays` 实测被忽略**(同 secid 的 `ndays=1` 与 `5` 回包逐点相同)⇒ 不接这条分支;注释从"历史记录"升级为本次实测,README 同步。 - **沪/深五日回归**:仍真 5 天 × 48 点 = 240 点。 - **国内期货**(`114.*`/`113.*`):连当日都取不到 ⇒ 文案改为「该市场当前没有任何可用的分时源」,不再显示拼接壳。 - README 事实更正为:「**我们的两条多日渠道(新浪/腾讯 5 分钟线)只覆盖沪/深**,其余市场靠本地归档逐日累积」。 ### 结论(回答用户) **五日分时并非"任何渠道都拿不到"**:沪/深是真多日(上游 5 天 240 点);港股经实验确认腾讯只给当日;东财忽略 `ndays`;国际指数/外盘商品/期货在当前免费源下**没有可用多日渠道**,只能靠本地归档逐日累积(这也解释了为什么"从本版起累积"——它是唯一途径)。 ### 验证 331 条测试 × 2 时区(323 → 329 → 331)。过程中又抓到两处静默失效并修掉:`caliberExplain` 一处**跨行模板的 `**` 泄漏**(旧扫描规则漏掉,规则已升级为"剥注释→去掉合法指数运算符→其余 `**` 全算泄漏");以及实现者第一版在界面里 `slice(0,5)` 把 `2026-09-24 13:41` 截成 `2026-` ⇒ **滞后判定静默失效**(已改为传原始 label 并加断言钉住)。 **未做**:港股五日仍不可用(腾讯只有当日,需另找源);国内期货无源;陈旧判据是工作日近似(无交易日历,国庆这类长假仍按工作日算,已在口径写明)。 ## v0.42.0 — 下一轮一起做:信号前瞻表现 + 做T 配对 + 分组一屏排序 + 批量录入 用户要求把分诊表里"下一轮"的条目**一起做完**。拆成 A(宿主/工具/文档)与 B(客户端界面)两批顺序执行,每批独立验证。报告:`docs/AUDIT-IMPL9.md`、`AUDIT-IMPL10.md`、`AUDIT-VERIFY6.md`。 ### 最高价值:信号的前瞻表现(补上我们完全缺失的那一层) 新 `host/signalForward.ts`,接进 `tradewatcher_rescue` 的 `forward` 段:每个信号事件给 `信号日 / 等级 / 触发指标 / 至今涨跌幅 / 同期基准(沪深300)/ 超额 / 持有期(自然日 + 交易日)/ 最新数据日`,汇总给 `样本数 / 胜率 / 平均超额 / 中位数`。 **口径纪律(红线)**:**基准与持有期必须同时出现**(竞品报告的反面教材是"有表无基准");样本 <10 照旧输出但标注「样本过少(N < 10),不构成统计结论」;`disclaimer` 明确写**不是"信号有预测能力"**,只能读作"过去 N 次信号之后发生了什么"。 退化情形:信号日=最新数据日 ⇒ 持有期 0、涨跌幅 **`—`(不是 0%)**、不计入样本;基准缺失 ⇒ 超额 `—`。 ### 做T / 日内往返配对(竞品唯一独特能力,我们的数据条件更好) 新 `host/dayTrade.ts`,接进 `tradewatcher_portfolio.dayTrades`:**保留时序方向**(先买后卖=正T、先卖后买=反T,方向如实标 —— 竞品把"先卖后买"渲染成"买入→卖出")、**按次数统计**(同日两轮 + 次日一轮 = **3** 次,不是 2 天)、每次给买卖价/毛盈亏/费用分摊/净盈亏,**数量对不齐如实单列未配对**(真实账本 85 笔单边单列)。 ### 批量分组一屏排序(把"批量取数 → 排序 → 一屏"这一层补上) 新 `host/groupRank.ts` + 工具 `tradewatcher_group_rank`:真实自选分组实测 `上涨 6 / 平盘 2 / 下跌 6`,**药丸与分档行各档之和 = 总数**;缺失沉底。 ⚠ 实现过程中**自己抓到一次"看着正常的错数"**:第一版把 ±3% 的涨跌幅套进 0–100 分位桶 ⇒ 14 只**全部**判成「极冷」(有值、有档位、看着完全正常)。已修:涨跌幅必须显式走三档桶(`returnBucketOf`),并把反例(`bucketOf(3.14) === '极冷'`)写进断言钉住。 ### 批量粘贴录入(当前最大效率瓶颈) 持仓页分组「…」菜单 → 三步模态(粘贴 → 预览 → 完成):五种分隔符、预览表带「记为」列(建仓/买入/卖出)、**解析失败逐行报错并保留原文**、写入只走既有 `mutatePortfolio`、**支持一次撤销**(整批真删 + 宿主写留痕,实测流水 5 → 3、条数精确还原)。 解析层有 7 条断言,含**与逐条手工录入字段级等价**。 ### 另外几条 - **滞后自动告警**:日频 >3 天 / 实时 >30 分钟才自动输出一行(11 天、45 分钟触发;2 天、5 分钟、`asOf=null`、时钟偏差**不输出** —— 断言钉住"非恒输出")。 - **工具索引**:README 新增「agent 工具索引」表(12 个工具),并有断言**锁住它与 `tools.ts` 的 defs 一致**(加一个假工具会失败,已给证据)。 - **费用按每股摊销**:`¥x/股`(数量 0 ⇒ `—`,绝不给 Infinity)。 - **文本分位**:`近 30 日分位 82%(↑12,8 日新高)`,算不出来就不写那一段。 - **§6.5 半步**:只为两个新工具写了 `output.schema` 关键字段,既有 10 个工具的文本输出一字未改。 ### 验证拦下的两条 P1(已修) 1. **`deleteLedger` 用子串匹配会误删**:按 `批量录入#1` 去删,`#1`、`#12`、`#1x` **三条一起没了**(契约写的是"前缀",实现是 `includes`)。改为**整词精确匹配**(标记前必须是串首或空白、后必须非词字符),字段改名 `notePrefix` → `noteMarker`(名字叫"前缀"却是前缀匹配,正是病根);`#1x`/`#12` 不被命中、`卖 批量录入#1` 要命中,取舍写死并钉进断言。当前真实路径本就不可达(批次号是定长 base36),但误删不可恢复。 2. **Markdown `**` 泄漏回归**(工具 description / methodology / disclaimer 共 12–25 处,取决于扫描宽度):此前专门清过一次、当时为 0 ⇒ 属回归。全量清 0,并新增**扫描断言**(剥注释后扫字符串字面量,`**` 计数必须为 0;临时插一个即失败,已给证据)。 修复过程本身也有教训:第一版清理脚本把 `**` 当子串全局删,**误伤 JSDoc 头与指数运算符**;按"类型检查器即完备判据"(运算符被删必然语法错误)验证并逐行还原缩进后才收口。 3. D3:留痕从"只记 id"改为**记下可恢复的关键字段**(verb/id/secid/数量@价格/ts/note),使注释里"可从那边恢复"成立。 ### 验证 323 条测试 × 2 时区(296 → 312 → 319 → 323)。独立验证员(非实现者)自造反例:信号前瞻手算涨/跌两例全对、退化为 `null` 而非 0;做T 三项(次数/方向/未配对);分组桶的**错误形态对照**(`bucketOf(3.14)='极冷'` vs `returnBucketOf(3.14)='上涨'`);滞后告警 8 例边界;工具索引断言非恒真;批量录入往返一致;新数字面在两条模糊清单;`build.mjs` 零改动。 ### 未做(已记录) 位置口径的分档卡(客户端没有日线,需宿主在持仓行给档位字段);`marketState`/`showing` 仍未在回包填值;扫描器未升级为跨行状态机;做T 样本受真实账本限制。 ## v0.41.0 — 竞品评测落地轮:先修一处我方静默错答,再吸收「口径纪律」 隔壁团队评测了竞品 **Arkvol Dashboard** 并给出改进清单(`arkvol-workbench/audit/EVALUATION.md`:P0×11 / P1×15 / P2×12 + 反向清单)。本轮做了**产品分诊** + **P0 实现** + 独立对抗验证。 ### 头号是一条**我们自己造成的静默错答**(v0.40.0 回归) v0.40.0 往 `TW_ROWS` 里插入「国债」组后,`tools.ts` 仍按**位置**硬编码 `TW_ROWS[2]` 取商品组 ⇒ **`tradewatcher_quotes commodity` 返回的是国债指数**、`all` 漏掉全部 8 只商品。有价、有出处、看着完全正常 —— 正是本仓库的红线。 改法:新增 `twGroupSecids(key)` / `twAllSecids()` **按组 key 查**,工具改用它并补 `bond` 预设;新增断言**并给了"修前会失败"的证据**(临时改回位置索引 → `actual: [国债 5 只]` / `expected: [商品 8 只]`)。 独立验证时发现该断言是"实现入口 vs 同源函数"的比较、**共用函数本身被改错时会恒真** —— 已补两条**互不相同的独立基准**(① 直接读 `TW_ROWS` 数据比对;② 字面项数表 `6/9/5/8/28`)。实测:`cn=6 intl=9 bond=5 commodity=8`,合计 **28**。 ### 第二处确定性缺陷:`ledger` 静默截断 默认 `limit=100` 而账本已有 149 条,且回包**不报 truncated**。现在默认 500,并如实回 `returned/total/truncated/limit`(真实账本 149 条 ⇒ `truncated:false`;旧默认对照为 `returned:100, truncated:true`)。 ### 吸收的三类「口径纪律」(报告里最值得学的部分,皆为纯模板) - **缺失四态**:`代码不存在 / 上游无此数据 / 本次失败 / 后台更新中`,并要求**无效代码不许渲染成"稍后重试"**。只有前两态有产出点;另两态**故意不接线**(区分不了就不硬造),启用条件写进 `docs/DECISIONS.md`。 - **档位给区间边界**(`适中(区间 33–67)`)、**样本窗 + 样本数**后缀(`有效样本 N(M 根日线)`)、**`methodology` 成为数据字段**(`tones`/`ytd`/`breadth`/`rescue` 四条链路随回包返回,口径与算法同源)。 - **三段说明**(怎么算 / 怎么读 / **不能用来干什么**):护盘口径末句写明"不能断言国家队已入场";常驻可见仍只有一行,长文本进 `title`/aria。 ### 出处:滞后时长人话化(部分落地,未硬凑) `DataProvenance` 新增 `lagHuman` / `marketState` / `showing`。**`lagHuman` 已落地**(`刚刚 / N 分钟前 / N 小时前 / N 天前`,拿不到时刻给 `null` **不编**)——报告要的「11 天前的快照」现在能直出。`marketState` / `showing` **只落了类型、未在回包填值**(每个源都要先确认"快照时刻"来源),**没有半成品上线**。 ### 两个新分析视角(我们缺、但数据本来就有) - **位置类指标**(新 `host/position.ts` + agent 工具 `tradewatcher_position`):52 周区间位置%、距高低点% + 文本标签(`极接近低点`…)。窗口是**单一常量并随结果返回**;**必须前复权**(指数/期货按原始价并如实标 `fqSupported=false`)。真实标的实测:`1.600519` 前复权 `posPct 28.79 · 接近低点 · 243 样本`;`1.000012`(fq=0)`posPct 100 · 极接近高点`。 - **组合构成**:权重、市场/行业分布、集中度(top1/top3/HHI);`weightOf` 从客户端**上移到 host**。真实持仓实测**权重合计 = 100.0000**、`top1 23.37% / top3 59.45% / HHI 0.1641`;行业取自本地分组名,**拿不到就 `—`**(绝不写"未知")。 过程中抓到一个**单位陷阱**:`weightOf` 是**分数**、组合构成用**百分比** —— 合并成一个函数时被客户端既有断言当场抓住,已拆成两个函数并注释隔离。 ### 产品分诊(`docs/PRODUCT-ARKVOL-TRIAGE.md`) 38 条逐条给结论:**本轮 10 条**(即上面这些)/ 下一轮 12 条 / 待议 9 条 / **明确不做 2 条** + 反向清单 6 条。 分诊时**自己回读源码**,纠正了报告两处:① `all` 应为 **28 项**(报告写 16);② **行业 α 在 host 侧根本没有纯函数**(只在 `client/sort.ts`),报告把它与 tones/breadth/anomaly 并列成"调现成函数"不准确 —— 接它需先写纯函数。 **反向清单第 7 条(温度矩阵跨标尺陷阱)确认不做**:我们的五档是**自参照分位**,与 Arkvol 的"统一标尺"并排会得到"看起来可比、其实不可比"的表;若将来要做,前置是一次**口径决策**(池化分位 或 等权 z-score),**禁止对自家分位取平均**。 架构债(`render` 只允许 text 块、数值格式化成中文)本轮只做**半步**建议(字段名进 `output.schema`),全量双通道排入下一轮。 ### 验证 296 条测试 × 2 时区(282 → 294 → 296)。独立验证员(非实现者)**副本注入**复核:模拟旧 bug 后回归断言失败;位置三例(新高/新低/中段)与 `fromHigh/fromLow` 独立手算吻合、越界是"夹取 + 如实说明";组合权重合计与 **HHI 按 Σw² 复算一致**(0.38 / 0.5556 两例);单位陷阱确认隔离;`methodology` 真在四个回包;`lagHuman` 拿不到时刻给 `null`;`marketState/showing` 确认零赋值点。 ### 未做(已记录) `marketState`/`showing` 接线;四态全量铺开(需可判别错误码);行业 α 工具(需先写纯函数);⚠ 报告里"重型可视化/多套外壳/颜色表情绪/L2 数据/未探活数据源"等反向项一律不搬。 ## v0.40.0 — 可配置行情卡片(齿轮 + 可拖动配置窗)+ 五国国债 + 五档状态 badge 用户要求:① 行情卡片可配置(右上角齿轮 → 可拖动的半透明圆角悬浮配置窗),并新增中国/美国/日本/德国/英国**五国国债**;② 每张卡片右侧挂一个基于**近 30 天涨跌幅与动量**的五档状态 badge(过冷/偏冷/适中/偏热/过热),每日一个,**不许改变卡片尺寸**。 前置两路只读调研(先定数据可行性再动手):`docs/RESEARCH-BOND-STRIP.md`(用插件自己的路由取证)、`docs/DESIGN-STRIP-CONFIG.md`(交互与视觉规格)。 ### 五国国债:只有中国现在真能取到,其余四国如实 `—` | 国家 | 代码 | 形态 | 现状 | | --- | --- | --- | --- | | 中国 | `1.000012` | 价格指数 | 实测有价 + 800 根日线 | | 美国/日本/德国/英国 | `171.US10Y`/`JP10Y`/`DE10Y`/`GB10Y` | **收益率 %** | 代码经搜索证实存在,但**当前取不到值**(东财三台行情主机整窗熔断,且 `171.*` 无腾讯/新浪兜底) | 这四张卡**如实显示 `—` + 原因**("日线本次未取到…稍后随轮询重试"),**没有 0、没有代理指标**;东财恢复后同一路由自动可用(复核命令写在调研文档 §1)。 **收益率与价格方向相反**:卡名行尾标类型字(`率`/`价`),收益率卡主值形如 `4.23%`、副值 `±bp` 且**不套涨跌红绿**。 ### 五档 badge:分位法,缺失绝不用「适中」冒充 - `R30` = 近 30 个交易日涨跌幅;`MOM` = 近 5 日涨跌幅 − 近 20 日涨跌幅;各自按**该标的自身**历史分布标准化(250→120→60 根降级),`score = 0.6·z(R30) + 0.4·z(MOM)`;档位取 **score 在该标的自身分布中的分位**:`<10%` 过冷 | `10–30%` 偏冷 | `30–70%` 适中 | `70–90%` 偏热 | `>90%` 过热。 - **收益率类先取负**(只作用于变化量 R30/MOM,**不是整条序列** —— 实现者第一版写成序列取负,那是恒等变换、等于没生效,被自己的对照断言当场抓住)。 - 样本三档:`bars ≥ 31` 才可算、`5–30` 显示「样本不足」、`< 5` 或历史不足 30 个 score 样本 ⇒ `—` + 原因。**任何缺失都不出现「适中」**(验证员用 0..35 全扫 + 退化输入实测确认)。 - **每日一个**:宿主按 `(secid, 交易日)` memo(失败冷却 10 分钟),复用 `em.fetchKline` 的既有磁盘缓存(未新建缓存层);并发沿用 4-worker;**首屏先出卡片、badge 后到**(badge 失败只吞自己)。 - 新增路由 `GET /tradewatcher/tones?ids=`,沿用既有读契约(`asOf/stale/source/missing[]`,`why` 分 `no-source`/`transient`)。 ### 齿轮 + 可拖动配置窗 齿轮落在 `.tw-topmeta` 按钮组最左;窗口 `z-index:1180`、半透明圆角、开窗时隐藏悬浮行情卡(否则 9999 层级会盖住窗口);拖动用 header 把手 + pointer capture + 视口 clamp(`[8,vw−w−8]×[8,vh−h−8]`)。 **真焦点陷阱**:`role=dialog` + `aria-modal`、打开聚焦首个控件、`Tab/Shift+Tab` 窗内循环、`Esc` 关闭并把焦点**还给齿轮** —— 拖动只是鼠标增强,键盘能完成全部配置动作。 `prefs.stripCfg = {hidden: string[]}` **存隐藏集合** ⇒ 老 profile 无该键=全可见、将来新增卡片默认可见(存"显示集合"会让老用户看不到新卡,是最典型的向后兼容事故)。不做调序(顺序在两端共享的 `TW_ROWS` 里,两个真相源会打架)。 ### badge 视觉(按设计规格) 绝对定位在卡片**右上角**(32×14 胶囊、圆角 7、10px 字),**卡片外框逐像素不变**:唯二让位是 `.nm` 内边距与延迟绿点的左移,且**只在有 tone 时生效**(无 tone 的卡片渲染完全不变)。 五档用**独立色系**(靛蓝→青→石板灰→紫→品红,浅/深两套,10 个值按 WCAG 实算均 ≥4.5:1),**不含涨跌红绿**(`grep` 计数 0),暖端**刻意避开橙/琥珀**(与既有警示色几乎同色);色盲兜底是档位词本身。 ### ⚠ 验证拦下的一条 P0(静默数据丢失) 验证员实测:`setPrefs` 是**逐字段白名单**、**没有 `stripCfg` 分支**,而客户端是"乐观更新 + 静默吞错" ⇒ **配置在当次会话看着完全正常,刷新/重启后全丢且无提示**。 修复:补 `stripCfg` 分支(`normalizeStripCfg`:非数组⇒空、只留非空字符串、去重、上界 400)+ **装载路径两处同样收敛**(手改坏的 `prefs.json` 不让启动失败)+ 客户端**保存失败时回滚乐观状态并提示一次**。 新增 3 条**跨实例**回归断言(写入 → `new DataStore` 重新装载 → 读回一致),并给出"**修前会失败**"的实证(临时注释掉分支 → `actual {hidden:[]}` / `expected ['1.000001','171.US10Y']`)。 方法论修正:验证员给的最小复现命令本身有假象(`getPrefs()` 同步、未 `init()` 时返回默认值 ⇒ 有没有 bug 都打印 `{"hidden":[]}`);实现者补 `await init()` 后实测 `读回 = {"hidden":["1.000001"]}`,Lead 另行独立复跑确认(含去重与非字符串过滤)。 ### 验证 282 条测试 × 2 时区(263 → 278 → 282)。独立验证员逐块核过:分位分档(上行加速⇒过热 99.6 分位 / 下行加速⇒过冷 / 边界八例)、样本三档与红线全扫、收益率取负(同序列 price⇒过热 +43.04 / yield⇒过冷 −43.04,分位互补)、memo 与冷却(调用计数 1→1→2)、`171.*` 契约(level/r30/pct 全 null、无代理)、卡片外框(绝对定位 + `[data-tone]` 属性让位 + 未改宽高)、焦点陷阱、两条模糊清单、五档无红绿字面量、`build.mjs` 零改动。 验证员同时**修正了我的一条验收口径**:我写"横盘 ⇒ 适中"不成立(分位法下噪声横盘只有约 1/3 落适中)—— 算法没错,是描述错了;现已用断言固定"能落适中且不集中于单一档位"。 ### 未做(已记录) 调序(设计判定不做);窗口位置不落 prefs;`171.*` 的可得性复核需等东财恢复;badge 未接入 agent 工具。 ## v0.39.0 — V3 复审轮:有效性修复 + 死码清理 + 原则合规(263 条测试) 用户要求"再次 review,重视**简洁有效**"。三路只读审计(有效性 / 简洁 / 原则合规)→ 一个四段实现批次 → 独立对抗验证(结论**通过**,无 P0)。 报告:[`docs/AUDIT-V3-EFFECT.md`](docs/AUDIT-V3-EFFECT.md)、[`docs/AUDIT-V3-CODE.md`](docs/AUDIT-V3-CODE.md)、[`docs/AUDIT-V3-COPY.md`](docs/AUDIT-V3-COPY.md)、[`docs/AUDIT-VERIFY3.md`](docs/AUDIT-VERIFY3.md),实现记录 `docs/AUDIT-IMPL6.md`。 ### 有效性:五日本地归档拼接此前"写了从不读"(本轮核心) 判据原是 `em.ts` 里的"上游结果**点数 > 1** 就短路",而备用源(腾讯)对**港股/美股**返回的正是**当日**数据 ⇒ 这条路永远走不到归档拼接,`trends/` 里的归档(54 个标的里 45 个只有 1 天)写了从不读。 改为新纯函数 `isMultiDayTrend(points)`(**不同日期数 ≥2** 才算多日)。实测(注入上游、零真实请求):当日 272 点 ⇒ `source: "local-stitch"` 且带 `coverage`;真两天 ⇒ 走上游、不标 local-stitch;**同一份数据上旧判据会短路 —— 修复确实改变了行为**。 同批另外三条"看着能用"的:`missing` 原样把 11 个日期塞进常显口径条 ⇒ 改 `missingBrief`(**只列最近 3 个 + 「另有 N 天」**,11 天输入实测 26 字);备用源下"按金额/主力资金"排序**可点但无效**(字段全 null)⇒ 置灰 + `title` 说明;云图无成交额时不再画空白图,直接说明不可用。 ### 简洁:清掉我们自己删出来的死代码 `wide.ts`/`useWide.ts`/`wide.test.ts` 三个文件(列头排序删除后**生产零调用**)、`data-span` 死分支、4 个零引用导出。 新增 `host/atomic.ts`:6 处原子写(其中 writeLog 与 calendar persist 逐字相同)收敛到一处,逐处语义比对一致。 `breadthView` 的本地 `num` 并入 `numOrNull`(**`em.ts` 的字符串解析版明确不合并** —— 合并会把东财以字符串返回的字段整片判成 null)。 ### 原则合规:按七条逐条收敛(详见 `docs/AUDIT-V3-COPY.md`) - **M1**(唯一同时踩两条规则的站点):家数红条的可见正文原样拼 40–60 字原因,且原因含"备用源不含该字段"这种**结构性**事实 ⇒ 正文压成「涨跌家数未取到」,原因进 `title`/`aria`;**红条本体保留**。 - **M3**:"同一条边两种红":呼吸关键帧写死 `rgba(229,72,77,…)` 而竖条用 `var(--tw-up)` ⇒ 改 `color-mix(in srgb, var(--tw-up) 10%, transparent)`。 - **M4**:`.tw-pos-title small`(内容是涨跌幅)漏登记两条 `data-blur=1` 清单 ⇒ 补上;`.tw-cal-day`/`.tw-code` 写进"非行情数字豁免"注释。 - **M5**:细节卡片的 `aria-label` 挂在**没有 role 的 div** 上 ⇒ 读屏读不到,补 `role="note"`。 - 另:删一处恒真句;20 处越表状态词/时间词收敛到用词表(并给词表补"**窄容器允许简称 `数据`**",否则规则与侧栏徽标实现永久冲突);`该标的不适用分时段网格` → `无固定交易时段`;13 处内联 `redUp` 三元抽成 `upDownColors(redUp)`(内联三元归零);两处硬编码深色色板集中到 `client/theme.ts`。 ### 文档与收口 `DESIGN-DASHBOARD.md` 加「**未落地**」抬头(v0.33.1 已回退);9 份一次性报告移入 `docs/archive/` 并同步链接;**S10 明确关闭**(测试文件按批次命名保留);`--tw-danger` 分离记一条待议。 ### 验证 263 条测试 × 2 时区。独立验证员(非实现者)自跑门禁、自造反例:A1 判据五种构造、调用点确实使用它、端到端回包(临时 DSH_HOME + mock fetch,零真实请求)、`wide.test.ts` 被删的三条断言其被测函数**同批一并删除**(不是"删测试换通过")、`WIDE_MIN_PX` 断言**注入证明非恒真**、6 处原子写逐处比对、`store.ts` 的 `actor==='tool'` 读入兼容仍在、合规各项按文本核对、`build.mjs` **零改动**。 它提出的 D1(注释仍指向已删的 `wide.ts`)已修;D2/D3(红条实测 7 字、异常 label 判据方向安全但当前不可达)仅记录。 ### 未做(已记录) A2(归档覆盖率低:修它要改"自动刷新时顺带归档自选标的",属写入策略与磁盘/请求量的产品决策);S7(同句两处实现,可见层不违规);K8(非故障语义的 6 处红色是否新增 `--tw-danger`,待议)。 ## v0.38.2 — 侧栏悬停提示砍回一行(它不该挂着数据当后缀) 用户指出:侧栏插件名后面那串 `盯盘·持仓当日盈亏 +0.07%(+795.00 元…` 又长又乱 —— 而且它**刚好违反刚入库的第一条开发原则**。这条是我们自己的问题(侧栏徽标的 `detail` 文案),不是宿主加的。 ### 改了什么 侧栏悬停提示**只留一行**,只写"是什么 + 数据时刻";被截断的金额、与标签重复的等级数字、可从上下文得知的"持仓"等词全部去掉: | 位置 | before | after | | --- | --- | --- | | 持仓当日盈亏 | `持仓当日盈亏 +0.07%(+795.00 元)· 数据 15:00` | `当日盈亏 +0.07% · 数据 15:00` | | 护盘 | `护盘信号 疑似护盘(等级 2)· 数据 15:00` | `护盘信号:疑似护盘 · 数据 15:00` | | 隐身视图 | `隐身视图:只显示点位(沪深300 3963)· 数据 15:00` | `隐身视图 · 点位 3963 · 数据 15:00` | | 无持仓行情 | `持仓当日盈亏 0.00%(0.00 元)· 数据 …`(**把"算不出"写成了 0.00%**) | `当日盈亏 —(无持仓行情)` | 最后一行顺带修掉一处**信息错误**:`dayPnlPct === null`(没有持仓行情,不可算)原先落进 `0.00%` 那个分支,等于把"不知道"说成了"今天没赚没亏"。现在 `null` 与 `0` 分开。 ### 防复发 - 文案从 `index.tsx` 里提出来,成为纯模块 `client/badgeView.ts`(此前它藏在 `.tsx` 里,`node --test` 跑不到 ⇒ 没有断言能锁住它)。 - 新增 4 条断言,其中两条是**硬约束**:悬停提示长度不得超过 `BADGE_DETAIL_MAX`(32 字,超了就会被宿主截断成 `…`,等于没写);**金额不许出现在悬停提示里**(面板里已有,宽度够)。另两条锁"缺失与零分开"与"徽标文字/配色档位不变"。 ### 验证 264 条测试 × 2 时区(260 → 264)。 ## v0.38.1 — 开发原则入库 + 口径标签简化 + 修复卡片涨跌色(CSS 覆盖) ### 1) 「不冗余解释、不啰嗦」写进开发原则(可执行版) `README.md`「开发」节新增子节,`docs/DECISIONS.md` 记同题决策。**不是口号,是七条可机械执行的规则**: 1. 常驻可见的说明性文本**只留一行**,只写"影响什么 + 原因";长解释、口径、边界一律进 `title` 与 `aria-label`(读屏要读得到 —— 只挂 `title` 等于键盘/触屏拿不到); 2. 同一事实全局**只留一处**(优先级固定:行级 tooltip > 面板级 hint > 正文); 3. 恒真/恒假句不渲染;空值**不占位**(缺失一律 `—` + 原因,不用 0); 4. 状态词、时间格式、单位**用词表统一**(`未取到`/`上游无此数据`/`数据时刻`/`HH:mm:ss`); 5. **结构性限制不进红色告警**(红色只留给故障); 6. **口径标签只写通用词**(如 `涨跌幅`),具体口径进 `title`/`aria-label`; 7. 新数字面必须登记进两条 `data-blur=1` 清单;涨跌着色一律走 `toneClass`/`dirClass`,**不许写死红绿**(第 7 条的前半句正是下面那个 bug 的教训)。 ### 2) 卡片口径标签统一为「涨跌幅」 `涨跌(距开盘)` / `涨跌(距首点)` / `涨跌(距当日开盘)` ⇒ 可见标签**一律 `涨跌幅`**(第 6 条原则的直接应用)。 **口径不丢**:搬进该行的 `title` 与 `aria-label`(`PCT_HINTS`),三种口径互不相同且各含关键短语。 ### 3) 修复涨跌幅百分比不上色 —— 真因是 CSS 覆盖,不是 tone 算错 ``` styles.ts:161 .tw-up{color:var(--tw-up)} .tw-down{…} ← 单类,0,1,0 styles.ts:420 .tw-chart-tip-v{…;color:var(--tw-text)} ← 同为 0,1,0 且在**后** ⇒ 覆盖掉上面那条 ``` 于是 `tw-up`/`tw-down` 类**挂上了也不生效**,卡片里每个值都被染成中性色。 修法:加三条**作用域提升**的规则(`.tw-chart-tip .tw-up{…}` 等,特异性 0,2,0,不依赖书写顺序)。 顺手扫了同类冲突(元素同时带 tone 类与另一个设 `color` 的类、且后者更靠后):**唯一真冲突就是这一处** —— `.tw-chart-tip-k` 不带 tone、`.tw-num/.tw-ytd` 不设 `color`、`.tw-metric`/`.tw-idxcard` 在本仓库不存在,都没动。 ### 验证 260 条测试 × 2 时区(258 → 260)。Lead 独立复核:① 样式文本里作用域规则**确实位于基础规则之后**(字符位 33551 > 33267); ② 三种口径下可见标签**都是 `涨跌幅`**,而 hint 三条互不相同(无表标的那条明写"按首点计算"); ③ `toneClass` 与全站 `dirClass` 在**两档配色下对同一数值给出同一个颜色**(四组合逐一对齐)。 **渲染像素仍无法在本环境验证** —— 样式层只能证明"类挂得上、规则赢(0,2,0 且在后)",实际红绿需装机复核。 ## v0.38.0 — 卡片:分时涨跌幅改「距开盘」口径 + 涨跌着色跟随 redUp ### 1) 分时档的涨跌幅改用「距开盘」 用户口径要求。新纯函数 `trendPctLine(points, i, { multiDay, session, open })`,四态**口径写进行标签**,不靠读者猜: | 情形 | 参照价 | 行标签 | | --- | --- | --- | | 调用方给了真实开盘价 | 用它 | `涨跌(距开盘)` | | 单日 + 有时段表 + **首点时刻 == 该表开盘时刻** | 序列首点 | `涨跌(距开盘)` | | 单日 + 无时段表 / 首点不在开盘 | 序列首点 | `涨跌(距首点)` | | 多日(五日)档 | **同一天**的首点 | `涨跌(距当日开盘)` | **退化判据是这条的要点**:`122.XAU`(首点 06:00)、`100.SPX`(首点 21:30)这类**没有时段表**的标的,拉到的序列首点**不是开盘** ⇒ 一律只能说「距首点」。 五日档额外按"当天首点"比 —— 跨天比五天前的首点没有意义。首点缺失或 ≤0 ⇒ `—`。 K 线档**未改动**(仍对前收盘)。 ### 2) 涨跌着色必须跟随 `prefs.redUp` `TipCard` 原先把 tone **写死**成 `tw-up`/`tw-down`,**完全没看 `redUp`** —— 在"绿涨红跌"档位下卡片颜色会与全站相反(与当年云图那个 bug 同类:同一个开关必须管到所有涨跌着色面)。 新增纯函数 `toneClass(tone, redUp)`:`redUp=true` ⇒ `up→tw-up`;`false` ⇒ **互换**;`undefined`/`muted` ⇒ `tw-muted`。 `TipCard(lines, pos, redUp)` 走它,`redUp` 由两个图已有 prop 传入 ⇒ **分时与 K 线卡片都生效**;涨跌行(分时的 价格/涨跌、K 线的 收盘/涨跌幅)统一走它,MACD/DIF/DEA/量额/开高低保持中性。 ### 验证 258 条测试 × 2 时区(252 → 258)。Lead 独立复核六项:A股指数(有表 + 首点在开盘)⇒ `涨跌(距开盘)` +5%; 数据从 09:45 开始 ⇒ **`涨跌(距首点)`**(不许声称距开盘);`122.XAU` 形态 ⇒ `涨跌(距首点)`;给了真实开盘价 ⇒ 用它(90→105 = +16.67%); 五日档 ⇒ `涨跌(距当日开盘)`(210→231 = +10%,而不是对五天前 +110%);`toneClass` 两档互换且**只改颜色不改数值**。 另修正一条旧断言:口径变更后"无昨收 ⇒ 涨跌幅写 `—`"不再成立(首点相对自身是真实的 `0.00%`),"不许 0 顶替"的检查仍在(均价/量/额缺失一律 `—`)。 ## v0.37.0 — 图表细节卡片:光标移到哪,就读到那个颗粒的全部细节 用户要求:**光标在分时图与其他 K 线图上移动时,展示当前颗粒单位的详细细节窗口(半透明卡片)**(券商 App 口径)。 ### 做了什么 - **纯逻辑模块** `client/chartCursor.ts`(不 import react): - `nearestIndex(px, xs, padX, innerW)`:像素 → 最近数据点。**一套实现覆盖三种轴**(时段网格 `ax.xs`、压缩轴 `eff/span`、K 线窗口 `(k+0.5)/count`);越界像素**夹到两端**、空数组给 `null`、单点给 `0`。 这里刻意**用线性扫描而不是二分**:时段网格的 `xs` 在午休折线处会重复、并非严格单调,二分的前提不成立(点数最多几百,代价可忽略)。 - `tipPlacement(...)`:卡片默认在光标右侧 12px,**放不下就翻到左侧**(回传 `flipped`),纵向以光标为中心并收进图内;图比卡还小时夹到 0。 - `timeCell(label, multiDay)`:**五日档时间列带日期**(`MM-DD HH:mm`),单日档只给 `HH:mm` —— 否则"09:35"会被读成今天。 - `trendReadout` / `klineReadout`:行数组,供两图共用。 - **两个图都接上**:竖向虚线 + 当前点小圆点(分时画在价格处、K 线画在收盘处);半透明卡片 `.tw-chart-tip` 为 DOM 覆盖层且 `pointer-events:none`(不挡滚轮缩放与买卖点悬停)。 - **只更新不重建**:光标只进 `useState`,图的 memo(路径 / MA / MACD / 轴)不依赖它 ⇒ 鼠标移动不会重算整图。 - **键盘同做(不只鼠标)**:未显示卡片时 `←/→` 仍是原平移(含 Shift 加速);`Enter`/`Space` 显示卡片,显示后 `←/→` 移动光标且缩放窗口跟随滚动,`Esc` 隐藏。 ### 口径与红线 | 情形 | 显示 | | --- | --- | | 分时无昨收 / K 线首根无前收盘 ⇒ 涨跌幅 | `—`(**不是 `0.00%`**) | | 均价(null / 0 / 越界非价格量) | `—` | | MA5/10/30/60(未成熟) | `—` | | 成交量 / 成交额缺失 | `—` | **MA/MACD 一律由调用方传入图上那一份序列**(卡片不另算),避免"图上 MA5=37.5、卡片写 38.1"。复权口径不重复写(图上口径行已有)。 新卡片是新的数字面 ⇒ `.tw-chart-tip-v` 已登记进 `styles.ts` **两条** `data-blur=1` 清单(隐身档一起模糊、金额遮罩)。 ### 验证与未做 252 条测试 × 2 时区(243 → 252,新增 9 条中文断言)。Lead 独立复核(自造数据):`nearestIndex` 两端/越界/空/单点全部夹取正确、`tipPlacement` 右侧翻转后 `left+152 ≤ 宽`、`timeCell` 五日带日期、四类缺失行全部为 `—`。 **未做**:「至今涨幅」不进卡片(图表数据里没有 YTD,需要 join 独立链路 ⇒ 另立任务);拖拽平移与触屏未接。 **需装机复核**(渲染层看不到):鼠标跟随手感、右侧翻边、五日日期、键盘流程、隐身档模糊、卡片不挡交互。 ## v0.36.1 — 修复:非价格量被当成均价,把分时图压成平线(A股指数等 17 条序列) 用户报:「**所有 A股指数的分时图都是几乎一条直线**」。截图里纵轴 `0.000/1000/2000/3000/4000`、价格线贴顶、页脚 `昨收 —`。 先排除已知项:右侧没有 ±%、页脚 `昨收 —` ⇒ **v0.35.1 的"基准 0"修复是生效的**。这次是**均价**那条。 ### 根因:`avg` 字段对部分标的不是均价,而判据只要求"正数" 真机回包(本机运行实例,非推断): ``` 1.000001 上证指数 price [3755.05, 3824.07] avg [15.15, 17.05] ← 量纲差两个数量级 0.399001 深证成指 price [12279.09, 12676.80] avg [15.87, 16.76] 1.000300 沪深300 price [4239.97, 4330.37] avg [24.91, 28.26] 1.000688 科创50 price [1390.3, 1466.41] avg [96.4, 146.73] 116.02513 港股 price [604, 658.5] avg [6.23, 6.45] 114.LHM 生猪主连 price [10345, 10570] avg [0, 10558.7] ← 部分点越界 ``` `isUsableAvg(v) = v > 0` ⇒ `15.15` 判"可用" ⇒ 并进纵轴域 ⇒ 域从 ~15 起到 3824 ⇒ **价格线贴顶成平线**。 全仓扫描:**77 条分时序列里有 17 条**中招(含 A股指数、港股、ETF、期货),44 条正常、16 条无均价。 倍数**不是**固定比例(316.67/3.22≈98、3755/15.15≈248、4240/24.9≈170)⇒ 它就是个量纲不同的字段,**不能猜它的含义**。 ### 修法:用一条自证式不变量把它拒掉(不去猜字段) **分时 VWAP 不可能跑到当日最低价之下或最高价之上** —— 这条不需要知道 `f58` 是什么,只要求它满足均价的基本性质: - 纯函数 `plausibleAvgs(values, avgs)`(`client/trendView.ts`):`isUsableAvg(v) && v >= lo*0.98 && v <= hi*1.02`,**逐点判**(`114.LHM` 那种"部分越界"只丢坏点);±2% 容差是因为采样点未必覆盖当日真实极值。 - **域与均价折线共用这一份结果**(`trendScale` 内部过滤 + `kline.tsx` 两档的均线折线),避免"域修好了、线还画在图外"。 - 宿主 `normalizeTrendSeries` 按同一规则逐点置 `null`(它手上就有整条价格序列),并写明"这条规则对所有市场成立,不是指数特判"。 - `isUsableAvg` 的语义**未改**(正数仍算"有值",区间校验由 `plausibleAvgs` 负责)—— 既有"安静分钟 0 成交量是合法值"那类行为不受影响。 顺带看一眼(**只记不改**):指数与个股走**同一行解析**(`avg: num(parts[7])`),只是上游给指数填的量纲不同。查清 `f58` 在指数上的确切含义需要单独任务与实证,**本轮不动字段映射**(那属于"按文档猜字段")。 ### 验证 243 条测试 × 2 时区(237 → 243)。Lead 独立复核——**拿真实落盘的 LKG 序列**跑: ``` 1.000001 均价保留 0/242 ⇒ 域宽 77(= 价格域) 1.000300 均价保留 0/214 ⇒ 域宽 101 114.LHM 均价保留 135/136(只丢那个坏点)⇒ 域宽 252(不变) 1.600519 均价保留 10/10(正常序列零影响) 对照(旧行为:把 avg 直接算进域)= [15.2, 3824.1],宽 3809 —— 就是那条"平线" ``` ## v0.36.0 — 删除列头排序,排序只留面板头部的段控 用户决定:持仓页/自选页里**两排功能重复的排序入口只保留一个** —— 保面板头部的段控(默认/市值/持仓盈亏/当日盈亏/仓位占比/现价/成本),**删掉列表上方那排列头**。 ### 删了什么 整份 `SortHeader.tsx`(61 行)、`styles.ts` 的 `.tw-sorthead*` 全部规则与其 1080 断点块、`pickSortEntry`/`useSortEntry` 与它们的断言、两个页面的列头挂载与入口判定、列头专用的 `WATCH_COLUMNS`/`PORT_COLUMNS`/`NO_NAME_SORT` —— **净 −183 行**。 段控改为**无条件常驻**(不再有"宽屏收起"这个概念)。`WATCH_SORT_KEYS`/`PORT_SORT_KEYS` 与 `prefs.watchSort/portSort` 的白名单**一行未动**:段控仍能排全部那些键。 核对:`grep -rn "SortHeader\|sorthead\|useSortEntry\|pickSortEntry" src/` **0 命中**;两页各只剩 `SortBar` × 1。 ### 顺带查明的根因(为什么"只挂一个"没生效) v0.34 引入的互斥判据是**循环依赖**:`useSortEntry` 去量 `.tw-sorthead` 的可见性,而那个元素**只有在它决定"用列头"时才会被挂载**。 首次渲染量不到 ⇒ 判定用段控 ⇒ 列头永不挂载 ⇒ 永远量不到 —— 于是 v0.34 之后列头其实**从来不会出现**。 用户截图里两排并存,只能说明**跑着的客户端仍是旧构建**(仓库里已多次记录"宿主进程未重启、`lib/` 新而进程旧"),那一版正是"两个都渲染、靠 CSS 藏一个"的形态。 无论是哪一种,**"靠测量一个由判定结果决定是否存在的元素来做判定"本身就是错的**;删掉这条路之后,排序入口在任何宽度、任何样式表状态下都只有一个。 ### 文档 `README.md` 的排序条目改为「排序沿用面板头部的段控(v0.36.0 起不再有列头排序)」,并删掉目录树里的 `SortHeader.tsx` 行;`docs/ROADMAP.md` 的竞品派生条目改为「v0.30.0 引入 → v0.36.0 删除」,写明原因是与段控功能重复。 ### 验证 237 条测试 × 2 时区(239 → 237:删掉列头专用的 2 条,另把 `styles.test.ts` 的宽屏断点下限从 3 改为 2 —— "所有 `min-width` 必须等于 `WIDE_MIN_PX`"这条规则继续跑绿);`build.mjs` 片段表一项未删未改。 ## v0.35.1 — 修复:基准为 0 把分时图压成平线(指数卡片与抽屉都中招) 用户报:「实时行情里的指数卡片当日分时图几乎为一条没有起伏的直线」。截图里纵轴是 `0.000/1000/2000/3000/4000`、 价格线贴在顶部,底部 0 处还有一条浅色横线,页脚写 `昨收 —`。 **根因**:**"基准为 0"被当成真实值参加了纵轴域**: - `charts.tsx` 的 `Sparkline`(卡片缩略图)只判 `Number.isFinite(baseline)` 就把它并进 `min/max` ⇒ 基准为 0 时域从 **0** 起, 几千点的价格波动被压成顶部一条平线;底部那条浅线就是**画在 0 上的基准虚线**。 - `trendView.ts` 的 `trendScale`(抽屉大图)同样的洞。 仓库里其实已经有人挡过这个坑(`RescuePanel` 用 `base > 0`、`yGrid` 判了 `baseline !== 0`),所以这是漏网的两次。 **修法**:新增单点判据 `isUsableBaseline(v)`(`有限 && > 0` —— 昨收不可能是 0 或负数),并让**所有基准使用点共用**: `trendScale`、`Sparkline` 的域计算(抽成纯函数 `chartDomain`,**域与虚线共用同一判据**)、`kline.tsx` 的三处(±% 刻度 / 涨跌方向 / 基准虚线)、`RescuePanel`, 以及 `MarketPage`/`QuoteDrawer`/`TopBar`/`PortfolioPage` 的传参与文案(不可用时基准传 `null`、显示 `昨收 —`)。 上游侧顺带核查:`em.ts` 的 `prePrice` 四个产生点里,东财 `trends2` 那处**可能拿到上游的 0** ⇒ 归一为 `null`(但跑着的宿主是旧构建,客户端判据才是主修复)。 **验证**:239 条测试 × 2 时区(234 → 239)。回归断言:指数在 3990 附近波动 + 基准为 0 ⇒ 域宽 < 60 且落在 (3900,4100)。 Lead 独立复核(自造数据、带对照):基准 `0 / -3 / NaN / null / undefined` 一律不参与域(域宽 `26.9`,即价格域),正常基准仍被包含; **对照旧行为**是 `[0, 4002]`(域宽 4002)—— 这正是"平线"的成因,也证明该断言不是恒真。 ## v0.35.0 — 分时图按交易时段切分网格(券商 App 口径) 用户要求:**当日分时图参考券商 App,按不同交易时段切分网格**。 ### 之前差在哪 横轴走的是"按相邻点间隔中位数折叠空隙"(`compressedAxis`)——它**只折叠午休**,整段宽度仍由**数据覆盖范围**决定。 后果很实际:早盘只跑到 10:00 时,曲线会被拉满整个宽度,**看着像一天都在走**;半日市同理。 ### 现在 - **横轴固定为完整交易时段**:`x = 时段内已过分钟 / 时段总分钟`,午休视为**零宽度** ⇒ 11:30 与 13:00 落在**同一条竖线**上。 - **竖格线按"每个时段取中点"**:A股为 `10:30` · `11:30/13:00` · `14:00`(把整段切成 **4 格等宽**,与券商 App 一致);港股为 `10:45` · `12:00/13:00` · `14:30`。 - **部分数据右侧留白、绝不拉伸**:只到 11:29 时曲线止于 `x=0.4917`(旧轴会拉到 1.0)。 - **右侧 ±% 刻度**:以昨收为 `0.00%`,与左侧价格刻度同一套纵轴(照截图补上)。 - 五日档按天各套自己的时段网格(每天一格,没走完就留白);**日分隔线与按天标签机制一行未改**。 ### 时段表只写"能举证的两张" | 表 | 适用 | 时段 | 依据(本机落盘数据实测) | | --- | --- | --- | --- | | A股/ETF/指数/板块 | `1.` `0.` | `09:30–11:30` + `13:00–15:00`(240 分) | `1.600519` 的 5 日序列共 240 点 ⇒ 每天 48 根 5 分钟 bar = 4 小时 | | 港股 | `116.` | `09:30–12:00` + `13:00–16:00`(330 分) | `116.02513` 在 13:40 有 192 点 = 151 + 41 ⇒ 上午收 12:00 | **美股/国际指数/外盘商品/期货不写时段表**(美股随夏令时漂移、商品有日夜盘,硬编码必错)⇒ 回落旧行为,并在口径行写一句「该标的不适用分时段网格」。 时段外的点(如 09:25 集合竞价)**夹到边界**并计数 `clampedBefore/clampedAfter`,绝不产生负坐标或 >1 的坐标。 ### 验证 234 条测试 × 2 时区(227 → 234,新增 7 条断言:四格等宽 / 右侧留白 / 午休零宽 / 无表返回 null / 边界夹取 / 港股时长 / 多日天槽)。 Lead 独立复核(自己构造数据,不采信实现者):A股 240 点 ⇒ 网格 `0.25/0.5/0.75`、只到 11:29 ⇒ 末点 `0.4917`、11:30 与 13:00 同为 `0.5`、`09:25 ⇒ 0`、美股/期货/商品 `sessionOf()===null`、港股 330 分网格 `10:45/12:00·13:00/14:30`。 (复核中我自己两处期望值先写错了 —— 最后一根 bar 的右边界才到 1.0、以及构造的港股点数与上游口径差 2 —— 修正后全部通过;不是实现缺陷。) ### 未做 港股**半日市**识别(没有交易日历,无法判断 ⇒ 由"右侧留白"如实呈现,不特殊标注);五日内部分割线(只画午休线,避免 5×3 条线糊成一片)。 ## v0.34.0 — 团队复审轮:家数链路打通 + 全面精简(净 −2040 行) 用户要求「召集开发与测试团队,检查当前版本的潜在问题与优化方案,主旨是**在保证功能可靠有效的前提下**,代码结构与 UI 提示尽量少而简洁」。 执行的是一轮完整分工:**3 名只读审计**(功能 / 结构 / 文案)→ **实现** → **对抗验证** → **返工**。 ### 两条 P0:新链路"算出来了却没接到位" - **自统计家数没接到三格**:三格仍读指数行情的 `f104/f105/f106`,而同屏却显示「家数由本插件自行统计」与一个占比 —— 三处互相打架。现在三格改走 `pickBreadthCounts`(指数口径优先,拿不到才用自统计),自统计时标出统计时刻与口径。 - **源 B 的一致性判据在真实数据下恒失败**:取数时过滤掉了无涨跌幅的行(停牌),校验却用**未扣除的总数**做"三类之和 = 总数" ⇒ 当日只要有 1 只停牌就每次判失败。基准改为**有效行数**,覆盖检查用**原始行数**独立判定,无涨跌幅行数如实进 `checks`。 **更要紧的是:测试掩盖了它** —— 用例把总数手工写成有效行数,所以一直是绿的。已把断言改到生产口径(总数含停牌行)。 ### 四条 P1 - 五日拼接没按 `limitDays` 裁剪 ⇒ 归档满 12 天后图上显示 12 天、界面写「本地拼接 12/5 天」; - `expired` 判据没有消费点 ⇒ 节假日上游回节前分时时旧交易日被当成今天(现在回包带 `sessionDay`,界面写「非当日数据(MM-DD)」); - YTD 丢弃回包的 `stale`/`source` ⇒ 按日 memo 时看不出"这是旧基准"; - 宽屏的排序入口是**镜像风险**:列头默认隐藏靠媒体查询显示,而宽屏只挂列头 ⇒ 样式表缺失时宽屏一个排序入口都没有(v0.30.1 修的是"两个同时出现")。现在由"宽窄 + 列头是否真的可见"共同决定,结构上恰好一个。 ### 文案:先定用词表,再改句子 一轮文案审计给出**全局用词表**(状态词一律 `未取到`/`上游无此数据`,时间格式 `HH:mm:ss` 与 `MM-DD HH:mm`,前缀统一 `数据时刻`,单位 `份/股`),再逐条执行必修 4 + 应修 10。 其中最关键的一条:`显示上次成功数据` 一个事实有 **6 种写法**,而 K 线兜底那条**没带日期** —— 正是"把昨天的图当今天读"那次事故的形态。现在统一为 `· 上次成功数据(MM-DD HH:mm)`,日期不可知时也有明确短形。 顺带修掉两处"声称已统一、实际残留"(抽屉的 `· 缓存数据(上游暂不可用)`、"观测时刻")与一处自相矛盾的机械套用(`MISSING_TIER_ADVICE` 尾句)。 ### 精简(可靠性优先,先划"不可精简"再动手) 结构审计先给出**不可精简档**(看着冗余、动了会出事):家数护栏与源 C 算法、`charts.tsx` 的存活图表部分、`ACTOR_TOOL` 与历史账本兼容、`breadthView` 的"任一分量缺失⇒整格 `—`"、样式表单次替换语义、护盘面板的本地格式化 —— **一条未碰**。 在安全档内:`charts.tsx` 432→257、`tencent.ts` 338→289、删 11 处零引用碎片、动词文案与"缺失两档"标签各收成一常量、数值判定工具按**实际语义**收窄合并(`em.ts` 的字符串解析版**不能**与 shared 合并 —— 合并会把整片行情判成 `null`)、历史叙述注释压缩 −677 行(含「不许/必须/红线/不变量/否则」的约束句整句保留,过程叙述迁入 `docs/DECISIONS.md`)、4 份一次性报告移入 `docs/archive/`。 **合计 94 文件 +619 / −2659(净 −2040)**:代码 ≈−280、注释 ≈−677、文档根目录 −1433(移动非删除)。 ### 对抗验证的结果 验证员(非实现者)自己跑门禁、自己构造反例:**红线扫描**(被删行里搜「不许/必须/红线/不变量/否则/宁可」共 16 处,逐条核实全部仍有承载)、**M 档 6 项逐项确认未被碰**、`build.mjs` **零改动**(75 项片段一项未删)、"少取一页会不会被放过"独立于等式生效。 它挖出的 **D1**(分页缺页统计用了稀疏数组 `filter`,跳过空槽 ⇒ 检查永不生效、最坏抛 `TypeError`)与两处文案漏网已修;修的过程中实现者纠正了 Lead 的修法建议:`Array.prototype.some` **同样跳过空槽**,必须用下标循环或显式填 `undefined`。 ### ⚠ 必须在装机后复核(本机无浏览器,且**宿主进程从未重启**) 审计期间发现:跑着的宿主仍是旧构建,**v0.30–v0.34 的新链路至今一次都没在真机执行过**。重启后请优先看四处: 1. 东财不可达时,大盘页三格是否显示**自统计的家数**(而不是 `—`),且带统计时刻; 2. 五日在归档满 12 天后是否只显示 5 天(不再出现「12/5 天」); 3. 分时/五日是否会出现「上次成功数据(日期)」而不是把旧图当今天; 4. 持仓页与抽屉的提示是否明显变少、且同屏不再出现两条降级说明。 **未做(已记录)**:结构审计的 S6(原子写抽公共模块)与 S10(测试文件归并)、R1–R4(LKG 抽象、`em.ts` 拆分、`PortfolioPage` 拆 Modal、两页行提示统一);功能审计的 P2 八条与验证的 D2/D3。 ## v0.33.1 — 大盘页布局回退 + 样式表热重载缺陷 ### 回退:大盘页 12 栏重排(v0.33.0)按用户要求改回上一版 用户装机后反馈"新布局还不如上一个版本"。**查明后:这不是审美分歧,而是样式根本没生效。** ### 根因:样式表在热重载后不会更新 ⇒ 新 DOM 配旧 CSS `ensureCss()` 原本的判据是"文档里有没有同 id 的 `<style>`",有就跳过注入。而客户端插件会被宿主**热重载**(不刷新页面): 新模块实例看到的是**上一版**留下的那份样式表 —— 旧规则里没有新类名,于是新版大盘页整块以"无样式的纯文本"呈现 (指标条变成一行行文字、指数卡塌成两行)。**看起来像设计变差了,实际是 CSS 版本错配。** 修法:注入决策改成**按内容判定**(纯函数 `cssInjectAction`):没有则注入、内容相同则跳过、**内容不同则就地替换**; `STYLE_ID` 提为常量。断言锁住三种情形(含"内容变了必须替换")。 这条缺陷与本次布局无关:**任何一次客户端 UI 改动(尤其新增类名)都可能被它伪装成"改坏了"**,因此单独修掉。 ### 保留 - 大盘页布局回到 v0.33.0 之前(`.tw-stat` 卡片 + 指数表;`.tw-board` / `.tw-metric*` / `.tw-idxcard` / `.tw-distbar` 一并撤掉,`boardLayout.ts` 及其断言删除)。 - **保留**家数自统计链(源 A/B 与 `source` / `caliber` / 统计时刻)—— 回退后大盘页多一行"家数由本插件自行统计(统计于 HH:mm:ss)",与上游直接给的数区分开。 - **保留**护盘面板的空壳修复(无通道数据不再默认展开空壳)。该修复此前误随布局一起回退,已就地恢复,并去掉对已删模块的依赖。 ### 验证 211 条测试 × 2 时区(216 → 211:删掉布局模块的 7 条断言,新增样式注入 3 条 + 既有断言微调)。 `docs/DESIGN-DASHBOARD.md` 保留在仓库里 —— 设计规范本身有效(12 栏网格、缺失收缩规则、7 项"当前不可得"清单), 只是**这次没能让用户看到它应有的样子**;样式表版本问题修好之后,是否重做由用户决定。 ## v0.33.0 — 大盘页重排(12 栏网格)+ 涨跌家数自统计 ### 涨跌家数:不再只挂在"指数行情的三个字段"上 用户反馈:**同类插件能显示涨跌家数,本插件显示 `—`**。 查明后的事实:我们的家数**只**来自指数行情对象的 `f104/f105/f106` —— 指数行情一旦走备用源,这三个字段就不存在,于是大盘页三格全 `—`。 另有两条实证:我们的行情主机清单**早已**把 `push2delay` 放在首位(所以"换主机"不是缺的东西);而实测 `pz=5000` 的排行查询只回 100 行、且**当前是腾讯/新浪在兜底**。 本版把家数改成**多源链 + 可自行统计**: - **源 A**:指数行情的三个字段(保持不变,0 次额外请求,`source: 'em-index'`); - **源 B(新)**:按沪深A股**全量分页统计**并本地计数(`source: 'em-clist'`)—— 100/页、并发 ≤4、页数上限 60、结果缓存 120s;首页非空、拿到总数、页行数齐全、扫过行数 ≥ 总数、三类之和 = 总数,缺一项即判失败; - 都不成 ⇒ 维持 `—`,但 `missing[].note` 会**逐个列出尝试过的源与各自的错误**(此前只给一句笼统原因,无法判断"是上游挂了还是这条路根本不通")。 配套:回包带 `source` 与 `caliber`("本插件自行按沪深A股全量统计(含科创板/创业板,不含北交所):涨跌幅 >0 上涨、=0 平盘、<0 下跌"); **`asOf` 用统计完成时刻**(与行情条的数据时刻分开标注,不混用);三者全 0 而总数 >0、和与总数不符、扫过行数不足一律按**失败**处理。 **不做涨停/跌停家数**:需要涨跌停价与 ST/科创/新股规则,按涨跌幅近似会在这些品种上稳定地算错。 ⚠ **已知缺口(如实记录)**:当主源不可达、自统计也拿不到数据时,家数仍是 `—`。本版尝试接入的第三条备用源**在当前环境不可用**(工具链层面被拒),因此其算法虽已实现并被断言覆盖,**运行时未接线**(`deps.sortedPctPage` 可选且未传,代码里写明"当前运行时未接入")。也就是说:主源可达时家数由我们自行统计出来;全不可达时仍如实显示 `—`。 ### 界面:大盘页过于空旷 设计规范见 `docs/DESIGN-DASHBOARD.md`(只读审计产出)。诊断是:**空旷不是元素少,而是每个数值占一张 52–60px 的两行卡,加上 6 列表格被 `width:100%` 摊到 2100px**。 - 新增唯一子容器 `.tw-board`(12 栏网格,`data-span` = 5/7/12/7/5,不使用 `dense`); - **B1 市场宽度**:三行 25px 指标条(家数 / 沪·深·两市成交额 / 上涨占比 + 日分位),第三行下挂 34px **历史分布条**(用现有分位样本,不新增数据源); - **B2 指数卡矩阵**:6 张 45px 指数卡(数据来自行情条同一轮,**零新增请求**); - **B3 护盘信号**:默认收起;无数据时**塌成一行** `护盘通道 — + 原因 + 重试`(顺带修掉"没有通道也默认展开一个空壳"); - **B4/B5**:板块/资金/ETF 与搜索详情按 7/5 分栏;数值列封顶 200px、表体 `min(46vh,360px)`。 - **缺失收缩**:家数缺失时三格**合并成一格**且原因只出现一次(行高仍 25px,不再三张 60px 的 `—` 卡);成交额分项独立判定;分位样本不足写原因不写 0;空样本的分布条不给全 0 数组。 - `.tw-stat/.tw-statrow` 一行未改(持仓页在用);新增数字面已登记进两条 `data-blur=1` 清单。 ### 验证 - 216 条测试 × 2 时区(209 → 216):家数统计 8 条(含"两次取数不一致就不发布""整页全正也要有证明")+ 布局 7 条(12 栏档位、家数缺失塌成一格、真 0 不被误判为缺失、分项独立、原因压缩、空样本不造 0、分布条升序与位置)。 - **渲染层仍未在真机验证**(本机无浏览器):块高、分布条观感、1080 下的指数卡宽度、窄面板换行、隐身档模糊 5 项需装机复核。 ## v0.32.0 — 分时兜底顺序、五日按天标注、界面文案精简(装机反馈) 三条反馈,第一条是与数据正确性同级的缺陷。 ### 修复:早上 9:38 分时还显示昨日整场 —— 兜底顺序错了 同一时刻三个标的的实测(2026-10-09 09:38): ``` 1.600519 → 10 个点、日期 2026-10-09、无 staleAt ← 活的腾讯分钟线 1.510300 → 42 点全是 2026-10-08、带 staleAt ← 昨天整场 1.000001 → 231 点全是 2026-10-08、带 staleAt ← 昨天整场 ``` 根因:`trendWithFallbackRaw` 的**两个分支都把"过期的 LKG"排在"活的备用源"前面** —— 一个分支直接 `lastGoodTrend ?? data`(根本不试腾讯),另一个分支也是 LKG 命中即返回。 于是"有昨日 LKG 的标的"(指数/ETF 常有)在东财不可达时永远显示昨天,而"恰好没有 LKG 的标的"反而实时。 修法:非休市定稿路径改为 `上游 → 活的备用源(腾讯分钟线)→ LKG`,判据换成**数据是不是今天的** (`trendSessionDay()` 取序列最后一个点的交易日,跨日即过期),决策抽成纯函数 `pickTrendFallback`; 休市定稿分支一行未动。回退到 LKG 时回包带 `sessionDay`,界面写 `显示上次成功数据(2026-10-08 11:08)`, 不再让人以为那是今天的图。 ### 体验:五日图看不出"天" 五日只在内部日边界画虚线 + 图内左上角 9px 小字,且首日永远没有标签、底部轴只有首末两个时间 —— 用户的原话是"只有横向四分格"。现在底部轴**按天标注**(`09-28` `09-29` `09-30` `10-08` `10-09`, 居各自区段中间;天数超出可容纳数时均匀抽样且首末必留),日分隔线从主图延伸到底部轴。 标签文字一律取自点里的 `label`,**不造日期**;只有 1 天时仍走时间轴。 ### 界面文案:删的是"重复"与"空值句",不是"说明" 一次只读审计(`docs/AUDIT-COPY.md`:规则 5 / 必修 6 / 应修 20 / **明确保留 10** / 动不得 7 类)先定规则再动手: 真正的问题不是"每条都太长",而是 ① 同一句话在面板级、行内、tooltip 各写一遍(三簇共 30 处); ② 空值时照样渲染整句(`T+1:今日买入 0 份…故可用少于持仓` 在今日无买入时是**假话**,却对每只持仓恒出)。 - 假话与空值句清零:`故可用少于持仓`、`尚无成交,占比不可算`、`本轮未算(悬停看原因)`、`基准 2026-01-02`(常态)全部不再渲染(改为**不占位**,口径进 `title`)。 - 三簇去重:「0 会被读成…」可见 11 → **1**、「三源都没有返回可用价格」7 → **1**、宿主重试措辞收敛为 `MISSING_TIER_ADVICE` 两档常量(`transient` 与 `no-source` 的区分**必须保留** —— 它决定"要不要再点重试")。 - 大盘页常驻口径行 ≈90 字 → `数据时刻 … · 来源 … · 口径 ⓘ`(`占比排序仅本页 40 条内` 按原样保留, 删了会把本页重排读成全市场排行)。 - 顺带修掉一处**自相矛盾**:持仓行无价时 title 写"按成本口径暂以 0 计",而徽标写"总额不含" —— 实际不计入总额,已删除那句。 **保留清单同样执行**(删过头就是新的信息错误):`no-source` vs `transient`、"频繁点重试会加重限流"、 "不自动改账"、口径本体、`—` 的原因本体(含年内上市的「上市首日至今」基准)、护盘概率口径、 第三方站点红跌绿涨提示、顶部四态徽标。 ### 验证 - 201 条测试 × 2 时区(197 → 201):兜底顺序三条 + 按天轴四条 + 文案"空值不占位"四条。 - Lead 独立复核(不采信转述):兜底决策三分支实测 `backup / lkg(expired, sessionDay=2026-10-08) / upstream`; 文案簇计数按剥注释后的字符串口径复核 —— 恒假句 0、三源说明 1、「0 会被读成」客户端可见 1。 ## v0.31.0 — 五日本地归档拼接 + 抽屉提示精简(装机反馈) ### 新能力:五日 = 真实多日源 → 本地归档拼接 背景:五日档原只走新浪/腾讯的 5 分钟 K 线,而**这两家只覆盖沪/深** —— 港股、国际指数、外盘商品、期货 的五日要么退化成"当日的五日",要么直接失败。用户提出「拿缓存的每日分时拼起来不就是多日分时?」—— 思路成立,但当时**没有可拼的历史**:`trends-lkg.json` 的键是 `secid|ndays`,每标的只存**最新一次**。 于是新增两层: - **按日归档**:`<DSH_HOME>/dsh-tradewatcher/trends/<secid>/<YYYY-MM-DD>.json`。每次成功取到某一日的分时都会归档, **包括从落盘 LKG 兜底命中那一次**(否则"从本版起累积"永远只有今天)。tmp+rename 原子写、同标的串行、同日幂等覆盖; 保留最近 **12 个交易日**,清理只删本目录里的日期文件;单日 >512KB 或单标的总量 >8MB 就**不写**并如实报「本次未归档」; 可用 `prefs.trendArchive` 关闭。(A股/ETF 的真实多日序列会被按日拆开归档,等于一次性回填几天。) - **拼接纯函数** `stitchTrendDays` → 连续点 + 日分隔 + **覆盖报告** `{have, missing, limit}`: 日期升序、同日只留一份、单点日不计入覆盖、非法价格丢弃;**缺的日子只列不补**。 界面口径条如实写:`本地拼接 3/5 天 · 缺 2026-10-07`;只有一天时写 `本地拼接 1/5 天(仅当日,从本版起累积)`。 **三条不许都守住了**:不假装立刻有五日(缺的只列)、不用日K冒充分时、不凭空补齐缺失的日子。 **边界**:归档从本版起累积,装好当天最多 1 天,之后逐日变多;某天没打开过该标的,那天就永久缺一格(`missing` 会列出)。 没有交易日历,所以法定假日也会落进 `missing`(如实列出,不猜)。这两点都写进了界面说明。 ### 修复:抽屉顶部的红色横幅把图挤掉了 装机反馈"提示太多、影响交互体验":抽屉上方同时挂着两条红色长条(详情不可用 + 五日不可用), 而后者根本不是故障 —— 是"该市场没有多日分钟源"这一**结构性事实**。 规则改为:**红色只留给故障,结构性限制进口径条**。 - 详情不可用:压成一行 `⚠ 详情不可用 · 上游限流,稍后自动重试` + 「重试」;含"频繁重试只会加重限流"的完整解释进 `title`/`aria-label`(键盘与触屏同样可读)。上游可达但该标的无详情字段(`no-source`)不标红、也压成一行。 - 五日不可用:**横幅删除**,并入口径条,完整解释进 `title`/`aria-label`。 图表上方现在最多一行常驻信息。 ### 验证 - 187 条测试 × 2 时区(174 → 187):拼接 6 条(升序/日分隔/缺口只列且周末不算/单日不谎报缺 4 天/同日去重/非法值丢弃) + 归档 7 条(往返还原 label、幂等覆盖、滚动清理不碰非日期文件、体积保护、坏文件与缺目录)。 - Lead 独立复核(不采信实现者转述):真实归档文件解码还原 48 点;拼接覆盖 `{have:[10-06,10-08,10-09], missing:[10-07]}` 且 `avg/vol` 全为 `null`(拼接不造数)。 ## v0.30.2 — 国际市场/大宗商品图表(装机反馈) 现象:自选里的国际指数与外盘商品,**分时被画成一条假平线**(y 轴写着 0.000 / 2000 / 4000,成交量印成 `1.00`), 日K 与五日则直接"加载失败"。两条症状同一个病根,且都是老毛病"缺失被编码成 0"在图表链路上的复发。 ### 根因一:`avg: 0` 污染 y 轴域 本机实机取到的数据(不是推断): ``` GET /tradewatcher/trend?secid=122.XAU → 462 点,price 有 390 个不同值(4274.05 ~ 4302.99,数据本身正常) avg 全为 0 · vol 全为 0 · amount 全为 0 ``` 均价(VWAP)缺失时被写成 `0`,而客户端算纵轴域时把 `0` 当成真实值参与 → `lo = 0`,于是 4274~4303 的走势被压进 0~4303 的轴上,看上去就是"一条贴着 4000 的平线"。 成交量同理:全 0 时窗格把 `max` 兜底成 1,于是印出一个不存在的 `成交量 1.00`。 修法:`normalizeTrendSeries` 逐点判 `avg`(VWAP 不可能 ≤0 ⇒ `null`)、按**整条序列**判 `vol`/`amount` (序列里有过成交量,则安静的分钟 `0` 是"没成交"这一事实,必须保留 —— 逐点抹零会把事实换成"没数据")。 客户端 `trendScale()` 只由**有效**价格/均价+昨收决定纵轴域,全无量时窗格显示「该市场不提供成交量」。 **一个关键的返工点**:只在解析上游处归一是修不干净的 —— `trends-lkg.json` 是**旧进程落盘的**, 里面存着的仍是 `avg:0`(实机确认 `122.XAU|1` 那 462 点)。因此归一同时放在 `trendWithFallback` 的**出口**, 走缓存兜底也会被清掉。 ### 根因二:没有多日分钟源的市场被当成"故障" - 五日档复用 `/trend` 的多日分钟点;国际指数/外盘商品/期货**根本没有**多日分钟源(腾讯、新浪都不覆盖), 于是它静默退化成"当日的五日"。 - `/trend`、`/kline` 返回 `null` 时回包不带原因,抽屉只能显示一句「详情加载失败 重试」。 修法:五日档在只覆盖 1 个交易日时明说「五日不可用:该市场只有当日分时(下图仅显示当日)」; `null` 回包带 `missing[{what, why, note}]`,`why` 的口径与既有 `quoteProvenance` 一致 (上游可达但没这个标的 = `no-source`;上游当前不可达 = `transient`,等恢复就有)。 归因改为**由发起请求的那一层记账**(含主机熔断状态),不再靠"东财整体是否可用"猜 —— 实测出现过"上游明明挂了却被判成结构性缺失、还告诉用户重试无用",那是方向性错误。 同时按代码事实写下**分钟源覆盖表**(就写在 `em.ts` 里):沪/深 = 东财 + 腾讯当日 + 新浪/腾讯多日; 港股 = 东财 + 腾讯当日、**无多日**;美股/国际指数/外盘商品/期货 = **只有东财当日**。 ### 验证 - 用**真实落盘的 LKG**(462 点)跑新纯函数:`avg` 不再含 0、`vol`/`amount` 归 `null`、 纵轴域 `4272.31 ~ 4304.73`(回到价格域)、`trendDayCount = 1`(五日档据此如实标注)。 - 反向用例:序列里出现过成交量时,安静分钟的 `0` **不得**被抹掉;有效均价不得被动。 - 162 → 174 条测试 × 2 时区全绿。 ### 边界(明确不做/做不到) - 国际指数、外盘商品、期货**没有**多日分钟源,这不是本版能修的:五日档现在"显示当日 + 明说不可用"。 - 恒生的分时归因当前是 `transient`(东财不可达且无腾讯分钟兜底);东财恢复后若仍无数据会自动变成 `no-source`(同一套判定,无需改代码)。"东财可达时恒生到底有没有分时"这一格留待恢复后复看。 ## v0.30.1 — 排序控件重复与提示冗余(装机反馈) 装机后用户立刻发现两处问题,都属 v0.30.0 的引入或遗留: ### 修复:宽屏下**两个排序控件同时出现** 现象:自选页顶部同时显示「自选分组 排序 默认/当日涨跌/总市值/成交额↓/涨跌额/行业α」的**段控** 与下方的列头「排序 ↺默认顺序 名称 成交额↓ 市值…」,两套控件各说一套。 根因:v0.30.0 把段控与列头**都渲染**,靠一条 CSS 隐藏其中一个(`[data-wide-hide=1]{display:none !important}`)。 **"藏起来"不是互斥** —— 只要选择器链任何一环失配(祖先类名、媒体查询、内联样式优先级),用户就会看到两个。 实测确实出现了这种情况。 修法:改由 `client/useWide.ts` 的 `useWideLayout()` 判定,**只挂那一个** —— 宽屏挂列头、窄屏挂段控, 于是"两个同时出现"在结构上不可能。同时删掉那条从来没拦住过的 CSS 兜底与 `wideHidden` 属性 (一个失效的兜底只会让人以为有两层防线)。断点一致性从注释升级为断言: `styles.test.ts` 直接读 `TW_CSS` 文本比对 `WIDE_MIN_PX`,并禁止重新引入隐藏式互斥 (该断言用"注入一条真规则→失败、还原→通过"验证过不是恒真)。 ### 修复:常驻提示冗余 现象:两条横幅把面板挤满且自我重复 —— ``` 板块涨跌与 α 本次未取到:上游暂时不可用(熔断中,约 2 分钟后自动重试)(22:46:17)。板块行情取不到时, 板块涨幅显示 —(不用 0 代替:0 会被读成"没涨没跌",那是错的),α 记为 —。下次行情轮询会自动重试。 年初至今(YTD):6 项本次算不出 YTD(显示 —,不用 0 顶替):日线本次取不到(上游限流或超时),稍后自动重试。 ``` 三处重复:① 重试承诺说两遍(上游错误里已带,尾巴又写一遍);② "YTD" 在前后缀各出现一次; ③ "显示 —、不用 0 顶替" 是全页统一约定,每轮刷新都印一遍。 修法:正文各压成一行、只留"影响什么 + 为什么 + 时刻",把规则解释移进 `title` / `aria-label` (可聚焦,键盘与触屏同样能读到);`ytdMissingSummary` 的正文不再重复渲染处已写的信息, 并加断言锁住(正文不得含 `YTD`/`年初至今`/`不用 0 顶替`/`显示 —`)。 ### 记录 - 本版**仍未经渲染层实测**(本机无浏览器自动化):两条修复都是逻辑/文案层面的确定性改动, 但"装机后是否真的只有一个控件"仍需肉眼确认一次。 - 已知限制:宽窄判定用**视口**宽度,而面板实际可用宽度小于视口(左侧栏占宽)。 视口 1080–1330 一段可能出现"列头已生效、面板只有一列"(可用但不最优)。 彻底解决需按容器宽度判定(或改用 `@container` 查询),留待后续;当前选择是简单可验证。 ## v0.30.0 — 团队审计驱动的修复与两处新能力 本版由一次**分工审计**产出(功能/口径审计 + UI/UX 审计,两份报告留在 `docs/AUDIT-FUNCTION.md`、`docs/AUDIT-UI.md`),加上竞品对照记录(`docs/COMPETITOR-NOTES.md`)。核心是继续收紧同一条线:**缺失不许伪装成 0,降级不许假装实时**。 ### 新能力 - **列头点击排序**:宽屏(≥1080px)列表改为点列头排序 —— 再点翻转方向、列头显示 ▲/▼、原生 `<button>` 键盘可达并带 `aria-sort`,另有「↺ 默认顺序」复位。自选 6 列 / 持仓 7 列各有排序键;窄屏保留原分段开关,两个入口**互斥且单向失败**(收起段控由调用方显式声明,漏传只会"两个入口都在",不会一个都不剩)。排序键白名单扩张后仍走宿主校验,非法键照旧 400。 - **年初至今(YTD)**:`YTD =(现价 − 本年内第一个交易日收盘价)÷ 该收盘价 × 100%`,序列用**前复权**(不复权在除权日会跳空,那不是真实收益)。口径常量单一来源(`shared/model.ts` 的 `YTD_CALIBER`),界面 tooltip 与 agent 工具引用同一句。三处边界随结果说明:指数/期货按原始价格(点位/合约价,不适用复权)、本年内上市标注「上市首日至今,不是年初至今」、**年内第一个交易日不给数**(口径要的是收盘价,不拿未收盘价或上一年收盘顶替)。宿主按日 memo 复用 K 线缓存,并发 ≤4、失败有冷却、单次上限 60 个标的并如实回报 `truncated`。新增读工具 `tradewatcher_ytd` 与路由 `GET /tradewatcher/ytd`。 ### 修复(审计 P0) - **大盘页用 0 顶替缺失的涨跌家数**:备用源不含 `f104/f105/f106` 时,页面会把缺失渲染成「上涨 0 / 下跌 0 / 平盘 0」,还会出现 `1234 / 0` 被读作"没有一只下跌"。现改为纯函数 `breadthCells`(不 import react):任一腿缺失 ⇒ 三格全 `—` + 原因;涨跌**成对**显示,一侧缺失整格 `—`;而真正的 0 仍照常显示 0(两者含义完全不同)。 - **日历的 `asOf`/`stale` 在上游全挂时恰好失效**:四个自动源各自 `catch {}` 吞错,随后**无条件**把 `syncedAt` 写成"现在",工具据此报"数据时刻=刚刚、未降级"。现拆 `syncedAt`(最近一次成功)与 `syncAttemptAt`(最近一次尝试),逐源记 `ok/failed/skipped` 并由 `syncStatus()` 统一出 `asOf/stale/missing`;零成功同步时 `asOf` 为 `null` 或上次成功时刻。**顺带补上 5 分钟失败冷却** —— 旧的"无条件写 syncedAt"其实兼任节流阀,拆开后不补会让上游全挂时每次面板刷新都重打四个源。 - **云图配色写死**:自绘热力图硬编码 `redUp: true`,而其第三方 iframe 是红跌绿涨 —— 同一面板可能出现三种相反色义。现自绘/列表/行情条/筹码块统一跟随 `prefs.redUp`;第三方站点是**改不了**的外部口径,只加常驻提示("该站:红=跌 绿=涨")。四态色点**不跟随**且不应跟随:它编码的是新鲜度,不是涨跌方向。 - **Markdown `**` 泄漏成界面星号**:18 处会被人读到的文案(客户端 11 + 工具输出 7)把 `**加粗**` 直接印在界面上;现全仓清理并在 CI 之外留扫查习惯。 - **输入控件没有可见焦点环**:`outline:none` 配一个**全项目未定义**的 `--tw-focus`,滑块连边框都没了。现改用已定义的 `--tw-ring` + `:focus-visible`(鼠标点击不画环)。 ### 修复(审计 P1) - **宽屏两列按行填充打乱名次**:`grid auto-fill` 是横向填充,按市值排序后第 2 名跑到右列、顺左列读是 1/3/5 名。现改 `columns` 竖向填充(名次自上而下 1/2/3),断点从 1500px 降到 **1080px**,并与列头、段控收起条件**三处同步**。 - **未录入成本被当成 0 成本**:新建持仓后「调整」留空成本 ⇒ `avgCost=0` ⇒ 浮盈 = 全额市值(凭空一笔大额浮盈)。现判据 `qty>0 && avgCost<=0 && turnover===0` ⇒ 市值照算、盈亏与率一律 `—`,界面与工具都指向「调整」补成本。 - **卖出不校验可用数量**:T+1 品种当天买入当天卖出会被记成一笔券商端不存在的成交。现新增纯函数 `availableQtyAt`(可卖 = 该时点前已持有 − 同日该时点前买入,时点取**该笔流水自己的 ts**),宿主 `trade()` 驳回、客户端表单同样拦截;自检里的用例改用 T+0 的 ETF 并补 2 条断言。 - **`no-source` 归因方向错误**:东财整体故障且该标的没有备用源映射时,仍被判 `no-source`("重试无效"),而实际等东财恢复就有。现区分「结构性缺失」与「东财此刻挂了」:后者给 `transient` + "等东财恢复就会有"。 - **账本坏流水被静默跳过**:`replayPosition` 遇到无法应用的流水只保留前值,既不计数也不上报 —— 账本里能看到、持仓快照里少一块。现返回 `skipped[]`,持仓行徽标 + 面板可聚焦明细 + 工具 `provenance.missing[]` 都报「N 条流水未能应用」。 - **缺失原因只能 hover**:原因写在不可聚焦的 `<span title>` 上,键盘与触屏拿不到。现关键缺失(大盘页、自选、持仓、日历)改为可聚焦(`tabIndex=0` + `role=note` + `aria-label`),正常状态不加 Tab 停靠点。 - **同一状态四种叫法**:统一为 `无行情源`(三源都没有)与 `暂无行情`(还没刷新),词表落在 `quoteState.ts` 单一来源。 ### 独立验收与两轮返工(记录在案) 本版改动全部经过**独立验收**(非实现者复核),并在验收中挖出一条实现者没覆盖的会计缺陷: - **D1(验收发现)未录入成本 + 卖出 ⇒ 凭空盈利**:`costUnknown` 的判据原本含 `turnover <= 0`,而**卖出会产生成交额**,守卫因此自行解除 —— `adjust(100,0)` → `sell(50,12.5)` 实测得到 `floatPnl=625 / dilutedPnl=1250 / realized=625`(市值仅 625,却报出 1250 的盈利)。修法:判据改为「**不存在产生成本的流水**」(`buy` 或带价 `adjust`),而不是"没有成交额"。 - **D1b(返工)成本未知期间卖出的已实现盈亏**:仅修 D1 还不够 —— 用户随后补录成本时守卫解除,那笔在 `avgCost=0` 时应用的卖出会被算成一个数字(625,真实应为 150)。修法:**逐笔判定** —— 重放时若某笔 `sell` 应用时 `avgCost<=0`,**不累加进 `realized`**(只记 `realizedUnknownQty`),`realized` 输出 `null` 并说明"其中 X 股在成本录入前卖出"。这样**能算对的仍算对**(补录的成本流水时点若早于该笔卖出,重放即得 150),算不了的才 `—`;不采用"持仓级粘性 null",那是用永久缺失掩盖可以算对的数。 - **收口(N1/N2/N5)**:面板级「累计已实现」补"不含 N 只"提示(此前只有行内与工具侧有,用户看到合计少一块却不知为什么);`selftest` 的恒等式在 `realized=null` 时会被 JS 当 0 静默通过,改为显式分叉;`tools.ts` 的 render 类型断言随 `number|null` 对齐。 验收报告见 `docs/AUDIT-VERIFY.md`(含三条真实写入路径的实测输出与 22 条独立反例)。**仍未验证**:全部渲染层行为(列头点击/键盘/焦点环/多列名次/云图配色)—— 本机无浏览器自动化,需装机在 360/1100/1600px 各看一次。 ### 竞品对照(记录在案) 同期的竞品观察见 `docs/COMPETITOR-NOTES.md`:其扩展是闭源 MV3 构建产物(作者仓库只有一份 README),但其 manifest 暴露了数据源清单;可取的是列头排序与 YTD(本版已做),不可取的是**行内直改成本/股数**(本仓库数量与成本全部由流水推导,直接改会破坏可审计性)。其云图官网自述口径是「面积=流通市值、红跌绿涨、09:30–15:00 时间轴复盘」,其中**时间轴复盘**列为待开发。 ## v0.29.0 — 宽屏布局 面板最初按侧栏宽度(~360px)设计,放到 1800px+ 的主区就是"卡片被拉成 300px 空壳、列表行名字在左价格在右中间一片空"。修法不是把内容拉长,而是**在宽屏下用宽度装更多内容**: - **行情条改为自动填充网格**(`repeat(auto-fill, minmax(112px, 1fr))`):宽屏多列、窄屏换行。此前是 `flex + overflow-x:auto` —— 宽屏卡片被拉伸,窄屏把超出部分藏进横向滚动(不滚就不知道还有几只);现在没有任何内容被藏起来。 - **列表在 ≥1500px 时铺成多列**(自选与持仓都是 `minmax(520px, 1fr)`):同样的高度看到两倍条目、滚动更少;窗口变窄自动退回单列。 - **提示文案限宽 96ch**:一行提示横跨 2000px 时读起来是断裂的。 ## v0.28.0 — 折算口径、图表可用性、自绘热力 - **跨市场折算(P1-11,定额路径)**:新增 `fxMode`(`none` 默认 / `fixed`)与 `fxRates`。非 A股**只有在能折算时**才计入总额;折算不了的分「口径未开」与「开了但没填该币种汇率」两类进 `unpriced`,都不按 1:1。`fxRates` 回读实际生效汇率并显示在总额旁。汇率只接受 `0.01–100`(把 `7.15` 填成 `715` 会得到差 100 倍的人民币市值)。宿主对 `fxMode='live'` **显式报错说明原因**,而不是接受一个什么都不做的档位。**`live` 档仍未完成**:汇率源尚未验证连通性与字段口径。 - **图表键盘操作与读屏摘要(P2-3)**:可聚焦,`←/→` 平移(Shift 加速 10 根)、`+`/`-` 缩放、`Home/End` 到两端;缩放滑块三个手柄补齐 slider 语义(`aria-valuenow`/`aria-valuetext`)。拖拽能用不代表键盘能用。`.tw-sr` 读屏摘要与图**同源同一次计算**,不会出现"摘要说涨、图上在跌";用 `clip` 而非 `display:none`(后者读屏也读不到)。 - **除权日提示(P2-4)**:复用**已同步**的日历事件(`div:` 前缀),不新增数据源;持仓页顶部列出涉及持仓的公司行为,持仓卡标「N 天后除权」。**不自动改账**——真实送转/派息以券商为准,自动改会把用户唯一的交易记录改成一个"看起来对但没人能核对"的状态。 - **自绘行业热力(P2-5)**:新增 `client/treemap.ts`(squarified treemap,纯函数:面积成比例、不越界不重叠、长宽比不退化、退化输入不产生 NaN)。云图页改「自绘 / 第三方站点」并**默认自绘**——只有自绘才标得了口径(面积=成交额、色深=涨跌幅,±5% 封顶)。无成交额的板块不参与面积分配并报出数量,避免"图铺满了、其实缺了 N 个板块"。 **未做(共同卡在"源未验证")**:`live` 实时汇率、南向实时与北向收盘口径(P1-12)、场外基金净值与估值双口径(P1-14)、ETF 季报持仓穿透(P2-1)。本仓库的规矩是不按文档猜字段——在拿到真实返回结构前写解析器,得到的是"看起来能跑、一改版就坏、且会给出看起来正常的错数据"的代码。 ## v0.27.0 — 截图保护、自选异动、涨跌家数分位、日历勾稽 - **截图 / 录屏保护(P2-2)**:面板不透明度 35%–100% 滑杆(只作用于 `.tw-root`),数字模糊(hover 所在行/卡时该行显形)。模糊目标集合是从 `styles.ts` 里**所有使用 `--tw-mono` 的数字面**逐条抽出的清单,不是凭印象写。 - **自选异动(P1-4)**:新增 `host/anomaly.ts`(纯函数 `judgeAnomaly`)与 `GET /tradewatcher/anomaly`。量能倍数用**同时点口径**(今日量 ÷ (20 日均量 × 日内进度),复用护盘标定的进度曲线,阈值 2.5x);价格异动 ±5%;量价共振(|涨跌幅| ≥ 3% 且量能 ≥ 2x)。**样本 < 5 个交易日不判定**,且"没判定"(样本不足/非交易时段/日线里还没有今天这一根)与"判定过且无异常"**严格分家**——前者不是结论,说成"正常"是错的信息。客户端 30 分钟静默记在 localStorage,**静默只压制提醒、不抹掉"它正在异动"这个事实**;行高亮用左侧竖条 + 缓慢呼吸背景(整行变红会与"跌了"的语义冲突)。 - **涨跌家数历史分位(P1-8)**:新增 `host/breadth.ts`(`breadth.json`,60 日窗口 + 30 天余量)与 `GET /tradewatcher/breadth`。数据源与大盘页**同一处**(上证/深证行情里的 f104/f105/f106),不另开链路。快照只在**收盘后**(15:05 起,15:30 后定稿不再改写)记录。分位口径写死并随结果返回:指标=上涨家数占比、基准=**之前** N 个交易日(不含今天,避免自我参照)、`N < 5` 返回 `null` 而不是 0。 - **日历改期留痕与勾稽(P1-10)**:`withChangeHistory()` 按 `autoKey` 对比上一条,日期/结束日/标题变化即记 `from → to`(上限 5 条),未变化不留痕;勾稽在**宿主一处**判定(●持仓 / ○仅自选,用字形而非颜色——颜色已被重要度占用)。当天事件默认按勾稽排序,agent 工具的事件行也打印 `⚠ 可能变更:date A→B ●持仓`。 ## v0.26.0 — 共同功能补强(ROADMAP P1 中不依赖新数据源的部分) - **分时缩略图(P1-1)**:hover 叠加均价线与开/高/低、振幅;均价线用与价格**同一套 y 映射**(不同映射的"均价线"不可比,比不画更糟),缺口不连线;图上不加常驻文字。 - **B/S 去噪与区间图例(P1-2)**:同一根 bar 的多笔合并为一个标记(`B3`/`S2`),悬停给当日小结;图下新增区间图例与 `(区间外另有 N 笔)` —— 不说这句,用户会把"看不到"读成"没有交易"。 - **板块 alpha 口径显式化(P1-3)**:缺失显示 `—` 并给原因,**禁用 0 代替**;行业接口整体失败时页面顶部如实报出错误与时刻(此前 `catch(() => undefined)` 静默吞掉,表现为"这些标的本来就没有行业")。 - **可用数量与费用列(P1-5)**:`isT0Secid()` 单点判定(沪深场内基金/港股/美股 T+0,拿不准按 T+1——保守方向是"绝不说今天买的能卖");累计费用给绝对金额 + 占累计成交额百分比(没有参照物的手续费数看不出贵不贵);成交额为 0 时占比是 `—` 而不是 `0`。 - **资金流排行口径(P1-7)**:口径行常显,15:05 后标「定稿」(按 `Asia/Shanghai` 判定,不用浏览器本地时区)。**偏离**:占比排序只做"本页 40 条内重排",界面写明——上游按净额取前 40,本插件没有"按净占比的全市场排行"这一口径。 - **概率面板统计护栏(P1-9)**:新增 `shared/stats.ts`(Wilson 区间,纯函数)。每个目标涨幅给 Wilson 95% 区间,`N < 30` 标「样本少」。用 Wilson 而非朴素正态近似:后者在 p̂ 接近 0/1 或 n 小时会给出越界区间。注意 Wilson 是 **score 区间**,极端比例下 p̂ 可落在区间外(测试注释里写明了,避免以后被当成 bug)。 ## v0.25.0 — 数据出处、状态外化与可撤销写入(ROADMAP P0 全部落地) 主题是**让已有数据不说谎**:先补口径与状态,再谈加法。 - **读工具数据时刻统一契约(P0-1)**:`shared/model.ts` 新增 `DataProvenance`(`asOf`/`stale`/`source`/`missing[]`);`host/em.ts` 的 `quoteProvenance()` 把行情新鲜度升级为出处契约。7 个读工具全部返回 `provenance` 并在 render 尾部打印【数据出处】与缺失明细。缺失原因分 `no-source`(上游根本没有这份数据,重试无用)与 `transient`(本次失败,稍后重试可能恢复)——两者对应的下一步动作完全不同,因此不许含糊。`stale`(降级复用)与 `cached`(休市定稿)并存。 - **总额口径改为只含 A股(P0-1)**:此前港/美股被按 **1:1** 悄悄加进总额——这比"缺一块"更危险(用户不会去质疑一个看起来正常的数)。现逐项列进 `unpriced[]`(`why`:`no-fx` 口径问题 / `no-quote` 行情问题),界面给 `不含 N 项 ▼` 可下钻。 - **卡片四态与列表头汇总(P0-2)**:6px 色点 + tooltip(源与观测时刻),列表头 `12/23 实时 · 9 延迟 · 2 缺失`,四个数字与逐卡判定**共用同一个函数**(`client/quoteState.ts`)。**未开盘(定稿)时不显示绿色**。灰色与黄色语义不同:定稿是"数据已确定、不需要回源",延迟是"上游这次没给,退回旧值"。 - **侧栏徽标(P0-3)**:新增 `GET /tradewatcher/badge`,只读**同一份** rescue 快照与持仓汇总,**不触发新采样**(徽标是常驻的,若它自己打一遍上游,等于把"看一眼侧栏"变成取数压力)。护盘 ≥ 疑似显示「护」(优先级高于个人盈亏),否则显示当日盈亏率,隐身视图只显示沪深300 点位。图标只有 18px、字形最多 4 个,完整语义放在侧栏展开态的标题与 `aria-label`。 - **非活跃时段显式态(P0-4)**:新增纯函数 `samplingWindow()`(与 `inTradingWindow` 同源);暂停时分数位显示 `—`、状态行 `采样暂停 · 下次 10-09 09:25(已收盘)`,跨周末指向下周一。**暂停时段一律不出现「采样缺口」字样**(说成"缺口"会让人以为要处理);当日曾失败则陈述为"当日曾采样失败 14:32"。 - **图表数据口径条(P0-5)**:`KlineData` 增 `asOf`/`barOpen`;口径条 `日K · klt 101 · 前复权 · 截至 10-09 14:31(当根未收盘,MA/MACD 已含未收盘当根)`。复权用回包的 `kline.fqt`(实际生效值)而非界面请求值;「截至」用 `asOf`,**不拿"现在"顶替**。 - **成本口径切换联动说明(P0-6)**:切档弹 6 秒 toast 报差值;`selftest` 断言"逐项差值之和 == 总额差值",因此那句话不是文案,是可执行约束。 - **护盘评分可解释性与阈值漂移(P0-7)**:因子表加「贡献」列(权 × 得分 × 时点系数)+ 合计行,断言"贡献求和 == 总分 ±0.5";新增 `DayLog.scoreLog`(含各因子原始值与当次生效阈值)。粒度取 **5 分钟刻度 + 等级变化时**而非每条样本:后者在 60 天滚动窗口下会产生数万条记录,而阈值漂移的回溯分辨率本来就不需要秒级。 - **紧凑 / 隐身视图(P0-8)**:`Alt+M` 轮换三档(输入控件内不抢键)。金额遮罩做在**格式化出口内部**(`fmtAmt`/`fmtMoneySigned`/`fmtRaw`),因此"新增一处显示金额忘了判断"不会漏出真实数字。**只影响显示**:取数、告警、agent 工具返回完全不受影响。价格与点位不属于"金额",照常显示。 - **导出 / 导入(P0-9)**:新增 `host/backup.ts`(纯函数 `makeBundle`/`verifyBundle`/`describeConflicts`)与 `GET|POST /tradewatcher/backup`。导入两段式(预览 → 确认覆盖),覆盖前写 `.bak`,导入后重读内存(不需要重启)。拒绝条件逐条给原因:非对象 / 缺 `schemaVersion` / 版本过高 / 缺文件 / 流水条目无法解析 / 引用不存在的 posId·groupId / **重放出现负持仓** / 校验和不匹配。最后一条是硬约束:**宁可不导入,也不导入一份算不平的账**。 - **内嵌 iframe 三态降级(P0-10)**:8 秒超时 → 超时态占位(域名 + 最后成功加载时间 + "本插件无法区分是本机网络还是该站点的问题")+ 一次「重试」;最后成功时间记 localStorage。仍不抓取该站内容。 - **写工具最小权限与可撤销(P0-11)**:新增 `host/writeLog.ts`(`write-log.json`,保留 500 条);`PluginToolExec` 增 `agent.id`(取不到记 `unknown`,不编造);`calendar_add` 过同秒 3 次节流(超限直接报错,**不静默丢弃**)并登记反向操作;新增 `tradewatcher_undo`(撤销本身也留痕)。 **实现过程中修正的两个真问题**:① `assemblePortfolio` 仍在把港/美股按 1:1 汇总(由 `p0.test.ts` 发现);② `verifyBundle` 的校验和在"导出时按原始内容、校验时按归一化后内容"两端不一致(往返一次就报不匹配)。另修正**构建校验器自身的假阴性**:esbuild 对码点 ≤ 0xFF 的字符输出 `\xHH` 而非 `\u00HH`,导致含 `·`/`¥` 的片段永远报"缺失"。 ## v0.24.0 — 适配 dsh 0.2.x 客户端 Slots 契约 dsh 大版本更新后本插件不可用:`ctx.betterSidebar` 已从宿主移除。改为按 **Slots 契约**注册: - 客户端扩展面 = `ctx.slots.inject('sidebar.panellist', …)` + `ctx.slots.register({name,id,order,label}, Component)`;`slot key 'main'` 与 `entryKey` 同时意味着"挂载即可见"(卸载即停止轮询)。 - `sidebar.panellist` 由 `dsh-client-ui-sidebar` 声明(list 类选项 `{id, order?, label?, priority?}`);**宿主没有徽标槽位**,因此徽标做成图标自身的 DOM 叠加。 - 侧栏图标组件与主面板**没有共同的 React 祖先**,唯一不引新依赖的通信通道是**模块级 store + `useSyncExternalStore`**。 - 插件不再依赖任何第三方侧边栏插件;宿主侧只需 `webServer` + `webRuntime`。 ## v0.23.0 — K 线复权(默认前复权) 此前 K 线一律按**不复权**取数:除权除息那天在图上就是一根无解释的暴跌,位置、形态与相对成本全被跳空污染 —— 那是"错的图"。本版把复权做成一等口径(细则见 README「K 线与复权」): - **默认前复权**;抽屉段控 `前复权 / 后复权 / 不复权`,口径常显在脚注。 - **显示的是实际生效口径**(用回包的 `fqt`,不是用户请求的值)——错误的口径标注比没有标注更糟。 - **适用性由宿主判定**(`fqSupported`):股票/ETF/基金/港美股可复权;指数、期货、板块回落不复权,段控**禁用并标「不适用」**且给出理由(禁用必须给理由,不能只灰掉)。 - **口径进缓存键**:`klines/<secid>_<klt>_<fqt>.json`,三个口径各自一份、各自增量。请求口径无缓存时**宁可回源失败也不拿另一口径顶替**——否则图上会出现两套价格拼出来的假连续序列,而 `stale` 标注救不了它。 - **取数混口径修正**:腾讯兜底此前优先读 `${period}`(不复权)键,即便请求前复权也可能拿到不复权序列;现严格按请求口径取 `qfq${period}`/`hfq${period}`/`${period}`。 - **指标与图同源**:MA5/10/30/60、MACD 与 K 线共用同一份序列;B/S 买卖点按当前口径对齐坐标系,tooltip 里仍是真实成交价。 - **详情头不被改写**:今开/昨收/最高/最低/成交量始终是真实成交价,脚注写明「(详情头为真实成交价)」。 - 断言:`chartCache.test.ts` +5、`routes.test.ts` +5、`selftest` +26。 ## v0.22.0 — 详情图入口与历史数据缓存 顶栏行情卡片此前只有 hover 小图、**没有点击入口**,而抽屉每次打开都会重新请求一遍历史数据。本轮补齐入口并重做取数链路: - **入口**:顶栏卡片可点(键盘 Enter/Space 同样可用,按下到抬起位移 >6px 判为拖动滚动,不误触);抽屉状态提升到 `App` 单实例,自选/持仓页共用同一个回调。抽屉打开时顶栏悬浮卡自动隐藏。 - **周期**:分时 / 五日 / 日K / 周K / 月K / 年K,**记住上次选中的周期**。 - **客户端缓存**(`chartCache.ts`):同标的同周期在 TTL 内零 HTTP,同键并发请求合并为一个,失败保留上次成功值并如实标注。TTL:分时 30s / 五日 120s / K 线 600s。 - **休市定稿零回源**(`isSettledOffline`):休市且本地缓存更新时间晚于**最近一个工作日 15:05** 时,K 线与分时直接复用本地并标 `cached: true`(与 `stale` 语义区分——定稿是完整数据,只是不必再问上游);此时客户端 TTL 也放长(5min / 10min / 1h),收盘后反复开关面板一个请求都不发。盘中永不成立。 - **K 线增量**:首次全量(日 800 / 周 400 / 月 240 根),此后只取增量后按日期合并去重;超过 300 个文件按 mtime 清理。 - **诚实标注**:抽屉脚注区分「本地缓存」/「休市定稿缓存(未回源)」/「显示上次成功数据(本次刷新失败)」。 - 断言:`chartCache.test.ts` 8 条、`routes.test.ts` 2 条、`time.test.ts` 3 条。 ## v0.21.0 — 信号正确性与生命周期(第二轮复核) - **脉冲因子曾拿过期的分钟序列去算「最近 5 分钟」**(高影响)。分钟序列此前每天只在冷启动回填写一次(`lastMinuteRefresh` 被赋值却从未读取,正是"计划中的定时刷新没写"的痕迹),而参考点以"序列末端"为锚点——序列过期后分子的成交额跨度变成几十分钟、分母仍是 5 分钟预期。实测(假时钟钉在北京时间 10:30、喂同一份真实行情):**过期序列 → 脉冲 5.01x、因子 60 分;采样环口径 → 1.12x、22 分**。修正两步:① 交易时段内每 60 秒节流刷新分钟序列;② 抽出纯函数 `pickPulseRef` 加**陈旧守卫**(序列末端距评估时点超过 60 秒、或参考点不在 `评估时点 − 5 分钟 ± 30 秒` 窗口内,一律退回采样环口径)。修后同一实验两边都是 **1.132x(差 0.00%)**。 - **插件卸载竞态**:`store.init().then(... rescue.start())` 与 effect disposer 竞态,卸载抢先时会变成"先 stop、再 start"——实测卸载后采样器 `running = true` 且仍持有定时器,热重载还会累积多个采样器并发写同一份 `rescue-log.json`。现加 `disposed` 标记,卸载后不再启动。 - **F2 标注了并未发生的折算**:此前只要核心通道净流入 > 0(哪怕 0.05x)就取 `Math.max(core, peri)`,外围强度按原值计入,而标注写「外围主导(已折算)」——文本与数值不一致(实测核心 0.05x + 外围 0.5x 判 63 分,真折算应为 51)。现口径:**核心净流入需达到第一档锚点才算"参与"**;未参与时外围真打 0.7 折,标注同步写明。自检锁死"标注数值 = 折算后的值"。 - **自选分组备注模态保存无效**(用户可见 bug):输入框绑在 `name` 上、保存提交的是从未更新的 `note`。已改为按模态切换状态源。 - **`noUnusedLocals`/`noUnusedParameters` 开门禁**:清理 13 处死代码;`quoteSource` 改为 `QuoteSource | null` 并在复盘兜底快照里如实输出,不再默认声称"来自东财";`LEDGER_VERBS` 从死代码变成读库时的未知动词告警(条目原样保留)。 - **其余加固**:逐标的行情槽加上限与最旧淘汰;K 线磁盘缓存超 300 文件按 mtime 清理;`nextId` 的 seq 定宽(同一毫秒内既有买又有卖时账本重放顺序不再被字符串序打乱);无价持仓按 0 计入时总额标注「不含无价」并写进 tooltip;agent 提示词段落由 848 字压到 580 字。 ## v0.20.0 — secid 大小写与行情取数次数 **secid 大小写修正**:`113.rbm`/`114.lhm` 这类商品/期货后缀区分大小写,而三家的映射关系完全不同(东财回包小写、腾讯映射数字代码、新浪以大小写无关键匹配)。此前 `em.ts` 内部按原样处理,但**边界层在改写**——`store.sanitizeSecid` 入库 `toUpperCase()`、`routes.splitIds` 请求 `toUpperCase()`、`tools.resolveQuoteIds` 与自定义通道同样大写化。后果:用户从搜索里添加 `114.lhm` → 入库变 `114.LHM` → **永久取不到行情**(同合约小写实测 `price=10680 src=sina`,大写取不到);行情条 23 项经真实路由只回 19 项,缺的正是 4 个国内商品主连,而响应里 `rows=19` 完全不提被丢的 4 项。修法:边界层去改写 + 上游映射与回包大小写不敏感,**历史写入的大写数据自动可用,不需要迁移脚本**;修完行情条 23/23。配套诚实性:`/quotes` 新增 `missing[]`(请求了但没有任何源给出价格的标的)、`requested`/`truncated`(超 160 项上限时如实回报,此前静默截断);搜索结果标注 `hasFallback`(仅东财源的标的必然更易空行,提前说清)。 **行情取数次数与新鲜度**:逐标的缓存槽(此前是"整批 id 拼键",`/quotes` 与 `/portfolio` 标的互相包含却落在两个缓存里)——实测一拍从 20 次上游请求降到 **12 次**,东财熔断后稳态 **0 次**,持仓页子集请求 **0 次**。新鲜度窗口 = `refreshSec`(夹在 5–60 秒),此前有 **40 秒隐式复用宽限**(界面写"每 10s"而数据约 43 秒才换一次)。代价如实说:东财健康时上游请求从约 2.8 次/分升到约 6 次/分,想更省就调大刷新间隔。另:瞬时失败(几十毫秒返回的 `fetch failed`)属针对本机 IP 的封锁特征,整轮都是瞬时失败时跳过第二轮重试;冷启动回填并行化 + 3 秒预算,实测首次采样 **7842ms → 3063ms**。 ## v0.19.1 — 熔断横幅的状态表述 东财主机封锁可能持续整天,而面板此时**往往仍有可用数据**(采样器自动回落腾讯备用源)。此前面板顶部常驻一整行红字「……期间不发起请求以免加重封锁,**面板显示当日复盘数据**」——在备用源兜底成功的场景下与事实相反。现改为: - **一行短句 + tooltip 明细**;逐主机剩余时间、连续次数、最后错误收进 tooltip。 - **按时段分场景**:交易时段说「最早约 N 分钟可重试」(取**最早**恢复的那台),收盘后说「下次开盘自动重试」。 - **因子缺失原因分开写**:`flowSource === 'tencent'` 时明确「该源不含分单资金流,不是等几分钟就能补齐」;只有东财源下的缺因子才是「冷启动/采样满 5 分钟自动补齐」。 - **数据时刻与失败时刻分离**:失败分支不再改写 `ts`(此前收盘后手动点一次「立即采样」而上游不可用,顶部的「已收盘 · 21:34:43」会变成**失败时刻**,看起来像刚采到数据)。 - **「本会话未采样」≠「采样缺口」**:收盘后重启用持久化的上次快照(红标「上次数据」+ note),`gap` 只反映当日真实失败。 - **来源与失败时刻落盘**(`flowSource`/`lastFailTs`),收盘后重启的面板仍能说明数据来自哪个源。 ## v0.19.0 — 排序 自选与持仓支持组内排序(键:自选 = 默认/当日涨跌/总市值;持仓 = 默认/市值/持仓盈亏/当日盈亏/仓位占比),选择存进 `prefs.json`,同键再点切换升降序。两条硬约定(`sort.ts`,8 组断言锁死):**无效值恒沉底**(升序也不许跑到最前——`null` 参与数值比较会退化成 0,正是这类功能最常见的错误)、**稳定且可预期**(同值保持原顺序,不会每次行情轮询后重新洗牌)。prefs 走白名单校验(非法键/非布尔方向抛错),明文文件被手改成非法值时装载即回退默认。市值来源:东财 `f20` → 腾讯 `f45`(原始单位**亿元**,已 ×1e8);**新浪备用源不提供市值**,故国际指数/商品/期货经新浪兜底时没有市值 → 排序沉底且界面不显示该栏(不编造)。 ## v0.18.0 — 数据诚实性 - **`/quotes` 回传真实 `asOf`/`stale`**:`asOf` 是**数据被观测到的时刻**(兜底行保留其原始观测时刻)。**此前显示的是响应生成时刻**——上游全挂、整屏都是几小时前的旧值时,界面依然写「更新 14:32:05」。 - **`tick()` 重入守卫**:定时循环、前端 `?force=1`、路由 `ensureFresh()` 三条路径统一经 `SingleFlight` 合并。此前会并发跑两份全量采样:上游请求翻倍、`today.samples` 重复计数、两份快照互相覆盖(样本数可能"回退")。 - **`buildBottom` 保留上次视图**:日线未到/校准冷却中/校准失败时返回上次成功结果并标 `stale` + `computedAt`,不再整块消失。 - **错误语义分级**(`http.ts`):400 请求本身有问题 · 413 请求体过大 · **503 上游不可用**(熔断中/连接被关闭/超时/上游 4xx-5xx/非 JSON)· 500 未归类(本插件 bug);503 附带 `retry-after`。此前一律 400,把"上游被限流"报成"你的请求写错了"。 - **熔断状态聚合**:`/rescue` 与 `/health` 返回 `breakerSummary`(逐主机明细 + `minutesLeft` + `allMinutesLeft` + 最近失败原因)。`minutesToRecover` 取**最小值**(最早恢复即可重试),此前取最大值会把"还有 1 分钟可试"显示成"约 14 分钟后重试"。 - **回测/标定路径同样清洗日线**:`sanitizeBars` 覆盖 `laneOutcomeStats`/`calibratePooled`/`computePosition`/`buildBottomLane`。此前只有展示路径清洗,一根 `NaN` 日线会让 `Math.max(...fwd)` 变 `NaN` 并污染概率、中位数与回撤中位数,**却仍被当作有效样本**。 - **构建期的类型门禁区分两种失败**:`tsc` 未能启动(依赖没装)退出码 2 并提示 `npm i`;被信号杀死退出码 3;类型错误退出码 1 —— 不再把"没装 typescript"报成"类型检查不通过"。 ## v0.17.0 — 时间口径统一为 Asia/Shanghai 此前 `rescue.ts`/`calendar.ts` 用宿主本地时间、而 `portfolio.ts` 用 `Asia/Shanghai`,两套并存——实测在 **TZ=UTC**(Docker / 云主机 / CI 的默认值)下,北京时间 10:00 会被判成"盘前",**护盘采样一次都不触发**,且时点系数、阶段语义、脉冲锚点全部错位(晚间盘面按"早盘"给 0.4 折)。现 `hhmmOf`/`dayOf`/`weekdayOf`/`calToday`/`shanghaiDayStart` 全部来自同一实现(`src/host/time.ts`),并在 **4 个时区**(Asia/Shanghai、UTC、America/New_York、Australia/Sydney)下断言一致;`npm test` 会把这套断言跑两遍(宿主 TZ 与 TZ=UTC),因为只有显式切 TZ 才锁得住时区回归。注意 `inTradingWindow` 里的**星期**也必须按北京时间——北京时间周一 10:00 在 `TZ=America/New_York` 下是周日 22:00,用宿主星期会把真实交易日判成休市。 ## v0.15.1 — 板块/资金/ETF 的备用源与 last-known-good 这一栏此前"一直不可用":它只在**打开面板或切换 scope/sort 时**才请求,属**冷连接**请求——东财行情 CDN(push2 系列)对本机新连接的封锁期间,行情与护盘因为持续轮询还养着旧连接与缓存,而这一栏每次都必然失败;且它**既没有备用源也没有 last-known-good**。现补三层:① 行业/概念 → 腾讯板块排行(`mktHs/rank`);② ETF 排行 → 新浪(`etf_hq_fund`);③ `board-lkg.json` 兜底(全部上游不可用时显示上次成功结果并标注时间)。面板注明数据来自哪个备用源,并说明**「主力净流入」仅东财提供**(备用源下显示 —)。另修正 ETF 榜的「换手」列(此前错映射到 f128,现取 f8 / 新浪换手率)。 ## v0.15.0 — 底部位置 / 形态 / 概率 回答"当前位置是否具备底部特征",与护盘行为识别并列展示(**不进入**护盘评分,避免把两件事混成一个数):**位置**(距 60/250 日最高回撤、近 60 日收盘分位、距 60 日最低、连续下跌天数、是否创 60 日新低);**日内形态**(现价相对当日最低的回升、下影线占振幅比例、当日最低出现的时刻、是否"跌破前 20 日低后收回"、是否"创 60 日新低后收回");**底部概率(历史频率)**——把当前状态与历史同类日类比(`近 60 日收盘分位 ≤ 25% 且 当日量能 ≥ 前 20 日均量 × 1`),统计其后 5 日内最高价达到当日收盘 ×(1+目标) 的频率,**同时给出无条件基线与样本量**。实测(2026-09-28,6 通道合并):目标 +1% 同类 74.3% vs 基线 72.5%(+1.8pp,几乎无优势);目标 **+2% 同类 61.4% vs 基线 50.8%(+10.6pp)**;前向 5 日收益中位数 +1.0%、期间最大回撤中位数 −2.0%;同类 N=140、基线 N=1536。 **说实话的边界**:① 概率是历史同类情形的频率、**不是预测**,必须与 N 和基线一起读,样本不足时不给结论;② **日内形态不进入概率**——没有可回算的历史分钟数据,混进去就是伪精确,它只作实时修正参考;③ **位置低 ≠ 见底**,护盘也可能发生在下跌半程。 **实现记录(踩过的坑)**:校准必须**逐通道判定特征与前向收益、只合并统计结果**——曾把多通道日线合并后按日期排序,导致"前 60 根"跨品种、前向窗口落到别的品种上,得出"前向收益中位数 **+105%**"的荒唐值;该场景已写成回归断言。 ## v0.14.2 — 搜索的熔断隔离与腾讯备用源 现象是"自选/持仓搜不出任何标的"。两个原因:① **熔断器范围过宽**——最初所有东财请求共用一个熔断器,行情主机(push2 系列)被限流时,走 `searchapi.eastmoney.com` 的**搜索被一起拦下**(而该主机当时完全正常)。现改为**按主机隔离**(`breakerFor(host)` + `hostsAllowed()`),并在 selftest 里锁死"行情熔断不得影响搜索";② 搜索接入**腾讯智慧搜索**备用源(`smartbox.gtimg.cn`,响应是带 `\uXXXX` 转义的 JS 字面量,需还原;类型词表已对齐东财口径),覆盖沪/深/港股。 ## v0.14.1 — 行情链路的腾讯备用源 此前备用源只接了护盘模块,**自选/持仓的行情仍在走东财**——东财被限流时它们只能吃 last-known-good,价格冻在旧值(表现为"无法加载最新数据")。现把腾讯接进主链路:EM 拿不到的标的(或价格为空)由腾讯补齐,行上带 `source: 'tencent'`,顶栏显示「备用源 N」;分时序列同样增加腾讯分钟线兜底(含当日均价 VWAP)。**覆盖沪/深/港股**;美股、国际指数、商品无腾讯映射,如实回落到 last-known-good。 ## v0.14.0 — 腾讯备用源与自定义通道 东财行情主机(push2 系列与 push2his)对本机 IP 限流/封锁时,改用**腾讯免费接口**保住「量能、脉冲、价格」:批量快照 `qt.gtimg.cn/q=`(1 次请求覆盖全部通道)与当日分钟线 `web.ifzq.gtimg.cn/appstock/minute`。**「超大单净流入」只有东财提供,无替代**——备用源下该字段为 null、面板如实标注,并在评分上**封顶为「资金异动」**(没有资金证据就不能说"有资金在托底"),`flowSource` 标出本次快照来源。通道统一以卡片呈现(上游不可用时按当日峰值合成「复盘」态卡片,而不是退回表格);设置里可添加**自定义通道**(通常为板块 ETF,如 `1.512480` 半导体ETF),代码取东财 secid 前缀(沪市 `1.` / 深市 `0.`)。自定义通道**只做量能与脉冲观察,不进入护盘评分与共振**——板块异动与"国家队托底"不是一回事;缺少 20 日均额基准时后台自动补算。 ## v0.13.3 — 复盘表的渲染修复 v0.13.2 的兜底数据在服务端已正确返回(实测接口带了 6 行峰值),但**客户端从未渲染**——上轮改客户端时用脚本替换且未加断言校验,插入复盘表的那处因缩进不匹配静默失败(同期横幅代码却插入了,导致"有横幅、无表格"的假象)。现用精确编辑修正,并在构建后**校验产物确实包含该段文案**;同时不再用 `etfs.length > 0` 守卫整个展开块(否则无卡片时会连分时图/时间线/历史回看一起隐藏)。 ## v0.13.2 — 上游封禁/限流时的熔断与复盘兜底 东财行情主机(`push2*`/`push2his`)在请求量偏大时会升级为**持续不可达**(实测失败率一天内从 25% 爬到 100%,而东财数据中心与腾讯/新浪仍正常 → 属针对本机 IP 的限流)。此时"重试 12 次"只会火上浇油,因此:① 进程级**熔断**(连续 3 次调用失败即停手,冷却 2→4→8→上限 15 分钟,半开试探成功即复位),行情中继与护盘采样器共用;② 重试轮数由 3×2 降到 2×2(最多 8 次尝试);③ 路由不再抛错(`trend` 返回 `null`),客户端自行降级;④ 面板显示熔断层(剩余时间 + 最后错误),并用**当日复盘数据**兜底:优先用各通道最后一次成功采样还原卡片,否则渲染「当日通道峰值」表——**不再出现整块空白**。 ## v0.13.0 — 消除「重启后因子断档」 - **冷启动回填**:采样环在内存里,进程重启后「脉冲」与「持续性」都需要 ≥5 分钟历史才可算;若重启发生在收盘前 5 分钟内,当天就再也算不出来(实测 9/24 14:44 重启即整日显示 `—`)。现首轮采样即用**当日分钟数据回填**(东财 `trends2` 每分钟成交额 + `fflow klt=1` 每分钟累计超大单/主力,约 240 点),实测重启后立刻得到「尾盘突袭 1.86x / 持续性 −2.0%」,6/6 通道可用。 - **阶段语义**:区分「尾盘时段」与「已收盘」——收盘后不再把脉冲因子叫「尾盘突袭」(改为「脉冲(盘后)」,面板显示「已收盘」)。 - **因子完整度如实标注**:快照带 `completeness`,缺因子时状态条显示「因子 N/6」并注明「评分偏保守」(缺因子按 0 计入,方向偏保守,不做归一化放大)。 - **资金方向可见**:通道卡新增「吸纳/撤离」标记(超大单占成交额 ±15%),避免把「大额净流出」误读成「哑火」。 - **旧口径留痕**:新事件写入引擎版本,面板对早期记录标「旧口径」——升级前后评分逻辑不同,不能直接比较。 - **快照 last-known-good + 无数据自动补采**:采样循环只在交易时段内 tick,**收盘后重启**的进程不会产生任何快照 → 前端拿到空快照,标的卡与因子明细会整块消失(8/24 盘后实测)。现三层防护:① 每次成功采样把轻量快照落盘(`rescue-log.json.lastSnapshot`),本会话无数据时回落到它并标 `stale`;② 路由在无新鲜数据时按 20 秒冷却自动补采一次;③ 客户端收到空快照时不覆盖已有数据。 - **盘后按「当日最后 5 分钟」评估**:非交易时段不写入新样本,改以采样环内最后一个盘中样本为评估时点;面板显示「已收盘」而不沿用「尾盘突袭」。 ## v0.12.0 — 护盘口径修订(针对早期误报) - **时点系数真正接入评分**:此前只用于展示,运行时恒为 1 → 早盘噪音最大的时段拿到满权重;实测同一盘面 09:48 由 62 分降为 25 分。 - **F2 以核心通道(沪深300/上证50)为主**:仅外围通道净流入时打 0.7 折;**核心通道超大单净流出达成交额 −15% 时封顶为「资金异动」**(与托底特征相反)。 - **F3 区分盘中与尾盘**:14:30 前为「盘中脉冲」并打 0.6 折(大单对倒易造假),14:30 后为「尾盘突袭」满分权重;**命中线为该时段 P90**,锚点按时段分档标定(早盘 1.23/1.73/2.03x、尾盘 1.26/1.91/2.43x,不再是单一 1.5/2.5/4x——旧口径下 14–20% 的 5 分钟窗口天然越线)。 - **F4 由「连续递增次数」改为「时间 + 幅度」**:最近 5 分钟超大单净增 ÷ 该窗口成交额(锚点 5%/12%/25%),窗口内单次回撤超净增 50% 时打 0.6 折;不再随采样间隔漂移。 - **F6 共振列出标的名并分层**:核心 0 只时不得给「强护盘信号」(外围单买不足以认定系统性护盘);面板显示「系统性 / 局部」。 - **通道自身触发要求有净流入**:只有量能或脉冲、没有超大单净流入不再计入共振。