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
337 changes: 15 additions & 322 deletions packages/loopover-engine/src/signals/engine.ts

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions src/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,9 +302,10 @@ declare global {
* secret, never a public var. When absent, BYOK is unavailable and review uses the configured instance
* reviewer when available. */
TOKEN_ENCRYPTION_SECRET?: string;
/** Convergence (Stage D): when truthy, the public PR comment is rendered by the unified-comment bridge
* (ONE in-place comment in the converged shape) instead of the legacy `buildPublicPrIntelligenceComment`
* panel. Default OFF — unset/false keeps the legacy panel byte-identical. */
/** #6103: retired -- the unified-comment bridge is now the ONLY PR-comment renderer unconditionally (the
* legacy `buildPublicPrIntelligenceComment` panel this flag used to switch away from was deleted, having
* no remaining production caller). No longer read; kept only so an operator's existing deployment config
* setting it doesn't error. Safe to remove from any env once noticed. */
LOOPOVER_REVIEW_UNIFIED_COMMENT?: string;
/** Inline comments (#inline-comments): when truthy (AND the repo is in LOOPOVER_REVIEW_REPOS AND the repo's
* `.loopover.yml` sets `review.inline_comments: true`), the AI reviewer ALSO leaves quiet, NON-BLOCKING
Expand Down
73 changes: 28 additions & 45 deletions src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,6 @@ import {
buildContributorStrategy,
buildDuplicateWinnerRelatedWorkView,
buildPreflightResult,
buildPublicPrIntelligenceComment,
buildPublicPrPanelSignalRows,
buildPublicReadinessScore,
buildPublicSafeCollapsibles,
Expand Down Expand Up @@ -7700,22 +7699,13 @@ async function maybePublishPrPublicSurface(
// a dry-run / pause / global-freeze publishes NOTHING (check-run, comment, label) — the gate verdict is still
// computed + returned for the disposition logic, the writes are just suppressed + audited. (#dry-run-chokepoint)
const mode = await resolveRepoActionMode(env, settings);
// Per-repo feature override (phase 2): the unified converged comment renders for THIS repo when the global
// LOOPOVER_REVIEW_UNIFIED_COMMENT kill-switch is ON and the repo's container-private `.loopover.yml`
// `features.unifiedComment` opts in — falling back to the LOOPOVER_REVIEW_REPOS allowlist when the manifest
// says nothing (byte-identical default). Computed once and used by both unified-comment sites below.
const unifiedCommentAllowed = await convergedFeatureActive(
env,
repoFullName,
"unifiedComment",
);
// improvementSignal (#4744): the first real caller of #4738's activation wiring (epic #4737's config-as-code
// foundation) -- nothing resolved this feature before this PR (see signals/improvement.ts's own header
// comment). Resolved once, independent of unifiedCommentAllowed above: it gates BOTH the deterministic
// tier's own computation further below (which has no AI dependency at all -- a paused repo, a non-reviewable
// author, or aiReviewMode: "off" still gets it) and, threaded into runAiReviewForAdvisory, the LLM tier's
// prompt addition (#4743). loadRepoFocusManifest is cached, so this second manifest resolution costs no
// extra fetch in the common case where something else already resolved it this pass.
// comment). Gates BOTH the deterministic tier's own computation further below (which has no AI dependency
// at all -- a paused repo, a non-reviewable author, or aiReviewMode: "off" still gets it) and, threaded
// into runAiReviewForAdvisory, the LLM tier's prompt addition (#4743). loadRepoFocusManifest is cached, so
// this second manifest resolution costs no extra fetch in the common case where something else already
// resolved it this pass.
const improvementSignalAllowed = await convergedFeatureActive(
env,
repoFullName,
Expand Down Expand Up @@ -9242,9 +9232,8 @@ async function maybePublishPrPublicSurface(
reviewSelfHostAiModel,
reviewImpactMap,
reviewCultureProfile,
// improvementSignal (#4744): resolved once above (independent of unifiedCommentAllowed), reused
// here so the LLM tier's value-assessment prompt addition (#4743) only fires when this repo has
// actually opted in.
// improvementSignal (#4744): resolved once above, reused here so the LLM tier's value-assessment
// prompt addition (#4743) only fires when this repo has actually opted in.
improvementSignal: improvementSignalAllowed,
// #regate-dup-prep: this call's own advisory lock is already claimed (by aiReviewCacheReadDecideAndRun's
// caller, above) — pass it through so runAiReviewForAdvisory trusts it instead of re-claiming (and
Expand Down Expand Up @@ -9941,30 +9930,12 @@ async function maybePublishPrPublicSurface(
})),
})
: undefined;
const commentArgs = {
repo,
pr,
profile,
detection,
queueHealth,
collisions,
preflight,
settings,
gate: gateEvaluation,
review: reviewConfig,
aiReview,
improvementSignal: structuralImprovementAssessment,
// #4745: the risk × value quadrant's risk half -- reuses the slop band already computed above (if any);
// never a second buildSlopAssessment call.
slopBand: slopBand ?? undefined,
duplicateWinnerEnabled,
env,
};
let deterministicBody: string;
// Convergence (Stage D): when the unified-review-comment flag is ON, render the single converged comment
// (loopover shape + reviewbot's review folded in). The gate stays authoritative (passed as `decision`),
// and the body carries the SAME panel marker so the upsert updates in place. Flag-OFF (default) keeps the
// legacy panel byte-identical. Only the comment lane is affected; the gate check-run/labels/audit are not.
// Convergence (Stage D, #6103): the converged comment (loopover shape + reviewbot's review folded in) is
// the only comment path -- the legacy buildPublicPrIntelligenceComment panel was retired once it had no
// remaining production caller (settings-preview.ts's sample preview was migrated to this same renderer).
// The gate stays authoritative (passed as `decision`), and the body carries the SAME panel marker so the
// upsert updates in place.
//
// RECONCILIATION INVARIANT (#1016 — two-gate → one authoritative path; pinned by
// test/unit/unified-comment-bridge.test.ts "reconciliation invariant"):
Expand All @@ -9979,7 +9950,21 @@ async function maybePublishPrPublicSurface(
// contradict the review-agent check-run conclusion.
// 3. The `ai_consensus_defect` surfaces exactly ONCE — as the Code-review blocker — never also in the
// gate signal row (which renders only the conclusion-derived status text, not the defect string).
if (unifiedCommentAllowed && gateEvaluation) {
{
// #6103: the converged renderer is the only comment path (the legacy buildPublicPrIntelligenceComment
// panel was retired). When the gate was never evaluated for this repo (reviewCheckMode disabled AND no
// autonomy configured -- see shouldEvaluateGate above), synthesize a "skipped" gate for rendering
// purposes only, mirroring buildClosedUnifiedCommentBody's own pattern for the identical situation.
// This never touches the check-run/label/audit/disposition lanes (those still read the real,
// possibly-undefined `gateEvaluation`) -- only what this comment's verdict/"Gate result" row says.
const commentGateEvaluation: NonNullable<typeof gateEvaluation> = gateEvaluation ?? {
enabled: false,
conclusion: "skipped",
title: `${LOOPOVER_GATE_CHECK_NAME} skipped`,
summary: "Gate evaluation is not configured for this repository.",
blockers: [],
warnings: [],
};
// FIX B: the unified comment's file count + visual-capture path filter need the real diff — reuse the
// shared resolver (one resolve per review; inline-fetches when stored is still empty pre-detail-sync).
const unifiedFiles = await getReviewFiles();
Expand Down Expand Up @@ -10025,7 +10010,7 @@ async function maybePublishPrPublicSurface(
repoFullName,
});
// The public comment must match the authoritative Gate check-run conclusion.
const commentGate = gateEvaluation;
const commentGate = commentGateEvaluation;
// Observability (#reviews-dashboard): record the would-be gate verdict so the Grafana panel shows the
// merge/close/hold mix — the "are we rubber-stamping?" signal — even in advisory/dryRun (this is the rendered verdict).
incr("loopover_gate_decisions_total", {
Expand Down Expand Up @@ -10337,8 +10322,6 @@ async function maybePublishPrPublicSurface(
// render warnings from the full/private manifest here.
manifestWarnings: publicRepoFocusManifestForComment?.warnings ?? [],
});
} else {
deterministicBody = buildPublicPrIntelligenceComment(commentArgs);
}
try {
await withReviewPipelineSpan(
Expand Down
31 changes: 16 additions & 15 deletions src/review/unified-comment-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
//
// A PURE, testable mapping from loopover's live PR-review data (the gate `GateCheckEvaluation`, the AI
// `advisoryNotes` + consensus defect, the readiness signal rows + total, the footer) onto the ported
// unified renderer (`renderUnifiedReviewComment`). Flag-gated and default-OFF in the processor; flag-OFF
// keeps the legacy `buildPublicPrIntelligenceComment` path byte-identical.
// unified renderer (`renderUnifiedReviewComment`). #6103: this is now the ONLY PR-comment renderer --
// the legacy `buildPublicPrIntelligenceComment` path this used to sit alongside was deleted once it had
// no remaining production caller.
//
// loopover's GATE stays authoritative: we pass the gate-derived `decision` into `buildUnifiedReviewInput`
// so `deriveUnifiedStatus` lets it override the reviewer recommendations (the renderer already enforces
// this). The output PREPENDS the exact panel marker the legacy body carries, so the existing in-place
// upsert (`createOrUpdatePrIntelligenceComment`) updates the same comment instead of posting a duplicate.
// this). The output PREPENDS the panel marker, so the existing in-place upsert
// (`createOrUpdatePrIntelligenceComment`) updates the same comment instead of posting a duplicate.
//
// Public-safe: most inputs are already safe by construction — the AI notes via
// `composeAdvisoryNotes`→`toPublicSafe`; the consensus-defect blocker via `toPublicSafe` (in
Expand Down Expand Up @@ -64,8 +65,7 @@ export { splitAiReviewNits } from "./ai-notes";
// `warnings` (turned into Nits): they carry an AdvisoryFinding's raw title/action. The gate/check-run
// path sanitizes those strings (sanitizeForCheckRun) before they reach GitHub, but this comment path
// did not. Rather than trust that every present and FUTURE warning finding is benign, scrub Nits with a
// boundary mirroring the check-run sanitizer + the legacy panel's private-term guard, and DROP a Nit
// that still trips the guard. This never alters flag-OFF (the legacy panel keeps its own filtering).
// boundary mirroring the check-run sanitizer's own private-term guard, and DROP a Nit that still trips it.
//
// Mirrors src/rules/advisory.ts CHECK_RUN_FORBIDDEN_TERMS (scrubbed → "[context]") and
// src/signals/engine.ts containsPrivatePublicTerm (drop if still present). Kept inline so this module
Expand Down Expand Up @@ -114,7 +114,7 @@ export function verdictToRecommendation(verdict: Verdict): ReviewRecommendation
}
}

/** Derive an ok/warn/fail state from a legacy panel result cell's leading status icon (✅/⚠️/❌). */
/** Derive an ok/warn/fail state from a panel result cell's leading status icon (✅/⚠️/❌). */
function rowState(resultCell: string): UnifiedSignalRow["state"] {
if (resultCell.startsWith("✅")) return "ok";
if (resultCell.startsWith("❌")) return "fail";
Expand All @@ -129,11 +129,12 @@ function rowResultText(resultCell: string): string {
return resultCell.replace(/^[✅⚠️❌ℹ️]+\s*/u, "").trim();
}

/** Map the legacy panel signal rows → the unified table's rows (label/state/result/evidence). The
* unified renderer adds its own "Code review" row first; these follow it (loopover's gate row included).
* `gates: true` only for the "Gate result" row (#6067) -- the ONLY row among these that can actually move
* the verdict; every other row's own Evidence/Action text already says it's advisory-only. Drives the split
* between the renderer's always-visible "Decision drivers" list and its collapsed advisory-signals fold. */
/** Map the panel signal rows (buildPublicPrPanelSignalRows) → the unified table's rows
* (label/state/result/evidence). The unified renderer adds its own "Code review" row first; these follow
* it (loopover's gate row included). `gates: true` only for the "Gate result" row (#6067) -- the ONLY row
* among these that can actually move the verdict; every other row's own Evidence/Action text already says
* it's advisory-only. Drives the split between the renderer's always-visible "Decision drivers" list and
* its collapsed advisory-signals fold. */
export function panelRowsToSignalRows(rows: PublicPrPanelSignalRow[]): UnifiedSignalRow[] {
return rows.map((row) => {
const [label, result, evidence] = row.cells;
Expand Down Expand Up @@ -322,7 +323,7 @@ export type UnifiedCommentBridgeArgs = {
aiReview?: { notes: string } | undefined;
/** The advisory findings — the bridge recovers the `ai_consensus_defect` consensus blocker from here. */
advisoryFindings?: AdvisoryFinding[] | undefined;
/** The legacy panel readiness signal rows (from `buildPublicPrPanelSignalRows`). */
/** The readiness signal rows (from `buildPublicPrPanelSignalRows`). */
panelRows: PublicPrPanelSignalRow[];
/** Which rows the maintainer kept visible (`.loopover.yml review.fields`); a key set to `false` is hidden. */
reviewFields?: Partial<Record<PublicPrPanelSignalRow["key"], boolean>> | undefined;
Expand Down Expand Up @@ -822,7 +823,7 @@ export function buildUnifiedCommentBody(args: UnifiedCommentBridgeArgs): string
: input.reviewerCount
: 0;

// Honor `.loopover.yml review.fields` row visibility, exactly as the legacy panel does.
// Honor `.loopover.yml review.fields` row visibility.
const visibleRows = args.panelRows.filter((row) => args.reviewFields?.[row.key] !== false);
const signals = panelRowsToSignalRows(visibleRows);

Expand Down Expand Up @@ -921,7 +922,7 @@ export function buildUnifiedCommentBody(args: UnifiedCommentBridgeArgs): string
* routed through `buildUnifiedCommentBody` so a comment that started life as a unified OPEN-PR comment keeps
* its unified shape (and the SAME marker) when the PR closes, instead of being overwritten by the legacy
* panel under the shared marker. A synthetic `skipped` gate maps (via `gateConclusionToVerdict`) to the
* `comment` verdict → `advisory` status, matching the legacy panel's non-blocking NOTE tone. No AI review,
* `comment` verdict → `advisory` status (a non-blocking NOTE tone). No AI review,
* no findings, and a single synthetic "Gate result — Skipped" signal row (the only signal we can assert for
* a PR we never finished evaluating). Public-safe by construction: every string here is a static literal.
*/
Expand Down
44 changes: 41 additions & 3 deletions src/signals/settings-preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,20 @@ import {
buildCollisionReport,
buildContributorProfile,
buildPreflightResult,
buildPublicPrIntelligenceComment,
buildPublicPrPanelSignalRows,
buildPublicReadinessScore,
buildQueueHealth,
type ContributorDetection,
} from "./engine";
import { buildExtensionPrStatus, type ExtensionPrStatus } from "./extension-contributor-context";
import { REQUIRED_INSTALLATION_PERMISSIONS } from "../github/backfill";
import type { LoopOverFooterEnv } from "../github/footer";
import { loopoverFooter, type LoopOverFooterEnv } from "../github/footer";
import type { GateCheckConclusion, GateCheckEvaluation } from "../rules/advisory";
import { LOOPOVER_GATE_CHECK_NAME, shouldPublishReviewCheck } from "../review/check-names";
import { decideReviewEligibility } from "../review/review-eligibility";
import { buildUnifiedCommentBody } from "../review/unified-comment-bridge";
import { requiredAgentActionPermissions } from "../settings/agent-execution";
import { isAgentConfigured } from "../settings/autonomy";

export function hasVisiblePrSurface(settings: RepositorySettings): boolean {
return settings.publicSurface !== "off" || settings.checkRunMode === "enabled" || shouldPublishReviewCheck(settings.reviewCheckMode);
Expand Down Expand Up @@ -722,5 +725,40 @@ function buildSamplePreviewComment(args: {
args.issues,
args.pullRequests,
);
return buildPublicPrIntelligenceComment({ repo: args.repo, pr: samplePr, profile, detection, queueHealth, collisions, preflight, settings: args.settings, env: args.env });

// Simulated gate verdict for this sample PR (#6103: migrated off the retired legacy renderer). Mirrors
// the same enabled / hard-linked-issue-block heuristic the shared panel builder used to fall back on
// internally when no real gate had run -- a duplicate-PR block is never simulated here since the
// synthetic PR #0 can't realistically collide with anything in this repo's real open PRs.
const gateEnabled = shouldPublishReviewCheck(args.settings.reviewCheckMode) || isAgentConfigured(args.settings.autonomy);
const hardLinkedIssueBlock = args.settings.linkedIssueGateMode === "block" && samplePr.linkedIssues.length === 0;
const gateConclusion: GateCheckConclusion = !gateEnabled ? "success" : hardLinkedIssueBlock ? "failure" : "success";
const gate: GateCheckEvaluation = {
enabled: gateEnabled,
conclusion: gateConclusion,
title: !gateEnabled ? `${LOOPOVER_GATE_CHECK_NAME} not configured` : gateConclusion === "failure" ? `${LOOPOVER_GATE_CHECK_NAME} failed` : `${LOOPOVER_GATE_CHECK_NAME} passed`,
summary: "Simulated for this settings preview — no live gate evaluation ran.",
blockers: [],
warnings: [],
};

const { rows, readinessTotal } = buildPublicPrPanelSignalRows({
repo: args.repo,
pr: samplePr,
profile,
detection,
queueHealth,
collisions,
preflight,
settings: args.settings,
gate: { conclusion: gateConclusion, summary: gate.summary },
});

return buildUnifiedCommentBody({
gate,
panelRows: rows,
readinessTotal,
changedFiles: 0,
footerMarkdown: loopoverFooter(args.env, {}),
});
}
Loading