@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.
Files changed (198) hide show
  1. package/.gga +50 -0
  2. package/.github/actions/drift-review/README.md +62 -0
  3. package/.github/actions/drift-review/action.yml +148 -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/publish-vscode.yml +1 -3
  7. package/.github/workflows/publish.yml +8 -0
  8. package/.github/workflows/quality.yml +15 -0
  9. package/.github/workflows/reusable-quality-checks.yml +95 -0
  10. package/.github/workflows/review-pr.yml +33 -41
  11. package/AGENTS.md +75 -251
  12. package/CHANGELOG.md +41 -0
  13. package/README.md +177 -43
  14. package/benchmarks/fixtures/critical/drift.config.ts +21 -0
  15. package/benchmarks/fixtures/critical/src/app/user-service.ts +30 -0
  16. package/benchmarks/fixtures/critical/src/domain/entities.ts +19 -0
  17. package/benchmarks/fixtures/critical/src/domain/policies.ts +22 -0
  18. package/benchmarks/fixtures/critical/src/index.ts +10 -0
  19. package/benchmarks/fixtures/critical/src/infra/memory-user-repo.ts +14 -0
  20. package/benchmarks/perf-budget.v1.json +27 -0
  21. package/dist/benchmark.d.ts +1 -1
  22. package/dist/benchmark.js +83 -52
  23. package/dist/cli.js +243 -8
  24. package/dist/config.js +16 -2
  25. package/dist/diff.js +42 -50
  26. package/dist/doctor.d.ts +26 -0
  27. package/dist/doctor.js +140 -0
  28. package/dist/format.d.ts +17 -0
  29. package/dist/format.js +45 -0
  30. package/dist/guard-baseline.d.ts +12 -0
  31. package/dist/guard-baseline.js +57 -0
  32. package/dist/guard-metrics.d.ts +6 -0
  33. package/dist/guard-metrics.js +39 -0
  34. package/dist/guard-types.d.ts +58 -0
  35. package/dist/guard-types.js +2 -0
  36. package/dist/guard.d.ts +16 -0
  37. package/dist/guard.js +178 -0
  38. package/dist/index.d.ts +10 -3
  39. package/dist/index.js +4 -1
  40. package/dist/init.d.ts +15 -0
  41. package/dist/init.js +273 -0
  42. package/dist/map-cycles.d.ts +2 -0
  43. package/dist/map-cycles.js +34 -0
  44. package/dist/map-svg.d.ts +19 -0
  45. package/dist/map-svg.js +97 -0
  46. package/dist/map.js +78 -138
  47. package/dist/metrics.js +70 -55
  48. package/dist/output-metadata.d.ts +15 -0
  49. package/dist/output-metadata.js +19 -0
  50. package/dist/plugins-capabilities.d.ts +4 -0
  51. package/dist/plugins-capabilities.js +21 -0
  52. package/dist/plugins-messages.d.ts +10 -0
  53. package/dist/plugins-messages.js +16 -0
  54. package/dist/plugins-rules.d.ts +9 -0
  55. package/dist/plugins-rules.js +137 -0
  56. package/dist/plugins.d.ts +1 -1
  57. package/dist/plugins.js +45 -142
  58. package/dist/reporter-constants.d.ts +16 -0
  59. package/dist/reporter-constants.js +39 -0
  60. package/dist/reporter.d.ts +3 -3
  61. package/dist/reporter.js +35 -55
  62. package/dist/review.d.ts +2 -1
  63. package/dist/review.js +2 -1
  64. package/dist/rules/phase3-configurable.js +23 -15
  65. package/dist/saas/constants.d.ts +15 -0
  66. package/dist/saas/constants.js +48 -0
  67. package/dist/saas/dashboard.d.ts +8 -0
  68. package/dist/saas/dashboard.js +132 -0
  69. package/dist/saas/errors.d.ts +19 -0
  70. package/dist/saas/errors.js +37 -0
  71. package/dist/saas/helpers.d.ts +21 -0
  72. package/dist/saas/helpers.js +110 -0
  73. package/dist/saas/ingest.d.ts +3 -0
  74. package/dist/saas/ingest.js +249 -0
  75. package/dist/saas/organization.d.ts +5 -0
  76. package/dist/saas/organization.js +82 -0
  77. package/dist/saas/plan-change.d.ts +10 -0
  78. package/dist/saas/plan-change.js +15 -0
  79. package/dist/saas/store.d.ts +21 -0
  80. package/dist/saas/store.js +159 -0
  81. package/dist/saas/types.d.ts +191 -0
  82. package/dist/saas/types.js +2 -0
  83. package/dist/saas.d.ts +8 -218
  84. package/dist/saas.js +7 -761
  85. package/dist/sarif.d.ts +74 -0
  86. package/dist/sarif.js +122 -0
  87. package/dist/trust-advanced.d.ts +14 -0
  88. package/dist/trust-advanced.js +65 -0
  89. package/dist/trust-kpi-fs.d.ts +3 -0
  90. package/dist/trust-kpi-fs.js +141 -0
  91. package/dist/trust-kpi-parse.d.ts +7 -0
  92. package/dist/trust-kpi-parse.js +186 -0
  93. package/dist/trust-kpi-types.d.ts +16 -0
  94. package/dist/trust-kpi-types.js +2 -0
  95. package/dist/trust-kpi.d.ts +1 -3
  96. package/dist/trust-kpi.js +6 -266
  97. package/dist/trust-policy.d.ts +32 -0
  98. package/dist/trust-policy.js +160 -0
  99. package/dist/trust-render.d.ts +9 -0
  100. package/dist/trust-render.js +54 -0
  101. package/dist/trust-scoring.d.ts +9 -0
  102. package/dist/trust-scoring.js +208 -0
  103. package/dist/trust.d.ts +5 -32
  104. package/dist/trust.js +29 -432
  105. package/dist/types/app.d.ts +30 -0
  106. package/dist/types/app.js +2 -0
  107. package/dist/types/config.d.ts +25 -0
  108. package/dist/types/config.js +2 -0
  109. package/dist/types/core.d.ts +100 -0
  110. package/dist/types/core.js +2 -0
  111. package/dist/types/diff.d.ts +55 -0
  112. package/dist/types/diff.js +2 -0
  113. package/dist/types/plugin.d.ts +41 -0
  114. package/dist/types/plugin.js +2 -0
  115. package/dist/types/trust.d.ts +120 -0
  116. package/dist/types/trust.js +2 -0
  117. package/dist/types.d.ts +8 -365
  118. package/docs/AGENTS.md +1 -1
  119. package/docs/release-notes-draft.md +40 -0
  120. package/docs/rules-catalog.md +49 -0
  121. package/docs/trust-core-release-checklist.md +37 -5
  122. package/package.json +11 -4
  123. package/packages/vscode-drift/src/code-actions.ts +1 -1
  124. package/schemas/drift-ai-output.v1.json +162 -0
  125. package/schemas/drift-doctor.v1.json +57 -0
  126. package/schemas/drift-guard.v1.json +298 -0
  127. package/schemas/drift-report.v1.json +151 -0
  128. package/schemas/drift-trust.v1.json +131 -0
  129. package/scripts/check-docs-drift.mjs +154 -0
  130. package/scripts/check-performance-budget.mjs +360 -0
  131. package/scripts/check-runtime-policy.mjs +66 -0
  132. package/scripts/smoke-repo.mjs +394 -0
  133. package/src/benchmark.ts +92 -53
  134. package/src/cli.ts +285 -13
  135. package/src/config.ts +19 -2
  136. package/src/diff.ts +57 -48
  137. package/src/doctor.ts +185 -0
  138. package/src/format.ts +81 -0
  139. package/src/guard-baseline.ts +74 -0
  140. package/src/guard-metrics.ts +52 -0
  141. package/src/guard-types.ts +66 -0
  142. package/src/guard.ts +248 -0
  143. package/src/index.ts +36 -0
  144. package/src/init.ts +298 -0
  145. package/src/map-cycles.ts +38 -0
  146. package/src/map-svg.ts +124 -0
  147. package/src/map.ts +111 -142
  148. package/src/metrics.ts +78 -59
  149. package/src/output-metadata.ts +32 -0
  150. package/src/plugins-capabilities.ts +36 -0
  151. package/src/plugins-messages.ts +35 -0
  152. package/src/plugins-rules.ts +296 -0
  153. package/src/plugins.ts +76 -283
  154. package/src/reporter-constants.ts +46 -0
  155. package/src/reporter.ts +64 -65
  156. package/src/review.ts +4 -2
  157. package/src/rules/phase3-configurable.ts +39 -26
  158. package/src/saas/constants.ts +56 -0
  159. package/src/saas/dashboard.ts +172 -0
  160. package/src/saas/errors.ts +45 -0
  161. package/src/saas/helpers.ts +140 -0
  162. package/src/saas/ingest.ts +278 -0
  163. package/src/saas/organization.ts +99 -0
  164. package/src/saas/plan-change.ts +19 -0
  165. package/src/saas/store.ts +172 -0
  166. package/src/saas/types.ts +216 -0
  167. package/src/saas.ts +49 -1031
  168. package/src/sarif.ts +232 -0
  169. package/src/trust-advanced.ts +99 -0
  170. package/src/trust-kpi-fs.ts +169 -0
  171. package/src/trust-kpi-parse.ts +219 -0
  172. package/src/trust-kpi-types.ts +19 -0
  173. package/src/trust-kpi.ts +8 -316
  174. package/src/trust-policy.ts +246 -0
  175. package/src/trust-render.ts +61 -0
  176. package/src/trust-scoring.ts +231 -0
  177. package/src/trust.ts +62 -576
  178. package/src/types/app.ts +30 -0
  179. package/src/types/config.ts +27 -0
  180. package/src/types/core.ts +105 -0
  181. package/src/types/diff.ts +61 -0
  182. package/src/types/plugin.ts +46 -0
  183. package/src/types/trust.ts +134 -0
  184. package/src/types.ts +79 -409
  185. package/tests/ci-quality-matrix.test.ts +37 -0
  186. package/tests/ci-smoke-gate.test.ts +26 -0
  187. package/tests/ci-version-alignment.test.ts +93 -0
  188. package/tests/cli-sarif.test.ts +92 -0
  189. package/tests/docs-drift-check.test.ts +115 -0
  190. package/tests/format.test.ts +157 -0
  191. package/tests/new-features.test.ts +11 -3
  192. package/tests/perf-budget-check.test.ts +146 -0
  193. package/tests/phase1-init-doctor-guard.test.ts +301 -0
  194. package/tests/runtime-policy-alignment.test.ts +46 -0
  195. package/tests/sarif.test.ts +160 -0
  196. package/tests/trust-kpi.test.ts +31 -4
  197. package/tests/trust.test.ts +18 -0
  198. package/vitest.config.ts +2 -0
package/dist/plugins.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { PluginLoadError, PluginLoadWarning, LoadedPlugin } from './types.js';
1
+ import type { LoadedPlugin, PluginLoadError, PluginLoadWarning } from './types.js';
2
2
  export declare function loadPlugins(projectRoot: string, pluginIds: string[] | undefined): {
3
3
  plugins: LoadedPlugin[];
4
4
  errors: PluginLoadError[];
package/dist/plugins.js CHANGED
@@ -1,171 +1,72 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { isAbsolute, resolve } from 'node:path';
3
3
  import { createRequire } from 'node:module';
4
+ import { normalizeRules } from './plugins-rules.js';
5
+ import { validateCapabilities } from './plugins-capabilities.js';
6
+ import { pushError, pushWarning } from './plugins-messages.js';
4
7
  const require = createRequire(import.meta.url);
5
- const VALID_SEVERITIES = ['error', 'warning', 'info'];
6
8
  const SUPPORTED_PLUGIN_API_VERSION = 1;
7
- const RULE_ID_REQUIRED = /^[a-z][a-z0-9]*(?:[-_/][a-z0-9]+)*$/;
8
9
  function normalizePluginExport(mod) {
9
10
  if (mod && typeof mod === 'object' && 'default' in mod) {
10
11
  return mod.default ?? mod;
11
12
  }
12
13
  return mod;
13
14
  }
14
- function pushError(errors, pluginId, message, options) {
15
- errors.push({
16
- pluginId,
17
- pluginName: options?.pluginName,
18
- ruleId: options?.ruleId,
19
- code: options?.code,
20
- message,
21
- });
22
- }
23
- function pushWarning(warnings, pluginId, message, options) {
24
- warnings.push({
25
- pluginId,
26
- pluginName: options?.pluginName,
27
- ruleId: options?.ruleId,
28
- code: options?.code,
29
- message,
30
- });
31
- }
32
- function normalizeRule(pluginId, pluginName, rawRule, ruleIndex, options, errors, warnings) {
33
- const rawRuleId = typeof rawRule.id === 'string'
34
- ? rawRule.id.trim()
35
- : typeof rawRule.name === 'string'
36
- ? rawRule.name.trim()
37
- : '';
38
- const ruleLabel = rawRuleId || `rule#${ruleIndex + 1}`;
39
- if (!rawRuleId) {
40
- pushError(errors, pluginId, `Invalid rule at index ${ruleIndex}. Expected 'id' or 'name' as a non-empty string.`, { pluginName, code: 'plugin-rule-id-missing' });
41
- return undefined;
42
- }
43
- if (typeof rawRule.detect !== 'function') {
44
- pushError(errors, pluginId, `Rule '${rawRuleId}' is invalid. Expected 'detect(file, context)' function.`, { pluginName, ruleId: rawRuleId, code: 'plugin-rule-detect-invalid' });
45
- return undefined;
46
- }
47
- if (rawRule.detect.length > 2) {
48
- pushWarning(warnings, pluginId, `Rule '${rawRuleId}' detect() declares ${rawRule.detect.length} parameters. Expected 1-2 parameters (file, context).`, { pluginName, ruleId: rawRuleId, code: 'plugin-rule-detect-arity' });
49
- }
50
- if (!RULE_ID_REQUIRED.test(rawRuleId)) {
51
- if (options.strictRuleId) {
52
- pushError(errors, pluginId, `Rule id '${ruleLabel}' is invalid. Use lowercase letters, numbers, and separators (-, _, /), starting with a letter.`, { pluginName, ruleId: rawRuleId, code: 'plugin-rule-id-invalid' });
53
- return undefined;
54
- }
55
- pushWarning(warnings, pluginId, `Rule id '${ruleLabel}' uses a legacy format. For forward compatibility, migrate to lowercase kebab-case and set apiVersion: ${SUPPORTED_PLUGIN_API_VERSION}.`, { pluginName, ruleId: rawRuleId, code: 'plugin-rule-id-format-legacy' });
56
- }
57
- let severity;
58
- if (rawRule.severity !== undefined) {
59
- if (typeof rawRule.severity === 'string' && VALID_SEVERITIES.includes(rawRule.severity)) {
60
- severity = rawRule.severity;
61
- }
62
- else {
63
- pushError(errors, pluginId, `Rule '${rawRuleId}' has invalid severity '${String(rawRule.severity)}'. Allowed: error, warning, info.`, { pluginName, ruleId: rawRuleId, code: 'plugin-rule-severity-invalid' });
64
- }
65
- }
66
- let weight;
67
- if (rawRule.weight !== undefined) {
68
- if (typeof rawRule.weight === 'number' && Number.isFinite(rawRule.weight) && rawRule.weight >= 0 && rawRule.weight <= 100) {
69
- weight = rawRule.weight;
70
- }
71
- else {
72
- pushError(errors, pluginId, `Rule '${rawRuleId}' has invalid weight '${String(rawRule.weight)}'. Expected a finite number between 0 and 100.`, { pluginName, ruleId: rawRuleId, code: 'plugin-rule-weight-invalid' });
73
- }
15
+ function ensureObjectCandidate(pluginId, candidate, errors) {
16
+ if (candidate && typeof candidate === 'object') {
17
+ return candidate;
74
18
  }
75
- let fix;
76
- if (rawRule.fix !== undefined) {
77
- if (typeof rawRule.fix === 'function') {
78
- fix = rawRule.fix;
79
- if (rawRule.fix.length > 3) {
80
- pushWarning(warnings, pluginId, `Rule '${rawRuleId}' fix() declares ${rawRule.fix.length} parameters. Expected up to 3 (issue, file, context).`, { pluginName, ruleId: rawRuleId, code: 'plugin-rule-fix-arity' });
81
- }
82
- }
83
- else {
84
- pushError(errors, pluginId, `Rule '${rawRuleId}' has invalid fix. Expected a function when provided.`, { pluginName, ruleId: rawRuleId, code: 'plugin-rule-fix-invalid' });
85
- }
86
- }
87
- return {
88
- id: rawRuleId,
89
- name: rawRuleId,
90
- detect: rawRule.detect,
91
- severity,
92
- weight,
93
- fix,
94
- };
19
+ pushError(errors, pluginId, `Invalid plugin contract in '${pluginId}'. Expected an object export with shape { name, rules[] }`, { code: 'plugin-shape-invalid' });
20
+ return undefined;
95
21
  }
96
- function validatePluginContract(pluginId, candidate) {
97
- const errors = [];
98
- const warnings = [];
99
- if (!candidate || typeof candidate !== 'object') {
100
- pushError(errors, pluginId, `Invalid plugin contract in '${pluginId}'. Expected an object export with shape { name, rules[] }`, { code: 'plugin-shape-invalid' });
101
- return { errors, warnings };
102
- }
103
- const plugin = candidate;
22
+ function ensurePluginName(pluginId, plugin, errors) {
104
23
  const pluginName = typeof plugin.name === 'string' ? plugin.name.trim() : '';
105
- if (!pluginName) {
106
- pushError(errors, pluginId, `Invalid plugin contract in '${pluginId}'. Expected 'name' as a non-empty string.`, { code: 'plugin-name-missing' });
107
- return { errors, warnings };
108
- }
24
+ if (pluginName)
25
+ return pluginName;
26
+ pushError(errors, pluginId, `Invalid plugin contract in '${pluginId}'. Expected 'name' as a non-empty string.`, { code: 'plugin-name-missing' });
27
+ return undefined;
28
+ }
29
+ function validateApiVersion(plugin, context) {
30
+ const { pluginId, pluginName, errors, warnings } = context;
109
31
  const hasExplicitApiVersion = plugin.apiVersion !== undefined;
110
32
  const isLegacyPlugin = !hasExplicitApiVersion;
111
33
  if (isLegacyPlugin) {
112
34
  pushWarning(warnings, pluginId, `Plugin '${pluginName}' does not declare 'apiVersion'. Assuming ${SUPPORTED_PLUGIN_API_VERSION} for backward compatibility; please add apiVersion: ${SUPPORTED_PLUGIN_API_VERSION}.`, { pluginName, code: 'plugin-api-version-implicit' });
35
+ return { hasExplicitApiVersion, isLegacyPlugin, isSupported: true };
113
36
  }
114
- else if (typeof plugin.apiVersion !== 'number' || !Number.isInteger(plugin.apiVersion) || plugin.apiVersion <= 0) {
37
+ if (typeof plugin.apiVersion !== 'number' || !Number.isInteger(plugin.apiVersion) || plugin.apiVersion <= 0) {
115
38
  pushError(errors, pluginId, `Plugin '${pluginName}' has invalid apiVersion '${String(plugin.apiVersion)}'. Expected a positive integer (for example: ${SUPPORTED_PLUGIN_API_VERSION}).`, { pluginName, code: 'plugin-api-version-invalid' });
116
- return { errors, warnings };
39
+ return { hasExplicitApiVersion, isLegacyPlugin, isSupported: false };
117
40
  }
118
- else if (plugin.apiVersion !== SUPPORTED_PLUGIN_API_VERSION) {
41
+ if (plugin.apiVersion !== SUPPORTED_PLUGIN_API_VERSION) {
119
42
  pushError(errors, pluginId, `Plugin '${pluginName}' targets apiVersion ${plugin.apiVersion}, but this drift build supports apiVersion ${SUPPORTED_PLUGIN_API_VERSION}.`, { pluginName, code: 'plugin-api-version-unsupported' });
120
- return { errors, warnings };
43
+ return { hasExplicitApiVersion, isLegacyPlugin, isSupported: false };
121
44
  }
122
- let capabilities;
123
- if (plugin.capabilities !== undefined) {
124
- if (!plugin.capabilities || typeof plugin.capabilities !== 'object' || Array.isArray(plugin.capabilities)) {
125
- pushError(errors, pluginId, `Plugin '${pluginName}' has invalid capabilities metadata. Expected an object map like { "fixes": true } when provided.`, { pluginName, code: 'plugin-capabilities-invalid' });
126
- return { errors, warnings };
127
- }
128
- const entries = Object.entries(plugin.capabilities);
129
- for (const [capabilityKey, capabilityValue] of entries) {
130
- const capabilityType = typeof capabilityValue;
131
- if (capabilityType !== 'string' && capabilityType !== 'number' && capabilityType !== 'boolean') {
132
- pushError(errors, pluginId, `Plugin '${pluginName}' capability '${capabilityKey}' has invalid value type '${capabilityType}'. Allowed: string | number | boolean.`, { pluginName, code: 'plugin-capabilities-value-invalid' });
133
- }
134
- }
135
- if (errors.length > 0) {
136
- return { errors, warnings };
137
- }
138
- capabilities = plugin.capabilities;
139
- }
140
- if (!Array.isArray(plugin.rules)) {
141
- pushError(errors, pluginId, `Invalid plugin '${pluginName}'. Expected 'rules' to be an array.`, { pluginName, code: 'plugin-rules-not-array' });
45
+ return { hasExplicitApiVersion, isLegacyPlugin, isSupported: true };
46
+ }
47
+ function validatePluginContractData(pluginId, candidate) {
48
+ const errors = [];
49
+ const warnings = [];
50
+ const plugin = ensureObjectCandidate(pluginId, candidate, errors);
51
+ if (!plugin)
142
52
  return { errors, warnings };
143
- }
144
- const normalizedRules = [];
145
- const seenRuleIds = new Set();
146
- for (const [ruleIndex, rawRule] of plugin.rules.entries()) {
147
- if (!rawRule || typeof rawRule !== 'object') {
148
- pushError(errors, pluginId, `Invalid rule at index ${ruleIndex} in plugin '${pluginName}'. Expected an object.`, { pluginName, code: 'plugin-rule-shape-invalid' });
149
- continue;
150
- }
151
- const normalized = normalizeRule(pluginId, pluginName, rawRule, ruleIndex, { strictRuleId: !isLegacyPlugin }, errors, warnings);
152
- if (normalized) {
153
- if (seenRuleIds.has(normalized.id ?? normalized.name)) {
154
- pushError(errors, pluginId, `Plugin '${pluginName}' defines duplicate rule id '${normalized.id ?? normalized.name}'. Rule ids must be unique within a plugin.`, { pluginName, ruleId: normalized.id ?? normalized.name, code: 'plugin-rule-id-duplicate' });
155
- continue;
156
- }
157
- seenRuleIds.add(normalized.id ?? normalized.name);
158
- normalizedRules.push(normalized);
159
- }
160
- }
161
- if (normalizedRules.length === 0) {
162
- pushError(errors, pluginId, `Plugin '${pluginName}' has no valid rules after validation.`, { pluginName, code: 'plugin-rules-empty' });
53
+ const pluginName = ensurePluginName(pluginId, plugin, errors);
54
+ if (!pluginName)
55
+ return { errors, warnings };
56
+ const context = { pluginId, pluginName, errors, warnings };
57
+ const apiVersion = validateApiVersion(plugin, context);
58
+ if (!apiVersion.isSupported)
59
+ return { errors, warnings };
60
+ const capabilities = validateCapabilities(plugin.capabilities, context);
61
+ if (errors.length > 0)
62
+ return { errors, warnings };
63
+ const normalizedRules = normalizeRules(plugin.rules, apiVersion.isLegacyPlugin, context);
64
+ if (!normalizedRules)
163
65
  return { errors, warnings };
164
- }
165
66
  return {
166
67
  plugin: {
167
68
  name: pluginName,
168
- apiVersion: hasExplicitApiVersion ? plugin.apiVersion : SUPPORTED_PLUGIN_API_VERSION,
69
+ apiVersion: apiVersion.hasExplicitApiVersion ? plugin.apiVersion : SUPPORTED_PLUGIN_API_VERSION,
169
70
  capabilities,
170
71
  rules: normalizedRules,
171
72
  },
@@ -173,6 +74,9 @@ function validatePluginContract(pluginId, candidate) {
173
74
  warnings,
174
75
  };
175
76
  }
77
+ function validatePluginContract(pluginId, candidate) {
78
+ return validatePluginContractData(pluginId, candidate);
79
+ }
176
80
  function resolvePluginSpecifier(projectRoot, pluginId) {
177
81
  if (pluginId.startsWith('.') || pluginId.startsWith('/')) {
178
82
  const abs = isAbsolute(pluginId) ? pluginId : resolve(projectRoot, pluginId);
@@ -205,9 +109,8 @@ export function loadPlugins(projectRoot, pluginIds) {
205
109
  const validation = validatePluginContract(pluginId, normalized);
206
110
  errors.push(...validation.errors);
207
111
  warnings.push(...validation.warnings);
208
- if (!validation.plugin) {
112
+ if (!validation.plugin)
209
113
  continue;
210
- }
211
114
  loaded.push({ id: pluginId, plugin: validation.plugin });
212
115
  }
213
116
  catch (error) {
@@ -0,0 +1,16 @@
1
+ import type { DriftIssue } from './types.js';
2
+ export declare const FIX_SUGGESTIONS: Record<string, string>;
3
+ export declare const RULE_EFFORT: Record<string, 'low' | 'medium' | 'high'>;
4
+ export declare const SEVERITY_ORDER: Record<string, number>;
5
+ export declare const EFFORT_ORDER: Record<string, number>;
6
+ export declare const AI_SIGNAL_RULES: Set<string>;
7
+ export declare const AI_CODE_SMELL_BOOST = 20;
8
+ export declare const AI_TRIGGER_LIMIT = 4;
9
+ export declare const AI_LIKELIHOOD_THRESHOLD = 35;
10
+ export declare const AI_SMELL_SCORE_MULTIPLIER = 15;
11
+ export declare const AI_SUSPECTED_LIMIT = 10;
12
+ export type DriftIssueWithFile = {
13
+ file: string;
14
+ issue: DriftIssue;
15
+ };
16
+ //# sourceMappingURL=reporter-constants.d.ts.map
@@ -0,0 +1,39 @@
1
+ export const FIX_SUGGESTIONS = {
2
+ 'large-file': 'Consider splitting this file into smaller modules with single responsibility',
3
+ 'large-function': 'Extract logic into smaller functions with descriptive names',
4
+ 'debug-leftover': 'Remove this console.log or replace with proper logging library',
5
+ 'dead-code': 'Remove unused import to keep code clean',
6
+ 'duplicate-function-name': 'Consolidate with existing function or rename to clarify different behavior',
7
+ 'any-abuse': "Replace 'any' with proper type definition",
8
+ 'catch-swallow': 'Add error handling or logging in catch block',
9
+ 'no-return-type': 'Add explicit return type for better type safety',
10
+ };
11
+ export const RULE_EFFORT = {
12
+ 'debug-leftover': 'low',
13
+ 'dead-code': 'low',
14
+ 'no-return-type': 'low',
15
+ 'any-abuse': 'medium',
16
+ 'catch-swallow': 'medium',
17
+ 'large-file': 'high',
18
+ 'large-function': 'high',
19
+ 'duplicate-function-name': 'high',
20
+ };
21
+ export const SEVERITY_ORDER = { error: 0, warning: 1, info: 2 };
22
+ export const EFFORT_ORDER = { low: 0, medium: 1, high: 2 };
23
+ export const AI_SIGNAL_RULES = new Set([
24
+ 'over-commented',
25
+ 'hardcoded-config',
26
+ 'inconsistent-error-handling',
27
+ 'unnecessary-abstraction',
28
+ 'naming-inconsistency',
29
+ 'comment-contradiction',
30
+ 'promise-style-mix',
31
+ 'any-abuse',
32
+ 'ai-code-smell',
33
+ ]);
34
+ export const AI_CODE_SMELL_BOOST = 20;
35
+ export const AI_TRIGGER_LIMIT = 4;
36
+ export const AI_LIKELIHOOD_THRESHOLD = 35;
37
+ export const AI_SMELL_SCORE_MULTIPLIER = 15;
38
+ export const AI_SUSPECTED_LIMIT = 10;
39
+ //# sourceMappingURL=reporter-constants.js.map
@@ -1,5 +1,5 @@
1
- import type { FileReport, DriftReport, AIOutput } from './types.js';
2
- export declare function buildReport(targetPath: string, files: FileReport[]): DriftReport;
1
+ import type { FileReport, DriftReport, AIOutputJson, DriftReportJson } from './types.js';
2
+ export declare function buildReport(targetPath: string, files: FileReport[]): DriftReportJson;
3
3
  export declare function formatMarkdown(report: DriftReport): string;
4
- export declare function formatAIOutput(report: DriftReport): AIOutput;
4
+ export declare function formatAIOutput(report: DriftReport): AIOutputJson;
5
5
  //# sourceMappingURL=reporter.d.ts.map
package/dist/reporter.js CHANGED
@@ -1,61 +1,34 @@
1
1
  import { scoreToGradeText, severityIcon } from './utils.js';
2
2
  import { computeRepoQuality, computeMaintenanceRisk } from './metrics.js';
3
- const FIX_SUGGESTIONS = {
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
- const RULE_EFFORT = {
14
- 'debug-leftover': 'low',
15
- 'dead-code': 'low',
16
- 'no-return-type': 'low',
17
- 'any-abuse': 'medium',
18
- 'catch-swallow': 'medium',
19
- 'large-file': 'high',
20
- 'large-function': 'high',
21
- 'duplicate-function-name': 'high',
22
- };
23
- const SEVERITY_ORDER = { error: 0, warning: 1, info: 2 };
24
- const EFFORT_ORDER = { low: 0, medium: 1, high: 2 };
25
- const AI_SIGNAL_RULES = new Set([
26
- 'over-commented',
27
- 'hardcoded-config',
28
- 'inconsistent-error-handling',
29
- 'unnecessary-abstraction',
30
- 'naming-inconsistency',
31
- 'comment-contradiction',
32
- 'promise-style-mix',
33
- 'any-abuse',
34
- 'ai-code-smell',
35
- ]);
36
- export function buildReport(targetPath, files) {
37
- const allIssues = files.flatMap((f) => f.issues);
3
+ import { OUTPUT_SCHEMA, withOutputMetadata } from './output-metadata.js';
4
+ import { AI_CODE_SMELL_BOOST, AI_LIKELIHOOD_THRESHOLD, AI_SIGNAL_RULES, AI_SMELL_SCORE_MULTIPLIER, AI_SUSPECTED_LIMIT, AI_TRIGGER_LIMIT, EFFORT_ORDER, FIX_SUGGESTIONS, RULE_EFFORT, SEVERITY_ORDER, } from './reporter-constants.js';
5
+ function summarizeIssues(allIssues) {
38
6
  const byRule = {};
39
7
  for (const issue of allIssues) {
40
8
  byRule[issue.rule] = (byRule[issue.rule] ?? 0) + 1;
41
9
  }
42
- const totalScore = files.length > 0
43
- ? Math.round(files.reduce((sum, f) => sum + f.score, 0) / files.length)
44
- : 0;
45
- const sortedFiles = files.filter((f) => f.issues.length > 0).sort((a, b) => b.score - a.score);
46
- const baseReport = {
10
+ return {
11
+ errors: allIssues.filter((issue) => issue.severity === 'error').length,
12
+ warnings: allIssues.filter((issue) => issue.severity === 'warning').length,
13
+ infos: allIssues.filter((issue) => issue.severity === 'info').length,
14
+ byRule,
15
+ };
16
+ }
17
+ function calculateTotalScore(files) {
18
+ if (files.length === 0)
19
+ return 0;
20
+ return Math.round(files.reduce((sum, file) => sum + file.score, 0) / files.length);
21
+ }
22
+ function baseReportDefaults(summary, targetPath, files) {
23
+ const filesWithIssues = files.filter((file) => file.issues.length > 0).sort((a, b) => b.score - a.score);
24
+ const report = {
47
25
  scannedAt: new Date().toISOString(),
48
26
  targetPath,
49
- files: sortedFiles,
50
- totalIssues: allIssues.length,
51
- totalScore,
27
+ files: filesWithIssues,
28
+ totalIssues: files.flatMap((file) => file.issues).length,
29
+ totalScore: calculateTotalScore(files),
52
30
  totalFiles: files.length,
53
- summary: {
54
- errors: allIssues.filter((i) => i.severity === 'error').length,
55
- warnings: allIssues.filter((i) => i.severity === 'warning').length,
56
- infos: allIssues.filter((i) => i.severity === 'info').length,
57
- byRule,
58
- },
31
+ summary,
59
32
  quality: {
60
33
  overall: 100,
61
34
  dimensions: {
@@ -76,6 +49,12 @@ export function buildReport(targetPath, files) {
76
49
  },
77
50
  },
78
51
  };
52
+ return withOutputMetadata(report, OUTPUT_SCHEMA.report);
53
+ }
54
+ export function buildReport(targetPath, files) {
55
+ const allIssues = files.flatMap((f) => f.issues);
56
+ const summary = summarizeIssues(allIssues);
57
+ const baseReport = baseReportDefaults(summary, targetPath, files);
79
58
  baseReport.quality = computeRepoQuality(targetPath, files);
80
59
  baseReport.maintenanceRisk = computeMaintenanceRisk(baseReport);
81
60
  return baseReport;
@@ -191,12 +170,12 @@ function fileAILikelihood(fileIssues) {
191
170
  triggerCounts.set(issue.rule, (triggerCounts.get(issue.rule) ?? 0) + 1);
192
171
  }
193
172
  const triggerTotal = [...triggerCounts.values()].reduce((sum, count) => sum + count, 0);
194
- const smellBoost = fileIssues.some((issue) => issue.rule === 'ai-code-smell') ? 20 : 0;
173
+ const smellBoost = fileIssues.some((issue) => issue.rule === 'ai-code-smell') ? AI_CODE_SMELL_BOOST : 0;
195
174
  const ratioScore = Math.round((triggerTotal / Math.max(fileIssues.length, 1)) * 100);
196
175
  const score = Math.max(0, Math.min(100, ratioScore + smellBoost));
197
176
  const triggers = [...triggerCounts.entries()]
198
177
  .sort((a, b) => b[1] - a[1])
199
- .slice(0, 4)
178
+ .slice(0, AI_TRIGGER_LIMIT)
200
179
  .map(([rule]) => rule);
201
180
  return { score, triggers };
202
181
  }
@@ -210,16 +189,16 @@ function computeAILikelihood(report) {
210
189
  triggers: likelihood.triggers,
211
190
  };
212
191
  })
213
- .filter((entry) => entry.ai_likelihood >= 35)
192
+ .filter((entry) => entry.ai_likelihood >= AI_LIKELIHOOD_THRESHOLD)
214
193
  .sort((a, b) => b.ai_likelihood - a.ai_likelihood);
215
194
  const overall = suspected.length === 0
216
195
  ? 0
217
196
  : Math.round(suspected.reduce((sum, entry) => sum + entry.ai_likelihood, 0) / suspected.length);
218
197
  const smellCount = report.files.flatMap((file) => file.issues).filter((issue) => issue.rule === 'ai-code-smell').length;
219
- const smellScore = Math.min(100, smellCount * 15);
198
+ const smellScore = Math.min(100, smellCount * AI_SMELL_SCORE_MULTIPLIER);
220
199
  return {
221
200
  overall,
222
- files: suspected.slice(0, 10),
201
+ files: suspected.slice(0, AI_SUSPECTED_LIMIT),
223
202
  smellScore,
224
203
  };
225
204
  }
@@ -230,7 +209,7 @@ export function formatAIOutput(report) {
230
209
  const rulesDetected = [...new Set(allIssues.map((i) => i.issue.rule))];
231
210
  const grade = scoreToGradeText(report.totalScore);
232
211
  const aiLikelihood = computeAILikelihood(report);
233
- return {
212
+ const output = {
234
213
  summary: {
235
214
  score: report.totalScore,
236
215
  grade: grade.label.toUpperCase(),
@@ -251,5 +230,6 @@ export function formatAIOutput(report) {
251
230
  recommended_action: buildRecommendedAction(priorityOrder),
252
231
  },
253
232
  };
233
+ return withOutputMetadata(output, OUTPUT_SCHEMA.ai);
254
234
  }
255
235
  //# sourceMappingURL=reporter.js.map
package/dist/review.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { DriftDiff } from './types.js';
2
- export interface DriftReview {
2
+ interface DriftReview {
3
3
  baseRef: string;
4
4
  scannedAt: string;
5
5
  totalDelta: number;
@@ -12,4 +12,5 @@ export interface DriftReview {
12
12
  }
13
13
  export declare function formatReviewMarkdown(review: DriftReview): string;
14
14
  export declare function generateReview(projectPath: string, baseRef: string): Promise<DriftReview>;
15
+ export {};
15
16
  //# sourceMappingURL=review.d.ts.map
package/dist/review.js CHANGED
@@ -4,10 +4,11 @@ import { loadConfig } from './config.js';
4
4
  import { buildReport } from './reporter.js';
5
5
  import { cleanupTempDir, extractFilesAtRef } from './git.js';
6
6
  import { computeDiff } from './diff.js';
7
+ const REVIEW_TOP_FILES_LIMIT = 8;
7
8
  export function formatReviewMarkdown(review) {
8
9
  const trendIcon = review.status === 'regressed' ? '⚠️' : review.status === 'improved' ? '✅' : 'ℹ️';
9
10
  const topFiles = review.diff.files
10
- .slice(0, 8)
11
+ .slice(0, REVIEW_TOP_FILES_LIMIT)
11
12
  .map((file) => {
12
13
  const sign = file.scoreDelta > 0 ? '+' : '';
13
14
  return `- \`${file.path}\`: ${file.scoreBefore} -> ${file.scoreAfter} (${sign}${file.scoreDelta}), +${file.newIssues.length} new / -${file.resolvedIssues.length} resolved`;
@@ -19,11 +19,13 @@ const HTTP_IMPORT_PATTERNS = [
19
19
  ];
20
20
  function isControllerFile(filePath) {
21
21
  const normalized = filePath.replace(/\\/g, '/').toLowerCase();
22
- return normalized.includes('/controller/') || normalized.includes('/controllers/') || normalized.endsWith('controller.ts') || normalized.endsWith('controller.js');
22
+ const segments = normalized.split('/');
23
+ return segments.includes('controller') || segments.includes('controllers') || normalized.endsWith('controller.ts') || normalized.endsWith('controller.js');
23
24
  }
24
25
  function isServiceFile(filePath) {
25
26
  const normalized = filePath.replace(/\\/g, '/').toLowerCase();
26
- return normalized.includes('/service/') || normalized.includes('/services/') || normalized.endsWith('service.ts') || normalized.endsWith('service.js');
27
+ const segments = normalized.split('/');
28
+ return segments.includes('service') || segments.includes('services') || normalized.endsWith('service.ts') || normalized.endsWith('service.js');
27
29
  }
28
30
  function createIssue(rule, message, line, snippet) {
29
31
  return {
@@ -74,24 +76,30 @@ export function detectMaxFunctionLines(file, config) {
74
76
  if (!maxLines || maxLines <= 0)
75
77
  return [];
76
78
  const issues = [];
79
+ collectFunctionLineIssues(file, maxLines, issues);
80
+ collectMethodLineIssues(file, maxLines, issues);
81
+ return issues;
82
+ }
83
+ function countBodyLines(body) {
84
+ if (!body)
85
+ return 0;
86
+ return body.getEndLineNumber() - body.getStartLineNumber() - 1;
87
+ }
88
+ function collectFunctionLineIssues(file, maxLines, issues) {
77
89
  for (const fn of file.getFunctions()) {
78
- const body = fn.getBody();
79
- if (!body)
90
+ const lines = countBodyLines(fn.getBody());
91
+ if (lines <= maxLines)
80
92
  continue;
81
- const lines = body.getEndLineNumber() - body.getStartLineNumber() - 1;
82
- if (lines > maxLines) {
83
- issues.push(createIssue('max-function-lines', `Function '${fn.getName() ?? '(anonymous)'}' has ${lines} lines (max: ${maxLines}).`, fn.getStartLineNumber(), fn.getName() ?? '(anonymous)'));
84
- }
93
+ const functionName = fn.getName() ?? '(anonymous)';
94
+ issues.push(createIssue('max-function-lines', `Function '${functionName}' has ${lines} lines (max: ${maxLines}).`, fn.getStartLineNumber(), functionName));
85
95
  }
96
+ }
97
+ function collectMethodLineIssues(file, maxLines, issues) {
86
98
  for (const method of file.getDescendantsOfKind(SyntaxKind.MethodDeclaration)) {
87
- const body = method.getBody();
88
- if (!body)
99
+ const lines = countBodyLines(method.getBody());
100
+ if (lines <= maxLines)
89
101
  continue;
90
- const lines = body.getEndLineNumber() - body.getStartLineNumber() - 1;
91
- if (lines > maxLines) {
92
- issues.push(createIssue('max-function-lines', `Method '${method.getName()}' has ${lines} lines (max: ${maxLines}).`, method.getStartLineNumber(), method.getName()));
93
- }
102
+ issues.push(createIssue('max-function-lines', `Method '${method.getName()}' has ${lines} lines (max: ${maxLines}).`, method.getStartLineNumber(), method.getName()));
94
103
  }
95
- return issues;
96
104
  }
97
105
  //# sourceMappingURL=phase3-configurable.js.map
@@ -0,0 +1,15 @@
1
+ import type { SaasOperation, SaasPlan, SaasPolicy, SaasRole } from './types.js';
2
+ export declare const STORE_VERSION = 3;
3
+ export declare const ACTIVE_WINDOW_DAYS = 30;
4
+ export declare const DEFAULT_ORGANIZATION_ID = "default-org";
5
+ export declare const DASHBOARD_REPO_LIMIT = 15;
6
+ export declare const DASHBOARD_BAR_UNIT = 8;
7
+ export declare const DASHBOARD_BAR_MIN_WIDTH = 8;
8
+ export declare const VALID_ROLES: SaasRole[];
9
+ export declare const VALID_PLANS: SaasPlan[];
10
+ export declare const ROLE_PRIORITY: Record<SaasRole, number>;
11
+ export declare const REQUIRED_ROLE_BY_OPERATION: Record<SaasOperation, SaasRole>;
12
+ export declare const DEFAULT_SAAS_POLICY: SaasPolicy;
13
+ export declare function daysAgo(days: number): number;
14
+ export declare function createRandomId(prefix: string): string;
15
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1,48 @@
1
+ export const STORE_VERSION = 3;
2
+ export const ACTIVE_WINDOW_DAYS = 30;
3
+ export const DEFAULT_ORGANIZATION_ID = 'default-org';
4
+ const HOURS_PER_DAY = 24;
5
+ const MINUTES_PER_HOUR = 60;
6
+ const SECONDS_PER_MINUTE = 60;
7
+ const MILLISECONDS_PER_SECOND = 1000;
8
+ const RANDOM_ID_RADIX = 16;
9
+ const RANDOM_ID_START = 2;
10
+ const RANDOM_ID_END = 10;
11
+ export const DASHBOARD_REPO_LIMIT = 15;
12
+ export const DASHBOARD_BAR_UNIT = 8;
13
+ export const DASHBOARD_BAR_MIN_WIDTH = 8;
14
+ export const VALID_ROLES = ['owner', 'member', 'viewer'];
15
+ export const VALID_PLANS = ['free', 'sponsor', 'team', 'business'];
16
+ export const ROLE_PRIORITY = {
17
+ viewer: 1,
18
+ member: 2,
19
+ owner: 3,
20
+ };
21
+ export const REQUIRED_ROLE_BY_OPERATION = {
22
+ 'snapshot:write': 'member',
23
+ 'snapshot:read': 'viewer',
24
+ 'summary:read': 'viewer',
25
+ 'billing:write': 'owner',
26
+ 'billing:read': 'viewer',
27
+ };
28
+ export const DEFAULT_SAAS_POLICY = {
29
+ freeUserThreshold: 7500,
30
+ maxRunsPerWorkspacePerMonth: 500,
31
+ maxReposPerWorkspace: 20,
32
+ retentionDays: 90,
33
+ strictActorEnforcement: false,
34
+ maxWorkspacesPerOrganizationByPlan: {
35
+ free: 20,
36
+ sponsor: 50,
37
+ team: 200,
38
+ business: 1000,
39
+ },
40
+ };
41
+ export function daysAgo(days) {
42
+ const now = Date.now();
43
+ return now - days * HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND;
44
+ }
45
+ export function createRandomId(prefix) {
46
+ return `${prefix}-${Math.random().toString(RANDOM_ID_RADIX).slice(RANDOM_ID_START, RANDOM_ID_END)}`;
47
+ }
48
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1,8 @@
1
+ import type { SaasPolicyOverrides, SaasQueryOptions, SaasSnapshot, SaasSummary } from './types.js';
2
+ export declare function listSaasSnapshots(options?: SaasQueryOptions): SaasSnapshot[];
3
+ export declare function getSaasSummary(options?: SaasQueryOptions): SaasSummary;
4
+ export declare function generateSaasDashboardHtml(options?: {
5
+ storeFile?: string;
6
+ policy?: SaasPolicyOverrides;
7
+ }): string;
8
+ //# sourceMappingURL=dashboard.d.ts.map