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,57 @@
import assert from "node:assert/strict";
import test from "node:test";
import { Agent } from "@mastra/core/agent";
import { validatePersonalReportAgentOutputAgainstPlan } from "../src/lib/personal-report-generation.ts";
import type { PersonalReportSectionPlan } from "../src/lib/personal-report-plan.ts";
import type { ReportEvidenceBundleV2 } from "../src/lib/report-evidence-bundle-v2.ts";
import { createPersonalReportAgent, type PersonalReportAgentOutput } from "../src/mastra/personal-report.ts";
// Writer-boundary unit inputs contain no astrological facts or fabricated engine payload.
const plan: PersonalReportSectionPlan = { schemaVersion: "personal_report_section_plan.v1", depth: "standard", sections: [] };
const bundle = { claimCards: [] } as unknown as ReportEvidenceBundleV2;
function prose(): PersonalReportAgentOutput {
return {
executiveSummary: { headline: "先把已有安排理清", summary: "把要做的事写下来,再安排下一步。", priorities: ["核对现有安排"] },
thematicNarrative: [],
};
}
const forbiddenCases = {
"Markdown table": () => ({ ...prose(), executiveSummary: { ...prose().executiveSummary, summary: "| 项目 | 说明 |\n| --- | --- |\n| 安排 | 待核对 |" } }),
"HTML table in nested array": () => ({ ...prose(), executiveSummary: { ...prose().executiveSummary, priorities: ['<TABLE class="facts"><tr><td>待核对</td></tr></TABLE>'] } }),
"factTables top-level key": () => ({ ...prose(), factTables: [] }),
"factTables nested key": () => ({ ...prose(), executiveSummary: { ...prose().executiveSummary, factTables: [] } }),
};
for (const [name, output] of Object.entries(forbiddenCases)) {
test(`writer rejects ${name} with the exact fact-table error code`, () => {
assert.throws(() => validatePersonalReportAgentOutputAgainstPlan(output(), plan, bundle), {
name: "Error", message: "report_writer_fact_tables_forbidden",
});
});
}
test("writer accepts normal prose unchanged, including a non-table inline pipe", () => {
const output = { ...prose(), executiveSummary: {
...prose().executiveSummary,
summary: prose().executiveSummary.summary + "安排 A | 安排 B 只是两种写法,不是表格。",
} };
assert.strictEqual(validatePersonalReportAgentOutputAgainstPlan(output, plan, bundle), output);
});
test("actual writer agent instructions prohibit tables and assign them to the system", async (t) => {
let calls = 0;
t.mock.method(Agent.prototype, "generate", async function (this: Agent) {
calls++;
const instructions = await this.getInstructions();
assert.match(String(instructions), /不要输出表格,表格由系统提供/);
assert.match(String(instructions), /Markdown 表格、HTML table 或 factTables 字段/);
return { object: prose(), usage: { inputTokens: 1, outputTokens: 1 }, finishReason: "stop" };
});
const writer = createPersonalReportAgent({
id: "writer-table-contract", label: "Writer test", description: "Writer test", creditCost: 0,
isDefault: false, mode: "openai",
model: { specificationVersion: "v2", provider: "writer-test", modelId: "writer-test" } as never,
});
assert.deepEqual(await writer.generate(bundle, plan), prose());
assert.equal(calls, 1);
});
@@ -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)]);
});
@@ -0,0 +1,55 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import test from "node:test";
import { bindFactTablePrint } from "../src/lib/report-fact-table-print";
function setup() {
const details = [{ open: false }, { open: true }, { open: false }];
const outside = { open: false };
const events = new EventTarget();
const root = { querySelectorAll(selector: string) { assert.equal(selector, "details"); return details; } };
const cleanup = bindFactTablePrint(root as unknown as HTMLElement, events);
return { details, outside, events, cleanup };
}
test("fact table printing expands all report groups and restores mixed disclosure state", () => {
const { details, outside, events, cleanup } = setup();
assert.deepEqual(details.map(item => item.open), [false, true, false]);
events.dispatchEvent(new Event("beforeprint"));
assert.deepEqual(details.map(item => item.open), [true, true, true]);
assert.equal(outside.open, false);
events.dispatchEvent(new Event("afterprint"));
assert.deepEqual(details.map(item => item.open), [false, true, false]);
cleanup();
});
test("fact table printing preserves the first snapshot across repeated beforeprint events", () => {
const { details, events, cleanup } = setup();
events.dispatchEvent(new Event("beforeprint"));
events.dispatchEvent(new Event("beforeprint"));
events.dispatchEvent(new Event("afterprint"));
assert.deepEqual(details.map(item => item.open), [false, true, false]);
details[0].open = true;
events.dispatchEvent(new Event("beforeprint"));
events.dispatchEvent(new Event("afterprint"));
assert.deepEqual(details.map(item => item.open), [true, true, false]);
cleanup();
});
test("fact table print cleanup restores pending state and removes both event listeners", () => {
const { details, events, cleanup } = setup();
events.dispatchEvent(new Event("afterprint"));
events.dispatchEvent(new Event("beforeprint"));
cleanup();
assert.deepEqual(details.map(item => item.open), [false, true, false]);
events.dispatchEvent(new Event("beforeprint"));
assert.deepEqual(details.map(item => item.open), [false, true, false]);
});
test("fact table print styles remove horizontal clipping and repeat table headers", () => {
const css = readFileSync(new URL("../src/app/globals.css", import.meta.url), "utf8");
const print = css.slice(css.indexOf("@media print"), css.indexOf("@theme inline"));
assert.match(print, /\.personal-report-fact-table-scroll\s*\{[^}]*overflow: visible !important/);
assert.match(print, /\.personal-report-fact-tables table\s*\{[^}]*table-layout: fixed/);
assert.match(print, /\.personal-report-fact-tables thead\s*\{[^}]*display: table-header-group/);
});
+114 -6
View File
@@ -12,6 +12,7 @@ import { projectOrdinaryReportMarkdown } from "../src/lib/report-public-projecti
const packet = JSON.parse(readFileSync(new URL("./fixtures/report-density-fictional-engine.json", import.meta.url), "utf8"));
const resolve = (path: string) => path.replace(/\[(\d+)\]/g, ".$1").split(".").reduce((value, key) => value?.[key], packet);
const fixed = (value: number) => value.toFixed(2);
test("density golden: sixteen real charts have complete houses and nine occupants", () => {
const charts = assembleLongformCharts(packet, "ev-cover-longform");
@@ -43,18 +44,35 @@ test("density golden: eight server groups contain only unchanged engine leaves",
const tables = assembleReportFactTables(packet);
assert.equal(tables.length, 8);
for (const table of tables) {
assert.ok(table.rows.length > 0, table.id);
for (const row of table.rows) assert.deepEqual(row.cells[1], resolve(row.sourcePath), row.sourcePath);
assert.ok(table.rows.length > 0 || table.subtables?.some(child => child.rows.length > 0), table.id);
for (const grid of [table, ...(table.subtables ?? [])]) {
assert.notEqual(resolve(grid.sourcePath), undefined, grid.sourcePath);
for (const row of grid.rows) assert.notEqual(resolve(row.sourcePath), undefined, row.sourcePath);
}
}
assert.equal(tables.find(t => t.id === "shadbala")?.rows.length, 42);
assert.equal(tables.find(t => t.id === "ashtakavarga")?.rows.length, 108);
assert.ok(tables.find(t => t.id === "annual")?.rows.some(row => row.sourcePath.includes("advanced_systems.sahams")));
const shadbala = tables.find(t => t.id === "shadbala")!;
assert.equal(shadbala.rows.length, 7);
for (const row of shadbala.rows) {
const raw = resolve(row.sourcePath);
assert.deepEqual(row.cells.slice(1), [raw.sthana_bala.total, raw.dig_bala, raw.kala_bala.total, raw.chesta_bala, raw.naisargika_bala, raw.drik_bala, raw.total_virupas].map(fixed));
}
const ashtakavarga = tables.find(t => t.id === "ashtakavarga")!;
assert.equal(ashtakavarga.rows.length, 12);
for (const row of ashtakavarga.rows) assert.equal(row.cells[1], resolve(row.sourcePath).toFixed(2));
assert.equal(ashtakavarga.subtables?.[0].rows.length, 8);
for (const row of ashtakavarga.subtables![0].rows) assert.deepEqual(row.cells.slice(1), resolve(row.sourcePath).bindus.map(fixed));
const annual = tables.find(t => t.id === "annual")!;
assert.equal(annual.rows.length, 0, "golden annual chart has no planet positions");
assert.ok(annual.subtables?.[0].rows.every(row => row.sourcePath.includes("annual_tajika_pack.sahams.data")));
assert.equal(annual.subtables?.[0].rows.length, 36);
});
test("density golden: Sade Sati reports missing cycle dates without inventing them", () => {
const table = assembleReportFactTables(packet).find(t => t.id === "sade_sati")!;
assert.match(table.note, /尚无三轮起止日期/);
assert.equal(table.rows.length, 6);
assert.equal(table.rows.length, 3);
assert.deepEqual(table.rows.map(row => row.cells[0]), ["上升", "高峰", "下降"]);
assert.match(table.note, /当前是否在进行中:否/);
assert.equal(table.claimStatus, "parameter_sensitive");
assert.ok(table.rows.every(row => !/start|end|date/.test(row.sourcePath)));
});
@@ -68,3 +86,93 @@ test("density facts preserve declared limitations and reject malformed provenanc
assert.equal(reportFactTablesSchema.safeParse(tables).success, false);
assert.deepEqual(assembleReportFactTables(undefined), []);
});
test("density visible columns never expose engine paths or excessive precision", () => {
for (const table of assembleReportFactTables(packet)) {
for (const grid of [table, ...(table.subtables ?? [])]) {
const visible = [grid.title, grid.note, ...grid.columns, ...grid.rows.flatMap(row => row.cells)].join("\n");
assert.doesNotMatch(visible, /[A-Za-z]+_[A-Za-z]+|\[\d+\]|\d+\.\d{3,}|jd_ut|dt_ut|\.data\./);
assert.ok(grid.rows.every(row => row.cells.every(cell => cell === null || typeof cell === "string")));
}
}
});
test("density golden: full years and birth balance have distinct sourced meanings", () => {
const table = assembleReportFactTables(packet).find(t => t.id === "vimshottari")!;
assert.equal(table.rows.length, 9);
assert.deepEqual(table.columns, ["主运", "起", "止", "年数", "当前"]);
assert.equal(resolve(table.rows[0].sourcePath).full_years, 18);
assert.equal(resolve(table.rows[0].sourcePath).balance_years, 0.72);
assert.equal(table.rows[0].cells[3], "18.00");
assert.match(table.note, /出生时.*剩余 0\.72 年/);
for (const row of table.rows) {
const raw = resolve(row.sourcePath);
assert.deepEqual(row.cells, [raw.lord_cn, raw.start, raw.end, fixed(raw.full_years), raw.is_current ? "是" : "否"]);
}
assert.deepEqual(table.subtables?.map(child => child.id), ["antardasha", "pratyantar"]);
for (const child of table.subtables!) {
assert.equal(resolve(child.sourcePath).is_current, true);
assert.equal(child.rows.length, 9);
for (const row of child.rows) {
const raw = resolve(row.sourcePath);
assert.deepEqual(row.cells, [raw.lord_cn, raw.start, raw.end, raw.is_current ? "是" : "否"]);
}
}
const altered = structuredClone(packet);
delete altered.worksheets.timing_and_predictive_systems.dasha.timeline[0].full_years;
assert.equal(assembleReportFactTables(altered).find(t => t.id === "vimshottari")!.rows[0].cells[3], null, "neither dates nor balance nor standard constants may supply missing duration");
});
test("density golden: state and position cells retain actual source values", () => {
const tables = assembleReportFactTables(packet);
const states = tables.find(t => t.id === "avasthas")!;
for (const row of states.rows) {
const raw = resolve(row.sourcePath);
assert.deepEqual(row.cells.slice(1), ["Bala", "Jagrat", "Deeptadi", "Lajjitadi", "Shayanadi"].map(key => raw.avasthas[key].state));
}
for (const grid of [tables.find(t => t.id === "special_points")!, ...tables.find(t => t.id === "annual")!.subtables!]) {
for (const row of grid.rows) {
const raw = resolve(row.sourcePath);
assert.equal(row.cells[2], fixed(raw.degree_in_sign ?? raw.sign_degree));
}
}
const altered = structuredClone(packet);
const strengths = altered.worksheets.strengths_and_scores;
delete strengths.shadbala.planets.Sun.total_virupas;
delete altered.worksheets.divisional_and_special_charts.upagrahas.raw.Kaala.degree_in_sign;
const output = assembleReportFactTables(altered);
assert.equal(output.find(t => t.id === "shadbala")!.rows[0].cells[7], null, "do not sum components");
assert.equal(output.find(t => t.id === "special_points")!.rows[0].cells[2], null, "do not derive sign degrees from longitude");
});
test("density fact subtable schema accepts legacy snapshots and rejects malformed new grids", () => {
const tables = assembleReportFactTables(packet);
const legacy = tables.map(source => {
const table = { ...source };
delete table.subtables;
return { ...table, columns: ["原始字段", "数值 / 状态"], rows: [{ sourcePath: table.sourcePath, cells: ["legacy", 1] }] };
});
assert.equal(reportFactTablesSchema.safeParse(legacy).success, true);
const invalid = structuredClone(tables);
invalid[0].subtables![0].rows[0].cells.pop();
assert.equal(reportFactTablesSchema.safeParse(invalid).success, false);
const wrongSource = structuredClone(tables);
wrongSource[0].subtables![0].sourcePath = "worksheets.other";
assert.equal(reportFactTablesSchema.safeParse(wrongSource).success, false);
const duplicates = structuredClone(tables);
duplicates[0].subtables!.push(duplicates[0].subtables![0]);
assert.equal(reportFactTablesSchema.safeParse(duplicates).success, false);
});
test("density blocked sources stay blocked and missing annual positions are not invented", () => {
const altered = structuredClone(packet);
altered.worksheets.strengths_and_scores.shadbala.claimStatus = "blocked";
altered.worksheets.timing_and_predictive_systems.annual_tajika_pack.sahams.status = "blocked";
const tables = assembleReportFactTables(altered);
assert.equal(tables.find(t => t.id === "shadbala")!.claimStatus, "blocked");
assert.deepEqual(tables.find(t => t.id === "shadbala")!.rows, []);
assert.deepEqual(tables.find(t => t.id === "annual")!.rows, []);
assert.deepEqual(tables.find(t => t.id === "annual")!.subtables![0].rows, []);
assert.match(tables.find(t => t.id === "annual")!.note, /年度上升:天蝎;年龄:26\.00/);
assert.match(tables.find(t => t.id === "annual")!.note, /未返回年度行星位置,不补算/);
});