@eduardbar/drift 1.2.0 → 1.3.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 (61) hide show
  1. package/.github/workflows/publish-vscode.yml +3 -3
  2. package/.github/workflows/publish.yml +3 -3
  3. package/.github/workflows/review-pr.yml +98 -6
  4. package/AGENTS.md +6 -0
  5. package/README.md +160 -10
  6. package/ROADMAP.md +6 -5
  7. package/dist/analyzer.d.ts +2 -2
  8. package/dist/analyzer.js +420 -159
  9. package/dist/benchmark.d.ts +2 -0
  10. package/dist/benchmark.js +185 -0
  11. package/dist/cli.js +453 -62
  12. package/dist/diff.js +74 -10
  13. package/dist/git.js +12 -0
  14. package/dist/index.d.ts +5 -3
  15. package/dist/index.js +3 -1
  16. package/dist/plugins.d.ts +2 -1
  17. package/dist/plugins.js +177 -28
  18. package/dist/printer.js +4 -0
  19. package/dist/review.js +2 -2
  20. package/dist/rules/comments.js +2 -2
  21. package/dist/rules/complexity.js +2 -7
  22. package/dist/rules/nesting.js +3 -13
  23. package/dist/rules/phase0-basic.js +10 -10
  24. package/dist/rules/shared.d.ts +2 -0
  25. package/dist/rules/shared.js +27 -3
  26. package/dist/saas.d.ts +143 -7
  27. package/dist/saas.js +478 -37
  28. package/dist/trust-kpi.d.ts +9 -0
  29. package/dist/trust-kpi.js +445 -0
  30. package/dist/trust.d.ts +65 -0
  31. package/dist/trust.js +571 -0
  32. package/dist/types.d.ts +154 -0
  33. package/docs/PRD.md +187 -109
  34. package/docs/plugin-contract.md +61 -0
  35. package/docs/trust-core-release-checklist.md +55 -0
  36. package/package.json +5 -3
  37. package/src/analyzer.ts +484 -155
  38. package/src/benchmark.ts +244 -0
  39. package/src/cli.ts +562 -79
  40. package/src/diff.ts +75 -10
  41. package/src/git.ts +16 -0
  42. package/src/index.ts +48 -0
  43. package/src/plugins.ts +354 -26
  44. package/src/printer.ts +4 -0
  45. package/src/review.ts +2 -2
  46. package/src/rules/comments.ts +2 -2
  47. package/src/rules/complexity.ts +2 -7
  48. package/src/rules/nesting.ts +3 -13
  49. package/src/rules/phase0-basic.ts +11 -12
  50. package/src/rules/shared.ts +31 -3
  51. package/src/saas.ts +641 -43
  52. package/src/trust-kpi.ts +518 -0
  53. package/src/trust.ts +774 -0
  54. package/src/types.ts +171 -0
  55. package/tests/diff.test.ts +124 -0
  56. package/tests/new-features.test.ts +71 -0
  57. package/tests/plugins.test.ts +219 -0
  58. package/tests/rules.test.ts +23 -1
  59. package/tests/saas-foundation.test.ts +358 -1
  60. package/tests/trust-kpi.test.ts +120 -0
  61. package/tests/trust.test.ts +584 -0
@@ -0,0 +1,244 @@
1
+ import { mkdirSync, writeFileSync } from 'node:fs'
2
+ import * as path from 'node:path'
3
+ import { analyzeProject } from './analyzer.js'
4
+ import { loadConfig } from './config.js'
5
+ import { buildReport } from './reporter.js'
6
+ import { generateReview } from './review.js'
7
+ import { buildTrustReport } from './trust.js'
8
+ import { cleanupTempDir, extractFilesAtRef } from './git.js'
9
+ import { computeDiff } from './diff.js'
10
+
11
+ interface BenchmarkOptions {
12
+ scanPath: string
13
+ reviewPath: string
14
+ trustPath: string
15
+ baseRef: string
16
+ warmupRuns: number
17
+ measuredRuns: number
18
+ jsonOut?: string
19
+ }
20
+
21
+ interface TaskResult {
22
+ name: 'scan' | 'review' | 'trust'
23
+ warmupRuns: number
24
+ measuredRuns: number
25
+ samplesMs: number[]
26
+ medianMs: number
27
+ meanMs: number
28
+ minMs: number
29
+ maxMs: number
30
+ }
31
+
32
+ interface BenchmarkOutput {
33
+ meta: {
34
+ scannedAt: string
35
+ node: string
36
+ platform: NodeJS.Platform
37
+ cwd: string
38
+ }
39
+ options: BenchmarkOptions
40
+ results: TaskResult[]
41
+ }
42
+
43
+ function parseNumberFlag(value: string, flagName: string): number {
44
+ const parsed = Number(value)
45
+ if (!Number.isFinite(parsed) || parsed < 0) {
46
+ throw new Error(`${flagName} must be a non-negative number, received '${value}'`)
47
+ }
48
+ return Math.floor(parsed)
49
+ }
50
+
51
+ function parseOptions(argv: string[]): BenchmarkOptions {
52
+ const options: BenchmarkOptions = {
53
+ scanPath: '.',
54
+ reviewPath: '.',
55
+ trustPath: '.',
56
+ baseRef: 'HEAD~1',
57
+ warmupRuns: 1,
58
+ measuredRuns: 5,
59
+ }
60
+
61
+ for (let i = 0; i < argv.length; i += 1) {
62
+ const arg = argv[i]
63
+ const next = argv[i + 1]
64
+
65
+ if (arg === '--scan-path' && next) {
66
+ options.scanPath = next
67
+ i += 1
68
+ continue
69
+ }
70
+ if (arg === '--review-path' && next) {
71
+ options.reviewPath = next
72
+ i += 1
73
+ continue
74
+ }
75
+ if (arg === '--trust-path' && next) {
76
+ options.trustPath = next
77
+ i += 1
78
+ continue
79
+ }
80
+ if (arg === '--base' && next) {
81
+ options.baseRef = next
82
+ i += 1
83
+ continue
84
+ }
85
+ if (arg === '--warmup' && next) {
86
+ options.warmupRuns = parseNumberFlag(next, '--warmup')
87
+ i += 1
88
+ continue
89
+ }
90
+ if (arg === '--runs' && next) {
91
+ options.measuredRuns = parseNumberFlag(next, '--runs')
92
+ i += 1
93
+ continue
94
+ }
95
+ if (arg === '--json-out' && next) {
96
+ options.jsonOut = next
97
+ i += 1
98
+ continue
99
+ }
100
+
101
+ throw new Error(`Unknown or incomplete argument: ${arg}`)
102
+ }
103
+
104
+ if (options.measuredRuns < 1) {
105
+ throw new Error('--runs must be at least 1')
106
+ }
107
+
108
+ return {
109
+ ...options,
110
+ scanPath: path.resolve(options.scanPath),
111
+ reviewPath: path.resolve(options.reviewPath),
112
+ trustPath: path.resolve(options.trustPath),
113
+ jsonOut: options.jsonOut ? path.resolve(options.jsonOut) : undefined,
114
+ }
115
+ }
116
+
117
+ function median(values: number[]): number {
118
+ const sorted = [...values].sort((a, b) => a - b)
119
+ const middle = Math.floor(sorted.length / 2)
120
+ if (sorted.length % 2 === 1) return sorted[middle]
121
+ return (sorted[middle - 1] + sorted[middle]) / 2
122
+ }
123
+
124
+ function formatMs(ms: number): string {
125
+ return ms.toFixed(2)
126
+ }
127
+
128
+ async function runTask(
129
+ name: TaskResult['name'],
130
+ warmupRuns: number,
131
+ measuredRuns: number,
132
+ task: () => Promise<void>,
133
+ ): Promise<TaskResult> {
134
+ for (let i = 0; i < warmupRuns; i += 1) {
135
+ await task()
136
+ }
137
+
138
+ const samplesMs: number[] = []
139
+ for (let i = 0; i < measuredRuns; i += 1) {
140
+ const started = performance.now()
141
+ await task()
142
+ samplesMs.push(performance.now() - started)
143
+ }
144
+
145
+ const total = samplesMs.reduce((sum, sample) => sum + sample, 0)
146
+ return {
147
+ name,
148
+ warmupRuns,
149
+ measuredRuns,
150
+ samplesMs,
151
+ medianMs: median(samplesMs),
152
+ meanMs: total / samplesMs.length,
153
+ minMs: Math.min(...samplesMs),
154
+ maxMs: Math.max(...samplesMs),
155
+ }
156
+ }
157
+
158
+ function printTable(results: TaskResult[]): void {
159
+ const headers = ['task', 'warmup', 'runs', 'median(ms)', 'mean(ms)', 'min(ms)', 'max(ms)']
160
+ const widths = [10, 8, 6, 13, 11, 10, 10]
161
+
162
+ const row = (values: string[]): string => values
163
+ .map((value, index) => value.padEnd(widths[index], ' '))
164
+ .join(' ')
165
+
166
+ process.stdout.write('\n')
167
+ process.stdout.write(row(headers) + '\n')
168
+ process.stdout.write(row(widths.map((width) => '-'.repeat(width))) + '\n')
169
+
170
+ for (const result of results) {
171
+ process.stdout.write(row([
172
+ result.name,
173
+ String(result.warmupRuns),
174
+ String(result.measuredRuns),
175
+ formatMs(result.medianMs),
176
+ formatMs(result.meanMs),
177
+ formatMs(result.minMs),
178
+ formatMs(result.maxMs),
179
+ ]) + '\n')
180
+ }
181
+ }
182
+
183
+ async function runScan(scanPath: string): Promise<void> {
184
+ const config = await loadConfig(scanPath)
185
+ const files = analyzeProject(scanPath, config)
186
+ buildReport(scanPath, files)
187
+ }
188
+
189
+ async function runTrust(trustPath: string, baseRef: string): Promise<void> {
190
+ const config = await loadConfig(trustPath)
191
+ const files = analyzeProject(trustPath, config)
192
+ const report = buildReport(trustPath, files)
193
+
194
+ let tempDir: string | undefined
195
+ let diff
196
+
197
+ try {
198
+ tempDir = extractFilesAtRef(trustPath, baseRef)
199
+ const baseFiles = analyzeProject(tempDir, config)
200
+ const baseReport = buildReport(tempDir, baseFiles)
201
+ diff = computeDiff(baseReport, report, baseRef)
202
+ } catch {
203
+ diff = undefined
204
+ } finally {
205
+ if (tempDir) cleanupTempDir(tempDir)
206
+ }
207
+
208
+ buildTrustReport(report, { diff })
209
+ }
210
+
211
+ async function main(): Promise<void> {
212
+ const options = parseOptions(process.argv.slice(2))
213
+
214
+ const results = [
215
+ await runTask('scan', options.warmupRuns, options.measuredRuns, () => runScan(options.scanPath)),
216
+ await runTask('review', options.warmupRuns, options.measuredRuns, () => generateReview(options.reviewPath, options.baseRef).then(() => undefined)),
217
+ await runTask('trust', options.warmupRuns, options.measuredRuns, () => runTrust(options.trustPath, options.baseRef)),
218
+ ]
219
+
220
+ const output: BenchmarkOutput = {
221
+ meta: {
222
+ scannedAt: new Date().toISOString(),
223
+ node: process.version,
224
+ platform: process.platform,
225
+ cwd: process.cwd(),
226
+ },
227
+ options,
228
+ results,
229
+ }
230
+
231
+ printTable(results)
232
+ process.stdout.write(`\n${JSON.stringify(output, null, 2)}\n`)
233
+
234
+ if (options.jsonOut) {
235
+ mkdirSync(path.dirname(options.jsonOut), { recursive: true })
236
+ writeFileSync(options.jsonOut, JSON.stringify(output, null, 2) + '\n', 'utf8')
237
+ process.stdout.write(`\nSaved benchmark JSON to ${options.jsonOut}\n`)
238
+ }
239
+ }
240
+
241
+ main().catch((error) => {
242
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
243
+ process.exit(1)
244
+ })