yam-flow 0.1.1 → 0.1.2

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/COMMANDS.md CHANGED
@@ -39,6 +39,14 @@ $ueye
39
39
  yam template ueye-comparison
40
40
  ```
41
41
 
42
+ Opt-in visual evidence helpers:
43
+
44
+ ```bash
45
+ yam ueye capture --url http://localhost:3000 --out .yam/screens/home.png
46
+ yam ueye compare --reference ./reference.png --actual .yam/screens/home.png
47
+ yam proof --route ueye --truth partial --visual "browser/local screenshot comparison executed" --require-visual
48
+ ```
49
+
42
50
  ## Review-Only
43
51
 
44
52
  ```text
@@ -136,6 +144,8 @@ yam proof /path/to/project
136
144
  yam proof --route deep --truth verified --command "npm run build: pass"
137
145
  yam proof --route ueye --truth verified --visual "reference image only"
138
146
  yam proof --route ueye --truth partial --visual-provenance '{"source_kind":"reference","source_hash":"unknown","comparison_result":"not-verified","truth_status":"partial"}'
147
+ yam ueye capture --url http://localhost:3000 --out .yam/screens/home.png
148
+ yam ueye compare --reference ./reference.png --actual .yam/screens/home.png --json
139
149
  yam proof --route mission --mission-envelope '{"agent_id":"implementer","assigned_scope":"target component","changed_files":["src/file.ts"],"verification_hint":"npm run typecheck","truth_status":"partial"}'
140
150
  yam proof --route deep --truth proven --require-runtime --runtime "mock server fixture"
141
151
  yam proof write /path/to/project --route deep --truth partial --command "npm run build: pass"
package/README.md CHANGED
@@ -127,6 +127,8 @@ yam proof /path/to/project
127
127
  yam proof write /path/to/project --route quick --truth verified --command "npm run verify:self: pass"
128
128
  yam proof --route ueye --truth verified --visual "reference image only"
129
129
  yam proof --route ueye --truth partial --visual-provenance '{"source_kind":"reference","source_hash":"unknown","comparison_result":"not-verified","truth_status":"partial"}'
130
+ yam ueye capture --url http://localhost:3000 --out .yam/screens/home.png
131
+ yam ueye compare --reference ./reference.png --actual .yam/screens/home.png
130
132
  yam proof --route mission --mission-envelope '{"agent_id":"implementer","assigned_scope":"target component","changed_files":["src/file.ts"],"verification_hint":"npm run typecheck","truth_status":"partial"}'
131
133
  yam release report --json
132
134
  yam safety "supabase db reset"
@@ -175,3 +177,18 @@ Publishing still requires confirming the final npm package name and account acce
175
177
  - DB/Supabase safety signals
176
178
 
177
179
  It is implemented locally in `src/lib/trust-kernel.ts` and kept route-scoped. It is not an always-on release gate.
180
+
181
+ ## Ueye Capture And Compare
182
+
183
+ `$ueye` stays one skill. It starts fast by default, then uses capture/compare only when a visual claim needs real evidence.
184
+
185
+ ```bash
186
+ yam ueye capture --url http://localhost:3000 --out .yam/screens/home.png
187
+ yam ueye compare --reference ./reference.png --actual .yam/screens/home.png --json
188
+ ```
189
+
190
+ `capture` uses a Playwright install from the current project when present, then falls back to the package context. It does not install browsers or add runtime dependencies by itself. If capture is unavailable, the visual claim stays `partial` or `blocked` until a real screenshot is supplied.
191
+
192
+ `compare` is local-only and dependency-free. It records file hashes, dimensions, comparison result, and proof-ready visual provenance. Exact image matches can be `verified`; different screenshots stay `partial` so visual parity is not overclaimed.
193
+
194
+ For agent-driven visual QA, prefer the Codex in-app Browser plugin. Use the user's Chrome browser only when explicitly requested or when Chrome-only session/profile state is required.
package/dist/bin/yam.js CHANGED
@@ -1,7 +1,9 @@
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';
@@ -118,6 +120,8 @@ 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]
121
125
  yam release report [--json]
122
126
  yam safety [text...]
123
127
  yam memory <init|add|list|summary|resolve> [dir] [options]
@@ -1017,6 +1021,271 @@ async function readStdinTextIfAvailable() {
1017
1021
  chunks.push(Buffer.from(chunk));
1018
1022
  return Buffer.concat(chunks).toString('utf8').trim();
1019
1023
  }
1024
+ async function ueye(args = []) {
1025
+ const subcommand = args[0] || 'help';
1026
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h')
1027
+ return ueyeUsage();
1028
+ if (subcommand === 'capture')
1029
+ return ueyeCapture(args.slice(1));
1030
+ if (subcommand === 'compare')
1031
+ return ueyeCompare(args.slice(1));
1032
+ console.error(`unknown ueye command: ${subcommand}`);
1033
+ return ueyeUsage();
1034
+ }
1035
+ function ueyeUsage() {
1036
+ console.log(`yam ueye
1037
+
1038
+ Opt-in visual evidence helpers. Ueye stays one skill: fast by default, capture/compare only when verified visual claims need real evidence.
1039
+
1040
+ Usage:
1041
+ yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
1042
+ yam ueye compare --reference ref.png --actual screenshot.png [--json]
1043
+
1044
+ Notes:
1045
+ capture uses a locally available Playwright install when present. It does not download browsers or install dependencies.
1046
+ compare uses local files only and reports sha256, dimensions, comparison_result, and proof-ready visual provenance.
1047
+ `);
1048
+ }
1049
+ async function ueyeCapture(args = []) {
1050
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h')
1051
+ return ueyeUsage();
1052
+ const flags = parseSimpleFlags(args, new Set(['url', 'out', 'viewport', 'wait-until', 'timeout', 'full-page', 'json']));
1053
+ const url = String(flags.url || '');
1054
+ const out = String(flags.out || '');
1055
+ if (!url || !out) {
1056
+ console.error('usage: yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]');
1057
+ process.exitCode = 1;
1058
+ return;
1059
+ }
1060
+ const target = path.resolve(expandHome(out));
1061
+ const viewport = parseViewport(String(flags.viewport || '1440x900'));
1062
+ const timeout = Number(flags.timeout || 30000);
1063
+ const waitUntil = String(flags.wait_until || 'networkidle');
1064
+ let browser;
1065
+ try {
1066
+ const playwright = await loadOptionalPlaywright();
1067
+ const chromium = playwright?.chromium;
1068
+ if (!chromium)
1069
+ throw new Error('Playwright chromium launcher was not found');
1070
+ await fsp.mkdir(path.dirname(target), { recursive: true });
1071
+ browser = await chromium.launch({ headless: true });
1072
+ const page = await browser.newPage({ viewport });
1073
+ await page.goto(url, { waitUntil, timeout });
1074
+ await page.screenshot({ path: target, fullPage: Boolean(flags.full_page) });
1075
+ const info = await imageFileInfo(target);
1076
+ const result = {
1077
+ schema: 'yam.ueye-capture.v1',
1078
+ status: 'verified',
1079
+ url,
1080
+ out: target,
1081
+ viewport: `${viewport.width}x${viewport.height}`,
1082
+ full_page: Boolean(flags.full_page),
1083
+ sha256: info.sha256,
1084
+ dimensions: info.dimensions,
1085
+ visual_evidence: `browser screenshot captured: ${target} (${info.dimensions}, sha256:${info.sha256})`
1086
+ };
1087
+ printJsonOrHuman(result, Boolean(flags.json), 'Ueye capture');
1088
+ }
1089
+ catch (error) {
1090
+ const result = {
1091
+ schema: 'yam.ueye-capture.v1',
1092
+ status: 'blocked',
1093
+ url,
1094
+ out: target,
1095
+ reason: errorMessage(error),
1096
+ next_action: 'Install Playwright in the current project or provide a user/browser screenshot, then use `yam ueye compare`.',
1097
+ visual_cap: 'blocked'
1098
+ };
1099
+ printJsonOrHuman(result, Boolean(flags.json), 'Ueye capture');
1100
+ process.exitCode = 1;
1101
+ }
1102
+ finally {
1103
+ if (browser)
1104
+ await browser.close().catch(() => { });
1105
+ }
1106
+ }
1107
+ async function ueyeCompare(args = []) {
1108
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h')
1109
+ return ueyeUsage();
1110
+ const flags = parseSimpleFlags(args, new Set(['reference', 'ref', 'actual', 'screenshot', 'json', 'reference-id', 'screenshot-id']));
1111
+ const reference = String(flags.reference || flags.ref || '');
1112
+ const actual = String(flags.actual || flags.screenshot || '');
1113
+ if (!reference || !actual) {
1114
+ console.error('usage: yam ueye compare --reference ref.png --actual screenshot.png [--json]');
1115
+ process.exitCode = 1;
1116
+ return;
1117
+ }
1118
+ const referencePath = path.resolve(expandHome(reference));
1119
+ const actualPath = path.resolve(expandHome(actual));
1120
+ try {
1121
+ const referenceInfo = await imageFileInfo(referencePath);
1122
+ const actualInfo = await imageFileInfo(actualPath);
1123
+ if (referenceInfo.dimensions === 'unknown' || actualInfo.dimensions === 'unknown') {
1124
+ throw new Error('compare requires image files with readable PNG, JPEG, or GIF dimensions');
1125
+ }
1126
+ const exactMatch = referenceInfo.sha256 === actualInfo.sha256;
1127
+ const sameDimensions = referenceInfo.dimensions === actualInfo.dimensions;
1128
+ const comparisonResult = exactMatch ? 'matched' : 'different';
1129
+ const truthStatus = exactMatch ? 'verified' : 'partial';
1130
+ const provenance = buildUeyeVisualProvenance({
1131
+ source_kind: 'implementation_screenshot',
1132
+ source_path: actualPath,
1133
+ source_hash: actualInfo.sha256,
1134
+ reference_id: String(flags.reference_id || path.basename(referencePath)),
1135
+ screenshot_id: String(flags.screenshot_id || path.basename(actualPath)),
1136
+ local_only: true,
1137
+ redacted: false,
1138
+ operator_provided: false,
1139
+ comparison_result: comparisonResult,
1140
+ truth_status: truthStatus
1141
+ });
1142
+ const result = {
1143
+ schema: 'yam.ueye-compare.v1',
1144
+ status: truthStatus,
1145
+ comparison_result: comparisonResult,
1146
+ exact_match: exactMatch,
1147
+ same_dimensions: sameDimensions,
1148
+ reference: referenceInfo,
1149
+ actual: actualInfo,
1150
+ visual_evidence: `browser/local screenshot comparison executed: reference=${referencePath}, actual=${actualPath}, result=${comparisonResult}`,
1151
+ visual_provenance: provenance,
1152
+ proof_hint: `yam proof --route ueye --truth ${truthStatus} --visual "browser/local screenshot comparison executed: ${comparisonResult}" --visual-provenance '${JSON.stringify(provenance)}' --require-visual`
1153
+ };
1154
+ printJsonOrHuman(result, Boolean(flags.json), 'Ueye compare');
1155
+ }
1156
+ catch (error) {
1157
+ const result = {
1158
+ schema: 'yam.ueye-compare.v1',
1159
+ status: 'blocked',
1160
+ comparison_result: 'not-verified',
1161
+ reason: errorMessage(error),
1162
+ visual_cap: 'blocked'
1163
+ };
1164
+ printJsonOrHuman(result, Boolean(flags.json), 'Ueye compare');
1165
+ process.exitCode = 1;
1166
+ }
1167
+ }
1168
+ function parseSimpleFlags(args = [], allowed = new Set()) {
1169
+ const flags = {};
1170
+ const positionals = [];
1171
+ for (let index = 0; index < args.length; index += 1) {
1172
+ const arg = String(args[index] || '');
1173
+ if (!arg.startsWith('--')) {
1174
+ positionals.push(arg);
1175
+ continue;
1176
+ }
1177
+ const [rawKey, inlineValue] = arg.includes('=') ? arg.split(/=(.*)/s, 2) : [arg, undefined];
1178
+ const key = rawKey.slice(2);
1179
+ if (allowed.size && !allowed.has(key))
1180
+ continue;
1181
+ const normalizedKey = key.replace(/-/g, '_');
1182
+ if (['json', 'full-page'].includes(key)) {
1183
+ flags[normalizedKey] = true;
1184
+ continue;
1185
+ }
1186
+ const value = inlineValue ?? args[index + 1] ?? '';
1187
+ if (inlineValue === undefined)
1188
+ index += 1;
1189
+ flags[normalizedKey] = value;
1190
+ }
1191
+ flags._ = positionals;
1192
+ return flags;
1193
+ }
1194
+ function parseViewport(value = '1440x900') {
1195
+ const match = String(value || '').match(/^(\d{2,5})x(\d{2,5})$/i);
1196
+ if (!match)
1197
+ return { width: 1440, height: 900 };
1198
+ return { width: Number(match[1]), height: Number(match[2]) };
1199
+ }
1200
+ async function loadOptionalPlaywright() {
1201
+ let projectError = '';
1202
+ try {
1203
+ const projectRequire = createRequire(path.join(process.cwd(), 'package.json'));
1204
+ return projectRequire('playwright');
1205
+ }
1206
+ catch (error) {
1207
+ projectError = errorMessage(error);
1208
+ // Fall through to the yam package context for local development or bundled installs.
1209
+ }
1210
+ const dynamicImport = new Function('specifier', 'return import(specifier)');
1211
+ try {
1212
+ return await dynamicImport('playwright');
1213
+ }
1214
+ catch (error) {
1215
+ throw new Error(`Playwright not found from current project or yam package context. Current project: ${projectError}; package context: ${errorMessage(error)}`);
1216
+ }
1217
+ }
1218
+ async function imageFileInfo(file) {
1219
+ const absolute = path.resolve(expandHome(file));
1220
+ const buffer = await fsp.readFile(absolute);
1221
+ const stat = await fsp.stat(absolute);
1222
+ return {
1223
+ path: absolute,
1224
+ sha256: crypto.createHash('sha256').update(buffer).digest('hex'),
1225
+ bytes: stat.size,
1226
+ dimensions: imageDimensions(buffer)
1227
+ };
1228
+ }
1229
+ function imageDimensions(buffer) {
1230
+ const png = pngDimensions(buffer);
1231
+ if (png)
1232
+ return png;
1233
+ const jpeg = jpegDimensions(buffer);
1234
+ if (jpeg)
1235
+ return jpeg;
1236
+ const gif = gifDimensions(buffer);
1237
+ if (gif)
1238
+ return gif;
1239
+ return 'unknown';
1240
+ }
1241
+ function pngDimensions(buffer) {
1242
+ if (buffer.length < 24)
1243
+ return '';
1244
+ if (buffer.readUInt32BE(0) !== 0x89504e47 || buffer.readUInt32BE(4) !== 0x0d0a1a0a)
1245
+ return '';
1246
+ return `${buffer.readUInt32BE(16)}x${buffer.readUInt32BE(20)}`;
1247
+ }
1248
+ function gifDimensions(buffer) {
1249
+ if (buffer.length < 10)
1250
+ return '';
1251
+ const signature = buffer.toString('ascii', 0, 6);
1252
+ if (signature !== 'GIF87a' && signature !== 'GIF89a')
1253
+ return '';
1254
+ return `${buffer.readUInt16LE(6)}x${buffer.readUInt16LE(8)}`;
1255
+ }
1256
+ function jpegDimensions(buffer) {
1257
+ if (buffer.length < 4 || buffer[0] !== 0xff || buffer[1] !== 0xd8)
1258
+ return '';
1259
+ let offset = 2;
1260
+ while (offset < buffer.length) {
1261
+ if (buffer[offset] !== 0xff)
1262
+ return '';
1263
+ const marker = buffer[offset + 1];
1264
+ const length = buffer.readUInt16BE(offset + 2);
1265
+ if (marker >= 0xc0 && marker <= 0xc3 && offset + 8 < buffer.length) {
1266
+ return `${buffer.readUInt16BE(offset + 7)}x${buffer.readUInt16BE(offset + 5)}`;
1267
+ }
1268
+ offset += 2 + length;
1269
+ }
1270
+ return '';
1271
+ }
1272
+ function printJsonOrHuman(result, json = false, label = 'yam') {
1273
+ if (json) {
1274
+ console.log(JSON.stringify(result, null, 2));
1275
+ return;
1276
+ }
1277
+ console.log(label);
1278
+ for (const [key, value] of Object.entries(result)) {
1279
+ if (key === 'schema')
1280
+ continue;
1281
+ if (typeof value === 'object' && value !== null) {
1282
+ console.log(`- ${key}: ${JSON.stringify(value)}`);
1283
+ }
1284
+ else {
1285
+ console.log(`- ${key}: ${value}`);
1286
+ }
1287
+ }
1288
+ }
1020
1289
  function detectDbSafetyText(text = '') {
1021
1290
  return detectTrustDbSafetyText(text);
1022
1291
  }
@@ -2112,6 +2381,8 @@ async function main() {
2112
2381
  return tools(process.argv.slice(3));
2113
2382
  if (command === 'proof')
2114
2383
  return proof(process.argv.slice(3));
2384
+ if (command === 'ueye')
2385
+ return ueye(process.argv.slice(3));
2115
2386
  if (command === 'release')
2116
2387
  return release(process.argv.slice(3));
2117
2388
  if (command === 'safety')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yam-flow",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Progressive proof-first agent flow: start fast, deepen deliberately, stay honest by design.",
5
5
  "type": "module",
6
6
  "author": "0kim0bos",
@@ -96,6 +96,7 @@
96
96
  "license": "MIT",
97
97
  "devDependencies": {
98
98
  "@types/node": "^25.9.1",
99
+ "playwright": "^1.60.0",
99
100
  "typescript": "^6.0.3"
100
101
  }
101
102
  }
@@ -19,6 +19,26 @@ Skip or compress them when:
19
19
 
20
20
  ## Artifact Set
21
21
 
22
+ ### Opt-In CLI Helpers
23
+
24
+ Ueye is one route. It should not split into separate lite/deep skills. Use these helpers only when the current visual claim needs real evidence:
25
+
26
+ ```bash
27
+ yam ueye capture --url http://localhost:3000 --out .yam/screens/home.png
28
+ yam ueye compare --reference ./reference.png --actual .yam/screens/home.png --json
29
+ ```
30
+
31
+ - `capture` uses a Playwright install from the current project when present, then reports `blocked` instead of installing dependencies or pretending a screenshot exists.
32
+ - `compare` is local-only and dependency-free. It records hashes, dimensions, comparison result, and visual provenance. Different screenshots stay `partial`; exact file matches can be `verified`.
33
+ - Without capture or user-provided screenshots, report implementation/source review separately from visual verification and cap the visual claim.
34
+
35
+ ### Browser Preference
36
+
37
+ - Prefer the Codex in-app Browser plugin for local pages, localhost, file URLs, and ordinary visual QA.
38
+ - Do not silently switch to the user's Chrome browser when the in-app Browser cannot capture or inspect.
39
+ - Use Chrome only when the user explicitly asks for it, or when the task requires existing Chrome cookies, sessions, extensions, or profile state.
40
+ - If the in-app Browser is unavailable and Chrome was not explicitly requested or required, cap the visual claim at `partial` or `blocked`.
41
+
22
42
  ### Visual Evidence Inventory
23
43
 
24
44
  Purpose: identify what visual sources were inspected.
@@ -35,6 +35,8 @@ Do not use for:
35
35
  - Generated annotated images are optional, not a default gate.
36
36
  - Image evidence should stay bounded: inspect the primary screen first, then only the states/images needed to support the claim.
37
37
  - Design quality judgment belongs after implementation/review: compare to the reference first, then judge whether the result is good design.
38
+ - Keep Ueye as one route: start fast, then opt into `yam ueye capture` and `yam ueye compare` only when a `verified` visual claim needs real screen evidence.
39
+ - Prefer the Codex in-app Browser plugin for interactive visual checks. Do not switch to the user's Chrome browser unless the user explicitly asks for Chrome or the task requires existing Chrome cookies, sessions, extensions, or profile state.
38
40
 
39
41
  ## Workflow
40
42
 
@@ -52,6 +54,10 @@ Do not use for:
52
54
  6. Implement the smallest coherent design improvement when implementation is requested.
53
55
  7. Check default, loading, error, empty, disabled, hover/focus, and mobile states when relevant.
54
56
  8. Run browser/screenshot verification when feasible.
57
+ - Prefer the Codex in-app Browser plugin for local pages, localhost, file URLs, and ordinary visual QA.
58
+ - Do not silently use Chrome as a fallback. If the in-app Browser cannot capture or inspect, report the visual cap as `partial` or `blocked`.
59
+ - Use `yam ueye capture --url URL --out path.png` when a local Playwright install is available and the claim needs real browser evidence.
60
+ - Use `yam ueye compare --reference ref.png --actual shot.png` when reference fidelity needs a proof-ready local comparison record.
55
61
  9. Compare reference and implementation when reference fidelity was requested:
56
62
  - matched, similar, different, not-applicable, or not-verified.
57
63
  - Separate "faithful to reference" from "good design."
package/src/bin/yam.ts CHANGED
@@ -1,7 +1,9 @@
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';
@@ -132,6 +134,8 @@ Usage:
132
134
  yam tools doctor [dir]
133
135
  yam proof [dir|--from file] [--route route] [--truth status] [--command text] [--evidence text]
134
136
  yam proof write [dir] [--format json|md] [--out file] [--route route] [--truth status] [--command text]
137
+ yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
138
+ yam ueye compare --reference ref.png --actual screenshot.png [--json]
135
139
  yam release report [--json]
136
140
  yam safety [text...]
137
141
  yam memory <init|add|list|summary|resolve> [dir] [options]
@@ -1010,6 +1014,261 @@ async function readStdinTextIfAvailable() {
1010
1014
  return Buffer.concat(chunks).toString('utf8').trim();
1011
1015
  }
1012
1016
 
1017
+ async function ueye(args = []) {
1018
+ const subcommand = args[0] || 'help';
1019
+ if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return ueyeUsage();
1020
+ if (subcommand === 'capture') return ueyeCapture(args.slice(1));
1021
+ if (subcommand === 'compare') return ueyeCompare(args.slice(1));
1022
+ console.error(`unknown ueye command: ${subcommand}`);
1023
+ return ueyeUsage();
1024
+ }
1025
+
1026
+ function ueyeUsage() {
1027
+ console.log(`yam ueye
1028
+
1029
+ Opt-in visual evidence helpers. Ueye stays one skill: fast by default, capture/compare only when verified visual claims need real evidence.
1030
+
1031
+ Usage:
1032
+ yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
1033
+ yam ueye compare --reference ref.png --actual screenshot.png [--json]
1034
+
1035
+ Notes:
1036
+ capture uses a locally available Playwright install when present. It does not download browsers or install dependencies.
1037
+ compare uses local files only and reports sha256, dimensions, comparison_result, and proof-ready visual provenance.
1038
+ `);
1039
+ }
1040
+
1041
+ async function ueyeCapture(args = []) {
1042
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return ueyeUsage();
1043
+ const flags = parseSimpleFlags(args, new Set(['url', 'out', 'viewport', 'wait-until', 'timeout', 'full-page', 'json']));
1044
+ const url = String(flags.url || '');
1045
+ const out = String(flags.out || '');
1046
+ if (!url || !out) {
1047
+ console.error('usage: yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]');
1048
+ process.exitCode = 1;
1049
+ return;
1050
+ }
1051
+
1052
+ const target = path.resolve(expandHome(out));
1053
+ const viewport = parseViewport(String(flags.viewport || '1440x900'));
1054
+ const timeout = Number(flags.timeout || 30000);
1055
+ const waitUntil = String(flags.wait_until || 'networkidle');
1056
+ let browser;
1057
+
1058
+ try {
1059
+ const playwright = await loadOptionalPlaywright();
1060
+ const chromium = playwright?.chromium;
1061
+ if (!chromium) throw new Error('Playwright chromium launcher was not found');
1062
+ await fsp.mkdir(path.dirname(target), { recursive: true });
1063
+ browser = await chromium.launch({ headless: true });
1064
+ const page = await browser.newPage({ viewport });
1065
+ await page.goto(url, { waitUntil, timeout });
1066
+ await page.screenshot({ path: target, fullPage: Boolean(flags.full_page) });
1067
+ const info = await imageFileInfo(target);
1068
+ const result = {
1069
+ schema: 'yam.ueye-capture.v1',
1070
+ status: 'verified',
1071
+ url,
1072
+ out: target,
1073
+ viewport: `${viewport.width}x${viewport.height}`,
1074
+ full_page: Boolean(flags.full_page),
1075
+ sha256: info.sha256,
1076
+ dimensions: info.dimensions,
1077
+ visual_evidence: `browser screenshot captured: ${target} (${info.dimensions}, sha256:${info.sha256})`
1078
+ };
1079
+ printJsonOrHuman(result, Boolean(flags.json), 'Ueye capture');
1080
+ } catch (error) {
1081
+ const result = {
1082
+ schema: 'yam.ueye-capture.v1',
1083
+ status: 'blocked',
1084
+ url,
1085
+ out: target,
1086
+ reason: errorMessage(error),
1087
+ next_action: 'Install Playwright in the current project or provide a user/browser screenshot, then use `yam ueye compare`.',
1088
+ visual_cap: 'blocked'
1089
+ };
1090
+ printJsonOrHuman(result, Boolean(flags.json), 'Ueye capture');
1091
+ process.exitCode = 1;
1092
+ } finally {
1093
+ if (browser) await browser.close().catch(() => {});
1094
+ }
1095
+ }
1096
+
1097
+ async function ueyeCompare(args = []) {
1098
+ if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return ueyeUsage();
1099
+ const flags = parseSimpleFlags(args, new Set(['reference', 'ref', 'actual', 'screenshot', 'json', 'reference-id', 'screenshot-id']));
1100
+ const reference = String(flags.reference || flags.ref || '');
1101
+ const actual = String(flags.actual || flags.screenshot || '');
1102
+ if (!reference || !actual) {
1103
+ console.error('usage: yam ueye compare --reference ref.png --actual screenshot.png [--json]');
1104
+ process.exitCode = 1;
1105
+ return;
1106
+ }
1107
+
1108
+ const referencePath = path.resolve(expandHome(reference));
1109
+ const actualPath = path.resolve(expandHome(actual));
1110
+ try {
1111
+ const referenceInfo = await imageFileInfo(referencePath);
1112
+ const actualInfo = await imageFileInfo(actualPath);
1113
+ if (referenceInfo.dimensions === 'unknown' || actualInfo.dimensions === 'unknown') {
1114
+ throw new Error('compare requires image files with readable PNG, JPEG, or GIF dimensions');
1115
+ }
1116
+ const exactMatch = referenceInfo.sha256 === actualInfo.sha256;
1117
+ const sameDimensions = referenceInfo.dimensions === actualInfo.dimensions;
1118
+ const comparisonResult = exactMatch ? 'matched' : 'different';
1119
+ const truthStatus = exactMatch ? 'verified' : 'partial';
1120
+ const provenance = buildUeyeVisualProvenance({
1121
+ source_kind: 'implementation_screenshot',
1122
+ source_path: actualPath,
1123
+ source_hash: actualInfo.sha256,
1124
+ reference_id: String(flags.reference_id || path.basename(referencePath)),
1125
+ screenshot_id: String(flags.screenshot_id || path.basename(actualPath)),
1126
+ local_only: true,
1127
+ redacted: false,
1128
+ operator_provided: false,
1129
+ comparison_result: comparisonResult,
1130
+ truth_status: truthStatus
1131
+ });
1132
+ const result = {
1133
+ schema: 'yam.ueye-compare.v1',
1134
+ status: truthStatus,
1135
+ comparison_result: comparisonResult,
1136
+ exact_match: exactMatch,
1137
+ same_dimensions: sameDimensions,
1138
+ reference: referenceInfo,
1139
+ actual: actualInfo,
1140
+ visual_evidence: `browser/local screenshot comparison executed: reference=${referencePath}, actual=${actualPath}, result=${comparisonResult}`,
1141
+ visual_provenance: provenance,
1142
+ proof_hint: `yam proof --route ueye --truth ${truthStatus} --visual "browser/local screenshot comparison executed: ${comparisonResult}" --visual-provenance '${JSON.stringify(provenance)}' --require-visual`
1143
+ };
1144
+ printJsonOrHuman(result, Boolean(flags.json), 'Ueye compare');
1145
+ } catch (error) {
1146
+ const result = {
1147
+ schema: 'yam.ueye-compare.v1',
1148
+ status: 'blocked',
1149
+ comparison_result: 'not-verified',
1150
+ reason: errorMessage(error),
1151
+ visual_cap: 'blocked'
1152
+ };
1153
+ printJsonOrHuman(result, Boolean(flags.json), 'Ueye compare');
1154
+ process.exitCode = 1;
1155
+ }
1156
+ }
1157
+
1158
+ function parseSimpleFlags(args = [], allowed = new Set<string>()) {
1159
+ const flags: AnyRecord = {};
1160
+ const positionals: string[] = [];
1161
+ for (let index = 0; index < args.length; index += 1) {
1162
+ const arg = String(args[index] || '');
1163
+ if (!arg.startsWith('--')) {
1164
+ positionals.push(arg);
1165
+ continue;
1166
+ }
1167
+ const [rawKey, inlineValue] = arg.includes('=') ? arg.split(/=(.*)/s, 2) : [arg, undefined];
1168
+ const key = rawKey.slice(2);
1169
+ if (allowed.size && !allowed.has(key)) continue;
1170
+ const normalizedKey = key.replace(/-/g, '_');
1171
+ if (['json', 'full-page'].includes(key)) {
1172
+ flags[normalizedKey] = true;
1173
+ continue;
1174
+ }
1175
+ const value = inlineValue ?? args[index + 1] ?? '';
1176
+ if (inlineValue === undefined) index += 1;
1177
+ flags[normalizedKey] = value;
1178
+ }
1179
+ flags._ = positionals;
1180
+ return flags;
1181
+ }
1182
+
1183
+ function parseViewport(value = '1440x900') {
1184
+ const match = String(value || '').match(/^(\d{2,5})x(\d{2,5})$/i);
1185
+ if (!match) return { width: 1440, height: 900 };
1186
+ return { width: Number(match[1]), height: Number(match[2]) };
1187
+ }
1188
+
1189
+ async function loadOptionalPlaywright() {
1190
+ let projectError = '';
1191
+ try {
1192
+ const projectRequire = createRequire(path.join(process.cwd(), 'package.json'));
1193
+ return projectRequire('playwright');
1194
+ } catch (error) {
1195
+ projectError = errorMessage(error);
1196
+ // Fall through to the yam package context for local development or bundled installs.
1197
+ }
1198
+ const dynamicImport = new Function('specifier', 'return import(specifier)') as (specifier: string) => Promise<any>;
1199
+ try {
1200
+ return await dynamicImport('playwright');
1201
+ } catch (error) {
1202
+ throw new Error(`Playwright not found from current project or yam package context. Current project: ${projectError}; package context: ${errorMessage(error)}`);
1203
+ }
1204
+ }
1205
+
1206
+ async function imageFileInfo(file) {
1207
+ const absolute = path.resolve(expandHome(file));
1208
+ const buffer = await fsp.readFile(absolute);
1209
+ const stat = await fsp.stat(absolute);
1210
+ return {
1211
+ path: absolute,
1212
+ sha256: crypto.createHash('sha256').update(buffer).digest('hex'),
1213
+ bytes: stat.size,
1214
+ dimensions: imageDimensions(buffer)
1215
+ };
1216
+ }
1217
+
1218
+ function imageDimensions(buffer) {
1219
+ const png = pngDimensions(buffer);
1220
+ if (png) return png;
1221
+ const jpeg = jpegDimensions(buffer);
1222
+ if (jpeg) return jpeg;
1223
+ const gif = gifDimensions(buffer);
1224
+ if (gif) return gif;
1225
+ return 'unknown';
1226
+ }
1227
+
1228
+ function pngDimensions(buffer) {
1229
+ if (buffer.length < 24) return '';
1230
+ if (buffer.readUInt32BE(0) !== 0x89504e47 || buffer.readUInt32BE(4) !== 0x0d0a1a0a) return '';
1231
+ return `${buffer.readUInt32BE(16)}x${buffer.readUInt32BE(20)}`;
1232
+ }
1233
+
1234
+ function gifDimensions(buffer) {
1235
+ if (buffer.length < 10) return '';
1236
+ const signature = buffer.toString('ascii', 0, 6);
1237
+ if (signature !== 'GIF87a' && signature !== 'GIF89a') return '';
1238
+ return `${buffer.readUInt16LE(6)}x${buffer.readUInt16LE(8)}`;
1239
+ }
1240
+
1241
+ function jpegDimensions(buffer) {
1242
+ if (buffer.length < 4 || buffer[0] !== 0xff || buffer[1] !== 0xd8) return '';
1243
+ let offset = 2;
1244
+ while (offset < buffer.length) {
1245
+ if (buffer[offset] !== 0xff) return '';
1246
+ const marker = buffer[offset + 1];
1247
+ const length = buffer.readUInt16BE(offset + 2);
1248
+ if (marker >= 0xc0 && marker <= 0xc3 && offset + 8 < buffer.length) {
1249
+ return `${buffer.readUInt16BE(offset + 7)}x${buffer.readUInt16BE(offset + 5)}`;
1250
+ }
1251
+ offset += 2 + length;
1252
+ }
1253
+ return '';
1254
+ }
1255
+
1256
+ function printJsonOrHuman(result, json = false, label = 'yam') {
1257
+ if (json) {
1258
+ console.log(JSON.stringify(result, null, 2));
1259
+ return;
1260
+ }
1261
+ console.log(label);
1262
+ for (const [key, value] of Object.entries(result)) {
1263
+ if (key === 'schema') continue;
1264
+ if (typeof value === 'object' && value !== null) {
1265
+ console.log(`- ${key}: ${JSON.stringify(value)}`);
1266
+ } else {
1267
+ console.log(`- ${key}: ${value}`);
1268
+ }
1269
+ }
1270
+ }
1271
+
1013
1272
  function detectDbSafetyText(text = '') {
1014
1273
  return detectTrustDbSafetyText(text);
1015
1274
  }
@@ -2105,6 +2364,7 @@ async function main() {
2105
2364
  if (command === 'measure') return measure(process.argv[3], process.argv.slice(4));
2106
2365
  if (command === 'tools') return tools(process.argv.slice(3));
2107
2366
  if (command === 'proof') return proof(process.argv.slice(3));
2367
+ if (command === 'ueye') return ueye(process.argv.slice(3));
2108
2368
  if (command === 'release') return release(process.argv.slice(3));
2109
2369
  if (command === 'safety') return safety(process.argv.slice(3));
2110
2370
  if (command === 'memory') return memory(process.argv.slice(3));