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

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,49 @@ 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.9.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.9.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` sums each finding and
125
+ systemic problem's `floor(severity) + max(0, ceiling floor) × confidence` (ceilings critical 4 ·
126
+ major 2 · minor 1 · nit 0; floors critical `threshold + 0.01` · major 0.5 · else 0), rounded to 2
127
+ decimals; `threshold` defaults to 1, and `converged` = score threshold as a literal
128
+ boolean `converged: true` means the last completed round is at or below the tolerance, so another
129
+ iteration round is not warranted. The commenter computes the signal from the review's own severities
130
+ (the agent never writes it); it appears once at least one full-review round has completed and is
131
+ carried verbatim afterward a mechanic (CI-fix) pass or an envelope-loss notice re-emits the last
132
+ completed round's signal rather than re-deriving it, so an operator changing `convergence_threshold`
133
+ mid-PR cannot flip a stored `converged`, and a notice never fabricates a stop signal for a run that
134
+ produced no review. Both markers survive the "review in progress" banner, which replaces only the
135
+ sticky's prose and carries them forward verbatim.
136
+
137
+ Cross-round recurrence (the sticky's "Scope metastasis" warning) is carried structurally in the
138
+ `<!-- code-review:rounds;base64 <base64> -->` marker — per-round severity counts and mechanism
139
+ frequencies from which the re-review seed re-derives the advisory `scope_metastasis` entry it
140
+ hands the next-round agent. It is deliberately NOT embedded in the findings blob: the blob is the
141
+ agent's own document, and a recurrence claim is round state the commenter owns. Each inline review
142
+ comment embeds only its own finding (a `schema_version` + one-finding fragment), and the
143
+ review-object body only links the sticky so the **sticky is the sole documented decode surface**
144
+ for the whole-document marker; a decoding agent reads it there. The review body is written only after
145
+ the sticky exists (a failed sticky write aborts the run first), so it never carries the blob itself.
158
146
  - **`code-review-transcript`** — the full Claude Code session transcripts for the triage and review
159
147
  phases. This is advisory/auditability only: it is never read by the comment job and never affects
160
148
  what gets posted.
package/dist/index.js CHANGED
@@ -31,6 +31,7 @@ var LineNumber = t.refinement(
31
31
  "LineNumber"
32
32
  );
33
33
  var Confidence = t.refinement(t.number, (n) => n >= 0 && n <= 1, "Confidence");
34
+ var Likelihood = t.refinement(t.number, (n) => n >= 0 && n <= 1, "Likelihood");
34
35
  var SCHEMA_VERSION_RE = /^(0|[1-9]\d*)\.(\d+)\.(\d+)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
35
36
  var SchemaVersion = t.refinement(
36
37
  t.string,
@@ -55,7 +56,8 @@ var FindingShape = t.intersection([
55
56
  title: t.string,
56
57
  description: t.string,
57
58
  reasoning: t.string,
58
- confidence: Confidence
59
+ confidence: Confidence,
60
+ likelihood: Likelihood
59
61
  }),
60
62
  FindingRuleCodec,
61
63
  t.partial({
@@ -75,7 +77,8 @@ var SystemicRequired = t.type({
75
77
  description: t.string,
76
78
  severity: SeverityCodec,
77
79
  reasoning: t.string,
78
- confidence: Confidence
80
+ confidence: Confidence,
81
+ likelihood: Likelihood
79
82
  });
80
83
  var SystemicOptional = t.partial({
81
84
  finding_codes: t.array(t.string),
@@ -202,7 +205,7 @@ var TestSummaryCodec = t.intersection([
202
205
  failures: t.array(TestFailureCodec)
203
206
  })
204
207
  ]);
205
- var DEFAULT_SCHEMA_VERSION = "0.6.0";
208
+ var DEFAULT_SCHEMA_VERSION = "0.9.0";
206
209
  var incompleteFindings = (summary) => ({
207
210
  schema_version: DEFAULT_SCHEMA_VERSION,
208
211
  summary,
@@ -585,41 +588,39 @@ var computeSameRootNotes = (priorRounds, findings, currentSha) => {
585
588
  }
586
589
  return Object.fromEntries(entries);
587
590
  };
588
- var CONVERGENCE_WEIGHTS = { critical: 4, major: 2, minor: 1, nit: 0 };
591
+ var CONVERGENCE_CEILINGS = { critical: 4, major: 2, minor: 1, nit: 0 };
592
+ var CRITICAL_FLOOR_MARGIN = 0.01;
589
593
  var DEFAULT_CONVERGENCE_THRESHOLD = 1;
590
- var convergenceScore = (counts) => SEVERITIES.reduce((sum, k) => sum + counts[k] * CONVERGENCE_WEIGHTS[k], 0);
591
- var convergenceSummary = (counts, threshold = DEFAULT_CONVERGENCE_THRESHOLD) => {
592
- const { score, converged } = convergenceSignal(counts, threshold);
594
+ var convergenceFloor = (severity, threshold) => severity === "critical" ? threshold + CRITICAL_FLOOR_MARGIN : severity === "major" ? 0.5 : 0;
595
+ var round2 = (n) => Math.round((n + Number.EPSILON) * 100) / 100;
596
+ var convergenceScore = (doc, threshold) => round2(
597
+ [...doc.findings, ...doc.systemic_problems ?? []].reduce(
598
+ (sum, { severity, confidence, likelihood }) => {
599
+ const floor = convergenceFloor(severity, threshold);
600
+ return sum + floor + Math.max(0, CONVERGENCE_CEILINGS[severity] - floor) * confidence * likelihood;
601
+ },
602
+ 0
603
+ )
604
+ );
605
+ var convergenceSummary = (doc, threshold = DEFAULT_CONVERGENCE_THRESHOLD) => {
606
+ const { score, converged } = convergenceSignal(doc, threshold);
593
607
  return converged ? `**Convergence** \u{1F3C1} ${String(score)} \u2264 ${String(threshold)} \u2014 converged` : `**Convergence** \u{1F504} ${String(score)} > ${String(threshold)} \u2014 iterating`;
594
608
  };
595
609
  var SURFACE_SCHEMA_VERSION = "0.8.0";
596
- var convergenceSignal = (counts, threshold = DEFAULT_CONVERGENCE_THRESHOLD) => {
597
- const score = convergenceScore(counts);
610
+ var convergenceSignal = (doc, threshold = DEFAULT_CONVERGENCE_THRESHOLD) => {
611
+ const score = convergenceScore(doc, threshold);
598
612
  return { score, threshold, converged: score <= threshold };
599
613
  };
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
+ var signalForRound = (round, doc, threshold = DEFAULT_CONVERGENCE_THRESHOLD) => ({ round, convergence: convergenceSignal(doc, threshold) });
614
615
  var signalMarker = (signal) => `<!-- code-review:signal;base64 ${Buffer.from(
615
616
  JSON.stringify({ schema_version: SURFACE_SCHEMA_VERSION, ...signal }),
616
617
  "utf-8"
617
618
  ).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)}`;
619
+ var joinSignalMarker = (base, marker) => base === "" ? marker : `${base}
620
+ ${marker}`;
621
+ var surfacedFindingsPointer = (findings, signal, jsonUrl) => {
622
+ const marker = findingsPointer(findings, jsonUrl);
623
+ return signal === null ? marker : joinSignalMarker(marker, signalMarker(signal));
623
624
  };
624
625
  var SIGNAL_RE = /<!-- code-review:signal;base64 ([A-Za-z0-9+/=]+) -->/;
625
626
  var parseSignalMarker = (body) => {
@@ -630,8 +631,7 @@ var parseSignalMarker = (body) => {
630
631
  var parseSurfaceSignal = (doc) => {
631
632
  if (typeof doc !== "object" || doc === null || Array.isArray(doc)) return null;
632
633
  const o = doc;
633
- const declared = o["schema_version"];
634
- if (typeof declared !== "string" || !SURFACE_SCHEMA_VERSIONS.includes(declared)) return null;
634
+ if (!isSurfaceVersion(o["schema_version"])) return null;
635
635
  const round = o["round"];
636
636
  const convergence = o["convergence"];
637
637
  if (typeof round !== "number" || !Number.isSafeInteger(round) || round < 1) return null;
@@ -646,11 +646,11 @@ var parseSurfaceSignal = (doc) => {
646
646
  };
647
647
  };
648
648
  var SURFACE_SCHEMA_VERSIONS = ["0.7.0", SURFACE_SCHEMA_VERSION];
649
+ var isSurfaceVersion = (version) => typeof version === "string" && SURFACE_SCHEMA_VERSIONS.includes(version);
649
650
  var stripSurfaceFields = (doc) => {
650
651
  if (typeof doc !== "object" || doc === null || Array.isArray(doc)) return doc;
651
652
  const o = doc;
652
- const declared = o["schema_version"];
653
- if (typeof declared !== "string" || !SURFACE_SCHEMA_VERSIONS.includes(declared)) return doc;
653
+ if (!isSurfaceVersion(o["schema_version"])) return doc;
654
654
  const rest = Object.fromEntries(
655
655
  Object.entries(o).filter(([key2]) => key2 !== "convergence" && key2 !== "round")
656
656
  );
@@ -674,12 +674,9 @@ var projectPatch = (patch) => {
674
674
  return typeof lowered === "string" ? { kind: "suggestion", text: escapeFence(lowered) } : { kind: "patch", raw: escapeFence(patch) };
675
675
  };
676
676
  var formatConfidence = (n) => n.toFixed(2);
677
- var reviewBodyPointer = (headSha, stickyUrl, marker) => {
677
+ var reviewBodyPointer = (headSha, stickyUrl) => {
678
678
  const sha7 = headSha.slice(0, 7);
679
- const linkLine = stickyUrl ? `\u{1F916} Automated code review for \`${sha7}\` \u2014 see the [summary comment](${stickyUrl}) for the verdict, walkthrough, and cost.` : `\u{1F916} Automated code review for \`${sha7}\` \u2014 see the summary comment for the verdict, walkthrough, and cost.`;
680
- return marker ? `${marker}
681
-
682
- ${linkLine}` : linkLine;
679
+ return stickyUrl ? `\u{1F916} Automated code review for \`${sha7}\` \u2014 see the [summary comment](${stickyUrl}) for the verdict, walkthrough, and cost.` : `\u{1F916} Automated code review for \`${sha7}\` \u2014 see the summary comment for the verdict, walkthrough, and cost.`;
683
680
  };
684
681
  var asRecord = (u) => typeof u === "object" && u !== null && !Array.isArray(u) ? u : null;
685
682
  var errMsg = (e) => e instanceof Error ? e.message : String(e);
@@ -1057,9 +1054,7 @@ var render = (input) => {
1057
1054
  const rounds = input.rounds ?? [];
1058
1055
  const sameRootNotes = input.sameRootNotes ?? computeSameRootNotes(rounds.slice(0, -1), input.findings.findings);
1059
1056
  const isFullReviewRound = (input.convergenceRound ?? (isConvergenceRound(route, incomplete) && rounds.length > 0)) && isReviewVerdict(input.findings.verdict);
1060
- const convergenceCounts = rounds[rounds.length - 1] ?? computeRoundCounts(input.findings);
1061
1057
  const advisoryAllowed = isFullReviewRound;
1062
- const scopeMetastasis = input.scopeMetastasis !== void 0 ? input.scopeMetastasis : advisoryAllowed ? computeScopeMetastasis(rounds) : null;
1063
1058
  return eta.renderString(input.template, {
1064
1059
  findings: input.findings,
1065
1060
  envelope: input.envelope,
@@ -1075,7 +1070,7 @@ var render = (input) => {
1075
1070
  reviewedSha: input.reviewedSha ?? "0000000000000000000000000000000000000000",
1076
1071
  postedAt: input.postedAt ?? "",
1077
1072
  severityCounts,
1078
- convergenceSummary: isFullReviewRound ? convergenceSummary(convergenceCounts, input.convergenceThreshold) : "",
1073
+ convergenceSummary: isFullReviewRound ? convergenceSummary(input.findings, input.convergenceThreshold) : "",
1079
1074
  strays: (input.strays ?? []).map((f) => sanitizeFinding(f, input.answeredNotes)),
1080
1075
  systemic: (input.findings.systemic_problems ?? []).map(sanitizeSystemic),
1081
1076
  unanchoredCount: input.unanchoredCount ?? 0,
@@ -1085,13 +1080,12 @@ var render = (input) => {
1085
1080
  findingsPointer: input.findingsPointer ?? surfacedFindingsPointer(
1086
1081
  input.findings,
1087
1082
  // The fallback embeds a signal exactly when the badge renders — never beside a suppressed
1088
- // badge, and from the same counts the badge reads. It assumes a post-style history (the
1083
+ // badge, and scores the same findings the badge does. It assumes a post-style history (the
1089
1084
  // caller appends this run's counts last), numbering the round exactly as the trajectory
1090
1085
  // label does; post always supplies the marker, so this path cannot disagree with it in
1091
1086
  // production (issue #141 review r4).
1092
- isFullReviewRound && rounds.length > 0 ? signalForRound(rounds.length, convergenceCounts, input.convergenceThreshold) : null,
1093
- input.jsonUrl,
1094
- scopeMetastasis
1087
+ isFullReviewRound && rounds.length > 0 ? signalForRound(rounds.length, input.findings, input.convergenceThreshold) : null,
1088
+ input.jsonUrl
1095
1089
  ),
1096
1090
  roundsMarker: roundsMarker(rounds),
1097
1091
  roundsSummary: roundsSummary(rounds, input.roundCount),
@@ -1672,6 +1666,14 @@ var findingsTable = [
1672
1666
  },
1673
1667
  {
1674
1668
  minor: "0.6",
1669
+ defaultVersion: "0.6.0",
1670
+ schemaFile: "findings.schema.json",
1671
+ codec: FindingsCodec,
1672
+ normalize: identity,
1673
+ latest: false
1674
+ },
1675
+ {
1676
+ minor: "0.9",
1675
1677
  defaultVersion: DEFAULT_SCHEMA_VERSION,
1676
1678
  schemaFile: "findings.schema.json",
1677
1679
  codec: FindingsCodec,
@@ -2042,8 +2044,8 @@ var commentPayload = (c) => ({
2042
2044
  ...c.start_line !== void 0 && c.start_side !== void 0 ? { start_line: c.start_line, start_side: c.start_side } : {},
2043
2045
  body: formatMarkdown(c.body)
2044
2046
  });
2045
- var postInlineReview = async (repo, prNumber, headSha, comments, inDiff, stickyUrl, marker, ghApi) => {
2046
- const pointer = reviewBodyPointer(headSha, stickyUrl, marker);
2047
+ var postInlineReview = async (repo, prNumber, headSha, comments, inDiff, stickyUrl, ghApi) => {
2048
+ const pointer = reviewBodyPointer(headSha, stickyUrl);
2047
2049
  const reviewBody = (withComments) => JSON.stringify({
2048
2050
  body: pointer,
2049
2051
  commit_id: headSha,
@@ -2273,10 +2275,9 @@ var post = async (input, ghApi = runGhApi) => {
2273
2275
  const emptyMechanicWouldBury = (route, incomplete) => route === "mechanic" && !incomplete && findings.findings.length === 0 && existingSticky !== null && priorIsFullReview(existingSticky.body);
2274
2276
  const priorRounds = existingSticky !== null ? parseRounds(existingSticky.body) : [];
2275
2277
  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)}`;
2278
+ const findingsMarkerFor = (findings2, signal2) => {
2279
+ const pointer = surfacedFindingsPointer(findings2, signal2, input.jsonUrl);
2280
+ return signal2 !== null || priorSignal === null ? pointer : joinSignalMarker(pointer, signalMarker(priorSignal));
2280
2281
  };
2281
2282
  const leaveInPlace = (message) => {
2282
2283
  process.stderr.write(
@@ -2430,8 +2431,8 @@ ${dropNote}` : ""}`,
2430
2431
  inlineDisposition: { kind: "no-envelope" },
2431
2432
  runUrl: input.runUrl,
2432
2433
  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.
2434
+ // Same signal rule as the main path: a completed-review doc and an error-verdict doc alike
2435
+ // ride the prior signal on the compact marker, never inside the blob.
2435
2436
  findingsPointer: findingsMarkerFor(
2436
2437
  findings,
2437
2438
  isReviewVerdict(findings.verdict) ? priorSignal : null
@@ -2489,20 +2490,19 @@ ${dropNote}` : ""}`,
2489
2490
  roundNumber
2490
2491
  )
2491
2492
  ] : priorRounds;
2492
- 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
- );
2493
+ const signal = isRound ? signalForRound(roundNumber, findings, input.convergenceThreshold) : thisIncomplete || !isReviewVerdict(findings.verdict) ? null : priorSignal;
2494
+ const findingsMarker = findingsMarkerFor(findings, signal);
2495
+ const markerForm = findingsMarkerForm(findings, input.jsonUrl);
2496
+ const signalNote = signal !== null || priorSignal !== null ? " (the stop signal still rides the compact marker)" : "";
2499
2497
  if (markerForm === "link") {
2500
2498
  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"
2499
+ `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}
2500
+ `
2502
2501
  );
2503
2502
  } else if (markerForm === "omitted") {
2504
2503
  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"
2504
+ `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}
2505
+ `
2506
2506
  );
2507
2507
  }
2508
2508
  const commonRenderInput = {
@@ -2521,7 +2521,6 @@ ${dropNote}` : ""}`,
2521
2521
  sameRootNotes,
2522
2522
  answeredNotes: reRaisedNotes,
2523
2523
  answeredReRaiseNote: answeredDropNote,
2524
- scopeMetastasis,
2525
2524
  roundCount: signal?.round ?? priorSignal?.round ?? priorRounds.length,
2526
2525
  convergenceThreshold: input.convergenceThreshold,
2527
2526
  convergenceRound: isRound,
@@ -2570,7 +2569,6 @@ ${dropNote}` : ""}`,
2570
2569
  comments,
2571
2570
  inDiff,
2572
2571
  stickyRef?.url,
2573
- findingsMarker,
2574
2572
  ghApi
2575
2573
  );
2576
2574
  process.stderr.write(
@@ -3683,7 +3681,7 @@ var resolvePrices = (pricesArg) => {
3683
3681
  return { kind: "absent", path: bundledPath("schema", "prices.example.json") };
3684
3682
  };
3685
3683
  var TEST_REPORT_DESCRIPTION = 'Path to a JSON test summary: {"passed": number, "failed": number, "total": number, "failures"?: [{"name": string, "message"?: string}]}';
3686
- 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)";
3684
+ var CONVERGENCE_THRESHOLD_DESCRIPTION = "Advisory convergence tolerance: the per-finding convergence score (each finding's floor + confidence-weighted headroom; ceilings critical 4 \xB7 major 2 \xB7 minor 1 \xB7 nit 0) at or below which the sticky reads as converged (default: 1)";
3687
3685
  var renderCmd = defineCommand({
3688
3686
  meta: {
3689
3687
  name: "render",
@@ -4166,6 +4164,8 @@ ${printableSchema(schemaPath)}
4166
4164
  }
4167
4165
  }
4168
4166
  });
4167
+ var isSurfaceStampedDoc = (doc) => typeof doc === "object" && doc !== null && !Array.isArray(doc) && doc["schema_version"] === SURFACE_SCHEMA_VERSION;
4168
+ var withoutScopeMetastasis = (doc) => typeof doc === "object" && doc !== null && !Array.isArray(doc) ? Object.fromEntries(Object.entries(doc).filter(([key2]) => key2 !== "scope_metastasis")) : doc;
4169
4169
  var seedDraftCmd = defineCommand({
4170
4170
  meta: {
4171
4171
  name: "seed-draft",
@@ -4239,7 +4239,10 @@ var seedDraftCmd = defineCommand({
4239
4239
  })();
4240
4240
  return typeof raw === "object" && raw !== null && "body" in raw && typeof raw.body === "string" ? raw.body : null;
4241
4241
  })();
4242
- const strippedPrior = priorBody === null ? null : stripSurfaceFields(parseFindingsMarker(priorBody));
4242
+ const parsedPrior = priorBody === null ? null : parseFindingsMarker(priorBody);
4243
+ const strippedPrior = parsedPrior === null ? null : stripSurfaceFields(
4244
+ isSurfaceStampedDoc(parsedPrior) ? parsedPrior : withoutScopeMetastasis(parsedPrior)
4245
+ );
4243
4246
  const priorFindings = (() => {
4244
4247
  if (strippedPrior === null || typeof strippedPrior !== "object" || Array.isArray(strippedPrior))
4245
4248
  return strippedPrior;