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/src/bin/yam.ts
CHANGED
|
@@ -141,6 +141,9 @@ Usage:
|
|
|
141
141
|
yam ueye compare --reference ref.png --actual screenshot.png [--json]
|
|
142
142
|
yam ueye report [--reference ref.png] [--actual screenshot.png] [--json]
|
|
143
143
|
yam media proof [--requested] [--attempted] [--output file] [--json]
|
|
144
|
+
yam runtime evidence [--backend terminal|in-app-browser|playwright|tmux|zellij] [--claim observed|started|stopped|cleanup-verified] [--json]
|
|
145
|
+
yam mission queue [--agent-id id] [--scope text] [--changed file] [--verification-hint text] [--json]
|
|
146
|
+
yam benchmark report [--baseline n] [--current n] [--unit ms] [--target lower|higher] [--json]
|
|
144
147
|
yam release report [--json]
|
|
145
148
|
yam safety [text...]
|
|
146
149
|
yam memory <init|add|list|summary|resolve> [dir] [options]
|
|
@@ -430,27 +433,97 @@ async function buildDoctorReport() {
|
|
|
430
433
|
const verifyIssues = await verify({ quiet: true });
|
|
431
434
|
if (verifyIssues > 0) issues.push(`verify reported ${verifyIssues} issue(s)`);
|
|
432
435
|
const globalHook = await readJsonOrDefault(path.join(os.homedir(), '.codex', 'hooks.json'), {});
|
|
436
|
+
const nextActionDetails = doctorNextActionDetails(issues);
|
|
433
437
|
return {
|
|
434
438
|
schema: 'yam.doctor.v1',
|
|
435
439
|
generatedAt: new Date().toISOString(),
|
|
436
440
|
ok: issues.length === 0,
|
|
437
441
|
issues,
|
|
438
|
-
nextActions:
|
|
442
|
+
nextActions: nextActionDetails.map((action) => action.next_action),
|
|
443
|
+
nextActionDetails,
|
|
439
444
|
yamLiteHook: hookConfigHasYamLite(globalHook) ? 'enabled' : 'disabled'
|
|
440
445
|
};
|
|
441
446
|
}
|
|
442
447
|
|
|
443
448
|
function doctorNextActions(issues = []) {
|
|
449
|
+
return doctorNextActionDetails(issues).map((action) => action.next_action);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function doctorNextActionDetails(issues = []) {
|
|
444
453
|
const actions = [];
|
|
445
454
|
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');
|
|
455
|
+
if (/missing source skill/i.test(issue)) 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'));
|
|
456
|
+
else if (/missing truth matrix/i.test(issue)) actions.push(nextActionDetail('restore-truth-matrix', 'warning', issue, 'restore references/truth-matrix.md before publishing', 'git checkout -- references/truth-matrix.md'));
|
|
457
|
+
else if (/unexpected local hooks/i.test(issue)) 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'));
|
|
458
|
+
else if (/unexpected .*automation/i.test(issue)) 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'));
|
|
459
|
+
else if (/legacy|retired/i.test(issue)) actions.push(nextActionDetail('replace-old-skills', 'warning', issue, 'run `yam install` to replace old skill entries', 'yam install'));
|
|
460
|
+
else if (/verify reported/i.test(issue)) actions.push(nextActionDetail('run-verify', 'error', issue, 'run `npm run verify` and fix the reported package boundary or metadata issue', 'npm run verify'));
|
|
461
|
+
}
|
|
462
|
+
return uniqueNextActionDetails(actions);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function nextActionDetail(id, severity, reason, nextAction, command = '') {
|
|
466
|
+
const priority = priorityForSeverity(severity);
|
|
467
|
+
const ownerRoute = ownerRouteForAction(id, reason, command);
|
|
468
|
+
return {
|
|
469
|
+
id,
|
|
470
|
+
severity,
|
|
471
|
+
priority,
|
|
472
|
+
owner_route: ownerRoute,
|
|
473
|
+
reason,
|
|
474
|
+
next_action: nextAction,
|
|
475
|
+
command,
|
|
476
|
+
tool_intent: toolIntentForCommand(command),
|
|
477
|
+
fix_first: priority === 'P0' || priority === 'P1',
|
|
478
|
+
blocks_release: priority === 'P0',
|
|
479
|
+
truth_status: 'partial'
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function priorityForSeverity(severity = '') {
|
|
484
|
+
if (/error|danger|block/i.test(String(severity))) return 'P0';
|
|
485
|
+
if (/warn/i.test(String(severity))) return 'P1';
|
|
486
|
+
if (/info/i.test(String(severity))) return 'P2';
|
|
487
|
+
return 'P3';
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function ownerRouteForAction(id = '', reason = '', command = '') {
|
|
491
|
+
const text = `${id} ${reason} ${command}`;
|
|
492
|
+
if (/visual|ueye|screenshot|browser/i.test(text)) return '$ueye';
|
|
493
|
+
if (/release|publish|package|registry|dist|version/i.test(text)) return '$deep';
|
|
494
|
+
if (/database|supabase|destructive|production|migration/i.test(text)) return '$deep';
|
|
495
|
+
if (/mission|subagent|team|lane/i.test(text)) return '$mission';
|
|
496
|
+
if (/research|docs|current/i.test(text)) return '$scout';
|
|
497
|
+
return '$quick';
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function toolIntentForCommand(command = '') {
|
|
501
|
+
const text = String(command || '').toLowerCase();
|
|
502
|
+
if (!text) return 'read_only';
|
|
503
|
+
if (/(publish|registry|release|npm pack|npm publish)/i.test(text)) return 'publish';
|
|
504
|
+
if (/(rm |delete|drop|truncate|reset|migrate|push|deploy|write|commit|add |install)/i.test(text)) return 'destructive';
|
|
505
|
+
if (/(dev|server|tmux|playwright|browser|screenshot|runtime|port|pid)/i.test(text)) return 'runtime';
|
|
506
|
+
if (/(ueye|visual|image|screen)/i.test(text)) return 'visual';
|
|
507
|
+
if (/(build|typecheck|lint|test|verify|doctor|status|pack)/i.test(text)) return 'read_only';
|
|
508
|
+
return 'write';
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
function normalizeToolIntent(value = '') {
|
|
512
|
+
const text = String(value || '').toLowerCase().replace(/[-\s]/g, '_');
|
|
513
|
+
if (['read_only', 'write', 'destructive', 'runtime', 'visual', 'publish'].includes(text)) return text;
|
|
514
|
+
return 'read_only';
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function uniqueNextActionDetails(actions = []) {
|
|
518
|
+
const seen = new Set<string>();
|
|
519
|
+
const unique = [];
|
|
520
|
+
for (const action of actions) {
|
|
521
|
+
const key = `${action.id}:${action.next_action}`;
|
|
522
|
+
if (seen.has(key)) continue;
|
|
523
|
+
seen.add(key);
|
|
524
|
+
unique.push(action);
|
|
452
525
|
}
|
|
453
|
-
return
|
|
526
|
+
return unique;
|
|
454
527
|
}
|
|
455
528
|
|
|
456
529
|
async function tools(args = []) {
|
|
@@ -531,6 +604,13 @@ async function buildToolsDoctorReport(targetDir = process.cwd()) {
|
|
|
531
604
|
...safety.hits,
|
|
532
605
|
...sqlScan.findings.map((finding) => ({ level: finding.level, reason: `${finding.file}: ${finding.reason}` }))
|
|
533
606
|
];
|
|
607
|
+
const nextActionDetails = toolsDoctorNextActionDetails({
|
|
608
|
+
pack,
|
|
609
|
+
rows,
|
|
610
|
+
riskNotes,
|
|
611
|
+
frameworkChecklist,
|
|
612
|
+
commands: detection.packageJson ? detection.commands : {}
|
|
613
|
+
});
|
|
534
614
|
return {
|
|
535
615
|
schemaVersion: 1,
|
|
536
616
|
generatedAt: new Date().toISOString(),
|
|
@@ -544,13 +624,8 @@ async function buildToolsDoctorReport(targetDir = process.cwd()) {
|
|
|
544
624
|
frameworkChecklist,
|
|
545
625
|
sqlScan,
|
|
546
626
|
riskNotes,
|
|
547
|
-
nextActions:
|
|
548
|
-
|
|
549
|
-
rows,
|
|
550
|
-
riskNotes,
|
|
551
|
-
frameworkChecklist,
|
|
552
|
-
commands: detection.packageJson ? detection.commands : {}
|
|
553
|
-
}),
|
|
627
|
+
nextActions: nextActionDetails.map((action) => action.next_action),
|
|
628
|
+
nextActionDetails,
|
|
554
629
|
routeRecommendations: {
|
|
555
630
|
dbSupabase: safety.hits.length || sqlScan.findings.length ? '$deep required before claiming safe' : '$deep when destructive or production mutation appears',
|
|
556
631
|
currentDocs: 'use current-docs proof only when version/freshness matters',
|
|
@@ -723,14 +798,18 @@ function checklistRow(id, label, route) {
|
|
|
723
798
|
return { id, label, route };
|
|
724
799
|
}
|
|
725
800
|
|
|
726
|
-
function toolsDoctorNextActions(
|
|
801
|
+
function toolsDoctorNextActions(input) {
|
|
802
|
+
return toolsDoctorNextActionDetails(input).map((action) => action.next_action);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
function toolsDoctorNextActionDetails({ pack, rows, riskNotes, frameworkChecklist, commands }) {
|
|
727
806
|
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
|
|
807
|
+
if (!pack) 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 .'));
|
|
808
|
+
if (rows.some((row) => row.name === 'Yam skills' && row.status !== 'ready')) actions.push(nextActionDetail('install-skills', 'warning', 'skill install state is incomplete', 'run `yam install` and restart Codex before relying on installed skills', 'yam install'));
|
|
809
|
+
if (riskNotes.some((note) => note.level === 'danger')) 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>"'));
|
|
810
|
+
if (frameworkChecklist?.detected && !commands?.build && !commands?.typecheck) actions.push(nextActionDetail('verification-command', 'warning', 'build/typecheck command not detected', 'add or document a small build/typecheck command for implementation verification', 'yam pack .'));
|
|
811
|
+
if (frameworkChecklist?.uiLike) 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);
|
|
734
813
|
}
|
|
735
814
|
|
|
736
815
|
async function scanSqlFiles(rootDir, { maxFiles = 30, maxBytes = 200000, maxDepth = 5 } = {}) {
|
|
@@ -1062,10 +1141,23 @@ async function release(args = []) {
|
|
|
1062
1141
|
console.log(`- ${check.id}: ${check.status} (${check.duration_ms}ms)`);
|
|
1063
1142
|
if (check.note) console.log(` ${check.note}`);
|
|
1064
1143
|
}
|
|
1144
|
+
if (report.provenance) {
|
|
1145
|
+
console.log(`- Git commit: ${report.provenance.git_commit || 'unknown'}`);
|
|
1146
|
+
console.log(`- Git dirty: ${report.provenance.git_dirty ? 'yes' : 'no'}`);
|
|
1147
|
+
console.log(`- Dist freshness: ${report.freshness.dist}`);
|
|
1148
|
+
}
|
|
1149
|
+
if (report.tarball) {
|
|
1150
|
+
console.log(`- Tarball: ${report.tarball.tarball_name || report.tarball.status}`);
|
|
1151
|
+
if (report.tarball.file_count !== undefined) console.log(`- Tarball files: ${report.tarball.file_count}`);
|
|
1152
|
+
}
|
|
1065
1153
|
if (report.failed.length) {
|
|
1066
1154
|
console.log('- Failed:');
|
|
1067
1155
|
for (const id of report.failed) console.log(` - ${id}`);
|
|
1068
1156
|
}
|
|
1157
|
+
if (report.next_actions?.length) {
|
|
1158
|
+
console.log('- Next actions:');
|
|
1159
|
+
for (const action of report.next_actions) console.log(` - ${action.next_action}`);
|
|
1160
|
+
}
|
|
1069
1161
|
if (!report.ok) process.exitCode = 1;
|
|
1070
1162
|
}
|
|
1071
1163
|
|
|
@@ -1080,6 +1172,10 @@ function runReleaseReport() {
|
|
|
1080
1172
|
['dist_freshness', ['npm', ['run', 'dist:freshness']]]
|
|
1081
1173
|
].map(([id, command]) => runReleaseCheck(id, command));
|
|
1082
1174
|
const failed = checks.filter((check) => check.status !== 'passed').map((check) => check.id);
|
|
1175
|
+
const provenance = releaseProvenance(checks);
|
|
1176
|
+
const tarball = releaseTarballProvenance();
|
|
1177
|
+
const freshness = releaseFreshness(checks, provenance, tarball);
|
|
1178
|
+
const nextActions = releaseNextActions(checks, provenance, tarball);
|
|
1083
1179
|
return {
|
|
1084
1180
|
schema: 'yam.release-report.v1',
|
|
1085
1181
|
generated_at: startedAt,
|
|
@@ -1088,10 +1184,117 @@ function runReleaseReport() {
|
|
|
1088
1184
|
ok: failed.length === 0,
|
|
1089
1185
|
truth_status: failed.length === 0 ? 'verified' : 'blocked',
|
|
1090
1186
|
checks,
|
|
1091
|
-
failed
|
|
1187
|
+
failed,
|
|
1188
|
+
provenance,
|
|
1189
|
+
tarball,
|
|
1190
|
+
freshness,
|
|
1191
|
+
next_actions: nextActions
|
|
1192
|
+
};
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function releaseProvenance(checks = []) {
|
|
1196
|
+
const gitCommit = runReadOnlyCommand('git', ['rev-parse', '--short', 'HEAD']);
|
|
1197
|
+
const gitStatus = runReadOnlyCommand('git', ['status', '--short']);
|
|
1198
|
+
const distChanged = runReadOnlyCommand('git', ['diff', '--name-only', '--', 'dist']);
|
|
1199
|
+
const packageFiles = checks.find((check) => check.id === 'package_boundary')?.status === 'passed' ? 'checked' : 'not-verified';
|
|
1200
|
+
return {
|
|
1201
|
+
schema: 'yam.release-provenance.v1',
|
|
1202
|
+
package_name: PACKAGE_JSON.name,
|
|
1203
|
+
version: VERSION,
|
|
1204
|
+
git_commit: gitCommit.trim(),
|
|
1205
|
+
git_dirty: Boolean(gitStatus.trim()),
|
|
1206
|
+
git_status_lines: gitStatus.trim().split(/\r?\n/).filter(Boolean).slice(0, 20),
|
|
1207
|
+
dist_changed_files: distChanged.trim().split(/\r?\n/).filter(Boolean).slice(0, 20),
|
|
1208
|
+
package_files: packageFiles,
|
|
1209
|
+
generated_at: new Date().toISOString(),
|
|
1210
|
+
truth_status: gitCommit.trim() && packageFiles === 'checked' ? 'partial' : 'assumed'
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
function releaseTarballProvenance() {
|
|
1215
|
+
const result = spawnSync('npm', ['pack', '--json', '--dry-run', '--ignore-scripts'], {
|
|
1216
|
+
cwd: ROOT,
|
|
1217
|
+
encoding: 'utf8',
|
|
1218
|
+
env: { ...process.env, npm_config_cache: path.join(os.tmpdir(), 'yam-npm-cache') },
|
|
1219
|
+
timeout: 120000
|
|
1220
|
+
});
|
|
1221
|
+
if (result.status !== 0) {
|
|
1222
|
+
return {
|
|
1223
|
+
schema: 'yam.release-tarball-provenance.v1',
|
|
1224
|
+
status: 'blocked',
|
|
1225
|
+
reason: summarizeCheckOutput([result.stdout, result.stderr, result.error?.message].filter(Boolean).join('\n')),
|
|
1226
|
+
truth_status: 'blocked'
|
|
1227
|
+
};
|
|
1228
|
+
}
|
|
1229
|
+
try {
|
|
1230
|
+
const data = JSON.parse(String(result.stdout || '[]'))[0] || {};
|
|
1231
|
+
return {
|
|
1232
|
+
schema: 'yam.release-tarball-provenance.v1',
|
|
1233
|
+
status: 'checked',
|
|
1234
|
+
tarball_name: String(data.filename || ''),
|
|
1235
|
+
package_size: Number(data.size || data.packageSize || 0),
|
|
1236
|
+
unpacked_size: Number(data.unpackedSize || 0),
|
|
1237
|
+
file_count: Array.isArray(data.files) ? data.files.length : Number(data.entryCount || 0),
|
|
1238
|
+
integrity: String(data.integrity || ''),
|
|
1239
|
+
shasum: String(data.shasum || ''),
|
|
1240
|
+
truth_status: data.filename ? 'verified' : 'partial'
|
|
1241
|
+
};
|
|
1242
|
+
} catch (error) {
|
|
1243
|
+
return {
|
|
1244
|
+
schema: 'yam.release-tarball-provenance.v1',
|
|
1245
|
+
status: 'blocked',
|
|
1246
|
+
reason: `could not parse npm pack output: ${errorMessage(error)}`,
|
|
1247
|
+
truth_status: 'blocked'
|
|
1248
|
+
};
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function releaseFreshness(checks = [], provenance: AnyRecord = {}, tarball: AnyRecord = {}) {
|
|
1253
|
+
const checkStatus = (id) => checks.find((check) => check.id === id)?.status || 'not-run';
|
|
1254
|
+
const dist = checkStatus('dist_freshness') === 'passed' && !provenance.dist_changed_files?.length ? 'fresh' : 'needs-check';
|
|
1255
|
+
const registry = checkStatus('registry_status') === 'passed' ? 'publishable-version' : 'not-verified';
|
|
1256
|
+
const boundary = checkStatus('package_boundary') === 'passed' ? 'checked' : 'not-verified';
|
|
1257
|
+
const smoke = checkStatus('cli_smoke') === 'passed' ? 'checked' : 'not-verified';
|
|
1258
|
+
const tarballStatus = tarball.status === 'checked' ? 'checked' : 'not-verified';
|
|
1259
|
+
return {
|
|
1260
|
+
schema: 'yam.release-freshness.v1',
|
|
1261
|
+
dist,
|
|
1262
|
+
registry,
|
|
1263
|
+
package_boundary: boundary,
|
|
1264
|
+
cli_smoke: smoke,
|
|
1265
|
+
tarball: tarballStatus,
|
|
1266
|
+
truth_status: [dist, registry, boundary, smoke, tarballStatus].every((value) => !/not-verified|needs-check/.test(value)) ? 'verified' : 'partial'
|
|
1092
1267
|
};
|
|
1093
1268
|
}
|
|
1094
1269
|
|
|
1270
|
+
function releaseNextActions(checks = [], provenance: AnyRecord = {}, tarball: AnyRecord = {}) {
|
|
1271
|
+
const actions = [];
|
|
1272
|
+
for (const check of checks) {
|
|
1273
|
+
if (check.status === 'passed') continue;
|
|
1274
|
+
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));
|
|
1275
|
+
}
|
|
1276
|
+
if (provenance.git_dirty) {
|
|
1277
|
+
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'));
|
|
1278
|
+
}
|
|
1279
|
+
if (provenance.dist_changed_files?.length) {
|
|
1280
|
+
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'));
|
|
1281
|
+
}
|
|
1282
|
+
if (tarball.status !== 'checked') {
|
|
1283
|
+
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'));
|
|
1284
|
+
}
|
|
1285
|
+
return uniqueNextActionDetails(actions);
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function runReadOnlyCommand(command, args = []) {
|
|
1289
|
+
const result = spawnSync(command, args, {
|
|
1290
|
+
cwd: ROOT,
|
|
1291
|
+
encoding: 'utf8',
|
|
1292
|
+
timeout: 30000
|
|
1293
|
+
});
|
|
1294
|
+
if (result.status !== 0) return '';
|
|
1295
|
+
return String(result.stdout || '');
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1095
1298
|
function runReleaseCheck(id, commandSpec) {
|
|
1096
1299
|
const [command, args] = commandSpec;
|
|
1097
1300
|
const start = Date.now();
|
|
@@ -1145,12 +1348,12 @@ Opt-in visual evidence helpers. Ueye stays one skill: fast by default, capture/c
|
|
|
1145
1348
|
Usage:
|
|
1146
1349
|
yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
|
|
1147
1350
|
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]
|
|
1351
|
+
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]
|
|
1149
1352
|
|
|
1150
1353
|
Notes:
|
|
1151
1354
|
capture uses a locally available Playwright install when present. It does not download browsers or install dependencies.
|
|
1152
1355
|
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.
|
|
1356
|
+
report produces a proof-ready Ueye visual run report and continuity/comparison record without requiring a new capture.
|
|
1154
1357
|
`);
|
|
1155
1358
|
}
|
|
1156
1359
|
|
|
@@ -1273,13 +1476,15 @@ async function ueyeCompare(args = []) {
|
|
|
1273
1476
|
|
|
1274
1477
|
async function ueyeReport(args = []) {
|
|
1275
1478
|
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']));
|
|
1479
|
+
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']));
|
|
1277
1480
|
const reference = String(flags.reference || flags.ref || '');
|
|
1278
1481
|
const actual = String(flags.actual || flags.screenshot || '');
|
|
1279
1482
|
const referenceSources = [];
|
|
1280
1483
|
const implementationSources = [];
|
|
1281
1484
|
let comparisonResult = 'not-verified';
|
|
1282
1485
|
let blockedReason = String(flags.blocked_reason || '');
|
|
1486
|
+
const reviewSessionId = String(flags.review_session_id || `ueye-${timestampId()}`);
|
|
1487
|
+
const previousReport = await readPreviousUeyeReport(flags.previous_report);
|
|
1283
1488
|
|
|
1284
1489
|
try {
|
|
1285
1490
|
if (reference) {
|
|
@@ -1288,7 +1493,7 @@ async function ueyeReport(args = []) {
|
|
|
1288
1493
|
source_kind: 'reference_image',
|
|
1289
1494
|
source_path: info.path,
|
|
1290
1495
|
source_hash: info.sha256,
|
|
1291
|
-
reference_id: path.basename(info.path),
|
|
1496
|
+
reference_id: String(flags.reference_id || path.basename(info.path)),
|
|
1292
1497
|
local_only: true,
|
|
1293
1498
|
operator_provided: true,
|
|
1294
1499
|
comparison_result: 'reference-recorded',
|
|
@@ -1301,7 +1506,8 @@ async function ueyeReport(args = []) {
|
|
|
1301
1506
|
source_kind: 'implementation_screenshot',
|
|
1302
1507
|
source_path: info.path,
|
|
1303
1508
|
source_hash: info.sha256,
|
|
1304
|
-
|
|
1509
|
+
reference_id: String(flags.reference_id || referenceSources[0]?.reference_id || ''),
|
|
1510
|
+
screenshot_id: String(flags.current_screenshot_id || flags.screenshot_id || path.basename(info.path)),
|
|
1305
1511
|
local_only: true,
|
|
1306
1512
|
operator_provided: false,
|
|
1307
1513
|
comparison_result: 'implementation-recorded',
|
|
@@ -1323,17 +1529,129 @@ async function ueyeReport(args = []) {
|
|
|
1323
1529
|
comparison_result: comparisonResult,
|
|
1324
1530
|
design_quality: String(flags.design_quality || 'not-checked'),
|
|
1325
1531
|
blocked_reason: blockedReason,
|
|
1326
|
-
next_action: String(flags.next_action || '')
|
|
1532
|
+
next_action: String(flags.next_visual_action || flags.next_action || '')
|
|
1533
|
+
});
|
|
1534
|
+
const comparisonReport = buildUeyeComparisonReport({
|
|
1535
|
+
reviewSessionId,
|
|
1536
|
+
previousReport,
|
|
1537
|
+
referenceSources,
|
|
1538
|
+
implementationSources,
|
|
1539
|
+
comparisonResult,
|
|
1540
|
+
designQuality: String(flags.design_quality || 'not-checked'),
|
|
1541
|
+
similar: arrayFlag(flags.similar),
|
|
1542
|
+
different: arrayFlag(flags.different),
|
|
1543
|
+
missing: arrayFlag(flags.missing),
|
|
1544
|
+
notes: arrayFlag(flags.comparison_notes),
|
|
1545
|
+
resolved: arrayFlag(flags.resolved),
|
|
1546
|
+
newFindings: arrayFlag(flags.new_finding),
|
|
1547
|
+
stillOpen: arrayFlag(flags.still_open),
|
|
1548
|
+
regressions: arrayFlag(flags.regression),
|
|
1549
|
+
previousScreenshotId: String(flags.previous_screenshot_id || previousReport?.screenshot_id || ''),
|
|
1550
|
+
currentScreenshotId: String(flags.current_screenshot_id || flags.screenshot_id || implementationSources[0]?.screenshot_id || ''),
|
|
1551
|
+
viewport: String(flags.viewport || ''),
|
|
1552
|
+
state: String(flags.state || 'unknown'),
|
|
1553
|
+
nextAction: report.next_action,
|
|
1554
|
+
truthStatus: report.truth_status
|
|
1327
1555
|
});
|
|
1328
1556
|
const result = {
|
|
1329
1557
|
...report,
|
|
1558
|
+
review_session_id: reviewSessionId,
|
|
1559
|
+
tool_intent: 'visual',
|
|
1330
1560
|
capture_backend: String(flags.capture_backend || 'not-recorded'),
|
|
1331
|
-
compare_backend: String(flags.compare_backend || 'local-file-hash')
|
|
1561
|
+
compare_backend: String(flags.compare_backend || 'local-file-hash'),
|
|
1562
|
+
continuity: comparisonReport.continuity,
|
|
1563
|
+
comparison_report: comparisonReport
|
|
1332
1564
|
};
|
|
1333
1565
|
printJsonOrHuman(result, Boolean(flags.json), 'Ueye report');
|
|
1334
1566
|
if (report.truth_status === 'blocked') process.exitCode = 1;
|
|
1335
1567
|
}
|
|
1336
1568
|
|
|
1569
|
+
async function readPreviousUeyeReport(file = '') {
|
|
1570
|
+
const target = String(file || '');
|
|
1571
|
+
if (!target) return null;
|
|
1572
|
+
try {
|
|
1573
|
+
const data = await readJson(path.resolve(expandHome(target)));
|
|
1574
|
+
return {
|
|
1575
|
+
path: path.resolve(expandHome(target)),
|
|
1576
|
+
review_session_id: data.review_session_id || data.comparison_report?.review_session_id || '',
|
|
1577
|
+
screenshot_id: data.comparison_report?.current_screenshot_id || data.current_screenshot_id || data.screenshot_id || '',
|
|
1578
|
+
truth_status: data.truth_status || data.comparison_report?.truth_status || 'partial',
|
|
1579
|
+
comparison_result: data.comparison_result || data.comparison_report?.comparison_result || 'not-verified'
|
|
1580
|
+
};
|
|
1581
|
+
} catch (error) {
|
|
1582
|
+
return {
|
|
1583
|
+
path: path.resolve(expandHome(target)),
|
|
1584
|
+
blocked_reason: errorMessage(error),
|
|
1585
|
+
truth_status: 'blocked',
|
|
1586
|
+
comparison_result: 'not-verified'
|
|
1587
|
+
};
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
function buildUeyeComparisonReport({
|
|
1592
|
+
reviewSessionId,
|
|
1593
|
+
previousReport,
|
|
1594
|
+
referenceSources,
|
|
1595
|
+
implementationSources,
|
|
1596
|
+
comparisonResult,
|
|
1597
|
+
designQuality,
|
|
1598
|
+
similar,
|
|
1599
|
+
different,
|
|
1600
|
+
missing,
|
|
1601
|
+
notes,
|
|
1602
|
+
resolved,
|
|
1603
|
+
newFindings,
|
|
1604
|
+
stillOpen,
|
|
1605
|
+
regressions,
|
|
1606
|
+
previousScreenshotId,
|
|
1607
|
+
currentScreenshotId,
|
|
1608
|
+
viewport,
|
|
1609
|
+
state,
|
|
1610
|
+
nextAction,
|
|
1611
|
+
truthStatus
|
|
1612
|
+
}) {
|
|
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
|
+
}
|
|
1654
|
+
|
|
1337
1655
|
async function media(args = []) {
|
|
1338
1656
|
const subcommand = args[0] || 'help';
|
|
1339
1657
|
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return mediaUsage();
|
|
@@ -1383,6 +1701,227 @@ async function mediaProof(args = []) {
|
|
|
1383
1701
|
if (proof.truth_status === 'blocked') process.exitCode = 1;
|
|
1384
1702
|
}
|
|
1385
1703
|
|
|
1704
|
+
async function runtime(args = []) {
|
|
1705
|
+
const subcommand = args[0] || 'help';
|
|
1706
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return runtimeUsage();
|
|
1707
|
+
if (subcommand === 'evidence') return runtimeEvidence(args.slice(1));
|
|
1708
|
+
console.error(`unknown runtime command: ${subcommand}`);
|
|
1709
|
+
return runtimeUsage();
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
function runtimeUsage() {
|
|
1713
|
+
console.log(`yam runtime
|
|
1714
|
+
|
|
1715
|
+
Usage:
|
|
1716
|
+
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]
|
|
1717
|
+
|
|
1718
|
+
Notes:
|
|
1719
|
+
Records a small runtime evidence shape. It does not start, stop, or inspect processes by itself.
|
|
1720
|
+
`);
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
async function runtimeEvidence(args = []) {
|
|
1724
|
+
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return runtimeUsage();
|
|
1725
|
+
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']));
|
|
1726
|
+
const evidence = buildRuntimeBackendEvidence({
|
|
1727
|
+
backend: flags.backend,
|
|
1728
|
+
claim: flags.claim,
|
|
1729
|
+
evidence_id: String(flags.evidence_id || `runtime-${timestampId()}`),
|
|
1730
|
+
command: String(flags.command || ''),
|
|
1731
|
+
cleanup_checked: Boolean(flags.cleanup_checked),
|
|
1732
|
+
note: String(flags.note || ''),
|
|
1733
|
+
truth_status: isTruthStatus(flags.truth) ? flags.truth : undefined
|
|
1734
|
+
});
|
|
1735
|
+
const runtimeDetails = runtimeEvidenceDetails(flags);
|
|
1736
|
+
if (!isTruthStatus(flags.truth) && evidence.truth_status === 'verified' && !runtimeDetailsHasVerification(runtimeDetails)) {
|
|
1737
|
+
evidence.truth_status = 'partial';
|
|
1738
|
+
}
|
|
1739
|
+
const result = {
|
|
1740
|
+
...evidence,
|
|
1741
|
+
runtime_details: runtimeDetails,
|
|
1742
|
+
tool_intent: normalizeToolIntent(flags.intent || 'runtime'),
|
|
1743
|
+
next_action: runtimeEvidenceNextAction(evidence)
|
|
1744
|
+
};
|
|
1745
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Runtime evidence');
|
|
1746
|
+
if (result.truth_status === 'blocked' || result.truth_status === 'real_required_missing') process.exitCode = 1;
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
function runtimeEvidenceDetails(flags: AnyRecord = {}) {
|
|
1750
|
+
return {
|
|
1751
|
+
schema: 'yam.runtime-details.v1',
|
|
1752
|
+
pid: numberOrNull(flags.pid),
|
|
1753
|
+
port: numberOrNull(flags.port),
|
|
1754
|
+
url: String(flags.url || ''),
|
|
1755
|
+
exit_code: numberOrNull(flags.exit_code),
|
|
1756
|
+
screenshot_id: String(flags.screenshot_id || ''),
|
|
1757
|
+
started_at: String(flags.started_at || ''),
|
|
1758
|
+
stopped_at: String(flags.stopped_at || '')
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
function runtimeDetailsHasVerification(details: AnyRecord = {}) {
|
|
1763
|
+
return Boolean(details.url || details.screenshot_id || details.pid !== null || details.port !== null || details.exit_code !== null || details.started_at || details.stopped_at);
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
function runtimeEvidenceNextAction(evidence) {
|
|
1767
|
+
if (evidence.truth_status === 'proven' || evidence.truth_status === 'verified') return 'attach this evidence to `yam proof` when making a runtime claim';
|
|
1768
|
+
if (evidence.claim === 'cleanup_verified' && !evidence.cleanup_checked) return 'rerun with --cleanup-checked only after process exit or intentional persistence is confirmed';
|
|
1769
|
+
if (evidence.backend === 'none' || evidence.backend === 'unknown') return 'record the actual runtime backend before claiming runtime verification';
|
|
1770
|
+
return 'add command output, screenshot id, pid/session id, or cleanup proof before upgrading the claim';
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1773
|
+
async function mission(args = []) {
|
|
1774
|
+
const subcommand = args[0] || 'help';
|
|
1775
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return missionUsage();
|
|
1776
|
+
if (subcommand === 'queue' || subcommand === 'patch') return missionQueue(args.slice(1));
|
|
1777
|
+
console.error(`unknown mission command: ${subcommand}`);
|
|
1778
|
+
return missionUsage();
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
function missionUsage() {
|
|
1782
|
+
console.log(`yam mission
|
|
1783
|
+
|
|
1784
|
+
Usage:
|
|
1785
|
+
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]
|
|
1786
|
+
|
|
1787
|
+
Notes:
|
|
1788
|
+
Produces a patch queue item for mission handoff/review. It does not run workers; persistence is opt-in with --out.
|
|
1789
|
+
`);
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
async function missionQueue(args = []) {
|
|
1793
|
+
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return missionUsage();
|
|
1794
|
+
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']));
|
|
1795
|
+
const changedFiles = arrayFlag(flags.changed);
|
|
1796
|
+
const generatedFiles = arrayFlag(flags.generated);
|
|
1797
|
+
const laneId = String(flags.lane_id || flags.agent_id || `lane-${timestampId()}`);
|
|
1798
|
+
const status = normalizeMissionQueueStatus(flags.status || 'pending');
|
|
1799
|
+
const rollback = buildRollbackHint({
|
|
1800
|
+
touched_files: changedFiles,
|
|
1801
|
+
generated_files: generatedFiles,
|
|
1802
|
+
before_check: String(flags.before_check || ''),
|
|
1803
|
+
safe_revert_note: String(flags.safe_revert_note || flags.rollback_hint || '')
|
|
1804
|
+
});
|
|
1805
|
+
const envelope = buildMissionPatchEnvelope({
|
|
1806
|
+
agent_id: String(flags.agent_id || `agent-${timestampId()}`),
|
|
1807
|
+
assigned_scope: String(flags.scope || ''),
|
|
1808
|
+
changed_files: changedFiles,
|
|
1809
|
+
verification_hint: String(flags.verification_hint || ''),
|
|
1810
|
+
rollback_hint: rollback,
|
|
1811
|
+
truth_status: isTruthStatus(flags.truth) ? flags.truth : changedFiles.length ? 'partial' : 'assumed'
|
|
1812
|
+
});
|
|
1813
|
+
const item = {
|
|
1814
|
+
schema: 'yam.mission-patch-queue-item.v1',
|
|
1815
|
+
lane_id: laneId,
|
|
1816
|
+
status,
|
|
1817
|
+
depends_on: arrayFlag(flags.depends_on),
|
|
1818
|
+
tool_intent: normalizeToolIntent(flags.intent || (changedFiles.length || generatedFiles.length ? 'write' : 'read_only')),
|
|
1819
|
+
patch_envelope: envelope,
|
|
1820
|
+
next_action: missionQueueNextAction(envelope, status)
|
|
1821
|
+
};
|
|
1822
|
+
const result = {
|
|
1823
|
+
schema: 'yam.mission-patch-queue-lite.v1',
|
|
1824
|
+
generated_at: new Date().toISOString(),
|
|
1825
|
+
persistence: flags.out ? 'written' : 'not-persisted',
|
|
1826
|
+
out: flags.out ? path.resolve(expandHome(flags.out)) : '',
|
|
1827
|
+
items: [item],
|
|
1828
|
+
queue_depth: 1,
|
|
1829
|
+
next_action: item.next_action,
|
|
1830
|
+
truth_status: envelope.truth_status
|
|
1831
|
+
};
|
|
1832
|
+
if (flags.out) {
|
|
1833
|
+
const target = path.resolve(expandHome(flags.out));
|
|
1834
|
+
await fsp.mkdir(path.dirname(target), { recursive: true });
|
|
1835
|
+
await fsp.writeFile(target, `${JSON.stringify(result, null, 2)}\n`);
|
|
1836
|
+
}
|
|
1837
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Mission patch queue');
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
function normalizeMissionQueueStatus(value = '') {
|
|
1841
|
+
const text = String(value || '').toLowerCase();
|
|
1842
|
+
if (['pending', 'applied', 'verified', 'blocked', 'reverted'].includes(text)) return text;
|
|
1843
|
+
return 'pending';
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
function missionQueueNextAction(envelope, status = 'pending') {
|
|
1847
|
+
if (status === 'blocked') return 'resolve or document the blocker before applying dependent mission lanes';
|
|
1848
|
+
if (status === 'reverted') return 'rerun the before-check or verification hint before continuing dependent work';
|
|
1849
|
+
if (!envelope.assigned_scope) return 'add --scope so the patch can be reviewed against an assigned boundary';
|
|
1850
|
+
if (!envelope.changed_files.length) return 'add at least one --changed file when this represents code-changing work';
|
|
1851
|
+
if (!envelope.verification_hint) return 'add --verification-hint so reviewers know the smallest honest check';
|
|
1852
|
+
if (!envelope.rollback_hint.safe_revert_note && !envelope.rollback_hint.before_check) return 'add rollback or before-check detail before using this for risky work';
|
|
1853
|
+
return 'attach this envelope to the mission proof summary and cross-check changed files';
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
async function benchmark(args = []) {
|
|
1857
|
+
const subcommand = args[0] || 'help';
|
|
1858
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return benchmarkUsage();
|
|
1859
|
+
if (subcommand === 'report') return benchmarkReport(args.slice(1));
|
|
1860
|
+
console.error(`unknown benchmark command: ${subcommand}`);
|
|
1861
|
+
return benchmarkUsage();
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
function benchmarkUsage() {
|
|
1865
|
+
console.log(`yam benchmark
|
|
1866
|
+
|
|
1867
|
+
Usage:
|
|
1868
|
+
yam benchmark report [--label text] [--baseline n] [--current n] [--unit ms] [--target lower|higher] [--next-action text] [--json]
|
|
1869
|
+
|
|
1870
|
+
Notes:
|
|
1871
|
+
Records a small optimization loop result. It does not run benchmarks by itself.
|
|
1872
|
+
`);
|
|
1873
|
+
}
|
|
1874
|
+
|
|
1875
|
+
async function benchmarkReport(args = []) {
|
|
1876
|
+
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return benchmarkUsage();
|
|
1877
|
+
const flags = parseSimpleFlags(args, new Set(['label', 'baseline', 'current', 'unit', 'target', 'next-action', 'rollback-hint', 'intent', 'json']));
|
|
1878
|
+
const baseline = numberOrNull(flags.baseline);
|
|
1879
|
+
const current = numberOrNull(flags.current);
|
|
1880
|
+
const target = String(flags.target || 'lower').toLowerCase() === 'higher' ? 'higher' : 'lower';
|
|
1881
|
+
const hasNumbers = baseline !== null && current !== null;
|
|
1882
|
+
const delta = hasNumbers ? current - baseline : null;
|
|
1883
|
+
const percent = hasNumbers && baseline !== 0 ? (delta / baseline) * 100 : null;
|
|
1884
|
+
const status = benchmarkStatus({ baseline, current, target });
|
|
1885
|
+
const result = {
|
|
1886
|
+
schema: 'yam.benchmark-report.v1',
|
|
1887
|
+
generated_at: new Date().toISOString(),
|
|
1888
|
+
label: String(flags.label || 'unnamed'),
|
|
1889
|
+
baseline,
|
|
1890
|
+
current,
|
|
1891
|
+
unit: String(flags.unit || ''),
|
|
1892
|
+
target,
|
|
1893
|
+
delta,
|
|
1894
|
+
percent,
|
|
1895
|
+
status,
|
|
1896
|
+
tool_intent: normalizeToolIntent(flags.intent || 'read_only'),
|
|
1897
|
+
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')),
|
|
1898
|
+
next_action: String(flags.next_action || benchmarkNextAction(status)),
|
|
1899
|
+
truth_status: hasNumbers ? 'verified' : 'partial'
|
|
1900
|
+
};
|
|
1901
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Benchmark report');
|
|
1902
|
+
if (status === 'regressed') process.exitCode = 1;
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1905
|
+
function numberOrNull(value) {
|
|
1906
|
+
if (value === undefined || value === null || value === '') return null;
|
|
1907
|
+
const number = Number(value);
|
|
1908
|
+
return Number.isFinite(number) ? number : null;
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
function benchmarkStatus({ baseline, current, target }) {
|
|
1912
|
+
if (baseline === null || current === null) return 'needs-baseline';
|
|
1913
|
+
if (current === baseline) return 'unchanged';
|
|
1914
|
+
if (target === 'higher') return current > baseline ? 'improved' : 'regressed';
|
|
1915
|
+
return current < baseline ? 'improved' : 'regressed';
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
function benchmarkNextAction(status) {
|
|
1919
|
+
if (status === 'improved') return 'record the measurement source and keep the smallest relevant regression check';
|
|
1920
|
+
if (status === 'regressed') return 'treat this as a fix-first item before continuing planned optimization';
|
|
1921
|
+
if (status === 'unchanged') return 'keep the change only if it improves clarity or enables the next measured step';
|
|
1922
|
+
return 'capture baseline and current values before claiming optimization impact';
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1386
1925
|
function parseSimpleFlags(args = [], allowed = new Set<string>()) {
|
|
1387
1926
|
const flags: AnyRecord = {};
|
|
1388
1927
|
const positionals: string[] = [];
|
|
@@ -1402,7 +1941,11 @@ function parseSimpleFlags(args = [], allowed = new Set<string>()) {
|
|
|
1402
1941
|
}
|
|
1403
1942
|
const value = inlineValue ?? args[index + 1] ?? '';
|
|
1404
1943
|
if (inlineValue === undefined) index += 1;
|
|
1405
|
-
flags[normalizedKey]
|
|
1944
|
+
if (flags[normalizedKey] !== undefined) {
|
|
1945
|
+
flags[normalizedKey] = [...arrayFlag(flags[normalizedKey]), value];
|
|
1946
|
+
} else {
|
|
1947
|
+
flags[normalizedKey] = value;
|
|
1948
|
+
}
|
|
1406
1949
|
}
|
|
1407
1950
|
flags._ = positionals;
|
|
1408
1951
|
return flags;
|
|
@@ -2644,6 +3187,9 @@ async function main() {
|
|
|
2644
3187
|
if (command === 'proof') return proof(process.argv.slice(3));
|
|
2645
3188
|
if (command === 'ueye') return ueye(process.argv.slice(3));
|
|
2646
3189
|
if (command === 'media') return media(process.argv.slice(3));
|
|
3190
|
+
if (command === 'runtime') return runtime(process.argv.slice(3));
|
|
3191
|
+
if (command === 'mission') return mission(process.argv.slice(3));
|
|
3192
|
+
if (command === 'benchmark') return benchmark(process.argv.slice(3));
|
|
2647
3193
|
if (command === 'release') return release(process.argv.slice(3));
|
|
2648
3194
|
if (command === 'safety') return safety(process.argv.slice(3));
|
|
2649
3195
|
if (command === 'memory') return memory(process.argv.slice(3));
|