domain.ts
Source: examples/_shared/tools/content/domain.ts · Download source · Example guide
ts
import { createHash } from "node:crypto";
import type { JsonValue } from "@codesoul-co/ditto/contracts";
export const modes = [
"generate",
"rewrite",
"summarize",
"expand",
"translate",
"convert",
"cite",
] as const;
export type Mode = (typeof modes)[number];
export interface Request {
id: string;
tenant: string;
mode: Mode;
anchors: string[];
sources: { id: string; file: string; sha256: string }[];
}
export interface Block {
id: string;
line: number;
text: string;
}
export interface Source {
id: string;
file: string;
sha256: string;
content: string;
blocks: Block[];
}
export interface Material {
sources: Source[];
}
export interface Citation {
blockId: string;
quote: string;
}
export interface Draft {
title: string;
language: "zh-CN" | "en";
sections: { heading: string; text: string; citations: Citation[] }[];
}
export interface Review {
approved: boolean;
checks: {
fidelity: boolean;
coverage: boolean;
transformation: boolean;
citations: boolean;
};
issues: string[];
}
export const json = (v: unknown): JsonValue => JSON.parse(JSON.stringify(v));
export const digest = (v: string) =>
createHash("sha256").update(v).digest("hex");
export const object = (v: unknown): Record<string, unknown> => {
if (!v || typeof v !== "object" || Array.isArray(v))
throw new Error("Expected object");
return v as Record<string, unknown>;
};
function text(v: unknown, max: number): string {
if (typeof v !== "string" || !v.trim() || v.length > max || /[\0\r]/.test(v))
throw new Error("Invalid bounded text");
return v;
}
export function request(v: unknown): Request {
const r = object(v);
for (const k of ["id", "tenant"])
if (typeof r[k] !== "string" || !/^[a-zA-Z0-9_-]{1,80}$/.test(r[k]))
throw new Error(`Invalid ${k}`);
if (
!modes.includes(r.mode as Mode) ||
!Array.isArray(r.anchors) ||
r.anchors.length !== 3 ||
!Array.isArray(r.sources) ||
r.sources.length !== 3
)
throw new Error("Invalid content request");
const sources = r.sources.map((v) => {
const s = object(v);
if (
!["brief", "notes", "draft"].includes(String(s.id)) ||
s.file !== `${s.id}.md` ||
typeof s.sha256 !== "string" ||
!/^[a-f0-9]{64}$/.test(s.sha256)
)
throw new Error("Invalid source descriptor");
return { id: String(s.id), file: String(s.file), sha256: s.sha256 };
});
if (new Set(sources.map((s) => s.id)).size !== 3)
throw new Error("Duplicate source");
return {
id: String(r.id),
tenant: String(r.tenant),
mode: r.mode as Mode,
anchors: r.anchors.map((v) => text(v, 80)),
sources,
};
}
export function source(id: string, file: string, content: string): Source {
text(content, 16384);
return {
id,
file,
content,
sha256: digest(content),
blocks: content
.split("\n")
.flatMap((line, i) =>
line.trim() && !line.startsWith("#")
? [{ id: `${id}:${i + 1}`, line: i + 1, text: line }]
: [],
),
};
}
export function validateMaterial(v: unknown, r: Request): Material {
const material = object(v);
if (
!Array.isArray(material.sources) ||
material.sources.length !== r.sources.length
)
throw new Error("Missing source material");
const rawSources = material.sources;
const sources = r.sources.map((expected) => {
const raw = object(
rawSources.find((v: unknown) => object(v).id === expected.id),
);
const rebuilt = source(
expected.id,
expected.file,
text(raw.content, 16384),
);
if (
raw.file !== expected.file ||
raw.sha256 !== rebuilt.sha256 ||
rebuilt.sha256 !== expected.sha256 ||
JSON.stringify(raw.blocks) !== JSON.stringify(rebuilt.blocks)
)
throw new Error("Source snapshot mismatch");
return rebuilt;
});
return { sources };
}
export const maxLength: Record<Mode, number> = {
generate: 850,
rewrite: 650,
summarize: 380,
expand: 1500,
translate: 2000,
convert: 1500,
cite: 1000,
};
export function requirement(mode: Mode): string {
return {
generate:
"根据资料生成面向试点团队的新通知,说明产品、开放日期、每日配额、人工审批要求与数据导出限制。使用清晰标题和正文,不能照搬整篇草稿。",
rewrite:
"将 draft 改写为更清晰、礼貌、简洁的团队通知;调整句式与组织方式,保留全部关键事实,不能原样复制整篇草稿。",
summarize:
"总结 brief、notes 中的重要信息,去除讨论过程与重复内容;正文总长不超过 380 字,保留产品、日期、配额、审批与导出限制。",
expand:
"以 draft 第一段为开头,第一节正文必须逐字保留该段;增加完整的启用说明与申请步骤,覆盖所有 brief 事实并引用 notes。新增内容必须来自材料,正文应明显长于原始首段。",
translate:
"将 draft 的全部正文翻译成自然英文,不补充草稿以外的信息;保留产品编号、ISO 日期、数字以及限定条件。术语:人工审批=manual approval,数据导出=data export,NimbusDesk 不翻译。",
convert:
"将 draft 解析成可渲染的结构化内容:title 与原文 Markdown 标题相同;每个正文段落成为一个 section,顺序和 text 必须逐字保留,citations 指向对应 draft 原段落。只改变展示格式,不改写正文。",
cite: "根据 brief 和 notes 写一份带出处的启用指南;每节附支持该节陈述的原文引文,覆盖全部 brief 事实并包含申请步骤,引用至少两个来源。",
}[mode];
}
/** Immutable source text is retained by the controller; the model supplies only additions. */
export function assembleDraft(
value: unknown,
r: Request,
material: Material,
): unknown {
if (r.mode !== "expand") return value;
const draft = object(value);
if (!Array.isArray(draft.sections))
throw new Error("Missing expansion sections");
const lead = material.sources.find((s) => s.id === "draft")!.blocks[0]!;
const supporting = material.sources
.flatMap((s) => s.blocks)
.filter((b) => b.id === lead.id || b.text === lead.text);
return {
...draft,
sections: [
{
heading: "原始内容",
text: lead.text,
citations: supporting.map((b) => ({ blockId: b.id, quote: b.text })),
},
...draft.sections,
],
};
}
export function validateDraft(v: unknown, r: Request, m: Material): Draft {
const d = object(v);
if (
Object.keys(d).sort().join() !== "language,sections,title" ||
!Array.isArray(d.sections) ||
d.sections.length < 1 ||
d.sections.length > 7 ||
d.language !== (r.mode === "translate" ? "en" : "zh-CN")
)
throw new Error("Invalid draft schema or language");
const blocks = m.sources.flatMap((s) => s.blocks),
seen = new Set<string>();
const sections = d.sections.map((v) => {
const s = object(v);
if (
Object.keys(s).sort().join() !== "citations,heading,text" ||
!Array.isArray(s.citations) ||
!s.citations.length ||
s.citations.length > 12
)
throw new Error("Missing section citations");
const citations = s.citations.map((v) => {
const c = object(v),
block = blocks.find((b) => b.id === c.blockId);
if (
Object.keys(c).sort().join() !== "blockId,quote" ||
!block ||
c.quote !== block.text
)
throw new Error("Unsupported citation quote or location");
seen.add(block.id);
return { blockId: block.id, quote: block.text };
});
if (new Set(citations.map((c) => c.blockId)).size !== citations.length)
throw new Error("Duplicate section citation");
return {
heading: text(s.heading, 100),
text: text(s.text, maxLength[r.mode]),
citations,
};
});
const out: Draft = {
title: text(d.title, 150),
language: r.mode === "translate" ? "en" : "zh-CN",
sections,
},
body = sections.map((s) => s.text).join("\n"),
original = m.sources.find((s) => s.id === "draft")!;
if (
body.length > maxLength[r.mode] ||
r.anchors.some((anchor) => !body.includes(anchor))
)
throw new Error("Missing protected fact or output exceeds length");
if (
r.mode === "translate"
? /[\u3400-\u9fff]/.test(
out.title + sections.map((s) => s.heading + s.text).join(""),
)
: !/[\u3400-\u9fff]/.test(body)
)
throw new Error("Output language mismatch");
const required = ["convert", "translate", "rewrite"].includes(r.mode)
? original.blocks
: m.sources.find((s) => s.id === "brief")!.blocks;
if (required.some((b) => !seen.has(b.id)))
throw new Error("Required source coverage missing");
const allowedNumbers = new Set(
m.sources.flatMap((s) => s.content.match(/\d+/g) ?? []),
);
if ((body.match(/\d+/g) ?? []).some((n) => !allowedNumbers.has(n)))
throw new Error("Unsupported numerical claim");
if (
r.mode === "translate" &&
(!/manual approval/i.test(body) || !/data export/i.test(body))
)
throw new Error("Required translation glossary missing");
if (
["generate", "rewrite"].includes(r.mode) &&
body === original.blocks.map((b) => b.text).join("\n")
)
throw new Error("Content was not transformed");
if (
r.mode === "expand" &&
(sections[0]!.text !== original.blocks[0]!.text ||
body.length < original.blocks[0]!.text.length + 80 ||
![...seen].some((id) => id.startsWith("notes:")))
)
throw new Error(
"Expansion must preserve the lead and add supported material",
);
if (
r.mode === "cite" &&
(!seen.has(m.sources.find((s) => s.id === "notes")!.blocks[0]!.id) ||
new Set([...seen].map((id) => id.split(":")[0])).size < 2)
)
throw new Error("Citation source diversity missing");
if (
r.mode === "convert" &&
(out.title !== original.content.split("\n")[0]!.replace(/^# /, "") ||
sections.length !== original.blocks.length ||
sections.some(
(s, i) =>
s.text !== original.blocks[i]!.text ||
s.citations.length !== 1 ||
s.citations[0]!.blockId !== original.blocks[i]!.id,
))
)
throw new Error("Format conversion changed source content");
return out;
}
export function validateReview(v: unknown): Review {
const r = object(v),
c = object(r.checks);
if (
Object.keys(r).sort().join() !== "approved,checks,issues" ||
Object.keys(c).sort().join() !==
"citations,coverage,fidelity,transformation" ||
typeof r.approved !== "boolean" ||
Object.values(c).some((v) => typeof v !== "boolean") ||
!Array.isArray(r.issues) ||
r.issues.length > 10
)
throw new Error("Invalid review schema");
const issues = r.issues.map((v) => text(v, 1000));
if (!r.approved || Object.values(c).some((v) => !v) || issues.length)
throw new Error(
`Content review rejected the draft: ${issues.join("; ") || "one or more checks failed"}`,
);
return {
approved: true,
checks: {
fidelity: true,
coverage: true,
transformation: true,
citations: true,
},
issues,
};
}