@alimaandev/ripple 0.4.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.4.0
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 |
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
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` | `-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 |
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
package/dist/bin.js CHANGED
@@ -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,18 +1861,19 @@ 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);
1876
+ const tracker = createStageTracker(format === "terminal");
1837
1877
  const started = Date.now();
1838
1878
  const context = await loadProjectContext(ctx2.cwd, options.config);
1839
1879
  const gate = options.gate ?? context.config.diff.gate ?? "high";
@@ -1867,9 +1907,9 @@ async function diffCommand(options, ctx2) {
1867
1907
  tracker.done();
1868
1908
  const results = [...byRel.values()].sort((a, b) => b.risk.score - a.risk.score);
1869
1909
  const counts = countLevels(results);
1870
- 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]);
1871
1911
  const durationMs = Date.now() - started;
1872
- if (options.json) {
1912
+ if (format === "json") {
1873
1913
  ctx2.writer.write(
1874
1914
  serializeJson(
1875
1915
  buildDiffJsonReport(
@@ -1886,6 +1926,18 @@ async function diffCommand(options, ctx2) {
1886
1926
  );
1887
1927
  return blocked ? ExitCode.Failure : ExitCode.Success;
1888
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
+ }
1889
1941
  renderDiffReport(
1890
1942
  {
1891
1943
  cwd: ctx2.cwd,
@@ -1928,7 +1980,7 @@ function renderDiffReport(input, writer) {
1928
1980
  writer.writeLine();
1929
1981
  writer.writeLine(sectionHeader(`Risk analysis (${results.length} files)`, style));
1930
1982
  for (const result of results) {
1931
- const risky = LEVEL_INDEX[result.risk.level] >= GATE_INDEX[gate];
1983
+ const risky = LEVEL_INDEX2[result.risk.level] >= GATE_INDEX2[gate];
1932
1984
  const fileIcon = risky ? icon("cross") : icon("tick");
1933
1985
  const fileIconText = style.color ? risky ? chalk.red(fileIcon) : chalk.green(fileIcon) : fileIcon;
1934
1986
  const score = result.risk.score.toFixed(1);
@@ -2011,7 +2063,7 @@ function createProgram(ctx2) {
2011
2063
  throw new ExitError(exitCode2);
2012
2064
  }
2013
2065
  });
2014
- 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(
2015
2067
  "-g, --gate <level>",
2016
2068
  "risk level that blocks the gate (medium | high | critical)",
2017
2069
  parseGateLevel
@@ -2019,6 +2071,7 @@ function createProgram(ctx2) {
2019
2071
  const exitCode2 = await diffCommand(
2020
2072
  {
2021
2073
  json: Boolean(options.json),
2074
+ format: options.format ?? (options.json ? "json" : "terminal"),
2022
2075
  verbose: Boolean(options.verbose),
2023
2076
  ...options.base !== void 0 ? { base: options.base } : {},
2024
2077
  ...options.gate !== void 0 ? { gate: options.gate } : {},
@@ -2093,6 +2146,16 @@ function parseGateLevel(value) {
2093
2146
  `expected one of medium | high | critical, got "${value}"`
2094
2147
  );
2095
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
+ }
2096
2159
  function printError(error, ctx2) {
2097
2160
  if (error instanceof RippleError) {
2098
2161
  ctx2.errorWriter.writeLine(`\u2716 ${error.message}`);
@@ -2126,7 +2189,7 @@ async function run({ argv, ctx: ctx2 }) {
2126
2189
 
2127
2190
  // package.json
2128
2191
  var package_default = {
2129
- version: "0.4.0"};
2192
+ version: "0.5.0"};
2130
2193
 
2131
2194
  // src/cli/bin.ts
2132
2195
  var ctx = {