@loopover/engine 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +94 -0
- package/LICENSE +661 -0
- package/README.md +676 -0
- package/dist/advisory/gate-advisory.d.ts +151 -0
- package/dist/advisory/gate-advisory.js +512 -0
- package/dist/ai-policy-map.d.ts +97 -0
- package/dist/ai-policy-map.js +377 -0
- package/dist/ams-policy-spec.d.ts +56 -0
- package/dist/ams-policy-spec.js +165 -0
- package/dist/calibration-dashboard.d.ts +23 -0
- package/dist/calibration-dashboard.js +65 -0
- package/dist/calibration-trend.d.ts +40 -0
- package/dist/calibration-trend.js +60 -0
- package/dist/contributor-fit.d.ts +24 -0
- package/dist/contributor-fit.js +50 -0
- package/dist/discovery-index-contract.d.ts +74 -0
- package/dist/discovery-index-contract.js +184 -0
- package/dist/discovery-soft-claim.d.ts +35 -0
- package/dist/discovery-soft-claim.js +50 -0
- package/dist/duplicate-winner.d.ts +57 -0
- package/dist/duplicate-winner.js +98 -0
- package/dist/feasibility.d.ts +27 -0
- package/dist/feasibility.js +86 -0
- package/dist/finding-severity-calibration.d.ts +116 -0
- package/dist/finding-severity-calibration.js +457 -0
- package/dist/fleet-run-manifest.d.ts +41 -0
- package/dist/fleet-run-manifest.js +151 -0
- package/dist/focus-manifest/guidance.d.ts +21 -0
- package/dist/focus-manifest/guidance.js +222 -0
- package/dist/focus-manifest.d.ts +826 -0
- package/dist/focus-manifest.js +2730 -0
- package/dist/gate-verdict-calibration.d.ts +111 -0
- package/dist/gate-verdict-calibration.js +411 -0
- package/dist/github/constants.d.ts +1 -0
- package/dist/github/constants.js +1 -0
- package/dist/github/sanitize-public-comment.d.ts +1 -0
- package/dist/github/sanitize-public-comment.js +26 -0
- package/dist/goal-model.d.ts +7 -0
- package/dist/goal-model.js +87 -0
- package/dist/governor/action-mode.d.ts +55 -0
- package/dist/governor/action-mode.js +74 -0
- package/dist/governor/budget-cap.d.ts +49 -0
- package/dist/governor/budget-cap.js +34 -0
- package/dist/governor/chokepoint.d.ts +58 -0
- package/dist/governor/chokepoint.js +269 -0
- package/dist/governor/kill-switch.d.ts +38 -0
- package/dist/governor/kill-switch.js +62 -0
- package/dist/governor/rate-limit.d.ts +40 -0
- package/dist/governor/rate-limit.js +63 -0
- package/dist/governor/reputation-throttle.d.ts +50 -0
- package/dist/governor/reputation-throttle.js +94 -0
- package/dist/governor/run-halt.d.ts +39 -0
- package/dist/governor/run-halt.js +83 -0
- package/dist/governor/self-plagiarism.d.ts +47 -0
- package/dist/governor/self-plagiarism.js +200 -0
- package/dist/governor/write-rate-limit.d.ts +52 -0
- package/dist/governor/write-rate-limit.js +132 -0
- package/dist/governor-ledger.d.ts +25 -0
- package/dist/governor-ledger.js +91 -0
- package/dist/index.d.ts +99 -0
- package/dist/index.js +120 -0
- package/dist/issue-plan-decomposition.d.ts +18 -0
- package/dist/issue-plan-decomposition.js +56 -0
- package/dist/issue-rag-query.d.ts +12 -0
- package/dist/issue-rag-query.js +35 -0
- package/dist/local-scorer.d.ts +34 -0
- package/dist/local-scorer.js +36 -0
- package/dist/metadata-best-min-score.d.ts +9 -0
- package/dist/metadata-best-min-score.js +9 -0
- package/dist/metadata-best-pick.d.ts +9 -0
- package/dist/metadata-best-pick.js +9 -0
- package/dist/metadata-min-score.d.ts +9 -0
- package/dist/metadata-min-score.js +13 -0
- package/dist/metadata-top-min-score.d.ts +9 -0
- package/dist/metadata-top-min-score.js +13 -0
- package/dist/metadata-top-pick.d.ts +9 -0
- package/dist/metadata-top-pick.js +13 -0
- package/dist/miner/acceptance-criteria.d.ts +43 -0
- package/dist/miner/acceptance-criteria.js +77 -0
- package/dist/miner/agent-sdk-driver.d.ts +39 -0
- package/dist/miner/agent-sdk-driver.js +162 -0
- package/dist/miner/attempt-log.d.ts +29 -0
- package/dist/miner/attempt-log.js +100 -0
- package/dist/miner/attempt-metering.d.ts +39 -0
- package/dist/miner/attempt-metering.js +67 -0
- package/dist/miner/cli-subprocess-driver.d.ts +68 -0
- package/dist/miner/cli-subprocess-driver.js +272 -0
- package/dist/miner/coding-agent-driver.d.ts +33 -0
- package/dist/miner/coding-agent-driver.js +36 -0
- package/dist/miner/coding-agent-invoke.d.ts +11 -0
- package/dist/miner/coding-agent-invoke.js +80 -0
- package/dist/miner/coding-agent-mode.d.ts +25 -0
- package/dist/miner/coding-agent-mode.js +43 -0
- package/dist/miner/driver-factory.d.ts +82 -0
- package/dist/miner/driver-factory.js +169 -0
- package/dist/miner/harness-submission-trigger.d.ts +37 -0
- package/dist/miner/harness-submission-trigger.js +46 -0
- package/dist/miner/iterate-loop.d.ts +97 -0
- package/dist/miner/iterate-loop.js +278 -0
- package/dist/miner/iterate-policy.d.ts +98 -0
- package/dist/miner/iterate-policy.js +98 -0
- package/dist/miner/lint-guard.d.ts +50 -0
- package/dist/miner/lint-guard.js +68 -0
- package/dist/miner/local-write-tools.d.ts +81 -0
- package/dist/miner/local-write-tools.js +113 -0
- package/dist/miner/loop-reentry-policy.d.ts +36 -0
- package/dist/miner/loop-reentry-policy.js +53 -0
- package/dist/miner/repo-map.d.ts +51 -0
- package/dist/miner/repo-map.js +268 -0
- package/dist/miner/self-review-adapter.d.ts +106 -0
- package/dist/miner/self-review-adapter.js +83 -0
- package/dist/miner/submission-gate.d.ts +40 -0
- package/dist/miner/submission-gate.js +89 -0
- package/dist/miner/worktree-allocator.d.ts +59 -0
- package/dist/miner/worktree-allocator.js +60 -0
- package/dist/miner/worktree-pool.d.ts +55 -0
- package/dist/miner/worktree-pool.js +65 -0
- package/dist/miner-goal-lane-fit.d.ts +18 -0
- package/dist/miner-goal-lane-fit.js +76 -0
- package/dist/miner-goal-spec.d.ts +139 -0
- package/dist/miner-goal-spec.js +281 -0
- package/dist/miner-prediction-metrics.d.ts +16 -0
- package/dist/miner-prediction-metrics.js +61 -0
- package/dist/miner-telemetry.d.ts +32 -0
- package/dist/miner-telemetry.js +87 -0
- package/dist/objective-anchor.d.ts +125 -0
- package/dist/objective-anchor.js +401 -0
- package/dist/opportunity-competition.d.ts +6 -0
- package/dist/opportunity-competition.js +29 -0
- package/dist/opportunity-freshness.d.ts +18 -0
- package/dist/opportunity-freshness.js +55 -0
- package/dist/opportunity-metadata.d.ts +53 -0
- package/dist/opportunity-metadata.js +204 -0
- package/dist/opportunity-ranker.d.ts +46 -0
- package/dist/opportunity-ranker.js +74 -0
- package/dist/pairwise-calibration.d.ts +49 -0
- package/dist/pairwise-calibration.js +106 -0
- package/dist/phase7-calibration-loop.d.ts +136 -0
- package/dist/phase7-calibration-loop.js +437 -0
- package/dist/plan-blocked.d.ts +6 -0
- package/dist/plan-blocked.js +24 -0
- package/dist/plan-completed.d.ts +5 -0
- package/dist/plan-completed.js +6 -0
- package/dist/plan-completion.d.ts +6 -0
- package/dist/plan-completion.js +7 -0
- package/dist/plan-empty.d.ts +5 -0
- package/dist/plan-empty.js +6 -0
- package/dist/plan-export.d.ts +27 -0
- package/dist/plan-export.js +86 -0
- package/dist/plan-failure.d.ts +5 -0
- package/dist/plan-failure.js +6 -0
- package/dist/plan-overall-status.d.ts +6 -0
- package/dist/plan-overall-status.js +25 -0
- package/dist/plan-pending.d.ts +5 -0
- package/dist/plan-pending.js +6 -0
- package/dist/plan-progress-complete.d.ts +7 -0
- package/dist/plan-progress-complete.js +10 -0
- package/dist/plan-ready.d.ts +6 -0
- package/dist/plan-ready.js +12 -0
- package/dist/plan-running.d.ts +5 -0
- package/dist/plan-running.js +6 -0
- package/dist/plan-skipped.d.ts +5 -0
- package/dist/plan-skipped.js +6 -0
- package/dist/plan-step-count.d.ts +5 -0
- package/dist/plan-step-count.js +6 -0
- package/dist/plan-step-stats.d.ts +5 -0
- package/dist/plan-step-stats.js +6 -0
- package/dist/plan-templates.d.ts +25 -0
- package/dist/plan-templates.js +107 -0
- package/dist/plan-terminated.d.ts +6 -0
- package/dist/plan-terminated.js +9 -0
- package/dist/portfolio/non-convergence.d.ts +37 -0
- package/dist/portfolio/non-convergence.js +57 -0
- package/dist/portfolio/queue.d.ts +30 -0
- package/dist/portfolio/queue.js +150 -0
- package/dist/predicted-gate.d.ts +103 -0
- package/dist/predicted-gate.js +218 -0
- package/dist/prompt-packet.d.ts +11 -0
- package/dist/prompt-packet.js +34 -0
- package/dist/ranked-opportunity-best-min-score.d.ts +8 -0
- package/dist/ranked-opportunity-best-min-score.js +9 -0
- package/dist/ranked-opportunity-best-pick.d.ts +8 -0
- package/dist/ranked-opportunity-best-pick.js +9 -0
- package/dist/ranked-opportunity-min-score.d.ts +8 -0
- package/dist/ranked-opportunity-min-score.js +13 -0
- package/dist/ranked-opportunity-top-min-score.d.ts +8 -0
- package/dist/ranked-opportunity-top-min-score.js +13 -0
- package/dist/review/advisory-ai-routing-config.d.ts +8 -0
- package/dist/review/advisory-ai-routing-config.js +40 -0
- package/dist/review/check-names.d.ts +1 -0
- package/dist/review/check-names.js +1 -0
- package/dist/review/cla-check.d.ts +49 -0
- package/dist/review/cla-check.js +71 -0
- package/dist/review/diff-file-priority.d.ts +1 -0
- package/dist/review/diff-file-priority.js +12 -0
- package/dist/review/enrichment-analyzer-names.d.ts +3 -0
- package/dist/review/enrichment-analyzer-names.js +63 -0
- package/dist/review/guardrail-config.d.ts +10 -0
- package/dist/review/guardrail-config.js +62 -0
- package/dist/review/linked-issue-hard-rules-config.d.ts +4 -0
- package/dist/review/linked-issue-hard-rules-config.js +79 -0
- package/dist/review/linked-issue-label-propagation.d.ts +9 -0
- package/dist/review/linked-issue-label-propagation.js +114 -0
- package/dist/review/pre-merge-checks.d.ts +28 -0
- package/dist/review/pre-merge-checks.js +66 -0
- package/dist/review/review-thread-findings.d.ts +1 -0
- package/dist/review/review-thread-findings.js +1 -0
- package/dist/review/safe-url.d.ts +5 -0
- package/dist/review/safe-url.js +137 -0
- package/dist/review/screenshot-table-gate.d.ts +87 -0
- package/dist/review/screenshot-table-gate.js +340 -0
- package/dist/review/unlinked-issue-guardrail-config.d.ts +9 -0
- package/dist/review/unlinked-issue-guardrail-config.js +44 -0
- package/dist/reviewer-consensus-calibration.d.ts +113 -0
- package/dist/reviewer-consensus-calibration.js +493 -0
- package/dist/reward-risk.d.ts +223 -0
- package/dist/reward-risk.js +618 -0
- package/dist/scoring/label-match.d.ts +4 -0
- package/dist/scoring/label-match.js +145 -0
- package/dist/scoring/model.d.ts +31 -0
- package/dist/scoring/model.js +157 -0
- package/dist/scoring/pending-pr-scenarios.d.ts +44 -0
- package/dist/scoring/pending-pr-scenarios.js +145 -0
- package/dist/scoring/preview.d.ts +246 -0
- package/dist/scoring/preview.js +1108 -0
- package/dist/scoring/types.d.ts +131 -0
- package/dist/scoring/types.js +7 -0
- package/dist/settings/auto-close-exempt.d.ts +10 -0
- package/dist/settings/auto-close-exempt.js +58 -0
- package/dist/settings/autonomy.d.ts +33 -0
- package/dist/settings/autonomy.js +80 -0
- package/dist/settings/command-authorization.d.ts +49 -0
- package/dist/settings/command-authorization.js +222 -0
- package/dist/settings/contributor-blacklist.d.ts +17 -0
- package/dist/settings/contributor-blacklist.js +86 -0
- package/dist/settings/moderation-rules.d.ts +51 -0
- package/dist/settings/moderation-rules.js +104 -0
- package/dist/settings/pr-type-label.d.ts +70 -0
- package/dist/settings/pr-type-label.js +149 -0
- package/dist/signals/change-guardrail.d.ts +49 -0
- package/dist/signals/change-guardrail.js +164 -0
- package/dist/signals/check-summary.d.ts +9 -0
- package/dist/signals/check-summary.js +12 -0
- package/dist/signals/contributor-calibration.d.ts +23 -0
- package/dist/signals/contributor-calibration.js +47 -0
- package/dist/signals/duplicate-winner.d.ts +9 -0
- package/dist/signals/duplicate-winner.js +9 -0
- package/dist/signals/path-matchers.d.ts +36 -0
- package/dist/signals/path-matchers.js +371 -0
- package/dist/signals/predicted-gate-engine.d.ts +62 -0
- package/dist/signals/predicted-gate-engine.js +895 -0
- package/dist/signals/preflight-limits.d.ts +14 -0
- package/dist/signals/preflight-limits.js +14 -0
- package/dist/signals/slop.d.ts +56 -0
- package/dist/signals/slop.js +324 -0
- package/dist/signals/test-evidence.d.ts +25 -0
- package/dist/signals/test-evidence.js +108 -0
- package/dist/subprocess-env.d.ts +23 -0
- package/dist/subprocess-env.js +74 -0
- package/dist/track-record-summary.d.ts +92 -0
- package/dist/track-record-summary.js +301 -0
- package/dist/types/manifest-deps-types.d.ts +514 -0
- package/dist/types/manifest-deps-types.js +4 -0
- package/dist/types/predicted-gate-types.d.ts +338 -0
- package/dist/types/predicted-gate-types.js +3 -0
- package/dist/types/reward-risk-types.d.ts +319 -0
- package/dist/types/reward-risk-types.js +11 -0
- package/dist/utils/json.d.ts +1 -0
- package/dist/utils/json.js +3 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +3 -0
- package/package.json +80 -0
|
@@ -0,0 +1,1108 @@
|
|
|
1
|
+
import { DEFAULT_SCORING_CONSTANTS } from "./model.js";
|
|
2
|
+
import { hasUnsafeWildcardCount } from "../signals/change-guardrail.js";
|
|
3
|
+
function nowIso() {
|
|
4
|
+
return new Date().toISOString();
|
|
5
|
+
}
|
|
6
|
+
const PROJECTED_SOLVED_BY_PULL_REQUEST_VALIDATION = Symbol("projectedSolvedByPullRequestValidation");
|
|
7
|
+
export function buildScorePreview(args) {
|
|
8
|
+
const branchEligibility = normalizeBranchEligibility(args.input);
|
|
9
|
+
const current = computeScoreCore(args.input, args.repo, args.snapshot, args.contributorEvidence);
|
|
10
|
+
const scenarioPreviews = buildScenarioPreviews(args.input, args.repo, args.snapshot, args.contributorEvidence, current);
|
|
11
|
+
const blockedBy = blockedByFor(args.input, args.repo, current, branchEligibility);
|
|
12
|
+
const gateDeltas = buildGateDeltas(current, scenarioPreviews);
|
|
13
|
+
const effectiveEstimatedScore = current.scoreEstimate.estimatedMergedScore;
|
|
14
|
+
const underlyingPotentialScore = current.scoreEstimate.pendingSaturationScore;
|
|
15
|
+
const scoreabilityStatus = statusFor(args.repo, blockedBy, effectiveEstimatedScore, scenarioPreviews);
|
|
16
|
+
const warnings = [...args.snapshot.warnings, ...warningsFor(args.input, args.repo, current, branchEligibility)];
|
|
17
|
+
const actions = [
|
|
18
|
+
...(!current.gates.baseTokenGatePassed ? ["Increase meaningful source change size or scope clarity before relying on this preview."] : []),
|
|
19
|
+
...(current.scoreEstimate.openPrMultiplier === 0 ? ["Land or close existing open PRs before opening more concurrent work."] : []),
|
|
20
|
+
...(current.scoreEstimate.openIssueMultiplier === 0 ? ["Close excess open issues to stay within the open-issue spam threshold."] : []),
|
|
21
|
+
...(current.scoreEstimate.mergedHistoryMultiplier === 0 ? ["Build merged PR history on this repo before relying on this preview; upstream requires a minimum merged count."] : []),
|
|
22
|
+
...(current.scoreEstimate.issueDiscoveryHistoryMultiplier === 0
|
|
23
|
+
? ["Build valid solved-issue history and issue credibility before relying on issue-discovery scoring on this repo."]
|
|
24
|
+
: []),
|
|
25
|
+
...(current.scoreEstimate.credibilityMultiplier < 1 ? ["Build or wait for contributor credibility evidence before relying on this preview."] : []),
|
|
26
|
+
...(current.scoreEstimate.reviewPenaltyMultiplier < 1 ? ["Reduce review churn with tighter tests and clearer evidence."] : []),
|
|
27
|
+
...(current.scoreEstimate.labelMultiplier <= 1 && Object.keys(args.repo?.registryConfig?.labelMultipliers ?? {}).length > 0
|
|
28
|
+
? ["Check whether the change legitimately matches one of the repo's configured trusted labels."]
|
|
29
|
+
: []),
|
|
30
|
+
...(branchEligibility.required && branchEligibility.status === "ineligible" ? ["Use an eligible branch or remove linked-issue assumptions before relying on this preview."] : []),
|
|
31
|
+
...(branchEligibility.required && (branchEligibility.evidence === "missing" || branchEligibility.stale)
|
|
32
|
+
? ["Refresh branch/base eligibility metadata before relying on linked-issue assumptions."]
|
|
33
|
+
: []),
|
|
34
|
+
...(current.linkedIssueMultiplier.mode === "standard" && !current.linkedIssueMultiplier.eligible
|
|
35
|
+
? ["Validate linked issue context with solved-by-PR evidence before relying on the standard issue multiplier."]
|
|
36
|
+
: []),
|
|
37
|
+
];
|
|
38
|
+
return {
|
|
39
|
+
repoFullName: args.input.repoFullName,
|
|
40
|
+
generatedAt: nowIso(),
|
|
41
|
+
scoringModelSnapshotId: args.snapshot.id,
|
|
42
|
+
activeModel: args.snapshot.activeModel,
|
|
43
|
+
privateOnly: true,
|
|
44
|
+
laneMath: current.laneMath,
|
|
45
|
+
scoreEstimate: current.scoreEstimate,
|
|
46
|
+
linkedIssueMultiplier: current.linkedIssueMultiplier,
|
|
47
|
+
gates: current.gates,
|
|
48
|
+
branchEligibility,
|
|
49
|
+
effectiveEstimatedScore,
|
|
50
|
+
underlyingPotentialScore,
|
|
51
|
+
blockedBy,
|
|
52
|
+
gateDeltas,
|
|
53
|
+
scenarioPreviews,
|
|
54
|
+
scoreabilityStatus,
|
|
55
|
+
warnings,
|
|
56
|
+
assumptions: [
|
|
57
|
+
"Advisory preview only; tied to the recorded scoring model snapshot and cached Gittensory data.",
|
|
58
|
+
"No future outcome or exact payout is guaranteed.",
|
|
59
|
+
"Private API/MCP output only; public comments intentionally omit these details.",
|
|
60
|
+
`Linked issue multiplier status: ${current.linkedIssueMultiplier.status}; ${current.linkedIssueMultiplier.reason}`,
|
|
61
|
+
...branchEligibility.warnings,
|
|
62
|
+
...(args.input.scenarioNotes ?? []).map((note) => `User scenario note: ${note}`),
|
|
63
|
+
],
|
|
64
|
+
recommendation: {
|
|
65
|
+
level: scoreabilityStatus === "hold" || warnings.some((warning) => /not registered|no active|exceeds/i.test(warning))
|
|
66
|
+
? "hold"
|
|
67
|
+
: effectiveEstimatedScore >= 30 && warnings.length === 0
|
|
68
|
+
? "strong_fit"
|
|
69
|
+
: effectiveEstimatedScore >= 15
|
|
70
|
+
? "reasonable_fit"
|
|
71
|
+
: "needs_work",
|
|
72
|
+
actions: actions.length > 0 ? actions : ["Keep the PR focused, linked, tested, and easy for maintainers to review."],
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
export function makeScorePreviewRecord(input, snapshot, result) {
|
|
77
|
+
return {
|
|
78
|
+
id: crypto.randomUUID(),
|
|
79
|
+
scoringModelSnapshotId: snapshot.id,
|
|
80
|
+
repoFullName: input.repoFullName,
|
|
81
|
+
targetType: input.targetType ?? "planned_pr",
|
|
82
|
+
targetKey: input.targetKey ?? `${input.repoFullName}:${input.targetType ?? "planned_pr"}:${Date.now()}`,
|
|
83
|
+
contributorLogin: input.contributorLogin,
|
|
84
|
+
input: input,
|
|
85
|
+
result: result,
|
|
86
|
+
generatedAt: result.generatedAt,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function computeScoreCore(input, repo, snapshot, contributorEvidence) {
|
|
90
|
+
const constants = { ...snapshot.constants };
|
|
91
|
+
const config = repo?.registryConfig;
|
|
92
|
+
const emissionShare = clamp(config?.emissionShare ?? 0, 0, 1);
|
|
93
|
+
const issueDiscoveryShare = clamp(config?.issueDiscoveryShare ?? 0, 0, 1);
|
|
94
|
+
const ossEmissionShare = constant(constants, "OSS_EMISSION_SHARE");
|
|
95
|
+
const repoSlice = emissionShare * ossEmissionShare;
|
|
96
|
+
const directPrSlice = repoSlice * (1 - issueDiscoveryShare);
|
|
97
|
+
const issueDiscoverySlice = repoSlice * issueDiscoveryShare;
|
|
98
|
+
const sourceTokenScore = nonNegative(input.sourceTokenScore);
|
|
99
|
+
// TEST_FILE_CONTRIBUTION_WEIGHT (#808): upstream weights test-file tokens at 0.05× relative to source tokens.
|
|
100
|
+
// Applied only when totalTokenScore is not explicitly provided — an explicit caller total is honoured as-is.
|
|
101
|
+
const testFileWeight = constant(constants, "TEST_FILE_CONTRIBUTION_WEIGHT");
|
|
102
|
+
const cappedNonCodeTokenScore = applyNonCodeLineCap(input, constants);
|
|
103
|
+
const derivedTotalTokenScore = sourceTokenScore + testFileWeight * nonNegative(input.testTokenScore) + cappedNonCodeTokenScore;
|
|
104
|
+
const totalTokenScore = input.totalTokenScore === undefined ? nonNegative(derivedTotalTokenScore) : applyNonCodeCapToTotal(input.totalTokenScore, input, cappedNonCodeTokenScore);
|
|
105
|
+
const sourceLines = Math.max(1, nonNegative(input.sourceLines ?? sourceTokenScore));
|
|
106
|
+
const fixedBaseScore = clampFixedBaseScore(input.fixedBaseScore ?? config?.fixedBaseScore);
|
|
107
|
+
const rawDensity = sourceTokenScore / sourceLines;
|
|
108
|
+
// Density branch (#812): upstream is on the saturation model, but `current_density_model` is still a
|
|
109
|
+
// supported `activeModel` (types.ts union, the public OpenAPI schema, the DB parser, ~20 test fixtures, and
|
|
110
|
+
// src/services/score-breakdown.ts which keys off densityMultiplier). The issue's "if density is dead,
|
|
111
|
+
// remove the branch" condition is therefore FALSE — the branch is retained as the supported alternate /
|
|
112
|
+
// fetch-failure-`unknown` fallback model. Its fallback constants are now single-sourced from
|
|
113
|
+
// DEFAULT_SCORING_CONSTANTS (model.ts) instead of silent duplicated literals, closing the drift surface.
|
|
114
|
+
const densityMultiplier = clamp(rawDensity || 0, 0, constant(constants, "MAX_CODE_DENSITY_MULTIPLIER"));
|
|
115
|
+
const densityTokenGatePassed = sourceTokenScore >= constant(constants, "MIN_TOKEN_SCORE_FOR_BASE_SCORE");
|
|
116
|
+
const baseTokenGatePassed = snapshot.activeModel === "pending_saturation_model" ? sourceTokenScore > 0 : densityTokenGatePassed;
|
|
117
|
+
const densityContributionBonus = contributionBonusRamp(totalTokenScore, constants);
|
|
118
|
+
const saturationContributionBonusValue = saturationContributionBonus(totalTokenScore, constants);
|
|
119
|
+
const saturationBaseScore = saturationScore(sourceTokenScore, totalTokenScore, constants);
|
|
120
|
+
const densityBaseScore = (densityTokenGatePassed ? constant(constants, "MERGED_PR_BASE_SCORE") * densityMultiplier : 0) + densityContributionBonus;
|
|
121
|
+
const baseScore = fixedBaseScore !== undefined
|
|
122
|
+
? fixedBaseScore
|
|
123
|
+
: snapshot.activeModel === "pending_saturation_model"
|
|
124
|
+
? saturationBaseScore
|
|
125
|
+
: densityBaseScore;
|
|
126
|
+
const baseScoreCap = fixedBaseScore !== undefined
|
|
127
|
+
? undefined
|
|
128
|
+
: snapshot.activeModel === "pending_saturation_model"
|
|
129
|
+
? constant(constants, "MERGED_PR_BASE_SCORE") + constant(constants, "MAX_CONTRIBUTION_BONUS")
|
|
130
|
+
: constant(constants, "MERGED_PR_BASE_SCORE") * constant(constants, "MAX_CODE_DENSITY_MULTIPLIER") + constant(constants, "MAX_CONTRIBUTION_BONUS");
|
|
131
|
+
const activeContributionBonus = snapshot.activeModel === "pending_saturation_model" ? saturationContributionBonusValue : densityContributionBonus;
|
|
132
|
+
const labelMultiplier = selectLabelMultiplier(input.labels ?? [], config?.labelMultipliers ?? {}, config?.defaultLabelMultiplier ?? 1);
|
|
133
|
+
const branchEligibility = normalizeBranchEligibility(input);
|
|
134
|
+
const linkedIssueMultiplier = decideLinkedIssueMultiplier(input.linkedIssueMode ?? "none", input.linkedIssueContext, constants, branchEligibility);
|
|
135
|
+
const issueMultiplier = linkedIssueMultiplier.appliedMultiplier;
|
|
136
|
+
const credibilityObserved = clamp(input.credibility ?? inferCredibility(contributorEvidence), 0, 1);
|
|
137
|
+
const credibilityFloor = constant(constants, "MIN_CREDIBILITY");
|
|
138
|
+
const credibilityMultiplier = credibilityObserved >= credibilityFloor ? 1 : credibilityObserved / credibilityFloor;
|
|
139
|
+
const changesRequestedCount = nonNegative(input.changesRequestedCount);
|
|
140
|
+
const reviewPenaltyRate = constant(constants, "REVIEW_PENALTY_RATE");
|
|
141
|
+
const reviewPenaltyMultiplier = clamp(1 - changesRequestedCount * reviewPenaltyRate, 0, 1);
|
|
142
|
+
const reviewCollateralMultiplier = Math.min(constant(constants, "MAX_OPEN_PR_REVIEW_COLLATERAL_MULTIPLIER"), 1 + changesRequestedCount * reviewPenaltyRate);
|
|
143
|
+
const openPrCollateralPercent = constant(constants, "OPEN_PR_COLLATERAL_PERCENT");
|
|
144
|
+
const openPrCount = nonNegative(input.openPrCount);
|
|
145
|
+
// The concurrency allowance is earned from the contributor's established merged-history token
|
|
146
|
+
// score; the planned PR's own tokens (totalTokenScore) must not inflate its own open-PR threshold.
|
|
147
|
+
const openPrThreshold = Math.min(constant(constants, "MAX_OPEN_PR_THRESHOLD"), constant(constants, "EXCESSIVE_PR_PENALTY_BASE_THRESHOLD") +
|
|
148
|
+
Math.floor(nonNegative(input.existingContributorTokenScore) / constant(constants, "OPEN_PR_THRESHOLD_TOKEN_SCORE")));
|
|
149
|
+
const openPrMultiplier = openPrCount <= openPrThreshold ? 1 : 0;
|
|
150
|
+
// Open-issue spam gate (#808): mirrors the open-PR gate for the issue-discovery channel.
|
|
151
|
+
// A contributor earns extra open-issue slots from their existing merged-history token score.
|
|
152
|
+
const openIssueCount = nonNegative(input.openIssueCount);
|
|
153
|
+
const openIssueThreshold = Math.min(constant(constants, "MAX_OPEN_ISSUE_THRESHOLD"), constant(constants, "OPEN_ISSUE_SPAM_BASE_THRESHOLD") +
|
|
154
|
+
Math.floor(nonNegative(input.existingContributorTokenScore) / constant(constants, "OPEN_ISSUE_SPAM_TOKEN_SCORE_PER_SLOT")));
|
|
155
|
+
const openIssueMultiplier = openIssueCount <= openIssueThreshold ? 1 : 0;
|
|
156
|
+
const mergedPrFloor = constant(constants, "MIN_VALID_MERGED_PRS");
|
|
157
|
+
const mergedPullRequestsObserved = resolveMergedPullRequests(input, contributorEvidence);
|
|
158
|
+
const mergedHistoryMultiplier = mergedPullRequestsObserved === undefined ? 1 : mergedPullRequestsObserved >= mergedPrFloor ? 1 : 0;
|
|
159
|
+
const validSolvedIssuesFloor = constant(constants, "MIN_VALID_SOLVED_ISSUES");
|
|
160
|
+
const issueCredibilityFloor = constant(constants, "MIN_ISSUE_CREDIBILITY");
|
|
161
|
+
const validSolvedIssuesObserved = input.validSolvedIssues !== undefined ? nonNegative(input.validSolvedIssues) : undefined;
|
|
162
|
+
const issueCredibilityObserved = input.issueCredibility !== undefined ? clamp(input.issueCredibility, 0, 1) : undefined;
|
|
163
|
+
// Issue-discovery validity mirrors upstream's separate issue lane, which is the `standard` linked-issue
|
|
164
|
+
// lane only. The `maintainer` lane explicitly does not require solved-by-PR issue linkage (see
|
|
165
|
+
// decideLinkedIssueMultiplier), so it must not be gated by the solved-issue history floor — otherwise a
|
|
166
|
+
// maintainer preview with sparse issue history is wrongly zeroed and flagged issue_discovery_validity_floor.
|
|
167
|
+
const issueDiscoveryRelevant = (input.linkedIssueMode ?? "none") === "standard";
|
|
168
|
+
const issueDiscoveryHistoryKnown = validSolvedIssuesObserved !== undefined && issueCredibilityObserved !== undefined;
|
|
169
|
+
const issueDiscoveryHistoryMultiplier = !issueDiscoveryRelevant || !issueDiscoveryHistoryKnown
|
|
170
|
+
? 1
|
|
171
|
+
: validSolvedIssuesObserved >= validSolvedIssuesFloor && issueCredibilityObserved >= issueCredibilityFloor
|
|
172
|
+
? 1
|
|
173
|
+
: 0;
|
|
174
|
+
// Upstream time-decay (#703): mirrors upstream's `scored.time_decay_multiplier` applied to a PR's score.
|
|
175
|
+
// Opt-in + env-gated (default off). A fresh PR (prAgeHours below the grace period) yields 1.0, so a normal
|
|
176
|
+
// new-PR preview is unchanged even when enabled — only an aged-PR projection decays.
|
|
177
|
+
// Per-repo curve (#703): the repo's registry `scoring.time_decay` overrides overlay the snapshot defaults.
|
|
178
|
+
const timeDecayMultiplier = input.applyTimeDecay ? calculateTimeDecay(nonNegative(input.prAgeHours), constants, config?.timeDecay) : 1;
|
|
179
|
+
const estimatedMergedScore = roundScore(baseScore *
|
|
180
|
+
labelMultiplier *
|
|
181
|
+
issueMultiplier *
|
|
182
|
+
credibilityMultiplier *
|
|
183
|
+
reviewPenaltyMultiplier *
|
|
184
|
+
openPrMultiplier *
|
|
185
|
+
openIssueMultiplier *
|
|
186
|
+
mergedHistoryMultiplier *
|
|
187
|
+
issueDiscoveryHistoryMultiplier *
|
|
188
|
+
timeDecayMultiplier);
|
|
189
|
+
const pendingSaturationScore = roundScore(saturationBaseScore);
|
|
190
|
+
return {
|
|
191
|
+
laneMath: {
|
|
192
|
+
repoEmissionShare: emissionShare,
|
|
193
|
+
ossEmissionShare,
|
|
194
|
+
repoSlice: roundScore(repoSlice),
|
|
195
|
+
directPrSlice: roundScore(directPrSlice),
|
|
196
|
+
issueDiscoverySlice: roundScore(issueDiscoverySlice),
|
|
197
|
+
issueDiscoveryShare,
|
|
198
|
+
},
|
|
199
|
+
scoreEstimate: {
|
|
200
|
+
baseScore: roundScore(baseScore),
|
|
201
|
+
...(baseScoreCap !== undefined ? { baseScoreCap: roundScore(baseScoreCap) } : {}),
|
|
202
|
+
densityMultiplier: roundScore(densityMultiplier),
|
|
203
|
+
contributionBonus: roundScore(activeContributionBonus),
|
|
204
|
+
labelMultiplier,
|
|
205
|
+
issueMultiplier,
|
|
206
|
+
credibilityMultiplier: roundScore(credibilityMultiplier),
|
|
207
|
+
reviewPenaltyMultiplier: roundScore(reviewPenaltyMultiplier),
|
|
208
|
+
openPrMultiplier,
|
|
209
|
+
openIssueMultiplier,
|
|
210
|
+
mergedHistoryMultiplier,
|
|
211
|
+
issueDiscoveryHistoryMultiplier,
|
|
212
|
+
timeDecayMultiplier: roundScore(timeDecayMultiplier),
|
|
213
|
+
estimatedMergedScore,
|
|
214
|
+
pendingSaturationScore,
|
|
215
|
+
},
|
|
216
|
+
linkedIssueMultiplier,
|
|
217
|
+
gates: {
|
|
218
|
+
baseTokenGatePassed,
|
|
219
|
+
openPrThreshold,
|
|
220
|
+
openPrCount,
|
|
221
|
+
reviewCollateralMultiplier: roundScore(reviewCollateralMultiplier),
|
|
222
|
+
collateralFraction: roundScore(openPrCollateralPercent * reviewCollateralMultiplier),
|
|
223
|
+
credibilityFloor,
|
|
224
|
+
credibilityObserved,
|
|
225
|
+
openIssueThreshold,
|
|
226
|
+
openIssueCount,
|
|
227
|
+
mergedPrFloor,
|
|
228
|
+
...(mergedPullRequestsObserved !== undefined ? { mergedPullRequests: mergedPullRequestsObserved } : {}),
|
|
229
|
+
validSolvedIssuesFloor,
|
|
230
|
+
...(validSolvedIssuesObserved !== undefined ? { validSolvedIssues: validSolvedIssuesObserved } : {}),
|
|
231
|
+
issueCredibilityFloor,
|
|
232
|
+
...(issueCredibilityObserved !== undefined ? { issueCredibility: issueCredibilityObserved } : {}),
|
|
233
|
+
nonCodeLineCap: constant(constants, "MAX_LINES_SCORED_FOR_NON_CODE_EXT"),
|
|
234
|
+
...(input.nonCodeLines !== undefined ? { nonCodeLinesObserved: nonNegative(input.nonCodeLines) } : {}),
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
function buildScenarioPreviews(input, repo, snapshot, contributorEvidence, current) {
|
|
239
|
+
// Count each pending open PR at most once. `approvedPrCount` and
|
|
240
|
+
// `pendingMergedPrCount` are the same merge-ready set — detectPendingPrScenario
|
|
241
|
+
// sets both to `mergeReady.length` — so they must be folded with `max`, not
|
|
242
|
+
// added, or the merge-ready PRs get double-subtracted from the open-PR
|
|
243
|
+
// projection. Closed/likely-close PRs are a disjoint set and add on top. This
|
|
244
|
+
// mirrors the canonical reduction in pending-pr-scenarios.ts
|
|
245
|
+
// (currentOpen - pendingMergedPrCount - pendingClosedPrCount).
|
|
246
|
+
const mergeReadyPending = Math.max(nonNegative(input.pendingMergedPrCount), nonNegative(input.approvedPrCount));
|
|
247
|
+
const userPendingCount = mergeReadyPending + nonNegative(input.pendingClosedPrCount);
|
|
248
|
+
const observedApprovedCount = nonNegative(input.observedApprovedPrCount);
|
|
249
|
+
const observedStaleCloseCount = nonNegative(input.observedStalePrCount);
|
|
250
|
+
const observedClosedCount = nonNegative(input.observedClosedPrCount);
|
|
251
|
+
const combinedPendingCount = userPendingCount + observedApprovedCount + observedStaleCloseCount;
|
|
252
|
+
const expectedOpenPrCountAfterMerge = input.expectedOpenPrCountAfterMerge !== undefined ? nonNegative(input.expectedOpenPrCountAfterMerge) : Math.max(0, current.gates.openPrCount - userPendingCount);
|
|
253
|
+
const projectedCredibility = input.projectedCredibility !== undefined
|
|
254
|
+
? clamp(input.projectedCredibility, 0, 1)
|
|
255
|
+
: userPendingCount > 0
|
|
256
|
+
? Math.max(current.gates.credibilityObserved, current.gates.credibilityFloor)
|
|
257
|
+
: current.gates.credibilityObserved;
|
|
258
|
+
const observedApprovalCredibility = observedApprovedCount > 0 ? Math.max(current.gates.credibilityObserved, current.gates.credibilityFloor) : current.gates.credibilityObserved;
|
|
259
|
+
const afterApprovedInput = {
|
|
260
|
+
...input,
|
|
261
|
+
openPrCount: Math.max(0, current.gates.openPrCount - observedApprovedCount),
|
|
262
|
+
credibility: observedApprovalCredibility,
|
|
263
|
+
};
|
|
264
|
+
const afterStaleInput = {
|
|
265
|
+
...input,
|
|
266
|
+
openPrCount: Math.max(0, current.gates.openPrCount - observedStaleCloseCount),
|
|
267
|
+
credibility: current.gates.credibilityObserved,
|
|
268
|
+
};
|
|
269
|
+
const cleanGatesInput = {
|
|
270
|
+
...input,
|
|
271
|
+
openPrCount: Math.min(current.gates.openPrCount, current.gates.openPrThreshold),
|
|
272
|
+
openIssueCount: Math.min(current.gates.openIssueCount, current.gates.openIssueThreshold),
|
|
273
|
+
credibility: Math.max(current.gates.credibilityObserved, current.gates.credibilityFloor),
|
|
274
|
+
...(current.gates.mergedPullRequests !== undefined
|
|
275
|
+
? { mergedPullRequests: Math.max(current.gates.mergedPullRequests, current.gates.mergedPrFloor) }
|
|
276
|
+
: {}),
|
|
277
|
+
...(current.gates.validSolvedIssues !== undefined
|
|
278
|
+
? { validSolvedIssues: Math.max(current.gates.validSolvedIssues, current.gates.validSolvedIssuesFloor) }
|
|
279
|
+
: {}),
|
|
280
|
+
...(current.gates.issueCredibility !== undefined
|
|
281
|
+
? { issueCredibility: Math.max(current.gates.issueCredibility, current.gates.issueCredibilityFloor) }
|
|
282
|
+
: {}),
|
|
283
|
+
};
|
|
284
|
+
const afterPendingInput = {
|
|
285
|
+
...input,
|
|
286
|
+
openPrCount: expectedOpenPrCountAfterMerge,
|
|
287
|
+
credibility: projectedCredibility,
|
|
288
|
+
...(current.gates.mergedPullRequests !== undefined
|
|
289
|
+
? { mergedPullRequests: nonNegative(current.gates.mergedPullRequests) + mergeReadyPending }
|
|
290
|
+
: {}),
|
|
291
|
+
};
|
|
292
|
+
const linkedIssueInput = withValidatedLinkedIssueScenario(input);
|
|
293
|
+
const bestReasonableInput = {
|
|
294
|
+
...linkedIssueInput,
|
|
295
|
+
openPrCount: Math.min(input.expectedOpenPrCountAfterMerge !== undefined ? expectedOpenPrCountAfterMerge : Math.max(0, current.gates.openPrCount - combinedPendingCount), current.gates.openPrThreshold),
|
|
296
|
+
// Project open-issue spam cleanup (#808): mirror the open-PR projection so the
|
|
297
|
+
// "best reasonable case" can clear the open-issue gate just like it clears open-PR pressure.
|
|
298
|
+
openIssueCount: Math.min(current.gates.openIssueCount, current.gates.openIssueThreshold),
|
|
299
|
+
credibility: Math.max(projectedCredibility, observedApprovalCredibility, current.gates.credibilityFloor),
|
|
300
|
+
...(current.gates.mergedPullRequests !== undefined
|
|
301
|
+
? {
|
|
302
|
+
mergedPullRequests: Math.max(nonNegative(current.gates.mergedPullRequests) + mergeReadyPending, current.gates.mergedPrFloor),
|
|
303
|
+
}
|
|
304
|
+
: {}),
|
|
305
|
+
...(current.gates.validSolvedIssues !== undefined
|
|
306
|
+
? { validSolvedIssues: Math.max(current.gates.validSolvedIssues, current.gates.validSolvedIssuesFloor) }
|
|
307
|
+
: {}),
|
|
308
|
+
...(current.gates.issueCredibility !== undefined
|
|
309
|
+
? { issueCredibility: Math.max(current.gates.issueCredibility, current.gates.issueCredibilityFloor) }
|
|
310
|
+
: {}),
|
|
311
|
+
};
|
|
312
|
+
return [
|
|
313
|
+
scenario("current", "current_data", input, current, ["Current cached/account state and supplied local diff metadata."], repo),
|
|
314
|
+
scenario("cleanGates", "gittensory_projection", cleanGatesInput, computeScoreCore(cleanGatesInput, repo, snapshot, contributorEvidence), [
|
|
315
|
+
"Open PR, open-issue, credibility, and contributor-history gates are projected as cleared; branch metadata is otherwise unchanged.",
|
|
316
|
+
], repo),
|
|
317
|
+
scenario("afterPendingMerges", input.pendingScenarioObserved
|
|
318
|
+
? "github_observed"
|
|
319
|
+
: userPendingCount > 0 || input.expectedOpenPrCountAfterMerge !== undefined || input.projectedCredibility !== undefined
|
|
320
|
+
? "user_supplied"
|
|
321
|
+
: "gittensory_projection", afterPendingInput, computeScoreCore(afterPendingInput, repo, snapshot, contributorEvidence), [
|
|
322
|
+
userPendingCount > 0
|
|
323
|
+
? `${userPendingCount} pending merged/closed PR(s) are treated as no longer open for this scenario${input.pendingScenarioObserved ? "" : " (caller-supplied)"}.`
|
|
324
|
+
: "No pending merge/close count was supplied; this scenario preserves current open PR pressure.",
|
|
325
|
+
...(input.projectedCredibility !== undefined
|
|
326
|
+
? [`Projected credibility is user-supplied as ${roundScore(projectedCredibility)}.`]
|
|
327
|
+
: userPendingCount > 0
|
|
328
|
+
? [`Projected credibility is raised to the current floor ${current.gates.credibilityFloor} because pending merges are expected to land.`]
|
|
329
|
+
: []),
|
|
330
|
+
...(input.scenarioNotes ?? []),
|
|
331
|
+
], repo),
|
|
332
|
+
scenario("afterApprovedPrsMerge", "github_observed", afterApprovedInput, computeScoreCore(afterApprovedInput, repo, snapshot, contributorEvidence), [
|
|
333
|
+
observedApprovedCount > 0
|
|
334
|
+
? `${observedApprovedCount} GitHub-observed approved or mergeable open PR(s) are treated as no longer open if they merge.`
|
|
335
|
+
: "No GitHub-observed approved or mergeable open PRs were available for this scenario.",
|
|
336
|
+
...(observedApprovedCount > 0 ? [`Projected credibility is raised to the current floor ${current.gates.credibilityFloor} after observed mergeable work lands.`] : []),
|
|
337
|
+
...observedScenarioNotes(input),
|
|
338
|
+
], repo),
|
|
339
|
+
scenario("afterStalePrsClose", "github_observed", afterStaleInput, computeScoreCore(afterStaleInput, repo, snapshot, contributorEvidence), [
|
|
340
|
+
observedStaleCloseCount > 0
|
|
341
|
+
? `${observedStaleCloseCount} GitHub-observed stale open PR(s) are treated as no longer open if they close or withdraw.`
|
|
342
|
+
: "No GitHub-observed stale open PRs were available for this scenario.",
|
|
343
|
+
...(observedClosedCount > 0 ? [`${observedClosedCount} GitHub-observed already-closed PR(s) are excluded because they no longer contribute to open PR pressure.`] : []),
|
|
344
|
+
"Credibility is not increased in this scenario because stale cleanup is not the same as merged work.",
|
|
345
|
+
...observedScenarioNotes(input),
|
|
346
|
+
], repo),
|
|
347
|
+
scenario("linkedIssueFixed", "gittensory_projection", linkedIssueInput, computeScoreCore(linkedIssueInput, repo, snapshot, contributorEvidence), [
|
|
348
|
+
input.linkedIssueMode === "none" || !input.linkedIssueMode
|
|
349
|
+
? "A standard linked-issue/no-issue rationale multiplier is projected as solved-by-PR validated."
|
|
350
|
+
: "Linked issue mode was already supplied; this scenario projects solved-by-PR validation where needed.",
|
|
351
|
+
], repo),
|
|
352
|
+
scenario("bestReasonableCase", "gittensory_projection", bestReasonableInput, computeScoreCore(bestReasonableInput, repo, snapshot, contributorEvidence), [
|
|
353
|
+
"Combines plausible near-term gate cleanup: open PR pressure at threshold or below, open-issue spam pressure at threshold or below, credibility at floor or above, contributor merged-history and issue-discovery validity at floor or above, and linked-issue context where applicable.",
|
|
354
|
+
...(input.scenarioNotes ?? []),
|
|
355
|
+
...observedScenarioNotes(input),
|
|
356
|
+
], repo),
|
|
357
|
+
];
|
|
358
|
+
}
|
|
359
|
+
function observedScenarioNotes(input) {
|
|
360
|
+
return [
|
|
361
|
+
...(nonNegative(input.observedDraftPrCount) > 0 ? [`${nonNegative(input.observedDraftPrCount)} draft PR(s) were excluded from likely-to-land projections.`] : []),
|
|
362
|
+
...(nonNegative(input.observedBlockedPrCount) > 0 ? [`${nonNegative(input.observedBlockedPrCount)} blocked PR(s) were excluded from likely-to-land projections.`] : []),
|
|
363
|
+
...(nonNegative(input.observedMaintainerPrCount) > 0 ? [`${nonNegative(input.observedMaintainerPrCount)} maintainer-lane PR(s) were kept out of outside-contributor projections.`] : []),
|
|
364
|
+
...(input.observedScenarioNotes ?? []),
|
|
365
|
+
];
|
|
366
|
+
}
|
|
367
|
+
function scenario(name, source, input, core, assumptions, repo) {
|
|
368
|
+
const blockedBy = blockedByFor(input, repo, core);
|
|
369
|
+
return {
|
|
370
|
+
name,
|
|
371
|
+
source,
|
|
372
|
+
assumptions,
|
|
373
|
+
scoreEstimate: core.scoreEstimate,
|
|
374
|
+
linkedIssueMultiplier: core.linkedIssueMultiplier,
|
|
375
|
+
gates: core.gates,
|
|
376
|
+
effectiveEstimatedScore: core.scoreEstimate.estimatedMergedScore,
|
|
377
|
+
underlyingPotentialScore: core.scoreEstimate.pendingSaturationScore,
|
|
378
|
+
blockedBy,
|
|
379
|
+
deltaExplanation: deltaExplanationFor(core, blockedBy),
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
function blockedByFor(input, repo, core, branchEligibility = normalizeBranchEligibility(input)) {
|
|
383
|
+
return [
|
|
384
|
+
...(!repo?.isRegistered
|
|
385
|
+
? [{ code: "repo_not_registered", severity: "blocker", detail: "Repository is not registered in the local Gittensory cache." }]
|
|
386
|
+
: []),
|
|
387
|
+
...(core.laneMath.repoEmissionShare <= 0
|
|
388
|
+
? [{ code: "inactive_allocation", severity: "blocker", detail: "Repository has no active allocation in the current registry snapshot." }]
|
|
389
|
+
: []),
|
|
390
|
+
...(input.metadataOnly
|
|
391
|
+
? [{ code: "metadata_only", severity: "context", detail: "Preview used metadata-only inputs, so token and density estimates are rough." }]
|
|
392
|
+
: []),
|
|
393
|
+
...(branchEligibility.required && branchEligibility.status === "ineligible"
|
|
394
|
+
? [
|
|
395
|
+
{
|
|
396
|
+
code: "branch_ineligible",
|
|
397
|
+
severity: "reducer",
|
|
398
|
+
detail: "Branch eligibility is confirmed ineligible; linked-issue multiplier assumptions are disabled.",
|
|
399
|
+
},
|
|
400
|
+
]
|
|
401
|
+
: []),
|
|
402
|
+
...(branchEligibility.required && branchEligibility.status === "unknown"
|
|
403
|
+
? [
|
|
404
|
+
{
|
|
405
|
+
code: "branch_eligibility_missing",
|
|
406
|
+
severity: "context",
|
|
407
|
+
detail: branchEligibility.evidence === "missing"
|
|
408
|
+
? "Branch eligibility evidence is missing; refresh branch/base metadata before relying on linked-issue assumptions."
|
|
409
|
+
: "Branch eligibility is unknown; refresh branch/base metadata before relying on linked-issue assumptions.",
|
|
410
|
+
},
|
|
411
|
+
]
|
|
412
|
+
: []),
|
|
413
|
+
...(!core.gates.baseTokenGatePassed
|
|
414
|
+
? [{ code: "base_token_gate", severity: "blocker", detail: "Source token score does not pass the current base-score token gate." }]
|
|
415
|
+
: []),
|
|
416
|
+
...(core.scoreEstimate.openPrMultiplier === 0
|
|
417
|
+
? [
|
|
418
|
+
{
|
|
419
|
+
code: "open_pr_threshold",
|
|
420
|
+
severity: "blocker",
|
|
421
|
+
detail: `Open PR count ${core.gates.openPrCount} exceeds threshold ${core.gates.openPrThreshold}.`,
|
|
422
|
+
},
|
|
423
|
+
]
|
|
424
|
+
: []),
|
|
425
|
+
...(core.scoreEstimate.openIssueMultiplier === 0
|
|
426
|
+
? [
|
|
427
|
+
{
|
|
428
|
+
code: "open_issue_threshold",
|
|
429
|
+
severity: "blocker",
|
|
430
|
+
detail: `Open issue count ${core.gates.openIssueCount} exceeds spam threshold ${core.gates.openIssueThreshold}.`,
|
|
431
|
+
},
|
|
432
|
+
]
|
|
433
|
+
: []),
|
|
434
|
+
...(core.scoreEstimate.mergedHistoryMultiplier === 0
|
|
435
|
+
? [
|
|
436
|
+
{
|
|
437
|
+
code: "merged_pr_history_floor",
|
|
438
|
+
severity: "blocker",
|
|
439
|
+
detail: `Merged PR count ${core.gates.mergedPullRequests} is below upstream floor ${core.gates.mergedPrFloor}.`,
|
|
440
|
+
},
|
|
441
|
+
]
|
|
442
|
+
: []),
|
|
443
|
+
...(core.scoreEstimate.issueDiscoveryHistoryMultiplier === 0
|
|
444
|
+
? [
|
|
445
|
+
{
|
|
446
|
+
code: "issue_discovery_validity_floor",
|
|
447
|
+
severity: "blocker",
|
|
448
|
+
detail: `Issue-discovery history (${core.gates.validSolvedIssues} valid solved, credibility ${roundScore(core.gates.issueCredibility)}) is below upstream floors (${core.gates.validSolvedIssuesFloor} valid solved, ${core.gates.issueCredibilityFloor} credibility).`,
|
|
449
|
+
},
|
|
450
|
+
]
|
|
451
|
+
: []),
|
|
452
|
+
...(core.gates.credibilityObserved < core.gates.credibilityFloor
|
|
453
|
+
? [
|
|
454
|
+
{
|
|
455
|
+
code: "credibility_floor",
|
|
456
|
+
severity: "reducer",
|
|
457
|
+
detail: `Credibility ${roundScore(core.gates.credibilityObserved)} is below floor ${core.gates.credibilityFloor}.`,
|
|
458
|
+
},
|
|
459
|
+
]
|
|
460
|
+
: []),
|
|
461
|
+
...(core.scoreEstimate.reviewPenaltyMultiplier < 1
|
|
462
|
+
? [{ code: "review_penalty", severity: "reducer", detail: "Change-request history reduces the estimate." }]
|
|
463
|
+
: []),
|
|
464
|
+
...(core.linkedIssueMultiplier.mode === "standard" && core.linkedIssueMultiplier.status === "invalid"
|
|
465
|
+
? [
|
|
466
|
+
{
|
|
467
|
+
code: "linked_issue_invalid",
|
|
468
|
+
severity: "reducer",
|
|
469
|
+
detail: core.linkedIssueMultiplier.reason,
|
|
470
|
+
},
|
|
471
|
+
]
|
|
472
|
+
: []),
|
|
473
|
+
...(core.linkedIssueMultiplier.mode === "standard" && ["raw", "plausible", "unavailable"].includes(core.linkedIssueMultiplier.status)
|
|
474
|
+
? [
|
|
475
|
+
{
|
|
476
|
+
code: "linked_issue_unvalidated",
|
|
477
|
+
severity: "context",
|
|
478
|
+
detail: core.linkedIssueMultiplier.reason,
|
|
479
|
+
},
|
|
480
|
+
]
|
|
481
|
+
: []),
|
|
482
|
+
...(nonNegative(input.observedStalePrCount) > 0
|
|
483
|
+
? [
|
|
484
|
+
{
|
|
485
|
+
code: "stale_work",
|
|
486
|
+
severity: "reducer",
|
|
487
|
+
detail: `${nonNegative(input.observedStalePrCount)} stale open PR(s) detected; consider closing stale work before opening new contributions.`,
|
|
488
|
+
},
|
|
489
|
+
]
|
|
490
|
+
: []),
|
|
491
|
+
...(nonNegative(input.duplicateRiskCount) > 0
|
|
492
|
+
? [
|
|
493
|
+
{
|
|
494
|
+
code: "duplicate_risk",
|
|
495
|
+
severity: "reducer",
|
|
496
|
+
detail: `${nonNegative(input.duplicateRiskCount)} duplicate-risk issue(s) or PR(s) detected; verify there is no conflicting work before proceeding.`,
|
|
497
|
+
},
|
|
498
|
+
]
|
|
499
|
+
: []),
|
|
500
|
+
];
|
|
501
|
+
}
|
|
502
|
+
function buildGateDeltas(current, scenarios) {
|
|
503
|
+
const currentScenario = scenarios[0];
|
|
504
|
+
/* v8 ignore next -- buildScenarioPreviews always emits a current scenario; this protects malformed adapters. */
|
|
505
|
+
if (!currentScenario)
|
|
506
|
+
return [];
|
|
507
|
+
const bestMatch = scenarios.find((scenarioPreview) => scenarioPreview.name === "bestReasonableCase");
|
|
508
|
+
/* v8 ignore next -- buildScenarioPreviews always emits bestReasonableCase; current is the defensive fallback. */
|
|
509
|
+
const best = bestMatch ?? currentScenario;
|
|
510
|
+
const linkedMatch = scenarios.find((scenarioPreview) => scenarioPreview.name === "linkedIssueFixed");
|
|
511
|
+
/* v8 ignore next -- buildScenarioPreviews always emits linkedIssueFixed; best is the defensive fallback. */
|
|
512
|
+
const linked = linkedMatch ?? best;
|
|
513
|
+
return [
|
|
514
|
+
...(current.scoreEstimate.openPrMultiplier !== best.scoreEstimate.openPrMultiplier || current.gates.openPrCount !== best.gates.openPrCount
|
|
515
|
+
? [
|
|
516
|
+
{
|
|
517
|
+
gate: "open_pr_threshold",
|
|
518
|
+
current: `${current.gates.openPrCount}/${current.gates.openPrThreshold} open PRs, multiplier ${current.scoreEstimate.openPrMultiplier}`,
|
|
519
|
+
projected: `${best.gates.openPrCount}/${best.gates.openPrThreshold} open PRs, multiplier ${best.scoreEstimate.openPrMultiplier}`,
|
|
520
|
+
explanation: `Open PR pressure changes estimated score ${current.scoreEstimate.estimatedMergedScore} -> ${best.scoreEstimate.estimatedMergedScore}.`,
|
|
521
|
+
},
|
|
522
|
+
]
|
|
523
|
+
: []),
|
|
524
|
+
...(current.scoreEstimate.openIssueMultiplier !== best.scoreEstimate.openIssueMultiplier || current.gates.openIssueCount !== best.gates.openIssueCount
|
|
525
|
+
? [
|
|
526
|
+
{
|
|
527
|
+
gate: "open_issue_threshold",
|
|
528
|
+
current: `${current.gates.openIssueCount}/${current.gates.openIssueThreshold} open issues, multiplier ${current.scoreEstimate.openIssueMultiplier}`,
|
|
529
|
+
projected: `${best.gates.openIssueCount}/${best.gates.openIssueThreshold} open issues, multiplier ${best.scoreEstimate.openIssueMultiplier}`,
|
|
530
|
+
explanation: `Open issue spam pressure changes estimated score ${current.scoreEstimate.estimatedMergedScore} -> ${best.scoreEstimate.estimatedMergedScore}.`,
|
|
531
|
+
},
|
|
532
|
+
]
|
|
533
|
+
: []),
|
|
534
|
+
...(current.scoreEstimate.mergedHistoryMultiplier !== best.scoreEstimate.mergedHistoryMultiplier
|
|
535
|
+
? [
|
|
536
|
+
{
|
|
537
|
+
gate: "merged_pr_history_floor",
|
|
538
|
+
current: `${current.gates.mergedPullRequests}/${current.gates.mergedPrFloor} merged PRs, multiplier ${current.scoreEstimate.mergedHistoryMultiplier}`,
|
|
539
|
+
projected: `${best.gates.mergedPullRequests}/${best.gates.mergedPrFloor} merged PRs, multiplier ${best.scoreEstimate.mergedHistoryMultiplier}`,
|
|
540
|
+
explanation: `Merged PR history changes estimated score ${current.scoreEstimate.estimatedMergedScore} -> ${best.scoreEstimate.estimatedMergedScore}.`,
|
|
541
|
+
},
|
|
542
|
+
]
|
|
543
|
+
: []),
|
|
544
|
+
...(current.scoreEstimate.issueDiscoveryHistoryMultiplier !== best.scoreEstimate.issueDiscoveryHistoryMultiplier
|
|
545
|
+
? [
|
|
546
|
+
{
|
|
547
|
+
gate: "issue_discovery_validity_floor",
|
|
548
|
+
current: `${current.gates.validSolvedIssues} valid solved / ${roundScore(current.gates.issueCredibility)} credibility, multiplier ${current.scoreEstimate.issueDiscoveryHistoryMultiplier}`,
|
|
549
|
+
projected: `${best.gates.validSolvedIssues} valid solved / ${roundScore(best.gates.issueCredibility)} credibility, multiplier ${best.scoreEstimate.issueDiscoveryHistoryMultiplier}`,
|
|
550
|
+
explanation: `Issue-discovery validity changes estimated score ${current.scoreEstimate.estimatedMergedScore} -> ${best.scoreEstimate.estimatedMergedScore}.`,
|
|
551
|
+
},
|
|
552
|
+
]
|
|
553
|
+
: []),
|
|
554
|
+
...(current.gates.credibilityObserved !== best.gates.credibilityObserved || current.scoreEstimate.credibilityMultiplier !== best.scoreEstimate.credibilityMultiplier
|
|
555
|
+
? [
|
|
556
|
+
{
|
|
557
|
+
gate: "credibility_floor",
|
|
558
|
+
current: `${roundScore(current.gates.credibilityObserved)} observed, multiplier ${current.scoreEstimate.credibilityMultiplier}`,
|
|
559
|
+
projected: `${roundScore(best.gates.credibilityObserved)} projected, multiplier ${best.scoreEstimate.credibilityMultiplier}`,
|
|
560
|
+
explanation: `Credibility changes estimated score ${current.scoreEstimate.estimatedMergedScore} -> ${best.scoreEstimate.estimatedMergedScore}.`,
|
|
561
|
+
},
|
|
562
|
+
]
|
|
563
|
+
: []),
|
|
564
|
+
...(current.scoreEstimate.issueMultiplier !== linked.scoreEstimate.issueMultiplier
|
|
565
|
+
? [
|
|
566
|
+
{
|
|
567
|
+
gate: "linked_issue_multiplier",
|
|
568
|
+
current: `${current.scoreEstimate.issueMultiplier}`,
|
|
569
|
+
projected: `${linked.scoreEstimate.issueMultiplier}`,
|
|
570
|
+
explanation: `Linked issue/no-issue context changes estimated score ${current.scoreEstimate.estimatedMergedScore} -> ${linked.scoreEstimate.estimatedMergedScore}.`,
|
|
571
|
+
},
|
|
572
|
+
]
|
|
573
|
+
: []),
|
|
574
|
+
];
|
|
575
|
+
}
|
|
576
|
+
function warningsFor(input, repo, core, branchEligibility = normalizeBranchEligibility(input)) {
|
|
577
|
+
return [...new Set([...blockedByFor(input, repo, core, branchEligibility).map((blocker) => blocker.detail), ...core.linkedIssueMultiplier.warnings])];
|
|
578
|
+
}
|
|
579
|
+
function statusFor(repo, blockedBy, effectiveEstimatedScore, scenarios) {
|
|
580
|
+
if (!repo?.isRegistered || blockedBy.some((blocker) => blocker.code === "inactive_allocation"))
|
|
581
|
+
return "hold";
|
|
582
|
+
if (effectiveEstimatedScore > 0 && !blockedBy.some((blocker) => blocker.severity === "blocker"))
|
|
583
|
+
return "scoreable";
|
|
584
|
+
if (scenarios.some((scenarioPreview) => scenarioPreview.name !== "current" && scenarioPreview.effectiveEstimatedScore > effectiveEstimatedScore)) {
|
|
585
|
+
return "conditionally_scoreable";
|
|
586
|
+
}
|
|
587
|
+
return "blocked";
|
|
588
|
+
}
|
|
589
|
+
function deltaExplanationFor(core, blockedBy) {
|
|
590
|
+
if (blockedBy.length === 0)
|
|
591
|
+
return `Currently scoreable at ${core.scoreEstimate.estimatedMergedScore}; underlying potential ${core.scoreEstimate.pendingSaturationScore}.`;
|
|
592
|
+
return `Effective score ${core.scoreEstimate.estimatedMergedScore}; underlying potential ${core.scoreEstimate.pendingSaturationScore}; blocked or reduced by ${blockedBy.map((blocker) => blocker.code).join(", ")}.`;
|
|
593
|
+
}
|
|
594
|
+
// A label multiplier must be a positive, finite number (mirrors the validity rule signals/engine.ts's own
|
|
595
|
+
// config-quality check already documents and enforces for its ADVISORY health score: "0, negative, NaN, or
|
|
596
|
+
// Infinity are config errors that would silently misweight scoring"). That check only ever adjusted a
|
|
597
|
+
// repo's informational config-quality score -- it never stopped an invalid value from reaching the REAL
|
|
598
|
+
// scoring formula here, where `labelMultiplier` multiplies directly into `estimatedMergedScore`. A
|
|
599
|
+
// registry-sourced label multiplier of 0 or a negative number is valid JSON and passed neither `numberValue`
|
|
600
|
+
// (only applied to the scalar overrides, not this map) nor any check in this function, so it would zero out
|
|
601
|
+
// or invert any PR/issue score for a matching label. Filtering here closes the gap where it actually matters.
|
|
602
|
+
function isValidLabelMultiplier(value) {
|
|
603
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
604
|
+
}
|
|
605
|
+
function selectLabelMultiplier(labels, multipliers, fallback) {
|
|
606
|
+
const normalized = labels.map((label) => label.toLowerCase());
|
|
607
|
+
const matched = Object.entries(multipliers).flatMap(([pattern, multiplier]) => {
|
|
608
|
+
if (!isValidLabelMultiplier(multiplier))
|
|
609
|
+
return [];
|
|
610
|
+
const matcher = labelPatternToRegExp(pattern.toLowerCase());
|
|
611
|
+
return normalized.some((label) => matcher.test(label)) ? [multiplier] : [];
|
|
612
|
+
});
|
|
613
|
+
return matched.length > 0 ? Math.max(...matched) : isValidLabelMultiplier(fallback) ? fallback : 1;
|
|
614
|
+
}
|
|
615
|
+
/** True when `label` matches the configured multiplier `pattern` under the SAME case-insensitive fnmatch glob
|
|
616
|
+
* semantics scoring uses to resolve label multipliers (see {@link labelPatternToRegExp}). Exported so the
|
|
617
|
+
* signals surfaces that audit configured label keys (config-quality, label-audit) match them as the GLOBS they
|
|
618
|
+
* are: a `type:*` key must count `type:bug-fix` as observed/configured, not silently report it missing because
|
|
619
|
+
* the literal pattern never appears verbatim on a real issue/PR. A literal key (no glob metacharacter) still
|
|
620
|
+
* matches only its exact label, so existing configs behave identically. */
|
|
621
|
+
export function labelMatchesPattern(label, pattern) {
|
|
622
|
+
return labelPatternToRegExp(pattern.toLowerCase()).test(label.toLowerCase());
|
|
623
|
+
}
|
|
624
|
+
// Compiled fnmatch→RegExp matchers are memoized by pattern. The same small,
|
|
625
|
+
// config-derived set of label keys is matched on every scored PR/issue, so the
|
|
626
|
+
// per-call recompile inside the nested label loops in engine.ts is pure waste.
|
|
627
|
+
// Keys come from a repo's registryConfig.labelMultipliers, sourced from the externally-fetched gittensor
|
|
628
|
+
// registry (registry/sync.ts + registry/normalize.ts, not a value this repo's own maintainer directly controls
|
|
629
|
+
// via .gittensory.yml) — so the pattern SET is small per repo, but individual pattern CONTENT is untrusted, not
|
|
630
|
+
// literally attacker-supplied-per-request the way GitHub PR content is. The wildcard-count cap below (#2456)
|
|
631
|
+
// bounds a single pattern's compile cost; this cache is additionally bounded to a fixed max entry count and
|
|
632
|
+
// evicted LRU, so a long-running isolate that observes many distinct registry snapshots over its life still
|
|
633
|
+
// can't grow the cache unboundedly. The compiled RegExp carries only the "i" flag (no global/sticky `lastIndex`
|
|
634
|
+
// state), so sharing one instance across calls is safe and byte-identical to recompiling on every call.
|
|
635
|
+
export const LABEL_PATTERN_REGEXP_CACHE_MAX_ENTRIES = 256;
|
|
636
|
+
const labelPatternRegExpCache = new Map();
|
|
637
|
+
// A RegExp that never matches any input — mirrors change-guardrail.ts's identical NEVER_MATCHES fallback for an
|
|
638
|
+
// over-complex pattern, so a pathological registry entry degrades to "this label multiplier never applies"
|
|
639
|
+
// instead of hanging the scoring path that evaluates it.
|
|
640
|
+
const LABEL_PATTERN_NEVER_MATCHES = /^(?!)$/;
|
|
641
|
+
// Upstream resolves label multipliers by matching each configured key as a Python `fnmatch` GLOB, not a
|
|
642
|
+
// literal string: `fnmatch(label.lower(), pattern.lower())` in
|
|
643
|
+
// gittensor/validator/oss_contributions/label_resolution.py, so a repo can configure `type:*`, `kind/*`, or
|
|
644
|
+
// `priority:?` and have it match `type:bug-fix`, `kind/bug`, `priority:1` (#1244-class scoring parity). The
|
|
645
|
+
// preview previously did exact equality, so it silently scored every wildcard-configured trusted label at the
|
|
646
|
+
// neutral default — under-/over-estimating the score for any repo using glob keys. Translate one fnmatch
|
|
647
|
+
// pattern to an anchored, case-insensitive RegExp. fnmatch semantics differ from the path-glob in
|
|
648
|
+
// change-guardrail.ts (there `*` stops at `/` and `?` is literal): labels are flat strings, so `*` matches any
|
|
649
|
+
// run, `?` any single character, and `[seq]`/`[!seq]` a character class. Literal keys are unaffected — for a
|
|
650
|
+
// pattern with no glob metacharacter the RegExp is an exact match, so existing configs score identically.
|
|
651
|
+
function labelPatternToRegExp(pattern) {
|
|
652
|
+
const cached = labelPatternRegExpCache.get(pattern);
|
|
653
|
+
if (cached !== undefined) {
|
|
654
|
+
// Refresh recency on hit so the cache behaves as an LRU: the most-recently-matched patterns
|
|
655
|
+
// survive eviction, not just the most-recently-inserted ones.
|
|
656
|
+
labelPatternRegExpCache.delete(pattern);
|
|
657
|
+
labelPatternRegExpCache.set(pattern, cached);
|
|
658
|
+
return cached;
|
|
659
|
+
}
|
|
660
|
+
// Reuses change-guardrail.ts's wildcard-GROUP counting (a `*` here matches the same "any run of chars"
|
|
661
|
+
// semantics as that glob compiler's `*`, so the same catastrophic-backtracking risk and the same empirically-
|
|
662
|
+
// safe threshold apply) — an over-complex registry-sourced label_multipliers key degrades to a safe never-match
|
|
663
|
+
// instead of hanging RegExp.test() on an adversarial near-miss label (#2456). Reachable via the public
|
|
664
|
+
// score-preview API, the MCP tool, and the per-PR label-audit signal, so one bad registry entry could otherwise
|
|
665
|
+
// hang scoring for every PR on that repo.
|
|
666
|
+
if (hasUnsafeWildcardCount(pattern)) {
|
|
667
|
+
setLabelPatternRegExpCacheEntry(pattern, LABEL_PATTERN_NEVER_MATCHES);
|
|
668
|
+
return LABEL_PATTERN_NEVER_MATCHES;
|
|
669
|
+
}
|
|
670
|
+
let regex = "";
|
|
671
|
+
let i = 0;
|
|
672
|
+
while (i < pattern.length) {
|
|
673
|
+
const char = pattern.charAt(i);
|
|
674
|
+
i += 1;
|
|
675
|
+
if (char === "*") {
|
|
676
|
+
regex += ".*";
|
|
677
|
+
}
|
|
678
|
+
else if (char === "?") {
|
|
679
|
+
regex += ".";
|
|
680
|
+
}
|
|
681
|
+
else if (char === "[") {
|
|
682
|
+
const close = pattern.indexOf("]", i);
|
|
683
|
+
if (close === -1) {
|
|
684
|
+
// No closing bracket: fnmatch treats the `[` as a literal character.
|
|
685
|
+
regex += "\\[";
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
const rawBody = pattern.slice(i, close);
|
|
689
|
+
if (rawBody === "" || rawBody === "!") {
|
|
690
|
+
// Empty classes and bare `[!]` stay literal in Python fnmatch instead of compiling as classes.
|
|
691
|
+
regex += `\\[${escapeRegExpLiteral(rawBody)}\\]`;
|
|
692
|
+
}
|
|
693
|
+
else if (hasDescendingCharacterRange(rawBody)) {
|
|
694
|
+
// Python fnmatch treats invalid ranges like `[z-a]` as a never-match pattern; RegExp throws.
|
|
695
|
+
regex += "(?!)";
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
let body = rawBody.replace(/\\/g, "\\\\");
|
|
699
|
+
// `[!seq]` is fnmatch's negated class; RegExp spells negation as `[^seq]`.
|
|
700
|
+
if (body.startsWith("!"))
|
|
701
|
+
body = `^${body.slice(1)}`;
|
|
702
|
+
else if (body.startsWith("^"))
|
|
703
|
+
body = `\\${body}`;
|
|
704
|
+
regex += `[${body}]`;
|
|
705
|
+
}
|
|
706
|
+
i = close + 1;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
else if (/[.+^${}()|\]\\]/.test(char)) {
|
|
710
|
+
regex += `\\${char}`;
|
|
711
|
+
}
|
|
712
|
+
else {
|
|
713
|
+
regex += char;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
const compiled = new RegExp(`^${regex}$`, "i");
|
|
717
|
+
setLabelPatternRegExpCacheEntry(pattern, compiled);
|
|
718
|
+
return compiled;
|
|
719
|
+
}
|
|
720
|
+
// Inserts a new (never-before-cached) entry, evicting the least-recently-used entry first if the
|
|
721
|
+
// cache is already at its bound. Callers must only use this for keys not already present — refreshing
|
|
722
|
+
// an existing key's recency on a cache hit is handled inline above via delete+set.
|
|
723
|
+
function setLabelPatternRegExpCacheEntry(pattern, compiled) {
|
|
724
|
+
if (labelPatternRegExpCache.size >= LABEL_PATTERN_REGEXP_CACHE_MAX_ENTRIES) {
|
|
725
|
+
// Map iteration order is insertion order, so the first key is always the least-recently-used
|
|
726
|
+
// one (recency is refreshed via delete+set on every hit/insert). The map is non-empty here
|
|
727
|
+
// because size >= LABEL_PATTERN_REGEXP_CACHE_MAX_ENTRIES (a positive constant), so the loop body
|
|
728
|
+
// always runs exactly once.
|
|
729
|
+
for (const oldestPattern of labelPatternRegExpCache.keys()) {
|
|
730
|
+
labelPatternRegExpCache.delete(oldestPattern);
|
|
731
|
+
break;
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
labelPatternRegExpCache.set(pattern, compiled);
|
|
735
|
+
}
|
|
736
|
+
export function clearLabelPatternRegExpCacheForTest() {
|
|
737
|
+
labelPatternRegExpCache.clear();
|
|
738
|
+
}
|
|
739
|
+
export function labelPatternRegExpCacheKeysForTest() {
|
|
740
|
+
return [...labelPatternRegExpCache.keys()];
|
|
741
|
+
}
|
|
742
|
+
function escapeRegExpLiteral(value) {
|
|
743
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
744
|
+
}
|
|
745
|
+
function hasDescendingCharacterRange(body) {
|
|
746
|
+
const start = body.startsWith("!") ? 1 : 0;
|
|
747
|
+
// Walk the class left-to-right, consuming each `X-Y` range as a unit so a range endpoint can't be
|
|
748
|
+
// misread as the start of a spurious second range. Only a genuinely inverted range like `[z-a]` — the
|
|
749
|
+
// case JS `RegExp` actually throws on — must degrade the class to never-match; a literal `-` that
|
|
750
|
+
// follows a completed range (as in `[a-z-9]`, a valid class) must NOT be suppressed. The prior scan
|
|
751
|
+
// flagged any `-` whose left neighbor outranked its right neighbor, so it wrongly killed `[a-z-9]`.
|
|
752
|
+
let i = start;
|
|
753
|
+
while (i < body.length) {
|
|
754
|
+
if (i + 2 < body.length && body.charAt(i + 1) === "-") {
|
|
755
|
+
if (body.charCodeAt(i) > body.charCodeAt(i + 2))
|
|
756
|
+
return true;
|
|
757
|
+
i += 3;
|
|
758
|
+
}
|
|
759
|
+
else {
|
|
760
|
+
i += 1;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
return false;
|
|
764
|
+
}
|
|
765
|
+
function decideLinkedIssueMultiplier(mode, context, constants, branchEligibility) {
|
|
766
|
+
const baseMultiplier = selectIssueMultiplier(mode, constants);
|
|
767
|
+
const issueNumbers = uniquePositiveInts(context?.issueNumbers ?? []);
|
|
768
|
+
const solvedByPullRequests = uniquePositiveInts(context?.solvedByPullRequests ?? []);
|
|
769
|
+
if (mode === "none") {
|
|
770
|
+
return {
|
|
771
|
+
mode,
|
|
772
|
+
status: "not_required",
|
|
773
|
+
source: "none",
|
|
774
|
+
eligible: false,
|
|
775
|
+
issueNumbers,
|
|
776
|
+
solvedByPullRequests,
|
|
777
|
+
baseMultiplier,
|
|
778
|
+
appliedMultiplier: 1,
|
|
779
|
+
reason: "No linked-issue multiplier mode was requested.",
|
|
780
|
+
warnings: [],
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
if (mode === "maintainer") {
|
|
784
|
+
return {
|
|
785
|
+
mode,
|
|
786
|
+
status: "not_required",
|
|
787
|
+
source: context?.source ?? "none",
|
|
788
|
+
eligible: true,
|
|
789
|
+
issueNumbers,
|
|
790
|
+
solvedByPullRequests,
|
|
791
|
+
baseMultiplier,
|
|
792
|
+
appliedMultiplier: baseMultiplier,
|
|
793
|
+
reason: "Maintainer-lane multiplier does not require solved-by-PR issue linkage.",
|
|
794
|
+
warnings: context?.warnings ?? [],
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
const projectedSolvedByPullRequestValidation = context?.[PROJECTED_SOLVED_BY_PULL_REQUEST_VALIDATION] === true;
|
|
798
|
+
const requestedStatus = context?.status ?? (solvedByPullRequests.length > 0 ? "validated" : issueNumbers.length > 0 ? "raw" : "unavailable");
|
|
799
|
+
const hasSolvedByPullRequestEvidence = solvedByPullRequests.length > 0 || projectedSolvedByPullRequestValidation;
|
|
800
|
+
const status = requestedStatus === "validated" && !hasSolvedByPullRequestEvidence ? (issueNumbers.length > 0 ? "raw" : "unavailable") : requestedStatus;
|
|
801
|
+
const source = context?.source ?? (status === "unavailable" ? "missing" : "user_supplied");
|
|
802
|
+
const branchEligible = isConfirmedBranchEligible(branchEligibility);
|
|
803
|
+
const eligible = status === "validated" && hasSolvedByPullRequestEvidence && branchEligible;
|
|
804
|
+
const reason = branchEligible || status !== "validated"
|
|
805
|
+
? status === requestedStatus
|
|
806
|
+
? context?.reason ?? linkedIssueReason(status, source, issueNumbers, solvedByPullRequests)
|
|
807
|
+
: linkedIssueReason(status, source, issueNumbers, solvedByPullRequests)
|
|
808
|
+
: branchEligibilityFailureReason(branchEligibility);
|
|
809
|
+
return {
|
|
810
|
+
mode,
|
|
811
|
+
status,
|
|
812
|
+
source,
|
|
813
|
+
eligible,
|
|
814
|
+
issueNumbers,
|
|
815
|
+
solvedByPullRequests,
|
|
816
|
+
baseMultiplier,
|
|
817
|
+
appliedMultiplier: eligible ? baseMultiplier : 1,
|
|
818
|
+
reason,
|
|
819
|
+
warnings: [...new Set([...linkedIssueWarnings(status), ...branchEligibility.warnings, ...(context?.warnings ?? [])])],
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
function isConfirmedBranchEligible(branchEligibility) {
|
|
823
|
+
return !branchEligibility.required || (branchEligibility.status === "eligible" && branchEligibility.evidence === "provided" && !branchEligibility.stale);
|
|
824
|
+
}
|
|
825
|
+
function branchEligibilityFailureReason(branchEligibility) {
|
|
826
|
+
if (branchEligibility.status === "ineligible")
|
|
827
|
+
return "Branch eligibility is confirmed ineligible; standard issue multiplier is not applied.";
|
|
828
|
+
if (branchEligibility.evidence === "missing")
|
|
829
|
+
return "Branch eligibility evidence is missing; standard issue multiplier is not applied.";
|
|
830
|
+
if (branchEligibility.status === "unknown")
|
|
831
|
+
return "Branch eligibility is unknown; standard issue multiplier is not applied.";
|
|
832
|
+
if (branchEligibility.stale)
|
|
833
|
+
return "Branch eligibility evidence is stale; standard issue multiplier is not applied.";
|
|
834
|
+
if (branchEligibility.source === "user_supplied")
|
|
835
|
+
return "Branch eligibility evidence is user-supplied; standard issue multiplier is not applied until verified metadata is available.";
|
|
836
|
+
return "Branch eligibility is not confirmed; standard issue multiplier is not applied.";
|
|
837
|
+
}
|
|
838
|
+
function withValidatedLinkedIssueScenario(input) {
|
|
839
|
+
const mode = input.linkedIssueMode ?? "none";
|
|
840
|
+
if (mode === "maintainer")
|
|
841
|
+
return input;
|
|
842
|
+
const issueNumbers = uniquePositiveInts(input.linkedIssueContext?.issueNumbers ?? []);
|
|
843
|
+
const solvedByPullRequests = uniquePositiveInts(input.linkedIssueContext?.solvedByPullRequests ?? []);
|
|
844
|
+
const linkedIssueContext = {
|
|
845
|
+
...input.linkedIssueContext,
|
|
846
|
+
status: "validated",
|
|
847
|
+
source: input.linkedIssueContext?.source ?? "user_supplied",
|
|
848
|
+
issueNumbers,
|
|
849
|
+
solvedByPullRequests,
|
|
850
|
+
warnings: [],
|
|
851
|
+
[PROJECTED_SOLVED_BY_PULL_REQUEST_VALIDATION]: true,
|
|
852
|
+
};
|
|
853
|
+
return {
|
|
854
|
+
...input,
|
|
855
|
+
linkedIssueMode: "standard",
|
|
856
|
+
linkedIssueContext,
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Project the standard linked-issue multiplier decision under the assumption that a planned PR
|
|
861
|
+
* becomes the merged solver of the given issue(s). Reuses {@link decideLinkedIssueMultiplier} — the
|
|
862
|
+
* same eligibility rule used by buildScorePreview — so standalone validators stay consistent with
|
|
863
|
+
* the scoring engine. The numeric multiplier on the returned decision is private; callers that are
|
|
864
|
+
* public-safe should surface only `eligible`/`status`/`reason`.
|
|
865
|
+
*/
|
|
866
|
+
export function projectLinkedIssueMultiplierForPlannedSolve(issueNumbers) {
|
|
867
|
+
const branchEligibility = {
|
|
868
|
+
required: true,
|
|
869
|
+
status: "eligible",
|
|
870
|
+
evidence: "provided",
|
|
871
|
+
source: "user_supplied",
|
|
872
|
+
stale: false,
|
|
873
|
+
warnings: [],
|
|
874
|
+
};
|
|
875
|
+
const context = {
|
|
876
|
+
status: "validated",
|
|
877
|
+
source: "user_supplied",
|
|
878
|
+
issueNumbers: uniquePositiveInts(issueNumbers),
|
|
879
|
+
solvedByPullRequests: [],
|
|
880
|
+
warnings: [],
|
|
881
|
+
[PROJECTED_SOLVED_BY_PULL_REQUEST_VALIDATION]: true,
|
|
882
|
+
};
|
|
883
|
+
return decideLinkedIssueMultiplier("standard", context, {}, branchEligibility);
|
|
884
|
+
}
|
|
885
|
+
function linkedIssueReason(status, source, issueNumbers, solvedByPullRequests) {
|
|
886
|
+
const issues = issueNumbers.length > 0 ? ` for issue(s) ${issueNumbers.map((number) => `#${number}`).join(", ")}` : "";
|
|
887
|
+
if (status === "validated") {
|
|
888
|
+
const solvers = solvedByPullRequests.length > 0 ? ` via solved-by-PR ${solvedByPullRequests.map((number) => `#${number}`).join(", ")}` : "";
|
|
889
|
+
return `Linked issue context is solved-by-PR validated${issues}${solvers}.`;
|
|
890
|
+
}
|
|
891
|
+
if (status === "invalid")
|
|
892
|
+
return `Linked issue context is invalid${issues}; standard issue multiplier is not applied.`;
|
|
893
|
+
if (status === "plausible")
|
|
894
|
+
return `Linked issue context is plausible${issues}, but solved-by-PR validation is not available yet.`;
|
|
895
|
+
if (status === "unavailable")
|
|
896
|
+
return `Linked issue mirror/cache data is unavailable${issues}; standard issue multiplier is not applied until validation is available.`;
|
|
897
|
+
return `Raw linked issue reference${issues} has no solved-by-PR validation from ${source}.`;
|
|
898
|
+
}
|
|
899
|
+
function linkedIssueWarnings(status) {
|
|
900
|
+
if (status === "validated")
|
|
901
|
+
return [];
|
|
902
|
+
if (status === "invalid")
|
|
903
|
+
return ["Linked issue context is invalid; standard issue multiplier is not applied."];
|
|
904
|
+
if (status === "unavailable")
|
|
905
|
+
return ["Linked issue mirror/cache data is unavailable; standard issue multiplier is not applied until validation is available."];
|
|
906
|
+
if (status === "plausible")
|
|
907
|
+
return ["Linked issue context is plausible but not solved-by-PR validated; standard issue multiplier is not applied."];
|
|
908
|
+
return ["Raw linked issue reference has no solved-by-PR evidence; standard issue multiplier is not applied."];
|
|
909
|
+
}
|
|
910
|
+
function uniquePositiveInts(values) {
|
|
911
|
+
return [...new Set(values.filter((value) => Number.isInteger(value) && value > 0))].sort((left, right) => left - right);
|
|
912
|
+
}
|
|
913
|
+
function selectIssueMultiplier(mode, constants) {
|
|
914
|
+
if (mode === "maintainer")
|
|
915
|
+
return constant(constants, "MAINTAINER_ISSUE_MULTIPLIER");
|
|
916
|
+
if (mode === "standard")
|
|
917
|
+
return constant(constants, "STANDARD_ISSUE_MULTIPLIER");
|
|
918
|
+
return 1;
|
|
919
|
+
}
|
|
920
|
+
function normalizeBranchEligibility(input) {
|
|
921
|
+
const required = input.linkedIssueMode === "standard";
|
|
922
|
+
const supplied = input.branchEligibility;
|
|
923
|
+
if (!required) {
|
|
924
|
+
return {
|
|
925
|
+
required: false,
|
|
926
|
+
status: "not_required",
|
|
927
|
+
evidence: supplied ? "provided" : "missing",
|
|
928
|
+
source: supplied ? supplied.source ?? "user_supplied" : "missing",
|
|
929
|
+
reason: supplied?.reason,
|
|
930
|
+
checkedAt: supplied?.checkedAt,
|
|
931
|
+
stale: Boolean(supplied?.stale),
|
|
932
|
+
warnings: [],
|
|
933
|
+
};
|
|
934
|
+
}
|
|
935
|
+
if (!supplied) {
|
|
936
|
+
return {
|
|
937
|
+
required: true,
|
|
938
|
+
status: "unknown",
|
|
939
|
+
evidence: "missing",
|
|
940
|
+
source: "missing",
|
|
941
|
+
stale: false,
|
|
942
|
+
warnings: ["Branch eligibility evidence is missing; refresh branch/base metadata before relying on linked-issue assumptions."],
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
const status = supplied.status ?? "unknown";
|
|
946
|
+
const stale = Boolean(supplied.stale);
|
|
947
|
+
const warnings = [
|
|
948
|
+
...(status === "ineligible" ? ["Branch eligibility is confirmed ineligible; linked-issue multiplier assumptions are disabled."] : []),
|
|
949
|
+
...(status === "unknown" ? ["Branch eligibility is unknown; refresh branch/base metadata before relying on linked-issue assumptions."] : []),
|
|
950
|
+
...(stale ? ["Branch eligibility evidence is stale; refresh branch/base metadata before relying on linked-issue assumptions."] : []),
|
|
951
|
+
];
|
|
952
|
+
return {
|
|
953
|
+
required: true,
|
|
954
|
+
status,
|
|
955
|
+
evidence: "provided",
|
|
956
|
+
source: supplied.source ?? "user_supplied",
|
|
957
|
+
reason: supplied.reason,
|
|
958
|
+
checkedAt: supplied.checkedAt,
|
|
959
|
+
stale,
|
|
960
|
+
warnings,
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
function resolveMergedPullRequests(input, contributorEvidence) {
|
|
964
|
+
if (input.mergedPullRequests !== undefined)
|
|
965
|
+
return nonNegative(input.mergedPullRequests);
|
|
966
|
+
const fromEvidence = Number(contributorEvidence?.payload?.mergedPullRequests);
|
|
967
|
+
return Number.isFinite(fromEvidence) ? nonNegative(fromEvidence) : undefined;
|
|
968
|
+
}
|
|
969
|
+
function inferCredibility(evidence) {
|
|
970
|
+
const payload = evidence?.payload;
|
|
971
|
+
const merged = Number(payload?.mergedPullRequests ?? 0);
|
|
972
|
+
const stale = Number(payload?.stalePullRequests ?? 0);
|
|
973
|
+
const unlinked = Number(payload?.unlinkedPullRequests ?? 0);
|
|
974
|
+
// The payload is a loosely-typed cache (`Record<string, JsonValue>`), so any count can arrive as a
|
|
975
|
+
// non-numeric value. A non-finite `stale`/`unlinked` propagates NaN through the arithmetic below —
|
|
976
|
+
// and `clamp` cannot rescue NaN — poisoning the whole credibility multiplier and score, so guard all
|
|
977
|
+
// three counts, not just `merged`, falling back to the neutral credibility.
|
|
978
|
+
if (!Number.isFinite(merged) || !Number.isFinite(stale) || !Number.isFinite(unlinked))
|
|
979
|
+
return 0.8;
|
|
980
|
+
return clamp(0.75 + merged * 0.04 - stale * 0.03 - unlinked * 0.02, 0.25, 1);
|
|
981
|
+
}
|
|
982
|
+
function applyNonCodeLineCap(input, constants) {
|
|
983
|
+
const score = nonNegative(input.nonCodeTokenScore);
|
|
984
|
+
const lines = nonNegative(input.nonCodeLines);
|
|
985
|
+
if (score <= 0 || lines <= 0)
|
|
986
|
+
return score;
|
|
987
|
+
const maxLines = constant(constants, "MAX_LINES_SCORED_FOR_NON_CODE_EXT");
|
|
988
|
+
return lines <= maxLines ? score : score * (maxLines / lines);
|
|
989
|
+
}
|
|
990
|
+
function applyNonCodeCapToTotal(totalTokenScore, input, cappedNonCodeTokenScore) {
|
|
991
|
+
const total = nonNegative(totalTokenScore);
|
|
992
|
+
const nonCodeTokenScore = nonNegative(input.nonCodeTokenScore);
|
|
993
|
+
if (nonCodeTokenScore <= 0 || cappedNonCodeTokenScore >= nonCodeTokenScore)
|
|
994
|
+
return total;
|
|
995
|
+
return Math.max(0, total - (nonCodeTokenScore - cappedNonCodeTokenScore));
|
|
996
|
+
}
|
|
997
|
+
// Single source of truth (#812): the fallback for any constant is ALWAYS DEFAULT_SCORING_CONSTANTS — never
|
|
998
|
+
// a duplicated literal at the call site. The live `constants` (snapshot.constants, which already merges
|
|
999
|
+
// DEFAULT_SCORING_CONSTANTS with parsed upstream values) wins when present; otherwise the declared default
|
|
1000
|
+
// is used. This removes the duplicate-source-of-truth drift surface without changing any value (every
|
|
1001
|
+
// former call-site literal already matched its DEFAULT_SCORING_CONSTANTS entry).
|
|
1002
|
+
function constant(constants, key) {
|
|
1003
|
+
const value = constants[key];
|
|
1004
|
+
if (typeof value === "number" && Number.isFinite(value))
|
|
1005
|
+
return value;
|
|
1006
|
+
const fallback = DEFAULT_SCORING_CONSTANTS[key];
|
|
1007
|
+
/* v8 ignore next -- defensive: every recognized key is in DEFAULT_SCORING_CONSTANTS; this guards typos/forward-compat. */
|
|
1008
|
+
return typeof fallback === "number" && Number.isFinite(fallback) ? fallback : 0;
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Resolve a repo's time-decay curve: each parameter is the repo's per-repo override (from the registry's
|
|
1012
|
+
* `scoring.time_decay`) when present, else the global default constant from the live scoring snapshot.
|
|
1013
|
+
* Mirrors upstream's `resolve_time_decay` (RepoTimeDecayConfig overlaid on the module constants).
|
|
1014
|
+
*
|
|
1015
|
+
* Parity note (#1320): upstream coerces ONLY `grace_period_hours` to an integer
|
|
1016
|
+
* (`grace_period_hours=int(pick(...))`) while the three curve params stay floats. A maintainer may legally
|
|
1017
|
+
* configure a fractional grace (upstream validates `0 <= grace_period_hours <= 168`), so the resolved grace
|
|
1018
|
+
* must be truncated toward zero to match the validator — otherwise a PR aged between `trunc(grace)` and
|
|
1019
|
+
* `grace` is treated as fresh in the preview but already decaying upstream.
|
|
1020
|
+
*/
|
|
1021
|
+
export function resolveTimeDecay(constants, overrides) {
|
|
1022
|
+
return {
|
|
1023
|
+
gracePeriodHours: clampGracePeriodHours(Math.trunc(pickOverride(overrides?.gracePeriodHours, constant(constants, "TIME_DECAY_GRACE_PERIOD_HOURS")))),
|
|
1024
|
+
sigmoidMidpointDays: pickOverride(overrides?.sigmoidMidpointDays, constant(constants, "TIME_DECAY_SIGMOID_MIDPOINT")),
|
|
1025
|
+
sigmoidSteepness: pickOverride(overrides?.sigmoidSteepness, constant(constants, "TIME_DECAY_SIGMOID_STEEPNESS_SCALAR")),
|
|
1026
|
+
minMultiplier: clampMinMultiplier(pickOverride(overrides?.minMultiplier, constant(constants, "TIME_DECAY_MIN_MULTIPLIER"))),
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
// Documented bound (see the parity note above): upstream validates 0 <= grace_period_hours <= 168. The
|
|
1030
|
+
// override reaches this resolver via a bare Number.isFinite check (registry/normalize.ts's parseTimeDecayOverrides),
|
|
1031
|
+
// so an out-of-band value (negative, or beyond a week) would otherwise apply verbatim.
|
|
1032
|
+
const GRACE_PERIOD_HOURS_MIN = 0;
|
|
1033
|
+
const GRACE_PERIOD_HOURS_MAX = 168;
|
|
1034
|
+
function clampGracePeriodHours(value) {
|
|
1035
|
+
return clamp(value, GRACE_PERIOD_HOURS_MIN, GRACE_PERIOD_HOURS_MAX);
|
|
1036
|
+
}
|
|
1037
|
+
// minMultiplier is the sigmoid's floor (calculateTimeDecay: Math.max(sigmoid, minMultiplier)), and the
|
|
1038
|
+
// sigmoid itself is always in (0, 1). A per-repo override above 1 would floor every aged PR ABOVE a fresh
|
|
1039
|
+
// PR's multiplier -- inverting time decay into an age bonus -- and a negative override applied verbatim
|
|
1040
|
+
// today has no floor semantics at all. Bound to the sigmoid's own range so the floor invariant always holds.
|
|
1041
|
+
const MIN_MULTIPLIER_MIN = 0;
|
|
1042
|
+
const MIN_MULTIPLIER_MAX = 1;
|
|
1043
|
+
function clampMinMultiplier(value) {
|
|
1044
|
+
return clamp(value, MIN_MULTIPLIER_MIN, MIN_MULTIPLIER_MAX);
|
|
1045
|
+
}
|
|
1046
|
+
function pickOverride(value, fallback) {
|
|
1047
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* Upstream gittensor's sigmoid time-decay multiplier (#703), ported verbatim from the validator's
|
|
1051
|
+
* `calculate_time_decay` (gittensor/validator/utils/datetime_utils.py): for the first grace-period hours the
|
|
1052
|
+
* multiplier is exactly 1.0 (hard cutoff); after that it follows a logistic on days-since-merge centred at
|
|
1053
|
+
* the sigmoid midpoint (50% at that point), floored at the minimum multiplier. The curve params are
|
|
1054
|
+
* resolved PER-REPO (overrides ?? snapshot defaults), so each maintainer's registry hyperparameters apply.
|
|
1055
|
+
* Pure + deterministic.
|
|
1056
|
+
*/
|
|
1057
|
+
export function calculateTimeDecay(prAgeHours, constants, overrides) {
|
|
1058
|
+
const { gracePeriodHours, sigmoidMidpointDays, sigmoidSteepness, minMultiplier } = resolveTimeDecay(constants, overrides);
|
|
1059
|
+
if (!Number.isFinite(prAgeHours) || prAgeHours < gracePeriodHours)
|
|
1060
|
+
return 1;
|
|
1061
|
+
const days = prAgeHours / 24;
|
|
1062
|
+
const sigmoid = 1 / (1 + Math.exp(sigmoidSteepness * (days - sigmoidMidpointDays)));
|
|
1063
|
+
return Math.max(sigmoid, minMultiplier);
|
|
1064
|
+
}
|
|
1065
|
+
function saturationScore(sourceTokenScore, totalTokenScore, constants) {
|
|
1066
|
+
// SRC_TOK_SATURATION_SCALE is per-repo overridable only within [10, 500] upstream; a snapshot value outside
|
|
1067
|
+
// that band (a bad override, a parse glitch) would otherwise distort the saturation curve — at scale 1 the
|
|
1068
|
+
// component saturates almost immediately, well above the documented floor. Clamp to the documented range so
|
|
1069
|
+
// the curve stays within upstream bounds (the prior Math.max(...,1) only guarded the divide-by-zero edge).
|
|
1070
|
+
const scale = clampSaturationScale(constant(constants, "SRC_TOK_SATURATION_SCALE"));
|
|
1071
|
+
return (constant(constants, "MERGED_PR_BASE_SCORE") * (1 - Math.exp(-sourceTokenScore / scale)) +
|
|
1072
|
+
saturationContributionBonus(totalTokenScore, constants));
|
|
1073
|
+
}
|
|
1074
|
+
function saturationContributionBonus(totalTokenScore, constants) {
|
|
1075
|
+
return contributionBonusRamp(totalTokenScore, constants);
|
|
1076
|
+
}
|
|
1077
|
+
// Shared contribution-bonus ramp used by both scoring models so the saturation
|
|
1078
|
+
// and density bonuses cannot drift: clamp(totalTokenScore / FULL_BONUS, 0, 1)
|
|
1079
|
+
// scaled by MAX_CONTRIBUTION_BONUS (upstream default 5; single-sourced in model.ts, see #807/#812).
|
|
1080
|
+
function contributionBonusRamp(totalTokenScore, constants) {
|
|
1081
|
+
return (clamp(totalTokenScore / constant(constants, "CONTRIBUTION_SCORE_FOR_FULL_BONUS"), 0, 1) *
|
|
1082
|
+
constant(constants, "MAX_CONTRIBUTION_BONUS"));
|
|
1083
|
+
}
|
|
1084
|
+
function nonNegative(value) {
|
|
1085
|
+
/* v8 ignore next -- API schemas and local scorers normalize numeric preview inputs before this defensive fallback. */
|
|
1086
|
+
return Number.isFinite(value) ? Math.max(0, value ?? 0) : 0;
|
|
1087
|
+
}
|
|
1088
|
+
function clamp(value, min, max) {
|
|
1089
|
+
return Math.min(max, Math.max(min, value));
|
|
1090
|
+
}
|
|
1091
|
+
// Bounds documented by upstream for the two repo-configurable scoring inputs the preview consumes directly.
|
|
1092
|
+
const FIXED_BASE_SCORE_MIN = 0;
|
|
1093
|
+
const FIXED_BASE_SCORE_MAX = 100;
|
|
1094
|
+
const SRC_TOK_SATURATION_SCALE_MIN = 10;
|
|
1095
|
+
const SRC_TOK_SATURATION_SCALE_MAX = 500;
|
|
1096
|
+
// A repo's fixed_base_score override forces base_score to a constant within [0, 100]. The value reaches the
|
|
1097
|
+
// preview unbounded above — the API schema only enforces `.min(0)` and registry normalization accepts any
|
|
1098
|
+
// finite number — so a misconfigured 150 would otherwise mint a base score above the model ceiling. Clamp to
|
|
1099
|
+
// the documented range; a non-finite/absent value falls through to the token-derived base score.
|
|
1100
|
+
function clampFixedBaseScore(value) {
|
|
1101
|
+
return Number.isFinite(value) ? clamp(value, FIXED_BASE_SCORE_MIN, FIXED_BASE_SCORE_MAX) : undefined;
|
|
1102
|
+
}
|
|
1103
|
+
function clampSaturationScale(value) {
|
|
1104
|
+
return clamp(value, SRC_TOK_SATURATION_SCALE_MIN, SRC_TOK_SATURATION_SCALE_MAX);
|
|
1105
|
+
}
|
|
1106
|
+
function roundScore(value) {
|
|
1107
|
+
return Math.round(value * 10000) / 10000;
|
|
1108
|
+
}
|