/
s.homyakov
/
dev2harness
Обзор
Документация
Войти
/
s.homyakov
/
dev2harness
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
src/courseData.js
174 строки
6 KB
Sergey Homyakov
Drop the lesson map step from the lesson flow
06 авг 2026, 20:41
06 авг 2026, 20:41
659dcc5
Код
Авторство
О чём код?
import catalog from "../lessons/catalog.json"; const markdownFiles = import.meta.glob("../lessons/**/*.md", { eager: true, import: "default", query: "?raw" }); const levelColors = ["var(--accent)", "var(--cyan)", "var(--green)", "var(--yellow)", "var(--pink)"]; const materialSections = [ { key: "material", file: "lesson.md", label: "Материал", title: "Разбор темы" }, { key: "practice", file: "exercise.md", label: "Практика", title: "Практическая работа" }, { key: "checks", file: "checks.md", label: "Проверки", title: "Критерии готовности" } ]; function readMaterial(moduleId, lessonId, file) { return markdownFiles[`../lessons/${moduleId}/${lessonId}/${file}`] || ""; } function estimateDuration(materials) { const words = Object.values(materials).join(" ").split(/\s+/).filter(Boolean).length; return Math.max(25, Math.min(75, Math.round((words / 120 + 18) / 5) * 5)); } function makeSubtitle(lesson) { return lesson.goal || lesson.practice || lesson.artifact || "Практический шаг курса OpenCode."; } export const levels = catalog.modules.map((module, index) => ({ id: module.id, icon: String(index + 1).padStart(2, "0"), title: module.title, subtitle: module.outcome, color: levelColors[index % levelColors.length] })); export const lessons = catalog.modules.flatMap((module, moduleIndex) => module.lessons.map((lesson, lessonIndex) => { const materials = Object.fromEntries( materialSections.map((section) => [ section.key, readMaterial(module.id, lesson.id, section.file) ]) ); return { ...lesson, level: module.id, levelTitle: module.title, moduleOutcome: module.outcome, number: moduleIndex * 10 + lessonIndex + 1, subtitle: makeSubtitle(lesson), duration: estimateDuration(materials), kind: "opencode-workflow", materials }; }) ); export const lessonMap = Object.fromEntries(lessons.map((lesson) => [lesson.id, lesson])); export function lessonsForLevel(levelId) { return lessons.filter((lesson) => lesson.level === levelId); } // Справочные материалы курса (файлы в корне lessons/). Если файл пока не // добавлен, строка остаётся пустой, и UI показывает аккуратное сообщение. export const glossary = markdownFiles["../lessons/GLOSSARY.md"] || ""; export const cheatsheet = markdownFiles["../lessons/CHEATSHEET.md"] || ""; export const rubric = markdownFiles["../lessons/RUBRIC.md"] || ""; // Маршруты курса: наборы номеров уроков (lesson.number, 01..21). export const courseRoutes = [ { id: "fast", label: "Быстрый", lessonNumbers: [1, 4, 8, 10, 16, 21] }, { id: "base", label: "Базовый", lessonNumbers: Array.from({ length: 21 }, (_, index) => index + 1) }, { id: "team", label: "Team leads", lessonNumbers: [4, 6, 7, 8, 11, 12, 14, 15, 17, 19, 20, 21] } ]; export function lessonIdsForRoute(routeId) { const route = courseRoutes.find((item) => item.id === routeId); if (!route) return []; const numbers = new Set(route.lessonNumbers); return lessons .filter((lesson) => numbers.has(lesson.number)) .map((lesson) => lesson.id); } export function buildLessonSteps(lesson) { const markdownSteps = materialSections .map((section) => ({ id: section.key, type: "markdown", label: section.label, title: section.title, content: lesson.materials?.[section.key] || "", source: section.file })) .filter((step) => step.content.trim()); const steps = [ ...markdownSteps, { id: "reflection", type: "reflection", label: "Рефлексия", title: "Зафиксируйте рабочий вывод", prompt: "Коротко запишите: что теперь можно поручить OpenCode, где нужна ручная проверка и какой артефакт должен остаться после такой сессии." } ]; if (lesson.id === "21-final-project") { steps.push({ id: "rubric", type: "rubric", label: "Рубрика", title: "Рубрика зрелости", prompt: "Оцените собранный harness по шкале Уровень 1–5. Отмечайте «дотянул» только для уровней, критерии которых реально выполнены: уровень определяется по самому слабому критерию.", content: rubric }); } return steps; } export const seedComments = [ { id: "c1", lessonId: "10-opencode-json", author: "Mira", text: "Самая полезная привычка: держать share policy и permissions прямо в проектном config, а не помнить их в голове.", votes: 11, createdAt: "2026-05-25" }, { id: "c2", lessonId: "07-subagents", author: "Anton", text: "Для subagents хорошо работает правило: один агент, один артефакт, один понятный boundary.", votes: 8, createdAt: "2026-05-25" }, { id: "c3", lessonId: "05-context-window-and-handoffs", author: "Lena", text: "Handoff начинает окупаться, когда в нем есть не transcript, а решения, риски и следующий безопасный шаг.", votes: 6, createdAt: "2026-05-25" } ]; export const seedHelpRequests = [ { id: "h1", lessonId: "08-permission-model", author: "Den", text: "Пытаюсь разделить allow/ask/deny для bash-команд. Где лучше поставить границу для test-команд?", status: "open" }, { id: "h2", lessonId: "14-mcp-basics", author: "Sasha", text: "Не понимаю, как описать MCP server disabled-by-default так, чтобы команда могла безопасно его включать.", status: "open" } ];