Skip to content
Closed
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
185 changes: 185 additions & 0 deletions apps/gittensory-ui/public/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -12777,6 +12777,164 @@
"weekly",
"byProject"
]
},
"ScoreScenarioExplanation": {
"type": "object",
"properties": {
"repoFullName": {
"type": "string"
},
"scoreabilityStatus": {
"type": "string",
"enum": [
"blocked",
"conditionally_scoreable",
"scoreable",
"hold"
]
},
"effectiveEstimatedScore": {
"type": "number"
},
"headline": {
"type": "string"
},
"scenarios": {
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string",
"enum": [
"current",
"cleanGates",
"afterPendingMerges",
"afterApprovedPrsMerge",
"afterStalePrsClose",
"linkedIssueFixed",
"bestReasonableCase"
]
},
"source": {
"type": "string",
"enum": [
"current_data",
"user_supplied",
"github_observed",
"gittensory_projection"
]
},
"band": {
"type": "string",
"enum": [
"blocked",
"conditionally_scoreable",
"scoreable",
"hold"
]
},
"rank": {
"type": "integer"
},
"summary": {
"type": "string"
},
"lever": {
"type": "string"
},
"assumptions": {
"type": "array",
"items": {
"type": "string"
}
},
"unlockDelta": {
"type": "string"
},
"leverageScore": {
"type": "number"
}
},
"required": [
"name",
"source",
"band",
"rank",
"summary",
"lever",
"assumptions",
"unlockDelta",
"leverageScore"
]
}
},
"gateDeltaNarratives": {
"type": "array",
"items": {
"type": "object",
"properties": {
"gate": {
"type": "string",
"enum": [
"open_pr_threshold",
"open_issue_threshold",
"merged_pr_history_floor",
"issue_discovery_validity_floor",
"credibility_floor",
"linked_issue_multiplier"
]
},
"narrative": {
"type": "string"
},
"lever": {
"type": "string"
}
},
"required": [
"gate",
"narrative",
"lever"
]
}
},
"recommendedPath": {
"type": "object",
"properties": {
"scenario": {
"type": "string"
},
"lever": {
"type": "string"
},
"reason": {
"type": "string"
},
"orderedLevers": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"scenario",
"lever",
"reason",
"orderedLevers"
]
}
},
"required": [
"repoFullName",
"scoreabilityStatus",
"effectiveEstimatedScore",
"headline",
"scenarios",
"gateDeltaNarratives",
"recommendedPath"
]
}
},
"parameters": {},
Expand Down Expand Up @@ -15648,6 +15806,33 @@
}
]
}
},
"/v1/scoring/explain-scenarios": {
"post": {
"responses": {
"200": {
"description": "Private score scenario explanation",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ScoreScenarioExplanation"
}
}
}
},
"400": {
"description": "Invalid scoring preview input"
}
},
"security": [
{
"GittensoryBearer": []
},
{
"GittensorySessionCookie": []
}
]
}
}
},
"servers": [
Expand Down
44 changes: 44 additions & 0 deletions packages/gittensory-mcp/bin/gittensory-mcp.js
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,50 @@ server.registerTool(
},
);

server.registerTool(
"gittensory_explain_score_scenarios",
{
description: "Explain private score-preview what-if scenarios and gate deltas with ranked cleanup paths.",
inputSchema: localScoreShape,
},
async (input) => {
const workspaceInput = await withClientWorkspaceRoots(input);
const contributorLogin = workspaceInput.contributorLogin ?? activeProfile.session?.login;
if (!contributorLogin) throw new Error("contributorLogin is required for score scenario explanation.");
const workspace = resolveWorkspaceCwd(workspaceInput);
const diff = collectLocalDiff(workspace.cwd, workspaceInput.baseRef, workspaceInput.workspaceRoots);
const branchPayload = buildBranchAnalysisPayload({
...workspaceInput,
login: contributorLogin,
cwd: workspace.cwd,
repoFullName: workspaceInput.repoFullName,
baseRef: workspaceInput.baseRef,
});
const upstreamPreview = branchPayload.localScorerStatus;
const estimatedSourceLines = workspaceInput.sourceLines ?? Math.max(1, diff.changedLineCount - diff.testFiles.length);
const body = {
repoFullName: workspaceInput.repoFullName,
contributorLogin,
sourceTokenScore: workspaceInput.sourceTokenScore ?? estimatedSourceLines,
sourceLines: estimatedSourceLines,
totalTokenScore: workspaceInput.totalTokenScore ?? diff.changedLineCount,
testTokenScore: diff.testFiles.length,
openPrCount: workspaceInput.openPrCount,
credibility: workspaceInput.credibility,
changesRequestedCount: workspaceInput.changesRequestedCount,
pendingMergedPrCount: workspaceInput.pendingMergedPrCount,
pendingClosedPrCount: workspaceInput.pendingClosedPrCount,
approvedPrCount: workspaceInput.approvedPrCount,
expectedOpenPrCountAfterMerge: workspaceInput.expectedOpenPrCountAfterMerge,
projectedCredibility: workspaceInput.projectedCredibility,
scenarioNotes: workspaceInput.scenarioNotes,
branchEligibility: workspaceInput.branchEligibility,
metadataOnly: !upstreamPreview.ok,
};
return toolResult("Gittensory private score scenarios.", await apiPost("/v1/scoring/explain-scenarios", body));
},
);

server.registerTool(
"gittensory_get_decision_pack",
{
Expand Down
20 changes: 20 additions & 0 deletions src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ import { buildRemediationPlan } from "../services/remediation-plan";
import { handleDraftCreate, handleDraftOAuthCallback, handleDraftStatus } from "../services/draft";
import { decidePendingAgentAction } from "../services/agent-approval-queue";
import { explainScoreBreakdown } from "../services/score-breakdown";
import { explainScoreScenarios } from "../services/score-scenario-explain";
import { buildMcpClientTelemetry } from "../services/client-telemetry";
import {
buildAndPersistContributorDecisionPack,
Expand Down Expand Up @@ -1704,6 +1705,25 @@ export function createApp() {
return c.json(explainScoreBreakdown(preview));
});

app.post("/v1/scoring/explain-scenarios", async (c) => {
const body = await c.req.json().catch(() => null);
const parsed = scorePreviewSchema.safeParse(body);
if (!parsed.success) return c.json({ error: "invalid_scoring_preview_request", issues: parsed.error.issues }, 400);
if (!parsed.data.contributorLogin) return c.json({ error: "contributor_login_required" }, 400);
const unauthorized = await requireContributorAccess(c, parsed.data.contributorLogin);
if (unauthorized) return unauthorized;
const [repo, snapshot, evidence, contributorIssues] = await Promise.all([
getRepository(c.env, parsed.data.repoFullName),
getOrCreateScoringModelSnapshot(c.env),
getContributorEvidence(c.env, parsed.data.contributorLogin),
listContributorIssues(c.env, parsed.data.contributorLogin),
]);
const openIssueCount = contributorOpenIssueCount(contributorIssues, parsed.data.repoFullName);
const input = { ...parsed.data, openIssueCount, applyTimeDecay: isTimeDecayEnabled(c.env) };
const preview = buildScorePreview({ input, repo, snapshot, contributorEvidence: evidence });
return c.json(explainScoreScenarios(preview));
});

app.get("/v1/sync/status", async (c) => {
const [snapshot, scoringSnapshot, repositories, segments, totals, detailStates, installations, rateLimits, signalSnapshots, bounties, upstreamDrift] = await Promise.all([
getLatestRegistrySnapshot(c.env),
Expand Down
42 changes: 42 additions & 0 deletions src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import { loadContributorDecisionPackForServing, repoDecisionFromPack } from "../
import { buildPublicPrBodyDraft } from "../services/pr-body-draft";
import { buildRemediationPlan } from "../services/remediation-plan";
import { explainScoreBreakdown } from "../services/score-breakdown";
import { explainScoreScenarios } from "../services/score-scenario-explain";
import { loadOrComputeIssueQualityResponse } from "../services/issue-quality";
import { loadOrComputeBurdenForecastResponse } from "../services/burden-forecast";
import { buildMcpClientTelemetry } from "../services/client-telemetry";
Expand Down Expand Up @@ -876,6 +877,16 @@ const scoreBreakdownOutputSchema = {
highestLeverageLever: z.unknown().optional(),
};

const scoreScenarioExplanationOutputSchema = {
repoFullName: z.string().optional(),
scoreabilityStatus: z.string().optional(),
effectiveEstimatedScore: z.number().optional(),
headline: z.string().optional(),
scenarios: z.unknown().optional(),
gateDeltaNarratives: z.unknown().optional(),
recommendedPath: z.unknown().optional(),
};

const lintPrTextOutputSchema = {
verdict: z.string().optional(),
score: z.number().optional(),
Expand Down Expand Up @@ -1501,6 +1512,17 @@ export class GittensoryMcp {
async (input) => this.toolResult(await this.explainScoreBreakdown(input)),
);

server.registerTool(
"gittensory_explain_score_scenarios",
{
description:
"Explain private score-preview what-if scenarios and gate deltas with ranked cleanup paths. Login and repo scoped; no new computation beyond the preview projection.",
inputSchema: scorePreviewShape,
outputSchema: scoreScenarioExplanationOutputSchema,
},
async (input) => this.toolResult(await this.explainScoreScenarios(input)),
);

server.registerTool(
"gittensory_explain_review_risk",
{
Expand Down Expand Up @@ -2569,6 +2591,26 @@ export class GittensoryMcp {
};
}

private async explainScoreScenarios(input: z.infer<z.ZodObject<typeof scorePreviewShape>>): Promise<ToolPayload> {
if (!input.contributorLogin) throw new Error("contributorLogin is required for score scenario explanation.");
this.requireContributorAccess(input.contributorLogin);
await this.requireRepoAccess(input.repoFullName);
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);
const scoreInput = { ...input, openIssueCount, applyTimeDecay: isTimeDecayEnabled(this.env) };
const preview = buildScorePreview({ input: scoreInput, repo, snapshot, contributorEvidence: evidence });
const explanation = explainScoreScenarios(preview);
return {
summary: `Private Gittensory score scenarios for ${input.contributorLogin} in ${input.repoFullName}. Recommended path: ${explanation.recommendedPath.scenario}.`,
data: explanation as unknown as Record<string, unknown>,
};
}

private async explainReviewRisk(input: z.infer<z.ZodObject<typeof preflightShape>>): Promise<ToolPayload> {
if (input.contributorLogin) this.requireContributorAccess(input.contributorLogin);
await this.requireRepoAccess(input.repoFullName);
Expand Down
Loading
Loading