@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,19 @@
1
+ import type { MergeRiskLevel, TrustDiffContext } from './types.js'
2
+
3
+ export interface ParsedTrustArtifact {
4
+ filePath: string
5
+ trustScore: number
6
+ mergeRisk: MergeRiskLevel
7
+ diffContext?: TrustDiffContext
8
+ }
9
+
10
+ export interface DiscoverResult {
11
+ files: string[]
12
+ diagnostics: import('./types.js').TrustKpiDiagnostic[]
13
+ }
14
+
15
+ export type DiffStatus = 'improved' | 'regressed' | 'neutral'
16
+
17
+ export interface TrustKpiOptions {
18
+ cwd?: string
19
+ }
@@ -0,0 +1,210 @@
1
+ import { normalizeMergeRiskLevel } from './trust.js'
2
+ import type { DriftTrustReport, MergeRiskLevel, TrustDiffTrendSummary, TrustKpiReport } from './types.js'
3
+ import { discoverTrustJsonFiles } from './trust-kpi-fs.js'
4
+ import { parseTrustArtifact } from './trust-kpi-parse.js'
5
+ import type { ParsedTrustArtifact, TrustKpiOptions } from './trust-kpi-types.js'
6
+
7
+ function round(value: number, decimals = 2): number {
8
+ return Number(value.toFixed(decimals))
9
+ }
10
+
11
+ function median(values: number[]): number | null {
12
+ if (values.length === 0) return null
13
+ const sorted = [...values].sort((a, b) => a - b)
14
+ const mid = Math.floor(sorted.length / 2)
15
+ if (sorted.length % 2 === 0) {
16
+ return round((sorted[mid - 1] + sorted[mid]) / 2)
17
+ }
18
+ return round(sorted[mid])
19
+ }
20
+
21
+ function average(values: number[]): number | null {
22
+ if (values.length === 0) return null
23
+ return round(values.reduce((sum, value) => sum + value, 0) / values.length)
24
+ }
25
+
26
+
27
+ function buildDiffTrend(records: ParsedTrustArtifact[]): TrustDiffTrendSummary {
28
+ const withDiff = records.filter((record) => record.diffContext)
29
+
30
+ if (withDiff.length === 0) {
31
+ return {
32
+ available: false,
33
+ samples: 0,
34
+ statusDistribution: {
35
+ improved: 0,
36
+ regressed: 0,
37
+ neutral: 0,
38
+ },
39
+ scoreDelta: {
40
+ average: null,
41
+ median: null,
42
+ },
43
+ issues: {
44
+ newTotal: 0,
45
+ resolvedTotal: 0,
46
+ netNew: 0,
47
+ },
48
+ }
49
+ }
50
+
51
+ const scoreDeltas = withDiff.map((record) => record.diffContext!.scoreDelta)
52
+ const newIssues = withDiff.reduce((sum, record) => sum + record.diffContext!.newIssues, 0)
53
+ const resolvedIssues = withDiff.reduce((sum, record) => sum + record.diffContext!.resolvedIssues, 0)
54
+
55
+ const statusDistribution = {
56
+ improved: withDiff.filter((record) => record.diffContext!.status === 'improved').length,
57
+ regressed: withDiff.filter((record) => record.diffContext!.status === 'regressed').length,
58
+ neutral: withDiff.filter((record) => record.diffContext!.status === 'neutral').length,
59
+ }
60
+
61
+ return {
62
+ available: true,
63
+ samples: withDiff.length,
64
+ statusDistribution,
65
+ scoreDelta: {
66
+ average: average(scoreDeltas),
67
+ median: median(scoreDeltas),
68
+ },
69
+ issues: {
70
+ newTotal: newIssues,
71
+ resolvedTotal: resolvedIssues,
72
+ netNew: newIssues - resolvedIssues,
73
+ },
74
+ }
75
+ }
76
+
77
+ const KPI_RATIO_DECIMALS = 4
78
+
79
+ export function computeTrustKpis(input: string, options?: TrustKpiOptions): TrustKpiReport {
80
+ const cwd = options?.cwd ?? process.cwd()
81
+ const discovered = discoverTrustJsonFiles(input, cwd)
82
+
83
+ const records: ParsedTrustArtifact[] = []
84
+ const diagnostics = [...discovered.diagnostics]
85
+
86
+ for (const filePath of discovered.files) {
87
+ const parsed = parseTrustArtifact(filePath)
88
+ diagnostics.push(...parsed.diagnostics)
89
+ if (parsed.record) records.push(parsed.record)
90
+ }
91
+
92
+ const trustScores = records.map((record) => record.trustScore)
93
+ const mergeRiskDistribution: Record<MergeRiskLevel, number> = {
94
+ LOW: 0,
95
+ MEDIUM: 0,
96
+ HIGH: 0,
97
+ CRITICAL: 0,
98
+ }
99
+
100
+ for (const record of records) {
101
+ mergeRiskDistribution[record.mergeRisk] += 1
102
+ }
103
+
104
+ const highRiskCount = mergeRiskDistribution.HIGH + mergeRiskDistribution.CRITICAL
105
+
106
+ return {
107
+ generatedAt: new Date().toISOString(),
108
+ input,
109
+ files: {
110
+ matched: discovered.files.length,
111
+ parsed: records.length,
112
+ malformed: discovered.files.length - records.length,
113
+ },
114
+ prsEvaluated: records.length,
115
+ mergeRiskDistribution,
116
+ trustScore: {
117
+ average: average(trustScores),
118
+ median: median(trustScores),
119
+ min: trustScores.length > 0 ? Math.min(...trustScores) : null,
120
+ max: trustScores.length > 0 ? Math.max(...trustScores) : null,
121
+ },
122
+ highRiskRatio: records.length > 0 ? round(highRiskCount / records.length, KPI_RATIO_DECIMALS) : null,
123
+ diffTrend: buildDiffTrend(records),
124
+ diagnostics,
125
+ }
126
+ }
127
+
128
+ export function formatTrustKpiConsole(kpi: TrustKpiReport): string {
129
+ const parts = [
130
+ 'drift kpi',
131
+ '',
132
+ `Input: ${kpi.input}`,
133
+ `Files matched: ${kpi.files.matched} | parsed: ${kpi.files.parsed} | malformed: ${kpi.files.malformed}`,
134
+ `PRs evaluated: ${kpi.prsEvaluated}`,
135
+ `Trust score (avg/median): ${kpi.trustScore.average ?? 'n/a'} / ${kpi.trustScore.median ?? 'n/a'}`,
136
+ `High-risk ratio (HIGH+CRITICAL): ${kpi.highRiskRatio == null ? 'n/a' : `${round(kpi.highRiskRatio * 100, 2)}%`}`,
137
+ `Merge risk distribution: LOW=${kpi.mergeRiskDistribution.LOW} MEDIUM=${kpi.mergeRiskDistribution.MEDIUM} HIGH=${kpi.mergeRiskDistribution.HIGH} CRITICAL=${kpi.mergeRiskDistribution.CRITICAL}`,
138
+ ]
139
+
140
+ if (kpi.diffTrend.available) {
141
+ const avgDelta = kpi.diffTrend.scoreDelta.average
142
+ const signedDelta = avgDelta == null ? 'n/a' : `${avgDelta >= 0 ? '+' : ''}${avgDelta}`
143
+ parts.push(
144
+ `Diff trend samples: ${kpi.diffTrend.samples} | avg score delta: ${signedDelta} | new/resolved: +${kpi.diffTrend.issues.newTotal}/-${kpi.diffTrend.issues.resolvedTotal}`,
145
+ )
146
+ } else {
147
+ parts.push('Diff trend samples: 0 (no diff_context found)')
148
+ }
149
+
150
+ if (kpi.diagnostics.length > 0) {
151
+ const errorCount = kpi.diagnostics.filter((diagnostic) => diagnostic.level === 'error').length
152
+ const warningCount = kpi.diagnostics.filter((diagnostic) => diagnostic.level === 'warning').length
153
+ parts.push(`Diagnostics: ${errorCount} error(s), ${warningCount} warning(s)`)
154
+ }
155
+
156
+ return parts.join('\n')
157
+ }
158
+
159
+ export function formatTrustKpiJson(kpi: TrustKpiReport): string {
160
+ return JSON.stringify(kpi, null, 2)
161
+ }
162
+
163
+ export function computeTrustKpisFromReports(reports: DriftTrustReport[]): TrustKpiReport {
164
+ const tempRecords: ParsedTrustArtifact[] = reports.reduce<ParsedTrustArtifact[]>((acc, report, index) => {
165
+ const mergeRisk = normalizeMergeRiskLevel(report.merge_risk)
166
+ if (!mergeRisk || typeof report.trust_score !== 'number') return acc
167
+ acc.push({
168
+ filePath: `report-${index + 1}`,
169
+ trustScore: report.trust_score,
170
+ mergeRisk,
171
+ diffContext: report.diff_context,
172
+ })
173
+ return acc
174
+ }, [])
175
+
176
+ const trustScores = tempRecords.map((record) => record.trustScore)
177
+ const mergeRiskDistribution: Record<MergeRiskLevel, number> = {
178
+ LOW: 0,
179
+ MEDIUM: 0,
180
+ HIGH: 0,
181
+ CRITICAL: 0,
182
+ }
183
+
184
+ for (const record of tempRecords) {
185
+ mergeRiskDistribution[record.mergeRisk] += 1
186
+ }
187
+
188
+ const highRiskCount = mergeRiskDistribution.HIGH + mergeRiskDistribution.CRITICAL
189
+
190
+ return {
191
+ generatedAt: new Date().toISOString(),
192
+ input: 'in-memory',
193
+ files: {
194
+ matched: reports.length,
195
+ parsed: tempRecords.length,
196
+ malformed: reports.length - tempRecords.length,
197
+ },
198
+ prsEvaluated: tempRecords.length,
199
+ mergeRiskDistribution,
200
+ trustScore: {
201
+ average: average(trustScores),
202
+ median: median(trustScores),
203
+ min: trustScores.length > 0 ? Math.min(...trustScores) : null,
204
+ max: trustScores.length > 0 ? Math.max(...trustScores) : null,
205
+ },
206
+ highRiskRatio: tempRecords.length > 0 ? round(highRiskCount / tempRecords.length, KPI_RATIO_DECIMALS) : null,
207
+ diffTrend: buildDiffTrend(tempRecords),
208
+ diagnostics: [],
209
+ }
210
+ }
@@ -0,0 +1,246 @@
1
+ import type { DriftConfig, MergeRiskLevel, TrustGatePolicyPack, TrustGatePolicyPreset } from './types.js'
2
+
3
+ export interface TrustGateOptions {
4
+ enabled?: boolean
5
+ minTrust?: number
6
+ maxRisk?: MergeRiskLevel
7
+ }
8
+
9
+ export interface TrustGatePolicyResolutionOptions {
10
+ branchName?: string
11
+ policyPack?: string
12
+ overrides?: TrustGateOptions
13
+ }
14
+
15
+ export interface TrustGatePolicyResolutionStep {
16
+ source: 'base' | 'policy-pack' | 'branch-preset' | 'overrides'
17
+ name: string
18
+ values: TrustGateOptions
19
+ }
20
+
21
+ export interface TrustGatePolicyExplanation {
22
+ effectivePolicy: TrustGateOptions
23
+ branchName?: string
24
+ selectedPolicyPack?: string
25
+ invalidPolicyPack?: string
26
+ steps: TrustGatePolicyResolutionStep[]
27
+ }
28
+
29
+ export const MERGE_RISK_ORDER: MergeRiskLevel[] = ['LOW', 'MEDIUM', 'HIGH', 'CRITICAL']
30
+
31
+ const BRANCH_ENV_CANDIDATES = [
32
+ 'DRIFT_BRANCH',
33
+ 'GITHUB_HEAD_REF',
34
+ 'GITHUB_REF_NAME',
35
+ 'CI_COMMIT_REF_NAME',
36
+ 'BRANCH_NAME',
37
+ ] as const
38
+
39
+ const PATTERN_EXACT_BOOST = 10_000
40
+ const PATTERN_STATIC_CHAR_WEIGHT = 10
41
+
42
+ function formatTrustGatePolicyValues(values: TrustGateOptions): string {
43
+ const enabled = typeof values.enabled === 'boolean' ? String(values.enabled) : 'inherit'
44
+ const minTrust = typeof values.minTrust === 'number' ? String(values.minTrust) : 'inherit'
45
+ const maxRisk = values.maxRisk ?? 'inherit'
46
+ return `enabled=${enabled} minTrust=${minTrust} maxRisk=${maxRisk}`
47
+ }
48
+
49
+ export function normalizeMergeRiskLevel(value: string): MergeRiskLevel | undefined {
50
+ const normalized = value.toUpperCase()
51
+ return MERGE_RISK_ORDER.find((level) => level === normalized)
52
+ }
53
+
54
+ function branchPatternToRegExp(pattern: string): RegExp {
55
+ const escaped = pattern.replace(/[|\\{}()[\]^$+?.]/g, '\\$&').replace(/\*/g, '.*')
56
+ return new RegExp(`^${escaped}$`)
57
+ }
58
+
59
+ function patternSpecificity(pattern: string): number {
60
+ const wildcardCount = (pattern.match(/\*/g) ?? []).length
61
+ const staticChars = pattern.replace(/\*/g, '').length
62
+ const exactBoost = wildcardCount === 0 ? PATTERN_EXACT_BOOST : 0
63
+ return exactBoost + staticChars * PATTERN_STATIC_CHAR_WEIGHT - wildcardCount
64
+ }
65
+
66
+ function resolvePresetsForBranch(
67
+ branchName: string,
68
+ presets: TrustGatePolicyPreset[] | undefined,
69
+ ): TrustGatePolicyPreset[] {
70
+ if (!presets || presets.length === 0) return []
71
+ const matched: Array<{ preset: TrustGatePolicyPreset; specificity: number; index: number }> = []
72
+
73
+ for (let index = 0; index < presets.length; index += 1) {
74
+ const preset = presets[index]
75
+ if (!preset?.branch) continue
76
+
77
+ const regex = branchPatternToRegExp(preset.branch)
78
+ if (!regex.test(branchName)) continue
79
+ matched.push({ preset, specificity: patternSpecificity(preset.branch), index })
80
+ }
81
+
82
+ matched.sort((a, b) => a.specificity - b.specificity || a.index - b.index)
83
+ return matched.map((entry) => entry.preset)
84
+ }
85
+
86
+ function normalizeMinTrust(value: unknown): number | undefined {
87
+ return typeof value === 'number' && !Number.isNaN(value) ? value : undefined
88
+ }
89
+
90
+ function normalizeMaxRisk(value: unknown): MergeRiskLevel | undefined {
91
+ if (typeof value !== 'string') return undefined
92
+ return normalizeMergeRiskLevel(value)
93
+ }
94
+
95
+ function normalizeTrustGateOptions(
96
+ source: { enabled?: unknown; minTrust?: unknown; maxRisk?: unknown } | undefined,
97
+ ): TrustGateOptions {
98
+ if (!source) return {}
99
+
100
+ return {
101
+ enabled: typeof source.enabled === 'boolean' ? source.enabled : undefined,
102
+ minTrust: normalizeMinTrust(source.minTrust),
103
+ maxRisk: normalizeMaxRisk(source.maxRisk),
104
+ }
105
+ }
106
+
107
+ function mergeTrustGateOptions(base: TrustGateOptions, layer: TrustGateOptions): TrustGateOptions {
108
+ return {
109
+ enabled: typeof layer.enabled === 'boolean' ? layer.enabled : base.enabled,
110
+ minTrust: layer.minTrust ?? base.minTrust,
111
+ maxRisk: layer.maxRisk ?? base.maxRisk,
112
+ }
113
+ }
114
+
115
+ function normalizeResolutionOptions(
116
+ branchNameOrOptions?: string | TrustGatePolicyResolutionOptions,
117
+ explicitOverrides?: TrustGateOptions,
118
+ ): TrustGatePolicyResolutionOptions {
119
+ if (typeof branchNameOrOptions === 'string') {
120
+ return {
121
+ branchName: branchNameOrOptions,
122
+ overrides: explicitOverrides,
123
+ }
124
+ }
125
+
126
+ if (!branchNameOrOptions) {
127
+ return { overrides: explicitOverrides }
128
+ }
129
+
130
+ return {
131
+ ...branchNameOrOptions,
132
+ overrides: explicitOverrides
133
+ ? mergeTrustGateOptions(normalizeTrustGateOptions(branchNameOrOptions.overrides), normalizeTrustGateOptions(explicitOverrides))
134
+ : branchNameOrOptions.overrides,
135
+ }
136
+ }
137
+
138
+ function resolvePolicyPack(
139
+ policyPacks: Record<string, TrustGatePolicyPack> | undefined,
140
+ policyPackName: string | undefined,
141
+ ): { name?: string; pack?: TrustGatePolicyPack; invalid?: string } {
142
+ const normalizedName = policyPackName?.trim()
143
+ if (!normalizedName) return {}
144
+ if (!policyPacks) return { name: normalizedName, invalid: normalizedName }
145
+
146
+ const pack = policyPacks[normalizedName]
147
+ if (!pack) return { name: normalizedName, invalid: normalizedName }
148
+ return { name: normalizedName, pack }
149
+ }
150
+
151
+ export function detectBranchName(env: NodeJS.ProcessEnv = process.env): string | undefined {
152
+ for (const key of BRANCH_ENV_CANDIDATES) {
153
+ const value = env[key]?.trim()
154
+ if (value) return value
155
+ }
156
+ return undefined
157
+ }
158
+
159
+ export function explainTrustGatePolicy(
160
+ config: DriftConfig | undefined,
161
+ branchName?: string,
162
+ overrides?: TrustGateOptions,
163
+ ): TrustGatePolicyExplanation
164
+ export function explainTrustGatePolicy(
165
+ config: DriftConfig | undefined,
166
+ options?: TrustGatePolicyResolutionOptions,
167
+ ): TrustGatePolicyExplanation
168
+ export function explainTrustGatePolicy(
169
+ config: DriftConfig | undefined,
170
+ branchNameOrOptions?: string | TrustGatePolicyResolutionOptions,
171
+ explicitOverrides?: TrustGateOptions,
172
+ ): TrustGatePolicyExplanation {
173
+ const policy = config?.trustGate
174
+ const resolution = normalizeResolutionOptions(branchNameOrOptions, explicitOverrides)
175
+ const normalizedBranch = resolution.branchName?.trim()
176
+ const packResolution = resolvePolicyPack(policy?.policyPacks, resolution.policyPack)
177
+
178
+ const steps: TrustGatePolicyResolutionStep[] = []
179
+ const base = normalizeTrustGateOptions(policy)
180
+ let effective = base
181
+ steps.push({ source: 'base', name: 'trustGate', values: base })
182
+
183
+ if (packResolution.pack) {
184
+ const packOptions = normalizeTrustGateOptions(packResolution.pack)
185
+ effective = mergeTrustGateOptions(effective, packOptions)
186
+ steps.push({ source: 'policy-pack', name: packResolution.name ?? 'unknown', values: packOptions })
187
+ }
188
+
189
+ if (normalizedBranch) {
190
+ const matchedPresets = resolvePresetsForBranch(normalizedBranch, policy?.presets)
191
+ for (const preset of matchedPresets) {
192
+ const presetOptions = normalizeTrustGateOptions(preset)
193
+ effective = mergeTrustGateOptions(effective, presetOptions)
194
+ steps.push({ source: 'branch-preset', name: preset.branch, values: presetOptions })
195
+ }
196
+ }
197
+
198
+ const normalizedOverrides = normalizeTrustGateOptions(resolution.overrides)
199
+ if (Object.values(normalizedOverrides).some((value) => value !== undefined)) {
200
+ effective = mergeTrustGateOptions(effective, normalizedOverrides)
201
+ steps.push({ source: 'overrides', name: 'cli', values: normalizedOverrides })
202
+ }
203
+
204
+ return {
205
+ effectivePolicy: effective,
206
+ branchName: normalizedBranch,
207
+ selectedPolicyPack: packResolution.name,
208
+ invalidPolicyPack: packResolution.invalid,
209
+ steps,
210
+ }
211
+ }
212
+
213
+ export function resolveTrustGatePolicy(
214
+ config: DriftConfig | undefined,
215
+ branchName?: string,
216
+ overrides?: TrustGateOptions,
217
+ ): TrustGateOptions
218
+ export function resolveTrustGatePolicy(
219
+ config: DriftConfig | undefined,
220
+ options?: TrustGatePolicyResolutionOptions,
221
+ ): TrustGateOptions
222
+ export function resolveTrustGatePolicy(
223
+ config: DriftConfig | undefined,
224
+ branchNameOrOptions?: string | TrustGatePolicyResolutionOptions,
225
+ explicitOverrides?: TrustGateOptions,
226
+ ): TrustGateOptions {
227
+ const options = normalizeResolutionOptions(branchNameOrOptions, explicitOverrides)
228
+ return explainTrustGatePolicy(config, options).effectivePolicy
229
+ }
230
+
231
+ export function formatTrustGatePolicyExplanation(explanation: TrustGatePolicyExplanation): string {
232
+ const lines = ['Trust gate policy resolution:']
233
+ lines.push(`- branch: ${explanation.branchName ?? 'not provided'}`)
234
+ lines.push(`- policy pack: ${explanation.selectedPolicyPack ?? 'not selected'}`)
235
+ if (explanation.invalidPolicyPack) {
236
+ lines.push(`- invalid policy pack: ${explanation.invalidPolicyPack}`)
237
+ }
238
+ lines.push('- steps:')
239
+
240
+ for (const [index, step] of explanation.steps.entries()) {
241
+ lines.push(` ${index + 1}. ${step.source} (${step.name}): ${formatTrustGatePolicyValues(step.values)}`)
242
+ }
243
+
244
+ lines.push(`- effective: ${formatTrustGatePolicyValues(explanation.effectivePolicy)}`)
245
+ return lines.join('\n')
246
+ }
@@ -0,0 +1,61 @@
1
+ import type { DriftTrustReport, TrustDiffContext, TrustFixPriority, TrustReason } from './types.js'
2
+
3
+ export function renderTrustReasons(reasons: TrustReason[]): string {
4
+ if (reasons.length === 0) return '- none'
5
+ return reasons.map((reason) => `- ${reason.label}: ${reason.detail} (impact ${reason.impact})`).join('\n')
6
+ }
7
+
8
+ export function renderTrustPriorities(priorities: TrustFixPriority[]): string {
9
+ if (priorities.length === 0) return '- none'
10
+ return priorities
11
+ .map((priority) =>
12
+ `- #${priority.rank} ${priority.rule} (${priority.severity}, x${priority.occurrences}${priority.confidence ? `, confidence ${priority.confidence}` : ''}): ${priority.suggestion}`
13
+ )
14
+ .join('\n')
15
+ }
16
+
17
+ export function renderTrustMarkdownReasons(reasons: TrustReason[]): string {
18
+ if (reasons.length === 0) return '- none'
19
+ return reasons.map((reason) => `- **${reason.label}**: ${reason.detail} (impact ${reason.impact})`).join('\n')
20
+ }
21
+
22
+ export function renderTrustMarkdownPriorities(priorities: TrustFixPriority[]): string {
23
+ if (priorities.length === 0) return '- none'
24
+ return priorities
25
+ .map((priority) =>
26
+ `- #${priority.rank} \`${priority.rule}\` (${priority.severity}, x${priority.occurrences}, effort: ${priority.effort}${priority.confidence ? `, confidence: ${priority.confidence}` : ''}) - ${priority.suggestion}${priority.explanation ? ` ${priority.explanation}` : ''}`
27
+ )
28
+ .join('\n')
29
+ }
30
+
31
+ export function renderTrustDiffBlock(diffContext: TrustDiffContext | undefined): string {
32
+ if (!diffContext) {
33
+ return [
34
+ '- Base ref: not provided',
35
+ '- Diff-aware adjustment: not applied',
36
+ ].join('\n')
37
+ }
38
+
39
+ return [
40
+ `- Base ref: \`${diffContext.baseRef}\``,
41
+ `- Diff status: **${diffContext.status.toUpperCase()}**`,
42
+ `- Score delta: **${diffContext.scoreDelta >= 0 ? '+' : ''}${diffContext.scoreDelta}**`,
43
+ `- Issues: **+${diffContext.newIssues}** new / **-${diffContext.resolvedIssues}** resolved`,
44
+ `- Trust adjustment: **+${diffContext.penalty}** penalty / **-${diffContext.bonus}** bonus (net ${diffContext.netImpact >= 0 ? '+' : ''}${diffContext.netImpact})`,
45
+ ].join('\n')
46
+ }
47
+
48
+ export function renderTrustAdvancedComparison(advancedContext: DriftTrustReport['advanced_context']): string {
49
+ if (!advancedContext?.comparison) return '- Historical comparison not available'
50
+
51
+ return [
52
+ `- Source: \`${advancedContext.comparison.source}\``,
53
+ `- Trend: **${advancedContext.comparison.trend.toUpperCase()}**`,
54
+ `- Summary: ${advancedContext.comparison.summary}`,
55
+ ].join('\n')
56
+ }
57
+
58
+ export function renderTrustAdvancedGuidance(advancedContext: DriftTrustReport['advanced_context']): string {
59
+ if (!advancedContext?.team_guidance?.length) return '- none'
60
+ return advancedContext.team_guidance.map((item) => `- ${item}`).join('\n')
61
+ }