diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 2d4568338d..e4221f88f9 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -2346,13 +2346,15 @@ export class GittensoryMcp { if (!input.contributorLogin) throw new Error("contributorLogin is required for score breakdown."); this.requireContributorAccess(input.contributorLogin); await this.requireRepoAccess(input.repoFullName); - const [repo, snapshot, evidence] = await Promise.all([ + const [repo, snapshot, evidence, contributorIssues] = await Promise.all([ getRepository(this.env, input.repoFullName), getOrCreateScoringModelSnapshot(this.env), getContributorEvidence(this.env, input.contributorLogin), + listContributorIssues(this.env, input.contributorLogin), ]); + const openIssueCount = contributorOpenIssueCount(contributorIssues, input.repoFullName); // Time-decay (#703) is an owner-gated global, injected server-side (not caller-controllable). - const scoreInput = { ...input, applyTimeDecay: isTimeDecayEnabled(this.env) }; + const scoreInput = { ...input, openIssueCount, applyTimeDecay: isTimeDecayEnabled(this.env) }; const preview = buildScorePreview({ input: scoreInput, repo, snapshot, contributorEvidence: evidence }); const breakdown = explainScoreBreakdown(preview); return { diff --git a/test/unit/mcp-output-schemas.test.ts b/test/unit/mcp-output-schemas.test.ts index eb67d72a70..9c505bbd84 100644 --- a/test/unit/mcp-output-schemas.test.ts +++ b/test/unit/mcp-output-schemas.test.ts @@ -271,6 +271,58 @@ describe("MCP tool calls return schema-valid structured content", () => { expect(data.highestLeverageLever).toBeTruthy(); }); + it("gittensory_explain_score_breakdown applies trusted open-issue counts", async () => { + const env = createTestEnv(); + await upsertRepositoryFromGitHub(env, { name: "demo", full_name: "octo/demo", private: false, owner: { login: "octo" }, default_branch: "main" }); + for (const number of [1, 2, 3]) { + await upsertIssueFromGitHub(env, "octo/demo", { + number, + title: `Open contributor issue ${number}`, + state: "open", + user: { login: "alice" }, + labels: [], + body: "Issue body", + }); + } + await upsertIssueFromGitHub(env, "octo/other", { + number: 99, + title: "Other repo issue", + state: "open", + user: { login: "alice" }, + labels: [], + body: "Issue body", + }); + await upsertIssueFromGitHub(env, "octo/demo", { + number: 4, + title: "Closed contributor issue", + state: "closed", + user: { login: "alice" }, + labels: [], + body: "Issue body", + }); + + const { client } = await connectTestClient(env); + const result = await client.callTool({ + name: "gittensory_explain_score_breakdown", + arguments: { + repoFullName: "octo/demo", + contributorLogin: "alice", + sourceTokenScore: 40, + totalTokenScore: 60, + sourceLines: 80, + openPrCount: 0, + credibility: 1, + }, + }); + + expect(result.isError).toBeFalsy(); + const data = result.structuredContent as Record; + expect(data.effectiveEstimatedScore).toBe(0); + expect(data.gateHighlights).toEqual( + expect.arrayContaining([expect.objectContaining({ gate: "open_issue_threshold" })]), + ); + }); + it("gittensory_explain_score_breakdown requires contributorLogin", async () => { const env = createTestEnv(); await upsertRepositoryFromGitHub(env, { name: "demo", full_name: "octo/demo", private: false, owner: { login: "octo" }, default_branch: "main" });