@devilsdev/rag-pipeline-utils 2.1.3 → 2.1.5

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.
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Version: 2.1.2
3
+ * Path: __tests__/scripts/ensure-roadmap-labels.test.js
4
+ * Description: Unit tests for roadmap label creation using ESM-compatible Jest syntax.
5
+ * Author: Ali Kahwaji
6
+ */
7
+
8
+ import { jest } from '@jest/globals';
9
+
10
+ jest.unstable_mockModule('octokit', () => ({
11
+ Octokit: jest.fn().mockImplementation(() => ({
12
+ rest: {
13
+ issues: {
14
+ listLabelsForRepo: mockListLabelsForRepo,
15
+ createLabel: mockCreateLabel
16
+ }
17
+ }
18
+ }))
19
+ }));
20
+
21
+ // ✅ Shared mocks outside import context
22
+ const mockCreateLabel = jest.fn();
23
+ const mockListLabelsForRepo = jest.fn();
24
+
25
+ let ensureRoadmapLabels;
26
+ let roadmapLabels;
27
+
28
+ beforeAll(async () => {
29
+ const imported = await import('../../scripts/ensure-roadmap-labels.js');
30
+ ensureRoadmapLabels = imported.ensureRoadmapLabels;
31
+ roadmapLabels = imported.roadmapLabels;
32
+ });
33
+
34
+ beforeEach(() => {
35
+ mockCreateLabel.mockReset();
36
+ mockListLabelsForRepo.mockReset();
37
+ });
38
+
39
+ describe('ensureRoadmapLabels', () => {
40
+ it('creates all labels if none exist', async () => {
41
+ mockListLabelsForRepo.mockResolvedValue({ data: [] });
42
+
43
+ await ensureRoadmapLabels({ token: 'test-token', owner: 'ali', repo: 'rag-pipeline-utils' });
44
+
45
+ expect(mockCreateLabel).toHaveBeenCalledTimes(roadmapLabels.length);
46
+ for (const label of roadmapLabels) {
47
+ expect(mockCreateLabel).toHaveBeenCalledWith({
48
+ owner: 'ali',
49
+ repo: 'rag-pipeline-utils',
50
+ ...label
51
+ });
52
+ }
53
+ });
54
+
55
+ it('skips already existing labels', async () => {
56
+ const existing = [{ name: 'priority: high' }, { name: 'group: docs' }];
57
+ mockListLabelsForRepo.mockResolvedValue({ data: existing });
58
+
59
+ await ensureRoadmapLabels({ token: 'test-token', owner: 'ali', repo: 'rag-pipeline-utils' });
60
+
61
+ const expected = roadmapLabels.filter(l => !existing.some(e => e.name === l.name));
62
+ expect(mockCreateLabel).toHaveBeenCalledTimes(expected.length);
63
+ });
64
+ });
@@ -0,0 +1,13 @@
1
+ ---
2
+ slug: release-v2.1.3
3
+ title: ✨ Version v2.1.3 Released
4
+ authors: [ali]
5
+ tags: [release, changelog]
6
+ ---
7
+
8
+ RAG Pipeline Utils vv2.1.3 is now available on NPM!
9
+
10
+ #### Highlights
11
+ - 🚀 Auto-published blog from GitHub Release
12
+ - 📦 Version bump: v2.1.3
13
+ - 📘 See CHANGELOG.md for full details
@@ -0,0 +1,13 @@
1
+ ---
2
+ slug: release-v2.1.4
3
+ title: ✨ Version v2.1.4 Released
4
+ authors: [ali]
5
+ tags: [release, changelog]
6
+ ---
7
+
8
+ RAG Pipeline Utils vv2.1.4 is now available on NPM!
9
+
10
+ #### Highlights
11
+ - 🚀 Auto-published blog from GitHub Release
12
+ - 📦 Version bump: v2.1.4
13
+ - 📘 See CHANGELOG.md for full details
@@ -11,7 +11,10 @@
11
11
  "clear": "docusaurus clear",
12
12
  "serve": "docusaurus serve",
13
13
  "write-translations": "docusaurus write-translations",
14
- "write-heading-ids": "docusaurus write-heading-ids"
14
+ "write-heading-ids": "docusaurus write-heading-ids",
15
+ "lint": "echo 'Lint skipped for docs-site'",
16
+ "test": "echo 'No tests in docs-site. Run from root.'",
17
+ "docs:build": "npm run --workspace ../ docs:build"
15
18
  },
16
19
  "dependencies": {
17
20
  "@docusaurus/core": "3.7.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devilsdev/rag-pipeline-utils",
3
- "version": "2.1.3",
3
+ "version": "2.1.5",
4
4
  "description": "A modular toolkit for building RAG (Retrieval-Augmented Generation) pipelines in Node.js",
5
5
  "main": "src/core/create-pipeline.js",
6
6
  "bin": {
@@ -27,7 +27,10 @@
27
27
  "docs:start": "docusaurus start docs-site",
28
28
  "docs:build": "docusaurus build docs-site",
29
29
  "docs:serve": "docusaurus serve docs-site",
30
- "docs:deploy": "docusaurus deploy docs-site"
30
+ "docs:deploy": "docusaurus deploy docs-site",
31
+ "release:tag": "node scripts/tag-release.js",
32
+ "roadmap:close": "node scripts/close-done-roadmap-issues.js",
33
+ "labels:sync": "node scripts/ensure-roadmap-labels.js"
31
34
  },
32
35
  "lint-staged": {
33
36
  "**/*.{js,ts}": [
@@ -68,14 +71,6 @@
68
71
  "@semantic-release/github": "^9.2.6",
69
72
  "@semantic-release/npm": "^11.0.3",
70
73
  "@semantic-release/release-notes-generator": "^14.0.3",
71
- "@storybook/addon-essentials": "^8.6.12",
72
- "@storybook/addon-interactions": "^8.6.12",
73
- "@storybook/addon-onboarding": "^8.6.12",
74
- "@storybook/addon-webpack5-compiler-swc": "^3.0.0",
75
- "@storybook/blocks": "^8.6.12",
76
- "@storybook/react": "^8.6.12",
77
- "@storybook/react-webpack5": "^8.6.12",
78
- "@storybook/test": "^8.6.12",
79
74
  "@types/jest": "^29.5.14",
80
75
  "babel-jest": "^29.7.0",
81
76
  "codecov": "^3.8.2",
@@ -92,10 +87,10 @@
92
87
  "prop-types": "^15.8.1",
93
88
  "rimraf": "^6.0.1",
94
89
  "semantic-release": "^21.1.2",
95
- "storybook": "^8.6.12",
96
90
  "ts-jest": "^29.3.2"
97
91
  },
98
92
  "dependencies": {
93
+ "@octokit/rest": "^21.1.1",
99
94
  "ajv": "^8.17.1",
100
95
  "axios": "^1.8.4",
101
96
  "chalk": "^5.4.1",
@@ -105,6 +100,7 @@
105
100
  "fast-glob": "^3.3.3",
106
101
  "framer-motion": "^12.7.4",
107
102
  "jsdom": "^26.1.0",
103
+ "octokit": "^4.1.3",
108
104
  "openai": "^4.93.0",
109
105
  "pino": "^9.6.0",
110
106
  "plaiceholder": "^3.0.0",
@@ -1,89 +1,102 @@
1
1
  /**
2
- * File: scripts/banner-injector.js
3
- * Description: Script to programmatically inject file-level banners into specified project files
4
- * Author: Ali Kahwaji
5
2
  * Version: 1.0.0
3
+ * Description: Syncs roadmap items from a structured Markdown file into GitHub Issues with labels and assignees.
4
+ * Author: Ali Kahwaji
6
5
  */
7
6
 
8
7
  import fs from 'fs';
9
8
  import path from 'path';
10
- import glob from 'glob';
9
+ import { Octokit } from 'octokit';
10
+ import dotenv from 'dotenv';
11
11
 
12
- const banners = {
13
- '__tests__/unit/reranker/llm-reranker.test.js': `/**
14
- * File: __tests__/unit/reranker/llm-reranker.test.js
15
- * Description: Unit and integration tests with mocks for RAG pipeline
16
- * Author: Ali Kahwaji
17
- * Version: 1.0.4
18
- */\n\n`,
12
+ dotenv.config();
19
13
 
20
- '__tests__/unit/reranker/reranker.snapshot.test.js': `/**
21
- * File: __tests__/unit/reranker/reranker.snapshot.test.js
22
- * Description: Snapshot tests for LLM reranker
23
- * Author: Ali Kahwaji
24
- * Version: 1.0.4
25
- */\n\n`,
14
+ /**
15
+ * GitHub Personal Access Token required via .env or CI secrets
16
+ * Ensure the token has 'repo' scope for private issue creation.
17
+ */
18
+ const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
19
+ const GITHUB_REPO = process.env.GITHUB_REPO; // e.g. DevilsDev/rag-pipeline-utils
20
+ const ROADMAP_PATH = path.resolve('.github/PROJECT_ROADMAP.md');
26
21
 
27
- '__tests__/unit/core/plugin-registry.test.js': `/**
28
- * File: __tests__/unit/core/plugin-registry.test.js
29
- * Description: Unit and integration tests with mocks for RAG pipeline
30
- * Author: Ali Kahwaji
31
- * Version: 1.0.4
32
- */\n\n`,
22
+ if (!GITHUB_TOKEN || !GITHUB_REPO) {
23
+ console.error('❌ GITHUB_TOKEN and GITHUB_REPO must be defined in environment.');
24
+ process.exit(1);
25
+ }
33
26
 
34
- '__tests__/integration/config/load-config.test.js': `/**
35
- * File: __tests__/integration/config/load-config.test.js
36
- * Description: Unit and integration tests with mocks for RAG pipeline
37
- * Author: Ali Kahwaji
38
- * Version: 1.0.4
39
- */\n\n`,
27
+ const octokit = new Octokit({ auth: GITHUB_TOKEN });
40
28
 
41
- '__tests__/integration/cli/config-flow.test.js': `/**
42
- * File: __tests__/integration/cli/config-flow.test.js
43
- * Description: Unit and integration tests with mocks for RAG pipeline
44
- * Author: Ali Kahwaji
45
- * Version: 1.0.4
46
- */\n\n`,
29
+ /**
30
+ * Parse roadmap Markdown file into structured issue definitions
31
+ */
32
+ function parseRoadmapMarkdown(markdown) {
33
+ const lines = markdown.split('\n');
34
+ const issues = [];
47
35
 
48
- 'src/mocks/pdf-loader.js': `/**
49
- * File: src/mocks/pdf-loader.js
50
- * Description: Unit and integration tests with mocks for RAG pipeline
51
- * Author: Ali Kahwaji
52
- * Version: 1.0.4
53
- */\n\n`,
36
+ for (const line of lines) {
37
+ if (line.startsWith('|')) {
38
+ const cells = line.split('|').map(c => c.trim());
54
39
 
55
- 'src/mocks/openai-embedder.js': `/**
56
- * File: src/mocks/openai-embedder.js
57
- * Description: Unit and integration tests with mocks for RAG pipeline
58
- * Author: Ali Kahwaji
59
- * Version: 1.0.4
60
- */\n\n`,
40
+ if (cells.length < 6 || cells[0] === 'Phase') continue;
61
41
 
62
- 'src/mocks/pinecone-retriever.js': `/**
63
- * File: src/mocks/pinecone-retriever.js
64
- * Description: Unit and integration tests with mocks for RAG pipeline
65
- * Author: Ali Kahwaji
66
- * Version: 1.0.4
67
- */\n\n`
68
- };
69
-
70
- Object.entries(banners).forEach(([filePath, banner]) => {
71
- const resolvedPath = path.resolve(filePath);
42
+ const [, phase, priority, feature, group, tags] = cells;
72
43
 
73
- if (!fs.existsSync(resolvedPath)) {
74
- console.warn(`[SKIPPED] ${filePath} does not exist.`);
75
- return;
44
+ issues.push({
45
+ title: `${phase.trim()}: ${feature.trim()}`,
46
+ body: `**Feature Group:** ${group}\n**Priority:** ${priority}`,
47
+ labels: [phase, priority, ...tags.split(',').map(tag => tag.trim())].filter(Boolean),
48
+ });
49
+ }
76
50
  }
77
51
 
78
- const originalContent = fs.readFileSync(resolvedPath, 'utf-8');
52
+ return issues;
53
+ }
54
+
55
+ /**
56
+ * Create an issue in GitHub if it doesn't already exist
57
+ */
58
+ async function createOrUpdateIssues(issues) {
59
+ const [owner, repo] = GITHUB_REPO.split('/');
60
+
61
+ const { data: existingIssues } = await octokit.rest.issues.listForRepo({
62
+ owner,
63
+ repo,
64
+ state: 'all',
65
+ per_page: 100,
66
+ });
79
67
 
80
- // Skip if banner already present
81
- if (originalContent.startsWith('/**')) {
82
- console.log(`[SKIPPED] ${filePath} already contains a banner.`);
83
- return;
68
+ for (const issue of issues) {
69
+ const exists = existingIssues.find(i => i.title === issue.title);
70
+ if (exists) {
71
+ console.log(`✅ Skipped (exists): ${issue.title}`);
72
+ continue;
73
+ }
74
+
75
+ await octokit.rest.issues.create({
76
+ owner,
77
+ repo,
78
+ title: issue.title,
79
+ body: issue.body,
80
+ labels: issue.labels,
81
+ });
82
+
83
+ console.log(`➕ Created: ${issue.title}`);
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Entrypoint
89
+ */
90
+ async function main() {
91
+ try {
92
+ const roadmap = fs.readFileSync(ROADMAP_PATH, 'utf-8');
93
+ const issues = parseRoadmapMarkdown(roadmap);
94
+ await createOrUpdateIssues(issues);
95
+ console.log(`🎯 Synced ${issues.length} roadmap issues.`);
96
+ } catch (err) {
97
+ console.error('🚨 Roadmap sync failed:', err.message);
98
+ process.exit(1);
84
99
  }
100
+ }
85
101
 
86
- const newContent = banner + originalContent;
87
- fs.writeFileSync(resolvedPath, newContent, 'utf-8');
88
- console.log(`[INJECTED] Banner added to ${filePath}`);
89
- });
102
+ main();
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Version: 1.1.0
3
+ * Description: Closes GitHub issues marked as ✅ Done in the PROJECT_ROADMAP.md
4
+ * Author: Ali Kahwaji
5
+ */
6
+
7
+ import fs from 'fs';
8
+ import { Octokit } from 'octokit';
9
+
10
+ const GITHUB_TOKEN = process.env.GITHUB_TOKEN;
11
+ const OWNER = 'DevilsDev';
12
+ const REPO = 'rag-pipeline-utils';
13
+ const ROADMAP_PATH = '.github/PROJECT_ROADMAP.md';
14
+
15
+ const octokit = new Octokit({ auth: GITHUB_TOKEN });
16
+
17
+ async function closeDoneIssues() {
18
+ const content = fs.readFileSync(ROADMAP_PATH, 'utf-8');
19
+ const lines = content.split('\n').filter(line => line.startsWith('|'));
20
+ if (lines.length < 3) {
21
+ console.log('⚠️ No roadmap rows found to evaluate.');
22
+ return;
23
+ }
24
+
25
+ const rows = lines.slice(2); // Skip header and separator
26
+
27
+ const { data: issues } = await octokit.rest.issues.listForRepo({
28
+ owner: OWNER,
29
+ repo: REPO,
30
+ state: 'open',
31
+ per_page: 100
32
+ });
33
+
34
+ for (const row of rows) {
35
+ const cols = row.split('|').map(col => col.trim());
36
+ const [, , , title, , status] = cols;
37
+
38
+ const safeStatus = status?.toLowerCase?.();
39
+ if (!safeStatus || !safeStatus.includes('✅')) continue;
40
+
41
+ const match = issues.find(issue => issue.title.trim() === title.trim());
42
+ if (!match) continue;
43
+
44
+ await octokit.rest.issues.createComment({
45
+ owner: OWNER,
46
+ repo: REPO,
47
+ issue_number: match.number,
48
+ body: '✅ Automatically closed from roadmap sync.'
49
+ });
50
+
51
+ await octokit.rest.issues.update({
52
+ owner: OWNER,
53
+ repo: REPO,
54
+ issue_number: match.number,
55
+ state: 'closed'
56
+ });
57
+
58
+ console.log(`🔒 Closed: #${match.number} "${title}"`);
59
+ }
60
+ }
61
+
62
+ closeDoneIssues().catch(error => {
63
+ console.error('❌ Error closing roadmap issues:', error);
64
+ process.exit(1);
65
+ });
@@ -0,0 +1,47 @@
1
+ // scripts/create-roadmap-issues.js
2
+
3
+ /**
4
+ * Version: 2.0.0
5
+ * Description: Parses PROJECT_ROADMAP.md and syncs GitHub issues
6
+ * Author: Ali Kahwaji
7
+ */
8
+
9
+ import fs from 'fs/promises';
10
+ import path from 'path';
11
+ import { Octokit } from 'octokit';
12
+ import yaml from 'js-yaml';
13
+
14
+ const roadmapPath = new URL('../.github/PROJECT_ROADMAP.md', import.meta.url);
15
+
16
+ export async function createRoadmapIssues({ token, owner, repo }) {
17
+ const octokit = new Octokit({ auth: token });
18
+ const content = await fs.readFile(roadmapPath, 'utf-8');
19
+
20
+ const blocks = content.split(/^##\s+/gm).slice(1);
21
+
22
+ for (const block of blocks) {
23
+ const [header, ...bodyLines] = block.trim().split('\n');
24
+ const title = header.trim();
25
+ const metadata = {};
26
+ const description = [];
27
+
28
+ for (const line of bodyLines) {
29
+ if (line.includes(':') && line.includes('**')) {
30
+ const [key, val] = line.split(':');
31
+ metadata[key.trim()] = val.replace(/\*\*/g, '').trim();
32
+ } else {
33
+ description.push(line);
34
+ }
35
+ }
36
+
37
+ const issue = await octokit.rest.issues.create({
38
+ owner,
39
+ repo,
40
+ title,
41
+ body: description.join('\n'),
42
+ labels: [metadata.Priority, metadata.Group].filter(Boolean),
43
+ });
44
+
45
+ console.log(`✅ Created: ${issue.data.title}`);
46
+ }
47
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Version: 2.0.1
3
+ * Path: scripts/ensure-roadmap-labels.js
4
+ * Description: Ensures consistent GitHub labels for roadmap tracking and automation.
5
+ * Author: Ali Kahwaji
6
+ */
7
+
8
+ import { Octokit } from 'octokit';
9
+
10
+
11
+ /**
12
+ * Label definitions for roadmap tracking.
13
+ * Modify this array to add or update roadmap labels.
14
+ * Each object must include: name, color (hex), description.
15
+ */
16
+ const roadmapLabels = [
17
+ { name: 'priority: high', color: 'e11d48', description: 'High priority roadmap item' },
18
+ { name: 'priority: medium', color: 'f59e0b', description: 'Medium priority roadmap item' },
19
+ { name: 'priority: low', color: '10b981', description: 'Low priority roadmap item' },
20
+ { name: 'group: docs', color: '6366f1', description: 'Documentation features' },
21
+ { name: 'group: devx', color: '06b6d4', description: 'Developer experience improvements' },
22
+ { name: 'group: community', color: 'ec4899', description: 'Community tools & engagement' },
23
+ { name: 'group: blog', color: 'f97316', description: 'Blog & SEO enhancements' },
24
+ { name: 'group: infra', color: '64748b', description: 'Infrastructure & deployment features' },
25
+ { name: 'group: hydra', color: '9333ea', description: 'Hydra runtime & configuration' },
26
+ { name: 'status: done', color: '10b981', description: 'Issue has been completed' }
27
+ ];
28
+
29
+ /**
30
+ * Ensures all roadmap labels are present in the target GitHub repository.
31
+ * Skips label creation if already present.
32
+ *
33
+ * @param {Object} params
34
+ * @param {string} params.token - GitHub access token
35
+ * @param {string} params.owner - Repository owner
36
+ * @param {string} params.repo - Repository name
37
+ */
38
+ async function ensureRoadmapLabels({ token, owner, repo }) {
39
+ const octokit = new Octokit({ auth: token });
40
+
41
+ const { data: existingLabels } = await octokit.rest.issues.listLabelsForRepo({
42
+ owner,
43
+ repo
44
+ });
45
+
46
+ const existingNames = new Set(existingLabels.map((label) => label.name));
47
+
48
+ for (const label of roadmapLabels) {
49
+ if (!existingNames.has(label.name)) {
50
+ await octokit.rest.issues.createLabel({ owner, repo, ...label });
51
+ }
52
+ }
53
+ }
54
+
55
+ // CLI support
56
+ if (process.argv[1] === new URL(import.meta.url).pathname) {
57
+ const [owner, repo] = process.env.GITHUB_REPOSITORY?.split('/') || [];
58
+ const token = process.env.GITHUB_TOKEN;
59
+
60
+ if (!token || !owner || !repo) {
61
+ console.error('Missing required GITHUB_TOKEN or GITHUB_REPOSITORY');
62
+ process.exit(1);
63
+ }
64
+
65
+ ensureRoadmapLabels({ token, owner, repo })
66
+ .then(() => console.log('✅ Roadmap labels ensured'))
67
+ .catch((err) => {
68
+ console.error('Label sync failed:', err);
69
+ process.exit(1);
70
+ });
71
+ }
72
+
73
+ export { ensureRoadmapLabels, roadmapLabels };
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Version: 2.0.0
3
+ * Description: Auto-label roadmap issues and clean up outdated items
4
+ * Author: Ali Kahwaji
5
+ */
6
+
7
+ import { Octokit } from 'octokit';
8
+ import fs from 'fs/promises';
9
+ import 'dotenv/config';
10
+
11
+ const repo = process.env.GITHUB_REPO;
12
+ const token = process.env.GITHUB_TOKEN;
13
+ const ROADMAP_FILE = '.github/PROJECT_ROADMAP.md';
14
+ const CLOSE_STALE = true;
15
+
16
+ if (!repo || !token) {
17
+ console.error('Missing GITHUB_REPO or GITHUB_TOKEN env vars');
18
+ process.exit(1);
19
+ }
20
+
21
+ const [owner, repoName] = repo.split('/');
22
+ const octokit = new Octokit({ auth: token });
23
+
24
+ const PRIORITY_LABELS = {
25
+ High: 'priority: high',
26
+ Medium: 'priority: medium',
27
+ Low: 'priority: low',
28
+ };
29
+
30
+ const GROUP_LABELS = {
31
+ 'Documentation': 'group: docs',
32
+ 'Developer Experience': 'group: devx',
33
+ 'Community': 'group: community',
34
+ 'Blog & SEO': 'group: blog',
35
+ 'Infrastructure': 'group: infra',
36
+ };
37
+
38
+ function parseRoadmapMarkdown(content) {
39
+ const lines = content.split('\n').filter(l => l.includes('|'));
40
+ const titles = new Set();
41
+
42
+ for (const line of lines) {
43
+ const parts = line.split('|').map(p => p.trim());
44
+ if (parts.length < 4 || parts[0] === 'Phase') continue;
45
+ const title = parts[3];
46
+ titles.add(title);
47
+ }
48
+
49
+ return titles;
50
+ }
51
+
52
+ async function run() {
53
+ const roadmapRaw = await fs.readFile(ROADMAP_FILE, 'utf8');
54
+ const roadmapTitles = parseRoadmapMarkdown(roadmapRaw);
55
+
56
+ const issues = await octokit.paginate(octokit.rest.issues.listForRepo, {
57
+ owner,
58
+ repo: repoName,
59
+ state: 'open',
60
+ per_page: 100,
61
+ });
62
+
63
+ for (const issue of issues) {
64
+ if (issue.pull_request) continue;
65
+
66
+ const title = issue.title.trim();
67
+ const body = issue.body || '';
68
+ const labels = new Set(issue.labels.map(l => typeof l === 'string' ? l : l.name));
69
+
70
+ const priority = Object.keys(PRIORITY_LABELS).find(p => body.includes(`Priority: ${p}`));
71
+ const group = Object.keys(GROUP_LABELS).find(g => body.includes(`Group: ${g}`));
72
+
73
+ const expectedLabels = [
74
+ ...(priority ? [PRIORITY_LABELS[priority]] : []),
75
+ ...(group ? [GROUP_LABELS[group]] : [])
76
+ ];
77
+
78
+ const currentRoadmap = roadmapTitles.has(title);
79
+
80
+ // Update Labels
81
+ const labelsToAdd = expectedLabels.filter(label => !labels.has(label));
82
+ const labelsToRemove = [...labels].filter(label =>
83
+ Object.values(PRIORITY_LABELS).includes(label) ||
84
+ Object.values(GROUP_LABELS).includes(label)
85
+ ).filter(label => !expectedLabels.includes(label));
86
+
87
+ if (labelsToAdd.length > 0 || labelsToRemove.length > 0) {
88
+ console.log(`🔁 Updating labels for issue #${issue.number}: ${title}`);
89
+ if (labelsToAdd.length) {
90
+ await octokit.rest.issues.addLabels({
91
+ owner,
92
+ repo: repoName,
93
+ issue_number: issue.number,
94
+ labels: labelsToAdd,
95
+ });
96
+ }
97
+ if (labelsToRemove.length) {
98
+ for (const label of labelsToRemove) {
99
+ await octokit.rest.issues.removeLabel({
100
+ owner,
101
+ repo: repoName,
102
+ issue_number: issue.number,
103
+ name: label,
104
+ });
105
+ }
106
+ }
107
+ }
108
+
109
+ // Optionally Close stale issues
110
+ if (CLOSE_STALE && !currentRoadmap) {
111
+ console.log(`🧹 Closing stale issue #${issue.number} (${title}) – not in roadmap`);
112
+ await octokit.rest.issues.update({
113
+ owner,
114
+ repo: repoName,
115
+ issue_number: issue.number,
116
+ state: 'closed',
117
+ });
118
+ }
119
+ }
120
+ }
121
+
122
+ run().catch(err => {
123
+ console.error('❌ Label script failed:', err);
124
+ process.exit(1);
125
+ });