@oss-scout/core 0.2.0 → 0.2.1

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 (54) hide show
  1. package/dist/cli.bundle.cjs +42 -42
  2. package/dist/cli.js +110 -86
  3. package/dist/commands/config.d.ts +1 -1
  4. package/dist/commands/config.js +76 -72
  5. package/dist/commands/results.d.ts +1 -1
  6. package/dist/commands/results.js +1 -1
  7. package/dist/commands/search.d.ts +2 -2
  8. package/dist/commands/search.js +16 -6
  9. package/dist/commands/setup.d.ts +1 -1
  10. package/dist/commands/setup.js +27 -21
  11. package/dist/commands/validation.d.ts +1 -1
  12. package/dist/commands/validation.js +1 -1
  13. package/dist/commands/vet-list.d.ts +2 -2
  14. package/dist/commands/vet-list.js +12 -5
  15. package/dist/commands/vet.d.ts +3 -3
  16. package/dist/commands/vet.js +9 -5
  17. package/dist/core/bootstrap.d.ts +1 -1
  18. package/dist/core/bootstrap.js +20 -16
  19. package/dist/core/category-mapping.d.ts +1 -1
  20. package/dist/core/category-mapping.js +104 -13
  21. package/dist/core/errors.d.ts +8 -1
  22. package/dist/core/errors.js +31 -19
  23. package/dist/core/gist-state-store.d.ts +1 -1
  24. package/dist/core/gist-state-store.js +36 -27
  25. package/dist/core/github.d.ts +1 -1
  26. package/dist/core/github.js +5 -5
  27. package/dist/core/http-cache.js +26 -22
  28. package/dist/core/issue-discovery.d.ts +3 -3
  29. package/dist/core/issue-discovery.js +325 -277
  30. package/dist/core/issue-eligibility.d.ts +2 -2
  31. package/dist/core/issue-eligibility.js +26 -21
  32. package/dist/core/issue-filtering.js +23 -15
  33. package/dist/core/issue-scoring.js +1 -1
  34. package/dist/core/issue-vetting.d.ts +2 -2
  35. package/dist/core/issue-vetting.js +66 -53
  36. package/dist/core/local-state.d.ts +1 -1
  37. package/dist/core/local-state.js +16 -14
  38. package/dist/core/repo-health.d.ts +2 -2
  39. package/dist/core/repo-health.js +46 -35
  40. package/dist/core/schemas.d.ts +1 -1
  41. package/dist/core/schemas.js +40 -18
  42. package/dist/core/search-budget.js +3 -3
  43. package/dist/core/search-phases.d.ts +6 -6
  44. package/dist/core/search-phases.js +23 -19
  45. package/dist/core/types.d.ts +9 -9
  46. package/dist/core/types.js +15 -3
  47. package/dist/core/utils.d.ts +10 -1
  48. package/dist/core/utils.js +44 -25
  49. package/dist/formatters/json.d.ts +1 -1
  50. package/dist/index.d.ts +7 -7
  51. package/dist/index.js +5 -5
  52. package/dist/scout.d.ts +4 -5
  53. package/dist/scout.js +72 -31
  54. package/package.json +1 -1
@@ -4,7 +4,7 @@
4
4
  * Stores ScoutState as a private GitHub Gist, with a local file cache
5
5
  * as fallback when the API is unavailable.
6
6
  */
7
- import type { ScoutState } from './schemas.js';
7
+ import type { ScoutState } from "./schemas.js";
8
8
  /** Minimal Octokit interface for gist operations — keeps the class testable. */
9
9
  export interface GistOctokitLike {
10
10
  gists: {
@@ -4,17 +4,17 @@
4
4
  * Stores ScoutState as a private GitHub Gist, with a local file cache
5
5
  * as fallback when the API is unavailable.
6
6
  */
7
- import * as fs from 'fs';
8
- import * as path from 'path';
9
- import { ScoutStateSchema } from './schemas.js';
10
- import { getDataDir } from './utils.js';
11
- import { debug, warn } from './logger.js';
12
- import { errorMessage } from './errors.js';
13
- const MODULE = 'gist-state';
14
- const GIST_DESCRIPTION = 'oss-scout-state';
15
- const GIST_FILENAME = 'state.json';
16
- const GIST_ID_FILE = 'gist-id';
17
- const CACHE_FILE = 'state-cache.json';
7
+ import * as fs from "fs";
8
+ import * as path from "path";
9
+ import { ScoutStateSchema } from "./schemas.js";
10
+ import { getDataDir } from "./utils.js";
11
+ import { debug, warn } from "./logger.js";
12
+ import { errorMessage } from "./errors.js";
13
+ const MODULE = "gist-state";
14
+ const GIST_DESCRIPTION = "oss-scout-state";
15
+ const GIST_FILENAME = "state.json";
16
+ const GIST_ID_FILE = "gist-id";
17
+ const CACHE_FILE = "state-cache.json";
18
18
  const SEARCH_MAX_PAGES = 5;
19
19
  function getGistIdPath() {
20
20
  return path.join(getDataDir(), GIST_ID_FILE);
@@ -47,7 +47,7 @@ export class GistStateStore {
47
47
  async push(state) {
48
48
  this.writeCache(state);
49
49
  if (!this.gistId) {
50
- warn(MODULE, 'No gist ID — cannot push');
50
+ warn(MODULE, "No gist ID — cannot push");
51
51
  return false;
52
52
  }
53
53
  try {
@@ -57,7 +57,7 @@ export class GistStateStore {
57
57
  [GIST_FILENAME]: { content: JSON.stringify(state, null, 2) },
58
58
  },
59
59
  });
60
- debug(MODULE, 'State pushed to gist');
60
+ debug(MODULE, "State pushed to gist");
61
61
  return true;
62
62
  }
63
63
  catch (err) {
@@ -104,7 +104,7 @@ export class GistStateStore {
104
104
  catch (err) {
105
105
  debug(MODULE, `Cached gist ID invalid: ${errorMessage(err)}`);
106
106
  }
107
- debug(MODULE, 'Cached gist ID invalid, searching...');
107
+ debug(MODULE, "Cached gist ID invalid, searching...");
108
108
  }
109
109
  // 2. Search user's gists
110
110
  const foundId = await this.searchForGist();
@@ -117,9 +117,13 @@ export class GistStateStore {
117
117
  this.writeCache(state);
118
118
  return { gistId: foundId, state, created: false };
119
119
  }
120
+ // Gist exists but content failed validation — fall back to cache
121
+ // to avoid overwriting the user's data by creating a new gist.
122
+ warn(MODULE, `Found existing gist ${foundId} but content failed validation. Using local cache to avoid data loss.`);
123
+ return this.bootstrapFromCache();
120
124
  }
121
125
  // 3. Create new gist
122
- debug(MODULE, 'No existing gist found, creating new one');
126
+ debug(MODULE, "No existing gist found, creating new one");
123
127
  const freshState = ScoutStateSchema.parse({ version: 1 });
124
128
  const newId = await this.createGist(freshState);
125
129
  this.saveGistId(newId);
@@ -130,20 +134,20 @@ export class GistStateStore {
130
134
  bootstrapFromCache() {
131
135
  const cached = this.readCache();
132
136
  if (cached) {
133
- debug(MODULE, 'Bootstrapped from local cache (degraded mode)');
137
+ debug(MODULE, "Bootstrapped from local cache (degraded mode)");
134
138
  const cachedId = this.readCachedGistId();
135
139
  if (cachedId)
136
140
  this.gistId = cachedId;
137
141
  return {
138
- gistId: cachedId ?? '',
142
+ gistId: cachedId ?? "",
139
143
  state: cached,
140
144
  created: false,
141
145
  degraded: true,
142
146
  };
143
147
  }
144
- debug(MODULE, 'No cache available, using fresh state (degraded mode)');
148
+ debug(MODULE, "No cache available, using fresh state (degraded mode)");
145
149
  const fresh = ScoutStateSchema.parse({ version: 1 });
146
- return { gistId: '', state: fresh, created: false, degraded: true };
150
+ return { gistId: "", state: fresh, created: false, degraded: true };
147
151
  }
148
152
  // ── Gist API operations ──────────────────────────────────────────────
149
153
  async fetchGistState(gistId) {
@@ -187,28 +191,28 @@ export class GistStateStore {
187
191
  // ── Local file helpers ───────────────────────────────────────────────
188
192
  readCachedGistId() {
189
193
  try {
190
- const id = fs.readFileSync(getGistIdPath(), 'utf-8').trim();
194
+ const id = fs.readFileSync(getGistIdPath(), "utf-8").trim();
191
195
  return id || null;
192
196
  }
193
197
  catch (err) {
194
198
  const code = err?.code;
195
- if (code !== 'ENOENT') {
199
+ if (code !== "ENOENT") {
196
200
  warn(MODULE, `Failed to read cached gist ID: ${errorMessage(err)}`);
197
201
  }
198
202
  return null;
199
203
  }
200
204
  }
201
205
  saveGistId(id) {
202
- fs.writeFileSync(getGistIdPath(), id + '\n', { mode: 0o600 });
206
+ fs.writeFileSync(getGistIdPath(), id + "\n", { mode: 0o600 });
203
207
  }
204
208
  readCache() {
205
209
  try {
206
- const raw = fs.readFileSync(getCachePath(), 'utf-8');
210
+ const raw = fs.readFileSync(getCachePath(), "utf-8");
207
211
  return ScoutStateSchema.parse(JSON.parse(raw));
208
212
  }
209
213
  catch (err) {
210
214
  const code = err?.code;
211
- if (code !== 'ENOENT') {
215
+ if (code !== "ENOENT") {
212
216
  warn(MODULE, `Failed to read state cache: ${errorMessage(err)}`);
213
217
  }
214
218
  return null;
@@ -216,7 +220,9 @@ export class GistStateStore {
216
220
  }
217
221
  writeCache(state) {
218
222
  try {
219
- fs.writeFileSync(getCachePath(), JSON.stringify(state, null, 2) + '\n', { mode: 0o600 });
223
+ fs.writeFileSync(getCachePath(), JSON.stringify(state, null, 2) + "\n", {
224
+ mode: 0o600,
225
+ });
220
226
  }
221
227
  catch (err) {
222
228
  warn(MODULE, `Failed to write cache: ${errorMessage(err)}`);
@@ -243,7 +249,8 @@ export function mergeStates(local, remote) {
243
249
  closedPRs: unionByUrl(local.closedPRs, remote.closedPRs),
244
250
  savedResults: mergeSavedResults(local.savedResults ?? [], remote.savedResults ?? []),
245
251
  lastSearchAt: pickFresherTimestamp(local.lastSearchAt, remote.lastSearchAt),
246
- lastRunAt: pickFresherTimestamp(local.lastRunAt, remote.lastRunAt) ?? new Date().toISOString(),
252
+ lastRunAt: pickFresherTimestamp(local.lastRunAt, remote.lastRunAt) ??
253
+ new Date().toISOString(),
247
254
  gistId: remote.gistId ?? local.gistId,
248
255
  };
249
256
  }
@@ -266,7 +273,9 @@ function mergeStarredRepos(local, remote) {
266
273
  const localTs = local.starredReposLastFetched;
267
274
  const remoteTs = remote.starredReposLastFetched;
268
275
  if (!localTs && !remoteTs)
269
- return remote.starredRepos.length >= local.starredRepos.length ? remote.starredRepos : local.starredRepos;
276
+ return remote.starredRepos.length >= local.starredRepos.length
277
+ ? remote.starredRepos
278
+ : local.starredRepos;
270
279
  if (!localTs)
271
280
  return remote.starredRepos;
272
281
  if (!remoteTs)
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Shared GitHub API client with rate limiting and throttling.
3
3
  */
4
- import { Octokit } from '@octokit/rest';
4
+ import { Octokit } from "@octokit/rest";
5
5
  interface RateLimitInfo {
6
6
  remaining: number;
7
7
  limit: number;
@@ -1,15 +1,15 @@
1
1
  /**
2
2
  * Shared GitHub API client with rate limiting and throttling.
3
3
  */
4
- import { Octokit } from '@octokit/rest';
5
- import { throttling } from '@octokit/plugin-throttling';
6
- import { warn } from './logger.js';
7
- const MODULE = 'github';
4
+ import { Octokit } from "@octokit/rest";
5
+ import { throttling } from "@octokit/plugin-throttling";
6
+ import { warn } from "./logger.js";
7
+ const MODULE = "github";
8
8
  const ThrottledOctokit = Octokit.plugin(throttling);
9
9
  let _octokit = null;
10
10
  let _currentToken = null;
11
11
  function formatResetTime(date) {
12
- return date.toLocaleTimeString('en-US', { hour12: false });
12
+ return date.toLocaleTimeString("en-US", { hour12: false });
13
13
  }
14
14
  export function getRateLimitCallbacks() {
15
15
  return {
@@ -9,13 +9,13 @@
9
9
  * for the same endpoint (e.g., star counts for two PRs in the same repo)
10
10
  * share a single HTTP round-trip.
11
11
  */
12
- import * as fs from 'fs';
13
- import * as path from 'path';
14
- import * as crypto from 'crypto';
15
- import { getCacheDir } from './utils.js';
16
- import { debug, warn } from './logger.js';
17
- import { errorMessage, getHttpStatusCode } from './errors.js';
18
- const MODULE = 'http-cache';
12
+ import * as fs from "fs";
13
+ import * as path from "path";
14
+ import * as crypto from "crypto";
15
+ import { getCacheDir } from "./utils.js";
16
+ import { debug, warn } from "./logger.js";
17
+ import { errorMessage, getHttpStatusCode } from "./errors.js";
18
+ const MODULE = "http-cache";
19
19
  /**
20
20
  * Maximum age (in ms) before a cache entry is considered stale and eligible for
21
21
  * eviction during cleanup. Defaults to 24 hours. Entries older than this are
@@ -39,7 +39,7 @@ export class HttpCache {
39
39
  }
40
40
  /** Derive a filesystem-safe cache key from a URL. */
41
41
  keyFor(url) {
42
- return crypto.createHash('sha256').update(url).digest('hex');
42
+ return crypto.createHash("sha256").update(url).digest("hex");
43
43
  }
44
44
  /** Full path to the cache file for a given URL. */
45
45
  pathFor(url) {
@@ -65,7 +65,7 @@ export class HttpCache {
65
65
  get(url) {
66
66
  const filePath = this.pathFor(url);
67
67
  try {
68
- const raw = fs.readFileSync(filePath, 'utf-8');
68
+ const raw = fs.readFileSync(filePath, "utf-8");
69
69
  const entry = JSON.parse(raw);
70
70
  // Sanity-check: the file should contain the URL we asked for
71
71
  if (entry.url !== url) {
@@ -76,7 +76,7 @@ export class HttpCache {
76
76
  }
77
77
  catch (err) {
78
78
  const code = err?.code;
79
- if (code === 'ENOENT')
79
+ if (code === "ENOENT")
80
80
  return null;
81
81
  if (err instanceof SyntaxError) {
82
82
  debug(MODULE, `Corrupt cache entry, deleting: ${url}`);
@@ -103,7 +103,10 @@ export class HttpCache {
103
103
  cachedAt: new Date().toISOString(),
104
104
  };
105
105
  try {
106
- fs.writeFileSync(this.pathFor(url), JSON.stringify(entry), { encoding: 'utf-8', mode: 0o600 });
106
+ fs.writeFileSync(this.pathFor(url), JSON.stringify(entry), {
107
+ encoding: "utf-8",
108
+ mode: 0o600,
109
+ });
107
110
  debug(MODULE, `Cached response for ${url}`);
108
111
  }
109
112
  catch (err) {
@@ -137,11 +140,11 @@ export class HttpCache {
137
140
  const files = fs.readdirSync(this.cacheDir);
138
141
  const now = Date.now();
139
142
  for (const file of files) {
140
- if (!file.endsWith('.json'))
143
+ if (!file.endsWith(".json"))
141
144
  continue;
142
145
  const filePath = path.join(this.cacheDir, file);
143
146
  try {
144
- const raw = fs.readFileSync(filePath, 'utf-8');
147
+ const raw = fs.readFileSync(filePath, "utf-8");
145
148
  const entry = JSON.parse(raw);
146
149
  const age = now - new Date(entry.cachedAt).getTime();
147
150
  if (age > maxAgeMs) {
@@ -163,7 +166,7 @@ export class HttpCache {
163
166
  }
164
167
  catch (err) {
165
168
  const code = err?.code;
166
- if (code !== 'ENOENT') {
169
+ if (code !== "ENOENT") {
167
170
  warn(MODULE, `Failed to evict stale cache entries: ${errorMessage(err)}`);
168
171
  }
169
172
  }
@@ -179,15 +182,15 @@ export class HttpCache {
179
182
  try {
180
183
  const files = fs.readdirSync(this.cacheDir);
181
184
  for (const file of files) {
182
- if (!file.endsWith('.json'))
185
+ if (!file.endsWith(".json"))
183
186
  continue;
184
187
  fs.unlinkSync(path.join(this.cacheDir, file));
185
188
  }
186
- debug(MODULE, 'Cache cleared');
189
+ debug(MODULE, "Cache cleared");
187
190
  }
188
191
  catch (err) {
189
192
  const code = err?.code;
190
- if (code !== 'ENOENT') {
193
+ if (code !== "ENOENT") {
191
194
  warn(MODULE, `Failed to clear cache: ${errorMessage(err)}`);
192
195
  }
193
196
  }
@@ -197,11 +200,12 @@ export class HttpCache {
197
200
  */
198
201
  size() {
199
202
  try {
200
- return fs.readdirSync(this.cacheDir).filter((f) => f.endsWith('.json')).length;
203
+ return fs.readdirSync(this.cacheDir).filter((f) => f.endsWith(".json"))
204
+ .length;
201
205
  }
202
206
  catch (err) {
203
207
  const code = err?.code;
204
- if (code !== 'ENOENT') {
208
+ if (code !== "ENOENT") {
205
209
  debug(MODULE, `Failed to read cache size: ${errorMessage(err)}`);
206
210
  }
207
211
  return 0;
@@ -253,12 +257,12 @@ export async function cachedRequest(cache, url, fetcher) {
253
257
  const extraHeaders = {};
254
258
  const cached = cache.get(url);
255
259
  if (cached) {
256
- extraHeaders['if-none-match'] = cached.etag;
260
+ extraHeaders["if-none-match"] = cached.etag;
257
261
  }
258
262
  try {
259
263
  const response = await fetcher(extraHeaders);
260
264
  // Store ETag if present (headers may be absent in test mocks)
261
- const etag = response.headers?.['etag'];
265
+ const etag = response.headers?.["etag"];
262
266
  if (etag) {
263
267
  cache.set(url, etag, response.data);
264
268
  }
@@ -302,7 +306,7 @@ export async function cachedTimeBased(cache, key, maxAgeMs, fetcher) {
302
306
  return cached;
303
307
  }
304
308
  const result = await fetcher();
305
- cache.set(key, '', result);
309
+ cache.set(key, "", result);
306
310
  return result;
307
311
  }
308
312
  /**
@@ -11,9 +11,9 @@
11
11
  *
12
12
  * All state is injected via constructor parameters (ScoutStateReader + ScoutPreferences).
13
13
  */
14
- import { type IssueCandidate } from './types.js';
15
- import type { ScoutPreferences, SearchStrategy } from './schemas.js';
16
- import { type ScoutStateReader } from './issue-vetting.js';
14
+ import { type IssueCandidate } from "./types.js";
15
+ import type { ScoutPreferences, SearchStrategy } from "./schemas.js";
16
+ import { type ScoutStateReader } from "./issue-vetting.js";
17
17
  /**
18
18
  * Multi-phase issue discovery engine that searches GitHub for contributable issues.
19
19
  *