@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
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import type { DriftIssue, DriftPluginRule, PluginLoadError, PluginLoadWarning } from './types.js'
|
|
2
|
+
import { pushError, pushWarning } from './plugins-messages.js'
|
|
3
|
+
|
|
4
|
+
const VALID_SEVERITIES: DriftIssue['severity'][] = ['error', 'warning', 'info']
|
|
5
|
+
const MAX_FIX_ARITY = 3
|
|
6
|
+
const RULE_ID_REQUIRED = /^[a-z][a-z0-9]*(?:[-_/][a-z0-9]+)*$/
|
|
7
|
+
|
|
8
|
+
type RuleCandidate = {
|
|
9
|
+
id?: unknown
|
|
10
|
+
name?: unknown
|
|
11
|
+
severity?: unknown
|
|
12
|
+
weight?: unknown
|
|
13
|
+
detect?: unknown
|
|
14
|
+
fix?: unknown
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type NormalizeRuleContext = {
|
|
18
|
+
pluginId: string
|
|
19
|
+
pluginName: string
|
|
20
|
+
ruleIndex: number
|
|
21
|
+
strictRuleId: boolean
|
|
22
|
+
errors: PluginLoadError[]
|
|
23
|
+
warnings: PluginLoadWarning[]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type RuleValidationContext = {
|
|
27
|
+
pluginId: string
|
|
28
|
+
pluginName: string
|
|
29
|
+
ruleId: string
|
|
30
|
+
errors: PluginLoadError[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type RuleMessageContext = {
|
|
34
|
+
pluginId: string
|
|
35
|
+
pluginName: string
|
|
36
|
+
ruleId: string
|
|
37
|
+
errors: PluginLoadError[]
|
|
38
|
+
warnings: PluginLoadWarning[]
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type PluginValidationContext = {
|
|
42
|
+
pluginId: string
|
|
43
|
+
pluginName: string
|
|
44
|
+
errors: PluginLoadError[]
|
|
45
|
+
warnings: PluginLoadWarning[]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveRawRuleId(rawRule: RuleCandidate): string {
|
|
49
|
+
if (typeof rawRule.id === 'string') return rawRule.id.trim()
|
|
50
|
+
if (typeof rawRule.name === 'string') return rawRule.name.trim()
|
|
51
|
+
return ''
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function ensureRuleId(
|
|
55
|
+
rawRuleId: string,
|
|
56
|
+
ruleIndex: number,
|
|
57
|
+
context: RuleMessageContext,
|
|
58
|
+
): boolean {
|
|
59
|
+
if (rawRuleId) return true
|
|
60
|
+
|
|
61
|
+
pushError(
|
|
62
|
+
context.errors,
|
|
63
|
+
context.pluginId,
|
|
64
|
+
`Invalid rule at index ${ruleIndex}. Expected 'id' or 'name' as a non-empty string.`,
|
|
65
|
+
{ pluginName: context.pluginName, code: 'plugin-rule-id-missing' },
|
|
66
|
+
)
|
|
67
|
+
return false
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function ensureDetectFunction(
|
|
71
|
+
detect: unknown,
|
|
72
|
+
context: RuleMessageContext,
|
|
73
|
+
): detect is DriftPluginRule['detect'] {
|
|
74
|
+
if (typeof detect === 'function') return true
|
|
75
|
+
|
|
76
|
+
pushError(
|
|
77
|
+
context.errors,
|
|
78
|
+
context.pluginId,
|
|
79
|
+
`Rule '${context.ruleId}' is invalid. Expected 'detect(file, context)' function.`,
|
|
80
|
+
{ pluginName: context.pluginName, ruleId: context.ruleId, code: 'plugin-rule-detect-invalid' },
|
|
81
|
+
)
|
|
82
|
+
return false
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function warnDetectArity(
|
|
86
|
+
detect: DriftPluginRule['detect'],
|
|
87
|
+
context: RuleMessageContext,
|
|
88
|
+
): void {
|
|
89
|
+
if (detect.length <= 2) return
|
|
90
|
+
|
|
91
|
+
pushWarning(
|
|
92
|
+
context.warnings,
|
|
93
|
+
context.pluginId,
|
|
94
|
+
`Rule '${context.ruleId}' detect() declares ${detect.length} parameters. Expected 1-2 parameters (file, context).`,
|
|
95
|
+
{ pluginName: context.pluginName, ruleId: context.ruleId, code: 'plugin-rule-detect-arity' },
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function validateRuleIdentifierFormat(
|
|
100
|
+
rawRuleId: string,
|
|
101
|
+
strictRuleId: boolean,
|
|
102
|
+
context: RuleMessageContext,
|
|
103
|
+
): void {
|
|
104
|
+
if (RULE_ID_REQUIRED.test(rawRuleId)) return
|
|
105
|
+
const ruleLabel = rawRuleId || 'unknown-rule'
|
|
106
|
+
|
|
107
|
+
if (strictRuleId) {
|
|
108
|
+
pushError(
|
|
109
|
+
context.errors,
|
|
110
|
+
context.pluginId,
|
|
111
|
+
`Rule id '${ruleLabel}' is invalid. Use lowercase letters, numbers, and separators (-, _, /), starting with a letter.`,
|
|
112
|
+
{ pluginName: context.pluginName, ruleId: rawRuleId, code: 'plugin-rule-id-invalid' },
|
|
113
|
+
)
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
pushWarning(
|
|
118
|
+
context.warnings,
|
|
119
|
+
context.pluginId,
|
|
120
|
+
`Rule id '${ruleLabel}' uses a legacy format. For forward compatibility, migrate to lowercase kebab-case and set apiVersion: 1.`,
|
|
121
|
+
{ pluginName: context.pluginName, ruleId: rawRuleId, code: 'plugin-rule-id-format-legacy' },
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function resolveRuleSeverity(
|
|
126
|
+
rawSeverity: unknown,
|
|
127
|
+
context: RuleValidationContext,
|
|
128
|
+
): DriftIssue['severity'] | undefined {
|
|
129
|
+
if (rawSeverity === undefined) return undefined
|
|
130
|
+
if (typeof rawSeverity === 'string' && VALID_SEVERITIES.includes(rawSeverity as DriftIssue['severity'])) {
|
|
131
|
+
return rawSeverity as DriftIssue['severity']
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
pushError(
|
|
135
|
+
context.errors,
|
|
136
|
+
context.pluginId,
|
|
137
|
+
`Rule '${context.ruleId}' has invalid severity '${String(rawSeverity)}'. Allowed: error, warning, info.`,
|
|
138
|
+
{ pluginName: context.pluginName, ruleId: context.ruleId, code: 'plugin-rule-severity-invalid' },
|
|
139
|
+
)
|
|
140
|
+
return undefined
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function resolveRuleWeight(rawWeight: unknown, context: RuleValidationContext): number | undefined {
|
|
144
|
+
if (rawWeight === undefined) return undefined
|
|
145
|
+
if (typeof rawWeight === 'number' && Number.isFinite(rawWeight) && rawWeight >= 0 && rawWeight <= 100) {
|
|
146
|
+
return rawWeight
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
pushError(
|
|
150
|
+
context.errors,
|
|
151
|
+
context.pluginId,
|
|
152
|
+
`Rule '${context.ruleId}' has invalid weight '${String(rawWeight)}'. Expected a finite number between 0 and 100.`,
|
|
153
|
+
{ pluginName: context.pluginName, ruleId: context.ruleId, code: 'plugin-rule-weight-invalid' },
|
|
154
|
+
)
|
|
155
|
+
return undefined
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function resolveRuleFix(
|
|
159
|
+
rawFix: unknown,
|
|
160
|
+
context: RuleMessageContext,
|
|
161
|
+
): DriftPluginRule['fix'] | undefined {
|
|
162
|
+
if (rawFix === undefined) return undefined
|
|
163
|
+
if (typeof rawFix !== 'function') {
|
|
164
|
+
pushError(
|
|
165
|
+
context.errors,
|
|
166
|
+
context.pluginId,
|
|
167
|
+
`Rule '${context.ruleId}' has invalid fix. Expected a function when provided.`,
|
|
168
|
+
{ pluginName: context.pluginName, ruleId: context.ruleId, code: 'plugin-rule-fix-invalid' },
|
|
169
|
+
)
|
|
170
|
+
return undefined
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (rawFix.length > MAX_FIX_ARITY) {
|
|
174
|
+
pushWarning(
|
|
175
|
+
context.warnings,
|
|
176
|
+
context.pluginId,
|
|
177
|
+
`Rule '${context.ruleId}' fix() declares ${rawFix.length} parameters. Expected up to ${MAX_FIX_ARITY} (issue, file, context).`,
|
|
178
|
+
{ pluginName: context.pluginName, ruleId: context.ruleId, code: 'plugin-rule-fix-arity' },
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return rawFix as DriftPluginRule['fix']
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function normalizeRule(
|
|
186
|
+
rawRule: RuleCandidate,
|
|
187
|
+
context: NormalizeRuleContext,
|
|
188
|
+
): DriftPluginRule | undefined {
|
|
189
|
+
const { pluginId, pluginName, ruleIndex, strictRuleId, errors, warnings } = context
|
|
190
|
+
const rawRuleId = resolveRawRuleId(rawRule)
|
|
191
|
+
const messageContext = { pluginId, pluginName, ruleId: rawRuleId, errors, warnings }
|
|
192
|
+
if (!ensureRuleId(rawRuleId, ruleIndex, messageContext)) return undefined
|
|
193
|
+
if (!ensureDetectFunction(rawRule.detect, messageContext)) return undefined
|
|
194
|
+
|
|
195
|
+
validateRuleIdentifierFormat(rawRuleId, strictRuleId, messageContext)
|
|
196
|
+
warnDetectArity(rawRule.detect, messageContext)
|
|
197
|
+
const ruleValidationContext: RuleValidationContext = { pluginId, pluginName, ruleId: rawRuleId, errors }
|
|
198
|
+
const severity = resolveRuleSeverity(rawRule.severity, ruleValidationContext)
|
|
199
|
+
const weight = resolveRuleWeight(rawRule.weight, ruleValidationContext)
|
|
200
|
+
const fix = resolveRuleFix(rawRule.fix, messageContext)
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
id: rawRuleId,
|
|
204
|
+
name: rawRuleId,
|
|
205
|
+
detect: rawRule.detect as DriftPluginRule['detect'],
|
|
206
|
+
severity,
|
|
207
|
+
weight,
|
|
208
|
+
fix,
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function ensureUniqueRuleId(
|
|
213
|
+
rule: DriftPluginRule,
|
|
214
|
+
seenRuleIds: Set<string>,
|
|
215
|
+
context: PluginValidationContext,
|
|
216
|
+
): boolean {
|
|
217
|
+
const normalizedRuleId = rule.id ?? rule.name
|
|
218
|
+
if (seenRuleIds.has(normalizedRuleId)) {
|
|
219
|
+
pushError(
|
|
220
|
+
context.errors,
|
|
221
|
+
context.pluginId,
|
|
222
|
+
`Plugin '${context.pluginName}' defines duplicate rule id '${normalizedRuleId}'. Rule ids must be unique within a plugin.`,
|
|
223
|
+
{ pluginName: context.pluginName, ruleId: normalizedRuleId, code: 'plugin-rule-id-duplicate' },
|
|
224
|
+
)
|
|
225
|
+
return false
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
seenRuleIds.add(normalizedRuleId)
|
|
229
|
+
return true
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function normalizeRulesArray(
|
|
233
|
+
rulesCandidate: unknown[],
|
|
234
|
+
context: PluginValidationContext,
|
|
235
|
+
strictRuleId: boolean,
|
|
236
|
+
): DriftPluginRule[] {
|
|
237
|
+
const normalizedRules: DriftPluginRule[] = []
|
|
238
|
+
const seenRuleIds = new Set<string>()
|
|
239
|
+
|
|
240
|
+
for (const [ruleIndex, rawRule] of rulesCandidate.entries()) {
|
|
241
|
+
if (!rawRule || typeof rawRule !== 'object') {
|
|
242
|
+
pushError(
|
|
243
|
+
context.errors,
|
|
244
|
+
context.pluginId,
|
|
245
|
+
`Invalid rule at index ${ruleIndex} in plugin '${context.pluginName}'. Expected an object.`,
|
|
246
|
+
{ pluginName: context.pluginName, code: 'plugin-rule-shape-invalid' },
|
|
247
|
+
)
|
|
248
|
+
continue
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const normalized = normalizeRule(rawRule as RuleCandidate, {
|
|
252
|
+
pluginId: context.pluginId,
|
|
253
|
+
pluginName: context.pluginName,
|
|
254
|
+
ruleIndex,
|
|
255
|
+
strictRuleId,
|
|
256
|
+
errors: context.errors,
|
|
257
|
+
warnings: context.warnings,
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
if (!normalized) continue
|
|
261
|
+
if (ensureUniqueRuleId(normalized, seenRuleIds, context)) {
|
|
262
|
+
normalizedRules.push(normalized)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return normalizedRules
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function normalizeRules(
|
|
270
|
+
rulesCandidate: unknown,
|
|
271
|
+
isLegacyPlugin: boolean,
|
|
272
|
+
context: PluginValidationContext,
|
|
273
|
+
): DriftPluginRule[] | undefined {
|
|
274
|
+
if (!Array.isArray(rulesCandidate)) {
|
|
275
|
+
pushError(
|
|
276
|
+
context.errors,
|
|
277
|
+
context.pluginId,
|
|
278
|
+
`Invalid plugin '${context.pluginName}'. Expected 'rules' to be an array.`,
|
|
279
|
+
{ pluginName: context.pluginName, code: 'plugin-rules-not-array' },
|
|
280
|
+
)
|
|
281
|
+
return undefined
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const normalizedRules = normalizeRulesArray(rulesCandidate, context, !isLegacyPlugin)
|
|
285
|
+
if (normalizedRules.length === 0) {
|
|
286
|
+
pushError(
|
|
287
|
+
context.errors,
|
|
288
|
+
context.pluginId,
|
|
289
|
+
`Plugin '${context.pluginName}' has no valid rules after validation.`,
|
|
290
|
+
{ pluginName: context.pluginName, code: 'plugin-rules-empty' },
|
|
291
|
+
)
|
|
292
|
+
return undefined
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return normalizedRules
|
|
296
|
+
}
|