149 lines
5.2 KiB
TypeScript
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;
|
|
}
|