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

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
@@ -9,6 +9,7 @@ import { Ajv2020 } from 'ajv/dist/2020.js';
9
9
  import _addFormats from 'ajv-formats';
10
10
  import * as t from 'io-ts';
11
11
  import { execFile } from 'child_process';
12
+ import { performance } from 'perf_hooks';
12
13
  import { PathReporter } from 'io-ts/lib/PathReporter.js';
13
14
 
14
15
  // src/cost.ts
@@ -185,6 +186,8 @@ var parseReviewedSha = (body) => {
185
186
  const sha = /<!-- reviewed-sha: ([0-9a-fA-F]{40}) -->/.exec(body)?.[1]?.toLowerCase();
186
187
  return sha && sha !== ZERO_SHA ? sha : null;
187
188
  };
189
+ var REVIEW_COMPLETE_MARKER = "<!-- review-complete -->";
190
+ var parseReviewComplete = (body) => body.includes(REVIEW_COMPLETE_MARKER);
188
191
  var parseFindingsMarker = (body) => {
189
192
  const match = /<!-- code-review:findings-json;base64 ([A-Za-z0-9+/=]+) -->/.exec(body);
190
193
  const b64 = match?.[1];
@@ -239,6 +242,8 @@ var computeSeverityCounts = (findings) => findings.reduce(
239
242
  var render = (input) => {
240
243
  const eta = new Eta({ autoTrim: false });
241
244
  const usageAvailable = input.envelope !== null;
245
+ const hasUsage = input.envelope !== null && input.envelope.models.length > 0;
246
+ const incomplete = input.incomplete ?? input.envelope?.incomplete ?? false;
242
247
  const costReport = input.envelope ? computeCost(input.envelope.models, input.prices) : null;
243
248
  const pricesProvided = input.pricesProvided ?? true;
244
249
  const route = input.route ?? input.envelope?.route ?? null;
@@ -248,6 +253,8 @@ var render = (input) => {
248
253
  findings: input.findings,
249
254
  envelope: input.envelope,
250
255
  usageAvailable,
256
+ hasUsage,
257
+ incomplete,
251
258
  costReport,
252
259
  pricesProvided,
253
260
  route,
@@ -601,7 +608,12 @@ var ResultEnvelopeCodec = t.intersection([
601
608
  t.partial({
602
609
  vendor_cost_usd: t.union([t.number, t.null]),
603
610
  route: t.string,
604
- effort: t.string
611
+ effort: t.string,
612
+ // The run produced a notice rather than a completed review (security-gate block, agent kill, no
613
+ // recoverable findings). An empty `findings` array alone can't say this — a genuine clean review
614
+ // is also empty — so the render suppresses "clean review" and the sticky precedence guard refuses
615
+ // to bury a completed review under it. Absent ⇒ a completed review.
616
+ incomplete: t.boolean
605
617
  })
606
618
  ]);
607
619
  var ModelPricesCodec = t.type({
@@ -958,6 +970,35 @@ var formatMarkdown = (md) => {
958
970
  };
959
971
  var pad2 = (n) => String(n).padStart(2, "0");
960
972
  var formatUtc = (d) => `${String(d.getUTCFullYear())}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())} ${pad2(d.getUTCHours())}:${pad2(d.getUTCMinutes())} UTC`;
973
+
974
+ // src/notice.ts
975
+ var isNoticeKind = (s) => s === "security-blocked" || s === "setup-failed" || s === "diff-apply-failed" || s === "no-output";
976
+ var blockquote = (text) => text.replaceAll("\n", "\n> ");
977
+ var noticeSummary = (kind, reasons) => {
978
+ switch (kind) {
979
+ case "security-blocked":
980
+ return reasons !== void 0 && reasons.trim() !== "" ? `### \u{1F6D1} Code review skipped by the security gate
981
+
982
+ The diff was flagged as unsafe to apply and execute:
983
+
984
+ > ${blockquote(reasons)}` : "### \u{1F6D1} Code review skipped by the security gate\n\nThe security triage returned an unsafe verdict without a reason. See workflow logs.";
985
+ case "setup-failed":
986
+ return "### \u{1F6E0}\uFE0F Review did not run\n\nThe review job failed before the security triage could run (e.g. dependency install or environment setup). See the workflow logs \u2014 this is an infrastructure failure, not a security verdict.";
987
+ case "diff-apply-failed":
988
+ return "### \u26A0\uFE0F Could not apply the diff\n\nThe PR diff could not be applied to the checked-out base commit, so the review was skipped rather than run against an unmodified tree. See workflow logs.";
989
+ case "no-output":
990
+ return "### \u26A0\uFE0F Review did not complete\n\nThe diff passed triage but the review produced no output. See workflow logs.";
991
+ }
992
+ };
993
+ var buildNoticeEnvelope = (kind, reasons) => ({
994
+ schema_version: DEFAULT_SCHEMA_VERSION,
995
+ findings: noticeFindings(noticeSummary(kind, reasons)),
996
+ models: [],
997
+ turns: 0,
998
+ duration_ms: 0,
999
+ vendor_cost_usd: null,
1000
+ incomplete: true
1001
+ });
961
1002
  var identity = (decoded) => decoded;
962
1003
  var findingsTable = [
963
1004
  {
@@ -1416,6 +1457,15 @@ var post = async (input, ghApi = runGhApi) => {
1416
1457
  DEFAULT_MARKER,
1417
1458
  ghApi
1418
1459
  );
1460
+ const existingComplete = existingSticky !== null && parseReviewComplete(existingSticky.body);
1461
+ const wouldBuryCompleted = (incomplete) => incomplete && existingComplete;
1462
+ const leaveInPlace = () => {
1463
+ process.stderr.write(
1464
+ `Review did not complete and the sticky already reflects a completed review \u2014 leaving it in place
1465
+ `
1466
+ );
1467
+ process.exit(0);
1468
+ };
1419
1469
  const prices = JSON.parse(readFileSync(input.pricesPath, "utf-8"));
1420
1470
  const decodedPrices = PriceMapCodec.decode(prices);
1421
1471
  if (decodedPrices._tag === "Left") {
@@ -1427,6 +1477,7 @@ var post = async (input, ghApi = runGhApi) => {
1427
1477
  render({
1428
1478
  findings: noticeFindings(`### \u26A0\uFE0F ${message}`),
1429
1479
  envelope: null,
1480
+ incomplete: true,
1430
1481
  prices: decodedPrices.right,
1431
1482
  pricesProvided: input.pricesProvided,
1432
1483
  template,
@@ -1439,6 +1490,7 @@ var post = async (input, ghApi = runGhApi) => {
1439
1490
  })
1440
1491
  );
1441
1492
  if (isEmptyDiff(diff)) {
1493
+ if (wouldBuryCompleted(true)) leaveInPlace();
1442
1494
  await upsertSticky(
1443
1495
  input.repo,
1444
1496
  prNumber,
@@ -1450,6 +1502,7 @@ var post = async (input, ghApi = runGhApi) => {
1450
1502
  }
1451
1503
  const findingsResult = loadFindings(input.findingsPath);
1452
1504
  if (findingsResult.kind !== "ok") {
1505
+ if (wouldBuryCompleted(true)) leaveInPlace();
1453
1506
  await upsertSticky(
1454
1507
  input.repo,
1455
1508
  prNumber,
@@ -1486,6 +1539,8 @@ var post = async (input, ghApi = runGhApi) => {
1486
1539
  );
1487
1540
  process.exit(0);
1488
1541
  }
1542
+ const thisIncomplete = envelope.incomplete === true;
1543
+ if (wouldBuryCompleted(thisIncomplete)) leaveInPlace();
1489
1544
  const findingsMarker = findingsPointer(findings, input.jsonUrl);
1490
1545
  const {
1491
1546
  comments: rawComments,
@@ -1509,6 +1564,7 @@ var post = async (input, ghApi = runGhApi) => {
1509
1564
  const commonRenderInput = {
1510
1565
  findings,
1511
1566
  envelope,
1567
+ incomplete: thisIncomplete,
1512
1568
  prices: decodedPrices.right,
1513
1569
  pricesProvided: input.pricesProvided,
1514
1570
  template,
@@ -1623,7 +1679,7 @@ var announce = async (input, ghApi = runGhApi) => {
1623
1679
  DEFAULT_MARKER,
1624
1680
  ghApi
1625
1681
  );
1626
- if (existing !== null && parseReviewedSha(existing.body) === input.headSha.toLowerCase()) {
1682
+ if (existing !== null && parseReviewComplete(existing.body) && parseReviewedSha(existing.body) === input.headSha.toLowerCase()) {
1627
1683
  process.stderr.write(
1628
1684
  `Sticky already reflects a completed review of ${input.headSha} \u2014 leaving it in place
1629
1685
  `
@@ -1829,15 +1885,21 @@ var RunCodec = t.type({
1829
1885
  conclusion: t.union([t.string, t.null]),
1830
1886
  run_number: t.number
1831
1887
  });
1832
- var RunsCodec = t.type({ workflow_runs: t.array(RunCodec) });
1888
+ var RUN_JQ = ".workflow_runs[] | {id: .id, name: .name, status: .status, conclusion: .conclusion, run_number: .run_number}";
1833
1889
  var resolveCiRun = async (repo, headSha, workflowName, ghApi) => {
1834
- const stdout = await ghApi([`repos/${repo}/actions/runs?head_sha=${headSha}&per_page=100`]);
1835
- const decoded = RunsCodec.decode(JSON.parse(stdout));
1836
- if (decoded._tag === "Left")
1837
- throw new Error(
1838
- `workflow runs for ${headSha} did not match the expected shape: ${PathReporter.report(decoded).join("; ")}`
1890
+ const endpoint = `repos/${repo}/actions/runs?head_sha=${headSha}&per_page=100`;
1891
+ const rows = parseJsonl(await ghApi([endpoint, "--paginate", "--jq", RUN_JQ]));
1892
+ const decoded = rows.map((row) => RunCodec.decode(row));
1893
+ const runs = decoded.flatMap((d) => d._tag === "Right" ? [d.right] : []);
1894
+ const dropped = decoded.length - runs.length;
1895
+ if (dropped > 0) {
1896
+ const firstDrift = decoded.find((d) => d._tag === "Left");
1897
+ const detail = firstDrift === void 0 ? "" : ` (${PathReporter.report(firstDrift).join("; ")})`;
1898
+ process.stderr.write(
1899
+ `Warning: ${String(dropped)} of ${String(rows.length)} workflow-run row(s) from ${endpoint} failed to decode${detail} \u2014 excluded from the lookup
1900
+ `
1839
1901
  );
1840
- const runs = decoded.right.workflow_runs;
1902
+ }
1841
1903
  const latest = runs.filter((r) => r.name === workflowName).reduce(
1842
1904
  (best, r) => best === null || r.run_number > best.run_number ? r : best,
1843
1905
  null
@@ -1848,21 +1910,34 @@ var resolveCiRun = async (repo, headSha, workflowName, ghApi) => {
1848
1910
  };
1849
1911
  };
1850
1912
  var awaitCiConclusion = async (repo, headSha, options, deps = { ghApi: runGhApi, sleep: defaultSleep, elapsedMs: monotonicElapsed() }) => {
1851
- const poll = async () => {
1852
- const { run, seenNames } = await resolveCiRun(repo, headSha, options.workflowName, deps.ghApi);
1853
- if (run !== null && run.status === "completed")
1854
- return { kind: "concluded", conclusion: run.conclusion ?? "unknown", runId: run.id };
1913
+ const safeResolve = async () => {
1914
+ try {
1915
+ return await resolveCiRun(repo, headSha, options.workflowName, deps.ghApi);
1916
+ } catch (err) {
1917
+ process.stderr.write(
1918
+ `Warning: CI-run lookup for ${headSha} failed (${errMsg(err)}) \u2014 retrying until the timeout
1919
+ `
1920
+ );
1921
+ return { run: null, seenNames: [] };
1922
+ }
1923
+ };
1924
+ const poll = async (lastSeenNames, lastRunId) => {
1925
+ const { run, seenNames } = await safeResolve();
1926
+ if (run !== null && run.status === "completed" && run.conclusion !== null)
1927
+ return { kind: "concluded", conclusion: run.conclusion, runId: run.id };
1928
+ const runId = run === null ? lastRunId : run.id;
1929
+ const names = seenNames.length > 0 ? seenNames : lastSeenNames;
1855
1930
  if (deps.elapsedMs() >= options.timeoutMs)
1856
- return { kind: "timed-out", runId: run === null ? null : run.id, seenNames };
1931
+ return { kind: "timed-out", runId, seenNames: names };
1857
1932
  await deps.sleep(options.pollIntervalMs);
1858
- return poll();
1933
+ return poll(names, runId);
1859
1934
  };
1860
- return poll();
1935
+ return poll([], null);
1861
1936
  };
1862
1937
  var defaultSleep = (ms) => new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
1863
1938
  var monotonicElapsed = () => {
1864
- const start = Date.now();
1865
- return () => Date.now() - start;
1939
+ const start = performance.now();
1940
+ return () => performance.now() - start;
1866
1941
  };
1867
1942
  var renderCiOutputs = (outcome) => outcome.kind === "concluded" ? `ci_settled=true
1868
1943
  ci_conclusion=${outcome.conclusion}
@@ -2345,6 +2420,7 @@ var buildEnvelope = (telemetry, native, agentFilePath, agentFileFallbackPath) =>
2345
2420
  findings: noticeFindings(`### \u26A0\uFE0F Review did not complete
2346
2421
 
2347
2422
  ${outcome.reason}`),
2423
+ incomplete: true,
2348
2424
  ...telemetry
2349
2425
  };
2350
2426
  }
@@ -3105,6 +3181,30 @@ var adaptCmd = defineCommand({
3105
3181
  `);
3106
3182
  }
3107
3183
  });
3184
+ var noticeCmd = defineCommand({
3185
+ meta: {
3186
+ name: "notice",
3187
+ description: "Emit an abstract envelope for a run that produced no completed review (security block, setup failure, unapplied diff, or empty run) \u2014 flagged incomplete so the commenter renders it honestly and won't bury a real review"
3188
+ },
3189
+ args: {
3190
+ kind: {
3191
+ type: "positional",
3192
+ description: "One of: security-blocked, setup-failed, diff-apply-failed, no-output",
3193
+ required: true
3194
+ },
3195
+ reasons: {
3196
+ type: "string",
3197
+ description: "security-blocked only: the triage's fail-closed reason string (empty/omitted \u21D2 the no-reason wording)"
3198
+ }
3199
+ },
3200
+ run: ({ args }) => {
3201
+ const kind = isNoticeKind(args.kind) ? args.kind : fail(
3202
+ `Unknown notice kind "${args.kind}" \u2014 expected one of: security-blocked, setup-failed, diff-apply-failed, no-output`
3203
+ );
3204
+ process.stdout.write(`${JSON.stringify(buildNoticeEnvelope(kind, args.reasons), null, 2)}
3205
+ `);
3206
+ }
3207
+ });
3108
3208
  var isExtractSchemaKind = (s) => s === "findings" || s === "triage";
3109
3209
  var requireExtractSchemaKind = (name) => isExtractSchemaKind(name) ? name : fail(`Unknown kind "${name}" for extract \u2014 expected one of: findings, triage`);
3110
3210
  var failClosedTriage = (outcome) => ({
@@ -3708,6 +3808,7 @@ var main = defineCommand({
3708
3808
  validate: validateCmd,
3709
3809
  "seed-draft": seedDraftCmd,
3710
3810
  adapt: adaptCmd,
3811
+ notice: noticeCmd,
3711
3812
  extract: extractCmd,
3712
3813
  "validate-patches": validatePatchesCmd,
3713
3814
  "print-schema": printSchemaCmd,