@alimaandev/ripple 0.7.0 → 0.7.2
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/README.md +114 -51
- package/dist/bin.d.ts +1 -1
- package/dist/bin.js +390 -189
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-CFH4UW2E.js → chunk-VSLCAPAH.js} +3 -3
- package/dist/chunk-VSLCAPAH.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/package.json +12 -3
- package/ripple.schema.json +2 -2
- package/dist/chunk-CFH4UW2E.js.map +0 -1
package/dist/bin.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { rippleConfigSchema, DEFAULT_CONFIG, formatSchemaIssues } from './chunk-
|
|
2
|
+
import { rippleConfigSchema, DEFAULT_CONFIG, formatSchemaIssues } from './chunk-VSLCAPAH.js';
|
|
3
3
|
import { CommanderError, Command } from 'commander';
|
|
4
4
|
import { createJiti } from 'jiti';
|
|
5
5
|
import path8 from 'path';
|
|
6
|
-
import
|
|
7
|
-
import
|
|
6
|
+
import fs4 from 'fs/promises';
|
|
7
|
+
import fs3 from 'fs';
|
|
8
8
|
import { Project, ts, SyntaxKind } from 'ts-morph';
|
|
9
9
|
import chalk from 'chalk';
|
|
10
10
|
import figures from 'figures';
|
|
11
11
|
import treeify from 'treeify';
|
|
12
|
-
import { createHash } from 'crypto';
|
|
12
|
+
import { createHash, randomUUID } from 'crypto';
|
|
13
13
|
import fg from 'fast-glob';
|
|
14
14
|
import ora from 'ora';
|
|
15
|
-
import picomatch from 'picomatch';
|
|
16
15
|
import { execFileSync } from 'child_process';
|
|
16
|
+
import picomatch from 'picomatch';
|
|
17
17
|
import { createInterface } from 'readline';
|
|
18
18
|
|
|
19
19
|
// src/output/writer.ts
|
|
@@ -40,7 +40,7 @@ var ExitCode = {
|
|
|
40
40
|
};
|
|
41
41
|
async function pathExists(filePath) {
|
|
42
42
|
try {
|
|
43
|
-
await
|
|
43
|
+
await fs4.access(filePath);
|
|
44
44
|
return true;
|
|
45
45
|
} catch {
|
|
46
46
|
return false;
|
|
@@ -48,7 +48,7 @@ async function pathExists(filePath) {
|
|
|
48
48
|
}
|
|
49
49
|
function pathExistsSync(filePath) {
|
|
50
50
|
try {
|
|
51
|
-
|
|
51
|
+
fs3.accessSync(filePath);
|
|
52
52
|
return true;
|
|
53
53
|
} catch {
|
|
54
54
|
return false;
|
|
@@ -56,7 +56,7 @@ function pathExistsSync(filePath) {
|
|
|
56
56
|
}
|
|
57
57
|
async function readFileSafe(filePath) {
|
|
58
58
|
try {
|
|
59
|
-
return await
|
|
59
|
+
return await fs4.readFile(filePath, "utf8");
|
|
60
60
|
} catch {
|
|
61
61
|
return void 0;
|
|
62
62
|
}
|
|
@@ -287,14 +287,31 @@ function computeConfidence(stats, targetInCycle) {
|
|
|
287
287
|
if (targetInCycle) confidence *= 0.95;
|
|
288
288
|
return Math.round(Math.min(1, Math.max(0, confidence)) * 100);
|
|
289
289
|
}
|
|
290
|
-
var SOURCE_EXTENSIONS = [
|
|
291
|
-
|
|
290
|
+
var SOURCE_EXTENSIONS = [
|
|
291
|
+
".ts",
|
|
292
|
+
".tsx",
|
|
293
|
+
".js",
|
|
294
|
+
".jsx",
|
|
295
|
+
".mts",
|
|
296
|
+
".cts",
|
|
297
|
+
".mjs",
|
|
298
|
+
".cjs"
|
|
299
|
+
];
|
|
300
|
+
var RESOLVABLE_EXTENSIONS = [...SOURCE_EXTENSIONS, ".d.ts"];
|
|
292
301
|
function toPosix(filePath) {
|
|
293
302
|
return filePath.replace(/\\/g, "/");
|
|
294
303
|
}
|
|
295
304
|
function normalizeAbsolute(filePath, cwd) {
|
|
296
305
|
return path8.resolve(cwd, filePath);
|
|
297
306
|
}
|
|
307
|
+
function realPath(filePath, cwd) {
|
|
308
|
+
const absolute = path8.resolve(cwd, filePath);
|
|
309
|
+
try {
|
|
310
|
+
return fs3.realpathSync.native(absolute);
|
|
311
|
+
} catch {
|
|
312
|
+
return absolute;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
298
315
|
function pathKey(filePath, cwd) {
|
|
299
316
|
const absolute = normalizeAbsolute(filePath, cwd);
|
|
300
317
|
const posix = toPosix(absolute);
|
|
@@ -308,13 +325,22 @@ function sourceFileKind(filePath) {
|
|
|
308
325
|
function extensionCandidates(basePath) {
|
|
309
326
|
return RESOLVABLE_EXTENSIONS.map((ext) => basePath + ext);
|
|
310
327
|
}
|
|
311
|
-
function
|
|
312
|
-
const ext = path8.extname(candidate);
|
|
313
|
-
if (ext === "
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
328
|
+
function jsToTsCandidates(candidate) {
|
|
329
|
+
const ext = path8.extname(candidate).toLowerCase();
|
|
330
|
+
if (ext === "") return [];
|
|
331
|
+
const stem = candidate.slice(0, -ext.length);
|
|
332
|
+
switch (ext) {
|
|
333
|
+
case ".js":
|
|
334
|
+
return [`${stem}.ts`, `${stem}.tsx`];
|
|
335
|
+
case ".jsx":
|
|
336
|
+
return [`${stem}.tsx`];
|
|
337
|
+
case ".mjs":
|
|
338
|
+
return [`${stem}.mts`];
|
|
339
|
+
case ".cjs":
|
|
340
|
+
return [`${stem}.cts`];
|
|
341
|
+
default:
|
|
342
|
+
return [];
|
|
343
|
+
}
|
|
318
344
|
}
|
|
319
345
|
function displayPath(filePath, cwd) {
|
|
320
346
|
const relative = path8.relative(cwd, filePath);
|
|
@@ -833,11 +859,9 @@ function impactLine(result, style) {
|
|
|
833
859
|
if (summary.utilities > 0) parts.push(pluralize(summary.utilities, "utility"));
|
|
834
860
|
return parts.length > 0 ? parts.join(" \xB7 ") : dim("none", style);
|
|
835
861
|
}
|
|
836
|
-
function buildAffectedTree(result, cwd, style) {
|
|
862
|
+
function buildAffectedTree(result, cwd, style, limit) {
|
|
837
863
|
const { affected, graph } = result;
|
|
838
|
-
const sorted = [...affected.entries()].sort(
|
|
839
|
-
([, a], [, b]) => a.depth - b.depth || a.path.localeCompare(b.path)
|
|
840
|
-
);
|
|
864
|
+
const sorted = [...affected.entries()].sort(([, a], [, b]) => a.depth - b.depth || a.path.localeCompare(b.path)).slice(0, limit);
|
|
841
865
|
const nodesByKey = /* @__PURE__ */ new Map();
|
|
842
866
|
const depthByKey = /* @__PURE__ */ new Map();
|
|
843
867
|
for (const [key, file] of sorted) {
|
|
@@ -911,9 +935,9 @@ function renderAnalyzeReport(result, options, writer) {
|
|
|
911
935
|
writer.writeLine();
|
|
912
936
|
if (summary.affectedFiles > 0) {
|
|
913
937
|
writer.writeLine(sectionHeader(`Affected files (${summary.affectedFiles})`, style));
|
|
914
|
-
const tree = buildAffectedTree(result, options.cwd, style);
|
|
915
|
-
writer.writeLine(renderTree(tree));
|
|
916
938
|
const limit = options.listLimit ?? DEFAULT_LIST_LIMIT;
|
|
939
|
+
const tree = buildAffectedTree(result, options.cwd, style, limit);
|
|
940
|
+
writer.writeLine(renderTree(tree));
|
|
917
941
|
if (summary.affectedFiles > limit) {
|
|
918
942
|
writer.writeLine(
|
|
919
943
|
dim(`${summary.affectedFiles - limit} more\u2026 use --json for the full list`, style)
|
|
@@ -945,7 +969,7 @@ function renderAnalyzeReport(result, options, writer) {
|
|
|
945
969
|
}
|
|
946
970
|
}
|
|
947
971
|
function buildAnalyzeJsonReport(result, version, cwd) {
|
|
948
|
-
const
|
|
972
|
+
const realPath2 = (key) => result.graph.nodes.get(key)?.path ?? key;
|
|
949
973
|
const affected = [...result.affected.values()].sort((a, b) => a.depth - b.depth || a.path.localeCompare(b.path)).map((file) => ({ ...file, path: displayPath(file.path, cwd) }));
|
|
950
974
|
return {
|
|
951
975
|
tool: "ripple",
|
|
@@ -956,8 +980,8 @@ function buildAnalyzeJsonReport(result, version, cwd) {
|
|
|
956
980
|
summary: result.summary,
|
|
957
981
|
affected,
|
|
958
982
|
cycles: result.graph.cycles.map((cycle) => ({
|
|
959
|
-
members: cycle.members.map((key) => displayPath(
|
|
960
|
-
path: cycle.path.map((key) => displayPath(
|
|
983
|
+
members: cycle.members.map((key) => displayPath(realPath2(key), cwd)),
|
|
984
|
+
path: cycle.path.map((key) => displayPath(realPath2(key), cwd))
|
|
961
985
|
})),
|
|
962
986
|
targetInCycle: result.targetInCycle,
|
|
963
987
|
durationMs: result.durationMs
|
|
@@ -1002,7 +1026,7 @@ function renderGraphReport(input, options, writer) {
|
|
|
1002
1026
|
}
|
|
1003
1027
|
}
|
|
1004
1028
|
function buildGraphJsonReport(input, version) {
|
|
1005
|
-
const
|
|
1029
|
+
const realPath2 = (key) => input.nodes.get(key)?.path ?? key;
|
|
1006
1030
|
return {
|
|
1007
1031
|
tool: "ripple",
|
|
1008
1032
|
version,
|
|
@@ -1011,8 +1035,8 @@ function buildGraphJsonReport(input, version) {
|
|
|
1011
1035
|
edgeCount: input.stats.internalEdges,
|
|
1012
1036
|
externalEdgeCount: input.stats.externalEdges,
|
|
1013
1037
|
cycles: input.cycles.map((cycle) => ({
|
|
1014
|
-
members: cycle.members.map((key) => displayPath(
|
|
1015
|
-
path: cycle.path.map((key) => displayPath(
|
|
1038
|
+
members: cycle.members.map((key) => displayPath(realPath2(key), input.cwd)),
|
|
1039
|
+
path: cycle.path.map((key) => displayPath(realPath2(key), input.cwd))
|
|
1016
1040
|
})),
|
|
1017
1041
|
...input.forward ? { forward: input.forward } : {},
|
|
1018
1042
|
...input.reverse ? { reverse: input.reverse } : {}
|
|
@@ -1038,8 +1062,8 @@ function riskToSarifLevel(level) {
|
|
|
1038
1062
|
return "note";
|
|
1039
1063
|
}
|
|
1040
1064
|
}
|
|
1041
|
-
function primaryLocationHash(ruleId, uri
|
|
1042
|
-
return createHash("sha256").update(`${ruleId}|${uri}
|
|
1065
|
+
function primaryLocationHash(ruleId, uri) {
|
|
1066
|
+
return createHash("sha256").update(`${ruleId}|${uri}`).digest("hex");
|
|
1043
1067
|
}
|
|
1044
1068
|
var RISK_RULE = {
|
|
1045
1069
|
id: "ripple/risk",
|
|
@@ -1055,6 +1079,7 @@ var RISK_RULE = {
|
|
|
1055
1079
|
};
|
|
1056
1080
|
function fileResult(entry, gate) {
|
|
1057
1081
|
const { rel, result, allowed } = entry;
|
|
1082
|
+
const uri = entry.repoRel ?? rel;
|
|
1058
1083
|
const level = allowed ? "note" : riskToSarifLevel(result.risk.level);
|
|
1059
1084
|
const blocked = LEVEL_INDEX[result.risk.level] >= GATE_INDEX[gate];
|
|
1060
1085
|
const score = result.risk.score.toFixed(1);
|
|
@@ -1067,13 +1092,13 @@ function fileResult(entry, gate) {
|
|
|
1067
1092
|
locations: [
|
|
1068
1093
|
{
|
|
1069
1094
|
physicalLocation: {
|
|
1070
|
-
artifactLocation: { uri
|
|
1095
|
+
artifactLocation: { uri },
|
|
1071
1096
|
region: { startLine: 1 }
|
|
1072
1097
|
}
|
|
1073
1098
|
}
|
|
1074
1099
|
],
|
|
1075
1100
|
partialFingerprints: {
|
|
1076
|
-
primaryLocationLineHash: primaryLocationHash("ripple/risk",
|
|
1101
|
+
primaryLocationLineHash: primaryLocationHash("ripple/risk", uri)
|
|
1077
1102
|
},
|
|
1078
1103
|
properties: {
|
|
1079
1104
|
ripple: {
|
|
@@ -1160,7 +1185,7 @@ function buildAnalyzeSarif(options) {
|
|
|
1160
1185
|
}
|
|
1161
1186
|
],
|
|
1162
1187
|
partialFingerprints: {
|
|
1163
|
-
primaryLocationLineHash: primaryLocationHash("ripple/risk", uri
|
|
1188
|
+
primaryLocationLineHash: primaryLocationHash("ripple/risk", uri)
|
|
1164
1189
|
},
|
|
1165
1190
|
properties: {
|
|
1166
1191
|
ripple: {
|
|
@@ -1343,41 +1368,80 @@ function extractSymbols(sourceFile) {
|
|
|
1343
1368
|
}
|
|
1344
1369
|
|
|
1345
1370
|
// src/parser/parse.ts
|
|
1346
|
-
|
|
1371
|
+
var MAX_REPORTED_DIAGNOSTICS = 3;
|
|
1372
|
+
function diagnosticText(diagnostic) {
|
|
1373
|
+
const message = diagnostic.getMessageText();
|
|
1374
|
+
return typeof message === "string" ? message : message.getMessageText();
|
|
1375
|
+
}
|
|
1376
|
+
function formatDiagnostics(diagnostics) {
|
|
1377
|
+
const parts = diagnostics.slice(0, MAX_REPORTED_DIAGNOSTICS).map((diagnostic) => {
|
|
1378
|
+
const line = diagnostic.getLineNumber();
|
|
1379
|
+
return `${line !== void 0 ? `line ${line}: ` : ""}${diagnosticText(diagnostic)}`;
|
|
1380
|
+
});
|
|
1381
|
+
const suffix = diagnostics.length > MAX_REPORTED_DIAGNOSTICS ? ` (+${diagnostics.length - MAX_REPORTED_DIAGNOSTICS} more)` : "";
|
|
1382
|
+
return `${parts.join("; ")}${suffix}`;
|
|
1383
|
+
}
|
|
1384
|
+
function collectSyntacticErrors(project, filePaths) {
|
|
1385
|
+
const errors = /* @__PURE__ */ new Map();
|
|
1386
|
+
if (filePaths.length === 0) return errors;
|
|
1387
|
+
const targets = new Set(filePaths.map((filePath) => pathKey(filePath, process.cwd())));
|
|
1388
|
+
let diagnostics;
|
|
1389
|
+
try {
|
|
1390
|
+
diagnostics = project.getProgram().getSyntacticDiagnostics();
|
|
1391
|
+
} catch {
|
|
1392
|
+
return errors;
|
|
1393
|
+
}
|
|
1394
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
1395
|
+
for (const diagnostic of diagnostics) {
|
|
1396
|
+
const key = pathKey(diagnostic.getSourceFile().getFilePath(), process.cwd());
|
|
1397
|
+
if (!targets.has(key)) continue;
|
|
1398
|
+
const list = byFile.get(key) ?? [];
|
|
1399
|
+
list.push(diagnostic);
|
|
1400
|
+
byFile.set(key, list);
|
|
1401
|
+
}
|
|
1402
|
+
for (const [key, list] of byFile) {
|
|
1403
|
+
errors.set(key, formatDiagnostics(list));
|
|
1404
|
+
}
|
|
1405
|
+
return errors;
|
|
1406
|
+
}
|
|
1407
|
+
function emptySurface(filePath, kind, parseError) {
|
|
1408
|
+
return {
|
|
1409
|
+
path: filePath,
|
|
1410
|
+
kind,
|
|
1411
|
+
imports: [],
|
|
1412
|
+
exports: { named: [], hasDefault: false, reExportedFrom: [], reExportedAll: [] },
|
|
1413
|
+
symbols: { functions: [], classes: [], interfaces: [], enums: [], typeAliases: [] },
|
|
1414
|
+
parseError
|
|
1415
|
+
};
|
|
1416
|
+
}
|
|
1417
|
+
function parseSurface(project, filePath) {
|
|
1418
|
+
const kind = sourceFileKind(filePath) ?? "ts";
|
|
1347
1419
|
let sourceFile;
|
|
1348
1420
|
try {
|
|
1349
1421
|
sourceFile = project.addSourceFileAtPath(filePath);
|
|
1350
1422
|
} catch (error) {
|
|
1351
|
-
return
|
|
1352
|
-
path: filePath,
|
|
1353
|
-
kind: sourceFileKind(filePath) ?? "ts",
|
|
1354
|
-
imports: [],
|
|
1355
|
-
exports: { named: [], hasDefault: false, reExportedFrom: [], reExportedAll: [] },
|
|
1356
|
-
symbols: { functions: [], classes: [], interfaces: [], enums: [], typeAliases: [] },
|
|
1357
|
-
parseError: error instanceof Error ? error.message : String(error)
|
|
1358
|
-
};
|
|
1423
|
+
return emptySurface(filePath, kind, error instanceof Error ? error.message : String(error));
|
|
1359
1424
|
}
|
|
1360
1425
|
try {
|
|
1361
1426
|
return {
|
|
1362
1427
|
path: filePath,
|
|
1363
|
-
kind
|
|
1428
|
+
kind,
|
|
1364
1429
|
imports: extractImports(sourceFile),
|
|
1365
1430
|
exports: extractExports(sourceFile),
|
|
1366
1431
|
symbols: extractSymbols(sourceFile)
|
|
1367
1432
|
};
|
|
1368
1433
|
} catch (error) {
|
|
1369
|
-
return
|
|
1370
|
-
path: filePath,
|
|
1371
|
-
kind: sourceFileKind(filePath) ?? "ts",
|
|
1372
|
-
imports: [],
|
|
1373
|
-
exports: { named: [], hasDefault: false, reExportedFrom: [], reExportedAll: [] },
|
|
1374
|
-
symbols: { functions: [], classes: [], interfaces: [], enums: [], typeAliases: [] },
|
|
1375
|
-
parseError: error instanceof Error ? error.message : String(error)
|
|
1376
|
-
};
|
|
1434
|
+
return emptySurface(filePath, kind, error instanceof Error ? error.message : String(error));
|
|
1377
1435
|
}
|
|
1378
1436
|
}
|
|
1379
1437
|
function parseMany(project, filePaths) {
|
|
1380
|
-
|
|
1438
|
+
const parsed = filePaths.map((filePath) => parseSurface(project, filePath));
|
|
1439
|
+
const syntaxErrors = collectSyntacticErrors(project, filePaths);
|
|
1440
|
+
return parsed.map((parsedFile) => {
|
|
1441
|
+
if (parsedFile.parseError !== void 0) return parsedFile;
|
|
1442
|
+
const error = syntaxErrors.get(pathKey(parsedFile.path, process.cwd()));
|
|
1443
|
+
return error === void 0 ? parsedFile : emptySurface(parsedFile.path, parsedFile.kind, error);
|
|
1444
|
+
});
|
|
1381
1445
|
}
|
|
1382
1446
|
|
|
1383
1447
|
// src/graph/cycles.ts
|
|
@@ -1478,16 +1542,15 @@ function probeCandidates(base, fileKeys, rootDir) {
|
|
|
1478
1542
|
const key = pathKey(candidate, rootDir);
|
|
1479
1543
|
if (fileKeys.has(key)) return path8.normalize(candidate);
|
|
1480
1544
|
}
|
|
1481
|
-
const
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
if (fileKeys.has(swappedKey)) return path8.normalize(jsSwapped);
|
|
1545
|
+
for (const swapped of jsToTsCandidates(base)) {
|
|
1546
|
+
const swappedKey = pathKey(swapped, rootDir);
|
|
1547
|
+
if (fileKeys.has(swappedKey)) return path8.normalize(swapped);
|
|
1485
1548
|
}
|
|
1486
1549
|
return void 0;
|
|
1487
1550
|
}
|
|
1488
1551
|
function existsOnDisk(filePath) {
|
|
1489
1552
|
try {
|
|
1490
|
-
|
|
1553
|
+
fs3.accessSync(filePath);
|
|
1491
1554
|
return true;
|
|
1492
1555
|
} catch {
|
|
1493
1556
|
return false;
|
|
@@ -1511,7 +1574,7 @@ function resolveToFile(base, context) {
|
|
|
1511
1574
|
if (fileKeys.has(exactKey)) return path8.normalize(base);
|
|
1512
1575
|
const probed = probeCandidates(base, fileKeys, rootDir);
|
|
1513
1576
|
if (probed) return probed;
|
|
1514
|
-
if (existsOnDisk(base) &&
|
|
1577
|
+
if (existsOnDisk(base) && fs3.statSync(base).isDirectory()) {
|
|
1515
1578
|
const indexProbed = probeCandidates(path8.join(base, "index"), fileKeys, rootDir);
|
|
1516
1579
|
if (indexProbed) return indexProbed;
|
|
1517
1580
|
}
|
|
@@ -1619,15 +1682,22 @@ function buildGraphFromParsed(parsedFiles, context) {
|
|
|
1619
1682
|
mark("cycles");
|
|
1620
1683
|
return graph;
|
|
1621
1684
|
}
|
|
1622
|
-
|
|
1623
|
-
|
|
1685
|
+
|
|
1686
|
+
// package.json
|
|
1687
|
+
var package_default = {
|
|
1688
|
+
version: "0.7.2"};
|
|
1689
|
+
|
|
1690
|
+
// src/cache/parsed.ts
|
|
1691
|
+
var CACHE_SCHEMA = 3;
|
|
1692
|
+
var CACHE_FILE = path8.join(".ripple", "cache", "parsed-v3.json");
|
|
1624
1693
|
var cacheEnabled = () => process.env.RIPPLE_NO_CACHE !== "1";
|
|
1625
1694
|
async function contentHash(filePath) {
|
|
1626
|
-
const bytes = await
|
|
1695
|
+
const bytes = await fs4.readFile(filePath);
|
|
1627
1696
|
return createHash("sha256").update(bytes).digest("hex");
|
|
1628
1697
|
}
|
|
1629
1698
|
function configCacheHash(config) {
|
|
1630
1699
|
const stable = {
|
|
1700
|
+
parser: `${CACHE_SCHEMA}:${package_default.version}`,
|
|
1631
1701
|
include: [...config.include].sort(),
|
|
1632
1702
|
ignore: [...config.ignore].sort()
|
|
1633
1703
|
};
|
|
@@ -1642,7 +1712,7 @@ function cachePath(rootDir) {
|
|
|
1642
1712
|
async function loadParsedCache(rootDir, configHash) {
|
|
1643
1713
|
let raw;
|
|
1644
1714
|
try {
|
|
1645
|
-
raw = await
|
|
1715
|
+
raw = await fs4.readFile(cachePath(rootDir), "utf8");
|
|
1646
1716
|
} catch {
|
|
1647
1717
|
return /* @__PURE__ */ new Map();
|
|
1648
1718
|
}
|
|
@@ -1663,14 +1733,14 @@ async function saveParsedCache(rootDir, configHash, entries) {
|
|
|
1663
1733
|
files: Object.fromEntries(entries)
|
|
1664
1734
|
};
|
|
1665
1735
|
const target = cachePath(rootDir);
|
|
1666
|
-
const tmp = target
|
|
1736
|
+
const tmp = `${target}.${process.pid}.${randomUUID()}.tmp`;
|
|
1667
1737
|
try {
|
|
1668
|
-
await
|
|
1669
|
-
await
|
|
1670
|
-
await
|
|
1738
|
+
await fs4.mkdir(path8.dirname(target), { recursive: true });
|
|
1739
|
+
await fs4.writeFile(tmp, JSON.stringify(manifest), "utf8");
|
|
1740
|
+
await fs4.rename(tmp, target);
|
|
1671
1741
|
} catch {
|
|
1672
1742
|
try {
|
|
1673
|
-
await
|
|
1743
|
+
await fs4.rm(tmp, { force: true });
|
|
1674
1744
|
} catch {
|
|
1675
1745
|
}
|
|
1676
1746
|
}
|
|
@@ -1694,7 +1764,7 @@ async function loadParsedFiles(options) {
|
|
|
1694
1764
|
const states = await Promise.all(
|
|
1695
1765
|
filePaths.map(async (abs) => {
|
|
1696
1766
|
try {
|
|
1697
|
-
const stat = await
|
|
1767
|
+
const stat = await fs4.stat(abs);
|
|
1698
1768
|
return { abs, rel: relPosix(rootDir, abs), size: stat.size, mtimeMs: stat.mtimeMs };
|
|
1699
1769
|
} catch {
|
|
1700
1770
|
return { abs, rel: relPosix(rootDir, abs), size: -1, mtimeMs: -1 };
|
|
@@ -1702,6 +1772,8 @@ async function loadParsedFiles(options) {
|
|
|
1702
1772
|
})
|
|
1703
1773
|
);
|
|
1704
1774
|
stage = traceAt("load-cache", stage);
|
|
1775
|
+
const stateByRel = new Map(states.map((state) => [state.rel, state]));
|
|
1776
|
+
const hashByRel = /* @__PURE__ */ new Map();
|
|
1705
1777
|
const hits = /* @__PURE__ */ new Map();
|
|
1706
1778
|
const stale = [];
|
|
1707
1779
|
const parsed = /* @__PURE__ */ new Map();
|
|
@@ -1718,6 +1790,7 @@ async function loadParsedFiles(options) {
|
|
|
1718
1790
|
hash = await contentHash(state.abs);
|
|
1719
1791
|
} catch {
|
|
1720
1792
|
}
|
|
1793
|
+
hashByRel.set(state.rel, hash);
|
|
1721
1794
|
if (entry !== void 0 && entry.hash === hash) {
|
|
1722
1795
|
if (entry.mtimeMs !== state.mtimeMs || entry.size !== state.size) {
|
|
1723
1796
|
hits.set(state.rel, { ...entry, size: state.size, mtimeMs: state.mtimeMs });
|
|
@@ -1742,17 +1815,13 @@ async function loadParsedFiles(options) {
|
|
|
1742
1815
|
traceAt("freshness+parse", stage);
|
|
1743
1816
|
if (dirty) {
|
|
1744
1817
|
const next = new Map(hits);
|
|
1745
|
-
for (const
|
|
1746
|
-
const rel = relPosix(rootDir,
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
try {
|
|
1751
|
-
hash = await contentHash(parsedFile.path);
|
|
1752
|
-
} catch {
|
|
1753
|
-
}
|
|
1818
|
+
for (const stalePath of stale) {
|
|
1819
|
+
const rel = relPosix(rootDir, stalePath);
|
|
1820
|
+
const parsedFile = parsed.get(rel);
|
|
1821
|
+
if (parsedFile === void 0) continue;
|
|
1822
|
+
const state = stateByRel.get(rel);
|
|
1754
1823
|
next.set(rel, {
|
|
1755
|
-
hash,
|
|
1824
|
+
hash: hashByRel.get(rel) ?? "",
|
|
1756
1825
|
size: state?.size ?? -1,
|
|
1757
1826
|
mtimeMs: state?.mtimeMs ?? -1,
|
|
1758
1827
|
parsed: { ...parsedFile, path: rel }
|
|
@@ -1832,23 +1901,23 @@ async function analyzeCommand(fileArg, options, ctx2) {
|
|
|
1832
1901
|
if (!await pathExists(targetPath)) {
|
|
1833
1902
|
throw new RippleError(`File not found: ${fileArg}`, ExitCode.NotFound);
|
|
1834
1903
|
}
|
|
1835
|
-
const tracker = createStageTracker(!options.json);
|
|
1904
|
+
const tracker = createStageTracker(!options.json && !options.sarif);
|
|
1836
1905
|
tracker.next("Loading config");
|
|
1837
1906
|
const started = Date.now();
|
|
1838
1907
|
const context = await loadProjectContext(ctx2.cwd, options.config);
|
|
1839
1908
|
tracker.next("Building dependency graph");
|
|
1840
|
-
const { graph, entryPoints
|
|
1909
|
+
const { graph, entryPoints } = await runPipeline(context);
|
|
1841
1910
|
tracker.next("Analyzing impact");
|
|
1842
|
-
const completed = Date.now();
|
|
1843
1911
|
const result = analyzeFile({
|
|
1844
1912
|
graph,
|
|
1845
1913
|
context,
|
|
1846
1914
|
entryPoints,
|
|
1847
1915
|
targetKey: requireNode(graph, targetPath, context.rootDir).key,
|
|
1848
1916
|
targetPath,
|
|
1849
|
-
durationMs:
|
|
1917
|
+
durationMs: 0,
|
|
1850
1918
|
...options.depth !== void 0 ? { maxDepth: options.depth } : {}
|
|
1851
1919
|
});
|
|
1920
|
+
result.durationMs = Date.now() - started;
|
|
1852
1921
|
tracker.done();
|
|
1853
1922
|
if (options.json) {
|
|
1854
1923
|
const report = buildAnalyzeJsonReport(result, ctx2.version, ctx2.cwd);
|
|
@@ -2174,7 +2243,11 @@ async function doctorCommand(options, ctx2) {
|
|
|
2174
2243
|
results.push({ label, state, detail });
|
|
2175
2244
|
};
|
|
2176
2245
|
try {
|
|
2177
|
-
check(
|
|
2246
|
+
check(
|
|
2247
|
+
"Node.js runtime",
|
|
2248
|
+
"ok",
|
|
2249
|
+
options.verbose ? `${process.version} \xB7 ${process.platform}/${process.arch}` : process.version
|
|
2250
|
+
);
|
|
2178
2251
|
const packageJson = await findPackageJson(ctx2.cwd);
|
|
2179
2252
|
if (packageJson) {
|
|
2180
2253
|
check("package.json", "ok", path8.relative(ctx2.cwd, packageJson));
|
|
@@ -2238,6 +2311,13 @@ async function doctorCommand(options, ctx2) {
|
|
|
2238
2311
|
} else {
|
|
2239
2312
|
check("circular dependencies", "warn", `${stats.cycles} cycle group(s)`);
|
|
2240
2313
|
}
|
|
2314
|
+
if (options.verbose) {
|
|
2315
|
+
check(
|
|
2316
|
+
"graph",
|
|
2317
|
+
"ok",
|
|
2318
|
+
`${stats.files} files \xB7 ${stats.internalEdges} internal edges \xB7 ${stats.externalEdges} external \xB7 ${stats.unresolvedEdges} unresolved`
|
|
2319
|
+
);
|
|
2320
|
+
}
|
|
2241
2321
|
} finally {
|
|
2242
2322
|
spinner.stop();
|
|
2243
2323
|
}
|
|
@@ -2263,7 +2343,7 @@ function renderResults(results, ctx2, color) {
|
|
|
2263
2343
|
}
|
|
2264
2344
|
ctx2.writer.writeLine();
|
|
2265
2345
|
}
|
|
2266
|
-
var SCHEMA_REF = "
|
|
2346
|
+
var SCHEMA_REF = "https://raw.githubusercontent.com/alimaandev/ripple/main/ripple.schema.json";
|
|
2267
2347
|
function jsonPayload(full) {
|
|
2268
2348
|
const payload = full ? { ...DEFAULT_CONFIG } : { include: DEFAULT_CONFIG.include };
|
|
2269
2349
|
return { ...payload, $schema: SCHEMA_REF };
|
|
@@ -2315,7 +2395,7 @@ async function initCommand(options, ctx2) {
|
|
|
2315
2395
|
const fileName = options.ts ? "ripple.config.ts" : "ripple.config.json";
|
|
2316
2396
|
const target = path8.join(ctx2.cwd, fileName);
|
|
2317
2397
|
try {
|
|
2318
|
-
await
|
|
2398
|
+
await fs4.access(target);
|
|
2319
2399
|
if (!options.force) {
|
|
2320
2400
|
const mark = style.color ? chalk.red(icon("cross")) : icon("cross");
|
|
2321
2401
|
ctx2.writer.writeLine(`${mark} ${fileName} already exists. Pass --force to overwrite it.`);
|
|
@@ -2326,7 +2406,7 @@ async function initCommand(options, ctx2) {
|
|
|
2326
2406
|
const content = options.ts ? tsPayload(Boolean(options.full)) : `${JSON.stringify(jsonPayload(Boolean(options.full)), null, 2)}
|
|
2327
2407
|
`;
|
|
2328
2408
|
try {
|
|
2329
|
-
await
|
|
2409
|
+
await fs4.writeFile(target, content, "utf8");
|
|
2330
2410
|
const mark = style.color ? chalk.green(icon("tick")) : icon("tick");
|
|
2331
2411
|
ctx2.writer.writeLine(`${mark} Wrote ${fileName}`);
|
|
2332
2412
|
ctx2.writer.writeLine(
|
|
@@ -2341,22 +2421,54 @@ async function initCommand(options, ctx2) {
|
|
|
2341
2421
|
return ExitCode.Failure;
|
|
2342
2422
|
}
|
|
2343
2423
|
}
|
|
2344
|
-
var DEFAULT_BASES = ["origin/main", "main", "
|
|
2345
|
-
function
|
|
2424
|
+
var DEFAULT_BASES = ["origin/HEAD", "origin/main", "origin/master", "main", "master"];
|
|
2425
|
+
function gitRaw(args, cwd) {
|
|
2346
2426
|
return execFileSync("git", args, {
|
|
2347
2427
|
cwd,
|
|
2348
2428
|
encoding: "utf8",
|
|
2349
2429
|
stdio: ["ignore", "pipe", "pipe"]
|
|
2350
|
-
})
|
|
2430
|
+
});
|
|
2431
|
+
}
|
|
2432
|
+
function chomp(text) {
|
|
2433
|
+
return text.replace(/\r?\n$/, "");
|
|
2434
|
+
}
|
|
2435
|
+
function git(args, cwd) {
|
|
2436
|
+
return gitRaw(args, cwd).trim();
|
|
2437
|
+
}
|
|
2438
|
+
function repoRootFor(cwd) {
|
|
2439
|
+
try {
|
|
2440
|
+
const root = chomp(gitRaw(["rev-parse", "--show-toplevel"], cwd));
|
|
2441
|
+
if (root) return realPath(root, cwd);
|
|
2442
|
+
} catch {
|
|
2443
|
+
}
|
|
2444
|
+
throw new RippleError(
|
|
2445
|
+
`Not a git repository: ${cwd}`,
|
|
2446
|
+
ExitCode.NotFound,
|
|
2447
|
+
"ripple diff needs a git repository. Run it from inside one, or pass --base only in a repo."
|
|
2448
|
+
);
|
|
2351
2449
|
}
|
|
2352
2450
|
function refExists(ref, cwd) {
|
|
2353
2451
|
try {
|
|
2354
|
-
const out = git(["rev-parse", "--verify", ref], cwd);
|
|
2452
|
+
const out = git(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`], cwd);
|
|
2355
2453
|
return out.length > 0;
|
|
2356
2454
|
} catch {
|
|
2357
2455
|
return false;
|
|
2358
2456
|
}
|
|
2359
2457
|
}
|
|
2458
|
+
function sameCommit(left, right, cwd) {
|
|
2459
|
+
try {
|
|
2460
|
+
return git(["rev-parse", `${left}^{commit}`], cwd) === git(["rev-parse", `${right}^{commit}`], cwd);
|
|
2461
|
+
} catch {
|
|
2462
|
+
return false;
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
function hasUncommittedChanges(cwd) {
|
|
2466
|
+
try {
|
|
2467
|
+
return git(["status", "--porcelain", "--untracked-files=normal"], cwd).length > 0;
|
|
2468
|
+
} catch {
|
|
2469
|
+
return false;
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2360
2472
|
function resolveBase(cwd, requested) {
|
|
2361
2473
|
if (requested) {
|
|
2362
2474
|
if (!refExists(requested, cwd)) {
|
|
@@ -2364,32 +2476,72 @@ function resolveBase(cwd, requested) {
|
|
|
2364
2476
|
}
|
|
2365
2477
|
return requested;
|
|
2366
2478
|
}
|
|
2367
|
-
|
|
2368
|
-
|
|
2479
|
+
const candidates = [];
|
|
2480
|
+
const githubBase = process.env.GITHUB_BASE_REF?.trim();
|
|
2481
|
+
if (githubBase) {
|
|
2482
|
+
candidates.push(`origin/${githubBase}`, githubBase);
|
|
2483
|
+
}
|
|
2484
|
+
candidates.push(...DEFAULT_BASES);
|
|
2485
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2486
|
+
let headCandidate;
|
|
2487
|
+
for (const candidate of candidates) {
|
|
2488
|
+
if (seen.has(candidate)) continue;
|
|
2489
|
+
seen.add(candidate);
|
|
2490
|
+
if (!refExists(candidate, cwd)) continue;
|
|
2491
|
+
if (sameCommit(candidate, "HEAD", cwd)) {
|
|
2492
|
+
headCandidate ??= candidate;
|
|
2493
|
+
continue;
|
|
2494
|
+
}
|
|
2495
|
+
return candidate;
|
|
2496
|
+
}
|
|
2497
|
+
if (headCandidate !== void 0) {
|
|
2498
|
+
const detail = hasUncommittedChanges(cwd) ? "Only uncommitted changes are analyzed" : "The working tree is clean, so the change set is empty";
|
|
2499
|
+
process.stderr.write(
|
|
2500
|
+
`ripple: warning: base ref "${headCandidate}" resolves to HEAD. ${detail} \u2014 pass --base <ref> to compare against a branch.
|
|
2501
|
+
`
|
|
2502
|
+
);
|
|
2503
|
+
return headCandidate;
|
|
2504
|
+
}
|
|
2505
|
+
if (refExists("HEAD~1", cwd)) {
|
|
2506
|
+
process.stderr.write(
|
|
2507
|
+
"ripple: warning: no branch base found; falling back to HEAD~1. Only the last commit will be analyzed \u2014 pass --base <ref> for a full change set.\n"
|
|
2508
|
+
);
|
|
2509
|
+
return "HEAD~1";
|
|
2369
2510
|
}
|
|
2370
2511
|
throw new RippleError(
|
|
2371
|
-
|
|
2512
|
+
`No git base ref found (tried ${candidates.join(", ")}). Pass --base <ref> explicitly.`,
|
|
2372
2513
|
ExitCode.NotFound
|
|
2373
2514
|
);
|
|
2374
2515
|
}
|
|
2516
|
+
function nulPaths(output) {
|
|
2517
|
+
return output.split("\0").filter((entry) => entry.length > 0);
|
|
2518
|
+
}
|
|
2519
|
+
function isRippleArtifact(repoPath) {
|
|
2520
|
+
return repoPath.replace(/\\/g, "/").split("/").includes(".ripple");
|
|
2521
|
+
}
|
|
2375
2522
|
function changedFiles(cwd, base) {
|
|
2376
|
-
const
|
|
2523
|
+
const repoRoot = repoRootFor(cwd);
|
|
2524
|
+
const baseLabel = resolveBase(repoRoot, base);
|
|
2377
2525
|
let baseCommit = baseLabel;
|
|
2378
2526
|
try {
|
|
2379
|
-
const mergeBase = git(["merge-base", baseLabel, "HEAD"],
|
|
2527
|
+
const mergeBase = git(["merge-base", baseLabel, "HEAD"], repoRoot).split(/\r?\n/)[0]?.trim();
|
|
2380
2528
|
if (mergeBase) baseCommit = mergeBase;
|
|
2381
2529
|
} catch {
|
|
2382
2530
|
}
|
|
2383
2531
|
const files = /* @__PURE__ */ new Set();
|
|
2384
|
-
const tracked =
|
|
2385
|
-
|
|
2386
|
-
|
|
2532
|
+
const tracked = nulPaths(
|
|
2533
|
+
gitRaw(["diff", "--name-only", "-z", "--diff-filter=ACMRT", baseCommit, "--"], repoRoot)
|
|
2534
|
+
);
|
|
2535
|
+
for (const file of tracked) {
|
|
2536
|
+
if (!isRippleArtifact(file)) files.add(file);
|
|
2387
2537
|
}
|
|
2388
|
-
const untracked =
|
|
2389
|
-
|
|
2390
|
-
|
|
2538
|
+
const untracked = nulPaths(
|
|
2539
|
+
gitRaw(["ls-files", "--others", "--exclude-standard", "-z"], repoRoot)
|
|
2540
|
+
);
|
|
2541
|
+
for (const file of untracked) {
|
|
2542
|
+
if (!isRippleArtifact(file)) files.add(file);
|
|
2391
2543
|
}
|
|
2392
|
-
return { baseLabel, files: [...files].sort() };
|
|
2544
|
+
return { baseLabel, repoRoot, files: [...files].sort() };
|
|
2393
2545
|
}
|
|
2394
2546
|
|
|
2395
2547
|
// src/formatter/annotations.ts
|
|
@@ -2430,9 +2582,12 @@ function buildGateAnnotation(blocked, gate, counts) {
|
|
|
2430
2582
|
message: blocked ? `Gate blocked: ${atGate} at or above ${gate}` : `Gate passed: nothing at ${gate} or above`
|
|
2431
2583
|
};
|
|
2432
2584
|
}
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2585
|
+
var LEVEL_INDEX3 = {
|
|
2586
|
+
LOW: 0,
|
|
2587
|
+
MEDIUM: 1,
|
|
2588
|
+
HIGH: 2,
|
|
2589
|
+
CRITICAL: 3
|
|
2590
|
+
};
|
|
2436
2591
|
var GATE_INDEX3 = { medium: 1, high: 2, critical: 3 };
|
|
2437
2592
|
var VERDICT_NOTE = {
|
|
2438
2593
|
medium: "no MEDIUM or worse",
|
|
@@ -2462,6 +2617,52 @@ function riskySummary(counts, gate) {
|
|
|
2462
2617
|
if (LEVEL_INDEX3.LOW >= gateIndex && counts.low > 0) parts.push(`${counts.low} LOW`);
|
|
2463
2618
|
return parts.length > 0 ? parts.join(" \xB7 ") : "none";
|
|
2464
2619
|
}
|
|
2620
|
+
function projectRelativePath(absPath, rootDir) {
|
|
2621
|
+
const rel = toPosix(path8.relative(realPath(rootDir, rootDir), realPath(absPath, rootDir)));
|
|
2622
|
+
return rel === "" ? "." : rel;
|
|
2623
|
+
}
|
|
2624
|
+
function evaluateGate(input) {
|
|
2625
|
+
const { context, graph, entryPoints, changed, gate, allowPatterns, maxDepth } = input;
|
|
2626
|
+
const changes = [];
|
|
2627
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2628
|
+
for (const repoPath of changed.files) {
|
|
2629
|
+
const gitPath = path8.resolve(changed.repoRoot, repoPath);
|
|
2630
|
+
const file = projectRelativePath(gitPath, context.rootDir);
|
|
2631
|
+
const absPath = file === "." ? context.rootDir : path8.resolve(context.rootDir, file);
|
|
2632
|
+
const key = pathKey(absPath, context.rootDir);
|
|
2633
|
+
if (seen.has(key)) continue;
|
|
2634
|
+
seen.add(key);
|
|
2635
|
+
if (!graph.nodes.has(key)) {
|
|
2636
|
+
changes.push({ file, repoPath, absPath, allowed: false });
|
|
2637
|
+
continue;
|
|
2638
|
+
}
|
|
2639
|
+
const result = analyzeFile({
|
|
2640
|
+
graph,
|
|
2641
|
+
context,
|
|
2642
|
+
entryPoints,
|
|
2643
|
+
targetKey: key,
|
|
2644
|
+
targetPath: absPath,
|
|
2645
|
+
durationMs: 0,
|
|
2646
|
+
...maxDepth !== void 0 ? { maxDepth } : {}
|
|
2647
|
+
});
|
|
2648
|
+
changes.push({
|
|
2649
|
+
file,
|
|
2650
|
+
repoPath,
|
|
2651
|
+
absPath,
|
|
2652
|
+
result,
|
|
2653
|
+
allowed: isAllowedByDiffAllowlist(file, allowPatterns)
|
|
2654
|
+
});
|
|
2655
|
+
}
|
|
2656
|
+
const entries = changes.filter((change) => change.result !== void 0).sort((a, b) => b.result.risk.score - a.result.risk.score || a.file.localeCompare(b.file));
|
|
2657
|
+
const skipped = changes.filter((change) => change.result === void 0);
|
|
2658
|
+
const gateResults = entries.filter((entry) => !entry.allowed).map((entry) => entry.result);
|
|
2659
|
+
const counts = countLevels(gateResults);
|
|
2660
|
+
const allowedCount = entries.filter((entry) => entry.allowed).length;
|
|
2661
|
+
const blocked = gateResults.some((result) => LEVEL_INDEX3[result.risk.level] >= GATE_INDEX3[gate]);
|
|
2662
|
+
return { changes, entries, skipped, counts, allowedCount, blocked, gate };
|
|
2663
|
+
}
|
|
2664
|
+
|
|
2665
|
+
// src/commands/diff.ts
|
|
2465
2666
|
async function diffCommand(options, ctx2) {
|
|
2466
2667
|
const format = options.format ?? (options.json ? "json" : "terminal");
|
|
2467
2668
|
const style = { color: resolveColor(options.color) };
|
|
@@ -2474,46 +2675,30 @@ async function diffCommand(options, ctx2) {
|
|
|
2474
2675
|
const changed = changedFiles(ctx2.cwd, base);
|
|
2475
2676
|
tracker.next("Analyzing changed files");
|
|
2476
2677
|
const { graph, entryPoints } = await runPipeline(context);
|
|
2477
|
-
const byRel = /* @__PURE__ */ new Map();
|
|
2478
|
-
const skipped = [];
|
|
2479
|
-
for (const rel of changed.files) {
|
|
2480
|
-
const abs = path8.resolve(ctx2.cwd, rel);
|
|
2481
|
-
const key = pathKey(abs, context.rootDir);
|
|
2482
|
-
if (!graph.nodes.has(key)) {
|
|
2483
|
-
skipped.push(rel);
|
|
2484
|
-
continue;
|
|
2485
|
-
}
|
|
2486
|
-
byRel.set(
|
|
2487
|
-
rel,
|
|
2488
|
-
analyzeFile({
|
|
2489
|
-
graph,
|
|
2490
|
-
context,
|
|
2491
|
-
entryPoints,
|
|
2492
|
-
targetKey: key,
|
|
2493
|
-
targetPath: abs,
|
|
2494
|
-
durationMs: 0,
|
|
2495
|
-
...options.depth !== void 0 ? { maxDepth: options.depth } : {}
|
|
2496
|
-
})
|
|
2497
|
-
);
|
|
2498
|
-
}
|
|
2499
|
-
tracker.done();
|
|
2500
2678
|
const allowPatterns = [...options.allow ?? [], ...context.config.diff.allow ?? []];
|
|
2501
|
-
const
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2679
|
+
const evaluation = evaluateGate({
|
|
2680
|
+
context,
|
|
2681
|
+
graph,
|
|
2682
|
+
entryPoints,
|
|
2683
|
+
changed,
|
|
2684
|
+
gate,
|
|
2685
|
+
allowPatterns,
|
|
2686
|
+
...options.depth !== void 0 ? { maxDepth: options.depth } : {}
|
|
2687
|
+
});
|
|
2688
|
+
tracker.done();
|
|
2689
|
+
const { changes, entries, skipped, counts, allowedCount, blocked } = evaluation;
|
|
2690
|
+
const byRel = new Map(entries.map((entry) => [entry.file, entry.result]));
|
|
2691
|
+
const allowedByRel = new Map(
|
|
2692
|
+
entries.map((entry) => [entry.file, entry.allowed])
|
|
2693
|
+
);
|
|
2694
|
+
const changedNames = changes.map((change) => change.file);
|
|
2510
2695
|
const durationMs = Date.now() - started;
|
|
2511
2696
|
if (format === "json") {
|
|
2512
2697
|
ctx2.writer.write(
|
|
2513
2698
|
serializeJson(
|
|
2514
2699
|
buildDiffJsonReport(
|
|
2515
2700
|
changed.baseLabel,
|
|
2516
|
-
|
|
2701
|
+
changedNames,
|
|
2517
2702
|
byRel,
|
|
2518
2703
|
allowedByRel,
|
|
2519
2704
|
gate,
|
|
@@ -2532,7 +2717,12 @@ async function diffCommand(options, ctx2) {
|
|
|
2532
2717
|
serializeJson(
|
|
2533
2718
|
buildDiffSarif({
|
|
2534
2719
|
baseLabel: changed.baseLabel,
|
|
2535
|
-
entries
|
|
2720
|
+
entries: entries.map((entry) => ({
|
|
2721
|
+
rel: entry.file,
|
|
2722
|
+
repoRel: entry.repoPath,
|
|
2723
|
+
result: entry.result,
|
|
2724
|
+
allowed: entry.allowed
|
|
2725
|
+
})),
|
|
2536
2726
|
gate,
|
|
2537
2727
|
counts,
|
|
2538
2728
|
blocked,
|
|
@@ -2546,7 +2736,7 @@ async function diffCommand(options, ctx2) {
|
|
|
2546
2736
|
if (format === "github") {
|
|
2547
2737
|
const annotations = [
|
|
2548
2738
|
...buildFileAnnotations(
|
|
2549
|
-
|
|
2739
|
+
entries.filter((entry) => !entry.allowed).map((entry) => ({ file: entry.repoPath, result: entry.result })),
|
|
2550
2740
|
gate
|
|
2551
2741
|
),
|
|
2552
2742
|
buildGateAnnotation(blocked, gate, counts)
|
|
@@ -2559,15 +2749,20 @@ async function diffCommand(options, ctx2) {
|
|
|
2559
2749
|
{
|
|
2560
2750
|
cwd: ctx2.cwd,
|
|
2561
2751
|
baseLabel: changed.baseLabel,
|
|
2562
|
-
files:
|
|
2563
|
-
entries
|
|
2564
|
-
|
|
2752
|
+
files: changedNames,
|
|
2753
|
+
entries: entries.map((entry) => ({
|
|
2754
|
+
rel: entry.file,
|
|
2755
|
+
result: entry.result,
|
|
2756
|
+
allowed: entry.allowed
|
|
2757
|
+
})),
|
|
2758
|
+
skipped: skipped.map((change) => change.file),
|
|
2565
2759
|
counts,
|
|
2566
2760
|
allowedCount,
|
|
2567
2761
|
gate,
|
|
2568
2762
|
blocked,
|
|
2569
2763
|
durationMs,
|
|
2570
|
-
style
|
|
2764
|
+
style,
|
|
2765
|
+
verbose: options.verbose
|
|
2571
2766
|
},
|
|
2572
2767
|
ctx2.writer
|
|
2573
2768
|
);
|
|
@@ -2585,7 +2780,8 @@ function renderDiffReport(input, writer) {
|
|
|
2585
2780
|
gate,
|
|
2586
2781
|
blocked,
|
|
2587
2782
|
durationMs,
|
|
2588
|
-
style
|
|
2783
|
+
style,
|
|
2784
|
+
verbose = false
|
|
2589
2785
|
} = input;
|
|
2590
2786
|
writer.writeLine(brandHeader({ label: `diff vs ${baseLabel}`, style }));
|
|
2591
2787
|
writer.writeLine();
|
|
@@ -2621,6 +2817,14 @@ function renderDiffReport(input, writer) {
|
|
|
2621
2817
|
writer.writeLine(
|
|
2622
2818
|
`${fileIconText} ${file} ${riskBadge(result.risk.level, style)} \xB7 ${score}/100${gauge ? ` ${gauge}` : ""}${allowedTag}`
|
|
2623
2819
|
);
|
|
2820
|
+
if (verbose) {
|
|
2821
|
+
writer.writeLine(
|
|
2822
|
+
` ${dim(
|
|
2823
|
+
`${pluralize(result.summary.affectedFiles, "file")} affected \xB7 max depth ${result.summary.maxDepth} \xB7 confidence ${result.summary.confidence}%`,
|
|
2824
|
+
style
|
|
2825
|
+
)}`
|
|
2826
|
+
);
|
|
2827
|
+
}
|
|
2624
2828
|
}
|
|
2625
2829
|
for (const rel of skipped) {
|
|
2626
2830
|
writer.writeLine(
|
|
@@ -2843,8 +3047,8 @@ function analyzeIn(project, cwd, file, maxDepth) {
|
|
|
2843
3047
|
});
|
|
2844
3048
|
}
|
|
2845
3049
|
function relPath(targetPath, cwd) {
|
|
2846
|
-
const rel =
|
|
2847
|
-
return (rel ||
|
|
3050
|
+
const rel = path8.relative(cwd, targetPath);
|
|
3051
|
+
return toPosix(rel || path8.basename(targetPath));
|
|
2848
3052
|
}
|
|
2849
3053
|
var IMPACT_SCHEMA = {
|
|
2850
3054
|
type: "object",
|
|
@@ -2891,7 +3095,7 @@ var GATE_SCHEMA = {
|
|
|
2891
3095
|
properties: {
|
|
2892
3096
|
base: {
|
|
2893
3097
|
type: "string",
|
|
2894
|
-
description: "Git ref to diff against (default: origin/main, main,
|
|
3098
|
+
description: "Git ref to diff against (default: GitHub base, origin/main, origin/master, main, master)."
|
|
2895
3099
|
},
|
|
2896
3100
|
gate: {
|
|
2897
3101
|
type: "string",
|
|
@@ -2900,7 +3104,6 @@ var GATE_SCHEMA = {
|
|
|
2900
3104
|
}
|
|
2901
3105
|
}
|
|
2902
3106
|
};
|
|
2903
|
-
var LEVEL_INDEX4 = { LOW: 1, MEDIUM: 2, HIGH: 3, CRITICAL: 4 };
|
|
2904
3107
|
function createMcpTools(deps) {
|
|
2905
3108
|
const { cwd, loadProject, getChanged } = deps;
|
|
2906
3109
|
return [
|
|
@@ -2940,7 +3143,7 @@ function createMcpTools(deps) {
|
|
|
2940
3143
|
const file = requireString(args, "file", "dependents");
|
|
2941
3144
|
const depth = optionalPositiveInt(args, "depth", "dependents") ?? 1;
|
|
2942
3145
|
const project = await loadProject();
|
|
2943
|
-
const result = analyzeIn(project, cwd, file);
|
|
3146
|
+
const result = analyzeIn(project, cwd, file, depth);
|
|
2944
3147
|
const dependents = [...result.affected.values()].filter((affected) => affected.depth <= depth).sort((a, b) => a.depth - b.depth || a.path.localeCompare(b.path)).map((affected) => ({
|
|
2945
3148
|
path: relPath(affected.path, cwd),
|
|
2946
3149
|
depth: affected.depth,
|
|
@@ -3014,39 +3217,42 @@ function createMcpTools(deps) {
|
|
|
3014
3217
|
)
|
|
3015
3218
|
);
|
|
3016
3219
|
}
|
|
3017
|
-
const changed = getChanged(base);
|
|
3018
3220
|
const project = await loadProject();
|
|
3019
3221
|
const resolvedGate = gate ?? project.context.config.diff.gate ?? "high";
|
|
3020
|
-
const
|
|
3021
|
-
const
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3033
|
-
|
|
3034
|
-
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
}
|
|
3222
|
+
const changed = getChanged(base ?? project.context.config.diff.base);
|
|
3223
|
+
const evaluation = evaluateGate({
|
|
3224
|
+
context: project.context,
|
|
3225
|
+
graph: project.graph,
|
|
3226
|
+
entryPoints: project.entryPoints,
|
|
3227
|
+
changed,
|
|
3228
|
+
gate: resolvedGate,
|
|
3229
|
+
allowPatterns: project.context.config.diff.allow ?? []
|
|
3230
|
+
});
|
|
3231
|
+
const files = evaluation.changes.map((change) => {
|
|
3232
|
+
if (!change.result) return { file: change.file, analyzed: false };
|
|
3233
|
+
return {
|
|
3234
|
+
file: change.file,
|
|
3235
|
+
analyzed: true,
|
|
3236
|
+
level: change.result.risk.level,
|
|
3237
|
+
score: change.result.risk.score,
|
|
3238
|
+
affectedFiles: change.result.summary.affectedFiles,
|
|
3239
|
+
targetInCycle: change.result.targetInCycle,
|
|
3240
|
+
allowed: change.allowed
|
|
3241
|
+
};
|
|
3242
|
+
});
|
|
3042
3243
|
return ok(
|
|
3043
3244
|
JSON.stringify(
|
|
3044
3245
|
{
|
|
3045
3246
|
base: changed.baseLabel,
|
|
3046
|
-
changedFiles:
|
|
3247
|
+
changedFiles: evaluation.changes.length,
|
|
3047
3248
|
files,
|
|
3048
|
-
counts,
|
|
3049
|
-
gate: {
|
|
3249
|
+
counts: evaluation.counts,
|
|
3250
|
+
gate: {
|
|
3251
|
+
level: resolvedGate,
|
|
3252
|
+
blocked: evaluation.blocked,
|
|
3253
|
+
verdict: evaluation.blocked ? "block" : "pass",
|
|
3254
|
+
allowed: evaluation.allowedCount
|
|
3255
|
+
}
|
|
3050
3256
|
},
|
|
3051
3257
|
null,
|
|
3052
3258
|
2
|
|
@@ -3062,14 +3268,10 @@ function createMcpTools(deps) {
|
|
|
3062
3268
|
|
|
3063
3269
|
// src/commands/mcp.ts
|
|
3064
3270
|
async function mcpCommand(options, ctx2) {
|
|
3065
|
-
let snapshot = null;
|
|
3066
3271
|
const loadProject = async () => {
|
|
3067
|
-
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
snapshot = { context, graph, entryPoints };
|
|
3071
|
-
}
|
|
3072
|
-
return snapshot;
|
|
3272
|
+
const context = await loadProjectContext(ctx2.cwd, options.config);
|
|
3273
|
+
const { graph, entryPoints } = await runPipeline(context);
|
|
3274
|
+
return { context, graph, entryPoints };
|
|
3073
3275
|
};
|
|
3074
3276
|
const server = new McpServer({
|
|
3075
3277
|
name: "ripple",
|
|
@@ -3163,7 +3365,10 @@ function createProgram(ctx2) {
|
|
|
3163
3365
|
"-f, --format <format>",
|
|
3164
3366
|
"output format: terminal | json | github | sarif",
|
|
3165
3367
|
parseDiffFormat
|
|
3166
|
-
).option("-v, --verbose", "include extra sections").option(
|
|
3368
|
+
).option("-v, --verbose", "include extra sections").option(
|
|
3369
|
+
"-b, --base <ref>",
|
|
3370
|
+
"git ref to diff against (default: GitHub base, origin/HEAD, origin/main, origin/master, main, master)"
|
|
3371
|
+
).option(
|
|
3167
3372
|
"-g, --gate <level>",
|
|
3168
3373
|
"risk level that blocks the gate (medium | high | critical)",
|
|
3169
3374
|
parseGateLevel
|
|
@@ -3317,10 +3522,6 @@ async function run({ argv, ctx: ctx2 }) {
|
|
|
3317
3522
|
}
|
|
3318
3523
|
}
|
|
3319
3524
|
|
|
3320
|
-
// package.json
|
|
3321
|
-
var package_default = {
|
|
3322
|
-
version: "0.7.0"};
|
|
3323
|
-
|
|
3324
3525
|
// src/cli/bin.ts
|
|
3325
3526
|
var ctx = {
|
|
3326
3527
|
writer: new TerminalWriter(),
|