Major repository reorganization for improved navigation: New directory structure: - guide/ - Core documentation (ultimate-guide, cheatsheet, adoption) - tools/ - Interactive utilities (audit, onboarding, mobile-access) - machine-readable/ - LLM/AI consumption (reference.yaml, llms.txt) - exports/ - Generated outputs (PDFs) Changes: - Move 10 files to thematic directories with cleaner names - Create README.md index for each new directory - Update 150+ internal links across all documentation - Add "Repository Structure" section to main README - Remove redundant npm install command from README header - Remove unverified cost estimate from prerequisites - Fix broken anchor link (#-quick-start-15-minutes) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
124 lines
3.7 KiB
JavaScript
124 lines
3.7 KiB
JavaScript
/**
|
|
* Question loading and filtering
|
|
*/
|
|
|
|
import { readdir, readFile } from 'fs/promises';
|
|
import { join } from 'path';
|
|
import YAML from 'yaml';
|
|
|
|
/**
|
|
* Load all questions from YAML files in the questions directory
|
|
*/
|
|
export async function loadQuestions(questionsDir) {
|
|
const allQuestions = [];
|
|
|
|
try {
|
|
const files = await readdir(questionsDir);
|
|
const yamlFiles = files.filter(f => f.endsWith('.yaml') || f.endsWith('.yml'));
|
|
|
|
for (const file of yamlFiles) {
|
|
try {
|
|
const content = await readFile(join(questionsDir, file), 'utf-8');
|
|
const parsed = YAML.parse(content);
|
|
|
|
if (parsed && parsed.questions && Array.isArray(parsed.questions)) {
|
|
// Add category info to each question
|
|
const questionsWithCategory = parsed.questions.map(q => ({
|
|
...q,
|
|
category: parsed.category || 'Unknown',
|
|
category_id: parsed.category_id || 0,
|
|
source_file: parsed.source_file || 'guide/ultimate-guide.md'
|
|
}));
|
|
allQuestions.push(...questionsWithCategory);
|
|
}
|
|
} catch (err) {
|
|
console.warn(`Warning: Could not parse ${file}: ${err.message}`);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error(`Error reading questions directory: ${err.message}`);
|
|
}
|
|
|
|
return allQuestions;
|
|
}
|
|
|
|
/**
|
|
* Filter questions based on profile, topics, and difficulty
|
|
*/
|
|
export function filterQuestions(questions, options) {
|
|
const { profile, topics, count, difficultyWeight } = options;
|
|
|
|
// Filter by topics
|
|
let filtered = questions.filter(q => topics.includes(q.category_id));
|
|
|
|
// Filter by profile (if profiles array exists on question)
|
|
filtered = filtered.filter(q => {
|
|
if (!q.profiles || !Array.isArray(q.profiles)) return true;
|
|
return q.profiles.includes(profile);
|
|
});
|
|
|
|
// Weight by difficulty
|
|
if (difficultyWeight) {
|
|
filtered = weightByDifficulty(filtered, difficultyWeight, count);
|
|
}
|
|
|
|
return filtered;
|
|
}
|
|
|
|
/**
|
|
* Weight questions by difficulty distribution
|
|
*/
|
|
function weightByDifficulty(questions, weights, targetCount) {
|
|
const byDifficulty = {
|
|
junior: questions.filter(q => q.difficulty === 'junior'),
|
|
senior: questions.filter(q => q.difficulty === 'senior'),
|
|
power: questions.filter(q => q.difficulty === 'power'),
|
|
all: questions.filter(q => q.difficulty === 'all' || !q.difficulty)
|
|
};
|
|
|
|
const result = [];
|
|
|
|
// Add 'all' difficulty questions first
|
|
result.push(...byDifficulty.all);
|
|
|
|
// Calculate target counts for each difficulty
|
|
const remaining = targetCount - result.length;
|
|
const juniorCount = Math.floor(remaining * (weights.junior || 0));
|
|
const seniorCount = Math.floor(remaining * (weights.senior || 0));
|
|
const powerCount = Math.floor(remaining * (weights.power || 0));
|
|
|
|
// Add questions by difficulty
|
|
result.push(...shuffleArray(byDifficulty.junior).slice(0, juniorCount));
|
|
result.push(...shuffleArray(byDifficulty.senior).slice(0, seniorCount));
|
|
result.push(...shuffleArray(byDifficulty.power).slice(0, powerCount));
|
|
|
|
// Fill remaining slots with any available questions
|
|
const used = new Set(result.map(q => q.id));
|
|
const unused = questions.filter(q => !used.has(q.id));
|
|
const stillNeeded = targetCount - result.length;
|
|
|
|
if (stillNeeded > 0) {
|
|
result.push(...shuffleArray(unused).slice(0, stillNeeded));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Shuffle array using Fisher-Yates algorithm
|
|
*/
|
|
export function shuffleArray(array) {
|
|
const result = [...array];
|
|
for (let i = result.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[result[i], result[j]] = [result[j], result[i]];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Get category name from ID
|
|
*/
|
|
export function getCategoryName(categoryId, topics) {
|
|
return topics[categoryId] || 'Unknown';
|
|
}
|