@nerviq/cli 1.29.0 → 1.30.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 (93) hide show
  1. package/CHANGELOG.md +1764 -1493
  2. package/README.md +568 -538
  3. package/SECURITY.md +78 -82
  4. package/bin/cli.js +2838 -2558
  5. package/docs/api-reference.md +356 -356
  6. package/docs/audit-fix.md +109 -0
  7. package/docs/autofix.md +3 -62
  8. package/docs/getting-started.md +1 -1
  9. package/docs/index.html +592 -592
  10. package/docs/integration-contracts.md +287 -287
  11. package/docs/maintenance.md +128 -128
  12. package/docs/new-platform-guide.md +202 -202
  13. package/docs/release-process.md +63 -0
  14. package/docs/shallow-risk.md +244 -244
  15. package/docs/why-nerviq.md +82 -82
  16. package/package.json +75 -67
  17. package/sdk/README.md +12 -3
  18. package/sdk/examples/langchain-integration.md +128 -0
  19. package/sdk/examples/self-governing-agent.js +135 -0
  20. package/sdk/index.d.ts +115 -0
  21. package/sdk/index.js +94 -0
  22. package/sdk/package.json +11 -0
  23. package/src/activity.js +13 -0
  24. package/src/aider/activity.js +226 -226
  25. package/src/aider/context.js +162 -162
  26. package/src/aider/freshness.js +123 -123
  27. package/src/aider/techniques.js +3465 -3465
  28. package/src/audit/layers.js +180 -180
  29. package/src/audit.js +1133 -1032
  30. package/src/auto-suggest.js +9 -2
  31. package/src/behavioral-drift.js +37 -2
  32. package/src/benchmark.js +299 -299
  33. package/src/codex/activity.js +324 -324
  34. package/src/codex/freshness.js +149 -142
  35. package/src/codex/techniques.js +4895 -4895
  36. package/src/context.js +326 -326
  37. package/src/continuous-ops.js +11 -1
  38. package/src/convert.js +340 -340
  39. package/src/copilot/config-parser.js +280 -280
  40. package/src/copilot/context.js +218 -218
  41. package/src/copilot/freshness.js +184 -177
  42. package/src/copilot/patch.js +238 -238
  43. package/src/copilot/techniques.js +3578 -3578
  44. package/src/cursor/freshness.js +194 -194
  45. package/src/cursor/patch.js +243 -243
  46. package/src/cursor/techniques.js +3735 -3735
  47. package/src/doctor.js +201 -201
  48. package/src/fix-engine.js +511 -8
  49. package/src/formatters/csv.js +86 -86
  50. package/src/formatters/junit.js +123 -123
  51. package/src/formatters/markdown.js +164 -164
  52. package/src/formatters/otel.js +151 -151
  53. package/src/freshness.js +163 -156
  54. package/src/gemini/activity.js +402 -402
  55. package/src/gemini/context.js +290 -290
  56. package/src/gemini/freshness.js +188 -188
  57. package/src/gemini/patch.js +229 -229
  58. package/src/gemini/techniques.js +3811 -3811
  59. package/src/governance.js +533 -533
  60. package/src/harmony/audit.js +306 -306
  61. package/src/i18n.js +63 -63
  62. package/src/insights.js +119 -119
  63. package/src/integrations.js +134 -134
  64. package/src/locales/en.json +33 -33
  65. package/src/locales/es.json +33 -33
  66. package/src/migrate.js +354 -354
  67. package/src/opencode/activity.js +286 -286
  68. package/src/opencode/freshness.js +137 -137
  69. package/src/opencode/techniques.js +3450 -3450
  70. package/src/safe-glyph.js +97 -0
  71. package/src/setup/analysis.js +12 -12
  72. package/src/setup.js +13 -6
  73. package/src/shallow-risk/index.js +113 -56
  74. package/src/shallow-risk/patterns/agent-config-cross-platform-drift.js +51 -50
  75. package/src/shallow-risk/patterns/agent-config-dangerous-autoapprove.js +47 -46
  76. package/src/shallow-risk/patterns/agent-config-deprecated-keys.js +47 -46
  77. package/src/shallow-risk/patterns/agent-config-framework-version-mismatch.js +138 -0
  78. package/src/shallow-risk/patterns/agent-config-missing-file.js +318 -317
  79. package/src/shallow-risk/patterns/agent-config-script-not-in-package-json.js +108 -0
  80. package/src/shallow-risk/patterns/agent-config-secret-literal.js +52 -49
  81. package/src/shallow-risk/patterns/agent-config-stack-contradiction.js +35 -34
  82. package/src/shallow-risk/patterns/hook-script-missing.js +71 -70
  83. package/src/shallow-risk/patterns/mcp-server-no-allowlist.js +53 -52
  84. package/src/shallow-risk/shared.js +653 -648
  85. package/src/source-urls.js +295 -295
  86. package/src/state-paths.js +85 -85
  87. package/src/supplemental-checks.js +805 -805
  88. package/src/telemetry.js +160 -160
  89. package/src/watch.js +46 -0
  90. package/src/windsurf/context.js +359 -359
  91. package/src/windsurf/freshness.js +194 -194
  92. package/src/windsurf/patch.js +231 -231
  93. package/src/windsurf/techniques.js +3779 -3779
package/src/fix-engine.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs');
2
2
  const path = require('path');
3
3
  const readline = require('readline');
4
+ const { spawnSync } = require('child_process');
4
5
 
5
6
  const { ProjectContext } = require('./context');
6
7
  const { TECHNIQUES, STACKS } = require('./techniques');
@@ -33,7 +34,10 @@ const CUSTOM_FIXER_KEYS = new Set([
33
34
  'changelog',
34
35
  'contributing',
35
36
  'gitIgnoreEnv',
37
+ 'gitIgnoreClaudeLocal',
38
+ 'gitignoreClaudeLocal',
36
39
  'secretsProtection',
40
+ 'editorconfig',
37
41
  ]);
38
42
 
39
43
  const AUDIT_FIX_KEYS = new Set([
@@ -45,6 +49,11 @@ const AUDIT_FIX_KEYS = new Set([
45
49
  'license',
46
50
  'changelog',
47
51
  'contributing',
52
+ 'gitIgnoreEnv',
53
+ 'gitIgnoreClaudeLocal',
54
+ 'gitignoreClaudeLocal',
55
+ 'secretsProtection',
56
+ 'editorconfig',
48
57
  ]);
49
58
 
50
59
  const INSTRUCTION_KEYS = new Set([
@@ -62,6 +71,25 @@ const QUALITY_COMMAND_KEYS = new Set([
62
71
  'buildCommand',
63
72
  ]);
64
73
 
74
+ const AUDIT_FIX_ALLOWED_PATHS = new Set([
75
+ '.claude/CLAUDE.md',
76
+ '.claude/settings.json',
77
+ '.editorconfig',
78
+ '.gitignore',
79
+ '.codex/AGENTS.md',
80
+ 'AGENTS.md',
81
+ 'CHANGELOG.md',
82
+ 'CLAUDE.md',
83
+ 'CONTRIBUTING.md',
84
+ 'LICENSE',
85
+ ]);
86
+
87
+ const GITIGNORE_ENTRIES_BY_KEY = {
88
+ gitIgnoreEnv: ['.env', '.env.*'],
89
+ gitIgnoreClaudeLocal: ['.claude/settings.local.json'],
90
+ gitignoreClaudeLocal: ['CLAUDE.local.md'],
91
+ };
92
+
65
93
  function normalizeNewlines(content) {
66
94
  return String(content || '').replace(/\r\n/g, '\n');
67
95
  }
@@ -301,6 +329,23 @@ function buildContributingTemplate(ctx, commands) {
301
329
  ].join('\n');
302
330
  }
303
331
 
332
+ function buildEditorConfigTemplate() {
333
+ return [
334
+ 'root = true',
335
+ '',
336
+ '[*]',
337
+ 'charset = utf-8',
338
+ 'end_of_line = lf',
339
+ 'indent_style = space',
340
+ 'indent_size = 2',
341
+ 'insert_final_newline = true',
342
+ 'trim_trailing_whitespace = true',
343
+ '',
344
+ '[*.md]',
345
+ 'trim_trailing_whitespace = false',
346
+ ].join('\n');
347
+ }
348
+
304
349
  function buildInstructionOperation({ ctx, stacks, failedByKey, platform, targetKeys }) {
305
350
  const keys = targetKeys.filter((key) => INSTRUCTION_KEYS.has(key));
306
351
  if (keys.length === 0) return null;
@@ -356,10 +401,20 @@ function buildSimpleCreateOperation(filePath, content, keys, failedByKey) {
356
401
  };
357
402
  }
358
403
 
359
- function buildGitIgnoreOperation(ctx, failedByKey) {
404
+ function buildGitIgnoreOperation(ctx, failedByKey, keys = ['gitIgnoreEnv']) {
360
405
  const existing = ctx.fileContent('.gitignore');
361
406
  const normalized = normalizeNewlines(existing || '');
362
- if (/(^|\n)\.env(\n|$)/.test(normalized) || normalized.includes('.env.*')) {
407
+ const normalizedKeys = unique(keys);
408
+ const entries = unique(normalizedKeys.flatMap((key) => GITIGNORE_ENTRIES_BY_KEY[key] || []));
409
+ const existingEntries = new Set(
410
+ normalized
411
+ .split('\n')
412
+ .map((line) => line.trim())
413
+ .filter((line) => line && !line.startsWith('#')),
414
+ );
415
+ const missingEntries = entries.filter((entry) => !existingEntries.has(entry));
416
+
417
+ if (missingEntries.length === 0) {
363
418
  return null;
364
419
  }
365
420
 
@@ -369,9 +424,9 @@ function buildGitIgnoreOperation(ctx, failedByKey) {
369
424
  path: '.gitignore',
370
425
  action: existing === null ? 'create' : 'patch',
371
426
  before: existing,
372
- after: ensureTrailingNewline(`${normalized}${prefix}.env\n.env.*\n`),
373
- keys: ['gitIgnoreEnv'],
374
- impact: highestImpact(['gitIgnoreEnv'], failedByKey),
427
+ after: ensureTrailingNewline(`${normalized}${prefix}${missingEntries.join('\n')}\n`),
428
+ keys: normalizedKeys,
429
+ impact: highestImpact(normalizedKeys, failedByKey),
375
430
  };
376
431
  }
377
432
 
@@ -389,7 +444,14 @@ function buildSecretsProtectionOperation(ctx, failedByKey) {
389
444
 
390
445
  settings.permissions = settings.permissions || {};
391
446
  settings.permissions.deny = Array.isArray(settings.permissions.deny) ? settings.permissions.deny : [];
392
- const denyEntries = ['.env', '.env.*', '**/.env', '**/*.pem', '**/secrets/**'];
447
+ const denyEntries = [
448
+ 'Read(.env)',
449
+ 'Read(.env.*)',
450
+ 'Read(**/.env)',
451
+ 'Read(**/.env.*)',
452
+ 'Read(**/*.pem)',
453
+ 'Read(**/secrets/**)',
454
+ ];
393
455
  for (const entry of denyEntries) {
394
456
  if (!settings.permissions.deny.includes(entry)) {
395
457
  settings.permissions.deny.push(entry);
@@ -418,6 +480,14 @@ function buildSecretsProtectionOperation(ctx, failedByKey) {
418
480
  };
419
481
  }
420
482
 
483
+ function buildEditorConfigOperation(ctx, failedByKey) {
484
+ if (ctx.fileContent('.editorconfig') !== null) {
485
+ return null;
486
+ }
487
+
488
+ return buildSimpleCreateOperation('.editorconfig', buildEditorConfigTemplate(), ['editorconfig'], failedByKey);
489
+ }
490
+
421
491
  function buildFixPlan({ dir, platform, auditResult, targetKeys }) {
422
492
  const ctx = new ProjectContext(dir);
423
493
  const stacks = ctx.detectStacks(STACKS);
@@ -457,8 +527,10 @@ function buildFixPlan({ dir, platform, auditResult, targetKeys }) {
457
527
  failedByKey,
458
528
  ));
459
529
  }
460
- if (customKeys.includes('gitIgnoreEnv')) {
461
- const gitIgnoreOperation = buildGitIgnoreOperation(ctx, failedByKey);
530
+ const gitIgnoreKeys = ['gitIgnoreEnv', 'gitIgnoreClaudeLocal', 'gitignoreClaudeLocal']
531
+ .filter((key) => customKeys.includes(key));
532
+ if (gitIgnoreKeys.length > 0) {
533
+ const gitIgnoreOperation = buildGitIgnoreOperation(ctx, failedByKey, gitIgnoreKeys);
462
534
  if (gitIgnoreOperation) {
463
535
  operations.push(gitIgnoreOperation);
464
536
  }
@@ -469,6 +541,12 @@ function buildFixPlan({ dir, platform, auditResult, targetKeys }) {
469
541
  operations.push(secretsOperation);
470
542
  }
471
543
  }
544
+ if (customKeys.includes('editorconfig')) {
545
+ const editorconfigOperation = buildEditorConfigOperation(ctx, failedByKey);
546
+ if (editorconfigOperation) {
547
+ operations.push(editorconfigOperation);
548
+ }
549
+ }
472
550
 
473
551
  for (const key of templateKeys) {
474
552
  operations.push({
@@ -486,6 +564,211 @@ function buildFixPlan({ dir, platform, auditResult, targetKeys }) {
486
564
  });
487
565
  }
488
566
 
567
+ function isAuditAllowedPath(filePath) {
568
+ return AUDIT_FIX_ALLOWED_PATHS.has(String(filePath || '').replace(/\\/g, '/'));
569
+ }
570
+
571
+ function normalizeOperationForAudit(operation, failedByKey) {
572
+ if (!operation || operation.type !== 'file' || !isAuditAllowedPath(operation.path)) {
573
+ return null;
574
+ }
575
+
576
+ const evidence = unique((operation.keys || []).map((key) => {
577
+ const failed = failedByKey.get(key);
578
+ const file = failed?.file || operation.path;
579
+ const line = Number.isFinite(failed?.line) ? failed.line : 1;
580
+ return `${key}|${file}|${line}`;
581
+ })).map((entry) => {
582
+ const [key, file, rawLine] = entry.split('|');
583
+ const failed = failedByKey.get(key) || {};
584
+ return {
585
+ key,
586
+ name: failed.name || key,
587
+ fix: failed.fix || null,
588
+ file: file || operation.path,
589
+ line: Number.isFinite(Number(rawLine)) ? Number(rawLine) : 1,
590
+ };
591
+ });
592
+
593
+ const summaryLocation = evidence[0]
594
+ ? `${evidence[0].file}:${evidence[0].line}`
595
+ : `${operation.path}:1`;
596
+
597
+ return {
598
+ ...operation,
599
+ evidence,
600
+ summaryLocation,
601
+ };
602
+ }
603
+
604
+ function buildAuditFixPlan({ dir, platform, auditResult, targetKeys }) {
605
+ const failedResults = ((auditResult && auditResult.results) || []).filter((item) => item && item.passed === false);
606
+ const failedByKey = new Map(failedResults.map((item) => [item.key, item]));
607
+ const requestedKeys = unique(targetKeys).filter((key) => AUDIT_FIX_KEYS.has(key));
608
+ const filePlan = buildFixPlan({
609
+ dir,
610
+ platform,
611
+ auditResult,
612
+ targetKeys: requestedKeys,
613
+ })
614
+ .map((operation) => normalizeOperationForAudit(operation, failedByKey))
615
+ .filter(Boolean);
616
+
617
+ const plannedKeySet = new Set(filePlan.flatMap((operation) => operation.keys || []));
618
+ const advisoryOnly = failedResults
619
+ .filter((item) => !plannedKeySet.has(item.key))
620
+ .map((item) => ({
621
+ key: item.key,
622
+ name: item.name || item.key,
623
+ impact: item.impact || 'medium',
624
+ fix: item.fix || 'Manual fix required.',
625
+ file: item.file || null,
626
+ line: Number.isFinite(item.line) ? item.line : null,
627
+ }))
628
+ .sort(sortFailedResults);
629
+
630
+ return {
631
+ requestedKeys,
632
+ plan: filePlan,
633
+ advisoryOnly,
634
+ failedByKey,
635
+ };
636
+ }
637
+
638
+ function trimTrailingEmptyLine(lines) {
639
+ if (lines.length > 0 && lines[lines.length - 1] === '') {
640
+ return lines.slice(0, -1);
641
+ }
642
+ return lines;
643
+ }
644
+
645
+ function formatUnifiedDiff(operation) {
646
+ const beforeLines = trimTrailingEmptyLine(
647
+ operation.before === null ? [] : normalizeNewlines(operation.before).split('\n'),
648
+ );
649
+ const afterLines = trimTrailingEmptyLine(normalizeNewlines(operation.after).split('\n'));
650
+
651
+ let start = 0;
652
+ while (
653
+ start < beforeLines.length &&
654
+ start < afterLines.length &&
655
+ beforeLines[start] === afterLines[start]
656
+ ) {
657
+ start += 1;
658
+ }
659
+
660
+ let endBefore = beforeLines.length - 1;
661
+ let endAfter = afterLines.length - 1;
662
+ while (
663
+ endBefore >= start &&
664
+ endAfter >= start &&
665
+ beforeLines[endBefore] === afterLines[endAfter]
666
+ ) {
667
+ endBefore -= 1;
668
+ endAfter -= 1;
669
+ }
670
+
671
+ const removed = beforeLines.slice(start, endBefore + 1);
672
+ const added = afterLines.slice(start, endAfter + 1);
673
+ const oldStart = operation.before === null ? 0 : start + 1;
674
+ const oldCount = operation.before === null ? 0 : removed.length;
675
+ const newStart = start + 1;
676
+ const newCount = added.length;
677
+
678
+ return [
679
+ `diff --git a/${operation.path} b/${operation.path}`,
680
+ operation.action === 'create' ? 'new file mode 100644' : null,
681
+ `--- ${operation.before === null ? '/dev/null' : `a/${operation.path}`}`,
682
+ `+++ b/${operation.path}`,
683
+ `@@ -${oldStart},${oldCount} +${newStart},${newCount} @@`,
684
+ ...removed.map((line) => `-${line}`),
685
+ ...added.map((line) => `+${line}`),
686
+ ].filter(Boolean).join('\n');
687
+ }
688
+
689
+ function renderAuditFixPatch(plan) {
690
+ if (!Array.isArray(plan) || plan.length === 0) {
691
+ return '';
692
+ }
693
+ return `${plan.map((operation) => formatUnifiedDiff(operation)).join('\n\n')}\n`;
694
+ }
695
+
696
+ function resolvePatchOutputPath(dir, outputPath) {
697
+ if (outputPath === '-') {
698
+ return '-';
699
+ }
700
+ if (outputPath) {
701
+ return path.isAbsolute(outputPath) ? outputPath : path.join(dir, outputPath);
702
+ }
703
+ return path.join(dir, 'audit-fix.patch');
704
+ }
705
+
706
+ function writeAuditFixPatch({ dir, outputPath, patch, logger }) {
707
+ const targetPath = resolvePatchOutputPath(dir, outputPath);
708
+ if (targetPath === '-') {
709
+ logger.log('');
710
+ logger.log(patch.trimEnd());
711
+ logger.log('');
712
+ return {
713
+ filePath: null,
714
+ relativePath: 'stdout',
715
+ };
716
+ }
717
+
718
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
719
+ fs.writeFileSync(targetPath, patch, 'utf8');
720
+ return {
721
+ filePath: targetPath,
722
+ relativePath: path.relative(dir, targetPath),
723
+ };
724
+ }
725
+
726
+ function formatAuditFixSummary(plan) {
727
+ return plan.map((operation) => {
728
+ const status = operation.action === 'create' ? 'A ' : 'M ';
729
+ const keys = (operation.keys || []).join(', ');
730
+ return ` ${status} ${operation.path} (${operation.summaryLocation}) [${keys}]`;
731
+ });
732
+ }
733
+
734
+ function formatAdvisoryItem(item) {
735
+ const where = item.file ? `${item.file}${item.line ? `:${item.line}` : ''}` : 'repo-level';
736
+ return ` - ${item.key} (${item.impact}) at ${where}: ${item.fix}`;
737
+ }
738
+
739
+ function runGit(args, dir) {
740
+ return spawnSync('git', args, {
741
+ cwd: dir,
742
+ encoding: 'utf8',
743
+ timeout: 30000,
744
+ });
745
+ }
746
+
747
+ function createAuditFixBranch(dir) {
748
+ const stamp = new Date().toISOString().replace(/[-:.TZ]/g, '').toLowerCase();
749
+ const branchName = `nerviq/autofix-${stamp}`;
750
+ let result = runGit(['switch', '-c', branchName], dir);
751
+ if (result.status !== 0) {
752
+ result = runGit(['checkout', '-b', branchName], dir);
753
+ }
754
+ if (result.status !== 0) {
755
+ throw new Error(result.stderr || result.stdout || 'Failed to create autofix branch.');
756
+ }
757
+ return branchName;
758
+ }
759
+
760
+ function stageAuditFixFiles(dir, plan, patchPath) {
761
+ const paths = unique([
762
+ ...plan.map((operation) => operation.path),
763
+ patchPath && patchPath !== 'stdout' ? patchPath : null,
764
+ ]);
765
+ if (paths.length === 0) return;
766
+ const result = runGit(['add', '--', ...paths], dir);
767
+ if (result.status !== 0) {
768
+ throw new Error(result.stderr || result.stdout || 'Failed to stage autofix files.');
769
+ }
770
+ }
771
+
489
772
  function formatDiff(filePath, before, after) {
490
773
  const beforeLines = before === null ? [] : normalizeNewlines(before).split('\n');
491
774
  const afterLines = normalizeNewlines(after).split('\n');
@@ -773,11 +1056,231 @@ async function applyFixes({
773
1056
  };
774
1057
  }
775
1058
 
1059
+ async function runAuditFixWorkflow({
1060
+ dir,
1061
+ platform,
1062
+ auditResult,
1063
+ targetKeys,
1064
+ auto = false,
1065
+ apply = false,
1066
+ pr = false,
1067
+ outputPath = null,
1068
+ logger = console,
1069
+ }) {
1070
+ const { requestedKeys, plan, advisoryOnly, failedByKey } = buildAuditFixPlan({
1071
+ dir,
1072
+ platform,
1073
+ auditResult,
1074
+ targetKeys,
1075
+ });
1076
+
1077
+ if (requestedKeys.length === 0 || plan.length === 0) {
1078
+ logger.log('');
1079
+ logger.log(' No deterministic audit autofixes are available for this repo.');
1080
+ if (advisoryOnly.length > 0) {
1081
+ logger.log(' Advisory only — manual fix required:');
1082
+ for (const item of advisoryOnly.slice(0, 12)) {
1083
+ logger.log(formatAdvisoryItem(item));
1084
+ }
1085
+ if (advisoryOnly.length > 12) {
1086
+ logger.log(` ... and ${advisoryOnly.length - 12} more advisory findings.`);
1087
+ }
1088
+ }
1089
+ logger.log('');
1090
+ return {
1091
+ exitCode: 2,
1092
+ requestedKeys,
1093
+ plan,
1094
+ advisoryOnly,
1095
+ patchArtifact: null,
1096
+ rollbackArtifact: null,
1097
+ reAudit: auditResult,
1098
+ unresolvedKeys: [],
1099
+ branchName: null,
1100
+ };
1101
+ }
1102
+
1103
+ if (apply && !auto && !pr) {
1104
+ logger.error('\n Error: `nerviq audit --fix --apply` requires `--auto`.\n');
1105
+ return {
1106
+ exitCode: 2,
1107
+ requestedKeys,
1108
+ plan,
1109
+ advisoryOnly,
1110
+ patchArtifact: null,
1111
+ rollbackArtifact: null,
1112
+ reAudit: auditResult,
1113
+ unresolvedKeys: [],
1114
+ branchName: null,
1115
+ };
1116
+ }
1117
+
1118
+ const patch = renderAuditFixPatch(plan);
1119
+ const patchArtifact = writeAuditFixPatch({
1120
+ dir,
1121
+ outputPath,
1122
+ patch,
1123
+ logger,
1124
+ });
1125
+
1126
+ logger.log('');
1127
+ logger.log(' Audit autofix plan');
1128
+ logger.log(' ═══════════════════════════════════════');
1129
+ for (const line of formatAuditFixSummary(plan)) {
1130
+ logger.log(line);
1131
+ }
1132
+ logger.log('');
1133
+ logger.log(` Patch: ${patchArtifact.relativePath}`);
1134
+ if (advisoryOnly.length > 0) {
1135
+ logger.log('');
1136
+ logger.log(' Advisory only — manual fix required:');
1137
+ for (const item of advisoryOnly.slice(0, 12)) {
1138
+ logger.log(formatAdvisoryItem(item));
1139
+ }
1140
+ if (advisoryOnly.length > 12) {
1141
+ logger.log(` ... and ${advisoryOnly.length - 12} more advisory findings.`);
1142
+ }
1143
+ }
1144
+
1145
+ if (!apply && !pr) {
1146
+ logger.log('');
1147
+ logger.log(' Dry run complete. No files were written.');
1148
+ logger.log(' Run `nerviq audit --fix --apply --auto` to apply these changes.');
1149
+ logger.log(' Run `nerviq audit --fix --pr` to create a local autofix branch and stage the files.');
1150
+ logger.log('');
1151
+ return {
1152
+ exitCode: 0,
1153
+ requestedKeys,
1154
+ plan,
1155
+ advisoryOnly,
1156
+ patchArtifact,
1157
+ rollbackArtifact: null,
1158
+ reAudit: auditResult,
1159
+ unresolvedKeys: [],
1160
+ branchName: null,
1161
+ };
1162
+ }
1163
+
1164
+ let branchName = null;
1165
+ const createdFiles = [];
1166
+ const patchedFiles = [];
1167
+ const warnings = [];
1168
+
1169
+ try {
1170
+ if (pr) {
1171
+ const repoCheck = runGit(['rev-parse', '--is-inside-work-tree'], dir);
1172
+ if (repoCheck.status !== 0) {
1173
+ throw new Error('`--pr` requires a git repository.');
1174
+ }
1175
+ branchName = createAuditFixBranch(dir);
1176
+ }
1177
+
1178
+ for (const operation of plan) {
1179
+ if (hasDoNotAutoEditMarker(operation.before)) {
1180
+ const warning = `Skipped ${operation.path}: DO NOT AUTOEDIT marker found.`;
1181
+ warnings.push(warning);
1182
+ logger.warn(` Warning: ${warning}`);
1183
+ continue;
1184
+ }
1185
+
1186
+ if (!isAuditAllowedPath(operation.path)) {
1187
+ const warning = `Skipped ${operation.path}: outside audit autofix allowlist.`;
1188
+ warnings.push(warning);
1189
+ logger.warn(` Warning: ${warning}`);
1190
+ continue;
1191
+ }
1192
+
1193
+ const fullPath = path.join(dir, operation.path);
1194
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
1195
+ fs.writeFileSync(fullPath, operation.after, 'utf8');
1196
+ if (operation.action === 'create') {
1197
+ createdFiles.push(operation.path);
1198
+ } else {
1199
+ patchedFiles.push({ path: operation.path, previousContent: operation.before });
1200
+ }
1201
+ logger.log(` Applied ${operation.action === 'create' ? 'create' : 'patch'}: ${operation.path}`);
1202
+ }
1203
+ } catch (error) {
1204
+ const rollbackArtifact = createRollbackArtifact(dir, createdFiles, patchedFiles, 'audit-fix');
1205
+ logger.error(`\n Error: ${error.message}\n`);
1206
+ return {
1207
+ exitCode: 1,
1208
+ requestedKeys,
1209
+ plan,
1210
+ advisoryOnly,
1211
+ patchArtifact,
1212
+ rollbackArtifact,
1213
+ reAudit: auditResult,
1214
+ unresolvedKeys: requestedKeys,
1215
+ branchName,
1216
+ warnings,
1217
+ };
1218
+ }
1219
+
1220
+ const rollbackArtifact = createRollbackArtifact(dir, createdFiles, patchedFiles, 'audit-fix');
1221
+ const reAudit = await audit({ dir, platform, silent: true });
1222
+ const unresolvedKeys = requestedKeys.filter((key) => {
1223
+ const planned = plan.some((operation) => (operation.keys || []).includes(key));
1224
+ if (!planned) return false;
1225
+ const failed = failedByKey.get(key);
1226
+ if (!failed) return false;
1227
+ const check = (reAudit.results || []).find((item) => item.key === key);
1228
+ return !check || check.passed !== true;
1229
+ });
1230
+
1231
+ if (pr) {
1232
+ stageAuditFixFiles(dir, plan, patchArtifact.relativePath);
1233
+ }
1234
+
1235
+ logger.log('');
1236
+ logger.log(` Re-audit score: ${auditResult.score} -> ${reAudit.score}`);
1237
+ if (rollbackArtifact) {
1238
+ logger.log(` Rollback: ${rollbackArtifact.relativePath}`);
1239
+ }
1240
+ if (branchName) {
1241
+ logger.log(` Branch: ${branchName}`);
1242
+ logger.log(' Files are staged for review.');
1243
+ }
1244
+ if (unresolvedKeys.length > 0) {
1245
+ logger.log(` Unresolved targeted checks: ${unresolvedKeys.join(', ')}`);
1246
+ logger.log('');
1247
+ return {
1248
+ exitCode: 1,
1249
+ requestedKeys,
1250
+ plan,
1251
+ advisoryOnly,
1252
+ patchArtifact,
1253
+ rollbackArtifact,
1254
+ reAudit,
1255
+ unresolvedKeys,
1256
+ branchName,
1257
+ warnings,
1258
+ };
1259
+ }
1260
+
1261
+ logger.log(' Audit autofix completed successfully.');
1262
+ logger.log('');
1263
+ return {
1264
+ exitCode: 0,
1265
+ requestedKeys,
1266
+ plan,
1267
+ advisoryOnly,
1268
+ patchArtifact,
1269
+ rollbackArtifact,
1270
+ reAudit,
1271
+ unresolvedKeys,
1272
+ branchName,
1273
+ warnings,
1274
+ };
1275
+ }
1276
+
776
1277
  module.exports = {
777
1278
  AUDIT_FIX_KEYS,
778
1279
  CUSTOM_FIXER_KEYS,
779
1280
  applyFixes,
780
1281
  buildFixPlan,
1282
+ buildAuditFixPlan,
781
1283
  getFixableFailedResults,
782
1284
  isFixableKey,
1285
+ runAuditFixWorkflow,
783
1286
  };