@eduardbar/drift 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) 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/publish-vscode.yml +3 -3
  7. package/.github/workflows/publish.yml +3 -3
  8. package/.github/workflows/review-pr.yml +94 -9
  9. package/AGENTS.md +75 -245
  10. package/CHANGELOG.md +28 -0
  11. package/README.md +308 -51
  12. package/ROADMAP.md +6 -5
  13. package/dist/analyzer.d.ts +2 -2
  14. package/dist/analyzer.js +420 -159
  15. package/dist/benchmark.d.ts +2 -0
  16. package/dist/benchmark.js +204 -0
  17. package/dist/cli.js +693 -67
  18. package/dist/config.js +16 -2
  19. package/dist/diff.js +66 -10
  20. package/dist/doctor.d.ts +5 -0
  21. package/dist/doctor.js +133 -0
  22. package/dist/format.d.ts +17 -0
  23. package/dist/format.js +45 -0
  24. package/dist/git.js +12 -0
  25. package/dist/guard-types.d.ts +57 -0
  26. package/dist/guard-types.js +2 -0
  27. package/dist/guard.d.ts +14 -0
  28. package/dist/guard.js +239 -0
  29. package/dist/index.d.ts +12 -3
  30. package/dist/index.js +6 -1
  31. package/dist/init.d.ts +15 -0
  32. package/dist/init.js +273 -0
  33. package/dist/map-cycles.d.ts +2 -0
  34. package/dist/map-cycles.js +34 -0
  35. package/dist/map-svg.d.ts +19 -0
  36. package/dist/map-svg.js +97 -0
  37. package/dist/map.js +78 -138
  38. package/dist/metrics.js +70 -55
  39. package/dist/output-metadata.d.ts +13 -0
  40. package/dist/output-metadata.js +17 -0
  41. package/dist/plugins-capabilities.d.ts +4 -0
  42. package/dist/plugins-capabilities.js +21 -0
  43. package/dist/plugins-messages.d.ts +10 -0
  44. package/dist/plugins-messages.js +16 -0
  45. package/dist/plugins-rules.d.ts +9 -0
  46. package/dist/plugins-rules.js +137 -0
  47. package/dist/plugins.d.ts +2 -1
  48. package/dist/plugins.js +80 -28
  49. package/dist/printer.js +4 -0
  50. package/dist/reporter-constants.d.ts +16 -0
  51. package/dist/reporter-constants.js +39 -0
  52. package/dist/reporter.d.ts +3 -3
  53. package/dist/reporter.js +35 -55
  54. package/dist/review.d.ts +2 -1
  55. package/dist/review.js +4 -3
  56. package/dist/rules/comments.js +2 -2
  57. package/dist/rules/complexity.js +2 -7
  58. package/dist/rules/nesting.js +3 -13
  59. package/dist/rules/phase0-basic.js +10 -10
  60. package/dist/rules/phase3-configurable.js +23 -15
  61. package/dist/rules/shared.d.ts +2 -0
  62. package/dist/rules/shared.js +27 -3
  63. package/dist/saas/constants.d.ts +15 -0
  64. package/dist/saas/constants.js +48 -0
  65. package/dist/saas/dashboard.d.ts +8 -0
  66. package/dist/saas/dashboard.js +132 -0
  67. package/dist/saas/errors.d.ts +19 -0
  68. package/dist/saas/errors.js +37 -0
  69. package/dist/saas/helpers.d.ts +21 -0
  70. package/dist/saas/helpers.js +110 -0
  71. package/dist/saas/ingest.d.ts +3 -0
  72. package/dist/saas/ingest.js +249 -0
  73. package/dist/saas/organization.d.ts +5 -0
  74. package/dist/saas/organization.js +82 -0
  75. package/dist/saas/plan-change.d.ts +10 -0
  76. package/dist/saas/plan-change.js +15 -0
  77. package/dist/saas/store.d.ts +21 -0
  78. package/dist/saas/store.js +159 -0
  79. package/dist/saas/types.d.ts +191 -0
  80. package/dist/saas/types.js +2 -0
  81. package/dist/saas.d.ts +8 -82
  82. package/dist/saas.js +7 -320
  83. package/dist/sarif.d.ts +74 -0
  84. package/dist/sarif.js +122 -0
  85. package/dist/trust-advanced.d.ts +14 -0
  86. package/dist/trust-advanced.js +65 -0
  87. package/dist/trust-kpi-fs.d.ts +3 -0
  88. package/dist/trust-kpi-fs.js +141 -0
  89. package/dist/trust-kpi-parse.d.ts +7 -0
  90. package/dist/trust-kpi-parse.js +186 -0
  91. package/dist/trust-kpi-types.d.ts +16 -0
  92. package/dist/trust-kpi-types.js +2 -0
  93. package/dist/trust-kpi.d.ts +7 -0
  94. package/dist/trust-kpi.js +185 -0
  95. package/dist/trust-policy.d.ts +32 -0
  96. package/dist/trust-policy.js +160 -0
  97. package/dist/trust-render.d.ts +9 -0
  98. package/dist/trust-render.js +54 -0
  99. package/dist/trust-scoring.d.ts +9 -0
  100. package/dist/trust-scoring.js +208 -0
  101. package/dist/trust.d.ts +37 -0
  102. package/dist/trust.js +168 -0
  103. package/dist/types/app.d.ts +30 -0
  104. package/dist/types/app.js +2 -0
  105. package/dist/types/config.d.ts +25 -0
  106. package/dist/types/config.js +2 -0
  107. package/dist/types/core.d.ts +100 -0
  108. package/dist/types/core.js +2 -0
  109. package/dist/types/diff.d.ts +55 -0
  110. package/dist/types/diff.js +2 -0
  111. package/dist/types/plugin.d.ts +41 -0
  112. package/dist/types/plugin.js +2 -0
  113. package/dist/types/trust.d.ts +120 -0
  114. package/dist/types/trust.js +2 -0
  115. package/dist/types.d.ts +8 -211
  116. package/docs/PRD.md +187 -109
  117. package/docs/plugin-contract.md +61 -0
  118. package/docs/release-notes-draft.md +40 -0
  119. package/docs/rules-catalog.md +49 -0
  120. package/docs/trust-core-release-checklist.md +87 -0
  121. package/package.json +6 -3
  122. package/packages/vscode-drift/src/code-actions.ts +1 -1
  123. package/schemas/drift-ai-output.v1.json +162 -0
  124. package/schemas/drift-report.v1.json +151 -0
  125. package/schemas/drift-trust.v1.json +131 -0
  126. package/scripts/smoke-repo.mjs +394 -0
  127. package/src/analyzer.ts +484 -155
  128. package/src/benchmark.ts +266 -0
  129. package/src/cli.ts +840 -85
  130. package/src/config.ts +19 -2
  131. package/src/diff.ts +84 -10
  132. package/src/doctor.ts +173 -0
  133. package/src/format.ts +81 -0
  134. package/src/git.ts +16 -0
  135. package/src/guard-types.ts +64 -0
  136. package/src/guard.ts +324 -0
  137. package/src/index.ts +83 -0
  138. package/src/init.ts +298 -0
  139. package/src/map-cycles.ts +38 -0
  140. package/src/map-svg.ts +124 -0
  141. package/src/map.ts +111 -142
  142. package/src/metrics.ts +78 -59
  143. package/src/output-metadata.ts +30 -0
  144. package/src/plugins-capabilities.ts +36 -0
  145. package/src/plugins-messages.ts +35 -0
  146. package/src/plugins-rules.ts +296 -0
  147. package/src/plugins.ts +148 -27
  148. package/src/printer.ts +4 -0
  149. package/src/reporter-constants.ts +46 -0
  150. package/src/reporter.ts +64 -65
  151. package/src/review.ts +6 -4
  152. package/src/rules/comments.ts +2 -2
  153. package/src/rules/complexity.ts +2 -7
  154. package/src/rules/nesting.ts +3 -13
  155. package/src/rules/phase0-basic.ts +11 -12
  156. package/src/rules/phase3-configurable.ts +39 -26
  157. package/src/rules/shared.ts +31 -3
  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 -433
  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 +210 -0
  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 +260 -0
  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 +78 -238
  185. package/tests/cli-sarif.test.ts +92 -0
  186. package/tests/diff.test.ts +124 -0
  187. package/tests/format.test.ts +157 -0
  188. package/tests/new-features.test.ts +80 -1
  189. package/tests/phase1-init-doctor-guard.test.ts +199 -0
  190. package/tests/plugins.test.ts +219 -0
  191. package/tests/rules.test.ts +23 -1
  192. package/tests/saas-foundation.test.ts +358 -1
  193. package/tests/sarif.test.ts +160 -0
  194. package/tests/trust-kpi.test.ts +147 -0
  195. package/tests/trust.test.ts +602 -0
package/src/saas.ts CHANGED
@@ -1,433 +1,49 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
- import { dirname, resolve } from 'node:path'
3
- import type { DriftReport, DriftConfig } from './types.js'
4
-
5
- export interface SaasPolicy {
6
- freeUserThreshold: number
7
- maxRunsPerWorkspacePerMonth: number
8
- maxReposPerWorkspace: number
9
- retentionDays: number
10
- }
11
-
12
- export interface SaasUser {
13
- id: string
14
- createdAt: string
15
- lastSeenAt: string
16
- }
17
-
18
- export interface SaasWorkspace {
19
- id: string
20
- createdAt: string
21
- lastSeenAt: string
22
- userIds: string[]
23
- repoIds: string[]
24
- }
25
-
26
- export interface SaasRepo {
27
- id: string
28
- workspaceId: string
29
- name: string
30
- createdAt: string
31
- lastSeenAt: string
32
- }
33
-
34
- export interface SaasSnapshot {
35
- id: string
36
- createdAt: string
37
- scannedAt: string
38
- workspaceId: string
39
- userId: string
40
- repoId: string
41
- repoName: string
42
- targetPath: string
43
- totalScore: number
44
- totalIssues: number
45
- totalFiles: number
46
- summary: {
47
- errors: number
48
- warnings: number
49
- infos: number
50
- }
51
- }
52
-
53
- export interface SaasStore {
54
- version: number
55
- policy: SaasPolicy
56
- users: Record<string, SaasUser>
57
- workspaces: Record<string, SaasWorkspace>
58
- repos: Record<string, SaasRepo>
59
- snapshots: SaasSnapshot[]
60
- }
61
-
62
- export interface SaasSummary {
63
- policy: SaasPolicy
64
- usersRegistered: number
65
- workspacesActive: number
66
- reposActive: number
67
- runsPerMonth: Record<string, number>
68
- totalSnapshots: number
69
- phase: 'free' | 'paid'
70
- thresholdReached: boolean
71
- freeUsersRemaining: number
72
- }
73
-
74
- export interface IngestOptions {
75
- workspaceId: string
76
- userId: string
77
- repoName?: string
78
- storeFile?: string
79
- policy?: Partial<SaasPolicy>
80
- }
81
-
82
- const STORE_VERSION = 1
83
- const ACTIVE_WINDOW_DAYS = 30
84
-
85
- export const DEFAULT_SAAS_POLICY: SaasPolicy = {
86
- freeUserThreshold: 7500,
87
- maxRunsPerWorkspacePerMonth: 500,
88
- maxReposPerWorkspace: 20,
89
- retentionDays: 90,
90
- }
91
-
92
- export function resolveSaasPolicy(policy?: Partial<SaasPolicy> | DriftConfig['saas']): SaasPolicy {
93
- return {
94
- ...DEFAULT_SAAS_POLICY,
95
- ...(policy ?? {}),
96
- }
97
- }
98
-
99
- export function defaultSaasStorePath(root = '.'): string {
100
- return resolve(root, '.drift-cloud', 'store.json')
101
- }
102
-
103
- function ensureStoreFile(storeFile: string, policy?: Partial<SaasPolicy>): void {
104
- const dir = dirname(storeFile)
105
- if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
106
- if (!existsSync(storeFile)) {
107
- const initial = createEmptyStore(policy)
108
- writeFileSync(storeFile, JSON.stringify(initial, null, 2), 'utf8')
109
- }
110
- }
111
-
112
- function createEmptyStore(policy?: Partial<SaasPolicy>): SaasStore {
113
- return {
114
- version: STORE_VERSION,
115
- policy: resolveSaasPolicy(policy),
116
- users: {},
117
- workspaces: {},
118
- repos: {},
119
- snapshots: [],
120
- }
121
- }
122
-
123
- function monthKey(isoDate: string): string {
124
- const date = new Date(isoDate)
125
- const month = String(date.getUTCMonth() + 1).padStart(2, '0')
126
- return `${date.getUTCFullYear()}-${month}`
127
- }
128
-
129
- function daysAgo(days: number): number {
130
- const now = Date.now()
131
- return now - days * 24 * 60 * 60 * 1000
132
- }
133
-
134
- function applyRetention(store: SaasStore): void {
135
- const cutoff = daysAgo(store.policy.retentionDays)
136
- store.snapshots = store.snapshots.filter((snapshot) => {
137
- return new Date(snapshot.createdAt).getTime() >= cutoff
138
- })
139
- }
140
-
141
- function saveStore(storeFile: string, store: SaasStore): void {
142
- writeFileSync(storeFile, JSON.stringify(store, null, 2), 'utf8')
143
- }
144
-
145
- function loadStoreInternal(storeFile: string, policy?: Partial<SaasPolicy>): SaasStore {
146
- ensureStoreFile(storeFile, policy)
147
- const raw = readFileSync(storeFile, 'utf8')
148
- const parsed = JSON.parse(raw) as Partial<SaasStore>
149
-
150
- const merged = createEmptyStore(parsed.policy)
151
- merged.version = parsed.version ?? STORE_VERSION
152
- merged.users = parsed.users ?? {}
153
- merged.workspaces = parsed.workspaces ?? {}
154
- merged.repos = parsed.repos ?? {}
155
- merged.snapshots = parsed.snapshots ?? []
156
- merged.policy = resolveSaasPolicy({ ...merged.policy, ...policy })
157
- applyRetention(merged)
158
-
159
- return merged
160
- }
161
-
162
- function isWorkspaceActive(workspace: SaasWorkspace): boolean {
163
- return new Date(workspace.lastSeenAt).getTime() >= daysAgo(ACTIVE_WINDOW_DAYS)
164
- }
165
-
166
- function isRepoActive(repo: SaasRepo): boolean {
167
- return new Date(repo.lastSeenAt).getTime() >= daysAgo(ACTIVE_WINDOW_DAYS)
168
- }
169
-
170
- function assertGuardrails(store: SaasStore, options: IngestOptions, nowIso: string): void {
171
- const usersRegistered = Object.keys(store.users).length
172
- const isFreePhase = usersRegistered < store.policy.freeUserThreshold
173
- if (!isFreePhase) return
174
-
175
- if (!store.users[options.userId] && usersRegistered + 1 > store.policy.freeUserThreshold) {
176
- throw new Error(`Free threshold reached (${store.policy.freeUserThreshold} users).`)
177
- }
178
-
179
- const workspace = store.workspaces[options.workspaceId]
180
- const repoName = options.repoName ?? 'default'
181
- const repoId = `${options.workspaceId}:${repoName}`
182
- const repoExists = Boolean(store.repos[repoId])
183
- const repoCount = workspace?.repoIds.length ?? 0
184
-
185
- if (!repoExists && repoCount >= store.policy.maxReposPerWorkspace) {
186
- throw new Error(`Workspace '${options.workspaceId}' reached max repos (${store.policy.maxReposPerWorkspace}).`)
187
- }
188
-
189
- const currentMonth = monthKey(nowIso)
190
- const runsThisMonth = store.snapshots.filter((snapshot) => {
191
- return snapshot.workspaceId === options.workspaceId && monthKey(snapshot.createdAt) === currentMonth
192
- }).length
193
-
194
- if (runsThisMonth >= store.policy.maxRunsPerWorkspacePerMonth) {
195
- throw new Error(`Workspace '${options.workspaceId}' reached max monthly runs (${store.policy.maxRunsPerWorkspacePerMonth}).`)
196
- }
197
- }
198
-
199
- export function ingestSnapshotFromReport(report: DriftReport, options: IngestOptions): SaasSnapshot {
200
- const storeFile = resolve(options.storeFile ?? defaultSaasStorePath())
201
- const store = loadStoreInternal(storeFile, options.policy)
202
- const nowIso = new Date().toISOString()
203
-
204
- assertGuardrails(store, options, nowIso)
205
-
206
- const user = store.users[options.userId]
207
- if (user) {
208
- user.lastSeenAt = nowIso
209
- } else {
210
- store.users[options.userId] = {
211
- id: options.userId,
212
- createdAt: nowIso,
213
- lastSeenAt: nowIso,
214
- }
215
- }
216
-
217
- const workspace = store.workspaces[options.workspaceId]
218
- if (workspace) {
219
- workspace.lastSeenAt = nowIso
220
- if (!workspace.userIds.includes(options.userId)) workspace.userIds.push(options.userId)
221
- } else {
222
- store.workspaces[options.workspaceId] = {
223
- id: options.workspaceId,
224
- createdAt: nowIso,
225
- lastSeenAt: nowIso,
226
- userIds: [options.userId],
227
- repoIds: [],
228
- }
229
- }
230
-
231
- const repoName = options.repoName ?? 'default'
232
- const repoId = `${options.workspaceId}:${repoName}`
233
- const repo = store.repos[repoId]
234
- if (repo) {
235
- repo.lastSeenAt = nowIso
236
- } else {
237
- store.repos[repoId] = {
238
- id: repoId,
239
- workspaceId: options.workspaceId,
240
- name: repoName,
241
- createdAt: nowIso,
242
- lastSeenAt: nowIso,
243
- }
244
- const ws = store.workspaces[options.workspaceId]
245
- if (!ws.repoIds.includes(repoId)) ws.repoIds.push(repoId)
246
- }
247
-
248
- const snapshot: SaasSnapshot = {
249
- id: `${Date.now()}-${Math.random().toString(16).slice(2, 10)}`,
250
- createdAt: nowIso,
251
- scannedAt: report.scannedAt,
252
- workspaceId: options.workspaceId,
253
- userId: options.userId,
254
- repoId,
255
- repoName,
256
- targetPath: report.targetPath,
257
- totalScore: report.totalScore,
258
- totalIssues: report.totalIssues,
259
- totalFiles: report.totalFiles,
260
- summary: {
261
- errors: report.summary.errors,
262
- warnings: report.summary.warnings,
263
- infos: report.summary.infos,
264
- },
265
- }
266
-
267
- store.snapshots.push(snapshot)
268
- applyRetention(store)
269
- saveStore(storeFile, store)
270
-
271
- return snapshot
272
- }
273
-
274
- export function getSaasSummary(options?: { storeFile?: string; policy?: Partial<SaasPolicy> }): SaasSummary {
275
- const storeFile = resolve(options?.storeFile ?? defaultSaasStorePath())
276
- const store = loadStoreInternal(storeFile, options?.policy)
277
- saveStore(storeFile, store)
278
-
279
- const usersRegistered = Object.keys(store.users).length
280
- const workspacesActive = Object.values(store.workspaces).filter(isWorkspaceActive).length
281
- const reposActive = Object.values(store.repos).filter(isRepoActive).length
282
-
283
- const runsPerMonth: Record<string, number> = {}
284
- for (const snapshot of store.snapshots) {
285
- const key = monthKey(snapshot.createdAt)
286
- runsPerMonth[key] = (runsPerMonth[key] ?? 0) + 1
287
- }
288
-
289
- const thresholdReached = usersRegistered >= store.policy.freeUserThreshold
290
-
291
- return {
292
- policy: store.policy,
293
- usersRegistered,
294
- workspacesActive,
295
- reposActive,
296
- runsPerMonth,
297
- totalSnapshots: store.snapshots.length,
298
- phase: thresholdReached ? 'paid' : 'free',
299
- thresholdReached,
300
- freeUsersRemaining: Math.max(0, store.policy.freeUserThreshold - usersRegistered),
301
- }
302
- }
303
-
304
- function escapeHtml(value: string): string {
305
- return value
306
- .replaceAll('&', '&amp;')
307
- .replaceAll('<', '&lt;')
308
- .replaceAll('>', '&gt;')
309
- .replaceAll('"', '&quot;')
310
- .replaceAll("'", '&#39;')
311
- }
312
-
313
- export function generateSaasDashboardHtml(options?: { storeFile?: string; policy?: Partial<SaasPolicy> }): string {
314
- const storeFile = resolve(options?.storeFile ?? defaultSaasStorePath())
315
- const store = loadStoreInternal(storeFile, options?.policy)
316
- const summary = getSaasSummary(options)
317
-
318
- const workspaceStats = Object.values(store.workspaces)
319
- .map((workspace) => {
320
- const snapshots = store.snapshots.filter((snapshot) => snapshot.workspaceId === workspace.id)
321
- const runs = snapshots.length
322
- const avgScore = runs === 0
323
- ? 0
324
- : Math.round(snapshots.reduce((sum, snapshot) => sum + snapshot.totalScore, 0) / runs)
325
- const lastRun = snapshots.sort((a, b) => b.createdAt.localeCompare(a.createdAt))[0]?.createdAt ?? 'n/a'
326
- return {
327
- id: workspace.id,
328
- runs,
329
- avgScore,
330
- lastRun,
331
- }
332
- })
333
- .sort((a, b) => b.avgScore - a.avgScore)
334
-
335
- const repoStats = Object.values(store.repos)
336
- .map((repo) => {
337
- const snapshots = store.snapshots.filter((snapshot) => snapshot.repoId === repo.id)
338
- const runs = snapshots.length
339
- const avgScore = runs === 0
340
- ? 0
341
- : Math.round(snapshots.reduce((sum, snapshot) => sum + snapshot.totalScore, 0) / runs)
342
- return {
343
- workspaceId: repo.workspaceId,
344
- name: repo.name,
345
- runs,
346
- avgScore,
347
- }
348
- })
349
- .sort((a, b) => b.avgScore - a.avgScore)
350
- .slice(0, 15)
351
-
352
- const runsRows = Object.entries(summary.runsPerMonth)
353
- .sort(([a], [b]) => a.localeCompare(b))
354
- .map(([month, count]) => {
355
- const width = Math.max(8, count * 8)
356
- return `<tr><td>${escapeHtml(month)}</td><td>${count}</td><td><div class="bar" style="width:${width}px"></div></td></tr>`
357
- })
358
- .join('')
359
-
360
- const workspaceRows = workspaceStats
361
- .map((workspace) => `<tr><td>${escapeHtml(workspace.id)}</td><td>${workspace.runs}</td><td>${workspace.avgScore}</td><td>${escapeHtml(workspace.lastRun)}</td></tr>`)
362
- .join('')
363
-
364
- const repoRows = repoStats
365
- .map((repo) => `<tr><td>${escapeHtml(repo.workspaceId)}</td><td>${escapeHtml(repo.name)}</td><td>${repo.runs}</td><td>${repo.avgScore}</td></tr>`)
366
- .join('')
367
-
368
- return `<!doctype html>
369
- <html lang="en">
370
- <head>
371
- <meta charset="utf-8" />
372
- <meta name="viewport" content="width=device-width, initial-scale=1" />
373
- <title>drift cloud dashboard</title>
374
- <style>
375
- :root { color-scheme: light; }
376
- body { margin: 0; font-family: "Segoe UI", Arial, sans-serif; background: #f4f7fb; color: #0f172a; }
377
- main { max-width: 980px; margin: 0 auto; padding: 24px; }
378
- h1 { margin: 0 0 6px; }
379
- p.meta { margin: 0 0 20px; color: #475569; }
380
- .cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; margin-bottom: 18px; }
381
- .card { background: #ffffff; border-radius: 10px; padding: 14px; border: 1px solid #dbe3ef; }
382
- .card .label { font-size: 12px; color: #64748b; text-transform: uppercase; letter-spacing: 0.08em; }
383
- .card .value { font-size: 26px; font-weight: 700; margin-top: 4px; }
384
- table { width: 100%; border-collapse: collapse; margin-top: 10px; background: #ffffff; border: 1px solid #dbe3ef; border-radius: 10px; overflow: hidden; }
385
- th, td { padding: 10px; border-bottom: 1px solid #e2e8f0; text-align: left; font-size: 14px; }
386
- th { background: #eef2f9; }
387
- .section { margin-top: 18px; }
388
- .bar { height: 10px; background: linear-gradient(90deg, #0ea5e9, #22c55e); border-radius: 999px; }
389
- .pill { display: inline-block; border-radius: 999px; padding: 4px 10px; font-size: 12px; font-weight: 600; }
390
- .pill.free { background: #dcfce7; color: #166534; }
391
- .pill.paid { background: #fee2e2; color: #991b1b; }
392
- </style>
393
- </head>
394
- <body>
395
- <main>
396
- <h1>drift cloud dashboard</h1>
397
- <p class="meta">Store: ${escapeHtml(storeFile)}</p>
398
- <div class="cards">
399
- <div class="card"><div class="label">Plan Phase</div><div class="value"><span class="pill ${summary.phase}">${summary.phase.toUpperCase()}</span></div></div>
400
- <div class="card"><div class="label">Users</div><div class="value">${summary.usersRegistered}</div></div>
401
- <div class="card"><div class="label">Active Workspaces</div><div class="value">${summary.workspacesActive}</div></div>
402
- <div class="card"><div class="label">Active Repos</div><div class="value">${summary.reposActive}</div></div>
403
- <div class="card"><div class="label">Snapshots</div><div class="value">${summary.totalSnapshots}</div></div>
404
- <div class="card"><div class="label">Free Seats Left</div><div class="value">${summary.freeUsersRemaining}</div></div>
405
- </div>
406
-
407
- <section class="section">
408
- <h2>Runs Per Month</h2>
409
- <table>
410
- <thead><tr><th>Month</th><th>Runs</th><th>Trend</th></tr></thead>
411
- <tbody>${runsRows || '<tr><td colspan="3">No runs yet</td></tr>'}</tbody>
412
- </table>
413
- </section>
414
-
415
- <section class="section">
416
- <h2>Workspace Hotspots</h2>
417
- <table>
418
- <thead><tr><th>Workspace</th><th>Runs</th><th>Avg Score</th><th>Last Run</th></tr></thead>
419
- <tbody>${workspaceRows || '<tr><td colspan="4">No workspace data</td></tr>'}</tbody>
420
- </table>
421
- </section>
422
-
423
- <section class="section">
424
- <h2>Repo Hotspots</h2>
425
- <table>
426
- <thead><tr><th>Workspace</th><th>Repo</th><th>Runs</th><th>Avg Score</th></tr></thead>
427
- <tbody>${repoRows || '<tr><td colspan="4">No repo data</td></tr>'}</tbody>
428
- </table>
429
- </section>
430
- </main>
431
- </body>
432
- </html>`
433
- }
1
+ export { DEFAULT_SAAS_POLICY } from './saas/constants.js'
2
+ export { SaasActorRequiredError, SaasPermissionError } from './saas/errors.js'
3
+ export {
4
+ resolveSaasPolicy,
5
+ } from './saas/helpers.js'
6
+ export {
7
+ defaultSaasStorePath,
8
+ getRequiredRoleForOperation,
9
+ assertSaasPermission,
10
+ getSaasEffectiveLimits,
11
+ getOrganizationEffectiveLimits,
12
+ } from './saas/store.js'
13
+ export { ingestSnapshotFromReport } from './saas/ingest.js'
14
+ export {
15
+ changeOrganizationPlan,
16
+ listOrganizationPlanChanges,
17
+ getOrganizationUsageSnapshot,
18
+ } from './saas/organization.js'
19
+ export {
20
+ listSaasSnapshots,
21
+ getSaasSummary,
22
+ generateSaasDashboardHtml,
23
+ } from './saas/dashboard.js'
24
+
25
+ export type {
26
+ SaasUser,
27
+ SaasOrganization,
28
+ SaasWorkspace,
29
+ SaasRepo,
30
+ SaasMembership,
31
+ SaasRole,
32
+ SaasPlan,
33
+ SaasPolicy,
34
+ SaasPolicyOverrides,
35
+ SaasStore,
36
+ SaasSummary,
37
+ SaasSnapshot,
38
+ SaasQueryOptions,
39
+ IngestOptions,
40
+ SaasPlanChange,
41
+ SaasOperation,
42
+ SaasPermissionContext,
43
+ SaasPermissionResult,
44
+ SaasEffectiveLimits,
45
+ SaasOrganizationUsageSnapshot,
46
+ ChangeOrganizationPlanOptions,
47
+ SaasUsageQueryOptions,
48
+ SaasPlanChangeQueryOptions,
49
+ } from './saas/types.js'