fix(reports): strip chart fences across newline styles

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
jesse-ux
2026-09-24 18:27:14 +08:00
co-authored by Claude Code
parent 5ec29eb736
commit fa8fef559b
7 changed files with 70 additions and 8 deletions
+1 -1
View File
@@ -11,7 +11,7 @@
## BUG-1020:CRLF 无 JSON 与旧导出逐字节合同冲突(2026-09-24)
- 2026-09-24 产品批准 D5 有限例外(D14),裁决阻塞解除,转为待领取修复单;以下原记录保留。
- 2026-09-24 产品批准 D5 有限例外(D14),裁决阻塞解除,修复已在本地完成;待定向门禁、staging 推送与部署 health 复核。
- 独立审查使用真实虚构golden只转换换行,正文下载保留22段图盘JSON,同时仍等于旧导出。根因是旧FENCE_RE仅接受语言标记后的LF,属于继承缺陷。
- D13只放开安全SVG,没有授权改变D5;未静默修复或声称全部验收通过。建议CRLF围栏输入作有限例外后最小修复,待裁决,见 `docs/tasks/TASK-report-chapter-export-fix-20260924.md`。
+5 -5
View File
@@ -13590,14 +13590,14 @@
## BUG-1020 | CRLF 普通报告下载保留图盘 JSON 围栏
- 状态:investigating(产品 2026-09-24 批准 D5 有限例外 D14,修复单待领取;未修复前不得标 resolved)
- 状态:resolved(本地修复,待 staging 门禁与部署 health 复核)
- 首次发现 / 最近更新:2026-09-24。
- 影响面:旧普通Markdown下载与新分块导出的正文组。
- 现象 / 触发:真实引擎虚构golden仅将LF换成CRLF,正文下载残留22段图盘JSON;新分盘组仍可提取22盘。
- 根因:report-chart-block.ts的FENCE_RE在jyotish-chart语言标记后仅匹配LF,stripReportChartBlocks共用它;旧测试验证LF剥除和CRLF字节等价,没有覆盖含真实围栏的CRLF无JSON。
- 修复:尚未实施。建议仅支持语言标记后的CRLF,保留其余换行、SVG与投影;需先授权D5只对错误JSON残留作有限例外,D13不是该授权。
- 验证:独立只读审查,转换golden后正文25块/残留22围栏/仍与旧导出byte-equal;相关105/105通过不能替代此未通过项。
- 防复发:待补真实golden CRLF/混合换行无JSON回归及冻结旧LF输出字节锁;禁止只比较两个共同改过剥离器的函数而声称历史输出不变。
- 修复:按产品D14有限例外,将语言标记及围栏尾部边界改为匹配`\\r?\\n`;不标准化整篇换行,不改SVG、投影、生成链或下载器。
- 验证:真实虚构reader golden的LF、CRLF、混合换行均输出0段`jyotish-chart`围栏、22个SVG且`ordinaryOutputLeaks`为空;LF普通导出SHA256固定为`3c52e8bfcfcb3ce3852bd5323867a69bc47d0f9aa821a34692172e7d61c6bcb8`。定向测试已通过;尚无部署health证据。
- 防复发:report-chart-block与report-export-blocks定向回归覆盖真实golden CRLF/混合输入及独立LF hash锁;禁止只比较两个共同修改后的helper而声称历史输出不变。
- 相关记录:BUG-999/1003;TASK-report-chapter-export-fix-20260924.md。
- 复发自:BUG-999同输出边界的未覆盖换行路径,继承剥离器缺陷,非本轮新增回归。
- 修复版本:未修复 / 未提交 / 未部署。
- 修复版本:本地待提交。
@@ -0,0 +1,31 @@
# PROGRESS · 报告 CRLF 围栏剥离修复(BUG-1020,2026-09-24)
## 当前结论
已按 D14 有限例外完成最小修复:图盘语言围栏的两个换行边界均接受 LF/CRLF;未标准化整篇正文。真实虚构 reader golden 的 LF、CRLF、混合换行均不再残留 JSON 围栏,LF 输出保留冻结 SHA。已提交 `d0cec97071f6979c59453d1a011c0f67639a61f9`;推送 staging 时远端认证失败,尚未取得 staging/deploy health 验收。
## 实现与验收
| 项 | 证据 | 结论 |
| --- | --- | --- |
| FENCE_RE | `frontend/src/lib/report-chart-block.ts` 仅将围栏边界 `\\n` 改为 `\\r?\\n` | 通过 |
| CRLF / 混合 golden | `report-chart-block.test.ts` 与 `report-export-blocks.test.ts` 使用真实 `report-density-fictional-reader.json`;输出 JSON 围栏 0、SVG 22、普通泄漏 0 | 通过(23/23) |
| LF 历史行为 | 普通导出 SHA256 `3c52e8bfcfcb3ce3852bd5323867a69bc47d0f9aa821a34692172e7d61c6bcb8` 冻结并断言 | 通过(23/23) |
| D5 范围 | CRLF 差异仅移除围栏及紧邻换行;未替换正文换行、未改 SVG/投影/生成链 | 通过 |
## 测试与环境
- 开工 pre-work check 已运行:远端可见性、适配器和碎片扫描通过;focused preflight 复现既有 `.workbuddy/skills/jyotish-vedic-astrology` 镜像路径断言失败,整体退出 1。未改测试、未伪造镜像。
- 定向报告测试 23/23 通过;`tsc --noEmit` 通过;`npm run lint` 0 errors、119 个既有 warnings。完整测试/build、真机/部署按清单如实记录。
## 既有断言三栏
| 原值 | 新值 | 原因 |
| --- | --- | --- |
| 旧 FENCE_RE 语言标记后仅匹配 `\\n` | 语言标记与围栏尾部均匹配 `\\r?\\n` | D14 仅允许删除 CRLF/混合输入残留 JSON 围栏;不改变 LF 既有输出 |
| 无真实 CRLF JSON 禁止断言 | 真实 golden 的 CRLF/混合换行输出 0 围栏、22 SVG、ordinaryOutputLeaks 为空 | BUG-1020 防复发;避免只比较两个共同修改后的 helper |
| 无冻结普通导出 hash | LF golden SHA256 固定为 `3c52e8bfcfcb3ce3852bd5323867a69bc47d0f9aa821a34692172e7d61c6bcb8` | D5 LF 逐字节锁 |
## 交付状态
已提交 `d0cec97071f6979c59453d1a011c0f67639a61f9`;按任务书同步 `origin/staging` 后尝试 `git push origin HEAD:staging`,远端返回 `Failed to authenticate user`,因此未推送、未部署,等待具备远端认证后重试。
@@ -27,7 +27,7 @@
- [ ] 成功触发下载后关闭并提示一次「已导出N块」;下载器可检测失败时保留抽屉/选择、不发成功提示。浏览器不能保证用户最终保存文件,不能把toast当磁盘持久化证明。
- [ ] 微信真机沿用系统浏览器提示,不下载、不关闭、不误报成功;切系统浏览器后可下载。
- [ ] 返回、打印、核对表打印前展开/打印后恢复、原始附录下载仍可用;普通导出无额外GET、服务端PDF或clipboard。
- [ ] CRLF真实golden无JSON回归在获准修复后重验,同时保留冻结旧LF输出逐字节比较,不能仅比较两个共享修改后helper的函数。
- [x] CRLF真实golden无JSON回归已在获准D14修复后加入;LF输出保留冻结SHA逐字节锁,未仅比较两个共享修改后helper的函数。staging/deploy health 仍待执行。
- [ ] 完整构建后核对 `/`、`/chart` Static,`/reports`、`/reports/[reportId]` 与基线动态标记一致;首页gzip±2%。
- [ ] staging发布后health deployment.gitCommit等于本轮含代码提交;未发布前不填通过。
+1 -1
View File
@@ -80,7 +80,7 @@ const SIGN_NUMBER: Record<string, number> = {
双鱼座: 12,
};
const FENCE_RE = /(?:\n[ \t]*)?```jyotish-chart\n[\s\S]*?```(?:\n[ \t]*)?/g;
const FENCE_RE = /(?:\r?\n[ \t]*)?```jyotish-chart\r?\n[\s\S]*?```(?:\r?\n[ \t]*)?/g;
export const reportChartBlockSchema = z.strictObject({
version: z.literal(1),
+12
View File
@@ -100,6 +100,18 @@ test("stripReportChartBlocks removes fences and keeps engine SVGs", () => {
assert.equal((stripped.match(/<svg/g) ?? []).length, (markdown.match(/<svg/g) ?? []).length);
});
test("stripReportChartBlocks removes chart fences from CRLF and mixed-newline input", () => {
const lf = wrapMarkdown(fixture.blocks);
for (const markdown of [
lf.replace(/\n/g, "\r\n"),
lf.split("\n").map((line, index) => index % 2 ? `${line}\r` : line).join("\n"),
]) {
const stripped = stripReportChartBlocks(markdown);
assert.doesNotMatch(stripped, /```jyotish-chart/);
assert.equal((stripped.match(/<svg/g) ?? []).length, (markdown.match(/<svg/g) ?? []).length);
}
});
test("house polygons fill the square without overlapping interiors", () => {
const total = HOUSE_NUMBERS.reduce((sum, houseNumber) => sum + polygonArea(HOUSE_POLYGONS[houseNumber]), 0);
assert.ok(Math.abs(total - CHART_SIZE * CHART_SIZE) < 1e-6, `area ${total}`);
@@ -1,4 +1,5 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import test from "node:test";
import { buildReportExportBlocks, chartBlockText, exportFilename, factTableToMarkdown, joinExportBlocks, presetSelection } from "../src/lib/report-export-blocks";
@@ -54,6 +55,24 @@ test("export counts actual Unicode characters, not bytes or estimates", () => {
test("full golden body is byte identical to the retired ordinary export", () => {
assert.equal(joinExportBlocks(body), ordinaryReportDownloadMarkdown(fixture.markdown));
});
test("LF golden ordinary export keeps its frozen output hash", () => {
const output = ordinaryReportDownloadMarkdown(fixture.markdown);
assert.equal(createHash("sha256").update(output).digest("hex"), "3c52e8bfcfcb3ce3852bd5323867a69bc47d0f9aa821a34692172e7d61c6bcb8");
});
test("CRLF and mixed-newline golden exports remove every JSON chart fence", () => {
const variants = [
fixture.markdown.replace(/\n/g, "\r\n"),
fixture.markdown.split("\n").map((line: string, index: number) => index % 2 ? `${line}\r` : line).join("\n"),
];
for (const markdown of variants) {
const output = ordinaryReportDownloadMarkdown(markdown);
assert.equal((output.match(/```jyotish-chart/g) ?? []).length, 0);
assert.equal((output.match(/<svg\b/gi) ?? []).length, 22);
assert.deepEqual(ordinaryOutputLeaks(output), []);
}
});
test("all-group export adds charts and tables after unchanged ordinary body", () => {
assert.ok(joinExportBlocks(blocks).startsWith(ordinaryReportDownloadMarkdown(fixture.markdown) + "\n\n####"));
assert.notEqual(joinExportBlocks(blocks), joinExportBlocks(body));