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.
- package/README.md +195 -0
- package/dist/analysis/ai-scorer.d.ts +34 -0
- package/dist/analysis/ai-scorer.d.ts.map +1 -0
- package/dist/analysis/ai-scorer.js +139 -0
- package/dist/analysis/ai-scorer.js.map +1 -0
- package/dist/analysis/cluster.d.ts +14 -0
- package/dist/analysis/cluster.d.ts.map +1 -0
- package/dist/analysis/cluster.js +223 -0
- package/dist/analysis/cluster.js.map +1 -0
- package/dist/analysis/scorer.d.ts +27 -0
- package/dist/analysis/scorer.d.ts.map +1 -0
- package/dist/analysis/scorer.js +196 -0
- package/dist/analysis/scorer.js.map +1 -0
- package/dist/analysis/signals.d.ts +30 -0
- package/dist/analysis/signals.d.ts.map +1 -0
- package/dist/analysis/signals.js +59 -0
- package/dist/analysis/signals.js.map +1 -0
- package/dist/analysis/tokenizer.d.ts +15 -0
- package/dist/analysis/tokenizer.d.ts.map +1 -0
- package/dist/analysis/tokenizer.js +64 -0
- package/dist/analysis/tokenizer.js.map +1 -0
- package/dist/commands/evidence.d.ts +9 -0
- package/dist/commands/evidence.d.ts.map +1 -0
- package/dist/commands/evidence.js +229 -0
- package/dist/commands/evidence.js.map +1 -0
- package/dist/commands/scan-org.d.ts +3 -0
- package/dist/commands/scan-org.d.ts.map +1 -0
- package/dist/commands/scan-org.js +88 -0
- package/dist/commands/scan-org.js.map +1 -0
- package/dist/commands/scan.d.ts +32 -0
- package/dist/commands/scan.d.ts.map +1 -0
- package/dist/commands/scan.js +197 -0
- package/dist/commands/scan.js.map +1 -0
- package/dist/commands/trending.d.ts +14 -0
- package/dist/commands/trending.d.ts.map +1 -0
- package/dist/commands/trending.js +145 -0
- package/dist/commands/trending.js.map +1 -0
- package/dist/github/auth.d.ts +3 -0
- package/dist/github/auth.d.ts.map +1 -0
- package/dist/github/auth.js +33 -0
- package/dist/github/auth.js.map +1 -0
- package/dist/github/cache.d.ts +18 -0
- package/dist/github/cache.d.ts.map +1 -0
- package/dist/github/cache.js +51 -0
- package/dist/github/cache.js.map +1 -0
- package/dist/github/client.d.ts +24 -0
- package/dist/github/client.d.ts.map +1 -0
- package/dist/github/client.js +140 -0
- package/dist/github/client.js.map +1 -0
- package/dist/github/fetchers.d.ts +13 -0
- package/dist/github/fetchers.d.ts.map +1 -0
- package/dist/github/fetchers.js +142 -0
- package/dist/github/fetchers.js.map +1 -0
- package/dist/github/types.d.ts +46 -0
- package/dist/github/types.d.ts.map +1 -0
- package/dist/github/types.js +2 -0
- package/dist/github/types.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +116 -0
- package/dist/index.js.map +1 -0
- package/dist/output/formatters.d.ts +35 -0
- package/dist/output/formatters.d.ts.map +1 -0
- package/dist/output/formatters.js +195 -0
- package/dist/output/formatters.js.map +1 -0
- 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":"AAAA,MAAM,iBAAiB,GAAG,IAAI,GAAG,CAAC;IAChC,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI;IACrE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM;IACnE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK;IACjE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,OAAO;IACvE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO;IACvE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ;IACrE,SAAS,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAClE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK;IACpE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI;IACpE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO;IACvE,OAAO;CACR,CAAC,CAAC;AAEH,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC;IAC/B,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS;IACvE,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM;IACzE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK;IACxE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO;IACxE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,QAAQ;IACvE,UAAU,EAAE,QAAQ,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ;IACvE,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO;IACrE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM;IACjE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,WAAW,EAAE,SAAS,EAAE,OAAO;IACxE,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;CAC/D,CAAC,CAAC;AAEH,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,iBAAiB,EAAE,GAAG,gBAAgB,CAAC,CAAC,CAAC;AAEvE,MAAM,eAAe,GAAG,yFAAyF,CAAC;AAElH;;;GAGG;AACH,MAAM,UAAU,QAAQ,CAAC,IAAY;IACnC,MAAM,OAAO,GAAG,IAAI;SACjB,WAAW,EAAE;SACb,OAAO,CAAC,eAAe,EAAE,GAAG,CAAC;SAC7B,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;SACpB,IAAI,EAAE,CAAC;IAEV,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,CAAC;IAExB,OAAO,OAAO;SACX,KAAK,CAAC,GAAG,CAAC;SACV,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;AAC1F,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,cAAc,CAAC,MAAgB;IAC7C,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,EAAE,CAAC;IAEjC,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3C,OAAO,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC;IAChD,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,KAAa;IACzC,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;IACpD,OAAO,QAAQ,CAAC,QAAQ,CAAC,CAAC;AAC5B,CAAC"}
|
|
@@ -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
|