@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.
- package/.gga +50 -0
- package/.github/actions/drift-review/README.md +60 -0
- package/.github/actions/drift-review/action.yml +131 -0
- package/.github/actions/drift-scan/README.md +28 -32
- package/.github/actions/drift-scan/action.yml +78 -14
- package/.github/workflows/publish-vscode.yml +3 -3
- package/.github/workflows/publish.yml +3 -3
- package/.github/workflows/review-pr.yml +94 -9
- package/AGENTS.md +75 -245
- package/CHANGELOG.md +28 -0
- package/README.md +308 -51
- package/ROADMAP.md +6 -5
- package/dist/analyzer.d.ts +2 -2
- package/dist/analyzer.js +420 -159
- package/dist/benchmark.d.ts +2 -0
- package/dist/benchmark.js +204 -0
- package/dist/cli.js +693 -67
- package/dist/config.js +16 -2
- package/dist/diff.js +66 -10
- package/dist/doctor.d.ts +5 -0
- package/dist/doctor.js +133 -0
- package/dist/format.d.ts +17 -0
- package/dist/format.js +45 -0
- package/dist/git.js +12 -0
- package/dist/guard-types.d.ts +57 -0
- package/dist/guard-types.js +2 -0
- package/dist/guard.d.ts +14 -0
- package/dist/guard.js +239 -0
- package/dist/index.d.ts +12 -3
- package/dist/index.js +6 -1
- package/dist/init.d.ts +15 -0
- package/dist/init.js +273 -0
- package/dist/map-cycles.d.ts +2 -0
- package/dist/map-cycles.js +34 -0
- package/dist/map-svg.d.ts +19 -0
- package/dist/map-svg.js +97 -0
- package/dist/map.js +78 -138
- package/dist/metrics.js +70 -55
- package/dist/output-metadata.d.ts +13 -0
- package/dist/output-metadata.js +17 -0
- package/dist/plugins-capabilities.d.ts +4 -0
- package/dist/plugins-capabilities.js +21 -0
- package/dist/plugins-messages.d.ts +10 -0
- package/dist/plugins-messages.js +16 -0
- package/dist/plugins-rules.d.ts +9 -0
- package/dist/plugins-rules.js +137 -0
- package/dist/plugins.d.ts +2 -1
- package/dist/plugins.js +80 -28
- package/dist/printer.js +4 -0
- package/dist/reporter-constants.d.ts +16 -0
- package/dist/reporter-constants.js +39 -0
- package/dist/reporter.d.ts +3 -3
- package/dist/reporter.js +35 -55
- package/dist/review.d.ts +2 -1
- package/dist/review.js +4 -3
- package/dist/rules/comments.js +2 -2
- package/dist/rules/complexity.js +2 -7
- package/dist/rules/nesting.js +3 -13
- package/dist/rules/phase0-basic.js +10 -10
- package/dist/rules/phase3-configurable.js +23 -15
- package/dist/rules/shared.d.ts +2 -0
- package/dist/rules/shared.js +27 -3
- package/dist/saas/constants.d.ts +15 -0
- package/dist/saas/constants.js +48 -0
- package/dist/saas/dashboard.d.ts +8 -0
- package/dist/saas/dashboard.js +132 -0
- package/dist/saas/errors.d.ts +19 -0
- package/dist/saas/errors.js +37 -0
- package/dist/saas/helpers.d.ts +21 -0
- package/dist/saas/helpers.js +110 -0
- package/dist/saas/ingest.d.ts +3 -0
- package/dist/saas/ingest.js +249 -0
- package/dist/saas/organization.d.ts +5 -0
- package/dist/saas/organization.js +82 -0
- package/dist/saas/plan-change.d.ts +10 -0
- package/dist/saas/plan-change.js +15 -0
- package/dist/saas/store.d.ts +21 -0
- package/dist/saas/store.js +159 -0
- package/dist/saas/types.d.ts +191 -0
- package/dist/saas/types.js +2 -0
- package/dist/saas.d.ts +8 -82
- package/dist/saas.js +7 -320
- package/dist/sarif.d.ts +74 -0
- package/dist/sarif.js +122 -0
- package/dist/trust-advanced.d.ts +14 -0
- package/dist/trust-advanced.js +65 -0
- package/dist/trust-kpi-fs.d.ts +3 -0
- package/dist/trust-kpi-fs.js +141 -0
- package/dist/trust-kpi-parse.d.ts +7 -0
- package/dist/trust-kpi-parse.js +186 -0
- package/dist/trust-kpi-types.d.ts +16 -0
- package/dist/trust-kpi-types.js +2 -0
- package/dist/trust-kpi.d.ts +7 -0
- package/dist/trust-kpi.js +185 -0
- package/dist/trust-policy.d.ts +32 -0
- package/dist/trust-policy.js +160 -0
- package/dist/trust-render.d.ts +9 -0
- package/dist/trust-render.js +54 -0
- package/dist/trust-scoring.d.ts +9 -0
- package/dist/trust-scoring.js +208 -0
- package/dist/trust.d.ts +37 -0
- package/dist/trust.js +168 -0
- package/dist/types/app.d.ts +30 -0
- package/dist/types/app.js +2 -0
- package/dist/types/config.d.ts +25 -0
- package/dist/types/config.js +2 -0
- package/dist/types/core.d.ts +100 -0
- package/dist/types/core.js +2 -0
- package/dist/types/diff.d.ts +55 -0
- package/dist/types/diff.js +2 -0
- package/dist/types/plugin.d.ts +41 -0
- package/dist/types/plugin.js +2 -0
- package/dist/types/trust.d.ts +120 -0
- package/dist/types/trust.js +2 -0
- package/dist/types.d.ts +8 -211
- package/docs/PRD.md +187 -109
- package/docs/plugin-contract.md +61 -0
- package/docs/release-notes-draft.md +40 -0
- package/docs/rules-catalog.md +49 -0
- package/docs/trust-core-release-checklist.md +87 -0
- package/package.json +6 -3
- package/packages/vscode-drift/src/code-actions.ts +1 -1
- package/schemas/drift-ai-output.v1.json +162 -0
- package/schemas/drift-report.v1.json +151 -0
- package/schemas/drift-trust.v1.json +131 -0
- package/scripts/smoke-repo.mjs +394 -0
- package/src/analyzer.ts +484 -155
- package/src/benchmark.ts +266 -0
- package/src/cli.ts +840 -85
- package/src/config.ts +19 -2
- package/src/diff.ts +84 -10
- package/src/doctor.ts +173 -0
- package/src/format.ts +81 -0
- package/src/git.ts +16 -0
- package/src/guard-types.ts +64 -0
- package/src/guard.ts +324 -0
- package/src/index.ts +83 -0
- package/src/init.ts +298 -0
- package/src/map-cycles.ts +38 -0
- package/src/map-svg.ts +124 -0
- package/src/map.ts +111 -142
- package/src/metrics.ts +78 -59
- package/src/output-metadata.ts +30 -0
- package/src/plugins-capabilities.ts +36 -0
- package/src/plugins-messages.ts +35 -0
- package/src/plugins-rules.ts +296 -0
- package/src/plugins.ts +148 -27
- package/src/printer.ts +4 -0
- package/src/reporter-constants.ts +46 -0
- package/src/reporter.ts +64 -65
- package/src/review.ts +6 -4
- package/src/rules/comments.ts +2 -2
- package/src/rules/complexity.ts +2 -7
- package/src/rules/nesting.ts +3 -13
- package/src/rules/phase0-basic.ts +11 -12
- package/src/rules/phase3-configurable.ts +39 -26
- package/src/rules/shared.ts +31 -3
- package/src/saas/constants.ts +56 -0
- package/src/saas/dashboard.ts +172 -0
- package/src/saas/errors.ts +45 -0
- package/src/saas/helpers.ts +140 -0
- package/src/saas/ingest.ts +278 -0
- package/src/saas/organization.ts +99 -0
- package/src/saas/plan-change.ts +19 -0
- package/src/saas/store.ts +172 -0
- package/src/saas/types.ts +216 -0
- package/src/saas.ts +49 -433
- package/src/sarif.ts +232 -0
- package/src/trust-advanced.ts +99 -0
- package/src/trust-kpi-fs.ts +169 -0
- package/src/trust-kpi-parse.ts +219 -0
- package/src/trust-kpi-types.ts +19 -0
- package/src/trust-kpi.ts +210 -0
- package/src/trust-policy.ts +246 -0
- package/src/trust-render.ts +61 -0
- package/src/trust-scoring.ts +231 -0
- package/src/trust.ts +260 -0
- package/src/types/app.ts +30 -0
- package/src/types/config.ts +27 -0
- package/src/types/core.ts +105 -0
- package/src/types/diff.ts +61 -0
- package/src/types/plugin.ts +46 -0
- package/src/types/trust.ts +134 -0
- package/src/types.ts +78 -238
- package/tests/cli-sarif.test.ts +92 -0
- package/tests/diff.test.ts +124 -0
- package/tests/format.test.ts +157 -0
- package/tests/new-features.test.ts +80 -1
- package/tests/phase1-init-doctor-guard.test.ts +199 -0
- package/tests/plugins.test.ts +219 -0
- package/tests/rules.test.ts +23 -1
- package/tests/saas-foundation.test.ts +358 -1
- package/tests/sarif.test.ts +160 -0
- package/tests/trust-kpi.test.ts +147 -0
- 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
|
+
}
|
package/src/trust-kpi.ts
ADDED
|
@@ -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
|
+
}
|