Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gittensory.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,12 @@ review:
# configured level are suppressed from inline comments — never from gate blockers. Default: null (show all).
# min_finding_severity: major

# Display-only caps on how many blocker/nit lines render in the unified comment (#2049).
# Never removes a blocker from the gate decision — truncation applies to display only.
# max_findings:
# blockers: 5
# nits: 8

# Inline-comment layer toggles (#1956 / #1958). Bool | null. Default: null/false — byte-identical.
# Requires operator flag GITTENSORY_REVIEW_INLINE_COMMENTS + cutover allowlist + review.inline_comments: true.
# inline_comments: false
Expand Down
6 changes: 6 additions & 0 deletions config/examples/gittensory.full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,12 @@ review:
# configured level are suppressed from inline comments — never from gate blockers. Default: null (show all).
# min_finding_severity: major

# Display-only caps on how many blocker/nit lines render in the unified comment (#2049).
# Never removes a blocker from the gate decision — truncation applies to display only.
# max_findings:
# blockers: 5
# nits: 8

# Inline-comment layer toggles (#1956 / #1958). Bool | null. Default: null/false — byte-identical.
# Requires operator flag GITTENSORY_REVIEW_INLINE_COMMENTS + cutover allowlist + review.inline_comments: true.
# inline_comments: false
Expand Down
1 change: 1 addition & 0 deletions src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9503,6 +9503,7 @@ async function maybePublishPrPublicSurface(
...(findingCategoriesEnabledForReview && aiReview?.inlineFindings?.length
? { findingCategories: aiReview.inlineFindings }
: {}),
maxFindingsCaps: reviewConfig.maxFindings,
});
} else {
deterministicBody = buildPublicPrIntelligenceComment(commentArgs);
Expand Down
3 changes: 3 additions & 0 deletions src/review/unified-comment-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,8 @@ export type UnifiedCommentBridgeArgs = {
* through to `buildUnifiedReviewInput`'s `reviewEffort`). No AI. Default OFF (the processor passes this only
* when the manifest opts in — see `resolveReviewPromptOverrides`'s `effortScore`). (#1955) */
reviewEffort?: { band: 1 | 2 | 3 | 4 | 5; minutes: number } | undefined;
/** Display-only caps from `review.max_findings` (#2049). */
maxFindingsCaps?: { blockers: number | null; nits: number | null } | undefined;
/** Line-anchored AI findings, one entry per inline finding (review.finding_categories port). When present +
* non-empty, a "Finding categories" collapsible (a count per security/correctness/performance/maintainability/
* tests/style category) is appended. A finding missing its own `category` falls back to
Expand Down Expand Up @@ -552,6 +554,7 @@ export function buildUnifiedCommentBody(args: UnifiedCommentBridgeArgs): string
...(args.mergeReadiness !== undefined ? { readiness: args.mergeReadiness } : {}),
...(args.merged !== undefined ? { merged: args.merged } : {}),
...(args.reviewEffort !== undefined ? { reviewEffort: args.reviewEffort } : {}),
...(args.maxFindingsCaps !== undefined ? { maxFindingsCaps: args.maxFindingsCaps } : {}),
});
// The gate already produced 0/1 reviewer notes from a synthesis of the model pair; reflect the caller's
// actual reviewer count (for the chip + the "N reviewers, synthesized" evidence) without re-deriving it.
Expand Down
40 changes: 35 additions & 5 deletions src/review/unified-comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ export interface UnifiedReviewInput {
consensusBlocker?: boolean;
/** Reviewers that produced no parseable verdict (a partial review → held, not ready). */
failedCount?: number;
/** Display-only caps from `review.max_findings` — truncate rendered blocker/nit lists with a "+N more" footer.
* Never affects gate logic. Absent/null sub-fields ⇒ byte-identical. (#2049) */
maxFindingsCaps?: { blockers: number | null; nits: number | null };
/** Deterministic per-PR review-effort estimate (`estimateReviewEffort`, `src/review/review-effort.ts`) — a
* 1-5 complexity band + a minutes estimate from the changed files' added-line volume and file-type mix. No
* AI. Rendered as a compact `review effort: N/5 (~M min)` chip only when the host passes this (gated by
Expand Down Expand Up @@ -377,6 +380,21 @@ function dedupeLines(items: string[], cap = 12): string[] {
return out;
}

/** Truncate a findings list for display-only rendering. Null/undefined cap ⇒ unchanged. */
export function truncateFindingsForDisplay(
items: string[],
cap: number | null | undefined,
): { shown: string[]; hiddenCount: number } {
if (cap === null || cap === undefined) return { shown: items, hiddenCount: 0 };
if (cap <= 0) return { shown: [], hiddenCount: items.length };
if (items.length <= cap) return { shown: items, hiddenCount: 0 };
return { shown: items.slice(0, cap), hiddenCount: items.length - cap };
}

function appendMoreFooter(lines: string, hiddenCount: number): string {
return hiddenCount > 0 ? `${lines}\n- _+${hiddenCount} more_` : lines;
}

/** Escape angle brackets in caller-provided public text so raw HTML, HTML comments,
* or stray closing tags cannot change the GitHub comment structure. */
function escapePublicHtmlAngles(text: string): string {
Expand Down Expand Up @@ -504,13 +522,23 @@ export function renderUnifiedReviewComment(input: UnifiedReviewInput, ctx: Unifi

if (input.summary.trim()) blocks.push(`**Review summary**\n${escapePublicHtmlAngles(input.summary.trim())}`);

const nits = dedupeLines(input.nits ?? []);
if (nits.length) blocks.push(details("Nits", taskList(nits), `${nits.length} non-blocking`));
const nitsAll = dedupeLines(input.nits ?? []);
const nitsTrunc = truncateFindingsForDisplay(nitsAll, input.maxFindingsCaps?.nits);
if (nitsAll.length) {
const nitsBody = nitsTrunc.shown.length
? appendMoreFooter(taskList(nitsTrunc.shown), nitsTrunc.hiddenCount)
: `_+${nitsTrunc.hiddenCount} more_`;
blocks.push(details("Nits", nitsBody, `${nitsAll.length} non-blocking`));
}

const blockers = dedupeLines(input.blockers ?? []);
if (blockers.length) {
const blockersAll = dedupeLines(input.blockers ?? []);
const blockersTrunc = truncateFindingsForDisplay(blockersAll, input.maxFindingsCaps?.blockers);
if (blockersAll.length) {
const heading = status === "blocked" ? "Why this is blocked" : "Concerns raised — review before merging";
blocks.push(`**${heading}**\n${bullets(blockers)}`);
const blockersBody = blockersTrunc.shown.length
? appendMoreFooter(bullets(blockersTrunc.shown), blockersTrunc.hiddenCount)
: `_+${blockersTrunc.hiddenCount} more_`;
blocks.push(`**${heading}**\n${blockersBody}`);
}

// Failing CI checks — list WHICH checks failed and WHY (codecov %/test/lint reason) under the "CI failing"
Expand Down Expand Up @@ -554,6 +582,7 @@ export function buildUnifiedReviewInput(opts: {
merged?: boolean;
verdictReason?: string;
reviewEffort?: { band: 1 | 2 | 3 | 4 | 5; minutes: number };
maxFindingsCaps?: { blockers: number | null; nits: number | null };
}): UnifiedReviewInput {
const ex = extractReviewSummary(opts.reviews);
const changedFiles = typeof opts.changedFiles === "number" ? opts.changedFiles : opts.changedFiles.length;
Expand All @@ -571,6 +600,7 @@ export function buildUnifiedReviewInput(opts: {
...(opts.merged !== undefined ? { merged: opts.merged } : {}),
...(opts.verdictReason !== undefined ? { verdictReason: opts.verdictReason } : {}),
...(opts.reviewEffort !== undefined ? { reviewEffort: opts.reviewEffort } : {}),
...(opts.maxFindingsCaps !== undefined ? { maxFindingsCaps: opts.maxFindingsCaps } : {}),
};
}

Expand Down
Loading