feat: 任务书 09,materials 模块与提示词

- src/materials:每种提交一个材料函数,生成给模型看的材料、第一条消息、对号清单、能读的代码版本;查询只在材料内按编号找;读提示词并算原始字节哈希;信念本按方法.md 6.10 现算。
- 账本里只存成 Json 的冻结协议、位置表、决定、结算的形状和编号规则由 materials 定义并导出。
- vocab 加 Checklist(对号清单)。
- prompts/:参考项目 3 份角色提示词、16 份任务说明,只做必要改动。
- TASK.md 补一句 unversionedNames 不存进代码版本。
- 方法.md 4.3 ① 改为"研究者任务"放信念本;结构.md vocab 上限 1150、补 materials 对外内容。

用户 2026-10-03 开工前的四个决定(选的都是推荐项):"甲:上限改 1150,只放对号清单";"甲:materials 定义并导出";"不放,改方法.md ① 的措辞";"按原提案挑"。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 04:08:46 -04:00
parent cba1678eaf
commit 446e26224e
41 changed files with 3665 additions and 10 deletions
+195
View File
@@ -0,0 +1,195 @@
import type { ChecklistOf } from "../vocab/index.ts";
import { Draft, factorLines, KIND_TEXT, offText, registeredMetrics, taskPoints } from "./blocks.ts";
import { fenced, json } from "./format.ts";
import { unassignedChanges } from "./locations.ts";
import { clauseKey, factorKey, splitCell, type FrozenProtocol, type LocationTable } from "./shapes.ts";
// 实验员、核验员的 5 种任务(方法.md 4.3 ⑨ ⑯):只看分给他的那个候选或那一格——冻结协议、起点代码、
// 要实现或核验的东西。不给信念本(用户 2026-10-03 决定 3)、不给任何运行成绩、不给别的候选。
// 排法照参考项目 assignedBlocks、ablationBlocks:任务与本轮 → 代码 → 本任务要处理的东西(核验把没有归属的改动放最前)。
type Items = { candidate: string; clauses: string[]; factors: string[]; safeguards: string[] };
/** 每个函数最先调用:一级分组"分给你的材料",冻结协议进查询,交回协议和对号清单的 FrozenItems。 */
function frozen(d: Draft, candidate: string): { p: FrozenProtocol; items: Items } {
d.group("分给你的材料", "下面是分给你的这一个对象的全部材料。");
const p = d.board.protocol(candidate);
d.record(candidate, `候选 ${candidate} 的冻结协议`, p);
return { p, items: { candidate, clauses: p.clauses.map((_, i) => clauseKey(i)), factors: p.factors.map((_, i) => factorKey(i)), safeguards: p.safeguards } };
}
function taskSection(d: Draft, role: string, what: string): void {
d.section("任务与本轮", [
`你是${role} ${d.req.actor},现在是第 ${d.round} 轮(阶段编号 ${d.req.stageId})。本次任务:${KIND_TEXT[d.req.kind]},${what}。`,
"",
...taskPoints(d),
]);
}
/** 协议要采集的指标:新登记的全文,引用的取原登记(方法.md 第五部分"引用别人登记过的指标时,沿用原登记的选法")。 */
function metricSection(d: Draft, p: FrozenProtocol): void {
const registered = registeredMetrics(d.board);
const all = [...p.metrics.defined, ...p.metrics.reused.map((id) => registered.find((x) => x.id === id)!)];
d.section("要实现的指标定义", [
all.length === 0 ? "(这份协议没有要实现的假设专用指标)" : json(all),
"",
"每个指标都由候选代码在正式运行时算出、写进结果文件;测不到的写 null,不写 0。",
]);
}
function locationTableSection(d: Draft, p: FrozenProtocol): void {
d.section("要交的因子位置表", [
"每个因子、每条保障各在哪几行;同一行既属于因子又属于保障时两边都写;不属于任何因子或保障的改动写进 unassigned 并说一句为什么。",
"",
...factorLines(p),
`保障:${p.safeguards.length === 0 ? "没有(保障位置表交空数组)" : p.safeguards.join("、")}`,
]);
}
function codeLines(d: Draft): string[] {
return [...d.readableLines(), "读这些目录用 read_source(核验员)或 shell(实验员)。"];
}
/** ⑨ 实现:冻结协议、指标定义、要交的位置表、起点代码(本批基线)。 */
export function implementation(d: Draft): ChecklistOf<"implementation"> {
const candidate = d.req.object!;
const { p, items } = frozen(d, candidate);
taskSection(d, "实验员", `候选 ${candidate}`);
d.read(p.baseline, "起点代码(本批基线)");
d.section("基线代码", [...codeLines(d), "你的工作目录是起点代码的副本,改动都落在工作目录里。"]);
d.section("本任务要处理的东西", [`### 候选 ${candidate} 的冻结协议`, "", json(p)]);
metricSection(d, p);
locationTableSection(d, p);
return { kind: "implementation", ...items };
}
/** ⑨ 修复:实现的全部材料,加退回意见(最近一次核验里没落实、判越界的几项)、上一次的位置表、上一次提交相对起点的改动。 */
export function repair(d: Draft): ChecklistOf<"repair"> {
const candidate = d.req.object!;
const { p, items } = frozen(d, candidate);
taskSection(d, "实验员", `候选 ${candidate}`);
d.read(p.baseline, "起点代码(本批基线)");
d.read(candidate, "你上一次提交的代码");
d.section("基线代码", [...codeLines(d), "你的工作目录是上一次提交的代码的副本。"]);
const v = d.board.published("verification").filter((s) => s.target === candidate).at(-1)!.content;
d.section("本任务要处理的东西", [
"### 退回意见",
"",
...v.clauses.filter((x) => !x.found).map((x) => `- 条款 ${x.clause} 没落实:${x.where}`),
...v.factors.filter((x) => !x.found).map((x) => `- 因子 ${x.factor} 没落实:${x.where}`),
...v.safeguards.filter((x) => !x.found).map((x) => `- 保障 ${x.safeguard} 没落实:${x.where}`),
...v.unassigned.filter((x) => x.outOfScope).map((x) => `- ${x.location} 判为越界:${x.reason}`),
"",
`核验员的说明:${v.body}`,
"",
"### 上一次交的位置表",
"",
json(d.board.code(candidate).locations),
"",
"### 上一次提交相对起点代码的改动",
"",
fenced(d.req.changes!.diff, "diff"),
"",
`### 候选 ${candidate} 的冻结协议`,
"",
json(p),
]);
metricSection(d, p);
locationTableSection(d, p);
return { kind: "repair", ...items };
}
/**
* ⑨ 核验:没归到任何因子或保障的改动排第一(程序按位置表现算);冻结协议、位置表、指标定义、改动全文、
* Jev 对这份实现的预扫结果。因子与保障同一行的地方只作提醒。
*/
export function verification(d: Draft): ChecklistOf<"verification"> {
const candidate = d.req.object!;
const { p, items } = frozen(d, candidate);
const table = d.board.code(candidate).locations as LocationTable;
const found = unassignedChanges(d.req.changes!, table);
d.section("没归到任何因子或保障的改动", [
"程序按实验员交的位置表,把一行都没落进任何因子或保障的改动列在下面,逐条判断是否越界(location 照这里抄):",
...(found.unassigned.length === 0 ? ["(没有)"] : found.unassigned.map((u) => `- ${u.location}(${u.note})`)),
"",
"实验员自己对不属于任何因子的改动写的说明(只是说明,不算归属):",
...(table.unassigned.length === 0 ? ["(没有)"] : table.unassigned.map((u) => `- ${u.location}:${u.note}`)),
"",
"同一行既属于因子又属于保障的地方(只是提醒,不判越界;在说明里写一句):",
...(found.overlaps.length === 0 ? ["(没有)"] : found.overlaps.map((o) => `- ${o.location}:因子 ${o.factor}、保障 ${o.safeguard}`)),
]);
taskSection(d, "核验员", `候选 ${candidate}`);
d.read(p.baseline, "起点代码(本批基线)");
d.read(candidate, "要核验的候选代码");
d.section("基线代码", codeLines(d));
const impl = d.board.implementation(candidate)!;
d.section("本任务要处理的东西", [
`### 候选 ${candidate} 的冻结协议`,
"",
json(p),
"",
"### 实验员交的位置表",
"",
json(table),
"",
"### 实验员的实现说明",
"",
impl.content.body,
"",
"### 改动全文(候选相对起点代码,每段前后各 3 行上下文)",
"",
fenced(d.req.changes!.diff, "diff"),
"",
"### 预扫结果(收下这份实现时 Jev 逐段判过;概率和阈值原样列出)",
"",
...(impl.jev === null ? ["(没有)"] : impl.jev.map((j) => `- ${j.question}:概率 ${j.probability},阈值 ${j.threshold}`)),
]);
metricSection(d, p);
return { kind: "verification", unassigned: found.unassigned.map((u) => u.location), ...items };
}
/**
* 消融两种任务共用(方法.md 4.3 ⑯):任务与本轮、完整代码与本批基线、这一格(关哪个因子、换成什么)。
* 交回本任务要处理的东西的公共部分:因子清单、完整代码的位置表、冻结协议。
*/
function ablation(d: Draft, role: string, codeNote: string[]): { cell: string; key: string; factors: string[]; lines: string[] } {
const cell = d.req.object!;
const { candidate, key } = splitCell(cell);
const { p, items } = frozen(d, candidate);
taskSection(d, role, `消融格 ${cell}(候选 ${candidate} 关掉 ${key})`);
d.read(candidate, "完整代码");
d.read(p.baseline, "本批基线(因子没写关掉时换成什么,就回到它在这件事上的做法)");
if (d.req.kind === "ablationVerification") d.read(cell, "要核验的这一格的代码");
d.section("完整代码(消融的起点)", [...codeLines(d), ...codeNote]);
const f = p.factors[items.factors.indexOf(key)]!;
d.section("这一格", [
`格 ${cell}:关掉 ${key} ${f.name}。关掉时换成:${offText(f)}。`,
`其余因子、配套设定、全部保障(${p.safeguards.length === 0 ? "没有保障" : p.safeguards.join("、")})都照完整代码保留。`,
]);
const lines = ["### 因子清单", "", ...factorLines(p), "", "### 完整代码那次实现交的位置表", "", json(d.board.code(candidate).locations), "", "### 冻结协议", "", json(p)];
return { cell, key, factors: items.factors, lines };
}
/** ⑯ 消融实现:从完整代码出发只关指定的因子。 */
export function ablationImplementation(d: Draft): ChecklistOf<"ablationImplementation"> {
const a = ablation(d, "实验员", ["你的工作目录是完整代码的副本,改动都落在工作目录里。"]);
d.section("本任务要处理的东西", a.lines);
return { kind: "ablationImplementation", cell: a.cell, off: a.key, factors: a.factors };
}
/** ⑯ 消融核验:只查是不是只关了指定的因子(加实验员报的一起关的因子)。不算没有归属的改动。 */
export function ablationVerification(d: Draft): ChecklistOf<"ablationVerification"> {
const a = ablation(d, "核验员", []);
const also = d.board.published("ablationImplementation").filter((s) => s.target === a.cell).at(-1)!.content.alsoOff;
d.section("这一格实际要关的因子", [
[a.key, ...also].join("、") + (also.length === 0 ? "(只关指定的因子)" : "(指定的因子加实验员报的一起关的因子)"),
]);
d.section("本任务要处理的东西", [
...a.lines,
"",
"### 改动全文(这一格的代码相对完整代码,每段前后各 3 行上下文)",
"",
fenced(d.req.changes!.diff, "diff"),
]);
return { kind: "ablationVerification", cell: a.cell, factors: a.factors };
}
+224
View File
@@ -0,0 +1,224 @@
import type { Ledger } from "../ledger/index.ts";
import {
authorVsMerged,
bitWidth,
compareIds,
forwardMixtures,
improvement,
runBalance,
surprise,
trackRecord,
worldBits,
type Phase,
} from "../rules/index.ts";
import type { ProcessResult, Range } from "../vocab/index.ts";
import { measured, num, pct, PLACEMENT_TEXT, rangeText, resourcesText, score, SYNERGY_TEXT, UNTESTED_TEXT } from "./format.ts";
import type { StudyFacts } from "./inputs.ts";
import { answerPathLines } from "./locations.ts";
import { Board, validScore } from "./read.ts";
import { cellId, factorId, factorKey, ORIGINAL_BASELINE, type Settlement } from "./shapes.ts";
// 信念本(方法.md 6.10):程序从已公开的记录现算,不存。数字全由 rules 的函数算,这里只挑出来写成字。
// 只读这几样:已关阶段的提交、按 Board.runPublic 公开的运行、决定、结算、解封;封存、第一次预测、退回一律不读。
/** 范围怎么换算成把握(方法.md 6.2、6.3),写在开头。只说程序怎么用这三个数,不要求研究者照着算。 */
function rangeToBelief(lambda: number): string {
return (
`范围怎么换算成把握:每人给的三个数(一成 / 一半 / 九成可能低于多少),程序画成一条"低于某个分数的把握"折线:` +
`一成到一半、一半到九成之间各放四成把握,一成以下、九成以上各放一成,铺成两条尾巴,尾巴宽度等于同一侧的半宽。` +
`各人的折线等权平均,再留 ${pct(lambda)} 的把握均匀铺在"全部折线连尾巴、两边各再扩出一半宽度"的区间上,代表"大家可能都说错了"。` +
`合起来就是下面说的"合并范围"。批内最好期望、比特、惊讶都由程序按它算,研究者不用算。`
);
}
/** 信念本全文。round 是读者所在的轮次:之前各轮没选中、现在留在候选池的候选,不附它们之前的合并范围;导出时写 null。 */
export function beliefBook(ledger: Ledger, study: StudyFacts, round: number | null): string {
return beliefBookOf(new Board(ledger), study, round);
}
export function beliefBookOf(board: Board, study: StudyFacts, round: number | null): string {
const latestRound = Math.max(0, ...[...board.stages.values()].map((s) => s.round));
// 读者在某一轮里时,之前各轮没选中、现在留池的候选不附旧范围(大家要重新给);导出时照常给出。
// 离池名单按读者所在的轮次判,导出时按"下一轮"判。
const carried = round === null ? [] : board.carried(round);
const width = bitWidth(study.epsilon);
const publicRuns = board.runs().filter((r) => board.runPublic(r));
const baseRun = board.publicRunOf(ORIGINAL_BASELINE)!.result!.process;
const lines: string[] = [rangeToBelief(study.lambda), ""];
// 分数:基线链与手里最好
const chain = [ORIGINAL_BASELINE];
for (let r = 1; r <= latestRound; r++) {
const next = board.baselineOf(r + 1);
if (next !== chain.at(-1)) chain.push(next);
}
const valid = publicRuns.flatMap((r) => {
const v = validScore(r.result!.judged);
return v === null ? [] : [v];
});
const best = valid.reduce<number | null>((b, v) => (b === null || improvement(b, v, study.direction) > 0 ? v : b), null);
lines.push(
`**分数**:基线 ${chain.map((b) => `${b} ${score(board.baselineScore(b))}`).join(" → ")};` +
`手里最好 ${best === null ? "还没有有效分数" : score(best)}(${study.finalMetric});` +
`剩余正式运行 ${runBalance(study.totalRuns, board.runStates()).balance} 次。`,
"",
);
// 3 选批历史和各次运行结果
lines.push("**选批历史与各次运行**");
const batches = board.decisions("batch");
if (batches.length === 0) lines.push("- 还没有选批");
for (const d of batches) {
const o = d.output;
lines.push(
`- 第 ${d.round} 轮选批:本批基线 ${score(d.input.baseline)},选中 ${o.chosen.length === 0 ? "(没有)" : o.chosen.join("、")},` +
`批内最好期望 ${num(o.chosenValue)},分数尺读数 ${num(o.scaleReading)}`,
);
const mixtures = forwardMixtures(d.input.candidates, d.input.lambda);
d.input.candidates.forEach((c, i) => {
const chosen = o.chosen.includes(c.id);
const m = o.candidates.find((x) => x.id === c.id)!;
if (round !== null && d.round < round && carried.includes(c.id)) {
lines.push(` - ${c.id}:没选中,留在本轮候选池;之前的合并范围不附,大家重新给范围`);
return;
}
const head = ` - ${c.id}:合并范围 ${rangeText(m.merged)},${m.inPool ? "入池" : "没入池"},${chosen ? "入选" : "没选中"}`;
const run = board.publicRunOf(c.id);
if (!chosen || run === undefined || run.plan.round !== d.round) return void lines.push(head);
const judged = run.result!.judged;
const v = validScore(judged);
const bits = v === null ? "" : `;世界比特 ${num(worldBits(mixtures[i]!, d.input.baseline, width))},惊讶 ${measured(surprise(mixtures[i]!, d.input.baseline, width, v).bits, (x) => `${num(x)} 比特`)}`;
lines.push(
`${head};运行 ${run.plan.run}:${judged === null ? "没有判分" : `${judged.verdict === "valid" ? "算数" : "不算数"}(${judged.verdictReason}),分数 ${measured(judged.score, score)}`}${bits}`,
` ${resourcesText(run.result!.process, baseRun, study.wallSeconds)}`,
);
if (judged !== null) {
const others = Object.keys(judged.metrics).filter((k) => k !== study.finalMetric).sort(compareIds);
if (others.length > 0) lines.push(` 其余指标:${others.map((k) => `${k} ${measured(judged.metrics[k]!, num)}`).join(";")}`);
lines.push(...answerPathLines(judged, board.code(c.id).locations!, null).map((x) => ` ${x}`));
}
});
}
// 1 按因子分行;2 每次消融一句结论和押的因子
lines.push("", "**按因子**(每个因子一行:名称(关键设定)、在哪个候选和基线上、关掉少多少分、撑几成、测前范围、比特、确认与否定、答案出自哪里)");
const protocols = board.protocols();
if (protocols.length === 0) lines.push("- 还没有冻结的协议");
const settlements = board.settlements();
for (const { candidate, protocol } of protocols) {
const s = settlements.find((x) => x.candidate === candidate);
protocol.factors.forEach((f, i) => {
const key = factorKey(i);
const title = f.keySettings.trim() === "无" ? f.name : `${f.name}(${f.keySettings})`;
const head = `- ${factorId(candidate, key)} ${title}${f.continues === null ? "" : `(延续 ${f.continues})`}:候选 ${candidate},基线 ${protocol.baseline}`;
lines.push(s === undefined ? `${head};没做消融` : `${head};${cellText(board, s, key, study, baseRun)}`);
});
}
for (const s of settlements) lines.push(...ablationSummary(board, s));
// 4 战绩
lines.push("", "**战绩**(只显示,不改任何权重;偏高偏低都按分数原值判)");
const samples: Array<{ researcher: string; phase: Phase; range: Range; value: number }> = batches.flatMap((d) =>
d.input.candidates.flatMap((c) => {
const run = board.publicRunOf(c.id);
const v = run === undefined || run.plan.round !== d.round ? null : validScore(run.result!.judged);
return v === null ? [] : c.ranges.map((r) => ({ researcher: r.researcher, phase: "forward" as const, range: r.range, value: v }));
}),
);
for (const s of settlements)
for (const c of s.input.cells) {
if (c.outcome.kind !== "valid") continue;
for (const e of s.input.explanations) {
const range = e.cells.find((x) => x.factor === c.factor)!.range;
samples.push({ researcher: e.author, phase: "ablation", range, value: c.outcome.score });
}
}
const tallies = trackRecord(samples);
if (tallies.length === 0) lines.push("- 还没有实测可以核对");
for (const { researcher, phase, tally: t } of tallies) {
lines.push(
`- ${researcher},${phase === "forward" ? "正向" : "消融"} ${t.total} 次:中位数偏高 ${t.medianHigh}、偏低 ${t.medianLow};` +
`穿过上沿 ${t.aboveTop}、下沿 ${t.belowBottom};包住 ${t.covered};落点 ` +
(Object.keys(PLACEMENT_TEXT) as Array<keyof typeof PLACEMENT_TEXT>).map((k) => `${PLACEMENT_TEXT[k]} ${t.placements[k]}`).join(","),
);
}
lines.push(" (说得准时,约八成落在一成到九成之间,四段约 1 : 4 : 4 : 1)");
const items = board.unsealed().flatMap((u) => {
const run = board.publicRunOf(u.candidate);
const v = run === undefined ? null : validScore(run.result!.judged);
const d = batches.find((x) => run !== undefined && x.round === run.plan.round);
if (v === null || d === undefined) return [];
return [{ candidate: u.candidate, author: u.author, authorRange: u.range, mergedRange: d.output.candidates.find((c) => c.id === u.candidate)!.merged, value: v }];
});
const avm = authorVsMerged(items, study.direction);
lines.push(
avm.length === 0
? "- 作者自评 vs 大家合并:还没有跑过、有有效成绩的候选"
: `- 作者自评 vs 大家合并:${avm.map((x) => `${x.candidate}(作者 ${x.author})作者比大家乐观 ${num(x.optimism)},实测落在作者的${PLACEMENT_TEXT[x.authorPlacement]}、大家的${PLACEMENT_TEXT[x.mergedPlacement]}`).join(";")}`,
);
// 5 基线变了离开候选池的旧候选
const left = board.leftPool(round ?? latestRound + 1);
lines.push("", `**因为基线变了而离开候选池的旧候选**:${left.length === 0 ? "没有" : left.join("、")}(可以重新提出)`);
return lines.join("\n");
}
/** 一个因子在一次消融里那一格的结局(方法.md 6.6"一格的几种结局"、6.10 第 1 条)。 */
function cellText(board: Board, s: { round: number } & Settlement, key: string, study: StudyFacts, baseRun: ProcessResult): string {
const out = s.output.cells.find((c) => c.factor === key)!;
const outcome = s.input.cells.find((c) => c.factor === key)!.outcome;
const cell = cellId(s.candidate, key);
const parts: string[] = [];
if (outcome.kind === "valid") {
const drop = improvement(outcome.score, s.input.full, study.direction);
parts.push(
out.support! < 0
? `关掉 ${key} 反而更好 ${score(-drop)} 分(实测;关掉后 ${score(outcome.score)},撑 ${pct(out.support!)})`
: `关掉后 ${score(outcome.score)},少 ${score(drop)} 分,撑 ${pct(out.support!)}`,
);
if (s.output.finding.chosen === key) parts.push("已采纳为下一轮基线(消融中发现)");
} else if (outcome.kind === "untested") {
const c = board.decision("cells", s.round)!.output.cells.find((x) => x.factor === key)!;
parts.push(`没测:${UNTESTED_TEXT[c.reason!]}(${c.bestCombo === null ? "没有可选组合" : `含它的最好组合净值 ${num(c.bestCombo.net)}`})`);
} else if (outcome.kind === "cannotRun") {
const sub = board.published("ablationImplementation").filter((x) => x.target === cell).at(-1)!;
parts.push(`跑不起来:${sub.content.cannotRun}`);
} else if (outcome.kind === "joint") {
parts.push(`证据不足(测的是 ${outcome.off.join(" + ")} 一起关${outcome.score === null ? "" : `,分数 ${score(outcome.score)}`})`);
} else {
parts.push(`证据不足:${board.runOf(cell)!.result!.judged!.verdictReason}`);
}
parts.push(`测前合并范围 ${rangeText(out.merged)}`);
if (out.worldBits !== null) parts.push(`世界比特 ${num(out.worldBits)}`);
if (out.surprise !== null) parts.push(`惊讶 ${measured(out.surprise.bits, (x) => `${num(x)} 比特`)}`);
if (outcome.kind === "valid") {
const verdicts = s.output.judgments.map((j) => ({ id: j.id, v: j.verdicts.find((x) => x.factor === key)!.verdict }));
const of = (v: string) => verdicts.filter((x) => x.v === v).map((x) => x.id).join("、") || "无";
parts.push(`确认 ${of("confirmed")},否定 ${of("denied")}`);
}
const run = board.runOf(cell);
// 答案路径各行自成一句,并进这一行时去掉句号再用分号接。
if (run?.result?.judged) parts.push(...answerPathLines(run.result.judged, board.code(s.candidate).locations!, key).map((x) => x.replace(/。$/, "")));
if (run !== undefined) parts.push(resourcesText(run.result!.process, baseRun, study.wallSeconds));
return parts.join(";");
}
/** 每次消融一句程序读出的结论,外加押的因子那一行(方法.md 6.10 第 2 条、6.8)。 */
function ablationSummary(board: Board, s: { round: number } & Settlement): string[] {
const o = s.output;
const label = o.label;
const sums = label.supportSum === null ? "" : `,各格撑几成之和 ${pct(label.supportSum)},相对交互量 ${num(label.interaction!)}`;
const groups = o.inseparable.length === 0 ? "" : `;现有测法分不开:${o.inseparable.map((g) => g.join(" 与 ")).join(";")}`;
const lines = [
`- 第 ${s.round} 轮消融(候选 ${s.candidate},基线 ${score(s.input.baseline)},完整 ${score(s.input.full)},总提升 ${score(o.gain)}):` +
`${SYNERGY_TEXT[label.label]}${sums}${groups}。总提升很小时,撑几成和标签大半是噪声。`,
];
const bet = board.decision("bet", s.round)?.output;
if (bet === undefined) return lines;
const tail = "这只是按公式推出来的参考,你可以不跟。";
if (bet.decision === "bet") lines.push(` 程序按方向收益押的因子:${factorId(s.candidate, bet.factor)}(预测收益 ${num(bet.predictedGain)})。${tail}`);
else if (bet.decision === "offBetter") {
lines.push(` 不押:${bet.offBetter.map((x) => `关掉 ${x.factor} 反而更好 ${score(x.better)} 分`).join(";")}。${tail}`);
} else lines.push(` 不押:估计收益不为正。${tail}`);
return lines;
}
+302
View File
@@ -0,0 +1,302 @@
import { adoptionLine, compareIds, runBalance } from "../rules/index.ts";
import type { Checklist, SubmissionKind } from "../vocab/index.ts";
import { beliefBookOf } from "./belief-book.ts";
import { directionText, json, measured, score } from "./format.ts";
import type { TaskRequest } from "./inputs.ts";
import { answerPathLines } from "./locations.ts";
import { Board, type Run } from "./read.ts";
import { entryId, explanationId, factorKey, splitCell, type FrozenProtocol } from "./shapes.ts";
// 任务材料的形状,和研究者各任务共用的几块(方法.md 第三部分"给研究者的材料顺序")。
/** 一条能被查询到的记录:编号、标题、全文。查询工具只在这份材料的记录里找。 */
export type MaterialRecord = { id: string; title: string; text: string };
/** 第一条消息里的一节。level 1 是"你自己的""公共材料"这类分组标题,level 2 是其下的一节。 */
export type MaterialSection = { level: 1 | 2; heading: string; text: string };
/** 一个能读的代码版本:flow 把它取出到 dir(`<codeRoot>/<版本号>`)。 */
export type ReadableVersion = { version: string; label: string; dir: string };
/** 一次任务的全部材料,整份存进账本的"模型调用开始"记录(结构.md 第四部分第 10 种)。 */
export type TaskMaterial = {
[K in SubmissionKind]: {
kind: K;
actor: string;
stageId: string;
round: number;
object: string | null;
sections: MaterialSection[];
records: MaterialRecord[];
firstMessage: string;
checklist: Extract<Checklist, { kind: K }>;
readable: ReadableVersion[];
};
}[SubmissionKind];
/** 一次任务的中文名,写进"任务与本轮"和第一条消息。 */
export const KIND_TEXT: Readonly<Record<SubmissionKind, string>> = {
entry: "第零轮闭门写入口",
discussion: "第零轮讨论",
claim: "第零轮认领入口",
proposal: "提案",
firstPrediction: "第一次预测",
critique: "挑刺",
response: "回应挑刺",
unification: "统一合并的候选",
secondPrediction: "第二次预测",
implementation: "实现冻结协议",
repair: "按核验意见修复",
verification: "核验实现",
explanation: "写解释",
ablationImplementation: "消融实现",
ablationVerification: "消融核验",
wrapUp: "本轮收尾",
};
/** 正在拼的一份材料。各材料函数往里放节、记录和能读的版本。 */
export class Draft {
readonly board: Board;
readonly round: number;
readonly sections: MaterialSection[] = [];
readonly records: MaterialRecord[] = [];
readonly readable: ReadableVersion[] = [];
readonly req: TaskRequest;
constructor(req: TaskRequest, board: Board) {
this.req = req;
this.board = board;
this.round = board.stage(req.stageId).round;
}
group(heading: string, text: string): void {
this.sections.push({ level: 1, heading, text });
}
section(heading: string, lines: readonly string[]): void {
this.sections.push({ level: 2, heading, text: lines.join("\n") });
}
/** 登记一条可查询的记录,返回它的编号。同一编号登记两次说明编号规则撞了,查询会拿错内容,直接报错。 */
record(id: string, title: string, content: unknown): string {
if (this.records.some((r) => r.id === id)) throw new Error(`材料里编号 ${id} 登记了两次(${title})`);
this.records.push({ id, title, text: typeof content === "string" ? content : json(content) });
return id;
}
read(object: string, label: string): void {
const version = this.board.code(object).version;
if (!this.readable.some((r) => r.version === version)) {
this.readable.push({ version, label, dir: `${this.req.codeRoot}/${version}` });
}
}
/** 能读的版本在第一条消息里的写法。 */
readableLines(): string[] {
return this.readable.map((r) => `- ${r.label}:版本 ${r.version},目录(只读)${r.dir}`);
}
}
/** 第 round 轮的本批基线:对象编号、分数、采纳线(方法.md 6.5)。 */
export function baselineLines(d: Draft): string[] {
const s = d.req.study;
const base = d.board.baselineOf(d.round);
const b = d.board.baselineScore(base);
const { line } = adoptionLine({ baseline: b, knownBest: s.knownBest, epsilon: s.epsilon, direction: s.direction });
return [
`本轮基线 ${base}:最终指标 ${score(b)}。采纳线 ${score(line)}:新结果要比当前基线严格好过这么多才采纳,只看实测分数。`,
];
}
/** 研究者的"任务与本轮"。 */
export function researcherTaskSection(d: Draft): void {
const { study: s, actor, stageId, kind, object } = d.req;
const roster = d.board.stage(stageId).roster;
d.section("任务与本轮", [
`你是研究者 ${actor},现在是第 ${d.round} 轮(阶段编号 ${stageId})。本次任务:${KIND_TEXT[kind]}${object === null ? "" : `,对象 ${object}`}。`,
`本阶段名单:${roster.join("、")}(${roster.length} 人);每人每轮最多 ${s.proposalLimit} 条提案,每条最多 ${s.factorLimit} 个因子。`,
`最终指标:${s.finalMetric}(${directionText(s.direction)})。精度 ε = ${s.epsilon}:变化在 ±ε 以内算基本不变;` +
`合并范围里"比本批基线差 ε 以上"的把握过半的候选不进选批的池。范围一律按最终指标的原值写。`,
`已知最好:${s.knownBest === null ? "没有" : score(s.knownBest)}。`,
`剩余正式运行 ${runBalance(s.totalRuns, d.board.runStates()).balance} 次(运行次数是唯一的预算)。`,
"",
...taskPoints(d),
]);
}
/** "任务与本轮"里的任务要点:三个角色都看。任务说明全文放进查询。 */
export function taskPoints(d: Draft): string[] {
const s = d.req.study;
d.record("task", "任务说明全文(TASK.md)", s.taskText);
return [
"### 任务要点",
"",
`- 最终指标 ${s.finalMetric}(${directionText(s.direction)}),由宿主从原始产物独立判分,不采信候选自报的成绩`,
`- 每次正式运行的墙钟时限 ${s.wallSeconds} 秒,超时的运行没有成绩,次数照扣`,
`- 已知最好:${s.knownBest === null ? "没有" : score(s.knownBest)}`,
"- 任务说明全文(评测收下什么样的结果、产物写在哪、不许改哪些文件):用 research_query 查编号 task",
];
}
/** 研究者能读的代码:本批基线,以及运行已经公开的各份代码(方法.md 第三部分"研究者能做的事")。 */
export function codeSection(d: Draft): void {
d.read(d.board.baselineOf(d.round), "本批基线");
for (const r of d.board.runs()) {
if (r.plan.object !== null && r.plan.purpose !== "baseline" && d.board.runPublic(r)) {
d.read(r.plan.object, `${r.plan.purpose === "forward" ? "候选" : "消融格"} ${r.plan.object} 的代码`);
}
}
d.section("基线", [...baselineLines(d), "", "能读的代码(用 read_source 读):", ...d.readableLines()]);
}
export function beliefSection(d: Draft): void {
d.section("信念本", [beliefBookOf(d.board, d.req.study, d.round)]);
}
/** 已登记指标清单(方法.md 第五部分"指标"):程序采集的,加冻结协议里登记过的假设专用指标。 */
export function metricsSection(d: Draft): void {
const registered = registeredMetrics(d.board);
d.section("已登记指标清单", [
"程序每次都会采集(不用登记):",
...d.req.study.metrics.map((m) => `- ${m.id}(${m.layer === "final" ? "最终指标" : "常规辅助"},${directionText(m.direction)},单位 ${m.unit}):${m.meaning}`),
"",
"研究者在冻结协议里登记过的假设专用指标(要测的量这里已经有,就写进 reused,不要重新定义;引用时沿用原登记的\"测不到时怎么办\"):",
...(registered.length === 0
? ["(还没有)"]
: registered.map((m) => `- ${m.id}(候选 ${m.candidate} 登记,${directionText(m.direction)},单位 ${m.unit},测不到时${m.missing === "recordMissing" ? "只记缺失" : "整次作废"}):${m.meaning};怎么测:${m.howMeasured}`)),
]);
}
export function registeredMetrics(board: Board) {
return board.protocols().flatMap((p) => p.protocol.metrics.defined.map((m) => ({ ...m, candidate: p.candidate })));
}
/**
* 研究者都能查的已公开记录:第零轮的入口(各版)、讨论、志愿、入口分配(方法.md 4.2"第零轮结束后,它的全部记录
* 对所有研究者公开"),已公开的提案、挑刺、解释、收尾,冻结协议,已公开的运行。
* 全文只放进查询,第一条消息里的"研究记录"一节只列编号和标题;材料里提到的编号都在这里查得到。
*/
export function publicRecords(d: Draft): void {
const b = d.board;
for (const [author, versions] of entryVersions(b)) {
versions.forEach((v, i) =>
v.entries.forEach((e, j) =>
d.record(v.ids[j]!, `入口 ${v.ids[j]}:${e.name}(作者 ${author}${i === versions.length - 1 ? "" : ",已被整份替换,作废"})`, e.body),
),
);
}
for (const s of b.published("discussion")) d.record(`${s.stageId}.${s.author}`, `${s.author} 在第零轮 ${s.stageId} 的讨论`, s.content.body);
for (const s of b.published("claim")) d.record(`${s.stageId}.${s.author}`, `${s.author} 的志愿`, s.content.choices.join("、"));
for (const a of b.decisions("entries")) d.record("entry-assignment", "第零轮的入口分配", a.output);
for (const p of b.published("proposal")) d.record(p.item!, `提案 ${p.item}(${p.author},第 ${p.round} 轮)`, p.content);
for (const c of b.published("critique")) d.record(c.item!, `挑刺 ${c.item}(${c.author} 挑提案 ${c.target})`, c.content);
for (const { candidate, protocol } of b.protocols()) d.record(candidate, `候选 ${candidate} 的冻结协议`, protocol);
for (const e of b.published("explanation")) {
e.content.explanations.forEach((x, i) => d.record(explanationId(e.round, e.author, i + 1), `解释 ${explanationId(e.round, e.author, i + 1)}(${e.author})`, x));
}
for (const w of b.published("wrapUp")) d.record(`${w.stageId}.${w.author}`, `${w.author} 第 ${w.round} 轮的收尾`, w.content.body);
for (const r of b.runs()) if (r.plan.purpose !== "baseline" && b.runPublic(r)) d.record(r.plan.run, `运行 ${r.plan.run}`, [...runLines(d, r), "", json(r.result!.judged)].join("\n"));
d.section("研究记录", [
"已经公开的记录,一行一条;全文用 research_query 按编号查。",
...d.records.map((r) => `- ${r.id}:${r.title}`),
]);
}
/** 一次运行写成几行:对象、判分、分数、答案路径(位置表现算)。全文(判分原样)在查询记录里。 */
export function runLines(d: Draft, r: Run): string[] {
const j = r.result!.judged;
const lines = [`运行 ${r.plan.run}:${r.plan.purpose === "forward" ? "正向,候选" : "消融,格"} ${r.plan.object},第 ${r.plan.round} 轮`];
if (j === null) return [...lines, `没有判分:${r.result!.process.ending}`];
const owner = r.plan.purpose === "forward" ? { candidate: r.plan.object!, key: null } : splitCell(r.plan.object!);
return [
...lines,
`${j.verdict === "valid" ? "算数" : "不算数"}(${j.verdictReason});分数 ${measured(j.score, score)}`,
...answerPathLines(j, d.board.code(owner.candidate).locations!, owner.key),
];
}
// ---------------------------------------------------------------------------
// 入口(方法.md 4.2)
// ---------------------------------------------------------------------------
/**
* 各人已公开的入口清单的各版:写入口时交的一版,讨论时整份替换的各版;编号按作者接着往后编(方法.md 4.2),
* 只有最后一版有效。
*/
export function entryVersions(board: Board): Map<string, Array<{ ids: string[]; entries: Array<{ name: string; body: string }> }>> {
const subs = [...board.published("entry"), ...board.published("discussion")].sort((a, b) => a.seq - b.seq);
const out = new Map<string, Array<{ ids: string[]; entries: Array<{ name: string; body: string }> }>>();
for (const s of subs) {
const entries = s.content.entries;
if (entries === undefined) continue;
const versions = out.get(s.author) ?? [];
const used = versions.reduce((n, v) => n + v.ids.length, 0);
versions.push({ ids: entries.map((_, i) => entryId(s.author, used + i + 1)), entries });
out.set(s.author, versions);
}
return new Map([...out].sort(([a], [b]) => compareIds(a, b)));
}
/** 有效入口:各人最后一版清单里的每个入口。 */
export function validEntries(board: Board): Array<{ id: string; author: string; name: string; body: string }> {
return [...entryVersions(board)].flatMap(([author, versions]) => {
const last = versions.at(-1)!;
return last.entries.map((e, i) => ({ id: last.ids[i]!, author, ...e }));
});
}
// ---------------------------------------------------------------------------
// "你自己的"(方法.md 第三部分"给研究者的材料顺序"第 2 条)
// ---------------------------------------------------------------------------
/**
* 你认领的入口、私人笔记、你自己的候选的运行与消融结果(含答案路径、实验员的提醒)、你在本阶段已经交的。
* extra 是某些任务另放在这里的东西(例如第二次预测时本人第一次给这个候选的三个数)。
*/
export function ownSection(d: Draft, extra: ReadonlyArray<{ heading: string; lines: string[] }>): void {
const { actor, stageId } = d.req;
d.group("你自己的", "这一节是你自己的东西,先读它,再读后面大家共用的公共材料。别人的判断是参考,不是答案。");
const assignment = d.board.decisions("entries").at(-1);
if (d.round >= 1 && assignment !== undefined) {
const mine = assignment.output.find((a) => a.researcher === actor);
const entry = mine === undefined ? undefined : validEntries(d.board).find((e) => e.id === mine.entry);
if (entry !== undefined) {
d.section("你认领的入口", [
`${entry.id}:${entry.name}(作者 ${entry.author};${mine!.rank === null ? "志愿用完后按编号分到" : `你的第 ${mine!.rank} 志愿`})`,
entry.body,
"",
d.round === 1 ? "从这里开始看。它是起点,不是结论;提案不必只围着它转。" : "第 1 轮的起点;之后是否沿用由你定。",
]);
}
}
const note = d.board.note(actor);
d.section("你的私人笔记", [note ?? "(还没写过)"]);
if (d.round >= 1) d.section("你自己的候选的结果", ownCandidateLines(d));
for (const x of extra) d.section(x.heading, x.lines);
const mine = d.board.mine(actor, stageId);
d.section("你在本阶段已经交的", mine.length === 0 ? ["(还没有)"] : mine.map((s) => `- ${s.kind}${s.target === null ? "" : `(对象 ${s.target})`},第 ${s.version} 版:\n${json(s.content)}`));
}
/** 本人提出的候选(合并的原提案里有本人的):运行结果、答案路径、实验员的提醒。 */
function ownCandidateLines(d: Draft): string[] {
const b = d.board;
const own = b.protocols().filter((p) => b.authorsOf(p.candidate).includes(d.req.actor));
if (own.length === 0) return ["(你还没有进了冻结协议的候选)"];
return own.flatMap(({ candidate }) => {
const lines = [`候选 ${candidate}:`];
for (const r of b.runs().filter((x) => b.runPublic(x) && x.plan.object !== null && (x.plan.object === candidate || splitOwner(x) === candidate))) {
lines.push(...runLines(d, r).map((x) => ` ${x}`));
}
const impl = b.implementation(candidate);
if (impl !== undefined) lines.push(` 实验员的提醒:${impl.content.concerns}`);
return lines;
});
}
const splitOwner = (r: Run): string | null => (r.plan.purpose === "ablation" ? splitCell(r.plan.object!).candidate : null);
/** 关掉一个因子时换成什么:提案没写就回到基线在这件事上的做法(方法.md 4.3 ⑬)。 */
export const offText = (f: FrozenProtocol["factors"][number]): string => f.offBehavior ?? "没写,回到基线在这件事上的做法";
/** 一份冻结协议的因子清单写成文字:编号、名称、关键设定、关掉时换成什么。 */
export function factorLines(p: FrozenProtocol): string[] {
return p.factors.map((f, i) => `- ${factorKey(i)} ${f.name}:${f.method}(关键设定:${f.keySettings};关掉时换成:${offText(f)})`);
}
+54
View File
@@ -0,0 +1,54 @@
import type { Checklist, SubmissionKind } from "../vocab/index.ts";
import * as assigned from "./assigned.ts";
import { Draft, type TaskMaterial } from "./blocks.ts";
import type { TaskRequest } from "./inputs.ts";
import { closing, composeFirstMessage } from "./prompts.ts";
import { Board } from "./read.ts";
import * as researcher from "./researcher.ts";
// 生成一份任务材料:每种提交种类一个材料函数(任务书 09 第二部分),这里只按种类挑函数、拼第一条消息。
const BUILDERS: { [K in SubmissionKind]: (d: Draft) => Extract<Checklist, { kind: K }> } = {
entry: researcher.entry,
discussion: researcher.discussion,
claim: researcher.claim,
proposal: researcher.proposal,
firstPrediction: researcher.firstPrediction,
critique: researcher.critique,
response: researcher.response,
unification: researcher.unification,
secondPrediction: researcher.secondPrediction,
explanation: researcher.explanation,
wrapUp: researcher.wrapUp,
implementation: assigned.implementation,
repair: assigned.repair,
verification: assigned.verification,
ablationImplementation: assigned.ablationImplementation,
ablationVerification: assigned.ablationVerification,
};
export function buildMaterial(req: TaskRequest): TaskMaterial {
const board = new Board(req.ledger);
const d = new Draft(req, board);
const checklist = BUILDERS[req.kind](d);
// 重新发起时放上一次被退回的原因(只放本人这一任务的退回,任务书 09 第五部分)。
const rejection = board.lastRejection(req.stageId, req.actor, req.kind, req.object);
const firstMessage = composeFirstMessage(
req.prompts.tasks[req.kind].text,
rejection,
d.sections,
closing(req.kind, req.object, req.study.proposalLimit),
);
return {
kind: req.kind,
actor: req.actor,
stageId: req.stageId,
round: d.round,
object: req.object,
sections: d.sections,
records: d.records,
firstMessage,
checklist,
readable: d.readable,
} as TaskMaterial;
}
+84
View File
@@ -0,0 +1,84 @@
import type { Placement, SynergyLabel } from "../rules/index.ts";
import type { Direction, Measured, ProcessResult, Range } from "../vocab/index.ts";
// 材料和信念本里所有数字的写法,只此一份(任务书 09 第四部分:参考项目写了两份、精度不一样)。
// 原值都在账本里,查询工具查得到。
/** 分数和分数上的范围:6 位有效数字,与研究者自己写的三个数同一精度,ε 小的任务上相邻两格也写得开。 */
export const score = (x: number): string => String(Number(x.toPrecision(6)));
/** 程序算出的导出量(比特、期望、收益、倍数):4 位有效数字。 */
export const num = (x: number): string => String(Number(x.toPrecision(4)));
/** 比例写成百分数,两位有效数字。 */
export const pct = (x: number): string => `${Number((x * 100).toPrecision(2))}%`;
/** 三个数:一成 / 一半 / 九成。 */
export const rangeText = (r: Range): string => `${score(r.q1)} / ${score(r.q5)} / ${score(r.q9)}`;
/** 测不到的量写原因,不写 0(方法.md 第七部分)。 */
export const measured = (m: Measured, show: (x: number) => string): string =>
m.status === "measured" ? show(m.value) : `测不到:${m.reason}`;
export const directionText = (d: Direction): string => (d === "maximize" ? "越大越好" : "越小越好");
/** 配合关系标签的中文(方法.md 6.6 那张表,含"给下一轮的话")。 */
export const SYNERGY_TEXT: Readonly<Record<SynergyLabel, string>> = {
offBetter: "有因子关掉反而更好,不贴标签",
incomplete: "不全,不贴标签",
indispensable: "缺一不可:要当成一个整体来改",
complementary: "互补:改其中一个时要顾及其余的",
substitute: "替代:几个因子在做同一件事,想想它们合起来做到了什么",
independent: "各管各:可以分头优化",
};
/** 落点四段(方法.md 第二部分)。 */
export const PLACEMENT_TEXT: Readonly<Record<Placement, string>> = {
belowQ1: "低于一成",
q1ToQ5: "一成到一半",
q5ToQ9: "一半到九成",
aboveQ9: "高于九成",
};
/** 没测的原因(方法.md 6.7"选格"第 3 条,以及第十一部分的对照规则)。 */
export const UNTESTED_TEXT: Readonly<Record<"budget" | "value" | "notDrawn" | "ruleNone", string>> = {
budget: "剩余次数不够",
value: "信息价值不足以抵过成本",
notDrawn: "随机没抽中",
ruleNone: "规则是都不测",
};
const ENDING_TEXT: Readonly<Record<ProcessResult["ending"], string>> = {
notStarted: "没启动",
exited: "正常退出",
timeout: "超时",
cancelled: "被从外面停掉",
};
/**
* 一次运行的资源(方法.md 第八部分"材料里同时写'是基线的几倍'和'占单次时限的几成'")。
* 基线测不到时倍数写算不出。
*/
export function resourcesText(p: ProcessResult, base: ProcessResult, wallLimit: number): string {
if (p.ending === "notStarted") return `进程:没启动(${p.reason})`;
const one = (name: string, m: Measured, b: Measured, unit: (x: number) => string, limited: boolean): string => {
if (m.status !== "measured") return `${name} 测不到:${m.reason}`;
const extra = [b.status === "measured" && b.value > 0 ? `基线的 ${num(m.value / b.value)} 倍` : "基线的几倍算不出"];
if (limited) extra.push(`占单次时限 ${wallLimit} 秒的 ${pct(m.value / wallLimit)}`);
return `${name} ${unit(m.value)}${extra.length > 0 ? `(${extra.join(",")})` : ""}`;
};
// 基线运行一定启动过(它有有效分数才当得了基线)。
const b = base as Exclude<ProcessResult, { ending: "notStarted" }>;
return [
`进程:${ENDING_TEXT[p.ending]}${p.ending === "exited" ? `,退出码 ${p.exitCode}` : ""}`,
one("CPU", p.cpuSeconds, b.cpuSeconds, (x) => `${num(x)} 秒`, true),
one("墙钟", p.wallSeconds, b.wallSeconds, (x) => `${num(x)} 秒`, true),
one("峰值内存", p.peakMemoryBytes, b.peakMemoryBytes, (x) => `${num(x / 2 ** 20)} MiB`, false),
].join(";");
}
/** JSON 原文,查询记录和材料里放整份内容时用。 */
export const json = (value: unknown): string => JSON.stringify(value, null, 2);
/** 代码围栏:比正文里最长的一串反引号多一个,正文里有 ``` 也不会提前收尾。 */
export function fenced(body: string, lang: string): string {
const longest = Math.max(2, ...[...body.matchAll(/`+/g)].map((m) => m[0].length));
const fence = "`".repeat(longest + 1);
return `${fence}${lang}\n${body}\n${fence}`;
}
+28
View File
@@ -0,0 +1,28 @@
// materials:每个任务能看到哪些材料、第一条消息怎么写、查询在材料范围内回答什么(docs/结构.md 第二部分),
// 以及账本里只存成 Json 的几样内容的形状和编号规则(用户 2026-10-03 决定 2)。
export { beliefBook } from "./belief-book.ts";
export { type MaterialRecord, type MaterialSection, type ReadableVersion, type TaskMaterial } from "./blocks.ts";
export { buildMaterial } from "./build.ts";
export { type CodeChanges, type PromptFile, type Prompts, type StudyFacts, type TaskRequest } from "./inputs.ts";
export { queryMaterial, readPrompts } from "./prompts.ts";
export {
callId,
candidateId,
cellId,
clauseKey,
critiqueId,
DECISION_KINDS,
entryId,
explanationId,
factorId,
factorKey,
ORIGINAL_BASELINE,
proposalId,
splitCell,
type DecisionKind,
type DecisionShapes,
type DedupOutput,
type FrozenProtocol,
type LocationTable,
type Settlement,
} from "./shapes.ts";
+69
View File
@@ -0,0 +1,69 @@
import type { Ledger } from "../ledger/index.ts";
import type { Direction, MetricDefinition, Role, SubmissionKind } from "../vocab/index.ts";
// flow 交给 materials 的东西:账本里没有、只有 flow 拿得到的(任务书 09 第六部分第 1 条)。
/**
* 一份研究的事实:来自冻结的研究配置和任务。账本里的配置记录只存 Json,materials 不能引用 config、tasks,
* 所以由 flow 取好传进来。
*/
export type StudyFacts = {
finalMetric: string;
direction: Direction;
epsilon: number;
/** 已知最好,没有为 null。 */
knownBest: number | null;
/** 开放预留 λ。 */
lambda: number;
/** 正式运行总次数。 */
totalRuns: number;
/** 每人每轮提案上限 M。 */
proposalLimit: number;
factorLimit: number;
/** 最终指标和常规辅助指标的定义(tasks 的 metricDefinitions)。 */
metrics: MetricDefinition[];
/** 任务说明全文(TASK.md)。 */
taskText: string;
/** 单次正式运行的墙钟时限(秒),资源用量写"占时限几成"用。 */
wallSeconds: number;
};
/**
* 两个代码版本的比较结果。与 workspace 的 compareVersions 交回的形状相同(jev 的 codeHunks 是同样的先例),
* materials 不引用 workspace。块可以为空:空文件增删、只改可执行位都没有块,按整个文件改了算。
*/
export type LineRange = { start: number; end: number } | { after: number };
export type CodeChanges = {
files: Array<
{ path: string; change: "added" | "deleted" | "modified"; modeOnly: boolean } & (
| { binary: true }
| { binary: false; hunks: Array<{ from: LineRange; to: LineRange }> }
)
>;
diff: string;
};
/** 一份提示词文件:文字和原始字节的 sha256。 */
export type PromptFile = { text: string; hash: string };
/** 提示词目录读出来的全部文件(readPrompts)。 */
export type Prompts = { roles: Record<Role, PromptFile>; tasks: Record<SubmissionKind, PromptFile> };
/**
* 生成一份任务材料要的全部输入。全部必填,用不到的写 null:
* - changes:核验是起点到候选代码,修复是起点到上一次提交的代码,消融核验是完整代码到这一格的代码;其余为 null;
* - artifacts:解释任务要的本轮各次运行的产物(运行编号 → 文件名 → 文字);其余为 null;
* - codeRoot:能读的代码版本取出到 `<codeRoot>/<版本号>`,flow 照材料里的目录取出。
*/
export type TaskRequest = {
kind: SubmissionKind;
actor: string;
stageId: string;
/** 一个任务一个对象:候选、原提案(挑刺)或消融格;入口、讨论、志愿、提案、解释、收尾为 null。 */
object: string | null;
ledger: Ledger;
study: StudyFacts;
prompts: Prompts;
codeRoot: string;
changes: CodeChanges | null;
artifacts: Record<string, Record<string, string>> | null;
};
+141
View File
@@ -0,0 +1,141 @@
import { compareIds } from "../rules/index.ts";
import type { JudgedResult } from "../vocab/index.ts";
import type { CodeChanges } from "./inputs.ts";
import type { LocationTable } from "./shapes.ts";
// 位置表现算的两件事:答案路径、保障动手次数在不在位置表里(方法.md 第五部分"答案路径";tasks 只记读到的原样内容),
// 以及"没有归到任何因子或保障的改动"(方法.md 4.3 ⑨)。
/** 一处位置:文件,加行号范围;只写文件名表示整个文件。 */
type Place = { path: string; lines: { start: number; end: number } | null };
function parse(location: string): Place {
const m = /^([^:]+)(?::(\d+)(?:-(\d+))?)?$/.exec(location);
if (m === null) throw new Error(`代码位置 ${location} 认不出`);
if (m[2] === undefined) return { path: m[1]!, lines: null };
const start = Number(m[2]);
return { path: m[1]!, lines: { start, end: m[3] === undefined ? start : Number(m[3]) } };
}
/** 一组位置在一个文件上盖住哪些行:整个文件,或行号集合。 */
function cover(places: readonly Place[], path: string): "whole" | Set<number> {
const lines = new Set<number>();
for (const p of places) {
if (p.path !== path) continue;
if (p.lines === null) return "whole";
for (let i = p.lines.start; i <= p.lines.end; i++) lines.add(i);
}
return lines;
}
const touches = (c: "whole" | Set<number>): boolean => c === "whole" || c.size > 0;
const has = (c: "whole" | Set<number>, line: number): boolean => c === "whole" || c.has(line);
/** 连续的行号写成"文件:a-b",单行写"文件:a",与提交字段 CodeLocation 的写法相同。 */
function runs(path: string, lines: readonly number[]): string[] {
const out: string[] = [];
let i = 0;
while (i < lines.length) {
let j = i;
while (j + 1 < lines.length && lines[j + 1] === lines[j]! + 1) j++;
out.push(lines[i] === lines[j] ? `${path}:${lines[i]}` : `${path}:${lines[i]}-${lines[j]}`);
i = j + 1;
}
return out;
}
const KIND_TEXT = { added: "新增", deleted: "删除", modified: "修改" } as const;
/**
* 没有归属的改动(方法.md 4.3 ⑨"程序把两类情况交给核验员"):
* - 位置表里因子、保障两栏的位置合起来算归属;实验员自己写的"不属于任何因子"只是说明,不算归属;
* - 二进制、只改可执行位、没有块(空文件增删)的文件按整个文件改了算:位置表写到这个文件就有归属;
* - 新版本里有行的块,列出块里没被盖住的连续行;只删不加的块,删除点前后一行或整个文件被盖住就有归属,
* 否则列"文件:删除点",附删了旧版本哪几行。
* overlaps 是同一行既属于因子又属于保障的地方,只作提醒,不判越界。
*/
export function unassignedChanges(
changes: CodeChanges,
table: LocationTable,
): { unassigned: Array<{ location: string; note: string }>; overlaps: Array<{ factor: string; safeguard: string; location: string }> } {
const factorPlaces = table.factorLocations.map((f) => ({ id: f.factor, places: f.locations.map(parse) }));
const safeguardPlaces = table.safeguardLocations.map((s) => ({ id: s.safeguard, places: s.locations.map(parse) }));
const all = [...factorPlaces, ...safeguardPlaces].flatMap((x) => x.places);
const unassigned: Array<{ location: string; note: string }> = [];
for (const file of changes.files) {
const c = cover(all, file.path);
const whole = file.binary || file.modeOnly || file.hunks.length === 0;
if (whole) {
const what = file.binary ? "二进制文件" : file.modeOnly ? "只改了可执行位" : "空文件";
if (!touches(c)) unassigned.push({ location: file.path, note: `${KIND_TEXT[file.change]}(${what},按整个文件算)` });
continue;
}
for (const h of file.hunks) {
if ("start" in h.to) {
const missing: number[] = [];
for (let i = h.to.start; i <= h.to.end; i++) if (!has(c, i)) missing.push(i);
for (const location of runs(file.path, missing)) unassigned.push({ location, note: KIND_TEXT[file.change] });
continue;
}
const at = h.to.after;
if (has(c, at) || has(c, at + 1)) continue;
const old = "start" in h.from ? `旧版本第 ${h.from.start}–${h.from.end} 行` : "旧版本没有行";
const k = "start" in h.from ? h.from.end - h.from.start + 1 : 0;
unassigned.push({ location: `${file.path}:${Math.max(at, 1)}`, note: `删除 ${k} 行(${old})` });
}
}
const overlaps: Array<{ factor: string; safeguard: string; location: string }> = [];
const paths = [...new Set(all.map((p) => p.path))].sort();
for (const f of factorPlaces)
for (const s of safeguardPlaces)
for (const path of paths) {
const a = cover(f.places, path);
const b = cover(s.places, path);
if (!touches(a) || !touches(b)) continue;
if (a === "whole" && b === "whole") {
overlaps.push({ factor: f.id, safeguard: s.id, location: path });
continue;
}
const lines = [...(a === "whole" ? (b as Set<number>) : a)].filter((i) => has(b, i)).sort((x, y) => x - y);
for (const location of runs(path, lines)) overlaps.push({ factor: f.id, safeguard: s.id, location });
}
overlaps.sort((x, y) => compareIds(x.factor, y.factor) || compareIds(x.safeguard, y.safeguard));
return { unassigned, overlaps };
}
/**
* 答案路径和保障动手次数,对照位置表写成文字(方法.md 第五部分"答案路径")。合法的答案路径是 main、
* 位置表里的因子编号、保障短名。答案出自保障时单独标一行:消融格用方法原句,正向运行换一句;
* 消融格里答案出自被关掉的因子时另写一句。offFactor 是消融格关掉的因子编号,正向运行为 null。
*/
export function answerPathLines(judged: JudgedResult, table: LocationTable, offFactor: string | null): string[] {
const factors = table.factorLocations.map((f) => f.factor);
const safeguards = table.safeguardLocations.map((s) => s.safeguard);
const lines: string[] = [];
const a = judged.answerPath;
if (a.status === "unreadable") lines.push(`答案路径读不到:${a.reason}`);
else if (a.value !== "main" && !factors.includes(a.value) && !safeguards.includes(a.value)) {
lines.push(`答案路径写的是 ${a.value},不在位置表里`);
} else {
lines.push(`答案出自 ${a.value}`);
if (safeguards.includes(a.value)) {
lines.push(
offFactor === null
? `这次答案出自保障 ${a.value},分数可能主要是这条保障的效果。`
: `本格答案出自保障 ${a.value},量到的可能是这条保障的效果,而不是关掉这个因子的效果。`,
);
}
if (a.value === offFactor) lines.push(`答案出自被关掉的因子 ${a.value}:这一格的代码里它本该关着。`);
}
const t = judged.safeguardTriggers;
if (t.status === "unreadable") lines.push(`保障动手次数读不到:${t.reason}`);
else {
const names = Object.keys(t.counts).sort(compareIds);
lines.push(
names.length === 0
? "保障动手次数:没有写任何保障"
: `保障动手次数:${names.map((n) => `${n} ${t.counts[n]} 次${safeguards.includes(n) ? "" : "(不在位置表里)"}`).join(",")}`,
);
}
return lines;
}
+95
View File
@@ -0,0 +1,95 @@
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { join } from "node:path";
import { ROLES, SUBMISSION_KINDS, SUBMISSION_SCHEMAS, type SubmissionKind } from "../vocab/index.ts";
import type { MaterialSection, TaskMaterial } from "./blocks.ts";
import type { PromptFile, Prompts } from "./inputs.ts";
// 提示词文件、第一条消息的拼法、查询(任务书 09 第五部分)。
// 提示词文件是我们自己写的,不查格式;不做模板变量替换,可变的内容全由程序拼。
/**
* 读提示词目录:roles/<角色>.md 三份,tasks/<提交种类>.md 每种一份;同时算每个文件原始字节的 sha256,
* 研究开始时由 flow 冻结进研究配置记录(方法.md 4.1 第 1 条)。缺文件时 readFileSync 直接报错。
*/
export function readPrompts(dir: string): Prompts {
const read = (path: string): PromptFile => {
const bytes = readFileSync(join(dir, path));
return { text: bytes.toString("utf8"), hash: createHash("sha256").update(bytes).digest("hex") };
};
const kinds = Object.keys(SUBMISSION_KINDS) as SubmissionKind[];
return {
roles: Object.fromEntries(ROLES.map((r) => [r, read(`roles/${r}.md`)])) as Prompts["roles"],
tasks: Object.fromEntries(kinds.map((k) => [k, read(`tasks/${k}.md`)])) as Prompts["tasks"],
};
}
/** 提交字段定义里收尾要读的几项(TypeBox 生成的就是 JSON Schema)。 */
type Shape = { type?: unknown; description: string; properties?: Record<string, Shape>; required?: string[]; items?: Shape };
/** 字段里面还要写哪些字段:对象写"(a、b;可以不写:c)",对象数组写"(每条:a、b)"。 */
function inner(s: Shape): string {
const of = s.type === "object" && s.properties !== undefined ? { pre: "", o: s } : s.type === "array" && s.items?.properties !== undefined ? { pre: "每条:", o: s.items } : null;
if (of === null) return "";
const req = new Set(of.o.required ?? []);
const names = Object.keys(of.o.properties!);
const optional = names.filter((n) => !req.has(n));
return `(${of.pre}${names.filter((n) => req.has(n)).map((n) => `${n}${inner(of.o.properties![n]!)}`).join("、")}${optional.length > 0 ? `;可以不写:${optional.join("、")}` : ""})`;
}
/**
* 第一条消息最后的"现在交什么":从 vocab SUBMISSION_SCHEMAS 的字段和字段说明生成,不另写一份。
* 每个顶层字段一行:名字、里面要写的字段、字段说明。
*/
export function closing(kind: SubmissionKind, object: string | null, proposalLimit: number): string {
const shape = SUBMISSION_SCHEMAS[kind] as unknown as Shape;
const req = new Set(shape.required ?? []);
const role = SUBMISSION_KINDS[kind].role;
const done =
kind === "proposal"
? `每调用一次 research_submit 交一条提案;本轮你最多交 ${proposalLimit} 条、至少 1 条。交完想交的几条就结束回复。`
: kind === "explanation"
? `调用一次 research_submit,explanations 里最多 ${proposalLimit} 条;看回执发现交错了,用 replaces 写那份的提交编号整份替换。`
: role === "researcher"
? "调用一次 research_submit 交上就完成了;看回执发现交错了,用 replaces 写那份的提交编号整份替换。"
: "调用一次 research_submit 交上,这个任务就结束了。";
return [
...(object === null ? [] : [`- 本次对象:${object}`]),
"- 调用 `research_submit`,payload 写这些字段:",
...Object.entries(shape.properties!).map(
([name, s]) => ` - \`${name}\`${req.has(name) ? "" : "(可以不写)"}${inner(s)}:${s.description}`,
),
"- 文字部分用中文写。",
`- ${done}`,
].join("\n");
}
/**
* 拼第一条消息:任务说明文件全文 → 上一次为什么没交成(重新发起时)→ 材料各节 → 现在交什么。
* 研究者的材料各节已按"你自己的 → 公共材料"排好(方法.md 第三部分"给研究者的材料顺序")。
*/
export function composeFirstMessage(taskPrompt: string, rejection: string | null, sections: readonly MaterialSection[], end: string): string {
const parts = [taskPrompt.trimEnd()];
if (rejection !== null) {
parts.push(
[
"# 上一次为什么没交成",
"",
"这个任务上一次没有交成,最后一次被退回的原因是:",
"",
rejection,
"",
"退回原因里引用的编号和写法来自你上次交的内容,可能本身就写错了;要写的编号以下面材料里列出的为准。",
].join("\n"),
);
}
for (const s of sections) parts.push(`${s.level === 1 ? "#" : "##"} ${s.heading}\n\n${s.text}`);
parts.push(`# 现在交什么\n\n${end}`);
return parts.join("\n\n");
}
/** 查询:只在这份材料的记录里找,找不到答 null(看不到和不存在故意不区分)。agents 的查询回调就是它。 */
export function queryMaterial(material: TaskMaterial, ref: string): string | null {
const r = material.records.find((x) => x.id === ref.trim());
return r === undefined ? null : `${r.title}\n\n${r.text}`;
}
+253
View File
@@ -0,0 +1,253 @@
import type { Content, Ledger, LedgerRecord, StoredSubmission } from "../ledger/index.ts";
import { compareIds, type RunState } from "../rules/index.ts";
import type { JudgedResult, Range, Stage, SubmissionKind } from "../vocab/index.ts";
import {
callPrefix,
cellId,
ORIGINAL_BASELINE,
type DecisionKind,
type DecisionShapes,
type FrozenProtocol,
type LocationTable,
type Settlement,
} from "./shapes.ts";
// 账本读取的几样原语。哪个任务能看到什么,由各材料函数明写调用哪几样(任务书 09 第二部分:不写通用判定)。
// 账本读出时已按种类检查过形状,这里不再逐字段检查(任务书 09 第七部分)。
export type Sub<K extends SubmissionKind = SubmissionKind> = Extract<StoredSubmission, { kind: K }> & { round: number; seq: number };
export type Run = {
plan: Content<"runPlanned">;
started: boolean;
result: Content<"runResult"> | null;
};
type StageInfo = { stage: Stage; round: number; roster: string[]; closed: boolean };
export class Board {
readonly stages = new Map<string, StageInfo>();
readonly #records: LedgerRecord[];
readonly #subs: Sub[] = [];
readonly #runs = new Map<string, Run>();
constructor(ledger: Ledger) {
this.#records = ledger.records();
for (const r of this.#records) {
if (r.kind === "stageOpen") this.stages.set(r.content.stageId, { ...r.content, closed: false });
if (r.kind === "stageClose") this.stage(r.content.stageId).closed = true;
if (r.kind === "submission") this.#subs.push({ ...r.content, round: this.stage(r.content.stageId).round, seq: r.seq } as Sub);
if (r.kind === "runPlanned") this.#runs.set(r.content.run, { plan: r.content, started: false, result: null });
if (r.kind === "runStarted") this.#runs.get(r.content.run)!.started = true;
if (r.kind === "runResult") this.#runs.get(r.content.run)!.result = r.content;
}
}
stage(stageId: string): StageInfo {
return this.stages.get(stageId)!;
}
#of<K extends LedgerRecord["kind"]>(kind: K): Array<LedgerRecord<K>> {
return this.#records.filter((r) => r.kind === kind) as Array<LedgerRecord<K>>;
}
/** 每份提交(阶段、作者、种类、对象、编号)的最后一版,按序号排。 */
#latest(subs: readonly Sub[]): Sub[] {
const last = new Map<string, Sub>();
for (const s of subs) last.set(JSON.stringify([s.stageId, s.author, s.kind, s.target, s.item]), s);
return [...last.values()].sort((a, b) => a.seq - b.seq);
}
/**
* 已经公开的某种提交:阶段关闭即公开,以最后一版为准(方法.md 第三部分"同一阶段的提交,要等全部应交者交齐才公开")。
* 别人的提交只经这里读。
*/
published<K extends SubmissionKind>(kind: K): Array<Sub<K>> {
return this.#latest(this.#subs.filter((s) => s.kind === kind && this.stage(s.stageId).closed)) as Array<Sub<K>>;
}
/** 本人在当前阶段已经交的(不论阶段关没关)。 */
mine(actor: string, stageId: string): Sub[] {
return this.#latest(this.#subs.filter((s) => s.author === actor && s.stageId === stageId));
}
/** 每个候选的冻结协议(冻结后只追加更正,以最后一条为准),按冻结先后。 */
protocols(): Array<{ candidate: string; protocol: FrozenProtocol }> {
const last = new Map<string, FrozenProtocol>();
for (const r of this.#of("protocol")) last.set(r.content.candidate, r.content.protocol as FrozenProtocol);
return [...last].map(([candidate, protocol]) => ({ candidate, protocol }));
}
protocol(candidate: string): FrozenProtocol {
return this.protocols().find((x) => x.candidate === candidate)!.protocol;
}
/**
* 第 round 轮的留池候选(方法.md 4.3 ①):之前各轮冻结的、从没进过任何一批、基线还是本轮基线的候选。
* 只看冻结协议和选批决定,本轮判重合并之前(提案阶段)也能算。
*/
carried(round: number): string[] {
return this.#unchosen(round).filter((p) => p.protocol.baseline === this.baselineOf(round)).map((p) => p.candidate);
}
/** 因为基线变了而离开候选池的旧候选(方法.md 4.3 ①、6.10 第 5 条):条件同 carried,只是基线不同。 */
leftPool(round: number): string[] {
return this.#unchosen(round).filter((p) => p.protocol.baseline !== this.baselineOf(round)).map((p) => p.candidate);
}
#unchosen(round: number) {
const chosen = new Set(this.decisions("batch").flatMap((d) => d.output.chosen));
return this.protocols().filter((p) => p.protocol.round < round && !chosen.has(p.candidate));
}
/** 一个对象(基线、候选、消融格)最新的冻结代码。 */
code(object: string): { version: string; locations: LocationTable | null } {
const r = this.#of("code").filter((x) => x.content.object === object).at(-1)!;
return { version: r.content.version, locations: r.content.locations as LocationTable | null };
}
runs(): Run[] {
return [...this.#runs.values()];
}
/**
* 运行什么时候公开(任务书 09 设计稿):基线运行有结果即公开;第 r 轮正向运行在第 r 轮的采纳决定写入后
* 整批公开(方法.md 4.3 ⑪);第 r 轮消融运行在第 r 轮结算写入后公开(4.3 ⑰ ⑱)。
*/
runPublic(run: Run): boolean {
if (run.result === null) return false;
if (run.plan.purpose === "baseline") return true;
if (run.plan.purpose === "forward") return this.decision("adopt", run.plan.round) !== undefined;
return this.settlement(run.plan.round) !== undefined;
}
/** 一个对象启动过、有结果的那次运行(确认没启动的不算),没有为 undefined。基线运行在账本里没有对象,就是原始基线。 */
runOf(object: string): Run | undefined {
return this.runs()
.filter((r) => (r.plan.object ?? ORIGINAL_BASELINE) === object && r.result !== null && r.result.process.ending !== "notStarted")
.at(-1);
}
/** 同上,只算已经公开的。 */
publicRunOf(object: string): Run | undefined {
const r = this.runOf(object);
return r !== undefined && this.runPublic(r) ? r : undefined;
}
/** 每次运行在账上的状态,算余额用(方法.md 第七部分)。 */
runStates(): RunState[] {
return this.runs().map((r) => (r.result?.process.ending === "notStarted" ? "notStarted" : r.started ? "started" : "reserved"));
}
/** 某个候选最新的实现或修复提交(已关阶段)。 */
implementation(candidate: string): Sub<"implementation" | "repair"> | undefined {
return [...this.published("implementation"), ...this.published("repair")]
.filter((s) => s.target === candidate)
.sort((a, b) => a.seq - b.seq)
.at(-1);
}
decisions<K extends DecisionKind>(kind: K): Array<{ round: number } & DecisionShapes[K]> {
return this.#of("decision")
.filter((r) => r.content.decision === kind)
.map((r) => ({ round: r.content.round, input: r.content.input, output: r.content.output }) as never);
}
decision<K extends DecisionKind>(kind: K, round: number): ({ round: number } & DecisionShapes[K]) | undefined {
return this.decisions(kind).filter((d) => d.round === round).at(-1);
}
settlement(round: number): Settlement | undefined {
return this.settlements().find((s) => s.round === round);
}
settlements(): Array<{ round: number } & Settlement> {
return this.#of("settlement").map((r) => ({ round: r.content.round, ...(r.content.result as unknown as Settlement) }));
}
/** 本人最新的私人笔记(方法.md 第三部分"私人笔记"),没写过为 null。 */
note(author: string): string | null {
return this.#of("note").filter((r) => r.content.author === author).at(-1)?.content.body ?? null;
}
/**
* 已解封的作者自评(方法.md 4.3 ⑱):候选、作者、三个数。封存只经解封记录读出,没解封的一律读不到。
*/
unsealed(): Array<{ candidate: string; author: string; range: Range }> {
const bySeq = new Map(this.#records.map((r) => [r.seq, r] as const));
return this.#of("unseal").flatMap((u) =>
u.content.seals.map((seq) => {
const seal = bySeq.get(seq) as LedgerRecord<"seal">;
const proposal = bySeq.get(seal.content.proposal) as LedgerRecord<"submission">;
return { candidate: u.content.candidate, author: proposal.content.author, range: seal.content.range };
}),
);
}
/**
* 重新发起时,本人上一次做同一个任务被退回的原因(任务书 09 第五部分):同一任务(callId 前缀相同)
* 的各次调用里,开始与结束之间本人在这个阶段被退回的最后一条。没有为 null。别人的退回不读。
*/
lastRejection(stageId: string, actor: string, kind: SubmissionKind, object: string | null): string | null {
const prefix = callPrefix(stageId, actor, kind, object);
let inCall = false;
let reason: string | null = null;
for (const r of this.#records) {
if (r.kind === "callStarted" && r.content.call.startsWith(prefix)) inCall = true;
if (r.kind === "callEnded" && r.content.call.startsWith(prefix)) inCall = false;
if (inCall && r.kind === "rejection" && r.content.author === actor && r.content.stageId === stageId && r.content.kind === kind) {
reason = r.content.reason;
}
}
return reason;
}
// -------------------------------------------------------------------------
// 由编号规则连起来的几样
// -------------------------------------------------------------------------
/** 一个候选合并进来的全部原提案:创建时的几条,加上之后各轮并进来的。 */
proposalsOf(candidate: string): string[] {
return this.decisions("dedup").flatMap((d) =>
[...d.output.fresh, ...d.output.carried].filter((c) => c.candidate === candidate).flatMap((c) => c.proposals),
);
}
/** 已公开的一条原提案。 */
proposal(id: string): Sub<"proposal"> {
return this.published("proposal").find((s) => s.item === id)!;
}
/** 候选的作者(各原提案的作者,去重、按编号排)。 */
authorsOf(candidate: string): string[] {
return [...new Set(this.proposalsOf(candidate).map((id) => this.proposal(id).author))].sort(compareIds);
}
/**
* 第 round 轮开始时的基线对象(方法.md 第二部分"基线"):原始基线起,每轮结算有"消融中发现"就换成那一格,
* 否则有采纳就换成被采纳的候选(4.3 ⑫ ⑰)。
*/
baselineOf(round: number): string {
let current = ORIGINAL_BASELINE;
for (let r = 1; r < round; r++) {
const found = this.settlement(r)?.output.finding.chosen ?? null;
const adopted = this.decision("adopt", r)?.output.adopted ?? null;
if (found !== null) current = cellId(adopted!, found);
else if (adopted !== null) current = adopted;
}
return current;
}
/** 基线对象的分数:原始基线取基线运行,候选取它的正向运行,消融格取它那次运行。读不出有效分数报错。 */
baselineScore(object: string): number {
const run = this.runOf(object);
const score = run === undefined ? null : validScore(run.result!.judged);
if (score === null) throw new Error(`基线 ${object} 没有有效分数,不能当基线`);
return score;
}
}
/** 判分结果里的有效分数:算数且测到了分数才有,否则 null。 */
export function validScore(judged: JudgedResult | null): number | null {
return judged !== null && judged.verdict === "valid" && judged.score.status === "measured" ? judged.score.value : null;
}
+270
View File
@@ -0,0 +1,270 @@
import { compareIds } from "../rules/index.ts";
import type { ChecklistOf } from "../vocab/index.ts";
import {
beliefSection,
codeSection,
Draft,
entryVersions,
factorLines,
metricsSection,
ownSection,
publicRecords,
registeredMetrics,
researcherTaskSection,
runLines,
validEntries,
} from "./blocks.ts";
import { json, num, rangeText, score } from "./format.ts";
import { factorId, factorKey } from "./shapes.ts";
// 研究者的 11 种任务,每种一个函数,明写从账本取哪几样(方法.md 第三部分、4.2、4.3)。
// 顺序:你自己的 → 公共材料(任务与本轮、基线、研究记录、信念本、各任务另加的几节、本任务要处理的东西、已登记指标清单)。
// 别人的提交一律经 Board.published(已关阶段的最后一版)读;作者自评、别人的第一次预测、别人的退回都不读。
type Own = ReadonlyArray<{ heading: string; lines: string[] }>;
/**
* 研究者材料的骨架。middle 放在信念本之后、指标清单之前。
* 第零轮(4.2)只有任务与本轮、基线:不跑实验,信念本不变,也还没有研究记录和指标可列。
*/
function frame(d: Draft, own: Own, middle: () => void): void {
ownSection(d, own);
d.group("公共材料", "下面是大家看到的同一份材料,不隐藏任何正式证据;第一部分已经给过的内容,这里只写一句指引。");
researcherTaskSection(d);
codeSection(d);
if (d.round >= 1) {
publicRecords(d);
beliefSection(d);
}
middle();
if (d.round >= 1) metricsSection(d);
}
/** 提案、回应、统一共用的因子规则(方法.md 第五部分"延续""指标"、4.3 ②)。 */
function factorRules(d: Draft) {
const registered = registeredMetrics(d.board).map((m) => m.id);
return {
factorLimit: d.req.study.factorLimit,
continuable: d.board.protocols().flatMap((p) => p.protocol.factors.map((_, i) => factorId(p.candidate, factorKey(i)))),
reusableMetrics: registered,
takenMetricIds: [...d.req.study.metrics.map((m) => m.id), ...registered],
};
}
const PRESUMPTION =
'挑刺里标"(推测)"的判断,是挑刺人没对照代码或已测运行核实过的猜想;作者在回应里同意它,也不会让它变成已核实。当作别人的猜想看,不当证据。';
// ---------------------------------------------------------------------------
// 第零轮(方法.md 4.2)
// ---------------------------------------------------------------------------
/** 0.1 闭门写入口:任务说明、基线代码、基线分数;看不到别人的入口(任何入口都不读)。 */
export function entry(d: Draft): ChecklistOf<"entry"> {
frame(d, [], () => undefined);
return { kind: "entry", entries: { min: 2, max: 3 } };
}
/** 入口和之前各次讨论:只读已关阶段的(写入口阶段关了才有讨论)。 */
function entriesAndDiscussions(d: Draft): void {
const lines: string[] = [];
for (const e of validEntries(d.board)) {
d.record(e.id, `入口 ${e.id}:${e.name}(作者 ${e.author})`, e.body);
lines.push(`### 入口 ${e.id}:${e.name}(作者 ${e.author})`, "", e.body, "");
}
for (const s of d.board.published("discussion")) lines.push(`### ${s.author} 的讨论(阶段 ${s.stageId})`, "", s.content.body, "");
d.section("本任务要处理的东西", lines);
const table: string[] = ['程序给的号;整份替换后的新清单接着往后编,旧编号作废,不会指到新入口。'];
for (const [author, versions] of entryVersions(d.board)) {
versions.forEach((v, i) => {
const last = i === versions.length - 1;
table.push(`- ${author},${i === 0 ? "写入口时交的" : `第 ${i} 次替换`}${last ? "(最后一版,有效)" : "(已被整份替换,编号作废)"}:`);
v.entries.forEach((e, j) => table.push(` - ${v.ids[j]}${last ? "" : "(作废)"}:${e.name}`));
});
}
d.section("入口编号对照", table);
}
/** 0.2 讨论:全部入口(标明作者)、之前各次的讨论。 */
export function discussion(d: Draft): ChecklistOf<"discussion"> {
frame(d, [], () => entriesAndDiscussions(d));
return { kind: "discussion", entries: { min: 2, max: 3 } };
}
/** 0.3 认领:最终的入口与全部讨论;至少排 min(N, 入口总数) 个。 */
export function claim(d: Draft): ChecklistOf<"claim"> {
frame(d, [], () => entriesAndDiscussions(d));
const entries = validEntries(d.board).map((e) => e.id);
const n = d.board.stage(d.req.stageId).roster.length;
return { kind: "claim", entries, minChoices: Math.min(n, entries.length) };
}
// ---------------------------------------------------------------------------
// 第 r 轮(方法.md 4.3)
// ---------------------------------------------------------------------------
/** 某一轮实验员的提醒:每个候选最新一次实现或修复的 concerns,加消融实现的(只读已关阶段)。 */
function concernLines(d: Draft, round: number): string[] {
const b = d.board;
const candidates = [...new Set([...b.published("implementation"), ...b.published("repair")].filter((s) => s.round === round).map((s) => s.target!))];
const lines = [
...candidates.map((c) => `- 候选 ${c}:${b.implementation(c)!.content.concerns}`),
...b.published("ablationImplementation").filter((s) => s.round === round).map((s) => `- 消融格 ${s.target}:${s.content.concerns}`),
];
return lines.length === 0 ? ["(没有)"] : lines;
}
/** ② 提案:公共材料,加上一轮全部收尾、上一轮实验员的提醒、可以写进"延续"的因子。 */
export function proposal(d: Draft): ChecklistOf<"proposal"> {
frame(d, [], () => {
const prev = d.round - 1;
const wraps = d.board.published("wrapUp").filter((w) => w.round === prev);
d.section(
"上一轮的收尾",
prev < 1 ? ["(这是第 1 轮,没有上一轮)"] : wraps.flatMap((w) => [`### ${w.author} 的收尾`, "", w.content.body, ""]),
);
d.section("上一轮实验员实现时的提醒", prev < 1 ? ["(这是第 1 轮,没有上一轮)"] : concernLines(d, prev));
const lines = d.board.protocols().flatMap((p) =>
p.protocol.factors.map((f, i) => `- ${factorId(p.candidate, factorKey(i))} ${f.name}(关键设定:${f.keySettings};候选 ${p.candidate},基线 ${p.protocol.baseline}${f.continues === null ? "" : `;它自己延续 ${f.continues}`})`),
);
d.section('可以写进"延续"的因子', lines.length === 0 ? ["(还没有任何冻结的因子)"] : lines);
});
return { kind: "proposal", maxProposals: d.req.study.proposalLimit, ...factorRules(d) };
}
/** 一条原提案全文,带作者。 */
function proposalLines(d: Draft, id: string): string[] {
const p = d.board.proposal(id);
return [`### 原提案 ${id}(作者 ${p.author})`, "", json(p.content)];
}
/** 一个候选由哪几条原提案合并而成(判重合并的决定,方法.md 4.3 ③)。 */
function mergeLine(d: Draft, candidate: string): string {
const ids = d.board.proposalsOf(candidate);
return ids.length === 1 ? `候选 ${candidate} 就是原提案 ${ids[0]}。` : `候选 ${candidate} 由判重合并的 ${ids.join("、")} 合成,作者都保留作者关系。`;
}
/** 合并说明,后面跟这几条原提案的全文。 */
function candidateProposals(d: Draft, candidate: string): string[] {
return [mergeLine(d, candidate), "", ...d.board.proposalsOf(candidate).flatMap((id) => proposalLines(d, id))];
}
/** ④ 第一次预测:这个候选的全部原始提案。看不到作者自评(账本里已拆出封存)、看不到别人的预测。 */
export function firstPrediction(d: Draft): ChecklistOf<"firstPrediction"> {
const candidate = d.req.object!;
frame(d, [], () => d.section("本任务要处理的东西", candidateProposals(d, candidate)));
return { kind: "firstPrediction", candidate };
}
/** ⑤ 挑刺:被挑的那条原提案全文(用户 2026-10-03 决定 4:按原提案挑)。 */
export function critique(d: Draft): ChecklistOf<"critique"> {
const id = d.req.object!;
frame(d, [], () => d.section("本任务要处理的东西", proposalLines(d, id)));
return { kind: "critique", proposal: id, factors: d.board.proposal(id).content.factors.map((_, i) => factorKey(i)) };
}
/**
* ⑥ 回应与统一:这个候选的全部原提案、针对它们的全部挑刺(已关的挑刺阶段)。
* 两者材料相同;统一另要逐条体现每份原提案(sources)。
*/
function answer(d: Draft): { candidate: string; critiques: string[] } & ReturnType<typeof factorRules> {
const candidate = d.req.object!;
const ids = d.board.proposalsOf(candidate);
const list = d.board.published("critique").filter((c) => ids.includes(c.target!)).sort((a, b) => compareIds(a.item!, b.item!));
frame(d, [], () => {
d.section("本任务要处理的东西", candidateProposals(d, candidate));
d.section(
"本任务要答复的挑刺",
list.length === 0
? ["(没有针对这个候选的挑刺,replies 交空数组)"]
: [PRESUMPTION, "", ...list.flatMap((c) => [`### 挑刺 ${c.item}(${c.author} 挑提案 ${c.target} 的 ${c.content.target})`, "", c.content.body, ""])],
);
});
return { candidate, critiques: list.map((c) => c.item!), ...factorRules(d) };
}
export const response = (d: Draft): ChecklistOf<"response"> => ({ kind: "response", ...answer(d) });
export function unification(d: Draft): ChecklistOf<"unification"> {
const a = answer(d);
return { kind: "unification", ...a, sources: d.board.proposalsOf(a.candidate) };
}
/**
* ⑦ 第二次预测:冻结协议、挑刺与回应按挑刺编号配对、本人第一次给这个候选的三个数(4.3 ⑦)。
* 第一次预测只取本人本轮的;别人的第一次和作者自评不读。留池候选本轮不做第一次预测(4.3 ④)。
*/
export function secondPrediction(d: Draft): ChecklistOf<"secondPrediction"> {
const candidate = d.req.object!;
const first = () =>
d.board.published("firstPrediction").filter((s) => s.author === d.req.actor && s.target === candidate && s.round === d.round).at(-1)!;
const own = [
{
heading: "你第一次给这个候选的三个数",
lines: d.board.carried(d.round).includes(candidate)
? ["(这是留池候选,本轮没有第一次预测)"]
: [`一成 / 一半 / 九成:${rangeText(first().content.range)}(当时只读了原始提案)`, "", "读了冻结协议和挑刺与回应再给一次:该改就改,不该改就照旧,两份都会存档。"],
},
];
frame(d, own, () => {
const p = d.board.protocol(candidate);
const critiques = d.board.published("critique");
const pairs = p.replies.flatMap((r) => {
const c = critiques.find((x) => x.item === r.critique)!;
return [`### 挑刺 ${r.critique}(${c.author} 挑的)`, "", c.content.body, "", `作者的答复:${r.reply}`, ""];
});
d.section("本任务要处理的东西", [mergeLine(d, candidate), "", `### 候选 ${candidate} 的冻结协议`, "", json(p)]);
d.section("挑刺与回应", pairs.length === 0 ? ["(没有针对这个候选的挑刺)"] : [PRESUMPTION, "", ...pairs]);
});
return { kind: "secondPrediction", candidate };
}
/**
* ⑭ 解释:本轮各候选的运行与分数、实验员的提醒、核验结论、被采纳改法的因子(要给三个数的格)、
* 本轮运行的产物(只放进查询)。本轮正向运行在采纳决定写入后公开。
*/
export function explanation(d: Draft): ChecklistOf<"explanation"> {
const adopted = d.board.decision("adopt", d.round)!.output.adopted!;
const p = d.board.protocol(adopted);
frame(d, [], () => {
const runs = d.board.runs().filter((r) => r.plan.purpose === "forward" && r.plan.round === d.round && d.board.runPublic(r));
const verifications = d.board.published("verification").filter((v) => v.round === d.round);
d.section("本任务要处理的东西", [
"### 本轮正向运行",
"",
...runs.flatMap((r) => [...runLines(d, r), `(判分全文用 research_query 查 ${r.plan.run})`, ""]),
"### 核验结论",
"",
...(verifications.length === 0 ? ["(没有)"] : verifications.map((v) => `- 候选 ${v.target}:${v.content.body}`)),
]);
d.section("本轮实验员实现时的提醒", concernLines(d, d.round));
d.section("本轮要给三个数的格", [
`被采纳的改法:候选 ${adopted},完整代码 ${score(d.board.baselineScore(adopted))};本批基线 ${p.baseline} ${score(d.board.baselineScore(p.baseline))}。`,
"每条解释对下面每一格各给三个数:只关这一个因子、其余因子和保障照完整代码保留时,那次运行的最终指标(按分数原值)。",
"",
...factorLines(p),
]);
const artifacts = d.req.artifacts!;
const listed = Object.keys(artifacts)
.sort(compareIds)
.flatMap((run) => Object.entries(artifacts[run]!).map(([file, text]) => `- ${d.record(`${run}/${file}`, `运行 ${run} 的产物 ${file}`, text)}`));
d.section("本轮运行的产物", ["用 research_query 按编号查全文:", ...(listed.length === 0 ? ["(没有)"] : listed)]);
});
return { kind: "explanation", cells: p.factors.map((_, i) => factorKey(i)), maxExplanations: d.req.study.proposalLimit };
}
/** ⑱ 收尾:本轮的采纳、选格(结算与押的因子在信念本里),实验员的提醒。 */
export function wrapUp(d: Draft): ChecklistOf<"wrapUp"> {
frame(d, [], () => {
const adopt = d.board.decision("adopt", d.round)!.output;
const cells = d.board.decision("cells", d.round);
d.section("本任务要处理的东西", [
adopt.adopted === null
? `本轮没有采纳:最好的有效候选 ${adopt.best === null ? "没有" : `${adopt.best.id},改进 ${score(adopt.best.improvement)}`},采纳线 ${score(adopt.line)}。`
: `本轮采纳候选 ${adopt.adopted},改进 ${score(adopt.best!.improvement)},采纳线 ${score(adopt.line)}。`,
cells === undefined ? "本轮没有选格。" : `本轮选格:测 ${cells.output.chosen.length === 0 ? "(一格都不测)" : cells.output.chosen.join("、")},净值 ${num(cells.output.chosenNet)}。`,
'消融结算、各格结论和押的因子见"信念本"。',
]);
d.section("本轮实验员实现时的提醒", concernLines(d, d.round));
});
return { kind: "wrapUp" };
}
+116
View File
@@ -0,0 +1,116 @@
import type {
AblationModelInput,
BatchDecision,
BatchInput,
BetResult,
CellDecision,
CellRuleInput,
EntryAssignment,
SettleInput,
adopt,
settleAblation,
} from "../rules/index.ts";
import type { Submission } from "../vocab/index.ts";
// 账本里只存成 Json 的几样内容长什么样、编号怎么起(用户 2026-10-03 决定 2):materials 读它们,flow 照着写。
// 决定和结算的输出直接用 rules 的返回类型;ledger 不认识这些类型,只存 Json。
// ---------------------------------------------------------------------------
// 编号规则(一处定义,flow 起编号、materials 认编号都用这里)
// ---------------------------------------------------------------------------
/** 原始基线(任务给的代码)的对象编号:它的冻结代码记录、基线运行都挂在这个名字下。 */
export const ORIGINAL_BASELINE = "base";
/** 入口:作者在第零轮交的第 n 个入口,讨论时整份替换的新清单接着往后编(方法.md 4.2)。 */
export const entryId = (author: string, n: number): string => `e-${author}-${n}`;
/** 提案:第 round 轮作者交的第 n 条,也是提交记录的 item。 */
export const proposalId = (round: number, author: string, n: number): string => `r${round}-${author}-${n}`;
/** 挑刺:挑刺人对这条原提案的第 n 条,也是提交记录的 item。 */
export const critiqueId = (proposal: string, author: string, n: number): string => `${proposal}.${author}.${n}`;
/** 候选:第 round 轮判重合并后的第 n 个。 */
export const candidateId = (round: number, n: number): string => `r${round}-c${n}`;
/** 因子、实现条款在冻结协议清单里的编号就是位置:第 i 个(从 0 数)是 p(i+1)、c(i+1)。 */
export const factorKey = (i: number): string => `p${i + 1}`;
export const clauseKey = (i: number): string => `c${i + 1}`;
/** 全局因子编号:候选编号加它在冻结协议里的位置(p1…)。"延续"写的就是它。 */
export const factorId = (candidate: string, key: string): string => `${candidate}-${key}`;
/** 消融格的编号就是被关因子的全局编号:每轮只有被采纳的那个候选做消融,一个因子一格。 */
export const cellId = factorId;
/** 消融格编号拆回候选和因子编号。 */
export function splitCell(cell: string): { candidate: string; key: string } {
const m = /^(.+)-(p[1-9][0-9]*)$/.exec(cell);
if (m === null) throw new Error(`消融格编号 ${cell} 不是"候选编号-p几"的写法`);
return { candidate: m[1]!, key: m[2]! };
}
/** 解释:第 round 轮作者交的第 i 条(从 1 数)。结算里判确认、否定用它。 */
export const explanationId = (round: number, author: string, i: number): string => `r${round}-${author}-x${i}`;
/**
* 模型调用:同一个任务(阶段、谁、哪种提交、对象)第 attempt 次发起。
* 重新发起时 materials 按这个前缀找回上一次在哪段记录之间被退回(结构.md 第四部分第 10 种记录)。
*/
export const callId = (stageId: string, actor: string, kind: string, object: string | null, attempt: number): string =>
`${callPrefix(stageId, actor, kind, object)}${attempt}`;
export const callPrefix = (stageId: string, actor: string, kind: string, object: string | null): string =>
`${stageId}.${actor}.${kind}.${object ?? "none"}.`;
// ---------------------------------------------------------------------------
// 冻结协议(账本记录 protocol.protocol,方法.md 4.3 ⑥)
// ---------------------------------------------------------------------------
/**
* 一个候选的冻结协议:原提案合上回应改过的部分(或统一后的整份),加上答复。
* baseline 是它出发的基线对象编号;round 是冻结的轮次;proposals 是合并进来的全部原提案。
* 留池候选沿用这一份,不重写(方法.md 4.3 ①)。
*/
export type FrozenProtocol = Pick<Submission<"unification">, "factors" | "safeguards" | "metrics" | "body" | "clauses" | "replies"> & {
round: number;
baseline: string;
proposals: string[];
/** 回应或统一的答复正文。 */
replyBody: string;
};
// ---------------------------------------------------------------------------
// 冻结代码的位置表(账本记录 code.locations)
// ---------------------------------------------------------------------------
/** 候选代码的位置表,就是实验员交的那三项;基线和消融格的代码记 null(消融格用完整代码的位置表)。 */
export type LocationTable = Pick<Submission<"implementation">, "factorLocations" | "safeguardLocations" | "unassigned">;
// ---------------------------------------------------------------------------
// 决定(账本记录 decision:decision 是种类,input、output 如下)
// ---------------------------------------------------------------------------
/** 判重合并的输出(方法.md 4.3 ③):本轮新候选各合并了哪几条提案;留池候选各并进了哪几条新提案(可以没有)。 */
export type DedupOutput = {
fresh: Array<{ candidate: string; proposals: string[] }>;
carried: Array<{ candidate: string; proposals: string[] }>;
};
/**
* 决定的种类和各自的输入、输出。选批、选格的输入输出是 rules 的类型;
* 采纳的输入就是 adopt() 的参数;押的因子的输入是 chooseBet() 的参数。
* 判重合并的输入是 Jev 每一对的判断,原样存(materials 不读)。
*/
export type DecisionShapes = {
entries: { output: EntryAssignment[] };
dedup: { output: DedupOutput };
batch: { input: BatchInput; output: BatchDecision };
adopt: { input: Parameters<typeof adopt>[0]; output: ReturnType<typeof adopt> };
cells: { input: CellRuleInput; output: CellDecision };
bet: { input: AblationModelInput & { cells: SettleInput["cells"] }; output: BetResult };
};
export type DecisionKind = keyof DecisionShapes;
export const DECISION_KINDS: readonly DecisionKind[] = ["entries", "dedup", "batch", "adopt", "cells", "bet"];
// ---------------------------------------------------------------------------
// 结算(账本记录 settlement.result,方法.md 4.3 ⑰)
// ---------------------------------------------------------------------------
/** 一次消融结算:被采纳的候选、settleAblation 的输入和输出。 */
export type Settlement = {
candidate: string;
input: SettleInput;
output: ReturnType<typeof settleAblation>;
};
+50
View File
@@ -0,0 +1,50 @@
import type { SubmissionKind } from "./roster.ts";
/**
* 对号清单:materials 为一次任务算出、submissions 拿来判"收不收"的那几样(任务书 09 第三部分"对号清单")。
* 两个模块互不引用,所以放在这里(用户 2026-10-03 决定 1)。只放提交之前就知道的东西;
* 实验员改了哪些文件这类提交之后才知道的,随提交一起交给 submissions。编号都是材料里写给模型的那一种。
*/
/** 提案、回应、统一共用:因子数上限、"延续"能写的全局因子编号、能引用的假设专用指标、已被占用的指标编号。 */
type FactorRules = {
factorLimit: number;
continuable: string[];
reusableMetrics: string[];
/** 最终指标、常规辅助指标和冻结协议里登记过的假设专用指标,新登记的不能和它们重名。 */
takenMetricIds: string[];
};
/** 实现、修复、核验共用:冻结协议的实现条款(c1…)、因子(p1…)、保障短名。 */
type FrozenItems = { candidate: string; clauses: string[]; factors: string[]; safeguards: string[] };
/** 入口个数的范围(方法.md 4.2:2–3 个)。 */
type EntryCount = { min: 2; max: 3 };
export type Checklist =
| { kind: "entry"; entries: EntryCount }
| { kind: "discussion"; entries: EntryCount }
/** minChoices = min(N, 入口总数)(方法.md 4.2 认领)。 */
| { kind: "claim"; entries: string[]; minChoices: number }
| ({ kind: "proposal"; maxProposals: number } & FactorRules)
| { kind: "firstPrediction"; candidate: string }
| { kind: "secondPrediction"; candidate: string }
/** 挑刺按原提案挑:那条原提案的编号和它的因子编号(用户 2026-10-03 决定 4)。 */
| { kind: "critique"; proposal: string; factors: string[] }
/** critiques 是针对本候选各份原提案的全部挑刺编号。 */
| ({ kind: "response"; candidate: string; critiques: string[] } & FactorRules)
/** sources 是被合并的原提案编号。 */
| ({ kind: "unification"; candidate: string; critiques: string[]; sources: string[] } & FactorRules)
| ({ kind: "implementation" } & FrozenItems)
| ({ kind: "repair" } & FrozenItems)
/** unassigned 是没有归属的改动,写法与材料里列的相同。 */
| ({ kind: "verification"; unassigned: string[] } & FrozenItems)
/** cells 是要给三个数的格(被采纳改法的因子编号 p1…),maxExplanations 是 M。 */
| { kind: "explanation"; cells: string[]; maxExplanations: number }
/** off 是这一格要关的因子编号,factors 是被采纳改法的全部因子编号。 */
| { kind: "ablationImplementation"; cell: string; off: string; factors: string[] }
| { kind: "ablationVerification"; cell: string; factors: string[] }
| { kind: "wrapUp" };
/** 某种提交的对号清单,例如 ChecklistOf<"proposal">。 */
export type ChecklistOf<K extends SubmissionKind> = Extract<Checklist, { kind: K }>;
+1
View File
@@ -29,3 +29,4 @@ export {
} from "./runs.ts";
export { JevJudgment } from "./jev.ts";
export { Range, SUBMISSION_SCHEMAS, type Submission } from "./submissions.ts";
export { type Checklist, type ChecklistOf } from "./checklist.ts";