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/dist/bin/yam.js CHANGED
@@ -1,12 +1,14 @@
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
- import { TRUTH_STATUSES, buildMissionPatchEnvelope, buildRollbackHint, buildUeyeVisualProvenance, buildYamCompletionProof, detectDbSafetyText as detectTrustDbSafetyText, isTruthStatus } from '../lib/trust-kernel.js';
11
+ import { TRUTH_STATUSES, buildMediaGenerationProof, buildMissionPatchEnvelope, buildRollbackHint, buildRuntimeBackendEvidence, buildUeyeVisualProvenance, buildUeyeRunReport, buildYamCompletionProof, detectDbSafetyText as detectTrustDbSafetyText, isTruthStatus } from '../lib/trust-kernel.js';
10
12
  function errorMessage(error) {
11
13
  return error instanceof Error ? error.message : String(error);
12
14
  }
@@ -118,6 +120,10 @@ Usage:
118
120
  yam tools doctor [dir]
119
121
  yam proof [dir|--from file] [--route route] [--truth status] [--command text] [--evidence text]
120
122
  yam proof write [dir] [--format json|md] [--out file] [--route route] [--truth status] [--command text]
123
+ yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
124
+ yam ueye compare --reference ref.png --actual screenshot.png [--json]
125
+ yam ueye report [--reference ref.png] [--actual screenshot.png] [--json]
126
+ yam media proof [--requested] [--attempted] [--output file] [--json]
121
127
  yam release report [--json]
122
128
  yam safety [text...]
123
129
  yam memory <init|add|list|summary|resolve> [dir] [options]
@@ -126,7 +132,7 @@ Usage:
126
132
  yam tune-log [dir]
127
133
  yam install
128
134
  yam uninstall
129
- yam doctor
135
+ yam doctor [--json]
130
136
  yam examples
131
137
  yam path
132
138
  yam version
@@ -355,7 +361,34 @@ function failOrReport(label, issues, quiet = false) {
355
361
  process.exitCode = 1;
356
362
  return issues.length;
357
363
  }
358
- async function doctor() {
364
+ async function doctor(args = []) {
365
+ const flags = parseSimpleFlags(args, new Set(['json']));
366
+ const report = await buildDoctorReport();
367
+ if (flags.json) {
368
+ console.log(JSON.stringify(report, null, 2));
369
+ if (!report.ok)
370
+ process.exitCode = 1;
371
+ return;
372
+ }
373
+ if (report.ok) {
374
+ console.log('yam doctor: ok');
375
+ console.log('No hooks, automations, or global config are required.');
376
+ console.log('Skill install state is reported by `yam status`.');
377
+ console.log(`yam-lite hook: ${report.yamLiteHook} (optional)`);
378
+ return;
379
+ }
380
+ console.log('yam doctor: issues');
381
+ for (const issue of report.issues)
382
+ console.log(`- ${issue}`);
383
+ if (report.nextActions.length) {
384
+ console.log('');
385
+ console.log('Next actions:');
386
+ for (const action of report.nextActions)
387
+ console.log(`- ${action}`);
388
+ }
389
+ process.exitCode = 1;
390
+ }
391
+ async function buildDoctorReport() {
359
392
  const issues = [];
360
393
  for (const skill of SKILLS) {
361
394
  if (!await exists(path.join(ROOT, 'skills', skill, 'SKILL.md')))
@@ -386,18 +419,33 @@ async function doctor() {
386
419
  const verifyIssues = await verify({ quiet: true });
387
420
  if (verifyIssues > 0)
388
421
  issues.push(`verify reported ${verifyIssues} issue(s)`);
389
- if (!issues.length) {
390
- console.log('yam doctor: ok');
391
- const globalHook = await readJsonOrDefault(path.join(os.homedir(), '.codex', 'hooks.json'), {});
392
- console.log('No hooks, automations, or global config are required.');
393
- console.log('Skill install state is reported by `yam status`.');
394
- console.log(`yam-lite hook: ${hookConfigHasYamLite(globalHook) ? 'enabled' : 'disabled'} (optional)`);
395
- return;
396
- }
397
- console.log('yam doctor: issues');
398
- for (const issue of issues)
399
- console.log(`- ${issue}`);
400
- process.exitCode = 1;
422
+ const globalHook = await readJsonOrDefault(path.join(os.homedir(), '.codex', 'hooks.json'), {});
423
+ return {
424
+ schema: 'yam.doctor.v1',
425
+ generatedAt: new Date().toISOString(),
426
+ ok: issues.length === 0,
427
+ issues,
428
+ nextActions: doctorNextActions(issues),
429
+ yamLiteHook: hookConfigHasYamLite(globalHook) ? 'enabled' : 'disabled'
430
+ };
431
+ }
432
+ function doctorNextActions(issues = []) {
433
+ const actions = [];
434
+ for (const issue of issues) {
435
+ if (/missing source skill/i.test(issue))
436
+ actions.push('run from a complete yam-flow checkout or reinstall the published package');
437
+ else if (/missing truth matrix/i.test(issue))
438
+ actions.push('restore references/truth-matrix.md before publishing');
439
+ else if (/unexpected local hooks/i.test(issue))
440
+ actions.push('remove project hooks unless this repo is intentionally dogfooding them');
441
+ else if (/unexpected .*automation/i.test(issue))
442
+ actions.push('remove stale yam/timeto automations before claiming clean install state');
443
+ else if (/legacy|retired/i.test(issue))
444
+ actions.push('run `yam install` to replace old skill entries');
445
+ else if (/verify reported/i.test(issue))
446
+ actions.push('run `npm run verify` and fix the reported package boundary or metadata issue');
447
+ }
448
+ return [...new Set(actions)];
401
449
  }
402
450
  async function tools(args = []) {
403
451
  const subcommand = args[0] || 'doctor';
@@ -456,6 +504,7 @@ async function buildToolsDoctorReport(targetDir = process.cwd()) {
456
504
  packageInfo.packageJson ? JSON.stringify(packageInfo.pkg?.devDependencies || {}) : ''
457
505
  ].join('\n'));
458
506
  const projectSurfaces = await detectProjectToolSurfaces(dir, packageInfo.pkg);
507
+ const frameworkChecklist = await detectFrameworkChecklist(dir, packageInfo.pkg);
459
508
  const sqlScan = await scanSqlFiles(dir);
460
509
  const rows = [
461
510
  readinessRow('Codex home', await exists(codexHome) ? 'ready' : 'missing', codexHome),
@@ -485,8 +534,16 @@ async function buildToolsDoctorReport(targetDir = process.cwd()) {
485
534
  packageManager: detection.packageJson ? detection.packageManager : null,
486
535
  instructionSurfaces,
487
536
  projectSurfaces,
537
+ frameworkChecklist,
488
538
  sqlScan,
489
539
  riskNotes,
540
+ nextActions: toolsDoctorNextActions({
541
+ pack,
542
+ rows,
543
+ riskNotes,
544
+ frameworkChecklist,
545
+ commands: detection.packageJson ? detection.commands : {}
546
+ }),
490
547
  routeRecommendations: {
491
548
  dbSupabase: safety.hits.length || sqlScan.findings.length ? '$deep required before claiming safe' : '$deep when destructive or production mutation appears',
492
549
  currentDocs: 'use current-docs proof only when version/freshness matters',
@@ -518,6 +575,13 @@ function printToolsDoctorReport(data) {
518
575
  for (const surface of data.projectSurfaces)
519
576
  console.log(`- ${surface}`);
520
577
  }
578
+ if (data.frameworkChecklist?.detected) {
579
+ console.log('');
580
+ console.log(`Framework checklist: ${data.frameworkChecklist.framework}`);
581
+ for (const check of data.frameworkChecklist.checks) {
582
+ console.log(`- ${check.id}: ${check.label} (${check.route})`);
583
+ }
584
+ }
521
585
  if (data.sqlScan.filesScanned > 0) {
522
586
  console.log('');
523
587
  console.log(`SQL scan: ${data.sqlScan.filesScanned} file(s), ${data.sqlScan.findings.length} risk finding(s)`);
@@ -531,6 +595,12 @@ function printToolsDoctorReport(data) {
531
595
  for (const note of data.riskNotes)
532
596
  console.log(`- ${note.level}: ${note.reason}`);
533
597
  }
598
+ if (data.nextActions?.length) {
599
+ console.log('');
600
+ console.log('Next actions:');
601
+ for (const action of data.nextActions)
602
+ console.log(`- ${action}`);
603
+ }
534
604
  console.log('');
535
605
  console.log('Route recommendations:');
536
606
  console.log(`- DB/Supabase destructive or production write work: ${data.routeRecommendations.dbSupabase}`);
@@ -607,6 +677,53 @@ async function detectProjectToolSurfaces(dir, pkg = null) {
607
677
  surfaces.push('.env file detected; values were not read');
608
678
  return surfaces;
609
679
  }
680
+ async function detectFrameworkChecklist(dir, pkg = null) {
681
+ const deps = {
682
+ ...pkg?.dependencies,
683
+ ...pkg?.devDependencies
684
+ };
685
+ const depNames = Object.keys(deps || {});
686
+ const hasReact = depNames.includes('react');
687
+ 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'));
688
+ const hasVite = depNames.includes('vite') || await exists(path.join(dir, 'vite.config.js')) || await exists(path.join(dir, 'vite.config.ts'));
689
+ const framework = hasNext ? 'next-app' : hasReact && hasVite ? 'react-vite' : hasReact ? 'react' : pkg ? 'node-package' : 'unknown';
690
+ const uiLike = hasReact || hasNext || hasVite || await exists(path.join(dir, 'app')) || await exists(path.join(dir, 'src', 'app')) || await exists(path.join(dir, 'pages'));
691
+ const checks = [];
692
+ if (uiLike) {
693
+ checks.push(checklistRow('states', 'default/loading/error/empty states are reachable before claiming complete', '$ueye'), checklistRow('responsive', 'mobile and desktop layouts avoid overlap and clipped text', '$ueye'), checklistRow('accessibility', 'interactive controls have accessible labels and keyboard path', '$ueye'), checklistRow('runtime', 'browser or in-app runtime evidence is recorded when claiming visual verified', '$ueye'));
694
+ }
695
+ if (hasNext) {
696
+ checks.push(checklistRow('boundaries', 'server/client boundaries and async data states match the framework pattern', '$deep when risky'));
697
+ }
698
+ if (pkg) {
699
+ checks.push(checklistRow('package', 'run the smallest honest type/build/package check before publishing', '$quick or $deep'));
700
+ }
701
+ return {
702
+ schema: 'yam.framework-checklist.v1',
703
+ detected: checks.length > 0,
704
+ uiLike,
705
+ framework,
706
+ routeHint: uiLike ? '$ueye for visual claims; $deep when runtime/data risk appears' : '$quick by default; deepen on risk',
707
+ checks
708
+ };
709
+ }
710
+ function checklistRow(id, label, route) {
711
+ return { id, label, route };
712
+ }
713
+ function toolsDoctorNextActions({ pack, rows, riskNotes, frameworkChecklist, commands }) {
714
+ const actions = [];
715
+ if (!pack)
716
+ actions.push('create or refresh yam.project.md so route choices reuse local project context');
717
+ if (rows.some((row) => row.name === 'Yam skills' && row.status !== 'ready'))
718
+ actions.push('run `yam install` and restart Codex before relying on installed skills');
719
+ if (riskNotes.some((note) => note.level === 'danger'))
720
+ actions.push('use $deep before claiming destructive database or production write safety');
721
+ if (frameworkChecklist?.detected && !commands?.build && !commands?.typecheck)
722
+ actions.push('add or document a small build/typecheck command for implementation verification');
723
+ if (frameworkChecklist?.uiLike)
724
+ actions.push('for Ueye verified claims, record reference, actual screenshot, and comparison result');
725
+ return [...new Set(actions)];
726
+ }
610
727
  async function scanSqlFiles(rootDir, { maxFiles = 30, maxBytes = 200000, maxDepth = 5 } = {}) {
611
728
  const root = path.resolve(rootDir || process.cwd());
612
729
  const files = [];
@@ -1017,6 +1134,390 @@ async function readStdinTextIfAvailable() {
1017
1134
  chunks.push(Buffer.from(chunk));
1018
1135
  return Buffer.concat(chunks).toString('utf8').trim();
1019
1136
  }
1137
+ async function ueye(args = []) {
1138
+ const subcommand = args[0] || 'help';
1139
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h')
1140
+ return ueyeUsage();
1141
+ if (subcommand === 'capture')
1142
+ return ueyeCapture(args.slice(1));
1143
+ if (subcommand === 'compare')
1144
+ return ueyeCompare(args.slice(1));
1145
+ if (subcommand === 'report')
1146
+ return ueyeReport(args.slice(1));
1147
+ console.error(`unknown ueye command: ${subcommand}`);
1148
+ return ueyeUsage();
1149
+ }
1150
+ function ueyeUsage() {
1151
+ console.log(`yam ueye
1152
+
1153
+ Opt-in visual evidence helpers. Ueye stays one skill: fast by default, capture/compare only when verified visual claims need real evidence.
1154
+
1155
+ Usage:
1156
+ yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
1157
+ yam ueye compare --reference ref.png --actual screenshot.png [--json]
1158
+ yam ueye report [--reference ref.png] [--actual screenshot.png] [--design-quality pass|needs-polish|fails|not-checked] [--json]
1159
+
1160
+ Notes:
1161
+ capture uses a locally available Playwright install when present. It does not download browsers or install dependencies.
1162
+ compare uses local files only and reports sha256, dimensions, comparison_result, and proof-ready visual provenance.
1163
+ report produces a proof-ready Ueye visual run report without requiring a new capture.
1164
+ `);
1165
+ }
1166
+ async function ueyeCapture(args = []) {
1167
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h')
1168
+ return ueyeUsage();
1169
+ const flags = parseSimpleFlags(args, new Set(['url', 'out', 'viewport', 'wait-until', 'timeout', 'full-page', 'json']));
1170
+ const url = String(flags.url || '');
1171
+ const out = String(flags.out || '');
1172
+ if (!url || !out) {
1173
+ console.error('usage: yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]');
1174
+ process.exitCode = 1;
1175
+ return;
1176
+ }
1177
+ const target = path.resolve(expandHome(out));
1178
+ const viewport = parseViewport(String(flags.viewport || '1440x900'));
1179
+ const timeout = Number(flags.timeout || 30000);
1180
+ const waitUntil = String(flags.wait_until || 'networkidle');
1181
+ let browser;
1182
+ try {
1183
+ const playwright = await loadOptionalPlaywright();
1184
+ const chromium = playwright?.chromium;
1185
+ if (!chromium)
1186
+ throw new Error('Playwright chromium launcher was not found');
1187
+ await fsp.mkdir(path.dirname(target), { recursive: true });
1188
+ browser = await chromium.launch({ headless: true });
1189
+ const page = await browser.newPage({ viewport });
1190
+ await page.goto(url, { waitUntil, timeout });
1191
+ await page.screenshot({ path: target, fullPage: Boolean(flags.full_page) });
1192
+ const info = await imageFileInfo(target);
1193
+ const result = {
1194
+ schema: 'yam.ueye-capture.v1',
1195
+ status: 'verified',
1196
+ url,
1197
+ out: target,
1198
+ viewport: `${viewport.width}x${viewport.height}`,
1199
+ full_page: Boolean(flags.full_page),
1200
+ sha256: info.sha256,
1201
+ dimensions: info.dimensions,
1202
+ visual_evidence: `browser screenshot captured: ${target} (${info.dimensions}, sha256:${info.sha256})`
1203
+ };
1204
+ printJsonOrHuman(result, Boolean(flags.json), 'Ueye capture');
1205
+ }
1206
+ catch (error) {
1207
+ const result = {
1208
+ schema: 'yam.ueye-capture.v1',
1209
+ status: 'blocked',
1210
+ url,
1211
+ out: target,
1212
+ reason: errorMessage(error),
1213
+ next_action: 'Install Playwright in the current project or provide a user/browser screenshot, then use `yam ueye compare`.',
1214
+ visual_cap: 'blocked'
1215
+ };
1216
+ printJsonOrHuman(result, Boolean(flags.json), 'Ueye capture');
1217
+ process.exitCode = 1;
1218
+ }
1219
+ finally {
1220
+ if (browser)
1221
+ await browser.close().catch(() => { });
1222
+ }
1223
+ }
1224
+ async function ueyeCompare(args = []) {
1225
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h')
1226
+ return ueyeUsage();
1227
+ const flags = parseSimpleFlags(args, new Set(['reference', 'ref', 'actual', 'screenshot', 'json', 'reference-id', 'screenshot-id']));
1228
+ const reference = String(flags.reference || flags.ref || '');
1229
+ const actual = String(flags.actual || flags.screenshot || '');
1230
+ if (!reference || !actual) {
1231
+ console.error('usage: yam ueye compare --reference ref.png --actual screenshot.png [--json]');
1232
+ process.exitCode = 1;
1233
+ return;
1234
+ }
1235
+ const referencePath = path.resolve(expandHome(reference));
1236
+ const actualPath = path.resolve(expandHome(actual));
1237
+ try {
1238
+ const referenceInfo = await imageFileInfo(referencePath);
1239
+ const actualInfo = await imageFileInfo(actualPath);
1240
+ if (referenceInfo.dimensions === 'unknown' || actualInfo.dimensions === 'unknown') {
1241
+ throw new Error('compare requires image files with readable PNG, JPEG, or GIF dimensions');
1242
+ }
1243
+ const exactMatch = referenceInfo.sha256 === actualInfo.sha256;
1244
+ const sameDimensions = referenceInfo.dimensions === actualInfo.dimensions;
1245
+ const comparisonResult = exactMatch ? 'matched' : 'different';
1246
+ const truthStatus = exactMatch ? 'verified' : 'partial';
1247
+ const provenance = buildUeyeVisualProvenance({
1248
+ source_kind: 'implementation_screenshot',
1249
+ source_path: actualPath,
1250
+ source_hash: actualInfo.sha256,
1251
+ reference_id: String(flags.reference_id || path.basename(referencePath)),
1252
+ screenshot_id: String(flags.screenshot_id || path.basename(actualPath)),
1253
+ local_only: true,
1254
+ redacted: false,
1255
+ operator_provided: false,
1256
+ comparison_result: comparisonResult,
1257
+ truth_status: truthStatus
1258
+ });
1259
+ const result = {
1260
+ schema: 'yam.ueye-compare.v1',
1261
+ status: truthStatus,
1262
+ comparison_result: comparisonResult,
1263
+ exact_match: exactMatch,
1264
+ same_dimensions: sameDimensions,
1265
+ reference: referenceInfo,
1266
+ actual: actualInfo,
1267
+ visual_evidence: `browser/local screenshot comparison executed: reference=${referencePath}, actual=${actualPath}, result=${comparisonResult}`,
1268
+ visual_provenance: provenance,
1269
+ proof_hint: `yam proof --route ueye --truth ${truthStatus} --visual "browser/local screenshot comparison executed: ${comparisonResult}" --visual-provenance '${JSON.stringify(provenance)}' --require-visual`
1270
+ };
1271
+ printJsonOrHuman(result, Boolean(flags.json), 'Ueye compare');
1272
+ }
1273
+ catch (error) {
1274
+ const result = {
1275
+ schema: 'yam.ueye-compare.v1',
1276
+ status: 'blocked',
1277
+ comparison_result: 'not-verified',
1278
+ reason: errorMessage(error),
1279
+ visual_cap: 'blocked'
1280
+ };
1281
+ printJsonOrHuman(result, Boolean(flags.json), 'Ueye compare');
1282
+ process.exitCode = 1;
1283
+ }
1284
+ }
1285
+ async function ueyeReport(args = []) {
1286
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h')
1287
+ return ueyeUsage();
1288
+ const flags = parseSimpleFlags(args, new Set(['reference', 'ref', 'actual', 'screenshot', 'capture-backend', 'compare-backend', 'design-quality', 'blocked-reason', 'next-action', 'json']));
1289
+ const reference = String(flags.reference || flags.ref || '');
1290
+ const actual = String(flags.actual || flags.screenshot || '');
1291
+ const referenceSources = [];
1292
+ const implementationSources = [];
1293
+ let comparisonResult = 'not-verified';
1294
+ let blockedReason = String(flags.blocked_reason || '');
1295
+ try {
1296
+ if (reference) {
1297
+ const info = await imageFileInfo(reference);
1298
+ referenceSources.push(buildUeyeVisualProvenance({
1299
+ source_kind: 'reference_image',
1300
+ source_path: info.path,
1301
+ source_hash: info.sha256,
1302
+ reference_id: path.basename(info.path),
1303
+ local_only: true,
1304
+ operator_provided: true,
1305
+ comparison_result: 'reference-recorded',
1306
+ truth_status: 'partial'
1307
+ }));
1308
+ }
1309
+ if (actual) {
1310
+ const info = await imageFileInfo(actual);
1311
+ implementationSources.push(buildUeyeVisualProvenance({
1312
+ source_kind: 'implementation_screenshot',
1313
+ source_path: info.path,
1314
+ source_hash: info.sha256,
1315
+ screenshot_id: path.basename(info.path),
1316
+ local_only: true,
1317
+ operator_provided: false,
1318
+ comparison_result: 'implementation-recorded',
1319
+ truth_status: 'partial'
1320
+ }));
1321
+ }
1322
+ if (reference && actual && referenceSources[0]?.source_hash === implementationSources[0]?.source_hash) {
1323
+ comparisonResult = 'matched';
1324
+ }
1325
+ else if (reference && actual) {
1326
+ comparisonResult = 'different';
1327
+ }
1328
+ }
1329
+ catch (error) {
1330
+ blockedReason = errorMessage(error);
1331
+ }
1332
+ const report = buildUeyeRunReport({
1333
+ reference_sources: referenceSources,
1334
+ implementation_sources: implementationSources,
1335
+ comparison_result: comparisonResult,
1336
+ design_quality: String(flags.design_quality || 'not-checked'),
1337
+ blocked_reason: blockedReason,
1338
+ next_action: String(flags.next_action || '')
1339
+ });
1340
+ const result = {
1341
+ ...report,
1342
+ capture_backend: String(flags.capture_backend || 'not-recorded'),
1343
+ compare_backend: String(flags.compare_backend || 'local-file-hash')
1344
+ };
1345
+ printJsonOrHuman(result, Boolean(flags.json), 'Ueye report');
1346
+ if (report.truth_status === 'blocked')
1347
+ process.exitCode = 1;
1348
+ }
1349
+ async function media(args = []) {
1350
+ const subcommand = args[0] || 'help';
1351
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h')
1352
+ return mediaUsage();
1353
+ if (subcommand === 'proof')
1354
+ return mediaProof(args.slice(1));
1355
+ console.error(`unknown media command: ${subcommand}`);
1356
+ return mediaUsage();
1357
+ }
1358
+ function mediaUsage() {
1359
+ console.log(`yam media
1360
+
1361
+ Opt-in media generation proof helpers. Generated media can support visual direction, but it cannot verify implemented UI without implementation screenshot evidence.
1362
+
1363
+ Usage:
1364
+ yam media proof [--tool name] [--requested] [--attempted] [--output file] [--wait-loop] [--blocked-reason text] [--json]
1365
+ `);
1366
+ }
1367
+ async function mediaProof(args = []) {
1368
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h')
1369
+ return mediaUsage();
1370
+ const flags = parseSimpleFlags(args, new Set(['tool', 'requested', 'attempted', 'output', 'wait-loop', 'blocked-reason', 'next-action', 'json']));
1371
+ const output = String(flags.output || '');
1372
+ let outputHash = 'unknown';
1373
+ let outputPath = '';
1374
+ let blockedReason = String(flags.blocked_reason || '');
1375
+ if (output) {
1376
+ try {
1377
+ const info = await imageFileInfo(output);
1378
+ outputHash = info.sha256;
1379
+ outputPath = info.path;
1380
+ }
1381
+ catch (error) {
1382
+ blockedReason = blockedReason || errorMessage(error);
1383
+ outputPath = path.resolve(expandHome(output));
1384
+ }
1385
+ }
1386
+ const proof = buildMediaGenerationProof({
1387
+ tool_name: String(flags.tool || ''),
1388
+ generation_requested: Boolean(flags.requested),
1389
+ generation_attempted: Boolean(flags.attempted || output),
1390
+ output_path: outputPath,
1391
+ output_hash: outputHash,
1392
+ wait_loop_checked: Boolean(flags.wait_loop),
1393
+ blocked_reason: blockedReason,
1394
+ next_action: String(flags.next_action || '')
1395
+ });
1396
+ printJsonOrHuman(proof, Boolean(flags.json), 'Media proof');
1397
+ if (proof.truth_status === 'blocked')
1398
+ process.exitCode = 1;
1399
+ }
1400
+ function parseSimpleFlags(args = [], allowed = new Set()) {
1401
+ const flags = {};
1402
+ const positionals = [];
1403
+ for (let index = 0; index < args.length; index += 1) {
1404
+ const arg = String(args[index] || '');
1405
+ if (!arg.startsWith('--')) {
1406
+ positionals.push(arg);
1407
+ continue;
1408
+ }
1409
+ const [rawKey, inlineValue] = arg.includes('=') ? arg.split(/=(.*)/s, 2) : [arg, undefined];
1410
+ const key = rawKey.slice(2);
1411
+ if (allowed.size && !allowed.has(key))
1412
+ continue;
1413
+ const normalizedKey = key.replace(/-/g, '_');
1414
+ if (['json', 'full-page', 'requested', 'attempted', 'available', 'wait-loop', 'cleanup-checked'].includes(key)) {
1415
+ flags[normalizedKey] = true;
1416
+ continue;
1417
+ }
1418
+ const value = inlineValue ?? args[index + 1] ?? '';
1419
+ if (inlineValue === undefined)
1420
+ index += 1;
1421
+ flags[normalizedKey] = value;
1422
+ }
1423
+ flags._ = positionals;
1424
+ return flags;
1425
+ }
1426
+ function parseViewport(value = '1440x900') {
1427
+ const match = String(value || '').match(/^(\d{2,5})x(\d{2,5})$/i);
1428
+ if (!match)
1429
+ return { width: 1440, height: 900 };
1430
+ return { width: Number(match[1]), height: Number(match[2]) };
1431
+ }
1432
+ async function loadOptionalPlaywright() {
1433
+ let projectError = '';
1434
+ try {
1435
+ const projectRequire = createRequire(path.join(process.cwd(), 'package.json'));
1436
+ return projectRequire('playwright');
1437
+ }
1438
+ catch (error) {
1439
+ projectError = errorMessage(error);
1440
+ // Fall through to the yam package context for local development or bundled installs.
1441
+ }
1442
+ const dynamicImport = new Function('specifier', 'return import(specifier)');
1443
+ try {
1444
+ return await dynamicImport('playwright');
1445
+ }
1446
+ catch (error) {
1447
+ throw new Error(`Playwright not found from current project or yam package context. Current project: ${projectError}; package context: ${errorMessage(error)}`);
1448
+ }
1449
+ }
1450
+ async function imageFileInfo(file) {
1451
+ const absolute = path.resolve(expandHome(file));
1452
+ const buffer = await fsp.readFile(absolute);
1453
+ const stat = await fsp.stat(absolute);
1454
+ return {
1455
+ path: absolute,
1456
+ sha256: crypto.createHash('sha256').update(buffer).digest('hex'),
1457
+ bytes: stat.size,
1458
+ dimensions: imageDimensions(buffer)
1459
+ };
1460
+ }
1461
+ function imageDimensions(buffer) {
1462
+ const png = pngDimensions(buffer);
1463
+ if (png)
1464
+ return png;
1465
+ const jpeg = jpegDimensions(buffer);
1466
+ if (jpeg)
1467
+ return jpeg;
1468
+ const gif = gifDimensions(buffer);
1469
+ if (gif)
1470
+ return gif;
1471
+ return 'unknown';
1472
+ }
1473
+ function pngDimensions(buffer) {
1474
+ if (buffer.length < 24)
1475
+ return '';
1476
+ if (buffer.readUInt32BE(0) !== 0x89504e47 || buffer.readUInt32BE(4) !== 0x0d0a1a0a)
1477
+ return '';
1478
+ return `${buffer.readUInt32BE(16)}x${buffer.readUInt32BE(20)}`;
1479
+ }
1480
+ function gifDimensions(buffer) {
1481
+ if (buffer.length < 10)
1482
+ return '';
1483
+ const signature = buffer.toString('ascii', 0, 6);
1484
+ if (signature !== 'GIF87a' && signature !== 'GIF89a')
1485
+ return '';
1486
+ return `${buffer.readUInt16LE(6)}x${buffer.readUInt16LE(8)}`;
1487
+ }
1488
+ function jpegDimensions(buffer) {
1489
+ if (buffer.length < 4 || buffer[0] !== 0xff || buffer[1] !== 0xd8)
1490
+ return '';
1491
+ let offset = 2;
1492
+ while (offset < buffer.length) {
1493
+ if (buffer[offset] !== 0xff)
1494
+ return '';
1495
+ const marker = buffer[offset + 1];
1496
+ const length = buffer.readUInt16BE(offset + 2);
1497
+ if (marker >= 0xc0 && marker <= 0xc3 && offset + 8 < buffer.length) {
1498
+ return `${buffer.readUInt16BE(offset + 7)}x${buffer.readUInt16BE(offset + 5)}`;
1499
+ }
1500
+ offset += 2 + length;
1501
+ }
1502
+ return '';
1503
+ }
1504
+ function printJsonOrHuman(result, json = false, label = 'yam') {
1505
+ if (json) {
1506
+ console.log(JSON.stringify(result, null, 2));
1507
+ return;
1508
+ }
1509
+ console.log(label);
1510
+ for (const [key, value] of Object.entries(result)) {
1511
+ if (key === 'schema')
1512
+ continue;
1513
+ if (typeof value === 'object' && value !== null) {
1514
+ console.log(`- ${key}: ${JSON.stringify(value)}`);
1515
+ }
1516
+ else {
1517
+ console.log(`- ${key}: ${value}`);
1518
+ }
1519
+ }
1520
+ }
1020
1521
  function detectDbSafetyText(text = '') {
1021
1522
  return detectTrustDbSafetyText(text);
1022
1523
  }
@@ -1057,9 +1558,11 @@ async function proof(args = []) {
1057
1558
  printProofList('Evidence', summary.evidence);
1058
1559
  printProofList('Visual evidence', summary.visual);
1059
1560
  printProofList('Runtime evidence', summary.runtime);
1561
+ printProofList('Runtime backend evidence', summary.runtimeBackendEvidence || []);
1060
1562
  printProofList('Visual provenance', summary.visualProvenance || []);
1061
1563
  printProofList('Mission patch envelope', summary.missionEnvelope || []);
1062
1564
  printProofList('Rollback hint', summary.rollbackHint || []);
1565
+ printProofList('Media proof', summary.mediaProof || []);
1063
1566
  printProofList('Changed surfaces', summary.changed);
1064
1567
  printProofList('Skipped', summary.skipped);
1065
1568
  printProofList('Blocked', summary.blocked);
@@ -1135,6 +1638,11 @@ function buildProofSummary(parsed, artifact) {
1135
1638
  evidence: [...artifact.evidence, ...arrayFlag(parsed.flags.evidence)],
1136
1639
  visual: [...artifact.visual, ...arrayFlag(parsed.flags.visual)],
1137
1640
  runtime: [...artifact.runtime, ...arrayFlag(parsed.flags.runtime)],
1641
+ runtimeBackendEvidence: [
1642
+ ...structuredEvidenceList(artifact.runtimeBackendEvidence),
1643
+ ...structuredEvidenceList(parsed.flags.runtime_backend_evidence),
1644
+ ...directRuntimeBackendEvidence(parsed.flags)
1645
+ ],
1138
1646
  visualProvenance: [
1139
1647
  ...structuredEvidenceList(artifact.visualProvenance),
1140
1648
  ...structuredEvidenceList(parsed.flags.visual_provenance)
@@ -1147,6 +1655,10 @@ function buildProofSummary(parsed, artifact) {
1147
1655
  ...structuredEvidenceList(artifact.rollbackHint),
1148
1656
  ...structuredEvidenceList(parsed.flags.rollback_hint)
1149
1657
  ],
1658
+ mediaProof: [
1659
+ ...structuredEvidenceList(artifact.mediaProof),
1660
+ ...structuredEvidenceList(parsed.flags.media_proof)
1661
+ ],
1150
1662
  cleanup: parsed.flags.cleanup || artifact.cleanup || '',
1151
1663
  changed: [...artifact.changed, ...arrayFlag(parsed.flags.changed)],
1152
1664
  skipped: [...artifact.skipped, ...arrayFlag(parsed.flags.skipped)],
@@ -1196,6 +1708,9 @@ function renderProofMarkdown(summary) {
1196
1708
  '## Runtime evidence',
1197
1709
  ...renderProofMarkdownList(summary.runtime),
1198
1710
  '',
1711
+ '## Runtime backend evidence',
1712
+ ...renderProofMarkdownList(summary.runtimeBackendEvidence || []),
1713
+ '',
1199
1714
  '## Visual provenance',
1200
1715
  ...renderProofMarkdownList(summary.visualProvenance || []),
1201
1716
  '',
@@ -1205,6 +1720,9 @@ function renderProofMarkdown(summary) {
1205
1720
  '## Rollback hint',
1206
1721
  ...renderProofMarkdownList(summary.rollbackHint || []),
1207
1722
  '',
1723
+ '## Media proof',
1724
+ ...renderProofMarkdownList(summary.mediaProof || []),
1725
+ '',
1208
1726
  '## Changed surfaces',
1209
1727
  ...renderProofMarkdownList(summary.changed),
1210
1728
  '',
@@ -1235,7 +1753,7 @@ function renderProofMarkdownList(values) {
1235
1753
  function parseProofArgs(args = []) {
1236
1754
  const flags = {};
1237
1755
  const positionals = [];
1238
- 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']);
1756
+ 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']);
1239
1757
  for (let index = 0; index < args.length; index += 1) {
1240
1758
  const arg = args[index];
1241
1759
  if (arg.startsWith('--')) {
@@ -1244,14 +1762,14 @@ function parseProofArgs(args = []) {
1244
1762
  if (!aliases.has(key))
1245
1763
  continue;
1246
1764
  const normalizedKey = key.replace(/-/g, '_');
1247
- if (key === 'json' || key.startsWith('require-')) {
1765
+ if (key === 'json' || key.startsWith('require-') || key === 'cleanup-checked') {
1248
1766
  flags[normalizedKey] = true;
1249
1767
  continue;
1250
1768
  }
1251
1769
  const value = inlineValue ?? args[index + 1] ?? '';
1252
1770
  if (inlineValue === undefined)
1253
1771
  index += 1;
1254
- if (['command', 'evidence', 'visual', 'runtime', 'visual-provenance', 'mission-envelope', 'rollback-hint', 'changed', 'skipped', 'blocked', 'assumed', 'assumption', 'unverified'].includes(key)) {
1772
+ if (['command', 'evidence', 'visual', 'runtime', 'runtime-backend-evidence', 'visual-provenance', 'mission-envelope', 'rollback-hint', 'media-proof', 'changed', 'skipped', 'blocked', 'assumed', 'assumption', 'unverified'].includes(key)) {
1255
1773
  flags[normalizedKey] = [...arrayFlag(flags[normalizedKey]), value];
1256
1774
  }
1257
1775
  else {
@@ -1290,9 +1808,11 @@ function emptyProofArtifact() {
1290
1808
  evidence: [],
1291
1809
  visual: [],
1292
1810
  runtime: [],
1811
+ runtimeBackendEvidence: [],
1293
1812
  visualProvenance: [],
1294
1813
  missionEnvelope: [],
1295
1814
  rollbackHint: [],
1815
+ mediaProof: [],
1296
1816
  cleanup: '',
1297
1817
  changed: [],
1298
1818
  skipped: [],
@@ -1314,9 +1834,11 @@ async function parseProofJson(file, empty) {
1314
1834
  evidence: arrayFlag(data.evidence),
1315
1835
  visual: arrayFlag(data.visual || data.visualEvidence),
1316
1836
  runtime: arrayFlag(data.runtime || data.runtimeEvidence),
1837
+ runtimeBackendEvidence: structuredEvidenceList(data.runtimeBackendEvidence || data.runtime_backend_evidence),
1317
1838
  visualProvenance: structuredEvidenceList(data.visualProvenance || data.visual_provenance),
1318
1839
  missionEnvelope: structuredEvidenceList(data.missionEnvelope || data.mission_envelope),
1319
1840
  rollbackHint: structuredEvidenceList(data.rollbackHint || data.rollback_hint),
1841
+ mediaProof: structuredEvidenceList(data.mediaProof || data.media_proof),
1320
1842
  cleanup: String(data.cleanup || data.cleanupStatus || ''),
1321
1843
  changed: arrayFlag(data.changed || data.files),
1322
1844
  skipped: arrayFlag(data.skipped),
@@ -1342,9 +1864,11 @@ async function parseProofMarkdown(file, empty) {
1342
1864
  evidence: readProofList(text, 'Evidence'),
1343
1865
  visual: readProofList(text, 'Visual evidence'),
1344
1866
  runtime: firstProofList(text, ['Runtime evidence', 'Processes', 'tmux']),
1867
+ runtimeBackendEvidence: readProofList(text, 'Runtime backend evidence'),
1345
1868
  visualProvenance: readProofList(text, 'Visual provenance'),
1346
1869
  missionEnvelope: readProofList(text, 'Mission patch envelope'),
1347
1870
  rollbackHint: readProofList(text, 'Rollback hint'),
1871
+ mediaProof: readProofList(text, 'Media proof'),
1348
1872
  cleanup: readProofField(text, 'Cleanup') || readProofField(text, 'Cleanup status'),
1349
1873
  changed: firstProofList(text, ['Changed surfaces', 'Files']),
1350
1874
  skipped: readProofList(text, 'Skipped'),
@@ -1392,6 +1916,18 @@ function arrayFlag(value) {
1392
1916
  function structuredEvidenceList(value) {
1393
1917
  return arrayFlag(value).map((item) => formatStructuredEvidence(item)).filter(Boolean);
1394
1918
  }
1919
+ function directRuntimeBackendEvidence(flags) {
1920
+ if (!flags.runtime_backend && !flags.runtime_claim && !flags.runtime_evidence_id && !flags.runtime_command && !flags.runtime_note && !flags.cleanup_checked)
1921
+ return [];
1922
+ return [JSON.stringify(buildRuntimeBackendEvidence({
1923
+ backend: flags.runtime_backend,
1924
+ claim: flags.runtime_claim,
1925
+ evidence_id: flags.runtime_evidence_id,
1926
+ command: flags.runtime_command,
1927
+ cleanup_checked: Boolean(flags.cleanup_checked),
1928
+ note: flags.runtime_note
1929
+ }))];
1930
+ }
1395
1931
  function formatStructuredEvidence(value) {
1396
1932
  if (!value)
1397
1933
  return '';
@@ -1413,6 +1949,12 @@ function formatStructuredEvidence(value) {
1413
1949
  if (data.schema === 'yam.rollback-hint.v1' || data.touched_files || data.safe_revert_note) {
1414
1950
  return JSON.stringify(buildRollbackHint(data));
1415
1951
  }
1952
+ if (data.schema === 'yam.runtime-backend-evidence.v1' || data.backend || data.evidence_id) {
1953
+ return JSON.stringify(buildRuntimeBackendEvidence(data));
1954
+ }
1955
+ if (data.schema === 'yam.media-generation-proof.v1' || data.generation_requested || data.output_hash) {
1956
+ return JSON.stringify(buildMediaGenerationProof(data));
1957
+ }
1416
1958
  return JSON.stringify(data);
1417
1959
  }
1418
1960
  catch {
@@ -1421,7 +1963,7 @@ function formatStructuredEvidence(value) {
1421
1963
  }
1422
1964
  function hasProofEvidence(parsed) {
1423
1965
  const flags = parsed.flags || {};
1424
- return ['command', 'evidence', 'visual', 'runtime', 'visual_provenance', 'mission_envelope', 'rollback_hint', 'changed', 'skipped', 'blocked'].some((key) => arrayFlag(flags[key]).length > 0) || Boolean(flags.cleanup);
1966
+ 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);
1425
1967
  }
1426
1968
  function printProofList(label, values) {
1427
1969
  console.log(`- ${label}:`);
@@ -1675,7 +2217,8 @@ async function detectProject(targetDir = process.cwd(), { quiet = false } = {})
1675
2217
  lint: null,
1676
2218
  test: null,
1677
2219
  build: null
1678
- }
2220
+ },
2221
+ frameworkChecklist: null
1679
2222
  };
1680
2223
  if (!result.packageJson) {
1681
2224
  if (!quiet) {
@@ -1692,6 +2235,7 @@ async function detectProject(targetDir = process.cwd(), { quiet = false } = {})
1692
2235
  result.commands.lint = runCommand(result.packageManager, pickScript(scripts, ['lint']));
1693
2236
  result.commands.test = runCommand(result.packageManager, pickScript(scripts, ['test', 'spec']));
1694
2237
  result.commands.build = runCommand(result.packageManager, pickScript(scripts, ['build']));
2238
+ result.frameworkChecklist = await detectFrameworkChecklist(dir, pkg);
1695
2239
  if (!quiet)
1696
2240
  printDetection(result);
1697
2241
  return result;
@@ -1709,6 +2253,13 @@ function printDetection(result) {
1709
2253
  console.log(`- $quick: ${result.commands.typecheck || result.commands.lint || result.commands.test || result.commands.build || 'Level 0 read/inspect; no command detected'}`);
1710
2254
  console.log(`- $ueye: ${result.commands.typecheck || result.commands.build || 'Browser/screenshot check; no command detected'}`);
1711
2255
  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}`}`);
2256
+ if (result.frameworkChecklist?.detected) {
2257
+ console.log('');
2258
+ console.log(`Framework checklist: ${result.frameworkChecklist.framework}`);
2259
+ for (const check of result.frameworkChecklist.checks) {
2260
+ console.log(`- ${check.id}: ${check.label} (${check.route})`);
2261
+ }
2262
+ }
1712
2263
  }
1713
2264
  function budget(routeArg = '') {
1714
2265
  const normalized = normalizeRoute(routeArg);
@@ -2112,6 +2663,10 @@ async function main() {
2112
2663
  return tools(process.argv.slice(3));
2113
2664
  if (command === 'proof')
2114
2665
  return proof(process.argv.slice(3));
2666
+ if (command === 'ueye')
2667
+ return ueye(process.argv.slice(3));
2668
+ if (command === 'media')
2669
+ return media(process.argv.slice(3));
2115
2670
  if (command === 'release')
2116
2671
  return release(process.argv.slice(3));
2117
2672
  if (command === 'safety')
@@ -2135,7 +2690,7 @@ async function main() {
2135
2690
  if (command === 'verify')
2136
2691
  return verify();
2137
2692
  if (command === 'doctor')
2138
- return doctor();
2693
+ return doctor(process.argv.slice(3));
2139
2694
  if (command === 'examples')
2140
2695
  return examples();
2141
2696
  if (command === 'path')