yam-flow 0.1.7 → 0.1.9

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/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 nextActions = releaseNextActions(checks, provenance, tarball, publishBlockerEvidence);
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: failed.length === 0,
1563
- truth_status: failed.length === 0 ? 'verified' : 'blocked',
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 releaseNextActions(checks = [], provenance: AnyRecord = {}, tarball: AnyRecord = {}, publishBlockers = []) {
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,59 @@ 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, handoff fields, 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] [--fix-first-item text] [--remaining-task text] [--recommended-direction text] [--implementation-note text] [--why-this-next text] [--blocked-by text] [--owner-route route] [--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', 'fix-first-item', 'remaining-task', 'recommended-direction', 'direction', 'implementation-note', 'why-this-next', 'blocked-by', 'owner-route', '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
+ fix_first_items: arrayFlag(flags.fix_first_item),
1993
+ remaining_tasks: arrayFlag(flags.remaining_task),
1994
+ recommended_direction: String(flags.recommended_direction || flags.direction || ''),
1995
+ implementation_notes: arrayFlag(flags.implementation_note),
1996
+ why_this_next: String(flags.why_this_next || ''),
1997
+ blocked_by: arrayFlag(flags.blocked_by),
1998
+ owner_route: String(flags.owner_route || flags.route || ''),
1999
+ issue_code: String(flags.issue_code || ''),
2000
+ issue_role: String(flags.issue_role || ''),
2001
+ issue_symptom: String(flags.issue_symptom || ''),
2002
+ changed_code: String(flags.changed_code || ''),
2003
+ changed_role: String(flags.changed_role || ''),
2004
+ change_summary: String(flags.change_summary || ''),
2005
+ why_important: String(flags.why_important || ''),
2006
+ learning_note: String(flags.learning_note || '')
2007
+ });
2008
+ printJsonOrHuman(report, Boolean(flags.json), 'Loop report');
2009
+ if (report.truth_status === 'blocked') process.exitCode = 1;
2010
+ }
2011
+
1784
2012
  async function ueye(args = []) {
1785
2013
  const subcommand = args[0] || 'help';
1786
2014
  if (subcommand === 'help' || subcommand === '--help' || subcommand === '-h') return ueyeUsage();
@@ -1801,7 +2029,7 @@ Usage:
1801
2029
  yam ueye capture --url URL --out screenshot.png [--viewport 1440x900] [--full-page] [--json]
1802
2030
  yam ueye compare --reference ref.png --actual screenshot.png [--json]
1803
2031
  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]
2032
+ 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
2033
 
1806
2034
  Notes:
1807
2035
  capture uses a locally available Playwright install when present. It does not download browsers or install dependencies.
@@ -2038,7 +2266,7 @@ async function ueyeCompare(args = []) {
2038
2266
 
2039
2267
  async function ueyeReport(args = []) {
2040
2268
  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']));
2269
+ 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
2270
  const reference = String(flags.reference || flags.ref || '');
2043
2271
  const actual = String(flags.actual || flags.screenshot || '');
2044
2272
  const referenceSources = [];
@@ -2047,6 +2275,8 @@ async function ueyeReport(args = []) {
2047
2275
  let blockedReason = String(flags.blocked_reason || '');
2048
2276
  const reviewSessionId = String(flags.review_session_id || `ueye-${timestampId()}`);
2049
2277
  const previousReport = await readPreviousUeyeReport(flags.previous_report);
2278
+ const designBrief = buildUeyeDesignBrief(flags);
2279
+ const antiSlopReview = buildUeyeAntiSlopReview(flags);
2050
2280
  const surfaceContext = buildUeyeSurfaceContext({
2051
2281
  provider_context: String(flags.provider_context || 'not-recorded'),
2052
2282
  provider_badge: String(flags.provider_badge || flags.provider_context || 'not-recorded'),
@@ -2130,7 +2360,7 @@ async function ueyeReport(args = []) {
2130
2360
  completion_claim: flags.completion_claim || flags.completion_status || 'draft',
2131
2361
  design_score: numberOrNull(flags.design_score),
2132
2362
  min_design_score: numberOrNull(flags.min_design_score) ?? 8,
2133
- p0: [...arrayFlag(flags.p0), ...arrayFlag(flags.open_p0), ...arrayFlag(flags.p0_risk)],
2363
+ p0: [...arrayFlag(flags.p0), ...arrayFlag(flags.open_p0), ...arrayFlag(flags.p0_risk), ...antiSlopReview.blockers],
2134
2364
  p1: [...arrayFlag(flags.p1), ...arrayFlag(flags.open_p1)],
2135
2365
  states_checked: Boolean(flags.states_checked),
2136
2366
  mobile_checked: Boolean(flags.mobile_checked),
@@ -2171,6 +2401,8 @@ async function ueyeReport(args = []) {
2171
2401
  const result = {
2172
2402
  ...report,
2173
2403
  review_session_id: reviewSessionId,
2404
+ design_brief: designBrief,
2405
+ anti_slop_review: antiSlopReview,
2174
2406
  preflight: {
2175
2407
  preflight_id: String(flags.preflight_id || ''),
2176
2408
  p0_risks: arrayFlag(flags.p0_risk),
@@ -2187,6 +2419,51 @@ async function ueyeReport(args = []) {
2187
2419
  if (report.truth_status === 'blocked') process.exitCode = 1;
2188
2420
  }
2189
2421
 
2422
+ function buildUeyeDesignBrief(flags: AnyRecord = {}) {
2423
+ const dimensions = arrayFlag(flags.brief_dimension);
2424
+ const constraints = arrayFlag(flags.constraint);
2425
+ const hasBrief = dimensions.length || constraints.length;
2426
+ return {
2427
+ schema: 'yam.ueye-design-brief.v1',
2428
+ dimensions,
2429
+ constraints,
2430
+ source_boundary: hasBrief ? 'operator_provided_cli_flags' : 'not_provided',
2431
+ next_action: hasBrief ? 'use this brief as context, not as visual proof' : 'add --brief-dimension and --constraint when design direction matters',
2432
+ truth_status: hasBrief ? 'partial' : 'assumed'
2433
+ };
2434
+ }
2435
+
2436
+ function buildUeyeAntiSlopReview(flags: AnyRecord = {}) {
2437
+ const explicit = arrayFlag(flags.anti_slop);
2438
+ const checks = [
2439
+ antiSlopCheck('invented_metric', 'Invented metrics or unsupported numeric claims', Boolean(flags.invented_metric), 'replace invented metrics with measured values or remove the claim'),
2440
+ antiSlopCheck('placeholder_copy', 'Placeholder copy remains in the UI', Boolean(flags.placeholder_copy), 'replace placeholder copy with product-specific text before claiming done'),
2441
+ 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')
2442
+ ];
2443
+ const blockers = [
2444
+ ...explicit,
2445
+ ...checks.filter((check) => check.status === 'fail').map((check) => `${check.id}: ${check.next_action}`)
2446
+ ];
2447
+ return {
2448
+ schema: 'yam.ueye-anti-slop-review.v1',
2449
+ checks,
2450
+ blockers,
2451
+ 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'),
2452
+ truth_status: blockers.length ? 'blocked' : checks.some((check) => check.status === 'pass') || explicit.length ? 'partial' : 'assumed'
2453
+ };
2454
+ }
2455
+
2456
+ function antiSlopCheck(id, label, failed = false, nextAction = '') {
2457
+ return {
2458
+ id,
2459
+ label,
2460
+ status: failed ? 'fail' : 'not_checked',
2461
+ severity: failed ? 'P0' : 'P2',
2462
+ next_action: nextAction,
2463
+ truth_status: failed ? 'blocked' : 'assumed'
2464
+ };
2465
+ }
2466
+
2190
2467
  async function readPreviousUeyeReport(file = '') {
2191
2468
  const target = String(file || '');
2192
2469
  if (!target) return null;
@@ -2577,7 +2854,7 @@ function parseSimpleFlags(args = [], allowed = new Set<string>()) {
2577
2854
  const key = rawKey.slice(2);
2578
2855
  if (allowed.size && !allowed.has(key)) continue;
2579
2856
  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)) {
2857
+ 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
2858
  flags[normalizedKey] = true;
2582
2859
  continue;
2583
2860
  }
@@ -3851,6 +4128,7 @@ async function main() {
3851
4128
  if (command === 'measure') return measure(process.argv[3], process.argv.slice(4));
3852
4129
  if (command === 'tools') return tools(process.argv.slice(3));
3853
4130
  if (command === 'proof') return proof(process.argv.slice(3));
4131
+ if (command === 'loop') return loop(process.argv.slice(3));
3854
4132
  if (command === 'ueye') return ueye(process.argv.slice(3));
3855
4133
  if (command === 'media') return media(process.argv.slice(3));
3856
4134
  if (command === 'runtime') return runtime(process.argv.slice(3));