@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
@@ -0,0 +1,231 @@
1
+ import { RULE_WEIGHTS } from './analyzer.js'
2
+ import type { DriftDiff, DriftReport, MergeRiskLevel, TrustDiffContext, TrustFixPriority, TrustReason } from './types.js'
3
+
4
+ const ARCHITECTURE_RULES = new Set([
5
+ 'circular-dependency',
6
+ 'layer-violation',
7
+ 'cross-boundary-import',
8
+ 'controller-no-db',
9
+ 'service-no-http',
10
+ ])
11
+
12
+ const RULE_SUGGESTIONS: Record<string, string> = {
13
+ 'circular-dependency': 'Break cycles first to reduce hidden merge blast radius.',
14
+ 'layer-violation': 'Fix layer violations to keep architecture boundaries enforceable.',
15
+ 'high-complexity': 'Split branch-heavy functions before adding more logic.',
16
+ 'deep-nesting': 'Flatten control flow with early returns.',
17
+ 'large-file': 'Split monolithic files by responsibility before merge.',
18
+ 'large-function': 'Extract smaller functions to reduce review complexity.',
19
+ 'catch-swallow': 'Handle or rethrow swallowed errors to avoid silent failures.',
20
+ 'debug-leftover': 'Remove debug leftovers from production paths.',
21
+ 'semantic-duplication': 'Consolidate duplicated logic to prevent divergent fixes.',
22
+ 'dead-file': 'Delete or wire dead files to avoid stale merge artifacts.',
23
+ }
24
+
25
+ const SYSTEMIC_RULES = new Set([
26
+ 'circular-dependency',
27
+ 'layer-violation',
28
+ 'cross-boundary-import',
29
+ 'unused-export',
30
+ 'unused-dependency',
31
+ 'dead-file',
32
+ 'semantic-duplication',
33
+ 'controller-no-db',
34
+ 'service-no-http',
35
+ ])
36
+
37
+ const TRUST_LOW_MIN = 80
38
+ const TRUST_MEDIUM_MIN = 60
39
+ const TRUST_HIGH_MIN = 40
40
+
41
+ const DRIFT_PRESSURE_FACTOR = 0.55
42
+ const ERROR_IMPACT_CAP = 22
43
+ const ERROR_IMPACT_FACTOR = 4
44
+ const ARCHITECTURE_IMPACT_CAP = 24
45
+ const ARCHITECTURE_IMPACT_FACTOR = 6
46
+ const HOTSPOT_IMPACT_CAP = 25
47
+ const HOTSPOT_IMPACT_FACTOR = 0.25
48
+ const WORST_FILE_IMPACT_CAP = 15
49
+ const WORST_FILE_IMPACT_FACTOR = 0.15
50
+ const TOP_REASONS_LIMIT = 4
51
+
52
+ const EFFORT_LOW_MAX_WEIGHT = 6
53
+ const EFFORT_MEDIUM_MAX_WEIGHT = 12
54
+
55
+ const SCORE_REGRESSION_PENALTY_FACTOR = 2
56
+ const NEW_ISSUE_PENALTY_FACTOR = 3
57
+ const CHURN_FILE_THRESHOLD = 15
58
+ const CHURN_PENALTY = 4
59
+ const PENALTY_CAP = 30
60
+ const RESOLVED_ISSUE_BONUS_FACTOR = 2
61
+ const BONUS_CAP = 20
62
+
63
+ const SEVERITY_ERROR_SCORE = 4
64
+ const SEVERITY_WARNING_SCORE = 2
65
+ const SEVERITY_INFO_SCORE = 1
66
+ const PRIORITY_OCCURRENCE_FACTOR = 2
67
+ const SYSTEMIC_CONFIDENCE_BOOST = 2
68
+ const CONFIDENCE_HIGH_MIN = 12
69
+ const CONFIDENCE_MEDIUM_MIN = 7
70
+
71
+ const DEFAULT_WEIGHT_CONFIG = { severity: 'warning' as const, weight: EFFORT_LOW_MAX_WEIGHT }
72
+ const SEVERITY_BOOST_ERROR = 25
73
+ const SEVERITY_BOOST_WARNING = 12
74
+ const SEVERITY_BOOST_INFO = 4
75
+ const SYSTEMIC_PRIORITY_BOOST = 25
76
+ const TRUST_GAIN_MAX = 30
77
+ const TRUST_GAIN_MIN = 3
78
+ const TRUST_GAIN_DIVISOR = 4
79
+ const FIX_PRIORITIES_LIMIT = 5
80
+
81
+ export const TOP_REASONS_SLICE = TOP_REASONS_LIMIT
82
+
83
+ export function clamp(value: number, min: number, max: number): number {
84
+ return Math.max(min, Math.min(max, value))
85
+ }
86
+
87
+ export function toMergeRisk(trustScore: number): MergeRiskLevel {
88
+ if (trustScore >= TRUST_LOW_MIN) return 'LOW'
89
+ if (trustScore >= TRUST_MEDIUM_MIN) return 'MEDIUM'
90
+ if (trustScore >= TRUST_HIGH_MIN) return 'HIGH'
91
+ return 'CRITICAL'
92
+ }
93
+
94
+ export function computeReasons(report: DriftReport): TrustReason[] {
95
+ const architectureIssues = Object.entries(report.summary.byRule)
96
+ .filter(([rule]) => ARCHITECTURE_RULES.has(rule))
97
+ .reduce((sum, [, count]) => sum + count, 0)
98
+
99
+ const worstHotspot = report.maintenanceRisk.hotspots[0]
100
+ const reasons: TrustReason[] = [
101
+ {
102
+ label: 'Drift score pressure',
103
+ detail: `Repository drift score is ${report.totalScore}/100.`,
104
+ impact: Math.round(report.totalScore * DRIFT_PRESSURE_FACTOR),
105
+ },
106
+ {
107
+ label: 'Error-level issues',
108
+ detail: `${report.summary.errors} error issue(s) increase merge volatility.`,
109
+ impact: Math.min(ERROR_IMPACT_CAP, report.summary.errors * ERROR_IMPACT_FACTOR),
110
+ },
111
+ {
112
+ label: 'Architecture signals',
113
+ detail: `${architectureIssues} architecture-related issue(s) detected.`,
114
+ impact: Math.min(ARCHITECTURE_IMPACT_CAP, architectureIssues * ARCHITECTURE_IMPACT_FACTOR),
115
+ },
116
+ {
117
+ label: 'Maintenance hotspots',
118
+ detail: `Maintenance risk is ${report.maintenanceRisk.level.toUpperCase()} (${report.maintenanceRisk.score}/100).`,
119
+ impact: Math.min(HOTSPOT_IMPACT_CAP, Math.round(report.maintenanceRisk.score * HOTSPOT_IMPACT_FACTOR)),
120
+ },
121
+ {
122
+ label: 'Highest-risk file',
123
+ detail: worstHotspot
124
+ ? `${worstHotspot.file} has hotspot risk ${worstHotspot.risk}/100.`
125
+ : 'No hotspot concentration detected.',
126
+ impact: worstHotspot ? Math.min(WORST_FILE_IMPACT_CAP, Math.round(worstHotspot.risk * WORST_FILE_IMPACT_FACTOR)) : 0,
127
+ },
128
+ ]
129
+
130
+ return reasons
131
+ .filter((reason) => reason.impact > 0)
132
+ .sort((a, b) => b.impact - a.impact)
133
+ .slice(0, TOP_REASONS_LIMIT)
134
+ }
135
+
136
+ function effortFromWeight(weight: number): 'low' | 'medium' | 'high' {
137
+ if (weight <= EFFORT_LOW_MAX_WEIGHT) return 'low'
138
+ if (weight <= EFFORT_MEDIUM_MAX_WEIGHT) return 'medium'
139
+ return 'high'
140
+ }
141
+
142
+ export function computeDiffContext(diff: DriftDiff): TrustDiffContext {
143
+ const scoreRegressionPenalty = Math.max(0, diff.totalDelta) * SCORE_REGRESSION_PENALTY_FACTOR
144
+ const newIssuePenalty = diff.newIssuesCount * NEW_ISSUE_PENALTY_FACTOR
145
+ const churnPenalty = diff.files.length >= CHURN_FILE_THRESHOLD ? CHURN_PENALTY : 0
146
+ const penalty = clamp(scoreRegressionPenalty + newIssuePenalty + churnPenalty, 0, PENALTY_CAP)
147
+
148
+ const scoreImprovementBonus = Math.max(0, -diff.totalDelta) * SCORE_REGRESSION_PENALTY_FACTOR
149
+ const resolvedIssueBonus = diff.resolvedIssuesCount * RESOLVED_ISSUE_BONUS_FACTOR
150
+ const bonus = clamp(scoreImprovementBonus + resolvedIssueBonus, 0, BONUS_CAP)
151
+
152
+ const netImpact = penalty - bonus
153
+ const status = netImpact > 0 ? 'regressed' : netImpact < 0 ? 'improved' : 'neutral'
154
+
155
+ return {
156
+ baseRef: diff.baseRef,
157
+ status,
158
+ scoreDelta: diff.totalDelta,
159
+ newIssues: diff.newIssuesCount,
160
+ resolvedIssues: diff.resolvedIssuesCount,
161
+ filesChanged: diff.files.length,
162
+ penalty,
163
+ bonus,
164
+ netImpact,
165
+ }
166
+ }
167
+
168
+ function confidenceFromPrioritySignals(
169
+ occurrences: number,
170
+ severity: 'error' | 'warning' | 'info',
171
+ systemic: boolean,
172
+ ): 'low' | 'medium' | 'high' {
173
+ const severityScore = severity === 'error' ? SEVERITY_ERROR_SCORE : severity === 'warning' ? SEVERITY_WARNING_SCORE : SEVERITY_INFO_SCORE
174
+ const systemicScore = systemic ? SYSTEMIC_CONFIDENCE_BOOST : 0
175
+ const score = occurrences * PRIORITY_OCCURRENCE_FACTOR + severityScore + systemicScore
176
+
177
+ if (score >= CONFIDENCE_HIGH_MIN) return 'high'
178
+ if (score >= CONFIDENCE_MEDIUM_MIN) return 'medium'
179
+ return 'low'
180
+ }
181
+
182
+ export function computeFixPriorities(report: DriftReport, advancedMode = false): TrustFixPriority[] {
183
+ const ordered = Object.entries(report.summary.byRule)
184
+ .map(([rule, occurrences]) => {
185
+ const weightConfig = RULE_WEIGHTS[rule] ?? DEFAULT_WEIGHT_CONFIG
186
+ const severityBoost = weightConfig.severity === 'error' ? SEVERITY_BOOST_ERROR : weightConfig.severity === 'warning' ? SEVERITY_BOOST_WARNING : SEVERITY_BOOST_INFO
187
+ const systemic = SYSTEMIC_RULES.has(rule)
188
+ const systemicBoost = advancedMode && systemic ? SYSTEMIC_PRIORITY_BOOST : 0
189
+ const priorityScore = occurrences * weightConfig.weight + severityBoost + systemicBoost
190
+ const confidence = confidenceFromPrioritySignals(occurrences, weightConfig.severity, systemic)
191
+ const explanation = advancedMode
192
+ ? systemic
193
+ ? 'System-level rule that propagates risk across multiple teams and modules.'
194
+ : 'Local rule with contained impact; treat as team-level cleanup after systemic fixes.'
195
+ : undefined
196
+
197
+ return {
198
+ rule,
199
+ severity: weightConfig.severity,
200
+ occurrences,
201
+ systemic,
202
+ priorityScore,
203
+ estimatedTrustGain: Math.min(TRUST_GAIN_MAX, Math.max(TRUST_GAIN_MIN, Math.round(priorityScore / TRUST_GAIN_DIVISOR))),
204
+ effort: effortFromWeight(weightConfig.weight),
205
+ suggestion: RULE_SUGGESTIONS[rule] ?? 'Address this rule in the highest-scored files first.',
206
+ confidence,
207
+ explanation,
208
+ }
209
+ })
210
+ .sort((a, b) => b.priorityScore - a.priorityScore)
211
+ .slice(0, FIX_PRIORITIES_LIMIT)
212
+
213
+ return ordered.map((item, index) => ({
214
+ rank: index + 1,
215
+ rule: item.rule,
216
+ severity: item.severity,
217
+ occurrences: item.occurrences,
218
+ estimated_trust_gain: item.estimatedTrustGain,
219
+ effort: item.effort,
220
+ suggestion: item.suggestion,
221
+ ...(advancedMode ? { confidence: item.confidence, explanation: item.explanation, systemic: item.systemic } : {}),
222
+ }))
223
+ }
224
+
225
+ export function buildDiffRegressionReason(diffContext: TrustDiffContext): TrustReason {
226
+ return {
227
+ label: 'Diff regression signals',
228
+ detail: `Against ${diffContext.baseRef}: score delta ${diffContext.scoreDelta >= 0 ? '+' : ''}${diffContext.scoreDelta}, +${diffContext.newIssues} new issue(s), -${diffContext.resolvedIssues} resolved.`,
229
+ impact: diffContext.netImpact,
230
+ }
231
+ }
package/src/trust.ts ADDED
@@ -0,0 +1,260 @@
1
+ import type {
2
+ DriftDiff,
3
+ DriftReport,
4
+ DriftTrustReport,
5
+ DriftTrustReportJson,
6
+ MergeRiskLevel,
7
+ } from './types.js'
8
+ import type { SnapshotEntry } from './snapshot.js'
9
+ import { MERGE_RISK_ORDER } from './trust-policy.js'
10
+ import type { TrustGateOptions } from './trust-policy.js'
11
+ import { buildAdvancedContext } from './trust-advanced.js'
12
+ import {
13
+ TOP_REASONS_SLICE,
14
+ buildDiffRegressionReason,
15
+ clamp,
16
+ computeDiffContext,
17
+ computeFixPriorities,
18
+ computeReasons,
19
+ toMergeRisk,
20
+ } from './trust-scoring.js'
21
+ import {
22
+ renderTrustAdvancedComparison,
23
+ renderTrustAdvancedGuidance,
24
+ renderTrustDiffBlock,
25
+ renderTrustMarkdownPriorities,
26
+ renderTrustMarkdownReasons,
27
+ renderTrustPriorities,
28
+ renderTrustReasons,
29
+ } from './trust-render.js'
30
+ import { OUTPUT_SCHEMA, withOutputMetadata } from './output-metadata.js'
31
+
32
+ export {
33
+ MERGE_RISK_ORDER,
34
+ detectBranchName,
35
+ explainTrustGatePolicy,
36
+ formatTrustGatePolicyExplanation,
37
+ normalizeMergeRiskLevel,
38
+ resolveTrustGatePolicy,
39
+ } from './trust-policy.js'
40
+ export type {
41
+ TrustGatePolicyExplanation,
42
+ TrustGatePolicyResolutionOptions,
43
+ TrustGatePolicyResolutionStep,
44
+ TrustGateOptions,
45
+ } from './trust-policy.js'
46
+
47
+ interface BuildTrustOptions {
48
+ diff?: DriftDiff
49
+ advanced?: {
50
+ enabled?: boolean
51
+ previousTrust?: Partial<DriftTrustReport>
52
+ snapshots?: SnapshotEntry[]
53
+ }
54
+ }
55
+
56
+ interface TrustRenderOptions {
57
+ json?: boolean
58
+ markdown?: boolean
59
+ }
60
+
61
+ export interface TrustGateEvaluation {
62
+ shouldFail: boolean
63
+ reasons: string[]
64
+ checks: {
65
+ gateDisabled: boolean
66
+ belowMinTrust: boolean
67
+ aboveMaxRisk: boolean
68
+ minTrust?: number
69
+ maxRisk?: MergeRiskLevel
70
+ }
71
+ }
72
+
73
+ const CONSOLE_DIFF_INSERT_INDEX = 5
74
+
75
+ export function buildTrustReport(report: DriftReport, options?: BuildTrustOptions): DriftTrustReport {
76
+ const reasons = computeReasons(report)
77
+
78
+ const diffContext = options?.diff ? computeDiffContext(options.diff) : undefined
79
+ if (diffContext && diffContext.netImpact > 0) {
80
+ reasons.push(buildDiffRegressionReason(diffContext))
81
+ }
82
+
83
+ const rankedReasons = reasons
84
+ .filter((reason) => reason.impact > 0)
85
+ .sort((a, b) => b.impact - a.impact)
86
+ .slice(0, TOP_REASONS_SLICE)
87
+
88
+ const totalPenalty = rankedReasons.reduce((sum, reason) => sum + reason.impact, 0)
89
+ const totalBonus = diffContext && diffContext.netImpact < 0 ? Math.abs(diffContext.netImpact) : 0
90
+ const trustScore = clamp(Math.round(100 - totalPenalty + totalBonus), 0, 100)
91
+
92
+ const advancedMode = options?.advanced?.enabled === true
93
+ const fixPriorities = computeFixPriorities(report, advancedMode)
94
+ const advancedContext = buildAdvancedContext({
95
+ report,
96
+ advancedOptions: options?.advanced,
97
+ trustScore,
98
+ fixPriorities,
99
+ diffContext,
100
+ })
101
+
102
+ return {
103
+ scannedAt: new Date().toISOString(),
104
+ targetPath: report.targetPath,
105
+ trust_score: trustScore,
106
+ merge_risk: toMergeRisk(trustScore),
107
+ top_reasons: rankedReasons,
108
+ fix_priorities: fixPriorities,
109
+ diff_context: diffContext,
110
+ ...(advancedContext ? { advanced_context: advancedContext } : {}),
111
+ }
112
+ }
113
+
114
+ export function formatTrustConsole(trust: DriftTrustReport): string {
115
+ const diffContext = trust.diff_context
116
+ const diffLines = diffContext
117
+ ? [
118
+ `- base: ${diffContext.baseRef}`,
119
+ `- status: ${diffContext.status.toUpperCase()}`,
120
+ `- score delta: ${diffContext.scoreDelta >= 0 ? '+' : ''}${diffContext.scoreDelta}`,
121
+ `- issues: +${diffContext.newIssues} new / -${diffContext.resolvedIssues} resolved`,
122
+ `- impact: +${diffContext.penalty} penalty / -${diffContext.bonus} bonus (net ${diffContext.netImpact >= 0 ? '+' : ''}${diffContext.netImpact})`,
123
+ ].join('\n')
124
+ : undefined
125
+
126
+ const reasons = renderTrustReasons(trust.top_reasons)
127
+ const priorities = renderTrustPriorities(trust.fix_priorities)
128
+
129
+ const advanced = trust.advanced_context
130
+ const advancedComparison = advanced?.comparison
131
+ ? [
132
+ `- source: ${advanced.comparison.source}`,
133
+ `- trend: ${advanced.comparison.trend.toUpperCase()}`,
134
+ `- summary: ${advanced.comparison.summary}`,
135
+ ].join('\n')
136
+ : '- no historical comparison available'
137
+ const advancedGuidance = advanced?.team_guidance?.length
138
+ ? advanced.team_guidance.map((item) => `- ${item}`).join('\n')
139
+ : '- none'
140
+
141
+ const sections = [
142
+ 'drift trust',
143
+ '',
144
+ `Trust Score: ${trust.trust_score}/100`,
145
+ `Merge Risk: ${trust.merge_risk}`,
146
+ '',
147
+ 'Top Reasons:',
148
+ reasons,
149
+ '',
150
+ 'Fix Priorities:',
151
+ priorities,
152
+ ]
153
+
154
+ if (diffLines) {
155
+ sections.splice(CONSOLE_DIFF_INSERT_INDEX, 0, 'Diff Context:', diffLines, '')
156
+ }
157
+
158
+ if (advanced) {
159
+ sections.push('', 'Advanced Team Guidance:', advancedComparison, '', advancedGuidance)
160
+ }
161
+
162
+ return sections.join('\n')
163
+ }
164
+
165
+ export function formatTrustMarkdown(trust: DriftTrustReport): string {
166
+ const reasons = renderTrustMarkdownReasons(trust.top_reasons)
167
+ const priorities = renderTrustMarkdownPriorities(trust.fix_priorities)
168
+ const diffBlock = renderTrustDiffBlock(trust.diff_context)
169
+ const advancedComparison = renderTrustAdvancedComparison(trust.advanced_context)
170
+ const advancedGuidance = renderTrustAdvancedGuidance(trust.advanced_context)
171
+
172
+ const sections = [
173
+ '## drift trust',
174
+ '',
175
+ `- Trust Score: **${trust.trust_score}/100**`,
176
+ `- Merge Risk: **${trust.merge_risk}**`,
177
+ `- Target: \`${trust.targetPath}\``,
178
+ '',
179
+ '### Diff signals',
180
+ diffBlock,
181
+ '',
182
+ '### Top reasons',
183
+ reasons,
184
+ '',
185
+ '### Fix priorities',
186
+ priorities,
187
+ ]
188
+
189
+ if (trust.advanced_context) {
190
+ sections.push('', '### Advanced comparison', advancedComparison, '', '### Team guidance', advancedGuidance)
191
+ }
192
+
193
+ return sections.join('\n')
194
+ }
195
+
196
+ function formatTrustJsonObject(trust: DriftTrustReport): DriftTrustReportJson {
197
+ return withOutputMetadata(trust, OUTPUT_SCHEMA.trust)
198
+ }
199
+
200
+ export function formatTrustJson(trust: DriftTrustReport): string {
201
+ return JSON.stringify(formatTrustJsonObject(trust), null, 2)
202
+ }
203
+
204
+ export function renderTrustOutput(trust: DriftTrustReport, options?: TrustRenderOptions): string {
205
+ if (options?.json) return formatTrustJson(trust)
206
+ if (options?.markdown) return formatTrustMarkdown(trust)
207
+ return formatTrustConsole(trust)
208
+ }
209
+
210
+ export function shouldFailByMaxRisk(actual: MergeRiskLevel, allowedMaxRisk: MergeRiskLevel): boolean {
211
+ return MERGE_RISK_ORDER.indexOf(actual) > MERGE_RISK_ORDER.indexOf(allowedMaxRisk)
212
+ }
213
+
214
+ export function evaluateTrustGate(trust: DriftTrustReport, options: TrustGateOptions): TrustGateEvaluation {
215
+ if (options.enabled === false) {
216
+ return {
217
+ shouldFail: false,
218
+ reasons: ['trust gate disabled by policy'],
219
+ checks: {
220
+ gateDisabled: true,
221
+ belowMinTrust: false,
222
+ aboveMaxRisk: false,
223
+ minTrust: options.minTrust,
224
+ maxRisk: options.maxRisk,
225
+ },
226
+ }
227
+ }
228
+
229
+ const belowMinTrust =
230
+ typeof options.minTrust === 'number' &&
231
+ !Number.isNaN(options.minTrust) &&
232
+ trust.trust_score < options.minTrust
233
+
234
+ const aboveMaxRisk = Boolean(options.maxRisk && shouldFailByMaxRisk(trust.merge_risk, options.maxRisk))
235
+ const reasons: string[] = []
236
+
237
+ if (belowMinTrust) {
238
+ reasons.push(`trust ${trust.trust_score} is below minTrust ${options.minTrust}`)
239
+ }
240
+
241
+ if (aboveMaxRisk && options.maxRisk) {
242
+ reasons.push(`merge risk ${trust.merge_risk} exceeds maxRisk ${options.maxRisk}`)
243
+ }
244
+
245
+ return {
246
+ shouldFail: belowMinTrust || aboveMaxRisk,
247
+ reasons,
248
+ checks: {
249
+ gateDisabled: false,
250
+ belowMinTrust,
251
+ aboveMaxRisk,
252
+ minTrust: options.minTrust,
253
+ maxRisk: options.maxRisk,
254
+ },
255
+ }
256
+ }
257
+
258
+ export function shouldFailTrustGate(trust: DriftTrustReport, options: TrustGateOptions): boolean {
259
+ return evaluateTrustGate(trust, options).shouldFail
260
+ }
@@ -0,0 +1,30 @@
1
+ import type { DriftPerformanceConfig, LayerDefinition, ModuleBoundary } from './config.js'
2
+ import type { TrustGatePolicyConfig } from './trust.js'
3
+
4
+ export interface DriftConfig {
5
+ layers?: LayerDefinition[]
6
+ modules?: ModuleBoundary[]
7
+ moduleBoundaries?: ModuleBoundary[]
8
+ boundaries?: ModuleBoundary[]
9
+ plugins?: string[]
10
+ performance?: DriftPerformanceConfig
11
+ architectureRules?: {
12
+ controllerNoDb?: boolean
13
+ serviceNoHttp?: boolean
14
+ maxFunctionLines?: number
15
+ }
16
+ saas?: {
17
+ freeUserThreshold?: number
18
+ maxRunsPerWorkspacePerMonth?: number
19
+ maxReposPerWorkspace?: number
20
+ retentionDays?: number
21
+ strictActorEnforcement?: boolean
22
+ maxWorkspacesPerOrganizationByPlan?: {
23
+ free?: number
24
+ sponsor?: number
25
+ team?: number
26
+ business?: number
27
+ }
28
+ }
29
+ trustGate?: TrustGatePolicyConfig
30
+ }
@@ -0,0 +1,27 @@
1
+ export interface LayerDefinition {
2
+ name: string
3
+ patterns: string[]
4
+ canImportFrom: string[]
5
+ }
6
+
7
+ export interface ModuleBoundary {
8
+ name: string
9
+ root: string
10
+ allowedExternalImports?: string[]
11
+ }
12
+
13
+ export interface DriftPerformanceConfig {
14
+ lowMemory?: boolean
15
+ chunkSize?: number
16
+ maxFiles?: number
17
+ maxFileSizeKb?: number
18
+ includeSemanticDuplication?: boolean
19
+ }
20
+
21
+ export interface DriftAnalysisOptions {
22
+ lowMemory?: boolean
23
+ chunkSize?: number
24
+ maxFiles?: number
25
+ maxFileSizeKb?: number
26
+ includeSemanticDuplication?: boolean
27
+ }
@@ -0,0 +1,105 @@
1
+ export interface DriftIssue {
2
+ rule: string
3
+ severity: 'error' | 'warning' | 'info'
4
+ message: string
5
+ line: number
6
+ column: number
7
+ snippet: string
8
+ }
9
+
10
+ export interface FileReport {
11
+ path: string
12
+ issues: DriftIssue[]
13
+ score: number
14
+ }
15
+
16
+ export interface RepoQualityScore {
17
+ overall: number
18
+ dimensions: {
19
+ architecture: number
20
+ complexity: number
21
+ 'ai-patterns': number
22
+ testing: number
23
+ }
24
+ }
25
+
26
+ export interface RiskHotspot {
27
+ file: string
28
+ driftScore: number
29
+ complexityIssues: number
30
+ hasNearbyTests: boolean
31
+ changeFrequency: number
32
+ risk: number
33
+ reasons: string[]
34
+ }
35
+
36
+ export interface MaintenanceRiskMetrics {
37
+ score: number
38
+ level: 'low' | 'medium' | 'high' | 'critical'
39
+ hotspots: RiskHotspot[]
40
+ signals: {
41
+ highComplexityFiles: number
42
+ filesWithoutNearbyTests: number
43
+ frequentChangeFiles: number
44
+ }
45
+ }
46
+
47
+ export interface DriftReport {
48
+ scannedAt: string
49
+ targetPath: string
50
+ files: FileReport[]
51
+ totalIssues: number
52
+ totalScore: number
53
+ totalFiles: number
54
+ summary: {
55
+ errors: number
56
+ warnings: number
57
+ infos: number
58
+ byRule: Record<string, number>
59
+ }
60
+ quality: RepoQualityScore
61
+ maintenanceRisk: MaintenanceRiskMetrics
62
+ }
63
+
64
+ export interface AIIssue {
65
+ rank: number
66
+ file: string
67
+ line: number
68
+ rule: string
69
+ severity: string
70
+ message: string
71
+ snippet: string
72
+ fix_suggestion: string
73
+ effort: 'low' | 'medium' | 'high'
74
+ }
75
+
76
+ export interface AIOutput {
77
+ summary: {
78
+ score: number
79
+ grade: string
80
+ total_issues: number
81
+ files_affected: number
82
+ files_clean: number
83
+ ai_likelihood: number
84
+ ai_code_smell_score: number
85
+ }
86
+ files_suspected: Array<{ path: string; ai_likelihood: number; triggers: string[] }>
87
+ priority_order: AIIssue[]
88
+ maintenance_risk: MaintenanceRiskMetrics
89
+ quality: RepoQualityScore
90
+ context_for_ai: {
91
+ project_type: string
92
+ scan_path: string
93
+ rules_detected: string[]
94
+ recommended_action: string
95
+ }
96
+ }
97
+
98
+ export interface DriftOutputMetadata {
99
+ $schema: string
100
+ toolVersion: string
101
+ }
102
+
103
+ export type DriftReportJson = DriftReport & DriftOutputMetadata
104
+
105
+ export type AIOutputJson = AIOutput & DriftOutputMetadata