yam-flow 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COMMANDS.md +14 -0
- package/README.md +31 -0
- package/dist/bin/yam.js +576 -21
- package/dist/lib/trust-kernel.d.ts +41 -0
- package/dist/lib/trust-kernel.js +117 -0
- package/package.json +2 -1
- package/references/ueye-proof.md +40 -0
- package/skills/ueye/SKILL.md +13 -0
- package/src/bin/yam.ts +558 -19
- package/src/lib/trust-kernel.ts +147 -0
package/src/bin/yam.ts
CHANGED
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
3
4
|
import fs from 'node:fs';
|
|
4
5
|
import fsp from 'node:fs/promises';
|
|
6
|
+
import { createRequire } from 'node:module';
|
|
5
7
|
import os from 'node:os';
|
|
6
8
|
import path from 'node:path';
|
|
7
9
|
import process from 'node:process';
|
|
8
10
|
import { fileURLToPath } from 'node:url';
|
|
9
11
|
import {
|
|
10
12
|
TRUTH_STATUSES,
|
|
13
|
+
buildMediaGenerationProof,
|
|
11
14
|
buildMissionPatchEnvelope,
|
|
12
15
|
buildRollbackHint,
|
|
16
|
+
buildRuntimeBackendEvidence,
|
|
13
17
|
buildUeyeVisualProvenance,
|
|
18
|
+
buildUeyeRunReport,
|
|
14
19
|
buildYamCompletionProof,
|
|
15
20
|
detectDbSafetyText as detectTrustDbSafetyText,
|
|
16
21
|
isTruthStatus
|
|
@@ -132,6 +137,10 @@ Usage:
|
|
|
132
137
|
yam tools doctor [dir]
|
|
133
138
|
yam proof [dir|--from file] [--route route] [--truth status] [--command text] [--evidence text]
|
|
134
139
|
yam proof write [dir] [--format json|md] [--out file] [--route route] [--truth status] [--command text]
|
|
140
|
+
yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
|
|
141
|
+
yam ueye compare --reference ref.png --actual screenshot.png [--json]
|
|
142
|
+
yam ueye report [--reference ref.png] [--actual screenshot.png] [--json]
|
|
143
|
+
yam media proof [--requested] [--attempted] [--output file] [--json]
|
|
135
144
|
yam release report [--json]
|
|
136
145
|
yam safety [text...]
|
|
137
146
|
yam memory <init|add|list|summary|resolve> [dir] [options]
|
|
@@ -140,7 +149,7 @@ Usage:
|
|
|
140
149
|
yam tune-log [dir]
|
|
141
150
|
yam install
|
|
142
151
|
yam uninstall
|
|
143
|
-
yam doctor
|
|
152
|
+
yam doctor [--json]
|
|
144
153
|
yam examples
|
|
145
154
|
yam path
|
|
146
155
|
yam version
|
|
@@ -371,7 +380,33 @@ function failOrReport(label, issues, quiet = false) {
|
|
|
371
380
|
return issues.length;
|
|
372
381
|
}
|
|
373
382
|
|
|
374
|
-
async function doctor() {
|
|
383
|
+
async function doctor(args = []) {
|
|
384
|
+
const flags = parseSimpleFlags(args, new Set(['json']));
|
|
385
|
+
const report = await buildDoctorReport();
|
|
386
|
+
if (flags.json) {
|
|
387
|
+
console.log(JSON.stringify(report, null, 2));
|
|
388
|
+
if (!report.ok) process.exitCode = 1;
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
if (report.ok) {
|
|
392
|
+
console.log('yam doctor: ok');
|
|
393
|
+
console.log('No hooks, automations, or global config are required.');
|
|
394
|
+
console.log('Skill install state is reported by `yam status`.');
|
|
395
|
+
console.log(`yam-lite hook: ${report.yamLiteHook} (optional)`);
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
console.log('yam doctor: issues');
|
|
400
|
+
for (const issue of report.issues) console.log(`- ${issue}`);
|
|
401
|
+
if (report.nextActions.length) {
|
|
402
|
+
console.log('');
|
|
403
|
+
console.log('Next actions:');
|
|
404
|
+
for (const action of report.nextActions) console.log(`- ${action}`);
|
|
405
|
+
}
|
|
406
|
+
process.exitCode = 1;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
async function buildDoctorReport() {
|
|
375
410
|
const issues = [];
|
|
376
411
|
for (const skill of SKILLS) {
|
|
377
412
|
if (!await exists(path.join(ROOT, 'skills', skill, 'SKILL.md'))) issues.push(`missing source skill: ${skill}`);
|
|
@@ -394,19 +429,28 @@ async function doctor() {
|
|
|
394
429
|
}
|
|
395
430
|
const verifyIssues = await verify({ quiet: true });
|
|
396
431
|
if (verifyIssues > 0) issues.push(`verify reported ${verifyIssues} issue(s)`);
|
|
432
|
+
const globalHook = await readJsonOrDefault(path.join(os.homedir(), '.codex', 'hooks.json'), {});
|
|
433
|
+
return {
|
|
434
|
+
schema: 'yam.doctor.v1',
|
|
435
|
+
generatedAt: new Date().toISOString(),
|
|
436
|
+
ok: issues.length === 0,
|
|
437
|
+
issues,
|
|
438
|
+
nextActions: doctorNextActions(issues),
|
|
439
|
+
yamLiteHook: hookConfigHasYamLite(globalHook) ? 'enabled' : 'disabled'
|
|
440
|
+
};
|
|
441
|
+
}
|
|
397
442
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
443
|
+
function doctorNextActions(issues = []) {
|
|
444
|
+
const actions = [];
|
|
445
|
+
for (const issue of issues) {
|
|
446
|
+
if (/missing source skill/i.test(issue)) actions.push('run from a complete yam-flow checkout or reinstall the published package');
|
|
447
|
+
else if (/missing truth matrix/i.test(issue)) actions.push('restore references/truth-matrix.md before publishing');
|
|
448
|
+
else if (/unexpected local hooks/i.test(issue)) actions.push('remove project hooks unless this repo is intentionally dogfooding them');
|
|
449
|
+
else if (/unexpected .*automation/i.test(issue)) actions.push('remove stale yam/timeto automations before claiming clean install state');
|
|
450
|
+
else if (/legacy|retired/i.test(issue)) actions.push('run `yam install` to replace old skill entries');
|
|
451
|
+
else if (/verify reported/i.test(issue)) actions.push('run `npm run verify` and fix the reported package boundary or metadata issue');
|
|
405
452
|
}
|
|
406
|
-
|
|
407
|
-
console.log('yam doctor: issues');
|
|
408
|
-
for (const issue of issues) console.log(`- ${issue}`);
|
|
409
|
-
process.exitCode = 1;
|
|
453
|
+
return [...new Set(actions)];
|
|
410
454
|
}
|
|
411
455
|
|
|
412
456
|
async function tools(args = []) {
|
|
@@ -467,6 +511,7 @@ async function buildToolsDoctorReport(targetDir = process.cwd()) {
|
|
|
467
511
|
packageInfo.packageJson ? JSON.stringify(packageInfo.pkg?.devDependencies || {}) : ''
|
|
468
512
|
].join('\n'));
|
|
469
513
|
const projectSurfaces = await detectProjectToolSurfaces(dir, packageInfo.pkg);
|
|
514
|
+
const frameworkChecklist = await detectFrameworkChecklist(dir, packageInfo.pkg);
|
|
470
515
|
const sqlScan = await scanSqlFiles(dir);
|
|
471
516
|
const rows = [
|
|
472
517
|
readinessRow('Codex home', await exists(codexHome) ? 'ready' : 'missing', codexHome),
|
|
@@ -496,8 +541,16 @@ async function buildToolsDoctorReport(targetDir = process.cwd()) {
|
|
|
496
541
|
packageManager: detection.packageJson ? detection.packageManager : null,
|
|
497
542
|
instructionSurfaces,
|
|
498
543
|
projectSurfaces,
|
|
544
|
+
frameworkChecklist,
|
|
499
545
|
sqlScan,
|
|
500
546
|
riskNotes,
|
|
547
|
+
nextActions: toolsDoctorNextActions({
|
|
548
|
+
pack,
|
|
549
|
+
rows,
|
|
550
|
+
riskNotes,
|
|
551
|
+
frameworkChecklist,
|
|
552
|
+
commands: detection.packageJson ? detection.commands : {}
|
|
553
|
+
}),
|
|
501
554
|
routeRecommendations: {
|
|
502
555
|
dbSupabase: safety.hits.length || sqlScan.findings.length ? '$deep required before claiming safe' : '$deep when destructive or production mutation appears',
|
|
503
556
|
currentDocs: 'use current-docs proof only when version/freshness matters',
|
|
@@ -530,6 +583,14 @@ function printToolsDoctorReport(data) {
|
|
|
530
583
|
for (const surface of data.projectSurfaces) console.log(`- ${surface}`);
|
|
531
584
|
}
|
|
532
585
|
|
|
586
|
+
if (data.frameworkChecklist?.detected) {
|
|
587
|
+
console.log('');
|
|
588
|
+
console.log(`Framework checklist: ${data.frameworkChecklist.framework}`);
|
|
589
|
+
for (const check of data.frameworkChecklist.checks) {
|
|
590
|
+
console.log(`- ${check.id}: ${check.label} (${check.route})`);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
533
594
|
if (data.sqlScan.filesScanned > 0) {
|
|
534
595
|
console.log('');
|
|
535
596
|
console.log(`SQL scan: ${data.sqlScan.filesScanned} file(s), ${data.sqlScan.findings.length} risk finding(s)`);
|
|
@@ -544,6 +605,12 @@ function printToolsDoctorReport(data) {
|
|
|
544
605
|
for (const note of data.riskNotes) console.log(`- ${note.level}: ${note.reason}`);
|
|
545
606
|
}
|
|
546
607
|
|
|
608
|
+
if (data.nextActions?.length) {
|
|
609
|
+
console.log('');
|
|
610
|
+
console.log('Next actions:');
|
|
611
|
+
for (const action of data.nextActions) console.log(`- ${action}`);
|
|
612
|
+
}
|
|
613
|
+
|
|
547
614
|
console.log('');
|
|
548
615
|
console.log('Route recommendations:');
|
|
549
616
|
console.log(`- DB/Supabase destructive or production write work: ${data.routeRecommendations.dbSupabase}`);
|
|
@@ -616,6 +683,56 @@ async function detectProjectToolSurfaces(dir, pkg = null) {
|
|
|
616
683
|
return surfaces;
|
|
617
684
|
}
|
|
618
685
|
|
|
686
|
+
async function detectFrameworkChecklist(dir, pkg = null) {
|
|
687
|
+
const deps = {
|
|
688
|
+
...pkg?.dependencies,
|
|
689
|
+
...pkg?.devDependencies
|
|
690
|
+
};
|
|
691
|
+
const depNames = Object.keys(deps || {});
|
|
692
|
+
const hasReact = depNames.includes('react');
|
|
693
|
+
const hasNext = depNames.includes('next') || await exists(path.join(dir, 'next.config.js')) || await exists(path.join(dir, 'next.config.mjs')) || await exists(path.join(dir, 'next.config.ts'));
|
|
694
|
+
const hasVite = depNames.includes('vite') || await exists(path.join(dir, 'vite.config.js')) || await exists(path.join(dir, 'vite.config.ts'));
|
|
695
|
+
const framework = hasNext ? 'next-app' : hasReact && hasVite ? 'react-vite' : hasReact ? 'react' : pkg ? 'node-package' : 'unknown';
|
|
696
|
+
const uiLike = hasReact || hasNext || hasVite || await exists(path.join(dir, 'app')) || await exists(path.join(dir, 'src', 'app')) || await exists(path.join(dir, 'pages'));
|
|
697
|
+
const checks = [];
|
|
698
|
+
if (uiLike) {
|
|
699
|
+
checks.push(
|
|
700
|
+
checklistRow('states', 'default/loading/error/empty states are reachable before claiming complete', '$ueye'),
|
|
701
|
+
checklistRow('responsive', 'mobile and desktop layouts avoid overlap and clipped text', '$ueye'),
|
|
702
|
+
checklistRow('accessibility', 'interactive controls have accessible labels and keyboard path', '$ueye'),
|
|
703
|
+
checklistRow('runtime', 'browser or in-app runtime evidence is recorded when claiming visual verified', '$ueye')
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
if (hasNext) {
|
|
707
|
+
checks.push(checklistRow('boundaries', 'server/client boundaries and async data states match the framework pattern', '$deep when risky'));
|
|
708
|
+
}
|
|
709
|
+
if (pkg) {
|
|
710
|
+
checks.push(checklistRow('package', 'run the smallest honest type/build/package check before publishing', '$quick or $deep'));
|
|
711
|
+
}
|
|
712
|
+
return {
|
|
713
|
+
schema: 'yam.framework-checklist.v1',
|
|
714
|
+
detected: checks.length > 0,
|
|
715
|
+
uiLike,
|
|
716
|
+
framework,
|
|
717
|
+
routeHint: uiLike ? '$ueye for visual claims; $deep when runtime/data risk appears' : '$quick by default; deepen on risk',
|
|
718
|
+
checks
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function checklistRow(id, label, route) {
|
|
723
|
+
return { id, label, route };
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function toolsDoctorNextActions({ pack, rows, riskNotes, frameworkChecklist, commands }) {
|
|
727
|
+
const actions = [];
|
|
728
|
+
if (!pack) actions.push('create or refresh yam.project.md so route choices reuse local project context');
|
|
729
|
+
if (rows.some((row) => row.name === 'Yam skills' && row.status !== 'ready')) actions.push('run `yam install` and restart Codex before relying on installed skills');
|
|
730
|
+
if (riskNotes.some((note) => note.level === 'danger')) actions.push('use $deep before claiming destructive database or production write safety');
|
|
731
|
+
if (frameworkChecklist?.detected && !commands?.build && !commands?.typecheck) actions.push('add or document a small build/typecheck command for implementation verification');
|
|
732
|
+
if (frameworkChecklist?.uiLike) actions.push('for Ueye verified claims, record reference, actual screenshot, and comparison result');
|
|
733
|
+
return [...new Set(actions)];
|
|
734
|
+
}
|
|
735
|
+
|
|
619
736
|
async function scanSqlFiles(rootDir, { maxFiles = 30, maxBytes = 200000, maxDepth = 5 } = {}) {
|
|
620
737
|
const root = path.resolve(rootDir || process.cwd());
|
|
621
738
|
const files = [];
|
|
@@ -1010,6 +1127,376 @@ async function readStdinTextIfAvailable() {
|
|
|
1010
1127
|
return Buffer.concat(chunks).toString('utf8').trim();
|
|
1011
1128
|
}
|
|
1012
1129
|
|
|
1130
|
+
async function ueye(args = []) {
|
|
1131
|
+
const subcommand = args[0] || 'help';
|
|
1132
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return ueyeUsage();
|
|
1133
|
+
if (subcommand === 'capture') return ueyeCapture(args.slice(1));
|
|
1134
|
+
if (subcommand === 'compare') return ueyeCompare(args.slice(1));
|
|
1135
|
+
if (subcommand === 'report') return ueyeReport(args.slice(1));
|
|
1136
|
+
console.error(`unknown ueye command: ${subcommand}`);
|
|
1137
|
+
return ueyeUsage();
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
function ueyeUsage() {
|
|
1141
|
+
console.log(`yam ueye
|
|
1142
|
+
|
|
1143
|
+
Opt-in visual evidence helpers. Ueye stays one skill: fast by default, capture/compare only when verified visual claims need real evidence.
|
|
1144
|
+
|
|
1145
|
+
Usage:
|
|
1146
|
+
yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
|
|
1147
|
+
yam ueye compare --reference ref.png --actual screenshot.png [--json]
|
|
1148
|
+
yam ueye report [--reference ref.png] [--actual screenshot.png] [--design-quality pass|needs-polish|fails|not-checked] [--json]
|
|
1149
|
+
|
|
1150
|
+
Notes:
|
|
1151
|
+
capture uses a locally available Playwright install when present. It does not download browsers or install dependencies.
|
|
1152
|
+
compare uses local files only and reports sha256, dimensions, comparison_result, and proof-ready visual provenance.
|
|
1153
|
+
report produces a proof-ready Ueye visual run report without requiring a new capture.
|
|
1154
|
+
`);
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
async function ueyeCapture(args = []) {
|
|
1158
|
+
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return ueyeUsage();
|
|
1159
|
+
const flags = parseSimpleFlags(args, new Set(['url', 'out', 'viewport', 'wait-until', 'timeout', 'full-page', 'json']));
|
|
1160
|
+
const url = String(flags.url || '');
|
|
1161
|
+
const out = String(flags.out || '');
|
|
1162
|
+
if (!url || !out) {
|
|
1163
|
+
console.error('usage: yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]');
|
|
1164
|
+
process.exitCode = 1;
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
const target = path.resolve(expandHome(out));
|
|
1169
|
+
const viewport = parseViewport(String(flags.viewport || '1440x900'));
|
|
1170
|
+
const timeout = Number(flags.timeout || 30000);
|
|
1171
|
+
const waitUntil = String(flags.wait_until || 'networkidle');
|
|
1172
|
+
let browser;
|
|
1173
|
+
|
|
1174
|
+
try {
|
|
1175
|
+
const playwright = await loadOptionalPlaywright();
|
|
1176
|
+
const chromium = playwright?.chromium;
|
|
1177
|
+
if (!chromium) throw new Error('Playwright chromium launcher was not found');
|
|
1178
|
+
await fsp.mkdir(path.dirname(target), { recursive: true });
|
|
1179
|
+
browser = await chromium.launch({ headless: true });
|
|
1180
|
+
const page = await browser.newPage({ viewport });
|
|
1181
|
+
await page.goto(url, { waitUntil, timeout });
|
|
1182
|
+
await page.screenshot({ path: target, fullPage: Boolean(flags.full_page) });
|
|
1183
|
+
const info = await imageFileInfo(target);
|
|
1184
|
+
const result = {
|
|
1185
|
+
schema: 'yam.ueye-capture.v1',
|
|
1186
|
+
status: 'verified',
|
|
1187
|
+
url,
|
|
1188
|
+
out: target,
|
|
1189
|
+
viewport: `${viewport.width}x${viewport.height}`,
|
|
1190
|
+
full_page: Boolean(flags.full_page),
|
|
1191
|
+
sha256: info.sha256,
|
|
1192
|
+
dimensions: info.dimensions,
|
|
1193
|
+
visual_evidence: `browser screenshot captured: ${target} (${info.dimensions}, sha256:${info.sha256})`
|
|
1194
|
+
};
|
|
1195
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Ueye capture');
|
|
1196
|
+
} catch (error) {
|
|
1197
|
+
const result = {
|
|
1198
|
+
schema: 'yam.ueye-capture.v1',
|
|
1199
|
+
status: 'blocked',
|
|
1200
|
+
url,
|
|
1201
|
+
out: target,
|
|
1202
|
+
reason: errorMessage(error),
|
|
1203
|
+
next_action: 'Install Playwright in the current project or provide a user/browser screenshot, then use `yam ueye compare`.',
|
|
1204
|
+
visual_cap: 'blocked'
|
|
1205
|
+
};
|
|
1206
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Ueye capture');
|
|
1207
|
+
process.exitCode = 1;
|
|
1208
|
+
} finally {
|
|
1209
|
+
if (browser) await browser.close().catch(() => {});
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
async function ueyeCompare(args = []) {
|
|
1214
|
+
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return ueyeUsage();
|
|
1215
|
+
const flags = parseSimpleFlags(args, new Set(['reference', 'ref', 'actual', 'screenshot', 'json', 'reference-id', 'screenshot-id']));
|
|
1216
|
+
const reference = String(flags.reference || flags.ref || '');
|
|
1217
|
+
const actual = String(flags.actual || flags.screenshot || '');
|
|
1218
|
+
if (!reference || !actual) {
|
|
1219
|
+
console.error('usage: yam ueye compare --reference ref.png --actual screenshot.png [--json]');
|
|
1220
|
+
process.exitCode = 1;
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
const referencePath = path.resolve(expandHome(reference));
|
|
1225
|
+
const actualPath = path.resolve(expandHome(actual));
|
|
1226
|
+
try {
|
|
1227
|
+
const referenceInfo = await imageFileInfo(referencePath);
|
|
1228
|
+
const actualInfo = await imageFileInfo(actualPath);
|
|
1229
|
+
if (referenceInfo.dimensions === 'unknown' || actualInfo.dimensions === 'unknown') {
|
|
1230
|
+
throw new Error('compare requires image files with readable PNG, JPEG, or GIF dimensions');
|
|
1231
|
+
}
|
|
1232
|
+
const exactMatch = referenceInfo.sha256 === actualInfo.sha256;
|
|
1233
|
+
const sameDimensions = referenceInfo.dimensions === actualInfo.dimensions;
|
|
1234
|
+
const comparisonResult = exactMatch ? 'matched' : 'different';
|
|
1235
|
+
const truthStatus = exactMatch ? 'verified' : 'partial';
|
|
1236
|
+
const provenance = buildUeyeVisualProvenance({
|
|
1237
|
+
source_kind: 'implementation_screenshot',
|
|
1238
|
+
source_path: actualPath,
|
|
1239
|
+
source_hash: actualInfo.sha256,
|
|
1240
|
+
reference_id: String(flags.reference_id || path.basename(referencePath)),
|
|
1241
|
+
screenshot_id: String(flags.screenshot_id || path.basename(actualPath)),
|
|
1242
|
+
local_only: true,
|
|
1243
|
+
redacted: false,
|
|
1244
|
+
operator_provided: false,
|
|
1245
|
+
comparison_result: comparisonResult,
|
|
1246
|
+
truth_status: truthStatus
|
|
1247
|
+
});
|
|
1248
|
+
const result = {
|
|
1249
|
+
schema: 'yam.ueye-compare.v1',
|
|
1250
|
+
status: truthStatus,
|
|
1251
|
+
comparison_result: comparisonResult,
|
|
1252
|
+
exact_match: exactMatch,
|
|
1253
|
+
same_dimensions: sameDimensions,
|
|
1254
|
+
reference: referenceInfo,
|
|
1255
|
+
actual: actualInfo,
|
|
1256
|
+
visual_evidence: `browser/local screenshot comparison executed: reference=${referencePath}, actual=${actualPath}, result=${comparisonResult}`,
|
|
1257
|
+
visual_provenance: provenance,
|
|
1258
|
+
proof_hint: `yam proof --route ueye --truth ${truthStatus} --visual "browser/local screenshot comparison executed: ${comparisonResult}" --visual-provenance '${JSON.stringify(provenance)}' --require-visual`
|
|
1259
|
+
};
|
|
1260
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Ueye compare');
|
|
1261
|
+
} catch (error) {
|
|
1262
|
+
const result = {
|
|
1263
|
+
schema: 'yam.ueye-compare.v1',
|
|
1264
|
+
status: 'blocked',
|
|
1265
|
+
comparison_result: 'not-verified',
|
|
1266
|
+
reason: errorMessage(error),
|
|
1267
|
+
visual_cap: 'blocked'
|
|
1268
|
+
};
|
|
1269
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Ueye compare');
|
|
1270
|
+
process.exitCode = 1;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
async function ueyeReport(args = []) {
|
|
1275
|
+
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return ueyeUsage();
|
|
1276
|
+
const flags = parseSimpleFlags(args, new Set(['reference', 'ref', 'actual', 'screenshot', 'capture-backend', 'compare-backend', 'design-quality', 'blocked-reason', 'next-action', 'json']));
|
|
1277
|
+
const reference = String(flags.reference || flags.ref || '');
|
|
1278
|
+
const actual = String(flags.actual || flags.screenshot || '');
|
|
1279
|
+
const referenceSources = [];
|
|
1280
|
+
const implementationSources = [];
|
|
1281
|
+
let comparisonResult = 'not-verified';
|
|
1282
|
+
let blockedReason = String(flags.blocked_reason || '');
|
|
1283
|
+
|
|
1284
|
+
try {
|
|
1285
|
+
if (reference) {
|
|
1286
|
+
const info = await imageFileInfo(reference);
|
|
1287
|
+
referenceSources.push(buildUeyeVisualProvenance({
|
|
1288
|
+
source_kind: 'reference_image',
|
|
1289
|
+
source_path: info.path,
|
|
1290
|
+
source_hash: info.sha256,
|
|
1291
|
+
reference_id: path.basename(info.path),
|
|
1292
|
+
local_only: true,
|
|
1293
|
+
operator_provided: true,
|
|
1294
|
+
comparison_result: 'reference-recorded',
|
|
1295
|
+
truth_status: 'partial'
|
|
1296
|
+
}));
|
|
1297
|
+
}
|
|
1298
|
+
if (actual) {
|
|
1299
|
+
const info = await imageFileInfo(actual);
|
|
1300
|
+
implementationSources.push(buildUeyeVisualProvenance({
|
|
1301
|
+
source_kind: 'implementation_screenshot',
|
|
1302
|
+
source_path: info.path,
|
|
1303
|
+
source_hash: info.sha256,
|
|
1304
|
+
screenshot_id: path.basename(info.path),
|
|
1305
|
+
local_only: true,
|
|
1306
|
+
operator_provided: false,
|
|
1307
|
+
comparison_result: 'implementation-recorded',
|
|
1308
|
+
truth_status: 'partial'
|
|
1309
|
+
}));
|
|
1310
|
+
}
|
|
1311
|
+
if (reference && actual && referenceSources[0]?.source_hash === implementationSources[0]?.source_hash) {
|
|
1312
|
+
comparisonResult = 'matched';
|
|
1313
|
+
} else if (reference && actual) {
|
|
1314
|
+
comparisonResult = 'different';
|
|
1315
|
+
}
|
|
1316
|
+
} catch (error) {
|
|
1317
|
+
blockedReason = errorMessage(error);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
const report = buildUeyeRunReport({
|
|
1321
|
+
reference_sources: referenceSources,
|
|
1322
|
+
implementation_sources: implementationSources,
|
|
1323
|
+
comparison_result: comparisonResult,
|
|
1324
|
+
design_quality: String(flags.design_quality || 'not-checked'),
|
|
1325
|
+
blocked_reason: blockedReason,
|
|
1326
|
+
next_action: String(flags.next_action || '')
|
|
1327
|
+
});
|
|
1328
|
+
const result = {
|
|
1329
|
+
...report,
|
|
1330
|
+
capture_backend: String(flags.capture_backend || 'not-recorded'),
|
|
1331
|
+
compare_backend: String(flags.compare_backend || 'local-file-hash')
|
|
1332
|
+
};
|
|
1333
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Ueye report');
|
|
1334
|
+
if (report.truth_status === 'blocked') process.exitCode = 1;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
async function media(args = []) {
|
|
1338
|
+
const subcommand = args[0] || 'help';
|
|
1339
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return mediaUsage();
|
|
1340
|
+
if (subcommand === 'proof') return mediaProof(args.slice(1));
|
|
1341
|
+
console.error(`unknown media command: ${subcommand}`);
|
|
1342
|
+
return mediaUsage();
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
function mediaUsage() {
|
|
1346
|
+
console.log(`yam media
|
|
1347
|
+
|
|
1348
|
+
Opt-in media generation proof helpers. Generated media can support visual direction, but it cannot verify implemented UI without implementation screenshot evidence.
|
|
1349
|
+
|
|
1350
|
+
Usage:
|
|
1351
|
+
yam media proof [--tool name] [--requested] [--attempted] [--output file] [--wait-loop] [--blocked-reason text] [--json]
|
|
1352
|
+
`);
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
async function mediaProof(args = []) {
|
|
1356
|
+
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return mediaUsage();
|
|
1357
|
+
const flags = parseSimpleFlags(args, new Set(['tool', 'requested', 'attempted', 'output', 'wait-loop', 'blocked-reason', 'next-action', 'json']));
|
|
1358
|
+
const output = String(flags.output || '');
|
|
1359
|
+
let outputHash = 'unknown';
|
|
1360
|
+
let outputPath = '';
|
|
1361
|
+
let blockedReason = String(flags.blocked_reason || '');
|
|
1362
|
+
if (output) {
|
|
1363
|
+
try {
|
|
1364
|
+
const info = await imageFileInfo(output);
|
|
1365
|
+
outputHash = info.sha256;
|
|
1366
|
+
outputPath = info.path;
|
|
1367
|
+
} catch (error) {
|
|
1368
|
+
blockedReason = blockedReason || errorMessage(error);
|
|
1369
|
+
outputPath = path.resolve(expandHome(output));
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
const proof = buildMediaGenerationProof({
|
|
1373
|
+
tool_name: String(flags.tool || ''),
|
|
1374
|
+
generation_requested: Boolean(flags.requested),
|
|
1375
|
+
generation_attempted: Boolean(flags.attempted || output),
|
|
1376
|
+
output_path: outputPath,
|
|
1377
|
+
output_hash: outputHash,
|
|
1378
|
+
wait_loop_checked: Boolean(flags.wait_loop),
|
|
1379
|
+
blocked_reason: blockedReason,
|
|
1380
|
+
next_action: String(flags.next_action || '')
|
|
1381
|
+
});
|
|
1382
|
+
printJsonOrHuman(proof, Boolean(flags.json), 'Media proof');
|
|
1383
|
+
if (proof.truth_status === 'blocked') process.exitCode = 1;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
function parseSimpleFlags(args = [], allowed = new Set<string>()) {
|
|
1387
|
+
const flags: AnyRecord = {};
|
|
1388
|
+
const positionals: string[] = [];
|
|
1389
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1390
|
+
const arg = String(args[index] || '');
|
|
1391
|
+
if (!arg.startsWith('--')) {
|
|
1392
|
+
positionals.push(arg);
|
|
1393
|
+
continue;
|
|
1394
|
+
}
|
|
1395
|
+
const [rawKey, inlineValue] = arg.includes('=') ? arg.split(/=(.*)/s, 2) : [arg, undefined];
|
|
1396
|
+
const key = rawKey.slice(2);
|
|
1397
|
+
if (allowed.size && !allowed.has(key)) continue;
|
|
1398
|
+
const normalizedKey = key.replace(/-/g, '_');
|
|
1399
|
+
if (['json', 'full-page', 'requested', 'attempted', 'available', 'wait-loop', 'cleanup-checked'].includes(key)) {
|
|
1400
|
+
flags[normalizedKey] = true;
|
|
1401
|
+
continue;
|
|
1402
|
+
}
|
|
1403
|
+
const value = inlineValue ?? args[index + 1] ?? '';
|
|
1404
|
+
if (inlineValue === undefined) index += 1;
|
|
1405
|
+
flags[normalizedKey] = value;
|
|
1406
|
+
}
|
|
1407
|
+
flags._ = positionals;
|
|
1408
|
+
return flags;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
function parseViewport(value = '1440x900') {
|
|
1412
|
+
const match = String(value || '').match(/^(\d{2,5})x(\d{2,5})$/i);
|
|
1413
|
+
if (!match) return { width: 1440, height: 900 };
|
|
1414
|
+
return { width: Number(match[1]), height: Number(match[2]) };
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
async function loadOptionalPlaywright() {
|
|
1418
|
+
let projectError = '';
|
|
1419
|
+
try {
|
|
1420
|
+
const projectRequire = createRequire(path.join(process.cwd(), 'package.json'));
|
|
1421
|
+
return projectRequire('playwright');
|
|
1422
|
+
} catch (error) {
|
|
1423
|
+
projectError = errorMessage(error);
|
|
1424
|
+
// Fall through to the yam package context for local development or bundled installs.
|
|
1425
|
+
}
|
|
1426
|
+
const dynamicImport = new Function('specifier', 'return import(specifier)') as (specifier: string) => Promise<any>;
|
|
1427
|
+
try {
|
|
1428
|
+
return await dynamicImport('playwright');
|
|
1429
|
+
} catch (error) {
|
|
1430
|
+
throw new Error(`Playwright not found from current project or yam package context. Current project: ${projectError}; package context: ${errorMessage(error)}`);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
async function imageFileInfo(file) {
|
|
1435
|
+
const absolute = path.resolve(expandHome(file));
|
|
1436
|
+
const buffer = await fsp.readFile(absolute);
|
|
1437
|
+
const stat = await fsp.stat(absolute);
|
|
1438
|
+
return {
|
|
1439
|
+
path: absolute,
|
|
1440
|
+
sha256: crypto.createHash('sha256').update(buffer).digest('hex'),
|
|
1441
|
+
bytes: stat.size,
|
|
1442
|
+
dimensions: imageDimensions(buffer)
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
function imageDimensions(buffer) {
|
|
1447
|
+
const png = pngDimensions(buffer);
|
|
1448
|
+
if (png) return png;
|
|
1449
|
+
const jpeg = jpegDimensions(buffer);
|
|
1450
|
+
if (jpeg) return jpeg;
|
|
1451
|
+
const gif = gifDimensions(buffer);
|
|
1452
|
+
if (gif) return gif;
|
|
1453
|
+
return 'unknown';
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
function pngDimensions(buffer) {
|
|
1457
|
+
if (buffer.length < 24) return '';
|
|
1458
|
+
if (buffer.readUInt32BE(0) !== 0x89504e47 || buffer.readUInt32BE(4) !== 0x0d0a1a0a) return '';
|
|
1459
|
+
return `${buffer.readUInt32BE(16)}x${buffer.readUInt32BE(20)}`;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
function gifDimensions(buffer) {
|
|
1463
|
+
if (buffer.length < 10) return '';
|
|
1464
|
+
const signature = buffer.toString('ascii', 0, 6);
|
|
1465
|
+
if (signature !== 'GIF87a' && signature !== 'GIF89a') return '';
|
|
1466
|
+
return `${buffer.readUInt16LE(6)}x${buffer.readUInt16LE(8)}`;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
function jpegDimensions(buffer) {
|
|
1470
|
+
if (buffer.length < 4 || buffer[0] !== 0xff || buffer[1] !== 0xd8) return '';
|
|
1471
|
+
let offset = 2;
|
|
1472
|
+
while (offset < buffer.length) {
|
|
1473
|
+
if (buffer[offset] !== 0xff) return '';
|
|
1474
|
+
const marker = buffer[offset + 1];
|
|
1475
|
+
const length = buffer.readUInt16BE(offset + 2);
|
|
1476
|
+
if (marker >= 0xc0 && marker <= 0xc3 && offset + 8 < buffer.length) {
|
|
1477
|
+
return `${buffer.readUInt16BE(offset + 7)}x${buffer.readUInt16BE(offset + 5)}`;
|
|
1478
|
+
}
|
|
1479
|
+
offset += 2 + length;
|
|
1480
|
+
}
|
|
1481
|
+
return '';
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
function printJsonOrHuman(result, json = false, label = 'yam') {
|
|
1485
|
+
if (json) {
|
|
1486
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
console.log(label);
|
|
1490
|
+
for (const [key, value] of Object.entries(result)) {
|
|
1491
|
+
if (key === 'schema') continue;
|
|
1492
|
+
if (typeof value === 'object' && value !== null) {
|
|
1493
|
+
console.log(`- ${key}: ${JSON.stringify(value)}`);
|
|
1494
|
+
} else {
|
|
1495
|
+
console.log(`- ${key}: ${value}`);
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1013
1500
|
function detectDbSafetyText(text = '') {
|
|
1014
1501
|
return detectTrustDbSafetyText(text);
|
|
1015
1502
|
}
|
|
@@ -1050,9 +1537,11 @@ async function proof(args = []) {
|
|
|
1050
1537
|
printProofList('Evidence', summary.evidence);
|
|
1051
1538
|
printProofList('Visual evidence', summary.visual);
|
|
1052
1539
|
printProofList('Runtime evidence', summary.runtime);
|
|
1540
|
+
printProofList('Runtime backend evidence', summary.runtimeBackendEvidence || []);
|
|
1053
1541
|
printProofList('Visual provenance', summary.visualProvenance || []);
|
|
1054
1542
|
printProofList('Mission patch envelope', summary.missionEnvelope || []);
|
|
1055
1543
|
printProofList('Rollback hint', summary.rollbackHint || []);
|
|
1544
|
+
printProofList('Media proof', summary.mediaProof || []);
|
|
1056
1545
|
printProofList('Changed surfaces', summary.changed);
|
|
1057
1546
|
printProofList('Skipped', summary.skipped);
|
|
1058
1547
|
printProofList('Blocked', summary.blocked);
|
|
@@ -1127,6 +1616,11 @@ function buildProofSummary(parsed, artifact) {
|
|
|
1127
1616
|
evidence: [...artifact.evidence, ...arrayFlag(parsed.flags.evidence)],
|
|
1128
1617
|
visual: [...artifact.visual, ...arrayFlag(parsed.flags.visual)],
|
|
1129
1618
|
runtime: [...artifact.runtime, ...arrayFlag(parsed.flags.runtime)],
|
|
1619
|
+
runtimeBackendEvidence: [
|
|
1620
|
+
...structuredEvidenceList(artifact.runtimeBackendEvidence),
|
|
1621
|
+
...structuredEvidenceList(parsed.flags.runtime_backend_evidence),
|
|
1622
|
+
...directRuntimeBackendEvidence(parsed.flags)
|
|
1623
|
+
],
|
|
1130
1624
|
visualProvenance: [
|
|
1131
1625
|
...structuredEvidenceList(artifact.visualProvenance),
|
|
1132
1626
|
...structuredEvidenceList(parsed.flags.visual_provenance)
|
|
@@ -1139,6 +1633,10 @@ function buildProofSummary(parsed, artifact) {
|
|
|
1139
1633
|
...structuredEvidenceList(artifact.rollbackHint),
|
|
1140
1634
|
...structuredEvidenceList(parsed.flags.rollback_hint)
|
|
1141
1635
|
],
|
|
1636
|
+
mediaProof: [
|
|
1637
|
+
...structuredEvidenceList(artifact.mediaProof),
|
|
1638
|
+
...structuredEvidenceList(parsed.flags.media_proof)
|
|
1639
|
+
],
|
|
1142
1640
|
cleanup: parsed.flags.cleanup || artifact.cleanup || '',
|
|
1143
1641
|
changed: [...artifact.changed, ...arrayFlag(parsed.flags.changed)],
|
|
1144
1642
|
skipped: [...artifact.skipped, ...arrayFlag(parsed.flags.skipped)],
|
|
@@ -1189,6 +1687,9 @@ function renderProofMarkdown(summary) {
|
|
|
1189
1687
|
'## Runtime evidence',
|
|
1190
1688
|
...renderProofMarkdownList(summary.runtime),
|
|
1191
1689
|
'',
|
|
1690
|
+
'## Runtime backend evidence',
|
|
1691
|
+
...renderProofMarkdownList(summary.runtimeBackendEvidence || []),
|
|
1692
|
+
'',
|
|
1192
1693
|
'## Visual provenance',
|
|
1193
1694
|
...renderProofMarkdownList(summary.visualProvenance || []),
|
|
1194
1695
|
'',
|
|
@@ -1198,6 +1699,9 @@ function renderProofMarkdown(summary) {
|
|
|
1198
1699
|
'## Rollback hint',
|
|
1199
1700
|
...renderProofMarkdownList(summary.rollbackHint || []),
|
|
1200
1701
|
'',
|
|
1702
|
+
'## Media proof',
|
|
1703
|
+
...renderProofMarkdownList(summary.mediaProof || []),
|
|
1704
|
+
'',
|
|
1201
1705
|
'## Changed surfaces',
|
|
1202
1706
|
...renderProofMarkdownList(summary.changed),
|
|
1203
1707
|
'',
|
|
@@ -1230,7 +1734,7 @@ function renderProofMarkdownList(values) {
|
|
|
1230
1734
|
function parseProofArgs(args = []) {
|
|
1231
1735
|
const flags: AnyRecord = {};
|
|
1232
1736
|
const positionals: string[] = [];
|
|
1233
|
-
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']);
|
|
1737
|
+
const aliases = new Set(['goal', 'route', 'truth', 'command', 'evidence', 'visual', 'runtime', 'runtime-backend', 'runtime-claim', 'runtime-evidence-id', 'runtime-command', 'cleanup-checked', 'runtime-note', 'runtime-backend-evidence', 'visual-provenance', 'mission-envelope', 'rollback-hint', 'media-proof', 'cleanup', 'changed', 'skipped', 'blocked', 'assumed', 'assumption', 'unverified', 'from', 'format', 'out', 'file', 'json', 'require-runtime', 'require-real-runtime', 'require-tmux', 'require-visual']);
|
|
1234
1738
|
for (let index = 0; index < args.length; index += 1) {
|
|
1235
1739
|
const arg = args[index];
|
|
1236
1740
|
if (arg.startsWith('--')) {
|
|
@@ -1238,13 +1742,13 @@ function parseProofArgs(args = []) {
|
|
|
1238
1742
|
const key = rawKey.slice(2);
|
|
1239
1743
|
if (!aliases.has(key)) continue;
|
|
1240
1744
|
const normalizedKey = key.replace(/-/g, '_');
|
|
1241
|
-
if (key === 'json' || key.startsWith('require-')) {
|
|
1745
|
+
if (key === 'json' || key.startsWith('require-') || key === 'cleanup-checked') {
|
|
1242
1746
|
flags[normalizedKey] = true;
|
|
1243
1747
|
continue;
|
|
1244
1748
|
}
|
|
1245
1749
|
const value = inlineValue ?? args[index + 1] ?? '';
|
|
1246
1750
|
if (inlineValue === undefined) index += 1;
|
|
1247
|
-
if (['command', 'evidence', 'visual', 'runtime', 'visual-provenance', 'mission-envelope', 'rollback-hint', 'changed', 'skipped', 'blocked', 'assumed', 'assumption', 'unverified'].includes(key)) {
|
|
1751
|
+
if (['command', 'evidence', 'visual', 'runtime', 'runtime-backend-evidence', 'visual-provenance', 'mission-envelope', 'rollback-hint', 'media-proof', 'changed', 'skipped', 'blocked', 'assumed', 'assumption', 'unverified'].includes(key)) {
|
|
1248
1752
|
flags[normalizedKey] = [...arrayFlag(flags[normalizedKey]), value];
|
|
1249
1753
|
} else {
|
|
1250
1754
|
flags[normalizedKey] = value;
|
|
@@ -1282,9 +1786,11 @@ function emptyProofArtifact() {
|
|
|
1282
1786
|
evidence: [],
|
|
1283
1787
|
visual: [],
|
|
1284
1788
|
runtime: [],
|
|
1789
|
+
runtimeBackendEvidence: [],
|
|
1285
1790
|
visualProvenance: [],
|
|
1286
1791
|
missionEnvelope: [],
|
|
1287
1792
|
rollbackHint: [],
|
|
1793
|
+
mediaProof: [],
|
|
1288
1794
|
cleanup: '',
|
|
1289
1795
|
changed: [],
|
|
1290
1796
|
skipped: [],
|
|
@@ -1307,9 +1813,11 @@ async function parseProofJson(file, empty) {
|
|
|
1307
1813
|
evidence: arrayFlag(data.evidence),
|
|
1308
1814
|
visual: arrayFlag(data.visual || data.visualEvidence),
|
|
1309
1815
|
runtime: arrayFlag(data.runtime || data.runtimeEvidence),
|
|
1816
|
+
runtimeBackendEvidence: structuredEvidenceList(data.runtimeBackendEvidence || data.runtime_backend_evidence),
|
|
1310
1817
|
visualProvenance: structuredEvidenceList(data.visualProvenance || data.visual_provenance),
|
|
1311
1818
|
missionEnvelope: structuredEvidenceList(data.missionEnvelope || data.mission_envelope),
|
|
1312
1819
|
rollbackHint: structuredEvidenceList(data.rollbackHint || data.rollback_hint),
|
|
1820
|
+
mediaProof: structuredEvidenceList(data.mediaProof || data.media_proof),
|
|
1313
1821
|
cleanup: String(data.cleanup || data.cleanupStatus || ''),
|
|
1314
1822
|
changed: arrayFlag(data.changed || data.files),
|
|
1315
1823
|
skipped: arrayFlag(data.skipped),
|
|
@@ -1335,9 +1843,11 @@ async function parseProofMarkdown(file, empty) {
|
|
|
1335
1843
|
evidence: readProofList(text, 'Evidence'),
|
|
1336
1844
|
visual: readProofList(text, 'Visual evidence'),
|
|
1337
1845
|
runtime: firstProofList(text, ['Runtime evidence', 'Processes', 'tmux']),
|
|
1846
|
+
runtimeBackendEvidence: readProofList(text, 'Runtime backend evidence'),
|
|
1338
1847
|
visualProvenance: readProofList(text, 'Visual provenance'),
|
|
1339
1848
|
missionEnvelope: readProofList(text, 'Mission patch envelope'),
|
|
1340
1849
|
rollbackHint: readProofList(text, 'Rollback hint'),
|
|
1850
|
+
mediaProof: readProofList(text, 'Media proof'),
|
|
1341
1851
|
cleanup: readProofField(text, 'Cleanup') || readProofField(text, 'Cleanup status'),
|
|
1342
1852
|
changed: firstProofList(text, ['Changed surfaces', 'Files']),
|
|
1343
1853
|
skipped: readProofList(text, 'Skipped'),
|
|
@@ -1388,6 +1898,18 @@ function structuredEvidenceList(value) {
|
|
|
1388
1898
|
return arrayFlag(value).map((item) => formatStructuredEvidence(item)).filter(Boolean);
|
|
1389
1899
|
}
|
|
1390
1900
|
|
|
1901
|
+
function directRuntimeBackendEvidence(flags) {
|
|
1902
|
+
if (!flags.runtime_backend && !flags.runtime_claim && !flags.runtime_evidence_id && !flags.runtime_command && !flags.runtime_note && !flags.cleanup_checked) return [];
|
|
1903
|
+
return [JSON.stringify(buildRuntimeBackendEvidence({
|
|
1904
|
+
backend: flags.runtime_backend,
|
|
1905
|
+
claim: flags.runtime_claim,
|
|
1906
|
+
evidence_id: flags.runtime_evidence_id,
|
|
1907
|
+
command: flags.runtime_command,
|
|
1908
|
+
cleanup_checked: Boolean(flags.cleanup_checked),
|
|
1909
|
+
note: flags.runtime_note
|
|
1910
|
+
}))];
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1391
1913
|
function formatStructuredEvidence(value) {
|
|
1392
1914
|
if (!value) return '';
|
|
1393
1915
|
if (typeof value === 'object') return JSON.stringify(value);
|
|
@@ -1405,6 +1927,12 @@ function formatStructuredEvidence(value) {
|
|
|
1405
1927
|
if (data.schema === 'yam.rollback-hint.v1' || data.touched_files || data.safe_revert_note) {
|
|
1406
1928
|
return JSON.stringify(buildRollbackHint(data));
|
|
1407
1929
|
}
|
|
1930
|
+
if (data.schema === 'yam.runtime-backend-evidence.v1' || data.backend || data.evidence_id) {
|
|
1931
|
+
return JSON.stringify(buildRuntimeBackendEvidence(data));
|
|
1932
|
+
}
|
|
1933
|
+
if (data.schema === 'yam.media-generation-proof.v1' || data.generation_requested || data.output_hash) {
|
|
1934
|
+
return JSON.stringify(buildMediaGenerationProof(data));
|
|
1935
|
+
}
|
|
1408
1936
|
return JSON.stringify(data);
|
|
1409
1937
|
} catch {
|
|
1410
1938
|
return text;
|
|
@@ -1413,7 +1941,7 @@ function formatStructuredEvidence(value) {
|
|
|
1413
1941
|
|
|
1414
1942
|
function hasProofEvidence(parsed) {
|
|
1415
1943
|
const flags = parsed.flags || {};
|
|
1416
|
-
return ['command', 'evidence', 'visual', 'runtime', 'visual_provenance', 'mission_envelope', 'rollback_hint', 'changed', 'skipped', 'blocked'].some((key) => arrayFlag(flags[key]).length > 0) || Boolean(flags.cleanup);
|
|
1944
|
+
return ['command', 'evidence', 'visual', 'runtime', 'runtime_backend_evidence', 'visual_provenance', 'mission_envelope', 'rollback_hint', 'media_proof', 'changed', 'skipped', 'blocked'].some((key) => arrayFlag(flags[key]).length > 0) || Boolean(flags.cleanup || flags.runtime_backend || flags.runtime_claim);
|
|
1417
1945
|
}
|
|
1418
1946
|
|
|
1419
1947
|
function printProofList(label, values) {
|
|
@@ -1665,7 +2193,8 @@ async function detectProject(targetDir = process.cwd(), { quiet = false } = {})
|
|
|
1665
2193
|
lint: null,
|
|
1666
2194
|
test: null,
|
|
1667
2195
|
build: null
|
|
1668
|
-
}
|
|
2196
|
+
},
|
|
2197
|
+
frameworkChecklist: null
|
|
1669
2198
|
};
|
|
1670
2199
|
if (!result.packageJson) {
|
|
1671
2200
|
if (!quiet) {
|
|
@@ -1683,6 +2212,7 @@ async function detectProject(targetDir = process.cwd(), { quiet = false } = {})
|
|
|
1683
2212
|
result.commands.lint = runCommand(result.packageManager, pickScript(scripts, ['lint']));
|
|
1684
2213
|
result.commands.test = runCommand(result.packageManager, pickScript(scripts, ['test', 'spec']));
|
|
1685
2214
|
result.commands.build = runCommand(result.packageManager, pickScript(scripts, ['build']));
|
|
2215
|
+
result.frameworkChecklist = await detectFrameworkChecklist(dir, pkg);
|
|
1686
2216
|
|
|
1687
2217
|
if (!quiet) printDetection(result);
|
|
1688
2218
|
return result;
|
|
@@ -1701,6 +2231,13 @@ function printDetection(result) {
|
|
|
1701
2231
|
console.log(`- $quick: ${result.commands.typecheck || result.commands.lint || result.commands.test || result.commands.build || 'Level 0 read/inspect; no command detected'}`);
|
|
1702
2232
|
console.log(`- $ueye: ${result.commands.typecheck || result.commands.build || 'Browser/screenshot check; no command detected'}`);
|
|
1703
2233
|
console.log(`- $deep: ${[result.commands.typecheck, result.commands.lint, result.commands.test, result.commands.build].filter(Boolean).join(' && ') || `No command detected; define in ${PROJECT_PACK}`}`);
|
|
2234
|
+
if (result.frameworkChecklist?.detected) {
|
|
2235
|
+
console.log('');
|
|
2236
|
+
console.log(`Framework checklist: ${result.frameworkChecklist.framework}`);
|
|
2237
|
+
for (const check of result.frameworkChecklist.checks) {
|
|
2238
|
+
console.log(`- ${check.id}: ${check.label} (${check.route})`);
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
1704
2241
|
}
|
|
1705
2242
|
|
|
1706
2243
|
function budget(routeArg = '') {
|
|
@@ -2105,6 +2642,8 @@ async function main() {
|
|
|
2105
2642
|
if (command === 'measure') return measure(process.argv[3], process.argv.slice(4));
|
|
2106
2643
|
if (command === 'tools') return tools(process.argv.slice(3));
|
|
2107
2644
|
if (command === 'proof') return proof(process.argv.slice(3));
|
|
2645
|
+
if (command === 'ueye') return ueye(process.argv.slice(3));
|
|
2646
|
+
if (command === 'media') return media(process.argv.slice(3));
|
|
2108
2647
|
if (command === 'release') return release(process.argv.slice(3));
|
|
2109
2648
|
if (command === 'safety') return safety(process.argv.slice(3));
|
|
2110
2649
|
if (command === 'memory') return memory(process.argv.slice(3));
|
|
@@ -2118,7 +2657,7 @@ async function main() {
|
|
|
2118
2657
|
}
|
|
2119
2658
|
if (command === 'list') return list();
|
|
2120
2659
|
if (command === 'verify') return verify();
|
|
2121
|
-
if (command === 'doctor') return doctor();
|
|
2660
|
+
if (command === 'doctor') return doctor(process.argv.slice(3));
|
|
2122
2661
|
if (command === 'examples') return examples();
|
|
2123
2662
|
if (command === 'path') return showPath();
|
|
2124
2663
|
if (command === 'init-project') return initProject(process.argv[3]);
|