@loj-lang/benchmark-core 0.5.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/README.md +16 -0
- package/dist/index.d.ts +282 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1312 -0
- package/dist/index.js.map +1 -0
- package/package.json +44 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1312 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { CANONICAL_RDSL_SOURCE_SUFFIX, LEGACY_RDSL_SOURCE_SUFFIX, compile, stripRdslSourceSuffix, } from '@loj-lang/rdsl-compiler';
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
const BENCHMARK_SOURCE_SUFFIXES = [
|
|
7
|
+
CANONICAL_RDSL_SOURCE_SUFFIX,
|
|
8
|
+
LEGACY_RDSL_SOURCE_SUFFIX,
|
|
9
|
+
];
|
|
10
|
+
const BENCHMARK_SCHEMA_VERSION = '0.1.0';
|
|
11
|
+
const DEFAULT_LANE = 'docs-only';
|
|
12
|
+
const repoRoot = fileURLToPath(new URL('../../../', import.meta.url));
|
|
13
|
+
const defaultCorpusDir = resolve(repoRoot, 'benchmarks/authoring');
|
|
14
|
+
const defaultReferenceDocs = resolve(repoRoot, 'subprojects/rdsl/docs/rdsl-reference.md');
|
|
15
|
+
const defaultPromptsOutDir = resolve(defaultCorpusDir, 'prompts/docs-only');
|
|
16
|
+
const defaultReportsDir = resolve(repoRoot, 'benchmarks/reports');
|
|
17
|
+
export function exportPrompts(options = {}) {
|
|
18
|
+
const corpusDir = resolve(options.corpusDir ?? defaultCorpusDir);
|
|
19
|
+
const referenceDocs = resolve(options.referenceDocs ?? defaultReferenceDocs);
|
|
20
|
+
const outDir = resolve(options.outDir ?? defaultPromptsOutDir);
|
|
21
|
+
const reference = readFileSync(referenceDocs, 'utf8').trim();
|
|
22
|
+
const corpus = loadCorpus(corpusDir);
|
|
23
|
+
mkdirSync(outDir, { recursive: true });
|
|
24
|
+
const prompts = corpus.tasks.map((task) => {
|
|
25
|
+
const outputPath = join(outDir, `${task.id}.md`);
|
|
26
|
+
writeFileSync(outputPath, buildPromptDocument(task, reference), 'utf8');
|
|
27
|
+
return {
|
|
28
|
+
taskId: task.id,
|
|
29
|
+
path: outputPath,
|
|
30
|
+
};
|
|
31
|
+
});
|
|
32
|
+
const manifest = {
|
|
33
|
+
artifact: 'rdsl.authoring-prompt-manifest',
|
|
34
|
+
schemaVersion: BENCHMARK_SCHEMA_VERSION,
|
|
35
|
+
generatedAt: new Date().toISOString(),
|
|
36
|
+
lane: DEFAULT_LANE,
|
|
37
|
+
corpusDir,
|
|
38
|
+
referenceDocs,
|
|
39
|
+
outDir,
|
|
40
|
+
prompts,
|
|
41
|
+
};
|
|
42
|
+
writeFileSync(join(outDir, 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf8');
|
|
43
|
+
return manifest;
|
|
44
|
+
}
|
|
45
|
+
export function runAuthoringBenchmark(options) {
|
|
46
|
+
const corpusDir = resolve(options.corpusDir ?? defaultCorpusDir);
|
|
47
|
+
const submissionsDir = resolve(options.submissionsDir);
|
|
48
|
+
const referenceDocs = resolve(options.referenceDocs ?? defaultReferenceDocs);
|
|
49
|
+
const runMeta = loadRunMeta(submissionsDir, referenceDocs);
|
|
50
|
+
const reportDir = resolve(options.reportDir ?? join(defaultReportsDir, formatLocalDate(new Date()), runMeta.runId));
|
|
51
|
+
const corpus = loadCorpus(corpusDir);
|
|
52
|
+
const taskReports = corpus.tasks.map((task) => evaluateTask(task, corpus.expectations.get(task.id), submissionsDir));
|
|
53
|
+
const summary = buildSummary(taskReports);
|
|
54
|
+
const report = {
|
|
55
|
+
artifact: 'rdsl.authoring-report',
|
|
56
|
+
schemaVersion: BENCHMARK_SCHEMA_VERSION,
|
|
57
|
+
generatedAt: new Date().toISOString(),
|
|
58
|
+
run: runMeta,
|
|
59
|
+
corpusDir,
|
|
60
|
+
submissionsDir,
|
|
61
|
+
reportDir,
|
|
62
|
+
referenceDocs,
|
|
63
|
+
tasks: taskReports,
|
|
64
|
+
summary,
|
|
65
|
+
};
|
|
66
|
+
writeBenchmarkReport(report);
|
|
67
|
+
return report;
|
|
68
|
+
}
|
|
69
|
+
export function importAuthoringRun(options) {
|
|
70
|
+
const inputPath = resolve(options.inputPath);
|
|
71
|
+
const outDir = resolve(options.outDir);
|
|
72
|
+
const corpusDir = resolve(options.corpusDir ?? defaultCorpusDir);
|
|
73
|
+
const referenceDocs = resolve(options.referenceDocs ?? defaultReferenceDocs);
|
|
74
|
+
const corpus = loadCorpus(corpusDir);
|
|
75
|
+
const knownTaskIds = new Set(corpus.tasks.map((task) => task.id));
|
|
76
|
+
const importedAttempts = loadImportedAttempts(inputPath, options.adapter)
|
|
77
|
+
.sort((left, right) => {
|
|
78
|
+
if (left.taskId === right.taskId) {
|
|
79
|
+
return left.attempt - right.attempt;
|
|
80
|
+
}
|
|
81
|
+
return left.taskId.localeCompare(right.taskId);
|
|
82
|
+
});
|
|
83
|
+
const seenAttemptKeys = new Set();
|
|
84
|
+
for (const attempt of importedAttempts) {
|
|
85
|
+
if (!knownTaskIds.has(attempt.taskId)) {
|
|
86
|
+
throw new Error(`Imported task "${attempt.taskId}" does not exist in corpus "${corpusDir}"`);
|
|
87
|
+
}
|
|
88
|
+
const attemptKey = `${attempt.taskId}#${attempt.attempt}`;
|
|
89
|
+
if (seenAttemptKeys.has(attemptKey)) {
|
|
90
|
+
throw new Error(`Duplicate imported attempt "${attemptKey}"`);
|
|
91
|
+
}
|
|
92
|
+
seenAttemptKeys.add(attemptKey);
|
|
93
|
+
}
|
|
94
|
+
const run = {
|
|
95
|
+
artifact: 'rdsl.authoring-run',
|
|
96
|
+
schemaVersion: BENCHMARK_SCHEMA_VERSION,
|
|
97
|
+
runId: options.runId ?? basename(outDir),
|
|
98
|
+
lane: options.lane ?? DEFAULT_LANE,
|
|
99
|
+
model: options.model,
|
|
100
|
+
provider: options.provider ?? inferProvider(options.adapter),
|
|
101
|
+
wrapper: options.wrapper ?? options.adapter,
|
|
102
|
+
referenceDocs,
|
|
103
|
+
notes: options.notes,
|
|
104
|
+
};
|
|
105
|
+
mkdirSync(outDir, { recursive: true });
|
|
106
|
+
writeFileSync(join(outDir, 'run.json'), JSON.stringify(run, null, 2), 'utf8');
|
|
107
|
+
const attempts = importedAttempts.map((attempt) => {
|
|
108
|
+
const taskDir = join(outDir, attempt.taskId);
|
|
109
|
+
mkdirSync(taskDir, { recursive: true });
|
|
110
|
+
const outputPath = join(taskDir, `attempt-${attempt.attempt}${CANONICAL_RDSL_SOURCE_SUFFIX}`);
|
|
111
|
+
writeFileSync(outputPath, normalizeImportedOutput(attempt.output), 'utf8');
|
|
112
|
+
let metaPath;
|
|
113
|
+
if (attempt.meta && hasAttemptMeta(attempt.meta)) {
|
|
114
|
+
metaPath = join(taskDir, `attempt-${attempt.attempt}.meta.json`);
|
|
115
|
+
writeFileSync(metaPath, JSON.stringify(attempt.meta, null, 2), 'utf8');
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
taskId: attempt.taskId,
|
|
119
|
+
attempt: attempt.attempt,
|
|
120
|
+
sourcePath: attempt.sourcePath,
|
|
121
|
+
outputPath,
|
|
122
|
+
metaPath,
|
|
123
|
+
};
|
|
124
|
+
});
|
|
125
|
+
const manifest = {
|
|
126
|
+
artifact: 'rdsl.authoring-import-manifest',
|
|
127
|
+
schemaVersion: BENCHMARK_SCHEMA_VERSION,
|
|
128
|
+
generatedAt: new Date().toISOString(),
|
|
129
|
+
adapter: options.adapter,
|
|
130
|
+
inputPath,
|
|
131
|
+
corpusDir,
|
|
132
|
+
outDir,
|
|
133
|
+
run,
|
|
134
|
+
attempts,
|
|
135
|
+
};
|
|
136
|
+
writeFileSync(join(outDir, 'import-manifest.json'), JSON.stringify(manifest, null, 2), 'utf8');
|
|
137
|
+
return manifest;
|
|
138
|
+
}
|
|
139
|
+
export function runCli(argv, io = {}) {
|
|
140
|
+
const cwd = io.cwd ?? process.cwd();
|
|
141
|
+
const stdout = io.stdout ?? ((text) => process.stdout.write(text));
|
|
142
|
+
const stderr = io.stderr ?? ((text) => process.stderr.write(text));
|
|
143
|
+
if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {
|
|
144
|
+
stdout(getUsage());
|
|
145
|
+
return 0;
|
|
146
|
+
}
|
|
147
|
+
const command = argv[0];
|
|
148
|
+
try {
|
|
149
|
+
if (command === 'export-prompts') {
|
|
150
|
+
const parsed = parseExportPromptsArgs(argv.slice(1), cwd);
|
|
151
|
+
const manifest = exportPrompts(parsed);
|
|
152
|
+
if (parsed.json) {
|
|
153
|
+
stdout(`${JSON.stringify(manifest, null, 2)}\n`);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
stdout(`Exported ${manifest.prompts.length} prompts to ${manifest.outDir}\n`);
|
|
157
|
+
}
|
|
158
|
+
return 0;
|
|
159
|
+
}
|
|
160
|
+
if (command === 'run') {
|
|
161
|
+
const parsed = parseRunArgs(argv.slice(1), cwd);
|
|
162
|
+
const report = runAuthoringBenchmark(parsed);
|
|
163
|
+
if (parsed.json) {
|
|
164
|
+
stdout(`${JSON.stringify(report, null, 2)}\n`);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
stdout(`Benchmark run complete: ${report.run.runId}\n`);
|
|
168
|
+
stdout(`report: ${report.reportDir}\n`);
|
|
169
|
+
stdout(`tasks: ${report.summary.taskCount}\n`);
|
|
170
|
+
stdout(`first-pass validity: ${formatRate(report.summary.firstPassValidRate)} (${report.summary.firstPassValidCount}/${report.summary.taskCount})\n`);
|
|
171
|
+
stdout(`solved rate: ${formatRate(report.summary.solvedRate)} (${report.summary.solvedTaskCount}/${report.summary.taskCount})\n`);
|
|
172
|
+
stdout(`average repairs before first valid: ${report.summary.averageRepairsBeforeFirstValid.toFixed(2)}\n`);
|
|
173
|
+
}
|
|
174
|
+
return report.summary.failedTaskCount === 0 ? 0 : 1;
|
|
175
|
+
}
|
|
176
|
+
if (command === 'import') {
|
|
177
|
+
const parsed = parseImportArgs(argv.slice(1), cwd);
|
|
178
|
+
const manifest = importAuthoringRun(parsed);
|
|
179
|
+
if (parsed.json) {
|
|
180
|
+
stdout(`${JSON.stringify(manifest, null, 2)}\n`);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
stdout(`Imported ${manifest.attempts.length} attempts to ${manifest.outDir}\n`);
|
|
184
|
+
stdout(`run: ${manifest.run.runId}\n`);
|
|
185
|
+
stdout(`adapter: ${manifest.adapter}\n`);
|
|
186
|
+
}
|
|
187
|
+
return 0;
|
|
188
|
+
}
|
|
189
|
+
stderr(`Unknown command: ${command}\n`);
|
|
190
|
+
stderr(getUsage());
|
|
191
|
+
return 1;
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
195
|
+
stderr(`${message}\n`);
|
|
196
|
+
return 1;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
function getUsage() {
|
|
200
|
+
return [
|
|
201
|
+
'ReactDSL benchmark harness',
|
|
202
|
+
'',
|
|
203
|
+
'Commands:',
|
|
204
|
+
' rdsl-benchmark export-prompts [--corpus <dir>] [--reference <file>] [--out-dir <dir>] [--json]',
|
|
205
|
+
' rdsl-benchmark import --adapter <jsonl|openai-responses> --input <path> --out-dir <dir> [--corpus <dir>] [--run-id <id>] [--model <model>] [--provider <provider>] [--wrapper <name>] [--json]',
|
|
206
|
+
' rdsl-benchmark run --submissions <dir> [--corpus <dir>] [--reference <file>] [--report-dir <dir>] [--json]',
|
|
207
|
+
'',
|
|
208
|
+
].join('\n');
|
|
209
|
+
}
|
|
210
|
+
function parseExportPromptsArgs(argv, cwd) {
|
|
211
|
+
let corpusDir;
|
|
212
|
+
let referenceDocs;
|
|
213
|
+
let outDir;
|
|
214
|
+
let json = false;
|
|
215
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
216
|
+
const arg = argv[index];
|
|
217
|
+
if (arg === '--corpus') {
|
|
218
|
+
corpusDir = resolve(cwd, requireValue(argv, ++index, '--corpus'));
|
|
219
|
+
}
|
|
220
|
+
else if (arg === '--reference') {
|
|
221
|
+
referenceDocs = resolve(cwd, requireValue(argv, ++index, '--reference'));
|
|
222
|
+
}
|
|
223
|
+
else if (arg === '--out-dir') {
|
|
224
|
+
outDir = resolve(cwd, requireValue(argv, ++index, '--out-dir'));
|
|
225
|
+
}
|
|
226
|
+
else if (arg === '--json') {
|
|
227
|
+
json = true;
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
throw new Error(`Unknown argument for export-prompts: ${arg}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return { corpusDir, referenceDocs, outDir, json };
|
|
234
|
+
}
|
|
235
|
+
function parseRunArgs(argv, cwd) {
|
|
236
|
+
let corpusDir;
|
|
237
|
+
let submissionsDir;
|
|
238
|
+
let referenceDocs;
|
|
239
|
+
let reportDir;
|
|
240
|
+
let json = false;
|
|
241
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
242
|
+
const arg = argv[index];
|
|
243
|
+
if (arg === '--corpus') {
|
|
244
|
+
corpusDir = resolve(cwd, requireValue(argv, ++index, '--corpus'));
|
|
245
|
+
}
|
|
246
|
+
else if (arg === '--submissions') {
|
|
247
|
+
submissionsDir = resolve(cwd, requireValue(argv, ++index, '--submissions'));
|
|
248
|
+
}
|
|
249
|
+
else if (arg === '--reference') {
|
|
250
|
+
referenceDocs = resolve(cwd, requireValue(argv, ++index, '--reference'));
|
|
251
|
+
}
|
|
252
|
+
else if (arg === '--report-dir') {
|
|
253
|
+
reportDir = resolve(cwd, requireValue(argv, ++index, '--report-dir'));
|
|
254
|
+
}
|
|
255
|
+
else if (arg === '--json') {
|
|
256
|
+
json = true;
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
throw new Error(`Unknown argument for run: ${arg}`);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
if (!submissionsDir) {
|
|
263
|
+
throw new Error('Missing required argument: --submissions <dir>');
|
|
264
|
+
}
|
|
265
|
+
return { corpusDir, submissionsDir, referenceDocs, reportDir, json };
|
|
266
|
+
}
|
|
267
|
+
function parseImportArgs(argv, cwd) {
|
|
268
|
+
let adapter;
|
|
269
|
+
let inputPath;
|
|
270
|
+
let outDir;
|
|
271
|
+
let corpusDir;
|
|
272
|
+
let runId;
|
|
273
|
+
let lane;
|
|
274
|
+
let model;
|
|
275
|
+
let provider;
|
|
276
|
+
let wrapper;
|
|
277
|
+
let referenceDocs;
|
|
278
|
+
let notes;
|
|
279
|
+
let json = false;
|
|
280
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
281
|
+
const arg = argv[index];
|
|
282
|
+
if (arg === '--adapter') {
|
|
283
|
+
const value = requireValue(argv, ++index, '--adapter');
|
|
284
|
+
if (value !== 'jsonl' && value !== 'openai-responses') {
|
|
285
|
+
throw new Error(`Unsupported import adapter: ${value}`);
|
|
286
|
+
}
|
|
287
|
+
adapter = value;
|
|
288
|
+
}
|
|
289
|
+
else if (arg === '--input') {
|
|
290
|
+
inputPath = resolve(cwd, requireValue(argv, ++index, '--input'));
|
|
291
|
+
}
|
|
292
|
+
else if (arg === '--out-dir') {
|
|
293
|
+
outDir = resolve(cwd, requireValue(argv, ++index, '--out-dir'));
|
|
294
|
+
}
|
|
295
|
+
else if (arg === '--corpus') {
|
|
296
|
+
corpusDir = resolve(cwd, requireValue(argv, ++index, '--corpus'));
|
|
297
|
+
}
|
|
298
|
+
else if (arg === '--run-id') {
|
|
299
|
+
runId = requireValue(argv, ++index, '--run-id');
|
|
300
|
+
}
|
|
301
|
+
else if (arg === '--lane') {
|
|
302
|
+
const value = requireValue(argv, ++index, '--lane');
|
|
303
|
+
if (value !== DEFAULT_LANE) {
|
|
304
|
+
throw new Error(`Unsupported benchmark lane: ${value}`);
|
|
305
|
+
}
|
|
306
|
+
lane = value;
|
|
307
|
+
}
|
|
308
|
+
else if (arg === '--model') {
|
|
309
|
+
model = requireValue(argv, ++index, '--model');
|
|
310
|
+
}
|
|
311
|
+
else if (arg === '--provider') {
|
|
312
|
+
provider = requireValue(argv, ++index, '--provider');
|
|
313
|
+
}
|
|
314
|
+
else if (arg === '--wrapper') {
|
|
315
|
+
wrapper = requireValue(argv, ++index, '--wrapper');
|
|
316
|
+
}
|
|
317
|
+
else if (arg === '--reference') {
|
|
318
|
+
referenceDocs = resolve(cwd, requireValue(argv, ++index, '--reference'));
|
|
319
|
+
}
|
|
320
|
+
else if (arg === '--notes') {
|
|
321
|
+
notes = requireValue(argv, ++index, '--notes');
|
|
322
|
+
}
|
|
323
|
+
else if (arg === '--json') {
|
|
324
|
+
json = true;
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
throw new Error(`Unknown argument for import: ${arg}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
if (!adapter) {
|
|
331
|
+
throw new Error('Missing required argument: --adapter <jsonl|openai-responses>');
|
|
332
|
+
}
|
|
333
|
+
if (!inputPath) {
|
|
334
|
+
throw new Error('Missing required argument: --input <path>');
|
|
335
|
+
}
|
|
336
|
+
if (!outDir) {
|
|
337
|
+
throw new Error('Missing required argument: --out-dir <dir>');
|
|
338
|
+
}
|
|
339
|
+
return {
|
|
340
|
+
adapter,
|
|
341
|
+
inputPath,
|
|
342
|
+
outDir,
|
|
343
|
+
corpusDir,
|
|
344
|
+
runId,
|
|
345
|
+
lane,
|
|
346
|
+
model,
|
|
347
|
+
provider,
|
|
348
|
+
wrapper,
|
|
349
|
+
referenceDocs,
|
|
350
|
+
notes,
|
|
351
|
+
json,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
function requireValue(argv, index, flag) {
|
|
355
|
+
const value = argv[index];
|
|
356
|
+
if (!value) {
|
|
357
|
+
throw new Error(`Missing value for ${flag}`);
|
|
358
|
+
}
|
|
359
|
+
return value;
|
|
360
|
+
}
|
|
361
|
+
function loadImportedAttempts(inputPath, adapter) {
|
|
362
|
+
if (adapter === 'jsonl') {
|
|
363
|
+
return loadJsonlImportedAttempts(inputPath);
|
|
364
|
+
}
|
|
365
|
+
return loadOpenAiImportedAttempts(inputPath);
|
|
366
|
+
}
|
|
367
|
+
function loadJsonlImportedAttempts(inputPath) {
|
|
368
|
+
const content = readFileSync(inputPath, 'utf8');
|
|
369
|
+
const attempts = [];
|
|
370
|
+
for (const [index, line] of content.split(/\r?\n/).entries()) {
|
|
371
|
+
const trimmed = line.trim();
|
|
372
|
+
if (!trimmed) {
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
let parsed;
|
|
376
|
+
try {
|
|
377
|
+
parsed = JSON.parse(trimmed);
|
|
378
|
+
}
|
|
379
|
+
catch (error) {
|
|
380
|
+
throw new Error(`Invalid JSONL at ${inputPath}:${index + 1}: ${error instanceof Error ? error.message : String(error)}`);
|
|
381
|
+
}
|
|
382
|
+
attempts.push(normalizeImportedAttempt(parsed, `${inputPath}:${index + 1}`));
|
|
383
|
+
}
|
|
384
|
+
return attempts;
|
|
385
|
+
}
|
|
386
|
+
function loadOpenAiImportedAttempts(inputPath) {
|
|
387
|
+
const jsonFiles = collectJsonFiles(inputPath);
|
|
388
|
+
return jsonFiles.map((filePath) => {
|
|
389
|
+
const parsed = loadJson(filePath);
|
|
390
|
+
return normalizeOpenAiImportedAttempt(parsed, filePath);
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
function collectJsonFiles(inputPath) {
|
|
394
|
+
const stats = statSync(inputPath);
|
|
395
|
+
if (stats.isFile()) {
|
|
396
|
+
if (!inputPath.endsWith('.json')) {
|
|
397
|
+
throw new Error(`Expected a .json file for openai-responses adapter: ${inputPath}`);
|
|
398
|
+
}
|
|
399
|
+
return [inputPath];
|
|
400
|
+
}
|
|
401
|
+
if (!stats.isDirectory()) {
|
|
402
|
+
throw new Error(`Unsupported import input path: ${inputPath}`);
|
|
403
|
+
}
|
|
404
|
+
const files = [];
|
|
405
|
+
for (const entry of readdirSync(inputPath)) {
|
|
406
|
+
const childPath = join(inputPath, entry);
|
|
407
|
+
const childStats = statSync(childPath);
|
|
408
|
+
if (childStats.isDirectory()) {
|
|
409
|
+
files.push(...collectJsonFiles(childPath));
|
|
410
|
+
}
|
|
411
|
+
else if (childStats.isFile() && childPath.endsWith('.json')) {
|
|
412
|
+
files.push(childPath);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return files.sort();
|
|
416
|
+
}
|
|
417
|
+
function normalizeImportedAttempt(record, label) {
|
|
418
|
+
const object = asRecord(record, label);
|
|
419
|
+
const taskId = getString(object.taskId) ?? getString(object.task_id);
|
|
420
|
+
if (!taskId) {
|
|
421
|
+
throw new Error(`Missing taskId in imported record: ${label}`);
|
|
422
|
+
}
|
|
423
|
+
const attempt = getPositiveInteger(object.attempt) ?? 1;
|
|
424
|
+
const output = extractOutputText(record);
|
|
425
|
+
if (!output) {
|
|
426
|
+
throw new Error(`Missing output text in imported record: ${label}`);
|
|
427
|
+
}
|
|
428
|
+
return {
|
|
429
|
+
taskId,
|
|
430
|
+
attempt,
|
|
431
|
+
output,
|
|
432
|
+
meta: extractAttemptMeta(record),
|
|
433
|
+
sourcePath: label,
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
function normalizeOpenAiImportedAttempt(record, filePath) {
|
|
437
|
+
const responseRecord = asRecord(record.response ?? record, filePath);
|
|
438
|
+
const fileInfo = parseTaskDescriptorFromPath(filePath);
|
|
439
|
+
const taskId = getString(record.taskId) ?? getString(responseRecord.taskId) ?? fileInfo.taskId;
|
|
440
|
+
if (!taskId) {
|
|
441
|
+
throw new Error(`Missing taskId in imported response file: ${filePath}`);
|
|
442
|
+
}
|
|
443
|
+
const attempt = getPositiveInteger(record.attempt) ?? getPositiveInteger(responseRecord.attempt) ?? fileInfo.attempt ?? 1;
|
|
444
|
+
const output = extractOutputText(record.response ?? record);
|
|
445
|
+
if (!output) {
|
|
446
|
+
throw new Error(`Missing output text in imported response file: ${filePath}`);
|
|
447
|
+
}
|
|
448
|
+
return {
|
|
449
|
+
taskId,
|
|
450
|
+
attempt,
|
|
451
|
+
output,
|
|
452
|
+
meta: extractAttemptMeta(record.response ?? record),
|
|
453
|
+
sourcePath: filePath,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
function parseTaskDescriptorFromPath(filePath) {
|
|
457
|
+
const parentName = basename(dirname(filePath));
|
|
458
|
+
const fileName = basename(filePath, '.json');
|
|
459
|
+
const fileMatch = fileName.match(/^(.*?)(?:[-_.]attempt[-_.]?(\d+))?$/);
|
|
460
|
+
const parentAttempt = parentName.match(/^attempt[-_.]?(\d+)$/);
|
|
461
|
+
if (parentAttempt) {
|
|
462
|
+
return {
|
|
463
|
+
taskId: basename(dirname(dirname(filePath))),
|
|
464
|
+
attempt: Number.parseInt(parentAttempt[1], 10),
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
if (!fileMatch) {
|
|
468
|
+
return {};
|
|
469
|
+
}
|
|
470
|
+
const rawTaskId = fileMatch[1]?.trim();
|
|
471
|
+
const attempt = fileMatch[2] ? Number.parseInt(fileMatch[2], 10) : undefined;
|
|
472
|
+
return {
|
|
473
|
+
taskId: rawTaskId || (parentName !== '.' ? parentName : undefined),
|
|
474
|
+
attempt,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
function extractOutputText(value) {
|
|
478
|
+
if (typeof value === 'string') {
|
|
479
|
+
return value;
|
|
480
|
+
}
|
|
481
|
+
if (!value || typeof value !== 'object') {
|
|
482
|
+
return undefined;
|
|
483
|
+
}
|
|
484
|
+
const record = value;
|
|
485
|
+
const direct = getString(record.output) ?? getString(record.rdsl) ?? getString(record.text) ?? getString(record.content);
|
|
486
|
+
if (direct) {
|
|
487
|
+
return direct;
|
|
488
|
+
}
|
|
489
|
+
const directOutputText = getString(record.output_text);
|
|
490
|
+
if (directOutputText) {
|
|
491
|
+
return directOutputText;
|
|
492
|
+
}
|
|
493
|
+
if (record.response) {
|
|
494
|
+
const nested = extractOutputText(record.response);
|
|
495
|
+
if (nested) {
|
|
496
|
+
return nested;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
if (Array.isArray(record.output)) {
|
|
500
|
+
const joined = flattenResponseText(record.output);
|
|
501
|
+
if (joined) {
|
|
502
|
+
return joined;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
if (Array.isArray(record.choices)) {
|
|
506
|
+
for (const choice of record.choices) {
|
|
507
|
+
const choiceRecord = asOptionalRecord(choice);
|
|
508
|
+
const messageRecord = asOptionalRecord(choiceRecord?.message);
|
|
509
|
+
const messageContent = messageRecord?.content;
|
|
510
|
+
if (typeof messageContent === 'string') {
|
|
511
|
+
return messageContent;
|
|
512
|
+
}
|
|
513
|
+
if (Array.isArray(messageContent)) {
|
|
514
|
+
const joined = flattenResponseText(messageContent);
|
|
515
|
+
if (joined) {
|
|
516
|
+
return joined;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return undefined;
|
|
522
|
+
}
|
|
523
|
+
function flattenResponseText(items) {
|
|
524
|
+
const chunks = [];
|
|
525
|
+
for (const item of items) {
|
|
526
|
+
if (typeof item === 'string') {
|
|
527
|
+
chunks.push(item);
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
const record = asOptionalRecord(item);
|
|
531
|
+
if (!record) {
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
const direct = getString(record.text) ?? getString(record.output_text);
|
|
535
|
+
if (direct) {
|
|
536
|
+
chunks.push(direct);
|
|
537
|
+
}
|
|
538
|
+
if (Array.isArray(record.content)) {
|
|
539
|
+
const nested = flattenResponseText(record.content);
|
|
540
|
+
if (nested) {
|
|
541
|
+
chunks.push(nested);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
const text = chunks.join('\n').trim();
|
|
546
|
+
return text || undefined;
|
|
547
|
+
}
|
|
548
|
+
function extractAttemptMeta(value) {
|
|
549
|
+
const record = asOptionalRecord(value);
|
|
550
|
+
if (!record) {
|
|
551
|
+
return undefined;
|
|
552
|
+
}
|
|
553
|
+
const usage = asOptionalRecord(record.usage);
|
|
554
|
+
const promptTokens = getFiniteNumber(record.promptTokens) ??
|
|
555
|
+
getFiniteNumber(record.prompt_tokens) ??
|
|
556
|
+
getFiniteNumber(usage?.promptTokens) ??
|
|
557
|
+
getFiniteNumber(usage?.prompt_tokens) ??
|
|
558
|
+
getFiniteNumber(usage?.inputTokens) ??
|
|
559
|
+
getFiniteNumber(usage?.input_tokens);
|
|
560
|
+
const completionTokens = getFiniteNumber(record.completionTokens) ??
|
|
561
|
+
getFiniteNumber(record.completion_tokens) ??
|
|
562
|
+
getFiniteNumber(usage?.completionTokens) ??
|
|
563
|
+
getFiniteNumber(usage?.completion_tokens) ??
|
|
564
|
+
getFiniteNumber(usage?.outputTokens) ??
|
|
565
|
+
getFiniteNumber(usage?.output_tokens);
|
|
566
|
+
const totalTokens = getFiniteNumber(record.totalTokens) ??
|
|
567
|
+
getFiniteNumber(record.total_tokens) ??
|
|
568
|
+
getFiniteNumber(usage?.totalTokens) ??
|
|
569
|
+
getFiniteNumber(usage?.total_tokens);
|
|
570
|
+
const latencyMs = getFiniteNumber(record.latencyMs) ??
|
|
571
|
+
getFiniteNumber(record.latency_ms) ??
|
|
572
|
+
getFiniteNumber(record.durationMs) ??
|
|
573
|
+
getFiniteNumber(record.duration_ms);
|
|
574
|
+
const meta = {
|
|
575
|
+
promptTokens,
|
|
576
|
+
completionTokens,
|
|
577
|
+
totalTokens: totalTokens ?? deriveTotalTokens(promptTokens, completionTokens),
|
|
578
|
+
latencyMs,
|
|
579
|
+
};
|
|
580
|
+
return hasAttemptMeta(meta) ? meta : undefined;
|
|
581
|
+
}
|
|
582
|
+
function deriveTotalTokens(promptTokens, completionTokens) {
|
|
583
|
+
if (typeof promptTokens === 'number' && typeof completionTokens === 'number') {
|
|
584
|
+
return promptTokens + completionTokens;
|
|
585
|
+
}
|
|
586
|
+
return undefined;
|
|
587
|
+
}
|
|
588
|
+
function hasAttemptMeta(meta) {
|
|
589
|
+
return typeof meta.promptTokens === 'number' ||
|
|
590
|
+
typeof meta.completionTokens === 'number' ||
|
|
591
|
+
typeof meta.totalTokens === 'number' ||
|
|
592
|
+
typeof meta.latencyMs === 'number';
|
|
593
|
+
}
|
|
594
|
+
function normalizeImportedOutput(output) {
|
|
595
|
+
const fenced = output.match(/```(?:yaml|yml|rdsl)?\s*\n([\s\S]*?)```/i);
|
|
596
|
+
const normalized = fenced ? fenced[1] : output;
|
|
597
|
+
const trimmed = normalized.trim();
|
|
598
|
+
return trimmed ? `${trimmed}\n` : '';
|
|
599
|
+
}
|
|
600
|
+
function inferProvider(adapter) {
|
|
601
|
+
return adapter === 'openai-responses' ? 'openai' : 'unknown';
|
|
602
|
+
}
|
|
603
|
+
function asRecord(value, label) {
|
|
604
|
+
if (!value || typeof value !== 'object') {
|
|
605
|
+
throw new Error(`Expected an object for ${label}`);
|
|
606
|
+
}
|
|
607
|
+
return value;
|
|
608
|
+
}
|
|
609
|
+
function asOptionalRecord(value) {
|
|
610
|
+
if (!value || typeof value !== 'object') {
|
|
611
|
+
return undefined;
|
|
612
|
+
}
|
|
613
|
+
return value;
|
|
614
|
+
}
|
|
615
|
+
function getString(value) {
|
|
616
|
+
return typeof value === 'string' ? value : undefined;
|
|
617
|
+
}
|
|
618
|
+
function getFiniteNumber(value) {
|
|
619
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
620
|
+
}
|
|
621
|
+
function getPositiveInteger(value) {
|
|
622
|
+
const numberValue = getFiniteNumber(value);
|
|
623
|
+
if (typeof numberValue !== 'number') {
|
|
624
|
+
return undefined;
|
|
625
|
+
}
|
|
626
|
+
return Number.isInteger(numberValue) && numberValue > 0 ? numberValue : undefined;
|
|
627
|
+
}
|
|
628
|
+
function loadCorpus(corpusDir) {
|
|
629
|
+
const tasksDir = join(corpusDir, 'tasks');
|
|
630
|
+
const expectedDir = join(corpusDir, 'expected');
|
|
631
|
+
const taskFiles = readdirSync(tasksDir)
|
|
632
|
+
.filter((entry) => entry.endsWith('.json'))
|
|
633
|
+
.sort();
|
|
634
|
+
const tasks = taskFiles.map((entry) => loadJson(join(tasksDir, entry)));
|
|
635
|
+
const expectations = new Map();
|
|
636
|
+
for (const task of tasks) {
|
|
637
|
+
assertArtifact(task.artifact, 'rdsl.authoring-task', `task ${task.id}`);
|
|
638
|
+
const expectedFile = join(expectedDir, `${task.id}.json`);
|
|
639
|
+
if (!existsSync(expectedFile)) {
|
|
640
|
+
throw new Error(`Missing expectation file for task "${task.id}": ${expectedFile}`);
|
|
641
|
+
}
|
|
642
|
+
const expectation = loadJson(expectedFile);
|
|
643
|
+
assertArtifact(expectation.artifact, 'rdsl.authoring-expected', `expectation ${task.id}`);
|
|
644
|
+
if (expectation.taskId !== task.id) {
|
|
645
|
+
throw new Error(`Expectation taskId mismatch for "${task.id}"`);
|
|
646
|
+
}
|
|
647
|
+
expectations.set(task.id, expectation);
|
|
648
|
+
}
|
|
649
|
+
return { tasks, expectations };
|
|
650
|
+
}
|
|
651
|
+
function assertArtifact(actual, expected, label) {
|
|
652
|
+
if (actual !== expected) {
|
|
653
|
+
throw new Error(`Invalid artifact for ${label}: expected "${expected}", received "${actual}"`);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
function loadRunMeta(submissionsDir, referenceDocs) {
|
|
657
|
+
const runFile = join(submissionsDir, 'run.json');
|
|
658
|
+
if (!existsSync(runFile)) {
|
|
659
|
+
return {
|
|
660
|
+
artifact: 'rdsl.authoring-run',
|
|
661
|
+
schemaVersion: BENCHMARK_SCHEMA_VERSION,
|
|
662
|
+
runId: basename(submissionsDir),
|
|
663
|
+
lane: DEFAULT_LANE,
|
|
664
|
+
referenceDocs,
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
const runMeta = loadJson(runFile);
|
|
668
|
+
assertArtifact(runMeta.artifact, 'rdsl.authoring-run', `run metadata ${runFile}`);
|
|
669
|
+
return {
|
|
670
|
+
...runMeta,
|
|
671
|
+
runId: runMeta.runId ?? basename(submissionsDir),
|
|
672
|
+
lane: runMeta.lane ?? DEFAULT_LANE,
|
|
673
|
+
referenceDocs: runMeta.referenceDocs ?? referenceDocs,
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
function evaluateTask(task, expectation, submissionsDir) {
|
|
677
|
+
const taskDir = join(submissionsDir, task.id);
|
|
678
|
+
if (!existsSync(taskDir)) {
|
|
679
|
+
return {
|
|
680
|
+
taskId: task.id,
|
|
681
|
+
title: task.title,
|
|
682
|
+
expectedCheckCount: expectation.checks.length + budgetCheckCount(expectation.budgets),
|
|
683
|
+
firstPassValid: false,
|
|
684
|
+
winningAttempt: null,
|
|
685
|
+
repairsBeforeFirstValid: null,
|
|
686
|
+
status: 'missing',
|
|
687
|
+
attempts: [],
|
|
688
|
+
failureArtifacts: [],
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
const attemptFiles = readdirSync(taskDir)
|
|
692
|
+
.filter((entry) => BENCHMARK_SOURCE_SUFFIXES.some((suffix) => new RegExp(`^attempt-\\d+${escapeRegExp(suffix)}$`).test(entry)))
|
|
693
|
+
.sort((left, right) => extractAttemptNumber(left) - extractAttemptNumber(right));
|
|
694
|
+
const attempts = attemptFiles.map((entry) => evaluateAttempt({
|
|
695
|
+
sourceFile: join(taskDir, entry),
|
|
696
|
+
attempt: extractAttemptNumber(entry),
|
|
697
|
+
expectation,
|
|
698
|
+
}));
|
|
699
|
+
const winningAttempt = attempts.find((attempt) => attempt.valid);
|
|
700
|
+
const firstPassValid = attempts[0]?.valid ?? false;
|
|
701
|
+
return {
|
|
702
|
+
taskId: task.id,
|
|
703
|
+
title: task.title,
|
|
704
|
+
expectedCheckCount: expectation.checks.length + budgetCheckCount(expectation.budgets),
|
|
705
|
+
firstPassValid,
|
|
706
|
+
winningAttempt: winningAttempt?.attempt ?? null,
|
|
707
|
+
repairsBeforeFirstValid: winningAttempt ? winningAttempt.attempt - 1 : null,
|
|
708
|
+
status: !attempts.length
|
|
709
|
+
? 'missing'
|
|
710
|
+
: firstPassValid
|
|
711
|
+
? 'first-pass-valid'
|
|
712
|
+
: winningAttempt
|
|
713
|
+
? 'repaired'
|
|
714
|
+
: 'failed',
|
|
715
|
+
attempts,
|
|
716
|
+
failureArtifacts: [],
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
function evaluateAttempt(input) {
|
|
720
|
+
const source = readFileSync(input.sourceFile, 'utf8');
|
|
721
|
+
const compileResult = compile(source, input.sourceFile);
|
|
722
|
+
const ir = compileResult.ir;
|
|
723
|
+
const checks = ir
|
|
724
|
+
? [
|
|
725
|
+
...input.expectation.checks.map((check) => evaluateExpectationCheck(check, ir)),
|
|
726
|
+
...evaluateBudgetChecks(input.expectation.budgets, compileResult.warnings.length, ir.escapeStats),
|
|
727
|
+
]
|
|
728
|
+
: [
|
|
729
|
+
...input.expectation.checks.map((check) => ({
|
|
730
|
+
id: check.id,
|
|
731
|
+
type: check.type,
|
|
732
|
+
status: 'skipped',
|
|
733
|
+
message: 'Skipped because compilation failed.',
|
|
734
|
+
})),
|
|
735
|
+
...evaluateBudgetChecks(input.expectation.budgets, compileResult.warnings.length, undefined),
|
|
736
|
+
];
|
|
737
|
+
const meta = loadAttemptMeta(input.sourceFile);
|
|
738
|
+
const valid = compileResult.success &&
|
|
739
|
+
checks.every((check) => check.status === 'passed');
|
|
740
|
+
return {
|
|
741
|
+
attempt: input.attempt,
|
|
742
|
+
sourceFile: input.sourceFile,
|
|
743
|
+
meta,
|
|
744
|
+
compileSuccess: compileResult.success,
|
|
745
|
+
warningCount: compileResult.warnings.length,
|
|
746
|
+
errors: compileResult.errors,
|
|
747
|
+
warnings: compileResult.warnings,
|
|
748
|
+
checks,
|
|
749
|
+
valid,
|
|
750
|
+
escapeStats: compileResult.ir?.escapeStats,
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
function loadAttemptMeta(sourceFile) {
|
|
754
|
+
const metaFile = `${stripRdslSourceSuffix(sourceFile)}.meta.json`;
|
|
755
|
+
if (!existsSync(metaFile)) {
|
|
756
|
+
return undefined;
|
|
757
|
+
}
|
|
758
|
+
return loadJson(metaFile);
|
|
759
|
+
}
|
|
760
|
+
function evaluateBudgetChecks(budgets, warningCount, escapeStats) {
|
|
761
|
+
if (!budgets) {
|
|
762
|
+
return [];
|
|
763
|
+
}
|
|
764
|
+
const results = [];
|
|
765
|
+
if (typeof budgets.maxWarnings === 'number') {
|
|
766
|
+
results.push({
|
|
767
|
+
id: 'budget.maxWarnings',
|
|
768
|
+
type: 'budget',
|
|
769
|
+
status: warningCount <= budgets.maxWarnings ? 'passed' : 'failed',
|
|
770
|
+
message: `Warnings ${warningCount}/${budgets.maxWarnings}.`,
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
if (!escapeStats) {
|
|
774
|
+
if (typeof budgets.maxEscapePercent === 'number' ||
|
|
775
|
+
typeof budgets.maxExprCount === 'number' ||
|
|
776
|
+
typeof budgets.maxFnCount === 'number' ||
|
|
777
|
+
typeof budgets.maxCustomCount === 'number') {
|
|
778
|
+
results.push({
|
|
779
|
+
id: 'budget.escapeStats',
|
|
780
|
+
type: 'budget',
|
|
781
|
+
status: 'skipped',
|
|
782
|
+
message: 'Skipped because compilation failed.',
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
return results;
|
|
786
|
+
}
|
|
787
|
+
if (typeof budgets.maxEscapePercent === 'number') {
|
|
788
|
+
results.push({
|
|
789
|
+
id: 'budget.maxEscapePercent',
|
|
790
|
+
type: 'budget',
|
|
791
|
+
status: escapeStats.escapePercent <= budgets.maxEscapePercent ? 'passed' : 'failed',
|
|
792
|
+
message: `Escape percent ${escapeStats.escapePercent}%/${budgets.maxEscapePercent}%.`,
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
if (typeof budgets.maxExprCount === 'number') {
|
|
796
|
+
results.push({
|
|
797
|
+
id: 'budget.maxExprCount',
|
|
798
|
+
type: 'budget',
|
|
799
|
+
status: escapeStats.exprCount <= budgets.maxExprCount ? 'passed' : 'failed',
|
|
800
|
+
message: `@expr count ${escapeStats.exprCount}/${budgets.maxExprCount}.`,
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
if (typeof budgets.maxFnCount === 'number') {
|
|
804
|
+
results.push({
|
|
805
|
+
id: 'budget.maxFnCount',
|
|
806
|
+
type: 'budget',
|
|
807
|
+
status: escapeStats.fnCount <= budgets.maxFnCount ? 'passed' : 'failed',
|
|
808
|
+
message: `@fn count ${escapeStats.fnCount}/${budgets.maxFnCount}.`,
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
if (typeof budgets.maxCustomCount === 'number') {
|
|
812
|
+
results.push({
|
|
813
|
+
id: 'budget.maxCustomCount',
|
|
814
|
+
type: 'budget',
|
|
815
|
+
status: escapeStats.customCount <= budgets.maxCustomCount ? 'passed' : 'failed',
|
|
816
|
+
message: `@custom count ${escapeStats.customCount}/${budgets.maxCustomCount}.`,
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
return results;
|
|
820
|
+
}
|
|
821
|
+
function budgetCheckCount(budgets) {
|
|
822
|
+
if (!budgets) {
|
|
823
|
+
return 0;
|
|
824
|
+
}
|
|
825
|
+
let count = 0;
|
|
826
|
+
if (typeof budgets.maxWarnings === 'number')
|
|
827
|
+
count += 1;
|
|
828
|
+
if (typeof budgets.maxEscapePercent === 'number' ||
|
|
829
|
+
typeof budgets.maxExprCount === 'number' ||
|
|
830
|
+
typeof budgets.maxFnCount === 'number' ||
|
|
831
|
+
typeof budgets.maxCustomCount === 'number') {
|
|
832
|
+
count += Number(typeof budgets.maxEscapePercent === 'number');
|
|
833
|
+
count += Number(typeof budgets.maxExprCount === 'number');
|
|
834
|
+
count += Number(typeof budgets.maxFnCount === 'number');
|
|
835
|
+
count += Number(typeof budgets.maxCustomCount === 'number');
|
|
836
|
+
}
|
|
837
|
+
return count;
|
|
838
|
+
}
|
|
839
|
+
function evaluateExpectationCheck(check, ir) {
|
|
840
|
+
switch (check.type) {
|
|
841
|
+
case 'appNameEquals':
|
|
842
|
+
return compareValue(check.id, check.type, ir.name, check.value, `App name is "${ir.name}".`);
|
|
843
|
+
case 'compilerTargetEquals':
|
|
844
|
+
return compareValue(check.id, check.type, ir.compiler.target, check.value, `Compiler target is "${ir.compiler.target}".`);
|
|
845
|
+
case 'modelExists': {
|
|
846
|
+
const model = ir.models.find((entry) => entry.name === check.model);
|
|
847
|
+
return booleanResult(check.id, check.type, Boolean(model), model ? `Model "${check.model}" exists.` : `Model "${check.model}" is missing.`);
|
|
848
|
+
}
|
|
849
|
+
case 'modelFieldExists': {
|
|
850
|
+
const model = ir.models.find((entry) => entry.name === check.model);
|
|
851
|
+
if (!model) {
|
|
852
|
+
return missingDependency(check, `Model "${check.model}" is missing.`);
|
|
853
|
+
}
|
|
854
|
+
const field = model.fields.find((entry) => entry.name === check.field);
|
|
855
|
+
if (!field) {
|
|
856
|
+
return booleanResult(check.id, check.type, false, `Field "${check.field}" is missing from model "${check.model}".`);
|
|
857
|
+
}
|
|
858
|
+
if (!matchesFieldType(field, check.fieldType, check.enumValues)) {
|
|
859
|
+
return booleanResult(check.id, check.type, false, `Field "${check.field}" has the wrong type.`);
|
|
860
|
+
}
|
|
861
|
+
if (!hasDecorators(field.decorators, check.decorators)) {
|
|
862
|
+
return booleanResult(check.id, check.type, false, `Field "${check.field}" is missing required decorators.`);
|
|
863
|
+
}
|
|
864
|
+
return booleanResult(check.id, check.type, true, `Field "${check.field}" on model "${check.model}" matches.`);
|
|
865
|
+
}
|
|
866
|
+
case 'resourceExists': {
|
|
867
|
+
const resource = findResource(ir, check.resource);
|
|
868
|
+
if (!resource) {
|
|
869
|
+
return booleanResult(check.id, check.type, false, `Resource "${check.resource}" is missing.`);
|
|
870
|
+
}
|
|
871
|
+
if (check.model && resource.model !== check.model) {
|
|
872
|
+
return booleanResult(check.id, check.type, false, `Resource "${check.resource}" points to model "${resource.model}".`);
|
|
873
|
+
}
|
|
874
|
+
if (check.api && resource.api !== check.api) {
|
|
875
|
+
return booleanResult(check.id, check.type, false, `Resource "${check.resource}" points to API "${resource.api}".`);
|
|
876
|
+
}
|
|
877
|
+
if (check.views && !check.views.every((view) => hasView(resource, view))) {
|
|
878
|
+
return booleanResult(check.id, check.type, false, `Resource "${check.resource}" is missing one or more required views.`);
|
|
879
|
+
}
|
|
880
|
+
return booleanResult(check.id, check.type, true, `Resource "${check.resource}" matches.`);
|
|
881
|
+
}
|
|
882
|
+
case 'listFilterExists': {
|
|
883
|
+
const list = getListView(ir, check.resource);
|
|
884
|
+
if (!list) {
|
|
885
|
+
return missingDependency(check, `List view for resource "${check.resource}" is missing.`);
|
|
886
|
+
}
|
|
887
|
+
return booleanResult(check.id, check.type, list.filters.some((filter) => filter.field === check.field), `List filter "${check.field}" ${list.filters.some((filter) => filter.field === check.field) ? 'exists' : 'is missing'} on "${check.resource}".`);
|
|
888
|
+
}
|
|
889
|
+
case 'listColumnExists': {
|
|
890
|
+
const list = getListView(ir, check.resource);
|
|
891
|
+
if (!list) {
|
|
892
|
+
return missingDependency(check, `List view for resource "${check.resource}" is missing.`);
|
|
893
|
+
}
|
|
894
|
+
const column = list.columns.find((entry) => entry.field === check.field);
|
|
895
|
+
if (!column) {
|
|
896
|
+
return booleanResult(check.id, check.type, false, `Column "${check.field}" is missing from "${check.resource}".`);
|
|
897
|
+
}
|
|
898
|
+
if (!hasDecorators(column.decorators, check.decorators)) {
|
|
899
|
+
return booleanResult(check.id, check.type, false, `Column "${check.field}" is missing required decorators.`);
|
|
900
|
+
}
|
|
901
|
+
return booleanResult(check.id, check.type, true, `Column "${check.field}" exists on "${check.resource}".`);
|
|
902
|
+
}
|
|
903
|
+
case 'actionExists': {
|
|
904
|
+
const list = getListView(ir, check.resource);
|
|
905
|
+
if (!list) {
|
|
906
|
+
return missingDependency(check, `List view for resource "${check.resource}" is missing.`);
|
|
907
|
+
}
|
|
908
|
+
const action = list.actions.find((entry) => actionMatches(entry, check.action, check.confirm));
|
|
909
|
+
return booleanResult(check.id, check.type, Boolean(action), action ? `Action "${check.action}" exists on "${check.resource}".` : `Action "${check.action}" is missing from "${check.resource}".`);
|
|
910
|
+
}
|
|
911
|
+
case 'paginationEquals': {
|
|
912
|
+
const list = getListView(ir, check.resource);
|
|
913
|
+
if (!list?.pagination) {
|
|
914
|
+
return missingDependency(check, `Pagination for resource "${check.resource}" is missing.`);
|
|
915
|
+
}
|
|
916
|
+
const passed = list.pagination.size === check.size && list.pagination.style === check.style;
|
|
917
|
+
return booleanResult(check.id, check.type, passed, `Pagination is ${list.pagination.size}/${list.pagination.style}.`);
|
|
918
|
+
}
|
|
919
|
+
case 'viewFieldExists': {
|
|
920
|
+
const view = getFormView(ir, check.resource, check.view);
|
|
921
|
+
if (!view) {
|
|
922
|
+
return missingDependency(check, `${check.view} view for resource "${check.resource}" is missing.`);
|
|
923
|
+
}
|
|
924
|
+
const field = view.fields.find((entry) => entry.field === check.field);
|
|
925
|
+
if (!field) {
|
|
926
|
+
return booleanResult(check.id, check.type, false, `Field "${check.field}" is missing from ${check.view} view "${check.resource}".`);
|
|
927
|
+
}
|
|
928
|
+
if (!hasDecorators(field.decorators, check.decorators)) {
|
|
929
|
+
return booleanResult(check.id, check.type, false, `Field "${check.field}" is missing required decorators.`);
|
|
930
|
+
}
|
|
931
|
+
return booleanResult(check.id, check.type, true, `Field "${check.field}" exists in ${check.view} view "${check.resource}".`);
|
|
932
|
+
}
|
|
933
|
+
case 'viewRuleEquals': {
|
|
934
|
+
const view = getAnyView(ir, check.resource, check.view);
|
|
935
|
+
if (!view?.rules) {
|
|
936
|
+
return missingDependency(check, `${check.view} view rules for resource "${check.resource}" are missing.`);
|
|
937
|
+
}
|
|
938
|
+
const rule = view.rules[check.rule];
|
|
939
|
+
if (!rule) {
|
|
940
|
+
return booleanResult(check.id, check.type, false, `Rule "${check.rule}" is missing from ${check.view} view "${check.resource}".`);
|
|
941
|
+
}
|
|
942
|
+
if (check.source && rule.source !== check.source) {
|
|
943
|
+
return booleanResult(check.id, check.type, false, `Rule "${check.rule}" uses source "${rule.source}".`);
|
|
944
|
+
}
|
|
945
|
+
if (check.canonicalExpr) {
|
|
946
|
+
const actual = serializeRule(rule);
|
|
947
|
+
if (actual !== check.canonicalExpr) {
|
|
948
|
+
return booleanResult(check.id, check.type, false, `Rule "${check.rule}" serializes to "${actual}".`);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
if (check.fnPath && (rule.source !== 'escape-fn' || rule.escape.path !== check.fnPath)) {
|
|
952
|
+
return booleanResult(check.id, check.type, false, `Rule "${check.rule}" does not use fn path "${check.fnPath}".`);
|
|
953
|
+
}
|
|
954
|
+
return booleanResult(check.id, check.type, true, `Rule "${check.rule}" matches on "${check.resource}".`);
|
|
955
|
+
}
|
|
956
|
+
case 'effectExists': {
|
|
957
|
+
const view = getFormView(ir, check.resource, check.view);
|
|
958
|
+
if (!view) {
|
|
959
|
+
return missingDependency(check, `${check.view} view for resource "${check.resource}" is missing.`);
|
|
960
|
+
}
|
|
961
|
+
const matched = view.onSuccess.some((effect) => matchesEffect(effect, check));
|
|
962
|
+
return booleanResult(check.id, check.type, matched, matched ? `Effect "${check.effectType}" exists on "${check.resource}.${check.view}".` : `Effect "${check.effectType}" is missing from "${check.resource}.${check.view}".`);
|
|
963
|
+
}
|
|
964
|
+
case 'pageExists': {
|
|
965
|
+
const page = ir.pages.find((entry) => entry.name === check.page);
|
|
966
|
+
if (!page) {
|
|
967
|
+
return booleanResult(check.id, check.type, false, `Page "${check.page}" is missing.`);
|
|
968
|
+
}
|
|
969
|
+
if (check.pageType && page.pageType !== check.pageType) {
|
|
970
|
+
return booleanResult(check.id, check.type, false, `Page "${check.page}" has type "${page.pageType}".`);
|
|
971
|
+
}
|
|
972
|
+
if (check.title && page.title !== check.title) {
|
|
973
|
+
return booleanResult(check.id, check.type, false, `Page "${check.page}" has title "${page.title}".`);
|
|
974
|
+
}
|
|
975
|
+
return booleanResult(check.id, check.type, true, `Page "${check.page}" matches.`);
|
|
976
|
+
}
|
|
977
|
+
case 'pageBlockExists': {
|
|
978
|
+
const page = ir.pages.find((entry) => entry.name === check.page);
|
|
979
|
+
if (!page) {
|
|
980
|
+
return missingDependency(check, `Page "${check.page}" is missing.`);
|
|
981
|
+
}
|
|
982
|
+
const block = page.blocks.find((entry) => entry.blockType === check.blockType &&
|
|
983
|
+
entry.title === check.title &&
|
|
984
|
+
(check.data ? entry.data === check.data : true));
|
|
985
|
+
return booleanResult(check.id, check.type, Boolean(block), block ? `Block "${check.title}" exists on page "${check.page}".` : `Block "${check.title}" is missing from page "${check.page}".`);
|
|
986
|
+
}
|
|
987
|
+
case 'navItemExists': {
|
|
988
|
+
const group = check.group
|
|
989
|
+
? ir.navigation.find((entry) => entry.group === check.group)
|
|
990
|
+
: undefined;
|
|
991
|
+
const candidates = group ? group.items : ir.navigation.flatMap((entry) => entry.items);
|
|
992
|
+
const item = candidates.find((entry) => entry.target === check.target &&
|
|
993
|
+
(check.label ? entry.label === check.label : true));
|
|
994
|
+
return booleanResult(check.id, check.type, Boolean(item), item ? `Navigation target "${check.target}" exists.` : `Navigation target "${check.target}" is missing.`);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
function buildSummary(tasks) {
|
|
999
|
+
const taskCount = tasks.length;
|
|
1000
|
+
const attemptedTaskCount = tasks.filter((task) => task.attempts.length > 0).length;
|
|
1001
|
+
const solvedTasks = tasks.filter((task) => task.winningAttempt !== null);
|
|
1002
|
+
const failedTaskCount = tasks.filter((task) => task.winningAttempt === null).length;
|
|
1003
|
+
const firstPassValidCount = tasks.filter((task) => task.firstPassValid).length;
|
|
1004
|
+
const totalAttemptCount = tasks.reduce((sum, task) => sum + task.attempts.length, 0);
|
|
1005
|
+
const repairValues = solvedTasks
|
|
1006
|
+
.map((task) => task.repairsBeforeFirstValid ?? 0);
|
|
1007
|
+
const averageRepairsBeforeFirstValid = repairValues.length
|
|
1008
|
+
? repairValues.reduce((sum, value) => sum + value, 0) / repairValues.length
|
|
1009
|
+
: 0;
|
|
1010
|
+
const tokenUsage = tasks.reduce((summary, task) => {
|
|
1011
|
+
for (const attempt of task.attempts) {
|
|
1012
|
+
summary.promptTokens += attempt.meta?.promptTokens ?? 0;
|
|
1013
|
+
summary.completionTokens += attempt.meta?.completionTokens ?? 0;
|
|
1014
|
+
summary.totalTokens += attempt.meta?.totalTokens ?? 0;
|
|
1015
|
+
}
|
|
1016
|
+
return summary;
|
|
1017
|
+
}, { promptTokens: 0, completionTokens: 0, totalTokens: 0 });
|
|
1018
|
+
const winningAttempts = solvedTasks
|
|
1019
|
+
.map((task) => task.attempts.find((attempt) => attempt.attempt === task.winningAttempt))
|
|
1020
|
+
.filter((attempt) => Boolean(attempt && attempt.escapeStats));
|
|
1021
|
+
const winningAttemptEscapeUsage = winningAttempts.reduce((summary, attempt) => {
|
|
1022
|
+
summary.exprCount += attempt.escapeStats?.exprCount ?? 0;
|
|
1023
|
+
summary.fnCount += attempt.escapeStats?.fnCount ?? 0;
|
|
1024
|
+
summary.customCount += attempt.escapeStats?.customCount ?? 0;
|
|
1025
|
+
summary.averagePercent += attempt.escapeStats?.escapePercent ?? 0;
|
|
1026
|
+
return summary;
|
|
1027
|
+
}, { exprCount: 0, fnCount: 0, customCount: 0, averagePercent: 0 });
|
|
1028
|
+
if (winningAttempts.length) {
|
|
1029
|
+
winningAttemptEscapeUsage.averagePercent /= winningAttempts.length;
|
|
1030
|
+
}
|
|
1031
|
+
return {
|
|
1032
|
+
taskCount,
|
|
1033
|
+
attemptedTaskCount,
|
|
1034
|
+
solvedTaskCount: solvedTasks.length,
|
|
1035
|
+
failedTaskCount,
|
|
1036
|
+
firstPassValidCount,
|
|
1037
|
+
firstPassValidRate: taskCount ? firstPassValidCount / taskCount : 0,
|
|
1038
|
+
solvedRate: taskCount ? solvedTasks.length / taskCount : 0,
|
|
1039
|
+
averageRepairsBeforeFirstValid,
|
|
1040
|
+
totalAttemptCount,
|
|
1041
|
+
tokenUsage,
|
|
1042
|
+
winningAttemptEscapeUsage,
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
function writeBenchmarkReport(report) {
|
|
1046
|
+
mkdirSync(report.reportDir, { recursive: true });
|
|
1047
|
+
writeFileSync(join(report.reportDir, 'report.json'), JSON.stringify(report, null, 2), 'utf8');
|
|
1048
|
+
writeFileSync(join(report.reportDir, 'summary.md'), buildSummaryMarkdown(report), 'utf8');
|
|
1049
|
+
for (const task of report.tasks) {
|
|
1050
|
+
const failuresDir = join(report.reportDir, 'failures', task.taskId);
|
|
1051
|
+
mkdirSync(failuresDir, { recursive: true });
|
|
1052
|
+
if (!task.attempts.length) {
|
|
1053
|
+
const missingPath = join(failuresDir, 'missing.json');
|
|
1054
|
+
writeFileSync(missingPath, JSON.stringify({
|
|
1055
|
+
taskId: task.taskId,
|
|
1056
|
+
reason: 'No attempts found for task.',
|
|
1057
|
+
}, null, 2), 'utf8');
|
|
1058
|
+
task.failureArtifacts.push(relative(report.reportDir, missingPath));
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1061
|
+
for (const attempt of task.attempts) {
|
|
1062
|
+
if (attempt.valid) {
|
|
1063
|
+
continue;
|
|
1064
|
+
}
|
|
1065
|
+
const copiedSource = join(failuresDir, `attempt-${attempt.attempt}${sourceFileSuffix(attempt.sourceFile)}`);
|
|
1066
|
+
writeFileSync(copiedSource, readFileSync(attempt.sourceFile, 'utf8'), 'utf8');
|
|
1067
|
+
const diagnostics = join(failuresDir, `attempt-${attempt.attempt}.diagnostics.json`);
|
|
1068
|
+
writeFileSync(diagnostics, JSON.stringify({
|
|
1069
|
+
taskId: task.taskId,
|
|
1070
|
+
attempt: attempt.attempt,
|
|
1071
|
+
sourceFile: attempt.sourceFile,
|
|
1072
|
+
compileSuccess: attempt.compileSuccess,
|
|
1073
|
+
warningCount: attempt.warningCount,
|
|
1074
|
+
errors: attempt.errors,
|
|
1075
|
+
warnings: attempt.warnings,
|
|
1076
|
+
failedChecks: attempt.checks.filter((check) => check.status !== 'passed'),
|
|
1077
|
+
}, null, 2), 'utf8');
|
|
1078
|
+
task.failureArtifacts.push(relative(report.reportDir, copiedSource));
|
|
1079
|
+
task.failureArtifacts.push(relative(report.reportDir, diagnostics));
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
writeFileSync(join(report.reportDir, 'report.json'), JSON.stringify(report, null, 2), 'utf8');
|
|
1083
|
+
}
|
|
1084
|
+
function buildSummaryMarkdown(report) {
|
|
1085
|
+
const lines = [
|
|
1086
|
+
'# ReactDSL Authoring Benchmark Report',
|
|
1087
|
+
'',
|
|
1088
|
+
`- Run: ${report.run.runId}`,
|
|
1089
|
+
`- Model: ${report.run.model ?? 'unknown'}`,
|
|
1090
|
+
`- Lane: ${report.run.lane}`,
|
|
1091
|
+
`- Provider: ${report.run.provider ?? 'unknown'}`,
|
|
1092
|
+
`- Generated: ${report.generatedAt}`,
|
|
1093
|
+
`- Corpus: ${report.corpusDir}`,
|
|
1094
|
+
`- Submissions: ${report.submissionsDir}`,
|
|
1095
|
+
'',
|
|
1096
|
+
'## Summary',
|
|
1097
|
+
'',
|
|
1098
|
+
`- Tasks: ${report.summary.taskCount}`,
|
|
1099
|
+
`- Attempted tasks: ${report.summary.attemptedTaskCount}`,
|
|
1100
|
+
`- First-pass validity rate: ${formatRate(report.summary.firstPassValidRate)} (${report.summary.firstPassValidCount}/${report.summary.taskCount})`,
|
|
1101
|
+
`- Solved rate: ${formatRate(report.summary.solvedRate)} (${report.summary.solvedTaskCount}/${report.summary.taskCount})`,
|
|
1102
|
+
`- Average repairs before first valid: ${report.summary.averageRepairsBeforeFirstValid.toFixed(2)}`,
|
|
1103
|
+
`- Total attempts: ${report.summary.totalAttemptCount}`,
|
|
1104
|
+
`- Token usage: prompt ${report.summary.tokenUsage.promptTokens}, completion ${report.summary.tokenUsage.completionTokens}, total ${report.summary.tokenUsage.totalTokens}`,
|
|
1105
|
+
`- Winning-attempt escape usage: @expr ${report.summary.winningAttemptEscapeUsage.exprCount}, @fn ${report.summary.winningAttemptEscapeUsage.fnCount}, @custom ${report.summary.winningAttemptEscapeUsage.customCount}, avg ${report.summary.winningAttemptEscapeUsage.averagePercent.toFixed(2)}%`,
|
|
1106
|
+
'',
|
|
1107
|
+
'## Tasks',
|
|
1108
|
+
'',
|
|
1109
|
+
'| Task | Status | First Pass | Winning Attempt | Repairs | Checks | Warnings |',
|
|
1110
|
+
'|------|--------|------------|-----------------|---------|--------|----------|',
|
|
1111
|
+
];
|
|
1112
|
+
for (const task of report.tasks) {
|
|
1113
|
+
const winningAttempt = task.winningAttempt ?? '-';
|
|
1114
|
+
const repairs = task.repairsBeforeFirstValid ?? '-';
|
|
1115
|
+
const attempt = task.winningAttempt
|
|
1116
|
+
? task.attempts.find((entry) => entry.attempt === task.winningAttempt)
|
|
1117
|
+
: task.attempts[0];
|
|
1118
|
+
const passedChecks = attempt ? attempt.checks.filter((check) => check.status === 'passed').length : 0;
|
|
1119
|
+
const totalChecks = attempt ? attempt.checks.length : task.expectedCheckCount;
|
|
1120
|
+
const warnings = attempt ? attempt.warningCount : '-';
|
|
1121
|
+
lines.push(`| ${task.taskId} | ${task.status} | ${task.firstPassValid ? 'yes' : 'no'} | ${winningAttempt} | ${repairs} | ${passedChecks}/${totalChecks} | ${warnings} |`);
|
|
1122
|
+
}
|
|
1123
|
+
return `${lines.join('\n')}\n`;
|
|
1124
|
+
}
|
|
1125
|
+
function buildPromptDocument(task, reference) {
|
|
1126
|
+
return [
|
|
1127
|
+
`# ReactDSL Benchmark Prompt: ${task.title}`,
|
|
1128
|
+
'',
|
|
1129
|
+
`Task ID: ${task.id}`,
|
|
1130
|
+
`Lane: ${task.lane}`,
|
|
1131
|
+
'',
|
|
1132
|
+
`Return exactly one valid \`${CANONICAL_RDSL_SOURCE_SUFFIX}\` document and nothing else.`,
|
|
1133
|
+
'Do not explain your answer. Do not wrap the output in Markdown fences.',
|
|
1134
|
+
'',
|
|
1135
|
+
'## Reference',
|
|
1136
|
+
'',
|
|
1137
|
+
reference,
|
|
1138
|
+
'',
|
|
1139
|
+
'## Task',
|
|
1140
|
+
'',
|
|
1141
|
+
task.prompt.trim(),
|
|
1142
|
+
'',
|
|
1143
|
+
].join('\n');
|
|
1144
|
+
}
|
|
1145
|
+
function compareValue(id, type, actual, expected, message) {
|
|
1146
|
+
return {
|
|
1147
|
+
id,
|
|
1148
|
+
type,
|
|
1149
|
+
status: actual === expected ? 'passed' : 'failed',
|
|
1150
|
+
message: actual === expected ? message : `Expected "${expected}", received "${actual}".`,
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
function booleanResult(id, type, passed, message) {
|
|
1154
|
+
return {
|
|
1155
|
+
id,
|
|
1156
|
+
type,
|
|
1157
|
+
status: passed ? 'passed' : 'failed',
|
|
1158
|
+
message,
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
function missingDependency(check, message) {
|
|
1162
|
+
return {
|
|
1163
|
+
id: check.id,
|
|
1164
|
+
type: check.type,
|
|
1165
|
+
status: 'failed',
|
|
1166
|
+
message,
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
function matchesFieldType(field, expectedType, enumValues) {
|
|
1170
|
+
if (expectedType === 'enum') {
|
|
1171
|
+
return field.fieldType.type === 'enum' &&
|
|
1172
|
+
(enumValues ? JSON.stringify(field.fieldType.values) === JSON.stringify(enumValues) : true);
|
|
1173
|
+
}
|
|
1174
|
+
return field.fieldType.type === 'scalar' && field.fieldType.name === expectedType;
|
|
1175
|
+
}
|
|
1176
|
+
function hasDecorators(decorators, expected) {
|
|
1177
|
+
if (!expected?.length) {
|
|
1178
|
+
return true;
|
|
1179
|
+
}
|
|
1180
|
+
const actualNames = new Set(decorators.map((decorator) => decorator.name));
|
|
1181
|
+
return expected.every((name) => actualNames.has(name));
|
|
1182
|
+
}
|
|
1183
|
+
function findResource(ir, name) {
|
|
1184
|
+
return ir.resources.find((entry) => entry.name === name);
|
|
1185
|
+
}
|
|
1186
|
+
function getListView(ir, resourceName) {
|
|
1187
|
+
return findResource(ir, resourceName)?.views.list;
|
|
1188
|
+
}
|
|
1189
|
+
function getFormView(ir, resourceName, viewName) {
|
|
1190
|
+
const resource = findResource(ir, resourceName);
|
|
1191
|
+
if (!resource) {
|
|
1192
|
+
return undefined;
|
|
1193
|
+
}
|
|
1194
|
+
return viewName === 'edit' ? resource.views.edit : resource.views.create;
|
|
1195
|
+
}
|
|
1196
|
+
function getAnyView(ir, resourceName, viewName) {
|
|
1197
|
+
const resource = findResource(ir, resourceName);
|
|
1198
|
+
if (!resource) {
|
|
1199
|
+
return undefined;
|
|
1200
|
+
}
|
|
1201
|
+
return resource.views[viewName];
|
|
1202
|
+
}
|
|
1203
|
+
function hasView(resource, viewName) {
|
|
1204
|
+
return Boolean(resource.views[viewName]);
|
|
1205
|
+
}
|
|
1206
|
+
function actionMatches(action, expectedName, expectedConfirm) {
|
|
1207
|
+
return action.name === expectedName && (expectedConfirm ? action.confirm === expectedConfirm : true);
|
|
1208
|
+
}
|
|
1209
|
+
function matchesEffect(effect, check) {
|
|
1210
|
+
if (effect.type !== check.effectType) {
|
|
1211
|
+
return false;
|
|
1212
|
+
}
|
|
1213
|
+
switch (effect.type) {
|
|
1214
|
+
case 'refresh':
|
|
1215
|
+
case 'invalidate':
|
|
1216
|
+
case 'redirect':
|
|
1217
|
+
return effect.target === check.target;
|
|
1218
|
+
case 'toast':
|
|
1219
|
+
return check.message === undefined ? true : matchesToastMessage(effect.message, check.message);
|
|
1220
|
+
case 'openDialog':
|
|
1221
|
+
return effect.dialog === check.dialog;
|
|
1222
|
+
case 'emitEvent':
|
|
1223
|
+
return effect.event === check.event;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
function matchesToastMessage(actual, expected) {
|
|
1227
|
+
if (typeof expected === 'string') {
|
|
1228
|
+
return actual === expected;
|
|
1229
|
+
}
|
|
1230
|
+
if (typeof actual === 'string') {
|
|
1231
|
+
return false;
|
|
1232
|
+
}
|
|
1233
|
+
return JSON.stringify(normalizeToastDescriptorForCompare(actual)) ===
|
|
1234
|
+
JSON.stringify(normalizeToastDescriptorForCompare(expected));
|
|
1235
|
+
}
|
|
1236
|
+
function normalizeToastDescriptorForCompare(message) {
|
|
1237
|
+
const values = Object.entries(message.values ?? {})
|
|
1238
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
1239
|
+
.map(([name, value]) => [name, normalizeToastValueForCompare(value)]);
|
|
1240
|
+
return {
|
|
1241
|
+
key: message.key,
|
|
1242
|
+
defaultMessage: message.defaultMessage ?? null,
|
|
1243
|
+
values,
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
function normalizeToastValueForCompare(value) {
|
|
1247
|
+
if (typeof value === 'object' && value !== null && 'ref' in value) {
|
|
1248
|
+
return { ref: value.ref };
|
|
1249
|
+
}
|
|
1250
|
+
return value;
|
|
1251
|
+
}
|
|
1252
|
+
function serializeRule(rule) {
|
|
1253
|
+
if (rule.source === 'escape-expr') {
|
|
1254
|
+
return rule.escape.raw;
|
|
1255
|
+
}
|
|
1256
|
+
if (rule.source === 'escape-fn') {
|
|
1257
|
+
return rule.escape.path;
|
|
1258
|
+
}
|
|
1259
|
+
return serializeExpr(rule.expr);
|
|
1260
|
+
}
|
|
1261
|
+
function serializeExpr(expr) {
|
|
1262
|
+
switch (expr.type) {
|
|
1263
|
+
case 'literal':
|
|
1264
|
+
return typeof expr.value === 'string' ? JSON.stringify(expr.value) : String(expr.value);
|
|
1265
|
+
case 'identifier':
|
|
1266
|
+
return expr.path.join('.');
|
|
1267
|
+
case 'binary':
|
|
1268
|
+
return `${serializeExpr(expr.left)} ${expr.op} ${serializeExpr(expr.right)}`;
|
|
1269
|
+
case 'unary':
|
|
1270
|
+
return `${expr.op} ${serializeExpr(expr.operand)}`;
|
|
1271
|
+
case 'call':
|
|
1272
|
+
return `${expr.fn}(${expr.args.map((arg) => serializeExpr(arg)).join(', ')})`;
|
|
1273
|
+
case 'member':
|
|
1274
|
+
return `${serializeExpr(expr.object)}.${expr.property}`;
|
|
1275
|
+
case 'in':
|
|
1276
|
+
return `${serializeExpr(expr.value)} in [${expr.list.map((entry) => serializeExpr(entry)).join(', ')}]`;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
function extractAttemptNumber(entry) {
|
|
1280
|
+
const match = entry.match(/^attempt-(\d+)(\.[A-Za-z0-9.-]+)$/);
|
|
1281
|
+
if (!match) {
|
|
1282
|
+
throw new Error(`Invalid attempt file name: ${entry}`);
|
|
1283
|
+
}
|
|
1284
|
+
return Number.parseInt(match[1], 10);
|
|
1285
|
+
}
|
|
1286
|
+
function sourceFileSuffix(fileName) {
|
|
1287
|
+
for (const suffix of BENCHMARK_SOURCE_SUFFIXES) {
|
|
1288
|
+
if (fileName.endsWith(suffix)) {
|
|
1289
|
+
return suffix;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
return CANONICAL_RDSL_SOURCE_SUFFIX;
|
|
1293
|
+
}
|
|
1294
|
+
function escapeRegExp(value) {
|
|
1295
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1296
|
+
}
|
|
1297
|
+
function loadJson(filePath) {
|
|
1298
|
+
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
1299
|
+
}
|
|
1300
|
+
function formatLocalDate(date) {
|
|
1301
|
+
const year = date.getFullYear();
|
|
1302
|
+
const month = `${date.getMonth() + 1}`.padStart(2, '0');
|
|
1303
|
+
const day = `${date.getDate()}`.padStart(2, '0');
|
|
1304
|
+
return `${year}-${month}-${day}`;
|
|
1305
|
+
}
|
|
1306
|
+
function formatRate(rate) {
|
|
1307
|
+
return `${(rate * 100).toFixed(2)}%`;
|
|
1308
|
+
}
|
|
1309
|
+
if (process.argv[1] && import.meta.url === new URL(process.argv[1], 'file:').href) {
|
|
1310
|
+
process.exitCode = runCli(process.argv.slice(2));
|
|
1311
|
+
}
|
|
1312
|
+
//# sourceMappingURL=index.js.map
|