diff --git a/src/signals/engine.ts b/src/signals/engine.ts index 5667536354..3bbd99c9fc 100644 --- a/src/signals/engine.ts +++ b/src/signals/engine.ts @@ -3839,6 +3839,14 @@ export function buildPublicReadinessScore(args: { export const PR_PANEL_RETRIGGER_MARKER = ""; +/** Earn-CTA target for a public-comment footer. The repo-scoped miner page is only meaningful for + * repos registered on Gittensor (per `gittensorRepoEarnUrl`'s documented contract); for an + * unregistered repo the page has no miner data, so fall back to the general Gittensor home URL + * (the `gittensoryFooter` default) instead of implying THIS repo's contributions already earn. */ +function footerEarnUrl(repo: RepositoryRecord | null, repoFullName: string): string | undefined { + return repo?.isRegistered ? gittensorRepoEarnUrl(repoFullName) : undefined; +} + export function buildPublicPrIntelligenceComment(args: { repo: RepositoryRecord | null; pr: PullRequestRecord; @@ -3948,9 +3956,10 @@ export function buildPublicPrIntelligenceComment(args: { publicFindings.length > 0 ? publicFindings.map((finding) => `- ${sanitizePanelText(finding.title)}: ${sanitizePanelText(finding.publicText ?? finding.detail)}`) : ["- No public-safe advisory findings were generated from cached metadata."]; - // Always-on earn CTA — a permanent, free marketing surface on every reviewed PR. The CTA points at - // this repo's public Gittensor miner page (social proof for THIS repo + a path to register). - const footer = gittensoryFooter({ earnUrl: gittensorRepoEarnUrl(args.pr.repoFullName) }); + // Always-on earn CTA — a permanent, free marketing surface on every reviewed PR. For a registered + // repo the CTA points at this repo's public Gittensor miner page (social proof for THIS repo + a + // path to register); for an unregistered repo it falls back to the general Gittensor home URL. + const footer = gittensoryFooter({ earnUrl: footerEarnUrl(args.repo, args.pr.repoFullName) }); return [ "", "", @@ -4014,7 +4023,7 @@ export function buildPublicPrIntelligenceComment(args: { * analysis is for registered Gittensor contributors, so we skip the panel and post a brief welcome * + earn invite; the always-on footer CTA does the conversion. Carries the same panel marker so it * updates in place if the author later registers (the full panel then replaces it). */ -function buildMinimalInviteComment(args: { pr: PullRequestRecord }): string { +function buildMinimalInviteComment(args: { repo: RepositoryRecord | null; pr: PullRequestRecord }): string { return [ "", "", @@ -4025,7 +4034,7 @@ function buildMinimalInviteComment(args: { pr: PullRequestRecord }): string { ]), "", "---", - gittensoryFooter({ earnUrl: gittensorRepoEarnUrl(args.pr.repoFullName) }), + gittensoryFooter({ earnUrl: footerEarnUrl(args.repo, args.pr.repoFullName) }), ].join("\n"); } diff --git a/test/unit/signals.test.ts b/test/unit/signals.test.ts index 8e84970595..ce1c22d3f6 100644 --- a/test/unit/signals.test.ts +++ b/test/unit/signals.test.ts @@ -30,6 +30,7 @@ import { detectGittensorContributor, shouldPublishPrIntelligenceComment, } from "../../src/signals/engine"; +import { GITTENSOR_HOME_URL } from "../../src/github/footer"; import type { BountyRecord, CheckSummaryRecord, @@ -357,6 +358,63 @@ describe("world-class backend signals", () => { expect(comment).not.toMatch(/wallet|raw trust score|ranking|farming|reward/i); }); + it("scopes the earn-footer CTA to the repo miner page only when the repo is registered", () => { + const currentPr = pullRequests[0]!; + const settings = { + repoFullName: repo.fullName, + commentMode: "detected_contributors_only" as const, + publicAudienceMode: "gittensor_only" as const, + publicSignalLevel: "standard" as const, + checkRunMode: "off" as const, + checkRunDetailLevel: "minimal" as const, + gateCheckMode: "off" as const, + linkedIssueGateMode: "advisory" as const, + duplicatePrGateMode: "advisory" as const, + qualityGateMode: "advisory" as const, + qualityGateMinScore: null, + autoLabelEnabled: true, + gittensorLabel: "gittensor", + createMissingLabel: true, + publicSurface: "comment_and_label" as const, + includeMaintainerAuthors: false, + requireLinkedIssue: false, + backfillEnabled: true, + privateTrustEnabled: true, + }; + const collisions = buildCollisionReport(repo.fullName, issues, pullRequests); + const queueHealth = buildQueueHealth(repo, issues, pullRequests, collisions); + const preflight = buildPreflightResult({ repoFullName: repo.fullName, title: currentPr.title, body: "Fixes #7", linkedIssues: [7] }, repo, issues, pullRequests); + const repoEarnPage = `${GITTENSOR_HOME_URL}/miners/repository?name=${encodeURIComponent(repo.fullName)}&tab=miners`; + const homeCta = `(${GITTENSOR_HOME_URL})`; + + // Detected contributor → full panel. Registered repo links the repo miner page; unregistered repo + // must NOT (the page has no miner data for an unregistered repo) and falls back to the home URL. + const priorPr: PullRequestRecord = { ...currentPr, number: 3, state: "closed", mergedAt: "2026-05-01T00:00:00.000Z" }; + const detected = { ...detectGittensorContributor("oktofeesh1", currentPr, [currentPr, priorPr], []), source: "official_gittensor_api" as const }; + const detectedProfile = buildContributorProfile("oktofeesh1", { login: "oktofeesh1", topLanguages: ["TypeScript"], source: "github" }, [currentPr, priorPr], []); + expect(detected.detected).toBe(true); + + const registeredComment = buildPublicPrIntelligenceComment({ repo, pr: currentPr, profile: detectedProfile, detection: detected, queueHealth, collisions, preflight, settings }); + expect(registeredComment).toContain(repoEarnPage); + + const unregisteredRepo = { ...repo, isRegistered: false, registryConfig: null }; + const unregisteredComment = buildPublicPrIntelligenceComment({ repo: unregisteredRepo, pr: currentPr, profile: detectedProfile, detection: detected, queueHealth, collisions, preflight, settings }); + expect(unregisteredComment).not.toContain("/miners/repository"); + expect(unregisteredComment).toContain(homeCta); + + // Non-detected contributor → minimal invite. Same registration gating must hold there. + const undetected = detectGittensorContributor("brand-new-outsider", currentPr, [], []); + const undetectedProfile = buildContributorProfile("brand-new-outsider", { login: "brand-new-outsider", topLanguages: [], source: "github" }, [], []); + expect(undetected.detected).toBe(false); + + const minimalRegistered = buildPublicPrIntelligenceComment({ repo, pr: currentPr, profile: undetectedProfile, detection: undetected, queueHealth, collisions, preflight, settings }); + expect(minimalRegistered).toContain(repoEarnPage); + + const minimalUnregistered = buildPublicPrIntelligenceComment({ repo: unregisteredRepo, pr: currentPr, profile: undetectedProfile, detection: undetected, queueHealth, collisions, preflight, settings }); + expect(minimalUnregistered).not.toContain("/miners/repository"); + expect(minimalUnregistered).toContain(homeCta); + }); + it("builds a compact, source-free public AI signal bundle", () => { const sourceMarker = "SECRET_SOURCE_LINE_should_never_reach_ai_provider"; const currentPr: PullRequestRecord = {