@oss-scout/core 1.0.0 → 1.2.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/dist/cli.bundle.cjs +70 -64
- package/dist/cli.js +19 -129
- package/dist/commands/search.d.ts +4 -0
- package/dist/commands/search.js +2 -0
- package/dist/core/anti-llm-policy.js +3 -30
- package/dist/core/issue-discovery.d.ts +10 -1
- package/dist/core/issue-discovery.js +83 -48
- package/dist/core/issue-eligibility.d.ts +2 -1
- package/dist/core/issue-eligibility.js +6 -3
- package/dist/core/issue-vetting.d.ts +10 -1
- package/dist/core/issue-vetting.js +12 -2
- package/dist/core/personalization.d.ts +27 -12
- package/dist/core/personalization.js +50 -18
- package/dist/core/preference-fields.js +2 -0
- package/dist/core/probe-repo-file.d.ts +47 -0
- package/dist/core/probe-repo-file.js +57 -0
- package/dist/core/repo-health.js +9 -17
- package/dist/core/roadmap.js +11 -21
- package/dist/core/schemas.d.ts +4 -0
- package/dist/core/schemas.js +9 -0
- package/dist/core/search-phases.d.ts +5 -4
- package/dist/core/search-phases.js +12 -9
- package/dist/core/types.d.ts +15 -0
- package/dist/formatters/human.d.ts +60 -0
- package/dist/formatters/human.js +199 -0
- package/dist/scout.d.ts +24 -10
- package/dist/scout.js +29 -14
- package/package.json +1 -1
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-readable (non-JSON) output formatters for the oss-scout CLI.
|
|
3
|
+
*
|
|
4
|
+
* Each renderer is a pure function that returns the exact multi-line string the
|
|
5
|
+
* CLI used to emit via a sequence of `console.log` calls. The caller does a
|
|
6
|
+
* single `console.log(renderX(...))`, which appends the one trailing newline
|
|
7
|
+
* that the final `console.log` in the old inline block produced.
|
|
8
|
+
*
|
|
9
|
+
* To stay byte-identical: every old `console.log(line)` becomes one entry in a
|
|
10
|
+
* lines array, a bare `console.log()` (blank line) becomes an empty entry, and
|
|
11
|
+
* the array is joined with "\n". The caller's own `console.log` supplies the
|
|
12
|
+
* last newline. STDERR output (the search rate-limit warning) is deliberately
|
|
13
|
+
* NOT folded in here — it stays a `console.error` in the caller.
|
|
14
|
+
*/
|
|
15
|
+
import type { SearchOutput } from "../commands/search.js";
|
|
16
|
+
import type { FeaturesOutput } from "../commands/features.js";
|
|
17
|
+
import type { SavedCandidate } from "../core/schemas.js";
|
|
18
|
+
import type { VetListResult } from "../core/types.js";
|
|
19
|
+
import type { VetOutput } from "../commands/vet.js";
|
|
20
|
+
/** Emoji for a vetting recommendation, shared by the search and vet renderers. */
|
|
21
|
+
export declare function recommendationIcon(recommendation: "approve" | "skip" | "needs_review"): string;
|
|
22
|
+
/**
|
|
23
|
+
* Render the human-readable `search` output: the "Found N issue candidates"
|
|
24
|
+
* block with per-candidate icon, personalization and stalled tags, and the
|
|
25
|
+
* optional repoScore line. The trailing rate-limit warning is NOT included
|
|
26
|
+
* here; it goes to stderr in the caller.
|
|
27
|
+
*/
|
|
28
|
+
export declare function renderSearch(results: SearchOutput): string;
|
|
29
|
+
/**
|
|
30
|
+
* Render the human-readable `features` output: the optional message, the
|
|
31
|
+
* "Feature opportunities" header, the anchor repos line, and the Quick wins /
|
|
32
|
+
* Bigger bets sections. Returns "" when there is nothing to print beyond an
|
|
33
|
+
* absent message (caller guards against logging a blank line).
|
|
34
|
+
*/
|
|
35
|
+
export declare function renderFeatures(result: FeaturesOutput, options: {
|
|
36
|
+
broad?: boolean;
|
|
37
|
+
}): string;
|
|
38
|
+
/** The empty-state message printed by `results` when nothing is saved. */
|
|
39
|
+
export declare const RESULTS_EMPTY_MESSAGE = "\nNo saved results. Run `oss-scout search` to find issues.\n";
|
|
40
|
+
/**
|
|
41
|
+
* Render the human-readable `results` table: the "Saved results" header and a
|
|
42
|
+
* Score / Repo / Issue / Recommendation / Title row per saved candidate.
|
|
43
|
+
* Callers handle the empty state (RESULTS_EMPTY_MESSAGE) separately.
|
|
44
|
+
*/
|
|
45
|
+
export declare function renderResults(results: SavedCandidate[]): string;
|
|
46
|
+
/** The empty-state message printed by `vet-list` when there is nothing to vet. */
|
|
47
|
+
export declare const VET_LIST_EMPTY_MESSAGE = "\nNo saved results to vet. Run `oss-scout search` first.\n";
|
|
48
|
+
/**
|
|
49
|
+
* Render the human-readable `vet-list` output: the "Vet-list results (N)"
|
|
50
|
+
* block with a per-row status icon, the "Changes since last check"
|
|
51
|
+
* transitions block, the summary line, and the optional pruned-count line.
|
|
52
|
+
* Callers handle the empty state (VET_LIST_EMPTY_MESSAGE) separately.
|
|
53
|
+
*/
|
|
54
|
+
export declare function renderVetList(result: VetListResult): string;
|
|
55
|
+
/**
|
|
56
|
+
* Render the human-readable single-issue `vet` output: the recommendation
|
|
57
|
+
* header, the reasons to approve / skip, and the project-health block. The
|
|
58
|
+
* checkFailed branch (#158) is preserved exactly.
|
|
59
|
+
*/
|
|
60
|
+
export declare function renderVet(result: VetOutput): string;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-readable (non-JSON) output formatters for the oss-scout CLI.
|
|
3
|
+
*
|
|
4
|
+
* Each renderer is a pure function that returns the exact multi-line string the
|
|
5
|
+
* CLI used to emit via a sequence of `console.log` calls. The caller does a
|
|
6
|
+
* single `console.log(renderX(...))`, which appends the one trailing newline
|
|
7
|
+
* that the final `console.log` in the old inline block produced.
|
|
8
|
+
*
|
|
9
|
+
* To stay byte-identical: every old `console.log(line)` becomes one entry in a
|
|
10
|
+
* lines array, a bare `console.log()` (blank line) becomes an empty entry, and
|
|
11
|
+
* the array is joined with "\n". The caller's own `console.log` supplies the
|
|
12
|
+
* last newline. STDERR output (the search rate-limit warning) is deliberately
|
|
13
|
+
* NOT folded in here — it stays a `console.error` in the caller.
|
|
14
|
+
*/
|
|
15
|
+
/** Emoji for a vetting recommendation, shared by the search and vet renderers. */
|
|
16
|
+
export function recommendationIcon(recommendation) {
|
|
17
|
+
if (recommendation === "approve")
|
|
18
|
+
return "✅";
|
|
19
|
+
if (recommendation === "skip")
|
|
20
|
+
return "❌";
|
|
21
|
+
return "⚠️";
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Render the human-readable `search` output: the "Found N issue candidates"
|
|
25
|
+
* block with per-candidate icon, personalization and stalled tags, and the
|
|
26
|
+
* optional repoScore line. The trailing rate-limit warning is NOT included
|
|
27
|
+
* here; it goes to stderr in the caller.
|
|
28
|
+
*/
|
|
29
|
+
export function renderSearch(results) {
|
|
30
|
+
const lines = [];
|
|
31
|
+
lines.push(`\nFound ${results.candidates.length} issue candidates:\n`);
|
|
32
|
+
for (const c of results.candidates) {
|
|
33
|
+
const icon = recommendationIcon(c.recommendation);
|
|
34
|
+
const stalledTag = c.linkedPR?.isStalled
|
|
35
|
+
? " (stalled PR, revive opportunity)"
|
|
36
|
+
: "";
|
|
37
|
+
// Personalization tag (#1244). A candidate is either boosted (matched a
|
|
38
|
+
// preference) or a diversity slot (matched none and filled a reserved
|
|
39
|
+
// slot); never both.
|
|
40
|
+
let personalizationTag = "";
|
|
41
|
+
if (c.boostReasons && c.boostReasons.length > 0) {
|
|
42
|
+
// Net score can be negative when avoidRepos applied (#168).
|
|
43
|
+
const verb = (c.boostScore ?? 0) >= 0 ? "boosted" : "deprioritized";
|
|
44
|
+
personalizationTag = ` [${verb}: ${c.boostReasons.join("; ")}]`;
|
|
45
|
+
}
|
|
46
|
+
else if (c.diversitySlot) {
|
|
47
|
+
personalizationTag = " [diversity slot]";
|
|
48
|
+
}
|
|
49
|
+
lines.push(` ${icon} ${c.issue.repo}#${c.issue.number} [${c.viabilityScore}/100]${personalizationTag}${stalledTag}`);
|
|
50
|
+
lines.push(` ${c.issue.title}`);
|
|
51
|
+
lines.push(` ${c.issue.url}`);
|
|
52
|
+
if (c.repoScore) {
|
|
53
|
+
lines.push(` Repo: ${c.repoScore.score}/10, ${c.repoScore.mergedPRCount} merged PRs`);
|
|
54
|
+
}
|
|
55
|
+
lines.push("");
|
|
56
|
+
}
|
|
57
|
+
return lines.join("\n");
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Render the human-readable `features` output: the optional message, the
|
|
61
|
+
* "Feature opportunities" header, the anchor repos line, and the Quick wins /
|
|
62
|
+
* Bigger bets sections. Returns "" when there is nothing to print beyond an
|
|
63
|
+
* absent message (caller guards against logging a blank line).
|
|
64
|
+
*/
|
|
65
|
+
export function renderFeatures(result, options) {
|
|
66
|
+
const lines = [];
|
|
67
|
+
const total = result.quickWins.length + result.biggerBets.length;
|
|
68
|
+
if (result.message) {
|
|
69
|
+
lines.push(`\n${result.message}\n`);
|
|
70
|
+
}
|
|
71
|
+
if (total === 0)
|
|
72
|
+
return lines.join("\n");
|
|
73
|
+
const headerScope = options.broad
|
|
74
|
+
? "across the ecosystem"
|
|
75
|
+
: "in your anchor repos";
|
|
76
|
+
lines.push(`\n🎯 Feature opportunities ${headerScope} (${result.quickWins.length} quick wins + ${result.biggerBets.length} bigger bets)\n`);
|
|
77
|
+
if (!options.broad) {
|
|
78
|
+
lines.push(`Anchor repos: ${result.anchorRepos.join(", ")}\n`);
|
|
79
|
+
}
|
|
80
|
+
if (result.quickWins.length) {
|
|
81
|
+
lines.push("── Quick wins ─────────────────────────────────────────");
|
|
82
|
+
for (const c of result.quickWins) {
|
|
83
|
+
const stalledTag = c.linkedPR?.isStalled
|
|
84
|
+
? " (stalled PR, revive opportunity)"
|
|
85
|
+
: "";
|
|
86
|
+
lines.push(` ${c.issue.repo}#${c.issue.number} [${c.viabilityScore}/100] ${c.issue.title}${stalledTag}`);
|
|
87
|
+
lines.push(` ${c.issue.url}`);
|
|
88
|
+
}
|
|
89
|
+
lines.push("");
|
|
90
|
+
}
|
|
91
|
+
if (result.biggerBets.length) {
|
|
92
|
+
lines.push("── Bigger bets ────────────────────────────────────────");
|
|
93
|
+
for (const c of result.biggerBets) {
|
|
94
|
+
const stalledTag = c.linkedPR?.isStalled
|
|
95
|
+
? " (stalled PR, revive opportunity)"
|
|
96
|
+
: "";
|
|
97
|
+
lines.push(` ${c.issue.repo}#${c.issue.number} [${c.viabilityScore}/100] ${c.issue.title}${stalledTag}`);
|
|
98
|
+
lines.push(` ${c.issue.url}`);
|
|
99
|
+
}
|
|
100
|
+
lines.push("");
|
|
101
|
+
}
|
|
102
|
+
return lines.join("\n");
|
|
103
|
+
}
|
|
104
|
+
/** The empty-state message printed by `results` when nothing is saved. */
|
|
105
|
+
export const RESULTS_EMPTY_MESSAGE = "\nNo saved results. Run `oss-scout search` to find issues.\n";
|
|
106
|
+
/**
|
|
107
|
+
* Render the human-readable `results` table: the "Saved results" header and a
|
|
108
|
+
* Score / Repo / Issue / Recommendation / Title row per saved candidate.
|
|
109
|
+
* Callers handle the empty state (RESULTS_EMPTY_MESSAGE) separately.
|
|
110
|
+
*/
|
|
111
|
+
export function renderResults(results) {
|
|
112
|
+
const lines = [];
|
|
113
|
+
lines.push(`\nSaved results (${results.length}):\n`);
|
|
114
|
+
lines.push(" Score Repo Issue Recommendation Title");
|
|
115
|
+
lines.push(" ───── ──────────────────────────────── ────── ────────────── ─────");
|
|
116
|
+
for (const r of results) {
|
|
117
|
+
const score = String(r.viabilityScore).padStart(3);
|
|
118
|
+
const repo = r.repo.padEnd(32).slice(0, 32);
|
|
119
|
+
const issue = `#${r.number}`.padEnd(6);
|
|
120
|
+
const rec = r.recommendation.padEnd(14);
|
|
121
|
+
const title = r.title.length > 50 ? r.title.slice(0, 47) + "..." : r.title;
|
|
122
|
+
lines.push(` ${score} ${repo} ${issue} ${rec} ${title}`);
|
|
123
|
+
}
|
|
124
|
+
lines.push("");
|
|
125
|
+
return lines.join("\n");
|
|
126
|
+
}
|
|
127
|
+
/** The empty-state message printed by `vet-list` when there is nothing to vet. */
|
|
128
|
+
export const VET_LIST_EMPTY_MESSAGE = "\nNo saved results to vet. Run `oss-scout search` first.\n";
|
|
129
|
+
/** Icon for a vet-list entry's availability status. */
|
|
130
|
+
function vetListStatusIcon(status) {
|
|
131
|
+
return status === "still_available"
|
|
132
|
+
? "✅"
|
|
133
|
+
: status === "claimed"
|
|
134
|
+
? "🔒"
|
|
135
|
+
: status === "has_pr"
|
|
136
|
+
? "🔀"
|
|
137
|
+
: status === "closed"
|
|
138
|
+
? "🚫"
|
|
139
|
+
: "❌";
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Render the human-readable `vet-list` output: the "Vet-list results (N)"
|
|
143
|
+
* block with a per-row status icon, the "Changes since last check"
|
|
144
|
+
* transitions block, the summary line, and the optional pruned-count line.
|
|
145
|
+
* Callers handle the empty state (VET_LIST_EMPTY_MESSAGE) separately.
|
|
146
|
+
*/
|
|
147
|
+
export function renderVetList(result) {
|
|
148
|
+
const lines = [];
|
|
149
|
+
lines.push(`\nVet-list results (${result.summary.total}):\n`);
|
|
150
|
+
for (const r of result.results) {
|
|
151
|
+
const icon = vetListStatusIcon(r.status);
|
|
152
|
+
const score = r.ok ? ` [${r.viabilityScore}/100]` : "";
|
|
153
|
+
lines.push(` ${icon} ${r.repo}#${r.number} — ${r.status}${score}`);
|
|
154
|
+
lines.push(` ${r.title}`);
|
|
155
|
+
}
|
|
156
|
+
if (result.transitions.length > 0) {
|
|
157
|
+
lines.push(`\n🔔 Changes since last check (${result.transitions.length}):`);
|
|
158
|
+
for (const t of result.transitions) {
|
|
159
|
+
lines.push(` ${t.repo}#${t.number}: ${t.from} → ${t.to}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
lines.push(`\nSummary: ${result.summary.stillAvailable} available, ${result.summary.claimed} claimed, ${result.summary.hasPR} has PR, ${result.summary.closed} closed, ${result.summary.errors} errors`);
|
|
163
|
+
if (result.prunedCount != null) {
|
|
164
|
+
lines.push(`Pruned ${result.prunedCount} unavailable issues from saved results.`);
|
|
165
|
+
}
|
|
166
|
+
lines.push("");
|
|
167
|
+
return lines.join("\n");
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Render the human-readable single-issue `vet` output: the recommendation
|
|
171
|
+
* header, the reasons to approve / skip, and the project-health block. The
|
|
172
|
+
* checkFailed branch (#158) is preserved exactly.
|
|
173
|
+
*/
|
|
174
|
+
export function renderVet(result) {
|
|
175
|
+
const lines = [];
|
|
176
|
+
const icon = recommendationIcon(result.recommendation);
|
|
177
|
+
lines.push(`\n${icon} ${result.issue.repo}#${result.issue.number}: ${result.recommendation.toUpperCase()}`);
|
|
178
|
+
lines.push(` ${result.issue.title}`);
|
|
179
|
+
lines.push(` ${result.issue.url}\n`);
|
|
180
|
+
if (result.reasonsToApprove.length > 0) {
|
|
181
|
+
lines.push("Reasons to approve:");
|
|
182
|
+
for (const r of result.reasonsToApprove)
|
|
183
|
+
lines.push(` + ${r}`);
|
|
184
|
+
}
|
|
185
|
+
if (result.reasonsToSkip.length > 0) {
|
|
186
|
+
lines.push("Reasons to skip:");
|
|
187
|
+
for (const r of result.reasonsToSkip)
|
|
188
|
+
lines.push(` - ${r}`);
|
|
189
|
+
}
|
|
190
|
+
if (result.projectHealth.checkFailed) {
|
|
191
|
+
lines.push(`\nProject health: unknown (check failed: ${result.projectHealth.failureReason})`);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
lines.push(`\nProject health: ${result.projectHealth.isActive ? "Active" : "Inactive"}`);
|
|
195
|
+
lines.push(` Last commit: ${result.projectHealth.daysSinceLastCommit} days ago`);
|
|
196
|
+
lines.push(` CI status: ${result.projectHealth.ciStatus}`);
|
|
197
|
+
}
|
|
198
|
+
return lines.join("\n");
|
|
199
|
+
}
|
package/dist/scout.d.ts
CHANGED
|
@@ -9,6 +9,14 @@ import { type FeatureSearchResult } from "./core/feature-discovery.js";
|
|
|
9
9
|
import type { ScoutState, ScoutPreferences, RepoScore, SavedCandidate, SkippedIssue, Horizon } from "./core/schemas.js";
|
|
10
10
|
import type { ScoutConfig, SearchOptions, SearchResult, IssueCandidate, MergedPRRecord, ClosedPRRecord, OpenPRRecord, RepoScoreUpdate, ProjectCategory, SyncResult, VetListOptions, VetListResult } from "./core/types.js";
|
|
11
11
|
import { GistStateStore } from "./core/gist-state-store.js";
|
|
12
|
+
import type { GistOctokitLike } from "./core/gist-state-store.js";
|
|
13
|
+
import type { Octokit } from "@octokit/rest";
|
|
14
|
+
/**
|
|
15
|
+
* Wrap a real Octokit instance as GistOctokitLike without unsafe double casts.
|
|
16
|
+
* Exported (not via the package index) so the response-narrowing logic — the
|
|
17
|
+
* "no id" guards and the files/list mapping — is unit-testable (#162).
|
|
18
|
+
*/
|
|
19
|
+
export declare function toGistOctokit(octokit: Octokit): GistOctokitLike;
|
|
12
20
|
/**
|
|
13
21
|
* Create an OssScout instance.
|
|
14
22
|
*
|
|
@@ -64,14 +72,13 @@ export declare class OssScout implements ScoutStateReader, ScoutStateWriter {
|
|
|
64
72
|
* calculateScore's +1 active-maintainers weight was inert; `isActive`
|
|
65
73
|
* (recent commit activity) is a real, already-computed proxy.
|
|
66
74
|
*
|
|
67
|
-
* `isResponsive` and `avgResponseDays` are deliberately NOT set here
|
|
68
|
-
* `
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* neutral-default fields don't pollute the score.
|
|
75
|
+
* `isResponsive` and `avgResponseDays` are deliberately NOT set here, and
|
|
76
|
+
* `isResponsive` no longer carries a score weight at all (#167): real
|
|
77
|
+
* responsiveness needs an actual response-time measurement (extra API calls)
|
|
78
|
+
* that is out of scope, and `hasActiveMaintainers` already covers the
|
|
79
|
+
* activity proxy. `hasHostileComments` stays a host-settable capability (it
|
|
80
|
+
* needs comment sentiment, out of scope). A failed health check is skipped so
|
|
81
|
+
* its neutral-default fields don't pollute the score.
|
|
75
82
|
*/
|
|
76
83
|
private updateRepoSignalsFromHealth;
|
|
77
84
|
/**
|
|
@@ -227,9 +234,16 @@ export declare class OssScout implements ScoutStateReader, ScoutStateWriter {
|
|
|
227
234
|
getState(): Readonly<ScoutState>;
|
|
228
235
|
private updateRepoScoreFromPRs;
|
|
229
236
|
/**
|
|
230
|
-
* Calculate repo score
|
|
237
|
+
* Calculate repo score from observed data.
|
|
231
238
|
* base 5, +1 per merged PR (max +3), -1 per closed-without-merge (max -3),
|
|
232
|
-
* +1
|
|
239
|
+
* +1 active maintainers, -2 hostile comments, clamped to [1, 10].
|
|
240
|
+
*
|
|
241
|
+
* `isResponsive` is intentionally NOT scored (#167): nothing in oss-scout
|
|
242
|
+
* ever computes it, so awarding +1 for it was dead weight that the
|
|
243
|
+
* now-computed `hasActiveMaintainers` signal already covers as the activity
|
|
244
|
+
* proxy. The field is retained on RepoSignals for backward compatibility but
|
|
245
|
+
* no longer affects the score. With the current signals the reachable range
|
|
246
|
+
* is [1, 9]; the upper clamp stays as defensive hygiene.
|
|
233
247
|
*/
|
|
234
248
|
private calculateScore;
|
|
235
249
|
}
|
package/dist/scout.js
CHANGED
|
@@ -29,8 +29,12 @@ function offlineModeMessage(reason) {
|
|
|
29
29
|
return `Gist sync unavailable — running in offline mode. ${tail}`;
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
-
/**
|
|
33
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Wrap a real Octokit instance as GistOctokitLike without unsafe double casts.
|
|
34
|
+
* Exported (not via the package index) so the response-narrowing logic — the
|
|
35
|
+
* "no id" guards and the files/list mapping — is unit-testable (#162).
|
|
36
|
+
*/
|
|
37
|
+
export function toGistOctokit(octokit) {
|
|
34
38
|
return {
|
|
35
39
|
gists: {
|
|
36
40
|
async get(params) {
|
|
@@ -175,6 +179,11 @@ export class OssScout {
|
|
|
175
179
|
const preferLanguages = options?.preferLanguages ??
|
|
176
180
|
(prefLangs.length > 0 ? prefLangs : undefined);
|
|
177
181
|
const preferRepos = options?.preferRepos ?? (prefRepos.length > 0 ? prefRepos : undefined);
|
|
182
|
+
const prefAvoid = prefs.avoidRepos ?? [];
|
|
183
|
+
const prefBoostTypes = prefs.boostIssueTypes ?? [];
|
|
184
|
+
const avoidRepos = options?.avoidRepos ?? (prefAvoid.length > 0 ? prefAvoid : undefined);
|
|
185
|
+
const boostIssueTypes = options?.boostIssueTypes ??
|
|
186
|
+
(prefBoostTypes.length > 0 ? prefBoostTypes : undefined);
|
|
178
187
|
const diversityRatio = options?.diversityRatio ?? prefs.diversityRatio ?? 0;
|
|
179
188
|
const { candidates, strategiesUsed } = await discovery.searchIssues({
|
|
180
189
|
maxResults: options?.maxResults,
|
|
@@ -182,6 +191,8 @@ export class OssScout {
|
|
|
182
191
|
skippedUrls,
|
|
183
192
|
preferLanguages,
|
|
184
193
|
preferRepos,
|
|
194
|
+
avoidRepos,
|
|
195
|
+
boostIssueTypes,
|
|
185
196
|
diversityRatio,
|
|
186
197
|
interPhaseDelayMs: options?.interPhaseDelayMs,
|
|
187
198
|
broadPhaseDelayMs: options?.broadPhaseDelayMs,
|
|
@@ -207,14 +218,13 @@ export class OssScout {
|
|
|
207
218
|
* calculateScore's +1 active-maintainers weight was inert; `isActive`
|
|
208
219
|
* (recent commit activity) is a real, already-computed proxy.
|
|
209
220
|
*
|
|
210
|
-
* `isResponsive` and `avgResponseDays` are deliberately NOT set here
|
|
211
|
-
* `
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
217
|
-
* neutral-default fields don't pollute the score.
|
|
221
|
+
* `isResponsive` and `avgResponseDays` are deliberately NOT set here, and
|
|
222
|
+
* `isResponsive` no longer carries a score weight at all (#167): real
|
|
223
|
+
* responsiveness needs an actual response-time measurement (extra API calls)
|
|
224
|
+
* that is out of scope, and `hasActiveMaintainers` already covers the
|
|
225
|
+
* activity proxy. `hasHostileComments` stays a host-settable capability (it
|
|
226
|
+
* needs comment sentiment, out of scope). A failed health check is skipped so
|
|
227
|
+
* its neutral-default fields don't pollute the score.
|
|
218
228
|
*/
|
|
219
229
|
updateRepoSignalsFromHealth(health) {
|
|
220
230
|
if (health.checkFailed)
|
|
@@ -821,16 +831,21 @@ export class OssScout {
|
|
|
821
831
|
});
|
|
822
832
|
}
|
|
823
833
|
/**
|
|
824
|
-
* Calculate repo score
|
|
834
|
+
* Calculate repo score from observed data.
|
|
825
835
|
* base 5, +1 per merged PR (max +3), -1 per closed-without-merge (max -3),
|
|
826
|
-
* +1
|
|
836
|
+
* +1 active maintainers, -2 hostile comments, clamped to [1, 10].
|
|
837
|
+
*
|
|
838
|
+
* `isResponsive` is intentionally NOT scored (#167): nothing in oss-scout
|
|
839
|
+
* ever computes it, so awarding +1 for it was dead weight that the
|
|
840
|
+
* now-computed `hasActiveMaintainers` signal already covers as the activity
|
|
841
|
+
* proxy. The field is retained on RepoSignals for backward compatibility but
|
|
842
|
+
* no longer affects the score. With the current signals the reachable range
|
|
843
|
+
* is [1, 9]; the upper clamp stays as defensive hygiene.
|
|
827
844
|
*/
|
|
828
845
|
calculateScore(repoScore) {
|
|
829
846
|
let score = 5;
|
|
830
847
|
score += Math.min(repoScore.mergedPRCount, 3);
|
|
831
848
|
score -= Math.min(repoScore.closedWithoutMergeCount, 3);
|
|
832
|
-
if (repoScore.signals.isResponsive)
|
|
833
|
-
score += 1;
|
|
834
849
|
if (repoScore.signals.hasActiveMaintainers)
|
|
835
850
|
score += 1;
|
|
836
851
|
if (repoScore.signals.hasHostileComments)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oss-scout/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Personalized GitHub issue finder with multi-strategy search, deep vetting, and viability scoring — CLI, library, MCP server, and Claude Code plugin",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|