@oss-scout/core 0.11.0 → 1.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.
Files changed (68) hide show
  1. package/dist/cli.bundle.cjs +89 -66
  2. package/dist/cli.js +302 -436
  3. package/dist/commands/command-scout.d.ts +21 -0
  4. package/dist/commands/command-scout.js +21 -0
  5. package/dist/commands/config.js +10 -128
  6. package/dist/commands/features.js +15 -28
  7. package/dist/commands/results.d.ts +13 -2
  8. package/dist/commands/results.js +29 -2
  9. package/dist/commands/search.d.ts +4 -0
  10. package/dist/commands/search.js +65 -70
  11. package/dist/commands/setup.d.ts +2 -0
  12. package/dist/commands/setup.js +35 -6
  13. package/dist/commands/skip.d.ts +4 -0
  14. package/dist/commands/skip.js +45 -55
  15. package/dist/commands/sync.d.ts +10 -0
  16. package/dist/commands/sync.js +10 -0
  17. package/dist/commands/vet-list.js +3 -19
  18. package/dist/commands/vet.js +18 -25
  19. package/dist/commands/with-scout.d.ts +32 -0
  20. package/dist/commands/with-scout.js +41 -0
  21. package/dist/core/anti-llm-policy.js +5 -33
  22. package/dist/core/bootstrap.d.ts +2 -2
  23. package/dist/core/bootstrap.js +5 -9
  24. package/dist/core/errors.d.ts +10 -0
  25. package/dist/core/errors.js +20 -5
  26. package/dist/core/feature-discovery.d.ts +13 -1
  27. package/dist/core/feature-discovery.js +104 -81
  28. package/dist/core/gist-state-store.d.ts +13 -12
  29. package/dist/core/gist-state-store.js +128 -53
  30. package/dist/core/http-cache.d.ts +32 -2
  31. package/dist/core/http-cache.js +74 -19
  32. package/dist/core/issue-discovery.d.ts +12 -1
  33. package/dist/core/issue-discovery.js +94 -67
  34. package/dist/core/issue-eligibility.d.ts +11 -4
  35. package/dist/core/issue-eligibility.js +124 -69
  36. package/dist/core/issue-graphql.d.ts +58 -0
  37. package/dist/core/issue-graphql.js +108 -0
  38. package/dist/core/issue-vetting.d.ts +115 -9
  39. package/dist/core/issue-vetting.js +246 -109
  40. package/dist/core/local-state.d.ts +6 -2
  41. package/dist/core/local-state.js +23 -5
  42. package/dist/core/logger.d.ts +12 -4
  43. package/dist/core/logger.js +33 -7
  44. package/dist/core/personalization.d.ts +30 -10
  45. package/dist/core/personalization.js +64 -24
  46. package/dist/core/preference-fields.d.ts +47 -0
  47. package/dist/core/preference-fields.js +180 -0
  48. package/dist/core/probe-repo-file.d.ts +47 -0
  49. package/dist/core/probe-repo-file.js +57 -0
  50. package/dist/core/repo-health.js +40 -32
  51. package/dist/core/roadmap.js +26 -22
  52. package/dist/core/schemas.d.ts +148 -26
  53. package/dist/core/schemas.js +83 -17
  54. package/dist/core/search-budget.d.ts +9 -0
  55. package/dist/core/search-budget.js +36 -3
  56. package/dist/core/search-phases.d.ts +4 -21
  57. package/dist/core/search-phases.js +37 -89
  58. package/dist/core/types.d.ts +151 -38
  59. package/dist/core/utils.js +60 -26
  60. package/dist/formatters/human.d.ts +60 -0
  61. package/dist/formatters/human.js +199 -0
  62. package/dist/formatters/markdown.d.ts +10 -0
  63. package/dist/formatters/markdown.js +31 -0
  64. package/dist/index.d.ts +6 -2
  65. package/dist/index.js +8 -0
  66. package/dist/scout.d.ts +75 -12
  67. package/dist/scout.js +265 -26
  68. package/package.json +1 -1
package/dist/scout.js CHANGED
@@ -7,13 +7,13 @@
7
7
  import { IssueDiscovery } from "./core/issue-discovery.js";
8
8
  import { IssueVetter } from "./core/issue-vetting.js";
9
9
  import { discoverFeatures, discoverFeaturesBroad, } from "./core/feature-discovery.js";
10
- import { ScoutStateSchema } from "./core/schemas.js";
11
10
  import { GistStateStore, mergeStates } from "./core/gist-state-store.js";
12
11
  import { getOctokit } from "./core/github.js";
13
- import { loadLocalState } from "./core/local-state.js";
14
- import { warn } from "./core/logger.js";
15
- import { extractRepoFromUrl } from "./core/utils.js";
16
- import { errorMessage, getHttpStatusCode, isRateLimitError, } from "./core/errors.js";
12
+ import { loadLocalState, saveLocalState } from "./core/local-state.js";
13
+ import { warn, setLogLevel } from "./core/logger.js";
14
+ import { extractRepoFromUrl, parseGitHubUrl } from "./core/utils.js";
15
+ import { errorMessage, getHttpStatusCode, isRateLimitError, rethrowIfFatal, } from "./core/errors.js";
16
+ import { getHttpCache } from "./core/http-cache.js";
17
17
  /** Cause-specific user-facing message for degraded (offline) mode. */
18
18
  function offlineModeMessage(reason) {
19
19
  const tail = "Changes will only be saved locally.";
@@ -29,8 +29,12 @@ function offlineModeMessage(reason) {
29
29
  return `Gist sync unavailable — running in offline mode. ${tail}`;
30
30
  }
31
31
  }
32
- /** Wrap a real Octokit instance as GistOctokitLike without unsafe double casts. */
33
- function toGistOctokit(octokit) {
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) {
@@ -93,8 +97,13 @@ function toGistOctokit(octokit) {
93
97
  * ```
94
98
  */
95
99
  export async function createScout(config) {
100
+ // Apply the host's log-level preference before any bootstrap chatter (#156).
101
+ if (config.logLevel !== undefined) {
102
+ setLogLevel(config.logLevel);
103
+ }
96
104
  let state;
97
105
  let gistStore = null;
106
+ let persistLocal = false;
98
107
  if (config.persistence === "provided") {
99
108
  state = config.initialState;
100
109
  }
@@ -114,9 +123,15 @@ export async function createScout(config) {
114
123
  }
115
124
  }
116
125
  else {
117
- state = ScoutStateSchema.parse({ version: 1 });
118
- }
119
- return new OssScout(config.githubToken, state, gistStore);
126
+ // Default: local-file persistence. The previous else-branch silently
127
+ // created throwaway in-memory state, so a documented standalone scout
128
+ // (and the MCP server) read no preferences and persisted nothing while
129
+ // checkpoint() reported success (#116). Load the real state and save it
130
+ // on checkpoint.
131
+ state = loadLocalState();
132
+ persistLocal = true;
133
+ }
134
+ return new OssScout(config.githubToken, state, gistStore, { persistLocal });
120
135
  }
121
136
  /**
122
137
  * Main oss-scout class. Provides search, vetting, and state management.
@@ -129,29 +144,64 @@ export class OssScout {
129
144
  gistStore;
130
145
  state;
131
146
  dirty = false;
132
- constructor(githubToken, initialState, gistStore = null) {
147
+ /** When true, checkpoint() also writes ~/.oss-scout/state.json. */
148
+ persistLocal;
149
+ constructor(githubToken, initialState, gistStore = null, opts = {}) {
133
150
  this.githubToken = githubToken;
134
151
  this.gistStore = gistStore;
135
152
  this.state = initialState;
153
+ this.persistLocal = opts.persistLocal ?? false;
136
154
  }
137
155
  // ── Search ──────────────────────────────────────────────────────────
156
+ /**
157
+ * Drop stale disk-cache entries. Called at the top of every cache-burning
158
+ * entry point (search, features, vetList); without it ~/.oss-scout/cache
159
+ * grows without bound. evictStale never throws (fs errors degrade to warn).
160
+ */
161
+ evictStaleCacheEntries() {
162
+ getHttpCache().evictStale();
163
+ }
138
164
  /**
139
165
  * Multi-strategy issue search. Returns scored, sorted candidates.
140
166
  * Automatically culls expired skip entries and filters skipped issues.
141
167
  */
142
168
  async search(options) {
169
+ this.evictStaleCacheEntries();
143
170
  // Auto-cull expired skips before searching
144
171
  this.cullExpiredSkips();
145
172
  const skippedUrls = new Set((this.state.skippedIssues ?? []).map((s) => s.url));
146
173
  const discovery = new IssueDiscovery(this.githubToken, this.state.preferences, this);
174
+ // Per-call flags override the persisted personalization defaults (#168).
175
+ // An empty preference array reads as "no boost" just like an absent flag.
176
+ const prefs = this.state.preferences;
177
+ const prefLangs = prefs.preferLanguages ?? [];
178
+ const prefRepos = prefs.preferRepos ?? [];
179
+ const preferLanguages = options?.preferLanguages ??
180
+ (prefLangs.length > 0 ? prefLangs : undefined);
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);
187
+ const diversityRatio = options?.diversityRatio ?? prefs.diversityRatio ?? 0;
147
188
  const { candidates, strategiesUsed } = await discovery.searchIssues({
148
189
  maxResults: options?.maxResults,
149
190
  strategies: options?.strategies,
150
191
  skippedUrls,
151
- preferLanguages: options?.preferLanguages,
152
- preferRepos: options?.preferRepos,
153
- diversityRatio: options?.diversityRatio,
192
+ preferLanguages,
193
+ preferRepos,
194
+ avoidRepos,
195
+ boostIssueTypes,
196
+ diversityRatio,
197
+ interPhaseDelayMs: options?.interPhaseDelayMs,
198
+ broadPhaseDelayMs: options?.broadPhaseDelayMs,
154
199
  });
200
+ // Feed the freshly observed maintainer-responsiveness signals back into the
201
+ // repo scores so the next search ranks responsive/active repos higher (#167).
202
+ for (const c of candidates) {
203
+ this.updateRepoSignalsFromHealth(c.projectHealth);
204
+ }
155
205
  this.state.lastSearchAt = new Date().toISOString();
156
206
  this.dirty = true;
157
207
  return {
@@ -162,6 +212,27 @@ export class OssScout {
162
212
  strategiesUsed,
163
213
  };
164
214
  }
215
+ /**
216
+ * Populate the `hasActiveMaintainers` repo-score signal from a freshly
217
+ * computed projectHealth (#167). It was initialized false and never set, so
218
+ * calculateScore's +1 active-maintainers weight was inert; `isActive`
219
+ * (recent commit activity) is a real, already-computed proxy.
220
+ *
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.
228
+ */
229
+ updateRepoSignalsFromHealth(health) {
230
+ if (health.checkFailed)
231
+ return;
232
+ this.updateRepoScore(health.repo, {
233
+ signals: { hasActiveMaintainers: health.isActive },
234
+ });
235
+ }
165
236
  /**
166
237
  * Vet a single issue URL for claimability.
167
238
  */
@@ -184,6 +255,7 @@ export class OssScout {
184
255
  * preferences and excluded repos/orgs.
185
256
  */
186
257
  async features(options) {
258
+ this.evictStaleCacheEntries();
187
259
  const count = options?.count ?? 10;
188
260
  const octokit = getOctokit(this.githubToken);
189
261
  const vetter = new IssueVetter(octokit, this);
@@ -218,6 +290,7 @@ export class OssScout {
218
290
  * Optionally prunes unavailable issues from saved results.
219
291
  */
220
292
  async vetList(options) {
293
+ this.evictStaleCacheEntries();
221
294
  const saved = this.getSavedResults();
222
295
  const concurrency = options?.concurrency ?? 5;
223
296
  const results = [];
@@ -239,6 +312,7 @@ export class OssScout {
239
312
  number: item.number,
240
313
  title: item.title,
241
314
  status: this.classifyVetResult(candidate),
315
+ ok: true,
242
316
  recommendation: candidate.recommendation,
243
317
  viabilityScore: candidate.viabilityScore,
244
318
  });
@@ -256,6 +330,7 @@ export class OssScout {
256
330
  number: item.number,
257
331
  title: item.title,
258
332
  status: isGone ? "closed" : "error",
333
+ ok: false,
259
334
  errorMessage: errorMessage(error),
260
335
  });
261
336
  })
@@ -283,6 +358,33 @@ export class OssScout {
283
358
  hasPR: results.filter((r) => r.status === "has_pr").length,
284
359
  errors: results.filter((r) => r.status === "error").length,
285
360
  };
361
+ // Claim-watch (#165): compare each result's current status to the status
362
+ // recorded on the saved result last time, then persist the new status so
363
+ // the next run can diff again. "error" is transient — never a transition
364
+ // target and never stored.
365
+ const prevStatus = new Map((this.state.savedResults ?? []).map((r) => [r.issueUrl, r.lastStatus]));
366
+ const transitions = results
367
+ .filter((r) => r.status !== "error")
368
+ .filter((r) => {
369
+ const prev = prevStatus.get(r.issueUrl);
370
+ return prev !== undefined && prev !== r.status;
371
+ })
372
+ .map((r) => ({
373
+ issueUrl: r.issueUrl,
374
+ repo: r.repo,
375
+ number: r.number,
376
+ from: prevStatus.get(r.issueUrl),
377
+ to: r.status,
378
+ }));
379
+ const currentStatus = new Map(results.map((r) => [r.issueUrl, r.status]));
380
+ for (const saved of this.state.savedResults ?? []) {
381
+ const status = currentStatus.get(saved.issueUrl);
382
+ if (status !== undefined && status !== "error") {
383
+ saved.lastStatus = status;
384
+ }
385
+ }
386
+ if (results.length > 0)
387
+ this.dirty = true;
286
388
  let prunedCount;
287
389
  if (options?.prune) {
288
390
  const unavailableUrls = new Set(results
@@ -291,11 +393,18 @@ export class OssScout {
291
393
  const before = (this.state.savedResults ?? []).length;
292
394
  this.state.savedResults = (this.state.savedResults ?? []).filter((r) => !unavailableUrls.has(r.issueUrl));
293
395
  prunedCount = before - (this.state.savedResults?.length ?? 0);
396
+ if (prunedCount > 0)
397
+ this.addTombstones([...unavailableUrls]);
294
398
  this.dirty = true;
295
399
  }
296
- return { results, summary, prunedCount };
400
+ return { results, summary, prunedCount, transitions };
297
401
  }
298
402
  classifyVetResult(candidate) {
403
+ // Closed wins over everything: GitHub returns 200 for closed issues, so
404
+ // the 404/410 catch path alone never saw them (#120). Candidates cached
405
+ // by older versions lack issueState and read as open.
406
+ if (candidate.issueState === "closed")
407
+ return "closed";
299
408
  const checks = candidate.vettingResult.checks;
300
409
  if (!checks.noExistingPR)
301
410
  return "has_pr";
@@ -335,19 +444,35 @@ export class OssScout {
335
444
  getProjectCategories() {
336
445
  return this.state.preferences.projectCategories;
337
446
  }
447
+ /** Configured GitHub username (used to classify own vs competing PRs, #166). */
448
+ getGitHubUsername() {
449
+ return this.state.preferences.githubUsername;
450
+ }
338
451
  getRepoScore(repo) {
339
452
  const score = this.state.repoScores[repo];
340
453
  return score ? score.score : null;
341
454
  }
342
455
  /**
343
- * Optional SLM pre-triage config read from preferences (oss-autopilot#1122).
344
- * Empty `model` disables the call; the vetter treats it as a no-op.
456
+ * Number of the user's PRs closed without merge in this repo (#125).
457
+ * Prefers the tracked repo score; falls back to counting closedPRs so the
458
+ * scoring penalty works even before a score record exists.
459
+ */
460
+ getClosedWithoutMergeCount(repo) {
461
+ const score = this.state.repoScores[repo];
462
+ if (score)
463
+ return score.closedWithoutMergeCount;
464
+ return (this.state.closedPRs ?? []).filter((p) => extractRepoFromUrl(p.url) === repo).length;
465
+ }
466
+ /**
467
+ * SLM pre-triage config read from preferences (oss-autopilot#1122). Returns
468
+ * `null` when no `slmTriageModel` is configured — the vetter skips the SLM
469
+ * call entirely (#158).
345
470
  */
346
471
  getSLMTriageConfig() {
347
- return {
348
- model: this.state.preferences.slmTriageModel ?? "",
349
- host: this.state.preferences.slmTriageHost ?? "",
350
- };
472
+ const model = this.state.preferences.slmTriageModel ?? "";
473
+ if (!model)
474
+ return null;
475
+ return { model, host: this.state.preferences.slmTriageHost ?? "" };
351
476
  }
352
477
  /** Get current preferences (read-only). */
353
478
  getPreferences() {
@@ -402,6 +527,78 @@ export class OssScout {
402
527
  ];
403
528
  this.dirty = true;
404
529
  }
530
+ /**
531
+ * Reconcile tracked open PRs against their current GitHub state (#164).
532
+ *
533
+ * `state.openPRs` was append-only — nothing transitioned an open PR to
534
+ * merged/closed, so getReposWithOpenPRs() over-reported forever once a PR
535
+ * merged. This checks each open PR, records merges/closures (which updates
536
+ * the repo score), prunes resolved entries, and checkpoints. Cheaper than a
537
+ * full bootstrap, so a host can call it on daily startup. Transient errors
538
+ * leave the entry in place; auth/rate-limit failures propagate.
539
+ */
540
+ async syncOpenPRs() {
541
+ const octokit = getOctokit(this.githubToken);
542
+ const open = this.state.openPRs ?? [];
543
+ const result = {
544
+ checked: open.length,
545
+ merged: 0,
546
+ closed: 0,
547
+ stillOpen: 0,
548
+ errors: 0,
549
+ };
550
+ const remaining = [];
551
+ for (const pr of open) {
552
+ const parsed = parseGitHubUrl(pr.url);
553
+ if (!parsed || parsed.type !== "pull") {
554
+ remaining.push(pr);
555
+ result.errors++;
556
+ continue;
557
+ }
558
+ const repoFullName = `${parsed.owner}/${parsed.repo}`;
559
+ try {
560
+ const { data } = await octokit.pulls.get({
561
+ owner: parsed.owner,
562
+ repo: parsed.repo,
563
+ pull_number: parsed.number,
564
+ });
565
+ if (data.merged) {
566
+ this.recordMergedPR({
567
+ url: pr.url,
568
+ title: pr.title,
569
+ mergedAt: data.merged_at ?? new Date().toISOString(),
570
+ repo: repoFullName,
571
+ });
572
+ result.merged++;
573
+ }
574
+ else if (data.state === "closed") {
575
+ this.recordClosedPR({
576
+ url: pr.url,
577
+ title: pr.title,
578
+ closedAt: data.closed_at ?? new Date().toISOString(),
579
+ repo: repoFullName,
580
+ });
581
+ result.closed++;
582
+ }
583
+ else {
584
+ remaining.push(pr);
585
+ result.stillOpen++;
586
+ }
587
+ }
588
+ catch (err) {
589
+ // Auth/rate-limit aborts the whole sync; a transient/404 leaves the
590
+ // entry untouched so a later sync can retry rather than losing it.
591
+ rethrowIfFatal(err);
592
+ warn("scout", `sync: could not check ${pr.url}: ${errorMessage(err)}`);
593
+ remaining.push(pr);
594
+ result.errors++;
595
+ }
596
+ }
597
+ this.state.openPRs = remaining;
598
+ this.dirty = true;
599
+ await this.checkpoint();
600
+ return result;
601
+ }
405
602
  /**
406
603
  * Update repo score with observed signals.
407
604
  */
@@ -437,6 +634,9 @@ export class OssScout {
437
634
  */
438
635
  updatePreferences(updates) {
439
636
  this.state.preferences = { ...this.state.preferences, ...updates };
637
+ // Stamp so the gist merge keeps the fresher preferences instead of
638
+ // always taking the remote copy (#117).
639
+ this.state.preferencesUpdatedAt = new Date().toISOString();
440
640
  this.dirty = true;
441
641
  }
442
642
  /**
@@ -486,9 +686,25 @@ export class OssScout {
486
686
  * Clear all saved results.
487
687
  */
488
688
  clearResults() {
689
+ this.addTombstones((this.state.savedResults ?? []).map((r) => r.issueUrl));
489
690
  this.state.savedResults = [];
490
691
  this.dirty = true;
491
692
  }
693
+ /**
694
+ * Record deletion tombstones (#117) so a later gist merge does not
695
+ * resurrect these URLs from another machine's copy. A re-add with a newer
696
+ * timestamp overrides the tombstone in mergeStates.
697
+ */
698
+ addTombstones(urls) {
699
+ if (urls.length === 0)
700
+ return;
701
+ const removedAt = new Date().toISOString();
702
+ const existing = this.state.tombstones ?? [];
703
+ const byUrl = new Map(existing.map((t) => [t.url, t]));
704
+ for (const url of urls)
705
+ byUrl.set(url, { url, removedAt });
706
+ this.state.tombstones = [...byUrl.values()];
707
+ }
492
708
  // ── Skip List ───────────────────────────────────────────────────────
493
709
  /**
494
710
  * Skip an issue — excludes it from future searches. Auto-culled after 90 days.
@@ -507,7 +723,10 @@ export class OssScout {
507
723
  skippedAt: new Date().toISOString(),
508
724
  },
509
725
  ];
510
- // Also remove from saved results if present
726
+ // Also remove from saved results if present. No tombstone needed: the
727
+ // skip entry itself is the durable record, and mergeStates reconciles
728
+ // saved results against the skip list so a merge can't resurrect a
729
+ // skipped URL into the saved list (#117).
511
730
  if (this.state.savedResults) {
512
731
  this.state.savedResults = this.state.savedResults.filter((r) => r.issueUrl !== url);
513
732
  }
@@ -523,13 +742,17 @@ export class OssScout {
523
742
  * Remove a specific issue from the skip list.
524
743
  */
525
744
  unskipIssue(url) {
745
+ const before = (this.state.skippedIssues ?? []).length;
526
746
  this.state.skippedIssues = (this.state.skippedIssues ?? []).filter((s) => s.url !== url);
747
+ if (this.state.skippedIssues.length < before)
748
+ this.addTombstones([url]);
527
749
  this.dirty = true;
528
750
  }
529
751
  /**
530
752
  * Clear all skipped issues.
531
753
  */
532
754
  clearSkippedIssues() {
755
+ this.addTombstones((this.state.skippedIssues ?? []).map((s) => s.url));
533
756
  this.state.skippedIssues = [];
534
757
  this.dirty = true;
535
758
  }
@@ -573,6 +796,17 @@ export class OssScout {
573
796
  if (!ok)
574
797
  return false;
575
798
  }
799
+ if (this.persistLocal) {
800
+ // Honest persistence: in local mode the previous no-op return true
801
+ // claimed success while saving nothing (#116). A failed write keeps
802
+ // the dirty flag and reports failure.
803
+ try {
804
+ saveLocalState(this.state);
805
+ }
806
+ catch {
807
+ return false;
808
+ }
809
+ }
576
810
  this.dirty = false;
577
811
  return true;
578
812
  }
@@ -597,16 +831,21 @@ export class OssScout {
597
831
  });
598
832
  }
599
833
  /**
600
- * Calculate repo score (1-10) from observed data.
834
+ * Calculate repo score from observed data.
601
835
  * base 5, +1 per merged PR (max +3), -1 per closed-without-merge (max -3),
602
- * +1 responsive, +1 active maintainers, -2 hostile comments, clamped 1-10
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.
603
844
  */
604
845
  calculateScore(repoScore) {
605
846
  let score = 5;
606
847
  score += Math.min(repoScore.mergedPRCount, 3);
607
848
  score -= Math.min(repoScore.closedWithoutMergeCount, 3);
608
- if (repoScore.signals.isResponsive)
609
- score += 1;
610
849
  if (repoScore.signals.hasActiveMaintainers)
611
850
  score += 1;
612
851
  if (repoScore.signals.hasHostileComments)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oss-scout/core",
3
- "version": "0.11.0",
3
+ "version": "1.1.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": {