@loopover/miner 0.1.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/DEPLOYMENT.md +156 -0
- package/Dockerfile +38 -0
- package/README.md +269 -0
- package/bin/gittensory-miner-mcp.d.ts +64 -0
- package/bin/gittensory-miner-mcp.js +279 -0
- package/bin/gittensory-miner.js +196 -0
- package/docs/coding-agent-driver.md +140 -0
- package/docs/config-precedence.md +96 -0
- package/docs/cross-repo-discovery-phase1.md +55 -0
- package/docs/discovery-index-contract.md +60 -0
- package/docs/discovery-plane-operator-guide.md +101 -0
- package/docs/env-reference.md +30 -0
- package/docs/fleet-run-manifest.md +50 -0
- package/docs/miner-goal-spec.md +76 -0
- package/docs/miner-selfimprove-calibration.md +105 -0
- package/docs/observability.md +60 -0
- package/docs/operations-runbook.md +209 -0
- package/docs/repo-agnostic-capability-audit.md +129 -0
- package/docs/unattended-scheduling.md +107 -0
- package/expected-engine.version +1 -0
- package/lib/ams-policy.d.ts +20 -0
- package/lib/ams-policy.js +68 -0
- package/lib/attempt-cli.d.ts +99 -0
- package/lib/attempt-cli.js +523 -0
- package/lib/attempt-input-builder.d.ts +24 -0
- package/lib/attempt-input-builder.js +91 -0
- package/lib/attempt-log.d.ts +37 -0
- package/lib/attempt-log.js +177 -0
- package/lib/attempt-runner.d.ts +59 -0
- package/lib/attempt-runner.js +164 -0
- package/lib/attempt-worktree.d.ts +31 -0
- package/lib/attempt-worktree.js +94 -0
- package/lib/calibration-cli.d.ts +1 -0
- package/lib/calibration-cli.js +92 -0
- package/lib/calibration-run.d.ts +161 -0
- package/lib/calibration-run.js +232 -0
- package/lib/calibration-types.d.ts +46 -0
- package/lib/calibration-types.js +74 -0
- package/lib/calibration.d.ts +24 -0
- package/lib/calibration.js +99 -0
- package/lib/ci-poller.d.ts +33 -0
- package/lib/ci-poller.js +234 -0
- package/lib/claim-adjudication.d.ts +21 -0
- package/lib/claim-adjudication.js +36 -0
- package/lib/claim-conflict-resolver.d.ts +29 -0
- package/lib/claim-conflict-resolver.js +101 -0
- package/lib/claim-ledger-cli.d.ts +59 -0
- package/lib/claim-ledger-cli.js +320 -0
- package/lib/claim-ledger-expiry.d.ts +20 -0
- package/lib/claim-ledger-expiry.js +41 -0
- package/lib/claim-ledger.d.ts +63 -0
- package/lib/claim-ledger.js +303 -0
- package/lib/cli-error.d.ts +3 -0
- package/lib/cli-error.js +27 -0
- package/lib/cli.d.ts +3 -0
- package/lib/cli.js +72 -0
- package/lib/coding-agent-construction.d.ts +17 -0
- package/lib/coding-agent-construction.js +103 -0
- package/lib/coding-agent-house-rules.d.ts +24 -0
- package/lib/coding-agent-house-rules.js +64 -0
- package/lib/coding-task-spec.d.ts +44 -0
- package/lib/coding-task-spec.js +183 -0
- package/lib/config-precedence.d.ts +25 -0
- package/lib/config-precedence.js +38 -0
- package/lib/deny-check.d.ts +11 -0
- package/lib/deny-check.js +76 -0
- package/lib/deny-hook-synthesis.d.ts +93 -0
- package/lib/deny-hook-synthesis.js +399 -0
- package/lib/deny-hooks.d.ts +28 -0
- package/lib/deny-hooks.js +155 -0
- package/lib/deployment-docs-audit.d.ts +41 -0
- package/lib/deployment-docs-audit.js +115 -0
- package/lib/discover-cli.d.ts +94 -0
- package/lib/discover-cli.js +284 -0
- package/lib/discovery-throttle.d.ts +8 -0
- package/lib/discovery-throttle.js +33 -0
- package/lib/env-file-indirection.d.ts +4 -0
- package/lib/env-file-indirection.js +45 -0
- package/lib/event-ledger-cli.d.ts +82 -0
- package/lib/event-ledger-cli.js +266 -0
- package/lib/event-ledger.d.ts +37 -0
- package/lib/event-ledger.js +210 -0
- package/lib/execute-local-write.d.ts +14 -0
- package/lib/execute-local-write.js +50 -0
- package/lib/feasibility-cli.d.ts +25 -0
- package/lib/feasibility-cli.js +80 -0
- package/lib/forge-config.d.ts +17 -0
- package/lib/forge-config.js +37 -0
- package/lib/governor-action-mode.d.ts +26 -0
- package/lib/governor-action-mode.js +47 -0
- package/lib/governor-chokepoint-persisted.d.ts +18 -0
- package/lib/governor-chokepoint-persisted.js +46 -0
- package/lib/governor-chokepoint.d.ts +14 -0
- package/lib/governor-chokepoint.js +52 -0
- package/lib/governor-kill-switch.d.ts +26 -0
- package/lib/governor-kill-switch.js +47 -0
- package/lib/governor-ledger-cli.d.ts +32 -0
- package/lib/governor-ledger-cli.js +155 -0
- package/lib/governor-ledger.d.ts +46 -0
- package/lib/governor-ledger.js +203 -0
- package/lib/governor-open-pr.d.ts +21 -0
- package/lib/governor-open-pr.js +27 -0
- package/lib/governor-pause-cli.d.ts +23 -0
- package/lib/governor-pause-cli.js +171 -0
- package/lib/governor-run-halt.d.ts +37 -0
- package/lib/governor-run-halt.js +58 -0
- package/lib/governor-state.d.ts +63 -0
- package/lib/governor-state.js +363 -0
- package/lib/governor-write-rate-limit.d.ts +30 -0
- package/lib/governor-write-rate-limit.js +64 -0
- package/lib/harness-submission-trigger.d.ts +69 -0
- package/lib/harness-submission-trigger.js +138 -0
- package/lib/http-retry.d.ts +12 -0
- package/lib/http-retry.js +52 -0
- package/lib/laptop-init.d.ts +53 -0
- package/lib/laptop-init.js +332 -0
- package/lib/live-issue-snapshot.d.ts +16 -0
- package/lib/live-issue-snapshot.js +117 -0
- package/lib/local-store.d.ts +18 -0
- package/lib/local-store.js +62 -0
- package/lib/logger.d.ts +58 -0
- package/lib/logger.js +167 -0
- package/lib/loop-cli.d.ts +66 -0
- package/lib/loop-cli.js +540 -0
- package/lib/loop-closure.d.ts +35 -0
- package/lib/loop-closure.js +66 -0
- package/lib/loop-reentry.d.ts +51 -0
- package/lib/loop-reentry.js +124 -0
- package/lib/manage-poll.d.ts +79 -0
- package/lib/manage-poll.js +254 -0
- package/lib/manage-status.d.ts +70 -0
- package/lib/manage-status.js +241 -0
- package/lib/metrics-cli.d.ts +9 -0
- package/lib/metrics-cli.js +50 -0
- package/lib/migrate-cli.d.ts +24 -0
- package/lib/migrate-cli.js +111 -0
- package/lib/miner-goal-spec.d.ts +12 -0
- package/lib/miner-goal-spec.js +67 -0
- package/lib/opportunity-fanout.d.ts +86 -0
- package/lib/opportunity-fanout.js +597 -0
- package/lib/opportunity-ranker.d.ts +36 -0
- package/lib/opportunity-ranker.js +118 -0
- package/lib/orb-export.d.ts +54 -0
- package/lib/orb-export.js +206 -0
- package/lib/plan-store-cli.d.ts +37 -0
- package/lib/plan-store-cli.js +151 -0
- package/lib/plan-store.d.ts +51 -0
- package/lib/plan-store.js +231 -0
- package/lib/policy-doc-cache.d.ts +25 -0
- package/lib/policy-doc-cache.js +79 -0
- package/lib/policy-verdict-cache.d.ts +35 -0
- package/lib/policy-verdict-cache.js +106 -0
- package/lib/portfolio-dashboard.d.ts +30 -0
- package/lib/portfolio-dashboard.js +105 -0
- package/lib/portfolio-discovery.d.ts +29 -0
- package/lib/portfolio-discovery.js +100 -0
- package/lib/portfolio-queue-cli.d.ts +87 -0
- package/lib/portfolio-queue-cli.js +509 -0
- package/lib/portfolio-queue-expiry.d.ts +20 -0
- package/lib/portfolio-queue-expiry.js +51 -0
- package/lib/portfolio-queue-manager.d.ts +50 -0
- package/lib/portfolio-queue-manager.js +134 -0
- package/lib/portfolio-queue.d.ts +62 -0
- package/lib/portfolio-queue.js +351 -0
- package/lib/pr-disposition-poller.d.ts +26 -0
- package/lib/pr-disposition-poller.js +163 -0
- package/lib/pr-number-parse.d.ts +4 -0
- package/lib/pr-number-parse.js +23 -0
- package/lib/pr-outcome.d.ts +41 -0
- package/lib/pr-outcome.js +90 -0
- package/lib/prediction-ledger.d.ts +47 -0
- package/lib/prediction-ledger.js +222 -0
- package/lib/pretooluse-hook.d.ts +31 -0
- package/lib/pretooluse-hook.js +93 -0
- package/lib/process-lifecycle.d.ts +31 -0
- package/lib/process-lifecycle.js +106 -0
- package/lib/purge-cli.d.ts +44 -0
- package/lib/purge-cli.js +198 -0
- package/lib/rejection-signal.d.ts +6 -0
- package/lib/rejection-signal.js +101 -0
- package/lib/rejection-state-machine.d.ts +37 -0
- package/lib/rejection-state-machine.js +81 -0
- package/lib/rejection-templates.d.ts +12 -0
- package/lib/rejection-templates.js +71 -0
- package/lib/replay-objective-anchor.d.ts +79 -0
- package/lib/replay-objective-anchor.js +179 -0
- package/lib/replay-snapshot.d.ts +41 -0
- package/lib/replay-snapshot.js +288 -0
- package/lib/replay-task-generation.d.ts +126 -0
- package/lib/replay-task-generation.js +223 -0
- package/lib/repo-clone.d.ts +19 -0
- package/lib/repo-clone.js +102 -0
- package/lib/run-state-cli.d.ts +27 -0
- package/lib/run-state-cli.js +154 -0
- package/lib/run-state.d.ts +37 -0
- package/lib/run-state.js +150 -0
- package/lib/schema-version.d.ts +17 -0
- package/lib/schema-version.js +71 -0
- package/lib/self-review-context.d.ts +28 -0
- package/lib/self-review-context.js +339 -0
- package/lib/slop-assessment.d.ts +3 -0
- package/lib/slop-assessment.js +20 -0
- package/lib/stack-detection.d.ts +41 -0
- package/lib/stack-detection.js +248 -0
- package/lib/status.d.ts +64 -0
- package/lib/status.js +441 -0
- package/lib/store-maintenance.d.ts +31 -0
- package/lib/store-maintenance.js +183 -0
- package/lib/submission-freshness-check.d.ts +32 -0
- package/lib/submission-freshness-check.js +93 -0
- package/lib/update-check.d.ts +36 -0
- package/lib/update-check.js +161 -0
- package/lib/version.d.ts +3 -0
- package/lib/version.js +10 -0
- package/lib/worktree-allocator.d.ts +39 -0
- package/lib/worktree-allocator.js +262 -0
- package/package.json +50 -0
- package/schema/miner-goal-spec.schema.json +111 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { resolveAiPolicyVerdict } from "@loopover/engine";
|
|
3
|
+
import { resolveForgeConfig } from "./forge-config.js";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_RATE_LIMIT_HIGH_WATER_MARK,
|
|
6
|
+
DEFAULT_RATE_LIMIT_LOW_WATER_MARK,
|
|
7
|
+
resolveThrottledConcurrency,
|
|
8
|
+
} from "./discovery-throttle.js";
|
|
9
|
+
import { fetchWithRetry } from "./http-retry.js";
|
|
10
|
+
|
|
11
|
+
const defaultConcurrency = 5;
|
|
12
|
+
// How long a parked worker waits before re-checking the live rate-limit-derived concurrency limit (#4844).
|
|
13
|
+
const throttleParkMs = 25;
|
|
14
|
+
const defaultPerPage = 100;
|
|
15
|
+
// Follow the GitHub Link header past the first page so a repo/search with >100 open issues isn't silently
|
|
16
|
+
// truncated (#4831); cap the follow loop so a pathological Link chain can't run away.
|
|
17
|
+
const defaultMaxPages = 10;
|
|
18
|
+
|
|
19
|
+
function normalizeLimit(value, fallback, min, max) {
|
|
20
|
+
if (!Number.isFinite(value)) return fallback;
|
|
21
|
+
return Math.min(max, Math.max(min, Math.floor(value)));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function targetKey(target) {
|
|
25
|
+
return `${target.owner.toLowerCase()}/${target.repo.toLowerCase()}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function normalizeTargets(targets) {
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
const normalized = [];
|
|
31
|
+
for (const target of Array.isArray(targets) ? targets : []) {
|
|
32
|
+
const owner = typeof target?.owner === "string" ? target.owner.trim() : "";
|
|
33
|
+
const repo = typeof target?.repo === "string" ? target.repo.trim() : "";
|
|
34
|
+
if (!owner || !repo) continue;
|
|
35
|
+
const key = targetKey({ owner, repo });
|
|
36
|
+
if (seen.has(key)) continue;
|
|
37
|
+
seen.add(key);
|
|
38
|
+
normalized.push({ owner, repo, repoFullName: `${owner}/${repo}` });
|
|
39
|
+
}
|
|
40
|
+
return normalized;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function targetFromFullName(fullName) {
|
|
44
|
+
if (typeof fullName !== "string") return null;
|
|
45
|
+
const [owner, repo, extra] = fullName.split("/");
|
|
46
|
+
if (!owner || !repo || extra) return null;
|
|
47
|
+
return { owner, repo, repoFullName: `${owner}/${repo}` };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function escapeRegExp(value) {
|
|
51
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Derive owner/repo from a search hit when `repository.full_name` is absent, using the tenant forge's own
|
|
55
|
+
// `repoPathPrefix` for the API `repository_url` and a forge-agnostic host for the web `html_url` (#4784). Hardcoding
|
|
56
|
+
// `/repos/` and `github.com` here dropped every custom-forge search result whose payload omitted `full_name`.
|
|
57
|
+
function targetFromSearchIssue(issue, forge) {
|
|
58
|
+
const repositoryFullName = targetFromFullName(issue?.repository?.full_name);
|
|
59
|
+
if (repositoryFullName) return repositoryFullName;
|
|
60
|
+
|
|
61
|
+
const repoPathPrefix = escapeRegExp(forge.repoPathPrefix.replace(/\/+$/, ""));
|
|
62
|
+
const repositoryUrl =
|
|
63
|
+
typeof issue?.repository_url === "string"
|
|
64
|
+
? issue.repository_url.match(new RegExp(`${repoPathPrefix}/([^/?#]+)/([^/?#]+)(?:[?#].*)?$`))
|
|
65
|
+
: null;
|
|
66
|
+
if (repositoryUrl) {
|
|
67
|
+
const owner = decodeURIComponent(repositoryUrl[1]);
|
|
68
|
+
const repo = decodeURIComponent(repositoryUrl[2]);
|
|
69
|
+
return { owner, repo, repoFullName: `${owner}/${repo}` };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const htmlUrl =
|
|
73
|
+
typeof issue?.html_url === "string"
|
|
74
|
+
? issue.html_url.match(/^https:\/\/[^/]+\/([^/]+)\/([^/]+)\/issues\/\d+(?:[?#].*)?$/)
|
|
75
|
+
: null;
|
|
76
|
+
if (htmlUrl) {
|
|
77
|
+
const owner = decodeURIComponent(htmlUrl[1]);
|
|
78
|
+
const repo = decodeURIComponent(htmlUrl[2]);
|
|
79
|
+
return { owner, repo, repoFullName: `${owner}/${repo}` };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function githubHeaders(githubToken, forge) {
|
|
86
|
+
const headers = {
|
|
87
|
+
accept: forge.acceptHeader,
|
|
88
|
+
"user-agent": forge.userAgent,
|
|
89
|
+
[forge.apiVersionHeader]: forge.apiVersion,
|
|
90
|
+
};
|
|
91
|
+
const token = typeof githubToken === "string" ? githubToken.trim() : "";
|
|
92
|
+
if (token) headers.authorization = `Bearer ${token}`;
|
|
93
|
+
return headers;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function apiUrl(apiBaseUrl, path, query = "") {
|
|
97
|
+
return `${apiBaseUrl.replace(/\/+$/, "")}${path}${query}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function repoPath(forge, target, suffix) {
|
|
101
|
+
return `${forge.repoPathPrefix}/${encodeURIComponent(target.owner)}/${encodeURIComponent(target.repo)}${suffix}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function recordRateLimit(summary, response) {
|
|
105
|
+
const remaining = Number(response.headers.get("x-ratelimit-remaining"));
|
|
106
|
+
if (Number.isFinite(remaining)) {
|
|
107
|
+
summary.rateLimitRemaining =
|
|
108
|
+
summary.rateLimitRemaining === null
|
|
109
|
+
? remaining
|
|
110
|
+
: Math.min(summary.rateLimitRemaining, remaining);
|
|
111
|
+
}
|
|
112
|
+
const resetSeconds = Number(response.headers.get("x-ratelimit-reset"));
|
|
113
|
+
if (Number.isFinite(resetSeconds) && resetSeconds > 0) {
|
|
114
|
+
const resetAt = new Date(resetSeconds * 1000).toISOString();
|
|
115
|
+
summary.rateLimitResetAt =
|
|
116
|
+
summary.rateLimitResetAt === null || resetAt > summary.rateLimitResetAt
|
|
117
|
+
? resetAt
|
|
118
|
+
: summary.rateLimitResetAt;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function githubGetJson(url, githubToken, summary, options, extraHeaders = {}) {
|
|
123
|
+
// Retry a transient 5xx from GitHub before dropping this target's results for the whole run (#4830) — the same
|
|
124
|
+
// discipline as the CI/gate-verdict pollers. A thrown network error still propagates to each caller's try/catch.
|
|
125
|
+
// `extraHeaders` carries per-call additions (e.g. a policy-doc If-None-Match, #4842) on top of the base auth set.
|
|
126
|
+
const response = await fetchWithRetry(
|
|
127
|
+
fetch,
|
|
128
|
+
url,
|
|
129
|
+
{ method: "GET", headers: { ...githubHeaders(githubToken, options.forge), ...extraHeaders } },
|
|
130
|
+
{ sleepFn: options?.sleepFn },
|
|
131
|
+
);
|
|
132
|
+
recordRateLimit(summary, response);
|
|
133
|
+
const payload = await response.json().catch(() => null);
|
|
134
|
+
return { response, payload };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function decodeContentPayload(payload) {
|
|
138
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return null;
|
|
139
|
+
if (typeof payload.content !== "string") return null;
|
|
140
|
+
if (payload.encoding === "base64") {
|
|
141
|
+
return Buffer.from(payload.content.replace(/\s/g, ""), "base64").toString("utf8");
|
|
142
|
+
}
|
|
143
|
+
return payload.content;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function warning(target, stage, message) {
|
|
147
|
+
return { repoFullName: target.repoFullName, stage, message };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Read a URL's prior ETag so an unchanged doc can be revalidated with a conditional GET (#4842). A cache that is
|
|
151
|
+
// absent, or whose read throws (corrupt/locked file), is treated as a plain miss: the caller does a full fetch,
|
|
152
|
+
// per the "never risk a stale policy" rule — the cache only ever makes discovery cheaper, never less correct.
|
|
153
|
+
function readCachedPolicyDoc(cache, url) {
|
|
154
|
+
if (!cache) return null;
|
|
155
|
+
try {
|
|
156
|
+
return cache.get(url);
|
|
157
|
+
} catch {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function readEtagHeader(response) {
|
|
163
|
+
const etag = response.headers.get("etag");
|
|
164
|
+
return typeof etag === "string" && etag.trim() ? etag : null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Persist the fresh ETag + body so the NEXT discover run can revalidate instead of re-downloading. Only a real
|
|
168
|
+
// ETag paired with decoded content is stored, and a write that throws must never fail discovery (same stale-safe
|
|
169
|
+
// rule) — it degrades to "not cached", so the next run simply refetches in full.
|
|
170
|
+
function writeCachedPolicyDoc(cache, url, etag, content) {
|
|
171
|
+
if (!cache || content === null || etag === null) return;
|
|
172
|
+
try {
|
|
173
|
+
cache.put(url, etag, content);
|
|
174
|
+
} catch {
|
|
175
|
+
// Leave this URL uncached; the next run refetches fully rather than serving anything stale.
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// A bare `owner/repo` is NOT a safe policy-verdict cache key: two different tenant forge hosts (#4784's
|
|
180
|
+
// per-tenant `apiBaseUrl`) can each have their own unrelated repo of the same name, and their policy docs are
|
|
181
|
+
// wholly independent. Scope the key by host, mirroring policy-doc-cache.js's own precedent of keying on the full
|
|
182
|
+
// request URL rather than a bare path.
|
|
183
|
+
function policyVerdictCacheKey(apiBaseUrl, repoFullName) {
|
|
184
|
+
return `${apiBaseUrl}::${repoFullName}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Read a repo scope's previously-resolved verdict for the SAME decisive doc + ETag (#4843). A cache that is
|
|
188
|
+
// absent, or whose read throws (corrupt/locked file), is treated as a plain miss — the caller resolves the
|
|
189
|
+
// verdict fresh, per the same "never risk a stale policy" rule as the doc cache above.
|
|
190
|
+
function readCachedPolicyVerdict(cache, repoScope) {
|
|
191
|
+
if (!cache) return null;
|
|
192
|
+
try {
|
|
193
|
+
return cache.get(repoScope);
|
|
194
|
+
} catch {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Persist the freshly-resolved verdict against the ETag of the doc that decided it, so the next run can reuse it
|
|
200
|
+
// outright once that ETag is confirmed unchanged. Only ever called with a real ETag; a write that throws must
|
|
201
|
+
// never fail discovery — it degrades to "not cached", so the next run just resolves the verdict again.
|
|
202
|
+
function writeCachedPolicyVerdict(cache, repoScope, decisiveDoc, etag, verdict) {
|
|
203
|
+
if (!cache || etag === null) return;
|
|
204
|
+
try {
|
|
205
|
+
cache.put(repoScope, decisiveDoc, etag, verdict);
|
|
206
|
+
} catch {
|
|
207
|
+
// Leave this repo scope uncached; the next run just resolves the verdict fresh again.
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function fetchRepoDoc(target, path, githubToken, options, summary, warnings) {
|
|
212
|
+
const url = apiUrl(
|
|
213
|
+
options.apiBaseUrl,
|
|
214
|
+
repoPath(options.forge, target, `/contents/${encodeURIComponent(path)}`),
|
|
215
|
+
);
|
|
216
|
+
const cached = readCachedPolicyDoc(options.policyDocCache, url);
|
|
217
|
+
const conditionalHeaders = cached ? { "if-none-match": cached.etag } : {};
|
|
218
|
+
try {
|
|
219
|
+
const { response, payload } = await githubGetJson(url, githubToken, summary, options, conditionalHeaders);
|
|
220
|
+
// A 304 only ever follows the If-None-Match we send above, which we only send when `cached` exists — so the
|
|
221
|
+
// cached body is the GitHub-confirmed current content, served with no extra rate-limit spend.
|
|
222
|
+
if (response.status === 304) return { content: cached.content, etag: cached.etag };
|
|
223
|
+
if (response.status === 404) return { content: null, etag: null };
|
|
224
|
+
if (!response.ok) {
|
|
225
|
+
warnings.push(warning(target, `policy:${path}`, `GitHub returned ${response.status}`));
|
|
226
|
+
return { content: null, etag: null };
|
|
227
|
+
}
|
|
228
|
+
const content = decodeContentPayload(payload);
|
|
229
|
+
const etag = readEtagHeader(response);
|
|
230
|
+
writeCachedPolicyDoc(options.policyDocCache, url, etag, content);
|
|
231
|
+
return { content, etag };
|
|
232
|
+
} catch (error) {
|
|
233
|
+
warnings.push(
|
|
234
|
+
warning(target, `policy:${path}`, error instanceof Error ? error.message : "policy fetch failed"),
|
|
235
|
+
);
|
|
236
|
+
return { content: null, etag: null };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Resolve a repo scope's AI-usage-policy verdict, reusing a cached one when the deciding doc's ETag hasn't moved
|
|
241
|
+
// since it was last resolved (#4843). Only ever consulted with an ETag that a same-run conditional-GET just
|
|
242
|
+
// confirmed is current, so a cache hit is exactly as correct as recomputing — it just skips the (cheap, but not
|
|
243
|
+
// free) parse.
|
|
244
|
+
function resolveOrCacheVerdict(cache, repoScope, decisiveDoc, etag, computeVerdict) {
|
|
245
|
+
if (etag !== null) {
|
|
246
|
+
const cached = readCachedPolicyVerdict(cache, repoScope);
|
|
247
|
+
if (cached && cached.decisiveDoc === decisiveDoc && cached.etag === etag) return cached.verdict;
|
|
248
|
+
}
|
|
249
|
+
const verdict = computeVerdict();
|
|
250
|
+
writeCachedPolicyVerdict(cache, repoScope, decisiveDoc, etag, verdict);
|
|
251
|
+
return verdict;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function resolveRepoAiPolicy(target, githubToken, options, summary, warnings) {
|
|
255
|
+
const repoScope = policyVerdictCacheKey(options.apiBaseUrl, target.repoFullName);
|
|
256
|
+
const { content: aiUsage, etag: aiUsageEtag } = await fetchRepoDoc(
|
|
257
|
+
target,
|
|
258
|
+
"AI-USAGE.md",
|
|
259
|
+
githubToken,
|
|
260
|
+
options,
|
|
261
|
+
summary,
|
|
262
|
+
warnings,
|
|
263
|
+
);
|
|
264
|
+
// Short-circuit only on AI-USAGE.md that has real content. A present-but-blank AI-USAGE.md must still fall
|
|
265
|
+
// through to CONTRIBUTING.md — otherwise a stub AI-USAGE.md silently fails open and swallows a ban declared in
|
|
266
|
+
// CONTRIBUTING.md (the exact case resolveAiPolicyVerdict was fixed to handle in #2900, which can only fire if
|
|
267
|
+
// both docs reach it).
|
|
268
|
+
if (aiUsage !== null && aiUsage.trim().length > 0) {
|
|
269
|
+
return resolveOrCacheVerdict(options.policyVerdictCache, repoScope, "AI-USAGE.md", aiUsageEtag, () =>
|
|
270
|
+
resolveAiPolicyVerdict({ aiUsage, contributing: null }),
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
const { content: contributing, etag: contributingEtag } = await fetchRepoDoc(
|
|
274
|
+
target,
|
|
275
|
+
"CONTRIBUTING.md",
|
|
276
|
+
githubToken,
|
|
277
|
+
options,
|
|
278
|
+
summary,
|
|
279
|
+
warnings,
|
|
280
|
+
);
|
|
281
|
+
return resolveOrCacheVerdict(
|
|
282
|
+
options.policyVerdictCache,
|
|
283
|
+
repoScope,
|
|
284
|
+
"CONTRIBUTING.md",
|
|
285
|
+
contributingEtag,
|
|
286
|
+
() => resolveAiPolicyVerdict({ aiUsage: null, contributing }),
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function labelNames(labels) {
|
|
291
|
+
if (!Array.isArray(labels)) return [];
|
|
292
|
+
return labels
|
|
293
|
+
.map((label) => {
|
|
294
|
+
if (typeof label === "string") return label;
|
|
295
|
+
if (label && typeof label === "object" && typeof label.name === "string") return label.name;
|
|
296
|
+
return "";
|
|
297
|
+
})
|
|
298
|
+
.filter((name) => name.length > 0);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function normalizeIssue(target, issue, policySource) {
|
|
302
|
+
if (!issue || typeof issue !== "object" || issue.pull_request) return null;
|
|
303
|
+
if (!Number.isInteger(issue.number) || issue.number <= 0) return null;
|
|
304
|
+
if (typeof issue.title !== "string" || issue.title.trim().length === 0) return null;
|
|
305
|
+
return {
|
|
306
|
+
owner: target.owner,
|
|
307
|
+
repo: target.repo,
|
|
308
|
+
repoFullName: target.repoFullName,
|
|
309
|
+
issueNumber: issue.number,
|
|
310
|
+
title: issue.title,
|
|
311
|
+
labels: labelNames(issue.labels),
|
|
312
|
+
commentsCount: Number.isFinite(issue.comments) ? issue.comments : 0,
|
|
313
|
+
createdAt: typeof issue.created_at === "string" ? issue.created_at : null,
|
|
314
|
+
updatedAt: typeof issue.updated_at === "string" ? issue.updated_at : null,
|
|
315
|
+
htmlUrl: typeof issue.html_url === "string" ? issue.html_url : null,
|
|
316
|
+
aiPolicyAllowed: true,
|
|
317
|
+
aiPolicySource: policySource,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function searchQueryWithIssueQualifiers(searchQuery, forge) {
|
|
322
|
+
const trimmed = typeof searchQuery === "string" ? searchQuery.trim() : "";
|
|
323
|
+
if (!trimmed) return "";
|
|
324
|
+
return `${trimmed} ${forge.searchQualifiers}`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// The URL of the next page from a GitHub Link header (`<url>; rel="next"`), constrained to the current
|
|
328
|
+
// token-bearing GitHub API endpoint so a forged Link header cannot redirect credentials off-origin.
|
|
329
|
+
function nextPageUrl(response, apiBaseUrl, expectedPath) {
|
|
330
|
+
const linkHeader = response.headers.get("link") ?? "";
|
|
331
|
+
const match = linkHeader.match(/<([^>]+)>;\s*rel="next"/);
|
|
332
|
+
if (match === null) return null;
|
|
333
|
+
|
|
334
|
+
let nextUrl;
|
|
335
|
+
let expectedUrl;
|
|
336
|
+
try {
|
|
337
|
+
expectedUrl = new URL(apiUrl(apiBaseUrl, expectedPath));
|
|
338
|
+
nextUrl = new URL(match[1], expectedUrl);
|
|
339
|
+
} catch {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (
|
|
344
|
+
nextUrl.protocol !== "https:" ||
|
|
345
|
+
nextUrl.origin !== expectedUrl.origin ||
|
|
346
|
+
nextUrl.pathname !== expectedUrl.pathname
|
|
347
|
+
) {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
return nextUrl.toString();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
async function fetchTargetIssues(target, githubToken, options, summary, warnings) {
|
|
354
|
+
const verdict = await resolveRepoAiPolicy(target, githubToken, options, summary, warnings);
|
|
355
|
+
if (!verdict.allowed) return [];
|
|
356
|
+
|
|
357
|
+
const issuesPath = repoPath(options.forge, target, "/issues");
|
|
358
|
+
let url = apiUrl(options.apiBaseUrl, issuesPath, `?state=open&per_page=${options.perPage}`);
|
|
359
|
+
const issues = [];
|
|
360
|
+
try {
|
|
361
|
+
for (let page = 0; url !== null && page < options.maxPages; page += 1) {
|
|
362
|
+
const { response, payload } = await githubGetJson(url, githubToken, summary, options);
|
|
363
|
+
if (!response.ok) {
|
|
364
|
+
warnings.push(warning(target, "issues", `GitHub returned ${response.status}`));
|
|
365
|
+
return issues;
|
|
366
|
+
}
|
|
367
|
+
if (!Array.isArray(payload)) {
|
|
368
|
+
warnings.push(warning(target, "issues", "GitHub returned a non-array issues payload"));
|
|
369
|
+
return issues;
|
|
370
|
+
}
|
|
371
|
+
for (const issue of payload) {
|
|
372
|
+
const normalized = normalizeIssue(target, issue, verdict.source);
|
|
373
|
+
if (normalized !== null) issues.push(normalized);
|
|
374
|
+
}
|
|
375
|
+
url = nextPageUrl(response, options.apiBaseUrl, issuesPath);
|
|
376
|
+
}
|
|
377
|
+
return issues;
|
|
378
|
+
} catch (error) {
|
|
379
|
+
warnings.push(
|
|
380
|
+
warning(target, "issues", error instanceof Error ? error.message : "issue fetch failed"),
|
|
381
|
+
);
|
|
382
|
+
return issues;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function fetchSearchIssues(searchQuery, githubToken, options, summary, warnings) {
|
|
387
|
+
const qualifiedQuery = searchQueryWithIssueQualifiers(searchQuery, options.forge);
|
|
388
|
+
if (!qualifiedQuery) return [];
|
|
389
|
+
|
|
390
|
+
const searchPath = options.forge.searchEndpoint;
|
|
391
|
+
let url = apiUrl(
|
|
392
|
+
options.apiBaseUrl,
|
|
393
|
+
searchPath,
|
|
394
|
+
`?q=${encodeURIComponent(qualifiedQuery)}&per_page=${options.perPage}`,
|
|
395
|
+
);
|
|
396
|
+
const items = [];
|
|
397
|
+
try {
|
|
398
|
+
for (let page = 0; url !== null && page < options.maxPages; page += 1) {
|
|
399
|
+
const { response, payload } = await githubGetJson(url, githubToken, summary, options);
|
|
400
|
+
if (!response.ok) {
|
|
401
|
+
warnings.push({
|
|
402
|
+
repoFullName: "*",
|
|
403
|
+
stage: "search",
|
|
404
|
+
message: `GitHub returned ${response.status}`,
|
|
405
|
+
});
|
|
406
|
+
return items;
|
|
407
|
+
}
|
|
408
|
+
if (!payload || typeof payload !== "object" || !Array.isArray(payload.items)) {
|
|
409
|
+
warnings.push({
|
|
410
|
+
repoFullName: "*",
|
|
411
|
+
stage: "search",
|
|
412
|
+
message: "GitHub returned a non-array search payload",
|
|
413
|
+
});
|
|
414
|
+
return items;
|
|
415
|
+
}
|
|
416
|
+
items.push(...payload.items);
|
|
417
|
+
url = nextPageUrl(response, options.apiBaseUrl, searchPath);
|
|
418
|
+
}
|
|
419
|
+
return items;
|
|
420
|
+
} catch (error) {
|
|
421
|
+
warnings.push({
|
|
422
|
+
repoFullName: "*",
|
|
423
|
+
stage: "search",
|
|
424
|
+
message: error instanceof Error ? error.message : "issue search failed",
|
|
425
|
+
});
|
|
426
|
+
return items;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function delay(ms) {
|
|
431
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Run `worker` over `items` with a dynamic in-flight cap (#4844). The pool spawns `maxConcurrency` loops, but a
|
|
435
|
+
// loop parks (re-checking every `throttleParkMs`) whenever the live `resolveLimit()` — derived from the recorded
|
|
436
|
+
// rate-limit budget — is already met by the number of in-flight workers, so effective concurrency tapers off as
|
|
437
|
+
// the budget drops instead of sprinting into a 403. `sleepFn` lets tests inject an instant wait for the park.
|
|
438
|
+
export async function mapWithConcurrency(items, maxConcurrency, worker, resolveLimit, sleepFn) {
|
|
439
|
+
const results = new Array(items.length);
|
|
440
|
+
const sleep = sleepFn ?? delay;
|
|
441
|
+
let next = 0;
|
|
442
|
+
let active = 0;
|
|
443
|
+
const runOne = async () => {
|
|
444
|
+
while (next < items.length) {
|
|
445
|
+
// Park while the live limit is already saturated. The check and the `active`/`next` bumps below run without
|
|
446
|
+
// an intervening await, so two loops can never claim the same slot.
|
|
447
|
+
while (active >= resolveLimit()) {
|
|
448
|
+
await sleep(throttleParkMs);
|
|
449
|
+
}
|
|
450
|
+
// The shared cursor can be drained by other loops while this one is parked, so re-check before claiming.
|
|
451
|
+
if (next >= items.length) return;
|
|
452
|
+
const index = next;
|
|
453
|
+
next += 1;
|
|
454
|
+
active += 1;
|
|
455
|
+
try {
|
|
456
|
+
results[index] = await worker(items[index], index);
|
|
457
|
+
} finally {
|
|
458
|
+
active -= 1;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
};
|
|
462
|
+
const workers = Array.from({ length: Math.min(maxConcurrency, items.length) }, runOne);
|
|
463
|
+
await Promise.all(workers);
|
|
464
|
+
return results;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** A live limit resolver for `mapWithConcurrency`, reading the summary's rate-limit budget as it is updated (#4844). */
|
|
468
|
+
function liveConcurrencyResolver(normalizedOptions, summary) {
|
|
469
|
+
return () =>
|
|
470
|
+
resolveThrottledConcurrency(
|
|
471
|
+
normalizedOptions.concurrency,
|
|
472
|
+
summary.rateLimitRemaining,
|
|
473
|
+
normalizedOptions.rateLimitLowWaterMark,
|
|
474
|
+
normalizedOptions.rateLimitHighWaterMark,
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function normalizeOptions(options = {}) {
|
|
479
|
+
// A legacy top-level `apiBaseUrl` (the pre-#4784 GitHub-Enterprise override every existing caller uses) still wins
|
|
480
|
+
// over `forge.apiBaseUrl`, so nothing that already passes `apiBaseUrl` changes behavior.
|
|
481
|
+
const apiBaseUrlOverride =
|
|
482
|
+
typeof options.apiBaseUrl === "string" && options.apiBaseUrl.trim()
|
|
483
|
+
? { apiBaseUrl: options.apiBaseUrl }
|
|
484
|
+
: {};
|
|
485
|
+
const forge = resolveForgeConfig({ ...(options.forge ?? {}), ...apiBaseUrlOverride });
|
|
486
|
+
return {
|
|
487
|
+
forge,
|
|
488
|
+
apiBaseUrl: forge.apiBaseUrl,
|
|
489
|
+
concurrency: normalizeLimit(options.concurrency, defaultConcurrency, 1, 10),
|
|
490
|
+
// Below/above these recorded-rate-limit-remaining marks the fanout serializes / runs at full concurrency; in
|
|
491
|
+
// between it scales down linearly (#4844).
|
|
492
|
+
rateLimitLowWaterMark: normalizeLimit(
|
|
493
|
+
options.rateLimitLowWaterMark,
|
|
494
|
+
DEFAULT_RATE_LIMIT_LOW_WATER_MARK,
|
|
495
|
+
0,
|
|
496
|
+
1_000_000,
|
|
497
|
+
),
|
|
498
|
+
rateLimitHighWaterMark: normalizeLimit(
|
|
499
|
+
options.rateLimitHighWaterMark,
|
|
500
|
+
DEFAULT_RATE_LIMIT_HIGH_WATER_MARK,
|
|
501
|
+
1,
|
|
502
|
+
1_000_000,
|
|
503
|
+
),
|
|
504
|
+
perPage: normalizeLimit(options.perPage, defaultPerPage, 1, 100),
|
|
505
|
+
maxPages: normalizeLimit(options.maxPages, defaultMaxPages, 1, 100),
|
|
506
|
+
// Passed through to the per-fetch retry so tests can inject an instant sleep; undefined uses the real backoff.
|
|
507
|
+
sleepFn: typeof options.sleepFn === "function" ? options.sleepFn : undefined,
|
|
508
|
+
// Optional local ETag cache for policy-doc revalidation (#4842). Absent (null) => every policy doc is fetched
|
|
509
|
+
// in full, exactly as before; discover-cli.js supplies the real on-disk store for a live run.
|
|
510
|
+
policyDocCache: options.policyDocCache ?? null,
|
|
511
|
+
// Optional local cache of resolved policy verdicts (#4843). Absent (null) => every verdict is resolved fresh,
|
|
512
|
+
// exactly as before; discover-cli.js supplies the real on-disk store for a live run.
|
|
513
|
+
policyVerdictCache: options.policyVerdictCache ?? null,
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export async function fetchCandidateIssuesWithSummary(targets, githubToken, options = {}) {
|
|
518
|
+
const normalizedOptions = normalizeOptions(options);
|
|
519
|
+
const normalizedTargets = normalizeTargets(targets);
|
|
520
|
+
const summary = {
|
|
521
|
+
rateLimitRemaining: null,
|
|
522
|
+
rateLimitResetAt: null,
|
|
523
|
+
};
|
|
524
|
+
const warnings = [];
|
|
525
|
+
const batches = await mapWithConcurrency(
|
|
526
|
+
normalizedTargets,
|
|
527
|
+
normalizedOptions.concurrency,
|
|
528
|
+
(target) => fetchTargetIssues(target, githubToken, normalizedOptions, summary, warnings),
|
|
529
|
+
liveConcurrencyResolver(normalizedOptions, summary),
|
|
530
|
+
normalizedOptions.sleepFn,
|
|
531
|
+
);
|
|
532
|
+
return {
|
|
533
|
+
issues: batches.flat(),
|
|
534
|
+
rateLimitRemaining: summary.rateLimitRemaining,
|
|
535
|
+
rateLimitResetAt: summary.rateLimitResetAt,
|
|
536
|
+
warnings,
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Metadata-only GitHub discovery (#2307): never clones source, never fetches blobs beyond small policy docs,
|
|
542
|
+
* never uploads source, and never performs writes. Call the WithSummary variant when rate-limit telemetry is
|
|
543
|
+
* needed.
|
|
544
|
+
*/
|
|
545
|
+
export async function fetchCandidateIssues(targets, githubToken, options = {}) {
|
|
546
|
+
const result = await fetchCandidateIssuesWithSummary(targets, githubToken, options);
|
|
547
|
+
return result.issues;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
export async function searchCandidateIssuesWithSummary(searchQuery, githubToken, options = {}) {
|
|
551
|
+
const normalizedOptions = normalizeOptions(options);
|
|
552
|
+
const summary = {
|
|
553
|
+
rateLimitRemaining: null,
|
|
554
|
+
rateLimitResetAt: null,
|
|
555
|
+
};
|
|
556
|
+
const warnings = [];
|
|
557
|
+
const searchItems = await fetchSearchIssues(searchQuery, githubToken, normalizedOptions, summary, warnings);
|
|
558
|
+
const targetsByKey = new Map();
|
|
559
|
+
for (const item of searchItems) {
|
|
560
|
+
if (!item || typeof item !== "object" || item.pull_request) continue;
|
|
561
|
+
const target = targetFromSearchIssue(item, normalizedOptions.forge);
|
|
562
|
+
if (target && !targetsByKey.has(targetKey(target))) targetsByKey.set(targetKey(target), target);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const policyEntries = await mapWithConcurrency(
|
|
566
|
+
[...targetsByKey.values()],
|
|
567
|
+
normalizedOptions.concurrency,
|
|
568
|
+
async (target) => {
|
|
569
|
+
const verdict = await resolveRepoAiPolicy(target, githubToken, normalizedOptions, summary, warnings);
|
|
570
|
+
return [targetKey(target), verdict];
|
|
571
|
+
},
|
|
572
|
+
liveConcurrencyResolver(normalizedOptions, summary),
|
|
573
|
+
normalizedOptions.sleepFn,
|
|
574
|
+
);
|
|
575
|
+
const policiesByKey = new Map(policyEntries);
|
|
576
|
+
const issues = [];
|
|
577
|
+
for (const item of searchItems) {
|
|
578
|
+
const target = targetFromSearchIssue(item, normalizedOptions.forge);
|
|
579
|
+
if (!target) continue;
|
|
580
|
+
const policy = policiesByKey.get(targetKey(target));
|
|
581
|
+
if (!policy?.allowed) continue;
|
|
582
|
+
const normalizedIssue = normalizeIssue(target, item, policy.source);
|
|
583
|
+
if (normalizedIssue) issues.push(normalizedIssue);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return {
|
|
587
|
+
issues,
|
|
588
|
+
rateLimitRemaining: summary.rateLimitRemaining,
|
|
589
|
+
rateLimitResetAt: summary.rateLimitResetAt,
|
|
590
|
+
warnings,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
export async function searchCandidateIssues(searchQuery, githubToken, options = {}) {
|
|
595
|
+
const result = await searchCandidateIssuesWithSummary(searchQuery, githubToken, options);
|
|
596
|
+
return result.issues;
|
|
597
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { MinerGoalSpec } from "@loopover/engine";
|
|
2
|
+
import type { RawCandidateIssue } from "./opportunity-fanout.js";
|
|
3
|
+
|
|
4
|
+
export type RankedCandidateIssue = RawCandidateIssue & {
|
|
5
|
+
potential: number;
|
|
6
|
+
feasibility: number;
|
|
7
|
+
laneFit: number;
|
|
8
|
+
freshness: number;
|
|
9
|
+
dupRisk: number;
|
|
10
|
+
rankScore: number;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type RankCandidateIssuesOptions = {
|
|
14
|
+
nowMs?: number;
|
|
15
|
+
highRiskDuplicateClusters?: number;
|
|
16
|
+
openPullRequests?: number;
|
|
17
|
+
goalSpecsByRepo?: Record<string, MinerGoalSpec>;
|
|
18
|
+
goalSpecContentByRepo?: Record<string, string>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type RankedCandidateSummary = {
|
|
22
|
+
issues: RankedCandidateIssue[];
|
|
23
|
+
skippedInvalid: number;
|
|
24
|
+
usedDefaultGoalSpec: boolean;
|
|
25
|
+
defaultGoalSpec: MinerGoalSpec;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function rankCandidateIssues(
|
|
29
|
+
candidates: RawCandidateIssue[],
|
|
30
|
+
options?: RankCandidateIssuesOptions,
|
|
31
|
+
): RankedCandidateIssue[];
|
|
32
|
+
|
|
33
|
+
export function rankCandidateIssuesWithSummary(
|
|
34
|
+
candidates: RawCandidateIssue[],
|
|
35
|
+
options?: RankCandidateIssuesOptions,
|
|
36
|
+
): RankedCandidateSummary;
|