@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.
Files changed (168) hide show
  1. package/.gga +50 -0
  2. package/.github/actions/drift-review/README.md +60 -0
  3. package/.github/actions/drift-review/action.yml +131 -0
  4. package/.github/actions/drift-scan/README.md +28 -32
  5. package/.github/actions/drift-scan/action.yml +78 -14
  6. package/.github/workflows/review-pr.yml +34 -41
  7. package/AGENTS.md +75 -251
  8. package/CHANGELOG.md +28 -0
  9. package/README.md +148 -41
  10. package/dist/benchmark.d.ts +1 -1
  11. package/dist/benchmark.js +71 -52
  12. package/dist/cli.js +243 -8
  13. package/dist/config.js +16 -2
  14. package/dist/diff.js +42 -50
  15. package/dist/doctor.d.ts +5 -0
  16. package/dist/doctor.js +133 -0
  17. package/dist/format.d.ts +17 -0
  18. package/dist/format.js +45 -0
  19. package/dist/guard-types.d.ts +57 -0
  20. package/dist/guard-types.js +2 -0
  21. package/dist/guard.d.ts +14 -0
  22. package/dist/guard.js +239 -0
  23. package/dist/index.d.ts +10 -3
  24. package/dist/index.js +4 -1
  25. package/dist/init.d.ts +15 -0
  26. package/dist/init.js +273 -0
  27. package/dist/map-cycles.d.ts +2 -0
  28. package/dist/map-cycles.js +34 -0
  29. package/dist/map-svg.d.ts +19 -0
  30. package/dist/map-svg.js +97 -0
  31. package/dist/map.js +78 -138
  32. package/dist/metrics.js +70 -55
  33. package/dist/output-metadata.d.ts +13 -0
  34. package/dist/output-metadata.js +17 -0
  35. package/dist/plugins-capabilities.d.ts +4 -0
  36. package/dist/plugins-capabilities.js +21 -0
  37. package/dist/plugins-messages.d.ts +10 -0
  38. package/dist/plugins-messages.js +16 -0
  39. package/dist/plugins-rules.d.ts +9 -0
  40. package/dist/plugins-rules.js +137 -0
  41. package/dist/plugins.d.ts +1 -1
  42. package/dist/plugins.js +45 -142
  43. package/dist/reporter-constants.d.ts +16 -0
  44. package/dist/reporter-constants.js +39 -0
  45. package/dist/reporter.d.ts +3 -3
  46. package/dist/reporter.js +35 -55
  47. package/dist/review.d.ts +2 -1
  48. package/dist/review.js +2 -1
  49. package/dist/rules/phase3-configurable.js +23 -15
  50. package/dist/saas/constants.d.ts +15 -0
  51. package/dist/saas/constants.js +48 -0
  52. package/dist/saas/dashboard.d.ts +8 -0
  53. package/dist/saas/dashboard.js +132 -0
  54. package/dist/saas/errors.d.ts +19 -0
  55. package/dist/saas/errors.js +37 -0
  56. package/dist/saas/helpers.d.ts +21 -0
  57. package/dist/saas/helpers.js +110 -0
  58. package/dist/saas/ingest.d.ts +3 -0
  59. package/dist/saas/ingest.js +249 -0
  60. package/dist/saas/organization.d.ts +5 -0
  61. package/dist/saas/organization.js +82 -0
  62. package/dist/saas/plan-change.d.ts +10 -0
  63. package/dist/saas/plan-change.js +15 -0
  64. package/dist/saas/store.d.ts +21 -0
  65. package/dist/saas/store.js +159 -0
  66. package/dist/saas/types.d.ts +191 -0
  67. package/dist/saas/types.js +2 -0
  68. package/dist/saas.d.ts +8 -218
  69. package/dist/saas.js +7 -761
  70. package/dist/sarif.d.ts +74 -0
  71. package/dist/sarif.js +122 -0
  72. package/dist/trust-advanced.d.ts +14 -0
  73. package/dist/trust-advanced.js +65 -0
  74. package/dist/trust-kpi-fs.d.ts +3 -0
  75. package/dist/trust-kpi-fs.js +141 -0
  76. package/dist/trust-kpi-parse.d.ts +7 -0
  77. package/dist/trust-kpi-parse.js +186 -0
  78. package/dist/trust-kpi-types.d.ts +16 -0
  79. package/dist/trust-kpi-types.js +2 -0
  80. package/dist/trust-kpi.d.ts +1 -3
  81. package/dist/trust-kpi.js +6 -266
  82. package/dist/trust-policy.d.ts +32 -0
  83. package/dist/trust-policy.js +160 -0
  84. package/dist/trust-render.d.ts +9 -0
  85. package/dist/trust-render.js +54 -0
  86. package/dist/trust-scoring.d.ts +9 -0
  87. package/dist/trust-scoring.js +208 -0
  88. package/dist/trust.d.ts +4 -32
  89. package/dist/trust.js +29 -432
  90. package/dist/types/app.d.ts +30 -0
  91. package/dist/types/app.js +2 -0
  92. package/dist/types/config.d.ts +25 -0
  93. package/dist/types/config.js +2 -0
  94. package/dist/types/core.d.ts +100 -0
  95. package/dist/types/core.js +2 -0
  96. package/dist/types/diff.d.ts +55 -0
  97. package/dist/types/diff.js +2 -0
  98. package/dist/types/plugin.d.ts +41 -0
  99. package/dist/types/plugin.js +2 -0
  100. package/dist/types/trust.d.ts +120 -0
  101. package/dist/types/trust.js +2 -0
  102. package/dist/types.d.ts +8 -365
  103. package/docs/release-notes-draft.md +40 -0
  104. package/docs/rules-catalog.md +49 -0
  105. package/docs/trust-core-release-checklist.md +37 -5
  106. package/package.json +3 -2
  107. package/packages/vscode-drift/src/code-actions.ts +1 -1
  108. package/schemas/drift-ai-output.v1.json +162 -0
  109. package/schemas/drift-report.v1.json +151 -0
  110. package/schemas/drift-trust.v1.json +131 -0
  111. package/scripts/smoke-repo.mjs +394 -0
  112. package/src/benchmark.ts +75 -53
  113. package/src/cli.ts +285 -13
  114. package/src/config.ts +19 -2
  115. package/src/diff.ts +57 -48
  116. package/src/doctor.ts +173 -0
  117. package/src/format.ts +81 -0
  118. package/src/guard-types.ts +64 -0
  119. package/src/guard.ts +324 -0
  120. package/src/index.ts +35 -0
  121. package/src/init.ts +298 -0
  122. package/src/map-cycles.ts +38 -0
  123. package/src/map-svg.ts +124 -0
  124. package/src/map.ts +111 -142
  125. package/src/metrics.ts +78 -59
  126. package/src/output-metadata.ts +30 -0
  127. package/src/plugins-capabilities.ts +36 -0
  128. package/src/plugins-messages.ts +35 -0
  129. package/src/plugins-rules.ts +296 -0
  130. package/src/plugins.ts +76 -283
  131. package/src/reporter-constants.ts +46 -0
  132. package/src/reporter.ts +64 -65
  133. package/src/review.ts +4 -2
  134. package/src/rules/phase3-configurable.ts +39 -26
  135. package/src/saas/constants.ts +56 -0
  136. package/src/saas/dashboard.ts +172 -0
  137. package/src/saas/errors.ts +45 -0
  138. package/src/saas/helpers.ts +140 -0
  139. package/src/saas/ingest.ts +278 -0
  140. package/src/saas/organization.ts +99 -0
  141. package/src/saas/plan-change.ts +19 -0
  142. package/src/saas/store.ts +172 -0
  143. package/src/saas/types.ts +216 -0
  144. package/src/saas.ts +49 -1031
  145. package/src/sarif.ts +232 -0
  146. package/src/trust-advanced.ts +99 -0
  147. package/src/trust-kpi-fs.ts +169 -0
  148. package/src/trust-kpi-parse.ts +219 -0
  149. package/src/trust-kpi-types.ts +19 -0
  150. package/src/trust-kpi.ts +8 -316
  151. package/src/trust-policy.ts +246 -0
  152. package/src/trust-render.ts +61 -0
  153. package/src/trust-scoring.ts +231 -0
  154. package/src/trust.ts +62 -576
  155. package/src/types/app.ts +30 -0
  156. package/src/types/config.ts +27 -0
  157. package/src/types/core.ts +105 -0
  158. package/src/types/diff.ts +61 -0
  159. package/src/types/plugin.ts +46 -0
  160. package/src/types/trust.ts +134 -0
  161. package/src/types.ts +78 -409
  162. package/tests/cli-sarif.test.ts +92 -0
  163. package/tests/format.test.ts +157 -0
  164. package/tests/new-features.test.ts +10 -2
  165. package/tests/phase1-init-doctor-guard.test.ts +199 -0
  166. package/tests/sarif.test.ts +160 -0
  167. package/tests/trust-kpi.test.ts +31 -4
  168. 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
+ }