ghscout 0.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 (66) hide show
  1. package/README.md +195 -0
  2. package/dist/analysis/ai-scorer.d.ts +34 -0
  3. package/dist/analysis/ai-scorer.d.ts.map +1 -0
  4. package/dist/analysis/ai-scorer.js +139 -0
  5. package/dist/analysis/ai-scorer.js.map +1 -0
  6. package/dist/analysis/cluster.d.ts +14 -0
  7. package/dist/analysis/cluster.d.ts.map +1 -0
  8. package/dist/analysis/cluster.js +223 -0
  9. package/dist/analysis/cluster.js.map +1 -0
  10. package/dist/analysis/scorer.d.ts +27 -0
  11. package/dist/analysis/scorer.d.ts.map +1 -0
  12. package/dist/analysis/scorer.js +196 -0
  13. package/dist/analysis/scorer.js.map +1 -0
  14. package/dist/analysis/signals.d.ts +30 -0
  15. package/dist/analysis/signals.d.ts.map +1 -0
  16. package/dist/analysis/signals.js +59 -0
  17. package/dist/analysis/signals.js.map +1 -0
  18. package/dist/analysis/tokenizer.d.ts +15 -0
  19. package/dist/analysis/tokenizer.d.ts.map +1 -0
  20. package/dist/analysis/tokenizer.js +64 -0
  21. package/dist/analysis/tokenizer.js.map +1 -0
  22. package/dist/commands/evidence.d.ts +9 -0
  23. package/dist/commands/evidence.d.ts.map +1 -0
  24. package/dist/commands/evidence.js +229 -0
  25. package/dist/commands/evidence.js.map +1 -0
  26. package/dist/commands/scan-org.d.ts +3 -0
  27. package/dist/commands/scan-org.d.ts.map +1 -0
  28. package/dist/commands/scan-org.js +88 -0
  29. package/dist/commands/scan-org.js.map +1 -0
  30. package/dist/commands/scan.d.ts +32 -0
  31. package/dist/commands/scan.d.ts.map +1 -0
  32. package/dist/commands/scan.js +197 -0
  33. package/dist/commands/scan.js.map +1 -0
  34. package/dist/commands/trending.d.ts +14 -0
  35. package/dist/commands/trending.d.ts.map +1 -0
  36. package/dist/commands/trending.js +145 -0
  37. package/dist/commands/trending.js.map +1 -0
  38. package/dist/github/auth.d.ts +3 -0
  39. package/dist/github/auth.d.ts.map +1 -0
  40. package/dist/github/auth.js +33 -0
  41. package/dist/github/auth.js.map +1 -0
  42. package/dist/github/cache.d.ts +18 -0
  43. package/dist/github/cache.d.ts.map +1 -0
  44. package/dist/github/cache.js +51 -0
  45. package/dist/github/cache.js.map +1 -0
  46. package/dist/github/client.d.ts +24 -0
  47. package/dist/github/client.d.ts.map +1 -0
  48. package/dist/github/client.js +140 -0
  49. package/dist/github/client.js.map +1 -0
  50. package/dist/github/fetchers.d.ts +13 -0
  51. package/dist/github/fetchers.d.ts.map +1 -0
  52. package/dist/github/fetchers.js +142 -0
  53. package/dist/github/fetchers.js.map +1 -0
  54. package/dist/github/types.d.ts +46 -0
  55. package/dist/github/types.d.ts.map +1 -0
  56. package/dist/github/types.js +2 -0
  57. package/dist/github/types.js.map +1 -0
  58. package/dist/index.d.ts +3 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/index.js +116 -0
  61. package/dist/index.js.map +1 -0
  62. package/dist/output/formatters.d.ts +35 -0
  63. package/dist/output/formatters.d.ts.map +1 -0
  64. package/dist/output/formatters.js +195 -0
  65. package/dist/output/formatters.js.map +1 -0
  66. package/package.json +50 -0
@@ -0,0 +1,196 @@
1
+ const FRUSTRATION_KEYWORDS = [
2
+ "broken",
3
+ "crash",
4
+ "fail",
5
+ "stuck",
6
+ "slow",
7
+ "cannot",
8
+ "doesn't work",
9
+ "not working",
10
+ "impossible",
11
+ "please fix",
12
+ "been waiting",
13
+ "any update",
14
+ "workaround",
15
+ "hacky",
16
+ "ugly hack",
17
+ "months ago",
18
+ "years ago",
19
+ "still broken",
20
+ "regression",
21
+ ];
22
+ /** Linearly normalize a value to 0-100 given min/max from the dataset. */
23
+ function relNormalize(value, min, max) {
24
+ if (max === min)
25
+ return 50; // all same = middle
26
+ return ((value - min) / (max - min)) * 100;
27
+ }
28
+ /** Count frustration keywords across all issue titles and bodies in a cluster.
29
+ * Title matches count 1.0, body matches count 0.5 (bodies are longer, more incidental matches). */
30
+ function countFrustrationKeywords(cluster) {
31
+ let count = 0;
32
+ for (const issue of cluster.issues) {
33
+ const lowerTitle = issue.title.toLowerCase();
34
+ const lowerBody = (issue.body ?? "").toLowerCase();
35
+ for (const keyword of FRUSTRATION_KEYWORDS) {
36
+ if (lowerTitle.includes(keyword)) {
37
+ count += 1.0;
38
+ }
39
+ if (lowerBody.includes(keyword)) {
40
+ count += 0.5;
41
+ }
42
+ }
43
+ }
44
+ return count;
45
+ }
46
+ /** Calculate average age of issues in days. */
47
+ function avgAgeDays(cluster) {
48
+ if (cluster.issues.length === 0)
49
+ return 0;
50
+ const now = Date.now();
51
+ let totalDays = 0;
52
+ for (const issue of cluster.issues) {
53
+ const created = new Date(issue.createdAt).getTime();
54
+ totalDays += (now - created) / (1000 * 60 * 60 * 24);
55
+ }
56
+ return totalDays / cluster.issues.length;
57
+ }
58
+ /** Raw demand: total thumbsUp reactions. */
59
+ function rawDemand(cluster) {
60
+ return cluster.issues.reduce((sum, issue) => sum + issue.reactions.thumbsUp, 0);
61
+ }
62
+ /** Raw frequency: issue count. */
63
+ function rawFrequency(cluster) {
64
+ return cluster.issueCount;
65
+ }
66
+ /** Raw frustration: thumbsDown + keywords + age bonus. */
67
+ function rawFrustration(cluster) {
68
+ const totalThumbsDown = cluster.issues.reduce((sum, issue) => sum + issue.reactions.thumbsDown, 0);
69
+ const keywordCount = countFrustrationKeywords(cluster);
70
+ let raw = totalThumbsDown + keywordCount;
71
+ if (avgAgeDays(cluster) > 90) {
72
+ raw += 30;
73
+ }
74
+ return raw;
75
+ }
76
+ /** Gap score (0-100): percentage of open issues. Not relative — absolute. */
77
+ function scoreGap(cluster) {
78
+ if (cluster.issues.length === 0)
79
+ return 0;
80
+ const openCount = cluster.issues.filter((issue) => issue.state === "open").length;
81
+ return (openCount / cluster.issues.length) * 100;
82
+ }
83
+ /** Market size score (0-100): repo stars, absolute normalization. */
84
+ function scoreMarketSize(repoMeta) {
85
+ if (repoMeta.stars >= 50000)
86
+ return 100;
87
+ if (repoMeta.stars <= 0)
88
+ return 0;
89
+ return (repoMeta.stars / 50000) * 100;
90
+ }
91
+ /**
92
+ * Score a single cluster. Used only when scoring one cluster in isolation.
93
+ * For proper relative scoring, use scoreClusters().
94
+ */
95
+ export function scoreCluster(cluster, repoMeta) {
96
+ // Fallback absolute normalization for single-cluster scoring
97
+ const demand = Math.min(100, (rawDemand(cluster) / 50) * 100);
98
+ const frequency = Math.min(100, ((rawFrequency(cluster) - 1) / 19) * 100);
99
+ const frustration = Math.min(100, (rawFrustration(cluster) / 50) * 100);
100
+ const marketSize = scoreMarketSize(repoMeta);
101
+ const gap = scoreGap(cluster);
102
+ let score = Math.round(demand * 0.3 +
103
+ frequency * 0.25 +
104
+ frustration * 0.15 +
105
+ marketSize * 0.15 +
106
+ gap * 0.15);
107
+ if (cluster.name === "other") {
108
+ score = Math.round(score * 0.3);
109
+ }
110
+ return {
111
+ ...cluster,
112
+ score,
113
+ breakdown: {
114
+ demand: Math.round(demand),
115
+ frequency: Math.round(frequency),
116
+ frustration: Math.round(frustration),
117
+ marketSize: Math.round(marketSize),
118
+ gap: Math.round(gap),
119
+ },
120
+ };
121
+ }
122
+ /** Weight presets per scoring mode. */
123
+ const WEIGHTS = {
124
+ single: { demand: 0.35, frequency: 0.30, frustration: 0.20, marketSize: 0, gap: 0.15 },
125
+ cross: { demand: 0.30, frequency: 0.25, frustration: 0.15, marketSize: 0.15, gap: 0.15 },
126
+ };
127
+ /**
128
+ * Score all clusters with relative normalization.
129
+ * Demand, frequency, and frustration are normalized against the min/max
130
+ * of the current dataset so they always spread 0-100.
131
+ *
132
+ * @param mode - "single" redistributes market size weight (constant in single-repo scans),
133
+ * "cross" keeps original weights (market size varies across repos).
134
+ */
135
+ export function scoreClusters(clusters, repoMeta, mode = "single") {
136
+ if (clusters.length === 0)
137
+ return [];
138
+ const w = WEIGHTS[mode];
139
+ // Filter out "other" for raw calculation, score it separately
140
+ const real = clusters.filter((c) => c.name !== "other");
141
+ const other = clusters.filter((c) => c.name === "other");
142
+ if (real.length === 0) {
143
+ // Only "other" clusters — use absolute scoring
144
+ return other.map((c) => scoreCluster(c, repoMeta));
145
+ }
146
+ // Step 1: Calculate raw values for each cluster
147
+ const raws = real.map((c) => ({
148
+ cluster: c,
149
+ demand: rawDemand(c),
150
+ frequency: rawFrequency(c),
151
+ frustration: rawFrustration(c),
152
+ gap: scoreGap(c),
153
+ }));
154
+ // Step 2: Find min/max for relative normalization
155
+ const demandValues = raws.map((r) => r.demand);
156
+ const freqValues = raws.map((r) => r.frequency);
157
+ const frustValues = raws.map((r) => r.frustration);
158
+ const demandMin = Math.min(...demandValues);
159
+ const demandMax = Math.max(...demandValues);
160
+ const freqMin = Math.min(...freqValues);
161
+ const freqMax = Math.max(...freqValues);
162
+ const frustMin = Math.min(...frustValues);
163
+ const frustMax = Math.max(...frustValues);
164
+ const marketSize = scoreMarketSize(repoMeta);
165
+ // Step 3: Score each cluster with relative normalization
166
+ const scored = raws.map((r) => {
167
+ const demand = relNormalize(r.demand, demandMin, demandMax);
168
+ const frequency = relNormalize(r.frequency, freqMin, freqMax);
169
+ const frustration = relNormalize(r.frustration, frustMin, frustMax);
170
+ const gap = r.gap;
171
+ const score = Math.round(demand * w.demand +
172
+ frequency * w.frequency +
173
+ frustration * w.frustration +
174
+ marketSize * w.marketSize +
175
+ gap * w.gap);
176
+ return {
177
+ ...r.cluster,
178
+ score,
179
+ breakdown: {
180
+ demand: Math.round(demand),
181
+ frequency: Math.round(frequency),
182
+ frustration: Math.round(frustration),
183
+ marketSize: Math.round(marketSize),
184
+ gap: Math.round(gap),
185
+ },
186
+ };
187
+ });
188
+ // Score "other" clusters with penalty
189
+ for (const c of other) {
190
+ const sc = scoreCluster(c, repoMeta);
191
+ sc.score = Math.round(sc.score * 0.3);
192
+ scored.push(sc);
193
+ }
194
+ return scored.sort((a, b) => b.score - a.score);
195
+ }
196
+ //# sourceMappingURL=scorer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scorer.js","sourceRoot":"","sources":["../../src/analysis/scorer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ import type { Pull, Issue } from "../github/types.js";
2
+ export interface RejectedPR {
3
+ number: number;
4
+ title: string;
5
+ htmlUrl: string;
6
+ reactions: {
7
+ thumbsUp: number;
8
+ thumbsDown: number;
9
+ total: number;
10
+ };
11
+ user: string;
12
+ createdAt: string;
13
+ }
14
+ export interface WorkaroundIssue {
15
+ number: number;
16
+ title: string;
17
+ htmlUrl: string;
18
+ signals: string[];
19
+ }
20
+ /**
21
+ * Finds closed PRs that were not merged but had significant positive reactions,
22
+ * indicating demand that was rejected by maintainers.
23
+ */
24
+ export declare function detectRejectedDemand(pulls: Pull[]): RejectedPR[];
25
+ /**
26
+ * Detects issues where users posted workarounds — code blocks or
27
+ * references to npm/yarn/pnpm packages as alternative solutions.
28
+ */
29
+ export declare function detectWorkarounds(issues: Issue[]): WorkaroundIssue[];
30
+ //# sourceMappingURL=signals.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signals.d.ts","sourceRoot":"","sources":["../../src/analysis/signals.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAEtD,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IACnE,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,IAAI,EAAE,GAAG,UAAU,EAAE,CAYhE;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,KAAK,EAAE,GAAG,eAAe,EAAE,CA2CpE"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Finds closed PRs that were not merged but had significant positive reactions,
3
+ * indicating demand that was rejected by maintainers.
4
+ */
5
+ export function detectRejectedDemand(pulls) {
6
+ return pulls
7
+ .filter((pr) => !pr.merged && pr.reactions.thumbsUp >= 3)
8
+ .sort((a, b) => b.reactions.thumbsUp - a.reactions.thumbsUp)
9
+ .map((pr) => ({
10
+ number: pr.number,
11
+ title: pr.title,
12
+ htmlUrl: pr.htmlUrl,
13
+ reactions: pr.reactions,
14
+ user: pr.user,
15
+ createdAt: pr.createdAt,
16
+ }));
17
+ }
18
+ /**
19
+ * Detects issues where users posted workarounds — code blocks or
20
+ * references to npm/yarn/pnpm packages as alternative solutions.
21
+ */
22
+ export function detectWorkarounds(issues) {
23
+ const results = [];
24
+ for (const issue of issues) {
25
+ const body = issue.body;
26
+ if (!body)
27
+ continue;
28
+ const signals = [];
29
+ // Detect code blocks
30
+ if (/```[\s\S]*?```/.test(body)) {
31
+ signals.push("code block");
32
+ }
33
+ // Detect npm install references
34
+ const npmMatches = body.matchAll(/npm i(?:nstall)?\s+([\w@/-]+)/g);
35
+ for (const match of npmMatches) {
36
+ signals.push(`npm package: ${match[1]}`);
37
+ }
38
+ // Detect yarn add references
39
+ const yarnMatches = body.matchAll(/yarn add\s+([\w@/-]+)/g);
40
+ for (const match of yarnMatches) {
41
+ signals.push(`yarn package: ${match[1]}`);
42
+ }
43
+ // Detect pnpm add references
44
+ const pnpmMatches = body.matchAll(/pnpm add\s+([\w@/-]+)/g);
45
+ for (const match of pnpmMatches) {
46
+ signals.push(`pnpm package: ${match[1]}`);
47
+ }
48
+ if (signals.length > 0) {
49
+ results.push({
50
+ number: issue.number,
51
+ title: issue.title,
52
+ htmlUrl: issue.htmlUrl,
53
+ signals,
54
+ });
55
+ }
56
+ }
57
+ return results;
58
+ }
59
+ //# sourceMappingURL=signals.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"signals.js","sourceRoot":"","sources":["../../src/analysis/signals.ts"],"names":[],"mappings":"AAkBA;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAAa;IAChD,OAAO,KAAK;SACT,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,MAAM,IAAI,EAAE,CAAC,SAAS,CAAC,QAAQ,IAAI,CAAC,CAAC;SACxD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,QAAQ,GAAG,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC;SAC3D,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;QACZ,MAAM,EAAE,EAAE,CAAC,MAAM;QACjB,KAAK,EAAE,EAAE,CAAC,KAAK;QACf,OAAO,EAAE,EAAE,CAAC,OAAO;QACnB,SAAS,EAAE,EAAE,CAAC,SAAS;QACvB,IAAI,EAAE,EAAE,CAAC,IAAI;QACb,SAAS,EAAE,EAAE,CAAC,SAAS;KACxB,CAAC,CAAC,CAAC;AACR,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAe;IAC/C,MAAM,OAAO,GAAsB,EAAE,CAAC;IAEtC,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC;QACxB,IAAI,CAAC,IAAI;YAAE,SAAS;QAEpB,MAAM,OAAO,GAAa,EAAE,CAAC;QAE7B,qBAAqB;QACrB,IAAI,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAChC,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAC7B,CAAC;QAED,gCAAgC;QAChC,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,CAAC,gCAAgC,CAAC,CAAC;QACnE,KAAK,MAAM,KAAK,IAAI,UAAU,EAAE,CAAC;YAC/B,OAAO,CAAC,IAAI,CAAC,gBAAgB,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC3C,CAAC;QAED,6BAA6B;QAC7B,MAAM,WAAW,GAAG,IAAI,CAAC,QAAQ,CAAC,wBAAwB,CAAC,CAAC;QAC5D,KAAK,MAAM,KAAK,IAAI,WAAW,EAAE,CAAC;YAChC,OAAO,CAAC,IAAI,CAAC,iBAAiB,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC5C,CAAC;QAED,6BAA6B;QAC7B,MAAM,WAAW,GAAG,IAAI,CAAC,QAAQ,CAAC,wBAAwB,CAAC,CAAC;QAC5D,KAAK,MAAM,KAAK,IAAI,WAAW,EAAE,CAAC;YAChC,OAAO,CAAC,IAAI,CAAC,iBAAiB,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC5C,CAAC;QAED,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,OAAO,CAAC,IAAI,CAAC;gBACX,MAAM,EAAE,KAAK,CAAC,MAAM;gBACpB,KAAK,EAAE,KAAK,CAAC,KAAK;gBAClB,OAAO,EAAE,KAAK,CAAC,OAAO;gBACtB,OAAO;aACR,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Tokenize text: lowercase, remove punctuation (keep alphanumeric + hyphens),
3
+ * split on whitespace, remove stopwords.
4
+ */
5
+ export declare function tokenize(text: string): string[];
6
+ /**
7
+ * Extract bigrams (consecutive token pairs) from a token array.
8
+ */
9
+ export declare function extractBigrams(tokens: string[]): string[];
10
+ /**
11
+ * Tokenize an issue title: strip common prefixes like [Bug], feat:, etc.,
12
+ * then tokenize normally.
13
+ */
14
+ export declare function tokenizeTitle(title: string): string[];
15
+ //# sourceMappingURL=tokenizer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokenizer.d.ts","sourceRoot":"","sources":["../../src/analysis/tokenizer.ts"],"names":[],"mappings":"AA+BA;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAY/C;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAQzD;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAGrD"}
@@ -0,0 +1,64 @@
1
+ const ENGLISH_STOPWORDS = new Set([
2
+ 'the', 'is', 'at', 'which', 'on', 'a', 'an', 'and', 'or', 'but', 'in',
3
+ 'with', 'to', 'for', 'of', 'from', 'by', 'as', 'it', 'this', 'that',
4
+ 'be', 'are', 'was', 'were', 'been', 'being', 'have', 'has', 'had',
5
+ 'do', 'does', 'did', 'will', 'would', 'could', 'should', 'may', 'might',
6
+ 'can', 'shall', 'not', 'no', 'so', 'if', 'when', 'how', 'what', 'where',
7
+ 'who', 'why', 'up', 'out', 'about', 'into', 'over', 'after', 'before',
8
+ 'between', 'under', 'above', 'each', 'few', 'more', 'most', 'some',
9
+ 'such', 'only', 'same', 'than', 'too', 'very', 'just', 'also', 'its',
10
+ 'his', 'her', 'their', 'our', 'your', 'i', 'we', 'you', 'they', 'he',
11
+ 'she', 'me', 'my', 'them', 'us', 'all', 'any', 'both', 'other', 'these',
12
+ 'those',
13
+ ]);
14
+ const GITHUB_STOPWORDS = new Set([
15
+ 'issue', 'bug', 'fix', 'error', 'please', 'using', 'version', 'feature',
16
+ 'request', 'support', 'add', 'update', 'get', 'set', 'use', 'new', 'like',
17
+ 'need', 'want', 'work', 'make', 'run', 'try', 'way', 'one', 'two', 'see',
18
+ 'sure', 'could', 'would', 'still', 'able', 'instead', 'related', 'seems',
19
+ 'etc', 'next', 'js', 'ts', 'app', 'file', 'page', 'component', 'module',
20
+ 'expected', 'actual', 'behavior', 'undefined', 'null', 'type', 'return',
21
+ 'function', 'class', 'import', 'export', 'default', 'config', 'build',
22
+ 'doesn', 'don', 'isn', 'wasn', 'won', 'shouldn', 'couldn', 'hasn',
23
+ 'working', 'works', 'happen', 'happens', 'happening', 'started', 'start',
24
+ 'react', 'vue', 'svelte', 'angular', 'node', 'webpack', 'vite',
25
+ ]);
26
+ const STOPWORDS = new Set([...ENGLISH_STOPWORDS, ...GITHUB_STOPWORDS]);
27
+ const TITLE_PREFIX_RE = /^\s*(\[[\w\s]+\]\s*|(?:feat|fix|bug|chore|docs|refactor|perf|test|ci|build|style):\s*)/i;
28
+ /**
29
+ * Tokenize text: lowercase, remove punctuation (keep alphanumeric + hyphens),
30
+ * split on whitespace, remove stopwords.
31
+ */
32
+ export function tokenize(text) {
33
+ const cleaned = text
34
+ .toLowerCase()
35
+ .replace(/[^a-z0-9\s-]/g, ' ')
36
+ .replace(/\s+/g, ' ')
37
+ .trim();
38
+ if (!cleaned)
39
+ return [];
40
+ return cleaned
41
+ .split(' ')
42
+ .filter((token) => token.length > 1 && !STOPWORDS.has(token) && !/^\d+$/.test(token));
43
+ }
44
+ /**
45
+ * Extract bigrams (consecutive token pairs) from a token array.
46
+ */
47
+ export function extractBigrams(tokens) {
48
+ if (tokens.length < 2)
49
+ return [];
50
+ const bigrams = [];
51
+ for (let i = 0; i < tokens.length - 1; i++) {
52
+ bigrams.push(`${tokens[i]} ${tokens[i + 1]}`);
53
+ }
54
+ return bigrams;
55
+ }
56
+ /**
57
+ * Tokenize an issue title: strip common prefixes like [Bug], feat:, etc.,
58
+ * then tokenize normally.
59
+ */
60
+ export function tokenizeTitle(title) {
61
+ const stripped = title.replace(TITLE_PREFIX_RE, '');
62
+ return tokenize(stripped);
63
+ }
64
+ //# sourceMappingURL=tokenizer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokenizer.js","sourceRoot":"","sources":["../../src/analysis/tokenizer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ export interface EvidenceOptions {
2
+ output: string;
3
+ sort: string;
4
+ limit: number;
5
+ verbose: boolean;
6
+ noCache: boolean;
7
+ }
8
+ export declare function runEvidence(repo: string, query: string, opts: EvidenceOptions): Promise<void>;
9
+ //# sourceMappingURL=evidence.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"evidence.d.ts","sourceRoot":"","sources":["../../src/commands/evidence.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;CAClB;AAkOD,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,eAAe,GACpB,OAAO,CAAC,IAAI,CAAC,CAmIf"}
@@ -0,0 +1,229 @@
1
+ import { resolveToken } from "../github/auth.js";
2
+ import { GitHubClient, RateLimitError } from "../github/client.js";
3
+ import { detectWorkarounds } from "../analysis/signals.js";
4
+ function verbose(msg, enabled) {
5
+ if (enabled) {
6
+ process.stderr.write(msg + "\n");
7
+ }
8
+ }
9
+ function mapToIssue(item) {
10
+ return {
11
+ number: item.number,
12
+ title: item.title,
13
+ body: item.body ?? "",
14
+ labels: item.labels.map((l) => l.name),
15
+ reactions: {
16
+ thumbsUp: item.reactions["+1"],
17
+ thumbsDown: item.reactions["-1"],
18
+ total: item.reactions.total_count,
19
+ },
20
+ commentsCount: item.comments,
21
+ createdAt: item.created_at,
22
+ htmlUrl: item.html_url,
23
+ user: item.user.login,
24
+ state: item.state,
25
+ };
26
+ }
27
+ function daysAgo(dateStr) {
28
+ const now = Date.now();
29
+ const then = new Date(dateStr).getTime();
30
+ return Math.floor((now - then) / (1000 * 60 * 60 * 24));
31
+ }
32
+ function sortParam(sort) {
33
+ if (sort === "reactions")
34
+ return "reactions-+1";
35
+ if (sort === "comments")
36
+ return "comments";
37
+ return "created";
38
+ }
39
+ const RESET = "\x1b[0m";
40
+ const BOLD = "\x1b[1m";
41
+ const DIM = "\x1b[2m";
42
+ function useColor() {
43
+ return !process.env["NO_COLOR"];
44
+ }
45
+ function c(code, text) {
46
+ if (!useColor())
47
+ return text;
48
+ return `${code}${text}${RESET}`;
49
+ }
50
+ function formatJson(result) {
51
+ return JSON.stringify({
52
+ summary: result.summary,
53
+ issues: result.issues,
54
+ });
55
+ }
56
+ function formatTable(result) {
57
+ const headers = ["#", "Reactions", "Title", "Age", "Comments", "URL"];
58
+ const rows = result.issues.map((issue, i) => [
59
+ String(i + 1),
60
+ String(issue.reactions.thumbsUp),
61
+ issue.title.length > 50 ? issue.title.slice(0, 47) + "..." : issue.title,
62
+ `${daysAgo(issue.createdAt)}d`,
63
+ String(issue.commentsCount),
64
+ issue.htmlUrl,
65
+ ]);
66
+ const widths = headers.map((h, col) => Math.max(h.length, ...rows.map((r) => r[col].length)));
67
+ const headerLine = headers.map((h, i) => h.padEnd(widths[i])).join(" ");
68
+ const separator = widths.map((w) => "-".repeat(w)).join(" ");
69
+ const dataLines = rows.map((row) => row.map((cell, i) => cell.padEnd(widths[i])).join(" "));
70
+ return [headerLine, separator, ...dataLines].join("\n");
71
+ }
72
+ function demandStrength(totalReactions) {
73
+ if (totalReactions > 100)
74
+ return "strong";
75
+ if (totalReactions >= 20)
76
+ return "moderate";
77
+ return "weak";
78
+ }
79
+ function formatPretty(result, repo, query, _sortBy) {
80
+ const lines = [];
81
+ // Title
82
+ lines.push(`# Evidence: "${query}" in ${repo}`);
83
+ lines.push("");
84
+ // Summary section
85
+ lines.push("## Summary");
86
+ const strength = demandStrength(result.summary.totalReactions);
87
+ lines.push(`- **${result.summary.totalIssues} open issues** across ${result.summary.uniqueUsers} unique authors`);
88
+ lines.push(`- **${result.summary.totalReactions} total \u{1F44D} reactions** \u2014 ${strength} demand signal`);
89
+ const { mergedPRs, rejectedPRsWithDemand, relatedPRs } = result.summary;
90
+ lines.push(`- **${relatedPRs} related PRs** (${mergedPRs} merged, ${rejectedPRsWithDemand} rejected with demand)`);
91
+ // Oldest unresolved issue
92
+ const openIssues = result.issues.filter((i) => i.state === "open");
93
+ if (openIssues.length > 0) {
94
+ let oldest = openIssues[0];
95
+ let oldestAge = daysAgo(oldest.createdAt);
96
+ for (const issue of openIssues) {
97
+ const age = daysAgo(issue.createdAt);
98
+ if (age > oldestAge) {
99
+ oldest = issue;
100
+ oldestAge = age;
101
+ }
102
+ }
103
+ lines.push(`- Oldest unresolved: ${oldestAge}d (issue #${oldest.number})`);
104
+ }
105
+ lines.push("");
106
+ // Top Issues by Demand
107
+ lines.push("## Top Issues by Demand");
108
+ const sorted = [...result.issues].sort((a, b) => b.reactions.thumbsUp - a.reactions.thumbsUp);
109
+ for (let i = 0; i < sorted.length; i++) {
110
+ const issue = sorted[i];
111
+ const age = daysAgo(issue.createdAt);
112
+ const labelsStr = issue.labels.length > 0 ? ` | labels: ${issue.labels.join(", ")}` : "";
113
+ lines.push(`${i + 1}. [${issue.reactions.thumbsUp} \u{1F44D}] ${issue.title} (#${issue.number})`);
114
+ lines.push(` Opened ${age} days ago | ${issue.commentsCount} comments${labelsStr}`);
115
+ lines.push(` ${issue.htmlUrl}`);
116
+ }
117
+ // Workarounds Detected
118
+ if (result.workarounds.length > 0) {
119
+ lines.push("");
120
+ lines.push("## Workarounds Detected");
121
+ lines.push(`- ${result.workarounds.length} issues contain code block workarounds`);
122
+ }
123
+ // Rejected PRs (unmet demand)
124
+ const rejectedPRs = result.prs.filter((pr) => pr.status === "rejected" && pr.reactions > 0);
125
+ if (rejectedPRs.length > 0) {
126
+ lines.push("");
127
+ lines.push("## Rejected PRs (unmet demand)");
128
+ for (const pr of rejectedPRs) {
129
+ lines.push(`- [${pr.reactions} \u{1F44D}] ${pr.title} (#${pr.number}) \u2014 closed without merge`);
130
+ }
131
+ }
132
+ return lines.join("\n");
133
+ }
134
+ export async function runEvidence(repo, query, opts) {
135
+ // 1. Validate repo format
136
+ if (!repo || !repo.includes("/")) {
137
+ console.error(`Error: Invalid repo format "${repo}". Expected "owner/repo" (e.g., "vercel/next.js").`);
138
+ process.exit(1);
139
+ }
140
+ const parts = repo.split("/");
141
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
142
+ console.error(`Error: Invalid repo format "${repo}". Expected "owner/repo" (e.g., "vercel/next.js").`);
143
+ process.exit(1);
144
+ }
145
+ try {
146
+ // 2. Resolve auth, create client
147
+ const token = await resolveToken();
148
+ const client = new GitHubClient(token);
149
+ // 3. Search issues matching query
150
+ const sortValue = sortParam(opts.sort);
151
+ const issueQ = `repo:${repo}+${encodeURIComponent(query)}+in:title,body`;
152
+ verbose(`Searching issues: q=${issueQ}&sort=${sortValue}&per_page=${opts.limit}`, opts.verbose);
153
+ const issueSearch = await client.get("https://api.github.com/search/issues", {
154
+ q: `repo:${repo} ${query} in:title,body`,
155
+ per_page: String(opts.limit),
156
+ sort: sortValue,
157
+ });
158
+ // 4. Map results to Issue type
159
+ const issues = issueSearch.items
160
+ .filter((item) => !item.pull_request)
161
+ .map(mapToIssue);
162
+ // 5. Detect workaround signals
163
+ const workarounds = detectWorkarounds(issues);
164
+ verbose(`Found ${workarounds.length} workaround signals`, opts.verbose);
165
+ // 6. Calculate summary stats
166
+ const uniqueUsers = new Set(issues.map((i) => i.user));
167
+ const totalReactions = issues.reduce((sum, i) => sum + i.reactions.thumbsUp, 0);
168
+ // 7. Search for related PRs
169
+ verbose("Searching related PRs...", opts.verbose);
170
+ const prSearch = await client.get("https://api.github.com/search/issues", {
171
+ q: `repo:${repo} ${query} is:pr`,
172
+ per_page: "20",
173
+ });
174
+ const prs = prSearch.items.map((item) => {
175
+ const prItem = item;
176
+ const merged = prItem.pull_request?.merged_at !== null;
177
+ return {
178
+ number: prItem.number,
179
+ title: prItem.title,
180
+ htmlUrl: prItem.html_url,
181
+ status: merged ? "merged" : "rejected",
182
+ reactions: prItem.reactions["+1"],
183
+ };
184
+ });
185
+ const mergedPRs = prs.filter((p) => p.status === "merged").length;
186
+ const rejectedPRsWithDemand = prs.filter((p) => p.status === "rejected" && p.reactions > 0).length;
187
+ const summary = {
188
+ totalIssues: issues.length,
189
+ uniqueUsers: uniqueUsers.size,
190
+ totalReactions,
191
+ relatedPRs: prs.length,
192
+ mergedPRs,
193
+ rejectedPRsWithDemand,
194
+ };
195
+ const result = { summary, issues, workarounds, prs };
196
+ // 8. Format output
197
+ let output;
198
+ switch (opts.output) {
199
+ case "json":
200
+ output = formatJson(result);
201
+ break;
202
+ case "table":
203
+ output = formatTable(result);
204
+ break;
205
+ default:
206
+ output = formatPretty(result, repo, query, opts.sort);
207
+ break;
208
+ }
209
+ console.log(output);
210
+ }
211
+ catch (error) {
212
+ if (error instanceof RateLimitError) {
213
+ console.error(`Rate limit exhausted. Resets at ${new Date(error.resetAt * 1000).toISOString()}. Try again later or authenticate with a GitHub token.`);
214
+ process.exit(1);
215
+ }
216
+ if (error instanceof Error) {
217
+ const msg = error.message;
218
+ if (msg.includes("404")) {
219
+ console.error(`Repository "${repo}" not found. Check the owner/repo name and try again.`);
220
+ process.exit(1);
221
+ }
222
+ console.error(`Error: ${msg}`);
223
+ process.exit(1);
224
+ }
225
+ console.error("An unexpected error occurred.");
226
+ process.exit(1);
227
+ }
228
+ }
229
+ //# sourceMappingURL=evidence.js.map