yam-flow 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/bin/yam.ts CHANGED
@@ -1,16 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync } from 'node:child_process';
3
+ import crypto from 'node:crypto';
3
4
  import fs from 'node:fs';
4
5
  import fsp from 'node:fs/promises';
6
+ import { createRequire } from 'node:module';
5
7
  import os from 'node:os';
6
8
  import path from 'node:path';
7
9
  import process from 'node:process';
8
10
  import { fileURLToPath } from 'node:url';
9
11
  import {
10
12
  TRUTH_STATUSES,
13
+ buildMediaGenerationProof,
11
14
  buildMissionPatchEnvelope,
12
15
  buildRollbackHint,
16
+ buildRuntimeBackendEvidence,
13
17
  buildUeyeVisualProvenance,
18
+ buildUeyeRunReport,
14
19
  buildYamCompletionProof,
15
20
  detectDbSafetyText as detectTrustDbSafetyText,
16
21
  isTruthStatus
@@ -132,6 +137,10 @@ Usage:
132
137
  yam tools doctor [dir]
133
138
  yam proof [dir|--from file] [--route route] [--truth status] [--command text] [--evidence text]
134
139
  yam proof write [dir] [--format json|md] [--out file] [--route route] [--truth status] [--command text]
140
+ yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
141
+ yam ueye compare --reference ref.png --actual screenshot.png [--json]
142
+ yam ueye report [--reference ref.png] [--actual screenshot.png] [--json]
143
+ yam media proof [--requested] [--attempted] [--output file] [--json]
135
144
  yam release report [--json]
136
145
  yam safety [text...]
137
146
  yam memory <init|add|list|summary|resolve> [dir] [options]
@@ -140,7 +149,7 @@ Usage:
140
149
  yam tune-log [dir]
141
150
  yam install
142
151
  yam uninstall
143
- yam doctor
152
+ yam doctor [--json]
144
153
  yam examples
145
154
  yam path
146
155
  yam version
@@ -371,7 +380,33 @@ function failOrReport(label, issues, quiet = false) {
371
380
  return issues.length;
372
381
  }
373
382
 
374
- async function doctor() {
383
+ async function doctor(args = []) {
384
+ const flags = parseSimpleFlags(args, new Set(['json']));
385
+ const report = await buildDoctorReport();
386
+ if (flags.json) {
387
+ console.log(JSON.stringify(report, null, 2));
388
+ if (!report.ok) process.exitCode = 1;
389
+ return;
390
+ }
391
+ if (report.ok) {
392
+ console.log('yam doctor: ok');
393
+ console.log('No hooks, automations, or global config are required.');
394
+ console.log('Skill install state is reported by `yam status`.');
395
+ console.log(`yam-lite hook: ${report.yamLiteHook} (optional)`);
396
+ return;
397
+ }
398
+
399
+ console.log('yam doctor: issues');
400
+ for (const issue of report.issues) console.log(`- ${issue}`);
401
+ if (report.nextActions.length) {
402
+ console.log('');
403
+ console.log('Next actions:');
404
+ for (const action of report.nextActions) console.log(`- ${action}`);
405
+ }
406
+ process.exitCode = 1;
407
+ }
408
+
409
+ async function buildDoctorReport() {
375
410
  const issues = [];
376
411
  for (const skill of SKILLS) {
377
412
  if (!await exists(path.join(ROOT, 'skills', skill, 'SKILL.md'))) issues.push(`missing source skill: ${skill}`);
@@ -394,19 +429,28 @@ async function doctor() {
394
429
  }
395
430
  const verifyIssues = await verify({ quiet: true });
396
431
  if (verifyIssues > 0) issues.push(`verify reported ${verifyIssues} issue(s)`);
432
+ const globalHook = await readJsonOrDefault(path.join(os.homedir(), '.codex', 'hooks.json'), {});
433
+ return {
434
+ schema: 'yam.doctor.v1',
435
+ generatedAt: new Date().toISOString(),
436
+ ok: issues.length === 0,
437
+ issues,
438
+ nextActions: doctorNextActions(issues),
439
+ yamLiteHook: hookConfigHasYamLite(globalHook) ? 'enabled' : 'disabled'
440
+ };
441
+ }
397
442
 
398
- if (!issues.length) {
399
- console.log('yam doctor: ok');
400
- const globalHook = await readJsonOrDefault(path.join(os.homedir(), '.codex', 'hooks.json'), {});
401
- console.log('No hooks, automations, or global config are required.');
402
- console.log('Skill install state is reported by `yam status`.');
403
- console.log(`yam-lite hook: ${hookConfigHasYamLite(globalHook) ? 'enabled' : 'disabled'} (optional)`);
404
- return;
443
+ function doctorNextActions(issues = []) {
444
+ const actions = [];
445
+ for (const issue of issues) {
446
+ if (/missing source skill/i.test(issue)) actions.push('run from a complete yam-flow checkout or reinstall the published package');
447
+ else if (/missing truth matrix/i.test(issue)) actions.push('restore references/truth-matrix.md before publishing');
448
+ else if (/unexpected local hooks/i.test(issue)) actions.push('remove project hooks unless this repo is intentionally dogfooding them');
449
+ else if (/unexpected .*automation/i.test(issue)) actions.push('remove stale yam/timeto automations before claiming clean install state');
450
+ else if (/legacy|retired/i.test(issue)) actions.push('run `yam install` to replace old skill entries');
451
+ else if (/verify reported/i.test(issue)) actions.push('run `npm run verify` and fix the reported package boundary or metadata issue');
405
452
  }
406
-
407
- console.log('yam doctor: issues');
408
- for (const issue of issues) console.log(`- ${issue}`);
409
- process.exitCode = 1;
453
+ return [...new Set(actions)];
410
454
  }
411
455
 
412
456
  async function tools(args = []) {
@@ -467,6 +511,7 @@ async function buildToolsDoctorReport(targetDir = process.cwd()) {
467
511
  packageInfo.packageJson ? JSON.stringify(packageInfo.pkg?.devDependencies || {}) : ''
468
512
  ].join('\n'));
469
513
  const projectSurfaces = await detectProjectToolSurfaces(dir, packageInfo.pkg);
514
+ const frameworkChecklist = await detectFrameworkChecklist(dir, packageInfo.pkg);
470
515
  const sqlScan = await scanSqlFiles(dir);
471
516
  const rows = [
472
517
  readinessRow('Codex home', await exists(codexHome) ? 'ready' : 'missing', codexHome),
@@ -496,8 +541,16 @@ async function buildToolsDoctorReport(targetDir = process.cwd()) {
496
541
  packageManager: detection.packageJson ? detection.packageManager : null,
497
542
  instructionSurfaces,
498
543
  projectSurfaces,
544
+ frameworkChecklist,
499
545
  sqlScan,
500
546
  riskNotes,
547
+ nextActions: toolsDoctorNextActions({
548
+ pack,
549
+ rows,
550
+ riskNotes,
551
+ frameworkChecklist,
552
+ commands: detection.packageJson ? detection.commands : {}
553
+ }),
501
554
  routeRecommendations: {
502
555
  dbSupabase: safety.hits.length || sqlScan.findings.length ? '$deep required before claiming safe' : '$deep when destructive or production mutation appears',
503
556
  currentDocs: 'use current-docs proof only when version/freshness matters',
@@ -530,6 +583,14 @@ function printToolsDoctorReport(data) {
530
583
  for (const surface of data.projectSurfaces) console.log(`- ${surface}`);
531
584
  }
532
585
 
586
+ if (data.frameworkChecklist?.detected) {
587
+ console.log('');
588
+ console.log(`Framework checklist: ${data.frameworkChecklist.framework}`);
589
+ for (const check of data.frameworkChecklist.checks) {
590
+ console.log(`- ${check.id}: ${check.label} (${check.route})`);
591
+ }
592
+ }
593
+
533
594
  if (data.sqlScan.filesScanned > 0) {
534
595
  console.log('');
535
596
  console.log(`SQL scan: ${data.sqlScan.filesScanned} file(s), ${data.sqlScan.findings.length} risk finding(s)`);
@@ -544,6 +605,12 @@ function printToolsDoctorReport(data) {
544
605
  for (const note of data.riskNotes) console.log(`- ${note.level}: ${note.reason}`);
545
606
  }
546
607
 
608
+ if (data.nextActions?.length) {
609
+ console.log('');
610
+ console.log('Next actions:');
611
+ for (const action of data.nextActions) console.log(`- ${action}`);
612
+ }
613
+
547
614
  console.log('');
548
615
  console.log('Route recommendations:');
549
616
  console.log(`- DB/Supabase destructive or production write work: ${data.routeRecommendations.dbSupabase}`);
@@ -616,6 +683,56 @@ async function detectProjectToolSurfaces(dir, pkg = null) {
616
683
  return surfaces;
617
684
  }
618
685
 
686
+ async function detectFrameworkChecklist(dir, pkg = null) {
687
+ const deps = {
688
+ ...pkg?.dependencies,
689
+ ...pkg?.devDependencies
690
+ };
691
+ const depNames = Object.keys(deps || {});
692
+ const hasReact = depNames.includes('react');
693
+ const hasNext = depNames.includes('next') || await exists(path.join(dir, 'next.config.js')) || await exists(path.join(dir, 'next.config.mjs')) || await exists(path.join(dir, 'next.config.ts'));
694
+ const hasVite = depNames.includes('vite') || await exists(path.join(dir, 'vite.config.js')) || await exists(path.join(dir, 'vite.config.ts'));
695
+ const framework = hasNext ? 'next-app' : hasReact && hasVite ? 'react-vite' : hasReact ? 'react' : pkg ? 'node-package' : 'unknown';
696
+ const uiLike = hasReact || hasNext || hasVite || await exists(path.join(dir, 'app')) || await exists(path.join(dir, 'src', 'app')) || await exists(path.join(dir, 'pages'));
697
+ const checks = [];
698
+ if (uiLike) {
699
+ checks.push(
700
+ checklistRow('states', 'default/loading/error/empty states are reachable before claiming complete', '$ueye'),
701
+ checklistRow('responsive', 'mobile and desktop layouts avoid overlap and clipped text', '$ueye'),
702
+ checklistRow('accessibility', 'interactive controls have accessible labels and keyboard path', '$ueye'),
703
+ checklistRow('runtime', 'browser or in-app runtime evidence is recorded when claiming visual verified', '$ueye')
704
+ );
705
+ }
706
+ if (hasNext) {
707
+ checks.push(checklistRow('boundaries', 'server/client boundaries and async data states match the framework pattern', '$deep when risky'));
708
+ }
709
+ if (pkg) {
710
+ checks.push(checklistRow('package', 'run the smallest honest type/build/package check before publishing', '$quick or $deep'));
711
+ }
712
+ return {
713
+ schema: 'yam.framework-checklist.v1',
714
+ detected: checks.length > 0,
715
+ uiLike,
716
+ framework,
717
+ routeHint: uiLike ? '$ueye for visual claims; $deep when runtime/data risk appears' : '$quick by default; deepen on risk',
718
+ checks
719
+ };
720
+ }
721
+
722
+ function checklistRow(id, label, route) {
723
+ return { id, label, route };
724
+ }
725
+
726
+ function toolsDoctorNextActions({ pack, rows, riskNotes, frameworkChecklist, commands }) {
727
+ const actions = [];
728
+ if (!pack) actions.push('create or refresh yam.project.md so route choices reuse local project context');
729
+ if (rows.some((row) => row.name === 'Yam skills' && row.status !== 'ready')) actions.push('run `yam install` and restart Codex before relying on installed skills');
730
+ if (riskNotes.some((note) => note.level === 'danger')) actions.push('use $deep before claiming destructive database or production write safety');
731
+ if (frameworkChecklist?.detected && !commands?.build && !commands?.typecheck) actions.push('add or document a small build/typecheck command for implementation verification');
732
+ if (frameworkChecklist?.uiLike) actions.push('for Ueye verified claims, record reference, actual screenshot, and comparison result');
733
+ return [...new Set(actions)];
734
+ }
735
+
619
736
  async function scanSqlFiles(rootDir, { maxFiles = 30, maxBytes = 200000, maxDepth = 5 } = {}) {
620
737
  const root = path.resolve(rootDir || process.cwd());
621
738
  const files = [];
@@ -1010,6 +1127,376 @@ async function readStdinTextIfAvailable() {
1010
1127
  return Buffer.concat(chunks).toString('utf8').trim();
1011
1128
  }
1012
1129
 
1130
+ async function ueye(args = []) {
1131
+ const subcommand = args[0] || 'help';
1132
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return ueyeUsage();
1133
+ if (subcommand === 'capture') return ueyeCapture(args.slice(1));
1134
+ if (subcommand === 'compare') return ueyeCompare(args.slice(1));
1135
+ if (subcommand === 'report') return ueyeReport(args.slice(1));
1136
+ console.error(`unknown ueye command: ${subcommand}`);
1137
+ return ueyeUsage();
1138
+ }
1139
+
1140
+ function ueyeUsage() {
1141
+ console.log(`yam ueye
1142
+
1143
+ Opt-in visual evidence helpers. Ueye stays one skill: fast by default, capture/compare only when verified visual claims need real evidence.
1144
+
1145
+ Usage:
1146
+ yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
1147
+ yam ueye compare --reference ref.png --actual screenshot.png [--json]
1148
+ yam ueye report [--reference ref.png] [--actual screenshot.png] [--design-quality pass|needs-polish|fails|not-checked] [--json]
1149
+
1150
+ Notes:
1151
+ capture uses a locally available Playwright install when present. It does not download browsers or install dependencies.
1152
+ compare uses local files only and reports sha256, dimensions, comparison_result, and proof-ready visual provenance.
1153
+ report produces a proof-ready Ueye visual run report without requiring a new capture.
1154
+ `);
1155
+ }
1156
+
1157
+ async function ueyeCapture(args = []) {
1158
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return ueyeUsage();
1159
+ const flags = parseSimpleFlags(args, new Set(['url', 'out', 'viewport', 'wait-until', 'timeout', 'full-page', 'json']));
1160
+ const url = String(flags.url || '');
1161
+ const out = String(flags.out || '');
1162
+ if (!url || !out) {
1163
+ console.error('usage: yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]');
1164
+ process.exitCode = 1;
1165
+ return;
1166
+ }
1167
+
1168
+ const target = path.resolve(expandHome(out));
1169
+ const viewport = parseViewport(String(flags.viewport || '1440x900'));
1170
+ const timeout = Number(flags.timeout || 30000);
1171
+ const waitUntil = String(flags.wait_until || 'networkidle');
1172
+ let browser;
1173
+
1174
+ try {
1175
+ const playwright = await loadOptionalPlaywright();
1176
+ const chromium = playwright?.chromium;
1177
+ if (!chromium) throw new Error('Playwright chromium launcher was not found');
1178
+ await fsp.mkdir(path.dirname(target), { recursive: true });
1179
+ browser = await chromium.launch({ headless: true });
1180
+ const page = await browser.newPage({ viewport });
1181
+ await page.goto(url, { waitUntil, timeout });
1182
+ await page.screenshot({ path: target, fullPage: Boolean(flags.full_page) });
1183
+ const info = await imageFileInfo(target);
1184
+ const result = {
1185
+ schema: 'yam.ueye-capture.v1',
1186
+ status: 'verified',
1187
+ url,
1188
+ out: target,
1189
+ viewport: `${viewport.width}x${viewport.height}`,
1190
+ full_page: Boolean(flags.full_page),
1191
+ sha256: info.sha256,
1192
+ dimensions: info.dimensions,
1193
+ visual_evidence: `browser screenshot captured: ${target} (${info.dimensions}, sha256:${info.sha256})`
1194
+ };
1195
+ printJsonOrHuman(result, Boolean(flags.json), 'Ueye capture');
1196
+ } catch (error) {
1197
+ const result = {
1198
+ schema: 'yam.ueye-capture.v1',
1199
+ status: 'blocked',
1200
+ url,
1201
+ out: target,
1202
+ reason: errorMessage(error),
1203
+ next_action: 'Install Playwright in the current project or provide a user/browser screenshot, then use `yam ueye compare`.',
1204
+ visual_cap: 'blocked'
1205
+ };
1206
+ printJsonOrHuman(result, Boolean(flags.json), 'Ueye capture');
1207
+ process.exitCode = 1;
1208
+ } finally {
1209
+ if (browser) await browser.close().catch(() => {});
1210
+ }
1211
+ }
1212
+
1213
+ async function ueyeCompare(args = []) {
1214
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return ueyeUsage();
1215
+ const flags = parseSimpleFlags(args, new Set(['reference', 'ref', 'actual', 'screenshot', 'json', 'reference-id', 'screenshot-id']));
1216
+ const reference = String(flags.reference || flags.ref || '');
1217
+ const actual = String(flags.actual || flags.screenshot || '');
1218
+ if (!reference || !actual) {
1219
+ console.error('usage: yam ueye compare --reference ref.png --actual screenshot.png [--json]');
1220
+ process.exitCode = 1;
1221
+ return;
1222
+ }
1223
+
1224
+ const referencePath = path.resolve(expandHome(reference));
1225
+ const actualPath = path.resolve(expandHome(actual));
1226
+ try {
1227
+ const referenceInfo = await imageFileInfo(referencePath);
1228
+ const actualInfo = await imageFileInfo(actualPath);
1229
+ if (referenceInfo.dimensions === 'unknown' || actualInfo.dimensions === 'unknown') {
1230
+ throw new Error('compare requires image files with readable PNG, JPEG, or GIF dimensions');
1231
+ }
1232
+ const exactMatch = referenceInfo.sha256 === actualInfo.sha256;
1233
+ const sameDimensions = referenceInfo.dimensions === actualInfo.dimensions;
1234
+ const comparisonResult = exactMatch ? 'matched' : 'different';
1235
+ const truthStatus = exactMatch ? 'verified' : 'partial';
1236
+ const provenance = buildUeyeVisualProvenance({
1237
+ source_kind: 'implementation_screenshot',
1238
+ source_path: actualPath,
1239
+ source_hash: actualInfo.sha256,
1240
+ reference_id: String(flags.reference_id || path.basename(referencePath)),
1241
+ screenshot_id: String(flags.screenshot_id || path.basename(actualPath)),
1242
+ local_only: true,
1243
+ redacted: false,
1244
+ operator_provided: false,
1245
+ comparison_result: comparisonResult,
1246
+ truth_status: truthStatus
1247
+ });
1248
+ const result = {
1249
+ schema: 'yam.ueye-compare.v1',
1250
+ status: truthStatus,
1251
+ comparison_result: comparisonResult,
1252
+ exact_match: exactMatch,
1253
+ same_dimensions: sameDimensions,
1254
+ reference: referenceInfo,
1255
+ actual: actualInfo,
1256
+ visual_evidence: `browser/local screenshot comparison executed: reference=${referencePath}, actual=${actualPath}, result=${comparisonResult}`,
1257
+ visual_provenance: provenance,
1258
+ proof_hint: `yam proof --route ueye --truth ${truthStatus} --visual "browser/local screenshot comparison executed: ${comparisonResult}" --visual-provenance '${JSON.stringify(provenance)}' --require-visual`
1259
+ };
1260
+ printJsonOrHuman(result, Boolean(flags.json), 'Ueye compare');
1261
+ } catch (error) {
1262
+ const result = {
1263
+ schema: 'yam.ueye-compare.v1',
1264
+ status: 'blocked',
1265
+ comparison_result: 'not-verified',
1266
+ reason: errorMessage(error),
1267
+ visual_cap: 'blocked'
1268
+ };
1269
+ printJsonOrHuman(result, Boolean(flags.json), 'Ueye compare');
1270
+ process.exitCode = 1;
1271
+ }
1272
+ }
1273
+
1274
+ async function ueyeReport(args = []) {
1275
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return ueyeUsage();
1276
+ const flags = parseSimpleFlags(args, new Set(['reference', 'ref', 'actual', 'screenshot', 'capture-backend', 'compare-backend', 'design-quality', 'blocked-reason', 'next-action', 'json']));
1277
+ const reference = String(flags.reference || flags.ref || '');
1278
+ const actual = String(flags.actual || flags.screenshot || '');
1279
+ const referenceSources = [];
1280
+ const implementationSources = [];
1281
+ let comparisonResult = 'not-verified';
1282
+ let blockedReason = String(flags.blocked_reason || '');
1283
+
1284
+ try {
1285
+ if (reference) {
1286
+ const info = await imageFileInfo(reference);
1287
+ referenceSources.push(buildUeyeVisualProvenance({
1288
+ source_kind: 'reference_image',
1289
+ source_path: info.path,
1290
+ source_hash: info.sha256,
1291
+ reference_id: path.basename(info.path),
1292
+ local_only: true,
1293
+ operator_provided: true,
1294
+ comparison_result: 'reference-recorded',
1295
+ truth_status: 'partial'
1296
+ }));
1297
+ }
1298
+ if (actual) {
1299
+ const info = await imageFileInfo(actual);
1300
+ implementationSources.push(buildUeyeVisualProvenance({
1301
+ source_kind: 'implementation_screenshot',
1302
+ source_path: info.path,
1303
+ source_hash: info.sha256,
1304
+ screenshot_id: path.basename(info.path),
1305
+ local_only: true,
1306
+ operator_provided: false,
1307
+ comparison_result: 'implementation-recorded',
1308
+ truth_status: 'partial'
1309
+ }));
1310
+ }
1311
+ if (reference && actual && referenceSources[0]?.source_hash === implementationSources[0]?.source_hash) {
1312
+ comparisonResult = 'matched';
1313
+ } else if (reference && actual) {
1314
+ comparisonResult = 'different';
1315
+ }
1316
+ } catch (error) {
1317
+ blockedReason = errorMessage(error);
1318
+ }
1319
+
1320
+ const report = buildUeyeRunReport({
1321
+ reference_sources: referenceSources,
1322
+ implementation_sources: implementationSources,
1323
+ comparison_result: comparisonResult,
1324
+ design_quality: String(flags.design_quality || 'not-checked'),
1325
+ blocked_reason: blockedReason,
1326
+ next_action: String(flags.next_action || '')
1327
+ });
1328
+ const result = {
1329
+ ...report,
1330
+ capture_backend: String(flags.capture_backend || 'not-recorded'),
1331
+ compare_backend: String(flags.compare_backend || 'local-file-hash')
1332
+ };
1333
+ printJsonOrHuman(result, Boolean(flags.json), 'Ueye report');
1334
+ if (report.truth_status === 'blocked') process.exitCode = 1;
1335
+ }
1336
+
1337
+ async function media(args = []) {
1338
+ const subcommand = args[0] || 'help';
1339
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return mediaUsage();
1340
+ if (subcommand === 'proof') return mediaProof(args.slice(1));
1341
+ console.error(`unknown media command: ${subcommand}`);
1342
+ return mediaUsage();
1343
+ }
1344
+
1345
+ function mediaUsage() {
1346
+ console.log(`yam media
1347
+
1348
+ Opt-in media generation proof helpers. Generated media can support visual direction, but it cannot verify implemented UI without implementation screenshot evidence.
1349
+
1350
+ Usage:
1351
+ yam media proof [--tool name] [--requested] [--attempted] [--output file] [--wait-loop] [--blocked-reason text] [--json]
1352
+ `);
1353
+ }
1354
+
1355
+ async function mediaProof(args = []) {
1356
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return mediaUsage();
1357
+ const flags = parseSimpleFlags(args, new Set(['tool', 'requested', 'attempted', 'output', 'wait-loop', 'blocked-reason', 'next-action', 'json']));
1358
+ const output = String(flags.output || '');
1359
+ let outputHash = 'unknown';
1360
+ let outputPath = '';
1361
+ let blockedReason = String(flags.blocked_reason || '');
1362
+ if (output) {
1363
+ try {
1364
+ const info = await imageFileInfo(output);
1365
+ outputHash = info.sha256;
1366
+ outputPath = info.path;
1367
+ } catch (error) {
1368
+ blockedReason = blockedReason || errorMessage(error);
1369
+ outputPath = path.resolve(expandHome(output));
1370
+ }
1371
+ }
1372
+ const proof = buildMediaGenerationProof({
1373
+ tool_name: String(flags.tool || ''),
1374
+ generation_requested: Boolean(flags.requested),
1375
+ generation_attempted: Boolean(flags.attempted || output),
1376
+ output_path: outputPath,
1377
+ output_hash: outputHash,
1378
+ wait_loop_checked: Boolean(flags.wait_loop),
1379
+ blocked_reason: blockedReason,
1380
+ next_action: String(flags.next_action || '')
1381
+ });
1382
+ printJsonOrHuman(proof, Boolean(flags.json), 'Media proof');
1383
+ if (proof.truth_status === 'blocked') process.exitCode = 1;
1384
+ }
1385
+
1386
+ function parseSimpleFlags(args = [], allowed = new Set<string>()) {
1387
+ const flags: AnyRecord = {};
1388
+ const positionals: string[] = [];
1389
+ for (let index = 0; index < args.length; index += 1) {
1390
+ const arg = String(args[index] || '');
1391
+ if (!arg.startsWith('--')) {
1392
+ positionals.push(arg);
1393
+ continue;
1394
+ }
1395
+ const [rawKey, inlineValue] = arg.includes('=') ? arg.split(/=(.*)/s, 2) : [arg, undefined];
1396
+ const key = rawKey.slice(2);
1397
+ if (allowed.size && !allowed.has(key)) continue;
1398
+ const normalizedKey = key.replace(/-/g, '_');
1399
+ if (['json', 'full-page', 'requested', 'attempted', 'available', 'wait-loop', 'cleanup-checked'].includes(key)) {
1400
+ flags[normalizedKey] = true;
1401
+ continue;
1402
+ }
1403
+ const value = inlineValue ?? args[index + 1] ?? '';
1404
+ if (inlineValue === undefined) index += 1;
1405
+ flags[normalizedKey] = value;
1406
+ }
1407
+ flags._ = positionals;
1408
+ return flags;
1409
+ }
1410
+
1411
+ function parseViewport(value = '1440x900') {
1412
+ const match = String(value || '').match(/^(\d{2,5})x(\d{2,5})$/i);
1413
+ if (!match) return { width: 1440, height: 900 };
1414
+ return { width: Number(match[1]), height: Number(match[2]) };
1415
+ }
1416
+
1417
+ async function loadOptionalPlaywright() {
1418
+ let projectError = '';
1419
+ try {
1420
+ const projectRequire = createRequire(path.join(process.cwd(), 'package.json'));
1421
+ return projectRequire('playwright');
1422
+ } catch (error) {
1423
+ projectError = errorMessage(error);
1424
+ // Fall through to the yam package context for local development or bundled installs.
1425
+ }
1426
+ const dynamicImport = new Function('specifier', 'return import(specifier)') as (specifier: string) => Promise<any>;
1427
+ try {
1428
+ return await dynamicImport('playwright');
1429
+ } catch (error) {
1430
+ throw new Error(`Playwright not found from current project or yam package context. Current project: ${projectError}; package context: ${errorMessage(error)}`);
1431
+ }
1432
+ }
1433
+
1434
+ async function imageFileInfo(file) {
1435
+ const absolute = path.resolve(expandHome(file));
1436
+ const buffer = await fsp.readFile(absolute);
1437
+ const stat = await fsp.stat(absolute);
1438
+ return {
1439
+ path: absolute,
1440
+ sha256: crypto.createHash('sha256').update(buffer).digest('hex'),
1441
+ bytes: stat.size,
1442
+ dimensions: imageDimensions(buffer)
1443
+ };
1444
+ }
1445
+
1446
+ function imageDimensions(buffer) {
1447
+ const png = pngDimensions(buffer);
1448
+ if (png) return png;
1449
+ const jpeg = jpegDimensions(buffer);
1450
+ if (jpeg) return jpeg;
1451
+ const gif = gifDimensions(buffer);
1452
+ if (gif) return gif;
1453
+ return 'unknown';
1454
+ }
1455
+
1456
+ function pngDimensions(buffer) {
1457
+ if (buffer.length < 24) return '';
1458
+ if (buffer.readUInt32BE(0) !== 0x89504e47 || buffer.readUInt32BE(4) !== 0x0d0a1a0a) return '';
1459
+ return `${buffer.readUInt32BE(16)}x${buffer.readUInt32BE(20)}`;
1460
+ }
1461
+
1462
+ function gifDimensions(buffer) {
1463
+ if (buffer.length < 10) return '';
1464
+ const signature = buffer.toString('ascii', 0, 6);
1465
+ if (signature !== 'GIF87a' && signature !== 'GIF89a') return '';
1466
+ return `${buffer.readUInt16LE(6)}x${buffer.readUInt16LE(8)}`;
1467
+ }
1468
+
1469
+ function jpegDimensions(buffer) {
1470
+ if (buffer.length < 4 || buffer[0] !== 0xff || buffer[1] !== 0xd8) return '';
1471
+ let offset = 2;
1472
+ while (offset < buffer.length) {
1473
+ if (buffer[offset] !== 0xff) return '';
1474
+ const marker = buffer[offset + 1];
1475
+ const length = buffer.readUInt16BE(offset + 2);
1476
+ if (marker >= 0xc0 && marker <= 0xc3 && offset + 8 < buffer.length) {
1477
+ return `${buffer.readUInt16BE(offset + 7)}x${buffer.readUInt16BE(offset + 5)}`;
1478
+ }
1479
+ offset += 2 + length;
1480
+ }
1481
+ return '';
1482
+ }
1483
+
1484
+ function printJsonOrHuman(result, json = false, label = 'yam') {
1485
+ if (json) {
1486
+ console.log(JSON.stringify(result, null, 2));
1487
+ return;
1488
+ }
1489
+ console.log(label);
1490
+ for (const [key, value] of Object.entries(result)) {
1491
+ if (key === 'schema') continue;
1492
+ if (typeof value === 'object' && value !== null) {
1493
+ console.log(`- ${key}: ${JSON.stringify(value)}`);
1494
+ } else {
1495
+ console.log(`- ${key}: ${value}`);
1496
+ }
1497
+ }
1498
+ }
1499
+
1013
1500
  function detectDbSafetyText(text = '') {
1014
1501
  return detectTrustDbSafetyText(text);
1015
1502
  }
@@ -1050,9 +1537,11 @@ async function proof(args = []) {
1050
1537
  printProofList('Evidence', summary.evidence);
1051
1538
  printProofList('Visual evidence', summary.visual);
1052
1539
  printProofList('Runtime evidence', summary.runtime);
1540
+ printProofList('Runtime backend evidence', summary.runtimeBackendEvidence || []);
1053
1541
  printProofList('Visual provenance', summary.visualProvenance || []);
1054
1542
  printProofList('Mission patch envelope', summary.missionEnvelope || []);
1055
1543
  printProofList('Rollback hint', summary.rollbackHint || []);
1544
+ printProofList('Media proof', summary.mediaProof || []);
1056
1545
  printProofList('Changed surfaces', summary.changed);
1057
1546
  printProofList('Skipped', summary.skipped);
1058
1547
  printProofList('Blocked', summary.blocked);
@@ -1127,6 +1616,11 @@ function buildProofSummary(parsed, artifact) {
1127
1616
  evidence: [...artifact.evidence, ...arrayFlag(parsed.flags.evidence)],
1128
1617
  visual: [...artifact.visual, ...arrayFlag(parsed.flags.visual)],
1129
1618
  runtime: [...artifact.runtime, ...arrayFlag(parsed.flags.runtime)],
1619
+ runtimeBackendEvidence: [
1620
+ ...structuredEvidenceList(artifact.runtimeBackendEvidence),
1621
+ ...structuredEvidenceList(parsed.flags.runtime_backend_evidence),
1622
+ ...directRuntimeBackendEvidence(parsed.flags)
1623
+ ],
1130
1624
  visualProvenance: [
1131
1625
  ...structuredEvidenceList(artifact.visualProvenance),
1132
1626
  ...structuredEvidenceList(parsed.flags.visual_provenance)
@@ -1139,6 +1633,10 @@ function buildProofSummary(parsed, artifact) {
1139
1633
  ...structuredEvidenceList(artifact.rollbackHint),
1140
1634
  ...structuredEvidenceList(parsed.flags.rollback_hint)
1141
1635
  ],
1636
+ mediaProof: [
1637
+ ...structuredEvidenceList(artifact.mediaProof),
1638
+ ...structuredEvidenceList(parsed.flags.media_proof)
1639
+ ],
1142
1640
  cleanup: parsed.flags.cleanup || artifact.cleanup || '',
1143
1641
  changed: [...artifact.changed, ...arrayFlag(parsed.flags.changed)],
1144
1642
  skipped: [...artifact.skipped, ...arrayFlag(parsed.flags.skipped)],
@@ -1189,6 +1687,9 @@ function renderProofMarkdown(summary) {
1189
1687
  '## Runtime evidence',
1190
1688
  ...renderProofMarkdownList(summary.runtime),
1191
1689
  '',
1690
+ '## Runtime backend evidence',
1691
+ ...renderProofMarkdownList(summary.runtimeBackendEvidence || []),
1692
+ '',
1192
1693
  '## Visual provenance',
1193
1694
  ...renderProofMarkdownList(summary.visualProvenance || []),
1194
1695
  '',
@@ -1198,6 +1699,9 @@ function renderProofMarkdown(summary) {
1198
1699
  '## Rollback hint',
1199
1700
  ...renderProofMarkdownList(summary.rollbackHint || []),
1200
1701
  '',
1702
+ '## Media proof',
1703
+ ...renderProofMarkdownList(summary.mediaProof || []),
1704
+ '',
1201
1705
  '## Changed surfaces',
1202
1706
  ...renderProofMarkdownList(summary.changed),
1203
1707
  '',
@@ -1230,7 +1734,7 @@ function renderProofMarkdownList(values) {
1230
1734
  function parseProofArgs(args = []) {
1231
1735
  const flags: AnyRecord = {};
1232
1736
  const positionals: string[] = [];
1233
- const aliases = new Set(['goal', 'route', 'truth', 'command', 'evidence', 'visual', 'runtime', 'visual-provenance', 'mission-envelope', 'rollback-hint', 'cleanup', 'changed', 'skipped', 'blocked', 'assumed', 'assumption', 'unverified', 'from', 'format', 'out', 'file', 'json', 'require-runtime', 'require-real-runtime', 'require-tmux', 'require-visual']);
1737
+ const aliases = new Set(['goal', 'route', 'truth', 'command', 'evidence', 'visual', 'runtime', 'runtime-backend', 'runtime-claim', 'runtime-evidence-id', 'runtime-command', 'cleanup-checked', 'runtime-note', 'runtime-backend-evidence', 'visual-provenance', 'mission-envelope', 'rollback-hint', 'media-proof', 'cleanup', 'changed', 'skipped', 'blocked', 'assumed', 'assumption', 'unverified', 'from', 'format', 'out', 'file', 'json', 'require-runtime', 'require-real-runtime', 'require-tmux', 'require-visual']);
1234
1738
  for (let index = 0; index < args.length; index += 1) {
1235
1739
  const arg = args[index];
1236
1740
  if (arg.startsWith('--')) {
@@ -1238,13 +1742,13 @@ function parseProofArgs(args = []) {
1238
1742
  const key = rawKey.slice(2);
1239
1743
  if (!aliases.has(key)) continue;
1240
1744
  const normalizedKey = key.replace(/-/g, '_');
1241
- if (key === 'json' || key.startsWith('require-')) {
1745
+ if (key === 'json' || key.startsWith('require-') || key === 'cleanup-checked') {
1242
1746
  flags[normalizedKey] = true;
1243
1747
  continue;
1244
1748
  }
1245
1749
  const value = inlineValue ?? args[index + 1] ?? '';
1246
1750
  if (inlineValue === undefined) index += 1;
1247
- if (['command', 'evidence', 'visual', 'runtime', 'visual-provenance', 'mission-envelope', 'rollback-hint', 'changed', 'skipped', 'blocked', 'assumed', 'assumption', 'unverified'].includes(key)) {
1751
+ if (['command', 'evidence', 'visual', 'runtime', 'runtime-backend-evidence', 'visual-provenance', 'mission-envelope', 'rollback-hint', 'media-proof', 'changed', 'skipped', 'blocked', 'assumed', 'assumption', 'unverified'].includes(key)) {
1248
1752
  flags[normalizedKey] = [...arrayFlag(flags[normalizedKey]), value];
1249
1753
  } else {
1250
1754
  flags[normalizedKey] = value;
@@ -1282,9 +1786,11 @@ function emptyProofArtifact() {
1282
1786
  evidence: [],
1283
1787
  visual: [],
1284
1788
  runtime: [],
1789
+ runtimeBackendEvidence: [],
1285
1790
  visualProvenance: [],
1286
1791
  missionEnvelope: [],
1287
1792
  rollbackHint: [],
1793
+ mediaProof: [],
1288
1794
  cleanup: '',
1289
1795
  changed: [],
1290
1796
  skipped: [],
@@ -1307,9 +1813,11 @@ async function parseProofJson(file, empty) {
1307
1813
  evidence: arrayFlag(data.evidence),
1308
1814
  visual: arrayFlag(data.visual || data.visualEvidence),
1309
1815
  runtime: arrayFlag(data.runtime || data.runtimeEvidence),
1816
+ runtimeBackendEvidence: structuredEvidenceList(data.runtimeBackendEvidence || data.runtime_backend_evidence),
1310
1817
  visualProvenance: structuredEvidenceList(data.visualProvenance || data.visual_provenance),
1311
1818
  missionEnvelope: structuredEvidenceList(data.missionEnvelope || data.mission_envelope),
1312
1819
  rollbackHint: structuredEvidenceList(data.rollbackHint || data.rollback_hint),
1820
+ mediaProof: structuredEvidenceList(data.mediaProof || data.media_proof),
1313
1821
  cleanup: String(data.cleanup || data.cleanupStatus || ''),
1314
1822
  changed: arrayFlag(data.changed || data.files),
1315
1823
  skipped: arrayFlag(data.skipped),
@@ -1335,9 +1843,11 @@ async function parseProofMarkdown(file, empty) {
1335
1843
  evidence: readProofList(text, 'Evidence'),
1336
1844
  visual: readProofList(text, 'Visual evidence'),
1337
1845
  runtime: firstProofList(text, ['Runtime evidence', 'Processes', 'tmux']),
1846
+ runtimeBackendEvidence: readProofList(text, 'Runtime backend evidence'),
1338
1847
  visualProvenance: readProofList(text, 'Visual provenance'),
1339
1848
  missionEnvelope: readProofList(text, 'Mission patch envelope'),
1340
1849
  rollbackHint: readProofList(text, 'Rollback hint'),
1850
+ mediaProof: readProofList(text, 'Media proof'),
1341
1851
  cleanup: readProofField(text, 'Cleanup') || readProofField(text, 'Cleanup status'),
1342
1852
  changed: firstProofList(text, ['Changed surfaces', 'Files']),
1343
1853
  skipped: readProofList(text, 'Skipped'),
@@ -1388,6 +1898,18 @@ function structuredEvidenceList(value) {
1388
1898
  return arrayFlag(value).map((item) => formatStructuredEvidence(item)).filter(Boolean);
1389
1899
  }
1390
1900
 
1901
+ function directRuntimeBackendEvidence(flags) {
1902
+ if (!flags.runtime_backend && !flags.runtime_claim && !flags.runtime_evidence_id && !flags.runtime_command && !flags.runtime_note && !flags.cleanup_checked) return [];
1903
+ return [JSON.stringify(buildRuntimeBackendEvidence({
1904
+ backend: flags.runtime_backend,
1905
+ claim: flags.runtime_claim,
1906
+ evidence_id: flags.runtime_evidence_id,
1907
+ command: flags.runtime_command,
1908
+ cleanup_checked: Boolean(flags.cleanup_checked),
1909
+ note: flags.runtime_note
1910
+ }))];
1911
+ }
1912
+
1391
1913
  function formatStructuredEvidence(value) {
1392
1914
  if (!value) return '';
1393
1915
  if (typeof value === 'object') return JSON.stringify(value);
@@ -1405,6 +1927,12 @@ function formatStructuredEvidence(value) {
1405
1927
  if (data.schema === 'yam.rollback-hint.v1' || data.touched_files || data.safe_revert_note) {
1406
1928
  return JSON.stringify(buildRollbackHint(data));
1407
1929
  }
1930
+ if (data.schema === 'yam.runtime-backend-evidence.v1' || data.backend || data.evidence_id) {
1931
+ return JSON.stringify(buildRuntimeBackendEvidence(data));
1932
+ }
1933
+ if (data.schema === 'yam.media-generation-proof.v1' || data.generation_requested || data.output_hash) {
1934
+ return JSON.stringify(buildMediaGenerationProof(data));
1935
+ }
1408
1936
  return JSON.stringify(data);
1409
1937
  } catch {
1410
1938
  return text;
@@ -1413,7 +1941,7 @@ function formatStructuredEvidence(value) {
1413
1941
 
1414
1942
  function hasProofEvidence(parsed) {
1415
1943
  const flags = parsed.flags || {};
1416
- return ['command', 'evidence', 'visual', 'runtime', 'visual_provenance', 'mission_envelope', 'rollback_hint', 'changed', 'skipped', 'blocked'].some((key) => arrayFlag(flags[key]).length > 0) || Boolean(flags.cleanup);
1944
+ return ['command', 'evidence', 'visual', 'runtime', 'runtime_backend_evidence', 'visual_provenance', 'mission_envelope', 'rollback_hint', 'media_proof', 'changed', 'skipped', 'blocked'].some((key) => arrayFlag(flags[key]).length > 0) || Boolean(flags.cleanup || flags.runtime_backend || flags.runtime_claim);
1417
1945
  }
1418
1946
 
1419
1947
  function printProofList(label, values) {
@@ -1665,7 +2193,8 @@ async function detectProject(targetDir = process.cwd(), { quiet = false } = {})
1665
2193
  lint: null,
1666
2194
  test: null,
1667
2195
  build: null
1668
- }
2196
+ },
2197
+ frameworkChecklist: null
1669
2198
  };
1670
2199
  if (!result.packageJson) {
1671
2200
  if (!quiet) {
@@ -1683,6 +2212,7 @@ async function detectProject(targetDir = process.cwd(), { quiet = false } = {})
1683
2212
  result.commands.lint = runCommand(result.packageManager, pickScript(scripts, ['lint']));
1684
2213
  result.commands.test = runCommand(result.packageManager, pickScript(scripts, ['test', 'spec']));
1685
2214
  result.commands.build = runCommand(result.packageManager, pickScript(scripts, ['build']));
2215
+ result.frameworkChecklist = await detectFrameworkChecklist(dir, pkg);
1686
2216
 
1687
2217
  if (!quiet) printDetection(result);
1688
2218
  return result;
@@ -1701,6 +2231,13 @@ function printDetection(result) {
1701
2231
  console.log(`- $quick: ${result.commands.typecheck || result.commands.lint || result.commands.test || result.commands.build || 'Level 0 read/inspect; no command detected'}`);
1702
2232
  console.log(`- $ueye: ${result.commands.typecheck || result.commands.build || 'Browser/screenshot check; no command detected'}`);
1703
2233
  console.log(`- $deep: ${[result.commands.typecheck, result.commands.lint, result.commands.test, result.commands.build].filter(Boolean).join(' && ') || `No command detected; define in ${PROJECT_PACK}`}`);
2234
+ if (result.frameworkChecklist?.detected) {
2235
+ console.log('');
2236
+ console.log(`Framework checklist: ${result.frameworkChecklist.framework}`);
2237
+ for (const check of result.frameworkChecklist.checks) {
2238
+ console.log(`- ${check.id}: ${check.label} (${check.route})`);
2239
+ }
2240
+ }
1704
2241
  }
1705
2242
 
1706
2243
  function budget(routeArg = '') {
@@ -2105,6 +2642,8 @@ async function main() {
2105
2642
  if (command === 'measure') return measure(process.argv[3], process.argv.slice(4));
2106
2643
  if (command === 'tools') return tools(process.argv.slice(3));
2107
2644
  if (command === 'proof') return proof(process.argv.slice(3));
2645
+ if (command === 'ueye') return ueye(process.argv.slice(3));
2646
+ if (command === 'media') return media(process.argv.slice(3));
2108
2647
  if (command === 'release') return release(process.argv.slice(3));
2109
2648
  if (command === 'safety') return safety(process.argv.slice(3));
2110
2649
  if (command === 'memory') return memory(process.argv.slice(3));
@@ -2118,7 +2657,7 @@ async function main() {
2118
2657
  }
2119
2658
  if (command === 'list') return list();
2120
2659
  if (command === 'verify') return verify();
2121
- if (command === 'doctor') return doctor();
2660
+ if (command === 'doctor') return doctor(process.argv.slice(3));
2122
2661
  if (command === 'examples') return examples();
2123
2662
  if (command === 'path') return showPath();
2124
2663
  if (command === 'init-project') return initProject(process.argv[3]);