@oss-scout/core 0.4.0 → 0.5.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.
@@ -7,6 +7,7 @@ export interface BootstrapResult {
7
7
  starredRepoCount: number;
8
8
  mergedPRCount: number;
9
9
  closedPRCount: number;
10
+ openPRCount: number;
10
11
  reposScoredCount: number;
11
12
  skippedDueToRateLimit: boolean;
12
13
  errors: string[];
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import { getOctokit, checkRateLimit } from "./github.js";
6
6
  import { debug, warn } from "./logger.js";
7
- import { ConfigurationError, errorMessage } from "./errors.js";
7
+ import { ConfigurationError, errorMessage, getHttpStatusCode, isRateLimitError, } from "./errors.js";
8
8
  import { extractRepoFromUrl } from "./utils.js";
9
9
  const MODULE = "bootstrap";
10
10
  const STARRED_MAX_PAGES = 5;
@@ -23,6 +23,7 @@ export async function bootstrapScout(scout, token) {
23
23
  starredRepoCount: 0,
24
24
  mergedPRCount: 0,
25
25
  closedPRCount: 0,
26
+ openPRCount: 0,
26
27
  reposScoredCount: 0,
27
28
  skippedDueToRateLimit: true,
28
29
  errors: [],
@@ -80,6 +81,8 @@ export async function bootstrapScout(scout, token) {
80
81
  debug(MODULE, `Imported ${mergedPRCount} merged PRs`);
81
82
  }
82
83
  catch (err) {
84
+ if (getHttpStatusCode(err) === 401 || isRateLimitError(err))
85
+ throw err;
83
86
  warn(MODULE, `Failed to fetch merged PRs: ${errorMessage(err)}`);
84
87
  errors.push("merged PR fetch failed");
85
88
  }
@@ -110,15 +113,50 @@ export async function bootstrapScout(scout, token) {
110
113
  debug(MODULE, `Imported ${closedPRCount} closed PRs`);
111
114
  }
112
115
  catch (err) {
116
+ if (getHttpStatusCode(err) === 401 || isRateLimitError(err))
117
+ throw err;
113
118
  warn(MODULE, `Failed to fetch closed PRs: ${errorMessage(err)}`);
114
119
  errors.push("closed PR fetch failed");
115
120
  }
121
+ // 4. Fetch currently-open PRs via Search API
122
+ let openPRCount = 0;
123
+ try {
124
+ for (let page = 1; page <= SEARCH_MAX_PAGES; page++) {
125
+ const { data } = await octokit.search.issuesAndPullRequests({
126
+ q: `is:pr is:open author:${username}`,
127
+ per_page: PER_PAGE,
128
+ page,
129
+ });
130
+ for (const item of data.items) {
131
+ const repo = extractRepoFromUrl(item.html_url);
132
+ if (!repo)
133
+ continue;
134
+ scout.recordOpenPR({
135
+ url: item.html_url,
136
+ title: item.title,
137
+ openedAt: item.created_at ?? new Date().toISOString(),
138
+ repo,
139
+ });
140
+ openPRCount++;
141
+ }
142
+ if (data.items.length < PER_PAGE)
143
+ break;
144
+ }
145
+ debug(MODULE, `Imported ${openPRCount} open PRs`);
146
+ }
147
+ catch (err) {
148
+ if (getHttpStatusCode(err) === 401 || isRateLimitError(err))
149
+ throw err;
150
+ warn(MODULE, `Failed to fetch open PRs: ${errorMessage(err)}`);
151
+ errors.push("open PR fetch failed");
152
+ }
116
153
  const state = scout.getState();
117
154
  const reposScoredCount = Object.keys(state.repoScores).length;
118
155
  return {
119
156
  starredRepoCount: starredRepos.length,
120
157
  mergedPRCount,
121
158
  closedPRCount,
159
+ openPRCount,
122
160
  reposScoredCount,
123
161
  skippedDueToRateLimit: false,
124
162
  errors,
@@ -88,7 +88,7 @@ export declare class GistStateStore {
88
88
  /**
89
89
  * Merge two ScoutState objects with conflict resolution:
90
90
  * - repoScores: per-repo, keep the one with more total PR activity
91
- * - mergedPRs/closedPRs: union by URL
91
+ * - mergedPRs/closedPRs/openPRs: union by URL
92
92
  * - preferences: remote wins
93
93
  * - starredRepos: keep the list with the fresher timestamp
94
94
  * - savedResults: union by issueUrl, keep newer lastSeenAt
@@ -238,7 +238,7 @@ export class GistStateStore {
238
238
  /**
239
239
  * Merge two ScoutState objects with conflict resolution:
240
240
  * - repoScores: per-repo, keep the one with more total PR activity
241
- * - mergedPRs/closedPRs: union by URL
241
+ * - mergedPRs/closedPRs/openPRs: union by URL
242
242
  * - preferences: remote wins
243
243
  * - starredRepos: keep the list with the fresher timestamp
244
244
  * - savedResults: union by issueUrl, keep newer lastSeenAt
@@ -252,6 +252,7 @@ export function mergeStates(local, remote) {
252
252
  starredReposLastFetched: pickFresherTimestamp(local.starredReposLastFetched, remote.starredReposLastFetched),
253
253
  mergedPRs: unionByUrl(local.mergedPRs, remote.mergedPRs),
254
254
  closedPRs: unionByUrl(local.closedPRs, remote.closedPRs),
255
+ openPRs: unionByUrl(local.openPRs ?? [], remote.openPRs ?? []),
255
256
  savedResults: mergeSavedResults(local.savedResults ?? [], remote.savedResults ?? []),
256
257
  skippedIssues: mergeSkippedIssues(local.skippedIssues ?? [], remote.skippedIssues ?? []),
257
258
  lastSearchAt: pickFresherTimestamp(local.lastSearchAt, remote.lastSearchAt),
@@ -326,6 +326,7 @@ export class IssueDiscovery {
326
326
  }
327
327
  // Derive search context
328
328
  const mergedPRRepos = this.stateReader.getReposWithMergedPRs();
329
+ const openPRRepos = this.stateReader.getReposWithOpenPRs();
329
330
  const starredRepos = this.getStarredRepos();
330
331
  const starredRepoSet = new Set(starredRepos);
331
332
  const lowScoringRepos = new Set(this.deriveLowScoringRepos(config.minRepoScoreThreshold));
@@ -352,8 +353,19 @@ export class IssueDiscovery {
352
353
  now: new Date(),
353
354
  includeDocIssues: config.includeDocIssues ?? true,
354
355
  });
355
- // Phase 0: Merged-PR repos
356
- const phase0Repos = mergedPRRepos.slice(0, 10);
356
+ // Phase 0: Repos the user has engaged with — merged PRs first (strongest
357
+ // signal), then open PRs (active engagement even without a merge yet).
358
+ // Deduped and capped so REST cost stays bounded.
359
+ const seenPhase0 = new Set();
360
+ const phase0Repos = [];
361
+ for (const repo of [...mergedPRRepos, ...openPRRepos]) {
362
+ if (seenPhase0.has(repo))
363
+ continue;
364
+ seenPhase0.add(repo);
365
+ phase0Repos.push(repo);
366
+ if (phase0Repos.length >= 10)
367
+ break;
368
+ }
357
369
  const phase0RepoSet = new Set(phase0Repos);
358
370
  if (phase0Repos.length > 0 && enabledStrategies.has("merged")) {
359
371
  const remaining = maxResults - allCandidates.length;
@@ -15,6 +15,8 @@ import { type SearchPriority, type IssueCandidate, type ProjectCategory } from "
15
15
  export interface ScoutStateReader {
16
16
  /** Repos where the user has at least one merged PR. */
17
17
  getReposWithMergedPRs(): string[];
18
+ /** Repos where the user has at least one open PR. */
19
+ getReposWithOpenPRs(): string[];
18
20
  /** User's starred repos (from GitHub). */
19
21
  getStarredRepos(): string[];
20
22
  /** Preferred project categories from user preferences. */
@@ -64,6 +64,11 @@ export declare const StoredClosedPRSchema: z.ZodObject<{
64
64
  title: z.ZodString;
65
65
  closedAt: z.ZodString;
66
66
  }, z.core.$strip>;
67
+ export declare const StoredOpenPRSchema: z.ZodObject<{
68
+ url: z.ZodString;
69
+ title: z.ZodString;
70
+ openedAt: z.ZodString;
71
+ }, z.core.$strip>;
67
72
  export declare const ContributionGuidelinesSchema: z.ZodObject<{
68
73
  branchNamingConvention: z.ZodOptional<z.ZodString>;
69
74
  commitMessageFormat: z.ZodOptional<z.ZodString>;
@@ -287,6 +292,11 @@ export declare const ScoutStateSchema: z.ZodObject<{
287
292
  title: z.ZodString;
288
293
  closedAt: z.ZodString;
289
294
  }, z.core.$strip>>>;
295
+ openPRs: z.ZodDefault<z.ZodArray<z.ZodObject<{
296
+ url: z.ZodString;
297
+ title: z.ZodString;
298
+ openedAt: z.ZodString;
299
+ }, z.core.$strip>>>;
290
300
  savedResults: z.ZodDefault<z.ZodArray<z.ZodObject<{
291
301
  issueUrl: z.ZodString;
292
302
  repo: z.ZodString;
@@ -322,6 +332,7 @@ export type RepoSignals = z.infer<typeof RepoSignalsSchema>;
322
332
  export type RepoScore = z.infer<typeof RepoScoreSchema>;
323
333
  export type StoredMergedPR = z.infer<typeof StoredMergedPRSchema>;
324
334
  export type StoredClosedPR = z.infer<typeof StoredClosedPRSchema>;
335
+ export type StoredOpenPR = z.infer<typeof StoredOpenPRSchema>;
325
336
  export type ContributionGuidelines = z.infer<typeof ContributionGuidelinesSchema>;
326
337
  export type IssueVettingResult = z.infer<typeof IssueVettingResultSchema>;
327
338
  export type TrackedIssue = z.infer<typeof TrackedIssueSchema>;
@@ -67,6 +67,11 @@ export const StoredClosedPRSchema = z.object({
67
67
  title: z.string(),
68
68
  closedAt: z.string(),
69
69
  });
70
+ export const StoredOpenPRSchema = z.object({
71
+ url: z.string(),
72
+ title: z.string(),
73
+ openedAt: z.string(),
74
+ });
70
75
  // ── Contribution schemas ────────────────────────────────────────────
71
76
  export const ContributionGuidelinesSchema = z.object({
72
77
  branchNamingConvention: z.string().optional(),
@@ -161,6 +166,7 @@ export const ScoutStateSchema = z.object({
161
166
  starredReposLastFetched: z.string().optional(),
162
167
  mergedPRs: z.array(StoredMergedPRSchema).default([]),
163
168
  closedPRs: z.array(StoredClosedPRSchema).default([]),
169
+ openPRs: z.array(StoredOpenPRSchema).default([]),
164
170
  savedResults: z.array(SavedCandidateSchema).default([]),
165
171
  skippedIssues: z.array(SkippedIssueSchema).default([]),
166
172
  lastSearchAt: z.string().optional(),
@@ -122,3 +122,10 @@ export interface ClosedPRRecord {
122
122
  closedAt: string;
123
123
  repo: string;
124
124
  }
125
+ /** Record of an open PR for state contribution. */
126
+ export interface OpenPRRecord {
127
+ url: string;
128
+ title: string;
129
+ openedAt: string;
130
+ repo: string;
131
+ }
package/dist/index.d.ts CHANGED
@@ -15,8 +15,8 @@
15
15
  * @packageDocumentation
16
16
  */
17
17
  export { createScout, OssScout } from "./scout.js";
18
- export type { ScoutConfig, SearchOptions, SearchResult, IssueCandidate, MergedPRRecord, ClosedPRRecord, RepoScoreUpdate, ProjectHealth, SearchPriority, CheckResult, VetListOptions, VetListResult, VetListEntry, VetListSummary, } from "./core/types.js";
19
- export type { ScoutState, ScoutPreferences, RepoScore, RepoSignals, IssueVettingResult, ContributionGuidelines, TrackedIssue, IssueScope, ProjectCategory, StoredMergedPR, StoredClosedPR, SearchStrategy, SkippedIssue, } from "./core/schemas.js";
18
+ export type { ScoutConfig, SearchOptions, SearchResult, IssueCandidate, MergedPRRecord, ClosedPRRecord, OpenPRRecord, RepoScoreUpdate, ProjectHealth, SearchPriority, CheckResult, VetListOptions, VetListResult, VetListEntry, VetListSummary, } from "./core/types.js";
19
+ export type { ScoutState, ScoutPreferences, RepoScore, RepoSignals, IssueVettingResult, ContributionGuidelines, TrackedIssue, IssueScope, ProjectCategory, StoredMergedPR, StoredClosedPR, StoredOpenPR, SearchStrategy, SkippedIssue, } from "./core/schemas.js";
20
20
  export { ScoutStateSchema, ScoutPreferencesSchema, RepoScoreSchema, IssueScopeSchema, ProjectCategorySchema, SearchStrategySchema, SkippedIssueSchema, } from "./core/schemas.js";
21
21
  export { requireGitHubToken, getGitHubToken } from "./core/utils.js";
22
22
  export { IssueDiscovery } from "./core/issue-discovery.js";
package/dist/scout.d.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import type { ScoutStateReader } from "./core/issue-vetting.js";
8
8
  import type { ScoutState, ScoutPreferences, RepoScore, SavedCandidate, SkippedIssue } from "./core/schemas.js";
9
- import type { ScoutConfig, SearchOptions, SearchResult, IssueCandidate, MergedPRRecord, ClosedPRRecord, RepoScoreUpdate, ProjectCategory, VetListOptions, VetListResult } from "./core/types.js";
9
+ import type { ScoutConfig, SearchOptions, SearchResult, IssueCandidate, MergedPRRecord, ClosedPRRecord, OpenPRRecord, RepoScoreUpdate, ProjectCategory, VetListOptions, VetListResult } from "./core/types.js";
10
10
  import { GistStateStore } from "./core/gist-state-store.js";
11
11
  /**
12
12
  * Create an OssScout instance.
@@ -59,6 +59,7 @@ export declare class OssScout implements ScoutStateReader {
59
59
  vetList(options?: VetListOptions): Promise<VetListResult>;
60
60
  private classifyVetResult;
61
61
  getReposWithMergedPRs(): string[];
62
+ getReposWithOpenPRs(): string[];
62
63
  getStarredRepos(): string[];
63
64
  getProjectCategories(): ProjectCategory[];
64
65
  getRepoScore(repo: string): number | null;
@@ -75,6 +76,11 @@ export declare class OssScout implements ScoutStateReader {
75
76
  * Record that a PR was closed without merge.
76
77
  */
77
78
  recordClosedPR(pr: ClosedPRRecord): void;
79
+ /**
80
+ * Record that a PR is currently open in this repo.
81
+ * Open PRs signal active engagement even when nothing is merged yet.
82
+ */
83
+ recordOpenPR(pr: OpenPRRecord): void;
78
84
  /**
79
85
  * Update repo score with observed signals.
80
86
  */
package/dist/scout.js CHANGED
@@ -236,6 +236,18 @@ export class OssScout {
236
236
  .sort((a, b) => b[1] - a[1])
237
237
  .map(([repo]) => repo);
238
238
  }
239
+ getReposWithOpenPRs() {
240
+ const repoCounts = new Map();
241
+ for (const pr of this.state.openPRs ?? []) {
242
+ const repo = extractRepoFromUrl(pr.url);
243
+ if (repo) {
244
+ repoCounts.set(repo, (repoCounts.get(repo) ?? 0) + 1);
245
+ }
246
+ }
247
+ return [...repoCounts.entries()]
248
+ .sort((a, b) => b[1] - a[1])
249
+ .map(([repo]) => repo);
250
+ }
239
251
  getStarredRepos() {
240
252
  return this.state.starredRepos;
241
253
  }
@@ -285,6 +297,20 @@ export class OssScout {
285
297
  this.updateRepoScoreFromPRs(pr.repo);
286
298
  this.dirty = true;
287
299
  }
300
+ /**
301
+ * Record that a PR is currently open in this repo.
302
+ * Open PRs signal active engagement even when nothing is merged yet.
303
+ */
304
+ recordOpenPR(pr) {
305
+ const existing = this.state.openPRs ?? [];
306
+ if (existing.some((p) => p.url === pr.url))
307
+ return;
308
+ this.state.openPRs = [
309
+ ...existing,
310
+ { url: pr.url, title: pr.title, openedAt: pr.openedAt },
311
+ ];
312
+ this.dirty = true;
313
+ }
288
314
  /**
289
315
  * Update repo score with observed signals.
290
316
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-scout/core",
3
- "version": "0.4.0",
3
+ "version": "0.5.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": {
@@ -55,11 +55,12 @@
55
55
  },
56
56
  "devDependencies": {
57
57
  "@types/node": "^25.5.0",
58
- "@vitest/coverage-v8": "^4.1.0",
58
+ "@vitest/coverage-v8": "^4.1.4",
59
59
  "esbuild": "^0.27.4",
60
60
  "tsx": "^4.21.0",
61
61
  "typescript": "^5.9.3",
62
- "vitest": "^4.1.0"
62
+ "vite": "^8.0.5",
63
+ "vitest": "^4.1.4"
63
64
  },
64
65
  "scripts": {
65
66
  "build": "tsc",