@jphutchins/code-review 0.1.0-alpha.42 → 0.1.0-alpha.43

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
@@ -100,61 +100,45 @@ links to:
100
100
  downstream tool) SHOULD base64-decode and parse that marker rather than parse the comment's prose.
101
101
  Embedding in the comment (rather than only linking the artifact) keeps the pointer from expiring
102
102
  with artifact retention; when the encoded findings are too large to embed, the sticky falls back to
103
- a `<!-- code-review:findings-json <url> -->` link marker instead (the linked `findings.json` is the
104
- agent's draft and lacks the surfaced fields below) — the shared serializer is
103
+ a `<!-- code-review:findings-json <url> -->` link marker instead. The shared serializer is
105
104
  [`src/surface.ts`](src/surface.ts).
106
105
 
107
- The embedded document is the **surfaced** findings document: the agent's findings
108
- (`schema_version` 0.6.0 contract, which is what the review agent is held to and what
109
- [`schema/findings.schema.json`](schema/findings.schema.json) validates) stamped with the
110
- `0.8.0` surface version, the pipeline-computed convergence state of the **last completed
111
- full-review round**, and the advisory `scope_metastasis` entry (issue #150):
106
+ The embedded document is the agent's **complete** findings document the same `schema_version`
107
+ 0.6.0 contract the review agent is held to and
108
+ [`schema/findings.schema.json`](schema/findings.schema.json) validates, and the same object the
109
+ comment is rendered from. It is embedded verbatim: no field is added or dropped, so the machine
110
+ channel can never carry less than, or drift from, the rendered prose.
112
111
 
113
112
  ```json
114
113
  {
115
- "schema_version": "0.8.0",
114
+ "schema_version": "0.6.0",
116
115
  "verdict": "comment",
117
116
  "summary": "...",
118
- "convergence": { "score": 1, "threshold": 1, "converged": true },
119
- "round": 4,
120
- "scope_metastasis": {
121
- "decision_prompt": "Findings keep recurring in the same mechanism across consecutive rounds — each fix keeps enabling the next finding in that machinery. This is a decision, not a directive: state in your summary whether you are committing to the expanding scope (plan the remaining facets of the recurring mechanism(s) above as one unit) or narrowing the scope so the recurrence stops.",
122
- "recurring": [
123
- { "code": "mixin-field-guard-gap", "consecutive_rounds": 4, "start_round": 1 }
124
- ]
125
- },
126
117
  "findings": []
127
118
  }
128
119
  ```
129
120
 
130
- `convergence` (`score` = critical·4 + major·2 + minor·1 + nit·0, `threshold` default 1, `converged`
131
- = score ≤ threshold, as a literal boolean) and `round` (the count of completed full-review rounds)
132
- are the deterministic **stop signal** for an iterating author-agent: `converged: true` means the
133
- last completed round is at or below the convergence tolerance, so another iteration round is not
134
- warranted. The agent never writes these fields the commenter computes them from the review's own
135
- severities at render time and they are omitted until at least one full-review round has
136
- completed. They survive the "review in progress" banner: the banner replaces only the sticky's
137
- prose and carries the embedded marker forward verbatim. `scope_metastasis` is the structured
138
- counterpart of the sticky's "Scope metastasis" warning per-code consecutive-round counts plus a
139
- decision prompt computed from the same rounds history and omitted when no mechanism recurred. It
140
- is deliberately NOT stripped when a surfaced blob feeds the re-review seed: a decoding agent — and
141
- the next-round reviewer must see the recurrence data to answer the scope decision. The 0.8.0
142
- surfaced contract applies to the **whole-document** marker only; each inline comment embeds a
143
- per-finding fragment (`schema_version` + one finding) at the draft's own version, since the
144
- fragment carries no stop signal.
145
-
146
- Semantics on non-round posts: a mechanic (CI-fix) pass or an envelope-loss post embeds the
147
- **last completed round's stored signal** (round + its own threshold never re-derived, so an
148
- operator changing `convergence_threshold` mid-PR cannot flip a stored `converged`) beside its own
149
- findings, so the signal always describes the last completed full-review round, not the findings
150
- that happen to sit beside it. A notice — a post whose verdict is `error` or whose run did not
151
- complete (empty diff, corrupt output, did-not-complete) — embeds **no** signal in its own blob:
152
- its document already says no review was produced this run, and a carried `converged` beside that
153
- would read as a stop signal for a run that produced none; the last completed round's signal still
154
- survives on the sticky in the compact `<!-- code-review:signal;base64 <base64> -->` marker, so
155
- the next post reads it back. The same compact marker is emitted when the embedded payload is too
156
- large and the whole-document marker falls back to the artifact link — an oversized review's stop
157
- signal stays readable and can be carried forward.
121
+ The deterministic **stop signal** for an iterating author-agent rides its own compact marker beside
122
+ the findings blob, `<!-- code-review:signal;base64 <base64> -->`, whose decoded payload is
123
+ `{ "schema_version": "0.8.0", "round": <n>, "convergence": { "score": <s>, "threshold": <t>, "converged": <bool> } }`.
124
+ `round` is the count of completed full-review rounds; `convergence.score` = critical·4 + major·2 +
125
+ minor·1 + nit·0, `threshold` defaults to 1, and `converged` = score threshold as a literal
126
+ boolean `converged: true` means the last completed round is at or below the tolerance, so another
127
+ iteration round is not warranted. The commenter computes the signal from the review's own severities
128
+ (the agent never writes it); it appears once at least one full-review round has completed and is
129
+ carried verbatim afterward a mechanic (CI-fix) pass or an envelope-loss notice re-emits the last
130
+ completed round's signal rather than re-deriving it, so an operator changing `convergence_threshold`
131
+ mid-PR cannot flip a stored `converged`, and a notice never fabricates a stop signal for a run that
132
+ produced no review. Both markers survive the "review in progress" banner, which replaces only the
133
+ sticky's prose and carries them forward verbatim.
134
+
135
+ Cross-round recurrence (the sticky's "Scope metastasis" warning) is carried structurally in the
136
+ `<!-- code-review:rounds;base64 <base64> -->` marker — per-round severity counts and mechanism
137
+ frequencies from which the re-review seed re-derives the advisory `scope_metastasis` entry it
138
+ hands the next-round agent. It is deliberately NOT embedded in the findings blob: the blob is the
139
+ agent's own document, and a recurrence claim is round state the commenter owns. Each inline review
140
+ comment embeds only its own finding (a `schema_version` + one-finding fragment), so the sticky and
141
+ review-body markers remain the whole-document source of truth.
158
142
  - **`code-review-transcript`** — the full Claude Code session transcripts for the triage and review
159
143
  phases. This is advisory/auditability only: it is never read by the comment job and never affects
160
144
  what gets posted.
package/dist/index.js CHANGED
@@ -598,28 +598,15 @@ var convergenceSignal = (counts, threshold = DEFAULT_CONVERGENCE_THRESHOLD) => {
598
598
  return { score, threshold, converged: score <= threshold };
599
599
  };
600
600
  var signalForRound = (round, counts, threshold = DEFAULT_CONVERGENCE_THRESHOLD) => ({ round, convergence: convergenceSignal(counts, threshold) });
601
- var surfaceFindings = (findings, signal, scopeMetastasis = null) => {
602
- const agentDoc = Object.fromEntries(
603
- Object.entries(findings).filter(
604
- ([key2]) => key2 !== "round" && key2 !== "convergence" && key2 !== "scope_metastasis"
605
- )
606
- );
607
- return {
608
- ...agentDoc,
609
- schema_version: SURFACE_SCHEMA_VERSION,
610
- ...signal === null ? {} : signal,
611
- ...scopeMetastasis === null ? {} : { scope_metastasis: scopeMetastasis }
612
- };
613
- };
614
601
  var signalMarker = (signal) => `<!-- code-review:signal;base64 ${Buffer.from(
615
602
  JSON.stringify({ schema_version: SURFACE_SCHEMA_VERSION, ...signal }),
616
603
  "utf-8"
617
604
  ).toString("base64")} -->`;
618
- var surfacedFindingsPointer = (findings, signal, jsonUrl, scopeMetastasis = null) => {
619
- const marker = findingsPointer(surfaceFindings(findings, signal, scopeMetastasis), jsonUrl);
620
- if (signal === null || marker.includes("<!-- code-review:findings-json;base64 ")) return marker;
621
- return marker === "" ? signalMarker(signal) : `${marker}
622
- ${signalMarker(signal)}`;
605
+ var joinSignalMarker = (base, marker) => base === "" ? marker : `${base}
606
+ ${marker}`;
607
+ var surfacedFindingsPointer = (findings, signal, jsonUrl) => {
608
+ const marker = findingsPointer(findings, jsonUrl);
609
+ return signal === null ? marker : joinSignalMarker(marker, signalMarker(signal));
623
610
  };
624
611
  var SIGNAL_RE = /<!-- code-review:signal;base64 ([A-Za-z0-9+/=]+) -->/;
625
612
  var parseSignalMarker = (body) => {
@@ -630,8 +617,7 @@ var parseSignalMarker = (body) => {
630
617
  var parseSurfaceSignal = (doc) => {
631
618
  if (typeof doc !== "object" || doc === null || Array.isArray(doc)) return null;
632
619
  const o = doc;
633
- const declared = o["schema_version"];
634
- if (typeof declared !== "string" || !SURFACE_SCHEMA_VERSIONS.includes(declared)) return null;
620
+ if (!isSurfaceVersion(o["schema_version"])) return null;
635
621
  const round = o["round"];
636
622
  const convergence = o["convergence"];
637
623
  if (typeof round !== "number" || !Number.isSafeInteger(round) || round < 1) return null;
@@ -646,11 +632,11 @@ var parseSurfaceSignal = (doc) => {
646
632
  };
647
633
  };
648
634
  var SURFACE_SCHEMA_VERSIONS = ["0.7.0", SURFACE_SCHEMA_VERSION];
635
+ var isSurfaceVersion = (version) => typeof version === "string" && SURFACE_SCHEMA_VERSIONS.includes(version);
649
636
  var stripSurfaceFields = (doc) => {
650
637
  if (typeof doc !== "object" || doc === null || Array.isArray(doc)) return doc;
651
638
  const o = doc;
652
- const declared = o["schema_version"];
653
- if (typeof declared !== "string" || !SURFACE_SCHEMA_VERSIONS.includes(declared)) return doc;
639
+ if (!isSurfaceVersion(o["schema_version"])) return doc;
654
640
  const rest = Object.fromEntries(
655
641
  Object.entries(o).filter(([key2]) => key2 !== "convergence" && key2 !== "round")
656
642
  );
@@ -1059,7 +1045,6 @@ var render = (input) => {
1059
1045
  const isFullReviewRound = (input.convergenceRound ?? (isConvergenceRound(route, incomplete) && rounds.length > 0)) && isReviewVerdict(input.findings.verdict);
1060
1046
  const convergenceCounts = rounds[rounds.length - 1] ?? computeRoundCounts(input.findings);
1061
1047
  const advisoryAllowed = isFullReviewRound;
1062
- const scopeMetastasis = input.scopeMetastasis !== void 0 ? input.scopeMetastasis : advisoryAllowed ? computeScopeMetastasis(rounds) : null;
1063
1048
  return eta.renderString(input.template, {
1064
1049
  findings: input.findings,
1065
1050
  envelope: input.envelope,
@@ -1090,8 +1075,7 @@ var render = (input) => {
1090
1075
  // label does; post always supplies the marker, so this path cannot disagree with it in
1091
1076
  // production (issue #141 review r4).
1092
1077
  isFullReviewRound && rounds.length > 0 ? signalForRound(rounds.length, convergenceCounts, input.convergenceThreshold) : null,
1093
- input.jsonUrl,
1094
- scopeMetastasis
1078
+ input.jsonUrl
1095
1079
  ),
1096
1080
  roundsMarker: roundsMarker(rounds),
1097
1081
  roundsSummary: roundsSummary(rounds, input.roundCount),
@@ -2273,10 +2257,9 @@ var post = async (input, ghApi = runGhApi) => {
2273
2257
  const emptyMechanicWouldBury = (route, incomplete) => route === "mechanic" && !incomplete && findings.findings.length === 0 && existingSticky !== null && priorIsFullReview(existingSticky.body);
2274
2258
  const priorRounds = existingSticky !== null ? parseRounds(existingSticky.body) : [];
2275
2259
  const priorSignal = existingSticky === null ? null : parseSignalMarker(existingSticky.body) ?? parseSurfaceSignal(parseFindingsMarker(existingSticky.body));
2276
- const findingsMarkerFor = (findings2, signal2, scopeMetastasis2) => {
2277
- const pointer = surfacedFindingsPointer(findings2, signal2, input.jsonUrl, scopeMetastasis2);
2278
- return signal2 !== null || priorSignal === null ? pointer : `${pointer}
2279
- ${signalMarker(priorSignal)}`;
2260
+ const findingsMarkerFor = (findings2, signal2) => {
2261
+ const pointer = surfacedFindingsPointer(findings2, signal2, input.jsonUrl);
2262
+ return signal2 !== null || priorSignal === null ? pointer : joinSignalMarker(pointer, signalMarker(priorSignal));
2280
2263
  };
2281
2264
  const leaveInPlace = (message) => {
2282
2265
  process.stderr.write(
@@ -2430,8 +2413,8 @@ ${dropNote}` : ""}`,
2430
2413
  inlineDisposition: { kind: "no-envelope" },
2431
2414
  runUrl: input.runUrl,
2432
2415
  jsonUrl: input.jsonUrl,
2433
- // Same signal rule as the main path: only a completed-review doc carries the prior signal
2434
- // in its blob; an error-verdict doc preserves it in the compact marker instead.
2416
+ // Same signal rule as the main path: a completed-review doc and an error-verdict doc alike
2417
+ // ride the prior signal on the compact marker, never inside the blob.
2435
2418
  findingsPointer: findingsMarkerFor(
2436
2419
  findings,
2437
2420
  isReviewVerdict(findings.verdict) ? priorSignal : null
@@ -2490,19 +2473,18 @@ ${dropNote}` : ""}`,
2490
2473
  )
2491
2474
  ] : priorRounds;
2492
2475
  const signal = isRound ? signalForRound(roundNumber, computeRoundCounts(findings), input.convergenceThreshold) : thisIncomplete || !isReviewVerdict(findings.verdict) ? null : priorSignal;
2493
- const scopeMetastasis = isRound ? computeScopeMetastasis(rounds) : null;
2494
- const findingsMarker = findingsMarkerFor(findings, signal, scopeMetastasis);
2495
- const markerForm = findingsMarkerForm(
2496
- surfaceFindings(findings, signal, scopeMetastasis),
2497
- input.jsonUrl
2498
- );
2476
+ const findingsMarker = findingsMarkerFor(findings, signal);
2477
+ const markerForm = findingsMarkerForm(findings, input.jsonUrl);
2478
+ const signalNote = signal !== null || priorSignal !== null ? " (the stop signal still rides the compact marker)" : "";
2499
2479
  if (markerForm === "link") {
2500
2480
  process.stderr.write(
2501
- "Warning: the findings-json marker exceeds the embed limit \u2014 degraded to the jsonUrl-link form; a decoding agent must fetch the artifact instead of the embedded JSON\n"
2481
+ `Warning: the findings-json blob exceeds the embed limit \u2014 degraded to the jsonUrl-link form; a decoding agent must fetch the artifact for the findings${signalNote}
2482
+ `
2502
2483
  );
2503
2484
  } else if (markerForm === "omitted") {
2504
2485
  process.stderr.write(
2505
- "Warning: the findings-json marker exceeds the embed limit and no --json-url was given \u2014 the machine-readable channel is omitted from the posted surfaces\n"
2486
+ `Warning: the findings-json blob exceeds the embed limit and no --json-url was given \u2014 the embedded findings seed is dropped from the posted surfaces${signalNote}
2487
+ `
2506
2488
  );
2507
2489
  }
2508
2490
  const commonRenderInput = {
@@ -2521,7 +2503,6 @@ ${dropNote}` : ""}`,
2521
2503
  sameRootNotes,
2522
2504
  answeredNotes: reRaisedNotes,
2523
2505
  answeredReRaiseNote: answeredDropNote,
2524
- scopeMetastasis,
2525
2506
  roundCount: signal?.round ?? priorSignal?.round ?? priorRounds.length,
2526
2507
  convergenceThreshold: input.convergenceThreshold,
2527
2508
  convergenceRound: isRound,
@@ -4166,6 +4147,8 @@ ${printableSchema(schemaPath)}
4166
4147
  }
4167
4148
  }
4168
4149
  });
4150
+ var isSurfaceStampedDoc = (doc) => typeof doc === "object" && doc !== null && !Array.isArray(doc) && doc["schema_version"] === SURFACE_SCHEMA_VERSION;
4151
+ var withoutScopeMetastasis = (doc) => typeof doc === "object" && doc !== null && !Array.isArray(doc) ? Object.fromEntries(Object.entries(doc).filter(([key2]) => key2 !== "scope_metastasis")) : doc;
4169
4152
  var seedDraftCmd = defineCommand({
4170
4153
  meta: {
4171
4154
  name: "seed-draft",
@@ -4239,7 +4222,10 @@ var seedDraftCmd = defineCommand({
4239
4222
  })();
4240
4223
  return typeof raw === "object" && raw !== null && "body" in raw && typeof raw.body === "string" ? raw.body : null;
4241
4224
  })();
4242
- const strippedPrior = priorBody === null ? null : stripSurfaceFields(parseFindingsMarker(priorBody));
4225
+ const parsedPrior = priorBody === null ? null : parseFindingsMarker(priorBody);
4226
+ const strippedPrior = parsedPrior === null ? null : stripSurfaceFields(
4227
+ isSurfaceStampedDoc(parsedPrior) ? parsedPrior : withoutScopeMetastasis(parsedPrior)
4228
+ );
4243
4229
  const priorFindings = (() => {
4244
4230
  if (strippedPrior === null || typeof strippedPrior !== "object" || Array.isArray(strippedPrior))
4245
4231
  return strippedPrior;