feat: 任务书 10,submissions 模块与 materials 修改

- 新增 src/submissions:先程序检查(14 条跨字段规则、不许改的文件、代码存不了、指标编号占用、提案数上限),全过再问 Jev;退回原因列出该写的全部编号;回应/统一拼冻结协议主体只在这一处;核验、消融核验通过没有。
- materials:挑刺单写"现在交什么";退回按阶段、作者、种类、对象找(ledger 退回记录加 target,表结构版本 4);核验材料去掉 Jev 预扫;信念本写合法化;删"本该关着";消融核验不通过可重做,不跑的格从 cellNotRun 事件读原因。
- 用户 2026-10-03 定:冻结协议去掉 replies、replyBody,第二次预测的答复从已公开的回应或统一读;takenMetricIds 加两个保留键;解释一律交 cells;Jev 退回只列判"是"的几问、用英文问题名。
- jev 判重材料带 keySettings;读代码工具删 offset;三处旧说法改正。
- 文档:结构.md 第四、七、八部分,WORK_LOG。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 06:08:34 -04:00
parent a0ee4ef7a2
commit 09c44031a0
34 changed files with 1221 additions and 169 deletions
+193
View File
@@ -0,0 +1,193 @@
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<ProtocolBody, "factors" | "safeguards" | "metrics">, 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));
}