@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
@@ -0,0 +1,394 @@
1
+ #!/usr/bin/env node
2
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
3
+ import { basename, resolve } from 'node:path'
4
+ import { spawnSync } from 'node:child_process'
5
+ import { fileURLToPath, pathToFileURL } from 'node:url'
6
+
7
+ const SMOKE_SCHEMA_VERSION = 'drift-smoke/v1'
8
+ const SMOKE_SCRIPT_VERSION = '1.0.0'
9
+ const DEFAULT_BASE_REF = 'HEAD~1'
10
+ const SNIPPET_MAX_LINES = 24
11
+ const SNIPPET_MAX_CHARS = 2400
12
+ const LOG_MAX_BUFFER = 32 * 1024 * 1024
13
+
14
+ function printHelp() {
15
+ process.stdout.write(
16
+ [
17
+ 'drift repo smoke',
18
+ '',
19
+ 'Usage:',
20
+ ' node ./scripts/smoke-repo.mjs <target-path> [--base <ref>] [--out <dir>] [--dry-run]',
21
+ '',
22
+ 'Options:',
23
+ ` --base <ref> Git base ref for review/trust (default: ${DEFAULT_BASE_REF})`,
24
+ ' --out <dir> Output directory (default: .drift-smoke/<repo>-<timestamp>)',
25
+ ' --dry-run Print planned commands and exit without running them',
26
+ ' --help Show this help',
27
+ '',
28
+ 'Examples:',
29
+ ' npm run smoke:repo -- ../my-repo',
30
+ ' npm run smoke:repo -- ../my-repo --base origin/main',
31
+ ' npm run smoke:repo -- ../my-repo --dry-run',
32
+ '',
33
+ 'This script is non-destructive for target repos.',
34
+ ].join('\n') + '\n',
35
+ )
36
+ }
37
+
38
+ function nowStamp() {
39
+ return new Date().toISOString().replace(/[:.]/g, '-').replace('T', '_').replace('Z', '')
40
+ }
41
+
42
+ function toSnippet(text) {
43
+ const lines = String(text || '').split(/\r?\n/).slice(0, SNIPPET_MAX_LINES)
44
+ const joined = lines.join('\n').trim()
45
+ if (joined.length <= SNIPPET_MAX_CHARS) return joined
46
+ return `${joined.slice(0, SNIPPET_MAX_CHARS)}...`
47
+ }
48
+
49
+ function parseArgs(argv) {
50
+ const options = {
51
+ targetPath: '.',
52
+ baseRef: DEFAULT_BASE_REF,
53
+ outDir: undefined,
54
+ dryRun: false,
55
+ help: false,
56
+ }
57
+ let targetPathSet = false
58
+
59
+ let index = 0
60
+ while (index < argv.length) {
61
+ const token = argv[index]
62
+
63
+ if (token === '--help' || token === '-h') {
64
+ options.help = true
65
+ index += 1
66
+ continue
67
+ }
68
+
69
+ if (token === '--dry-run') {
70
+ options.dryRun = true
71
+ index += 1
72
+ continue
73
+ }
74
+
75
+ if (token === '--base') {
76
+ const next = argv[index + 1]
77
+ if (!next) throw new Error('--base requires a value')
78
+ options.baseRef = next
79
+ index += 2
80
+ continue
81
+ }
82
+
83
+ if (token === '--out') {
84
+ const next = argv[index + 1]
85
+ if (!next) throw new Error('--out requires a value')
86
+ options.outDir = next
87
+ index += 2
88
+ continue
89
+ }
90
+
91
+ if (token.startsWith('--')) {
92
+ throw new Error(`Unknown option: ${token}`)
93
+ }
94
+
95
+ if (!targetPathSet) {
96
+ options.targetPath = token
97
+ targetPathSet = true
98
+ } else {
99
+ throw new Error(`Unexpected positional argument: ${token}`)
100
+ }
101
+
102
+ index += 1
103
+ }
104
+
105
+ return options
106
+ }
107
+
108
+ function runGit(cwd, args) {
109
+ return spawnSync('git', args, {
110
+ cwd,
111
+ encoding: 'utf8',
112
+ maxBuffer: LOG_MAX_BUFFER,
113
+ })
114
+ }
115
+
116
+ function runDriftCommand({ id, description, args, cwd, logsDir, expectFailure, cliPath, tsxLoaderSpecifier }) {
117
+ const start = Date.now()
118
+ const startedAt = new Date(start).toISOString()
119
+ const child = spawnSync(process.execPath, ['--import', tsxLoaderSpecifier, cliPath, ...args], {
120
+ cwd,
121
+ encoding: 'utf8',
122
+ maxBuffer: LOG_MAX_BUFFER,
123
+ })
124
+ const end = Date.now()
125
+ const finishedAt = new Date(end).toISOString()
126
+
127
+ const stdout = child.stdout ?? ''
128
+ const spawnError = child.error ? `${child.error.name}: ${child.error.message}` : ''
129
+ const stderr = `${child.stderr ?? ''}${spawnError ? `\n${spawnError}\n` : ''}`
130
+ const stdoutFile = resolve(logsDir, `${id}.stdout.log`)
131
+ const stderrFile = resolve(logsDir, `${id}.stderr.log`)
132
+ writeFileSync(stdoutFile, stdout, 'utf8')
133
+ writeFileSync(stderrFile, stderr, 'utf8')
134
+
135
+ const exitCode = child.status == null ? -1 : child.status
136
+ const signal = child.signal ?? null
137
+
138
+ let status = 'fail'
139
+ if (expectFailure) {
140
+ status = exitCode === 0 ? 'fail' : 'expected-fail'
141
+ } else {
142
+ status = exitCode === 0 ? 'pass' : 'fail'
143
+ }
144
+
145
+ return {
146
+ id,
147
+ description,
148
+ command: `node --import ${tsxLoaderSpecifier} ${cliPath} ${args.join(' ')}`,
149
+ args,
150
+ status,
151
+ expectedFailure: expectFailure,
152
+ exitCode,
153
+ signal,
154
+ durationMs: end - start,
155
+ startedAt,
156
+ finishedAt,
157
+ stdoutSnippet: toSnippet(stdout),
158
+ stderrSnippet: toSnippet(stderr),
159
+ stdoutLog: stdoutFile,
160
+ stderrLog: stderrFile,
161
+ }
162
+ }
163
+
164
+ function markdownForSummary(report) {
165
+ const lines = []
166
+ lines.push('# drift repository smoke summary')
167
+ lines.push('')
168
+ lines.push(`- schema: ${report.schemaVersion}`)
169
+ lines.push(`- generated_at: ${report.generatedAt}`)
170
+ lines.push(`- target_repo: ${report.targetPath}`)
171
+ lines.push(`- base_ref: ${report.baseRef}`)
172
+ lines.push(`- overall_status: ${report.overallStatus}`)
173
+ lines.push(`- pass: ${report.totals.pass} | expected-fail: ${report.totals.expectedFail} | fail: ${report.totals.fail}`)
174
+ lines.push('')
175
+ lines.push('## Commands')
176
+ lines.push('')
177
+ lines.push('| id | status | duration_ms | exit | notes |')
178
+ lines.push('|---|---|---:|---:|---|')
179
+
180
+ for (const result of report.commands) {
181
+ const note = result.expectedFailure ? 'expected non-zero exit' : 'expected zero exit'
182
+ lines.push(`| ${result.id} | ${result.status} | ${result.durationMs} | ${result.exitCode} | ${note} |`)
183
+ }
184
+
185
+ lines.push('')
186
+ lines.push('## Artifacts')
187
+ lines.push('')
188
+ lines.push(`- report_json: ${report.artifacts.reportJson}`)
189
+ lines.push(`- summary_md: ${report.artifacts.summaryMarkdown}`)
190
+ lines.push(`- trust_json: ${report.artifacts.trustJson}`)
191
+ lines.push(`- architecture_svg: ${report.artifacts.architectureSvg}`)
192
+ lines.push(`- html_report: ${report.artifacts.htmlReport}`)
193
+ lines.push(`- kpi_json: ${report.artifacts.kpiJson}`)
194
+
195
+ return `${lines.join('\n')}\n`
196
+ }
197
+
198
+ function main() {
199
+ const opts = parseArgs(process.argv.slice(2))
200
+ if (opts.help) {
201
+ printHelp()
202
+ return
203
+ }
204
+
205
+ const scriptPath = fileURLToPath(import.meta.url)
206
+ const repoRoot = resolve(scriptPath, '..', '..')
207
+ const cliPath = resolve(repoRoot, 'src', 'cli.ts')
208
+ const packageJsonPath = resolve(repoRoot, 'package.json')
209
+ const tsxLoaderPath = resolve(repoRoot, 'node_modules', 'tsx', 'dist', 'loader.mjs')
210
+ if (!existsSync(tsxLoaderPath)) {
211
+ throw new Error(`Missing tsx loader at ${tsxLoaderPath}. Run npm ci in drift repository first.`)
212
+ }
213
+ const tsxLoaderSpecifier = pathToFileURL(tsxLoaderPath).href
214
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
215
+
216
+ const targetPath = resolve(process.cwd(), opts.targetPath)
217
+ const targetStat = statSync(targetPath)
218
+ if (!targetStat.isDirectory()) {
219
+ throw new Error(`Target path must be a directory: ${targetPath}`)
220
+ }
221
+ const outputDir = opts.outDir
222
+ ? resolve(process.cwd(), opts.outDir)
223
+ : resolve(repoRoot, '.drift-smoke', `${basename(targetPath) || 'repo'}-${nowStamp()}`)
224
+
225
+ const logsDir = resolve(outputDir, 'logs')
226
+ const artifactsDir = resolve(outputDir, 'artifacts')
227
+ const trustDir = resolve(artifactsDir, 'trust')
228
+
229
+ const trustJson = resolve(trustDir, 'drift-trust.json')
230
+ const architectureSvg = resolve(artifactsDir, 'architecture.svg')
231
+ const htmlReport = resolve(artifactsDir, 'drift-report.html')
232
+ const kpiJson = resolve(artifactsDir, 'trust-kpi.json')
233
+ const reportJson = resolve(outputDir, 'smoke-report.json')
234
+ const summaryMarkdown = resolve(outputDir, 'smoke-summary.md')
235
+
236
+ const gitRepoProbe = runGit(targetPath, ['rev-parse', '--is-inside-work-tree'])
237
+ const isGitRepo = gitRepoProbe.status === 0
238
+ const gitBaseProbe = isGitRepo ? runGit(targetPath, ['rev-parse', '--verify', `${opts.baseRef}^{commit}`]) : null
239
+ const baseRefResolvable = Boolean(gitBaseProbe && gitBaseProbe.status === 0)
240
+ const gitReady = isGitRepo && baseRefResolvable
241
+
242
+ const plan = [
243
+ {
244
+ id: 'scan-json',
245
+ description: 'scan output as JSON',
246
+ args: ['scan', '.', '--json'],
247
+ expectFailure: false,
248
+ },
249
+ {
250
+ id: 'scan-ai',
251
+ description: 'scan output as AI JSON',
252
+ args: ['scan', '.', '--ai'],
253
+ expectFailure: false,
254
+ },
255
+ {
256
+ id: 'review-base-json',
257
+ description: 'review against base ref as JSON',
258
+ args: ['review', '--base', opts.baseRef, '--json'],
259
+ expectFailure: !gitReady,
260
+ },
261
+ {
262
+ id: 'trust-base-json-output',
263
+ description: 'trust JSON + trust artifact output',
264
+ args: ['trust', '.', '--base', opts.baseRef, '--json', '--json-output', trustJson],
265
+ expectFailure: !gitReady,
266
+ },
267
+ {
268
+ id: 'trust-gate-strict',
269
+ description: 'strict trust gate expected to fail',
270
+ args: ['trust-gate', trustJson, '--min-trust', '101', '--max-risk', 'LOW'],
271
+ expectFailure: true,
272
+ },
273
+ {
274
+ id: 'trust-gate-relaxed',
275
+ description: 'relaxed trust gate expected to pass',
276
+ args: ['trust-gate', trustJson, '--min-trust', '0', '--max-risk', 'CRITICAL'],
277
+ expectFailure: !gitReady,
278
+ },
279
+ {
280
+ id: 'fix-preview',
281
+ description: 'fix preview mode only',
282
+ args: ['fix', '.', '--preview'],
283
+ expectFailure: false,
284
+ },
285
+ {
286
+ id: 'map-output',
287
+ description: 'generate architecture map artifact',
288
+ args: ['map', '.', '--output', architectureSvg],
289
+ expectFailure: false,
290
+ },
291
+ {
292
+ id: 'report-output',
293
+ description: 'generate html report artifact',
294
+ args: ['report', '.', '--output', htmlReport],
295
+ expectFailure: false,
296
+ },
297
+ {
298
+ id: 'kpi-trust-artifacts',
299
+ description: 'compute trust KPIs from artifacts',
300
+ args: ['kpi', trustDir],
301
+ expectFailure: false,
302
+ captureStdoutAs: kpiJson,
303
+ },
304
+ ]
305
+
306
+ if (opts.dryRun) {
307
+ process.stdout.write(`drift smoke dry-run (${SMOKE_SCHEMA_VERSION})\n`)
308
+ process.stdout.write(`target: ${targetPath}\n`)
309
+ process.stdout.write(`base: ${opts.baseRef}\n`)
310
+ process.stdout.write(`output: ${outputDir}\n`)
311
+ process.stdout.write(`git_ready: ${gitReady}\n\n`)
312
+ for (const item of plan) {
313
+ process.stdout.write(`- ${item.id}: node --import ${tsxLoaderSpecifier} ${cliPath} ${item.args.join(' ')}\n`)
314
+ }
315
+ return
316
+ }
317
+
318
+ mkdirSync(logsDir, { recursive: true })
319
+ mkdirSync(trustDir, { recursive: true })
320
+
321
+ const commandResults = []
322
+
323
+ for (const item of plan) {
324
+ const result = runDriftCommand({
325
+ id: item.id,
326
+ description: item.description,
327
+ args: item.args,
328
+ cwd: targetPath,
329
+ logsDir,
330
+ expectFailure: item.expectFailure,
331
+ cliPath,
332
+ tsxLoaderSpecifier,
333
+ })
334
+
335
+ if (item.captureStdoutAs) {
336
+ writeFileSync(item.captureStdoutAs, readFileSync(result.stdoutLog, 'utf8'), 'utf8')
337
+ }
338
+
339
+ commandResults.push(result)
340
+ }
341
+
342
+ const totals = {
343
+ pass: commandResults.filter((result) => result.status === 'pass').length,
344
+ expectedFail: commandResults.filter((result) => result.status === 'expected-fail').length,
345
+ fail: commandResults.filter((result) => result.status === 'fail').length,
346
+ }
347
+
348
+ const overallStatus = totals.fail > 0 ? 'fail' : 'pass'
349
+
350
+ const report = {
351
+ schemaVersion: SMOKE_SCHEMA_VERSION,
352
+ smokeScriptVersion: SMOKE_SCRIPT_VERSION,
353
+ driftVersion: packageJson.version,
354
+ generatedAt: new Date().toISOString(),
355
+ targetPath,
356
+ baseRef: opts.baseRef,
357
+ outputDir,
358
+ overallStatus,
359
+ gitContext: {
360
+ isGitRepo,
361
+ baseRefResolvable,
362
+ gitReady,
363
+ },
364
+ totals,
365
+ artifacts: {
366
+ reportJson,
367
+ summaryMarkdown,
368
+ trustJson,
369
+ architectureSvg,
370
+ htmlReport,
371
+ kpiJson,
372
+ },
373
+ commands: commandResults,
374
+ }
375
+
376
+ writeFileSync(reportJson, `${JSON.stringify(report, null, 2)}\n`, 'utf8')
377
+ writeFileSync(summaryMarkdown, markdownForSummary(report), 'utf8')
378
+
379
+ process.stdout.write(`drift smoke complete: ${overallStatus.toUpperCase()}\n`)
380
+ process.stdout.write(`report: ${reportJson}\n`)
381
+ process.stdout.write(`summary: ${summaryMarkdown}\n`)
382
+
383
+ if (overallStatus === 'fail') {
384
+ process.exitCode = 1
385
+ }
386
+ }
387
+
388
+ try {
389
+ main()
390
+ } catch (error) {
391
+ const message = error instanceof Error ? error.message : String(error)
392
+ process.stderr.write(`Error: ${message}\n`)
393
+ process.exit(1)
394
+ }
package/src/benchmark.ts 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'
@@ -23,10 +24,14 @@ interface TaskResult {
23
24
  warmupRuns: number
24
25
  measuredRuns: number
25
26
  samplesMs: number[]
27
+ samplesRssMb: number[]
26
28
  medianMs: number
27
29
  meanMs: number
28
30
  minMs: number
29
31
  maxMs: number
32
+ medianRssMb: number
33
+ meanRssMb: number
34
+ maxRssMb: number
30
35
  }
31
36
 
32
37
  interface BenchmarkOutput {
@@ -40,6 +45,33 @@ interface BenchmarkOutput {
40
45
  results: TaskResult[]
41
46
  }
42
47
 
48
+ const DEFAULT_SCAN_PATH = '.'
49
+ const DEFAULT_REVIEW_PATH = '.'
50
+ const DEFAULT_TRUST_PATH = '.'
51
+ const DEFAULT_BASE_REF = 'HEAD~1'
52
+ const DEFAULT_WARMUP_RUNS = 1
53
+ const DEFAULT_MEASURED_RUNS = 5
54
+
55
+ const TABLE_WIDTHS = {
56
+ task: 10,
57
+ warmup: 8,
58
+ runs: 6,
59
+ median: 13,
60
+ mean: 11,
61
+ min: 10,
62
+ max: 10,
63
+ } as const
64
+
65
+ const TABLE_COLUMNS = [
66
+ { key: 'task', header: 'task' },
67
+ { key: 'warmup', header: 'warmup' },
68
+ { key: 'runs', header: 'runs' },
69
+ { key: 'median', header: 'median(ms)' },
70
+ { key: 'mean', header: 'mean(ms)' },
71
+ { key: 'min', header: 'min(ms)' },
72
+ { key: 'max', header: 'max(ms)' },
73
+ ] as const
74
+
43
75
  function parseNumberFlag(value: string, flagName: string): number {
44
76
  const parsed = Number(value)
45
77
  if (!Number.isFinite(parsed) || parsed < 0) {
@@ -50,55 +82,31 @@ function parseNumberFlag(value: string, flagName: string): number {
50
82
 
51
83
  function parseOptions(argv: string[]): BenchmarkOptions {
52
84
  const options: BenchmarkOptions = {
53
- scanPath: '.',
54
- reviewPath: '.',
55
- trustPath: '.',
56
- baseRef: 'HEAD~1',
57
- warmupRuns: 1,
58
- measuredRuns: 5,
85
+ scanPath: DEFAULT_SCAN_PATH,
86
+ reviewPath: DEFAULT_REVIEW_PATH,
87
+ trustPath: DEFAULT_TRUST_PATH,
88
+ baseRef: DEFAULT_BASE_REF,
89
+ warmupRuns: DEFAULT_WARMUP_RUNS,
90
+ measuredRuns: DEFAULT_MEASURED_RUNS,
91
+ }
92
+
93
+ const handlers: Record<string, (value: string) => void> = {
94
+ '--scan-path': (value) => { options.scanPath = value },
95
+ '--review-path': (value) => { options.reviewPath = value },
96
+ '--trust-path': (value) => { options.trustPath = value },
97
+ '--base': (value) => { options.baseRef = value },
98
+ '--warmup': (value) => { options.warmupRuns = parseNumberFlag(value, '--warmup') },
99
+ '--runs': (value) => { options.measuredRuns = parseNumberFlag(value, '--runs') },
100
+ '--json-out': (value) => { options.jsonOut = value },
59
101
  }
60
102
 
61
- for (let i = 0; i < argv.length; i += 1) {
103
+ for (let i = 0; i < argv.length; i += 2) {
62
104
  const arg = argv[i]
63
105
  const next = argv[i + 1]
106
+ const handler = handlers[arg]
64
107
 
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}`)
108
+ if (!handler || !next) throw new Error(`Unknown or incomplete argument: ${arg}`)
109
+ handler(next)
102
110
  }
103
111
 
104
112
  if (options.measuredRuns < 1) {
@@ -125,6 +133,10 @@ function formatMs(ms: number): string {
125
133
  return ms.toFixed(2)
126
134
  }
127
135
 
136
+ function bytesToMb(bytes: number): number {
137
+ return bytes / (1024 * 1024)
138
+ }
139
+
128
140
  async function runTask(
129
141
  name: TaskResult['name'],
130
142
  warmupRuns: number,
@@ -136,28 +148,37 @@ async function runTask(
136
148
  }
137
149
 
138
150
  const samplesMs: number[] = []
151
+ const samplesRssMb: number[] = []
139
152
  for (let i = 0; i < measuredRuns; i += 1) {
153
+ const rssBefore = process.memoryUsage().rss
140
154
  const started = performance.now()
141
155
  await task()
142
156
  samplesMs.push(performance.now() - started)
157
+ const rssAfter = process.memoryUsage().rss
158
+ samplesRssMb.push(bytesToMb(Math.max(rssBefore, rssAfter)))
143
159
  }
144
160
 
145
161
  const total = samplesMs.reduce((sum, sample) => sum + sample, 0)
162
+ const totalRss = samplesRssMb.reduce((sum, sample) => sum + sample, 0)
146
163
  return {
147
164
  name,
148
165
  warmupRuns,
149
166
  measuredRuns,
150
167
  samplesMs,
168
+ samplesRssMb,
151
169
  medianMs: median(samplesMs),
152
170
  meanMs: total / samplesMs.length,
153
171
  minMs: Math.min(...samplesMs),
154
172
  maxMs: Math.max(...samplesMs),
173
+ medianRssMb: median(samplesRssMb),
174
+ meanRssMb: totalRss / samplesRssMb.length,
175
+ maxRssMb: Math.max(...samplesRssMb),
155
176
  }
156
177
  }
157
178
 
158
179
  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]
180
+ const headers = TABLE_COLUMNS.map((column) => column.header)
181
+ const widths = TABLE_COLUMNS.map((column) => TABLE_WIDTHS[column.key])
161
182
 
162
183
  const row = (values: string[]): string => values
163
184
  .map((value, index) => value.padEnd(widths[index], ' '))
@@ -208,12 +229,16 @@ async function runTrust(trustPath: string, baseRef: string): Promise<void> {
208
229
  buildTrustReport(report, { diff })
209
230
  }
210
231
 
211
- async function main(): Promise<void> {
212
- const options = parseOptions(process.argv.slice(2))
232
+ async function runReview(reviewPath: string, baseRef: string): Promise<void> {
233
+ await generateReview(reviewPath, baseRef)
234
+ }
235
+
236
+ async function main(argv: string[]): Promise<void> {
237
+ const options = parseOptions(argv)
213
238
 
214
239
  const results = [
215
240
  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)),
241
+ await runTask('review', options.warmupRuns, options.measuredRuns, () => runReview(options.reviewPath, options.baseRef)),
217
242
  await runTask('trust', options.warmupRuns, options.measuredRuns, () => runTrust(options.trustPath, options.baseRef)),
218
243
  ]
219
244
 
@@ -238,7 +263,21 @@ async function main(): Promise<void> {
238
263
  }
239
264
  }
240
265
 
241
- main().catch((error) => {
242
- process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
243
- process.exit(1)
244
- })
266
+ function isExecutedAsEntryPoint(): boolean {
267
+ const entryArg = process.argv[1]
268
+ if (!entryArg) return false
269
+ return import.meta.url === pathToFileURL(path.resolve(entryArg)).href
270
+ }
271
+
272
+ export async function runBenchmarkCli(argv = process.argv.slice(2)): Promise<void> {
273
+ try {
274
+ await main(argv)
275
+ } catch (error) {
276
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`)
277
+ process.exit(1)
278
+ }
279
+ }
280
+
281
+ if (isExecutedAsEntryPoint()) {
282
+ void runBenchmarkCli()
283
+ }