@oss-scout/core 0.1.1 → 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 +48 -48
  2. package/dist/cli.js +110 -86
  3. package/dist/commands/config.d.ts +1 -1
  4. package/dist/commands/config.js +77 -71
  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 -270
  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 +3 -1
  41. package/dist/core/schemas.js +41 -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
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Search command — finds contributable issues using multi-strategy search.
3
3
  */
4
- import type { ScoutState, SearchStrategy } from '../core/schemas.js';
4
+ import type { ScoutState, SearchStrategy } from "../core/schemas.js";
5
5
  export interface SearchOutput {
6
6
  candidates: Array<{
7
7
  issue: {
@@ -12,7 +12,7 @@ export interface SearchOutput {
12
12
  url: string;
13
13
  labels: string[];
14
14
  };
15
- recommendation: 'approve' | 'skip' | 'needs_review';
15
+ recommendation: "approve" | "skip" | "needs_review";
16
16
  reasonsToApprove: string[];
17
17
  reasonsToSkip: string[];
18
18
  searchPriority: string;
@@ -1,19 +1,29 @@
1
1
  /**
2
2
  * Search command — finds contributable issues using multi-strategy search.
3
3
  */
4
- import { createScout } from '../scout.js';
5
- import { requireGitHubToken } from '../core/utils.js';
6
- import { saveLocalState } from '../core/local-state.js';
4
+ import { createScout } from "../scout.js";
5
+ import { requireGitHubToken } from "../core/utils.js";
6
+ import { saveLocalState } from "../core/local-state.js";
7
7
  export async function runSearch(options) {
8
8
  const token = requireGitHubToken();
9
9
  const scout = options.state
10
- ? await createScout({ githubToken: token, persistence: 'provided', initialState: options.state })
10
+ ? await createScout({
11
+ githubToken: token,
12
+ persistence: "provided",
13
+ initialState: options.state,
14
+ })
11
15
  : await createScout({ githubToken: token });
12
- const result = await scout.search({ maxResults: options.maxResults, strategies: options.strategies });
16
+ const result = await scout.search({
17
+ maxResults: options.maxResults,
18
+ strategies: options.strategies,
19
+ });
13
20
  // Persist results to local state and gist
14
21
  scout.saveResults(result.candidates);
15
22
  saveLocalState(scout.getState());
16
- await scout.checkpoint();
23
+ const persisted = await scout.checkpoint();
24
+ if (!persisted) {
25
+ console.error("Warning: changes saved locally but gist sync failed.");
26
+ }
17
27
  return {
18
28
  candidates: result.candidates.map((c) => {
19
29
  const repoScoreRecord = scout.getRepoScoreRecord(c.issue.repo);
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Setup command — interactive first-run configuration for oss-scout.
3
3
  */
4
- import type { ScoutPreferences } from '../core/schemas.js';
4
+ import type { ScoutPreferences } from "../core/schemas.js";
5
5
  interface ReadlineInterface {
6
6
  question(query: string, callback: (answer: string) => void): void;
7
7
  close(): void;
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Setup command — interactive first-run configuration for oss-scout.
3
3
  */
4
- import * as readline from 'readline';
5
- import { execFile } from 'child_process';
6
- import { ScoutPreferencesSchema, ProjectCategorySchema, IssueScopeSchema } from '../core/schemas.js';
4
+ import * as readline from "readline";
5
+ import { execFile } from "child_process";
6
+ import { ScoutPreferencesSchema, ProjectCategorySchema, IssueScopeSchema, } from "../core/schemas.js";
7
7
  const ALL_CATEGORIES = ProjectCategorySchema.options;
8
8
  const ALL_SCOPES = IssueScopeSchema.options;
9
9
  function createReadlineInterface() {
@@ -19,9 +19,9 @@ function ask(rl, query) {
19
19
  }
20
20
  function detectGitHubUsername() {
21
21
  return new Promise((resolve) => {
22
- execFile('gh', ['api', 'user', '--jq', '.login'], { timeout: 5000 }, (err, stdout) => {
22
+ execFile("gh", ["api", "user", "--jq", ".login"], { timeout: 5000 }, (err, stdout) => {
23
23
  if (err || !stdout.trim()) {
24
- resolve('');
24
+ resolve("");
25
25
  }
26
26
  else {
27
27
  resolve(stdout.trim());
@@ -31,7 +31,7 @@ function detectGitHubUsername() {
31
31
  }
32
32
  function parseCSV(input) {
33
33
  return input
34
- .split(',')
34
+ .split(",")
35
35
  .map((s) => s.trim())
36
36
  .filter((s) => s.length > 0);
37
37
  }
@@ -48,40 +48,46 @@ export async function runSetup(options) {
48
48
  const rl = options?.rl ?? createReadlineInterface();
49
49
  const detect = options?.detectUsername ?? detectGitHubUsername;
50
50
  try {
51
- console.log('\n🔧 oss-scout setup\n');
51
+ console.log("\n🔧 oss-scout setup\n");
52
52
  // Detect GitHub username
53
- console.log('Detecting GitHub username...');
53
+ console.log("Detecting GitHub username...");
54
54
  const detectedUsername = await detect();
55
- const usernameDefault = detectedUsername || '';
55
+ const usernameDefault = detectedUsername || "";
56
56
  const usernamePrompt = detectedUsername
57
57
  ? `GitHub username [${detectedUsername}]: `
58
- : 'GitHub username: ';
58
+ : "GitHub username: ";
59
59
  const usernameInput = await ask(rl, usernamePrompt);
60
60
  const githubUsername = usernameInput || usernameDefault;
61
61
  // Languages
62
- const defaultLangs = 'typescript, javascript';
62
+ const defaultLangs = "typescript, javascript";
63
63
  const langsInput = await ask(rl, `Preferred languages [${defaultLangs}]: `);
64
- const languages = langsInput ? parseCSV(langsInput) : ['typescript', 'javascript'];
64
+ const languages = langsInput
65
+ ? parseCSV(langsInput)
66
+ : ["typescript", "javascript"];
65
67
  // Issue labels
66
- const defaultLabels = 'good first issue, help wanted';
68
+ const defaultLabels = "good first issue, help wanted";
67
69
  const labelsInput = await ask(rl, `Issue labels to search for [${defaultLabels}]: `);
68
- const labels = labelsInput ? parseCSV(labelsInput) : ['good first issue', 'help wanted'];
70
+ const labels = labelsInput
71
+ ? parseCSV(labelsInput)
72
+ : ["good first issue", "help wanted"];
69
73
  // Difficulty scope
70
- const scopeOptions = ALL_SCOPES.join(', ');
74
+ const scopeOptions = ALL_SCOPES.join(", ");
71
75
  const scopeInput = await ask(rl, `Difficulty scope (${scopeOptions}) [all]: `);
72
- const scope = scopeInput ? parseMultiSelect(scopeInput, ALL_SCOPES) : [...ALL_SCOPES];
76
+ const scope = scopeInput
77
+ ? parseMultiSelect(scopeInput, ALL_SCOPES)
78
+ : [...ALL_SCOPES];
73
79
  // Minimum stars
74
- const minStarsInput = await ask(rl, 'Minimum repo stars [50]: ');
80
+ const minStarsInput = await ask(rl, "Minimum repo stars [50]: ");
75
81
  const minStars = minStarsInput ? parseInt(minStarsInput, 10) : 50;
76
82
  // Preferred organizations
77
- const orgsInput = await ask(rl, 'Preferred organizations (comma-separated, optional): ');
83
+ const orgsInput = await ask(rl, "Preferred organizations (comma-separated, optional): ");
78
84
  const preferredOrgs = parseCSV(orgsInput);
79
85
  // Project categories
80
- const categoryOptions = ALL_CATEGORIES.join(', ');
86
+ const categoryOptions = ALL_CATEGORIES.join(", ");
81
87
  const categoriesInput = await ask(rl, `Project categories (${categoryOptions}) [none]: `);
82
88
  const projectCategories = parseMultiSelect(categoriesInput, ALL_CATEGORIES);
83
89
  // Repos to exclude
84
- const excludeInput = await ask(rl, 'Repos to exclude (owner/repo, comma-separated, optional): ');
90
+ const excludeInput = await ask(rl, "Repos to exclude (owner/repo, comma-separated, optional): ");
85
91
  const excludeRepos = parseCSV(excludeInput);
86
92
  const prefs = ScoutPreferencesSchema.parse({
87
93
  githubUsername,
@@ -93,7 +99,7 @@ export async function runSetup(options) {
93
99
  projectCategories,
94
100
  minStars: isNaN(minStars) ? 50 : minStars,
95
101
  });
96
- console.log('\n✅ Setup complete! Preferences saved.\n');
102
+ console.log("\n✅ Setup complete! Preferences saved.\n");
97
103
  return prefs;
98
104
  }
99
105
  finally {
@@ -2,5 +2,5 @@
2
2
  * Shared validation patterns and helpers for CLI commands.
3
3
  */
4
4
  export declare const ISSUE_URL_PATTERN: RegExp;
5
- export declare function validateGitHubUrl(url: string, pattern: RegExp, entityType: 'issue'): void;
5
+ export declare function validateGitHubUrl(url: string, pattern: RegExp, entityType: "issue"): void;
6
6
  export declare function validateUrl(url: string): string;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Shared validation patterns and helpers for CLI commands.
3
3
  */
4
- import { ValidationError } from '../core/errors.js';
4
+ import { ValidationError } from "../core/errors.js";
5
5
  export const ISSUE_URL_PATTERN = /^https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/\d+$/;
6
6
  const MAX_URL_LENGTH = 2048;
7
7
  export function validateGitHubUrl(url, pattern, entityType) {
@@ -1,5 +1,5 @@
1
- import type { ScoutState } from '../core/schemas.js';
2
- import type { VetListResult } from '../core/types.js';
1
+ import type { ScoutState } from "../core/schemas.js";
2
+ import type { VetListResult } from "../core/types.js";
3
3
  interface VetListCommandOptions {
4
4
  concurrency?: number;
5
5
  prune?: boolean;
@@ -1,16 +1,23 @@
1
- import { createScout } from '../scout.js';
2
- import { requireGitHubToken } from '../core/utils.js';
3
- import { saveLocalState } from '../core/local-state.js';
1
+ import { createScout } from "../scout.js";
2
+ import { requireGitHubToken } from "../core/utils.js";
3
+ import { saveLocalState } from "../core/local-state.js";
4
4
  export async function runVetList(options) {
5
5
  const token = requireGitHubToken();
6
6
  const scout = options.state
7
- ? await createScout({ githubToken: token, persistence: 'provided', initialState: options.state })
7
+ ? await createScout({
8
+ githubToken: token,
9
+ persistence: "provided",
10
+ initialState: options.state,
11
+ })
8
12
  : await createScout({ githubToken: token });
9
13
  const result = await scout.vetList({
10
14
  concurrency: options.concurrency,
11
15
  prune: options.prune,
12
16
  });
13
17
  saveLocalState(scout.getState());
14
- await scout.checkpoint();
18
+ const persisted = await scout.checkpoint();
19
+ if (!persisted) {
20
+ console.error("Warning: changes saved locally but gist sync failed.");
21
+ }
15
22
  return result;
16
23
  }
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Vet command — vets a specific issue for claimability.
3
3
  */
4
- import type { ProjectHealth } from '../core/types.js';
5
- import type { IssueVettingResult, ScoutState } from '../core/schemas.js';
4
+ import type { ProjectHealth } from "../core/types.js";
5
+ import type { IssueVettingResult, ScoutState } from "../core/schemas.js";
6
6
  export interface VetOutput {
7
7
  issue: {
8
8
  repo: string;
@@ -11,7 +11,7 @@ export interface VetOutput {
11
11
  url: string;
12
12
  labels: string[];
13
13
  };
14
- recommendation: 'approve' | 'skip' | 'needs_review';
14
+ recommendation: "approve" | "skip" | "needs_review";
15
15
  reasonsToApprove: string[];
16
16
  reasonsToSkip: string[];
17
17
  projectHealth: ProjectHealth;
@@ -1,15 +1,19 @@
1
1
  /**
2
2
  * Vet command — vets a specific issue for claimability.
3
3
  */
4
- import { createScout } from '../scout.js';
5
- import { requireGitHubToken } from '../core/utils.js';
6
- import { ISSUE_URL_PATTERN, validateGitHubUrl, validateUrl } from './validation.js';
4
+ import { createScout } from "../scout.js";
5
+ import { requireGitHubToken } from "../core/utils.js";
6
+ import { ISSUE_URL_PATTERN, validateGitHubUrl, validateUrl, } from "./validation.js";
7
7
  export async function runVet(options) {
8
8
  validateUrl(options.issueUrl);
9
- validateGitHubUrl(options.issueUrl, ISSUE_URL_PATTERN, 'issue');
9
+ validateGitHubUrl(options.issueUrl, ISSUE_URL_PATTERN, "issue");
10
10
  const token = requireGitHubToken();
11
11
  const scout = options.state
12
- ? await createScout({ githubToken: token, persistence: 'provided', initialState: options.state })
12
+ ? await createScout({
13
+ githubToken: token,
14
+ persistence: "provided",
15
+ initialState: options.state,
16
+ })
13
17
  : await createScout({ githubToken: token });
14
18
  const candidate = await scout.vetIssue(options.issueUrl);
15
19
  return {
@@ -2,7 +2,7 @@
2
2
  * First-run bootstrap — fetches starred repos and PR history from GitHub
3
3
  * to seed the scout's state with the user's contribution context.
4
4
  */
5
- import type { OssScout } from '../scout.js';
5
+ import type { OssScout } from "../scout.js";
6
6
  export interface BootstrapResult {
7
7
  starredRepoCount: number;
8
8
  mergedPRCount: number;
@@ -2,22 +2,23 @@
2
2
  * First-run bootstrap — fetches starred repos and PR history from GitHub
3
3
  * to seed the scout's state with the user's contribution context.
4
4
  */
5
- import { getOctokit, checkRateLimit } from './github.js';
6
- import { debug, warn } from './logger.js';
7
- import { errorMessage } from './errors.js';
8
- const MODULE = 'bootstrap';
5
+ import { getOctokit, checkRateLimit } from "./github.js";
6
+ import { debug, warn } from "./logger.js";
7
+ import { errorMessage } from "./errors.js";
8
+ import { extractRepoFromUrl } from "./utils.js";
9
+ const MODULE = "bootstrap";
9
10
  const STARRED_MAX_PAGES = 5;
10
11
  const SEARCH_MAX_PAGES = 3;
11
12
  const PER_PAGE = 100;
12
13
  export async function bootstrapScout(scout, token) {
13
14
  const username = scout.getPreferences().githubUsername;
14
15
  if (!username) {
15
- throw new Error('GitHub username not configured. Run `oss-scout setup` first.');
16
+ throw new Error("GitHub username not configured. Run `oss-scout setup` first.");
16
17
  }
17
18
  const rateLimit = await checkRateLimit(token);
18
19
  debug(MODULE, `Rate limit: ${rateLimit.remaining}/${rateLimit.limit}, resets at ${rateLimit.resetAt}`);
19
20
  if (rateLimit.remaining < 15) {
20
- debug(MODULE, 'Insufficient rate limit, skipping bootstrap');
21
+ debug(MODULE, "Insufficient rate limit, skipping bootstrap");
21
22
  return {
22
23
  starredRepoCount: 0,
23
24
  mergedPRCount: 0,
@@ -33,7 +34,10 @@ export async function bootstrapScout(scout, token) {
33
34
  const starredRepos = [];
34
35
  try {
35
36
  let starredPage = 0;
36
- for await (const response of octokit.paginate.iterator(octokit.activity.listReposStarredByAuthenticatedUser, { per_page: PER_PAGE, headers: { accept: 'application/vnd.github.v3+json' } })) {
37
+ for await (const response of octokit.paginate.iterator(octokit.activity.listReposStarredByAuthenticatedUser, {
38
+ per_page: PER_PAGE,
39
+ headers: { accept: "application/vnd.github.v3+json" },
40
+ })) {
37
41
  for (const repo of response.data) {
38
42
  const r = repo;
39
43
  starredRepos.push(r.full_name);
@@ -47,7 +51,7 @@ export async function bootstrapScout(scout, token) {
47
51
  }
48
52
  catch (err) {
49
53
  warn(MODULE, `Failed to fetch starred repos: ${errorMessage(err)}`);
50
- errors.push('starred repos fetch failed');
54
+ errors.push("starred repos fetch failed");
51
55
  }
52
56
  // 2. Fetch merged PRs via Search API
53
57
  let mergedPRCount = 0;
@@ -59,14 +63,14 @@ export async function bootstrapScout(scout, token) {
59
63
  page,
60
64
  });
61
65
  for (const item of data.items) {
62
- const repoMatch = item.html_url.match(/github\.com\/([^/]+\/[^/]+)\//);
63
- if (!repoMatch)
66
+ const repo = extractRepoFromUrl(item.html_url);
67
+ if (!repo)
64
68
  continue;
65
69
  scout.recordMergedPR({
66
70
  url: item.html_url,
67
71
  title: item.title,
68
72
  mergedAt: item.closed_at ?? new Date().toISOString(),
69
- repo: repoMatch[1],
73
+ repo,
70
74
  });
71
75
  mergedPRCount++;
72
76
  }
@@ -77,7 +81,7 @@ export async function bootstrapScout(scout, token) {
77
81
  }
78
82
  catch (err) {
79
83
  warn(MODULE, `Failed to fetch merged PRs: ${errorMessage(err)}`);
80
- errors.push('merged PR fetch failed');
84
+ errors.push("merged PR fetch failed");
81
85
  }
82
86
  // 3. Fetch closed-without-merge PRs via Search API
83
87
  let closedPRCount = 0;
@@ -89,14 +93,14 @@ export async function bootstrapScout(scout, token) {
89
93
  page,
90
94
  });
91
95
  for (const item of data.items) {
92
- const repoMatch = item.html_url.match(/github\.com\/([^/]+\/[^/]+)\//);
93
- if (!repoMatch)
96
+ const repo = extractRepoFromUrl(item.html_url);
97
+ if (!repo)
94
98
  continue;
95
99
  scout.recordClosedPR({
96
100
  url: item.html_url,
97
101
  title: item.title,
98
102
  closedAt: item.closed_at ?? new Date().toISOString(),
99
- repo: repoMatch[1],
103
+ repo,
100
104
  });
101
105
  closedPRCount++;
102
106
  }
@@ -107,7 +111,7 @@ export async function bootstrapScout(scout, token) {
107
111
  }
108
112
  catch (err) {
109
113
  warn(MODULE, `Failed to fetch closed PRs: ${errorMessage(err)}`);
110
- errors.push('closed PR fetch failed');
114
+ errors.push("closed PR fetch failed");
111
115
  }
112
116
  const state = scout.getState();
113
117
  const reposScoredCount = Object.keys(state.repoScores).length;
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Used by issue discovery to prioritize repos matching user's category preferences.
5
5
  */
6
- import type { ProjectCategory } from './types.js';
6
+ import type { ProjectCategory } from "./types.js";
7
7
  /** GitHub topics associated with each project category, used for `topic:` search queries. */
8
8
  export declare const CATEGORY_TOPICS: Record<ProjectCategory, string[]>;
9
9
  /** Well-known GitHub organizations associated with each project category. */
@@ -5,21 +5,112 @@
5
5
  */
6
6
  /** GitHub topics associated with each project category, used for `topic:` search queries. */
7
7
  export const CATEGORY_TOPICS = {
8
- nonprofit: ['nonprofit', 'social-good', 'humanitarian', 'charity', 'social-impact', 'civic-tech'],
9
- devtools: ['developer-tools', 'devtools', 'cli', 'sdk', 'linter', 'formatter', 'build-tool'],
10
- infrastructure: ['infrastructure', 'cloud', 'kubernetes', 'docker', 'devops', 'monitoring', 'observability'],
11
- 'web-frameworks': ['web-framework', 'frontend', 'backend', 'fullstack', 'nextjs', 'react', 'vue'],
12
- 'data-ml': ['machine-learning', 'data-science', 'deep-learning', 'nlp', 'data-pipeline', 'analytics'],
13
- education: ['education', 'learning', 'tutorial', 'courseware', 'edtech', 'teaching'],
8
+ nonprofit: [
9
+ "nonprofit",
10
+ "social-good",
11
+ "humanitarian",
12
+ "charity",
13
+ "social-impact",
14
+ "civic-tech",
15
+ ],
16
+ devtools: [
17
+ "developer-tools",
18
+ "devtools",
19
+ "cli",
20
+ "sdk",
21
+ "linter",
22
+ "formatter",
23
+ "build-tool",
24
+ ],
25
+ infrastructure: [
26
+ "infrastructure",
27
+ "cloud",
28
+ "kubernetes",
29
+ "docker",
30
+ "devops",
31
+ "monitoring",
32
+ "observability",
33
+ ],
34
+ "web-frameworks": [
35
+ "web-framework",
36
+ "frontend",
37
+ "backend",
38
+ "fullstack",
39
+ "nextjs",
40
+ "react",
41
+ "vue",
42
+ ],
43
+ "data-ml": [
44
+ "machine-learning",
45
+ "data-science",
46
+ "deep-learning",
47
+ "nlp",
48
+ "data-pipeline",
49
+ "analytics",
50
+ ],
51
+ education: [
52
+ "education",
53
+ "learning",
54
+ "tutorial",
55
+ "courseware",
56
+ "edtech",
57
+ "teaching",
58
+ ],
14
59
  };
15
60
  /** Well-known GitHub organizations associated with each project category. */
16
61
  export const CATEGORY_ORGS = {
17
- nonprofit: ['code-for-america', 'opengovfoundation', 'ushahidi', 'hotosm', 'openfn', 'democracyearth'],
18
- devtools: ['eslint', 'prettier', 'vitejs', 'biomejs', 'oxc-project', 'ast-grep', 'turbot'],
19
- infrastructure: ['kubernetes', 'hashicorp', 'grafana', 'prometheus', 'open-telemetry', 'envoyproxy', 'cncf'],
20
- 'web-frameworks': ['vercel', 'remix-run', 'sveltejs', 'nuxt', 'astro', 'redwoodjs', 'blitz-js'],
21
- 'data-ml': ['huggingface', 'mlflow', 'apache', 'dbt-labs', 'dagster-io', 'prefecthq', 'langchain-ai'],
22
- education: ['freeCodeCamp', 'TheOdinProject', 'exercism', 'codecademy', 'oppia', 'Khan'],
62
+ nonprofit: [
63
+ "code-for-america",
64
+ "opengovfoundation",
65
+ "ushahidi",
66
+ "hotosm",
67
+ "openfn",
68
+ "democracyearth",
69
+ ],
70
+ devtools: [
71
+ "eslint",
72
+ "prettier",
73
+ "vitejs",
74
+ "biomejs",
75
+ "oxc-project",
76
+ "ast-grep",
77
+ "turbot",
78
+ ],
79
+ infrastructure: [
80
+ "kubernetes",
81
+ "hashicorp",
82
+ "grafana",
83
+ "prometheus",
84
+ "open-telemetry",
85
+ "envoyproxy",
86
+ "cncf",
87
+ ],
88
+ "web-frameworks": [
89
+ "vercel",
90
+ "remix-run",
91
+ "sveltejs",
92
+ "nuxt",
93
+ "astro",
94
+ "redwoodjs",
95
+ "blitz-js",
96
+ ],
97
+ "data-ml": [
98
+ "huggingface",
99
+ "mlflow",
100
+ "apache",
101
+ "dbt-labs",
102
+ "dagster-io",
103
+ "prefecthq",
104
+ "langchain-ai",
105
+ ],
106
+ education: [
107
+ "freeCodeCamp",
108
+ "TheOdinProject",
109
+ "exercism",
110
+ "codecademy",
111
+ "oppia",
112
+ "Khan",
113
+ ],
23
114
  };
24
115
  /**
25
116
  * Check if a repo belongs to any of the given categories based on its owner matching a category org.
@@ -28,7 +119,7 @@ export const CATEGORY_ORGS = {
28
119
  export function repoBelongsToCategory(repoFullName, categories) {
29
120
  if (categories.length === 0)
30
121
  return false;
31
- const owner = repoFullName.split('/')[0]?.toLowerCase();
122
+ const owner = repoFullName.split("/")[0]?.toLowerCase();
32
123
  if (!owner)
33
124
  return false;
34
125
  for (const category of categories) {
@@ -1,5 +1,12 @@
1
1
  /**
2
2
  * Custom error type hierarchy for oss-scout.
3
+ *
4
+ * Error strategy:
5
+ * - Auth errors (401) and rate limit errors (429, 403+rate-limit): ALWAYS propagate
6
+ * - Network errors (ENOTFOUND, ECONNREFUSED, ETIMEDOUT): propagate with context
7
+ * - Validation errors: propagate
8
+ * - Cache/filesystem errors: degrade gracefully with warn logging
9
+ * - API data errors (unexpected shapes): degrade gracefully with warn logging
3
10
  */
4
11
  export declare class OssScoutError extends Error {
5
12
  readonly code: string;
@@ -15,7 +22,7 @@ export declare function errorMessage(e: unknown): string;
15
22
  export declare function getHttpStatusCode(error: unknown): number | undefined;
16
23
  export declare function isRateLimitError(error: unknown): boolean;
17
24
  /** Error codes for JSON output. */
18
- export type ErrorCode = 'AUTH_REQUIRED' | 'CONFIGURATION' | 'NETWORK' | 'NOT_FOUND' | 'RATE_LIMITED' | 'STATE_CORRUPTED' | 'UNKNOWN' | 'VALIDATION';
25
+ export type ErrorCode = "AUTH_REQUIRED" | "CONFIGURATION" | "NETWORK" | "NOT_FOUND" | "RATE_LIMITED" | "STATE_CORRUPTED" | "UNKNOWN" | "VALIDATION";
19
26
  /**
20
27
  * Map an unknown error to a structured ErrorCode for JSON output.
21
28
  */
@@ -1,33 +1,42 @@
1
1
  /**
2
2
  * Custom error type hierarchy for oss-scout.
3
+ *
4
+ * Error strategy:
5
+ * - Auth errors (401) and rate limit errors (429, 403+rate-limit): ALWAYS propagate
6
+ * - Network errors (ENOTFOUND, ECONNREFUSED, ETIMEDOUT): propagate with context
7
+ * - Validation errors: propagate
8
+ * - Cache/filesystem errors: degrade gracefully with warn logging
9
+ * - API data errors (unexpected shapes): degrade gracefully with warn logging
3
10
  */
4
11
  export class OssScoutError extends Error {
5
12
  code;
6
13
  constructor(message, code) {
7
14
  super(message);
8
15
  this.code = code;
9
- this.name = 'OssScoutError';
16
+ this.name = "OssScoutError";
10
17
  }
11
18
  }
12
19
  export class ConfigurationError extends OssScoutError {
13
20
  constructor(message) {
14
- super(message, 'CONFIGURATION_ERROR');
15
- this.name = 'ConfigurationError';
21
+ super(message, "CONFIGURATION_ERROR");
22
+ this.name = "ConfigurationError";
16
23
  }
17
24
  }
18
25
  export class ValidationError extends OssScoutError {
19
26
  constructor(message) {
20
- super(message, 'VALIDATION_ERROR');
21
- this.name = 'ValidationError';
27
+ super(message, "VALIDATION_ERROR");
28
+ this.name = "ValidationError";
22
29
  }
23
30
  }
24
31
  export function errorMessage(e) {
25
32
  return e instanceof Error ? e.message : String(e);
26
33
  }
27
34
  export function getHttpStatusCode(error) {
28
- if (error && typeof error === 'object' && 'status' in error) {
35
+ if (error && typeof error === "object" && "status" in error) {
29
36
  const status = error.status;
30
- return typeof status === 'number' && Number.isFinite(status) ? status : undefined;
37
+ return typeof status === "number" && Number.isFinite(status)
38
+ ? status
39
+ : undefined;
31
40
  }
32
41
  return undefined;
33
42
  }
@@ -37,7 +46,7 @@ export function isRateLimitError(error) {
37
46
  return true;
38
47
  if (status === 403) {
39
48
  const msg = errorMessage(error).toLowerCase();
40
- return msg.includes('rate limit');
49
+ return msg.includes("rate limit");
41
50
  }
42
51
  return false;
43
52
  }
@@ -46,24 +55,27 @@ export function isRateLimitError(error) {
46
55
  */
47
56
  export function resolveErrorCode(err) {
48
57
  if (err instanceof ConfigurationError)
49
- return 'CONFIGURATION';
58
+ return "CONFIGURATION";
50
59
  if (err instanceof ValidationError)
51
- return 'VALIDATION';
60
+ return "VALIDATION";
52
61
  const status = getHttpStatusCode(err);
53
62
  if (status === 401)
54
- return 'AUTH_REQUIRED';
63
+ return "AUTH_REQUIRED";
55
64
  if (status === 403) {
56
65
  const msg = errorMessage(err).toLowerCase();
57
- if (msg.includes('rate limit') || msg.includes('abuse detection'))
58
- return 'RATE_LIMITED';
59
- return 'AUTH_REQUIRED';
66
+ if (msg.includes("rate limit") || msg.includes("abuse detection"))
67
+ return "RATE_LIMITED";
68
+ return "AUTH_REQUIRED";
60
69
  }
61
70
  if (status === 404)
62
- return 'NOT_FOUND';
71
+ return "NOT_FOUND";
63
72
  if (status === 429)
64
- return 'RATE_LIMITED';
73
+ return "RATE_LIMITED";
65
74
  const msg = errorMessage(err).toLowerCase();
66
- if (msg.includes('enotfound') || msg.includes('econnrefused') || msg.includes('etimedout') || msg.includes('fetch failed'))
67
- return 'NETWORK';
68
- return 'UNKNOWN';
75
+ if (msg.includes("enotfound") ||
76
+ msg.includes("econnrefused") ||
77
+ msg.includes("etimedout") ||
78
+ msg.includes("fetch failed"))
79
+ return "NETWORK";
80
+ return "UNKNOWN";
69
81
  }