eval: 准备正向采纳的真实运行恢复与质量评估
新增单份串行启动入口、首个正向运行杀进程恢复证据、逐候选审阅导出与账目统计;参考只读副本分数对照。456项免费测试通过,尚未启动三份真实验收。同步结构说明与当前工作状态。
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import { mkdirSync, writeFileSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
import {
|
||||
Board,
|
||||
protocolMetrics,
|
||||
type TaskMaterial,
|
||||
} from "../../src/materials/index.ts";
|
||||
import { checkoutVersion, compareVersions } from "../../src/workspace/index.ts";
|
||||
import { candidateHistory, jsonFile } from "../lib/forward.ts";
|
||||
import { ofKind, readLedger } from "../lib/ledger.ts";
|
||||
|
||||
// 任务书16五项质量评估的候选审阅材料;不判路线、不代替全新上下文逐条阅读。
|
||||
// node eval/forward/export.ts <研究目录> <全新绝对输出目录>
|
||||
const [study, output, ...extra] = process.argv.slice(2);
|
||||
if (!study || !output || extra.length || output !== resolve(output))
|
||||
throw new Error(
|
||||
"用法:node eval/forward/export.ts <研究目录> <全新绝对输出目录>",
|
||||
);
|
||||
mkdirSync(output); // 不覆盖以前的审阅材料。
|
||||
const records = readLedger(study);
|
||||
const board = new Board(records);
|
||||
const repo = join(study, "repo.git");
|
||||
const batch = board.decision("batch", 1)!;
|
||||
const baseline = board.code(board.baselineOf(1));
|
||||
const plans = ofKind(records, "runPlanned").filter(
|
||||
(p) => p.content.round === 1 && p.content.purpose === "forward",
|
||||
);
|
||||
const index = [];
|
||||
for (const candidate of batch.output.chosen) {
|
||||
const dir = join(output, candidate);
|
||||
mkdirSync(dir);
|
||||
const history = candidateHistory(records, candidate);
|
||||
const candidatePlans = plans.filter((p) => p.content.object === candidate);
|
||||
const started = candidatePlans.filter((p) =>
|
||||
ofKind(records, "runStarted").some((s) => s.content.run === p.content.run),
|
||||
);
|
||||
const protocol = ofKind(records, "protocol")
|
||||
.filter((p) => p.content.candidate === candidate)
|
||||
.at(-1)!;
|
||||
jsonFile(join(dir, "protocol.json"), {
|
||||
record: protocol,
|
||||
resolvedMetrics: protocolMetrics(board, board.protocol(candidate)),
|
||||
});
|
||||
mkdirSync(join(dir, "baseline"));
|
||||
checkoutVersion(repo, baseline.version, join(dir, "baseline"));
|
||||
for (const attempt of history.attempts) {
|
||||
const path = join(dir, `code-${attempt.code.seq}`);
|
||||
mkdirSync(path);
|
||||
checkoutVersion(repo, attempt.code.content.version, path);
|
||||
jsonFile(
|
||||
join(dir, `changes-${attempt.code.seq}.json`),
|
||||
compareVersions(repo, baseline.version, attempt.code.content.version),
|
||||
);
|
||||
}
|
||||
// 首轮里每个候选只正式跑一个已通过版本;无启动的候选仍导出历史,但不冒称有最终通过代码。
|
||||
const firstRun = started[0];
|
||||
const finalCode =
|
||||
firstRun === undefined
|
||||
? null
|
||||
: ofKind(records, "code")
|
||||
.filter((c) => c.content.object === candidate && c.seq < firstRun.seq)
|
||||
.at(-1)!;
|
||||
if (finalCode !== null) {
|
||||
mkdirSync(join(dir, "final"));
|
||||
checkoutVersion(repo, finalCode.content.version, join(dir, "final"));
|
||||
}
|
||||
const finalAttempt =
|
||||
finalCode === null
|
||||
? null
|
||||
: history.attempts.find((a) => a.code.seq === finalCode.seq)!;
|
||||
jsonFile(join(dir, "final-implementation.json"), finalAttempt);
|
||||
jsonFile(join(dir, "attempts.json"), history.attempts);
|
||||
jsonFile(join(dir, "verifications.json"), history.verifications);
|
||||
const calls = ofKind(records, "callStarted").filter((c) => {
|
||||
const material = c.content.materials as TaskMaterial;
|
||||
return (
|
||||
material.object === candidate &&
|
||||
["implementation", "repair", "verification"].includes(material.kind)
|
||||
);
|
||||
});
|
||||
jsonFile(
|
||||
join(dir, "calls.json"),
|
||||
calls.map((c) => ({
|
||||
record: c,
|
||||
sessions: join(study, "calls", c.content.call, "sessions"),
|
||||
})),
|
||||
);
|
||||
jsonFile(
|
||||
join(dir, "rejections.json"),
|
||||
ofKind(records, "rejection").filter(
|
||||
(r) =>
|
||||
r.content.target === candidate &&
|
||||
["implementation", "repair", "verification"].includes(r.content.kind),
|
||||
),
|
||||
);
|
||||
const manifest = {
|
||||
study,
|
||||
candidate,
|
||||
protocolSeq: protocol.seq,
|
||||
baselineVersion: baseline.version,
|
||||
finalCode,
|
||||
ran: started.length > 0,
|
||||
runs: candidatePlans.map((p) => ({
|
||||
plan: p,
|
||||
result:
|
||||
ofKind(records, "runResult").find(
|
||||
(r) => r.content.run === p.content.run,
|
||||
) ?? null,
|
||||
artifacts: join(study, "runs", p.content.run),
|
||||
})),
|
||||
};
|
||||
jsonFile(join(dir, "manifest.json"), manifest);
|
||||
writeFileSync(
|
||||
join(dir, "review.txt"),
|
||||
`任务书16第3.2节:只读本候选,分两步评估,中文直接回复,不写文件。\n` +
|
||||
`第一步只读 protocol.json(冻结协议全文及引用指标原定义)、baseline/、final/、final-implementation.json(正文、位置表、concerns)。\n` +
|
||||
`逐条实现条款、因子、保障检查漏做、协议之外的改进、位置表是否准确,每条附代码文件和行号。先形成独立结论,再打开核验历史。\n` +
|
||||
`第二步读 verifications.json、attempts.json、code-<序号>/、changes-<序号>.json;每次核验对照其中code指向的那一版,不拿最终修好版本倒判历史。判断该拒却通过、不该拒却拒绝;修复是否只改指出处。\n` +
|
||||
`需要时读 calls.json 和 rejections.json;模型材料和原始会话都可追溯。不把提交格式退回当成核验失败。\n` +
|
||||
`答案路径只核对每个可能交出最终答案分支的标签,不预测实际走哪个分支。报告先写实现忠实度,再写逐次核验误判和修复范围。\n` +
|
||||
`按任务书15附录一路线代号归类冻结协议,另标是否完整高上限路线(起点、连续改进全部圆、多起点或扰动、合法收尾),附协议原文位置。\n` +
|
||||
`本候选${manifest.ran ? "已启动正式运行" : "未启动正式运行;没有final/,只评历史,不列成跑过的候选"}。\n`,
|
||||
);
|
||||
index.push({
|
||||
candidate,
|
||||
ran: manifest.ran,
|
||||
directory: dir,
|
||||
finalVersion: finalCode?.content.version ?? null,
|
||||
});
|
||||
}
|
||||
jsonFile(join(output, "index.json"), index);
|
||||
console.log(`候选审阅包:${output}`);
|
||||
@@ -0,0 +1,259 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { appendFileSync, existsSync, mkdirSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
import { forwardAccounts, frozenConfig, jsonFile } from "../lib/forward.ts";
|
||||
import { ofKind, readLedger } from "../lib/ledger.ts";
|
||||
import { launch, sleep, waitForLedger, writeConfig } from "../lib/study.ts";
|
||||
import { watchTimeline } from "../lib/timeline.ts";
|
||||
|
||||
// 任务书16:一次只启动一份;三次由父会话逐个执行。kill 只打首个正向运行,随后恢复同一研究一次。
|
||||
// node eval/forward/run.ts <new|kill|resume> <研究编号> <绝对输出目录>
|
||||
const [mode, name, output, ...extra] = process.argv.slice(2);
|
||||
if (
|
||||
!mode ||
|
||||
!name ||
|
||||
!output ||
|
||||
extra.length ||
|
||||
!["new", "kill", "resume"].includes(mode) ||
|
||||
output !== resolve(output)
|
||||
) {
|
||||
throw new Error(
|
||||
"用法:node eval/forward/run.ts <new|kill|resume> <研究编号> <绝对输出目录>",
|
||||
);
|
||||
}
|
||||
const out = output;
|
||||
const studyDir = join(out, "study");
|
||||
if (mode === "resume") {
|
||||
if (frozenConfig(readLedger(studyDir)).studyId !== name)
|
||||
throw new Error("研究编号与账本不符");
|
||||
} else {
|
||||
if (existsSync(out))
|
||||
throw new Error(`输出目录已存在,不覆盖现场:${out}`);
|
||||
mkdirSync(out, { recursive: true });
|
||||
}
|
||||
const config =
|
||||
mode === "resume"
|
||||
? null
|
||||
: writeConfig(
|
||||
"eval/round-zero/research.json",
|
||||
join(out, "research.json"),
|
||||
name,
|
||||
null,
|
||||
);
|
||||
const inspect = (container: string) =>
|
||||
JSON.parse(
|
||||
execFileSync("docker", ["inspect", container], {
|
||||
encoding: "utf8",
|
||||
}),
|
||||
)[0] as {
|
||||
Id: string;
|
||||
Name: string;
|
||||
Created: string;
|
||||
State: {
|
||||
Running: boolean;
|
||||
StartedAt: string;
|
||||
FinishedAt: string;
|
||||
Status: string;
|
||||
};
|
||||
};
|
||||
function observe(label: string) {
|
||||
appendFileSync(
|
||||
join(out, "machine.jsonl"),
|
||||
`${JSON.stringify({
|
||||
at: new Date().toISOString(),
|
||||
label,
|
||||
processes: execFileSync(
|
||||
"ps",
|
||||
["-eo", "pid,ppid,lstart,args"],
|
||||
{ encoding: "utf8" },
|
||||
),
|
||||
containers: execFileSync(
|
||||
"docker",
|
||||
["ps", "--no-trunc"],
|
||||
{ encoding: "utf8" },
|
||||
),
|
||||
})}\n`,
|
||||
);
|
||||
}
|
||||
function start(path: string | null) {
|
||||
observe(path === null ? "resume" : "start");
|
||||
const study = launch(path, studyDir, {});
|
||||
appendFileSync(
|
||||
join(out, "launches.jsonl"),
|
||||
`${JSON.stringify({ event: "start", pid: study.child.pid, at: new Date().toISOString(), config: path })}\n`,
|
||||
);
|
||||
const interrupt = () => study.child.kill("SIGINT");
|
||||
process.on("SIGINT", interrupt);
|
||||
study.done.then((end) => {
|
||||
process.off("SIGINT", interrupt);
|
||||
appendFileSync(
|
||||
join(out, "launches.jsonl"),
|
||||
`${JSON.stringify({ event: "end", pid: study.child.pid, at: new Date().toISOString(), ...end })}\n`,
|
||||
);
|
||||
});
|
||||
return study;
|
||||
}
|
||||
let active = start(config);
|
||||
try {
|
||||
await waitForLedger(studyDir, active.child);
|
||||
let watching = watchTimeline(
|
||||
studyDir,
|
||||
active.child,
|
||||
join(out, "timeline.jsonl"),
|
||||
);
|
||||
if (mode === "kill") {
|
||||
let hit = false;
|
||||
while (
|
||||
active.child.exitCode === null &&
|
||||
active.child.signalCode === null
|
||||
) {
|
||||
const records = readLedger(studyDir);
|
||||
const plan = ofKind(records, "runPlanned").find(
|
||||
(r) =>
|
||||
r.content.purpose === "forward" &&
|
||||
r.content.round === 1,
|
||||
);
|
||||
if (plan !== undefined) {
|
||||
const run = plan.content.run;
|
||||
if (
|
||||
ofKind(records, "runResult").some(
|
||||
(r) => r.content.run === run,
|
||||
)
|
||||
)
|
||||
throw new Error(
|
||||
"首个正向运行已经结束,未打中;保留现场,不改杀别的候选",
|
||||
);
|
||||
const started = ofKind(
|
||||
records,
|
||||
"runStarted",
|
||||
).find((r) => r.content.run === run);
|
||||
if (started !== undefined) {
|
||||
// 容器名是正式入口记录在结构文档里的约定,不 import flow 内部文件。
|
||||
const container = `inforsi-${name}-${run}`;
|
||||
const before = inspect(container);
|
||||
jsonFile(
|
||||
join(
|
||||
out,
|
||||
"container-before.json",
|
||||
),
|
||||
before,
|
||||
);
|
||||
if (!before.State.Running)
|
||||
throw new Error(
|
||||
"首个正向容器已不在运行,未打中",
|
||||
);
|
||||
if (
|
||||
ofKind(
|
||||
readLedger(studyDir),
|
||||
"runResult",
|
||||
).some(
|
||||
(r) =>
|
||||
r.content
|
||||
.run ===
|
||||
run,
|
||||
)
|
||||
)
|
||||
throw new Error(
|
||||
"杀前已有结果,未打中",
|
||||
);
|
||||
jsonFile(join(out, "kill.json"), {
|
||||
run,
|
||||
candidate: plan.content.object,
|
||||
planSeq: plan.seq,
|
||||
startSeq: started.seq,
|
||||
beforeKillSeq:
|
||||
records.at(-1)!.seq,
|
||||
pid: active.child.pid,
|
||||
at: new Date().toISOString(),
|
||||
container,
|
||||
});
|
||||
if (!active.child.kill("SIGKILL"))
|
||||
throw new Error(
|
||||
"未发出 SIGKILL",
|
||||
);
|
||||
const killed = await active.done;
|
||||
await watching;
|
||||
if (killed.signal !== "SIGKILL")
|
||||
throw new Error(
|
||||
"控制进程不是以 SIGKILL 结束",
|
||||
);
|
||||
const atKill = readLedger(studyDir);
|
||||
jsonFile(
|
||||
join(
|
||||
out,
|
||||
"ledger-at-kill.json",
|
||||
),
|
||||
atKill,
|
||||
);
|
||||
if (
|
||||
ofKind(
|
||||
atKill,
|
||||
"runResult",
|
||||
).some(
|
||||
(r) =>
|
||||
r.content
|
||||
.run ===
|
||||
run,
|
||||
)
|
||||
)
|
||||
throw new Error(
|
||||
"杀后核对发现目标运行已经写结果,未打中;保留现场",
|
||||
);
|
||||
const afterKill = inspect(container);
|
||||
jsonFile(
|
||||
join(
|
||||
out,
|
||||
"container-after-kill.json",
|
||||
),
|
||||
afterKill,
|
||||
);
|
||||
observe("after-kill");
|
||||
active = start(null);
|
||||
const afterResume = inspect(container);
|
||||
jsonFile(
|
||||
join(
|
||||
out,
|
||||
"container-after-resume.json",
|
||||
),
|
||||
afterResume,
|
||||
);
|
||||
watching = watchTimeline(
|
||||
studyDir,
|
||||
active.child,
|
||||
join(out, "timeline.jsonl"),
|
||||
);
|
||||
hit = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
await sleep(100);
|
||||
}
|
||||
if (!hit)
|
||||
throw new Error(
|
||||
`没有打中首个正向运行:${JSON.stringify(await active.done)}`,
|
||||
);
|
||||
}
|
||||
await watching;
|
||||
const end = await active.done;
|
||||
jsonFile(join(out, "outcome.json"), end);
|
||||
observe("end");
|
||||
const accounts = forwardAccounts(readLedger(studyDir));
|
||||
jsonFile(join(out, "accounts.json"), accounts);
|
||||
// 杀进程报告由 stats 读原始容器观察和账本重算;即使外部服务失败后人工恢复,也不覆盖原杀进程证据。
|
||||
if (end.code !== 0 || !Object.values(accounts.checks).every(Boolean)) {
|
||||
throw new Error(
|
||||
`没有完成采纳验收,保留现场;查明原因后才能手动 resume:${JSON.stringify(end)}`,
|
||||
);
|
||||
}
|
||||
console.log(
|
||||
`已到采纳后停点:${studyDir}。仍需 export、stats 和独立质量审阅。`,
|
||||
);
|
||||
} finally {
|
||||
if (
|
||||
active.child.exitCode === null &&
|
||||
active.child.signalCode === null
|
||||
) {
|
||||
active.child.kill("SIGINT");
|
||||
await active.done;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
||||
import { join, resolve } from "node:path";
|
||||
import type { LedgerRecord } from "../../src/ledger/index.ts";
|
||||
import {
|
||||
Board,
|
||||
validScore,
|
||||
type TaskMaterial,
|
||||
} from "../../src/materials/index.ts";
|
||||
import {
|
||||
forwardAccounts,
|
||||
frozenConfig,
|
||||
jsonFile,
|
||||
scoreBand,
|
||||
} from "../lib/forward.ts";
|
||||
import { finalSubmissions, ofKind, readLedger } from "../lib/ledger.ts";
|
||||
import { lightCheckUsage } from "../lib/light-checks.ts";
|
||||
import { referenceScores } from "../lib/reference-scores.ts";
|
||||
|
||||
// 五项表中可计算的字段。路线与审阅结论故意不自动猜,由父会话按候选审阅包填写报告。
|
||||
// node eval/forward/stats.ts <全新绝对输出目录> <五份参考副本根目录> <检验目录>...
|
||||
const [output, referenceRoot, ...dirs] = process.argv.slice(2);
|
||||
if (
|
||||
!output ||
|
||||
!referenceRoot ||
|
||||
dirs.length === 0 ||
|
||||
output !== resolve(output)
|
||||
)
|
||||
throw new Error(
|
||||
"用法:node eval/forward/stats.ts <全新绝对输出目录> <参考副本根目录> <检验目录>...",
|
||||
);
|
||||
mkdirSync(output);
|
||||
const summaries = dirs.map((dir) => {
|
||||
const studyDir = join(dir, "study");
|
||||
const records = readLedger(studyDir);
|
||||
const board = new Board(records);
|
||||
const config = frozenConfig(records);
|
||||
const accounts = forwardAccounts(records);
|
||||
const batch = board.decision("batch", 1)!;
|
||||
const finals = finalSubmissions(records);
|
||||
const scores = accounts.candidates.map((c) => {
|
||||
const p = board.protocol(c.candidate);
|
||||
const merged = batch.output.candidates.find(
|
||||
(b) => b.id === c.candidate,
|
||||
)!.merged;
|
||||
const secondPredictions = finals.flatMap((s) =>
|
||||
s.content.kind === "secondPrediction" &&
|
||||
s.content.target === c.candidate &&
|
||||
board.stage(s.content.stageId).round === 1
|
||||
? [
|
||||
{
|
||||
researcher: s.content.author,
|
||||
q5: s.content.content.range.q5,
|
||||
seq: s.seq,
|
||||
},
|
||||
]
|
||||
: [],
|
||||
);
|
||||
const authorEstimates = p.proposals.map((id) => {
|
||||
const proposal = finals.find(
|
||||
(s) => s.content.kind === "proposal" && s.content.item === id,
|
||||
)!;
|
||||
const seal = ofKind(records, "seal").find(
|
||||
(s) => s.content.proposal === proposal.seq,
|
||||
)!;
|
||||
return {
|
||||
proposal: id,
|
||||
author: proposal.content.author,
|
||||
range: seal.content.range,
|
||||
proposalSeq: proposal.seq,
|
||||
sealSeq: seal.seq,
|
||||
};
|
||||
});
|
||||
const runs = c.runs.map((r) => {
|
||||
const result = r.results[0] ?? null;
|
||||
const score = validScore(result?.content.judged ?? null);
|
||||
return {
|
||||
run: r.plan.content.run,
|
||||
planSeq: r.plan.seq,
|
||||
started: r.starts.length > 0,
|
||||
result,
|
||||
score,
|
||||
band: scoreBand(score, merged),
|
||||
measuredMinusQ5: score === null ? null : score - merged.q5,
|
||||
};
|
||||
});
|
||||
return {
|
||||
study: config.studyId,
|
||||
candidate: c.candidate,
|
||||
name: p.factors.map((f) => f.name).join(";"),
|
||||
merged,
|
||||
secondPredictions,
|
||||
authorEstimates,
|
||||
runs,
|
||||
adopted: board.decision("adopt", 1)?.output.adopted === c.candidate,
|
||||
verificationCount: c.verificationCount,
|
||||
repairCount: c.repairCount,
|
||||
started: c.started,
|
||||
manualRequired:
|
||||
"路线按冻结协议归类;完整高上限路线单列 measuredMinusQ5;忠实度及误判见独立审阅回复",
|
||||
};
|
||||
});
|
||||
const lightChecks = ofKind(records, "callStarted")
|
||||
.filter((c) =>
|
||||
["implementation", "repair"].includes(
|
||||
(c.content.materials as TaskMaterial).kind,
|
||||
),
|
||||
)
|
||||
.map((c) => ({
|
||||
call: c.content.call,
|
||||
seq: c.seq,
|
||||
...lightCheckUsage(
|
||||
join(studyDir, "calls", c.content.call, "sessions"),
|
||||
config.lightChecks.maxCount,
|
||||
),
|
||||
}));
|
||||
let kill = null;
|
||||
if (existsSync(join(dir, "kill.json"))) {
|
||||
const evidence = JSON.parse(
|
||||
readFileSync(join(dir, "kill.json"), "utf8"),
|
||||
) as { run: string; candidate: string; beforeKillSeq: number };
|
||||
type Container = {
|
||||
Id: string;
|
||||
State: { StartedAt: string; Running: boolean };
|
||||
};
|
||||
const snapshots = ["before", "after-kill", "after-resume"].map(
|
||||
(part) =>
|
||||
JSON.parse(
|
||||
readFileSync(join(dir, `container-${part}.json`), "utf8"),
|
||||
) as Container,
|
||||
);
|
||||
const target = accounts.runs.find(
|
||||
(r) => r.plan.content.run === evidence.run,
|
||||
)!;
|
||||
const candidatePlans = accounts.runs.filter(
|
||||
(r) =>
|
||||
r.plan.content.purpose === "forward" &&
|
||||
r.plan.content.object === evidence.candidate,
|
||||
);
|
||||
const atKill = JSON.parse(
|
||||
readFileSync(join(dir, "ledger-at-kill.json"), "utf8"),
|
||||
) as LedgerRecord[];
|
||||
const killSeq = atKill.at(-1)!.seq;
|
||||
kill = {
|
||||
evidence,
|
||||
killSeq,
|
||||
snapshots,
|
||||
charged: target.starts.length,
|
||||
resultSeq: target.results[0]?.seq ?? null,
|
||||
checks: {
|
||||
sameContainer:
|
||||
snapshots[0]!.State.Running &&
|
||||
snapshots.every(
|
||||
(c) =>
|
||||
c.Id === snapshots[0]!.Id &&
|
||||
c.State.StartedAt === snapshots[0]!.State.StartedAt,
|
||||
),
|
||||
chargedOnceNoRerun:
|
||||
target.starts.length === 1 && candidatePlans.length === 1,
|
||||
resultAndContinued:
|
||||
!ofKind(atKill, "runResult").some(
|
||||
(r) => r.content.run === evidence.run,
|
||||
) &&
|
||||
target.results.length === 1 &&
|
||||
target.results[0]!.seq > killSeq &&
|
||||
accounts.checks.adoptedAndStopped,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
dir,
|
||||
study: config.studyId,
|
||||
scores,
|
||||
accounts,
|
||||
lightChecks,
|
||||
kill,
|
||||
pass:
|
||||
Object.values(accounts.checks).every(Boolean) &&
|
||||
lightChecks.every((c) => c.pass) &&
|
||||
(kill === null || Object.values(kill.checks).every(Boolean)),
|
||||
};
|
||||
});
|
||||
const references = [
|
||||
"16-real-1",
|
||||
"15-real-1",
|
||||
"15-tight-2",
|
||||
"14-real-1",
|
||||
"12-real-2",
|
||||
].flatMap((name) => referenceScores(join(referenceRoot, name, "board.sqlite")));
|
||||
jsonFile(join(output, "stats.json"), summaries);
|
||||
jsonFile(
|
||||
join(output, "predictions-scores.json"),
|
||||
summaries.flatMap((s) => s.scores),
|
||||
);
|
||||
jsonFile(join(output, "reference-scores.json"), references);
|
||||
jsonFile(join(output, "score-comparison.json"), {
|
||||
references,
|
||||
ours: summaries.flatMap((s) => s.scores),
|
||||
caveat:
|
||||
"参考项目提示词带题目具体做法、方法版本也不同;保留无效成绩及重复尝试,不作公平因果比较",
|
||||
});
|
||||
jsonFile(join(output, "manual-review.json"), {
|
||||
implementation:
|
||||
"每个已启动候选一个全新上下文:协议逐条、基线、最终代码、concerns、位置表;逐项漏做/额外改进/位置表准确性和代码位置",
|
||||
verification:
|
||||
"同一审阅者再读各次核验及对应版本:误放行/误拒、修复是否仅改指出处;保存原始回复",
|
||||
route:
|
||||
"逐候选按任务书15附录一路线归类,并单列完整高上限路线的实测-q5;不自动按关键词贴标签",
|
||||
comparison:
|
||||
"复用eval/route导出、items、thinking、verify-ranges、原四问Jev及原阅读问卷,填A–J;与任务15三次历史结果并排,标历史批次",
|
||||
serial:
|
||||
"核对各目录launches.jsonl、machine.jsonl和父会话观察;上一份全部结束后才起下一份;不宣称快照排除任意外部进程",
|
||||
});
|
||||
console.log(
|
||||
`统计:${output};程序检查${summaries.every((s) => s.pass) ? "通过" : "未通过"};质量判断仍待独立阅读。`,
|
||||
);
|
||||
if (!summaries.every((s) => s.pass)) process.exitCode = 1;
|
||||
@@ -0,0 +1,171 @@
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { checkConfig } from "../../src/config/index.ts";
|
||||
import type { LedgerRecord } from "../../src/ledger/index.ts";
|
||||
import {
|
||||
Board,
|
||||
FORWARD_NOT_RUN,
|
||||
type StudyRecord,
|
||||
} from "../../src/materials/index.ts";
|
||||
import { placement, runBalance } from "../../src/rules/index.ts";
|
||||
import { verificationOutcome } from "../../src/submissions/index.ts";
|
||||
import type { Range } from "../../src/vocab/index.ts";
|
||||
import { finalSubmissions, ofKind } from "./ledger.ts";
|
||||
|
||||
export const FORWARD_STOP = "消融还没写(任务书 17)";
|
||||
export const jsonFile = (path: string, value: unknown): void =>
|
||||
writeFileSync(path, `${JSON.stringify(value, null, 2)}\n`);
|
||||
export const frozenConfig = (records: LedgerRecord[]) =>
|
||||
checkConfig((ofKind(records, "config")[0]!.content as StudyRecord).config);
|
||||
|
||||
/** 区间边界只是报告口径,不参与研究决定;失败或测不到分数保持 null。 */
|
||||
export function scoreBand(score: number | null, range: Range): string | null {
|
||||
if (score === null) return null;
|
||||
return {
|
||||
belowQ1: "<q1",
|
||||
q1ToQ5: "[q1,q5)",
|
||||
q5ToQ9: "[q5,q9]",
|
||||
aboveQ9: ">q9",
|
||||
}[placement(range, score)];
|
||||
}
|
||||
|
||||
/** 核验要对应当时那版代码,不能拿最终修好后的代码替代历史被审代码。 */
|
||||
export function candidateHistory(records: LedgerRecord[], candidate: string) {
|
||||
const code = ofKind(records, "code").filter(
|
||||
(r) => r.content.object === candidate,
|
||||
);
|
||||
const submissions = ofKind(records, "submission").filter(
|
||||
(r) => r.content.target === candidate,
|
||||
);
|
||||
const attempts = submissions.flatMap((r) => {
|
||||
if (r.content.kind !== "implementation" && r.content.kind !== "repair")
|
||||
return [];
|
||||
// flow 在同一事务里先写 submission,紧接着写 code。
|
||||
return [{ submission: r, code: code.find((c) => c.seq === r.seq + 1)! }];
|
||||
});
|
||||
const verifications = submissions.flatMap((r) =>
|
||||
r.content.kind === "verification"
|
||||
? [
|
||||
{
|
||||
submission: r,
|
||||
code: code.filter((c) => c.seq < r.seq).at(-1)!,
|
||||
outcome: verificationOutcome(r.content.content),
|
||||
},
|
||||
]
|
||||
: [],
|
||||
);
|
||||
return { attempts, verifications };
|
||||
}
|
||||
|
||||
/** 程序验收只读记录重算;基线也算正式运行,没启动的结果不扣次。 */
|
||||
export function forwardAccounts(records: LedgerRecord[]) {
|
||||
const board = new Board(records);
|
||||
const config = frozenConfig(records);
|
||||
const opens = ofKind(records, "stageOpen");
|
||||
const closes = ofKind(records, "stageClose");
|
||||
const stages = opens.map((r) => {
|
||||
const closing = closes.filter(
|
||||
(c) => c.content.stageId === r.content.stageId,
|
||||
);
|
||||
return {
|
||||
...r.content,
|
||||
openSeq: r.seq,
|
||||
closeSeqs: closing.map((c) => c.seq),
|
||||
pass: closing.length === 1 && closing[0]!.seq > r.seq,
|
||||
};
|
||||
});
|
||||
const orphanCloses = closes.filter(
|
||||
(c) => !opens.some((o) => o.content.stageId === c.content.stageId),
|
||||
);
|
||||
const started = ofKind(records, "runStarted");
|
||||
const balance = runBalance(config.runs, board.runStates());
|
||||
const runs = ofKind(records, "runPlanned").map((p) => ({
|
||||
plan: p,
|
||||
starts: started.filter((s) => s.content.run === p.content.run),
|
||||
results: ofKind(records, "runResult").filter(
|
||||
(r) => r.content.run === p.content.run,
|
||||
),
|
||||
}));
|
||||
const batch = board.decision("batch", 1);
|
||||
const finals = finalSubmissions(records);
|
||||
const candidates = (batch?.output.chosen ?? []).map((candidate) => {
|
||||
const history = candidateHistory(records, candidate);
|
||||
const events = ofKind(records, "event").filter(
|
||||
(r) =>
|
||||
r.content.event === FORWARD_NOT_RUN &&
|
||||
r.content.objects.includes(candidate),
|
||||
);
|
||||
const candidateRuns = runs.filter(
|
||||
(r) =>
|
||||
r.plan.content.purpose === "forward" &&
|
||||
r.plan.content.round === 1 &&
|
||||
r.plan.content.object === candidate,
|
||||
);
|
||||
const verificationCount = finals.filter(
|
||||
(r) =>
|
||||
r.content.target === candidate && r.content.kind === "verification",
|
||||
).length;
|
||||
const repairCount = finals.filter(
|
||||
(r) => r.content.target === candidate && r.content.kind === "repair",
|
||||
).length;
|
||||
const lastPassed = history.verifications.at(-1)?.outcome.passed ?? null;
|
||||
const exhausted = lastPassed === false && repairCount >= config.maxRepairs;
|
||||
return {
|
||||
candidate,
|
||||
verificationCount,
|
||||
repairCount,
|
||||
lastPassed,
|
||||
exhausted,
|
||||
notRunEvents: events,
|
||||
started: candidateRuns.some((r) => r.starts.length > 0),
|
||||
runs: candidateRuns,
|
||||
pass: exhausted
|
||||
? events.length === 1 && candidateRuns.length === 0
|
||||
: lastPassed === true &&
|
||||
candidateRuns.some(
|
||||
(r) => r.starts.length === 1 && r.results.length === 1,
|
||||
),
|
||||
};
|
||||
});
|
||||
const adoption =
|
||||
ofKind(records, "decision").find(
|
||||
(r) => r.content.decision === "adopt" && r.content.round === 1,
|
||||
) ?? null;
|
||||
const halt =
|
||||
ofKind(records, "event")
|
||||
.filter((r) => r.content.event === "halted")
|
||||
.at(-1) ?? null;
|
||||
const checks = {
|
||||
stagesPaired: stages.every((s) => s.pass) && orphanCloses.length === 0,
|
||||
chargedOnce:
|
||||
balance.charged === started.length &&
|
||||
config.runs - balance.balance === started.length &&
|
||||
balance.reserved === 0,
|
||||
forwardSerial: runs
|
||||
.filter((r) => r.plan.content.purpose === "forward")
|
||||
.every(
|
||||
(r, i, forwards) =>
|
||||
i === 0 ||
|
||||
(forwards[i - 1]!.results.length === 1 &&
|
||||
forwards[i - 1]!.results[0]!.seq < r.plan.seq),
|
||||
),
|
||||
runsFinished: runs.length > 0 && runs.every((r) => r.results.length === 1),
|
||||
candidatesFinished:
|
||||
candidates.length > 0 && candidates.every((c) => c.pass),
|
||||
adoptedAndStopped:
|
||||
adoption !== null &&
|
||||
halt?.content.reason === FORWARD_STOP &&
|
||||
halt.seq === records.at(-1)!.seq,
|
||||
};
|
||||
return {
|
||||
initial: config.runs,
|
||||
confirmedStarts: started.length,
|
||||
balance,
|
||||
runs,
|
||||
candidates,
|
||||
stages,
|
||||
orphanCloses,
|
||||
adoption,
|
||||
halt,
|
||||
checks,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { readdirSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
|
||||
/** Pi 会话里的 research_check:请求和结果按 toolCallId 配对,不把超限请求算成执行。 */
|
||||
export function lightCheckUsage(sessionDir: string, maxCount: number) {
|
||||
type Part = { type: string; id?: string; name?: string; text?: string };
|
||||
type Message = {
|
||||
role: string;
|
||||
toolCallId?: string;
|
||||
toolName?: string;
|
||||
isError?: boolean;
|
||||
content: Part[];
|
||||
};
|
||||
const requests: Array<{ id: string; file: string; line: number }> = [];
|
||||
const results = new Map<
|
||||
string,
|
||||
{ file: string; line: number; message: Message }
|
||||
>();
|
||||
const files = readdirSync(sessionDir)
|
||||
.filter((f) => f.endsWith(".jsonl"))
|
||||
.sort();
|
||||
if (files.length === 0) throw new Error(`没有会话文件:${sessionDir}`);
|
||||
for (const file of files) {
|
||||
readFileSync(join(sessionDir, file), "utf8")
|
||||
.split("\n")
|
||||
.forEach((line, index) => {
|
||||
if (!line.trim()) return;
|
||||
const entry = JSON.parse(line) as { type: string; message?: Message };
|
||||
const m = entry.message;
|
||||
if (entry.type !== "message" || m === undefined) return;
|
||||
if (m.role === "assistant") {
|
||||
for (const p of m.content)
|
||||
if (p.type === "toolCall" && p.name === "research_check")
|
||||
requests.push({ id: p.id!, file, line: index + 1 });
|
||||
} else if (m.role === "toolResult" && m.toolName === "research_check")
|
||||
results.set(m.toolCallId!, { file, line: index + 1, message: m });
|
||||
});
|
||||
}
|
||||
const checks = requests.map((request) => {
|
||||
const result = results.get(request.id);
|
||||
if (result === undefined)
|
||||
return { request, result: null, status: "missingResult", used: null };
|
||||
const text = result.message.content
|
||||
.filter((p) => p.type === "text")
|
||||
.map((p) => p.text)
|
||||
.join("\n");
|
||||
const source = { file: result.file, line: result.line, text };
|
||||
if (result.message.isError)
|
||||
return { request, result: source, status: "toolError", used: null };
|
||||
if (text.startsWith("本次调用的轻量检查已用完"))
|
||||
return { request, result: source, status: "deniedLimit", used: null };
|
||||
const value = JSON.parse(text) as {
|
||||
used: number;
|
||||
maxCount: number;
|
||||
seconds: number;
|
||||
result: unknown;
|
||||
};
|
||||
return { request, result: source, status: "executed", used: value.used };
|
||||
});
|
||||
const used = checks.flatMap((c) => (c.used === null ? [] : [c.used]));
|
||||
const unresolved = checks.some(
|
||||
(c) => c.status === "missingResult" || c.status === "toolError",
|
||||
);
|
||||
return {
|
||||
sessionDir,
|
||||
maxCount,
|
||||
requested: checks.length,
|
||||
executed: used.length,
|
||||
deniedLimit: checks.filter((c) => c.status === "deniedLimit").length,
|
||||
// 工具报错或缺结果时不能猜实际执行量,也不当作 0 次通过。
|
||||
pass:
|
||||
!unresolved &&
|
||||
used.length <= maxCount &&
|
||||
used.every((n, i) => n === i + 1),
|
||||
checks,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { basename, dirname } from "node:path";
|
||||
import { DatabaseSync } from "node:sqlite";
|
||||
|
||||
/** 只读已有副本;保留 incomplete 的数值及重复尝试,不冒充有效成绩。 */
|
||||
export function referenceScores(path: string) {
|
||||
const study = basename(dirname(path));
|
||||
const db = new DatabaseSync(path, { readOnly: true });
|
||||
try {
|
||||
type Fact<T> = { seq: number; content: T };
|
||||
const facts = <T>(kind: string): Fact<T>[] =>
|
||||
(
|
||||
db
|
||||
.prepare(
|
||||
"SELECT created_seq, payload FROM facts WHERE kind = ? ORDER BY created_seq",
|
||||
)
|
||||
.all(kind) as Array<{ created_seq: number; payload: string }>
|
||||
).map((r) => ({
|
||||
seq: r.created_seq,
|
||||
content: JSON.parse(r.payload) as T,
|
||||
}));
|
||||
const links = facts<{
|
||||
purpose: string;
|
||||
roundId: string;
|
||||
runId: string;
|
||||
candidateId: string;
|
||||
}>("run-link");
|
||||
const observations = facts<{
|
||||
runId: string;
|
||||
status: string;
|
||||
finalMetric: { value: number | null };
|
||||
reasons: string[];
|
||||
}>("run-observation");
|
||||
const adoption = facts<{
|
||||
roundId: string;
|
||||
adoptedRunId: string | null;
|
||||
newBaselineId: string;
|
||||
}>("adoption").find((r) => r.content.roundId === `${study}.r1`)!;
|
||||
return links
|
||||
.filter(
|
||||
(r) =>
|
||||
r.content.purpose === "forward" &&
|
||||
r.content.roundId === `${study}.r1`,
|
||||
)
|
||||
.map((link) => {
|
||||
const observation = observations.find(
|
||||
(r) => r.content.runId === link.content.runId,
|
||||
)!;
|
||||
return {
|
||||
source: path,
|
||||
study,
|
||||
candidate: link.content.candidateId,
|
||||
run: link.content.runId,
|
||||
status: observation.content.status,
|
||||
rawScore: observation.content.finalMetric.value,
|
||||
validScore:
|
||||
observation.content.status === "valid"
|
||||
? observation.content.finalMetric.value
|
||||
: null,
|
||||
adopted: adoption.content.adoptedRunId === link.content.runId,
|
||||
newBaseline: adoption.content.newBaselineId,
|
||||
linkSeq: link.seq,
|
||||
observationSeq: observation.seq,
|
||||
adoptionSeq: adoption.seq,
|
||||
reasons: observation.content.reasons,
|
||||
};
|
||||
});
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user