@terra-symposium/standards-quality 0.8.0 → 0.9.0

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.
Files changed (39) hide show
  1. package/README.md +8 -0
  2. package/dist/index.d.ts +4 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +3 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/node-reporter.d.ts +79 -0
  7. package/dist/node-reporter.d.ts.map +1 -0
  8. package/dist/node-reporter.js +217 -0
  9. package/dist/node-reporter.js.map +1 -0
  10. package/dist/playwright-reporter.d.ts +107 -0
  11. package/dist/playwright-reporter.d.ts.map +1 -0
  12. package/dist/playwright-reporter.js +138 -0
  13. package/dist/playwright-reporter.js.map +1 -0
  14. package/dist/progress.d.ts +5 -2
  15. package/dist/progress.d.ts.map +1 -1
  16. package/dist/progress.js +6 -4
  17. package/dist/progress.js.map +1 -1
  18. package/dist/runner.d.ts +3 -1
  19. package/dist/runner.d.ts.map +1 -1
  20. package/dist/runner.js +580 -86
  21. package/dist/runner.js.map +1 -1
  22. package/dist/single-use.d.ts +2 -1
  23. package/dist/single-use.d.ts.map +1 -1
  24. package/dist/single-use.js +42 -15
  25. package/dist/single-use.js.map +1 -1
  26. package/dist/test-workspace.d.ts +6 -0
  27. package/dist/test-workspace.d.ts.map +1 -0
  28. package/dist/test-workspace.js +48 -0
  29. package/dist/test-workspace.js.map +1 -0
  30. package/dist/ui-recommendations.d.ts +43 -0
  31. package/dist/ui-recommendations.d.ts.map +1 -0
  32. package/dist/ui-recommendations.js +182 -0
  33. package/dist/ui-recommendations.js.map +1 -0
  34. package/dist/workspace-test-runner.d.ts +6 -0
  35. package/dist/workspace-test-runner.d.ts.map +1 -0
  36. package/dist/workspace-test-runner.js +48 -0
  37. package/dist/workspace-test-runner.js.map +1 -0
  38. package/package.json +2 -2
  39. package/quality.schema.json +195 -130
package/dist/runner.js CHANGED
@@ -16,12 +16,14 @@ import { collapseInlineIfs } from './inline-if.js';
16
16
  import { collapseTypesAndObjects } from './collapse.js';
17
17
  import { deadCodeEditsIntroduceTypeErrors, deadCodeTypeProgram, fixSingleUseDeclarations, singleUseFindings } from './single-use.js';
18
18
  import { documentSrcFile, documentationFindings, duplicateImportFindings, maintainSrcFile, protectLongImports } from './documentation.js';
19
- import { existsSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
19
+ import { existsSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs';
20
20
  import { fileURLToPath } from 'node:url';
21
21
  import { fixObjectForwarding, objectForwardingFindings } from './object-forwarding.js';
22
22
  import { fixRegexDedup, fixRepeatedClasses, regexFindings, repeatedClassFindings, runTailwindCanonical, uiPatternFindings } from './patterns.js';
23
23
  import { qualityProgress } from './progress.js';
24
+ import { qualityNodeReporterOptions } from './node-reporter.js';
24
25
  import { securityFindings } from './security.js';
26
+ import { uiComponentRecommendations } from './ui-recommendations.js';
25
27
  import { spawnSync } from 'node:child_process';
26
28
  import { srcFiles, workingSrcFiles } from './files.js';
27
29
  import { writeHistoryReport } from './history-report.js';
@@ -31,6 +33,12 @@ import { PROTECTED_IDENTIFIERS } from './naming.js';
31
33
  /*//////////////////////////////////////////////////
32
34
  REGEX
33
35
  //////////////////////////////////////////////////*/
36
+ const BPLAYWRIGHT_B_U_REGEX = /\bplaywright\b/u;
37
+ const BWORKSPACE_TEST_RUNNER_JS_B_U_REGEX = /\bworkspace-test-runner\.js\b/u;
38
+ const NON_WORKSPACE_DIRS = new Set(['.git', '.next', '.nuxt', '.turbo', '.vercel', '.vite', '.astro', 'build', 'coverage', 'dist', 'node_modules', 'out', 'vendor']);
39
+ /*//////////////////////////////////////////////////
40
+ REGEX
41
+ //////////////////////////////////////////////////*/
34
42
  const R_N_REGEX = /\r?\n/;
35
43
  const SHARED_REGEX = /\/\*|\/\//;
36
44
  /*//////////////////////////////////////////////////
@@ -56,6 +64,7 @@ const CLI_CMDS = new Set([
56
64
  'tailwind-canonical',
57
65
  'regex-dedup',
58
66
  'ui-patterns',
67
+ 'ui-recommendations',
59
68
  'imports',
60
69
  'aliases',
61
70
  'forwarding',
@@ -74,11 +83,13 @@ const ESLINT_GLOB_SPECIALS = new Set(['*', '?', '[', ']', '{', '}', '(', ')', '!
74
83
  let reportStages;
75
84
  let reportStageName;
76
85
  let reportGitHubData;
86
+ let outputPhase;
77
87
  /*//////////////////////////////////////////////////
78
88
  REGEX
79
89
  //////////////////////////////////////////////////*/
80
90
  const C_M_JS_TS_X_REGEX = /\.(?:c|m)?(?:js|ts)x?$/;
81
91
  const C_M_JS_TS_X_CLASS_2 = /\.(?:(?:c|m)?(?:js|ts)x?)$/;
92
+ const WORKSPACE_MANIFESTS = new Map();
82
93
  const ANSI_ESCAPE = new RegExp(`${String.fromCharCode(27)}\\[[0-?]*[ -/]*[@-~]`, 'g');
83
94
  /*//////////////////////////////////////////////////
84
95
  HELPERS
@@ -235,9 +246,294 @@ function qualityHeader(name, detail) {
235
246
  reportStageName = name;
236
247
  reportStages.set(name, { name, status: 'passed', findings: 0 });
237
248
  }
249
+ if (outputPhase) {
250
+ const indent = name.startsWith('dead-code/') || (outputPhase === 'Tests' && name.startsWith('command:')) ? ' ' : ' ';
251
+ const labels = {
252
+ 'dead-code/safe-fixes': 'Safe fixes',
253
+ 'dead-code/verification': 'Verification',
254
+ 'dead-code/knip-fix': 'Knip import-consumer scan',
255
+ 'dead-code/eslint-fix': 'ESLint unused-declaration scan',
256
+ 'dead-code/single-use': 'Single-use declaration/reference scan and classification',
257
+ 'dead-code/knip': 'Knip reachability verification',
258
+ 'dead-code': 'Dead code',
259
+ };
260
+ const label = labels[name] ?? name.replace(/^command:/u, '').replace(/(^|[-/])([a-z])/gu, (_match, separator, letter) => `${separator === '-' ? ' ' : separator}${letter.toUpperCase()}`);
261
+ console.error(`${indent}${label}`);
262
+ if (detail)
263
+ console.error(`${indent} ${detail}`);
264
+ return;
265
+ }
238
266
  console.error(`\n=== ${name} ===`);
239
267
  console.error(detail);
240
268
  }
269
+ /**
270
+ * Starts one fixed output phase for the full quality command.
271
+ *
272
+ * @param phase Phase heading owned by the Quality runner.
273
+ *
274
+ * @return Nothing; the heading is written once before the phase checks.
275
+ */
276
+ function beginQualityPhase(phase) {
277
+ outputPhase = phase;
278
+ console.error(`\n${phase.toUpperCase()}`);
279
+ }
280
+ /**
281
+ * Finds package manifests once so filtered workspace scripts can be classified.
282
+ *
283
+ * @param root Repository root whose package scripts are selected.
284
+ *
285
+ * @return Workspace package manifests outside generated and dependency directories.
286
+ */
287
+ function workspaceManifests(root) {
288
+ const cached = WORKSPACE_MANIFESTS.get(root);
289
+ if (cached)
290
+ return cached;
291
+ const manifests = [];
292
+ const visit = (dir) => {
293
+ let entries;
294
+ try {
295
+ entries = readdirSync(dir, { withFileTypes: true });
296
+ }
297
+ catch {
298
+ return;
299
+ }
300
+ for (const entry of entries) {
301
+ const path = resolve(dir, entry.name);
302
+ if (entry.isDirectory()) {
303
+ if (!NON_WORKSPACE_DIRS.has(entry.name) && !entry.name.startsWith('.'))
304
+ visit(path);
305
+ continue;
306
+ }
307
+ if (entry.name !== 'package.json' || !entry.isFile())
308
+ continue;
309
+ try {
310
+ const pkg = JSON.parse(readFileSync(path, 'utf8'));
311
+ manifests.push({ directory: dir, ...(typeof pkg.name === 'string' ? { name: pkg.name } : {}), scripts: pkg.scripts ?? {} });
312
+ }
313
+ catch {
314
+ continue;
315
+ }
316
+ }
317
+ };
318
+ visit(root);
319
+ WORKSPACE_MANIFESTS.set(root, manifests);
320
+ return manifests;
321
+ }
322
+ /**
323
+ * Finds the selected package script name without treating option values as check names.
324
+ *
325
+ * @param check Configured command whose arguments may select a package script.
326
+ *
327
+ * @return Script name for `run`, `run-script`, or a direct package-manager script command.
328
+ */
329
+ function scriptNameForInvocation(check) {
330
+ const args = check.args ?? [];
331
+ const runIdx = args.findIndex((arg) => arg === 'run' || arg === 'run-script');
332
+ if (runIdx >= 0)
333
+ return args[runIdx + 1] ?? '';
334
+ if (!/^(?:pnpm|npm|yarn|bun)(?:\.cmd)?$/iu.test(basename(check.command)))
335
+ return '';
336
+ for (let idx = 0; idx < args.length; idx++) {
337
+ const arg = args[idx];
338
+ if (arg === '--filter') {
339
+ idx++;
340
+ continue;
341
+ }
342
+ if (arg.startsWith('--')) {
343
+ if (arg === '--workspace-concurrency' || arg === '--reporter' || arg === '--dir')
344
+ idx++;
345
+ continue;
346
+ }
347
+ if (arg.startsWith('-'))
348
+ continue;
349
+ return arg;
350
+ }
351
+ return '';
352
+ }
353
+ /**
354
+ * Returns repository package filters supplied to a package-manager command.
355
+ *
356
+ * @param check Configured command whose arguments may select workspace packages.
357
+ *
358
+ * @return Explicit `--filter` selectors in argument order.
359
+ */
360
+ function workspaceFilters(check) {
361
+ const args = check.args ?? [];
362
+ const selectors = [];
363
+ for (let idx = 0; idx < args.length; idx++) {
364
+ const arg = args[idx];
365
+ if (arg === '--filter' && args[idx + 1])
366
+ selectors.push(args[++idx]);
367
+ else if (arg.startsWith('--filter='))
368
+ selectors.push(arg.slice('--filter='.length));
369
+ }
370
+ return selectors;
371
+ }
372
+ /**
373
+ * Reads package scripts selected by a direct or filtered package-manager command.
374
+ *
375
+ * @param cfg Quality configuration containing the repository root.
376
+ * @param check Configured command whose script may identify its test runner or phase.
377
+ *
378
+ * @return Script text from the root package or selected workspace packages.
379
+ */
380
+ function scriptsForInvocation(cfg, check) {
381
+ const scriptName = scriptNameForInvocation(check);
382
+ if (!scriptName)
383
+ return [];
384
+ const selectors = workspaceFilters(check);
385
+ const packages = selectors.length
386
+ ? workspaceManifests(cfg.root).filter((manifest) => selectors.some((selector) => matchesWorkspaceFilter(cfg.root, selector, manifest)))
387
+ : workspaceManifests(cfg.root).filter((manifest) => manifest.directory === cfg.root);
388
+ return packages.map((manifest) => manifest.scripts[scriptName]).filter((script) => typeof script === 'string');
389
+ }
390
+ /**
391
+ * Matches an exact workspace name or a simple repository-relative package filter.
392
+ *
393
+ * @param root Repository root that owns workspace package paths.
394
+ * @param selector Package name or path expression following `--filter`.
395
+ * @param manifest Candidate workspace package metadata.
396
+ *
397
+ * @return Whether the selector can be matched to this package without guessing.
398
+ */
399
+ function matchesWorkspaceFilter(root, selector, manifest) {
400
+ const packageSelector = selector.replace(/^\.\.\./u, '').replace(/\.\.\.$/u, '');
401
+ const pathSelector = packageSelector.startsWith('.') || (!packageSelector.startsWith('@') && packageSelector.includes('/'));
402
+ if (!pathSelector)
403
+ return manifest.name === packageSelector;
404
+ const norm = packageSelector.replace(/^\.\//u, '').replace(/\*.*$/u, '').replace(/\/$/u, '');
405
+ const packagePath = resolve(root, norm || '.');
406
+ const hasGlob = /[*?{[]/u.test(packageSelector);
407
+ if (!hasGlob)
408
+ return manifest.directory === packagePath;
409
+ const relativePackage = relative(packagePath, manifest.directory);
410
+ if (relativePackage === '..' || relativePackage.startsWith(`..${sep}`) || isAbsolute(relativePackage))
411
+ return false;
412
+ if (packageSelector.includes('**'))
413
+ return true;
414
+ return !relativePackage.includes(sep);
415
+ }
416
+ /**
417
+ * Reads one display string for the command and its selected package script.
418
+ *
419
+ * @param cfg Quality configuration containing the repository root.
420
+ * @param check Configured command to describe.
421
+ *
422
+ * @return Lowercase command and script text; free-form argument values are excluded.
423
+ */
424
+ function invocationText(cfg, check) {
425
+ return `${check.name} ${check.command} ${scriptNameForInvocation(check)} ${scriptsForInvocation(cfg, check).join(' ')}`.toLowerCase();
426
+ }
427
+ /**
428
+ * Identifies test commands without adding grouping metadata to quality.json.
429
+ *
430
+ * @param cfg Quality configuration containing repository commands.
431
+ * @param check Configured command to classify.
432
+ *
433
+ * @return Whether the command selects a test suite.
434
+ */
435
+ function isTestInvocation(cfg, check) {
436
+ return (testRunner(cfg, check) !== 'unknown' ||
437
+ /\b(?:test|tests|spec|specs|playwright|vitest|jest|mocha|e2e|contract|integration)\b/u.test(invocationText(cfg, check)) ||
438
+ (check.args ?? []).some((arg) => arg === '--test' || arg.startsWith('--test=')));
439
+ }
440
+ /**
441
+ * Assigns a configured command to the fixed runner phase order.
442
+ *
443
+ * @param cfg Quality configuration containing repository commands.
444
+ * @param check Configured command to classify.
445
+ *
446
+ * @return The output and execution phase selected by Terra Quality.
447
+ */
448
+ function phaseForInvocation(cfg, check) {
449
+ const text = invocationText(cfg, check);
450
+ if (isTestInvocation(cfg, check))
451
+ return 'Tests';
452
+ if (/\b(?:format|prettier)\b/u.test(text))
453
+ return 'Format';
454
+ if (/\b(?:typecheck|type-check|types|tsc|astro\s+check)\b/u.test(text))
455
+ return 'Framework / type checks';
456
+ if (/\b(?:build|compile|bundle)\b/u.test(text))
457
+ return 'Build';
458
+ if (/\b(?:github|diff|repository-health|repo-health)\b/u.test(text))
459
+ return 'Repository health';
460
+ return 'Static quality';
461
+ }
462
+ /**
463
+ * Groups selected tests by the kind of boundary they exercise.
464
+ *
465
+ * @param cfg Quality configuration containing repository commands.
466
+ * @param check Configured test command to classify.
467
+ *
468
+ * @return Stable display category for the test suite.
469
+ */
470
+ function testGroup(cfg, check) {
471
+ const text = invocationText(cfg, check);
472
+ if (testRunner(cfg, check) === 'playwright' || /\b(?:playwright|browser|e2e|visual|cloud)\b/u.test(text))
473
+ return 'Browser / Cloud';
474
+ if (/\b(?:integration|database|postgres|neon|http|api)\b/u.test(text))
475
+ return 'Integration';
476
+ if (/\b(?:unit|contract)\b/u.test(text))
477
+ return 'Unit / Contract';
478
+ return 'Other tests';
479
+ }
480
+ /**
481
+ * Identifies the runner family for configured test-command reporting.
482
+ *
483
+ * @param cfg Quality configuration containing repository commands.
484
+ * @param check Configured test command to classify.
485
+ *
486
+ * @return Node, Playwright, or unknown when the runner cannot be inferred safely.
487
+ */
488
+ function testRunner(cfg, check) {
489
+ const text = invocationText(cfg, check);
490
+ const args = check.args ?? [];
491
+ if (BPLAYWRIGHT_B_U_REGEX.test(text) || /\bplaywright\b/iu.test(args.join(' ')))
492
+ return 'playwright';
493
+ const nodeCmd = /^(?:node|node\.exe)$/iu.test(basename(check.command));
494
+ const nodeTestArgs = args.some((arg) => arg === '--test' || arg.startsWith('--test=')) || args.some((arg) => /\.(?:mjs|cjs)$/iu.test(arg));
495
+ if ((nodeCmd && nodeTestArgs) || BWORKSPACE_TEST_RUNNER_JS_B_U_REGEX.test(text) || /\bnode\s+[^\n]*--test\b/u.test(text) || /\bpnpm\s+-r\s+test\b/u.test(text))
496
+ return 'node';
497
+ return 'unknown';
498
+ }
499
+ /**
500
+ * Removes configured Node test reporters so the shared reporter remains authoritative.
501
+ *
502
+ * @param args Selected Node or package-manager arguments.
503
+ *
504
+ * @return Arguments without CLI reporter selections or their destination values.
505
+ */
506
+ function withoutNodeReporterArgs(args) {
507
+ const retained = [];
508
+ for (let idx = 0; idx < args.length; idx++) {
509
+ const arg = args[idx];
510
+ if (arg === '--test-reporter' || arg === '--test-reporter-destination') {
511
+ idx++;
512
+ continue;
513
+ }
514
+ if (arg.startsWith('--test-reporter=') || arg.startsWith('--test-reporter-destination='))
515
+ continue;
516
+ retained.push(arg);
517
+ }
518
+ return retained;
519
+ }
520
+ /**
521
+ * Removes duplicate configured test invocations while preserving the first selected name.
522
+ *
523
+ * @param checks Configured commands in their source order.
524
+ *
525
+ * @return Commands with repeated executable-and-argument vectors removed.
526
+ */
527
+ function uniqueTestSuites(checks) {
528
+ const seen = new Set();
529
+ return checks.filter((check) => {
530
+ const key = JSON.stringify([check.command, check.args ?? []]);
531
+ if (seen.has(key))
532
+ return false;
533
+ seen.add(key);
534
+ return true;
535
+ });
536
+ }
241
537
  /**
242
538
  * Converts tool output into compact single-line messages without terminal control codes.
243
539
  *
@@ -290,6 +586,143 @@ function run(cfg, cmd, args, opts = {}) {
290
586
  console.error(` ${cmd} failed: ${result.error.message}`);
291
587
  return result.status ?? 1;
292
588
  }
589
+ /**
590
+ * Runs one selected test command with shared case reporting when its runner is known.
591
+ *
592
+ * @param cfg Quality configuration defining the repository root.
593
+ * @param check Test command selected by quality.json.
594
+ * @param completed One-based completed-suite count.
595
+ * @param total Number of selected, deduplicated suites.
596
+ * @param lastGroup Previously printed test category, if any.
597
+ *
598
+ * @return Updated group label and the command exit status.
599
+ */
600
+ function runTestSuite(cfg, check, completed, total, lastGroup) {
601
+ const group = testGroup(cfg, check);
602
+ if (group !== lastGroup)
603
+ console.error(`\n ${group}`);
604
+ const runner = testRunner(cfg, check);
605
+ const args = [...(check.args ?? [])];
606
+ const env = { ...process.env };
607
+ let inheritOutput = runner !== 'unknown';
608
+ if (runner === 'node') {
609
+ const reporterUrl = new URL('./node-reporter.js', import.meta.url).href;
610
+ env.NODE_OPTIONS = qualityNodeReporterOptions(env.NODE_OPTIONS, reporterUrl);
611
+ env.TERRA_QUALITY_NODE_REPORTER_OWNER = '1';
612
+ args.splice(0, args.length, ...withoutNodeReporterArgs(args));
613
+ }
614
+ else if (runner === 'playwright') {
615
+ const reporterPath = resolve(dirname(fileURLToPath(import.meta.url)), 'playwright-reporter.js');
616
+ const reporterArg = `--reporter=${reporterPath}`;
617
+ const selectedArgs = [];
618
+ for (let idx = 0; idx < args.length; idx++) {
619
+ const arg = args[idx];
620
+ if (arg === '--reporter') {
621
+ idx++;
622
+ continue;
623
+ }
624
+ if (!arg.startsWith('--reporter='))
625
+ selectedArgs.push(arg);
626
+ }
627
+ args.splice(0, args.length, ...selectedArgs);
628
+ const packageScript = /\b(?:pnpm|npm|yarn)\b/u.test(check.command) && args.some((arg) => arg === 'run' || arg === 'run-script');
629
+ if (packageScript) {
630
+ if (!args.includes('--'))
631
+ args.push('--');
632
+ args.push(reporterArg);
633
+ }
634
+ else {
635
+ const separator = args.indexOf('--');
636
+ if (separator < 0)
637
+ args.push(reporterArg);
638
+ else
639
+ args.splice(separator, 0, reporterArg);
640
+ }
641
+ }
642
+ else {
643
+ inheritOutput = false;
644
+ }
645
+ qualityHeader(`command:${check.name}`, `Running ${check.command} ${(check.args ?? []).join(' ')}`.trim());
646
+ const result = spawnSync(check.command, args, { cwd: cfg.root, stdio: inheritOutput ? 'inherit' : ['inherit', 'pipe', 'pipe'], env, maxBuffer: TOOL_OUTPUT_MAX_BUFFER });
647
+ if (!inheritOutput) {
648
+ printChildOutput(result.stdout);
649
+ printChildOutput(result.stderr);
650
+ }
651
+ if (result.error)
652
+ console.error(` ${check.command} failed: ${result.error.message}`);
653
+ const status = result.status ?? 1;
654
+ if (reportStages && reportStageName) {
655
+ const stage = reportStages.get(reportStageName);
656
+ if (stage && status)
657
+ stage.status = 'failed';
658
+ }
659
+ if (status)
660
+ process.exitCode = status;
661
+ console.error(` ${status ? '✗ failed' : '✓ passed'}`);
662
+ qualityProgress('Test suites', completed, total);
663
+ return { group, status };
664
+ }
665
+ /**
666
+ * Runs configured checks assigned to one fixed phase.
667
+ *
668
+ * @param cfg Quality configuration containing selected repository commands.
669
+ * @param phase Fixed Quality runner phase to execute.
670
+ *
671
+ * @return Number of commands that failed in this phase.
672
+ */
673
+ function runQualityPhase(cfg, phase) {
674
+ const cmds = (cfg.qualityCommands ?? []).filter((check) => qualityCmdEnabled(cfg, check.name) && phaseForInvocation(cfg, check) === phase);
675
+ let failures = 0;
676
+ let lastGroup;
677
+ if (phase === 'Tests') {
678
+ const suites = uniqueTestSuites(cmds);
679
+ if (!suites.length) {
680
+ console.error(' No configured test suites.');
681
+ return 0;
682
+ }
683
+ const groupOrder = ['Unit / Contract', 'Integration', 'Browser / Cloud', 'Other tests'];
684
+ suites.sort((left, right) => groupOrder.indexOf(testGroup(cfg, left)) - groupOrder.indexOf(testGroup(cfg, right)));
685
+ qualityProgress('Test suites', 0, suites.length);
686
+ console.error('');
687
+ for (const [idx, check] of suites.entries()) {
688
+ const result = runTestSuite(cfg, check, idx + 1, suites.length, lastGroup);
689
+ lastGroup = result.group;
690
+ if (result.status)
691
+ failures += 1;
692
+ }
693
+ return failures;
694
+ }
695
+ for (const check of cmds) {
696
+ qualityHeader(`command:${check.name}`, `Running ${check.command} ${(check.args ?? []).join(' ')}`.trim());
697
+ const status = run(cfg, check.command, check.args ?? []);
698
+ if (status) {
699
+ failures += 1;
700
+ process.exitCode = status;
701
+ }
702
+ console.error(` ${status ? '✗ failed' : '✓ passed'}`);
703
+ }
704
+ return failures;
705
+ }
706
+ /**
707
+ * Identifies an exact configured Git whitespace check.
708
+ *
709
+ * @param check Configured repository-health command.
710
+ *
711
+ * @return Whether the invocation duplicates the built-in `git diff --check`.
712
+ */
713
+ function isGitDiffCheck(check) {
714
+ return basename(check.command).toLowerCase() === 'git' && check.args?.length === 2 && check.args[0] === 'diff' && check.args[1] === '--check';
715
+ }
716
+ /**
717
+ * Detects whether an enabled repository-health command already performs Git's diff check.
718
+ *
719
+ * @param cfg Normalized repository configuration.
720
+ *
721
+ * @return Whether a selected exact Git diff check is present.
722
+ */
723
+ function hasCfgdGitDiffCheck(cfg) {
724
+ return (cfg.qualityCommands ?? []).some((check) => qualityCmdEnabled(cfg, check.name) && isGitDiffCheck(check));
725
+ }
293
726
  /**
294
727
  * Captures source text so automatic tools can report changed-file counts without
295
728
  * replaying their successful per-file output.
@@ -823,18 +1256,6 @@ function runCfgdCmd(cfg, name) {
823
1256
  process.exitCode = status;
824
1257
  console.error(` ${status ? '✗ failed' : '✓ passed'}`);
825
1258
  }
826
- /**
827
- * Runs every enabled configured quality command once.
828
- *
829
- * @param cfg Quality configuration containing repository commands.
830
- *
831
- * @return Nothing; command failures are reflected in the process exit code.
832
- */
833
- function runCfgdCmds(cfg) {
834
- for (const check of cfg.qualityCommands ?? [])
835
- if (qualityCmdEnabled(cfg, check.name))
836
- runCfgdCmd(cfg, check.name);
837
- }
838
1259
  /**
839
1260
  * Runs configured advisory checks after Knip without turning recommendations into failures.
840
1261
  *
@@ -1383,7 +1804,7 @@ function printDeadCodeFindings(findings, manualWarnings, typeSafetyVerified = tr
1383
1804
  const errs = findings.filter((finding) => finding.severity === 'error' || manualWarnings === 'error');
1384
1805
  const warnings = findings.filter((finding) => finding.severity === 'warning' && manualWarnings !== 'error');
1385
1806
  const manualCount = findings.filter((finding) => finding.severity === 'warning').length;
1386
- for (const finding of findings) {
1807
+ for (const finding of [...findings].sort((left, right) => left.file.localeCompare(right.file) || left.line - right.line || left.rule.localeCompare(right.rule) || left.name.localeCompare(right.name))) {
1387
1808
  const severity = finding.severity === 'warning' && manualWarnings === 'error' ? 'ERROR' : finding.severity.toUpperCase();
1388
1809
  console.error(` - [${severity}] ${finding.rule}: ${finding.file}:${finding.line} ${finding.name}`);
1389
1810
  console.error(` Reason: ${finding.reason}`);
@@ -1393,7 +1814,7 @@ function printDeadCodeFindings(findings, manualWarnings, typeSafetyVerified = tr
1393
1814
  }
1394
1815
  console.error(` ${errs.length} error${errs.length === 1 ? '' : 's'}; ${manualCount} manual finding${manualCount === 1 ? '' : 's'}${manualWarnings === 'warning' ? ` (${warnings.length} warning${warnings.length === 1 ? '' : 's'})` : ''}.`);
1395
1816
  if (manualWarnings === 'error' && manualCount > 0)
1396
- console.error(' Manual findings block quality by default. Fix each finding one by one, rerun quality, and continue only when all are resolved before committing.');
1817
+ console.error(' Resolve each manual finding one by one, in the order listed, before committing.');
1397
1818
  if (errs.length)
1398
1819
  process.exitCode = 1;
1399
1820
  }
@@ -1411,6 +1832,14 @@ function printDeadCodeFindings(findings, manualWarnings, typeSafetyVerified = tr
1411
1832
  function runDeadCode(cfg, fix, all, verify = true, emit = true) {
1412
1833
  if (emit)
1413
1834
  qualityHeader('dead-code', `${fix ? 'Fixing safe cases and checking' : 'Read-only check of'} unused files, exports, declarations, and single-use code`);
1835
+ let verificationStarted = false;
1836
+ const beginVerification = () => {
1837
+ if (verificationStarted)
1838
+ return;
1839
+ verificationStarted = true;
1840
+ if (emit)
1841
+ qualityHeader('dead-code/verification', 'Verify remaining unused files, exports, declarations, and single-use code');
1842
+ };
1414
1843
  const changedFiles = new Set();
1415
1844
  const srcSnapshot = fix ? new Map(filesFor(cfg, true).map((f) => [f, readFileSync(f, 'utf8')])) : new Map();
1416
1845
  const initialInventory = fix ? new Set(srcSnapshot.keys()) : new Set();
@@ -1420,6 +1849,7 @@ function runDeadCode(cfg, fix, all, verify = true, emit = true) {
1420
1849
  if (fix && emit)
1421
1850
  console.error(' TypeScript baseline ready.');
1422
1851
  const typeRejectedFiles = new Set();
1852
+ const singleUseVerificationFindings = [];
1423
1853
  const eslintReportsByPath = new Map();
1424
1854
  const eslintScannedContents = new Map();
1425
1855
  let importedConsumersForRun;
@@ -1437,6 +1867,8 @@ function runDeadCode(cfg, fix, all, verify = true, emit = true) {
1437
1867
  return dirtyFiles.length ? scanEslint(dirtyFiles) : [];
1438
1868
  };
1439
1869
  if (fix) {
1870
+ if (emit)
1871
+ qualityHeader('dead-code/safe-fixes', 'Apply only mechanically proven fixes');
1440
1872
  if (qualityCheckEnabled(cfg, 'knip')) {
1441
1873
  if (emit)
1442
1874
  qualityHeader('dead-code/knip-fix', 'Removing only unambiguous unused export modifiers');
@@ -1445,7 +1877,7 @@ function runDeadCode(cfg, fix, all, verify = true, emit = true) {
1445
1877
  importedConsumersForRun = qualityCheckEnabled(cfg, 'knip') || qualityCheckEnabled(cfg, 'lint') ? importedExportConsumers(cfg) : undefined;
1446
1878
  if (qualityCheckEnabled(cfg, 'lint')) {
1447
1879
  if (emit)
1448
- console.error(' Scanning configured files for safe unused-declaration fixes...');
1880
+ qualityHeader('dead-code/eslint-fix', 'Scan configured files and apply only safe unused-declaration fixes');
1449
1881
  let reportsToFix = scanEslint();
1450
1882
  for (let round = 0; round < 5; round++) {
1451
1883
  if (!reportsToFix.some((report) => report.messages.some((message) => message.ruleId === 'unused-imports/no-unused-vars')))
@@ -1461,12 +1893,12 @@ function runDeadCode(cfg, fix, all, verify = true, emit = true) {
1461
1893
  }
1462
1894
  }
1463
1895
  if (emit)
1464
- console.error(' Checking and inlining safe single-use declarations...');
1896
+ qualityHeader('dead-code/single-use', 'Scan declarations and references, classify candidates, and inline only proven cases');
1465
1897
  const singleUseChangedFiles = new Set();
1466
1898
  fixSingleUseDeclarations(cfg, singleUseChangedFiles, (pass, filesChanged) => {
1467
1899
  if (emit)
1468
1900
  console.error(` Single-use rewrite pass ${pass} starting (${filesChanged} source files changed so far).`);
1469
- });
1901
+ }, singleUseVerificationFindings);
1470
1902
  for (const file of singleUseChangedFiles)
1471
1903
  changedFiles.add(file);
1472
1904
  const currInventory = new Set(filesFor(cfg, true));
@@ -1484,6 +1916,8 @@ function runDeadCode(cfg, fix, all, verify = true, emit = true) {
1484
1916
  if (curr !== original)
1485
1917
  proposed.set(file, curr);
1486
1918
  }
1919
+ if (verify)
1920
+ beginVerification();
1487
1921
  if (proposed.size) {
1488
1922
  if (!typeProgram)
1489
1923
  throw new Error('Dead-code edit validation requires its baseline TypeScript program.');
@@ -1504,11 +1938,14 @@ function runDeadCode(cfg, fix, all, verify = true, emit = true) {
1504
1938
  if (readFileSync(file, 'utf8') !== text)
1505
1939
  writeFileSync(file, text);
1506
1940
  }
1941
+ if (verify)
1942
+ beginVerification();
1507
1943
  if (qualityCheckEnabled(cfg, 'lint'))
1508
1944
  scanDirtyEslintFiles();
1509
1945
  }
1510
1946
  if (!verify)
1511
1947
  return [];
1948
+ beginVerification();
1512
1949
  const findings = [];
1513
1950
  if (qualityCheckEnabled(cfg, 'knip')) {
1514
1951
  if (emit)
@@ -1520,7 +1957,8 @@ function runDeadCode(cfg, fix, all, verify = true, emit = true) {
1520
1957
  scanEslint();
1521
1958
  findings.push(...unusedDeclarationFindings(cfg, [...eslintReportsByPath.values()]));
1522
1959
  }
1523
- for (const single of singleUseFindings(cfg))
1960
+ const finalSingleUseFindings = fix && !typeRejectedFiles.size ? singleUseVerificationFindings : singleUseFindings(cfg);
1961
+ for (const single of finalSingleUseFindings)
1524
1962
  findings.push({
1525
1963
  rule: 'typescript/single-use',
1526
1964
  file: single.file,
@@ -2030,8 +2468,14 @@ export function runQuality(cfg, strict = false, all = true, analysis) {
2030
2468
  const duplicateImports = qualityCheckEnabled(cfg, 'import') ? duplicateImportFindings(ctx.files) : [];
2031
2469
  if (qualityCheckEnabled(cfg, 'import')) {
2032
2470
  qualityHeader('imports', 'Checking local import resolution');
2033
- printFindings(result.imports, (f) => `Unresolved import: ${f.importer} -> ${f.specifier}`);
2034
- printFindings(duplicateImports, (f) => `Duplicate named import: ${f.file}:${f.line} repeats an import from ${f.module}`);
2471
+ if (!result.imports.length && !duplicateImports.length)
2472
+ console.error(' ✓ clean');
2473
+ else {
2474
+ if (result.imports.length)
2475
+ printFindings(result.imports, (f) => `Unresolved import: ${f.importer} -> ${f.specifier}`);
2476
+ if (duplicateImports.length)
2477
+ printFindings(duplicateImports, (f) => `Duplicate named import: ${f.file}:${f.line} repeats an import from ${f.module}`);
2478
+ }
2035
2479
  }
2036
2480
  if (qualityCheckEnabled(cfg, 'name')) {
2037
2481
  qualityHeader('naming', 'Checking concise, consistent symbol names');
@@ -2170,6 +2614,107 @@ export function runQualityCli(cfg, args = process.argv.slice(2)) {
2170
2614
  }
2171
2615
  writeReport();
2172
2616
  }
2617
+ /**
2618
+ * Runs the full quality matrix in package-owned phase order.
2619
+ *
2620
+ * @param cfg Normalized repository configuration.
2621
+ * @param args Validated command arguments.
2622
+ * @param opts Scope and write controls parsed by the CLI.
2623
+ *
2624
+ * @return A promise only when the asynchronous Tailwind check is selected.
2625
+ */
2626
+ function runFullQuality(cfg, args, opts) {
2627
+ const fix = (check) => qualityAutofixEnabled(cfg, check, opts.fix);
2628
+ const scoped = scopedCfg(cfg, opts.all);
2629
+ if (opts.fix) {
2630
+ outputPhase = 'PREPARING SAFE FIXES';
2631
+ console.error('\nPREPARING SAFE FIXES');
2632
+ }
2633
+ if (opts.fix && qualityCheckEnabled(cfg, 'name') && fix('name')) {
2634
+ const namingScope = scopedCfg(cfg, opts.all);
2635
+ if (namingFindings(namingScope).length)
2636
+ fixNames(cfg, opts.all);
2637
+ }
2638
+ if (fix('forward') && qualityCheckEnabled(cfg, 'forward')) {
2639
+ const result = fixObjectForwarding(scoped);
2640
+ if (result.fixed || result.skipped)
2641
+ console.info(`Forwarding fixer: ${result.fixed} exact duplicates removed${result.skipped ? `; ${result.skipped} advisory cases retained` : ''}.`);
2642
+ }
2643
+ if (fix('repeatedClasses') && qualityCheckEnabled(cfg, 'repeated-classes')) {
2644
+ const result = fixRepeatedClasses(scoped);
2645
+ if (result.fixed || result.skipped)
2646
+ console.info(`Class fixer: ${result.fixed} attributes replaced${result.skipped ? `; ${result.skipped} advisory cases retained` : ''}.`);
2647
+ }
2648
+ if (fix('regexDedup') && qualityCheckEnabled(cfg, 'regex-dedup')) {
2649
+ const result = fixRegexDedup(scoped);
2650
+ if (result.fixed || result.skipped)
2651
+ console.info(`Regex fixer: ${result.fixed} literals replaced${result.skipped ? `; ${result.skipped} advisory cases retained` : ''}.`);
2652
+ }
2653
+ if (fix('lint') && qualityCheckEnabled(cfg, 'lint'))
2654
+ runLint(cfg, { ...opts, fix: true }, true, true);
2655
+ if (opts.fix && qualityCheckEnabled(cfg, 'alias'))
2656
+ aliasFindings(scoped, true);
2657
+ if (qualityCheckEnabled(cfg, 'document')) {
2658
+ const documentationIssues = documentationFindings(scopedCfg(cfg, opts.all)).filter((finding) => finding.severity === 'error');
2659
+ if (documentationIssues.length)
2660
+ runDocumentation(cfg, opts, ['document', ...new Set(documentationIssues.map((finding) => resolve(cfg.root, finding.file))), '--quiet'], true);
2661
+ }
2662
+ if (opts.fix && qualityCheckEnabled(cfg, 'import') && qualityAutofixEnabled(cfg, 'document', opts.fix)) {
2663
+ const repeated = duplicateImportFindings(filesFor(cfg, opts.all));
2664
+ for (const file of new Set(repeated.map((finding) => finding.file)))
2665
+ maintainSrcFile(cfg.root, file);
2666
+ }
2667
+ outputPhase = undefined;
2668
+ beginQualityPhase('Format');
2669
+ if (qualityCheckEnabled(cfg, 'format'))
2670
+ runFormatter(cfg, { ...opts, fix: fix('format') }, opts.all ? [] : filesFor(cfg, false));
2671
+ if (!opts.skipCommands)
2672
+ runQualityPhase(cfg, 'Format');
2673
+ beginQualityPhase('Static quality');
2674
+ const continueStaticAndLater = () => {
2675
+ if (qualityCheckEnabled(cfg, 'dead-code'))
2676
+ runDeadCodeCmd(cfg, qualityAutofixEnabled(cfg, 'dead-code', opts.fix), opts.all);
2677
+ if (qualityCheckEnabled(cfg, 'lint'))
2678
+ runLint(cfg, { ...opts, fix: false }, true, true);
2679
+ const analysis = createQualityAnalysis(scoped);
2680
+ runGenericChecks(scoped, analysis);
2681
+ runQuality(cfg, opts.strict, opts.all, analysis);
2682
+ if (!opts.skipCommands)
2683
+ runQualityPhase(cfg, 'Static quality');
2684
+ if (!opts.skipRecommendations)
2685
+ runRecommendationCmds(cfg);
2686
+ beginQualityPhase('Tests');
2687
+ const testFailures = opts.skipCommands ? 0 : runQualityPhase(cfg, 'Tests');
2688
+ const testSuites = opts.skipCommands ? 0 : uniqueTestSuites((cfg.qualityCommands ?? []).filter((check) => qualityCmdEnabled(cfg, check.name) && phaseForInvocation(cfg, check) === 'Tests')).length;
2689
+ beginQualityPhase('Framework / type checks');
2690
+ if (qualityCheckEnabled(cfg, 'type') && (cfg.tools?.tsc || cfg.typecheckCommand)) {
2691
+ runTypecheck(cfg);
2692
+ if (cfg.typecheckExtensions?.length && !cfg.typecheckRoots?.length && !cfg.typecheckCommand)
2693
+ runProjectTypecheck(cfg);
2694
+ }
2695
+ if (!opts.skipCommands)
2696
+ runQualityPhase(cfg, 'Framework / type checks');
2697
+ beginQualityPhase('Build');
2698
+ if (!opts.skipCommands)
2699
+ runQualityPhase(cfg, 'Build');
2700
+ beginQualityPhase('Repository health');
2701
+ if (!opts.skipCommands)
2702
+ runQualityPhase(cfg, 'Repository health');
2703
+ if (qualityCheckEnabled(cfg, 'diff') && !hasCfgdGitDiffCheck(cfg))
2704
+ runDiffCheck(cfg);
2705
+ if (qualityCheckEnabled(cfg, 'github') && cfg.github?.repositories?.length)
2706
+ runGitHubCheck(cfg, [], false, args.includes('--warn'));
2707
+ console.error(`\nSUMMARY checks complete · ${testSuites} suites · ${testFailures} failed ${testFailures === 1 ? 'suite' : 'suites'} · ${process.exitCode ? 'checks failed' : 'all checks passed'}`);
2708
+ outputPhase = undefined;
2709
+ };
2710
+ if (cfg.tailwindCanonical && qualityCheckEnabled(cfg, 'tailwind-canonical'))
2711
+ return runTailwindCanonicalCheck(cfg, fix('tailwindCanonical')).then(continueStaticAndLater, (error) => {
2712
+ process.exitCode = 1;
2713
+ console.error(` Tailwind canonical check failed: ${error instanceof Error ? error.message : String(error)}`);
2714
+ continueStaticAndLater();
2715
+ });
2716
+ continueStaticAndLater();
2717
+ }
2173
2718
  /**
2174
2719
  * Route validated arguments to the requested quality command.
2175
2720
  *
@@ -2187,7 +2732,7 @@ function dispatchQualityCli(cfg, args) {
2187
2732
  }
2188
2733
  if (args.includes('--help') || args.includes('-h')) {
2189
2734
  console.info('Usage: terra-quality <command> [options]');
2190
- console.info('Commands include quality, history, github, document, format, lint, knip, types, single-use, dead-code, tailwind-canonical, security, and domain.');
2735
+ console.info('Commands include quality, history, github, document, format, lint, knip, types, single-use, dead-code, tailwind-canonical, security, ui-recommendations, and domain.');
2191
2736
  console.info('Options: --all, --warn, --strict, --skip-commands, --skip-recommendations, --report-json <path>, --repository owner/name; history also accepts --branch <name> and --base-sha <sha>.');
2192
2737
  return;
2193
2738
  }
@@ -2200,7 +2745,10 @@ function dispatchQualityCli(cfg, args) {
2200
2745
  skipRecommendations: args.includes('--skip-recommendations'),
2201
2746
  };
2202
2747
  const fix = (check) => qualityAutofixEnabled(cfg, check, opts.fix);
2203
- if (cfg.tailwindCanonical && qualityCheckEnabled(cfg, 'tailwind-canonical') && ['quality', 'quality:all', 'quality:baseline', 'imports', 'quality:patterns', 'types:architecture'].includes(cmd))
2748
+ if (cfg.tailwindCanonical &&
2749
+ qualityCheckEnabled(cfg, 'tailwind-canonical') &&
2750
+ !['quality', 'quality:all', 'quality:baseline'].includes(cmd) &&
2751
+ ['imports', 'quality:patterns', 'types:architecture'].includes(cmd))
2204
2752
  return runTailwindCanonicalCheck(cfg, fix('tailwindCanonical')).then(() => {
2205
2753
  const checks = { ...cfg.enabledChecks, 'tailwind-canonical': false };
2206
2754
  return dispatchQualityCli({ ...cfg, enabledChecks: checks }, args);
@@ -2285,6 +2833,13 @@ function dispatchQualityCli(cfg, args) {
2285
2833
  process.exitCode = 1;
2286
2834
  return;
2287
2835
  }
2836
+ if (cmd === 'ui-recommendations') {
2837
+ const findings = uiComponentRecommendations(cfg);
2838
+ console.info(`UI component recommendations (${findings.length})`);
2839
+ for (const finding of findings)
2840
+ console.info(` ${finding.file}:${finding.line}:${finding.column} <${finding.element}> → <${finding.component}> (import from '${finding.import}') — ${finding.reason}`);
2841
+ return;
2842
+ }
2288
2843
  if (cfg.qualityCommands?.some((c) => c.name === cmd)) {
2289
2844
  runCfgdCmd(cfg, cmd);
2290
2845
  return;
@@ -2307,69 +2862,8 @@ function dispatchQualityCli(cfg, args) {
2307
2862
  runQuality(cfg, opts.strict, opts.all);
2308
2863
  return;
2309
2864
  }
2310
- if (cmd === 'quality' || cmd === 'quality:all' || cmd === 'quality:baseline') {
2311
- if (opts.fix && qualityCheckEnabled(cfg, 'alias'))
2312
- aliasFindings(cfg, true);
2313
- if (fix('name') && qualityCheckEnabled(cfg, 'name')) {
2314
- const namingScope = scopedCfg(cfg, opts.all);
2315
- if (namingFindings(namingScope).length)
2316
- fixNames(cfg, opts.all);
2317
- }
2318
- const scoped = scopedCfg(cfg, opts.all);
2319
- if (fix('forward') && qualityCheckEnabled(cfg, 'forward')) {
2320
- const result = fixObjectForwarding(scoped);
2321
- if (result.fixed || result.skipped)
2322
- console.info(`Forwarding fixer: ${result.fixed} exact duplicates removed${result.skipped ? `; ${result.skipped} advisory cases retained` : ''}.`);
2323
- }
2324
- if (fix('repeatedClasses') && qualityCheckEnabled(cfg, 'repeated-classes')) {
2325
- const result = fixRepeatedClasses(scoped);
2326
- if (result.fixed || result.skipped)
2327
- console.info(`Class fixer: ${result.fixed} attributes replaced${result.skipped ? `; ${result.skipped} advisory cases retained` : ''}.`);
2328
- }
2329
- if (fix('regexDedup') && qualityCheckEnabled(cfg, 'regex-dedup')) {
2330
- const result = fixRegexDedup(scoped);
2331
- if (result.fixed || result.skipped)
2332
- console.info(`Regex fixer: ${result.fixed} literals replaced${result.skipped ? `; ${result.skipped} advisory cases retained` : ''}.`);
2333
- }
2334
- if (fix('lint') && qualityCheckEnabled(cfg, 'lint'))
2335
- runLint(cfg, { ...opts, fix: true }, true, true);
2336
- if (qualityCheckEnabled(cfg, 'document')) {
2337
- const documentationIssues = documentationFindings(scopedCfg(cfg, opts.all)).filter((f) => f.severity === 'error');
2338
- if (documentationIssues.length)
2339
- runDocumentation(cfg, opts, ['document', ...new Set(documentationIssues.map((f) => resolve(cfg.root, f.file))), '--quiet'], true);
2340
- }
2341
- if (fix('format') && qualityCheckEnabled(cfg, 'format'))
2342
- runFormatter(cfg, { ...opts, fix: true }, opts.all ? [] : filesFor(cfg, false));
2343
- if (opts.fix && qualityCheckEnabled(cfg, 'alias'))
2344
- aliasFindings(scoped, true);
2345
- if (qualityCheckEnabled(cfg, 'dead-code'))
2346
- runDeadCodeCmd(cfg, qualityAutofixEnabled(cfg, 'dead-code', opts.fix), opts.all);
2347
- if (qualityCheckEnabled(cfg, 'lint'))
2348
- runLint(cfg, { ...opts, fix: false }, true, true);
2349
- const analysis = createQualityAnalysis(scoped);
2350
- runGenericChecks(scoped, analysis);
2351
- if (qualityCheckEnabled(cfg, 'type') && cfg.tools?.tsc) {
2352
- runTypecheck(cfg);
2353
- if (cfg.typecheckExtensions?.length && !cfg.typecheckRoots?.length)
2354
- runProjectTypecheck(cfg);
2355
- }
2356
- if (!opts.skipCommands)
2357
- runCfgdCmds(cfg);
2358
- if (!opts.skipRecommendations)
2359
- runRecommendationCmds(cfg);
2360
- if (opts.fix && qualityCheckEnabled(cfg, 'import') && qualityAutofixEnabled(cfg, 'document', opts.fix)) {
2361
- const repeated = duplicateImportFindings(filesFor(cfg, opts.all));
2362
- for (const file of new Set(repeated.map((finding) => finding.file)))
2363
- maintainSrcFile(cfg.root, file);
2364
- }
2365
- runQuality(cfg, opts.strict, opts.all, analysis);
2366
- if (qualityCheckEnabled(cfg, 'diff'))
2367
- runDiffCheck(cfg);
2368
- if (opts.fix && qualityCheckEnabled(cfg, 'diff'))
2369
- runDiffCheck(cfg);
2370
- if (qualityCheckEnabled(cfg, 'github') && cfg.github?.repositories?.length)
2371
- runGitHubCheck(cfg, [], false, args.includes('--warn'));
2372
- }
2865
+ if (cmd === 'quality' || cmd === 'quality:all' || cmd === 'quality:baseline')
2866
+ return runFullQuality(cfg, args, opts);
2373
2867
  }
2374
2868
  /**
2375
2869
  * Loads the repository configuration and runs the CLI in one boundary call.