@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,92 @@
1
+ import { afterEach, describe, expect, it } from 'vitest'
2
+ import { spawnSync } from 'node:child_process'
3
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
4
+ import { join } from 'node:path'
5
+ import { tmpdir } from 'node:os'
6
+
7
+ type CliResult = {
8
+ status: number | null
9
+ stdout: string
10
+ stderr: string
11
+ }
12
+
13
+ type SarifOutput = {
14
+ version: string
15
+ runs: Array<{
16
+ tool: {
17
+ driver: {
18
+ name: string
19
+ }
20
+ }
21
+ results?: Array<{
22
+ ruleId?: string
23
+ message?: {
24
+ text?: string
25
+ }
26
+ }>
27
+ }>
28
+ }
29
+
30
+ function runCli(args: string[]): CliResult {
31
+ const result = spawnSync(process.execPath, ['--import', 'tsx', 'src/cli.ts', ...args], {
32
+ cwd: process.cwd(),
33
+ encoding: 'utf8',
34
+ })
35
+
36
+ return {
37
+ status: result.status,
38
+ stdout: result.stdout,
39
+ stderr: result.stderr,
40
+ }
41
+ }
42
+
43
+ function expectValidSarifFrom(result: CliResult): SarifOutput {
44
+ expect(result.status).toBe(0)
45
+ expect(result.stderr).not.toContain('Error:')
46
+
47
+ const sarif = JSON.parse(result.stdout) as SarifOutput
48
+ expect(sarif.version).toBe('2.1.0')
49
+ expect(Array.isArray(sarif.runs)).toBe(true)
50
+ expect(sarif.runs.length).toBeGreaterThan(0)
51
+ expect(sarif.runs[0]?.tool.driver.name).toBe('drift')
52
+ expect(Array.isArray(sarif.runs[0]?.results)).toBe(true)
53
+
54
+ return sarif
55
+ }
56
+
57
+ describe('cli sarif output', () => {
58
+ let tmpDir = ''
59
+
60
+ afterEach(() => {
61
+ if (tmpDir) rmSync(tmpDir, { recursive: true, force: true })
62
+ tmpDir = ''
63
+ })
64
+
65
+ it('serializes scan --format sarif output as SARIF JSON', () => {
66
+ tmpDir = mkdtempSync(join(tmpdir(), 'drift-cli-sarif-scan-'))
67
+ writeFileSync(join(tmpDir, 'sample.ts'), 'console.log("debug")\n')
68
+
69
+ const result = runCli(['scan', tmpDir, '--format', 'sarif'])
70
+ const sarif = expectValidSarifFrom(result)
71
+ expect(sarif.runs[0]?.results?.some((entry) => entry.ruleId === 'debug-leftover')).toBe(true)
72
+ })
73
+
74
+ it('serializes ci --format sarif output as SARIF JSON', () => {
75
+ tmpDir = mkdtempSync(join(tmpdir(), 'drift-cli-sarif-ci-'))
76
+ writeFileSync(join(tmpDir, 'sample.ts'), 'console.log("debug")\n')
77
+
78
+ const result = runCli(['ci', tmpDir, '--format', 'sarif'])
79
+ const sarif = expectValidSarifFrom(result)
80
+ expect(sarif.runs[0]?.results?.some((entry) => entry.ruleId === 'debug-leftover')).toBe(true)
81
+ })
82
+
83
+ it('serializes trust --format sarif output as SARIF JSON without requiring git base', () => {
84
+ tmpDir = mkdtempSync(join(tmpdir(), 'drift-cli-sarif-trust-'))
85
+ writeFileSync(join(tmpDir, 'sample.ts'), 'console.log("debug")\n')
86
+
87
+ const result = runCli(['trust', tmpDir, '--format', 'sarif'])
88
+ const sarif = expectValidSarifFrom(result)
89
+ expect(sarif.runs[0]?.results?.some((entry) => entry.ruleId === 'debug-leftover')).toBe(true)
90
+ })
91
+
92
+ })
@@ -0,0 +1,115 @@
1
+ import { afterEach, describe, expect, it } from 'vitest'
2
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
3
+ import { tmpdir } from 'node:os'
4
+ import { dirname, join } from 'node:path'
5
+ import { spawnSync } from 'node:child_process'
6
+
7
+ const repoRoot = process.cwd()
8
+ const checkScriptPath = join(repoRoot, 'scripts/check-docs-drift.mjs')
9
+ const tempDirs: string[] = []
10
+
11
+ function runCheck(cwd: string) {
12
+ return spawnSync(process.execPath, [checkScriptPath], {
13
+ cwd,
14
+ encoding: 'utf8',
15
+ })
16
+ }
17
+
18
+ function writeFixtureFile(root: string, relativePath: string, content: string) {
19
+ const filePath = join(root, relativePath)
20
+ const directory = dirname(filePath)
21
+ mkdirSync(directory, { recursive: true })
22
+ writeFileSync(filePath, content, 'utf8')
23
+ }
24
+
25
+ function createFixture(options?: { agentsVersion?: string; catalogMissingRule?: boolean; readmeCount?: number }) {
26
+ const fixtureRoot = mkdtempSync(join(tmpdir(), 'drift-docs-check-'))
27
+ tempDirs.push(fixtureRoot)
28
+
29
+ const agentsVersion = options?.agentsVersion ?? '1.4.0'
30
+ const catalogMissingRule = options?.catalogMissingRule ?? false
31
+ const readmeCount = options?.readmeCount ?? 2
32
+
33
+ writeFixtureFile(fixtureRoot, 'package.json', JSON.stringify({ version: '1.4.0' }, null, 2))
34
+ writeFixtureFile(
35
+ fixtureRoot,
36
+ 'src/analyzer.ts',
37
+ `export const RULE_WEIGHTS = {\n 'large-file': { severity: 'error', weight: 20 },\n 'dead-code': { severity: 'warning', weight: 8 },\n}\n`,
38
+ )
39
+
40
+ const catalogTableRows = catalogMissingRule
41
+ ? "| `large-file` | error | 20 | phase0-basic | sample |"
42
+ : [
43
+ '| `large-file` | error | 20 | phase0-basic | sample |',
44
+ '| `dead-code` | warning | 8 | phase0-basic | sample |',
45
+ ].join('\n')
46
+
47
+ const catalogTotal = catalogMissingRule ? 1 : 2
48
+ writeFixtureFile(
49
+ fixtureRoot,
50
+ 'docs/rules-catalog.md',
51
+ [
52
+ '# drift rules catalog (current)',
53
+ '',
54
+ 'Source of truth: `RULE_WEIGHTS` in `src/analyzer.ts`.',
55
+ '',
56
+ '| id | severity | weight | phase/origin | note |',
57
+ '|---|---|---:|---|---|',
58
+ catalogTableRows,
59
+ '',
60
+ `- Total rule IDs currently defined: **${catalogTotal}**.`,
61
+ '',
62
+ ].join('\n'),
63
+ )
64
+
65
+ writeFixtureFile(fixtureRoot, 'README.md', `drift currently defines **${readmeCount} rule IDs** in RULE_WEIGHTS.`)
66
+ writeFixtureFile(
67
+ fixtureRoot,
68
+ 'AGENTS.md',
69
+ [
70
+ '# AGENTS.md — drift',
71
+ `- Versión del paquete: \`${agentsVersion}\` (\`package.json\`)`,
72
+ '- Estado actual: **2 rule IDs** (sample).',
73
+ '',
74
+ ].join('\n'),
75
+ )
76
+
77
+ return fixtureRoot
78
+ }
79
+
80
+ afterEach(() => {
81
+ while (tempDirs.length > 0) {
82
+ const dir = tempDirs.pop()
83
+ if (!dir) continue
84
+ rmSync(dir, { recursive: true, force: true })
85
+ }
86
+ })
87
+
88
+ describe('docs drift checker', () => {
89
+ it('passes against the real repository contract', () => {
90
+ const result = runCheck(repoRoot)
91
+ expect(result.status).toBe(0)
92
+ expect(result.stdout).toContain('Docs drift check passed')
93
+ expect(result.stderr).toBe('')
94
+ })
95
+
96
+ it('fails when AGENTS package version diverges from package.json', () => {
97
+ const fixtureRoot = createFixture({ agentsVersion: '9.9.9' })
98
+
99
+ const result = runCheck(fixtureRoot)
100
+
101
+ expect(result.status).toBe(1)
102
+ expect(result.stderr).toContain('Docs drift check failed:')
103
+ expect(result.stderr).toContain('AGENTS.md package version (9.9.9) does not match package.json (1.4.0)')
104
+ })
105
+
106
+ it('fails when catalog rule IDs diverge from RULE_WEIGHTS', () => {
107
+ const fixtureRoot = createFixture({ catalogMissingRule: true, readmeCount: 1 })
108
+
109
+ const result = runCheck(fixtureRoot)
110
+
111
+ expect(result.status).toBe(1)
112
+ expect(result.stderr).toContain('rules missing in docs/rules-catalog.md: dead-code')
113
+ expect(result.stderr).toContain('README.md rule ID count (1) does not match RULE_WEIGHTS (2)')
114
+ })
115
+ })
@@ -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', () => {
@@ -110,7 +118,7 @@ describe('new feature MVP', () => {
110
118
  expect(rules).toContain('controller-no-db')
111
119
  expect(rules).toContain('service-no-http')
112
120
  expect(rules).toContain('max-function-lines')
113
- })
121
+ }, 15000)
114
122
 
115
123
  it('generates architecture SVG map', () => {
116
124
  tmpDir = mkdtempSync(join(tmpdir(), 'drift-map-'))
@@ -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
+ }, 30000)
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,146 @@
1
+ import { afterEach, describe, expect, it } from 'vitest'
2
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
3
+ import { tmpdir } from 'node:os'
4
+ import { dirname, join } from 'node:path'
5
+ import { spawnSync } from 'node:child_process'
6
+
7
+ const repoRoot = process.cwd()
8
+ const checkScriptPath = join(repoRoot, 'scripts/check-performance-budget.mjs')
9
+ const tempDirs: string[] = []
10
+
11
+ function runCheck(cwd: string, args: string[]) {
12
+ return spawnSync(process.execPath, [checkScriptPath, ...args], {
13
+ cwd,
14
+ encoding: 'utf8',
15
+ })
16
+ }
17
+
18
+ function writeFixtureFile(root: string, relativePath: string, content: string) {
19
+ const filePath = join(root, relativePath)
20
+ mkdirSync(dirname(filePath), { recursive: true })
21
+ writeFileSync(filePath, content, 'utf8')
22
+ }
23
+
24
+ function createFixture(options?: {
25
+ schemaVersion?: string
26
+ scanMedianMs?: number
27
+ reviewMedianMs?: number
28
+ trustMedianMs?: number
29
+ measuredRuns?: number
30
+ includeTrustResult?: boolean
31
+ }) {
32
+ const fixtureRoot = mkdtempSync(join(tmpdir(), 'drift-perf-check-'))
33
+ tempDirs.push(fixtureRoot)
34
+
35
+ const budget = {
36
+ schemaVersion: options?.schemaVersion ?? 'drift-perf-budget/v1',
37
+ budgetVersion: 'test-budget',
38
+ benchmark: {
39
+ scanPath: 'benchmarks/fixtures/critical',
40
+ reviewPath: 'benchmarks/fixtures/critical',
41
+ trustPath: 'benchmarks/fixtures/critical',
42
+ baseRef: 'HEAD',
43
+ warmupRuns: 1,
44
+ measuredRuns: options?.measuredRuns ?? 5,
45
+ },
46
+ tolerance: {
47
+ runtimePct: 10,
48
+ memoryPct: 10,
49
+ },
50
+ tasks: {
51
+ scan: { maxMedianMs: 1000, maxRssMb: 400 },
52
+ review: { maxMedianMs: 1200, maxRssMb: 450 },
53
+ trust: { maxMedianMs: 1400, maxRssMb: 500 },
54
+ },
55
+ }
56
+
57
+ const benchmarkResult = {
58
+ meta: {
59
+ scannedAt: new Date().toISOString(),
60
+ node: process.version,
61
+ platform: process.platform,
62
+ cwd: fixtureRoot,
63
+ },
64
+ options: budget.benchmark,
65
+ results: [
66
+ {
67
+ name: 'scan',
68
+ medianMs: options?.scanMedianMs ?? 800,
69
+ maxRssMb: 320,
70
+ },
71
+ {
72
+ name: 'review',
73
+ medianMs: options?.reviewMedianMs ?? 900,
74
+ maxRssMb: 360,
75
+ },
76
+ ...(options?.includeTrustResult === false
77
+ ? []
78
+ : [{
79
+ name: 'trust',
80
+ medianMs: options?.trustMedianMs ?? 1100,
81
+ maxRssMb: 390,
82
+ }]),
83
+ ],
84
+ }
85
+
86
+ writeFixtureFile(fixtureRoot, 'benchmarks/perf-budget.v1.json', `${JSON.stringify(budget, null, 2)}\n`)
87
+ writeFixtureFile(fixtureRoot, '.drift-perf/benchmark-sample.json', `${JSON.stringify(benchmarkResult, null, 2)}\n`)
88
+
89
+ return fixtureRoot
90
+ }
91
+
92
+ afterEach(() => {
93
+ while (tempDirs.length > 0) {
94
+ const dir = tempDirs.pop()
95
+ if (!dir) continue
96
+ rmSync(dir, { recursive: true, force: true })
97
+ }
98
+ })
99
+
100
+ describe('performance budget checker', () => {
101
+ it('passes when benchmark result stays within budget and tolerance', () => {
102
+ const fixtureRoot = createFixture()
103
+
104
+ const result = runCheck(fixtureRoot, ['--result', '.drift-perf/benchmark-sample.json'])
105
+
106
+ expect(result.status).toBe(0)
107
+ expect(result.stdout).toContain('Performance budget check passed.')
108
+ })
109
+
110
+ it('fails when runtime budget is exceeded', () => {
111
+ const fixtureRoot = createFixture({ scanMedianMs: 1300 })
112
+
113
+ const result = runCheck(fixtureRoot, ['--result', '.drift-perf/benchmark-sample.json'])
114
+
115
+ expect(result.status).toBe(1)
116
+ expect(result.stderr).toContain('Performance budget check failed:')
117
+ expect(result.stderr).toContain('scan: median runtime')
118
+ })
119
+
120
+ it('fails when budget schema version is invalid', () => {
121
+ const fixtureRoot = createFixture({ schemaVersion: 'drift-perf-budget/v0' })
122
+
123
+ const result = runCheck(fixtureRoot, ['--result', '.drift-perf/benchmark-sample.json'])
124
+
125
+ expect(result.status).toBe(1)
126
+ expect(result.stderr).toContain('Unsupported budget schemaVersion')
127
+ })
128
+
129
+ it('fails when benchmark output does not include all required tasks', () => {
130
+ const fixtureRoot = createFixture({ includeTrustResult: false })
131
+
132
+ const result = runCheck(fixtureRoot, ['--result', '.drift-perf/benchmark-sample.json'])
133
+
134
+ expect(result.status).toBe(1)
135
+ expect(result.stderr).toContain("Benchmark output is missing task 'trust'")
136
+ })
137
+
138
+ it('fails when benchmark measuredRuns is invalid', () => {
139
+ const fixtureRoot = createFixture({ measuredRuns: 0 })
140
+
141
+ const result = runCheck(fixtureRoot, ['--result', '.drift-perf/benchmark-sample.json'])
142
+
143
+ expect(result.status).toBe(1)
144
+ expect(result.stderr).toContain('benchmark.measuredRuns must be at least 1')
145
+ })
146
+ })