From e279b8d07be071af6d2d751c811eacb6af9a0c21 Mon Sep 17 00:00:00 2001 From: Clayton Date: Fri, 5 Jun 2026 06:14:29 -0500 Subject: [PATCH 1/6] feat(agent): render public-safe scenario summaries --- src/scenarios/scenario-summary.ts | 165 +++++++++++ test/unit/scenario-summary.test.ts | 446 +++++++++++++++++++++++++++++ 2 files changed, 611 insertions(+) create mode 100644 src/scenarios/scenario-summary.ts create mode 100644 test/unit/scenario-summary.test.ts diff --git a/src/scenarios/scenario-summary.ts b/src/scenarios/scenario-summary.ts new file mode 100644 index 0000000000..d69703e164 --- /dev/null +++ b/src/scenarios/scenario-summary.ts @@ -0,0 +1,165 @@ +import { sanitizePublicComment } from "../github/commands"; +import type { EligibilityPlan } from "../services/eligibility-plan"; +import type { OpenPrPressureSimulation, OpenPrStrategyOption } from "../services/open-pr-pressure-scenarios"; +import type { ScoreGateBlocker } from "../scoring/preview"; +import type { PendingPrScenarioDetection } from "../scoring/pending-pr-scenarios"; +import type { AgentScenarioInput } from "./input-model"; +import { serializeScenarioInputPublic } from "./input-model"; + +/** + * Public-safe rendering of scenario simulator outputs for MCP/API clients and + * control-panel UIs. Ranked options, rationales, obstacles, assumptions, and + * next steps are included; all reward, score, wallet, hotkey, trust-score, + * and private-scoreability data is excluded. + * + * Advisory only — never files issues, opens PRs, comments, labels, closes, or merges. + */ + +export type RenderedScenarioOption = { + rank: number; + label: string; + rationale: string; + obstacles: string[]; + assumptions: string[]; + nextStep: string; + recommended: boolean; +}; + +export type PublicScenarioSummary = { + repoFullName: string; + generatedAt: string; + advisoryOnly: true; + notAutonomousPrBot: true; + notPublicScoring: true; + headline: string; + options: RenderedScenarioOption[]; + eligibilityNotes: string[]; + blockerNotes: string[]; + dataClassification: { + facts: string[]; + assumptions: string[]; + unavailableSignals: string[]; + }; +}; + +export type ScenarioSummaryInput = { + repoFullName: string; + generatedAt: string; + pressureSimulation?: OpenPrPressureSimulation | undefined; + eligibilityPlan?: EligibilityPlan | undefined; + pendingDetection?: PendingPrScenarioDetection | undefined; + publicBlockers?: ScoreGateBlocker[] | undefined; + scenarioInput?: AgentScenarioInput | undefined; +}; + +const OPTION_NEXT_STEPS: Record = { + open_new_work: "Verify linked issue eligibility and branch signals before opening the new PR.", + wait: "Monitor the repo queue and re-evaluate when pressure drops or existing work lands.", + cleanup_first: "Review your open PR(s): advance, rebase, or close stale work before opening more.", +}; + +const PUBLIC_BLOCKER_TEXT: Partial> = { + base_token_gate: "The change size may be too small to meet the contribution threshold.", + open_pr_threshold: "Too many concurrent open PRs exist; landing or closing some would help.", + credibility_floor: "Contributor credibility evidence is below the expected floor.", + review_penalty: "Review churn history may reduce the contribution quality signal.", + metadata_only: "Only metadata signals are available; detailed analysis requires full context.", + linked_issue_invalid: "The linked issue is invalid or no longer open.", + linked_issue_unvalidated: "The linked issue context is present but not yet validated.", + branch_ineligible: "The branch is not eligible for linked-issue assumptions.", + branch_eligibility_missing: "Branch eligibility metadata is missing; refresh before proceeding.", + duplicate_risk: "Potential duplicate or conflicting open work was detected.", + stale_work: "Stale open PR(s) detected; consider closing stale work before opening more.", +}; + +const FORBIDDEN_PUBLIC_LANGUAGE = + /wallet|hotkey|coldkey|mnemonic|seed phrase|payout|reward[-\s]?estimate|farming|raw trust|trust[-\s]?score|scoreability|private[-\s]?reviewability|public[-\s]?score[-\s]?(?:estimate|prediction)/i; + +function renderOptions(simulation: OpenPrPressureSimulation): RenderedScenarioOption[] { + return simulation.scenarios.map((s) => { + const rationaleParts = [...s.facts.slice(0, 1), ...s.tradeoffs.slice(0, 1)]; + return { + rank: s.rank, + label: sanitizePublicComment(s.label), + rationale: sanitizePublicComment(rationaleParts.join(" ")), + obstacles: s.blockers.map((b) => sanitizePublicComment(b)), + assumptions: s.assumptions.map((a) => sanitizePublicComment(a)), + nextStep: sanitizePublicComment(OPTION_NEXT_STEPS[s.option] ?? "Review available signals before acting."), + recommended: s.recommended, + }; + }); +} + +function renderHeadline( + pressureSimulation: OpenPrPressureSimulation | undefined, + eligibilityPlan: EligibilityPlan | undefined, +): string { + if (pressureSimulation) { + return sanitizePublicComment(pressureSimulation.summary); + } + if (eligibilityPlan) { + return sanitizePublicComment(eligibilityPlan.publicSummary); + } + return "Advisory scenario summary generated from available repo signals."; +} + +function extractEligibilityNotes(plan: EligibilityPlan): string[] { + return [ + sanitizePublicComment(plan.publicSummary), + ...plan.blockers.map((b) => sanitizePublicComment(b)), + ...plan.cleanupPaths.map((p) => sanitizePublicComment(p)), + ...(plan.linkedIssueProjection ? [sanitizePublicComment(plan.linkedIssueProjection)] : []), + ].filter(Boolean); +} + +function extractBlockerNotes(blockers: ScoreGateBlocker[]): string[] { + return blockers + .filter((b) => b.code !== "repo_not_registered" && b.code !== "inactive_allocation") + .map((b) => sanitizePublicComment(PUBLIC_BLOCKER_TEXT[b.code] ?? b.detail)) + .filter(Boolean); +} + +function extractDataClassification(scenarioInput: AgentScenarioInput | undefined): PublicScenarioSummary["dataClassification"] { + if (!scenarioInput) { + return { facts: [], assumptions: [], unavailableSignals: [] }; + } + const pub = serializeScenarioInputPublic(scenarioInput); + return { + facts: pub.facts.map((e) => sanitizePublicComment(e.label)), + assumptions: pub.assumptions.map((e) => sanitizePublicComment(e.label)), + unavailableSignals: pub.unavailableSignals.map((e) => sanitizePublicComment(e.label)), + }; +} + +function assertPublicSummaryClean(summary: PublicScenarioSummary): void { + const serialized = JSON.stringify(summary); + /* v8 ignore start -- All text fields are sanitized before this guard; defensive check for future fields. */ + if (FORBIDDEN_PUBLIC_LANGUAGE.test(serialized)) { + throw new Error("Public scenario summary still contains forbidden language."); + } + /* v8 ignore end */ +} + +/** + * Render a public-safe scenario summary from simulator outputs. + * + * Pure and read-only; no network or state access. All text fields pass through + * `sanitizePublicComment` before output, and a final defensive guard rejects any + * serialization that still contains forbidden language. + */ +export function renderPublicScenarioSummary(input: ScenarioSummaryInput): PublicScenarioSummary { + const summary: PublicScenarioSummary = { + repoFullName: input.repoFullName, + generatedAt: input.generatedAt, + advisoryOnly: true, + notAutonomousPrBot: true, + notPublicScoring: true, + headline: renderHeadline(input.pressureSimulation, input.eligibilityPlan), + options: input.pressureSimulation ? renderOptions(input.pressureSimulation) : [], + eligibilityNotes: input.eligibilityPlan ? extractEligibilityNotes(input.eligibilityPlan) : [], + blockerNotes: input.publicBlockers ? extractBlockerNotes(input.publicBlockers) : [], + dataClassification: extractDataClassification(input.scenarioInput), + }; + assertPublicSummaryClean(summary); + return summary; +} diff --git a/test/unit/scenario-summary.test.ts b/test/unit/scenario-summary.test.ts new file mode 100644 index 0000000000..7d5275b0ea --- /dev/null +++ b/test/unit/scenario-summary.test.ts @@ -0,0 +1,446 @@ +import { describe, expect, it } from "vitest"; +import { sanitizePublicComment } from "../../src/github/commands"; +import { buildScorePreview, type ScorePreviewInput } from "../../src/scoring/preview"; +import { deriveEligibilityPlan } from "../../src/services/eligibility-plan"; +import { simulateOpenPrPressure, type OpenPrPressureInput } from "../../src/services/open-pr-pressure-scenarios"; +import { buildScenarioInput, createScenarioSignalEntry } from "../../src/scenarios/input-model"; +import { renderPublicScenarioSummary, type ScenarioSummaryInput } from "../../src/scenarios/scenario-summary"; +import type { QueueHealth, RoleContext } from "../../src/signals/engine"; +import type { ScoringModelSnapshotRecord } from "../../src/types"; + +const FORBIDDEN_PUBLIC_LANGUAGE = + /wallet|hotkey|coldkey|mnemonic|seed phrase|payout|reward[-\s]?estimate|farming|raw trust|trust[-\s]?score|scoreability|private[-\s]?reviewability|public[-\s]?score[-\s]?(?:estimate|prediction)/i; + +// ── Shared fixtures ──────────────────────────────────────────────────────── + +const snapshot: ScoringModelSnapshotRecord = { + id: "summary-test-model", + sourceKind: "test", + sourceUrl: "fixture://constants.py", + fetchedAt: "2026-06-03T00:00:00.000Z", + activeModel: "current_density_model", + constants: { + OSS_EMISSION_SHARE: 0.9, + MERGED_PR_BASE_SCORE: 25, + MIN_TOKEN_SCORE_FOR_BASE_SCORE: 5, + MAX_CODE_DENSITY_MULTIPLIER: 1.15, + MAX_CONTRIBUTION_BONUS: 25, + CONTRIBUTION_SCORE_FOR_FULL_BONUS: 1500, + STANDARD_ISSUE_MULTIPLIER: 1.33, + MAINTAINER_ISSUE_MULTIPLIER: 1.66, + MIN_CREDIBILITY: 0.8, + REVIEW_PENALTY_RATE: 0.15, + EXCESSIVE_PR_PENALTY_BASE_THRESHOLD: 2, + OPEN_PR_THRESHOLD_TOKEN_SCORE: 300, + MAX_OPEN_PR_THRESHOLD: 30, + OPEN_PR_COLLATERAL_PERCENT: 0.2, + SRC_TOK_SATURATION_SCALE: 58, + }, + programmingLanguages: {}, + registrySnapshotId: "registry-fixture", + warnings: [], + payload: {}, +}; + +const repo = { + fullName: "octo/demo", + owner: "octo", + name: "demo", + isInstalled: true, + isRegistered: true, + isPrivate: false, + registryConfig: { repo: "octo/demo", emissionShare: 0.02, issueDiscoveryShare: 0, labelMultipliers: {}, maintainerCut: 0, raw: {} }, +}; + +function queueHealth(level: QueueHealth["level"], overrides: Partial = {}): QueueHealth { + return { + repoFullName: "octo/demo", + generatedAt: "2026-06-03T00:00:00.000Z", + burdenScore: level === "low" ? 10 : level === "medium" ? 40 : level === "high" ? 65 : 90, + level, + summary: `Queue is ${level}.`, + signals: { + openIssues: 5, + openPullRequests: level === "low" ? 1 : 12, + unlinkedPullRequests: 0, + stalePullRequests: level === "high" || level === "critical" ? 4 : 0, + maintainerAuthoredPullRequests: 0, + collisionClusters: 0, + ageBuckets: { under7Days: 1, days7To30: 0, over30Days: 0 }, + likelyReviewablePullRequests: 1, + ...overrides, + }, + findings: [], + }; +} + +function roleContext(maintainerLane: boolean): RoleContext { + return { + login: "miner-a", + repoFullName: "octo/demo", + generatedAt: "2026-06-03T00:00:00.000Z", + role: maintainerLane ? "owner" : "outside_contributor", + maintainerLane, + normalContributorEvidenceAllowed: !maintainerLane, + source: maintainerLane ? "repo_owner_match" : "cache", + association: maintainerLane ? "OWNER" : "NONE", + reasons: [], + guidance: maintainerLane ? "maintainer" : "contributor", + }; +} + +function pressureInput(overrides: Partial = {}): OpenPrPressureInput { + return { + repoFullName: "octo/demo", + generatedAt: "2026-06-03T00:00:00.000Z", + queueHealth: queueHealth("low"), + roleContext: roleContext(false), + contributorOpenPrCount: 0, + ...overrides, + }; +} + +function previewResult(input: Partial = {}) { + return buildScorePreview({ + repo, + snapshot, + input: { + repoFullName: "octo/demo", + sourceTokenScore: 60, + totalTokenScore: 80, + sourceLines: 50, + openPrCount: 1, + credibility: 1, + ...input, + }, + }); +} + +function baseSummaryInput(overrides: Partial = {}): ScenarioSummaryInput { + return { + repoFullName: "octo/demo", + generatedAt: "2026-06-03T00:00:00.000Z", + ...overrides, + }; +} + +// ── Public summary structure ─────────────────────────────────────────────── + +describe("renderPublicScenarioSummary — structure", () => { + it("always includes advisory-only flags", () => { + const summary = renderPublicScenarioSummary(baseSummaryInput()); + expect(summary.advisoryOnly).toBe(true); + expect(summary.notAutonomousPrBot).toBe(true); + expect(summary.notPublicScoring).toBe(true); + }); + + it("passes through repoFullName and generatedAt unchanged", () => { + const summary = renderPublicScenarioSummary(baseSummaryInput()); + expect(summary.repoFullName).toBe("octo/demo"); + expect(summary.generatedAt).toBe("2026-06-03T00:00:00.000Z"); + }); + + it("returns empty options when no pressure simulation is provided", () => { + const summary = renderPublicScenarioSummary(baseSummaryInput()); + expect(summary.options).toHaveLength(0); + }); + + it("returns empty eligibilityNotes when no eligibility plan is provided", () => { + const summary = renderPublicScenarioSummary(baseSummaryInput()); + expect(summary.eligibilityNotes).toHaveLength(0); + }); + + it("returns empty blockerNotes when no blockers are provided", () => { + const summary = renderPublicScenarioSummary(baseSummaryInput()); + expect(summary.blockerNotes).toHaveLength(0); + }); + + it("returns empty dataClassification when no scenarioInput is provided", () => { + const summary = renderPublicScenarioSummary(baseSummaryInput()); + expect(summary.dataClassification).toEqual({ facts: [], assumptions: [], unavailableSignals: [] }); + }); + + it("emits a fallback headline when neither simulation nor eligibility plan is provided", () => { + const summary = renderPublicScenarioSummary(baseSummaryInput()); + expect(summary.headline.length).toBeGreaterThan(0); + expect(summary.headline).not.toMatch(FORBIDDEN_PUBLIC_LANGUAGE); + }); +}); + +// ── Snapshot: open-PR pressure simulation ──────────────────────────────── + +describe("renderPublicScenarioSummary — open-PR pressure simulation", () => { + it("renders ranked options from a low-pressure contributor simulation", () => { + const simulation = simulateOpenPrPressure(pressureInput({ queueHealth: queueHealth("low"), contributorOpenPrCount: 0 })); + const summary = renderPublicScenarioSummary(baseSummaryInput({ pressureSimulation: simulation })); + + expect(summary.options).toHaveLength(3); + expect(summary.options.map((o) => o.rank)).toEqual([1, 2, 3]); + expect(summary.options[0]).toMatchObject({ + rank: 1, + recommended: true, + }); + expect(summary.options[0]!.label.length).toBeGreaterThan(0); + expect(summary.options[0]!.rationale.length).toBeGreaterThan(0); + expect(summary.options[0]!.nextStep.length).toBeGreaterThan(0); + }); + + it("uses the simulation summary as the headline", () => { + const simulation = simulateOpenPrPressure(pressureInput({ queueHealth: queueHealth("low"), contributorOpenPrCount: 0 })); + const summary = renderPublicScenarioSummary(baseSummaryInput({ pressureSimulation: simulation })); + expect(summary.headline).toBe(simulation.summary); + }); + + it("renders ranked options from a high-pressure contributor simulation with open PRs", () => { + const simulation = simulateOpenPrPressure(pressureInput({ queueHealth: queueHealth("critical"), contributorOpenPrCount: 2 })); + const summary = renderPublicScenarioSummary(baseSummaryInput({ pressureSimulation: simulation })); + + const top = summary.options[0]!; + expect(top.recommended).toBe(true); + expect(top.obstacles.length + top.assumptions.length).toBeGreaterThan(0); + }); + + it("renders ranked options for a maintainer-lane simulation", () => { + const simulation = simulateOpenPrPressure( + pressureInput({ queueHealth: queueHealth("medium"), roleContext: roleContext(true), contributorOpenPrCount: 1 }), + ); + const summary = renderPublicScenarioSummary(baseSummaryInput({ pressureSimulation: simulation })); + expect(summary.options).toHaveLength(3); + expect(summary.options[0]!.recommended).toBe(true); + }); + + it("renders ranked options when queue signals are missing", () => { + const simulation = simulateOpenPrPressure(pressureInput({ queueHealth: null, contributorOpenPrCount: 0 })); + const summary = renderPublicScenarioSummary(baseSummaryInput({ pressureSimulation: simulation })); + expect(summary.options).toHaveLength(3); + expect(summary.headline).toMatch(/unavailable|conservative/i); + }); +}); + +// ── Snapshot: eligibility plan ───────────────────────────────────────────── + +describe("renderPublicScenarioSummary — eligibility plan", () => { + it("uses eligibility plan summary as headline when no pressure simulation is present", () => { + const result = previewResult({ + linkedIssueMode: "standard", + linkedIssueContext: { status: "validated", source: "official_mirror", issueNumbers: [42], solvedByPullRequests: [] }, + branchEligibility: { status: "eligible", source: "github_metadata" }, + }); + const plan = deriveEligibilityPlan(result); + const summary = renderPublicScenarioSummary(baseSummaryInput({ eligibilityPlan: plan })); + expect(summary.headline).toBe(plan.publicSummary); + expect(summary.eligibilityNotes).toContain(plan.publicSummary); + }); + + it("surfaces eligibility blockers in eligibilityNotes", () => { + const result = previewResult({ + linkedIssueMode: "standard", + linkedIssueContext: { status: "invalid", source: "official_mirror", issueNumbers: [99] }, + branchEligibility: { status: "eligible", source: "github_metadata" }, + }); + const plan = deriveEligibilityPlan(result); + const summary = renderPublicScenarioSummary(baseSummaryInput({ eligibilityPlan: plan })); + const notesText = summary.eligibilityNotes.join(" "); + expect(notesText).toMatch(/invalid|no longer open|verify/i); + expect(notesText).not.toMatch(FORBIDDEN_PUBLIC_LANGUAGE); + }); + + it("includes cleanup paths in eligibilityNotes when present", () => { + const result = previewResult({ + linkedIssueMode: "standard", + linkedIssueContext: { status: "raw", source: "user_supplied", issueNumbers: [77] }, + branchEligibility: { status: "eligible", source: "github_metadata" }, + }); + const plan = deriveEligibilityPlan(result); + const summary = renderPublicScenarioSummary(baseSummaryInput({ eligibilityPlan: plan })); + expect(plan.cleanupPaths.length).toBeGreaterThan(0); + expect(summary.eligibilityNotes.some((n) => n.match(/solved-by-PR|validate|evidence/i))).toBe(true); + }); + + it("includes linked issue projection in eligibilityNotes when available", () => { + const result = previewResult({ + linkedIssueMode: "standard", + linkedIssueContext: { status: "raw", source: "user_supplied", issueNumbers: [77] }, + branchEligibility: { status: "eligible", source: "github_metadata" }, + }); + const plan = deriveEligibilityPlan(result); + if (plan.linkedIssueProjection) { + const summary = renderPublicScenarioSummary(baseSummaryInput({ eligibilityPlan: plan })); + expect(summary.eligibilityNotes.join(" ")).toMatch(/linked.issue|contribution consideration/i); + } + }); +}); + +// ── Snapshot: blocker notes ──────────────────────────────────────────────── + +describe("renderPublicScenarioSummary — blocker notes", () => { + it("surfaces open_pr_threshold and stale_work blockers as human-readable notes", () => { + const result = previewResult({ openPrCount: 5, observedStalePrCount: 2 }); + const summary = renderPublicScenarioSummary(baseSummaryInput({ publicBlockers: result.blockedBy })); + const notes = summary.blockerNotes.join(" "); + expect(notes).toMatch(/too many.*open PR|open PR.*exist|land|close/i); + expect(notes).toMatch(/stale/i); + }); + + it("includes duplicate_risk note when the blocker is present", () => { + const result = previewResult({ duplicateRiskCount: 2 }); + const summary = renderPublicScenarioSummary(baseSummaryInput({ publicBlockers: result.blockedBy })); + expect(summary.blockerNotes.join(" ")).toMatch(/duplicate|conflicting/i); + }); + + it("excludes repo_not_registered and inactive_allocation from public blocker notes", () => { + const unregisteredRepo = { ...repo, isRegistered: false }; + const result = buildScorePreview({ repo: unregisteredRepo, snapshot, input: { repoFullName: "octo/demo", sourceTokenScore: 0 } }); + const summary = renderPublicScenarioSummary(baseSummaryInput({ publicBlockers: result.blockedBy })); + const notes = summary.blockerNotes.join(" "); + expect(notes).not.toMatch(/not registered|no active allocation/i); + }); + + it("blocker notes are empty when no blockers are present", () => { + const result = previewResult({ openPrCount: 1, credibility: 1 }); + const cleanBlockers = result.blockedBy.filter((b) => b.severity === "blocker"); + const summary = renderPublicScenarioSummary(baseSummaryInput({ publicBlockers: cleanBlockers })); + expect(summary.blockerNotes).toHaveLength(0); + }); +}); + +// ── Snapshot: data classification from scenario input ───────────────────── + +describe("renderPublicScenarioSummary — data classification", () => { + it("populates facts, assumptions, and unavailableSignals labels from a scenario input", () => { + const input = buildScenarioInput({ + scenarioType: "branch_preflight", + repoFullName: "octo/demo", + facts: [ + createScenarioSignalEntry({ id: "queue", kind: "fact", label: "Queue signals", detail: "Two open PRs.", source: "github_observed" }), + ], + assumptions: [ + createScenarioSignalEntry({ id: "pending", kind: "assumption", label: "Pending merges", detail: "One approved PR.", source: "user_supplied" }), + ], + unavailableSignals: [ + createScenarioSignalEntry({ id: "stats", kind: "unavailable", label: "Official stats", detail: "Not available.", source: "missing" }), + ], + }); + const summary = renderPublicScenarioSummary(baseSummaryInput({ scenarioInput: input })); + expect(summary.dataClassification.facts).toContain("Queue signals"); + expect(summary.dataClassification.assumptions).toContain("Pending merges"); + expect(summary.dataClassification.unavailableSignals).toContain("Official stats"); + }); + + it("does not include estimates bucket in the public data classification", () => { + const summary = renderPublicScenarioSummary(baseSummaryInput()); + expect(summary.dataClassification).not.toHaveProperty("estimates"); + }); +}); + +// ── Combined snapshot: pressure + eligibility + blockers ────────────────── + +describe("renderPublicScenarioSummary — combined inputs", () => { + it("combines pressure headline, options, eligibility notes, and blocker notes correctly", () => { + const simulation = simulateOpenPrPressure( + pressureInput({ queueHealth: queueHealth("high"), contributorOpenPrCount: 1 }), + ); + const result = previewResult({ + openPrCount: 1, + linkedIssueMode: "standard", + linkedIssueContext: { status: "raw", source: "user_supplied", issueNumbers: [5] }, + branchEligibility: { status: "eligible", source: "github_metadata" }, + observedStalePrCount: 1, + }); + const plan = deriveEligibilityPlan(result); + const summary = renderPublicScenarioSummary({ + repoFullName: "octo/demo", + generatedAt: "2026-06-03T00:00:00.000Z", + pressureSimulation: simulation, + eligibilityPlan: plan, + publicBlockers: result.blockedBy, + }); + + expect(summary.options.length).toBeGreaterThan(0); + expect(summary.eligibilityNotes.length).toBeGreaterThan(0); + expect(summary.blockerNotes.length).toBeGreaterThan(0); + expect(summary.headline).toBe(simulation.summary); + }); +}); + +// ── Sanitizer fixtures: restricted terminology ──────────────────────────── + +describe("sanitizer fixtures for restricted terminology", () => { + it("all text fields across a full-signal summary pass the forbidden-language check", () => { + const simulation = simulateOpenPrPressure( + pressureInput({ queueHealth: queueHealth("high"), contributorOpenPrCount: 2 }), + ); + const result = previewResult({ + openPrCount: 2, + linkedIssueMode: "standard", + linkedIssueContext: { status: "raw", source: "user_supplied", issueNumbers: [7] }, + branchEligibility: { status: "eligible", source: "github_metadata" }, + observedStalePrCount: 1, + duplicateRiskCount: 1, + }); + const plan = deriveEligibilityPlan(result); + const summary = renderPublicScenarioSummary({ + repoFullName: "octo/demo", + generatedAt: "2026-06-03T00:00:00.000Z", + pressureSimulation: simulation, + eligibilityPlan: plan, + publicBlockers: result.blockedBy, + }); + + const allText = JSON.stringify(summary); + expect(allText).not.toMatch(FORBIDDEN_PUBLIC_LANGUAGE); + }); + + it("all text fields are unchanged by a second pass of sanitizePublicComment", () => { + const simulation = simulateOpenPrPressure(pressureInput()); + const summary = renderPublicScenarioSummary(baseSummaryInput({ pressureSimulation: simulation })); + const textFields = [ + summary.headline, + ...summary.options.flatMap((o) => [o.label, o.rationale, o.nextStep, ...o.obstacles, ...o.assumptions]), + ...summary.eligibilityNotes, + ...summary.blockerNotes, + ]; + for (const field of textFields) { + expect(field).toBe(sanitizePublicComment(field)); + } + }); + + it("does not expose score, reward, or private context in any fixture variant", () => { + const fixtures: ScenarioSummaryInput[] = [ + baseSummaryInput(), + baseSummaryInput({ pressureSimulation: simulateOpenPrPressure(pressureInput({ queueHealth: queueHealth("critical"), contributorOpenPrCount: 3 })) }), + baseSummaryInput({ pressureSimulation: simulateOpenPrPressure(pressureInput({ queueHealth: null })) }), + baseSummaryInput({ eligibilityPlan: deriveEligibilityPlan(previewResult({ linkedIssueMode: "none" })) }), + baseSummaryInput({ publicBlockers: previewResult({ observedStalePrCount: 2, duplicateRiskCount: 1 }).blockedBy }), + ]; + for (const fixture of fixtures) { + const summary = renderPublicScenarioSummary(fixture); + expect(JSON.stringify(summary)).not.toMatch(/\bscore\b|reward|earn|payout|hotkey|wallet|trust score|scoreability/i); + } + }); + + it("makes no claim about autonomous PR filing, issue creation, or merge actions", () => { + const simulation = simulateOpenPrPressure(pressureInput({ queueHealth: queueHealth("medium"), contributorOpenPrCount: 1 })); + const summary = renderPublicScenarioSummary(baseSummaryInput({ pressureSimulation: simulation })); + const allText = JSON.stringify(summary); + expect(allText).not.toMatch(/will open|will merge|will close|will file|automatically|autonomously/i); + }); +}); + +// ── Advisory-only invariants ─────────────────────────────────────────────── + +describe("advisory-only invariants", () => { + it("advisory flags are present and set to true in all fixture variants", () => { + const fixtures: ScenarioSummaryInput[] = [ + baseSummaryInput(), + baseSummaryInput({ pressureSimulation: simulateOpenPrPressure(pressureInput()) }), + baseSummaryInput({ eligibilityPlan: deriveEligibilityPlan(previewResult()) }), + ]; + for (const fixture of fixtures) { + const summary = renderPublicScenarioSummary(fixture); + expect(summary.advisoryOnly).toBe(true); + expect(summary.notAutonomousPrBot).toBe(true); + expect(summary.notPublicScoring).toBe(true); + } + }); +}); From 02a0291946a34934ddf020538501c15b11c8e76d Mon Sep 17 00:00:00 2001 From: Clayton Date: Mon, 8 Jun 2026 00:10:30 -0500 Subject: [PATCH 2/6] fix: update --- src/scenarios/scenario-summary.ts | 52 ++- test/unit/scenario-summary.test.ts | 580 +++++++++++++---------------- 2 files changed, 303 insertions(+), 329 deletions(-) diff --git a/src/scenarios/scenario-summary.ts b/src/scenarios/scenario-summary.ts index d69703e164..02c04160ea 100644 --- a/src/scenarios/scenario-summary.ts +++ b/src/scenarios/scenario-summary.ts @@ -2,7 +2,7 @@ import { sanitizePublicComment } from "../github/commands"; import type { EligibilityPlan } from "../services/eligibility-plan"; import type { OpenPrPressureSimulation, OpenPrStrategyOption } from "../services/open-pr-pressure-scenarios"; import type { ScoreGateBlocker } from "../scoring/preview"; -import type { PendingPrScenarioDetection } from "../scoring/pending-pr-scenarios"; +import type { PendingPrScenarioDetection, OpenPrPendingClass } from "../scoring/pending-pr-scenarios"; import type { AgentScenarioInput } from "./input-model"; import { serializeScenarioInputPublic } from "./input-model"; @@ -25,6 +25,12 @@ export type RenderedScenarioOption = { recommended: boolean; }; +export type RenderedPendingPullRequest = { + pullNumber: number; + classification: string; + notes: string[]; +}; + export type PublicScenarioSummary = { repoFullName: string; generatedAt: string; @@ -35,6 +41,8 @@ export type PublicScenarioSummary = { options: RenderedScenarioOption[]; eligibilityNotes: string[]; blockerNotes: string[]; + pendingScenarioNotes: string[]; + pendingPullRequests: RenderedPendingPullRequest[]; dataClassification: { facts: string[]; assumptions: string[]; @@ -52,6 +60,15 @@ export type ScenarioSummaryInput = { scenarioInput?: AgentScenarioInput | undefined; }; +const PENDING_CLASSIFICATION_LABELS: Record = { + merge_ready: "merge-ready pending resolution", + stale_likely_close: "stale open work likely to close", + draft: "draft open PR", + blocked: "blocked open PR", + maintainer_lane: "maintainer-lane open PR", + open_other: "open PR", +}; + const OPTION_NEXT_STEPS: Record = { open_new_work: "Verify linked issue eligibility and branch signals before opening the new PR.", wait: "Monitor the repo queue and re-evaluate when pressure drops or existing work lands.", @@ -93,6 +110,7 @@ function renderOptions(simulation: OpenPrPressureSimulation): RenderedScenarioOp function renderHeadline( pressureSimulation: OpenPrPressureSimulation | undefined, eligibilityPlan: EligibilityPlan | undefined, + pendingDetection: PendingPrScenarioDetection | undefined, ): string { if (pressureSimulation) { return sanitizePublicComment(pressureSimulation.summary); @@ -100,6 +118,13 @@ function renderHeadline( if (eligibilityPlan) { return sanitizePublicComment(eligibilityPlan.publicSummary); } + if (pendingDetection) { + return sanitizePublicComment( + pendingDetection.source === "user_supplied" + ? "Pending open PR scenario assumptions were supplied for advisory planning." + : "Pending open PR resolution scenarios are available from cached GitHub metadata.", + ); + } return "Advisory scenario summary generated from available repo signals."; } @@ -119,6 +144,27 @@ function extractBlockerNotes(blockers: ScoreGateBlocker[]): string[] { .filter(Boolean); } +function extractPendingScenarioNotes(detection: PendingPrScenarioDetection): string[] { + const notes = [ + detection.source === "user_supplied" + ? sanitizePublicComment("Pending PR scenario counts were supplied by the caller as assumptions.") + : sanitizePublicComment("Pending PR scenarios were inferred from cached GitHub reviews, checks, and activity."), + ...detection.scenarioNotes.map((note) => sanitizePublicComment(note)), + ...(detection.expectedOpenPrCountAfterMerge !== undefined + ? [sanitizePublicComment(`Projected open PR count after pending cleanup: ${detection.expectedOpenPrCountAfterMerge}.`)] + : []), + ]; + return [...new Set(notes.filter(Boolean))]; +} + +function extractPendingPullRequests(detection: PendingPrScenarioDetection): RenderedPendingPullRequest[] { + return detection.classified.slice(0, 8).map((entry) => ({ + pullNumber: entry.number, + classification: sanitizePublicComment(PENDING_CLASSIFICATION_LABELS[entry.classification] ?? entry.classification.replace(/_/g, " ")), + notes: entry.reasons.slice(0, 3).map((reason) => sanitizePublicComment(reason)), + })); +} + function extractDataClassification(scenarioInput: AgentScenarioInput | undefined): PublicScenarioSummary["dataClassification"] { if (!scenarioInput) { return { facts: [], assumptions: [], unavailableSignals: [] }; @@ -154,10 +200,12 @@ export function renderPublicScenarioSummary(input: ScenarioSummaryInput): Public advisoryOnly: true, notAutonomousPrBot: true, notPublicScoring: true, - headline: renderHeadline(input.pressureSimulation, input.eligibilityPlan), + headline: renderHeadline(input.pressureSimulation, input.eligibilityPlan, input.pendingDetection), options: input.pressureSimulation ? renderOptions(input.pressureSimulation) : [], eligibilityNotes: input.eligibilityPlan ? extractEligibilityNotes(input.eligibilityPlan) : [], blockerNotes: input.publicBlockers ? extractBlockerNotes(input.publicBlockers) : [], + pendingScenarioNotes: input.pendingDetection ? extractPendingScenarioNotes(input.pendingDetection) : [], + pendingPullRequests: input.pendingDetection ? extractPendingPullRequests(input.pendingDetection) : [], dataClassification: extractDataClassification(input.scenarioInput), }; assertPublicSummaryClean(summary); diff --git a/test/unit/scenario-summary.test.ts b/test/unit/scenario-summary.test.ts index 7d5275b0ea..9bc55c6fc4 100644 --- a/test/unit/scenario-summary.test.ts +++ b/test/unit/scenario-summary.test.ts @@ -1,20 +1,19 @@ import { describe, expect, it } from "vitest"; import { sanitizePublicComment } from "../../src/github/commands"; -import { buildScorePreview, type ScorePreviewInput } from "../../src/scoring/preview"; -import { deriveEligibilityPlan } from "../../src/services/eligibility-plan"; -import { simulateOpenPrPressure, type OpenPrPressureInput } from "../../src/services/open-pr-pressure-scenarios"; import { buildScenarioInput, createScenarioSignalEntry } from "../../src/scenarios/input-model"; -import { renderPublicScenarioSummary, type ScenarioSummaryInput } from "../../src/scenarios/scenario-summary"; +import { renderPublicScenarioSummary } from "../../src/scenarios/scenario-summary"; +import { deriveEligibilityPlan } from "../../src/services/eligibility-plan"; +import { simulateOpenPrPressure } from "../../src/services/open-pr-pressure-scenarios"; +import type { PendingPrScenarioDetection } from "../../src/scoring/pending-pr-scenarios"; +import { buildScorePreview, type ScoreGateBlocker } from "../../src/scoring/preview"; import type { QueueHealth, RoleContext } from "../../src/signals/engine"; import type { ScoringModelSnapshotRecord } from "../../src/types"; const FORBIDDEN_PUBLIC_LANGUAGE = /wallet|hotkey|coldkey|mnemonic|seed phrase|payout|reward[-\s]?estimate|farming|raw trust|trust[-\s]?score|scoreability|private[-\s]?reviewability|public[-\s]?score[-\s]?(?:estimate|prediction)/i; -// ── Shared fixtures ──────────────────────────────────────────────────────── - const snapshot: ScoringModelSnapshotRecord = { - id: "summary-test-model", + id: "scenario-summary-model", sourceKind: "test", sourceUrl: "fixture://constants.py", fetchedAt: "2026-06-03T00:00:00.000Z", @@ -52,395 +51,322 @@ const repo = { registryConfig: { repo: "octo/demo", emissionShare: 0.02, issueDiscoveryShare: 0, labelMultipliers: {}, maintainerCut: 0, raw: {} }, }; -function queueHealth(level: QueueHealth["level"], overrides: Partial = {}): QueueHealth { +function queueHealth(level: QueueHealth["level"]): QueueHealth { return { repoFullName: "octo/demo", generatedAt: "2026-06-03T00:00:00.000Z", - burdenScore: level === "low" ? 10 : level === "medium" ? 40 : level === "high" ? 65 : 90, + burdenScore: 10, level, summary: `Queue is ${level}.`, signals: { - openIssues: 5, - openPullRequests: level === "low" ? 1 : 12, + openIssues: 2, + openPullRequests: 1, unlinkedPullRequests: 0, - stalePullRequests: level === "high" || level === "critical" ? 4 : 0, + stalePullRequests: 0, maintainerAuthoredPullRequests: 0, collisionClusters: 0, ageBuckets: { under7Days: 1, days7To30: 0, over30Days: 0 }, likelyReviewablePullRequests: 1, - ...overrides, }, findings: [], }; } -function roleContext(maintainerLane: boolean): RoleContext { +function roleContext(): RoleContext { return { login: "miner-a", repoFullName: "octo/demo", generatedAt: "2026-06-03T00:00:00.000Z", - role: maintainerLane ? "owner" : "outside_contributor", - maintainerLane, - normalContributorEvidenceAllowed: !maintainerLane, - source: maintainerLane ? "repo_owner_match" : "cache", - association: maintainerLane ? "OWNER" : "NONE", + role: "outside_contributor", + maintainerLane: false, + normalContributorEvidenceAllowed: true, + source: "cache", + association: "NONE", reasons: [], - guidance: maintainerLane ? "maintainer" : "contributor", + guidance: "contributor", }; } -function pressureInput(overrides: Partial = {}): OpenPrPressureInput { +function pendingDetection(overrides: Partial = {}): PendingPrScenarioDetection { return { - repoFullName: "octo/demo", - generatedAt: "2026-06-03T00:00:00.000Z", - queueHealth: queueHealth("low"), - roleContext: roleContext(false), - contributorOpenPrCount: 0, + source: "github_observed", + pendingMergedPrCount: 1, + pendingClosedPrCount: 1, + approvedPrCount: 1, + expectedOpenPrCountAfterMerge: 2, + scenarioNotes: [ + "GitHub-observed open PR state from cached reviews, checks, and activity timestamps (estimate only).", + "1 open PR(s) look merge-ready (approved, no changes requested, no failing checks, not draft/stale).", + ], + classified: [ + { + repoFullName: "octo/demo", + number: 11, + title: "Ready cleanup", + classification: "merge_ready", + reasons: ["Approved review in cache."], + }, + { + repoFullName: "octo/demo", + number: 12, + title: "Stale cleanup", + classification: "stale_likely_close", + reasons: ["No cached update for 30 day(s)."], + }, + { + repoFullName: "octo/demo", + number: 13, + title: "Draft work", + classification: "draft", + reasons: ["Draft PRs are not treated as likely to land."], + }, + { + repoFullName: "octo/demo", + number: 14, + title: "Blocked work", + classification: "blocked", + reasons: ["No approved review in cache."], + }, + { + repoFullName: "octo/demo", + number: 15, + title: "Maintainer work", + classification: "maintainer_lane", + reasons: ["Maintainer-lane context for this repo."], + }, + { + repoFullName: "octo/demo", + number: 16, + title: "Other work", + classification: "open_other", + reasons: ["Open PR still needs triage."], + }, + ], ...overrides, }; } -function previewResult(input: Partial = {}) { - return buildScorePreview({ - repo, - snapshot, - input: { +describe("renderPublicScenarioSummary", () => { + it("renders default headline and empty sections when only repo metadata is present", () => { + const summary = renderPublicScenarioSummary({ repoFullName: "octo/demo", - sourceTokenScore: 60, - totalTokenScore: 80, - sourceLines: 50, - openPrCount: 1, - credibility: 1, - ...input, - }, - }); -} - -function baseSummaryInput(overrides: Partial = {}): ScenarioSummaryInput { - return { - repoFullName: "octo/demo", - generatedAt: "2026-06-03T00:00:00.000Z", - ...overrides, - }; -} - -// ── Public summary structure ─────────────────────────────────────────────── - -describe("renderPublicScenarioSummary — structure", () => { - it("always includes advisory-only flags", () => { - const summary = renderPublicScenarioSummary(baseSummaryInput()); - expect(summary.advisoryOnly).toBe(true); - expect(summary.notAutonomousPrBot).toBe(true); - expect(summary.notPublicScoring).toBe(true); - }); - - it("passes through repoFullName and generatedAt unchanged", () => { - const summary = renderPublicScenarioSummary(baseSummaryInput()); - expect(summary.repoFullName).toBe("octo/demo"); - expect(summary.generatedAt).toBe("2026-06-03T00:00:00.000Z"); - }); - - it("returns empty options when no pressure simulation is provided", () => { - const summary = renderPublicScenarioSummary(baseSummaryInput()); - expect(summary.options).toHaveLength(0); - }); - - it("returns empty eligibilityNotes when no eligibility plan is provided", () => { - const summary = renderPublicScenarioSummary(baseSummaryInput()); - expect(summary.eligibilityNotes).toHaveLength(0); - }); - - it("returns empty blockerNotes when no blockers are provided", () => { - const summary = renderPublicScenarioSummary(baseSummaryInput()); - expect(summary.blockerNotes).toHaveLength(0); - }); - - it("returns empty dataClassification when no scenarioInput is provided", () => { - const summary = renderPublicScenarioSummary(baseSummaryInput()); + generatedAt: "2026-06-03T00:00:00.000Z", + }); + expect(summary.headline).toMatch(/Advisory scenario summary generated from available repo signals/i); + expect(summary.options).toEqual([]); + expect(summary.eligibilityNotes).toEqual([]); + expect(summary.blockerNotes).toEqual([]); + expect(summary.pendingScenarioNotes).toEqual([]); + expect(summary.pendingPullRequests).toEqual([]); expect(summary.dataClassification).toEqual({ facts: [], assumptions: [], unavailableSignals: [] }); + expect(summary.advisoryOnly).toBe(true); }); - it("emits a fallback headline when neither simulation nor eligibility plan is provided", () => { - const summary = renderPublicScenarioSummary(baseSummaryInput()); - expect(summary.headline.length).toBeGreaterThan(0); - expect(summary.headline).not.toMatch(FORBIDDEN_PUBLIC_LANGUAGE); - }); -}); - -// ── Snapshot: open-PR pressure simulation ──────────────────────────────── - -describe("renderPublicScenarioSummary — open-PR pressure simulation", () => { - it("renders ranked options from a low-pressure contributor simulation", () => { - const simulation = simulateOpenPrPressure(pressureInput({ queueHealth: queueHealth("low"), contributorOpenPrCount: 0 })); - const summary = renderPublicScenarioSummary(baseSummaryInput({ pressureSimulation: simulation })); + it("renders pressure options, eligibility notes, blockers, and data classification", () => { + const pressureSimulation = simulateOpenPrPressure({ + repoFullName: "octo/demo", + generatedAt: "2026-06-03T00:00:00.000Z", + queueHealth: queueHealth("low"), + roleContext: roleContext(), + contributorOpenPrCount: 0, + }); + const preview = buildScorePreview({ + repo, + snapshot, + input: { + repoFullName: "octo/demo", + sourceTokenScore: 60, + totalTokenScore: 80, + sourceLines: 50, + openPrCount: 1, + credibility: 1, + metadataOnly: true, + linkedIssueMode: "standard", + linkedIssueContext: { status: "raw", source: "user_supplied", issueNumbers: [77] }, + branchEligibility: { status: "eligible", source: "github_metadata" }, + }, + }); + const eligibilityPlan = deriveEligibilityPlan(preview); + const scenarioInput = buildScenarioInput({ + scenarioType: "open_pr_pressure", + repoFullName: "octo/demo", + facts: [ + createScenarioSignalEntry({ + id: "queue", + kind: "fact", + label: "Queue", + detail: "Repo has one open PR in cached metadata.", + source: "github_observed", + }), + ], + }); - expect(summary.options).toHaveLength(3); - expect(summary.options.map((o) => o.rank)).toEqual([1, 2, 3]); - expect(summary.options[0]).toMatchObject({ - rank: 1, - recommended: true, + const summary = renderPublicScenarioSummary({ + repoFullName: "octo/demo", + generatedAt: "2026-06-03T00:00:00.000Z", + pressureSimulation, + eligibilityPlan, + publicBlockers: [ + ...preview.blockedBy, + { code: "repo_not_registered", severity: "blocker", detail: "Repo is not registered." }, + { code: "inactive_allocation", severity: "blocker", detail: "Allocation inactive." }, + { + code: "future_blocker" as ScoreGateBlocker["code"], + severity: "context", + detail: "Custom public blocker detail.", + }, + ], + scenarioInput, }); - expect(summary.options[0]!.label.length).toBeGreaterThan(0); - expect(summary.options[0]!.rationale.length).toBeGreaterThan(0); - expect(summary.options[0]!.nextStep.length).toBeGreaterThan(0); - }); - it("uses the simulation summary as the headline", () => { - const simulation = simulateOpenPrPressure(pressureInput({ queueHealth: queueHealth("low"), contributorOpenPrCount: 0 })); - const summary = renderPublicScenarioSummary(baseSummaryInput({ pressureSimulation: simulation })); - expect(summary.headline).toBe(simulation.summary); + expect(summary.headline).toBe(sanitizePublicComment(pressureSimulation.summary)); + expect(summary.options.length).toBe(3); + expect(summary.options[0]?.recommended).toBe(true); + expect(summary.eligibilityNotes.length).toBeGreaterThan(0); + expect(summary.blockerNotes.join(" ")).toMatch(/linked issue context is present but not yet validated|Custom public blocker detail/i); + expect(summary.blockerNotes.join(" ")).not.toMatch(/Repo is not registered|Allocation inactive/i); + expect(summary.dataClassification.facts).toContain("Queue"); }); - it("renders ranked options from a high-pressure contributor simulation with open PRs", () => { - const simulation = simulateOpenPrPressure(pressureInput({ queueHealth: queueHealth("critical"), contributorOpenPrCount: 2 })); - const summary = renderPublicScenarioSummary(baseSummaryInput({ pressureSimulation: simulation })); - - const top = summary.options[0]!; - expect(top.recommended).toBe(true); - expect(top.obstacles.length + top.assumptions.length).toBeGreaterThan(0); - }); + it("renders sanitized pending scenario notes and classified open PR summaries", () => { + const summary = renderPublicScenarioSummary({ + repoFullName: "octo/demo", + generatedAt: "2026-06-03T00:00:00.000Z", + pendingDetection: pendingDetection(), + }); - it("renders ranked options for a maintainer-lane simulation", () => { - const simulation = simulateOpenPrPressure( - pressureInput({ queueHealth: queueHealth("medium"), roleContext: roleContext(true), contributorOpenPrCount: 1 }), + expect(summary.headline).toMatch(/Pending open PR resolution scenarios are available from cached GitHub metadata/i); + expect(summary.pendingScenarioNotes.join(" ")).toMatch(/cached GitHub reviews, checks, and activity/i); + expect(summary.pendingScenarioNotes.join(" ")).toMatch(/merge-ready|Projected open PR count after pending cleanup: 2/i); + expect(summary.pendingPullRequests).toEqual( + expect.arrayContaining([ + expect.objectContaining({ pullNumber: 11, classification: "merge-ready pending resolution" }), + expect.objectContaining({ pullNumber: 12, classification: "stale open work likely to close" }), + expect.objectContaining({ pullNumber: 13, classification: "draft open PR" }), + expect.objectContaining({ pullNumber: 14, classification: "blocked open PR" }), + expect.objectContaining({ pullNumber: 15, classification: "maintainer-lane open PR" }), + expect.objectContaining({ pullNumber: 16, classification: "open PR" }), + ]), ); - const summary = renderPublicScenarioSummary(baseSummaryInput({ pressureSimulation: simulation })); - expect(summary.options).toHaveLength(3); - expect(summary.options[0]!.recommended).toBe(true); + expect(JSON.stringify(summary)).not.toMatch(FORBIDDEN_PUBLIC_LANGUAGE); }); - it("renders ranked options when queue signals are missing", () => { - const simulation = simulateOpenPrPressure(pressureInput({ queueHealth: null, contributorOpenPrCount: 0 })); - const summary = renderPublicScenarioSummary(baseSummaryInput({ pressureSimulation: simulation })); - expect(summary.options).toHaveLength(3); - expect(summary.headline).toMatch(/unavailable|conservative/i); - }); -}); - -// ── Snapshot: eligibility plan ───────────────────────────────────────────── - -describe("renderPublicScenarioSummary — eligibility plan", () => { - it("uses eligibility plan summary as headline when no pressure simulation is present", () => { - const result = previewResult({ - linkedIssueMode: "standard", - linkedIssueContext: { status: "validated", source: "official_mirror", issueNumbers: [42], solvedByPullRequests: [] }, - branchEligibility: { status: "eligible", source: "github_metadata" }, + it("renders user-supplied pending scenario assumptions without classified PR rows", () => { + const summary = renderPublicScenarioSummary({ + repoFullName: "octo/demo", + generatedAt: "2026-06-03T00:00:00.000Z", + pendingDetection: pendingDetection({ + source: "user_supplied", + pendingMergedPrCount: 2, + pendingClosedPrCount: 0, + approvedPrCount: 2, + expectedOpenPrCountAfterMerge: 1, + scenarioNotes: ["Caller assumes two approved PRs will land soon."], + classified: [], + }), }); - const plan = deriveEligibilityPlan(result); - const summary = renderPublicScenarioSummary(baseSummaryInput({ eligibilityPlan: plan })); - expect(summary.headline).toBe(plan.publicSummary); - expect(summary.eligibilityNotes).toContain(plan.publicSummary); - }); - it("surfaces eligibility blockers in eligibilityNotes", () => { - const result = previewResult({ - linkedIssueMode: "standard", - linkedIssueContext: { status: "invalid", source: "official_mirror", issueNumbers: [99] }, - branchEligibility: { status: "eligible", source: "github_metadata" }, - }); - const plan = deriveEligibilityPlan(result); - const summary = renderPublicScenarioSummary(baseSummaryInput({ eligibilityPlan: plan })); - const notesText = summary.eligibilityNotes.join(" "); - expect(notesText).toMatch(/invalid|no longer open|verify/i); - expect(notesText).not.toMatch(FORBIDDEN_PUBLIC_LANGUAGE); + expect(summary.headline).toMatch(/Pending open PR scenario assumptions were supplied/i); + expect(summary.pendingScenarioNotes.join(" ")).toMatch(/supplied by the caller as assumptions/i); + expect(summary.pendingScenarioNotes.join(" ")).toContain("Caller assumes two approved PRs will land soon."); + expect(summary.pendingPullRequests).toEqual([]); }); - it("includes cleanup paths in eligibilityNotes when present", () => { - const result = previewResult({ - linkedIssueMode: "standard", - linkedIssueContext: { status: "raw", source: "user_supplied", issueNumbers: [77] }, - branchEligibility: { status: "eligible", source: "github_metadata" }, + it("uses eligibility headline and linked-issue projection notes when pressure simulation is absent", () => { + const preview = buildScorePreview({ + repo, + snapshot, + input: { + repoFullName: "octo/demo", + sourceTokenScore: 60, + totalTokenScore: 80, + sourceLines: 50, + openPrCount: 1, + credibility: 1, + metadataOnly: true, + linkedIssueMode: "standard", + linkedIssueContext: { status: "raw", source: "user_supplied", issueNumbers: [77] }, + branchEligibility: { status: "eligible", source: "github_metadata" }, + }, }); - const plan = deriveEligibilityPlan(result); - const summary = renderPublicScenarioSummary(baseSummaryInput({ eligibilityPlan: plan })); - expect(plan.cleanupPaths.length).toBeGreaterThan(0); - expect(summary.eligibilityNotes.some((n) => n.match(/solved-by-PR|validate|evidence/i))).toBe(true); - }); - - it("includes linked issue projection in eligibilityNotes when available", () => { - const result = previewResult({ - linkedIssueMode: "standard", - linkedIssueContext: { status: "raw", source: "user_supplied", issueNumbers: [77] }, - branchEligibility: { status: "eligible", source: "github_metadata" }, + const eligibilityPlan = deriveEligibilityPlan(preview); + const summary = renderPublicScenarioSummary({ + repoFullName: "octo/demo", + generatedAt: "2026-06-03T00:00:00.000Z", + eligibilityPlan, }); - const plan = deriveEligibilityPlan(result); - if (plan.linkedIssueProjection) { - const summary = renderPublicScenarioSummary(baseSummaryInput({ eligibilityPlan: plan })); - expect(summary.eligibilityNotes.join(" ")).toMatch(/linked.issue|contribution consideration/i); - } - }); -}); - -// ── Snapshot: blocker notes ──────────────────────────────────────────────── - -describe("renderPublicScenarioSummary — blocker notes", () => { - it("surfaces open_pr_threshold and stale_work blockers as human-readable notes", () => { - const result = previewResult({ openPrCount: 5, observedStalePrCount: 2 }); - const summary = renderPublicScenarioSummary(baseSummaryInput({ publicBlockers: result.blockedBy })); - const notes = summary.blockerNotes.join(" "); - expect(notes).toMatch(/too many.*open PR|open PR.*exist|land|close/i); - expect(notes).toMatch(/stale/i); - }); - it("includes duplicate_risk note when the blocker is present", () => { - const result = previewResult({ duplicateRiskCount: 2 }); - const summary = renderPublicScenarioSummary(baseSummaryInput({ publicBlockers: result.blockedBy })); - expect(summary.blockerNotes.join(" ")).toMatch(/duplicate|conflicting/i); + expect(summary.headline).toBe(sanitizePublicComment(eligibilityPlan.publicSummary)); + expect(summary.eligibilityNotes.join(" ")).toMatch(/not yet validated|Validating the linked issue would enable/i); }); - it("excludes repo_not_registered and inactive_allocation from public blocker notes", () => { - const unregisteredRepo = { ...repo, isRegistered: false }; - const result = buildScorePreview({ repo: unregisteredRepo, snapshot, input: { repoFullName: "octo/demo", sourceTokenScore: 0 } }); - const summary = renderPublicScenarioSummary(baseSummaryInput({ publicBlockers: result.blockedBy })); - const notes = summary.blockerNotes.join(" "); - expect(notes).not.toMatch(/not registered|no active allocation/i); - }); - - it("blocker notes are empty when no blockers are present", () => { - const result = previewResult({ openPrCount: 1, credibility: 1 }); - const cleanBlockers = result.blockedBy.filter((b) => b.severity === "blocker"); - const summary = renderPublicScenarioSummary(baseSummaryInput({ publicBlockers: cleanBlockers })); - expect(summary.blockerNotes).toHaveLength(0); - }); -}); - -// ── Snapshot: data classification from scenario input ───────────────────── - -describe("renderPublicScenarioSummary — data classification", () => { - it("populates facts, assumptions, and unavailableSignals labels from a scenario input", () => { - const input = buildScenarioInput({ - scenarioType: "branch_preflight", + it("falls back to generic option next steps for unknown strategy options", () => { + const pressureSimulation = simulateOpenPrPressure({ repoFullName: "octo/demo", - facts: [ - createScenarioSignalEntry({ id: "queue", kind: "fact", label: "Queue signals", detail: "Two open PRs.", source: "github_observed" }), - ], - assumptions: [ - createScenarioSignalEntry({ id: "pending", kind: "assumption", label: "Pending merges", detail: "One approved PR.", source: "user_supplied" }), - ], - unavailableSignals: [ - createScenarioSignalEntry({ id: "stats", kind: "unavailable", label: "Official stats", detail: "Not available.", source: "missing" }), - ], + generatedAt: "2026-06-03T00:00:00.000Z", + queueHealth: queueHealth("low"), + roleContext: roleContext(), + contributorOpenPrCount: 0, }); - const summary = renderPublicScenarioSummary(baseSummaryInput({ scenarioInput: input })); - expect(summary.dataClassification.facts).toContain("Queue signals"); - expect(summary.dataClassification.assumptions).toContain("Pending merges"); - expect(summary.dataClassification.unavailableSignals).toContain("Official stats"); - }); - - it("does not include estimates bucket in the public data classification", () => { - const summary = renderPublicScenarioSummary(baseSummaryInput()); - expect(summary.dataClassification).not.toHaveProperty("estimates"); - }); -}); + pressureSimulation.scenarios[0] = { + ...pressureSimulation.scenarios[0]!, + option: "unexpected_option" as typeof pressureSimulation.scenarios[0]["option"], + }; -// ── Combined snapshot: pressure + eligibility + blockers ────────────────── - -describe("renderPublicScenarioSummary — combined inputs", () => { - it("combines pressure headline, options, eligibility notes, and blocker notes correctly", () => { - const simulation = simulateOpenPrPressure( - pressureInput({ queueHealth: queueHealth("high"), contributorOpenPrCount: 1 }), - ); - const result = previewResult({ - openPrCount: 1, - linkedIssueMode: "standard", - linkedIssueContext: { status: "raw", source: "user_supplied", issueNumbers: [5] }, - branchEligibility: { status: "eligible", source: "github_metadata" }, - observedStalePrCount: 1, - }); - const plan = deriveEligibilityPlan(result); const summary = renderPublicScenarioSummary({ repoFullName: "octo/demo", generatedAt: "2026-06-03T00:00:00.000Z", - pressureSimulation: simulation, - eligibilityPlan: plan, - publicBlockers: result.blockedBy, + pressureSimulation, }); - expect(summary.options.length).toBeGreaterThan(0); - expect(summary.eligibilityNotes.length).toBeGreaterThan(0); - expect(summary.blockerNotes.length).toBeGreaterThan(0); - expect(summary.headline).toBe(simulation.summary); + expect(summary.options[0]?.nextStep).toMatch(/Review available signals before acting/i); }); -}); -// ── Sanitizer fixtures: restricted terminology ──────────────────────────── - -describe("sanitizer fixtures for restricted terminology", () => { - it("all text fields across a full-signal summary pass the forbidden-language check", () => { - const simulation = simulateOpenPrPressure( - pressureInput({ queueHealth: queueHealth("high"), contributorOpenPrCount: 2 }), - ); - const result = previewResult({ - openPrCount: 2, - linkedIssueMode: "standard", - linkedIssueContext: { status: "raw", source: "user_supplied", issueNumbers: [7] }, - branchEligibility: { status: "eligible", source: "github_metadata" }, - observedStalePrCount: 1, - duplicateRiskCount: 1, + it("omits linked-issue projection notes when the eligibility plan has no projection", () => { + const preview = buildScorePreview({ + repo, + snapshot, + input: { + repoFullName: "octo/demo", + sourceTokenScore: 60, + totalTokenScore: 80, + sourceLines: 50, + openPrCount: 0, + credibility: 1, + metadataOnly: true, + linkedIssueMode: "none", + }, }); - const plan = deriveEligibilityPlan(result); + const eligibilityPlan = deriveEligibilityPlan(preview); const summary = renderPublicScenarioSummary({ repoFullName: "octo/demo", generatedAt: "2026-06-03T00:00:00.000Z", - pressureSimulation: simulation, - eligibilityPlan: plan, - publicBlockers: result.blockedBy, + eligibilityPlan, }); - const allText = JSON.stringify(summary); - expect(allText).not.toMatch(FORBIDDEN_PUBLIC_LANGUAGE); - }); - - it("all text fields are unchanged by a second pass of sanitizePublicComment", () => { - const simulation = simulateOpenPrPressure(pressureInput()); - const summary = renderPublicScenarioSummary(baseSummaryInput({ pressureSimulation: simulation })); - const textFields = [ - summary.headline, - ...summary.options.flatMap((o) => [o.label, o.rationale, o.nextStep, ...o.obstacles, ...o.assumptions]), - ...summary.eligibilityNotes, - ...summary.blockerNotes, - ]; - for (const field of textFields) { - expect(field).toBe(sanitizePublicComment(field)); - } + expect(eligibilityPlan.linkedIssueProjection).toBeNull(); + expect(summary.eligibilityNotes.join(" ")).not.toMatch(/Validating the linked issue would enable/i); }); - it("does not expose score, reward, or private context in any fixture variant", () => { - const fixtures: ScenarioSummaryInput[] = [ - baseSummaryInput(), - baseSummaryInput({ pressureSimulation: simulateOpenPrPressure(pressureInput({ queueHealth: queueHealth("critical"), contributorOpenPrCount: 3 })) }), - baseSummaryInput({ pressureSimulation: simulateOpenPrPressure(pressureInput({ queueHealth: null })) }), - baseSummaryInput({ eligibilityPlan: deriveEligibilityPlan(previewResult({ linkedIssueMode: "none" })) }), - baseSummaryInput({ publicBlockers: previewResult({ observedStalePrCount: 2, duplicateRiskCount: 1 }).blockedBy }), - ]; - for (const fixture of fixtures) { - const summary = renderPublicScenarioSummary(fixture); - expect(JSON.stringify(summary)).not.toMatch(/\bscore\b|reward|earn|payout|hotkey|wallet|trust score|scoreability/i); - } - }); - - it("makes no claim about autonomous PR filing, issue creation, or merge actions", () => { - const simulation = simulateOpenPrPressure(pressureInput({ queueHealth: queueHealth("medium"), contributorOpenPrCount: 1 })); - const summary = renderPublicScenarioSummary(baseSummaryInput({ pressureSimulation: simulation })); - const allText = JSON.stringify(summary); - expect(allText).not.toMatch(/will open|will merge|will close|will file|automatically|autonomously/i); - }); -}); - -// ── Advisory-only invariants ─────────────────────────────────────────────── + it("omits projected open-count notes when pending detection has no after-cleanup projection", () => { + const summary = renderPublicScenarioSummary({ + repoFullName: "octo/demo", + generatedAt: "2026-06-03T00:00:00.000Z", + pendingDetection: pendingDetection({ + expectedOpenPrCountAfterMerge: undefined, + classified: [ + { + repoFullName: "octo/demo", + number: 99, + title: "Unknown classification", + classification: "custom_pending_class" as PendingPrScenarioDetection["classified"][number]["classification"], + reasons: ["Custom pending classification reason."], + }, + ], + }), + }); -describe("advisory-only invariants", () => { - it("advisory flags are present and set to true in all fixture variants", () => { - const fixtures: ScenarioSummaryInput[] = [ - baseSummaryInput(), - baseSummaryInput({ pressureSimulation: simulateOpenPrPressure(pressureInput()) }), - baseSummaryInput({ eligibilityPlan: deriveEligibilityPlan(previewResult()) }), - ]; - for (const fixture of fixtures) { - const summary = renderPublicScenarioSummary(fixture); - expect(summary.advisoryOnly).toBe(true); - expect(summary.notAutonomousPrBot).toBe(true); - expect(summary.notPublicScoring).toBe(true); - } + expect(summary.pendingScenarioNotes.join(" ")).not.toMatch(/Projected open PR count after pending cleanup/i); + expect(summary.pendingPullRequests[0]?.classification).toBe("custom pending class"); }); }); From 5349657c72d715b8a47f060f8d387ecc06f97a24 Mon Sep 17 00:00:00 2001 From: Clayton Date: Mon, 8 Jun 2026 18:14:50 -0500 Subject: [PATCH 3/6] fix: update --- src/api/routes.ts | 9 + src/mcp/server.ts | 30 +++ src/openapi/schemas.ts | 51 ++++ src/openapi/spec.ts | 12 + .../contributor-repo-scenario-summary.ts | 246 ++++++++++++++++++ test/integration/api.test.ts | 30 +++ .../contributor-repo-scenario-summary.test.ts | 211 +++++++++++++++ test/unit/mcp-output-schemas.test.ts | 1 + 8 files changed, 590 insertions(+) create mode 100644 src/services/contributor-repo-scenario-summary.ts create mode 100644 test/unit/contributor-repo-scenario-summary.test.ts diff --git a/src/api/routes.ts b/src/api/routes.ts index 0d2458d3a2..832e5b13de 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -178,6 +178,7 @@ import { } from "../signals/engine"; import { attachDataQuality, buildCoreSignalFidelity, buildFreshnessSloReport, buildRepoDataQuality, buildSignalFidelity } from "../signals/data-quality"; import { buildContributorOpenPrMonitor } from "../signals/contributor-open-pr-monitor"; +import { buildContributorRepoScenarioSummary } from "../services/contributor-repo-scenario-summary"; import { buildPullRequestReviewability, type PullRequestReviewability } from "../signals/reward-risk"; import { buildLocalBranchAnalysis, findCurrentBranchPullRequest } from "../signals/local-branch"; import { MAX_LOCAL_SCORER_WARNING_CHARS, MAX_LOCAL_SCORER_WARNING_COUNT } from "../signals/local-scorer-diagnostics"; @@ -1672,6 +1673,14 @@ export function createApp() { }); }); + app.get("/v1/contributors/:login/repos/:owner/:repo/scenario-summary", async (c) => { + const login = c.req.param("login"); + const unauthorized = await requireContributorAccess(c, login); + if (unauthorized) return unauthorized; + const fullName = `${c.req.param("owner")}/${c.req.param("repo")}`; + return c.json(await buildContributorRepoScenarioSummary(c.env, login, fullName)); + }); + app.post("/v1/preflight/pr", async (c) => { const body = await c.req.json().catch(() => null); const parsed = preflightSchema.safeParse(body); diff --git a/src/mcp/server.ts b/src/mcp/server.ts index 0a4543c9b4..b3edcec023 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -72,6 +72,7 @@ import { buildRegistryChangeReport, buildRoleContext, } from "../signals/engine"; +import { buildContributorRepoScenarioSummary } from "../services/contributor-repo-scenario-summary"; import { buildContributorOpenPrMonitor } from "../signals/contributor-open-pr-monitor"; import { buildLocalBranchAnalysis, findCurrentBranchPullRequest } from "../signals/local-branch"; import { loadRepoFocusManifest } from "../signals/focus-manifest-loader"; @@ -337,6 +338,14 @@ const explainRepoDecisionOutputSchema = { dataQuality: z.unknown().optional(), }; +const scenarioSummaryOutputSchema = { + status: z.string().optional(), + login: z.string().optional(), + repoFullName: z.string().optional(), + generatedAt: z.string().optional(), + summary: z.unknown().optional(), +}; + const registryChangesOutputSchema = { generatedAt: z.string().optional(), currentSnapshotId: z.string().optional(), @@ -505,6 +514,17 @@ export class GittensoryMcp { async (input) => this.toolResult(await this.explainRepoDecision(input)), ); + server.registerTool( + "gittensory_get_scenario_summary", + { + description: + "Return a public-safe contributor/repo scenario summary with ranked strategy options, eligibility notes, and pending PR context from cached metadata.", + inputSchema: loginRepoShape, + outputSchema: scenarioSummaryOutputSchema, + }, + async (input) => this.toolResult(await this.getScenarioSummary(input)), + ); + server.registerTool( "gittensory_preflight_pr", { @@ -976,6 +996,16 @@ export class GittensoryMcp { }; } + private async getScenarioSummary(input: { login: string; owner: string; repo: string }): Promise { + this.requireContributorAccess(input.login); + const fullName = `${input.owner}/${input.repo}`; + const response = await buildContributorRepoScenarioSummary(this.env, input.login, fullName); + return { + summary: response.summary.headline, + data: response as unknown as Record, + }; + } + private async getRegistryChanges(): Promise { const report = buildRegistryChangeReport(await listLatestRegistrySnapshots(this.env, 2)); return { diff --git a/src/openapi/schemas.ts b/src/openapi/schemas.ts index ee748ab123..512083b65a 100644 --- a/src/openapi/schemas.ts +++ b/src/openapi/schemas.ts @@ -336,6 +336,57 @@ export const ContributorOpenPrMonitorSchema = z }) .openapi("ContributorOpenPrMonitor"); +export const RenderedScenarioOptionSchema = z + .object({ + rank: z.number(), + label: z.string(), + rationale: z.string(), + obstacles: z.array(z.string()), + assumptions: z.array(z.string()), + nextStep: z.string(), + recommended: z.boolean(), + }) + .openapi("RenderedScenarioOption"); + +export const RenderedPendingPullRequestSchema = z + .object({ + pullNumber: z.number(), + classification: z.string(), + notes: z.array(z.string()), + }) + .openapi("RenderedPendingPullRequest"); + +export const PublicScenarioSummarySchema = z + .object({ + repoFullName: z.string(), + generatedAt: z.string(), + advisoryOnly: z.literal(true), + notAutonomousPrBot: z.literal(true), + notPublicScoring: z.literal(true), + headline: z.string(), + options: z.array(RenderedScenarioOptionSchema), + eligibilityNotes: z.array(z.string()), + blockerNotes: z.array(z.string()), + pendingScenarioNotes: z.array(z.string()), + pendingPullRequests: z.array(RenderedPendingPullRequestSchema), + dataClassification: z.object({ + facts: z.array(z.string()), + assumptions: z.array(z.string()), + unavailableSignals: z.array(z.string()), + }), + }) + .openapi("PublicScenarioSummary"); + +export const ContributorRepoScenarioSummaryResponseSchema = z + .object({ + status: z.literal("ready"), + login: z.string(), + repoFullName: z.string(), + generatedAt: z.string(), + summary: PublicScenarioSummarySchema, + }) + .openapi("ContributorRepoScenarioSummaryResponse"); + export const ContributorOpportunitySchema = z .object({ repoFullName: z.string(), diff --git a/src/openapi/spec.ts b/src/openapi/spec.ts index b173894d37..fc787dcb10 100644 --- a/src/openapi/spec.ts +++ b/src/openapi/spec.ts @@ -22,6 +22,7 @@ import { ContributorPatternReportSchema, ContributorDecisionPackSchema, ContributorOpenPrMonitorSchema, + ContributorRepoScenarioSummaryResponseSchema, ContributorRewardRiskStrategySchema, ContributorProfileSchema, ContributorScoringProfileSchema, @@ -43,6 +44,7 @@ import { PullRequestMaintainerPacketSchema, PullRequestReviewIntelligenceSchema, PullRequestReviewabilitySchema, + PublicScenarioSummarySchema, PreflightResultSchema, QueueHealthSchema, ReadinessSchema, @@ -447,6 +449,16 @@ export function buildOpenApiSpec() { 202: { description: "Decision pack snapshot is missing; a background rebuild has been requested", content: { "application/json": { schema: DecisionPackRefreshNeededSchema } } }, }, }); + registry.registerPath({ + method: "get", + path: "/v1/contributors/{login}/repos/{owner}/{repo}/scenario-summary", + responses: { + 200: { + description: "Public-safe contributor/repo scenario summary from cached metadata and advisory simulators.", + content: { "application/json": { schema: ContributorRepoScenarioSummaryResponseSchema } }, + }, + }, + }); registry.registerPath({ method: "post", path: "/v1/preflight/pr", diff --git a/src/services/contributor-repo-scenario-summary.ts b/src/services/contributor-repo-scenario-summary.ts new file mode 100644 index 0000000000..44174e86c9 --- /dev/null +++ b/src/services/contributor-repo-scenario-summary.ts @@ -0,0 +1,246 @@ +import { + getRepository, + listContributorIssues, + listContributorPullRequests, + listContributorRepoStats, + listIssues, + listPullRequests, + listRecentMergedPullRequests, + listRepositories, +} from "../db/repositories"; +import { contributorRepoStatsFromGittensor, fetchGittensorContributorSnapshot } from "../gittensor/api"; +import { fetchPublicContributorProfile } from "../github/public"; +import { buildScenarioInput, createScenarioSignalEntry } from "../scenarios/input-model"; +import { renderPublicScenarioSummary, type PublicScenarioSummary } from "../scenarios/scenario-summary"; +import { buildScorePreview } from "../scoring/preview"; +import { detectPendingPrScenario, loadContributorRepoOpenPrSignals } from "../scoring/pending-pr-scenarios"; +import { deriveEligibilityPlan } from "./eligibility-plan"; +import { simulateOpenPrPressure } from "./open-pr-pressure-scenarios"; +import { getOrCreateScoringModelSnapshot } from "../scoring/model"; +import { + buildCollisionReport, + buildContributorOutcomeHistory, + buildContributorProfile, + buildLaneAdvice, + buildQueueHealth, + buildRoleContext, + type ContributorOutcomeHistory, + type ContributorProfile, +} from "../signals/engine"; +import type { PullRequestRecord, RepositoryRecord } from "../types"; +import { nowIso } from "../utils/json"; + +export type ContributorRepoScenarioSummaryResponse = { + status: "ready"; + login: string; + repoFullName: string; + generatedAt: string; + summary: PublicScenarioSummary; +}; + +export type ContributorRepoScenarioSummaryContext = { + login: string; + repoFullName: string; + repo: RepositoryRecord | null; + profile: ContributorProfile; + outcomeHistory: ContributorOutcomeHistory; + contributorPullRequests: PullRequestRecord[]; +}; + +function authoritativeContributorRepoStats( + gittensorSnapshot: Awaited>, + cachedRepoStats: Awaited>, +) { + const officialRepoStats = contributorRepoStatsFromGittensor(gittensorSnapshot); + return officialRepoStats.length > 0 ? officialRepoStats : cachedRepoStats; +} + +function sameLogin(value: string | null | undefined, login: string): boolean { + return Boolean(value && value.toLowerCase() === login.toLowerCase()); +} + +function contributorOpenPrsOnRepo(login: string, repoFullName: string, pullRequests: PullRequestRecord[]): PullRequestRecord[] { + return pullRequests.filter( + (pr) => pr.state === "open" && sameLogin(pr.authorLogin, login) && pr.repoFullName.toLowerCase() === repoFullName.toLowerCase(), + ); +} + +function metadataPreviewTokenEstimates(outcome: ContributorOutcomeHistory["repoOutcomes"][number] | undefined) { + const merged = outcome?.mergedPullRequests ?? 0; + const sourceTokenScore = Math.min(120, Math.max(30, 42 + merged * 2)); + const totalTokenScore = Math.min(220, Math.max(60, 70 + merged * 4)); + return { + sourceTokenScore, + totalTokenScore, + sourceLines: Math.max(12, sourceTokenScore), + }; +} + +function bestFitLabels(repo: RepositoryRecord | null): string[] { + const multipliers = repo?.registryConfig?.labelMultipliers ?? {}; + const labels = Object.entries(multipliers) + .filter(([label]) => !/status|source|contributor|verified|risk|codex/i.test(label)) + .sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0])) + .map(([label]) => label); + return labels.slice(0, 1); +} + +export function buildContributorRepoScenarioSummaryFromContext( + context: ContributorRepoScenarioSummaryContext & { + issues: Awaited>; + pullRequests: Awaited>; + recentMergedPullRequests: Awaited>; + scoringSnapshot: Awaited>; + pendingDetection: ReturnType; + generatedAt?: string | undefined; + }, +): ContributorRepoScenarioSummaryResponse { + const generatedAt = context.generatedAt ?? nowIso(); + const { login, repoFullName, repo, profile, outcomeHistory, contributorPullRequests, issues, pullRequests, recentMergedPullRequests, scoringSnapshot, pendingDetection } = + context; + const repoOutcome = outcomeHistory.repoOutcomes.find((entry) => entry.repoFullName.toLowerCase() === repoFullName.toLowerCase()); + const roleContext = buildRoleContext({ + login, + repo, + repoFullName, + pullRequests, + issues, + profile, + }); + const lane = buildLaneAdvice(repo, repoFullName); + const collisions = buildCollisionReport(repoFullName, issues, pullRequests, recentMergedPullRequests); + const queueHealth = buildQueueHealth(repo, issues, pullRequests, collisions); + const contributorOpenPrCount = contributorOpenPrsOnRepo(login, repoFullName, contributorPullRequests).length; + const tokenEstimates = metadataPreviewTokenEstimates(repoOutcome); + const credibility = repoOutcome?.credibility && repoOutcome.credibility > 0 ? repoOutcome.credibility : outcomeHistory.totals.credibility ?? 0.8; + const preview = buildScorePreview({ + input: { + repoFullName, + targetType: "planned_pr", + targetKey: `${login}:${repoFullName}:scenario-summary`, + contributorLogin: login, + labels: bestFitLabels(repo), + linkedIssueMode: lane.lane === "issue_discovery" ? "none" : "standard", + ...tokenEstimates, + existingContributorTokenScore: 0, + credibility, + metadataOnly: true, + duplicateRiskCount: collisions.summary.highRiskCount, + openPrCount: contributorOpenPrCount, + }, + repo, + snapshot: scoringSnapshot, + }); + const pressureSimulation = simulateOpenPrPressure({ + repoFullName, + generatedAt, + queueHealth, + roleContext, + contributorOpenPrCount, + }); + const eligibilityPlan = deriveEligibilityPlan(preview); + const scenarioInput = buildScenarioInput({ + scenarioType: "general_repo", + repoFullName, + registered: Boolean(repo?.isRegistered), + maintainerLane: roleContext.role !== "outside_contributor", + facts: [ + createScenarioSignalEntry({ + id: "queue", + kind: "fact", + label: "Queue pressure", + detail: queueHealth.summary, + source: "github_observed", + }), + createScenarioSignalEntry({ + id: "open_prs", + kind: "fact", + label: "Open PRs", + detail: `Contributor has ${contributorOpenPrCount} open PR(s) on ${repoFullName}.`, + source: "github_observed", + }), + ], + assumptions: pendingDetection + ? pendingDetection.scenarioNotes.slice(0, 3).map((note, index) => + createScenarioSignalEntry({ + id: `pending-${index + 1}`, + kind: "assumption", + label: "Pending PR scenario", + detail: note, + source: pendingDetection.source === "user_supplied" ? "user_supplied" : "github_observed", + }), + ) + : [], + unavailableSignals: [], + }); + const summary = renderPublicScenarioSummary({ + repoFullName, + generatedAt, + pressureSimulation, + eligibilityPlan, + pendingDetection: pendingDetection ?? undefined, + publicBlockers: preview.blockedBy, + scenarioInput, + }); + return { status: "ready", login, repoFullName, generatedAt, summary }; +} + +export async function buildContributorRepoScenarioSummary(env: Env, login: string, repoFullName: string): Promise { + const [github, contributorPullRequests, contributorIssues, repositories, cachedRepoStats, gittensorSnapshot, repo, issues, pullRequests, recentMergedPullRequests, scoringSnapshot] = + await Promise.all([ + fetchPublicContributorProfile(login), + listContributorPullRequests(env, login), + listContributorIssues(env, login), + listRepositories(env), + listContributorRepoStats(env, login), + fetchGittensorContributorSnapshot(login), + getRepository(env, repoFullName), + listIssues(env, repoFullName), + listPullRequests(env, repoFullName), + listRecentMergedPullRequests(env, repoFullName), + getOrCreateScoringModelSnapshot(env), + ]); + const repoStats = authoritativeContributorRepoStats(gittensorSnapshot, cachedRepoStats); + const profile = buildContributorProfile(login, github, contributorPullRequests, contributorIssues, repoStats, gittensorSnapshot); + const outcomeHistory = buildContributorOutcomeHistory({ + login, + profile, + repositories, + pullRequests: contributorPullRequests, + issues: contributorIssues, + repoStats, + cachedRepoStats, + }); + const repoOpen = contributorOpenPrsOnRepo(login, repoFullName, contributorPullRequests); + const signals = await loadContributorRepoOpenPrSignals(env, repoFullName, repoOpen); + const roleContext = buildRoleContext({ + login, + repo, + repoFullName, + pullRequests, + issues, + profile, + }); + const pendingDetection = detectPendingPrScenario({ + login, + repoFullName, + pullRequests, + roleContext, + openPrCount: repoOpen.length, + reviewsByPullNumber: signals.reviewsByPullNumber, + checksByPullNumber: signals.checksByPullNumber, + }); + return buildContributorRepoScenarioSummaryFromContext({ + login, + repoFullName, + repo, + profile, + outcomeHistory, + contributorPullRequests, + issues, + pullRequests, + recentMergedPullRequests, + scoringSnapshot, + pendingDetection, + }); +} diff --git a/test/integration/api.test.ts b/test/integration/api.test.ts index 7618fd9f98..323bfeac3d 100644 --- a/test/integration/api.test.ts +++ b/test/integration/api.test.ts @@ -668,6 +668,34 @@ describe("api routes", () => { }, }); + const scenarioSummary = await app.request("/v1/contributors/oktofeesh1/repos/entrius/allways-ui/scenario-summary", { headers: apiHeaders(env) }, env); + expect(scenarioSummary.status).toBe(200); + const scenarioSummaryPayload = (await scenarioSummary.json()) as { + status: string; + login: string; + repoFullName: string; + summary: { + advisoryOnly: boolean; + headline: string; + options: Array<{ rank: number; recommended: boolean; nextStep: string }>; + pendingScenarioNotes: string[]; + pendingPullRequests: Array<{ pullNumber: number; classification: string }>; + }; + }; + expect(scenarioSummaryPayload).toMatchObject({ + status: "ready", + login: "oktofeesh1", + repoFullName: "entrius/allways-ui", + summary: { + advisoryOnly: true, + headline: expect.any(String), + options: expect.any(Array), + }, + }); + expect(scenarioSummaryPayload.summary.options.length).toBe(3); + expect(scenarioSummaryPayload.summary.options.some((option) => option.recommended)).toBe(true); + expect(JSON.stringify(scenarioSummaryPayload.summary)).not.toMatch(/wallet|hotkey|reward estimate|payout|farming|raw trust score|scoreability/i); + const agentPlan = await app.request( "/v1/agent/plan-next-work", { @@ -3888,6 +3916,7 @@ describe("api routes", () => { expect(toolNames).toContain("gittensory_get_contributor_profile"); expect(toolNames).toContain("gittensory_get_decision_pack"); expect(toolNames).toContain("gittensory_explain_repo_decision"); + expect(toolNames).toContain("gittensory_get_scenario_summary"); expect(toolNames).toContain("gittensory_preflight_pr"); expect(toolNames).toContain("gittensory_preflight_local_diff"); expect(toolNames).toContain("gittensory_preview_local_pr_score"); @@ -4126,6 +4155,7 @@ describe("api routes", () => { ["gittensory_get_contributor_profile", { login: "oktofeesh1" }], ["gittensory_get_decision_pack", { login: "oktofeesh1" }], ["gittensory_explain_repo_decision", { login: "oktofeesh1", owner: "entrius", repo: "allways-ui" }], + ["gittensory_get_scenario_summary", { login: "oktofeesh1", owner: "entrius", repo: "allways-ui" }], ["gittensory_agent_plan_next_work", { login: "oktofeesh1", repoFullName: "entrius/allways-ui" }], [ "gittensory_preflight_pr", diff --git a/test/unit/contributor-repo-scenario-summary.test.ts b/test/unit/contributor-repo-scenario-summary.test.ts new file mode 100644 index 0000000000..c844c2c25d --- /dev/null +++ b/test/unit/contributor-repo-scenario-summary.test.ts @@ -0,0 +1,211 @@ +import { describe, expect, it } from "vitest"; +import { + buildContributorRepoScenarioSummaryFromContext, + type ContributorRepoScenarioSummaryContext, +} from "../../src/services/contributor-repo-scenario-summary"; +import type { PendingPrScenarioDetection } from "../../src/scoring/pending-pr-scenarios"; +import type { ContributorOutcomeHistory, ContributorProfile } from "../../src/signals/engine"; +import type { PullRequestRecord, ScoringModelSnapshotRecord } from "../../src/types"; + +const FORBIDDEN_PUBLIC_LANGUAGE = + /wallet|hotkey|coldkey|mnemonic|seed phrase|payout|reward[-\s]?estimate|farming|raw trust|trust[-\s]?score|scoreability|private[-\s]?reviewability|public[-\s]?score[-\s]?(?:estimate|prediction)/i; + +const snapshot: ScoringModelSnapshotRecord = { + id: "contributor-scenario-summary-model", + sourceKind: "test", + sourceUrl: "fixture://constants.py", + fetchedAt: "2026-06-03T00:00:00.000Z", + activeModel: "current_density_model", + constants: { + OSS_EMISSION_SHARE: 0.9, + MERGED_PR_BASE_SCORE: 25, + MIN_TOKEN_SCORE_FOR_BASE_SCORE: 5, + MAX_CODE_DENSITY_MULTIPLIER: 1.15, + MAX_CONTRIBUTION_BONUS: 25, + CONTRIBUTION_SCORE_FOR_FULL_BONUS: 1500, + STANDARD_ISSUE_MULTIPLIER: 1.33, + MAINTAINER_ISSUE_MULTIPLIER: 1.66, + MIN_CREDIBILITY: 0.8, + REVIEW_PENALTY_RATE: 0.15, + EXCESSIVE_PR_PENALTY_BASE_THRESHOLD: 2, + OPEN_PR_THRESHOLD_TOKEN_SCORE: 300, + MAX_OPEN_PR_THRESHOLD: 30, + OPEN_PR_COLLATERAL_PERCENT: 0.2, + SRC_TOK_SATURATION_SCALE: 58, + }, + programmingLanguages: {}, + registrySnapshotId: "registry-fixture", + warnings: [], + payload: {}, +}; + +const repo = { + fullName: "octo/demo", + owner: "octo", + name: "demo", + isInstalled: true, + isRegistered: true, + isPrivate: false, + registryConfig: { repo: "octo/demo", emissionShare: 0.02, issueDiscoveryShare: 0, labelMultipliers: { bug: 1.1 }, maintainerCut: 0, raw: {} }, +}; + +const profile: ContributorProfile = { + login: "miner-a", + generatedAt: "2026-06-03T00:00:00.000Z", + github: { login: "miner-a", topLanguages: ["TypeScript"], source: "github" }, + source: "github_cache", + registeredRepoActivity: { + pullRequests: 2, + mergedPullRequests: 1, + issues: 0, + reposTouched: [repo.fullName], + dominantLabels: ["bug"], + }, + trustSignals: { + evidenceScore: 80, + level: "emerging", + unlinkedOpenPullRequests: 0, + maintainerAssociatedPullRequests: 0, + }, +}; + +const outcomeHistory: ContributorOutcomeHistory = { + login: "miner-a", + generatedAt: "2026-06-03T00:00:00.000Z", + source: "github_cache", + totals: { + pullRequests: 2, + mergedPullRequests: 1, + openPullRequests: 1, + closedPullRequests: 0, + closedPullRequestRate: 0, + issues: 0, + openIssues: 0, + closedIssues: 0, + solvedIssues: 0, + validSolvedIssues: 0, + credibility: 0.9, + issueCredibility: 1, + }, + repoOutcomes: [ + { + repoFullName: "octo/demo", + role: "outside_contributor", + lane: "direct_pr", + maintainerLane: false, + pullRequests: 2, + mergedPullRequests: 1, + openPullRequests: 1, + closedPullRequests: 0, + closedPullRequestRate: 0, + issues: 0, + openIssues: 0, + closedIssues: 0, + solvedIssues: 0, + validSolvedIssues: 0, + credibility: 0.9, + issueCredibility: 1, + isEligible: true, + successLevel: "emerging", + strengths: ["Merged prior PRs."], + risks: [], + }, + ], + successPatterns: [], + failurePatterns: [], + summary: "fixture history", +}; + +const contributorPullRequests: PullRequestRecord[] = [ + { + repoFullName: "octo/demo", + number: 11, + title: "Ready cleanup", + state: "open", + authorLogin: "miner-a", + authorAssociation: "NONE", + labels: [], + linkedIssues: [], + updatedAt: "2026-05-20T00:00:00.000Z", + }, +]; + +const pendingDetection: PendingPrScenarioDetection = { + source: "github_observed", + pendingMergedPrCount: 1, + pendingClosedPrCount: 0, + approvedPrCount: 1, + expectedOpenPrCountAfterMerge: 0, + scenarioNotes: ["1 open PR(s) look merge-ready (approved, no changes requested, no failing checks, not draft/stale)."], + classified: [ + { + repoFullName: "octo/demo", + number: 11, + title: "Ready cleanup", + classification: "merge_ready", + reasons: ["Approved review in cache."], + }, + ], +}; + +type BuildContext = ContributorRepoScenarioSummaryContext & { + issues: []; + pullRequests: PullRequestRecord[]; + recentMergedPullRequests: []; + scoringSnapshot: ScoringModelSnapshotRecord; + pendingDetection: PendingPrScenarioDetection | null; + generatedAt?: string; +}; + +function baseContext(): BuildContext { + return { + login: "miner-a", + repoFullName: "octo/demo", + repo, + profile, + outcomeHistory, + contributorPullRequests, + issues: [], + pullRequests: contributorPullRequests, + recentMergedPullRequests: [], + scoringSnapshot: snapshot, + pendingDetection, + generatedAt: "2026-06-03T00:00:00.000Z", + }; +} + +describe("buildContributorRepoScenarioSummaryFromContext", () => { + it("composes pressure, eligibility, pending, and public-safe summary fields", () => { + const response = buildContributorRepoScenarioSummaryFromContext(baseContext()); + expect(response).toMatchObject({ + status: "ready", + login: "miner-a", + repoFullName: "octo/demo", + generatedAt: "2026-06-03T00:00:00.000Z", + summary: { + advisoryOnly: true, + notAutonomousPrBot: true, + notPublicScoring: true, + repoFullName: "octo/demo", + }, + }); + expect(response.summary.options.length).toBe(3); + expect(response.summary.eligibilityNotes.length).toBeGreaterThan(0); + expect(response.summary.pendingScenarioNotes.length).toBeGreaterThan(0); + expect(response.summary.pendingPullRequests).toEqual([ + expect.objectContaining({ pullNumber: 11, classification: expect.stringMatching(/merge-ready/i) }), + ]); + expect(response.summary.dataClassification.facts.join(" ")).toMatch(/Queue pressure|Open PRs/i); + expect(JSON.stringify(response.summary)).not.toMatch(FORBIDDEN_PUBLIC_LANGUAGE); + }); + + it("still returns ranked options when pending detection is absent", () => { + const response = buildContributorRepoScenarioSummaryFromContext({ + ...baseContext(), + pendingDetection: null, + }); + expect(response.summary.options.length).toBe(3); + expect(response.summary.pendingScenarioNotes).toEqual([]); + expect(response.summary.pendingPullRequests).toEqual([]); + }); +}); diff --git a/test/unit/mcp-output-schemas.test.ts b/test/unit/mcp-output-schemas.test.ts index bd9bc65dcc..f914bdd6fa 100644 --- a/test/unit/mcp-output-schemas.test.ts +++ b/test/unit/mcp-output-schemas.test.ts @@ -17,6 +17,7 @@ const TOOLS_WITH_OUTPUT_SCHEMA = [ "gittensory_get_decision_pack", "gittensory_monitor_open_prs", "gittensory_explain_repo_decision", + "gittensory_get_scenario_summary", "gittensory_get_issue_quality", "gittensory_get_registry_changes", "gittensory_get_upstream_drift", From 602ead3009ec5c26dd8ad939d4a2b9f313dabc6e Mon Sep 17 00:00:00 2001 From: Clayton Date: Mon, 8 Jun 2026 19:25:42 -0500 Subject: [PATCH 4/6] Revert "fix: update" This reverts commit 5349657c72d715b8a47f060f8d387ecc06f97a24. --- src/api/routes.ts | 9 - src/mcp/server.ts | 30 --- src/openapi/schemas.ts | 51 ---- src/openapi/spec.ts | 12 - .../contributor-repo-scenario-summary.ts | 246 ------------------ test/integration/api.test.ts | 30 --- .../contributor-repo-scenario-summary.test.ts | 211 --------------- test/unit/mcp-output-schemas.test.ts | 1 - 8 files changed, 590 deletions(-) delete mode 100644 src/services/contributor-repo-scenario-summary.ts delete mode 100644 test/unit/contributor-repo-scenario-summary.test.ts diff --git a/src/api/routes.ts b/src/api/routes.ts index 832e5b13de..0d2458d3a2 100644 --- a/src/api/routes.ts +++ b/src/api/routes.ts @@ -178,7 +178,6 @@ import { } from "../signals/engine"; import { attachDataQuality, buildCoreSignalFidelity, buildFreshnessSloReport, buildRepoDataQuality, buildSignalFidelity } from "../signals/data-quality"; import { buildContributorOpenPrMonitor } from "../signals/contributor-open-pr-monitor"; -import { buildContributorRepoScenarioSummary } from "../services/contributor-repo-scenario-summary"; import { buildPullRequestReviewability, type PullRequestReviewability } from "../signals/reward-risk"; import { buildLocalBranchAnalysis, findCurrentBranchPullRequest } from "../signals/local-branch"; import { MAX_LOCAL_SCORER_WARNING_CHARS, MAX_LOCAL_SCORER_WARNING_COUNT } from "../signals/local-scorer-diagnostics"; @@ -1673,14 +1672,6 @@ export function createApp() { }); }); - app.get("/v1/contributors/:login/repos/:owner/:repo/scenario-summary", async (c) => { - const login = c.req.param("login"); - const unauthorized = await requireContributorAccess(c, login); - if (unauthorized) return unauthorized; - const fullName = `${c.req.param("owner")}/${c.req.param("repo")}`; - return c.json(await buildContributorRepoScenarioSummary(c.env, login, fullName)); - }); - app.post("/v1/preflight/pr", async (c) => { const body = await c.req.json().catch(() => null); const parsed = preflightSchema.safeParse(body); diff --git a/src/mcp/server.ts b/src/mcp/server.ts index b3edcec023..0a4543c9b4 100644 --- a/src/mcp/server.ts +++ b/src/mcp/server.ts @@ -72,7 +72,6 @@ import { buildRegistryChangeReport, buildRoleContext, } from "../signals/engine"; -import { buildContributorRepoScenarioSummary } from "../services/contributor-repo-scenario-summary"; import { buildContributorOpenPrMonitor } from "../signals/contributor-open-pr-monitor"; import { buildLocalBranchAnalysis, findCurrentBranchPullRequest } from "../signals/local-branch"; import { loadRepoFocusManifest } from "../signals/focus-manifest-loader"; @@ -338,14 +337,6 @@ const explainRepoDecisionOutputSchema = { dataQuality: z.unknown().optional(), }; -const scenarioSummaryOutputSchema = { - status: z.string().optional(), - login: z.string().optional(), - repoFullName: z.string().optional(), - generatedAt: z.string().optional(), - summary: z.unknown().optional(), -}; - const registryChangesOutputSchema = { generatedAt: z.string().optional(), currentSnapshotId: z.string().optional(), @@ -514,17 +505,6 @@ export class GittensoryMcp { async (input) => this.toolResult(await this.explainRepoDecision(input)), ); - server.registerTool( - "gittensory_get_scenario_summary", - { - description: - "Return a public-safe contributor/repo scenario summary with ranked strategy options, eligibility notes, and pending PR context from cached metadata.", - inputSchema: loginRepoShape, - outputSchema: scenarioSummaryOutputSchema, - }, - async (input) => this.toolResult(await this.getScenarioSummary(input)), - ); - server.registerTool( "gittensory_preflight_pr", { @@ -996,16 +976,6 @@ export class GittensoryMcp { }; } - private async getScenarioSummary(input: { login: string; owner: string; repo: string }): Promise { - this.requireContributorAccess(input.login); - const fullName = `${input.owner}/${input.repo}`; - const response = await buildContributorRepoScenarioSummary(this.env, input.login, fullName); - return { - summary: response.summary.headline, - data: response as unknown as Record, - }; - } - private async getRegistryChanges(): Promise { const report = buildRegistryChangeReport(await listLatestRegistrySnapshots(this.env, 2)); return { diff --git a/src/openapi/schemas.ts b/src/openapi/schemas.ts index 512083b65a..ee748ab123 100644 --- a/src/openapi/schemas.ts +++ b/src/openapi/schemas.ts @@ -336,57 +336,6 @@ export const ContributorOpenPrMonitorSchema = z }) .openapi("ContributorOpenPrMonitor"); -export const RenderedScenarioOptionSchema = z - .object({ - rank: z.number(), - label: z.string(), - rationale: z.string(), - obstacles: z.array(z.string()), - assumptions: z.array(z.string()), - nextStep: z.string(), - recommended: z.boolean(), - }) - .openapi("RenderedScenarioOption"); - -export const RenderedPendingPullRequestSchema = z - .object({ - pullNumber: z.number(), - classification: z.string(), - notes: z.array(z.string()), - }) - .openapi("RenderedPendingPullRequest"); - -export const PublicScenarioSummarySchema = z - .object({ - repoFullName: z.string(), - generatedAt: z.string(), - advisoryOnly: z.literal(true), - notAutonomousPrBot: z.literal(true), - notPublicScoring: z.literal(true), - headline: z.string(), - options: z.array(RenderedScenarioOptionSchema), - eligibilityNotes: z.array(z.string()), - blockerNotes: z.array(z.string()), - pendingScenarioNotes: z.array(z.string()), - pendingPullRequests: z.array(RenderedPendingPullRequestSchema), - dataClassification: z.object({ - facts: z.array(z.string()), - assumptions: z.array(z.string()), - unavailableSignals: z.array(z.string()), - }), - }) - .openapi("PublicScenarioSummary"); - -export const ContributorRepoScenarioSummaryResponseSchema = z - .object({ - status: z.literal("ready"), - login: z.string(), - repoFullName: z.string(), - generatedAt: z.string(), - summary: PublicScenarioSummarySchema, - }) - .openapi("ContributorRepoScenarioSummaryResponse"); - export const ContributorOpportunitySchema = z .object({ repoFullName: z.string(), diff --git a/src/openapi/spec.ts b/src/openapi/spec.ts index fc787dcb10..b173894d37 100644 --- a/src/openapi/spec.ts +++ b/src/openapi/spec.ts @@ -22,7 +22,6 @@ import { ContributorPatternReportSchema, ContributorDecisionPackSchema, ContributorOpenPrMonitorSchema, - ContributorRepoScenarioSummaryResponseSchema, ContributorRewardRiskStrategySchema, ContributorProfileSchema, ContributorScoringProfileSchema, @@ -44,7 +43,6 @@ import { PullRequestMaintainerPacketSchema, PullRequestReviewIntelligenceSchema, PullRequestReviewabilitySchema, - PublicScenarioSummarySchema, PreflightResultSchema, QueueHealthSchema, ReadinessSchema, @@ -449,16 +447,6 @@ export function buildOpenApiSpec() { 202: { description: "Decision pack snapshot is missing; a background rebuild has been requested", content: { "application/json": { schema: DecisionPackRefreshNeededSchema } } }, }, }); - registry.registerPath({ - method: "get", - path: "/v1/contributors/{login}/repos/{owner}/{repo}/scenario-summary", - responses: { - 200: { - description: "Public-safe contributor/repo scenario summary from cached metadata and advisory simulators.", - content: { "application/json": { schema: ContributorRepoScenarioSummaryResponseSchema } }, - }, - }, - }); registry.registerPath({ method: "post", path: "/v1/preflight/pr", diff --git a/src/services/contributor-repo-scenario-summary.ts b/src/services/contributor-repo-scenario-summary.ts deleted file mode 100644 index 44174e86c9..0000000000 --- a/src/services/contributor-repo-scenario-summary.ts +++ /dev/null @@ -1,246 +0,0 @@ -import { - getRepository, - listContributorIssues, - listContributorPullRequests, - listContributorRepoStats, - listIssues, - listPullRequests, - listRecentMergedPullRequests, - listRepositories, -} from "../db/repositories"; -import { contributorRepoStatsFromGittensor, fetchGittensorContributorSnapshot } from "../gittensor/api"; -import { fetchPublicContributorProfile } from "../github/public"; -import { buildScenarioInput, createScenarioSignalEntry } from "../scenarios/input-model"; -import { renderPublicScenarioSummary, type PublicScenarioSummary } from "../scenarios/scenario-summary"; -import { buildScorePreview } from "../scoring/preview"; -import { detectPendingPrScenario, loadContributorRepoOpenPrSignals } from "../scoring/pending-pr-scenarios"; -import { deriveEligibilityPlan } from "./eligibility-plan"; -import { simulateOpenPrPressure } from "./open-pr-pressure-scenarios"; -import { getOrCreateScoringModelSnapshot } from "../scoring/model"; -import { - buildCollisionReport, - buildContributorOutcomeHistory, - buildContributorProfile, - buildLaneAdvice, - buildQueueHealth, - buildRoleContext, - type ContributorOutcomeHistory, - type ContributorProfile, -} from "../signals/engine"; -import type { PullRequestRecord, RepositoryRecord } from "../types"; -import { nowIso } from "../utils/json"; - -export type ContributorRepoScenarioSummaryResponse = { - status: "ready"; - login: string; - repoFullName: string; - generatedAt: string; - summary: PublicScenarioSummary; -}; - -export type ContributorRepoScenarioSummaryContext = { - login: string; - repoFullName: string; - repo: RepositoryRecord | null; - profile: ContributorProfile; - outcomeHistory: ContributorOutcomeHistory; - contributorPullRequests: PullRequestRecord[]; -}; - -function authoritativeContributorRepoStats( - gittensorSnapshot: Awaited>, - cachedRepoStats: Awaited>, -) { - const officialRepoStats = contributorRepoStatsFromGittensor(gittensorSnapshot); - return officialRepoStats.length > 0 ? officialRepoStats : cachedRepoStats; -} - -function sameLogin(value: string | null | undefined, login: string): boolean { - return Boolean(value && value.toLowerCase() === login.toLowerCase()); -} - -function contributorOpenPrsOnRepo(login: string, repoFullName: string, pullRequests: PullRequestRecord[]): PullRequestRecord[] { - return pullRequests.filter( - (pr) => pr.state === "open" && sameLogin(pr.authorLogin, login) && pr.repoFullName.toLowerCase() === repoFullName.toLowerCase(), - ); -} - -function metadataPreviewTokenEstimates(outcome: ContributorOutcomeHistory["repoOutcomes"][number] | undefined) { - const merged = outcome?.mergedPullRequests ?? 0; - const sourceTokenScore = Math.min(120, Math.max(30, 42 + merged * 2)); - const totalTokenScore = Math.min(220, Math.max(60, 70 + merged * 4)); - return { - sourceTokenScore, - totalTokenScore, - sourceLines: Math.max(12, sourceTokenScore), - }; -} - -function bestFitLabels(repo: RepositoryRecord | null): string[] { - const multipliers = repo?.registryConfig?.labelMultipliers ?? {}; - const labels = Object.entries(multipliers) - .filter(([label]) => !/status|source|contributor|verified|risk|codex/i.test(label)) - .sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0])) - .map(([label]) => label); - return labels.slice(0, 1); -} - -export function buildContributorRepoScenarioSummaryFromContext( - context: ContributorRepoScenarioSummaryContext & { - issues: Awaited>; - pullRequests: Awaited>; - recentMergedPullRequests: Awaited>; - scoringSnapshot: Awaited>; - pendingDetection: ReturnType; - generatedAt?: string | undefined; - }, -): ContributorRepoScenarioSummaryResponse { - const generatedAt = context.generatedAt ?? nowIso(); - const { login, repoFullName, repo, profile, outcomeHistory, contributorPullRequests, issues, pullRequests, recentMergedPullRequests, scoringSnapshot, pendingDetection } = - context; - const repoOutcome = outcomeHistory.repoOutcomes.find((entry) => entry.repoFullName.toLowerCase() === repoFullName.toLowerCase()); - const roleContext = buildRoleContext({ - login, - repo, - repoFullName, - pullRequests, - issues, - profile, - }); - const lane = buildLaneAdvice(repo, repoFullName); - const collisions = buildCollisionReport(repoFullName, issues, pullRequests, recentMergedPullRequests); - const queueHealth = buildQueueHealth(repo, issues, pullRequests, collisions); - const contributorOpenPrCount = contributorOpenPrsOnRepo(login, repoFullName, contributorPullRequests).length; - const tokenEstimates = metadataPreviewTokenEstimates(repoOutcome); - const credibility = repoOutcome?.credibility && repoOutcome.credibility > 0 ? repoOutcome.credibility : outcomeHistory.totals.credibility ?? 0.8; - const preview = buildScorePreview({ - input: { - repoFullName, - targetType: "planned_pr", - targetKey: `${login}:${repoFullName}:scenario-summary`, - contributorLogin: login, - labels: bestFitLabels(repo), - linkedIssueMode: lane.lane === "issue_discovery" ? "none" : "standard", - ...tokenEstimates, - existingContributorTokenScore: 0, - credibility, - metadataOnly: true, - duplicateRiskCount: collisions.summary.highRiskCount, - openPrCount: contributorOpenPrCount, - }, - repo, - snapshot: scoringSnapshot, - }); - const pressureSimulation = simulateOpenPrPressure({ - repoFullName, - generatedAt, - queueHealth, - roleContext, - contributorOpenPrCount, - }); - const eligibilityPlan = deriveEligibilityPlan(preview); - const scenarioInput = buildScenarioInput({ - scenarioType: "general_repo", - repoFullName, - registered: Boolean(repo?.isRegistered), - maintainerLane: roleContext.role !== "outside_contributor", - facts: [ - createScenarioSignalEntry({ - id: "queue", - kind: "fact", - label: "Queue pressure", - detail: queueHealth.summary, - source: "github_observed", - }), - createScenarioSignalEntry({ - id: "open_prs", - kind: "fact", - label: "Open PRs", - detail: `Contributor has ${contributorOpenPrCount} open PR(s) on ${repoFullName}.`, - source: "github_observed", - }), - ], - assumptions: pendingDetection - ? pendingDetection.scenarioNotes.slice(0, 3).map((note, index) => - createScenarioSignalEntry({ - id: `pending-${index + 1}`, - kind: "assumption", - label: "Pending PR scenario", - detail: note, - source: pendingDetection.source === "user_supplied" ? "user_supplied" : "github_observed", - }), - ) - : [], - unavailableSignals: [], - }); - const summary = renderPublicScenarioSummary({ - repoFullName, - generatedAt, - pressureSimulation, - eligibilityPlan, - pendingDetection: pendingDetection ?? undefined, - publicBlockers: preview.blockedBy, - scenarioInput, - }); - return { status: "ready", login, repoFullName, generatedAt, summary }; -} - -export async function buildContributorRepoScenarioSummary(env: Env, login: string, repoFullName: string): Promise { - const [github, contributorPullRequests, contributorIssues, repositories, cachedRepoStats, gittensorSnapshot, repo, issues, pullRequests, recentMergedPullRequests, scoringSnapshot] = - await Promise.all([ - fetchPublicContributorProfile(login), - listContributorPullRequests(env, login), - listContributorIssues(env, login), - listRepositories(env), - listContributorRepoStats(env, login), - fetchGittensorContributorSnapshot(login), - getRepository(env, repoFullName), - listIssues(env, repoFullName), - listPullRequests(env, repoFullName), - listRecentMergedPullRequests(env, repoFullName), - getOrCreateScoringModelSnapshot(env), - ]); - const repoStats = authoritativeContributorRepoStats(gittensorSnapshot, cachedRepoStats); - const profile = buildContributorProfile(login, github, contributorPullRequests, contributorIssues, repoStats, gittensorSnapshot); - const outcomeHistory = buildContributorOutcomeHistory({ - login, - profile, - repositories, - pullRequests: contributorPullRequests, - issues: contributorIssues, - repoStats, - cachedRepoStats, - }); - const repoOpen = contributorOpenPrsOnRepo(login, repoFullName, contributorPullRequests); - const signals = await loadContributorRepoOpenPrSignals(env, repoFullName, repoOpen); - const roleContext = buildRoleContext({ - login, - repo, - repoFullName, - pullRequests, - issues, - profile, - }); - const pendingDetection = detectPendingPrScenario({ - login, - repoFullName, - pullRequests, - roleContext, - openPrCount: repoOpen.length, - reviewsByPullNumber: signals.reviewsByPullNumber, - checksByPullNumber: signals.checksByPullNumber, - }); - return buildContributorRepoScenarioSummaryFromContext({ - login, - repoFullName, - repo, - profile, - outcomeHistory, - contributorPullRequests, - issues, - pullRequests, - recentMergedPullRequests, - scoringSnapshot, - pendingDetection, - }); -} diff --git a/test/integration/api.test.ts b/test/integration/api.test.ts index 323bfeac3d..7618fd9f98 100644 --- a/test/integration/api.test.ts +++ b/test/integration/api.test.ts @@ -668,34 +668,6 @@ describe("api routes", () => { }, }); - const scenarioSummary = await app.request("/v1/contributors/oktofeesh1/repos/entrius/allways-ui/scenario-summary", { headers: apiHeaders(env) }, env); - expect(scenarioSummary.status).toBe(200); - const scenarioSummaryPayload = (await scenarioSummary.json()) as { - status: string; - login: string; - repoFullName: string; - summary: { - advisoryOnly: boolean; - headline: string; - options: Array<{ rank: number; recommended: boolean; nextStep: string }>; - pendingScenarioNotes: string[]; - pendingPullRequests: Array<{ pullNumber: number; classification: string }>; - }; - }; - expect(scenarioSummaryPayload).toMatchObject({ - status: "ready", - login: "oktofeesh1", - repoFullName: "entrius/allways-ui", - summary: { - advisoryOnly: true, - headline: expect.any(String), - options: expect.any(Array), - }, - }); - expect(scenarioSummaryPayload.summary.options.length).toBe(3); - expect(scenarioSummaryPayload.summary.options.some((option) => option.recommended)).toBe(true); - expect(JSON.stringify(scenarioSummaryPayload.summary)).not.toMatch(/wallet|hotkey|reward estimate|payout|farming|raw trust score|scoreability/i); - const agentPlan = await app.request( "/v1/agent/plan-next-work", { @@ -3916,7 +3888,6 @@ describe("api routes", () => { expect(toolNames).toContain("gittensory_get_contributor_profile"); expect(toolNames).toContain("gittensory_get_decision_pack"); expect(toolNames).toContain("gittensory_explain_repo_decision"); - expect(toolNames).toContain("gittensory_get_scenario_summary"); expect(toolNames).toContain("gittensory_preflight_pr"); expect(toolNames).toContain("gittensory_preflight_local_diff"); expect(toolNames).toContain("gittensory_preview_local_pr_score"); @@ -4155,7 +4126,6 @@ describe("api routes", () => { ["gittensory_get_contributor_profile", { login: "oktofeesh1" }], ["gittensory_get_decision_pack", { login: "oktofeesh1" }], ["gittensory_explain_repo_decision", { login: "oktofeesh1", owner: "entrius", repo: "allways-ui" }], - ["gittensory_get_scenario_summary", { login: "oktofeesh1", owner: "entrius", repo: "allways-ui" }], ["gittensory_agent_plan_next_work", { login: "oktofeesh1", repoFullName: "entrius/allways-ui" }], [ "gittensory_preflight_pr", diff --git a/test/unit/contributor-repo-scenario-summary.test.ts b/test/unit/contributor-repo-scenario-summary.test.ts deleted file mode 100644 index c844c2c25d..0000000000 --- a/test/unit/contributor-repo-scenario-summary.test.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - buildContributorRepoScenarioSummaryFromContext, - type ContributorRepoScenarioSummaryContext, -} from "../../src/services/contributor-repo-scenario-summary"; -import type { PendingPrScenarioDetection } from "../../src/scoring/pending-pr-scenarios"; -import type { ContributorOutcomeHistory, ContributorProfile } from "../../src/signals/engine"; -import type { PullRequestRecord, ScoringModelSnapshotRecord } from "../../src/types"; - -const FORBIDDEN_PUBLIC_LANGUAGE = - /wallet|hotkey|coldkey|mnemonic|seed phrase|payout|reward[-\s]?estimate|farming|raw trust|trust[-\s]?score|scoreability|private[-\s]?reviewability|public[-\s]?score[-\s]?(?:estimate|prediction)/i; - -const snapshot: ScoringModelSnapshotRecord = { - id: "contributor-scenario-summary-model", - sourceKind: "test", - sourceUrl: "fixture://constants.py", - fetchedAt: "2026-06-03T00:00:00.000Z", - activeModel: "current_density_model", - constants: { - OSS_EMISSION_SHARE: 0.9, - MERGED_PR_BASE_SCORE: 25, - MIN_TOKEN_SCORE_FOR_BASE_SCORE: 5, - MAX_CODE_DENSITY_MULTIPLIER: 1.15, - MAX_CONTRIBUTION_BONUS: 25, - CONTRIBUTION_SCORE_FOR_FULL_BONUS: 1500, - STANDARD_ISSUE_MULTIPLIER: 1.33, - MAINTAINER_ISSUE_MULTIPLIER: 1.66, - MIN_CREDIBILITY: 0.8, - REVIEW_PENALTY_RATE: 0.15, - EXCESSIVE_PR_PENALTY_BASE_THRESHOLD: 2, - OPEN_PR_THRESHOLD_TOKEN_SCORE: 300, - MAX_OPEN_PR_THRESHOLD: 30, - OPEN_PR_COLLATERAL_PERCENT: 0.2, - SRC_TOK_SATURATION_SCALE: 58, - }, - programmingLanguages: {}, - registrySnapshotId: "registry-fixture", - warnings: [], - payload: {}, -}; - -const repo = { - fullName: "octo/demo", - owner: "octo", - name: "demo", - isInstalled: true, - isRegistered: true, - isPrivate: false, - registryConfig: { repo: "octo/demo", emissionShare: 0.02, issueDiscoveryShare: 0, labelMultipliers: { bug: 1.1 }, maintainerCut: 0, raw: {} }, -}; - -const profile: ContributorProfile = { - login: "miner-a", - generatedAt: "2026-06-03T00:00:00.000Z", - github: { login: "miner-a", topLanguages: ["TypeScript"], source: "github" }, - source: "github_cache", - registeredRepoActivity: { - pullRequests: 2, - mergedPullRequests: 1, - issues: 0, - reposTouched: [repo.fullName], - dominantLabels: ["bug"], - }, - trustSignals: { - evidenceScore: 80, - level: "emerging", - unlinkedOpenPullRequests: 0, - maintainerAssociatedPullRequests: 0, - }, -}; - -const outcomeHistory: ContributorOutcomeHistory = { - login: "miner-a", - generatedAt: "2026-06-03T00:00:00.000Z", - source: "github_cache", - totals: { - pullRequests: 2, - mergedPullRequests: 1, - openPullRequests: 1, - closedPullRequests: 0, - closedPullRequestRate: 0, - issues: 0, - openIssues: 0, - closedIssues: 0, - solvedIssues: 0, - validSolvedIssues: 0, - credibility: 0.9, - issueCredibility: 1, - }, - repoOutcomes: [ - { - repoFullName: "octo/demo", - role: "outside_contributor", - lane: "direct_pr", - maintainerLane: false, - pullRequests: 2, - mergedPullRequests: 1, - openPullRequests: 1, - closedPullRequests: 0, - closedPullRequestRate: 0, - issues: 0, - openIssues: 0, - closedIssues: 0, - solvedIssues: 0, - validSolvedIssues: 0, - credibility: 0.9, - issueCredibility: 1, - isEligible: true, - successLevel: "emerging", - strengths: ["Merged prior PRs."], - risks: [], - }, - ], - successPatterns: [], - failurePatterns: [], - summary: "fixture history", -}; - -const contributorPullRequests: PullRequestRecord[] = [ - { - repoFullName: "octo/demo", - number: 11, - title: "Ready cleanup", - state: "open", - authorLogin: "miner-a", - authorAssociation: "NONE", - labels: [], - linkedIssues: [], - updatedAt: "2026-05-20T00:00:00.000Z", - }, -]; - -const pendingDetection: PendingPrScenarioDetection = { - source: "github_observed", - pendingMergedPrCount: 1, - pendingClosedPrCount: 0, - approvedPrCount: 1, - expectedOpenPrCountAfterMerge: 0, - scenarioNotes: ["1 open PR(s) look merge-ready (approved, no changes requested, no failing checks, not draft/stale)."], - classified: [ - { - repoFullName: "octo/demo", - number: 11, - title: "Ready cleanup", - classification: "merge_ready", - reasons: ["Approved review in cache."], - }, - ], -}; - -type BuildContext = ContributorRepoScenarioSummaryContext & { - issues: []; - pullRequests: PullRequestRecord[]; - recentMergedPullRequests: []; - scoringSnapshot: ScoringModelSnapshotRecord; - pendingDetection: PendingPrScenarioDetection | null; - generatedAt?: string; -}; - -function baseContext(): BuildContext { - return { - login: "miner-a", - repoFullName: "octo/demo", - repo, - profile, - outcomeHistory, - contributorPullRequests, - issues: [], - pullRequests: contributorPullRequests, - recentMergedPullRequests: [], - scoringSnapshot: snapshot, - pendingDetection, - generatedAt: "2026-06-03T00:00:00.000Z", - }; -} - -describe("buildContributorRepoScenarioSummaryFromContext", () => { - it("composes pressure, eligibility, pending, and public-safe summary fields", () => { - const response = buildContributorRepoScenarioSummaryFromContext(baseContext()); - expect(response).toMatchObject({ - status: "ready", - login: "miner-a", - repoFullName: "octo/demo", - generatedAt: "2026-06-03T00:00:00.000Z", - summary: { - advisoryOnly: true, - notAutonomousPrBot: true, - notPublicScoring: true, - repoFullName: "octo/demo", - }, - }); - expect(response.summary.options.length).toBe(3); - expect(response.summary.eligibilityNotes.length).toBeGreaterThan(0); - expect(response.summary.pendingScenarioNotes.length).toBeGreaterThan(0); - expect(response.summary.pendingPullRequests).toEqual([ - expect.objectContaining({ pullNumber: 11, classification: expect.stringMatching(/merge-ready/i) }), - ]); - expect(response.summary.dataClassification.facts.join(" ")).toMatch(/Queue pressure|Open PRs/i); - expect(JSON.stringify(response.summary)).not.toMatch(FORBIDDEN_PUBLIC_LANGUAGE); - }); - - it("still returns ranked options when pending detection is absent", () => { - const response = buildContributorRepoScenarioSummaryFromContext({ - ...baseContext(), - pendingDetection: null, - }); - expect(response.summary.options.length).toBe(3); - expect(response.summary.pendingScenarioNotes).toEqual([]); - expect(response.summary.pendingPullRequests).toEqual([]); - }); -}); diff --git a/test/unit/mcp-output-schemas.test.ts b/test/unit/mcp-output-schemas.test.ts index f914bdd6fa..bd9bc65dcc 100644 --- a/test/unit/mcp-output-schemas.test.ts +++ b/test/unit/mcp-output-schemas.test.ts @@ -17,7 +17,6 @@ const TOOLS_WITH_OUTPUT_SCHEMA = [ "gittensory_get_decision_pack", "gittensory_monitor_open_prs", "gittensory_explain_repo_decision", - "gittensory_get_scenario_summary", "gittensory_get_issue_quality", "gittensory_get_registry_changes", "gittensory_get_upstream_drift", From e202a5d899b58ddfcd3c92aff2d33a2fdbbd5f06 Mon Sep 17 00:00:00 2001 From: Clayton Date: Mon, 8 Jun 2026 20:10:18 -0500 Subject: [PATCH 5/6] fix: update --- src/signals/local-branch.ts | 37 ++++++++++ test/unit/local-branch.test.ts | 124 +++++++++++++++++++++++++++++++++ 2 files changed, 161 insertions(+) diff --git a/src/signals/local-branch.ts b/src/signals/local-branch.ts index f2bd132e46..6d2e8c536c 100644 --- a/src/signals/local-branch.ts +++ b/src/signals/local-branch.ts @@ -19,6 +19,9 @@ import { buildRepoRewardRisk, type RepoRewardRisk, type RewardRiskAction } from import { buildLocalWorkspaceIntelligence, type LocalWorkspaceIntelligence } from "./local-workspace-intelligence"; import { buildFocusManifestGuidance, parseFocusManifest, type FocusManifestGuidance } from "./focus-manifest"; import { sanitizeLocalScorerWarnings } from "./local-scorer-diagnostics"; +import { deriveEligibilityPlan } from "../services/eligibility-plan"; +import { scenarioInputFromLocalBranchMetadata } from "../scenarios/input-model"; +import { renderPublicScenarioSummary, type PublicScenarioSummary, type ScenarioSummaryInput } from "../scenarios/scenario-summary"; export type LocalBranchChangedFile = { path: string; @@ -170,6 +173,7 @@ export type LocalBranchAnalysis = { }; nextActions: RewardRiskAction[]; workspaceIntelligence: LocalWorkspaceIntelligence; + scenarioSummary: PublicScenarioSummary; summary: string; }; @@ -334,6 +338,38 @@ export function buildLocalBranchAnalysis(args: { ...scorePreview.warnings.filter((warning) => /not registered|no active|exceeds|credibility|token gate|confirmed ineligible/i.test(warning)), ...preflight.findings.filter((finding) => finding.severity !== "info").map((finding) => finding.title), ]; + const eligibilityPlan = deriveEligibilityPlan(scorePreview); + const eligibilityStatusForScenario = scorePreview.branchEligibility.status === "not_required" ? undefined : scorePreview.branchEligibility.status; + const branchScenarioInput = scenarioInputFromLocalBranchMetadata({ + scenarioType: "branch_preflight", + login: args.input.login, + repoFullName: args.input.repoFullName, + ...(args.input.branchName ? { branchName: args.input.branchName } : {}), + ...(args.input.baseRef ? { baseRef: args.input.baseRef } : {}), + changedFileCount: changedFiles.length, + ...(args.input.linkedIssues ? { linkedIssues: args.input.linkedIssues } : {}), + scenarioNotes: observedPullRequestScenarios.notes.slice(0, 4), + ...(eligibilityStatusForScenario ? { eligibilityStatus: eligibilityStatusForScenario } : {}), + }); + const pendingDetectionForSummary: ScenarioSummaryInput["pendingDetection"] = + observedPullRequestScenarios.approvedOrMergeable > 0 || observedPullRequestScenarios.stale > 0 + ? { + source: "github_observed", + pendingMergedPrCount: observedPullRequestScenarios.approvedOrMergeable, + pendingClosedPrCount: observedPullRequestScenarios.stale, + approvedPrCount: observedPullRequestScenarios.approvedOrMergeable, + scenarioNotes: observedPullRequestScenarios.notes, + classified: [], + } + : undefined; + const scenarioSummary = renderPublicScenarioSummary({ + repoFullName: args.input.repoFullName, + generatedAt: nowIso(), + eligibilityPlan, + publicBlockers: scorePreview.blockedBy, + scenarioInput: branchScenarioInput, + pendingDetection: pendingDetectionForSummary, + }); return { login: args.input.login, repoFullName: args.input.repoFullName, @@ -367,6 +403,7 @@ export function buildLocalBranchAnalysis(args: { manifestGuidance, prPacket, nextActions: withSituationalAction(rewardRisk.actions, branchQualityBlockers, accountStateBlockers, scorePreview).slice(0, 6), + scenarioSummary, workspaceIntelligence: buildLocalWorkspaceIntelligence({ input: args.input, analysis: { diff --git a/test/unit/local-branch.test.ts b/test/unit/local-branch.test.ts index eb8bb2746a..53d05bfaed 100644 --- a/test/unit/local-branch.test.ts +++ b/test/unit/local-branch.test.ts @@ -1518,6 +1518,130 @@ describe("local MCP git metadata collection", () => { expect(error instanceof Error ? error.message : String(error)).not.toContain(tempDir); } }); + + it("emits scenarioSummary with advisory flags set and no forbidden public language", () => { + const analysis = buildLocalBranchAnalysis({ + input: { + login: "oktofeesh1", + repoFullName: repo.fullName, + changedFiles: [{ path: "src/util.ts", additions: 30, deletions: 2, status: "modified" }], + localScorer: { mode: "external_command", sourceTokenScore: 40, totalTokenScore: 60, sourceLines: 38 }, + }, + repo, + issues: [], + pullRequests: [], + profile, + outcomeHistory, + scoringSnapshot, + scoringProfile, + }); + + expect(analysis.scenarioSummary.advisoryOnly).toBe(true); + expect(analysis.scenarioSummary.notAutonomousPrBot).toBe(true); + expect(analysis.scenarioSummary.notPublicScoring).toBe(true); + expect(analysis.scenarioSummary.headline.length).toBeGreaterThan(0); + expect(JSON.stringify(analysis.scenarioSummary)).not.toMatch( + /wallet|hotkey|coldkey|mnemonic|seed phrase|payout|reward[-\s]?estimate|farming|raw trust|trust[-\s]?score|scoreability|private[-\s]?reviewability|public[-\s]?score[-\s]?(?:estimate|prediction)/i, + ); + }); + + it("populates scenarioSummary.dataClassification with contributor and repo facts from branch metadata", () => { + const analysis = buildLocalBranchAnalysis({ + input: { + login: "oktofeesh1", + repoFullName: repo.fullName, + branchName: "feat-util-refactor", + changedFiles: [{ path: "src/util.ts", additions: 20, deletions: 1, status: "modified" }], + localScorer: { mode: "external_command", sourceTokenScore: 35, totalTokenScore: 55, sourceLines: 30 }, + }, + repo, + issues: [], + pullRequests: [], + profile, + outcomeHistory, + scoringSnapshot, + scoringProfile, + }); + + expect(analysis.scenarioSummary.dataClassification.facts).toContain("Contributor"); + expect(analysis.scenarioSummary.dataClassification.facts).toContain("Repository"); + expect(analysis.scenarioSummary.dataClassification.facts).toContain("Branch"); + }); + + it("populates scenarioSummary.eligibilityNotes from the derived eligibility plan", () => { + const analysis = buildLocalBranchAnalysis({ + input: { + login: "oktofeesh1", + repoFullName: repo.fullName, + changedFiles: [{ path: "src/cache.ts", additions: 30, deletions: 2, status: "modified" }], + localScorer: { mode: "external_command", sourceTokenScore: 40, totalTokenScore: 60, sourceLines: 38 }, + }, + repo, + issues: [], + pullRequests: [], + profile, + outcomeHistory, + scoringSnapshot, + scoringProfile, + }); + + expect(analysis.scenarioSummary.eligibilityNotes.length).toBeGreaterThan(0); + }); + + it("populates scenarioSummary.blockerNotes when the score preview has metadata-only signals", () => { + const analysis = buildLocalBranchAnalysis({ + input: { + login: "oktofeesh1", + repoFullName: repo.fullName, + // No localScorer — falls back to metadata-only mode, triggering the metadata_only blocker + changedFiles: [{ path: "src/cache.ts", additions: 5, deletions: 0, status: "modified" }], + }, + repo, + issues: [], + pullRequests: [], + profile, + outcomeHistory, + scoringSnapshot, + scoringProfile, + }); + + expect(analysis.scenarioSummary.blockerNotes.join(" ")).toMatch(/metadata signals are available/i); + }); + + it("populates scenarioSummary.pendingScenarioNotes when approved open PRs are observed from cached GitHub state", () => { + const analysis = buildLocalBranchAnalysis({ + input: { + login: "oktofeesh1", + repoFullName: repo.fullName, + changedFiles: [{ path: "src/cache.ts", additions: 30, deletions: 2, status: "modified" }], + localScorer: { mode: "external_command", sourceTokenScore: 40, totalTokenScore: 60, sourceLines: 38 }, + }, + repo, + issues: [], + pullRequests: [], + contributorPullRequests: [ + { + repoFullName: repo.fullName, + number: 21, + title: "Approved cache fix", + state: "open", + authorLogin: "oktofeesh1", + authorAssociation: "CONTRIBUTOR", + reviewDecision: "APPROVED", + labels: [], + linkedIssues: [], + }, + ], + profile, + outcomeHistory: { ...outcomeHistory, totals: { ...outcomeHistory.totals, openPullRequests: 1 } }, + scoringSnapshot, + scoringProfile, + }); + + expect(analysis.observedPullRequestScenarios.approvedOrMergeable).toBeGreaterThan(0); + expect(analysis.scenarioSummary.pendingScenarioNotes.length).toBeGreaterThan(0); + expect(analysis.scenarioSummary.pendingScenarioNotes.join(" ")).toMatch(/cached GitHub reviews, checks, and activity/i); + }); }); const repo: RepositoryRecord = { From da0b33f4b2d4d383b151222b9b95be2dec725e11 Mon Sep 17 00:00:00 2001 From: Clayton Date: Wed, 10 Jun 2026 23:54:28 -0500 Subject: [PATCH 6/6] fix: update --- src/signals/local-branch.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/signals/local-branch.ts b/src/signals/local-branch.ts index e528a4c13a..694549061c 100644 --- a/src/signals/local-branch.ts +++ b/src/signals/local-branch.ts @@ -351,6 +351,9 @@ export function buildLocalBranchAnalysis(args: { scenarioNotes: observedPullRequestScenarios.notes.slice(0, 4), ...(eligibilityStatusForScenario ? { eligibilityStatus: eligibilityStatusForScenario } : {}), }); + // classified is intentionally empty here: buildLocalBranchAnalysis only has aggregate + // counts and notes from observedPullRequestScenarios, not per-PR detail rows. The + // pendingScenarioNotes surface the counts; per-PR rows require caller-supplied classification. const pendingDetectionForSummary: ScenarioSummaryInput["pendingDetection"] = observedPullRequestScenarios.approvedOrMergeable > 0 || observedPullRequestScenarios.stale > 0 ? {