@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.
Files changed (198) hide show
  1. package/.gga +50 -0
  2. package/.github/actions/drift-review/README.md +62 -0
  3. package/.github/actions/drift-review/action.yml +148 -0
  4. package/.github/actions/drift-scan/README.md +28 -32
  5. package/.github/actions/drift-scan/action.yml +78 -14
  6. package/.github/workflows/publish-vscode.yml +1 -3
  7. package/.github/workflows/publish.yml +8 -0
  8. package/.github/workflows/quality.yml +15 -0
  9. package/.github/workflows/reusable-quality-checks.yml +95 -0
  10. package/.github/workflows/review-pr.yml +33 -41
  11. package/AGENTS.md +75 -251
  12. package/CHANGELOG.md +41 -0
  13. package/README.md +177 -43
  14. package/benchmarks/fixtures/critical/drift.config.ts +21 -0
  15. package/benchmarks/fixtures/critical/src/app/user-service.ts +30 -0
  16. package/benchmarks/fixtures/critical/src/domain/entities.ts +19 -0
  17. package/benchmarks/fixtures/critical/src/domain/policies.ts +22 -0
  18. package/benchmarks/fixtures/critical/src/index.ts +10 -0
  19. package/benchmarks/fixtures/critical/src/infra/memory-user-repo.ts +14 -0
  20. package/benchmarks/perf-budget.v1.json +27 -0
  21. package/dist/benchmark.d.ts +1 -1
  22. package/dist/benchmark.js +83 -52
  23. package/dist/cli.js +243 -8
  24. package/dist/config.js +16 -2
  25. package/dist/diff.js +42 -50
  26. package/dist/doctor.d.ts +26 -0
  27. package/dist/doctor.js +140 -0
  28. package/dist/format.d.ts +17 -0
  29. package/dist/format.js +45 -0
  30. package/dist/guard-baseline.d.ts +12 -0
  31. package/dist/guard-baseline.js +57 -0
  32. package/dist/guard-metrics.d.ts +6 -0
  33. package/dist/guard-metrics.js +39 -0
  34. package/dist/guard-types.d.ts +58 -0
  35. package/dist/guard-types.js +2 -0
  36. package/dist/guard.d.ts +16 -0
  37. package/dist/guard.js +178 -0
  38. package/dist/index.d.ts +10 -3
  39. package/dist/index.js +4 -1
  40. package/dist/init.d.ts +15 -0
  41. package/dist/init.js +273 -0
  42. package/dist/map-cycles.d.ts +2 -0
  43. package/dist/map-cycles.js +34 -0
  44. package/dist/map-svg.d.ts +19 -0
  45. package/dist/map-svg.js +97 -0
  46. package/dist/map.js +78 -138
  47. package/dist/metrics.js +70 -55
  48. package/dist/output-metadata.d.ts +15 -0
  49. package/dist/output-metadata.js +19 -0
  50. package/dist/plugins-capabilities.d.ts +4 -0
  51. package/dist/plugins-capabilities.js +21 -0
  52. package/dist/plugins-messages.d.ts +10 -0
  53. package/dist/plugins-messages.js +16 -0
  54. package/dist/plugins-rules.d.ts +9 -0
  55. package/dist/plugins-rules.js +137 -0
  56. package/dist/plugins.d.ts +1 -1
  57. package/dist/plugins.js +45 -142
  58. package/dist/reporter-constants.d.ts +16 -0
  59. package/dist/reporter-constants.js +39 -0
  60. package/dist/reporter.d.ts +3 -3
  61. package/dist/reporter.js +35 -55
  62. package/dist/review.d.ts +2 -1
  63. package/dist/review.js +2 -1
  64. package/dist/rules/phase3-configurable.js +23 -15
  65. package/dist/saas/constants.d.ts +15 -0
  66. package/dist/saas/constants.js +48 -0
  67. package/dist/saas/dashboard.d.ts +8 -0
  68. package/dist/saas/dashboard.js +132 -0
  69. package/dist/saas/errors.d.ts +19 -0
  70. package/dist/saas/errors.js +37 -0
  71. package/dist/saas/helpers.d.ts +21 -0
  72. package/dist/saas/helpers.js +110 -0
  73. package/dist/saas/ingest.d.ts +3 -0
  74. package/dist/saas/ingest.js +249 -0
  75. package/dist/saas/organization.d.ts +5 -0
  76. package/dist/saas/organization.js +82 -0
  77. package/dist/saas/plan-change.d.ts +10 -0
  78. package/dist/saas/plan-change.js +15 -0
  79. package/dist/saas/store.d.ts +21 -0
  80. package/dist/saas/store.js +159 -0
  81. package/dist/saas/types.d.ts +191 -0
  82. package/dist/saas/types.js +2 -0
  83. package/dist/saas.d.ts +8 -218
  84. package/dist/saas.js +7 -761
  85. package/dist/sarif.d.ts +74 -0
  86. package/dist/sarif.js +122 -0
  87. package/dist/trust-advanced.d.ts +14 -0
  88. package/dist/trust-advanced.js +65 -0
  89. package/dist/trust-kpi-fs.d.ts +3 -0
  90. package/dist/trust-kpi-fs.js +141 -0
  91. package/dist/trust-kpi-parse.d.ts +7 -0
  92. package/dist/trust-kpi-parse.js +186 -0
  93. package/dist/trust-kpi-types.d.ts +16 -0
  94. package/dist/trust-kpi-types.js +2 -0
  95. package/dist/trust-kpi.d.ts +1 -3
  96. package/dist/trust-kpi.js +6 -266
  97. package/dist/trust-policy.d.ts +32 -0
  98. package/dist/trust-policy.js +160 -0
  99. package/dist/trust-render.d.ts +9 -0
  100. package/dist/trust-render.js +54 -0
  101. package/dist/trust-scoring.d.ts +9 -0
  102. package/dist/trust-scoring.js +208 -0
  103. package/dist/trust.d.ts +5 -32
  104. package/dist/trust.js +29 -432
  105. package/dist/types/app.d.ts +30 -0
  106. package/dist/types/app.js +2 -0
  107. package/dist/types/config.d.ts +25 -0
  108. package/dist/types/config.js +2 -0
  109. package/dist/types/core.d.ts +100 -0
  110. package/dist/types/core.js +2 -0
  111. package/dist/types/diff.d.ts +55 -0
  112. package/dist/types/diff.js +2 -0
  113. package/dist/types/plugin.d.ts +41 -0
  114. package/dist/types/plugin.js +2 -0
  115. package/dist/types/trust.d.ts +120 -0
  116. package/dist/types/trust.js +2 -0
  117. package/dist/types.d.ts +8 -365
  118. package/docs/AGENTS.md +1 -1
  119. package/docs/release-notes-draft.md +40 -0
  120. package/docs/rules-catalog.md +49 -0
  121. package/docs/trust-core-release-checklist.md +37 -5
  122. package/package.json +11 -4
  123. package/packages/vscode-drift/src/code-actions.ts +1 -1
  124. package/schemas/drift-ai-output.v1.json +162 -0
  125. package/schemas/drift-doctor.v1.json +57 -0
  126. package/schemas/drift-guard.v1.json +298 -0
  127. package/schemas/drift-report.v1.json +151 -0
  128. package/schemas/drift-trust.v1.json +131 -0
  129. package/scripts/check-docs-drift.mjs +154 -0
  130. package/scripts/check-performance-budget.mjs +360 -0
  131. package/scripts/check-runtime-policy.mjs +66 -0
  132. package/scripts/smoke-repo.mjs +394 -0
  133. package/src/benchmark.ts +92 -53
  134. package/src/cli.ts +285 -13
  135. package/src/config.ts +19 -2
  136. package/src/diff.ts +57 -48
  137. package/src/doctor.ts +185 -0
  138. package/src/format.ts +81 -0
  139. package/src/guard-baseline.ts +74 -0
  140. package/src/guard-metrics.ts +52 -0
  141. package/src/guard-types.ts +66 -0
  142. package/src/guard.ts +248 -0
  143. package/src/index.ts +36 -0
  144. package/src/init.ts +298 -0
  145. package/src/map-cycles.ts +38 -0
  146. package/src/map-svg.ts +124 -0
  147. package/src/map.ts +111 -142
  148. package/src/metrics.ts +78 -59
  149. package/src/output-metadata.ts +32 -0
  150. package/src/plugins-capabilities.ts +36 -0
  151. package/src/plugins-messages.ts +35 -0
  152. package/src/plugins-rules.ts +296 -0
  153. package/src/plugins.ts +76 -283
  154. package/src/reporter-constants.ts +46 -0
  155. package/src/reporter.ts +64 -65
  156. package/src/review.ts +4 -2
  157. package/src/rules/phase3-configurable.ts +39 -26
  158. package/src/saas/constants.ts +56 -0
  159. package/src/saas/dashboard.ts +172 -0
  160. package/src/saas/errors.ts +45 -0
  161. package/src/saas/helpers.ts +140 -0
  162. package/src/saas/ingest.ts +278 -0
  163. package/src/saas/organization.ts +99 -0
  164. package/src/saas/plan-change.ts +19 -0
  165. package/src/saas/store.ts +172 -0
  166. package/src/saas/types.ts +216 -0
  167. package/src/saas.ts +49 -1031
  168. package/src/sarif.ts +232 -0
  169. package/src/trust-advanced.ts +99 -0
  170. package/src/trust-kpi-fs.ts +169 -0
  171. package/src/trust-kpi-parse.ts +219 -0
  172. package/src/trust-kpi-types.ts +19 -0
  173. package/src/trust-kpi.ts +8 -316
  174. package/src/trust-policy.ts +246 -0
  175. package/src/trust-render.ts +61 -0
  176. package/src/trust-scoring.ts +231 -0
  177. package/src/trust.ts +62 -576
  178. package/src/types/app.ts +30 -0
  179. package/src/types/config.ts +27 -0
  180. package/src/types/core.ts +105 -0
  181. package/src/types/diff.ts +61 -0
  182. package/src/types/plugin.ts +46 -0
  183. package/src/types/trust.ts +134 -0
  184. package/src/types.ts +79 -409
  185. package/tests/ci-quality-matrix.test.ts +37 -0
  186. package/tests/ci-smoke-gate.test.ts +26 -0
  187. package/tests/ci-version-alignment.test.ts +93 -0
  188. package/tests/cli-sarif.test.ts +92 -0
  189. package/tests/docs-drift-check.test.ts +115 -0
  190. package/tests/format.test.ts +157 -0
  191. package/tests/new-features.test.ts +11 -3
  192. package/tests/perf-budget-check.test.ts +146 -0
  193. package/tests/phase1-init-doctor-guard.test.ts +301 -0
  194. package/tests/runtime-policy-alignment.test.ts +46 -0
  195. package/tests/sarif.test.ts +160 -0
  196. package/tests/trust-kpi.test.ts +31 -4
  197. package/tests/trust.test.ts +18 -0
  198. 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 { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
2
- import { dirname, isAbsolute, resolve } from 'node:path'
3
- import { MERGE_RISK_ORDER, normalizeMergeRiskLevel } from './trust.js'
4
- import type { DriftTrustReport, MergeRiskLevel, TrustDiffContext, TrustDiffTrendSummary, TrustKpiDiagnostic, TrustKpiReport } from './types.js'
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
- export interface TrustKpiOptions {
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, 4) : null,
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, 4) : null,
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
+ }