Skip to content

Commit 10fbc14

Browse files
sounmindclaude
andcommitted
feat: add learning data fetching utilities
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c11c641 commit 10fbc14

1 file changed

Lines changed: 162 additions & 0 deletions

File tree

utils/learningData.js

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/**
2+
* Learning data fetching utilities for DaleStudy GitHub App
3+
*/
4+
5+
/**
6+
* Fetches problem-categories.json from the repo root via GitHub API.
7+
* Returns parsed JSON object, or null if the file is not found (404).
8+
* Throws on other errors.
9+
*
10+
* @param {string} repoOwner
11+
* @param {string} repoName
12+
* @param {string} appToken
13+
* @returns {Promise<object|null>}
14+
*/
15+
export async function fetchProblemCategories(repoOwner, repoName, appToken) {
16+
const url = `https://api.github.com/repos/${repoOwner}/${repoName}/contents/problem-categories.json`;
17+
18+
const response = await fetch(url, {
19+
headers: {
20+
Authorization: `Bearer ${appToken}`,
21+
Accept: "application/vnd.github.raw+json",
22+
"User-Agent": "DaleStudy-GitHub-App",
23+
},
24+
});
25+
26+
if (response.status === 404) {
27+
return null;
28+
}
29+
30+
if (!response.ok) {
31+
throw new Error(
32+
`Failed to fetch problem-categories.json: ${response.status} ${response.statusText}`
33+
);
34+
}
35+
36+
return await response.json();
37+
}
38+
39+
/**
40+
* Fetches the full repo file tree and returns a deduplicated array of problem
41+
* names that have a solution file submitted by the given username.
42+
*
43+
* Matches files of the form: {problem-name}/{username}.{ext}
44+
*
45+
* @param {string} repoOwner
46+
* @param {string} repoName
47+
* @param {string} username
48+
* @param {string} appToken
49+
* @returns {Promise<string[]>}
50+
*/
51+
export async function fetchUserSolutions(
52+
repoOwner,
53+
repoName,
54+
username,
55+
appToken
56+
) {
57+
const url = `https://api.github.com/repos/${repoOwner}/${repoName}/git/trees/main?recursive=1`;
58+
59+
const response = await fetch(url, {
60+
headers: {
61+
Authorization: `Bearer ${appToken}`,
62+
Accept: "application/vnd.github+json",
63+
"User-Agent": "DaleStudy-GitHub-App",
64+
},
65+
});
66+
67+
if (!response.ok) {
68+
throw new Error(
69+
`Failed to fetch repo tree: ${response.status} ${response.statusText}`
70+
);
71+
}
72+
73+
const data = await response.json();
74+
75+
// Pattern: {problem-name}/{username}.{ext}
76+
// The path must have exactly two segments and the filename must be username.ext
77+
const usernamePattern = new RegExp(
78+
`^([^/]+)/${escapeRegExp(username)}\\.[^/]+$`
79+
);
80+
81+
const problemNames = new Set();
82+
83+
for (const item of data.tree) {
84+
if (item.type !== "blob") continue;
85+
86+
const match = item.path.match(usernamePattern);
87+
if (match) {
88+
problemNames.add(match[1]);
89+
}
90+
}
91+
92+
return Array.from(problemNames);
93+
}
94+
95+
/**
96+
* Fetches the files changed in a PR and returns those that match
97+
* {problem-name}/{username}.{ext} and are added or modified.
98+
*
99+
* @param {string} repoOwner
100+
* @param {string} repoName
101+
* @param {number} prNumber
102+
* @param {string} username
103+
* @param {string} appToken
104+
* @returns {Promise<Array<{ problemName: string, filename: string, rawUrl: string }>>}
105+
*/
106+
export async function fetchPRSubmissions(
107+
repoOwner,
108+
repoName,
109+
prNumber,
110+
username,
111+
appToken
112+
) {
113+
const url = `https://api.github.com/repos/${repoOwner}/${repoName}/pulls/${prNumber}/files?per_page=100`;
114+
115+
const response = await fetch(url, {
116+
headers: {
117+
Authorization: `Bearer ${appToken}`,
118+
Accept: "application/vnd.github+json",
119+
"User-Agent": "DaleStudy-GitHub-App",
120+
},
121+
});
122+
123+
if (!response.ok) {
124+
throw new Error(
125+
`Failed to fetch PR files: ${response.status} ${response.statusText}`
126+
);
127+
}
128+
129+
const files = await response.json();
130+
131+
// Pattern: {problem-name}/{username}.{ext}
132+
const usernamePattern = new RegExp(
133+
`^([^/]+)/${escapeRegExp(username)}\\.[^/]+$`
134+
);
135+
136+
const results = [];
137+
138+
for (const file of files) {
139+
if (file.status !== "added" && file.status !== "modified") continue;
140+
141+
const match = file.filename.match(usernamePattern);
142+
if (match) {
143+
results.push({
144+
problemName: match[1],
145+
filename: file.filename,
146+
rawUrl: file.raw_url,
147+
});
148+
}
149+
}
150+
151+
return results;
152+
}
153+
154+
/**
155+
* Escapes special regex characters in a string.
156+
*
157+
* @param {string} str
158+
* @returns {string}
159+
*/
160+
function escapeRegExp(str) {
161+
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
162+
}

0 commit comments

Comments
 (0)