From 0750fa8d6f1f26b86592ecd13462b0ccf0ab7fc0 Mon Sep 17 00:00:00 2001 From: Aryam Goyal Date: Thu, 20 Aug 2026 17:58:59 +0530 Subject: [PATCH] fix: harden controlled agent study evaluation --- benchmarks/agent-study/README.md | 38 +++ benchmarks/agent-study/protocol.json | 26 +- benchmarks/agent-study/tasks.json | 122 +++++++++ docs/AGENT_STUDY.md | 119 +++++--- docs/assets/fixmap-cli-demo.svg | 2 +- .../reports/declines-fabricated-identifier.md | 2 +- examples/reports/declines-unmatched-terms.md | 2 +- examples/reports/declines-vague-task.md | 2 +- examples/reports/workspace-api-discount.md | 2 +- examples/reports/workspace-utils-rounding.md | 2 +- package.json | 5 +- scripts/evaluate-agent-study.mjs | 177 +++++++++--- scripts/evaluate-agent-study.test.mjs | 255 ++++++++++++++++++ 13 files changed, 677 insertions(+), 77 deletions(-) create mode 100644 benchmarks/agent-study/README.md create mode 100644 benchmarks/agent-study/tasks.json create mode 100644 scripts/evaluate-agent-study.test.mjs diff --git a/benchmarks/agent-study/README.md b/benchmarks/agent-study/README.md new file mode 100644 index 0000000..e273e82 --- /dev/null +++ b/benchmarks/agent-study/README.md @@ -0,0 +1,38 @@ +# Controlled agent-study artifacts + +This directory defines study structure, not study results. + +- `protocol.json` freezes the four arms, enforced requirements, metrics, aggregation policy, and + the path to the task manifest. +- `tasks.json` freezes the complete task universe for `fixmap-navigation-heldout-v1`. + +## Task-manifest schema + +Top-level fields: + +| Field | Contract | +| --- | --- | +| `manifestVersion` | `1` | +| `protocolVersion` | Must equal `protocol.json` (`3`) | +| `studyId` | Non-empty stable study identity required in every run row | +| `status` | Must be `frozen` | +| `frozenAt` | Date on which task membership and text stopped changing | +| `taskSelection` | Source, deterministic inclusion rule, and pre-outcome rationale | +| `tasks` | Non-empty array with unique `taskId` values | + +Every task requires `taskId`, exact `taskText`, its lowercase hexadecimal `taskTextSha256`, public +`repository`, exact 40-character `revision`, `sourceIssue`, and `selectionRationale`. The evaluator +recalculates every manifest task hash before it accepts the protocol. + +## Publication boundary + +The evaluator has no partial-results mode. With no `--input`, it validates only the protocol and +manifest and explicitly claims no result. With `--input`, it requires one global `--model`, +`--model-version`, and `--fixmap-revision`, verifies the exact manifest task ร— protocol arm +cross-product and transcript bytes, and emits an aggregate only when the whole group passes. +Every row also needs a unique declared `contextId`; this prevents accidental context-ID reuse but +does not prove process isolation. + +Separate models require separate complete run files and evaluator invocations. The evaluator does +not pool model groups. Failed and timed-out runs remain in their arm totals and all-run outcome +rate denominators. diff --git a/benchmarks/agent-study/protocol.json b/benchmarks/agent-study/protocol.json index 8616463..dbf2e59 100644 --- a/benchmarks/agent-study/protocol.json +++ b/benchmarks/agent-study/protocol.json @@ -1,25 +1,35 @@ { - "protocolVersion": 2, - "status": "protocol-only", + "protocolVersion": 3, + "status": "frozen-no-runs", "suite": "navigation-focused pinned tasks", + "taskManifest": "benchmarks/agent-study/tasks.json", "arms": [ "baseline", "fixmap-available", "fixmap-instructed", "fixmap-impact" ], + "aggregationPolicy": { + "publishableOnlyWhenComplete": true, + "modelGrouping": "single-global-model-version", + "failedAndTimedOutRuns": "retained" + }, "requirements": { - "sameModelVersion": true, + "sameGlobalModelVersion": true, "sameTaskText": true, "sameRepositoryRevision": true, - "freshContextPerRun": true, - "randomizedArmOrder": true, + "uniqueContextIdPerRun": true, + "uniqueArmOrderPerTask": true, "fixedTimeoutAndBudget": true, "noFixMapChangesMidStudy": true, "rawTranscriptsRequired": true, - "taskSuccessRubricFrozen": true, - "tokenAccountingSourceRecorded": true, - "costRateCardRecorded": true, + "transcriptContentHashVerified": true, + "taskManifestFrozen": true, + "completeTaskArmCrossProduct": true, + "taskTextMatchesManifest": true, + "taskSuccessRubricIdRecorded": true, + "providerReportedTokenCountersOnly": true, + "priceSheetIdRecorded": true, "failuresRetained": true }, "metrics": [ diff --git a/benchmarks/agent-study/tasks.json b/benchmarks/agent-study/tasks.json new file mode 100644 index 0000000..df95899 --- /dev/null +++ b/benchmarks/agent-study/tasks.json @@ -0,0 +1,122 @@ +{ + "manifestVersion": 1, + "protocolVersion": 3, + "studyId": "fixmap-navigation-heldout-v1", + "status": "frozen", + "frozenAt": "2026-08-20", + "taskSelection": { + "source": "All cases in benchmarks/heldout/dataset.json at FixMap commit bbc8469c937397fecac787c71c04521a6eb1a87d.", + "rule": "Use the complete pre-existing 12-case held-out suite in its frozen order; no task may be added, removed, or rewritten after controlled runs begin.", + "rationale": "These public MIT-licensed issue tasks and pinned pre-fix revisions were selected mechanically before any controlled agent-study outcome existed." + }, + "tasks": [ + { + "taskId": "heldout-automattic-mongoose-16379", + "taskText": "Document.isModified(path) rebuilds the full modifiedPaths() set on every call โ€” O(N^2) when checked per-path in loops (including mongoose's own validation)\n\n## Document.isModified(path) rebuilds the full modifiedPaths() set on every call โ€” O(N^2) when checked per-path in loops (including mongoose's own validation)\n\n**Repo:** `Automattic/mongoose`\n**Location:** lib/document.js:2339 (isModified -> modifiedPaths rebuild) called per-path from lib/document.js:2965 (_getPathsToValidate) and user hooks\n**Severity:** medium ยท **Confidence:** 0.8\n**Type:** complexity-at-a-distance\n\n### Description\n\nDocument.prototype.isModified in lib/document.js (line ~2321): when `path` is not directly modified, it falls through to `this[documentModifiedPaths]()` (line ~", + "taskTextSha256": "abab98df3c8461269aa9250a9cd3938adcd80765679b54afc3c9e9384740dfc9", + "repository": "https://github.com/Automattic/mongoose.git", + "revision": "0c3034d2886b5ff11eac8f3749cb33a896f53a57", + "sourceIssue": "https://github.com/Automattic/mongoose/issues/16379", + "selectionRationale": "Included by the pre-existing mechanical held-out selection rule before any controlled agent-study run; public issue 16379 in MIT-licensed Automattic/mongoose at the pinned pre-fix revision." + }, + { + "taskId": "heldout-immerjs-immer-1045", + "taskText": "`produceWithPatches` wrong return type\n\n## ๐Ÿ› Bug Report\r\n\r\nThe curried form of `produceWithPatches` returns the wrong type, while `produce` does.\r\n\r\n## Link to repro\r\n\r\n[https://codesandbox.io/s/producewithpatches-wrong-return-type-mr5hdp?file=/src/index.ts](https://codesandbox.io/s/producewithpatches-wrong-return-type-mr5hdp?file=/src/index.ts)\r\n\r\n## Observed behavior\r\n\r\nThe state returned by `produceWithPatches((draft: Draft, m: number) => { ... })` is of type `WritableDraft` but should be of type `N`.\r\n\r\nThe type of `produceWithPatches((draft, m) => { ... })` is `never` while the type of `produce(", + "taskTextSha256": "0cff254c49c2db9178c3251b4ffb920258042dfaec273c2302c8a639762d53ba", + "repository": "https://github.com/immerjs/immer.git", + "revision": "bf2d15439259887f98f2737cf7ebde4234d5adea", + "sourceIssue": "https://github.com/immerjs/immer/issues/1045", + "selectionRationale": "Included by the pre-existing mechanical held-out selection rule before any controlled agent-study run; public issue 1045 in MIT-licensed immerjs/immer at the pinned pre-fix revision." + }, + { + "taskId": "heldout-jestjs-jest-16174", + "taskText": "[Bug]: Spurious type error when calling `mockRejectedValue` on mocks of certain overloaded functions\n\n### Version\n\n30.4\n\n### Steps to reproduce\n\nConsider a method like [`pg.Client.end`](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/5b8b3cdc7c82f3b424e9cbb18c229f40b6497cec/types/pg/index.d.ts#L307-L308). It has two overloads:\n\n```\n end(): Promise;\n end(callback: (err: Error) => void): void;\n```\n\nWhen using a mocked instance, one might want to mock resolved or rejected values:\n\n```\ntest('mock pg', async () => {\n const client: jest.Mocked = setUpMockClient();\n client.end.mockRejectedValueOnce(new Error('test error when closing client')) // Type error\n await exp", + "taskTextSha256": "e2d59db074a767feac25c1c2cbf3800f6f661ed4fea2fe951b07c10b369a9106", + "repository": "https://github.com/jestjs/jest.git", + "revision": "4c3091b4204d703f4ebe343b8ac9d8a28ac4388e", + "sourceIssue": "https://github.com/jestjs/jest/issues/16174", + "selectionRationale": "Included by the pre-existing mechanical held-out selection rule before any controlled agent-study run; public issue 16174 in MIT-licensed jestjs/jest at the pinned pre-fix revision." + }, + { + "taskId": "heldout-knex-knex-5053", + "taskText": "BUG - FOR UPDATE must specify unqualified relation names\n\n# Environment\r\n\r\nKnex version: 1.0.3\r\nDatabase + version: PostgreSQL 13\r\nOS: Node 14\r\n\r\n# Bug\r\n\r\nThe error comes up when you build a query like knex('users').withSchema('myschema').where('id','xyz').forUpdate('users')\r\nIt seems incompatible to use withSchema and forUpdate in the same query because withSchema add the schema also in front of the forUpdate table name, but should not.\r\n\r\nError message: \"FOR UPDATE must specify unqualified relation names\"\r\n\r\nPs.: the above query works if I do not specify table name in forUpdate, but in query with joins you must specify the table name otherwise you ", + "taskTextSha256": "c17185088d4cc6080774013036641447ee39ecee9f807ccf687e24f2b0e9e754", + "repository": "https://github.com/knex/knex.git", + "revision": "6f42421e595e47653b131a621c2bd20fcb3337a4", + "sourceIssue": "https://github.com/knex/knex/issues/5053", + "selectionRationale": "Included by the pre-existing mechanical held-out selection rule before any controlled agent-study run; public issue 5053 in MIT-licensed knex/knex at the pinned pre-fix revision." + }, + { + "taskId": "heldout-mochajs-mocha-4526", + "taskText": "๐Ÿ› Bug: xunit reporter does not strip ansi escape sequences, leading to invalid XML\n\n\r\n\r\n### Prerequisites\r\n\r\n