@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.
- 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/review-pr.yml +34 -41
- package/AGENTS.md +75 -251
- package/CHANGELOG.md +28 -0
- package/README.md +148 -41
- package/dist/benchmark.d.ts +1 -1
- package/dist/benchmark.js +71 -52
- package/dist/cli.js +243 -8
- package/dist/config.js +16 -2
- package/dist/diff.js +42 -50
- 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/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 +10 -3
- package/dist/index.js +4 -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 +1 -1
- package/dist/plugins.js +45 -142
- 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 +2 -1
- package/dist/rules/phase3-configurable.js +23 -15
- 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 -218
- package/dist/saas.js +7 -761
- 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 +1 -3
- package/dist/trust-kpi.js +6 -266
- 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 +4 -32
- package/dist/trust.js +29 -432
- 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 -365
- package/docs/release-notes-draft.md +40 -0
- package/docs/rules-catalog.md +49 -0
- package/docs/trust-core-release-checklist.md +37 -5
- package/package.json +3 -2
- 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/benchmark.ts +75 -53
- package/src/cli.ts +285 -13
- package/src/config.ts +19 -2
- package/src/diff.ts +57 -48
- package/src/doctor.ts +173 -0
- package/src/format.ts +81 -0
- package/src/guard-types.ts +64 -0
- package/src/guard.ts +324 -0
- package/src/index.ts +35 -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 +76 -283
- package/src/reporter-constants.ts +46 -0
- package/src/reporter.ts +64 -65
- package/src/review.ts +4 -2
- package/src/rules/phase3-configurable.ts +39 -26
- 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 -1031
- 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 +8 -316
- 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 +62 -576
- 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 -409
- package/tests/cli-sarif.test.ts +92 -0
- package/tests/format.test.ts +157 -0
- package/tests/new-features.test.ts +10 -2
- package/tests/phase1-init-doctor-guard.test.ts +199 -0
- package/tests/sarif.test.ts +160 -0
- package/tests/trust-kpi.test.ts +31 -4
- 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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
|
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 =
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
|
603
|
-
|
|
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(
|
|
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
|
|
652
|
-
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
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 {
|