import type { ChecklistOf, Range, Submission } from "../vocab/index.ts"; import type { AblationCode, ForwardCode, Incoming, ProtocolBody } from "./types.ts"; // 程序检查:vocab 字段定义管不到的跨字段规则(WORK_LOG"留给 submissions 的跨字段规则"14 条, // 加任务书 10 第四部分的不许改的文件、代码存不了、指标编号重名、提案条数)。 // 每条规则交回零到几行问题,一份提交的问题一次列全。编号写错时列出对号清单里该写的全部编号: // 只说"不对",模型拿不到可对照的写法(参考项目真实运行 15-tight-2 第 3 轮,提交 378fe68)。 /** 一份提交的全部程序检查。回应、统一查拼好的协议(protocol),其余种类 protocol 为 null。 */ export function programProblems(inc: Incoming, protocol: ProtocolBody | null): string[] { switch (inc.kind) { case "entry": case "discussion": case "wrapUp": return []; case "claim": return claimProblems(inc.content, inc.checklist); case "proposal": return [ ...(inc.known.accepted + 1 > inc.checklist.maxProposals ? [`本阶段你已经收下 ${inc.known.accepted} 条提案,每人最多 ${inc.checklist.maxProposals} 条:要改哪一条,就在 replaces 里写它的提交编号整份替换,不要另交`] : []), ...factorProblems(inc.content, inc.checklist), ...increasing("authorEstimate", inc.content.authorEstimate), ]; case "firstPrediction": case "secondPrediction": return increasing("range", inc.content.range); case "critique": return within("target", [inc.content.target], ["whole", ...inc.checklist.factors]); case "response": return [...covers("replies 的挑刺编号", inc.content.replies.map((r) => r.critique), inc.checklist.critiques), ...factorProblems(protocol!, inc.checklist)]; case "unification": return [ ...covers("replies 的挑刺编号", inc.content.replies.map((r) => r.critique), inc.checklist.critiques), ...covers("sources 的原提案编号", inc.content.sources.map((s) => s.proposal), inc.checklist.sources), ...factorProblems(protocol!, inc.checklist), ]; case "implementation": case "repair": return [...locationProblems(inc.content, inc.checklist), ...codeProblems(inc.known.code)]; case "verification": return verificationProblems(inc.content, inc.checklist); case "explanation": return explanationProblems(inc.content, inc.checklist); case "ablationImplementation": return ablationProblems(inc.content, inc.checklist, inc.known.code); case "ablationVerification": return covers("factors 的因子", inc.content.factors.map((f) => f.factor), inc.checklist.factors); } } // --------------------------------------------------------------------------- // 各种提交 // --------------------------------------------------------------------------- /** 规则 7:志愿至少 min(N, 入口总数) 个、不重复、编号都在入口清单里(方法.md 4.2)。 */ function claimProblems(c: Submission<"claim">, k: ChecklistOf<"claim">): string[] { return [ ...(c.choices.length < k.minChoices ? [`choices 至少要排 ${k.minChoices} 个入口,你排了 ${c.choices.length} 个`] : []), ...within("choices", c.choices, k.entries), ...repeated(c.choices).map((x) => `choices 里 ${x} 写了不止一次`), ]; } /** * 提案、回应、统一共用(方法.md 第五部分"延续""指标"、4.3 ②⑥): * 规则 2 因子数上限;3 两个因子不延续同一个、延续的编号看得到;4 保障短名;10 指标的引用与新登记。 */ function factorProblems(p: Pick, k: ChecklistOf<"proposal" | "response" | "unification">): string[] { const continued = p.factors.flatMap((f) => (f.continues === null ? [] : [f.continues])); const defined = p.metrics.defined.map((m) => m.id); return [ ...(p.factors.length > k.factorLimit ? [`factors 有 ${p.factors.length} 个因子,超过上限 ${k.factorLimit} 个:多出来的留到下一轮,不要几个捆成一个`] : []), ...repeated(continued).map((x) => `有两个以上因子的 continues 都写了 ${x}:同一份提案里两个因子不能延续同一个因子`), ...within("continues", unique(continued), k.continuable).map((x) => `${x};新因子写 null`), ...unique(p.safeguards) .filter((s) => s === "main" || /^p[1-9][0-9]*$/.test(s)) .map((s) => `保障短名 ${s} 不能用:不能叫 main,也不能写成 p1、p2 这种因子编号的样子`), ...repeated(p.safeguards).map((s) => `safeguards 里 ${s} 写了不止一次`), ...within("metrics.reused", p.metrics.reused, k.reusableMetrics), ...repeated(defined).map((x) => `metrics.defined 里编号 ${x} 写了不止一次`), ...unique(defined) .filter((x) => k.takenMetricIds.includes(x)) .map((x) => `metrics.defined 的编号 ${x} 已被占用(已占用的是 ${k.takenMetricIds.join("、")}):换一个编号;要用别人登记过的就写进 metrics.reused`), ]; } /** 规则 11:位置表覆盖每个因子和每条保障;代码位置起始行不大于结束行(方法.md 4.3 ⑨)。 */ function locationProblems(c: Submission<"implementation">, k: ChecklistOf<"implementation" | "repair">): string[] { const all = [...c.factorLocations.flatMap((f) => f.locations), ...c.safeguardLocations.flatMap((s) => s.locations), ...c.unassigned.map((u) => u.location)]; return [ ...covers("factorLocations 的因子", c.factorLocations.map((f) => f.factor), k.factors), ...covers("safeguardLocations 的保障", c.safeguardLocations.map((s) => s.safeguard), k.safeguards), ...unique(all) .filter((loc) => { const m = /:([0-9]+)-([0-9]+)$/.exec(loc); return m !== null && Number(m[1]) > Number(m[2]); }) .map((loc) => `代码位置 ${loc} 的起始行大于结束行`), ]; } /** 规则 12:核验的条款、因子、保障逐项全覆盖;没有归属的改动与材料列出的逐条对应(方法.md 4.3 ⑨)。 */ function verificationProblems(c: Submission<"verification">, k: ChecklistOf<"verification">): string[] { return [ ...covers("clauses 的实现条款", c.clauses.map((x) => x.clause), k.clauses), ...covers("factors 的因子", c.factors.map((x) => x.factor), k.factors), ...covers("safeguards 的保障", c.safeguards.map((x) => x.safeguard), k.safeguards), ...covers("unassigned 的改动位置", c.unassigned.map((x) => x.location), k.unassigned), ]; } /** * 规则 5、6、1:解释最多 M 条;每条都交 cells、恰好覆盖材料列出的格,三个数严格递增(方法.md 4.3 ⑭)。 * 用户 2026-10-03 定:只按完整流程全开实现,一律要求交 cells;关掉消融的情形以后做开关时再补。 */ function explanationProblems(c: Submission<"explanation">, k: ChecklistOf<"explanation">): string[] { return [ ...(c.explanations.length > k.maxExplanations ? [`交了 ${c.explanations.length} 条解释,最多 ${k.maxExplanations} 条`] : []), ...c.explanations.flatMap((e, i) => e.cells === undefined ? [`第 ${i + 1} 条解释没交 cells:每条解释都要对 ${k.cells.join("、")} 每一格给三个数`] : [ ...covers(`第 ${i + 1} 条解释 cells 的格`, e.cells.map((x) => x.factor), k.cells), ...e.cells.flatMap((x) => increasing(`第 ${i + 1} 条解释 ${x.factor} 格的 range`, x.range)), ], ), ]; } /** * 规则 13:写了 cannotRun 时 alsoOff 为空;alsoOff 不含本格要关的因子、都在因子清单里、不重复(方法.md 4.3 ⑯)。 * 跑得起来的另查代码:运行的规矩和正向运行相同,改了不许改的文件同样退回。 */ function ablationProblems(c: Submission<"ablationImplementation">, k: ChecklistOf<"ablationImplementation">, code: AblationCode | null): string[] { return [ ...(c.cannotRun !== null && c.alsoOff.length > 0 ? ["写了 cannotRun(这一格跑不起来)时 alsoOff 要交空数组:两种做法只能选一种"] : []), ...(c.alsoOff.includes(k.off) ? [`alsoOff 不能写本格要关的因子 ${k.off},只写还要一起关的别的因子`] : []), ...within("alsoOff", c.alsoOff.filter((x) => x !== k.off), k.factors.filter((x) => x !== k.off)), ...repeated(c.alsoOff).map((x) => `alsoOff 里 ${x} 写了不止一次`), ...(c.cannotRun === null ? codeProblems(code!) : []), ]; } /** 代码存不了、改了不许改的文件(方法.md 4.3 ⑨)。 */ function codeProblems(code: ForwardCode | AblationCode): string[] { if (!code.saved) return [`代码存不成版本:${code.path}(${code.reason})。改掉它再交`]; return code.protectedFiles.length === 0 ? [] : [`改了不许改的文件:${code.protectedFiles.join("、")}。把它们改回本批基线的样子再交`]; } // --------------------------------------------------------------------------- // 共用 // --------------------------------------------------------------------------- /** 规则 1:三个数严格递增(方法.md 第五部分"三个数")。 */ function increasing(field: string, r: Range): string[] { return r.q1 < r.q5 && r.q5 < r.q9 ? [] : [`${field} 的三个数要严格递增 q1 < q5 < q9,你写的是 ${r.q1} / ${r.q5} / ${r.q9}`]; } /** 一组编号必须恰好覆盖 expected:不多、不少、不重复。只写出现了的几段。 */ function covers(field: string, actual: readonly string[], expected: readonly string[]): string[] { const unknown = unique(actual.filter((x) => !expected.includes(x))); const missing = expected.filter((x) => !actual.includes(x)); const twice = repeated(actual); if (unknown.length + missing.length + twice.length === 0) return []; return [ [ `${field}应当是 ${expected.length === 0 ? "没有(交空数组)" : expected.join("、")}`, ...(unknown.length > 0 ? [`你写的 ${unknown.join("、")} 不在里面`] : []), ...(missing.length > 0 ? [`还缺 ${missing.join("、")}`] : []), ...(twice.length > 0 ? [`${twice.join("、")} 写了不止一次`] : []), ].join(";"), ]; } /** 一组编号都要在 allowed 里(可以只写其中几个)。 */ function within(field: string, actual: readonly string[], allowed: readonly string[]): string[] { const unknown = unique(actual.filter((x) => !allowed.includes(x))); if (unknown.length === 0) return []; return [`${field} 只能写 ${allowed.length === 0 ? "(现在没有可写的编号)" : allowed.join("、")};你写的 ${unknown.join("、")} 不在里面`]; } function unique(xs: readonly string[]): string[] { return [...new Set(xs)]; } /** 出现了不止一次的值,各列一次。 */ function repeated(xs: readonly string[]): string[] { return unique(xs.filter((x, i) => xs.indexOf(x) !== i)); }