yam-flow 0.1.0 → 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/dist/bin/yam.js CHANGED
@@ -1,11 +1,14 @@
1
1
  #!/usr/bin/env node
2
+ import { spawnSync } from 'node:child_process';
3
+ import crypto from 'node:crypto';
2
4
  import fs from 'node:fs';
3
5
  import fsp from 'node:fs/promises';
6
+ import { createRequire } from 'node:module';
4
7
  import os from 'node:os';
5
8
  import path from 'node:path';
6
9
  import process from 'node:process';
7
10
  import { fileURLToPath } from 'node:url';
8
- import { TRUTH_STATUSES, buildYamCompletionProof, detectDbSafetyText as detectTrustDbSafetyText, isTruthStatus } from '../lib/trust-kernel.js';
11
+ import { TRUTH_STATUSES, buildMissionPatchEnvelope, buildRollbackHint, buildUeyeVisualProvenance, buildYamCompletionProof, detectDbSafetyText as detectTrustDbSafetyText, isTruthStatus } from '../lib/trust-kernel.js';
9
12
  function errorMessage(error) {
10
13
  return error instanceof Error ? error.message : String(error);
11
14
  }
@@ -117,6 +120,9 @@ Usage:
117
120
  yam tools doctor [dir]
118
121
  yam proof [dir|--from file] [--route route] [--truth status] [--command text] [--evidence text]
119
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 release report [--json]
120
126
  yam safety [text...]
121
127
  yam memory <init|add|list|summary|resolve> [dir] [options]
122
128
  yam hook <status|enable|disable|run> [lite] [--global|--project dir]
@@ -927,6 +933,86 @@ async function safety(args = []) {
927
933
  const result = detectDbSafetyText(text);
928
934
  printSafetyResult(result);
929
935
  }
936
+ async function release(args = []) {
937
+ const subcommand = args[0] || 'help';
938
+ if (subcommand !== 'report') {
939
+ console.error('usage: yam release report [--json]');
940
+ process.exitCode = 1;
941
+ return;
942
+ }
943
+ const asJson = args.includes('--json');
944
+ const report = runReleaseReport();
945
+ if (asJson) {
946
+ console.log(JSON.stringify(report, null, 2));
947
+ if (!report.ok)
948
+ process.exitCode = 1;
949
+ return;
950
+ }
951
+ console.log('yam release report');
952
+ console.log(`- Package: ${report.packageName}@${report.version}`);
953
+ console.log(`- Truth status: ${report.truth_status}`);
954
+ for (const check of report.checks) {
955
+ console.log(`- ${check.id}: ${check.status} (${check.duration_ms}ms)`);
956
+ if (check.note)
957
+ console.log(` ${check.note}`);
958
+ }
959
+ if (report.failed.length) {
960
+ console.log('- Failed:');
961
+ for (const id of report.failed)
962
+ console.log(` - ${id}`);
963
+ }
964
+ if (!report.ok)
965
+ process.exitCode = 1;
966
+ }
967
+ function runReleaseReport() {
968
+ const startedAt = new Date().toISOString();
969
+ const checks = [
970
+ ['typecheck', ['npm', ['run', 'typecheck']]],
971
+ ['forbidden_names', ['npm', ['run', 'forbidden-names:check']]],
972
+ ['package_boundary', ['npm', ['run', 'package-boundary:check']]],
973
+ ['registry_status', ['npm', ['run', 'registry:check']]],
974
+ ['cli_smoke', ['npm', ['run', 'cli-smoke']]],
975
+ ['dist_freshness', ['npm', ['run', 'dist:freshness']]]
976
+ ].map(([id, command]) => runReleaseCheck(id, command));
977
+ const failed = checks.filter((check) => check.status !== 'passed').map((check) => check.id);
978
+ return {
979
+ schema: 'yam.release-report.v1',
980
+ generated_at: startedAt,
981
+ packageName: PACKAGE_JSON.name,
982
+ version: VERSION,
983
+ ok: failed.length === 0,
984
+ truth_status: failed.length === 0 ? 'verified' : 'blocked',
985
+ checks,
986
+ failed
987
+ };
988
+ }
989
+ function runReleaseCheck(id, commandSpec) {
990
+ const [command, args] = commandSpec;
991
+ const start = Date.now();
992
+ const result = spawnSync(command, args, {
993
+ cwd: ROOT,
994
+ encoding: 'utf8',
995
+ env: { ...process.env, npm_config_cache: path.join(os.tmpdir(), 'yam-npm-cache') },
996
+ timeout: 120000
997
+ });
998
+ const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
999
+ const timedOut = result.error?.message?.includes('ETIMEDOUT') || result.signal === 'SIGTERM';
1000
+ return {
1001
+ id,
1002
+ command: [command, ...args].join(' '),
1003
+ status: result.status === 0 ? 'passed' : timedOut ? 'blocked' : 'failed',
1004
+ exit_code: typeof result.status === 'number' ? result.status : null,
1005
+ duration_ms: Date.now() - start,
1006
+ note: summarizeCheckOutput(output || result.error?.message || '')
1007
+ };
1008
+ }
1009
+ function summarizeCheckOutput(output = '') {
1010
+ const lines = String(output || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
1011
+ if (!lines.length)
1012
+ return '';
1013
+ const highSignal = lines.filter((line) => !line.startsWith('>')).slice(-4);
1014
+ return highSignal.join(' | ').slice(0, 600);
1015
+ }
930
1016
  async function readStdinTextIfAvailable() {
931
1017
  if (process.stdin.isTTY)
932
1018
  return '';
@@ -935,6 +1021,271 @@ async function readStdinTextIfAvailable() {
935
1021
  chunks.push(Buffer.from(chunk));
936
1022
  return Buffer.concat(chunks).toString('utf8').trim();
937
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
+ }
938
1289
  function detectDbSafetyText(text = '') {
939
1290
  return detectTrustDbSafetyText(text);
940
1291
  }
@@ -975,6 +1326,9 @@ async function proof(args = []) {
975
1326
  printProofList('Evidence', summary.evidence);
976
1327
  printProofList('Visual evidence', summary.visual);
977
1328
  printProofList('Runtime evidence', summary.runtime);
1329
+ printProofList('Visual provenance', summary.visualProvenance || []);
1330
+ printProofList('Mission patch envelope', summary.missionEnvelope || []);
1331
+ printProofList('Rollback hint', summary.rollbackHint || []);
978
1332
  printProofList('Changed surfaces', summary.changed);
979
1333
  printProofList('Skipped', summary.skipped);
980
1334
  printProofList('Blocked', summary.blocked);
@@ -1050,6 +1404,18 @@ function buildProofSummary(parsed, artifact) {
1050
1404
  evidence: [...artifact.evidence, ...arrayFlag(parsed.flags.evidence)],
1051
1405
  visual: [...artifact.visual, ...arrayFlag(parsed.flags.visual)],
1052
1406
  runtime: [...artifact.runtime, ...arrayFlag(parsed.flags.runtime)],
1407
+ visualProvenance: [
1408
+ ...structuredEvidenceList(artifact.visualProvenance),
1409
+ ...structuredEvidenceList(parsed.flags.visual_provenance)
1410
+ ],
1411
+ missionEnvelope: [
1412
+ ...structuredEvidenceList(artifact.missionEnvelope),
1413
+ ...structuredEvidenceList(parsed.flags.mission_envelope)
1414
+ ],
1415
+ rollbackHint: [
1416
+ ...structuredEvidenceList(artifact.rollbackHint),
1417
+ ...structuredEvidenceList(parsed.flags.rollback_hint)
1418
+ ],
1053
1419
  cleanup: parsed.flags.cleanup || artifact.cleanup || '',
1054
1420
  changed: [...artifact.changed, ...arrayFlag(parsed.flags.changed)],
1055
1421
  skipped: [...artifact.skipped, ...arrayFlag(parsed.flags.skipped)],
@@ -1099,6 +1465,15 @@ function renderProofMarkdown(summary) {
1099
1465
  '## Runtime evidence',
1100
1466
  ...renderProofMarkdownList(summary.runtime),
1101
1467
  '',
1468
+ '## Visual provenance',
1469
+ ...renderProofMarkdownList(summary.visualProvenance || []),
1470
+ '',
1471
+ '## Mission patch envelope',
1472
+ ...renderProofMarkdownList(summary.missionEnvelope || []),
1473
+ '',
1474
+ '## Rollback hint',
1475
+ ...renderProofMarkdownList(summary.rollbackHint || []),
1476
+ '',
1102
1477
  '## Changed surfaces',
1103
1478
  ...renderProofMarkdownList(summary.changed),
1104
1479
  '',
@@ -1129,7 +1504,7 @@ function renderProofMarkdownList(values) {
1129
1504
  function parseProofArgs(args = []) {
1130
1505
  const flags = {};
1131
1506
  const positionals = [];
1132
- const aliases = new Set(['goal', 'route', 'truth', 'command', 'evidence', 'visual', 'runtime', 'cleanup', 'changed', 'skipped', 'blocked', 'assumed', 'assumption', 'unverified', 'from', 'format', 'out', 'file', 'json', 'require-runtime', 'require-real-runtime', 'require-tmux', 'require-visual']);
1507
+ 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']);
1133
1508
  for (let index = 0; index < args.length; index += 1) {
1134
1509
  const arg = args[index];
1135
1510
  if (arg.startsWith('--')) {
@@ -1145,7 +1520,7 @@ function parseProofArgs(args = []) {
1145
1520
  const value = inlineValue ?? args[index + 1] ?? '';
1146
1521
  if (inlineValue === undefined)
1147
1522
  index += 1;
1148
- if (['command', 'evidence', 'visual', 'runtime', 'changed', 'skipped', 'blocked', 'assumed', 'assumption', 'unverified'].includes(key)) {
1523
+ if (['command', 'evidence', 'visual', 'runtime', 'visual-provenance', 'mission-envelope', 'rollback-hint', 'changed', 'skipped', 'blocked', 'assumed', 'assumption', 'unverified'].includes(key)) {
1149
1524
  flags[normalizedKey] = [...arrayFlag(flags[normalizedKey]), value];
1150
1525
  }
1151
1526
  else {
@@ -1184,6 +1559,9 @@ function emptyProofArtifact() {
1184
1559
  evidence: [],
1185
1560
  visual: [],
1186
1561
  runtime: [],
1562
+ visualProvenance: [],
1563
+ missionEnvelope: [],
1564
+ rollbackHint: [],
1187
1565
  cleanup: '',
1188
1566
  changed: [],
1189
1567
  skipped: [],
@@ -1205,6 +1583,9 @@ async function parseProofJson(file, empty) {
1205
1583
  evidence: arrayFlag(data.evidence),
1206
1584
  visual: arrayFlag(data.visual || data.visualEvidence),
1207
1585
  runtime: arrayFlag(data.runtime || data.runtimeEvidence),
1586
+ visualProvenance: structuredEvidenceList(data.visualProvenance || data.visual_provenance),
1587
+ missionEnvelope: structuredEvidenceList(data.missionEnvelope || data.mission_envelope),
1588
+ rollbackHint: structuredEvidenceList(data.rollbackHint || data.rollback_hint),
1208
1589
  cleanup: String(data.cleanup || data.cleanupStatus || ''),
1209
1590
  changed: arrayFlag(data.changed || data.files),
1210
1591
  skipped: arrayFlag(data.skipped),
@@ -1230,6 +1611,9 @@ async function parseProofMarkdown(file, empty) {
1230
1611
  evidence: readProofList(text, 'Evidence'),
1231
1612
  visual: readProofList(text, 'Visual evidence'),
1232
1613
  runtime: firstProofList(text, ['Runtime evidence', 'Processes', 'tmux']),
1614
+ visualProvenance: readProofList(text, 'Visual provenance'),
1615
+ missionEnvelope: readProofList(text, 'Mission patch envelope'),
1616
+ rollbackHint: readProofList(text, 'Rollback hint'),
1233
1617
  cleanup: readProofField(text, 'Cleanup') || readProofField(text, 'Cleanup status'),
1234
1618
  changed: firstProofList(text, ['Changed surfaces', 'Files']),
1235
1619
  skipped: readProofList(text, 'Skipped'),
@@ -1274,9 +1658,39 @@ function arrayFlag(value) {
1274
1658
  return [];
1275
1659
  return Array.isArray(value) ? value.filter(Boolean) : [value].filter(Boolean);
1276
1660
  }
1661
+ function structuredEvidenceList(value) {
1662
+ return arrayFlag(value).map((item) => formatStructuredEvidence(item)).filter(Boolean);
1663
+ }
1664
+ function formatStructuredEvidence(value) {
1665
+ if (!value)
1666
+ return '';
1667
+ if (typeof value === 'object')
1668
+ return JSON.stringify(value);
1669
+ const text = String(value).trim();
1670
+ if (!text)
1671
+ return '';
1672
+ if (!text.startsWith('{'))
1673
+ return text;
1674
+ try {
1675
+ const data = JSON.parse(text);
1676
+ if (data.schema === 'yam.ueye-visual-provenance.v1' || data.source_kind || data.reference_id || data.screenshot_id) {
1677
+ return JSON.stringify(buildUeyeVisualProvenance(data));
1678
+ }
1679
+ if (data.schema === 'yam.mission-patch-envelope.v1' || data.agent_id || data.assigned_scope) {
1680
+ return JSON.stringify(buildMissionPatchEnvelope(data));
1681
+ }
1682
+ if (data.schema === 'yam.rollback-hint.v1' || data.touched_files || data.safe_revert_note) {
1683
+ return JSON.stringify(buildRollbackHint(data));
1684
+ }
1685
+ return JSON.stringify(data);
1686
+ }
1687
+ catch {
1688
+ return text;
1689
+ }
1690
+ }
1277
1691
  function hasProofEvidence(parsed) {
1278
1692
  const flags = parsed.flags || {};
1279
- return ['command', 'evidence', 'visual', 'runtime', 'changed', 'skipped', 'blocked'].some((key) => arrayFlag(flags[key]).length > 0) || Boolean(flags.cleanup);
1693
+ return ['command', 'evidence', 'visual', 'runtime', 'visual_provenance', 'mission_envelope', 'rollback_hint', 'changed', 'skipped', 'blocked'].some((key) => arrayFlag(flags[key]).length > 0) || Boolean(flags.cleanup);
1280
1694
  }
1281
1695
  function printProofList(label, values) {
1282
1696
  console.log(`- ${label}:`);
@@ -1967,6 +2381,10 @@ async function main() {
1967
2381
  return tools(process.argv.slice(3));
1968
2382
  if (command === 'proof')
1969
2383
  return proof(process.argv.slice(3));
2384
+ if (command === 'ueye')
2385
+ return ueye(process.argv.slice(3));
2386
+ if (command === 'release')
2387
+ return release(process.argv.slice(3));
1970
2388
  if (command === 'safety')
1971
2389
  return safety(process.argv.slice(3));
1972
2390
  if (command === 'memory')
@@ -29,6 +29,35 @@ export interface RuntimeTruthMatrix {
29
29
  rows: RuntimeTruthRow[];
30
30
  blockers: string[];
31
31
  }
32
+ export interface UeyeVisualProvenance {
33
+ schema: 'yam.ueye-visual-provenance.v1';
34
+ source_kind: string;
35
+ source_path: string;
36
+ source_hash: string;
37
+ reference_id: string;
38
+ screenshot_id: string;
39
+ local_only: boolean;
40
+ redacted: boolean;
41
+ operator_provided: boolean;
42
+ comparison_result: string;
43
+ truth_status: TruthStatus;
44
+ }
45
+ export interface RollbackHint {
46
+ schema: 'yam.rollback-hint.v1';
47
+ touched_files: string[];
48
+ generated_files: string[];
49
+ before_check: string;
50
+ safe_revert_note: string;
51
+ }
52
+ export interface MissionPatchEnvelope {
53
+ schema: 'yam.mission-patch-envelope.v1';
54
+ agent_id: string;
55
+ assigned_scope: string;
56
+ changed_files: string[];
57
+ verification_hint: string;
58
+ rollback_hint: RollbackHint;
59
+ truth_status: TruthStatus;
60
+ }
32
61
  export interface FakeRealPolicyResult {
33
62
  schema: 'yam.fake-real-policy.v1';
34
63
  ok: boolean;
@@ -52,6 +81,9 @@ export interface ProofSummary {
52
81
  blocked?: unknown;
53
82
  assumptions?: unknown;
54
83
  unverified?: unknown;
84
+ visualProvenance?: unknown;
85
+ missionEnvelope?: unknown;
86
+ rollbackHint?: unknown;
55
87
  }
56
88
  export interface ProofOptions {
57
89
  requireRealRuntime?: boolean;
@@ -79,6 +111,9 @@ export interface YamCompletionProof {
79
111
  evidence: string[];
80
112
  visual: string[];
81
113
  runtime: string[];
114
+ visualProvenance: string[];
115
+ missionEnvelope: string[];
116
+ rollbackHint: string[];
82
117
  cleanup: unknown;
83
118
  changed: string[];
84
119
  skipped: string[];
@@ -90,6 +125,9 @@ export interface YamCompletionProof {
90
125
  fakeReal: FakeRealPolicyResult;
91
126
  runtimeTruth: RuntimeTruthMatrix;
92
127
  }
128
+ export declare function buildUeyeVisualProvenance(input?: Partial<UeyeVisualProvenance>): UeyeVisualProvenance;
129
+ export declare function buildRollbackHint(input?: Partial<RollbackHint>): RollbackHint;
130
+ export declare function buildMissionPatchEnvelope(input?: Partial<MissionPatchEnvelope>): MissionPatchEnvelope;
93
131
  export declare function isTruthStatus(value?: unknown): value is TruthStatus;
94
132
  export declare function weakestTruth(...statuses: unknown[]): TruthStatus;
95
133
  export declare function detectDbSafetyText(text?: unknown): DbSafetyScanResult;
@@ -10,6 +10,45 @@ export const TRUTH_STATUSES = Object.freeze([
10
10
  'blocked',
11
11
  'assumed'
12
12
  ]);
13
+ export function buildUeyeVisualProvenance(input = {}) {
14
+ const truth = isTruthStatus(input.truth_status) ? input.truth_status : 'partial';
15
+ return {
16
+ schema: 'yam.ueye-visual-provenance.v1',
17
+ source_kind: String(input.source_kind || ''),
18
+ source_path: String(input.source_path || ''),
19
+ source_hash: String(input.source_hash || 'unknown'),
20
+ reference_id: String(input.reference_id || ''),
21
+ screenshot_id: String(input.screenshot_id || ''),
22
+ local_only: Boolean(input.local_only),
23
+ redacted: Boolean(input.redacted),
24
+ operator_provided: Boolean(input.operator_provided),
25
+ comparison_result: String(input.comparison_result || 'not-verified'),
26
+ truth_status: truth
27
+ };
28
+ }
29
+ export function buildRollbackHint(input = {}) {
30
+ return {
31
+ schema: 'yam.rollback-hint.v1',
32
+ touched_files: asList(input.touched_files),
33
+ generated_files: asList(input.generated_files),
34
+ before_check: String(input.before_check || ''),
35
+ safe_revert_note: String(input.safe_revert_note || '')
36
+ };
37
+ }
38
+ export function buildMissionPatchEnvelope(input = {}) {
39
+ const rollback = input.rollback_hint && typeof input.rollback_hint === 'object'
40
+ ? buildRollbackHint(input.rollback_hint)
41
+ : buildRollbackHint();
42
+ return {
43
+ schema: 'yam.mission-patch-envelope.v1',
44
+ agent_id: String(input.agent_id || ''),
45
+ assigned_scope: String(input.assigned_scope || ''),
46
+ changed_files: asList(input.changed_files),
47
+ verification_hint: String(input.verification_hint || ''),
48
+ rollback_hint: rollback,
49
+ truth_status: isTruthStatus(input.truth_status) ? input.truth_status : 'partial'
50
+ };
51
+ }
13
52
  const TRUTH_RANK = Object.freeze({
14
53
  blocked: 0,
15
54
  real_required_missing: 0,
@@ -81,6 +120,15 @@ export function classifyEvidenceTruth(summary = {}, options = {}) {
81
120
  for (const visual of asList(summary.visual)) {
82
121
  rows.push(evidenceRow('visual', visual, classifyVisualEvidence(visual)));
83
122
  }
123
+ for (const provenance of asList(summary.visualProvenance)) {
124
+ rows.push(evidenceRow('visual', provenance, metadataTruth(provenance, 'partial')));
125
+ }
126
+ for (const envelope of asList(summary.missionEnvelope)) {
127
+ rows.push(evidenceRow('evidence', envelope, metadataTruth(envelope, 'partial')));
128
+ }
129
+ for (const rollback of asList(summary.rollbackHint)) {
130
+ rows.push(evidenceRow('evidence', rollback, metadataTruth(rollback, 'partial')));
131
+ }
84
132
  for (const runtime of asList(summary.runtime)) {
85
133
  rows.push(evidenceRow('runtime', runtime, classifyRuntimeEvidence(runtime, options)));
86
134
  }
@@ -166,6 +214,9 @@ export function buildYamCompletionProof(summary = {}, options = {}) {
166
214
  evidence: asList(summary.evidence),
167
215
  visual: asList(summary.visual),
168
216
  runtime: asList(summary.runtime),
217
+ visualProvenance: asList(summary.visualProvenance),
218
+ missionEnvelope: asList(summary.missionEnvelope),
219
+ rollbackHint: asList(summary.rollbackHint),
169
220
  cleanup: summary.cleanup || '',
170
221
  changed: asList(summary.changed),
171
222
  skipped: asList(summary.skipped),
@@ -223,6 +274,24 @@ function classifyCleanupEvidence(value = '') {
223
274
  function commandPassed(value = '') {
224
275
  return /pass|passed|ok|success|exit\s*0|green/i.test(String(value || '')) && !/fail|failed|error|blocked/i.test(String(value || ''));
225
276
  }
277
+ function metadataTruth(value = '', fallback = 'partial') {
278
+ return weakestTruth(readStructuredTruth(value, fallback), 'partial');
279
+ }
280
+ function readStructuredTruth(value = '', fallback = 'partial') {
281
+ if (typeof value === 'object' && value) {
282
+ const candidate = value.truth_status || value.truth;
283
+ return isTruthStatus(candidate) ? candidate : fallback;
284
+ }
285
+ const text = String(value || '');
286
+ try {
287
+ const data = JSON.parse(text);
288
+ const candidate = data.truth_status || data.truth;
289
+ return isTruthStatus(candidate) ? candidate : fallback;
290
+ }
291
+ catch {
292
+ return fallback;
293
+ }
294
+ }
226
295
  function strongestSupportedTruth(rows = []) {
227
296
  if (rows.some((row) => row.truth === 'blocked'))
228
297
  return 'blocked';