@jphutchins/code-review 0.1.0-alpha.53 → 0.1.0-alpha.54

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
@@ -568,6 +568,7 @@ var findingPayload = (finding, schemaVersion) => Buffer.from(
568
568
  JSON.stringify({ schema_version: schemaVersion, findings: [finding] }),
569
569
  "utf-8"
570
570
  ).toString("base64");
571
+ var lineRange = (startLine, endLine, separator) => startLine === endLine ? String(startLine) : `${String(startLine)}${separator}${String(endLine)}`;
571
572
  var findingPointer = (finding, schemaVersion, jsonUrl) => {
572
573
  const payload = findingPayload(finding, schemaVersion);
573
574
  if (payload.length > INLINE_EMBED_LIMIT_CHARS) {
@@ -1232,17 +1233,39 @@ var fetchThreadComments = async (ghApi, repo, prNumber) => {
1232
1233
 
1233
1234
  // src/render.ts
1234
1235
  var escapePipes = (text) => text.replace(/\|/g, "\\|");
1235
- var linkSafeUrl = (url) => escapeCodeBackticks(url).replace(/\(/g, "%28").replace(/\)/g, "%29");
1236
+ var encodeAutolinkParens = (url) => url.replace(/\(/g, "%28").replace(/\)/g, "%29");
1237
+ var linkSafeUrl = (url) => encodeAutolinkParens(escapeCodeBackticks(url));
1238
+ var permalinkFor = (base, f, anchor) => {
1239
+ if (f.path === "" || f.side === "LEFT") return void 0;
1240
+ const path = f.path.split("/").map(
1241
+ (segment) => encodeAutolinkParens(
1242
+ encodeURIComponent(
1243
+ segment.replace(
1244
+ /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/g,
1245
+ "\uFFFD"
1246
+ )
1247
+ )
1248
+ // Markdown emphasis/syntax characters: `__` runs split the bare URL across CommonMark
1249
+ // nodes and truncate the autolink, and a path-only link ending in one of these loses its
1250
+ // tail to the autolinker's trailing-punctuation trim (issue #231 r4).
1251
+ ).replace(/_/g, "%5F").replace(/\*/g, "%2A").replace(/'/g, "%27").replace(/!/g, "%21")
1252
+ ).join("/");
1253
+ return anchor ? `${base}${path}#L${lineRange(f.start_line, f.end_line, "-L")}` : `${base}${path}`;
1254
+ };
1236
1255
  var CARRIED_TOTAL_CHARS = 4e4;
1237
1256
  var SUPPRESSED_NIT_BLOCK_OVERHEAD = 280;
1238
- var sanitizeFinding = (f, answeredNotes) => {
1257
+ var sanitizeFinding = (f, answeredNotes, permalinkBase, unanchored) => {
1239
1258
  const key2 = answeredNoteKey(f);
1259
+ const anchored = permalinkBase !== void 0 && !(unanchored?.has(f) ?? false);
1260
+ const permalink = permalinkBase === void 0 ? void 0 : permalinkFor(permalinkBase, f, anchored);
1240
1261
  return {
1241
1262
  ...f,
1242
1263
  title: escapePipes(f.title),
1243
1264
  path: escapeCodeBackticks(f.path),
1244
1265
  ...f.code !== void 0 ? { code: escapeCodeBackticks(f.code), codeKey: f.code } : {},
1245
1266
  ...f.code_url !== void 0 ? { code_url: linkSafeUrl(f.code_url) } : {},
1267
+ rangeLabel: lineRange(f.start_line, f.end_line, "\u2013"),
1268
+ ...permalink !== void 0 ? { permalink, permalinkAnchored: anchored } : {},
1246
1269
  patchProjection: projectPatch(f.patch, "comment-body"),
1247
1270
  answeredNote: answeredNotes !== void 0 && Object.prototype.hasOwnProperty.call(answeredNotes, key2) ? answeredNotes[key2] ?? "" : ""
1248
1271
  };
@@ -1313,6 +1336,8 @@ var render = (input) => {
1313
1336
  },
1314
1337
  { list: [], used: 0, dropped: 0 }
1315
1338
  );
1339
+ const permalinkBase = input.repo !== void 0 && input.repo !== "" && input.reviewedSha !== void 0 && input.reviewedSha !== "" ? `https://github.com/${input.repo}/blob/${input.reviewedSha}/` : void 0;
1340
+ const unanchored = new Set(input.unanchoredStrays ?? []);
1316
1341
  return eta.renderString(input.template, {
1317
1342
  findings: input.findings,
1318
1343
  envelope: input.envelope,
@@ -1333,7 +1358,9 @@ var render = (input) => {
1333
1358
  postedAt: input.postedAt ?? "",
1334
1359
  severityCounts,
1335
1360
  convergenceSummary: !isFullReviewRound ? "" : convergence ? convergenceBadge(convergence) : convergenceSummary(input.findings, input.convergenceThreshold),
1336
- strays: (input.strays ?? []).map((f) => sanitizeFinding(f, input.answeredNotes)),
1361
+ strays: (input.strays ?? []).map(
1362
+ (f) => sanitizeFinding(f, input.answeredNotes, permalinkBase, unanchored)
1363
+ ),
1337
1364
  suppressedNits: suppressedBudget.list,
1338
1365
  carriedDroppedNits: suppressedBudget.dropped,
1339
1366
  nitVisibilityFloor: input.nitVisibilityFloor ?? DEFAULT_NIT_VISIBILITY_FLOOR,
@@ -2083,14 +2110,32 @@ var execFileWithTimeout = (spec) => new Promise((resolve4, reject) => {
2083
2110
 
2084
2111
  // src/gh.ts
2085
2112
  var describeEndpoint = (args) => args.find((a) => a === "graphql" || a.includes("/") && !a.startsWith("-")) ?? args[0] ?? "(no endpoint)";
2086
- var runGhApi = (args, stdin, env) => execFileWithTimeout({
2087
- command: "gh",
2088
- args: ["api", ...args],
2089
- label: `gh api ${describeEndpoint(args)}`,
2090
- timeoutMs: subprocessTimeoutMs(),
2091
- env,
2092
- stdin
2093
- });
2113
+ var flagFirst;
2114
+ var withEscapeRetry = async (run2, idempotent) => {
2115
+ if (flagFirst) return run2(true);
2116
+ try {
2117
+ return await run2(false);
2118
+ } catch (err) {
2119
+ if (errMsg(err).includes("--allow-escape-sequences")) {
2120
+ if (!idempotent) throw err;
2121
+ flagFirst = true;
2122
+ return run2(true);
2123
+ }
2124
+ throw err;
2125
+ }
2126
+ };
2127
+ var isIdempotentCall = (args) => !args.includes("--input") && !args.includes("--method") && !args.includes("graphql");
2128
+ var runGhApi = (args, stdin, env) => withEscapeRetry(
2129
+ (withFlag) => execFileWithTimeout({
2130
+ command: "gh",
2131
+ args: ["api", ...withFlag ? ["--allow-escape-sequences"] : [], ...args],
2132
+ label: `gh api ${describeEndpoint(args)}`,
2133
+ timeoutMs: subprocessTimeoutMs(),
2134
+ env,
2135
+ stdin
2136
+ }),
2137
+ isIdempotentCall(args)
2138
+ );
2094
2139
  var run = promisify(execFile);
2095
2140
  var STEP_TIMEOUT_MS = 6e4;
2096
2141
  var MARKER_URL = /<!-- code-review:findings-json (https?:\/\/[^\s>]+) -->/;
@@ -2143,11 +2188,14 @@ var readArtifactFindings = (ghApiPath) => {
2143
2188
  };
2144
2189
  };
2145
2190
  var ghArtifactReader = readArtifactFindings(async (url, outPath) => {
2146
- const { stdout } = await run("gh", ["api", url], {
2147
- encoding: "buffer",
2148
- maxBuffer: 256 * 1024 * 1024,
2149
- timeout: STEP_TIMEOUT_MS
2150
- });
2191
+ const { stdout } = await withEscapeRetry(
2192
+ (withFlag) => run("gh", ["api", ...withFlag ? ["--allow-escape-sequences"] : [], url], {
2193
+ encoding: "buffer",
2194
+ maxBuffer: 256 * 1024 * 1024,
2195
+ timeout: STEP_TIMEOUT_MS
2196
+ }),
2197
+ true
2198
+ );
2151
2199
  await writeFile(outPath, stdout);
2152
2200
  });
2153
2201
  var withoutKey = (doc, key2) => Object.fromEntries(Object.entries(doc).filter(([k]) => k !== key2));
@@ -2862,6 +2910,7 @@ ${dropNote}` : ""}`,
2862
2910
  template,
2863
2911
  route: effectiveRoute,
2864
2912
  reviewedSha: input.headSha,
2913
+ repo: input.headRepo || input.repo,
2865
2914
  effort: input.effort,
2866
2915
  sameRootNotes: {},
2867
2916
  // The answered-state honesty rules apply on EVERY surface that renders the filtered
@@ -2954,6 +3003,7 @@ ${dropNote}` : ""}`,
2954
3003
  template,
2955
3004
  route: effectiveRoute,
2956
3005
  reviewedSha: input.headSha,
3006
+ repo: input.headRepo || input.repo,
2957
3007
  effort: input.effort,
2958
3008
  testReport,
2959
3009
  clocDiff,
@@ -2980,11 +3030,12 @@ ${dropNote}` : ""}`,
2980
3030
 
2981
3031
  > **Note:** ${String(longFiles.length)} suggestion(s) exceeded GitHub's ~10-line inline suggestion limit and were omitted from the inline comments; the affected findings remain in the review.
2982
3032
  ` : "";
2983
- const renderBody = (inlineDisposition, reviewUrl2, straysOverride, unanchoredCount2) => formatMarkdown(
3033
+ const renderBody = (inlineDisposition, reviewUrl2, straysOverride, unanchoredCount2, unanchoredStrays) => formatMarkdown(
2984
3034
  render({
2985
3035
  ...commonRenderInput,
2986
3036
  ...straysOverride ? { strays: straysOverride } : {},
2987
3037
  ...unanchoredCount2 !== void 0 ? { unanchoredCount: unanchoredCount2 } : {},
3038
+ ...unanchoredStrays !== void 0 && unanchoredStrays.length > 0 ? { unanchoredStrays } : {},
2988
3039
  inlineDisposition,
2989
3040
  reviewUrl: reviewUrl2
2990
3041
  }) + longFilesNote
@@ -3041,7 +3092,7 @@ ${dropNote}` : ""}`,
3041
3092
  await patchComment(
3042
3093
  input.repo,
3043
3094
  stickyRef.id,
3044
- renderBody(finalDisposition, reviewUrl, finalStrays, unanchoredCount),
3095
+ renderBody(finalDisposition, reviewUrl, finalStrays, unanchoredCount, unposted),
3045
3096
  ghApi
3046
3097
  );
3047
3098
  process.stderr.write(
@@ -3060,7 +3111,13 @@ ${dropNote}` : ""}`,
3060
3111
  () => renderBody(
3061
3112
  { kind: "whole-document", inlineCount: inlinePosted, rejectedCount: unanchoredCount },
3062
3113
  reviewUrl,
3063
- visibleFindings
3114
+ visibleFindings,
3115
+ void 0,
3116
+ // The rejected-anchor invariant holds on EVERY surface, the run summary included — this
3117
+ // document deliberately carries every finding, so the GitHub-rejected ones must keep their
3118
+ // path-only links here too (issue #231 r2). Unconditional: renderBody's own gate treats an
3119
+ // empty array exactly like absence, so the ternary was a duplicated decision (issue #231 r3).
3120
+ unposted
3064
3121
  )
3065
3122
  );
3066
3123
  };
@@ -5447,6 +5504,10 @@ var postCmd = defineCommand({
5447
5504
  const priceResolution = resolvePrices(args.prices);
5448
5505
  await post({
5449
5506
  repo: args.repo,
5507
+ // The workflow's post step threads HEAD_REPO env (the fork's owner/name) — a finding
5508
+ // permalink targets the tree the reviewed SHA lives in (issue #231 r1). Absent/empty ⇒ the
5509
+ // base repo. Env rather than a flag: an older pinned CLI simply ignores it.
5510
+ headRepo: process.env["HEAD_REPO"] || void 0,
5450
5511
  headSha: args["head-sha"],
5451
5512
  botLogin: args["bot-login"] || "github-actions[bot]",
5452
5513
  findingsPath: args.findings,