loadout-ai 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/MASTER_PLAN.md +1711 -0
- package/README.md +453 -0
- package/SECURITY.md +18 -0
- package/SIMPLE_PLAN.md +44 -0
- package/catalog/discovered.json +10469 -0
- package/catalog/packages.json +1150 -0
- package/dashboard/app.js +607 -0
- package/dashboard/index.html +249 -0
- package/dashboard/styles.css +384 -0
- package/dist/src/cli.js +2735 -0
- package/dist/src/core/active-policy.js +233 -0
- package/dist/src/core/active-set.js +382 -0
- package/dist/src/core/adapters.js +254 -0
- package/dist/src/core/adopt.js +92 -0
- package/dist/src/core/agent-health-score.js +475 -0
- package/dist/src/core/agent-inspection.js +152 -0
- package/dist/src/core/agent-versions.js +131 -0
- package/dist/src/core/api.js +90 -0
- package/dist/src/core/atomic-file.js +22 -0
- package/dist/src/core/audit.js +174 -0
- package/dist/src/core/benchmark-campaign.js +496 -0
- package/dist/src/core/benchmark-evidence.js +458 -0
- package/dist/src/core/benchmark-fixtures.js +1157 -0
- package/dist/src/core/benchmark-runner.js +301 -0
- package/dist/src/core/benchmark-trust.js +795 -0
- package/dist/src/core/canary.js +77 -0
- package/dist/src/core/candidate-intelligence.js +661 -0
- package/dist/src/core/catalog-coverage.js +85 -0
- package/dist/src/core/catalog-install.js +186 -0
- package/dist/src/core/catalog-release.js +286 -0
- package/dist/src/core/catalog.js +327 -0
- package/dist/src/core/codex-mcp.js +122 -0
- package/dist/src/core/community.js +86 -0
- package/dist/src/core/compatibility-intelligence.js +465 -0
- package/dist/src/core/completion.js +160 -0
- package/dist/src/core/components.js +138 -0
- package/dist/src/core/conversion.js +56 -0
- package/dist/src/core/credentials.js +377 -0
- package/dist/src/core/demo.js +136 -0
- package/dist/src/core/diff.js +83 -0
- package/dist/src/core/discovery-connector.js +165 -0
- package/dist/src/core/doctor.js +95 -0
- package/dist/src/core/ecosystem-import.js +1072 -0
- package/dist/src/core/evaluate.js +72 -0
- package/dist/src/core/file-lock.js +140 -0
- package/dist/src/core/freshness-alerts.js +230 -0
- package/dist/src/core/github-discovery.js +97 -0
- package/dist/src/core/github.js +148 -0
- package/dist/src/core/head-to-head.js +437 -0
- package/dist/src/core/health-score-evidence.js +169 -0
- package/dist/src/core/health.js +129 -0
- package/dist/src/core/improve.js +223 -0
- package/dist/src/core/install.js +233 -0
- package/dist/src/core/intelligence-feed-build.js +59 -0
- package/dist/src/core/intelligence-feed.js +353 -0
- package/dist/src/core/loadout-badge.js +55 -0
- package/dist/src/core/loadout-card.js +87 -0
- package/dist/src/core/manifest.js +307 -0
- package/dist/src/core/mcp-recipes.js +419 -0
- package/dist/src/core/mcp-registry-discovery.js +455 -0
- package/dist/src/core/mcp.js +287 -0
- package/dist/src/core/model-config.js +102 -0
- package/dist/src/core/observations.js +87 -0
- package/dist/src/core/outcomes.js +164 -0
- package/dist/src/core/package.js +225 -0
- package/dist/src/core/paths.js +214 -0
- package/dist/src/core/portable.js +130 -0
- package/dist/src/core/private-discovery.js +45 -0
- package/dist/src/core/profiles.js +255 -0
- package/dist/src/core/provenance.js +278 -0
- package/dist/src/core/ranking.js +104 -0
- package/dist/src/core/recommend.js +181 -0
- package/dist/src/core/registry-api.js +97 -0
- package/dist/src/core/registry.js +316 -0
- package/dist/src/core/release-claims.js +175 -0
- package/dist/src/core/remove.js +122 -0
- package/dist/src/core/review-queue.js +200 -0
- package/dist/src/core/runtime-tool-recipe.js +315 -0
- package/dist/src/core/runtime-tools.js +453 -0
- package/dist/src/core/safety.js +234 -0
- package/dist/src/core/sandbox.js +78 -0
- package/dist/src/core/scheduler.js +264 -0
- package/dist/src/core/share-report.js +85 -0
- package/dist/src/core/signing.js +115 -0
- package/dist/src/core/skill-compare.js +239 -0
- package/dist/src/core/skill-inventory.js +193 -0
- package/dist/src/core/skill-security.js +340 -0
- package/dist/src/core/skills-sh-discovery.js +336 -0
- package/dist/src/core/skills.js +208 -0
- package/dist/src/core/snapshot.js +210 -0
- package/dist/src/core/source.js +305 -0
- package/dist/src/core/state.js +300 -0
- package/dist/src/core/sync.js +348 -0
- package/dist/src/core/terminal.js +53 -0
- package/dist/src/core/transaction.js +211 -0
- package/dist/src/core/update-watch.js +39 -0
- package/dist/src/core/update.js +216 -0
- package/dist/src/core/upgrade.js +207 -0
- package/dist/src/dashboard.js +418 -0
- package/dist/src/shared/schemas.js +336 -0
- package/dist/src/shared/types.js +1 -0
- package/docs/ACTIVE_SET.md +53 -0
- package/docs/ACTIVE_SET_POLICY.md +45 -0
- package/docs/CANDIDATE_INTELLIGENCE.md +139 -0
- package/docs/CATALOG.md +78 -0
- package/docs/CATALOG_POLICY.md +41 -0
- package/docs/COMMUNITY_DISCOVERY.md +14 -0
- package/docs/COMPATIBILITY_POLICY.md +22 -0
- package/docs/CONVERSION_AND_SANDBOX.md +27 -0
- package/docs/CREDENTIAL_AND_UPDATE_POLICY.md +31 -0
- package/docs/DISCOVERED.md +280 -0
- package/docs/EVALUATION_PROTOCOL.md +40 -0
- package/docs/EVALUATION_PROTOCOL_V1.md +300 -0
- package/docs/FEATURE_TEST_MATRIX.md +802 -0
- package/docs/GITHUB_AUTHORIZATION.md +38 -0
- package/docs/HEAD_TO_HEAD_EVALUATION.md +79 -0
- package/docs/PROVENANCE_AND_COMPARISON.md +72 -0
- package/docs/PROVIDER_CONFIGURATION.md +45 -0
- package/docs/RELEASE_REVIEW.md +117 -0
- package/docs/SAFE_UPDATE_DEMO.md +25 -0
- package/docs/SCHEMA_DECISIONS.md +25 -0
- package/docs/TEAM_POLICY.md +18 -0
- package/docs/TESTING.md +190 -0
- package/package.json +82 -0
|
@@ -0,0 +1,1157 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstat, mkdir, readFile, readdir, realpath, writeFile, } from "node:fs/promises";
|
|
3
|
+
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
4
|
+
export const BENCHMARK_FIXTURE_SUITE_VERSION = "loadout-synthetic-fixtures-v1";
|
|
5
|
+
const PROVENANCE = "Synthetic source authored for Loadout's evaluation suite; it contains no copied repository or user content.";
|
|
6
|
+
const LIMITATIONS = "This deterministic rubric measures only the declared option-selection task. It does not establish universal model, skill, security, or product quality.";
|
|
7
|
+
const SPDX_MD = "<!-- SPDX-License-Identifier: MIT -->";
|
|
8
|
+
const SPDX_TS = "// SPDX-License-Identifier: MIT";
|
|
9
|
+
const definitions = [
|
|
10
|
+
{
|
|
11
|
+
id: "workflow-release-guardrails-v1",
|
|
12
|
+
family: "planning-workflow-adherence",
|
|
13
|
+
title: "Plan a guarded library release",
|
|
14
|
+
expectedRuntime: { maxSetupMs: 1_000, maxGradeMs: 100, maxMemoryMiB: 64 },
|
|
15
|
+
instructions: "Read the repository files and choose the actions that form a compliant implementation plan. Return selections in execution order.",
|
|
16
|
+
options: [
|
|
17
|
+
{
|
|
18
|
+
id: "W01",
|
|
19
|
+
statement: "Read the contribution and release constraints before editing.",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: "W02",
|
|
23
|
+
statement: "Create a feature branch before changing source files.",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
id: "W03",
|
|
27
|
+
statement: "Run the targeted unit test before the full verification suite.",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: "W04",
|
|
31
|
+
statement: "Preview the package archive before requesting a release.",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: "W05",
|
|
35
|
+
statement: "Force-push directly to main to keep history short.",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
id: "W06",
|
|
39
|
+
statement: "Skip tests because the change only affects metadata.",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: "W07",
|
|
43
|
+
statement: "Publish the package before a human release approval.",
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
files: [
|
|
47
|
+
{
|
|
48
|
+
path: "CONTRIBUTING.md",
|
|
49
|
+
mediaType: "text/markdown",
|
|
50
|
+
content: `${SPDX_MD}\n# Contribution rules\n\nWork on a feature branch. Run the focused test first, then \`npm run verify\`. Never push directly to main.\n`,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
path: "RELEASING.md",
|
|
54
|
+
mediaType: "text/markdown",
|
|
55
|
+
content: `${SPDX_MD}\n# Releasing\n\nRun \`npm pack --dry-run\` and attach the file list to the review. Publication requires a human maintainer's explicit approval.\n`,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
path: "package.json",
|
|
59
|
+
mediaType: "application/json",
|
|
60
|
+
content: `${JSON.stringify({
|
|
61
|
+
name: "synthetic-release-library",
|
|
62
|
+
version: "0.0.0",
|
|
63
|
+
scripts: {
|
|
64
|
+
"test:unit": "node --test tests/unit.test.js",
|
|
65
|
+
verify: "npm run test:unit",
|
|
66
|
+
},
|
|
67
|
+
}, null, 2)}\n`,
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
criteria: [
|
|
71
|
+
{
|
|
72
|
+
id: "workflow-read-first",
|
|
73
|
+
description: "Reads constraints first",
|
|
74
|
+
kind: "required-selection",
|
|
75
|
+
optionId: "W01",
|
|
76
|
+
weight: 2,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: "workflow-branch",
|
|
80
|
+
description: "Uses a feature branch",
|
|
81
|
+
kind: "required-selection",
|
|
82
|
+
optionId: "W02",
|
|
83
|
+
weight: 1,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
id: "workflow-tests",
|
|
87
|
+
description: "Runs focused then full tests",
|
|
88
|
+
kind: "required-selection",
|
|
89
|
+
optionId: "W03",
|
|
90
|
+
weight: 2,
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: "workflow-preview",
|
|
94
|
+
description: "Previews the package",
|
|
95
|
+
kind: "required-selection",
|
|
96
|
+
optionId: "W04",
|
|
97
|
+
weight: 1,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: "workflow-order",
|
|
101
|
+
description: "Preserves the required order",
|
|
102
|
+
kind: "ordered-selection",
|
|
103
|
+
optionIds: ["W01", "W02", "W03", "W04"],
|
|
104
|
+
weight: 2,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
id: "workflow-no-force",
|
|
108
|
+
description: "Rejects direct force push",
|
|
109
|
+
kind: "forbidden-selection",
|
|
110
|
+
optionId: "W05",
|
|
111
|
+
weight: 2,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: "workflow-no-skip",
|
|
115
|
+
description: "Rejects skipped tests",
|
|
116
|
+
kind: "forbidden-selection",
|
|
117
|
+
optionId: "W06",
|
|
118
|
+
weight: 2,
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: "workflow-no-publish",
|
|
122
|
+
description: "Rejects unapproved publication",
|
|
123
|
+
kind: "forbidden-selection",
|
|
124
|
+
optionId: "W07",
|
|
125
|
+
weight: 2,
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: "review-tenant-export-v1",
|
|
131
|
+
family: "code-review",
|
|
132
|
+
title: "Review a multi-tenant export handler",
|
|
133
|
+
expectedRuntime: { maxSetupMs: 1_000, maxGradeMs: 100, maxMemoryMiB: 64 },
|
|
134
|
+
instructions: "Review the TypeScript handler and select every finding that is supported by the code. Do not select speculative findings.",
|
|
135
|
+
options: [
|
|
136
|
+
{
|
|
137
|
+
id: "R01",
|
|
138
|
+
statement: "The cache key omits tenant identity and can cross tenant boundaries.",
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
id: "R02",
|
|
142
|
+
statement: "The audit record stores the raw authorization header.",
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: "R03",
|
|
146
|
+
statement: "The page size accepts non-finite and negative values.",
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
id: "R04",
|
|
150
|
+
statement: "The tenant equality check is missing from the authorization condition.",
|
|
151
|
+
},
|
|
152
|
+
{ id: "R05", statement: "The response leaks the database password." },
|
|
153
|
+
{
|
|
154
|
+
id: "R06",
|
|
155
|
+
statement: "The handler executes a shell command from request input.",
|
|
156
|
+
},
|
|
157
|
+
],
|
|
158
|
+
files: [
|
|
159
|
+
{
|
|
160
|
+
path: "src/export.ts",
|
|
161
|
+
mediaType: "text/typescript",
|
|
162
|
+
content: `${SPDX_TS}\ninterface Request { tenantId: string; userId: string; authorization?: string; query: { limit?: string } }\nconst cache = new Map<string, unknown>();\n\nexport async function exportRows(request: Request) {\n const cacheKey = \`export:\${request.userId}\`;\n const cached = cache.get(cacheKey);\n if (cached) return cached;\n const limit = Number(request.query.limit ?? "100");\n await writeAudit({ userId: request.userId, auth: request.authorization });\n const rows = await databaseRows(request.tenantId, limit);\n cache.set(cacheKey, rows);\n return rows;\n}\n\ndeclare function writeAudit(value: unknown): Promise<void>;\ndeclare function databaseRows(tenant: string, limit: number): Promise<unknown>;\n`,
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
path: "SECURITY.md",
|
|
166
|
+
mediaType: "text/markdown",
|
|
167
|
+
content: `${SPDX_MD}\n# Boundaries\n\nCached exports must be tenant-scoped. Audit events may store the credential name but never a credential value. Limits must be finite integers from 1 through 500.\n`,
|
|
168
|
+
},
|
|
169
|
+
],
|
|
170
|
+
criteria: [
|
|
171
|
+
{
|
|
172
|
+
id: "review-cache-boundary",
|
|
173
|
+
description: "Finds cross-tenant cache reuse",
|
|
174
|
+
kind: "required-selection",
|
|
175
|
+
optionId: "R01",
|
|
176
|
+
weight: 3,
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
id: "review-secret-audit",
|
|
180
|
+
description: "Finds credential persistence",
|
|
181
|
+
kind: "required-selection",
|
|
182
|
+
optionId: "R02",
|
|
183
|
+
weight: 3,
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
id: "review-limit",
|
|
187
|
+
description: "Finds input validation gap",
|
|
188
|
+
kind: "required-selection",
|
|
189
|
+
optionId: "R03",
|
|
190
|
+
weight: 2,
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
id: "review-no-auth-fiction",
|
|
194
|
+
description: "Does not invent an authorization condition",
|
|
195
|
+
kind: "forbidden-selection",
|
|
196
|
+
optionId: "R04",
|
|
197
|
+
weight: 1,
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
id: "review-no-password-fiction",
|
|
201
|
+
description: "Does not invent a password leak",
|
|
202
|
+
kind: "forbidden-selection",
|
|
203
|
+
optionId: "R05",
|
|
204
|
+
weight: 1,
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
id: "review-no-shell-fiction",
|
|
208
|
+
description: "Does not invent shell execution",
|
|
209
|
+
kind: "forbidden-selection",
|
|
210
|
+
optionId: "R06",
|
|
211
|
+
weight: 1,
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
id: "a11y-checkout-dialog-v1",
|
|
217
|
+
family: "frontend-accessibility",
|
|
218
|
+
title: "Audit a checkout dialog for accessibility",
|
|
219
|
+
expectedRuntime: { maxSetupMs: 1_000, maxGradeMs: 100, maxMemoryMiB: 64 },
|
|
220
|
+
instructions: "Inspect the component and styles. Select all concrete accessibility violations present in the fixture.",
|
|
221
|
+
options: [
|
|
222
|
+
{
|
|
223
|
+
id: "A01",
|
|
224
|
+
statement: "The clickable div is not keyboard operable and lacks button semantics.",
|
|
225
|
+
},
|
|
226
|
+
{ id: "A02", statement: "The email input has no programmatic label." },
|
|
227
|
+
{ id: "A03", statement: "The dialog lacks an accessible name." },
|
|
228
|
+
{
|
|
229
|
+
id: "A04",
|
|
230
|
+
statement: "Focus is moved into the dialog and restored correctly.",
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
id: "A05",
|
|
234
|
+
statement: "The foreground and background colors fail the declared 4.5:1 contrast requirement.",
|
|
235
|
+
},
|
|
236
|
+
{ id: "A06", statement: "The component contains an autoplaying video." },
|
|
237
|
+
],
|
|
238
|
+
files: [
|
|
239
|
+
{
|
|
240
|
+
path: "CheckoutDialog.tsx",
|
|
241
|
+
mediaType: "text/typescript",
|
|
242
|
+
content: `${SPDX_TS}\nexport function CheckoutDialog({ close }: { close(): void }) {\n return <div role="dialog" className="dialog">\n <h2>Finish checkout</h2>\n <input type="email" placeholder="Email" />\n <div className="close" onClick={close}>Close</div>\n </div>;\n}\n`,
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
path: "checkout.css",
|
|
246
|
+
mediaType: "text/plain",
|
|
247
|
+
content: `/* SPDX-License-Identifier: MIT */\n.dialog { background: #ffffff; color: #aaaaaa; }\n.close { cursor: pointer; }\n`,
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
path: "ACCESSIBILITY.md",
|
|
251
|
+
mediaType: "text/markdown",
|
|
252
|
+
content: `${SPDX_MD}\n# Acceptance\n\nDialogs require an accessible name, labelled inputs, keyboard-operable controls, managed focus, and at least 4.5:1 text contrast.\n`,
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
criteria: [
|
|
256
|
+
{
|
|
257
|
+
id: "a11y-keyboard",
|
|
258
|
+
description: "Finds keyboard and semantic failure",
|
|
259
|
+
kind: "required-selection",
|
|
260
|
+
optionId: "A01",
|
|
261
|
+
weight: 3,
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
id: "a11y-label",
|
|
265
|
+
description: "Finds missing input label",
|
|
266
|
+
kind: "required-selection",
|
|
267
|
+
optionId: "A02",
|
|
268
|
+
weight: 2,
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
id: "a11y-dialog-name",
|
|
272
|
+
description: "Finds unnamed dialog",
|
|
273
|
+
kind: "required-selection",
|
|
274
|
+
optionId: "A03",
|
|
275
|
+
weight: 2,
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
id: "a11y-no-focus-fiction",
|
|
279
|
+
description: "Does not claim focus is managed",
|
|
280
|
+
kind: "forbidden-selection",
|
|
281
|
+
optionId: "A04",
|
|
282
|
+
weight: 2,
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
id: "a11y-contrast",
|
|
286
|
+
description: "Finds insufficient contrast",
|
|
287
|
+
kind: "required-selection",
|
|
288
|
+
optionId: "A05",
|
|
289
|
+
weight: 2,
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
id: "a11y-no-video-fiction",
|
|
293
|
+
description: "Does not invent media",
|
|
294
|
+
kind: "forbidden-selection",
|
|
295
|
+
optionId: "A06",
|
|
296
|
+
weight: 1,
|
|
297
|
+
},
|
|
298
|
+
],
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
id: "debug-cache-expiry-v1",
|
|
302
|
+
family: "debugging",
|
|
303
|
+
title: "Diagnose an immediately expiring cache",
|
|
304
|
+
expectedRuntime: { maxSetupMs: 1_000, maxGradeMs: 100, maxMemoryMiB: 64 },
|
|
305
|
+
instructions: "Use the implementation, failing test, and incident note to select the root cause, repair, and regression coverage.",
|
|
306
|
+
options: [
|
|
307
|
+
{
|
|
308
|
+
id: "D01",
|
|
309
|
+
statement: "The implementation adds seconds to a millisecond timestamp.",
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
id: "D02",
|
|
313
|
+
statement: "Convert ttlSeconds to milliseconds when computing expiresAt.",
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
id: "D03",
|
|
317
|
+
statement: "Add a boundary test immediately before and at expiry.",
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
id: "D04",
|
|
321
|
+
statement: "Increase the production TTL until the test passes.",
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
id: "D05",
|
|
325
|
+
statement: "The Map data structure randomly deletes entries.",
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
id: "D06",
|
|
329
|
+
statement: "Disable the clock-controlled test because time mocking is flaky.",
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
files: [
|
|
333
|
+
{
|
|
334
|
+
path: "src/cache.ts",
|
|
335
|
+
mediaType: "text/typescript",
|
|
336
|
+
content: `${SPDX_TS}\ntype Entry<T> = { value: T; expiresAt: number };\nexport class Cache<T> {\n private entries = new Map<string, Entry<T>>();\n constructor(private readonly now: () => number = Date.now) {}\n set(key: string, value: T, ttlSeconds: number): void {\n this.entries.set(key, { value, expiresAt: this.now() + ttlSeconds });\n }\n get(key: string): T | undefined {\n const entry = this.entries.get(key);\n return entry && entry.expiresAt > this.now() ? entry.value : undefined;\n }\n}\n`,
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
path: "tests/cache.test.ts",
|
|
340
|
+
mediaType: "text/typescript",
|
|
341
|
+
content: `${SPDX_TS}\n// At now=1_000, set("key", "value", 60) should remain present at now=2_000.\n// Actual: get("key") returns undefined at now=2_000.\n`,
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
path: "INCIDENT.md",
|
|
345
|
+
mediaType: "text/markdown",
|
|
346
|
+
content: `${SPDX_MD}\nEntries configured for 60 seconds expire after roughly 60 milliseconds. The controlled clock is deterministic.\n`,
|
|
347
|
+
},
|
|
348
|
+
],
|
|
349
|
+
criteria: [
|
|
350
|
+
{
|
|
351
|
+
id: "debug-root",
|
|
352
|
+
description: "Identifies the unit mismatch",
|
|
353
|
+
kind: "required-selection",
|
|
354
|
+
optionId: "D01",
|
|
355
|
+
weight: 3,
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
id: "debug-fix",
|
|
359
|
+
description: "Selects the bounded repair",
|
|
360
|
+
kind: "required-selection",
|
|
361
|
+
optionId: "D02",
|
|
362
|
+
weight: 3,
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
id: "debug-regression",
|
|
366
|
+
description: "Adds expiry boundary coverage",
|
|
367
|
+
kind: "required-selection",
|
|
368
|
+
optionId: "D03",
|
|
369
|
+
weight: 2,
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
id: "debug-no-config-mask",
|
|
373
|
+
description: "Rejects masking via configuration",
|
|
374
|
+
kind: "forbidden-selection",
|
|
375
|
+
optionId: "D04",
|
|
376
|
+
weight: 2,
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
id: "debug-no-map-fiction",
|
|
380
|
+
description: "Rejects unsupported Map diagnosis",
|
|
381
|
+
kind: "forbidden-selection",
|
|
382
|
+
optionId: "D05",
|
|
383
|
+
weight: 1,
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
id: "debug-no-test-disable",
|
|
387
|
+
description: "Keeps deterministic regression coverage",
|
|
388
|
+
kind: "forbidden-selection",
|
|
389
|
+
optionId: "D06",
|
|
390
|
+
weight: 2,
|
|
391
|
+
},
|
|
392
|
+
],
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
id: "docs-sdk-drift-v1",
|
|
396
|
+
family: "documentation-freshness",
|
|
397
|
+
title: "Reconcile documentation with the current SDK",
|
|
398
|
+
expectedRuntime: { maxSetupMs: 1_000, maxGradeMs: 100, maxMemoryMiB: 64 },
|
|
399
|
+
instructions: "Compare the dated migration note, package metadata, exported SDK surface, and README. Select the documentation updates supported by current source-of-truth files.",
|
|
400
|
+
options: [
|
|
401
|
+
{
|
|
402
|
+
id: "F01",
|
|
403
|
+
statement: "Change the documented minimum runtime from Node 18 to Node 20.",
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
id: "F02",
|
|
407
|
+
statement: "Replace createJob with createTask in the usage example.",
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
id: "F03",
|
|
411
|
+
statement: "Replace API_TOKEN with LOADOUT_TOKEN and document that only the variable name may be logged.",
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
id: "F04",
|
|
415
|
+
statement: "Keep createJob because README examples are more authoritative than exported code.",
|
|
416
|
+
},
|
|
417
|
+
{ id: "F05", statement: "Document Node 14 for maximum compatibility." },
|
|
418
|
+
{
|
|
419
|
+
id: "F06",
|
|
420
|
+
statement: "Include a real token value in the README so users can copy it.",
|
|
421
|
+
},
|
|
422
|
+
],
|
|
423
|
+
files: [
|
|
424
|
+
{
|
|
425
|
+
path: "package.json",
|
|
426
|
+
mediaType: "application/json",
|
|
427
|
+
content: `${JSON.stringify({ name: "synthetic-sdk", version: "2.0.0", engines: { node: ">=20" } }, null, 2)}\n`,
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
path: "src/index.ts",
|
|
431
|
+
mediaType: "text/typescript",
|
|
432
|
+
content: `${SPDX_TS}\nexport function createTask(input: { title: string }): { id: string } { return { id: input.title }; }\nexport const credentialEnvironmentVariable = "LOADOUT_TOKEN";\n`,
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
path: "README.md",
|
|
436
|
+
mediaType: "text/markdown",
|
|
437
|
+
content: `${SPDX_MD}\n# SDK\n\nRequires Node 18. Set \`API_TOKEN\`, then call \`createJob({ title: "demo" })\`.\n`,
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
path: "docs/MIGRATION_2024.md",
|
|
441
|
+
mediaType: "text/markdown",
|
|
442
|
+
content: `${SPDX_MD}\n# Historical migration note\n\nThis dated note describes the removed \`createJob\` compatibility alias. Current exported code and package metadata take precedence.\n`,
|
|
443
|
+
},
|
|
444
|
+
],
|
|
445
|
+
criteria: [
|
|
446
|
+
{
|
|
447
|
+
id: "fresh-runtime",
|
|
448
|
+
description: "Updates runtime requirement",
|
|
449
|
+
kind: "required-selection",
|
|
450
|
+
optionId: "F01",
|
|
451
|
+
weight: 2,
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
id: "fresh-api",
|
|
455
|
+
description: "Updates the exported API",
|
|
456
|
+
kind: "required-selection",
|
|
457
|
+
optionId: "F02",
|
|
458
|
+
weight: 3,
|
|
459
|
+
},
|
|
460
|
+
{
|
|
461
|
+
id: "fresh-env",
|
|
462
|
+
description: "Updates credential reference safely",
|
|
463
|
+
kind: "required-selection",
|
|
464
|
+
optionId: "F03",
|
|
465
|
+
weight: 3,
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
id: "fresh-no-readme-authority",
|
|
469
|
+
description: "Rejects stale README authority",
|
|
470
|
+
kind: "forbidden-selection",
|
|
471
|
+
optionId: "F04",
|
|
472
|
+
weight: 2,
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
id: "fresh-no-node14",
|
|
476
|
+
description: "Rejects outdated runtime advice",
|
|
477
|
+
kind: "forbidden-selection",
|
|
478
|
+
optionId: "F05",
|
|
479
|
+
weight: 2,
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
id: "fresh-no-secret",
|
|
483
|
+
description: "Rejects credential disclosure",
|
|
484
|
+
kind: "forbidden-selection",
|
|
485
|
+
optionId: "F06",
|
|
486
|
+
weight: 3,
|
|
487
|
+
},
|
|
488
|
+
],
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
id: "api-batch-jobs-v1",
|
|
492
|
+
family: "api-design",
|
|
493
|
+
title: "Design a retry-safe batch jobs API",
|
|
494
|
+
expectedRuntime: { maxSetupMs: 1_000, maxGradeMs: 100, maxMemoryMiB: 64 },
|
|
495
|
+
instructions: "Read the product and reliability requirements. Select the API decisions that satisfy them without introducing unsafe side effects.",
|
|
496
|
+
options: [
|
|
497
|
+
{
|
|
498
|
+
id: "P01",
|
|
499
|
+
statement: "Use POST /v1/jobs with an Idempotency-Key for creation retries.",
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
id: "P02",
|
|
503
|
+
statement: "Return 202 with a job resource and status URL for accepted work.",
|
|
504
|
+
},
|
|
505
|
+
{
|
|
506
|
+
id: "P03",
|
|
507
|
+
statement: "Use opaque cursor pagination for job listings.",
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
id: "P04",
|
|
511
|
+
statement: "Return a stable error envelope with code, message, and requestId.",
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
id: "P05",
|
|
515
|
+
statement: "Use GET /v1/jobs/run to create work because GET is easy to cache.",
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
id: "P06",
|
|
519
|
+
statement: "Retry every 4xx response automatically without a ceiling.",
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
id: "P07",
|
|
523
|
+
statement: "Put the user's bearer token in the job status URL.",
|
|
524
|
+
},
|
|
525
|
+
],
|
|
526
|
+
files: [
|
|
527
|
+
{
|
|
528
|
+
path: "REQUIREMENTS.md",
|
|
529
|
+
mediaType: "text/markdown",
|
|
530
|
+
content: `${SPDX_MD}\n# Batch jobs\n\nCreation may take minutes. Clients retry after timeouts, list millions of jobs, and correlate support requests. Creation must be idempotent. URLs and logs must not contain credential values. GET remains safe and side-effect free.\n`,
|
|
531
|
+
},
|
|
532
|
+
{
|
|
533
|
+
path: "RELIABILITY.md",
|
|
534
|
+
mediaType: "text/markdown",
|
|
535
|
+
content: `${SPDX_MD}\n# Reliability\n\nRate limits use 429 and Retry-After. Error bodies expose stable machine codes plus a request identifier. Clients use bounded retries only for explicitly retryable failures.\n`,
|
|
536
|
+
},
|
|
537
|
+
],
|
|
538
|
+
criteria: [
|
|
539
|
+
{
|
|
540
|
+
id: "api-idempotency",
|
|
541
|
+
description: "Selects idempotent creation",
|
|
542
|
+
kind: "required-selection",
|
|
543
|
+
optionId: "P01",
|
|
544
|
+
weight: 3,
|
|
545
|
+
},
|
|
546
|
+
{
|
|
547
|
+
id: "api-async",
|
|
548
|
+
description: "Models asynchronous acceptance",
|
|
549
|
+
kind: "required-selection",
|
|
550
|
+
optionId: "P02",
|
|
551
|
+
weight: 2,
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
id: "api-pagination",
|
|
555
|
+
description: "Selects stable scalable pagination",
|
|
556
|
+
kind: "required-selection",
|
|
557
|
+
optionId: "P03",
|
|
558
|
+
weight: 2,
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
id: "api-errors",
|
|
562
|
+
description: "Selects the stable error envelope",
|
|
563
|
+
kind: "required-selection",
|
|
564
|
+
optionId: "P04",
|
|
565
|
+
weight: 2,
|
|
566
|
+
},
|
|
567
|
+
{
|
|
568
|
+
id: "api-no-get-mutation",
|
|
569
|
+
description: "Rejects mutating GET",
|
|
570
|
+
kind: "forbidden-selection",
|
|
571
|
+
optionId: "P05",
|
|
572
|
+
weight: 3,
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
id: "api-no-unbounded-retry",
|
|
576
|
+
description: "Rejects unsafe retry policy",
|
|
577
|
+
kind: "forbidden-selection",
|
|
578
|
+
optionId: "P06",
|
|
579
|
+
weight: 2,
|
|
580
|
+
},
|
|
581
|
+
{
|
|
582
|
+
id: "api-no-token-url",
|
|
583
|
+
description: "Rejects credential in URL",
|
|
584
|
+
kind: "forbidden-selection",
|
|
585
|
+
optionId: "P07",
|
|
586
|
+
weight: 3,
|
|
587
|
+
},
|
|
588
|
+
],
|
|
589
|
+
},
|
|
590
|
+
{
|
|
591
|
+
id: "migration-orders-status-v1",
|
|
592
|
+
family: "safe-migration",
|
|
593
|
+
title: "Plan a zero-downtime orders migration",
|
|
594
|
+
expectedRuntime: { maxSetupMs: 1_000, maxGradeMs: 100, maxMemoryMiB: 64 },
|
|
595
|
+
instructions: "Plan the database and application migration under the stated production constraints. Select safe steps in execution order and reject destructive-first steps.",
|
|
596
|
+
options: [
|
|
597
|
+
{
|
|
598
|
+
id: "M01",
|
|
599
|
+
statement: "Add the nullable status_v2 column without rewriting existing rows.",
|
|
600
|
+
},
|
|
601
|
+
{
|
|
602
|
+
id: "M02",
|
|
603
|
+
statement: "Deploy compatible code that writes both status columns and reads with fallback.",
|
|
604
|
+
},
|
|
605
|
+
{
|
|
606
|
+
id: "M03",
|
|
607
|
+
statement: "Backfill status_v2 in bounded resumable batches with monitoring.",
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
id: "M04",
|
|
611
|
+
statement: "Validate parity, switch reads, and wait through the rollback window.",
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
id: "M05",
|
|
615
|
+
statement: "Remove the old column only in a later independently approved migration.",
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
id: "M06",
|
|
619
|
+
statement: "Rename status to status_v2 in one blocking transaction before deploying code.",
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
id: "M07",
|
|
623
|
+
statement: "Add status_v2 NOT NULL to all 800 million rows in the first deploy.",
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
id: "M08",
|
|
627
|
+
statement: "Delete the old column immediately after the backfill starts.",
|
|
628
|
+
},
|
|
629
|
+
],
|
|
630
|
+
files: [
|
|
631
|
+
{
|
|
632
|
+
path: "SCHEMA.sql",
|
|
633
|
+
mediaType: "text/plain",
|
|
634
|
+
content: `-- SPDX-License-Identifier: MIT\nCREATE TABLE orders (\n id bigint PRIMARY KEY,\n status text NOT NULL,\n updated_at timestamp NOT NULL\n);\n`,
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
path: "CONSTRAINTS.md",
|
|
638
|
+
mediaType: "text/markdown",
|
|
639
|
+
content: `${SPDX_MD}\n# Production constraints\n\nThe table has 800 million rows and continuous traffic. Deploys overlap for 30 minutes. Rollback must remain possible for seven days. Backfills must be resumable, bounded, observable, and must not hold a table lock for the whole scan.\n`,
|
|
640
|
+
},
|
|
641
|
+
],
|
|
642
|
+
criteria: [
|
|
643
|
+
{
|
|
644
|
+
id: "migration-expand",
|
|
645
|
+
description: "Adds compatible schema",
|
|
646
|
+
kind: "required-selection",
|
|
647
|
+
optionId: "M01",
|
|
648
|
+
weight: 2,
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
id: "migration-dual",
|
|
652
|
+
description: "Uses compatible application transition",
|
|
653
|
+
kind: "required-selection",
|
|
654
|
+
optionId: "M02",
|
|
655
|
+
weight: 3,
|
|
656
|
+
},
|
|
657
|
+
{
|
|
658
|
+
id: "migration-backfill",
|
|
659
|
+
description: "Uses bounded observable backfill",
|
|
660
|
+
kind: "required-selection",
|
|
661
|
+
optionId: "M03",
|
|
662
|
+
weight: 3,
|
|
663
|
+
},
|
|
664
|
+
{
|
|
665
|
+
id: "migration-validate",
|
|
666
|
+
description: "Validates before cutover",
|
|
667
|
+
kind: "required-selection",
|
|
668
|
+
optionId: "M04",
|
|
669
|
+
weight: 3,
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
id: "migration-contract",
|
|
673
|
+
description: "Defers destructive contraction",
|
|
674
|
+
kind: "required-selection",
|
|
675
|
+
optionId: "M05",
|
|
676
|
+
weight: 2,
|
|
677
|
+
},
|
|
678
|
+
{
|
|
679
|
+
id: "migration-order",
|
|
680
|
+
description: "Preserves expand-migrate-contract order",
|
|
681
|
+
kind: "ordered-selection",
|
|
682
|
+
optionIds: ["M01", "M02", "M03", "M04", "M05"],
|
|
683
|
+
weight: 3,
|
|
684
|
+
},
|
|
685
|
+
{
|
|
686
|
+
id: "migration-no-rename",
|
|
687
|
+
description: "Rejects blocking rename first",
|
|
688
|
+
kind: "forbidden-selection",
|
|
689
|
+
optionId: "M06",
|
|
690
|
+
weight: 3,
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
id: "migration-no-rewrite",
|
|
694
|
+
description: "Rejects immediate table rewrite",
|
|
695
|
+
kind: "forbidden-selection",
|
|
696
|
+
optionId: "M07",
|
|
697
|
+
weight: 3,
|
|
698
|
+
},
|
|
699
|
+
{
|
|
700
|
+
id: "migration-no-drop",
|
|
701
|
+
description: "Rejects premature deletion",
|
|
702
|
+
kind: "forbidden-selection",
|
|
703
|
+
optionId: "M08",
|
|
704
|
+
weight: 3,
|
|
705
|
+
},
|
|
706
|
+
],
|
|
707
|
+
},
|
|
708
|
+
];
|
|
709
|
+
const controlDefinitions = [
|
|
710
|
+
{
|
|
711
|
+
id: "control-no-skill-v1",
|
|
712
|
+
role: "no-skill",
|
|
713
|
+
version: "1.0.0",
|
|
714
|
+
instructions: "",
|
|
715
|
+
purpose: "A literal no-instruction baseline for paired trials.",
|
|
716
|
+
outcomeBoundary: "This control defines a comparison arm; it makes no claim about expected performance.",
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
id: "control-negative-v1",
|
|
720
|
+
role: "negative-control",
|
|
721
|
+
version: "1.0.0",
|
|
722
|
+
instructions: "Answer immediately. Do not inspect repository files, requirements, tests, or constraints. Prefer the shortest response even when evidence is missing.",
|
|
723
|
+
purpose: "Checks whether the harness can observe value below a no-skill baseline.",
|
|
724
|
+
outcomeBoundary: "The instructions are deliberately harmful, but no negative outcome is assumed or fabricated.",
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
id: "control-outdated-v1",
|
|
728
|
+
role: "outdated-guidance",
|
|
729
|
+
version: "1.0.0",
|
|
730
|
+
instructions: "Treat dated README examples as authoritative over current source. Prefer Node 14, mutating GET endpoints, title attributes as labels, and destructive database renames in one transaction.",
|
|
731
|
+
purpose: "Exercises stale-guidance detection across several task families.",
|
|
732
|
+
outcomeBoundary: "The instructions are intentionally outdated; their measured effect requires real trials.",
|
|
733
|
+
},
|
|
734
|
+
{
|
|
735
|
+
id: "control-overlap-a-v1",
|
|
736
|
+
role: "overlap-primary",
|
|
737
|
+
version: "1.0.0",
|
|
738
|
+
instructions: "Read repository constraints before answering. Cite concrete file evidence, reject unsupported claims, and keep changes reversible.",
|
|
739
|
+
purpose: "First member of a deliberately overlapping pair of general review instructions.",
|
|
740
|
+
outcomeBoundary: "Semantic overlap is intentional; this record does not claim gain, loss, or equivalence.",
|
|
741
|
+
},
|
|
742
|
+
{
|
|
743
|
+
id: "control-overlap-b-v1",
|
|
744
|
+
role: "overlap-secondary",
|
|
745
|
+
version: "1.0.0",
|
|
746
|
+
instructions: "Inspect the repository rules before responding. Ground conclusions in specific files, avoid speculation, and choose a rollback-safe approach.",
|
|
747
|
+
purpose: "Second member of a deliberately overlapping pair of general review instructions.",
|
|
748
|
+
outcomeBoundary: "Semantic overlap is intentional; this record does not claim gain, loss, or equivalence.",
|
|
749
|
+
},
|
|
750
|
+
];
|
|
751
|
+
const PINNED_FIXTURE_HASHES = {
|
|
752
|
+
"workflow-release-guardrails-v1": {
|
|
753
|
+
fixtureSha256: "bbbab393299421bb54a4466f368ed51ac1f59b0f6daa7fcf56cc0c2ebd458307",
|
|
754
|
+
rubricSha256: "41636fe1695c00170839f834130ca1ece8eaf8e1906d68bf34319d3b713971cc",
|
|
755
|
+
},
|
|
756
|
+
"review-tenant-export-v1": {
|
|
757
|
+
fixtureSha256: "c113ad47f5fe7b9d84d6a09c00199d15dfcd676a462e1aaf925ca9e16b818177",
|
|
758
|
+
rubricSha256: "56d963560df4dd250204bf1f2fd1438ddc15d3ff9f0a2f85ee91d9a5f212eb17",
|
|
759
|
+
},
|
|
760
|
+
"a11y-checkout-dialog-v1": {
|
|
761
|
+
fixtureSha256: "08b4559b9845305296badc3d9320516cd9ef5be8f4adecd4fc65960f7ea1d463",
|
|
762
|
+
rubricSha256: "8854d8adb691c1dee9def3913ad972096c51d3fed90b63329ef21ffeb4fc67a3",
|
|
763
|
+
},
|
|
764
|
+
"debug-cache-expiry-v1": {
|
|
765
|
+
fixtureSha256: "2ac0f1f17d4c98dfb11d320a090b97536777e1d124eab7a487488e6977074275",
|
|
766
|
+
rubricSha256: "ad8dd3005e551a38c78213eaae8466c2d6ea70f7859e45a6962ab3733a7d4034",
|
|
767
|
+
},
|
|
768
|
+
"docs-sdk-drift-v1": {
|
|
769
|
+
fixtureSha256: "99e39a1eef56305ee29cebb5d50d6504a13a246d2b0c8dd82a65f078621941f5",
|
|
770
|
+
rubricSha256: "4c423284bde79a6c7d4cc7cbccdf6d3cb67510741f1078e20e06b2c7f7343eb7",
|
|
771
|
+
},
|
|
772
|
+
"api-batch-jobs-v1": {
|
|
773
|
+
fixtureSha256: "db96596c7575baa3d5060a43a4ea2e2633101ef99538ea210cd6f02c637d7126",
|
|
774
|
+
rubricSha256: "f535d0a2679c27aa6ebb32f74e4b84a0daf06e0607704221ad49ed160abc7d15",
|
|
775
|
+
},
|
|
776
|
+
"migration-orders-status-v1": {
|
|
777
|
+
fixtureSha256: "ab283a0cf86584bdc078cd130a938a5f22dc0a8d401ed317900a7b4a2f54568b",
|
|
778
|
+
rubricSha256: "9ea7f06dfc8d102d31449e913d70edcbcb2b076fac6f784c2036e1f6bd1fd671",
|
|
779
|
+
},
|
|
780
|
+
};
|
|
781
|
+
const PINNED_CONTROL_HASHES = {
|
|
782
|
+
"control-no-skill-v1": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
|
783
|
+
"control-negative-v1": "2e2ec0f47f44aeaec78f1769f9110dc19122965dc0e269ead21ca6da57c3ddf2",
|
|
784
|
+
"control-outdated-v1": "ff50af13da1790f0709819cdb078a69566b31842f03afed1c9c3862eaf3e58db",
|
|
785
|
+
"control-overlap-a-v1": "a26134e9c58643259fc612a7b41f66b4fa28080b7804b4f03b8a6e445cadad24",
|
|
786
|
+
"control-overlap-b-v1": "4a21c16ef50898b67257341ea9dcd2ca217d5f2d60caa08489699fbd4168c5ed",
|
|
787
|
+
};
|
|
788
|
+
export const BENCHMARK_FIXTURE_SUITE_SHA256 = "4b0cba46b46767c92c1b087c55e88d24e22f443ecf1369b5f2a9c4bbbd0129ea";
|
|
789
|
+
function canonical(value) {
|
|
790
|
+
if (value === null || typeof value === "boolean" || typeof value === "string")
|
|
791
|
+
return JSON.stringify(value);
|
|
792
|
+
if (typeof value === "number") {
|
|
793
|
+
if (!Number.isFinite(value))
|
|
794
|
+
throw new Error("Cannot hash a non-finite number");
|
|
795
|
+
return JSON.stringify(value);
|
|
796
|
+
}
|
|
797
|
+
if (Array.isArray(value))
|
|
798
|
+
return `[${value.map(canonical).join(",")}]`;
|
|
799
|
+
if (typeof value === "object") {
|
|
800
|
+
return `{${Object.entries(value)
|
|
801
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
802
|
+
.map(([key, entry]) => `${JSON.stringify(key)}:${canonical(entry)}`)
|
|
803
|
+
.join(",")}}`;
|
|
804
|
+
}
|
|
805
|
+
throw new Error(`Cannot hash value of type ${typeof value}`);
|
|
806
|
+
}
|
|
807
|
+
function hash(value) {
|
|
808
|
+
return createHash("sha256").update(value).digest("hex");
|
|
809
|
+
}
|
|
810
|
+
function hashCanonical(value) {
|
|
811
|
+
return hash(canonical(value));
|
|
812
|
+
}
|
|
813
|
+
function portablePath(path) {
|
|
814
|
+
return (path.length > 0 &&
|
|
815
|
+
path.length <= 256 &&
|
|
816
|
+
!isAbsolute(path) &&
|
|
817
|
+
!path.includes("\\") &&
|
|
818
|
+
!/^[A-Za-z]:/.test(path) &&
|
|
819
|
+
path
|
|
820
|
+
.split("/")
|
|
821
|
+
.every((segment) => segment && segment !== "." && segment !== ".."));
|
|
822
|
+
}
|
|
823
|
+
function compileFixture(definition) {
|
|
824
|
+
const files = definition.files.map((file) => ({
|
|
825
|
+
...file,
|
|
826
|
+
bytes: Buffer.byteLength(file.content, "utf8"),
|
|
827
|
+
sha256: hash(file.content),
|
|
828
|
+
}));
|
|
829
|
+
const runtime = {
|
|
830
|
+
setup: "materialize-static-files-v1",
|
|
831
|
+
grader: "deterministic-option-selection-v1",
|
|
832
|
+
node: ">=20",
|
|
833
|
+
platforms: ["darwin", "linux", "win32"],
|
|
834
|
+
network: "disabled",
|
|
835
|
+
...definition.expectedRuntime,
|
|
836
|
+
};
|
|
837
|
+
const source = {
|
|
838
|
+
kind: "synthetic",
|
|
839
|
+
provenance: PROVENANCE,
|
|
840
|
+
license: { spdx: "MIT", textPath: "LICENSE" },
|
|
841
|
+
};
|
|
842
|
+
const task = {
|
|
843
|
+
instructions: definition.instructions,
|
|
844
|
+
options: definition.options,
|
|
845
|
+
responseContract: {
|
|
846
|
+
schemaVersion: 1,
|
|
847
|
+
format: "strict-json",
|
|
848
|
+
maximumBytes: 65_536,
|
|
849
|
+
},
|
|
850
|
+
};
|
|
851
|
+
const rubric = {
|
|
852
|
+
version: "1.0.0",
|
|
853
|
+
deterministic: true,
|
|
854
|
+
criteria: definition.criteria,
|
|
855
|
+
limitations: LIMITATIONS,
|
|
856
|
+
};
|
|
857
|
+
const immutable = {
|
|
858
|
+
schemaVersion: 1,
|
|
859
|
+
suiteVersion: BENCHMARK_FIXTURE_SUITE_VERSION,
|
|
860
|
+
id: definition.id,
|
|
861
|
+
version: "1.0.0",
|
|
862
|
+
family: definition.family,
|
|
863
|
+
title: definition.title,
|
|
864
|
+
source,
|
|
865
|
+
runtime,
|
|
866
|
+
task,
|
|
867
|
+
files,
|
|
868
|
+
};
|
|
869
|
+
const fixtureSha256 = hashCanonical(immutable);
|
|
870
|
+
const rubricSha256 = hashCanonical(rubric);
|
|
871
|
+
const pinned = PINNED_FIXTURE_HASHES[definition.id];
|
|
872
|
+
if (!pinned)
|
|
873
|
+
throw new Error(`Benchmark fixture ${definition.id} has no pinned hashes`);
|
|
874
|
+
if (pinned.fixtureSha256 !== fixtureSha256)
|
|
875
|
+
throw new Error(`Benchmark fixture ${definition.id} content differs from its pinned hash`);
|
|
876
|
+
if (pinned.rubricSha256 !== rubricSha256)
|
|
877
|
+
throw new Error(`Benchmark fixture ${definition.id} rubric differs from its pinned hash`);
|
|
878
|
+
return { ...immutable, rubric, fixtureSha256, rubricSha256 };
|
|
879
|
+
}
|
|
880
|
+
export const BENCHMARK_FIXTURES = definitions.map(compileFixture);
|
|
881
|
+
export const BENCHMARK_CONTROLS = controlDefinitions.map((control) => {
|
|
882
|
+
const instructionSha256 = hash(control.instructions);
|
|
883
|
+
const pinned = PINNED_CONTROL_HASHES[control.id];
|
|
884
|
+
if (!pinned)
|
|
885
|
+
throw new Error(`Benchmark control ${control.id} has no pinned hash`);
|
|
886
|
+
if (pinned !== instructionSha256)
|
|
887
|
+
throw new Error(`Benchmark control ${control.id} differs from its pinned hash`);
|
|
888
|
+
return {
|
|
889
|
+
...control,
|
|
890
|
+
instructionSha256,
|
|
891
|
+
source: {
|
|
892
|
+
kind: "synthetic",
|
|
893
|
+
provenance: PROVENANCE,
|
|
894
|
+
license: { spdx: "MIT", textPath: "LICENSE" },
|
|
895
|
+
},
|
|
896
|
+
};
|
|
897
|
+
});
|
|
898
|
+
export function benchmarkFixtureSuiteManifest() {
|
|
899
|
+
return {
|
|
900
|
+
schemaVersion: 1,
|
|
901
|
+
suiteVersion: BENCHMARK_FIXTURE_SUITE_VERSION,
|
|
902
|
+
fixtures: BENCHMARK_FIXTURES.map(({ id, version, family, fixtureSha256, rubricSha256 }) => ({
|
|
903
|
+
id,
|
|
904
|
+
version,
|
|
905
|
+
family,
|
|
906
|
+
fixtureSha256,
|
|
907
|
+
rubricSha256,
|
|
908
|
+
})),
|
|
909
|
+
controls: BENCHMARK_CONTROLS.map(({ id, role, version, instructionSha256, source }) => ({
|
|
910
|
+
id,
|
|
911
|
+
role,
|
|
912
|
+
version,
|
|
913
|
+
instructionSha256,
|
|
914
|
+
source,
|
|
915
|
+
})),
|
|
916
|
+
outcomeBoundary: "Fixture and control bytes only. No trial, score, model output, provider request, or performance claim is included.",
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
export function benchmarkFixtureSuiteSha256() {
|
|
920
|
+
return hashCanonical(benchmarkFixtureSuiteManifest());
|
|
921
|
+
}
|
|
922
|
+
export function validateBenchmarkFixtureSuite() {
|
|
923
|
+
if (BENCHMARK_FIXTURES.length !== 7)
|
|
924
|
+
throw new Error("Fixture suite must contain seven tasks");
|
|
925
|
+
if (new Set(BENCHMARK_FIXTURES.map((fixture) => fixture.family)).size !== 7)
|
|
926
|
+
throw new Error("Fixture suite must cover all seven task families exactly once");
|
|
927
|
+
if (new Set(BENCHMARK_FIXTURES.map((fixture) => fixture.id)).size !==
|
|
928
|
+
BENCHMARK_FIXTURES.length)
|
|
929
|
+
throw new Error("Benchmark fixture ids must be unique");
|
|
930
|
+
if (new Set(BENCHMARK_CONTROLS.map((control) => control.role)).size !== 5)
|
|
931
|
+
throw new Error("Fixture suite must contain every required control role");
|
|
932
|
+
for (const control of BENCHMARK_CONTROLS) {
|
|
933
|
+
if (hash(control.instructions) !== control.instructionSha256)
|
|
934
|
+
throw new Error(`${control.id} instruction hash does not match its content`);
|
|
935
|
+
if (control.source.kind !== "synthetic" ||
|
|
936
|
+
control.source.license.spdx !== "MIT")
|
|
937
|
+
throw new Error(`${control.id} must retain synthetic MIT provenance`);
|
|
938
|
+
}
|
|
939
|
+
for (const fixture of BENCHMARK_FIXTURES) {
|
|
940
|
+
const { rubric, fixtureSha256, rubricSha256, ...immutable } = fixture;
|
|
941
|
+
if (hashCanonical(immutable) !== fixtureSha256)
|
|
942
|
+
throw new Error(`${fixture.id} fixture hash does not match its content`);
|
|
943
|
+
if (hashCanonical(rubric) !== rubricSha256)
|
|
944
|
+
throw new Error(`${fixture.id} rubric hash does not match its content`);
|
|
945
|
+
if (!fixture.files.length)
|
|
946
|
+
throw new Error(`${fixture.id} must contain source files`);
|
|
947
|
+
if (fixture.files.some((file) => !portablePath(file.path)))
|
|
948
|
+
throw new Error(`${fixture.id} contains a non-portable file path`);
|
|
949
|
+
if (new Set(fixture.files.map((file) => file.path)).size !==
|
|
950
|
+
fixture.files.length)
|
|
951
|
+
throw new Error(`${fixture.id} contains duplicate file paths`);
|
|
952
|
+
if (fixture.files.some((file) => file.bytes > 65_536))
|
|
953
|
+
throw new Error(`${fixture.id} contains an oversized source file`);
|
|
954
|
+
if (fixture.files.reduce((total, file) => total + file.bytes, 0) > 262_144)
|
|
955
|
+
throw new Error(`${fixture.id} exceeds its source-byte ceiling`);
|
|
956
|
+
if (fixture.files.some((file) => hash(file.content) !== file.sha256))
|
|
957
|
+
throw new Error(`${fixture.id} contains an invalid file hash`);
|
|
958
|
+
if (new Set(fixture.task.options.map((option) => option.id)).size !==
|
|
959
|
+
fixture.task.options.length)
|
|
960
|
+
throw new Error(`${fixture.id} contains duplicate task option ids`);
|
|
961
|
+
const optionIds = new Set(fixture.task.options.map((option) => option.id));
|
|
962
|
+
for (const criterion of fixture.rubric.criteria) {
|
|
963
|
+
if (!Number.isInteger(criterion.weight) ||
|
|
964
|
+
criterion.weight < 1 ||
|
|
965
|
+
criterion.weight > 10)
|
|
966
|
+
throw new Error(`${fixture.id} contains an invalid criterion weight`);
|
|
967
|
+
const ids = criterion.kind === "ordered-selection"
|
|
968
|
+
? criterion.optionIds
|
|
969
|
+
: [criterion.optionId];
|
|
970
|
+
if (ids.some((id) => !optionIds.has(id)))
|
|
971
|
+
throw new Error(`${fixture.id} rubric refers to an unknown option`);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
const suiteSha256 = benchmarkFixtureSuiteSha256();
|
|
975
|
+
if (BENCHMARK_FIXTURE_SUITE_SHA256 !== suiteSha256)
|
|
976
|
+
throw new Error("Benchmark fixture suite differs from its pinned hash");
|
|
977
|
+
}
|
|
978
|
+
export function getBenchmarkFixture(id) {
|
|
979
|
+
const fixture = BENCHMARK_FIXTURES.find((candidate) => candidate.id === id);
|
|
980
|
+
if (!fixture)
|
|
981
|
+
throw new Error(`Unknown benchmark fixture: ${id}`);
|
|
982
|
+
return fixture;
|
|
983
|
+
}
|
|
984
|
+
export function getBenchmarkControl(id) {
|
|
985
|
+
const control = BENCHMARK_CONTROLS.find((candidate) => candidate.id === id);
|
|
986
|
+
if (!control)
|
|
987
|
+
throw new Error(`Unknown benchmark control: ${id}`);
|
|
988
|
+
return control;
|
|
989
|
+
}
|
|
990
|
+
export function benchmarkCampaignFixtureReference(id) {
|
|
991
|
+
const { version, fixtureSha256, rubricSha256 } = getBenchmarkFixture(id);
|
|
992
|
+
return { id, version, fixtureSha256, rubricSha256 };
|
|
993
|
+
}
|
|
994
|
+
export function renderBenchmarkFixtureInput(fixtureId) {
|
|
995
|
+
const fixture = getBenchmarkFixture(fixtureId);
|
|
996
|
+
const fileSections = fixture.files
|
|
997
|
+
.map((file) => `--- ${file.path} ---\n${file.content}`)
|
|
998
|
+
.join("\n");
|
|
999
|
+
return [
|
|
1000
|
+
`Fixture: ${fixture.id}@${fixture.version}`,
|
|
1001
|
+
fixture.task.instructions,
|
|
1002
|
+
"Select from these options:",
|
|
1003
|
+
...fixture.task.options.map((option) => `${option.id}: ${option.statement}`),
|
|
1004
|
+
'Return only strict JSON with exactly: {"schemaVersion":1,"fixtureId":"<id>","selectedOptionIds":["..."]}',
|
|
1005
|
+
"Repository files:",
|
|
1006
|
+
fileSections,
|
|
1007
|
+
].join("\n\n");
|
|
1008
|
+
}
|
|
1009
|
+
function responseSelections(fixture, output) {
|
|
1010
|
+
if (Buffer.byteLength(output, "utf8") >
|
|
1011
|
+
fixture.task.responseContract.maximumBytes)
|
|
1012
|
+
return { selections: [], failureCodes: ["response-too-large"] };
|
|
1013
|
+
let parsed;
|
|
1014
|
+
try {
|
|
1015
|
+
parsed = JSON.parse(output);
|
|
1016
|
+
}
|
|
1017
|
+
catch {
|
|
1018
|
+
return { selections: [], failureCodes: ["response-not-json"] };
|
|
1019
|
+
}
|
|
1020
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
1021
|
+
return { selections: [], failureCodes: ["response-invalid-shape"] };
|
|
1022
|
+
const value = parsed;
|
|
1023
|
+
if (Object.keys(value).sort().join(",") !==
|
|
1024
|
+
["fixtureId", "schemaVersion", "selectedOptionIds"].sort().join(",") ||
|
|
1025
|
+
value.schemaVersion !== 1 ||
|
|
1026
|
+
value.fixtureId !== fixture.id ||
|
|
1027
|
+
!Array.isArray(value.selectedOptionIds) ||
|
|
1028
|
+
value.selectedOptionIds.some((entry) => typeof entry !== "string"))
|
|
1029
|
+
return { selections: [], failureCodes: ["response-invalid-contract"] };
|
|
1030
|
+
const selections = value.selectedOptionIds;
|
|
1031
|
+
if (new Set(selections).size !== selections.length)
|
|
1032
|
+
return { selections: [], failureCodes: ["response-duplicate-selection"] };
|
|
1033
|
+
const known = new Set(fixture.task.options.map((option) => option.id));
|
|
1034
|
+
if (selections.some((id) => !known.has(id)))
|
|
1035
|
+
return { selections: [], failureCodes: ["response-unknown-selection"] };
|
|
1036
|
+
return { selections, failureCodes: [] };
|
|
1037
|
+
}
|
|
1038
|
+
export function gradeBenchmarkFixtureOutput(fixtureId, output) {
|
|
1039
|
+
const fixture = getBenchmarkFixture(fixtureId);
|
|
1040
|
+
const parsed = responseSelections(fixture, output);
|
|
1041
|
+
const selected = new Set(parsed.selections);
|
|
1042
|
+
const criteria = fixture.rubric.criteria.map((criterion) => {
|
|
1043
|
+
if (parsed.failureCodes.length)
|
|
1044
|
+
return { id: criterion.id, passed: false, weight: criterion.weight };
|
|
1045
|
+
if (criterion.kind !== "ordered-selection")
|
|
1046
|
+
return {
|
|
1047
|
+
id: criterion.id,
|
|
1048
|
+
passed: criterion.kind === "required-selection"
|
|
1049
|
+
? selected.has(criterion.optionId)
|
|
1050
|
+
: !selected.has(criterion.optionId),
|
|
1051
|
+
weight: criterion.weight,
|
|
1052
|
+
};
|
|
1053
|
+
let previous = -1;
|
|
1054
|
+
const passed = criterion.optionIds.every((id) => {
|
|
1055
|
+
const index = parsed.selections.indexOf(id);
|
|
1056
|
+
const inOrder = index > previous;
|
|
1057
|
+
previous = index;
|
|
1058
|
+
return inOrder;
|
|
1059
|
+
});
|
|
1060
|
+
return { id: criterion.id, passed, weight: criterion.weight };
|
|
1061
|
+
});
|
|
1062
|
+
const totalWeight = criteria.reduce((total, criterion) => total + criterion.weight, 0);
|
|
1063
|
+
const passingWeight = criteria.reduce((total, criterion) => total + (criterion.passed ? criterion.weight : 0), 0);
|
|
1064
|
+
const failureCodes = [
|
|
1065
|
+
...parsed.failureCodes,
|
|
1066
|
+
...criteria
|
|
1067
|
+
.filter((criterion) => !criterion.passed)
|
|
1068
|
+
.map((criterion) => `criterion-failed:${criterion.id}`),
|
|
1069
|
+
];
|
|
1070
|
+
return {
|
|
1071
|
+
schemaVersion: 1,
|
|
1072
|
+
suiteVersion: BENCHMARK_FIXTURE_SUITE_VERSION,
|
|
1073
|
+
fixtureId,
|
|
1074
|
+
fixtureSha256: fixture.fixtureSha256,
|
|
1075
|
+
rubricSha256: fixture.rubricSha256,
|
|
1076
|
+
outputSha256: hash(output),
|
|
1077
|
+
validResponse: parsed.failureCodes.length === 0,
|
|
1078
|
+
passed: failureCodes.length === 0,
|
|
1079
|
+
score: Math.round((passingWeight / totalWeight) * 10_000) / 100,
|
|
1080
|
+
criteria,
|
|
1081
|
+
failureCodes,
|
|
1082
|
+
safetyBoundary: "Deterministic option-selection grading only. The raw response is hashed but not retained; no model judge can override these criteria.",
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
async function listMaterializedFiles(root, prefix = "") {
|
|
1086
|
+
const directory = resolve(root, prefix);
|
|
1087
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
1088
|
+
const files = [];
|
|
1089
|
+
for (const entry of entries) {
|
|
1090
|
+
const path = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
1091
|
+
if (entry.isSymbolicLink())
|
|
1092
|
+
throw new Error(`Fixture tree contains a symbolic link: ${path}`);
|
|
1093
|
+
if (entry.isDirectory())
|
|
1094
|
+
files.push(...(await listMaterializedFiles(root, path)));
|
|
1095
|
+
else if (entry.isFile())
|
|
1096
|
+
files.push(path);
|
|
1097
|
+
else
|
|
1098
|
+
throw new Error(`Fixture tree contains an unsupported entry: ${path}`);
|
|
1099
|
+
}
|
|
1100
|
+
return files.sort();
|
|
1101
|
+
}
|
|
1102
|
+
export async function materializeBenchmarkFixture(fixtureId, targetDirectory) {
|
|
1103
|
+
const fixture = getBenchmarkFixture(fixtureId);
|
|
1104
|
+
const absoluteTarget = resolve(targetDirectory);
|
|
1105
|
+
try {
|
|
1106
|
+
const existing = await lstat(absoluteTarget);
|
|
1107
|
+
if (!existing.isDirectory())
|
|
1108
|
+
throw new Error("Fixture target exists and is not a directory");
|
|
1109
|
+
if ((await readdir(absoluteTarget)).length)
|
|
1110
|
+
throw new Error("Fixture target directory must be empty");
|
|
1111
|
+
}
|
|
1112
|
+
catch (error) {
|
|
1113
|
+
if (error.code === "ENOENT")
|
|
1114
|
+
await mkdir(absoluteTarget, { recursive: true, mode: 0o700 });
|
|
1115
|
+
else
|
|
1116
|
+
throw error;
|
|
1117
|
+
}
|
|
1118
|
+
const canonicalTarget = await realpath(absoluteTarget);
|
|
1119
|
+
for (const file of fixture.files) {
|
|
1120
|
+
const destination = resolve(canonicalTarget, file.path);
|
|
1121
|
+
const relation = relative(canonicalTarget, destination);
|
|
1122
|
+
if (!relation || relation.startsWith("..") || isAbsolute(relation))
|
|
1123
|
+
throw new Error(`Fixture path escapes target: ${file.path}`);
|
|
1124
|
+
await mkdir(dirname(destination), { recursive: true, mode: 0o700 });
|
|
1125
|
+
await writeFile(destination, file.content, {
|
|
1126
|
+
encoding: "utf8",
|
|
1127
|
+
flag: "wx",
|
|
1128
|
+
mode: 0o600,
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
await verifyMaterializedBenchmarkFixture(fixtureId, canonicalTarget);
|
|
1132
|
+
return {
|
|
1133
|
+
fixtureId,
|
|
1134
|
+
fixtureSha256: fixture.fixtureSha256,
|
|
1135
|
+
files: fixture.files.map(({ path, sha256 }) => ({ path, sha256 })),
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
export async function verifyMaterializedBenchmarkFixture(fixtureId, targetDirectory) {
|
|
1139
|
+
const fixture = getBenchmarkFixture(fixtureId);
|
|
1140
|
+
const root = await realpath(resolve(targetDirectory));
|
|
1141
|
+
const actualPaths = await listMaterializedFiles(root);
|
|
1142
|
+
const expectedPaths = fixture.files.map((file) => file.path).sort();
|
|
1143
|
+
if (actualPaths.join("\0") !== expectedPaths.join("\0"))
|
|
1144
|
+
throw new Error("Materialized fixture file inventory differs from the pinned source");
|
|
1145
|
+
for (const file of fixture.files) {
|
|
1146
|
+
const path = resolve(root, file.path);
|
|
1147
|
+
const relation = relative(root, path);
|
|
1148
|
+
if (!relation || relation.startsWith("..") || isAbsolute(relation))
|
|
1149
|
+
throw new Error(`Fixture path escapes target: ${file.path}`);
|
|
1150
|
+
const stat = await lstat(path);
|
|
1151
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
1152
|
+
throw new Error(`Materialized fixture entry is not a regular file: ${file.path}`);
|
|
1153
|
+
const content = await readFile(path);
|
|
1154
|
+
if (hash(content.toString("utf8")) !== file.sha256)
|
|
1155
|
+
throw new Error(`Materialized fixture content differs from pinned hash: ${file.path}`);
|
|
1156
|
+
}
|
|
1157
|
+
}
|