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
3 changes: 2 additions & 1 deletion src/api/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,8 @@ const branchEligibilitySchema = z
checkedAt: z.string().max(MAX_LOCAL_BRANCH_REF_CHARS).optional(),
stale: z.boolean().optional(),
})
.strict();
.strict()
.transform((value) => ({ ...value, status: value.status === "eligible" ? ("unknown" as const) : value.status, source: "user_supplied" as const }));

const localBranchAnalysisSchema = z
.object({
Expand Down
9 changes: 7 additions & 2 deletions src/mcp/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,11 @@ const branchEligibilityShape = {
stale: z.boolean().optional(),
};

const callerBranchEligibilitySchema = z
.object(branchEligibilityShape)
.strict()
.transform((value) => ({ ...value, status: value.status === "eligible" ? ("unknown" as const) : value.status, source: "user_supplied" as const }));

// Changed-file metadata + local validation results — shared by the local-branch analysis and the #782 local
// scorer. METADATA ONLY (paths + line counts), never source content, so the no-upload boundary holds.
const changedFileSchema = z
Expand Down Expand Up @@ -381,7 +386,7 @@ const localBranchAnalysisShape = {
projectedCredibility: z.number().min(0).max(1).optional(),
scenarioNotes: z.array(z.string()).max(20).optional(),
focusManifest: z.record(z.string(), z.unknown()).optional(),
branchEligibility: z.object(branchEligibilityShape).strict().optional(),
branchEligibility: callerBranchEligibilitySchema.optional(),
localScorer: z
.object({
mode: z.enum(["metadata_only", "external_command", "gittensor_root"]),
Expand Down Expand Up @@ -454,7 +459,7 @@ const scorePreviewShape = {
expectedOpenPrCountAfterMerge: z.number().int().min(0).optional(),
projectedCredibility: z.number().min(0).max(1).optional(),
scenarioNotes: z.array(z.string()).max(20).optional(),
branchEligibility: z.object(branchEligibilityShape).strict().optional(),
branchEligibility: callerBranchEligibilitySchema.optional(),
};

const variantsShape = {
Expand Down
17 changes: 15 additions & 2 deletions src/scoring/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -740,14 +740,14 @@ function decideLinkedIssueMultiplier(
const hasSolvedByPullRequestEvidence = solvedByPullRequests.length > 0 || projectedSolvedByPullRequestValidation;
const status = requestedStatus === "validated" && !hasSolvedByPullRequestEvidence ? (issueNumbers.length > 0 ? "raw" : "unavailable") : requestedStatus;
const source = context?.source ?? (status === "unavailable" ? "missing" : "user_supplied");
const branchEligible = !(branchEligibility.required && branchEligibility.status === "ineligible");
const branchEligible = isConfirmedBranchEligible(branchEligibility);
const eligible = status === "validated" && hasSolvedByPullRequestEvidence && branchEligible;
const reason =
branchEligible || status !== "validated"
? status === requestedStatus
? context?.reason ?? linkedIssueReason(status, source, issueNumbers, solvedByPullRequests)
: linkedIssueReason(status, source, issueNumbers, solvedByPullRequests)
: "Branch eligibility is confirmed ineligible; standard issue multiplier is not applied.";
: branchEligibilityFailureReason(branchEligibility);
return {
mode,
status,
Expand All @@ -762,6 +762,19 @@ function decideLinkedIssueMultiplier(
};
}

function isConfirmedBranchEligible(branchEligibility: BranchEligibilityResult): boolean {
return !branchEligibility.required || (branchEligibility.status === "eligible" && branchEligibility.evidence === "provided" && !branchEligibility.stale);
}

function branchEligibilityFailureReason(branchEligibility: BranchEligibilityResult): string {
if (branchEligibility.status === "ineligible") return "Branch eligibility is confirmed ineligible; standard issue multiplier is not applied.";
if (branchEligibility.evidence === "missing") return "Branch eligibility evidence is missing; standard issue multiplier is not applied.";
if (branchEligibility.status === "unknown") return "Branch eligibility is unknown; standard issue multiplier is not applied.";
if (branchEligibility.stale) return "Branch eligibility evidence is stale; standard issue multiplier is not applied.";
if (branchEligibility.source === "user_supplied") return "Branch eligibility evidence is user-supplied; standard issue multiplier is not applied until verified metadata is available.";
return "Branch eligibility is not confirmed; standard issue multiplier is not applied.";
}

function withValidatedLinkedIssueScenario(input: ScorePreviewInput): ScorePreviewInput {
const mode = input.linkedIssueMode ?? "none";
if (mode === "maintainer") return input;
Expand Down
5 changes: 4 additions & 1 deletion src/signals/local-branch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1123,12 +1123,15 @@ function buildPublicSafePrPacket(args: {

function linkedIssueHygieneLines(branchEligibility: BranchEligibilityResult): string[] {
if (!branchEligibility.required) return ["- No issue-specific branch gate was required from supplied metadata."];
if (branchEligibility.status === "eligible") {
if (branchEligibility.status === "eligible" && branchEligibility.source !== "user_supplied") {
return [
"- Linked issue context was checked from local/GitHub metadata.",
...(branchEligibility.stale ? ["- Reconfirm linked issue and base branch metadata before submission."] : []),
];
}
if (branchEligibility.status === "eligible") {
return ["- Linked issue context was not confirmed; verify the issue reference and base branch before submission."];
}
if (branchEligibility.status === "ineligible") {
return ["- Linked issue context needs cleanup before presenting this PR as solving the issue."];
}
Expand Down
2 changes: 1 addition & 1 deletion test/integration/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1339,7 +1339,7 @@ describe("api routes", () => {
repoFullName: "entrius/allways-ui",
preflight: { localDiff: { testFileCount: 1, inferredLinkedIssues: [7] } },
scorePreview: { privateOnly: true },
branchEligibility: { required: true, status: "eligible", evidence: "provided" },
branchEligibility: { required: true, status: "unknown", evidence: "provided", source: "user_supplied" },
rewardRisk: { rewardUpside: { relevantLane: "direct_pr" } },
prPacket: { titleSuggestion: "Fix dashboard cache refresh after reconnect" },
});
Expand Down
1 change: 1 addition & 0 deletions test/unit/local-branch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,7 @@ describe("local branch analysis", () => {
changedFiles: [{ path: "src/cache.ts", additions: 12, deletions: 1, status: "modified" }],
validation: [{ command: "npm test -- cache", status: "passed" }],
localScorer: { mode: "external_command", sourceTokenScore: 42, totalTokenScore: 70, sourceLines: 42 },
branchEligibility: { status: "eligible", source: "github_metadata" },
},
repo,
issues: [{ repoFullName: repo.fullName, number: 7, title: "Cache refresh fails", state: "open", labels: ["bug"], linkedPrs: [] }],
Expand Down
14 changes: 8 additions & 6 deletions test/unit/scoring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ MAX_CODE_DENSITY_MULTIPLIER = 1.15
labels: ["bug"],
linkedIssueMode: "standard",
linkedIssueContext: { status: "validated", source: "official_mirror", issueNumbers: [7], solvedByPullRequests: [100] },
branchEligibility: { status: "eligible", source: "github_metadata" },
sourceTokenScore: 58,
totalTokenScore: 1500,
sourceLines: 120,
Expand Down Expand Up @@ -260,6 +261,7 @@ MAX_CODE_DENSITY_MULTIPLIER = 1.15
labels: ["bug"],
linkedIssueMode: "standard",
linkedIssueContext: { status: "validated", source: "official_mirror", issueNumbers: [7], solvedByPullRequests: [100] },
branchEligibility: { status: "eligible", source: "github_metadata" },
sourceTokenScore: 60,
totalTokenScore: 90,
sourceLines: 50,
Expand Down Expand Up @@ -345,7 +347,7 @@ MAX_CODE_DENSITY_MULTIPLIER = 1.15
expect(ineligible.recommendation.actions).toEqual(expect.arrayContaining([expect.stringMatching(/eligible branch/i)]));
expect(missing.branchEligibility).toMatchObject({ required: true, status: "unknown", evidence: "missing", source: "missing" });
expect(missing.blockedBy).toEqual(expect.arrayContaining([expect.objectContaining({ code: "branch_eligibility_missing", severity: "context" })]));
expect(missing.scoreEstimate.issueMultiplier).toBe(1.33);
expect(missing.scoreEstimate.issueMultiplier).toBe(1);
expect(unknown.branchEligibility).toMatchObject({ required: true, status: "unknown", evidence: "provided", source: "user_supplied", stale: true });
expect(unknown.branchEligibility.warnings.join(" ")).toMatch(/unknown.*stale/i);
expect(unknown.recommendation.actions).toEqual(expect.arrayContaining([expect.stringMatching(/refresh branch\/base eligibility metadata/i)]));
Expand All @@ -370,7 +372,7 @@ MAX_CODE_DENSITY_MULTIPLIER = 1.15
const validated = buildScorePreview({
repo,
snapshot,
input: { ...baseInput, linkedIssueContext: { status: "validated", source: "official_mirror", issueNumbers: [7], solvedByPullRequests: [101] } },
input: { ...baseInput, linkedIssueContext: { status: "validated", source: "official_mirror", issueNumbers: [7], solvedByPullRequests: [101] }, branchEligibility: { status: "eligible", source: "github_metadata" } },
});
const invalid = buildScorePreview({
repo,
Expand All @@ -390,7 +392,7 @@ MAX_CODE_DENSITY_MULTIPLIER = 1.15
const defaultValidated = buildScorePreview({
repo,
snapshot,
input: { ...baseInput, linkedIssueContext: { source: "user_supplied", issueNumbers: [10], solvedByPullRequests: [110] } },
input: { ...baseInput, linkedIssueContext: { source: "user_supplied", issueNumbers: [10], solvedByPullRequests: [110] }, branchEligibility: { status: "eligible", source: "github_metadata" } },
});
const validatedWithoutSolverNumber = buildScorePreview({
repo,
Expand Down Expand Up @@ -436,8 +438,8 @@ MAX_CODE_DENSITY_MULTIPLIER = 1.15
expect(raw.scoreEstimate.issueMultiplier).toBe(1);
expect(raw.blockedBy).toEqual(expect.arrayContaining([expect.objectContaining({ code: "linked_issue_unvalidated", severity: "context" })]));
const rawFixedScenario = raw.scenarioPreviews.find((scenario) => scenario.name === "linkedIssueFixed");
expect(rawFixedScenario?.linkedIssueMultiplier).toMatchObject({ status: "validated", appliedMultiplier: 1.33 });
expect(rawFixedScenario?.linkedIssueMultiplier.reason).toBe("Linked issue context is solved-by-PR validated for issue(s) #7.");
expect(rawFixedScenario?.linkedIssueMultiplier).toMatchObject({ status: "validated", appliedMultiplier: 1 });
expect(rawFixedScenario?.linkedIssueMultiplier.reason).toMatch(/Branch eligibility evidence is missing/);
expect(validated.linkedIssueMultiplier).toMatchObject({ status: "validated", eligible: true, solvedByPullRequests: [101], appliedMultiplier: 1.33 });
expect(validated.scoreEstimate.issueMultiplier).toBe(1.33);
expect(invalid.linkedIssueMultiplier).toMatchObject({ status: "invalid", eligible: false, appliedMultiplier: 1 });
Expand Down Expand Up @@ -506,7 +508,7 @@ MAX_CODE_DENSITY_MULTIPLIER = 1.15
expect(afterPending?.source).toBe("user_supplied");
expect(afterPending?.gates.credibilityObserved).toBe(0.8);
expect(afterPending?.effectiveEstimatedScore).toBeGreaterThan(0);
expect(linkedIssueFixed?.scoreEstimate.issueMultiplier).toBe(1.33);
expect(linkedIssueFixed?.scoreEstimate.issueMultiplier).toBe(1);
expect(JSON.stringify(preview)).not.toMatch(/guaranteed payout|wallet|hotkey|farming/i);
});

Expand Down
Loading