完善国标公文全条款核验与特定版式

This commit is contained in:
mizzlelover
2026-09-11 07:51:42 +08:00
parent ac994d4881
commit de1fd6574d
44 changed files with 319 additions and 77 deletions
+5 -4
View File
@@ -86,7 +86,7 @@ https://std.samr.gov.cn/gb/search/gbDetailed?id=lOIe27f77QU%3D&mode=p
| 标题与目录 | 四级标题写入 Word/WPS 标题样式及大纲级别,可插入自动目录 |
| 页码 | 居中、单双页或不显示 |
| 印章、签名章 | 在生成的 DOCX 中按实际需要插入图片或完成实体盖章 |
| 特定格式 | `letter``command``minutes` 使用对应生成分支 |
| 特定格式 | `letter``command``minutes``horizontal-table` 使用对应生成分支;联合行文用 `--joint-org`,分离装订附件用 `--attachment-detached` |
GB/T 9704-2012 当前为现行标准,2025-05-30 复审继续有效。标准适用于党政机关制发公文,其他机关和单位可以参照执行。[全国标准信息公共服务平台](https://std.samr.gov.cn/gb/search/gbDetailed?id=lOIe27f77QU%3D&mode=p)
@@ -106,11 +106,11 @@ node scripts/verify_gongwen_docx.mjs --input /tmp/formal.docx --profile formal -
只有需要完整电子红头文件时,才加 `--letterhead digital`。红头和标题要使用目标电脑中实际安装的小标宋体;生成器缺少小标宋体或仿宋体时会在终端明确警告并写出替代字体,要求严格阻止替代输出时加 `--require-standard-fonts`。红线下标题会写入两个显式的28磅版心空行,不会依赖渲染器自行解释段前距;发文机关标志到文号也按相同方式留出两行。上行文的文号和签发人会在同一行编排。打开文档后,Word/WPS 的“引用→目录”可以按“标题1—标题4”生成并更新目录;是否显示第四级,在目录设置中选择。
信函、命令(令)纪要走专用分支:信函生成170mm上粗下细、下页边20mm处上细下粗的两条红色双线,底线放在页脚并关闭页码;命令(令)落实机关标志下空二行、令号下空二行;纪要把“出席”“请假”“列席”标签设为黑体、人员名单设为仿宋。附件可用重复的 `--attachment-file` 另页生成“附件”标签、第三行标题和正文,并保持正式页码连续。带抄送或印发信息时,版记浮动锚定在最后一页版心底部,首末粗线约0.35mm、中间细线约0.25mm。
信函、命令(令)纪要和横排表格走专用分支:信函生成170mm上粗下细、下页边20mm处上细下粗的两条红色双线,底线放在页脚并关闭页码;命令(令)落实机关标志距版心上边缘20mm、令号下空二行、正文下空二行;纪要把“出席”“请假”“列席”标签设为黑体、人员名单设为仿宋,并放在正文或附件说明下一行;横排表格生成横向A4和225mm表格,单双页表头方向仍需目标Word/WPS模板核对。联合行文可重复传入 `--joint-org`,不能与正文一起装订的附件加 `--attachment-detached` 会在首行补排文号和附件序号。附件可用重复的 `--attachment-file` 另页生成“附件”标签、第三行标题和正文,并保持正式页码连续。带抄送或印发信息时,版记浮动锚定在最后一页版心底部,首末粗线约0.35mm、中间细线约0.25mm。
## 跨平台安装
本项目使用开放的 `SKILL.md` 目录结构,可在 Codex、Claude Code、OpenCode、Trae Code、Trae CLI、Kimi Code CLI、Kimi Code、WorkBuddy 和 ZCode 使用。
本项目使用开放的 `SKILL.md` 目录结构,可在 Codex、Claude Code、OpenCode、Trae Code、Trae CLI、Kimi Code CLI、Kimi Code、TraeWork、WorkBuddy 和 ZCode 使用。
macOS / Linux
@@ -135,7 +135,8 @@ Windows
- [生成器](scripts/generate_gongwen_docx.mjs):根据输入生成 DOCX 版式。
- [校验器](scripts/verify_gongwen_docx.mjs):检查生成器可承诺的 DOCX 版式要素。
- [回归测试](tests/run_tests.sh):运行居中与单双页页码生成、DOCX 包结构与 PDF 转换验证。
- [全条款视觉核验](references/gbt9704-visual-audit.md):实际生成 13 份 DOCX、渲染 24 张 PNG,并逐条记录截图证据与人工边界。
- [全条款视觉核验](references/gbt9704-visual-audit.md):实际生成 18 份 DOCX、37 个 PDF 页面和 PNG,并逐条记录截图证据与人工边界。
- [全条款截图证据矩阵](references/gbt9704-screenshot-evidence.md):把 GB/T 9704-2012 每个条款绑定到实际截图,明确哪些项目截图不能证明。
- [版头坐标量测](tests/measure-header-coordinates.mjs):用 PDF 字形坐标独立核对份号首行、机关标志 35 mm 定位及可选字段不移位。
- [跨平台安装测试](tests/test-install.sh):隔离环境中验证九个本地目录和 TraeWork 导入包。
+4 -4
View File
@@ -15,7 +15,7 @@ GB/T 9704-2012《党政机关公文格式》现行,2025-05-30 复审继续有
本工具只处理排版,不判断单位身份、授权状态或后续盖章方式。它会根据用户明确选择的版式模式应用对应的排版规则,不因“正式”“国企”“报告”或仅提供机构名称而擅自使用红头。
生成器可输出 A4、156mm×225mm 版心、三号仿宋正文、小标宋二号标题、四级标题层次、两字缩进,以及居中或单双页页码。四级标题均写入 Word/WPS 标题样式和大纲级别,便于后续插入、更新目录;文章总标题使用独立“公文标题”样式,不进入目录。上行文的文号和签发人在同一行编排,左右各空一字;信函、命令(令)、纪要使用各自专用版式;重复传入 `--attachment-file` 时必须同时提供与附件顺序、标题一致的 `--attachment-note`,附件会另页生成并保持页码连续。版记会锚定最后一页版心底部,正式页码按版心下边缘下7mm的位置生成。正式版头的份号、密级、紧急程度和电子机关标志以版心上边缘为垂直锚点固定定位,不会因为左上字段而把机关标志或预印红头留白向下推移。只有传入密级而未传入份号时,首行会保留空位,密级仍从第二行开始。
生成器可输出 A4、156mm×225mm 版心、三号仿宋正文、小标宋二号标题、四级标题层次、两字缩进,以及居中或单双页页码。四级标题均写入 Word/WPS 标题样式和大纲级别,便于后续插入、更新目录;文章总标题使用独立“公文标题”样式,不进入目录。上行文的文号和签发人在同一行编排,左右各空一字;信函、命令(令)、纪要和横排表格使用各自专用版式;电子联合行文可重复传入 `--joint-org`,分离装订附件可加 `--attachment-detached`重复传入 `--attachment-file` 时必须同时提供与附件顺序、标题一致的 `--attachment-note`,附件会另页生成并保持页码连续。版记会锚定最后一页版心底部,正式页码按版心下边缘下7mm的位置生成。正式版头的份号、密级、紧急程度和电子机关标志以版心上边缘为垂直锚点固定定位,不会因为左上字段而把机关标志或预印红头留白向下推移。只有传入密级而未传入份号时,首行会保留空位,密级仍从第二行开始。
`references/formal-checklist.md` 是可选的版式复核清单。涉及印章或签名章时,使用者可在 Word/WPS 中插入已取得的图片或完成实体盖章;本生成器不内置印章图像。
@@ -35,7 +35,7 @@ node scripts/generate_gongwen_docx.mjs --input source.md --output output.docx --
node scripts/generate_gongwen_docx.mjs --input source.md --output output.docx --format formal --letterhead digital --org "发文机关名称" --doc-no "单位发〔20261号" --title "文档标题"
```
`letter``command``minutes` 分别对应信函、命令(令)、纪要格式;需要时显式传入 `--format` 和相应字段。`command` 会要求机关标志以“命令”或“令”结尾;信函中的份号、密级和紧急程度会在第一条双线下按标准顺序编排,联合行文和机关专用信函模板仍需目标软件核对。正式发文默认单双页页码;`standard` 为单页右、双页左,`center``none` 供普通材料选择。
`letter``command``minutes``horizontal-table` 分别对应信函、命令(令)、纪要和横排表格格式;需要时显式传入 `--format` 和相应字段。横排表格会生成横向A4页面和225mm表格,单双页表头在订口/切口的朝向仍需目标 Word/WPS 模板核对。联合行文使用 `--joint-org`;附件不能与正文一起装订时使用 `--attachment-detached`,附件首行会补排文号和附件序号。`command` 会要求机关标志以“命令”或“令”结尾;信函中的份号、密级和紧急程度会在第一条双线下按标准顺序编排,联合行文和机关专用信函模板仍需目标软件核对。正式发文默认单双页页码;`standard` 为单页右、双页左,`center``none` 供普通材料选择。
附件可重复传入 `--attachment-file attachment.md`。附件 Markdown 的首个一级标题会作为附件标题,第一页第一行写“附件1”(后续文件递增),第三行写标题,随后排正文;附件说明通过 `--attachment-note` 提供,工具会统一为“附件:”并去除末尾标点,同时拒绝顺序或标题与附件文件不一致的输入。复杂横排附件、不能与正文一起装订的附件和机关专用模板仍需在目标 Word/WPS 中核对。
@@ -55,9 +55,9 @@ node scripts/verify_gongwen_docx.mjs --input output.docx --profile formal --lett
bash tests/visual-audit.sh /tmp/gongwen-visual-audit
```
全条款视觉证据和未自动化边界见[GB/T 9704-2012 全条款视觉核验记录](references/gbt9704-visual-audit.md)。横排表格、联合行文、实体印章、纸张和装订不能只由 DOCX 生成器证明。
全条款视觉证据和未自动化边界见[GB/T 9704-2012 全条款视觉核验记录](references/gbt9704-visual-audit.md)和[全条款截图证据矩阵](references/gbt9704-screenshot-evidence.md)。横排表格的单双页表头方向、实体印章、纸张和装订不能只由 DOCX 生成器证明。
校验器核验生成器可检查的版式要素,并按 `ordinary``formal``letter``command``minutes` 选择校验档案。输出后仍须在目标 Word/WPS 与实际打印条件下检查首页正文、分页、附件、表格、页码、红头纸套打和装订。
校验器核验生成器可检查的版式要素,并按 `ordinary``formal``letter``command``minutes``horizontal-table` 选择校验档案。输出后仍须在目标 Word/WPS 与实际打印条件下检查首页正文、分页、附件、表格、页码、红头纸套打和装订。
## 输出说明
BIN
View File
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

@@ -0,0 +1 @@
Header coordinate measurement passed: copy=39.88mm, agency=71.41mm, no-fields-agency=71.41mm, delta=0.00mm
Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

@@ -0,0 +1,19 @@
scenario profile pdf_pages committed_screenshot_examples
command command 2 references/assets/visual-audit/all-rules/command-1.png, references/assets/visual-audit/all-rules/command-2.png
formal-attachments-detached digital 4 references/assets/visual-audit/all-rules/formal-attachments-detached-3.png
formal-attachments digital 4 references/assets/visual-audit/all-rules/formal-attachments-3.png, references/assets/visual-audit/all-rules/formal-attachments-4.png
formal-digital-all digital 2 references/assets/visual-audit/all-rules/formal-digital-all-1.png, references/assets/visual-audit/all-rules/formal-digital-all-2.png
formal-digital-upward digital 2 references/assets/visual-audit/all-rules/formal-digital-upward-1.png, references/assets/visual-audit/all-rules/formal-digital-upward-2.png
formal-joint digital 2 references/assets/visual-audit/all-rules/formal-joint-1.png, references/assets/visual-audit/all-rules/formal-joint-2.png
formal-long digital 3 references/assets/visual-audit/all-rules/formal-long-2.png, references/assets/visual-audit/all-rules/formal-long-3.png
formal-preprinted digital 2 references/assets/visual-audit/all-rules/formal-preprinted-1.png, references/assets/visual-audit/all-rules/formal-preprinted-2.png
formal-seal digital 2 references/assets/visual-audit/all-rules/formal-seal-2.png
formal-signed digital 2 references/assets/visual-audit/all-rules/formal-signed-2.png
formal-unsigned digital 2 references/assets/visual-audit/all-rules/formal-unsigned-1.png, references/assets/visual-audit/all-rules/formal-unsigned-2.png
horizontal-table horizontal-table 1 references/assets/visual-audit/all-rules/horizontal-table-1.png
letter-fields letter 2 references/assets/visual-audit/all-rules/letter-fields-1.png
letter letter 1 references/assets/visual-audit/all-rules/letter-1.png
minutes digital 2 references/assets/visual-audit/all-rules/minutes-1.png, references/assets/visual-audit/all-rules/minutes-2.png
ordinary-calibration ordinary 2 references/assets/visual-audit/all-rules/ordinary-calibration-1.png
ordinary ordinary 1 references/assets/visual-audit/all-rules/ordinary-1.png
table ordinary 1 references/assets/visual-audit/all-rules/table-1.png
1 scenario profile pdf_pages committed_screenshot_examples
2 command command 2 references/assets/visual-audit/all-rules/command-1.png, references/assets/visual-audit/all-rules/command-2.png
3 formal-attachments-detached digital 4 references/assets/visual-audit/all-rules/formal-attachments-detached-3.png
4 formal-attachments digital 4 references/assets/visual-audit/all-rules/formal-attachments-3.png, references/assets/visual-audit/all-rules/formal-attachments-4.png
5 formal-digital-all digital 2 references/assets/visual-audit/all-rules/formal-digital-all-1.png, references/assets/visual-audit/all-rules/formal-digital-all-2.png
6 formal-digital-upward digital 2 references/assets/visual-audit/all-rules/formal-digital-upward-1.png, references/assets/visual-audit/all-rules/formal-digital-upward-2.png
7 formal-joint digital 2 references/assets/visual-audit/all-rules/formal-joint-1.png, references/assets/visual-audit/all-rules/formal-joint-2.png
8 formal-long digital 3 references/assets/visual-audit/all-rules/formal-long-2.png, references/assets/visual-audit/all-rules/formal-long-3.png
9 formal-preprinted digital 2 references/assets/visual-audit/all-rules/formal-preprinted-1.png, references/assets/visual-audit/all-rules/formal-preprinted-2.png
10 formal-seal digital 2 references/assets/visual-audit/all-rules/formal-seal-2.png
11 formal-signed digital 2 references/assets/visual-audit/all-rules/formal-signed-2.png
12 formal-unsigned digital 2 references/assets/visual-audit/all-rules/formal-unsigned-1.png, references/assets/visual-audit/all-rules/formal-unsigned-2.png
13 horizontal-table horizontal-table 1 references/assets/visual-audit/all-rules/horizontal-table-1.png
14 letter-fields letter 2 references/assets/visual-audit/all-rules/letter-fields-1.png
15 letter letter 1 references/assets/visual-audit/all-rules/letter-1.png
16 minutes digital 2 references/assets/visual-audit/all-rules/minutes-1.png, references/assets/visual-audit/all-rules/minutes-2.png
17 ordinary-calibration ordinary 2 references/assets/visual-audit/all-rules/ordinary-calibration-1.png
18 ordinary ordinary 1 references/assets/visual-audit/all-rules/ordinary-1.png
19 table ordinary 1 references/assets/visual-audit/all-rules/table-1.png
Binary file not shown.

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

+6 -3
View File
@@ -12,7 +12,7 @@
## 1. 前提与制作
- [ ] 已明确选择 `ordinary``formal``letter``command``minutes`;报告、方案、汇报材料不因出现机构名称而自动采用红头。
- [ ] 已明确选择 `ordinary``formal``letter``command``minutes``horizontal-table`;报告、方案、汇报材料不因出现机构名称而自动采用红头。
- [ ] 正式发文已确认采用预印红头纸套打或完整电子红头。套打稿以实际纸样核定首页预留高度,不重复打印纸上既有的红色机关标志、红线。
- [ ] 已确认发文机关、文种、行文关系、主送范围、发文字号、签发权限及机关现行模板。
- [ ] A4 成品幅面为 210mm×297mm;天头 37mm±1mm、订口 28mm±1mm、版心 156mm×225mm。
@@ -43,7 +43,7 @@
- [ ] 署名、成文日期和印章已按“加盖印章/不加盖印章/签发人签名章/联合行文”之一完整处理:签发人签名章模式正文下空二行、职务左空二字,签名章下空一行、日期右空四字;成文日期年月日用阿拉伯数字全称且月日不补零;没有空白印章或伪造印章。空白不足时记录行距、字距调整。
- [ ] 附注适用时在成文日期下一行左空二字,以圆括号标注。
- [ ] 附件另面编排、置于版记之前;左上第一行为“附件”及序号黑体三号,标题在第三行居中;附件序号和标题与正文附件说明逐项一致;与正文一起装订的附件页码连续。不能一起装订时,左上补排发文字号及“附件”序号。
- [ ] 使用 `--attachment-file` 生成的附件已逐个另页,附件标签在第一行、标题在第三行;无法一起装订时已人工补排发文字号和“附件”标识。
- [ ] 使用 `--attachment-file` 生成的附件已逐个另页,附件标签在第一行、标题在第三行;使用 `--attachment-detached` 时已确认附件首行补排发文字号和“附件”标识;复杂附件仍需人工核对
## 4. 版记、页码与特殊格式
@@ -51,8 +51,11 @@
- [ ] 抄送机关适用时为四号仿宋、左右各空一字、回行与冒号后首字对齐、末尾句号;主送移入版记时按“主送”规则处理。
- [ ] 印发机关和日期为四号仿宋,左空一字与右空一字;日期完整且不补零,后加“印发”。
- [ ] 页码为四号半角宋体阿拉伯数字,两侧各一字线,线距版心下边缘7mm;单页右空一字、双页左空一字;空白页、特定版记页和附件连续页码已按规则处理。
- [ ] 横排表格、信函、命令(令)、纪要、联合行文已使用相应专用版式;不能用普通打印稿替代
- [ ] 横排表格使用 `--format horizontal-table` 或目标 Word/WPS 横向模板;本项目已实际渲染横向页面和225mm表格,但单双页表头在订口/切口的方向仍需目标软件核对
- [ ] 信函、命令(令)、纪要、联合行文已使用相应专用版式;`formal --joint-org` 的截图已确认主办机关在前,复杂机关标志仍按现行模板核对;不能用普通打印稿替代。
- [ ] 信函格式已核对上边缘30mm、上粗下细双线、下页边20mm处上细下粗双线,双线长度均170mm;如有份号、密级、紧急程度,均置于第一条双线下并距线3号字高7/8;文号顶格居右;首页无页码且版记不画分隔线。当前生成器对这些可选信函版头字段要求使用专用模板。
- [ ] 命令(令)格式已核对发文机关全称加“命令”或“令”、标志上边缘距版心20mm、令号下空二行、正文下空二行;签发章和日期按7.3.5.3核对。
- [ ] 纪要格式已核对标志上边缘距版心35mm,出席/请假/列席标签为黑体,人员为仿宋。
- [ ] 计量单位、标点符号、数字分别按 GB 3100、GB 3101、GB 3102、GB/T 15834、GB/T 15835 复核。
- [ ] 已保存本次实际渲染的[全条款截图证据矩阵](gbt9704-screenshot-evidence.md),并逐项记录截图通过或截图不能证明的边界。
+5 -4
View File
@@ -140,7 +140,7 @@
## 八、横排表格和特定格式
- A4横排表格的页码位置与其他页一致;单双页表头方向按标准处理。
- A4横排表格的页码位置与其他页一致;生成器的 `--format horizontal-table` 会生成横向A4和225mm表格,单双页表头在订口/切口的朝向仍需目标 Word/WPS 模板处理。
- 信函、命令(令)、纪要均有专门格式,不能套用普通公文首页。
- 信函首页不显示页码,版记规则也不同。上边缘距纸边30mm;发文机关标志下4mm处为170mm上粗下细红色双线,距下页边20mm处为170mm上细下粗红色双线;标题与其上最后一个要素相距二行。
- 命令(令)发文机关标志由机关全称加“命令”或“令”组成,上边缘距版心20mm;标志下空二行编排令号,令号下空二行编排正文;签发人职务、签名章和日期按7.3.5.3处理。
@@ -155,14 +155,15 @@
| 四级正文标题 | 是 | 特殊机关模板调整 |
| 报告居中页码 | 是 | 无 |
| 正式单双页页码 | 是 | `--page-number standard`;页脚按版心下边缘下7mm的位置生成 |
| 普通表格 | 是 | 横排表格方向复杂跨页表格复核 |
| 横排表格 | 部分通过 | `--format horizontal-table` 生成横向页面和225mm表格;单双页表头方向复杂跨页表格复核 |
| 普通材料 | 是 | `--format ordinary`,不绘制红头 |
| 预印红头纸套打 | 是 | `--format formal --letterhead preprinted`,按纸样设置预留高度 |
| 完整电子红头 | 是 | `--format formal --letterhead digital` |
| 印章、签名章 | 文字版式之外 | 在 Word/WPS 中插入图片或实体盖章 |
| 附件 | 通过 `--attachment-file` 另页生成标签、标题和正文 | 无法一起装订时的发文字号标注、复杂附件版式仍需人工复核 |
| 联合行文 | 通过 `--joint-org` 同行生成,主办机关在前 | 复杂机关专用标志和联署模板复核 |
| 附件 | 通过 `--attachment-file` 另页生成标签、标题和正文;`--attachment-detached` 补排文号和附件序号 | 复杂附件版式仍需人工复核 |
| 版记 | 是 | `--cc``--print-org``--print-date` 适用时生成 |
| 信函、命令、纪要 | 是 | `--format letter|command|minutes`,按实际字段生成 |
| 信函、命令、纪要、横排表格 | 是/部分通过 | `--format letter|command|minutes|horizontal-table`,按实际字段生成;横排表头方向需模板复核 |
## 十、建议检查
+6 -6
View File
@@ -20,7 +20,7 @@
| 7.2.1 | 份号六位、3 号、左上第一行 | `--copy-no` 只接受 1—6 位阿拉伯数字并补足六位 | 结构检查;实际是否需要份号由发文机关确认 |
| 7.2.2 | 密级和保密期限第二行、3 号黑体 | `--secret` 按黑体3号写入;没有份号时自动保留第一行空位,使密级仍在第二行 | 结构检查和视觉审计;文字内容和期限写法人工复核 |
| 7.2.3 | 紧急程度及排列顺序 | `--urgent` 按黑体 3 号写入,并按份号、密级、紧急程度顺序下移 | 适用性和紧急级别人工确认 |
| 7.2.4 | 发文机关标志居中、红色、版心上边缘下 35mm、推荐小标宋 | `formal --letterhead digital` 才绘制;预印模式只留纸样区域;首面字段以版心上边缘为垂直锚点,机关标志固定在其下35mm,不推动机关标志或预留区;联合行文仍需模板 | `verify_gongwen_docx.mjs``tests/measure-header-coordinates.mjs`视觉核验记录;当前机缺少推荐字体时严格模式失败 |
| 7.2.4 | 发文机关标志居中、红色、版心上边缘下 35mm、推荐小标宋 | `formal --letterhead digital` 才绘制;预印模式只留纸样区域;首面字段以版心上边缘为垂直锚点,机关标志固定在其下35mm,不推动机关标志或预留区;联合行文通过 `--joint-org` 同行生成,主办机关在前,名称过长时自适应标志字号 | `verify_gongwen_docx.mjs``tests/measure-header-coordinates.mjs`[`gbt9704-screenshot-evidence.md`](gbt9704-screenshot-evidence.md);当前机缺少推荐字体时严格模式失败,复杂机关专用标志仍需模板核对 |
| 7.2.5 | 文号下空二行、六角括号、全年份、顺序号不加“第”不补零 | `formal`/`letter``--doc-no` 做格式校验,并写入两个显式 28 磅空行 | 文号语义由机关编号规则确认 |
| 7.2.6 | 上行文文号左空一字、签发人右空一字并同一行;标签仿宋、姓名楷体 | `--upward true --signer` 同行生成单一或两个签发人,左右各空一字;多签发人、联合行文转人工模板 | 结构校验和 PDF;复杂联合行文需按机关顺序人工排布 |
| 7.2.7 | 文号下 4mm、版心等宽红线 | `redRule()` 生成 156mm 红线,文号段后 4mm;预印模式不重绘 | PDF 量测;红色油墨和实际纸样人工核对 |
@@ -30,16 +30,16 @@
| 7.3.4 | 附件说明正文下空一行、左空二字、名称末无标点 | `--attachment-note` 统一为“附件:”并去除末尾标点,表格后不重复空行 | 长名称回行和多附件编号仍需 PDF/模板复核 |
| 7.3.5 | 署名、成文日期、印章/签名章 | 分别记录加盖印章、不加盖印章、签发人签名章、日期数字和特殊空白处理;生成器排文字占位并补足签名章模式的两行/一行间距,不伪造印章 | `--seal-mode` 仍需把实体章或已取得的图片放入 Word/WPS;单/联合机关印章、红色签名章和空白不足时调整人工完成 |
| 7.3.6 | 附注在日期下一行左空二字、圆括号 | `--note` 规范化为全角圆括号 | 日期与附注的最终分页人工核对 |
| 7.3.7 | 附件另面、第一行附件标签黑体、第三行标题、版记前、页码连续且与附件说明一致 | `--attachment-file` 用分页符、显式空行和标题样式生成,并校验附件顺序和标题与附件说明一致,附件共用连续页脚 | 不能一起装订时的文号+附件标识、复杂附件版式人工处理 |
| 7.3.7 | 附件另面、第一行附件标签黑体、第三行标题、版记前、页码连续且与附件说明一致 | `--attachment-file` 用分页符、显式空行和标题样式生成,并校验附件顺序和标题与附件说明一致,附件共用连续页脚 | `--attachment-detached` 会在附件首行补排文号和“附件”序号;复杂附件版式仍需人工处理 |
| 7.4.1 | 版记首末粗线、中间细线,末线贴最后一页版心下边缘 | 版记改为浮动表,锚定最后一页版心底部;粗线 0.35mm、细线 0.25mm | 已用长文 PDF 截图实测;Word/WPS 和极端分页仍需抽检 |
| 7.4.2 | 抄送 4 号仿宋、左右一字、回行对齐、末尾句号 | `--cc` 生成 4 号、左右一字、全角冒号和句号 | 多行抄送回行和主送移版记人工复核 |
| 7.4.3 | 印发机关/日期 4 号仿宋,左右一字,日期后“印发”,细线隔开 | `--print-org``--print-date` 生成右制表位、日期校验和末条粗线 | 实际机关名称长度和目标软件分页人工复核 |
| 7.5 | 4 号半角宋体页码、两侧一字线、距版心下边缘 7mm、单双页位置、附件连续 | 正式页脚用半角宋体、单双页字段,footer 距离调整为 7mm 位置;信函关闭页码 | XML + PDF bbox;空白页/特定版记页的例外仍需人工复核 |
| 8 | 横排表格页码与表头方向 | 普通 Markdown 表格按纵向版心生成;视觉审计保留 `table-1.png` 作为未自动化证据,不把纵向表格冒充横排 | 横排表格须在 Word/WPS 采用专用横向模板人工检查 |
| 8 | 横排表格页码与表头方向 | 新增 `--format horizontal-table`,实际生成横向 A4 和225mm表格,页码保持标准单双页位置;单双页表头在订口/切口方向尚未自动化 | [`horizontal-table-1.png`](gbt9704-screenshot-evidence.md);表头方向须在 Word/WPS 专用横向模板人工检查 |
| 9 | 计量单位、标点、数字按引用标准 | 结构字段(文号、日期、附件冒号、句号)做边界校验 | 正文语义、单位、标点和数字用法需人工或专门文本审校 |
| 10.1 | 信函上下双红线、30mm/20mm、170mm、可选版头字段、标题二行、无首页页码、无版记线 | `--format letter` 生成双线、专用边距、文号距离、标题空行、底部双线页脚和无页码,并支持份号、密级、紧急程度在第一条双线下按顺序编排 | PDF 实测;联合行文及模板字段人工复核 |
| 10.2 | 命令(令)标志由机关全称加“命令”或“令”、上边缘20mm、令号下空二行、正文下空二行 | `--format command` 要求 `--org` 以“命令/令”结尾、`--doc-no`两个间隔均为显式28磅空行 | 令号文字和签名章/日期按7.3.5.3人工复核 |
| 10.3 | 纪要标志 35mm;出席/请假/列席标签黑体,人员仿宋 | `--format minutes` 要求机关标志以“纪要”结尾,三类人员分 run 写入对应字体,并按两字起排、悬挂缩进处理回行 | `references/gbt9704-visual-audit.md``minutes-1.png`/`minutes-2.png`;纪要具体版式可按机关制度调整 |
| 10.2 | 命令(令)标志由机关全称加“命令”或“令”、上边缘20mm、令号下空二行、正文下空二行 | `--format command` 要求 `--org` 以“命令/令”结尾、`--doc-no`机关标志从版心上边缘下20mm开始,令号和正文均为空二行 | [`command-1.png`](gbt9704-screenshot-evidence.md)签名章/日期按7.3.5.3人工复核 |
| 10.3 | 纪要标志 35mm;出席/请假/列席标签黑体,人员仿宋 | `--format minutes` 要求机关标志以“纪要”结尾,三类人员分 run 写入对应字体,紧跟正文或附件说明下一行,并按两字起排、悬挂缩进处理回行 | [`minutes-1.png`](gbt9704-screenshot-evidence.md)/[`minutes-2.png`](gbt9704-screenshot-evidence.md);纪要具体版式可按机关制度调整 |
| 11 | 标准式样图 | 各分支按条款参数实现,不把截图当作唯一依据 | 最终仍需与机关现行式样、Word/WPS 和打印样张逐页比对 |
## 本轮硬性修正
@@ -50,4 +50,4 @@
- 文号、成文日期、附件说明的结构边界现在会拒绝明显不符合标准的输入;缺失小标宋或仿宋时仍会明确告警,严格模式会停止生成。
本矩阵的实际渲染证据、截图文件名和逐条人工边界见[GB/T 9704-2012 全条款视觉核验记录](gbt9704-visual-audit.md)。
本矩阵的实际渲染证据、截图文件名和逐条人工边界见[GB/T 9704-2012 全条款视觉核验记录](gbt9704-visual-audit.md)和[全条款截图证据矩阵](gbt9704-screenshot-evidence.md)
+111
View File
@@ -0,0 +1,111 @@
# GB/T 9704-2012 全条款截图证据矩阵
本文件是本次修订的逐条复核记录。它把国标条款、生成场景、实际渲染 PNG 和可证明边界逐一对应起来。截图只证明截图中可观察到的版式结果;纸张指标、油墨、套印、装订、机关语义和目标 Word/WPS 的专用模板行为,明确标为“截图不能证明”,不以 DOCX ZIP 属性替代。
标准依据:
- [全国标准信息公共服务平台现行状态页](https://std.samr.gov.cn/gb/search/gbDetailed?id=lOIe27f77QU%3D&mode=p)GB/T 9704-20122025-05-30 复审结论为继续有效。
- [GB/T 9704-2012 条文全文转载页](https://dzb.cumtb.edu.cn/info/1040/1184.htm):本次逐条对照的公开全文。
## 本次实际渲染批次
运行命令:
```bash
bash tests/visual-audit.sh /tmp/gongwen-visual-audit-all-rules
```
结果:18 份 DOCX、18 份 PDF、37 个 PDF 页面;每份 DOCX 先由校验器检查,再由 LibreOffice 转 PDF,最后用 `pdftoppm` 生成 PNG。`tests/measure-header-coordinates.mjs` 还用 `pdftotext -bbox-layout` 独立量测了电子红头首面:份号 39.88mm,机关标志 71.41mm,带字段和不带字段的位置差 0.00mm。
提交到仓库的证据目录为 [`references/assets/visual-audit/all-rules/`](assets/visual-audit/all-rules/),其中:
- [`manifest.tsv`](assets/visual-audit/all-rules/manifest.tsv) 记录 18 个场景、页数和已提交截图。
- [`contact-sheet.png`](assets/visual-audit/all-rules/contact-sheet.png) 是场景总览;单页 PNG 保留原始截图尺寸。
- [`header-coordinates.log`](assets/visual-audit/all-rules/header-coordinates.log) 是独立坐标量测结果。
## 场景与截图索引
| 场景 | 用途 | 截图 |
| --- | --- | --- |
| `ordinary` | 普通材料,不因机构名称变红头 | [`ordinary-1.png`](assets/visual-audit/all-rules/ordinary-1.png) |
| `ordinary-calibration` | 22 行、28 字网格校准页 | [`ordinary-calibration-1.png`](assets/visual-audit/all-rules/ordinary-calibration-1.png) |
| `formal-digital-all` | 电子红头全要素、附件说明、附注、版记 | [`formal-digital-all-1.png`](assets/visual-audit/all-rules/formal-digital-all-1.png)、[`formal-digital-all-2.png`](assets/visual-audit/all-rules/formal-digital-all-2.png) |
| `formal-digital-upward` | 上行文文号、签发人、单双页页码 | [`formal-digital-upward-1.png`](assets/visual-audit/all-rules/formal-digital-upward-1.png)、[`formal-digital-upward-2.png`](assets/visual-audit/all-rules/formal-digital-upward-2.png) |
| `formal-preprinted` | 预印红头纸套打留白 | [`formal-preprinted-1.png`](assets/visual-audit/all-rules/formal-preprinted-1.png)、[`formal-preprinted-2.png`](assets/visual-audit/all-rules/formal-preprinted-2.png) |
| `formal-joint` | 联合行文机关标志 | [`formal-joint-1.png`](assets/visual-audit/all-rules/formal-joint-1.png)、[`formal-joint-2.png`](assets/visual-audit/all-rules/formal-joint-2.png) |
| `formal-unsigned` | 不加盖印章文字位置 | [`formal-unsigned-1.png`](assets/visual-audit/all-rules/formal-unsigned-1.png)、[`formal-unsigned-2.png`](assets/visual-audit/all-rules/formal-unsigned-2.png) |
| `formal-seal` / `formal-signed` | 印章、签名章文字占位 | [`formal-seal-2.png`](assets/visual-audit/all-rules/formal-seal-2.png)、[`formal-signed-2.png`](assets/visual-audit/all-rules/formal-signed-2.png) |
| `formal-attachments` / `formal-attachments-detached` | 附件另面、分离装订附件 | [`formal-attachments-3.png`](assets/visual-audit/all-rules/formal-attachments-3.png)、[`formal-attachments-4.png`](assets/visual-audit/all-rules/formal-attachments-4.png)、[`formal-attachments-detached-3.png`](assets/visual-audit/all-rules/formal-attachments-detached-3.png) |
| `formal-long` | 版记底部锚定、单双页页码 | [`formal-long-2.png`](assets/visual-audit/all-rules/formal-long-2.png)、[`formal-long-3.png`](assets/visual-audit/all-rules/formal-long-3.png) |
| `letter` / `letter-fields` | 信函双线和可选字段 | [`letter-1.png`](assets/visual-audit/all-rules/letter-1.png)、[`letter-fields-1.png`](assets/visual-audit/all-rules/letter-fields-1.png) |
| `command` | 命令(令)标志、令号、正文间距 | [`command-1.png`](assets/visual-audit/all-rules/command-1.png) |
| `minutes` | 纪要标志和出席、请假、列席人员 | [`minutes-1.png`](assets/visual-audit/all-rules/minutes-1.png)、[`minutes-2.png`](assets/visual-audit/all-rules/minutes-2.png) |
| `horizontal-table` / `table` | 横排表格和纵向表格对照 | [`horizontal-table-1.png`](assets/visual-audit/all-rules/horizontal-table-1.png)、[`table-1.png`](assets/visual-audit/all-rules/table-1.png) |
## 逐条核对
状态只使用以下三种含义:
- **截图通过**:本次 PNG 中已经看到条款要求的可视结果,并有结构或坐标检查辅助。
- **截图通过,字体/目标软件仍需核对**:位置或结构可见,但本机缺标准字体,或 Word/WPS 的专用行为还要在目标环境复核。
- **截图不能证明**:标准要求的是纸张、印刷、装订、语义或实体印章等外部条件;工具没有伪造通过结论。
| 条款 | 国标要求 | 本批截图与结果 | 状态及边界 |
| --- | --- | --- | --- |
| 1 | 规定党政机关公文的纸张、版面、印制装订、格式要素和式样;其他单位可参照。 | `ordinary-1.png``formal-digital-all-1.png``letter-1.png``command-1.png``minutes-1.png` 展示了不同格式分支。 | 截图通过;具体文种、行文关系和机关模板仍由使用者确认。 |
| 2 | 引用 GB/T 148、GB 3100/3101/3102、GB/T 15834、GB/T 15835。 | 本仓库摘要、矩阵和本证据文件均列出引用文件;截图不能验证引用标准的语义执行。 | 截图不能证明正文单位、标点和数字语义。 |
| 3.1 | 一字为一个汉字宽度。 | `ordinary-calibration-1.png` 显示固定字宽网格样例。 | 截图通过,实际字体字面宽度随目标字体核对。 |
| 3.2 | 一行为一个汉字高度加三号字高度 7/8。 | `ordinary-calibration-1.png` 显示 28 磅实现网格;XML 校验确认 `w:linePitch=560`。 | 截图通过,术语本身不能只靠截图定义。 |
| 4 | 纸张 60—80g/m²、白度80%—90%、横向耐折度≥15、不透明度≥85%、pH7.5—9.5。 | 生成器没有把这些物性写成 DOCX 属性;截图只显示页面。 | 截图不能证明;须以纸张规格和成品检测为准。 |
| 5.1 | A4 成品 210mm×297mm。 | 18 个 PDF 场景均由脚本检查 A4;`horizontal-table-1.png` 为 A4 横向页面。 | 截图通过;裁切误差仍属成品核验。 |
| 5.2.1 | 天头37±1mm、订口28±1mm、版心156×225mm。 | `formal-digital-all-1.png``formal-preprinted-1.png``ordinary-calibration-1.png`;校验器检查页边,标题和正文落在版心内。 | 截图通过;打印缩放关闭后需再量纸样。 |
| 5.2.2 | 默认三号仿宋,特殊情况可调整。 | 全部截图可见三号网格;本机实际使用 `STFangsong` 替代并输出警告,严格模式会失败。 | 截图通过,字体仍需目标机安装方正小标宋/仿宋后复核。 |
| 5.2.3 | 一般每面22行、每行28字并撑满版心。 | `ordinary-calibration-1.png` 是 22 行×28 字校准页;普通长文和特定格式按内容自然分页。 | 截图通过;长标题、表格和目标字体导致的特殊调整需逐文档看图。 |
| 5.2.4 | 未特别说明时文字为黑色。 | `ordinary-1.png``formal-digital-all-2.png` 的正文、标题、版记文字为黑色;红色仅用于规定的红头和线。 | 截图通过。 |
| 6.1 | 版面干净无底灰、字迹清楚无断划、尺寸标准、版心不斜,误差≤1mm。 | `contact-sheet.png` 中所有 PDF 页面均无生成的底灰或倾斜;屏幕截图不能证明实体制版误差。 | 截图部分通过;实体误差和印品质量不能由本工具证明。 |
| 6.2 | 双面印刷、套正≤2mm、黑墨BL100%、红墨Y80%/M80%,着墨均匀无缺陷。 | `formal-long-2.png``formal-long-3.png` 展示双页页码位置;无实体双面印刷样。 | 截图不能证明油墨、套正和印品质量。 |
| 6.3 | 左侧装订、页码误差≤4mm、成品±2mm、两钉距上下边缘70±4mm、平订距书脊3—5mm等。 | `formal-long-3.png` 只显示页面顺序和版记位置。 | 截图不能证明装订、裁切、钉位和封皮。 |
| 7.1 | 版头、主体、版记三区;页码位于版心外。 | `formal-digital-all-1.png` 展示版头与主体,`formal-long-3.png` 展示末页版记和页码。 | 截图通过。 |
| 7.2.1 | 份号六位三号阿拉伯数字,版心左上第一行。 | `formal-digital-all-1.png``formal-preprinted-1.png` 显示 `000007`;独立坐标日志显示 39.88mm 首行位置。 | 截图通过。 |
| 7.2.2 | 密级和保密期限第二行三号黑体,数字用阿拉伯数字。 | `formal-digital-all-1.png` 显示 `机密★3年` 位于第二行。 | 截图通过;密级文字及期限是否适用需机关确认。 |
| 7.2.3 | 紧急程度三号黑体;与份号、密级同时出现时按份号、密级、紧急程度自上而下。 | `formal-digital-all-1.png` 显示三项顺序;`letter-fields-1.png` 显示信函双线下同顺序。 | 截图通过。 |
| 7.2.4 | 机关全称/规范简称可加“文件”,居中红色;上边缘距版心上边缘35mm;联合行文主办机关在前,联署名称上下居中。 | `formal-digital-all-1.png``formal-joint-1.png`;坐标日志显示机关标志 71.41mm 页面坐标,字段差0.00mm;联合行文以主办机关在前并自适应同一行字号。 | 截图通过,推荐小标宋字体需目标机复核;复杂机关专用标志仍需模板核对。 |
| 7.2.5 | 机关标志下空二行;年份全称、六角括号、顺序号不加“第”不补零、末尾“号”;上行文文号左空一字。 | `formal-digital-all-1.png``formal-digital-upward-1.png`;文号输入边界和显式空行均经校验。 | 截图通过。 |
| 7.2.6 | “签发人:”三号仿宋、姓名三号楷体,右空一字;多签发人按顺序、一般每行两名、回行对齐。 | `formal-digital-upward-1.png` 显示同一行双签发人,姓名字体和标签分 run。 | 截图通过;超过两名或联合行文复杂签发人仍须专用模板。 |
| 7.2.7 | 文号下4mm处、版心等宽红色分隔线。 | `formal-digital-all-1.png``formal-joint-1.png`;线宽156mm,红线下标题空两行。 | 截图通过;预印模式明确不重绘红线。 |
| 7.3.1 | 标题二号小标宋,红线下空二行,居中;多行应词意完整、对称、梯形或菱形。 | `formal-digital-all-1.png``formal-preprinted-1.png`;标题样式为 `GongwenTitle`,空行在 PNG 中可见。 | 截图通过;小标宋缺失时严格模式失败;长标题断行需逐稿人工调整。 |
| 7.3.2 | 标题下空一行,主送机关顶格,回行顶格,末尾全角冒号;过多可移版记。 | `formal-digital-all-1.png``formal-joint-1.png`。 | 截图通过;自动移版记的决定由发文机关确认。 |
| 7.3.3 | 首页有正文;正文三号仿宋、自然段左空二字回行顶格;层次序数依次“一、”“(一)”“1.”“(1)”,字体黑体/楷体/仿宋。 | `formal-digital-all-1.png``ordinary-1.png` 显示首页正文和四级层次;校验器确认 Heading1—Heading4、大纲级别。 | 截图通过,标准字体和复杂分页仍需目标软件复核。 |
| 7.3.4 | 正文下空一行左空二字写“附件:”;多附件编号;附件名称末不加标点,回行与首字对齐。 | `formal-digital-all-2.png` 显示附件说明;生成器校验说明与附件标题、顺序一致。 | 截图通过;极长名称需看最终换行。 |
| 7.3.5.1 | 加盖印章:日期右空四字,红色印章压署名和日期,印章顶端距正文/附件说明一行内,不得空白印章;联合章须排列整齐。 | `formal-seal-2.png` 展示署名和日期占位;生成器不写入虚假印章。 | 截图不能证明实体章和联合章几何关系;需 Word/WPS 或打印盖章。 |
| 7.3.5.2 | 不盖章:署名正文下空一行右空二字,日期下一行首字右移二字;联合署名自上而下。 | `formal-unsigned-2.png` 显示署名日期两行和右侧定位。 | 截图通过;多机关联合署名需目标模板复核。 |
| 7.3.5.3 | 签发人签名章:正文下空二行,职务左空二字,签名章右空四字;日期下空一行右空四字;联合逐行对齐。 | `formal-signed-2.png` 展示职务/签名章文字占位和日期行。 | 截图部分通过;实体签名章颜色、尺寸、联合对齐不能由本工具证明。 |
| 7.3.5.4 | 年月日使用阿拉伯数字,年份全称,月日不补零。 | `formal-digital-all-2.png``formal-unsigned-2.png` 显示 `2026年9月10日`;输入校验拒绝 `09月`。 | 截图通过。 |
| 7.3.5.5 | 空白不足时可调行距、字距容纳印章或签名章、日期。 | `formal-seal-2.png``formal-signed-2.png` 仅覆盖正常空间。 | 截图不能证明极端内容自动调节;需按稿件人工调节。 |
| 7.3.6 | 附注在日期下一行左空二字,用圆括号。 | `formal-digital-all-2.png` 显示 `(此件公开)`。 | 截图通过。 |
| 7.3.7 | 附件另面、版记前;第一行附件标签三号黑体顶格,标题第三行居中;与附件说明一致;不能一起装订时左上补发文字号和附件序号。 | `formal-attachments-3.png``formal-attachments-4.png``formal-attachments-detached-3.png`;分离装订截图显示文号+附件序号。 | 截图通过;复杂附件内容和实体装订仍需复核。 |
| 7.4.1 | 版记线与版心等宽,首末粗线约0.35mm,中间细线约0.25mm,末线与末页版心下边缘重合。 | `formal-long-3.png` 显示粗—细—粗顺序;版记表锚定末页版心底部。 | 截图通过;极端内容在 Word/WPS 需抽检。 |
| 7.4.2 | 抄送四号仿宋,印发机关日期上一行,左右各空一字,回行对齐,末尾句号;主送移版记时改“主送”。 | `formal-digital-all-2.png``formal-long-3.png` 显示抄送、回行和句号。 | 截图通过;主送自动移版记仍是人工决定。 |
| 7.4.3 | 印发机关日期四号仿宋,末条线之上,左一字右一字,日期后“印发”;其他要素用细线隔开。 | `formal-digital-all-2.png``formal-long-3.png`。 | 截图通过。 |
| 7.5 | 四号半角宋体页码,两侧一字线,线上距版心下边缘7mm;单页右、双页左;空白/版记例外;附件连续。 | `formal-long-2.png``formal-long-3.png``formal-attachments-4.png`;校验器和 PDF 坐标检查页脚。 | 截图通过;空白页例外和实体套印仍需人工。 |
| 8 | 横排 A4 表格页码位置不变,单页表头在订口边、双页表头在切口边。 | `horizontal-table-1.png` 已实际渲染 A4 横向页面、225mm 表格和标准页码;截图同时暴露当前表头仍在横向页面上边缘,尚未实现单双页订口/切口方向转换。 | 部分通过;表头朝向必须在 Word/WPS 横向模板中处理,不能宣称本生成器已完全覆盖8。 |
| 9 | 计量单位、标点、数字分别符合引用标准。 | `formal-digital-all-2.png``10 kg、5 m、3.14` 观察样例;工具只校验字段边界。 | 截图不能证明全文语义;需文本审校。 |
| 10.1 | 信函:标志距上页边30mm;上粗下细双线、下页边20mm处上细下粗,均170mm;可选字段顺序;标题空二行;首页无页码;版记无印发/分隔线。 | `letter-1.png``letter-fields-1.png`;校验器检查双线宽度、方向、底线页脚和无页码。 | 截图通过;联合行文信函和机关专用模板仍需复核。 |
| 10.2 | 命令(令):机关全称加“命令/令”,标志上边缘距版心上边缘20mm;令号下空二行,正文下空二行;签章日期按7.3.5.3。 | `command-1.png`;独立截图中标志约57mm页面坐标,令号和正文间隔可见。 | 截图通过;签章和日期实体环节仍需人工。 |
| 10.3 | 纪要标志上边缘距版心上边缘35mm;出席、请假、列席标签三号黑体,人员三号仿宋,正文/附件说明下空一行左空二字,回行对齐。 | `minutes-1.png``minutes-2.png`;出席信息已移动到正文/附件说明之后,而不是错误地追加到版记之后。 | 截图通过;纪要可按机关实际制度调整。 |
| 11 | 给出 A4 页边、普通首页、联合行文、末页、附件、信函、命令等式样。 | `contact-sheet.png` 覆盖普通、联合、末页、附件、信函、命令和纪要实际渲染场景。 | 截图通过;正式发文仍应对照本机关现行式样图。 |
## 本次发现并修正的实现问题
1. 命令(令)分支曾把20mm误当成纸张上边距,导致机关标志距离纸顶约20mm;现在恢复标准37mm天头,并在版心上边缘下20mm处排标志,实际截图已复核。
2. 联合行文曾把联署机关名称拆成多行,容易把“上下居中”误做成额外机关标志行;现在主办机关在前,联署名称同一居中行,自适应较小标志字号以保持一行。
3. 纪要出席、请假、列席名单曾追加在附件和版记之后;现在紧跟正文或附件说明下一行生成,符合10.3的编排位置。
4. PDF 字形量测曾按 `yMin` 粗略分组,遇到高字形会把同一视觉行拆开;现在按字形垂直重叠合并后再量测,独立坐标结果稳定。
5. 视觉审计补入了横排表格、22×28校准页、联合行文、分离装订附件、不盖章公文和命令(令)等场景,不再只依赖电子红头样例。
## 不能由生成器或截图单独保证的边界
- 当前机器没有方正小标宋和标准仿宋字体;生成器会明确报警,严格模式会停止。要交付标准字体稿,应在目标机器安装标准字体后重新生成并截图。
- 纸张克重、白度、耐折度、不透明度、pH、黑红油墨、双面套正、裁切、装订、钉位和实体印章属于制作环节。
- 正文单位、标点、数字语义,以及文种、行文关系、主送范围、机关名称规范化和发文权限属于内容或机关审校。
- 横排表格的单双页表头朝向、复杂联合机关标志、超过两名签发人、复杂联合印章和目标机关专用模板仍需在 Word/WPS 或打印样张中完成。
+17 -16
View File
@@ -4,19 +4,17 @@
## 核验方法
2026 年 9 月 10 日运行:
2026 年 9 月 11 日运行:
```bash
bash tests/visual-audit.sh /tmp/gongwen-visual-audit-latest
bash tests/visual-audit.sh /tmp/gongwen-visual-audit-all-rules
```
脚本实际生成 13 份 DOCX,逐份通过校验器,使用 LibreOffice 转 PDF,再用 `pdftoppm` 输出 24 张 PNG。人工检查以 PNG 为准,XML 和校验器只作为辅助证据。脚本还用 `pdftotext -bbox-layout` 独立量测电子红头首页:份号字形落在 37 mm 版心首行附近,机关标志字形落在 35 mm 版心上边缘下方附近,并把带版头字段与不带字段的机关标志位置差控制在 0.8 mm 内;结果写入运行目录的 `logs/header-coordinates.log`。当前批次的文件清单保存在运行目录的 `manifest.tsv`每份文件的校验日志保存在 `logs/`
脚本实际生成 18 份 DOCX,逐份通过校验器,使用 LibreOffice 转 PDF,再用 `pdftoppm` 输出 37 个 PDF 页面和 PNG。人工检查以 PNG 为准,XML 和校验器只作为辅助证据。脚本还用 `pdftotext -bbox-layout` 独立量测电子红头首页:份号字形落在 37 mm 版心首行附近,机关标志字形落在 35 mm 版心上边缘下方附近,并把带版头字段与不带字段的机关标志位置差控制在 0.8 mm 内;结果写入运行目录的 `logs/header-coordinates.log`。当前批次的文件清单保存在提交的 [`assets/visual-audit/all-rules/manifest.tsv`](assets/visual-audit/all-rules/manifest.tsv),单页证据和总览图保存在同目录;每份文件的校验日志保存在运行目录的 `logs/`完整逐条矩阵见 [`gbt9704-screenshot-evidence.md`](gbt9704-screenshot-evidence.md)。
![各场景首页视觉核验](assets/visual-audit/first-pages.png)
![本次18个场景全览](assets/visual-audit/all-rules/contact-sheet.png)
![全部核验页缩略图](assets/visual-audit/all-pages.png)
![单双页页码与末页版记](assets/visual-audit/long-pages-colophon.png)
本批次的原始单页截图、场景清单和坐标日志见 [`assets/visual-audit/all-rules/`](assets/visual-audit/all-rules)
## 场景证据
@@ -30,9 +28,9 @@ bash tests/visual-audit.sh /tmp/gongwen-visual-audit-latest
| 长文版记 | `formal-long-1.png``formal-long-2.png``formal-long-3.png` | 单页右、双页左页码连续;末页版记贴近版心底部,粗细线顺序可见。 |
| 盖章/签名章位置 | `formal-seal-2.png``formal-signed-2.png` | 文字落款和日期位置已生成;实体印章或签名章图片没有被伪造,需在 Word/WPS 或打印环节完成。 |
| 信函 | `letter-1.png``letter-fields-1.png` | 上粗下细双红线、文号、标题和页末上细下粗双红线可见;带份号、密级、紧急程度的回归页也按第一条双线下顺序出现;不显示页码。 |
| 命令(令) | `command-1.png` | 机关标志令号下空二行、正文前再空二行可见。 |
| 纪要 | `minutes-1.png``minutes-2.png` | “出席/请假/列席”标签为黑体,人员为仿宋;人员标签左空二字,回行对齐。 |
| 横排表格 | `table-1.png` | 当前示例仍是纵向表格。这一张图是故意保留的缺口证据,不能把普通纵向表格宣称为标准横排表格。 |
| 命令(令) | `command-1.png` | 机关标志距版心上边缘20mm令号下空二行、正文前再空二行可见。 |
| 纪要 | `minutes-1.png``minutes-2.png` | “出席/请假/列席”标签为黑体,人员为仿宋;人员标签左空二字,回行对齐,且已移动到正文/附件说明下一行。 |
| 横排表格 | `horizontal-table-1.png``table-1.png` | `horizontal-table-1.png` 已实际渲染 A4 横向页面和225mm表格;`table-1.png` 保留纵向对照。单双页表头朝向仍需目标 Word/WPS 模板处理。 |
## 条款逐项对照
@@ -75,19 +73,22 @@ bash tests/visual-audit.sh /tmp/gongwen-visual-audit-latest
| 7.4.2 | 抄送机关字号、缩进、句号 | 截图通过 | `formal-long-3.png`;多行和主送移版记仍需结合机关模板。 |
| 7.4.3 | 印发机关、日期、印发和细线 | 截图通过 | `formal-long-3.png`;实际名称过长时仍需在目标软件确认。 |
| 7.5 | 页码字号、单双页位置、7 mm、附件连续 | 截图通过 | `formal-long-2.png``formal-long-3.png``formal-attachments-4.png`;空白页及特殊版记例外需人工复核。 |
| 8 | 横排表格 | 当前未自动化 | `table-1.png` 明确显示当前生成器输出纵向表格;横排 A4 表格、表头方向和页码方向必须使用 Word/WPS 专用横向模板。 |
| 8 | 横排表格 | 部分通过,表头方向未自动化 | `horizontal-table-1.png` 显示横向 A4 表格和标准页码;`table-1.png` 是纵向对照。单双页表头在订口/切口的方向切换尚未自动化,必须使用 Word/WPS 专用横向模板。 |
| 9 | 计量单位、标点、数字 | 结构通过,仍需人工 | 字段边界已校验;正文语义必须按引用标准审校。 |
| 10.1 | 信函格式 | 结构通过,仍需人工 | `letter-1.png` 显示 170 mm 上粗下细、下粗上细双线、文号、标题、无首页页码和无版记分隔线;可选份号、密级、紧急程度已在回归场景中按第一条双线下的顺序生成,联合行文仍需专用模板。 |
| 10.2 | 命令(令)格式 | 结构通过,仍需人工 | `command-1.png` 显示机关全称加“命令”、令号下空二行和正文下空二行;签名章和日期按7.3.5.3仍需人工。 |
| 10.3 | 纪要格式 | 截图通过 | `minutes-1.png``minutes-2.png`;机关标志 35 mm、人员标签字体两字起排已观察。 |
| 10.2 | 命令(令)格式 | 结构通过,仍需人工 | `command-1.png` 显示机关全称加“命令”、标志距版心上边缘20mm、令号下空二行和正文下空二行;签名章和日期按7.3.5.3仍需人工。 |
| 10.3 | 纪要格式 | 截图通过 | `minutes-1.png``minutes-2.png`;机关标志35mm、人员标签字体两字起排和人员名单在正文/附件说明下一行均已观察。 |
| 11 | 标准式样图 | 结构通过,仍需人工 | 本记录使用标准条文和实际截图;最终发文仍须对照本单位现行式样图或授权模板。 |
## 发现并修正的实现问题
1. 版头字段曾以普通段落排列,份号、密级、紧急程度会把电子红头机关标志向下推移,预印红头套打的留白也会被挤开。现在这些字段改为首面固定定位,版心中保留等高占位;`formal-digital-all-1.png``formal-preprinted-1.png` 已重新观察。
2. 红线下标题曾只依赖段前距,部分渲染器会把标题贴在线上。现在红线或纸上红线之后写入两个显式 28 磅空段落;截图能看到标题与红线之间的实际距离
3. 主送机关带不带末尾冒号时曾可能重复,现在归一化为一行全角冒号。
4. 纪要出席名单曾从版心左边缘开始,现改为左空二字并用悬挂缩进让回行对齐
2. 命令(令)分支曾把20mm误当成纸顶边距;现在恢复37mm天头,并从版心上边缘下20mm量测
3. 联合行文曾把联署机关拆成多行;现在主办机关在前、联署名称同一居中行,并在名称过长时自适应标志字号。
4. 纪要人员曾追加在附件和版记之后;现在紧跟正文或附件说明下一行
5. 红线下标题曾只依赖段前距,部分渲染器会把标题贴在线上。现在红线或纸上红线之后写入两个显式 28 磅空段落;截图能看到标题与红线之间的实际距离。
6. 主送机关带不带末尾冒号时曾可能重复,现在归一化为一行全角冒号。
7. 纪要出席名单曾从版心左边缘开始,现改为左空二字并用悬挂缩进让回行对齐。
## 仍然不能由本工具单独保证的事项
+52 -26
View File
@@ -108,7 +108,7 @@ function reportFontStatus(args) {
function usage() {
console.log(`Usage:
node generate_gongwen_docx.mjs --input source.md --output out.docx [--format ordinary|formal|letter|command|minutes] [--letterhead preprinted|digital] [--org 发文机关] [--doc-no 发文字号] [--title 标题] [--subtitle 副标题] [--to 主送机关] [--sender 落款] [--date 日期] [--attachment-note 附件说明] [--attachment-file attachment.md ...] [--page-number center|standard|none] [--require-standard-fonts]
node generate_gongwen_docx.mjs --input source.md --output out.docx [--format ordinary|formal|letter|command|minutes|horizontal-table] [--letterhead preprinted|digital] [--org 发文机关] [--joint-org 联署机关 ...] [--doc-no 发文字号] [--title 标题] [--subtitle 副标题] [--to 主送机关] [--sender 落款] [--date 日期] [--attachment-note 附件说明] [--attachment-file attachment.md ...] [--attachment-detached] [--page-number center|standard|none] [--require-standard-fonts]
Notes:
- Converts Markdown to a GB/T 9704-2012 page-layout DOCX.
@@ -124,8 +124,8 @@ Notes:
function parseArgs(argv) {
const args = {};
const flags = new Set(["help", "no-page-number", "no-page-numbers", "require-standard-fonts"]);
const repeatable = new Set(["attachment-file"]);
const flags = new Set(["help", "no-page-number", "no-page-numbers", "require-standard-fonts", "attachment-detached"]);
const repeatable = new Set(["attachment-file", "joint-org"]);
for (let i = 2; i < argv.length; i += 1) {
const key = argv[i];
if (!key.startsWith("--")) continue;
@@ -175,7 +175,8 @@ function run(text, opts = {}) {
const size = opts.size ?? "32";
const bold = opts.bold ? "<w:b/>" : "";
const color = opts.color ?? "000000";
return `<w:r><w:rPr><w:rFonts ${fontAttrs(opts)}/>${bold}<w:color w:val="${color}"/><w:sz w:val="${size}"/><w:szCs w:val="${size}"/><w:lang w:val="en-US" w:eastAsia="zh-CN"/></w:rPr><w:t xml:space="preserve">${esc(text)}</w:t></w:r>`;
const textXml = String(text ?? "").split("\n").map((line, index) => `${index ? "<w:br/>" : ""}<w:t xml:space="preserve">${esc(line)}</w:t>`).join("");
return `<w:r><w:rPr><w:rFonts ${fontAttrs(opts)}/>${bold}<w:color w:val="${color}"/><w:sz w:val="${size}"/><w:szCs w:val="${size}"/><w:lang w:val="en-US" w:eastAsia="zh-CN"/></w:rPr>${textXml}</w:r>`;
}
function paragraph(text, opts = {}) {
@@ -223,19 +224,19 @@ function floatingHeaderFieldParagraph(text, fontPreset, index) {
});
}
function floatingAgencyMarkParagraph(text) {
function floatingAgencyMarkParagraph(text, lineCount = 1, size = "56") {
return paragraph(text, {
align: "center",
indent: false,
fontPreset: "title",
size: "56",
size,
color: "FF0000",
before: AGENCY_FRAME_BEFORE,
frame: { hAnchor: "margin", vAnchor: "margin", xAlign: "center", y: AGENCY_FRAME_Y, w: CONTENT_W, h: 800 },
frame: { hAnchor: "margin", vAnchor: "margin", xAlign: "center", y: AGENCY_FRAME_Y, w: CONTENT_W, h: 800 * Math.max(1, lineCount) },
});
}
function agencyMarkFlowSpacer() {
function agencyMarkFlowSpacer(extraDxa = 0) {
return paragraph("\u00a0", {
align: "left",
indent: false,
@@ -243,7 +244,7 @@ function agencyMarkFlowSpacer() {
size: "2",
line: "560",
snapToGrid: false,
before: AGENCY_FLOW_BEFORE,
before: AGENCY_FLOW_BEFORE + extraDxa,
after: "0",
});
}
@@ -547,15 +548,15 @@ function blankGridLines(count = 2) {
return Array.from({ length: count }, () => blankGridLine());
}
function tableXml(headers, rows) {
function tableXml(headers, rows, width = CONTENT_W) {
const colCount = headers.length;
const colWidth = Math.floor(CONTENT_W / colCount);
const colWidth = Math.floor(width / colCount);
const grid = Array.from({ length: colCount }, () => `<w:gridCol w:w="${colWidth}"/>`).join("");
const tr = (cells, header = false) => {
const trPr = header ? "<w:trPr><w:tblHeader/></w:trPr>" : "";
return `<w:tr>${trPr}${cells.map((cell) => tableCell(cell, colWidth, header)).join("")}</w:tr>`;
};
return `<w:tbl><w:tblPr><w:tblW w:w="${CONTENT_W}" w:type="dxa"/><w:tblBorders><w:top w:val="single" w:sz="4" w:space="0" w:color="000000"/><w:left w:val="single" w:sz="4" w:space="0" w:color="000000"/><w:bottom w:val="single" w:sz="4" w:space="0" w:color="000000"/><w:right w:val="single" w:sz="4" w:space="0" w:color="000000"/><w:insideH w:val="single" w:sz="4" w:space="0" w:color="000000"/><w:insideV w:val="single" w:sz="4" w:space="0" w:color="000000"/></w:tblBorders><w:tblLook w:firstRow="1" w:lastRow="0" w:firstColumn="0" w:lastColumn="0" w:noHBand="0" w:noVBand="1"/></w:tblPr><w:tblGrid>${grid}</w:tblGrid>${tr(headers, true)}${rows.map((row) => tr(row)).join("")}</w:tbl>`;
return `<w:tbl><w:tblPr><w:tblW w:w="${width}" w:type="dxa"/><w:tblBorders><w:top w:val="single" w:sz="4" w:space="0" w:color="000000"/><w:left w:val="single" w:sz="4" w:space="0" w:color="000000"/><w:bottom w:val="single" w:sz="4" w:space="0" w:color="000000"/><w:right w:val="single" w:sz="4" w:space="0" w:color="000000"/><w:insideH w:val="single" w:sz="4" w:space="0" w:color="000000"/><w:insideV w:val="single" w:sz="4" w:space="0" w:color="000000"/></w:tblBorders><w:tblLook w:firstRow="1" w:lastRow="0" w:firstColumn="0" w:lastColumn="0" w:noHBand="0" w:noVBand="1"/></w:tblPr><w:tblGrid>${grid}</w:tblGrid>${tr(headers, true)}${rows.map((row) => tr(row)).join("")}</w:tbl>`;
}
function tableCell(text, width, header) {
@@ -715,14 +716,14 @@ function renderParagraph(block) {
return paragraph(block.text);
}
function attachmentPage(blocks, index, filePath) {
function attachmentPage(blocks, index, filePath, detachedDocNo = "") {
const attachmentBlocks = [...blocks];
let attachmentTitle = path.basename(filePath, path.extname(filePath));
if (attachmentBlocks[0]?.type === "heading") {
attachmentTitle = attachmentBlocks.shift().text;
}
const body = [
paragraph(`附件${index}`, { align: "left", indent: false, fontPreset: "h1", size: "32", pageBreakBefore: true, keepNext: true }),
paragraph(detachedDocNo ? `${detachedDocNo} 附件${index}` : `附件${index}`, { align: "left", indent: false, fontPreset: "h1", size: "32", pageBreakBefore: true, keepNext: true }),
emptyLine(),
titleParagraph(attachmentTitle),
];
@@ -738,18 +739,22 @@ function resolvePageNumberMode(args, format) {
if (args["no-page-number"] || args["no-page-numbers"]) return "none";
if (["center", "standard", "none"].includes(args["page-number"])) return args["page-number"];
if (format === "letter") return "none";
if (format === "formal" || format === "command" || format === "minutes") return "standard";
if (format === "formal" || format === "command" || format === "minutes" || format === "horizontal-table") return "standard";
return "center";
}
function buildDocument(blocks, args) {
let title = args.title;
const format = ["ordinary", "formal", "letter", "command", "minutes"].includes(args.format) ? args.format : "ordinary";
const format = ["ordinary", "formal", "letter", "command", "minutes", "horizontal-table"].includes(args.format) ? args.format : "ordinary";
const letterhead = args.letterhead === "digital" ? "digital" : "preprinted";
const pageNumberMode = resolvePageNumberMode(args, format);
const horizontalTable = format === "horizontal-table" || args["horizontal-table"] === true || args["horizontal-table"] === "true";
const horizontalContentWidth = mmToDxa(225);
const body = [];
let titleGapLines = 0;
const pageMarginTop = format === "letter" ? 1701 : format === "command" ? 1134 : 2098;
// All formats keep the standard 37 mm top white margin. Special formats
// offset their marks inside the type area, measured from that boundary.
const pageMarginTop = format === "letter" ? 1701 : 2098;
const sourceBlocks = [...blocks];
if (!title && sourceBlocks[0]?.type === "heading") {
title = sourceBlocks.shift().text;
@@ -779,7 +784,18 @@ function buildDocument(blocks, args) {
// GB/T 9704-2012 7.2.4 fixes the agency mark at 35 mm below the
// type-area top. Left-corner fields occupy their own column and must
// not move the centered mark upward.
body.push(floatingAgencyMarkParagraph(args.org));
const jointOrganizations = Array.isArray(args["joint-org"]) ? args["joint-org"].map((name) => String(name).trim()).filter(Boolean) : [];
const agencyNames = [String(args.org).trim(), ...jointOrganizations];
// GB/T 9704-2012 7.2.4.2 keeps joint agency names on one centered
// mark row, with the host agency first. Full-width spaces preserve a
// visible separation without creating extra logo rows that would move
// the document number and title downward.
const host = String(args.org).trim();
const jointText = jointOrganizations.length && host.endsWith("文件")
? [...[host.slice(0, -2), ...jointOrganizations], "文件"].join(" ")
: agencyNames.join(" ");
const agencyMarkSize = agencyNames.length > 1 ? (jointText.length > 16 ? "44" : "48") : "56";
body.push(floatingAgencyMarkParagraph(jointText, 1, agencyMarkSize));
body.push(agencyMarkFlowSpacer());
if (args["doc-no"]) body.push(...blankGridLines(2));
}
@@ -819,7 +835,9 @@ function buildDocument(blocks, args) {
if (!args.org) throw new Error("command format requires --org");
if (!/(?:命令|令)$/.test(String(args.org).trim())) throw new Error("command --org must end with“命令”or“令”");
if (!args["doc-no"]) throw new Error("command format requires --doc-no for the 令号");
body.push(paragraph(args.org, { align: "center", indent: false, fontPreset: "title", size: "44", color: "FF0000", before: "0", after: "0" }));
// GB/T 9704-2012 10.2 measures the command mark from the top edge of
// the type area, not from the paper edge: 37 mm + 20 mm from the page.
body.push(paragraph(args.org, { align: "center", indent: false, fontPreset: "title", size: "44", color: "FF0000", before: String(mmToDxa(20)), after: "0" }));
if (args["doc-no"]) {
body.push(...blankGridLines(2));
body.push(paragraph(args["doc-no"], { align: "center", indent: false, fontPreset: "body", size: "32", after: "0" }));
@@ -845,7 +863,7 @@ function buildDocument(blocks, args) {
continue;
}
if (block.type === "table") {
body.push(tableXml(block.headers, block.rows));
body.push(tableXml(block.headers, block.rows, horizontalTable ? horizontalContentWidth : CONTENT_W));
body.push(emptyLine());
continue;
}
@@ -855,6 +873,13 @@ function buildDocument(blocks, args) {
if (body.at(-1) === emptyLine()) body.pop();
body.push(attachmentNote(args["attachment-note"]));
}
// GB/T 9704-2012 10.3 places attendance information one line below the
// minutes body (or its attachment note), before any trailing signature or
// note material. Keeping it here also makes the screenshot evidence match
// the standard's “正文或附件说明下空一行” rule.
if (format === "minutes" && args.attendees) body.push(minutesPersonParagraph("出席", args.attendees, "560"));
if (format === "minutes" && args.absent) body.push(minutesPersonParagraph("请假", args.absent));
if (format === "minutes" && args.observers) body.push(minutesPersonParagraph("列席", args.observers));
if (args.sender || args.date) {
body.push(...(args["seal-mode"] === "signed" ? blankGridLines(2) : [emptyLine()]));
body.push(...signatureParagraphs(args));
@@ -867,13 +892,11 @@ function buildDocument(blocks, args) {
return absolutePath;
});
validateAttachmentConsistency(args["attachment-note"], absoluteAttachmentFiles);
if (args["attachment-detached"] && !args["doc-no"]) throw new Error("--attachment-detached requires --doc-no so the detached attachment can carry the document number");
absoluteAttachmentFiles.forEach((absolutePath, index) => {
const attachmentBlocks = parseMarkdown(fs.readFileSync(absolutePath, "utf8"));
body.push(attachmentPage(attachmentBlocks, index + 1, absolutePath));
body.push(attachmentPage(attachmentBlocks, index + 1, absolutePath, args["attachment-detached"] ? args["doc-no"] : ""));
});
if (format === "minutes" && args.attendees) body.push(minutesPersonParagraph("出席", args.attendees, "560"));
if (format === "minutes" && args.absent) body.push(minutesPersonParagraph("请假", args.absent));
if (format === "minutes" && args.observers) body.push(minutesPersonParagraph("列席", args.observers));
if (format !== "letter") body.push(...colophon(args));
const letterFooter = format === "letter" ? '<w:footerReference w:type="default" r:id="rIdLetterFooter"/>' : "";
const footerRefs = letterFooter || (pageNumberMode === "standard"
@@ -882,7 +905,10 @@ function buildDocument(blocks, args) {
? '<w:footerReference w:type="default" r:id="rIdFooterCenter"/>'
: "");
const footerDistance = format === "letter" || pageNumberMode === "standard" ? "1134" : "1588";
body.push(`<w:sectPr>${footerRefs}<w:pgSz w:w="${PAGE_W}" w:h="${PAGE_H}"/><w:pgMar w:top="${pageMarginTop}" w:right="${MARGIN.right}" w:bottom="${MARGIN.bottom}" w:left="${MARGIN.left}" w:header="720" w:footer="${footerDistance}" w:gutter="0"/><w:docGrid w:type="lines" w:linePitch="560"/></w:sectPr>`);
const pageSize = horizontalTable
? `<w:pgSz w:w="${PAGE_H}" w:h="${PAGE_W}" w:orient="landscape"/>`
: `<w:pgSz w:w="${PAGE_W}" w:h="${PAGE_H}"/>`;
body.push(`<w:sectPr>${footerRefs}${pageSize}<w:pgMar w:top="${pageMarginTop}" w:right="${MARGIN.right}" w:bottom="${MARGIN.bottom}" w:left="${MARGIN.left}" w:header="720" w:footer="${footerDistance}" w:gutter="0"/><w:docGrid w:type="lines" w:linePitch="560"/></w:sectPr>`);
return `<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" xmlns:v="urn:schemas-microsoft-com:vml"><w:body>${body.join("")}</w:body></w:document>`;
}
@@ -1003,7 +1029,7 @@ const md = fs.readFileSync(args.input, "utf8");
try {
const blocks = parseMarkdown(md);
const documentXml = buildDocument(blocks, args);
const format = ["ordinary", "formal", "letter", "command", "minutes"].includes(args.format) ? args.format : "ordinary";
const format = ["ordinary", "formal", "letter", "command", "minutes", "horizontal-table"].includes(args.format) ? args.format : "ordinary";
const pageNumberMode = resolvePageNumberMode(args, format);
writeDocx(path.resolve(args.output), documentXml, pageNumberMode, format);
console.log(`Generated: ${path.resolve(args.output)}`);
+12 -7
View File
@@ -6,7 +6,7 @@ import os from "node:os";
import path from "node:path";
function usage() {
console.log("Usage: node verify_gongwen_docx.mjs --input file.docx [--profile ordinary|formal|letter|command|minutes] [--letterhead preprinted|digital] [--require-standard-fonts]");
console.log("Usage: node verify_gongwen_docx.mjs --input file.docx [--profile ordinary|formal|letter|command|minutes|horizontal-table] [--letterhead preprinted|digital] [--require-standard-fonts]");
}
function parseArgs(argv) {
@@ -35,7 +35,7 @@ function paragraphs(xml) { return xml.match(/<w:p(?:\s[^>]*)?>[\s\S]*?<\/w:p>/g)
const args = parseArgs(process.argv);
if (!args.input || !fs.existsSync(args.input)) { usage(); process.exit(2); }
const profile = args.profile ?? "ordinary";
if (!["ordinary", "formal", "letter", "command", "minutes"].includes(profile)) { console.error("Unsupported profile."); process.exit(2); }
if (!["ordinary", "formal", "letter", "command", "minutes", "horizontal-table"].includes(profile)) { console.error("Unsupported profile."); process.exit(2); }
const doc = readPart(args.input, "word/document.xml");
const styles = readPart(args.input, "word/styles.xml");
@@ -71,9 +71,10 @@ const installedFonts = (() => {
const checks = [];
function check(name, ok, detail) { checks.push({ name, ok, detail }); }
check("A4 page size", /<w:pgSz[^>]*w:w="11906"[^>]*w:h="16838"/.test(doc), "210 mm x 297 mm");
const expectedTopMargin = profile === "letter" ? "1701" : profile === "command" ? "1134" : "2098";
check("Page margins", new RegExp(`<w:pgMar[^>]*w:top="${expectedTopMargin}"[^>]*w:right="1474"[^>]*w:bottom="1984"[^>]*w:left="1588"`).test(doc), `top ${expectedTopMargin === "2098" ? 37 : expectedTopMargin === "1701" ? 30 : 20} mm, bottom 35, left 28, right 26 mm`);
const horizontal = profile === "horizontal-table";
check("A4 page size", horizontal ? /<w:pgSz[^>]*w:w="16838"[^>]*w:h="11906"[^>]*w:orient="landscape"/.test(doc) : /<w:pgSz[^>]*w:w="11906"[^>]*w:h="16838"/.test(doc), horizontal ? "297 mm x 210 mm landscape table page" : "210 mm x 297 mm");
const expectedTopMargin = profile === "letter" ? "1701" : "2098";
check("Page margins", new RegExp(`<w:pgMar[^>]*w:top="${expectedTopMargin}"[^>]*w:right="1474"[^>]*w:bottom="1984"[^>]*w:left="1588"`).test(doc), `top ${expectedTopMargin === "2098" ? 37 : 30} mm, bottom 35, left 28, right 26 mm`);
check("Document grid", /<w:docGrid[^>]*w:linePitch="560"/.test(doc), "28 pt implementation grid");
const bodyFontStandard = /w:eastAsia="(?:仿宋|仿宋_GB2312|FangSong|FangSong_GB2312)"/.test(styles);
const bodyFontAvailable = bodyFontStandard || (!requireStandardFonts && /w:eastAsia="STFangsong"/.test(styles));
@@ -122,13 +123,16 @@ if (profile === "ordinary") {
check("Odd/even page positions", /w:jc w:val="right"/.test(footerOdd) && /w:jc w:val="left"/.test(footerEven), "odd right and even left");
check("Formal page-number distance", /w:footer="1134"/.test(doc), "page-number footer is 7 mm below the 225 mm type area");
}
if (horizontal) {
check("Horizontal table width", /<w:tblW w:w="12756" w:type="dxa"/.test(doc), "table uses the 225 mm landscape writing width");
}
if (profile === "formal") {
if (args.letterhead === "digital") {
check("Digital red head", /w:color w:val="FF0000"/.test(doc), "digital formal output contains requested red elements");
check("Red rule thickness", /height:0\.5mm/.test(doc), "header separator uses the recommended 0.35-0.5 mm range");
check("Red rule width", /style="width:156mm;height:0\.5mm"/.test(doc), "header separator spans the 156 mm type area");
const docParagraphs = paragraphs(doc);
const agencyIndex = docParagraphs.findIndex((p) => /w:sz w:val="56"/.test(p) && /w:color w:val="FF0000"/.test(p));
const agencyIndex = docParagraphs.findIndex((p) => /w:sz w:val="(?:56|48|44)"/.test(p) && /w:color w:val="FF0000"/.test(p));
// Keep this expected value independent from the generator's calibration
// constants. It is 35 mm in twentieths of a point, as required by 7.2.4.
const agencyOffsetFromTypeAreaTop = Math.round(35 * 56.692913);
@@ -140,7 +144,7 @@ if (profile === "formal") {
check("Digital red-head field frames", fixedHeaderFrames.every((value, index) => value === index * 560 || value === agencyOffsetFromTypeAreaTop), "left-corner fields use first-line grid positions without changing the agency mark anchor");
const docNoIndex = docParagraphs.findIndex((p) => /\d{4}[1-9]\d*/.test(textOf(p)));
const agencyToDocNo = agencyIndex >= 0 && docNoIndex > agencyIndex ? docParagraphs.slice(agencyIndex + 1, docNoIndex) : [];
const agencyFlowSpacer = agencyToDocNo.filter((p) => /w:before="1880"/.test(p));
const agencyFlowSpacer = agencyToDocNo.filter((p) => /w:before="[1-9]\d*"/.test(p) && /w:snapToGrid w:val="false"/.test(p));
const agencyGapLines = agencyToDocNo.slice(-2);
check("Agency mark to document number two blank lines", docNoIndex < 0 || (agencyFlowSpacer.length === 1 && agencyGapLines.length === 2 && agencyGapLines.every(isExplicitBlankGridLine)), "document number follows the fixed agency mark with two exact 28-point grid lines");
check("Two blank lines below red rule", hasExplicitTwoLineTitleGap, "title is preceded by two exact 28-point blank paragraphs below the red rule");
@@ -165,6 +169,7 @@ if (profile === "letter") {
if (profile === "command") {
const commandParagraphs = paragraphs(doc);
const commandAgencyIndex = commandParagraphs.findIndex((p) => /w:color w:val="FF0000"/.test(p));
check("Command agency position", commandAgencyIndex >= 0 && /w:before="1134"/.test(commandParagraphs[commandAgencyIndex]), "command mark starts 20 mm below the 37 mm type-area top");
const commandOrderIndex = commandParagraphs.findIndex((p, index) => index > commandAgencyIndex && /w:jc w:val="center"/.test(p) && /w:sz w:val="32"/.test(p) && !/w:color w:val="FF0000"/.test(p));
const agencyToOrder = commandAgencyIndex >= 0 && commandOrderIndex > commandAgencyIndex ? commandParagraphs.slice(commandAgencyIndex + 1, commandOrderIndex) : [];
check("Command agency-to-order spacing", commandOrderIndex < 0 || (agencyToOrder.length === 2 && agencyToOrder.every(isExplicitBlankGridLine)), "command agency mark is followed by two exact 28-point grid lines");
+43
View File
@@ -0,0 +1,43 @@
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
甲乙丙丁戊己庚辛壬癸子丑寅卯辰巳午未申酉戌亥天地玄黄宇宙
+9 -5
View File
@@ -20,11 +20,12 @@ function decodeHtml(value) {
function words(pdf) {
const html = execFileSync("pdftotext", ["-f", "1", "-l", "1", "-bbox-layout", pdf, "-"], { encoding: "utf8" });
return [...html.matchAll(/<word\s+[^>]*xMin="([0-9.]+)"[^>]*yMin="([0-9.]+)"[^>]*xMax="([0-9.]+)"[^>]*>([\s\S]*?)<\/word>/g)].map((match) => ({
return [...html.matchAll(/<word\s+[^>]*xMin="([0-9.]+)"[^>]*yMin="([0-9.]+)"[^>]*xMax="([0-9.]+)"[^>]*yMax="([0-9.]+)"[^>]*>([\s\S]*?)<\/word>/g)].map((match) => ({
xMinPt: Number(match[1]),
yMinPt: Number(match[2]),
xMaxPt: Number(match[3]),
text: decodeHtml(match[4]),
yMaxPt: Number(match[4]),
text: decodeHtml(match[5]),
}));
}
@@ -34,9 +35,12 @@ function findText(pdfWords, expected) {
// measurement is independent of that tokenization choice.
const rows = [];
for (const word of [...pdfWords].sort((left, right) => left.yMinPt - right.yMinPt || left.xMinPt - right.xMinPt)) {
const row = rows.find(({ yMinPt }) => Math.abs(yMinPt - word.yMinPt) <= 1.5);
if (row) row.words.push(word);
else rows.push({ yMinPt: word.yMinPt, words: [word] });
const row = rows.find(({ yMinPt, yMaxPt }) => word.yMinPt <= yMaxPt + 1.5 && word.yMaxPt >= yMinPt - 1.5);
if (row) {
row.words.push(word);
row.yMinPt = Math.min(row.yMinPt, word.yMinPt);
row.yMaxPt = Math.max(row.yMaxPt, word.yMaxPt);
} else rows.push({ yMinPt: word.yMinPt, yMaxPt: word.yMaxPt, words: [word] });
}
for (const row of rows) {
const ordered = row.words.sort((left, right) => left.xMinPt - right.xMinPt);
+20
View File
@@ -12,6 +12,7 @@ FIXTURE="$SKILL_DIR/tests/fixture.md"
"$NODE" --check "$GEN"
"$NODE" --check "$VERIFY"
"$NODE" --check "$SKILL_DIR/tests/measure-header-coordinates.mjs"
"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/layout.docx" --format ordinary --org "不应变红头的单位" --title "公文格式回归测试" --page-number center
"$NODE" "$VERIFY" --input "$TMP_DIR/layout.docx" --profile ordinary
@@ -74,6 +75,9 @@ unzip -p "$TMP_DIR/letter.docx" word/footerLetter.xml | grep -q 'w:fill="FF0000"
unzip -p "$TMP_DIR/with-attachment.docx" word/document.xml | grep -q '附件:1. 测试附件'
unzip -p "$TMP_DIR/with-attachment.docx" word/document.xml | grep -q 'w:pageBreakBefore'
"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/detached-attachment.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --doc-no "示例发〔20261号" --title "分离附件测试" --attachment-note "1. 测试附件" --attachment-file "$TMP_DIR/attachment.md" --attachment-detached
unzip -p "$TMP_DIR/detached-attachment.docx" word/document.xml | grep -q '示例发〔20261号 附件1'
if "$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/bad-attachment.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --doc-no "示例发〔20266号" --title "附件一致性测试" --attachment-note "1. 另一份附件" --attachment-file "$TMP_DIR/attachment.md" >"$TMP_DIR/bad-attachment.out" 2>&1; then
echo "mismatched attachment note unexpectedly succeeded" >&2
exit 1
@@ -91,6 +95,10 @@ grep -q 'GENERATOR ERROR:' "$TMP_DIR/bad-command.out"
"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/minutes.docx" --format minutes --org "示例机关纪要" --title "纪要正文" --attendees "张三、李四" --absent "王五"
"$NODE" "$VERIFY" --input "$TMP_DIR/minutes.docx" --profile minutes
minutes_xml=$(unzip -p "$TMP_DIR/minutes.docx" word/document.xml)
test "${minutes_xml%%出席:*}" != "$minutes_xml"
test "${minutes_xml%%w:sectPr*}" != "$minutes_xml"
test "${minutes_xml%%出席:*}" != "${minutes_xml%%w:sectPr*}"
"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/seal-layout.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --title "签章位置测试" --sender "示例机关" --date "2026年9月10日" --seal-mode seal
unzip -p "$TMP_DIR/seal-layout.docx" word/document.xml | grep -q 'w:right="1280"'
@@ -112,6 +120,18 @@ grep -q 'GENERATOR ERROR:' "$TMP_DIR/bad-date.out"
unzip -p "$TMP_DIR/letter-fields.docx" word/document.xml | grep -q '000007'
unzip -p "$TMP_DIR/letter-fields.docx" word/document.xml | grep -q '机密★3年'
"$NODE" "$GEN" --input "$SKILL_DIR/tests/fixtures/visual-horizontal.md" --output "$TMP_DIR/horizontal-table.docx" --format horizontal-table --title "横排表格测试" --page-number standard
"$NODE" "$VERIFY" --input "$TMP_DIR/horizontal-table.docx" --profile horizontal-table
unzip -p "$TMP_DIR/horizontal-table.docx" word/document.xml | grep -q 'w:orient="landscape"'
unzip -p "$TMP_DIR/horizontal-table.docx" word/document.xml | grep -q 'w:tblW w:w="12756"'
"$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/joint.docx" --format formal --letterhead digital --org "主办机关文件" --joint-org "协办机关" --doc-no "示例发〔20263号" --title "联合行文测试"
"$NODE" "$VERIFY" --input "$TMP_DIR/joint.docx" --profile formal --letterhead digital
unzip -p "$TMP_DIR/joint.docx" word/document.xml | grep -q '主办机关'
unzip -p "$TMP_DIR/joint.docx" word/document.xml | grep -q '协办机关'
unzip -p "$TMP_DIR/joint.docx" word/document.xml | grep -q '文件'
unzip -p "$TMP_DIR/joint.docx" word/document.xml | grep -q 'w:sz w:val="48"'
if "$NODE" "$GEN" --input "$FIXTURE" --output "$TMP_DIR/strict.docx" --format formal --letterhead digital --org "示例单位文件" --title "严格字体测试" --require-standard-fonts >"$TMP_DIR/strict.out" 2>&1; then
if ! fc-list -f '%{family}\n' | grep -Eq '方正小标宋简体|方正小标宋_GBK|FZXiaoBiaoSong'; then
echo "strict font mode unexpectedly succeeded without a small-standard-title font" >&2
+9 -2
View File
@@ -15,6 +15,7 @@ long="$skill_dir/tests/fixtures/visual-long.md"
attach_a="$skill_dir/tests/fixtures/visual-attachment-a.md"
attach_b="$skill_dir/tests/fixtures/visual-attachment-b.md"
horizontal="$skill_dir/tests/fixtures/visual-horizontal.md"
calibration="$skill_dir/tests/fixtures/visual-calibration.md"
node "$gen" --input "$all" --output "$out_dir/docx/ordinary.docx" --format ordinary --title "普通材料全要素实测" --page-number center
node "$gen" --input "$all" --output "$out_dir/docx/formal-digital-all.docx" --format formal --letterhead digital --copy-no 7 --secret "机密★3年" --urgent "特急" --org "示例单位文件" --doc-no "示例发〔20267号" --title "正式公文全要素实测" --to "各主送机关" --sender "示例单位" --date "2026年9月10日" --note "此件公开" --attachment-note "1. 数据清单;2. 实施计划。" --cc "办公室、财务部" --print-org "示例印发机关" --print-date "2026年9月10日" --page-number standard
@@ -22,25 +23,31 @@ node "$gen" --input "$all" --output "$out_dir/docx/formal-digital-upward.docx" -
node "$gen" --input "$all" --output "$out_dir/docx/formal-preprinted.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --copy-no 7 --secret "机密★3年" --urgent "特急" --doc-no "示例发〔20269号" --title "预印红头套打实测" --page-number standard
node "$gen" --input "$all" --output "$out_dir/docx/formal-seal.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --title "盖章公文实测" --sender "示例单位" --date "2026年9月10日" --seal-mode seal --page-number standard
node "$gen" --input "$all" --output "$out_dir/docx/formal-signed.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --title "签名章公文实测" --sender "示例单位" --date "2026年9月10日" --seal-mode signed --signer-title "主要负责人" --signer "张三" --page-number standard
node "$gen" --input "$all" --output "$out_dir/docx/formal-unsigned.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --title "不盖章公文实测" --sender "示例单位" --date "2026年9月10日" --page-number standard
node "$gen" --input "$all" --output "$out_dir/docx/formal-attachments.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --doc-no "示例发〔202610号" --title "多附件实测" --attachment-note "1. 数据清单;2. 实施计划。" --attachment-file "$attach_a" --attachment-file "$attach_b" --page-number standard
node "$gen" --input "$all" --output "$out_dir/docx/formal-attachments-detached.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --doc-no "示例发〔202615号" --title "分离装订附件实测" --attachment-note "1. 数据清单;2. 实施计划。" --attachment-file "$attach_a" --attachment-file "$attach_b" --attachment-detached --page-number standard
node "$gen" --input "$long" --output "$out_dir/docx/formal-long.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --doc-no "示例发〔202611号" --title "长文版记与单双页实测" --cc "办公室、财务部、审计部、采购部、法务部、信息中心" --print-org "示例印发机关" --print-date "2026年9月10日" --page-number standard
node "$gen" --input "$all" --output "$out_dir/docx/formal-joint.docx" --format formal --letterhead digital --org "主办机关文件" --joint-org "协办机关一" --joint-org "协办机关二" --doc-no "示例发〔202616号" --title "联合行文实测" --page-number standard
node "$gen" --input "$all" --output "$out_dir/docx/letter.docx" --format letter --org "示例机关" --doc-no "示例〔202612号" --title "信函格式实测" --page-number none
node "$gen" --input "$all" --output "$out_dir/docx/letter-fields.docx" --format letter --org "示例机关" --copy-no 7 --secret "机密★3年" --urgent "特急" --doc-no "示例〔202614号" --title "信函版头字段实测" --page-number none
node "$gen" --input "$all" --output "$out_dir/docx/command.docx" --format command --org "示例机关命令" --doc-no "第1号" --title "命令格式实测" --page-number standard
node "$gen" --input "$all" --output "$out_dir/docx/minutes.docx" --format minutes --org "示例机关纪要" --title "纪要格式实测" --attendees "张三(办公室)、李四(财务部)" --absent "王五(审计部)" --observers "赵六(采购部)" --page-number standard
node "$gen" --input "$horizontal" --output "$out_dir/docx/table.docx" --format formal --letterhead preprinted --letterhead-reserve-mm 72 --doc-no "示例发〔202613号" --title "表格版式实测" --page-number standard
node "$gen" --input "$horizontal" --output "$out_dir/docx/horizontal-table.docx" --format horizontal-table --title "横排表格实测" --page-number standard
node "$gen" --input "$calibration" --output "$out_dir/docx/ordinary-calibration.docx" --format ordinary --page-number center
for file in "$out_dir/docx"/*.docx; do
base=$(basename "$file" .docx)
case "$base" in
ordinary) profile=ordinary ;;
ordinary|ordinary-calibration) profile=ordinary ;;
letter|letter-fields) profile=letter ;;
horizontal-table) profile=horizontal-table ;;
command) profile=command ;;
minutes) profile=minutes ;;
*) profile=formal ;;
esac
case "$base" in
formal-digital-*) letterhead=digital ;;
formal-digital-*|formal-joint) letterhead=digital ;;
*) letterhead=preprinted ;;
esac
node "$verify" --input "$file" --profile "$profile" --letterhead "$letterhead" >"$out_dir/logs/$base.verify.log"