feat(reports): a line under D9/D10 chart and table sections when the adopted segment left them undetermined (BUG-1139, T3)

Applied before the longform snapshot is hashed, Chinese and English bodies.
The engine payload is unchanged; legacy and unrectified reports store the
engine text byte-for-byte. Chart cards keep their grid (the line becomes the
card caption).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
Jesse_Chen
2026-10-01 10:15:10 +08:00
co-authored by Claude Opus 5.5
parent 7f39c9346d
commit 2004a6edc1
4 changed files with 180 additions and 3 deletions
@@ -0,0 +1,45 @@
import type { TargetChart } from "./rectification-agentic/core/segment-summary.ts";
/**
* Section caveats for charts a segment-v1 adoption could not determine (BUG-1139).
*
* The reader edition prints each divisional chart under its own heading
* ("##### D9 九分盘" + chart fence, "#### D9 九分盘 数值位置表" + table). One plain
* line goes directly under each such heading; the chart and table stay. A chart
* card accepts exactly one paragraph between heading and fence
* (`report-chart-grid-rehype.ts::parseCard`), so the line never splits a grid.
* Headings are matched on the chart code the engine writes, not on prose.
*/
export const CHART_CAVEAT_ZH = "提示:出生时间范围内这张盘的上升会变,这一节仅供参考。";
export const CHART_CAVEAT_EN = "Note: this chart's ascendant changes within the birth-time range; treat this section as reference only.";
const HEADING_RE = /^(#{3,6})\s+(.+?)\s*$/;
// Engine chart labels: "D9 九分盘", "D9 Navamsa", "Navamsha"; "D10 Dasamsa"; "D1 Rashi".
const CHART_PATTERNS: Readonly<Record<TargetChart, RegExp>> = {
D1: /^D1\b/,
D9: /^(?:D9\b|Navamsh?a\b)/,
D10: /^(?:D10\b|Dasamsh?a\b)/,
};
export function annotateUnreliableChartSections(
markdown: string,
vargas: readonly TargetChart[] | undefined,
language: "zh" | "en",
): string {
if (!vargas?.length) return markdown;
const caveat = language === "en" ? CHART_CAVEAT_EN : CHART_CAVEAT_ZH;
const lines = markdown.split("\n");
const out: string[] = [];
for (let index = 0; index < lines.length; index += 1) {
const line = lines[index]!;
out.push(line);
const heading = HEADING_RE.exec(line);
if (!heading || !vargas.some((varga) => CHART_PATTERNS[varga].test(heading[2]!))) continue;
let next = index + 1;
while (next < lines.length && !lines[next]!.trim()) next += 1;
if (lines[next]?.trim() === caveat) continue;
out.push("", caveat);
}
return out.join("\n");
}
@@ -5,6 +5,7 @@ import type { ReportFactTable } from "./report-fact-table-schema";
import { persistLongformSnapshot, readLongformSnapshot, validateLongformSnapshot, type LongformSnapshot } from "./personal-report-longform-snapshot";
import { acceptEnglishEdition, type EnglishOutcome } from "./personal-report-english-edition";
import { hashLongformMarkdown } from "./personal-report-longform-appendix";
import { annotateUnreliableChartSections } from "./personal-report-chart-caveats";
import {
LONGFORM_APPENDIX_TABLE,
@@ -231,9 +232,21 @@ export async function generatePersonalReportLongform(
fetchImpl: deps.fetchImpl ?? fetch,
signal: deps.signal,
});
// Segment-v1 adoptions that left D9/D10 undetermined get a line under those
// chart sections before the snapshot is hashed (BUG-1139); no-op otherwise.
const unreliable = deps.candidateRange?.unreliableVargas;
const annotated = unreliable?.length ? {
...generated,
markdown: annotateUnreliableChartSections(generated.markdown, unreliable, "zh"),
...(generated.english ? { english: {
...generated.english,
markdown: annotateUnreliableChartSections(generated.english.markdown, unreliable, "en"),
contentSha256: hashLongformMarkdown(annotateUnreliableChartSections(generated.english.markdown, unreliable, "en")),
} } : {}),
} : generated;
snapshot = validateLongformSnapshot({
...generated, ...identity, kind: "longform_calculation_snapshot", version: 2,
contentSha256: hashLongformMarkdown(generated.markdown),
...annotated, ...identity, kind: "longform_calculation_snapshot", version: 2,
contentSha256: hashLongformMarkdown(annotated.markdown),
}, identity);
// Durable complete snapshot precedes the shared Markdown cache and final report.
deps.signal?.throwIfAborted();