@eduardbar/drift 1.3.0 → 1.5.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 (198) hide show
  1. package/.gga +50 -0
  2. package/.github/actions/drift-review/README.md +62 -0
  3. package/.github/actions/drift-review/action.yml +148 -0
  4. package/.github/actions/drift-scan/README.md +28 -32
  5. package/.github/actions/drift-scan/action.yml +78 -14
  6. package/.github/workflows/publish-vscode.yml +1 -3
  7. package/.github/workflows/publish.yml +8 -0
  8. package/.github/workflows/quality.yml +15 -0
  9. package/.github/workflows/reusable-quality-checks.yml +95 -0
  10. package/.github/workflows/review-pr.yml +33 -41
  11. package/AGENTS.md +75 -251
  12. package/CHANGELOG.md +41 -0
  13. package/README.md +177 -43
  14. package/benchmarks/fixtures/critical/drift.config.ts +21 -0
  15. package/benchmarks/fixtures/critical/src/app/user-service.ts +30 -0
  16. package/benchmarks/fixtures/critical/src/domain/entities.ts +19 -0
  17. package/benchmarks/fixtures/critical/src/domain/policies.ts +22 -0
  18. package/benchmarks/fixtures/critical/src/index.ts +10 -0
  19. package/benchmarks/fixtures/critical/src/infra/memory-user-repo.ts +14 -0
  20. package/benchmarks/perf-budget.v1.json +27 -0
  21. package/dist/benchmark.d.ts +1 -1
  22. package/dist/benchmark.js +83 -52
  23. package/dist/cli.js +243 -8
  24. package/dist/config.js +16 -2
  25. package/dist/diff.js +42 -50
  26. package/dist/doctor.d.ts +26 -0
  27. package/dist/doctor.js +140 -0
  28. package/dist/format.d.ts +17 -0
  29. package/dist/format.js +45 -0
  30. package/dist/guard-baseline.d.ts +12 -0
  31. package/dist/guard-baseline.js +57 -0
  32. package/dist/guard-metrics.d.ts +6 -0
  33. package/dist/guard-metrics.js +39 -0
  34. package/dist/guard-types.d.ts +58 -0
  35. package/dist/guard-types.js +2 -0
  36. package/dist/guard.d.ts +16 -0
  37. package/dist/guard.js +178 -0
  38. package/dist/index.d.ts +10 -3
  39. package/dist/index.js +4 -1
  40. package/dist/init.d.ts +15 -0
  41. package/dist/init.js +273 -0
  42. package/dist/map-cycles.d.ts +2 -0
  43. package/dist/map-cycles.js +34 -0
  44. package/dist/map-svg.d.ts +19 -0
  45. package/dist/map-svg.js +97 -0
  46. package/dist/map.js +78 -138
  47. package/dist/metrics.js +70 -55
  48. package/dist/output-metadata.d.ts +15 -0
  49. package/dist/output-metadata.js +19 -0
  50. package/dist/plugins-capabilities.d.ts +4 -0
  51. package/dist/plugins-capabilities.js +21 -0
  52. package/dist/plugins-messages.d.ts +10 -0
  53. package/dist/plugins-messages.js +16 -0
  54. package/dist/plugins-rules.d.ts +9 -0
  55. package/dist/plugins-rules.js +137 -0
  56. package/dist/plugins.d.ts +1 -1
  57. package/dist/plugins.js +45 -142
  58. package/dist/reporter-constants.d.ts +16 -0
  59. package/dist/reporter-constants.js +39 -0
  60. package/dist/reporter.d.ts +3 -3
  61. package/dist/reporter.js +35 -55
  62. package/dist/review.d.ts +2 -1
  63. package/dist/review.js +2 -1
  64. package/dist/rules/phase3-configurable.js +23 -15
  65. package/dist/saas/constants.d.ts +15 -0
  66. package/dist/saas/constants.js +48 -0
  67. package/dist/saas/dashboard.d.ts +8 -0
  68. package/dist/saas/dashboard.js +132 -0
  69. package/dist/saas/errors.d.ts +19 -0
  70. package/dist/saas/errors.js +37 -0
  71. package/dist/saas/helpers.d.ts +21 -0
  72. package/dist/saas/helpers.js +110 -0
  73. package/dist/saas/ingest.d.ts +3 -0
  74. package/dist/saas/ingest.js +249 -0
  75. package/dist/saas/organization.d.ts +5 -0
  76. package/dist/saas/organization.js +82 -0
  77. package/dist/saas/plan-change.d.ts +10 -0
  78. package/dist/saas/plan-change.js +15 -0
  79. package/dist/saas/store.d.ts +21 -0
  80. package/dist/saas/store.js +159 -0
  81. package/dist/saas/types.d.ts +191 -0
  82. package/dist/saas/types.js +2 -0
  83. package/dist/saas.d.ts +8 -218
  84. package/dist/saas.js +7 -761
  85. package/dist/sarif.d.ts +74 -0
  86. package/dist/sarif.js +122 -0
  87. package/dist/trust-advanced.d.ts +14 -0
  88. package/dist/trust-advanced.js +65 -0
  89. package/dist/trust-kpi-fs.d.ts +3 -0
  90. package/dist/trust-kpi-fs.js +141 -0
  91. package/dist/trust-kpi-parse.d.ts +7 -0
  92. package/dist/trust-kpi-parse.js +186 -0
  93. package/dist/trust-kpi-types.d.ts +16 -0
  94. package/dist/trust-kpi-types.js +2 -0
  95. package/dist/trust-kpi.d.ts +1 -3
  96. package/dist/trust-kpi.js +6 -266
  97. package/dist/trust-policy.d.ts +32 -0
  98. package/dist/trust-policy.js +160 -0
  99. package/dist/trust-render.d.ts +9 -0
  100. package/dist/trust-render.js +54 -0
  101. package/dist/trust-scoring.d.ts +9 -0
  102. package/dist/trust-scoring.js +208 -0
  103. package/dist/trust.d.ts +5 -32
  104. package/dist/trust.js +29 -432
  105. package/dist/types/app.d.ts +30 -0
  106. package/dist/types/app.js +2 -0
  107. package/dist/types/config.d.ts +25 -0
  108. package/dist/types/config.js +2 -0
  109. package/dist/types/core.d.ts +100 -0
  110. package/dist/types/core.js +2 -0
  111. package/dist/types/diff.d.ts +55 -0
  112. package/dist/types/diff.js +2 -0
  113. package/dist/types/plugin.d.ts +41 -0
  114. package/dist/types/plugin.js +2 -0
  115. package/dist/types/trust.d.ts +120 -0
  116. package/dist/types/trust.js +2 -0
  117. package/dist/types.d.ts +8 -365
  118. package/docs/AGENTS.md +1 -1
  119. package/docs/release-notes-draft.md +40 -0
  120. package/docs/rules-catalog.md +49 -0
  121. package/docs/trust-core-release-checklist.md +37 -5
  122. package/package.json +11 -4
  123. package/packages/vscode-drift/src/code-actions.ts +1 -1
  124. package/schemas/drift-ai-output.v1.json +162 -0
  125. package/schemas/drift-doctor.v1.json +57 -0
  126. package/schemas/drift-guard.v1.json +298 -0
  127. package/schemas/drift-report.v1.json +151 -0
  128. package/schemas/drift-trust.v1.json +131 -0
  129. package/scripts/check-docs-drift.mjs +154 -0
  130. package/scripts/check-performance-budget.mjs +360 -0
  131. package/scripts/check-runtime-policy.mjs +66 -0
  132. package/scripts/smoke-repo.mjs +394 -0
  133. package/src/benchmark.ts +92 -53
  134. package/src/cli.ts +285 -13
  135. package/src/config.ts +19 -2
  136. package/src/diff.ts +57 -48
  137. package/src/doctor.ts +185 -0
  138. package/src/format.ts +81 -0
  139. package/src/guard-baseline.ts +74 -0
  140. package/src/guard-metrics.ts +52 -0
  141. package/src/guard-types.ts +66 -0
  142. package/src/guard.ts +248 -0
  143. package/src/index.ts +36 -0
  144. package/src/init.ts +298 -0
  145. package/src/map-cycles.ts +38 -0
  146. package/src/map-svg.ts +124 -0
  147. package/src/map.ts +111 -142
  148. package/src/metrics.ts +78 -59
  149. package/src/output-metadata.ts +32 -0
  150. package/src/plugins-capabilities.ts +36 -0
  151. package/src/plugins-messages.ts +35 -0
  152. package/src/plugins-rules.ts +296 -0
  153. package/src/plugins.ts +76 -283
  154. package/src/reporter-constants.ts +46 -0
  155. package/src/reporter.ts +64 -65
  156. package/src/review.ts +4 -2
  157. package/src/rules/phase3-configurable.ts +39 -26
  158. package/src/saas/constants.ts +56 -0
  159. package/src/saas/dashboard.ts +172 -0
  160. package/src/saas/errors.ts +45 -0
  161. package/src/saas/helpers.ts +140 -0
  162. package/src/saas/ingest.ts +278 -0
  163. package/src/saas/organization.ts +99 -0
  164. package/src/saas/plan-change.ts +19 -0
  165. package/src/saas/store.ts +172 -0
  166. package/src/saas/types.ts +216 -0
  167. package/src/saas.ts +49 -1031
  168. package/src/sarif.ts +232 -0
  169. package/src/trust-advanced.ts +99 -0
  170. package/src/trust-kpi-fs.ts +169 -0
  171. package/src/trust-kpi-parse.ts +219 -0
  172. package/src/trust-kpi-types.ts +19 -0
  173. package/src/trust-kpi.ts +8 -316
  174. package/src/trust-policy.ts +246 -0
  175. package/src/trust-render.ts +61 -0
  176. package/src/trust-scoring.ts +231 -0
  177. package/src/trust.ts +62 -576
  178. package/src/types/app.ts +30 -0
  179. package/src/types/config.ts +27 -0
  180. package/src/types/core.ts +105 -0
  181. package/src/types/diff.ts +61 -0
  182. package/src/types/plugin.ts +46 -0
  183. package/src/types/trust.ts +134 -0
  184. package/src/types.ts +79 -409
  185. package/tests/ci-quality-matrix.test.ts +37 -0
  186. package/tests/ci-smoke-gate.test.ts +26 -0
  187. package/tests/ci-version-alignment.test.ts +93 -0
  188. package/tests/cli-sarif.test.ts +92 -0
  189. package/tests/docs-drift-check.test.ts +115 -0
  190. package/tests/format.test.ts +157 -0
  191. package/tests/new-features.test.ts +11 -3
  192. package/tests/perf-budget-check.test.ts +146 -0
  193. package/tests/phase1-init-doctor-guard.test.ts +301 -0
  194. package/tests/runtime-policy-alignment.test.ts +46 -0
  195. package/tests/sarif.test.ts +160 -0
  196. package/tests/trust-kpi.test.ts +31 -4
  197. package/tests/trust.test.ts +18 -0
  198. package/vitest.config.ts +2 -0
package/dist/benchmark.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { mkdirSync, writeFileSync } from 'node:fs';
2
2
  import * as path from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
3
4
  import { analyzeProject } from './analyzer.js';
4
5
  import { loadConfig } from './config.js';
5
6
  import { buildReport } from './reporter.js';
@@ -7,6 +8,30 @@ import { generateReview } from './review.js';
7
8
  import { buildTrustReport } from './trust.js';
8
9
  import { cleanupTempDir, extractFilesAtRef } from './git.js';
9
10
  import { computeDiff } from './diff.js';
11
+ const DEFAULT_SCAN_PATH = '.';
12
+ const DEFAULT_REVIEW_PATH = '.';
13
+ const DEFAULT_TRUST_PATH = '.';
14
+ const DEFAULT_BASE_REF = 'HEAD~1';
15
+ const DEFAULT_WARMUP_RUNS = 1;
16
+ const DEFAULT_MEASURED_RUNS = 5;
17
+ const TABLE_WIDTHS = {
18
+ task: 10,
19
+ warmup: 8,
20
+ runs: 6,
21
+ median: 13,
22
+ mean: 11,
23
+ min: 10,
24
+ max: 10,
25
+ };
26
+ const TABLE_COLUMNS = [
27
+ { key: 'task', header: 'task' },
28
+ { key: 'warmup', header: 'warmup' },
29
+ { key: 'runs', header: 'runs' },
30
+ { key: 'median', header: 'median(ms)' },
31
+ { key: 'mean', header: 'mean(ms)' },
32
+ { key: 'min', header: 'min(ms)' },
33
+ { key: 'max', header: 'max(ms)' },
34
+ ];
10
35
  function parseNumberFlag(value, flagName) {
11
36
  const parsed = Number(value);
12
37
  if (!Number.isFinite(parsed) || parsed < 0) {
@@ -16,52 +41,29 @@ function parseNumberFlag(value, flagName) {
16
41
  }
17
42
  function parseOptions(argv) {
18
43
  const options = {
19
- scanPath: '.',
20
- reviewPath: '.',
21
- trustPath: '.',
22
- baseRef: 'HEAD~1',
23
- warmupRuns: 1,
24
- measuredRuns: 5,
44
+ scanPath: DEFAULT_SCAN_PATH,
45
+ reviewPath: DEFAULT_REVIEW_PATH,
46
+ trustPath: DEFAULT_TRUST_PATH,
47
+ baseRef: DEFAULT_BASE_REF,
48
+ warmupRuns: DEFAULT_WARMUP_RUNS,
49
+ measuredRuns: DEFAULT_MEASURED_RUNS,
25
50
  };
26
- for (let i = 0; i < argv.length; i += 1) {
51
+ const handlers = {
52
+ '--scan-path': (value) => { options.scanPath = value; },
53
+ '--review-path': (value) => { options.reviewPath = value; },
54
+ '--trust-path': (value) => { options.trustPath = value; },
55
+ '--base': (value) => { options.baseRef = value; },
56
+ '--warmup': (value) => { options.warmupRuns = parseNumberFlag(value, '--warmup'); },
57
+ '--runs': (value) => { options.measuredRuns = parseNumberFlag(value, '--runs'); },
58
+ '--json-out': (value) => { options.jsonOut = value; },
59
+ };
60
+ for (let i = 0; i < argv.length; i += 2) {
27
61
  const arg = argv[i];
28
62
  const next = argv[i + 1];
29
- if (arg === '--scan-path' && next) {
30
- options.scanPath = next;
31
- i += 1;
32
- continue;
33
- }
34
- if (arg === '--review-path' && next) {
35
- options.reviewPath = next;
36
- i += 1;
37
- continue;
38
- }
39
- if (arg === '--trust-path' && next) {
40
- options.trustPath = next;
41
- i += 1;
42
- continue;
43
- }
44
- if (arg === '--base' && next) {
45
- options.baseRef = next;
46
- i += 1;
47
- continue;
48
- }
49
- if (arg === '--warmup' && next) {
50
- options.warmupRuns = parseNumberFlag(next, '--warmup');
51
- i += 1;
52
- continue;
53
- }
54
- if (arg === '--runs' && next) {
55
- options.measuredRuns = parseNumberFlag(next, '--runs');
56
- i += 1;
57
- continue;
58
- }
59
- if (arg === '--json-out' && next) {
60
- options.jsonOut = next;
61
- i += 1;
62
- continue;
63
- }
64
- throw new Error(`Unknown or incomplete argument: ${arg}`);
63
+ const handler = handlers[arg];
64
+ if (!handler || !next)
65
+ throw new Error(`Unknown or incomplete argument: ${arg}`);
66
+ handler(next);
65
67
  }
66
68
  if (options.measuredRuns < 1) {
67
69
  throw new Error('--runs must be at least 1');
@@ -84,31 +86,43 @@ function median(values) {
84
86
  function formatMs(ms) {
85
87
  return ms.toFixed(2);
86
88
  }
89
+ function bytesToMb(bytes) {
90
+ return bytes / (1024 * 1024);
91
+ }
87
92
  async function runTask(name, warmupRuns, measuredRuns, task) {
88
93
  for (let i = 0; i < warmupRuns; i += 1) {
89
94
  await task();
90
95
  }
91
96
  const samplesMs = [];
97
+ const samplesRssMb = [];
92
98
  for (let i = 0; i < measuredRuns; i += 1) {
99
+ const rssBefore = process.memoryUsage().rss;
93
100
  const started = performance.now();
94
101
  await task();
95
102
  samplesMs.push(performance.now() - started);
103
+ const rssAfter = process.memoryUsage().rss;
104
+ samplesRssMb.push(bytesToMb(Math.max(rssBefore, rssAfter)));
96
105
  }
97
106
  const total = samplesMs.reduce((sum, sample) => sum + sample, 0);
107
+ const totalRss = samplesRssMb.reduce((sum, sample) => sum + sample, 0);
98
108
  return {
99
109
  name,
100
110
  warmupRuns,
101
111
  measuredRuns,
102
112
  samplesMs,
113
+ samplesRssMb,
103
114
  medianMs: median(samplesMs),
104
115
  meanMs: total / samplesMs.length,
105
116
  minMs: Math.min(...samplesMs),
106
117
  maxMs: Math.max(...samplesMs),
118
+ medianRssMb: median(samplesRssMb),
119
+ meanRssMb: totalRss / samplesRssMb.length,
120
+ maxRssMb: Math.max(...samplesRssMb),
107
121
  };
108
122
  }
109
123
  function printTable(results) {
110
- const headers = ['task', 'warmup', 'runs', 'median(ms)', 'mean(ms)', 'min(ms)', 'max(ms)'];
111
- const widths = [10, 8, 6, 13, 11, 10, 10];
124
+ const headers = TABLE_COLUMNS.map((column) => column.header);
125
+ const widths = TABLE_COLUMNS.map((column) => TABLE_WIDTHS[column.key]);
112
126
  const row = (values) => values
113
127
  .map((value, index) => value.padEnd(widths[index], ' '))
114
128
  .join(' ');
@@ -153,11 +167,14 @@ async function runTrust(trustPath, baseRef) {
153
167
  }
154
168
  buildTrustReport(report, { diff });
155
169
  }
156
- async function main() {
157
- const options = parseOptions(process.argv.slice(2));
170
+ async function runReview(reviewPath, baseRef) {
171
+ await generateReview(reviewPath, baseRef);
172
+ }
173
+ async function main(argv) {
174
+ const options = parseOptions(argv);
158
175
  const results = [
159
176
  await runTask('scan', options.warmupRuns, options.measuredRuns, () => runScan(options.scanPath)),
160
- await runTask('review', options.warmupRuns, options.measuredRuns, () => generateReview(options.reviewPath, options.baseRef).then(() => undefined)),
177
+ await runTask('review', options.warmupRuns, options.measuredRuns, () => runReview(options.reviewPath, options.baseRef)),
161
178
  await runTask('trust', options.warmupRuns, options.measuredRuns, () => runTrust(options.trustPath, options.baseRef)),
162
179
  ];
163
180
  const output = {
@@ -178,8 +195,22 @@ async function main() {
178
195
  process.stdout.write(`\nSaved benchmark JSON to ${options.jsonOut}\n`);
179
196
  }
180
197
  }
181
- main().catch((error) => {
182
- process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
183
- process.exit(1);
184
- });
198
+ function isExecutedAsEntryPoint() {
199
+ const entryArg = process.argv[1];
200
+ if (!entryArg)
201
+ return false;
202
+ return import.meta.url === pathToFileURL(path.resolve(entryArg)).href;
203
+ }
204
+ export async function runBenchmarkCli(argv = process.argv.slice(2)) {
205
+ try {
206
+ await main(argv);
207
+ }
208
+ catch (error) {
209
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
210
+ process.exit(1);
211
+ }
212
+ }
213
+ if (isExecutedAsEntryPoint()) {
214
+ void runBenchmarkCli();
215
+ }
185
216
  //# sourceMappingURL=benchmark.js.map
package/dist/cli.js CHANGED
@@ -14,6 +14,7 @@ import { printConsole, printDiff } from './printer.js';
14
14
  import { loadConfig } from './config.js';
15
15
  import { extractFilesAtRef, cleanupTempDir } from './git.js';
16
16
  import { computeDiff } from './diff.js';
17
+ import { formatGuardJson, runGuard } from './guard.js';
17
18
  import { generateHtmlReport } from './report.js';
18
19
  import { generateBadge } from './badge.js';
19
20
  import { emitCIAnnotations, printCISummary } from './ci.js';
@@ -24,6 +25,11 @@ import { generateArchitectureMap } from './map.js';
24
25
  import { changeOrganizationPlan, generateSaasDashboardHtml, getOrganizationEffectiveLimits, getOrganizationUsageSnapshot, getSaasSummary, ingestSnapshotFromReport, listOrganizationPlanChanges, } from './saas.js';
25
26
  import { buildTrustReport, explainTrustGatePolicy, formatTrustGatePolicyExplanation, formatTrustJson, renderTrustOutput, shouldFailTrustGate, normalizeMergeRiskLevel, MERGE_RISK_ORDER, detectBranchName, } from './trust.js';
26
27
  import { computeTrustKpis, formatTrustKpiConsole, formatTrustKpiJson } from './trust-kpi.js';
28
+ import { runBenchmarkCli } from './benchmark.js';
29
+ import { runInit, INIT_PRESETS } from './init.js';
30
+ import { runDoctor } from './doctor.js';
31
+ import { resolveOutputFormat } from './format.js';
32
+ import { toSarif, diffToSarif } from './sarif.js';
27
33
  const program = new Command();
28
34
  function parseOptionalPositiveInt(rawValue, flagName) {
29
35
  if (rawValue == null)
@@ -51,6 +57,77 @@ function addResourceOptions(command) {
51
57
  .option('--max-file-size-kb <n>', 'Skip files above this size and report diagnostics')
52
58
  .option('--with-semantic-duplication', 'Keep semantic-duplication rule enabled in low-memory mode');
53
59
  }
60
+ function parseOptionalNumber(rawValue, flagName) {
61
+ if (rawValue == null)
62
+ return undefined;
63
+ const value = Number(rawValue);
64
+ if (!Number.isFinite(value)) {
65
+ throw new Error(`${flagName} must be a valid number`);
66
+ }
67
+ return value;
68
+ }
69
+ function parseBySeverity(rawValue) {
70
+ if (rawValue == null)
71
+ return undefined;
72
+ const spec = rawValue.trim();
73
+ if (!spec) {
74
+ throw new Error('--by-severity must not be empty. Expected format: error=0,warning=2,info=5');
75
+ }
76
+ const thresholds = {};
77
+ const seen = new Set();
78
+ for (const segment of spec.split(',')) {
79
+ const pair = segment.trim();
80
+ if (!pair)
81
+ continue;
82
+ const equalIndex = pair.indexOf('=');
83
+ if (equalIndex <= 0 || equalIndex === pair.length - 1) {
84
+ throw new Error(`Invalid --by-severity entry '${pair}'. Expected key=value (e.g. warning=2).`);
85
+ }
86
+ const key = pair.slice(0, equalIndex).trim().toLowerCase();
87
+ const rawThreshold = pair.slice(equalIndex + 1).trim();
88
+ if (key !== 'error' && key !== 'warning' && key !== 'info') {
89
+ throw new Error(`Invalid --by-severity key '${key}'. Allowed keys: error, warning, info.`);
90
+ }
91
+ if (seen.has(key)) {
92
+ throw new Error(`Duplicate --by-severity key '${key}'.`);
93
+ }
94
+ const threshold = Number(rawThreshold);
95
+ if (!Number.isFinite(threshold)) {
96
+ throw new Error(`Invalid --by-severity value for '${key}': '${rawThreshold}'. Must be a valid number.`);
97
+ }
98
+ const severityKey = key;
99
+ thresholds[severityKey] = threshold;
100
+ seen.add(severityKey);
101
+ }
102
+ if (seen.size === 0) {
103
+ throw new Error('--by-severity must include at least one threshold. Example: error=0,warning=2');
104
+ }
105
+ return thresholds;
106
+ }
107
+ function formatSigned(value) {
108
+ return value > 0 ? `+${value}` : `${value}`;
109
+ }
110
+ function printGuardSummary(result) {
111
+ const modeLabel = result.mode === 'diff' ? `diff (${result.baseRef ?? 'unknown base'})` : 'baseline';
112
+ const statusLabel = result.passed ? 'PASS' : 'FAIL';
113
+ process.stdout.write('\n');
114
+ process.stdout.write(`Guard mode: ${modeLabel}\n`);
115
+ process.stdout.write(`Result: ${statusLabel}\n`);
116
+ process.stdout.write(`Score delta: ${formatSigned(result.metrics.scoreDelta)}\n`);
117
+ process.stdout.write(`Total issues delta: ${formatSigned(result.metrics.totalIssuesDelta)}\n`);
118
+ process.stdout.write(`Severity delta: error=${formatSigned(result.metrics.severityDelta.error)}, warning=${formatSigned(result.metrics.severityDelta.warning)}, info=${formatSigned(result.metrics.severityDelta.info)}\n`);
119
+ if (result.mode === 'baseline' && result.baselinePath) {
120
+ process.stdout.write(`Baseline file: ${result.baselinePath}\n`);
121
+ }
122
+ if (result.checks.length === 0) {
123
+ process.stdout.write('Checks: none configured\n');
124
+ return;
125
+ }
126
+ process.stdout.write('Checks:\n');
127
+ for (const check of result.checks) {
128
+ process.stdout.write(` - [${check.passed ? 'PASS' : 'FAIL'}] ${check.id}: ${check.message} (actual=${check.actual}, limit=${check.limit})\n`);
129
+ }
130
+ }
54
131
  function parseTrustGateOverrides(options) {
55
132
  const cliMinTrust = options.minTrust ? Number(options.minTrust) : undefined;
56
133
  if (options.minTrust && Number.isNaN(cliMinTrust)) {
@@ -95,6 +172,7 @@ addResourceOptions(program
95
172
  .command('scan [path]', { isDefault: true })
96
173
  .description('Scan a directory for vibe coding drift')
97
174
  .option('-o, --output <file>', 'Write report to a Markdown file')
175
+ .option('--format <type>', 'Output format: console|json|markdown|ai|sarif')
98
176
  .option('--json', 'Output raw JSON report')
99
177
  .option('--ai', 'Output AI-optimized JSON for LLM consumption')
100
178
  .option('--fix', 'Show fix suggestions for each issue')
@@ -106,15 +184,33 @@ addResourceOptions(program
106
184
  const files = analyzeProject(resolvedPath, config, resolveAnalysisOptions(options));
107
185
  process.stderr.write(` Found ${files.length} TypeScript file(s)\n\n`);
108
186
  const report = buildReport(resolvedPath, files);
109
- if (options.ai) {
187
+ const format = resolveOutputFormat({
188
+ command: 'scan',
189
+ format: options.format,
190
+ supported: ['console', 'json', 'markdown', 'ai', 'sarif'],
191
+ legacyAliases: [
192
+ { flag: 'json', used: options.json, mapsTo: 'json' },
193
+ { flag: 'ai', used: options.ai, mapsTo: 'ai' },
194
+ ],
195
+ onWarning: (message) => process.stderr.write(`${message}\n`),
196
+ });
197
+ if (format === 'sarif') {
198
+ process.stdout.write(`${JSON.stringify(toSarif(report), null, 2)}\n`);
199
+ return;
200
+ }
201
+ if (format === 'ai') {
110
202
  const aiOutput = formatAIOutput(report);
111
203
  process.stdout.write(JSON.stringify(aiOutput, null, 2));
112
204
  return;
113
205
  }
114
- if (options.json) {
206
+ if (format === 'json') {
115
207
  process.stdout.write(JSON.stringify(report, null, 2));
116
208
  return;
117
209
  }
210
+ if (format === 'markdown') {
211
+ process.stdout.write(`${formatMarkdown(report)}\n`);
212
+ return;
213
+ }
118
214
  printConsole(report, { showFix: options.fix });
119
215
  if (options.output) {
120
216
  const md = formatMarkdown(report);
@@ -128,9 +224,31 @@ addResourceOptions(program
128
224
  process.exit(1);
129
225
  }
130
226
  }));
227
+ program
228
+ .command('init')
229
+ .description('Initialize drift configuration with presets and scaffolding')
230
+ .option('--preset <type>', `Scaffold config with preset: ${INIT_PRESETS.join(', ')}`)
231
+ .option('--ci', 'Generate GitHub Actions workflow for drift review')
232
+ .option('--baseline', 'Create drift-baseline.json with current project score')
233
+ .action(async (options) => {
234
+ const projectRoot = resolve('.');
235
+ try {
236
+ await runInit(projectRoot, {
237
+ preset: options.preset,
238
+ ci: options.ci,
239
+ baseline: options.baseline,
240
+ });
241
+ }
242
+ catch (err) {
243
+ const message = err instanceof Error ? err.message : String(err);
244
+ process.stderr.write(`\n Error: ${message}\n\n`);
245
+ process.exit(1);
246
+ }
247
+ });
131
248
  addResourceOptions(program
132
249
  .command('diff [ref]')
133
250
  .description('Compare current state against a git ref (default: HEAD~1)')
251
+ .option('--format <type>', 'Output format: console|json|markdown|ai|sarif')
134
252
  .option('--json', 'Output raw JSON diff')
135
253
  .action(async (ref, options) => {
136
254
  const baseRef = ref ?? 'HEAD~1';
@@ -139,6 +257,13 @@ addResourceOptions(program
139
257
  let tempDir;
140
258
  try {
141
259
  process.stderr.write(`\nComputing diff: HEAD vs ${baseRef}...\n\n`);
260
+ const format = resolveOutputFormat({
261
+ command: 'diff',
262
+ format: options.format,
263
+ supported: ['console', 'json', 'sarif'],
264
+ legacyAliases: [{ flag: 'json', used: options.json, mapsTo: 'json' }],
265
+ onWarning: (message) => process.stderr.write(`${message}\n`),
266
+ });
142
267
  // Scan current state
143
268
  const config = await loadConfig(projectPath);
144
269
  const currentFiles = analyzeProject(projectPath, config, analysisOptions);
@@ -157,7 +282,10 @@ addResourceOptions(program
157
282
  })),
158
283
  };
159
284
  const diff = computeDiff(remappedBase, currentReport, baseRef);
160
- if (options.json) {
285
+ if (format === 'sarif') {
286
+ process.stdout.write(`${JSON.stringify(diffToSarif(diff), null, 2)}\n`);
287
+ }
288
+ else if (format === 'json') {
161
289
  process.stdout.write(JSON.stringify(diff, null, 2) + '\n');
162
290
  }
163
291
  else {
@@ -174,21 +302,81 @@ addResourceOptions(program
174
302
  cleanupTempDir(tempDir);
175
303
  }
176
304
  }));
305
+ addResourceOptions(program
306
+ .command('guard [path]')
307
+ .description('Evaluate drift guard thresholds against diff or baseline')
308
+ .option('--base <ref>', 'Git base ref for diff guard mode')
309
+ .option('--baseline <file>', 'Baseline file path (default: drift-baseline.json)')
310
+ .option('--budget <n>', 'Allowed score delta budget')
311
+ .option('--by-severity <spec>', 'Severity thresholds: error=0,warning=2,info=5')
312
+ .option('--json', 'Output raw JSON guard result')
313
+ .action(async (targetPath, options) => {
314
+ try {
315
+ const resolvedPath = resolve(targetPath ?? '.');
316
+ const budget = parseOptionalNumber(options.budget, '--budget');
317
+ const bySeverity = parseBySeverity(options.bySeverity);
318
+ const result = await runGuard(resolvedPath, {
319
+ baseRef: options.base,
320
+ baselinePath: options.baseline,
321
+ budget,
322
+ bySeverity,
323
+ analysis: resolveAnalysisOptions(options),
324
+ });
325
+ if (options.json) {
326
+ process.stdout.write(`${formatGuardJson(result)}\n`);
327
+ }
328
+ else {
329
+ printGuardSummary(result);
330
+ }
331
+ if (!result.passed) {
332
+ process.exit(1);
333
+ }
334
+ }
335
+ catch (err) {
336
+ const message = err instanceof Error ? err.message : String(err);
337
+ process.stderr.write(`\n Error: ${message}\n\n`);
338
+ process.exit(1);
339
+ }
340
+ }));
341
+ program
342
+ .command('benchmark')
343
+ .description('Run benchmark harness for scan/review/trust commands')
344
+ .allowUnknownOption(true)
345
+ .action(async () => {
346
+ await runBenchmarkCli(process.argv.slice(3));
347
+ });
177
348
  program
178
349
  .command('review')
179
350
  .description('Review drift against a base ref and output PR markdown')
180
351
  .option('--base <ref>', 'Git base ref to compare against', 'origin/main')
352
+ .option('--format <type>', 'Output format: console|json|markdown|ai|sarif')
181
353
  .option('--json', 'Output structured review JSON')
182
354
  .option('--comment', 'Output markdown comment body')
183
355
  .option('--fail-on <n>', 'Exit with code 1 if score delta is >= n')
184
356
  .action(async (options) => {
185
357
  try {
186
358
  const review = await generateReview(resolve('.'), options.base);
187
- if (options.json) {
359
+ const format = resolveOutputFormat({
360
+ command: 'review',
361
+ format: options.format,
362
+ supported: ['console', 'json', 'markdown', 'sarif'],
363
+ legacyAliases: [
364
+ { flag: 'json', used: options.json, mapsTo: 'json' },
365
+ { flag: 'comment', used: options.comment, mapsTo: 'markdown' },
366
+ ],
367
+ onWarning: (message) => process.stderr.write(`${message}\n`),
368
+ });
369
+ if (format === 'sarif') {
370
+ process.stdout.write(`${JSON.stringify(diffToSarif(review.diff), null, 2)}\n`);
371
+ }
372
+ else if (format === 'json') {
188
373
  process.stdout.write(JSON.stringify(review, null, 2) + '\n');
189
374
  }
375
+ else if (format === 'markdown') {
376
+ process.stdout.write(`${review.markdown}\n`);
377
+ }
190
378
  else {
191
- process.stdout.write((options.comment ? review.markdown : `${review.summary}\n\n${review.markdown}`) + '\n');
379
+ process.stdout.write(`${review.summary}\n\n${review.markdown}\n`);
192
380
  }
193
381
  const failOn = options.failOn ? Number(options.failOn) : undefined;
194
382
  if (typeof failOn === 'number' && !Number.isNaN(failOn) && review.totalDelta >= failOn) {
@@ -205,6 +393,7 @@ addResourceOptions(program
205
393
  .command('trust [path]')
206
394
  .description('Compute merge trust baseline from drift signals')
207
395
  .option('--base <ref>', 'Git base ref for diff-aware trust scoring')
396
+ .option('--format <type>', 'Output format: console|json|markdown|ai|sarif')
208
397
  .option('--json', 'Output structured trust JSON')
209
398
  .option('--markdown', 'Output trust report as markdown (PR comment ready)')
210
399
  .option('-o, --output <file>', 'Write trust output to file')
@@ -284,7 +473,22 @@ addResourceOptions(program
284
473
  snapshots,
285
474
  },
286
475
  });
287
- const rendered = `${renderTrustOutput(trust, options)}\n`;
476
+ const format = resolveOutputFormat({
477
+ command: 'trust',
478
+ format: options.format,
479
+ supported: ['console', 'json', 'markdown', 'sarif'],
480
+ legacyAliases: [
481
+ { flag: 'json', used: options.json, mapsTo: 'json' },
482
+ { flag: 'markdown', used: options.markdown, mapsTo: 'markdown' },
483
+ ],
484
+ onWarning: (message) => process.stderr.write(`${message}\n`),
485
+ });
486
+ const rendered = format === 'sarif'
487
+ ? `${JSON.stringify(toSarif(report), null, 2)}\n`
488
+ : `${renderTrustOutput(trust, {
489
+ json: format === 'json',
490
+ markdown: format === 'markdown',
491
+ })}\n`;
288
492
  process.stdout.write(rendered);
289
493
  if (options.output) {
290
494
  const outPath = resolve(options.output);
@@ -378,6 +582,20 @@ program
378
582
  process.exit(1);
379
583
  }
380
584
  });
585
+ program
586
+ .command('doctor')
587
+ .description('Run project environment diagnostics')
588
+ .option('--json', 'Output structured doctor JSON')
589
+ .action(async (opts) => {
590
+ try {
591
+ await runDoctor(process.cwd(), { json: opts.json });
592
+ }
593
+ catch (err) {
594
+ const message = err instanceof Error ? err.message : String(err);
595
+ process.stderr.write(`\n Error: ${message}\n\n`);
596
+ process.exitCode = 1;
597
+ }
598
+ });
381
599
  program
382
600
  .command('kpi <path>')
383
601
  .description('Aggregate trust KPIs from trust JSON artifacts')
@@ -442,14 +660,31 @@ addResourceOptions(program
442
660
  addResourceOptions(program
443
661
  .command('ci [path]')
444
662
  .description('Emit GitHub Actions annotations and step summary')
663
+ .option('--format <type>', 'Output format: console|json|markdown|ai|sarif')
664
+ .option('--json', 'Output raw JSON report (legacy alias for --format json)')
445
665
  .option('--min-score <n>', 'Exit with code 1 if overall score exceeds this threshold', '0')
446
666
  .action(async (targetPath, options) => {
447
667
  const resolvedPath = resolve(targetPath ?? '.');
448
668
  const config = await loadConfig(resolvedPath);
449
669
  const files = analyzeProject(resolvedPath, config, resolveAnalysisOptions(options));
450
670
  const report = buildReport(resolvedPath, files);
451
- emitCIAnnotations(report);
452
- printCISummary(report);
671
+ const format = resolveOutputFormat({
672
+ command: 'ci',
673
+ format: options.format,
674
+ supported: ['console', 'json', 'sarif'],
675
+ legacyAliases: [{ flag: 'json', used: options.json, mapsTo: 'json' }],
676
+ onWarning: (message) => process.stderr.write(`${message}\n`),
677
+ });
678
+ if (format === 'sarif') {
679
+ process.stdout.write(`${JSON.stringify(toSarif(report), null, 2)}\n`);
680
+ }
681
+ else if (format === 'json') {
682
+ process.stdout.write(JSON.stringify(report, null, 2) + '\n');
683
+ }
684
+ else {
685
+ emitCIAnnotations(report);
686
+ printCISummary(report);
687
+ }
453
688
  const minScore = Number(options.minScore);
454
689
  if (minScore > 0 && report.totalScore > minScore) {
455
690
  process.exit(1);
package/dist/config.js CHANGED
@@ -1,6 +1,19 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { join, resolve } from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
+ function normalizeLegacyConfig(config) {
5
+ if (config.modules !== undefined) {
6
+ return config;
7
+ }
8
+ const legacyModules = config.moduleBoundaries ?? config.boundaries;
9
+ if (!legacyModules || legacyModules.length === 0) {
10
+ return config;
11
+ }
12
+ return {
13
+ ...config,
14
+ modules: legacyModules,
15
+ };
16
+ }
4
17
  /**
5
18
  * Load drift.config.ts / .js / .json from the given project root.
6
19
  * Returns undefined if no config file is found.
@@ -23,13 +36,14 @@ export async function loadConfig(projectRoot) {
23
36
  const ext = candidate.split('.').pop();
24
37
  if (ext === 'json') {
25
38
  const { readFileSync } = await import('node:fs');
26
- return JSON.parse(readFileSync(candidate, 'utf-8'));
39
+ const rawConfig = JSON.parse(readFileSync(candidate, 'utf-8'));
40
+ return normalizeLegacyConfig(rawConfig);
27
41
  }
28
42
  // .ts / .js — dynamic import via file URL
29
43
  const fileUrl = pathToFileURL(resolve(candidate)).href;
30
44
  const mod = await import(fileUrl);
31
45
  const config = mod.default ?? mod;
32
- return config;
46
+ return normalizeLegacyConfig(config);
33
47
  }
34
48
  catch { // drift-ignore
35
49
  // drift-ignore: catch-swallow — config is optional; load failure is non-fatal