@alimaandev/ripple 0.7.0 → 0.7.1

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/dist/bin.js CHANGED
@@ -1,19 +1,19 @@
1
1
  #!/usr/bin/env node
2
- import { rippleConfigSchema, DEFAULT_CONFIG, formatSchemaIssues } from './chunk-CFH4UW2E.js';
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 fs3 from 'fs/promises';
7
- import fs2 from 'fs';
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 fs3.access(filePath);
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
- fs2.accessSync(filePath);
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 fs3.readFile(filePath, "utf8");
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 = [".ts", ".tsx", ".js", ".jsx"];
291
- var RESOLVABLE_EXTENSIONS = [...SOURCE_EXTENSIONS, ".mjs", ".cjs", ".d.ts"];
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 jsToTsCandidate(candidate) {
312
- const ext = path8.extname(candidate);
313
- if (ext === ".js") return candidate.slice(0, -3) + ".ts";
314
- if (ext === ".jsx") return candidate.slice(0, -4) + ".tsx";
315
- if (ext === ".mjs") return candidate.slice(0, -4) + ".mts";
316
- if (ext === ".cjs") return candidate.slice(0, -4) + ".cts";
317
- return candidate;
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 realPath = (key) => result.graph.nodes.get(key)?.path ?? key;
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(realPath(key), cwd)),
960
- path: cycle.path.map((key) => displayPath(realPath(key), cwd))
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 realPath = (key) => input.nodes.get(key)?.path ?? key;
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(realPath(key), input.cwd)),
1015
- path: cycle.path.map((key) => displayPath(realPath(key), input.cwd))
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, score) {
1042
- return createHash("sha256").update(`${ruleId}|${uri}|${score.toFixed(1)}`).digest("hex");
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: rel },
1095
+ artifactLocation: { uri },
1071
1096
  region: { startLine: 1 }
1072
1097
  }
1073
1098
  }
1074
1099
  ],
1075
1100
  partialFingerprints: {
1076
- primaryLocationLineHash: primaryLocationHash("ripple/risk", rel, result.risk.score)
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, result.risk.score)
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
- function parseSourceFile(project, filePath) {
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: sourceFileKind(filePath) ?? "ts",
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
- return filePaths.map((filePath) => parseSourceFile(project, filePath));
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 jsSwapped = jsToTsCandidate(base);
1482
- if (jsSwapped !== base) {
1483
- const swappedKey = pathKey(jsSwapped, rootDir);
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
- fs2.accessSync(filePath);
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) && fs2.statSync(base).isDirectory()) {
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
- var CACHE_SCHEMA = 2;
1623
- var CACHE_FILE = path8.join(".ripple", "cache", "parsed-v2.json");
1685
+
1686
+ // package.json
1687
+ var package_default = {
1688
+ version: "0.7.1"};
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 fs3.readFile(filePath);
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 fs3.readFile(cachePath(rootDir), "utf8");
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 + ".tmp";
1736
+ const tmp = `${target}.${process.pid}.${randomUUID()}.tmp`;
1667
1737
  try {
1668
- await fs3.mkdir(path8.dirname(target), { recursive: true });
1669
- await fs3.writeFile(tmp, JSON.stringify(manifest), "utf8");
1670
- await fs3.rename(tmp, target);
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 fs3.rm(tmp, { force: true });
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 fs3.stat(abs);
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 parsedFile of parsed.values()) {
1746
- const rel = relPosix(rootDir, parsedFile.path);
1747
- if (next.has(rel)) continue;
1748
- const state = states.find((candidate) => candidate.rel === rel);
1749
- let hash = "";
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, durationMs: pipelineMs } = await runPipeline(context);
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: pipelineMs + (completed - started),
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("Node.js runtime", "ok", process.version);
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 = "./node_modules/@alimaandev/ripple/ripple.schema.json";
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 fs3.access(target);
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 fs3.writeFile(target, content, "utf8");
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", "HEAD~1"];
2345
- function git(args, cwd) {
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
- }).trim();
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
- for (const candidate of DEFAULT_BASES) {
2368
- if (refExists(candidate, cwd)) return candidate;
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
- "No git base ref found (tried origin/main, main, HEAD~1). Pass --base <ref> explicitly.",
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 baseLabel = resolveBase(cwd, base);
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"], cwd);
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 = git(["diff", "--name-only", "--diff-filter=ACMRT", baseCommit], cwd);
2385
- for (const line of tracked.split("\n")) {
2386
- if (line.trim()) files.add(line.trim());
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 = git(["ls-files", "--others", "--exclude-standard"], cwd);
2389
- for (const line of untracked.split("\n")) {
2390
- if (line.trim()) files.add(line.trim());
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
- // src/commands/diff.ts
2435
- var LEVEL_INDEX3 = { LOW: 0, MEDIUM: 1, HIGH: 2, CRITICAL: 3 };
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 allowedByRel = /* @__PURE__ */ new Map();
2502
- for (const rel of byRel.keys()) {
2503
- allowedByRel.set(rel, isAllowedByDiffAllowlist(rel, allowPatterns));
2504
- }
2505
- const entries = [...byRel.entries()].map(([rel, result]) => ({ rel, result, allowed: allowedByRel.get(rel) ?? false })).sort((a, b) => b.result.risk.score - a.result.risk.score);
2506
- const gateResults = entries.filter((entry) => !entry.allowed).map((entry) => entry.result);
2507
- const counts = countLevels(gateResults);
2508
- const allowedCount = entries.filter((entry) => entry.allowed).length;
2509
- const blocked = gateResults.some((result) => LEVEL_INDEX3[result.risk.level] >= GATE_INDEX3[gate]);
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
- changed.files,
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
- [...byRel.entries()].filter(([rel]) => !(allowedByRel.get(rel) ?? false)).map(([file, result]) => ({ file, result })),
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: changed.files,
2563
- entries,
2564
- skipped,
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 = targetPath.slice(cwd.length).replace(/^[/\\]+/, "");
2847
- return (rel || targetPath).replace(/\\/g, "/");
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, or HEAD~1)."
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 gateIndex = LEVEL_INDEX4[resolvedGate.toUpperCase()] ?? 3;
3021
- const files = [];
3022
- const counts = { low: 0, medium: 0, high: 0, critical: 0 };
3023
- let blocked = false;
3024
- for (const rel of changed.files) {
3025
- try {
3026
- const result = analyzeIn(project, cwd, rel);
3027
- const level = result.risk.level;
3028
- counts[level.toLowerCase()] += 1;
3029
- if ((LEVEL_INDEX4[level] ?? 1) >= gateIndex) blocked = true;
3030
- files.push({
3031
- file: rel,
3032
- analyzed: true,
3033
- level,
3034
- score: result.risk.score,
3035
- affectedFiles: result.summary.affectedFiles,
3036
- targetInCycle: result.targetInCycle
3037
- });
3038
- } catch {
3039
- files.push({ file: rel, analyzed: false });
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: changed.files.length,
3247
+ changedFiles: evaluation.changes.length,
3047
3248
  files,
3048
- counts,
3049
- gate: { level: resolvedGate, blocked, verdict: blocked ? "block" : "pass" }
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
- if (!snapshot) {
3068
- const context = await loadProjectContext(ctx2.cwd, options.config);
3069
- const { graph, entryPoints } = await runPipeline(context);
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("-b, --base <ref>", "git ref to diff against (default: origin/main, main, HEAD~1)").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(),