# dsh-table-ingest — 表格文件转核对格式

### 快速试跑（1 分钟）

```bash
node samples/quickstart/run.mjs --check
```

样例在 `samples/quickstart/input/`，期望输出在 `samples/quickstart/expected/result.json`；截图见 `docs/assets/quickstart.png`。


[English](README.md) · [简体中文](README-zh.md) · [Español](README-es.md) · [Português](README-pt.md) · [हिन्दी](README-hi.md)

<!-- topblock:v1 -->
> **这是什么**　把 docx / xlsx / csv 转成核对器能读的表格，并告诉你它认出了哪些表头和列。
> **给谁用**　需要把表格转成核对格式的经办人。
> **你交什么**　docx / xlsx / csv 文件；转成核对器能读的表格 JSON。
> **它不做什么**　不判合规、不认定责任、不替代审核人；只列材料内部能逐条指认的差异。
> **怎么开始**　命令行安装（暂未单独上架）：
> `dsh plugin --profile <name> add dsh-table-ingest`
<!-- /topblock:v1 -->

[![dshfind](https://dshfind.com/api/badge/PerryLink/dsh-table-ingest?metric=downloads)](https://dshfind.com/en/plugins/PerryLink/dsh-table-ingest?ref=badge)

`dsh-table-ingest` 把 `.docx`、`.xlsx`、`.xlsm`、`.csv`、`.tsv` 文件转成本族规则检查器能吃的表格 JSON。检查器只能读它认得出的表；而职场里的真实文档多是带多张表、合并单元格、表头之上还有标题行的 Word。这个包就是那一步转换——更重要的是，**转不出来时它会说，而不是猜**。

本包不作任何业务判断：不判定材料是否合规，也没有自己的规则，`rules/table-ingest.yaml` 是刻意留空的。

## Install

```sh
dsh plugin --profile <name> add dsh-table-ingest
dsh --profile <name> --dump-config | grep 'dsh-table-ingest'
```

## What it looks like

![Workflow diagram for dsh-table-ingest: document → rule pack → findings.](docs/assets/dsh-table-ingest-flow.svg)

*Workflow overview: document → rule pack → findings.*

**边界：** 本包是转换器，不是检查器。它不产出发现、不给文档打分、也不改写读到的文件。一份文档里有几张表都可能说得通时，它返回候选并问你要哪一张——因为悄悄抽错表会让下游检查器报出**并不存在的差异**，那比多问一句贵得多。它也不读 `.doc`（二进制）、扫描件和 PDF：那些需要本族刻意不自带的转换器或 OCR。

## What it answers

- 这份文档里到底有几张表，各有多大？
- 哪一行是表头，列名是什么？
- 选定的这张表够不够可靠到可以跑规则，还是该先让人确认？
- 没有可用的表，是文档本来就没有，还是需要 OCR？

## Standards it follows

本包不编码任何法律或监管条款，所以规则库为空、也没有可引用的证据目录。它编码的是 `word/document.xml` 与 `xl/worksheets/*.xml` 的结构：

- `<w:tbl>`、`<w:tr>`、`<w:tc>` 描述表结构；`<w:gridSpan>` 是横向合并，`<w:vMerge/>`（不带 `w:val`）表示"沿用上一格"——后者在 XML 里是**空的**，不补下来，一份完整的文档会看起来缺日期。
- `<w:tblHeader>` 加底纹与加粗作为表头线索；格式本身没有"表头行"的规范定义，所以表头识别是打分启发式，结果带置信度。
- `sharedStrings.xml` 的共享串（`<v>` 里是**索引不是值**）、内联串（`t="inlineStr"`，值不在 `<v>` 里），以及 `c@style` → `cellXfs` → `numFmtId` 的三层间接——它决定一个数字是不是日期。

## Compatibility

| Surface | Status |
|---|---|
| Harness | peer range `>=0.1.2-rc.1 <0.2.0 \|\| >=0.2.0-0 <0.3.0` |
| Node | `^22.19.0 || >=24.0.0` |
| Platforms | all (pure ESM; no native code, no network, no model call) |

## What it does

没有规则。工具只暴露一个操作 `table_ingest`，输入文件路径与若干可选提示，返回：

| 字段 | 含义 |
|---|---|
| `ok` | 是否以足够置信度定位到唯一一张表 |
| `tableIndex`、`headerRow`、`columns` | 选了什么 |
| `rowCount`、`candidateCount` | 读了多少行、发现几张表 |
| `confidence` | `high` / `medium` / `low`；`low` 表示"该有人看一眼" |
| `preview` | 前几行的 markdown 表，供确认 |
| `tableJson` | 规则检查器可接受的表格形状 |
| `warnings`、`error` | 哪里奇怪，或为什么没选表 |

选定的表按 `parseTable` 接受的形状输出，检查器可直接消费。

## Configuration

| Key | Default | Meaning |
|---|---|---|
| `rulesFile` | `rules/table-ingest.yaml` | 规则库路径 |

## Material format

一份带标题行、表头行和两条数据行的 `.docx`：

```
序号 | 日期        | 施工部位   | 监理情况
1    | 2026-10-08  | 1#楼三层   | 旁站检查…
2    | 2026-10-08  | 1#楼三层   | 旁站全过程…
```

转换成：

```json
{
  "target": "site-log.docx",
  "tableIndex": 0,
  "headerRows": [2],
  "columns": ["序号", "日期", "施工部位", "监理情况"],
  "header": {},
  "rows": [
    { "序号": "1", "日期": "2026-10-08", "施工部位": "1#楼三层", "监理情况": "旁站检查…" },
    { "序号": "2", "日期": "2026-10-08", "施工部位": "1#楼三层", "监理情况": "旁站全过程…" }
  ],
  "warnings": ["1 个单元格为空，已从上方合并单元格补值"],
  "mergedCells": 1,
  "sections": [],
  "ragged": false
}
```

`.csv` 与 `.tsv` 产出同样形状，`headerRows` 为 `[1]`。

## Rule sources

本包不引用条款，因此没有 `rules/evidence/` 目录，规则库按设计为空。可用下面这条确认：

```sh
pnpm run check:citations   # 期望 0 segments, 0 placeholders, 0 untraceable
```

已知边界，写出来而不是藏起来：

- **表头识别是启发式。** 它在前六行里按加粗、底纹、`tblHeader`、短文本、无数字、小词表打分。表头更靠下会被判成"无表头"，而不是无限往下找。
- **两级表头默认不合并。** 合并会破坏下游的别名匹配（`金额（元）/合计` 不等于 `合计`），所以取最具体的一行；只有子级重名时才加父级前缀。
- **跨度不一致的表会被报告，不会被修复。** 某行的格数对不上声明的列宽时，结果标 `ragged` 且置信度 `low`；文件里没有能还原作者意图的信息。
- **`expectedColumns` 是你最强的一个信号。** 传了它，一份四张表的歧义文档会变成确定性选择。

## Troubleshooting

- **The plugin installs but the tool never appears**: check that `main` resolves to `lib/index.mjs` and that `pnpm run build` produced it.
- **`dsh plugin add` rejects the package**: the peer range covers `0.1.x` and `0.2.x`; outside it, grant an explicit exemption.
- **A rule did not run**: read the `skipped` array.

## Development

```sh
pnpm install
pnpm run typecheck
pnpm test
pnpm run build
node ../scripts/sync-shared.mjs dsh-table-ingest
```

## PerryLink DSH Plugin Family

完整清单（62 个核查器 + 通用插件）见 [按材料选核查器](https://perrylink.github.io/zh/checkers/choose/)；本仓只是其中一个。

## License

[Apache License 2.0](LICENSE) © 2026 dsh-table-ingest contributors.
