aub-workspace 0.3.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 +201 -0
- package/README.md +23 -0
- package/bin/aub-workspace.mjs +246 -0
- package/package.json +32 -0
- package/vendor/aub/apps/editor/dist/assets/_commonjs-dynamic-modules-TDtrdbi3.js +1 -0
- package/vendor/aub/apps/editor/dist/assets/angular-importer.lib-dB_jK4mR.js +32 -0
- package/vendor/aub/apps/editor/dist/assets/canvas-tools-CuYC7cA2.js +364 -0
- package/vendor/aub/apps/editor/dist/assets/design-bridge.lib-DJvaK6AX.js +1 -0
- package/vendor/aub/apps/editor/dist/assets/export-agent-prompt.lib-BsP0KNqo.js +2 -0
- package/vendor/aub/apps/editor/dist/assets/export-md.lib-DdmdeWgO.js +3 -0
- package/vendor/aub/apps/editor/dist/assets/handoff-package.lib-DDYpcEma.js +20 -0
- package/vendor/aub/apps/editor/dist/assets/implementation-report.lib-CmsSB_8s.js +1 -0
- package/vendor/aub/apps/editor/dist/assets/index-BCH-ek3h.js +2 -0
- package/vendor/aub/apps/editor/dist/assets/index-lAnc928Q.css +1 -0
- package/vendor/aub/apps/editor/dist/assets/index-vt1nM1M4.js +507 -0
- package/vendor/aub/apps/editor/dist/assets/jszip.min-CRfXyL92.js +12 -0
- package/vendor/aub/apps/editor/dist/assets/react-vendor-ByX9Pqse.js +40 -0
- package/vendor/aub/apps/editor/dist/brand/android-chrome-192x192.png +0 -0
- package/vendor/aub/apps/editor/dist/brand/android-chrome-512x512.png +0 -0
- package/vendor/aub/apps/editor/dist/brand/app-icon-1024.png +0 -0
- package/vendor/aub/apps/editor/dist/brand/app-icon-192.png +0 -0
- package/vendor/aub/apps/editor/dist/brand/app-icon-512.png +0 -0
- package/vendor/aub/apps/editor/dist/brand/apple-touch-icon.png +0 -0
- package/vendor/aub/apps/editor/dist/brand/aub-logo-mark.svg +28 -0
- package/vendor/aub/apps/editor/dist/brand/favicon-16x16.png +0 -0
- package/vendor/aub/apps/editor/dist/brand/favicon-32x32.png +0 -0
- package/vendor/aub/apps/editor/dist/brand/favicon-48x48.png +0 -0
- package/vendor/aub/apps/editor/dist/brand/favicon.ico +0 -0
- package/vendor/aub/apps/editor/dist/brand/favicon.svg +9 -0
- package/vendor/aub/apps/editor/dist/brand/maskable-icon-512.png +0 -0
- package/vendor/aub/apps/editor/dist/brand/mstile-150x150.png +0 -0
- package/vendor/aub/apps/editor/dist/brand/safari-pinned-tab.svg +8 -0
- package/vendor/aub/apps/editor/dist/browserconfig.xml +9 -0
- package/vendor/aub/apps/editor/dist/index.html +22 -0
- package/vendor/aub/apps/editor/dist/manifest.webmanifest +28 -0
- package/vendor/aub/apps/editor/dist/template-previews/admin-table.png +0 -0
- package/vendor/aub/apps/editor/dist/template-previews/booking.png +0 -0
- package/vendor/aub/apps/editor/dist/template-previews/calendar.png +0 -0
- package/vendor/aub/apps/editor/dist/template-previews/catalog.png +0 -0
- package/vendor/aub/apps/editor/dist/template-previews/chat.png +0 -0
- package/vendor/aub/apps/editor/dist/template-previews/checkout.png +0 -0
- package/vendor/aub/apps/editor/dist/template-previews/crm.png +0 -0
- package/vendor/aub/apps/editor/dist/template-previews/dashboard.png +0 -0
- package/vendor/aub/apps/editor/dist/template-previews/feed.png +0 -0
- package/vendor/aub/apps/editor/dist/template-previews/files.png +0 -0
- package/vendor/aub/apps/editor/dist/template-previews/kanban.png +0 -0
- package/vendor/aub/apps/editor/dist/template-previews/landing.png +0 -0
- package/vendor/aub/apps/editor/dist/template-previews/mail.png +0 -0
- package/vendor/aub/apps/editor/dist/template-previews/onboarding.png +0 -0
- package/vendor/aub/apps/editor/dist/template-previews/pricing.png +0 -0
- package/vendor/aub/apps/editor/dist/template-previews/product-detail.png +0 -0
- package/vendor/aub/apps/editor/dist/template-previews/settings.png +0 -0
- package/vendor/aub/apps/editor/dist/template-previews/wiki.png +0 -0
- package/vendor/aub/apps/mcp-server/dist/aub.js +15 -0
- package/vendor/aub/apps/mcp-server/dist/context.js +1 -0
- package/vendor/aub/apps/mcp-server/dist/http.js +123 -0
- package/vendor/aub/apps/mcp-server/dist/index.js +23 -0
- package/vendor/aub/apps/mcp-server/dist/repo.js +17 -0
- package/vendor/aub/apps/mcp-server/dist/schema.js +42 -0
- package/vendor/aub/apps/mcp-server/dist/server.js +80 -0
- package/vendor/aub/apps/mcp-server/dist/tools/approve-component-candidate.js +27 -0
- package/vendor/aub/apps/mcp-server/dist/tools/diff-blueprints.js +27 -0
- package/vendor/aub/apps/mcp-server/dist/tools/export-handoff.js +87 -0
- package/vendor/aub/apps/mcp-server/dist/tools/export-prompt.js +35 -0
- package/vendor/aub/apps/mcp-server/dist/tools/export-template-authoring-prompt.js +13 -0
- package/vendor/aub/apps/mcp-server/dist/tools/generate-template-from-source.js +25 -0
- package/vendor/aub/apps/mcp-server/dist/tools/get-aub-session.js +13 -0
- package/vendor/aub/apps/mcp-server/dist/tools/get-blueprint.js +28 -0
- package/vendor/aub/apps/mcp-server/dist/tools/get-project.js +45 -0
- package/vendor/aub/apps/mcp-server/dist/tools/get-workspace-status.js +10 -0
- package/vendor/aub/apps/mcp-server/dist/tools/import-design-bridge.js +62 -0
- package/vendor/aub/apps/mcp-server/dist/tools/list-blueprints.js +11 -0
- package/vendor/aub/apps/mcp-server/dist/tools/list-projects.js +11 -0
- package/vendor/aub/apps/mcp-server/dist/tools/lock-blueprint.js +33 -0
- package/vendor/aub/apps/mcp-server/dist/tools/migrate-blueprint.js +38 -0
- package/vendor/aub/apps/mcp-server/dist/tools/resolve-component.js +51 -0
- package/vendor/aub/apps/mcp-server/dist/tools/scaffold-blueprint.js +53 -0
- package/vendor/aub/apps/mcp-server/dist/tools/scan-project-ui.js +18 -0
- package/vendor/aub/apps/mcp-server/dist/tools/submit-report.js +48 -0
- package/vendor/aub/apps/mcp-server/dist/tools/update-aub-session.js +14 -0
- package/vendor/aub/apps/mcp-server/dist/tools/validate-blueprint.js +67 -0
- package/vendor/aub/apps/mcp-server/dist/tools/validate-project.js +74 -0
- package/vendor/aub/apps/mcp-server/dist/tools/write-blueprint.js +72 -0
- package/vendor/aub/apps/mcp-server/dist/workspace.js +138 -0
- package/vendor/aub/docs/agent-handoff.md +85 -0
- package/vendor/aub/docs/agent-handoff.zh-Hant.md +85 -0
- package/vendor/aub/docs/template-authoring-agent.md +86 -0
- package/vendor/aub/schema/aub-ci.schema.json +34 -0
- package/vendor/aub/schema/aub.registry.schema.json +118 -0
- package/vendor/aub/schema/design-bridge.schema.json +44 -0
- package/vendor/aub/schema/implementation-report.schema.json +93 -0
- package/vendor/aub/schema/project-types.ts +72 -0
- package/vendor/aub/schema/registry/components.json +118 -0
- package/vendor/aub/schema/types.js +13 -0
- package/vendor/aub/schema/types.ts +348 -0
- package/vendor/aub/schema/ui-blueprint-lock.schema.json +61 -0
- package/vendor/aub/schema/ui-blueprint.schema.json +1339 -0
- package/vendor/aub/schema/ui-project.schema.json +139 -0
- package/vendor/aub/scripts/agent-implementation-benchmark.lib.mjs +125 -0
- package/vendor/aub/scripts/angular-importer.lib.mjs +982 -0
- package/vendor/aub/scripts/check-editor-bundle-budget.mjs +36 -0
- package/vendor/aub/scripts/ci-verify.lib.mjs +256 -0
- package/vendor/aub/scripts/ci-verify.mjs +45 -0
- package/vendor/aub/scripts/create-authoring-kit.mjs +84 -0
- package/vendor/aub/scripts/create-implementation-report.mjs +24 -0
- package/vendor/aub/scripts/design-bridge.lib.d.mts +32 -0
- package/vendor/aub/scripts/design-bridge.lib.mjs +69 -0
- package/vendor/aub/scripts/diff-blueprint.lib.d.mts +18 -0
- package/vendor/aub/scripts/diff-blueprint.lib.mjs +148 -0
- package/vendor/aub/scripts/diff-blueprint.mjs +25 -0
- package/vendor/aub/scripts/export-agent-prompt.lib.d.mts +10 -0
- package/vendor/aub/scripts/export-agent-prompt.lib.mjs +160 -0
- package/vendor/aub/scripts/export-agent-prompt.mjs +79 -0
- package/vendor/aub/scripts/export-md.lib.d.mts +3 -0
- package/vendor/aub/scripts/export-md.lib.mjs +302 -0
- package/vendor/aub/scripts/export-md.mjs +43 -0
- package/vendor/aub/scripts/generate-registry-artifacts.lib.mjs +118 -0
- package/vendor/aub/scripts/generate-registry-artifacts.mjs +65 -0
- package/vendor/aub/scripts/generate-site-locales.mjs +545 -0
- package/vendor/aub/scripts/handoff-package.lib.d.mts +20 -0
- package/vendor/aub/scripts/handoff-package.lib.mjs +111 -0
- package/vendor/aub/scripts/implementation-report.lib.d.mts +21 -0
- package/vendor/aub/scripts/implementation-report.lib.mjs +97 -0
- package/vendor/aub/scripts/import-angular-component.mjs +72 -0
- package/vendor/aub/scripts/import-design-bridge.mjs +59 -0
- package/vendor/aub/scripts/lock-blueprint.lib.d.mts +23 -0
- package/vendor/aub/scripts/lock-blueprint.lib.mjs +58 -0
- package/vendor/aub/scripts/lock-blueprint.mjs +36 -0
- package/vendor/aub/scripts/migrate-blueprint-cli.mjs +28 -0
- package/vendor/aub/scripts/migrate-blueprint.d.mts +5 -0
- package/vendor/aub/scripts/migrate-blueprint.mjs +95 -0
- package/vendor/aub/scripts/package-workspace-cli.mjs +34 -0
- package/vendor/aub/scripts/project.lib.d.mts +44 -0
- package/vendor/aub/scripts/project.lib.mjs +175 -0
- package/vendor/aub/scripts/project.mjs +332 -0
- package/vendor/aub/scripts/registry.lib.d.mts +52 -0
- package/vendor/aub/scripts/registry.lib.mjs +222 -0
- package/vendor/aub/scripts/run-agent-implementation.mjs +423 -0
- package/vendor/aub/scripts/run-agent-readability.mjs +145 -0
- package/vendor/aub/scripts/run-ollama-prompt.mjs +30 -0
- package/vendor/aub/scripts/scaffold-blueprint.lib.d.mts +38 -0
- package/vendor/aub/scripts/scaffold-blueprint.lib.mjs +316 -0
- package/vendor/aub/scripts/scaffold-blueprint.mjs +86 -0
- package/vendor/aub/scripts/score-agent-implementation.mjs +27 -0
- package/vendor/aub/scripts/score-agent-readability.mjs +54 -0
- package/vendor/aub/scripts/sync-brand-assets.mjs +33 -0
- package/vendor/aub/scripts/validate-blueprint.lib.d.mts +14 -0
- package/vendor/aub/scripts/validate-blueprint.lib.mjs +136 -0
- package/vendor/aub/scripts/validate.mjs +128 -0
- package/vendor/aub/scripts/verify-implementation-report.mjs +36 -0
- package/vendor/aub/scripts/workspace-loop.lib.d.mts +17 -0
- package/vendor/aub/scripts/workspace-loop.lib.mjs +674 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { dirname, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { scoreAgentOutput } from './score-agent-readability.mjs';
|
|
8
|
+
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
const separator = args.indexOf('--');
|
|
11
|
+
const options = separator >= 0 ? args.slice(0, separator) : args;
|
|
12
|
+
const command = separator >= 0 ? args.slice(separator + 1) : [];
|
|
13
|
+
const name = options.find((value) => !value.startsWith('--'));
|
|
14
|
+
const allowsExternal = options.includes('--allow-external');
|
|
15
|
+
|
|
16
|
+
if (!name || command.length === 0) {
|
|
17
|
+
console.error('Usage: node scripts/run-agent-readability.mjs <agent-name> --allow-external -- <command> [args...]');
|
|
18
|
+
process.exit(2);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!allowsExternal) {
|
|
22
|
+
console.error('Refusing to run an external agent without the explicit --allow-external flag.');
|
|
23
|
+
process.exit(2);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
27
|
+
const benchmarkDir = resolve(root, 'benchmarks/agent-readability');
|
|
28
|
+
const resultDir = resolve(benchmarkDir, 'results');
|
|
29
|
+
const prompt = await readFile(resolve(benchmarkDir, 'prompt.md'), 'utf8');
|
|
30
|
+
const fixture = await readFile(resolve(root, 'examples/freeform-actions.ui.json'), 'utf8');
|
|
31
|
+
const expected = JSON.parse(await readFile(resolve(benchmarkDir, 'expected.json'), 'utf8'));
|
|
32
|
+
await mkdir(resultDir, { recursive: true });
|
|
33
|
+
|
|
34
|
+
const benchmarkInput = [
|
|
35
|
+
prompt.trim(),
|
|
36
|
+
'',
|
|
37
|
+
'<blueprint_json>',
|
|
38
|
+
fixture.trim(),
|
|
39
|
+
'</blueprint_json>',
|
|
40
|
+
'',
|
|
41
|
+
].join('\n');
|
|
42
|
+
const result = await run(command[0], command.slice(1), benchmarkInput, root);
|
|
43
|
+
const slug = name.replace(/[^a-zA-Z0-9._-]+/g, '-');
|
|
44
|
+
await writeFile(resolve(resultDir, `${slug}.stdout.txt`), result.stdout, 'utf8');
|
|
45
|
+
await writeFile(resolve(resultDir, `${slug}.stderr.txt`), result.stderr, 'utf8');
|
|
46
|
+
|
|
47
|
+
if (result.code !== 0) {
|
|
48
|
+
console.error(`${name} exited with code ${result.code}. See benchmarks/agent-readability/results/${slug}.stderr.txt`);
|
|
49
|
+
process.exit(result.code || 1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const actual = extractJsonObject(result.stdout);
|
|
53
|
+
const report = {
|
|
54
|
+
agent: name,
|
|
55
|
+
command: command.map(redactArgument),
|
|
56
|
+
executed_at: new Date().toISOString(),
|
|
57
|
+
...scoreAgentOutput(actual, expected),
|
|
58
|
+
};
|
|
59
|
+
await writeFile(resolve(resultDir, `${slug}.output.json`), `${JSON.stringify(actual, null, 2)}\n`, 'utf8');
|
|
60
|
+
await writeFile(resolve(resultDir, `${slug}.report.json`), `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
|
61
|
+
console.log(JSON.stringify(report, null, 2));
|
|
62
|
+
process.exit(report.score === 100 ? 0 : 1);
|
|
63
|
+
|
|
64
|
+
function run(executable, commandArgs, stdin, cwd) {
|
|
65
|
+
return new Promise((resolveRun, reject) => {
|
|
66
|
+
const child = spawn(executable, commandArgs, {
|
|
67
|
+
cwd,
|
|
68
|
+
env: process.env,
|
|
69
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
70
|
+
});
|
|
71
|
+
let stdout = '';
|
|
72
|
+
let stderr = '';
|
|
73
|
+
child.stdout.setEncoding('utf8');
|
|
74
|
+
child.stderr.setEncoding('utf8');
|
|
75
|
+
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
76
|
+
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
77
|
+
child.on('error', reject);
|
|
78
|
+
child.on('close', (code) => resolveRun({ code: code ?? 1, stdout, stderr }));
|
|
79
|
+
child.stdin.end(stdin);
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function extractJsonObject(text) {
|
|
84
|
+
const trimmed = text.trim();
|
|
85
|
+
const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1]?.trim();
|
|
86
|
+
for (const candidate of [trimmed, fenced].filter(Boolean)) {
|
|
87
|
+
try {
|
|
88
|
+
const value = JSON.parse(candidate);
|
|
89
|
+
if (isBenchmarkOutput(value)) return value;
|
|
90
|
+
} catch {
|
|
91
|
+
// Continue to balanced-object extraction.
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (let start = trimmed.indexOf('{'); start >= 0; start = trimmed.indexOf('{', start + 1)) {
|
|
96
|
+
const candidate = balancedObjectAt(trimmed, start);
|
|
97
|
+
if (!candidate) continue;
|
|
98
|
+
try {
|
|
99
|
+
const value = JSON.parse(candidate);
|
|
100
|
+
if (isBenchmarkOutput(value)) return value;
|
|
101
|
+
} catch {
|
|
102
|
+
// Try the next opening brace.
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
throw new Error('Agent output did not contain one parseable JSON object.');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isBenchmarkOutput(value) {
|
|
109
|
+
return Boolean(
|
|
110
|
+
value &&
|
|
111
|
+
typeof value === 'object' &&
|
|
112
|
+
!Array.isArray(value) &&
|
|
113
|
+
typeof value.version === 'string' &&
|
|
114
|
+
typeof value.root_id === 'string' &&
|
|
115
|
+
Array.isArray(value.direct_root_children)
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function balancedObjectAt(text, start) {
|
|
120
|
+
let depth = 0;
|
|
121
|
+
let inString = false;
|
|
122
|
+
let escaped = false;
|
|
123
|
+
for (let index = start; index < text.length; index += 1) {
|
|
124
|
+
const character = text[index];
|
|
125
|
+
if (inString) {
|
|
126
|
+
if (escaped) escaped = false;
|
|
127
|
+
else if (character === '\\') escaped = true;
|
|
128
|
+
else if (character === '"') inString = false;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (character === '"') inString = true;
|
|
132
|
+
else if (character === '{') depth += 1;
|
|
133
|
+
else if (character === '}') {
|
|
134
|
+
depth -= 1;
|
|
135
|
+
if (depth === 0) return text.slice(start, index + 1);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function redactArgument(argument) {
|
|
142
|
+
return /(?:key|token|secret|password)=/i.test(argument)
|
|
143
|
+
? argument.replace(/=.*/, '=REDACTED')
|
|
144
|
+
: argument;
|
|
145
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const [model, baseUrl = 'http://127.0.0.1:11434'] = process.argv.slice(2);
|
|
4
|
+
if (!model) {
|
|
5
|
+
console.error('Usage: node scripts/run-ollama-prompt.mjs <model> [ollama-base-url]');
|
|
6
|
+
process.exit(2);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let prompt = '';
|
|
10
|
+
process.stdin.setEncoding('utf8');
|
|
11
|
+
for await (const chunk of process.stdin) prompt += chunk;
|
|
12
|
+
const response = await fetch(`${baseUrl.replace(/\/$/, '')}/api/generate`, {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
headers: { 'content-type': 'application/json' },
|
|
15
|
+
body: JSON.stringify({
|
|
16
|
+
model,
|
|
17
|
+
prompt,
|
|
18
|
+
stream: false,
|
|
19
|
+
think: false,
|
|
20
|
+
format: 'json',
|
|
21
|
+
options: { temperature: 0 },
|
|
22
|
+
}),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
throw new Error(`Ollama request failed: ${response.status} ${await response.text()}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const result = await response.json();
|
|
30
|
+
process.stdout.write(result.response ?? '');
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export type Blueprint = Record<string, any>;
|
|
2
|
+
|
|
3
|
+
export type ScaffoldSection = 'interactions' | 'responsive' | 'acceptance';
|
|
4
|
+
export type ScaffoldLanguage = 'en' | 'zh-Hant';
|
|
5
|
+
|
|
6
|
+
export interface ScaffoldOptions {
|
|
7
|
+
sections?: ScaffoldSection[];
|
|
8
|
+
language?: ScaffoldLanguage;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ScaffoldSummary {
|
|
12
|
+
interactions: number;
|
|
13
|
+
responsive: number;
|
|
14
|
+
acceptance: number;
|
|
15
|
+
total: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const SCAFFOLD_SECTIONS: ScaffoldSection[];
|
|
19
|
+
|
|
20
|
+
export function scaffoldInteractions(
|
|
21
|
+
blueprint: Blueprint,
|
|
22
|
+
options?: { language?: ScaffoldLanguage }
|
|
23
|
+
): { interactions: any[]; added: any[] };
|
|
24
|
+
|
|
25
|
+
export function scaffoldResponsive(
|
|
26
|
+
blueprint: Blueprint,
|
|
27
|
+
options?: { language?: ScaffoldLanguage }
|
|
28
|
+
): { responsive: any[]; added: any[] };
|
|
29
|
+
|
|
30
|
+
export function scaffoldAcceptance(
|
|
31
|
+
blueprint: Blueprint,
|
|
32
|
+
options?: { language?: ScaffoldLanguage }
|
|
33
|
+
): { acceptance: any[]; added: any[] };
|
|
34
|
+
|
|
35
|
+
export function scaffoldBlueprint(
|
|
36
|
+
blueprint: Blueprint,
|
|
37
|
+
options?: ScaffoldOptions
|
|
38
|
+
): { blueprint: Blueprint; summary: ScaffoldSummary };
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
// Pure, deterministic scaffolding for the three specification sections that are
|
|
2
|
+
// often left empty when a blueprint does not come from a built-in template
|
|
3
|
+
// (blank starts, custom trees, Angular imports): interactions, responsive rules,
|
|
4
|
+
// and acceptance criteria.
|
|
5
|
+
//
|
|
6
|
+
// Every function is NON-DESTRUCTIVE: it only appends derived items for things not
|
|
7
|
+
// already covered and never rewrites or reorders existing user content. Output is
|
|
8
|
+
// deterministic so the editor, the CLI, and the MCP server all produce the same
|
|
9
|
+
// result for the same blueprint.
|
|
10
|
+
|
|
11
|
+
export const SCAFFOLD_SECTIONS = ['interactions', 'responsive', 'acceptance'];
|
|
12
|
+
|
|
13
|
+
const MAX_GENERATED_INTERACTIONS = 12;
|
|
14
|
+
|
|
15
|
+
// Component types that imply a user interaction worth declaring.
|
|
16
|
+
const ACTIONABLE_TYPES = new Set([
|
|
17
|
+
'button',
|
|
18
|
+
'icon_button',
|
|
19
|
+
'button_group',
|
|
20
|
+
'nav_item',
|
|
21
|
+
'link',
|
|
22
|
+
'menu',
|
|
23
|
+
'command_palette',
|
|
24
|
+
'form',
|
|
25
|
+
'search_input',
|
|
26
|
+
'text_input',
|
|
27
|
+
'textarea',
|
|
28
|
+
'select',
|
|
29
|
+
'checkbox',
|
|
30
|
+
'radio_group',
|
|
31
|
+
'toggle',
|
|
32
|
+
'slider',
|
|
33
|
+
'date_picker',
|
|
34
|
+
'file_upload',
|
|
35
|
+
'tabs',
|
|
36
|
+
'stepper',
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const CHANGE_TYPES = new Set([
|
|
40
|
+
'select',
|
|
41
|
+
'checkbox',
|
|
42
|
+
'radio_group',
|
|
43
|
+
'toggle',
|
|
44
|
+
'slider',
|
|
45
|
+
'date_picker',
|
|
46
|
+
'file_upload',
|
|
47
|
+
'text_input',
|
|
48
|
+
'textarea',
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
const SUBMIT_TYPES = new Set(['form', 'search_input']);
|
|
52
|
+
|
|
53
|
+
function pick(language, en, zh) {
|
|
54
|
+
return language === 'zh-Hant' ? zh : en;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function uniqueId(prefix, existing) {
|
|
58
|
+
const used = new Set(existing);
|
|
59
|
+
let index = existing.length + 1;
|
|
60
|
+
let candidate = `${prefix}_${index}`;
|
|
61
|
+
while (used.has(candidate)) {
|
|
62
|
+
index += 1;
|
|
63
|
+
candidate = `${prefix}_${index}`;
|
|
64
|
+
}
|
|
65
|
+
return candidate;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function nodeList(blueprint) {
|
|
69
|
+
return Array.isArray(blueprint?.nodes) ? blueprint.nodes : [];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function triggerFor(type) {
|
|
73
|
+
if (SUBMIT_TYPES.has(type)) return 'submit';
|
|
74
|
+
if (CHANGE_TYPES.has(type)) return 'change';
|
|
75
|
+
return 'click';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function actionFor(node) {
|
|
79
|
+
const declared = node?.content?.action;
|
|
80
|
+
if (typeof declared === 'string' && declared.trim()) return declared.trim();
|
|
81
|
+
const type = node.type;
|
|
82
|
+
if (SUBMIT_TYPES.has(type)) return `submit:${node.id}`;
|
|
83
|
+
if (CHANGE_TYPES.has(type)) return `change:${node.id}`;
|
|
84
|
+
if (type === 'link' || type === 'nav_item') return `navigate:${node.id}`;
|
|
85
|
+
return `activate:${node.id}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Append interactions derived from actionable nodes not already wired. */
|
|
89
|
+
export function scaffoldInteractions(blueprint, { language = 'en' } = {}) {
|
|
90
|
+
const existing = Array.isArray(blueprint?.interactions) ? blueprint.interactions : [];
|
|
91
|
+
const wired = new Set(existing.map((item) => item?.source_node_id).filter(Boolean));
|
|
92
|
+
const ids = existing.map((item) => item?.id).filter(Boolean);
|
|
93
|
+
const added = [];
|
|
94
|
+
|
|
95
|
+
for (const node of nodeList(blueprint)) {
|
|
96
|
+
if (added.length >= MAX_GENERATED_INTERACTIONS) break;
|
|
97
|
+
if (!node?.id || !ACTIONABLE_TYPES.has(node.type)) continue;
|
|
98
|
+
if (wired.has(node.id)) continue;
|
|
99
|
+
wired.add(node.id);
|
|
100
|
+
|
|
101
|
+
const trigger = triggerFor(node.type);
|
|
102
|
+
const id = uniqueId('interaction', [...ids, ...added.map((item) => item.id)]);
|
|
103
|
+
const name = node.name ?? node.id;
|
|
104
|
+
added.push({
|
|
105
|
+
id,
|
|
106
|
+
trigger,
|
|
107
|
+
source_node_id: node.id,
|
|
108
|
+
action: actionFor(node),
|
|
109
|
+
result_state: pick(
|
|
110
|
+
language,
|
|
111
|
+
`The result of ${name} is visibly presented to the user.`,
|
|
112
|
+
`${name} 的操作結果清楚呈現給使用者。`
|
|
113
|
+
),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { interactions: [...existing, ...added], added };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const SUB_DESKTOP_VIEWPORTS = new Set(['tablet', 'mobile']);
|
|
121
|
+
|
|
122
|
+
function isMultiColumnGrid(node) {
|
|
123
|
+
const layout = node?.layout;
|
|
124
|
+
if (!layout) return false;
|
|
125
|
+
const columns = layout.grid?.columns ?? layout.columns;
|
|
126
|
+
return (layout.display === 'grid' || layout.mode === 'grid') && Number(columns) > 1;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function responsiveTargetsFor(blueprint, viewportId) {
|
|
130
|
+
const nodes = nodeList(blueprint);
|
|
131
|
+
const targets = [];
|
|
132
|
+
const seen = new Set();
|
|
133
|
+
const push = (nodeId, rule, changes) => {
|
|
134
|
+
if (!nodeId || seen.has(nodeId)) return;
|
|
135
|
+
seen.add(nodeId);
|
|
136
|
+
targets.push({ target_node_id: nodeId, rule, changes });
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
for (const node of nodes) {
|
|
140
|
+
if (node.type === 'sidebar') push(node.id, 'drawer', {});
|
|
141
|
+
}
|
|
142
|
+
for (const node of nodes) {
|
|
143
|
+
if (node.type === 'top_bar' || node.type === 'bottom_nav' || node.type === 'toolbar') {
|
|
144
|
+
push(node.id, viewportId === 'mobile' ? 'bottom_nav' : 'icon_only', {});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
for (const node of nodes) {
|
|
148
|
+
if (isMultiColumnGrid(node)) push(node.id, 'col_reduce', { columns: viewportId === 'mobile' ? 1 : 2 });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (targets.length === 0) {
|
|
152
|
+
const root = nodes.find((node) => node.parent_id == null) ?? nodes[0];
|
|
153
|
+
if (root) push(root.id, 'stack', {});
|
|
154
|
+
}
|
|
155
|
+
return targets;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Append responsive rules for sub-desktop viewports not already covered. */
|
|
159
|
+
export function scaffoldResponsive(blueprint) {
|
|
160
|
+
const existing = Array.isArray(blueprint?.responsive) ? blueprint.responsive : [];
|
|
161
|
+
const covered = new Set(existing.map((item) => `${item?.viewport}::${item?.target_node_id}`));
|
|
162
|
+
const viewports = Array.isArray(blueprint?.viewports) ? blueprint.viewports : [];
|
|
163
|
+
const added = [];
|
|
164
|
+
|
|
165
|
+
for (const viewport of viewports) {
|
|
166
|
+
const viewportId = viewport?.id;
|
|
167
|
+
if (!SUB_DESKTOP_VIEWPORTS.has(viewportId)) continue;
|
|
168
|
+
for (const target of responsiveTargetsFor(blueprint, viewportId)) {
|
|
169
|
+
const key = `${viewportId}::${target.target_node_id}`;
|
|
170
|
+
if (covered.has(key)) continue;
|
|
171
|
+
covered.add(key);
|
|
172
|
+
added.push({
|
|
173
|
+
viewport: viewportId,
|
|
174
|
+
rule: target.rule,
|
|
175
|
+
target_node_id: target.target_node_id,
|
|
176
|
+
changes: target.changes,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return { responsive: [...existing, ...added], added };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const REQUIRED_ACCEPTANCE_TYPES = ['layout', 'interaction', 'responsive', 'a11y'];
|
|
185
|
+
const MIN_ACCEPTANCE = 5;
|
|
186
|
+
|
|
187
|
+
function acceptanceCriterion(type, blueprint, language) {
|
|
188
|
+
const nodes = nodeList(blueprint);
|
|
189
|
+
const root = nodes.find((node) => node.parent_id == null) ?? nodes[0];
|
|
190
|
+
const subViewport =
|
|
191
|
+
(blueprint?.viewports ?? []).map((viewport) => viewport?.id).find((id) => SUB_DESKTOP_VIEWPORTS.has(id)) ??
|
|
192
|
+
'mobile';
|
|
193
|
+
switch (type) {
|
|
194
|
+
case 'layout':
|
|
195
|
+
return {
|
|
196
|
+
type: 'layout',
|
|
197
|
+
statement: pick(
|
|
198
|
+
language,
|
|
199
|
+
'All major regions match their declared placements on desktop.',
|
|
200
|
+
'桌面版所有主要區域的位置與尺寸符合 placement。'
|
|
201
|
+
),
|
|
202
|
+
target: 'desktop',
|
|
203
|
+
priority: 'blocker',
|
|
204
|
+
verification_method: 'screenshot_diff',
|
|
205
|
+
};
|
|
206
|
+
case 'interaction':
|
|
207
|
+
return {
|
|
208
|
+
type: 'interaction',
|
|
209
|
+
statement: pick(
|
|
210
|
+
language,
|
|
211
|
+
'Primary buttons and navigation controls declare and perform their actions.',
|
|
212
|
+
'所有主要按鈕與導覽操作都有明確 action 並可執行。'
|
|
213
|
+
),
|
|
214
|
+
target: '*',
|
|
215
|
+
priority: 'must',
|
|
216
|
+
verification_method: 'interaction_replay',
|
|
217
|
+
};
|
|
218
|
+
case 'responsive':
|
|
219
|
+
return {
|
|
220
|
+
type: 'responsive',
|
|
221
|
+
statement: pick(
|
|
222
|
+
language,
|
|
223
|
+
`The ${subViewport} layout adapts without horizontal overflow.`,
|
|
224
|
+
`${subViewport} 版面自適應且不水平溢出。`
|
|
225
|
+
),
|
|
226
|
+
target: subViewport,
|
|
227
|
+
priority: 'must',
|
|
228
|
+
verification_method: 'screenshot_diff',
|
|
229
|
+
};
|
|
230
|
+
case 'a11y':
|
|
231
|
+
return {
|
|
232
|
+
type: 'a11y',
|
|
233
|
+
statement: pick(
|
|
234
|
+
language,
|
|
235
|
+
'Interactive controls have understandable labels and visible focus states.',
|
|
236
|
+
'互動元件具備可理解的文字標籤與可見的 focus 狀態。'
|
|
237
|
+
),
|
|
238
|
+
target: '*',
|
|
239
|
+
priority: 'must',
|
|
240
|
+
verification_method: 'axe_audit',
|
|
241
|
+
};
|
|
242
|
+
case 'content':
|
|
243
|
+
default:
|
|
244
|
+
return {
|
|
245
|
+
type: 'content',
|
|
246
|
+
statement: pick(
|
|
247
|
+
language,
|
|
248
|
+
`Names and content explain the purpose of each major region${root ? ` starting at ${root.name ?? root.id}` : ''}.`,
|
|
249
|
+
'每個主要區域的名稱與內容能說明其用途。'
|
|
250
|
+
),
|
|
251
|
+
target: '*',
|
|
252
|
+
priority: 'should',
|
|
253
|
+
verification_method: 'manual_ia_review',
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Append acceptance criteria so coverage spans the required types and totals >= 5. */
|
|
259
|
+
export function scaffoldAcceptance(blueprint, { language = 'en' } = {}) {
|
|
260
|
+
const existing = Array.isArray(blueprint?.acceptance) ? blueprint.acceptance : [];
|
|
261
|
+
const presentTypes = new Set(existing.map((item) => item?.type).filter(Boolean));
|
|
262
|
+
const ids = existing.map((item) => item?.id).filter(Boolean);
|
|
263
|
+
const added = [];
|
|
264
|
+
|
|
265
|
+
const emit = (type) => {
|
|
266
|
+
const criterion = acceptanceCriterion(type, blueprint, language);
|
|
267
|
+
const id = uniqueId('acc', [...ids, ...added.map((item) => item.id)]);
|
|
268
|
+
added.push({ id, ...criterion });
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
for (const type of REQUIRED_ACCEPTANCE_TYPES) {
|
|
272
|
+
if (!presentTypes.has(type)) {
|
|
273
|
+
presentTypes.add(type);
|
|
274
|
+
emit(type);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const padOrder = ['content', 'responsive', 'a11y', 'interaction', 'layout'];
|
|
279
|
+
let padIndex = 0;
|
|
280
|
+
while (existing.length + added.length < MIN_ACCEPTANCE) {
|
|
281
|
+
emit(padOrder[padIndex % padOrder.length]);
|
|
282
|
+
padIndex += 1;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return { acceptance: [...existing, ...added], added };
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Scaffold the requested sections of a blueprint. Returns a NEW blueprint plus a
|
|
290
|
+
* per-section summary of how many items were added. Never mutates the input and
|
|
291
|
+
* never overwrites existing entries.
|
|
292
|
+
*/
|
|
293
|
+
export function scaffoldBlueprint(blueprint, { sections = SCAFFOLD_SECTIONS, language = 'en' } = {}) {
|
|
294
|
+
const requested = new Set(sections);
|
|
295
|
+
const next = { ...blueprint };
|
|
296
|
+
const summary = { interactions: 0, responsive: 0, acceptance: 0 };
|
|
297
|
+
|
|
298
|
+
if (requested.has('interactions')) {
|
|
299
|
+
const result = scaffoldInteractions(blueprint, { language });
|
|
300
|
+
next.interactions = result.interactions;
|
|
301
|
+
summary.interactions = result.added.length;
|
|
302
|
+
}
|
|
303
|
+
if (requested.has('responsive')) {
|
|
304
|
+
const result = scaffoldResponsive(next, { language });
|
|
305
|
+
next.responsive = result.responsive;
|
|
306
|
+
summary.responsive = result.added.length;
|
|
307
|
+
}
|
|
308
|
+
if (requested.has('acceptance')) {
|
|
309
|
+
const result = scaffoldAcceptance(next, { language });
|
|
310
|
+
next.acceptance = result.acceptance;
|
|
311
|
+
summary.acceptance = result.added.length;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
summary.total = summary.interactions + summary.responsive + summary.acceptance;
|
|
315
|
+
return { blueprint: next, summary };
|
|
316
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Scaffold the spec sections (interactions, responsive, acceptance) of a Blueprint.
|
|
3
|
+
// Derivation is deterministic and non-destructive: existing entries are preserved
|
|
4
|
+
// and only missing items are appended.
|
|
5
|
+
//
|
|
6
|
+
// Usage:
|
|
7
|
+
// node scripts/scaffold-blueprint.mjs <file.ui.json|yaml> [options]
|
|
8
|
+
// Options:
|
|
9
|
+
// --sections a,b,c Comma list of sections to scaffold (default: all).
|
|
10
|
+
// --language <lang> 'en' (default) or 'zh-Hant' for generated statements.
|
|
11
|
+
// --write Write the result back to the input file.
|
|
12
|
+
// --stdout Print the scaffolded blueprint to stdout (default if not --write).
|
|
13
|
+
|
|
14
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
15
|
+
import { extname, resolve } from 'node:path';
|
|
16
|
+
import yaml from 'js-yaml';
|
|
17
|
+
import { scaffoldBlueprint, SCAFFOLD_SECTIONS } from './scaffold-blueprint.lib.mjs';
|
|
18
|
+
|
|
19
|
+
function parseArgs(argv) {
|
|
20
|
+
const args = { file: null, sections: null, language: 'en', write: false, stdout: false };
|
|
21
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
22
|
+
const token = argv[i];
|
|
23
|
+
if (token === '--write') args.write = true;
|
|
24
|
+
else if (token === '--stdout') args.stdout = true;
|
|
25
|
+
else if (token === '--sections') args.sections = splitSections(argv[++i]);
|
|
26
|
+
else if (token.startsWith('--sections=')) args.sections = splitSections(token.slice('--sections='.length));
|
|
27
|
+
else if (token === '--language') args.language = argv[++i];
|
|
28
|
+
else if (token.startsWith('--language=')) args.language = token.slice('--language='.length);
|
|
29
|
+
else if (!args.file) args.file = token;
|
|
30
|
+
}
|
|
31
|
+
return args;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function splitSections(value) {
|
|
35
|
+
return (value ?? '')
|
|
36
|
+
.split(',')
|
|
37
|
+
.map((part) => part.trim())
|
|
38
|
+
.filter(Boolean);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isYaml(path) {
|
|
42
|
+
const ext = extname(path).toLowerCase();
|
|
43
|
+
return ext === '.yaml' || ext === '.yml';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function main() {
|
|
47
|
+
const args = parseArgs(process.argv.slice(2));
|
|
48
|
+
if (!args.file) {
|
|
49
|
+
console.error('Usage: node scripts/scaffold-blueprint.mjs <file.ui.json|yaml> [--sections a,b,c] [--language en|zh-Hant] [--write|--stdout]');
|
|
50
|
+
process.exit(2);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const sections = args.sections ?? SCAFFOLD_SECTIONS;
|
|
54
|
+
const unknown = sections.filter((section) => !SCAFFOLD_SECTIONS.includes(section));
|
|
55
|
+
if (unknown.length > 0) {
|
|
56
|
+
console.error(`Unknown section(s): ${unknown.join(', ')}. Valid: ${SCAFFOLD_SECTIONS.join(', ')}`);
|
|
57
|
+
process.exit(2);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const filePath = resolve(args.file);
|
|
61
|
+
const raw = await readFile(filePath, 'utf8');
|
|
62
|
+
const document = isYaml(filePath) ? yaml.load(raw) : JSON.parse(raw);
|
|
63
|
+
|
|
64
|
+
const { blueprint, summary } = scaffoldBlueprint(document, { sections, language: args.language });
|
|
65
|
+
|
|
66
|
+
const serialized = isYaml(filePath)
|
|
67
|
+
? yaml.dump(blueprint, { noRefs: true, lineWidth: 120, sortKeys: false })
|
|
68
|
+
: `${JSON.stringify(blueprint, null, 2)}\n`;
|
|
69
|
+
|
|
70
|
+
if (args.write) {
|
|
71
|
+
await writeFile(filePath, serialized, 'utf8');
|
|
72
|
+
console.error(
|
|
73
|
+
`✓ scaffolded ${args.file} (+${summary.interactions} interactions, +${summary.responsive} responsive, +${summary.acceptance} acceptance)`
|
|
74
|
+
);
|
|
75
|
+
} else {
|
|
76
|
+
process.stdout.write(serialized);
|
|
77
|
+
console.error(
|
|
78
|
+
`✓ +${summary.interactions} interactions, +${summary.responsive} responsive, +${summary.acceptance} acceptance (use --write to save)`
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
main().catch((err) => {
|
|
84
|
+
console.error(err);
|
|
85
|
+
process.exit(2);
|
|
86
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import { scoreImplementationBenchmark } from './agent-implementation-benchmark.lib.mjs';
|
|
6
|
+
|
|
7
|
+
const [measurementsPath, implementationReportPath, outputPath] = process.argv.slice(2);
|
|
8
|
+
if (!measurementsPath || !implementationReportPath) {
|
|
9
|
+
console.error('Usage: node scripts/score-agent-implementation.mjs <measurements.json> <implementation-report.json> [output.json]');
|
|
10
|
+
process.exit(2);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const [blueprint, measurements, implementationReport] = await Promise.all([
|
|
14
|
+
readFile(resolve('examples/freeform-actions.ui.json'), 'utf8').then(JSON.parse),
|
|
15
|
+
readFile(resolve(measurementsPath), 'utf8').then(JSON.parse),
|
|
16
|
+
readFile(resolve(implementationReportPath), 'utf8').then(JSON.parse),
|
|
17
|
+
]);
|
|
18
|
+
const result = scoreImplementationBenchmark(
|
|
19
|
+
blueprint,
|
|
20
|
+
measurements.candidate,
|
|
21
|
+
measurements.reference,
|
|
22
|
+
implementationReport
|
|
23
|
+
);
|
|
24
|
+
const output = `${JSON.stringify(result, null, 2)}\n`;
|
|
25
|
+
if (outputPath) await writeFile(resolve(outputPath), output, 'utf8');
|
|
26
|
+
else process.stdout.write(output);
|
|
27
|
+
process.exit(result.ready ? 0 : 1);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFile } from 'node:fs/promises';
|
|
4
|
+
import { resolve } from 'node:path';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
6
|
+
|
|
7
|
+
export function scoreAgentOutput(actual, expected) {
|
|
8
|
+
const checks = [];
|
|
9
|
+
compare(expected, actual, '$', checks);
|
|
10
|
+
const passed = checks.filter((check) => check.pass).length;
|
|
11
|
+
return {
|
|
12
|
+
score: Math.round((passed / checks.length) * 100),
|
|
13
|
+
passed,
|
|
14
|
+
total: checks.length,
|
|
15
|
+
checks,
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function main() {
|
|
20
|
+
const [actualPath, expectedPath = 'benchmarks/agent-readability/expected.json'] = process.argv.slice(2);
|
|
21
|
+
if (!actualPath) {
|
|
22
|
+
console.error('Usage: node scripts/score-agent-readability.mjs <agent-output.json> [expected.json]');
|
|
23
|
+
process.exit(2);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const actual = JSON.parse(await readFile(resolve(actualPath), 'utf8'));
|
|
27
|
+
const expected = JSON.parse(await readFile(resolve(expectedPath), 'utf8'));
|
|
28
|
+
const report = scoreAgentOutput(actual, expected);
|
|
29
|
+
console.log(JSON.stringify(report, null, 2));
|
|
30
|
+
process.exit(report.score === 100 ? 0 : 1);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function compare(expectedValue, actualValue, path, checks) {
|
|
34
|
+
if (Array.isArray(expectedValue)) {
|
|
35
|
+
checks.push({
|
|
36
|
+
path,
|
|
37
|
+
pass: JSON.stringify(actualValue) === JSON.stringify(expectedValue),
|
|
38
|
+
expected: expectedValue,
|
|
39
|
+
actual: actualValue,
|
|
40
|
+
});
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (expectedValue && typeof expectedValue === 'object') {
|
|
44
|
+
for (const [key, value] of Object.entries(expectedValue)) {
|
|
45
|
+
compare(value, actualValue?.[key], `${path}.${key}`, checks);
|
|
46
|
+
}
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
checks.push({ path, pass: Object.is(actualValue, expectedValue), expected: expectedValue, actual: actualValue });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
|
|
53
|
+
await main();
|
|
54
|
+
}
|