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:
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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\(\)/);
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user