Skip to content

Commit 1924ae9

Browse files
authored
Merge pull request #318 from raifdmueller/feat/discussion-feedback
feat: anchor feedback via GitHub Discussions
2 parents f8029c0 + 7914715 commit 1924ae9

14 files changed

Lines changed: 901 additions & 17 deletions

File tree

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Create one GitHub Discussion per semantic anchor in the "Anchor Feedback" category.
4+
*
5+
* Prerequisites:
6+
* - gh CLI authenticated with discussions:write scope
7+
* - "Anchor Feedback" discussion category exists
8+
*
9+
* Usage: node scripts/create-anchor-discussions.js [--dry-run]
10+
*/
11+
12+
const { execSync } = require('child_process')
13+
const fs = require('fs')
14+
const path = require('path')
15+
16+
const REPO_ID = 'R_kgDOQTHmRw'
17+
const CATEGORY_ID = 'DIC_kwDOQTHmR84C4oz7'
18+
const WEBSITE_URL = 'https://llm-coding.github.io/Semantic-Anchors'
19+
const DRY_RUN = process.argv.includes('--dry-run')
20+
21+
const anchorsPath = path.join(__dirname, '..', 'website', 'public', 'data', 'anchors.json')
22+
const anchors = JSON.parse(fs.readFileSync(anchorsPath, 'utf-8'))
23+
24+
function sleep(ms) {
25+
return new Promise((resolve) => setTimeout(resolve, ms))
26+
}
27+
28+
function ghGraphql(query) {
29+
const payload = JSON.stringify({ query })
30+
const result = execSync('gh api graphql --input -', {
31+
input: payload,
32+
encoding: 'utf-8',
33+
})
34+
return JSON.parse(result)
35+
}
36+
37+
function fetchExistingDiscussions() {
38+
const discussions = []
39+
let cursor = null
40+
let hasNext = true
41+
42+
while (hasNext) {
43+
const afterClause = cursor ? `, after: "${cursor}"` : ''
44+
const result = ghGraphql(`{
45+
repository(owner: "LLM-Coding", name: "Semantic-Anchors") {
46+
discussions(first: 100, categoryId: "${CATEGORY_ID}"${afterClause}) {
47+
nodes { title body }
48+
pageInfo { hasNextPage endCursor }
49+
}
50+
}
51+
}`)
52+
53+
const data = result.data.repository.discussions
54+
discussions.push(...data.nodes)
55+
hasNext = data.pageInfo.hasNextPage
56+
cursor = data.pageInfo.endCursor
57+
}
58+
59+
return discussions
60+
}
61+
62+
function extractAnchorId(body) {
63+
const match = body.match(/<!-- anchor-id: ([a-z0-9-]+) -->/)
64+
return match ? match[1] : null
65+
}
66+
67+
async function main() {
68+
console.log(`Found ${anchors.length} anchors`)
69+
70+
// Fetch existing discussions to avoid duplicates
71+
console.log('Fetching existing discussions...')
72+
const existing = fetchExistingDiscussions()
73+
const existingIds = new Set(existing.map((d) => extractAnchorId(d.body)).filter(Boolean))
74+
console.log(`Found ${existing.length} existing discussions (${existingIds.size} with anchor IDs)`)
75+
76+
const toCreate = anchors.filter((a) => !existingIds.has(a.id))
77+
console.log(`${toCreate.length} discussions to create`)
78+
79+
if (DRY_RUN) {
80+
toCreate.forEach((a) => console.log(` [dry-run] Would create: ⚓ ${a.title}`))
81+
return
82+
}
83+
84+
let created = 0
85+
let failed = 0
86+
let index = 0
87+
const total = toCreate.length
88+
for (const anchor of toCreate) {
89+
index++
90+
const title = `⚓ ${anchor.title}`
91+
const body = [
92+
`<!-- anchor-id: ${anchor.id} -->`,
93+
'',
94+
`**${anchor.title}** — vote and discuss this semantic anchor.`,
95+
'',
96+
`👉 [View on Semantic Anchors website](${WEBSITE_URL}/#/anchor/${anchor.id})`,
97+
'',
98+
'---',
99+
'_Upvote this discussion if you find this anchor useful. Leave a comment to suggest improvements._',
100+
].join('\n')
101+
102+
try {
103+
const result = ghGraphql(`mutation {
104+
createDiscussion(input: {
105+
repositoryId: "${REPO_ID}",
106+
categoryId: "${CATEGORY_ID}",
107+
title: ${JSON.stringify(title)},
108+
body: ${JSON.stringify(body)}
109+
}) {
110+
discussion { url }
111+
}
112+
}`)
113+
const url = result.data.createDiscussion.discussion.url
114+
created++
115+
console.log(` [${created}/${toCreate.length}] Created: ${title}${url}`)
116+
} catch (err) {
117+
failed++
118+
console.error(` ✗ Failed: ${title}${err.stderr || err.message}`)
119+
}
120+
121+
// Delay to avoid secondary rate limits
122+
if (index < total) {
123+
await sleep(2000)
124+
}
125+
}
126+
127+
console.log(`\nDone. Created ${created} discussions.`)
128+
if (failed > 0) {
129+
console.error(`Failed to create ${failed} discussions.`)
130+
process.exitCode = 1
131+
}
132+
}
133+
134+
main().catch(console.error)

scripts/eslint.config.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ const nodeGlobals = {
1010
process: 'readonly',
1111
console: 'readonly',
1212
Buffer: 'readonly',
13+
setTimeout: 'readonly',
14+
setInterval: 'readonly',
15+
clearTimeout: 'readonly',
16+
clearInterval: 'readonly',
1317
}
1418

1519
export default [

scripts/fetch-discussion-votes.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Fetch upvote and comment counts from GitHub Discussions (Anchor Feedback category).
4+
* Outputs website/public/data/feedback.json mapping anchor IDs to vote/comment data.
5+
*
6+
* Usage: node scripts/fetch-discussion-votes.js
7+
*/
8+
9+
const { execSync } = require('child_process')
10+
const fs = require('fs')
11+
const path = require('path')
12+
13+
const CATEGORY_ID = 'DIC_kwDOQTHmR84C4oz7'
14+
15+
function ghGraphql(query) {
16+
const payload = JSON.stringify({ query })
17+
const result = execSync('gh api graphql --input -', {
18+
input: payload,
19+
encoding: 'utf-8',
20+
})
21+
return JSON.parse(result)
22+
}
23+
24+
function fetchAllDiscussions() {
25+
const discussions = []
26+
let cursor = null
27+
let hasNext = true
28+
29+
while (hasNext) {
30+
const afterClause = cursor ? `, after: "${cursor}"` : ''
31+
const result = ghGraphql(`{
32+
repository(owner: "LLM-Coding", name: "Semantic-Anchors") {
33+
discussions(first: 100, categoryId: "${CATEGORY_ID}"${afterClause}) {
34+
nodes {
35+
title
36+
body
37+
url
38+
upvoteCount
39+
comments { totalCount }
40+
}
41+
pageInfo { hasNextPage endCursor }
42+
}
43+
}
44+
}`)
45+
46+
const data = result.data.repository.discussions
47+
discussions.push(...data.nodes)
48+
hasNext = data.pageInfo.hasNextPage
49+
cursor = data.pageInfo.endCursor
50+
}
51+
52+
return discussions
53+
}
54+
55+
function extractAnchorId(body) {
56+
const match = body.match(/<!-- anchor-id: ([a-z0-9-]+) -->/)
57+
return match ? match[1] : null
58+
}
59+
60+
function main() {
61+
console.log('Fetching discussion data...')
62+
const discussions = fetchAllDiscussions()
63+
console.log(`Fetched ${discussions.length} discussions`)
64+
65+
const feedback = {}
66+
let mapped = 0
67+
68+
for (const d of discussions) {
69+
const anchorId = extractAnchorId(d.body)
70+
if (!anchorId) continue
71+
72+
if (feedback[anchorId]) {
73+
console.warn(`Duplicate mapping for "${anchorId}" found: ${d.url}`)
74+
continue
75+
}
76+
77+
feedback[anchorId] = {
78+
upvotes: d.upvoteCount,
79+
comments: d.comments.totalCount,
80+
url: d.url,
81+
}
82+
mapped++
83+
}
84+
85+
const outPath = path.join(__dirname, '..', 'website', 'public', 'data', 'feedback.json')
86+
fs.writeFileSync(outPath, JSON.stringify(feedback, null, 2) + '\n', 'utf-8')
87+
console.log(`✓ Written ${outPath}`)
88+
console.log(` ${mapped} anchors mapped, ${discussions.length - mapped} unmapped`)
89+
}
90+
91+
main()

0 commit comments

Comments
 (0)