yam-flow 0.1.3 → 0.1.4
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/CHANGELOG.md +36 -0
- package/COMMANDS.md +28 -1
- package/README.md +17 -2
- package/ROADMAP.md +5 -5
- package/dist/bin/yam.js +579 -31
- package/package.json +3 -2
- package/references/current-docs.md +3 -3
- package/references/doctor-scan.md +19 -0
- package/references/final-report.md +21 -0
- package/references/hook-lite.md +1 -1
- package/references/memory.md +1 -1
- package/references/mission.md +25 -0
- package/references/runtime-orchestration.md +25 -1
- package/references/tool-trust-layer.md +100 -6
- package/references/ueye-proof.md +34 -5
- package/references/ueye.md +24 -2
- package/skills/question/SKILL.md +1 -1
- package/skills/ueye/SKILL.md +10 -5
- package/src/bin/yam.ts +577 -31
- package/templates/mission-plan.md +12 -0
- package/templates/runtime-proof.md +41 -2
- package/templates/tuning-log.md +11 -1
- package/templates/ueye-comparison.md +44 -0
- package/templates/ueye-review.md +44 -0
- package/templates/yam.project.md +2 -2
package/dist/bin/yam.js
CHANGED
|
@@ -124,6 +124,9 @@ Usage:
|
|
|
124
124
|
yam ueye compare --reference ref.png --actual screenshot.png [--json]
|
|
125
125
|
yam ueye report [--reference ref.png] [--actual screenshot.png] [--json]
|
|
126
126
|
yam media proof [--requested] [--attempted] [--output file] [--json]
|
|
127
|
+
yam runtime evidence [--backend terminal|in-app-browser|playwright|tmux|zellij] [--claim observed|started|stopped|cleanup-verified] [--json]
|
|
128
|
+
yam mission queue [--agent-id id] [--scope text] [--changed file] [--verification-hint text] [--json]
|
|
129
|
+
yam benchmark report [--baseline n] [--current n] [--unit ms] [--target lower|higher] [--json]
|
|
127
130
|
yam release report [--json]
|
|
128
131
|
yam safety [text...]
|
|
129
132
|
yam memory <init|add|list|summary|resolve> [dir] [options]
|
|
@@ -420,32 +423,111 @@ async function buildDoctorReport() {
|
|
|
420
423
|
if (verifyIssues > 0)
|
|
421
424
|
issues.push(`verify reported ${verifyIssues} issue(s)`);
|
|
422
425
|
const globalHook = await readJsonOrDefault(path.join(os.homedir(), '.codex', 'hooks.json'), {});
|
|
426
|
+
const nextActionDetails = doctorNextActionDetails(issues);
|
|
423
427
|
return {
|
|
424
428
|
schema: 'yam.doctor.v1',
|
|
425
429
|
generatedAt: new Date().toISOString(),
|
|
426
430
|
ok: issues.length === 0,
|
|
427
431
|
issues,
|
|
428
|
-
nextActions:
|
|
432
|
+
nextActions: nextActionDetails.map((action) => action.next_action),
|
|
433
|
+
nextActionDetails,
|
|
429
434
|
yamLiteHook: hookConfigHasYamLite(globalHook) ? 'enabled' : 'disabled'
|
|
430
435
|
};
|
|
431
436
|
}
|
|
432
437
|
function doctorNextActions(issues = []) {
|
|
438
|
+
return doctorNextActionDetails(issues).map((action) => action.next_action);
|
|
439
|
+
}
|
|
440
|
+
function doctorNextActionDetails(issues = []) {
|
|
433
441
|
const actions = [];
|
|
434
442
|
for (const issue of issues) {
|
|
435
443
|
if (/missing source skill/i.test(issue))
|
|
436
|
-
actions.push('run from a complete yam-flow checkout or reinstall the published package');
|
|
444
|
+
actions.push(nextActionDetail('restore-skill-source', 'warning', issue, 'run from a complete yam-flow checkout or reinstall the published package', 'npm install -g yam-flow@latest && yam install'));
|
|
437
445
|
else if (/missing truth matrix/i.test(issue))
|
|
438
|
-
actions.push('restore references/truth-matrix.md before publishing');
|
|
446
|
+
actions.push(nextActionDetail('restore-truth-matrix', 'warning', issue, 'restore references/truth-matrix.md before publishing', 'git checkout -- references/truth-matrix.md'));
|
|
439
447
|
else if (/unexpected local hooks/i.test(issue))
|
|
440
|
-
actions.push('remove project hooks unless this repo is intentionally dogfooding them');
|
|
448
|
+
actions.push(nextActionDetail('remove-project-hook', 'warning', issue, 'remove project hooks unless this repo is intentionally dogfooding them', 'inspect .codex/hooks.json and remove only intentional stale hooks'));
|
|
441
449
|
else if (/unexpected .*automation/i.test(issue))
|
|
442
|
-
actions.push('remove stale yam/timeto automations before claiming clean install state');
|
|
450
|
+
actions.push(nextActionDetail('remove-stale-automation', 'warning', issue, 'remove stale yam/timeto automations before claiming clean install state', 'inspect ~/.codex/automations and remove stale entries manually'));
|
|
443
451
|
else if (/legacy|retired/i.test(issue))
|
|
444
|
-
actions.push('run `yam install` to replace old skill entries');
|
|
452
|
+
actions.push(nextActionDetail('replace-old-skills', 'warning', issue, 'run `yam install` to replace old skill entries', 'yam install'));
|
|
445
453
|
else if (/verify reported/i.test(issue))
|
|
446
|
-
actions.push('run `npm run verify` and fix the reported package boundary or metadata issue');
|
|
454
|
+
actions.push(nextActionDetail('run-verify', 'error', issue, 'run `npm run verify` and fix the reported package boundary or metadata issue', 'npm run verify'));
|
|
447
455
|
}
|
|
448
|
-
return
|
|
456
|
+
return uniqueNextActionDetails(actions);
|
|
457
|
+
}
|
|
458
|
+
function nextActionDetail(id, severity, reason, nextAction, command = '') {
|
|
459
|
+
const priority = priorityForSeverity(severity);
|
|
460
|
+
const ownerRoute = ownerRouteForAction(id, reason, command);
|
|
461
|
+
return {
|
|
462
|
+
id,
|
|
463
|
+
severity,
|
|
464
|
+
priority,
|
|
465
|
+
owner_route: ownerRoute,
|
|
466
|
+
reason,
|
|
467
|
+
next_action: nextAction,
|
|
468
|
+
command,
|
|
469
|
+
tool_intent: toolIntentForCommand(command),
|
|
470
|
+
fix_first: priority === 'P0' || priority === 'P1',
|
|
471
|
+
blocks_release: priority === 'P0',
|
|
472
|
+
truth_status: 'partial'
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
function priorityForSeverity(severity = '') {
|
|
476
|
+
if (/error|danger|block/i.test(String(severity)))
|
|
477
|
+
return 'P0';
|
|
478
|
+
if (/warn/i.test(String(severity)))
|
|
479
|
+
return 'P1';
|
|
480
|
+
if (/info/i.test(String(severity)))
|
|
481
|
+
return 'P2';
|
|
482
|
+
return 'P3';
|
|
483
|
+
}
|
|
484
|
+
function ownerRouteForAction(id = '', reason = '', command = '') {
|
|
485
|
+
const text = `${id} ${reason} ${command}`;
|
|
486
|
+
if (/visual|ueye|screenshot|browser/i.test(text))
|
|
487
|
+
return '$ueye';
|
|
488
|
+
if (/release|publish|package|registry|dist|version/i.test(text))
|
|
489
|
+
return '$deep';
|
|
490
|
+
if (/database|supabase|destructive|production|migration/i.test(text))
|
|
491
|
+
return '$deep';
|
|
492
|
+
if (/mission|subagent|team|lane/i.test(text))
|
|
493
|
+
return '$mission';
|
|
494
|
+
if (/research|docs|current/i.test(text))
|
|
495
|
+
return '$scout';
|
|
496
|
+
return '$quick';
|
|
497
|
+
}
|
|
498
|
+
function toolIntentForCommand(command = '') {
|
|
499
|
+
const text = String(command || '').toLowerCase();
|
|
500
|
+
if (!text)
|
|
501
|
+
return 'read_only';
|
|
502
|
+
if (/(publish|registry|release|npm pack|npm publish)/i.test(text))
|
|
503
|
+
return 'publish';
|
|
504
|
+
if (/(rm |delete|drop|truncate|reset|migrate|push|deploy|write|commit|add |install)/i.test(text))
|
|
505
|
+
return 'destructive';
|
|
506
|
+
if (/(dev|server|tmux|playwright|browser|screenshot|runtime|port|pid)/i.test(text))
|
|
507
|
+
return 'runtime';
|
|
508
|
+
if (/(ueye|visual|image|screen)/i.test(text))
|
|
509
|
+
return 'visual';
|
|
510
|
+
if (/(build|typecheck|lint|test|verify|doctor|status|pack)/i.test(text))
|
|
511
|
+
return 'read_only';
|
|
512
|
+
return 'write';
|
|
513
|
+
}
|
|
514
|
+
function normalizeToolIntent(value = '') {
|
|
515
|
+
const text = String(value || '').toLowerCase().replace(/[-\s]/g, '_');
|
|
516
|
+
if (['read_only', 'write', 'destructive', 'runtime', 'visual', 'publish'].includes(text))
|
|
517
|
+
return text;
|
|
518
|
+
return 'read_only';
|
|
519
|
+
}
|
|
520
|
+
function uniqueNextActionDetails(actions = []) {
|
|
521
|
+
const seen = new Set();
|
|
522
|
+
const unique = [];
|
|
523
|
+
for (const action of actions) {
|
|
524
|
+
const key = `${action.id}:${action.next_action}`;
|
|
525
|
+
if (seen.has(key))
|
|
526
|
+
continue;
|
|
527
|
+
seen.add(key);
|
|
528
|
+
unique.push(action);
|
|
529
|
+
}
|
|
530
|
+
return unique;
|
|
449
531
|
}
|
|
450
532
|
async function tools(args = []) {
|
|
451
533
|
const subcommand = args[0] || 'doctor';
|
|
@@ -524,6 +606,13 @@ async function buildToolsDoctorReport(targetDir = process.cwd()) {
|
|
|
524
606
|
...safety.hits,
|
|
525
607
|
...sqlScan.findings.map((finding) => ({ level: finding.level, reason: `${finding.file}: ${finding.reason}` }))
|
|
526
608
|
];
|
|
609
|
+
const nextActionDetails = toolsDoctorNextActionDetails({
|
|
610
|
+
pack,
|
|
611
|
+
rows,
|
|
612
|
+
riskNotes,
|
|
613
|
+
frameworkChecklist,
|
|
614
|
+
commands: detection.packageJson ? detection.commands : {}
|
|
615
|
+
});
|
|
527
616
|
return {
|
|
528
617
|
schemaVersion: 1,
|
|
529
618
|
generatedAt: new Date().toISOString(),
|
|
@@ -537,13 +626,8 @@ async function buildToolsDoctorReport(targetDir = process.cwd()) {
|
|
|
537
626
|
frameworkChecklist,
|
|
538
627
|
sqlScan,
|
|
539
628
|
riskNotes,
|
|
540
|
-
nextActions:
|
|
541
|
-
|
|
542
|
-
rows,
|
|
543
|
-
riskNotes,
|
|
544
|
-
frameworkChecklist,
|
|
545
|
-
commands: detection.packageJson ? detection.commands : {}
|
|
546
|
-
}),
|
|
629
|
+
nextActions: nextActionDetails.map((action) => action.next_action),
|
|
630
|
+
nextActionDetails,
|
|
547
631
|
routeRecommendations: {
|
|
548
632
|
dbSupabase: safety.hits.length || sqlScan.findings.length ? '$deep required before claiming safe' : '$deep when destructive or production mutation appears',
|
|
549
633
|
currentDocs: 'use current-docs proof only when version/freshness matters',
|
|
@@ -710,19 +794,22 @@ async function detectFrameworkChecklist(dir, pkg = null) {
|
|
|
710
794
|
function checklistRow(id, label, route) {
|
|
711
795
|
return { id, label, route };
|
|
712
796
|
}
|
|
713
|
-
function toolsDoctorNextActions(
|
|
797
|
+
function toolsDoctorNextActions(input) {
|
|
798
|
+
return toolsDoctorNextActionDetails(input).map((action) => action.next_action);
|
|
799
|
+
}
|
|
800
|
+
function toolsDoctorNextActionDetails({ pack, rows, riskNotes, frameworkChecklist, commands }) {
|
|
714
801
|
const actions = [];
|
|
715
802
|
if (!pack)
|
|
716
|
-
actions.push('create or refresh yam.project.md so route choices reuse local project context');
|
|
803
|
+
actions.push(nextActionDetail('project-pack', 'info', 'project pack missing', 'create or refresh yam.project.md so route choices reuse local project context', 'yam init-project .'));
|
|
717
804
|
if (rows.some((row) => row.name === 'Yam skills' && row.status !== 'ready'))
|
|
718
|
-
actions.push('run `yam install` and restart Codex before relying on installed skills');
|
|
805
|
+
actions.push(nextActionDetail('install-skills', 'warning', 'skill install state is incomplete', 'run `yam install` and restart Codex before relying on installed skills', 'yam install'));
|
|
719
806
|
if (riskNotes.some((note) => note.level === 'danger'))
|
|
720
|
-
actions.push('use $deep before claiming destructive database or production write safety');
|
|
807
|
+
actions.push(nextActionDetail('deep-safety', 'error', 'destructive database or production write signal detected', 'use $deep before claiming destructive database or production write safety', 'yam safety "<command or SQL>"'));
|
|
721
808
|
if (frameworkChecklist?.detected && !commands?.build && !commands?.typecheck)
|
|
722
|
-
actions.push('add or document a small build/typecheck command for implementation verification');
|
|
809
|
+
actions.push(nextActionDetail('verification-command', 'warning', 'build/typecheck command not detected', 'add or document a small build/typecheck command for implementation verification', 'yam pack .'));
|
|
723
810
|
if (frameworkChecklist?.uiLike)
|
|
724
|
-
actions.push('for Ueye verified claims, record reference, actual screenshot, and comparison result');
|
|
725
|
-
return
|
|
811
|
+
actions.push(nextActionDetail('ueye-evidence', 'info', 'UI-like project detected', 'for Ueye verified claims, record reference, actual screenshot, and comparison result', 'yam ueye report --reference ref.png --actual shot.png --json'));
|
|
812
|
+
return uniqueNextActionDetails(actions);
|
|
726
813
|
}
|
|
727
814
|
async function scanSqlFiles(rootDir, { maxFiles = 30, maxBytes = 200000, maxDepth = 5 } = {}) {
|
|
728
815
|
const root = path.resolve(rootDir || process.cwd());
|
|
@@ -1069,11 +1156,26 @@ async function release(args = []) {
|
|
|
1069
1156
|
if (check.note)
|
|
1070
1157
|
console.log(` ${check.note}`);
|
|
1071
1158
|
}
|
|
1159
|
+
if (report.provenance) {
|
|
1160
|
+
console.log(`- Git commit: ${report.provenance.git_commit || 'unknown'}`);
|
|
1161
|
+
console.log(`- Git dirty: ${report.provenance.git_dirty ? 'yes' : 'no'}`);
|
|
1162
|
+
console.log(`- Dist freshness: ${report.freshness.dist}`);
|
|
1163
|
+
}
|
|
1164
|
+
if (report.tarball) {
|
|
1165
|
+
console.log(`- Tarball: ${report.tarball.tarball_name || report.tarball.status}`);
|
|
1166
|
+
if (report.tarball.file_count !== undefined)
|
|
1167
|
+
console.log(`- Tarball files: ${report.tarball.file_count}`);
|
|
1168
|
+
}
|
|
1072
1169
|
if (report.failed.length) {
|
|
1073
1170
|
console.log('- Failed:');
|
|
1074
1171
|
for (const id of report.failed)
|
|
1075
1172
|
console.log(` - ${id}`);
|
|
1076
1173
|
}
|
|
1174
|
+
if (report.next_actions?.length) {
|
|
1175
|
+
console.log('- Next actions:');
|
|
1176
|
+
for (const action of report.next_actions)
|
|
1177
|
+
console.log(` - ${action.next_action}`);
|
|
1178
|
+
}
|
|
1077
1179
|
if (!report.ok)
|
|
1078
1180
|
process.exitCode = 1;
|
|
1079
1181
|
}
|
|
@@ -1088,6 +1190,10 @@ function runReleaseReport() {
|
|
|
1088
1190
|
['dist_freshness', ['npm', ['run', 'dist:freshness']]]
|
|
1089
1191
|
].map(([id, command]) => runReleaseCheck(id, command));
|
|
1090
1192
|
const failed = checks.filter((check) => check.status !== 'passed').map((check) => check.id);
|
|
1193
|
+
const provenance = releaseProvenance(checks);
|
|
1194
|
+
const tarball = releaseTarballProvenance();
|
|
1195
|
+
const freshness = releaseFreshness(checks, provenance, tarball);
|
|
1196
|
+
const nextActions = releaseNextActions(checks, provenance, tarball);
|
|
1091
1197
|
return {
|
|
1092
1198
|
schema: 'yam.release-report.v1',
|
|
1093
1199
|
generated_at: startedAt,
|
|
@@ -1096,9 +1202,114 @@ function runReleaseReport() {
|
|
|
1096
1202
|
ok: failed.length === 0,
|
|
1097
1203
|
truth_status: failed.length === 0 ? 'verified' : 'blocked',
|
|
1098
1204
|
checks,
|
|
1099
|
-
failed
|
|
1205
|
+
failed,
|
|
1206
|
+
provenance,
|
|
1207
|
+
tarball,
|
|
1208
|
+
freshness,
|
|
1209
|
+
next_actions: nextActions
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
function releaseProvenance(checks = []) {
|
|
1213
|
+
const gitCommit = runReadOnlyCommand('git', ['rev-parse', '--short', 'HEAD']);
|
|
1214
|
+
const gitStatus = runReadOnlyCommand('git', ['status', '--short']);
|
|
1215
|
+
const distChanged = runReadOnlyCommand('git', ['diff', '--name-only', '--', 'dist']);
|
|
1216
|
+
const packageFiles = checks.find((check) => check.id === 'package_boundary')?.status === 'passed' ? 'checked' : 'not-verified';
|
|
1217
|
+
return {
|
|
1218
|
+
schema: 'yam.release-provenance.v1',
|
|
1219
|
+
package_name: PACKAGE_JSON.name,
|
|
1220
|
+
version: VERSION,
|
|
1221
|
+
git_commit: gitCommit.trim(),
|
|
1222
|
+
git_dirty: Boolean(gitStatus.trim()),
|
|
1223
|
+
git_status_lines: gitStatus.trim().split(/\r?\n/).filter(Boolean).slice(0, 20),
|
|
1224
|
+
dist_changed_files: distChanged.trim().split(/\r?\n/).filter(Boolean).slice(0, 20),
|
|
1225
|
+
package_files: packageFiles,
|
|
1226
|
+
generated_at: new Date().toISOString(),
|
|
1227
|
+
truth_status: gitCommit.trim() && packageFiles === 'checked' ? 'partial' : 'assumed'
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
function releaseTarballProvenance() {
|
|
1231
|
+
const result = spawnSync('npm', ['pack', '--json', '--dry-run', '--ignore-scripts'], {
|
|
1232
|
+
cwd: ROOT,
|
|
1233
|
+
encoding: 'utf8',
|
|
1234
|
+
env: { ...process.env, npm_config_cache: path.join(os.tmpdir(), 'yam-npm-cache') },
|
|
1235
|
+
timeout: 120000
|
|
1236
|
+
});
|
|
1237
|
+
if (result.status !== 0) {
|
|
1238
|
+
return {
|
|
1239
|
+
schema: 'yam.release-tarball-provenance.v1',
|
|
1240
|
+
status: 'blocked',
|
|
1241
|
+
reason: summarizeCheckOutput([result.stdout, result.stderr, result.error?.message].filter(Boolean).join('\n')),
|
|
1242
|
+
truth_status: 'blocked'
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
try {
|
|
1246
|
+
const data = JSON.parse(String(result.stdout || '[]'))[0] || {};
|
|
1247
|
+
return {
|
|
1248
|
+
schema: 'yam.release-tarball-provenance.v1',
|
|
1249
|
+
status: 'checked',
|
|
1250
|
+
tarball_name: String(data.filename || ''),
|
|
1251
|
+
package_size: Number(data.size || data.packageSize || 0),
|
|
1252
|
+
unpacked_size: Number(data.unpackedSize || 0),
|
|
1253
|
+
file_count: Array.isArray(data.files) ? data.files.length : Number(data.entryCount || 0),
|
|
1254
|
+
integrity: String(data.integrity || ''),
|
|
1255
|
+
shasum: String(data.shasum || ''),
|
|
1256
|
+
truth_status: data.filename ? 'verified' : 'partial'
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
catch (error) {
|
|
1260
|
+
return {
|
|
1261
|
+
schema: 'yam.release-tarball-provenance.v1',
|
|
1262
|
+
status: 'blocked',
|
|
1263
|
+
reason: `could not parse npm pack output: ${errorMessage(error)}`,
|
|
1264
|
+
truth_status: 'blocked'
|
|
1265
|
+
};
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
function releaseFreshness(checks = [], provenance = {}, tarball = {}) {
|
|
1269
|
+
const checkStatus = (id) => checks.find((check) => check.id === id)?.status || 'not-run';
|
|
1270
|
+
const dist = checkStatus('dist_freshness') === 'passed' && !provenance.dist_changed_files?.length ? 'fresh' : 'needs-check';
|
|
1271
|
+
const registry = checkStatus('registry_status') === 'passed' ? 'publishable-version' : 'not-verified';
|
|
1272
|
+
const boundary = checkStatus('package_boundary') === 'passed' ? 'checked' : 'not-verified';
|
|
1273
|
+
const smoke = checkStatus('cli_smoke') === 'passed' ? 'checked' : 'not-verified';
|
|
1274
|
+
const tarballStatus = tarball.status === 'checked' ? 'checked' : 'not-verified';
|
|
1275
|
+
return {
|
|
1276
|
+
schema: 'yam.release-freshness.v1',
|
|
1277
|
+
dist,
|
|
1278
|
+
registry,
|
|
1279
|
+
package_boundary: boundary,
|
|
1280
|
+
cli_smoke: smoke,
|
|
1281
|
+
tarball: tarballStatus,
|
|
1282
|
+
truth_status: [dist, registry, boundary, smoke, tarballStatus].every((value) => !/not-verified|needs-check/.test(value)) ? 'verified' : 'partial'
|
|
1100
1283
|
};
|
|
1101
1284
|
}
|
|
1285
|
+
function releaseNextActions(checks = [], provenance = {}, tarball = {}) {
|
|
1286
|
+
const actions = [];
|
|
1287
|
+
for (const check of checks) {
|
|
1288
|
+
if (check.status === 'passed')
|
|
1289
|
+
continue;
|
|
1290
|
+
actions.push(nextActionDetail(`release-${check.id}`, check.status === 'blocked' ? 'error' : 'warning', check.note || `${check.id} did not pass`, `fix ${check.id} before publishing`, check.command));
|
|
1291
|
+
}
|
|
1292
|
+
if (provenance.git_dirty) {
|
|
1293
|
+
actions.push(nextActionDetail('release-git-dirty', 'warning', 'working tree has uncommitted changes', 'commit or intentionally keep local-only changes before tagging a release', 'git status --short'));
|
|
1294
|
+
}
|
|
1295
|
+
if (provenance.dist_changed_files?.length) {
|
|
1296
|
+
actions.push(nextActionDetail('release-dist-drift', 'warning', 'dist has unstaged or uncommitted differences', 'run build and commit generated dist only if this package publishes dist', 'npm run build'));
|
|
1297
|
+
}
|
|
1298
|
+
if (tarball.status !== 'checked') {
|
|
1299
|
+
actions.push(nextActionDetail('release-tarball-provenance', 'error', tarball.reason || 'tarball provenance was not produced', 'run npm pack dry-run before publishing', 'npm pack --dry-run'));
|
|
1300
|
+
}
|
|
1301
|
+
return uniqueNextActionDetails(actions);
|
|
1302
|
+
}
|
|
1303
|
+
function runReadOnlyCommand(command, args = []) {
|
|
1304
|
+
const result = spawnSync(command, args, {
|
|
1305
|
+
cwd: ROOT,
|
|
1306
|
+
encoding: 'utf8',
|
|
1307
|
+
timeout: 30000
|
|
1308
|
+
});
|
|
1309
|
+
if (result.status !== 0)
|
|
1310
|
+
return '';
|
|
1311
|
+
return String(result.stdout || '');
|
|
1312
|
+
}
|
|
1102
1313
|
function runReleaseCheck(id, commandSpec) {
|
|
1103
1314
|
const [command, args] = commandSpec;
|
|
1104
1315
|
const start = Date.now();
|
|
@@ -1155,12 +1366,12 @@ Opt-in visual evidence helpers. Ueye stays one skill: fast by default, capture/c
|
|
|
1155
1366
|
Usage:
|
|
1156
1367
|
yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
|
|
1157
1368
|
yam ueye compare --reference ref.png --actual screenshot.png [--json]
|
|
1158
|
-
yam ueye report [--reference ref.png] [--actual screenshot.png] [--design-quality pass|needs-polish|fails|not-checked] [--json]
|
|
1369
|
+
yam ueye report [--reference ref.png] [--actual screenshot.png] [--review-session-id id] [--similar text] [--different text] [--missing text] [--resolved text] [--new-finding text] [--still-open text] [--regression text] [--viewport 1440x900] [--state default] [--design-quality pass|needs-polish|fails|not-checked] [--json]
|
|
1159
1370
|
|
|
1160
1371
|
Notes:
|
|
1161
1372
|
capture uses a locally available Playwright install when present. It does not download browsers or install dependencies.
|
|
1162
1373
|
compare uses local files only and reports sha256, dimensions, comparison_result, and proof-ready visual provenance.
|
|
1163
|
-
report produces a proof-ready Ueye visual run report without requiring a new capture.
|
|
1374
|
+
report produces a proof-ready Ueye visual run report and continuity/comparison record without requiring a new capture.
|
|
1164
1375
|
`);
|
|
1165
1376
|
}
|
|
1166
1377
|
async function ueyeCapture(args = []) {
|
|
@@ -1285,13 +1496,15 @@ async function ueyeCompare(args = []) {
|
|
|
1285
1496
|
async function ueyeReport(args = []) {
|
|
1286
1497
|
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h')
|
|
1287
1498
|
return ueyeUsage();
|
|
1288
|
-
const flags = parseSimpleFlags(args, new Set(['reference', 'ref', 'actual', 'screenshot', 'capture-backend', 'compare-backend', 'design-quality', 'blocked-reason', 'next-action', 'json']));
|
|
1499
|
+
const flags = parseSimpleFlags(args, new Set(['reference', 'ref', 'actual', 'screenshot', 'capture-backend', 'compare-backend', 'design-quality', 'blocked-reason', 'next-action', 'next-visual-action', 'review-session-id', 'reference-id', 'screenshot-id', 'previous-screenshot-id', 'current-screenshot-id', 'previous-report', 'comparison-notes', 'similar', 'different', 'missing', 'resolved', 'new-finding', 'still-open', 'regression', 'viewport', 'state', 'json']));
|
|
1289
1500
|
const reference = String(flags.reference || flags.ref || '');
|
|
1290
1501
|
const actual = String(flags.actual || flags.screenshot || '');
|
|
1291
1502
|
const referenceSources = [];
|
|
1292
1503
|
const implementationSources = [];
|
|
1293
1504
|
let comparisonResult = 'not-verified';
|
|
1294
1505
|
let blockedReason = String(flags.blocked_reason || '');
|
|
1506
|
+
const reviewSessionId = String(flags.review_session_id || `ueye-${timestampId()}`);
|
|
1507
|
+
const previousReport = await readPreviousUeyeReport(flags.previous_report);
|
|
1295
1508
|
try {
|
|
1296
1509
|
if (reference) {
|
|
1297
1510
|
const info = await imageFileInfo(reference);
|
|
@@ -1299,7 +1512,7 @@ async function ueyeReport(args = []) {
|
|
|
1299
1512
|
source_kind: 'reference_image',
|
|
1300
1513
|
source_path: info.path,
|
|
1301
1514
|
source_hash: info.sha256,
|
|
1302
|
-
reference_id: path.basename(info.path),
|
|
1515
|
+
reference_id: String(flags.reference_id || path.basename(info.path)),
|
|
1303
1516
|
local_only: true,
|
|
1304
1517
|
operator_provided: true,
|
|
1305
1518
|
comparison_result: 'reference-recorded',
|
|
@@ -1312,7 +1525,8 @@ async function ueyeReport(args = []) {
|
|
|
1312
1525
|
source_kind: 'implementation_screenshot',
|
|
1313
1526
|
source_path: info.path,
|
|
1314
1527
|
source_hash: info.sha256,
|
|
1315
|
-
|
|
1528
|
+
reference_id: String(flags.reference_id || referenceSources[0]?.reference_id || ''),
|
|
1529
|
+
screenshot_id: String(flags.current_screenshot_id || flags.screenshot_id || path.basename(info.path)),
|
|
1316
1530
|
local_only: true,
|
|
1317
1531
|
operator_provided: false,
|
|
1318
1532
|
comparison_result: 'implementation-recorded',
|
|
@@ -1335,17 +1549,108 @@ async function ueyeReport(args = []) {
|
|
|
1335
1549
|
comparison_result: comparisonResult,
|
|
1336
1550
|
design_quality: String(flags.design_quality || 'not-checked'),
|
|
1337
1551
|
blocked_reason: blockedReason,
|
|
1338
|
-
next_action: String(flags.next_action || '')
|
|
1552
|
+
next_action: String(flags.next_visual_action || flags.next_action || '')
|
|
1553
|
+
});
|
|
1554
|
+
const comparisonReport = buildUeyeComparisonReport({
|
|
1555
|
+
reviewSessionId,
|
|
1556
|
+
previousReport,
|
|
1557
|
+
referenceSources,
|
|
1558
|
+
implementationSources,
|
|
1559
|
+
comparisonResult,
|
|
1560
|
+
designQuality: String(flags.design_quality || 'not-checked'),
|
|
1561
|
+
similar: arrayFlag(flags.similar),
|
|
1562
|
+
different: arrayFlag(flags.different),
|
|
1563
|
+
missing: arrayFlag(flags.missing),
|
|
1564
|
+
notes: arrayFlag(flags.comparison_notes),
|
|
1565
|
+
resolved: arrayFlag(flags.resolved),
|
|
1566
|
+
newFindings: arrayFlag(flags.new_finding),
|
|
1567
|
+
stillOpen: arrayFlag(flags.still_open),
|
|
1568
|
+
regressions: arrayFlag(flags.regression),
|
|
1569
|
+
previousScreenshotId: String(flags.previous_screenshot_id || previousReport?.screenshot_id || ''),
|
|
1570
|
+
currentScreenshotId: String(flags.current_screenshot_id || flags.screenshot_id || implementationSources[0]?.screenshot_id || ''),
|
|
1571
|
+
viewport: String(flags.viewport || ''),
|
|
1572
|
+
state: String(flags.state || 'unknown'),
|
|
1573
|
+
nextAction: report.next_action,
|
|
1574
|
+
truthStatus: report.truth_status
|
|
1339
1575
|
});
|
|
1340
1576
|
const result = {
|
|
1341
1577
|
...report,
|
|
1578
|
+
review_session_id: reviewSessionId,
|
|
1579
|
+
tool_intent: 'visual',
|
|
1342
1580
|
capture_backend: String(flags.capture_backend || 'not-recorded'),
|
|
1343
|
-
compare_backend: String(flags.compare_backend || 'local-file-hash')
|
|
1581
|
+
compare_backend: String(flags.compare_backend || 'local-file-hash'),
|
|
1582
|
+
continuity: comparisonReport.continuity,
|
|
1583
|
+
comparison_report: comparisonReport
|
|
1344
1584
|
};
|
|
1345
1585
|
printJsonOrHuman(result, Boolean(flags.json), 'Ueye report');
|
|
1346
1586
|
if (report.truth_status === 'blocked')
|
|
1347
1587
|
process.exitCode = 1;
|
|
1348
1588
|
}
|
|
1589
|
+
async function readPreviousUeyeReport(file = '') {
|
|
1590
|
+
const target = String(file || '');
|
|
1591
|
+
if (!target)
|
|
1592
|
+
return null;
|
|
1593
|
+
try {
|
|
1594
|
+
const data = await readJson(path.resolve(expandHome(target)));
|
|
1595
|
+
return {
|
|
1596
|
+
path: path.resolve(expandHome(target)),
|
|
1597
|
+
review_session_id: data.review_session_id || data.comparison_report?.review_session_id || '',
|
|
1598
|
+
screenshot_id: data.comparison_report?.current_screenshot_id || data.current_screenshot_id || data.screenshot_id || '',
|
|
1599
|
+
truth_status: data.truth_status || data.comparison_report?.truth_status || 'partial',
|
|
1600
|
+
comparison_result: data.comparison_result || data.comparison_report?.comparison_result || 'not-verified'
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
catch (error) {
|
|
1604
|
+
return {
|
|
1605
|
+
path: path.resolve(expandHome(target)),
|
|
1606
|
+
blocked_reason: errorMessage(error),
|
|
1607
|
+
truth_status: 'blocked',
|
|
1608
|
+
comparison_result: 'not-verified'
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
function buildUeyeComparisonReport({ reviewSessionId, previousReport, referenceSources, implementationSources, comparisonResult, designQuality, similar, different, missing, notes, resolved, newFindings, stillOpen, regressions, previousScreenshotId, currentScreenshotId, viewport, state, nextAction, truthStatus }) {
|
|
1613
|
+
const hasReference = referenceSources.length > 0;
|
|
1614
|
+
const hasImplementation = implementationSources.length > 0;
|
|
1615
|
+
const continuityTruth = previousReport?.blocked_reason ? 'blocked' : previousReport ? 'partial' : 'assumed';
|
|
1616
|
+
return {
|
|
1617
|
+
schema: 'yam.ueye-comparison-report.v1',
|
|
1618
|
+
review_session_id: reviewSessionId,
|
|
1619
|
+
previous_report: previousReport,
|
|
1620
|
+
continuity: {
|
|
1621
|
+
schema: 'yam.ueye-review-continuity.v1',
|
|
1622
|
+
current_review_session_id: reviewSessionId,
|
|
1623
|
+
previous_review_session_id: previousReport?.review_session_id || '',
|
|
1624
|
+
previous_report_path: previousReport?.path || '',
|
|
1625
|
+
previous_screenshot_id: previousScreenshotId || previousReport?.screenshot_id || '',
|
|
1626
|
+
current_screenshot_id: currentScreenshotId || '',
|
|
1627
|
+
carries_forward: Boolean(previousReport && !previousReport.blocked_reason),
|
|
1628
|
+
truth_status: continuityTruth
|
|
1629
|
+
},
|
|
1630
|
+
evidence_summary: {
|
|
1631
|
+
has_reference: hasReference,
|
|
1632
|
+
has_implementation_screenshot: hasImplementation,
|
|
1633
|
+
reference_count: referenceSources.length,
|
|
1634
|
+
implementation_count: implementationSources.length
|
|
1635
|
+
},
|
|
1636
|
+
comparison_result: comparisonResult,
|
|
1637
|
+
design_quality: designQuality,
|
|
1638
|
+
similar,
|
|
1639
|
+
different,
|
|
1640
|
+
missing,
|
|
1641
|
+
resolved,
|
|
1642
|
+
new_findings: newFindings,
|
|
1643
|
+
still_open: stillOpen,
|
|
1644
|
+
regressions,
|
|
1645
|
+
previous_screenshot_id: previousScreenshotId || previousReport?.screenshot_id || '',
|
|
1646
|
+
current_screenshot_id: currentScreenshotId || '',
|
|
1647
|
+
viewport,
|
|
1648
|
+
state,
|
|
1649
|
+
notes,
|
|
1650
|
+
next_action: nextAction || (truthStatus === 'verified' ? 'no action required' : 'capture or provide implementation screenshot, then compare again'),
|
|
1651
|
+
truth_status: truthStatus
|
|
1652
|
+
};
|
|
1653
|
+
}
|
|
1349
1654
|
async function media(args = []) {
|
|
1350
1655
|
const subcommand = args[0] || 'help';
|
|
1351
1656
|
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h')
|
|
@@ -1397,6 +1702,238 @@ async function mediaProof(args = []) {
|
|
|
1397
1702
|
if (proof.truth_status === 'blocked')
|
|
1398
1703
|
process.exitCode = 1;
|
|
1399
1704
|
}
|
|
1705
|
+
async function runtime(args = []) {
|
|
1706
|
+
const subcommand = args[0] || 'help';
|
|
1707
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h')
|
|
1708
|
+
return runtimeUsage();
|
|
1709
|
+
if (subcommand === 'evidence')
|
|
1710
|
+
return runtimeEvidence(args.slice(1));
|
|
1711
|
+
console.error(`unknown runtime command: ${subcommand}`);
|
|
1712
|
+
return runtimeUsage();
|
|
1713
|
+
}
|
|
1714
|
+
function runtimeUsage() {
|
|
1715
|
+
console.log(`yam runtime
|
|
1716
|
+
|
|
1717
|
+
Usage:
|
|
1718
|
+
yam runtime evidence [--backend terminal|in-app-browser|playwright|tmux|zellij] [--claim observed|started|stopped|cleanup-verified] [--evidence-id id] [--command text] [--pid n] [--port n] [--url URL] [--exit-code n] [--screenshot-id id] [--cleanup-checked] [--note text] [--json]
|
|
1719
|
+
|
|
1720
|
+
Notes:
|
|
1721
|
+
Records a small runtime evidence shape. It does not start, stop, or inspect processes by itself.
|
|
1722
|
+
`);
|
|
1723
|
+
}
|
|
1724
|
+
async function runtimeEvidence(args = []) {
|
|
1725
|
+
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h')
|
|
1726
|
+
return runtimeUsage();
|
|
1727
|
+
const flags = parseSimpleFlags(args, new Set(['backend', 'claim', 'evidence-id', 'command', 'pid', 'port', 'url', 'exit-code', 'screenshot-id', 'started-at', 'stopped-at', 'cleanup-checked', 'note', 'truth', 'intent', 'json']));
|
|
1728
|
+
const evidence = buildRuntimeBackendEvidence({
|
|
1729
|
+
backend: flags.backend,
|
|
1730
|
+
claim: flags.claim,
|
|
1731
|
+
evidence_id: String(flags.evidence_id || `runtime-${timestampId()}`),
|
|
1732
|
+
command: String(flags.command || ''),
|
|
1733
|
+
cleanup_checked: Boolean(flags.cleanup_checked),
|
|
1734
|
+
note: String(flags.note || ''),
|
|
1735
|
+
truth_status: isTruthStatus(flags.truth) ? flags.truth : undefined
|
|
1736
|
+
});
|
|
1737
|
+
const runtimeDetails = runtimeEvidenceDetails(flags);
|
|
1738
|
+
if (!isTruthStatus(flags.truth) && evidence.truth_status === 'verified' && !runtimeDetailsHasVerification(runtimeDetails)) {
|
|
1739
|
+
evidence.truth_status = 'partial';
|
|
1740
|
+
}
|
|
1741
|
+
const result = {
|
|
1742
|
+
...evidence,
|
|
1743
|
+
runtime_details: runtimeDetails,
|
|
1744
|
+
tool_intent: normalizeToolIntent(flags.intent || 'runtime'),
|
|
1745
|
+
next_action: runtimeEvidenceNextAction(evidence)
|
|
1746
|
+
};
|
|
1747
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Runtime evidence');
|
|
1748
|
+
if (result.truth_status === 'blocked' || result.truth_status === 'real_required_missing')
|
|
1749
|
+
process.exitCode = 1;
|
|
1750
|
+
}
|
|
1751
|
+
function runtimeEvidenceDetails(flags = {}) {
|
|
1752
|
+
return {
|
|
1753
|
+
schema: 'yam.runtime-details.v1',
|
|
1754
|
+
pid: numberOrNull(flags.pid),
|
|
1755
|
+
port: numberOrNull(flags.port),
|
|
1756
|
+
url: String(flags.url || ''),
|
|
1757
|
+
exit_code: numberOrNull(flags.exit_code),
|
|
1758
|
+
screenshot_id: String(flags.screenshot_id || ''),
|
|
1759
|
+
started_at: String(flags.started_at || ''),
|
|
1760
|
+
stopped_at: String(flags.stopped_at || '')
|
|
1761
|
+
};
|
|
1762
|
+
}
|
|
1763
|
+
function runtimeDetailsHasVerification(details = {}) {
|
|
1764
|
+
return Boolean(details.url || details.screenshot_id || details.pid !== null || details.port !== null || details.exit_code !== null || details.started_at || details.stopped_at);
|
|
1765
|
+
}
|
|
1766
|
+
function runtimeEvidenceNextAction(evidence) {
|
|
1767
|
+
if (evidence.truth_status === 'proven' || evidence.truth_status === 'verified')
|
|
1768
|
+
return 'attach this evidence to `yam proof` when making a runtime claim';
|
|
1769
|
+
if (evidence.claim === 'cleanup_verified' && !evidence.cleanup_checked)
|
|
1770
|
+
return 'rerun with --cleanup-checked only after process exit or intentional persistence is confirmed';
|
|
1771
|
+
if (evidence.backend === 'none' || evidence.backend === 'unknown')
|
|
1772
|
+
return 'record the actual runtime backend before claiming runtime verification';
|
|
1773
|
+
return 'add command output, screenshot id, pid/session id, or cleanup proof before upgrading the claim';
|
|
1774
|
+
}
|
|
1775
|
+
async function mission(args = []) {
|
|
1776
|
+
const subcommand = args[0] || 'help';
|
|
1777
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h')
|
|
1778
|
+
return missionUsage();
|
|
1779
|
+
if (subcommand === 'queue' || subcommand === 'patch')
|
|
1780
|
+
return missionQueue(args.slice(1));
|
|
1781
|
+
console.error(`unknown mission command: ${subcommand}`);
|
|
1782
|
+
return missionUsage();
|
|
1783
|
+
}
|
|
1784
|
+
function missionUsage() {
|
|
1785
|
+
console.log(`yam mission
|
|
1786
|
+
|
|
1787
|
+
Usage:
|
|
1788
|
+
yam mission queue [--lane-id id] [--agent-id id] [--status pending|applied|verified|blocked|reverted] [--scope text] [--changed file] [--depends-on lane] [--generated file] [--verification-hint text] [--rollback-hint text] [--before-check text] [--out file] [--truth status] [--json]
|
|
1789
|
+
|
|
1790
|
+
Notes:
|
|
1791
|
+
Produces a patch queue item for mission handoff/review. It does not run workers; persistence is opt-in with --out.
|
|
1792
|
+
`);
|
|
1793
|
+
}
|
|
1794
|
+
async function missionQueue(args = []) {
|
|
1795
|
+
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h')
|
|
1796
|
+
return missionUsage();
|
|
1797
|
+
const flags = parseSimpleFlags(args, new Set(['lane-id', 'agent-id', 'status', 'scope', 'changed', 'generated', 'depends-on', 'verification-hint', 'rollback-hint', 'before-check', 'safe-revert-note', 'truth', 'intent', 'out', 'json']));
|
|
1798
|
+
const changedFiles = arrayFlag(flags.changed);
|
|
1799
|
+
const generatedFiles = arrayFlag(flags.generated);
|
|
1800
|
+
const laneId = String(flags.lane_id || flags.agent_id || `lane-${timestampId()}`);
|
|
1801
|
+
const status = normalizeMissionQueueStatus(flags.status || 'pending');
|
|
1802
|
+
const rollback = buildRollbackHint({
|
|
1803
|
+
touched_files: changedFiles,
|
|
1804
|
+
generated_files: generatedFiles,
|
|
1805
|
+
before_check: String(flags.before_check || ''),
|
|
1806
|
+
safe_revert_note: String(flags.safe_revert_note || flags.rollback_hint || '')
|
|
1807
|
+
});
|
|
1808
|
+
const envelope = buildMissionPatchEnvelope({
|
|
1809
|
+
agent_id: String(flags.agent_id || `agent-${timestampId()}`),
|
|
1810
|
+
assigned_scope: String(flags.scope || ''),
|
|
1811
|
+
changed_files: changedFiles,
|
|
1812
|
+
verification_hint: String(flags.verification_hint || ''),
|
|
1813
|
+
rollback_hint: rollback,
|
|
1814
|
+
truth_status: isTruthStatus(flags.truth) ? flags.truth : changedFiles.length ? 'partial' : 'assumed'
|
|
1815
|
+
});
|
|
1816
|
+
const item = {
|
|
1817
|
+
schema: 'yam.mission-patch-queue-item.v1',
|
|
1818
|
+
lane_id: laneId,
|
|
1819
|
+
status,
|
|
1820
|
+
depends_on: arrayFlag(flags.depends_on),
|
|
1821
|
+
tool_intent: normalizeToolIntent(flags.intent || (changedFiles.length || generatedFiles.length ? 'write' : 'read_only')),
|
|
1822
|
+
patch_envelope: envelope,
|
|
1823
|
+
next_action: missionQueueNextAction(envelope, status)
|
|
1824
|
+
};
|
|
1825
|
+
const result = {
|
|
1826
|
+
schema: 'yam.mission-patch-queue-lite.v1',
|
|
1827
|
+
generated_at: new Date().toISOString(),
|
|
1828
|
+
persistence: flags.out ? 'written' : 'not-persisted',
|
|
1829
|
+
out: flags.out ? path.resolve(expandHome(flags.out)) : '',
|
|
1830
|
+
items: [item],
|
|
1831
|
+
queue_depth: 1,
|
|
1832
|
+
next_action: item.next_action,
|
|
1833
|
+
truth_status: envelope.truth_status
|
|
1834
|
+
};
|
|
1835
|
+
if (flags.out) {
|
|
1836
|
+
const target = path.resolve(expandHome(flags.out));
|
|
1837
|
+
await fsp.mkdir(path.dirname(target), { recursive: true });
|
|
1838
|
+
await fsp.writeFile(target, `${JSON.stringify(result, null, 2)}\n`);
|
|
1839
|
+
}
|
|
1840
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Mission patch queue');
|
|
1841
|
+
}
|
|
1842
|
+
function normalizeMissionQueueStatus(value = '') {
|
|
1843
|
+
const text = String(value || '').toLowerCase();
|
|
1844
|
+
if (['pending', 'applied', 'verified', 'blocked', 'reverted'].includes(text))
|
|
1845
|
+
return text;
|
|
1846
|
+
return 'pending';
|
|
1847
|
+
}
|
|
1848
|
+
function missionQueueNextAction(envelope, status = 'pending') {
|
|
1849
|
+
if (status === 'blocked')
|
|
1850
|
+
return 'resolve or document the blocker before applying dependent mission lanes';
|
|
1851
|
+
if (status === 'reverted')
|
|
1852
|
+
return 'rerun the before-check or verification hint before continuing dependent work';
|
|
1853
|
+
if (!envelope.assigned_scope)
|
|
1854
|
+
return 'add --scope so the patch can be reviewed against an assigned boundary';
|
|
1855
|
+
if (!envelope.changed_files.length)
|
|
1856
|
+
return 'add at least one --changed file when this represents code-changing work';
|
|
1857
|
+
if (!envelope.verification_hint)
|
|
1858
|
+
return 'add --verification-hint so reviewers know the smallest honest check';
|
|
1859
|
+
if (!envelope.rollback_hint.safe_revert_note && !envelope.rollback_hint.before_check)
|
|
1860
|
+
return 'add rollback or before-check detail before using this for risky work';
|
|
1861
|
+
return 'attach this envelope to the mission proof summary and cross-check changed files';
|
|
1862
|
+
}
|
|
1863
|
+
async function benchmark(args = []) {
|
|
1864
|
+
const subcommand = args[0] || 'help';
|
|
1865
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h')
|
|
1866
|
+
return benchmarkUsage();
|
|
1867
|
+
if (subcommand === 'report')
|
|
1868
|
+
return benchmarkReport(args.slice(1));
|
|
1869
|
+
console.error(`unknown benchmark command: ${subcommand}`);
|
|
1870
|
+
return benchmarkUsage();
|
|
1871
|
+
}
|
|
1872
|
+
function benchmarkUsage() {
|
|
1873
|
+
console.log(`yam benchmark
|
|
1874
|
+
|
|
1875
|
+
Usage:
|
|
1876
|
+
yam benchmark report [--label text] [--baseline n] [--current n] [--unit ms] [--target lower|higher] [--next-action text] [--json]
|
|
1877
|
+
|
|
1878
|
+
Notes:
|
|
1879
|
+
Records a small optimization loop result. It does not run benchmarks by itself.
|
|
1880
|
+
`);
|
|
1881
|
+
}
|
|
1882
|
+
async function benchmarkReport(args = []) {
|
|
1883
|
+
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h')
|
|
1884
|
+
return benchmarkUsage();
|
|
1885
|
+
const flags = parseSimpleFlags(args, new Set(['label', 'baseline', 'current', 'unit', 'target', 'next-action', 'rollback-hint', 'intent', 'json']));
|
|
1886
|
+
const baseline = numberOrNull(flags.baseline);
|
|
1887
|
+
const current = numberOrNull(flags.current);
|
|
1888
|
+
const target = String(flags.target || 'lower').toLowerCase() === 'higher' ? 'higher' : 'lower';
|
|
1889
|
+
const hasNumbers = baseline !== null && current !== null;
|
|
1890
|
+
const delta = hasNumbers ? current - baseline : null;
|
|
1891
|
+
const percent = hasNumbers && baseline !== 0 ? (delta / baseline) * 100 : null;
|
|
1892
|
+
const status = benchmarkStatus({ baseline, current, target });
|
|
1893
|
+
const result = {
|
|
1894
|
+
schema: 'yam.benchmark-report.v1',
|
|
1895
|
+
generated_at: new Date().toISOString(),
|
|
1896
|
+
label: String(flags.label || 'unnamed'),
|
|
1897
|
+
baseline,
|
|
1898
|
+
current,
|
|
1899
|
+
unit: String(flags.unit || ''),
|
|
1900
|
+
target,
|
|
1901
|
+
delta,
|
|
1902
|
+
percent,
|
|
1903
|
+
status,
|
|
1904
|
+
tool_intent: normalizeToolIntent(flags.intent || 'read_only'),
|
|
1905
|
+
rollback_hint: String(flags.rollback_hint || (status === 'regressed' ? 'inspect the optimization patch and revert the touched surface if the regression is confirmed' : 'no rollback action recorded')),
|
|
1906
|
+
next_action: String(flags.next_action || benchmarkNextAction(status)),
|
|
1907
|
+
truth_status: hasNumbers ? 'verified' : 'partial'
|
|
1908
|
+
};
|
|
1909
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Benchmark report');
|
|
1910
|
+
if (status === 'regressed')
|
|
1911
|
+
process.exitCode = 1;
|
|
1912
|
+
}
|
|
1913
|
+
function numberOrNull(value) {
|
|
1914
|
+
if (value === undefined || value === null || value === '')
|
|
1915
|
+
return null;
|
|
1916
|
+
const number = Number(value);
|
|
1917
|
+
return Number.isFinite(number) ? number : null;
|
|
1918
|
+
}
|
|
1919
|
+
function benchmarkStatus({ baseline, current, target }) {
|
|
1920
|
+
if (baseline === null || current === null)
|
|
1921
|
+
return 'needs-baseline';
|
|
1922
|
+
if (current === baseline)
|
|
1923
|
+
return 'unchanged';
|
|
1924
|
+
if (target === 'higher')
|
|
1925
|
+
return current > baseline ? 'improved' : 'regressed';
|
|
1926
|
+
return current < baseline ? 'improved' : 'regressed';
|
|
1927
|
+
}
|
|
1928
|
+
function benchmarkNextAction(status) {
|
|
1929
|
+
if (status === 'improved')
|
|
1930
|
+
return 'record the measurement source and keep the smallest relevant regression check';
|
|
1931
|
+
if (status === 'regressed')
|
|
1932
|
+
return 'treat this as a fix-first item before continuing planned optimization';
|
|
1933
|
+
if (status === 'unchanged')
|
|
1934
|
+
return 'keep the change only if it improves clarity or enables the next measured step';
|
|
1935
|
+
return 'capture baseline and current values before claiming optimization impact';
|
|
1936
|
+
}
|
|
1400
1937
|
function parseSimpleFlags(args = [], allowed = new Set()) {
|
|
1401
1938
|
const flags = {};
|
|
1402
1939
|
const positionals = [];
|
|
@@ -1418,7 +1955,12 @@ function parseSimpleFlags(args = [], allowed = new Set()) {
|
|
|
1418
1955
|
const value = inlineValue ?? args[index + 1] ?? '';
|
|
1419
1956
|
if (inlineValue === undefined)
|
|
1420
1957
|
index += 1;
|
|
1421
|
-
flags[normalizedKey]
|
|
1958
|
+
if (flags[normalizedKey] !== undefined) {
|
|
1959
|
+
flags[normalizedKey] = [...arrayFlag(flags[normalizedKey]), value];
|
|
1960
|
+
}
|
|
1961
|
+
else {
|
|
1962
|
+
flags[normalizedKey] = value;
|
|
1963
|
+
}
|
|
1422
1964
|
}
|
|
1423
1965
|
flags._ = positionals;
|
|
1424
1966
|
return flags;
|
|
@@ -2667,6 +3209,12 @@ async function main() {
|
|
|
2667
3209
|
return ueye(process.argv.slice(3));
|
|
2668
3210
|
if (command === 'media')
|
|
2669
3211
|
return media(process.argv.slice(3));
|
|
3212
|
+
if (command === 'runtime')
|
|
3213
|
+
return runtime(process.argv.slice(3));
|
|
3214
|
+
if (command === 'mission')
|
|
3215
|
+
return mission(process.argv.slice(3));
|
|
3216
|
+
if (command === 'benchmark')
|
|
3217
|
+
return benchmark(process.argv.slice(3));
|
|
2670
3218
|
if (command === 'release')
|
|
2671
3219
|
return release(process.argv.slice(3));
|
|
2672
3220
|
if (command === 'safety')
|