yam-flow 0.1.3 → 0.1.5
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 +49 -0
- package/COMMANDS.md +29 -2
- package/README.md +21 -4
- package/ROADMAP.md +5 -5
- package/dist/bin/yam.js +661 -35
- package/dist/lib/trust-kernel.d.ts +26 -0
- package/dist/lib/trust-kernel.js +30 -0
- 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 +63 -7
- package/references/ueye.md +40 -2
- package/skills/question/SKILL.md +1 -1
- package/skills/ueye/SKILL.md +18 -9
- package/src/bin/yam.ts +660 -34
- package/src/lib/trust-kernel.ts +57 -0
- 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 +73 -0
- package/templates/ueye-review.md +73 -0
- package/templates/yam.project.md +2 -2
- package/yam.manifest.json +2 -2
package/src/bin/yam.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
buildRuntimeBackendEvidence,
|
|
17
17
|
buildUeyeVisualProvenance,
|
|
18
18
|
buildUeyeRunReport,
|
|
19
|
+
buildUeyeSurfaceContext,
|
|
19
20
|
buildYamCompletionProof,
|
|
20
21
|
detectDbSafetyText as detectTrustDbSafetyText,
|
|
21
22
|
isTruthStatus
|
|
@@ -139,8 +140,11 @@ Usage:
|
|
|
139
140
|
yam proof write [dir] [--format json|md] [--out file] [--route route] [--truth status] [--command text]
|
|
140
141
|
yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
|
|
141
142
|
yam ueye compare --reference ref.png --actual screenshot.png [--json]
|
|
142
|
-
yam ueye report [--reference ref.png] [--actual screenshot.png] [--json]
|
|
143
|
+
yam ueye report [--reference ref.png] [--actual screenshot.png] [--provider-context local] [--execution-surface in-app-browser] [--json]
|
|
143
144
|
yam media proof [--requested] [--attempted] [--output file] [--json]
|
|
145
|
+
yam runtime evidence [--backend terminal|in-app-browser|playwright|tmux|zellij] [--claim observed|started|stopped|cleanup-verified] [--json]
|
|
146
|
+
yam mission queue [--agent-id id] [--scope text] [--changed file] [--verification-hint text] [--json]
|
|
147
|
+
yam benchmark report [--baseline n] [--current n] [--unit ms] [--target lower|higher] [--json]
|
|
144
148
|
yam release report [--json]
|
|
145
149
|
yam safety [text...]
|
|
146
150
|
yam memory <init|add|list|summary|resolve> [dir] [options]
|
|
@@ -430,27 +434,97 @@ async function buildDoctorReport() {
|
|
|
430
434
|
const verifyIssues = await verify({ quiet: true });
|
|
431
435
|
if (verifyIssues > 0) issues.push(`verify reported ${verifyIssues} issue(s)`);
|
|
432
436
|
const globalHook = await readJsonOrDefault(path.join(os.homedir(), '.codex', 'hooks.json'), {});
|
|
437
|
+
const nextActionDetails = doctorNextActionDetails(issues);
|
|
433
438
|
return {
|
|
434
439
|
schema: 'yam.doctor.v1',
|
|
435
440
|
generatedAt: new Date().toISOString(),
|
|
436
441
|
ok: issues.length === 0,
|
|
437
442
|
issues,
|
|
438
|
-
nextActions:
|
|
443
|
+
nextActions: nextActionDetails.map((action) => action.next_action),
|
|
444
|
+
nextActionDetails,
|
|
439
445
|
yamLiteHook: hookConfigHasYamLite(globalHook) ? 'enabled' : 'disabled'
|
|
440
446
|
};
|
|
441
447
|
}
|
|
442
448
|
|
|
443
449
|
function doctorNextActions(issues = []) {
|
|
450
|
+
return doctorNextActionDetails(issues).map((action) => action.next_action);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function doctorNextActionDetails(issues = []) {
|
|
444
454
|
const actions = [];
|
|
445
455
|
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');
|
|
456
|
+
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'));
|
|
457
|
+
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'));
|
|
458
|
+
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'));
|
|
459
|
+
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'));
|
|
460
|
+
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'));
|
|
461
|
+
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'));
|
|
452
462
|
}
|
|
453
|
-
return
|
|
463
|
+
return uniqueNextActionDetails(actions);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function nextActionDetail(id, severity, reason, nextAction, command = '') {
|
|
467
|
+
const priority = priorityForSeverity(severity);
|
|
468
|
+
const ownerRoute = ownerRouteForAction(id, reason, command);
|
|
469
|
+
return {
|
|
470
|
+
id,
|
|
471
|
+
severity,
|
|
472
|
+
priority,
|
|
473
|
+
owner_route: ownerRoute,
|
|
474
|
+
reason,
|
|
475
|
+
next_action: nextAction,
|
|
476
|
+
command,
|
|
477
|
+
tool_intent: toolIntentForCommand(command),
|
|
478
|
+
fix_first: priority === 'P0' || priority === 'P1',
|
|
479
|
+
blocks_release: priority === 'P0',
|
|
480
|
+
truth_status: 'partial'
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function priorityForSeverity(severity = '') {
|
|
485
|
+
if (/error|danger|block/i.test(String(severity))) return 'P0';
|
|
486
|
+
if (/warn/i.test(String(severity))) return 'P1';
|
|
487
|
+
if (/info/i.test(String(severity))) return 'P2';
|
|
488
|
+
return 'P3';
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function ownerRouteForAction(id = '', reason = '', command = '') {
|
|
492
|
+
const text = `${id} ${reason} ${command}`;
|
|
493
|
+
if (/visual|ueye|screenshot|browser/i.test(text)) return '$ueye';
|
|
494
|
+
if (/release|publish|package|registry|dist|version/i.test(text)) return '$deep';
|
|
495
|
+
if (/database|supabase|destructive|production|migration/i.test(text)) return '$deep';
|
|
496
|
+
if (/mission|subagent|team|lane/i.test(text)) return '$mission';
|
|
497
|
+
if (/research|docs|current/i.test(text)) return '$scout';
|
|
498
|
+
return '$quick';
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function toolIntentForCommand(command = '') {
|
|
502
|
+
const text = String(command || '').toLowerCase();
|
|
503
|
+
if (!text) return 'read_only';
|
|
504
|
+
if (/(publish|registry|release|npm pack|npm publish)/i.test(text)) return 'publish';
|
|
505
|
+
if (/(rm |delete|drop|truncate|reset|migrate|push|deploy|write|commit|add |install)/i.test(text)) return 'destructive';
|
|
506
|
+
if (/(dev|server|tmux|playwright|browser|screenshot|runtime|port|pid)/i.test(text)) return 'runtime';
|
|
507
|
+
if (/(ueye|visual|image|screen)/i.test(text)) return 'visual';
|
|
508
|
+
if (/(build|typecheck|lint|test|verify|doctor|status|pack)/i.test(text)) return 'read_only';
|
|
509
|
+
return 'write';
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function normalizeToolIntent(value = '') {
|
|
513
|
+
const text = String(value || '').toLowerCase().replace(/[-\s]/g, '_');
|
|
514
|
+
if (['read_only', 'write', 'destructive', 'runtime', 'visual', 'publish'].includes(text)) return text;
|
|
515
|
+
return 'read_only';
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function uniqueNextActionDetails(actions = []) {
|
|
519
|
+
const seen = new Set<string>();
|
|
520
|
+
const unique = [];
|
|
521
|
+
for (const action of actions) {
|
|
522
|
+
const key = `${action.id}:${action.next_action}`;
|
|
523
|
+
if (seen.has(key)) continue;
|
|
524
|
+
seen.add(key);
|
|
525
|
+
unique.push(action);
|
|
526
|
+
}
|
|
527
|
+
return unique;
|
|
454
528
|
}
|
|
455
529
|
|
|
456
530
|
async function tools(args = []) {
|
|
@@ -531,6 +605,13 @@ async function buildToolsDoctorReport(targetDir = process.cwd()) {
|
|
|
531
605
|
...safety.hits,
|
|
532
606
|
...sqlScan.findings.map((finding) => ({ level: finding.level, reason: `${finding.file}: ${finding.reason}` }))
|
|
533
607
|
];
|
|
608
|
+
const nextActionDetails = toolsDoctorNextActionDetails({
|
|
609
|
+
pack,
|
|
610
|
+
rows,
|
|
611
|
+
riskNotes,
|
|
612
|
+
frameworkChecklist,
|
|
613
|
+
commands: detection.packageJson ? detection.commands : {}
|
|
614
|
+
});
|
|
534
615
|
return {
|
|
535
616
|
schemaVersion: 1,
|
|
536
617
|
generatedAt: new Date().toISOString(),
|
|
@@ -544,13 +625,8 @@ async function buildToolsDoctorReport(targetDir = process.cwd()) {
|
|
|
544
625
|
frameworkChecklist,
|
|
545
626
|
sqlScan,
|
|
546
627
|
riskNotes,
|
|
547
|
-
nextActions:
|
|
548
|
-
|
|
549
|
-
rows,
|
|
550
|
-
riskNotes,
|
|
551
|
-
frameworkChecklist,
|
|
552
|
-
commands: detection.packageJson ? detection.commands : {}
|
|
553
|
-
}),
|
|
628
|
+
nextActions: nextActionDetails.map((action) => action.next_action),
|
|
629
|
+
nextActionDetails,
|
|
554
630
|
routeRecommendations: {
|
|
555
631
|
dbSupabase: safety.hits.length || sqlScan.findings.length ? '$deep required before claiming safe' : '$deep when destructive or production mutation appears',
|
|
556
632
|
currentDocs: 'use current-docs proof only when version/freshness matters',
|
|
@@ -723,14 +799,18 @@ function checklistRow(id, label, route) {
|
|
|
723
799
|
return { id, label, route };
|
|
724
800
|
}
|
|
725
801
|
|
|
726
|
-
function toolsDoctorNextActions(
|
|
802
|
+
function toolsDoctorNextActions(input) {
|
|
803
|
+
return toolsDoctorNextActionDetails(input).map((action) => action.next_action);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function toolsDoctorNextActionDetails({ pack, rows, riskNotes, frameworkChecklist, commands }) {
|
|
727
807
|
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
|
|
733
|
-
return
|
|
808
|
+
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 .'));
|
|
809
|
+
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'));
|
|
810
|
+
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>"'));
|
|
811
|
+
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 .'));
|
|
812
|
+
if (frameworkChecklist?.uiLike) actions.push(nextActionDetail('ueye-evidence', 'info', 'UI-like project detected', 'for Ueye verified claims, record reference, actual screenshot, comparison result, and surface context', 'yam ueye report --reference ref.png --actual shot.png --provider-context local --execution-surface in-app-browser --json'));
|
|
813
|
+
return uniqueNextActionDetails(actions);
|
|
734
814
|
}
|
|
735
815
|
|
|
736
816
|
async function scanSqlFiles(rootDir, { maxFiles = 30, maxBytes = 200000, maxDepth = 5 } = {}) {
|
|
@@ -1062,10 +1142,23 @@ async function release(args = []) {
|
|
|
1062
1142
|
console.log(`- ${check.id}: ${check.status} (${check.duration_ms}ms)`);
|
|
1063
1143
|
if (check.note) console.log(` ${check.note}`);
|
|
1064
1144
|
}
|
|
1145
|
+
if (report.provenance) {
|
|
1146
|
+
console.log(`- Git commit: ${report.provenance.git_commit || 'unknown'}`);
|
|
1147
|
+
console.log(`- Git dirty: ${report.provenance.git_dirty ? 'yes' : 'no'}`);
|
|
1148
|
+
console.log(`- Dist freshness: ${report.freshness.dist}`);
|
|
1149
|
+
}
|
|
1150
|
+
if (report.tarball) {
|
|
1151
|
+
console.log(`- Tarball: ${report.tarball.tarball_name || report.tarball.status}`);
|
|
1152
|
+
if (report.tarball.file_count !== undefined) console.log(`- Tarball files: ${report.tarball.file_count}`);
|
|
1153
|
+
}
|
|
1065
1154
|
if (report.failed.length) {
|
|
1066
1155
|
console.log('- Failed:');
|
|
1067
1156
|
for (const id of report.failed) console.log(` - ${id}`);
|
|
1068
1157
|
}
|
|
1158
|
+
if (report.next_actions?.length) {
|
|
1159
|
+
console.log('- Next actions:');
|
|
1160
|
+
for (const action of report.next_actions) console.log(` - ${action.next_action}`);
|
|
1161
|
+
}
|
|
1069
1162
|
if (!report.ok) process.exitCode = 1;
|
|
1070
1163
|
}
|
|
1071
1164
|
|
|
@@ -1080,6 +1173,10 @@ function runReleaseReport() {
|
|
|
1080
1173
|
['dist_freshness', ['npm', ['run', 'dist:freshness']]]
|
|
1081
1174
|
].map(([id, command]) => runReleaseCheck(id, command));
|
|
1082
1175
|
const failed = checks.filter((check) => check.status !== 'passed').map((check) => check.id);
|
|
1176
|
+
const provenance = releaseProvenance(checks);
|
|
1177
|
+
const tarball = releaseTarballProvenance();
|
|
1178
|
+
const freshness = releaseFreshness(checks, provenance, tarball);
|
|
1179
|
+
const nextActions = releaseNextActions(checks, provenance, tarball);
|
|
1083
1180
|
return {
|
|
1084
1181
|
schema: 'yam.release-report.v1',
|
|
1085
1182
|
generated_at: startedAt,
|
|
@@ -1088,10 +1185,117 @@ function runReleaseReport() {
|
|
|
1088
1185
|
ok: failed.length === 0,
|
|
1089
1186
|
truth_status: failed.length === 0 ? 'verified' : 'blocked',
|
|
1090
1187
|
checks,
|
|
1091
|
-
failed
|
|
1188
|
+
failed,
|
|
1189
|
+
provenance,
|
|
1190
|
+
tarball,
|
|
1191
|
+
freshness,
|
|
1192
|
+
next_actions: nextActions
|
|
1092
1193
|
};
|
|
1093
1194
|
}
|
|
1094
1195
|
|
|
1196
|
+
function releaseProvenance(checks = []) {
|
|
1197
|
+
const gitCommit = runReadOnlyCommand('git', ['rev-parse', '--short', 'HEAD']);
|
|
1198
|
+
const gitStatus = runReadOnlyCommand('git', ['status', '--short']);
|
|
1199
|
+
const distChanged = runReadOnlyCommand('git', ['diff', '--name-only', '--', 'dist']);
|
|
1200
|
+
const packageFiles = checks.find((check) => check.id === 'package_boundary')?.status === 'passed' ? 'checked' : 'not-verified';
|
|
1201
|
+
return {
|
|
1202
|
+
schema: 'yam.release-provenance.v1',
|
|
1203
|
+
package_name: PACKAGE_JSON.name,
|
|
1204
|
+
version: VERSION,
|
|
1205
|
+
git_commit: gitCommit.trim(),
|
|
1206
|
+
git_dirty: Boolean(gitStatus.trim()),
|
|
1207
|
+
git_status_lines: gitStatus.trim().split(/\r?\n/).filter(Boolean).slice(0, 20),
|
|
1208
|
+
dist_changed_files: distChanged.trim().split(/\r?\n/).filter(Boolean).slice(0, 20),
|
|
1209
|
+
package_files: packageFiles,
|
|
1210
|
+
generated_at: new Date().toISOString(),
|
|
1211
|
+
truth_status: gitCommit.trim() && packageFiles === 'checked' ? 'partial' : 'assumed'
|
|
1212
|
+
};
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
function releaseTarballProvenance() {
|
|
1216
|
+
const result = spawnSync('npm', ['pack', '--json', '--dry-run', '--ignore-scripts'], {
|
|
1217
|
+
cwd: ROOT,
|
|
1218
|
+
encoding: 'utf8',
|
|
1219
|
+
env: { ...process.env, npm_config_cache: path.join(os.tmpdir(), 'yam-npm-cache') },
|
|
1220
|
+
timeout: 120000
|
|
1221
|
+
});
|
|
1222
|
+
if (result.status !== 0) {
|
|
1223
|
+
return {
|
|
1224
|
+
schema: 'yam.release-tarball-provenance.v1',
|
|
1225
|
+
status: 'blocked',
|
|
1226
|
+
reason: summarizeCheckOutput([result.stdout, result.stderr, result.error?.message].filter(Boolean).join('\n')),
|
|
1227
|
+
truth_status: 'blocked'
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
try {
|
|
1231
|
+
const data = JSON.parse(String(result.stdout || '[]'))[0] || {};
|
|
1232
|
+
return {
|
|
1233
|
+
schema: 'yam.release-tarball-provenance.v1',
|
|
1234
|
+
status: 'checked',
|
|
1235
|
+
tarball_name: String(data.filename || ''),
|
|
1236
|
+
package_size: Number(data.size || data.packageSize || 0),
|
|
1237
|
+
unpacked_size: Number(data.unpackedSize || 0),
|
|
1238
|
+
file_count: Array.isArray(data.files) ? data.files.length : Number(data.entryCount || 0),
|
|
1239
|
+
integrity: String(data.integrity || ''),
|
|
1240
|
+
shasum: String(data.shasum || ''),
|
|
1241
|
+
truth_status: data.filename ? 'verified' : 'partial'
|
|
1242
|
+
};
|
|
1243
|
+
} catch (error) {
|
|
1244
|
+
return {
|
|
1245
|
+
schema: 'yam.release-tarball-provenance.v1',
|
|
1246
|
+
status: 'blocked',
|
|
1247
|
+
reason: `could not parse npm pack output: ${errorMessage(error)}`,
|
|
1248
|
+
truth_status: 'blocked'
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
function releaseFreshness(checks = [], provenance: AnyRecord = {}, tarball: AnyRecord = {}) {
|
|
1254
|
+
const checkStatus = (id) => checks.find((check) => check.id === id)?.status || 'not-run';
|
|
1255
|
+
const dist = checkStatus('dist_freshness') === 'passed' && !provenance.dist_changed_files?.length ? 'fresh' : 'needs-check';
|
|
1256
|
+
const registry = checkStatus('registry_status') === 'passed' ? 'publishable-version' : 'not-verified';
|
|
1257
|
+
const boundary = checkStatus('package_boundary') === 'passed' ? 'checked' : 'not-verified';
|
|
1258
|
+
const smoke = checkStatus('cli_smoke') === 'passed' ? 'checked' : 'not-verified';
|
|
1259
|
+
const tarballStatus = tarball.status === 'checked' ? 'checked' : 'not-verified';
|
|
1260
|
+
return {
|
|
1261
|
+
schema: 'yam.release-freshness.v1',
|
|
1262
|
+
dist,
|
|
1263
|
+
registry,
|
|
1264
|
+
package_boundary: boundary,
|
|
1265
|
+
cli_smoke: smoke,
|
|
1266
|
+
tarball: tarballStatus,
|
|
1267
|
+
truth_status: [dist, registry, boundary, smoke, tarballStatus].every((value) => !/not-verified|needs-check/.test(value)) ? 'verified' : 'partial'
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
function releaseNextActions(checks = [], provenance: AnyRecord = {}, tarball: AnyRecord = {}) {
|
|
1272
|
+
const actions = [];
|
|
1273
|
+
for (const check of checks) {
|
|
1274
|
+
if (check.status === 'passed') continue;
|
|
1275
|
+
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));
|
|
1276
|
+
}
|
|
1277
|
+
if (provenance.git_dirty) {
|
|
1278
|
+
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'));
|
|
1279
|
+
}
|
|
1280
|
+
if (provenance.dist_changed_files?.length) {
|
|
1281
|
+
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'));
|
|
1282
|
+
}
|
|
1283
|
+
if (tarball.status !== 'checked') {
|
|
1284
|
+
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'));
|
|
1285
|
+
}
|
|
1286
|
+
return uniqueNextActionDetails(actions);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function runReadOnlyCommand(command, args = []) {
|
|
1290
|
+
const result = spawnSync(command, args, {
|
|
1291
|
+
cwd: ROOT,
|
|
1292
|
+
encoding: 'utf8',
|
|
1293
|
+
timeout: 30000
|
|
1294
|
+
});
|
|
1295
|
+
if (result.status !== 0) return '';
|
|
1296
|
+
return String(result.stdout || '');
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1095
1299
|
function runReleaseCheck(id, commandSpec) {
|
|
1096
1300
|
const [command, args] = commandSpec;
|
|
1097
1301
|
const start = Date.now();
|
|
@@ -1145,18 +1349,18 @@ Opt-in visual evidence helpers. Ueye stays one skill: fast by default, capture/c
|
|
|
1145
1349
|
Usage:
|
|
1146
1350
|
yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
|
|
1147
1351
|
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]
|
|
1352
|
+
yam ueye report [--reference ref.png] [--actual screenshot.png] [--review-session-id id] [--provider-context local] [--execution-surface in-app-browser] [--app-surface codex-app] [--browser-surface in-app-browser] [--control-mode manual|automated] [--preserved-state] [--preserved-url URL] [--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
1353
|
|
|
1150
1354
|
Notes:
|
|
1151
1355
|
capture uses a locally available Playwright install when present. It does not download browsers or install dependencies.
|
|
1152
1356
|
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.
|
|
1357
|
+
report produces a proof-ready Ueye visual run report and continuity/comparison record without requiring a new capture.
|
|
1154
1358
|
`);
|
|
1155
1359
|
}
|
|
1156
1360
|
|
|
1157
1361
|
async function ueyeCapture(args = []) {
|
|
1158
1362
|
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return ueyeUsage();
|
|
1159
|
-
const flags = parseSimpleFlags(args, new Set(['url', 'out', 'viewport', 'wait-until', 'timeout', 'full-page', 'json']));
|
|
1363
|
+
const flags = parseSimpleFlags(args, new Set(['url', 'out', 'viewport', 'wait-until', 'timeout', 'full-page', 'provider-context', 'provider-badge', 'execution-surface', 'app-surface', 'browser-surface', 'control-mode', 'preserved-state', 'preserved-url', 'evidence-id', 'screenshot-id', 'json']));
|
|
1160
1364
|
const url = String(flags.url || '');
|
|
1161
1365
|
const out = String(flags.out || '');
|
|
1162
1366
|
if (!url || !out) {
|
|
@@ -1181,6 +1385,24 @@ async function ueyeCapture(args = []) {
|
|
|
1181
1385
|
await page.goto(url, { waitUntil, timeout });
|
|
1182
1386
|
await page.screenshot({ path: target, fullPage: Boolean(flags.full_page) });
|
|
1183
1387
|
const info = await imageFileInfo(target);
|
|
1388
|
+
const surfaceContext = buildUeyeSurfaceContext({
|
|
1389
|
+
provider_context: String(flags.provider_context || 'local-playwright'),
|
|
1390
|
+
provider_badge: String(flags.provider_badge || flags.provider_context || 'local-playwright'),
|
|
1391
|
+
execution_surface: String(flags.execution_surface || 'browser-capture'),
|
|
1392
|
+
app_surface: String(flags.app_surface || 'local-project'),
|
|
1393
|
+
browser_surface: String(flags.browser_surface || 'playwright'),
|
|
1394
|
+
control_mode: String(flags.control_mode || 'automated-capture'),
|
|
1395
|
+
route: 'ueye',
|
|
1396
|
+
mode: 'capture',
|
|
1397
|
+
url,
|
|
1398
|
+
viewport: `${viewport.width}x${viewport.height}`,
|
|
1399
|
+
screenshot_id: String(flags.screenshot_id || path.basename(target)),
|
|
1400
|
+
evidence_id: String(flags.evidence_id || `ueye-capture-${timestampId()}`),
|
|
1401
|
+
preserved_state: Boolean(flags.preserved_state),
|
|
1402
|
+
preserved_url: String(flags.preserved_url || url),
|
|
1403
|
+
local_only: true,
|
|
1404
|
+
truth_status: 'verified'
|
|
1405
|
+
});
|
|
1184
1406
|
const result = {
|
|
1185
1407
|
schema: 'yam.ueye-capture.v1',
|
|
1186
1408
|
status: 'verified',
|
|
@@ -1190,6 +1412,7 @@ async function ueyeCapture(args = []) {
|
|
|
1190
1412
|
full_page: Boolean(flags.full_page),
|
|
1191
1413
|
sha256: info.sha256,
|
|
1192
1414
|
dimensions: info.dimensions,
|
|
1415
|
+
surface_context: surfaceContext,
|
|
1193
1416
|
visual_evidence: `browser screenshot captured: ${target} (${info.dimensions}, sha256:${info.sha256})`
|
|
1194
1417
|
};
|
|
1195
1418
|
printJsonOrHuman(result, Boolean(flags.json), 'Ueye capture');
|
|
@@ -1212,7 +1435,7 @@ async function ueyeCapture(args = []) {
|
|
|
1212
1435
|
|
|
1213
1436
|
async function ueyeCompare(args = []) {
|
|
1214
1437
|
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return ueyeUsage();
|
|
1215
|
-
const flags = parseSimpleFlags(args, new Set(['reference', 'ref', 'actual', 'screenshot', 'json', 'reference-id', 'screenshot-id']));
|
|
1438
|
+
const flags = parseSimpleFlags(args, new Set(['reference', 'ref', 'actual', 'screenshot', 'json', 'reference-id', 'screenshot-id', 'provider-context', 'provider-badge', 'execution-surface', 'app-surface', 'browser-surface', 'control-mode', 'preserved-state', 'preserved-url', 'url', 'viewport', 'evidence-id']));
|
|
1216
1439
|
const reference = String(flags.reference || flags.ref || '');
|
|
1217
1440
|
const actual = String(flags.actual || flags.screenshot || '');
|
|
1218
1441
|
if (!reference || !actual) {
|
|
@@ -1233,12 +1456,35 @@ async function ueyeCompare(args = []) {
|
|
|
1233
1456
|
const sameDimensions = referenceInfo.dimensions === actualInfo.dimensions;
|
|
1234
1457
|
const comparisonResult = exactMatch ? 'matched' : 'different';
|
|
1235
1458
|
const truthStatus = exactMatch ? 'verified' : 'partial';
|
|
1459
|
+
const surfaceContext = buildUeyeSurfaceContext({
|
|
1460
|
+
provider_context: String(flags.provider_context || 'local-file'),
|
|
1461
|
+
provider_badge: String(flags.provider_badge || flags.provider_context || 'local-file'),
|
|
1462
|
+
execution_surface: String(flags.execution_surface || 'local-file-compare'),
|
|
1463
|
+
app_surface: String(flags.app_surface || 'local-project'),
|
|
1464
|
+
browser_surface: String(flags.browser_surface || 'not-used'),
|
|
1465
|
+
control_mode: String(flags.control_mode || 'local-compare'),
|
|
1466
|
+
route: 'ueye',
|
|
1467
|
+
mode: 'compare',
|
|
1468
|
+
url: String(flags.url || ''),
|
|
1469
|
+
viewport: String(flags.viewport || ''),
|
|
1470
|
+
screenshot_id: String(flags.screenshot_id || path.basename(actualPath)),
|
|
1471
|
+
evidence_id: String(flags.evidence_id || `ueye-compare-${timestampId()}`),
|
|
1472
|
+
preserved_state: Boolean(flags.preserved_state),
|
|
1473
|
+
preserved_url: String(flags.preserved_url || flags.url || ''),
|
|
1474
|
+
local_only: true,
|
|
1475
|
+
truth_status: truthStatus
|
|
1476
|
+
});
|
|
1236
1477
|
const provenance = buildUeyeVisualProvenance({
|
|
1237
1478
|
source_kind: 'implementation_screenshot',
|
|
1238
1479
|
source_path: actualPath,
|
|
1239
1480
|
source_hash: actualInfo.sha256,
|
|
1240
1481
|
reference_id: String(flags.reference_id || path.basename(referencePath)),
|
|
1241
1482
|
screenshot_id: String(flags.screenshot_id || path.basename(actualPath)),
|
|
1483
|
+
provider_context: surfaceContext.provider_context,
|
|
1484
|
+
provider_badge: surfaceContext.provider_badge,
|
|
1485
|
+
execution_surface: surfaceContext.execution_surface,
|
|
1486
|
+
app_surface: surfaceContext.app_surface,
|
|
1487
|
+
browser_surface: surfaceContext.browser_surface,
|
|
1242
1488
|
local_only: true,
|
|
1243
1489
|
redacted: false,
|
|
1244
1490
|
operator_provided: false,
|
|
@@ -1253,6 +1499,7 @@ async function ueyeCompare(args = []) {
|
|
|
1253
1499
|
same_dimensions: sameDimensions,
|
|
1254
1500
|
reference: referenceInfo,
|
|
1255
1501
|
actual: actualInfo,
|
|
1502
|
+
surface_context: surfaceContext,
|
|
1256
1503
|
visual_evidence: `browser/local screenshot comparison executed: reference=${referencePath}, actual=${actualPath}, result=${comparisonResult}`,
|
|
1257
1504
|
visual_provenance: provenance,
|
|
1258
1505
|
proof_hint: `yam proof --route ueye --truth ${truthStatus} --visual "browser/local screenshot comparison executed: ${comparisonResult}" --visual-provenance '${JSON.stringify(provenance)}' --require-visual`
|
|
@@ -1273,13 +1520,33 @@ async function ueyeCompare(args = []) {
|
|
|
1273
1520
|
|
|
1274
1521
|
async function ueyeReport(args = []) {
|
|
1275
1522
|
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']));
|
|
1523
|
+
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', 'provider-context', 'provider-badge', 'execution-surface', 'app-surface', 'browser-surface', 'control-mode', 'preserved-state', 'preserved-url', 'url', 'evidence-id', 'json']));
|
|
1277
1524
|
const reference = String(flags.reference || flags.ref || '');
|
|
1278
1525
|
const actual = String(flags.actual || flags.screenshot || '');
|
|
1279
1526
|
const referenceSources = [];
|
|
1280
1527
|
const implementationSources = [];
|
|
1281
1528
|
let comparisonResult = 'not-verified';
|
|
1282
1529
|
let blockedReason = String(flags.blocked_reason || '');
|
|
1530
|
+
const reviewSessionId = String(flags.review_session_id || `ueye-${timestampId()}`);
|
|
1531
|
+
const previousReport = await readPreviousUeyeReport(flags.previous_report);
|
|
1532
|
+
const surfaceContext = buildUeyeSurfaceContext({
|
|
1533
|
+
provider_context: String(flags.provider_context || 'not-recorded'),
|
|
1534
|
+
provider_badge: String(flags.provider_badge || flags.provider_context || 'not-recorded'),
|
|
1535
|
+
execution_surface: String(flags.execution_surface || flags.capture_backend || 'not-recorded'),
|
|
1536
|
+
app_surface: String(flags.app_surface || 'not-recorded'),
|
|
1537
|
+
browser_surface: String(flags.browser_surface || flags.capture_backend || 'not-recorded'),
|
|
1538
|
+
control_mode: String(flags.control_mode || 'not-recorded'),
|
|
1539
|
+
route: 'ueye',
|
|
1540
|
+
mode: 'report',
|
|
1541
|
+
url: String(flags.url || flags.preserved_url || ''),
|
|
1542
|
+
viewport: String(flags.viewport || ''),
|
|
1543
|
+
screenshot_id: String(flags.current_screenshot_id || flags.screenshot_id || ''),
|
|
1544
|
+
evidence_id: String(flags.evidence_id || reviewSessionId),
|
|
1545
|
+
preserved_state: Boolean(flags.preserved_state),
|
|
1546
|
+
preserved_url: String(flags.preserved_url || flags.url || ''),
|
|
1547
|
+
local_only: true,
|
|
1548
|
+
truth_status: actual ? 'partial' : 'assumed'
|
|
1549
|
+
});
|
|
1283
1550
|
|
|
1284
1551
|
try {
|
|
1285
1552
|
if (reference) {
|
|
@@ -1288,7 +1555,12 @@ async function ueyeReport(args = []) {
|
|
|
1288
1555
|
source_kind: 'reference_image',
|
|
1289
1556
|
source_path: info.path,
|
|
1290
1557
|
source_hash: info.sha256,
|
|
1291
|
-
reference_id: path.basename(info.path),
|
|
1558
|
+
reference_id: String(flags.reference_id || path.basename(info.path)),
|
|
1559
|
+
provider_context: surfaceContext.provider_context,
|
|
1560
|
+
provider_badge: surfaceContext.provider_badge,
|
|
1561
|
+
execution_surface: surfaceContext.execution_surface,
|
|
1562
|
+
app_surface: surfaceContext.app_surface,
|
|
1563
|
+
browser_surface: surfaceContext.browser_surface,
|
|
1292
1564
|
local_only: true,
|
|
1293
1565
|
operator_provided: true,
|
|
1294
1566
|
comparison_result: 'reference-recorded',
|
|
@@ -1301,7 +1573,13 @@ async function ueyeReport(args = []) {
|
|
|
1301
1573
|
source_kind: 'implementation_screenshot',
|
|
1302
1574
|
source_path: info.path,
|
|
1303
1575
|
source_hash: info.sha256,
|
|
1304
|
-
|
|
1576
|
+
reference_id: String(flags.reference_id || referenceSources[0]?.reference_id || ''),
|
|
1577
|
+
screenshot_id: String(flags.current_screenshot_id || flags.screenshot_id || path.basename(info.path)),
|
|
1578
|
+
provider_context: surfaceContext.provider_context,
|
|
1579
|
+
provider_badge: surfaceContext.provider_badge,
|
|
1580
|
+
execution_surface: surfaceContext.execution_surface,
|
|
1581
|
+
app_surface: surfaceContext.app_surface,
|
|
1582
|
+
browser_surface: surfaceContext.browser_surface,
|
|
1305
1583
|
local_only: true,
|
|
1306
1584
|
operator_provided: false,
|
|
1307
1585
|
comparison_result: 'implementation-recorded',
|
|
@@ -1320,20 +1598,140 @@ async function ueyeReport(args = []) {
|
|
|
1320
1598
|
const report = buildUeyeRunReport({
|
|
1321
1599
|
reference_sources: referenceSources,
|
|
1322
1600
|
implementation_sources: implementationSources,
|
|
1601
|
+
surface_context: {
|
|
1602
|
+
...surfaceContext,
|
|
1603
|
+
screenshot_id: surfaceContext.screenshot_id || String(flags.current_screenshot_id || flags.screenshot_id || implementationSources[0]?.screenshot_id || ''),
|
|
1604
|
+
truth_status: blockedReason ? 'blocked' : implementationSources.length ? 'partial' : 'assumed'
|
|
1605
|
+
},
|
|
1323
1606
|
comparison_result: comparisonResult,
|
|
1324
1607
|
design_quality: String(flags.design_quality || 'not-checked'),
|
|
1325
1608
|
blocked_reason: blockedReason,
|
|
1326
|
-
next_action: String(flags.next_action || '')
|
|
1609
|
+
next_action: String(flags.next_visual_action || flags.next_action || '')
|
|
1610
|
+
});
|
|
1611
|
+
const comparisonReport = buildUeyeComparisonReport({
|
|
1612
|
+
reviewSessionId,
|
|
1613
|
+
previousReport,
|
|
1614
|
+
referenceSources,
|
|
1615
|
+
implementationSources,
|
|
1616
|
+
surfaceContext: report.surface_context,
|
|
1617
|
+
comparisonResult,
|
|
1618
|
+
designQuality: String(flags.design_quality || 'not-checked'),
|
|
1619
|
+
similar: arrayFlag(flags.similar),
|
|
1620
|
+
different: arrayFlag(flags.different),
|
|
1621
|
+
missing: arrayFlag(flags.missing),
|
|
1622
|
+
notes: arrayFlag(flags.comparison_notes),
|
|
1623
|
+
resolved: arrayFlag(flags.resolved),
|
|
1624
|
+
newFindings: arrayFlag(flags.new_finding),
|
|
1625
|
+
stillOpen: arrayFlag(flags.still_open),
|
|
1626
|
+
regressions: arrayFlag(flags.regression),
|
|
1627
|
+
previousScreenshotId: String(flags.previous_screenshot_id || previousReport?.screenshot_id || ''),
|
|
1628
|
+
currentScreenshotId: String(flags.current_screenshot_id || flags.screenshot_id || implementationSources[0]?.screenshot_id || ''),
|
|
1629
|
+
viewport: String(flags.viewport || ''),
|
|
1630
|
+
state: String(flags.state || 'unknown'),
|
|
1631
|
+
nextAction: report.next_action,
|
|
1632
|
+
truthStatus: report.truth_status
|
|
1327
1633
|
});
|
|
1328
1634
|
const result = {
|
|
1329
1635
|
...report,
|
|
1636
|
+
review_session_id: reviewSessionId,
|
|
1637
|
+
tool_intent: 'visual',
|
|
1330
1638
|
capture_backend: String(flags.capture_backend || 'not-recorded'),
|
|
1331
|
-
compare_backend: String(flags.compare_backend || 'local-file-hash')
|
|
1639
|
+
compare_backend: String(flags.compare_backend || 'local-file-hash'),
|
|
1640
|
+
continuity: comparisonReport.continuity,
|
|
1641
|
+
comparison_report: comparisonReport
|
|
1332
1642
|
};
|
|
1333
1643
|
printJsonOrHuman(result, Boolean(flags.json), 'Ueye report');
|
|
1334
1644
|
if (report.truth_status === 'blocked') process.exitCode = 1;
|
|
1335
1645
|
}
|
|
1336
1646
|
|
|
1647
|
+
async function readPreviousUeyeReport(file = '') {
|
|
1648
|
+
const target = String(file || '');
|
|
1649
|
+
if (!target) return null;
|
|
1650
|
+
try {
|
|
1651
|
+
const data = await readJson(path.resolve(expandHome(target)));
|
|
1652
|
+
return {
|
|
1653
|
+
path: path.resolve(expandHome(target)),
|
|
1654
|
+
review_session_id: data.review_session_id || data.comparison_report?.review_session_id || '',
|
|
1655
|
+
screenshot_id: data.comparison_report?.current_screenshot_id || data.current_screenshot_id || data.screenshot_id || '',
|
|
1656
|
+
truth_status: data.truth_status || data.comparison_report?.truth_status || 'partial',
|
|
1657
|
+
comparison_result: data.comparison_result || data.comparison_report?.comparison_result || 'not-verified'
|
|
1658
|
+
};
|
|
1659
|
+
} catch (error) {
|
|
1660
|
+
return {
|
|
1661
|
+
path: path.resolve(expandHome(target)),
|
|
1662
|
+
blocked_reason: errorMessage(error),
|
|
1663
|
+
truth_status: 'blocked',
|
|
1664
|
+
comparison_result: 'not-verified'
|
|
1665
|
+
};
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
function buildUeyeComparisonReport({
|
|
1670
|
+
reviewSessionId,
|
|
1671
|
+
previousReport,
|
|
1672
|
+
referenceSources,
|
|
1673
|
+
implementationSources,
|
|
1674
|
+
surfaceContext,
|
|
1675
|
+
comparisonResult,
|
|
1676
|
+
designQuality,
|
|
1677
|
+
similar,
|
|
1678
|
+
different,
|
|
1679
|
+
missing,
|
|
1680
|
+
notes,
|
|
1681
|
+
resolved,
|
|
1682
|
+
newFindings,
|
|
1683
|
+
stillOpen,
|
|
1684
|
+
regressions,
|
|
1685
|
+
previousScreenshotId,
|
|
1686
|
+
currentScreenshotId,
|
|
1687
|
+
viewport,
|
|
1688
|
+
state,
|
|
1689
|
+
nextAction,
|
|
1690
|
+
truthStatus
|
|
1691
|
+
}) {
|
|
1692
|
+
const hasReference = referenceSources.length > 0;
|
|
1693
|
+
const hasImplementation = implementationSources.length > 0;
|
|
1694
|
+
const continuityTruth = previousReport?.blocked_reason ? 'blocked' : previousReport ? 'partial' : 'assumed';
|
|
1695
|
+
return {
|
|
1696
|
+
schema: 'yam.ueye-comparison-report.v1',
|
|
1697
|
+
review_session_id: reviewSessionId,
|
|
1698
|
+
previous_report: previousReport,
|
|
1699
|
+
continuity: {
|
|
1700
|
+
schema: 'yam.ueye-review-continuity.v1',
|
|
1701
|
+
current_review_session_id: reviewSessionId,
|
|
1702
|
+
previous_review_session_id: previousReport?.review_session_id || '',
|
|
1703
|
+
previous_report_path: previousReport?.path || '',
|
|
1704
|
+
previous_screenshot_id: previousScreenshotId || previousReport?.screenshot_id || '',
|
|
1705
|
+
current_screenshot_id: currentScreenshotId || '',
|
|
1706
|
+
carries_forward: Boolean(previousReport && !previousReport.blocked_reason),
|
|
1707
|
+
truth_status: continuityTruth
|
|
1708
|
+
},
|
|
1709
|
+
evidence_summary: {
|
|
1710
|
+
has_reference: hasReference,
|
|
1711
|
+
has_implementation_screenshot: hasImplementation,
|
|
1712
|
+
reference_count: referenceSources.length,
|
|
1713
|
+
implementation_count: implementationSources.length
|
|
1714
|
+
},
|
|
1715
|
+
surface_context: surfaceContext || buildUeyeSurfaceContext(),
|
|
1716
|
+
comparison_result: comparisonResult,
|
|
1717
|
+
design_quality: designQuality,
|
|
1718
|
+
similar,
|
|
1719
|
+
different,
|
|
1720
|
+
missing,
|
|
1721
|
+
resolved,
|
|
1722
|
+
new_findings: newFindings,
|
|
1723
|
+
still_open: stillOpen,
|
|
1724
|
+
regressions,
|
|
1725
|
+
previous_screenshot_id: previousScreenshotId || previousReport?.screenshot_id || '',
|
|
1726
|
+
current_screenshot_id: currentScreenshotId || '',
|
|
1727
|
+
viewport,
|
|
1728
|
+
state,
|
|
1729
|
+
notes,
|
|
1730
|
+
next_action: nextAction || (truthStatus === 'verified' ? 'no action required' : 'capture or provide implementation screenshot, then compare again'),
|
|
1731
|
+
truth_status: truthStatus
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1337
1735
|
async function media(args = []) {
|
|
1338
1736
|
const subcommand = args[0] || 'help';
|
|
1339
1737
|
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return mediaUsage();
|
|
@@ -1383,6 +1781,227 @@ async function mediaProof(args = []) {
|
|
|
1383
1781
|
if (proof.truth_status === 'blocked') process.exitCode = 1;
|
|
1384
1782
|
}
|
|
1385
1783
|
|
|
1784
|
+
async function runtime(args = []) {
|
|
1785
|
+
const subcommand = args[0] || 'help';
|
|
1786
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return runtimeUsage();
|
|
1787
|
+
if (subcommand === 'evidence') return runtimeEvidence(args.slice(1));
|
|
1788
|
+
console.error(`unknown runtime command: ${subcommand}`);
|
|
1789
|
+
return runtimeUsage();
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
function runtimeUsage() {
|
|
1793
|
+
console.log(`yam runtime
|
|
1794
|
+
|
|
1795
|
+
Usage:
|
|
1796
|
+
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]
|
|
1797
|
+
|
|
1798
|
+
Notes:
|
|
1799
|
+
Records a small runtime evidence shape. It does not start, stop, or inspect processes by itself.
|
|
1800
|
+
`);
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
async function runtimeEvidence(args = []) {
|
|
1804
|
+
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return runtimeUsage();
|
|
1805
|
+
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']));
|
|
1806
|
+
const evidence = buildRuntimeBackendEvidence({
|
|
1807
|
+
backend: flags.backend,
|
|
1808
|
+
claim: flags.claim,
|
|
1809
|
+
evidence_id: String(flags.evidence_id || `runtime-${timestampId()}`),
|
|
1810
|
+
command: String(flags.command || ''),
|
|
1811
|
+
cleanup_checked: Boolean(flags.cleanup_checked),
|
|
1812
|
+
note: String(flags.note || ''),
|
|
1813
|
+
truth_status: isTruthStatus(flags.truth) ? flags.truth : undefined
|
|
1814
|
+
});
|
|
1815
|
+
const runtimeDetails = runtimeEvidenceDetails(flags);
|
|
1816
|
+
if (!isTruthStatus(flags.truth) && evidence.truth_status === 'verified' && !runtimeDetailsHasVerification(runtimeDetails)) {
|
|
1817
|
+
evidence.truth_status = 'partial';
|
|
1818
|
+
}
|
|
1819
|
+
const result = {
|
|
1820
|
+
...evidence,
|
|
1821
|
+
runtime_details: runtimeDetails,
|
|
1822
|
+
tool_intent: normalizeToolIntent(flags.intent || 'runtime'),
|
|
1823
|
+
next_action: runtimeEvidenceNextAction(evidence)
|
|
1824
|
+
};
|
|
1825
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Runtime evidence');
|
|
1826
|
+
if (result.truth_status === 'blocked' || result.truth_status === 'real_required_missing') process.exitCode = 1;
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
function runtimeEvidenceDetails(flags: AnyRecord = {}) {
|
|
1830
|
+
return {
|
|
1831
|
+
schema: 'yam.runtime-details.v1',
|
|
1832
|
+
pid: numberOrNull(flags.pid),
|
|
1833
|
+
port: numberOrNull(flags.port),
|
|
1834
|
+
url: String(flags.url || ''),
|
|
1835
|
+
exit_code: numberOrNull(flags.exit_code),
|
|
1836
|
+
screenshot_id: String(flags.screenshot_id || ''),
|
|
1837
|
+
started_at: String(flags.started_at || ''),
|
|
1838
|
+
stopped_at: String(flags.stopped_at || '')
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
function runtimeDetailsHasVerification(details: AnyRecord = {}) {
|
|
1843
|
+
return Boolean(details.url || details.screenshot_id || details.pid !== null || details.port !== null || details.exit_code !== null || details.started_at || details.stopped_at);
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
function runtimeEvidenceNextAction(evidence) {
|
|
1847
|
+
if (evidence.truth_status === 'proven' || evidence.truth_status === 'verified') return 'attach this evidence to `yam proof` when making a runtime claim';
|
|
1848
|
+
if (evidence.claim === 'cleanup_verified' && !evidence.cleanup_checked) return 'rerun with --cleanup-checked only after process exit or intentional persistence is confirmed';
|
|
1849
|
+
if (evidence.backend === 'none' || evidence.backend === 'unknown') return 'record the actual runtime backend before claiming runtime verification';
|
|
1850
|
+
return 'add command output, screenshot id, pid/session id, or cleanup proof before upgrading the claim';
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
async function mission(args = []) {
|
|
1854
|
+
const subcommand = args[0] || 'help';
|
|
1855
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return missionUsage();
|
|
1856
|
+
if (subcommand === 'queue' || subcommand === 'patch') return missionQueue(args.slice(1));
|
|
1857
|
+
console.error(`unknown mission command: ${subcommand}`);
|
|
1858
|
+
return missionUsage();
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
function missionUsage() {
|
|
1862
|
+
console.log(`yam mission
|
|
1863
|
+
|
|
1864
|
+
Usage:
|
|
1865
|
+
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]
|
|
1866
|
+
|
|
1867
|
+
Notes:
|
|
1868
|
+
Produces a patch queue item for mission handoff/review. It does not run workers; persistence is opt-in with --out.
|
|
1869
|
+
`);
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
async function missionQueue(args = []) {
|
|
1873
|
+
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return missionUsage();
|
|
1874
|
+
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']));
|
|
1875
|
+
const changedFiles = arrayFlag(flags.changed);
|
|
1876
|
+
const generatedFiles = arrayFlag(flags.generated);
|
|
1877
|
+
const laneId = String(flags.lane_id || flags.agent_id || `lane-${timestampId()}`);
|
|
1878
|
+
const status = normalizeMissionQueueStatus(flags.status || 'pending');
|
|
1879
|
+
const rollback = buildRollbackHint({
|
|
1880
|
+
touched_files: changedFiles,
|
|
1881
|
+
generated_files: generatedFiles,
|
|
1882
|
+
before_check: String(flags.before_check || ''),
|
|
1883
|
+
safe_revert_note: String(flags.safe_revert_note || flags.rollback_hint || '')
|
|
1884
|
+
});
|
|
1885
|
+
const envelope = buildMissionPatchEnvelope({
|
|
1886
|
+
agent_id: String(flags.agent_id || `agent-${timestampId()}`),
|
|
1887
|
+
assigned_scope: String(flags.scope || ''),
|
|
1888
|
+
changed_files: changedFiles,
|
|
1889
|
+
verification_hint: String(flags.verification_hint || ''),
|
|
1890
|
+
rollback_hint: rollback,
|
|
1891
|
+
truth_status: isTruthStatus(flags.truth) ? flags.truth : changedFiles.length ? 'partial' : 'assumed'
|
|
1892
|
+
});
|
|
1893
|
+
const item = {
|
|
1894
|
+
schema: 'yam.mission-patch-queue-item.v1',
|
|
1895
|
+
lane_id: laneId,
|
|
1896
|
+
status,
|
|
1897
|
+
depends_on: arrayFlag(flags.depends_on),
|
|
1898
|
+
tool_intent: normalizeToolIntent(flags.intent || (changedFiles.length || generatedFiles.length ? 'write' : 'read_only')),
|
|
1899
|
+
patch_envelope: envelope,
|
|
1900
|
+
next_action: missionQueueNextAction(envelope, status)
|
|
1901
|
+
};
|
|
1902
|
+
const result = {
|
|
1903
|
+
schema: 'yam.mission-patch-queue-lite.v1',
|
|
1904
|
+
generated_at: new Date().toISOString(),
|
|
1905
|
+
persistence: flags.out ? 'written' : 'not-persisted',
|
|
1906
|
+
out: flags.out ? path.resolve(expandHome(flags.out)) : '',
|
|
1907
|
+
items: [item],
|
|
1908
|
+
queue_depth: 1,
|
|
1909
|
+
next_action: item.next_action,
|
|
1910
|
+
truth_status: envelope.truth_status
|
|
1911
|
+
};
|
|
1912
|
+
if (flags.out) {
|
|
1913
|
+
const target = path.resolve(expandHome(flags.out));
|
|
1914
|
+
await fsp.mkdir(path.dirname(target), { recursive: true });
|
|
1915
|
+
await fsp.writeFile(target, `${JSON.stringify(result, null, 2)}\n`);
|
|
1916
|
+
}
|
|
1917
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Mission patch queue');
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
function normalizeMissionQueueStatus(value = '') {
|
|
1921
|
+
const text = String(value || '').toLowerCase();
|
|
1922
|
+
if (['pending', 'applied', 'verified', 'blocked', 'reverted'].includes(text)) return text;
|
|
1923
|
+
return 'pending';
|
|
1924
|
+
}
|
|
1925
|
+
|
|
1926
|
+
function missionQueueNextAction(envelope, status = 'pending') {
|
|
1927
|
+
if (status === 'blocked') return 'resolve or document the blocker before applying dependent mission lanes';
|
|
1928
|
+
if (status === 'reverted') return 'rerun the before-check or verification hint before continuing dependent work';
|
|
1929
|
+
if (!envelope.assigned_scope) return 'add --scope so the patch can be reviewed against an assigned boundary';
|
|
1930
|
+
if (!envelope.changed_files.length) return 'add at least one --changed file when this represents code-changing work';
|
|
1931
|
+
if (!envelope.verification_hint) return 'add --verification-hint so reviewers know the smallest honest check';
|
|
1932
|
+
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';
|
|
1933
|
+
return 'attach this envelope to the mission proof summary and cross-check changed files';
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
async function benchmark(args = []) {
|
|
1937
|
+
const subcommand = args[0] || 'help';
|
|
1938
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return benchmarkUsage();
|
|
1939
|
+
if (subcommand === 'report') return benchmarkReport(args.slice(1));
|
|
1940
|
+
console.error(`unknown benchmark command: ${subcommand}`);
|
|
1941
|
+
return benchmarkUsage();
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
function benchmarkUsage() {
|
|
1945
|
+
console.log(`yam benchmark
|
|
1946
|
+
|
|
1947
|
+
Usage:
|
|
1948
|
+
yam benchmark report [--label text] [--baseline n] [--current n] [--unit ms] [--target lower|higher] [--next-action text] [--json]
|
|
1949
|
+
|
|
1950
|
+
Notes:
|
|
1951
|
+
Records a small optimization loop result. It does not run benchmarks by itself.
|
|
1952
|
+
`);
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
async function benchmarkReport(args = []) {
|
|
1956
|
+
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return benchmarkUsage();
|
|
1957
|
+
const flags = parseSimpleFlags(args, new Set(['label', 'baseline', 'current', 'unit', 'target', 'next-action', 'rollback-hint', 'intent', 'json']));
|
|
1958
|
+
const baseline = numberOrNull(flags.baseline);
|
|
1959
|
+
const current = numberOrNull(flags.current);
|
|
1960
|
+
const target = String(flags.target || 'lower').toLowerCase() === 'higher' ? 'higher' : 'lower';
|
|
1961
|
+
const hasNumbers = baseline !== null && current !== null;
|
|
1962
|
+
const delta = hasNumbers ? current - baseline : null;
|
|
1963
|
+
const percent = hasNumbers && baseline !== 0 ? (delta / baseline) * 100 : null;
|
|
1964
|
+
const status = benchmarkStatus({ baseline, current, target });
|
|
1965
|
+
const result = {
|
|
1966
|
+
schema: 'yam.benchmark-report.v1',
|
|
1967
|
+
generated_at: new Date().toISOString(),
|
|
1968
|
+
label: String(flags.label || 'unnamed'),
|
|
1969
|
+
baseline,
|
|
1970
|
+
current,
|
|
1971
|
+
unit: String(flags.unit || ''),
|
|
1972
|
+
target,
|
|
1973
|
+
delta,
|
|
1974
|
+
percent,
|
|
1975
|
+
status,
|
|
1976
|
+
tool_intent: normalizeToolIntent(flags.intent || 'read_only'),
|
|
1977
|
+
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')),
|
|
1978
|
+
next_action: String(flags.next_action || benchmarkNextAction(status)),
|
|
1979
|
+
truth_status: hasNumbers ? 'verified' : 'partial'
|
|
1980
|
+
};
|
|
1981
|
+
printJsonOrHuman(result, Boolean(flags.json), 'Benchmark report');
|
|
1982
|
+
if (status === 'regressed') process.exitCode = 1;
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
function numberOrNull(value) {
|
|
1986
|
+
if (value === undefined || value === null || value === '') return null;
|
|
1987
|
+
const number = Number(value);
|
|
1988
|
+
return Number.isFinite(number) ? number : null;
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
function benchmarkStatus({ baseline, current, target }) {
|
|
1992
|
+
if (baseline === null || current === null) return 'needs-baseline';
|
|
1993
|
+
if (current === baseline) return 'unchanged';
|
|
1994
|
+
if (target === 'higher') return current > baseline ? 'improved' : 'regressed';
|
|
1995
|
+
return current < baseline ? 'improved' : 'regressed';
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
function benchmarkNextAction(status) {
|
|
1999
|
+
if (status === 'improved') return 'record the measurement source and keep the smallest relevant regression check';
|
|
2000
|
+
if (status === 'regressed') return 'treat this as a fix-first item before continuing planned optimization';
|
|
2001
|
+
if (status === 'unchanged') return 'keep the change only if it improves clarity or enables the next measured step';
|
|
2002
|
+
return 'capture baseline and current values before claiming optimization impact';
|
|
2003
|
+
}
|
|
2004
|
+
|
|
1386
2005
|
function parseSimpleFlags(args = [], allowed = new Set<string>()) {
|
|
1387
2006
|
const flags: AnyRecord = {};
|
|
1388
2007
|
const positionals: string[] = [];
|
|
@@ -1402,7 +2021,11 @@ function parseSimpleFlags(args = [], allowed = new Set<string>()) {
|
|
|
1402
2021
|
}
|
|
1403
2022
|
const value = inlineValue ?? args[index + 1] ?? '';
|
|
1404
2023
|
if (inlineValue === undefined) index += 1;
|
|
1405
|
-
flags[normalizedKey]
|
|
2024
|
+
if (flags[normalizedKey] !== undefined) {
|
|
2025
|
+
flags[normalizedKey] = [...arrayFlag(flags[normalizedKey]), value];
|
|
2026
|
+
} else {
|
|
2027
|
+
flags[normalizedKey] = value;
|
|
2028
|
+
}
|
|
1406
2029
|
}
|
|
1407
2030
|
flags._ = positionals;
|
|
1408
2031
|
return flags;
|
|
@@ -2644,6 +3267,9 @@ async function main() {
|
|
|
2644
3267
|
if (command === 'proof') return proof(process.argv.slice(3));
|
|
2645
3268
|
if (command === 'ueye') return ueye(process.argv.slice(3));
|
|
2646
3269
|
if (command === 'media') return media(process.argv.slice(3));
|
|
3270
|
+
if (command === 'runtime') return runtime(process.argv.slice(3));
|
|
3271
|
+
if (command === 'mission') return mission(process.argv.slice(3));
|
|
3272
|
+
if (command === 'benchmark') return benchmark(process.argv.slice(3));
|
|
2647
3273
|
if (command === 'release') return release(process.argv.slice(3));
|
|
2648
3274
|
if (command === 'safety') return safety(process.argv.slice(3));
|
|
2649
3275
|
if (command === 'memory') return memory(process.argv.slice(3));
|