fix(report): use the upstream reader edition for new longform bodies

New reports request reader_main from upstream origin/main 23b9609e.
KP and transit no longer copy an empty vars() dict, solar returns keep
birth_asc_sign_idx, and ordinary projection deletes internal lines whole.
This commit is contained in:
jesse-ux
2026-09-25 02:19:32 +08:00
parent 514951d708
commit 2a1f5f0fc9
22 changed files with 3126 additions and 65 deletions
@@ -148,7 +148,7 @@ async function fetchLongformMarkdown(input: Readonly<{
const upstream = await input.fetchImpl(`${input.apiBase.replace(/\/$/, "")}/api/professional_report_reference`, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify({ ...input.payload, include_fact_tables: true }),
body: JSON.stringify({ ...input.payload, include_fact_tables: true, edition: "reader_main" }),
cache: "no-store",
signal,
});
+44 -9
View File
@@ -184,6 +184,20 @@ export const ORDINARY_OUTPUT_LEAK_PATTERNS: readonly RegExp[] = [
/<\s*object\b/i,
/<\s*embed\b/i,
/<\s*img\b/i,
/\bblocked\b/i,
/\bexecuted\b/i,
/\bpartial_verified\b/i,
/\bmissing_in_local\b/i,
/\bproducer\b/i,
/\bparity\b/i,
/PL9\s*第\s*\d+\s*页/,
/full-report:\/\//,
/\bpl9_[A-Za-z0-9_]+\b/i,
/\b[A-Za-z][A-Za-z0-9]*(?:_[A-Za-z0-9]+)*_unavailable\b/,
/\b[A-Za-z][A-Za-z0-9]*(?:_[A-Za-z0-9]+)*_(?:pack|packet)\b/i,
/object has no attribute/,
/\bTraceback\b/,
/(?:\b[A-Za-z]{2,}(?:'[A-Za-z]+)?\b\s+){11}\b[A-Za-z]{2,}(?:'[A-Za-z]+)?\b/,
];
export function ordinaryOutputLeaks(text: string): string[] {
@@ -319,15 +333,40 @@ function limitationsFromSignals(input: ChatExportInput): string[] {
return notes;
}
function adjacentInternalTableIndexes(blocks: Block[]): Set<number> {
const drop = new Set<number>();
blocks.forEach((block, index) => {
if (block.type !== "table" || !tableIsInternal(block.text)) return;
drop.add(index);
let cursor = index - 1;
while (cursor >= 0 && blocks[cursor]?.type === "blank") cursor -= 1;
if (cursor >= 0 && (blocks[cursor]?.type === "paragraph" || blocks[cursor]?.type === "list")) {
drop.add(cursor);
cursor -= 1;
while (cursor >= 0 && blocks[cursor]?.type === "blank") cursor -= 1;
}
if (cursor >= 0 && blocks[cursor]?.type === "heading") drop.add(cursor);
});
return drop;
}
function analyzeOrdinaryReportMarkdown(markdown: string): Analysis {
if (!markdown) return { prose: markdown, limitations: [], changed: false };
const blocks = parseBlocks(markdown);
const dropWithTable = adjacentInternalTableIndexes(blocks);
const limitations = new Set<string>();
const kept: string[] = [];
let changed = false;
let skipUntilLevel: number | null = null;
for (const block of blocks) {
for (let index = 0; index < blocks.length; index += 1) {
const block = blocks[index];
if (!block) continue;
if (dropWithTable.has(index)) {
changed = true;
collectLimitations(block.raw, limitations);
continue;
}
if (block.type === "heading") {
const level = block.level ?? 1;
if (skipUntilLevel !== null && level <= skipUntilLevel) skipUntilLevel = null;
@@ -419,15 +458,11 @@ function projectProseLine(line: string): { keep: boolean; text: string; limitati
if (field && isInternalKey(field[1])) {
return { keep: false, text: "", limitations: notesForField(field[1], field[2] ?? "") };
}
const tokenAt = line.search(INTERNAL_KEY_TOKEN);
const limitations = tokenAt >= 0 ? notesFromText(line.slice(tokenAt)) : [];
const visible = tokenAt >= 0 ? line.slice(0, tokenAt) : line;
const cleaned = projectInline(visible);
if (!cleaned.trim()) return { keep: false, text: "", limitations };
if (containsInternalToken(cleaned) || ordinaryOutputLeaks(cleaned).length > 0) {
return { keep: false, text: "", limitations };
const cleaned = projectInline(line);
if (!cleaned.trim() || containsInternalToken(cleaned) || ordinaryOutputLeaks(cleaned).length > 0) {
return { keep: false, text: "", limitations: notesFromText(line) };
}
return { keep: true, text: cleaned, limitations };
return { keep: true, text: cleaned, limitations: [] };
}
/** Safety-only boundary for the separately authorized raw attachment, not prose projection. */
File diff suppressed because one or more lines are too long
@@ -15,8 +15,10 @@ const golden = JSON.parse(readFileSync(new URL("./fixtures/report-density-fictio
test("density full engine reader cleanup removes embedded product contracts without losing rows or numeric fields", () => {
assert.equal(golden.fixtureProvenance.fictional, true);
const raw = golden.markdown as string;
// Captured by running baeec66f8's actual projector on this complete fictional engine Markdown.
assert.equal(createHash("sha256").update(projectOrdinaryReportMarkdown(raw)).digest("hex"), "01cc1e03c5adca2833e014156e1162fff12e5cb336e4484c5871859cd878acd7");
// 原值 01cc1e03c5adca2833e014156e1162fff12e5cb336e4484c5871859cd878acd7
// 新值 f3aeb8f4b09ba0a78af4ab5e7aee4db32d7499b8e6b1488f2925b35fd91750b9
// 原因:原 sha 锁的是投影截断后的泄漏输出(半句与孤立标记仍在)。整行删除 PL9 内部词后正文更短。
assert.equal(createHash("sha256").update(projectOrdinaryReportMarkdown(raw)).digest("hex"), "f3aeb8f4b09ba0a78af4ab5e7aee4db32d7499b8e6b1488f2925b35fd91750b9");
assert.match(raw, /pl9_personal_long_report\.v2/);
const clean = cleanReaderAppendixMarkdown(raw);
assert.doesNotMatch(clean, /pl9/i);
@@ -101,6 +101,15 @@ function executeReadyReportExport(cached: boolean) {
};
}
test("new longform generation requests the reader_main edition", () => {
const generateSource = readFileSync(
new URL("../src/lib/personal-report-longform-generate.ts", import.meta.url),
"utf8",
);
assert.match(generateSource, /edition:\s*"reader_main"/);
assert.match(generateSource, /include_fact_tables:\s*true/);
});
test("professional reference route authenticates, checks origin, owner and ready status", () => {
assert.match(routeSource, /createServerSupabaseClient/);
assert.match(routeSource, /supabase\.auth\.getUser\(\)/);
+4 -1
View File
@@ -58,7 +58,10 @@ test("full golden body is byte identical to the retired ordinary export", () =>
test("LF golden ordinary export keeps its frozen output hash", () => {
const output = ordinaryReportDownloadMarkdown(fixture.markdown);
assert.equal(createHash("sha256").update(output).digest("hex"), "3c52e8bfcfcb3ce3852bd5323867a69bc47d0f9aa821a34692172e7d61c6bcb8");
// 原值 3c52e8bfcfcb3ce3852bd5323867a69bc47d0f9aa821a34692172e7d61c6bcb8
// 新值 b7f6947f6ae6f82e4a972be3c1f2bc50260ed0c5f029847264f40a5ee6ba7b88
// 原因:原 sha 锁的是同一份泄漏正文的普通下载。BUG-1026 起内部词整行删除,不再截成半句。
assert.equal(createHash("sha256").update(output).digest("hex"), "b7f6947f6ae6f82e4a972be3c1f2bc50260ed0c5f029847264f40a5ee6ba7b88");
});
test("CRLF and mixed-newline golden exports remove every JSON chart fence", () => {
@@ -301,6 +301,70 @@ test("markdown view and chart fence contracts are not weakened", () => {
assert.match(coreSource, /projectOrdinaryReportDocument/);
});
test("PL9 audit vocabulary deletes the whole line and the adjacent table caption", () => {
const source = [
"# 读者报告",
"",
"这一句保留。",
"未完成对照,所有行保持 parameter_sensitive。",
"",
"### KP 原始表",
"",
"本节只解释下面这张表。",
"",
"| 项 | 状态 |",
"| --- | --- |",
"| annual_tajika_pack | blocked |",
"",
"表后的正文还在。",
"KP producer failed: 'types.SimpleNamespace' object has no attribute 'year'",
"Traceback (most recent call last):",
"full-report://profile/hash",
"PL9 第 42 页 fixture",
"pl9_personal_long_report.v2",
"kp_lord_sub_unavailable",
].join("\n");
const projected = projectOrdinaryReportMarkdown(source);
assert.match(projected, /这一句保留/);
assert.match(projected, /表后的正文还在/);
assert.doesNotMatch(projected, /所有行保持/);
assert.doesNotMatch(projected, /parameter_sensitive/);
assert.doesNotMatch(projected, /KP 原始表/);
assert.doesNotMatch(projected, /本节只解释下面这张表/);
assert.doesNotMatch(projected, /annual_tajika_pack/);
assert.doesNotMatch(projected, /^- `$/m);
assert.doesNotMatch(projected, /object has no attribute|Traceback|full-report:\/\/|PL9 第 42 页|pl9_personal_long_report|_unavailable/);
assert.deepEqual(ordinaryOutputLeaks(projected), []);
});
test("real reader_main fixture has no ordinary output leaks", () => {
const fixture = JSON.parse(readFileSync(new URL("./fixtures/report-reader-main-fictional.json", import.meta.url), "utf8"));
assert.equal(fixture.fixtureProvenance.fictional, true);
assert.equal(fixture.fixtureProvenance.edition, "reader_main");
const markdown = fixture.markdown as string;
assert.equal(ordinaryOutputLeaks(markdown).length, 0, ordinaryOutputLeaks(markdown).join(", "));
for (const chapter of ["出生资料", "Ashtakavarga", "标准大运", "KP", "Year Lord", "Tajika", "Muntha"]) {
assert.match(markdown, new RegExp(chapter));
}
const projected = projectOrdinaryReportMarkdown(markdown);
assert.equal(ordinaryOutputLeaks(projected).length, 0);
});
test("twelve English words count as a paragraph leak, planet names do not", () => {
const planet = projectOrdinaryReportMarkdown("太阳与 Mars 同宫,再看 Venus。");
assert.match(planet, /Mars/);
assert.match(planet, /Venus/);
const english = [
"This reference export combines local calculations with available external engine",
"replays and source comparisons that stay explicitly labeled.",
].join(" ");
const projected = projectOrdinaryReportMarkdown(`开头。\n${english}\n结尾。`);
assert.match(projected, /开头/);
assert.match(projected, /结尾/);
assert.doesNotMatch(projected, /This reference export/);
assert.deepEqual(ordinaryOutputLeaks(projected), []);
});
function syntheticRecord(): PersonalReportRecord {
return {
id: "11111111-1111-4111-8111-111111111111",