@terra-symposium/standards-quality 0.8.0 → 0.8.1

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