diff --git a/src/queue/processors.ts b/src/queue/processors.ts index a05a34363f..157c3d0f83 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -8941,6 +8941,13 @@ async function maybePublishPrPublicSurface( // runAiReviewForAdvisory's identical check -- can read it without a second, audit-event-duplicating // getCachedOfficialMinerDetection lookup. Defaults false; only ever set true inside that try-block. let confirmedContributor = false; + // #4745: hoisted out of the try-block below (same reason/shape as confirmedContributor just above) so the + // risk × value quadrant label -- built once the comment/panel builders are reached, further down and OUTSIDE + // that try -- can reuse the ALREADY-computed slop band without a second buildSlopAssessment call. Stays null + // exactly when slopRisk (the sibling hoisted-inside-the-try variable) does: shouldCollectSlopEvidence(settings) + // resolving false this pass, in which case the quadrant degrades to showing nothing extra rather than + // fabricating a risk reading (see formatRiskValueQuadrant's own doc comment). + let slopBand: SlopBand | null = null; // Resolve the repo's action mode ONCE for the whole publish pass and thread it into every GitHub write below, so // 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) @@ -9710,6 +9717,7 @@ async function maybePublishPrPublicSurface( isPullRequestInDuplicateCluster(collisions, pr.number), }); slopRisk = slop.slopRisk; + slopBand = slop.band; advisory.findings.push(...slop.findings); // Persist dashboard-visible slop only when the repo opted into the slop gate. Merge-readiness may // still use the live score above, but disabling slop should clear any previously cached dashboard row. @@ -11163,6 +11171,9 @@ async function maybePublishPrPublicSurface( 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, }; @@ -11296,6 +11307,8 @@ async function maybePublishPrPublicSurface( duplicateWinnerEnabled, improvementSignal: structuralImprovementAssessment, valueAssessment: aiReview?.valueAssessment, + // #4745: same reused slop band as the legacy commentArgs above -- the two panel builders never diverge. + slopBand: slopBand ?? undefined, }); // Visual before/after capture (visual-capture port). Fires ONLY when (1) the "screenshots" converged // feature resolves active for this repo (resolveConvergedFeature — the global flag AND (a per-repo diff --git a/src/signals/engine.ts b/src/signals/engine.ts index ca1a2ab770..f9ece57a49 100644 --- a/src/signals/engine.ts +++ b/src/signals/engine.ts @@ -39,6 +39,7 @@ import { isAgentConfigured } from "../settings/autonomy"; import { diffFilePriority } from "../review/review-diff"; import type { ImprovementBand, StructuralImprovementAssessment } from "./improvement"; import type { ImprovementMagnitude } from "../services/ai-review"; +import type { SlopBand } from "./slop"; export type ParticipationLane = "direct_pr" | "issue_discovery" | "split" | "inactive" | "unknown"; export type SignalFinding = AdvisoryFinding; @@ -4305,6 +4306,13 @@ export function buildPublicPrIntelligenceComment(args: { * `resolveConvergedFeature(env, manifest, "improvementSignal", repoFullName)` resolving false for the repo, * or a caller that hasn't wired this yet. */ improvementSignal?: StructuralImprovementAssessment | undefined; + /** The existing deterministic slop-risk band (#4745, sub-issue H of epic #4737), pre-computed by the + * caller via `buildSlopAssessment` and passed through exactly like `improvementSignal` above is a + * pre-computed result, not a raw input. Threaded into the Improvement row (when present) as the risk + * half of the risk × value quadrant label -- see `formatRiskValueQuadrant`. Absent (every existing + * caller today, and any repo where `shouldCollectSlopEvidence` resolves false this pass) ⇒ no quadrant + * text is added, matching this epic's "degrade cleanly, never fabricate a reading" convention. */ + slopBand?: SlopBand | undefined; /** Resolved by the caller from `env.PUBLIC_SITE_ORIGIN` so a self-hoster's own domain reaches the * always-on footer's attribution link instead of `GITTENSORY_SITE_URL` (#4613). */ env: GittensoryFooterEnv; @@ -4429,7 +4437,7 @@ export function buildPublicPrIntelligenceComment(args: { // Improvement row (#4744): combines the deterministic tier (#4742) + LLM tier (#4743). `improvementRow` is // null (row omitted entirely) when the caller passes no `improvementSignal` -- see buildImprovementSignalRow's // own doc comment for why that's what keeps this byte-identical to today for every existing caller. - const improvementRow = buildImprovementSignalRow(args.improvementSignal, args.aiReview?.valueAssessment); + const improvementRow = buildImprovementSignalRow(args.improvementSignal, args.aiReview?.valueAssessment, args.slopBand); if (improvementRow) allRows.push(improvementRow); const reviewFields = args.review?.fields; const rows: Array<[string, string, string, string]> = allRows.filter((row) => reviewFields?.[row.key] !== false).map((row) => row.cells); @@ -4586,6 +4594,9 @@ export function buildPublicPrPanelSignalRows(args: { * depth, #4744) before it can reach the row. Absent ⇒ the row (when `improvementSignal` above is present) * shows the deterministic tier only. */ valueAssessment?: { magnitude: ImprovementMagnitude; rationale: string } | undefined; + /** The existing deterministic slop-risk band (#4745, sub-issue H of epic #4737) -- see the matching doc + * comment on `buildPublicPrIntelligenceComment`'s own `slopBand` field, which this mirrors. */ + slopBand?: SlopBand | undefined; }): { rows: PublicPrPanelSignalRow[]; readinessTotal: number } { const relatedWork = buildDuplicateWinnerRelatedWorkView({ pr: args.pr, @@ -4628,11 +4639,11 @@ export function buildPublicPrPanelSignalRows(args: { { key: "contributorContext", cells: ["Contributor context", contributorContext.result, contributorContext.evidence, contributorContext.action] }, { key: "gateResult", cells: ["Gate result", gateStatus(gateEnabled, gateConclusion), gateEnabled ? gateAction(gateConclusion) : "Advisory only.", gateEnabled ? gateNextAction(gateConclusion) : "No action."] }, ]; - const improvementRow = buildImprovementSignalRow(args.improvementSignal, args.valueAssessment); + const improvementRow = buildImprovementSignalRow(args.improvementSignal, args.valueAssessment, args.slopBand); return { rows: improvementRow ? [...rows, improvementRow] : rows, readinessTotal: readiness.total }; } -// ── Improvement-signal row (#4744) ─────────────────────────────────────────────────────────────────── +// ── Improvement-signal row (#4744) + risk × value quadrant (#4745) ───────────────────────────────────── // // Combines the deterministic tier (#4742, `buildStructuralImprovementAssessment`) and, when also active, the // LLM tier's composed judgment (#4743, `composeImprovementSignal`) into the optional 8th panel row. Shared by @@ -4642,6 +4653,17 @@ export function buildPublicPrPanelSignalRows(args: { // omits `improvementSignal` (the `improvementSignal` converged feature resolving false for the repo, or the // deterministic tier having nothing to report today isn't possible -- `buildStructuralImprovementAssessment` // always returns a band, even "insufficient-signal"). +// +// #4745 (sub-issue H, the epic's last core sub-issue) crosses that same band with the EXISTING slop-risk band +// (src/signals/slop.ts) into a compact "risk: X · value: Y" quadrant label -- the maintainer 2x2 from the +// issue body (safe-but-worthless churn vs. risky-but-valuable work vs. actual slop vs. a fast-track candidate). +// It is deliberately threaded in as an EXTRA prefix on the SAME Improvement row's Evidence cell rather than a +// new row/toggle key: the row (and therefore the quadrant prefix) already only renders when `improvementSignal` +// resolves on for the repo, so reusing it keeps opted-out repos byte-identical for free, with no second +// `fields:` key to hand-sync across `.gittensory.yml.example` / `config/examples/gittensory.full.yml` / +// `gittensory-repo-focus-manifest.ts` / `.gittensory.yml`. No dashboard visualization (`apps/gittensory-ui/`) +// or queue-level "high risk / low value" worklist is built here -- explicitly out of scope for this issue (see +// its own "Optional" deliverable and this PR's description for the fast-follow call). /** Static template labels (#4744), one per {@link ImprovementBand} -- never runtime-interpolated free text, so * this bypasses the public-comment sanitizer safely, mirroring how `"**Readiness score: ${total}/100**"` @@ -4678,6 +4700,29 @@ function improvementEvidenceText( return `${deterministicPart}${valuePart}`; } +/** The risk × value quadrant label (#4745): crosses the existing `SlopBand` (risk axis, `src/signals/slop.ts`) + * with the deterministic `ImprovementBand` (value axis, #4742) into one compact string, e.g. + * `"risk: low · value: moderate"` -- exactly the issue's own example wording. Both band types are closed + * enums interpolated verbatim (never free text sourced from a finding/rationale), so this is public-safe by + * construction the same way {@link IMPROVEMENT_BAND_LABELS} is -- no `containsPrivatePublicTerm` check needed. + * + * Degrades in two independent steps, never fabricating a reading for an axis this pass didn't compute: + * - `improvementBand` absent (the `improvementSignal` converged feature off for the repo, or a caller that + * hasn't wired it) ⇒ risk-only label, e.g. `"risk: low"`. + * - `slopBand` absent (`shouldCollectSlopEvidence` resolved false this pass, e.g. both `slopGateMode` and + * `mergeReadinessGateMode` are `"off"`) ⇒ `undefined` -- nothing to show, since a value-only reading with no + * risk context at all isn't part of this issue's quadrant. + * + * Exported for direct unit testing of all four quadrant combinations plus the degraded case, per #4745's own + * acceptance criteria. */ +export function formatRiskValueQuadrant( + slopBand: SlopBand | undefined, + improvementBand: ImprovementBand | undefined, +): string | undefined { + if (slopBand === undefined) return undefined; + return improvementBand === undefined ? `risk: ${slopBand}` : `risk: ${slopBand} · value: ${improvementBand}`; +} + /** Builds the optional "Improvement" row, or `null` when the caller has no improvement data to show. `null` * here (rather than a placeholder row) is what keeps `allRows`/`buildPublicPrPanelSignalRows`'s `rows` * byte-identical to today for every existing caller that doesn't pass `improvementSignal` -- see the @@ -4690,18 +4735,25 @@ function improvementEvidenceText( function buildImprovementSignalRow( assessment: StructuralImprovementAssessment | undefined, valueAssessment: { magnitude: ImprovementMagnitude; rationale: string } | undefined, + slopBand?: SlopBand | undefined, ): PublicPrPanelSignalRow | null { if (!assessment) return null; const safeFindings = assessment.findings.filter( (finding) => !containsPrivatePublicTerm([finding.title, finding.detail, finding.publicText].filter(Boolean).join(" ")), ); const safeValueAssessment = valueAssessment && !containsPrivatePublicTerm(valueAssessment.rationale) ? valueAssessment : undefined; + const evidence = improvementEvidenceText(assessment.band, safeFindings, safeValueAssessment); + // #4745: prefixes the risk × value quadrant onto the SAME Evidence cell instead of a new row/column -- + // `assessment.band` is always defined here (the row already bailed out above when `assessment` is absent), + // so this only ever needs `slopBand` to produce the full quadrant; absent slopBand (slop wasn't computed + // this pass) leaves the evidence text exactly as it was before this PR, never a fabricated risk reading. + const quadrant = formatRiskValueQuadrant(slopBand, assessment.band); return { key: "improvementSignal", cells: [ "Improvement", IMPROVEMENT_BAND_LABELS[assessment.band], - improvementEvidenceText(assessment.band, safeFindings, safeValueAssessment), + quadrant ? `${quadrant} — ${evidence}` : evidence, "Advisory only — never blocks merge.", ], }; diff --git a/test/unit/queue-4.test.ts b/test/unit/queue-4.test.ts index caa35eeab4..f5bcff8cd6 100644 --- a/test/unit/queue-4.test.ts +++ b/test/unit/queue-4.test.ts @@ -3030,6 +3030,185 @@ describe("queue processors", () => { } }); + // #4745 (risk x value quadrant, sub-issue H of epic #4737): same scaffold as the #4744 test just above, but + // ALSO opts the repo into slop evidence collection (slopGateMode: "advisory", never "block" -- this test is + // not exercising the slop gate itself) so `maybePublishPrPublicSurface`'s hoisted `slopBand` is populated from + // a REAL `buildSlopAssessment` call this pass, not left null. Proves the risk x value quadrant threads end to + // end from that real slop band through to the posted comment's Improvement row -- the only place in the + // existing suite where BOTH improvementSignalAllowed AND shouldCollectSlopEvidence resolve true in the same + // pass (every other test exercises them independently). + it("#4745: threads the real slop band into the Improvement row's quadrant prefix when both improvementSignal and slop evidence collection are on", async () => { + const env = createTestEnv({ + GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), + GITTENSORY_REVIEW_UNIFIED_COMMENT: "1", + GITTENSORY_REVIEW_IMPROVEMENT_SIGNAL: "true", + GITTENSORY_REVIEW_REPOS: "JSONbored/gittensory", + }); + await persistRegistrySnapshot( + env, + normalizeRegistryPayload( + { "JSONbored/gittensory": { emission_share: 0.01, issue_discovery_share: 0 } }, + { kind: "raw-github", url: "https://example.test" }, + "2026-05-23T00:00:00.000Z", + ), + ); + await upsertRepositorySettings(env, { + repoFullName: "JSONbored/gittensory", + commentMode: "detected_contributors_only", + publicAudienceMode: "gittensor_only", + publicSignalLevel: "standard", + publicSurface: "comment_and_label", + autoLabelEnabled: false, + checkRunMode: "off", + checkRunDetailLevel: "minimal", + gateCheckMode: "enabled", reviewCheckMode: "required", + backfillEnabled: true, + autonomy: { update_branch: "auto" }, + // The only delta from the #4744 test above: turns on slop evidence collection so `slopBand` is populated + // this pass. "advisory" never blocks (only "block" mode does, at the configured threshold) -- this test + // is exercising the quadrant label, not the slop gate. + slopGateMode: "advisory", + }); + let postedBody = ""; + const calls = { comments: 0, gateChecks: 0 }; + let gateFinalized = false; + let failedPostGateMint = false; + const liveCiSpy = vi + .spyOn(backfillModule, "fetchLiveCiAggregatePreferGraphQl") + .mockRejectedValueOnce(new Error("transient CI read failed")) + .mockResolvedValue({ + ciState: "passed", + hasPending: false, + hasVisiblePending: false, + hasMissingRequiredContext: false, + failingDetails: [], + nonRequiredFailingDetails: [], + ciCompletenessWarning: null, + }); + vi.stubGlobal("fetch", async (input: RequestInfo | URL, init?: RequestInit) => { + const url = input.toString(); + const method = init?.method ?? "GET"; + if (url === "https://api.gittensor.io/miners") { + return Response.json([ + { + uid: 7, + githubUsername: "oktofeesh1", + githubId: "123", + totalPrs: 4, + totalMergedPrs: 3, + totalOpenPrs: 1, + totalClosedPrs: 0, + totalOpenIssues: 0, + totalClosedIssues: 0, + totalSolvedIssues: 0, + totalValidSolvedIssues: 0, + isEligible: true, + credibility: 1, + eligibleRepoCount: 1, + hotkey: "must-not-leak", + }, + ]); + } + if (url === "https://api.gittensor.io/miners/123") { + return Response.json({ + repositories: [ + { + repositoryFullName: "JSONbored/gittensory", + totalPrs: "4", + totalMergedPrs: "3", + totalOpenPrs: "1", + totalClosedPrs: "0", + totalOpenIssues: "0", + totalClosedIssues: "0", + isEligible: true, + credibility: "1.000000", + }, + ], + }); + } + if (url === "https://api.gittensor.io/miners/123/prs") return Response.json([]); + if (url === "https://mirror.gittensor.io/api/v1/miners/123/issues") return Response.json({ issues: [] }); + if (url.endsWith("/users/oktofeesh1")) return Response.json({ login: "oktofeesh1", public_repos: 2, followers: 1 }); + if (url.includes("/users/oktofeesh1/repos")) return Response.json([{ language: "TypeScript" }]); + if (url.includes("/access_tokens")) { + if (gateFinalized && !failedPostGateMint) { + failedPostGateMint = true; + return new Response("mint failed", { status: 500 }); + } + return Response.json({ token: "installation-token", expires_at: "2026-05-28T00:04:00.000Z" }); + } + // PR files — one plain code file with no accompanying test evidence: `missingTestEvidence` is the only + // slop finding that can fire for this fixture (churn is far below MIN_CHURN_LINES, description/linked + // issue are both present) -- slopRisk 15, band "low". Also what the improvement-signal deterministic + // tier reads via getReviewFiles() for its own test-evidence axis (#4742); same inputs, band "none". + if (url.includes("/pulls/4/files")) return Response.json([{ filename: "src/cache.ts", additions: 5, deletions: 1, status: "modified" }]); + if (/\/pulls\/4(?:\?|$)/.test(url)) return Response.json({ number: 4, mergeable_state: "clean" }); + if (url.includes("/check-runs") && method === "GET") return Response.json({ total_count: 0, check_runs: [] }); + if (url.includes("/check-runs") && method === "POST") { + calls.gateChecks += 1; + const body = JSON.parse(String(init?.body ?? "{}")) as { status?: string; conclusion?: string }; + if (body.status !== "in_progress" || body.conclusion) { + gateFinalized = true; + clearInstallationTokenCacheForTest(); + } + return Response.json({ id: 901 }, { status: 201 }); + } + if (url.includes("/check-runs/901") && method === "PATCH") { + calls.gateChecks += 1; + gateFinalized = true; + clearInstallationTokenCacheForTest(); + return Response.json({ id: 901 }); + } + if (url.includes("/issues/4/comments") && method === "GET") return Response.json([]); + if (url.includes("/issues/4/comments") && method === "POST") { + calls.comments += 1; + postedBody = String((JSON.parse(String(init?.body ?? "{}")) as { body?: string }).body ?? ""); + return Response.json({ id: 1, html_url: "https://github.com/comment/1" }, { status: 201 }); + } + return new Response("not found", { status: 404 }); + }); + + try { + await processJob(env, { + type: "github-webhook", + deliveryId: "pr-improvement-signal-quadrant", + eventName: "pull_request", + payload: { + action: "synchronize", + installation: { + id: 123, + account: { login: "JSONbored", id: 1, type: "User" }, + repository_selection: "selected", + permissions: { metadata: "read", pull_requests: "read", issues: "write", checks: "write" }, + events: ["issues", "issue_comment", "pull_request", "repository", "installation_repositories"], + }, + repository: { name: "gittensory", full_name: "JSONbored/gittensory", private: false, owner: { login: "JSONbored" } }, + pull_request: { + number: 4, + title: "Cache invalidation cleanup", + state: "open", + user: { login: "oktofeesh1" }, + head: { sha: "quadrant123" }, + labels: [{ name: "bug" }], + body: "Fixes #1", + }, + }, + }); + + expect(calls.comments).toBeGreaterThan(0); + expect(postedBody).toContain(""); + // The quadrant prefix ("risk: low · value: none") threaded from the REAL slopBand computed this pass + // (missingTestEvidence only, slopRisk 15 -> band "low") ahead of the SAME evidence text #4744 already + // asserts verbatim -- proving processors.ts's new hoisted slopBand reaches the rendered comment, not + // just computed and discarded. + expect(postedBody).toContain("| Improvement | ⚠️ ℹ️ None detected | risk: low · value: none — No structural-improvement signals were detected for this PR. |"); + // Public-safe regardless: no internal trust/economics fields leak through the new quadrant clause either. + expect(postedBody).not.toMatch(/wallet|hotkey|coldkey|reward|trust score/i); + } finally { + liveCiSpy.mockRestore(); + } + }); + it("INVARIANT (#4498): the disposition planner reuses the public surface's own live mergeable_state/CI read instead of re-fetching a third time", async () => { const env = createTestEnv({ GITHUB_APP_PRIVATE_KEY: await generatePrivateKeyPem(), GITTENSORY_REVIEW_UNIFIED_COMMENT: "1" }); await persistRegistrySnapshot( diff --git a/test/unit/signals-coverage.test.ts b/test/unit/signals-coverage.test.ts index 8b3af5b61c..d658591e6f 100644 --- a/test/unit/signals-coverage.test.ts +++ b/test/unit/signals-coverage.test.ts @@ -25,6 +25,7 @@ import { buildQueueHealth, buildRoleContext, detectGittensorContributor, + formatRiskValueQuadrant, itemSharesPlannedLinkedIssue, shouldPublishPrIntelligenceComment, unionScopedOverlapClusters, @@ -1042,6 +1043,86 @@ describe("signal coverage edge cases", () => { }); }); + describe("#4745: risk x value quadrant label (sub-issue H, epic #4737 -- the maintainer 2x2 from the issue body)", () => { + it("computes the full quadrant label crossing the existing slop-risk band with the deterministic improvement band, across all four representative combinations", () => { + expect(formatRiskValueQuadrant("low", "minor")).toBe("risk: low · value: minor"); + expect(formatRiskValueQuadrant("low", "significant")).toBe("risk: low · value: significant"); + expect(formatRiskValueQuadrant("high", "minor")).toBe("risk: high · value: minor"); + expect(formatRiskValueQuadrant("high", "significant")).toBe("risk: high · value: significant"); + }); + + it("degrades to a risk-only label when the improvement band is unavailable (improvementSignal off for the repo, or a caller that hasn't wired it)", () => { + expect(formatRiskValueQuadrant("low", undefined)).toBe("risk: low"); + expect(formatRiskValueQuadrant("elevated", undefined)).toBe("risk: elevated"); + expect(formatRiskValueQuadrant("clean", undefined)).toBe("risk: clean"); + }); + + it("returns undefined (nothing to show) when the slop band itself is unavailable this pass, regardless of the improvement band -- never fabricates a risk reading", () => { + expect(formatRiskValueQuadrant(undefined, "significant")).toBeUndefined(); + expect(formatRiskValueQuadrant(undefined, undefined)).toBeUndefined(); + }); + + const quadrantRepo = repo("owner/quadrant"); + const quadrantPr = pr(quadrantRepo.fullName, 121, "Simplify retry logic", { authorLogin: "miner", linkedIssues: [8], body: "Fixes #8" }); + const quadrantProfile = buildContributorProfile("miner", { login: "miner", topLanguages: ["TypeScript"], source: "github" }, [], []); + const quadrantDetection = { detected: true, source: "official_gittensor_api" as const, reason: "Confirmed.", priorPullRequests: 1, priorMergedPullRequests: 0, priorIssues: 0 }; + const quadrantCollisions = buildCollisionReport(quadrantRepo.fullName, [], []); + const quadrantQueueHealth = buildQueueHealth(quadrantRepo, [], [], quadrantCollisions); + const quadrantPreflight = buildPreflightResult( + { repoFullName: quadrantRepo.fullName, title: quadrantPr.title, body: quadrantPr.body ?? undefined, linkedIssues: quadrantPr.linkedIssues, changedFiles: ["src/retry.ts"] }, + quadrantRepo, + [], + [], + ); + const quadrantSettings = repoSettings(quadrantRepo.fullName); + const quadrantBaseArgs = { + repo: quadrantRepo, + pr: quadrantPr, + profile: quadrantProfile, + detection: quadrantDetection, + queueHealth: quadrantQueueHealth, + collisions: quadrantCollisions, + preflight: quadrantPreflight, + settings: quadrantSettings, + }; + const quadrantAssessment = { improvementScore: 10, band: "minor" as const, findings: [] }; + + it("prefixes the quadrant label onto the Improvement row's Evidence cell when slopBand is threaded alongside improvementSignal", () => { + const panel = buildPublicPrPanelSignalRows({ ...quadrantBaseArgs, improvementSignal: quadrantAssessment, slopBand: "low" }); + const row = panel.rows.find((r) => r.key === "improvementSignal")!; + expect(row.cells[2]).toBe("risk: low · value: minor — No structural-improvement signals were detected for this PR."); + // The Result cell (the deterministic band label) is untouched by the quadrant -- the two tiers never blend. + expect(row.cells[1]).toBe("✅ Minor"); + + const comment = buildPublicPrIntelligenceComment({ ...quadrantBaseArgs, improvementSignal: quadrantAssessment, slopBand: "low", env: {} }); + expect(comment).toContain("risk: low · value: minor — No structural-improvement signals were detected for this PR."); + }); + + it("leaves the Evidence cell exactly as #4744 shipped it when slopBand is omitted -- byte-identical for every caller that hasn't threaded it yet", () => { + const panel = buildPublicPrPanelSignalRows({ ...quadrantBaseArgs, improvementSignal: quadrantAssessment }); + const row = panel.rows.find((r) => r.key === "improvementSignal")!; + expect(row.cells[2]).toBe("No structural-improvement signals were detected for this PR."); + expect(row.cells[2]).not.toContain("risk:"); + }); + + it("never renders the row at all when improvementSignal is off for the repo, even when slopBand IS available -- repos that have not opted into the epic's feature stay byte-identical (degraded/slop-risk-only case)", () => { + const panel = buildPublicPrPanelSignalRows({ ...quadrantBaseArgs, slopBand: "high" }); + expect(panel.rows.find((r) => r.key === "improvementSignal")).toBeUndefined(); + expect(panel.rows).toHaveLength(7); + + const comment = buildPublicPrIntelligenceComment({ ...quadrantBaseArgs, slopBand: "high", env: {} }); + expect(comment).not.toContain("| Improvement |"); + expect(comment).not.toContain("risk: high"); + }); + + it("REGRESSION: the quadrant clause can never leak forbidden vocabulary -- SlopBand/ImprovementBand are closed enums, never free text, so no sanitizer check is needed for this clause specifically", () => { + const forbidden = /wallet|hotkey|coldkey|trust score|reward|payout|scoreability|reviewability|farming/i; + const panel = buildPublicPrPanelSignalRows({ ...quadrantBaseArgs, improvementSignal: quadrantAssessment, slopBand: "high" }); + const row = panel.rows.find((r) => r.key === "improvementSignal")!; + expect(JSON.stringify(row)).not.toMatch(forbidden); + }); + }); + it("#dup-winner: panel hard-duplicate block is suppressed for the winner, kept for the loser, byte-identical when flag OFF", () => { const directRepo = repo("owner/dupwin"); const dupIssue = issue(directRepo.fullName, 42, "Cache invalidation race");