Repository navigation
Expand file tree
/
Copy pathpackage.json
More file actions
593 lines (593 loc) · 117 KB
/
Copy pathpackage.json
File metadata and controls
593 lines (593 loc) · 117 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
{
"name": "splotch",
"version": "1.6.0",
"description": "Splotch is a drawing app for toddlers: one SvelteKit codebase shipping the splotch.art web app and the Capacitor Android and iOS apps.",
"private": true,
"license": "MIT",
"repository": {
"type": "git",
"url": "git+https://github.com/KyleMit/Splotch.git"
},
"bugs": {
"url": "https://github.com/KyleMit/Splotch/issues"
},
"homepage": "https://splotch.art",
"type": "module",
"engines": {
"node": ">=22.13"
},
"packageManager": "pnpm@11.22.0",
"scripts": {
"info": "scripts-info",
"dev": "node tools/run-web-tool.mjs vite dev",
"predev:netlify": "node tools/check-netlify-cli.mjs",
"dev:netlify": "netlify dev --cwd web",
"dev:host": "node tools/run-web-tool.mjs vite dev --host",
"dev:cap": "CAPACITOR=true node tools/run-web-tool.mjs vite dev --host",
"dev:tunnel": "node tools/start-cloud-tunnel.mjs",
"dev:stop": "node tools/stop-dev-servers.mjs",
"prebuild": "node tools/run-web-tool.mjs svelte-kit sync && npm run gen:icon-names && npm run gen:releases",
"build": "node tools/run-web-tool.mjs vite build",
"postbuild": "node tools/check-release-seams.mjs && node --experimental-strip-types --disable-warning=ExperimentalWarning tools/check-build-version.mjs && node tools/check-pwa-precache.mjs && node --experimental-strip-types --disable-warning=ExperimentalWarning tools/mobile/check-static-bundle.mjs --web && node tools/check-bundle-budgets.mjs && node tools/check-eager-error-bundle.mjs",
"prebuild:cap": "node tools/run-web-tool.mjs svelte-kit sync && npm run gen:icon-names && npm run gen:releases",
"build:cap": "CAPACITOR=true node tools/run-web-tool.mjs vite build && node --experimental-strip-types --disable-warning=ExperimentalWarning tools/mobile/strip-static-assets.mjs",
"postbuild:cap": "node tools/check-release-seams.mjs && node --experimental-strip-types --disable-warning=ExperimentalWarning tools/check-build-version.mjs --native && node --experimental-strip-types --disable-warning=ExperimentalWarning tools/mobile/check-static-bundle.mjs && node tools/check-bundle-budgets.mjs --native && node tools/check-eager-error-bundle.mjs --native",
"preview": "node tools/run-web-tool.mjs vite preview",
"precheck": "node tools/mobile/check-app-ids.mjs && node tools/run-web-tool.mjs svelte-kit sync",
"check": "node tools/run-web-tool.mjs svelte-check --tsconfig ./tsconfig.json --fail-on-warnings && npm run check:netlify",
"check:netlify": "tsc --project netlify/tsconfig.json",
"check:watch": "node tools/run-web-tool.mjs svelte-check --tsconfig ./tsconfig.json --watch",
"check:adrs": "node tools/adrs/check-adr-integrity.mjs",
"check:coloring-assets": "node --experimental-strip-types --disable-warning=ExperimentalWarning tools/check-coloring-assets.mjs",
"check:assets:manifest": "node tools/asset-gen/gen-asset-manifest.mjs --check",
"check:ideas-review": "node tools/asset-gen/ideas-exploration/build-review.mjs --check",
"check:svg-assets": "node tools/optimize-svg-assets.mjs --check",
"optimize:svg-assets": "node tools/optimize-svg-assets.mjs",
"check:quality": "node tools/ci-mirror/run-quality-checks.mjs",
"check:github-actions": "node tools/check-github-action-versions.mjs",
"check:skill-refs": "node tools/check-skill-reference-syntax.mjs",
"check:doc-refs": "node tools/check-doc-references.mjs",
"check:agent-memories": "node tools/check-agent-memory-references.mjs",
"report:browser-floor": "node tools/report-browser-floor.mjs",
"show:pr-state": "node tools/github/show-pr-state.mjs",
"show:epic-children": "node tools/github/show-epic-children.mjs",
"show:free-port": "node tools/show-free-port.mjs",
"check:coloring-pack-retention": "node tools/release/check-coloring-pack-retention.mjs",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"lint:css": "stylelint \"**/*.{css,svelte}\"",
"lint:tokens": "node tools/tokens/lint-token-styles.mjs",
"lint:dead": "knip --no-progress --include files,exports,types,duplicates",
"lint:deps:prod": "knip --config knip.production.json --production --strict --no-progress",
"format": "prettier --write . && npm run format:md",
"format:check": "prettier --check . && npm run format:md:check",
"preformat:md": "node tools/check-dprint-plugins.mjs",
"format:md": "dprint fmt",
"preformat:md:check": "node tools/check-dprint-plugins.mjs",
"format:md:check": "dprint check --incremental=false",
"ruler:apply": "node tools/ruler/apply-ruler.mjs",
"ruler:check": "node tools/ruler/check-generated-files.mjs",
"ruler:dry-run": "ruler apply --dry-run --verbose",
"run-claude:install": "node .agents/skills/run-rival-agent/scripts/install-codex-policy.mjs",
"run-claude:policy:check": "node .agents/skills/run-rival-agent/scripts/check-codex-policy.mjs",
"run-claude:installation:check": "node .agents/skills/run-rival-agent/scripts/install-run-claude.mjs --check",
"rival:health": "node .claude/skills/run-rival-agent/scripts/codex-health.mjs",
"rival:launch": "node .claude/skills/run-rival-agent/scripts/launch-codex.mjs",
"rival:next": "node tools/rival-agent/broker.mjs next",
"rival:reply": "node tools/rival-agent/broker.mjs reply",
"rival:status": "node tools/rival-agent/broker.mjs status",
"rival:post": "node tools/rival-agent/post-review.mjs",
"rival:bench": "node tools/rival-agent/bench/run-bench.mjs",
"rival:seed": "node tools/export-codex-seed.mjs",
"gen:rival-acceptance": "node tools/rival-agent/gen-acceptance-suite.mjs",
"stack:push:rebased": "node tools/push-rebased-stack.mjs",
"test": "npm run test:unit:coverage && npm run test:asset-gen && npm run test:store-drawings && npm run test:tools && npm run test:e2e",
"perf:web": "PERF_MARKS=true node tools/perf/web/capture-web-session.mjs",
"perf:web:raw": "PERF_MARKS=true node tools/perf/web/capture-web-session.mjs --no-throttle",
"perf:web:mount": "PERF_MARKS=true node tools/perf/web/capture-web-mount.mjs",
"perf:web:settings": "node tools/perf/web/capture-settings-open.mjs",
"perf:android": "PERF_MARKS=true node tools/perf/android/capture-webview-session.mjs",
"preperf:android:browser:actions": "npm run perf:build",
"perf:android:browser:actions": "PERF_MARKS=true PUBLIC_ENABLE_DEV_HARNESS=true node tools/perf/android/capture-browser-actions.mjs",
"preperf:android:browser:clear-drag": "npm run perf:build",
"perf:android:browser:clear-drag": "PERF_MARKS=true PUBLIC_ENABLE_DEV_HARNESS=true node tools/perf/android/capture-clear-drag.mjs",
"perf:android:bundled:frames": "node tools/perf/android/capture-bundled-frames.mjs",
"perf:web:webkit": "PERF_MARKS=true node tools/perf/web/capture-webkit-session.mjs",
"preperf:ios:webkit:gates": "npm run perf:build",
"perf:ios:webkit:gates": "PERF_MARKS=true PUBLIC_ENABLE_DEV_HARNESS=true node tools/perf/ios/capture-webkit-gates.mjs",
"preperf:ios:webkit:frames": "npm run perf:build",
"perf:ios:webkit:frames": "PERF_MARKS=true PUBLIC_ENABLE_DEV_HARNESS=true node tools/perf/ios/capture-webkit-frames.mjs",
"preperf:ios:xcuitest:screen": "npm run perf:build",
"perf:ios:xcuitest:screen": "PERF_MARKS=true PUBLIC_ENABLE_DEV_HARNESS=true node tools/perf/ios/capture-xcuitest-screen.mjs",
"preperf:ios:webkit:commit": "npm run perf:build",
"perf:ios:webkit:commit": "PERF_MARKS=true PUBLIC_ENABLE_DEV_HARNESS=true node tools/perf/ios/capture-commit-contract.mjs",
"perf:ios:bundled:frames": "PERF_MARKS=true PUBLIC_ENABLE_DEV_HARNESS=true node tools/perf/ios/capture-xcuitest-screen.mjs --native-app --bundled-report",
"preperf:ios:xcuitest:actions": "npm run perf:build",
"perf:ios:xcuitest:actions": "PERF_MARKS=true PUBLIC_ENABLE_DEV_HARNESS=true node tools/perf/ios/capture-xcuitest-actions.mjs",
"perf:web:actions": "PERF_MARKS=true PUBLIC_ENABLE_DEV_HARNESS=true node tools/perf/web/capture-desktop-actions.mjs",
"perf:preflight": "node tools/perf/prepare-capture.mjs",
"perf:release": "node tools/perf/release-capture.mjs",
"perf:operator": "node tools/perf/run-operator-session.mjs",
"perf:session:person": "node tools/perf/run-person-session.mjs",
"perf:campaign": "node tools/perf/run-campaign.mjs",
"perf:campaign:sources": "node tools/perf/campaign-sources.mjs",
"perf:campaign:status": "node tools/perf/campaign-status.mjs",
"gen:crayon-glaze-sheet": "node tools/perf/gen-crayon-glaze-sheet.mjs",
"gen:performance-matrix": "node tools/perf/gen-performance-matrix.mjs",
"gen:flaky-digest": "node tools/flaky-digest/gen-flaky-digest.mjs",
"gen:code-map": "node tools/code-map/gen-code-map.mjs",
"check:matrix-staleness": "node tools/perf/check-matrix-staleness.mjs",
"check:device-identifiers": "node tools/perf/check-device-identifiers.mjs",
"perf:centerline-tracing": "uv run --project tools/centerline-tracing --locked python -m centerline_tracing.pipeline.bench all --width-mode piecewise",
"perf:web:undo": "PERF_MARKS=true node tools/perf/web/run-undo-scenarios.mjs",
"perf:web:undo:webkit": "PERF_MARKS=true node tools/perf/web/run-undo-scenarios.mjs --engine=webkit --no-throttle",
"perf:web:undo:webkit:fast": "PERF_MARKS=true node tools/perf/web/run-undo-scenarios.mjs --engine=webkit --no-throttle --suite=fast",
"perf:web:replay": "PERF_MARKS=true node tools/perf/web/replay-input-recording.mjs",
"perf:build": "PERF_MARKS=true PUBLIC_ENABLE_DEV_HARNESS=true npm run build",
"postperf:build": "node tools/perf/write-build-provenance.mjs",
"perf:build:cap": "PERF_MARKS=true PUBLIC_ENABLE_DEV_HARNESS=true npm run cap:sync",
"preperf:serve": "npm run perf:build",
"perf:serve": "PUBLIC_ENABLE_DEV_HARNESS=true node tools/perf/serve-profile-build.mjs",
"perf:ios:secure-origin": "node tools/perf/ios/secure-origin.mjs",
"perf:analyze:chrome": "node tools/perf/analyze-chrome-trace.mjs",
"perf:analyze:web-inspector": "node tools/perf/analyze-web-inspector.mjs",
"perf:analyze:frames": "node tools/perf/analyze-frame-capture.mjs",
"perf:rescore": "node tools/perf/rescore-captures.mjs",
"perf:evidence:keep": "node tools/perf/keep-capture-evidence.mjs",
"perf:web:frames": "PERF_MARKS=true PUBLIC_ENABLE_DEV_HARNESS=true node tools/perf/web/capture-local-frames.mjs",
"perf:device:serve": "PUBLIC_ENABLE_DEV_HARNESS=true node tools/perf/split-capture/serve-probe-host.mjs",
"perf:device:frames": "node tools/perf/split-capture/capture-device-frames.mjs",
"perf:device:hand": "node tools/perf/split-capture/capture-hand-input.mjs",
"perf:device:floor": "node tools/perf/split-capture/serve-floor-control.mjs",
"perf:device:verify-android": "node tools/perf/split-capture/verify-android-input.mjs",
"perf:device:verify-android-rotation": "node tools/perf/split-capture/verify-android-rotation.mjs",
"perf:device:probe-overhead": "node tools/perf/split-capture/measure-probe-overhead.mjs",
"test:unit": "node tools/run-web-tool.mjs vitest run && node tools/run-web-tool.mjs vitest run --config vitest.webSsr.config.ts",
"test:unit:coverage": "node tools/run-web-tool.mjs vitest run --coverage && node tools/run-web-tool.mjs vitest run --config vitest.webSsr.config.ts",
"test:unit:watch": "node tools/run-web-tool.mjs vitest",
"test:browserless": "node tools/ci-mirror/run-browserless-tests.mjs",
"test:asset-gen": "vitest run --config tools/asset-gen/vitest.config.mjs",
"test:store-drawings": "vitest run --config tools/store-drawings/vitest.config.mjs",
"test:centerline-tracing": "uv run --project tools/centerline-tracing --locked ruff check tools/centerline-tracing/src tools/centerline-tracing/tests && uv run --project tools/centerline-tracing --locked pytest -m 'not integration' && vitest run --config tools/centerline-tracing/vitest.config.mjs",
"test:tools": "vitest run --config tools/vitest.config.mjs",
"test:e2e": "node tools/run-web-tool.mjs playwright test",
"test:e2e:ui": "node tools/run-web-tool.mjs playwright test --ui",
"test:e2e:headed": "SLOWMO=500 node tools/run-web-tool.mjs playwright test --headed",
"test:e2e:debug": "node tools/run-web-tool.mjs playwright test --debug",
"test:e2e:install": "node tools/run-web-tool.mjs playwright install",
"test:e2e:sweep": "node tools/e2e-tuning/run-worker-sweep.mjs",
"test:android": "node tools/mobile/android/run-smoke-test.mjs",
"test:android:device": "maestro test .maestro/smoke.yaml",
"test:ios": "node tools/mobile/ios/run-simulator-smoke-test.mjs",
"test:api:smoke": "node --experimental-strip-types --disable-warning=ExperimentalWarning tools/api-smoke/run-local-contract.mjs",
"test:deploy:smoke": "node --experimental-strip-types --disable-warning=ExperimentalWarning tools/api-smoke/check-deployed-contract.mjs",
"test:blobs:smoke": "node tools/api-smoke/check-deployed-blobs.mjs",
"test:driver:smoke": "node tools/app-driver/run-driver-smoke-test.mjs",
"test:sweep:smoke": "node tools/e2e-tuning/run-worker-sweep.mjs --workers=1 --reps=1 --prebuilt --grep=changelog.spec.ts --out=/tmp/sweep-smoke",
"test:lighthouse:ci": "node tools/page-load/run-lighthouse-ci.mjs",
"test:firefox:smoke": "node tools/run-web-tool.mjs playwright test --project firefox",
"test:webkit:smoke": "node tools/run-web-tool.mjs playwright test --project webkit",
"fetch:image-reports": "node --experimental-strip-types --disable-warning=ExperimentalWarning tools/fetch-image-reports.mjs",
"redteam": "node --experimental-strip-types --disable-warning=ExperimentalWarning tools/redteam/run-safety-evaluation.mjs",
"redteam:encrypt": "node tools/redteam/manage-encrypted-fixtures.mjs encrypt",
"redteam:decrypt": "node tools/redteam/manage-encrypted-fixtures.mjs decrypt",
"model-eval": "node --experimental-strip-types --disable-warning=ExperimentalWarning tools/model-eval/run-model-evaluation.mjs",
"model-eval:fixtures": "node --experimental-strip-types --disable-warning=ExperimentalWarning tools/model-eval/gen-model-fixtures.mjs",
"model-eval:gen-inputs": "node --experimental-strip-types --disable-warning=ExperimentalWarning tools/model-eval/gen-model-inputs.mjs",
"model-eval:gen-crayon": "node --experimental-strip-types --disable-warning=ExperimentalWarning tools/model-eval/gen-crayon-inputs.mjs",
"model-eval:adherence": "node --experimental-strip-types --disable-warning=ExperimentalWarning tools/model-eval/run-prompt-adherence.mjs",
"gen:sound-effect": "node tools/elevenlabs/generate-sound-effects.mjs",
"gen:pencil-sounds": "node tools/sounds/gen-pencil-sounds.mjs",
"vectorize": "node tools/vectorize/vectorize-image.mjs",
"vectorize:coloring": "node tools/vectorize/vectorize-coloring-overlays.mjs",
"vectorize:coloring:analyze": "node tools/vectorize/analyze-coloring-overlays.mjs",
"vectorize:coloring:check": "node tools/vectorize/vectorize-coloring-overlays.mjs --check",
"vectorize:postprocess": "node tools/vectorize/postprocess-svg.mjs",
"vectorize:postprocess:check": "node tools/vectorize/postprocess-svg.mjs --check",
"gen:icon-names": "node tools/icons/gen-icon-names.mjs",
"gen:icon-viewbox": "node tools/icons/rebase-icon-viewbox.mjs",
"gen:releases": "node tools/release/gen-release-notes.mjs",
"gen:coloring-pack-snapshot": "node tools/release/gen-coloring-pack-snapshot.mjs",
"gen:tokens": "node --experimental-strip-types --disable-warning=ExperimentalWarning tools/tokens/gen-token-css.mjs",
"gen:tokens:check": "node --experimental-strip-types --disable-warning=ExperimentalWarning tools/tokens/gen-token-css.mjs --check",
"gen:share-cards": "node --experimental-strip-types --disable-warning=ExperimentalWarning tools/marketing-assets/gen-share-cards.mjs",
"gen:promotional-image": "node --experimental-strip-types --disable-warning=ExperimentalWarning tools/marketing-assets/gen-promotional-image.mjs",
"gen:centerlines": "node tools/centerline-tracing/trace-centerlines.mjs",
"gen:store-drawings": "node --experimental-strip-types --disable-warning=ExperimentalWarning tools/store-drawings/gen-pointer-instructions.mjs",
"gen:store-drawings:check": "node --experimental-strip-types --disable-warning=ExperimentalWarning tools/store-drawings/gen-pointer-instructions.mjs --check",
"gen:store-drawings:evaluate": "node --experimental-strip-types --disable-warning=ExperimentalWarning tools/store-drawings/evaluate-drawing-fidelity.mjs",
"gen:store-drawings:review": "node --experimental-strip-types --disable-warning=ExperimentalWarning tools/store-drawings/gen-brush-review.mjs",
"gen:readme-hero": "node --experimental-strip-types --disable-warning=ExperimentalWarning tools/marketing-assets/gen-readme-hero.mjs",
"gen:store-assets": "node --experimental-strip-types --disable-warning=ExperimentalWarning tools/marketing-assets/gen-store-assets.mjs",
"gen:store-assets:frames": "node --experimental-strip-types --disable-warning=ExperimentalWarning tools/marketing-assets/gen-store-assets.mjs --frames-only",
"gen:style-covers": "node --experimental-strip-types --disable-warning=ExperimentalWarning tools/asset-gen/style-covers/gen-style-covers.mjs",
"gen:coloring-chalk": "node --experimental-strip-types --disable-warning=ExperimentalWarning tools/asset-gen/coloring/gen-chalk-outlines.mjs",
"gen:coloring-fills": "node --experimental-strip-types --disable-warning=ExperimentalWarning tools/asset-gen/coloring/gen-light-fills.mjs",
"check:coloring-fill-drift": "node tools/asset-gen/coloring/check-fill-drift.mjs",
"check:coloring-fill-eyes": "node tools/asset-gen/coloring/check-fill-eyes.mjs",
"gen:coloring-eye-review": "node tools/asset-gen/coloring/gen-eye-review.mjs",
"check:coloring-invented-shapes": "node tools/asset-gen/coloring/check-invented-shapes.mjs",
"check:coloring-night-halo": "node tools/asset-gen/coloring/check-night-halo.mjs",
"check:coloring-outline-quality": "node tools/asset-gen/coloring/check-outline-quality.mjs",
"gen:coloring-outlines:fresh": "node --experimental-strip-types --disable-warning=ExperimentalWarning tools/asset-gen/coloring/gen-fresh-outlines.mjs",
"gen:coloring-outlines:normalize": "node --experimental-strip-types --disable-warning=ExperimentalWarning tools/asset-gen/coloring/normalize-outline-strokes.mjs",
"update:coloring-golden-scores": "node tools/asset-gen/coloring/check-golden-scores.mjs --freeze",
"check:coloring-golden-scores": "node tools/asset-gen/coloring/check-golden-scores.mjs --diff",
"gen:coloring-punched-fills": "node tools/asset-gen/coloring/punch-fill-outlines.mjs",
"gen:coloring-responsive": "node --experimental-strip-types --disable-warning=ExperimentalWarning tools/asset-gen/coloring/gen-responsive-assets.mjs",
"gen:coloring-thumbs": "node tools/asset-gen/coloring/gen-thumbnails.mjs",
"gen:coloring-book-proof-sheet": "node --experimental-strip-types --disable-warning=ExperimentalWarning tools/asset-gen/coloring/gen-book-proof-sheet.mjs",
"gen:e2e-tuning-report": "node tools/e2e-tuning/gen-tuning-report.mjs",
"capture:page-inventory": "node --experimental-strip-types --disable-warning=ExperimentalWarning tools/page-inventory/capture-page-inventory.mjs",
"review:page-inventory": "node tools/page-inventory/run-inventory-critiques.mjs",
"finalize:page-inventory-critique": "node tools/page-inventory/finalize-page-critique.mjs",
"attach:page-inventory-feedback": "node --experimental-strip-types --disable-warning=ExperimentalWarning tools/page-inventory/attach-page-feedback.mjs",
"gen:assets:manifest": "node tools/asset-gen/gen-asset-manifest.mjs",
"scrapbook:publish": "node tools/scrapbook/publish-scrapbook.mjs",
"scrapbook:index": "node tools/scrapbook/publish-scrapbook.mjs --index-only",
"scrapbook:serve": "vite scrapbook --host 127.0.0.1 --port 4174 --strictPort",
"scrapbook:proof-sheet-hub": "node tools/scrapbook/gen-proof-sheet-hub.mjs",
"scrapbook:check": "node tools/scrapbook/publish-scrapbook.mjs --check",
"scrapbook:clear-sound-sheet": "node tools/scrapbook/clear-sound-sheet/gen.mjs",
"scrapbook:clear-sound-sheet:audit": "node tools/scrapbook/clear-sound-sheet/audit.mjs",
"cap:sync": "npm run build:cap && cap sync",
"cap:android": "npm run cap:sync && cap open android",
"cap:ios": "npm run cap:sync && cap open ios",
"android:setup": "node tools/mobile/android/setup-emulator.mjs",
"android:boot": "node tools/mobile/android/run-emulator.mjs boot",
"android:emulator": "node tools/mobile/android/run-emulator.mjs emulator",
"android:live": "node tools/mobile/android/run-emulator.mjs live",
"android:apk": "npm run cap:sync && node tools/mobile/android/run-gradle.mjs :app:assembleDebug",
"android:apk:release": "npm run cap:sync && node tools/mobile/android/run-gradle.mjs :app:assembleRelease",
"android:run": "npm run cap:sync && node tools/mobile/android/run-gradle.mjs :app:installDebug",
"android:run:device": "npm run cap:sync && node tools/mobile/android/run-on-device.mjs",
"android:bundle": "npm run cap:sync && node tools/mobile/android/run-gradle.mjs :app:bundleRelease",
"android:verify": "node tools/mobile/android/verify-release-bundle.mjs",
"android:open": "node tools/mobile/android/open-release-bundle.mjs",
"android:clean": "node tools/mobile/android/run-gradle.mjs clean",
"android:compile": "mkdir -p android/app/src/main/assets/public && cap update android && node tools/mobile/android/run-gradle.mjs :app:compileReleaseJavaWithJavac",
"ios:build": "npm run cap:sync && cd ios/App && xcodebuild -scheme App -configuration Debug -destination \"generic/platform=iOS Simulator\" -derivedDataPath build build",
"ios:build:release": "npm run cap:sync && cd ios/App && xcodebuild -scheme App -configuration Release -destination \"generic/platform=iOS Simulator\" -derivedDataPath build CODE_SIGNING_ALLOWED=NO build",
"ios:run": "npm run cap:sync && cap run ios",
"ios:run:emulator": "npm run cap:sync && cap run ios --target C6012C49-AA93-4869-B3A6-E47C9EAAC567",
"ios:run:device": "npm run cap:sync && node tools/mobile/ios/run-on-device.mjs",
"ios:live": "cap run ios --live-reload --port 5173",
"ios:archive": "npm run cap:sync && cd ios/App && xcodebuild -scheme App -configuration Release -destination generic/platform=iOS -archivePath build/App.xcarchive -xcconfig ../local.xcconfig -allowProvisioningUpdates archive",
"ios:ipa": "npm run ios:archive && cd ios/App && xcodebuild -exportArchive -archivePath build/App.xcarchive -exportOptionsPlist ExportOptions.plist -exportPath build/ipa -allowProvisioningUpdates",
"ios:open": "node tools/mobile/ios/open-release-artifacts.mjs",
"ios:clean": "cd ios/App && xcodebuild -scheme App clean",
"adb:devices": "adb devices",
"adb:reverse": "adb reverse tcp:5173 tcp:5173",
"audit:preflight": "node tools/audit-burndown/check-preflight.mjs",
"audit:burndown": "node tools/audit-burndown/run-burndown.mjs",
"audit:burndown:overnight": "node tools/audit-burndown/launch-overnight.mjs",
"audit:status": "node tools/audit-burndown/show-status.mjs",
"audit:cost": "node tools/audit-burndown/show-cost.mjs",
"audit:watch": "node tools/audit-burndown/watch-run.mjs",
"prerelease": "npm run check:coloring-assets && npm run check:assets:manifest",
"release": "node tools/release/cut-release.mjs",
"release:publish": "node tools/release/publish-release-artifacts.mjs",
"worktrees:salvage": "node tools/git-housekeeping/salvage-worktree-evidence.mjs",
"worktrees:prune": "node tools/git-housekeeping/prune-agent-worktrees.mjs",
"branches:prune": "node tools/git-housekeeping/prune-local-branches.mjs",
"branches:gather": "node tools/git-housekeeping/gather-remote-branches.mjs",
"gen:android:assets": "capacitor-assets generate --android && node tools/mobile/android/gen-monochrome-icon.mjs"
},
"scripts-info": {
"info": "Lists every npm script with its description (this output)",
"dev": "Dev server at localhost:5173 (no /api functions)",
"predev:netlify": "Checks the Netlify CLI is on PATH, logged in, and linked before dev:netlify (Blobs needs auth), with a hint if not",
"dev:netlify": "Dev server with the /api/* serverless functions (Netlify Dev)",
"dev:host": "Dev server exposed on the local network (for phones and other devices)",
"dev:cap": "Dev server in native mode (CAPACITOR=true) on the network, for android:live",
"dev:tunnel": "Claude Code Cloud only: start dev + a chisel reverse tunnel and print the public phone-preview URL (needs TUNNEL_AUTH; see docs/CLOUD/Claude-Code.md, ADR-0021)",
"dev:stop": "Kill any stale dev servers holding the dev ports (vite 5173, netlify dev 8888)",
"prebuild": "svelte-kit sync + gen:icon-names + gen:releases before build",
"build": "Web build (adapter-netlify) for splotch.art",
"postbuild": "Verify release seams, one app version across client/server/version.json/coloring manifest/service worker, PWA precache integrity, native-code exclusion, startup JS/CSS bytes and modulepreload count, the largest lazy JS chunk, and the eagerly loaded root error import closure after build",
"prebuild:cap": "svelte-kit sync + gen:icon-names + gen:releases before build:cap",
"build:cap": "Native static build (CAPACITOR=true, adapter-static); keeps one starter coloring book and strips downloadable/web-only assets",
"postbuild:cap": "Verify release seams, one app version across client/server/version.json/coloring manifest, native bundle boundaries, the stripped static export byte budget, the index.html modulepreload count, and the eagerly loaded root error import closure after build:cap",
"preview": "Serve the production web build locally",
"precheck": "Verify every native/tooling app ID matches canonical capacitor.config.json.appId, then generate .svelte-kit types (svelte-kit sync) so type checking can run standalone",
"check": "Type-check the SvelteKit app and custom Netlify functions (one-shot; Svelte warnings fail too)",
"check:netlify": "Type-check custom Netlify functions with their standalone TypeScript project",
"check:watch": "svelte-check in watch mode",
"check:adrs": "Fails when docs/adrs/ identity or index invariants drift: unique numbers across this tree and its base, matching H1s, exactly one canonical index entry per record, and matching labels and targets for every local ADR link; run by the ADR Integrity workflow on every PR (pass --base=<ref>; defaults to origin/main)",
"check:coloring-assets": "Validate the coloring-book catalog against web/static: every catalog asset exists, no coloring asset is unreferenced, no book is both mobile-eligible and stripped as web-only, and no Markdown sits in the publicly served tree. Auto-runs before release; also run against the real tree by npm run test:tools, so CI checks it on every PR",
"check:assets:manifest": "CI guard for gen:assets:manifest: fail if any committed art asset's bytes drifted from tools/asset-gen/golden/asset-manifest.sha256 (e.g. a night pass touched a light file); run gen:assets:manifest and commit to fix",
"check:ideas-review": "CI drift guard for the committed tools/asset-gen/ideas-exploration/ideas-review.html dashboard; regenerate it with node tools/asset-gen/ideas-exploration/build-review.mjs and commit the result",
"check:svg-assets": "CI guard for optimize:svg-assets: fail if any SVG under web/ is not already optimized (run optimize:svg-assets and commit to fix)",
"optimize:svg-assets": "Optimize every shipped/inlined SVG under web/ in place with SVGO (visually-lossless preset; keeps viewBox). Idempotent — re-running only rewrites new/edited SVGs",
"check:quality": "Run CI's Quality job locally, in its order — format, type, lint, the drift guards, the token ratchet, dead code, the asset manifest, the scrapbook index, and the dependency audit. Unlike CI it keeps going after a failure, so one run surfaces every problem instead of one per push; test:browserless mirrors the Browserless tests job. tools/ci-mirror/tests/run-quality-checks.test.mjs fails if this list drifts from the workflow",
"check:github-actions": "Report the GitHub Actions version pins across .github/workflows and flag drift; add --check-latest to compare each against its latest upstream release (network)",
"check:skill-refs": "Fail when shared prose spells a skill with a runner-specific sigil — Claude Code's /name or Codex's $name — instead of naming it bare; runner-owned trees and the historical records are exempt. Also run as a test by npm run test:tools",
"check:doc-refs": "Fail when a living doc, skill, or rule names a repo path or npm script that no longer resolves; history records, skill notes, and generated copies are exempt, gitignored paths pass, and ALLOWED_REFERENCES holds the deliberate exceptions. -- --identifiers also lists code identifiers no tracked source names (advisory, for the reconcile-docs-with-code skill). Also run as a test by npm run test:tools",
"check:agent-memories": "Read-only check of a supplied Claude project memory directory: index, wiki links, missing frontmatter names, and npm scripts fail on drift; display names, unscanned entries, repo paths, possible skill names, and flags print for human review. Pass -- --memory-dir=<path>; the external memory is not in CI",
"report:browser-floor": "Read-only evidence for the audit-compatibility skill: global usage the browser floor reaches vs Baseline Widely Available, every runtime feature probe in shipped web/src with whether the risk register cites it, and -- --feature <web-features id> lookups against the floor",
"show:pr-state": "Read-only PR snapshot from gh: current head, merge metadata, registered checks, and paginated review threads; -- <number> [--repo owner/name] [--json]. Check expected CI registration separately",
"show:epic-children": "Read-only inventory of an epic's nested GitHub sub-issues, with per-parent and unique counts; -- <number> [--repo owner/name] [--json]. Read comments before classifying children",
"show:free-port": "Print one currently free local TCP port without stopping any listener; -- [--from 5300] [--to 5399] [--host localhost]. Retry on EADDRINUSE when starting the server",
"check:coloring-pack-retention": "Fail when web/static no longer serves, byte-identically, a coloring-pack file some released native app downloads by manifest path + SHA-256 (ADR-0103); reads the committed tools/release/coloring-pack-snapshots/ and prints per-release unserved/blocked counts. Every pack-shipping release is listed with its exact unserved-file count in RELEASED_PACK_BROKEN_FILES (0 when healthy), so a lost snapshot also fails. Also run as a test by npm run test:tools",
"lint": "ESLint over the whole repo minus the ignores in eslint.config.js (flat config; enforces runes-only, no legacy stores)",
"lint:fix": "ESLint with autofix",
"lint:css": "Stylelint over every <style> block and hand-authored .css file in the repo (stylelint.config.js; postcss-html parses the components, ignoreFiles excludes generated and promoted output). The rule set is grown one measured rule at a time — see ADR-0031 for the adopted set and the rejected candidates with their counts",
"lint:tokens": "Token-style ratchet for component <style> blocks (ADR-0071): fails if any file's raw hex-color, raw font-size, or entirely unpinned :global() selector count drifts from its allowlisted baseline in tools/tokens/lint-token-styles.mjs, or a raw 2+-digit z-index bypasses the --z-* tokens",
"lint:dead": "Dead-code gate (knip): fails on unused files, exports, exported types, and duplicate exports across web/ (src, tests, root configs), netlify, and the tools capabilities enumerated in knip.json — remove the symbol or tag a deliberate seam with /** @public */",
"lint:deps:prod": "ADR-0070 dependency-split gate (knip --production --strict over knip.production.json): fails when code the Netlify deploy executes imports a package filed under devDependencies, which Netlify's --prod install omits, or when a dependencies entry is something the deploy never uses",
"format": "Format the whole repo: Prettier for source and config files, dprint for markdown",
"format:check": "Check Prettier + dprint formatting without writing — what CI runs",
"preformat:md": "Checks this checkout's installed dprint plugins against the ranges in package.json, so a stale worktree install fails naming the version gap instead of a config option dprint cannot parse",
"format:md": "Format all markdown docs with dprint (asterisk bullets/emphasis, wrap at 100, ADR-0057)",
"preformat:md:check": "Checks this checkout's installed dprint plugins against the ranges in package.json, so a stale worktree install fails naming the version gap instead of a config option dprint cannot parse",
"format:md:check": "Check markdown formatting without writing or a host cache — part of format:check",
"ruler:apply": "Regenerate agent files from .ruler/ while preserving direct provider skill packages, apply managed skill forks, then dprint-format the committed output (ADR-0058)",
"ruler:check": "CI drift guard for ruler:apply: re-apply and fail if generated agent output changed; excludes registered direct provider packages",
"ruler:dry-run": "Preview what ruler:apply would regenerate without writing any files (note: skips the dprint fmt pass, so blank-line noise it reports may format away)",
"run-claude:install": "Install the trusted rival-agent package (core + Codex-side launcher, poster, health probe) under ~/.local/libexec and the Codex prompt/deny policy; restart Codex afterward",
"run-claude:policy:check": "Read-only preflight: verify Codex approval settings, the effective wrapper/raw-Claude rules, and every installed rival-agent byte",
"run-claude:installation:check": "Read-only preflight: verify the installed rival-agent package and shims match the trusted canonical checkout",
"rival:health": "Read-only preflight for the Codex rival: verify the Codex CLI is installed and logged in on the ChatGPT plan rather than API billing",
"rival:launch": "Launch the Codex rival agent read-only in a disposable worktree with the broker attached; append --pr <n>, --base <ref>, --commit <sha>, or --uncommitted, plus --question-file or --prompt-file",
"rival:next": "Handler side of the broker: block for the rival's next command request, or its finished findings; append --session <dir>",
"rival:reply": "Handler side of the broker: answer one request with an exit code and captured output, or decline it; append --session <dir> --request <seq> …",
"rival:status": "Summarize a rival session: pending requests, done or failed; append --session <dir>",
"rival:post": "Post a finished rival session's findings to a PR as one COMMENT review with a hidden marker; append --pr <n> --session <dir>",
"rival:bench": "Run the seeded-defect bench: one fresh rival round per seed, sandbox mode, and repetition on the corpus under tools/rival-agent/bench/seeds, the bench serving the broker; --validate proves every seed first. Manual, spends plan usage, never in CI",
"rival:seed": "Laptop-side: sign in to Codex under a dedicated ~/.codex-cloud login and copy the CODEX_AUTH_JSON seed for the Claude cloud environment dialog to the clipboard, so cloud sessions can run the Codex rival on the ChatGPT plan; repeat once a cloud review has refreshed and rotated the previous seed",
"gen:rival-acceptance": "Generate a nonce-bearing manual real-agent communication question under the temp root; never runs in CI",
"stack:push:rebased": "Publish an intentional gh stack rebase, refusing to push when it changed the content of any PR another open PR is based on",
"test": "Coverage-gated app unit + asset-pipeline + store-drawing + repo-script unit (Vitest) + E2E (Playwright) — what CI runs on every push",
"perf:web": "Autonomous performance profile of the web app: builds the production bundle with PERF_MARKS=true, serves it via vite preview, drives a scripted toddler session (multi-finger draw, color/size changes, erase, undo, clear) in headless Chromium under 4x CPU throttle while recording a Chrome trace + runtime metrics, then writes trace.json/metrics.json/summary.json/report.md + a screenshot to perf-profiles/. Flags: --device=phone|tablet|desktop, --throttle=N, --no-build (reuse last build)",
"perf:web:raw": "perf:web without CPU throttling (raw desktop speed)",
"perf:web:mount": "Autonomous page-load (mount) profile — the Lighthouse-TBT window perf:web skips: builds the production bundle, then records a Chrome trace ACROSS the initial navigation in headless Chromium (phone viewport, 4x CPU throttle, Slow-4G network emulation), capturing load-phase long tasks via a buffered longtask observer plus paint timings. Writes trace.json (feed to perf:analyze:chrome) + mount-summary.json to perf-profiles/. Flags: --device=phone|tablet|desktop, --throttle=N, --no-throttle, --no-build",
"perf:web:settings": "First-show cost of the Settings dialog scored against a reopen — the dialog mounts closed, its pane prewarms at idle, and the closed card stays laid out but hidden (ADR-0049 amendment), with presentation staged separately (the open flip paints nothing, the fold paints a frame later, the rest reveal per frame after the fly-in); shown = the first presented section inside an [open] dialog, and the first-open-over-reopen gap is the open edge's unpayable residual: builds the production bundle, serves it with vite preview, taps Settings in headless Chromium under 4x CPU throttle with a longtask PerformanceObserver running, then closes and reopens, scoring the wide table-of-contents shell (desktop viewport) against the phone hub baseline (phone viewport). Reports the longest long task and tap-to-shown latency for first open and reopen, medians over --repeats. Writes settings-open.json to perf-profiles/. Flags: --throttle=N, --no-throttle, --repeats=N, --port=N, --no-build",
"perf:android": "Autonomous performance profile of the REAL Capacitor WebView on a connected Android emulator/device (local-only): rebuilds + installs the native app with PERF_MARKS=true, finds the WebView's DevTools socket, forwards it, connects Playwright over CDP, and runs the same toddler session + capture as perf:web (no CPU throttle — the device is the target). --no-build profiles the installed app as-is; --device-id=<serial> picks one device when adb lists several; unknown flags are refused. Writes to perf-profiles/",
"perf:android:bundled:frames": "Drawing capture of the BUNDLED Capacitor WebView on a physical Android device (issue 1323): the app loads its own assets — no probe host, no server.url — and the report leaves over the WebView DevTools socket (CDP), so the mixed-content block that forced remote delivery does not apply and no automation session touches the input path. Launches the installed debug app, verifies the attached target is the bundled Capacitor origin and the UA is the WebView, verifies the PAGE measured the requested orientation (releasing and afterwards restoring the app's own rotation lock when it holds the Activity away from the request; no artifact on a mismatch or a mid-capture resize), injects the real-screen probe over CDP, drives adb trusted input (--input=adb, default; an eraser cell fills verified ink first and proves before each pass that the paper is fully inked and after it that the pass erased ink, refilling between passes) or opens a countdown window for a real finger (--input=hand --seconds=N), then pulls the report and scores fidelity + drawing gates. Flags: --device-serial= (required), --brush=, --gesture-repeats=, --orientation=, --theme=, --input=adb|hand, --seconds=, --cdp-forward-port=, --label=, --output=. Requires a debug build installed (PERF_MARKS build for engine columns). Writes to perf-profiles/bundled/",
"preperf:android:browser:actions": "npm run perf:build — so perf:android:browser:actions measures a freshly instrumented production bundle. Skip the rebuild with: npm run perf:android:browser:actions --ignore-scripts",
"perf:android:browser:actions": "Run the shared discrete-action plan directly against Chrome on an ADB-connected Android emulator/device over CDP, avoiding Appium renderer pauses and coordinate mapping. Loads the page at localhost through `adb reverse` (a --url on this machine is rewritten; the reverse is removed on exit). Uses trusted CDP touch, isolates profiler-owned tabs, evicts service-worker caches and blocks re-registration, waits for stable frames before each sweep, and pins an adaptive-sync panel to 60Hz for the sweep, verified via dumpsys and restored afterward (ADR-0143 — an interrupted run can leak the pin; recovery in docs/PROFILING-CAMPAIGNS.md). Records the applicable action plan and product-surface context separately from measured summaries. Flags: --url=, --port=N, --device-id=, --cdp-port=N, --orientation=PORTRAIT|LANDSCAPE, --theme=light|dark, --actions=..., --repeats=N, --label=, --output=, --trace, --report-only, --no-serve, --allow-foreign-build (accept a --url serving another commit's build, for a historical comparison). Writes actions.json and optional trace.json to perf-profiles/.",
"preperf:android:browser:clear-drag": "npm run perf:build — so perf:android:browser:clear-drag measures a freshly instrumented production bundle. Skip the rebuild with: npm run perf:android:browser:clear-drag --ignore-scripts",
"perf:android:browser:clear-drag": "Hold the clear button in Android Chrome over CDP and scrub it back and forth across the accept radius for each toolbar style, sampling frame pacing in the page — the drag the discrete-action sweep cannot see. Loads the page at localhost through adb reverse (a --url on this machine is rewritten; the reverse is removed on exit) and blocks service-worker registration, because Chrome's Always-use-secure-connections setting blocks a plain-http LAN origin. Report-only. Flags: --toolbars=buttons,bare (default both, interleaved per repeat), --orientation=PORTRAIT|LANDSCAPE, --repeats=N, --cycles=N, --device-id=, --cdp-port=N, --port=N, --url=, --label=, --output=, --trace, --no-serve, --allow-foreign-build. Writes clear-drag.json and optional trace.json to perf-profiles/.",
"perf:web:webkit": "Autonomous performance profile in Playwright's WebKit (the same WebKit+JavaScriptCore engine the iOS WKWebView runs), driving the production preview build. WebKit has no CDP trace, so this captures the engine.* user-timing marks + FPS (not the main-thread/CPU breakdown); it profiles the engine, not the Simulator app. For device-accurate numbers, record a Safari Web Inspector Timeline against the running Simulator and feed it to perf:analyze:web-inspector. Writes to perf-profiles/",
"preperf:ios:webkit:frames": "npm run perf:build — so perf:ios:webkit:frames always measures a freshly instrumented bundle rather than whatever build happens to be on disk. Skip the rebuild with: npm run perf:ios:webkit:frames --ignore-scripts",
"perf:ios:webkit:frames": "Real-SCREEN performance capture on a REAL USB-connected iPad (local-only): serves the instrumented build on the LAN, attaches to Safari over the WebKit Inspector Protocol, navigates the tab to the actual app at / (not /dev/engine), and injects tools/perf/probes/real-screen-probe.js to record per-frame requestAnimationFrame deltas, input queue delay (event.timeStamp vs when its handler ran), paint latency, pointermove delivery/coalescing, engine.* measures and the stroke-end hitch. An on-device banner walks a human through a phase sweep — blank paper vs coloring page, then with the blend nudge / mix-blend-mode / PointerHalos suppressed by CSS — so the felt lag can be attributed to a named subsystem. Answers what perf:ios:webkit:gates structurally cannot: it passes every ADR-0066 gate while the real screen lags. Flags: --timeline (also count the rendering work — RenderingFrame/Composite/Paint/RecalculateStyles/Layout — off the protocol Timeline domain; needs exactly one --phases key, since those records arrive with zeroed timestamps and cannot be attributed to a moment), --free-draw=SECONDS (wall-clock capture opened by the HUD's START button), --drive[=long|short|mixed] (unattended: the synthetic hand, no human needed — this is what produced the synthetic half of the evidence), --drive-hz=N (moves per second; unset drives one per frame), --pointer-type=touch|pen, --brush=pen|crayon|magic|eraser, --hud (force the on-device banner on for a driven run, so \"hand runs had the HUD and synthetic runs did not\" is ruled out rather than assumed harmless), --phases=key1,key2, --contact-seconds=N, --url=, --port=N, --device-id=, --no-serve, --no-hud. Writes real-screen.json to perf-profiles/",
"preperf:ios:webkit:commit": "npm run perf:build — so perf:ios:webkit:commit always scores a freshly instrumented bundle; a bundle built without PERF_MARKS has no engine.commit samples and the check refuses it. Skip the rebuild with: npm run perf:ios:webkit:commit --ignore-scripts",
"perf:ios:webkit:commit": "The per-release commit-latency contract check on a REAL USB-connected iPad (ADR-0173): serves the instrumented build on the LAN, attaches to Safari over the WebKit Inspector Protocol, and runs the finger-paced crayon session (tools/perf/probes/paced-crayon-session.js, the 2026-09-18 baseline payload unchanged) on a fresh /dev/engine load once per deposition arm, restamp then glaze-direct. Scores each arm's engine.commit P95 against COMMIT_GATE_MS, prints the iPadOS/Safari versions, build SHA and per-arm P95/max for the release notes, and exits non-zero on a breach or on any arm it cannot score (no engine.commit samples, a failed session, a missing arm). Flags: --url=, --port=N, --device-id=, --no-serve. Writes commit-contract.json to perf-profiles/",
"preperf:ios:xcuitest:screen": "npm run perf:build — so perf:ios:xcuitest:screen measures a freshly instrumented bundle. Skip the rebuild with: npm run perf:ios:xcuitest:screen --ignore-scripts",
"perf:ios:xcuitest:screen": "Automated trusted-touch real-screen + undo capture through Appium: opens mobile Safari/Chrome or attaches to a native Capacitor WebView, evicts stale caches, injects the real-screen probe, switches to native coordinates for gestures, then optionally drives Undo and scores engine + next-frame response. Local physical iPad setup uses --device-id= and ios/local.xcconfig; capability files and borrowed sessions support simulators, Android, and hosted providers. Flags: --url=, --port=N, --device-id=, --appium-url=, --xcode-config=, --wda-bundle-id=, --allow-provisioning, --capabilities-file=, --session-id=, --native-app, --native-webview-class=, --brush=pen|crayon|magic|eraser, --gesture-repeats=N, --repeat-pause-ms=N, --undo-count=N, --undo-pause-ms=N, --history-settle-ms=N, --refresh-regime=60hz|120hz (the frame regime this capture is judged against; resolved from the runtime when its targets declare exactly one, and an off-regime capture is REFUSED unless --report-only, because lostFrameTimeShare is a share of the beat), --orientation=PORTRAIT|LANDSCAPE, --theme=light|dark, --rotate-before-undo, --label=, --output=, --report-only, --no-serve. Native orientation preparation uses the persisted Settings rotation-lock control and restores the observed lock and orientation in cleanup. Writes real-screen.json to perf-profiles/; physical-iPad fidelity, drawing, and requested undo gates exit nonzero unless --report-only is set.",
"perf:ios:bundled:frames": "Drawing capture of the BUNDLED Capacitor WKWebView on a physical iPad (issue 1323): Appium arms/injects and, by default, drives trusted native touch; the complete probe report leaves the secure bundled origin through a dev-harness-only Capacitor Preferences mailbox, then devicectl pulls the Preferences plist from the app container. A random per-session nonce proves page identity, the pull requires exact report bytes and table counts, and the runtime UA must identify WKWebView. Requires an installed perf:build:cap build; no server.url, probe host, mixed-content HTTP, localStorage report copy, or new native plugin. Flags: --device-id= (required), --brush=, --gesture-repeats=, --orientation=, --theme=, --label=, --output=, --report-only; --hand-input --seconds=N (maximum 60) [--speak: say 'Draw now'/'Stop' on the Mac] opens an experimental real-finger control with WDA still attached, not a clean coalescing witness until a paired attached-vs-detached control clears that assumption. Writes to perf-profiles/.",
"preperf:ios:xcuitest:actions": "npm run perf:build — so perf:ios:xcuitest:actions measures a freshly instrumented production bundle. Skip the rebuild with: npm run perf:ios:xcuitest:actions --ignore-scripts",
"perf:ios:xcuitest:actions": "Automated discrete-interaction regression sweep through Appium on mobile Safari/Chrome or a native Capacitor WebView: repeats an idle-frame control plus drawer, palette, brush, stroke-width, responsive Settings, theme, coloring-grid open/scroll and page selection, screenshot, the AI waiting print through the dev seam, undo, the unavailable-action flash, drag-to-clear on a blank page and on a coloring page, and rotation actions; records the applicable action plan and product-surface context separately from measured summaries; records in-page action-to-first-frame, first-observed readiness, frame P95, and worst-frame timing; and exits nonzero above the 20 ms P95 / 33.5 ms first-frame and max-frame gates (a max breach counts only when two of the three scored repeats show it — ADR-0156) (per-action documented exceptions for this calibrated iOS capture live in IOS_ACTION_FRAME_P95_ALLOWANCES_MS in tools/perf/lib/action-stats.mjs and are recorded into each capture as gateAllowances; other targets stay on base gates). Local devices use --device-id= and ios/local.xcconfig; an Android native session adds --platform=android, which builds UiAutomator2 capabilities from the --device-id= serial; hosted endpoints use --appium-url= plus --capabilities-file=. Flags: --url=, --port=N, --device-id=, --platform=ios|android, --appium-url=, --xcode-config=, --wda-bundle-id=, --allow-provisioning, --capabilities-file=, --session-id=, --native-app, --native-webview-class=, --webdriver-clicks, --orientation=PORTRAIT|LANDSCAPE, --theme=light|dark, --actions=idle,drawer,palette,color-picker,brushes,stroke-width,settings,settings-sections,settings-controls,theme,coloring,screenshot,ai-waiting,undo,unavailable,clear,rotation, --repeats=N, --label=, --output=, --report-only, --no-serve. Rotation first frames on iPad Safari score N/A rather than gated (ADR-0142: resize lands in the same rendering turn as the first frame). Writes actions.json to perf-profiles/.",
"perf:web:actions": "Run the same discrete-action plan and frame scorer as perf:ios:xcuitest:actions against a production preview in Playwright on the local Mac. Defaults to WebKit at 1512x982@2x with one retained warmup and three scored repeats; records the applicable action plan and product-surface context separately from measured summaries; supports --engine=webkit|chromium|firefox, --url= for an externally served historical build, --viewport=WIDTHxHEIGHT, --device-scale-factor=N, --theme=light|dark, --headed, --actions=..., --repeats=N, --label=, --output=, --report-only, and --no-build. Desktop coloring-grid scroll uses a trusted wheel event over the dialog; CDP and Appium targets retain trusted touch. Writes actions.json to perf-profiles/ for direct current-vs-historical renderer comparisons.",
"perf:preflight": "Preflight a physical-device performance session: confirms an Android device is present, unlocked and held awake (it sleeps mid-session and takes the capture with it), confirms the iPad is reachable and reports the HARDWARE UDID Appium needs rather than the CoreDevice UUID `devicectl` prints, confirms the root-owned RemoteXPC tunnel is already running so its password prompt is not needed again, and resolves every capture port without stopping a listener another worktree owns. Preview ownership comes from the listener cwd, not its command line; foreign canonical preview/probe ports shift to free alternates, and a probe is reused only after its checkout, upstream, build, and run identity agree. Simultaneous empty adb/usbmuxd enumeration in a known sandbox reports USB access denial rather than two missing devices. Every other check is host-side unless you ask for more, which is why a device blocked by Guided Access can still report ready. Flags: --wake-android (wake it and set stay-awake — the only flag that writes to a device), --hold-android-awake (the same, re-asserted every 60s for the whole session, and reports when either device goes away), --verify-ios-launch (build and launch a real WebDriverAgent session, then tear it down — slow and needs the device to itself, and the only check that proves iOS will accept a capture; names the innermost cause, such as Guided Access, rather than the `xcodebuild failed with code 65` Appium reports), --verify-android-input (drive a real touch against the floor control and read the contact cadence back — the Android counterpart, and the check whose absence let a transport delivering 46.8 moves/s against a 100-170 band survive a whole campaign while every capture parsed), --json (machine-readable, for a session runner), --android-serial=, --ios-udid=, --appium-url=. Exits non-zero when anything is blocking.",
"perf:release": "Release the physical-device capture rig — the mirror image of perf:preflight. Stops every capture-rig process a checkout of this repo owns (previews, probe hosts, iPad secure-origin fronts, Appium, the hold-awake watcher, inspector proxies — from this worktree, any registered worktree, a pruned worktree under either runner's container, or a pruned one anywhere whose process or parent runs one of this repo's rig scripts), drains Appium's WebDriverAgent sessions before signalling it so nothing is stranded on the iPad, removes the selected phone's adb forwards to Chrome/WebView devtools sockets (another device's are left), and puts the phone back to stock (stay-awake off, 30 s screen timeout, dumpsys battery reset, auto-rotate on, wm fixed-to-user-rotation default) — so the phone WILL sleep and lock afterwards. Never stops a listener from another project, one with an unreadable cwd, or the root-owned RemoteXPC tunnel (prints the sudo command instead), and refuses while a campaign or operator driver is live. Flags: --dry-run (inventory and verdicts only), --host-only (leave the phone's state), --stop-campaigns (stop a live driver too — only for an abandoned campaign), --json, --android-serial= (required when several phones are attached — it refuses to guess). Exits non-zero when blocked, when a process survives SIGKILL, or when any adb step fails — a run that could not undo stay-awake has not released the phone.",
"perf:session:person": "The person-present session for epic 2210 (runbook: docs/scratchpad/perf/2026-09-23-epic-2210-person-session.md): walks every task that needs a human at the iPad, the rig phone, or a notched iPhone in the order that keeps the person there least — bring-up (preflight, own preview/probe host/Appium on free ports, reuse of a WebDriverAgent that answers or a direct launch with a spoken passcode cue), paired driven-vs-finger iPad Safari captures (pen, Magic x3 first loads), landscape eraser finger captures, bundled-app finger captures, the secure-origin fronts with an on-iPad constraint check, the phone overlay check, then unattended the issue-2229 portrait A/B (with a Reduce Motion arm) and the four iPad web action sweeps over HTTPS; a second visit updates the iPad, brings the rig up again on the new iPadOS, re-proves the constraint probe there (appending the verdict to the tracked perf-profiles/evidence/operator/ipad-constraint-probe.tsv that CONSTRAINT_PROVEN_RELEASES is tested against), repeats the paired pen and Magic driven-vs-finger controls, and runs perf:ios:webkit:commit, with an optional iPhone contentInset checklist. Before each capture it says what to do with a finger (spoken cues via say); after each it judges fidelity, regime, product commit, contact time and pointerdowns and prints PASS or REDO. Resumable from perf-profiles/person-session/<id>/state.json; drafts each issue comment for review and never posts. Stops only the processes it started. Bring-up proves WebDriverAgent by opening a session, not by /status, and before each iPad capture it re-proves WDA, sets the finger capture's orientation through WDA, and closes Safari ahead of a bundled-app capture. Flags: --plan, --check=overlay (the phone NU Navigation Bar overlay PASS/FAIL alone), --redo=<step>, --skip=<step>, --new, --session=<dir>, --wda-url=, --relaunch-wda (launch a fresh WDA over a runner that holds the iPad but cannot open sessions, ending it; run while watching the iPad for the passcode prompt), --teardown",
"perf:operator": "Guided session for the capture inputs only a HUMAN AT THE DEVICES can give, so the rest of a campaign runs unattended (issues 1275 and 1299). Three steps, each re-runnable: grant (launch one WebDriverAgent session while the operator watches the iPad — the passcode / Enable UI Automation prompt exists only during a launch, never at rest — appending every attempt to the TRACKED grant log at perf-profiles/evidence/operator/ipad-grant-log.tsv so the grant's unknown lifetime accrues data), android-hand and ios-hand (real-finger perf:device:hand captures inside each installed Capacitor WebView, written to a per-session directory under perf-profiles/split-capture/hand-native/). Devices, ports, and the Android wake come from perf:preflight's own resolution; the iPad app is launched deterministically via devicectl (never the foregrounded app, which once put a Safari capture under a WKWebView label); the capture tool refuses an artifact whose user agent contradicts the labelled runtime. Restarts an owned preview, reuses compatible probe-host and Appium servers, and deliberately leaves them running. Each capture is a child process, so one failed cell costs that cell. Flags: --plan (print the checklist and exit, offline-safe), --steps=grant,android-hand,ios-hand, --brushes=pen,crayon,magic,eraser, --orientations=PORTRAIT,LANDSCAPE, --theme=light|dark, --seconds=N, --probe-port=N, --android-serial=<serial> and --ios-udid=<udid> (read by the in-process perf:preflight, which otherwise picks each device itself)",
"perf:campaign": "Drive one deployment-target capture campaign to completion, resumably: expands --target into its orientation/theme modes x pen(+undo)/crayon/Magic/eraser/actions cells, runs each through the right transport (Appium real-screen for drawing, the ADR-0135 split input/measurement path for targets that declare it, Appium or direct CDP for actions per ADR-0092), and records every attempt in a ledger. Physical-device queues containing drawing cells repeat one same-build/same-mode crayon reference at the start, middle, and end; action-only queues add no drawing control. references.json beside instrument.json records their capture-session scope and lost-frame spread, and warns beyond the evidence-backed 0.5-percentage-point boundary (issue 1458). A cell whose artifact already parses is skipped, a failed cell retries up to --max-attempts, and a cell that exhausts them is recorded as a P1 while the queue continues — so an interrupted run resumes by rerunning the same command. Acceptance is a parseable artifact that matches the requested runtime AND passed its input-fidelity verdict, never the child exit code — so a valid red gate is kept rather than retried until green, while a capture that cannot be scored is recorded as failed-input-fidelity rather than counted complete. A split-transport target requires --probe-host= (this host's LAN address, as the DEVICE sees it); the campaign asserts that host answers before the queue starts rather than starting it. Host identity stays an input: --device-id, --capabilities-file, --url, --appium-url, --cdp-port, --probe-host, --wda-url. Flags: --target=, --modes=, --items=, --output-root=, --ledger=, --max-attempts=N, --label=, --probe-host=, --wda-url=, --reboot-simulator=UDID (reboot between attempts; the iPad Simulator degrades across successive capture sessions), --dry-run. Writes beneath --output-root and a ledger.tsv beside it.",
"perf:campaign:sources": "Fold a finished perf:campaign output tree into the performance-matrix manifest: derives each cell's evidence path from the same campaign plan that wrote it, normally accepts a mode only when all four brushes plus the action sweep are present and accepted by the campaign runner's own cellInspection (the check perf:campaign:status reports from, so a blocked-coverage sweep or an unscoreable drawing counts as missing), and rewrites those modes in --manifest (or prints them when it is omitted). --sections=drawing,undo,actions (any subset) folds only the named sections, each needing only its own cells and written with this --product-commit and its own capturedOn date, while the mode's other sections keep their published route, commit, and date (issue 2268). When the action transport is blocked, --actions-unavailable=REASON replaces the action section with that reason, while --preserve-actions marks the action section \"preserved\" so the generator copies the published section from preservedEvidence.from instead of re-scoring raw sweep pointers or re-dating them to the drawing recapture (the manifest must declare preservedEvidence); neither relaxes four-brush completeness, neither combines with --sections, and preservation refuses to discard a usable fresh action artifact. Flags: --target=, --output-root=, --product-commit=, --modes=, --sections=, --manifest=, --actions-unavailable=, --preserve-actions",
"gen:performance-matrix": "Rebuild the committed deployment-target report from its source manifest, then run the section-age report IN PROCESS against the manifest this run resolved. Chained in-process rather than as a shell `&&`: npm appends forwarded arguments to the end of a compound command, so passing a manifest handed it to the checker while the generator wrote the default one — and the command exited 0 having verified a different file than it produced. Takes the manifest as a positional argument. The age report exits 0 by default — every section is behind the tip between campaigns, and its age is what a reader needs (ADR-0175); pass --strict at a campaign's regenerate to assert provenance-complete, and a section missing its capturedOn date or a resolvable product commit then fails it.",
"gen:flaky-digest": "Harvest flaky.json records from the Tests workflow's Playwright report artifacts into a rolling history (starting from the latest flaky-digest-history artifact, or -- --fresh) and rank masked flakes per test with run/attempt/branch/shard provenance and named coverage gaps, written to test-results/flaky-digest/. Needs GITHUB_TOKEN, GH_TOKEN, or gh auth",
"gen:code-map": "Regenerate the tables in docs/CODE-MAP.md (lines of code per area and web/src domain) from one commit's tree, read from git so a ref always regenerates identically; the prose around them belongs to the reconcile-code-map skill. Flags: --ref <rev> (default HEAD), --assignments (print every tracked path's placement and line count as TSV instead of writing)",
"gen:crayon-glaze-sheet": "Render a proof sheet of the native crayon glaze (ADR-0148) across candidate perOpGlazeReturn values: one row per value, one column per number of times the crossing colour is redrawn, so the two things that value trades off - how green a FIRST crossing reads and how fast redrawing walks it toward the new colour - are visible together. Drives the real pipeline through /dev/engine in desktop WebKit, which reproduces the device colour but NOT its frame cost; never read performance off it. Flags: --returns=a,b,c, --passes=n,n, --engine=webkit|chromium, --port=N, --out-dir=. Writes a self-contained HTML sheet.",
"perf:centerline-tracing": "Run the opt-in centerline tracer's full synthetic corpus and committed-input benchmark through the locked uv environment; refreshes numeric records for human regression review",
"perf:web:undo": "Focused tiled-history profile (ADR-0085/ADR-0086): builds the production bundle with PERF_MARKS=true, serves it via vite preview with the /dev/engine harness unlocked, and drives seven deliberately-shaped sessions (long squiggles; short dot/dash marks; a mix; five-finger drags; pen back-and-forth scribbles; crayon squiggles; crayon scribbles with mid-stroke pass splits) in headless Chromium under 4x CPU throttle. Records per scenario — via getUndoDebug() — undo depth, live patch and folded-base rasters, retained command count, direct raster bytes, and engine.draw/commit/undo timing. Writes trace.json/summary.json/report.md + undo-scenarios.json/.md to perf-profiles/. Flags: --engine=chromium|webkit, --throttle=N, --no-throttle, --no-build, --scenarios=key1,key2 (subset)",
"perf:web:undo:webkit": "The full seven-scenario WebKit gate. Enforces the 25 ms commit-P95 budget, recomputes ideal fast-set membership from the rolling full-run history, records whether the fast tier would catch each breach, and fails on membership drift or two consecutive misses. Pass --fast-set-history=<path> to update a restored durable history in place; the run always writes undo-fast-set-history.json beside its reports",
"perf:web:undo:webkit:fast": "Post-merge WebKit commit gate (ADR-0100's 2026-08-11 amendment) over the single exported fast-set constant: multi-finger is the sole multi-pointer exerciser and crayon-scribbles is the sole mid-stroke pass-split exerciser. Runs on pushes to main, not on pull requests — its millisecond P95 needs WebKit and a quiet host, and it was the wall-clock floor of a PR run. CI consumes this script by name; it exits non-zero on a commit-gate breach, a skipped scenario, or missing engine.commit samples",
"perf:web:replay": "Replay a REAL finger-input recording (captured on-device with tools/perf/probes/input-recorder.js) through the engine under the profiler, so the harness measures actual human strokes instead of synthetic squiggles: opens /dev/engine, sizes the canvas to the recorded device, replays the pointer stream + UI actions at recorded timing (real frame pacing), captures a CDP trace + engine marks, and reports how your input landed in tiled history (peak/end undo depth, retained commands, patch/base rasters and bytes via getUndoDebug, plus engine.draw/commit/undo hot-path rows). npm run perf:web:replay -- --recording=<file.json> [--turbo] [--throttle=N] [--no-build]. Writes trace/report + replay-summary to perf-profiles/",
"perf:build": "Production web build instrumented for profiling: PERF_MARKS=true bakes in the engine.* performance.mark/measure calls and keeps function names through minification (web/vite.config.ts). Runs automatically as preperf:serve; run it directly to refresh the bundle a native or manual flow will serve",
"postperf:build": "Stamp web/build with the commit it was built from (.perf-build-provenance.json, plus a dirty-tree flag) so capture artifacts record build-time provenance instead of guessing from capture-time HEAD",
"preperf:ios:webkit:gates": "npm run perf:build — so perf:ios:webkit:gates always drives a freshly instrumented bundle rather than whatever build happens to be on disk. Skip the rebuild with: npm run perf:ios:webkit:gates --ignore-scripts",
"perf:ios:webkit:gates": "Autonomous ADR-0066 gates run on a REAL USB-connected iPad (local-only): serves the instrumented build on the LAN, attaches to Safari over the WebKit Inspector Protocol via ios_webkit_debug_proxy (brew install ios-webkit-debug-proxy — Safari has no CDP on a device), navigates the tab to /dev/engine so the bundle is never stale, injects tools/perf/probes/engine-gates.js, and reads back the table plus the driver's warnings. This is the automated form of Approach A in docs/PROFILING-IPAD.md; the Timeline run there stays manual. Needs the iPad unlocked, trusting this Mac, on the same Wi-Fi, with Safari open on a tab and Web Inspector enabled. Flags: --scenarios=key1,key2, --strokes=N, --ops=N, --url=, --port=N, --device-id=, --no-serve. Writes ipad-gates.json to perf-profiles/",
"preperf:serve": "npm run perf:build — so perf:serve always hands the iPad a freshly instrumented bundle rather than whatever build happens to be on disk. Skip the rebuild with: npm run perf:serve --ignore-scripts",
"perf:ios:secure-origin": "A trusted HTTPS origin for iPad Safari captures, which a LAN http:// page is not (the AI-waiting actions need crypto.randomUUID). `make-ca --host=<mac>.local --ip=<lan>` creates a root name-constrained to this Mac plus a server leaf and a constraint probe under ~/.splotch-rig/secure-origin-ca/, and checks both against the macOS trust engine; `serve --listen=<LAN address>:<port> --upstream=<perf:serve port>` is an HTTPS front that forwards only GET/HEAD for the page, /_app/* and build files and refuses /api, /admin and /dev (bind the LAN address, never 0.0.0.0). `check --url=https://<mac>.local:<port>/ --device-id=<udid>` replaces the on-iPad constraint look for an unattended session: it refuses unless the device runs an OS release CONSTRAINT_PROVEN_RELEASES lists, macOS trust accepts the leaf and refuses the constraint probe, and the front answers 200 for the page and 403 for /api. Install, removal, and the local permission rules an unattended start needs: docs/PROFILING-IPAD.md",
"perf:serve": "Serve the current instrumented production build on the LAN (0.0.0.0:4173) with the /dev/* routes unlocked. PUBLIC_ENABLE_DEV_HARNESS=true is a runtime server-route gate and a compile-time client-seam flag, so preperf:serve sets it while building and perf:serve sets it while serving. A real iPad/phone can then record input or drive /dev/engine. The wrapper prints only reachable Network + Harness URLs, filtering dead link-local interfaces. Stop it before perf:web:replay (same port). See docs/PROFILING-IPAD.md",
"perf:analyze:chrome": "Re-run the trace analyzer over a saved profile dir or trace.json (regenerates summary.json + report.md without re-capturing — the path a native-exported trace takes): npm run perf:analyze:chrome -- perf-profiles/<dir>",
"perf:analyze:web-inspector": "Analyze a Safari Web Inspector Timeline export from a REAL iOS device (a different format from perf:analyze:chrome — {recording:{records,markers}}, mark-only, ring-buffered, ~1ms clock). Recovers per-op main-thread cost (undo/snapshot/fold/commit/resize/draw) from the enclosing timeline record — engine.undo pairs an explicit end mark so async deep undos attribute fully — plus paint/composite and frame stats: npm run perf:analyze:web-inspector -- <export>.json. See docs/PROFILING-IPAD.md",
"perf:analyze:frames": "Re-read a saved real-screen capture (perf-profiles/<dir>/real-screen.json from perf:ios:webkit:frames) without re-drawing it: recomputes every percentile, verdict, suppression comparison and the worst-frame forensics from the raw tables the probe recorded, and writes summaries.json beside it. The probe deliberately records and computes nothing, so a capture outlives the metric definitions that were current when it was taken — this is how a definition change is checked against real device data. Flags: --no-forensics",
"perf:web:frames": "The real-screen probe WITHOUT an iPad: drives the same probe against / in a local browser at iPad Pro 12.9 geometry (1366x915 @2x, so the 4.7 Mpx canvas and full-paper blend are the same size) and reports the same tables. --engine=webkit (default) is the engine family the iOS app ships; --engine=chromium adds --throttle=N via CDP but is a different compositor rendering through SwiftShader; --engine=firefox is Gecko, which no deployment target ships, and belongs to the desktop matrix row rather than to any conclusion about WebKit or Blink. The device findings are compositor-side, so a stall that reproduces here is a cheap regression signal, while one that does NOT reproduce says nothing about the device — confirm with perf:ios:webkit:frames. --undo-count=N drives the shared undo-response measurement after the phase sweep, so a Mac undo result carries the same orientation and theme metadata as its drawing capture. Flags: --engine=webkit|chromium|firefox, --url= for an externally served historical build, --brush=pen|crayon|magic|eraser, --throttle=N, --phases=, --contact-seconds=N, --drive=long|short|mixed, --drive-hz=N, --theme=light|dark, --viewport=WIDTHxHEIGHT, --device-scale-factor=N, --label=, --output=, --port=N, --undo-count=N, --undo-pause-ms=N, --headed, --no-serve, --no-forensics",
"perf:device:serve": "Serve the perf build with the real-screen probe bootstrapped into the page, and collect the report the page uploads (ADR-0135). Binds 0.0.0.0 because a physical device loads it over the LAN. Everything but the drawing route's HTML is proxied to the real preview server, so the bundle and headers under test are the ones perf:serve hands out; the HTML gains exactly one same-origin <script src>, which the route's CSP already allows. Pair it with perf:device:frames. Flags: --port=N, --upstream=, --report-dir=",
"perf:device:frames": "Drive one drawing capture on a physical device with input and measurement on SEPARATE channels (ADR-0135): trusted touch via `adb shell input` on Android or WebDriverAgent W3C actions on iPadOS, and the measurement uploaded over HTTP by the page itself. Use this instead of perf:ios:xcuitest:screen for Android, where the Appium browser transport delivers 46.8 contact moves/s against a 100-170 fidelity band and produces cells that cannot be scored. Requires perf:device:serve running first, and --host must be the probe host as the DEVICE sees it (a LAN address); Android Chrome instead loads a --host on this machine at localhost through `adb reverse`, which Chrome's Always use secure connections setting exempts, and the page blocks service-worker registration. Records orientation, theme, the service-worker guard state, and the fidelity verdict in the artifact, and exits non-zero when fidelity fails. On the Android browser path it clears this tooling's own leftover tabs (tool-signature pages on the session host — ?probe=/?verify= params or the stand-down path — never operator tabs, other apps' pages, or bare about:blank) and ACTIVATES the run page over the devtools HTTP endpoint, after launch and before dispatch: session restore could front a stale tab while the run page loaded in the background, receiving none of the injected touch (issue 1294), and activation alone loses that race. A dispatch whose page pulsed zero input events fails naming that cause rather than waiting out the report timeout. Records dispatchedStrokes (one per adb swipe) and exits non-zero, naming both counts, when the page's trusted pointerdowns differ from it: an overlay that drops touches leaves the arriving strokes passing fidelity (issue 2229). It also records strokeLanding, each swipe's planned start beside the pointerdown the page recorded, and exits non-zero when any landed more than a CSS pixel off: a wrong dispatch origin moves every stroke while every count passes (issue 2271). --reduce-motion=reduce|system seeds the product's Reduce Motion preference through the probe plan (one reload) and records requestedReduceMotion beside the page-resolved observedReducedMotion; a reduce seed the page does not resolve is refused. Flags: --platform=android|ios, --device-serial=, --cdp-port=N, --wda-url=, --host=, --brush=pen|crayon|magic|eraser, --orientation=PORTRAIT|LANDSCAPE, --theme=light|dark, --gesture-repeats=N, --reduce-motion=reduce|system, --allow-foreign-build, --label=, --output=",
"perf:device:hand": "Record what a REAL HUMAN FINGER reports in one capture runtime, so an input-fidelity threshold is set from a measurement of that runtime instead of another platform's numbers (issue 1218). Same split-capture path as perf:device:frames with the injection half removed: a person draws in place of `adb shell input`, and the page instruments and uploads itself exactly as it does for a driven run -- the symmetry is what makes the hand number a calibration for the driven one. The artifact keeps the probe's RAW event rows, so a later revision of the fidelity table re-reads the file rather than asking for another finger. The verdict is printed but never fails the run: an uncalibrated check not passing is the reason the capture exists. Requires perf:device:serve running first, and --host must be the probe host as the DEVICE sees it; with --open=adb, Android Chrome loads a --host on this machine at localhost through `adb reverse`. Flags: --platform=android|ios, --open=adb|devicectl|safari|manual (safari opens iPad Safari at the run's nonce URL through devicectl --payload-url, keeping page identity without typing; manual prints the URL for a human to open, which is how a runtime with no automation reachable to it gets calibrated), --device-serial=, --device-udid=, --native-app, --speak (say 'Draw now', the last seconds, and 'Stop' on the Mac, for a person holding an iPad the host cannot buzz), --host=, --brush=pen|crayon|magic|eraser, --orientation=PORTRAIT|LANDSCAPE, --theme=light|dark, --seconds=N, --label=, --output=",
"perf:device:floor": "Serve the FLOOR CONTROL: one full-size canvas, one stroke() per pointermove, no tiles, no blend planes, no framework — the cheapest drawing app that could exist, measured by the same probe and the same maths as Splotch. Answers how much of a browser's in-contact frame loss is the browser rather than the app; ADR-0136 used it to show a physical-iPad capture scoring 1.46% lost frame time had zero presentation deficit, which is why the credited charge exists. A DIAGNOSTIC, never a gate: its own single ~3 Mpx canvas is the architecture ADR-0085 rejected, so a gate derived from it would enshrine a bad implementation. Drive it with perf:device:frames --host=<floor host>, or draw on it by hand with perf:device:hand --host=<floor host> (browser only, --brush=pen, --theme=light, no undo): either capture recognises the floor and proves its served bytes are this checkout's floor in place of the SvelteKit build guard, and the artifact records page floor-control with no productCommit, so the matrix fold refuses it. Flags: --port=N, --report-dir=",
"perf:device:verify-android": "Prove synthesized touch reaches an Android page at usable fidelity BEFORE a campaign spends hours on captures that cannot be scored. The counterpart to the iOS launch probe, and the check whose absence let the Appium transport's 46.8 contact moves/s survive a whole campaign while every capture parsed cleanly. Drives the floor control rather than the app, so the answer is about the input path alone and needs no product build. Before and after readiness it clears the capture tooling's own leftover Chrome tabs (tool-signature pages on the session host — never operator tabs) and fronts the verify page, best-effort over --cdp-port, because a stale tool tab holding the restored foreground once made this verifier report zero input on a healthy rig. Cadence decides the verdict; pressure and contact geometry are reported but do not, because their thresholds were calibrated from a hand capture on the iPad and Chrome cannot produce them. Chrome loads the floor control at localhost through `adb reverse` (removed on exit), which Chrome's Always use secure connections setting exempts. Exits non-zero when input is unusable. Flags: --device-serial=, --port=N, --cdp-port=N, --gesture-repeats=N",
"perf:device:verify-android-rotation": "Prove an Android device rotates AND that a loaded page follows it, before a campaign spends its landscape half producing nothing. The preflight's touch check never rotates the device, so a rotation fault passes every cheap check and then fails all eight landscape drawing cells — which is what happened on 2026-08-23 on a fully green rig. Drives the same stop-rotate-launch order a capture drives, and reads the orientation the PAGE reports rather than the one requested: the page mismatch is what was observed, while the mechanism behind it is unexplained — the force-stop reset it was attributed to did not reproduce in 8 later trials, so the ordering is cheap insurance rather than a fix for a known cause. Writes rotation settings and restores what it found. Loads the floor control at localhost through `adb reverse`, like perf:device:verify-android. Chained into perf:preflight --verify-android-input. Flags: --device-serial=, --port=N",
"perf:device:probe-overhead": "Measure what the in-page probe costs the page it measures. A/Bs the same page and the same OS-driven gesture with and without the probe injected, both arms served from ONE origin so the persisted brush carries between them, and both carrying an identical minimal requestAnimationFrame counter as the common clock — the probe cannot score the arm that has no probe, and on this phone neither dumpsys gfxinfo (zero frames for Chrome) nor SurfaceFlinger --latency (all-zero rows on Android 16) can. The brush is requested, committed and VERIFIED before any sample, because the control arm runs first and would otherwise draw with whatever the previous page persisted. Arms are interleaved so device warming cannot be mistaken for the probe, and any sample whose page reported an error fails the run rather than joining the mean. Chrome loads the A/B host at localhost through `adb reverse`, and both arms block service-worker registration. Needs perf:serve and perf:device:serve running. Flags: --device-serial=, --brush=pen|crayon|magic|eraser, --port=N, --upstream=, --probe-host=, --samples=N",
"test:unit": "Unit tests, one-shot (Vitest, happy-dom), then the web-build SSR guards (vitest.webSsr.config.ts)",
"test:unit:coverage": "Unit tests with V8 coverage over the web/src production TypeScript set and the measured coverage ratchet enforced, then the web-build SSR guards",
"test:unit:watch": "Vitest watch mode",
"test:browserless": "Run CI's Browserless tests job locally, in its order — coverage-gated app unit, asset-pipeline, store-drawing, and repo-script Vitest tiers, then the API contract smoke (SMOKE_PORT picks its port). No Playwright, unlike npm test. Unlike CI it keeps going after a failure, so one run surfaces every problem. tools/ci-mirror/tests/run-browserless-tests.test.mjs fails if this list drifts from the workflow",
"test:asset-gen": "Asset-gen pipeline tests (Vitest, Node env): image-analysis helpers against committed fixtures plus mocked generator CLI workflows — no Gemini/network",
"test:store-drawings": "Store-drawing pipeline tests (Vitest, Node env, tools/store-drawings/tests/): SVG subset parsing, static instruction generation, scene fitting, and generated-module drift",
"test:centerline-tracing": "Opt-in locked Python and Node bridge checks for tools/centerline-tracing; deliberately excluded from aggregate npm test",
"test:tools": "Repo-automation script tests (Vitest, Node env, tools/tests/): audit-burndown state/runner seams, complete Ruler skill-fork replacement, the Android emulator API-level drift invariants (local literals match CURRENT_ANDROID_API_LEVEL and the deploy matrix adds the declared floor), the throwaway-server env guard (both test servers must declare every private env var web/src reads instead of inheriting it from web/.env), and one live case that boots a real vite dev server to prove a released one outlives its caller",
"test:e2e": "Playwright E2E tests, headless",
"test:e2e:ui": "Playwright UI mode",
"test:e2e:headed": "Playwright with browser visible, slowed down (SLOWMO=500)",
"test:e2e:debug": "Playwright inspector",
"test:e2e:install": "Install the Playwright browsers (one-time)",
"test:e2e:sweep": "Repeat the E2E suite with retries disabled and one fresh Playwright-owned preview server per repetition, so rate-limit state cannot contaminate flake discovery. Required: --workers=N; optional: --reps=N (default 5), --grep=PATTERN, --out=DIR, --prebuilt",
"test:android": "Android smoke test, self-contained: boots a headless emulator, builds + installs, runs Maestro, tears down",
"test:android:device": "Maestro smoke test against an already-running emulator/device (what CI uses; same flow works on a booted iOS simulator)",
"test:ios": "iOS smoke test, self-contained: boots a simulator, builds + installs, runs Maestro, tears down (macOS-only)",
"test:api:smoke": "API contract smoke test, self-contained: boots a throwaway vite dev with test env, exercises the CORS/preflight contract + the admin auth flow + a public oracle against the documented /api/* shapes, tears down (no Gemini/Blobs needed)",
"test:deploy:smoke": "Hosted deploy contract: static routes, headers, version freshness, native CORS, safe API failures, and persistent Blobs (read-only on production; preview adds a write round-trip) (DEPLOY_SMOKE_URL + ADMIN_ACCESS_TOKEN)",
"test:blobs:smoke": "Deploy smoke test (ADR-0025): against a real Netlify deploy (BLOBS_SMOKE_URL + ADMIN_ACCESS_TOKEN), asserts persistent:true without mutating production; previews also round-trip and clean up a token",
"test:driver:smoke": "App-driver smoke test: boots the app and drives tools/app-driver/lib/app-driver.mjs (openAppPage + expandDrawer + palette/hex-grid colors + pickBrush + setStrokeSize + drawStroke) against current markup, so the uncovered gen:* drivers can't rot silently on selector/import changes",
"test:sweep:smoke": "Worker-sweep harness smoke test: one grepped rep through tools/e2e-tuning/run-worker-sweep.mjs, exiting nonzero unless it actually executed tests, so a Playwright-config change can't silently break the re-tune harness again (issue 1044). --prebuilt assumes the preview bundle was just built (CI runs it right after the e2e shard)",
"test:lighthouse:ci": "Build the production web app and run the committed page-load gate: median of three simulated Slow 4G + 4x CPU Lighthouse runs for phone portrait/tablet landscape x first/repeat visit; FCP/LCP gate against the current baseline while TBT/score report only (-- --port=N --samples=odd>=3 --no-build --report-only --out=DIR --baseline=FILE)",
"test:firefox:smoke": "Run the critical-path engine smoke subset in Firefox (needs the Firefox browser)",
"test:webkit:smoke": "Run just the WebKit critical-path smoke subset (needs the WebKit browser)",
"fetch:image-reports": "Read-only production AI-report fetch: resolve splotch.art through the authenticated Netlify CLI, snapshot complete ai-image-reports bundles under .eval-tmp/ai-image-reports/<timestamp>, manifest per-report failures without blocking other bundles, and first delete local snapshot folders plus report__ model-eval inputs and the images and result rows made from them past the AI-report retention window. Pass -- --import-eval-inputs to deliberately copy PNG drawings into the gitignored model-eval corpus. Makes no Gemini calls and never modifies production",
"redteam": "MANUAL AI-safety red-team (ADR-0023): boots a throwaway vite dev, decrypts the corpus, flattens each safe-*/block-* drawing onto the app paper the way the real canvas export does, sends it to a real model call, and writes an HTML input->output report to tools/redteam/output/ for human review (opens in your browser). Pass ids to run a subset (npm run redteam -- block-gun) to iterate on one drawing. Real OPENAI_API_KEY + REDTEAM_FIXTURE_KEY required; excluded from npm test by design",
"redteam:encrypt": "Encrypt tools/redteam/source/*.png -> tools/redteam/encrypted/*.enc (commit the .enc); needs REDTEAM_FIXTURE_KEY",
"redteam:decrypt": "Decrypt tools/redteam/encrypted/*.enc -> tools/redteam/decrypted/ (gitignored); needs REDTEAM_FIXTURE_KEY",
"model-eval": "MANUAL paid image-model bake-off: compares selected model/quality variants over tools/model-eval/inputs/ using the production prompt and safety orchestrator, with a quality/cost/latency report under tools/model-eval/output/<runId>/report/. Requires keys for selected providers (OPENAI_API_KEY or GEMINI_API_KEY); excluded from npm test. Env: VARIANTS (exact key/model first, otherwise substring; comma-separated exact keys), PER_CATEGORY, SAMPLES, FILTER, CONCURRENCY, OUT_TAG, SKIP_REPORT; RESUME=<run dir> keeps completed images only when recorded settings match; REPORT_FROM=<run dir> (+ VERDICT_FILE) rebuilds without API calls",
"model-eval:fixtures": "Regenerate tools/model-eval/inputs/ — the deterministic fixtures plus every committed sample in samples/. Env: FILTER, DEBUG_SAMPLE. Rewrites only the inputs it produces itself and reports the rest, so paid output from model-eval:gen-inputs and captures from model-eval:gen-crayon survive until they are promoted into samples/",
"model-eval:gen-inputs": "Author canvas-plausible input drawings for the eval corpus (line/gen/scribble/mess categories) with gpt-image-2, or AUTHOR=gemini; ONLY=<substr[,substr]> selects a subset. Needs the chosen author's API key",
"model-eval:gen-crayon": "Capture crayon-brush eval inputs by replaying the authored store scenes in the live app with the crayon selected and screenshotting the drawing surface. No API key and no paid call. Env: SCENES, BRUSH, SPLOTCH_E2E_PORT",
"model-eval:adherence": "MANUAL prompt-adherence lab: holds gpt-image-2 low fixed and compares generate-image prompt variants (and image-tool knobs) by how faithfully outputs keep the child's composition, scored by tools/model-eval/lib/composition-score.mjs, writing a promotable report/ per run. Real OPENAI_API_KEY required; excluded from npm test. Env: LABS (exact keys), INPUTS/FILTER, SAMPLES, CONCURRENCY, OUT_TAG; REPORT_FROM=<run dir> rebuilds report/ with no API calls",
"gen:sound-effect": "Generate one or a restart-safe JSON batch of sound effects through ElevenLabs' sound-generation-only API (-- --text ... --duration N --out FILE, or --input FILE --out-dir DIR). Needs ELEVENLABS_API_KEY; --dry-run makes no API call",
"gen:pencil-sounds": "Re-encode the stereo pencil masters in tools/sounds/masters into the mono clips web/static/sounds serves, verifying each is mono with the master's sample rate and gapless loop length; prints bytes and decoded AudioBuffer size per clip. Needs the lame CLI on PATH",
"vectorize": "Trace a bitmap to SVG/PDF/EPS/DXF/PNG through Vectorizer.AI (-- <input> --out file …). Defaults to the free watermarked test mode; --production spends 1 metered credit. Needs VECTORIZER_ID + VECTORIZER_SECRET — see tools/vectorize/README.md",
"vectorize:coloring": "Plan or run restart-safe, paid light/dark coloring-outline SVG batches through Vectorizer.AI (-- --theme=dark --book=creatures --batch-size=12 --production); existing raw responses resume without another trace, --force explicitly re-traces stale derivations, and --write-ledger records source/output hashes",
"vectorize:coloring:analyze": "Rasterize generated light/dark SVGs against their staged authoring sources, fail below the campaign fidelity thresholds, and report raw/gzip SVG bytes versus those WebPs (-- --theme=dark --book=creatures --match=cover)",
"vectorize:coloring:check": "CI drift guard for every committed coloring-outline derivation ledger: source and light/dark SVG hashes and byte counts must remain exact",
"vectorize:postprocess": "Optimize Vectorizer.AI SVG output and restore width/height from viewBox (-- input.svg --out output.svg; no input normalizes committed vector art in place)",
"vectorize:postprocess:check": "CI drift guard for committed Vectorizer.AI SVGs: require byte-stable SVGO output and width/height matching viewBox",
"gen:icon-names": "Generate src/lib/components/icon-names.d.ts (typed IconName union) from the SVGs in src/lib/icons/ and src/lib/icons/deferred/ (auto-runs before build)",
"gen:icon-viewbox": "Rebase src/lib/icons/**/*.svg onto the canonical square viewBox 0 0 1000 1000 by baking the transform into the coordinate data, pixel-verified per file (iconViewBox.test.ts enforces the grid; run optimize:svg-assets after). Optional: -- <icon-name> ...",
"gen:releases": "Generate in-app/current/full release notes + fastlane store changelogs from releases/*.md (auto-runs before build)",
"gen:coloring-pack-snapshot": "Record a release tag's native coloring-pack download inventory (regenerated with that tag's own manifest generator and static tree) into tools/release/coloring-pack-snapshots/<version>.json; run once per release after tagging (-- --ref v1.7.0)",
"gen:tokens": "Generate web/src/tokens.css (the design-token custom properties, light + dark) from the source of truth in web/src/lib/design/tokens.ts (ADR-0071)",
"gen:tokens:check": "CI drift guard for gen:tokens: regenerate and fail if the committed tokens.css differs — fix by running gen:tokens and committing (never edit tokens.css directly)",
"gen:share-cards": "Generate the four standalone-page link-preview PNGs and content hashes from the live Svelte share-card harness",
"gen:promotional-image": "Regenerate web/static/large-image.png by replaying tools/marketing-assets/assets/promotional-image.svg onto the live canvas (Playwright)",
"gen:centerlines": "Trace an explicit filled SVG file/directory into an explicit centerline SVG file/directory through the isolated locked uv project; requires --input and --output",
"gen:store-drawings": "Compile tools/store-drawings/samples/*.svg into named, static pointer-instruction functions in tools/store-drawings/generated/store-drawings.mjs; the generated runtime has no SVG dependency",
"gen:store-drawings:check": "Fail when tools/store-drawings/generated/store-drawings.mjs differs from a fresh SVG-to-pointer-instructions conversion",
"gen:store-drawings:evaluate": "Draw the static store scenes through real Playwright pointer movements and compare SVG→points geometry plus points→app runtime masks under screenshots/store-drawing-eval/",
"gen:store-drawings:review": "Capture review-only Google Play phone/tablet store shots from the same static pointer instructions with Pen, Crayon, and Magic under screenshots/store-drawing-review/; never changes store-assets/",
"gen:style-covers": "Regenerate the AI style thumbnails in web/static/styles/ from tools/asset-gen/style-covers/source.svg via Gemini (needs GEMINI_API_KEY; -- --style X --temperature N for one style / different takes)",
"gen:coloring-chalk": "Generate a page or cover's CHALK line art from its pen via Gemini — --apply stages an uncommitted dark SVG trace source; solid-white eye whites/catchlights, gated on registration + regional chalk-vs-pen ink checks; candidates land in .coloring-samples-dark/chalk/ (needs GEMINI_API_KEY; -- page-or-category… or book/cover [--apply] [--max-attempts N] [-t F] [--notes \"…\"] [--force]; --rescore re-gates saved candidates offline)",
"gen:coloring-fills": "Generate gated flat-color LIGHT fill candidates via Gemini into .coloring-samples/; --rescore [--sample N] --apply ships the selected reviewed bytes only when every requested page passes outline keep, local warp, white, and eye gates, otherwise committed raw/punched assets stay unchanged (model runs need GEMINI_API_KEY; -- page-or-category [--rescore] [--sample N] [--apply] [--samples N] [--temperature N] [--warp-max N] [--notes TEXT])",
"check:coloring-fill-drift": "Audit both themes' committed raw fills (tools/asset-gen/fill-src/) for registration vs their line art: light global/worst-tile keep plus per-tile local warp with residual global shift reported separately; flags any that need regenerating. No API key/network (-- page-or-category, --warp-max N, --overlay to dump light drift maps)",
"check:coloring-fill-eyes": "Audit the committed fills' EYES — gates ringed light pupils on raw-fill contrast (source-solid pupils are owned by the pen overlay and report n/a), plus night contrast and a final-composite `orb` check for blank white pupils. Deterministic, no API key/network (-- page-or-category… to filter)",
"gen:coloring-eye-review": "Build a self-contained HTML review of every BLANK-ORB-flagged night eye (the `orb` column of check:coloring-fill-eyes) — whole-page composite + zoomed eye crops with median/white — to publish with the Artifact tool and triage real defects vs over-flags before burning them down. Deterministic, no API key/network (-- page-or-category…, --out FILE)",
"check:coloring-invented-shapes": "Audit the committed raw fills for INVENTED COLORED SHAPES floating on the open background (extra stars/flowers no drift/white/eye gate can see; anchoring to source lines or the border is the discriminator). Deterministic, no API key/network (-- page-or-category…[.light|.night], --verbose, --overlay to dump detection maps)",
"check:coloring-night-halo": "Audit every shipped night fill for RESIDUAL DARK HALO around the chalk strokes after the punch: normalized haloScore shows the automatic new-candidate bar, while rawScore separately flags human crop review. Deterministic, no API key/network (-- page-or-category… to filter, --out FILE for per-page JSON to diff across punch changes)",
"check:coloring-outline-quality": "Audit shipped line art for solid black regions, over-ringed eyes, and page frames (continuous four-sided ink or page-spanning near-white ghost lines). Deterministic, no API key/network (-- page-or-category… to filter)",
"gen:coloring-outlines:fresh": "Author a BRAND-NEW pen outline for a page from a text scene description (no conditioning on the old drawing) via Gemini, gated on solidity/ring-depth/eye-cores/outer whitespace/page frames/ink; candidates land in .coloring-samples/fresh/ (needs GEMINI_API_KEY; -- page --scene \"…\" [--eyes] [--apply] [--max-attempts N] [-t F] [--notes \"…\"])",
"gen:coloring-outlines:normalize": "Redraw an outline's solid black regions as thin outlined strokes via Gemini, gated on solidity + outline registration; candidates land in .coloring-samples-dark/normalize/ (needs GEMINI_API_KEY; -- page… [--apply] [--max-attempts N] [-t F] [--notes \"…\"])",
"update:coloring-golden-scores": "Freeze every offline audit score for the whole coloring catalog (outline solidity/eye rings/page frames, light keep/localKeep + eyes, both themes' local warp/residual shift, night drift/bgLuma/lineWhite + eyes/blank-orbs) into tools/asset-gen/golden/golden-scores.json — the committed regression baseline. Deterministic, no API key/network (~2 min)",
"check:coloring-golden-scores": "Re-run every offline audit and diff against golden/golden-scores.json: verdict flips (including composite blank-orbs) and bad-direction score movements = regressions (exit 1); improvements/moves reported. Run after any pipeline or asset change; update the baseline to adopt intended changes. No API key/network (~1 min)",
"gen:coloring-punched-fills": "Re-derive the shipped fills-only .light/.night.webp from the raw fills in tools/asset-gen/fill-src/ by punching their outlines out (line art as mask). Deterministic, no API key/network (-- page-or-category… to filter)",
"gen:coloring-responsive": "Derive compact fills, cover thumbnails, and responsive alpha-preserving page-selector rasters; full-page art stays canonical SVG. Deterministic, no API key/network (-- category… to filter)",
"gen:coloring-thumbs": "Regenerate the light/dark .thumb.webp cover thumbnails for the coloring-book picker (-- category… to filter)",
"gen:coloring-book-proof-sheet": "Build a self-contained HTML proof sheet of ONE coloring category — line art, chalk, light + night fills side by side with outline-keep scores, plus the composited view — to review/publish as an Artifact (-- category[/page[-orient]], --source shipped|samples, --out FILE)",
"gen:e2e-tuning-report": "Rebuild the E2E tuning scrapbook page (ADR-0059) from the worker-count measurements recorded in the script: the sweep tables, the local/CI comparison, and the flake hypotheses that were falsified. Edit the datasets at the top of tools/e2e-tuning/gen-tuning-report.mjs after a re-tune, then re-run. Deterministic, no network",
"capture:page-inventory": "Capture the light/night scrapbook app-page inventory from the live app with Playwright at four Apple devices in portrait + landscape, retrying then failing on any capture that comes back near-uniform or at dimensions other than its viewport's, and write its review-ID/hash-bound capture manifest (-- --port N --critique FILE; stale feedback is preserved but detached). Spot-check a slice with -- --surface ID --viewport ID --theme ID, which writes captures plus their review descriptions to a scratch --out and no manifest, since a partial one cannot be the coverage authority. --out is replaced wholesale, so it may only name the directory each mode owns: scrapbook/page-inventory for a full run, and .scrapbook-scratch/page-inventory-spot-check or a directory beneath it for a spot check",
"review:page-inventory": "Run one fresh image-only agent review per current page-inventory capture and write one hash-bound checkpoint per review ID, using whichever of claude or codex is installed (-- --runner claude|codex --concurrency N --limit N --review-id ID --model MODEL --effort LEVEL)",
"finalize:page-inventory-critique": "Strictly merge independent page-inventory review checkpoints into a manifest-bound design-critique.json, deriving coverage + severity totals, recording every group of captures that share a digest and theme with whether their severities diverge, and refusing missing/stale reviews (-- --status lists the queue; --manifest FILE --checkpoints DIR --out FILE; --allow-partial requires a scratch --out)",
"attach:page-inventory-feedback": "Rebuild the existing scrapbook app-page inventory HTML without generating images, attaching only a complete design critique whose per-image SHA-256 values match capture-manifest.json (-- --critique FILE; defaults to scrapbook/page-inventory/design-critique.json when present)",
"gen:assets:manifest": "Rewrite tools/asset-gen/golden/asset-manifest.sha256 (sha256 per committed art asset: coloring pages, style covers, AI value-prop tiles, fill-src raws) so binary asset changes review as text diffs; verified in CI by check:assets:manifest",
"gen:readme-hero": "Capture balloon/phone and dinosaur/tablet scenes from the live app in equal-height generic device frames into docs/assets/readme-hero.webp (--port, --out)",
"gen:store-assets": "Regenerate the store screenshots + feature graphic for both stores into store-assets/ (Playwright): capture app scenes into store-assets/captures/, then screenshot the /dev/store-frames route",
"gen:store-assets:frames": "Re-render the store screenshots + feature graphic from the committed store-assets/captures/ without re-driving the app — the fast path for frame copy/layout iteration",
"scrapbook:publish": "Promote a keeper run output into the committed scrapbook/ tree and rebuild the index — copies a file or dir under scrapbook/<type>/<name> for a live GitHub Pages URL (ADR-0059). Usage: -- <source> <type>/<name>, e.g. -- lighthouse-reports lighthouse/latest",
"scrapbook:index": "Rebuild scrapbook/index.html and the coloring-book proof-sheet hub from the current scrapbook/ tree — no copy",
"scrapbook:serve": "Serve the committed scrapbook locally at http://127.0.0.1:4174",
"scrapbook:proof-sheet-hub": "Rebuild scrapbook/coloring-book-proof-sheets/index.html from its shared chrome and category registry",
"scrapbook:check": "Drift guard (CI): fail if any scrapbook/ collection dir has no reachable entry page, so the index's collection count always matches the cards it renders (md-only collections included)",
"scrapbook:clear-sound-sheet": "Rebuild the drag-to-clear sound audition sheet from the clips committed under scrapbook/sound-design/clear-sound-contact-sheet/assets. Usage: -- --from <dir of new mp3s> --inline <standalone.html>",
"scrapbook:clear-sound-sheet:audit": "Drive every option on the sound audition sheet through every scripted gesture in headless Chromium and fail on any that renders silence. Set CHROMIUM_PATH when Playwright has no browser download",
"cap:sync": "build:cap + copy web assets & plugins into the native projects",
"cap:android": "cap:sync + open the Android project in Android Studio",
"cap:ios": "cap:sync + open the iOS project in Xcode (macOS-only)",
"android:setup": "One-time emulator setup: installs the API 33 system image and creates the Pixel_7_Pro_API_33 AVD",
"android:boot": "Boot the Pixel_7_Pro_API_33 emulator",
"android:emulator": "cap:sync + build, install, and launch on the Pixel_7_Pro_API_33 emulator",
"android:live": "Run on the emulator with live reload against the dev server on port 5173",
"android:apk": "Debug APK → android/app/build/outputs/apk/debug/",
"android:apk:release": "Release APK with R8 + resource shrinking → android/app/build/outputs/apk/release/ (signed only when android/keystore.properties exists)",
"android:run": "Build + install the debug app onto the connected device/emulator",
"android:run:device": "cap:sync + build, install onto the connected physical phone (emulators are ignored; set ANDROID_SERIAL when several phones are attached — find serials with adb:devices)",
"android:bundle": "Signed release AAB; needs android/keystore.properties",
"android:verify": "Verify the release AAB's signature (expect 'jar verified')",
"android:open": "Reveal the release bundle folder in the OS file manager",
"android:clean": "Gradle clean, no cap:sync",
"android:compile": "Compile the Release Java (app + Capacitor plugins) with no web build: cap update against an empty assets/public, then :app:compileReleaseJavaWithJavac. The Native compile workflow runs it on native PRs",
"ios:build": "Debug build for the iOS Simulator → ios/App/build/Build/Products/Debug-iphonesimulator/App.app (macOS-only)",
"ios:build:release": "Release build for the iOS Simulator without store signing → ios/App/build/Build/Products/Release-iphonesimulator/App.app (macOS-only; no signing team needed)",
"ios:run": "cap:sync + build, install, and launch, prompting to choose the iOS simulator or connected device (macOS-only)",
"ios:run:emulator": "cap:sync + build, install, and launch on the iPad mini (A17 Pro) simulator (macOS-only; use ios:run to pick a device)",
"ios:run:device": "cap:sync + build, install, and launch on the connected physical iPad/iPhone (macOS-only; set IOS_UDID when several are attached; needs signing + a trusted developer cert — see the mobile guide)",
"ios:live": "Run on a simulator/device with live reload against the dev server on port 5173 (macOS-only; pair with dev:cap)",
"ios:archive": "Release archive (signed; needs the Apple Developer team set in Xcode) → ios/App/build/App.xcarchive (macOS-only)",
"ios:ipa": "ios:archive + export the App Store .ipa → ios/App/build/ipa/ (macOS-only; see the mobile guide)",
"ios:open": "Reveal the exported .ipa folder in the OS file manager",
"ios:clean": "Xcode clean of the App scheme (macOS-only)",
"adb:devices": "List connected Android devices (expect 'device', not 'unauthorized')",
"adb:reverse": "Forward the phone's port 5173 to the desktop dev server (re-run after each USB reconnect)",
"audit:preflight": "Read-only go/no-go check before an unattended audit burndown: selected agent auth, clean tree, origin reachable, docs/AUDIT.md parses, npm run check green (burn-down-audits skill)",
"audit:burndown": "Scripted burndown of docs/AUDIT.md: one isolated Claude/Codex session per role per finding (verify → implement → adversarial review → fix), one commit each, pushed every finding. MAX_ISSUES=5 canary by default; knobs are env vars — see the burn-down-audits skill",
"audit:burndown:overnight": "Launch audit:burndown unattended (-- N findings, default 600): preflight-gated, spawned detached so a closed terminal can't kill it. Stop with `touch .audit-work/STOP`",
"audit:status": "Where the audit burndown stands: completed/deferred/remaining counts, progress bar, run state, unposted PR comments, recent Audit: commits, last log lines",
"audit:cost": "Audit-burndown usage from saved Claude JSON or Codex JSONL: cost/tokens by role, per-issue average, projected remaining total",
"audit:watch": "Live view of a running audit burndown: tail -f the run log, or -- --dash for a status summary refreshing every 10s",
"prerelease": "check:coloring-assets + check:assets:manifest gates before release",
"release": "Cut a release from releases/<version>.md: bump versions, commit, tag, push (use the cut-release skill to draft notes first). Attaches no store artifacts — they cannot exist until this bump is committed",
"release:publish": "Attach the built .aab/.ipa to an existing GitHub Release, refusing any artifact whose embedded version does not match (run after the cut-release then build skills)",
"worktrees:salvage": "Move gitignored evidence (perf-profiles/, red-team decrypted/output) out of agent worktrees into ~/Code/splotch-worktree-evidence before they are pruned; dry run by default, -- --apply to move (prune-git-workspace skill)",
"worktrees:prune": "Remove agent worktrees that are clean, merged into origin/main, salvaged, and no process's cwd — one removed/kept/skip (in use) line each; dry run by default, -- --apply to remove; never touches the main checkout or any branch",
"branches:prune": "Delete local branches proven dead against origin/main with git branch -d, report why every other one stays (open PR, worktree-held, unique commits); dry run by default, -- --apply to delete, -- --include-equivalent to also delete rebase/squash-merged branches after a verbatim proof. Second form -- --delete-branch=<name> --at=<commit> is the judgment pass's guarded single-branch delete",
"branches:gather": "One row of facts per origin branch (ahead/behind/inbase/age) for the prune-git-workspace skill's remote triage; deletes nothing",
"perf:rescore": "Re-derive every capture in a corpus from its raw frame table, offline and in seconds. Reads the `report` a capture carries — never the `summaries` it was written with, which were computed by whichever estimator the branch had at capture time — and re-scores through the SHIPPED scoring modules, so the answer is what the gate says rather than what a private reimplementation says. Handles all three envelopes (split transport, Appium runner, bare probe upload). Captures whose corpus index marks cellAttributable: false are refused by default; --include-unattributable re-admits them deliberately, visibly marked in the table and any --json export. This is how a metric correction is judged against every number already taken: ADR-0136 was validated across 43 captures this way before a line of product code changed, and trialling a NEW charge is the same operation — change the shipped charge on a branch and re-run. Prints the fidelity verdict beside every row, because a capture that fails it must not be scored however plausible its number looks. Flags: --corpus=<dir> (required), --filter=<substring>, --target=<matrix target id, to apply that cell's gate exception>, --json=<out>, --include-unattributable",
"perf:evidence:keep": "Promote a campaign's representative drawing captures or complete action suites into the tracked evidence corpus at perf-profiles/evidence/<campaign>/ (ADR-0138). Raw captures are otherwise gitignored, so a metric correction can only be recaptured on hardware rather than re-scored against history — which is what left ten of eleven matrix targets publishing numbers from a superseded estimator. Replaces device identifiers with [redacted] and the capture host's private IPv4 / mDNS name with lan-host / rig-mac.local in the tracked copy only, keeping every URL's path and ?probe= query. Keeps captures WHOLE (every trimming that saves meaningful space drops a gate or turns the fidelity verdict false) and keeps one per target x brush rather than per matrix cell — except hand captures (handCapture: true), which are ALL kept and filed as basename plus an 8-hex digest of their corpus-relative path (injective where a separator join was not), because a hand corpus exists to show spread and each capture cost a person's time. Requires the exact capture --product-commit and stamps it into index.json beside each sample's cell and fidelity verdict. Read drawing captures back with perf:rescore; action suites retain each raw repeat for action-stats scoring. Refuses an existing --campaign destination rather than promoting into it: reusing a name overwrites only the files the new run selects, and perf:rescore scores every JSON it finds rather than treating index.json as an allowlist, so unselected captures from the earlier run would be read as current evidence. --force replaces the corpus whole. Selection prefers a scoreable representative per target x brush (passing, then calibration-only failures, then unreported; a number-invalidating trustedTouch/cadence failure ranks last) and refuses a cell whose pool holds only invalid-or-unreported captures. Flags: --corpus=<dir> (required), --campaign=<name> (required), --product-commit=<40-character SHA> (required), --filter=<substring>, --target=<id> (the fallback when neither the artifact, its path under --corpus, nor the corpus root names a target; refused when it contradicts a target-named corpus root, and a capture nothing resolves is refused rather than filed as unknown), --keep-all with --study=<rationale>, --force, --allow-failed (keep an unscoreable representative deliberately)",
"perf:build:cap": "Build and sync the NATIVE app with the profiling seams compiled in. Plain build:cap (and therefore ios:run:device / android:run) dead-code-eliminates __committedBrushMode and the engine.* marks, so a native capture cannot confirm which brush the engine committed and reports no engine work — the capture still runs and still writes an artifact, which is the failure mode this repo keeps finding. Run this before perf:ios:xcuitest:screen --native-app, then install with ios:run:device or android:run:device.",
"check:matrix-staleness": "Report each performance-matrix section's capture age, oldest first (ADR-0175, superseding ADR-0159): every captured drawing, undo, and action section, preserved ones included, with its capturedOn date, its age in days, its product commit, and the engine and measured-surface commits that landed since. Age is reported, not failed, by default: main takes several product merges a day while campaigns refresh the matrix periodically, so a current-or-stale verdict was false for most of every section's life. Every release-gate section older than RELEASE_GATE_MAX_AGE_DAYS (or undated) is named with its date and age in a WARN line; tripwire and advisory rows are never flagged. --release-gate-age fails on any such section: it is the age clause of a performance campaign's completion gate. --strict asserts provenance-complete instead: every captured section carries a valid capturedOn date and a product commit this checkout can resolve. Chained onto gen:performance-matrix. Flags: --manifest=, --base= (default HEAD), --strict, --release-gate-age",
"check:device-identifiers": "Fail when any tracked file contains a physical-device identifier (Apple hardware UDID or rig Android serial shapes), or any tracked file under perf-profiles/evidence/ carries a capture-host address (a private IPv4 or an mDNS .local name) — promoted evidence must carry [redacted] for a device and lan-host / rig-mac.local for a host, and tests the FAKE_* constants from tools/perf/lib/device-identifiers.mjs; output masks matched values (issues 1645 and 2221). Also run as a test by tools/perf/tests/device-identifier-guard.test.mjs in the CI tools tier",
"perf:campaign:status": "How far along is a campaign, and what is left. Exists because both obvious ways to read this off ledger.tsv are wrong: it is an append-only log with skip and retry rows, so `wc -l` is not a cell count, and a resumed run records `already-valid` rather than `valid-json`, so grepping the latter undercounts a finished target. Completion is decided by the RUNNER'S OWN artifact inspection — matching runtime AND the fidelity verdict where the transport writes one — so status cannot call a structurally rejected measurement complete, and a ledger row cannot certify a cell whose artifact was deleted or replaced. Physical-device status includes the same drift-reference queue the runner executes. The ledger supplies attempts spent and is reported when it disagrees with disk. Flags: --target= (required), --modes=, --items=, --output-root=, --ledger=",
"gen:android:assets": "Regenerate Android launcher icons and splash with Capacitor assets, then restore the app-designed monochrome drawable and both adaptive references from assets/icon-monochrome.xml"
},
"devDependencies": {
"@axe-core/playwright": "^4.13.0",
"@capacitor/android": "^8.5.2",
"@capacitor/assets": "^3.0.5",
"@capacitor/cli": "^8.5.2",
"@capacitor/ios": "^8.5.2",
"@dprint/json": "^0.25.0",
"@dprint/markdown": "^0.24.0",
"@dprint/typescript": "^0.96.1",
"@eslint/js": "^10.0.1",
"@google/genai": "^2.24.0",
"@intellectronica/ruler": "0.3.44",
"@playwright/test": "^1.63.0",
"@resvg/resvg-js": "^2.6.2",
"@types/node": "^22.20.4",
"@vitest/coverage-v8": "^5.0.2",
"@vitest/eslint-plugin": "^1.6.27",
"browserslist": "4.29.2",
"dprint": "^0.57.4",
"eslint": "^10.11.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-playwright": "^2.12.0",
"eslint-plugin-svelte": "^3.23.0",
"fit-curve": "^0.2.0",
"globals": "^17.12.0",
"happy-dom": "^20.14.5",
"knip": "^6.38.0",
"lighthouse": "13.5.0",
"postcss-html": "^2.0.0",
"prettier": "^3.9.9",
"prettier-plugin-svelte": "^4.1.1",
"scripts-info": "^1.0.5",
"stylelint": "^17.15.0",
"svelte-check": "^4.7.6",
"svgo": "^4.1.0",
"typescript": "^6.0.3",
"typescript-eslint": "^8.71.0",
"vitest": "^5.0.2",
"web-features": "3.40.0"
},
"dependencies": {
"@aparajita/capacitor-secure-storage": "^8.0.1",
"@capacitor-community/media": "^9.1.0",
"@capacitor/core": "^8.5.2",
"@capacitor/device": "^8.0.3",
"@capacitor/haptics": "^8.0.2",
"@capacitor/network": "^8.0.1",
"@capacitor/preferences": "^8.0.1",
"@capacitor/screen-orientation": "^8.0.1",
"@fontsource-variable/quicksand": "^5.3.0",
"@netlify/blobs": "^11.1.1",
"@sveltejs/adapter-netlify": "^6.0.4",
"@sveltejs/adapter-static": "^3.0.10",
"@sveltejs/kit": "^2.70.3",
"@sveltejs/vite-plugin-svelte": "^7.3.1",
"openai": "^7.23.0",
"sharp": "^0.35.5",
"svelte": "^5.57.1",
"vite": "^8.3.1",
"vite-plugin-pwa": "^1.3.0"
}
}