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 +10 -0
- package/README.md +17 -0
- package/dist/bin/yam.js +271 -0
- package/package.json +2 -1
- package/references/ueye-proof.md +20 -0
- package/skills/ueye/SKILL.md +6 -0
- package/src/bin/yam.ts +260 -0
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.
|
|
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
|
}
|
package/references/ueye-proof.md
CHANGED
|
@@ -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.
|
package/skills/ueye/SKILL.md
CHANGED
|
@@ -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));
|