Skip to content

Commit baeed61

Browse files
authored
Merge pull request #1 from spivx/dev/add-ut-pr-githubaction
feat: implement data ID validation and refactor validation script
2 parents 5d9e488 + 82c6d57 commit baeed61

File tree

6 files changed

+143
-114
lines changed

6 files changed

+143
-114
lines changed

.husky/pre-commit

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { scanDataIds } from '../data-id-validator'
2+
3+
describe('data ID validation', () => {
4+
test('ensures IDs across data files remain unique', () => {
5+
const { duplicates } = scanDataIds()
6+
7+
if (duplicates.length > 0) {
8+
const summary = duplicates
9+
.map(({ id, files }) => `- ${id}: ${files.join(', ')}`)
10+
.join('\n')
11+
12+
throw new Error(`Duplicate IDs detected:\n${summary}`)
13+
}
14+
})
15+
})
16+

lib/data-id-validator.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import fs from 'fs'
2+
import path from 'path'
3+
4+
export type DuplicateIdRecord = {
5+
id: string
6+
files: string[]
7+
}
8+
9+
export type IdScanResult = {
10+
totalIds: number
11+
duplicates: DuplicateIdRecord[]
12+
}
13+
14+
export function scanDataIds(dataDir = path.join(process.cwd(), 'data')): IdScanResult {
15+
const jsonFiles = collectJsonFiles(dataDir)
16+
const idOrigins = new Map<string, string>()
17+
const duplicateSources = new Map<string, Set<string>>()
18+
19+
for (const filePath of jsonFiles) {
20+
const relativePath = path.relative(dataDir, filePath)
21+
const content = fs.readFileSync(filePath, 'utf8')
22+
let parsedContent: unknown
23+
24+
try {
25+
parsedContent = JSON.parse(content)
26+
} catch (error) {
27+
throw new Error(`Failed to parse ${relativePath}: ${(error as Error).message}`)
28+
}
29+
30+
collectIds(parsedContent, relativePath, idOrigins, duplicateSources)
31+
}
32+
33+
const duplicates: DuplicateIdRecord[] = Array.from(duplicateSources.entries()).map(([id, files]) => ({
34+
id,
35+
files: Array.from(files),
36+
}))
37+
38+
duplicates.sort((a, b) => a.id.localeCompare(b.id))
39+
40+
return {
41+
totalIds: idOrigins.size,
42+
duplicates,
43+
}
44+
}
45+
46+
function collectJsonFiles(targetDir: string): string[] {
47+
const directoryEntries = fs.readdirSync(targetDir, { withFileTypes: true })
48+
const collected: string[] = []
49+
50+
for (const entry of directoryEntries) {
51+
const entryPath = path.join(targetDir, entry.name)
52+
53+
if (entry.isDirectory()) {
54+
collected.push(...collectJsonFiles(entryPath))
55+
} else if (entry.isFile() && entry.name.endsWith('.json')) {
56+
collected.push(entryPath)
57+
}
58+
}
59+
60+
return collected
61+
}
62+
63+
function collectIds(
64+
node: unknown,
65+
filePath: string,
66+
idOrigins: Map<string, string>,
67+
duplicateSources: Map<string, Set<string>>
68+
) {
69+
if (Array.isArray(node)) {
70+
for (const value of node) {
71+
collectIds(value, filePath, idOrigins, duplicateSources)
72+
}
73+
return
74+
}
75+
76+
if (node === null || typeof node !== 'object') {
77+
return
78+
}
79+
80+
const record = node as Record<string, unknown>
81+
82+
if (typeof record.id === 'string' && record.id.trim().length > 0) {
83+
registerId(record.id, filePath, idOrigins, duplicateSources)
84+
}
85+
86+
for (const value of Object.values(record)) {
87+
collectIds(value, filePath, idOrigins, duplicateSources)
88+
}
89+
}
90+
91+
function registerId(
92+
id: string,
93+
filePath: string,
94+
idOrigins: Map<string, string>,
95+
duplicateSources: Map<string, Set<string>>
96+
) {
97+
const trimmedId = id.trim()
98+
99+
if (trimmedId.length === 0) {
100+
return
101+
}
102+
103+
if (!idOrigins.has(trimmedId)) {
104+
idOrigins.set(trimmedId, filePath)
105+
return
106+
}
107+
108+
const origin = idOrigins.get(trimmedId) as string
109+
const sources = duplicateSources.get(trimmedId) ?? new Set<string>([origin])
110+
sources.add(filePath)
111+
duplicateSources.set(trimmedId, sources)
112+
}

package-lock.json

Lines changed: 0 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@
1010
"test": "vitest",
1111
"test:ui": "vitest --ui",
1212
"test:run": "vitest run",
13-
"validate:ids": "tsx scripts/validate-question-ids.ts",
14-
"prepare": "husky"
13+
"validate:ids": "tsx scripts/validate-question-ids.ts"
1514
},
1615
"dependencies": {
1716
"@radix-ui/react-slot": "^1.2.3",
@@ -39,7 +38,6 @@
3938
"eslint-config-next": "15.5.3",
4039
"eslint-config-prettier": "^10.1.8",
4140
"eslint-plugin-prettier": "^5.5.4",
42-
"husky": "^9.1.7",
4341
"jsdom": "^27.0.0",
4442
"prettier": "^3.6.2",
4543
"tailwindcss": "^4",
@@ -48,4 +46,4 @@
4846
"typescript": "^5",
4947
"vitest": "^3.2.4"
5048
}
51-
}
49+
}

scripts/validate-question-ids.ts

Lines changed: 13 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,25 @@
1-
import fs from 'fs';
2-
import path from 'path';
1+
import path from 'path'
32

4-
interface Question {
5-
id: string;
6-
question: string;
7-
answers: Array<{
8-
value: string;
9-
label: string;
10-
example?: string;
11-
docs?: string;
12-
}>;
13-
}
14-
15-
function validateQuestionIds() {
16-
const dataDir = path.join(process.cwd(), 'data');
17-
const questionsDir = path.join(dataDir, 'questions');
18-
19-
const allIds = new Set<string>();
20-
const duplicates: string[] = [];
21-
const fileIdMap = new Map<string, string>();
3+
import { scanDataIds } from '../lib/data-id-validator'
224

23-
// Check main data files
24-
const mainFiles = [
25-
'general.json',
26-
'architecture.json',
27-
'performance.json',
28-
'security.json',
29-
'commits.json',
30-
'files.json'
31-
];
32-
33-
for (const file of mainFiles) {
34-
const filePath = path.join(dataDir, file);
35-
if (fs.existsSync(filePath)) {
36-
checkFileForDuplicates(filePath, file, allIds, duplicates, fileIdMap);
37-
}
38-
}
39-
40-
// Check framework-specific question files
41-
if (fs.existsSync(questionsDir)) {
42-
const questionFiles = fs.readdirSync(questionsDir)
43-
.filter(file => file.endsWith('.json'));
44-
45-
for (const file of questionFiles) {
46-
const filePath = path.join(questionsDir, file);
47-
checkFileForDuplicates(filePath, `questions/${file}`, allIds, duplicates, fileIdMap);
48-
}
49-
}
5+
export function validateQuestionIds() {
6+
const dataDir = path.join(process.cwd(), 'data')
7+
const { duplicates, totalIds } = scanDataIds(dataDir)
508

519
if (duplicates.length > 0) {
52-
console.error('❌ Duplicate question IDs found:');
53-
duplicates.forEach(id => {
54-
console.error(` - "${id}" in ${fileIdMap.get(id)}`);
55-
});
56-
process.exit(1);
57-
}
58-
59-
console.log('✅ All question IDs are unique across all files');
60-
console.log(`📊 Total unique question IDs: ${allIds.size}`);
61-
}
10+
console.error('❌ Duplicate IDs found across data files:')
6211

63-
function checkFileForDuplicates(
64-
filePath: string,
65-
fileName: string,
66-
allIds: Set<string>,
67-
duplicates: string[],
68-
fileIdMap: Map<string, string>
69-
) {
70-
try {
71-
const content = fs.readFileSync(filePath, 'utf-8');
72-
const questions: Question[] = JSON.parse(content);
73-
74-
if (!Array.isArray(questions)) {
75-
console.warn(`⚠️ Skipping ${fileName}: not an array of questions`);
76-
return;
12+
for (const { id, files } of duplicates) {
13+
console.error(` - "${id}" in ${files.join(', ')}`)
7714
}
7815

79-
questions.forEach((question, index) => {
80-
if (!question.id) {
81-
console.warn(`⚠️ Question at index ${index} in ${fileName} has no ID`);
82-
return;
83-
}
84-
85-
if (allIds.has(question.id)) {
86-
duplicates.push(question.id);
87-
const existingFile = fileIdMap.get(question.id);
88-
fileIdMap.set(question.id, `${existingFile} and ${fileName}`);
89-
} else {
90-
allIds.add(question.id);
91-
fileIdMap.set(question.id, fileName);
92-
}
93-
});
94-
} catch (error) {
95-
console.error(`❌ Error reading ${fileName}:`, error);
96-
process.exit(1);
16+
process.exit(1)
9717
}
18+
19+
console.log('✅ All IDs are unique across data files')
20+
console.log(`📊 Total unique IDs: ${totalIds}`)
9821
}
9922

10023
if (require.main === module) {
101-
validateQuestionIds();
24+
validateQuestionIds()
10225
}
103-
104-
export { validateQuestionIds };

0 commit comments

Comments
 (0)