dsh-plugin-show-me-data 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/LICENSE +27 -0
  2. package/README.md +96 -0
  3. package/cordis.patch.yml +40 -0
  4. package/docs/01-product-effect.md +178 -0
  5. package/docs/02-architecture.md +275 -0
  6. package/docs/03-data-contracts.md +291 -0
  7. package/docs/04-sources.md +342 -0
  8. package/docs/05-ui-spec.md +167 -0
  9. package/docs/06-ai-layer.md +194 -0
  10. package/docs/07-implementation-plan.md +399 -0
  11. package/docs/08-test-plan.md +133 -0
  12. package/docs/09-packaging-install.md +249 -0
  13. package/docs/10-kickoff-prompt.md +94 -0
  14. package/docs/11-decisions.md +203 -0
  15. package/docs/12-runtime-verified.md +115 -0
  16. package/docs/13-acceptance.md +153 -0
  17. package/docs/14-progress.md +150 -0
  18. package/docs/15-publish.md +185 -0
  19. package/lib/app/ai-deterministic.js +327 -0
  20. package/lib/app/ai-validate.js +284 -0
  21. package/lib/app/ai.js +440 -0
  22. package/lib/app/health.js +77 -0
  23. package/lib/app/overview.js +349 -0
  24. package/lib/app/propose-indicator.js +122 -0
  25. package/lib/app/refresh.js +251 -0
  26. package/lib/app/series-view.js +195 -0
  27. package/lib/app/watchlist.js +102 -0
  28. package/lib/client.js +4322 -0
  29. package/lib/core/ai/prompts.js +213 -0
  30. package/lib/core/chart/axis.js +133 -0
  31. package/lib/core/chart/bar.js +58 -0
  32. package/lib/core/chart/candle.js +216 -0
  33. package/lib/core/chart/line.js +186 -0
  34. package/lib/core/chart/scale.js +132 -0
  35. package/lib/core/format.js +143 -0
  36. package/lib/core/indicators/catalog.js +1011 -0
  37. package/lib/core/indicators/resolve.js +196 -0
  38. package/lib/core/insight/digest.js +250 -0
  39. package/lib/core/insight/rank.js +115 -0
  40. package/lib/core/insight/related.js +90 -0
  41. package/lib/core/insight/rules.js +417 -0
  42. package/lib/core/stats/derive.js +123 -0
  43. package/lib/core/stats/series.js +465 -0
  44. package/lib/core/time/range.js +242 -0
  45. package/lib/core/types.js +478 -0
  46. package/lib/host/ai/discussion.js +559 -0
  47. package/lib/host/ai/dsh-llm-gateway.js +333 -0
  48. package/lib/host/config.js +194 -0
  49. package/lib/host/http/respond.js +165 -0
  50. package/lib/host/http/routes.js +689 -0
  51. package/lib/host/index.js +293 -0
  52. package/lib/host/infra/fs-repos.js +179 -0
  53. package/lib/host/infra/memory-fallback.js +64 -0
  54. package/lib/host/tools/define-tool.js +295 -0
  55. package/lib/host/tools/register.js +431 -0
  56. package/lib/host.js +7 -0
  57. package/lib/ports/clock.js +57 -0
  58. package/lib/ports/snapshot-repo.js +48 -0
  59. package/lib/sources/eastmoney-macro.js +197 -0
  60. package/lib/sources/eastmoney-quote.js +201 -0
  61. package/lib/sources/ecb.js +179 -0
  62. package/lib/sources/fred.js +207 -0
  63. package/lib/sources/http.js +136 -0
  64. package/lib/sources/ohlc.js +36 -0
  65. package/lib/sources/quote-cascade.js +177 -0
  66. package/lib/sources/registry.js +153 -0
  67. package/lib/sources/sina-cn.js +197 -0
  68. package/lib/sources/sina-us.js +187 -0
  69. package/lib/sources/tencent.js +158 -0
  70. package/lib/sources/us-treasury-rates.js +275 -0
  71. package/lib/sources/us-treasury.js +196 -0
  72. package/lib/sources/worldbank.js +170 -0
  73. package/package.json +69 -0
  74. package/src/app/ai-deterministic.js +327 -0
  75. package/src/app/ai-validate.js +284 -0
  76. package/src/app/ai.js +440 -0
  77. package/src/app/health.js +77 -0
  78. package/src/app/overview.js +349 -0
  79. package/src/app/propose-indicator.js +122 -0
  80. package/src/app/refresh.js +251 -0
  81. package/src/app/series-view.js +195 -0
  82. package/src/app/watchlist.js +102 -0
  83. package/src/client/api.js +323 -0
  84. package/src/client/components.js +1877 -0
  85. package/src/client/copy.js +368 -0
  86. package/src/client/index.js +169 -0
  87. package/src/client/store.js +219 -0
  88. package/src/core/ai/prompts.js +213 -0
  89. package/src/core/chart/axis.js +133 -0
  90. package/src/core/chart/bar.js +58 -0
  91. package/src/core/chart/candle.js +216 -0
  92. package/src/core/chart/line.js +186 -0
  93. package/src/core/chart/scale.js +132 -0
  94. package/src/core/format.js +143 -0
  95. package/src/core/indicators/catalog.js +1011 -0
  96. package/src/core/indicators/resolve.js +196 -0
  97. package/src/core/insight/digest.js +250 -0
  98. package/src/core/insight/rank.js +115 -0
  99. package/src/core/insight/related.js +90 -0
  100. package/src/core/insight/rules.js +417 -0
  101. package/src/core/stats/derive.js +123 -0
  102. package/src/core/stats/series.js +465 -0
  103. package/src/core/time/range.js +242 -0
  104. package/src/core/types.js +478 -0
  105. package/src/host/ai/discussion.js +559 -0
  106. package/src/host/ai/dsh-llm-gateway.js +333 -0
  107. package/src/host/config.js +194 -0
  108. package/src/host/http/respond.js +165 -0
  109. package/src/host/http/routes.js +689 -0
  110. package/src/host/index.js +293 -0
  111. package/src/host/infra/fs-repos.js +179 -0
  112. package/src/host/infra/memory-fallback.js +64 -0
  113. package/src/host/tools/define-tool.js +295 -0
  114. package/src/host/tools/register.js +431 -0
  115. package/src/ports/clock.js +57 -0
  116. package/src/ports/snapshot-repo.js +48 -0
  117. package/src/sources/eastmoney-macro.js +197 -0
  118. package/src/sources/eastmoney-quote.js +201 -0
  119. package/src/sources/ecb.js +179 -0
  120. package/src/sources/fred.js +207 -0
  121. package/src/sources/http.js +136 -0
  122. package/src/sources/ohlc.js +36 -0
  123. package/src/sources/quote-cascade.js +177 -0
  124. package/src/sources/registry.js +153 -0
  125. package/src/sources/sina-cn.js +197 -0
  126. package/src/sources/sina-us.js +187 -0
  127. package/src/sources/tencent.js +158 -0
  128. package/src/sources/us-treasury-rates.js +275 -0
  129. package/src/sources/us-treasury.js +196 -0
  130. package/src/sources/worldbank.js +170 -0
@@ -0,0 +1,399 @@
1
+ # 07 · 实施计划(TDD,M0–M10)
2
+
3
+ ## 0. 工作约定
4
+
5
+ **每次改动的最小循环**
6
+
7
+ 1. **红**:先写/改测试文件,跑 `node --test`,确认**因为缺少实现而失败**(不是语法错)。
8
+ 2. **绿**:写最小实现让其通过。**一次只让一个测试变绿**。
9
+ 3. **重构**:消除重复、抽出纯函数、把 IO 推到边界外。测试保持绿。
10
+ 4. **提交**:一条提交只做一个任务卡(T0.x);提交信息写 `M<m>.<t>: <做了什么> (tests: N pass)`。
11
+
12
+ **硬性纪律**
13
+
14
+ | 纪律 | 理由 |
15
+ | --- | --- |
16
+ | `src/core/**` 与 `src/app/**` 禁止出现 `fetch` / `fs` / `Date.now` / 任何 DSH 导入 | 保证 100% 可离线单测;时间与网络从端口注入 |
17
+ | 测试默认**不打网**;真网测试用 `RUN_NET=1` 门控并单独文件 | CI 稳定、速度快 |
18
+ | 每个适配器必须有 fixture;fixture 由 `scripts/record-fixtures.mjs` 录制 | 可回放、可回归 |
19
+ | 先写"会失败"的边界测试(空数组、缺失值、单点、全等值、超长序列) | 这类 bug 是数据面板的主要缺陷来源 |
20
+ | 不许为了让测试通过而在 core 里加 IO | 破坏分层比测试失败更糟 |
21
+ | 新增指标/适配器**不允许改动 core 逻辑文件** | 这是"可扩展"的验收方式(见 §11 验收) |
22
+
23
+ **运行方式(零外部依赖)**
24
+
25
+ ```bash
26
+ node --test test/ # 全部
27
+ node --test test/core/ # 单层
28
+ RUN_NET=1 node --test test/net/ # 真网冒烟(不进默认门禁)
29
+ node scripts/build-client.mjs # 生成 lib/client.js
30
+ node --test test/host/build-client.test.js
31
+ ```
32
+
33
+ > 不需要 pytest/jest/vitest。`node:test` + `node:assert` 足够,且**避免一切安装依赖的风险**
34
+ > (当前 profile 里没有 vitest/tsdown;npm registry 可达,但少一个依赖少一个故障点)。
35
+
36
+ ---
37
+
38
+ ## M0 · 校准与骨架(半天)
39
+
40
+ **目标**:把本文档的假设换成运行时事实;建立可跑的测试骨架。
41
+
42
+ ### T0.1 运行时校准(**先做,且必须做的第一件事**)
43
+
44
+ - 在创造模式会话里执行 `cordis_inspect`,按 `02` 文档 §6 的 7 项逐一核对:
45
+ `what:"client"`(槽位)、`what:"api" name:"webServer"`、`name:"tools"`、`name:"llm"`、
46
+ `name:"settings"`、`Theme.listTokens`、以及 `dsh.client.inject` 需要哪些包。
47
+ - 产出 `docs/12-runtime-verified.md`:表格「假设 / 实测 / 结论 / 需要改哪些文档与测试」。
48
+ - **验收**:7 项全部有实测结论;若有偏差,回到对应文档改掉假设(不要留给后面的里程碑)。
49
+
50
+ ### T0.2 骨架与契约冻结
51
+
52
+ - 建目录(照 `02` §2)、`package.json`(含 `dsh.client` 元数据,参照 `dsh-client-ui-jobs`)。
53
+ - 写 `src/core/types.js` 的校验器**测试**:`test/core/types.test.js`
54
+ - 合法 `IndicatorDef` 通过;非法 id(`US.CPI`、`a`、`a.`)逐个失败且错误信息可读。
55
+ - `RawSeries` 乱序、含 `NaN`、日期格式错 → 失败。
56
+ - 实现校验器。
57
+ - **验收**:`node --test test/core/types.test.js` 全绿;`docs/12-runtime-verified.md` 存在。
58
+
59
+ ### T0.3 测试基建
60
+
61
+ - `test/helpers/{fixtures,clock,memory-repos,client-shim,fake-fetch}.js`
62
+ - `test/core/_smoke.test.js`:证明 clock 可固定、memory repo 可读写。
63
+ - **验收**:假 fetch 能返回 fixture;client shim 能取出 `factory` 并执行。
64
+
65
+ ---
66
+
67
+ ## M1 · 时间与统计(纯函数,1 天)
68
+
69
+ **目标**:所有数值语义在这里定死。这是最容易出错、也最值得先写测试的一层。
70
+
71
+ ### T1.1 `core/time/range.js`
72
+
73
+ 测试 `test/core/time-range.test.js`:
74
+
75
+ | 用例 | 断言 |
76
+ | --- | --- |
77
+ | `resolveRange('1M', '2026-09-11')` | `from='2026-08-11'`, `to='2026-09-11'` |
78
+ | `resolveRange('YTD', '2026-09-11')` | `from='2026-01-01'` |
79
+ | `resolveRange('1Y', '2026-03-31')` | 跨年正确(`2025-03-31`) |
80
+ | 月末边界 `resolveRange('1M','2026-03-31')` | 不产生 `2026-02-31`(钳到 `2026-02-28`,或按约定回退到 `2026-03-01`——**选定一种并在测试里固化**) |
81
+ | 闰年 `resolveRange('1Y','2024-02-29')` | `2023-02-28` |
82
+ | `CUSTOM` 且 `from > to` | 抛 `RangeError` |
83
+ | `resolveRange` 不读系统时间 | 用固定 clock 传入,测试无法察觉系统时间(可用 `--test` 下 monkey-patch 断言未被调用) |
84
+
85
+ ### T1.2 `core/stats/series.js`
86
+
87
+ 测试 `test/core/stats-series.test.js`,每个函数至少覆盖:正常、空输入、单点、含缺口、全等值。
88
+
89
+ - `sortDedupe(points)`:升序;同日重复保留**最后一条**。
90
+ - `parseNumber(raw)`:`'.'`/`''`/`'N/A'`/`'-'`/`'null'` → `undefined`;`'1e3'` → 1000。
91
+ - `filterRange(points, range)`:闭区间。
92
+ - `mom(points)` / `yoy(points)`:**同月对齐**;缺 12 个月前的点时用 ±1 月容差;都不存在 → `undefined` 且记 `drift`。
93
+ - `movingAverage(points, n)`:前 n−1 个点返回 `undefined`(不返回部分均值)。
94
+ - `diff(points, window=1)`:窗口 3 → `v[i]-v[i-3]`。
95
+ - `stdDev`:n<6 → `undefined`(**明确不编 σ**)。
96
+ - `percentile`:n<12 → `undefined`;线性插值边界(p=0 返回 min,p=1 返回 max)。
97
+ - `slope`:完美线性序列 → 精确等于斜率(用容差 1e-9 断言)。
98
+ - `zScoreLatestChange`:历史变化全为 0 → `undefined`(避免除零)。
99
+ - `stats(points)`:组装 `SeriesStats`,字段与 `03` 文档一致。
100
+
101
+ ### T1.3 `core/stats/derive.js`
102
+
103
+ - `spread(a,b)`:按日期**内连接**;结果点数 < 3 → 空 + `reason:'insufficient-overlap'`。
104
+ - `ratio(a,b)`:b 含 0 → 该点跳过(不产生 Infinity)。
105
+ - `avgOf([...])`:缺任一 operand 的日期 → 该日跳过。
106
+
107
+ **验收 M1**:`test/core/time-range.test.js`、`test/core/stats-*.test.js` 全绿;
108
+ `core/stats` 行覆盖 ≥ 95%;**没有任何 IO 导入**(用一个静态断言测试扫描源码文本,见 `08`)。
109
+
110
+ ---
111
+
112
+ ## M2 · 图表几何(纯函数,半天)
113
+
114
+ **目标**:图先能被测,然后再谈好不好看。
115
+
116
+ ### T2.1 `core/chart/scale.js` / `line.js` / `bar.js` / `axis.js`
117
+
118
+ 按 `05` 文档 §4.1 的函数签名与 §4.1 表格里的**必测边界**逐条写测试(先写边界用例)。
119
+
120
+ 必须包含的退化用例:
121
+
122
+ - `[]` → `''`;单点 → `'M…'`(无 `L`);两点 → 一条 `L`。
123
+ - `min === max` → 除以零保护,输出水平线,`Number.isFinite` 全真。
124
+ - 含 `NaN` → 断点分段(断言出现两个 `M`)。
125
+ - `niceTicks(0.31, 0.42, 5)` → 刻度整齐且首尾包含(断言为 `[0.30,0.35,0.40,0.45]` 之类整步长)。
126
+ - 10,000 点抽稀 → 路径点数 ≤ `2*width`,耗时 < 30ms。
127
+
128
+ **验收**:`test/core/chart-*.test.js` 全绿;覆盖 ≥ 95%。
129
+
130
+ ---
131
+
132
+ ## M3 · 数据源适配器(2 天)
133
+
134
+ **目标**:真能取到数,且测试完全离线。
135
+
136
+ ### T3.1 适配器契约测试(先写)
137
+
138
+ `test/contract/source-adapter.test.js` 遍历 `registry.js`,
139
+ 对每个适配器跑 `04` 文档 §7 的 7 组断言。此时注册表为空 → 测试 vacuous 通过,
140
+ 所以**同时**写 `test/contract/_registry-nonempty.test.js`:断言注册表至少含 6 个适配器
141
+ (`fred, eastmoney-quote, eastmoney-macro, us-treasury, worldbank, ecb`)——这样"忘了注册"会红灯。
142
+
143
+ ### T3.2 录制 fixtures
144
+
145
+ `scripts/record-fixtures.mjs`(用系统 `fetch`,`RUN_NET=1` 才跑):
146
+
147
+ - 固定一组 `seriesRef`(见 `03` 文档种子目录),把**原始响应体**原样存到
148
+ `test/fixtures/<adapter>/<seriesRef-slug>.<ext>`,并写一个 `index.json` 记录
149
+ `{ url, fetchedAt, httpStatus }`。
150
+ - 同时保存**异常样本**:把 FRED 的 `"."`、eastmoney 的 `success:false`、
151
+ World Bank 的 `[meta, null]`、空响应各存一份为 `*.error.*`。
152
+
153
+ ### T3.3 逐个实现适配器
154
+
155
+ 顺序:`fred` → `eastmoney-quote` → `eastmoney-macro` → `us-treasury` → `worldbank` → `ecb`。
156
+ 每个适配器的测试文件只做一件事:**用 fixture 断言归一化结果**。
157
+
158
+ 必测点(每个适配器都要有):
159
+
160
+ | 适配器 | 必测 |
161
+ | --- | --- |
162
+ | `fred` | `.` 被跳过;`cosd` 参数被拼进 URL;升序输出;`Number()` 精度收敛(`115.004000000000005` → 按 decimals) |
163
+ | `eastmoney-quote` | `fields2` 顺序映射(**f53=收盘**);`rc!==0` → `unsupported`;`1.000001` 的名称断言含「上证」 |
164
+ | `eastmoney-macro` | 倒序 → 升序;`REPORT_DATE` 只取日期部分;`success:false` → `unsupported`;字段缺失 → `parse` 而不是 `undefined`;**`params.column` 必须显式指定**,且断言 `BASIC_CURRENCY_SAME` 命中 `cn.m2.yoy`(M2 同比量级 0–20%)而不是误用 `CURRENCY_SAME`(M1) |
165
+ | `us-treasury` | 字符串数值转换;空串跳过;`filter` 参数拼接 |
166
+ | `worldbank` | `rows === null` → `empty`;`date:'2025'` → `2025-12-31`;`value:null` 跳过 |
167
+ | `ecb` | CSV 表头解析;`lastNObservations` 被使用;`OBS_VALUE` 空值跳过 |
168
+
169
+ ### T3.4 (可选 spike)中国 10Y 国债收益率与 LPR
170
+
171
+ - 独立任务,**时间盒 2 小时**。找不到可用源就把目录里对应条目标 `unsupported` 并注明调查结论。
172
+ - **禁止**猜接口、禁止伪造 seriesRef。
173
+
174
+ **验收 M3**:`node --test test/contract` 与 `node --test test/sources` 全绿(离线);
175
+ `RUN_NET=1 node --test test/net/sources-live.test.js` 在真网可用时全绿(该文件断言
176
+ "每个适配器至少一个 seriesRef 能返回 ≥ 5 个点")。
177
+
178
+ ---
179
+
180
+ ## M4 · 指标目录(1 天)
181
+
182
+ **目标**:把 `03` 文档的种子目录变成数据,并保证它自洽。
183
+
184
+ ### T4.1 目录完整性测试 `test/core/indicators-catalog.test.js`
185
+
186
+ - 每个 `IndicatorDef` 过 `validateIndicatorDef`。
187
+ - `id` 全局唯一;`group` 在枚举内;`importance=5` 的指标数量在 8–14 之间(首页展示量合理)。
188
+ - 每个指标引用的 `source.adapter` 在注册表里存在。
189
+ - **每个指标在 fixture 里都有对应回放样本** → 若缺,红灯(防止"目录写了但取不到数")。
190
+ - 派生指标的 `operands` 都存在于目录中,且**无循环依赖**(拓扑排序测试)。
191
+
192
+ ### T4.2 解析测试 `test/core/indicators-resolve.test.js`
193
+
194
+ - 中文关键词命中:`'非农'` → 含 `us.payrolls.change`;`'国债'` → 含 `cn.bond.govt.index`。
195
+ - 英文与别名命中(`'CPI'`、`'payrolls'`、`'pmi'`)。
196
+ - 无命中 → 返回空数组 + 建议(不抛错)。
197
+ - 模糊查询排序稳定(同分按 `importance` 降序)。
198
+
199
+ **验收 M4**:目录 ≥ 45 条、全部有 fixture、解析测试全绿。
200
+
201
+ ---
202
+
203
+ ## M5 · 值得关注规则引擎(1 天)
204
+
205
+ **目标**:`01` 文档 §4 的 8 条规则全部可复现、可解释。
206
+
207
+ ### T5.1 每条规则一个 describe 块 `test/core/insight-rules.test.js`
208
+
209
+ 对每条规则给**三组用例**:命中、临界不命中、数据不足(不得报错)。
210
+
211
+ | 规则 | 关键断言 |
212
+ | --- | --- |
213
+ | `fresh-release` | 频率决定窗口(日 3 / 月 10 / 季 45 / 年 400 天);窗口内命中的 `reason` 含"新发布" |
214
+ | `surprise-sigma` | z ≥ 1.5 命中;样本 < 6 → 不命中(而不是算出一个假 z) |
215
+ | `extreme` | 近 3 年新高/新低 或 分位 ≥0.95/≤0.05;数据不足 12 点 → 不命中 |
216
+ | `trend-break` | 斜率符号翻转且幅度 > 阈值 |
217
+ | `threshold-cross` | 政策利率变动、CPI 同比跨 3%、失业率跨 4.5%(阈值表集中定义、可配置) |
218
+ | `spread-signal` | 利差由负转正(倒挂解除)、快速收敛(Δ > 2σ) |
219
+ | `divergence` | 非农向上 + 失业率不动(两者变化都 < 0.1)→ 命中;需要显式配置的指标对 |
220
+ | `stale-gap` | 按频率推断"应更新而未更新"(例:月频指标距离上次观测 > 40 天) |
221
+
222
+ ### T5.2 `core/insight/rank.js`
223
+
224
+ - 评分公式固定:`score = Σ(ruleScore × ruleWeight) × importanceFactor`,权重表在文件顶部常量。
225
+ - 排序稳定:同分按 `importance` 降序、再按 `latestAt` 降序。
226
+ - 断言一个**黄金用例**:给定 6 个手工构造的指标(含非农超预期、利差收敛、普通指标),
227
+ 输出的 top-3 顺序与 `test/fixtures/insight-golden.json` 完全一致(黄金文件回归)。
228
+
229
+ ### T5.3 `core/insight/digest.js`
230
+
231
+ - 40 指标 → 字符数 ≤ 8000;含全部 `importance=5`;含省略说明。
232
+ - 单个指标超过 12 点时降采样到 12 点,且**保留极值点**。
233
+ - 数值格式遵守 `decimals`。
234
+
235
+ **验收 M5**:规则/排序/digest 全绿;黄金用例可作为回归基线。
236
+
237
+ ---
238
+
239
+ ## M6 · 应用层用例(1.5 天)
240
+
241
+ **目标**:把 core + 端口拼成真正的用例,全部用**内存端口**测。
242
+
243
+ ### T6.1 `app/refresh.js`
244
+
245
+ - TTL:日频 15min 内命中缓存不取数(断言假 fetch 调用次数 = 0)。
246
+ - 并发去重:同一 key 并发 5 次 → 上游调用 1 次。
247
+ - 失败降级:有快照 → `stale` 且数值 = 快照;无快照 → `error`。
248
+ - 重试:`network` 重试 1 次;`unsupported` 不重试(断言调用次数)。
249
+ - 部分失败不影响其他(`allSettled` 语义)。
250
+
251
+ ### T6.2 `app/overview.js`
252
+
253
+ - 返回 `Metric[]`(按 `group` 分组)与 `Noteworthy[]`(top N)。
254
+ - 每个 `Metric` 有 `sourceRef.url`(断言非空且 `https://`)。
255
+ - 缺失/陈旧项也有卡片(状态正确),不静默消失。
256
+
257
+ ### T6.3 `app/series-view.js`
258
+
259
+ - 统计字段齐全;`compareWith` 时两条序列按日期内连接;重叠不足 → 明确错误码。
260
+ - 范围过滤正确;`transform` 生效(`yoy`/`diff`)。
261
+
262
+ ### T6.4 `app/watchlist.js`
263
+
264
+ - `add` 幂等;`remove` 不存在 id → 返回 `notFound` 而不是抛错;`update` 局部 patch。
265
+ - 持久化适配器契约测试(内存版与文件版**跑同一套测试** → 契约测试工厂)。
266
+
267
+ **验收 M6**:`test/app/**` 全绿;用例层零 DSH 导入。
268
+
269
+ ---
270
+
271
+ ## M7 · 宿主半装配(1.5 天)
272
+
273
+ **目标**:插件真的能在 DSH 里跑起来,并在浏览器半可用之前先能用 curl 验证。
274
+
275
+ ### T7.1 `host/index.js`
276
+
277
+ - 测试 `test/host/plugin-shape.test.js`:导出 `name`/`inject`/`apply`;`apply` 无副作用地注册;
278
+ dispose 后所有注册被撤销(用假 ctx 记录 disposer 调用)。**参照 `04` 的插件开发纪律:
279
+ 每个贡献都要能被卸载。**
280
+ - 缺服务时不崩:`ctx.get('webServer') === undefined` → 只注册工具,不注册路由(并打日志)。
281
+
282
+ ### T7.2 `host/http/routes.js`
283
+
284
+ - 路由处理是**薄层**:测试直接调用 handler 函数(传假 req/res 或返回对象的形式,按
285
+ `cordis_inspect` 的 `webServer` 真实签名定),断言:
286
+ - 参数校验(未知 `range` → 400 + 结构化错误);
287
+ - 用例抛错 → 500 + `{error:{kind,...}}`,**不泄漏堆栈**;
288
+ - 成功 → JSON 形状符合契约。
289
+
290
+ ### T7.3 `host/infra/*-repo.js`
291
+
292
+ - 文件仓储跑与内存仓储**相同的契约测试**(幂等、并发写不丢数据、损坏文件可恢复为默认值)。
293
+
294
+ ### T7.4 `host/tools/register.js`
295
+
296
+ - 用假 `tools` 服务断言:注册了全部 8 个工具、名字与 schema 与 `06` §5 一致、
297
+ 卸载后全部移除;`data_watchlist` 的 add 幂等;`data_refresh` 节流。
298
+
299
+ **验收 M7**:`node --test test/host` 全绿。**此时应该能用 `curl` 验证**(M10 安装后):
300
+ `/api/show-me-data/health` 返回各源状态。
301
+
302
+ ---
303
+
304
+ ## M8 · 浏览器半(2 天)
305
+
306
+ **目标**:面板可用、可看、可溯源。
307
+
308
+ ### T8.1 构建脚本
309
+
310
+ - `test/host/build-client.test.js`:产物含且仅含一个 `__ModuleLoader__.load`;`id` 正确;
311
+ 无残留 `import`/`export` 关键字;能被 `new Function()` 解析;
312
+ 源码含 JSX 时**构建失败并报可读错误**(我们不允许 JSX)。
313
+
314
+ ### T8.2 纯函数与组件形状(走 client shim)
315
+
316
+ - `format.js`:数值(% / pp / 万人 / 点数)、日期、变化符号、颜色语义(**按 `polarity` 而非正负**)。
317
+ - 组件树冒烟:用 `react-dom/server` 不存在…… 因此**用假 React**(`test/helpers/react-stub.js`
318
+ 提供最小 `createElement`),断言渲染调用树:给定 `Metric` 卡片 props,断言包含标签、
319
+ 数值、单位、来源徽章文本。**这不是视觉测试**,是"渲染不炸 + 关键字段都上屏"的结构测试。
320
+ - 缺数据显示:`status='error'` 的 Metric → 断言渲染出"数据源暂不可用"与重试回调。
321
+
322
+ ### T8.3 手动验收(在 GUI 里)
323
+
324
+ 按 `01` 文档 §3 的 15 条交互清单逐条打勾,并把结果记入 `docs/13-acceptance.md`。
325
+
326
+ **验收 M8**:构建测试 + 组件形状测试全绿;GUI 里手动过 15 条。
327
+
328
+ ---
329
+
330
+ ## M9 · AI 层(1.5 天)
331
+
332
+ **目标**:`06` 文档的 7 条验收标准全部满足。
333
+
334
+ ### T9.1 提示词与 digest(纯函数)
335
+
336
+ - 每个提示词函数:断言包含传入的全部关键统计量、**不包含**未传入的数字、长度在预算内。
337
+ - 恶意用例:传入空指标数组 → 提示词明确写"无数据",函数不抛错。
338
+
339
+ ### T9.2 校验器 `app/ai-validate.js`(重点)
340
+
341
+ - 构造 6 类假输出:完美、引用错 ID、引用错日期、数值不符、含未溯源数字、含预测性措辞 →
342
+ 分别断言 `violations` 与最终处置(通过 / 重试一次 / 降级)。
343
+ - **必须有一条测试**:给一个"故意编造 seriesRef"的假 LLM → `propose` 必须落到 `unsupported`。
344
+
345
+ ### T9.3 三个 Gateway 适配器
346
+
347
+ - `DeterministicGateway`:给定 digest → 输出可读文本 + `usedPoints` 完整;确定性强(同输入同输出)。
348
+ - `DshLlmGateway`:用假 `ctx.llm`(按 `06` 的 `StreamChunk` 协议发 chunk)断言
349
+ 映射为 `AiChunk`、`done.result.usedPoints` 非空、超时能中断。
350
+ - 装配逻辑:无 `llm` 服务 → 自动选 `DeterministicGateway`(断言 `describe().mode`)。
351
+
352
+ ### T9.4 SSE 端点
353
+
354
+ - 断言响应头、事件格式、客户端断连时不泄漏(dispose 被调用)。
355
+
356
+ **验收 M9**:`06` §7 的 7 条验收标准有对应测试或人工记录。
357
+
358
+ ---
359
+
360
+ ## M10 · 安装、联调与打磨(1 天)
361
+
362
+ 1. 按 `09` 文档:装 pnpm → `dsh plugin --profile web add <path>` → 往
363
+ `/data/profiles/web/cordis.patch.yml` 追加 `insert` 行(**这一步需要一次性提权**)。
364
+ 2. 重启 Web profile 进程(用**受管后台作业**启动,并记录确切 URL),刷新页面确认面板出现。
365
+ 3. 逐条过 `01` §3 的 15 条交互 + `05` §7 的性能预算。
366
+ 4. 冒烟脚本 `scripts/smoke.mjs`:逐个打 `/api/show-me-data/*` 路由,断言 200 与结构;
367
+ 打一遍真网源可用性(`RUN_NET=1`)。
368
+ 5. 写 `docs/13-acceptance.md`(结果记录)+ 更新 `README.md`(真实使用说明)。
369
+ 6. 更新 `docs/12-runtime-verified.md`:安装过程中发现的偏差与最终形态。
370
+
371
+ **M10 完成定义(DoD)**:见 `10-kickoff-prompt.md` §DoD。
372
+
373
+ ---
374
+
375
+ ## 11. 「可扩展」的验收方式(不是口号,是可测的)
376
+
377
+ M10 结束后做一次**扩展演练**,两个动作都必须在 **30 分钟内**完成且**不改 core 逻辑文件**:
378
+
379
+ | 演练 | 允许的改动 | 期望 |
380
+ | --- | --- | --- |
381
+ | 加指标「美国 30 年期房贷利率」 | `core/indicators/catalog.js` 加 1 条 + 录制 1 个 fixture | 面板自动出现该卡片、有 sparkline、可溯源、可被 AI 引用、可被规则命中 |
382
+ | 加数据源「某新公开 API」 | `src/sources/<new>.js` + `registry.js` 1 行 + 1 个 fixture | 契约测试自动覆盖它;目录里引用它的指标立即可用 |
383
+
384
+ 若任一步需要改动 `core/stats`、`core/insight`、`app/*`,说明**扩展点没做对**,
385
+ 应当重构后再验收(这是本方案的核心质量目标)。
386
+
387
+ ---
388
+
389
+ ## 12. 里程碑依赖与总量
390
+
391
+ ```
392
+ M0 ─▶ M1 ─▶ M2 ─┐
393
+ ├─▶ M6 ─▶ M7 ─▶ M8 ─▶ M10
394
+ M3 ─▶ M4 ─▶ M5 ─┘ M9 ─┘
395
+ ```
396
+
397
+ - M3/M4/M5 可在 M1/M2 之后并行(不同人/并行子代理),但**M4 依赖 M3 的 fixture**。
398
+ - 预估总工作量:**约 13 个「人·天」**;用 subagent 并行 M3/M5/M8 可压到约 7–8 个日历天。
399
+ - 每完成一个里程碑更新 `docs/14-progress.md`(勾选 + 偏差记录)。
@@ -0,0 +1,133 @@
1
+ # 08 · 测试计划与门禁
2
+
3
+ 工具:**`node:test` + `node:assert/strict`**(Node 24 内置,零依赖)。
4
+ 没有 jest/vitest/tsdown,理由是当前 profile 里没有这些工具,而 npm 依赖每多一个就多一个故障点。
5
+
6
+ ---
7
+
8
+ ## 1. 测试分层
9
+
10
+ | 层 | 目录 | 是否打网 | 速度目标 | 覆盖门槛 |
11
+ | --- | --- | --- | --- | --- |
12
+ | 纯逻辑 | `test/core/` | 否 | < 1s | 行 ≥ 95% |
13
+ | 端口契约 | `test/contract/` | 否 | < 2s | 每个适配器 100% 覆盖契约断言 |
14
+ | 适配器 | `test/sources/` | 否(fixtures) | < 2s | 行 ≥ 85% |
15
+ | 用例 | `test/app/` | 否(内存端口) | < 2s | 行 ≥ 90% |
16
+ | 装配 | `test/host/` | 否(假服务) | < 2s | 关键路径全覆盖 |
17
+ | 真网冒烟 | `test/net/` | **是**(`RUN_NET=1`) | < 60s | 不计入覆盖率 |
18
+
19
+ ```bash
20
+ node --test test/ # 门禁:以上前五层
21
+ RUN_NET=1 node --test test/net/ # 人工/定时跑,不进 CI 门禁
22
+ ```
23
+
24
+ ---
25
+
26
+ ## 2. 必测的"数据面板专属"缺陷类型
27
+
28
+ 这些是同类产品最常见的线上问题,**每一条都要有对应测试**:
29
+
30
+ | # | 缺陷类型 | 测试方式 |
31
+ | --- | --- | --- |
32
+ | 1 | 缺失值被当成 0(把 `"."` 算成 0,导致同比爆表) | fixture 里植入 `.`,断言产出无该点、统计值合理 |
33
+ | 2 | 单位错位(0.034 当成 3.4%) | 断言 `yoy` 输出量级(`0.5 < \|v\| < 100`)且 `unit='%'` |
34
+ | 3 | 倒序数据导致"最新值"取到最旧 | 断言 eastmoney-macro 产出升序 |
35
+ | 4 | 同日重复导致同比错位 | `sortDedupe` 保留最后一条的用例 |
36
+ | 5 | 全等序列导致 SVG 除零 → `NaN` 路径 | `min===max` 用例断言无 `NaN` |
37
+ | 6 | 样本不足却报"2σ 异常" | n<6 → `zScore` 为 `undefined` 的用例 |
38
+ | 7 | 时区导致"今天"错一天 | 所有日期走注入的 `Clock`;测试用固定时钟,断言 `resolveRange('1M')` |
39
+ | 8 | 陈旧数据当成新鲜 | 按 `freq` 的陈旧阈值用例(含 annual 400 天) |
40
+ | 9 | 单个源失败拖垮整个面板 | `allSettled` 降级用例 |
41
+ | 10 | AI 编造数字/指标 | `ai-validate` 的 6 类违规用例 |
42
+ | 11 | 溯源链接指向错误系列 | 每个指标断言 `sourceRef.url` 含其 `seriesRef` |
43
+ | 12 | 卸载后残留副作用(定时器、路由、工具) | 假 ctx 断言所有 disposer 被调用 |
44
+ | 13 | 并发重复取数(打爆上游) | in-flight 去重计数用例 |
45
+ | 14 | 缓存永不失效 / 永不过期 | TTL 边界用例(过期前 1ms / 过期后 1ms) |
46
+
47
+ ---
48
+
49
+ ## 3. 静态断言(用测试守住分层)
50
+
51
+ `test/core/layering.test.js` 读取 `src/core/**` 与 `src/app/**` 的源码文本,断言:
52
+
53
+ - 不出现 `fetch(`、`require(`、`from 'node:`、`from "node:`、`process.`、`Date.now(`、`new Date(`;
54
+ - 不出现 `@deepseek-ai/`(core/app 不得依赖 DSH);
55
+ - `src/sources/**` 不出现 `node:fs`(适配器只做 HTTP + 解析)。
56
+
57
+ `test/host/client-purity.test.js` 断言 `src/client/**`:
58
+
59
+ - 不使用 JSX 语法(`<Tag` 形式);不出现 `import`/`export`;
60
+ - 只能 `require` 白名单模块(`react`、`react/jsx-runtime`、DSH 客户端包);
61
+ - 不直连外部数据源域名(必须经 `/api/show-me-data/*`)。
62
+
63
+ 这类测试很便宜,但能防止架构在后期腐烂。
64
+
65
+ ---
66
+
67
+ ## 4. 覆盖率
68
+
69
+ 不需要额外依赖:用 Node 内置覆盖率。
70
+
71
+ ```bash
72
+ node --test --experimental-test-coverage test/core test/app
73
+ ```
74
+
75
+ 门禁(人工核对,写入 `docs/14-progress.md`):
76
+
77
+ - `src/core/**` ≥ 95%
78
+ - `src/app/**` ≥ 90%
79
+ - `src/sources/**` ≥ 85%
80
+ - `src/host/**` ≥ 70%(装配层,靠集成冒烟补足)
81
+
82
+ ---
83
+
84
+ ## 5. Fixtures 策略
85
+
86
+ | 规则 | 说明 |
87
+ | --- | --- |
88
+ | **录制而非手写** | `scripts/record-fixtures.mjs` 打真网存**原始响应体**,保证 fixture 与真实上游同构 |
89
+ | **必须含异常样本** | 每个适配器至少 2 个异常 fixture(缺失值、业务失败、空响应、HTML 错误页) |
90
+ | **index.json 记录来源** | `{ url, fetchedAt, httpStatus, note }`,便于日后判断"上游变了还是我们解析错了" |
91
+ | **fixture 变更要单独提交** | 提交信息说明原因(上游改版 / 补异常样本),避免"顺手更新把 bug 更新没了" |
92
+ | **体积控制** | 单文件 ≤ 400KB;超大响应在录制时按 `lmt`/`cosd` 收窄 |
93
+ | **不含敏感信息** | 全部为公开数据;脚本断言响应中无 `api_key`/`token`(防止以后接入带 key 的源时泄漏) |
94
+
95
+ ---
96
+
97
+ ## 6. 真网冒烟(`test/net/`)
98
+
99
+ `test/net/sources-live.test.js`(`RUN_NET=1` 才执行):
100
+
101
+ - 对注册表里**每个**适配器:至少 1 个 `seriesRef` 返回 ≥ 5 个点、升序、无 `NaN`。
102
+ - 对目录里 `importance>=4` 的指标:**全部**能取到数(这是"首页不掉卡"的保障)。
103
+ - 断言每个源耗时 < 20s,失败时输出**具体是哪个 seriesRef**。
104
+ - 建议作为「每周手动跑一次」的巡检,并配 `data_health` 工具在面板内可视。
105
+
106
+ ---
107
+
108
+ ## 7. 回归黄金文件(golden)
109
+
110
+ | 文件 | 内容 | 用途 |
111
+ | --- | --- | --- |
112
+ | `test/fixtures/insight-golden.json` | 6 个手工构造指标的规则打分与 top-3 顺序 | 规则权重调整时必须**显式**更新此文件(评审可见) |
113
+ | `test/fixtures/digest-golden.txt` | 40 指标 digest 的完整输出 | 防止 token 预算被悄悄突破 |
114
+ | `test/fixtures/chart-golden/*.svg` | 4 张图(线/柱/双轴/sparkline)的路径字符串 | 图表几何回归;视觉变化必须显式更新 |
115
+
116
+ 黄金文件的纪律:**变化即信号**,不允许在无关提交里被"顺手更新"。
117
+
118
+ ---
119
+
120
+ ## 8. 完成定义(DoD,可机械核对)
121
+
122
+ | # | 条件 | 核对方式 |
123
+ | --- | --- | --- |
124
+ | 1 | `node --test test/` 全绿 | 命令输出 |
125
+ | 2 | 覆盖率门槛达标 | `--experimental-test-coverage` 输出 |
126
+ | 3 | 分层静态断言通过 | `test/core/layering.test.js` |
127
+ | 4 | 扩展演练两个动作在 30 分钟内完成且不改 core | 人工计时 + `git diff --stat` 证明只改了目录数据/新增适配器 |
128
+ | 5 | GUI 里 15 条交互清单全过 | `docs/13-acceptance.md` |
129
+ | 6 | 关掉 LLM 后三个 AI 入口仍可用 | 手动 + `describe().mode` 断言 |
130
+ | 7 | AI 引用校验能拦下伪造输出 | `test/app/ai-validate.test.js` |
131
+ | 8 | 卸载插件后无残留(路由/工具/定时器/样式) | 假 ctx disposer 断言 + GUI 观察 |
132
+ | 9 | 面板在"全部数据源不可用"时给出降级态而非空白 | 用假 fetch 全失败跑一次 |
133
+ | 10 | `docs/12–14` 三份记录文件已填写 | 人工核对 |