@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.
- package/.gga +50 -0
- package/.github/actions/drift-review/README.md +62 -0
- package/.github/actions/drift-review/action.yml +148 -0
- package/.github/actions/drift-scan/README.md +28 -32
- package/.github/actions/drift-scan/action.yml +78 -14
- 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 +33 -41
- package/AGENTS.md +75 -251
- package/CHANGELOG.md +41 -0
- package/README.md +177 -43
- 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.d.ts +1 -1
- package/dist/benchmark.js +83 -52
- package/dist/cli.js +243 -8
- package/dist/config.js +16 -2
- package/dist/diff.js +42 -50
- package/dist/doctor.d.ts +26 -0
- package/dist/doctor.js +140 -0
- package/dist/format.d.ts +17 -0
- package/dist/format.js +45 -0
- 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 +58 -0
- package/dist/guard-types.js +2 -0
- package/dist/guard.d.ts +16 -0
- package/dist/guard.js +178 -0
- package/dist/index.d.ts +10 -3
- package/dist/index.js +4 -1
- package/dist/init.d.ts +15 -0
- package/dist/init.js +273 -0
- package/dist/map-cycles.d.ts +2 -0
- package/dist/map-cycles.js +34 -0
- package/dist/map-svg.d.ts +19 -0
- package/dist/map-svg.js +97 -0
- package/dist/map.js +78 -138
- package/dist/metrics.js +70 -55
- package/dist/output-metadata.d.ts +15 -0
- package/dist/output-metadata.js +19 -0
- package/dist/plugins-capabilities.d.ts +4 -0
- package/dist/plugins-capabilities.js +21 -0
- package/dist/plugins-messages.d.ts +10 -0
- package/dist/plugins-messages.js +16 -0
- package/dist/plugins-rules.d.ts +9 -0
- package/dist/plugins-rules.js +137 -0
- package/dist/plugins.d.ts +1 -1
- package/dist/plugins.js +45 -142
- package/dist/reporter-constants.d.ts +16 -0
- package/dist/reporter-constants.js +39 -0
- package/dist/reporter.d.ts +3 -3
- package/dist/reporter.js +35 -55
- package/dist/review.d.ts +2 -1
- package/dist/review.js +2 -1
- package/dist/rules/phase3-configurable.js +23 -15
- package/dist/saas/constants.d.ts +15 -0
- package/dist/saas/constants.js +48 -0
- package/dist/saas/dashboard.d.ts +8 -0
- package/dist/saas/dashboard.js +132 -0
- package/dist/saas/errors.d.ts +19 -0
- package/dist/saas/errors.js +37 -0
- package/dist/saas/helpers.d.ts +21 -0
- package/dist/saas/helpers.js +110 -0
- package/dist/saas/ingest.d.ts +3 -0
- package/dist/saas/ingest.js +249 -0
- package/dist/saas/organization.d.ts +5 -0
- package/dist/saas/organization.js +82 -0
- package/dist/saas/plan-change.d.ts +10 -0
- package/dist/saas/plan-change.js +15 -0
- package/dist/saas/store.d.ts +21 -0
- package/dist/saas/store.js +159 -0
- package/dist/saas/types.d.ts +191 -0
- package/dist/saas/types.js +2 -0
- package/dist/saas.d.ts +8 -218
- package/dist/saas.js +7 -761
- package/dist/sarif.d.ts +74 -0
- package/dist/sarif.js +122 -0
- package/dist/trust-advanced.d.ts +14 -0
- package/dist/trust-advanced.js +65 -0
- package/dist/trust-kpi-fs.d.ts +3 -0
- package/dist/trust-kpi-fs.js +141 -0
- package/dist/trust-kpi-parse.d.ts +7 -0
- package/dist/trust-kpi-parse.js +186 -0
- package/dist/trust-kpi-types.d.ts +16 -0
- package/dist/trust-kpi-types.js +2 -0
- package/dist/trust-kpi.d.ts +1 -3
- package/dist/trust-kpi.js +6 -266
- package/dist/trust-policy.d.ts +32 -0
- package/dist/trust-policy.js +160 -0
- package/dist/trust-render.d.ts +9 -0
- package/dist/trust-render.js +54 -0
- package/dist/trust-scoring.d.ts +9 -0
- package/dist/trust-scoring.js +208 -0
- package/dist/trust.d.ts +5 -32
- package/dist/trust.js +29 -432
- package/dist/types/app.d.ts +30 -0
- package/dist/types/app.js +2 -0
- package/dist/types/config.d.ts +25 -0
- package/dist/types/config.js +2 -0
- package/dist/types/core.d.ts +100 -0
- package/dist/types/core.js +2 -0
- package/dist/types/diff.d.ts +55 -0
- package/dist/types/diff.js +2 -0
- package/dist/types/plugin.d.ts +41 -0
- package/dist/types/plugin.js +2 -0
- package/dist/types/trust.d.ts +120 -0
- package/dist/types/trust.js +2 -0
- package/dist/types.d.ts +8 -365
- package/docs/AGENTS.md +1 -1
- package/docs/release-notes-draft.md +40 -0
- package/docs/rules-catalog.md +49 -0
- package/docs/trust-core-release-checklist.md +37 -5
- package/package.json +11 -4
- package/packages/vscode-drift/src/code-actions.ts +1 -1
- package/schemas/drift-ai-output.v1.json +162 -0
- package/schemas/drift-doctor.v1.json +57 -0
- package/schemas/drift-guard.v1.json +298 -0
- package/schemas/drift-report.v1.json +151 -0
- package/schemas/drift-trust.v1.json +131 -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/scripts/smoke-repo.mjs +394 -0
- package/src/benchmark.ts +92 -53
- package/src/cli.ts +285 -13
- package/src/config.ts +19 -2
- package/src/diff.ts +57 -48
- package/src/doctor.ts +185 -0
- package/src/format.ts +81 -0
- package/src/guard-baseline.ts +74 -0
- package/src/guard-metrics.ts +52 -0
- package/src/guard-types.ts +66 -0
- package/src/guard.ts +248 -0
- package/src/index.ts +36 -0
- package/src/init.ts +298 -0
- package/src/map-cycles.ts +38 -0
- package/src/map-svg.ts +124 -0
- package/src/map.ts +111 -142
- package/src/metrics.ts +78 -59
- package/src/output-metadata.ts +32 -0
- package/src/plugins-capabilities.ts +36 -0
- package/src/plugins-messages.ts +35 -0
- package/src/plugins-rules.ts +296 -0
- package/src/plugins.ts +76 -283
- package/src/reporter-constants.ts +46 -0
- package/src/reporter.ts +64 -65
- package/src/review.ts +4 -2
- package/src/rules/phase3-configurable.ts +39 -26
- package/src/saas/constants.ts +56 -0
- package/src/saas/dashboard.ts +172 -0
- package/src/saas/errors.ts +45 -0
- package/src/saas/helpers.ts +140 -0
- package/src/saas/ingest.ts +278 -0
- package/src/saas/organization.ts +99 -0
- package/src/saas/plan-change.ts +19 -0
- package/src/saas/store.ts +172 -0
- package/src/saas/types.ts +216 -0
- package/src/saas.ts +49 -1031
- package/src/sarif.ts +232 -0
- package/src/trust-advanced.ts +99 -0
- package/src/trust-kpi-fs.ts +169 -0
- package/src/trust-kpi-parse.ts +219 -0
- package/src/trust-kpi-types.ts +19 -0
- package/src/trust-kpi.ts +8 -316
- package/src/trust-policy.ts +246 -0
- package/src/trust-render.ts +61 -0
- package/src/trust-scoring.ts +231 -0
- package/src/trust.ts +62 -576
- package/src/types/app.ts +30 -0
- package/src/types/config.ts +27 -0
- package/src/types/core.ts +105 -0
- package/src/types/diff.ts +61 -0
- package/src/types/plugin.ts +46 -0
- package/src/types/trust.ts +134 -0
- package/src/types.ts +79 -409
- 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/cli-sarif.test.ts +92 -0
- package/tests/docs-drift-check.test.ts +115 -0
- package/tests/format.test.ts +157 -0
- package/tests/new-features.test.ts +11 -3
- package/tests/perf-budget-check.test.ts +146 -0
- package/tests/phase1-init-doctor-guard.test.ts +301 -0
- package/tests/runtime-policy-alignment.test.ts +46 -0
- package/tests/sarif.test.ts +160 -0
- package/tests/trust-kpi.test.ts +31 -4
- package/tests/trust.test.ts +18 -0
- 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
|
|
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
|
+
})
|