import type { Json } from "./round-data.ts"; // 给审阅者的文本去掉来历(任务书 14 第 3.2 节第 1 步):运行名、模型名、研究编号去掉; // 研究者、提案、候选、挑刺、入口的编号改写成"研究者 1""提案 1"这类中性写法,按出现顺序编,一份文本一张映射表。 // 两个项目的编号写法都认:我们的 r1-researcher-2-1、r1-c1、e-researcher-1-4,参考项目的 r1-v1-p-researcher-2-1、r1-v1-c1、 // researcher-1-e4、挑刺的事实编号(uuid)。字段名(points 对 factors)不改。 type Family = "研究者" | "提案" | "候选" | "挑刺" | "入口"; /** 认编号的写法,按先后试:长的、带别的编号在里面的写法放前面。 */ const PATTERNS: Array<[Family | "实验员" | "核验员", string]> = [ ["挑刺", String.raw`r\d+-researcher-\d+-\d+\.researcher-\d+\.\d+`], ["提案", String.raw`r\d+-v\d+-p-researcher-\d+-\d+`], ["提案", String.raw`r\d+-researcher-\d+-\d+`], ["实验员", String.raw`implementer-[A-Za-z0-9-]+`], ["核验员", String.raw`verifier-[A-Za-z0-9-]+`], ["入口", String.raw`e-researcher-\d+-\d+`], ["入口", String.raw`researcher-\d+-e\d+`], ["候选", String.raw`r\d+-v\d+-c\d+`], ["候选", String.raw`r\d+-c\d+`], ["研究者", String.raw`researcher-\d+`], ["挑刺", String.raw`[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`], ]; const ID_RE = new RegExp(PATTERNS.map(([, p]) => `(? = [ [/(?(); private readonly counts = new Map(); private readonly hidden: RegExp | null; constructor(hiddenNames: string[]) { const esc = hiddenNames.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")); this.hidden = esc.length === 0 ? null : new RegExp(esc.sort((a, b) => b.length - a.length).join("|"), "g"); } /** 一个编号的中性名:第一次见到时按这一类的出现顺序编号。 */ name(family: Family | "实验员" | "核验员", raw: string): string { const seen = this.names.get(raw); if (seen !== undefined) return seen; const n = (this.counts.get(family) ?? 0) + 1; this.counts.set(family, n); const named = `${family} ${n}`; this.names.set(raw, named); return named; } /** 一段文字里的编号和来历都换掉。研究编号、运行名先换:我们的研究编号里可能带着像编号的片段。 */ text(s: string): string { let t = this.hidden === null ? s : s.replace(this.hidden, "某次运行"); t = t.replace(ID_RE, (match, ...groups: unknown[]) => { const i = groups.findIndex((g, k) => k < PATTERNS.length && g !== undefined); return this.name(PATTERNS[i]![0], match); }); for (const [re, to] of SCRUB) t = t.replace(re, to); return t; } /** 映射表(原编号 → 中性名),存进 data 旁边,审阅者看不到。 */ table(): Record { return Object.fromEntries(this.names); } } /** 渲染时去掉的键:只说明来历的哈希、请求编号、轮次编号(编号类的值照常经 text 改写成中性名)。 */ const DROP = new Set(["baselineCodeHash", "materialHash", "questionHash", "requestId", "roundId", "protocolId"]); /** 把 Json 写成缩进的纯文本:对象一键一行,多行文字缩进成块,数组逐项编号。 */ export function render(value: Json, nt: Neutral, indent = ""): string { if (value === null) return `${indent}(空)`; if (typeof value !== "object") return block(nt.text(String(value)), indent); if (Array.isArray(value)) { if (value.length === 0) return `${indent}(无)`; return value.map((v, i) => (isScalar(v) ? `${indent}${i + 1}. ${inline(v, nt)}` : `${indent}${i + 1}.\n${render(v, nt, `${indent} `)}`)).join("\n"); } const entries = Object.entries(value).filter(([k]) => !DROP.has(k)); if (entries.length === 0) return `${indent}(无)`; return entries .map(([k, v]) => (isScalar(v) && !String(v).includes("\n") ? `${indent}${k}: ${inline(v, nt)}` : `${indent}${k}:\n${render(v, nt, `${indent} `)}`)) .join("\n"); } const isScalar = (v: Json) => v === null || typeof v !== "object"; const inline = (v: Json, nt: Neutral) => (v === null ? "(空)" : nt.text(String(v))); const block = (s: string, indent: string) => s .split("\n") .map((l) => `${indent}${l}`) .join("\n");