@eduardbar/drift 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish-vscode.yml +3 -3
- package/.github/workflows/publish.yml +3 -3
- package/.github/workflows/review-pr.yml +153 -0
- package/AGENTS.md +6 -0
- package/README.md +192 -4
- package/ROADMAP.md +6 -5
- package/dist/analyzer.d.ts +2 -2
- package/dist/analyzer.js +420 -159
- package/dist/benchmark.d.ts +2 -0
- package/dist/benchmark.js +185 -0
- package/dist/cli.js +509 -23
- package/dist/diff.js +74 -10
- package/dist/git.js +12 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -0
- package/dist/map.d.ts +3 -2
- package/dist/map.js +98 -10
- package/dist/plugins.d.ts +2 -1
- package/dist/plugins.js +177 -28
- package/dist/printer.js +4 -0
- package/dist/review.js +2 -2
- package/dist/rules/comments.js +2 -2
- package/dist/rules/complexity.js +2 -7
- package/dist/rules/nesting.js +3 -13
- package/dist/rules/phase0-basic.js +10 -10
- package/dist/rules/shared.d.ts +2 -0
- package/dist/rules/shared.js +27 -3
- package/dist/saas.d.ts +219 -0
- package/dist/saas.js +762 -0
- package/dist/trust-kpi.d.ts +9 -0
- package/dist/trust-kpi.js +445 -0
- package/dist/trust.d.ts +65 -0
- package/dist/trust.js +571 -0
- package/dist/types.d.ts +160 -0
- package/docs/PRD.md +199 -172
- package/docs/plugin-contract.md +61 -0
- package/docs/trust-core-release-checklist.md +55 -0
- package/package.json +5 -3
- package/packages/vscode-drift/src/code-actions.ts +53 -0
- package/packages/vscode-drift/src/extension.ts +11 -0
- package/src/analyzer.ts +484 -155
- package/src/benchmark.ts +244 -0
- package/src/cli.ts +628 -36
- package/src/diff.ts +75 -10
- package/src/git.ts +16 -0
- package/src/index.ts +63 -0
- package/src/map.ts +112 -10
- package/src/plugins.ts +354 -26
- package/src/printer.ts +4 -0
- package/src/review.ts +2 -2
- package/src/rules/comments.ts +2 -2
- package/src/rules/complexity.ts +2 -7
- package/src/rules/nesting.ts +3 -13
- package/src/rules/phase0-basic.ts +11 -12
- package/src/rules/shared.ts +31 -3
- package/src/saas.ts +1031 -0
- package/src/trust-kpi.ts +518 -0
- package/src/trust.ts +774 -0
- package/src/types.ts +177 -0
- package/tests/diff.test.ts +124 -0
- package/tests/new-features.test.ts +98 -0
- package/tests/plugins.test.ts +219 -0
- package/tests/rules.test.ts +23 -1
- package/tests/saas-foundation.test.ts +464 -0
- package/tests/trust-kpi.test.ts +120 -0
- package/tests/trust.test.ts +584 -0
package/dist/rules/nesting.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SyntaxKind } from 'ts-morph';
|
|
2
|
-
import { hasIgnoreComment, getSnippet } from './shared.js';
|
|
2
|
+
import { hasIgnoreComment, getSnippet, collectFunctionLikes } from './shared.js';
|
|
3
3
|
const NESTING_THRESHOLD = 3;
|
|
4
4
|
const PARAMS_THRESHOLD = 4;
|
|
5
5
|
const NESTING_KINDS = new Set([
|
|
@@ -29,12 +29,7 @@ function getMaxNestingDepth(fn) {
|
|
|
29
29
|
}
|
|
30
30
|
export function detectDeepNesting(file) {
|
|
31
31
|
const issues = [];
|
|
32
|
-
const fns =
|
|
33
|
-
...file.getFunctions(),
|
|
34
|
-
...file.getDescendantsOfKind(SyntaxKind.ArrowFunction),
|
|
35
|
-
...file.getDescendantsOfKind(SyntaxKind.FunctionExpression),
|
|
36
|
-
...file.getClasses().flatMap((c) => c.getMethods()),
|
|
37
|
-
];
|
|
32
|
+
const fns = collectFunctionLikes(file);
|
|
38
33
|
for (const fn of fns) {
|
|
39
34
|
const depth = getMaxNestingDepth(fn);
|
|
40
35
|
if (depth > NESTING_THRESHOLD) {
|
|
@@ -55,12 +50,7 @@ export function detectDeepNesting(file) {
|
|
|
55
50
|
}
|
|
56
51
|
export function detectTooManyParams(file) {
|
|
57
52
|
const issues = [];
|
|
58
|
-
const fns =
|
|
59
|
-
...file.getFunctions(),
|
|
60
|
-
...file.getDescendantsOfKind(SyntaxKind.ArrowFunction),
|
|
61
|
-
...file.getDescendantsOfKind(SyntaxKind.FunctionExpression),
|
|
62
|
-
...file.getClasses().flatMap((c) => c.getMethods()),
|
|
63
|
-
];
|
|
53
|
+
const fns = collectFunctionLikes(file);
|
|
64
54
|
for (const fn of fns) {
|
|
65
55
|
const paramCount = fn.getParameters().length;
|
|
66
56
|
if (paramCount > PARAMS_THRESHOLD) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { SyntaxKind } from 'ts-morph';
|
|
2
|
-
import { hasIgnoreComment, getSnippet, getFunctionLikeLines } from './shared.js';
|
|
2
|
+
import { hasIgnoreComment, getSnippet, getFunctionLikeLines, collectFunctionLikes, getFileLines } from './shared.js';
|
|
3
3
|
const LARGE_FILE_THRESHOLD = 300;
|
|
4
4
|
const LARGE_FUNCTION_THRESHOLD = 50;
|
|
5
5
|
const SNIPPET_TRUNCATE_SHORT = 60;
|
|
@@ -22,12 +22,7 @@ export function detectLargeFile(file) {
|
|
|
22
22
|
}
|
|
23
23
|
export function detectLargeFunctions(file) {
|
|
24
24
|
const issues = [];
|
|
25
|
-
const fns =
|
|
26
|
-
...file.getFunctions(),
|
|
27
|
-
...file.getDescendantsOfKind(SyntaxKind.ArrowFunction),
|
|
28
|
-
...file.getDescendantsOfKind(SyntaxKind.FunctionExpression),
|
|
29
|
-
...file.getClasses().flatMap((c) => c.getMethods()),
|
|
30
|
-
];
|
|
25
|
+
const fns = collectFunctionLikes(file);
|
|
31
26
|
for (const fn of fns) {
|
|
32
27
|
const lines = getFunctionLikeLines(fn);
|
|
33
28
|
const startLine = fn.getStartLineNumber();
|
|
@@ -64,7 +59,7 @@ export function detectDebugLeftovers(file) {
|
|
|
64
59
|
});
|
|
65
60
|
}
|
|
66
61
|
}
|
|
67
|
-
const lines = file
|
|
62
|
+
const lines = getFileLines(file);
|
|
68
63
|
lines.forEach((lineContent, i) => {
|
|
69
64
|
if (/\/\/\s*(TODO|FIXME|HACK|XXX|TEMP)\b/i.test(lineContent)) {
|
|
70
65
|
if (hasIgnoreComment(file, i + 1))
|
|
@@ -83,11 +78,16 @@ export function detectDebugLeftovers(file) {
|
|
|
83
78
|
}
|
|
84
79
|
export function detectDeadCode(file) {
|
|
85
80
|
const issues = [];
|
|
81
|
+
const identifierCounts = new Map();
|
|
82
|
+
for (const id of file.getDescendantsOfKind(SyntaxKind.Identifier)) {
|
|
83
|
+
const text = id.getText();
|
|
84
|
+
identifierCounts.set(text, (identifierCounts.get(text) ?? 0) + 1);
|
|
85
|
+
}
|
|
86
86
|
for (const imp of file.getImportDeclarations()) {
|
|
87
87
|
for (const named of imp.getNamedImports()) {
|
|
88
88
|
const name = named.getName();
|
|
89
|
-
const
|
|
90
|
-
if (
|
|
89
|
+
const refsCount = Math.max(0, (identifierCounts.get(name) ?? 0) - 1);
|
|
90
|
+
if (refsCount === 0) {
|
|
91
91
|
issues.push({
|
|
92
92
|
rule: 'dead-code',
|
|
93
93
|
severity: 'warning',
|
package/dist/rules/shared.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { SourceFile, Node, FunctionDeclaration, ArrowFunction, FunctionExpression, MethodDeclaration } from 'ts-morph';
|
|
2
2
|
export type FunctionLike = FunctionDeclaration | ArrowFunction | FunctionExpression | MethodDeclaration;
|
|
3
|
+
export declare function getFileLines(file: SourceFile): string[];
|
|
4
|
+
export declare function collectFunctionLikes(file: SourceFile): FunctionLike[];
|
|
3
5
|
export declare function hasIgnoreComment(file: SourceFile, line: number): boolean;
|
|
4
6
|
export declare function isFileIgnored(file: SourceFile): boolean;
|
|
5
7
|
export declare function getSnippet(node: Node, file: SourceFile): string;
|
package/dist/rules/shared.js
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
|
-
|
|
1
|
+
import { SyntaxKind, } from 'ts-morph';
|
|
2
|
+
const fileLinesCache = new WeakMap();
|
|
3
|
+
const functionLikesCache = new WeakMap();
|
|
4
|
+
export function getFileLines(file) {
|
|
5
|
+
const cached = fileLinesCache.get(file);
|
|
6
|
+
if (cached)
|
|
7
|
+
return cached;
|
|
2
8
|
const lines = file.getFullText().split('\n');
|
|
9
|
+
fileLinesCache.set(file, lines);
|
|
10
|
+
return lines;
|
|
11
|
+
}
|
|
12
|
+
export function collectFunctionLikes(file) {
|
|
13
|
+
const cached = functionLikesCache.get(file);
|
|
14
|
+
if (cached)
|
|
15
|
+
return cached;
|
|
16
|
+
const fns = [
|
|
17
|
+
...file.getFunctions(),
|
|
18
|
+
...file.getDescendantsOfKind(SyntaxKind.ArrowFunction),
|
|
19
|
+
...file.getDescendantsOfKind(SyntaxKind.FunctionExpression),
|
|
20
|
+
...file.getClasses().flatMap((c) => c.getMethods()),
|
|
21
|
+
];
|
|
22
|
+
functionLikesCache.set(file, fns);
|
|
23
|
+
return fns;
|
|
24
|
+
}
|
|
25
|
+
export function hasIgnoreComment(file, line) {
|
|
26
|
+
const lines = getFileLines(file);
|
|
3
27
|
const currentLine = lines[line - 1] ?? '';
|
|
4
28
|
const prevLine = lines[line - 2] ?? '';
|
|
5
29
|
if (/\/\/\s*drift-ignore\b/.test(currentLine))
|
|
@@ -9,12 +33,12 @@ export function hasIgnoreComment(file, line) {
|
|
|
9
33
|
return false;
|
|
10
34
|
}
|
|
11
35
|
export function isFileIgnored(file) {
|
|
12
|
-
const firstLines = file
|
|
36
|
+
const firstLines = getFileLines(file).slice(0, 10).join('\n'); // drift-ignore
|
|
13
37
|
return /\/\/\s*drift-ignore-file\b/.test(firstLines);
|
|
14
38
|
}
|
|
15
39
|
export function getSnippet(node, file) {
|
|
16
40
|
const startLine = node.getStartLineNumber();
|
|
17
|
-
const lines = file
|
|
41
|
+
const lines = getFileLines(file);
|
|
18
42
|
return lines
|
|
19
43
|
.slice(Math.max(0, startLine - 1), startLine + 1)
|
|
20
44
|
.join('\n')
|
package/dist/saas.d.ts
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import type { DriftReport, DriftConfig } from './types.js';
|
|
2
|
+
export interface SaasPolicy {
|
|
3
|
+
freeUserThreshold: number;
|
|
4
|
+
maxRunsPerWorkspacePerMonth: number;
|
|
5
|
+
maxReposPerWorkspace: number;
|
|
6
|
+
retentionDays: number;
|
|
7
|
+
strictActorEnforcement: boolean;
|
|
8
|
+
maxWorkspacesPerOrganizationByPlan: Record<SaasPlan, number>;
|
|
9
|
+
}
|
|
10
|
+
export type SaasRole = 'owner' | 'member' | 'viewer';
|
|
11
|
+
export type SaasPlan = 'free' | 'sponsor' | 'team' | 'business';
|
|
12
|
+
export interface SaasUser {
|
|
13
|
+
id: string;
|
|
14
|
+
createdAt: string;
|
|
15
|
+
lastSeenAt: string;
|
|
16
|
+
}
|
|
17
|
+
export interface SaasOrganization {
|
|
18
|
+
id: string;
|
|
19
|
+
plan: SaasPlan;
|
|
20
|
+
createdAt: string;
|
|
21
|
+
lastSeenAt: string;
|
|
22
|
+
workspaceIds: string[];
|
|
23
|
+
}
|
|
24
|
+
export interface SaasWorkspace {
|
|
25
|
+
id: string;
|
|
26
|
+
organizationId: string;
|
|
27
|
+
createdAt: string;
|
|
28
|
+
lastSeenAt: string;
|
|
29
|
+
userIds: string[];
|
|
30
|
+
repoIds: string[];
|
|
31
|
+
}
|
|
32
|
+
export interface SaasRepo {
|
|
33
|
+
id: string;
|
|
34
|
+
organizationId: string;
|
|
35
|
+
workspaceId: string;
|
|
36
|
+
name: string;
|
|
37
|
+
createdAt: string;
|
|
38
|
+
lastSeenAt: string;
|
|
39
|
+
}
|
|
40
|
+
export interface SaasMembership {
|
|
41
|
+
id: string;
|
|
42
|
+
organizationId: string;
|
|
43
|
+
workspaceId: string;
|
|
44
|
+
userId: string;
|
|
45
|
+
role: SaasRole;
|
|
46
|
+
createdAt: string;
|
|
47
|
+
lastSeenAt: string;
|
|
48
|
+
}
|
|
49
|
+
export interface SaasPlanChange {
|
|
50
|
+
id: string;
|
|
51
|
+
organizationId: string;
|
|
52
|
+
fromPlan: SaasPlan;
|
|
53
|
+
toPlan: SaasPlan;
|
|
54
|
+
changedAt: string;
|
|
55
|
+
changedByUserId: string;
|
|
56
|
+
reason?: string;
|
|
57
|
+
}
|
|
58
|
+
export interface SaasSnapshot {
|
|
59
|
+
id: string;
|
|
60
|
+
createdAt: string;
|
|
61
|
+
scannedAt: string;
|
|
62
|
+
organizationId: string;
|
|
63
|
+
workspaceId: string;
|
|
64
|
+
userId: string;
|
|
65
|
+
role: SaasRole;
|
|
66
|
+
plan: SaasPlan;
|
|
67
|
+
repoId: string;
|
|
68
|
+
repoName: string;
|
|
69
|
+
targetPath: string;
|
|
70
|
+
totalScore: number;
|
|
71
|
+
totalIssues: number;
|
|
72
|
+
totalFiles: number;
|
|
73
|
+
summary: {
|
|
74
|
+
errors: number;
|
|
75
|
+
warnings: number;
|
|
76
|
+
infos: number;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
export interface SaasStore {
|
|
80
|
+
version: number;
|
|
81
|
+
policy: SaasPolicy;
|
|
82
|
+
users: Record<string, SaasUser>;
|
|
83
|
+
organizations: Record<string, SaasOrganization>;
|
|
84
|
+
workspaces: Record<string, SaasWorkspace>;
|
|
85
|
+
memberships: Record<string, SaasMembership>;
|
|
86
|
+
repos: Record<string, SaasRepo>;
|
|
87
|
+
snapshots: SaasSnapshot[];
|
|
88
|
+
planChanges: SaasPlanChange[];
|
|
89
|
+
}
|
|
90
|
+
export type SaasOperation = 'snapshot:write' | 'snapshot:read' | 'summary:read' | 'billing:write' | 'billing:read';
|
|
91
|
+
export interface SaasPermissionContext {
|
|
92
|
+
operation: SaasOperation;
|
|
93
|
+
organizationId: string;
|
|
94
|
+
workspaceId?: string;
|
|
95
|
+
actorUserId?: string;
|
|
96
|
+
}
|
|
97
|
+
export interface SaasPermissionResult {
|
|
98
|
+
actorRole?: SaasRole;
|
|
99
|
+
requiredRole: SaasRole;
|
|
100
|
+
}
|
|
101
|
+
export interface SaasEffectiveLimits {
|
|
102
|
+
plan: SaasPlan;
|
|
103
|
+
maxWorkspaces: number;
|
|
104
|
+
maxReposPerWorkspace: number;
|
|
105
|
+
maxRunsPerWorkspacePerMonth: number;
|
|
106
|
+
retentionDays: number;
|
|
107
|
+
}
|
|
108
|
+
export interface SaasOrganizationUsageSnapshot {
|
|
109
|
+
organizationId: string;
|
|
110
|
+
plan: SaasPlan;
|
|
111
|
+
capturedAt: string;
|
|
112
|
+
workspaceCount: number;
|
|
113
|
+
repoCount: number;
|
|
114
|
+
runCount: number;
|
|
115
|
+
runCountThisMonth: number;
|
|
116
|
+
}
|
|
117
|
+
export interface ChangeOrganizationPlanOptions {
|
|
118
|
+
organizationId: string;
|
|
119
|
+
actorUserId: string;
|
|
120
|
+
newPlan: SaasPlan;
|
|
121
|
+
reason?: string;
|
|
122
|
+
storeFile?: string;
|
|
123
|
+
policy?: SaasPolicyOverrides;
|
|
124
|
+
}
|
|
125
|
+
export interface SaasUsageQueryOptions {
|
|
126
|
+
organizationId: string;
|
|
127
|
+
month?: string;
|
|
128
|
+
storeFile?: string;
|
|
129
|
+
policy?: SaasPolicyOverrides;
|
|
130
|
+
actorUserId?: string;
|
|
131
|
+
}
|
|
132
|
+
export interface SaasPlanChangeQueryOptions {
|
|
133
|
+
organizationId: string;
|
|
134
|
+
storeFile?: string;
|
|
135
|
+
policy?: SaasPolicyOverrides;
|
|
136
|
+
actorUserId?: string;
|
|
137
|
+
}
|
|
138
|
+
export interface SaasSummary {
|
|
139
|
+
policy: SaasPolicy;
|
|
140
|
+
usersRegistered: number;
|
|
141
|
+
workspacesActive: number;
|
|
142
|
+
reposActive: number;
|
|
143
|
+
runsPerMonth: Record<string, number>;
|
|
144
|
+
totalSnapshots: number;
|
|
145
|
+
phase: 'free' | 'paid';
|
|
146
|
+
thresholdReached: boolean;
|
|
147
|
+
freeUsersRemaining: number;
|
|
148
|
+
}
|
|
149
|
+
export interface SaasPolicyOverrides {
|
|
150
|
+
freeUserThreshold?: number;
|
|
151
|
+
maxRunsPerWorkspacePerMonth?: number;
|
|
152
|
+
maxReposPerWorkspace?: number;
|
|
153
|
+
retentionDays?: number;
|
|
154
|
+
strictActorEnforcement?: boolean;
|
|
155
|
+
maxWorkspacesPerOrganizationByPlan?: Partial<Record<SaasPlan, number>>;
|
|
156
|
+
}
|
|
157
|
+
export interface SaasQueryOptions {
|
|
158
|
+
storeFile?: string;
|
|
159
|
+
policy?: SaasPolicyOverrides;
|
|
160
|
+
organizationId?: string;
|
|
161
|
+
workspaceId?: string;
|
|
162
|
+
actorUserId?: string;
|
|
163
|
+
}
|
|
164
|
+
export interface IngestOptions {
|
|
165
|
+
organizationId?: string;
|
|
166
|
+
workspaceId: string;
|
|
167
|
+
userId: string;
|
|
168
|
+
role?: SaasRole;
|
|
169
|
+
plan?: SaasPlan;
|
|
170
|
+
repoName?: string;
|
|
171
|
+
actorUserId?: string;
|
|
172
|
+
storeFile?: string;
|
|
173
|
+
policy?: SaasPolicyOverrides;
|
|
174
|
+
}
|
|
175
|
+
export declare class SaasPermissionError extends Error {
|
|
176
|
+
readonly code = "SAAS_PERMISSION_DENIED";
|
|
177
|
+
readonly operation: SaasOperation;
|
|
178
|
+
readonly organizationId: string;
|
|
179
|
+
readonly workspaceId?: string;
|
|
180
|
+
readonly actorUserId?: string;
|
|
181
|
+
readonly requiredRole: SaasRole;
|
|
182
|
+
readonly actorRole?: SaasRole;
|
|
183
|
+
constructor(context: SaasPermissionContext, requiredRole: SaasRole, actorRole?: SaasRole);
|
|
184
|
+
}
|
|
185
|
+
export declare class SaasActorRequiredError extends Error {
|
|
186
|
+
readonly code = "SAAS_ACTOR_REQUIRED";
|
|
187
|
+
readonly operation: SaasOperation;
|
|
188
|
+
readonly organizationId: string;
|
|
189
|
+
readonly workspaceId?: string;
|
|
190
|
+
constructor(context: SaasPermissionContext);
|
|
191
|
+
}
|
|
192
|
+
export declare const DEFAULT_SAAS_POLICY: SaasPolicy;
|
|
193
|
+
export declare function resolveSaasPolicy(policy?: SaasPolicyOverrides | DriftConfig['saas']): SaasPolicy;
|
|
194
|
+
export declare function defaultSaasStorePath(root?: string): string;
|
|
195
|
+
export declare function getRequiredRoleForOperation(operation: SaasOperation): SaasRole;
|
|
196
|
+
export declare function assertSaasPermission(context: SaasPermissionContext & {
|
|
197
|
+
storeFile?: string;
|
|
198
|
+
policy?: SaasPolicyOverrides;
|
|
199
|
+
}): SaasPermissionResult;
|
|
200
|
+
export declare function getSaasEffectiveLimits(input: {
|
|
201
|
+
plan: SaasPlan;
|
|
202
|
+
policy?: SaasPolicyOverrides;
|
|
203
|
+
}): SaasEffectiveLimits;
|
|
204
|
+
export declare function getOrganizationEffectiveLimits(options: {
|
|
205
|
+
organizationId: string;
|
|
206
|
+
storeFile?: string;
|
|
207
|
+
policy?: SaasPolicyOverrides;
|
|
208
|
+
}): SaasEffectiveLimits;
|
|
209
|
+
export declare function changeOrganizationPlan(options: ChangeOrganizationPlanOptions): SaasPlanChange;
|
|
210
|
+
export declare function listOrganizationPlanChanges(options: SaasPlanChangeQueryOptions): SaasPlanChange[];
|
|
211
|
+
export declare function getOrganizationUsageSnapshot(options: SaasUsageQueryOptions): SaasOrganizationUsageSnapshot;
|
|
212
|
+
export declare function ingestSnapshotFromReport(report: DriftReport, options: IngestOptions): SaasSnapshot;
|
|
213
|
+
export declare function listSaasSnapshots(options?: SaasQueryOptions): SaasSnapshot[];
|
|
214
|
+
export declare function getSaasSummary(options?: SaasQueryOptions): SaasSummary;
|
|
215
|
+
export declare function generateSaasDashboardHtml(options?: {
|
|
216
|
+
storeFile?: string;
|
|
217
|
+
policy?: SaasPolicyOverrides;
|
|
218
|
+
}): string;
|
|
219
|
+
//# sourceMappingURL=saas.d.ts.map
|