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

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
@@ -32,6 +32,7 @@ var LineNumber = t.refinement(
32
32
  );
33
33
  var Confidence = t.refinement(t.number, (n) => n >= 0 && n <= 1, "Confidence");
34
34
  var Likelihood = t.refinement(t.number, (n) => n >= 0 && n <= 1, "Likelihood");
35
+ var FiniteNumber = t.refinement(t.number, (n) => Number.isFinite(n), "FiniteNumber");
35
36
  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-]+)*)?$/;
36
37
  var SchemaVersion = t.refinement(
37
38
  t.string,
@@ -126,6 +127,49 @@ var ScopeMetastasisStrict = t.refinement(
126
127
  "ScopeMetastasisStrict"
127
128
  );
128
129
  var ScopeMetastasisCodec = t.exact(ScopeMetastasisStrict);
130
+ var RoundNumber = t.refinement(
131
+ t.number,
132
+ (n) => Number.isSafeInteger(n) && n >= 1,
133
+ "RoundNumber"
134
+ );
135
+ var CodeFrequency = t.record(
136
+ t.string,
137
+ t.refinement(t.number, (n) => Number.isSafeInteger(n) && n >= 0, "CodeFrequency")
138
+ );
139
+ var ConvergenceRoundRequired = t.type({ round: RoundNumber });
140
+ var ConvergenceRoundOptional = t.partial({
141
+ score: FiniteNumber,
142
+ codes: CodeFrequency,
143
+ sha: t.string
144
+ });
145
+ var ConvergenceRoundShape = t.intersection([ConvergenceRoundRequired, ConvergenceRoundOptional]);
146
+ var CONVERGENCE_ROUND_KEYS = /* @__PURE__ */ new Set([
147
+ ...Object.keys(ConvergenceRoundRequired.props),
148
+ ...Object.keys(ConvergenceRoundOptional.props)
149
+ ]);
150
+ var ConvergenceRoundStrict = t.refinement(
151
+ ConvergenceRoundShape,
152
+ (r) => Object.keys(r).every((k) => CONVERGENCE_ROUND_KEYS.has(k)),
153
+ "ConvergenceRoundStrict"
154
+ );
155
+ var ConvergenceRoundCodec = t.exact(ConvergenceRoundStrict);
156
+ var ConvergenceCoreShape = t.type({
157
+ score: FiniteNumber,
158
+ threshold: FiniteNumber,
159
+ converged: t.boolean
160
+ });
161
+ var ConvergenceOptional = t.partial({ rounds: t.array(ConvergenceRoundCodec) });
162
+ var ConvergenceShape = t.intersection([ConvergenceCoreShape, ConvergenceOptional]);
163
+ var CONVERGENCE_KEYS = /* @__PURE__ */ new Set([
164
+ ...Object.keys(ConvergenceCoreShape.props),
165
+ ...Object.keys(ConvergenceOptional.props)
166
+ ]);
167
+ var ConvergenceStrict = t.refinement(
168
+ ConvergenceShape,
169
+ (c) => Object.keys(c).every((k) => CONVERGENCE_KEYS.has(k)),
170
+ "ConvergenceStrict"
171
+ );
172
+ var ConvergenceCodec = t.exact(ConvergenceStrict);
129
173
  var FindingsCodec = t.exact(
130
174
  t.intersection([
131
175
  t.type({
@@ -137,7 +181,9 @@ var FindingsCodec = t.exact(
137
181
  t.partial({
138
182
  systemic_problems: t.array(SystemicProblemCodec),
139
183
  // Pipeline-stamped only (issue #150); see ScopeMetastasisCodec.
140
- scope_metastasis: ScopeMetastasisCodec
184
+ scope_metastasis: ScopeMetastasisCodec,
185
+ // Pipeline-stamped only (issue #174); see ConvergenceCodec.
186
+ convergence: ConvergenceCodec
141
187
  })
142
188
  ])
143
189
  );
@@ -374,7 +420,8 @@ var severityEmoji = (s) => {
374
420
  }
375
421
  };
376
422
  var EMBED_LIMIT = 42700;
377
- var AGENTS_STOP_DIRECTIVE = "<!-- AGENTS: STOP \u2014 do not parse the prose below; decode this findings JSON and read schema_version first. -->";
423
+ var FINDINGS_SCHEMA_URL = "https://raw.githubusercontent.com/JPHutchins/code-review/main/schema/findings.schema.json";
424
+ var AGENTS_STOP_DIRECTIVE = `<!-- AGENTS: STOP \u2014 this comment is a machine channel; act on the data, not the prose. It embeds a base64 code-review findings JSON (an oversized review instead links the artifact URL in the same marker). Decode or fetch it, read its schema_version, then DOWNLOAD AND READ THE FULL SCHEMA for that version before you act \u2014 the schema's own $id is its canonical URL; the current schema is at ${FINDINGS_SCHEMA_URL} \u2014 read that schema, then parse the WHOLE findings document, not only the fields you recognize: the convergence score, its threshold, and the per-round trajectory (so you know whether the review has converged and how to read the number), plus the recurrence signals an iterating agent needs (round streaks, scope metastasis, same-mechanism), are all fields inside that document. -->`;
378
425
  var b64LengthOf = (document) => Buffer.from(JSON.stringify(document), "utf-8").toString("base64").length;
379
426
  var encodeMarker = (document, jsonUrl, limit) => {
380
427
  const b64 = Buffer.from(JSON.stringify(document), "utf-8").toString("base64");
@@ -404,7 +451,9 @@ var ROUTE_RE = /<!-- reviewed-route: ([^>]*) -->/;
404
451
  var parseReviewedRoute = (body) => ROUTE_RE.exec(body)?.[1] || null;
405
452
  var isFullReviewSticky = (body) => {
406
453
  const route = parseReviewedRoute(body);
407
- return route === "full review" || route !== "mechanic" && parseRounds(body).length > 0;
454
+ if (route === "full review") return true;
455
+ if (route === "mechanic") return false;
456
+ return priorTrajectory(parseFindingsMarker(body), body).length > 0;
408
457
  };
409
458
  var COMPLETED_ANCESTOR_MARKER = "<!-- review-complete-ancestor -->";
410
459
  var parseCompletedAncestor = (body) => body.includes(COMPLETED_ANCESTOR_MARKER);
@@ -468,30 +517,12 @@ var parseRounds = (body) => {
468
517
  }
469
518
  return kept;
470
519
  };
471
- var ROUNDS_MARKER_LIMIT = 8e3;
472
- var roundsMarker = (rounds) => {
473
- if (rounds.length === 0) return "";
474
- const serialize = (kept2) => `<!-- code-review:rounds;base64 ${Buffer.from(JSON.stringify(kept2), "utf-8").toString("base64")} -->`;
475
- const stripCodes = (n) => rounds.map(
476
- (r, i) => i < n ? { critical: r.critical, major: r.major, minor: r.minor, nit: r.nit } : r
477
- );
478
- let stripped = 0;
479
- while (stripped < rounds.length && serialize(stripCodes(stripped)).length > ROUNDS_MARKER_LIMIT) {
480
- stripped += 1;
481
- }
482
- const kept = stripCodes(stripped);
483
- const bounded = serialize(kept).length > ROUNDS_MARKER_LIMIT ? kept.slice(-8) : kept;
484
- return serialize(bounded);
485
- };
486
- var roundChip = (c) => {
487
- const parts = SEVERITIES.filter((k) => c[k] > 0).map((k) => `${severityEmoji(k)}${String(c[k])}`);
488
- return parts.length === 0 ? "clean" : parts.join(" ");
489
- };
490
- var TRAJECTORY_CHIPS = 8;
520
+ var formatScore = (score) => formatConfidence(score);
521
+ var TRAJECTORY_SCORES = 8;
491
522
  var roundsSummary = (rounds, count = rounds.length) => {
492
523
  if (count === 0) return "";
493
- const chips = rounds.slice(-TRAJECTORY_CHIPS).map(roundChip);
494
- const trajectory = chips.length === 0 ? "" : rounds.length > TRAJECTORY_CHIPS ? `\u2026 \u2192 ${chips.join(" \u2192 ")}` : chips.join(" \u2192 ");
524
+ const cells = rounds.slice(-TRAJECTORY_SCORES).map((r) => typeof r.score === "number" ? formatScore(r.score) : "\u2014");
525
+ const trajectory = cells.length === 0 ? "" : rounds.length > TRAJECTORY_SCORES ? `\u2026 \u2192 ${cells.join(" \u2192 ")}` : cells.join(" \u2192 ");
495
526
  return trajectory === "" ? `**Round ${String(count)}**` : `**Round ${String(count)}** \xB7 ${trajectory}`;
496
527
  };
497
528
  var computeCodeCounts = (findings, systemic = []) => {
@@ -505,15 +536,6 @@ var computeCodeCounts = (findings, systemic = []) => {
505
536
  }
506
537
  return Object.fromEntries(counts);
507
538
  };
508
- var roundRecord = (counts, codes, priorCodes, sha, round) => {
509
- const normalized = normalizeCodeCounts(codes, priorCodes);
510
- const record3 = normalized === void 0 ? { ...counts } : { ...counts, codes: normalized };
511
- return {
512
- ...record3,
513
- ...sha !== void 0 ? { sha } : {},
514
- ...round !== void 0 ? { round } : {}
515
- };
516
- };
517
539
  var consecutiveCodeStreaks = (rounds) => {
518
540
  const entries = [];
519
541
  if (rounds.length === 0) return {};
@@ -590,38 +612,65 @@ var computeSameRootNotes = (priorRounds, findings, currentSha) => {
590
612
  };
591
613
  var CONVERGENCE_CEILINGS = { critical: 4, major: 2, minor: 1, nit: 0 };
592
614
  var CRITICAL_FLOOR_MARGIN = 0.01;
615
+ var MINOR_FLOOR = 0.1;
593
616
  var DEFAULT_CONVERGENCE_THRESHOLD = 1;
594
- var convergenceFloor = (severity, threshold) => severity === "critical" ? threshold + CRITICAL_FLOOR_MARGIN : severity === "major" ? 0.5 : 0;
617
+ var convergenceFloor = (severity, threshold) => severity === "critical" ? threshold + CRITICAL_FLOOR_MARGIN : severity === "major" ? 0.5 : severity === "minor" ? MINOR_FLOOR : 0;
595
618
  var round2 = (n) => Math.round((n + Number.EPSILON) * 100) / 100;
619
+ var contribution = (severity, confidence, likelihood, threshold) => {
620
+ const floor = convergenceFloor(severity, threshold);
621
+ return floor + Math.max(0, CONVERGENCE_CEILINGS[severity] - floor) * confidence * likelihood;
622
+ };
596
623
  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
- },
624
+ doc.findings.reduce(
625
+ (sum, { severity, confidence, likelihood }) => sum + contribution(severity, confidence, likelihood, threshold),
626
+ 0
627
+ ) + (doc.systemic_problems ?? []).reduce(
628
+ (sum, { severity, confidence }) => sum + contribution(severity, confidence, 1, threshold),
602
629
  0
603
630
  )
604
631
  );
605
- var convergenceSummary = (doc, threshold = DEFAULT_CONVERGENCE_THRESHOLD) => {
606
- const { score, converged } = convergenceSignal(doc, threshold);
607
- return converged ? `**Convergence** \u{1F3C1} ${String(score)} \u2264 ${String(threshold)} \u2014 converged` : `**Convergence** \u{1F504} ${String(score)} > ${String(threshold)} \u2014 iterating`;
632
+ var buildConvergence = (doc, threshold = DEFAULT_CONVERGENCE_THRESHOLD, priorRounds = [], round = 1, codes = {}, sha) => {
633
+ const score = convergenceScore(doc, threshold);
634
+ const normalized = normalizeCodeCounts(codes, priorRounds[priorRounds.length - 1]?.codes);
635
+ const current = {
636
+ round,
637
+ score,
638
+ ...normalized !== void 0 ? { codes: { ...normalized } } : {},
639
+ ...sha !== void 0 ? { sha } : {}
640
+ };
641
+ const rounds = [...priorRounds, current].slice(-64);
642
+ return { score, threshold, converged: score <= threshold, rounds };
643
+ };
644
+ var DEFAULT_NIT_VISIBILITY_FLOOR = 0.25;
645
+ var isBelowVisibilityFloor = (f, floor = DEFAULT_NIT_VISIBILITY_FLOOR) => f.severity === "nit" && typeof f.confidence === "number" && typeof f.likelihood === "number" && f.confidence * f.likelihood < floor;
646
+ var priorBelowFloorNits = (priorDoc, floor = DEFAULT_NIT_VISIBILITY_FLOOR) => {
647
+ if (typeof priorDoc !== "object" || priorDoc === null) return [];
648
+ const arr = priorDoc["findings"];
649
+ if (!Array.isArray(arr)) return [];
650
+ const nits = [];
651
+ for (const f of arr) {
652
+ if (typeof f !== "object" || f === null) continue;
653
+ const rec = f;
654
+ if (!isBelowVisibilityFloor(rec, floor)) continue;
655
+ const title = rec["title"];
656
+ if (typeof title !== "string") continue;
657
+ const code = typeof rec["code"] === "string" && rec["code"] !== "" ? rec["code"] : void 0;
658
+ const path = typeof rec["path"] === "string" ? rec["path"] : void 0;
659
+ nits.push({
660
+ title,
661
+ ...code !== void 0 ? { code } : {},
662
+ ...path !== void 0 ? { path } : {}
663
+ });
664
+ }
665
+ return nits;
608
666
  };
667
+ var convergenceBadge = (c) => c.converged ? `**Convergence** \u{1F3C1} ${formatScore(c.score)} \u2264 ${String(c.threshold)} \u2014 converged` : `**Convergence** \u{1F504} ${formatScore(c.score)} > ${String(c.threshold)} \u2014 iterating`;
668
+ var convergenceSummary = (doc, threshold = DEFAULT_CONVERGENCE_THRESHOLD) => convergenceBadge(convergenceSignal(doc, threshold));
609
669
  var SURFACE_SCHEMA_VERSION = "0.8.0";
610
670
  var convergenceSignal = (doc, threshold = DEFAULT_CONVERGENCE_THRESHOLD) => {
611
671
  const score = convergenceScore(doc, threshold);
612
672
  return { score, threshold, converged: score <= threshold };
613
673
  };
614
- var signalForRound = (round, doc, threshold = DEFAULT_CONVERGENCE_THRESHOLD) => ({ round, convergence: convergenceSignal(doc, threshold) });
615
- var signalMarker = (signal) => `<!-- code-review:signal;base64 ${Buffer.from(
616
- JSON.stringify({ schema_version: SURFACE_SCHEMA_VERSION, ...signal }),
617
- "utf-8"
618
- ).toString("base64")} -->`;
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));
624
- };
625
674
  var SIGNAL_RE = /<!-- code-review:signal;base64 ([A-Za-z0-9+/=]+) -->/;
626
675
  var parseSignalMarker = (body) => {
627
676
  const b64 = SIGNAL_RE.exec(body)?.[1];
@@ -645,6 +694,39 @@ var parseSurfaceSignal = (doc) => {
645
694
  convergence: { score: c["score"], threshold: c["threshold"], converged: c["converged"] }
646
695
  };
647
696
  };
697
+ var validStampedConvergence = (raw) => {
698
+ const decoded = ConvergenceCodec.decode(raw);
699
+ return decoded._tag === "Right" && decoded.right.rounds !== void 0 && decoded.right.rounds.length > 0 ? decoded.right : null;
700
+ };
701
+ var parseConvergence = (priorDoc) => {
702
+ if (typeof priorDoc !== "object" || priorDoc === null) return null;
703
+ const raw = priorDoc["convergence"];
704
+ return raw === void 0 ? null : validStampedConvergence(raw);
705
+ };
706
+ var CONVERGENCE_RE = /<!-- code-review:convergence;base64 ([A-Za-z0-9+/=]+) -->/;
707
+ var convergenceMarker = (convergence) => `<!-- code-review:convergence;base64 ${Buffer.from(JSON.stringify(convergence), "utf-8").toString(
708
+ "base64"
709
+ )} -->`;
710
+ var parseConvergenceMarker = (body) => {
711
+ const b64 = CONVERGENCE_RE.exec(body)?.[1];
712
+ return b64 === void 0 ? null : validStampedConvergence(decodeBase64Json(b64));
713
+ };
714
+ var roundRecordsToConvergenceRounds = (records) => records.map((r, i) => ({
715
+ round: r.round ?? i + 1,
716
+ ...r.codes !== void 0 ? { codes: { ...r.codes } } : {},
717
+ ...r.sha !== void 0 ? { sha: r.sha } : {}
718
+ }));
719
+ var priorTrajectory = (priorDoc, priorBody) => parseConvergence(priorDoc)?.rounds ?? parseConvergenceMarker(priorBody)?.rounds ?? roundRecordsToConvergenceRounds(parseRounds(priorBody));
720
+ var carriedConvergence = (priorDoc, priorBody) => {
721
+ const stamped = parseConvergence(priorDoc) ?? parseConvergenceMarker(priorBody);
722
+ if (stamped !== null) return stamped;
723
+ const signal = parseSignalMarker(priorBody) ?? parseSurfaceSignal(priorDoc);
724
+ if (signal === null) return null;
725
+ const legacy = roundRecordsToConvergenceRounds(parseRounds(priorBody));
726
+ const last = legacy[legacy.length - 1];
727
+ const rounds = last === void 0 ? [{ round: signal.round }] : last.round < signal.round ? [...legacy.slice(0, -1), { ...last, round: signal.round }] : legacy;
728
+ return { ...signal.convergence, rounds };
729
+ };
648
730
  var SURFACE_SCHEMA_VERSIONS = ["0.7.0", SURFACE_SCHEMA_VERSION];
649
731
  var isSurfaceVersion = (version) => typeof version === "string" && SURFACE_SCHEMA_VERSIONS.includes(version);
650
732
  var stripSurfaceFields = (doc) => {
@@ -660,12 +742,13 @@ var carryForwardMarkers = (body) => {
660
742
  const findings = /<!-- code-review:findings-json[^>]*-->/.exec(body)?.[0];
661
743
  const reviewedSha = /<!-- reviewed-sha: [0-9a-fA-F]{40} -->/.exec(body)?.[0];
662
744
  const reviewedRoute = ROUTE_RE.exec(body)?.[0];
745
+ const convergence = CONVERGENCE_RE.exec(body)?.[0];
663
746
  const rounds = ROUNDS_RE.exec(body)?.[0];
664
747
  const signal = SIGNAL_RE.exec(body)?.[0];
665
748
  const completedAncestor = parseReviewComplete(body) || parseCompletedAncestor(body) ? COMPLETED_ANCESTOR_MARKER : void 0;
666
749
  const findingsBlock = findings ? `${AGENTS_STOP_DIRECTIVE}
667
750
  ${findings}` : void 0;
668
- return [findingsBlock, reviewedSha, reviewedRoute, completedAncestor, rounds, signal].filter((m) => m !== void 0).join("\n\n");
751
+ return [findingsBlock, reviewedSha, reviewedRoute, completedAncestor, convergence, rounds, signal].filter((m) => m !== void 0).join("\n\n");
669
752
  };
670
753
  var escapeFence = (text) => text.replace(/```/g, "`` ` ``");
671
754
  var projectPatch = (patch) => {
@@ -1018,6 +1101,13 @@ var sanitizeFinding = (f, answeredNotes) => {
1018
1101
  answeredNote: answeredNotes !== void 0 && Object.prototype.hasOwnProperty.call(answeredNotes, key2) ? answeredNotes[key2] ?? "" : ""
1019
1102
  };
1020
1103
  };
1104
+ var sanitizeSuppressedNit = (f) => ({
1105
+ title: escapeCodeBackticks(f.title),
1106
+ ...f.code !== void 0 ? { code: escapeCodeBackticks(f.code) } : {},
1107
+ path: escapeCodeBackticks(f.path),
1108
+ startLine: f.start_line,
1109
+ m: formatConfidence(f.confidence * f.likelihood)
1110
+ });
1021
1111
  var sanitizeSystemic = (s) => ({
1022
1112
  ...s,
1023
1113
  title: escapePipes(s.title),
@@ -1035,10 +1125,6 @@ var computeSeverityCounts = (findings) => findings.reduce(
1035
1125
  emptySeverityCounts()
1036
1126
  );
1037
1127
  var isReviewVerdict = (verdict) => verdict !== "error";
1038
- var computeRoundCounts = (findings) => (findings.systemic_problems ?? []).reduce(
1039
- (acc, s) => s.severity in acc ? { ...acc, [s.severity]: acc[s.severity] + 1 } : acc,
1040
- computeSeverityCounts(findings.findings)
1041
- );
1042
1128
  var isConvergenceRound = (route, incomplete) => route === "full review" && !incomplete;
1043
1129
  var render = (input) => {
1044
1130
  const eta = new Eta({ autoTrim: false });
@@ -1051,9 +1137,10 @@ var render = (input) => {
1051
1137
  const effort = input.effort ?? input.envelope?.effort ?? null;
1052
1138
  const modelNames = input.envelope ? input.envelope.models.map((m) => m.model).join(", ") : "";
1053
1139
  const severityCounts = input.severityCounts ?? computeSeverityCounts(input.findings.findings);
1054
- const rounds = input.rounds ?? [];
1055
- const sameRootNotes = input.sameRootNotes ?? computeSameRootNotes(rounds.slice(0, -1), input.findings.findings);
1056
- const isFullReviewRound = (input.convergenceRound ?? (isConvergenceRound(route, incomplete) && rounds.length > 0)) && isReviewVerdict(input.findings.verdict);
1140
+ const convergence = input.findings.convergence;
1141
+ const trajectory = convergence?.rounds ?? input.rounds ?? [];
1142
+ const sameRootNotes = input.sameRootNotes ?? computeSameRootNotes(trajectory.slice(0, -1), input.findings.findings);
1143
+ const isFullReviewRound = (input.convergenceRound ?? (isConvergenceRound(route, incomplete) && trajectory.length > 0)) && isReviewVerdict(input.findings.verdict);
1057
1144
  const advisoryAllowed = isFullReviewRound;
1058
1145
  return eta.renderString(input.template, {
1059
1146
  findings: input.findings,
@@ -1070,26 +1157,22 @@ var render = (input) => {
1070
1157
  reviewedSha: input.reviewedSha ?? "0000000000000000000000000000000000000000",
1071
1158
  postedAt: input.postedAt ?? "",
1072
1159
  severityCounts,
1073
- convergenceSummary: isFullReviewRound ? convergenceSummary(input.findings, input.convergenceThreshold) : "",
1160
+ convergenceSummary: !isFullReviewRound ? "" : convergence ? convergenceBadge(convergence) : convergenceSummary(input.findings, input.convergenceThreshold),
1074
1161
  strays: (input.strays ?? []).map((f) => sanitizeFinding(f, input.answeredNotes)),
1162
+ suppressedNits: (input.suppressedNits ?? []).map(sanitizeSuppressedNit),
1163
+ nitVisibilityFloor: input.nitVisibilityFloor ?? DEFAULT_NIT_VISIBILITY_FLOOR,
1075
1164
  systemic: (input.findings.systemic_problems ?? []).map(sanitizeSystemic),
1076
1165
  unanchoredCount: input.unanchoredCount ?? 0,
1077
1166
  inlineDisposition: input.inlineDisposition ?? null,
1078
1167
  runUrl: input.runUrl ?? null,
1079
1168
  jsonUrl: input.jsonUrl ?? null,
1080
- findingsPointer: input.findingsPointer ?? surfacedFindingsPointer(
1081
- input.findings,
1082
- // The fallback embeds a signal exactly when the badge renders never beside a suppressed
1083
- // badge, and scores the same findings the badge does. It assumes a post-style history (the
1084
- // caller appends this run's counts last), numbering the round exactly as the trajectory
1085
- // label does; post always supplies the marker, so this path cannot disagree with it in
1086
- // production (issue #141 review r4).
1087
- isFullReviewRound && rounds.length > 0 ? signalForRound(rounds.length, input.findings, input.convergenceThreshold) : null,
1088
- input.jsonUrl
1089
- ),
1090
- roundsMarker: roundsMarker(rounds),
1091
- roundsSummary: roundsSummary(rounds, input.roundCount),
1092
- metastasisNote: advisoryAllowed ? metastasisNote(rounds) : "",
1169
+ // The blob is the agent's complete document with the pipeline-stamped convergence field inside it
1170
+ // (issue #174) — no separate signal or rounds marker rides beside it. post always supplies the
1171
+ // precomputed marker; the standalone `render` command falls back to encoding the doc here.
1172
+ findingsPointer: input.findingsPointer ?? findingsPointer(input.findings, input.jsonUrl),
1173
+ roundsMarker: "",
1174
+ roundsSummary: roundsSummary(trajectory, input.roundCount),
1175
+ metastasisNote: advisoryAllowed ? metastasisNote(trajectory) : "",
1093
1176
  sameRootNotes: advisoryAllowed ? sameRootNotes : {},
1094
1177
  answeredNotes: input.answeredNotes ?? {},
1095
1178
  answeredReRaiseNote: input.answeredReRaiseNote ?? "",
@@ -1454,6 +1537,7 @@ var sidecarPath = (draftPath, postfix) => {
1454
1537
  };
1455
1538
  var priorContextPath = (draftPath) => sidecarPath(draftPath, ".prior");
1456
1539
  var priorAnswersPath = (draftPath) => sidecarPath(draftPath, ".prior-answers");
1540
+ var priorSuppressedPath = (draftPath) => sidecarPath(draftPath, ".prior-suppressed");
1457
1541
  var lastValidPath = (draftPath) => sidecarPath(draftPath, ".last-valid");
1458
1542
  var spawnFloorMessage = (draftPath) => `Write your own first-pass findings to ${draftPath} before spawning subagents \u2014 a review must never depend on subagents alone, and the pre-seeded $DRAFT is a non-review sentinel: it does not count until you have replaced it with your own review. Write ${draftPath} from what you have read so far (preliminary findings are fine), run \`code-review validate ${draftPath} --explain\` until it passes, then fan out; your subagents run in the background, so keep refining the draft as their reports arrive.`;
1459
1543
  var forceBackgroundSpawn = (toolInput) => ({
@@ -1971,14 +2055,9 @@ var checkLongSuggestions = (comments) => {
1971
2055
  });
1972
2056
  return { comments: adjusted, longFiles };
1973
2057
  };
1974
- var loadFindings = (path) => {
1975
- let raw;
1976
- try {
1977
- raw = JSON.parse(readFileSync(path, "utf-8"));
1978
- } catch {
1979
- return { kind: "corrupt" };
1980
- }
1981
- const resolution = resolve("findings", raw);
2058
+ var PIPELINE_STAMPED_FIELDS = /* @__PURE__ */ new Set(["convergence", "scope_metastasis"]);
2059
+ var decodeFindings = (doc) => {
2060
+ const resolution = resolve("findings", doc);
1982
2061
  switch (resolution.kind) {
1983
2062
  case "ok":
1984
2063
  return { kind: "ok", findings: resolution.value };
@@ -1989,6 +2068,28 @@ var loadFindings = (path) => {
1989
2068
  return { kind: "invalid-shape" };
1990
2069
  }
1991
2070
  };
2071
+ var loadFindings = (path) => {
2072
+ let raw;
2073
+ try {
2074
+ raw = JSON.parse(readFileSync(path, "utf-8"));
2075
+ } catch {
2076
+ return { kind: "corrupt" };
2077
+ }
2078
+ const first = decodeFindings(raw);
2079
+ if (first.kind === "invalid-shape" && typeof raw === "object" && raw !== null && !Array.isArray(raw) && Object.keys(raw).some((k) => PIPELINE_STAMPED_FIELDS.has(k))) {
2080
+ const stripped = Object.fromEntries(
2081
+ Object.entries(raw).filter(([key2]) => !PIPELINE_STAMPED_FIELDS.has(key2))
2082
+ );
2083
+ const retry = decodeFindings(stripped);
2084
+ if (retry.kind === "ok") {
2085
+ process.stderr.write(
2086
+ "Warning: the review draft carried an invalid pipeline-stamped field (convergence/scope_metastasis) \u2014 stripped it and used the rest; the pipeline re-stamps convergence\n"
2087
+ );
2088
+ return retry;
2089
+ }
2090
+ }
2091
+ return first;
2092
+ };
1992
2093
  var noticeMessageFor = (result) => {
1993
2094
  switch (result.kind) {
1994
2095
  case "corrupt":
@@ -2273,11 +2374,24 @@ var post = async (input, ghApi = runGhApi) => {
2273
2374
  const wouldBuryCompleted = (incomplete) => incomplete && existingComplete;
2274
2375
  const priorIsFullReview = (body) => isFullReviewSticky(body) || parseReviewedRoute(body) === null && (parseReviewComplete(body) || parseCompletedAncestor(body));
2275
2376
  const emptyMechanicWouldBury = (route, incomplete) => route === "mechanic" && !incomplete && findings.findings.length === 0 && existingSticky !== null && priorIsFullReview(existingSticky.body);
2276
- const priorRounds = existingSticky !== null ? parseRounds(existingSticky.body) : [];
2277
- const priorSignal = existingSticky === null ? null : parseSignalMarker(existingSticky.body) ?? parseSurfaceSignal(parseFindingsMarker(existingSticky.body));
2278
- const findingsMarkerFor = (findings2, signal2) => {
2279
- const pointer = surfacedFindingsPointer(findings2, signal2, input.jsonUrl);
2280
- return signal2 !== null || priorSignal === null ? pointer : joinSignalMarker(pointer, signalMarker(priorSignal));
2377
+ const priorDoc = existingSticky !== null ? parseFindingsMarker(existingSticky.body) : null;
2378
+ const priorBody = existingSticky?.body ?? "";
2379
+ const priorTraj = priorTrajectory(priorDoc, priorBody);
2380
+ const priorConv = carriedConvergence(priorDoc, priorBody);
2381
+ const lastRound = (rounds) => rounds.length > 0 ? rounds[rounds.length - 1]?.round ?? rounds.length : 0;
2382
+ const priorRoundCount = Math.max(lastRound(priorTraj), lastRound(priorConv?.rounds ?? []));
2383
+ const stampConvergence = (doc, conv) => ({
2384
+ ...doc,
2385
+ convergence: conv ?? void 0
2386
+ });
2387
+ const findingsBlob = (doc) => {
2388
+ const marker = findingsPointer(doc, input.jsonUrl);
2389
+ if (doc.convergence === void 0 || findingsMarkerForm(doc, input.jsonUrl) === "embedded") {
2390
+ return marker;
2391
+ }
2392
+ const conv = convergenceMarker(doc.convergence);
2393
+ return marker === "" ? conv : `${marker}
2394
+ ${conv}`;
2281
2395
  };
2282
2396
  const leaveInPlace = (message) => {
2283
2397
  process.stderr.write(
@@ -2321,7 +2435,7 @@ ${dropNote}` : ""}`,
2321
2435
  const template = readFileSync(input.templatePath, "utf-8");
2322
2436
  const inlineTemplate = readFileSync(input.inlineTemplatePath, "utf-8");
2323
2437
  const renderNotice = (message) => {
2324
- const findings2 = incompleteFindings(`### \u26A0\uFE0F ${message}`);
2438
+ const findings2 = stampConvergence(incompleteFindings(`### \u26A0\uFE0F ${message}`), priorConv);
2325
2439
  return formatMarkdown(
2326
2440
  render({
2327
2441
  findings: findings2,
@@ -2333,17 +2447,12 @@ ${dropNote}` : ""}`,
2333
2447
  route: input.route,
2334
2448
  reviewedSha: input.headSha,
2335
2449
  effort: input.effort,
2336
- rounds: priorRounds,
2337
2450
  sameRootNotes: {},
2338
- roundCount: priorSignal?.round ?? priorRounds.length,
2451
+ roundCount: priorRoundCount,
2339
2452
  convergenceRound: false,
2340
2453
  runUrl: input.runUrl,
2341
2454
  jsonUrl: input.jsonUrl,
2342
- // A notice's own blob stays clean: verdict "error" + a carried "converged" would read as a
2343
- // stop signal for a run that produced no verdict (issue #141 review r2). The prior signal
2344
- // survives on the sticky in the compact marker (findingsMarkerFor), and the carried-forward
2345
- // trajectory (rounds marker) remains the historical record.
2346
- findingsPointer: findingsMarkerFor(findings2, null),
2455
+ findingsPointer: findingsBlob(findings2),
2347
2456
  postedAt: input.postedAt
2348
2457
  })
2349
2458
  );
@@ -2393,6 +2502,12 @@ ${dropNote}` : ""}`,
2393
2502
  findings: [...answeredFilter.findings],
2394
2503
  ...systemic.length > 0 ? { systemic_problems: systemic } : {}
2395
2504
  };
2505
+ const priorSuppressedKeys = new Set(
2506
+ (existingSticky !== null && isFullReviewSticky(existingSticky.body) ? priorBelowFloorNits(parseFindingsMarker(existingSticky.body), input.nitVisibilityFloor) : []).map((n) => answeredNoteKey({ code: n.code, title: n.title }))
2507
+ );
2508
+ const isSuppressedNit = (f) => f.severity === "nit" && (isBelowVisibilityFloor(f, input.nitVisibilityFloor) || priorSuppressedKeys.has(answeredNoteKey(f)));
2509
+ const suppressedNits = findings.findings.filter(isSuppressedNit);
2510
+ const visibleFindings = findings.findings.filter((f) => !isSuppressedNit(f));
2396
2511
  const answeredDropNote = answeredReRaiseNote(verbatimReRaised, droppedCount) + (verbatimReRaised.length > 0 && findings.findings.length === 0 ? "\n> _The stop signal reflects the kept findings \u2014 this round carries none._" : "");
2397
2512
  const envelope = loadEnvelope(input.envelopePath);
2398
2513
  const testReport = input.testReportPath ? loadTestReport(input.testReportPath) : void 0;
@@ -2405,9 +2520,10 @@ ${dropNote}` : ""}`,
2405
2520
  }
2406
2521
  if (emptyMechanicWouldBury(effectiveRoute, envelopelessIncomplete) && existingSticky !== null)
2407
2522
  await emptyMechanicLeaveOrNote(existingSticky);
2523
+ const stampedFindings2 = stampConvergence(findings, priorConv);
2408
2524
  const body = formatMarkdown(
2409
2525
  render({
2410
- findings,
2526
+ findings: stampedFindings2,
2411
2527
  envelope: null,
2412
2528
  incomplete: envelopelessIncomplete,
2413
2529
  prices: decodedPrices.right,
@@ -2416,27 +2532,26 @@ ${dropNote}` : ""}`,
2416
2532
  route: effectiveRoute,
2417
2533
  reviewedSha: input.headSha,
2418
2534
  effort: input.effort,
2419
- rounds: priorRounds,
2420
2535
  sameRootNotes: {},
2421
2536
  // The answered-state honesty rules apply on EVERY surface that renders the filtered
2422
- // findings — the lost-envelope branch lists every finding (no inline review exists to
2537
+ // findings — the lost-envelope branch lists every VISIBLE finding (no inline review exists to
2423
2538
  // carry them) so the kept re-raises' annotations actually render, and names the drops
2424
- // exactly like the main path (issues #151 review r1 + r2).
2425
- strays: findings.findings,
2539
+ // exactly like the main path (issues #151 review r1 + r2). The nit visibility floor applies
2540
+ // here too (issue #164): below-floor nits are hidden from the human list and shown only in the
2541
+ // collapsed aside — the floor is a human-visibility policy, not an inline-comment policy, so it
2542
+ // must hold on the surface that lists findings without an inline review.
2543
+ strays: visibleFindings,
2544
+ suppressedNits,
2545
+ nitVisibilityFloor: input.nitVisibilityFloor,
2426
2546
  answeredNotes: reRaisedNotes,
2427
2547
  answeredReRaiseNote: answeredDropNote,
2428
- roundCount: priorSignal?.round ?? priorRounds.length,
2548
+ roundCount: priorRoundCount,
2429
2549
  convergenceRound: false,
2430
2550
  testReport,
2431
2551
  inlineDisposition: { kind: "no-envelope" },
2432
2552
  runUrl: input.runUrl,
2433
2553
  jsonUrl: input.jsonUrl,
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.
2436
- findingsPointer: findingsMarkerFor(
2437
- findings,
2438
- isReviewVerdict(findings.verdict) ? priorSignal : null
2439
- ),
2554
+ findingsPointer: findingsBlob(stampedFindings2),
2440
2555
  postedAt: input.postedAt
2441
2556
  })
2442
2557
  );
@@ -2454,12 +2569,12 @@ ${dropNote}` : ""}`,
2454
2569
  if (emptyMechanicWouldBury(effectiveRoute, thisIncomplete) && existingSticky !== null)
2455
2570
  await emptyMechanicLeaveOrNote(existingSticky);
2456
2571
  const isRound = isConvergenceRound(effectiveRoute, thisIncomplete) && isReviewVerdict(findings.verdict);
2457
- const sameRootNotes = isRound ? computeSameRootNotes(priorRounds, findings.findings, input.headSha.slice(0, 12)) : {};
2572
+ const sameRootNotes = isRound ? computeSameRootNotes(priorTraj, findings.findings, input.headSha.slice(0, 12)) : {};
2458
2573
  const {
2459
2574
  comments: rawComments,
2460
2575
  strays,
2461
2576
  inDiff
2462
- } = buildInlineComments(findings.findings, diff, {
2577
+ } = buildInlineComments(visibleFindings, diff, {
2463
2578
  inlineTemplate,
2464
2579
  models: envelope.models.map((m) => m.model),
2465
2580
  findings,
@@ -2478,35 +2593,30 @@ ${dropNote}` : ""}`,
2478
2593
  const initialDisposition = comments.length === 0 && strays.length > 0 ? { kind: "none-in-diff" } : void 0;
2479
2594
  const currentCounts = computeSeverityCounts(findings.findings);
2480
2595
  const currentCodes = computeCodeCounts(findings.findings, findings.systemic_problems ?? []);
2481
- const priorLastCodes = priorRounds.length > 0 ? priorRounds[priorRounds.length - 1]?.codes : void 0;
2482
- const roundNumber = Math.max(priorSignal?.round ?? priorRounds.length, priorRounds.length) + 1;
2483
- const rounds = isRound ? [
2484
- ...priorRounds,
2485
- roundRecord(
2486
- computeRoundCounts(findings),
2487
- currentCodes,
2488
- priorLastCodes,
2489
- input.headSha.slice(0, 12),
2490
- roundNumber
2491
- )
2492
- ] : priorRounds;
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)" : "";
2596
+ const roundNumber = priorRoundCount + 1;
2597
+ const convergence = isRound ? buildConvergence(
2598
+ findings,
2599
+ input.convergenceThreshold,
2600
+ priorTraj,
2601
+ roundNumber,
2602
+ currentCodes,
2603
+ input.headSha.slice(0, 12)
2604
+ ) : priorConv;
2605
+ const stampedFindings = stampConvergence(findings, convergence);
2606
+ const currentRoundCount = isRound ? roundNumber : priorRoundCount;
2607
+ const findingsMarker = findingsBlob(stampedFindings);
2608
+ const markerForm = findingsMarkerForm(stampedFindings, input.jsonUrl);
2497
2609
  if (markerForm === "link") {
2498
2610
  process.stderr.write(
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
- `
2611
+ "Warning: the findings-json blob exceeds the embed limit \u2014 degraded to the jsonUrl-link form; the convergence rides a compact marker beside it, but a decoding agent (and the next-round seed) must fetch the artifact for the FINDINGS\n"
2501
2612
  );
2502
2613
  } else if (markerForm === "omitted") {
2503
2614
  process.stderr.write(
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
- `
2615
+ "Warning: the findings-json blob exceeds the embed limit and no --json-url was given \u2014 the convergence rides a compact marker but the embedded findings seed is dropped from the posted surfaces\n"
2506
2616
  );
2507
2617
  }
2508
2618
  const commonRenderInput = {
2509
- findings,
2619
+ findings: stampedFindings,
2510
2620
  envelope,
2511
2621
  incomplete: thisIncomplete,
2512
2622
  prices: decodedPrices.right,
@@ -2517,14 +2627,15 @@ ${dropNote}` : ""}`,
2517
2627
  effort: input.effort,
2518
2628
  testReport,
2519
2629
  severityCounts: currentCounts,
2520
- rounds,
2521
2630
  sameRootNotes,
2522
2631
  answeredNotes: reRaisedNotes,
2523
2632
  answeredReRaiseNote: answeredDropNote,
2524
- roundCount: signal?.round ?? priorSignal?.round ?? priorRounds.length,
2633
+ roundCount: currentRoundCount,
2525
2634
  convergenceThreshold: input.convergenceThreshold,
2635
+ nitVisibilityFloor: input.nitVisibilityFloor,
2526
2636
  convergenceRound: isRound,
2527
2637
  strays,
2638
+ suppressedNits,
2528
2639
  runUrl: input.runUrl,
2529
2640
  jsonUrl: input.jsonUrl,
2530
2641
  findingsPointer: findingsMarker,
@@ -3681,7 +3792,8 @@ var resolvePrices = (pricesArg) => {
3681
3792
  return { kind: "absent", path: bundledPath("schema", "prices.example.json") };
3682
3793
  };
3683
3794
  var TEST_REPORT_DESCRIPTION = 'Path to a JSON test summary: {"passed": number, "failed": number, "total": number, "failures"?: [{"name": string, "message"?: string}]}';
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)";
3795
+ var CONVERGENCE_THRESHOLD_DESCRIPTION = "Advisory convergence tolerance: the per-finding convergence score (each finding's severity floor + confidence-and-likelihood-weighted headroom; ceilings critical 4 \xB7 major 2 \xB7 minor 1 \xB7 nit 0) at or below which the sticky reads as converged. The floor values and the systemic-likelihood rule are documented in the README and the findings schema (default: 1)";
3796
+ var NIT_VISIBILITY_FLOOR_DESCRIPTION = "Nit visibility floor: nits whose confidence \xD7 likelihood falls below this are hidden from humans (no inline comment; a collapsed aside in the sticky) but kept in the machine blob as adjudicated. In [0, 1] (default: 0.25)";
3685
3797
  var renderCmd = defineCommand({
3686
3798
  meta: {
3687
3799
  name: "render",
@@ -3725,6 +3837,10 @@ var renderCmd = defineCommand({
3725
3837
  "convergence-threshold": {
3726
3838
  type: "string",
3727
3839
  description: CONVERGENCE_THRESHOLD_DESCRIPTION
3840
+ },
3841
+ "nit-visibility-floor": {
3842
+ type: "string",
3843
+ description: NIT_VISIBILITY_FLOOR_DESCRIPTION
3728
3844
  }
3729
3845
  },
3730
3846
  run: async ({ args }) => {
@@ -3737,9 +3853,13 @@ var renderCmd = defineCommand({
3737
3853
  const testReport = args["test-report"] ? decode(TestSummaryCodec.decode(readJSON(args["test-report"])), "test report") : void 0;
3738
3854
  const route = args.route || envelope.route || null;
3739
3855
  const isRound = isConvergenceRound(route, envelope.incomplete === true || isIncompleteFindings(findings)) && isReviewVerdict(findings.verdict);
3740
- const counts = computeRoundCounts(findings);
3856
+ const threshold = parseConvergenceThreshold(args["convergence-threshold"]);
3857
+ const stampedFindings = {
3858
+ ...findings,
3859
+ convergence: isRound ? buildConvergence(findings, threshold) : void 0
3860
+ };
3741
3861
  const output2 = render({
3742
- findings,
3862
+ findings: stampedFindings,
3743
3863
  envelope,
3744
3864
  prices,
3745
3865
  pricesProvided: priceResolution.kind === "provided",
@@ -3748,8 +3868,9 @@ var renderCmd = defineCommand({
3748
3868
  route: args.route,
3749
3869
  effort: args.effort,
3750
3870
  testReport,
3751
- rounds: isRound ? [counts] : [],
3752
- convergenceThreshold: parseConvergenceThreshold(args["convergence-threshold"]),
3871
+ convergenceThreshold: threshold,
3872
+ nitVisibilityFloor: parseNitVisibilityFloor(args["nit-visibility-floor"]),
3873
+ convergenceRound: isRound,
3753
3874
  postedAt: formatUtc(/* @__PURE__ */ new Date())
3754
3875
  });
3755
3876
  process.stdout.write(output2);
@@ -3774,13 +3895,19 @@ var inlineCmd = defineCommand({
3774
3895
  template: {
3775
3896
  type: "string",
3776
3897
  description: "Path to inline comment Eta template (default: bundled templates/inline.eta)"
3898
+ },
3899
+ "nit-visibility-floor": {
3900
+ type: "string",
3901
+ description: NIT_VISIBILITY_FLOOR_DESCRIPTION
3777
3902
  }
3778
3903
  },
3779
3904
  run: async ({ args }) => {
3780
3905
  const findings = decode(FindingsCodec.decode(readJSON(args.findings)), "findings");
3781
3906
  const diff = readFileSync(resolve$1(args.diff), "utf-8");
3782
3907
  const inlineTemplate = readFileSync(resolveInlineTemplatePath(args.template), "utf-8");
3783
- const { comments, strays } = buildInlineComments(findings.findings, diff, {
3908
+ const floor = parseNitVisibilityFloor(args["nit-visibility-floor"]);
3909
+ const visibleFindings = findings.findings.filter((f) => !isBelowVisibilityFloor(f, floor));
3910
+ const { comments, strays } = buildInlineComments(visibleFindings, diff, {
3784
3911
  inlineTemplate,
3785
3912
  findings
3786
3913
  });
@@ -3882,6 +4009,26 @@ var parseConvergenceThreshold = (raw) => {
3882
4009
  }
3883
4010
  return n;
3884
4011
  };
4012
+ var parseNitVisibilityFloor = (raw) => {
4013
+ const trimmed = raw?.trim();
4014
+ if (trimmed === void 0 || trimmed === "") return void 0;
4015
+ if (!/^\d+(\.\d+)?$/.test(trimmed)) {
4016
+ fail(`--nit-visibility-floor must be a number in [0, 1]; got "${trimmed}"`);
4017
+ }
4018
+ const n = Number.parseFloat(trimmed);
4019
+ if (!Number.isFinite(n) || n < 0 || n > 1) {
4020
+ fail(
4021
+ `--nit-visibility-floor must be in [0, 1] (it gates confidence \xD7 likelihood); got "${trimmed}"`
4022
+ );
4023
+ }
4024
+ return n;
4025
+ };
4026
+ var parseNitVisibilityFloorLenient = (raw) => {
4027
+ const trimmed = raw?.trim();
4028
+ if (trimmed === void 0 || trimmed === "" || !/^\d+(\.\d+)?$/.test(trimmed)) return void 0;
4029
+ const n = Number.parseFloat(trimmed);
4030
+ return Number.isFinite(n) && n >= 0 && n <= 1 ? n : void 0;
4031
+ };
3885
4032
  var transcriptPathOf = (input) => {
3886
4033
  const tp = (typeof input === "object" && input !== null ? input : {})["transcript_path"];
3887
4034
  return typeof tp === "string" ? tp : void 0;
@@ -4165,6 +4312,7 @@ ${printableSchema(schemaPath)}
4165
4312
  }
4166
4313
  });
4167
4314
  var isSurfaceStampedDoc = (doc) => typeof doc === "object" && doc !== null && !Array.isArray(doc) && doc["schema_version"] === SURFACE_SCHEMA_VERSION;
4315
+ var PIPELINE_STAMPED_FIELDS2 = /* @__PURE__ */ new Set(["scope_metastasis", "convergence"]);
4168
4316
  var withoutScopeMetastasis = (doc) => typeof doc === "object" && doc !== null && !Array.isArray(doc) ? Object.fromEntries(Object.entries(doc).filter(([key2]) => key2 !== "scope_metastasis")) : doc;
4169
4317
  var seedDraftCmd = defineCommand({
4170
4318
  meta: {
@@ -4180,6 +4328,10 @@ var seedDraftCmd = defineCommand({
4180
4328
  type: "string",
4181
4329
  description: "Path to the gather-staged answered-findings registry (answered.json) \u2014 the prior inline findings whose threads a human reply answered (issue #151). Delivered out-of-band to the .prior-answers sidecar beside the prior context so the next-round agent sees the already-answered state; best-effort, never fails the seed"
4182
4330
  },
4331
+ "nit-visibility-floor": {
4332
+ type: "string",
4333
+ description: "The nit visibility floor (issue #164), matched to the commenter's: the prior review's below-floor nits (confidence \xD7 likelihood below it) are re-derived from --prior and delivered to the .prior-suppressed sidecar as adjudicated context, so the next-round agent does not re-raise them as fresh nits; best-effort, never fails the seed. Empty \u21D2 the default"
4334
+ },
4183
4335
  "head-sha": {
4184
4336
  type: "string",
4185
4337
  description: "Current head SHA, compared against the prior review's embedded reviewed-sha to distinguish a same-commit re-review from a new-commit one; an unknown or mismatched prior SHA is treated as a new commit"
@@ -4249,7 +4401,7 @@ var seedDraftCmd = defineCommand({
4249
4401
  const carried = strippedPrior["scope_metastasis"];
4250
4402
  if (ScopeMetastasisCodec.decode(carried)._tag === "Right") return strippedPrior;
4251
4403
  if (strippedPrior["verdict"] === "error") return strippedPrior;
4252
- const computed = computeScopeMetastasis(parseRounds(priorBody ?? ""));
4404
+ const computed = computeScopeMetastasis(priorTrajectory(parsedPrior, priorBody ?? ""));
4253
4405
  return computed === null ? strippedPrior : { ...strippedPrior, scope_metastasis: computed };
4254
4406
  })();
4255
4407
  if (args["prior-answers"]) {
@@ -4275,6 +4427,27 @@ var seedDraftCmd = defineCommand({
4275
4427
  } catch (err) {
4276
4428
  process.stderr.write(
4277
4429
  `Warning: could not read the answered-findings registry ${args["prior-answers"]} (${errMsg(err)}) \u2014 no prior-answers sidecar
4430
+ `
4431
+ );
4432
+ }
4433
+ }
4434
+ if (parsedPrior !== null && parseReviewedRoute(priorBody ?? "") === "full review") {
4435
+ try {
4436
+ const belowFloor = priorBelowFloorNits(
4437
+ parsedPrior,
4438
+ parseNitVisibilityFloorLenient(args["nit-visibility-floor"])
4439
+ );
4440
+ if (belowFloor.length > 0) {
4441
+ writeFileSync(priorSuppressedPath(outPath), `${JSON.stringify(belowFloor, null, 2)}
4442
+ `);
4443
+ process.stderr.write(
4444
+ `Seeded ${priorSuppressedPath(outPath)} with ${String(belowFloor.length)} below-floor nit(s) as adjudicated context
4445
+ `
4446
+ );
4447
+ }
4448
+ } catch (err) {
4449
+ process.stderr.write(
4450
+ `Warning: could not derive the prior below-floor nits (${errMsg(err)}) \u2014 no prior-suppressed sidecar
4278
4451
  `
4279
4452
  );
4280
4453
  }
@@ -4284,7 +4457,7 @@ var seedDraftCmd = defineCommand({
4284
4457
  const schemaPath = args.schema ? resolve$1(args.schema) : schemaPathFor(kind, args["schema-version"]);
4285
4458
  const barePrior = typeof priorFindings === "object" && !Array.isArray(priorFindings) ? Object.fromEntries(
4286
4459
  Object.entries(priorFindings).filter(
4287
- ([key2]) => key2 !== "scope_metastasis"
4460
+ ([key2]) => !PIPELINE_STAMPED_FIELDS2.has(key2)
4288
4461
  )
4289
4462
  ) : priorFindings;
4290
4463
  const accepts = (doc) => validateAgainstSchema(doc, schemaPath).valid && resolve("findings", doc).kind === "ok";
@@ -4789,6 +4962,10 @@ var postCmd = defineCommand({
4789
4962
  "convergence-threshold": {
4790
4963
  type: "string",
4791
4964
  description: CONVERGENCE_THRESHOLD_DESCRIPTION
4965
+ },
4966
+ "nit-visibility-floor": {
4967
+ type: "string",
4968
+ description: NIT_VISIBILITY_FLOOR_DESCRIPTION
4792
4969
  }
4793
4970
  },
4794
4971
  run: async ({ args }) => {
@@ -4810,6 +4987,7 @@ var postCmd = defineCommand({
4810
4987
  runUrl: args["run-url"],
4811
4988
  jsonUrl: args["json-url"],
4812
4989
  convergenceThreshold: parseConvergenceThreshold(args["convergence-threshold"]),
4990
+ nitVisibilityFloor: parseNitVisibilityFloor(args["nit-visibility-floor"]),
4813
4991
  postedAt: formatUtc(/* @__PURE__ */ new Date())
4814
4992
  });
4815
4993
  }