FrontierCode 式可合并代码任务

成熟真实项目里的代码改动,按维护者会不会合并来评分:带阻断项和权重的评分细则,另附 Agent 轨迹。

对应:FrontierCode 1.1

索取样例
每题评分细则条目
12–⁠16其中阻断项 4–⁠8 条,任一不过即记 0 分
参考解改动行数
195–⁠387另附其他正确解法 2–⁠4 份、取巧解 6–⁠14 份、部分完成解 4 份,均带评分结果
每条轨迹的回合数
53–⁠120工具调用 63–⁠135 次,用时 41–⁠73 分钟
编程语言
7TypeScript、JavaScript、Python、C++、Go、Rust、PHP
一条数据
一个任务环境 + Agent 轨迹
环境
由随附构建文件生成的开发环境镜像,含基线提交的项目代码;测试、lint 和构建均可离线运行
需求
以维护者口吻写成,1,226–⁠2,254 字符,后附仓库的测试、lint 和代码风格规范
评分器
带权重的细则,分别由隐藏测试、项目命令、“改前失败、改后通过”检查、改动范围检查或评审模型判定
轨迹
Claude Code + Claude Opus 5.5,max effort
格式
任务目录 + 请求级 JSONL 轨迹(zstd 压缩),附参考 harness:重建环境、验证题目、重评作答

概览

每条数据是一道代码改动任务,改的是真实、成熟的软件项目,按“维护者会不会合并这份改动”评分:需求以维护者的口吻写成,后附仓库自己的测试、lint 和代码风格规范;带权重的评分细则从行为正确、已有功能、测试、改动范围和代码质量几方面检查,任一阻断项不过即记 0 分。每条数据包含两部分:一是可复跑的任务环境(离线开发环境镜像的构建文件、需求、评分细则及其隐藏测试、参考解,以及带评分结果的其他正确解法、取巧解和部分完成解),可直接用于强化学习;二是 Claude Opus 5.5 以 max effort 在 Claude Code 中完成该任务的轨迹,逐请求完整记录,附逐条细则的评分,可用于监督微调。

交付内容

<任务>/
├── item.json                    # 任务与轨迹摘要
├── task/
│   ├── prompt.md                # 需求和仓库规范:Agent 收到的唯一输入
│   ├── task.json                # 元数据:项目、语言、题型、考察点
│   ├── rubric.json              # 评分细则:类别、检查方式、是否阻断、权重
│   ├── hidden_tests/            # 隐藏测试,评分时注入
│   ├── solutions/               # 参考解、其他正确解法、取巧解、部分完成解,及各自的期望判定
│   ├── validation.json          # 评分器对每份随附解法和未改动代码的判定
│   └── env/                     # 开发环境镜像构建文件、基线提交的仓库、命令清单
└── trajectories/attempt_<k>/
    ├── traj/                    # requests.jsonl.zst:请求级记录,每次请求的完整上下文与回复,含思考摘要
    ├── steps.jsonl              # 逐回合视图:思考摘要、说明、工具调用及结果
    ├── final.patch              # Agent 的最终改动
    ├── grade.json               # 每条细则的结果,评审模型判定的细则附理由
    └── result.json              # 回合、工具调用、用时、Agent 的最终回复
harness/                         # 参考 harness:构建环境、验证题目、重评作答、运行你自己的 Agent
index.json                       # 任务与轨迹总览
SHA256SUMS                       # 全部文件的校验和
README.md                        # 格式、构建和评分说明
  • 评分细则:每题 12 到 16 条,分六类:行为正确、不破坏已有行为、构建/lint/格式、测试质量、改动范围、代码质量与规范。每条由隐藏测试、项目命令、对 Agent 自写测试的“改前失败、改后通过”检查、改动范围检查或评审模型之一判定;权重为相对值(卡片中折算为百分比)。阻断项全部通过才计分,否则记 0 分。
  • 随附解法:除参考解外,每题还附其他正确解法(须通过)、取巧解(钻空子、偷工减料或违反仓库规范,须被阻断项拦下)和从刚起步到接近完成的部分完成解,均带评分器的判定。
  • 隔离:Agent 断网工作,看不到评分细则、隐藏测试和随附解法。

样例

7 道样题,每种语言一道、项目各不相同:5 道新功能、1 道缺陷修复、1 道跨模块一致性改动,参考解改动 195–⁠387 行。从随附解法的判定可以看出评分器有多严:取巧解不计阻断项的加权得分最高达 95,但因阻断项不过,得分仍为 0。

样题 语言 项目 题型 参考解改动 细则(阻断项)
1 TypeScript hono 新功能 6 个文件,307 行 14(7)
2 JavaScript webpack 新功能 29 个文件,349 行 15(5)
3 Python pydantic 缺陷修复 6 个文件,296 行 12(4)
4 C++ spdlog 新功能 8 个文件,195 行 12(7)
5 Go fiber 新功能 6 个文件,387 行 16(8)
6 Rust just 新功能 18 个文件,294 行 16(4)
7 PHP composer 跨模块一致性改动 10 个文件,266 行 15(5)

每张卡片给出 Agent 收到的题面原文、评分细则、评分器对随附解法的判定,以及 Claude Opus 5.5 的两条轨迹和关键时刻;每条轨迹下折叠着其最终改动的节选。

样例 1 / 7

构建期预编译 RegExpRouter

  • TypeScript
  • hono
  • 新功能
  • 参考解 6 个文件、改动 307 行
  • 路由匹配语义
  • JS 表达式序列化
  • 重构以复用内部逻辑
  • 一致性与差分测试
  • 难点
  • 复用现有编译器,不得复制
  • 不改原路由器的公开 API
  • 参数须自带数据,不引用模块状态
  • 列表不得按请求过滤或复制

构建期把路由路径编译成可序列化参数,由 PreparedRegExpRouter 运行时加载。运行时不编译,结果须与 RegExpRouter 一致(一种跨方法情形除外)。

题面(Agent 收到的原文)

prompt.md

Task description

Apps whose routes are known ahead of time pay for RegExpRouter's route compilation at every cold start; move it to build time.

Export from hono/router/reg-exp-router (src/router/reg-exp-router/index.ts):

  • buildInitParams({ paths }: { paths: string[] }): compiles the app's route paths (any order, repeats allowed) into the constructor arguments of PreparedRegExpRouter; paths that cannot share one RegExpRouter matcher may throw UnsupportedPathError.
  • serializeInitParams(params): string: a self-contained JavaScript expression evaluating to equivalent params, for generated code like new PreparedRegExpRouter(...<serialized>).
  • PreparedRegExpRouter<T>: a Router<T> created with new PreparedRegExpRouter(...params) (params serve one router), then add(method, path, handler) as usual; adding a path not given to buildInitParams (catch-alls included) throws an Error.

At runtime (creating the router, add(), match()) nothing is compiled: no RegExpRouter add() processing, no trie or RegExp construction. match() returns lists prepared beforehand (one array per route, as in RegExpRouter), never filtered or copied per request, with the handlers, order and params RegExpRouter gives for the same routes, in its [[handler, paramIndexMap][], paramStash] form. Exception: a request whose best-matching prepared path was added only for other methods may get just its method's and ALL's wildcard routes covering that path.

题面后半:测试、lint 和代码风格规范
prompt.md(续)

Test guidelines

Tests are Vitest files next to the code (src/**/*.test.ts, globals enabled, picked up automatically). Every router is checked against the shared conformance suite runTest in src/router/common.case.test.ts: each router's router.test.ts runs it (with a skip list for the cases that router cannot support) and adds router-specific cases where needed. New behaviour must be covered by tests in this style.

Run the router tests (unset the proxy variables, as the test scripts do): env -u http_proxy -u https_proxy -u HTTP_PROXY -u HTTPS_PROXY ./node_modules/.bin/vitest --run --coverage.enabled=false src/router

Before finishing, run the whole suite, which also type-checks the project (many tests are type-level): env -u http_proxy -u https_proxy -u HTTP_PROXY -u HTTPS_PROXY bun run test

Lint guidelines

CI runs these and they must pass:

  • bun run lint (ESLint over src, build, benchmarks, ...; the 40 existing warnings elsewhere are known, the code you add must not add errors or warnings)
  • bun run editorconfig-checker
  • bun run format (Prettier check; bun run format:fix rewrites the files)
  • ./node_modules/.bin/tsc --noEmit (also the first step of bun run test)

Style guidelines

Follow the existing router code in src/router/: TypeScript with import type for type-only imports, #private fields for internal state, the shared router types and constants from src/router.ts, small focused modules. Keep the change inside the RegExpRouter module (src/router/reg-exp-router/); package.json and jsr.json stay unchanged. Keep RegExpRouter's behaviour and public API as they are (no new public or protected members, add()/match() called as before); its internals may be refactored so that the new router reuses them instead of duplicating them, and its compiler modules (trie.ts, node.ts) need no changes. No debug output or commented-out code.

You are already on the correct base commit. Create your branch from this commit. Do not rebase or start from master, main, or any other branch.

评分细则

# 细则 类别 检查方式 阻断项 权重
C1 序列化参数下的共享套件、添加未知路径时报错、类型检查 行为正确 隐藏测试 是 18%
C2 可替换原路由器,结果与格式相同(题面所述情形除外) 行为正确 隐藏测试 是 14%
C3 运行时无原路由器注册、trie、正则构造及逐请求复制 行为正确 隐藏测试 是 12%
C4 全量测试与类型检查(bun run test) 不破坏已有行为 项目命令 是 10%
C5 只改本模块,RegExpRouter 接口不变 改动范围 改动范围检查 是 8%
C6 预编译的匹配器由现有编译器生成,而非复制或重写 代码质量与规范 评审判定 是 10%
C7 所附测试在改动前失败、改动后通过 测试质量 改前失败、改后通过 — 5%
C8 两个路由器共用请求时的查找逻辑,不重复实现 代码质量与规范 评审判定 — 3%
C9 新路由器测试超出冒烟级别,序列化结果被求值或整串比对 测试质量 评审判定 是 8%
C10 ESLint、editorconfig 均无新问题 构建/lint/格式 项目命令 — 3%
C11 Prettier 格式检查 构建/lint/格式 项目命令 — 2%
C12 无调试输出、注释掉的代码或叙述改动过程的注释 构建/lint/格式 评审判定 — 2%
C13 改动规模与需求相称 改动范围 改动范围检查 — 2%
C14 add() 也拒绝未提供的 * 和 /* 行为正确 隐藏测试 — 3%

评分器对随附解法的判定

解法 份数 得分 不计阻断项的加权得分
参考解 1 100 100
其他正确解法 3 100 100
接近完成解 1 97 97
部分完成解 3 0 27–⁠71
取巧解 9 0 57–⁠92
未改动的代码 1 0 17

满分 100。任一阻断项不过,得分即为 0。

轨迹 1

回合
53
工具调用
63
用时(分钟)
47
改动文件
5
增 / 删行数
+617 / −44

从 RegExpRouter 抽出内部类 MatcherBuilder,构建期一次编译全部路径并记下各路径处理函数所属的预备列表,运行时 add() 按记录放入处理函数。以随机差分对比和七处注入缺陷验证,新测试用原始与序列化参数各跑一遍共享套件。

第 7 回合 读完 RegExpRouter 源码与共享套件,计划复用其代码,把全部路径登记在同一方法下。

第 20 回合 把路由登记和匹配器构建代码基本原样移入内部类 MatcherBuilder,供构建期复用。

第 28 回合 与 RegExpRouter 做随机路由对比:共 5 轮,每轮 36 万次比较,结果无差异。

第 31 回合 发现中段 * 与同位命名参数共用匹配器时可能丢参,现对会丢参的路径在构建期报错。

第 33 回合 用 6 万组随机路径集核对:构建期报错与 RegExpRouter 报错或丢失参数完全对应。

第 41 回合 确认注入 prepared-router.ts 的七处缺陷(如方法间路由串扰)均被测试发现。

第 46 回合 用 git stash 撤下改动后重跑全套测试,通过用例正好少了新测试文件的 194 个。

轨迹 1 的最终改动(节选)
diff --git a/src/router/reg-exp-router/prepared-router.ts b/src/router/reg-exp-router/prepared-router.ts
new file mode 100644
--- /dev/null
+++ b/src/router/reg-exp-router/prepared-router.ts
@@ -0,0 +1,167 @@
+import type { ParamIndexMap, Result, Router } from '../../router'
+import {
+  MESSAGE_MATCHER_IS_ALREADY_BUILT,
+  METHOD_NAME_ALL,
+  UnsupportedPathError,
+} from '../../router'
+import type { HandlerData, Matcher, StaticMap } from './matcher'
+import { buildMatch, emptyParam } from './matcher'
+import { MatcherBuilder } from './router'
+
+/**
+ * path => [indexes of the lists that the handlers of the path go into, paramIndexMap of the handlers][]
+ *
+ * The lists matched by the RegExp of the matcher are indexed by number, the static ones by path.
+ */
+type RelocateMap = Record<string, [(number | string)[], ParamIndexMap][]>
+
+/**
+ * RegExpRouter whose routes are compiled at build time by `buildInitParams()`.
+ *
+ * @example
+ * ```ts
+ * // at build time
+ * const params = buildInitParams({ paths: app.routes.map(({ path }) => path) })
+ * const code = `const router = new PreparedRegExpRouter(...${serializeInitParams(params)})`
+ *
+ * // at runtime, with the generated `router`
+ * const app = new Hono({ router })
+ * ```
+ */
+export class PreparedRegExpRouter<T> implements Router<T> {
+  name: string = 'PreparedRegExpRouter'
+  #matchers: Record<string, Matcher<T>> = Object.create(null)
+  #relocateMap?: RelocateMap
+
+  constructor(matcher: Matcher<never>, relocateMap: RelocateMap) {
+    this.#matchers[METHOD_NAME_ALL] = matcher
+    this.#relocateMap = relocateMap
+  }
+
+  add(method: string, path: string, handler: T) {
+    if (!this.#relocateMap) {
+      throw new Error(MESSAGE_MATCHER_IS_ALREADY_BUILT)
+    }
+
+    const relocations = this.#relocateMap[path]
+    if (!relocations) {
+      throw new Error(
+        `Can not add a route since the path "${path}" was not given to buildInitParams.`
+      )
+    }
+
+    if (!this.#matchers[method]) {
+      // as in RegExpRouter, the lists of a method start with the routes of ALL
+      const [regexp, handlerData, staticMap] = this.#matchers[METHOD_NAME_ALL]
+      const methodStaticMap: StaticMap<T> = Object.create(null)
+      for (const staticPath in staticMap) {
+        methodStaticMap[staticPath] = [staticMap[staticPath][0].slice(), emptyParam]
+      }
+      this.#matchers[method] = [regexp, handlerData.map((list) => list.slice()), methodStaticMap]
+    }
+
+    const matchers =
+      method === METHOD_NAME_ALL ? Object.values(this.#matchers) : [this.#matchers[method]]
+    for (const [indexes, paramIndexMap] of relocations) {
+      const data: [T, ParamIndexMap] = [handler, paramIndexMap]
+      for (const matcher of matchers) {
+        for (const index of indexes) {
+          ;(typeof index === 'number' ? matcher[1][index] : matcher[2][index][0]).push(data)
+        }
+      }
+    }
+  }
+
+  match(method: string, path: string): Result<T> {
+    // Release the relocate map, which is used only by add()
+    this.#relocateMap = undefined
+
+    this.match = buildMatch(this.#matchers)
+    return this.match(method, path)
+  }
+}
+
+/**
+ * Compiles the paths of all the routes of an app into the arguments of `new PreparedRegExpRouter()`.
+ *
+ * @param params.paths - The paths, in any order and with repeats allowed.
+ * @returns The arguments, which serve one router.
+ * @throws {UnsupportedPathError} If the paths cannot share one matcher of RegExpRouter.
+ */
+export const buildInitParams = ({
+  paths,
+}: {
+  paths: string[]
+}): ConstructorParameters<typeof PreparedRegExpRouter> => {
+  const uniquePaths = [...new Set(paths)]
+
+  // compile the paths as the routes of one method, with their indexes as the handlers
+  const builder = new MatcherBuilder<number>()
+  const relocateMap: RelocateMap = Object.create(null)
+  uniquePaths.forEach((path, i) => {
+    builder.add(METHOD_NAME_ALL, path, i)
+    relocateMap[path] = []
+  })
+  const [regexp, handlerData, staticMap] = builder.buildAllMatchers()[
+    METHOD_NAME_ALL
+  ] as Matcher<number>
+
+  // record the lists that the handlers of each path go into, and leave them empty for the router
+  const relocate = (list: HandlerData<number>, index: number | string): HandlerData<never> => {
+    list.forEach(([i, paramIndexMap]) => {
+      if (Object.values(paramIndexMap).some((paramIndex) => paramIndex === undefined)) {
+        // e.g. `:id` of `/posts/:id/edit` is not captured when `/posts/*/new` comes first
+        throw new UnsupportedPathError(uniquePaths[i])
+      }
+
+      const relocations = relocateMap[uniquePaths[i]]
+      const key = JSON.stringify(paramIndexMap)
+      const relocation = relocations.find(([, map]) => JSON.stringify(map) === key)
+      if (relocation) {
+        relocation[0].push(index)
+      } else {
+        relocations.push([[index], paramIndexMap])
+      }
+    })
…… 其余 610 行从略(共改动 5 个文件)

轨迹 2

回合
80
工具调用
90
用时(分钟)
51
改动文件
5
增 / 删行数
+693 / −120

把编译与查找代码从 router.ts 移入共享的 matcher.ts;构建期以路径作处理函数,按原路由器的登记方式建表后一次编译,并记下各路径应进入的预备列表。以差分模糊测试(含跨方法例外)、注入缺陷和生成代码的端到端运行验证,最后本地提交。

第 13 回合 先查 request.ts 如何读取参数,因为序列化后的参数可能丢掉空原型。

第 19 回合 把编译与查找代码移入 matcher.ts,router.ts 此外只改了导入和一行。

第 26 回合 约百万次随机比较均与原路由器一致,但从未触发题面所述的例外,遂补充一批贪婪 {.+} 路径。

第 32 回合 注入三处缺陷:运行时构造正则、处理函数前插而非追加、按请求复制列表,均被对应测试发现。

第 70 回合 生成内嵌序列化参数的模块,在独立进程中导入并经 Hono 应用处理请求。

第 77 回合 测 402 条路由启动开销:原路由器 1.503 ms,预编译版(含参数求值)0.166 ms。

第 79 回合 重跑题面列出的 CI 检查与全量测试后,在新分支上做了一次本地提交。

轨迹 2 的最终改动(节选)
diff --git a/src/router/reg-exp-router/prepared-router.ts b/src/router/reg-exp-router/prepared-router.ts
new file mode 100644
--- /dev/null
+++ b/src/router/reg-exp-router/prepared-router.ts
@@ -0,0 +1,182 @@
+import type { ParamIndexMap, Result, Router } from '../../router'
+import { METHOD_NAME_ALL } from '../../router'
+import { checkOptionalParameter } from '../../utils/url'
+import type { HandlerData, HandlerWithMetadata, Matcher, MatcherMap, StaticMap } from './matcher'
+import {
+  buildMatch,
+  buildMatcherFromPreprocessedRoutes,
+  buildWildcardRegExp,
+  clearWildcardRegExpCache,
+  emptyParam,
+} from './matcher'
+
+type Target = number | string // index of a handler list in Matcher[1], or a key of Matcher[2]
+// path -> the handler lists that handlers for the path are added to, with their ParamIndexMap
+type RelocateMap = Record<string, [Target[], ParamIndexMap][]>
+
+function cloneMatcher<T>([regexp, handlerMap, staticMap]: Matcher<T>): Matcher<T> {
+  const clonedStaticMap: StaticMap<T> = Object.create(null)
+  for (const path in staticMap) {
+    clonedStaticMap[path] = [[...staticMap[path][0]], emptyParam]
+  }
+  return [regexp, handlerMap.map((handlerData) => [...handlerData]), clonedStaticMap]
+}
+
+/**
+ * A router that matches like `RegExpRouter`, with its matcher compiled ahead of time by
+ * `buildInitParams()`, so that creating it, adding routes and matching compile nothing.
+ *
+ * All methods share the prepared matcher: a request whose best-matching path was added only for
+ * other methods gets just the wildcard routes of its method and `ALL` covering that path.
+ *
+ * @example
+ * ```ts
+ * const createApp = (router: Hono['router']) => new Hono({ router }).get('/posts/:id', handler)
+ *
+ * // At build time, compile the paths of the application
+ * const paths = createApp(new RegExpRouter()).routes.map(({ path }) => path)
+ * const serialized = serializeInitParams(buildInitParams({ paths }))
+ *
+ * // At runtime, in the code generated with `serialized`
+ * const app = createApp(new PreparedRegExpRouter(...<serialized>))
+ * ```
+ */
+export class PreparedRegExpRouter<T> implements Router<T> {
+  name: string = 'PreparedRegExpRouter'
+  #matchers: MatcherMap<T> = Object.create(null)
+  #relocateMap: RelocateMap
+
+  constructor(matcher: Matcher<never>, relocateMap: RelocateMap) {
+    this.#matchers[METHOD_NAME_ALL] = matcher
+    this.#relocateMap = relocateMap
+  }
+
+  add(method: string, path: string, handler: T) {
+    const relocation = this.#relocateMap[path]
+    if (!relocation) {
+      throw new Error(`Can not add a route since the path is not prepared: ${path}`)
+    }
+
+    const matchers = this.#matchers
+    // like RegExpRouter, a method starts with the handlers added for ALL so far
+    matchers[method] ||= cloneMatcher(matchers[METHOD_NAME_ALL] as Matcher<T>)
+
+    for (const m of method === METHOD_NAME_ALL ? Object.keys(matchers) : [method]) {
+      const [, handlerMap, staticMap] = matchers[m] as Matcher<T>
+      for (const [targets, paramIndexMap] of relocation) {
+        for (const target of targets) {
+          const handlerData = typeof target === 'number' ? handlerMap[target] : staticMap[target][0]
+          handlerData.push([handler, paramIndexMap])
+        }
+      }
+    }
+  }
+
+  match(method: string, path: string): Result<T> {
+    this.match = buildMatch(this.#matchers)
+
+    return this.match(method, path)
+  }
+}
+
+/**
+ * Compiles the paths of the routes of an application into the parameters of `PreparedRegExpRouter`.
+ *
+ * @param {object} options - The options.
+ * @param {string[]} options.paths - The paths the routes will be added with, in any order. Repeats are allowed.
+ * @returns {ConstructorParameters<typeof PreparedRegExpRouter>} The parameters for `new PreparedRegExpRouter(...params)`, serving one router.
+ * @throws {UnsupportedPathError} If the paths can not share one matcher of `RegExpRouter`.
+ */
+export function buildInitParams({
+  paths,
+}: {
+  paths: string[]
+}): ConstructorParameters<typeof PreparedRegExpRouter> {
+  // Build the handler lists as RegExpRouter does for routes added for ALL, with paths as handlers
+  const routes: Record<string, HandlerWithMetadata<string>[]> = Object.create(null)
+  const wildcards: [string, RegExp, number][] = []
+  for (const path of new Set(paths)) {
+    const routePath = path === '/*' ? '*' : path
+    const paramCount = (routePath.match(/\/:/g) || []).length
+    if (/\*$/.test(routePath)) {
+      routes[routePath] ||= []
+      wildcards.push([path, buildWildcardRegExp(routePath), paramCount])
+    } else {
+      const routePaths = checkOptionalParameter(routePath) || [routePath]
+      routePaths.forEach((p, i) => {
+        ;(routes[p] ||= []).push([path, paramCount - routePaths.length + i + 1])
+      })
+    }
+  }
+  for (const [path, re, paramCount] of wildcards) {
+    for (const p in routes) {
+      if (re.test(p)) {
+        routes[p].push([path, paramCount])
+      }
+    }
+  }
+  clearWildcardRegExpCache()
+
+  const [regexp, handlerMap, staticMap] = buildMatcherFromPreprocessedRoutes(Object.entries(routes))
+
+  // Record the handler lists each path is added to, and leave the lists empty for the router.
+  // Lists sharing the same ParamIndexMap are grouped to keep the serialized params small.
+  const relocateMap: RelocateMap = Object.create(null)
+  for (const path of paths) {
…… 其余 720 行从略(共改动 5 个文件)

样例 2 / 7

将无 DOM 时收集的 CSS 暴露为 __webpack_css_server_styles__

  • JavaScript
  • webpack
  • 新功能
  • 参考解 29 个文件、改动 349 行
  • 运行时代码生成
  • 对象键枚举顺序
  • 热更新与分块加载
  • 旧版 Node 兼容
  • 测试夹具编写
  • 难点
  • 数字模块 id 下仍须按样式应用顺序输出
  • 原有的注册表写入也不能假定有 globalThis
  • 只依赖该变量的包也要能运行
  • 读取方须与写入方共用 cssServerStyleRegistry()

将通用(universal)目标 ["web", "node"] 的包在没有 DOM 时收集的 CSS 暴露为只读 string 变量 __webpack_css_server_styles__,并在 module.d.ts 中声明;每次读取返回注册表当时的 CSS,未收集到样式时为 ""。改动涉及所有通用 CSS 构建共享的运行时代码。

题面(Agent 收到的原文)

prompt.md

Task description

When a universal bundle (target: ["web", "node"] with experiments.css) runs without a DOM, the CSS runtime keeps the styles it would have put into the document in a registry on the global object, under an internal key derived from output.uniqueName. Today the only way to get them back out (to inline them into server-rendered HTML, say) is to scan the global for that key, which is what our own universal CSS tests do.

Expose that registry to application code as a module variable, __webpack_css_server_styles__, which evaluates to all the CSS in the current build's registry as a single string.

  • It is a read-only webpack API variable of type string, like __webpack_hash__, usable from any module, and declared for TypeScript users in module.d.ts.
  • Every read reflects the registry at that moment: styles of exportType: "style" modules, CSS chunks the runtime has loaded since, and hot updates that replaced or removed a stylesheet.
  • The CSS comes out in the order the styles were applied, so the cascade survives. This has to hold for every kind of module id, including the numeric ids production builds use.
  • When nothing was collected (the styles went into a document, the bundle has no CSS, the target is not a universal one) it is "". A bundle that needs nothing else from the runtime must still work.
  • Don't assume the host has globalThis unless output.environment.globalThis says so: this path has to run on the oldest Node.js we support. That goes for the existing code that fills the registry too.
题面后半:测试、lint 和代码风格规范
prompt.md(续)

Test guidelines

  • Tests run on jest through the test:base wrapper (yarn test:base ...); never call jest directly, and don't run plain yarn test (it lints and then runs every suite).
  • Behaviour is tested with fixture cases that are picked up automatically, no registration needed. test/configCases/<category>/<case>/ holds an index.js whose it() blocks run inside the emitted bundle, plus a webpack.config.js; CSS cases live in test/configCases/css/. HMR cases live in test/hotCases/<category>/<case>/ (universal-target ones in test/hotCases/universal/). TESTING_DOCS.md and neighbouring cases such as test/configCases/css/export-type-style-universal show the layout.
  • The config-case harness executes a ["web", "node"] bundle without a DOM. Only when ES module output is enabled (experiments.outputModule + output.module) does it run the bundle twice, once with a fake DOM and once without; without it, code in a typeof document !== "undefined" branch never runs. Universal HMR cases always run both ways.
  • Hot updates of type: "css" modules with exportType: "style" already fail at this commit (__webpack_require__ is not defined in the hot-update chunk); type: "css/module" with exportType: "style" works, as in test/hotCases/universal/css-export-type-style. Fixing that bug is optional.
  • Run one case with yarn test:base --ci test/ConfigTestCases.basictest.js -t "css <case>" (the same case also runs in test/ConfigCacheTestCases.basictest.js), an HMR case with yarn test:base --ci test/HotTestCasesUniversal.test.js -t "universal <case>". webpack-test-files <test paths...> (on PATH in this environment) runs every suite that picks up the given files.
  • New behaviour needs tests that fail without your change and pass with it. Prefer fixture cases over unit tests, and cover each branch you add, the ordering guarantee and the empty result included.
  • Before you finish, yarn test:unit --ci and yarn test:basic --ci (about three minutes together) and yarn test:base --ci test/HotTestCasesUniversal.test.js test/HotTestCasesWeb.test.js must be green.

Lint guidelines

  • yarn lint must pass: it is what CI's lint job runs (about a minute). It consists of yarn lint:code (ESLint), yarn lint:special (generated files such as types.d.ts are up to date), the tsc projects (yarn lint:types over the JSDoc in lib/, yarn lint:types-test, yarn lint:types-module-test which checks module.d.ts against test/typesCases/, ...), yarn fmt:check (Prettier) and yarn lint:spellcheck (cspell).
  • Regenerate generated files with yarn fix:special; never edit them by hand. yarn fix runs fix:code, fix:special and fmt (Prettier, writing) in one go.
  • A user-facing change needs a changeset in .changeset/ (format in AGENTS.md, "Adding a Changeset"); yarn validate:changeset checks it.

Style guidelines

  • Follow AGENTS.md for code conventions, comments, changesets and generated files (its git and pull-request sections do not apply to this task).
  • lib/ is CommonJS with JSDoc types and has to run on Node.js >= 10.13 (no ?. or ??). New files under lib/ start with the MIT license header.
  • Runtime code emitted into bundles must honour output.environment: build it with the runtimeTemplate helpers (basicFunction, returningFunction, renderConst, ...) instead of hard-coding newer syntax.
  • Wire the variable the way the existing module variables and runtime globals are wired (lib/APIPlugin.js, lib/RuntimeGlobals.js, RuntimeModule subclasses), and reuse existing helpers instead of duplicating their logic.
  • Comments: one line, two at most, and only when they say something the code does not.
  • Keep the change focused on this feature and leave the shared test harness alone. Don't loosen or delete existing tests; the universal CSS tests that read the registry directly may switch to the new variable.

You are already on the correct base commit. Create your branch from this commit. Do not rebase or start from master, main, or any other branch.

评分细则

# 细则 类别 检查方式 阻断项 权重
C1 无 DOM 时返回样式模块、后加载的 chunk 及热更新后的 CSS,有 DOM 时为空 行为正确 隐藏测试 是 24%
C2 CSS 按样式应用的顺序返回,使用数字模块 id 时也是如此 行为正确 隐藏测试 是 10%
C3 无 CSS 的包、仅 web 或仅 node 目标、懒加载 chunk 中读取均为 "" 且不崩溃 行为正确 隐藏测试 是 7%
C4 未启用 output.environment.globalThis 时,样式注入、分块加载与读取都不直接引用 globalThis 行为正确 隐藏测试 — 5%
C5 ES5 目标下,使用该变量的包只含 ES5 语法 行为正确 隐藏测试 — 2%
C6 在原有测试下,单元、配置用例、stats 与通用/web 目标的 HMR 套件仍通过 不破坏已有行为 项目命令 是 10%
C7 新增测试在没有该改动时失败,有改动时通过 测试质量 改前失败、改后通过 是 9%
C8 新增测试中实际执行的断言覆盖数字 id 下的顺序与空结果 测试质量 评审判定 — 5%
C9 yarn lint 无报错:ESLint、生成文件最新、类型检查、Prettier、拼写检查 构建/lint/格式 项目命令 — 6%
C10 附带 webpack 的 minor changeset,格式符合项目要求 构建/lint/格式 项目命令 — 3%
C11 module.d.ts 将 __webpack_css_server_styles__ 声明为 string 构建/lint/格式 隐藏测试 — 3%
C12 读取方复用写入方的 cssServerStyleRegistry(),别处不重建注册表表达式 代码质量与规范 评审判定 — 5%
C13 经 APIPlugin 替换表、带注释的 RuntimeGlobals 常量和 RuntimeModule 子类接入 代码质量与规范 评审判定 — 4%
C14 注释简短、言之有物,无调试输出、注释掉的代码、复述代码的注释 代码质量与规范 评审判定 — 3%
C15 规模适度,只改源码、测试、changeset 和声明;不动测试框架、不削弱测试 改动范围 改动范围检查 — 4%

评分器对随附解法的判定

解法 份数 得分 不计阻断项的加权得分
参考解 1 100 100
其他正确解法 3 100 100
部分完成解 4 0 33–⁠88
取巧解 8 0 12–⁠87
未改动的代码 1 0 16

满分 100。任一阻断项不过,得分即为 0。

轨迹 1

回合
120
工具调用
121
用时(分钟)
52
改动文件
61
增 / 删行数
+740 / −10

把 __webpack_css_server_styles__ 接到新的 __webpack_require__.ss 运行时模块上,在通用(universal)目标以外直接替换为 "";cssServerStyleRegistry() 在未启用 output.environment.globalThis 时改用 __webpack_require__.g。模块键加上 "module-" 前缀,使数字 id 的条目保持插入顺序。新用例给样式模块分配递减的数字 id、推迟第一个 chunk 的 fs.readFile 并屏蔽 globalThis;用 git stash 撤下源码改动后这些用例失败。

第 11 回合 读完 cssServerStyleRegistry() 及写入注册表的代码后,发现普通对象按数值顺序列出数字 id 键,且 globalThis 是写死的。

第 16 回合 确认默认通用目标(["web", "node"])未设置 output.environment.globalThis,注册表须改走全局对象运行时 __webpack_require__.g。

第 97 回合 临时让 server-styles-unique-name 的两个构建使用相同的 uniqueName,两个隔离测试随即失败。

第 105 回合 把 output.environment.globalThis: true 的变体移入单独用例 server-styles-global-this,并用 supportsGlobalThis 过滤,因为 Node.js 10 和 11 没有 globalThis。

第 106 回合 变异检查中删去 GLOBALS_ON_REQUIRE 的新项,server-styles-global-this 用例中只需该变量的产物随即无法启动。

第 107 回合 用 git stash 撤下 lib、module.d.ts 和 types.d.ts 的改动后,新的 server-styles- 配置用例中有 24 个测试失败。

第 113 回合 确认重新运行的 yarn lint、yarn test:unit --ci、yarn test:basic --ci、HotTestCasesUniversal.test.js 和 HotTestCasesWeb.test.js 均无失败。

轨迹 1 的最终改动(节选)
diff --git a/lib/css/CssServerStylesRuntimeModule.js b/lib/css/CssServerStylesRuntimeModule.js
new file mode 100644
--- /dev/null
+++ b/lib/css/CssServerStylesRuntimeModule.js
@@ -0,0 +1,38 @@
+/*
+	MIT License http://www.opensource.org/licenses/mit-license.php
+*/
+
+"use strict";
+
+const RuntimeGlobals = require("../RuntimeGlobals");
+const RuntimeModule = require("../RuntimeModule");
+
+/** @typedef {import("../Compilation")} Compilation */
+
+class CssServerStylesRuntimeModule extends RuntimeModule {
+	constructor() {
+		super("css server styles");
+	}
+
+	/**
+	 * Generates runtime code for this runtime module.
+	 * @returns {string | null} runtime code
+	 */
+	generate() {
+		const { runtimeTemplate } = /** @type {Compilation} */ (this.compilation);
+
+		return `${RuntimeGlobals.cssServerStyles} = ${runtimeTemplate.basicFunction(
+			"",
+			[
+				`${runtimeTemplate.renderConst()} registry = ${runtimeTemplate.cssServerStyleRegistry(false)};`,
+				// keys are never array indices, so they list in insertion order; a chunk still loading holds ""
+				`return registry ? Object.keys(registry).map(${runtimeTemplate.returningFunction(
+					"registry[key]",
+					"key"
+				)}).filter(Boolean).join("\\n") : "";`
+			]
+		)};`;
+	}
+}
+
+module.exports = CssServerStylesRuntimeModule;
diff --git a/lib/APIPlugin.js b/lib/APIPlugin.js
--- a/lib/APIPlugin.js
+++ b/lib/APIPlugin.js
@@ -14,6 +14,7 @@ const {
 	JAVASCRIPT_MODULE_TYPE_ESM
 } = require("./ModuleTypeConstants");
 const RuntimeGlobals = require("./RuntimeGlobals");
+const CssServerStylesRuntimeModule = require("./css/CssServerStylesRuntimeModule");
 const ConstDependency = require("./dependencies/ConstDependency");
 const ModuleInitFragmentDependency = require("./dependencies/ModuleInitFragmentDependency");
 const RuntimeRequirementsDependency = require("./dependencies/RuntimeRequirementsDependency");
@@ -113,6 +114,12 @@ function getReplacements() {
 			type: "string",
 			assign: false
 		},
+		__webpack_css_server_styles__: {
+			expr: `${RuntimeGlobals.cssServerStyles}()`,
+			req: [RuntimeGlobals.cssServerStyles],
+			type: "string",
+			assign: false
+		},
 		__webpack_get_script_filename__: {
 			expr: RuntimeGlobals.getChunkScriptFilename,
 			req: [RuntimeGlobals.getChunkScriptFilename],
@@ -172,6 +179,11 @@ class APIPlugin {
 					REPLACEMENTS.__non_webpack_require__.expr =
 						"__WEBPACK_EXTERNAL_createRequire_require";
 				}
+				// only universal targets collect styles outside of a document
+				if (!compilation.runtimeTemplate.isNeutralPlatform()) {
+					REPLACEMENTS.__webpack_css_server_styles__.expr = '""';
+					REPLACEMENTS.__webpack_css_server_styles__.req = null;
+				}
 
 				compilation.dependencyTemplates.set(
 					ConstDependency,
@@ -199,6 +211,19 @@ class APIPlugin {
 						return true;
 					});
 
+				compilation.hooks.runtimeRequirementInTree
+					.for(RuntimeGlobals.cssServerStyles)
+					.tap(PLUGIN_NAME, (chunk, set) => {
+						if (!compilation.outputOptions.environment.globalThis) {
+							set.add(RuntimeGlobals.global);
+						}
+						compilation.addRuntimeModule(
+							chunk,
+							new CssServerStylesRuntimeModule()
+						);
+						return true;
+					});
+
 				const hooks = JavascriptModulesPlugin.getCompilationHooks(compilation);
 
 				hooks.renderModuleContent.tap(
diff --git a/lib/RuntimeTemplate.js b/lib/RuntimeTemplate.js
--- a/lib/RuntimeTemplate.js
+++ b/lib/RuntimeTemplate.js
@@ -191,16 +191,23 @@ class RuntimeTemplate {
 
 	/**
 	 * Expression for the global registry that collects CSS server-side when there
-	 * is no DOM (SSR). An SSR host reads it from `globalThis`; it is keyed by the
-	 * style/chunk identifier and namespaced by `output.uniqueName`.
+	 * is no DOM (SSR). It is keyed by the style/chunk identifier and namespaced by
+	 * `output.uniqueName`. It lives on `globalThis`, or on `RuntimeGlobals.global`
+	 * (which the caller then has to require) when the environment has no `globalThis`.
+	 * @param {boolean=} create false to only read it (`undefined` while nothing was collected)
 	 * @returns {string} runtime expression evaluating to the registry object
 	 */
-	cssServerStyleRegistry() {
+	cssServerStyleRegistry(create = true) {
 		const name = this.outputOptions.uniqueName;
 		const key = JSON.stringify(
 			name ? `__webpack_css__${name}` : "__webpack_css__"
 		);
-		return `(${this.assignOr(`globalThis[${key}]`, "{}")})`;
+		const registry = `${
+			this.outputOptions.environment.globalThis
+				? "globalThis"
+				: RuntimeGlobals.global
+		}[${key}]`;
+		return create ? `(${this.assignOr(registry, "{}")})` : registry;
 	}
 
 	supportsConst() {
…… 其余 1,045 行从略(共改动 61 个文件)

轨迹 2

回合
102
工具调用
108
用时(分钟)
44
改动文件
67
增 / 删行数
+722 / −10

新增定义 __webpack_require__.ss 的 CssServerStylesRuntimeModule(非通用目标下返回 ""),以及判断何时需要 __webpack_require__.g 的辅助方法 cssServerStyleRegistryNeedsGlobal()。模块键加上 "module-" 前缀,CSS chunk 在请求时先占位,以保持应用顺序;把 cssInjectStyle 加入 GLOBALS_ON_REQUIRE,顺带修复运行时 chunk 之外的 type: "css" 样式模块。新用例覆盖数字 id 下的顺序和没有 globalThis 的 vm 上下文,并用变异测试检查。

第 10 回合 把变量设计成运行时函数调用,每次读取都拿到最新注册表,并注意到普通对象不按插入顺序列出数字键。

第 12 回合 在仓库外的临时目录构建 ["web", "node"] 示例,发现 output.environment.globalThis 默认未设置,注册表须改走全局对象运行时 __webpack_require__.g。

第 73 回合 在发现 __webpack_require__ is not defined 崩溃(可选修复)除热更新外也会在异步 chunk 中出现后,把 cssInjectStyle 加入 GLOBALS_ON_REQUIRE。

第 85 回合 运行变异测试:去掉 "module-" 前缀后 server-styles-order 失败,写死 globalThis 后 server-styles-no-global-this 失败。

第 87 回合 补充测试,断言读取变量后 globalThis 上没有以 __webpack_css__ 开头的键,此前漏检的“读取时创建注册表”变异随即被测出。

第 92 回合 确认 yarn test:unit --ci、yarn test:basic --ci 和全部五个 HotTestCases* 套件均无失败,快照也无变化。

第 97 回合 以 ["web", "node10", "es5"] 为目标,在仓库外的临时目录构建示例,产物中只剩示例源码自带的箭头函数。

轨迹 2 的最终改动(节选)
diff --git a/lib/css/CssServerStylesRuntimeModule.js b/lib/css/CssServerStylesRuntimeModule.js
new file mode 100644
--- /dev/null
+++ b/lib/css/CssServerStylesRuntimeModule.js
@@ -0,0 +1,41 @@
+/*
+	MIT License http://www.opensource.org/licenses/mit-license.php
+*/
+
+"use strict";
+
+const RuntimeGlobals = require("../RuntimeGlobals");
+const RuntimeModule = require("../RuntimeModule");
+
+/** @typedef {import("../Compilation")} Compilation */
+
+class CssServerStylesRuntimeModule extends RuntimeModule {
+	constructor() {
+		super("css server styles");
+	}
+
+	/**
+	 * Generates runtime code for this runtime module.
+	 * @returns {string | null} runtime code
+	 */
+	generate() {
+		const { runtimeTemplate } = /** @type {Compilation} */ (this.compilation);
+		// only universal targets collect styles
+		if (!runtimeTemplate.isNeutralPlatform()) {
+			return `${RuntimeGlobals.cssServerStyles} = ${runtimeTemplate.returningFunction('""')};`;
+		}
+		return `${RuntimeGlobals.cssServerStyles} = ${runtimeTemplate.basicFunction(
+			"",
+			[
+				`${runtimeTemplate.renderConst()} registry = ${runtimeTemplate.cssServerStyleRegistry(false)};`,
+				// keys are never array indices, so they enumerate in the order the styles were applied
+				`return Object.keys(registry).map(${runtimeTemplate.returningFunction(
+					"registry[key]",
+					"key"
+				)}).filter(Boolean).join("\\n");`
+			]
+		)};`;
+	}
+}
+
+module.exports = CssServerStylesRuntimeModule;
diff --git a/lib/APIPlugin.js b/lib/APIPlugin.js
--- a/lib/APIPlugin.js
+++ b/lib/APIPlugin.js
@@ -14,6 +14,7 @@ const {
 	JAVASCRIPT_MODULE_TYPE_ESM
 } = require("./ModuleTypeConstants");
 const RuntimeGlobals = require("./RuntimeGlobals");
+const CssServerStylesRuntimeModule = require("./css/CssServerStylesRuntimeModule");
 const ConstDependency = require("./dependencies/ConstDependency");
 const ModuleInitFragmentDependency = require("./dependencies/ModuleInitFragmentDependency");
 const RuntimeRequirementsDependency = require("./dependencies/RuntimeRequirementsDependency");
@@ -107,6 +108,12 @@ function getReplacements() {
 			type: "string",
 			assign: false
 		},
+		__webpack_css_server_styles__: {
+			expr: `${RuntimeGlobals.cssServerStyles}()`,
+			req: [RuntimeGlobals.cssServerStyles],
+			type: "string",
+			assign: false
+		},
 		__webpack_chunkname__: {
 			expr: RuntimeGlobals.chunkName,
 			req: [RuntimeGlobals.chunkName],
@@ -199,6 +206,21 @@ class APIPlugin {
 						return true;
 					});
 
+				compilation.hooks.runtimeRequirementInTree
+					.for(RuntimeGlobals.cssServerStyles)
+					.tap(PLUGIN_NAME, (chunk, set) => {
+						if (
+							compilation.runtimeTemplate.cssServerStyleRegistryNeedsGlobal()
+						) {
+							set.add(RuntimeGlobals.global);
+						}
+						compilation.addRuntimeModule(
+							chunk,
+							new CssServerStylesRuntimeModule()
+						);
+						return true;
+					});
+
 				const hooks = JavascriptModulesPlugin.getCompilationHooks(compilation);
 
 				hooks.renderModuleContent.tap(
diff --git a/lib/RuntimeTemplate.js b/lib/RuntimeTemplate.js
--- a/lib/RuntimeTemplate.js
+++ b/lib/RuntimeTemplate.js
@@ -191,16 +191,33 @@ class RuntimeTemplate {
 
 	/**
 	 * Expression for the global registry that collects CSS server-side when there
-	 * is no DOM (SSR). An SSR host reads it from `globalThis`; it is keyed by the
-	 * style/chunk identifier and namespaced by `output.uniqueName`.
+	 * is no DOM (SSR), read back through `__webpack_css_server_styles__`. It is
+	 * keyed by the style/chunk identifier and namespaced by `output.uniqueName`.
+	 * @param {boolean=} create false to read the registry without creating it
 	 * @returns {string} runtime expression evaluating to the registry object
 	 */
-	cssServerStyleRegistry() {
+	cssServerStyleRegistry(create = true) {
 		const name = this.outputOptions.uniqueName;
 		const key = JSON.stringify(
 			name ? `__webpack_css__${name}` : "__webpack_css__"
 		);
-		return `(${this.assignOr(`globalThis[${key}]`, "{}")})`;
+		const slot = `${
+			this.outputOptions.environment.globalThis
+				? "globalThis"
+				: RuntimeGlobals.global
+		}[${key}]`;
+		return `(${create ? this.assignOr(slot, "{}") : `${slot} || {}`})`;
+	}
+
+	/**
+	 * Whether `cssServerStyleRegistry()` reaches the global object through
+	 * `RuntimeGlobals.global`, which its runtime module then has to require.
+	 * @returns {boolean} true when `RuntimeGlobals.global` is needed
+	 */
+	cssServerStyleRegistryNeedsGlobal() {
+		return (
+			this.isNeutralPlatform() && !this.outputOptions.environment.globalThis
+		);
…… 其余 1,050 行从略(共改动 67 个文件)

样例 3 / 7

修复 Pydantic 装饰器收集的三个问题

  • Python
  • pydantic
  • 缺陷修复
  • 参考解 6 个文件、改动 296 行
  • MRO 遍历
  • 描述符代理解包
  • 共享状态隔离
  • 回归测试编写
  • 难点
  • 被转换的标准库数据类保持原样
  • 计算字段 setter 仍须可用
  • 保留重复序列化器的报错
  • 装饰器信息类保留 slots

让 DecoratorInfos.build() 不再改动 Pydantic 不拥有的类,每个基类只处理一次,配置只作用于正在构建的类。同时保留从根到叶的顺序与按名称覆盖。

题面(Agent 收到的原文)

prompt.md

Task description

DecoratorInfos.build() in pydantic/_internal/_decorators.py has three problems:

  • It modifies classes Pydantic doesn't own. Using a plain class with Pydantic decorators as a model base, or building a TypedDict schema, sets __pydantic_decorators__ on that class and unwraps its decorated attributes. For a TypedDict it even breaks validation: a second TypeAdapter no longer runs its validators. No such class may be modified, including a stdlib dataclass passed to pydantic.dataclasses.dataclass() (it is subclassed). Pydantic models and Pydantic dataclasses must still be unwrapped as today (computed-field setters keep working).
  • It re-processes bases recursively, so creating a model on top of a long chain of plain classes takes exponential time. Each base should be processed once per build.
  • Decorator infos are shared: config such as field_title_generator/alias_generator applied to computed fields leaks into other classes. A subclass rewrites its parent's model_computed_fields; a TypedDict used by several models shows the first model's generated titles in the others. Config must only affect the class being built.

Keep the existing semantics: root-to-leaf order, overriding by name, and multiple-field-serializers when a class adds a second serializer for a field.

题面后半:测试、lint 和代码风格规范
prompt.md(续)

Test guidelines

  • Tests use pytest and live under tests/; every tests/**/test_*.py file is collected automatically (no registration). Decorator collection is tested in tests/test_decorators.py; related behaviour lives in tests/test_types_typeddict.py, tests/test_dataclasses.py, tests/test_computed_fields.py and tests/test_validators.py.
  • Cover the fixed behaviour with regression tests that fail without your change, written like the surrounding tests (plain test_* functions exercising public behaviour where possible).
  • Run a module or a single test with uv run pytest tests/test_decorators.py (add -k <name> to select). Run the whole suite (about a minute) with ulimit -s 65536; uv run pytest. All existing tests must keep passing without being modified.
  • uv run rebuilds the local packages when needed; don't run a bare uv sync (it would uninstall the dev tools).

Lint guidelines

CI runs make lint-python and pyright. Their offline equivalents must pass:

  • uv run ruff check pydantic tests docs/plugins release/
  • uv run ruff format --check pydantic tests docs/plugins release/ (fix with uv run ruff format <files>)
  • uv run pyright pydantic

Style guidelines

  • Match the style of pydantic/_internal/_decorators.py: fully typed code, Google-style docstrings that say what a function does (keep them accurate when behaviour changes, and document new parameters and helpers), comments only where the code isn't self-explanatory.
  • Reuse the existing helpers rather than duplicating logic, and don't change the dataclass configuration of existing classes.
  • Keep the change focused on decorator collection and its call sites: no unrelated refactors or reformatting, no docs or HISTORY.md edits (the changelog is generated at release time).
  • You are already on the correct base commit. Create your branch from this commit. Do not rebase or start from master, main, or any other branch.

评分细则

# 细则 类别 检查方式 阻断项 权重
C1 不改非自有类、自有类仍解包、配置隔离、基类只处理一次 行为正确 隐藏测试 是 37%
C2 未经修改的原有测试套件仍通过 不破坏已有行为 项目命令 是 11%
C3 改动仅限装饰器收集、其调用点与测试 改动范围 改动范围检查 是 7%
C4 新增测试在基线提交上失败、改动后通过 测试质量 改前失败、改后通过 是 11%
C5 源码改动规模适度 改动范围 改动范围检查 — 2%
C6 “不改动非自有类”与“配置隔离”两项修复均有行为测试 测试质量 评审判定 — 9%
C7 ruff 代码检查与格式检查通过 构建/lint/格式 项目命令 — 3%
C8 pyright 对包的类型检查通过 构建/lint/格式 项目命令 — 3%
C9 按类扫描命名空间与按信息类型分派只写一次 代码质量与规范 评审判定 — 6%
C10 文档字符串与注释符合新行为,新参数与辅助函数有文档 代码质量与规范 评审判定 — 6%
C11 装饰器信息类保留 slots 数据类配置 代码质量与规范 项目命令 — 2%
C12 三个调用点文件共改动不超过 40 行 改动范围 项目命令 — 3%

评分器对随附解法的判定

解法 份数 得分 不计阻断项的加权得分
参考解 1 100 100
其他正确解法 3 100 100
接近完成解 1 94 94
部分完成解 3 0 31–⁠63
取巧解 6 0 46–⁠94
未改动的代码 1 0 31

满分 100。任一阻断项不过,得分即为 0。

轨迹 1

回合
114
工具调用
135
用时(分钟)
55
改动文件
7
增 / 删行数
+290 / −39

把 build() 改为单次遍历 MRO,不再改动非自有类;计算字段信息先复制再更新。仅模型和 Pydantic 数据类的调用点传 replace_wrapped=True,后者改在子类化标准库数据类之后调用。用复现脚本和 9 个新测试用例验证。

第 12 回合 用复现脚本确认第二个 TypeAdapter 不再运行 TypedDict 的校验器。

第 14 回合 统计 build() 调用次数:每多一个普通基类就翻倍,18 个时达 524,288 次。

第 31 回合 确认三个问题后定下方案:从根到叶只遍历一次 MRO、按需开启解包、按类复制装饰器信息。

第 92 回合 按现有惯例给监视 build() 调用的测试加上 thread_unsafe 标记。

第 99 回合 剖析类创建耗时后,改为只在 update_from_config 中复制计算字段信息。

第 101 回合 改完后重跑复现脚本:mixin 保留代理,第二个 TypeAdapter 仍运行校验器。

第 112 回合 改完文档字符串后重跑 ruff、pyright 和全量测试:12,070 个测试通过。

轨迹 1 的最终改动(节选)
diff --git a/pydantic/_internal/_decorators.py b/pydantic/_internal/_decorators.py
--- a/pydantic/_internal/_decorators.py
+++ b/pydantic/_internal/_decorators.py
@@ -6,10 +6,11 @@ import sys
 import types
 from collections import deque
 from collections.abc import Iterable
+from copy import copy
 from dataclasses import dataclass, field
 from functools import cached_property, partial, partialmethod
 from inspect import Parameter, Signature, isdatadescriptor, ismethoddescriptor, signature
-from itertools import islice
+from itertools import chain, islice
 from typing import TYPE_CHECKING, Any, Callable, ClassVar, Generic, Literal, TypeVar, Union
 
 from pydantic_core import PydanticUndefined, PydanticUndefinedType, core_schema
@@ -428,9 +429,9 @@ class DecoratorInfos:
     computed_fields: dict[str, Decorator[ComputedFieldInfo]] = field(default_factory=dict)
 
     @staticmethod
-    def build(model_dc: type[Any]) -> DecoratorInfos:  # noqa: C901 (ignore complexity)
+    def build(model_dc: type[Any], *, replace_wrapped: bool = False) -> DecoratorInfos:
         """We want to collect all DecFunc instances that exist as
-        attributes in the namespace of the class (a BaseModel or dataclass)
+        attributes in the namespace of the class (a BaseModel, dataclass or TypedDict)
         that called us
         But we want to collect these in the order of the bases
         So instead of getting them all from the leaf class (the class that called us),
@@ -440,13 +441,36 @@ class DecoratorInfos:
         works with inheritance.
         If we do replace any functions we put the replacement into the position
         the replaced function was in; that is, we maintain the order.
+
+        Each base is only processed once: Pydantic models and Pydantic dataclasses already
+        store their decorators (including the inherited ones) in `__pydantic_decorators__`,
+        and for the other bases (which we don't own, and as such don't modify), we collect
+        the decorators defined in their own namespace. The collected decorators are bound to
+        the class.
+
+        Args:
+            model_dc: The class to collect the decorators of.
+            replace_wrapped: Whether to replace the decorated attributes of the class by the objects they wrap,
+                which allows validator class methods to also function as regular class methods, and computed
+                field setters to be used. Decorated attributes inherited from bases that aren't Pydantic models
+                or Pydantic dataclasses are set on the class instead, leaving these bases untouched. As the
+                decorators can't be collected again from the class afterwards, this should only be enabled for
+                classes owned by Pydantic, which store the result in `__pydantic_decorators__`.
+
+        Returns:
+            The collected decorators.
         """
         # reminder: dicts are ordered and replacement does not alter the order
         res = DecoratorInfos()
-        for base in reversed(mro(model_dc)[1:]):
+        model_mro = mro(model_dc)
+        # bases might not be hashable, so we key them by id:
+        bases_decorators: dict[int, DecoratorInfos] = {}
+        for base in reversed(model_mro[1:]):
             existing: DecoratorInfos | None = base.__dict__.get('__pydantic_decorators__')
             if existing is None:
-                existing = DecoratorInfos.build(base)
+                existing = DecoratorInfos()
+                existing._add_namespace_decorators(base, bases_decorators)
+            bases_decorators[id(base)] = existing
             res.validators.update({k: v.bind_to_cls(model_dc) for k, v in existing.validators.items()})
             res.field_validators.update({k: v.bind_to_cls(model_dc) for k, v in existing.field_validators.items()})
             res.root_validators.update({k: v.bind_to_cls(model_dc) for k, v in existing.root_validators.items()})
diff --git a/pydantic/dataclasses.py b/pydantic/dataclasses.py
--- a/pydantic/dataclasses.py
+++ b/pydantic/dataclasses.py
@@ -183,8 +183,6 @@ def dataclass(
         # if config is not explicitly provided, try to read it from the type
         config_dict = config if config is not None else getattr(cls, '__pydantic_config__', None)
         config_wrapper = _config.ConfigWrapper(config_dict)
-        decorators = _decorators.DecoratorInfos.build(cls)
-        decorators.update_from_config(config_wrapper)
 
         # Keep track of the original __doc__ so that we can restore it after applying the dataclasses decorator
         # Otherwise, classes with no __doc__ will have their signature added into the JSON schema description,
@@ -205,6 +203,11 @@ def dataclass(
                 bases = bases + (generic_base,)
             cls = types.new_class(cls.__name__, bases)
 
+        # The decorators are collected after subclassing stdlib dataclasses, so that the decorated
+        # attributes are replaced on the subclass (and not on the original class):
+        decorators = _decorators.DecoratorInfos.build(cls, replace_wrapped=True)
+        decorators.update_from_config(config_wrapper)
+
         # Respect frozen setting from dataclass constructor and fallback to config setting if not provided
         if frozen is not None:
             frozen_ = frozen
diff --git a/pydantic/_internal/_model_construction.py b/pydantic/_internal/_model_construction.py
--- a/pydantic/_internal/_model_construction.py
+++ b/pydantic/_internal/_model_construction.py
@@ -172,7 +172,7 @@ class ModelMetaclass(ABCMeta):
 
             cls.__pydantic_setattr_handlers__ = {}
 
-            cls.__pydantic_decorators__ = DecoratorInfos.build(cls)
+            cls.__pydantic_decorators__ = DecoratorInfos.build(cls, replace_wrapped=True)
             cls.__pydantic_decorators__.update_from_config(config_wrapper)
 
             # Use the getattr below to grab the __parameters__ from the `typing.Generic` parent class
diff --git a/tests/test_computed_fields.py b/tests/test_computed_fields.py
--- a/tests/test_computed_fields.py
+++ b/tests/test_computed_fields.py
@@ -853,3 +853,24 @@ def test_computed_fields_exclude() -> None:
     assert m.model_dump(mode='json', exclude_computed_fields=True) == {}
     assert m.model_dump_json() == '{"prop":1}'
     assert m.model_dump_json(exclude_computed_fields=True) == '{}'
+
+
+def test_computed_field_config_not_shared_with_parent() -> None:
+    class Parent(BaseModel):
+        model_config = {'defer_build': True}
+
+        @computed_field
+        @property
+        def my_field(self) -> int:
+            return 1
+
+    class Child(Parent):
+        model_config = {'alias_generator': to_camel, 'field_title_generator': lambda name, _: name.upper()}
+
+    assert Parent.model_computed_fields['my_field'].alias is None
+    assert Parent.model_computed_fields['my_field'].title is None
+    assert Parent().model_dump(by_alias=True) == {'my_field': 1}
+
+    assert Child.model_computed_fields['my_field'].alias == 'myField'
+    assert Child.model_computed_fields['my_field'].title == 'MY_FIELD'
…… 其余 340 行从略(共改动 7 个文件)

轨迹 2

回合
115
工具调用
131
用时(分钟)
71
改动文件
8
增 / 删行数
+336 / −53

把 build() 改为单次遍历 MRO,不再改动非自有类;计算字段信息改为写时复制。仅模型和 Pydantic 数据类的调用点传 unwrap_attributes=True。新增 10 个测试用例;临时去掉继承属性解包或信息复制,测试都会失败。

第 10 回合 用一个临时脚本复现全部三个问题,其中 18 个普通基类组成的链约需 1.5 秒。

第 19 回合 发现 object.__setattr__ 把代理视为非数据描述符,影响标准库数据类转换。

第 24 回合 记录边界情况的基线行为:重复序列化器检查只与自身祖先比较、不涉及兄弟基类,决定保留。

第 93 回合 性能剖析后将辅助方法改为 _add_from_namespace,按需收集继承的序列化器。

第 100 回合 让 _update_from_config 只在确实需要改动时才返回副本,否则返回原对象。

第 108 回合 临时去掉继承属性的解包、再去掉信息复制,分别有 1 个和 3 个测试失败,随后恢复原文件。

第 114 回合 最后一次全量运行 12,071 个测试通过后,在新分支上提交改动。

轨迹 2 的最终改动(节选)
diff --git a/pydantic/_internal/_decorators.py b/pydantic/_internal/_decorators.py
--- a/pydantic/_internal/_decorators.py
+++ b/pydantic/_internal/_decorators.py
@@ -428,25 +428,44 @@ class DecoratorInfos:
     computed_fields: dict[str, Decorator[ComputedFieldInfo]] = field(default_factory=dict)
 
     @staticmethod
-    def build(model_dc: type[Any]) -> DecoratorInfos:  # noqa: C901 (ignore complexity)
-        """We want to collect all DecFunc instances that exist as
-        attributes in the namespace of the class (a BaseModel or dataclass)
-        that called us
-        But we want to collect these in the order of the bases
-        So instead of getting them all from the leaf class (the class that called us),
-        we traverse the bases from root (the oldest ancestor class) to leaf
-        and collect all of the instances as we go, taking care to replace
-        any duplicate ones with the last one we see to mimic how function overriding
-        works with inheritance.
-        If we do replace any functions we put the replacement into the position
-        the replaced function was in; that is, we maintain the order.
+    def build(model_dc: type[Any], *, unwrap_attributes: bool = False) -> DecoratorInfos:
+        """Collect the decorators of a class (a model, dataclass or typed dictionary), including the inherited ones.
+
+        We want to collect the decorators in the order of the bases. So instead of getting them all from the
+        class itself, we traverse the bases from root (the oldest ancestor class) to leaf, processing each base once,
+        and collect the decorators as we go, taking care to replace any duplicate ones with the last one we see to
+        mimic how function overriding works with inheritance. If we do replace any decorators we put the replacement
+        into the position the replaced decorator was in; that is, we maintain the order.
+
+        Pydantic models and dataclasses provide their collected decorators (which include the inherited ones) as
+        `__pydantic_decorators__`, while the decorators of other classes are collected from their namespace. Classes
+        not owned by Pydantic (e.g. typed dictionaries, stdlib dataclasses or plain classes used as bases) are left
+        untouched.
+
+        Args:
+            model_dc: The class to collect the decorators of.
+            unwrap_attributes: Whether to replace the decorated attributes of the class by the objects they wrap,
+                which allows validator class methods to also function as regular class methods and computed field
+                setters to be used. Decorated attributes inherited from classes not owned by Pydantic are replaced
+                on the class as well. As the class is modified, this must only be used for Pydantic models and
+                dataclasses.
+
+        Returns:
+            The decorators of the class, bound to it.
         """
         # reminder: dicts are ordered and replacement does not alter the order
         res = DecoratorInfos()
-        for base in reversed(mro(model_dc)[1:]):
+        model_dc_mro = mro(model_dc)
+        # the decorators provided by each base:
+        bases_decorators: dict[Any, DecoratorInfos] = {}
+        # the names of the decorated attributes defined in the namespace of the class and of the bases we don't own:
+        decorated_names: list[str] = []
+        for base in reversed(model_dc_mro[1:]):
             existing: DecoratorInfos | None = base.__dict__.get('__pydantic_decorators__')
             if existing is None:
-                existing = DecoratorInfos.build(base)
+                existing = DecoratorInfos()
+                decorated_names += existing._add_from_namespace(base, bases_decorators)
+            bases_decorators[base] = existing
             res.validators.update({k: v.bind_to_cls(model_dc) for k, v in existing.validators.items()})
             res.field_validators.update({k: v.bind_to_cls(model_dc) for k, v in existing.field_validators.items()})
             res.root_validators.update({k: v.bind_to_cls(model_dc) for k, v in existing.root_validators.items()})
@@ -455,26 +474,55 @@ class DecoratorInfos:
             res.model_validators.update({k: v.bind_to_cls(model_dc) for k, v in existing.model_validators.items()})
             res.computed_fields.update({k: v.bind_to_cls(model_dc) for k, v in existing.computed_fields.items()})
 
-        to_replace: list[tuple[str, Any]] = []
+        decorated_names += res._add_from_namespace(model_dc, bases_decorators)
+
+        if unwrap_attributes:
+            for var_name in decorated_names:
+                # the decorated attribute is either defined on the class or inherited from a class we don't own:
+                attribute = next(base.__dict__[var_name] for base in model_dc_mro if var_name in base.__dict__)
+                if isinstance(attribute, PydanticDescriptorProxy):
+                    setattr(model_dc, var_name, attribute.wrapped)
+        return res
+
+    def _add_from_namespace(self, tp: type[Any], bases_decorators: dict[Any, DecoratorInfos]) -> list[str]:
+        """Add the decorators defined in the namespace of a class (i.e. not the inherited ones), bound to the class.
+
+        Args:
+            tp: The class to collect the decorators of.
+            bases_decorators: The decorators provided by each base of the class, used to check that each field
+                has at most one serializer function.
+
+        Returns:
+            The names of the decorated attributes defined in the namespace of the class.
 
-        for var_name, var_value in vars(model_dc).items():
+        Raises:
+            PydanticUserError: If the class defines a serializer function for a field that already has one.
+        """
+        decorated_names: list[str] = []
+        # the field serializers of the class, including the inherited ones (only computed if needed):
+        field_serializers: dict[str, Decorator[FieldSerializerDecoratorInfo]] | None = None
+        for var_name, var_value in vars(tp).items():
             if isinstance(var_value, PydanticDescriptorProxy):
                 info = var_value.decorator_info
                 if isinstance(info, ValidatorDecoratorInfo):
-                    res.validators[var_name] = Decorator.build(
-                        model_dc, cls_var_name=var_name, shim=var_value.shim, info=info
+                    self.validators[var_name] = Decorator.build(
+                        tp, cls_var_name=var_name, shim=var_value.shim, info=info
                     )
                 elif isinstance(info, FieldValidatorDecoratorInfo):
-                    res.field_validators[var_name] = Decorator.build(
-                        model_dc, cls_var_name=var_name, shim=var_value.shim, info=info
+                    self.field_validators[var_name] = Decorator.build(
+                        tp, cls_var_name=var_name, shim=var_value.shim, info=info
                     )
                 elif isinstance(info, RootValidatorDecoratorInfo):
-                    res.root_validators[var_name] = Decorator.build(
-                        model_dc, cls_var_name=var_name, shim=var_value.shim, info=info
+                    self.root_validators[var_name] = Decorator.build(
+                        tp, cls_var_name=var_name, shim=var_value.shim, info=info
                     )
                 elif isinstance(info, FieldSerializerDecoratorInfo):
+                    if field_serializers is None:
+                        field_serializers = {}
+                        for base in reversed(mro(tp)[1:]):
+                            field_serializers.update(bases_decorators[base].field_serializers)
                     # check whether a serializer function is already registered for fields
-                    for field_serializer_decorator in res.field_serializers.values():
+                    for field_serializer_decorator in field_serializers.values():
                         # check that each field has at most one serializer function.
                         # serializer functions for the same field in subclasses are allowed,
                         # and are treated as overrides
…… 其余 411 行从略(共改动 8 个文件)

样例 4 / 7

为 spdlog 文件 sink 添加缓冲大小选项

  • C++
  • spdlog
  • 新功能
  • 参考解 8 个文件、改动 195 行
  • stdio 缓冲
  • 缓冲区生命周期管理
  • 线程安全
  • API 向后兼容
  • Catch2 单元测试
  • 难点
  • 大小须真正生效,不能只存数值
  • 运行中改大小不丢失、不损坏输出
  • 轮转、截断后重开的文件仍须缓冲
  • 易漏改按 strftime 格式命名文件的 daily 工厂函数

为 basic、rotating、daily 文件 sink 的构造函数和工厂函数加尾部参数 buffer_size,并给 sink 加访问器 set_buffer_size() 与 buffer_size();0 保持现有行为。sink 写日志期间设非零值须立即生效,且不丢失已缓冲的输出。

题面(Agent 收到的原文)

prompt.md

Task description

The file sinks always write through the C runtime's default stdio buffer, which is only a few KB. Users who log at high volume want a larger buffer to cut down on write calls, and today they have to install one on the sink's FILE themselves with setvbuf. Please make the buffer size a proper option of the basic, rotating and daily file sinks.

  • The constructors of basic_file_sink, rotating_file_sink and daily_file_sink, and their logger factory functions, take a new trailing parameter size_t buffer_size = 0, placed after event_handlers. 0 keeps today's behaviour. A non-zero value makes the files the sink writes fully buffered with a buffer of that many bytes: output reaches the file only when the buffer fills up, the sink is flushed or the file is closed.
  • The three sink classes get void set_buffer_size(size_t buffer_size) and size_t buffer_size(). The getter returns the configured value (0 by default). The setter can be called while the sink is logging: a non-zero size takes effect right away, without losing output buffered so far; going back to 0 may wait until the sink opens its next file.

Existing code that uses these sinks must keep compiling and behaving exactly as it does now.

题面后半:测试、lint 和代码风格规范
prompt.md(续)

Test guidelines

  • Unit tests are Catch2 v3 cases compiled into one binary, spdlog-utests, from the sources listed in tests/CMakeLists.txt. File sink tests live in tests/test_file_logging.cpp (basic and rotating sinks), tests/test_daily_logger.cpp (daily sink) and tests/test_file_helper.cpp (details::file_helper); add the tests for this change there. Use the helpers in tests/utils.h (prepare_logdir(), file_contents(), get_filesize(), ...) and write log files under test_logs/.
  • The Windows CI builds the tests with SPDLOG_WCHAR_FILENAMES=ON, where spdlog::filename_t is std::wstring: pass file names to spdlog as SPDLOG_FILENAME_T(...), and convert a filename_t you get back from spdlog with spdlog::details::os::filename_to_str() before handing it to the test helpers.
  • Build and run the file sink tests (from the repository root):
    cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_STANDARD=20 -DSPDLOG_BUILD_EXAMPLE=ON -DSPDLOG_BUILD_EXAMPLE_HO=ON -DSPDLOG_BUILD_WARNINGS=ON -DSPDLOG_BUILD_TESTS=ON && cmake --build build -j8 --target spdlog-utests
    cd build/tests && ./spdlog-utests --order decl --filenames-as-tags "[#test_file_logging],[#test_daily_logger],[#test_file_helper]"
    
  • The whole suite must stay green: cmake --build build -j8 && ctest --test-dir build -j4 --output-on-failure.
  • spdlog is also used header-only with a C++11 compiler. Check that configuration too: cmake -S . -B build/ho11 -DCMAKE_CXX_STANDARD=11 -DSPDLOG_BUILD_WARNINGS=ON -DSPDLOG_BUILD_TESTS=OFF -DSPDLOG_BUILD_TESTS_HO=ON && cmake --build build/ho11 -j8 && cd build/ho11/tests && ./spdlog-utests-ho --order decl

Lint guidelines

  • Every build above uses -DSPDLOG_BUILD_WARNINGS=ON (-Wall -Wextra -Wconversion -pedantic -Werror): library, examples and tests must compile without warnings.
  • The project's clang-tidy run must not report errors: cmake -S . -B build/tidy -DSPDLOG_TIDY=ON && cmake --build build/tidy -j8 --clean-first (findings are printed as warnings; the ones in example/example.cpp already exist).
  • Formatting follows .clang-format (Google based, 4-space indent, 100 columns; clang-format 14 is installed). Format the code you write, but do not reformat code you did not change: the tree is not clang-format clean.

Style guidelines

  • Follow the conventions of the existing file sinks and of details::file_helper: public sink methods that touch the file take the sink's mutex, out-of-class definitions in -inl.h files are marked SPDLOG_INLINE, and failures are reported with throw_spdlog_ex(...).
  • Keep the public API source compatible and keep the change focused: no unrelated refactors or reformatting. The project keeps no changelog, and README.md is curated by the maintainer and is not edited in feature changes like this one; if you want to show the new option, extend example/example.cpp.
  • You are already on the correct base commit. Create your branch from this commit. Do not rebase or start from master, main, or any other branch.

评分细则

# 细则 类别 检查方式 阻断项 权重
C1 basic、rotating、daily sink 的构造函数和工厂函数都带尾部参数 buffer_size,sink 提供 set_buffer_size() / buffer_size() 行为正确 隐藏测试 是 13%
C2 buffer_size 真正决定缓冲:构造、轮转/截断/按日切换、0 保持默认、运行时修改 行为正确 隐藏测试 是 22%
C3 无回归:编译无警告;C++11 仅头文件构建下原有测试通过、模板可编译 不破坏已有行为 项目命令 是 10%
C4 只改动属于该功能的文件 改动范围 改动范围检查 是 7%
C5 新测试在基线提交上失败、加入改动后通过,Catch2 断言未被改写 测试质量 改前失败、改后通过 是 8%
C6 sink 只做转发:缓冲区在 details::file_helper 中,set_buffer_size() 先取 sink 互斥锁再转发 代码质量与规范 评审判定 是 11%
C7 Windows CI 开启 SPDLOG_WCHAR_FILENAMES 时,新测试和库中新增的消息仍可编译 测试质量 评审判定 是 6%
C8 setvbuf 失败须上报或转入有文档说明的回退,不可忽略 代码质量与规范 评审判定 — 6%
C9 流仍在使用缓冲区期间不释放其内存(含运行中改大小) 代码质量与规范 评审判定 — 6%
C10 项目的 clang-tidy 构建(C++11,库与示例)无错误完成 构建/lint/格式 项目命令 — 4%
C11 无调试残留、注释掉的代码、叙述改动过程的注释、无关格式改动 构建/lint/格式 评审判定 — 4%
C12 改动规模适度(非测试代码的行数与文件数) 改动范围 改动范围检查 — 3%

评分器对随附解法的判定

解法 份数 得分 不计阻断项的加权得分
参考解 1 100 100
其他正确解法 2 100 100
接近完成解 1 89 89
部分完成解 3 0 24–⁠59
取巧解 11 0 42–⁠94
未改动的代码 1 0 24

满分 100。任一阻断项不过,得分即为 0。

轨迹 1

回合
65
工具调用
91
用时(分钟)
42
改动文件
11
增 / 删行数
+479 / −30

用探针确认 glibc 在 setvbuf 的缓冲区参数为空指针时忽略大小,因此由 file_helper 自备缓冲区并在每次打开文件时装上,三种 sink 持锁转发;运行中切换时先 flush(),再做一次不移动读写位置的 fseek(偏移 0),然后才调用 setvbuf。新增 11 个用例,用三种变异确认它们能发现缺陷,并在 ASan 构建和 C++11 仅头文件构建下运行整套测试。

第 11 回合 用探针检验 glibc:setvbuf 的缓冲区参数为空指针时只用 4096 字节而非 64K,运行中切换后首条输出会被提前写出。

第 13 回合 试验先做不移动位置的 fseek 再调用 setvbuf:已写过的流仍保持缓冲;对管道 fseek 会失败,但无害。

第 18 回合 实现时把 setvbuf 放在 after_open 回调之前;运行中改大小时,等切换成功后才释放旧缓冲区。

第 40 回合 变异测试:把两处 setvbuf 的缓冲区参数改成 nullptr,加强后的两个 daily 测试都随之失败。

第 45 回合 变异测试:大小改回 0 时立即释放仍在用的缓冲区;ASan 没有发现,Release 构建下的测试以 SIGSEGV 崩溃。

第 49 回合 用 git stash 撤下改动,在基线上跑同样带头文件过滤的 clang-tidy,与改动后的告警逐条对比。

第 55 回合 终审 diff 时订正一处注释:MSVC 拒绝小于 2 字节的缓冲区,musl 则忽略小于 8 字节的缓冲区。

轨迹 1 的最终改动(节选)
diff --git a/include/spdlog/details/file_helper-inl.h b/include/spdlog/details/file_helper-inl.h
--- a/include/spdlog/details/file_helper-inl.h
+++ b/include/spdlog/details/file_helper-inl.h
@@ -12,20 +12,29 @@
 
 #include <cerrno>
 #include <cstdio>
+#include <limits>
 #include <string>
 #include <tuple>
 
 SPDLOG_NAMESPACE_BEGIN
 namespace details {
 
-SPDLOG_INLINE file_helper::file_helper(const file_event_handlers &event_handlers)
-    : event_handlers_(event_handlers) {}
+SPDLOG_INLINE file_helper::file_helper(const file_event_handlers &event_handlers,
+                                       size_t buffer_size)
+    : event_handlers_(event_handlers) {
+    set_buffer_size(buffer_size);
+}
 
 SPDLOG_INLINE file_helper::~file_helper() { close(); }
 
 SPDLOG_INLINE void file_helper::open(const filename_t &fname, bool truncate) {
     close();
     filename_ = fname;
+    // no file uses the buffer now: size it for the new file.
+    const auto alloc_size = buffer_alloc_size_(buffer_size_);
+    if (buffer_.size() != alloc_size) {
+        buffer_ = std::vector<char>(alloc_size);
+    }
 
     auto *mode = SPDLOG_FILENAME_T("ab");
     auto *trunc_mode = SPDLOG_FILENAME_T("wb");
@@ -48,6 +57,13 @@ SPDLOG_INLINE void file_helper::open(const filename_t &fname, bool truncate) {
             std::fclose(tmp);
         }
         if (!os::fopen_s(&fd_, fname, mode)) {
+            // setvbuf() must come before any other operation on the file, after_open's included.
+            if (!buffer_.empty() &&
+                std::setvbuf(fd_, buffer_.data(), _IOFBF, buffer_.size()) != 0) {
+                std::fclose(fd_);
+                fd_ = nullptr;
+                throw_spdlog_ex("Failed setting buffer of file " + os::filename_to_str(filename_));
+            }
             if (event_handlers_.after_open) {
                 event_handlers_.after_open(filename_, fd_);
             }
@@ -114,6 +130,36 @@ SPDLOG_INLINE size_t file_helper::size() const {
 
 SPDLOG_INLINE const filename_t &file_helper::filename() const { return filename_; }
 
+SPDLOG_INLINE void file_helper::set_buffer_size(size_t buffer_size) {
+    // MSVC and the BSD libcs (e.g. macOS) limit setvbuf() to INT_MAX bytes
+    if (buffer_size > static_cast<size_t>((std::numeric_limits<int>::max)())) {
+        throw_spdlog_ex("Invalid file buffer size " + std::to_string(buffer_size) +
+                        ": cannot exceed INT_MAX");
+    }
+    const auto alloc_size = buffer_alloc_size_(buffer_size);
+    if (fd_ != nullptr && alloc_size != 0 && alloc_size != buffer_.size()) {
+        std::vector<char> buffer(alloc_size);
+        flush();
+        // On a stream that was written to, glibc stays in write mode across setvbuf() with an
+        // empty put area, so the second write after it flushes the first one. A no-op seek leaves
+        // write mode (even where seeking fails, e.g. on pipes) and avoids that.
+        (void)std::fseek(fd_, 0, SEEK_CUR);
+        if (std::setvbuf(fd_, buffer.data(), _IOFBF, buffer.size()) != 0) {
+            throw_spdlog_ex("Failed setting buffer of file " + os::filename_to_str(filename_));
+        }
+        // the old buffer is released only now that the file no longer uses it.
+        buffer_.swap(buffer);
+    }
+    buffer_size_ = buffer_size;
+}
+
+SPDLOG_INLINE size_t file_helper::buffer_size() const { return buffer_size_; }
+
+// setvbuf() rejects buffers smaller than 2 bytes on MSVC and ignores those smaller than 8 on musl
+SPDLOG_INLINE size_t file_helper::buffer_alloc_size_(size_t buffer_size) {
+    return buffer_size == 0 || buffer_size >= 8 ? buffer_size : 8;
+}
+
 //
 // return file path and its extension:
 //
diff --git a/include/spdlog/sinks/daily_file_sink.h b/include/spdlog/sinks/daily_file_sink.h
--- a/include/spdlog/sinks/daily_file_sink.h
+++ b/include/spdlog/sinks/daily_file_sink.h
@@ -62,6 +62,8 @@ struct daily_filename_format_calculator {
  * Rotating file sink based on date.
  * If truncate != false , the created file will be truncated.
  * If max_files > 0, retain only the last max_files and delete previous.
+ * If buffer_size > 0, the files are fully buffered with a buffer of buffer_size bytes: output
+ * reaches a file when the buffer fills up, on flush or when the file is closed.
  * Note that old log files from previous executions will not be deleted by this class,
  * rotation and deletion is only applied while the program is running.
  */
@@ -74,11 +76,12 @@ public:
                     int rotation_minute,
                     bool truncate = false,
                     uint16_t max_files = 0,
-                    const file_event_handlers &event_handlers = {})
+                    const file_event_handlers &event_handlers = {},
+                    size_t buffer_size = 0)
         : base_filename_(std::move(base_filename)),
           rotation_h_(rotation_hour),
           rotation_m_(rotation_minute),
-          file_helper_{event_handlers},
+          file_helper_{event_handlers, buffer_size},
           truncate_(truncate),
           max_files_(max_files),
           filenames_q_() {
diff --git a/include/spdlog/sinks/basic_file_sink-inl.h b/include/spdlog/sinks/basic_file_sink-inl.h
--- a/include/spdlog/sinks/basic_file_sink-inl.h
+++ b/include/spdlog/sinks/basic_file_sink-inl.h
@@ -16,8 +16,9 @@ namespace sinks {
 template <typename Mutex>
 SPDLOG_INLINE basic_file_sink<Mutex>::basic_file_sink(const filename_t &filename,
                                                       bool truncate,
-                                                      const file_event_handlers &event_handlers)
-    : file_helper_{event_handlers} {
+                                                      const file_event_handlers &event_handlers,
+                                                      size_t buffer_size)
+    : file_helper_{event_handlers, buffer_size} {
     file_helper_.open(filename, truncate);
 }
 
…… 其余 678 行从略(共改动 11 个文件)

轨迹 2

回合
76
工具调用
90
用时(分钟)
42
改动文件
11
增 / 删行数
+486 / −31

把缓冲区放在 details::file_helper,在 after_open 之前装上,流不再使用后才释放;大小为 1 或超过 INT_MAX 时抛异常,三种 sink 持锁转发。新增 11 个用例,其中一个在另一线程写日志时改大小;用变异、ASan 与 TSan 构建、C++11 仅头文件构建以及新用例连跑 30 轮来检验。

第 7 回合 读完三种 sink、file_helper 和相关测试后,决定把缓冲逻辑集中在 file_helper,经 setvbuf 实现,sink 只做转发。

第 15 回合 规定大小为 1 或超过 INT_MAX 时抛异常,因为 Windows C 运行库拒绝这些值时默认会终止程序。

第 29 回合 新增多线程测试:一个线程写 2,000 条日志,主线程在 0、1KB、2KB 间循环改大小,最后核对完整的文件内容。

第 47 回合 变异测试:让 file_helper::set_buffer_size() 在大小设为 0 时释放仍在用的缓冲区,文件内容损坏,3 个用例失败,含多线程用例。

第 59 回合 作为变异去掉 basic_file_sink::set_buffer_size() 里的锁,TSan 构建下的多线程测试中止。

第 72 回合 重新构建并运行 C++11 仅头文件测试,再把 11 个新用例连跑 30 遍,检查多线程和依赖时钟的用例是否稳定。

第 75 回合 把改动的 11 个文件作为一个提交,放在 file-sink-buffer-size 分支上。

轨迹 2 的最终改动(节选)
diff --git a/include/spdlog/details/file_helper-inl.h b/include/spdlog/details/file_helper-inl.h
--- a/include/spdlog/details/file_helper-inl.h
+++ b/include/spdlog/details/file_helper-inl.h
@@ -12,14 +12,18 @@
 
 #include <cerrno>
 #include <cstdio>
+#include <limits>
 #include <string>
 #include <tuple>
 
 SPDLOG_NAMESPACE_BEGIN
 namespace details {
 
-SPDLOG_INLINE file_helper::file_helper(const file_event_handlers &event_handlers)
-    : event_handlers_(event_handlers) {}
+SPDLOG_INLINE file_helper::file_helper(const file_event_handlers &event_handlers,
+                                       size_t buffer_size)
+    : event_handlers_(event_handlers) {
+    set_buffer_size(buffer_size);
+}
 
 SPDLOG_INLINE file_helper::~file_helper() { close(); }
 
@@ -48,6 +52,9 @@ SPDLOG_INLINE void file_helper::open(const filename_t &fname, bool truncate) {
             std::fclose(tmp);
         }
         if (!os::fopen_s(&fd_, fname, mode)) {
+            if (buffer_size_ > 0) {
+                install_buffer_(buffer_size_);
+            }
             if (event_handlers_.after_open) {
                 event_handlers_.after_open(filename_, fd_);
             }
@@ -88,6 +95,7 @@ SPDLOG_INLINE void file_helper::close() {
 
         std::fclose(fd_);
         fd_ = nullptr;
+        buffer_.reset();
 
         if (event_handlers_.after_close) {
             event_handlers_.after_close(filename_);
@@ -114,6 +122,39 @@ SPDLOG_INLINE size_t file_helper::size() const {
 
 SPDLOG_INLINE const filename_t &file_helper::filename() const { return filename_; }
 
+SPDLOG_INLINE void file_helper::set_buffer_size(size_t buffer_size) {
+    // accept the sizes setvbuf() supports everywhere: the Windows C runtime rejects sizes out of
+    // [2, INT_MAX] through its invalid parameter handler, which terminates the program by default.
+    if (buffer_size == 1 || buffer_size > static_cast<size_t>((std::numeric_limits<int>::max)())) {
+        throw_spdlog_ex("Invalid file buffer size " + std::to_string(buffer_size) +
+                        ": must be 0 or between 2 and INT_MAX");
+    }
+    if (fd_ != nullptr && buffer_size > 0) {
+        // write out what the current buffer holds before replacing it, then take the stream out
+        // of writing mode with a seek: otherwise glibc doesn't set up the new buffer for writing,
+        // and writes out early the first output that goes into it. the seek fails on files that
+        // are not seekable (e.g. pipes), which doesn't matter here.
+        flush();
+        (void)std::fseek(fd_, 0, SEEK_CUR);
+        install_buffer_(buffer_size);
+    }
+    buffer_size_ = buffer_size;
+}
+
+SPDLOG_INLINE size_t file_helper::buffer_size() const { return buffer_size_; }
+
+// Make the open file fully buffered, with a new buffer of the given size.
+// The file must not hold buffered output: it was just opened, or flushed by set_buffer_size().
+SPDLOG_INLINE void file_helper::install_buffer_(size_t buffer_size) {
+    std::unique_ptr<char[]> buffer(new char[buffer_size]);
+    if (std::setvbuf(fd_, buffer.get(), _IOFBF, buffer_size) != 0) {
+        throw_spdlog_ex("Failed setting the buffer of file " + os::filename_to_str(filename_),
+                        errno);
+    }
+    // the previous buffer, if any, is not used by the file anymore
+    buffer_ = std::move(buffer);
+}
+
 //
 // return file path and its extension:
 //
diff --git a/include/spdlog/sinks/daily_file_sink.h b/include/spdlog/sinks/daily_file_sink.h
--- a/include/spdlog/sinks/daily_file_sink.h
+++ b/include/spdlog/sinks/daily_file_sink.h
@@ -74,11 +74,12 @@ public:
                     int rotation_minute,
                     bool truncate = false,
                     uint16_t max_files = 0,
-                    const file_event_handlers &event_handlers = {})
+                    const file_event_handlers &event_handlers = {},
+                    size_t buffer_size = 0)
         : base_filename_(std::move(base_filename)),
           rotation_h_(rotation_hour),
           rotation_m_(rotation_minute),
-          file_helper_{event_handlers},
+          file_helper_{event_handlers, buffer_size},
           truncate_(truncate),
           max_files_(max_files),
           filenames_q_() {
@@ -102,6 +103,17 @@ public:
         return file_helper_.filename();
     }
 
+    // see details::file_helper::set_buffer_size()
+    void set_buffer_size(size_t buffer_size) {
+        std::lock_guard<Mutex> lock(base_sink<Mutex>::mutex_);
+        file_helper_.set_buffer_size(buffer_size);
+    }
+
+    size_t buffer_size() {
+        std::lock_guard<Mutex> lock(base_sink<Mutex>::mutex_);
+        return file_helper_.buffer_size();
+    }
+
 protected:
     void sink_it_(const details::log_msg &msg) override {
         auto time = msg.time;
diff --git a/include/spdlog/sinks/basic_file_sink-inl.h b/include/spdlog/sinks/basic_file_sink-inl.h
--- a/include/spdlog/sinks/basic_file_sink-inl.h
+++ b/include/spdlog/sinks/basic_file_sink-inl.h
@@ -16,8 +16,9 @@ namespace sinks {
 template <typename Mutex>
 SPDLOG_INLINE basic_file_sink<Mutex>::basic_file_sink(const filename_t &filename,
                                                       bool truncate,
-                                                      const file_event_handlers &event_handlers)
-    : file_helper_{event_handlers} {
+                                                      const file_event_handlers &event_handlers,
+                                                      size_t buffer_size)
+    : file_helper_{event_handlers, buffer_size} {
…… 其余 664 行从略(共改动 11 个文件)

样例 5 / 7

新增 Endpoint(),预知目标端点

  • Go
  • fiber
  • 新功能
  • 参考解 6 个文件、改动 387 行
  • Go 路由器内部机制
  • 无副作用的前瞻查询
  • 路由边界用例测试
  • 守住热路径性能
  • API 参考文档
  • 难点
  • 错误处理器重放时返回 nil
  • 须随方法覆盖、路径覆盖和 RestartRouting 变化
  • 不得指向链上已越过的端点
  • 复用路由匹配逻辑,但不改路由循环

为 Ctx 和 Req 增加 Endpoint(),让中间件在 Next() 前得知将运行的端点(无则为 nil),并补充文档。结果须与路由器一致且无副作用。

题面(Agent 收到的原文)

prompt.md

Task description

Middleware that authorizes or audits by route (RBAC keyed on Route.Name, audit logs keyed on the route pattern) has no way to learn which route will handle a request before it calls c.Next(): until then c.Route() is the middleware's own route.

Add Endpoint() *Route to Ctx, and mirror it on Req the way Route() is mirrored. It returns the endpoint (a route registered with Get, Post, Add, ..., never one registered with Use) that Next() will reach from the current position in the handler chain. It is a pure look-ahead: it runs nothing and leaves the request's state alone (handler position, Route(), Matched(), route params). It never names an endpoint the chain has already passed, and once the chain is on an endpoint (any of its handlers) it returns that endpoint.

The answer has to agree with the router: whatever Endpoint() names is the endpoint whose handlers the router then runs, and it returns nil whenever the router will run no endpoint for the request (404, 405, ...). Routes registered through Domain check the hostname inside those handlers, so report them as the router reaches them; host-aware filtering is out of scope.

Document the method in the Ctx API reference, docs/api/ctx.md.

题面后半:测试、lint 和代码风格规范
prompt.md(续)

Test guidelines

  • Tests are plain Go tests in package fiber next to the code: Ctx behaviour is tested in ctx_test.go, router behaviour in router_test.go. Add your tests there rather than in new files.
  • Follow the surrounding tests: drive requests through app.Test(httptest.NewRequest(...)), assert with testify's require, and close response bodies.
  • Per AGENTS.md, call t.Parallel() at the start of every test and subtest.
  • Cover the new behaviour, including the cases where no endpoint will run; feature PRs without unit tests are not merged (.github/CONTRIBUTING.md).
  • Run the relevant tests with go test -race -count=1 -run 'Endpoint' . and the whole suite with make test (gotestsum with -race -count=1 -shuffle=on).

Lint guidelines

  • make lint (golangci-lint v2.12.2 with the repository's .golangci.yml, which also enforces gofmt/gofumpt/goimports) must report 0 issues. Mind the strict errcheck settings described in AGENTS.md: never discard an error or a type-assertion result with _.
  • Format with make format or golangci-lint fmt; golangci-lint fmt --diff must print nothing.
  • go vet ./... must pass, and Markdown must pass make markdown (markdownlint-cli2 "**/*.md" "#vendor"). The govulncheck step of make audit needs network access and cannot run here.
  • ctx_interface_gen.go and req_interface_gen.go are generated by ifacemaker from the //go:generate directives in ctx.go, req.go and res.go: after adding methods, run make generate (or go generate ./...) instead of editing them by hand.
  • make betteralign is not clean at this commit (it reorders deliberately ordered structs in path.go and router.go); do not commit its output.

Style guidelines

  • Match the surrounding code: every exported method has a doc comment (ifacemaker copies it into the generated interfaces); reuse the package's existing helpers and state instead of re-implementing their logic; prefer github.com/gofiber/utils/v2 helpers where they fit.
  • The router loop and the per-request setup are hot paths (AGENTS.md: "Protect hot paths from regressions"): the look-ahead must cost nothing for requests that never call it. Leave App.next/nextCustom, the request handlers, context acquire/Reset/release, Next/RestartRouting and the path and method overrides as they are, apart from resetting a field.
  • docs/api/ctx.md documents each method in its own ### section, in alphabetical order within its part of the page, with Signature and Example code blocks.
  • Keep the change focused on this feature: put the code in the existing files it extends (ctx.go, req.go, or router.go for router-side helpers) rather than new source files, make no drive-by refactors, and leave other docs pages alone beyond the API reference (a line in the new-methods list of docs/whats_new.md is fine).
  • You are already on the correct base commit. Create your branch from this commit. Do not rebase or start from master, main, or any other branch.

评分细则

# 细则 类别 检查方式 阻断项 权重
C1 Ctx / Req 返回前方或当前端点,404/405 为 nil,无副作用 行为正确 隐藏测试 是 18%
C2 与路由器一致,含错误重放、覆盖与 RestartRouting、已越过路由 行为正确 隐藏测试 是 18%
C3 原有测试套件仍然通过 不破坏已有行为 项目命令 是 8%
C4 golangci-lint、格式化、go vet 与 markdownlint 均无问题 构建/lint/格式 项目命令 是 5%
C5 改动只落在该功能所属的文件中 改动范围 改动范围检查 是 5%
C6 附带 Endpoint 测试且全部通过 测试质量 改前失败、改后通过 是 7%
C7 Endpoint 写入 ctx.md 文档 代码质量与规范 项目命令 是 5%
C8 生成的接口文件与 go generate 一致 构建/lint/格式 项目命令 — 3%
C9 新测试与子测试首先调用 t.Parallel() 测试质量 隐藏测试 — 2%
C10 Endpoint 文档章节按字母序排列 代码质量与规范 项目命令 — 2%
C11 路由循环、请求处理器和上下文初始化不变,复位字段除外 不破坏已有行为 项目命令 是 6%
C12 前瞻复用路由器匹配逻辑,不另写匹配器 代码质量与规范 评审判定 — 6%
C13 文档含签名、中间件示例、nil 情形及与 Route 的区别 代码质量与规范 评审判定 — 4%
C14 新增测试锁定朴素前瞻与路由器分歧的情形 测试质量 评审判定 — 5%
C15 导出方法文档注释写明契约;无调试输出、叙述改动的注释 代码质量与规范 评审判定 — 4%
C16 非测试改动规模适度 改动范围 改动范围检查 — 2%

评分器对随附解法的判定

解法 份数 得分 不计阻断项的加权得分
参考解 1 100 100
其他正确解法 2 100 100
接近完成解 1 98 98
部分完成解 3 0 28–⁠82
取巧解 9 0 70–⁠95
未改动的代码 1 0 29

满分 100。任一阻断项不过,得分即为 0。

轨迹 1

回合
113
工具调用
128
用时(分钟)
45
改动文件
9
增 / 删行数
+938 / −1

以新增的 nextEndpoint 实现 Endpoint():复用 Route.match 从当前位置扫描路由桶,参数写入临时数组。测试在上下文类型、路由配置与参数中间件的 32 种组合下与路由器对照;跑了 make test,并补写文档。

第 19 回合 发现 serverErrorHandler 只重放中间件,此时前瞻须返回 nil。

第 21 回合 发现 getMatch 按 path 校验路由约束,因此前瞻改用自定义上下文的 Path()。

第 35 回合 在仓库外写临时模块,确认无处理函数的分组 Use() 会终止处理链,其后的端点不会运行。

第 38 回合 在 router.go 新增 nextEndpoint,沿用路由扫描但不运行处理函数。

第 100 回合 与基线提交对比约 4,700 个函数的反汇编,确认现有函数的指令除地址外均未改变。

第 108 回合 用 make test 跑完整套件:5,371 个测试,无失败。

第 110 回合 分两次本地提交:先提交功能与测试,再提交文档。

轨迹 1 的最终改动(节选)
diff --git a/router.go b/router.go
--- a/router.go
+++ b/router.go
@@ -737,6 +737,60 @@ func (app *App) nextCustom(c CustomCtx) (bool, error) {
 	return false, ErrNotFound
 }
 
+// nextEndpoint returns the endpoint next would run for c if the chain went on
+// from its current position, or nil when it would run none. It scans the bucket
+// next scans, with the same filters and the same use of the SkipUnmatchedRoutes
+// lookahead, but runs nothing: the middleware it passes is taken to call Next,
+// and parameters are captured into a scratch array instead of c.values, so the
+// only thing it leaves on c is the slash count pathSlashCount caches. path is
+// the path next matches against; nextCustom reads it through the custom
+// context's Path.
+func (app *App) nextEndpoint(c *DefaultCtx, path string) *Route {
+	methodInt := c.methodInt
+	// Middleware-only traversal reaches no endpoint, and a method outside
+	// RequestMethods (as the server error handler can see) has no tree
+	if c.shouldSkipNonUseRoutes || methodInt < 0 || methodInt >= len(app.treeIndex) {
+		return nil
+	}
+
+	detectionPath := utils.UnsafeString(c.detectionPath)
+	tree := app.treeIndex[methodInt].lookup(c.treePathHash)
+	head := pathHeadWord(detectionPath)
+	pathSlashes := c.pathSlashCount(app)
+	firstMatchIndex := c.firstMatchIndex
+	var values [maxParams]string
+
+	for indexRoute := max(c.indexRoute+1, 0); indexRoute < len(tree); indexRoute++ {
+		route := tree[indexRoute]
+		if route.prefixRejects(head) || route.mount {
+			continue
+		}
+
+		// next takes the endpoint the lookahead pre-resolved without matching it again
+		if firstMatchIndex >= 0 && !route.use {
+			if indexRoute < firstMatchIndex {
+				continue
+			}
+			if indexRoute == firstMatchIndex && !app.skip.hasParamUse {
+				return route
+			}
+		}
+
+		if !route.match(detectionPath, path, &values, pathSlashes) {
+			continue
+		}
+		if !route.use {
+			return route
+		}
+		// next stops at a matching route that has no handler to run
+		if len(route.Handlers) == 0 {
+			return nil
+		}
+	}
+
+	return nil
+}
+
 func (app *App) defaultRequestHandler(rctx *fasthttp.RequestCtx) {
 	ctx, ok := app.acquireDefaultCtx(rctx)
 	if !ok {
diff --git a/ctx.go b/ctx.go
--- a/ctx.go
+++ b/ctx.go
@@ -387,6 +387,25 @@ func (c *DefaultCtx) routeFallback() *Route {
 	}
 }
 
+// Endpoint returns the endpoint that Next will reach from the current position
+// in the handler chain: a route registered with Get, Post, Add and the like,
+// never one registered with Use. It returns nil when the router will run no
+// endpoint for the request (404, 405, ...), and once the chain is on an
+// endpoint, in any of its handlers, it returns that endpoint. It only looks
+// ahead: it runs no handler and leaves the handler position, Route, Matched and
+// the route params as they are. Routes registered through Domain check the
+// hostname inside their handlers, so they are reported as the router reaches them.
+func (c *DefaultCtx) Endpoint() *Route {
+	if c.route != nil && !c.route.use {
+		return c.route
+	}
+	if c.handlerCtx != nil {
+		// nextCustom matches against the custom context's Path
+		return c.app.nextEndpoint(c, c.handlerCtx.Path())
+	}
+	return c.app.nextEndpoint(c, utils.UnsafeString(c.path))
+}
+
 // FullPath returns the matched route path, including any group prefixes.
 func (c *DefaultCtx) FullPath() string {
 	return c.Route().Path
diff --git a/req.go b/req.go
--- a/req.go
+++ b/req.go
@@ -1333,6 +1333,18 @@ func (r *DefaultReq) Route() *Route {
 	return r.c.Route()
 }
 
+// Endpoint returns the endpoint that Next will reach from the current position
+// in the handler chain: a route registered with Get, Post, Add and the like,
+// never one registered with Use. It returns nil when the router will run no
+// endpoint for the request (404, 405, ...), and once the chain is on an
+// endpoint, in any of its handlers, it returns that endpoint. It only looks
+// ahead: it runs no handler and leaves the handler position, Route, Matched and
+// the route params as they are. Routes registered through Domain check the
+// hostname inside their handlers, so they are reported as the router reaches them.
+func (r *DefaultReq) Endpoint() *Route {
+	return r.c.Endpoint()
+}
+
 // Subdomains returns a slice of subdomains from the host, excluding the last `offset` components.
 // If the offset is negative or exceeds the number of subdomains, an empty slice is returned.
 // If the offset is zero every label (no trimming) is returned.
diff --git a/ctx_interface_gen.go b/ctx_interface_gen.go
--- a/ctx_interface_gen.go
+++ b/ctx_interface_gen.go
@@ -114,6 +114,15 @@ type Ctx interface {
 	// fallback for unregistered methods) so Route and Method always agree.
 	// Never inlined: inlining it would push Route over the inlining budget.
 	routeFallback() *Route
+	// Endpoint returns the endpoint that Next will reach from the current position
+	// in the handler chain: a route registered with Get, Post, Add and the like,
+	// never one registered with Use. It returns nil when the router will run no
+	// endpoint for the request (404, 405, ...), and once the chain is on an
+	// endpoint, in any of its handlers, it returns that endpoint. It only looks
+	// ahead: it runs no handler and leaves the handler position, Route, Matched and
+	// the route params as they are. Routes registered through Domain check the
+	// hostname inside their handlers, so they are reported as the router reaches them.
…… 其余 920 行从略(共改动 9 个文件)

轨迹 2

回合
104
工具调用
122
用时(分钟)
73
改动文件
9
增 / 删行数
+1,025 / −1

为 Ctx / Req 新增 Endpoint(),由只读的 peekEndpoint 复用 Route.match 重放 next 的扫描。一个测试在 32 种应用配置下与路由器比对;运行 make test 与 lint,并编写文档。

第 10 回合 发现方法覆盖保留 indexRoute,next 从旧下标之后扫描新方法的树,前瞻照做。

第 15 回合 用临时测试确认无处理函数的 grp.Use() 会终止处理链,朴素扫描会与路由器不一致。

第 31 回合 新增 peekEndpoint 作为 next 的只读版本,参数写入临时数组。

第 35 回合 用 go build -gcflags='-m' 检查临时参数数组是否逃逸到堆上。

第 91 回合 运行 make lint、go vet、make markdown,均无问题。

第 100 回合 最后一次修改测试文件后重跑 make test:5,377 个测试无失败。

第 103 回合 把 9 个改动文件一次性提交到本地。

轨迹 2 的最终改动(节选)
diff --git a/router.go b/router.go
--- a/router.go
+++ b/router.go
@@ -737,6 +737,66 @@ func (app *App) nextCustom(c CustomCtx) (bool, error) {
 	return false, ErrNotFound
 }
 
+// peekEndpoint is the read-only twin of next: it replays next's scan of the
+// route bucket from c's position and returns the endpoint next selects once
+// each middleware matched on the way has called Next. It returns nil wherever
+// next selects none: a 404 or 405, a middleware-only pass
+// (shouldSkipNonUseRoutes), or a matched middleware route without handlers,
+// which ends the chain. path is what next or nextCustom slices parameters from.
+//
+// Nothing on c changes: parameters are matched into a scratch array, and
+// pathSlashCount only caches the count next would compute itself. Keep this in
+// step with next and nextCustom; Test_Endpoint_AgreesWithRouter and
+// Test_Endpoint_MirrorsNext compare them.
+func (app *App) peekEndpoint(c *DefaultCtx, path string) *Route {
+	methodInt := c.methodInt
+	// A middleware-only pass runs no endpoint, and an unregistered method is
+	// answered with 501 before routing starts.
+	if c.shouldSkipNonUseRoutes || methodInt < 0 || methodInt >= len(app.treeIndex) {
+		return nil
+	}
+
+	detectionPath := utils.UnsafeString(c.detectionPath)
+	tree := app.treeIndex[methodInt].lookup(c.treePathHash)
+	head := pathHeadWord(detectionPath)
+	pathSlashes := c.pathSlashCount(app)
+	firstMatchIndex := c.firstMatchIndex
+	var values [maxParams]string
+
+	for indexRoute := max(c.indexRoute+1, 0); indexRoute < len(tree); indexRoute++ {
+		route := tree[indexRoute]
+		if route.prefixRejects(head) || route.mount {
+			continue
+		}
+
+		// next skips the endpoints the SkipUnmatchedRoutes lookahead ruled out
+		// and takes its pick without matching again, as long as it may reuse
+		// the lookahead's params.
+		if firstMatchIndex >= 0 && !route.use {
+			if indexRoute < firstMatchIndex {
+				continue
+			}
+			if indexRoute == firstMatchIndex && !app.skip.hasParamUse {
+				return route
+			}
+		}
+
+		if !route.match(detectionPath, path, &values, pathSlashes) {
+			continue
+		}
+		if !route.use {
+			return route
+		}
+		// Matched middleware passes the request on, but next stops the chain
+		// at a middleware route without handlers.
+		if len(route.Handlers) == 0 {
+			return nil
+		}
+	}
+
+	return nil
+}
+
 func (app *App) defaultRequestHandler(rctx *fasthttp.RequestCtx) {
 	ctx, ok := app.acquireDefaultCtx(rctx)
 	if !ok {
diff --git a/ctx.go b/ctx.go
--- a/ctx.go
+++ b/ctx.go
@@ -387,6 +387,25 @@ func (c *DefaultCtx) routeFallback() *Route {
 	}
 }
 
+// Endpoint returns the endpoint, a route registered with Get, Post, Add and
+// the like but never with Use, that Next will reach from the current position
+// in the handler chain. Once the chain is on an endpoint, it returns that
+// endpoint. It returns nil when the router will run no endpoint for the
+// request, such as for a 404 or 405. Endpoint only looks ahead: it runs no
+// handler and leaves the handler position, Route, Matched and the route
+// params untouched.
+func (c *DefaultCtx) Endpoint() *Route {
+	if route := c.route; route != nil && !route.use && c.indexHandler < len(route.Handlers) {
+		return route
+	}
+	path := utils.UnsafeString(c.path)
+	if c.handlerCtx != nil {
+		// nextCustom matches against the custom context's Path
+		path = c.handlerCtx.Path()
+	}
+	return c.app.peekEndpoint(c, path)
+}
+
 // FullPath returns the matched route path, including any group prefixes.
 func (c *DefaultCtx) FullPath() string {
 	return c.Route().Path
diff --git a/req.go b/req.go
--- a/req.go
+++ b/req.go
@@ -1333,6 +1333,17 @@ func (r *DefaultReq) Route() *Route {
 	return r.c.Route()
 }
 
+// Endpoint returns the endpoint, a route registered with Get, Post, Add and
+// the like but never with Use, that Next will reach from the current position
+// in the handler chain. Once the chain is on an endpoint, it returns that
+// endpoint. It returns nil when the router will run no endpoint for the
+// request, such as for a 404 or 405. Endpoint only looks ahead: it runs no
+// handler and leaves the handler position, Route, Matched and the route
+// params untouched.
+func (r *DefaultReq) Endpoint() *Route {
+	return r.c.Endpoint()
+}
+
 // Subdomains returns a slice of subdomains from the host, excluding the last `offset` components.
 // If the offset is negative or exceeds the number of subdomains, an empty slice is returned.
 // If the offset is zero every label (no trimming) is returned.
…… 其余 1,010 行从略(共改动 9 个文件)

样例 6 / 7

新增“?”守卫符号与 guards 设置

  • Rust
  • just
  • 新功能
  • 参考解 18 个文件、改动 294 行
  • 设置项全链路接入
  • 行首符号解析
  • 退出码语义
  • 编译期报错
  • 集成测试
  • 难点
  • 设置关闭时“?”仍属命令
  • “?”/“-”编译检查依赖设置
  • 执行与编译检查共用符号解析
  • JSON 测试的设置镜像需补字段

为逐行配方新增 ? 守卫符号,由布尔设置 guards 开启:退出码 0 继续,1 则不报错地结束当前配方,其余报错;? 与 - 同用为编译错误。新符号与设置均需写入 README。

题面(Agent 收到的原文)

prompt.md

Task description

Recipe lines can already be prefixed with @ (toggle echoing) and - (ignore a failing exit status). Add a third sigil, ?, for guard lines: checks that end the current recipe early, without an error, when they don't pass, e.g. ?[[ -f config.toml ]] at the top of a recipe.

Since existing justfiles may contain commands that start with ?, the sigil is opt-in through a new boolean setting, guards (set guards, set guards := true|false, default false). Without it, a leading ? stays part of the command, exactly as today.

With guards enabled, a line in a linewise recipe that starts with the ? sigil is handled according to its exit status:

  • 0: the recipe continues as normal.
  • 1: the rest of the current recipe is skipped and no error is reported; other recipes (dependents, subsequents, further recipes on the command line) keep running.
  • Anything else is reserved for future use and is an error: Guard line in recipe `RECIPE` on line N returned reserved exit code CODE, where N is the guard's line number in the justfile.

Like - and @, ? is stripped from the command before it is echoed and run, and it can be combined with @ in either order. Using the ? and - sigils together on the same line is a compile error: The guard `?` and infallible `-` sigils may not be used together. Shebang and [script] recipes are unaffected.

Document the new sigil and setting in the README.

题面后半:测试、lint 和代码风格规范
prompt.md(续)

Test guidelines

  • Integration tests live in tests/, one module per feature (see tests/ignore_comments.rs or tests/line_prefixes.rs). Cargo.toml sets autotests = false and builds a single integration test binary from tests/lib.rs, so a new test file must be registered there with mod <name>; or it is silently not compiled.
  • Write tests with the Test builder from tests/test.rs: Test::new().justfile("...").args([...]).stdout("...").stderr("...").success() (or .failure() / .status(n)). It runs just --shell bash, unindents the justfile, and on success also checks that the justfile round-trips through --dump.
  • Cover the new behaviour with tests, including its error cases.
  • Run one module with cargo test --all -- <module>:: (e.g. cargo test --all -- line_prefixes::), and the whole suite with cargo test --all before you finish. Update existing tests whose expectations legitimately change; don't delete or #[ignore] them.

Lint guidelines

CI's lint and MSRV jobs must pass. All of these work offline here:

  • cargo clippy --all --all-targets -- --deny warnings. Cargo.toml denies clippy::all and clippy::pedantic, and clippy.toml makes clippy enforce alphabetical order of enum variants, struct fields and trait items.
  • cargo fmt --all -- --check
  • ./bin/forbid, which rejects the words dbg!, fixme, todo and xxx (in any case) in every file except CHANGELOG.md, comments and test strings included.
  • shellcheck www/install.sh
  • RUSTFLAGS='--deny warnings' cargo +1.82.0 check (the MSRV is Rust 1.82; its "unknown lint: mismatched_lifetime_syntaxes" warning is expected).

Style guidelines

  • Follow the existing patterns for this kind of change: how a boolean setting such as ignore-comments is wired through the codebase, how recipe-line prefixes are handled when linewise recipes run, and how errors and compile errors are defined and displayed. Prefer generalizing an existing helper over adding logic that duplicates it.
  • Keep enum variants, struct fields and match arms in the alphabetical order used throughout.
  • Comments are sparse in this codebase; don't add ones that narrate the code, and don't leave commented-out code or debugging output behind.
  • CHANGELOG.md is written at release time; don't edit it.
  • You are already on the correct base commit. Create your branch from this commit. Do not rebase or start from master, main, or any other branch.

评分细则

# 细则 类别 检查方式 阻断项 权重
C1 守卫退出码:0 继续,1 只结束本配方,其余失败 行为正确 隐藏测试 是 20%
C2 ? 仅在开启设置时为符号,- / @ 照旧 不破坏已有行为 隐藏测试 是 12%
C3 完整测试套件通过,原有测试不改也通过 不破坏已有行为 项目命令 是 12%
C4 新增守卫集成测试:改动前失败、改动后通过 测试质量 改前失败、改后通过 是 10%
C5 ?+- 报指定编译错误,仅当 ? 是符号 行为正确 隐藏测试 — 6%
C6 遇保留退出码时,报错信息含配方名与守卫行号 行为正确 隐藏测试 — 4%
C7 ? 可与 @ 任意顺序组合,回显时去掉 行为正确 隐藏测试 — 5%
C8 clippy(警告视为错误)、禁用词检查、shellcheck 通过 构建/lint/格式 项目命令 — 4%
C9 代码已按 rustfmt 格式化 构建/lint/格式 项目命令 — 2%
C10 Rust 1.82(MSRV)下可编译,警告视为错误 构建/lint/格式 项目命令 — 2%
C11 执行与编译期检查共用同一行首符号解析 代码质量与规范 评审判定 — 5%
C12 失败均为专用错误变体,由现有显示代码输出 代码质量与规范 评审判定 — 4%
C13 每种守卫行为都有 Test 构建器集成测试 测试质量 评审判定 — 4%
C14 README 准确写明 ? 及 guards 代码质量与规范 评审判定 — 5%
C15 改动限于源码、测试与文档,规模适中 改动范围 改动范围检查 — 3%
C16 无残留调试输出、注释掉的代码或复述代码的注释 构建/lint/格式 评审判定 — 2%

评分器对随附解法的判定

解法 份数 得分 不计阻断项的加权得分
参考解 1 100 100
其他正确解法 2 100 100
接近完成解 2 81–⁠95 81–⁠95
部分完成解 2 0 22–⁠57
取巧解 6 0 35–⁠86
未改动的代码 1 0 23

满分 100。任一阻断项不过,得分即为 0。

轨迹 1

回合
93
工具调用
129
用时(分钟)
56
改动文件
17
增 / 删行数
+574 / −16

用单一 Line::sigils 取代前缀判断,配方分析移到设置求值后以检查 ? / -;遇到保留退出码时报错,just 以该守卫的退出码退出。编写 23 个集成测试与 README 说明,运行完整测试、lint、MSRV 检查,手动验证中断处理。

第 2 回合 查遍 ignore-comments 的引用,再读 src/line.rs 的前缀判断。

第 28 回合 决定用一个 Line 辅助函数读行首不重复的符号,开启 guards 时才识别 ?。

第 39 回合 权衡报错顺序后,转向在设置求值后调用 analyze_recipe 以检查 ? / -。

第 67 回合 让守卫收到 TERM 时以 1 退出,运行中发 SIGTERM:报中断,后续配方未运行。

第 86 回合 运行 generate-book,确认设置表的 #guard-lines 链接有效。

第 89 回合 验证根模块的 set guards 不波及子模块,子模块编译错误指向 sub.just。

第 92 回合 删除临时目录和生成的 book/ 源文件,再用 git status 确认只剩预期改动。

轨迹 1 的最终改动(节选)
diff --git a/src/line.rs b/src/line.rs
--- a/src/line.rs
+++ b/src/line.rs
@@ -33,19 +33,37 @@ impl Line<'_> {
     self.fragments.is_empty()
   }
 
-  pub(crate) fn is_infallible(&self) -> bool {
-    self
-      .first()
-      .is_some_and(|text| text.starts_with('-') || text.starts_with("@-"))
+  pub(crate) fn is_guard(&self, guards: bool) -> bool {
+    self.sigils(guards).contains('?')
   }
 
-  pub(crate) fn is_quiet(&self) -> bool {
-    self
-      .first()
-      .is_some_and(|text| text.starts_with('@') || text.starts_with("-@"))
+  pub(crate) fn is_infallible(&self, guards: bool) -> bool {
+    self.sigils(guards).contains('-')
+  }
+
+  pub(crate) fn is_quiet(&self, guards: bool) -> bool {
+    self.sigils(guards).contains('@')
   }
 
   pub(crate) fn is_shebang(&self) -> bool {
     self.first().is_some_and(|text| text.starts_with("#!"))
   }
+
+  fn sigils(&self, guards: bool) -> &str {
+    let text = self.first().unwrap_or_default();
+
+    let mut sigils = 0;
+
+    for c in text.chars() {
+      let sigil = c == '-' || c == '@' || (guards && c == '?');
+
+      if !sigil || text[..sigils].contains(c) {
+        break;
+      }
+
+      sigils += 1;
+    }
+
+    &text[..sigils]
+  }
 }
diff --git a/src/recipe.rs b/src/recipe.rs
--- a/src/recipe.rs
+++ b/src/recipe.rs
@@ -245,6 +245,7 @@ impl<'src, D> Recipe<'src, D> {
     mut evaluator: Evaluator<'src, 'run>,
   ) -> RunResult<'src, ()> {
     let config = &context.config;
+    let guards = context.module.settings.guards;
 
     let mut lines = self.body.iter().peekable();
     let mut line_number = self.line_number() + 1;
@@ -254,8 +255,9 @@ impl<'src, D> Recipe<'src, D> {
       }
       let mut evaluated = String::new();
       let mut continued = false;
-      let quiet_line = lines.peek().is_some_and(|line| line.is_quiet());
-      let infallible_line = lines.peek().is_some_and(|line| line.is_infallible());
+      let guard_line = lines.peek().is_some_and(|line| line.is_guard(guards));
+      let quiet_line = lines.peek().is_some_and(|line| line.is_quiet(guards));
+      let infallible_line = lines.peek().is_some_and(|line| line.is_infallible(guards));
 
       let comment_line = context.module.settings.ignore_comments
         && lines.peek().is_some_and(|line| line.is_comment());
@@ -283,7 +285,7 @@ impl<'src, D> Recipe<'src, D> {
 
       let mut command = evaluated.as_str();
 
-      let sigils = usize::from(infallible_line) + usize::from(quiet_line);
+      let sigils = usize::from(guard_line) + usize::from(infallible_line) + usize::from(quiet_line);
 
       command = &command[sigils..];
 
@@ -349,10 +351,24 @@ impl<'src, D> Recipe<'src, D> {
 
       let (result, caught) = cmd.status_guard();
 
+      let mut guard_failed = false;
+
       match result {
         Ok(exit_status) => {
           if let Some(code) = exit_status.code() {
-            if code != 0 && !infallible_line {
+            if guard_line {
+              match code {
+                0 => {}
+                1 => guard_failed = true,
+                _ => {
+                  return Err(Error::GuardCode {
+                    code,
+                    line_number,
+                    recipe: self.name(),
+                  });
+                }
+              }
+            } else if code != 0 && !infallible_line {
               return Err(Error::Code {
                 recipe: self.name(),
                 line_number: Some(line_number),
@@ -381,6 +397,10 @@ impl<'src, D> Recipe<'src, D> {
           return Err(Error::Interrupted { signal });
         }
       }
+
+      if guard_failed {
+        return Ok(());
+      }
     }
   }
 
diff --git a/src/error.rs b/src/error.rs
--- a/src/error.rs
+++ b/src/error.rs
@@ -120,6 +120,11 @@ pub(crate) enum Error<'src> {
   GetConfirmation {
     io_error: io::Error,
   },
+  GuardCode {
+    code: i32,
+    line_number: usize,
+    recipe: &'src str,
+  },
…… 其余 731 行从略(共改动 17 个文件)

轨迹 2

回合
81
工具调用
92
用时(分钟)
48
改动文件
17
增 / 删行数
+520 / −15

用单一 Line::sigils 扫描取代前缀判断,把配方分析移到设置求值后以检查 ? / -;遇到保留退出码时报错,just 以状态 1 退出。编写 20 个集成测试与 README 说明,运行基线与完整测试、lint、MSRV,并与基线版逐一对比输出。

第 12 回合 读 run_recipe 后判断:守卫失败时提前返回 Ok(()),其他配方照常运行。

第 32 回合 先构建并运行 cargo test --all 记录基线,再动代码。

第 35 回合 读 README 的信号处理一节与 src/command_ext.rs 后决定:捕获信号时,守卫以 1 退出也须报中断。

第 48 回合 构建基线版逐一对比:未设 guards 时 720 种前缀/命令/静默组合一致。

第 57 回合 用 POSIX test -f 写 README 示例以适配默认 sh,并运行核对输出。

第 73 回合 发现 Rust 1.82(MSRV)检查结束得过快,用 touch 强制重新检查,只出现预期的那条 lint 警告。

第 80 回合 重跑全部测试、lint 与 MSRV 检查后,在新分支 guard-sigil 上提交。

轨迹 2 的最终改动(节选)
diff --git a/src/line.rs b/src/line.rs
--- a/src/line.rs
+++ b/src/line.rs
@@ -33,19 +33,39 @@ impl Line<'_> {
     self.fragments.is_empty()
   }
 
-  pub(crate) fn is_infallible(&self) -> bool {
-    self
-      .first()
-      .is_some_and(|text| text.starts_with('-') || text.starts_with("@-"))
+  pub(crate) fn is_guard(&self, guards: bool) -> bool {
+    self.sigils(guards).contains('?')
   }
 
-  pub(crate) fn is_quiet(&self) -> bool {
-    self
-      .first()
-      .is_some_and(|text| text.starts_with('@') || text.starts_with("-@"))
+  pub(crate) fn is_infallible(&self, guards: bool) -> bool {
+    self.sigils(guards).contains('-')
+  }
+
+  pub(crate) fn is_quiet(&self, guards: bool) -> bool {
+    self.sigils(guards).contains('@')
   }
 
   pub(crate) fn is_shebang(&self) -> bool {
     self.first().is_some_and(|text| text.starts_with("#!"))
   }
+
+  /// Leading `@`, `-`, and, if `guards` is set, `?` sigils, each of which may
+  /// appear at most once
+  fn sigils(&self, guards: bool) -> &str {
+    let text = self.first().unwrap_or_default();
+
+    let mut end = 0;
+
+    for c in text.chars() {
+      let sigil = c == '@' || c == '-' || (guards && c == '?');
+
+      if !sigil || text[..end].contains(c) {
+        break;
+      }
+
+      end += c.len_utf8();
+    }
+
+    &text[..end]
+  }
 }
diff --git a/src/recipe.rs b/src/recipe.rs
--- a/src/recipe.rs
+++ b/src/recipe.rs
@@ -245,6 +245,7 @@ impl<'src, D> Recipe<'src, D> {
     mut evaluator: Evaluator<'src, 'run>,
   ) -> RunResult<'src, ()> {
     let config = &context.config;
+    let guards = context.module.settings.guards;
 
     let mut lines = self.body.iter().peekable();
     let mut line_number = self.line_number() + 1;
@@ -254,8 +255,9 @@ impl<'src, D> Recipe<'src, D> {
       }
       let mut evaluated = String::new();
       let mut continued = false;
-      let quiet_line = lines.peek().is_some_and(|line| line.is_quiet());
-      let infallible_line = lines.peek().is_some_and(|line| line.is_infallible());
+      let quiet_line = lines.peek().is_some_and(|line| line.is_quiet(guards));
+      let infallible_line = lines.peek().is_some_and(|line| line.is_infallible(guards));
+      let guard_line = lines.peek().is_some_and(|line| line.is_guard(guards));
 
       let comment_line = context.module.settings.ignore_comments
         && lines.peek().is_some_and(|line| line.is_comment());
@@ -283,7 +285,7 @@ impl<'src, D> Recipe<'src, D> {
 
       let mut command = evaluated.as_str();
 
-      let sigils = usize::from(infallible_line) + usize::from(quiet_line);
+      let sigils = usize::from(guard_line) + usize::from(infallible_line) + usize::from(quiet_line);
 
       command = &command[sigils..];
 
@@ -349,10 +351,24 @@ impl<'src, D> Recipe<'src, D> {
 
       let (result, caught) = cmd.status_guard();
 
+      let mut guard_failed = false;
+
       match result {
         Ok(exit_status) => {
           if let Some(code) = exit_status.code() {
-            if code != 0 && !infallible_line {
+            if guard_line {
+              match code {
+                0 => {}
+                1 => guard_failed = true,
+                _ => {
+                  return Err(Error::GuardCode {
+                    code,
+                    line_number,
+                    recipe: self.name(),
+                  });
+                }
+              }
+            } else if code != 0 && !infallible_line {
               return Err(Error::Code {
                 recipe: self.name(),
                 line_number: Some(line_number),
@@ -381,6 +397,10 @@ impl<'src, D> Recipe<'src, D> {
           return Err(Error::Interrupted { signal });
         }
       }
+
+      if guard_failed {
+        return Ok(());
+      }
     }
   }
 
…… 其余 679 行从略(共改动 17 个文件)

样例 7 / 7

让 Composer 替换文件而非原地重写

  • PHP
  • composer
  • 跨模块一致性改动
  • 参考解 10 个文件、改动 266 行
  • 原子替换写入
  • 权限与属主属组保留
  • 写入点排查
  • PHP 7.2 兼容
  • PHPUnit 测试
  • 难点
  • 凭据临时文件不得比目标更可读
  • 无法保留属主属组则原地写
  • 未静默的警告会抛异常并跳过回退
  • 安装失败后的回滚也须改用替换写入

新增 safeFilePutContents():用临时文件替换目标,使硬链接副本保持原样,Composer 生成或改写的文件都改用它。权限、属主属组、符号链接和报错须不变。

题面(Agent 收到的原文)

prompt.md

Task description

Composer writes the files it generates or updates with file_put_contents(), which truncates and rewrites the existing inode. Every path hardlinked to it changes too (cp -al copies, worktrees sharing a vendor dir, hardlink-based CI caches) and a concurrent reader can see a half-written file:

cd "$(mktemp -d)" && mkdir project && cd project   # a scratch directory, never this repository
echo '{"autoload":{"files":["a.php"]}}' > composer.json && touch a.php b.php && composer dump-autoload
cp -al . ../copy
sed -i 's/"a.php"]/"a.php","b.php"]/' composer.json && composer dump-autoload
grep -c b.php ../copy/vendor/composer/autoload_files.php   # 1: the copy changed too

Make Composer replace those files instead: write a temporary file next to the target and move it onto the target with PHP's rename(), like Cache::write() does.

  • Add public static function safeFilePutContents(string $path, string $content) to Composer\Util\Filesystem, returning what file_put_contents() returns (bytes written, or false).
  • Use it for every file Composer itself generates or rewrites: the files it writes under vendor/ (autoload files, installed.json/installed.php, bin proxies; Filesystem::filePutContentsIfModified() and Filesystem::safeCopy() must replace their target too), composer.json/composer.lock as written by require, remove, bump and config (including the reverts after a failed install), auth.json/config.json, and the .gitignore entry added by init. Installed package files are out of scope.

Nothing else may change:

  • A replaced file keeps its permissions, owner and group: give the replacement the original group (any user may change a file's group to one they belong to) and owner, or write the file in place if that is not possible. New files get the permissions a plain write gives them. Credentials (auth.json) must never sit in a file more readable than the target, not even briefly.
  • Symlinks are written through, read-only files still fail to be written, and unmodified files are still left untouched.
  • If the file cannot be replaced (e.g. unwritable directory, rename refused), remove the temporary file and write the target in place instead, so failures are reported as today.
题面后半:测试、lint 和代码风格规范
prompt.md(续)

Test guidelines

Tests use PHPUnit 9 through symfony/phpunit-bridge and live in tests/Composer/Test/, mirroring src/Composer/ (Composer\Util\Filesystem → tests/Composer/Test/Util/FilesystemTest.php); there is nothing to register. Test classes extend Composer\Test\TestCase, which provides unique temp dirs (self::getUniqueTmpDirectory()), initTempComposer() (creates a temp project and chdirs into it) and getApplicationTester() to run commands.

The change must be covered by new tests: that a hardlinked copy keeps its content, and that the behaviours listed above do not change. Skip with markTestSkipped() what the platform cannot do (Windows, no hardlink/symlink support, running as root), like the existing tests do.

  • One file: vendor/bin/simple-phpunit tests/Composer/Test/Util/FilesystemTest.php (PHPUnit 9 takes one path per run; add --filter <test name> to narrow it down).
  • Whole suite (~15 s). 19 tests need the internet and only fail after long HTTP retries offline, and PerforceDriverTest::testInitializeCapturesVariablesFromRepoConfig occasionally fails with a broken pipe because the p4 binary is not installed (unrelated to this change), so exclude them:
vendor/bin/simple-phpunit --verbose --filter '/^(?!.*(AuditCommandTest::testAuditPackageWithNoSecurityVulnerabilities|DiagnoseCommandTest::testCmd(Fail|Success)|InstallCommandTest::testInstall(FromEmptyVendor|FromEmptyVendorNoDev|NewPackagesWithExistingPartialVendor)|ShowCommandTest::testNotExistingPackage with data set "package with --all option"|StatusCommandTest::testLocallyModifiedPackages with data set "locally modified package from (source|dist)"|CompletionFunctionalTest::testComplete with data set #(0|1|3|10|11|15|16|21|22)|RemoteFilesystemTest::testBitBucketPublicDownload( with data set .*)?|PerforceDriverTest::testInitializeCapturesVariablesFromRepoConfig)$)/'

Running the whole suite re-dumps this repository's own autoloader in classmap-authoritative mode (DumpAutoloadCommandTest dumps it in the repository root), after which classes you add are no longer found; sed -i '/setClassMapAuthoritative(true)/d' vendor/composer/autoload_real.php restores the PSR-4 fallback.

Lint guidelines

  • PHPStan (level 8, a blocking CI job) must report no errors: vendor/bin/phpstan analyse --configuration=phpstan/config.neon --no-progress (ignore the "PHPStan 2.x is available" banner).
  • Composer supports PHP 7.2.5+ (require.php in composer.json) and CI runs it on PHP 7.2, so src/ and tests/ must not use newer syntax or native types — no native union types, mixed, arrow functions, typed properties, match, named arguments, etc.; express such types in phpdoc as the codebase does. Only PHP 8.3 is installed here; this check must print nothing:
printf 'parameters:\n    level: 0\n    phpVersion: 70205\n    reportUnmatchedIgnoredErrors: false\n' > /tmp/php72.neon
vendor/bin/phpstan analyse -c /tmp/php72.neon --no-progress --error-format=raw src tests | grep -E 'Syntax error|supported only on PHP'

That check does not see typed properties, ?->, ??=, numeric literal separators or trailing commas in calls and signatures (PHP 7.3+/7.4+/8.0), so avoid those as well.

  • Do not edit phpstan/ (configuration or baselines): fix reported errors in the code.
  • php -l <file> must pass for every changed file. Coding style follows .php-cs-fixer.php (PSR-2 based); check only the files you touched with php-cs-fixer fix --config=.php-cs-fixer.php --dry-run --diff --path-mode=intersection <files>. Some files already have unrelated findings: leave those alone.

Style guidelines

Match the surrounding code in src/Composer/Util/Filesystem.php: declare(strict_types=1), scalar type declarations plus phpdoc for what PHP 7.2 cannot express, Silencer::call() around filesystem calls whose failure you handle yourself (Composer's ErrorHandler turns every PHP warning that is not silenced into an exception, which would skip your cleanup and fallback), and brief comments that explain why rather than what. Keep the replace-instead-of-rewrite logic in one place behind Composer\Util\Filesystem and switch the call sites over to it instead of duplicating it. Keep the diff focused: no unrelated refactoring, and no coding-style fixes in code you do not otherwise change (the project does not accept CS-only changes). The maintainers update CHANGELOG.md at release time, so leave it alone.

You are already on the correct base commit. Create your branch from this commit. Do not rebase or start from master, main, or any other branch.

评分细则

# 细则 类别 检查方式 阻断项 权重
C1 Filesystem 三个方法替换文件,其余不变 行为正确 隐藏测试 是 19%
C2 生成或改写的文件被替换,硬链接副本不受影响 行为正确 隐藏测试 是 15%
C3 原有测试套件仍然通过 不破坏已有行为 项目命令 是 9%
C4 代码兼容 PHP 7.2.5,无新语法或新原生类型 不破坏已有行为 项目命令 是 8%
C5 PHPStan 第 8 级在项目配置与基线下无报错 构建/lint/格式 项目命令 — 4%
C6 新增测试在无改动时失败、有改动时通过 测试质量 改前失败、改后通过 — 6%
C7 替换后保留属组与属主,做不到时才原地写 不破坏已有行为 评审判定 — 6%
C8 临时文件含内容时权限不比目标宽松 不破坏已有行为 评审判定 — 6%
C9 同目录唯一名临时文件,经 rename() 替换 代码质量与规范 评审判定 — 3%
C10 替换经 Filesystem 统一实现,各处复用 代码质量与规范 评审判定 — 4%
C11 回滚、.gitignore、bin 代理等均替换 行为正确 评审判定 是 8%
C12 新测试遵循项目惯例,检验行为而非仅测接口 测试质量 评审判定 — 3%
C13 无调试输出、注释掉的代码、复述代码的注释;方法有文档 构建/lint/格式 评审判定 — 2%
C14 改动限于源码和测试、规模合理,不含无关重构或格式修正 改动范围 改动范围检查 — 3%
C15 失败步骤不抛出警告,删除临时文件后原地写入 不破坏已有行为 评审判定 — 4%

评分器对随附解法的判定

解法 份数 得分 不计阻断项的加权得分
参考解 1 100 100
其他正确解法 4 94–⁠100 94–⁠100
部分完成解 4 0 20–⁠92
取巧解 14 0 36–⁠92
未改动的代码 1 0 21

满分 100。任一阻断项不过,得分即为 0。

轨迹 1

回合
105
工具调用
118
用时(分钟)
45
改动文件
18
增 / 删行数
+624 / −26

新增 safeFilePutContents():已有文件先写同目录 0600 临时文件,补回属组、属主与权限后 rename(),失败时删除临时文件并原地写入;所列写入点改用它。cp -al 后跑 require,副本 14 个文件未变。

第 21 回合 在临时目录用 bin/composer 复现:硬链接副本也带上了 b.php。

第 29 回合 实现 safeFilePutContents:解析符号链接,只读文件或悬空链接回退原地写入。

第 33 回合 用命名空间 fstat() 垫片确认:目标已存在时,临时文件在写入内容期间权限为 600。

第 88 回合 用 git stash 撤下 src/ 改动:新增调用点测试除回滚与未改动文件外均失败。

第 97 回合 用原代码重跑:cp -al 后 require 改动了副本中 6 个文件,修复后为 0。

第 101 回合 为 Windows 调整测试:跳过 umask 测试,符号链接改查 is_link()。

第 105 回合 收尾时确认只改了预期的 18 个文件;最后一次全量运行共 3,450 个测试,退出码 0。

轨迹 1 的最终改动(节选)
diff --git a/src/Composer/Util/Filesystem.php b/src/Composer/Util/Filesystem.php
--- a/src/Composer/Util/Filesystem.php
+++ b/src/Composer/Util/Filesystem.php
@@ -918,26 +918,94 @@ public function filePutContentsIfModified(string $path, string $content)
     {
         $currentContent = Silencer::call('file_get_contents', $path);
         if (false === $currentContent || $currentContent !== $content) {
-            return file_put_contents($path, $content);
+            return self::safeFilePutContents($path, $content);
         }
 
         return 0;
     }
 
     /**
-     * Copy file using stream_copy_to_stream to work around https://bugs.php.net/bug.php?id=6463
+     * Writes a file by replacing it instead of rewriting it in place
+     *
+     * Other hardlinks to the file thus keep their content and readers never see a partially written file. The file is
+     * written in place if it cannot be replaced, e.g. if its directory is not writable or its owner cannot be kept.
+     *
+     * @return int|false the number of bytes written, or false on failure
+     * @phpstan-return int<0, max>|false
+     */
+    public static function safeFilePutContents(string $path, string $content)
+    {
+        // symlinks are written through by replacing the file they point to
+        $target = is_link($path) ? realpath($path) : $path;
+        if (false !== $target && self::replaceFile($target, $content)) {
+            return \strlen($content);
+        }
+
+        return file_put_contents($path, $content);
+    }
+
+    /**
+     * @return bool whether the file was replaced, it is left untouched otherwise
+     */
+    private static function replaceFile(string $path, string $content): bool
+    {
+        $stat = null;
+        if (file_exists($path)) {
+            // only regular files can be replaced, and read-only ones must keep failing to be written
+            if (!is_file($path) || !is_writable($path)) {
+                return false;
+            }
+            $stat = Silencer::call('stat', $path);
+            if (!is_array($stat)) {
+                return false;
+            }
+        }
+
+        $tempPath = $path.'.'.bin2hex(random_bytes(5)).'.tmp';
+
+        // the content may be credentials (auth.json), so a replacement is only accessible to its owner until it has the
+        // owner, group and permissions of the file it replaces, while new files get the permissions a plain write gives
+        $umask = umask();
+        if (null !== $stat) {
+            umask(0077);
+        }
+        $handle = Silencer::call('fopen', $tempPath, 'xb');
+        umask($umask);
+        if (!is_resource($handle)) {
+            return false;
+        }
+
+        $written = Silencer::call('fwrite', $handle, $content);
+        $tempStat = fstat($handle);
+        fclose($handle);
+
+        // only change what differs, as not all of it can be changed everywhere, e.g. only root may change the owner
+        $replaced = $written === \strlen($content)
+            && (null === $stat || (
+                false !== $tempStat
+                && ($tempStat['gid'] === $stat['gid'] || true === Silencer::call('chgrp', $tempPath, $stat['gid']))
+                && ($tempStat['uid'] === $stat['uid'] || true === Silencer::call('chown', $tempPath, $stat['uid']))
+                && (($tempStat['mode'] & 07777) === ($stat['mode'] & 07777) || true === Silencer::call('chmod', $tempPath, $stat['mode'] & 07777))
+            ))
+            && true === Silencer::call('rename', $tempPath, $path);
+
+        if (!$replaced) {
+            Silencer::call('unlink', $tempPath);
+        }
+
+        return $replaced;
+    }
+
+    /**
+     * Copy file without using copy() to work around https://bugs.php.net/bug.php?id=6463
      */
     public function safeCopy(string $source, string $target): void
     {
         if (!file_exists($target) || !file_exists($source) || !$this->filesAreEqual($source, $target)) {
-            $sourceHandle = fopen($source, 'r');
-            assert($sourceHandle !== false, 'Could not open "'.$source.'" for reading.');
-            $targetHandle = fopen($target, 'w+');
-            assert($targetHandle !== false, 'Could not open "'.$target.'" for writing.');
-
-            stream_copy_to_stream($sourceHandle, $targetHandle);
-            fclose($sourceHandle);
-            fclose($targetHandle);
+            $content = file_get_contents($source);
+            assert($content !== false, 'Could not read "'.$source.'".');
+
+            self::safeFilePutContents($target, $content);
 
             touch($target, (int) filemtime($source), (int) fileatime($source));
         }
diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php
--- a/src/Composer/Command/RequireCommand.php
+++ b/src/Composer/Command/RequireCommand.php
@@ -137,7 +137,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
         }
 
         $this->newlyCreated = !file_exists($this->file);
-        if ($this->newlyCreated && !file_put_contents($this->file, "{\n}\n")) {
+        if ($this->newlyCreated && !Filesystem::safeFilePutContents($this->file, "{\n}\n")) {
             $io->writeError('<error>'.$this->file.' could not be created.</error>');
 
             return 1;
@@ -149,7 +149,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
         }
 
         if (filesize($this->file) === 0) {
-            file_put_contents($this->file, "{\n}\n");
+            Filesystem::safeFilePutContents($this->file, "{\n}\n");
         }
 
         $this->json = new JsonFile($this->file);
…… 其余 790 行从略(共改动 18 个文件)

轨迹 2

回合
100
工具调用
117
用时(分钟)
41
改动文件
16
增 / 删行数
+545 / −26

新增 safeFilePutContents():已有目标先写入同目录的 0600 tempnam() 文件,补回属主属组再设权限后 rename(),失败时删除临时文件并原地写入;safeCopy() 等写入点改用它。经故障注入和命令行验证。

第 21 回合 编码前先探查 PHP:tempnam() 遇不可写目录会退回系统临时目录,实现遇此改原地写。

第 28 回合 把属主属组处理抽进 applyOwnerAndMode(),权限最后设,防内容被错组读到。

第 64 回合 强制 chgrp()、rename()、chmod() 失败,均原地写且无残留。

第 77 回合 发现 require --no-update 仍查询仓库,测试改用本地包仓库。

第 80 回合 用 git stash 撤下 src/ 改动后重跑新增的调用点测试:硬链接测试均失败。

第 90 回合 config --auth 写入硬链接 0600 auth.json:权限与副本均未变。

第 99 回合 确认相关测试文件均跑通;因任务未要求提交,改动留在新分支上未提交。

轨迹 2 的最终改动(节选)
diff --git a/src/Composer/Util/Filesystem.php b/src/Composer/Util/Filesystem.php
--- a/src/Composer/Util/Filesystem.php
+++ b/src/Composer/Util/Filesystem.php
@@ -918,26 +918,107 @@ public function filePutContentsIfModified(string $path, string $content)
     {
         $currentContent = Silencer::call('file_get_contents', $path);
         if (false === $currentContent || $currentContent !== $content) {
-            return file_put_contents($path, $content);
+            return self::safeFilePutContents($path, $content);
         }
 
         return 0;
     }
 
     /**
-     * Copy file using stream_copy_to_stream to work around https://bugs.php.net/bug.php?id=6463
+     * Writes a file by replacing it with a new one instead of truncating and rewriting it like file_put_contents() does
+     *
+     * Other hardlinks to the file thus keep their content, and concurrent readers never see a partially written file.
+     * Symlinks are written through. If the file can not be replaced while keeping its owner, group and permissions,
+     * e.g. because its directory is not writable, it is written in place so that failures are reported as usual.
+     *
+     * @return int|false the number of bytes written, or false on failure
+     */
+    public static function safeFilePutContents(string $path, string $content)
+    {
+        // write through symlinks by replacing the file they point to, dangling ones are written in place
+        $target = is_link($path) ? realpath($path) : $path;
+        $stat = false !== $target ? Silencer::call('stat', $target) : false;
+        $tmpFile = false !== $target ? self::createReplacementFile($target, $stat) : false;
+
+        if (false !== $tmpFile) {
+            $written = Silencer::call('file_put_contents', $tmpFile, $content);
+            if (
+                false !== $written
+                && (false === $stat || self::applyOwnerAndMode($tmpFile, $stat))
+                && true === Silencer::call('rename', $tmpFile, $target)
+            ) {
+                return $written;
+            }
+
+            Silencer::call('unlink', $tmpFile);
+        }
+
+        return file_put_contents($path, $content);
+    }
+
+    /**
+     * Creates an empty file next to $target to write its replacement to
+     *
+     * @param array<int|string, int>|false $stat stat() of $target, or false if it does not exist
+     * @return string|false the path of the created file, or false if $target can not be replaced
+     */
+    private static function createReplacementFile(string $target, $stat)
+    {
+        if (false === $stat) {
+            // created the way file_put_contents() would create $target, so it gets the same permissions
+            $tmpFile = $target.bin2hex(random_bytes(5)).'.tmp';
+            $handle = Silencer::call('fopen', $tmpFile, 'x');
+            if (false === $handle) {
+                return false;
+            }
+            fclose($handle);
+
+            return $tmpFile;
+        }
+
+        // devices like /dev/null can not be replaced, and read-only files must still fail to be written
+        if (!is_file($target) || !is_writable($target)) {
+            return false;
+        }
+
+        // tempnam() creates the file readable by its owner only, so the content (e.g. credentials) is never readable
+        // by more users than the target is
+        $tmpFile = Silencer::call('tempnam', dirname($target), basename($target));
+
+        // tempnam() falls back to the system temp dir if it can not create the file in the given one
+        if (false !== $tmpFile && realpath(dirname($tmpFile)) !== realpath(dirname($target))) {
+            Silencer::call('unlink', $tmpFile);
+
+            return false;
+        }
+
+        return $tmpFile;
+    }
+
+    /**
+     * @param array<int|string, int> $stat stat() of the file replaced by $file
+     */
+    private static function applyOwnerAndMode(string $file, array $stat): bool
+    {
+        $current = Silencer::call('stat', $file);
+
+        // the mode is set last: the content must not become readable by the group $file was created with, and
+        // changing the owner or group can clear the setuid/setgid bits
+        return false !== $current
+            && ($current['uid'] === $stat['uid'] || true === Silencer::call('chown', $file, $stat['uid']))
+            && ($current['gid'] === $stat['gid'] || true === Silencer::call('chgrp', $file, $stat['gid']))
+            && true === Silencer::call('chmod', $file, $stat['mode'] & 07777);
+    }
+
+    /**
+     * Copy file without using copy() to work around https://bugs.php.net/bug.php?id=64634
      */
     public function safeCopy(string $source, string $target): void
     {
         if (!file_exists($target) || !file_exists($source) || !$this->filesAreEqual($source, $target)) {
-            $sourceHandle = fopen($source, 'r');
-            assert($sourceHandle !== false, 'Could not open "'.$source.'" for reading.');
-            $targetHandle = fopen($target, 'w+');
-            assert($targetHandle !== false, 'Could not open "'.$target.'" for writing.');
-
-            stream_copy_to_stream($sourceHandle, $targetHandle);
-            fclose($sourceHandle);
-            fclose($targetHandle);
+            $content = file_get_contents($source);
+            assert($content !== false, 'Could not read "'.$source.'".');
+            self::safeFilePutContents($target, $content);
 
             touch($target, (int) filemtime($source), (int) fileatime($source));
         }
…… 其余 693 行从略(共改动 16 个文件)

可定制

包括但不限于以下几项,其他需求都可以商量。

  • 语言与项目:语言配比,领域(Web 框架、编译器、数据库、前端应用、CLI 工具等),单题改动规模。
  • 题型与侧重:新功能、缺陷修复、重构、跨模块一致性改动;更看重测试质量、改动范围还是代码规范。
  • 评分方式:阻断项与权重,隐藏测试、项目命令、评审判定各占多少,以及评审所用的模型。
  • 难度:对标的模型和要达到的通过率区间。
  • 轨迹:Claude Code 原生工具调用,或按你的评测框架转写(如只用 bash 的 Agent 框架);每题一条或多条。
  • 数量和排期:交付数量和时间安排。

想提升模型的哪项能力?

来信告诉我们,我们会回复样例和规格说明。