@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.
Files changed (168) hide show
  1. package/.gga +50 -0
  2. package/.github/actions/drift-review/README.md +60 -0
  3. package/.github/actions/drift-review/action.yml +131 -0
  4. package/.github/actions/drift-scan/README.md +28 -32
  5. package/.github/actions/drift-scan/action.yml +78 -14
  6. package/.github/workflows/review-pr.yml +34 -41
  7. package/AGENTS.md +75 -251
  8. package/CHANGELOG.md +28 -0
  9. package/README.md +148 -41
  10. package/dist/benchmark.d.ts +1 -1
  11. package/dist/benchmark.js +71 -52
  12. package/dist/cli.js +243 -8
  13. package/dist/config.js +16 -2
  14. package/dist/diff.js +42 -50
  15. package/dist/doctor.d.ts +5 -0
  16. package/dist/doctor.js +133 -0
  17. package/dist/format.d.ts +17 -0
  18. package/dist/format.js +45 -0
  19. package/dist/guard-types.d.ts +57 -0
  20. package/dist/guard-types.js +2 -0
  21. package/dist/guard.d.ts +14 -0
  22. package/dist/guard.js +239 -0
  23. package/dist/index.d.ts +10 -3
  24. package/dist/index.js +4 -1
  25. package/dist/init.d.ts +15 -0
  26. package/dist/init.js +273 -0
  27. package/dist/map-cycles.d.ts +2 -0
  28. package/dist/map-cycles.js +34 -0
  29. package/dist/map-svg.d.ts +19 -0
  30. package/dist/map-svg.js +97 -0
  31. package/dist/map.js +78 -138
  32. package/dist/metrics.js +70 -55
  33. package/dist/output-metadata.d.ts +13 -0
  34. package/dist/output-metadata.js +17 -0
  35. package/dist/plugins-capabilities.d.ts +4 -0
  36. package/dist/plugins-capabilities.js +21 -0
  37. package/dist/plugins-messages.d.ts +10 -0
  38. package/dist/plugins-messages.js +16 -0
  39. package/dist/plugins-rules.d.ts +9 -0
  40. package/dist/plugins-rules.js +137 -0
  41. package/dist/plugins.d.ts +1 -1
  42. package/dist/plugins.js +45 -142
  43. package/dist/reporter-constants.d.ts +16 -0
  44. package/dist/reporter-constants.js +39 -0
  45. package/dist/reporter.d.ts +3 -3
  46. package/dist/reporter.js +35 -55
  47. package/dist/review.d.ts +2 -1
  48. package/dist/review.js +2 -1
  49. package/dist/rules/phase3-configurable.js +23 -15
  50. package/dist/saas/constants.d.ts +15 -0
  51. package/dist/saas/constants.js +48 -0
  52. package/dist/saas/dashboard.d.ts +8 -0
  53. package/dist/saas/dashboard.js +132 -0
  54. package/dist/saas/errors.d.ts +19 -0
  55. package/dist/saas/errors.js +37 -0
  56. package/dist/saas/helpers.d.ts +21 -0
  57. package/dist/saas/helpers.js +110 -0
  58. package/dist/saas/ingest.d.ts +3 -0
  59. package/dist/saas/ingest.js +249 -0
  60. package/dist/saas/organization.d.ts +5 -0
  61. package/dist/saas/organization.js +82 -0
  62. package/dist/saas/plan-change.d.ts +10 -0
  63. package/dist/saas/plan-change.js +15 -0
  64. package/dist/saas/store.d.ts +21 -0
  65. package/dist/saas/store.js +159 -0
  66. package/dist/saas/types.d.ts +191 -0
  67. package/dist/saas/types.js +2 -0
  68. package/dist/saas.d.ts +8 -218
  69. package/dist/saas.js +7 -761
  70. package/dist/sarif.d.ts +74 -0
  71. package/dist/sarif.js +122 -0
  72. package/dist/trust-advanced.d.ts +14 -0
  73. package/dist/trust-advanced.js +65 -0
  74. package/dist/trust-kpi-fs.d.ts +3 -0
  75. package/dist/trust-kpi-fs.js +141 -0
  76. package/dist/trust-kpi-parse.d.ts +7 -0
  77. package/dist/trust-kpi-parse.js +186 -0
  78. package/dist/trust-kpi-types.d.ts +16 -0
  79. package/dist/trust-kpi-types.js +2 -0
  80. package/dist/trust-kpi.d.ts +1 -3
  81. package/dist/trust-kpi.js +6 -266
  82. package/dist/trust-policy.d.ts +32 -0
  83. package/dist/trust-policy.js +160 -0
  84. package/dist/trust-render.d.ts +9 -0
  85. package/dist/trust-render.js +54 -0
  86. package/dist/trust-scoring.d.ts +9 -0
  87. package/dist/trust-scoring.js +208 -0
  88. package/dist/trust.d.ts +4 -32
  89. package/dist/trust.js +29 -432
  90. package/dist/types/app.d.ts +30 -0
  91. package/dist/types/app.js +2 -0
  92. package/dist/types/config.d.ts +25 -0
  93. package/dist/types/config.js +2 -0
  94. package/dist/types/core.d.ts +100 -0
  95. package/dist/types/core.js +2 -0
  96. package/dist/types/diff.d.ts +55 -0
  97. package/dist/types/diff.js +2 -0
  98. package/dist/types/plugin.d.ts +41 -0
  99. package/dist/types/plugin.js +2 -0
  100. package/dist/types/trust.d.ts +120 -0
  101. package/dist/types/trust.js +2 -0
  102. package/dist/types.d.ts +8 -365
  103. package/docs/release-notes-draft.md +40 -0
  104. package/docs/rules-catalog.md +49 -0
  105. package/docs/trust-core-release-checklist.md +37 -5
  106. package/package.json +3 -2
  107. package/packages/vscode-drift/src/code-actions.ts +1 -1
  108. package/schemas/drift-ai-output.v1.json +162 -0
  109. package/schemas/drift-report.v1.json +151 -0
  110. package/schemas/drift-trust.v1.json +131 -0
  111. package/scripts/smoke-repo.mjs +394 -0
  112. package/src/benchmark.ts +75 -53
  113. package/src/cli.ts +285 -13
  114. package/src/config.ts +19 -2
  115. package/src/diff.ts +57 -48
  116. package/src/doctor.ts +173 -0
  117. package/src/format.ts +81 -0
  118. package/src/guard-types.ts +64 -0
  119. package/src/guard.ts +324 -0
  120. package/src/index.ts +35 -0
  121. package/src/init.ts +298 -0
  122. package/src/map-cycles.ts +38 -0
  123. package/src/map-svg.ts +124 -0
  124. package/src/map.ts +111 -142
  125. package/src/metrics.ts +78 -59
  126. package/src/output-metadata.ts +30 -0
  127. package/src/plugins-capabilities.ts +36 -0
  128. package/src/plugins-messages.ts +35 -0
  129. package/src/plugins-rules.ts +296 -0
  130. package/src/plugins.ts +76 -283
  131. package/src/reporter-constants.ts +46 -0
  132. package/src/reporter.ts +64 -65
  133. package/src/review.ts +4 -2
  134. package/src/rules/phase3-configurable.ts +39 -26
  135. package/src/saas/constants.ts +56 -0
  136. package/src/saas/dashboard.ts +172 -0
  137. package/src/saas/errors.ts +45 -0
  138. package/src/saas/helpers.ts +140 -0
  139. package/src/saas/ingest.ts +278 -0
  140. package/src/saas/organization.ts +99 -0
  141. package/src/saas/plan-change.ts +19 -0
  142. package/src/saas/store.ts +172 -0
  143. package/src/saas/types.ts +216 -0
  144. package/src/saas.ts +49 -1031
  145. package/src/sarif.ts +232 -0
  146. package/src/trust-advanced.ts +99 -0
  147. package/src/trust-kpi-fs.ts +169 -0
  148. package/src/trust-kpi-parse.ts +219 -0
  149. package/src/trust-kpi-types.ts +19 -0
  150. package/src/trust-kpi.ts +8 -316
  151. package/src/trust-policy.ts +246 -0
  152. package/src/trust-render.ts +61 -0
  153. package/src/trust-scoring.ts +231 -0
  154. package/src/trust.ts +62 -576
  155. package/src/types/app.ts +30 -0
  156. package/src/types/config.ts +27 -0
  157. package/src/types/core.ts +105 -0
  158. package/src/types/diff.ts +61 -0
  159. package/src/types/plugin.ts +46 -0
  160. package/src/types/trust.ts +134 -0
  161. package/src/types.ts +78 -409
  162. package/tests/cli-sarif.test.ts +92 -0
  163. package/tests/format.test.ts +157 -0
  164. package/tests/new-features.test.ts +10 -2
  165. package/tests/phase1-init-doctor-guard.test.ts +199 -0
  166. package/tests/sarif.test.ts +160 -0
  167. package/tests/trust-kpi.test.ts +31 -4
  168. package/tests/trust.test.ts +18 -0
package/src/doctor.ts ADDED
@@ -0,0 +1,173 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import kleur from 'kleur'
4
+
5
+ export interface DoctorOptions {
6
+ json?: boolean
7
+ }
8
+
9
+ interface DoctorReport {
10
+ targetPath: string
11
+ node: {
12
+ version: string
13
+ major: number
14
+ supported: boolean
15
+ }
16
+ project: {
17
+ packageJsonFound: boolean
18
+ esm: boolean
19
+ tsconfigFound: boolean
20
+ sourceFilesCount: number
21
+ lowMemorySuggested: boolean
22
+ driftConfigFile: string | null
23
+ }
24
+ }
25
+
26
+ const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx'])
27
+ const IGNORED_DIRECTORIES = new Set(['node_modules', '.git', 'dist', '.next', 'coverage'])
28
+ const DECIMAL_RADIX = 10
29
+ const MIN_SUPPORTED_NODE_MAJOR = 18
30
+ const LOW_MEMORY_SOURCE_FILE_THRESHOLD = 500
31
+ const DRIFT_CONFIG_CANDIDATES = [
32
+ 'drift.config.ts',
33
+ 'drift.config.js',
34
+ 'drift.config.mjs',
35
+ 'drift.config.cjs',
36
+ 'drift.config.json',
37
+ ] as const
38
+
39
+ function parseNodeMajor(version: string): number {
40
+ const parsed = Number.parseInt(version.replace(/^v/, '').split('.')[0] ?? '0', DECIMAL_RADIX)
41
+ return Number.isFinite(parsed) ? parsed : 0
42
+ }
43
+
44
+ function detectDriftConfig(projectPath: string): string | null {
45
+ for (const candidate of DRIFT_CONFIG_CANDIDATES) {
46
+ if (existsSync(join(projectPath, candidate))) {
47
+ return candidate
48
+ }
49
+ }
50
+ return null
51
+ }
52
+
53
+ function countSourceFiles(projectPath: string): number {
54
+ let total = 0
55
+ const stack = [projectPath]
56
+
57
+ while (stack.length > 0) {
58
+ const currentDir = stack.pop()
59
+ if (!currentDir) continue
60
+
61
+ const entries = readdirSync(currentDir, { withFileTypes: true })
62
+ for (const entry of entries) {
63
+ if (entry.isDirectory() && !IGNORED_DIRECTORIES.has(entry.name)) {
64
+ stack.push(join(currentDir, entry.name))
65
+ continue
66
+ }
67
+
68
+ if (entry.isDirectory()) {
69
+ continue
70
+ }
71
+
72
+ if (!entry.isFile()) continue
73
+
74
+ const lastDot = entry.name.lastIndexOf('.')
75
+ if (lastDot === -1) continue
76
+
77
+ const extension = entry.name.slice(lastDot)
78
+ if (SOURCE_EXTENSIONS.has(extension)) {
79
+ total += 1
80
+ }
81
+ }
82
+ }
83
+
84
+ return total
85
+ }
86
+
87
+ function buildDoctorReport(projectPath: string): DoctorReport {
88
+ const nodeMajor = parseNodeMajor(process.version)
89
+ const packageJsonPath = join(projectPath, 'package.json')
90
+ const packageJsonFound = existsSync(packageJsonPath)
91
+
92
+ let esm = false
93
+ if (packageJsonFound) {
94
+ const parsed = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as { type?: string }
95
+ esm = parsed.type === 'module'
96
+ }
97
+
98
+ const sourceFilesCount = countSourceFiles(projectPath)
99
+
100
+ return {
101
+ targetPath: projectPath,
102
+ node: {
103
+ version: process.version,
104
+ major: nodeMajor,
105
+ supported: nodeMajor >= MIN_SUPPORTED_NODE_MAJOR,
106
+ },
107
+ project: {
108
+ packageJsonFound,
109
+ esm,
110
+ tsconfigFound: existsSync(join(projectPath, 'tsconfig.json')),
111
+ sourceFilesCount,
112
+ lowMemorySuggested: sourceFilesCount > LOW_MEMORY_SOURCE_FILE_THRESHOLD,
113
+ driftConfigFile: detectDriftConfig(projectPath),
114
+ },
115
+ }
116
+ }
117
+
118
+ function printConsoleReport(report: DoctorReport): void {
119
+ const icons = {
120
+ check: kleur.green('✓'),
121
+ warn: kleur.yellow('⚠'),
122
+ error: kleur.red('✗'),
123
+ info: kleur.cyan('ℹ'),
124
+ }
125
+
126
+ process.stdout.write('\n')
127
+ process.stdout.write(`${kleur.bold().white('drift doctor')} ${kleur.gray('- environment diagnostics')}\n\n`)
128
+
129
+ const nodeStatus = report.node.supported
130
+ ? `${icons.check} ${kleur.green('Node runtime supported')}`
131
+ : `${icons.warn} ${kleur.yellow('Node runtime below recommended minimum (>=18)')}`
132
+ process.stdout.write(`${nodeStatus} ${kleur.gray(`(${report.node.version})`)}\n`)
133
+
134
+ if (report.project.packageJsonFound) {
135
+ process.stdout.write(`${icons.check} package.json found\n`)
136
+ process.stdout.write(`${icons.info} ESM mode: ${report.project.esm ? kleur.green('yes') : kleur.yellow('no')}\n`)
137
+ } else {
138
+ process.stdout.write(`${icons.warn} package.json not found\n`)
139
+ process.stdout.write(`${icons.info} ESM mode: ${kleur.gray('unknown')}\n`)
140
+ }
141
+
142
+ if (report.project.tsconfigFound) {
143
+ process.stdout.write(`${icons.check} tsconfig.json found\n`)
144
+ } else {
145
+ process.stdout.write(`${icons.warn} tsconfig.json not found\n`)
146
+ }
147
+
148
+ process.stdout.write(`${icons.info} Source files (.ts/.tsx/.js/.jsx): ${report.project.sourceFilesCount}\n`)
149
+
150
+ if (report.project.lowMemorySuggested) {
151
+ process.stdout.write(`${icons.warn} Large codebase detected, consider ${kleur.bold('--low-memory')}\n`)
152
+ }
153
+
154
+ if (report.project.driftConfigFile) {
155
+ process.stdout.write(`${icons.check} Drift config: ${report.project.driftConfigFile}\n`)
156
+ } else {
157
+ process.stdout.write(`${icons.warn} Drift config not found (drift.config.*)\n`)
158
+ }
159
+
160
+ process.stdout.write('\n')
161
+ }
162
+
163
+ export async function runDoctor(projectPath: string, options?: DoctorOptions): Promise<number> {
164
+ const report = buildDoctorReport(projectPath)
165
+
166
+ if (options?.json) {
167
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`)
168
+ } else {
169
+ printConsoleReport(report)
170
+ }
171
+
172
+ return 0
173
+ }
package/src/format.ts ADDED
@@ -0,0 +1,81 @@
1
+ const UNIFIED_FORMAT_VALUES = ['console', 'json', 'markdown', 'ai', 'sarif'] as const
2
+
3
+ type UnifiedOutputFormat = (typeof UNIFIED_FORMAT_VALUES)[number]
4
+
5
+ type LegacyAlias = {
6
+ flag: string
7
+ used?: boolean
8
+ mapsTo: UnifiedOutputFormat
9
+ }
10
+
11
+ interface ResolveOutputFormatOptions {
12
+ command: string
13
+ format?: string
14
+ supported: readonly UnifiedOutputFormat[]
15
+ legacyAliases?: LegacyAlias[]
16
+ onWarning?: (message: string) => void
17
+ }
18
+
19
+ function assertSupportedFormatValue(command: string, format: string): asserts format is UnifiedOutputFormat {
20
+ if ((UNIFIED_FORMAT_VALUES as readonly string[]).includes(format)) return
21
+ throw new Error(
22
+ `Invalid --format '${format}' for '${command}'. Allowed values: ${UNIFIED_FORMAT_VALUES.join(', ')}.`,
23
+ )
24
+ }
25
+
26
+ function throwUnsupportedFormat(command: string, selected: UnifiedOutputFormat, supported: readonly UnifiedOutputFormat[]): never {
27
+ throw new Error(
28
+ `Format '${selected}' is not supported for '${command}'. Supported formats: ${supported.join(', ')}.`,
29
+ )
30
+ }
31
+
32
+ function normalizeLegacyFormatSelection(command: string, selectedLegacyFormats: UnifiedOutputFormat[]): UnifiedOutputFormat | undefined {
33
+ if (selectedLegacyFormats.length === 0) return undefined
34
+
35
+ const uniqueFormats = [...new Set(selectedLegacyFormats)]
36
+ if (uniqueFormats.length > 1) {
37
+ throw new Error(
38
+ `Conflicting legacy format flags for '${command}': ${uniqueFormats.join(' vs ')}. Use a single format option.`,
39
+ )
40
+ }
41
+
42
+ return uniqueFormats[0]
43
+ }
44
+
45
+ export function resolveOutputFormat(options: ResolveOutputFormatOptions): UnifiedOutputFormat {
46
+ const { command, format, supported, onWarning } = options
47
+ const legacyAliases = options.legacyAliases ?? []
48
+
49
+ for (const alias of legacyAliases) {
50
+ if (!alias.used) continue
51
+ onWarning?.(`Warning: --${alias.flag} is deprecated for '${command}'. Use --format ${alias.mapsTo} instead.`)
52
+ }
53
+
54
+ const selectedLegacyFormat = normalizeLegacyFormatSelection(
55
+ command,
56
+ legacyAliases.filter((alias) => alias.used).map((alias) => alias.mapsTo),
57
+ )
58
+
59
+ const selectedFormat = format?.trim()
60
+ if (selectedFormat) {
61
+ assertSupportedFormatValue(command, selectedFormat)
62
+ if (selectedLegacyFormat && selectedLegacyFormat !== selectedFormat) {
63
+ throw new Error(
64
+ `Conflicting format flags for '${command}': --format ${selectedFormat} and legacy alias for ${selectedLegacyFormat}.`,
65
+ )
66
+ }
67
+
68
+ if (!supported.includes(selectedFormat)) {
69
+ throwUnsupportedFormat(command, selectedFormat, supported)
70
+ }
71
+
72
+ return selectedFormat
73
+ }
74
+
75
+ const resolvedFromLegacy = selectedLegacyFormat ?? 'console'
76
+ if (!supported.includes(resolvedFromLegacy)) {
77
+ throwUnsupportedFormat(command, resolvedFromLegacy, supported)
78
+ }
79
+
80
+ return resolvedFromLegacy
81
+ }
@@ -0,0 +1,64 @@
1
+ import type { DriftAnalysisOptions, DriftDiff, DriftIssue, DriftReport } from './types.js'
2
+
3
+ export type IssueSeverity = DriftIssue['severity']
4
+
5
+ export interface GuardBaseline {
6
+ score?: number
7
+ totalIssues?: number
8
+ errors?: number
9
+ warnings?: number
10
+ infos?: number
11
+ bySeverity?: Partial<Record<IssueSeverity, number>>
12
+ summary?: {
13
+ errors?: number
14
+ warnings?: number
15
+ infos?: number
16
+ }
17
+ }
18
+
19
+ export interface GuardThresholds {
20
+ error?: number
21
+ warning?: number
22
+ info?: number
23
+ }
24
+
25
+ export interface GuardOptions {
26
+ baseRef?: string
27
+ baselinePath?: string
28
+ baseline?: GuardBaseline
29
+ budget?: number
30
+ bySeverity?: GuardThresholds
31
+ analysis?: DriftAnalysisOptions
32
+ }
33
+
34
+ export interface GuardMetrics {
35
+ scoreDelta: number
36
+ totalIssuesDelta: number
37
+ severityDelta: Record<IssueSeverity, number>
38
+ }
39
+
40
+ export interface GuardCheck {
41
+ id: string
42
+ passed: boolean
43
+ actual: number
44
+ limit: number
45
+ message: string
46
+ }
47
+
48
+ export interface GuardEvaluation {
49
+ passed: boolean
50
+ checks: GuardCheck[]
51
+ }
52
+
53
+ export interface GuardResult {
54
+ scannedAt: string
55
+ projectPath: string
56
+ mode: 'diff' | 'baseline'
57
+ passed: boolean
58
+ baseRef?: string
59
+ baselinePath?: string
60
+ metrics: GuardMetrics
61
+ checks: GuardCheck[]
62
+ current: DriftReport
63
+ diff?: DriftDiff
64
+ }
package/src/guard.ts ADDED
@@ -0,0 +1,324 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { relative, resolve } from 'node:path'
3
+ import { analyzeProject } from './analyzer.js'
4
+ import { loadConfig } from './config.js'
5
+ import { computeDiff } from './diff.js'
6
+ import { cleanupTempDir, extractFilesAtRef } from './git.js'
7
+ import { buildReport } from './reporter.js'
8
+ import type { DriftDiff, DriftReport } from './types.js'
9
+ import type {
10
+ GuardBaseline,
11
+ GuardCheck,
12
+ GuardEvaluation,
13
+ GuardMetrics,
14
+ GuardOptions,
15
+ GuardResult,
16
+ GuardThresholds,
17
+ IssueSeverity,
18
+ } from './guard-types.js'
19
+
20
+ interface NormalizedBaseline {
21
+ score?: number
22
+ totalIssues?: number
23
+ bySeverity: Partial<Record<IssueSeverity, number>>
24
+ }
25
+
26
+ interface GuardEvalInput {
27
+ metrics: GuardMetrics
28
+ budget?: number
29
+ bySeverity?: GuardThresholds
30
+ enforceNoRegression: {
31
+ score: boolean
32
+ totalIssues: boolean
33
+ }
34
+ }
35
+
36
+ interface GuardRuntimeState {
37
+ currentReport: DriftReport
38
+ config: Awaited<ReturnType<typeof loadConfig>>
39
+ projectPath: string
40
+ }
41
+
42
+ interface DiffGuardResultInput {
43
+ projectPath: string
44
+ currentReport: DriftReport
45
+ options: GuardOptions
46
+ tempDir: string
47
+ config: Awaited<ReturnType<typeof loadConfig>>
48
+ baseRef: string
49
+ }
50
+
51
+ interface BaselineGuardResultInput {
52
+ projectPath: string
53
+ currentReport: DriftReport
54
+ options: GuardOptions
55
+ baseline: NormalizedBaseline
56
+ baselinePath?: string
57
+ }
58
+
59
+ function parseNumber(value: unknown): number | undefined {
60
+ return typeof value === 'number' && !Number.isNaN(value) ? value : undefined
61
+ }
62
+
63
+ function normalizeBaseline(baseline: GuardBaseline): NormalizedBaseline {
64
+ const bySeverityFromRoot = baseline.bySeverity
65
+ const bySeverity = {
66
+ error: parseNumber(bySeverityFromRoot?.error) ?? parseNumber(baseline.errors) ?? parseNumber(baseline.summary?.errors),
67
+ warning: parseNumber(bySeverityFromRoot?.warning) ?? parseNumber(baseline.warnings) ?? parseNumber(baseline.summary?.warnings),
68
+ info: parseNumber(bySeverityFromRoot?.info) ?? parseNumber(baseline.infos) ?? parseNumber(baseline.summary?.infos),
69
+ }
70
+
71
+ const normalized: NormalizedBaseline = {
72
+ score: parseNumber(baseline.score),
73
+ totalIssues: parseNumber(baseline.totalIssues),
74
+ bySeverity,
75
+ }
76
+
77
+ const hasAnyAnchor =
78
+ normalized.score !== undefined ||
79
+ normalized.totalIssues !== undefined ||
80
+ normalized.bySeverity.error !== undefined ||
81
+ normalized.bySeverity.warning !== undefined ||
82
+ normalized.bySeverity.info !== undefined
83
+
84
+ if (!hasAnyAnchor) {
85
+ throw new Error('Invalid guard baseline: expected score, totalIssues, or severity counters (error/warning/info).')
86
+ }
87
+
88
+ return normalized
89
+ }
90
+
91
+ function readBaselineFromFile(projectPath: string, baselinePath?: string): { baseline: NormalizedBaseline; path: string } | undefined {
92
+ const resolvedBaselinePath = resolve(projectPath, baselinePath ?? 'drift-baseline.json')
93
+ if (!existsSync(resolvedBaselinePath)) return undefined
94
+
95
+ const raw = JSON.parse(readFileSync(resolvedBaselinePath, 'utf8')) as GuardBaseline
96
+ return {
97
+ baseline: normalizeBaseline(raw),
98
+ path: resolvedBaselinePath,
99
+ }
100
+ }
101
+
102
+ function remapBaseReportPaths(baseReport: DriftReport, tempDir: string, projectPath: string): DriftReport {
103
+ return {
104
+ ...baseReport,
105
+ files: baseReport.files.map((file) => ({
106
+ ...file,
107
+ path: resolve(projectPath, relative(tempDir, file.path)),
108
+ })),
109
+ }
110
+ }
111
+
112
+ function countSeverityDeltaFromDiff(diff: DriftDiff): Record<IssueSeverity, number> {
113
+ const severityDelta: Record<IssueSeverity, number> = {
114
+ error: 0,
115
+ warning: 0,
116
+ info: 0,
117
+ }
118
+
119
+ for (const file of diff.files) {
120
+ for (const issue of file.newIssues) {
121
+ severityDelta[issue.severity] += 1
122
+ }
123
+ for (const issue of file.resolvedIssues) {
124
+ severityDelta[issue.severity] -= 1
125
+ }
126
+ }
127
+
128
+ return severityDelta
129
+ }
130
+
131
+ function buildMetricsFromDiff(diff: DriftDiff): GuardMetrics {
132
+ return {
133
+ scoreDelta: diff.totalDelta,
134
+ totalIssuesDelta: diff.newIssuesCount - diff.resolvedIssuesCount,
135
+ severityDelta: countSeverityDeltaFromDiff(diff),
136
+ }
137
+ }
138
+
139
+ function buildMetricsFromBaseline(current: DriftReport, baseline: NormalizedBaseline): GuardMetrics {
140
+ return {
141
+ scoreDelta: current.totalScore - (baseline.score ?? current.totalScore),
142
+ totalIssuesDelta: current.totalIssues - (baseline.totalIssues ?? current.totalIssues),
143
+ severityDelta: {
144
+ error: current.summary.errors - (baseline.bySeverity.error ?? current.summary.errors),
145
+ warning: current.summary.warnings - (baseline.bySeverity.warning ?? current.summary.warnings),
146
+ info: current.summary.infos - (baseline.bySeverity.info ?? current.summary.infos),
147
+ },
148
+ }
149
+ }
150
+
151
+ interface GuardCheckInput {
152
+ id: string
153
+ actual: number
154
+ limit: number
155
+ message: string
156
+ }
157
+
158
+ function addCheck(checks: GuardCheck[], input: GuardCheckInput): void {
159
+ checks.push({
160
+ id: input.id,
161
+ passed: input.actual <= input.limit,
162
+ actual: input.actual,
163
+ limit: input.limit,
164
+ message: input.message,
165
+ })
166
+ }
167
+
168
+ export function evaluateGuard(input: GuardEvalInput): GuardEvaluation {
169
+ const checks: GuardCheck[] = []
170
+
171
+ if (input.enforceNoRegression.score) {
172
+ addCheck(checks, {
173
+ id: 'no-regression-score',
174
+ actual: input.metrics.scoreDelta,
175
+ limit: 0,
176
+ message: 'Score delta must be <= 0.',
177
+ })
178
+ }
179
+
180
+ if (input.enforceNoRegression.totalIssues) {
181
+ addCheck(checks, {
182
+ id: 'no-regression-total-issues',
183
+ actual: input.metrics.totalIssuesDelta,
184
+ limit: 0,
185
+ message: 'Total issues delta must be <= 0.',
186
+ })
187
+ }
188
+
189
+ if (typeof input.budget === 'number' && !Number.isNaN(input.budget)) {
190
+ addCheck(checks, {
191
+ id: 'budget-total-delta',
192
+ actual: input.metrics.scoreDelta,
193
+ limit: input.budget,
194
+ message: `Score delta must be <= budget (${input.budget}).`,
195
+ })
196
+ }
197
+
198
+ const severityThresholds = input.bySeverity
199
+ if (severityThresholds) {
200
+ const severities: IssueSeverity[] = ['error', 'warning', 'info']
201
+ for (const severity of severities) {
202
+ const threshold = severityThresholds[severity]
203
+ if (typeof threshold !== 'number' || Number.isNaN(threshold)) continue
204
+ addCheck(checks, {
205
+ id: `severity-${severity}`,
206
+ actual: input.metrics.severityDelta[severity],
207
+ limit: threshold,
208
+ message: `${severity} delta must be <= ${threshold}.`,
209
+ })
210
+ }
211
+ }
212
+
213
+ return {
214
+ passed: checks.every((check) => check.passed),
215
+ checks,
216
+ }
217
+ }
218
+
219
+ export async function runGuard(targetPath: string, options: GuardOptions = {}): Promise<GuardResult> {
220
+ const runtimeState = await initializeGuardRuntime(targetPath, options)
221
+ const { projectPath, config, currentReport } = runtimeState
222
+
223
+ let tempDir: string | undefined
224
+ try {
225
+ if (options.baseRef) {
226
+ tempDir = extractFilesAtRef(projectPath, options.baseRef)
227
+ return createDiffGuardResult({
228
+ projectPath,
229
+ currentReport,
230
+ options,
231
+ tempDir,
232
+ config,
233
+ baseRef: options.baseRef,
234
+ })
235
+ }
236
+
237
+ const inlineBaseline = options.baseline ? normalizeBaseline(options.baseline) : undefined
238
+ const fileBaseline = inlineBaseline ? undefined : readBaselineFromFile(projectPath, options.baselinePath)
239
+ const baseline = inlineBaseline ?? fileBaseline?.baseline
240
+ const baselinePath = fileBaseline?.path
241
+
242
+ if (!baseline) {
243
+ throw new Error('Guard requires a comparison point: provide baseRef or a baseline (inline or file).')
244
+ }
245
+
246
+ return createBaselineGuardResult({
247
+ projectPath,
248
+ currentReport,
249
+ options,
250
+ baseline,
251
+ baselinePath,
252
+ })
253
+ } finally {
254
+ if (tempDir) cleanupTempDir(tempDir)
255
+ }
256
+ }
257
+
258
+ async function initializeGuardRuntime(targetPath: string, options: GuardOptions): Promise<GuardRuntimeState> {
259
+ const projectPath = resolve(targetPath)
260
+ const config = await loadConfig(projectPath)
261
+ const currentFiles = analyzeProject(projectPath, config, options.analysis)
262
+ const currentReport = buildReport(projectPath, currentFiles)
263
+
264
+ return {
265
+ projectPath,
266
+ config,
267
+ currentReport,
268
+ }
269
+ }
270
+
271
+ function createDiffGuardResult(input: DiffGuardResultInput): GuardResult {
272
+ const { projectPath, currentReport, options, tempDir, config, baseRef } = input
273
+ const baseFiles = analyzeProject(tempDir, config, options.analysis)
274
+ const baseReport = buildReport(tempDir, baseFiles)
275
+ const remappedBase = remapBaseReportPaths(baseReport, tempDir, projectPath)
276
+ const diff = computeDiff(remappedBase, currentReport, baseRef)
277
+ const metrics = buildMetricsFromDiff(diff)
278
+ const evaluation = evaluateGuard({
279
+ metrics,
280
+ budget: options.budget,
281
+ bySeverity: options.bySeverity,
282
+ enforceNoRegression: {
283
+ score: true,
284
+ totalIssues: true,
285
+ },
286
+ })
287
+
288
+ return {
289
+ scannedAt: new Date().toISOString(),
290
+ projectPath,
291
+ mode: 'diff',
292
+ passed: evaluation.passed,
293
+ baseRef,
294
+ metrics,
295
+ checks: evaluation.checks,
296
+ current: currentReport,
297
+ diff,
298
+ }
299
+ }
300
+
301
+ function createBaselineGuardResult(input: BaselineGuardResultInput): GuardResult {
302
+ const { projectPath, currentReport, options, baseline, baselinePath } = input
303
+ const metrics = buildMetricsFromBaseline(currentReport, baseline)
304
+ const evaluation = evaluateGuard({
305
+ metrics,
306
+ budget: options.budget,
307
+ bySeverity: options.bySeverity,
308
+ enforceNoRegression: {
309
+ score: baseline.score !== undefined,
310
+ totalIssues: baseline.totalIssues !== undefined,
311
+ },
312
+ })
313
+
314
+ return {
315
+ scannedAt: new Date().toISOString(),
316
+ projectPath,
317
+ mode: 'baseline',
318
+ passed: evaluation.passed,
319
+ baselinePath,
320
+ metrics,
321
+ checks: evaluation.checks,
322
+ current: currentReport,
323
+ }
324
+ }
package/src/index.ts CHANGED
@@ -1,23 +1,51 @@
1
1
  export { analyzeProject, analyzeFile, RULE_WEIGHTS } from './analyzer.js'
2
2
  export { buildReport, formatMarkdown } from './reporter.js'
3
3
  export { computeDiff } from './diff.js'
4
+ export { runGuard, evaluateGuard } from './guard.js'
5
+ export type {
6
+ GuardBaseline,
7
+ GuardThresholds,
8
+ GuardOptions,
9
+ GuardMetrics,
10
+ GuardCheck,
11
+ GuardEvaluation,
12
+ GuardResult,
13
+ } from './guard-types.js'
4
14
  export { generateReview, formatReviewMarkdown } from './review.js'
15
+ export { runDoctor } from './doctor.js'
16
+ export type { DoctorOptions } from './doctor.js'
5
17
  export {
6
18
  buildTrustReport,
7
19
  formatTrustConsole,
8
20
  formatTrustMarkdown,
9
21
  formatTrustJson,
22
+ resolveTrustGatePolicy,
23
+ evaluateTrustGate,
10
24
  shouldFailByMaxRisk,
11
25
  shouldFailTrustGate,
12
26
  normalizeMergeRiskLevel,
13
27
  MERGE_RISK_ORDER,
14
28
  } from './trust.js'
29
+ export type {
30
+ TrustGateOptions,
31
+ TrustGatePolicyResolutionOptions,
32
+ TrustGatePolicyResolutionStep,
33
+ TrustGateEvaluation,
34
+ } from './trust.js'
15
35
  export {
16
36
  computeTrustKpis,
17
37
  computeTrustKpisFromReports,
18
38
  formatTrustKpiConsole,
19
39
  formatTrustKpiJson,
20
40
  } from './trust-kpi.js'
41
+ export { toSarif, diffToSarif } from './sarif.js'
42
+ export type {
43
+ SarifLevel,
44
+ DriftSarifRule,
45
+ DriftSarifResult,
46
+ DriftSarifRun,
47
+ DriftSarifLog,
48
+ } from './sarif.js'
21
49
  export { generateArchitectureMap, generateArchitectureSvg } from './map.js'
22
50
  export type {
23
51
  DriftReport,
@@ -39,6 +67,8 @@ export type {
39
67
  MergeRiskLevel,
40
68
  DriftPlugin,
41
69
  DriftPluginRule,
70
+ TrustGatePolicyConfig,
71
+ TrustAdvancedContext,
42
72
  } from './types.js'
43
73
  export { loadHistory, saveSnapshot } from './snapshot.js'
44
74
  export type { SnapshotEntry, SnapshotHistory } from './snapshot.js'
@@ -61,6 +91,11 @@ export {
61
91
  generateSaasDashboardHtml,
62
92
  } from './saas.js'
63
93
  export type {
94
+ SaasUser,
95
+ SaasOrganization,
96
+ SaasWorkspace,
97
+ SaasRepo,
98
+ SaasMembership,
64
99
  SaasRole,
65
100
  SaasPlan,
66
101
  SaasPolicy,