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/COMMANDS.md +18 -0
- package/README.md +20 -0
- package/ROADMAP.md +110 -0
- package/dist/bin/yam.js +422 -4
- package/dist/lib/trust-kernel.d.ts +38 -0
- package/dist/lib/trust-kernel.js +69 -0
- package/package.json +2 -1
- package/references/mission.md +24 -0
- package/references/tool-trust-layer.md +13 -0
- package/references/ueye-proof.md +39 -0
- package/skills/mission/SKILL.md +10 -6
- package/skills/ueye/SKILL.md +12 -3
- package/src/bin/yam.ts +408 -3
- package/src/lib/trust-kernel.ts +111 -0
- package/templates/mission-plan.md +13 -0
- package/templates/ueye-comparison.md +15 -0
- package/templates/ueye-review.md +15 -0
- package/yam.manifest.json +2 -2
package/src/bin/yam.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
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
11
|
import {
|
|
9
12
|
TRUTH_STATUSES,
|
|
13
|
+
buildMissionPatchEnvelope,
|
|
14
|
+
buildRollbackHint,
|
|
15
|
+
buildUeyeVisualProvenance,
|
|
10
16
|
buildYamCompletionProof,
|
|
11
17
|
detectDbSafetyText as detectTrustDbSafetyText,
|
|
12
18
|
isTruthStatus
|
|
@@ -128,6 +134,9 @@ Usage:
|
|
|
128
134
|
yam tools doctor [dir]
|
|
129
135
|
yam proof [dir|--from file] [--route route] [--truth status] [--command text] [--evidence text]
|
|
130
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]
|
|
139
|
+
yam release report [--json]
|
|
131
140
|
yam safety [text...]
|
|
132
141
|
yam memory <init|add|list|summary|resolve> [dir] [options]
|
|
133
142
|
yam hook <status|enable|disable|run> [lite] [--global|--project dir]
|
|
@@ -919,6 +928,85 @@ async function safety(args = []) {
|
|
|
919
928
|
printSafetyResult(result);
|
|
920
929
|
}
|
|
921
930
|
|
|
931
|
+
async function release(args = []) {
|
|
932
|
+
const subcommand = args[0] || 'help';
|
|
933
|
+
if (subcommand !== 'report') {
|
|
934
|
+
console.error('usage: yam release report [--json]');
|
|
935
|
+
process.exitCode = 1;
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
const asJson = args.includes('--json');
|
|
939
|
+
const report = runReleaseReport();
|
|
940
|
+
if (asJson) {
|
|
941
|
+
console.log(JSON.stringify(report, null, 2));
|
|
942
|
+
if (!report.ok) process.exitCode = 1;
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
console.log('yam release report');
|
|
946
|
+
console.log(`- Package: ${report.packageName}@${report.version}`);
|
|
947
|
+
console.log(`- Truth status: ${report.truth_status}`);
|
|
948
|
+
for (const check of report.checks) {
|
|
949
|
+
console.log(`- ${check.id}: ${check.status} (${check.duration_ms}ms)`);
|
|
950
|
+
if (check.note) console.log(` ${check.note}`);
|
|
951
|
+
}
|
|
952
|
+
if (report.failed.length) {
|
|
953
|
+
console.log('- Failed:');
|
|
954
|
+
for (const id of report.failed) console.log(` - ${id}`);
|
|
955
|
+
}
|
|
956
|
+
if (!report.ok) process.exitCode = 1;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function runReleaseReport() {
|
|
960
|
+
const startedAt = new Date().toISOString();
|
|
961
|
+
const checks = [
|
|
962
|
+
['typecheck', ['npm', ['run', 'typecheck']]],
|
|
963
|
+
['forbidden_names', ['npm', ['run', 'forbidden-names:check']]],
|
|
964
|
+
['package_boundary', ['npm', ['run', 'package-boundary:check']]],
|
|
965
|
+
['registry_status', ['npm', ['run', 'registry:check']]],
|
|
966
|
+
['cli_smoke', ['npm', ['run', 'cli-smoke']]],
|
|
967
|
+
['dist_freshness', ['npm', ['run', 'dist:freshness']]]
|
|
968
|
+
].map(([id, command]) => runReleaseCheck(id, command));
|
|
969
|
+
const failed = checks.filter((check) => check.status !== 'passed').map((check) => check.id);
|
|
970
|
+
return {
|
|
971
|
+
schema: 'yam.release-report.v1',
|
|
972
|
+
generated_at: startedAt,
|
|
973
|
+
packageName: PACKAGE_JSON.name,
|
|
974
|
+
version: VERSION,
|
|
975
|
+
ok: failed.length === 0,
|
|
976
|
+
truth_status: failed.length === 0 ? 'verified' : 'blocked',
|
|
977
|
+
checks,
|
|
978
|
+
failed
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
function runReleaseCheck(id, commandSpec) {
|
|
983
|
+
const [command, args] = commandSpec;
|
|
984
|
+
const start = Date.now();
|
|
985
|
+
const result = spawnSync(command, args, {
|
|
986
|
+
cwd: ROOT,
|
|
987
|
+
encoding: 'utf8',
|
|
988
|
+
env: { ...process.env, npm_config_cache: path.join(os.tmpdir(), 'yam-npm-cache') },
|
|
989
|
+
timeout: 120000
|
|
990
|
+
});
|
|
991
|
+
const output = [result.stdout, result.stderr].filter(Boolean).join('\n').trim();
|
|
992
|
+
const timedOut = result.error?.message?.includes('ETIMEDOUT') || result.signal === 'SIGTERM';
|
|
993
|
+
return {
|
|
994
|
+
id,
|
|
995
|
+
command: [command, ...args].join(' '),
|
|
996
|
+
status: result.status === 0 ? 'passed' : timedOut ? 'blocked' : 'failed',
|
|
997
|
+
exit_code: typeof result.status === 'number' ? result.status : null,
|
|
998
|
+
duration_ms: Date.now() - start,
|
|
999
|
+
note: summarizeCheckOutput(output || result.error?.message || '')
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function summarizeCheckOutput(output = '') {
|
|
1004
|
+
const lines = String(output || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
1005
|
+
if (!lines.length) return '';
|
|
1006
|
+
const highSignal = lines.filter((line) => !line.startsWith('>')).slice(-4);
|
|
1007
|
+
return highSignal.join(' | ').slice(0, 600);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
922
1010
|
async function readStdinTextIfAvailable() {
|
|
923
1011
|
if (process.stdin.isTTY) return '';
|
|
924
1012
|
const chunks = [];
|
|
@@ -926,6 +1014,261 @@ async function readStdinTextIfAvailable() {
|
|
|
926
1014
|
return Buffer.concat(chunks).toString('utf8').trim();
|
|
927
1015
|
}
|
|
928
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
|
+
|
|
929
1272
|
function detectDbSafetyText(text = '') {
|
|
930
1273
|
return detectTrustDbSafetyText(text);
|
|
931
1274
|
}
|
|
@@ -966,6 +1309,9 @@ async function proof(args = []) {
|
|
|
966
1309
|
printProofList('Evidence', summary.evidence);
|
|
967
1310
|
printProofList('Visual evidence', summary.visual);
|
|
968
1311
|
printProofList('Runtime evidence', summary.runtime);
|
|
1312
|
+
printProofList('Visual provenance', summary.visualProvenance || []);
|
|
1313
|
+
printProofList('Mission patch envelope', summary.missionEnvelope || []);
|
|
1314
|
+
printProofList('Rollback hint', summary.rollbackHint || []);
|
|
969
1315
|
printProofList('Changed surfaces', summary.changed);
|
|
970
1316
|
printProofList('Skipped', summary.skipped);
|
|
971
1317
|
printProofList('Blocked', summary.blocked);
|
|
@@ -1040,6 +1386,18 @@ function buildProofSummary(parsed, artifact) {
|
|
|
1040
1386
|
evidence: [...artifact.evidence, ...arrayFlag(parsed.flags.evidence)],
|
|
1041
1387
|
visual: [...artifact.visual, ...arrayFlag(parsed.flags.visual)],
|
|
1042
1388
|
runtime: [...artifact.runtime, ...arrayFlag(parsed.flags.runtime)],
|
|
1389
|
+
visualProvenance: [
|
|
1390
|
+
...structuredEvidenceList(artifact.visualProvenance),
|
|
1391
|
+
...structuredEvidenceList(parsed.flags.visual_provenance)
|
|
1392
|
+
],
|
|
1393
|
+
missionEnvelope: [
|
|
1394
|
+
...structuredEvidenceList(artifact.missionEnvelope),
|
|
1395
|
+
...structuredEvidenceList(parsed.flags.mission_envelope)
|
|
1396
|
+
],
|
|
1397
|
+
rollbackHint: [
|
|
1398
|
+
...structuredEvidenceList(artifact.rollbackHint),
|
|
1399
|
+
...structuredEvidenceList(parsed.flags.rollback_hint)
|
|
1400
|
+
],
|
|
1043
1401
|
cleanup: parsed.flags.cleanup || artifact.cleanup || '',
|
|
1044
1402
|
changed: [...artifact.changed, ...arrayFlag(parsed.flags.changed)],
|
|
1045
1403
|
skipped: [...artifact.skipped, ...arrayFlag(parsed.flags.skipped)],
|
|
@@ -1090,6 +1448,15 @@ function renderProofMarkdown(summary) {
|
|
|
1090
1448
|
'## Runtime evidence',
|
|
1091
1449
|
...renderProofMarkdownList(summary.runtime),
|
|
1092
1450
|
'',
|
|
1451
|
+
'## Visual provenance',
|
|
1452
|
+
...renderProofMarkdownList(summary.visualProvenance || []),
|
|
1453
|
+
'',
|
|
1454
|
+
'## Mission patch envelope',
|
|
1455
|
+
...renderProofMarkdownList(summary.missionEnvelope || []),
|
|
1456
|
+
'',
|
|
1457
|
+
'## Rollback hint',
|
|
1458
|
+
...renderProofMarkdownList(summary.rollbackHint || []),
|
|
1459
|
+
'',
|
|
1093
1460
|
'## Changed surfaces',
|
|
1094
1461
|
...renderProofMarkdownList(summary.changed),
|
|
1095
1462
|
'',
|
|
@@ -1122,7 +1489,7 @@ function renderProofMarkdownList(values) {
|
|
|
1122
1489
|
function parseProofArgs(args = []) {
|
|
1123
1490
|
const flags: AnyRecord = {};
|
|
1124
1491
|
const positionals: string[] = [];
|
|
1125
|
-
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']);
|
|
1492
|
+
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']);
|
|
1126
1493
|
for (let index = 0; index < args.length; index += 1) {
|
|
1127
1494
|
const arg = args[index];
|
|
1128
1495
|
if (arg.startsWith('--')) {
|
|
@@ -1136,7 +1503,7 @@ function parseProofArgs(args = []) {
|
|
|
1136
1503
|
}
|
|
1137
1504
|
const value = inlineValue ?? args[index + 1] ?? '';
|
|
1138
1505
|
if (inlineValue === undefined) index += 1;
|
|
1139
|
-
if (['command', 'evidence', 'visual', 'runtime', 'changed', 'skipped', 'blocked', 'assumed', 'assumption', 'unverified'].includes(key)) {
|
|
1506
|
+
if (['command', 'evidence', 'visual', 'runtime', 'visual-provenance', 'mission-envelope', 'rollback-hint', 'changed', 'skipped', 'blocked', 'assumed', 'assumption', 'unverified'].includes(key)) {
|
|
1140
1507
|
flags[normalizedKey] = [...arrayFlag(flags[normalizedKey]), value];
|
|
1141
1508
|
} else {
|
|
1142
1509
|
flags[normalizedKey] = value;
|
|
@@ -1174,6 +1541,9 @@ function emptyProofArtifact() {
|
|
|
1174
1541
|
evidence: [],
|
|
1175
1542
|
visual: [],
|
|
1176
1543
|
runtime: [],
|
|
1544
|
+
visualProvenance: [],
|
|
1545
|
+
missionEnvelope: [],
|
|
1546
|
+
rollbackHint: [],
|
|
1177
1547
|
cleanup: '',
|
|
1178
1548
|
changed: [],
|
|
1179
1549
|
skipped: [],
|
|
@@ -1196,6 +1566,9 @@ async function parseProofJson(file, empty) {
|
|
|
1196
1566
|
evidence: arrayFlag(data.evidence),
|
|
1197
1567
|
visual: arrayFlag(data.visual || data.visualEvidence),
|
|
1198
1568
|
runtime: arrayFlag(data.runtime || data.runtimeEvidence),
|
|
1569
|
+
visualProvenance: structuredEvidenceList(data.visualProvenance || data.visual_provenance),
|
|
1570
|
+
missionEnvelope: structuredEvidenceList(data.missionEnvelope || data.mission_envelope),
|
|
1571
|
+
rollbackHint: structuredEvidenceList(data.rollbackHint || data.rollback_hint),
|
|
1199
1572
|
cleanup: String(data.cleanup || data.cleanupStatus || ''),
|
|
1200
1573
|
changed: arrayFlag(data.changed || data.files),
|
|
1201
1574
|
skipped: arrayFlag(data.skipped),
|
|
@@ -1221,6 +1594,9 @@ async function parseProofMarkdown(file, empty) {
|
|
|
1221
1594
|
evidence: readProofList(text, 'Evidence'),
|
|
1222
1595
|
visual: readProofList(text, 'Visual evidence'),
|
|
1223
1596
|
runtime: firstProofList(text, ['Runtime evidence', 'Processes', 'tmux']),
|
|
1597
|
+
visualProvenance: readProofList(text, 'Visual provenance'),
|
|
1598
|
+
missionEnvelope: readProofList(text, 'Mission patch envelope'),
|
|
1599
|
+
rollbackHint: readProofList(text, 'Rollback hint'),
|
|
1224
1600
|
cleanup: readProofField(text, 'Cleanup') || readProofField(text, 'Cleanup status'),
|
|
1225
1601
|
changed: firstProofList(text, ['Changed surfaces', 'Files']),
|
|
1226
1602
|
skipped: readProofList(text, 'Skipped'),
|
|
@@ -1267,9 +1643,36 @@ function arrayFlag(value) {
|
|
|
1267
1643
|
return Array.isArray(value) ? value.filter(Boolean) : [value].filter(Boolean);
|
|
1268
1644
|
}
|
|
1269
1645
|
|
|
1646
|
+
function structuredEvidenceList(value) {
|
|
1647
|
+
return arrayFlag(value).map((item) => formatStructuredEvidence(item)).filter(Boolean);
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
function formatStructuredEvidence(value) {
|
|
1651
|
+
if (!value) return '';
|
|
1652
|
+
if (typeof value === 'object') return JSON.stringify(value);
|
|
1653
|
+
const text = String(value).trim();
|
|
1654
|
+
if (!text) return '';
|
|
1655
|
+
if (!text.startsWith('{')) return text;
|
|
1656
|
+
try {
|
|
1657
|
+
const data = JSON.parse(text);
|
|
1658
|
+
if (data.schema === 'yam.ueye-visual-provenance.v1' || data.source_kind || data.reference_id || data.screenshot_id) {
|
|
1659
|
+
return JSON.stringify(buildUeyeVisualProvenance(data));
|
|
1660
|
+
}
|
|
1661
|
+
if (data.schema === 'yam.mission-patch-envelope.v1' || data.agent_id || data.assigned_scope) {
|
|
1662
|
+
return JSON.stringify(buildMissionPatchEnvelope(data));
|
|
1663
|
+
}
|
|
1664
|
+
if (data.schema === 'yam.rollback-hint.v1' || data.touched_files || data.safe_revert_note) {
|
|
1665
|
+
return JSON.stringify(buildRollbackHint(data));
|
|
1666
|
+
}
|
|
1667
|
+
return JSON.stringify(data);
|
|
1668
|
+
} catch {
|
|
1669
|
+
return text;
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1270
1673
|
function hasProofEvidence(parsed) {
|
|
1271
1674
|
const flags = parsed.flags || {};
|
|
1272
|
-
return ['command', 'evidence', 'visual', 'runtime', 'changed', 'skipped', 'blocked'].some((key) => arrayFlag(flags[key]).length > 0) || Boolean(flags.cleanup);
|
|
1675
|
+
return ['command', 'evidence', 'visual', 'runtime', 'visual_provenance', 'mission_envelope', 'rollback_hint', 'changed', 'skipped', 'blocked'].some((key) => arrayFlag(flags[key]).length > 0) || Boolean(flags.cleanup);
|
|
1273
1676
|
}
|
|
1274
1677
|
|
|
1275
1678
|
function printProofList(label, values) {
|
|
@@ -1961,6 +2364,8 @@ async function main() {
|
|
|
1961
2364
|
if (command === 'measure') return measure(process.argv[3], process.argv.slice(4));
|
|
1962
2365
|
if (command === 'tools') return tools(process.argv.slice(3));
|
|
1963
2366
|
if (command === 'proof') return proof(process.argv.slice(3));
|
|
2367
|
+
if (command === 'ueye') return ueye(process.argv.slice(3));
|
|
2368
|
+
if (command === 'release') return release(process.argv.slice(3));
|
|
1964
2369
|
if (command === 'safety') return safety(process.argv.slice(3));
|
|
1965
2370
|
if (command === 'memory') return memory(process.argv.slice(3));
|
|
1966
2371
|
if (command === 'hook') return hook(process.argv.slice(3));
|
package/src/lib/trust-kernel.ts
CHANGED
|
@@ -47,6 +47,38 @@ export interface RuntimeTruthMatrix {
|
|
|
47
47
|
blockers: string[];
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
export interface UeyeVisualProvenance {
|
|
51
|
+
schema: 'yam.ueye-visual-provenance.v1';
|
|
52
|
+
source_kind: string;
|
|
53
|
+
source_path: string;
|
|
54
|
+
source_hash: string;
|
|
55
|
+
reference_id: string;
|
|
56
|
+
screenshot_id: string;
|
|
57
|
+
local_only: boolean;
|
|
58
|
+
redacted: boolean;
|
|
59
|
+
operator_provided: boolean;
|
|
60
|
+
comparison_result: string;
|
|
61
|
+
truth_status: TruthStatus;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface RollbackHint {
|
|
65
|
+
schema: 'yam.rollback-hint.v1';
|
|
66
|
+
touched_files: string[];
|
|
67
|
+
generated_files: string[];
|
|
68
|
+
before_check: string;
|
|
69
|
+
safe_revert_note: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface MissionPatchEnvelope {
|
|
73
|
+
schema: 'yam.mission-patch-envelope.v1';
|
|
74
|
+
agent_id: string;
|
|
75
|
+
assigned_scope: string;
|
|
76
|
+
changed_files: string[];
|
|
77
|
+
verification_hint: string;
|
|
78
|
+
rollback_hint: RollbackHint;
|
|
79
|
+
truth_status: TruthStatus;
|
|
80
|
+
}
|
|
81
|
+
|
|
50
82
|
export interface FakeRealPolicyResult {
|
|
51
83
|
schema: 'yam.fake-real-policy.v1';
|
|
52
84
|
ok: boolean;
|
|
@@ -71,6 +103,9 @@ export interface ProofSummary {
|
|
|
71
103
|
blocked?: unknown;
|
|
72
104
|
assumptions?: unknown;
|
|
73
105
|
unverified?: unknown;
|
|
106
|
+
visualProvenance?: unknown;
|
|
107
|
+
missionEnvelope?: unknown;
|
|
108
|
+
rollbackHint?: unknown;
|
|
74
109
|
}
|
|
75
110
|
|
|
76
111
|
export interface ProofOptions {
|
|
@@ -101,6 +136,9 @@ export interface YamCompletionProof {
|
|
|
101
136
|
evidence: string[];
|
|
102
137
|
visual: string[];
|
|
103
138
|
runtime: string[];
|
|
139
|
+
visualProvenance: string[];
|
|
140
|
+
missionEnvelope: string[];
|
|
141
|
+
rollbackHint: string[];
|
|
104
142
|
cleanup: unknown;
|
|
105
143
|
changed: string[];
|
|
106
144
|
skipped: string[];
|
|
@@ -113,6 +151,48 @@ export interface YamCompletionProof {
|
|
|
113
151
|
runtimeTruth: RuntimeTruthMatrix;
|
|
114
152
|
}
|
|
115
153
|
|
|
154
|
+
export function buildUeyeVisualProvenance(input: Partial<UeyeVisualProvenance> = {}): UeyeVisualProvenance {
|
|
155
|
+
const truth = isTruthStatus(input.truth_status) ? input.truth_status : 'partial';
|
|
156
|
+
return {
|
|
157
|
+
schema: 'yam.ueye-visual-provenance.v1',
|
|
158
|
+
source_kind: String(input.source_kind || ''),
|
|
159
|
+
source_path: String(input.source_path || ''),
|
|
160
|
+
source_hash: String(input.source_hash || 'unknown'),
|
|
161
|
+
reference_id: String(input.reference_id || ''),
|
|
162
|
+
screenshot_id: String(input.screenshot_id || ''),
|
|
163
|
+
local_only: Boolean(input.local_only),
|
|
164
|
+
redacted: Boolean(input.redacted),
|
|
165
|
+
operator_provided: Boolean(input.operator_provided),
|
|
166
|
+
comparison_result: String(input.comparison_result || 'not-verified'),
|
|
167
|
+
truth_status: truth
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function buildRollbackHint(input: Partial<RollbackHint> = {}): RollbackHint {
|
|
172
|
+
return {
|
|
173
|
+
schema: 'yam.rollback-hint.v1',
|
|
174
|
+
touched_files: asList(input.touched_files),
|
|
175
|
+
generated_files: asList(input.generated_files),
|
|
176
|
+
before_check: String(input.before_check || ''),
|
|
177
|
+
safe_revert_note: String(input.safe_revert_note || '')
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function buildMissionPatchEnvelope(input: Partial<MissionPatchEnvelope> = {}): MissionPatchEnvelope {
|
|
182
|
+
const rollback = input.rollback_hint && typeof input.rollback_hint === 'object'
|
|
183
|
+
? buildRollbackHint(input.rollback_hint)
|
|
184
|
+
: buildRollbackHint();
|
|
185
|
+
return {
|
|
186
|
+
schema: 'yam.mission-patch-envelope.v1',
|
|
187
|
+
agent_id: String(input.agent_id || ''),
|
|
188
|
+
assigned_scope: String(input.assigned_scope || ''),
|
|
189
|
+
changed_files: asList(input.changed_files),
|
|
190
|
+
verification_hint: String(input.verification_hint || ''),
|
|
191
|
+
rollback_hint: rollback,
|
|
192
|
+
truth_status: isTruthStatus(input.truth_status) ? input.truth_status : 'partial'
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
116
196
|
const TRUTH_RANK: Readonly<Record<TruthStatus, number>> = Object.freeze({
|
|
117
197
|
blocked: 0,
|
|
118
198
|
real_required_missing: 0,
|
|
@@ -184,6 +264,15 @@ export function classifyEvidenceTruth(summary: ProofSummary = {}, options: Proof
|
|
|
184
264
|
for (const visual of asList(summary.visual)) {
|
|
185
265
|
rows.push(evidenceRow('visual', visual, classifyVisualEvidence(visual)));
|
|
186
266
|
}
|
|
267
|
+
for (const provenance of asList(summary.visualProvenance)) {
|
|
268
|
+
rows.push(evidenceRow('visual', provenance, metadataTruth(provenance, 'partial')));
|
|
269
|
+
}
|
|
270
|
+
for (const envelope of asList(summary.missionEnvelope)) {
|
|
271
|
+
rows.push(evidenceRow('evidence', envelope, metadataTruth(envelope, 'partial')));
|
|
272
|
+
}
|
|
273
|
+
for (const rollback of asList(summary.rollbackHint)) {
|
|
274
|
+
rows.push(evidenceRow('evidence', rollback, metadataTruth(rollback, 'partial')));
|
|
275
|
+
}
|
|
187
276
|
for (const runtime of asList(summary.runtime)) {
|
|
188
277
|
rows.push(evidenceRow('runtime', runtime, classifyRuntimeEvidence(runtime, options)));
|
|
189
278
|
}
|
|
@@ -267,6 +356,9 @@ export function buildYamCompletionProof(summary: ProofSummary = {}, options: Pro
|
|
|
267
356
|
evidence: asList(summary.evidence),
|
|
268
357
|
visual: asList(summary.visual),
|
|
269
358
|
runtime: asList(summary.runtime),
|
|
359
|
+
visualProvenance: asList(summary.visualProvenance),
|
|
360
|
+
missionEnvelope: asList(summary.missionEnvelope),
|
|
361
|
+
rollbackHint: asList(summary.rollbackHint),
|
|
270
362
|
cleanup: summary.cleanup || '',
|
|
271
363
|
changed: asList(summary.changed),
|
|
272
364
|
skipped: asList(summary.skipped),
|
|
@@ -317,6 +409,25 @@ function commandPassed(value: unknown = ''): boolean {
|
|
|
317
409
|
return /pass|passed|ok|success|exit\s*0|green/i.test(String(value || '')) && !/fail|failed|error|blocked/i.test(String(value || ''));
|
|
318
410
|
}
|
|
319
411
|
|
|
412
|
+
function metadataTruth(value: unknown = '', fallback: TruthStatus = 'partial'): TruthStatus {
|
|
413
|
+
return weakestTruth(readStructuredTruth(value, fallback), 'partial');
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function readStructuredTruth(value: unknown = '', fallback: TruthStatus = 'partial'): TruthStatus {
|
|
417
|
+
if (typeof value === 'object' && value) {
|
|
418
|
+
const candidate = (value as { truth_status?: unknown; truth?: unknown }).truth_status || (value as { truth?: unknown }).truth;
|
|
419
|
+
return isTruthStatus(candidate) ? candidate : fallback;
|
|
420
|
+
}
|
|
421
|
+
const text = String(value || '');
|
|
422
|
+
try {
|
|
423
|
+
const data = JSON.parse(text);
|
|
424
|
+
const candidate = data.truth_status || data.truth;
|
|
425
|
+
return isTruthStatus(candidate) ? candidate : fallback;
|
|
426
|
+
} catch {
|
|
427
|
+
return fallback;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
320
431
|
function strongestSupportedTruth(rows: EvidenceRow[] = []): TruthStatus {
|
|
321
432
|
if (rows.some((row) => row.truth === 'blocked')) return 'blocked';
|
|
322
433
|
if (rows.some((row) => row.truth === 'real_required_missing')) return 'real_required_missing';
|
|
@@ -29,6 +29,19 @@ Subagent 판단:
|
|
|
29
29
|
- 이유:
|
|
30
30
|
- subagent가 불가능하거나 불필요하면 기본적으로 $deep으로 전환:
|
|
31
31
|
|
|
32
|
+
Patch envelope:
|
|
33
|
+
- agent_id:
|
|
34
|
+
- assigned_scope:
|
|
35
|
+
- changed_files:
|
|
36
|
+
- verification_hint:
|
|
37
|
+
- truth_status: verified / partial / skipped / blocked / assumed
|
|
38
|
+
|
|
39
|
+
Rollback hint:
|
|
40
|
+
- touched_files:
|
|
41
|
+
- generated_files:
|
|
42
|
+
- before_check:
|
|
43
|
+
- safe_revert_note:
|
|
44
|
+
|
|
32
45
|
검증:
|
|
33
46
|
- 필요한 가장 작은 검증 명령을 우선 실행
|
|
34
47
|
- 필요하면 tmux/dev server/browser QA/process cleanup proof 사용
|