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

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
@@ -104,14 +104,14 @@ links to:
104
104
  [`src/surface.ts`](src/surface.ts).
105
105
 
106
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
107
+ 0.9.0 contract the review agent is held to and
108
108
  [`schema/findings.schema.json`](schema/findings.schema.json) validates, and the same object the
109
109
  comment is rendered from. It is embedded verbatim: no field is added or dropped, so the machine
110
110
  channel can never carry less than, or drift from, the rendered prose.
111
111
 
112
112
  ```json
113
113
  {
114
- "schema_version": "0.6.0",
114
+ "schema_version": "0.9.0",
115
115
  "verdict": "comment",
116
116
  "summary": "...",
117
117
  "findings": []
@@ -121,8 +121,12 @@ links to:
121
121
  The deterministic **stop signal** for an iterating author-agent rides its own compact marker beside
122
122
  the findings blob, `<!-- code-review:signal;base64 <base64> -->`, whose decoded payload is
123
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
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 × likelihood` (ceilings
126
+ critical 4 · major 2 · minor 1 · nit 0; floors critical `threshold + 0.01` · major 0.5 · minor 0.1
127
+ · nit 0). A **systemic problem is scored with `likelihood` = 1** — a structural observation has no
128
+ single triggering input, so it is never discounted by likelihood. Rounded to 2
129
+ decimals; `threshold` defaults to 1, and `converged` = score ≤ threshold as a literal
126
130
  boolean — `converged: true` means the last completed round is at or below the tolerance, so another
127
131
  iteration round is not warranted. The commenter computes the signal from the review's own severities
128
132
  (the agent never writes it); it appears once at least one full-review round has completed and is
@@ -137,8 +141,10 @@ links to:
137
141
  frequencies — from which the re-review seed re-derives the advisory `scope_metastasis` entry it
138
142
  hands the next-round agent. It is deliberately NOT embedded in the findings blob: the blob is the
139
143
  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.
144
+ comment embeds only its own finding (a `schema_version` + one-finding fragment), and the
145
+ review-object body only links the sticky so the **sticky is the sole documented decode surface**
146
+ for the whole-document marker; a decoding agent reads it there. The review body is written only after
147
+ the sticky exists (a failed sticky write aborts the run first), so it never carries the blob itself.
142
148
  - **`code-review-transcript`** — the full Claude Code session transcripts for the triage and review
143
149
  phases. This is advisory/auditability only: it is never read by the comment job and never affects
144
150
  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,
@@ -371,7 +374,8 @@ var severityEmoji = (s) => {
371
374
  }
372
375
  };
373
376
  var EMBED_LIMIT = 42700;
374
- var AGENTS_STOP_DIRECTIVE = "<!-- AGENTS: STOP \u2014 do not parse the prose below; decode this findings JSON and read schema_version first. -->";
377
+ var FINDINGS_SCHEMA_URL = "https://raw.githubusercontent.com/JPHutchins/code-review/main/schema/findings.schema.json";
378
+ 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 and every code-review:* marker (findings, signal, rounds), not only the fields you recognize: the recurrence signals an iterating agent needs (round streaks, scope metastasis, same-mechanism) are decodable there. -->`;
375
379
  var b64LengthOf = (document) => Buffer.from(JSON.stringify(document), "utf-8").toString("base64").length;
376
380
  var encodeMarker = (document, jsonUrl, limit) => {
377
381
  const b64 = Buffer.from(JSON.stringify(document), "utf-8").toString("base64");
@@ -585,19 +589,58 @@ var computeSameRootNotes = (priorRounds, findings, currentSha) => {
585
589
  }
586
590
  return Object.fromEntries(entries);
587
591
  };
588
- var CONVERGENCE_WEIGHTS = { critical: 4, major: 2, minor: 1, nit: 0 };
592
+ var CONVERGENCE_CEILINGS = { critical: 4, major: 2, minor: 1, nit: 0 };
593
+ var CRITICAL_FLOOR_MARGIN = 0.01;
594
+ var MINOR_FLOOR = 0.1;
589
595
  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);
596
+ var convergenceFloor = (severity, threshold) => severity === "critical" ? threshold + CRITICAL_FLOOR_MARGIN : severity === "major" ? 0.5 : severity === "minor" ? MINOR_FLOOR : 0;
597
+ var round2 = (n) => Math.round((n + Number.EPSILON) * 100) / 100;
598
+ var contribution = (severity, confidence, likelihood, threshold) => {
599
+ const floor = convergenceFloor(severity, threshold);
600
+ return floor + Math.max(0, CONVERGENCE_CEILINGS[severity] - floor) * confidence * likelihood;
601
+ };
602
+ var convergenceScore = (doc, threshold) => round2(
603
+ doc.findings.reduce(
604
+ (sum, { severity, confidence, likelihood }) => sum + contribution(severity, confidence, likelihood, threshold),
605
+ 0
606
+ ) + (doc.systemic_problems ?? []).reduce(
607
+ (sum, { severity, confidence }) => sum + contribution(severity, confidence, 1, threshold),
608
+ 0
609
+ )
610
+ );
611
+ var DEFAULT_NIT_VISIBILITY_FLOOR = 0.25;
612
+ var isBelowVisibilityFloor = (f, floor = DEFAULT_NIT_VISIBILITY_FLOOR) => f.severity === "nit" && typeof f.confidence === "number" && typeof f.likelihood === "number" && f.confidence * f.likelihood < floor;
613
+ var priorBelowFloorNits = (priorDoc, floor = DEFAULT_NIT_VISIBILITY_FLOOR) => {
614
+ if (typeof priorDoc !== "object" || priorDoc === null) return [];
615
+ const arr = priorDoc["findings"];
616
+ if (!Array.isArray(arr)) return [];
617
+ const nits = [];
618
+ for (const f of arr) {
619
+ if (typeof f !== "object" || f === null) continue;
620
+ const rec = f;
621
+ if (!isBelowVisibilityFloor(rec, floor)) continue;
622
+ const title = rec["title"];
623
+ if (typeof title !== "string") continue;
624
+ const code = typeof rec["code"] === "string" && rec["code"] !== "" ? rec["code"] : void 0;
625
+ const path = typeof rec["path"] === "string" ? rec["path"] : void 0;
626
+ nits.push({
627
+ title,
628
+ ...code !== void 0 ? { code } : {},
629
+ ...path !== void 0 ? { path } : {}
630
+ });
631
+ }
632
+ return nits;
633
+ };
634
+ var convergenceSummary = (doc, threshold = DEFAULT_CONVERGENCE_THRESHOLD) => {
635
+ const { score, converged } = convergenceSignal(doc, threshold);
593
636
  return converged ? `**Convergence** \u{1F3C1} ${String(score)} \u2264 ${String(threshold)} \u2014 converged` : `**Convergence** \u{1F504} ${String(score)} > ${String(threshold)} \u2014 iterating`;
594
637
  };
595
638
  var SURFACE_SCHEMA_VERSION = "0.8.0";
596
- var convergenceSignal = (counts, threshold = DEFAULT_CONVERGENCE_THRESHOLD) => {
597
- const score = convergenceScore(counts);
639
+ var convergenceSignal = (doc, threshold = DEFAULT_CONVERGENCE_THRESHOLD) => {
640
+ const score = convergenceScore(doc, threshold);
598
641
  return { score, threshold, converged: score <= threshold };
599
642
  };
600
- var signalForRound = (round, counts, threshold = DEFAULT_CONVERGENCE_THRESHOLD) => ({ round, convergence: convergenceSignal(counts, threshold) });
643
+ var signalForRound = (round, doc, threshold = DEFAULT_CONVERGENCE_THRESHOLD) => ({ round, convergence: convergenceSignal(doc, threshold) });
601
644
  var signalMarker = (signal) => `<!-- code-review:signal;base64 ${Buffer.from(
602
645
  JSON.stringify({ schema_version: SURFACE_SCHEMA_VERSION, ...signal }),
603
646
  "utf-8"
@@ -660,12 +703,9 @@ var projectPatch = (patch) => {
660
703
  return typeof lowered === "string" ? { kind: "suggestion", text: escapeFence(lowered) } : { kind: "patch", raw: escapeFence(patch) };
661
704
  };
662
705
  var formatConfidence = (n) => n.toFixed(2);
663
- var reviewBodyPointer = (headSha, stickyUrl, marker) => {
706
+ var reviewBodyPointer = (headSha, stickyUrl) => {
664
707
  const sha7 = headSha.slice(0, 7);
665
- 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.`;
666
- return marker ? `${marker}
667
-
668
- ${linkLine}` : linkLine;
708
+ 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.`;
669
709
  };
670
710
  var asRecord = (u) => typeof u === "object" && u !== null && !Array.isArray(u) ? u : null;
671
711
  var errMsg = (e) => e instanceof Error ? e.message : String(e);
@@ -1007,6 +1047,13 @@ var sanitizeFinding = (f, answeredNotes) => {
1007
1047
  answeredNote: answeredNotes !== void 0 && Object.prototype.hasOwnProperty.call(answeredNotes, key2) ? answeredNotes[key2] ?? "" : ""
1008
1048
  };
1009
1049
  };
1050
+ var sanitizeSuppressedNit = (f) => ({
1051
+ title: escapeCodeBackticks(f.title),
1052
+ ...f.code !== void 0 ? { code: escapeCodeBackticks(f.code) } : {},
1053
+ path: escapeCodeBackticks(f.path),
1054
+ startLine: f.start_line,
1055
+ m: formatConfidence(f.confidence * f.likelihood)
1056
+ });
1010
1057
  var sanitizeSystemic = (s) => ({
1011
1058
  ...s,
1012
1059
  title: escapePipes(s.title),
@@ -1043,7 +1090,6 @@ var render = (input) => {
1043
1090
  const rounds = input.rounds ?? [];
1044
1091
  const sameRootNotes = input.sameRootNotes ?? computeSameRootNotes(rounds.slice(0, -1), input.findings.findings);
1045
1092
  const isFullReviewRound = (input.convergenceRound ?? (isConvergenceRound(route, incomplete) && rounds.length > 0)) && isReviewVerdict(input.findings.verdict);
1046
- const convergenceCounts = rounds[rounds.length - 1] ?? computeRoundCounts(input.findings);
1047
1093
  const advisoryAllowed = isFullReviewRound;
1048
1094
  return eta.renderString(input.template, {
1049
1095
  findings: input.findings,
@@ -1060,8 +1106,10 @@ var render = (input) => {
1060
1106
  reviewedSha: input.reviewedSha ?? "0000000000000000000000000000000000000000",
1061
1107
  postedAt: input.postedAt ?? "",
1062
1108
  severityCounts,
1063
- convergenceSummary: isFullReviewRound ? convergenceSummary(convergenceCounts, input.convergenceThreshold) : "",
1109
+ convergenceSummary: isFullReviewRound ? convergenceSummary(input.findings, input.convergenceThreshold) : "",
1064
1110
  strays: (input.strays ?? []).map((f) => sanitizeFinding(f, input.answeredNotes)),
1111
+ suppressedNits: (input.suppressedNits ?? []).map(sanitizeSuppressedNit),
1112
+ nitVisibilityFloor: input.nitVisibilityFloor ?? DEFAULT_NIT_VISIBILITY_FLOOR,
1065
1113
  systemic: (input.findings.systemic_problems ?? []).map(sanitizeSystemic),
1066
1114
  unanchoredCount: input.unanchoredCount ?? 0,
1067
1115
  inlineDisposition: input.inlineDisposition ?? null,
@@ -1070,11 +1118,11 @@ var render = (input) => {
1070
1118
  findingsPointer: input.findingsPointer ?? surfacedFindingsPointer(
1071
1119
  input.findings,
1072
1120
  // The fallback embeds a signal exactly when the badge renders — never beside a suppressed
1073
- // badge, and from the same counts the badge reads. It assumes a post-style history (the
1121
+ // badge, and scores the same findings the badge does. It assumes a post-style history (the
1074
1122
  // caller appends this run's counts last), numbering the round exactly as the trajectory
1075
1123
  // label does; post always supplies the marker, so this path cannot disagree with it in
1076
1124
  // production (issue #141 review r4).
1077
- isFullReviewRound && rounds.length > 0 ? signalForRound(rounds.length, convergenceCounts, input.convergenceThreshold) : null,
1125
+ isFullReviewRound && rounds.length > 0 ? signalForRound(rounds.length, input.findings, input.convergenceThreshold) : null,
1078
1126
  input.jsonUrl
1079
1127
  ),
1080
1128
  roundsMarker: roundsMarker(rounds),
@@ -1444,6 +1492,7 @@ var sidecarPath = (draftPath, postfix) => {
1444
1492
  };
1445
1493
  var priorContextPath = (draftPath) => sidecarPath(draftPath, ".prior");
1446
1494
  var priorAnswersPath = (draftPath) => sidecarPath(draftPath, ".prior-answers");
1495
+ var priorSuppressedPath = (draftPath) => sidecarPath(draftPath, ".prior-suppressed");
1447
1496
  var lastValidPath = (draftPath) => sidecarPath(draftPath, ".last-valid");
1448
1497
  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.`;
1449
1498
  var forceBackgroundSpawn = (toolInput) => ({
@@ -1656,6 +1705,14 @@ var findingsTable = [
1656
1705
  },
1657
1706
  {
1658
1707
  minor: "0.6",
1708
+ defaultVersion: "0.6.0",
1709
+ schemaFile: "findings.schema.json",
1710
+ codec: FindingsCodec,
1711
+ normalize: identity,
1712
+ latest: false
1713
+ },
1714
+ {
1715
+ minor: "0.9",
1659
1716
  defaultVersion: DEFAULT_SCHEMA_VERSION,
1660
1717
  schemaFile: "findings.schema.json",
1661
1718
  codec: FindingsCodec,
@@ -2026,8 +2083,8 @@ var commentPayload = (c) => ({
2026
2083
  ...c.start_line !== void 0 && c.start_side !== void 0 ? { start_line: c.start_line, start_side: c.start_side } : {},
2027
2084
  body: formatMarkdown(c.body)
2028
2085
  });
2029
- var postInlineReview = async (repo, prNumber, headSha, comments, inDiff, stickyUrl, marker, ghApi) => {
2030
- const pointer = reviewBodyPointer(headSha, stickyUrl, marker);
2086
+ var postInlineReview = async (repo, prNumber, headSha, comments, inDiff, stickyUrl, ghApi) => {
2087
+ const pointer = reviewBodyPointer(headSha, stickyUrl);
2031
2088
  const reviewBody = (withComments) => JSON.stringify({
2032
2089
  body: pointer,
2033
2090
  commit_id: headSha,
@@ -2375,6 +2432,12 @@ ${dropNote}` : ""}`,
2375
2432
  findings: [...answeredFilter.findings],
2376
2433
  ...systemic.length > 0 ? { systemic_problems: systemic } : {}
2377
2434
  };
2435
+ const priorSuppressedKeys = new Set(
2436
+ (existingSticky !== null && isFullReviewSticky(existingSticky.body) ? priorBelowFloorNits(parseFindingsMarker(existingSticky.body), input.nitVisibilityFloor) : []).map((n) => answeredNoteKey({ code: n.code, title: n.title }))
2437
+ );
2438
+ const isSuppressedNit = (f) => f.severity === "nit" && (isBelowVisibilityFloor(f, input.nitVisibilityFloor) || priorSuppressedKeys.has(answeredNoteKey(f)));
2439
+ const suppressedNits = findings.findings.filter(isSuppressedNit);
2440
+ const visibleFindings = findings.findings.filter((f) => !isSuppressedNit(f));
2378
2441
  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._" : "");
2379
2442
  const envelope = loadEnvelope(input.envelopePath);
2380
2443
  const testReport = input.testReportPath ? loadTestReport(input.testReportPath) : void 0;
@@ -2401,10 +2464,15 @@ ${dropNote}` : ""}`,
2401
2464
  rounds: priorRounds,
2402
2465
  sameRootNotes: {},
2403
2466
  // The answered-state honesty rules apply on EVERY surface that renders the filtered
2404
- // findings — the lost-envelope branch lists every finding (no inline review exists to
2467
+ // findings — the lost-envelope branch lists every VISIBLE finding (no inline review exists to
2405
2468
  // carry them) so the kept re-raises' annotations actually render, and names the drops
2406
- // exactly like the main path (issues #151 review r1 + r2).
2407
- strays: findings.findings,
2469
+ // exactly like the main path (issues #151 review r1 + r2). The nit visibility floor applies
2470
+ // here too (issue #164): below-floor nits are hidden from the human list and shown only in the
2471
+ // collapsed aside — the floor is a human-visibility policy, not an inline-comment policy, so it
2472
+ // must hold on the surface that lists findings without an inline review.
2473
+ strays: visibleFindings,
2474
+ suppressedNits,
2475
+ nitVisibilityFloor: input.nitVisibilityFloor,
2408
2476
  answeredNotes: reRaisedNotes,
2409
2477
  answeredReRaiseNote: answeredDropNote,
2410
2478
  roundCount: priorSignal?.round ?? priorRounds.length,
@@ -2441,7 +2509,7 @@ ${dropNote}` : ""}`,
2441
2509
  comments: rawComments,
2442
2510
  strays,
2443
2511
  inDiff
2444
- } = buildInlineComments(findings.findings, diff, {
2512
+ } = buildInlineComments(visibleFindings, diff, {
2445
2513
  inlineTemplate,
2446
2514
  models: envelope.models.map((m) => m.model),
2447
2515
  findings,
@@ -2472,7 +2540,7 @@ ${dropNote}` : ""}`,
2472
2540
  roundNumber
2473
2541
  )
2474
2542
  ] : priorRounds;
2475
- const signal = isRound ? signalForRound(roundNumber, computeRoundCounts(findings), input.convergenceThreshold) : thisIncomplete || !isReviewVerdict(findings.verdict) ? null : priorSignal;
2543
+ const signal = isRound ? signalForRound(roundNumber, findings, input.convergenceThreshold) : thisIncomplete || !isReviewVerdict(findings.verdict) ? null : priorSignal;
2476
2544
  const findingsMarker = findingsMarkerFor(findings, signal);
2477
2545
  const markerForm = findingsMarkerForm(findings, input.jsonUrl);
2478
2546
  const signalNote = signal !== null || priorSignal !== null ? " (the stop signal still rides the compact marker)" : "";
@@ -2505,8 +2573,10 @@ ${dropNote}` : ""}`,
2505
2573
  answeredReRaiseNote: answeredDropNote,
2506
2574
  roundCount: signal?.round ?? priorSignal?.round ?? priorRounds.length,
2507
2575
  convergenceThreshold: input.convergenceThreshold,
2576
+ nitVisibilityFloor: input.nitVisibilityFloor,
2508
2577
  convergenceRound: isRound,
2509
2578
  strays,
2579
+ suppressedNits,
2510
2580
  runUrl: input.runUrl,
2511
2581
  jsonUrl: input.jsonUrl,
2512
2582
  findingsPointer: findingsMarker,
@@ -2551,7 +2621,6 @@ ${dropNote}` : ""}`,
2551
2621
  comments,
2552
2622
  inDiff,
2553
2623
  stickyRef?.url,
2554
- findingsMarker,
2555
2624
  ghApi
2556
2625
  );
2557
2626
  process.stderr.write(
@@ -3664,7 +3733,8 @@ var resolvePrices = (pricesArg) => {
3664
3733
  return { kind: "absent", path: bundledPath("schema", "prices.example.json") };
3665
3734
  };
3666
3735
  var TEST_REPORT_DESCRIPTION = 'Path to a JSON test summary: {"passed": number, "failed": number, "total": number, "failures"?: [{"name": string, "message"?: string}]}';
3667
- 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)";
3736
+ 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)";
3737
+ 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)";
3668
3738
  var renderCmd = defineCommand({
3669
3739
  meta: {
3670
3740
  name: "render",
@@ -3708,6 +3778,10 @@ var renderCmd = defineCommand({
3708
3778
  "convergence-threshold": {
3709
3779
  type: "string",
3710
3780
  description: CONVERGENCE_THRESHOLD_DESCRIPTION
3781
+ },
3782
+ "nit-visibility-floor": {
3783
+ type: "string",
3784
+ description: NIT_VISIBILITY_FLOOR_DESCRIPTION
3711
3785
  }
3712
3786
  },
3713
3787
  run: async ({ args }) => {
@@ -3733,6 +3807,7 @@ var renderCmd = defineCommand({
3733
3807
  testReport,
3734
3808
  rounds: isRound ? [counts] : [],
3735
3809
  convergenceThreshold: parseConvergenceThreshold(args["convergence-threshold"]),
3810
+ nitVisibilityFloor: parseNitVisibilityFloor(args["nit-visibility-floor"]),
3736
3811
  postedAt: formatUtc(/* @__PURE__ */ new Date())
3737
3812
  });
3738
3813
  process.stdout.write(output2);
@@ -3757,13 +3832,19 @@ var inlineCmd = defineCommand({
3757
3832
  template: {
3758
3833
  type: "string",
3759
3834
  description: "Path to inline comment Eta template (default: bundled templates/inline.eta)"
3835
+ },
3836
+ "nit-visibility-floor": {
3837
+ type: "string",
3838
+ description: NIT_VISIBILITY_FLOOR_DESCRIPTION
3760
3839
  }
3761
3840
  },
3762
3841
  run: async ({ args }) => {
3763
3842
  const findings = decode(FindingsCodec.decode(readJSON(args.findings)), "findings");
3764
3843
  const diff = readFileSync(resolve$1(args.diff), "utf-8");
3765
3844
  const inlineTemplate = readFileSync(resolveInlineTemplatePath(args.template), "utf-8");
3766
- const { comments, strays } = buildInlineComments(findings.findings, diff, {
3845
+ const floor = parseNitVisibilityFloor(args["nit-visibility-floor"]);
3846
+ const visibleFindings = findings.findings.filter((f) => !isBelowVisibilityFloor(f, floor));
3847
+ const { comments, strays } = buildInlineComments(visibleFindings, diff, {
3767
3848
  inlineTemplate,
3768
3849
  findings
3769
3850
  });
@@ -3865,6 +3946,26 @@ var parseConvergenceThreshold = (raw) => {
3865
3946
  }
3866
3947
  return n;
3867
3948
  };
3949
+ var parseNitVisibilityFloor = (raw) => {
3950
+ const trimmed = raw?.trim();
3951
+ if (trimmed === void 0 || trimmed === "") return void 0;
3952
+ if (!/^\d+(\.\d+)?$/.test(trimmed)) {
3953
+ fail(`--nit-visibility-floor must be a number in [0, 1]; got "${trimmed}"`);
3954
+ }
3955
+ const n = Number.parseFloat(trimmed);
3956
+ if (!Number.isFinite(n) || n < 0 || n > 1) {
3957
+ fail(
3958
+ `--nit-visibility-floor must be in [0, 1] (it gates confidence \xD7 likelihood); got "${trimmed}"`
3959
+ );
3960
+ }
3961
+ return n;
3962
+ };
3963
+ var parseNitVisibilityFloorLenient = (raw) => {
3964
+ const trimmed = raw?.trim();
3965
+ if (trimmed === void 0 || trimmed === "" || !/^\d+(\.\d+)?$/.test(trimmed)) return void 0;
3966
+ const n = Number.parseFloat(trimmed);
3967
+ return Number.isFinite(n) && n >= 0 && n <= 1 ? n : void 0;
3968
+ };
3868
3969
  var transcriptPathOf = (input) => {
3869
3970
  const tp = (typeof input === "object" && input !== null ? input : {})["transcript_path"];
3870
3971
  return typeof tp === "string" ? tp : void 0;
@@ -4163,6 +4264,10 @@ var seedDraftCmd = defineCommand({
4163
4264
  type: "string",
4164
4265
  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"
4165
4266
  },
4267
+ "nit-visibility-floor": {
4268
+ type: "string",
4269
+ 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"
4270
+ },
4166
4271
  "head-sha": {
4167
4272
  type: "string",
4168
4273
  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"
@@ -4258,6 +4363,27 @@ var seedDraftCmd = defineCommand({
4258
4363
  } catch (err) {
4259
4364
  process.stderr.write(
4260
4365
  `Warning: could not read the answered-findings registry ${args["prior-answers"]} (${errMsg(err)}) \u2014 no prior-answers sidecar
4366
+ `
4367
+ );
4368
+ }
4369
+ }
4370
+ if (parsedPrior !== null && parseReviewedRoute(priorBody ?? "") === "full review") {
4371
+ try {
4372
+ const belowFloor = priorBelowFloorNits(
4373
+ parsedPrior,
4374
+ parseNitVisibilityFloorLenient(args["nit-visibility-floor"])
4375
+ );
4376
+ if (belowFloor.length > 0) {
4377
+ writeFileSync(priorSuppressedPath(outPath), `${JSON.stringify(belowFloor, null, 2)}
4378
+ `);
4379
+ process.stderr.write(
4380
+ `Seeded ${priorSuppressedPath(outPath)} with ${String(belowFloor.length)} below-floor nit(s) as adjudicated context
4381
+ `
4382
+ );
4383
+ }
4384
+ } catch (err) {
4385
+ process.stderr.write(
4386
+ `Warning: could not derive the prior below-floor nits (${errMsg(err)}) \u2014 no prior-suppressed sidecar
4261
4387
  `
4262
4388
  );
4263
4389
  }
@@ -4772,6 +4898,10 @@ var postCmd = defineCommand({
4772
4898
  "convergence-threshold": {
4773
4899
  type: "string",
4774
4900
  description: CONVERGENCE_THRESHOLD_DESCRIPTION
4901
+ },
4902
+ "nit-visibility-floor": {
4903
+ type: "string",
4904
+ description: NIT_VISIBILITY_FLOOR_DESCRIPTION
4775
4905
  }
4776
4906
  },
4777
4907
  run: async ({ args }) => {
@@ -4793,6 +4923,7 @@ var postCmd = defineCommand({
4793
4923
  runUrl: args["run-url"],
4794
4924
  jsonUrl: args["json-url"],
4795
4925
  convergenceThreshold: parseConvergenceThreshold(args["convergence-threshold"]),
4926
+ nitVisibilityFloor: parseNitVisibilityFloor(args["nit-visibility-floor"]),
4796
4927
  postedAt: formatUtc(/* @__PURE__ */ new Date())
4797
4928
  });
4798
4929
  }