@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.
- package/.gga +50 -0
- package/.github/actions/drift-review/README.md +62 -0
- package/.github/actions/drift-review/action.yml +148 -0
- package/.github/actions/drift-scan/README.md +28 -32
- package/.github/actions/drift-scan/action.yml +78 -14
- package/.github/workflows/publish-vscode.yml +1 -3
- package/.github/workflows/publish.yml +8 -0
- package/.github/workflows/quality.yml +15 -0
- package/.github/workflows/reusable-quality-checks.yml +95 -0
- package/.github/workflows/review-pr.yml +33 -41
- package/AGENTS.md +75 -251
- package/CHANGELOG.md +41 -0
- package/README.md +177 -43
- package/benchmarks/fixtures/critical/drift.config.ts +21 -0
- package/benchmarks/fixtures/critical/src/app/user-service.ts +30 -0
- package/benchmarks/fixtures/critical/src/domain/entities.ts +19 -0
- package/benchmarks/fixtures/critical/src/domain/policies.ts +22 -0
- package/benchmarks/fixtures/critical/src/index.ts +10 -0
- package/benchmarks/fixtures/critical/src/infra/memory-user-repo.ts +14 -0
- package/benchmarks/perf-budget.v1.json +27 -0
- package/dist/benchmark.d.ts +1 -1
- package/dist/benchmark.js +83 -52
- package/dist/cli.js +243 -8
- package/dist/config.js +16 -2
- package/dist/diff.js +42 -50
- package/dist/doctor.d.ts +26 -0
- package/dist/doctor.js +140 -0
- package/dist/format.d.ts +17 -0
- package/dist/format.js +45 -0
- package/dist/guard-baseline.d.ts +12 -0
- package/dist/guard-baseline.js +57 -0
- package/dist/guard-metrics.d.ts +6 -0
- package/dist/guard-metrics.js +39 -0
- package/dist/guard-types.d.ts +58 -0
- package/dist/guard-types.js +2 -0
- package/dist/guard.d.ts +16 -0
- package/dist/guard.js +178 -0
- package/dist/index.d.ts +10 -3
- package/dist/index.js +4 -1
- package/dist/init.d.ts +15 -0
- package/dist/init.js +273 -0
- package/dist/map-cycles.d.ts +2 -0
- package/dist/map-cycles.js +34 -0
- package/dist/map-svg.d.ts +19 -0
- package/dist/map-svg.js +97 -0
- package/dist/map.js +78 -138
- package/dist/metrics.js +70 -55
- package/dist/output-metadata.d.ts +15 -0
- package/dist/output-metadata.js +19 -0
- package/dist/plugins-capabilities.d.ts +4 -0
- package/dist/plugins-capabilities.js +21 -0
- package/dist/plugins-messages.d.ts +10 -0
- package/dist/plugins-messages.js +16 -0
- package/dist/plugins-rules.d.ts +9 -0
- package/dist/plugins-rules.js +137 -0
- package/dist/plugins.d.ts +1 -1
- package/dist/plugins.js +45 -142
- package/dist/reporter-constants.d.ts +16 -0
- package/dist/reporter-constants.js +39 -0
- package/dist/reporter.d.ts +3 -3
- package/dist/reporter.js +35 -55
- package/dist/review.d.ts +2 -1
- package/dist/review.js +2 -1
- package/dist/rules/phase3-configurable.js +23 -15
- package/dist/saas/constants.d.ts +15 -0
- package/dist/saas/constants.js +48 -0
- package/dist/saas/dashboard.d.ts +8 -0
- package/dist/saas/dashboard.js +132 -0
- package/dist/saas/errors.d.ts +19 -0
- package/dist/saas/errors.js +37 -0
- package/dist/saas/helpers.d.ts +21 -0
- package/dist/saas/helpers.js +110 -0
- package/dist/saas/ingest.d.ts +3 -0
- package/dist/saas/ingest.js +249 -0
- package/dist/saas/organization.d.ts +5 -0
- package/dist/saas/organization.js +82 -0
- package/dist/saas/plan-change.d.ts +10 -0
- package/dist/saas/plan-change.js +15 -0
- package/dist/saas/store.d.ts +21 -0
- package/dist/saas/store.js +159 -0
- package/dist/saas/types.d.ts +191 -0
- package/dist/saas/types.js +2 -0
- package/dist/saas.d.ts +8 -218
- package/dist/saas.js +7 -761
- package/dist/sarif.d.ts +74 -0
- package/dist/sarif.js +122 -0
- package/dist/trust-advanced.d.ts +14 -0
- package/dist/trust-advanced.js +65 -0
- package/dist/trust-kpi-fs.d.ts +3 -0
- package/dist/trust-kpi-fs.js +141 -0
- package/dist/trust-kpi-parse.d.ts +7 -0
- package/dist/trust-kpi-parse.js +186 -0
- package/dist/trust-kpi-types.d.ts +16 -0
- package/dist/trust-kpi-types.js +2 -0
- package/dist/trust-kpi.d.ts +1 -3
- package/dist/trust-kpi.js +6 -266
- package/dist/trust-policy.d.ts +32 -0
- package/dist/trust-policy.js +160 -0
- package/dist/trust-render.d.ts +9 -0
- package/dist/trust-render.js +54 -0
- package/dist/trust-scoring.d.ts +9 -0
- package/dist/trust-scoring.js +208 -0
- package/dist/trust.d.ts +5 -32
- package/dist/trust.js +29 -432
- package/dist/types/app.d.ts +30 -0
- package/dist/types/app.js +2 -0
- package/dist/types/config.d.ts +25 -0
- package/dist/types/config.js +2 -0
- package/dist/types/core.d.ts +100 -0
- package/dist/types/core.js +2 -0
- package/dist/types/diff.d.ts +55 -0
- package/dist/types/diff.js +2 -0
- package/dist/types/plugin.d.ts +41 -0
- package/dist/types/plugin.js +2 -0
- package/dist/types/trust.d.ts +120 -0
- package/dist/types/trust.js +2 -0
- package/dist/types.d.ts +8 -365
- package/docs/AGENTS.md +1 -1
- package/docs/release-notes-draft.md +40 -0
- package/docs/rules-catalog.md +49 -0
- package/docs/trust-core-release-checklist.md +37 -5
- package/package.json +11 -4
- package/packages/vscode-drift/src/code-actions.ts +1 -1
- package/schemas/drift-ai-output.v1.json +162 -0
- package/schemas/drift-doctor.v1.json +57 -0
- package/schemas/drift-guard.v1.json +298 -0
- package/schemas/drift-report.v1.json +151 -0
- package/schemas/drift-trust.v1.json +131 -0
- package/scripts/check-docs-drift.mjs +154 -0
- package/scripts/check-performance-budget.mjs +360 -0
- package/scripts/check-runtime-policy.mjs +66 -0
- package/scripts/smoke-repo.mjs +394 -0
- package/src/benchmark.ts +92 -53
- package/src/cli.ts +285 -13
- package/src/config.ts +19 -2
- package/src/diff.ts +57 -48
- package/src/doctor.ts +185 -0
- package/src/format.ts +81 -0
- package/src/guard-baseline.ts +74 -0
- package/src/guard-metrics.ts +52 -0
- package/src/guard-types.ts +66 -0
- package/src/guard.ts +248 -0
- package/src/index.ts +36 -0
- package/src/init.ts +298 -0
- package/src/map-cycles.ts +38 -0
- package/src/map-svg.ts +124 -0
- package/src/map.ts +111 -142
- package/src/metrics.ts +78 -59
- package/src/output-metadata.ts +32 -0
- package/src/plugins-capabilities.ts +36 -0
- package/src/plugins-messages.ts +35 -0
- package/src/plugins-rules.ts +296 -0
- package/src/plugins.ts +76 -283
- package/src/reporter-constants.ts +46 -0
- package/src/reporter.ts +64 -65
- package/src/review.ts +4 -2
- package/src/rules/phase3-configurable.ts +39 -26
- package/src/saas/constants.ts +56 -0
- package/src/saas/dashboard.ts +172 -0
- package/src/saas/errors.ts +45 -0
- package/src/saas/helpers.ts +140 -0
- package/src/saas/ingest.ts +278 -0
- package/src/saas/organization.ts +99 -0
- package/src/saas/plan-change.ts +19 -0
- package/src/saas/store.ts +172 -0
- package/src/saas/types.ts +216 -0
- package/src/saas.ts +49 -1031
- package/src/sarif.ts +232 -0
- package/src/trust-advanced.ts +99 -0
- package/src/trust-kpi-fs.ts +169 -0
- package/src/trust-kpi-parse.ts +219 -0
- package/src/trust-kpi-types.ts +19 -0
- package/src/trust-kpi.ts +8 -316
- package/src/trust-policy.ts +246 -0
- package/src/trust-render.ts +61 -0
- package/src/trust-scoring.ts +231 -0
- package/src/trust.ts +62 -576
- package/src/types/app.ts +30 -0
- package/src/types/config.ts +27 -0
- package/src/types/core.ts +105 -0
- package/src/types/diff.ts +61 -0
- package/src/types/plugin.ts +46 -0
- package/src/types/trust.ts +134 -0
- package/src/types.ts +79 -409
- package/tests/ci-quality-matrix.test.ts +37 -0
- package/tests/ci-smoke-gate.test.ts +26 -0
- package/tests/ci-version-alignment.test.ts +93 -0
- package/tests/cli-sarif.test.ts +92 -0
- package/tests/docs-drift-check.test.ts +115 -0
- package/tests/format.test.ts +157 -0
- package/tests/new-features.test.ts +11 -3
- package/tests/perf-budget-check.test.ts +146 -0
- package/tests/phase1-init-doctor-guard.test.ts +301 -0
- package/tests/runtime-policy-alignment.test.ts +46 -0
- package/tests/sarif.test.ts +160 -0
- package/tests/trust-kpi.test.ts +31 -4
- package/tests/trust.test.ts +18 -0
- 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:
|
|
57
|
-
warmupRuns:
|
|
58
|
-
measuredRuns:
|
|
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 +=
|
|
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 (
|
|
66
|
-
|
|
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 =
|
|
160
|
-
const widths =
|
|
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
|
|
212
|
-
|
|
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, () =>
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
+
}
|