@alimaandev/ripple 0.3.0 → 0.5.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/README.md CHANGED
@@ -97,7 +97,7 @@ Everything below is real output against a shipped example project in `tests/fixt
97
97
  ```bash
98
98
  npm install -g @alimaandev/ripple
99
99
 
100
- ripple version # → 0.1.1
100
+ ripple version # → 0.5.0
101
101
  ```
102
102
 
103
103
  Or install from source:
@@ -212,28 +212,29 @@ ripple <command> [options]
212
212
 
213
213
  ### Options
214
214
 
215
- | Command | Flag | Description |
216
- | --------- | --------------------- | ------------------------------------------------------------------ |
217
- | `analyze` | `-j, --json` | Emit the JSON report instead of the terminal report |
218
- | `analyze` | `-v, --verbose` | Include the risk-factor point breakdown |
219
- | `analyze` | `-d, --depth <n>` | Cap the reverse traversal at `n` levels |
220
- | `analyze` | `-c, --config <path>` | Use a specific config file |
221
- | `analyze` | `--no-color` | Disable ANSI colors |
222
- | `graph` | `-j, --json` | Emit the JSON report |
223
- | `graph` | `-r, --reverse` | Show dependents (what imports this file) instead of dependencies |
224
- | `graph` | `-d, --depth <n>` | Cap tree depth |
225
- | `graph` | `-c, --config <path>` | Use a specific config file |
226
- | `graph` | `--no-color` | Disable ANSI colors |
227
- | `doctor` | `-c, --config <path>` | Use a specific config file |
228
- | `doctor` | `-v, --verbose` | Verbose output |
229
- | `doctor` | `--no-color` | Disable ANSI colors |
230
- | `init` | `-f, --force` | Overwrite an existing config |
231
- | `diff` | `-j, --json` | Emit the JSON report instead of the terminal report |
232
- | `diff` | `-b, --base <ref>` | Git ref to diff against (default: `origin/main`, `main`, `HEAD~1`) |
233
- | `diff` | `-g, --gate <level>` | Blocking level: `medium`, `high`, or `critical` (default: `high`) |
234
- | `diff` | `-d, --depth <n>` | Cap reverse traversal per file |
235
- | `diff` | `-c, --config <path>` | Use a specific config file |
236
- | `diff` | `--no-color` | Disable ANSI colors |
215
+ | Command | Flag | Description |
216
+ | --------- | --------------------- | ---------------------------------------------------------------------------------------------- |
217
+ | `analyze` | `-j, --json` | Emit the JSON report instead of the terminal report |
218
+ | `analyze` | `-v, --verbose` | Include the risk-factor point breakdown |
219
+ | `analyze` | `-d, --depth <n>` | Cap the reverse traversal at `n` levels |
220
+ | `analyze` | `-c, --config <path>` | Use a specific config file |
221
+ | `analyze` | `--no-color` | Disable ANSI colors |
222
+ | `graph` | `-j, --json` | Emit the JSON report |
223
+ | `graph` | `-r, --reverse` | Show dependents (what imports this file) instead of dependencies |
224
+ | `graph` | `-d, --depth <n>` | Cap tree depth |
225
+ | `graph` | `-c, --config <path>` | Use a specific config file |
226
+ | `graph` | `--no-color` | Disable ANSI colors |
227
+ | `doctor` | `-c, --config <path>` | Use a specific config file |
228
+ | `doctor` | `-v, --verbose` | Verbose output |
229
+ | `doctor` | `--no-color` | Disable ANSI colors |
230
+ | `init` | `-f, --force` | Overwrite an existing config |
231
+ | `diff` | `-j, --json` | Emit the JSON report instead of the terminal report |
232
+ | `diff` | `-b, --base <ref>` | Git ref to diff against (default: `origin/main`, `main`, `HEAD~1`, or `diff.base` from config) |
233
+ | `diff` | `-g, --gate <level>` | Blocking level: `medium`, `high`, or `critical` (default: `high`, or `diff.gate` from config) |
234
+ | `diff` | `-f, --format <fmt>` | Output: `terminal`, `json`, or `github` (workflow annotations) |
235
+ | `diff` | `-d, --depth <n>` | Cap reverse traversal per file |
236
+ | `diff` | `-c, --config <path>` | Use a specific config file |
237
+ | `diff` | `--no-color` | Disable ANSI colors |
237
238
 
238
239
  ### `ripple doctor`
239
240
 
@@ -298,6 +299,23 @@ Risk analysis (2 files)
298
299
  Run `ripple diff` locally before opening a PR, and in CI as the same gate —
299
300
  identical code, identical verdict.
300
301
 
302
+ ```bash
303
+ ripple diff --format github
304
+ ```
305
+
306
+ `--format github` emits GitHub Actions workflow commands. Each risky change
307
+ shows as a warning or error annotation right on the PR's Files tab —
308
+ `::error` when a file blocks the gate, `::warning` otherwise — plus a
309
+ `::notice` line with the gate verdict. The exit code still carries the
310
+ verdict, so the job fails when the gate blocks:
311
+
312
+ ```yaml
313
+ - run: npx @alimaandev/ripple diff --format github
314
+ ```
315
+
316
+ (`--json` is shorthand for `--format json`; the format flag also accepts
317
+ `terminal`, the default.)
318
+
301
319
  ## Configuration
302
320
 
303
321
  Ripple discovers `ripple.config.ts`, `.js`, `.cjs`, `.mjs`, or `.json` in the
@@ -321,6 +339,10 @@ defaults — every field is optional.
321
339
  "cycleMembership": 0.1
322
340
  },
323
341
  "thresholds": { "medium": 30, "high": 55, "critical": 80 }
342
+ },
343
+ "diff": {
344
+ "base": "origin/main",
345
+ "gate": "high"
324
346
  }
325
347
  }
326
348
  ```
@@ -333,9 +355,14 @@ defaults — every field is optional.
333
355
  | `tsconfigPath` | `tsconfig.json` | tsconfig to read for `paths` |
334
356
  | `risk.weights` | see example | Risk signal weights |
335
357
  | `risk.thresholds` | 30 / 55 / 80 | Score thresholds for MEDIUM / HIGH / CRITICAL |
358
+ | `diff.base` | `origin/main` → `main` → `HEAD~1` | Git ref to diff against when `--base` is not given |
359
+ | `diff.gate` | `high` | Blocking level when `--gate` is not given |
336
360
 
337
361
  `node_modules` is always excluded, regardless of config.
338
362
 
363
+ The `diff` block sets command defaults only — `--base`/`--gate` flags always
364
+ win over the config file, which wins over the built-in defaults.
365
+
339
366
  ## Risk scoring
340
367
 
341
368
  The score is a weighted sum of seven signals, normalized to 0–1. Affected
package/dist/bin.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { DEFAULT_CONFIG, rippleConfigSchema, formatSchemaIssues } from './chunk-OOERV74D.js';
2
+ import { DEFAULT_CONFIG, rippleConfigSchema, formatSchemaIssues } from './chunk-LDCFNORX.js';
3
3
  import { CommanderError, Command } from 'commander';
4
4
  import { createJiti } from 'jiti';
5
5
  import path8 from 'path';
@@ -1806,9 +1806,48 @@ function changedFiles(cwd, base) {
1806
1806
  return { baseLabel, files: [...files].sort() };
1807
1807
  }
1808
1808
 
1809
- // src/commands/diff.ts
1809
+ // src/formatter/annotations.ts
1810
1810
  var LEVEL_INDEX = { LOW: 0, MEDIUM: 1, HIGH: 2, CRITICAL: 3 };
1811
1811
  var GATE_INDEX = { medium: 1, high: 2, critical: 3 };
1812
+ function escapeProperty(value) {
1813
+ return value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A").replace(/:/g, "%3A").replace(/,/g, "%2C");
1814
+ }
1815
+ function escapeMessage(value) {
1816
+ return value.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A");
1817
+ }
1818
+ function renderAnnotation(annotation) {
1819
+ const props = annotation.file ? ` file=${escapeProperty(annotation.file)},title=${escapeProperty(annotation.title)}` : ` title=${escapeProperty(annotation.title)}`;
1820
+ return `::${annotation.level}${props}::${escapeMessage(annotation.message)}`;
1821
+ }
1822
+ function buildFileAnnotations(files, gate) {
1823
+ return files.map(({ file, result }) => {
1824
+ const blocked = LEVEL_INDEX[result.risk.level] >= GATE_INDEX[gate];
1825
+ return {
1826
+ level: blocked ? "error" : "warning",
1827
+ file,
1828
+ title: `Ripple ${result.risk.level}`,
1829
+ message: `${result.risk.level} risk (score ${result.risk.score}) \u2014 ${result.summary.affectedFiles} affected ${result.summary.affectedFiles === 1 ? "file" : "files"}`
1830
+ };
1831
+ });
1832
+ }
1833
+ function buildGateAnnotation(blocked, gate, counts) {
1834
+ const gateIndex = GATE_INDEX[gate];
1835
+ const parts = [];
1836
+ if (LEVEL_INDEX.CRITICAL >= gateIndex && counts.critical > 0)
1837
+ parts.push(`${counts.critical} CRITICAL`);
1838
+ if (LEVEL_INDEX.HIGH >= gateIndex && counts.high > 0) parts.push(`${counts.high} HIGH`);
1839
+ if (LEVEL_INDEX.MEDIUM >= gateIndex && counts.medium > 0) parts.push(`${counts.medium} MEDIUM`);
1840
+ const atGate = parts.length > 0 ? parts.join(" \xB7 ") : "none";
1841
+ return {
1842
+ level: "notice",
1843
+ title: `Ripple diff ${gate} gate`,
1844
+ message: blocked ? `Gate blocked: ${atGate} at or above ${gate}` : `Gate passed: nothing at ${gate} or above`
1845
+ };
1846
+ }
1847
+
1848
+ // src/commands/diff.ts
1849
+ var LEVEL_INDEX2 = { LOW: 0, MEDIUM: 1, HIGH: 2, CRITICAL: 3 };
1850
+ var GATE_INDEX2 = { medium: 1, high: 2, critical: 3 };
1812
1851
  var VERDICT_NOTE = {
1813
1852
  medium: "no MEDIUM or worse",
1814
1853
  high: "no HIGH or CRITICAL",
@@ -1822,24 +1861,26 @@ function countLevels(results) {
1822
1861
  return counts;
1823
1862
  }
1824
1863
  function riskySummary(counts, gate) {
1825
- const gateIndex = GATE_INDEX[gate];
1864
+ const gateIndex = GATE_INDEX2[gate];
1826
1865
  const parts = [];
1827
- if (LEVEL_INDEX.CRITICAL >= gateIndex && counts.critical > 0)
1866
+ if (LEVEL_INDEX2.CRITICAL >= gateIndex && counts.critical > 0)
1828
1867
  parts.push(`${counts.critical} CRITICAL`);
1829
- if (LEVEL_INDEX.HIGH >= gateIndex && counts.high > 0) parts.push(`${counts.high} HIGH`);
1830
- if (LEVEL_INDEX.MEDIUM >= gateIndex && counts.medium > 0) parts.push(`${counts.medium} MEDIUM`);
1831
- if (LEVEL_INDEX.LOW >= gateIndex && counts.low > 0) parts.push(`${counts.low} LOW`);
1868
+ if (LEVEL_INDEX2.HIGH >= gateIndex && counts.high > 0) parts.push(`${counts.high} HIGH`);
1869
+ if (LEVEL_INDEX2.MEDIUM >= gateIndex && counts.medium > 0) parts.push(`${counts.medium} MEDIUM`);
1870
+ if (LEVEL_INDEX2.LOW >= gateIndex && counts.low > 0) parts.push(`${counts.low} LOW`);
1832
1871
  return parts.length > 0 ? parts.join(" \xB7 ") : "none";
1833
1872
  }
1834
1873
  async function diffCommand(options, ctx2) {
1874
+ const format = options.format ?? (options.json ? "json" : "terminal");
1835
1875
  const style = { color: resolveColor(options.color) };
1836
- const tracker = createStageTracker(!options.json);
1837
- const gate = options.gate ?? "high";
1876
+ const tracker = createStageTracker(format === "terminal");
1838
1877
  const started = Date.now();
1878
+ const context = await loadProjectContext(ctx2.cwd, options.config);
1879
+ const gate = options.gate ?? context.config.diff.gate ?? "high";
1880
+ const base = options.base ?? context.config.diff.base;
1839
1881
  tracker.next("Resolving git changes");
1840
- const changed = changedFiles(ctx2.cwd, options.base);
1882
+ const changed = changedFiles(ctx2.cwd, base);
1841
1883
  tracker.next("Analyzing changed files");
1842
- const context = await loadProjectContext(ctx2.cwd, options.config);
1843
1884
  const { graph, entryPoints } = await runPipeline(context);
1844
1885
  const byRel = /* @__PURE__ */ new Map();
1845
1886
  const skipped = [];
@@ -1866,9 +1907,9 @@ async function diffCommand(options, ctx2) {
1866
1907
  tracker.done();
1867
1908
  const results = [...byRel.values()].sort((a, b) => b.risk.score - a.risk.score);
1868
1909
  const counts = countLevels(results);
1869
- const blocked = results.some((result) => LEVEL_INDEX[result.risk.level] >= GATE_INDEX[gate]);
1910
+ const blocked = results.some((result) => LEVEL_INDEX2[result.risk.level] >= GATE_INDEX2[gate]);
1870
1911
  const durationMs = Date.now() - started;
1871
- if (options.json) {
1912
+ if (format === "json") {
1872
1913
  ctx2.writer.write(
1873
1914
  serializeJson(
1874
1915
  buildDiffJsonReport(
@@ -1885,6 +1926,18 @@ async function diffCommand(options, ctx2) {
1885
1926
  );
1886
1927
  return blocked ? ExitCode.Failure : ExitCode.Success;
1887
1928
  }
1929
+ if (format === "github") {
1930
+ const annotations = [
1931
+ ...buildFileAnnotations(
1932
+ [...byRel.entries()].map(([file, result]) => ({ file, result })),
1933
+ gate
1934
+ ),
1935
+ buildGateAnnotation(blocked, gate, counts)
1936
+ ];
1937
+ ctx2.writer.write(`${annotations.map(renderAnnotation).join("\n")}
1938
+ `);
1939
+ return blocked ? ExitCode.Failure : ExitCode.Success;
1940
+ }
1888
1941
  renderDiffReport(
1889
1942
  {
1890
1943
  cwd: ctx2.cwd,
@@ -1927,7 +1980,7 @@ function renderDiffReport(input, writer) {
1927
1980
  writer.writeLine();
1928
1981
  writer.writeLine(sectionHeader(`Risk analysis (${results.length} files)`, style));
1929
1982
  for (const result of results) {
1930
- const risky = LEVEL_INDEX[result.risk.level] >= GATE_INDEX[gate];
1983
+ const risky = LEVEL_INDEX2[result.risk.level] >= GATE_INDEX2[gate];
1931
1984
  const fileIcon = risky ? icon("cross") : icon("tick");
1932
1985
  const fileIconText = style.color ? risky ? chalk.red(fileIcon) : chalk.green(fileIcon) : fileIcon;
1933
1986
  const score = result.risk.score.toFixed(1);
@@ -2010,7 +2063,7 @@ function createProgram(ctx2) {
2010
2063
  throw new ExitError(exitCode2);
2011
2064
  }
2012
2065
  });
2013
- program.command("diff").description("Analyze changed files since a base ref and gate risky changes").option("-j, --json", "emit a machine-readable JSON report").option("-v, --verbose", "include extra sections").option("-b, --base <ref>", "git ref to diff against (default: origin/main, main, HEAD~1)").option(
2066
+ program.command("diff").description("Analyze changed files since a base ref and gate risky changes").option("-j, --json", "emit a machine-readable JSON report").option("-f, --format <format>", "output format: terminal | json | github", parseDiffFormat).option("-v, --verbose", "include extra sections").option("-b, --base <ref>", "git ref to diff against (default: origin/main, main, HEAD~1)").option(
2014
2067
  "-g, --gate <level>",
2015
2068
  "risk level that blocks the gate (medium | high | critical)",
2016
2069
  parseGateLevel
@@ -2018,6 +2071,7 @@ function createProgram(ctx2) {
2018
2071
  const exitCode2 = await diffCommand(
2019
2072
  {
2020
2073
  json: Boolean(options.json),
2074
+ format: options.format ?? (options.json ? "json" : "terminal"),
2021
2075
  verbose: Boolean(options.verbose),
2022
2076
  ...options.base !== void 0 ? { base: options.base } : {},
2023
2077
  ...options.gate !== void 0 ? { gate: options.gate } : {},
@@ -2092,6 +2146,16 @@ function parseGateLevel(value) {
2092
2146
  `expected one of medium | high | critical, got "${value}"`
2093
2147
  );
2094
2148
  }
2149
+ function parseDiffFormat(value) {
2150
+ if (value === "terminal" || value === "json" || value === "github") {
2151
+ return value;
2152
+ }
2153
+ throw new CommanderError(
2154
+ 1,
2155
+ "commander.invalidArgument",
2156
+ `expected one of terminal | json | github, got "${value}"`
2157
+ );
2158
+ }
2095
2159
  function printError(error, ctx2) {
2096
2160
  if (error instanceof RippleError) {
2097
2161
  ctx2.errorWriter.writeLine(`\u2716 ${error.message}`);
@@ -2125,7 +2189,7 @@ async function run({ argv, ctx: ctx2 }) {
2125
2189
 
2126
2190
  // package.json
2127
2191
  var package_default = {
2128
- version: "0.3.0"};
2192
+ version: "0.5.0"};
2129
2193
 
2130
2194
  // src/cli/bin.ts
2131
2195
  var ctx = {