@jphutchins/code-review 0.1.0-alpha.26 → 0.1.0-alpha.27

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
@@ -195,6 +195,13 @@ var parseFindingsMarker = (body) => {
195
195
  return null;
196
196
  }
197
197
  };
198
+ var carryForwardMarkers = (body) => {
199
+ const findings = /<!-- code-review:findings-json[^>]*-->/.exec(body)?.[0];
200
+ const reviewedSha = /<!-- reviewed-sha: [0-9a-fA-F]{40} -->/.exec(body)?.[0];
201
+ const findingsBlock = findings ? `${AGENTS_STOP_DIRECTIVE}
202
+ ${findings}` : void 0;
203
+ return [findingsBlock, reviewedSha].filter((m) => m !== void 0).join("\n\n");
204
+ };
198
205
  var escapeFence = (text) => text.replace(/```/g, "`` ` ``");
199
206
  var projectPatch = (patch) => {
200
207
  if (patch === void 0) return { kind: "none" };
@@ -1590,6 +1597,47 @@ var post = async (input, ghApi = runGhApi) => {
1590
1597
  }
1591
1598
  }
1592
1599
  };
1600
+ var announceBody = (headSha, runUrl, existingBody) => {
1601
+ const notice = `${DEFAULT_MARKER}
1602
+
1603
+ \u{1F504} **Code review in progress** for \`${headSha.slice(0, 7)}\` \u2014 see the [workflow run](${runUrl}) for progress; this comment is updated with the review when it completes.`;
1604
+ const carried = existingBody ? carryForwardMarkers(existingBody) : "";
1605
+ return carried ? `${notice}
1606
+
1607
+ ${carried}` : notice;
1608
+ };
1609
+ var announce = async (input, ghApi = runGhApi) => {
1610
+ const candidates = await fetchPrCandidates(input.repo, input.headSha, ghApi);
1611
+ const resolution = resolvePr(candidates, input.headBranch);
1612
+ if (resolution.kind !== "open") {
1613
+ process.stderr.write(
1614
+ `No open PR for ${input.headSha} \u2014 nothing to announce (${resolution.kind})
1615
+ `
1616
+ );
1617
+ return;
1618
+ }
1619
+ const existing = await findBotComment(
1620
+ input.repo,
1621
+ resolution.prNumber,
1622
+ input.botLogin,
1623
+ DEFAULT_MARKER,
1624
+ ghApi
1625
+ );
1626
+ if (existing !== null && parseReviewedSha(existing.body) === input.headSha.toLowerCase()) {
1627
+ process.stderr.write(
1628
+ `Sticky already reflects a completed review of ${input.headSha} \u2014 leaving it in place
1629
+ `
1630
+ );
1631
+ return;
1632
+ }
1633
+ await upsertSticky(
1634
+ input.repo,
1635
+ resolution.prNumber,
1636
+ existing,
1637
+ announceBody(input.headSha, input.runUrl, existing?.body),
1638
+ ghApi
1639
+ );
1640
+ };
1593
1641
  var DURATION_RE = /^(\d+)(h|m|s)$/;
1594
1642
  var USD_RE = /^\$(\d+(?:\.\d+)?)$/;
1595
1643
  var toSeconds = (n, unit) => unit === "h" ? n * 3600 : unit === "m" ? n * 60 : n;
@@ -1855,12 +1903,17 @@ var PrMetaCodec = t.type({
1855
1903
  title: t.string,
1856
1904
  body: t.union([t.string, t.null])
1857
1905
  });
1858
- var IssueCommentCodec = t.type({
1859
- id: t.number,
1860
- body: t.union([t.string, t.null]),
1861
- user: t.type({ login: t.string })
1862
- });
1863
- var IssueCommentsCodec = t.array(IssueCommentCodec);
1906
+ var IssueCommentCodec = t.intersection([
1907
+ t.type({
1908
+ id: t.number,
1909
+ body: t.union([t.string, t.null]),
1910
+ user: t.type({ login: t.string })
1911
+ }),
1912
+ t.partial({
1913
+ created_at: t.union([t.string, t.null]),
1914
+ author_association: t.union([t.string, t.null])
1915
+ })
1916
+ ]);
1864
1917
  var JobCodec = t.type({ id: t.number, conclusion: t.union([t.string, t.null]) });
1865
1918
  var JobsResponseCodec = t.type({ jobs: t.array(JobCodec) });
1866
1919
  var fetchPrMeta = async (repo, prNumber, ghApi) => {
@@ -1882,18 +1935,92 @@ var fetchApiDiff = async (repo, prNumber, ghApi) => {
1882
1935
  return null;
1883
1936
  }
1884
1937
  };
1885
- var fetchPriorReview = async (repo, prNumber, botLogin, ghApi) => {
1938
+ var COMMENT_JQ = ".[] | {id: .id, body: .body, user: {login: .user.login}, created_at: .created_at, author_association: .author_association}";
1939
+ var REVIEW_COMMENT_JQ = ".[] | {body: .body, user: {login: .user.login}, created_at: .created_at, author_association: .author_association, path: .path, line: .line}";
1940
+ var REVIEW_JQ = ".[] | {body: .body, user: {login: .user.login}, submitted_at: .submitted_at, author_association: .author_association, state: .state}";
1941
+ var ReviewCommentCodec = t.intersection([
1942
+ t.type({ body: t.union([t.string, t.null]), user: t.type({ login: t.string }) }),
1943
+ t.partial({
1944
+ created_at: t.union([t.string, t.null]),
1945
+ author_association: t.union([t.string, t.null]),
1946
+ path: t.union([t.string, t.null]),
1947
+ line: t.union([t.number, t.null])
1948
+ })
1949
+ ]);
1950
+ var ReviewCodec = t.intersection([
1951
+ t.type({ body: t.union([t.string, t.null]), user: t.type({ login: t.string }) }),
1952
+ t.partial({
1953
+ submitted_at: t.union([t.string, t.null]),
1954
+ author_association: t.union([t.string, t.null]),
1955
+ state: t.union([t.string, t.null])
1956
+ })
1957
+ ]);
1958
+ var fetchJsonlRows = async (ghApi, endpoint, jq) => {
1886
1959
  try {
1887
- const stdout = await ghApi([`repos/${repo}/issues/${String(prNumber)}/comments`, "--paginate"]);
1888
- const decoded = IssueCommentsCodec.decode(JSON.parse(stdout || "[]"));
1889
- if (decoded._tag === "Left") return null;
1890
- const byBot = decoded.right.filter((c) => c.user.login === botLogin);
1891
- const last = byBot[byBot.length - 1];
1892
- return last ? { id: last.id, body: last.body } : null;
1893
- } catch {
1960
+ return parseJsonl(await ghApi([endpoint, "--paginate", "--jq", jq]));
1961
+ } catch (err) {
1962
+ process.stderr.write(
1963
+ `Warning: could not fetch ${endpoint} (${errMsg(err)}) \u2014 omitting it from the review context
1964
+ `
1965
+ );
1894
1966
  return null;
1895
1967
  }
1896
1968
  };
1969
+ var decodeArrayOrNull = (codec, rows) => {
1970
+ if (rows === null) return null;
1971
+ return rows.flatMap((row) => {
1972
+ const decoded = codec.decode(row);
1973
+ return decoded._tag === "Right" ? [decoded.right] : [];
1974
+ });
1975
+ };
1976
+ var priorReviewFrom = (comments, botLogin) => {
1977
+ const byBot = comments.filter((c) => c.user.login === botLogin);
1978
+ const last = byBot[byBot.length - 1];
1979
+ return last ? { id: last.id, body: last.body } : null;
1980
+ };
1981
+ var MAX_CONVERSATION_COMMENTS = 50;
1982
+ var MAX_CONVERSATION_BODY_CHARS = 4e3;
1983
+ var clip = (body) => {
1984
+ if (body.length <= MAX_CONVERSATION_BODY_CHARS) return body;
1985
+ const cut = body.slice(0, MAX_CONVERSATION_BODY_CHARS);
1986
+ const safe = /[\uD800-\uDBFF]$/.test(cut) ? cut.slice(0, -1) : cut;
1987
+ return `${safe}
1988
+ \u2026 [truncated]`;
1989
+ };
1990
+ var boundedHuman = (items, botLogin, label, project) => {
1991
+ const human = items.filter(
1992
+ (a) => a.user.login !== botLogin && typeof a.body === "string" && a.body.trim() !== ""
1993
+ );
1994
+ const kept = human.slice(-MAX_CONVERSATION_COMMENTS);
1995
+ if (kept.length < human.length) {
1996
+ process.stderr.write(
1997
+ `Note: PR has ${String(human.length)} ${label} \u2014 feeding the review the most recent ${String(MAX_CONVERSATION_COMMENTS)}
1998
+ `
1999
+ );
2000
+ }
2001
+ return kept.map(project);
2002
+ };
2003
+ var issueCommentsFrom = (comments, botLogin) => boundedHuman(comments, botLogin, "discussion comments", (c) => ({
2004
+ author: c.user.login,
2005
+ author_association: c.author_association ?? null,
2006
+ created_at: c.created_at ?? null,
2007
+ body: clip(c.body)
2008
+ }));
2009
+ var reviewCommentsFrom = (comments, botLogin) => boundedHuman(comments, botLogin, "inline review comments", (c) => ({
2010
+ author: c.user.login,
2011
+ author_association: c.author_association ?? null,
2012
+ created_at: c.created_at ?? null,
2013
+ path: c.path ?? null,
2014
+ line: c.line ?? null,
2015
+ body: clip(c.body)
2016
+ }));
2017
+ var reviewsFrom = (reviews, botLogin) => boundedHuman(reviews, botLogin, "review submissions", (r) => ({
2018
+ author: r.user.login,
2019
+ author_association: r.author_association ?? null,
2020
+ submitted_at: r.submitted_at ?? null,
2021
+ state: r.state ?? null,
2022
+ body: clip(r.body)
2023
+ }));
1897
2024
  var downloadFailingJobLogs = async (repo, runId, outDir, ghApi) => {
1898
2025
  const stdout = await ghApi([`repos/${repo}/actions/runs/${runId}/jobs`]);
1899
2026
  const decoded = JobsResponseCodec.decode(JSON.parse(stdout));
@@ -1943,11 +2070,31 @@ var gather = async (input, ghApi = runGhApi, gitRun = runGit) => {
1943
2070
  join(input.outDir, "pr_context.json"),
1944
2071
  JSON.stringify({ title: meta.title, body: meta.body })
1945
2072
  );
1946
- const prior = await fetchPriorReview(input.repo, prNumber, input.botLogin, ghApi);
2073
+ const [issueRows, reviewCommentRows, reviewRows] = await Promise.all([
2074
+ fetchJsonlRows(ghApi, `repos/${input.repo}/issues/${String(prNumber)}/comments`, COMMENT_JQ),
2075
+ fetchJsonlRows(
2076
+ ghApi,
2077
+ `repos/${input.repo}/pulls/${String(prNumber)}/comments`,
2078
+ REVIEW_COMMENT_JQ
2079
+ ),
2080
+ fetchJsonlRows(ghApi, `repos/${input.repo}/pulls/${String(prNumber)}/reviews`, REVIEW_JQ)
2081
+ ]);
2082
+ const issueComments = decodeArrayOrNull(IssueCommentCodec, issueRows);
2083
+ const reviewComments = decodeArrayOrNull(ReviewCommentCodec, reviewCommentRows);
2084
+ const reviews = decodeArrayOrNull(ReviewCodec, reviewRows);
2085
+ const prior = issueComments === null ? null : priorReviewFrom(issueComments, input.botLogin);
1947
2086
  writeFileSync(
1948
2087
  join(input.outDir, "prior_review.json"),
1949
2088
  prior === null ? "null" : JSON.stringify(prior)
1950
2089
  );
2090
+ writeFileSync(
2091
+ join(input.outDir, "pr_conversation.json"),
2092
+ JSON.stringify({
2093
+ issue_comments: issueComments === null ? [] : issueCommentsFrom(issueComments, input.botLogin),
2094
+ review_comments: reviewComments === null ? [] : reviewCommentsFrom(reviewComments, input.botLogin),
2095
+ reviews: reviews === null ? [] : reviewsFrom(reviews, input.botLogin)
2096
+ })
2097
+ );
1951
2098
  if (input.conclusion === "failure") {
1952
2099
  await downloadFailingJobLogs(input.repo, input.runId, input.outDir, ghApi);
1953
2100
  }
@@ -3204,7 +3351,7 @@ var stopGateCmd = defineCommand({
3204
3351
  var gatherCmd = defineCommand({
3205
3352
  meta: {
3206
3353
  name: "gather",
3207
- description: "Resolve the PR from the CI head SHA and gather review inputs (diff with git-diff fallback, PR context, prior bot review, failing-job logs) as files for the review agent"
3354
+ description: "Resolve the PR from the CI head SHA and gather review inputs (diff with git-diff fallback, PR context, prior bot review, untrusted PR conversation to triage, failing-job logs) as files for the review agent"
3208
3355
  },
3209
3356
  args: {
3210
3357
  repo: { type: "string", description: "Repository (owner/name)", required: true },
@@ -3338,6 +3485,51 @@ var postCmd = defineCommand({
3338
3485
  });
3339
3486
  }
3340
3487
  });
3488
+ var announceCmd = defineCommand({
3489
+ meta: {
3490
+ name: "announce",
3491
+ description: "Post (or update) the sticky the moment a review starts \u2014 an in-progress placeholder linking the run \u2014 so a workflow_run review, which runs from the default branch and is otherwise invisible on the PR, is visibly under way. Preserves a prior sticky's embedded findings + reviewed-sha markers so the re-review seed survives the swap."
3492
+ },
3493
+ args: {
3494
+ "head-sha": {
3495
+ type: "string",
3496
+ description: "Trusted head SHA to resolve the PR (from workflow_run.head_sha)",
3497
+ required: true
3498
+ },
3499
+ repo: {
3500
+ type: "string",
3501
+ description: "Repository (owner/name)",
3502
+ required: true
3503
+ },
3504
+ "run-url": {
3505
+ type: "string",
3506
+ description: "Workflow run URL the placeholder links to",
3507
+ required: true
3508
+ },
3509
+ "bot-login": {
3510
+ type: "string",
3511
+ description: "Bot login to trust for the sticky comment upsert (default: github-actions[bot])"
3512
+ },
3513
+ "head-branch": {
3514
+ type: "string",
3515
+ description: "Head branch to disambiguate the PR when multiple share a commit"
3516
+ }
3517
+ },
3518
+ run: async ({ args }) => {
3519
+ await announce({
3520
+ repo: args.repo,
3521
+ headSha: args["head-sha"],
3522
+ botLogin: args["bot-login"] || "github-actions[bot]",
3523
+ runUrl: args["run-url"],
3524
+ headBranch: args["head-branch"]
3525
+ }).catch(
3526
+ (err) => process.stderr.write(
3527
+ `code-review announce: could not post the in-progress sticky (${errMsg(err)}) \u2014 continuing (cosmetic)
3528
+ `
3529
+ )
3530
+ );
3531
+ }
3532
+ });
3341
3533
  var requireCeilingSec = (raw) => {
3342
3534
  if (raw === void 0) return null;
3343
3535
  const ms = parseWallMs(raw);
@@ -3510,6 +3702,7 @@ var main = defineCommand({
3510
3702
  render: renderCmd,
3511
3703
  inline: inlineCmd,
3512
3704
  post: postCmd,
3705
+ announce: announceCmd,
3513
3706
  cost: costCmd,
3514
3707
  "check-cost": checkCostCmd,
3515
3708
  validate: validateCmd,