@eduardbar/drift 1.2.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 (195) 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/publish-vscode.yml +3 -3
  7. package/.github/workflows/publish.yml +3 -3
  8. package/.github/workflows/review-pr.yml +94 -9
  9. package/AGENTS.md +75 -245
  10. package/CHANGELOG.md +28 -0
  11. package/README.md +308 -51
  12. package/ROADMAP.md +6 -5
  13. package/dist/analyzer.d.ts +2 -2
  14. package/dist/analyzer.js +420 -159
  15. package/dist/benchmark.d.ts +2 -0
  16. package/dist/benchmark.js +204 -0
  17. package/dist/cli.js +693 -67
  18. package/dist/config.js +16 -2
  19. package/dist/diff.js +66 -10
  20. package/dist/doctor.d.ts +5 -0
  21. package/dist/doctor.js +133 -0
  22. package/dist/format.d.ts +17 -0
  23. package/dist/format.js +45 -0
  24. package/dist/git.js +12 -0
  25. package/dist/guard-types.d.ts +57 -0
  26. package/dist/guard-types.js +2 -0
  27. package/dist/guard.d.ts +14 -0
  28. package/dist/guard.js +239 -0
  29. package/dist/index.d.ts +12 -3
  30. package/dist/index.js +6 -1
  31. package/dist/init.d.ts +15 -0
  32. package/dist/init.js +273 -0
  33. package/dist/map-cycles.d.ts +2 -0
  34. package/dist/map-cycles.js +34 -0
  35. package/dist/map-svg.d.ts +19 -0
  36. package/dist/map-svg.js +97 -0
  37. package/dist/map.js +78 -138
  38. package/dist/metrics.js +70 -55
  39. package/dist/output-metadata.d.ts +13 -0
  40. package/dist/output-metadata.js +17 -0
  41. package/dist/plugins-capabilities.d.ts +4 -0
  42. package/dist/plugins-capabilities.js +21 -0
  43. package/dist/plugins-messages.d.ts +10 -0
  44. package/dist/plugins-messages.js +16 -0
  45. package/dist/plugins-rules.d.ts +9 -0
  46. package/dist/plugins-rules.js +137 -0
  47. package/dist/plugins.d.ts +2 -1
  48. package/dist/plugins.js +80 -28
  49. package/dist/printer.js +4 -0
  50. package/dist/reporter-constants.d.ts +16 -0
  51. package/dist/reporter-constants.js +39 -0
  52. package/dist/reporter.d.ts +3 -3
  53. package/dist/reporter.js +35 -55
  54. package/dist/review.d.ts +2 -1
  55. package/dist/review.js +4 -3
  56. package/dist/rules/comments.js +2 -2
  57. package/dist/rules/complexity.js +2 -7
  58. package/dist/rules/nesting.js +3 -13
  59. package/dist/rules/phase0-basic.js +10 -10
  60. package/dist/rules/phase3-configurable.js +23 -15
  61. package/dist/rules/shared.d.ts +2 -0
  62. package/dist/rules/shared.js +27 -3
  63. package/dist/saas/constants.d.ts +15 -0
  64. package/dist/saas/constants.js +48 -0
  65. package/dist/saas/dashboard.d.ts +8 -0
  66. package/dist/saas/dashboard.js +132 -0
  67. package/dist/saas/errors.d.ts +19 -0
  68. package/dist/saas/errors.js +37 -0
  69. package/dist/saas/helpers.d.ts +21 -0
  70. package/dist/saas/helpers.js +110 -0
  71. package/dist/saas/ingest.d.ts +3 -0
  72. package/dist/saas/ingest.js +249 -0
  73. package/dist/saas/organization.d.ts +5 -0
  74. package/dist/saas/organization.js +82 -0
  75. package/dist/saas/plan-change.d.ts +10 -0
  76. package/dist/saas/plan-change.js +15 -0
  77. package/dist/saas/store.d.ts +21 -0
  78. package/dist/saas/store.js +159 -0
  79. package/dist/saas/types.d.ts +191 -0
  80. package/dist/saas/types.js +2 -0
  81. package/dist/saas.d.ts +8 -82
  82. package/dist/saas.js +7 -320
  83. package/dist/sarif.d.ts +74 -0
  84. package/dist/sarif.js +122 -0
  85. package/dist/trust-advanced.d.ts +14 -0
  86. package/dist/trust-advanced.js +65 -0
  87. package/dist/trust-kpi-fs.d.ts +3 -0
  88. package/dist/trust-kpi-fs.js +141 -0
  89. package/dist/trust-kpi-parse.d.ts +7 -0
  90. package/dist/trust-kpi-parse.js +186 -0
  91. package/dist/trust-kpi-types.d.ts +16 -0
  92. package/dist/trust-kpi-types.js +2 -0
  93. package/dist/trust-kpi.d.ts +7 -0
  94. package/dist/trust-kpi.js +185 -0
  95. package/dist/trust-policy.d.ts +32 -0
  96. package/dist/trust-policy.js +160 -0
  97. package/dist/trust-render.d.ts +9 -0
  98. package/dist/trust-render.js +54 -0
  99. package/dist/trust-scoring.d.ts +9 -0
  100. package/dist/trust-scoring.js +208 -0
  101. package/dist/trust.d.ts +37 -0
  102. package/dist/trust.js +168 -0
  103. package/dist/types/app.d.ts +30 -0
  104. package/dist/types/app.js +2 -0
  105. package/dist/types/config.d.ts +25 -0
  106. package/dist/types/config.js +2 -0
  107. package/dist/types/core.d.ts +100 -0
  108. package/dist/types/core.js +2 -0
  109. package/dist/types/diff.d.ts +55 -0
  110. package/dist/types/diff.js +2 -0
  111. package/dist/types/plugin.d.ts +41 -0
  112. package/dist/types/plugin.js +2 -0
  113. package/dist/types/trust.d.ts +120 -0
  114. package/dist/types/trust.js +2 -0
  115. package/dist/types.d.ts +8 -211
  116. package/docs/PRD.md +187 -109
  117. package/docs/plugin-contract.md +61 -0
  118. package/docs/release-notes-draft.md +40 -0
  119. package/docs/rules-catalog.md +49 -0
  120. package/docs/trust-core-release-checklist.md +87 -0
  121. package/package.json +6 -3
  122. package/packages/vscode-drift/src/code-actions.ts +1 -1
  123. package/schemas/drift-ai-output.v1.json +162 -0
  124. package/schemas/drift-report.v1.json +151 -0
  125. package/schemas/drift-trust.v1.json +131 -0
  126. package/scripts/smoke-repo.mjs +394 -0
  127. package/src/analyzer.ts +484 -155
  128. package/src/benchmark.ts +266 -0
  129. package/src/cli.ts +840 -85
  130. package/src/config.ts +19 -2
  131. package/src/diff.ts +84 -10
  132. package/src/doctor.ts +173 -0
  133. package/src/format.ts +81 -0
  134. package/src/git.ts +16 -0
  135. package/src/guard-types.ts +64 -0
  136. package/src/guard.ts +324 -0
  137. package/src/index.ts +83 -0
  138. package/src/init.ts +298 -0
  139. package/src/map-cycles.ts +38 -0
  140. package/src/map-svg.ts +124 -0
  141. package/src/map.ts +111 -142
  142. package/src/metrics.ts +78 -59
  143. package/src/output-metadata.ts +30 -0
  144. package/src/plugins-capabilities.ts +36 -0
  145. package/src/plugins-messages.ts +35 -0
  146. package/src/plugins-rules.ts +296 -0
  147. package/src/plugins.ts +148 -27
  148. package/src/printer.ts +4 -0
  149. package/src/reporter-constants.ts +46 -0
  150. package/src/reporter.ts +64 -65
  151. package/src/review.ts +6 -4
  152. package/src/rules/comments.ts +2 -2
  153. package/src/rules/complexity.ts +2 -7
  154. package/src/rules/nesting.ts +3 -13
  155. package/src/rules/phase0-basic.ts +11 -12
  156. package/src/rules/phase3-configurable.ts +39 -26
  157. package/src/rules/shared.ts +31 -3
  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 -433
  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 +210 -0
  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 +260 -0
  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 +78 -238
  185. package/tests/cli-sarif.test.ts +92 -0
  186. package/tests/diff.test.ts +124 -0
  187. package/tests/format.test.ts +157 -0
  188. package/tests/new-features.test.ts +80 -1
  189. package/tests/phase1-init-doctor-guard.test.ts +199 -0
  190. package/tests/plugins.test.ts +219 -0
  191. package/tests/rules.test.ts +23 -1
  192. package/tests/saas-foundation.test.ts +358 -1
  193. package/tests/sarif.test.ts +160 -0
  194. package/tests/trust-kpi.test.ts +147 -0
  195. package/tests/trust.test.ts +602 -0
package/dist/plugins.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- import type { PluginLoadError, 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[];
5
+ warnings: PluginLoadWarning[];
5
6
  };
6
7
  //# sourceMappingURL=plugins.d.ts.map
package/dist/plugins.js CHANGED
@@ -1,30 +1,82 @@
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
- function isPluginShape(value) {
6
- if (!value || typeof value !== 'object')
7
- return false;
8
- const candidate = value;
9
- if (typeof candidate.name !== 'string')
10
- return false;
11
- if (!Array.isArray(candidate.rules))
12
- return false;
13
- return candidate.rules.every((rule) => rule &&
14
- typeof rule === 'object' &&
15
- typeof rule.name === 'string' &&
16
- typeof rule.detect === 'function');
17
- }
8
+ const SUPPORTED_PLUGIN_API_VERSION = 1;
18
9
  function normalizePluginExport(mod) {
19
- if (isPluginShape(mod))
20
- return mod;
21
10
  if (mod && typeof mod === 'object' && 'default' in mod) {
22
- const maybeDefault = mod.default;
23
- if (isPluginShape(maybeDefault))
24
- return maybeDefault;
11
+ return mod.default ?? mod;
12
+ }
13
+ return mod;
14
+ }
15
+ function ensureObjectCandidate(pluginId, candidate, errors) {
16
+ if (candidate && typeof candidate === 'object') {
17
+ return candidate;
25
18
  }
19
+ pushError(errors, pluginId, `Invalid plugin contract in '${pluginId}'. Expected an object export with shape { name, rules[] }`, { code: 'plugin-shape-invalid' });
26
20
  return undefined;
27
21
  }
22
+ function ensurePluginName(pluginId, plugin, errors) {
23
+ const pluginName = typeof plugin.name === 'string' ? plugin.name.trim() : '';
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;
31
+ const hasExplicitApiVersion = plugin.apiVersion !== undefined;
32
+ const isLegacyPlugin = !hasExplicitApiVersion;
33
+ if (isLegacyPlugin) {
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 };
36
+ }
37
+ if (typeof plugin.apiVersion !== 'number' || !Number.isInteger(plugin.apiVersion) || plugin.apiVersion <= 0) {
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' });
39
+ return { hasExplicitApiVersion, isLegacyPlugin, isSupported: false };
40
+ }
41
+ if (plugin.apiVersion !== SUPPORTED_PLUGIN_API_VERSION) {
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' });
43
+ return { hasExplicitApiVersion, isLegacyPlugin, isSupported: false };
44
+ }
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)
52
+ return { errors, warnings };
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)
65
+ return { errors, warnings };
66
+ return {
67
+ plugin: {
68
+ name: pluginName,
69
+ apiVersion: apiVersion.hasExplicitApiVersion ? plugin.apiVersion : SUPPORTED_PLUGIN_API_VERSION,
70
+ capabilities,
71
+ rules: normalizedRules,
72
+ },
73
+ errors,
74
+ warnings,
75
+ };
76
+ }
77
+ function validatePluginContract(pluginId, candidate) {
78
+ return validatePluginContractData(pluginId, candidate);
79
+ }
28
80
  function resolvePluginSpecifier(projectRoot, pluginId) {
29
81
  if (pluginId.startsWith('.') || pluginId.startsWith('/')) {
30
82
  const abs = isAbsolute(pluginId) ? pluginId : resolve(projectRoot, pluginId);
@@ -44,31 +96,31 @@ function resolvePluginSpecifier(projectRoot, pluginId) {
44
96
  }
45
97
  export function loadPlugins(projectRoot, pluginIds) {
46
98
  if (!pluginIds || pluginIds.length === 0) {
47
- return { plugins: [], errors: [] };
99
+ return { plugins: [], errors: [], warnings: [] };
48
100
  }
49
101
  const loaded = [];
50
102
  const errors = [];
103
+ const warnings = [];
51
104
  for (const pluginId of pluginIds) {
52
105
  const resolved = resolvePluginSpecifier(projectRoot, pluginId);
53
106
  try {
54
107
  const mod = require(resolved);
55
- const plugin = normalizePluginExport(mod);
56
- if (!plugin) {
57
- errors.push({
58
- pluginId,
59
- message: `Invalid plugin contract in '${pluginId}'. Expected: { name, rules[] }`,
60
- });
108
+ const normalized = normalizePluginExport(mod);
109
+ const validation = validatePluginContract(pluginId, normalized);
110
+ errors.push(...validation.errors);
111
+ warnings.push(...validation.warnings);
112
+ if (!validation.plugin)
61
113
  continue;
62
- }
63
- loaded.push({ id: pluginId, plugin });
114
+ loaded.push({ id: pluginId, plugin: validation.plugin });
64
115
  }
65
116
  catch (error) {
66
117
  errors.push({
67
118
  pluginId,
119
+ code: 'plugin-load-failed',
68
120
  message: error instanceof Error ? error.message : String(error),
69
121
  });
70
122
  }
71
123
  }
72
- return { plugins: loaded, errors };
124
+ return { plugins: loaded, errors, warnings };
73
125
  }
74
126
  //# sourceMappingURL=plugins.js.map
package/dist/printer.js CHANGED
@@ -131,6 +131,10 @@ function formatFixSuggestion(issue) {
131
131
  'Fix or remove the failing plugin in drift.config.*',
132
132
  'Validate plugin contract: export { name, rules[] } and detector functions',
133
133
  ],
134
+ 'plugin-warning': [
135
+ 'Review plugin validation warnings and align with the recommended contract',
136
+ 'Use explicit rule ids and valid severity/weight values',
137
+ ],
134
138
  };
135
139
  return suggestions[issue.rule] ?? ['Review and fix manually'];
136
140
  }
@@ -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
@@ -1,13 +1,14 @@
1
- import { resolve } from 'node:path';
1
+ import { relative, resolve } from 'node:path';
2
2
  import { analyzeProject } from './analyzer.js';
3
3
  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`;
@@ -48,7 +49,7 @@ export async function generateReview(projectPath, baseRef) {
48
49
  ...baseReport,
49
50
  files: baseReport.files.map((file) => ({
50
51
  ...file,
51
- path: file.path.replace(tempDir, resolvedPath),
52
+ path: resolve(resolvedPath, relative(tempDir, file.path)),
52
53
  })),
53
54
  };
54
55
  const diff = computeDiff(remappedBase, currentReport, baseRef);
@@ -1,4 +1,4 @@
1
- import { hasIgnoreComment } from './shared.js';
1
+ import { hasIgnoreComment, getFileLines } from './shared.js';
2
2
  const TRIVIAL_COMMENT_PATTERNS = [
3
3
  { comment: /\/\/\s*return\b/i, code: /^\s*return\b/ },
4
4
  { comment: /\/\/\s*(increment|increase|add\s+1|plus\s+1)\b/i, code: /\+\+|(\+= ?1)\b/ },
@@ -31,7 +31,7 @@ function checkLineForContradiction(commentLine, nextLine, lineNumber, file) {
31
31
  }
32
32
  export function detectCommentContradiction(file) {
33
33
  const issues = [];
34
- const lines = file.getFullText().split('\n');
34
+ const lines = getFileLines(file);
35
35
  for (let i = 0; i < lines.length - 1; i++) {
36
36
  const commentLine = lines[i].trim();
37
37
  const nextLine = lines[i + 1];
@@ -1,5 +1,5 @@
1
1
  import { SyntaxKind } from 'ts-morph';
2
- import { hasIgnoreComment, getSnippet } from './shared.js';
2
+ import { hasIgnoreComment, getSnippet, collectFunctionLikes } from './shared.js';
3
3
  const COMPLEXITY_THRESHOLD = 10;
4
4
  const INCREMENT_KINDS = [
5
5
  SyntaxKind.IfStatement,
@@ -24,12 +24,7 @@ function getCyclomaticComplexity(fn) {
24
24
  }
25
25
  export function detectHighComplexity(file) {
26
26
  const issues = [];
27
- const fns = [
28
- ...file.getFunctions(),
29
- ...file.getDescendantsOfKind(SyntaxKind.ArrowFunction),
30
- ...file.getDescendantsOfKind(SyntaxKind.FunctionExpression),
31
- ...file.getClasses().flatMap((c) => c.getMethods()),
32
- ];
27
+ const fns = collectFunctionLikes(file);
33
28
  for (const fn of fns) {
34
29
  const complexity = getCyclomaticComplexity(fn);
35
30
  if (complexity > COMPLEXITY_THRESHOLD) {
@@ -1,5 +1,5 @@
1
1
  import { SyntaxKind } from 'ts-morph';
2
- import { hasIgnoreComment, getSnippet } from './shared.js';
2
+ import { hasIgnoreComment, getSnippet, collectFunctionLikes } from './shared.js';
3
3
  const NESTING_THRESHOLD = 3;
4
4
  const PARAMS_THRESHOLD = 4;
5
5
  const NESTING_KINDS = new Set([
@@ -29,12 +29,7 @@ function getMaxNestingDepth(fn) {
29
29
  }
30
30
  export function detectDeepNesting(file) {
31
31
  const issues = [];
32
- const fns = [
33
- ...file.getFunctions(),
34
- ...file.getDescendantsOfKind(SyntaxKind.ArrowFunction),
35
- ...file.getDescendantsOfKind(SyntaxKind.FunctionExpression),
36
- ...file.getClasses().flatMap((c) => c.getMethods()),
37
- ];
32
+ const fns = collectFunctionLikes(file);
38
33
  for (const fn of fns) {
39
34
  const depth = getMaxNestingDepth(fn);
40
35
  if (depth > NESTING_THRESHOLD) {
@@ -55,12 +50,7 @@ export function detectDeepNesting(file) {
55
50
  }
56
51
  export function detectTooManyParams(file) {
57
52
  const issues = [];
58
- const fns = [
59
- ...file.getFunctions(),
60
- ...file.getDescendantsOfKind(SyntaxKind.ArrowFunction),
61
- ...file.getDescendantsOfKind(SyntaxKind.FunctionExpression),
62
- ...file.getClasses().flatMap((c) => c.getMethods()),
63
- ];
53
+ const fns = collectFunctionLikes(file);
64
54
  for (const fn of fns) {
65
55
  const paramCount = fn.getParameters().length;
66
56
  if (paramCount > PARAMS_THRESHOLD) {
@@ -1,5 +1,5 @@
1
1
  import { SyntaxKind } from 'ts-morph';
2
- import { hasIgnoreComment, getSnippet, getFunctionLikeLines } from './shared.js';
2
+ import { hasIgnoreComment, getSnippet, getFunctionLikeLines, collectFunctionLikes, getFileLines } from './shared.js';
3
3
  const LARGE_FILE_THRESHOLD = 300;
4
4
  const LARGE_FUNCTION_THRESHOLD = 50;
5
5
  const SNIPPET_TRUNCATE_SHORT = 60;
@@ -22,12 +22,7 @@ export function detectLargeFile(file) {
22
22
  }
23
23
  export function detectLargeFunctions(file) {
24
24
  const issues = [];
25
- const fns = [
26
- ...file.getFunctions(),
27
- ...file.getDescendantsOfKind(SyntaxKind.ArrowFunction),
28
- ...file.getDescendantsOfKind(SyntaxKind.FunctionExpression),
29
- ...file.getClasses().flatMap((c) => c.getMethods()),
30
- ];
25
+ const fns = collectFunctionLikes(file);
31
26
  for (const fn of fns) {
32
27
  const lines = getFunctionLikeLines(fn);
33
28
  const startLine = fn.getStartLineNumber();
@@ -64,7 +59,7 @@ export function detectDebugLeftovers(file) {
64
59
  });
65
60
  }
66
61
  }
67
- const lines = file.getFullText().split('\n');
62
+ const lines = getFileLines(file);
68
63
  lines.forEach((lineContent, i) => {
69
64
  if (/\/\/\s*(TODO|FIXME|HACK|XXX|TEMP)\b/i.test(lineContent)) {
70
65
  if (hasIgnoreComment(file, i + 1))
@@ -83,11 +78,16 @@ export function detectDebugLeftovers(file) {
83
78
  }
84
79
  export function detectDeadCode(file) {
85
80
  const issues = [];
81
+ const identifierCounts = new Map();
82
+ for (const id of file.getDescendantsOfKind(SyntaxKind.Identifier)) {
83
+ const text = id.getText();
84
+ identifierCounts.set(text, (identifierCounts.get(text) ?? 0) + 1);
85
+ }
86
86
  for (const imp of file.getImportDeclarations()) {
87
87
  for (const named of imp.getNamedImports()) {
88
88
  const name = named.getName();
89
- const refs = file.getDescendantsOfKind(SyntaxKind.Identifier).filter((id) => id.getText() === name && id !== named.getNameNode());
90
- if (refs.length === 0) {
89
+ const refsCount = Math.max(0, (identifierCounts.get(name) ?? 0) - 1);
90
+ if (refsCount === 0) {
91
91
  issues.push({
92
92
  rule: 'dead-code',
93
93
  severity: 'warning',
@@ -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
@@ -1,5 +1,7 @@
1
1
  import { SourceFile, Node, FunctionDeclaration, ArrowFunction, FunctionExpression, MethodDeclaration } from 'ts-morph';
2
2
  export type FunctionLike = FunctionDeclaration | ArrowFunction | FunctionExpression | MethodDeclaration;
3
+ export declare function getFileLines(file: SourceFile): string[];
4
+ export declare function collectFunctionLikes(file: SourceFile): FunctionLike[];
3
5
  export declare function hasIgnoreComment(file: SourceFile, line: number): boolean;
4
6
  export declare function isFileIgnored(file: SourceFile): boolean;
5
7
  export declare function getSnippet(node: Node, file: SourceFile): string;
@@ -1,5 +1,29 @@
1
- export function hasIgnoreComment(file, line) {
1
+ import { SyntaxKind, } from 'ts-morph';
2
+ const fileLinesCache = new WeakMap();
3
+ const functionLikesCache = new WeakMap();
4
+ export function getFileLines(file) {
5
+ const cached = fileLinesCache.get(file);
6
+ if (cached)
7
+ return cached;
2
8
  const lines = file.getFullText().split('\n');
9
+ fileLinesCache.set(file, lines);
10
+ return lines;
11
+ }
12
+ export function collectFunctionLikes(file) {
13
+ const cached = functionLikesCache.get(file);
14
+ if (cached)
15
+ return cached;
16
+ const fns = [
17
+ ...file.getFunctions(),
18
+ ...file.getDescendantsOfKind(SyntaxKind.ArrowFunction),
19
+ ...file.getDescendantsOfKind(SyntaxKind.FunctionExpression),
20
+ ...file.getClasses().flatMap((c) => c.getMethods()),
21
+ ];
22
+ functionLikesCache.set(file, fns);
23
+ return fns;
24
+ }
25
+ export function hasIgnoreComment(file, line) {
26
+ const lines = getFileLines(file);
3
27
  const currentLine = lines[line - 1] ?? '';
4
28
  const prevLine = lines[line - 2] ?? '';
5
29
  if (/\/\/\s*drift-ignore\b/.test(currentLine))
@@ -9,12 +33,12 @@ export function hasIgnoreComment(file, line) {
9
33
  return false;
10
34
  }
11
35
  export function isFileIgnored(file) {
12
- const firstLines = file.getFullText().split('\n').slice(0, 10).join('\n'); // drift-ignore
36
+ const firstLines = getFileLines(file).slice(0, 10).join('\n'); // drift-ignore
13
37
  return /\/\/\s*drift-ignore-file\b/.test(firstLines);
14
38
  }
15
39
  export function getSnippet(node, file) {
16
40
  const startLine = node.getStartLineNumber();
17
- const lines = file.getFullText().split('\n');
41
+ const lines = getFileLines(file);
18
42
  return lines
19
43
  .slice(Math.max(0, startLine - 1), startLine + 1)
20
44
  .join('\n')