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:
@@ -0,0 +1,185 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { test } from "node:test";
|
||||
import { Ledger } from "../src/ledger/index.ts";
|
||||
import { beliefBook, queryMaterial } from "../src/materials/index.ts";
|
||||
import { adopt, selectBestExpected, settleAblation, type BatchInput, type SettleInput } from "../src/rules/index.ts";
|
||||
import { tempDir } from "./ledger-helpers.ts";
|
||||
import { batch, judged, material, NONE, P_A1, P_A2, protocolOf, r, study, studyUpTo, TABLE_C1, Writer } from "./materials-helpers.ts";
|
||||
|
||||
// 信念本的留池、离池,按条文手算的数字,第零轮记录在第 1 轮起公开。
|
||||
|
||||
function fresh(t: import("node:test").TestContext): Writer {
|
||||
const l = Ledger.open(join(tempDir(t), "ledger.db"));
|
||||
t.after(() => l.close());
|
||||
const w = new Writer(l);
|
||||
w.l.append("code", { object: "base", version: "v-base", locations: null });
|
||||
w.run("run-0", "baseline", 0, null, judged(0.96, "main"));
|
||||
return w;
|
||||
}
|
||||
|
||||
const adoptInput = (baseline: number, candidates: Array<{ id: string; score: number }>) => ({
|
||||
baseline,
|
||||
knownBest: 2.635983,
|
||||
epsilon: 0.01,
|
||||
direction: "maximize" as const,
|
||||
candidates,
|
||||
});
|
||||
|
||||
/** 第 1 轮冻结 r1-c1、r1-c2,选中 r1-c1、跑了没采纳:r1-c2 留到第 2 轮的候选池。 */
|
||||
function roundOneNoAdoption(t: import("node:test").TestContext, before: (w: Writer) => void): Writer {
|
||||
const w = fresh(t);
|
||||
before(w);
|
||||
w.protocol("r1-c1", protocolOf(P_A1, 1, "base", ["r1-a1-1"], [], ["c"]));
|
||||
w.protocol("r1-c2", protocolOf(P_A2, 1, "base", ["r1-a2-1"], [], ["c"]));
|
||||
const b1 = batch(1, 0.96, { "r1-c1": { a1: r(1.0, 1.1, 1.2) }, "r1-c2": { a1: r(0.95, 0.97, 0.99) } });
|
||||
assert.deepEqual(b1.output.chosen, ["r1-c1"]);
|
||||
w.decision("batch", 1, b1.input, b1.output);
|
||||
w.l.append("code", { object: "r1-c1", version: "v-c1", locations: TABLE_C1 });
|
||||
w.run("run-1", "forward", 1, "r1-c1", judged(0.965, "main"));
|
||||
const a1 = adoptInput(0.96, [{ id: "r1-c1", score: 0.965 }]);
|
||||
w.decision("adopt", 1, a1, adopt(a1));
|
||||
return w;
|
||||
}
|
||||
|
||||
test("留池:第 1 轮没选中、第 2 轮入选跑了没采纳的候选,第 3 轮不再算留池(复现审阅意见 1)", (t) => {
|
||||
const w = roundOneNoAdoption(t, () => undefined);
|
||||
w.open("s2-prop", "proposal", 2, ["a1"]);
|
||||
// 第 2 轮提案阶段(还没有本轮判重合并):r1-c2 没选中、基线没变,留在候选池。
|
||||
assert.ok(beliefBook(w.l, study, 2).includes("r1-c2:没选中,留在本轮候选池;之前的合并范围不附"));
|
||||
const b2 = batch(2, 0.96, { "r1-c2": { a1: r(0.95, 0.97, 0.99) } });
|
||||
w.decision("batch", 2, b2.input, b2.output);
|
||||
w.l.append("code", { object: "r1-c2", version: "v-c2", locations: { factorLocations: [{ factor: "p1", locations: ["x.py"] }], safeguardLocations: [], unassigned: [] } });
|
||||
w.run("run-2", "forward", 2, "r1-c2", judged(0.962, "main"));
|
||||
const a2 = adoptInput(0.96, [{ id: "r1-c2", score: 0.962 }]);
|
||||
w.decision("adopt", 2, a2, adopt(a2));
|
||||
w.open("s3-prop", "proposal", 3, ["a1"]);
|
||||
const book = beliefBook(w.l, study, 3);
|
||||
assert.ok(!book.includes("留在本轮候选池"), "r1-c2 第 2 轮已经入选,不再留池");
|
||||
assert.match(book, /- 第 1 轮选批[^\n]*\n(?:[^\n]*\n)*? - r1-c2:合并范围 /);
|
||||
assert.ok(book.includes("离开候选池的旧候选**:没有"));
|
||||
});
|
||||
|
||||
test("导出时(round 为 null)不藏任何候选的合并范围(复现复审意见 1)", (t) => {
|
||||
const w = roundOneNoAdoption(t, () => undefined);
|
||||
w.open("s2-prop", "proposal", 2, ["a1"]);
|
||||
const book = beliefBook(w.l, study, null);
|
||||
assert.ok(!book.includes("留在本轮候选池"));
|
||||
assert.match(book, / - r1-c2:合并范围 /);
|
||||
});
|
||||
|
||||
test("第二次预测:留池候选按 carried 判,不取它上一轮的第一次预测(复现复审意见 2)", (t) => {
|
||||
const w = roundOneNoAdoption(t, (w) => {
|
||||
w.open("s1-prop", "proposal", 1, ["a1"]);
|
||||
w.sub("s1-prop", "a1", "proposal", null, "r1-a1-1", P_A1);
|
||||
w.sub("s1-prop", "a1", "proposal", null, "r1-a1-2", P_A2);
|
||||
w.close("s1-prop");
|
||||
w.decision("dedup", 1, null, { fresh: [{ candidate: "r1-c1", proposals: ["r1-a1-1"] }, { candidate: "r1-c2", proposals: ["r1-a1-2"] }], carried: [] });
|
||||
w.open("s1-fp", "firstPrediction", 1, ["a1"]);
|
||||
w.sub("s1-fp", "a1", "firstPrediction", "r1-c2", null, { range: r(4.441, 4.442, 4.443), body: "上一轮的第一次" });
|
||||
w.close("s1-fp");
|
||||
});
|
||||
w.open("s2-prop", "proposal", 2, ["a1"]);
|
||||
w.close("s2-prop");
|
||||
w.decision("dedup", 2, null, { fresh: [], carried: [{ candidate: "r1-c2", proposals: [] }] });
|
||||
w.open("s2-sp", "secondPrediction", 2, ["a1"]);
|
||||
const m = material(w.l, "secondPrediction", "a1", "s2-sp", "r1-c2", NONE);
|
||||
assert.ok(m.firstMessage.includes("(这是留池候选,本轮没有第一次预测)"));
|
||||
assert.ok(!m.firstMessage.includes("4.442"));
|
||||
});
|
||||
|
||||
test("离池:基线变了、从没入选的旧候选列在离开候选池里,不再写留池", (t) => {
|
||||
const book = beliefBook(studyUpTo(t, "proposal2Open"), study, 2);
|
||||
assert.ok(book.includes("离开候选池的旧候选**:r1-c2"));
|
||||
assert.ok(!book.includes("留在本轮候选池"));
|
||||
});
|
||||
|
||||
test("信念本的数按条文手算:少多少分、撑几成、世界比特、惊讶、战绩次数、标签", (t) => {
|
||||
// 正向的三个候选、各人范围、本批基线 b 和 c3 的实测取自 tests/fixtures/rules/info.json 的 bits_forward_r1_real,
|
||||
// 世界比特、惊讶的预期值是那里不看实现算出的独立值。
|
||||
const fx = JSON.parse(readFileSync(join(import.meta.dirname, "fixtures", "rules", "info.json"), "utf8")).scenarios[0];
|
||||
const b: number = fx.input.b;
|
||||
const full: number = fx.input.actual["r1-v1-c3"];
|
||||
const w = fresh(t);
|
||||
const input: BatchInput = {
|
||||
candidates: Object.entries(fx.input.candidates as Record<string, Array<{ researcher: string; range: { q1: number; q5: number; q9: number } }>>).map(
|
||||
([id, ranges]) => ({ id, ranges }),
|
||||
),
|
||||
baseline: b,
|
||||
direction: "maximize",
|
||||
epsilon: 0.01,
|
||||
lambda: 0.1,
|
||||
batchCap: 1,
|
||||
balance: 9,
|
||||
seed: "seed",
|
||||
};
|
||||
// 这里只要 c3 入选并跑过:选批记录的其余内容照规则算出,chosen 写成 c3。
|
||||
w.decision("batch", 1, input, { ...selectBestExpected(input), chosen: ["r1-v1-c3"] });
|
||||
w.protocol("r1-v1-c3", protocolOf(P_A1, 1, "base", ["r1-a1-1"], [], ["c"]));
|
||||
w.l.append("code", { object: "r1-v1-c3", version: "v-c3", locations: TABLE_C1 });
|
||||
w.run("run-1", "forward", 1, "r1-v1-c3", judged(full, "main"));
|
||||
const a = adoptInput(b, [{ id: "r1-v1-c3", score: full }]);
|
||||
w.decision("adopt", 1, a, adopt(a));
|
||||
w.run("run-2", "ablation", 1, "r1-v1-c3-p1", judged(1.3, "main"));
|
||||
w.run("run-3", "ablation", 1, "r1-v1-c3-p2", judged(1.45, "main"));
|
||||
const settle: SettleInput = {
|
||||
baseline: b,
|
||||
full,
|
||||
direction: "maximize",
|
||||
epsilon: 0.01,
|
||||
knownBest: 2.635983,
|
||||
lambda: 0.1,
|
||||
factors: ["p1", "p2"],
|
||||
cells: [
|
||||
{ factor: "p1", outcome: { kind: "valid", score: 1.3 } },
|
||||
{ factor: "p2", outcome: { kind: "valid", score: 1.45 } },
|
||||
],
|
||||
explanations: [
|
||||
{ id: "r1-researcher-1-x1", author: "researcher-1", cells: [{ factor: "p1", range: r(1.25, 1.35, 1.45) }, { factor: "p2", range: r(1.5, 1.55, 1.6) }] },
|
||||
{ id: "r1-researcher-2-x1", author: "researcher-2", cells: [{ factor: "p1", range: r(1.1, 1.2, 1.28) }, { factor: "p2", range: r(1.4, 1.44, 1.5) }] },
|
||||
],
|
||||
};
|
||||
w.l.append("settlement", { round: 1, result: { candidate: "r1-v1-c3", input: settle, output: settleAblation(settle) } as never });
|
||||
w.open("s2-prop", "proposal", 2, ["researcher-1"]);
|
||||
const book = beliefBook(w.l, study, 2);
|
||||
// 世界比特 13.018692667257758、惊讶 18.609640474436972(fixture),写 4 位有效数字。
|
||||
assert.ok(book.includes("世界比特 13.02,惊讶 18.61 比特"), "正向 c3 的世界比特与惊讶");
|
||||
// G = 1.552176048093269 − 0.9597642169962064 = 0.5924118311;关 p1:少 0.252176,撑 0.4257 → 43%;关 p2:少 0.102176,撑 0.1725 → 17%。
|
||||
assert.ok(book.includes("关掉后 1.3,少 0.252176 分,撑 43%"));
|
||||
assert.ok(book.includes("关掉后 1.45,少 0.102176 分,撑 17%"));
|
||||
// Σ撑 = 0.598,相对交互量 −0.402 < −0.2:替代(方法.md 6.6)。
|
||||
assert.ok(book.includes("替代:几个因子在做同一件事"));
|
||||
// 正向:三人对 c3 的一成都在 2.1 以上,实测 1.552 低于一成:中位数偏高、穿过下沿,没包住。
|
||||
for (const who of ["researcher-1", "researcher-2", "researcher-3"]) {
|
||||
assert.ok(book.includes(`- ${who},正向 1 次:中位数偏高 1、偏低 0;穿过上沿 0、下沿 1;包住 0`), who);
|
||||
}
|
||||
// 消融:researcher-1 p1 1.3 落在 1.25–1.35(包住,中位数偏高)、p2 1.45 < 1.5(穿下沿,偏高);
|
||||
// researcher-2 p1 1.3 > 1.28(穿上沿,偏低)、p2 1.45 在 1.44–1.5(包住,偏低)。
|
||||
assert.ok(book.includes("- researcher-1,消融 2 次:中位数偏高 2、偏低 0;穿过上沿 0、下沿 1;包住 1"));
|
||||
assert.ok(book.includes("- researcher-2,消融 2 次:中位数偏高 0、偏低 2;穿过上沿 1、下沿 0;包住 1"));
|
||||
});
|
||||
|
||||
test("第零轮的入口、讨论、志愿、分配在第 1 轮起的研究记录里查得到(方法.md 4.2)", (t) => {
|
||||
const m = material(studyUpTo(t, "proposalOpen"), "proposal", "a2", "s1-prop", null, NONE);
|
||||
assert.ok(queryMaterial(m, "e-a1-1")!.includes("已被整份替换,作废"));
|
||||
assert.ok(queryMaterial(m, "e-a2-1")!.includes("乙一起点"));
|
||||
assert.ok(queryMaterial(m, "s0-d1.a1")!.includes("甲的讨论"));
|
||||
assert.ok(queryMaterial(m, "s0-claim.a2")!.includes("e-a2-1"));
|
||||
assert.ok(queryMaterial(m, "entry-assignment")!.includes("e-a1-3"));
|
||||
});
|
||||
|
||||
test("第 2 轮解释的研究记录里查得到第 1 轮和第零轮的已公开记录", (t) => {
|
||||
const m = material(studyUpTo(t, "explOpen2"), "explanation", "a1", "s2-expl", null, { changes: null, artifacts: {} });
|
||||
const list = m.sections.find((s) => s.heading === "研究记录")!.text;
|
||||
const ids = ["run-1", "run-2", "run-3", "r1-c1", "r1-a1-1", "r1-a2-1", "r1-a1-1.a2.1", "r1-a1-x1", "r1-a2-x1", "s1-wrap.a1", "s0-d1.a1", "s0-claim.a2", "entry-assignment"];
|
||||
for (const id of ids) {
|
||||
assert.ok(list.includes(`- ${id}:`), `研究记录清单里应有 ${id}`);
|
||||
assert.notEqual(queryMaterial(m, id), null, id);
|
||||
}
|
||||
});
|
||||
|
||||
test("解释的现在交什么写明每人最多 M 条", (t) => {
|
||||
const m = material(studyUpTo(t, "explOpen1"), "explanation", "a1", "s1-expl", null, { changes: null, artifacts: {} });
|
||||
assert.ok(m.firstMessage.slice(m.firstMessage.lastIndexOf("# 现在交什么")).includes("最多 2 条"));
|
||||
});
|
||||
@@ -0,0 +1,340 @@
|
||||
import type { TestContext } from "node:test";
|
||||
import { Ledger, type SubmissionInput } from "../src/ledger/index.ts";
|
||||
import {
|
||||
adopt,
|
||||
chooseBet,
|
||||
selectAll,
|
||||
selectBestExpected,
|
||||
settleAblation,
|
||||
type AblationExplanation,
|
||||
type BatchInput,
|
||||
type SettleInput,
|
||||
} from "../src/rules/index.ts";
|
||||
import {
|
||||
buildMaterial,
|
||||
readPrompts,
|
||||
type CodeChanges,
|
||||
type FrozenProtocol,
|
||||
type StudyFacts,
|
||||
type TaskMaterial,
|
||||
} from "../src/materials/index.ts";
|
||||
import type { JudgedResult, Range, Stage, Submission, SubmissionKind } from "../src/vocab/index.ts";
|
||||
import { definition, exited, tempDir } from "./ledger-helpers.ts";
|
||||
import { join } from "node:path";
|
||||
|
||||
// materials 测试共用:在真实账本(临时文件)里一步步写入一段研究进程。
|
||||
// 研究者 a1、a2,实验员 x1,核验员 v1。第 1 轮采纳 r1-c1(两个因子),消融测 p1、p2 没测;第 2 轮采纳 r2-c1。
|
||||
// 好认的数和字(7.77x 作者自评、5.55x 甲的第一次预测等)用来检查"不该看到的没出现"。
|
||||
|
||||
export const study: StudyFacts = {
|
||||
finalMetric: "sum-of-radii",
|
||||
direction: "maximize",
|
||||
epsilon: 0.01,
|
||||
knownBest: 2.635983,
|
||||
lambda: 0.1,
|
||||
totalRuns: 10,
|
||||
proposalLimit: 2,
|
||||
factorLimit: 3,
|
||||
metrics: [
|
||||
{ id: "sum-of-radii", layer: "final", meaning: "半径和", unit: "无", direction: "maximize", howMeasured: "校验器", missing: "invalidateRun" },
|
||||
{ id: "wall-seconds", layer: "auxiliary", meaning: "墙钟", unit: "秒", direction: "minimize", howMeasured: "计量 shell", missing: "recordMissing" },
|
||||
],
|
||||
taskText: "# 填圆任务说明(测试用)",
|
||||
wallSeconds: 120,
|
||||
};
|
||||
|
||||
export const prompts = readPrompts(join(import.meta.dirname, "..", "prompts"));
|
||||
|
||||
export const r = (q1: number, q5: number, q9: number): Range => ({ q1, q5, q9 });
|
||||
|
||||
export const SEALED_A1 = r(7.771, 7.772, 7.773);
|
||||
export const SEALED_A2 = r(8.881, 8.882, 8.883);
|
||||
export const FIRST_A1 = r(5.551, 5.552, 5.553);
|
||||
export const FIRST_A2 = r(6.661, 6.662, 6.663);
|
||||
export const CRITIQUE_ID = "r1-a1-1.a2.1";
|
||||
|
||||
const factor = (name: string, continues: string | null) => ({ name, method: `${name}的做法`, keySettings: `${name}的设定`, continues });
|
||||
|
||||
const P_A1: Submission<"proposal"> = {
|
||||
factors: [factor("多起点重启", null), factor("停滞扰动", null)],
|
||||
safeguards: ["guard"],
|
||||
metrics: { defined: [{ id: "restart-count", meaning: "重启次数", unit: "次", direction: "maximize", howMeasured: "数", missing: "recordMissing" }], reused: [] },
|
||||
authorEstimate: SEALED_A1,
|
||||
body: "甲的提案正文",
|
||||
};
|
||||
const P_A2: Submission<"proposal"> = {
|
||||
factors: [factor("收缩半径修复", null)],
|
||||
safeguards: [],
|
||||
metrics: { defined: [], reused: [] },
|
||||
authorEstimate: SEALED_A2,
|
||||
body: "乙的提案正文",
|
||||
};
|
||||
|
||||
export { P_A1, P_A2 };
|
||||
|
||||
export const TABLE_C1 = {
|
||||
factorLocations: [
|
||||
{ factor: "p1", locations: ["solve.py:10-20"] },
|
||||
{ factor: "p2", locations: ["solve.py:30-40"] },
|
||||
],
|
||||
safeguardLocations: [{ safeguard: "guard", locations: ["solve.py:40-45"] }],
|
||||
unassigned: [],
|
||||
};
|
||||
|
||||
export function judged(score: number, answer: string): JudgedResult {
|
||||
return {
|
||||
verdict: "valid",
|
||||
verdictReason: "校验器判合法",
|
||||
score: { status: "measured", value: score },
|
||||
metrics: { "sum-of-radii": { status: "measured", value: score } },
|
||||
answerPath: { status: "read", value: answer },
|
||||
safeguardTriggers: { status: "read", counts: { guard: 2 } },
|
||||
extra: null,
|
||||
};
|
||||
}
|
||||
|
||||
/** 往账本里写的小工具。 */
|
||||
export class Writer {
|
||||
readonly l: Ledger;
|
||||
constructor(l: Ledger) {
|
||||
this.l = l;
|
||||
}
|
||||
open(stageId: string, stage: Stage, round: number, roster: string[]): void {
|
||||
this.l.append("stageOpen", { stageId, stage, round, roster });
|
||||
}
|
||||
close(stageId: string): void {
|
||||
this.l.append("stageClose", { stageId });
|
||||
}
|
||||
sub<K extends SubmissionKind>(stageId: string, author: string, kind: K, target: string | null, item: string | null, content: Submission<K>): number {
|
||||
return this.l.append("submission", { stageId, author, kind, target, item, version: 1, content, jev: null } as SubmissionInput).seq;
|
||||
}
|
||||
decision(decision: string, round: number, input: unknown, output: unknown): void {
|
||||
this.l.append("decision", { decision, round, rule: "test", ruleVersion: 1, params: null, seed: null, input: input as never, output: output as never });
|
||||
}
|
||||
run(run: string, purpose: "baseline" | "forward" | "ablation", round: number, object: string | null, j: JudgedResult): void {
|
||||
this.l.append("runPlanned", { run, purpose, round, object, definition });
|
||||
this.l.append("runStarted", { run });
|
||||
this.l.append("runResult", { run, process: exited, judged: j });
|
||||
}
|
||||
protocol(candidate: string, p: FrozenProtocol): void {
|
||||
this.l.append("protocol", { candidate, protocol: p as never });
|
||||
}
|
||||
}
|
||||
|
||||
export function protocolOf(p: Submission<"proposal">, round: number, baseline: string, proposals: string[], replies: FrozenProtocol["replies"], clauses: string[]): FrozenProtocol {
|
||||
return { factors: p.factors, safeguards: p.safeguards, metrics: p.metrics, body: p.body, clauses, replies, round, baseline, proposals, replyBody: "无" };
|
||||
}
|
||||
|
||||
export function batch(round: number, baseline: number, ranges: Record<string, Record<string, Range>>): { input: BatchInput; output: ReturnType<typeof selectBestExpected> } {
|
||||
const input: BatchInput = {
|
||||
candidates: Object.entries(ranges).map(([id, rs]) => ({ id, ranges: Object.entries(rs).map(([researcher, range]) => ({ researcher, range })) })),
|
||||
baseline,
|
||||
direction: "maximize",
|
||||
epsilon: 0.01,
|
||||
lambda: 0.1,
|
||||
batchCap: 1,
|
||||
balance: 9 - round,
|
||||
seed: "seed",
|
||||
};
|
||||
return { input, output: selectBestExpected(input) };
|
||||
}
|
||||
|
||||
/**
|
||||
* 研究进程的步骤,按先后。studyUpTo 写到某一步(含)为止,返回账本。
|
||||
* 每一步只写这一步的记录,阶段开着的停在"开着",便于测"阶段关闭前别人看不到"。
|
||||
*/
|
||||
const STEPS: Array<[string, (w: Writer, s: Record<string, number>) => void]> = [
|
||||
["baseline", (w) => {
|
||||
w.l.append("code", { object: "base", version: "v-base", locations: null });
|
||||
w.run("run-0", "baseline", 0, null, judged(0.96, "main"));
|
||||
}],
|
||||
["entryOpen", (w) => w.open("s0-entry", "entryDraft", 0, ["a1", "a2"])],
|
||||
["entryA1", (w) => w.sub("s0-entry", "a1", "entry", null, null, { entries: [{ name: "入口甲一", body: "甲一起点" }, { name: "入口甲二", body: "甲二起点" }] })],
|
||||
["entryA2", (w) => w.sub("s0-entry", "a2", "entry", null, null, { entries: [{ name: "入口乙一", body: "乙一起点" }, { name: "入口乙二", body: "乙二起点" }] })],
|
||||
["entryClose", (w) => w.close("s0-entry")],
|
||||
["discussionOpen", (w) => w.open("s0-d1", "discussion", 0, ["a1", "a2"])],
|
||||
["discussion", (w) => {
|
||||
w.sub("s0-d1", "a1", "discussion", null, null, { body: "甲的讨论", entries: [{ name: "入口甲三", body: "甲三起点" }, { name: "入口甲四", body: "甲四起点" }] });
|
||||
w.sub("s0-d1", "a2", "discussion", null, null, { body: "乙的讨论" });
|
||||
w.close("s0-d1");
|
||||
}],
|
||||
["claimOpen", (w) => w.open("s0-claim", "claim", 0, ["a1", "a2"])],
|
||||
["claim", (w) => {
|
||||
w.sub("s0-claim", "a1", "claim", null, null, { choices: ["e-a1-3", "e-a2-1"] });
|
||||
w.sub("s0-claim", "a2", "claim", null, null, { choices: ["e-a2-1"] });
|
||||
w.close("s0-claim");
|
||||
w.decision("entries", 0, null, [{ researcher: "a1", entry: "e-a1-3", rank: 1 }, { researcher: "a2", entry: "e-a2-1", rank: 1 }]);
|
||||
}],
|
||||
["proposalOpen", (w) => w.open("s1-prop", "proposal", 1, ["a1", "a2"])],
|
||||
["proposalA1", (w, s) => {
|
||||
s.sealA1 = w.sub("s1-prop", "a1", "proposal", null, "r1-a1-1", P_A1) + 1;
|
||||
}],
|
||||
["proposalClose", (w) => {
|
||||
w.sub("s1-prop", "a2", "proposal", null, "r1-a2-1", P_A2);
|
||||
w.close("s1-prop");
|
||||
w.decision("dedup", 1, null, { fresh: [{ candidate: "r1-c1", proposals: ["r1-a1-1"] }, { candidate: "r1-c2", proposals: ["r1-a2-1"] }], carried: [] });
|
||||
}],
|
||||
["firstOpen", (w) => w.open("s1-fp", "firstPrediction", 1, ["a1", "a2"])],
|
||||
["firstA1", (w) => w.sub("s1-fp", "a1", "firstPrediction", "r1-c1", null, { range: FIRST_A1, body: "甲第一次理由" })],
|
||||
["firstClose", (w) => {
|
||||
w.sub("s1-fp", "a1", "firstPrediction", "r1-c2", null, { range: r(0.9, 0.95, 1.0), body: "甲第一次理由二" });
|
||||
w.sub("s1-fp", "a2", "firstPrediction", "r1-c1", null, { range: FIRST_A2, body: "乙第一次理由" });
|
||||
w.sub("s1-fp", "a2", "firstPrediction", "r1-c2", null, { range: r(0.9, 0.95, 1.0), body: "乙第一次理由二" });
|
||||
w.close("s1-fp");
|
||||
}],
|
||||
["critiqueOpen", (w) => w.open("s1-crit", "critique", 1, ["a1", "a2"])],
|
||||
["critique", (w) => {
|
||||
w.sub("s1-crit", "a2", "critique", "r1-a1-1", CRITIQUE_ID, { target: "p2", body: "乙的挑刺:扰动可能混杂(推测)" });
|
||||
w.close("s1-crit");
|
||||
}],
|
||||
["responseOpen", (w) => w.open("s1-resp", "response", 1, ["a1", "a2"])],
|
||||
["response", (w) => {
|
||||
w.sub("s1-resp", "a1", "response", "r1-c1", null, { replies: [{ critique: CRITIQUE_ID, reply: "甲答复:保留扰动" }], changes: {}, clauses: ["加多起点", "加扰动"], body: "无" });
|
||||
w.sub("s1-resp", "a2", "response", "r1-c2", null, { replies: [], changes: {}, clauses: ["加修复"], body: "无" });
|
||||
w.close("s1-resp");
|
||||
w.protocol("r1-c1", protocolOf(P_A1, 1, "base", ["r1-a1-1"], [{ critique: CRITIQUE_ID, reply: "甲答复:保留扰动" }], ["加多起点", "加扰动"]));
|
||||
w.protocol("r1-c2", protocolOf(P_A2, 1, "base", ["r1-a2-1"], [], ["加修复"]));
|
||||
}],
|
||||
["secondOpen", (w) => w.open("s1-sp", "secondPrediction", 1, ["a1", "a2"])],
|
||||
["second", (w) => {
|
||||
const ranges = { "r1-c1": { a1: r(1.05, 1.15, 1.25), a2: r(1.0, 1.1, 1.2) }, "r1-c2": { a1: r(0.9, 0.97, 1.0), a2: r(0.9, 0.96, 1.0) } };
|
||||
for (const [c, rs] of Object.entries(ranges)) for (const [a, range] of Object.entries(rs)) w.sub("s1-sp", a, "secondPrediction", c, null, { range, body: "第二次理由" });
|
||||
w.close("s1-sp");
|
||||
const b = batch(1, 0.96, ranges);
|
||||
w.decision("batch", 1, b.input, b.output);
|
||||
}],
|
||||
["implOpen", (w) => w.open("s1-impl-c1", "implementation", 1, ["x1"])],
|
||||
["impl", (w) => {
|
||||
w.sub("s1-impl-c1", "x1", "implementation", "r1-c1", null, { ...TABLE_C1, concerns: "guard 可能挡掉一些摆法", body: "实现说明" });
|
||||
w.close("s1-impl-c1");
|
||||
w.l.append("code", { object: "r1-c1", version: "v-c1", locations: TABLE_C1 });
|
||||
}],
|
||||
["verifyOpen", (w) => w.open("s1-ver-c1", "verification", 1, ["v1"])],
|
||||
["verify", (w) => {
|
||||
w.sub("s1-ver-c1", "v1", "verification", "r1-c1", null, {
|
||||
clauses: [{ clause: "c1", found: true, where: "solve.py:10" }, { clause: "c2", found: true, where: "solve.py:30" }],
|
||||
factors: [{ factor: "p1", found: true, where: "solve.py:10-20" }, { factor: "p2", found: true, where: "solve.py:30-40" }],
|
||||
safeguards: [{ safeguard: "guard", found: true, where: "solve.py:40-45" }],
|
||||
unassigned: [],
|
||||
body: "核验员:都找到了",
|
||||
});
|
||||
w.close("s1-ver-c1");
|
||||
}],
|
||||
["run1", (w) => w.run("run-1", "forward", 1, "r1-c1", judged(1.2, "guard"))],
|
||||
["adopt1", (w) => {
|
||||
const input = { baseline: 0.96, knownBest: 2.635983, epsilon: 0.01, direction: "maximize" as const, candidates: [{ id: "r1-c1", score: 1.2 }] };
|
||||
w.decision("adopt", 1, input, adopt(input));
|
||||
}],
|
||||
["explOpen1", (w) => w.open("s1-expl", "explanation", 1, ["a1", "a2"])],
|
||||
["expl1", (w) => {
|
||||
w.sub("s1-expl", "a1", "explanation", null, null, { explanations: [{ body: "甲的解释", cells: [{ factor: "p1", range: r(1.0, 1.1, 1.15) }, { factor: "p2", range: r(1.1, 1.15, 1.19) }] }] });
|
||||
w.sub("s1-expl", "a2", "explanation", null, null, { explanations: [{ body: "乙的解释", cells: [{ factor: "p1", range: r(0.98, 1.05, 1.12) }, { factor: "p2", range: r(1.12, 1.16, 1.19) }] }] });
|
||||
w.close("s1-expl");
|
||||
w.decision("cells", 1, null, selectAll({ ...ablationBase(), balance: 2, batchCap: 1, u: 0.5, decay: 0.5, seed: "seed" }));
|
||||
}],
|
||||
["ablationImplOpen", (w) => w.open("s1-ai", "ablationImplementation", 1, ["x1"])],
|
||||
["ablationImpl", (w) => {
|
||||
w.sub("s1-ai", "x1", "ablationImplementation", "r1-c1-p1", null, { alsoOff: [], cannotRun: null, concerns: "无" });
|
||||
w.close("s1-ai");
|
||||
w.l.append("code", { object: "r1-c1-p1", version: "v-c1-p1", locations: null });
|
||||
w.open("s1-av", "ablationVerification", 1, ["v1"]);
|
||||
}],
|
||||
["ablation1", (w) => {
|
||||
w.sub("s1-av", "v1", "ablationVerification", "r1-c1-p1", null, { factors: [{ factor: "p1", off: true, where: "solve.py:10" }, { factor: "p2", off: false, where: "solve.py:30" }] });
|
||||
w.close("s1-av");
|
||||
w.run("run-2", "ablation", 1, "r1-c1-p1", judged(1.05, "guard"));
|
||||
const input: SettleInput = { ...ablationBase(), cells: [{ factor: "p1", outcome: { kind: "valid", score: 1.05 } }, { factor: "p2", outcome: { kind: "untested" } }] };
|
||||
w.l.append("settlement", { round: 1, result: { candidate: "r1-c1", input, output: settleAblation(input) } as never });
|
||||
const betInput = { ...ablationBase(), cells: input.cells };
|
||||
w.decision("bet", 1, betInput, chooseBet(betInput));
|
||||
}],
|
||||
["wrapOpen", (w) => w.open("s1-wrap", "wrapUp", 1, ["a1", "a2"])],
|
||||
["wrap1", (w, s) => {
|
||||
w.sub("s1-wrap", "a1", "wrapUp", null, null, { body: "甲的收尾" });
|
||||
w.sub("s1-wrap", "a2", "wrapUp", null, null, { body: "乙的收尾" });
|
||||
w.close("s1-wrap");
|
||||
w.l.append("unseal", { candidate: "r1-c1", seals: [s.sealA1!] });
|
||||
}],
|
||||
// 第 2 轮:基线换成 r1-c1(1.2);r1-c2 因基线变了离开候选池。
|
||||
["proposal2Open", (w) => w.open("s2-prop", "proposal", 2, ["a1", "a2"])],
|
||||
["proposal2", (w) => {
|
||||
w.sub("s2-prop", "a1", "proposal", null, "r2-a1-1", { ...P_A1, factors: [factor("新的局部搜索", "r1-c1-p1"), factor("更多起点", null)], body: "甲的第二轮提案", authorEstimate: r(1.21, 1.25, 1.3) });
|
||||
w.sub("s2-prop", "a2", "proposal", null, "r2-a2-1", { ...P_A2, factors: [factor("更细的修复", null), factor("早停", null)], body: "乙的第二轮提案", authorEstimate: r(1.2, 1.22, 1.24) });
|
||||
w.close("s2-prop");
|
||||
w.decision("dedup", 2, null, { fresh: [{ candidate: "r2-c1", proposals: ["r2-a1-1"] }, { candidate: "r2-c2", proposals: ["r2-a2-1"] }], carried: [] });
|
||||
}],
|
||||
["round2", (w) => {
|
||||
w.open("s2-fp", "firstPrediction", 2, ["a1", "a2"]);
|
||||
for (const c of ["r2-c1", "r2-c2"]) for (const a of ["a1", "a2"]) w.sub("s2-fp", a, "firstPrediction", c, null, { range: r(1.19, 1.22, 1.26), body: "理由" });
|
||||
w.close("s2-fp");
|
||||
w.open("s2-crit", "critique", 2, ["a1", "a2"]);
|
||||
w.close("s2-crit");
|
||||
w.open("s2-resp", "response", 2, ["a1", "a2"]);
|
||||
w.sub("s2-resp", "a1", "response", "r2-c1", null, { replies: [], changes: {}, clauses: ["加局部搜索"], body: "无" });
|
||||
w.sub("s2-resp", "a2", "response", "r2-c2", null, { replies: [], changes: {}, clauses: ["改修复", "加早停"], body: "无" });
|
||||
w.close("s2-resp");
|
||||
w.protocol("r2-c1", protocolOf({ ...P_A1, metrics: { defined: [], reused: ["restart-count"] }, factors: [factor("新的局部搜索", "r1-c1-p1"), factor("更多起点", null)], body: "甲的第二轮提案" }, 2, "r1-c1", ["r2-a1-1"], [], ["加局部搜索"]));
|
||||
w.protocol("r2-c2", protocolOf({ ...P_A2, factors: [factor("更细的修复", null), factor("早停", null)], body: "乙的第二轮提案" }, 2, "r1-c1", ["r2-a2-1"], [], ["改修复", "加早停"]));
|
||||
w.open("s2-sp", "secondPrediction", 2, ["a1", "a2"]);
|
||||
const ranges = { "r2-c1": { a1: r(1.2, 1.24, 1.3), a2: r(1.19, 1.22, 1.27) }, "r2-c2": { a1: r(1.18, 1.2, 1.22), a2: r(1.17, 1.2, 1.22) } };
|
||||
for (const [c, rs] of Object.entries(ranges)) for (const [a, range] of Object.entries(rs)) w.sub("s2-sp", a, "secondPrediction", c, null, { range, body: "理由" });
|
||||
w.close("s2-sp");
|
||||
const b = batch(2, 1.2, ranges);
|
||||
w.decision("batch", 2, b.input, b.output);
|
||||
}],
|
||||
["implOpen2", (w) => w.open("s2-impl-c1", "implementation", 2, ["x1"])],
|
||||
["adopt2", (w) => {
|
||||
const table = { factorLocations: [{ factor: "p1", locations: ["solve.py:50-60"] }, { factor: "p2", locations: ["solve.py:10-20"] }], safeguardLocations: [{ safeguard: "guard", locations: ["solve.py:40-45"] }], unassigned: [] };
|
||||
w.sub("s2-impl-c1", "x1", "implementation", "r2-c1", null, { ...table, concerns: "无", body: "第二轮实现说明" });
|
||||
w.close("s2-impl-c1");
|
||||
w.l.append("code", { object: "r2-c1", version: "v-r2-c1", locations: table });
|
||||
w.run("run-3", "forward", 2, "r2-c1", judged(1.3, "main"));
|
||||
const input = { baseline: 1.2, knownBest: 2.635983, epsilon: 0.01, direction: "maximize" as const, candidates: [{ id: "r2-c1", score: 1.3 }] };
|
||||
w.decision("adopt", 2, input, adopt(input));
|
||||
}],
|
||||
["explOpen2", (w) => w.open("s2-expl", "explanation", 2, ["a1", "a2"])],
|
||||
];
|
||||
|
||||
function ablationBase() {
|
||||
const explanations: AblationExplanation[] = [
|
||||
{ id: "r1-a1-x1", author: "a1", cells: [{ factor: "p1", range: r(1.0, 1.1, 1.15) }, { factor: "p2", range: r(1.1, 1.15, 1.19) }] },
|
||||
{ id: "r1-a2-x1", author: "a2", cells: [{ factor: "p1", range: r(0.98, 1.05, 1.12) }, { factor: "p2", range: r(1.12, 1.16, 1.19) }] },
|
||||
];
|
||||
return { baseline: 0.96, full: 1.2, direction: "maximize" as const, knownBest: 2.635983, epsilon: 0.01, lambda: 0.1, factors: ["p1", "p2"], explanations };
|
||||
}
|
||||
|
||||
export type Step = (typeof STEPS)[number][0];
|
||||
|
||||
/** 写到 last 这一步(含)为止的账本,测试结束时关掉。 */
|
||||
export function studyUpTo(t: TestContext, last: string): Ledger {
|
||||
const l = Ledger.open(join(tempDir(t), "ledger.db"));
|
||||
t.after(() => l.close());
|
||||
const w = new Writer(l);
|
||||
const state: Record<string, number> = {};
|
||||
const end = STEPS.findIndex(([name]) => name === last);
|
||||
if (end < 0) throw new Error(`没有这一步:${last}`);
|
||||
for (const [, step] of STEPS.slice(0, end + 1)) step(w, state);
|
||||
return l;
|
||||
}
|
||||
|
||||
/** 生成一份材料。 */
|
||||
export function material(
|
||||
ledger: Ledger,
|
||||
kind: SubmissionKind,
|
||||
actor: string,
|
||||
stageId: string,
|
||||
object: string | null,
|
||||
extra: { changes: CodeChanges | null; artifacts: Record<string, Record<string, string>> | null },
|
||||
): TaskMaterial {
|
||||
return buildMaterial({ kind, actor, stageId, object, ledger, study, prompts, codeRoot: "/code", ...extra });
|
||||
}
|
||||
|
||||
export const NONE = { changes: null, artifacts: null };
|
||||
|
||||
/** 材料里给模型的全部文字:第一条消息和查询记录。 */
|
||||
export const everything = (m: TaskMaterial): string => [m.firstMessage, ...m.records.map((r) => `${r.title}\n${r.text}`)].join("\n");
|
||||
|
||||
export const CHANGES_C1: CodeChanges = {
|
||||
files: [{ path: "solve.py", change: "modified", modeOnly: false, binary: false, hunks: [{ from: { start: 10, end: 12 }, to: { start: 10, end: 22 } }] }],
|
||||
diff: "diff --git a/solve.py b/solve.py\n@@ -10,3 +10,13 @@\n+多起点循环\n",
|
||||
};
|
||||
@@ -0,0 +1,446 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { test } from "node:test";
|
||||
import { beliefBook, callId, queryMaterial, readPrompts, type TaskMaterial } from "../src/materials/index.ts";
|
||||
import { SUBMISSION_KINDS, type SubmissionKind } from "../src/vocab/index.ts";
|
||||
import { tempDir } from "./ledger-helpers.ts";
|
||||
import { CHANGES_C1, CRITIQUE_ID, everything, material, NONE, study, studyUpTo } from "./materials-helpers.ts";
|
||||
|
||||
// 每种任务至少一个测试:在真实账本里写到某一步,生成材料,查该有的有、不该有的没有、对号清单对。
|
||||
// 预期值按方法.md 条文手写(研究进程见 materials-helpers.ts 的注释),不从实现推。
|
||||
|
||||
const headings = (m: TaskMaterial) => m.sections.map((s) => s.heading);
|
||||
const has = (m: TaskMaterial, text: string) => assert.ok(everything(m).includes(text), `材料里应有"${text}"`);
|
||||
const lacks = (m: TaskMaterial, text: string) => assert.ok(!everything(m).includes(text), `材料里不该有"${text}"`);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 第零轮(方法.md 4.2)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("入口:只有任务与基线;看不到别人已交的入口(4.2 闭门写入口)", (t) => {
|
||||
const l = studyUpTo(t, "entryA1");
|
||||
const m = material(l, "entry", "a2", "s0-entry", null, NONE);
|
||||
assert.deepEqual(m.checklist, { kind: "entry", entries: { min: 2, max: 3 } });
|
||||
has(m, "本轮基线 base:最终指标 0.96");
|
||||
assert.deepEqual(m.readable, [{ version: "v-base", label: "本批基线", dir: "/code/v-base" }]);
|
||||
lacks(m, "入口甲一");
|
||||
assert.ok(!headings(m).includes("信念本"), "第零轮不放信念本(4.2:信念本不变)");
|
||||
});
|
||||
|
||||
test("讨论:全部入口标明作者,编号按作者从 1 编", (t) => {
|
||||
const l = studyUpTo(t, "discussionOpen");
|
||||
const m = material(l, "discussion", "a1", "s0-d1", null, NONE);
|
||||
assert.deepEqual(m.checklist, { kind: "discussion", entries: { min: 2, max: 3 } });
|
||||
for (const x of ["e-a1-1:入口甲一", "e-a1-2:入口甲二", "e-a2-1:入口乙一", "e-a2-2:入口乙二"]) has(m, x);
|
||||
assert.ok(headings(m).includes("入口编号对照"));
|
||||
});
|
||||
|
||||
test("认领:整份替换后新清单接着编号,旧编号作废;至少排 min(N, 入口总数)", (t) => {
|
||||
const l = studyUpTo(t, "claimOpen");
|
||||
const m = material(l, "claim", "a2", "s0-claim", null, NONE);
|
||||
// a1 讨论时整份换成两个新入口,接着 e-a1-2 往后编:e-a1-3、e-a1-4;a2 没换。N = 2,入口 4 个。
|
||||
assert.deepEqual(m.checklist, { kind: "claim", entries: ["e-a1-3", "e-a1-4", "e-a2-1", "e-a2-2"], minChoices: 2 });
|
||||
has(m, "e-a1-1(作废):入口甲一");
|
||||
has(m, "甲的讨论");
|
||||
has(m, "乙的讨论");
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 第 r 轮(方法.md 4.3)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("第 1 轮提案:你认领的入口放在最前;没有上一轮;对号清单", (t) => {
|
||||
const l = studyUpTo(t, "proposalOpen");
|
||||
const m = material(l, "proposal", "a1", "s1-prop", null, NONE);
|
||||
assert.deepEqual(headings(m).slice(0, 2), ["你自己的", "你认领的入口"]);
|
||||
has(m, "e-a1-3:入口甲三");
|
||||
has(m, "从这里开始看");
|
||||
has(m, "这是第 1 轮,没有上一轮");
|
||||
assert.deepEqual(m.checklist, {
|
||||
kind: "proposal",
|
||||
maxProposals: 2,
|
||||
factorLimit: 3,
|
||||
continuable: [],
|
||||
reusableMetrics: [],
|
||||
takenMetricIds: ["sum-of-radii", "wall-seconds"],
|
||||
});
|
||||
const h = headings(m);
|
||||
assert.ok(h.indexOf("你自己的") < h.indexOf("公共材料") && h.indexOf("公共材料") < h.indexOf("信念本"), "先你自己的、后公共材料");
|
||||
});
|
||||
|
||||
test("第 2 轮提案:上一轮全部收尾、上一轮实验员的提醒、可延续的因子、已登记指标;入口标为第 1 轮的起点", (t) => {
|
||||
const l = studyUpTo(t, "proposal2Open");
|
||||
const m = material(l, "proposal", "a2", "s2-prop", null, NONE);
|
||||
has(m, "甲的收尾");
|
||||
has(m, "乙的收尾");
|
||||
has(m, "guard 可能挡掉一些摆法");
|
||||
has(m, "第 1 轮的起点;之后是否沿用由你定");
|
||||
assert.deepEqual(m.checklist, {
|
||||
kind: "proposal",
|
||||
maxProposals: 2,
|
||||
factorLimit: 3,
|
||||
continuable: ["r1-c1-p1", "r1-c1-p2", "r1-c2-p1"],
|
||||
reusableMetrics: ["restart-count"],
|
||||
takenMetricIds: ["sum-of-radii", "wall-seconds", "restart-count"],
|
||||
});
|
||||
for (const x of ["上一轮的收尾", "上一轮实验员实现时的提醒", '可以写进"延续"的因子', "已登记指标清单", "研究记录", "信念本"]) {
|
||||
assert.ok(headings(m).includes(x), `应有一节"${x}"`);
|
||||
}
|
||||
});
|
||||
|
||||
test("第一次预测:给这个候选的原始提案;看不到别人刚交的第一次预测,也看不到作者自评", (t) => {
|
||||
const l = studyUpTo(t, "firstA1");
|
||||
const m = material(l, "firstPrediction", "a2", "s1-fp", "r1-c1", NONE);
|
||||
assert.deepEqual(m.checklist, { kind: "firstPrediction", candidate: "r1-c1" });
|
||||
has(m, "甲的提案正文");
|
||||
lacks(m, "5.551");
|
||||
lacks(m, "甲第一次理由");
|
||||
lacks(m, "7.772");
|
||||
for (const x of ["研究记录", "信念本"]) assert.ok(headings(m).includes(x));
|
||||
});
|
||||
|
||||
test("挑刺:按原提案挑,对号清单是那条提案和它的因子", (t) => {
|
||||
const l = studyUpTo(t, "critiqueOpen");
|
||||
const m = material(l, "critique", "a2", "s1-crit", "r1-a1-1", NONE);
|
||||
assert.deepEqual(m.checklist, { kind: "critique", proposal: "r1-a1-1", factors: ["p1", "p2"] });
|
||||
has(m, "甲的提案正文");
|
||||
lacks(m, "5.551");
|
||||
lacks(m, "6.661");
|
||||
});
|
||||
|
||||
test("回应:本人原提案和针对它的全部挑刺", (t) => {
|
||||
const l = studyUpTo(t, "responseOpen");
|
||||
const m = material(l, "response", "a1", "s1-resp", "r1-c1", NONE);
|
||||
has(m, "乙的挑刺:扰动可能混杂(推测)");
|
||||
assert.deepEqual(m.checklist, {
|
||||
kind: "response",
|
||||
candidate: "r1-c1",
|
||||
critiques: [CRITIQUE_ID],
|
||||
factorLimit: 3,
|
||||
continuable: [],
|
||||
reusableMetrics: [],
|
||||
takenMetricIds: ["sum-of-radii", "wall-seconds"],
|
||||
});
|
||||
});
|
||||
|
||||
test("统一:全部被合并的原提案、针对它们的挑刺、要逐条体现的原提案", (t) => {
|
||||
const l = studyUpTo(t, "responseOpen");
|
||||
const m = material(l, "unification", "a1", "s1-resp", "r1-c1", NONE);
|
||||
const c = m.checklist as Extract<TaskMaterial["checklist"], { kind: "unification" }>;
|
||||
assert.deepEqual([c.sources, c.critiques], [["r1-a1-1"], [CRITIQUE_ID]]);
|
||||
has(m, "甲的提案正文");
|
||||
});
|
||||
|
||||
test("第二次预测:冻结协议、挑刺与答复配对、本人第一次的三个数;看不到别人的第一次和作者自评(4.3 ⑦)", (t) => {
|
||||
const l = studyUpTo(t, "secondOpen");
|
||||
const m = material(l, "secondPrediction", "a2", "s1-sp", "r1-c1", NONE);
|
||||
assert.deepEqual(m.checklist, { kind: "secondPrediction", candidate: "r1-c1" });
|
||||
has(m, "6.661 / 6.662 / 6.663");
|
||||
has(m, "乙的挑刺:扰动可能混杂(推测)");
|
||||
has(m, "作者的答复:甲答复:保留扰动");
|
||||
lacks(m, "5.551");
|
||||
lacks(m, "甲第一次理由");
|
||||
lacks(m, "7.772");
|
||||
});
|
||||
|
||||
test("解释:本轮运行、提醒、要给三个数的格、产物只进查询;看不到别人的第一次预测", (t) => {
|
||||
const l = studyUpTo(t, "explOpen1");
|
||||
const m = material(l, "explanation", "a2", "s1-expl", null, { changes: null, artifacts: { "run-1": { "stdout.log": "日志正文" } } });
|
||||
assert.deepEqual(m.checklist, { kind: "explanation", cells: ["p1", "p2"], maxExplanations: 2 });
|
||||
has(m, "分数 1.2");
|
||||
// 正向运行的答案出自保障:换一句;方法原句只用在消融格(见收尾的测试)。
|
||||
has(m, "这次答案出自保障 guard,分数可能主要是这条保障的效果。");
|
||||
lacks(m, "本格答案出自保障");
|
||||
has(m, "guard 可能挡掉一些摆法");
|
||||
assert.ok(!m.firstMessage.includes("日志正文"), "产物不进第一条消息");
|
||||
assert.equal(queryMaterial(m, "run-1/stdout.log"), "运行 run-1 的产物 stdout.log\n\n日志正文");
|
||||
lacks(m, "5.551");
|
||||
lacks(m, "8.882");
|
||||
});
|
||||
|
||||
test("收尾:本轮采纳、选格、押的因子", (t) => {
|
||||
const l = studyUpTo(t, "wrapOpen");
|
||||
const m = material(l, "wrapUp", "a1", "s1-wrap", null, NONE);
|
||||
assert.deepEqual(m.checklist, { kind: "wrapUp" });
|
||||
has(m, "本轮采纳候选 r1-c1");
|
||||
has(m, "本轮选格:测 p1");
|
||||
has(m, "程序按方向收益押的因子:r1-c1-p1");
|
||||
has(m, "r1-c1-p2 停滞扰动(停滞扰动的设定):候选 r1-c1,基线 base;没测:剩余次数不够");
|
||||
has(m, "本格答案出自保障 guard,量到的可能是这条保障的效果,而不是关掉这个因子的效果;保障动手次数");
|
||||
lacks(m, "7.772");
|
||||
});
|
||||
|
||||
test("实现:冻结协议、指标定义、要交的位置表、起点代码;没有信念本、成绩和别的候选", (t) => {
|
||||
const l = studyUpTo(t, "implOpen");
|
||||
const m = material(l, "implementation", "x1", "s1-impl-c1", "r1-c1", NONE);
|
||||
assert.deepEqual(m.checklist, { kind: "implementation", candidate: "r1-c1", clauses: ["c1", "c2"], factors: ["p1", "p2"], safeguards: ["guard"] });
|
||||
assert.deepEqual(m.readable.map((r) => r.version), ["v-base"]);
|
||||
for (const x of ["任务与本轮", "基线代码", "本任务要处理的东西", "要实现的指标定义", "要交的因子位置表"]) assert.ok(headings(m).includes(x));
|
||||
assert.ok(!headings(m).includes("信念本"));
|
||||
has(m, "restart-count");
|
||||
lacks(m, "乙的提案正文");
|
||||
lacks(m, "收缩半径修复");
|
||||
lacks(m, "6.661");
|
||||
});
|
||||
|
||||
test("修复:退回意见、上一次的位置表、上一次的改动", (t) => {
|
||||
const l = studyUpTo(t, "verifyOpen");
|
||||
l.append("submission", {
|
||||
stageId: "s1-ver-c1",
|
||||
author: "v1",
|
||||
kind: "verification",
|
||||
target: "r1-c1",
|
||||
item: null,
|
||||
version: 1,
|
||||
jev: null,
|
||||
content: {
|
||||
clauses: [{ clause: "c1", found: true, where: "solve.py:10" }, { clause: "c2", found: true, where: "solve.py:30" }],
|
||||
factors: [{ factor: "p1", found: true, where: "solve.py:10" }, { factor: "p2", found: false, where: "看了 solve.py 全文" }],
|
||||
safeguards: [{ safeguard: "guard", found: true, where: "solve.py:40" }],
|
||||
unassigned: [],
|
||||
body: "p2 没找到",
|
||||
},
|
||||
});
|
||||
l.append("stageClose", { stageId: "s1-ver-c1" });
|
||||
l.append("stageOpen", { stageId: "s1-repair-c1", stage: "implementation", round: 1, roster: ["x1"] });
|
||||
const m = material(l, "repair", "x1", "s1-repair-c1", "r1-c1", { changes: CHANGES_C1, artifacts: null });
|
||||
assert.deepEqual(m.checklist, { kind: "repair", candidate: "r1-c1", clauses: ["c1", "c2"], factors: ["p1", "p2"], safeguards: ["guard"] });
|
||||
has(m, "因子 p2 没落实:看了 solve.py 全文");
|
||||
has(m, "+多起点循环");
|
||||
assert.deepEqual(m.readable.map((r) => r.version), ["v-base", "v-c1"]);
|
||||
});
|
||||
|
||||
test("核验:没有归属的改动排第一;因子与保障同行只作提醒;对号清单带没有归属的改动", (t) => {
|
||||
const l = studyUpTo(t, "verifyOpen");
|
||||
const m = material(l, "verification", "v1", "s1-ver-c1", "r1-c1", { changes: CHANGES_C1, artifacts: null });
|
||||
assert.deepEqual(headings(m).slice(0, 2), ["分给你的材料", "没归到任何因子或保障的改动"]);
|
||||
// 改动是候选第 10–22 行,位置表 p1 盖住 10–20:21–22 没有归属。p2 的 30–40 与 guard 的 40–45 在第 40 行重叠。
|
||||
assert.deepEqual(m.checklist, {
|
||||
kind: "verification",
|
||||
unassigned: ["solve.py:21-22"],
|
||||
candidate: "r1-c1",
|
||||
clauses: ["c1", "c2"],
|
||||
factors: ["p1", "p2"],
|
||||
safeguards: ["guard"],
|
||||
});
|
||||
has(m, "solve.py:40:因子 p2、保障 guard");
|
||||
assert.deepEqual(m.readable.map((r) => r.version), ["v-base", "v-c1"]);
|
||||
assert.ok(!headings(m).includes("信念本"));
|
||||
});
|
||||
|
||||
test("消融实现:这一格、因子清单、完整代码的位置表;看不到运行成绩", (t) => {
|
||||
const l = studyUpTo(t, "ablationImplOpen");
|
||||
const m = material(l, "ablationImplementation", "x1", "s1-ai", "r1-c1-p1", NONE);
|
||||
assert.deepEqual(m.checklist, { kind: "ablationImplementation", cell: "r1-c1-p1", off: "p1", factors: ["p1", "p2"] });
|
||||
assert.ok(headings(m).includes("完整代码(消融的起点)"));
|
||||
assert.deepEqual(m.readable.map((r) => r.version), ["v-c1", "v-base"]);
|
||||
lacks(m, "1.2");
|
||||
lacks(m, "甲的解释");
|
||||
});
|
||||
|
||||
test("消融核验:实际要关的因子、改动全文", (t) => {
|
||||
const l = studyUpTo(t, "ablationImpl");
|
||||
const m = material(l, "ablationVerification", "v1", "s1-av", "r1-c1-p1", { changes: CHANGES_C1, artifacts: null });
|
||||
assert.deepEqual(m.checklist, { kind: "ablationVerification", cell: "r1-c1-p1", factors: ["p1", "p2"] });
|
||||
has(m, "p1(只关指定的因子)");
|
||||
assert.deepEqual(m.readable.map((r) => r.version), ["v-c1", "v-base", "v-c1-p1"]);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 七条可见性规则(任务书 09 第二部分),每条一个"不该看到"
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("可见性 1:同一阶段没关之前,别人看不到已交的提交", (t) => {
|
||||
const l = studyUpTo(t, "proposalA1");
|
||||
const m = material(l, "proposal", "a2", "s1-prop", null, NONE);
|
||||
lacks(m, "甲的提案正文");
|
||||
lacks(m, "多起点重启");
|
||||
// 本人自己当前阶段已交的看得到
|
||||
has(material(l, "proposal", "a1", "s1-prop", null, NONE), "甲的提案正文");
|
||||
});
|
||||
|
||||
test("可见性 2:作者自评到本轮结束解封前,任何任务的材料里都没有", (t) => {
|
||||
const cases: Array<[string, SubmissionKind, string, string, string | null]> = [
|
||||
["firstOpen", "firstPrediction", "a1", "s1-fp", "r1-c1"],
|
||||
["critiqueOpen", "critique", "a2", "s1-crit", "r1-a1-1"],
|
||||
["responseOpen", "response", "a1", "s1-resp", "r1-c1"],
|
||||
["secondOpen", "secondPrediction", "a1", "s1-sp", "r1-c1"],
|
||||
["implOpen", "implementation", "x1", "s1-impl-c1", "r1-c1"],
|
||||
["wrapOpen", "wrapUp", "a1", "s1-wrap", null],
|
||||
];
|
||||
for (const [step, kind, actor, stage, object] of cases) {
|
||||
const l = studyUpTo(t, step);
|
||||
const m = material(l, kind, actor, stage, object, NONE);
|
||||
for (const sealed of ["7.771", "7.772", "8.881", "8.882"]) lacks(m, sealed);
|
||||
}
|
||||
// 解封以后(跑过的 r1-c1)进战绩;没跑的 r1-c2 继续封存。
|
||||
const after = beliefBook(studyUpTo(t, "proposal2Open"), study, 2);
|
||||
assert.ok(after.includes("作者自评 vs 大家合并:r1-c1(作者 a1)"));
|
||||
assert.ok(!after.includes("8.882"));
|
||||
});
|
||||
|
||||
test("可见性 3:第一次预测只有本人看得到,之后各阶段也不给别人", (t) => {
|
||||
const l = studyUpTo(t, "wrapOpen");
|
||||
for (const m of [material(l, "wrapUp", "a2", "s1-wrap", null, NONE), material(l, "wrapUp", "a1", "s1-wrap", null, NONE)]) {
|
||||
lacks(m, "5.551");
|
||||
lacks(m, "6.661");
|
||||
}
|
||||
});
|
||||
|
||||
test("可见性 4:第二次预测看不到别人第一次的数", (t) => {
|
||||
const l = studyUpTo(t, "secondOpen");
|
||||
const m = material(l, "secondPrediction", "a1", "s1-sp", "r1-c1", NONE);
|
||||
has(m, "5.551 / 5.552 / 5.553");
|
||||
lacks(m, "6.661");
|
||||
lacks(m, "乙第一次理由");
|
||||
});
|
||||
|
||||
test("可见性 5:第零轮闭门写入口时看不到别人的入口", (t) => {
|
||||
const l = studyUpTo(t, "entryA2");
|
||||
const m = material(l, "entry", "a1", "s0-entry", null, NONE);
|
||||
lacks(m, "入口乙一");
|
||||
has(m, "入口甲一");
|
||||
});
|
||||
|
||||
test("可见性 6:别人的退回不放进材料;本人重新发起时放上一次被退回的原因", (t) => {
|
||||
const l = studyUpTo(t, "secondOpen");
|
||||
const call = callId("s1-sp", "a1", "secondPrediction", "r1-c1", 1);
|
||||
l.append("callStarted", { call, actor: "a1", stageId: "s1-sp", model: "glm-5.3", materials: null });
|
||||
l.append("rejection", { stageId: "s1-sp", author: "a1", kind: "secondPrediction", submitted: { range: { q1: 3, q5: 2, q9: 1 } }, reason: "甲被退回:三个数不递增", jev: null });
|
||||
l.append("callEnded", { call, wallSeconds: { status: "measured", value: 3 }, status: "notSubmitted", reason: "没交成" });
|
||||
const retry = material(l, "secondPrediction", "a1", "s1-sp", "r1-c1", NONE);
|
||||
assert.ok(retry.firstMessage.includes("# 上一次为什么没交成"));
|
||||
has(retry, "甲被退回:三个数不递增");
|
||||
lacks(material(l, "secondPrediction", "a1", "s1-sp", "r1-c2", NONE), "甲被退回");
|
||||
lacks(material(l, "secondPrediction", "a2", "s1-sp", "r1-c1", NONE), "甲被退回");
|
||||
});
|
||||
|
||||
test("可见性 7:信念本只写已公开的记录:正向运行在采纳决定写入后才出现", (t) => {
|
||||
const before = beliefBook(studyUpTo(t, "run1"), study, 1);
|
||||
assert.ok(!before.includes("run-1"));
|
||||
const after = beliefBook(studyUpTo(t, "adopt1"), study, 1);
|
||||
assert.ok(after.includes("运行 run-1:算数(校验器判合法),分数 1.2"));
|
||||
});
|
||||
|
||||
test("实验员、核验员看不到别的候选", (t) => {
|
||||
const l = studyUpTo(t, "verifyOpen");
|
||||
for (const m of [
|
||||
material(l, "verification", "v1", "s1-ver-c1", "r1-c1", { changes: CHANGES_C1, artifacts: null }),
|
||||
material(studyUpTo(t, "implOpen"), "implementation", "x1", "s1-impl-c1", "r1-c1", NONE),
|
||||
]) {
|
||||
lacks(m, "r1-c2");
|
||||
lacks(m, "乙的");
|
||||
assert.equal(queryMaterial(m, "r1-c2"), null);
|
||||
}
|
||||
});
|
||||
|
||||
test("没有归属的改动:手写两个版本和位置表,算出的清单与手算的一致", (t) => {
|
||||
const l = studyUpTo(t, "verifyOpen");
|
||||
// 位置表:p1 a.py:1-5;p2 a.py:20 和整个 b.py;保障 guard a.py:5-6。
|
||||
l.append("code", {
|
||||
object: "r1-c1",
|
||||
version: "v-c1",
|
||||
locations: {
|
||||
factorLocations: [{ factor: "p1", locations: ["a.py:1-5"] }, { factor: "p2", locations: ["a.py:20", "b.py"] }],
|
||||
safeguardLocations: [{ safeguard: "guard", locations: ["a.py:5-6"] }],
|
||||
unassigned: [{ location: "a.py:7", note: "实验员说是 import" }],
|
||||
},
|
||||
});
|
||||
// 旧版本 → 新版本:a.py 新版本第 1–8 行是改过的;删了旧第 11–12 行(新版本第 10 行之后);删了旧第 21 行(新版本第 19 行之后)。
|
||||
// b.py 改了第 3–4 行;c.bin 新增二进制;run.sh 只改可执行位;empty.txt 新增空文件。
|
||||
const changes = {
|
||||
files: [
|
||||
{ path: "a.py", change: "modified" as const, modeOnly: false, binary: false as const, hunks: [
|
||||
{ from: { start: 1, end: 3 }, to: { start: 1, end: 8 } },
|
||||
{ from: { start: 11, end: 12 }, to: { after: 10 } },
|
||||
{ from: { start: 21, end: 21 }, to: { after: 19 } },
|
||||
] },
|
||||
{ path: "b.py", change: "modified" as const, modeOnly: false, binary: false as const, hunks: [{ from: { start: 3, end: 4 }, to: { start: 3, end: 4 } }] },
|
||||
{ path: "c.bin", change: "added" as const, modeOnly: false, binary: true as const },
|
||||
{ path: "empty.txt", change: "added" as const, modeOnly: false, binary: false as const, hunks: [] },
|
||||
{ path: "run.sh", change: "modified" as const, modeOnly: true, binary: false as const, hunks: [] },
|
||||
],
|
||||
diff: "",
|
||||
};
|
||||
const m = material(l, "verification", "v1", "s1-ver-c1", "r1-c1", { changes, artifacts: null });
|
||||
// 手算:a.py 第 6 行归 guard,7–8 行没有归属(实验员自己的说明不算归属);删除点 10 前后(10、11)都没被盖住;
|
||||
// 删除点 19 后一行 20 归 p2;b.py 整个文件归 p2;其余三个文件按整个文件算,位置表都没写到。
|
||||
const c = m.checklist as Extract<TaskMaterial["checklist"], { kind: "verification" }>;
|
||||
assert.deepEqual(c.unassigned, ["a.py:7-8", "a.py:10", "c.bin", "empty.txt", "run.sh"]);
|
||||
has(m, "- a.py:10(删除 2 行(旧版本第 11–12 行))");
|
||||
has(m, "- c.bin(新增(二进制文件,按整个文件算))");
|
||||
has(m, "- run.sh(修改(只改了可执行位,按整个文件算))");
|
||||
has(m, "- a.py:7:实验员说是 import");
|
||||
// 因子与保障同一行:p1 的 1–5 与 guard 的 5–6 在第 5 行。
|
||||
has(m, "- a.py:5:因子 p1、保障 guard");
|
||||
lacks(m, "因子 p2、保障 guard");
|
||||
});
|
||||
|
||||
test("答案路径不在位置表里、读不到,都写明状态", (t) => {
|
||||
const l = studyUpTo(t, "run1");
|
||||
l.append("runPlanned", { run: "run-x", purpose: "forward", round: 1, object: "r1-c2", definition: { command: ["python"], workdir: "/w", image: { digest: `sha256:${"0".repeat(64)}`, runtime: "runc" }, wallSeconds: 120, cores: 1, inheritEnv: false, env: {} } });
|
||||
l.append("runStarted", { run: "run-x" });
|
||||
l.append("runResult", {
|
||||
run: "run-x",
|
||||
process: { ending: "exited", exitCode: 0, cpuSeconds: { status: "measured", value: 1 }, wallSeconds: { status: "measured", value: 1 }, peakMemoryBytes: { status: "measured", value: 1 } },
|
||||
judged: {
|
||||
verdict: "valid", verdictReason: "合法", score: { status: "measured", value: 1.1 }, metrics: {}, extra: null,
|
||||
answerPath: { status: "read", value: "p9" },
|
||||
safeguardTriggers: { status: "unreadable", reason: "没写 safeguard-triggers" },
|
||||
},
|
||||
});
|
||||
l.append("code", { object: "r1-c2", version: "v-c2", locations: { factorLocations: [{ factor: "p1", locations: ["x.py"] }], safeguardLocations: [], unassigned: [] } });
|
||||
const input = { baseline: 0.96, knownBest: 2.635983, epsilon: 0.01, direction: "maximize" as const, candidates: [{ id: "r1-c1", score: 1.2 }] };
|
||||
l.append("decision", { decision: "adopt", round: 1, rule: "test", ruleVersion: 1, params: null, seed: null, input: input as never, output: { line: 0.01, quarter: 0.4, best: { id: "r1-c1", score: 1.2, improvement: 0.24 }, adopted: "r1-c1" } });
|
||||
l.append("stageOpen", { stageId: "s1-expl", stage: "explanation", round: 1, roster: ["a1", "a2"] });
|
||||
const m = material(l, "explanation", "a1", "s1-expl", null, { changes: null, artifacts: {} });
|
||||
const run = queryMaterial(m, "run-x")!;
|
||||
assert.ok(run.includes("答案路径写的是 p9,不在位置表里"));
|
||||
assert.ok(run.includes("保障动手次数读不到:没写 safeguard-triggers"));
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 查询、提示词
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test("查询:材料里的编号查得到;同一账本里材料外的编号查不到", (t) => {
|
||||
const l = studyUpTo(t, "secondOpen");
|
||||
const m = material(l, "secondPrediction", "a2", "s1-sp", "r1-c1", NONE);
|
||||
assert.ok(queryMaterial(m, "r1-a1-1")!.includes("甲的提案正文"));
|
||||
assert.ok(queryMaterial(m, "task")!.includes("填圆任务说明"));
|
||||
assert.equal(queryMaterial(m, "s1-sp"), null);
|
||||
assert.equal(queryMaterial(m, "run-0"), null);
|
||||
assert.equal(queryMaterial(m, "不存在"), null);
|
||||
const impl = material(studyUpTo(t, "implOpen"), "implementation", "x1", "s1-impl-c1", "r1-c1", NONE);
|
||||
assert.equal(queryMaterial(impl, "r1-a1-1"), null);
|
||||
});
|
||||
|
||||
test("提示词:每种提交种类都有任务说明文件,每个角色都有角色提示词", () => {
|
||||
const p = readPrompts(join(import.meta.dirname, "..", "prompts"));
|
||||
assert.deepEqual(Object.keys(p.tasks).sort(), Object.keys(SUBMISSION_KINDS).sort());
|
||||
for (const f of [...Object.values(p.tasks), ...Object.values(p.roles)]) assert.match(f.text, /\S/);
|
||||
});
|
||||
|
||||
test("提示词哈希是文件原始字节的 sha256", (t) => {
|
||||
const dir = tempDir(t);
|
||||
mkdirSync(join(dir, "roles"));
|
||||
mkdirSync(join(dir, "tasks"));
|
||||
const bytes = Buffer.from("x\r\n中", "utf8");
|
||||
for (const r of ["researcher", "experimenter", "verifier"]) writeFileSync(join(dir, "roles", `${r}.md`), bytes);
|
||||
for (const k of Object.keys(SUBMISSION_KINDS)) writeFileSync(join(dir, "tasks", `${k}.md`), bytes);
|
||||
const p = readPrompts(dir);
|
||||
// printf 'x\r\n中' | sha256sum
|
||||
assert.equal(p.roles.researcher.hash, "965da20bc965975b4eae297a49cabf4be539ef62bec455fe932093535de2c593");
|
||||
assert.equal(p.tasks.wrapUp.hash, "965da20bc965975b4eae297a49cabf4be539ef62bec455fe932093535de2c593");
|
||||
assert.equal(p.tasks.wrapUp.text, "x\r\n中");
|
||||
});
|
||||
|
||||
test("第一条消息:任务说明文件在最前,现在交什么在最后并列出提交字段", (t) => {
|
||||
const l = studyUpTo(t, "proposalOpen");
|
||||
const m = material(l, "proposal", "a1", "s1-prop", null, NONE);
|
||||
const p = readPrompts(join(import.meta.dirname, "..", "prompts"));
|
||||
assert.ok(m.firstMessage.startsWith(p.tasks.proposal.text.trimEnd()));
|
||||
const end = m.firstMessage.slice(m.firstMessage.lastIndexOf("# 现在交什么"));
|
||||
for (const f of ["`factors`", "`safeguards`", "`metrics`", "`authorEstimate`", "`body`", "最多交 2 条"]) assert.ok(end.includes(f), f);
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
RunDefinition,
|
||||
SUBMISSION_KINDS,
|
||||
SUBMISSION_SCHEMAS,
|
||||
type Checklist,
|
||||
type Submission,
|
||||
type SubmissionKind,
|
||||
} from "../src/vocab/index.ts";
|
||||
@@ -198,3 +199,10 @@ test("环境变量和指标取值里,不合格式的键被拒收", () => {
|
||||
const badMetrics = { "../x": { status: "measured", value: 1 } };
|
||||
assert.equal(Value.Check(JudgedResult, { ...judged, metrics: badMetrics }), false);
|
||||
});
|
||||
|
||||
test("对号清单:16 种提交种类每种都有一项,不多不少(类型断言,npm run check 时检查)", () => {
|
||||
type Missing = Exclude<SubmissionKind, Checklist["kind"]>;
|
||||
type Extra = Exclude<Checklist["kind"], SubmissionKind>;
|
||||
const none: [Missing, Extra] extends [never, never] ? true : false = true;
|
||||
assert.equal(none, true);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user