@eduardbar/drift 1.3.0 → 1.4.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 +60 -0
- package/.github/actions/drift-review/action.yml +131 -0
- package/.github/actions/drift-scan/README.md +28 -32
- package/.github/actions/drift-scan/action.yml +78 -14
- package/.github/workflows/review-pr.yml +34 -41
- package/AGENTS.md +75 -251
- package/CHANGELOG.md +28 -0
- package/README.md +148 -41
- package/dist/benchmark.d.ts +1 -1
- package/dist/benchmark.js +71 -52
- package/dist/cli.js +243 -8
- package/dist/config.js +16 -2
- package/dist/diff.js +42 -50
- package/dist/doctor.d.ts +5 -0
- package/dist/doctor.js +133 -0
- package/dist/format.d.ts +17 -0
- package/dist/format.js +45 -0
- package/dist/guard-types.d.ts +57 -0
- package/dist/guard-types.js +2 -0
- package/dist/guard.d.ts +14 -0
- package/dist/guard.js +239 -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 +13 -0
- package/dist/output-metadata.js +17 -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 +4 -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/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 +3 -2
- package/packages/vscode-drift/src/code-actions.ts +1 -1
- package/schemas/drift-ai-output.v1.json +162 -0
- package/schemas/drift-report.v1.json +151 -0
- package/schemas/drift-trust.v1.json +131 -0
- package/scripts/smoke-repo.mjs +394 -0
- package/src/benchmark.ts +75 -53
- package/src/cli.ts +285 -13
- package/src/config.ts +19 -2
- package/src/diff.ts +57 -48
- package/src/doctor.ts +173 -0
- package/src/format.ts +81 -0
- package/src/guard-types.ts +64 -0
- package/src/guard.ts +324 -0
- package/src/index.ts +35 -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 +30 -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 +78 -409
- package/tests/cli-sarif.test.ts +92 -0
- package/tests/format.test.ts +157 -0
- package/tests/new-features.test.ts +10 -2
- package/tests/phase1-init-doctor-guard.test.ts +199 -0
- package/tests/sarif.test.ts +160 -0
- package/tests/trust-kpi.test.ts +31 -4
- package/tests/trust.test.ts +18 -0
package/src/doctor.ts
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import kleur from 'kleur'
|
|
4
|
+
|
|
5
|
+
export interface DoctorOptions {
|
|
6
|
+
json?: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface DoctorReport {
|
|
10
|
+
targetPath: string
|
|
11
|
+
node: {
|
|
12
|
+
version: string
|
|
13
|
+
major: number
|
|
14
|
+
supported: boolean
|
|
15
|
+
}
|
|
16
|
+
project: {
|
|
17
|
+
packageJsonFound: boolean
|
|
18
|
+
esm: boolean
|
|
19
|
+
tsconfigFound: boolean
|
|
20
|
+
sourceFilesCount: number
|
|
21
|
+
lowMemorySuggested: boolean
|
|
22
|
+
driftConfigFile: string | null
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx'])
|
|
27
|
+
const IGNORED_DIRECTORIES = new Set(['node_modules', '.git', 'dist', '.next', 'coverage'])
|
|
28
|
+
const DECIMAL_RADIX = 10
|
|
29
|
+
const MIN_SUPPORTED_NODE_MAJOR = 18
|
|
30
|
+
const LOW_MEMORY_SOURCE_FILE_THRESHOLD = 500
|
|
31
|
+
const DRIFT_CONFIG_CANDIDATES = [
|
|
32
|
+
'drift.config.ts',
|
|
33
|
+
'drift.config.js',
|
|
34
|
+
'drift.config.mjs',
|
|
35
|
+
'drift.config.cjs',
|
|
36
|
+
'drift.config.json',
|
|
37
|
+
] as const
|
|
38
|
+
|
|
39
|
+
function parseNodeMajor(version: string): number {
|
|
40
|
+
const parsed = Number.parseInt(version.replace(/^v/, '').split('.')[0] ?? '0', DECIMAL_RADIX)
|
|
41
|
+
return Number.isFinite(parsed) ? parsed : 0
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function detectDriftConfig(projectPath: string): string | null {
|
|
45
|
+
for (const candidate of DRIFT_CONFIG_CANDIDATES) {
|
|
46
|
+
if (existsSync(join(projectPath, candidate))) {
|
|
47
|
+
return candidate
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function countSourceFiles(projectPath: string): number {
|
|
54
|
+
let total = 0
|
|
55
|
+
const stack = [projectPath]
|
|
56
|
+
|
|
57
|
+
while (stack.length > 0) {
|
|
58
|
+
const currentDir = stack.pop()
|
|
59
|
+
if (!currentDir) continue
|
|
60
|
+
|
|
61
|
+
const entries = readdirSync(currentDir, { withFileTypes: true })
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
if (entry.isDirectory() && !IGNORED_DIRECTORIES.has(entry.name)) {
|
|
64
|
+
stack.push(join(currentDir, entry.name))
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (entry.isDirectory()) {
|
|
69
|
+
continue
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!entry.isFile()) continue
|
|
73
|
+
|
|
74
|
+
const lastDot = entry.name.lastIndexOf('.')
|
|
75
|
+
if (lastDot === -1) continue
|
|
76
|
+
|
|
77
|
+
const extension = entry.name.slice(lastDot)
|
|
78
|
+
if (SOURCE_EXTENSIONS.has(extension)) {
|
|
79
|
+
total += 1
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return total
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function buildDoctorReport(projectPath: string): DoctorReport {
|
|
88
|
+
const nodeMajor = parseNodeMajor(process.version)
|
|
89
|
+
const packageJsonPath = join(projectPath, 'package.json')
|
|
90
|
+
const packageJsonFound = existsSync(packageJsonPath)
|
|
91
|
+
|
|
92
|
+
let esm = false
|
|
93
|
+
if (packageJsonFound) {
|
|
94
|
+
const parsed = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as { type?: string }
|
|
95
|
+
esm = parsed.type === 'module'
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const sourceFilesCount = countSourceFiles(projectPath)
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
targetPath: projectPath,
|
|
102
|
+
node: {
|
|
103
|
+
version: process.version,
|
|
104
|
+
major: nodeMajor,
|
|
105
|
+
supported: nodeMajor >= MIN_SUPPORTED_NODE_MAJOR,
|
|
106
|
+
},
|
|
107
|
+
project: {
|
|
108
|
+
packageJsonFound,
|
|
109
|
+
esm,
|
|
110
|
+
tsconfigFound: existsSync(join(projectPath, 'tsconfig.json')),
|
|
111
|
+
sourceFilesCount,
|
|
112
|
+
lowMemorySuggested: sourceFilesCount > LOW_MEMORY_SOURCE_FILE_THRESHOLD,
|
|
113
|
+
driftConfigFile: detectDriftConfig(projectPath),
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function printConsoleReport(report: DoctorReport): void {
|
|
119
|
+
const icons = {
|
|
120
|
+
check: kleur.green('✓'),
|
|
121
|
+
warn: kleur.yellow('⚠'),
|
|
122
|
+
error: kleur.red('✗'),
|
|
123
|
+
info: kleur.cyan('ℹ'),
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
process.stdout.write('\n')
|
|
127
|
+
process.stdout.write(`${kleur.bold().white('drift doctor')} ${kleur.gray('- environment diagnostics')}\n\n`)
|
|
128
|
+
|
|
129
|
+
const nodeStatus = report.node.supported
|
|
130
|
+
? `${icons.check} ${kleur.green('Node runtime supported')}`
|
|
131
|
+
: `${icons.warn} ${kleur.yellow('Node runtime below recommended minimum (>=18)')}`
|
|
132
|
+
process.stdout.write(`${nodeStatus} ${kleur.gray(`(${report.node.version})`)}\n`)
|
|
133
|
+
|
|
134
|
+
if (report.project.packageJsonFound) {
|
|
135
|
+
process.stdout.write(`${icons.check} package.json found\n`)
|
|
136
|
+
process.stdout.write(`${icons.info} ESM mode: ${report.project.esm ? kleur.green('yes') : kleur.yellow('no')}\n`)
|
|
137
|
+
} else {
|
|
138
|
+
process.stdout.write(`${icons.warn} package.json not found\n`)
|
|
139
|
+
process.stdout.write(`${icons.info} ESM mode: ${kleur.gray('unknown')}\n`)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (report.project.tsconfigFound) {
|
|
143
|
+
process.stdout.write(`${icons.check} tsconfig.json found\n`)
|
|
144
|
+
} else {
|
|
145
|
+
process.stdout.write(`${icons.warn} tsconfig.json not found\n`)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
process.stdout.write(`${icons.info} Source files (.ts/.tsx/.js/.jsx): ${report.project.sourceFilesCount}\n`)
|
|
149
|
+
|
|
150
|
+
if (report.project.lowMemorySuggested) {
|
|
151
|
+
process.stdout.write(`${icons.warn} Large codebase detected, consider ${kleur.bold('--low-memory')}\n`)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (report.project.driftConfigFile) {
|
|
155
|
+
process.stdout.write(`${icons.check} Drift config: ${report.project.driftConfigFile}\n`)
|
|
156
|
+
} else {
|
|
157
|
+
process.stdout.write(`${icons.warn} Drift config not found (drift.config.*)\n`)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
process.stdout.write('\n')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export async function runDoctor(projectPath: string, options?: DoctorOptions): Promise<number> {
|
|
164
|
+
const report = buildDoctorReport(projectPath)
|
|
165
|
+
|
|
166
|
+
if (options?.json) {
|
|
167
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`)
|
|
168
|
+
} else {
|
|
169
|
+
printConsoleReport(report)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return 0
|
|
173
|
+
}
|
package/src/format.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const UNIFIED_FORMAT_VALUES = ['console', 'json', 'markdown', 'ai', 'sarif'] as const
|
|
2
|
+
|
|
3
|
+
type UnifiedOutputFormat = (typeof UNIFIED_FORMAT_VALUES)[number]
|
|
4
|
+
|
|
5
|
+
type LegacyAlias = {
|
|
6
|
+
flag: string
|
|
7
|
+
used?: boolean
|
|
8
|
+
mapsTo: UnifiedOutputFormat
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface ResolveOutputFormatOptions {
|
|
12
|
+
command: string
|
|
13
|
+
format?: string
|
|
14
|
+
supported: readonly UnifiedOutputFormat[]
|
|
15
|
+
legacyAliases?: LegacyAlias[]
|
|
16
|
+
onWarning?: (message: string) => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function assertSupportedFormatValue(command: string, format: string): asserts format is UnifiedOutputFormat {
|
|
20
|
+
if ((UNIFIED_FORMAT_VALUES as readonly string[]).includes(format)) return
|
|
21
|
+
throw new Error(
|
|
22
|
+
`Invalid --format '${format}' for '${command}'. Allowed values: ${UNIFIED_FORMAT_VALUES.join(', ')}.`,
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function throwUnsupportedFormat(command: string, selected: UnifiedOutputFormat, supported: readonly UnifiedOutputFormat[]): never {
|
|
27
|
+
throw new Error(
|
|
28
|
+
`Format '${selected}' is not supported for '${command}'. Supported formats: ${supported.join(', ')}.`,
|
|
29
|
+
)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeLegacyFormatSelection(command: string, selectedLegacyFormats: UnifiedOutputFormat[]): UnifiedOutputFormat | undefined {
|
|
33
|
+
if (selectedLegacyFormats.length === 0) return undefined
|
|
34
|
+
|
|
35
|
+
const uniqueFormats = [...new Set(selectedLegacyFormats)]
|
|
36
|
+
if (uniqueFormats.length > 1) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Conflicting legacy format flags for '${command}': ${uniqueFormats.join(' vs ')}. Use a single format option.`,
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return uniqueFormats[0]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function resolveOutputFormat(options: ResolveOutputFormatOptions): UnifiedOutputFormat {
|
|
46
|
+
const { command, format, supported, onWarning } = options
|
|
47
|
+
const legacyAliases = options.legacyAliases ?? []
|
|
48
|
+
|
|
49
|
+
for (const alias of legacyAliases) {
|
|
50
|
+
if (!alias.used) continue
|
|
51
|
+
onWarning?.(`Warning: --${alias.flag} is deprecated for '${command}'. Use --format ${alias.mapsTo} instead.`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const selectedLegacyFormat = normalizeLegacyFormatSelection(
|
|
55
|
+
command,
|
|
56
|
+
legacyAliases.filter((alias) => alias.used).map((alias) => alias.mapsTo),
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
const selectedFormat = format?.trim()
|
|
60
|
+
if (selectedFormat) {
|
|
61
|
+
assertSupportedFormatValue(command, selectedFormat)
|
|
62
|
+
if (selectedLegacyFormat && selectedLegacyFormat !== selectedFormat) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Conflicting format flags for '${command}': --format ${selectedFormat} and legacy alias for ${selectedLegacyFormat}.`,
|
|
65
|
+
)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!supported.includes(selectedFormat)) {
|
|
69
|
+
throwUnsupportedFormat(command, selectedFormat, supported)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return selectedFormat
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const resolvedFromLegacy = selectedLegacyFormat ?? 'console'
|
|
76
|
+
if (!supported.includes(resolvedFromLegacy)) {
|
|
77
|
+
throwUnsupportedFormat(command, resolvedFromLegacy, supported)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return resolvedFromLegacy
|
|
81
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { DriftAnalysisOptions, DriftDiff, DriftIssue, DriftReport } from './types.js'
|
|
2
|
+
|
|
3
|
+
export type IssueSeverity = DriftIssue['severity']
|
|
4
|
+
|
|
5
|
+
export interface GuardBaseline {
|
|
6
|
+
score?: number
|
|
7
|
+
totalIssues?: number
|
|
8
|
+
errors?: number
|
|
9
|
+
warnings?: number
|
|
10
|
+
infos?: number
|
|
11
|
+
bySeverity?: Partial<Record<IssueSeverity, number>>
|
|
12
|
+
summary?: {
|
|
13
|
+
errors?: number
|
|
14
|
+
warnings?: number
|
|
15
|
+
infos?: number
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface GuardThresholds {
|
|
20
|
+
error?: number
|
|
21
|
+
warning?: number
|
|
22
|
+
info?: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface GuardOptions {
|
|
26
|
+
baseRef?: string
|
|
27
|
+
baselinePath?: string
|
|
28
|
+
baseline?: GuardBaseline
|
|
29
|
+
budget?: number
|
|
30
|
+
bySeverity?: GuardThresholds
|
|
31
|
+
analysis?: DriftAnalysisOptions
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface GuardMetrics {
|
|
35
|
+
scoreDelta: number
|
|
36
|
+
totalIssuesDelta: number
|
|
37
|
+
severityDelta: Record<IssueSeverity, number>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface GuardCheck {
|
|
41
|
+
id: string
|
|
42
|
+
passed: boolean
|
|
43
|
+
actual: number
|
|
44
|
+
limit: number
|
|
45
|
+
message: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface GuardEvaluation {
|
|
49
|
+
passed: boolean
|
|
50
|
+
checks: GuardCheck[]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface GuardResult {
|
|
54
|
+
scannedAt: string
|
|
55
|
+
projectPath: string
|
|
56
|
+
mode: 'diff' | 'baseline'
|
|
57
|
+
passed: boolean
|
|
58
|
+
baseRef?: string
|
|
59
|
+
baselinePath?: string
|
|
60
|
+
metrics: GuardMetrics
|
|
61
|
+
checks: GuardCheck[]
|
|
62
|
+
current: DriftReport
|
|
63
|
+
diff?: DriftDiff
|
|
64
|
+
}
|
package/src/guard.ts
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { relative, resolve } from 'node:path'
|
|
3
|
+
import { analyzeProject } from './analyzer.js'
|
|
4
|
+
import { loadConfig } from './config.js'
|
|
5
|
+
import { computeDiff } from './diff.js'
|
|
6
|
+
import { cleanupTempDir, extractFilesAtRef } from './git.js'
|
|
7
|
+
import { buildReport } from './reporter.js'
|
|
8
|
+
import type { DriftDiff, DriftReport } from './types.js'
|
|
9
|
+
import type {
|
|
10
|
+
GuardBaseline,
|
|
11
|
+
GuardCheck,
|
|
12
|
+
GuardEvaluation,
|
|
13
|
+
GuardMetrics,
|
|
14
|
+
GuardOptions,
|
|
15
|
+
GuardResult,
|
|
16
|
+
GuardThresholds,
|
|
17
|
+
IssueSeverity,
|
|
18
|
+
} from './guard-types.js'
|
|
19
|
+
|
|
20
|
+
interface NormalizedBaseline {
|
|
21
|
+
score?: number
|
|
22
|
+
totalIssues?: number
|
|
23
|
+
bySeverity: Partial<Record<IssueSeverity, number>>
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface GuardEvalInput {
|
|
27
|
+
metrics: GuardMetrics
|
|
28
|
+
budget?: number
|
|
29
|
+
bySeverity?: GuardThresholds
|
|
30
|
+
enforceNoRegression: {
|
|
31
|
+
score: boolean
|
|
32
|
+
totalIssues: boolean
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface GuardRuntimeState {
|
|
37
|
+
currentReport: DriftReport
|
|
38
|
+
config: Awaited<ReturnType<typeof loadConfig>>
|
|
39
|
+
projectPath: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface DiffGuardResultInput {
|
|
43
|
+
projectPath: string
|
|
44
|
+
currentReport: DriftReport
|
|
45
|
+
options: GuardOptions
|
|
46
|
+
tempDir: string
|
|
47
|
+
config: Awaited<ReturnType<typeof loadConfig>>
|
|
48
|
+
baseRef: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface BaselineGuardResultInput {
|
|
52
|
+
projectPath: string
|
|
53
|
+
currentReport: DriftReport
|
|
54
|
+
options: GuardOptions
|
|
55
|
+
baseline: NormalizedBaseline
|
|
56
|
+
baselinePath?: string
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parseNumber(value: unknown): number | undefined {
|
|
60
|
+
return typeof value === 'number' && !Number.isNaN(value) ? value : undefined
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalizeBaseline(baseline: GuardBaseline): NormalizedBaseline {
|
|
64
|
+
const bySeverityFromRoot = baseline.bySeverity
|
|
65
|
+
const bySeverity = {
|
|
66
|
+
error: parseNumber(bySeverityFromRoot?.error) ?? parseNumber(baseline.errors) ?? parseNumber(baseline.summary?.errors),
|
|
67
|
+
warning: parseNumber(bySeverityFromRoot?.warning) ?? parseNumber(baseline.warnings) ?? parseNumber(baseline.summary?.warnings),
|
|
68
|
+
info: parseNumber(bySeverityFromRoot?.info) ?? parseNumber(baseline.infos) ?? parseNumber(baseline.summary?.infos),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const normalized: NormalizedBaseline = {
|
|
72
|
+
score: parseNumber(baseline.score),
|
|
73
|
+
totalIssues: parseNumber(baseline.totalIssues),
|
|
74
|
+
bySeverity,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const hasAnyAnchor =
|
|
78
|
+
normalized.score !== undefined ||
|
|
79
|
+
normalized.totalIssues !== undefined ||
|
|
80
|
+
normalized.bySeverity.error !== undefined ||
|
|
81
|
+
normalized.bySeverity.warning !== undefined ||
|
|
82
|
+
normalized.bySeverity.info !== undefined
|
|
83
|
+
|
|
84
|
+
if (!hasAnyAnchor) {
|
|
85
|
+
throw new Error('Invalid guard baseline: expected score, totalIssues, or severity counters (error/warning/info).')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return normalized
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function readBaselineFromFile(projectPath: string, baselinePath?: string): { baseline: NormalizedBaseline; path: string } | undefined {
|
|
92
|
+
const resolvedBaselinePath = resolve(projectPath, baselinePath ?? 'drift-baseline.json')
|
|
93
|
+
if (!existsSync(resolvedBaselinePath)) return undefined
|
|
94
|
+
|
|
95
|
+
const raw = JSON.parse(readFileSync(resolvedBaselinePath, 'utf8')) as GuardBaseline
|
|
96
|
+
return {
|
|
97
|
+
baseline: normalizeBaseline(raw),
|
|
98
|
+
path: resolvedBaselinePath,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function remapBaseReportPaths(baseReport: DriftReport, tempDir: string, projectPath: string): DriftReport {
|
|
103
|
+
return {
|
|
104
|
+
...baseReport,
|
|
105
|
+
files: baseReport.files.map((file) => ({
|
|
106
|
+
...file,
|
|
107
|
+
path: resolve(projectPath, relative(tempDir, file.path)),
|
|
108
|
+
})),
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function countSeverityDeltaFromDiff(diff: DriftDiff): Record<IssueSeverity, number> {
|
|
113
|
+
const severityDelta: Record<IssueSeverity, number> = {
|
|
114
|
+
error: 0,
|
|
115
|
+
warning: 0,
|
|
116
|
+
info: 0,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
for (const file of diff.files) {
|
|
120
|
+
for (const issue of file.newIssues) {
|
|
121
|
+
severityDelta[issue.severity] += 1
|
|
122
|
+
}
|
|
123
|
+
for (const issue of file.resolvedIssues) {
|
|
124
|
+
severityDelta[issue.severity] -= 1
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return severityDelta
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function buildMetricsFromDiff(diff: DriftDiff): GuardMetrics {
|
|
132
|
+
return {
|
|
133
|
+
scoreDelta: diff.totalDelta,
|
|
134
|
+
totalIssuesDelta: diff.newIssuesCount - diff.resolvedIssuesCount,
|
|
135
|
+
severityDelta: countSeverityDeltaFromDiff(diff),
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function buildMetricsFromBaseline(current: DriftReport, baseline: NormalizedBaseline): GuardMetrics {
|
|
140
|
+
return {
|
|
141
|
+
scoreDelta: current.totalScore - (baseline.score ?? current.totalScore),
|
|
142
|
+
totalIssuesDelta: current.totalIssues - (baseline.totalIssues ?? current.totalIssues),
|
|
143
|
+
severityDelta: {
|
|
144
|
+
error: current.summary.errors - (baseline.bySeverity.error ?? current.summary.errors),
|
|
145
|
+
warning: current.summary.warnings - (baseline.bySeverity.warning ?? current.summary.warnings),
|
|
146
|
+
info: current.summary.infos - (baseline.bySeverity.info ?? current.summary.infos),
|
|
147
|
+
},
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
interface GuardCheckInput {
|
|
152
|
+
id: string
|
|
153
|
+
actual: number
|
|
154
|
+
limit: number
|
|
155
|
+
message: string
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function addCheck(checks: GuardCheck[], input: GuardCheckInput): void {
|
|
159
|
+
checks.push({
|
|
160
|
+
id: input.id,
|
|
161
|
+
passed: input.actual <= input.limit,
|
|
162
|
+
actual: input.actual,
|
|
163
|
+
limit: input.limit,
|
|
164
|
+
message: input.message,
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function evaluateGuard(input: GuardEvalInput): GuardEvaluation {
|
|
169
|
+
const checks: GuardCheck[] = []
|
|
170
|
+
|
|
171
|
+
if (input.enforceNoRegression.score) {
|
|
172
|
+
addCheck(checks, {
|
|
173
|
+
id: 'no-regression-score',
|
|
174
|
+
actual: input.metrics.scoreDelta,
|
|
175
|
+
limit: 0,
|
|
176
|
+
message: 'Score delta must be <= 0.',
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (input.enforceNoRegression.totalIssues) {
|
|
181
|
+
addCheck(checks, {
|
|
182
|
+
id: 'no-regression-total-issues',
|
|
183
|
+
actual: input.metrics.totalIssuesDelta,
|
|
184
|
+
limit: 0,
|
|
185
|
+
message: 'Total issues delta must be <= 0.',
|
|
186
|
+
})
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (typeof input.budget === 'number' && !Number.isNaN(input.budget)) {
|
|
190
|
+
addCheck(checks, {
|
|
191
|
+
id: 'budget-total-delta',
|
|
192
|
+
actual: input.metrics.scoreDelta,
|
|
193
|
+
limit: input.budget,
|
|
194
|
+
message: `Score delta must be <= budget (${input.budget}).`,
|
|
195
|
+
})
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const severityThresholds = input.bySeverity
|
|
199
|
+
if (severityThresholds) {
|
|
200
|
+
const severities: IssueSeverity[] = ['error', 'warning', 'info']
|
|
201
|
+
for (const severity of severities) {
|
|
202
|
+
const threshold = severityThresholds[severity]
|
|
203
|
+
if (typeof threshold !== 'number' || Number.isNaN(threshold)) continue
|
|
204
|
+
addCheck(checks, {
|
|
205
|
+
id: `severity-${severity}`,
|
|
206
|
+
actual: input.metrics.severityDelta[severity],
|
|
207
|
+
limit: threshold,
|
|
208
|
+
message: `${severity} delta must be <= ${threshold}.`,
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
passed: checks.every((check) => check.passed),
|
|
215
|
+
checks,
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function runGuard(targetPath: string, options: GuardOptions = {}): Promise<GuardResult> {
|
|
220
|
+
const runtimeState = await initializeGuardRuntime(targetPath, options)
|
|
221
|
+
const { projectPath, config, currentReport } = runtimeState
|
|
222
|
+
|
|
223
|
+
let tempDir: string | undefined
|
|
224
|
+
try {
|
|
225
|
+
if (options.baseRef) {
|
|
226
|
+
tempDir = extractFilesAtRef(projectPath, options.baseRef)
|
|
227
|
+
return createDiffGuardResult({
|
|
228
|
+
projectPath,
|
|
229
|
+
currentReport,
|
|
230
|
+
options,
|
|
231
|
+
tempDir,
|
|
232
|
+
config,
|
|
233
|
+
baseRef: options.baseRef,
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const inlineBaseline = options.baseline ? normalizeBaseline(options.baseline) : undefined
|
|
238
|
+
const fileBaseline = inlineBaseline ? undefined : readBaselineFromFile(projectPath, options.baselinePath)
|
|
239
|
+
const baseline = inlineBaseline ?? fileBaseline?.baseline
|
|
240
|
+
const baselinePath = fileBaseline?.path
|
|
241
|
+
|
|
242
|
+
if (!baseline) {
|
|
243
|
+
throw new Error('Guard requires a comparison point: provide baseRef or a baseline (inline or file).')
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return createBaselineGuardResult({
|
|
247
|
+
projectPath,
|
|
248
|
+
currentReport,
|
|
249
|
+
options,
|
|
250
|
+
baseline,
|
|
251
|
+
baselinePath,
|
|
252
|
+
})
|
|
253
|
+
} finally {
|
|
254
|
+
if (tempDir) cleanupTempDir(tempDir)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function initializeGuardRuntime(targetPath: string, options: GuardOptions): Promise<GuardRuntimeState> {
|
|
259
|
+
const projectPath = resolve(targetPath)
|
|
260
|
+
const config = await loadConfig(projectPath)
|
|
261
|
+
const currentFiles = analyzeProject(projectPath, config, options.analysis)
|
|
262
|
+
const currentReport = buildReport(projectPath, currentFiles)
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
projectPath,
|
|
266
|
+
config,
|
|
267
|
+
currentReport,
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function createDiffGuardResult(input: DiffGuardResultInput): GuardResult {
|
|
272
|
+
const { projectPath, currentReport, options, tempDir, config, baseRef } = input
|
|
273
|
+
const baseFiles = analyzeProject(tempDir, config, options.analysis)
|
|
274
|
+
const baseReport = buildReport(tempDir, baseFiles)
|
|
275
|
+
const remappedBase = remapBaseReportPaths(baseReport, tempDir, projectPath)
|
|
276
|
+
const diff = computeDiff(remappedBase, currentReport, baseRef)
|
|
277
|
+
const metrics = buildMetricsFromDiff(diff)
|
|
278
|
+
const evaluation = evaluateGuard({
|
|
279
|
+
metrics,
|
|
280
|
+
budget: options.budget,
|
|
281
|
+
bySeverity: options.bySeverity,
|
|
282
|
+
enforceNoRegression: {
|
|
283
|
+
score: true,
|
|
284
|
+
totalIssues: true,
|
|
285
|
+
},
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
scannedAt: new Date().toISOString(),
|
|
290
|
+
projectPath,
|
|
291
|
+
mode: 'diff',
|
|
292
|
+
passed: evaluation.passed,
|
|
293
|
+
baseRef,
|
|
294
|
+
metrics,
|
|
295
|
+
checks: evaluation.checks,
|
|
296
|
+
current: currentReport,
|
|
297
|
+
diff,
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function createBaselineGuardResult(input: BaselineGuardResultInput): GuardResult {
|
|
302
|
+
const { projectPath, currentReport, options, baseline, baselinePath } = input
|
|
303
|
+
const metrics = buildMetricsFromBaseline(currentReport, baseline)
|
|
304
|
+
const evaluation = evaluateGuard({
|
|
305
|
+
metrics,
|
|
306
|
+
budget: options.budget,
|
|
307
|
+
bySeverity: options.bySeverity,
|
|
308
|
+
enforceNoRegression: {
|
|
309
|
+
score: baseline.score !== undefined,
|
|
310
|
+
totalIssues: baseline.totalIssues !== undefined,
|
|
311
|
+
},
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
scannedAt: new Date().toISOString(),
|
|
316
|
+
projectPath,
|
|
317
|
+
mode: 'baseline',
|
|
318
|
+
passed: evaluation.passed,
|
|
319
|
+
baselinePath,
|
|
320
|
+
metrics,
|
|
321
|
+
checks: evaluation.checks,
|
|
322
|
+
current: currentReport,
|
|
323
|
+
}
|
|
324
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,23 +1,51 @@
|
|
|
1
1
|
export { analyzeProject, analyzeFile, RULE_WEIGHTS } from './analyzer.js'
|
|
2
2
|
export { buildReport, formatMarkdown } from './reporter.js'
|
|
3
3
|
export { computeDiff } from './diff.js'
|
|
4
|
+
export { runGuard, evaluateGuard } from './guard.js'
|
|
5
|
+
export type {
|
|
6
|
+
GuardBaseline,
|
|
7
|
+
GuardThresholds,
|
|
8
|
+
GuardOptions,
|
|
9
|
+
GuardMetrics,
|
|
10
|
+
GuardCheck,
|
|
11
|
+
GuardEvaluation,
|
|
12
|
+
GuardResult,
|
|
13
|
+
} from './guard-types.js'
|
|
4
14
|
export { generateReview, formatReviewMarkdown } from './review.js'
|
|
15
|
+
export { runDoctor } from './doctor.js'
|
|
16
|
+
export type { DoctorOptions } from './doctor.js'
|
|
5
17
|
export {
|
|
6
18
|
buildTrustReport,
|
|
7
19
|
formatTrustConsole,
|
|
8
20
|
formatTrustMarkdown,
|
|
9
21
|
formatTrustJson,
|
|
22
|
+
resolveTrustGatePolicy,
|
|
23
|
+
evaluateTrustGate,
|
|
10
24
|
shouldFailByMaxRisk,
|
|
11
25
|
shouldFailTrustGate,
|
|
12
26
|
normalizeMergeRiskLevel,
|
|
13
27
|
MERGE_RISK_ORDER,
|
|
14
28
|
} from './trust.js'
|
|
29
|
+
export type {
|
|
30
|
+
TrustGateOptions,
|
|
31
|
+
TrustGatePolicyResolutionOptions,
|
|
32
|
+
TrustGatePolicyResolutionStep,
|
|
33
|
+
TrustGateEvaluation,
|
|
34
|
+
} from './trust.js'
|
|
15
35
|
export {
|
|
16
36
|
computeTrustKpis,
|
|
17
37
|
computeTrustKpisFromReports,
|
|
18
38
|
formatTrustKpiConsole,
|
|
19
39
|
formatTrustKpiJson,
|
|
20
40
|
} from './trust-kpi.js'
|
|
41
|
+
export { toSarif, diffToSarif } from './sarif.js'
|
|
42
|
+
export type {
|
|
43
|
+
SarifLevel,
|
|
44
|
+
DriftSarifRule,
|
|
45
|
+
DriftSarifResult,
|
|
46
|
+
DriftSarifRun,
|
|
47
|
+
DriftSarifLog,
|
|
48
|
+
} from './sarif.js'
|
|
21
49
|
export { generateArchitectureMap, generateArchitectureSvg } from './map.js'
|
|
22
50
|
export type {
|
|
23
51
|
DriftReport,
|
|
@@ -39,6 +67,8 @@ export type {
|
|
|
39
67
|
MergeRiskLevel,
|
|
40
68
|
DriftPlugin,
|
|
41
69
|
DriftPluginRule,
|
|
70
|
+
TrustGatePolicyConfig,
|
|
71
|
+
TrustAdvancedContext,
|
|
42
72
|
} from './types.js'
|
|
43
73
|
export { loadHistory, saveSnapshot } from './snapshot.js'
|
|
44
74
|
export type { SnapshotEntry, SnapshotHistory } from './snapshot.js'
|
|
@@ -61,6 +91,11 @@ export {
|
|
|
61
91
|
generateSaasDashboardHtml,
|
|
62
92
|
} from './saas.js'
|
|
63
93
|
export type {
|
|
94
|
+
SaasUser,
|
|
95
|
+
SaasOrganization,
|
|
96
|
+
SaasWorkspace,
|
|
97
|
+
SaasRepo,
|
|
98
|
+
SaasMembership,
|
|
64
99
|
SaasRole,
|
|
65
100
|
SaasPlan,
|
|
66
101
|
SaasPolicy,
|