yam-flow 0.1.7 → 0.1.8
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 +27 -0
- package/COMMANDS.md +6 -1
- package/README.md +9 -1
- package/dist/bin/yam.js +272 -10
- package/dist/lib/trust-kernel.d.ts +42 -0
- package/dist/lib/trust-kernel.js +154 -0
- package/package.json +1 -1
- package/src/bin/yam.ts +283 -11
- package/src/lib/trust-kernel.ts +188 -0
- package/yam.manifest.json +3 -2
package/dist/lib/trust-kernel.js
CHANGED
|
@@ -199,6 +199,70 @@ export function buildRuntimeBackendEvidence(input = {}) {
|
|
|
199
199
|
})
|
|
200
200
|
};
|
|
201
201
|
}
|
|
202
|
+
export function buildStudyNote(input = {}) {
|
|
203
|
+
const problem = buildStudyNoteSection(input.problem, {
|
|
204
|
+
code: input.issue_code,
|
|
205
|
+
role: input.issue_role,
|
|
206
|
+
symptom: input.issue_symptom
|
|
207
|
+
}, 'problem');
|
|
208
|
+
const change = buildStudyNoteSection(input.change, {
|
|
209
|
+
code: input.changed_code,
|
|
210
|
+
role: input.changed_role,
|
|
211
|
+
summary: input.change_summary
|
|
212
|
+
}, 'change');
|
|
213
|
+
const whyItMatters = shortText(input.why_it_matters || input.why_important);
|
|
214
|
+
const learningNote = shortText(input.learning_note);
|
|
215
|
+
const limits = [
|
|
216
|
+
...asList(input.limits),
|
|
217
|
+
...missingStudyNoteLimits(problem, change, whyItMatters, learningNote)
|
|
218
|
+
];
|
|
219
|
+
const hasProblem = Boolean(problem.code || problem.role || problem.symptom);
|
|
220
|
+
const hasChange = Boolean(change.code || change.role || change.summary);
|
|
221
|
+
const truth = isTruthStatus(input.truth_status) ? input.truth_status
|
|
222
|
+
: hasProblem || hasChange || whyItMatters || learningNote ? 'partial'
|
|
223
|
+
: 'assumed';
|
|
224
|
+
return {
|
|
225
|
+
schema: 'yam.study-note.v1',
|
|
226
|
+
problem,
|
|
227
|
+
change,
|
|
228
|
+
why_it_matters: whyItMatters,
|
|
229
|
+
learning_note: learningNote,
|
|
230
|
+
limits: [...new Set(limits)],
|
|
231
|
+
truth_status: truth
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
export function buildLoopReport(input = {}) {
|
|
235
|
+
const stages = Array.isArray(input.stages)
|
|
236
|
+
? input.stages.map((stage) => buildLoopStage(stage))
|
|
237
|
+
: [];
|
|
238
|
+
const evidence = asList(input.evidence);
|
|
239
|
+
const blockers = asList(input.blockers || input.blocked);
|
|
240
|
+
const hasBlockedStage = stages.some((stage) => stage.status === 'blocked' || stage.status === 'failed');
|
|
241
|
+
const requestedTruth = isTruthStatus(input.truth_status) ? input.truth_status : '';
|
|
242
|
+
const derivedTruth = blockers.length || hasBlockedStage ? 'blocked'
|
|
243
|
+
: evidence.length && stages.some((stage) => stage.status === 'passed') ? 'verified'
|
|
244
|
+
: evidence.length || stages.length ? 'partial'
|
|
245
|
+
: 'assumed';
|
|
246
|
+
const truth = requestedTruth ? weakestTruth(requestedTruth, derivedTruth) : derivedTruth;
|
|
247
|
+
const studyNote = buildStudyNote(input.study_note && typeof input.study_note === 'object' ? input.study_note : input);
|
|
248
|
+
const intentLabel = normalizeToolIntentLabel(input.intent_label || input.tool_intent);
|
|
249
|
+
return {
|
|
250
|
+
schema: 'yam.loop-report.v1',
|
|
251
|
+
generated_at: String(input.generated_at || new Date().toISOString()),
|
|
252
|
+
route: String(input.route || ''),
|
|
253
|
+
intent: shortText(input.intent),
|
|
254
|
+
loop_kind: String(input.loop_kind || 'harness'),
|
|
255
|
+
stages,
|
|
256
|
+
evidence,
|
|
257
|
+
blockers,
|
|
258
|
+
truth_status: truth,
|
|
259
|
+
intent_label: intentLabel,
|
|
260
|
+
tool_intent: intentLabel,
|
|
261
|
+
next_action: shortText(input.next_action || (blockers[0] ? 'resolve the blocker before claiming this loop complete' : 'record the next smallest useful action')),
|
|
262
|
+
remaining_tasks: asList(input.remaining_tasks || input.remaining_task),
|
|
263
|
+
study_note: studyNote
|
|
264
|
+
};
|
|
265
|
+
}
|
|
202
266
|
export function buildUeyeRunReport(input = {}) {
|
|
203
267
|
const references = Array.isArray(input.reference_sources)
|
|
204
268
|
? input.reference_sources.map((item) => buildUeyeVisualProvenance(item))
|
|
@@ -240,6 +304,96 @@ export function buildUeyeRunReport(input = {}) {
|
|
|
240
304
|
truth_status: isTruthStatus(input.truth_status) ? input.truth_status : finalTruth
|
|
241
305
|
};
|
|
242
306
|
}
|
|
307
|
+
function buildStudyNoteSection(section, fallback, kind) {
|
|
308
|
+
const source = section && typeof section === 'object' ? section : fallback;
|
|
309
|
+
const code = shortText(source.code);
|
|
310
|
+
const role = shortText(source.role);
|
|
311
|
+
const symptom = kind === 'problem' ? shortText(source.symptom) : undefined;
|
|
312
|
+
const summary = kind === 'change' ? shortText(source.summary) : undefined;
|
|
313
|
+
return {
|
|
314
|
+
code,
|
|
315
|
+
role,
|
|
316
|
+
...(symptom !== undefined ? { symptom } : {}),
|
|
317
|
+
...(summary !== undefined ? { summary } : {}),
|
|
318
|
+
truth_status: code || role || symptom || summary ? 'partial' : 'assumed'
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
function missingStudyNoteLimits(problem, change, whyItMatters = '', learningNote = '') {
|
|
322
|
+
const limits = [];
|
|
323
|
+
if (!problem.code)
|
|
324
|
+
limits.push('issue_code not provided');
|
|
325
|
+
if (!problem.role)
|
|
326
|
+
limits.push('issue_role not provided');
|
|
327
|
+
if (!problem.symptom)
|
|
328
|
+
limits.push('issue_symptom not provided');
|
|
329
|
+
if (!change.code)
|
|
330
|
+
limits.push('changed_code not provided');
|
|
331
|
+
if (!change.role)
|
|
332
|
+
limits.push('changed_role not provided');
|
|
333
|
+
if (!change.summary)
|
|
334
|
+
limits.push('change_summary not provided');
|
|
335
|
+
if (!whyItMatters)
|
|
336
|
+
limits.push('why_it_matters not provided');
|
|
337
|
+
if (!learningNote)
|
|
338
|
+
limits.push('learning_note not provided');
|
|
339
|
+
return limits;
|
|
340
|
+
}
|
|
341
|
+
function buildLoopStage(value = '') {
|
|
342
|
+
if (value && typeof value === 'object') {
|
|
343
|
+
const item = value;
|
|
344
|
+
return loopStage(String(item.id || ''), item.status, String(item.note || ''));
|
|
345
|
+
}
|
|
346
|
+
const [id = '', rawStatus = '', ...noteParts] = String(value || '').split(':');
|
|
347
|
+
return loopStage(id, rawStatus, noteParts.join(':'));
|
|
348
|
+
}
|
|
349
|
+
function loopStage(id = '', rawStatus = '', note = '') {
|
|
350
|
+
const status = normalizeLoopStageStatus(rawStatus);
|
|
351
|
+
return {
|
|
352
|
+
id: shortText(id || 'stage'),
|
|
353
|
+
status,
|
|
354
|
+
note: shortText(note),
|
|
355
|
+
truth_status: stageTruth(status)
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
function normalizeLoopStageStatus(value = '') {
|
|
359
|
+
const text = String(value || '').toLowerCase().replace(/[-\s]/g, '_');
|
|
360
|
+
if (text === 'pass' || text === 'passed' || text === 'ok')
|
|
361
|
+
return 'passed';
|
|
362
|
+
if (text === 'fail' || text === 'failed' || text === 'error')
|
|
363
|
+
return 'failed';
|
|
364
|
+
if (text === 'block' || text === 'blocked')
|
|
365
|
+
return 'blocked';
|
|
366
|
+
if (text === 'skip' || text === 'skipped')
|
|
367
|
+
return 'skipped';
|
|
368
|
+
if (text === 'partial')
|
|
369
|
+
return 'partial';
|
|
370
|
+
if (text === 'pending')
|
|
371
|
+
return 'pending';
|
|
372
|
+
return 'recorded';
|
|
373
|
+
}
|
|
374
|
+
function stageTruth(status) {
|
|
375
|
+
if (status === 'passed')
|
|
376
|
+
return 'verified';
|
|
377
|
+
if (status === 'failed' || status === 'blocked')
|
|
378
|
+
return 'blocked';
|
|
379
|
+
if (status === 'skipped')
|
|
380
|
+
return 'skipped';
|
|
381
|
+
if (status === 'pending')
|
|
382
|
+
return 'assumed';
|
|
383
|
+
return 'partial';
|
|
384
|
+
}
|
|
385
|
+
function normalizeToolIntentLabel(value = '') {
|
|
386
|
+
const text = String(value || '').toLowerCase().replace(/[-\s]/g, '_');
|
|
387
|
+
if (['read_only', 'write', 'destructive', 'runtime', 'visual', 'publish'].includes(text))
|
|
388
|
+
return text;
|
|
389
|
+
return 'read_only';
|
|
390
|
+
}
|
|
391
|
+
function shortText(value = '', limit = 240) {
|
|
392
|
+
const text = String(value || '').trim().replace(/\s+/g, ' ');
|
|
393
|
+
if (text.length <= limit)
|
|
394
|
+
return text;
|
|
395
|
+
return `${text.slice(0, limit - 3)}...`;
|
|
396
|
+
}
|
|
243
397
|
export function buildMediaGenerationProof(input = {}) {
|
|
244
398
|
const requested = Boolean(input.generation_requested);
|
|
245
399
|
const attempted = Boolean(input.generation_attempted);
|
package/package.json
CHANGED
package/src/bin/yam.ts
CHANGED
|
@@ -10,10 +10,12 @@ import process from 'node:process';
|
|
|
10
10
|
import { fileURLToPath } from 'node:url';
|
|
11
11
|
import {
|
|
12
12
|
TRUTH_STATUSES,
|
|
13
|
+
buildLoopReport,
|
|
13
14
|
buildMediaGenerationProof,
|
|
14
15
|
buildMissionPatchEnvelope,
|
|
15
16
|
buildRollbackHint,
|
|
16
17
|
buildRuntimeBackendEvidence,
|
|
18
|
+
buildStudyNote,
|
|
17
19
|
buildUeyeDesignCompletionGate,
|
|
18
20
|
buildUeyeVisualProvenance,
|
|
19
21
|
buildUeyeRunReport,
|
|
@@ -22,6 +24,7 @@ import {
|
|
|
22
24
|
detectDbSafetyText as detectTrustDbSafetyText,
|
|
23
25
|
isTruthStatus
|
|
24
26
|
} from '../lib/trust-kernel.js';
|
|
27
|
+
import type { ToolIntent } from '../lib/trust-kernel.js';
|
|
25
28
|
|
|
26
29
|
type AnyRecord = Record<string, any>;
|
|
27
30
|
|
|
@@ -141,6 +144,7 @@ Usage:
|
|
|
141
144
|
yam tools doctor [dir]
|
|
142
145
|
yam proof [dir|--from file] [--route route] [--truth status] [--command text] [--evidence text]
|
|
143
146
|
yam proof write [dir] [--format json|md] [--out file] [--route route] [--truth status] [--command text]
|
|
147
|
+
yam loop report [--route route] [--intent text] [--stage id:status:note] [--evidence text] [--json]
|
|
144
148
|
yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
|
|
145
149
|
yam ueye compare --reference ref.png --actual screenshot.png [--json]
|
|
146
150
|
yam ueye preflight [dir] [--json]
|
|
@@ -506,6 +510,7 @@ function ownerRouteForAction(id = '', reason = '', command = '') {
|
|
|
506
510
|
function toolIntentForCommand(command = '') {
|
|
507
511
|
const text = String(command || '').toLowerCase();
|
|
508
512
|
if (!text) return 'read_only';
|
|
513
|
+
if (/(npm\s+(view|whoami|config\s+get|owner\s+ls)|registry:check)/i.test(text)) return 'read_only';
|
|
509
514
|
if (/(publish|registry|release|npm pack|npm publish)/i.test(text)) return 'publish';
|
|
510
515
|
if (/(rm |delete|drop|truncate|reset|migrate|push|deploy|write|commit|add |install)/i.test(text)) return 'destructive';
|
|
511
516
|
if (/(dev|server|tmux|playwright|browser|screenshot|runtime|port|pid)/i.test(text)) return 'runtime';
|
|
@@ -514,9 +519,9 @@ function toolIntentForCommand(command = '') {
|
|
|
514
519
|
return 'write';
|
|
515
520
|
}
|
|
516
521
|
|
|
517
|
-
function normalizeToolIntent(value = '') {
|
|
522
|
+
function normalizeToolIntent(value = ''): ToolIntent {
|
|
518
523
|
const text = String(value || '').toLowerCase().replace(/[-\s]/g, '_');
|
|
519
|
-
if (['read_only', 'write', 'destructive', 'runtime', 'visual', 'publish'].includes(text)) return text;
|
|
524
|
+
if (['read_only', 'write', 'destructive', 'runtime', 'visual', 'publish'].includes(text)) return text as ToolIntent;
|
|
520
525
|
return 'read_only';
|
|
521
526
|
}
|
|
522
527
|
|
|
@@ -1520,6 +1525,11 @@ async function release(args = []) {
|
|
|
1520
1525
|
console.log(`- Tarball: ${report.tarball.tarball_name || report.tarball.status}`);
|
|
1521
1526
|
if (report.tarball.file_count !== undefined) console.log(`- Tarball files: ${report.tarball.file_count}`);
|
|
1522
1527
|
}
|
|
1528
|
+
if (report.publish_readiness) {
|
|
1529
|
+
console.log(`- Publish readiness: ${report.publish_readiness.status}`);
|
|
1530
|
+
console.log(`- Registry: ${report.publish_readiness.registry.url || 'not-verified'}`);
|
|
1531
|
+
console.log(`- npm auth: ${report.publish_readiness.auth.status}`);
|
|
1532
|
+
}
|
|
1523
1533
|
if (report.failed.length) {
|
|
1524
1534
|
console.log('- Failed:');
|
|
1525
1535
|
for (const id of report.failed) console.log(` - ${id}`);
|
|
@@ -1535,6 +1545,9 @@ async function release(args = []) {
|
|
|
1535
1545
|
console.log('- Next actions:');
|
|
1536
1546
|
for (const action of report.next_actions) console.log(` - ${action.next_action}`);
|
|
1537
1547
|
}
|
|
1548
|
+
if (report.study_note?.problem?.summary || report.study_note?.problem?.symptom) {
|
|
1549
|
+
console.log(`- Study note: ${report.study_note.problem.summary || report.study_note.problem.symptom}`);
|
|
1550
|
+
}
|
|
1538
1551
|
if (!report.ok) process.exitCode = 1;
|
|
1539
1552
|
}
|
|
1540
1553
|
|
|
@@ -1553,19 +1566,23 @@ function runReleaseReport() {
|
|
|
1553
1566
|
const tarball = releaseTarballProvenance();
|
|
1554
1567
|
const freshness = releaseFreshness(checks, provenance, tarball);
|
|
1555
1568
|
const publishBlockerEvidence = publishBlockerEvidenceFromRelease(checks, tarball);
|
|
1556
|
-
const
|
|
1569
|
+
const publishReadiness = releasePublishReadiness(checks, provenance, tarball, publishBlockerEvidence);
|
|
1570
|
+
const nextActions = releaseNextActions(checks, provenance, tarball, publishBlockerEvidence, publishReadiness);
|
|
1571
|
+
const ok = failed.length === 0 && publishReadiness.status === 'ready';
|
|
1557
1572
|
return {
|
|
1558
1573
|
schema: 'yam.release-report.v1',
|
|
1559
1574
|
generated_at: startedAt,
|
|
1560
1575
|
packageName: PACKAGE_JSON.name,
|
|
1561
1576
|
version: VERSION,
|
|
1562
|
-
ok
|
|
1563
|
-
truth_status:
|
|
1577
|
+
ok,
|
|
1578
|
+
truth_status: ok ? 'verified' : 'blocked',
|
|
1564
1579
|
checks,
|
|
1565
1580
|
failed,
|
|
1566
1581
|
provenance,
|
|
1567
1582
|
tarball,
|
|
1568
1583
|
freshness,
|
|
1584
|
+
publish_readiness: publishReadiness,
|
|
1585
|
+
study_note: releaseStudyNote(publishReadiness, publishBlockerEvidence),
|
|
1569
1586
|
publishBlockerEvidence,
|
|
1570
1587
|
next_actions: nextActions
|
|
1571
1588
|
};
|
|
@@ -1646,7 +1663,136 @@ function releaseFreshness(checks = [], provenance: AnyRecord = {}, tarball: AnyR
|
|
|
1646
1663
|
};
|
|
1647
1664
|
}
|
|
1648
1665
|
|
|
1649
|
-
function
|
|
1666
|
+
function releasePublishReadiness(checks = [], provenance: AnyRecord = {}, tarball: AnyRecord = {}, publishBlockers = []) {
|
|
1667
|
+
const failedChecks = checks.filter((check) => check.status !== 'passed');
|
|
1668
|
+
const registryProbe = runReadOnlyCommandResult('npm', ['config', 'get', 'registry']);
|
|
1669
|
+
const registryUrl = registryProbe.stdout.trim() || String(PACKAGE_JSON.publishConfig?.registry || 'https://registry.npmjs.org/');
|
|
1670
|
+
const whoamiProbe = runReadOnlyCommandResult('npm', ['whoami', '--registry', registryUrl]);
|
|
1671
|
+
const registryStatus = releaseRegistryStatusFromChecks(checks);
|
|
1672
|
+
const latestProbe = registryStatus.checked
|
|
1673
|
+
? { ok: true, stdout: registryStatus.latest_version, note: registryStatus.note }
|
|
1674
|
+
: runReadOnlyCommandResult('npm', ['view', PACKAGE_JSON.name, 'version', '--registry', registryUrl]);
|
|
1675
|
+
const latestVersion = latestProbe.ok ? String(latestProbe.stdout || '').trim() : '';
|
|
1676
|
+
const blockers = [];
|
|
1677
|
+
const versionStatus = latestProbe.ok
|
|
1678
|
+
? registryStatus.not_published ? 'package_not_published' : latestVersion === VERSION ? 'already_published' : 'new_version'
|
|
1679
|
+
: 'not_verified';
|
|
1680
|
+
|
|
1681
|
+
if (!registryProbe.ok) {
|
|
1682
|
+
blockers.push(readinessBlocker('registry_not_verified', 'npm registry could not be read from this shell', 'run `npm config get registry` and confirm it points to the intended public registry', 'npm config get registry', 'error'));
|
|
1683
|
+
}
|
|
1684
|
+
if (!whoamiProbe.ok) {
|
|
1685
|
+
blockers.push(readinessBlocker('auth_not_verified', whoamiProbe.note || 'npm whoami did not confirm an authenticated publisher in this shell', 'refresh npm login/token, then rerun `npm whoami` before publishing', 'npm whoami', 'error'));
|
|
1686
|
+
}
|
|
1687
|
+
if (!latestProbe.ok) {
|
|
1688
|
+
const blocker = classifyPublishBlockerText(latestProbe.note);
|
|
1689
|
+
blockers.push(readinessBlocker(blocker?.blocker_kind || 'latest_version_not_verified', blocker?.likely_cause || 'npm latest version could not be read', blocker?.safe_next_action || 'rerun the read-only registry check before publishing', 'npm view', 'error'));
|
|
1690
|
+
}
|
|
1691
|
+
if (versionStatus === 'already_published') {
|
|
1692
|
+
blockers.push(readinessBlocker('version_already_published', `${PACKAGE_JSON.name}@${VERSION} is already present on the registry`, 'bump package.json to the next patch version before publishing', 'npm view', 'error'));
|
|
1693
|
+
}
|
|
1694
|
+
if (failedChecks.length) {
|
|
1695
|
+
blockers.push(readinessBlocker('release_checks_failed', `${failedChecks.length} release check(s) did not pass`, 'fix failing release checks before publishing', 'npm run release:check', 'error'));
|
|
1696
|
+
}
|
|
1697
|
+
if (provenance.git_dirty) {
|
|
1698
|
+
blockers.push(readinessBlocker('git_dirty', 'working tree has uncommitted changes', 'commit intended changes or keep them explicitly local before tagging/publishing', 'git status --short'));
|
|
1699
|
+
}
|
|
1700
|
+
if (provenance.dist_changed_files?.length) {
|
|
1701
|
+
blockers.push(readinessBlocker('dist_not_fresh', 'dist has changed files after freshness check', 'run build and commit dist only if this package publishes dist', 'npm run build'));
|
|
1702
|
+
}
|
|
1703
|
+
if (tarball.status !== 'checked') {
|
|
1704
|
+
blockers.push(readinessBlocker('tarball_not_verified', tarball.reason || 'npm pack dry-run did not produce tarball provenance', 'fix tarball/package-boundary issues before publishing', 'npm pack --dry-run', 'error'));
|
|
1705
|
+
}
|
|
1706
|
+
for (const blocker of publishBlockers) {
|
|
1707
|
+
blockers.push(readinessBlocker(blocker.blocker_kind, blocker.likely_cause, blocker.safe_next_action, 'npm publish', 'error'));
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
const uniqueBlockers = uniqueReadinessBlockers(blockers);
|
|
1711
|
+
const status = uniqueBlockers.length ? 'blocked' : 'ready';
|
|
1712
|
+
return {
|
|
1713
|
+
schema: 'yam.release-publish-readiness.v1',
|
|
1714
|
+
generated_at: new Date().toISOString(),
|
|
1715
|
+
package_name: PACKAGE_JSON.name,
|
|
1716
|
+
local_version: VERSION,
|
|
1717
|
+
status,
|
|
1718
|
+
registry: {
|
|
1719
|
+
url: registryUrl,
|
|
1720
|
+
latest_version: latestVersion,
|
|
1721
|
+
version_status: versionStatus,
|
|
1722
|
+
truth_status: latestProbe.ok ? 'verified' : 'blocked'
|
|
1723
|
+
},
|
|
1724
|
+
auth: {
|
|
1725
|
+
command: 'npm whoami',
|
|
1726
|
+
status: whoamiProbe.ok ? 'authenticated' : 'not_authenticated',
|
|
1727
|
+
account: whoamiProbe.ok ? 'observed_redacted' : '',
|
|
1728
|
+
note: whoamiProbe.ok ? 'npm account observed; username intentionally redacted' : whoamiProbe.note,
|
|
1729
|
+
truth_status: whoamiProbe.ok ? 'verified' : 'blocked'
|
|
1730
|
+
},
|
|
1731
|
+
blockers: uniqueBlockers,
|
|
1732
|
+
next_action: uniqueBlockers[0]?.safe_next_action || 'publish readiness is complete; run publish only when the user explicitly intends it',
|
|
1733
|
+
truth_status: status === 'ready' ? 'verified' : 'blocked'
|
|
1734
|
+
};
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
function releaseRegistryStatusFromChecks(checks = []) {
|
|
1738
|
+
const registryCheck = checks.find((check) => check.id === 'registry_status');
|
|
1739
|
+
const note = String(registryCheck?.note || '');
|
|
1740
|
+
if (registryCheck?.status !== 'passed') return { checked: false, latest_version: '', not_published: false, note };
|
|
1741
|
+
const latest = note.match(/\blatest\s+([^,\s)]+)/i)?.[1] || '';
|
|
1742
|
+
return {
|
|
1743
|
+
checked: true,
|
|
1744
|
+
latest_version: latest,
|
|
1745
|
+
not_published: /not published yet/i.test(note),
|
|
1746
|
+
note
|
|
1747
|
+
};
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
function readinessBlocker(kind, reason, safeNextAction, command = '', severity = 'warning') {
|
|
1751
|
+
return {
|
|
1752
|
+
kind,
|
|
1753
|
+
severity,
|
|
1754
|
+
reason: summarizeCheckOutput(redactSensitiveText(reason)),
|
|
1755
|
+
safe_next_action: safeNextAction,
|
|
1756
|
+
command,
|
|
1757
|
+
truth_status: 'partial'
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
function uniqueReadinessBlockers(blockers = []) {
|
|
1762
|
+
const seen = new Set<string>();
|
|
1763
|
+
return blockers.filter((blocker) => {
|
|
1764
|
+
const key = `${blocker.kind}:${blocker.safe_next_action}`;
|
|
1765
|
+
if (seen.has(key)) return false;
|
|
1766
|
+
seen.add(key);
|
|
1767
|
+
return true;
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
function releaseStudyNote(publishReadiness: AnyRecord = {}, publishBlockers = []) {
|
|
1772
|
+
const authBlocked = publishReadiness.blockers?.find((blocker) => /auth|otp|permission|forbidden|not_found/i.test(`${blocker.kind} ${blocker.reason}`));
|
|
1773
|
+
const firstBlocker = authBlocked || publishReadiness.blockers?.[0] || publishBlockers[0];
|
|
1774
|
+
if (!firstBlocker) {
|
|
1775
|
+
return buildStudyNote({
|
|
1776
|
+
changed_code: 'yam release report',
|
|
1777
|
+
changed_role: 'It checks whether the package looks ready to publish without running npm publish.',
|
|
1778
|
+
change_summary: 'Release readiness passed the read-only checks that were available.',
|
|
1779
|
+
why_important: 'A publish command changes public package state, so yam keeps the readiness proof separate from the publish action.',
|
|
1780
|
+
learning_note: 'Readiness means the checks looked good; publishing still requires an explicit user action.'
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
return buildStudyNote({
|
|
1784
|
+
issue_code: 'npm publish readiness',
|
|
1785
|
+
issue_role: 'This part checks registry, version, auth, tarball, and local release checks before any public publish.',
|
|
1786
|
+
issue_symptom: firstBlocker.reason || firstBlocker.likely_cause || 'Publish readiness is blocked.',
|
|
1787
|
+
changed_code: 'yam release report',
|
|
1788
|
+
changed_role: 'It now explains the likely publish blocker instead of only showing a raw npm failure.',
|
|
1789
|
+
change_summary: 'The report adds auth-safe readiness, safe next action, and beginner-readable blocker evidence.',
|
|
1790
|
+
why_important: 'A bad token or wrong npm account can make publish fail or target the wrong package context.',
|
|
1791
|
+
learning_note: 'When release readiness is blocked, fix the first blocker before trying a publish command.'
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
function releaseNextActions(checks = [], provenance: AnyRecord = {}, tarball: AnyRecord = {}, publishBlockers = [], publishReadiness: AnyRecord = {}) {
|
|
1650
1796
|
const actions = [];
|
|
1651
1797
|
for (const check of checks) {
|
|
1652
1798
|
if (check.status === 'passed') continue;
|
|
@@ -1664,6 +1810,9 @@ function releaseNextActions(checks = [], provenance: AnyRecord = {}, tarball: An
|
|
|
1664
1810
|
for (const blocker of publishBlockers) {
|
|
1665
1811
|
actions.push(nextActionDetail(`publish-${blocker.blocker_kind}`, blocker.truth_status === 'blocked' ? 'error' : 'warning', blocker.likely_cause, blocker.safe_next_action, 'npm publish'));
|
|
1666
1812
|
}
|
|
1813
|
+
for (const blocker of publishReadiness.blockers || []) {
|
|
1814
|
+
actions.push(nextActionDetail(`publish-readiness-${blocker.kind}`, blocker.severity || 'warning', blocker.reason, blocker.safe_next_action, blocker.command || 'npm publish'));
|
|
1815
|
+
}
|
|
1667
1816
|
return uniqueNextActionDetails(actions);
|
|
1668
1817
|
}
|
|
1669
1818
|
|
|
@@ -1736,6 +1885,24 @@ function runReadOnlyCommand(command, args = []) {
|
|
|
1736
1885
|
return String(result.stdout || '');
|
|
1737
1886
|
}
|
|
1738
1887
|
|
|
1888
|
+
function runReadOnlyCommandResult(command, args = []) {
|
|
1889
|
+
const result = spawnSync(command, args, {
|
|
1890
|
+
cwd: ROOT,
|
|
1891
|
+
encoding: 'utf8',
|
|
1892
|
+
env: { ...process.env, npm_config_cache: path.join(os.tmpdir(), 'yam-npm-cache') },
|
|
1893
|
+
timeout: 30000
|
|
1894
|
+
});
|
|
1895
|
+
const stdout = redactSensitiveText(String(result.stdout || ''));
|
|
1896
|
+
const stderr = redactSensitiveText(String(result.stderr || result.error?.message || ''));
|
|
1897
|
+
return {
|
|
1898
|
+
ok: result.status === 0,
|
|
1899
|
+
status: typeof result.status === 'number' ? result.status : null,
|
|
1900
|
+
stdout,
|
|
1901
|
+
stderr,
|
|
1902
|
+
note: summarizeCheckOutput([stdout, stderr].filter(Boolean).join('\n'))
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1739
1906
|
function runReadOnlyCommandIn(cwd, command, args = []) {
|
|
1740
1907
|
const result = spawnSync(command, args, {
|
|
1741
1908
|
cwd,
|
|
@@ -1768,12 +1935,20 @@ function runReleaseCheck(id, commandSpec) {
|
|
|
1768
1935
|
}
|
|
1769
1936
|
|
|
1770
1937
|
function summarizeCheckOutput(output = '') {
|
|
1771
|
-
const lines = String(output || '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
1938
|
+
const lines = redactSensitiveText(String(output || '')).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
1772
1939
|
if (!lines.length) return '';
|
|
1773
1940
|
const highSignal = lines.filter((line) => !line.startsWith('>')).slice(-4);
|
|
1774
1941
|
return highSignal.join(' | ').slice(0, 600);
|
|
1775
1942
|
}
|
|
1776
1943
|
|
|
1944
|
+
function redactSensitiveText(text = '') {
|
|
1945
|
+
return String(text || '')
|
|
1946
|
+
.replace(/(_authToken\s*=\s*)[^\s]+/gi, '$1[redacted]')
|
|
1947
|
+
.replace(/\b(password|passwd|api[_-]?key|secret|token)\s*[:=]\s*\S+/gi, '$1=[redacted]')
|
|
1948
|
+
.replace(/\/\/([^:\s]+):([^@\s]+)@/g, '//[redacted]@')
|
|
1949
|
+
.replace(/\b(npm_[A-Za-z0-9]{20,}|gh[pousr]_[A-Za-z0-9_]{20,}|sk-[A-Za-z0-9_-]{20,})\b/g, '[redacted-token]');
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1777
1952
|
async function readStdinTextIfAvailable() {
|
|
1778
1953
|
if (process.stdin.isTTY) return '';
|
|
1779
1954
|
const chunks = [];
|
|
@@ -1781,6 +1956,53 @@ async function readStdinTextIfAvailable() {
|
|
|
1781
1956
|
return Buffer.concat(chunks).toString('utf8').trim();
|
|
1782
1957
|
}
|
|
1783
1958
|
|
|
1959
|
+
async function loop(args = []) {
|
|
1960
|
+
const subcommand = args[0] || 'help';
|
|
1961
|
+
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return loopUsage();
|
|
1962
|
+
if (subcommand === 'report') return loopReport(args.slice(1));
|
|
1963
|
+
console.error(`unknown loop command: ${subcommand}`);
|
|
1964
|
+
process.exitCode = 1;
|
|
1965
|
+
return loopUsage();
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
function loopUsage() {
|
|
1969
|
+
console.log(`yam loop
|
|
1970
|
+
|
|
1971
|
+
Read-only loop artifact helpers. A loop report records intent, stages, evidence, blockers, next action, and a short study note. It does not run agents, start processes, publish packages, or write files.
|
|
1972
|
+
|
|
1973
|
+
Usage:
|
|
1974
|
+
yam loop report [--route route] [--intent text] [--loop-kind harness|release|ueye|scout|deep|mission] [--stage id:status:note] [--evidence text] [--blocked text] [--remaining-task text] [--truth status] [--intent-label read_only|write|destructive|runtime|visual|publish] [--issue-code text] [--issue-role text] [--issue-symptom text] [--changed-code text] [--changed-role text] [--change-summary text] [--why-important text] [--learning-note text] [--json]
|
|
1975
|
+
`);
|
|
1976
|
+
}
|
|
1977
|
+
|
|
1978
|
+
async function loopReport(args = []) {
|
|
1979
|
+
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return loopUsage();
|
|
1980
|
+
const flags = parseSimpleFlags(args, new Set(['route', 'intent', 'loop-kind', 'stage', 'evidence', 'blocked', 'blocker', 'next-action', 'remaining-task', 'truth', 'intent-label', 'tool-intent', 'issue-code', 'issue-role', 'issue-symptom', 'changed-code', 'changed-role', 'change-summary', 'why-important', 'learning-note', 'json']));
|
|
1981
|
+
const blockers = [...arrayFlag(flags.blocked), ...arrayFlag(flags.blocker)];
|
|
1982
|
+
const report = buildLoopReport({
|
|
1983
|
+
route: normalizeRoute(flags.route) || String(flags.route || ''),
|
|
1984
|
+
intent: String(flags.intent || ''),
|
|
1985
|
+
loop_kind: String(flags.loop_kind || flags.route || 'harness'),
|
|
1986
|
+
stages: arrayFlag(flags.stage),
|
|
1987
|
+
evidence: arrayFlag(flags.evidence),
|
|
1988
|
+
blockers,
|
|
1989
|
+
truth_status: isTruthStatus(flags.truth) ? flags.truth : undefined,
|
|
1990
|
+
intent_label: normalizeToolIntent(flags.intent_label || flags.tool_intent || 'read_only'),
|
|
1991
|
+
next_action: String(flags.next_action || ''),
|
|
1992
|
+
remaining_tasks: arrayFlag(flags.remaining_task),
|
|
1993
|
+
issue_code: String(flags.issue_code || ''),
|
|
1994
|
+
issue_role: String(flags.issue_role || ''),
|
|
1995
|
+
issue_symptom: String(flags.issue_symptom || ''),
|
|
1996
|
+
changed_code: String(flags.changed_code || ''),
|
|
1997
|
+
changed_role: String(flags.changed_role || ''),
|
|
1998
|
+
change_summary: String(flags.change_summary || ''),
|
|
1999
|
+
why_important: String(flags.why_important || ''),
|
|
2000
|
+
learning_note: String(flags.learning_note || '')
|
|
2001
|
+
});
|
|
2002
|
+
printJsonOrHuman(report, Boolean(flags.json), 'Loop report');
|
|
2003
|
+
if (report.truth_status === 'blocked') process.exitCode = 1;
|
|
2004
|
+
}
|
|
2005
|
+
|
|
1784
2006
|
async function ueye(args = []) {
|
|
1785
2007
|
const subcommand = args[0] || 'help';
|
|
1786
2008
|
if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return ueyeUsage();
|
|
@@ -1801,7 +2023,7 @@ Usage:
|
|
|
1801
2023
|
yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
|
|
1802
2024
|
yam ueye compare --reference ref.png --actual screenshot.png [--json]
|
|
1803
2025
|
yam ueye preflight [dir] [--json]
|
|
1804
|
-
yam ueye report [--reference ref.png] [--actual screenshot.png] [--preflight-id id] [--p0-risk text] [--quality-gate-note text] [--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] [--completion-claim draft|needs-polish|done] [--strict] [--design-score n] [--p0 text] [--p1 text] [--states-checked] [--mobile-checked] [--contrast-checked] [--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]
|
|
2026
|
+
yam ueye report [--reference ref.png] [--actual screenshot.png] [--preflight-id id] [--p0-risk text] [--quality-gate-note text] [--brief-dimension text] [--constraint text] [--anti-slop text] [--invented-metric] [--placeholder-copy] [--generic-visual] [--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] [--completion-claim draft|needs-polish|done] [--strict] [--design-score n] [--p0 text] [--p1 text] [--states-checked] [--mobile-checked] [--contrast-checked] [--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]
|
|
1805
2027
|
|
|
1806
2028
|
Notes:
|
|
1807
2029
|
capture uses a locally available Playwright install when present. It does not download browsers or install dependencies.
|
|
@@ -2038,7 +2260,7 @@ async function ueyeCompare(args = []) {
|
|
|
2038
2260
|
|
|
2039
2261
|
async function ueyeReport(args = []) {
|
|
2040
2262
|
if (args[0] === 'help' || args[0] === '--help' || args[0] === '-h') return ueyeUsage();
|
|
2041
|
-
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', 'preflight-id', 'p0-risk', 'quality-gate-note', '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', 'completion-claim', 'completion-status', 'gate-mode', 'strict', 'design-score', 'min-design-score', 'p0', 'p1', 'open-p0', 'open-p1', 'states-checked', 'mobile-checked', 'responsive-checked', 'contrast-checked', 'accessibility-checked', 'cta-checked', 'direction-locked', 'reference-read', 'comparison-result', 'json']));
|
|
2263
|
+
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', 'preflight-id', 'p0-risk', 'quality-gate-note', 'brief-dimension', 'constraint', 'anti-slop', 'invented-metric', 'placeholder-copy', 'generic-visual', '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', 'completion-claim', 'completion-status', 'gate-mode', 'strict', 'design-score', 'min-design-score', 'p0', 'p1', 'open-p0', 'open-p1', 'states-checked', 'mobile-checked', 'responsive-checked', 'contrast-checked', 'accessibility-checked', 'cta-checked', 'direction-locked', 'reference-read', 'comparison-result', 'json']));
|
|
2042
2264
|
const reference = String(flags.reference || flags.ref || '');
|
|
2043
2265
|
const actual = String(flags.actual || flags.screenshot || '');
|
|
2044
2266
|
const referenceSources = [];
|
|
@@ -2047,6 +2269,8 @@ async function ueyeReport(args = []) {
|
|
|
2047
2269
|
let blockedReason = String(flags.blocked_reason || '');
|
|
2048
2270
|
const reviewSessionId = String(flags.review_session_id || `ueye-${timestampId()}`);
|
|
2049
2271
|
const previousReport = await readPreviousUeyeReport(flags.previous_report);
|
|
2272
|
+
const designBrief = buildUeyeDesignBrief(flags);
|
|
2273
|
+
const antiSlopReview = buildUeyeAntiSlopReview(flags);
|
|
2050
2274
|
const surfaceContext = buildUeyeSurfaceContext({
|
|
2051
2275
|
provider_context: String(flags.provider_context || 'not-recorded'),
|
|
2052
2276
|
provider_badge: String(flags.provider_badge || flags.provider_context || 'not-recorded'),
|
|
@@ -2130,7 +2354,7 @@ async function ueyeReport(args = []) {
|
|
|
2130
2354
|
completion_claim: flags.completion_claim || flags.completion_status || 'draft',
|
|
2131
2355
|
design_score: numberOrNull(flags.design_score),
|
|
2132
2356
|
min_design_score: numberOrNull(flags.min_design_score) ?? 8,
|
|
2133
|
-
p0: [...arrayFlag(flags.p0), ...arrayFlag(flags.open_p0), ...arrayFlag(flags.p0_risk)],
|
|
2357
|
+
p0: [...arrayFlag(flags.p0), ...arrayFlag(flags.open_p0), ...arrayFlag(flags.p0_risk), ...antiSlopReview.blockers],
|
|
2134
2358
|
p1: [...arrayFlag(flags.p1), ...arrayFlag(flags.open_p1)],
|
|
2135
2359
|
states_checked: Boolean(flags.states_checked),
|
|
2136
2360
|
mobile_checked: Boolean(flags.mobile_checked),
|
|
@@ -2171,6 +2395,8 @@ async function ueyeReport(args = []) {
|
|
|
2171
2395
|
const result = {
|
|
2172
2396
|
...report,
|
|
2173
2397
|
review_session_id: reviewSessionId,
|
|
2398
|
+
design_brief: designBrief,
|
|
2399
|
+
anti_slop_review: antiSlopReview,
|
|
2174
2400
|
preflight: {
|
|
2175
2401
|
preflight_id: String(flags.preflight_id || ''),
|
|
2176
2402
|
p0_risks: arrayFlag(flags.p0_risk),
|
|
@@ -2187,6 +2413,51 @@ async function ueyeReport(args = []) {
|
|
|
2187
2413
|
if (report.truth_status === 'blocked') process.exitCode = 1;
|
|
2188
2414
|
}
|
|
2189
2415
|
|
|
2416
|
+
function buildUeyeDesignBrief(flags: AnyRecord = {}) {
|
|
2417
|
+
const dimensions = arrayFlag(flags.brief_dimension);
|
|
2418
|
+
const constraints = arrayFlag(flags.constraint);
|
|
2419
|
+
const hasBrief = dimensions.length || constraints.length;
|
|
2420
|
+
return {
|
|
2421
|
+
schema: 'yam.ueye-design-brief.v1',
|
|
2422
|
+
dimensions,
|
|
2423
|
+
constraints,
|
|
2424
|
+
source_boundary: hasBrief ? 'operator_provided_cli_flags' : 'not_provided',
|
|
2425
|
+
next_action: hasBrief ? 'use this brief as context, not as visual proof' : 'add --brief-dimension and --constraint when design direction matters',
|
|
2426
|
+
truth_status: hasBrief ? 'partial' : 'assumed'
|
|
2427
|
+
};
|
|
2428
|
+
}
|
|
2429
|
+
|
|
2430
|
+
function buildUeyeAntiSlopReview(flags: AnyRecord = {}) {
|
|
2431
|
+
const explicit = arrayFlag(flags.anti_slop);
|
|
2432
|
+
const checks = [
|
|
2433
|
+
antiSlopCheck('invented_metric', 'Invented metrics or unsupported numeric claims', Boolean(flags.invented_metric), 'replace invented metrics with measured values or remove the claim'),
|
|
2434
|
+
antiSlopCheck('placeholder_copy', 'Placeholder copy remains in the UI', Boolean(flags.placeholder_copy), 'replace placeholder copy with product-specific text before claiming done'),
|
|
2435
|
+
antiSlopCheck('generic_visual', 'Generic visual treatment does not fit the product context', Boolean(flags.generic_visual), 'tie visuals to the product, state, or reference before claiming design quality')
|
|
2436
|
+
];
|
|
2437
|
+
const blockers = [
|
|
2438
|
+
...explicit,
|
|
2439
|
+
...checks.filter((check) => check.status === 'fail').map((check) => `${check.id}: ${check.next_action}`)
|
|
2440
|
+
];
|
|
2441
|
+
return {
|
|
2442
|
+
schema: 'yam.ueye-anti-slop-review.v1',
|
|
2443
|
+
checks,
|
|
2444
|
+
blockers,
|
|
2445
|
+
next_action: blockers[0] || (checks.some((check) => check.status === 'pass') || explicit.length ? 'anti-slop risks are recorded; keep visual proof separate' : 'record anti-slop risks when they matter before claiming done'),
|
|
2446
|
+
truth_status: blockers.length ? 'blocked' : checks.some((check) => check.status === 'pass') || explicit.length ? 'partial' : 'assumed'
|
|
2447
|
+
};
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
function antiSlopCheck(id, label, failed = false, nextAction = '') {
|
|
2451
|
+
return {
|
|
2452
|
+
id,
|
|
2453
|
+
label,
|
|
2454
|
+
status: failed ? 'fail' : 'not_checked',
|
|
2455
|
+
severity: failed ? 'P0' : 'P2',
|
|
2456
|
+
next_action: nextAction,
|
|
2457
|
+
truth_status: failed ? 'blocked' : 'assumed'
|
|
2458
|
+
};
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2190
2461
|
async function readPreviousUeyeReport(file = '') {
|
|
2191
2462
|
const target = String(file || '');
|
|
2192
2463
|
if (!target) return null;
|
|
@@ -2577,7 +2848,7 @@ function parseSimpleFlags(args = [], allowed = new Set<string>()) {
|
|
|
2577
2848
|
const key = rawKey.slice(2);
|
|
2578
2849
|
if (allowed.size && !allowed.has(key)) continue;
|
|
2579
2850
|
const normalizedKey = key.replace(/-/g, '_');
|
|
2580
|
-
if (['json', 'full-page', 'requested', 'attempted', 'available', 'wait-loop', 'cleanup-checked', 'cleanup-observed', 'left-running-intentionally', 'strict', 'preserved-state', 'states-checked', 'mobile-checked', 'responsive-checked', 'contrast-checked', 'accessibility-checked', 'cta-checked', 'direction-locked', 'reference-read'].includes(key)) {
|
|
2851
|
+
if (['json', 'full-page', 'requested', 'attempted', 'available', 'wait-loop', 'cleanup-checked', 'cleanup-observed', 'left-running-intentionally', 'strict', 'preserved-state', 'states-checked', 'mobile-checked', 'responsive-checked', 'contrast-checked', 'accessibility-checked', 'cta-checked', 'direction-locked', 'reference-read', 'invented-metric', 'placeholder-copy', 'generic-visual'].includes(key)) {
|
|
2581
2852
|
flags[normalizedKey] = true;
|
|
2582
2853
|
continue;
|
|
2583
2854
|
}
|
|
@@ -3851,6 +4122,7 @@ async function main() {
|
|
|
3851
4122
|
if (command === 'measure') return measure(process.argv[3], process.argv.slice(4));
|
|
3852
4123
|
if (command === 'tools') return tools(process.argv.slice(3));
|
|
3853
4124
|
if (command === 'proof') return proof(process.argv.slice(3));
|
|
4125
|
+
if (command === 'loop') return loop(process.argv.slice(3));
|
|
3854
4126
|
if (command === 'ueye') return ueye(process.argv.slice(3));
|
|
3855
4127
|
if (command === 'media') return media(process.argv.slice(3));
|
|
3856
4128
|
if (command === 'runtime') return runtime(process.argv.slice(3));
|