@jphutchins/code-review 0.1.0-alpha.37 → 0.1.0-alpha.39

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/index.js CHANGED
@@ -338,7 +338,8 @@ var parseFindingsMarker = (body) => {
338
338
  return decodeBase64Json(b64) ?? null;
339
339
  };
340
340
  var ROUNDS_RE = /<!-- code-review:rounds;base64 ([A-Za-z0-9+/=]+) -->/;
341
- var isSeverityCounts = (u) => typeof u === "object" && u !== null && ["critical", "major", "minor", "nit"].every((k) => {
341
+ var SEVERITIES = ["critical", "major", "minor", "nit"];
342
+ var isSeverityCounts = (u) => typeof u === "object" && u !== null && SEVERITIES.every((k) => {
342
343
  const v = u[k];
343
344
  return typeof v === "number" && Number.isSafeInteger(v) && v >= 0;
344
345
  });
@@ -350,7 +351,7 @@ var parseRounds = (body) => {
350
351
  };
351
352
  var roundsMarker = (rounds) => rounds.length === 0 ? "" : `<!-- code-review:rounds;base64 ${Buffer.from(JSON.stringify(rounds), "utf-8").toString("base64")} -->`;
352
353
  var roundChip = (c) => {
353
- const parts = ["critical", "major", "minor", "nit"].filter((k) => c[k] > 0).map((k) => `${severityEmoji(k)}${String(c[k])}`);
354
+ const parts = SEVERITIES.filter((k) => c[k] > 0).map((k) => `${severityEmoji(k)}${String(c[k])}`);
354
355
  return parts.length === 0 ? "clean" : parts.join(" ");
355
356
  };
356
357
  var TRAJECTORY_CHIPS = 8;
@@ -360,6 +361,13 @@ var roundsSummary = (rounds) => {
360
361
  const trajectory = rounds.length > TRAJECTORY_CHIPS ? `\u2026 \u2192 ${chips.join(" \u2192 ")}` : chips.join(" \u2192 ");
361
362
  return `**Round ${String(rounds.length)}** \xB7 ${trajectory}`;
362
363
  };
364
+ var CONVERGENCE_WEIGHTS = { critical: 4, major: 2, minor: 1, nit: 0 };
365
+ var DEFAULT_CONVERGENCE_THRESHOLD = 1;
366
+ var convergenceScore = (counts) => SEVERITIES.reduce((sum, k) => sum + counts[k] * CONVERGENCE_WEIGHTS[k], 0);
367
+ var convergenceSummary = (counts, threshold = DEFAULT_CONVERGENCE_THRESHOLD) => {
368
+ const score = convergenceScore(counts);
369
+ return score <= threshold ? `**Convergence** \u{1F3C1} ${String(score)} \u2264 ${String(threshold)} \u2014 converged` : `**Convergence** \u{1F504} ${String(score)} > ${String(threshold)} \u2014 iterating`;
370
+ };
363
371
  var carryForwardMarkers = (body) => {
364
372
  const findings = /<!-- code-review:findings-json[^>]*-->/.exec(body)?.[0];
365
373
  const reviewedSha = /<!-- reviewed-sha: [0-9a-fA-F]{40} -->/.exec(body)?.[0];
@@ -402,6 +410,7 @@ var computeSeverityCounts = (findings) => findings.reduce(
402
410
  (acc, f) => f.severity in acc ? { ...acc, [f.severity]: acc[f.severity] + 1 } : acc,
403
411
  emptySeverityCounts()
404
412
  );
413
+ var isConvergenceRound = (route, incomplete) => route === "full review" && !incomplete;
405
414
  var render = (input) => {
406
415
  const eta = new Eta({ autoTrim: false });
407
416
  const usageAvailable = input.envelope !== null;
@@ -412,6 +421,9 @@ var render = (input) => {
412
421
  const route = input.route ?? input.envelope?.route ?? null;
413
422
  const effort = input.effort ?? input.envelope?.effort ?? null;
414
423
  const modelNames = input.envelope ? input.envelope.models.map((m) => m.model).join(", ") : "";
424
+ const severityCounts = input.severityCounts ?? computeSeverityCounts(input.findings.findings);
425
+ const rounds = input.rounds ?? [];
426
+ const isFullReviewRound = (input.convergenceRound ?? isConvergenceRound(route, incomplete)) && input.findings.verdict !== "error";
415
427
  return eta.renderString(input.template, {
416
428
  findings: input.findings,
417
429
  envelope: input.envelope,
@@ -426,15 +438,16 @@ var render = (input) => {
426
438
  testReport: input.testReport ?? null,
427
439
  reviewedSha: input.reviewedSha ?? "0000000000000000000000000000000000000000",
428
440
  postedAt: input.postedAt ?? "",
429
- severityCounts: input.severityCounts ?? computeSeverityCounts(input.findings.findings),
441
+ severityCounts,
442
+ convergenceSummary: isFullReviewRound ? convergenceSummary(severityCounts, input.convergenceThreshold) : "",
430
443
  strays: (input.strays ?? []).map(sanitizeFinding),
431
444
  unanchoredCount: input.unanchoredCount ?? 0,
432
445
  inlineDisposition: input.inlineDisposition ?? null,
433
446
  runUrl: input.runUrl ?? null,
434
447
  jsonUrl: input.jsonUrl ?? null,
435
448
  findingsPointer: input.findingsPointer ?? findingsPointer(input.findings, input.jsonUrl),
436
- roundsMarker: roundsMarker(input.rounds ?? []),
437
- roundsSummary: roundsSummary(input.rounds ?? []),
449
+ roundsMarker: roundsMarker(rounds),
450
+ roundsSummary: roundsSummary(rounds),
438
451
  reviewUrl: input.reviewUrl ?? null,
439
452
  formatTokens: (n) => Number.isFinite(n) && n >= 0 ? n.toLocaleString("en-US") : "\u2014",
440
453
  // N/A (never a false $0.00) when no real price map was provided — real tokens, no rates to price them.
@@ -1622,6 +1635,7 @@ var post = async (input, ghApi = runGhApi) => {
1622
1635
  reviewedSha: input.headSha,
1623
1636
  effort: input.effort,
1624
1637
  rounds: priorRounds,
1638
+ convergenceRound: false,
1625
1639
  runUrl: input.runUrl,
1626
1640
  jsonUrl: input.jsonUrl,
1627
1641
  postedAt: input.postedAt
@@ -1668,6 +1682,7 @@ var post = async (input, ghApi = runGhApi) => {
1668
1682
  reviewedSha: input.headSha,
1669
1683
  effort: input.effort,
1670
1684
  rounds: priorRounds,
1685
+ convergenceRound: false,
1671
1686
  testReport,
1672
1687
  inlineDisposition: { kind: "no-envelope" },
1673
1688
  runUrl: input.runUrl,
@@ -1705,7 +1720,8 @@ var post = async (input, ghApi = runGhApi) => {
1705
1720
  const initialDisposition = comments.length === 0 && strays.length > 0 ? { kind: "none-in-diff" } : void 0;
1706
1721
  const currentCounts = computeSeverityCounts(findings.findings);
1707
1722
  const effectiveRoute = input.route ?? envelope.route;
1708
- const rounds = effectiveRoute === "full review" && !thisIncomplete ? [...priorRounds, currentCounts] : priorRounds;
1723
+ const isRound = isConvergenceRound(effectiveRoute, thisIncomplete);
1724
+ const rounds = isRound ? [...priorRounds, currentCounts] : priorRounds;
1709
1725
  const commonRenderInput = {
1710
1726
  findings,
1711
1727
  envelope,
@@ -1719,6 +1735,8 @@ var post = async (input, ghApi = runGhApi) => {
1719
1735
  testReport,
1720
1736
  severityCounts: currentCounts,
1721
1737
  rounds,
1738
+ convergenceThreshold: input.convergenceThreshold,
1739
+ convergenceRound: isRound,
1722
1740
  strays,
1723
1741
  runUrl: input.runUrl,
1724
1742
  jsonUrl: input.jsonUrl,
@@ -2809,6 +2827,12 @@ var deriveModelHost = (apiBaseUrl) => {
2809
2827
  }
2810
2828
  return host;
2811
2829
  };
2830
+ var KNOWN_MODEL_HOST_SUFFIXES = ["anthropic.com", "deepseek.com"];
2831
+ var isKnownModelHost = (host, declared = []) => {
2832
+ const normalize = (h) => h.replace(/\.$/, "").toLowerCase();
2833
+ const target = normalize(host);
2834
+ return declared.some((d) => normalize(d) === target) || KNOWN_MODEL_HOST_SUFFIXES.some((suffix) => target.endsWith(`.${suffix}`));
2835
+ };
2812
2836
  var parseExtraEndpoints = (extra) => extra.split(/\s+/).map((token) => token.replace(/:\d+$/, "")).filter((token) => token.length > 0);
2813
2837
  var buildSandboxConfig = (opts) => ({
2814
2838
  network: {
@@ -2932,6 +2956,7 @@ var resolvePrices = (pricesArg) => {
2932
2956
  return { kind: "absent", path: bundledPath("schema", "prices.example.json") };
2933
2957
  };
2934
2958
  var TEST_REPORT_DESCRIPTION = 'Path to a JSON test summary: {"passed": number, "failed": number, "total": number, "failures"?: [{"name": string, "message"?: string}]}';
2959
+ var CONVERGENCE_THRESHOLD_DESCRIPTION = "Advisory convergence tolerance: the weighted-severity score (critical 4 \xB7 major 2 \xB7 minor 1 \xB7 nit 0) at or below which the sticky reads as converged (default: 1 \u2014 unlimited nits plus at most one minor)";
2935
2960
  var renderCmd = defineCommand({
2936
2961
  meta: {
2937
2962
  name: "render",
@@ -2971,6 +2996,10 @@ var renderCmd = defineCommand({
2971
2996
  "test-report": {
2972
2997
  type: "string",
2973
2998
  description: TEST_REPORT_DESCRIPTION
2999
+ },
3000
+ "convergence-threshold": {
3001
+ type: "string",
3002
+ description: CONVERGENCE_THRESHOLD_DESCRIPTION
2974
3003
  }
2975
3004
  },
2976
3005
  run: async ({ args }) => {
@@ -2991,6 +3020,7 @@ var renderCmd = defineCommand({
2991
3020
  route: args.route,
2992
3021
  effort: args.effort,
2993
3022
  testReport,
3023
+ convergenceThreshold: parseConvergenceThreshold(args["convergence-threshold"]),
2994
3024
  postedAt: formatUtc(/* @__PURE__ */ new Date())
2995
3025
  });
2996
3026
  process.stdout.write(output2);
@@ -3111,6 +3141,18 @@ var parseBudgetUsd = (raw) => {
3111
3141
  const n = Number.parseFloat(raw);
3112
3142
  return Number.isFinite(n) && n >= 0 ? n : null;
3113
3143
  };
3144
+ var parseConvergenceThreshold = (raw) => {
3145
+ const trimmed = raw?.trim();
3146
+ if (trimmed === void 0 || trimmed === "") return void 0;
3147
+ if (!/^\d+(\.\d+)?$/.test(trimmed)) {
3148
+ fail(`--convergence-threshold must be a non-negative number; got "${trimmed}"`);
3149
+ }
3150
+ const n = Number.parseFloat(trimmed);
3151
+ if (!Number.isFinite(n)) {
3152
+ fail(`--convergence-threshold is too large to be a meaningful tolerance; got "${trimmed}"`);
3153
+ }
3154
+ return n;
3155
+ };
3114
3156
  var mtimeMsOf = (path) => {
3115
3157
  try {
3116
3158
  return statSync(path).mtimeMs;
@@ -3957,6 +3999,10 @@ var postCmd = defineCommand({
3957
3999
  "json-url": {
3958
4000
  type: "string",
3959
4001
  description: "URL to the machine-readable findings JSON artifact, pointed at from the sticky and each inline comment"
4002
+ },
4003
+ "convergence-threshold": {
4004
+ type: "string",
4005
+ description: CONVERGENCE_THRESHOLD_DESCRIPTION
3960
4006
  }
3961
4007
  },
3962
4008
  run: async ({ args }) => {
@@ -3977,6 +4023,7 @@ var postCmd = defineCommand({
3977
4023
  testReportPath: args["test-report"],
3978
4024
  runUrl: args["run-url"],
3979
4025
  jsonUrl: args["json-url"],
4026
+ convergenceThreshold: parseConvergenceThreshold(args["convergence-threshold"]),
3980
4027
  postedAt: formatUtc(/* @__PURE__ */ new Date())
3981
4028
  });
3982
4029
  }
@@ -4292,9 +4339,32 @@ var sandboxConfigCmd = defineCommand({
4292
4339
  out: {
4293
4340
  type: "string",
4294
4341
  description: "Write the settings JSON here instead of stdout"
4342
+ },
4343
+ "known-model-host": {
4344
+ type: "string",
4345
+ description: "Additional model HOST(S) to treat as known (space-separated bare hostnames, not URLs) \u2014 the consumer's declared host(s) for a provider outside the built-in set; a derived host outside the built-ins and this list warns (or fails under --strict-host)"
4346
+ },
4347
+ "strict-host": {
4348
+ type: "boolean",
4349
+ description: "Fail (instead of warning) when the model host derived from api_base_url is not a well-known or declared host \u2014 closes the fail-open-on-typo gap where a mistyped api_base_url would still be allowlisted and sent the key"
4295
4350
  }
4296
4351
  },
4297
4352
  run: ({ args }) => {
4353
+ const modelHost = deriveModelHost(args["api-base-url"]);
4354
+ const declaredHosts = [
4355
+ ...args["known-model-host"] ? parseExtraEndpoints(args["known-model-host"]) : [],
4356
+ ...args.extra ? parseExtraEndpoints(args.extra) : []
4357
+ ];
4358
+ if (!isKnownModelHost(modelHost, declaredHosts)) {
4359
+ const message = `code-review sandbox-config: derived model host "${modelHost}" is not a well-known or declared host \u2014 the jail will allow egress to it and send MODEL_API_KEY there; verify api_base_url is correct`;
4360
+ if (args["strict-host"]) {
4361
+ process.stderr.write(`::error::${annotationSafe(message)}
4362
+ `);
4363
+ process.exit(1);
4364
+ }
4365
+ process.stderr.write(`::warning::${annotationSafe(message)}
4366
+ `);
4367
+ }
4298
4368
  const config = buildSandboxConfig({ apiBaseUrl: args["api-base-url"], extra: args.extra });
4299
4369
  const json = `${JSON.stringify(config, null, 2)}
4300
4370
  `;