@opensassi/opencode 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/AGENTS.md +35 -0
  2. package/README.md +81 -0
  3. package/bin/opencode.js +3 -0
  4. package/lib/cli.js +38 -0
  5. package/lib/commands/init.js +117 -0
  6. package/lib/commands/print-agents.js +6 -0
  7. package/lib/commands/print-skill.js +8 -0
  8. package/lib/commands/run.js +57 -0
  9. package/lib/index.js +4 -0
  10. package/lib/util/paths.js +21 -0
  11. package/package.json +40 -0
  12. package/scripts/asm-optimizer/run-baseline.sh +158 -0
  13. package/scripts/check-artifacts.js +131 -0
  14. package/scripts/extract-artifacts.js +204 -0
  15. package/scripts/install/linux/ubuntu-noble-24.04/install.sh +94 -0
  16. package/scripts/install/osx/macos-sequoia-15.0/install.sh +115 -0
  17. package/scripts/install/windows/wsl2/install.ps1 +98 -0
  18. package/scripts/install.ps1 +32 -0
  19. package/scripts/install.sh +83 -0
  20. package/scripts/puppeteer-config.json +3 -0
  21. package/scripts/test-artifacts.js +346 -0
  22. package/scripts/validate-all.js +18 -0
  23. package/scripts/verify-artifact.js +157 -0
  24. package/skills/asm-optimizer/SKILL.md +295 -0
  25. package/skills/daily-evaluation/SKILL.md +86 -0
  26. package/skills/git/SKILL.md +100 -0
  27. package/skills/issue/SKILL.md +104 -0
  28. package/skills/npm-optimizer/SKILL.md +218 -0
  29. package/skills/opensassi/SKILL.md +77 -0
  30. package/skills/opensassi/scripts/ensure-gitignore.sh +89 -0
  31. package/skills/opensassi/scripts/env-check.ps1 +139 -0
  32. package/skills/opensassi/scripts/env-check.sh +200 -0
  33. package/skills/opensassi/scripts/install-flamegraph.sh +32 -0
  34. package/skills/opensassi/scripts/install-npm-deps.sh +25 -0
  35. package/skills/profiler/SKILL.md +213 -0
  36. package/skills/profiler/scripts/benchmark.sh +63 -0
  37. package/skills/profiler/scripts/common.sh +55 -0
  38. package/skills/profiler/scripts/compare.sh +63 -0
  39. package/skills/profiler/scripts/profile.sh +63 -0
  40. package/skills/profiler/scripts/setup.sh +32 -0
  41. package/skills/session-evaluation/SKILL.md +128 -0
  42. package/skills/skill-manager/SKILL.md +251 -0
  43. package/skills/system-design/SKILL.md +558 -0
  44. package/skills/system-design-review/SKILL.md +396 -0
  45. package/skills/todo/SKILL.md +165 -0
  46. package/skills-index.json +137 -0
@@ -0,0 +1,83 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # opencode project environment installer — OS detection and dispatch
5
+ # Usage: bash scripts/install.sh
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
8
+ OS="$(uname -s)"
9
+
10
+ detect_macos_codename() {
11
+ local ver="$1"
12
+ if [[ $ver == 15.* ]]; then echo "sequoia"
13
+ elif [[ $ver == 14.* ]]; then echo "sonoma"
14
+ elif [[ $ver == 13.* ]]; then echo "ventura"
15
+ elif [[ $ver == 12.* ]]; then echo "monterey"
16
+ elif [[ $ver == 11.* ]]; then echo "big-sur"
17
+ else echo "unknown"
18
+ fi
19
+ }
20
+
21
+ case "$OS" in
22
+ Darwin)
23
+ VERSION=$(sw_vers -productVersion)
24
+ CODENAME=$(detect_macos_codename "$VERSION")
25
+ DIR="macos-${CODENAME}-${VERSION}"
26
+ INSTALLER="$SCRIPT_DIR/install/osx/$DIR/install.sh"
27
+
28
+ if [ -f "$INSTALLER" ]; then
29
+ echo "==> opencode: detected macOS ${VERSION} (${CODENAME})"
30
+ bash "$INSTALLER"
31
+ else
32
+ echo "==> opencode: no installer for macOS ${VERSION} (${CODENAME})"
33
+ echo " Expected: $INSTALLER"
34
+ echo " Try running with a supported macOS version (11.0 or later)."
35
+ exit 1
36
+ fi
37
+ ;;
38
+
39
+ Linux)
40
+ # Check for WSL (Windows Subsystem for Linux)
41
+ if grep -qi "microsoft\|wsl" /proc/version 2>/dev/null; then
42
+ echo "==> opencode: WSL detected"
43
+ echo " Run the Windows installer from PowerShell as Administrator:"
44
+ echo ""
45
+ echo " powershell -ExecutionPolicy Bypass -File scripts\\install.ps1"
46
+ echo ""
47
+ echo " If scripts/install.ps1 is not available, run the WSL2 installer directly:"
48
+ echo " powershell -ExecutionPolicy Bypass -File scripts\\install\\windows\\wsl2\\install.ps1"
49
+ echo ""
50
+ exit 0
51
+ fi
52
+
53
+ # Read distro info
54
+ if [ ! -f /etc/os-release ]; then
55
+ echo "==> opencode: /etc/os-release not found — cannot detect Linux distro"
56
+ exit 1
57
+ fi
58
+
59
+ source /etc/os-release
60
+ NAME_LOWER=$(echo "${NAME%% *}" | tr '[:upper:]' '[:lower:]')
61
+ CODENAME="${VERSION_CODENAME:-$UBUNTU_CODENAME}"
62
+ VERSION="${VERSION_ID}"
63
+ DIR="${NAME_LOWER}-${CODENAME}-${VERSION}"
64
+ INSTALLER="$SCRIPT_DIR/install/linux/$DIR/install.sh"
65
+
66
+ if [ -f "$INSTALLER" ]; then
67
+ echo "==> opencode: detected ${NAME} ${VERSION} (${CODENAME})"
68
+ bash "$INSTALLER"
69
+ else
70
+ echo "==> opencode: no installer for ${NAME} ${VERSION} (${CODENAME})"
71
+ echo " Expected: $INSTALLER"
72
+ echo " Supported distributions are listed under scripts/install/linux/"
73
+ exit 1
74
+ fi
75
+ ;;
76
+
77
+ *)
78
+ echo "==> opencode: unsupported operating system: $OS"
79
+ echo " Supported: macOS, Linux (Ubuntu, Debian, Fedora, etc.)"
80
+ echo " Windows: use WSL2 + the PowerShell installer"
81
+ exit 1
82
+ ;;
83
+ esac
@@ -0,0 +1,3 @@
1
+ {
2
+ "args": ["--no-sandbox", "--disable-setuid-sandbox", "--disable-gpu"]
3
+ }
@@ -0,0 +1,346 @@
1
+ #!/usr/bin/env node
2
+ import { fileURLToPath } from 'url';
3
+ import { chromium } from 'playwright';
4
+ import { execSync } from 'child_process';
5
+ import fs from 'fs';
6
+ import path from 'path';
7
+ import sharp from 'sharp';
8
+
9
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
10
+ const ROOT = path.resolve(__dirname, '..');
11
+
12
+ function findArtifactDirs(specPath) {
13
+ // When --file is given, return only that spec's artifact directory
14
+ if (specPath) {
15
+ const absSpec = path.resolve(ROOT, specPath);
16
+ const specDir = path.dirname(absSpec);
17
+ const specName = path.basename(absSpec);
18
+ const artifactDir = path.join(specDir, '.artifacts', specName);
19
+ if (fs.existsSync(artifactDir)) {
20
+ const label = specPath.replace(/\.spec\.md$/, '');
21
+ return { [label]: artifactDir };
22
+ }
23
+ console.error(` No artifact directory found for ${specPath} (expected ${artifactDir})`);
24
+ return {};
25
+ }
26
+
27
+ const dirs = {};
28
+
29
+ const rootArtifacts = path.join(ROOT, '.artifacts');
30
+ if (fs.existsSync(rootArtifacts)) dirs['.artifacts'] = rootArtifacts;
31
+
32
+ for (const base of ['src', 'source']) {
33
+ const baseDir = path.join(ROOT, base);
34
+ if (!fs.existsSync(baseDir)) continue;
35
+
36
+ const xFind = (dir, label) => {
37
+ const artifactPath = path.join(dir, '.artifacts');
38
+ if (fs.existsSync(artifactPath)) {
39
+ dirs[label] = artifactPath;
40
+ }
41
+ for (const sub of fs.readdirSync(dir, { withFileTypes: true })) {
42
+ if (sub.isDirectory() && sub.name !== '.artifacts' && sub.name !== 'node_modules') {
43
+ xFind(path.join(dir, sub.name), `${label}/${sub.name}`);
44
+ }
45
+ }
46
+ };
47
+
48
+ for (const entry of fs.readdirSync(baseDir, { withFileTypes: true })) {
49
+ if (entry.isDirectory()) {
50
+ xFind(path.join(baseDir, entry.name), `${base}/${entry.name}`);
51
+ }
52
+ }
53
+ }
54
+
55
+ return dirs;
56
+ }
57
+
58
+ function findFilesRecursive(dir, ext) {
59
+ const results = [];
60
+ if (!fs.existsSync(dir)) return results;
61
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
62
+ const fullPath = path.join(dir, entry.name);
63
+ if (entry.isDirectory()) {
64
+ results.push(...findFilesRecursive(fullPath, ext));
65
+ } else if (entry.name.endsWith(ext)) {
66
+ results.push(fullPath);
67
+ }
68
+ }
69
+ return results;
70
+ }
71
+
72
+ async function validateMermaid(specPath) {
73
+ const results = { pass: [], fail: [] };
74
+ const dirs = findArtifactDirs(specPath);
75
+
76
+ let totalFound = 0;
77
+ for (const [label, dir] of Object.entries(dirs)) {
78
+ const mmdFiles = findFilesRecursive(dir, '.mmd');
79
+ totalFound += mmdFiles.length;
80
+ for (const mmdFile of mmdFiles) {
81
+ const pngFile = mmdFile.replace(/\.mmd$/, '.png');
82
+ const rel = path.relative(ROOT, mmdFile);
83
+ try {
84
+ const puppeteerConfig = path.join(ROOT, 'scripts', 'puppeteer-config.json');
85
+ execSync(`mmdc -i "${mmdFile}" -o "${pngFile}" -b transparent -p "${puppeteerConfig}"`, { stdio: 'pipe', timeout: 30000 });
86
+ results.pass.push({ file: rel, png: path.relative(ROOT, pngFile) });
87
+ console.log(` ✓ ${rel}`);
88
+ } catch (e) {
89
+ const stderr = (e.stderr || '').toString() || e.message;
90
+ results.fail.push({ file: rel, error: stderr.substring(0, 300) });
91
+ console.error(` ✗ ${rel}: ${stderr.substring(0, 100)}`);
92
+ }
93
+ }
94
+ }
95
+
96
+ if (totalFound === 0) {
97
+ console.log(' (no .mmd files found — run `npm run extract` first)');
98
+ }
99
+
100
+ return results;
101
+ }
102
+
103
+ async function captureFilmstrip(htmlFile) {
104
+ const browser = await chromium.launch({ headless: true });
105
+ try {
106
+ const page = await browser.newPage({ viewport: { width: 720, height: 480 } });
107
+ await page.goto(`file://${htmlFile}`, { waitUntil: 'networkidle', timeout: 30000 });
108
+
109
+ const duration = await page.evaluate(() => {
110
+ if (typeof window.ANIMATION_DURATION_MS !== 'undefined') return window.ANIMATION_DURATION_MS;
111
+ try {
112
+ const meta = document.querySelector('meta[name="animation-duration-ms"]');
113
+ if (meta) return parseInt(meta.getAttribute('content'), 10);
114
+ } catch {}
115
+ return null;
116
+ });
117
+
118
+ const keyframes = await page.evaluate(() => {
119
+ if (typeof window.ANIMATION_KEYFRAMES !== 'undefined' && Array.isArray(window.ANIMATION_KEYFRAMES) && window.ANIMATION_KEYFRAMES.length > 0) {
120
+ return window.ANIMATION_KEYFRAMES.map(kf => ({ time: kf.time, label: kf.label }));
121
+ }
122
+ return null;
123
+ });
124
+
125
+ if (!keyframes) {
126
+ if (!duration || duration <= 0) {
127
+ return { pass: false, error: 'ANIMATION_KEYFRAMES not set in page' };
128
+ }
129
+ }
130
+
131
+ const filmDir = path.join(path.dirname(htmlFile), 'd3-animation-filmstrip');
132
+ fs.mkdirSync(filmDir, { recursive: true });
133
+
134
+ const frames = [];
135
+
136
+ if (keyframes) {
137
+ for (let i = 0; i < keyframes.length; i++) {
138
+ const kf = keyframes[i];
139
+ await page.evaluate((idx) => {
140
+ if (typeof window.jumpToKeyframe === 'function') {
141
+ window.jumpToKeyframe(idx);
142
+ } else {
143
+ // Fallback: seek via keyframes time if no jumpToKeyframe
144
+ const allKf = window.ANIMATION_KEYFRAMES || [];
145
+ const at = allKf[idx] ? allKf[idx].time : 0;
146
+ if (window.__seekToTime) window.__seekToTime(at);
147
+ }
148
+ }, i);
149
+ const settle = i === 0 ? 1000 : 200;
150
+ await new Promise(r => setTimeout(r, settle));
151
+ const domState = await page.evaluate(() => {
152
+ if (typeof window.getAnimationState === 'function') {
153
+ const s = window.getAnimationState();
154
+ return `hor=${s.hor} ver=${s.ver} prec=${s.precision} log=${s.logCount}`;
155
+ }
156
+ return '';
157
+ });
158
+ const frameFile = path.join(filmDir, `frame-${i}-${kf.label}.png`);
159
+ const tmpFile = path.join(filmDir, `frame-${i}-${kf.label}.tmp.png`);
160
+ await page.screenshot({ path: tmpFile });
161
+ const image = sharp(tmpFile);
162
+ const meta = await image.metadata();
163
+ if (meta.width !== 720) {
164
+ image.resize({ width: 720, fit: 'inside', withoutEnlargement: true });
165
+ }
166
+ await image.greyscale().toFile(frameFile);
167
+ fs.unlinkSync(tmpFile);
168
+ execSync(`convert "${frameFile}" -colorspace Gray "${frameFile}"`, { stdio: 'ignore' });
169
+ frames.push({ file: path.relative(ROOT, frameFile), label: kf.label, time: kf.time });
170
+ console.log(` frame ${i}/${keyframes.length} → frame-${i}-${kf.label}.png [${domState}]`);
171
+ }
172
+ return { pass: true, animationDurationMs: duration || keyframes[keyframes.length - 1].time, frames, keyframes, skipped: false };
173
+ }
174
+
175
+ // Fallback: fixed 5-fraction capture
176
+ for (let i = 0; i < 5; i++) {
177
+ const fraction = i / 4;
178
+ const waitMs = Math.round(fraction * duration);
179
+
180
+ if (i === 0) {
181
+ await new Promise(r => setTimeout(r, 500));
182
+ } else {
183
+ const prevFraction = (i - 1) / 4;
184
+ await new Promise(r => setTimeout(r, Math.round((fraction - prevFraction) * duration)));
185
+ }
186
+
187
+ const frameFile = path.join(filmDir, `frame-${i}.png`);
188
+ const tmpFile = path.join(filmDir, `frame-${i}.tmp.png`);
189
+ await page.screenshot({ path: tmpFile });
190
+ const image = sharp(tmpFile);
191
+ const meta = await image.metadata();
192
+ if (meta.width !== 720) {
193
+ image.resize({ width: 720, fit: 'inside', withoutEnlargement: true });
194
+ }
195
+ await image.greyscale().toFile(frameFile);
196
+ fs.unlinkSync(tmpFile);
197
+ execSync(`convert "${frameFile}" -colorspace Gray "${frameFile}"`, { stdio: 'ignore' });
198
+ frames.push({ file: path.relative(ROOT, frameFile), label: `frame-${i}`, time: waitMs });
199
+ console.log(` frame ${i}/5 @ ${waitMs}ms → frame-${i}.png`);
200
+ }
201
+
202
+ return { pass: true, animationDurationMs: duration, frames, keyframes: null, skipped: false };
203
+ } finally {
204
+ await browser.close();
205
+ }
206
+ }
207
+
208
+ async function validateD3(specPath) {
209
+ const results = { pass: [], fail: [], skipped: [] };
210
+ const dirs = findArtifactDirs(specPath);
211
+
212
+ let totalFound = 0;
213
+ for (const [label, dir] of Object.entries(dirs)) {
214
+ const htmlFiles = findFilesRecursive(dir, '.html');
215
+ totalFound += htmlFiles.length;
216
+ for (const htmlFile of htmlFiles) {
217
+ const rel = path.relative(ROOT, htmlFile);
218
+ console.log(` Loading ${rel}...`);
219
+ try {
220
+ const filmResult = await captureFilmstrip(htmlFile);
221
+ if (filmResult.pass) {
222
+ results.pass.push({ file: rel, ...filmResult });
223
+ console.log(` ✓ ${rel} — ${filmResult.animationDurationMs}ms, ${filmResult.frames.length} frames`);
224
+ } else {
225
+ results.fail.push({ file: rel, error: filmResult.error });
226
+ console.error(` ✗ ${rel}: ${filmResult.error}`);
227
+ }
228
+ } catch (e) {
229
+ results.fail.push({ file: rel, error: e.message });
230
+ console.error(` ✗ ${rel}: ${e.message}`);
231
+ }
232
+ }
233
+ }
234
+
235
+ if (totalFound === 0) {
236
+ console.log(' (no HTML files found)');
237
+ }
238
+
239
+ return results;
240
+ }
241
+
242
+ function writeReports(mermaidResults, d3Results, specPath) {
243
+ const dirs = findArtifactDirs(specPath);
244
+ for (const [label, dir] of Object.entries(dirs)) {
245
+ const report = {
246
+ timestamp: new Date().toISOString(),
247
+ mermaid: [],
248
+ d3_filmstrip: []
249
+ };
250
+
251
+ const allResults = [
252
+ ...mermaidResults.pass.map(r => ({ ...r, pass: true })),
253
+ ...mermaidResults.fail.map(r => ({ ...r, pass: false })),
254
+ ];
255
+ for (const r of allResults) {
256
+ const absFile = path.join(ROOT, r.file);
257
+ if (absFile.startsWith(dir)) {
258
+ report.mermaid.push(r);
259
+ }
260
+ }
261
+
262
+ const allD3 = [
263
+ ...d3Results.pass.map(r => ({ ...r, pass: true, skipped: false })),
264
+ ...d3Results.fail.map(r => ({ ...r, pass: false, skipped: false })),
265
+ ...d3Results.skipped.map(r => ({ ...r, pass: true, skipped: true })),
266
+ ];
267
+ for (const r of allD3) {
268
+ const absFile = path.join(ROOT, r.file);
269
+ if (absFile.startsWith(dir)) {
270
+ report.d3_filmstrip.push(r);
271
+ }
272
+ }
273
+
274
+ fs.writeFileSync(path.join(dir, 'report.json'), JSON.stringify(report, null, 2));
275
+ console.log(` report written to ${path.join(label, 'report.json')}`);
276
+ }
277
+ }
278
+
279
+ async function main() {
280
+ const args = process.argv.slice(2);
281
+ const fileIdx = args.indexOf('--file');
282
+ const specPath = fileIdx !== -1 ? args[fileIdx + 1] : null;
283
+
284
+ if (fileIdx !== -1 && !specPath) {
285
+ console.error('Usage: node scripts/test-artifacts.js [--file <relative-spec-path>]');
286
+ process.exit(1);
287
+ }
288
+ if (specPath) {
289
+ const absPath = path.resolve(ROOT, specPath);
290
+ if (!fs.existsSync(absPath)) {
291
+ console.error(`ERROR: Spec file not found: ${absPath}`);
292
+ process.exit(1);
293
+ }
294
+ }
295
+
296
+ console.log('=== Artifact Validation ===\n');
297
+
298
+ console.log('Searching for artifact directories...');
299
+ const dirs = findArtifactDirs(specPath);
300
+ if (Object.keys(dirs).length === 0) {
301
+ console.log(' No .artifacts/ directories found.');
302
+ console.log(' Run `npm run extract -- --all` first to extract artifacts from spec files.\n');
303
+ return;
304
+ }
305
+ for (const [label] of Object.entries(dirs)) {
306
+ console.log(` found ${label}/`);
307
+ }
308
+
309
+ console.log('\n--- Mermaid Diagrams ---');
310
+ const mermaidResults = await validateMermaid(specPath);
311
+ const mMetrics = {
312
+ passed: mermaidResults.pass.length,
313
+ failed: mermaidResults.fail.length,
314
+ total: mermaidResults.pass.length + mermaidResults.fail.length,
315
+ };
316
+ console.log(`\n ${mMetrics.passed} passed, ${mMetrics.failed} failed${mMetrics.total > 0 ? ` (${Math.round(mMetrics.passed / mMetrics.total * 100)}%)` : ''}\n`);
317
+
318
+ console.log('--- D3 Animation Filmstrip ---');
319
+ const d3Results = await validateD3(specPath);
320
+ const dMetrics = {
321
+ passed: d3Results.pass.length,
322
+ failed: d3Results.fail.length,
323
+ skipped: d3Results.skipped.length
324
+ };
325
+ console.log(`\n ${dMetrics.passed} captured, ${dMetrics.failed} failed, ${dMetrics.skipped} skipped\n`);
326
+
327
+ console.log('--- Writing Reports ---');
328
+ writeReports(mermaidResults, d3Results, specPath);
329
+
330
+ const totalFail = mermaidResults.fail.length + d3Results.fail.length;
331
+ console.log(`\n=== Summary ===`);
332
+ console.log(` Mermaid: ${mMetrics.passed}/${mMetrics.total} passed`);
333
+ console.log(` D3 Filmstrip: ${dMetrics.passed} captured, ${dMetrics.failed} failed`);
334
+
335
+ if (totalFail > 0) {
336
+ console.error(`\n✗ ${totalFail} artifact(s) failed validation.`);
337
+ process.exit(1);
338
+ }
339
+
340
+ console.log('\n✓ All artifact validations passed.');
341
+ }
342
+
343
+ main().catch(e => {
344
+ console.error('Fatal error:', e);
345
+ process.exit(1);
346
+ });
@@ -0,0 +1,18 @@
1
+ import { spawnSync } from 'node:child_process'
2
+ import { resolve } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+
5
+ const dir = resolve(fileURLToPath(import.meta.url), '..')
6
+
7
+ function run(script, args) {
8
+ const result = spawnSync(process.execPath, [resolve(dir, script), ...args], {
9
+ stdio: 'inherit'
10
+ })
11
+ return result.status ?? 1
12
+ }
13
+
14
+ const extractOk = run('extract-artifacts.js', ['--all'])
15
+ if (extractOk !== 0) process.exit(extractOk)
16
+
17
+ const testOk = run('test-artifacts.js', [])
18
+ process.exit(testOk ?? 0)
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env node
2
+ import { fileURLToPath } from 'url';
3
+ import path from 'path';
4
+ import fs from 'fs';
5
+ import http from 'http';
6
+ import { chromium } from 'playwright';
7
+
8
+ const __dirname = fileURLToPath(new URL('.', import.meta.url));
9
+ const ROOT = path.resolve(__dirname, '..');
10
+
11
+ function serveStatic(dir) {
12
+ const server = http.createServer((req, res) => {
13
+ const filePath = path.join(dir, req.url === '/' ? 'index.html' : req.url);
14
+ if (!fs.existsSync(filePath) || fs.statSync(filePath).isDirectory()) {
15
+ res.writeHead(404);
16
+ res.end('Not found');
17
+ return;
18
+ }
19
+ const ext = path.extname(filePath);
20
+ const mime = { '.html': 'text/html', '.js': 'application/javascript', '.css': 'text/css', '.png': 'image/png', '.svg': 'image/svg+xml' };
21
+ res.writeHead(200, { 'Content-Type': mime[ext] || 'application/octet-stream' });
22
+ fs.createReadStream(filePath).pipe(res);
23
+ });
24
+ return new Promise((resolve) => {
25
+ server.listen(0, '127.0.0.1', () => resolve(server));
26
+ });
27
+ }
28
+
29
+ function parseArgs() {
30
+ const args = process.argv.slice(2);
31
+ const fileIdx = args.indexOf('--file');
32
+ if (fileIdx === -1) {
33
+ console.error('Usage: node scripts/verify-artifact.js --file <path-to-d3-animation.html>');
34
+ process.exit(1);
35
+ }
36
+ return { filePath: args[fileIdx + 1] };
37
+ }
38
+
39
+ async function verify() {
40
+ const { filePath } = parseArgs();
41
+ const absPath = path.resolve(ROOT, filePath);
42
+ if (!fs.existsSync(absPath)) {
43
+ console.error(`ERROR: File not found: ${absPath}`);
44
+ process.exit(1);
45
+ }
46
+
47
+ const dir = path.dirname(absPath);
48
+ const fileName = path.basename(absPath);
49
+ const rel = path.relative(ROOT, absPath);
50
+
51
+ console.log(`Verifying ${rel}...\n`);
52
+
53
+ const server = await serveStatic(dir);
54
+ const port = server.address().port;
55
+ const url = `http://127.0.0.1:${port}/${fileName}`;
56
+
57
+ let browser;
58
+ const results = { pass: 0, fail: 0, total: 0, details: [] };
59
+
60
+ try {
61
+ browser = await chromium.launch({ headless: true });
62
+ const page = await browser.newPage({ viewport: { width: 720, height: 480 } });
63
+
64
+ await page.goto(url, { waitUntil: 'networkidle', timeout: 15000 });
65
+
66
+ // Check required globals
67
+ const duration = await page.evaluate(() => window.ANIMATION_DURATION_MS);
68
+ const keyframes = await page.evaluate(() => window.ANIMATION_KEYFRAMES);
69
+ const verification = await page.evaluate(() => window.ANIMATION_VERIFICATION);
70
+ const hasPlayPause = await page.evaluate(() => !!document.querySelector('[data-testid="play-pause"]'));
71
+
72
+ if (!duration || duration <= 0) {
73
+ console.error(' ✗ ANIMATION_DURATION_MS not set or invalid');
74
+ process.exit(1);
75
+ }
76
+ if (!keyframes || !keyframes.length) {
77
+ console.error(' ✗ ANIMATION_KEYFRAMES not set or empty');
78
+ process.exit(1);
79
+ }
80
+ if (!verification || verification.length !== keyframes.length) {
81
+ console.error(' ✗ ANIMATION_VERIFICATION missing or length mismatch');
82
+ process.exit(1);
83
+ }
84
+ if (!hasPlayPause) {
85
+ console.error(' ✗ [data-testid="play-pause"] not found');
86
+ process.exit(1);
87
+ }
88
+ console.log(` ✓ Globals: duration=${duration}ms, keyframes=${keyframes.length}, [data-testid="play-pause"] found\n`);
89
+
90
+ // Verify each keyframe
91
+ for (let i = 0; i < verification.length; i++) {
92
+ const expected = verification[i];
93
+
94
+ await page.evaluate((idx) => window.jumpToKeyframe(idx), i);
95
+
96
+ const state = await page.evaluate(() => window.getAnimationState());
97
+
98
+ const assertions = [
99
+ { field: 'hor', actual: state.hor, expected: expected.hor },
100
+ { field: 'ver', actual: state.ver, expected: expected.ver },
101
+ { field: 'precision', actual: state.precision, expected: expected.precision },
102
+ { field: 'logCount', actual: state.logCount, expected: expected.logCount },
103
+ ];
104
+
105
+ if (expected.bounds) {
106
+ assertions.push({ field: 'boundsOpacity', actual: state.boundsOpacity, expected: '1' });
107
+ }
108
+
109
+ const failures = assertions.filter(a => String(a.actual) !== String(a.expected));
110
+ const passed = failures.length === 0;
111
+
112
+ if (passed) {
113
+ results.pass++;
114
+ console.log(` ✓ [${i}] ${expected.label.padEnd(20)} hor=${state.hor} ver=${state.ver} prec=${state.precision} log=${state.logCount}`);
115
+ } else {
116
+ results.fail++;
117
+ console.error(` ✗ [${i}] ${expected.label}:`);
118
+ for (const f of failures) {
119
+ console.error(` ${f.field}: expected ${f.expected}, got ${f.actual}`);
120
+ }
121
+ }
122
+ results.total++;
123
+ results.details.push({ keyframe: i, label: expected.label, passed, failures: failures.map(f => f.field) });
124
+ }
125
+
126
+ // Check play/pause toggle
127
+ await page.evaluate(() => window.resetAnimation());
128
+ const playBtnTextBefore = await page.evaluate(() => document.querySelector('[data-testid="play-pause"]').textContent);
129
+ await page.click('[data-testid="play-pause"]');
130
+ await new Promise(r => setTimeout(r, 200));
131
+ const playBtnTextAfter = await page.evaluate(() => document.querySelector('[data-testid="play-pause"]').textContent);
132
+ const toggleOk = playBtnTextBefore !== playBtnTextAfter;
133
+ if (toggleOk) {
134
+ console.log(` ✓ Play/Pause toggle works: "${playBtnTextBefore.trim()}" → "${playBtnTextAfter.trim()}"`);
135
+ } else {
136
+ console.error(` ✗ Play/Pause toggle failed: text did not change ("${playBtnTextBefore.trim()}")`);
137
+ }
138
+
139
+ console.log(`\n=== Results ===`);
140
+ console.log(` ${results.pass}/${results.total} keyframes passed`);
141
+ if (results.fail > 0) {
142
+ console.error(` ${results.fail} keyframe(s) FAILED`);
143
+ process.exit(1);
144
+ }
145
+ console.log(` Play/Pause toggle: ${toggleOk ? '✓' : '✗'}`);
146
+ console.log(`\n✓ All assertions passed.`);
147
+
148
+ } catch (e) {
149
+ console.error('Fatal error:', e.message);
150
+ process.exit(1);
151
+ } finally {
152
+ if (browser) await browser.close();
153
+ server.close();
154
+ }
155
+ }
156
+
157
+ verify();