220 lines
7.9 KiB
TypeScript
220 lines
7.9 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import {
|
|
buildDocTypeTree,
|
|
docTypeLabels,
|
|
extractDocMetadata,
|
|
extractDocNo,
|
|
} from "../src/components/frontend/src/decision-center.tsx";
|
|
|
|
type JsonRecord = Record<string, unknown>;
|
|
|
|
function assertCondition(condition: unknown, message: string, detail: JsonRecord = {}): void {
|
|
if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
|
|
}
|
|
|
|
function source(path: string): string {
|
|
return readFileSync(path, "utf8");
|
|
}
|
|
|
|
function includesAll(text: string, snippets: string[]): boolean {
|
|
return snippets.every((snippet) => text.includes(snippet));
|
|
}
|
|
|
|
function assertEqual<T>(actual: T, expected: T, message: string, detail: JsonRecord = {}): void {
|
|
assertCondition(Object.is(actual, expected), message, { ...detail, actual, expected });
|
|
}
|
|
|
|
function groupCount(records: JsonRecord[], type: string): number {
|
|
return buildDocTypeTree(records).find((group) => group.type === type)?.nodes.length || 0;
|
|
}
|
|
|
|
export function runDecisionCenterWorkspaceContract(): JsonRecord {
|
|
const frontend = source("src/components/frontend/src/decision-center.tsx");
|
|
const css = source("src/components/frontend/public/style.css");
|
|
|
|
const titleDcsn = {
|
|
id: "title-dcsn",
|
|
title: "DC-DCSN-P0-2026-001 决策记录",
|
|
tags: [],
|
|
};
|
|
const tagGoal = {
|
|
id: "tag-goal",
|
|
title: "目标文书",
|
|
tags: ["doc-no:DC-GOAL-P0-2026-002"],
|
|
};
|
|
const bodyRprt = {
|
|
id: "body-rprt",
|
|
title: "报告正文",
|
|
body: "DC-RPRT-P2-2026-003\n\n报告正文第一段。",
|
|
tags: [],
|
|
};
|
|
const tagRetr = {
|
|
id: "tag-retr",
|
|
title: "复盘文书",
|
|
tags: ["doc-type:RETR"],
|
|
};
|
|
const structuredPlan = {
|
|
id: "structured-plan",
|
|
title: "结构化计划文书",
|
|
docType: "PLAN",
|
|
priority: "P1",
|
|
year: 2026,
|
|
sequence: 4,
|
|
};
|
|
|
|
assertEqual(extractDocNo(titleDcsn), "DC-DCSN-P0-2026-001", "title prefix must extract complete DCSN doc number");
|
|
assertEqual(extractDocMetadata(titleDcsn).docType, "DCSN", "title prefix must extract DCSN doc type");
|
|
assertEqual(extractDocNo(tagGoal), "DC-GOAL-P0-2026-002", "doc-no tag must extract complete GOAL doc number");
|
|
assertEqual(extractDocMetadata(tagGoal).docType, "GOAL", "doc-no tag must extract GOAL doc type");
|
|
assertEqual(extractDocNo(bodyRprt), "DC-RPRT-P2-2026-003", "body first paragraph must fallback extract complete RPRT doc number");
|
|
assertEqual(extractDocMetadata(bodyRprt).priority, "P2", "body fallback must extract RPRT priority");
|
|
assertEqual(extractDocMetadata(tagRetr).docType, "RETR", "doc-type tag must extract RETR doc type");
|
|
assertEqual(docTypeLabels.RETR, "复盘", "RETR label must be 复盘");
|
|
assertEqual(extractDocNo(structuredPlan), "DC-PLAN-P1-2026-004", "structured fields must compose complete PLAN doc number");
|
|
assertEqual(groupCount([titleDcsn, tagGoal, bodyRprt, tagRetr, structuredPlan], "DCSN"), 1, "DCSN records must be grouped by parsed type");
|
|
assertEqual(groupCount([titleDcsn, tagGoal, bodyRprt, tagRetr, structuredPlan], "GOAL"), 1, "GOAL records must be grouped by parsed type");
|
|
assertEqual(groupCount([titleDcsn, tagGoal, bodyRprt, tagRetr, structuredPlan], "RPRT"), 1, "RPRT records must be grouped by parsed type");
|
|
assertEqual(groupCount([titleDcsn, tagGoal, bodyRprt, tagRetr, structuredPlan], "RETR"), 1, "RETR records must be grouped by parsed type");
|
|
assertEqual(groupCount([titleDcsn, tagGoal, bodyRprt, tagRetr, structuredPlan], "PLAN"), 1, "PLAN records must be grouped by structured type");
|
|
|
|
assertCondition(
|
|
includesAll(frontend, [
|
|
"export function extractDocMetadata(record: any): DocMetadata",
|
|
"export function extractDocNo(record: any): string",
|
|
"export function buildDocTypeTree(records: any[]): Array<{ type: DocTypeCode",
|
|
"function buildTagGroups(records: any[]): Array<{ tag: string",
|
|
"DOC_TYPE_CODES = [\"DCSN\", \"GOAL\", \"PLAN\", \"RPRT\", \"ACTN\", \"ISSU\", \"RETR\", \"RQST\", \"RESP\", \"MINS\"]",
|
|
"docTypeLabels: Record<DocTypeCode, string>",
|
|
]),
|
|
"frontend must implement exported doc metadata extraction, doc-type tree, and tag grouping",
|
|
);
|
|
|
|
assertCondition(
|
|
includesAll(frontend, [
|
|
"function DocTreeNode({ record, depth, onSelect, selectedId }: AnyRecord)",
|
|
"function DocTreeGroup({ group, onSelect, selectedId, defaultOpen }: AnyRecord)",
|
|
"function TagTreeGroup({ group, onSelect, selectedId, defaultOpen }: AnyRecord)",
|
|
"function DocTypeTree({ records, onSelect, selectedId, activeGrouping }: AnyRecord)",
|
|
]),
|
|
"frontend must implement DocTreeNode, DocTreeGroup, TagTreeGroup, and DocTypeTree components",
|
|
);
|
|
|
|
assertCondition(
|
|
includesAll(frontend, [
|
|
"function DocDetailSidebar({ record, onSelect, onSave, saving, error, saveMsg, onRaw }: AnyRecord)",
|
|
"function DocEditor({ form, saving, onChange, onSubmit }: AnyRecord)",
|
|
"function DocViewer({ record }: AnyRecord)",
|
|
"function DocDetailHeader({ record, onBack }: AnyRecord)",
|
|
]),
|
|
"frontend must implement DocDetailSidebar, DocEditor, DocViewer, and DocDetailHeader",
|
|
);
|
|
|
|
assertCondition(
|
|
includesAll(frontend, [
|
|
"function DocWorkspace({ records, selectedRecord, onSelect, onSave, saving, saveError, saveMsg, onRaw }: AnyRecord)",
|
|
"h(DocWorkspace, {",
|
|
"activeView === \"workspace\"",
|
|
"decision-tab-workspace",
|
|
]),
|
|
"frontend must implement DocWorkspace component and workspace tab",
|
|
);
|
|
|
|
assertCondition(
|
|
includesAll(frontend, [
|
|
"mode === \"view\"",
|
|
"mode === \"edit\"",
|
|
"setMode(\"view\")",
|
|
"setMode(\"edit\")",
|
|
"doc-sidebar-mode-tabs",
|
|
]),
|
|
"frontend must implement read/edit mode toggle in DocDetailSidebar",
|
|
);
|
|
|
|
assertCondition(
|
|
includesAll(frontend, [
|
|
"selectedRecord?.id",
|
|
"setState((prev: any) => ({ ...prev, selectedDoc: record",
|
|
"state.selectedDoc",
|
|
]),
|
|
"frontend must track selectedDoc in state and sync on selection",
|
|
);
|
|
|
|
assertCondition(
|
|
includesAll(frontend, [
|
|
"await onSave(form)",
|
|
"recordPayloadFromForm(form)",
|
|
"setRecordSaveState({ saving: false, error: \"\", message:",
|
|
"setState((prev: any) => ({ ...prev, selectedDoc: saved",
|
|
]),
|
|
"frontend must call onSave, payload conversion, error handling, and sync selectedDoc after save",
|
|
);
|
|
|
|
assertCondition(
|
|
css.includes(".doc-workspace"),
|
|
"CSS must include .doc-workspace layout",
|
|
);
|
|
|
|
assertCondition(
|
|
css.includes(".doc-sidebar"),
|
|
"CSS must include .doc-sidebar",
|
|
);
|
|
|
|
assertCondition(
|
|
css.includes("grid-template-columns: minmax(200px, 260px) minmax(0, 1fr) minmax(0, 50%)"),
|
|
"CSS must implement three-column layout with right sidebar at 50%",
|
|
);
|
|
|
|
assertCondition(
|
|
includesAll(css, [
|
|
".doc-type-tree",
|
|
".doc-tree-group",
|
|
".doc-tree-node",
|
|
".doc-list-item",
|
|
".doc-full-markdown",
|
|
".doc-editor-form",
|
|
]),
|
|
"CSS must include all workspace component styles",
|
|
);
|
|
|
|
assertCondition(
|
|
includesAll(frontend, [
|
|
"extractDocMetadata(record)",
|
|
"recordTagValues(record, \"doc-no\")",
|
|
"firstBodyWindow(record)",
|
|
]),
|
|
"extractDocNo must use metadata extraction from title, doc-no tag, and body fallback",
|
|
);
|
|
|
|
assertCondition(
|
|
includesAll(frontend, [
|
|
"grouping === \"type\"",
|
|
"grouping === \"tag\"",
|
|
"setGrouping(\"type\")",
|
|
"setGrouping(\"tag\")",
|
|
]),
|
|
"workspace must support type and tag grouping toggle",
|
|
);
|
|
|
|
return {
|
|
ok: true,
|
|
checks: [
|
|
"doc-no-extraction-functions",
|
|
"doc-type-tree-components",
|
|
"doc-detail-sidebar-components",
|
|
"doc-workspace-component",
|
|
"read-edit-mode-toggle",
|
|
"selected-doc-state-sync",
|
|
"save-and-sync",
|
|
"workspace-css-layout",
|
|
"workspace-css-components",
|
|
"three-column-layout-50-percent",
|
|
"doc-metadata-fixtures",
|
|
"type-tag-grouping-toggle",
|
|
],
|
|
};
|
|
}
|
|
|
|
if (import.meta.main) {
|
|
process.stdout.write(`${JSON.stringify(runDecisionCenterWorkspaceContract(), null, 2)}\n`);
|
|
}
|