@eduardbar/drift 1.3.0 → 1.4.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 +60 -0
- package/.github/actions/drift-review/action.yml +131 -0
- package/.github/actions/drift-scan/README.md +28 -32
- package/.github/actions/drift-scan/action.yml +78 -14
- package/.github/workflows/review-pr.yml +34 -41
- package/AGENTS.md +75 -251
- package/CHANGELOG.md +28 -0
- package/README.md +148 -41
- package/dist/benchmark.d.ts +1 -1
- package/dist/benchmark.js +71 -52
- package/dist/cli.js +243 -8
- package/dist/config.js +16 -2
- package/dist/diff.js +42 -50
- package/dist/doctor.d.ts +5 -0
- package/dist/doctor.js +133 -0
- package/dist/format.d.ts +17 -0
- package/dist/format.js +45 -0
- package/dist/guard-types.d.ts +57 -0
- package/dist/guard-types.js +2 -0
- package/dist/guard.d.ts +14 -0
- package/dist/guard.js +239 -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 +13 -0
- package/dist/output-metadata.js +17 -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 +4 -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/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 +3 -2
- package/packages/vscode-drift/src/code-actions.ts +1 -1
- package/schemas/drift-ai-output.v1.json +162 -0
- package/schemas/drift-report.v1.json +151 -0
- package/schemas/drift-trust.v1.json +131 -0
- package/scripts/smoke-repo.mjs +394 -0
- package/src/benchmark.ts +75 -53
- package/src/cli.ts +285 -13
- package/src/config.ts +19 -2
- package/src/diff.ts +57 -48
- package/src/doctor.ts +173 -0
- package/src/format.ts +81 -0
- package/src/guard-types.ts +64 -0
- package/src/guard.ts +324 -0
- package/src/index.ts +35 -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 +30 -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 +78 -409
- package/tests/cli-sarif.test.ts +92 -0
- package/tests/format.test.ts +157 -0
- package/tests/new-features.test.ts +10 -2
- package/tests/phase1-init-doctor-guard.test.ts +199 -0
- package/tests/sarif.test.ts +160 -0
- package/tests/trust-kpi.test.ts +31 -4
- package/tests/trust.test.ts +18 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { resolveOutputFormat } from '../src/format.js'
|
|
3
|
+
|
|
4
|
+
describe('resolveOutputFormat', () => {
|
|
5
|
+
it('defaults to console when no format flags are passed', () => {
|
|
6
|
+
const format = resolveOutputFormat({
|
|
7
|
+
command: 'scan',
|
|
8
|
+
supported: ['console', 'json', 'markdown', 'ai', 'sarif'],
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
expect(format).toBe('console')
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('resolves from --format when supported', () => {
|
|
15
|
+
const format = resolveOutputFormat({
|
|
16
|
+
command: 'trust',
|
|
17
|
+
format: 'markdown',
|
|
18
|
+
supported: ['console', 'json', 'markdown'],
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
expect(format).toBe('markdown')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('maps legacy aliases and emits deprecation warnings', () => {
|
|
25
|
+
const onWarning = vi.fn()
|
|
26
|
+
|
|
27
|
+
const format = resolveOutputFormat({
|
|
28
|
+
command: 'review',
|
|
29
|
+
supported: ['console', 'json', 'markdown'],
|
|
30
|
+
legacyAliases: [{ flag: 'comment', used: true, mapsTo: 'markdown' }],
|
|
31
|
+
onWarning,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
expect(format).toBe('markdown')
|
|
35
|
+
expect(onWarning).toHaveBeenCalledWith("Warning: --comment is deprecated for 'review'. Use --format markdown instead.")
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('covers legacy alias mapping per phase-1.4 command', () => {
|
|
39
|
+
expect(
|
|
40
|
+
resolveOutputFormat({
|
|
41
|
+
command: 'scan',
|
|
42
|
+
supported: ['console', 'json', 'markdown', 'ai', 'sarif'],
|
|
43
|
+
legacyAliases: [{ flag: 'ai', used: true, mapsTo: 'ai' }],
|
|
44
|
+
}),
|
|
45
|
+
).toBe('ai')
|
|
46
|
+
|
|
47
|
+
expect(
|
|
48
|
+
resolveOutputFormat({
|
|
49
|
+
command: 'trust',
|
|
50
|
+
supported: ['console', 'json', 'markdown', 'sarif'],
|
|
51
|
+
legacyAliases: [{ flag: 'markdown', used: true, mapsTo: 'markdown' }],
|
|
52
|
+
}),
|
|
53
|
+
).toBe('markdown')
|
|
54
|
+
|
|
55
|
+
expect(
|
|
56
|
+
resolveOutputFormat({
|
|
57
|
+
command: 'diff',
|
|
58
|
+
supported: ['console', 'json', 'sarif'],
|
|
59
|
+
legacyAliases: [{ flag: 'json', used: true, mapsTo: 'json' }],
|
|
60
|
+
}),
|
|
61
|
+
).toBe('json')
|
|
62
|
+
|
|
63
|
+
expect(
|
|
64
|
+
resolveOutputFormat({
|
|
65
|
+
command: 'ci',
|
|
66
|
+
supported: ['console', 'json', 'sarif'],
|
|
67
|
+
legacyAliases: [{ flag: 'json', used: true, mapsTo: 'json' }],
|
|
68
|
+
}),
|
|
69
|
+
).toBe('json')
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('allows sarif when command supports it', () => {
|
|
73
|
+
expect(
|
|
74
|
+
resolveOutputFormat({
|
|
75
|
+
command: 'scan',
|
|
76
|
+
format: 'sarif',
|
|
77
|
+
supported: ['console', 'json', 'markdown', 'ai', 'sarif'],
|
|
78
|
+
}),
|
|
79
|
+
).toBe('sarif')
|
|
80
|
+
|
|
81
|
+
expect(
|
|
82
|
+
resolveOutputFormat({
|
|
83
|
+
command: 'ci',
|
|
84
|
+
format: 'sarif',
|
|
85
|
+
supported: ['console', 'json', 'sarif'],
|
|
86
|
+
}),
|
|
87
|
+
).toBe('sarif')
|
|
88
|
+
|
|
89
|
+
expect(
|
|
90
|
+
resolveOutputFormat({
|
|
91
|
+
command: 'diff',
|
|
92
|
+
format: 'sarif',
|
|
93
|
+
supported: ['console', 'json', 'sarif'],
|
|
94
|
+
}),
|
|
95
|
+
).toBe('sarif')
|
|
96
|
+
|
|
97
|
+
expect(
|
|
98
|
+
resolveOutputFormat({
|
|
99
|
+
command: 'review',
|
|
100
|
+
format: 'sarif',
|
|
101
|
+
supported: ['console', 'json', 'markdown', 'sarif'],
|
|
102
|
+
}),
|
|
103
|
+
).toBe('sarif')
|
|
104
|
+
|
|
105
|
+
expect(
|
|
106
|
+
resolveOutputFormat({
|
|
107
|
+
command: 'trust',
|
|
108
|
+
format: 'sarif',
|
|
109
|
+
supported: ['console', 'json', 'markdown', 'sarif'],
|
|
110
|
+
}),
|
|
111
|
+
).toBe('sarif')
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
it('fails on unsupported format per command', () => {
|
|
115
|
+
expect(() =>
|
|
116
|
+
resolveOutputFormat({
|
|
117
|
+
command: 'diff',
|
|
118
|
+
format: 'markdown',
|
|
119
|
+
supported: ['console', 'json', 'sarif'],
|
|
120
|
+
}),
|
|
121
|
+
).toThrow("Format 'markdown' is not supported for 'diff'. Supported formats: console, json, sarif.")
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('fails when sarif is not supported by the command', () => {
|
|
125
|
+
expect(() =>
|
|
126
|
+
resolveOutputFormat({
|
|
127
|
+
command: 'guard',
|
|
128
|
+
format: 'sarif',
|
|
129
|
+
supported: ['console', 'json'],
|
|
130
|
+
}),
|
|
131
|
+
).toThrow("Format 'sarif' is not supported for 'guard'. Supported formats: console, json.")
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('fails when legacy aliases conflict', () => {
|
|
135
|
+
expect(() =>
|
|
136
|
+
resolveOutputFormat({
|
|
137
|
+
command: 'scan',
|
|
138
|
+
supported: ['console', 'json', 'markdown', 'ai', 'sarif'],
|
|
139
|
+
legacyAliases: [
|
|
140
|
+
{ flag: 'json', used: true, mapsTo: 'json' },
|
|
141
|
+
{ flag: 'ai', used: true, mapsTo: 'ai' },
|
|
142
|
+
],
|
|
143
|
+
}),
|
|
144
|
+
).toThrow("Conflicting legacy format flags for 'scan': json vs ai. Use a single format option.")
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('fails when --format conflicts with a legacy alias', () => {
|
|
148
|
+
expect(() =>
|
|
149
|
+
resolveOutputFormat({
|
|
150
|
+
command: 'trust',
|
|
151
|
+
format: 'json',
|
|
152
|
+
supported: ['console', 'json', 'markdown', 'sarif'],
|
|
153
|
+
legacyAliases: [{ flag: 'markdown', used: true, mapsTo: 'markdown' }],
|
|
154
|
+
}),
|
|
155
|
+
).toThrow("Conflicting format flags for 'trust': --format json and legacy alias for markdown.")
|
|
156
|
+
})
|
|
157
|
+
})
|
|
@@ -4,10 +4,12 @@ import { tmpdir } from 'node:os'
|
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
import { analyzeProject } from '../src/analyzer.js'
|
|
6
6
|
import { buildReport, formatAIOutput } from '../src/reporter.js'
|
|
7
|
-
import { formatReviewMarkdown
|
|
7
|
+
import { formatReviewMarkdown } from '../src/review.js'
|
|
8
8
|
import { generateArchitectureSvg } from '../src/map.js'
|
|
9
9
|
import { applyFixes } from '../src/fix.js'
|
|
10
10
|
|
|
11
|
+
type DriftReview = Parameters<typeof formatReviewMarkdown>[0]
|
|
12
|
+
|
|
11
13
|
describe('new feature MVP', () => {
|
|
12
14
|
let tmpDir = ''
|
|
13
15
|
|
|
@@ -36,6 +38,12 @@ describe('new feature MVP', () => {
|
|
|
36
38
|
expect(Array.isArray(ai.files_suspected)).toBe(true)
|
|
37
39
|
expect(ai.maintenance_risk).toBeDefined()
|
|
38
40
|
expect(ai.quality).toBeDefined()
|
|
41
|
+
expect(ai.$schema).toBe('schemas/drift-ai-output.v1.json')
|
|
42
|
+
expect(typeof ai.toolVersion).toBe('string')
|
|
43
|
+
expect(ai.toolVersion.length).toBeGreaterThan(0)
|
|
44
|
+
expect(report.$schema).toBe('schemas/drift-report.v1.json')
|
|
45
|
+
expect(typeof report.toolVersion).toBe('string')
|
|
46
|
+
expect(report.toolVersion.length).toBeGreaterThan(0)
|
|
39
47
|
})
|
|
40
48
|
|
|
41
49
|
it('formats review markdown for PR comments', () => {
|
|
@@ -191,7 +199,7 @@ describe('new feature MVP', () => {
|
|
|
191
199
|
|
|
192
200
|
expect(fullRules.has('circular-dependency')).toBe(true)
|
|
193
201
|
expect(lowMemoryRules.has('circular-dependency')).toBe(true)
|
|
194
|
-
})
|
|
202
|
+
}, 15000)
|
|
195
203
|
|
|
196
204
|
it('adds diagnostics when max file size guardrail skips files', () => {
|
|
197
205
|
tmpDir = mkdtempSync(join(tmpdir(), 'drift-max-file-size-'))
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { runDoctor } from '../src/doctor.js'
|
|
6
|
+
import { runInit } from '../src/init.js'
|
|
7
|
+
import { evaluateGuard, runGuard } from '../src/guard.js'
|
|
8
|
+
import { analyzeProject } from '../src/analyzer.js'
|
|
9
|
+
import { buildReport } from '../src/reporter.js'
|
|
10
|
+
|
|
11
|
+
function createTempDir(prefix: string): string {
|
|
12
|
+
return mkdtempSync(join(tmpdir(), prefix))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe('phase 1: doctor/init/guard', () => {
|
|
16
|
+
const tempDirs: string[] = []
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
vi.restoreAllMocks()
|
|
20
|
+
for (const dir of tempDirs) {
|
|
21
|
+
rmSync(dir, { recursive: true, force: true })
|
|
22
|
+
}
|
|
23
|
+
tempDirs.length = 0
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe('runDoctor', () => {
|
|
27
|
+
it('prints a basic diagnostic report and exits with code 0', async () => {
|
|
28
|
+
const projectDir = createTempDir('drift-doctor-basic-')
|
|
29
|
+
tempDirs.push(projectDir)
|
|
30
|
+
writeFileSync(join(projectDir, 'index.ts'), 'export const value = 1\n')
|
|
31
|
+
|
|
32
|
+
const output: string[] = []
|
|
33
|
+
vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => {
|
|
34
|
+
output.push(String(chunk))
|
|
35
|
+
return true
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
const exitCode = await runDoctor(projectDir)
|
|
39
|
+
const text = output.join('')
|
|
40
|
+
|
|
41
|
+
expect(exitCode).toBe(0)
|
|
42
|
+
expect(text).toContain('drift doctor')
|
|
43
|
+
expect(text).toContain('Source files (.ts/.tsx/.js/.jsx): 1')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('prints valid JSON output with expected shape', async () => {
|
|
47
|
+
const projectDir = createTempDir('drift-doctor-json-')
|
|
48
|
+
tempDirs.push(projectDir)
|
|
49
|
+
mkdirSync(join(projectDir, 'src'))
|
|
50
|
+
writeFileSync(join(projectDir, 'package.json'), JSON.stringify({ type: 'module' }, null, 2))
|
|
51
|
+
writeFileSync(join(projectDir, 'tsconfig.json'), '{"compilerOptions":{}}\n')
|
|
52
|
+
writeFileSync(join(projectDir, 'drift.config.ts'), 'export default {}\n')
|
|
53
|
+
writeFileSync(join(projectDir, 'src', 'app.ts'), 'export const answer = 42\n')
|
|
54
|
+
|
|
55
|
+
const output: string[] = []
|
|
56
|
+
vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => {
|
|
57
|
+
output.push(String(chunk))
|
|
58
|
+
return true
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
await runDoctor(projectDir, { json: true })
|
|
62
|
+
const report = JSON.parse(output.join('')) as {
|
|
63
|
+
targetPath: string
|
|
64
|
+
node: { version: string; major: number; supported: boolean }
|
|
65
|
+
project: {
|
|
66
|
+
packageJsonFound: boolean
|
|
67
|
+
esm: boolean
|
|
68
|
+
tsconfigFound: boolean
|
|
69
|
+
sourceFilesCount: number
|
|
70
|
+
lowMemorySuggested: boolean
|
|
71
|
+
driftConfigFile: string | null
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
expect(report.targetPath).toBe(projectDir)
|
|
76
|
+
expect(typeof report.node.version).toBe('string')
|
|
77
|
+
expect(typeof report.node.major).toBe('number')
|
|
78
|
+
expect(typeof report.node.supported).toBe('boolean')
|
|
79
|
+
expect(report.project.packageJsonFound).toBe(true)
|
|
80
|
+
expect(report.project.esm).toBe(true)
|
|
81
|
+
expect(report.project.tsconfigFound).toBe(true)
|
|
82
|
+
expect(report.project.sourceFilesCount).toBe(2)
|
|
83
|
+
expect(report.project.driftConfigFile).toBe('drift.config.ts')
|
|
84
|
+
expect(typeof report.project.lowMemorySuggested).toBe('boolean')
|
|
85
|
+
})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
describe('runInit', () => {
|
|
89
|
+
it('creates drift.config.ts when using a valid preset', async () => {
|
|
90
|
+
const projectDir = createTempDir('drift-init-preset-')
|
|
91
|
+
tempDirs.push(projectDir)
|
|
92
|
+
|
|
93
|
+
await runInit(projectDir, { preset: 'node-backend' })
|
|
94
|
+
|
|
95
|
+
const generated = readFileSync(join(projectDir, 'drift.config.ts'), 'utf8')
|
|
96
|
+
expect(generated).toContain('satisfies DriftConfig')
|
|
97
|
+
expect(generated).toContain("name: 'api'")
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('throws on invalid preset', async () => {
|
|
101
|
+
const projectDir = createTempDir('drift-init-invalid-')
|
|
102
|
+
tempDirs.push(projectDir)
|
|
103
|
+
|
|
104
|
+
await expect(runInit(projectDir, { preset: 'invalid-preset' })).rejects.toThrow("Invalid preset 'invalid-preset'")
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('creates ci workflow when --ci flag is enabled', async () => {
|
|
108
|
+
const projectDir = createTempDir('drift-init-ci-')
|
|
109
|
+
tempDirs.push(projectDir)
|
|
110
|
+
|
|
111
|
+
await runInit(projectDir, { ci: true })
|
|
112
|
+
|
|
113
|
+
const workflowPath = join(projectDir, '.github', 'workflows', 'drift-review.yml')
|
|
114
|
+
const workflow = readFileSync(workflowPath, 'utf8')
|
|
115
|
+
expect(workflow).toContain('name: drift PR Review')
|
|
116
|
+
expect(workflow).toContain('npx drift review --base')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('prints no-actions message when no flags are provided', async () => {
|
|
120
|
+
const projectDir = createTempDir('drift-init-empty-')
|
|
121
|
+
tempDirs.push(projectDir)
|
|
122
|
+
|
|
123
|
+
const output: string[] = []
|
|
124
|
+
vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => {
|
|
125
|
+
output.push(String(chunk))
|
|
126
|
+
return true
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
await runInit(projectDir, {})
|
|
130
|
+
|
|
131
|
+
expect(output.join('')).toContain('No actions taken. Use --preset, --ci, or --baseline flags.')
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
describe('guard', () => {
|
|
136
|
+
it('evaluateGuard applies no-regression, budget and severity checks', () => {
|
|
137
|
+
const evaluation = evaluateGuard({
|
|
138
|
+
metrics: {
|
|
139
|
+
scoreDelta: 3,
|
|
140
|
+
totalIssuesDelta: 1,
|
|
141
|
+
severityDelta: { error: 1, warning: 0, info: 0 },
|
|
142
|
+
},
|
|
143
|
+
budget: 2,
|
|
144
|
+
bySeverity: { error: 0, warning: 1, info: 0 },
|
|
145
|
+
enforceNoRegression: {
|
|
146
|
+
score: true,
|
|
147
|
+
totalIssues: true,
|
|
148
|
+
},
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
expect(evaluation.passed).toBe(false)
|
|
152
|
+
expect(evaluation.checks.some((check) => check.id === 'no-regression-score' && !check.passed)).toBe(true)
|
|
153
|
+
expect(evaluation.checks.some((check) => check.id === 'no-regression-total-issues' && !check.passed)).toBe(true)
|
|
154
|
+
expect(evaluation.checks.some((check) => check.id === 'budget-total-delta' && !check.passed)).toBe(true)
|
|
155
|
+
expect(evaluation.checks.some((check) => check.id === 'severity-error' && !check.passed)).toBe(true)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
it('runs in baseline mode with inline baseline fixture', async () => {
|
|
159
|
+
const projectDir = createTempDir('drift-guard-baseline-')
|
|
160
|
+
tempDirs.push(projectDir)
|
|
161
|
+
writeFileSync(join(projectDir, 'main.ts'), 'export const value: any = 42\n')
|
|
162
|
+
|
|
163
|
+
const reports = analyzeProject(projectDir)
|
|
164
|
+
const current = buildReport(projectDir, reports)
|
|
165
|
+
|
|
166
|
+
const result = await runGuard(projectDir, {
|
|
167
|
+
baseline: {
|
|
168
|
+
score: current.totalScore,
|
|
169
|
+
totalIssues: current.totalIssues,
|
|
170
|
+
bySeverity: {
|
|
171
|
+
error: current.summary.errors,
|
|
172
|
+
warning: current.summary.warnings,
|
|
173
|
+
info: current.summary.infos,
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
budget: 0,
|
|
177
|
+
bySeverity: { error: 0, warning: 0, info: 0 },
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
expect(result.mode).toBe('baseline')
|
|
181
|
+
expect(result.passed).toBe(true)
|
|
182
|
+
expect(result.metrics.scoreDelta).toBe(0)
|
|
183
|
+
expect(result.metrics.totalIssuesDelta).toBe(0)
|
|
184
|
+
expect(result.metrics.severityDelta).toEqual({ error: 0, warning: 0, info: 0 })
|
|
185
|
+
expect(result.checks.some((check) => check.id === 'no-regression-score')).toBe(true)
|
|
186
|
+
expect(result.checks.some((check) => check.id === 'no-regression-total-issues')).toBe(true)
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('throws when guard has no baseRef and no baseline', async () => {
|
|
190
|
+
const projectDir = createTempDir('drift-guard-missing-anchor-')
|
|
191
|
+
tempDirs.push(projectDir)
|
|
192
|
+
writeFileSync(join(projectDir, 'main.ts'), 'export const value = 1\n')
|
|
193
|
+
|
|
194
|
+
await expect(runGuard(projectDir, {})).rejects.toThrow(
|
|
195
|
+
'Guard requires a comparison point: provide baseRef or a baseline (inline or file).',
|
|
196
|
+
)
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
})
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { diffToSarif, toSarif } from '../src/sarif.js'
|
|
3
|
+
import type { DriftDiff, DriftReport } from '../src/types.js'
|
|
4
|
+
|
|
5
|
+
describe('toSarif', () => {
|
|
6
|
+
function createReport(): DriftReport {
|
|
7
|
+
return {
|
|
8
|
+
scannedAt: '2026-03-17T10:20:30.000Z',
|
|
9
|
+
targetPath: '/repo',
|
|
10
|
+
files: [{
|
|
11
|
+
path: 'src/app.ts',
|
|
12
|
+
score: 72,
|
|
13
|
+
issues: [
|
|
14
|
+
{
|
|
15
|
+
rule: 'large-file',
|
|
16
|
+
severity: 'error',
|
|
17
|
+
message: 'File exceeds threshold',
|
|
18
|
+
line: 14,
|
|
19
|
+
column: 3,
|
|
20
|
+
snippet: 'export function app() {}',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
rule: 'debug-leftover',
|
|
24
|
+
severity: 'warning',
|
|
25
|
+
message: 'console.log detected',
|
|
26
|
+
line: 22,
|
|
27
|
+
column: 1,
|
|
28
|
+
snippet: 'console.log(value)',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
rule: 'plugin-warning',
|
|
32
|
+
severity: 'info',
|
|
33
|
+
message: 'Plugin diagnostic',
|
|
34
|
+
line: 1,
|
|
35
|
+
column: 1,
|
|
36
|
+
snippet: 'app.ts',
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
}],
|
|
40
|
+
totalIssues: 3,
|
|
41
|
+
totalScore: 72,
|
|
42
|
+
totalFiles: 1,
|
|
43
|
+
summary: {
|
|
44
|
+
errors: 1,
|
|
45
|
+
warnings: 1,
|
|
46
|
+
infos: 1,
|
|
47
|
+
byRule: {
|
|
48
|
+
'large-file': 1,
|
|
49
|
+
'debug-leftover': 1,
|
|
50
|
+
'plugin-warning': 1,
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
quality: {
|
|
54
|
+
overall: 90,
|
|
55
|
+
dimensions: {
|
|
56
|
+
architecture: 91,
|
|
57
|
+
complexity: 87,
|
|
58
|
+
'ai-patterns': 92,
|
|
59
|
+
testing: 89,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
maintenanceRisk: {
|
|
63
|
+
score: 20,
|
|
64
|
+
level: 'low',
|
|
65
|
+
hotspots: [],
|
|
66
|
+
signals: {
|
|
67
|
+
highComplexityFiles: 0,
|
|
68
|
+
filesWithoutNearbyTests: 0,
|
|
69
|
+
frequentChangeFiles: 0,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
it('maps drift severities to SARIF levels', () => {
|
|
76
|
+
const sarif = toSarif(createReport())
|
|
77
|
+
const levels = sarif.runs[0].results.map((result) => result.level)
|
|
78
|
+
|
|
79
|
+
expect(levels).toContain('error')
|
|
80
|
+
expect(levels).toContain('warning')
|
|
81
|
+
expect(levels).toContain('note')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('builds SARIF minimal valid structure', () => {
|
|
85
|
+
const sarif = toSarif(createReport())
|
|
86
|
+
|
|
87
|
+
expect(sarif.version).toBe('2.1.0')
|
|
88
|
+
expect(sarif.runs).toHaveLength(1)
|
|
89
|
+
expect(sarif.runs[0].tool.driver.name).toBe('drift')
|
|
90
|
+
expect(Array.isArray(sarif.runs[0].results)).toBe(true)
|
|
91
|
+
expect(sarif.runs[0].results).toHaveLength(3)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('maps message and location fields for each issue', () => {
|
|
95
|
+
const sarif = toSarif(createReport())
|
|
96
|
+
const result = sarif.runs[0].results.find((item) => item.ruleId === 'large-file')
|
|
97
|
+
|
|
98
|
+
expect(result).toBeDefined()
|
|
99
|
+
expect(result?.message.text).toBe('File exceeds threshold')
|
|
100
|
+
expect(result?.locations[0].physicalLocation.artifactLocation.uri).toBe('src/app.ts')
|
|
101
|
+
expect(result?.locations[0].physicalLocation.region.startLine).toBe(14)
|
|
102
|
+
expect(result?.locations[0].physicalLocation.region.startColumn).toBe(3)
|
|
103
|
+
expect(result?.properties?.weight).toBe(20)
|
|
104
|
+
expect(result?.properties?.fileScore).toBe(72)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('maps diff newIssues to SARIF results', () => {
|
|
108
|
+
const diff: DriftDiff = {
|
|
109
|
+
baseRef: 'origin/main',
|
|
110
|
+
projectPath: '/repo',
|
|
111
|
+
scannedAt: '2026-03-17T10:20:30.000Z',
|
|
112
|
+
files: [
|
|
113
|
+
{
|
|
114
|
+
path: 'src/app.ts',
|
|
115
|
+
scoreBefore: 60,
|
|
116
|
+
scoreAfter: 72,
|
|
117
|
+
scoreDelta: 12,
|
|
118
|
+
newIssues: [
|
|
119
|
+
{
|
|
120
|
+
rule: 'debug-leftover',
|
|
121
|
+
severity: 'warning',
|
|
122
|
+
message: 'console.log detected',
|
|
123
|
+
line: 22,
|
|
124
|
+
column: 1,
|
|
125
|
+
snippet: 'console.log(value)',
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
resolvedIssues: [
|
|
129
|
+
{
|
|
130
|
+
rule: 'magic-number',
|
|
131
|
+
severity: 'info',
|
|
132
|
+
message: 'legacy issue resolved',
|
|
133
|
+
line: 10,
|
|
134
|
+
column: 5,
|
|
135
|
+
snippet: '42',
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
totalScoreBefore: 60,
|
|
141
|
+
totalScoreAfter: 72,
|
|
142
|
+
totalDelta: 12,
|
|
143
|
+
newIssuesCount: 1,
|
|
144
|
+
resolvedIssuesCount: 1,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const sarif = diffToSarif(diff)
|
|
148
|
+
|
|
149
|
+
expect(sarif.version).toBe('2.1.0')
|
|
150
|
+
expect(sarif.runs[0].results).toHaveLength(1)
|
|
151
|
+
expect(sarif.runs[0].results[0]?.ruleId).toBe('debug-leftover')
|
|
152
|
+
expect(sarif.runs[0].results[0]?.locations[0]?.physicalLocation?.artifactLocation?.uri).toBe('src/app.ts')
|
|
153
|
+
expect(sarif.runs[0].results[0]?.properties?.baseRef).toBe('origin/main')
|
|
154
|
+
expect(sarif.runs[0].results[0]?.properties?.scoreDelta).toBe(12)
|
|
155
|
+
expect(sarif.runs[0].results[0]?.properties?.changeType).toBe('new-issue')
|
|
156
|
+
expect(sarif.runs[0].properties.baseRef).toBe('origin/main')
|
|
157
|
+
expect(sarif.runs[0].properties.newIssuesCount).toBe(1)
|
|
158
|
+
expect(sarif.runs[0].properties.resolvedIssuesCount).toBe(1)
|
|
159
|
+
})
|
|
160
|
+
})
|
package/tests/trust-kpi.test.ts
CHANGED
|
@@ -16,6 +16,8 @@ describe('trust KPI aggregation', () => {
|
|
|
16
16
|
tempDir = mkdtempSync(join(tmpdir(), 'drift-kpi-aggregate-'))
|
|
17
17
|
|
|
18
18
|
writeFileSync(join(tempDir, 'trust-a.json'), JSON.stringify({
|
|
19
|
+
$schema: 'schemas/drift-trust.v1.json',
|
|
20
|
+
toolVersion: '1.3.0',
|
|
19
21
|
trust_score: 80,
|
|
20
22
|
merge_risk: 'LOW',
|
|
21
23
|
diff_context: {
|
|
@@ -32,6 +34,8 @@ describe('trust KPI aggregation', () => {
|
|
|
32
34
|
}, null, 2))
|
|
33
35
|
|
|
34
36
|
writeFileSync(join(tempDir, 'trust-b.json'), JSON.stringify({
|
|
37
|
+
$schema: 'schemas/drift-trust.v1.json',
|
|
38
|
+
toolVersion: '1.3.0',
|
|
35
39
|
trust_score: 60,
|
|
36
40
|
merge_risk: 'MEDIUM',
|
|
37
41
|
diff_context: {
|
|
@@ -48,6 +52,8 @@ describe('trust KPI aggregation', () => {
|
|
|
48
52
|
}, null, 2))
|
|
49
53
|
|
|
50
54
|
writeFileSync(join(tempDir, 'trust-c.json'), JSON.stringify({
|
|
55
|
+
$schema: 'schemas/drift-trust.v1.json',
|
|
56
|
+
toolVersion: '1.3.0',
|
|
51
57
|
trust_score: 30,
|
|
52
58
|
merge_risk: 'HIGH',
|
|
53
59
|
}, null, 2))
|
|
@@ -72,6 +78,8 @@ describe('trust KPI aggregation', () => {
|
|
|
72
78
|
tempDir = mkdtempSync(join(tmpdir(), 'drift-kpi-parse-'))
|
|
73
79
|
|
|
74
80
|
writeFileSync(join(tempDir, 'valid.json'), JSON.stringify({
|
|
81
|
+
$schema: 'schemas/drift-trust.v1.json',
|
|
82
|
+
toolVersion: '1.3.0',
|
|
75
83
|
trust_score: 70,
|
|
76
84
|
merge_risk: 'MEDIUM',
|
|
77
85
|
diff_context: {
|
|
@@ -84,16 +92,25 @@ describe('trust KPI aggregation', () => {
|
|
|
84
92
|
writeFileSync(join(tempDir, 'broken.json'), '{"trust_score":70')
|
|
85
93
|
writeFileSync(join(tempDir, 'invalid-shape.json'), JSON.stringify({ trust_score: 70 }, null, 2))
|
|
86
94
|
writeFileSync(join(tempDir, 'bad-diff.json'), JSON.stringify({
|
|
95
|
+
$schema: 'schemas/drift-trust.v1.json',
|
|
96
|
+
toolVersion: '1.3.0',
|
|
87
97
|
trust_score: 50,
|
|
88
98
|
merge_risk: 'HIGH',
|
|
89
99
|
diff_context: 'oops',
|
|
90
100
|
}, null, 2))
|
|
91
101
|
|
|
102
|
+
writeFileSync(join(tempDir, 'wrong-schema.json'), JSON.stringify({
|
|
103
|
+
$schema: 'schemas/drift-report.v1.json',
|
|
104
|
+
toolVersion: '1.3.0',
|
|
105
|
+
trust_score: 65,
|
|
106
|
+
merge_risk: 'MEDIUM',
|
|
107
|
+
}, null, 2))
|
|
108
|
+
|
|
92
109
|
const kpi = computeTrustKpis(tempDir)
|
|
93
110
|
|
|
94
|
-
expect(kpi.files.matched).toBe(
|
|
111
|
+
expect(kpi.files.matched).toBe(5)
|
|
95
112
|
expect(kpi.files.parsed).toBe(2)
|
|
96
|
-
expect(kpi.files.malformed).toBe(
|
|
113
|
+
expect(kpi.files.malformed).toBe(3)
|
|
97
114
|
expect(kpi.prsEvaluated).toBe(2)
|
|
98
115
|
|
|
99
116
|
const byCode = new Set(kpi.diagnostics.map((diagnostic) => diagnostic.code))
|
|
@@ -106,8 +123,18 @@ describe('trust KPI aggregation', () => {
|
|
|
106
123
|
tempDir = mkdtempSync(join(tmpdir(), 'drift-kpi-glob-'))
|
|
107
124
|
mkdirSync(join(tempDir, 'nested'))
|
|
108
125
|
|
|
109
|
-
writeFileSync(join(tempDir, 'trust-1.json'), JSON.stringify({
|
|
110
|
-
|
|
126
|
+
writeFileSync(join(tempDir, 'trust-1.json'), JSON.stringify({
|
|
127
|
+
$schema: 'schemas/drift-trust.v1.json',
|
|
128
|
+
toolVersion: '1.3.0',
|
|
129
|
+
trust_score: 90,
|
|
130
|
+
merge_risk: 'LOW',
|
|
131
|
+
}))
|
|
132
|
+
writeFileSync(join(tempDir, 'nested', 'trust-2.json'), JSON.stringify({
|
|
133
|
+
$schema: 'schemas/drift-trust.v1.json',
|
|
134
|
+
toolVersion: '1.3.0',
|
|
135
|
+
trust_score: 20,
|
|
136
|
+
merge_risk: 'CRITICAL',
|
|
137
|
+
}))
|
|
111
138
|
writeFileSync(join(tempDir, 'other.json'), JSON.stringify({ trust_score: 55, merge_risk: 'MEDIUM' }))
|
|
112
139
|
|
|
113
140
|
const pattern = join(tempDir, '**', 'trust-*.json')
|
package/tests/trust.test.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
formatTrustGatePolicyExplanation,
|
|
7
7
|
explainTrustGatePolicy,
|
|
8
8
|
formatTrustJson,
|
|
9
|
+
formatTrustJsonObject,
|
|
9
10
|
formatTrustMarkdown,
|
|
10
11
|
normalizeMergeRiskLevel,
|
|
11
12
|
renderTrustOutput,
|
|
@@ -201,6 +202,23 @@ describe('drift trust baseline', () => {
|
|
|
201
202
|
expect(renderTrustOutput(trust, { json: true, markdown: true })).toBe(formatTrustJson(trust))
|
|
202
203
|
})
|
|
203
204
|
|
|
205
|
+
it('adds schema metadata in trust JSON output without breaking trust payload', () => {
|
|
206
|
+
const report = createBaseReport({ targetPath: '/tmp/metadata' })
|
|
207
|
+
const trust = buildTrustReport(report)
|
|
208
|
+
|
|
209
|
+
const jsonObject = formatTrustJsonObject(trust)
|
|
210
|
+
expect(jsonObject.$schema).toBe('schemas/drift-trust.v1.json')
|
|
211
|
+
expect(typeof jsonObject.toolVersion).toBe('string')
|
|
212
|
+
expect(jsonObject.toolVersion.length).toBeGreaterThan(0)
|
|
213
|
+
expect(jsonObject.trust_score).toBe(trust.trust_score)
|
|
214
|
+
expect(jsonObject.merge_risk).toBe(trust.merge_risk)
|
|
215
|
+
expect(jsonObject.targetPath).toBe('/tmp/metadata')
|
|
216
|
+
|
|
217
|
+
const parsed = JSON.parse(formatTrustJson(trust)) as Record<string, unknown>
|
|
218
|
+
expect(parsed.$schema).toBe('schemas/drift-trust.v1.json')
|
|
219
|
+
expect(typeof parsed.toolVersion).toBe('string')
|
|
220
|
+
})
|
|
221
|
+
|
|
204
222
|
it('keeps baseline trust contract unchanged when advanced mode is disabled', () => {
|
|
205
223
|
const report = createBaseReport({
|
|
206
224
|
totalScore: 28,
|