@eduardbar/drift 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/.gga +50 -0
  2. package/.github/actions/drift-review/README.md +62 -0
  3. package/.github/actions/drift-review/action.yml +148 -0
  4. package/.github/actions/drift-scan/README.md +28 -32
  5. package/.github/actions/drift-scan/action.yml +78 -14
  6. package/.github/workflows/publish-vscode.yml +1 -3
  7. package/.github/workflows/publish.yml +8 -0
  8. package/.github/workflows/quality.yml +15 -0
  9. package/.github/workflows/reusable-quality-checks.yml +95 -0
  10. package/.github/workflows/review-pr.yml +33 -41
  11. package/AGENTS.md +75 -251
  12. package/CHANGELOG.md +41 -0
  13. package/README.md +177 -43
  14. package/benchmarks/fixtures/critical/drift.config.ts +21 -0
  15. package/benchmarks/fixtures/critical/src/app/user-service.ts +30 -0
  16. package/benchmarks/fixtures/critical/src/domain/entities.ts +19 -0
  17. package/benchmarks/fixtures/critical/src/domain/policies.ts +22 -0
  18. package/benchmarks/fixtures/critical/src/index.ts +10 -0
  19. package/benchmarks/fixtures/critical/src/infra/memory-user-repo.ts +14 -0
  20. package/benchmarks/perf-budget.v1.json +27 -0
  21. package/dist/benchmark.d.ts +1 -1
  22. package/dist/benchmark.js +83 -52
  23. package/dist/cli.js +243 -8
  24. package/dist/config.js +16 -2
  25. package/dist/diff.js +42 -50
  26. package/dist/doctor.d.ts +26 -0
  27. package/dist/doctor.js +140 -0
  28. package/dist/format.d.ts +17 -0
  29. package/dist/format.js +45 -0
  30. package/dist/guard-baseline.d.ts +12 -0
  31. package/dist/guard-baseline.js +57 -0
  32. package/dist/guard-metrics.d.ts +6 -0
  33. package/dist/guard-metrics.js +39 -0
  34. package/dist/guard-types.d.ts +58 -0
  35. package/dist/guard-types.js +2 -0
  36. package/dist/guard.d.ts +16 -0
  37. package/dist/guard.js +178 -0
  38. package/dist/index.d.ts +10 -3
  39. package/dist/index.js +4 -1
  40. package/dist/init.d.ts +15 -0
  41. package/dist/init.js +273 -0
  42. package/dist/map-cycles.d.ts +2 -0
  43. package/dist/map-cycles.js +34 -0
  44. package/dist/map-svg.d.ts +19 -0
  45. package/dist/map-svg.js +97 -0
  46. package/dist/map.js +78 -138
  47. package/dist/metrics.js +70 -55
  48. package/dist/output-metadata.d.ts +15 -0
  49. package/dist/output-metadata.js +19 -0
  50. package/dist/plugins-capabilities.d.ts +4 -0
  51. package/dist/plugins-capabilities.js +21 -0
  52. package/dist/plugins-messages.d.ts +10 -0
  53. package/dist/plugins-messages.js +16 -0
  54. package/dist/plugins-rules.d.ts +9 -0
  55. package/dist/plugins-rules.js +137 -0
  56. package/dist/plugins.d.ts +1 -1
  57. package/dist/plugins.js +45 -142
  58. package/dist/reporter-constants.d.ts +16 -0
  59. package/dist/reporter-constants.js +39 -0
  60. package/dist/reporter.d.ts +3 -3
  61. package/dist/reporter.js +35 -55
  62. package/dist/review.d.ts +2 -1
  63. package/dist/review.js +2 -1
  64. package/dist/rules/phase3-configurable.js +23 -15
  65. package/dist/saas/constants.d.ts +15 -0
  66. package/dist/saas/constants.js +48 -0
  67. package/dist/saas/dashboard.d.ts +8 -0
  68. package/dist/saas/dashboard.js +132 -0
  69. package/dist/saas/errors.d.ts +19 -0
  70. package/dist/saas/errors.js +37 -0
  71. package/dist/saas/helpers.d.ts +21 -0
  72. package/dist/saas/helpers.js +110 -0
  73. package/dist/saas/ingest.d.ts +3 -0
  74. package/dist/saas/ingest.js +249 -0
  75. package/dist/saas/organization.d.ts +5 -0
  76. package/dist/saas/organization.js +82 -0
  77. package/dist/saas/plan-change.d.ts +10 -0
  78. package/dist/saas/plan-change.js +15 -0
  79. package/dist/saas/store.d.ts +21 -0
  80. package/dist/saas/store.js +159 -0
  81. package/dist/saas/types.d.ts +191 -0
  82. package/dist/saas/types.js +2 -0
  83. package/dist/saas.d.ts +8 -218
  84. package/dist/saas.js +7 -761
  85. package/dist/sarif.d.ts +74 -0
  86. package/dist/sarif.js +122 -0
  87. package/dist/trust-advanced.d.ts +14 -0
  88. package/dist/trust-advanced.js +65 -0
  89. package/dist/trust-kpi-fs.d.ts +3 -0
  90. package/dist/trust-kpi-fs.js +141 -0
  91. package/dist/trust-kpi-parse.d.ts +7 -0
  92. package/dist/trust-kpi-parse.js +186 -0
  93. package/dist/trust-kpi-types.d.ts +16 -0
  94. package/dist/trust-kpi-types.js +2 -0
  95. package/dist/trust-kpi.d.ts +1 -3
  96. package/dist/trust-kpi.js +6 -266
  97. package/dist/trust-policy.d.ts +32 -0
  98. package/dist/trust-policy.js +160 -0
  99. package/dist/trust-render.d.ts +9 -0
  100. package/dist/trust-render.js +54 -0
  101. package/dist/trust-scoring.d.ts +9 -0
  102. package/dist/trust-scoring.js +208 -0
  103. package/dist/trust.d.ts +5 -32
  104. package/dist/trust.js +29 -432
  105. package/dist/types/app.d.ts +30 -0
  106. package/dist/types/app.js +2 -0
  107. package/dist/types/config.d.ts +25 -0
  108. package/dist/types/config.js +2 -0
  109. package/dist/types/core.d.ts +100 -0
  110. package/dist/types/core.js +2 -0
  111. package/dist/types/diff.d.ts +55 -0
  112. package/dist/types/diff.js +2 -0
  113. package/dist/types/plugin.d.ts +41 -0
  114. package/dist/types/plugin.js +2 -0
  115. package/dist/types/trust.d.ts +120 -0
  116. package/dist/types/trust.js +2 -0
  117. package/dist/types.d.ts +8 -365
  118. package/docs/AGENTS.md +1 -1
  119. package/docs/release-notes-draft.md +40 -0
  120. package/docs/rules-catalog.md +49 -0
  121. package/docs/trust-core-release-checklist.md +37 -5
  122. package/package.json +11 -4
  123. package/packages/vscode-drift/src/code-actions.ts +1 -1
  124. package/schemas/drift-ai-output.v1.json +162 -0
  125. package/schemas/drift-doctor.v1.json +57 -0
  126. package/schemas/drift-guard.v1.json +298 -0
  127. package/schemas/drift-report.v1.json +151 -0
  128. package/schemas/drift-trust.v1.json +131 -0
  129. package/scripts/check-docs-drift.mjs +154 -0
  130. package/scripts/check-performance-budget.mjs +360 -0
  131. package/scripts/check-runtime-policy.mjs +66 -0
  132. package/scripts/smoke-repo.mjs +394 -0
  133. package/src/benchmark.ts +92 -53
  134. package/src/cli.ts +285 -13
  135. package/src/config.ts +19 -2
  136. package/src/diff.ts +57 -48
  137. package/src/doctor.ts +185 -0
  138. package/src/format.ts +81 -0
  139. package/src/guard-baseline.ts +74 -0
  140. package/src/guard-metrics.ts +52 -0
  141. package/src/guard-types.ts +66 -0
  142. package/src/guard.ts +248 -0
  143. package/src/index.ts +36 -0
  144. package/src/init.ts +298 -0
  145. package/src/map-cycles.ts +38 -0
  146. package/src/map-svg.ts +124 -0
  147. package/src/map.ts +111 -142
  148. package/src/metrics.ts +78 -59
  149. package/src/output-metadata.ts +32 -0
  150. package/src/plugins-capabilities.ts +36 -0
  151. package/src/plugins-messages.ts +35 -0
  152. package/src/plugins-rules.ts +296 -0
  153. package/src/plugins.ts +76 -283
  154. package/src/reporter-constants.ts +46 -0
  155. package/src/reporter.ts +64 -65
  156. package/src/review.ts +4 -2
  157. package/src/rules/phase3-configurable.ts +39 -26
  158. package/src/saas/constants.ts +56 -0
  159. package/src/saas/dashboard.ts +172 -0
  160. package/src/saas/errors.ts +45 -0
  161. package/src/saas/helpers.ts +140 -0
  162. package/src/saas/ingest.ts +278 -0
  163. package/src/saas/organization.ts +99 -0
  164. package/src/saas/plan-change.ts +19 -0
  165. package/src/saas/store.ts +172 -0
  166. package/src/saas/types.ts +216 -0
  167. package/src/saas.ts +49 -1031
  168. package/src/sarif.ts +232 -0
  169. package/src/trust-advanced.ts +99 -0
  170. package/src/trust-kpi-fs.ts +169 -0
  171. package/src/trust-kpi-parse.ts +219 -0
  172. package/src/trust-kpi-types.ts +19 -0
  173. package/src/trust-kpi.ts +8 -316
  174. package/src/trust-policy.ts +246 -0
  175. package/src/trust-render.ts +61 -0
  176. package/src/trust-scoring.ts +231 -0
  177. package/src/trust.ts +62 -576
  178. package/src/types/app.ts +30 -0
  179. package/src/types/config.ts +27 -0
  180. package/src/types/core.ts +105 -0
  181. package/src/types/diff.ts +61 -0
  182. package/src/types/plugin.ts +46 -0
  183. package/src/types/trust.ts +134 -0
  184. package/src/types.ts +79 -409
  185. package/tests/ci-quality-matrix.test.ts +37 -0
  186. package/tests/ci-smoke-gate.test.ts +26 -0
  187. package/tests/ci-version-alignment.test.ts +93 -0
  188. package/tests/cli-sarif.test.ts +92 -0
  189. package/tests/docs-drift-check.test.ts +115 -0
  190. package/tests/format.test.ts +157 -0
  191. package/tests/new-features.test.ts +11 -3
  192. package/tests/perf-budget-check.test.ts +146 -0
  193. package/tests/phase1-init-doctor-guard.test.ts +301 -0
  194. package/tests/runtime-policy-alignment.test.ts +46 -0
  195. package/tests/sarif.test.ts +160 -0
  196. package/tests/trust-kpi.test.ts +31 -4
  197. package/tests/trust.test.ts +18 -0
  198. package/vitest.config.ts +2 -0
@@ -0,0 +1,301 @@
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, formatGuardJsonObject, runGuard } from '../src/guard.js'
8
+ import { analyzeProject } from '../src/analyzer.js'
9
+ import { buildReport } from '../src/reporter.js'
10
+
11
+ type JsonSchema = {
12
+ type?: string | string[]
13
+ required?: string[]
14
+ properties?: Record<string, JsonSchema>
15
+ additionalProperties?: boolean | JsonSchema
16
+ items?: JsonSchema
17
+ const?: unknown
18
+ enum?: unknown[]
19
+ }
20
+
21
+ function validateAgainstSchema(schema: JsonSchema, value: unknown, path = '$'): string[] {
22
+ const errors: string[] = []
23
+
24
+ if (schema.const !== undefined && value !== schema.const) {
25
+ errors.push(`${path} must be ${JSON.stringify(schema.const)}`)
26
+ return errors
27
+ }
28
+
29
+ if (schema.enum && !schema.enum.includes(value)) {
30
+ errors.push(`${path} must be one of ${JSON.stringify(schema.enum)}`)
31
+ return errors
32
+ }
33
+
34
+ if (schema.type) {
35
+ const allowedTypes = Array.isArray(schema.type) ? schema.type : [schema.type]
36
+ const actualType = value === null ? 'null' : Array.isArray(value) ? 'array' : typeof value
37
+ if (!allowedTypes.includes(actualType)) {
38
+ errors.push(`${path} must be type ${allowedTypes.join('|')}, got ${actualType}`)
39
+ return errors
40
+ }
41
+ }
42
+
43
+ if (schema.type === 'array' && schema.items && Array.isArray(value)) {
44
+ for (let i = 0; i < value.length; i += 1) {
45
+ errors.push(...validateAgainstSchema(schema.items, value[i], `${path}[${i}]`))
46
+ }
47
+ return errors
48
+ }
49
+
50
+ if (schema.type === 'object' && value !== null && typeof value === 'object' && !Array.isArray(value)) {
51
+ const objectValue = value as Record<string, unknown>
52
+ const required = schema.required ?? []
53
+ const properties = schema.properties ?? {}
54
+
55
+ for (const key of required) {
56
+ if (!(key in objectValue)) {
57
+ errors.push(`${path}.${key} is required`)
58
+ }
59
+ }
60
+
61
+ for (const [key, propertySchema] of Object.entries(properties)) {
62
+ if (key in objectValue) {
63
+ errors.push(...validateAgainstSchema(propertySchema, objectValue[key], `${path}.${key}`))
64
+ }
65
+ }
66
+
67
+ if (schema.additionalProperties === false) {
68
+ for (const key of Object.keys(objectValue)) {
69
+ if (!(key in properties)) {
70
+ errors.push(`${path}.${key} is not allowed`)
71
+ }
72
+ }
73
+ }
74
+
75
+ if (
76
+ schema.additionalProperties &&
77
+ typeof schema.additionalProperties === 'object' &&
78
+ schema.additionalProperties !== null
79
+ ) {
80
+ for (const [key, propValue] of Object.entries(objectValue)) {
81
+ if (!(key in properties)) {
82
+ errors.push(...validateAgainstSchema(schema.additionalProperties, propValue, `${path}.${key}`))
83
+ }
84
+ }
85
+ }
86
+ }
87
+
88
+ return errors
89
+ }
90
+
91
+ function loadSchema(schemaFileName: string): JsonSchema {
92
+ const raw = readFileSync(join(process.cwd(), 'schemas', schemaFileName), 'utf8')
93
+ return JSON.parse(raw) as JsonSchema
94
+ }
95
+
96
+ function createTempDir(prefix: string): string {
97
+ return mkdtempSync(join(tmpdir(), prefix))
98
+ }
99
+
100
+ describe('phase 1: doctor/init/guard', () => {
101
+ const tempDirs: string[] = []
102
+
103
+ afterEach(() => {
104
+ vi.restoreAllMocks()
105
+ for (const dir of tempDirs) {
106
+ rmSync(dir, { recursive: true, force: true })
107
+ }
108
+ tempDirs.length = 0
109
+ })
110
+
111
+ describe('runDoctor', () => {
112
+ it('prints a basic diagnostic report and exits with code 0', async () => {
113
+ const projectDir = createTempDir('drift-doctor-basic-')
114
+ tempDirs.push(projectDir)
115
+ writeFileSync(join(projectDir, 'index.ts'), 'export const value = 1\n')
116
+
117
+ const output: string[] = []
118
+ vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => {
119
+ output.push(String(chunk))
120
+ return true
121
+ })
122
+
123
+ const exitCode = await runDoctor(projectDir)
124
+ const text = output.join('')
125
+
126
+ expect(exitCode).toBe(0)
127
+ expect(text).toContain('drift doctor')
128
+ expect(text).toContain('Source files (.ts/.tsx/.js/.jsx): 1')
129
+ })
130
+
131
+ it('prints valid JSON output with expected shape', async () => {
132
+ const projectDir = createTempDir('drift-doctor-json-')
133
+ tempDirs.push(projectDir)
134
+ mkdirSync(join(projectDir, 'src'))
135
+ writeFileSync(join(projectDir, 'package.json'), JSON.stringify({ type: 'module' }, null, 2))
136
+ writeFileSync(join(projectDir, 'tsconfig.json'), '{"compilerOptions":{}}\n')
137
+ writeFileSync(join(projectDir, 'drift.config.ts'), 'export default {}\n')
138
+ writeFileSync(join(projectDir, 'src', 'app.ts'), 'export const answer = 42\n')
139
+
140
+ const output: string[] = []
141
+ vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => {
142
+ output.push(String(chunk))
143
+ return true
144
+ })
145
+
146
+ await runDoctor(projectDir, { json: true })
147
+ const report = JSON.parse(output.join('')) as {
148
+ $schema: string
149
+ toolVersion: string
150
+ targetPath: string
151
+ node: { version: string; major: number; supported: boolean }
152
+ project: {
153
+ packageJsonFound: boolean
154
+ esm: boolean
155
+ tsconfigFound: boolean
156
+ sourceFilesCount: number
157
+ lowMemorySuggested: boolean
158
+ driftConfigFile: string | null
159
+ }
160
+ }
161
+
162
+ const schema = loadSchema('drift-doctor.v1.json')
163
+ const schemaErrors = validateAgainstSchema(schema, report)
164
+
165
+ expect(report.$schema).toBe('schemas/drift-doctor.v1.json')
166
+ expect(typeof report.toolVersion).toBe('string')
167
+ expect(report.toolVersion.length).toBeGreaterThan(0)
168
+ expect(report.targetPath).toBe(projectDir)
169
+ expect(typeof report.node.version).toBe('string')
170
+ expect(typeof report.node.major).toBe('number')
171
+ expect(typeof report.node.supported).toBe('boolean')
172
+ expect(report.project.packageJsonFound).toBe(true)
173
+ expect(report.project.esm).toBe(true)
174
+ expect(report.project.tsconfigFound).toBe(true)
175
+ expect(report.project.sourceFilesCount).toBe(2)
176
+ expect(report.project.driftConfigFile).toBe('drift.config.ts')
177
+ expect(typeof report.project.lowMemorySuggested).toBe('boolean')
178
+ expect(schemaErrors).toEqual([])
179
+ })
180
+ })
181
+
182
+ describe('runInit', () => {
183
+ it('creates drift.config.ts when using a valid preset', async () => {
184
+ const projectDir = createTempDir('drift-init-preset-')
185
+ tempDirs.push(projectDir)
186
+
187
+ await runInit(projectDir, { preset: 'node-backend' })
188
+
189
+ const generated = readFileSync(join(projectDir, 'drift.config.ts'), 'utf8')
190
+ expect(generated).toContain('satisfies DriftConfig')
191
+ expect(generated).toContain("name: 'api'")
192
+ })
193
+
194
+ it('throws on invalid preset', async () => {
195
+ const projectDir = createTempDir('drift-init-invalid-')
196
+ tempDirs.push(projectDir)
197
+
198
+ await expect(runInit(projectDir, { preset: 'invalid-preset' })).rejects.toThrow("Invalid preset 'invalid-preset'")
199
+ })
200
+
201
+ it('creates ci workflow when --ci flag is enabled', async () => {
202
+ const projectDir = createTempDir('drift-init-ci-')
203
+ tempDirs.push(projectDir)
204
+
205
+ await runInit(projectDir, { ci: true })
206
+
207
+ const workflowPath = join(projectDir, '.github', 'workflows', 'drift-review.yml')
208
+ const workflow = readFileSync(workflowPath, 'utf8')
209
+ expect(workflow).toContain('name: drift PR Review')
210
+ expect(workflow).toContain('node-version: 20')
211
+ expect(workflow).toContain('npx drift review --base')
212
+ })
213
+
214
+ it('prints no-actions message when no flags are provided', async () => {
215
+ const projectDir = createTempDir('drift-init-empty-')
216
+ tempDirs.push(projectDir)
217
+
218
+ const output: string[] = []
219
+ vi.spyOn(process.stdout, 'write').mockImplementation((chunk) => {
220
+ output.push(String(chunk))
221
+ return true
222
+ })
223
+
224
+ await runInit(projectDir, {})
225
+
226
+ expect(output.join('')).toContain('No actions taken. Use --preset, --ci, or --baseline flags.')
227
+ })
228
+ })
229
+
230
+ describe('guard', () => {
231
+ it('evaluateGuard applies no-regression, budget and severity checks', () => {
232
+ const evaluation = evaluateGuard({
233
+ metrics: {
234
+ scoreDelta: 3,
235
+ totalIssuesDelta: 1,
236
+ severityDelta: { error: 1, warning: 0, info: 0 },
237
+ },
238
+ budget: 2,
239
+ bySeverity: { error: 0, warning: 1, info: 0 },
240
+ enforceNoRegression: {
241
+ score: true,
242
+ totalIssues: true,
243
+ },
244
+ })
245
+
246
+ expect(evaluation.passed).toBe(false)
247
+ expect(evaluation.checks.some((check) => check.id === 'no-regression-score' && !check.passed)).toBe(true)
248
+ expect(evaluation.checks.some((check) => check.id === 'no-regression-total-issues' && !check.passed)).toBe(true)
249
+ expect(evaluation.checks.some((check) => check.id === 'budget-total-delta' && !check.passed)).toBe(true)
250
+ expect(evaluation.checks.some((check) => check.id === 'severity-error' && !check.passed)).toBe(true)
251
+ })
252
+
253
+ it('runs in baseline mode with inline baseline fixture', async () => {
254
+ const projectDir = createTempDir('drift-guard-baseline-')
255
+ tempDirs.push(projectDir)
256
+ writeFileSync(join(projectDir, 'main.ts'), 'export const value: any = 42\n')
257
+
258
+ const reports = analyzeProject(projectDir)
259
+ const current = buildReport(projectDir, reports)
260
+
261
+ const result = await runGuard(projectDir, {
262
+ baseline: {
263
+ score: current.totalScore,
264
+ totalIssues: current.totalIssues,
265
+ bySeverity: {
266
+ error: current.summary.errors,
267
+ warning: current.summary.warnings,
268
+ info: current.summary.infos,
269
+ },
270
+ },
271
+ budget: 0,
272
+ bySeverity: { error: 0, warning: 0, info: 0 },
273
+ })
274
+ const resultJson = formatGuardJsonObject(result)
275
+ const schema = loadSchema('drift-guard.v1.json')
276
+ const schemaErrors = validateAgainstSchema(schema, JSON.parse(JSON.stringify(resultJson)))
277
+
278
+ expect(result.mode).toBe('baseline')
279
+ expect(result.passed).toBe(true)
280
+ expect(result.metrics.scoreDelta).toBe(0)
281
+ expect(result.metrics.totalIssuesDelta).toBe(0)
282
+ expect(result.metrics.severityDelta).toEqual({ error: 0, warning: 0, info: 0 })
283
+ expect(result.checks.some((check) => check.id === 'no-regression-score')).toBe(true)
284
+ expect(result.checks.some((check) => check.id === 'no-regression-total-issues')).toBe(true)
285
+ expect(resultJson.$schema).toBe('schemas/drift-guard.v1.json')
286
+ expect(typeof resultJson.toolVersion).toBe('string')
287
+ expect(resultJson.toolVersion.length).toBeGreaterThan(0)
288
+ expect(schemaErrors).toEqual([])
289
+ }, 15000)
290
+
291
+ it('throws when guard has no baseRef and no baseline', async () => {
292
+ const projectDir = createTempDir('drift-guard-missing-anchor-')
293
+ tempDirs.push(projectDir)
294
+ writeFileSync(join(projectDir, 'main.ts'), 'export const value = 1\n')
295
+
296
+ await expect(runGuard(projectDir, {})).rejects.toThrow(
297
+ 'Guard requires a comparison point: provide baseRef or a baseline (inline or file).',
298
+ )
299
+ })
300
+ })
301
+ })
@@ -0,0 +1,46 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { readFileSync } from 'node:fs'
3
+ import { join } from 'node:path'
4
+
5
+ const repoRoot = process.cwd()
6
+ const EXPECTED_ENGINE_RANGE = '^20.0.0 || ^22.0.0'
7
+ const EXPECTED_NODE_MATRIX = '["20", "22"]'
8
+ const EXPECTED_README_RUNTIME = '**Runtime:** Node.js 20.x and 22.x (LTS)'
9
+
10
+ function readRepoFile(path: string): string {
11
+ return readFileSync(join(repoRoot, path), 'utf8')
12
+ }
13
+
14
+ describe('runtime support policy alignment', () => {
15
+ it('keeps package engines aligned with supported Node policy', () => {
16
+ const pkg = JSON.parse(readRepoFile('package.json')) as { engines?: { node?: string } }
17
+ expect(pkg.engines?.node).toBe(EXPECTED_ENGINE_RANGE)
18
+ })
19
+
20
+ it('keeps CI workflows aligned with supported Node policy', () => {
21
+ const reusable = readRepoFile('.github/workflows/reusable-quality-checks.yml')
22
+ const quality = readRepoFile('.github/workflows/quality.yml')
23
+ const initTemplate = readRepoFile('src/init.ts')
24
+
25
+ expect(reusable).toContain(`default: '${EXPECTED_NODE_MATRIX}'`)
26
+ expect(quality).toContain(`node_versions: '${EXPECTED_NODE_MATRIX}'`)
27
+ expect(reusable).toContain('run: npm run check:runtime-policy')
28
+ expect(initTemplate).toContain('node-version: 20')
29
+ })
30
+
31
+ it('keeps runtime docs and doctor messaging aligned with supported minimum', () => {
32
+ const readme = readRepoFile('README.md')
33
+ const doctor = readRepoFile('src/doctor.ts')
34
+
35
+ expect(readme).toContain(EXPECTED_README_RUNTIME)
36
+ expect(doctor).toContain('const MIN_SUPPORTED_NODE_MAJOR = 20')
37
+ expect(doctor).toContain('Node runtime below supported minimum (>=20)')
38
+ })
39
+
40
+ it('documents dependency-driven minimum from lockfile constraints', () => {
41
+ const lockfile = readRepoFile('package-lock.json')
42
+
43
+ expect(lockfile).toContain('"node_modules/commander"')
44
+ expect(lockfile).toContain('"node": ">=20"')
45
+ })
46
+ })
@@ -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,
package/vitest.config.ts CHANGED
@@ -6,6 +6,8 @@ export default defineConfig({
6
6
  globals: true,
7
7
  environment: 'node',
8
8
  include: ['tests/**/*.test.ts'],
9
+ testTimeout: 15000,
10
+ hookTimeout: 15000,
9
11
  coverage: {
10
12
  provider: 'v8',
11
13
  include: ['src/**/*.ts'],