@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
@@ -0,0 +1,99 @@
1
+ import { resolve } from 'node:path'
2
+ import type {
3
+ ChangeOrganizationPlanOptions,
4
+ SaasOrganizationUsageSnapshot,
5
+ SaasPlanChange,
6
+ SaasPlanChangeQueryOptions,
7
+ SaasUsageQueryOptions,
8
+ } from './types.js'
9
+ import { assertPermissionInStore, defaultSaasStorePath, loadStoreInternal, saveStore } from './store.js'
10
+ import { monthKey, normalizePlan, workspaceKey } from './helpers.js'
11
+ import { appendPlanChange } from './plan-change.js'
12
+
13
+ export function changeOrganizationPlan(options: ChangeOrganizationPlanOptions): SaasPlanChange {
14
+ const storeFile = resolve(options.storeFile ?? defaultSaasStorePath())
15
+ const store = loadStoreInternal(storeFile, options.policy)
16
+ const nowIso = new Date().toISOString()
17
+
18
+ const organization = store.organizations[options.organizationId]
19
+ if (!organization) throw new Error(`Organization '${options.organizationId}' does not exist.`)
20
+
21
+ assertPermissionInStore(store, {
22
+ operation: 'billing:write',
23
+ organizationId: options.organizationId,
24
+ actorUserId: options.actorUserId,
25
+ })
26
+
27
+ const nextPlan = normalizePlan(options.newPlan)
28
+ if (organization.plan === nextPlan) {
29
+ const unchanged = appendPlanChange(store, {
30
+ organizationId: organization.id,
31
+ fromPlan: organization.plan,
32
+ toPlan: nextPlan,
33
+ changedAt: nowIso,
34
+ changedByUserId: options.actorUserId,
35
+ reason: options.reason,
36
+ })
37
+ saveStore(storeFile, store)
38
+ return unchanged
39
+ }
40
+
41
+ const previousPlan = organization.plan
42
+ organization.plan = nextPlan
43
+ organization.lastSeenAt = nowIso
44
+ const change = appendPlanChange(store, {
45
+ organizationId: organization.id,
46
+ fromPlan: previousPlan,
47
+ toPlan: nextPlan,
48
+ changedAt: nowIso,
49
+ changedByUserId: options.actorUserId,
50
+ reason: options.reason,
51
+ })
52
+ saveStore(storeFile, store)
53
+ return change
54
+ }
55
+
56
+ export function listOrganizationPlanChanges(options: SaasPlanChangeQueryOptions): SaasPlanChange[] {
57
+ const storeFile = resolve(options.storeFile ?? defaultSaasStorePath())
58
+ const store = loadStoreInternal(storeFile, options.policy)
59
+
60
+ assertPermissionInStore(store, {
61
+ operation: 'billing:read',
62
+ organizationId: options.organizationId,
63
+ actorUserId: options.actorUserId,
64
+ })
65
+
66
+ return store.planChanges
67
+ .filter((change) => change.organizationId === options.organizationId)
68
+ .sort((a, b) => b.changedAt.localeCompare(a.changedAt))
69
+ }
70
+
71
+ export function getOrganizationUsageSnapshot(options: SaasUsageQueryOptions): SaasOrganizationUsageSnapshot {
72
+ const storeFile = resolve(options.storeFile ?? defaultSaasStorePath())
73
+ const store = loadStoreInternal(storeFile, options.policy)
74
+
75
+ assertPermissionInStore(store, {
76
+ operation: 'billing:read',
77
+ organizationId: options.organizationId,
78
+ actorUserId: options.actorUserId,
79
+ })
80
+
81
+ const organization = store.organizations[options.organizationId]
82
+ if (!organization) throw new Error(`Organization '${options.organizationId}' does not exist.`)
83
+
84
+ const month = options.month ?? monthKey(new Date().toISOString())
85
+ const organizationRunSnapshots = store.snapshots.filter((snapshot) => snapshot.organizationId === options.organizationId)
86
+
87
+ return {
88
+ organizationId: options.organizationId,
89
+ plan: organization.plan,
90
+ capturedAt: new Date().toISOString(),
91
+ workspaceCount: organization.workspaceIds.length,
92
+ repoCount: organization.workspaceIds
93
+ .map((workspaceId) => store.workspaces[workspaceKey(options.organizationId, workspaceId)])
94
+ .filter((workspace) => Boolean(workspace))
95
+ .reduce((count, workspace) => count + workspace.repoIds.length, 0),
96
+ runCount: organizationRunSnapshots.length,
97
+ runCountThisMonth: organizationRunSnapshots.filter((snapshot) => monthKey(snapshot.createdAt) === month).length,
98
+ }
99
+ }
@@ -0,0 +1,19 @@
1
+ import type { SaasPlan, SaasPlanChange, SaasStore } from './types.js'
2
+ import { createRandomId } from './constants.js'
3
+
4
+ export function appendPlanChange(
5
+ store: SaasStore,
6
+ input: { organizationId: string; fromPlan: SaasPlan; toPlan: SaasPlan; changedByUserId: string; reason?: string; changedAt: string },
7
+ ): SaasPlanChange {
8
+ const change: SaasPlanChange = {
9
+ id: createRandomId(input.changedAt),
10
+ organizationId: input.organizationId,
11
+ fromPlan: input.fromPlan,
12
+ toPlan: input.toPlan,
13
+ changedAt: input.changedAt,
14
+ changedByUserId: input.changedByUserId,
15
+ reason: input.reason,
16
+ }
17
+ store.planChanges.push(change)
18
+ return change
19
+ }
@@ -0,0 +1,172 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { dirname, resolve } from 'node:path'
3
+ import type { SaasEffectiveLimits, SaasOperation, SaasPermissionContext, SaasPermissionResult, SaasPlan, SaasPolicyOverrides, SaasRole, SaasStore, SaasWorkspace } from './types.js'
4
+ import { DEFAULT_ORGANIZATION_ID, REQUIRED_ROLE_BY_OPERATION, ROLE_PRIORITY, STORE_VERSION } from './constants.js'
5
+ import { SaasActorRequiredError, SaasPermissionError } from './errors.js'
6
+ import { hasRoleAtLeast, membershipKey, mergePolicy, normalizePlan, resolveSaasPolicy } from './helpers.js'
7
+
8
+ const HOURS_PER_DAY = 24
9
+ const MINUTES_PER_HOUR = 60
10
+ const SECONDS_PER_MINUTE = 60
11
+ const MILLISECONDS_PER_SECOND = 1000
12
+
13
+ export function defaultSaasStorePath(root = '.'): string {
14
+ return resolve(root, '.drift-cloud', 'store.json')
15
+ }
16
+
17
+ function createEmptyStore(policy?: SaasPolicyOverrides): SaasStore {
18
+ return {
19
+ version: STORE_VERSION,
20
+ policy: resolveSaasPolicy(policy),
21
+ users: {},
22
+ organizations: {},
23
+ workspaces: {},
24
+ memberships: {},
25
+ repos: {},
26
+ snapshots: [],
27
+ planChanges: [],
28
+ }
29
+ }
30
+
31
+ function ensureStoreFile(storeFile: string, policy?: SaasPolicyOverrides): void {
32
+ const dir = dirname(storeFile)
33
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
34
+ if (!existsSync(storeFile)) {
35
+ const initial = createEmptyStore(policy)
36
+ writeFileSync(storeFile, JSON.stringify(initial, null, 2), 'utf8')
37
+ }
38
+ }
39
+
40
+ function applyStoreEntityDefaults(store: SaasStore): void {
41
+ for (const workspace of Object.values(store.workspaces)) {
42
+ if (!workspace.organizationId) workspace.organizationId = DEFAULT_ORGANIZATION_ID
43
+ }
44
+ for (const repo of Object.values(store.repos)) {
45
+ if (!repo.organizationId) repo.organizationId = DEFAULT_ORGANIZATION_ID
46
+ }
47
+ for (const snapshot of store.snapshots) {
48
+ if (!snapshot.organizationId) snapshot.organizationId = DEFAULT_ORGANIZATION_ID
49
+ if (!snapshot.plan) snapshot.plan = 'free'
50
+ if (!snapshot.role) snapshot.role = 'member'
51
+ }
52
+ }
53
+
54
+ function ensureOrganizationFromWorkspace(store: SaasStore, workspace: SaasWorkspace): void {
55
+ const orgId = workspace.organizationId
56
+ const existingOrg = store.organizations[orgId]
57
+
58
+ if (!existingOrg) {
59
+ store.organizations[orgId] = {
60
+ id: orgId,
61
+ plan: 'free',
62
+ createdAt: workspace.createdAt,
63
+ lastSeenAt: workspace.lastSeenAt,
64
+ workspaceIds: [workspace.id],
65
+ }
66
+ return
67
+ }
68
+
69
+ if (!existingOrg.workspaceIds.includes(workspace.id)) existingOrg.workspaceIds.push(workspace.id)
70
+ if (workspace.lastSeenAt > existingOrg.lastSeenAt) existingOrg.lastSeenAt = workspace.lastSeenAt
71
+ }
72
+
73
+ function hydrateOrganizationsFromWorkspaces(store: SaasStore): void {
74
+ for (const workspace of Object.values(store.workspaces)) {
75
+ ensureOrganizationFromWorkspace(store, workspace)
76
+ }
77
+ }
78
+
79
+ export function applyRetentionPolicy(store: SaasStore): void {
80
+ const millisecondsPerDay = HOURS_PER_DAY * MINUTES_PER_HOUR * SECONDS_PER_MINUTE * MILLISECONDS_PER_SECOND
81
+ const cutoff = Date.now() - store.policy.retentionDays * millisecondsPerDay
82
+ store.snapshots = store.snapshots.filter((snapshot) => new Date(snapshot.createdAt).getTime() >= cutoff)
83
+ }
84
+
85
+ export function saveStore(storeFile: string, store: SaasStore): void {
86
+ writeFileSync(storeFile, JSON.stringify(store, null, 2), 'utf8')
87
+ }
88
+
89
+ export function loadStoreInternal(storeFile: string, policy?: SaasPolicyOverrides): SaasStore {
90
+ ensureStoreFile(storeFile, policy)
91
+ const raw = readFileSync(storeFile, 'utf8')
92
+ const parsed = JSON.parse(raw) as Partial<SaasStore>
93
+
94
+ const merged = createEmptyStore(parsed.policy)
95
+ merged.version = parsed.version ?? STORE_VERSION
96
+ merged.users = parsed.users ?? {}
97
+ merged.organizations = parsed.organizations ?? {}
98
+ merged.workspaces = parsed.workspaces ?? {}
99
+ merged.memberships = parsed.memberships ?? {}
100
+ merged.repos = parsed.repos ?? {}
101
+ merged.snapshots = parsed.snapshots ?? []
102
+ merged.planChanges = parsed.planChanges ?? []
103
+ merged.policy = mergePolicy(policy, merged.policy)
104
+
105
+ applyStoreEntityDefaults(merged)
106
+ hydrateOrganizationsFromWorkspaces(merged)
107
+ applyRetentionPolicy(merged)
108
+
109
+ return merged
110
+ }
111
+
112
+ function resolveActorRole(store: SaasStore, organizationId: string, actorUserId: string, workspaceId?: string): SaasRole | undefined {
113
+ if (workspaceId) {
114
+ const scopedMembershipId = membershipKey(organizationId, workspaceId, actorUserId)
115
+ return store.memberships[scopedMembershipId]?.role
116
+ }
117
+
118
+ let highestRole: SaasRole | undefined
119
+ for (const membership of Object.values(store.memberships)) {
120
+ if (membership.organizationId !== organizationId) continue
121
+ if (membership.userId !== actorUserId) continue
122
+ if (!highestRole || ROLE_PRIORITY[membership.role] > ROLE_PRIORITY[highestRole]) highestRole = membership.role
123
+ if (highestRole === 'owner') break
124
+ }
125
+ return highestRole
126
+ }
127
+
128
+ export function assertPermissionInStore(store: SaasStore, context: SaasPermissionContext): SaasPermissionResult {
129
+ const requiredRole = REQUIRED_ROLE_BY_OPERATION[context.operation]
130
+ if (!context.actorUserId) {
131
+ if (store.policy.strictActorEnforcement) throw new SaasActorRequiredError(context)
132
+ return { requiredRole }
133
+ }
134
+
135
+ const actorRole = resolveActorRole(store, context.organizationId, context.actorUserId, context.workspaceId)
136
+ if (!hasRoleAtLeast(actorRole, requiredRole)) {
137
+ throw new SaasPermissionError(context, requiredRole, actorRole)
138
+ }
139
+
140
+ return { requiredRole, actorRole }
141
+ }
142
+
143
+ export function getRequiredRoleForOperation(operation: SaasOperation): SaasRole {
144
+ return REQUIRED_ROLE_BY_OPERATION[operation]
145
+ }
146
+
147
+ export function assertSaasPermission(
148
+ context: SaasPermissionContext & { storeFile?: string; policy?: SaasPolicyOverrides },
149
+ ): SaasPermissionResult {
150
+ const storeFile = resolve(context.storeFile ?? defaultSaasStorePath())
151
+ const store = loadStoreInternal(storeFile, context.policy)
152
+ return assertPermissionInStore(store, context)
153
+ }
154
+
155
+ export function getSaasEffectiveLimits(input: { plan: SaasPlan; policy?: SaasPolicyOverrides }): SaasEffectiveLimits {
156
+ const policy = resolveSaasPolicy(input.policy)
157
+ const plan = normalizePlan(input.plan)
158
+ return {
159
+ plan,
160
+ maxWorkspaces: policy.maxWorkspacesPerOrganizationByPlan[plan],
161
+ maxReposPerWorkspace: policy.maxReposPerWorkspace,
162
+ maxRunsPerWorkspacePerMonth: policy.maxRunsPerWorkspacePerMonth,
163
+ retentionDays: policy.retentionDays,
164
+ }
165
+ }
166
+
167
+ export function getOrganizationEffectiveLimits(options: { organizationId: string; storeFile?: string; policy?: SaasPolicyOverrides }): SaasEffectiveLimits {
168
+ const storeFile = resolve(options.storeFile ?? defaultSaasStorePath())
169
+ const store = loadStoreInternal(storeFile, options.policy)
170
+ const plan = normalizePlan(store.organizations[options.organizationId]?.plan)
171
+ return getSaasEffectiveLimits({ plan, policy: store.policy })
172
+ }
@@ -0,0 +1,216 @@
1
+ import type { DriftConfig, DriftReport } from '../types.js'
2
+
3
+ export interface SaasPolicy {
4
+ freeUserThreshold: number
5
+ maxRunsPerWorkspacePerMonth: number
6
+ maxReposPerWorkspace: number
7
+ retentionDays: number
8
+ strictActorEnforcement: boolean
9
+ maxWorkspacesPerOrganizationByPlan: Record<SaasPlan, number>
10
+ }
11
+
12
+ export type SaasRole = 'owner' | 'member' | 'viewer'
13
+ export type SaasPlan = 'free' | 'sponsor' | 'team' | 'business'
14
+
15
+ export interface SaasUser {
16
+ id: string
17
+ createdAt: string
18
+ lastSeenAt: string
19
+ }
20
+
21
+ export interface SaasOrganization {
22
+ id: string
23
+ plan: SaasPlan
24
+ createdAt: string
25
+ lastSeenAt: string
26
+ workspaceIds: string[]
27
+ }
28
+
29
+ export interface SaasWorkspace {
30
+ id: string
31
+ organizationId: string
32
+ createdAt: string
33
+ lastSeenAt: string
34
+ userIds: string[]
35
+ repoIds: string[]
36
+ }
37
+
38
+ export interface SaasRepo {
39
+ id: string
40
+ organizationId: string
41
+ workspaceId: string
42
+ name: string
43
+ createdAt: string
44
+ lastSeenAt: string
45
+ }
46
+
47
+ export interface SaasMembership {
48
+ id: string
49
+ organizationId: string
50
+ workspaceId: string
51
+ userId: string
52
+ role: SaasRole
53
+ createdAt: string
54
+ lastSeenAt: string
55
+ }
56
+
57
+ export interface SaasPlanChange {
58
+ id: string
59
+ organizationId: string
60
+ fromPlan: SaasPlan
61
+ toPlan: SaasPlan
62
+ changedAt: string
63
+ changedByUserId: string
64
+ reason?: string
65
+ }
66
+
67
+ export interface SaasSnapshot {
68
+ id: string
69
+ createdAt: string
70
+ scannedAt: string
71
+ organizationId: string
72
+ workspaceId: string
73
+ userId: string
74
+ role: SaasRole
75
+ plan: SaasPlan
76
+ repoId: string
77
+ repoName: string
78
+ targetPath: string
79
+ totalScore: number
80
+ totalIssues: number
81
+ totalFiles: number
82
+ summary: {
83
+ errors: number
84
+ warnings: number
85
+ infos: number
86
+ }
87
+ }
88
+
89
+ export interface SaasStore {
90
+ version: number
91
+ policy: SaasPolicy
92
+ users: Record<string, SaasUser>
93
+ organizations: Record<string, SaasOrganization>
94
+ workspaces: Record<string, SaasWorkspace>
95
+ memberships: Record<string, SaasMembership>
96
+ repos: Record<string, SaasRepo>
97
+ snapshots: SaasSnapshot[]
98
+ planChanges: SaasPlanChange[]
99
+ }
100
+
101
+ export type SaasOperation = 'snapshot:write' | 'snapshot:read' | 'summary:read' | 'billing:write' | 'billing:read'
102
+
103
+ export interface SaasPermissionContext {
104
+ operation: SaasOperation
105
+ organizationId: string
106
+ workspaceId?: string
107
+ actorUserId?: string
108
+ }
109
+
110
+ export interface SaasPermissionResult {
111
+ actorRole?: SaasRole
112
+ requiredRole: SaasRole
113
+ }
114
+
115
+ export interface SaasEffectiveLimits {
116
+ plan: SaasPlan
117
+ maxWorkspaces: number
118
+ maxReposPerWorkspace: number
119
+ maxRunsPerWorkspacePerMonth: number
120
+ retentionDays: number
121
+ }
122
+
123
+ export interface SaasOrganizationUsageSnapshot {
124
+ organizationId: string
125
+ plan: SaasPlan
126
+ capturedAt: string
127
+ workspaceCount: number
128
+ repoCount: number
129
+ runCount: number
130
+ runCountThisMonth: number
131
+ }
132
+
133
+ export interface ChangeOrganizationPlanOptions {
134
+ organizationId: string
135
+ actorUserId: string
136
+ newPlan: SaasPlan
137
+ reason?: string
138
+ storeFile?: string
139
+ policy?: SaasPolicyOverrides
140
+ }
141
+
142
+ export interface SaasUsageQueryOptions {
143
+ organizationId: string
144
+ month?: string
145
+ storeFile?: string
146
+ policy?: SaasPolicyOverrides
147
+ actorUserId?: string
148
+ }
149
+
150
+ export interface SaasPlanChangeQueryOptions {
151
+ organizationId: string
152
+ storeFile?: string
153
+ policy?: SaasPolicyOverrides
154
+ actorUserId?: string
155
+ }
156
+
157
+ export interface SaasSummary {
158
+ policy: SaasPolicy
159
+ usersRegistered: number
160
+ workspacesActive: number
161
+ reposActive: number
162
+ runsPerMonth: Record<string, number>
163
+ totalSnapshots: number
164
+ phase: 'free' | 'paid'
165
+ thresholdReached: boolean
166
+ freeUsersRemaining: number
167
+ }
168
+
169
+ export interface SaasPolicyOverrides {
170
+ freeUserThreshold?: number
171
+ maxRunsPerWorkspacePerMonth?: number
172
+ maxReposPerWorkspace?: number
173
+ retentionDays?: number
174
+ strictActorEnforcement?: boolean
175
+ maxWorkspacesPerOrganizationByPlan?: Partial<Record<SaasPlan, number>>
176
+ }
177
+
178
+ export interface SaasQueryOptions {
179
+ storeFile?: string
180
+ policy?: SaasPolicyOverrides
181
+ organizationId?: string
182
+ workspaceId?: string
183
+ actorUserId?: string
184
+ }
185
+
186
+ export interface IngestOptions {
187
+ organizationId?: string
188
+ workspaceId: string
189
+ userId: string
190
+ role?: SaasRole
191
+ plan?: SaasPlan
192
+ repoName?: string
193
+ actorUserId?: string
194
+ storeFile?: string
195
+ policy?: SaasPolicyOverrides
196
+ }
197
+
198
+ export interface ScopedIdentity {
199
+ organizationId: string
200
+ workspaceId: string
201
+ workspaceKey: string
202
+ repoName: string
203
+ repoId: string
204
+ }
205
+
206
+ export interface IngestMutationContext {
207
+ store: SaasStore
208
+ scoped: ScopedIdentity
209
+ options: IngestOptions
210
+ nowIso: string
211
+ requestedPlan: SaasPlan
212
+ }
213
+
214
+ export type SaasPolicyInput = SaasPolicyOverrides | DriftConfig['saas'] | undefined
215
+
216
+ export type DriftReportInput = DriftReport