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
27 changes: 22 additions & 5 deletions src/services/score-breakdown.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
import { sanitizePublicComment } from "../github/commands";
import { PUBLIC_LOCAL_PATH_INLINE } from "../signals/redaction";
import type { ScoreGateDelta, ScorePreviewResult } from "../scoring/preview";

// This endpoint (POST /v1/scoring/explain-breakdown, gated by requireContributorAccess) is authenticated and
// scoped to the requesting contributor's OWN score -- not a public GitHub comment surface -- so "score",
// "credibility", and "leverage" are its legitimate core vocabulary, not a leak. Reusing the shared
// sanitizePublicComment (src/github/commands.ts, tuned for genuinely public GitHub comment surfaces) mangled
// this feature's own output ("saturated near the score cap" -> "saturated near the private context cap",
// "Contributor credibility evidence..." -> "Contributor private context evidence..."). Mirrors the
// established, documented pattern in agent-action-explanation-card.ts / miner-dashboard-recommendations.ts
// (see src/signals/redaction.ts's note above PUBLIC_UNSAFE_TERMS): this endpoint's output is entirely
// computed, structured score data (numbers and gate deltas), so the only genuine residual risk is an
// accidentally-embedded token or local filesystem path -- keep just that minimal safety net rather than the
// full gittensor-economic-vocabulary substitution.
const TOKEN_OR_PATH_PATTERN = new RegExp(`\\bgithub_pat_[A-Za-z0-9_]+|\\bgh[pousr]_[A-Za-z0-9_]+|(?:${PUBLIC_LOCAL_PATH_INLINE})\\S+`, "gi");

function sanitizeScoreBreakdownText(value: string): string {
return value.replace(TOKEN_OR_PATH_PATTERN, "<redacted>");
}

export type ScoreMultiplierBand = "full" | "reduced" | "neutral" | "blocked";

export type ScoreMultiplierBreakdown = {
Expand Down Expand Up @@ -359,7 +376,7 @@ function roundBand(value: number): string {
function gateHighlightsFor(preview: ScorePreviewResult): ScoreBreakdownExplanation["gateHighlights"] {
return preview.gateDeltas.map((delta) => ({
gate: delta.gate,
explanation: sanitizePublicComment(delta.explanation),
explanation: sanitizeScoreBreakdownText(delta.explanation),
}));
}

Expand All @@ -383,7 +400,7 @@ function pickHighestLeverage(components: ScoreMultiplierBreakdown[]): ScoreBreak
component: top.component,
lever: top.lever,
tiedLeverageComponents: tied,
reason: sanitizePublicComment(reason),
reason: sanitizeScoreBreakdownText(reason),
};
}

Expand All @@ -408,8 +425,8 @@ export function explainScoreBreakdown(preview: ScorePreviewResult): ScoreBreakdo
nonCodeCapBreakdown(preview),
].map((entry) => ({
...entry,
summary: sanitizePublicComment(entry.summary),
lever: sanitizePublicComment(entry.lever),
summary: sanitizeScoreBreakdownText(entry.summary),
lever: sanitizeScoreBreakdownText(entry.lever),
}));

return {
Expand Down
36 changes: 35 additions & 1 deletion test/unit/score-breakdown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,10 @@ describe("explainScoreBreakdown", () => {

const breakdown = explainScoreBreakdown(preview);
expect(breakdown.gateHighlights.length).toBeGreaterThan(0);
expect(breakdown.gateHighlights[0]?.explanation).toMatch(/private context|estimated score/i);
// score-breakdown.ts now uses its own curated vocabulary (not the shared public-comment sanitizer), so
// "estimated score" is the only possible outcome here -- the "private context" alternative was pinned to
// the old shared-sanitizer mangling and is no longer reachable.
expect(breakdown.gateHighlights[0]?.explanation).toMatch(/estimated score/i);
expect(JSON.stringify(breakdown.gateHighlights)).not.toMatch(FORBIDDEN);
});

Expand Down Expand Up @@ -728,4 +731,35 @@ describe("explainScoreBreakdown", () => {
expect(top.tiedLeverageComponents).toEqual([]);
expect(top.reason).not.toMatch(/ties with/);
});

// REGRESSION (score-breakdown-own-vocabulary): explainScoreBreakdown previously piped its own copy through
// the shared public-comment sanitizer (src/github/commands.ts's sanitizePublicComment), which mangles bare
// "score"/"credibility" into "private context" -- fine for a genuinely public GitHub comment, but this
// endpoint is authenticated and per-contributor, and "score"/"credibility" are its entire point. Asserts
// the feature's own core vocabulary survives byte-for-byte, not just "no forbidden wallet/hotkey word leaked".
it("REGRESSION: never mangles this feature's own core vocabulary (score/credibility/leverage) into private context", () => {
const preview = buildScorePreview({
repo,
snapshot,
input: {
repoFullName: repo.fullName,
sourceTokenScore: 100,
totalTokenScore: 100,
sourceLines: 100,
openPrCount: 0,
openIssueCount: 0,
existingContributorTokenScore: 5000,
credibility: 1,
linkedIssueMode: "none",
},
});
const breakdown = explainScoreBreakdown(preview);
const serialized = JSON.stringify(breakdown);
expect(serialized).not.toMatch(/private context/i);
const baseScore = breakdown.components.find((c) => c.component === "baseScore");
expect(baseScore?.summary).toMatch(/score cap|score pipeline/i);
const credibilityComponent = breakdown.components.find((c) => c.component === "credibilityMultiplier");
expect(credibilityComponent?.summary).toMatch(/credibility/i);
expect(serialized).not.toMatch(FORBIDDEN);
});
});
Loading