@eduardbar/drift 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/actions/drift-review/README.md +4 -2
- package/.github/actions/drift-review/action.yml +22 -5
- package/.github/actions/drift-scan/README.md +3 -3
- package/.github/actions/drift-scan/action.yml +1 -1
- package/.github/workflows/publish-vscode.yml +1 -3
- package/.github/workflows/publish.yml +8 -0
- package/.github/workflows/quality.yml +15 -0
- package/.github/workflows/reusable-quality-checks.yml +95 -0
- package/.github/workflows/review-pr.yml +0 -1
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +14 -1
- package/README.md +30 -3
- package/benchmarks/fixtures/critical/drift.config.ts +21 -0
- package/benchmarks/fixtures/critical/src/app/user-service.ts +30 -0
- package/benchmarks/fixtures/critical/src/domain/entities.ts +19 -0
- package/benchmarks/fixtures/critical/src/domain/policies.ts +22 -0
- package/benchmarks/fixtures/critical/src/index.ts +10 -0
- package/benchmarks/fixtures/critical/src/infra/memory-user-repo.ts +14 -0
- package/benchmarks/perf-budget.v1.json +27 -0
- package/dist/benchmark.js +12 -0
- package/dist/cli.js +2 -2
- package/dist/doctor.d.ts +21 -0
- package/dist/doctor.js +10 -3
- package/dist/guard-baseline.d.ts +12 -0
- package/dist/guard-baseline.js +57 -0
- package/dist/guard-metrics.d.ts +6 -0
- package/dist/guard-metrics.js +39 -0
- package/dist/guard-types.d.ts +2 -1
- package/dist/guard.d.ts +3 -1
- package/dist/guard.js +9 -70
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/init.js +1 -1
- package/dist/output-metadata.d.ts +2 -0
- package/dist/output-metadata.js +2 -0
- package/dist/trust.d.ts +2 -1
- package/dist/trust.js +1 -1
- package/dist/types.d.ts +1 -1
- package/docs/AGENTS.md +1 -1
- package/package.json +10 -4
- package/schemas/drift-doctor.v1.json +57 -0
- package/schemas/drift-guard.v1.json +298 -0
- package/scripts/check-docs-drift.mjs +154 -0
- package/scripts/check-performance-budget.mjs +360 -0
- package/scripts/check-runtime-policy.mjs +66 -0
- package/src/benchmark.ts +17 -0
- package/src/cli.ts +2 -2
- package/src/doctor.ts +15 -3
- package/src/guard-baseline.ts +74 -0
- package/src/guard-metrics.ts +52 -0
- package/src/guard-types.ts +3 -1
- package/src/guard.ts +14 -90
- package/src/index.ts +1 -0
- package/src/init.ts +1 -1
- package/src/output-metadata.ts +2 -0
- package/src/trust.ts +1 -1
- package/src/types.ts +1 -0
- package/tests/ci-quality-matrix.test.ts +37 -0
- package/tests/ci-smoke-gate.test.ts +26 -0
- package/tests/ci-version-alignment.test.ts +93 -0
- package/tests/docs-drift-check.test.ts +115 -0
- package/tests/new-features.test.ts +2 -2
- package/tests/perf-budget-check.test.ts +146 -0
- package/tests/phase1-init-doctor-guard.test.ts +104 -2
- package/tests/runtime-policy-alignment.test.ts +46 -0
- package/vitest.config.ts +2 -0
|
@@ -4,10 +4,95 @@ import { tmpdir } from 'node:os'
|
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
import { runDoctor } from '../src/doctor.js'
|
|
6
6
|
import { runInit } from '../src/init.js'
|
|
7
|
-
import { evaluateGuard, runGuard } from '../src/guard.js'
|
|
7
|
+
import { evaluateGuard, formatGuardJsonObject, runGuard } from '../src/guard.js'
|
|
8
8
|
import { analyzeProject } from '../src/analyzer.js'
|
|
9
9
|
import { buildReport } from '../src/reporter.js'
|
|
10
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
|
+
|
|
11
96
|
function createTempDir(prefix: string): string {
|
|
12
97
|
return mkdtempSync(join(tmpdir(), prefix))
|
|
13
98
|
}
|
|
@@ -60,6 +145,8 @@ describe('phase 1: doctor/init/guard', () => {
|
|
|
60
145
|
|
|
61
146
|
await runDoctor(projectDir, { json: true })
|
|
62
147
|
const report = JSON.parse(output.join('')) as {
|
|
148
|
+
$schema: string
|
|
149
|
+
toolVersion: string
|
|
63
150
|
targetPath: string
|
|
64
151
|
node: { version: string; major: number; supported: boolean }
|
|
65
152
|
project: {
|
|
@@ -72,6 +159,12 @@ describe('phase 1: doctor/init/guard', () => {
|
|
|
72
159
|
}
|
|
73
160
|
}
|
|
74
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)
|
|
75
168
|
expect(report.targetPath).toBe(projectDir)
|
|
76
169
|
expect(typeof report.node.version).toBe('string')
|
|
77
170
|
expect(typeof report.node.major).toBe('number')
|
|
@@ -82,6 +175,7 @@ describe('phase 1: doctor/init/guard', () => {
|
|
|
82
175
|
expect(report.project.sourceFilesCount).toBe(2)
|
|
83
176
|
expect(report.project.driftConfigFile).toBe('drift.config.ts')
|
|
84
177
|
expect(typeof report.project.lowMemorySuggested).toBe('boolean')
|
|
178
|
+
expect(schemaErrors).toEqual([])
|
|
85
179
|
})
|
|
86
180
|
})
|
|
87
181
|
|
|
@@ -113,6 +207,7 @@ describe('phase 1: doctor/init/guard', () => {
|
|
|
113
207
|
const workflowPath = join(projectDir, '.github', 'workflows', 'drift-review.yml')
|
|
114
208
|
const workflow = readFileSync(workflowPath, 'utf8')
|
|
115
209
|
expect(workflow).toContain('name: drift PR Review')
|
|
210
|
+
expect(workflow).toContain('node-version: 20')
|
|
116
211
|
expect(workflow).toContain('npx drift review --base')
|
|
117
212
|
})
|
|
118
213
|
|
|
@@ -176,6 +271,9 @@ describe('phase 1: doctor/init/guard', () => {
|
|
|
176
271
|
budget: 0,
|
|
177
272
|
bySeverity: { error: 0, warning: 0, info: 0 },
|
|
178
273
|
})
|
|
274
|
+
const resultJson = formatGuardJsonObject(result)
|
|
275
|
+
const schema = loadSchema('drift-guard.v1.json')
|
|
276
|
+
const schemaErrors = validateAgainstSchema(schema, JSON.parse(JSON.stringify(resultJson)))
|
|
179
277
|
|
|
180
278
|
expect(result.mode).toBe('baseline')
|
|
181
279
|
expect(result.passed).toBe(true)
|
|
@@ -184,7 +282,11 @@ describe('phase 1: doctor/init/guard', () => {
|
|
|
184
282
|
expect(result.metrics.severityDelta).toEqual({ error: 0, warning: 0, info: 0 })
|
|
185
283
|
expect(result.checks.some((check) => check.id === 'no-regression-score')).toBe(true)
|
|
186
284
|
expect(result.checks.some((check) => check.id === 'no-regression-total-issues')).toBe(true)
|
|
187
|
-
|
|
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)
|
|
188
290
|
|
|
189
291
|
it('throws when guard has no baseRef and no baseline', async () => {
|
|
190
292
|
const projectDir = createTempDir('drift-guard-missing-anchor-')
|
|
@@ -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
|
+
})
|