@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
package/src/plugins.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
2
|
import { isAbsolute, resolve } from 'node:path'
|
|
3
3
|
import { createRequire } from 'node:module'
|
|
4
|
-
import type {
|
|
4
|
+
import type { DriftPlugin, LoadedPlugin, PluginLoadError, PluginLoadWarning } from './types.js'
|
|
5
|
+
import { normalizeRules, type PluginValidationContext } from './plugins-rules.js'
|
|
6
|
+
import { validateCapabilities } from './plugins-capabilities.js'
|
|
7
|
+
import { pushError, pushWarning } from './plugins-messages.js'
|
|
5
8
|
|
|
6
9
|
const require = createRequire(import.meta.url)
|
|
7
|
-
const VALID_SEVERITIES: DriftIssue['severity'][] = ['error', 'warning', 'info']
|
|
8
10
|
const SUPPORTED_PLUGIN_API_VERSION = 1
|
|
9
|
-
const RULE_ID_REQUIRED = /^[a-z][a-z0-9]*(?:[-_/][a-z0-9]+)*$/
|
|
10
11
|
|
|
11
12
|
type PluginCandidate = {
|
|
12
13
|
name?: unknown
|
|
@@ -15,15 +16,6 @@ type PluginCandidate = {
|
|
|
15
16
|
rules?: unknown
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
type RuleCandidate = {
|
|
19
|
-
id?: unknown
|
|
20
|
-
name?: unknown
|
|
21
|
-
severity?: unknown
|
|
22
|
-
weight?: unknown
|
|
23
|
-
detect?: unknown
|
|
24
|
-
fix?: unknown
|
|
25
|
-
}
|
|
26
|
-
|
|
27
19
|
function normalizePluginExport(mod: unknown): unknown {
|
|
28
20
|
if (mod && typeof mod === 'object' && 'default' in mod) {
|
|
29
21
|
return (mod as { default?: unknown }).default ?? mod
|
|
@@ -31,194 +23,46 @@ function normalizePluginExport(mod: unknown): unknown {
|
|
|
31
23
|
return mod
|
|
32
24
|
}
|
|
33
25
|
|
|
34
|
-
function
|
|
35
|
-
errors: PluginLoadError[],
|
|
26
|
+
function ensureObjectCandidate(
|
|
36
27
|
pluginId: string,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
):
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
ruleId: options?.ruleId,
|
|
44
|
-
code: options?.code,
|
|
45
|
-
message,
|
|
46
|
-
})
|
|
47
|
-
}
|
|
28
|
+
candidate: unknown,
|
|
29
|
+
errors: PluginLoadError[],
|
|
30
|
+
): PluginCandidate | undefined {
|
|
31
|
+
if (candidate && typeof candidate === 'object') {
|
|
32
|
+
return candidate as PluginCandidate
|
|
33
|
+
}
|
|
48
34
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
pluginId: string,
|
|
52
|
-
message: string,
|
|
53
|
-
options?: { pluginName?: string; ruleId?: string; code?: string },
|
|
54
|
-
): void {
|
|
55
|
-
warnings.push({
|
|
35
|
+
pushError(
|
|
36
|
+
errors,
|
|
56
37
|
pluginId,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
})
|
|
38
|
+
`Invalid plugin contract in '${pluginId}'. Expected an object export with shape { name, rules[] }`,
|
|
39
|
+
{ code: 'plugin-shape-invalid' },
|
|
40
|
+
)
|
|
41
|
+
return undefined
|
|
62
42
|
}
|
|
63
43
|
|
|
64
|
-
function
|
|
44
|
+
function ensurePluginName(
|
|
65
45
|
pluginId: string,
|
|
66
|
-
|
|
67
|
-
rawRule: RuleCandidate,
|
|
68
|
-
ruleIndex: number,
|
|
69
|
-
options: { strictRuleId: boolean },
|
|
46
|
+
plugin: PluginCandidate,
|
|
70
47
|
errors: PluginLoadError[],
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
? rawRule.id.trim()
|
|
75
|
-
: typeof rawRule.name === 'string'
|
|
76
|
-
? rawRule.name.trim()
|
|
77
|
-
: ''
|
|
78
|
-
|
|
79
|
-
const ruleLabel = rawRuleId || `rule#${ruleIndex + 1}`
|
|
80
|
-
|
|
81
|
-
if (!rawRuleId) {
|
|
82
|
-
pushError(
|
|
83
|
-
errors,
|
|
84
|
-
pluginId,
|
|
85
|
-
`Invalid rule at index ${ruleIndex}. Expected 'id' or 'name' as a non-empty string.`,
|
|
86
|
-
{ pluginName, code: 'plugin-rule-id-missing' },
|
|
87
|
-
)
|
|
88
|
-
return undefined
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (typeof rawRule.detect !== 'function') {
|
|
92
|
-
pushError(
|
|
93
|
-
errors,
|
|
94
|
-
pluginId,
|
|
95
|
-
`Rule '${rawRuleId}' is invalid. Expected 'detect(file, context)' function.`,
|
|
96
|
-
{ pluginName, ruleId: rawRuleId, code: 'plugin-rule-detect-invalid' },
|
|
97
|
-
)
|
|
98
|
-
return undefined
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (rawRule.detect.length > 2) {
|
|
102
|
-
pushWarning(
|
|
103
|
-
warnings,
|
|
104
|
-
pluginId,
|
|
105
|
-
`Rule '${rawRuleId}' detect() declares ${rawRule.detect.length} parameters. Expected 1-2 parameters (file, context).`,
|
|
106
|
-
{ pluginName, ruleId: rawRuleId, code: 'plugin-rule-detect-arity' },
|
|
107
|
-
)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (!RULE_ID_REQUIRED.test(rawRuleId)) {
|
|
111
|
-
if (options.strictRuleId) {
|
|
112
|
-
pushError(
|
|
113
|
-
errors,
|
|
114
|
-
pluginId,
|
|
115
|
-
`Rule id '${ruleLabel}' is invalid. Use lowercase letters, numbers, and separators (-, _, /), starting with a letter.`,
|
|
116
|
-
{ pluginName, ruleId: rawRuleId, code: 'plugin-rule-id-invalid' },
|
|
117
|
-
)
|
|
118
|
-
return undefined
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
pushWarning(
|
|
122
|
-
warnings,
|
|
123
|
-
pluginId,
|
|
124
|
-
`Rule id '${ruleLabel}' uses a legacy format. For forward compatibility, migrate to lowercase kebab-case and set apiVersion: ${SUPPORTED_PLUGIN_API_VERSION}.`,
|
|
125
|
-
{ pluginName, ruleId: rawRuleId, code: 'plugin-rule-id-format-legacy' },
|
|
126
|
-
)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
let severity: DriftIssue['severity'] | undefined
|
|
130
|
-
if (rawRule.severity !== undefined) {
|
|
131
|
-
if (typeof rawRule.severity === 'string' && VALID_SEVERITIES.includes(rawRule.severity as DriftIssue['severity'])) {
|
|
132
|
-
severity = rawRule.severity as DriftIssue['severity']
|
|
133
|
-
} else {
|
|
134
|
-
pushError(
|
|
135
|
-
errors,
|
|
136
|
-
pluginId,
|
|
137
|
-
`Rule '${rawRuleId}' has invalid severity '${String(rawRule.severity)}'. Allowed: error, warning, info.`,
|
|
138
|
-
{ pluginName, ruleId: rawRuleId, code: 'plugin-rule-severity-invalid' },
|
|
139
|
-
)
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
let weight: number | undefined
|
|
144
|
-
if (rawRule.weight !== undefined) {
|
|
145
|
-
if (typeof rawRule.weight === 'number' && Number.isFinite(rawRule.weight) && rawRule.weight >= 0 && rawRule.weight <= 100) {
|
|
146
|
-
weight = rawRule.weight
|
|
147
|
-
} else {
|
|
148
|
-
pushError(
|
|
149
|
-
errors,
|
|
150
|
-
pluginId,
|
|
151
|
-
`Rule '${rawRuleId}' has invalid weight '${String(rawRule.weight)}'. Expected a finite number between 0 and 100.`,
|
|
152
|
-
{ pluginName, ruleId: rawRuleId, code: 'plugin-rule-weight-invalid' },
|
|
153
|
-
)
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
let fix: DriftPluginRule['fix'] | undefined
|
|
158
|
-
if (rawRule.fix !== undefined) {
|
|
159
|
-
if (typeof rawRule.fix === 'function') {
|
|
160
|
-
fix = rawRule.fix as DriftPluginRule['fix']
|
|
161
|
-
if (rawRule.fix.length > 3) {
|
|
162
|
-
pushWarning(
|
|
163
|
-
warnings,
|
|
164
|
-
pluginId,
|
|
165
|
-
`Rule '${rawRuleId}' fix() declares ${rawRule.fix.length} parameters. Expected up to 3 (issue, file, context).`,
|
|
166
|
-
{ pluginName, ruleId: rawRuleId, code: 'plugin-rule-fix-arity' },
|
|
167
|
-
)
|
|
168
|
-
}
|
|
169
|
-
} else {
|
|
170
|
-
pushError(
|
|
171
|
-
errors,
|
|
172
|
-
pluginId,
|
|
173
|
-
`Rule '${rawRuleId}' has invalid fix. Expected a function when provided.`,
|
|
174
|
-
{ pluginName, ruleId: rawRuleId, code: 'plugin-rule-fix-invalid' },
|
|
175
|
-
)
|
|
176
|
-
}
|
|
177
|
-
}
|
|
48
|
+
): string | undefined {
|
|
49
|
+
const pluginName = typeof plugin.name === 'string' ? plugin.name.trim() : ''
|
|
50
|
+
if (pluginName) return pluginName
|
|
178
51
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
}
|
|
52
|
+
pushError(
|
|
53
|
+
errors,
|
|
54
|
+
pluginId,
|
|
55
|
+
`Invalid plugin contract in '${pluginId}'. Expected 'name' as a non-empty string.`,
|
|
56
|
+
{ code: 'plugin-name-missing' },
|
|
57
|
+
)
|
|
58
|
+
return undefined
|
|
187
59
|
}
|
|
188
60
|
|
|
189
|
-
function
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
): {
|
|
193
|
-
|
|
194
|
-
errors: PluginLoadError[]
|
|
195
|
-
warnings: PluginLoadWarning[]
|
|
196
|
-
} {
|
|
197
|
-
const errors: PluginLoadError[] = []
|
|
198
|
-
const warnings: PluginLoadWarning[] = []
|
|
199
|
-
|
|
200
|
-
if (!candidate || typeof candidate !== 'object') {
|
|
201
|
-
pushError(
|
|
202
|
-
errors,
|
|
203
|
-
pluginId,
|
|
204
|
-
`Invalid plugin contract in '${pluginId}'. Expected an object export with shape { name, rules[] }`,
|
|
205
|
-
{ code: 'plugin-shape-invalid' },
|
|
206
|
-
)
|
|
207
|
-
return { errors, warnings }
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const plugin = candidate as PluginCandidate
|
|
211
|
-
const pluginName = typeof plugin.name === 'string' ? plugin.name.trim() : ''
|
|
212
|
-
if (!pluginName) {
|
|
213
|
-
pushError(
|
|
214
|
-
errors,
|
|
215
|
-
pluginId,
|
|
216
|
-
`Invalid plugin contract in '${pluginId}'. Expected 'name' as a non-empty string.`,
|
|
217
|
-
{ code: 'plugin-name-missing' },
|
|
218
|
-
)
|
|
219
|
-
return { errors, warnings }
|
|
220
|
-
}
|
|
221
|
-
|
|
61
|
+
function validateApiVersion(
|
|
62
|
+
plugin: PluginCandidate,
|
|
63
|
+
context: PluginValidationContext,
|
|
64
|
+
): { hasExplicitApiVersion: boolean; isLegacyPlugin: boolean; isSupported: boolean } {
|
|
65
|
+
const { pluginId, pluginName, errors, warnings } = context
|
|
222
66
|
const hasExplicitApiVersion = plugin.apiVersion !== undefined
|
|
223
67
|
const isLegacyPlugin = !hasExplicitApiVersion
|
|
224
68
|
|
|
@@ -229,119 +73,60 @@ function validatePluginContract(
|
|
|
229
73
|
`Plugin '${pluginName}' does not declare 'apiVersion'. Assuming ${SUPPORTED_PLUGIN_API_VERSION} for backward compatibility; please add apiVersion: ${SUPPORTED_PLUGIN_API_VERSION}.`,
|
|
230
74
|
{ pluginName, code: 'plugin-api-version-implicit' },
|
|
231
75
|
)
|
|
232
|
-
|
|
76
|
+
return { hasExplicitApiVersion, isLegacyPlugin, isSupported: true }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (typeof plugin.apiVersion !== 'number' || !Number.isInteger(plugin.apiVersion) || plugin.apiVersion <= 0) {
|
|
233
80
|
pushError(
|
|
234
81
|
errors,
|
|
235
82
|
pluginId,
|
|
236
83
|
`Plugin '${pluginName}' has invalid apiVersion '${String(plugin.apiVersion)}'. Expected a positive integer (for example: ${SUPPORTED_PLUGIN_API_VERSION}).`,
|
|
237
84
|
{ pluginName, code: 'plugin-api-version-invalid' },
|
|
238
85
|
)
|
|
239
|
-
return {
|
|
240
|
-
}
|
|
86
|
+
return { hasExplicitApiVersion, isLegacyPlugin, isSupported: false }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (plugin.apiVersion !== SUPPORTED_PLUGIN_API_VERSION) {
|
|
241
90
|
pushError(
|
|
242
91
|
errors,
|
|
243
92
|
pluginId,
|
|
244
93
|
`Plugin '${pluginName}' targets apiVersion ${plugin.apiVersion}, but this drift build supports apiVersion ${SUPPORTED_PLUGIN_API_VERSION}.`,
|
|
245
94
|
{ pluginName, code: 'plugin-api-version-unsupported' },
|
|
246
95
|
)
|
|
247
|
-
return {
|
|
96
|
+
return { hasExplicitApiVersion, isLegacyPlugin, isSupported: false }
|
|
248
97
|
}
|
|
249
98
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if (!plugin.capabilities || typeof plugin.capabilities !== 'object' || Array.isArray(plugin.capabilities)) {
|
|
253
|
-
pushError(
|
|
254
|
-
errors,
|
|
255
|
-
pluginId,
|
|
256
|
-
`Plugin '${pluginName}' has invalid capabilities metadata. Expected an object map like { "fixes": true } when provided.`,
|
|
257
|
-
{ pluginName, code: 'plugin-capabilities-invalid' },
|
|
258
|
-
)
|
|
259
|
-
return { errors, warnings }
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const entries = Object.entries(plugin.capabilities as Record<string, unknown>)
|
|
263
|
-
for (const [capabilityKey, capabilityValue] of entries) {
|
|
264
|
-
const capabilityType = typeof capabilityValue
|
|
265
|
-
if (capabilityType !== 'string' && capabilityType !== 'number' && capabilityType !== 'boolean') {
|
|
266
|
-
pushError(
|
|
267
|
-
errors,
|
|
268
|
-
pluginId,
|
|
269
|
-
`Plugin '${pluginName}' capability '${capabilityKey}' has invalid value type '${capabilityType}'. Allowed: string | number | boolean.`,
|
|
270
|
-
{ pluginName, code: 'plugin-capabilities-value-invalid' },
|
|
271
|
-
)
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (errors.length > 0) {
|
|
276
|
-
return { errors, warnings }
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
capabilities = plugin.capabilities as DriftPlugin['capabilities']
|
|
280
|
-
}
|
|
99
|
+
return { hasExplicitApiVersion, isLegacyPlugin, isSupported: true }
|
|
100
|
+
}
|
|
281
101
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
return { errors, warnings }
|
|
290
|
-
}
|
|
102
|
+
function validatePluginContractData(pluginId: string, candidate: unknown): {
|
|
103
|
+
plugin?: DriftPlugin
|
|
104
|
+
errors: PluginLoadError[]
|
|
105
|
+
warnings: PluginLoadWarning[]
|
|
106
|
+
} {
|
|
107
|
+
const errors: PluginLoadError[] = []
|
|
108
|
+
const warnings: PluginLoadWarning[] = []
|
|
291
109
|
|
|
292
|
-
const
|
|
293
|
-
|
|
110
|
+
const plugin = ensureObjectCandidate(pluginId, candidate, errors)
|
|
111
|
+
if (!plugin) return { errors, warnings }
|
|
294
112
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
pushError(
|
|
298
|
-
errors,
|
|
299
|
-
pluginId,
|
|
300
|
-
`Invalid rule at index ${ruleIndex} in plugin '${pluginName}'. Expected an object.`,
|
|
301
|
-
{ pluginName, code: 'plugin-rule-shape-invalid' },
|
|
302
|
-
)
|
|
303
|
-
continue
|
|
304
|
-
}
|
|
113
|
+
const pluginName = ensurePluginName(pluginId, plugin, errors)
|
|
114
|
+
if (!pluginName) return { errors, warnings }
|
|
305
115
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
rawRule as RuleCandidate,
|
|
310
|
-
ruleIndex,
|
|
311
|
-
{ strictRuleId: !isLegacyPlugin },
|
|
312
|
-
errors,
|
|
313
|
-
warnings,
|
|
314
|
-
)
|
|
315
|
-
if (normalized) {
|
|
316
|
-
if (seenRuleIds.has(normalized.id ?? normalized.name)) {
|
|
317
|
-
pushError(
|
|
318
|
-
errors,
|
|
319
|
-
pluginId,
|
|
320
|
-
`Plugin '${pluginName}' defines duplicate rule id '${normalized.id ?? normalized.name}'. Rule ids must be unique within a plugin.`,
|
|
321
|
-
{ pluginName, ruleId: normalized.id ?? normalized.name, code: 'plugin-rule-id-duplicate' },
|
|
322
|
-
)
|
|
323
|
-
continue
|
|
324
|
-
}
|
|
116
|
+
const context: PluginValidationContext = { pluginId, pluginName, errors, warnings }
|
|
117
|
+
const apiVersion = validateApiVersion(plugin, context)
|
|
118
|
+
if (!apiVersion.isSupported) return { errors, warnings }
|
|
325
119
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
}
|
|
329
|
-
}
|
|
120
|
+
const capabilities = validateCapabilities(plugin.capabilities, context)
|
|
121
|
+
if (errors.length > 0) return { errors, warnings }
|
|
330
122
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
errors,
|
|
334
|
-
pluginId,
|
|
335
|
-
`Plugin '${pluginName}' has no valid rules after validation.`,
|
|
336
|
-
{ pluginName, code: 'plugin-rules-empty' },
|
|
337
|
-
)
|
|
338
|
-
return { errors, warnings }
|
|
339
|
-
}
|
|
123
|
+
const normalizedRules = normalizeRules(plugin.rules, apiVersion.isLegacyPlugin, context)
|
|
124
|
+
if (!normalizedRules) return { errors, warnings }
|
|
340
125
|
|
|
341
126
|
return {
|
|
342
127
|
plugin: {
|
|
343
128
|
name: pluginName,
|
|
344
|
-
apiVersion: hasExplicitApiVersion ? plugin.apiVersion as number : SUPPORTED_PLUGIN_API_VERSION,
|
|
129
|
+
apiVersion: apiVersion.hasExplicitApiVersion ? plugin.apiVersion as number : SUPPORTED_PLUGIN_API_VERSION,
|
|
345
130
|
capabilities,
|
|
346
131
|
rules: normalizedRules,
|
|
347
132
|
},
|
|
@@ -350,6 +135,17 @@ function validatePluginContract(
|
|
|
350
135
|
}
|
|
351
136
|
}
|
|
352
137
|
|
|
138
|
+
function validatePluginContract(
|
|
139
|
+
pluginId: string,
|
|
140
|
+
candidate: unknown,
|
|
141
|
+
): {
|
|
142
|
+
plugin?: DriftPlugin
|
|
143
|
+
errors: PluginLoadError[]
|
|
144
|
+
warnings: PluginLoadWarning[]
|
|
145
|
+
} {
|
|
146
|
+
return validatePluginContractData(pluginId, candidate)
|
|
147
|
+
}
|
|
148
|
+
|
|
353
149
|
function resolvePluginSpecifier(projectRoot: string, pluginId: string): string {
|
|
354
150
|
if (pluginId.startsWith('.') || pluginId.startsWith('/')) {
|
|
355
151
|
const abs = isAbsolute(pluginId) ? pluginId : resolve(projectRoot, pluginId)
|
|
@@ -386,10 +182,7 @@ export function loadPlugins(projectRoot: string, pluginIds: string[] | undefined
|
|
|
386
182
|
errors.push(...validation.errors)
|
|
387
183
|
warnings.push(...validation.warnings)
|
|
388
184
|
|
|
389
|
-
if (!validation.plugin)
|
|
390
|
-
continue
|
|
391
|
-
}
|
|
392
|
-
|
|
185
|
+
if (!validation.plugin) continue
|
|
393
186
|
loaded.push({ id: pluginId, plugin: validation.plugin })
|
|
394
187
|
} catch (error) {
|
|
395
188
|
errors.push({
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { DriftIssue } from './types.js'
|
|
2
|
+
|
|
3
|
+
export const FIX_SUGGESTIONS: Record<string, string> = {
|
|
4
|
+
'large-file': 'Consider splitting this file into smaller modules with single responsibility',
|
|
5
|
+
'large-function': 'Extract logic into smaller functions with descriptive names',
|
|
6
|
+
'debug-leftover': 'Remove this console.log or replace with proper logging library',
|
|
7
|
+
'dead-code': 'Remove unused import to keep code clean',
|
|
8
|
+
'duplicate-function-name': 'Consolidate with existing function or rename to clarify different behavior',
|
|
9
|
+
'any-abuse': "Replace 'any' with proper type definition",
|
|
10
|
+
'catch-swallow': 'Add error handling or logging in catch block',
|
|
11
|
+
'no-return-type': 'Add explicit return type for better type safety',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const RULE_EFFORT: Record<string, 'low' | 'medium' | 'high'> = {
|
|
15
|
+
'debug-leftover': 'low',
|
|
16
|
+
'dead-code': 'low',
|
|
17
|
+
'no-return-type': 'low',
|
|
18
|
+
'any-abuse': 'medium',
|
|
19
|
+
'catch-swallow': 'medium',
|
|
20
|
+
'large-file': 'high',
|
|
21
|
+
'large-function': 'high',
|
|
22
|
+
'duplicate-function-name': 'high',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const SEVERITY_ORDER: Record<string, number> = { error: 0, warning: 1, info: 2 }
|
|
26
|
+
export const EFFORT_ORDER: Record<string, number> = { low: 0, medium: 1, high: 2 }
|
|
27
|
+
|
|
28
|
+
export const AI_SIGNAL_RULES = new Set([
|
|
29
|
+
'over-commented',
|
|
30
|
+
'hardcoded-config',
|
|
31
|
+
'inconsistent-error-handling',
|
|
32
|
+
'unnecessary-abstraction',
|
|
33
|
+
'naming-inconsistency',
|
|
34
|
+
'comment-contradiction',
|
|
35
|
+
'promise-style-mix',
|
|
36
|
+
'any-abuse',
|
|
37
|
+
'ai-code-smell',
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
export const AI_CODE_SMELL_BOOST = 20
|
|
41
|
+
export const AI_TRIGGER_LIMIT = 4
|
|
42
|
+
export const AI_LIKELIHOOD_THRESHOLD = 35
|
|
43
|
+
export const AI_SMELL_SCORE_MULTIPLIER = 15
|
|
44
|
+
export const AI_SUSPECTED_LIMIT = 10
|
|
45
|
+
|
|
46
|
+
export type DriftIssueWithFile = { file: string; issue: DriftIssue }
|