feat: 工具链与 vocab 模块(任务书 01)

- npm 包、TypeScript、node --test、dependency-cruiser;结构.md 第六部分的检查规则一次写全,临时违规实测均报错。
- vocab:基础类型与编号格式、规范序列化与哈希、角色/阶段/提交名单、指标与运行定义及结果、模型用量、16 种提交的 TypeBox 字段定义。
- 测试:规范序列化预期串手写,哈希预期值用 sha256sum 算出;每种提交一份合法样例。
- 结构.md 第八部分补 vocab 对外内容;WORK_LOG 记下用户决定。

用户决定(2026-10-01):"同意 A,vocab 上限改为 1100 行";"同意 A:解释的 cells 去掉 minItems,改为可选";"3、4、5、6:同意保留你的写法";答案路径与保障动手次数"tasks 只记读到的原始内容",对表由以后有位置表的模块现算。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 23:37:58 -04:00
parent 30e4d98ed2
commit 97a43591a3
13 changed files with 2082 additions and 3 deletions
+260
View File
@@ -0,0 +1,260 @@
import { Type, type Static } from "typebox";
import { DIRECTIONS, Id, Measured, type Json } from "./basics.ts";
/** 不认识的字段、不合格式的键一律不收。 */
const CLOSED = { additionalProperties: false } as const;
const REASON = Type.String({ minLength: 1 });
// ---------------------------------------------------------------------------
// 指标(方法.md 第五部分"指标")
// ---------------------------------------------------------------------------
/** 指标层次:最终指标由任务规定;常规辅助指标由程序统一采集;假设专用指标由研究者登记。 */
export const METRIC_LAYERS = ["final", "auxiliary", "hypothesis"] as const;
export type MetricLayer = (typeof METRIC_LAYERS)[number];
/** "测不到时怎么办"只有两档。"只记缺失"排在前面:它是诊断量的常规选择。 */
export const MISSING_POLICIES = ["recordMissing", "invalidateRun"] as const;
export type MissingPolicy = (typeof MISSING_POLICIES)[number];
/** 每一档的后果,字段说明和给模型看的材料都用这一份。 */
export const MISSING_POLICY_EFFECTS: Readonly<Record<MissingPolicy, string>> = {
recordMissing: "缺了只记缺失:这次运行的成绩照常算。诊断用的量一般选这一种。",
invalidateRun:
"缺了整次作废:这次运行不能被采纳,运行次数照扣。只有没有这个量成绩就不可信时,才选这一种。",
};
/** 一条指标除编号和层次以外的内容。研究者登记假设专用指标时填的就是这些,所以说明写给模型看。 */
const METRIC_FIELDS = {
meaning: Type.String({
minLength: 1,
description: "含义:这个量测的是什么",
}),
unit: Type.String({
minLength: 1,
description: "单位;没有单位(计数、比例)写\"无\"",
}),
direction: Type.Enum(DIRECTIONS, {
type: "string",
description: "maximize:越大越好;minimize:越小越好",
}),
howMeasured: Type.String({
minLength: 1,
description:
"怎么采样和汇总:在什么时刻测、测几次、怎么合成一个数,写到换一个人照着写代码能测出同一个数",
}),
missing: Type.Enum(MISSING_POLICIES, {
type: "string",
description:
`测不到时怎么办。recordMissing——${MISSING_POLICY_EFFECTS.recordMissing}` +
`invalidateRun——${MISSING_POLICY_EFFECTS.invalidateRun}` +
"消融格里一律只记缺失。引用别人登记过的指标时,沿用原登记的选法",
}),
};
/** 研究者在协议里登记的一条假设专用指标。编号由研究者起,候选代码按这个编号写出取值。 */
export const HypothesisMetric = Type.Object(
{
id: Type.String({
...Id,
description: "指标短名,只用字母、数字和 ._-,候选代码按它写出取值",
}),
...METRIC_FIELDS,
},
CLOSED,
);
export type HypothesisMetric = Static<typeof HypothesisMetric>;
/** 一条完整的指标定义:任务给的最终指标、常规辅助指标,以及冻结后的假设专用指标。 */
export const MetricDefinition = Type.Object(
{
id: Id,
layer: Type.Enum(METRIC_LAYERS, { type: "string" }),
...METRIC_FIELDS,
},
CLOSED,
);
export type MetricDefinition = Static<typeof MetricDefinition>;
// ---------------------------------------------------------------------------
// 运行定义:tasks 交给 runner,告诉它怎么起这个进程
// ---------------------------------------------------------------------------
export const RunDefinition = Type.Object(
{
/** 命令行,按参数切好,不经过 shell。 */
command: Type.Array(Type.String({ minLength: 1 }), { minItems: 1 }),
/** 进程在容器里的工作目录,绝对路径。 */
workdir: Type.String({ pattern: "^/" }),
/**
* 镜像按 sha256 钉死,不接受 tag:tag 可以被重新指向另一份内容。
* digest 填本机镜像 ID,即 `docker image inspect -f '{{.Id}}'` 的值;
* 不是仓库清单摘要(RepoDigests),本地构建的镜像没有那个值。
* 运行时必须写明:本机 Docker 的默认运行时不一定是 runc,跟着默认走,换台机器环境就悄悄变了。
*/
image: Type.Object(
{
digest: Type.String({ pattern: "^sha256:[0-9a-f]{64}$" }),
runtime: Type.String({ minLength: 1 }),
},
CLOSED,
),
/** 单次运行的墙钟时限(秒)和核数,由任务规定,是同批候选共用的比较条件。 */
wallSeconds: Type.Number({ exclusiveMinimum: 0 }),
cores: Type.Integer({ minimum: 1 }),
/**
* 候选进程不继承宿主的环境变量,类型上只允许 false。参考项目实测过:宿主环境被污染时,
* 同一份代码的分数从 0.9598 变成 0.8849,退出码是 0,没有任何报错。
*/
inheritEnv: Type.Literal(false),
/** 进程拿到的全部环境变量。 */
env: Type.Record(Type.String({ pattern: "^[A-Za-z_][A-Za-z0-9_]*$" }), Type.String(), CLOSED),
},
CLOSED,
);
export type RunDefinition = Static<typeof RunDefinition>;
// ---------------------------------------------------------------------------
// 运行结果第一层:runner 交出的进程层面结果
// ---------------------------------------------------------------------------
/** 资源用量,每一项都可能测不到。 */
const RESOURCE_USAGE = {
cpuSeconds: Measured,
wallSeconds: Measured,
peakMemoryBytes: Measured,
};
/**
* 以什么方式结束。有没有启动由它决定:只有 notStarted 是没启动,
* 其余三种都启动过、要扣次数(方法.md 第七部分)。
* 启动前就取消的记 notStarted;启动后取消的记 cancelled。
* 退出码只有正常退出才有,资源用量只有启动过才有,所以分成四种形状。
*/
export const ProcessResult = Type.Union([
Type.Object(
{
ending: Type.Literal("notStarted"),
reason: REASON,
},
CLOSED,
),
Type.Object(
{
ending: Type.Literal("exited"),
exitCode: Type.Integer(),
...RESOURCE_USAGE,
},
CLOSED,
),
Type.Object(
{
ending: Type.Literal("timeout"),
...RESOURCE_USAGE,
},
CLOSED,
),
Type.Object(
{
ending: Type.Literal("cancelled"),
...RESOURCE_USAGE,
},
CLOSED,
),
]);
export type ProcessResult = Static<typeof ProcessResult>;
// ---------------------------------------------------------------------------
// 运行结果第二层:tasks 从原始产物独立判分
// ---------------------------------------------------------------------------
/** 判定三选一:算数、不算数、判不了(判定所需的证据没拿到)。 */
export const VERDICTS = ["valid", "invalid", "undecidable"] as const;
export type Verdict = (typeof VERDICTS)[number];
/**
* 候选在写最终指标的同一个文件里多写的两个保留键(方法.md 第五部分"答案路径")。
* 实现的字段说明(tasks 一侧写)和给模型的材料(materials 一侧读)都用这两个常量。
*/
export const ANSWER_PATH_KEY = "answer-path";
export const SAFEGUARD_TRIGGERS_KEY = "safeguard-triggers";
/**
* 答案路径:候选写的最终答案出自哪里(main、某个因子编号或某条保障的短名)。
* tasks 只记读到的原始内容:读到了就原样存字符串;没写或写的不是字符串,记读不到并附原因。
* 取值在不在位置表里不在这里判断,由以后拿得到位置表的模块现算。
*/
export const AnswerPath = Type.Union([
Type.Object(
{
status: Type.Literal("read"),
value: Type.String(),
},
CLOSED,
),
Type.Object(
{
status: Type.Literal("unreadable"),
reason: REASON,
},
CLOSED,
),
]);
export type AnswerPath = Static<typeof AnswerPath>;
/**
* 各条保障这次动手的次数。tasks 只记读到的原始内容:读到"名字 → 非负整数"的对象就原样存;
* 没写,或者写的不是对象、值不是非负整数,记读不到并附原因。
* 名字和位置表对不对得上不在这里判断,由以后拿得到位置表的模块现算。
*/
export const SafeguardTriggers = Type.Union([
Type.Object(
{
status: Type.Literal("read"),
counts: Type.Record(Type.String(), Type.Integer({ minimum: 0 })),
},
CLOSED,
),
Type.Object(
{
status: Type.Literal("unreadable"),
reason: REASON,
},
CLOSED,
),
]);
export type SafeguardTriggers = Static<typeof SafeguardTriggers>;
export const JudgedResult = Type.Object(
{
verdict: Type.Enum(VERDICTS, { type: "string" }),
verdictReason: REASON,
/** 最终指标的分数(合法化之后的)。 */
score: Measured,
/** 各指标的取值,键是指标编号。 */
metrics: Type.Record(Id, Measured, CLOSED),
answerPath: AnswerPath,
safeguardTriggers: SafeguardTriggers,
/** 任务自己的附加记录,任务想记什么就记什么,例如填圆的合法化。 */
extra: Type.Unsafe<Json>({}),
},
CLOSED,
);
export type JudgedResult = Static<typeof JudgedResult>;
// ---------------------------------------------------------------------------
// 模型用量(方法.md 第七部分:拿不到用量时记"未知",不记 0)
// ---------------------------------------------------------------------------
/** 一次模型调用的用量。四种 token 互不重叠;价格单位为美元,与 Pi 的用量报告一致。 */
export const ModelUsage = Type.Object(
{
inputTokens: Measured,
cacheReadTokens: Measured,
cacheWriteTokens: Measured,
outputTokens: Measured,
cost: Measured,
},
CLOSED,
);
export type ModelUsage = Static<typeof ModelUsage>;