@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.
Files changed (168) hide show
  1. package/.gga +50 -0
  2. package/.github/actions/drift-review/README.md +60 -0
  3. package/.github/actions/drift-review/action.yml +131 -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/review-pr.yml +34 -41
  7. package/AGENTS.md +75 -251
  8. package/CHANGELOG.md +28 -0
  9. package/README.md +148 -41
  10. package/dist/benchmark.d.ts +1 -1
  11. package/dist/benchmark.js +71 -52
  12. package/dist/cli.js +243 -8
  13. package/dist/config.js +16 -2
  14. package/dist/diff.js +42 -50
  15. package/dist/doctor.d.ts +5 -0
  16. package/dist/doctor.js +133 -0
  17. package/dist/format.d.ts +17 -0
  18. package/dist/format.js +45 -0
  19. package/dist/guard-types.d.ts +57 -0
  20. package/dist/guard-types.js +2 -0
  21. package/dist/guard.d.ts +14 -0
  22. package/dist/guard.js +239 -0
  23. package/dist/index.d.ts +10 -3
  24. package/dist/index.js +4 -1
  25. package/dist/init.d.ts +15 -0
  26. package/dist/init.js +273 -0
  27. package/dist/map-cycles.d.ts +2 -0
  28. package/dist/map-cycles.js +34 -0
  29. package/dist/map-svg.d.ts +19 -0
  30. package/dist/map-svg.js +97 -0
  31. package/dist/map.js +78 -138
  32. package/dist/metrics.js +70 -55
  33. package/dist/output-metadata.d.ts +13 -0
  34. package/dist/output-metadata.js +17 -0
  35. package/dist/plugins-capabilities.d.ts +4 -0
  36. package/dist/plugins-capabilities.js +21 -0
  37. package/dist/plugins-messages.d.ts +10 -0
  38. package/dist/plugins-messages.js +16 -0
  39. package/dist/plugins-rules.d.ts +9 -0
  40. package/dist/plugins-rules.js +137 -0
  41. package/dist/plugins.d.ts +1 -1
  42. package/dist/plugins.js +45 -142
  43. package/dist/reporter-constants.d.ts +16 -0
  44. package/dist/reporter-constants.js +39 -0
  45. package/dist/reporter.d.ts +3 -3
  46. package/dist/reporter.js +35 -55
  47. package/dist/review.d.ts +2 -1
  48. package/dist/review.js +2 -1
  49. package/dist/rules/phase3-configurable.js +23 -15
  50. package/dist/saas/constants.d.ts +15 -0
  51. package/dist/saas/constants.js +48 -0
  52. package/dist/saas/dashboard.d.ts +8 -0
  53. package/dist/saas/dashboard.js +132 -0
  54. package/dist/saas/errors.d.ts +19 -0
  55. package/dist/saas/errors.js +37 -0
  56. package/dist/saas/helpers.d.ts +21 -0
  57. package/dist/saas/helpers.js +110 -0
  58. package/dist/saas/ingest.d.ts +3 -0
  59. package/dist/saas/ingest.js +249 -0
  60. package/dist/saas/organization.d.ts +5 -0
  61. package/dist/saas/organization.js +82 -0
  62. package/dist/saas/plan-change.d.ts +10 -0
  63. package/dist/saas/plan-change.js +15 -0
  64. package/dist/saas/store.d.ts +21 -0
  65. package/dist/saas/store.js +159 -0
  66. package/dist/saas/types.d.ts +191 -0
  67. package/dist/saas/types.js +2 -0
  68. package/dist/saas.d.ts +8 -218
  69. package/dist/saas.js +7 -761
  70. package/dist/sarif.d.ts +74 -0
  71. package/dist/sarif.js +122 -0
  72. package/dist/trust-advanced.d.ts +14 -0
  73. package/dist/trust-advanced.js +65 -0
  74. package/dist/trust-kpi-fs.d.ts +3 -0
  75. package/dist/trust-kpi-fs.js +141 -0
  76. package/dist/trust-kpi-parse.d.ts +7 -0
  77. package/dist/trust-kpi-parse.js +186 -0
  78. package/dist/trust-kpi-types.d.ts +16 -0
  79. package/dist/trust-kpi-types.js +2 -0
  80. package/dist/trust-kpi.d.ts +1 -3
  81. package/dist/trust-kpi.js +6 -266
  82. package/dist/trust-policy.d.ts +32 -0
  83. package/dist/trust-policy.js +160 -0
  84. package/dist/trust-render.d.ts +9 -0
  85. package/dist/trust-render.js +54 -0
  86. package/dist/trust-scoring.d.ts +9 -0
  87. package/dist/trust-scoring.js +208 -0
  88. package/dist/trust.d.ts +4 -32
  89. package/dist/trust.js +29 -432
  90. package/dist/types/app.d.ts +30 -0
  91. package/dist/types/app.js +2 -0
  92. package/dist/types/config.d.ts +25 -0
  93. package/dist/types/config.js +2 -0
  94. package/dist/types/core.d.ts +100 -0
  95. package/dist/types/core.js +2 -0
  96. package/dist/types/diff.d.ts +55 -0
  97. package/dist/types/diff.js +2 -0
  98. package/dist/types/plugin.d.ts +41 -0
  99. package/dist/types/plugin.js +2 -0
  100. package/dist/types/trust.d.ts +120 -0
  101. package/dist/types/trust.js +2 -0
  102. package/dist/types.d.ts +8 -365
  103. package/docs/release-notes-draft.md +40 -0
  104. package/docs/rules-catalog.md +49 -0
  105. package/docs/trust-core-release-checklist.md +37 -5
  106. package/package.json +3 -2
  107. package/packages/vscode-drift/src/code-actions.ts +1 -1
  108. package/schemas/drift-ai-output.v1.json +162 -0
  109. package/schemas/drift-report.v1.json +151 -0
  110. package/schemas/drift-trust.v1.json +131 -0
  111. package/scripts/smoke-repo.mjs +394 -0
  112. package/src/benchmark.ts +75 -53
  113. package/src/cli.ts +285 -13
  114. package/src/config.ts +19 -2
  115. package/src/diff.ts +57 -48
  116. package/src/doctor.ts +173 -0
  117. package/src/format.ts +81 -0
  118. package/src/guard-types.ts +64 -0
  119. package/src/guard.ts +324 -0
  120. package/src/index.ts +35 -0
  121. package/src/init.ts +298 -0
  122. package/src/map-cycles.ts +38 -0
  123. package/src/map-svg.ts +124 -0
  124. package/src/map.ts +111 -142
  125. package/src/metrics.ts +78 -59
  126. package/src/output-metadata.ts +30 -0
  127. package/src/plugins-capabilities.ts +36 -0
  128. package/src/plugins-messages.ts +35 -0
  129. package/src/plugins-rules.ts +296 -0
  130. package/src/plugins.ts +76 -283
  131. package/src/reporter-constants.ts +46 -0
  132. package/src/reporter.ts +64 -65
  133. package/src/review.ts +4 -2
  134. package/src/rules/phase3-configurable.ts +39 -26
  135. package/src/saas/constants.ts +56 -0
  136. package/src/saas/dashboard.ts +172 -0
  137. package/src/saas/errors.ts +45 -0
  138. package/src/saas/helpers.ts +140 -0
  139. package/src/saas/ingest.ts +278 -0
  140. package/src/saas/organization.ts +99 -0
  141. package/src/saas/plan-change.ts +19 -0
  142. package/src/saas/store.ts +172 -0
  143. package/src/saas/types.ts +216 -0
  144. package/src/saas.ts +49 -1031
  145. package/src/sarif.ts +232 -0
  146. package/src/trust-advanced.ts +99 -0
  147. package/src/trust-kpi-fs.ts +169 -0
  148. package/src/trust-kpi-parse.ts +219 -0
  149. package/src/trust-kpi-types.ts +19 -0
  150. package/src/trust-kpi.ts +8 -316
  151. package/src/trust-policy.ts +246 -0
  152. package/src/trust-render.ts +61 -0
  153. package/src/trust-scoring.ts +231 -0
  154. package/src/trust.ts +62 -576
  155. package/src/types/app.ts +30 -0
  156. package/src/types/config.ts +27 -0
  157. package/src/types/core.ts +105 -0
  158. package/src/types/diff.ts +61 -0
  159. package/src/types/plugin.ts +46 -0
  160. package/src/types/trust.ts +134 -0
  161. package/src/types.ts +78 -409
  162. package/tests/cli-sarif.test.ts +92 -0
  163. package/tests/format.test.ts +157 -0
  164. package/tests/new-features.test.ts +10 -2
  165. package/tests/phase1-init-doctor-guard.test.ts +199 -0
  166. package/tests/sarif.test.ts +160 -0
  167. package/tests/trust-kpi.test.ts +31 -4
  168. 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, type DriftReview } from '../src/review.js'
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
+ })
@@ -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(4)
111
+ expect(kpi.files.matched).toBe(5)
95
112
  expect(kpi.files.parsed).toBe(2)
96
- expect(kpi.files.malformed).toBe(2)
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({ trust_score: 90, merge_risk: 'LOW' }))
110
- writeFileSync(join(tempDir, 'nested', 'trust-2.json'), JSON.stringify({ trust_score: 20, merge_risk: 'CRITICAL' }))
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')
@@ -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,