fix(report): make density facts readable and printable
Independent Staging Quality Gate / validate (push) Failing after 11m4s
Independent Staging Quality Gate / publish (push) Skipped

Unify reader cleanup rules, lock writer table guards, and register the exact fictional timestamp collision. Preserve existing ordinary-report safety contracts and source-data gaps.

Validation: report Node 165/165, final safety 29/29, Python 101/101, Chrome 28/28; both PDFs retain all 130 rows. Full Node 3704 tests with the same 91 baseline failures. Privacy test: 62 passed, 1 failed due to 17 protected-file READ_ERRORs; not a green gate. Build, DB, manual checklist and controlled-login gaps remain documented. User explicitly authorized staging push with these gaps disclosed.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
jesse-ux
2026-09-23 15:26:25 +08:00
co-authored by Claude Code
parent 0d37bec15a
commit a12f2c0d26
27 changed files with 1220 additions and 210 deletions
@@ -0,0 +1,84 @@
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { readFileSync } from "node:fs";
import test from "node:test";
import { fileURLToPath } from "node:url";
import { cleanReaderAppendixMarkdown } from "../src/lib/reader-appendix-language.ts";
const golden = JSON.parse(readFileSync(new URL("./fixtures/report-density-fictional-reader.json", import.meta.url), "utf8"));
// Vocabulary-only cases, not invented calculation fixtures.
const pageCases = [
"PL9 第 42 页", "PL9第42页", "PL9.pdf 第 42 页", "PL9.pdf第42页后文",
"参见PL9 第 42 页。", "PL9 第 43–44 页", "PL9 第 40 / 48 页",
"PL9 pages 99-100", "PL9 p-42", "PL9第42页中的原始字段",
];
const chartFence = ' ```jyotish-chart\r\n{"title":"PL9 第 42 页 parameter_sensitive","id":"D1"}\r\n ```\r\n';
const copyCases = [
...pageCases,
"中文parameter_sensitive中文 PyJHora中文 cmd_full_reading后文",
"some_parameter_sensitive_field PL99 XPL9 PL9_suffix",
"The blocked / executed / available / computed result is not a claim.",
"| blocked | `executed` | available | computed | sign_cn | 12.50 |\n",
`parameter_sensitive\r\n${chartFence}| parameter_sensitive |\r\n`,
];
function pythonClean(inputs: string[]): string[] {
const script = [
"import json, sys",
"from scripts.reader_appendix_language import clean_reader_appendix_markdown",
"sys.stdout.write(json.dumps([clean_reader_appendix_markdown(value) for value in json.load(sys.stdin)], ensure_ascii=False))",
].join("\n");
const result = spawnSync(process.env.PYTHON ?? (process.platform === "win32" ? "python" : "python3"), ["-c", script], {
cwd: fileURLToPath(new URL("../../", import.meta.url)),
encoding: "utf8", input: JSON.stringify(inputs), maxBuffer: 16 * 1024 * 1024,
env: { ...process.env, PYTHONIOENCODING: "utf-8", PYTHONDONTWRITEBYTECODE: "1" },
});
assert.equal(result.status, 0, `${result.error ?? ""}\n${result.stderr}`);
return JSON.parse(result.stdout);
}
test("appendix Python and TS share byte-identical golden and Chinese-boundary output", () => {
assert.equal(golden.fixtureProvenance.fictional, true);
const inputs = [golden.markdown, golden.reader_dasha_applicability, ...copyCases] as string[];
const python = pythonClean(inputs);
for (const [index, input] of inputs.entries()) {
const ts = cleanReaderAppendixMarkdown(input);
assert.deepEqual(Buffer.from(python[index], "utf8"), Buffer.from(ts, "utf8"), `case ${index}`);
assert.equal(cleanReaderAppendixMarkdown(ts), ts, `idempotent case ${index}`);
}
assert.doesNotMatch(python[0], /第\s*\d+(?:\s*[–—/-]\s*\d+)*\s*页/);
assert.doesNotMatch(cleanReaderAppendixMarkdown(golden.markdown), /第\s*\d+(?:\s*[–—/-]\s*\d+)*\s*页/);
});
test("appendix page references disappear without swallowing adjacent Chinese prose", () => {
for (const source of pageCases) {
const output = cleanReaderAppendixMarkdown(source);
assert.doesNotMatch(output, /PL9|第\s*\d+|pages?\s*\d+|p-42/);
}
assert.equal(cleanReaderAppendixMarkdown("参见PL9第42页中的原始字段"), "参见外部参照资料中的原始字段");
assert.equal(cleanReaderAppendixMarkdown("PL9.pdf第42页后文"), "外部参照资料后文");
assert.equal(cleanReaderAppendixMarkdown("some_parameter_sensitive_field PL99 XPL9"), "some_parameter_sensitive_field PL99 外部参照资料");
});
test("appendix golden preserves line and table shape, field names, numeric cells and chart bytes", () => {
const raw = golden.markdown as string;
const clean = cleanReaderAppendixMarkdown(raw);
const sourceLines = raw.split("\n");
const cleanLines = clean.split("\n");
assert.equal(cleanLines.length, sourceLines.length);
for (const [index, line] of sourceLines.entries()) {
if (!line.startsWith("|")) continue;
const cells = line.split("|");
const outputCells = cleanLines[index].split("|");
assert.equal(outputCells.length, cells.length, `table columns at line ${index}`);
cells.forEach((cell, column) => {
if (/^\s*-?\d+(?:\.\d+)?\s*$/.test(cell)) assert.equal(outputCells[column], cell);
});
}
for (const field of ["sign_cn", "degree_in_sign", "nakshatra_lord"]) {
assert.equal(clean.split(field).length, raw.split(field).length);
}
const fences = (text: string) => [...text.matchAll(/^[ \t]*```jyotish-chart[^\n]*\n[\s\S]*?^[ \t]*```[^\n]*(?:\n|$)/gm)].map(match => Buffer.from(match[0]));
assert.deepEqual(fences(clean), fences(raw));
assert.deepEqual(fences(cleanReaderAppendixMarkdown(copyCases.at(-1)!)), [Buffer.from(chartFence)]);
});