@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.
Files changed (66) hide show
  1. package/.github/actions/drift-review/README.md +4 -2
  2. package/.github/actions/drift-review/action.yml +22 -5
  3. package/.github/actions/drift-scan/README.md +3 -3
  4. package/.github/actions/drift-scan/action.yml +1 -1
  5. package/.github/workflows/publish-vscode.yml +1 -3
  6. package/.github/workflows/publish.yml +8 -0
  7. package/.github/workflows/quality.yml +15 -0
  8. package/.github/workflows/reusable-quality-checks.yml +95 -0
  9. package/.github/workflows/review-pr.yml +0 -1
  10. package/AGENTS.md +2 -2
  11. package/CHANGELOG.md +14 -1
  12. package/README.md +30 -3
  13. package/benchmarks/fixtures/critical/drift.config.ts +21 -0
  14. package/benchmarks/fixtures/critical/src/app/user-service.ts +30 -0
  15. package/benchmarks/fixtures/critical/src/domain/entities.ts +19 -0
  16. package/benchmarks/fixtures/critical/src/domain/policies.ts +22 -0
  17. package/benchmarks/fixtures/critical/src/index.ts +10 -0
  18. package/benchmarks/fixtures/critical/src/infra/memory-user-repo.ts +14 -0
  19. package/benchmarks/perf-budget.v1.json +27 -0
  20. package/dist/benchmark.js +12 -0
  21. package/dist/cli.js +2 -2
  22. package/dist/doctor.d.ts +21 -0
  23. package/dist/doctor.js +10 -3
  24. package/dist/guard-baseline.d.ts +12 -0
  25. package/dist/guard-baseline.js +57 -0
  26. package/dist/guard-metrics.d.ts +6 -0
  27. package/dist/guard-metrics.js +39 -0
  28. package/dist/guard-types.d.ts +2 -1
  29. package/dist/guard.d.ts +3 -1
  30. package/dist/guard.js +9 -70
  31. package/dist/index.d.ts +1 -1
  32. package/dist/index.js +1 -1
  33. package/dist/init.js +1 -1
  34. package/dist/output-metadata.d.ts +2 -0
  35. package/dist/output-metadata.js +2 -0
  36. package/dist/trust.d.ts +2 -1
  37. package/dist/trust.js +1 -1
  38. package/dist/types.d.ts +1 -1
  39. package/docs/AGENTS.md +1 -1
  40. package/package.json +10 -4
  41. package/schemas/drift-doctor.v1.json +57 -0
  42. package/schemas/drift-guard.v1.json +298 -0
  43. package/scripts/check-docs-drift.mjs +154 -0
  44. package/scripts/check-performance-budget.mjs +360 -0
  45. package/scripts/check-runtime-policy.mjs +66 -0
  46. package/src/benchmark.ts +17 -0
  47. package/src/cli.ts +2 -2
  48. package/src/doctor.ts +15 -3
  49. package/src/guard-baseline.ts +74 -0
  50. package/src/guard-metrics.ts +52 -0
  51. package/src/guard-types.ts +3 -1
  52. package/src/guard.ts +14 -90
  53. package/src/index.ts +1 -0
  54. package/src/init.ts +1 -1
  55. package/src/output-metadata.ts +2 -0
  56. package/src/trust.ts +1 -1
  57. package/src/types.ts +1 -0
  58. package/tests/ci-quality-matrix.test.ts +37 -0
  59. package/tests/ci-smoke-gate.test.ts +26 -0
  60. package/tests/ci-version-alignment.test.ts +93 -0
  61. package/tests/docs-drift-check.test.ts +115 -0
  62. package/tests/new-features.test.ts +2 -2
  63. package/tests/perf-budget-check.test.ts +146 -0
  64. package/tests/phase1-init-doctor-guard.test.ts +104 -2
  65. package/tests/runtime-policy-alignment.test.ts +46 -0
  66. 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
+ })
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'],