Skip to content
Closed
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
8 changes: 8 additions & 0 deletions .gittensory.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,10 @@ review:
# finding_categories: false
# When true, inline findings are tagged with a category label (requires inline_comments).

# Read-only auto-merge conditions table (#2051): when enabled, the unified review comment includes a
# collapsible summary of CI/gate/merge-state/linked-issue readiness. Does NOT change merge decisions.
# auto_merge_summary: false

# Deterministic label suggestions (#2045). Each rule SUGGESTS a non-scoring label when a PR matches ALL of the
# `when` criteria it sets (at least one is required): when_paths (any changed path matches a glob), title_contains,
# description_contains (both case-insensitive). Suggestions are advisory; they are auto-applied only when the repo's
Expand Down Expand Up @@ -783,6 +787,10 @@ settings:
# # compact "review effort: N/5 (~M min)" chip -- a deterministic, no-AI complexity/time estimate from the
# # changed files' added-line volume and file-type mix. Bool or null. Default: null/false.
# effort_score: false
# # When true, the unified review comment gains a read-only "Auto-merge conditions" collapsible: a table of
# # which merge prerequisites currently pass/fail (CI green, gate passing, mergeable-clean, valid linked issue).
# # Surface only — does NOT change the merge/close decision. Bool or null. Default: null/false.
# auto_merge_summary: false
# # When true, an inline finding is ALSO tagged with a category (security/correctness/performance/
# # maintainability/tests/style) -- the AI reviewer self-categorizes, with a deterministic path/keyword
# # fallback for whatever it omits. Only takes effect when inline_comments is already on. Bool or null.
Expand Down
8 changes: 8 additions & 0 deletions config/examples/gittensory.full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,10 @@ review:
# finding_categories: false
# When true, inline findings are tagged with a category label (requires inline_comments).

# Read-only auto-merge conditions table (#2051): when enabled, the unified review comment includes a
# collapsible summary of CI/gate/merge-state/linked-issue readiness. Does NOT change merge decisions.
# auto_merge_summary: false

# Deterministic label suggestions (#2045). Each rule SUGGESTS a non-scoring label when a PR matches ALL of the
# `when` criteria it sets (at least one is required): when_paths (any changed path matches a glob), title_contains,
# description_contains (both case-insensitive). Suggestions are advisory; they are auto-applied only when the repo's
Expand Down Expand Up @@ -796,6 +800,10 @@ settings:
# # compact "review effort: N/5 (~M min)" chip -- a deterministic, no-AI complexity/time estimate from the
# # changed files' added-line volume and file-type mix. Bool or null. Default: null/false.
# effort_score: false
# # When true, the unified review comment gains a read-only "Auto-merge conditions" collapsible: a table of
# # which merge prerequisites currently pass/fail (CI green, gate passing, mergeable-clean, valid linked issue).
# # Surface only — does NOT change the merge/close decision. Bool or null. Default: null/false.
# auto_merge_summary: false
# # When true, an inline finding is ALSO tagged with a category (security/correctness/performance/
# # maintainability/tests/style) -- the AI reviewer self-categorizes, with a deterministic path/keyword
# # fallback for whatever it omits. Only takes effect when inline_comments is already on. Bool or null.
Expand Down
3 changes: 3 additions & 0 deletions src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7684,6 +7684,7 @@ async function maybePublishPrPublicSurface(
let suggestionsEnabledForReview = false;
let changedFilesSummaryEnabledForReview = false;
let effortScoreEnabledForReview = false;
let autoMergeSummaryEnabledForReview = false;
let findingCategoriesEnabledForReview = false;
let minFindingSeverityForReview: ReviewFindingSeverity | null = null;
let aiReviewExpected = false;
Expand Down Expand Up @@ -8190,6 +8191,7 @@ async function maybePublishPrPublicSurface(
changedFilesSummaryEnabledForReview = deterministicReviewOverrides.changedFilesSummary;
effortScoreEnabledForReview = deterministicReviewOverrides.effortScore;
minFindingSeverityForReview = deterministicReviewOverrides.minFindingSeverity;
autoMergeSummaryEnabledForReview = deterministicReviewOverrides.autoMergeSummary;
const aiReviewWillRun =
!authorBlacklisted &&
!isFrozenForManualReview &&
Expand Down Expand Up @@ -9425,6 +9427,7 @@ async function maybePublishPrPublicSurface(
),
}
: {}),
...(autoMergeSummaryEnabledForReview ? { autoMergeSummary: true } : {}),
...(findingCategoriesEnabledForReview && aiReview?.inlineFindings?.length
? { findingCategories: aiReview.inlineFindings }
: {}),
Expand Down
115 changes: 114 additions & 1 deletion src/review/unified-comment-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ import { classifyChangedFile, type ReviewFileClass } from "./changed-files-class
import { classifyFindingCategory, FINDING_CATEGORIES, type FindingCategory } from "./finding-category-classify";
import {
buildUnifiedReviewInput,
buildAutoMergeSummaryCollapsible,
renderUnifiedReviewComment,
type AutoMergeConditionRow,
type DualReviewNote,
type MergeReadiness,
type ReviewNotes,
Expand Down Expand Up @@ -325,6 +327,11 @@ export type UnifiedCommentBridgeArgs = {
* `classifyFindingCategory` — never omitted from the count. Default OFF (the processor passes this only when
* the manifest opts in — see `resolveReviewPromptOverrides`'s `findingCategories`). (#1958) */
findingCategories?: FindingCategoryInput[] | undefined;
/** When true, append a read-only "Auto-merge conditions" collapsible built from the ALREADY-computed gate,
* merge-readiness, and panel signal rows passed into this call. Display-only — does NOT change the merge/close
* decision. Default OFF (the processor passes this only when the manifest opts in — see
* `resolveReviewPromptOverrides`'s `autoMergeSummary`). (#2051) */
autoMergeSummary?: boolean | undefined;
/** The disposition holds this PR for owner review because its diff touches a hard-guardrail path — so an
* otherwise-ready comment renders "held for review" instead of "safe to merge". (#guarded-hold-comment) */
heldForReview?: boolean | undefined;
Expand Down Expand Up @@ -515,6 +522,99 @@ export function buildFindingCategoryCollapsible(findings: FindingCategoryInput[]
return { title: "Finding categories", body };
}

/** Map a legacy panel result cell's leading status icon (✅/⚠️/❌) → an auto-merge condition state. */
function panelResultToConditionState(resultCell: string): AutoMergeConditionRow["state"] {
return rowState(resultCell);
}

/** Strip the leading status icon from a panel result cell for human-readable evidence text. */
function panelResultEvidence(resultCell: string, detailCell: string): string {
const detail = detailCell.trim();
const result = rowResultText(resultCell);
if (detail && result) return `${result} — ${detail}`;
return detail || result || "No details.";
}

/**
* Derive the four standard auto-merge condition rows from ALREADY-computed readiness signals — the gate
* evaluation, merge-readiness facts, and legacy panel rows the caller already resolved. Pure + read-only:
* does NOT call `deriveUnifiedStatus` or any merge/close decision path. (#2051)
*/
export function deriveAutoMergeConditionsFromSignals(args: {
gate: GateCheckEvaluation;
mergeReadiness?: MergeReadiness | undefined;
panelRows: PublicPrPanelSignalRow[];
}): AutoMergeConditionRow[] {
const linkedIssueRow = args.panelRows.find((row) => row.key === "linkedIssue");
const gateResultRow = args.panelRows.find((row) => row.key === "gateResult");

const ciState = args.mergeReadiness?.ciState;
const ciRow: AutoMergeConditionRow = {
condition: "CI green",
state: ciState === "passed" ? "ok" : ciState === "failed" ? "fail" : "warn",
evidence:
ciState === "passed"
? "All required CI checks are green."
: ciState === "failed"
? args.mergeReadiness?.failingChecks?.length
? `Failing: ${args.mergeReadiness.failingChecks.join(", ")}.`
: "CI checks are failing."
: ciState === "unverified"
? "CI is pending or unverified."
: "CI state was not resolved.",
};

const gateEnabled = args.gate.enabled;
const gateConclusion = args.gate.conclusion;
const gateRow: AutoMergeConditionRow = gateResultRow
? {
condition: "Gate passing",
state: panelResultToConditionState(gateResultRow.cells[1] ?? ""),
evidence: panelResultEvidence(gateResultRow.cells[1] ?? "", gateResultRow.cells[2] ?? ""),
}
: {
condition: "Gate passing",
state: !gateEnabled ? "warn" : gateConclusion === "success" ? "ok" : gateConclusion === "failure" ? "fail" : "warn",
evidence: !gateEnabled
? "Gate is advisory-only (not enforcing)."
: gateConclusion === "success"
? "No configured hard blocker found."
: gateConclusion === "failure"
? "Repo-configured hard blocker found."
: gateConclusion === "action_required"
? "Install/config needs attention."
: "Gate is not blocking this PR.",
};

const mergeState = args.mergeReadiness?.mergeStateLabel?.trim().toLowerCase();
const mergeRow: AutoMergeConditionRow = {
condition: "Mergeable / clean",
state:
mergeState === "clean"
? "ok"
: mergeState === "dirty" || mergeState === "behind"
? "fail"
: "warn",
evidence: mergeState
? `GitHub merge state: ${args.mergeReadiness?.mergeStateLabel}.`
: "Merge state was not resolved.",
};

const linkedIssueRowResult: AutoMergeConditionRow = linkedIssueRow
? {
condition: "Valid linked issue",
state: panelResultToConditionState(linkedIssueRow.cells[1] ?? ""),
evidence: panelResultEvidence(linkedIssueRow.cells[1] ?? "", linkedIssueRow.cells[2] ?? ""),
}
: {
condition: "Valid linked issue",
state: "warn",
evidence: "Linked issue signal was not resolved.",
};

return [ciRow, gateRow, mergeRow, linkedIssueRowResult];
}

/**
* Build the unified PR-review comment body from gittensory's live data. Returns a string that STARTS with
* the panel marker (so the existing upsert updates in place) followed by the rendered unified comment.
Expand Down Expand Up @@ -569,6 +669,19 @@ export function buildUnifiedCommentBody(args: UnifiedCommentBridgeArgs): string
const visibleRows = args.panelRows.filter((row) => args.reviewFields?.[row.key] !== false);
const signals = panelRowsToSignalRows(visibleRows);

// review.auto_merge_summary (#2051): read-only conditions table from signals the caller already computed.
const autoMergeCollapsible = args.autoMergeSummary
? buildAutoMergeSummaryCollapsible(
deriveAutoMergeConditionsFromSignals({
gate: args.gate,
mergeReadiness: args.mergeReadiness,
panelRows: args.panelRows,
}),
)
: null;
const withAutoMerge =
autoMergeCollapsible !== null ? [autoMergeCollapsible, ...(args.extraCollapsibles ?? [])] : args.extraCollapsibles;

// review.changed_files_summary port: when the manifest opts in, the processor hands us every changed file's
// path + deltas here; append the grouped "Changed files" collapsible ahead of the visual preview (structure
// before pixels). Flag-OFF (the processor passes undefined) ⇒ extraCollapsibles is unchanged. (#1957)
Expand All @@ -577,7 +690,7 @@ export function buildUnifiedCommentBody(args: UnifiedCommentBridgeArgs): string
? buildChangedFilesSummaryCollapsible(args.changedFilesSummary)
: null;
const withChangedFiles =
changedFilesCollapsible !== null ? [...(args.extraCollapsibles ?? []), changedFilesCollapsible] : args.extraCollapsibles;
changedFilesCollapsible !== null ? [...(withAutoMerge ?? []), changedFilesCollapsible] : withAutoMerge;
// review.finding_categories port: when the manifest opts in, the processor hands us this review's line-anchored
// AI findings here; append the "Finding categories" collapsible right after Changed files (both are structural
// review-shape summaries, ahead of the visual preview). Flag-OFF (the processor passes undefined) ⇒
Expand Down
27 changes: 27 additions & 0 deletions src/review/unified-comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,33 @@ export interface UnifiedCommentContext {
reviewedAt?: string | number | Date | undefined;
}

/** One read-only auto-merge condition row — pre-computed by the bridge from already-resolved readiness
* signals; this renderer only formats them, never re-derives a merge/close decision. (#2051) */
export type AutoMergeConditionRow = {
condition: string;
state: UnifiedSignalRow["state"];
evidence: string;
};

/** Build the "Auto-merge conditions" collapsible from pre-computed condition rows. Pure + display-only —
* does NOT call `deriveUnifiedStatus` or any merge/close decision path. Returns null when empty. (#2051) */
export function buildAutoMergeSummaryCollapsible(conditions: AutoMergeConditionRow[]): UnifiedCollapsible | null {
if (conditions.length === 0) return null;
const lines = conditions.map((row) => {
const label = escapePublicHtmlAngles(row.condition);
const evidence = escapePublicHtmlAngles(row.evidence);
return `| ${label} | ${SIGNAL_ICON[row.state]} | ${evidence} |`;
});
const body = [
"| Condition | Status | Evidence |",
"|---|---|---|",
...lines,
"",
"_Read-only summary of conditions that must pass before auto-merge. Does not change the merge decision._",
].join("\n");
return { title: "Auto-merge conditions", body };
}

const STATUS_META: Record<UnifiedCommentStatus, { alert: string; square: string; icon: string }> = {
ready: { alert: "TIP", square: "🟩", icon: "✅" },
advisory: { alert: "NOTE", square: "🟦", icon: "💡" },
Expand Down
Loading
Loading