@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.
- package/.gga +50 -0
- package/.github/actions/drift-review/README.md +60 -0
- package/.github/actions/drift-review/action.yml +131 -0
- package/.github/actions/drift-scan/README.md +28 -32
- package/.github/actions/drift-scan/action.yml +78 -14
- package/.github/workflows/publish-vscode.yml +3 -3
- package/.github/workflows/publish.yml +3 -3
- package/.github/workflows/review-pr.yml +94 -9
- package/AGENTS.md +75 -245
- package/CHANGELOG.md +28 -0
- package/README.md +308 -51
- 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 +204 -0
- package/dist/cli.js +693 -67
- package/dist/config.js +16 -2
- package/dist/diff.js +66 -10
- package/dist/doctor.d.ts +5 -0
- package/dist/doctor.js +133 -0
- package/dist/format.d.ts +17 -0
- package/dist/format.js +45 -0
- package/dist/git.js +12 -0
- package/dist/guard-types.d.ts +57 -0
- package/dist/guard-types.js +2 -0
- package/dist/guard.d.ts +14 -0
- package/dist/guard.js +239 -0
- package/dist/index.d.ts +12 -3
- package/dist/index.js +6 -1
- package/dist/init.d.ts +15 -0
- package/dist/init.js +273 -0
- package/dist/map-cycles.d.ts +2 -0
- package/dist/map-cycles.js +34 -0
- package/dist/map-svg.d.ts +19 -0
- package/dist/map-svg.js +97 -0
- package/dist/map.js +78 -138
- package/dist/metrics.js +70 -55
- package/dist/output-metadata.d.ts +13 -0
- package/dist/output-metadata.js +17 -0
- package/dist/plugins-capabilities.d.ts +4 -0
- package/dist/plugins-capabilities.js +21 -0
- package/dist/plugins-messages.d.ts +10 -0
- package/dist/plugins-messages.js +16 -0
- package/dist/plugins-rules.d.ts +9 -0
- package/dist/plugins-rules.js +137 -0
- package/dist/plugins.d.ts +2 -1
- package/dist/plugins.js +80 -28
- package/dist/printer.js +4 -0
- package/dist/reporter-constants.d.ts +16 -0
- package/dist/reporter-constants.js +39 -0
- package/dist/reporter.d.ts +3 -3
- package/dist/reporter.js +35 -55
- package/dist/review.d.ts +2 -1
- package/dist/review.js +4 -3
- 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/phase3-configurable.js +23 -15
- package/dist/rules/shared.d.ts +2 -0
- package/dist/rules/shared.js +27 -3
- package/dist/saas/constants.d.ts +15 -0
- package/dist/saas/constants.js +48 -0
- package/dist/saas/dashboard.d.ts +8 -0
- package/dist/saas/dashboard.js +132 -0
- package/dist/saas/errors.d.ts +19 -0
- package/dist/saas/errors.js +37 -0
- package/dist/saas/helpers.d.ts +21 -0
- package/dist/saas/helpers.js +110 -0
- package/dist/saas/ingest.d.ts +3 -0
- package/dist/saas/ingest.js +249 -0
- package/dist/saas/organization.d.ts +5 -0
- package/dist/saas/organization.js +82 -0
- package/dist/saas/plan-change.d.ts +10 -0
- package/dist/saas/plan-change.js +15 -0
- package/dist/saas/store.d.ts +21 -0
- package/dist/saas/store.js +159 -0
- package/dist/saas/types.d.ts +191 -0
- package/dist/saas/types.js +2 -0
- package/dist/saas.d.ts +8 -82
- package/dist/saas.js +7 -320
- package/dist/sarif.d.ts +74 -0
- package/dist/sarif.js +122 -0
- package/dist/trust-advanced.d.ts +14 -0
- package/dist/trust-advanced.js +65 -0
- package/dist/trust-kpi-fs.d.ts +3 -0
- package/dist/trust-kpi-fs.js +141 -0
- package/dist/trust-kpi-parse.d.ts +7 -0
- package/dist/trust-kpi-parse.js +186 -0
- package/dist/trust-kpi-types.d.ts +16 -0
- package/dist/trust-kpi-types.js +2 -0
- package/dist/trust-kpi.d.ts +7 -0
- package/dist/trust-kpi.js +185 -0
- package/dist/trust-policy.d.ts +32 -0
- package/dist/trust-policy.js +160 -0
- package/dist/trust-render.d.ts +9 -0
- package/dist/trust-render.js +54 -0
- package/dist/trust-scoring.d.ts +9 -0
- package/dist/trust-scoring.js +208 -0
- package/dist/trust.d.ts +37 -0
- package/dist/trust.js +168 -0
- package/dist/types/app.d.ts +30 -0
- package/dist/types/app.js +2 -0
- package/dist/types/config.d.ts +25 -0
- package/dist/types/config.js +2 -0
- package/dist/types/core.d.ts +100 -0
- package/dist/types/core.js +2 -0
- package/dist/types/diff.d.ts +55 -0
- package/dist/types/diff.js +2 -0
- package/dist/types/plugin.d.ts +41 -0
- package/dist/types/plugin.js +2 -0
- package/dist/types/trust.d.ts +120 -0
- package/dist/types/trust.js +2 -0
- package/dist/types.d.ts +8 -211
- package/docs/PRD.md +187 -109
- package/docs/plugin-contract.md +61 -0
- package/docs/release-notes-draft.md +40 -0
- package/docs/rules-catalog.md +49 -0
- package/docs/trust-core-release-checklist.md +87 -0
- package/package.json +6 -3
- package/packages/vscode-drift/src/code-actions.ts +1 -1
- package/schemas/drift-ai-output.v1.json +162 -0
- package/schemas/drift-report.v1.json +151 -0
- package/schemas/drift-trust.v1.json +131 -0
- package/scripts/smoke-repo.mjs +394 -0
- package/src/analyzer.ts +484 -155
- package/src/benchmark.ts +266 -0
- package/src/cli.ts +840 -85
- package/src/config.ts +19 -2
- package/src/diff.ts +84 -10
- package/src/doctor.ts +173 -0
- package/src/format.ts +81 -0
- package/src/git.ts +16 -0
- package/src/guard-types.ts +64 -0
- package/src/guard.ts +324 -0
- package/src/index.ts +83 -0
- package/src/init.ts +298 -0
- package/src/map-cycles.ts +38 -0
- package/src/map-svg.ts +124 -0
- package/src/map.ts +111 -142
- package/src/metrics.ts +78 -59
- package/src/output-metadata.ts +30 -0
- package/src/plugins-capabilities.ts +36 -0
- package/src/plugins-messages.ts +35 -0
- package/src/plugins-rules.ts +296 -0
- package/src/plugins.ts +148 -27
- package/src/printer.ts +4 -0
- package/src/reporter-constants.ts +46 -0
- package/src/reporter.ts +64 -65
- package/src/review.ts +6 -4
- 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/phase3-configurable.ts +39 -26
- package/src/rules/shared.ts +31 -3
- package/src/saas/constants.ts +56 -0
- package/src/saas/dashboard.ts +172 -0
- package/src/saas/errors.ts +45 -0
- package/src/saas/helpers.ts +140 -0
- package/src/saas/ingest.ts +278 -0
- package/src/saas/organization.ts +99 -0
- package/src/saas/plan-change.ts +19 -0
- package/src/saas/store.ts +172 -0
- package/src/saas/types.ts +216 -0
- package/src/saas.ts +49 -433
- package/src/sarif.ts +232 -0
- package/src/trust-advanced.ts +99 -0
- package/src/trust-kpi-fs.ts +169 -0
- package/src/trust-kpi-parse.ts +219 -0
- package/src/trust-kpi-types.ts +19 -0
- package/src/trust-kpi.ts +210 -0
- package/src/trust-policy.ts +246 -0
- package/src/trust-render.ts +61 -0
- package/src/trust-scoring.ts +231 -0
- package/src/trust.ts +260 -0
- package/src/types/app.ts +30 -0
- package/src/types/config.ts +27 -0
- package/src/types/core.ts +105 -0
- package/src/types/diff.ts +61 -0
- package/src/types/plugin.ts +46 -0
- package/src/types/trust.ts +134 -0
- package/src/types.ts +78 -238
- package/tests/cli-sarif.test.ts +92 -0
- package/tests/diff.test.ts +124 -0
- package/tests/format.test.ts +157 -0
- package/tests/new-features.test.ts +80 -1
- package/tests/phase1-init-doctor-guard.test.ts +199 -0
- package/tests/plugins.test.ts +219 -0
- package/tests/rules.test.ts +23 -1
- package/tests/saas-foundation.test.ts +358 -1
- package/tests/sarif.test.ts +160 -0
- package/tests/trust-kpi.test.ts +147 -0
- package/tests/trust.test.ts +602 -0
package/dist/config.js
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { join, resolve } from 'node:path';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
|
+
function normalizeLegacyConfig(config) {
|
|
5
|
+
if (config.modules !== undefined) {
|
|
6
|
+
return config;
|
|
7
|
+
}
|
|
8
|
+
const legacyModules = config.moduleBoundaries ?? config.boundaries;
|
|
9
|
+
if (!legacyModules || legacyModules.length === 0) {
|
|
10
|
+
return config;
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
...config,
|
|
14
|
+
modules: legacyModules,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
4
17
|
/**
|
|
5
18
|
* Load drift.config.ts / .js / .json from the given project root.
|
|
6
19
|
* Returns undefined if no config file is found.
|
|
@@ -23,13 +36,14 @@ export async function loadConfig(projectRoot) {
|
|
|
23
36
|
const ext = candidate.split('.').pop();
|
|
24
37
|
if (ext === 'json') {
|
|
25
38
|
const { readFileSync } = await import('node:fs');
|
|
26
|
-
|
|
39
|
+
const rawConfig = JSON.parse(readFileSync(candidate, 'utf-8'));
|
|
40
|
+
return normalizeLegacyConfig(rawConfig);
|
|
27
41
|
}
|
|
28
42
|
// .ts / .js — dynamic import via file URL
|
|
29
43
|
const fileUrl = pathToFileURL(resolve(candidate)).href;
|
|
30
44
|
const mod = await import(fileUrl);
|
|
31
45
|
const config = mod.default ?? mod;
|
|
32
|
-
return config;
|
|
46
|
+
return normalizeLegacyConfig(config);
|
|
33
47
|
}
|
|
34
48
|
catch { // drift-ignore
|
|
35
49
|
// drift-ignore: catch-swallow — config is optional; load failure is non-fatal
|
package/dist/diff.js
CHANGED
|
@@ -1,7 +1,59 @@
|
|
|
1
|
+
function normalizePath(filePath) {
|
|
2
|
+
return filePath.replace(/\\/g, '/');
|
|
3
|
+
}
|
|
4
|
+
function normalizeIssueText(value) {
|
|
5
|
+
return value
|
|
6
|
+
.replace(/\r\n/g, '\n')
|
|
7
|
+
.replace(/\r/g, '\n')
|
|
8
|
+
.replace(/\s+/g, ' ')
|
|
9
|
+
.trim();
|
|
10
|
+
}
|
|
11
|
+
const SNIPPET_PREFIX_LENGTH = 80;
|
|
12
|
+
function strictIssueKey(i) {
|
|
13
|
+
return `${i.rule}:${i.line}:${i.column}`;
|
|
14
|
+
}
|
|
15
|
+
function normalizedIssueKey(i) {
|
|
16
|
+
const normalizedMessage = normalizeIssueText(i.message);
|
|
17
|
+
const normalizedSnippetPrefix = normalizeIssueText(i.snippet).slice(0, SNIPPET_PREFIX_LENGTH);
|
|
18
|
+
return `${i.rule}:${i.severity}:${i.line}:${normalizedMessage}:${normalizedSnippetPrefix}`;
|
|
19
|
+
}
|
|
20
|
+
function buildIssueIndex(issues, getKey, skip) {
|
|
21
|
+
const index = new Map();
|
|
22
|
+
for (const [idx, issue] of issues.entries()) {
|
|
23
|
+
if (skip?.has(idx))
|
|
24
|
+
continue;
|
|
25
|
+
const key = getKey(issue);
|
|
26
|
+
const bucket = index.get(key);
|
|
27
|
+
if (bucket)
|
|
28
|
+
bucket.push(idx);
|
|
29
|
+
else
|
|
30
|
+
index.set(key, [idx]);
|
|
31
|
+
}
|
|
32
|
+
return index;
|
|
33
|
+
}
|
|
34
|
+
function matchIssues(currentIssues, index, state, getKey) {
|
|
35
|
+
for (const [currentIndex, issue] of currentIssues.entries()) {
|
|
36
|
+
if (state.matchedCurrentIndexes.has(currentIndex))
|
|
37
|
+
continue;
|
|
38
|
+
const bucket = index.get(getKey(issue));
|
|
39
|
+
if (!bucket || bucket.length === 0)
|
|
40
|
+
continue;
|
|
41
|
+
const matchedIndex = bucket.shift();
|
|
42
|
+
if (matchedIndex === undefined)
|
|
43
|
+
continue;
|
|
44
|
+
state.matchedBaseIndexes.add(matchedIndex);
|
|
45
|
+
state.matchedCurrentIndexes.add(currentIndex);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
1
48
|
/**
|
|
2
49
|
* Compute the diff between two DriftReports.
|
|
3
50
|
*
|
|
4
|
-
* Issues are matched
|
|
51
|
+
* Issues are matched in two passes:
|
|
52
|
+
* 1) strict location key (rule + line + column)
|
|
53
|
+
* 2) normalized content key (rule + severity + line + message + snippet)
|
|
54
|
+
*
|
|
55
|
+
* This keeps deterministic matching while preventing false churn caused by
|
|
56
|
+
* cross-platform line ending changes and small column offset noise.
|
|
5
57
|
* A "new" issue exists in `current` but not in `base`.
|
|
6
58
|
* A "resolved" issue exists in `base` but not in `current`.
|
|
7
59
|
*/
|
|
@@ -11,11 +63,15 @@ function computeFileDiff(filePath, baseFile, currentFile) {
|
|
|
11
63
|
const scoreDelta = scoreAfter - scoreBefore;
|
|
12
64
|
const baseIssues = baseFile?.issues ?? [];
|
|
13
65
|
const currentIssues = currentFile?.issues ?? [];
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
|
|
66
|
+
const matchedBaseIndexes = new Set();
|
|
67
|
+
const matchedCurrentIndexes = new Set();
|
|
68
|
+
const matchState = { matchedBaseIndexes, matchedCurrentIndexes };
|
|
69
|
+
const baseStrictIndex = buildIssueIndex(baseIssues, strictIssueKey);
|
|
70
|
+
matchIssues(currentIssues, baseStrictIndex, matchState, strictIssueKey);
|
|
71
|
+
const baseNormalizedIndex = buildIssueIndex(baseIssues, normalizedIssueKey, matchedBaseIndexes);
|
|
72
|
+
matchIssues(currentIssues, baseNormalizedIndex, matchState, normalizedIssueKey);
|
|
73
|
+
const newIssues = currentIssues.filter((_, index) => !matchedCurrentIndexes.has(index));
|
|
74
|
+
const resolvedIssues = baseIssues.filter((_, index) => !matchedBaseIndexes.has(index));
|
|
19
75
|
if (scoreDelta !== 0 || newIssues.length > 0 || resolvedIssues.length > 0) {
|
|
20
76
|
return {
|
|
21
77
|
path: filePath,
|
|
@@ -30,11 +86,11 @@ function computeFileDiff(filePath, baseFile, currentFile) {
|
|
|
30
86
|
}
|
|
31
87
|
export function computeDiff(base, current, baseRef) {
|
|
32
88
|
const fileDiffs = [];
|
|
33
|
-
const baseByPath = new Map(base.files.map(f => [f.path, f]));
|
|
34
|
-
const currentByPath = new Map(current.files.map(f => [f.path, f]));
|
|
89
|
+
const baseByPath = new Map(base.files.map(f => [normalizePath(f.path), f]));
|
|
90
|
+
const currentByPath = new Map(current.files.map(f => [normalizePath(f.path), f]));
|
|
35
91
|
const allPaths = new Set([
|
|
36
|
-
...base.files.map(f => f.path),
|
|
37
|
-
...current.files.map(f => f.path),
|
|
92
|
+
...base.files.map(f => normalizePath(f.path)),
|
|
93
|
+
...current.files.map(f => normalizePath(f.path)),
|
|
38
94
|
]);
|
|
39
95
|
for (const filePath of allPaths) {
|
|
40
96
|
const baseFile = baseByPath.get(filePath);
|
package/dist/doctor.d.ts
ADDED
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import kleur from 'kleur';
|
|
4
|
+
const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx']);
|
|
5
|
+
const IGNORED_DIRECTORIES = new Set(['node_modules', '.git', 'dist', '.next', 'coverage']);
|
|
6
|
+
const DECIMAL_RADIX = 10;
|
|
7
|
+
const MIN_SUPPORTED_NODE_MAJOR = 18;
|
|
8
|
+
const LOW_MEMORY_SOURCE_FILE_THRESHOLD = 500;
|
|
9
|
+
const DRIFT_CONFIG_CANDIDATES = [
|
|
10
|
+
'drift.config.ts',
|
|
11
|
+
'drift.config.js',
|
|
12
|
+
'drift.config.mjs',
|
|
13
|
+
'drift.config.cjs',
|
|
14
|
+
'drift.config.json',
|
|
15
|
+
];
|
|
16
|
+
function parseNodeMajor(version) {
|
|
17
|
+
const parsed = Number.parseInt(version.replace(/^v/, '').split('.')[0] ?? '0', DECIMAL_RADIX);
|
|
18
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
19
|
+
}
|
|
20
|
+
function detectDriftConfig(projectPath) {
|
|
21
|
+
for (const candidate of DRIFT_CONFIG_CANDIDATES) {
|
|
22
|
+
if (existsSync(join(projectPath, candidate))) {
|
|
23
|
+
return candidate;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
function countSourceFiles(projectPath) {
|
|
29
|
+
let total = 0;
|
|
30
|
+
const stack = [projectPath];
|
|
31
|
+
while (stack.length > 0) {
|
|
32
|
+
const currentDir = stack.pop();
|
|
33
|
+
if (!currentDir)
|
|
34
|
+
continue;
|
|
35
|
+
const entries = readdirSync(currentDir, { withFileTypes: true });
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
if (entry.isDirectory() && !IGNORED_DIRECTORIES.has(entry.name)) {
|
|
38
|
+
stack.push(join(currentDir, entry.name));
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (entry.isDirectory()) {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (!entry.isFile())
|
|
45
|
+
continue;
|
|
46
|
+
const lastDot = entry.name.lastIndexOf('.');
|
|
47
|
+
if (lastDot === -1)
|
|
48
|
+
continue;
|
|
49
|
+
const extension = entry.name.slice(lastDot);
|
|
50
|
+
if (SOURCE_EXTENSIONS.has(extension)) {
|
|
51
|
+
total += 1;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return total;
|
|
56
|
+
}
|
|
57
|
+
function buildDoctorReport(projectPath) {
|
|
58
|
+
const nodeMajor = parseNodeMajor(process.version);
|
|
59
|
+
const packageJsonPath = join(projectPath, 'package.json');
|
|
60
|
+
const packageJsonFound = existsSync(packageJsonPath);
|
|
61
|
+
let esm = false;
|
|
62
|
+
if (packageJsonFound) {
|
|
63
|
+
const parsed = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
64
|
+
esm = parsed.type === 'module';
|
|
65
|
+
}
|
|
66
|
+
const sourceFilesCount = countSourceFiles(projectPath);
|
|
67
|
+
return {
|
|
68
|
+
targetPath: projectPath,
|
|
69
|
+
node: {
|
|
70
|
+
version: process.version,
|
|
71
|
+
major: nodeMajor,
|
|
72
|
+
supported: nodeMajor >= MIN_SUPPORTED_NODE_MAJOR,
|
|
73
|
+
},
|
|
74
|
+
project: {
|
|
75
|
+
packageJsonFound,
|
|
76
|
+
esm,
|
|
77
|
+
tsconfigFound: existsSync(join(projectPath, 'tsconfig.json')),
|
|
78
|
+
sourceFilesCount,
|
|
79
|
+
lowMemorySuggested: sourceFilesCount > LOW_MEMORY_SOURCE_FILE_THRESHOLD,
|
|
80
|
+
driftConfigFile: detectDriftConfig(projectPath),
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function printConsoleReport(report) {
|
|
85
|
+
const icons = {
|
|
86
|
+
check: kleur.green('✓'),
|
|
87
|
+
warn: kleur.yellow('⚠'),
|
|
88
|
+
error: kleur.red('✗'),
|
|
89
|
+
info: kleur.cyan('ℹ'),
|
|
90
|
+
};
|
|
91
|
+
process.stdout.write('\n');
|
|
92
|
+
process.stdout.write(`${kleur.bold().white('drift doctor')} ${kleur.gray('- environment diagnostics')}\n\n`);
|
|
93
|
+
const nodeStatus = report.node.supported
|
|
94
|
+
? `${icons.check} ${kleur.green('Node runtime supported')}`
|
|
95
|
+
: `${icons.warn} ${kleur.yellow('Node runtime below recommended minimum (>=18)')}`;
|
|
96
|
+
process.stdout.write(`${nodeStatus} ${kleur.gray(`(${report.node.version})`)}\n`);
|
|
97
|
+
if (report.project.packageJsonFound) {
|
|
98
|
+
process.stdout.write(`${icons.check} package.json found\n`);
|
|
99
|
+
process.stdout.write(`${icons.info} ESM mode: ${report.project.esm ? kleur.green('yes') : kleur.yellow('no')}\n`);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
process.stdout.write(`${icons.warn} package.json not found\n`);
|
|
103
|
+
process.stdout.write(`${icons.info} ESM mode: ${kleur.gray('unknown')}\n`);
|
|
104
|
+
}
|
|
105
|
+
if (report.project.tsconfigFound) {
|
|
106
|
+
process.stdout.write(`${icons.check} tsconfig.json found\n`);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
process.stdout.write(`${icons.warn} tsconfig.json not found\n`);
|
|
110
|
+
}
|
|
111
|
+
process.stdout.write(`${icons.info} Source files (.ts/.tsx/.js/.jsx): ${report.project.sourceFilesCount}\n`);
|
|
112
|
+
if (report.project.lowMemorySuggested) {
|
|
113
|
+
process.stdout.write(`${icons.warn} Large codebase detected, consider ${kleur.bold('--low-memory')}\n`);
|
|
114
|
+
}
|
|
115
|
+
if (report.project.driftConfigFile) {
|
|
116
|
+
process.stdout.write(`${icons.check} Drift config: ${report.project.driftConfigFile}\n`);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
process.stdout.write(`${icons.warn} Drift config not found (drift.config.*)\n`);
|
|
120
|
+
}
|
|
121
|
+
process.stdout.write('\n');
|
|
122
|
+
}
|
|
123
|
+
export async function runDoctor(projectPath, options) {
|
|
124
|
+
const report = buildDoctorReport(projectPath);
|
|
125
|
+
if (options?.json) {
|
|
126
|
+
process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
printConsoleReport(report);
|
|
130
|
+
}
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=doctor.js.map
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
declare const UNIFIED_FORMAT_VALUES: readonly ["console", "json", "markdown", "ai", "sarif"];
|
|
2
|
+
type UnifiedOutputFormat = (typeof UNIFIED_FORMAT_VALUES)[number];
|
|
3
|
+
type LegacyAlias = {
|
|
4
|
+
flag: string;
|
|
5
|
+
used?: boolean;
|
|
6
|
+
mapsTo: UnifiedOutputFormat;
|
|
7
|
+
};
|
|
8
|
+
interface ResolveOutputFormatOptions {
|
|
9
|
+
command: string;
|
|
10
|
+
format?: string;
|
|
11
|
+
supported: readonly UnifiedOutputFormat[];
|
|
12
|
+
legacyAliases?: LegacyAlias[];
|
|
13
|
+
onWarning?: (message: string) => void;
|
|
14
|
+
}
|
|
15
|
+
export declare function resolveOutputFormat(options: ResolveOutputFormatOptions): UnifiedOutputFormat;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=format.d.ts.map
|
package/dist/format.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const UNIFIED_FORMAT_VALUES = ['console', 'json', 'markdown', 'ai', 'sarif'];
|
|
2
|
+
function assertSupportedFormatValue(command, format) {
|
|
3
|
+
if (UNIFIED_FORMAT_VALUES.includes(format))
|
|
4
|
+
return;
|
|
5
|
+
throw new Error(`Invalid --format '${format}' for '${command}'. Allowed values: ${UNIFIED_FORMAT_VALUES.join(', ')}.`);
|
|
6
|
+
}
|
|
7
|
+
function throwUnsupportedFormat(command, selected, supported) {
|
|
8
|
+
throw new Error(`Format '${selected}' is not supported for '${command}'. Supported formats: ${supported.join(', ')}.`);
|
|
9
|
+
}
|
|
10
|
+
function normalizeLegacyFormatSelection(command, selectedLegacyFormats) {
|
|
11
|
+
if (selectedLegacyFormats.length === 0)
|
|
12
|
+
return undefined;
|
|
13
|
+
const uniqueFormats = [...new Set(selectedLegacyFormats)];
|
|
14
|
+
if (uniqueFormats.length > 1) {
|
|
15
|
+
throw new Error(`Conflicting legacy format flags for '${command}': ${uniqueFormats.join(' vs ')}. Use a single format option.`);
|
|
16
|
+
}
|
|
17
|
+
return uniqueFormats[0];
|
|
18
|
+
}
|
|
19
|
+
export function resolveOutputFormat(options) {
|
|
20
|
+
const { command, format, supported, onWarning } = options;
|
|
21
|
+
const legacyAliases = options.legacyAliases ?? [];
|
|
22
|
+
for (const alias of legacyAliases) {
|
|
23
|
+
if (!alias.used)
|
|
24
|
+
continue;
|
|
25
|
+
onWarning?.(`Warning: --${alias.flag} is deprecated for '${command}'. Use --format ${alias.mapsTo} instead.`);
|
|
26
|
+
}
|
|
27
|
+
const selectedLegacyFormat = normalizeLegacyFormatSelection(command, legacyAliases.filter((alias) => alias.used).map((alias) => alias.mapsTo));
|
|
28
|
+
const selectedFormat = format?.trim();
|
|
29
|
+
if (selectedFormat) {
|
|
30
|
+
assertSupportedFormatValue(command, selectedFormat);
|
|
31
|
+
if (selectedLegacyFormat && selectedLegacyFormat !== selectedFormat) {
|
|
32
|
+
throw new Error(`Conflicting format flags for '${command}': --format ${selectedFormat} and legacy alias for ${selectedLegacyFormat}.`);
|
|
33
|
+
}
|
|
34
|
+
if (!supported.includes(selectedFormat)) {
|
|
35
|
+
throwUnsupportedFormat(command, selectedFormat, supported);
|
|
36
|
+
}
|
|
37
|
+
return selectedFormat;
|
|
38
|
+
}
|
|
39
|
+
const resolvedFromLegacy = selectedLegacyFormat ?? 'console';
|
|
40
|
+
if (!supported.includes(resolvedFromLegacy)) {
|
|
41
|
+
throwUnsupportedFormat(command, resolvedFromLegacy, supported);
|
|
42
|
+
}
|
|
43
|
+
return resolvedFromLegacy;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=format.js.map
|
package/dist/git.js
CHANGED
|
@@ -54,6 +54,15 @@ function extractFile(projectPath, ref, filePath, tempDir) {
|
|
|
54
54
|
mkdirSync(destDir, { recursive: true });
|
|
55
55
|
writeFileSync(destPath, content, 'utf-8');
|
|
56
56
|
}
|
|
57
|
+
function extractArchiveAtRef(projectPath, ref, tempDir) {
|
|
58
|
+
try {
|
|
59
|
+
execSync(`git archive --format=tar ${ref} | tar -x -C "${tempDir}"`, { cwd: projectPath, stdio: 'pipe' });
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
57
66
|
export function extractFilesAtRef(projectPath, ref) {
|
|
58
67
|
verifyGitRepo(projectPath);
|
|
59
68
|
verifyRefExists(projectPath, ref);
|
|
@@ -63,6 +72,9 @@ export function extractFilesAtRef(projectPath, ref) {
|
|
|
63
72
|
}
|
|
64
73
|
const tempDir = join(tmpdir(), `drift-diff-${randomUUID()}`);
|
|
65
74
|
mkdirSync(tempDir, { recursive: true });
|
|
75
|
+
if (extractArchiveAtRef(projectPath, ref, tempDir)) {
|
|
76
|
+
return tempDir;
|
|
77
|
+
}
|
|
66
78
|
for (const filePath of tsFiles) {
|
|
67
79
|
extractFile(projectPath, ref, filePath, tempDir);
|
|
68
80
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { DriftAnalysisOptions, DriftDiff, DriftIssue, DriftReport } from './types.js';
|
|
2
|
+
export type IssueSeverity = DriftIssue['severity'];
|
|
3
|
+
export interface GuardBaseline {
|
|
4
|
+
score?: number;
|
|
5
|
+
totalIssues?: number;
|
|
6
|
+
errors?: number;
|
|
7
|
+
warnings?: number;
|
|
8
|
+
infos?: number;
|
|
9
|
+
bySeverity?: Partial<Record<IssueSeverity, number>>;
|
|
10
|
+
summary?: {
|
|
11
|
+
errors?: number;
|
|
12
|
+
warnings?: number;
|
|
13
|
+
infos?: number;
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export interface GuardThresholds {
|
|
17
|
+
error?: number;
|
|
18
|
+
warning?: number;
|
|
19
|
+
info?: number;
|
|
20
|
+
}
|
|
21
|
+
export interface GuardOptions {
|
|
22
|
+
baseRef?: string;
|
|
23
|
+
baselinePath?: string;
|
|
24
|
+
baseline?: GuardBaseline;
|
|
25
|
+
budget?: number;
|
|
26
|
+
bySeverity?: GuardThresholds;
|
|
27
|
+
analysis?: DriftAnalysisOptions;
|
|
28
|
+
}
|
|
29
|
+
export interface GuardMetrics {
|
|
30
|
+
scoreDelta: number;
|
|
31
|
+
totalIssuesDelta: number;
|
|
32
|
+
severityDelta: Record<IssueSeverity, number>;
|
|
33
|
+
}
|
|
34
|
+
export interface GuardCheck {
|
|
35
|
+
id: string;
|
|
36
|
+
passed: boolean;
|
|
37
|
+
actual: number;
|
|
38
|
+
limit: number;
|
|
39
|
+
message: string;
|
|
40
|
+
}
|
|
41
|
+
export interface GuardEvaluation {
|
|
42
|
+
passed: boolean;
|
|
43
|
+
checks: GuardCheck[];
|
|
44
|
+
}
|
|
45
|
+
export interface GuardResult {
|
|
46
|
+
scannedAt: string;
|
|
47
|
+
projectPath: string;
|
|
48
|
+
mode: 'diff' | 'baseline';
|
|
49
|
+
passed: boolean;
|
|
50
|
+
baseRef?: string;
|
|
51
|
+
baselinePath?: string;
|
|
52
|
+
metrics: GuardMetrics;
|
|
53
|
+
checks: GuardCheck[];
|
|
54
|
+
current: DriftReport;
|
|
55
|
+
diff?: DriftDiff;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=guard-types.d.ts.map
|
package/dist/guard.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { GuardEvaluation, GuardMetrics, GuardOptions, GuardResult, GuardThresholds } from './guard-types.js';
|
|
2
|
+
interface GuardEvalInput {
|
|
3
|
+
metrics: GuardMetrics;
|
|
4
|
+
budget?: number;
|
|
5
|
+
bySeverity?: GuardThresholds;
|
|
6
|
+
enforceNoRegression: {
|
|
7
|
+
score: boolean;
|
|
8
|
+
totalIssues: boolean;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export declare function evaluateGuard(input: GuardEvalInput): GuardEvaluation;
|
|
12
|
+
export declare function runGuard(targetPath: string, options?: GuardOptions): Promise<GuardResult>;
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=guard.d.ts.map
|
package/dist/guard.js
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
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
|
+
function parseNumber(value) {
|
|
9
|
+
return typeof value === 'number' && !Number.isNaN(value) ? value : undefined;
|
|
10
|
+
}
|
|
11
|
+
function normalizeBaseline(baseline) {
|
|
12
|
+
const bySeverityFromRoot = baseline.bySeverity;
|
|
13
|
+
const bySeverity = {
|
|
14
|
+
error: parseNumber(bySeverityFromRoot?.error) ?? parseNumber(baseline.errors) ?? parseNumber(baseline.summary?.errors),
|
|
15
|
+
warning: parseNumber(bySeverityFromRoot?.warning) ?? parseNumber(baseline.warnings) ?? parseNumber(baseline.summary?.warnings),
|
|
16
|
+
info: parseNumber(bySeverityFromRoot?.info) ?? parseNumber(baseline.infos) ?? parseNumber(baseline.summary?.infos),
|
|
17
|
+
};
|
|
18
|
+
const normalized = {
|
|
19
|
+
score: parseNumber(baseline.score),
|
|
20
|
+
totalIssues: parseNumber(baseline.totalIssues),
|
|
21
|
+
bySeverity,
|
|
22
|
+
};
|
|
23
|
+
const hasAnyAnchor = normalized.score !== undefined ||
|
|
24
|
+
normalized.totalIssues !== undefined ||
|
|
25
|
+
normalized.bySeverity.error !== undefined ||
|
|
26
|
+
normalized.bySeverity.warning !== undefined ||
|
|
27
|
+
normalized.bySeverity.info !== undefined;
|
|
28
|
+
if (!hasAnyAnchor) {
|
|
29
|
+
throw new Error('Invalid guard baseline: expected score, totalIssues, or severity counters (error/warning/info).');
|
|
30
|
+
}
|
|
31
|
+
return normalized;
|
|
32
|
+
}
|
|
33
|
+
function readBaselineFromFile(projectPath, baselinePath) {
|
|
34
|
+
const resolvedBaselinePath = resolve(projectPath, baselinePath ?? 'drift-baseline.json');
|
|
35
|
+
if (!existsSync(resolvedBaselinePath))
|
|
36
|
+
return undefined;
|
|
37
|
+
const raw = JSON.parse(readFileSync(resolvedBaselinePath, 'utf8'));
|
|
38
|
+
return {
|
|
39
|
+
baseline: normalizeBaseline(raw),
|
|
40
|
+
path: resolvedBaselinePath,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function remapBaseReportPaths(baseReport, tempDir, projectPath) {
|
|
44
|
+
return {
|
|
45
|
+
...baseReport,
|
|
46
|
+
files: baseReport.files.map((file) => ({
|
|
47
|
+
...file,
|
|
48
|
+
path: resolve(projectPath, relative(tempDir, file.path)),
|
|
49
|
+
})),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function countSeverityDeltaFromDiff(diff) {
|
|
53
|
+
const severityDelta = {
|
|
54
|
+
error: 0,
|
|
55
|
+
warning: 0,
|
|
56
|
+
info: 0,
|
|
57
|
+
};
|
|
58
|
+
for (const file of diff.files) {
|
|
59
|
+
for (const issue of file.newIssues) {
|
|
60
|
+
severityDelta[issue.severity] += 1;
|
|
61
|
+
}
|
|
62
|
+
for (const issue of file.resolvedIssues) {
|
|
63
|
+
severityDelta[issue.severity] -= 1;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return severityDelta;
|
|
67
|
+
}
|
|
68
|
+
function buildMetricsFromDiff(diff) {
|
|
69
|
+
return {
|
|
70
|
+
scoreDelta: diff.totalDelta,
|
|
71
|
+
totalIssuesDelta: diff.newIssuesCount - diff.resolvedIssuesCount,
|
|
72
|
+
severityDelta: countSeverityDeltaFromDiff(diff),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function buildMetricsFromBaseline(current, baseline) {
|
|
76
|
+
return {
|
|
77
|
+
scoreDelta: current.totalScore - (baseline.score ?? current.totalScore),
|
|
78
|
+
totalIssuesDelta: current.totalIssues - (baseline.totalIssues ?? current.totalIssues),
|
|
79
|
+
severityDelta: {
|
|
80
|
+
error: current.summary.errors - (baseline.bySeverity.error ?? current.summary.errors),
|
|
81
|
+
warning: current.summary.warnings - (baseline.bySeverity.warning ?? current.summary.warnings),
|
|
82
|
+
info: current.summary.infos - (baseline.bySeverity.info ?? current.summary.infos),
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function addCheck(checks, input) {
|
|
87
|
+
checks.push({
|
|
88
|
+
id: input.id,
|
|
89
|
+
passed: input.actual <= input.limit,
|
|
90
|
+
actual: input.actual,
|
|
91
|
+
limit: input.limit,
|
|
92
|
+
message: input.message,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
export function evaluateGuard(input) {
|
|
96
|
+
const checks = [];
|
|
97
|
+
if (input.enforceNoRegression.score) {
|
|
98
|
+
addCheck(checks, {
|
|
99
|
+
id: 'no-regression-score',
|
|
100
|
+
actual: input.metrics.scoreDelta,
|
|
101
|
+
limit: 0,
|
|
102
|
+
message: 'Score delta must be <= 0.',
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
if (input.enforceNoRegression.totalIssues) {
|
|
106
|
+
addCheck(checks, {
|
|
107
|
+
id: 'no-regression-total-issues',
|
|
108
|
+
actual: input.metrics.totalIssuesDelta,
|
|
109
|
+
limit: 0,
|
|
110
|
+
message: 'Total issues delta must be <= 0.',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
if (typeof input.budget === 'number' && !Number.isNaN(input.budget)) {
|
|
114
|
+
addCheck(checks, {
|
|
115
|
+
id: 'budget-total-delta',
|
|
116
|
+
actual: input.metrics.scoreDelta,
|
|
117
|
+
limit: input.budget,
|
|
118
|
+
message: `Score delta must be <= budget (${input.budget}).`,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
const severityThresholds = input.bySeverity;
|
|
122
|
+
if (severityThresholds) {
|
|
123
|
+
const severities = ['error', 'warning', 'info'];
|
|
124
|
+
for (const severity of severities) {
|
|
125
|
+
const threshold = severityThresholds[severity];
|
|
126
|
+
if (typeof threshold !== 'number' || Number.isNaN(threshold))
|
|
127
|
+
continue;
|
|
128
|
+
addCheck(checks, {
|
|
129
|
+
id: `severity-${severity}`,
|
|
130
|
+
actual: input.metrics.severityDelta[severity],
|
|
131
|
+
limit: threshold,
|
|
132
|
+
message: `${severity} delta must be <= ${threshold}.`,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return {
|
|
137
|
+
passed: checks.every((check) => check.passed),
|
|
138
|
+
checks,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
export async function runGuard(targetPath, options = {}) {
|
|
142
|
+
const runtimeState = await initializeGuardRuntime(targetPath, options);
|
|
143
|
+
const { projectPath, config, currentReport } = runtimeState;
|
|
144
|
+
let tempDir;
|
|
145
|
+
try {
|
|
146
|
+
if (options.baseRef) {
|
|
147
|
+
tempDir = extractFilesAtRef(projectPath, options.baseRef);
|
|
148
|
+
return createDiffGuardResult({
|
|
149
|
+
projectPath,
|
|
150
|
+
currentReport,
|
|
151
|
+
options,
|
|
152
|
+
tempDir,
|
|
153
|
+
config,
|
|
154
|
+
baseRef: options.baseRef,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
const inlineBaseline = options.baseline ? normalizeBaseline(options.baseline) : undefined;
|
|
158
|
+
const fileBaseline = inlineBaseline ? undefined : readBaselineFromFile(projectPath, options.baselinePath);
|
|
159
|
+
const baseline = inlineBaseline ?? fileBaseline?.baseline;
|
|
160
|
+
const baselinePath = fileBaseline?.path;
|
|
161
|
+
if (!baseline) {
|
|
162
|
+
throw new Error('Guard requires a comparison point: provide baseRef or a baseline (inline or file).');
|
|
163
|
+
}
|
|
164
|
+
return createBaselineGuardResult({
|
|
165
|
+
projectPath,
|
|
166
|
+
currentReport,
|
|
167
|
+
options,
|
|
168
|
+
baseline,
|
|
169
|
+
baselinePath,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
if (tempDir)
|
|
174
|
+
cleanupTempDir(tempDir);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
async function initializeGuardRuntime(targetPath, options) {
|
|
178
|
+
const projectPath = resolve(targetPath);
|
|
179
|
+
const config = await loadConfig(projectPath);
|
|
180
|
+
const currentFiles = analyzeProject(projectPath, config, options.analysis);
|
|
181
|
+
const currentReport = buildReport(projectPath, currentFiles);
|
|
182
|
+
return {
|
|
183
|
+
projectPath,
|
|
184
|
+
config,
|
|
185
|
+
currentReport,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function createDiffGuardResult(input) {
|
|
189
|
+
const { projectPath, currentReport, options, tempDir, config, baseRef } = input;
|
|
190
|
+
const baseFiles = analyzeProject(tempDir, config, options.analysis);
|
|
191
|
+
const baseReport = buildReport(tempDir, baseFiles);
|
|
192
|
+
const remappedBase = remapBaseReportPaths(baseReport, tempDir, projectPath);
|
|
193
|
+
const diff = computeDiff(remappedBase, currentReport, baseRef);
|
|
194
|
+
const metrics = buildMetricsFromDiff(diff);
|
|
195
|
+
const evaluation = evaluateGuard({
|
|
196
|
+
metrics,
|
|
197
|
+
budget: options.budget,
|
|
198
|
+
bySeverity: options.bySeverity,
|
|
199
|
+
enforceNoRegression: {
|
|
200
|
+
score: true,
|
|
201
|
+
totalIssues: true,
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
return {
|
|
205
|
+
scannedAt: new Date().toISOString(),
|
|
206
|
+
projectPath,
|
|
207
|
+
mode: 'diff',
|
|
208
|
+
passed: evaluation.passed,
|
|
209
|
+
baseRef,
|
|
210
|
+
metrics,
|
|
211
|
+
checks: evaluation.checks,
|
|
212
|
+
current: currentReport,
|
|
213
|
+
diff,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function createBaselineGuardResult(input) {
|
|
217
|
+
const { projectPath, currentReport, options, baseline, baselinePath } = input;
|
|
218
|
+
const metrics = buildMetricsFromBaseline(currentReport, baseline);
|
|
219
|
+
const evaluation = evaluateGuard({
|
|
220
|
+
metrics,
|
|
221
|
+
budget: options.budget,
|
|
222
|
+
bySeverity: options.bySeverity,
|
|
223
|
+
enforceNoRegression: {
|
|
224
|
+
score: baseline.score !== undefined,
|
|
225
|
+
totalIssues: baseline.totalIssues !== undefined,
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
return {
|
|
229
|
+
scannedAt: new Date().toISOString(),
|
|
230
|
+
projectPath,
|
|
231
|
+
mode: 'baseline',
|
|
232
|
+
passed: evaluation.passed,
|
|
233
|
+
baselinePath,
|
|
234
|
+
metrics,
|
|
235
|
+
checks: evaluation.checks,
|
|
236
|
+
current: currentReport,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
//# sourceMappingURL=guard.js.map
|