@eduardbar/drift 1.3.0 → 1.5.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 +62 -0
- package/.github/actions/drift-review/action.yml +148 -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 +1 -3
- package/.github/workflows/publish.yml +8 -0
- package/.github/workflows/quality.yml +15 -0
- package/.github/workflows/reusable-quality-checks.yml +95 -0
- package/.github/workflows/review-pr.yml +33 -41
- package/AGENTS.md +75 -251
- package/CHANGELOG.md +41 -0
- package/README.md +177 -43
- package/benchmarks/fixtures/critical/drift.config.ts +21 -0
- package/benchmarks/fixtures/critical/src/app/user-service.ts +30 -0
- package/benchmarks/fixtures/critical/src/domain/entities.ts +19 -0
- package/benchmarks/fixtures/critical/src/domain/policies.ts +22 -0
- package/benchmarks/fixtures/critical/src/index.ts +10 -0
- package/benchmarks/fixtures/critical/src/infra/memory-user-repo.ts +14 -0
- package/benchmarks/perf-budget.v1.json +27 -0
- package/dist/benchmark.d.ts +1 -1
- package/dist/benchmark.js +83 -52
- package/dist/cli.js +243 -8
- package/dist/config.js +16 -2
- package/dist/diff.js +42 -50
- package/dist/doctor.d.ts +26 -0
- package/dist/doctor.js +140 -0
- package/dist/format.d.ts +17 -0
- package/dist/format.js +45 -0
- package/dist/guard-baseline.d.ts +12 -0
- package/dist/guard-baseline.js +57 -0
- package/dist/guard-metrics.d.ts +6 -0
- package/dist/guard-metrics.js +39 -0
- package/dist/guard-types.d.ts +58 -0
- package/dist/guard-types.js +2 -0
- package/dist/guard.d.ts +16 -0
- package/dist/guard.js +178 -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 +15 -0
- package/dist/output-metadata.js +19 -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 +5 -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/AGENTS.md +1 -1
- 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 +11 -4
- package/packages/vscode-drift/src/code-actions.ts +1 -1
- package/schemas/drift-ai-output.v1.json +162 -0
- package/schemas/drift-doctor.v1.json +57 -0
- package/schemas/drift-guard.v1.json +298 -0
- package/schemas/drift-report.v1.json +151 -0
- package/schemas/drift-trust.v1.json +131 -0
- package/scripts/check-docs-drift.mjs +154 -0
- package/scripts/check-performance-budget.mjs +360 -0
- package/scripts/check-runtime-policy.mjs +66 -0
- package/scripts/smoke-repo.mjs +394 -0
- package/src/benchmark.ts +92 -53
- package/src/cli.ts +285 -13
- package/src/config.ts +19 -2
- package/src/diff.ts +57 -48
- package/src/doctor.ts +185 -0
- package/src/format.ts +81 -0
- package/src/guard-baseline.ts +74 -0
- package/src/guard-metrics.ts +52 -0
- package/src/guard-types.ts +66 -0
- package/src/guard.ts +248 -0
- package/src/index.ts +36 -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 +32 -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 +79 -409
- package/tests/ci-quality-matrix.test.ts +37 -0
- package/tests/ci-smoke-gate.test.ts +26 -0
- package/tests/ci-version-alignment.test.ts +93 -0
- package/tests/cli-sarif.test.ts +92 -0
- package/tests/docs-drift-check.test.ts +115 -0
- package/tests/format.test.ts +157 -0
- package/tests/new-features.test.ts +11 -3
- package/tests/perf-budget-check.test.ts +146 -0
- package/tests/phase1-init-doctor-guard.test.ts +301 -0
- package/tests/runtime-policy-alignment.test.ts +46 -0
- package/tests/sarif.test.ts +160 -0
- package/tests/trust-kpi.test.ts +31 -4
- package/tests/trust.test.ts +18 -0
- package/vitest.config.ts +2 -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
CHANGED
|
@@ -1,25 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
interface ParsedTrustArtifact {
|
|
7
|
-
filePath: string
|
|
8
|
-
trustScore: number
|
|
9
|
-
mergeRisk: MergeRiskLevel
|
|
10
|
-
diffContext?: TrustDiffContext
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface DiscoverResult {
|
|
14
|
-
files: string[]
|
|
15
|
-
diagnostics: TrustKpiDiagnostic[]
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
const IGNORED_DIRECTORIES = new Set(['node_modules', '.git', 'dist', '.next', 'build'])
|
|
19
|
-
|
|
20
|
-
function toPosixPath(path: string): string {
|
|
21
|
-
return path.replace(/\\/g, '/')
|
|
22
|
-
}
|
|
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'
|
|
23
6
|
|
|
24
7
|
function round(value: number, decimals = 2): number {
|
|
25
8
|
return Number(value.toFixed(decimals))
|
|
@@ -40,295 +23,6 @@ function average(values: number[]): number | null {
|
|
|
40
23
|
return round(values.reduce((sum, value) => sum + value, 0) / values.length)
|
|
41
24
|
}
|
|
42
25
|
|
|
43
|
-
function listFilesRecursively(root: string): string[] {
|
|
44
|
-
if (!existsSync(root)) return []
|
|
45
|
-
const out: string[] = []
|
|
46
|
-
const stack = [root]
|
|
47
|
-
|
|
48
|
-
while (stack.length > 0) {
|
|
49
|
-
const current = stack.pop()!
|
|
50
|
-
for (const entry of readdirSync(current)) {
|
|
51
|
-
const fullPath = resolve(current, entry)
|
|
52
|
-
const info = statSync(fullPath)
|
|
53
|
-
if (info.isDirectory()) {
|
|
54
|
-
if (IGNORED_DIRECTORIES.has(entry)) continue
|
|
55
|
-
stack.push(fullPath)
|
|
56
|
-
} else {
|
|
57
|
-
out.push(fullPath)
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return out
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function isGlobPattern(input: string): boolean {
|
|
66
|
-
return /[*?[\]{}]/.test(input)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function escapeRegex(char: string): string {
|
|
70
|
-
return /[\\^$+?.()|{}\[\]]/.test(char) ? `\\${char}` : char
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function globToRegex(pattern: string): RegExp {
|
|
74
|
-
const normalized = toPosixPath(pattern)
|
|
75
|
-
let expression = '^'
|
|
76
|
-
|
|
77
|
-
for (let index = 0; index < normalized.length; index += 1) {
|
|
78
|
-
const char = normalized[index]
|
|
79
|
-
const nextChar = normalized[index + 1]
|
|
80
|
-
const nextNextChar = normalized[index + 2]
|
|
81
|
-
|
|
82
|
-
if (char === '*' && nextChar === '*') {
|
|
83
|
-
if (nextNextChar === '/') {
|
|
84
|
-
expression += '(?:.*/)?'
|
|
85
|
-
index += 2
|
|
86
|
-
continue
|
|
87
|
-
}
|
|
88
|
-
expression += '.*'
|
|
89
|
-
index += 1
|
|
90
|
-
continue
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (char === '*') {
|
|
94
|
-
expression += '[^/]*'
|
|
95
|
-
continue
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (char === '?') {
|
|
99
|
-
expression += '[^/]'
|
|
100
|
-
continue
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
expression += escapeRegex(char)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
expression += '$'
|
|
107
|
-
return new RegExp(expression)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function globBaseDir(pattern: string): string {
|
|
111
|
-
const normalized = toPosixPath(pattern)
|
|
112
|
-
const wildcardIndex = normalized.search(/[*?[\]{}]/)
|
|
113
|
-
|
|
114
|
-
if (wildcardIndex < 0) return dirname(pattern)
|
|
115
|
-
|
|
116
|
-
const prefix = normalized.slice(0, wildcardIndex)
|
|
117
|
-
const slashIndex = prefix.lastIndexOf('/')
|
|
118
|
-
|
|
119
|
-
if (slashIndex < 0) return '.'
|
|
120
|
-
if (slashIndex === 0) return '/'
|
|
121
|
-
|
|
122
|
-
return prefix.slice(0, slashIndex)
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function discoverTrustJsonFiles(input: string, cwd: string): DiscoverResult {
|
|
126
|
-
const diagnostics: TrustKpiDiagnostic[] = []
|
|
127
|
-
const source = input.trim() || '.'
|
|
128
|
-
|
|
129
|
-
if (isGlobPattern(source)) {
|
|
130
|
-
const absolutePattern = isAbsolute(source) ? source : resolve(cwd, source)
|
|
131
|
-
const regex = globToRegex(toPosixPath(absolutePattern))
|
|
132
|
-
const base = resolve(cwd, globBaseDir(source))
|
|
133
|
-
|
|
134
|
-
if (!existsSync(base)) {
|
|
135
|
-
diagnostics.push({
|
|
136
|
-
level: 'error',
|
|
137
|
-
code: 'path-not-found',
|
|
138
|
-
message: `Glob base path does not exist: ${base}`,
|
|
139
|
-
})
|
|
140
|
-
return { files: [], diagnostics }
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const matched = listFilesRecursively(base)
|
|
144
|
-
.filter((filePath) => regex.test(toPosixPath(filePath)))
|
|
145
|
-
.filter((filePath) => filePath.toLowerCase().endsWith('.json'))
|
|
146
|
-
.sort((a, b) => a.localeCompare(b))
|
|
147
|
-
|
|
148
|
-
return { files: matched, diagnostics }
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const absolute = isAbsolute(source) ? source : resolve(cwd, source)
|
|
152
|
-
if (!existsSync(absolute)) {
|
|
153
|
-
diagnostics.push({
|
|
154
|
-
level: 'error',
|
|
155
|
-
code: 'path-not-found',
|
|
156
|
-
message: `Path does not exist: ${absolute}`,
|
|
157
|
-
})
|
|
158
|
-
return { files: [], diagnostics }
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const info = statSync(absolute)
|
|
162
|
-
if (info.isDirectory()) {
|
|
163
|
-
const files = listFilesRecursively(absolute)
|
|
164
|
-
.filter((filePath) => filePath.toLowerCase().endsWith('.json'))
|
|
165
|
-
.sort((a, b) => a.localeCompare(b))
|
|
166
|
-
return { files, diagnostics }
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (info.isFile()) {
|
|
170
|
-
if (!absolute.toLowerCase().endsWith('.json')) {
|
|
171
|
-
diagnostics.push({
|
|
172
|
-
level: 'warning',
|
|
173
|
-
code: 'path-not-supported',
|
|
174
|
-
file: absolute,
|
|
175
|
-
message: 'Input file is not JSON; attempting to parse anyway',
|
|
176
|
-
})
|
|
177
|
-
}
|
|
178
|
-
return { files: [absolute], diagnostics }
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
diagnostics.push({
|
|
182
|
-
level: 'error',
|
|
183
|
-
code: 'path-not-supported',
|
|
184
|
-
message: `Path is neither a file nor directory: ${absolute}`,
|
|
185
|
-
})
|
|
186
|
-
|
|
187
|
-
return { files: [], diagnostics }
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function isObjectLike(value: unknown): value is Record<string, unknown> {
|
|
191
|
-
return typeof value === 'object' && value !== null
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function normalizeDiffContext(raw: unknown): { diffContext?: TrustDiffContext; diagnostic?: TrustKpiDiagnostic } {
|
|
195
|
-
if (!isObjectLike(raw)) {
|
|
196
|
-
return {
|
|
197
|
-
diagnostic: {
|
|
198
|
-
level: 'warning',
|
|
199
|
-
code: 'invalid-diff-context',
|
|
200
|
-
message: 'diff_context is present but malformed; skipping diff trend fields for this artifact',
|
|
201
|
-
},
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
const baseRef = typeof raw.baseRef === 'string' ? raw.baseRef : 'unknown'
|
|
206
|
-
const status = raw.status
|
|
207
|
-
const scoreDelta = typeof raw.scoreDelta === 'number' && Number.isFinite(raw.scoreDelta) ? raw.scoreDelta : null
|
|
208
|
-
const newIssues = typeof raw.newIssues === 'number' && Number.isFinite(raw.newIssues) ? raw.newIssues : null
|
|
209
|
-
const resolvedIssues = typeof raw.resolvedIssues === 'number' && Number.isFinite(raw.resolvedIssues) ? raw.resolvedIssues : null
|
|
210
|
-
const filesChanged = typeof raw.filesChanged === 'number' && Number.isFinite(raw.filesChanged) ? raw.filesChanged : 0
|
|
211
|
-
const penalty = typeof raw.penalty === 'number' && Number.isFinite(raw.penalty) ? raw.penalty : 0
|
|
212
|
-
const bonus = typeof raw.bonus === 'number' && Number.isFinite(raw.bonus) ? raw.bonus : 0
|
|
213
|
-
const netImpact = typeof raw.netImpact === 'number' && Number.isFinite(raw.netImpact) ? raw.netImpact : 0
|
|
214
|
-
|
|
215
|
-
if (scoreDelta == null || newIssues == null || resolvedIssues == null) {
|
|
216
|
-
return {
|
|
217
|
-
diagnostic: {
|
|
218
|
-
level: 'warning',
|
|
219
|
-
code: 'invalid-diff-context',
|
|
220
|
-
message: 'diff_context is missing numeric scoreDelta/newIssues/resolvedIssues; skipping diff trend fields for this artifact',
|
|
221
|
-
},
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const normalizedStatus = status === 'improved' || status === 'regressed' || status === 'neutral'
|
|
226
|
-
? status
|
|
227
|
-
: scoreDelta < 0
|
|
228
|
-
? 'improved'
|
|
229
|
-
: scoreDelta > 0
|
|
230
|
-
? 'regressed'
|
|
231
|
-
: 'neutral'
|
|
232
|
-
|
|
233
|
-
return {
|
|
234
|
-
diffContext: {
|
|
235
|
-
baseRef,
|
|
236
|
-
status: normalizedStatus,
|
|
237
|
-
scoreDelta,
|
|
238
|
-
newIssues,
|
|
239
|
-
resolvedIssues,
|
|
240
|
-
filesChanged,
|
|
241
|
-
penalty,
|
|
242
|
-
bonus,
|
|
243
|
-
netImpact,
|
|
244
|
-
},
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function parseTrustArtifact(filePath: string): { record?: ParsedTrustArtifact; diagnostics: TrustKpiDiagnostic[] } {
|
|
249
|
-
const diagnostics: TrustKpiDiagnostic[] = []
|
|
250
|
-
|
|
251
|
-
let rawContent = ''
|
|
252
|
-
try {
|
|
253
|
-
rawContent = readFileSync(filePath, 'utf8')
|
|
254
|
-
} catch (error) {
|
|
255
|
-
diagnostics.push({
|
|
256
|
-
level: 'error',
|
|
257
|
-
code: 'read-failed',
|
|
258
|
-
file: filePath,
|
|
259
|
-
message: error instanceof Error ? error.message : String(error),
|
|
260
|
-
})
|
|
261
|
-
return { diagnostics }
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
let parsed: unknown
|
|
265
|
-
try {
|
|
266
|
-
parsed = JSON.parse(rawContent)
|
|
267
|
-
} catch (error) {
|
|
268
|
-
diagnostics.push({
|
|
269
|
-
level: 'error',
|
|
270
|
-
code: 'parse-failed',
|
|
271
|
-
file: filePath,
|
|
272
|
-
message: error instanceof Error ? error.message : String(error),
|
|
273
|
-
})
|
|
274
|
-
return { diagnostics }
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (!isObjectLike(parsed)) {
|
|
278
|
-
diagnostics.push({
|
|
279
|
-
level: 'error',
|
|
280
|
-
code: 'invalid-shape',
|
|
281
|
-
file: filePath,
|
|
282
|
-
message: 'Trust artifact must be a JSON object',
|
|
283
|
-
})
|
|
284
|
-
return { diagnostics }
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const trustScore = parsed.trust_score
|
|
288
|
-
if (typeof trustScore !== 'number' || !Number.isFinite(trustScore)) {
|
|
289
|
-
diagnostics.push({
|
|
290
|
-
level: 'error',
|
|
291
|
-
code: 'invalid-shape',
|
|
292
|
-
file: filePath,
|
|
293
|
-
message: 'Missing numeric trust_score',
|
|
294
|
-
})
|
|
295
|
-
return { diagnostics }
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const mergeRisk = typeof parsed.merge_risk === 'string'
|
|
299
|
-
? normalizeMergeRiskLevel(parsed.merge_risk)
|
|
300
|
-
: undefined
|
|
301
|
-
|
|
302
|
-
if (!mergeRisk) {
|
|
303
|
-
diagnostics.push({
|
|
304
|
-
level: 'error',
|
|
305
|
-
code: 'invalid-shape',
|
|
306
|
-
file: filePath,
|
|
307
|
-
message: `Missing/invalid merge_risk (expected one of ${MERGE_RISK_ORDER.join(', ')})`,
|
|
308
|
-
})
|
|
309
|
-
return { diagnostics }
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
let diffContext: TrustDiffContext | undefined
|
|
313
|
-
if (parsed.diff_context !== undefined) {
|
|
314
|
-
const normalized = normalizeDiffContext(parsed.diff_context)
|
|
315
|
-
if (normalized.diagnostic) {
|
|
316
|
-
diagnostics.push({ ...normalized.diagnostic, file: filePath })
|
|
317
|
-
} else {
|
|
318
|
-
diffContext = normalized.diffContext
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
return {
|
|
323
|
-
record: {
|
|
324
|
-
filePath,
|
|
325
|
-
trustScore,
|
|
326
|
-
mergeRisk,
|
|
327
|
-
diffContext,
|
|
328
|
-
},
|
|
329
|
-
diagnostics,
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
26
|
|
|
333
27
|
function buildDiffTrend(records: ParsedTrustArtifact[]): TrustDiffTrendSummary {
|
|
334
28
|
const withDiff = records.filter((record) => record.diffContext)
|
|
@@ -380,9 +74,7 @@ function buildDiffTrend(records: ParsedTrustArtifact[]): TrustDiffTrendSummary {
|
|
|
380
74
|
}
|
|
381
75
|
}
|
|
382
76
|
|
|
383
|
-
|
|
384
|
-
cwd?: string
|
|
385
|
-
}
|
|
77
|
+
const KPI_RATIO_DECIMALS = 4
|
|
386
78
|
|
|
387
79
|
export function computeTrustKpis(input: string, options?: TrustKpiOptions): TrustKpiReport {
|
|
388
80
|
const cwd = options?.cwd ?? process.cwd()
|
|
@@ -427,7 +119,7 @@ export function computeTrustKpis(input: string, options?: TrustKpiOptions): Trus
|
|
|
427
119
|
min: trustScores.length > 0 ? Math.min(...trustScores) : null,
|
|
428
120
|
max: trustScores.length > 0 ? Math.max(...trustScores) : null,
|
|
429
121
|
},
|
|
430
|
-
highRiskRatio: records.length > 0 ? round(highRiskCount / records.length,
|
|
122
|
+
highRiskRatio: records.length > 0 ? round(highRiskCount / records.length, KPI_RATIO_DECIMALS) : null,
|
|
431
123
|
diffTrend: buildDiffTrend(records),
|
|
432
124
|
diagnostics,
|
|
433
125
|
}
|
|
@@ -511,7 +203,7 @@ export function computeTrustKpisFromReports(reports: DriftTrustReport[]): TrustK
|
|
|
511
203
|
min: trustScores.length > 0 ? Math.min(...trustScores) : null,
|
|
512
204
|
max: trustScores.length > 0 ? Math.max(...trustScores) : null,
|
|
513
205
|
},
|
|
514
|
-
highRiskRatio: tempRecords.length > 0 ? round(highRiskCount / tempRecords.length,
|
|
206
|
+
highRiskRatio: tempRecords.length > 0 ? round(highRiskCount / tempRecords.length, KPI_RATIO_DECIMALS) : null,
|
|
515
207
|
diffTrend: buildDiffTrend(tempRecords),
|
|
516
208
|
diagnostics: [],
|
|
517
209
|
}
|
|
@@ -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
|
+
}
|