@eduardbar/drift 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/.gga +50 -0
  2. package/.github/actions/drift-review/README.md +60 -0
  3. package/.github/actions/drift-review/action.yml +131 -0
  4. package/.github/actions/drift-scan/README.md +28 -32
  5. package/.github/actions/drift-scan/action.yml +78 -14
  6. package/.github/workflows/review-pr.yml +34 -41
  7. package/AGENTS.md +75 -251
  8. package/CHANGELOG.md +28 -0
  9. package/README.md +148 -41
  10. package/dist/benchmark.d.ts +1 -1
  11. package/dist/benchmark.js +71 -52
  12. package/dist/cli.js +243 -8
  13. package/dist/config.js +16 -2
  14. package/dist/diff.js +42 -50
  15. package/dist/doctor.d.ts +5 -0
  16. package/dist/doctor.js +133 -0
  17. package/dist/format.d.ts +17 -0
  18. package/dist/format.js +45 -0
  19. package/dist/guard-types.d.ts +57 -0
  20. package/dist/guard-types.js +2 -0
  21. package/dist/guard.d.ts +14 -0
  22. package/dist/guard.js +239 -0
  23. package/dist/index.d.ts +10 -3
  24. package/dist/index.js +4 -1
  25. package/dist/init.d.ts +15 -0
  26. package/dist/init.js +273 -0
  27. package/dist/map-cycles.d.ts +2 -0
  28. package/dist/map-cycles.js +34 -0
  29. package/dist/map-svg.d.ts +19 -0
  30. package/dist/map-svg.js +97 -0
  31. package/dist/map.js +78 -138
  32. package/dist/metrics.js +70 -55
  33. package/dist/output-metadata.d.ts +13 -0
  34. package/dist/output-metadata.js +17 -0
  35. package/dist/plugins-capabilities.d.ts +4 -0
  36. package/dist/plugins-capabilities.js +21 -0
  37. package/dist/plugins-messages.d.ts +10 -0
  38. package/dist/plugins-messages.js +16 -0
  39. package/dist/plugins-rules.d.ts +9 -0
  40. package/dist/plugins-rules.js +137 -0
  41. package/dist/plugins.d.ts +1 -1
  42. package/dist/plugins.js +45 -142
  43. package/dist/reporter-constants.d.ts +16 -0
  44. package/dist/reporter-constants.js +39 -0
  45. package/dist/reporter.d.ts +3 -3
  46. package/dist/reporter.js +35 -55
  47. package/dist/review.d.ts +2 -1
  48. package/dist/review.js +2 -1
  49. package/dist/rules/phase3-configurable.js +23 -15
  50. package/dist/saas/constants.d.ts +15 -0
  51. package/dist/saas/constants.js +48 -0
  52. package/dist/saas/dashboard.d.ts +8 -0
  53. package/dist/saas/dashboard.js +132 -0
  54. package/dist/saas/errors.d.ts +19 -0
  55. package/dist/saas/errors.js +37 -0
  56. package/dist/saas/helpers.d.ts +21 -0
  57. package/dist/saas/helpers.js +110 -0
  58. package/dist/saas/ingest.d.ts +3 -0
  59. package/dist/saas/ingest.js +249 -0
  60. package/dist/saas/organization.d.ts +5 -0
  61. package/dist/saas/organization.js +82 -0
  62. package/dist/saas/plan-change.d.ts +10 -0
  63. package/dist/saas/plan-change.js +15 -0
  64. package/dist/saas/store.d.ts +21 -0
  65. package/dist/saas/store.js +159 -0
  66. package/dist/saas/types.d.ts +191 -0
  67. package/dist/saas/types.js +2 -0
  68. package/dist/saas.d.ts +8 -218
  69. package/dist/saas.js +7 -761
  70. package/dist/sarif.d.ts +74 -0
  71. package/dist/sarif.js +122 -0
  72. package/dist/trust-advanced.d.ts +14 -0
  73. package/dist/trust-advanced.js +65 -0
  74. package/dist/trust-kpi-fs.d.ts +3 -0
  75. package/dist/trust-kpi-fs.js +141 -0
  76. package/dist/trust-kpi-parse.d.ts +7 -0
  77. package/dist/trust-kpi-parse.js +186 -0
  78. package/dist/trust-kpi-types.d.ts +16 -0
  79. package/dist/trust-kpi-types.js +2 -0
  80. package/dist/trust-kpi.d.ts +1 -3
  81. package/dist/trust-kpi.js +6 -266
  82. package/dist/trust-policy.d.ts +32 -0
  83. package/dist/trust-policy.js +160 -0
  84. package/dist/trust-render.d.ts +9 -0
  85. package/dist/trust-render.js +54 -0
  86. package/dist/trust-scoring.d.ts +9 -0
  87. package/dist/trust-scoring.js +208 -0
  88. package/dist/trust.d.ts +4 -32
  89. package/dist/trust.js +29 -432
  90. package/dist/types/app.d.ts +30 -0
  91. package/dist/types/app.js +2 -0
  92. package/dist/types/config.d.ts +25 -0
  93. package/dist/types/config.js +2 -0
  94. package/dist/types/core.d.ts +100 -0
  95. package/dist/types/core.js +2 -0
  96. package/dist/types/diff.d.ts +55 -0
  97. package/dist/types/diff.js +2 -0
  98. package/dist/types/plugin.d.ts +41 -0
  99. package/dist/types/plugin.js +2 -0
  100. package/dist/types/trust.d.ts +120 -0
  101. package/dist/types/trust.js +2 -0
  102. package/dist/types.d.ts +8 -365
  103. package/docs/release-notes-draft.md +40 -0
  104. package/docs/rules-catalog.md +49 -0
  105. package/docs/trust-core-release-checklist.md +37 -5
  106. package/package.json +3 -2
  107. package/packages/vscode-drift/src/code-actions.ts +1 -1
  108. package/schemas/drift-ai-output.v1.json +162 -0
  109. package/schemas/drift-report.v1.json +151 -0
  110. package/schemas/drift-trust.v1.json +131 -0
  111. package/scripts/smoke-repo.mjs +394 -0
  112. package/src/benchmark.ts +75 -53
  113. package/src/cli.ts +285 -13
  114. package/src/config.ts +19 -2
  115. package/src/diff.ts +57 -48
  116. package/src/doctor.ts +173 -0
  117. package/src/format.ts +81 -0
  118. package/src/guard-types.ts +64 -0
  119. package/src/guard.ts +324 -0
  120. package/src/index.ts +35 -0
  121. package/src/init.ts +298 -0
  122. package/src/map-cycles.ts +38 -0
  123. package/src/map-svg.ts +124 -0
  124. package/src/map.ts +111 -142
  125. package/src/metrics.ts +78 -59
  126. package/src/output-metadata.ts +30 -0
  127. package/src/plugins-capabilities.ts +36 -0
  128. package/src/plugins-messages.ts +35 -0
  129. package/src/plugins-rules.ts +296 -0
  130. package/src/plugins.ts +76 -283
  131. package/src/reporter-constants.ts +46 -0
  132. package/src/reporter.ts +64 -65
  133. package/src/review.ts +4 -2
  134. package/src/rules/phase3-configurable.ts +39 -26
  135. package/src/saas/constants.ts +56 -0
  136. package/src/saas/dashboard.ts +172 -0
  137. package/src/saas/errors.ts +45 -0
  138. package/src/saas/helpers.ts +140 -0
  139. package/src/saas/ingest.ts +278 -0
  140. package/src/saas/organization.ts +99 -0
  141. package/src/saas/plan-change.ts +19 -0
  142. package/src/saas/store.ts +172 -0
  143. package/src/saas/types.ts +216 -0
  144. package/src/saas.ts +49 -1031
  145. package/src/sarif.ts +232 -0
  146. package/src/trust-advanced.ts +99 -0
  147. package/src/trust-kpi-fs.ts +169 -0
  148. package/src/trust-kpi-parse.ts +219 -0
  149. package/src/trust-kpi-types.ts +19 -0
  150. package/src/trust-kpi.ts +8 -316
  151. package/src/trust-policy.ts +246 -0
  152. package/src/trust-render.ts +61 -0
  153. package/src/trust-scoring.ts +231 -0
  154. package/src/trust.ts +62 -576
  155. package/src/types/app.ts +30 -0
  156. package/src/types/config.ts +27 -0
  157. package/src/types/core.ts +105 -0
  158. package/src/types/diff.ts +61 -0
  159. package/src/types/plugin.ts +46 -0
  160. package/src/types/trust.ts +134 -0
  161. package/src/types.ts +78 -409
  162. package/tests/cli-sarif.test.ts +92 -0
  163. package/tests/format.test.ts +157 -0
  164. package/tests/new-features.test.ts +10 -2
  165. package/tests/phase1-init-doctor-guard.test.ts +199 -0
  166. package/tests/sarif.test.ts +160 -0
  167. package/tests/trust-kpi.test.ts +31 -4
  168. package/tests/trust.test.ts +18 -0
package/src/trust.ts CHANGED
@@ -1,51 +1,48 @@
1
- import { RULE_WEIGHTS } from './analyzer.js'
2
1
  import type {
3
- DriftConfig,
4
2
  DriftDiff,
5
3
  DriftReport,
6
4
  DriftTrustReport,
5
+ DriftTrustReportJson,
7
6
  MergeRiskLevel,
8
- TrustGatePolicyPack,
9
- TrustGatePolicyPreset,
10
- TrustDiffContext,
11
- TrustFixPriority,
12
- TrustReason,
13
- TrustAdvancedComparison,
14
7
  } from './types.js'
15
8
  import type { SnapshotEntry } from './snapshot.js'
16
-
17
- const ARCHITECTURE_RULES = new Set([
18
- 'circular-dependency',
19
- 'layer-violation',
20
- 'cross-boundary-import',
21
- 'controller-no-db',
22
- 'service-no-http',
23
- ])
24
-
25
- const RULE_SUGGESTIONS: Record<string, string> = {
26
- 'circular-dependency': 'Break cycles first to reduce hidden merge blast radius.',
27
- 'layer-violation': 'Fix layer violations to keep architecture boundaries enforceable.',
28
- 'high-complexity': 'Split branch-heavy functions before adding more logic.',
29
- 'deep-nesting': 'Flatten control flow with early returns.',
30
- 'large-file': 'Split monolithic files by responsibility before merge.',
31
- 'large-function': 'Extract smaller functions to reduce review complexity.',
32
- 'catch-swallow': 'Handle or rethrow swallowed errors to avoid silent failures.',
33
- 'debug-leftover': 'Remove debug leftovers from production paths.',
34
- 'semantic-duplication': 'Consolidate duplicated logic to prevent divergent fixes.',
35
- 'dead-file': 'Delete or wire dead files to avoid stale merge artifacts.',
36
- }
37
-
38
- const SYSTEMIC_RULES = new Set([
39
- 'circular-dependency',
40
- 'layer-violation',
41
- 'cross-boundary-import',
42
- 'unused-export',
43
- 'unused-dependency',
44
- 'dead-file',
45
- 'semantic-duplication',
46
- 'controller-no-db',
47
- 'service-no-http',
48
- ])
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'
49
46
 
50
47
  interface BuildTrustOptions {
51
48
  diff?: DriftDiff
@@ -61,39 +58,6 @@ interface TrustRenderOptions {
61
58
  markdown?: boolean
62
59
  }
63
60
 
64
- function formatTrustGatePolicyValues(values: TrustGateOptions): string {
65
- const enabled = typeof values.enabled === 'boolean' ? String(values.enabled) : 'inherit'
66
- const minTrust = typeof values.minTrust === 'number' ? String(values.minTrust) : 'inherit'
67
- const maxRisk = values.maxRisk ?? 'inherit'
68
- return `enabled=${enabled} minTrust=${minTrust} maxRisk=${maxRisk}`
69
- }
70
-
71
- export interface TrustGateOptions {
72
- enabled?: boolean
73
- minTrust?: number
74
- maxRisk?: MergeRiskLevel
75
- }
76
-
77
- export interface TrustGatePolicyResolutionOptions {
78
- branchName?: string
79
- policyPack?: string
80
- overrides?: TrustGateOptions
81
- }
82
-
83
- export interface TrustGatePolicyResolutionStep {
84
- source: 'base' | 'policy-pack' | 'branch-preset' | 'overrides'
85
- name: string
86
- values: TrustGateOptions
87
- }
88
-
89
- export interface TrustGatePolicyExplanation {
90
- effectivePolicy: TrustGateOptions
91
- branchName?: string
92
- selectedPolicyPack?: string
93
- invalidPolicyPack?: string
94
- steps: TrustGatePolicyResolutionStep[]
95
- }
96
-
97
61
  export interface TrustGateEvaluation {
98
62
  shouldFail: boolean
99
63
  reasons: string[]
@@ -106,474 +70,34 @@ export interface TrustGateEvaluation {
106
70
  }
107
71
  }
108
72
 
109
- export const MERGE_RISK_ORDER: MergeRiskLevel[] = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']
110
-
111
- const BRANCH_ENV_CANDIDATES = [
112
- 'DRIFT_BRANCH',
113
- 'GITHUB_HEAD_REF',
114
- 'GITHUB_REF_NAME',
115
- 'CI_COMMIT_REF_NAME',
116
- 'BRANCH_NAME',
117
- ] as const
118
-
119
- export function normalizeMergeRiskLevel(value: string): MergeRiskLevel | undefined {
120
- const normalized = value.toUpperCase()
121
- return MERGE_RISK_ORDER.find((level) => level === normalized)
122
- }
123
-
124
- function branchPatternToRegExp(pattern: string): RegExp {
125
- const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, '\\$&').replace(/\*/g, '.*')
126
- return new RegExp(`^${escaped}$`)
127
- }
128
-
129
- function patternSpecificity(pattern: string): number {
130
- const wildcardCount = (pattern.match(/\*/g) ?? []).length
131
- const staticChars = pattern.replace(/\*/g, '').length
132
- const exactBoost = wildcardCount === 0 ? 10_000 : 0
133
- return exactBoost + staticChars * 10 - wildcardCount
134
- }
135
-
136
- function resolvePresetsForBranch(
137
- branchName: string,
138
- presets: TrustGatePolicyPreset[] | undefined,
139
- ): TrustGatePolicyPreset[] {
140
- if (!presets || presets.length === 0) return []
141
-
142
- const matched: Array<{ preset: TrustGatePolicyPreset; specificity: number; index: number }> = []
143
-
144
- for (let index = 0; index < presets.length; index += 1) {
145
- const preset = presets[index]
146
- if (!preset?.branch) continue
147
-
148
- const regex = branchPatternToRegExp(preset.branch)
149
- if (!regex.test(branchName)) continue
150
-
151
- matched.push({ preset, specificity: patternSpecificity(preset.branch), index })
152
- }
153
-
154
- matched.sort((a, b) => a.specificity - b.specificity || a.index - b.index)
155
- return matched.map((entry) => entry.preset)
156
- }
157
-
158
- function normalizeMinTrust(value: unknown): number | undefined {
159
- return typeof value === 'number' && !Number.isNaN(value) ? value : undefined
160
- }
161
-
162
- function normalizeMaxRisk(value: unknown): MergeRiskLevel | undefined {
163
- if (typeof value !== 'string') return undefined
164
- return normalizeMergeRiskLevel(value)
165
- }
166
-
167
- function normalizeTrustGateOptions(
168
- source: { enabled?: unknown; minTrust?: unknown; maxRisk?: unknown } | undefined,
169
- ): TrustGateOptions {
170
- if (!source) return {}
171
-
172
- return {
173
- enabled: typeof source.enabled === 'boolean' ? source.enabled : undefined,
174
- minTrust: normalizeMinTrust(source.minTrust),
175
- maxRisk: normalizeMaxRisk(source.maxRisk),
176
- }
177
- }
178
-
179
- function mergeTrustGateOptions(base: TrustGateOptions, layer: TrustGateOptions): TrustGateOptions {
180
- return {
181
- enabled: typeof layer.enabled === 'boolean' ? layer.enabled : base.enabled,
182
- minTrust: layer.minTrust ?? base.minTrust,
183
- maxRisk: layer.maxRisk ?? base.maxRisk,
184
- }
185
- }
186
-
187
- function normalizeResolutionOptions(
188
- branchNameOrOptions?: string | TrustGatePolicyResolutionOptions,
189
- explicitOverrides?: TrustGateOptions,
190
- ): TrustGatePolicyResolutionOptions {
191
- if (typeof branchNameOrOptions === 'string') {
192
- return {
193
- branchName: branchNameOrOptions,
194
- overrides: explicitOverrides,
195
- }
196
- }
197
-
198
- if (!branchNameOrOptions) {
199
- return {
200
- overrides: explicitOverrides,
201
- }
202
- }
203
-
204
- return {
205
- ...branchNameOrOptions,
206
- overrides: explicitOverrides
207
- ? mergeTrustGateOptions(normalizeTrustGateOptions(branchNameOrOptions.overrides), normalizeTrustGateOptions(explicitOverrides))
208
- : branchNameOrOptions.overrides,
209
- }
210
- }
211
-
212
- function resolvePolicyPack(
213
- policyPacks: Record<string, TrustGatePolicyPack> | undefined,
214
- policyPackName: string | undefined,
215
- ): { name?: string; pack?: TrustGatePolicyPack; invalid?: string } {
216
- const normalizedName = policyPackName?.trim()
217
- if (!normalizedName) return {}
218
-
219
- if (!policyPacks) {
220
- return { name: normalizedName, invalid: normalizedName }
221
- }
222
-
223
- const pack = policyPacks[normalizedName]
224
- if (!pack) {
225
- return { name: normalizedName, invalid: normalizedName }
226
- }
227
-
228
- return { name: normalizedName, pack }
229
- }
230
-
231
- export function detectBranchName(env: NodeJS.ProcessEnv = process.env): string | undefined {
232
- for (const key of BRANCH_ENV_CANDIDATES) {
233
- const value = env[key]?.trim()
234
- if (value) return value
235
- }
236
-
237
- return undefined
238
- }
239
-
240
- export function explainTrustGatePolicy(
241
- config: DriftConfig | undefined,
242
- branchName?: string,
243
- overrides?: TrustGateOptions,
244
- ): TrustGatePolicyExplanation
245
- export function explainTrustGatePolicy(
246
- config: DriftConfig | undefined,
247
- options?: TrustGatePolicyResolutionOptions,
248
- ): TrustGatePolicyExplanation
249
- export function explainTrustGatePolicy(
250
- config: DriftConfig | undefined,
251
- branchNameOrOptions?: string | TrustGatePolicyResolutionOptions,
252
- explicitOverrides?: TrustGateOptions,
253
- ): TrustGatePolicyExplanation {
254
- const policy = config?.trustGate
255
- const resolution = normalizeResolutionOptions(branchNameOrOptions, explicitOverrides)
256
- const normalizedBranch = resolution.branchName?.trim()
257
- const packResolution = resolvePolicyPack(policy?.policyPacks, resolution.policyPack)
258
-
259
- const steps: TrustGatePolicyResolutionStep[] = []
260
- const base = normalizeTrustGateOptions(policy)
261
- let effective = base
262
- steps.push({ source: 'base', name: 'trustGate', values: base })
263
-
264
- if (packResolution.pack) {
265
- const packOptions = normalizeTrustGateOptions(packResolution.pack)
266
- effective = mergeTrustGateOptions(effective, packOptions)
267
- steps.push({ source: 'policy-pack', name: packResolution.name ?? 'unknown', values: packOptions })
268
- }
269
-
270
- if (normalizedBranch) {
271
- const matchedPresets = resolvePresetsForBranch(normalizedBranch, policy?.presets)
272
- for (const preset of matchedPresets) {
273
- const presetOptions = normalizeTrustGateOptions(preset)
274
- effective = mergeTrustGateOptions(effective, presetOptions)
275
- steps.push({ source: 'branch-preset', name: preset.branch, values: presetOptions })
276
- }
277
- }
278
-
279
- const overrides = normalizeTrustGateOptions(resolution.overrides)
280
- if (Object.values(overrides).some((value) => value !== undefined)) {
281
- effective = mergeTrustGateOptions(effective, overrides)
282
- steps.push({ source: 'overrides', name: 'cli', values: overrides })
283
- }
284
-
285
- return {
286
- effectivePolicy: effective,
287
- branchName: normalizedBranch,
288
- selectedPolicyPack: packResolution.name,
289
- invalidPolicyPack: packResolution.invalid,
290
- steps,
291
- }
292
- }
293
-
294
- export function resolveTrustGatePolicy(
295
- config: DriftConfig | undefined,
296
- branchName?: string,
297
- overrides?: TrustGateOptions,
298
- ): TrustGateOptions
299
- export function resolveTrustGatePolicy(
300
- config: DriftConfig | undefined,
301
- options?: TrustGatePolicyResolutionOptions,
302
- ): TrustGateOptions
303
- export function resolveTrustGatePolicy(
304
- config: DriftConfig | undefined,
305
- branchNameOrOptions?: string | TrustGatePolicyResolutionOptions,
306
- explicitOverrides?: TrustGateOptions,
307
- ): TrustGateOptions {
308
- const options = normalizeResolutionOptions(branchNameOrOptions, explicitOverrides)
309
- return explainTrustGatePolicy(config, options).effectivePolicy
310
- }
311
-
312
- export function formatTrustGatePolicyExplanation(explanation: TrustGatePolicyExplanation): string {
313
- const lines = ['Trust gate policy resolution:']
314
- lines.push(`- branch: ${explanation.branchName ?? 'not provided'}`)
315
- lines.push(`- policy pack: ${explanation.selectedPolicyPack ?? 'not selected'}`)
316
- if (explanation.invalidPolicyPack) {
317
- lines.push(`- invalid policy pack: ${explanation.invalidPolicyPack}`)
318
- }
319
- lines.push('- steps:')
320
-
321
- for (const [index, step] of explanation.steps.entries()) {
322
- lines.push(` ${index + 1}. ${step.source} (${step.name}): ${formatTrustGatePolicyValues(step.values)}`)
323
- }
324
-
325
- lines.push(`- effective: ${formatTrustGatePolicyValues(explanation.effectivePolicy)}`)
326
- return lines.join('\n')
327
- }
328
-
329
- function clamp(value: number, min: number, max: number): number {
330
- return Math.max(min, Math.min(max, value))
331
- }
332
-
333
- function toMergeRisk(trustScore: number): MergeRiskLevel {
334
- if (trustScore >= 80) return 'LOW'
335
- if (trustScore >= 60) return 'MEDIUM'
336
- if (trustScore >= 40) return 'HIGH'
337
- return 'CRITICAL'
338
- }
339
-
340
- function computeReasons(report: DriftReport): TrustReason[] {
341
- const architectureIssues = Object.entries(report.summary.byRule)
342
- .filter(([rule]) => ARCHITECTURE_RULES.has(rule))
343
- .reduce((sum, [, count]) => sum + count, 0)
344
-
345
- const worstHotspot = report.maintenanceRisk.hotspots[0]
346
- const reasons: TrustReason[] = [
347
- {
348
- label: 'Drift score pressure',
349
- detail: `Repository drift score is ${report.totalScore}/100.`,
350
- impact: Math.round(report.totalScore * 0.55),
351
- },
352
- {
353
- label: 'Error-level issues',
354
- detail: `${report.summary.errors} error issue(s) increase merge volatility.`,
355
- impact: Math.min(22, report.summary.errors * 4),
356
- },
357
- {
358
- label: 'Architecture signals',
359
- detail: `${architectureIssues} architecture-related issue(s) detected.`,
360
- impact: Math.min(24, architectureIssues * 6),
361
- },
362
- {
363
- label: 'Maintenance hotspots',
364
- detail: `Maintenance risk is ${report.maintenanceRisk.level.toUpperCase()} (${report.maintenanceRisk.score}/100).`,
365
- impact: Math.min(25, Math.round(report.maintenanceRisk.score * 0.25)),
366
- },
367
- {
368
- label: 'Highest-risk file',
369
- detail: worstHotspot
370
- ? `${worstHotspot.file} has hotspot risk ${worstHotspot.risk}/100.`
371
- : 'No hotspot concentration detected.',
372
- impact: worstHotspot ? Math.min(15, Math.round(worstHotspot.risk * 0.15)) : 0,
373
- },
374
- ]
375
-
376
- return reasons
377
- .filter((reason) => reason.impact > 0)
378
- .sort((a, b) => b.impact - a.impact)
379
- .slice(0, 4)
380
- }
381
-
382
- function effortFromWeight(weight: number): 'low' | 'medium' | 'high' {
383
- if (weight <= 6) return 'low'
384
- if (weight <= 12) return 'medium'
385
- return 'high'
386
- }
387
-
388
- function computeDiffContext(diff: DriftDiff): TrustDiffContext {
389
- const scoreRegressionPenalty = Math.max(0, diff.totalDelta) * 2
390
- const newIssuePenalty = diff.newIssuesCount * 3
391
- const churnPenalty = diff.files.length >= 15 ? 4 : 0
392
- const penalty = clamp(scoreRegressionPenalty + newIssuePenalty + churnPenalty, 0, 30)
393
-
394
- const scoreImprovementBonus = Math.max(0, -diff.totalDelta) * 2
395
- const resolvedIssueBonus = diff.resolvedIssuesCount * 2
396
- const bonus = clamp(scoreImprovementBonus + resolvedIssueBonus, 0, 20)
397
-
398
- const netImpact = penalty - bonus
399
- const status = netImpact > 0 ? 'regressed' : netImpact < 0 ? 'improved' : 'neutral'
400
-
401
- return {
402
- baseRef: diff.baseRef,
403
- status,
404
- scoreDelta: diff.totalDelta,
405
- newIssues: diff.newIssuesCount,
406
- resolvedIssues: diff.resolvedIssuesCount,
407
- filesChanged: diff.files.length,
408
- penalty,
409
- bonus,
410
- netImpact,
411
- }
412
- }
413
-
414
- function confidenceFromPrioritySignals(
415
- occurrences: number,
416
- severity: 'error' | 'warning' | 'info',
417
- systemic: boolean,
418
- ): 'low' | 'medium' | 'high' {
419
- const severityScore = severity === 'error' ? 4 : severity === 'warning' ? 2 : 1
420
- const systemicScore = systemic ? 2 : 0
421
- const score = occurrences * 2 + severityScore + systemicScore
422
-
423
- if (score >= 12) return 'high'
424
- if (score >= 7) return 'medium'
425
- return 'low'
426
- }
427
-
428
- function computeFixPriorities(report: DriftReport, advancedMode = false): TrustFixPriority[] {
429
- const ordered = Object.entries(report.summary.byRule)
430
- .map(([rule, occurrences]) => {
431
- const weightConfig = RULE_WEIGHTS[rule] ?? { severity: 'warning' as const, weight: 6 }
432
- const severityBoost = weightConfig.severity === 'error' ? 25 : weightConfig.severity === 'warning' ? 12 : 4
433
- const systemic = SYSTEMIC_RULES.has(rule)
434
- const systemicBoost = advancedMode && systemic ? 25 : 0
435
- const priorityScore = occurrences * weightConfig.weight + severityBoost + systemicBoost
436
- const confidence = confidenceFromPrioritySignals(occurrences, weightConfig.severity, systemic)
437
- const explanation = advancedMode
438
- ? systemic
439
- ? 'System-level rule that propagates risk across multiple teams and modules.'
440
- : 'Local rule with contained impact; treat as team-level cleanup after systemic fixes.'
441
- : undefined
442
-
443
- return {
444
- rule,
445
- severity: weightConfig.severity,
446
- occurrences,
447
- systemic,
448
- priorityScore,
449
- estimatedTrustGain: Math.min(30, Math.max(3, Math.round(priorityScore / 4))),
450
- effort: effortFromWeight(weightConfig.weight),
451
- suggestion: RULE_SUGGESTIONS[rule] ?? 'Address this rule in the highest-scored files first.',
452
- confidence,
453
- explanation,
454
- }
455
- })
456
- .sort((a, b) => b.priorityScore - a.priorityScore)
457
- .slice(0, 5)
458
-
459
- return ordered.map((item, index) => ({
460
- rank: index + 1,
461
- rule: item.rule,
462
- severity: item.severity,
463
- occurrences: item.occurrences,
464
- estimated_trust_gain: item.estimatedTrustGain,
465
- effort: item.effort,
466
- suggestion: item.suggestion,
467
- ...(advancedMode ? { confidence: item.confidence, explanation: item.explanation, systemic: item.systemic } : {}),
468
- }))
469
- }
470
-
471
- function buildComparisonFromPreviousTrust(
472
- trustScore: number,
473
- previousTrust: Partial<DriftTrustReport> | undefined,
474
- ): TrustAdvancedComparison | undefined {
475
- if (!previousTrust || typeof previousTrust.trust_score !== 'number') return undefined
476
-
477
- const trustDelta = trustScore - previousTrust.trust_score
478
- const trend = trustDelta > 0 ? 'improving' : trustDelta < 0 ? 'regressing' : 'stable'
479
-
480
- return {
481
- source: 'previous-trust-json',
482
- trend,
483
- summary: `Trust moved ${trustDelta >= 0 ? '+' : ''}${trustDelta} vs provided previous trust JSON.`,
484
- trust_delta: trustDelta,
485
- previous_trust_score: previousTrust.trust_score,
486
- previous_merge_risk: previousTrust.merge_risk,
487
- }
488
- }
489
-
490
- function buildComparisonFromSnapshotHistory(
491
- report: DriftReport,
492
- snapshots: SnapshotEntry[] | undefined,
493
- ): TrustAdvancedComparison | undefined {
494
- const lastSnapshot = snapshots && snapshots.length > 0 ? snapshots[snapshots.length - 1] : undefined
495
- if (!lastSnapshot) return undefined
496
-
497
- const snapshotScoreDelta = report.totalScore - lastSnapshot.score
498
- const trend = snapshotScoreDelta < 0 ? 'improving' : snapshotScoreDelta > 0 ? 'regressing' : 'stable'
499
- const snapshotContext = lastSnapshot.label
500
- ? `${lastSnapshot.timestamp} (${lastSnapshot.label})`
501
- : lastSnapshot.timestamp
502
-
503
- return {
504
- source: 'snapshot-history',
505
- trend,
506
- summary: `Drift score moved ${snapshotScoreDelta >= 0 ? '+' : ''}${snapshotScoreDelta} vs snapshot ${snapshotContext}.`,
507
- snapshot_score_delta: snapshotScoreDelta,
508
- snapshot_label: lastSnapshot.label || undefined,
509
- snapshot_timestamp: lastSnapshot.timestamp,
510
- }
511
- }
512
-
513
- function buildTeamGuidance(
514
- priorities: TrustFixPriority[],
515
- comparison: TrustAdvancedComparison | undefined,
516
- diffContext: TrustDiffContext | undefined,
517
- ): string[] {
518
- const systemicTargets = priorities
519
- .filter((priority) => priority.systemic)
520
- .slice(0, 2)
521
- .map((priority) => `${priority.rule} (x${priority.occurrences})`)
522
-
523
- const guidance: string[] = []
524
- if (systemicTargets.length > 0) {
525
- guidance.push(`Start with systemic rules: ${systemicTargets.join(', ')}.`)
526
- }
527
-
528
- if (comparison?.trend === 'regressing') {
529
- guidance.push('Trend regressed; freeze net-new debt in CI and assign owners per systemic rule.')
530
- }
531
-
532
- if (diffContext && diffContext.newIssues > 0) {
533
- guidance.push(`Block net-new issue growth first (+${diffContext.newIssues} new issue(s) in diff context).`)
534
- }
535
-
536
- if (guidance.length === 0) {
537
- guidance.push('Maintain current baseline and schedule periodic systemic debt cleanup by rule ownership.')
538
- }
539
-
540
- return guidance.slice(0, 3)
541
- }
73
+ const CONSOLE_DIFF_INSERT_INDEX = 5
542
74
 
543
75
  export function buildTrustReport(report: DriftReport, options?: BuildTrustOptions): DriftTrustReport {
544
76
  const reasons = computeReasons(report)
545
- const advancedMode = options?.advanced?.enabled === true
546
77
 
547
78
  const diffContext = options?.diff ? computeDiffContext(options.diff) : undefined
548
79
  if (diffContext && diffContext.netImpact > 0) {
549
- reasons.push({
550
- label: 'Diff regression signals',
551
- detail: `Against ${diffContext.baseRef}: score delta ${diffContext.scoreDelta >= 0 ? '+' : ''}${diffContext.scoreDelta}, +${diffContext.newIssues} new issue(s), -${diffContext.resolvedIssues} resolved.`,
552
- impact: diffContext.netImpact,
553
- })
80
+ reasons.push(buildDiffRegressionReason(diffContext))
554
81
  }
555
82
 
556
83
  const rankedReasons = reasons
557
84
  .filter((reason) => reason.impact > 0)
558
85
  .sort((a, b) => b.impact - a.impact)
559
- .slice(0, 4)
86
+ .slice(0, TOP_REASONS_SLICE)
560
87
 
561
88
  const totalPenalty = rankedReasons.reduce((sum, reason) => sum + reason.impact, 0)
562
89
  const totalBonus = diffContext && diffContext.netImpact < 0 ? Math.abs(diffContext.netImpact) : 0
563
90
  const trustScore = clamp(Math.round(100 - totalPenalty + totalBonus), 0, 100)
564
91
 
565
- const comparison = advancedMode
566
- ? buildComparisonFromPreviousTrust(trustScore, options?.advanced?.previousTrust)
567
- ?? buildComparisonFromSnapshotHistory(report, options?.advanced?.snapshots)
568
- : undefined
569
-
92
+ const advancedMode = options?.advanced?.enabled === true
570
93
  const fixPriorities = computeFixPriorities(report, advancedMode)
571
- const advancedContext = advancedMode
572
- ? {
573
- comparison,
574
- team_guidance: buildTeamGuidance(fixPriorities, comparison, diffContext),
575
- }
576
- : undefined
94
+ const advancedContext = buildAdvancedContext({
95
+ report,
96
+ advancedOptions: options?.advanced,
97
+ trustScore,
98
+ fixPriorities,
99
+ diffContext,
100
+ })
577
101
 
578
102
  return {
579
103
  scannedAt: new Date().toISOString(),
@@ -599,17 +123,8 @@ export function formatTrustConsole(trust: DriftTrustReport): string {
599
123
  ].join('\n')
600
124
  : undefined
601
125
 
602
- const reasons = trust.top_reasons.length === 0
603
- ? '- none'
604
- : trust.top_reasons.map((reason) => `- ${reason.label}: ${reason.detail} (impact ${reason.impact})`).join('\n')
605
-
606
- const priorities = trust.fix_priorities.length === 0
607
- ? '- none'
608
- : trust.fix_priorities
609
- .map((priority) =>
610
- `- #${priority.rank} ${priority.rule} (${priority.severity}, x${priority.occurrences}${priority.confidence ? `, confidence ${priority.confidence}` : ''}): ${priority.suggestion}`
611
- )
612
- .join('\n')
126
+ const reasons = renderTrustReasons(trust.top_reasons)
127
+ const priorities = renderTrustPriorities(trust.fix_priorities)
613
128
 
614
129
  const advanced = trust.advanced_context
615
130
  const advancedComparison = advanced?.comparison
@@ -637,7 +152,7 @@ export function formatTrustConsole(trust: DriftTrustReport): string {
637
152
  ]
638
153
 
639
154
  if (diffLines) {
640
- sections.splice(5, 0, 'Diff Context:', diffLines, '')
155
+ sections.splice(CONSOLE_DIFF_INSERT_INDEX, 0, 'Diff Context:', diffLines, '')
641
156
  }
642
157
 
643
158
  if (advanced) {
@@ -648,44 +163,11 @@ export function formatTrustConsole(trust: DriftTrustReport): string {
648
163
  }
649
164
 
650
165
  export function formatTrustMarkdown(trust: DriftTrustReport): string {
651
- const diffContext = trust.diff_context
652
-
653
- const reasons = trust.top_reasons.length === 0
654
- ? '- none'
655
- : trust.top_reasons.map((reason) => `- **${reason.label}**: ${reason.detail} (impact ${reason.impact})`).join('\n')
656
-
657
- const priorities = trust.fix_priorities.length === 0
658
- ? '- none'
659
- : trust.fix_priorities
660
- .map((priority) =>
661
- `- #${priority.rank} \`${priority.rule}\` (${priority.severity}, x${priority.occurrences}, effort: ${priority.effort}${priority.confidence ? `, confidence: ${priority.confidence}` : ''}) - ${priority.suggestion}${priority.explanation ? ` ${priority.explanation}` : ''}`
662
- )
663
- .join('\n')
664
-
665
- const diffBlock = !diffContext
666
- ? [
667
- '- Base ref: not provided',
668
- '- Diff-aware adjustment: not applied',
669
- ].join('\n')
670
- : [
671
- `- Base ref: \`${diffContext.baseRef}\``,
672
- `- Diff status: **${diffContext.status.toUpperCase()}**`,
673
- `- Score delta: **${diffContext.scoreDelta >= 0 ? '+' : ''}${diffContext.scoreDelta}**`,
674
- `- Issues: **+${diffContext.newIssues}** new / **-${diffContext.resolvedIssues}** resolved`,
675
- `- Trust adjustment: **+${diffContext.penalty}** penalty / **-${diffContext.bonus}** bonus (net ${diffContext.netImpact >= 0 ? '+' : ''}${diffContext.netImpact})`,
676
- ].join('\n')
677
-
678
- const advancedComparison = trust.advanced_context?.comparison
679
- ? [
680
- `- Source: \`${trust.advanced_context.comparison.source}\``,
681
- `- Trend: **${trust.advanced_context.comparison.trend.toUpperCase()}**`,
682
- `- Summary: ${trust.advanced_context.comparison.summary}`,
683
- ].join('\n')
684
- : '- Historical comparison not available'
685
-
686
- const advancedGuidance = trust.advanced_context?.team_guidance?.length
687
- ? trust.advanced_context.team_guidance.map((item) => `- ${item}`).join('\n')
688
- : '- none'
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)
689
171
 
690
172
  const sections = [
691
173
  '## drift trust',
@@ -711,8 +193,12 @@ export function formatTrustMarkdown(trust: DriftTrustReport): string {
711
193
  return sections.join('\n')
712
194
  }
713
195
 
196
+ function formatTrustJsonObject(trust: DriftTrustReport): DriftTrustReportJson {
197
+ return withOutputMetadata(trust, OUTPUT_SCHEMA.trust)
198
+ }
199
+
714
200
  export function formatTrustJson(trust: DriftTrustReport): string {
715
- return JSON.stringify(trust, null, 2)
201
+ return JSON.stringify(formatTrustJsonObject(trust), null, 2)
716
202
  }
717
203
 
718
204
  export function renderTrustOutput(trust: DriftTrustReport, options?: TrustRenderOptions): string {