dsh-data-insight 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.
- package/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/PUBLISHING.md +79 -0
- package/README.md +73 -0
- package/cordis.patch.yml +11 -0
- package/docs/chart-spec.md +67 -0
- package/docs/report-template.md +72 -0
- package/examples/sample-report.md +102 -0
- package/examples/sample-sales.csv +32 -0
- package/package.json +49 -0
- package/plugin/index.js +41 -0
- package/scripts/csv-profile.mjs +263 -0
- package/scripts/setup-duckdb.ps1 +83 -0
- package/skills/data-insight-runbook/SKILL.md +153 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/) 约定。
|
|
4
|
+
|
|
5
|
+
## [0.1.0] - 2026-08-16
|
|
6
|
+
|
|
7
|
+
### 新增
|
|
8
|
+
- 主技能 `data-insight-runbook`:五阶段数据洞察流水线(受理 → 探查 → 指标 → 图表 → 报告),每阶段硬门槛。
|
|
9
|
+
- 四种数据源接入:CSV、粘贴表格、SQL 查询结果、DuckDB 直连(可选,强制只读)。
|
|
10
|
+
- 三通道图表规范(`docs/chart-spec.md`):Markdown 表格 + Mermaid + ASCII。
|
|
11
|
+
- 报告模板与严谨性检查清单(`docs/report-template.md`)。
|
|
12
|
+
- 零依赖 CSV 探查脚本 `scripts/csv-profile.mjs`。
|
|
13
|
+
- 可选 DuckDB CLI 安装脚本 `scripts/setup-duckdb.ps1`(winget / GitHub 直链,UTF-8 BOM)。
|
|
14
|
+
- 样例 CSV 与样例报告(`examples/`)。
|
|
15
|
+
- 发布与分发指南 `PUBLISHING.md`。
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 duyanta123
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/PUBLISHING.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# 发布与分发指南(PUBLISHING)
|
|
2
|
+
|
|
3
|
+
本文档记录 `dsh-data-insight` 从源码到分发的完整步骤,供维护者在有网终端执行。
|
|
4
|
+
|
|
5
|
+
## 当前状态(构建侧已就绪)
|
|
6
|
+
|
|
7
|
+
- 包结构 14 个文件已建成;`node --check`、JSON、插件 API、SKILL.md frontmatter 均已校验通过。
|
|
8
|
+
- `csv-profile.mjs` 已在 `examples/sample-sales.csv` 上跑通。
|
|
9
|
+
- `setup-duckdb.ps1` 语法校验通过(UTF-8 BOM),版本探测数据源 `duckdb.org/data/latest_stable_version.txt` 实测返回 `1.5.5`。
|
|
10
|
+
- 已 `git init` + 初始 commit(本地 `main` 分支)。
|
|
11
|
+
- npm 包名 `dsh-data-insight` 已确认未被占用(`npm view` 返回 404)。
|
|
12
|
+
|
|
13
|
+
## 前置条件
|
|
14
|
+
|
|
15
|
+
- 一个有网、能访问 github.com 与 registry.npmjs.org 的终端。
|
|
16
|
+
- 已登录 npm:`npm login`(需要 npm 账号 + 2FA)。
|
|
17
|
+
- GitHub 账号(用于建仓库与 push)。
|
|
18
|
+
|
|
19
|
+
## 步骤 1:发布到 GitHub
|
|
20
|
+
|
|
21
|
+
```powershell
|
|
22
|
+
# 1) 浏览器打开 https://github.com/new,仓库名 dsh-data-insight,公开,
|
|
23
|
+
# 不要勾选 "Initialize with README / .gitignore / license"(本地已有)。
|
|
24
|
+
cd D:\Agent预设\UI\dsh-data-insight
|
|
25
|
+
git remote add origin https://github.com/<你的用户名>/dsh-data-insight.git
|
|
26
|
+
git push -u origin main
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
推送后打一个版本 tag(可选但推荐,供 `dsh plugin add github:...#vX.Y.Z` 引用):
|
|
30
|
+
|
|
31
|
+
```powershell
|
|
32
|
+
git tag v0.1.0
|
|
33
|
+
git push origin v0.1.0
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 步骤 2:发布到 npm
|
|
37
|
+
|
|
38
|
+
```powershell
|
|
39
|
+
cd D:\Agent预设\UI\dsh-data-insight
|
|
40
|
+
npm view dsh-data-insight version # 再次确认包名可用(应 404)
|
|
41
|
+
npm login # 首次需要;已登录可跳过
|
|
42
|
+
npm publish --access public
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 步骤 3:安装到 profile(二选一)
|
|
46
|
+
|
|
47
|
+
发布后,任选一种分发形态安装:
|
|
48
|
+
|
|
49
|
+
```powershell
|
|
50
|
+
# npm 形态
|
|
51
|
+
dsh plugin --profile web add dsh-data-insight
|
|
52
|
+
|
|
53
|
+
# GitHub 形态(与 dsh-preset-scaffold 一致)
|
|
54
|
+
dsh plugin --profile web add github:<你的用户名>/dsh-data-insight#v0.1.0
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
> 本地开发期可用 `file:` 链接(无需发布):
|
|
58
|
+
> 在 profile 的 `package.json` 里加 `"dsh-data-insight": "file:D:/Agent预设/UI/dsh-data-insight"`,
|
|
59
|
+
> 并在 `dsh.profile.bundles` 数组加 `"dsh-data-insight"`,然后 `pnpm install`。
|
|
60
|
+
|
|
61
|
+
## 步骤 4:验证
|
|
62
|
+
|
|
63
|
+
1. 重启 profile(`dsh web` 重开),技能列表应出现 `data-insight-runbook`。
|
|
64
|
+
2. 说「分析 examples/sample-sales.csv 出报告」,确认五阶段执行并产出报告。
|
|
65
|
+
3. 可选:`scripts/setup-duckdb.ps1` 装 DuckDB 后,验证直连只读查询。
|
|
66
|
+
|
|
67
|
+
## 常见问题
|
|
68
|
+
|
|
69
|
+
- **`npm publish` 报 403/404**:包名被占或未登录;用 `npm whoami` 检查登录态。
|
|
70
|
+
- **`dsh plugin add` 报找不到包**:确认包已发布且 profile 的 `dsh.profile.bundles` 含包名。
|
|
71
|
+
- **技能没出现**:重启 profile 才加载 bundle patch;确认 `cordis.patch.yml` 随包发布(`files` 字段已包含)。
|
|
72
|
+
- **DuckDB 直连被拒**:runbook 强制 `-readonly`;连接串走环境变量 `DATA_INSIGHT_DB_URL`,不要写进命令/报告。
|
|
73
|
+
|
|
74
|
+
## 本地开发循环(改技能内容后)
|
|
75
|
+
|
|
76
|
+
1. 改 `skills/`、`docs/`、`scripts/` 下的文件。
|
|
77
|
+
2. 本地 `file:` 链接形态下,改 `skills/` 无需重装(FileSystemSkillProvider 会 watch 技能根)。
|
|
78
|
+
3. 改 `plugin/index.js` / `cordis.patch.yml` / `package.json` 后需 `pnpm install` 并重启 profile。
|
|
79
|
+
4. 提交前:`node --check scripts/csv-profile.mjs`、PowerShell 语法校验、`git status` 确认。
|
package/README.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# dsh-data-insight
|
|
2
|
+
|
|
3
|
+
DSH(DeepSeek Harness)**数据洞察技能插件**:把原始数据变成「业务结论 + 指标数据 + 图表」的结构化 Markdown 分析报告。
|
|
4
|
+
|
|
5
|
+
纯指令型技能插件,零依赖、零构建。计算由宿主已提供的文件 / Shell 工具驱动的 LLM 完成,包内附带一个零依赖的 CSV 探查脚本与完整图表 / 报告规范。
|
|
6
|
+
|
|
7
|
+
## 功能
|
|
8
|
+
|
|
9
|
+
- **四种数据源**:CSV 文件、粘贴表格文本、SQL 查询结果、DuckDB 直连数据库(可选)。
|
|
10
|
+
- **五阶段流水线**(每阶段带硬门槛):输入受理 → 数据探查 → 指标计算 → 图表呈现 → 报告产出。
|
|
11
|
+
- **三类指标**:汇总统计、同环比(基准期规则写死)、TopN、异常值(Z-score / IQR)。
|
|
12
|
+
- **三通道图表**(全零依赖):Markdown 表格 + 数字 / Mermaid / ASCII 条形图。
|
|
13
|
+
- **严谨性保障**:结论必有数字支撑、事实与推断分离、口径可复现、不编造数据。
|
|
14
|
+
|
|
15
|
+
## 安装
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
dsh plugin --profile web add dsh-data-insight
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
或手动两步(在目标 profile 目录下):
|
|
22
|
+
|
|
23
|
+
1. `package.json` 的 `dependencies` 加 `"dsh-data-insight": "^0.1.0"`;
|
|
24
|
+
2. `dsh.profile.bundles` 数组加 `"dsh-data-insight"`。
|
|
25
|
+
|
|
26
|
+
重启 profile 后,技能 `data-insight-runbook` 出现在技能列表即可用。
|
|
27
|
+
|
|
28
|
+
## 使用
|
|
29
|
+
|
|
30
|
+
在会话中说「分析这份 CSV 出报告」「看看这个数据」「帮我算一下指标」并附上数据源(文件路径 / 粘贴表格 / DuckDB 连接),模型会加载 `data-insight-runbook` 并按五阶段执行。产物是一份 Markdown 报告,落盘到工作区。
|
|
31
|
+
|
|
32
|
+
### 快速示例
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
node scripts/csv-profile.mjs examples/sample-sales.csv
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
会输出 `examples/sample-sales.csv` 的探查报告(schema / 缺失 / 分布 / 异常),对应报告样例见 `examples/sample-report.md`。
|
|
39
|
+
|
|
40
|
+
## 目录结构
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
dsh-data-insight/
|
|
44
|
+
├── plugin/index.js # 插件入口:注册 skills/ 为技能根
|
|
45
|
+
├── cordis.patch.yml # bundle patch(dsh plugin add 时注入)
|
|
46
|
+
├── skills/data-insight-runbook/SKILL.md # 主技能:五阶段 runbook
|
|
47
|
+
├── docs/chart-spec.md # 三通道图表规范与示例
|
|
48
|
+
├── docs/report-template.md # 报告骨架 + 严谨性检查清单
|
|
49
|
+
├── scripts/csv-profile.mjs # 零依赖 CSV 探查脚本
|
|
50
|
+
└── examples/ # 样例 CSV + 样例报告
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## DuckDB 直连(可选)
|
|
54
|
+
|
|
55
|
+
默认零依赖;如需直连数据库,安装 [DuckDB](https://duckdb.org/) 单文件 CLI(加入 PATH):
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
# 连接串走环境变量,强制只读
|
|
59
|
+
duckdb -readonly -csv -c "SELECT * FROM read_csv_auto('data.csv') LIMIT 100"
|
|
60
|
+
duckdb -readonly -csv -c "SELECT ... LIMIT 5000" "$env:DATA_INSIGHT_DB_URL"
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
安全红线:一律 `-readonly`;连接串走环境变量 `DATA_INSIGHT_DB_URL`;查询默认 `LIMIT 5000`。
|
|
64
|
+
|
|
65
|
+
## 参考文档
|
|
66
|
+
|
|
67
|
+
- `skills/data-insight-runbook/SKILL.md` — 完整流程与门槛
|
|
68
|
+
- `docs/chart-spec.md`、`docs/report-template.md` — 图表与报告规范
|
|
69
|
+
- `examples/` — 输入 / 输出样例
|
|
70
|
+
|
|
71
|
+
## 开源协议
|
|
72
|
+
|
|
73
|
+
[MIT](LICENSE)
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# dsh-data-insight — DSH bundle patch.
|
|
2
|
+
#
|
|
3
|
+
# 当某个 profile 在 dsh.profile.bundles 中列出本包时(dsh plugin add 即完成此事),
|
|
4
|
+
# dsh 启动时把本补丁打入配置树:插入本包插件行,加载 package.json 的 main
|
|
5
|
+
# (plugin/index.js),将自带 skills/ 注册为技能根。
|
|
6
|
+
#
|
|
7
|
+
# 人设与工具不在此处重复声明——宿主 profile(web/standard)已提供文件、shell、
|
|
8
|
+
# 计划模式、web 搜索、子代理等 runbook 引用的全部工具。
|
|
9
|
+
- insert:
|
|
10
|
+
- id: dsh-data-insight
|
|
11
|
+
name: dsh-data-insight
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# 三通道图表规范(chart-spec)
|
|
2
|
+
|
|
3
|
+
数据洞察报告的图表采用三通道策略,全部零依赖。**核心原则:图表只是呈现,数字才是根基——任何图表都必须能在正文/附录表格中找到对应数据点。**
|
|
4
|
+
|
|
5
|
+
## 通道选择
|
|
6
|
+
|
|
7
|
+
| 通道 | 用途 | 渲染环境 | 优先级 |
|
|
8
|
+
|---|---|---|---|
|
|
9
|
+
| 主通道:Markdown 表格 + 关键数字 | 指标、TopN、占比、对比 | 任何查看器 | 必须 |
|
|
10
|
+
| 次通道:Mermaid | 趋势、占比、流程的可视化增强 | GitHub / VS Code 等支持 Mermaid 的渲染器 | 可选 |
|
|
11
|
+
| 兜底:ASCII 条形图 | 量级对比的纯文本可视化 | 任何查看器(含 DSH Web GUI) | 可选 |
|
|
12
|
+
|
|
13
|
+
> ⚠️ DSH Web GUI 的 Markdown 渲染**不支持 Mermaid**(会显示为代码块)。因此主通道表格必须能独立撑起报告;Mermaid 仅作为「贴到 GitHub/文档平台」时的增强,正文要标注「需 Mermaid 渲染器」。
|
|
14
|
+
|
|
15
|
+
## 主通道:Markdown 表格
|
|
16
|
+
|
|
17
|
+
用 GFM 表格,数字右对齐,统一千分位与小数位(默认保留 2 位小数,金额类可保留 0 位并注明单位)。
|
|
18
|
+
|
|
19
|
+
```markdown
|
|
20
|
+
| 渠道 | 销售额(万元) | 环比 | 占比 |
|
|
21
|
+
|---:|---:|---:|---:|
|
|
22
|
+
| 线上直营 | 1,234.56 | +12.3% | 45.2% |
|
|
23
|
+
| 线下门店 | 987.65 | -3.1% | 36.1% |
|
|
24
|
+
| 分销 | 512.30 | +1.5% | 18.7% |
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## 次通道:Mermaid
|
|
28
|
+
|
|
29
|
+
### 趋势(xychart-beta)
|
|
30
|
+
```mermaid
|
|
31
|
+
xychart-beta
|
|
32
|
+
title "月度销售额趋势(万元)"
|
|
33
|
+
x-axis ["1月","2月","3月","4月","5月","6月"]
|
|
34
|
+
y-axis "销售额" 0 --> 1500
|
|
35
|
+
line [980, 1120, 1050, 1280, 1340, 1420]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 占比(pie)
|
|
39
|
+
```mermaid
|
|
40
|
+
pie title 渠道销售占比
|
|
41
|
+
"线上直营" : 45.2
|
|
42
|
+
"线下门店" : 36.1
|
|
43
|
+
"分销" : 18.7
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 注意
|
|
47
|
+
- 数据点必须与附录表格完全一致。
|
|
48
|
+
- Mermaid 语法错一个字符整段不渲染,生成后自查:标签无未转义引号、百分比为纯数字、`xychart-beta` 系列括号匹配。
|
|
49
|
+
|
|
50
|
+
## 兜底:ASCII 条形图
|
|
51
|
+
|
|
52
|
+
用 code block 画,纯文本处处可读。
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
渠道销售对比(万元)
|
|
56
|
+
线上直营 ████████████████████ 1234.56
|
|
57
|
+
线下门店 ████████████████ 987.65
|
|
58
|
+
分销 ████████ 512.30
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
条形宽度按「值 / 最大值 × 20」取整(最大 20 格),保证比例可读。
|
|
62
|
+
|
|
63
|
+
## 禁止事项
|
|
64
|
+
|
|
65
|
+
- ❌ 手写 SVG:模型手写 SVG 易错、体积大、难维护。
|
|
66
|
+
- ❌ 图表数字与表格不一致(这是最严重的可信度问题)。
|
|
67
|
+
- ❌ 用图片外链代替本地可复现的表格数据。
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# 报告模板与严谨性检查清单(report-template)
|
|
2
|
+
|
|
3
|
+
数据洞察报告的固定骨架。产出时严格按此结构,并在交付前过完下方的严谨性检查清单。
|
|
4
|
+
|
|
5
|
+
## 报告骨架
|
|
6
|
+
|
|
7
|
+
```markdown
|
|
8
|
+
# {主题}数据洞察报告
|
|
9
|
+
|
|
10
|
+
> 数据源:{文件路径 / 粘贴 / DuckDB 连接类型}
|
|
11
|
+
> 时间范围:{起止} 粒度:{日/周/月}
|
|
12
|
+
> 生成时间:{时间} 工具:dsh-data-insight v{版本}
|
|
13
|
+
|
|
14
|
+
## 一、核心结论
|
|
15
|
+
1. **{结论}**:{具体数字支撑}(见「三/四/五」节)
|
|
16
|
+
2. …
|
|
17
|
+
(3–5 条,每条带数字)
|
|
18
|
+
|
|
19
|
+
## 二、数据概况
|
|
20
|
+
- 行数 / 列数 / 编码 / 分隔符
|
|
21
|
+
- 缺失值概览(哪些列缺失、缺失率)
|
|
22
|
+
- 数据质量问题与处置(重复、单位、异常格式)
|
|
23
|
+
|
|
24
|
+
## 三、关键指标
|
|
25
|
+
| 指标 | 值 | 口径 |
|
|
26
|
+
|---|---|---|
|
|
27
|
+
| … | … | … |
|
|
28
|
+
(汇总统计:总数/均值/中位数/环比/同比…)
|
|
29
|
+
|
|
30
|
+
## 四、趋势与对比
|
|
31
|
+
- 趋势图(三通道,见 docs/chart-spec.md)
|
|
32
|
+
- 解读:涨跌与关键拐点,附数据表格
|
|
33
|
+
|
|
34
|
+
## 五、TopN 与异常
|
|
35
|
+
- TopN 表(N=5,并列标注)
|
|
36
|
+
- 异常值表(值 + 所属维度 + 判定依据)
|
|
37
|
+
|
|
38
|
+
## 六、风险与建议
|
|
39
|
+
- 【事实】…(纯数据陈述)
|
|
40
|
+
- 【推断】…(模型分析,标注依据与置信度)
|
|
41
|
+
- 建议(可执行、对应到具体数字)
|
|
42
|
+
|
|
43
|
+
## 附录 A:口径与可复现说明
|
|
44
|
+
- 数据源、过滤条件、缺失/重复处置
|
|
45
|
+
- 算法与参数(同环比基准期、TopN 取值、异常阈值 Z=3 / IQR)
|
|
46
|
+
- 核心指标计算公式
|
|
47
|
+
- 图表数据表(每个图的数据源)
|
|
48
|
+
|
|
49
|
+
## 附录 B:探查记录
|
|
50
|
+
- Schema(列名 + 推断类型)
|
|
51
|
+
- 值分布摘要
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 严谨性检查清单(交付前逐项过)
|
|
55
|
+
|
|
56
|
+
| # | 检查项 | 通过标准 |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| 1 | 结论有数字 | 每条核心结论引用至少一个具体数字,数字可在正文/附录查到 |
|
|
59
|
+
| 2 | 事实/推断分离 | 「风险与建议」用【事实】【推断】明确标注;推断给出依据 |
|
|
60
|
+
| 3 | 口径可复现 | 附录含过滤条件、聚合方式、计算公式、算法参数 |
|
|
61
|
+
| 4 | 图数一致 | 每个图表数据点都能在附录表格找到 |
|
|
62
|
+
| 5 | 不编造 | 无探查依据的数字不出现;缺失值处置如实记录 |
|
|
63
|
+
| 6 | 单位与小数位 | 全文单位统一(万元/元/人…),小数位一致 |
|
|
64
|
+
| 7 | 时间口径明确 | 起止日期、粒度、时区(如涉及时区)写明 |
|
|
65
|
+
| 8 | 脱敏 | 附录不含密码/密钥;连接串脱敏 |
|
|
66
|
+
|
|
67
|
+
任一条不过,回到对应阶段返工,禁止「应该没问题」式收尾。
|
|
68
|
+
|
|
69
|
+
## 精简版(用户要求「快速看一眼」时)
|
|
70
|
+
|
|
71
|
+
保留:核心结论 + 数据概况 + 关键指标表 + 一个图表(趋势或 TopN)。
|
|
72
|
+
豁免:完整趋势解读、附录 B。**不豁免**:检查项 1(结论有数字)、2(事实/推断分离)、8(脱敏)。
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# 电商销售数据洞察报告(示例)
|
|
2
|
+
|
|
3
|
+
> 数据源:`examples/sample-sales.csv`
|
|
4
|
+
> 时间范围:2025-01-01 ~ 2025-01-10 粒度:日
|
|
5
|
+
> 生成时间:示例 工具:dsh-data-insight v0.1.0
|
|
6
|
+
|
|
7
|
+
## 一、核心结论
|
|
8
|
+
|
|
9
|
+
1. **线上直营是绝对主力渠道**:销售额占比 **60.3%**,日均约 **2.58 万元**,是线下门店(约 0.99 万元)的 **2.6 倍**(见「四、趋势与对比」)。
|
|
10
|
+
2. **1 月 9 日出现销售尖峰**:当日销售额 **138,600 元**(其中线上直营华北单笔 120,000 元),为日均(42,767 元)的 **3.2 倍**,属显著异常值(Z-score > 3),需核实是否为大促/错单(见「五、TopN 与异常」)。
|
|
11
|
+
3. **数据存在少量质量问题**:`orders` 列缺失 3 行(缺失率 9.7%),并存在 1 条重复行(01-03 分销华南),已按「保留首条」去重处置(见「二、数据概况」)。
|
|
12
|
+
4. **剔除异常后整体趋势平稳上行**:1 月 5 日后销售额由 31,890 元温和爬升至 36,100 元(见「四」)。
|
|
13
|
+
|
|
14
|
+
## 二、数据概况
|
|
15
|
+
|
|
16
|
+
- 行数 31(含 1 条重复)→ 去重后 **30**;列数 **5**;编码 UTF-8;分隔符逗号。
|
|
17
|
+
- 缺失值:`orders` 缺失 3 行(9.7%,去重后 2 行),`revenue` 无缺失。
|
|
18
|
+
- 质量处置:1 条重复行按「保留首条」去重;`orders` 缺失行在订单类聚合中排除,并如实记录。
|
|
19
|
+
|
|
20
|
+
## 三、关键指标
|
|
21
|
+
|
|
22
|
+
| 指标 | 值 | 口径 |
|
|
23
|
+
|---|---:|---|
|
|
24
|
+
| 总销售额 | 427,671.25 元 | 去重后 30 行 revenue 求和 |
|
|
25
|
+
| 总订单 | 2,636 单 | 去重后 orders 求和(排除 2 个缺失) |
|
|
26
|
+
| 日均销售额 | 42,767.13 元 | 总销售额 / 10 天 |
|
|
27
|
+
| 客单价 | 162.24 元 | 总销售额 / 总订单 |
|
|
28
|
+
| 环比(第 10 日 vs 第 9 日) | -73.9% | 受 1/9 异常尖峰回落影响 |
|
|
29
|
+
|
|
30
|
+
## 四、趋势与对比
|
|
31
|
+
|
|
32
|
+
### 渠道销售额占比
|
|
33
|
+
| 渠道 | 销售额(元) | 占比 |
|
|
34
|
+
|---:|---:|---:|
|
|
35
|
+
| 线上直营 | 257,801.25 | 60.3% |
|
|
36
|
+
| 线下门店 | 99,400.00 | 23.2% |
|
|
37
|
+
| 分销 | 70,470.00 | 16.5% |
|
|
38
|
+
|
|
39
|
+
```mermaid
|
|
40
|
+
pie title 渠道销售占比
|
|
41
|
+
"线上直营" : 60.3
|
|
42
|
+
"线下门店" : 23.2
|
|
43
|
+
"分销" : 16.5
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### 渠道日均销售额对比(ASCII)
|
|
47
|
+
```
|
|
48
|
+
线上直营 ████████████████████ 25780
|
|
49
|
+
线下门店 ████████ 9940
|
|
50
|
+
分销 █████ 7047
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 日销售额趋势(Mermaid,需 Mermaid 渲染器)
|
|
54
|
+
```mermaid
|
|
55
|
+
xychart-beta
|
|
56
|
+
title "日销售额趋势(元)"
|
|
57
|
+
x-axis ["01","02","03","04","05","06","07","08","09","10"]
|
|
58
|
+
y-axis "销售额" 0 --> 140000
|
|
59
|
+
line [30641, 30040, 30241, 31480, 31890, 32480, 33800, 32400, 138600, 36100]
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## 五、TopN 与异常
|
|
63
|
+
|
|
64
|
+
### Top 5 销售额日
|
|
65
|
+
| 日期 | 销售额(元) |
|
|
66
|
+
|---|---:|
|
|
67
|
+
| 2025-01-09 | 138,600.00 |
|
|
68
|
+
| 2025-01-10 | 36,100.00 |
|
|
69
|
+
| 2025-01-07 | 33,800.00 |
|
|
70
|
+
| 2025-01-06 | 32,480.00 |
|
|
71
|
+
| 2025-01-08 | 32,400.00 |
|
|
72
|
+
|
|
73
|
+
### 异常值
|
|
74
|
+
| 值 | 所属 | 判定 |
|
|
75
|
+
|---|---:|---|
|
|
76
|
+
| 120,000.00(revenue) | 2025-01-09 线上直营华北 | Z-score > 3,需核实大促/错单 |
|
|
77
|
+
| 900(orders) | 2025-01-09 线上直营华北 | Z-score > 3,同上 |
|
|
78
|
+
|
|
79
|
+
## 六、风险与建议
|
|
80
|
+
|
|
81
|
+
- 【事实】线上直营贡献 60.3% 销售额,渠道集中度偏高。
|
|
82
|
+
- 【事实】1/9 出现 138,600 元销售额尖峰,显著偏离日均值。
|
|
83
|
+
- 【推断】尖峰可能为大促活动或数据录入错误,需业务侧核实(置信度:中)。
|
|
84
|
+
- 【建议】① 核实 1/9 订单真实性,避免结论被异常值带偏;② 分销占比仅 16.5%,若为增长目标,建议补充渠道运营动作;③ 修复 `orders` 缺失,保证口径一致。
|
|
85
|
+
|
|
86
|
+
## 附录 A:口径与可复现说明
|
|
87
|
+
|
|
88
|
+
- 数据源:`examples/sample-sales.csv`(UTF-8,逗号分隔)。
|
|
89
|
+
- 过滤:无;重复行去重(保留首条)。
|
|
90
|
+
- 缺失处置:`orders` 缺失行在订单类聚合中排除;`revenue` 无缺失。
|
|
91
|
+
- 算法与参数:占比 = 渠道销售额 / 总销售额;环比 = (本期−上期)/上期;异常阈值 Z-score |z|>3;TopN = 5。
|
|
92
|
+
- 客单价 = 总销售额 / 总订单;日均销售额 = 总销售额 / 天数。
|
|
93
|
+
|
|
94
|
+
## 附录 B:探查记录(节选,见 `node scripts/csv-profile.mjs examples/sample-sales.csv`)
|
|
95
|
+
|
|
96
|
+
| 列名 | 类型 | 缺失 | 摘要 |
|
|
97
|
+
|---|---|---|---|
|
|
98
|
+
| date | date | 0 | 2025-01-01 ~ 2025-01-10,唯一值 10 |
|
|
99
|
+
| channel | text | 0 | 唯一值 3:分销/线上直营/线下门店 |
|
|
100
|
+
| region | text | 0 | 唯一值 3:华东/华南/华北 |
|
|
101
|
+
| orders | number | 3 | min=30 max=900 mean≈94.1(含异常) |
|
|
102
|
+
| revenue | number | 0 | min=6,120 max=120,000 mean≈14,018 |
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
date,channel,region,orders,revenue
|
|
2
|
+
2025-01-01,线上直营,华东,120,15600.50
|
|
3
|
+
2025-01-02,线上直营,华东,98,12890.00
|
|
4
|
+
2025-01-03,线上直营,华东,105,14120.75
|
|
5
|
+
2025-01-01,线下门店,华东,45,8920.00
|
|
6
|
+
2025-01-02,线下门店,华东,52,10100.00
|
|
7
|
+
2025-01-03,线下门店,华东,47,9230.00
|
|
8
|
+
2025-01-01,分销,华南,30,6120.00
|
|
9
|
+
2025-01-02,分销,华南,35,7050.00
|
|
10
|
+
2025-01-03,分销,华南,,6890.00
|
|
11
|
+
2025-01-04,线上直营,华东,110,14980.00
|
|
12
|
+
2025-01-04,线下门店,华东,50,9800.00
|
|
13
|
+
2025-01-04,分销,华南,33,6700.00
|
|
14
|
+
2025-01-05,线上直营,华东,115,15230.00
|
|
15
|
+
2025-01-05,线下门店,华东,48,9450.00
|
|
16
|
+
2025-01-05,分销,华南,36,7210.00
|
|
17
|
+
2025-01-06,线上直营,华东,118,15680.00
|
|
18
|
+
2025-01-06,线下门店,华东,51,9900.00
|
|
19
|
+
2025-01-06,分销,华南,34,6900.00
|
|
20
|
+
2025-01-07,线上直营,华东,122,16000.00
|
|
21
|
+
2025-01-07,线下门店,华东,53,10400.00
|
|
22
|
+
2025-01-07,分销,华南,37,7400.00
|
|
23
|
+
2025-01-08,线上直营,华东,,16500.00
|
|
24
|
+
2025-01-08,线下门店,华东,49,9600.00
|
|
25
|
+
2025-01-08,分销,华南,31,6300.00
|
|
26
|
+
2025-01-09,线上直营,华北,900,120000.00
|
|
27
|
+
2025-01-09,线下门店,华北,55,10800.00
|
|
28
|
+
2025-01-09,分销,华北,39,7800.00
|
|
29
|
+
2025-01-10,线上直营,华北,126,16800.00
|
|
30
|
+
2025-01-10,线下门店,华北,57,11200.00
|
|
31
|
+
2025-01-10,分销,华北,40,8100.00
|
|
32
|
+
2025-01-03,分销,华南,,6890.00
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-data-insight",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "DSH (DeepSeek Harness) data-insight skill plugin: turn raw data (CSV / pasted table / SQL results / DuckDB) into a structured Markdown report with business conclusions, metrics and charts.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./plugin/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./plugin/index.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"dsh": {
|
|
13
|
+
"bundle": {
|
|
14
|
+
"patch": "./cordis.patch.yml"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"plugin/index.js",
|
|
19
|
+
"cordis.patch.yml",
|
|
20
|
+
"skills/",
|
|
21
|
+
"docs/",
|
|
22
|
+
"scripts/",
|
|
23
|
+
"examples/",
|
|
24
|
+
"README.md",
|
|
25
|
+
"CHANGELOG.md",
|
|
26
|
+
"PUBLISHING.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"peerDependencies": {
|
|
30
|
+
"@deepseek-ai/dsh-skill-filesystem": "*"
|
|
31
|
+
},
|
|
32
|
+
"peerDependenciesMeta": {
|
|
33
|
+
"@deepseek-ai/dsh-skill-filesystem": {
|
|
34
|
+
"optional": true
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"dsh",
|
|
39
|
+
"dsh-plugin",
|
|
40
|
+
"deepseek-harness",
|
|
41
|
+
"data-analysis",
|
|
42
|
+
"data-insight",
|
|
43
|
+
"analytics",
|
|
44
|
+
"report",
|
|
45
|
+
"csv",
|
|
46
|
+
"duckdb",
|
|
47
|
+
"skills"
|
|
48
|
+
]
|
|
49
|
+
}
|
package/plugin/index.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-data-insight — DSH (DeepSeek Harness) 插件入口。
|
|
3
|
+
*
|
|
4
|
+
* 复用官方 @deepseek-ai/dsh-skill-filesystem 提供者,把本包自带的 skills/
|
|
5
|
+
* 目录注册为技能根(includeDefaultRoots: false,避免与宿主 profile 的
|
|
6
|
+
* 技能根重复)。零构建:本 ESM 模块由 harness 直接加载。
|
|
7
|
+
*
|
|
8
|
+
* 本包为纯指令型技能插件:它只注入 runbook 指令,计算由宿主已提供的
|
|
9
|
+
* 文件 / shell 工具驱动的 LLM 完成;csv-profile.mjs 脚本由 runbook 指引
|
|
10
|
+
* 通过 shell 直接调用(Node 内建能力,零依赖,不 spawn 子进程)。
|
|
11
|
+
*/
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
import { FileSystemSkillProvider } from "@deepseek-ai/dsh-skill-filesystem";
|
|
15
|
+
|
|
16
|
+
export const name = "dsh-data-insight";
|
|
17
|
+
export const inject = ["skills"];
|
|
18
|
+
|
|
19
|
+
const rootDir = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
20
|
+
const skillsDir = join(rootDir, "skills");
|
|
21
|
+
|
|
22
|
+
export function apply(ctx, config = {}) {
|
|
23
|
+
let provider;
|
|
24
|
+
ctx.skills.registerProvider((control) => {
|
|
25
|
+
provider = new FileSystemSkillProvider(ctx, control, {
|
|
26
|
+
providerName: "dsh-data-insight",
|
|
27
|
+
includeDefaultRoots: false,
|
|
28
|
+
customSkillDirs: [skillsDir],
|
|
29
|
+
...config,
|
|
30
|
+
});
|
|
31
|
+
return provider;
|
|
32
|
+
});
|
|
33
|
+
ctx.effect(
|
|
34
|
+
function* () {
|
|
35
|
+
yield async () => {
|
|
36
|
+
await provider?.dispose();
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
"dsh-data-insight skill provider"
|
|
40
|
+
);
|
|
41
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* csv-profile.mjs — 零依赖 CSV 数据探查脚本(纯 Node 内建,不 spawn 子进程)。
|
|
4
|
+
*
|
|
5
|
+
* 用途:读取一个 CSV/TSV 文件,输出结构化探查报告(schema / 缺失值 /
|
|
6
|
+
* 值分布 / 重复行 / 数值统计 / 异常值),供 data-insight-runbook 阶段 1 使用。
|
|
7
|
+
*
|
|
8
|
+
* 用法:
|
|
9
|
+
* node csv-profile.mjs <file> [--sep <分隔符>] [--encoding <utf8|utf16le|latin1>] [--limit <N>] [--json]
|
|
10
|
+
*
|
|
11
|
+
* 说明:
|
|
12
|
+
* - 默认自动探测分隔符(, \t ;)。
|
|
13
|
+
* - 编码默认 utf8;GBK 文件请先转码(PowerShell: Get-Content -Encoding GBK file | Set-Content -Encoding UTF8 out)。
|
|
14
|
+
* - --limit 限制读取行数(默认 0 = 全部),用于超大数据集抽样探查。
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { readFileSync } from "node:fs";
|
|
18
|
+
|
|
19
|
+
// ── 参数解析 ────────────────────────────────────────────────────────────────
|
|
20
|
+
const args = process.argv.slice(2);
|
|
21
|
+
const file = args.find((a) => !a.startsWith("--"));
|
|
22
|
+
const getArg = (name, def) => {
|
|
23
|
+
const i = args.indexOf(name);
|
|
24
|
+
return i >= 0 && args[i + 1] ? args[i + 1] : def;
|
|
25
|
+
};
|
|
26
|
+
const limit = Number(getArg("--limit", "0")) || 0;
|
|
27
|
+
const encoding = getArg("--encoding", "utf8");
|
|
28
|
+
const asJson = args.includes("--json");
|
|
29
|
+
const forcedSep = getArg("--sep", null);
|
|
30
|
+
|
|
31
|
+
if (!file) {
|
|
32
|
+
console.error("用法: node csv-profile.mjs <file> [--sep <分隔符>] [--encoding utf8|utf16le|latin1] [--limit N] [--json]");
|
|
33
|
+
process.exit(2);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ── 读取与编码 ─────────────────────────────────────────────────────────────
|
|
37
|
+
let text;
|
|
38
|
+
try {
|
|
39
|
+
const buf = readFileSync(file);
|
|
40
|
+
if (buf[0] === 0xef && buf[1] === 0xbb && buf[2] === 0xbf) text = buf.slice(3).toString(encoding);
|
|
41
|
+
else text = buf.toString(encoding);
|
|
42
|
+
} catch (e) {
|
|
43
|
+
console.error(`读取失败: ${e.message}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ── 分隔符探测 ──────────────────────────────────────────────────────────────
|
|
48
|
+
function detectSep(line) {
|
|
49
|
+
if (forcedSep) return forcedSep;
|
|
50
|
+
const cands = [",", "\t", ";", "|"];
|
|
51
|
+
// 只在引号外计数
|
|
52
|
+
let best = ",";
|
|
53
|
+
let bestCount = -1;
|
|
54
|
+
for (const c of cands) {
|
|
55
|
+
let count = 0;
|
|
56
|
+
let inQ = false;
|
|
57
|
+
for (const ch of line) {
|
|
58
|
+
if (ch === '"') inQ = !inQ;
|
|
59
|
+
else if (ch === c && !inQ) count++;
|
|
60
|
+
}
|
|
61
|
+
if (count > bestCount) {
|
|
62
|
+
bestCount = count;
|
|
63
|
+
best = c;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return best;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── CSV 解析(支持引号包裹与转义引号)──────────────────────────────────────
|
|
70
|
+
function parseCsv(text, sep) {
|
|
71
|
+
const rows = [];
|
|
72
|
+
let field = "";
|
|
73
|
+
let row = [];
|
|
74
|
+
let inQ = false;
|
|
75
|
+
for (let i = 0; i < text.length; i++) {
|
|
76
|
+
const ch = text[i];
|
|
77
|
+
if (inQ) {
|
|
78
|
+
if (ch === '"') {
|
|
79
|
+
if (text[i + 1] === '"') {
|
|
80
|
+
field += '"';
|
|
81
|
+
i++;
|
|
82
|
+
} else inQ = false;
|
|
83
|
+
} else field += ch;
|
|
84
|
+
} else {
|
|
85
|
+
if (ch === '"') inQ = true;
|
|
86
|
+
else if (ch === sep) {
|
|
87
|
+
row.push(field);
|
|
88
|
+
field = "";
|
|
89
|
+
} else if (ch === "\n" || ch === "\r") {
|
|
90
|
+
if (ch === "\r" && text[i + 1] === "\n") i++;
|
|
91
|
+
row.push(field);
|
|
92
|
+
field = "";
|
|
93
|
+
if (row.some((c) => c.trim() !== "")) rows.push(row);
|
|
94
|
+
row = [];
|
|
95
|
+
} else field += ch;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (field !== "" || row.length) {
|
|
99
|
+
row.push(field);
|
|
100
|
+
if (row.some((c) => c.trim() !== "")) rows.push(row);
|
|
101
|
+
}
|
|
102
|
+
return rows;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const sampleLine = text.split(/\r?\n/).find((l) => l.trim() !== "") || "";
|
|
106
|
+
const sep = detectSep(sampleLine);
|
|
107
|
+
const rows = parseCsv(text, sep);
|
|
108
|
+
if (rows.length === 0) {
|
|
109
|
+
console.error("未解析到数据行。");
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const header = rows[0].map((h) => h.trim());
|
|
114
|
+
let data = rows.slice(1);
|
|
115
|
+
if (limit > 0) data = data.slice(0, limit);
|
|
116
|
+
const nRows = data.length;
|
|
117
|
+
const nCols = header.length;
|
|
118
|
+
|
|
119
|
+
// ── 类型推断 ────────────────────────────────────────────────────────────────
|
|
120
|
+
const numRe = /^[-+]?\d[\d,]*(\.\d+)?$/;
|
|
121
|
+
const dateRe = /^\d{4}[-/]\d{1,2}[-/]\d{1,2}/;
|
|
122
|
+
const boolRe = /^(true|false|yes|no|是|否|y|n)$/i;
|
|
123
|
+
|
|
124
|
+
function inferType(vals) {
|
|
125
|
+
const nonEmpty = vals.filter((v) => v.trim() !== "");
|
|
126
|
+
if (nonEmpty.length === 0) return "empty";
|
|
127
|
+
if (nonEmpty.every((v) => numRe.test(v.replace(/,/g, "")))) return "number";
|
|
128
|
+
if (nonEmpty.every((v) => dateRe.test(v.trim()))) return "date";
|
|
129
|
+
if (nonEmpty.every((v) => boolRe.test(v.trim()))) return "boolean";
|
|
130
|
+
return "text";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function toNum(v) {
|
|
134
|
+
return Number(v.replace(/,/g, ""));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ── 统计计算 ────────────────────────────────────────────────────────────────
|
|
138
|
+
function quantile(sorted, q) {
|
|
139
|
+
if (sorted.length === 0) return null;
|
|
140
|
+
const pos = (sorted.length - 1) * q;
|
|
141
|
+
const base = Math.floor(pos);
|
|
142
|
+
const rest = pos - base;
|
|
143
|
+
return sorted[base] + (sorted[base + 1] !== undefined ? (sorted[base + 1] - sorted[base]) * rest : 0);
|
|
144
|
+
}
|
|
145
|
+
function mean(arr) {
|
|
146
|
+
return arr.reduce((a, b) => a + b, 0) / arr.length;
|
|
147
|
+
}
|
|
148
|
+
function stddev(arr) {
|
|
149
|
+
const m = mean(arr);
|
|
150
|
+
return Math.sqrt(arr.reduce((a, b) => a + (b - m) ** 2, 0) / arr.length);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const cols = header.map((name, i) => {
|
|
154
|
+
const vals = data.map((r) => (r[i] !== undefined ? r[i].trim() : ""));
|
|
155
|
+
const type = inferType(vals);
|
|
156
|
+
const missing = vals.filter((v) => v === "").length;
|
|
157
|
+
const missingRate = nRows ? ((missing / nRows) * 100).toFixed(1) : "0.0";
|
|
158
|
+
const info = { name, type, missing, missingRate };
|
|
159
|
+
if (type === "number") {
|
|
160
|
+
const nums = vals.filter((v) => v.trim() !== "").map(toNum).sort((a, b) => a - b);
|
|
161
|
+
info.min = nums[0];
|
|
162
|
+
info.max = nums[nums.length - 1];
|
|
163
|
+
info.mean = mean(nums);
|
|
164
|
+
info.median = quantile(nums, 0.5);
|
|
165
|
+
info.stddev = stddev(nums);
|
|
166
|
+
info.p25 = quantile(nums, 0.25);
|
|
167
|
+
info.p75 = quantile(nums, 0.75);
|
|
168
|
+
info.p90 = quantile(nums, 0.9);
|
|
169
|
+
// Z-score 异常值 (|z| > 3)
|
|
170
|
+
const m = info.mean;
|
|
171
|
+
const s = info.stddev || 1;
|
|
172
|
+
const outliers = [];
|
|
173
|
+
vals.forEach((v, idx) => {
|
|
174
|
+
if (v.trim() === "") return;
|
|
175
|
+
const x = toNum(v);
|
|
176
|
+
if (Math.abs((x - m) / s) > 3) outliers.push({ row: idx + 2, value: x });
|
|
177
|
+
});
|
|
178
|
+
info.outliers = outliers.slice(0, 20);
|
|
179
|
+
info.outlierCount = outliers.length;
|
|
180
|
+
} else if (type === "text") {
|
|
181
|
+
const freq = new Map();
|
|
182
|
+
vals.forEach((v) => {
|
|
183
|
+
if (v.trim() === "") return;
|
|
184
|
+
freq.set(v, (freq.get(v) || 0) + 1);
|
|
185
|
+
});
|
|
186
|
+
info.unique = freq.size;
|
|
187
|
+
info.top = [...freq.entries()].sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
188
|
+
} else if (type === "date") {
|
|
189
|
+
const dates = vals.filter((v) => v.trim() !== "").map((v) => v.trim());
|
|
190
|
+
info.min = dates.sort()[0];
|
|
191
|
+
info.max = dates.sort()[dates.length - 1];
|
|
192
|
+
info.unique = new Set(dates).size;
|
|
193
|
+
} else if (type === "boolean") {
|
|
194
|
+
const freq = new Map();
|
|
195
|
+
vals.forEach((v) => {
|
|
196
|
+
if (v.trim() === "") return;
|
|
197
|
+
freq.set(v.trim().toLowerCase(), (freq.get(v.trim().toLowerCase()) || 0) + 1);
|
|
198
|
+
});
|
|
199
|
+
info.top = [...freq.entries()];
|
|
200
|
+
}
|
|
201
|
+
return info;
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
// ── 重复行 ──────────────────────────────────────────────────────────────────
|
|
205
|
+
const seen = new Set();
|
|
206
|
+
let dupCount = 0;
|
|
207
|
+
data.forEach((r) => {
|
|
208
|
+
const key = r.join("\u0001");
|
|
209
|
+
if (seen.has(key)) dupCount++;
|
|
210
|
+
else seen.add(key);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// ── 输出 ────────────────────────────────────────────────────────────────────
|
|
214
|
+
const fmt = (n) => (typeof n === "number" ? (Math.round(n * 100) / 100).toLocaleString() : String(n ?? ""));
|
|
215
|
+
const report = {
|
|
216
|
+
file,
|
|
217
|
+
encoding,
|
|
218
|
+
delimiter: sep === "\t" ? "TAB" : sep,
|
|
219
|
+
rows: nRows,
|
|
220
|
+
columns: nCols,
|
|
221
|
+
duplicateRows: dupCount,
|
|
222
|
+
columnsDetail: cols,
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
if (asJson) {
|
|
226
|
+
console.log(JSON.stringify(report, null, 2));
|
|
227
|
+
process.exit(0);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const out = [];
|
|
231
|
+
out.push(`# CSV 数据探查报告`);
|
|
232
|
+
out.push(``);
|
|
233
|
+
out.push(`- 文件:\`${file}\``);
|
|
234
|
+
out.push(`- 编码:${encoding} 分隔符:${report.delimiter}`);
|
|
235
|
+
out.push(`- 数据行数(不含表头):${nRows} 列数:${nCols} 重复行:${dupCount}`);
|
|
236
|
+
out.push(``);
|
|
237
|
+
out.push(`## Schema 与质量`);
|
|
238
|
+
out.push(`| 列名 | 类型 | 缺失数 | 缺失率 | 摘要 |`);
|
|
239
|
+
out.push(`|---|---|---|---|---|`);
|
|
240
|
+
for (const c of cols) {
|
|
241
|
+
let summary = "";
|
|
242
|
+
if (c.type === "number")
|
|
243
|
+
summary = `min=${fmt(c.min)} max=${fmt(c.max)} mean=${fmt(c.mean)} median=${fmt(c.median)} std=${fmt(c.stddev)} P25=${fmt(c.p25)} P75=${fmt(c.p75)} P90=${fmt(c.p90)}`;
|
|
244
|
+
else if (c.type === "text")
|
|
245
|
+
summary = `唯一值=${c.unique} Top=${(c.top || []).map(([k, v]) => `${k}(${v})`).join(", ")}`;
|
|
246
|
+
else if (c.type === "date") summary = `范围=${c.min} ~ ${c.max} 唯一值=${c.unique}`;
|
|
247
|
+
else if (c.type === "boolean") summary = (c.top || []).map(([k, v]) => `${k}=${v}`).join(", ");
|
|
248
|
+
else summary = "(全空列)";
|
|
249
|
+
out.push(`| ${c.name} | ${c.type} | ${c.missing} | ${c.missingRate}% | ${summary} |`);
|
|
250
|
+
}
|
|
251
|
+
out.push(``);
|
|
252
|
+
const hasOutliers = cols.some((c) => c.type === "number" && c.outlierCount > 0);
|
|
253
|
+
if (hasOutliers) {
|
|
254
|
+
out.push(`## 数值异常值(Z-score |z|>3)`);
|
|
255
|
+
for (const c of cols) {
|
|
256
|
+
if (c.type === "number" && c.outlierCount > 0) {
|
|
257
|
+
out.push(`- **${c.name}**:${c.outlierCount} 个异常值,示例:${c.outliers.map((o) => `行${o.row}=${fmt(o.value)}`).join(", ")}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
out.push(``);
|
|
261
|
+
}
|
|
262
|
+
out.push(`> 说明:缺失值按空字符串判定;数值型已去除千分位逗号。GBK 文件请先转码为 UTF-8。`);
|
|
263
|
+
console.log(out.join("\n"));
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# setup-duckdb.ps1 — 安装 DuckDB CLI(可选依赖,用于 dsh-data-insight 直连数据库路径)
|
|
2
|
+
#
|
|
3
|
+
# 用法:
|
|
4
|
+
# powershell -ExecutionPolicy Bypass -File scripts/setup-duckdb.ps1 # 安装最新稳定版
|
|
5
|
+
# powershell -ExecutionPolicy Bypass -File scripts/setup-duckdb.ps1 -Version 1.5.5 # 指定版本
|
|
6
|
+
#
|
|
7
|
+
# 安装位置:$env:LOCALAPPDATA\DuckDB\cli\duckdb.exe,并写入用户 PATH(持久)。
|
|
8
|
+
# 安全红线由 runbook 保证:调用时一律 -readonly,连接串走环境变量。
|
|
9
|
+
|
|
10
|
+
param(
|
|
11
|
+
[string]$Version = "" # 留空则自动取 duckdb.org 最新稳定版
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
$ErrorActionPreference = "Stop"
|
|
15
|
+
|
|
16
|
+
function Get-LatestVersion {
|
|
17
|
+
try {
|
|
18
|
+
$v = (Invoke-WebRequest -Uri "https://duckdb.org/data/latest_stable_version.txt" -UseBasicParsing -TimeoutSec 20).Content.Trim()
|
|
19
|
+
if ($v -match '^\d+\.\d+\.\d+$') { return $v }
|
|
20
|
+
} catch {
|
|
21
|
+
Write-Warning "无法获取最新版本号:$($_.Exception.Message)"
|
|
22
|
+
}
|
|
23
|
+
return "1.5.5"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (-not $Version) { $Version = Get-LatestVersion }
|
|
27
|
+
Write-Host "目标 DuckDB 版本:$Version"
|
|
28
|
+
|
|
29
|
+
$installDir = Join-Path $env:LOCALAPPDATA "DuckDB\cli"
|
|
30
|
+
$exe = Join-Path $installDir "duckdb.exe"
|
|
31
|
+
|
|
32
|
+
# 已安装且版本匹配则跳过
|
|
33
|
+
if (Test-Path $exe) {
|
|
34
|
+
$installed = (& $exe --version 2>$null | Select-Object -First 1)
|
|
35
|
+
if ($installed -match [regex]::Escape($Version)) {
|
|
36
|
+
Write-Host "已安装 DuckDB $Version($exe)"
|
|
37
|
+
Write-Host "确认 PATH 已包含:$installDir"
|
|
38
|
+
exit 0
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# ── 方案 1:winget ──────────────────────────────────────────────────────────
|
|
43
|
+
$winget = Get-Command winget -ErrorAction SilentlyContinue
|
|
44
|
+
if ($winget) {
|
|
45
|
+
Write-Host "尝试 winget 安装 DuckDB.cli $Version ..."
|
|
46
|
+
winget install --id DuckDB.cli --version $Version --accept-source-agreements --accept-package-agreements --silent
|
|
47
|
+
if ($LASTEXITCODE -eq 0) {
|
|
48
|
+
Write-Host "winget 安装完成。若 PATH 未更新,请重开终端或手动添加:$installDir"
|
|
49
|
+
exit 0
|
|
50
|
+
}
|
|
51
|
+
Write-Warning "winget 安装未成功(exit $LASTEXITCODE),回退到直接下载。"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# ── 方案 2:直接下载 GitHub release ────────────────────────────────────────
|
|
55
|
+
New-Item -ItemType Directory -Force -Path $installDir | Out-Null
|
|
56
|
+
$zip = Join-Path $env:TEMP "duckdb_cli-windows-amd64-$Version.zip"
|
|
57
|
+
$url = "https://github.com/duckdb/duckdb/releases/download/v$Version/duckdb_cli-windows-amd64.zip"
|
|
58
|
+
|
|
59
|
+
Write-Host "下载 $url ..."
|
|
60
|
+
Invoke-WebRequest -Uri $url -OutFile $zip -UseBasicParsing -TimeoutSec 120
|
|
61
|
+
|
|
62
|
+
Write-Host "解压到 $installDir ..."
|
|
63
|
+
Expand-Archive -Path $zip -DestinationPath $installDir -Force
|
|
64
|
+
Remove-Item $zip -Force -ErrorAction SilentlyContinue
|
|
65
|
+
|
|
66
|
+
if (-not (Test-Path $exe)) {
|
|
67
|
+
Write-Error "安装失败:未找到 $exe。请手动从 https://duckdb.org/docs/stable/clients/cli/overview 安装。"
|
|
68
|
+
exit 1
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# ── 写入用户 PATH(持久)───────────────────────────────────────────────────
|
|
72
|
+
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
|
73
|
+
if ($userPath -notlike "*$installDir*") {
|
|
74
|
+
[Environment]::SetEnvironmentVariable("Path", "$userPath;$installDir", "User")
|
|
75
|
+
Write-Host "已写入用户 PATH:$installDir(重开终端后生效)"
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# ── 验证 ───────────────────────────────────────────────────────────────────
|
|
79
|
+
Write-Host "验证安装:"
|
|
80
|
+
& $exe --version
|
|
81
|
+
Write-Host "`n完成。用法示例(只读):"
|
|
82
|
+
Write-Host " duckdb -readonly -csv -c ""SELECT * FROM read_csv_auto('data.csv') LIMIT 100"""
|
|
83
|
+
Write-Host " duckdb -readonly -csv -c ""SELECT ... LIMIT 5000"" `"`$env:DATA_INSIGHT_DB_URL`""
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: data-insight-runbook
|
|
3
|
+
description: 数据洞察报告:输入原始数据(CSV 文件 / 粘贴表格 / SQL 查询结果 / DuckDB 直连数据库)→ 输出带图表与业务结论的结构化 Markdown 分析报告。覆盖数据探查、指标计算、同环比、TopN、异常值、图表生成与严谨性检查。需要做数据分析、出报告、看数据、算指标、画图时加载本技能。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 数据洞察 Runbook
|
|
7
|
+
|
|
8
|
+
把原始数据变成「业务结论 + 指标数据 + 图表」的结构化 Markdown 报告。
|
|
9
|
+
|
|
10
|
+
## 执行原则(贯穿全程,违反即返工)
|
|
11
|
+
|
|
12
|
+
1. **每个数字必须有口径**:时间范围、过滤条件、聚合方式、计算公式都要写清楚。
|
|
13
|
+
2. **每条结论必须有数字支撑**:禁止无依据的推断;结论引用的数字必须在正文或附录可查到。
|
|
14
|
+
3. **区分事实与推断**:数据事实(GMV 环比 -12%)与模型推断(可能受大促错期影响)分开标注,推断要给出依据与置信度。
|
|
15
|
+
4. **报告可复现**:附录记录原始数据、处理步骤、算法参数,任何人能按附录重算得到同样数字。
|
|
16
|
+
5. **不编造数据**:探查不到的数字不猜;缺失值按下方规则处置并如实记录。
|
|
17
|
+
|
|
18
|
+
按阶段执行,每阶段有硬性门槛;未过门槛不得进入下一阶段。
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 阶段 0:输入受理与口径确认
|
|
23
|
+
|
|
24
|
+
识别数据源类型,确认分析口径。**门槛:数据已加载 + 口径已确认,才进入探查。**
|
|
25
|
+
|
|
26
|
+
### 0.1 识别数据源
|
|
27
|
+
- **CSV 文件**:用户给路径,或在工作区找 `.csv`/`.tsv`。
|
|
28
|
+
- **粘贴表格**:用户直接粘贴的表格文本(含 SQL 查询结果导出)。约定:列用 `|` 或制表符分隔,首行为表头;NULL 写 `NULL`/`null`/空值需说明。
|
|
29
|
+
- **DuckDB 直连**:见下方「数据源接入细则·DuckDB」。
|
|
30
|
+
|
|
31
|
+
### 0.2 确认口径(缺失就用 ask_user_question 一次问清)
|
|
32
|
+
- **分析目标**:要回答什么业务问题?(看趋势 / 找异常 / 做对比 / 找 TopN / 综合体检)
|
|
33
|
+
- **指标定义**:核心指标(GMV、转化率、留存…)的确切计算公式。
|
|
34
|
+
- **时间范围与粒度**:起止日期、聚合粒度(日/周/月)。
|
|
35
|
+
- **维度**:按什么分组(渠道/地区/品类…)。
|
|
36
|
+
- **过滤条件**:排除哪些脏数据 / 测试数据。
|
|
37
|
+
|
|
38
|
+
用户说「随便 / 你定」时采用默认:全量数据、日粒度、核心指标按列名语义推断并在报告中明确写出推断依据。
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 阶段 1:数据探查
|
|
43
|
+
|
|
44
|
+
**门槛:探查记录完成,异常数据有处置决定。**
|
|
45
|
+
|
|
46
|
+
### 1.1 加载与解析
|
|
47
|
+
- CSV:用 `scripts/csv-profile.mjs` 探查(优先),或 read 头几行确认结构。
|
|
48
|
+
- 编码:优先 UTF-8;出现乱码再按 GBK 重读。分隔符:`,` > `\t` > `;` 依次尝试,以「列数一致且无单列吞并」为准。
|
|
49
|
+
- 记录:文件路径、行数(不含表头)、列数、编码、分隔符。
|
|
50
|
+
|
|
51
|
+
### 1.2 探查内容(写入探查记录,供附录引用)
|
|
52
|
+
- **Schema**:列名、每列推断类型(数值/文本/日期/布尔/ID)。
|
|
53
|
+
- **缺失值**:每列缺失数、缺失率;>20% 的列标注。
|
|
54
|
+
- **值分布**:数值列(min/max/mean/median/分位数)、文本列(唯一值数、Top 高频值)、日期列(范围与粒度)。
|
|
55
|
+
- **脏数据**:重复行、异常格式(日期不统一、数字带单位/千分位)、明显错误值(负数年龄、未来日期)。
|
|
56
|
+
|
|
57
|
+
### 1.3 异常处置(必须明确决定,写进报告附录)
|
|
58
|
+
- 缺失值:删除 / 填 0 / 填均值 / 单独标记——按列语义决定并记录。
|
|
59
|
+
- 重复行:去重(保留规则)或保留(说明)。
|
|
60
|
+
- 单位不一致:统一单位并记录换算。
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 阶段 2:指标计算
|
|
65
|
+
|
|
66
|
+
**门槛:每个输出数字都有明确口径与计算公式。**
|
|
67
|
+
|
|
68
|
+
用确定性规则计算,禁止每轮自创算法。可用 `scripts/csv-profile.mjs` 或让 LLM 按规则手算(大数据量优先脚本,避免算错与上下文爆掉)。
|
|
69
|
+
|
|
70
|
+
### 2.1 汇总统计
|
|
71
|
+
总数、求和、均值、中位数、标准差、min/max、分位数(P25/P50/P75/P90)。
|
|
72
|
+
|
|
73
|
+
### 2.2 同环比(基准期规则写死)
|
|
74
|
+
- 环比:本期 vs 上一期(日环比=昨天;周环比=上周;月环比=上月)。
|
|
75
|
+
- 同比:本期 vs 去年同期;跨年数据缺失时降级为「不可算,记录原因」。
|
|
76
|
+
- 增长率公式:`(本期 − 基期) / 基期 × 100%`;基期为 0 时标注「不适用(基期为 0)」。
|
|
77
|
+
|
|
78
|
+
### 2.3 TopN
|
|
79
|
+
- N 默认 5(报告空间有限),用户可指定 5/10/20。
|
|
80
|
+
- 按指标降序;并列时都列出并标注并列名次。
|
|
81
|
+
|
|
82
|
+
### 2.4 异常值
|
|
83
|
+
- 数值列:Z-score 法 `|z| > 3` 判定异常,同时给出 IQR(1.5×IQR)对照;口径写进附录。
|
|
84
|
+
- 报告中异常值列出「值 + 所属维度 + 判定依据」,不做无依据的归因猜测。
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## 阶段 3:图表呈现(三通道)
|
|
89
|
+
|
|
90
|
+
**门槛:图中每个数据点都能在正文或附录表格找到,禁止图表与数字不一致。**
|
|
91
|
+
|
|
92
|
+
三通道规则见 `docs/chart-spec.md`,要点:
|
|
93
|
+
1. **主通道**:Markdown 表格 + 关键数字——永远可靠,任何查看器都渲染。
|
|
94
|
+
2. **次通道**:Mermaid(趋势用 `xychart-beta`、占比用 `pie`),标注「需 Mermaid 渲染器」;DSH Web GUI 不渲染 Mermaid,主通道表格必须能独立撑起报告。
|
|
95
|
+
3. **兜底**:ASCII 条形图(code block 内),纯文本处处可读。
|
|
96
|
+
- **禁止**手写 SVG(易错、难维护)。
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 阶段 4:报告产出
|
|
101
|
+
|
|
102
|
+
**门槛:严谨性检查清单全过。**
|
|
103
|
+
|
|
104
|
+
### 4.1 模板与检查清单
|
|
105
|
+
严格按 `docs/report-template.md` 的骨架与严谨性检查清单产出,固定结构:
|
|
106
|
+
核心结论 → 数据概况 → 关键指标 → 趋势与对比 → TopN 与异常 → 风险与建议 → 附录(口径与可复现说明)。
|
|
107
|
+
|
|
108
|
+
### 4.2 硬性要求
|
|
109
|
+
- 核心结论 3–5 条,每条带具体数字。
|
|
110
|
+
- 「风险与建议」中事实与推断分开标注。
|
|
111
|
+
- 附录含:数据源、时间范围、过滤条件、缺失/重复处置、算法与参数、图表数据表。
|
|
112
|
+
- 报告以 Markdown 文件落盘到工作区(文件名如 `data-insight-report.md`),并汇报路径。
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## 数据源接入细则
|
|
117
|
+
|
|
118
|
+
### CSV 文件
|
|
119
|
+
- 优先 `node scripts/csv-profile.mjs <file.csv>` 出探查报告,再按口径计算。
|
|
120
|
+
- 大文件(>2000 行):先探查,指标计算交给脚本/DuckDB,不要让 LLM 逐行读全文(会爆上下文)。
|
|
121
|
+
|
|
122
|
+
### 粘贴表格 / SQL 查询结果
|
|
123
|
+
- 直接按上述阶段 1 解析(表头 + 分隔符 + 类型推断)。
|
|
124
|
+
- 若来自 SQL:记录来源查询(脱敏后写入附录),确认是否已聚合。
|
|
125
|
+
|
|
126
|
+
### DuckDB 直连
|
|
127
|
+
- **启用检测**:`duckdb --version` 可用才走直连;不可用时 CSV/粘贴仍全功能,直连路径明确告知「需安装 DuckDB(见 scripts/setup-duckdb.ps1)」,不静默降级。
|
|
128
|
+
- **安全红线(必须遵守)**:
|
|
129
|
+
- 一律 `-readonly`,禁止任何写库 / 建表 / `INSTALL`/`LOAD` 之外的破坏性语句。
|
|
130
|
+
- 连接串走环境变量 `DATA_INSIGHT_DB_URL`,禁止把密码写进命令或报告。
|
|
131
|
+
- 查询加 `LIMIT`(默认 5000),列宽截断;超限提示分批。
|
|
132
|
+
- **调用方式(直接调 CLI,勿写 Node spawn 包装——规避沙箱子进程管道 EPERM)**:
|
|
133
|
+
- CSV/Parquet 直接查:`duckdb -readonly -csv -c "SELECT * FROM read_csv_auto('data.csv') LIMIT 100"`
|
|
134
|
+
- 远程库:`duckdb -readonly -csv -c "SELECT ... LIMIT 5000" "$env:DATA_INSIGHT_DB_URL"`
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## 沙箱与环境约束(重要)
|
|
139
|
+
|
|
140
|
+
- 运行环境受 DSH 文件沙箱限制:读写尽量落在工作区内;`workdir` 必须指向已存在目录。
|
|
141
|
+
- **不要写 Node 脚本去 spawn 子进程**(如脚本内调用 duckdb/ffmpeg):confined 模式下子进程管道捕获会报 `EPERM`。duckdb 由 runbook 指引通过 shell 工具直接调用;`csv-profile.mjs` 只做纯文件读写与计算,不 spawn 子进程。
|
|
142
|
+
- 需要联网查数据字典 / 口径时用 web_search,不臆造。
|
|
143
|
+
|
|
144
|
+
## 例外与裁剪
|
|
145
|
+
|
|
146
|
+
- 极小数据(≤50 行、≤10 列):可跳过脚本,LLM 直接探查与计算,但五阶段门槛不豁免。
|
|
147
|
+
- 用户明确只要「快速看一眼」:产出精简版(核心结论 + 关键指标表 + 一个图表),但严谨性清单中「结论有数字、区分事实/推断」不豁免。
|
|
148
|
+
|
|
149
|
+
## 参考文档
|
|
150
|
+
|
|
151
|
+
- `docs/chart-spec.md`:三通道图表规范与代码示例。
|
|
152
|
+
- `docs/report-template.md`:报告骨架与严谨性检查清单。
|
|
153
|
+
- `scripts/csv-profile.mjs`:零依赖 CSV 探查脚本。
|