Files
Jyotisha/frontend/src/lib/personal-report-longform-cover.ts
T
2026-09-23 10:42:17 +08:00

149 lines
5.2 KiB
TypeScript

import { createHash } from "node:crypto";
import { computeEvidenceHash, parseServerReportDocument } from "./personal-report-contract.server-core.ts";
import {
REPORT_DOCUMENT_V2_SCHEMA_VERSION,
type ReportDocumentV2,
} from "./personal-report-contract.ts";
import {
PERSONAL_REPORT_CARD_SUMMARY_MAX_CHARS,
extractLongformSummary,
} from "./personal-report-longform-outline.ts";
import type { PersonalReportRecord } from "./personal-report-service-core.ts";
type CoverSkillSnapshot = Readonly<{
name: string;
version: string;
sha256: string;
sourceCommit: string | null;
}>;
const COVER_EVIDENCE_ID = "ev-cover-longform";
const SIGNS_CN = [
"白羊座", "金牛座", "双子座", "巨蟹座", "狮子座", "处女座",
"天秤座", "天蝎座", "射手座", "摩羯座", "水瓶座", "双鱼座",
] as const;
export type LongformCoverInput = Readonly<{
report: Pick<
PersonalReportRecord,
| "id"
| "reportType"
| "presentationMode"
| "depth"
| "requestedThemes"
| "skillName"
| "skillVersion"
| "skillSourceCommit"
| "skillSnapshotSha256"
>;
subject: Readonly<{
displayName: string;
birthTimeStatus: ReportDocumentV2["subject"]["birthTimeStatus"];
birthPlaceLabel: string;
}>;
markdown: string;
factTables?: ReportDocumentV2["factTables"];
charts?: ReportDocumentV2["charts"];
generatedAt?: string;
skillSnapshot?: CoverSkillSnapshot;
}>;
function coverHouses(): ReportDocumentV2["charts"][number]["houses"] {
return SIGNS_CN.map((sign, index) => ({
houseNumber: index + 1,
sign,
occupants: [],
}));
}
export function buildLongformCoverDocument(input: LongformCoverInput): ReportDocumentV2 {
const summary = extractLongformSummary(input.markdown, PERSONAL_REPORT_CARD_SUMMARY_MAX_CHARS);
const skillName = input.skillSnapshot?.name ?? input.report.skillName;
const skillVersion = input.skillSnapshot?.version ?? input.report.skillVersion;
const skillSnapshotSha256 = input.skillSnapshot?.sha256 ?? input.report.skillSnapshotSha256;
const skillSourceCommit = input.skillSnapshot?.sourceCommit ?? input.report.skillSourceCommit;
if (!skillName || !skillVersion || !skillSnapshotSha256) {
throw new Error("report_schema_invalid");
}
const calculationHash = createHash("sha256").update(input.markdown, "utf8").digest("hex");
const evidenceAppendix: ReportDocumentV2["evidenceAppendix"] = {
expandedByDefault: false,
techniqueAudit: [{
id: COVER_EVIDENCE_ID,
techniqueId: "pl9_personal_long_report",
techniqueName: "个人长报告 Markdown",
status: "partial",
used: true,
notes: "正文为长报告 Markdown,本封面仅用于落库校验。",
}],
conflicts: [],
calculationEvidence: [],
blockedTechniques: [],
};
const document = {
schemaVersion: REPORT_DOCUMENT_V2_SCHEMA_VERSION,
reportId: input.report.id,
reportType: input.report.reportType,
presentationMode: input.report.presentationMode,
depth: input.report.depth,
requestedThemes: [...input.report.requestedThemes],
generatedAt: input.generatedAt ?? new Date().toISOString(),
subject: input.subject,
provenance: {
skillName,
skillVersion,
skillSourceCommit: skillSourceCommit && /^[0-9a-f]{40}$/.test(skillSourceCommit)
? skillSourceCommit
: null,
skillSnapshotSha256,
calculationHash,
evidenceHash: computeEvidenceHash(evidenceAppendix),
reportContractVersion: "2" as const,
},
executiveSummary: {
headline: "个人长报告已生成",
summary,
priorities: ["阅读长报告 Markdown 正文"],
overallClaimStatus: "parameter_sensitive" as const,
evidenceRefs: [COVER_EVIDENCE_ID],
},
natalFoundation: {
title: "封面占位",
narrative: "本封面不作为阅读正文。完整报告见长报告 Markdown。",
keyFactors: ["长报告 Markdown 为正文"],
caveats: ["本封面文档不在详情页渲染"],
claimStatus: "parameter_sensitive" as const,
evidenceRefs: [COVER_EVIDENCE_ID],
},
currentPhase: null,
actionNotes: [{
id: "read-longform",
title: "阅读长报告",
note: "请在详情页阅读长报告 Markdown 正文。",
priority: "now" as const,
evidenceRefs: [COVER_EVIDENCE_ID],
}],
charts: input.charts?.some(chart => chart.id === "D1") ? input.charts : [{
id: "D1" as const,
title: "D1 占位",
houses: coverHouses(),
claimStatus: "parameter_sensitive" as const,
evidenceRefs: [COVER_EVIDENCE_ID],
}],
...(input.factTables?.length ? { factTables: input.factTables } : {}),
thematicNarrative: [],
blockedConflictDisclosure: input.report.requestedThemes.map((theme) => ({
theme,
title: `${theme} 主题见长报告`,
reason: "本封面不展开主题叙事。完整证据与解读见长报告 Markdown。",
missingEvidence: ["封面不装配主题叙事,请阅读长报告正文"],
conflictNotes: [],
evidenceRefs: [] as string[],
claimStatus: "blocked" as const,
})),
evidenceAppendix,
disclaimer: "本封面仅用于系统落库校验。阅读请以长报告 Markdown 为准。",
};
return parseServerReportDocument(document) as ReportDocumentV2;
}