dsh-plugin-lookatstudy 0.8.1

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 @@
1
+ {"version":3,"file":"index.mjs","names":["cards.importLines","cards.mapLines","cards.answerLine","cards.completeLines","cards.dueLines","cards.reviewLine"],"sources":["../src/config.ts","../src/markdown.ts","../src/vendor/sm2.ts","../src/vendor/bkt.ts","../src/state.ts","../src/dashboard.ts","../src/vendor/markdown-course.ts","../src/vendor/local-folder-scanner.ts","../src/vendor/file-classifier.ts","../src/vendor/repo-fetcher.ts","../src/cards.ts","../src/tools.ts","../src/index.ts"],"sourcesContent":["/**\n * Plugin configuration schema (Schemastery) for dsh-plugin-lookatstudy.\n * @module dsh-plugin-lookatstudy/config\n */\n\nimport z from '@deepseek-ai/schemastery'\n\n/** Tutoring soul injected into the system prompt (LookatStudy's three builtin souls). */\nexport type TutorMode = 'direct' | 'guide' | 'practice'\n\n/** Resolved plugin configuration. */\nexport interface Config {\n /**\n * Initial tutoring soul: `guide` asks questions and hands over steps\n * (default), `direct` explains first then verifies, `practice` teaches\n * inside real messy problems. The learner can switch at any time and the\n * choice persists in the learning state.\n */\n mode: TutorMode\n /**\n * Absolute path of the JSON learning-state file. Empty resolves to\n * `$DSH_HOME/lookatstudy-plugin/state.json` (`~/.dsh` when `DSH_HOME` is unset).\n */\n statePath: string\n}\n\n/** Schemastery configuration validated at plugin load. */\nexport const Config: z<Config> = z.object({\n mode: z.union(['direct', 'guide', 'practice'] as const).default('guide'),\n statePath: z.string().default(''),\n})\n","/**\n * Server-side markdown → HTML for the study workbench's 讲解 view. Escapes\n * every HTML character first, then renders a pragmatic GFM subset (headings,\n * fenced code, inline code, bold/italic, links, lists, blockquotes, tables,\n * hr, paragraphs). Lesson bodies are imported teaching material, so raw HTML\n * never passes through.\n * @module dsh-plugin-lookatstudy/markdown\n */\n\n/** Escape all HTML-significant characters. */\nfunction escapeHtml(text: string): string {\n return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\"/g, '&quot;')\n}\n\n/** Render inline markup (code, bold, italic, links) over escaped text. */\nfunction inline(escaped: string): string {\n return escaped\n .replace(/`([^`]+)`/g, '<code>$1</code>')\n .replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>')\n .replace(/\\*([^*]+)\\*/g, '<em>$1</em>')\n .replace(/\\[([^\\]]+)\\]\\((https?:\\/\\/[^)\\s]+)\\)/g, '<a href=\"$2\" target=\"_blank\" rel=\"noreferrer\">$1</a>')\n}\n\n/** True when the line opens a GFM table row (pipes with a delimiter row next). */\nfunction isTableRow(line: string): boolean {\n return line.trim().startsWith('|') && line.trim().endsWith('|') && line.includes('|', 1)\n}\n\nfunction isDelimiterRow(line: string): boolean {\n const trimmed = line.trim()\n if (!trimmed.startsWith('|') || !trimmed.endsWith('|')) return false\n const cells = trimmed.replace(/^\\|/, '').replace(/\\|$/, '').split('|').map(c => c.trim())\n return cells.length > 0 && cells.every(c => /^:?-+:?$/.test(c))\n}\n\n/** Split a table row into trimmed cells (leading/trailing pipes removed). */\nfunction rowCells(line: string): string[] {\n return line.trim().replace(/^\\|/, '').replace(/\\|$/, '').split('|').map(c => c.trim())\n}\n\n/**\n * Render markdown text to sanitized HTML.\n * @param md - markdown source (lesson body).\n * @returns HTML string safe to inject into the page.\n */\nexport function renderMarkdown(md: string): string {\n const lines = escapeHtml(md).split(/\\r?\\n/)\n const out: string[] = []\n let i = 0\n\n const flushParagraph = (buffer: string[]): void => {\n if (buffer.length > 0) out.push(`<p>${inline(buffer.join(' '))}</p>`)\n }\n\n let paragraph: string[] = []\n while (i < lines.length) {\n const line = lines[i]!\n\n // Fenced code block\n const fence = /^```(\\w*)\\s*$/.exec(line.trim())\n if (fence) {\n flushParagraph(paragraph)\n paragraph = []\n const lang = fence[1] ?? ''\n const code: string[] = []\n i++\n while (i < lines.length && lines[i]!.trim() !== '```') {\n code.push(lines[i]!)\n i++\n }\n i++ // closing fence\n out.push(`<pre><code${lang === '' ? '' : ` class=\"lang-${lang}\"`}>${code.join('\\n')}</code></pre>`)\n continue\n }\n\n // Heading\n const heading = /^(#{1,6})\\s+(.*)$/.exec(line)\n if (heading) {\n flushParagraph(paragraph)\n paragraph = []\n const level = heading[1]!.length\n out.push(`<h${level}>${inline(heading[2]!)}</h${level}>`)\n i++\n continue\n }\n\n // Horizontal rule\n if (/^\\s*(-{3,}|\\*{3,}|_{3,})\\s*$/.test(line)) {\n flushParagraph(paragraph)\n paragraph = []\n out.push('<hr>')\n i++\n continue\n }\n\n // Blockquote\n if (/^\\s*&gt;\\s?/.test(line)) {\n flushParagraph(paragraph)\n paragraph = []\n const quote: string[] = []\n while (i < lines.length && /^\\s*&gt;\\s?/.test(lines[i]!)) {\n quote.push(lines[i]!.replace(/^\\s*&gt;\\s?/, ''))\n i++\n }\n out.push(`<blockquote><p>${inline(quote.join(' '))}</p></blockquote>`)\n continue\n }\n\n // Unordered list\n if (/^\\s*[-*+]\\s+/.test(line)) {\n flushParagraph(paragraph)\n paragraph = []\n const items: string[] = []\n while (i < lines.length && /^\\s*[-*+]\\s+/.test(lines[i]!)) {\n items.push(`<li>${inline(lines[i]!.replace(/^\\s*[-*+]\\s+/, ''))}</li>`)\n i++\n }\n out.push(`<ul>${items.join('')}</ul>`)\n continue\n }\n\n // Ordered list\n if (/^\\s*\\d+\\.\\s+/.test(line)) {\n flushParagraph(paragraph)\n paragraph = []\n const items: string[] = []\n while (i < lines.length && /^\\s*\\d+\\.\\s+/.test(lines[i]!)) {\n items.push(`<li>${inline(lines[i]!.replace(/^\\s*\\d+\\.\\s+/, ''))}</li>`)\n i++\n }\n out.push(`<ol>${items.join('')}</ol>`)\n continue\n }\n\n // GFM table\n if (isTableRow(line) && i + 1 < lines.length && isDelimiterRow(lines[i + 1]!)) {\n flushParagraph(paragraph)\n paragraph = []\n const headers = rowCells(line)\n i += 2\n const rows: string[] = []\n while (i < lines.length && isTableRow(lines[i]!)) {\n const cells = rowCells(lines[i]!)\n rows.push(`<tr>${cells.map(c => `<td>${inline(c)}</td>`).join('')}</tr>`)\n i++\n }\n out.push(`<table><thead><tr>${headers.map(h => `<th>${inline(h)}</th>`).join('')}</tr></thead><tbody>${rows.join('')}</tbody></table>`)\n continue\n }\n\n // Blank line ends the paragraph\n if (line.trim() === '') {\n flushParagraph(paragraph)\n paragraph = []\n i++\n continue\n }\n\n paragraph.push(line.trim())\n i++\n }\n flushParagraph(paragraph)\n return out.join('\\n')\n}\n","// Vendored from LookatStudy src/main/services/pure/sm2.ts (MIT License, https://github.com/kaiji/LookatStudy).\n// Unmodified except this provenance header. PDF/PPTX branches resolve unavailable optional libs and are skipped per upstream try/catch.\n/**\n * SM-2 间隔重复算法 —— 纯函数,零依赖(不 import DB / electron / @shared)。\n *\n * 为什么单独一个 pure/ 文件:\n * 测试(scripts/verify-srs.mjs)需要 import 真实源码而非副本(VERIFICATION §3.1),\n * 但 srs.ts 顶层 import electron + DB,纯 Node 环境加载即崩。\n * 把纯算法抽到这里,srs.ts re-export,测试只 import 这个文件 —— 既能测真实源码,又不引入运行时副作用。\n *\n * 算法参考:https://www.supermemo.com/en/blog/application-of-a-computer-to-improve-the-results-obtained-in-working-with-the-supermemo-method\n *\n * quality: 0-5\n * 0-2: 答错,重置 repetitions=0,interval=1\n * 3: 勉强对\n * 4-5: 答对,推进 repetitions\n * easeFactor: 1.3 ~ 3.0,初始 2.5\n */\n\nexport type ReviewQuality = 0 | 1 | 2 | 3 | 4 | 5;\n\nexport interface Sm2State {\n easeFactor: number; // 1.3 ~ 3.0\n intervalDays: number;\n repetitions: number;\n}\n\nexport interface Sm2Result extends Sm2State {\n dueAt: string; // ISO date\n}\n\nexport function computeSm2(\n prev: Sm2State,\n quality: ReviewQuality,\n now: Date = new Date(),\n): Sm2Result {\n let { easeFactor, intervalDays, repetitions } = prev;\n\n if (quality < 3) {\n // 答错:重置\n repetitions = 0;\n intervalDays = 1;\n } else {\n // 答对:推进\n repetitions += 1;\n if (repetitions === 1) {\n intervalDays = 1;\n } else if (repetitions === 2) {\n intervalDays = 6;\n } else {\n intervalDays = Math.round(intervalDays * easeFactor);\n }\n }\n\n // 更新 EF:EF' = EF + (0.1 - (5 - q) * (0.08 + (5 - q) * 0.02))\n const q = quality;\n const delta = 0.1 - (5 - q) * (0.08 + (5 - q) * 0.02);\n easeFactor = Math.max(1.3, Math.min(3.0, easeFactor + delta));\n\n const dueAt = new Date(now.getTime() + intervalDays * 24 * 60 * 60 * 1000);\n\n return { easeFactor, intervalDays, repetitions, dueAt: dueAt.toISOString() };\n}\n","// Vendored from LookatStudy src/main/services/pure/bkt.ts (MIT License, https://github.com/kaiji/LookatStudy).\n// Unmodified except this provenance header. PDF/PPTX branches resolve unavailable optional libs and are skipped per upstream try/catch.\n/**\r\n * Bayesian Knowledge Tracing (BKT) —— 掌握度概率的贝叶斯更新。\r\n *\r\n * 这是差异化护城河之一(多数 AI 家教停留在检索增强,无显式掌握度建模)。\r\n * 经典 BKT 四参数(文献默认,R1 风险项已定):\r\n * P(L0) 初始已掌握概率 = 0.5 (无先验信息时五五开)\r\n * P(T) 每次学习后 未掌握→掌握 = 0.1 (单次学习能转化的概率)\r\n * P(S) 已掌握但答错(slip) = 0.1\r\n * P(G) 未掌握但答对(guess) = 0.2\r\n *\r\n * 更新公式(观察到观测 correct 后):\r\n * 先按当前 P(L) 算\"答对/答错的似然\"\r\n * 后验 P(L|obs) = P(obs|L)·P(L) / P(obs)\r\n * 再乘 (1 + P(T)·(1-P(L))/P(L)) 做\"学习迁移\"\r\n *\r\n * 纯函数零依赖,测试直接 import 真实源码(VERIFICATION §3.1)。\r\n */\r\n\r\nexport interface BktParams {\r\n /** 初始掌握概率 [0,1] */\r\n pInit: number;\r\n /** transit 未掌握→掌握 [0,1] */\r\n pTransit: number;\r\n /** slip 已掌握答错 [0,1] */\r\n pSlip: number;\r\n /** guess 未掌握答对 [0,1] */\r\n pGuess: number;\r\n}\r\n\r\n/** 文献默认参数(ROADMAP R1:先验用文献默认,数据多后再调) */\r\nexport const BKT_DEFAULTS: BktParams = {\r\n pInit: 0.5,\r\n pTransit: 0.1,\r\n pSlip: 0.1,\r\n pGuess: 0.2,\r\n};\r\n\r\nconst clamp01 = (x: number): number => Math.max(0, Math.min(1, x));\r\n\r\n/**\r\n * 单次观测后的掌握度更新。\r\n *\r\n * @param prev 更新前的 P(L)。null/undefined → 用 params.pInit\r\n * @param correct 这次观测是否答对\r\n * @param params BKT 四参数(默认文献值)\r\n * @returns 新的 P(L),已 clamp 到 [0,1]\r\n */\r\nexport function updateMastery(\r\n prev: number | null | undefined,\r\n correct: boolean,\r\n params: BktParams = BKT_DEFAULTS,\r\n): number {\r\n const pL = clamp01(prev ?? params.pInit);\r\n const { pTransit, pSlip, pGuess } = params;\r\n\r\n // 1. 后验(不做 transit):P(L|obs)\r\n // P(obs=correct | L) = 1 - P(slip)\r\n // P(obs=wrong | L) = P(slip)\r\n // P(obs=correct | ¬L) = P(guess)\r\n // P(obs=wrong | ¬L) = 1 - P(guess)\r\n const pObsGivenL = correct ? 1 - pSlip : pSlip;\r\n const pObsGivenNotL = correct ? pGuess : 1 - pGuess;\r\n const pObs = pObsGivenL * pL + pObsGivenNotL * (1 - pL);\r\n if (pObs === 0) return pL; // 数值退化兜底\r\n const pLGivenObs = (pObsGivenL * pL) / pObs;\r\n\r\n // 2. 学习迁移:这次观测后,未掌握者可能 transit 到掌握\r\n // P(L)' = P(L|obs) + P(T)·(1 - P(L|obs))\r\n const pLAfterTransit = pLGivenObs + pTransit * (1 - pLGivenObs);\r\n\r\n return clamp01(pLAfterTransit);\r\n}\r\n\r\n/**\r\n * 把 mastery 概率映射成 crown level(1-5)给 UI 用。\r\n * < 0.3 → 1, <0.5 → 2, <0.7 → 3, <0.9 → 4, ≥0.9 → 5。null → 0。\r\n */\r\nexport function masteryToCrown(mastery: number | null | undefined): number {\r\n if (mastery == null) return 0;\r\n if (mastery < 0.3) return 1;\r\n if (mastery < 0.5) return 2;\r\n if (mastery < 0.7) return 3;\r\n if (mastery < 0.9) return 4;\r\n return 5;\r\n}\r\n","/**\n * Learning state: courses → sections → lessons with mastery-driven gating,\n * per-concept (KC) BKT tracking aggregated as the weakest concept, SM-2\n * spaced repetition, pending mastery proposals, friction log, learner\n * memory, and Cornell-style notes. Persisted as one JSON file; every\n * mutation is saved synchronously.\n * @module dsh-plugin-lookatstudy/state\n */\n\nimport { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport { homedir } from 'node:os'\nimport { randomBytes } from 'node:crypto'\nimport { computeSm2, type ReviewQuality, type Sm2State } from './vendor/sm2.ts'\nimport { masteryToCrown, updateMastery } from './vendor/bkt.ts'\nimport type { ParsedCourse } from './vendor/markdown-course.ts'\n\n/** Lesson position on the mastery-gated path (LookatStudy NodeStatus). */\nexport type LessonStatus = 'locked' | 'available' | 'in_progress' | 'mastered'\n\n/** Lesson role: gated teaching material, free practice material (LookatStudy's 实操 world), or a section exam node gated on sibling mastery. */\nexport type LessonKind = 'study' | 'practice' | 'exam'\n\n/** Tutoring persona (soul), switchable at runtime. */\nexport type StudyMode = 'direct' | 'guide' | 'practice'\n\n/** Friction categories the tutor silently logs (ported from LookatStudy). */\nexport type FrictionCategory = 'confused' | 'blocked' | 'frustrated'\n\n/** Memory slot: cross-course style, per-course pattern, or per-lesson note. */\nexport type MemoryCategory = 'global' | 'pattern' | 'lesson'\n\n/** Cornell notebook zone: structures (AI) / learner records / practice log. */\nexport type NoteZone = 'understand' | 'record' | 'practice'\n\n/** Where a note's content came from. */\nexport type NoteSource = 'ai' | 'content' | 'chat'\n\n/** One knowledge component a lesson can be quizzed on independently. */\nexport interface ConceptDef {\n title: string\n description: string\n}\n\n/** One learner-facing note in the notebook zones. */\nexport interface LessonNote {\n id: string\n zone: NoteZone\n title: string\n text: string\n source: NoteSource\n /** Quoted source text the note refers to (record zone), verbatim. */\n quote: string | null\n at: string\n}\n\n/** One logged friction event. */\nexport interface FrictionEntry {\n category: FrictionCategory\n summary: string | null\n at: string\n}\n\n/** A tutor-proposed state change awaiting the learner's decision in chat. */\nexport interface MasteryProposal {\n id: string\n lessonId: string\n rationale: string\n status: 'pending' | 'applied' | 'rejected'\n createdAt: string\n}\n\n/** One lesson: content plus the learner's tracked state. */\nexport interface LessonState {\n /** Stable id of the form `${courseId}:${sectionIndex}:${lessonIndex}`. */\n id: string\n title: string\n anchor: string\n /** Lesson markdown body (verbatim from import). */\n body: string\n /** Teaching node or section exam node (exams gate on sibling mastery in the UI). */\n kind: LessonKind\n status: LessonStatus\n /** Knowledge components defined by the tutor (null until defined). */\n concepts: ConceptDef[] | null\n /** Per-concept BKT P(known), keyed by concept index. */\n conceptMastery: Record<number, number> | null\n /** Lesson-level BKT P(known); equals min(concepts) once KCs exist. */\n mastery: number | null\n attempts: number\n correctCount: number\n lastAnsweredAt: string | null\n completedAt: string | null\n /** SM-2 scheduling state; null until the lesson is completed. */\n sm2: Sm2State | null\n /** Next SM-2 review due time (ISO); null until the lesson is completed. */\n dueAt: string | null\n /** Silent friction log for this lesson (most recent last, capped). */\n friction: FrictionEntry[]\n /** Per-lesson memory slot (\"what specifically is missing here\"). */\n memory: string | null\n /** Cornell notebook entries across the three zones. */\n notes: LessonNote[]\n}\n\n/** A section holding an ordered list of lessons. */\nexport interface SectionState {\n title: string\n anchor: string\n lessons: LessonState[]\n}\n\n/** Where a course came from. */\nexport type CourseSource = 'markdown' | 'folder' | 'github'\n\n/** One imported course. */\nexport interface CourseState {\n id: string\n title: string\n source: CourseSource\n /** Markdown text, folder path, or repo URL the course was imported from. */\n sourceRef: string\n createdAt: string\n sections: SectionState[]\n}\n\n/** Whole persisted state; `version` gates migrations (v1 → v2 renamed completed→mastered and added lesson.kind). */\nexport interface LearningState {\n version: 2\n courses: CourseState[]\n /** Active tutoring soul. */\n mode: StudyMode\n /** Lesson the learner last opened (snapshot focus), or null. */\n focus: { lessonId: string } | null\n /** Cross-course style memory. */\n memoryGlobal: string | null\n /** Per-course friction-pattern memory. */\n memoryPatterns: Record<string, string>\n /** Mastery proposals across courses. */\n proposals: MasteryProposal[]\n /** Lesson id → dsh session id (the simplified thread system: one session per lesson node). */\n lessonSessions: Record<string, string>\n}\n\n/** A lesson located inside its course, for mutation results. */\nexport interface LessonRef {\n course: CourseState\n section: SectionState\n lesson: LessonState\n}\n\nconst DAY_MS = 86_400_000\n/** Mastery at or above this graduates the lesson automatically (LookatStudy MASTERED_MASTERY_THRESHOLD). */\nexport const MASTERED_THRESHOLD = 0.9\n/** Mastery at or above this makes the next lesson available early (LookatStudy UNLOCK_MASTERY_THRESHOLD). */\nexport const UNLOCK_THRESHOLD = 0.5\n/** Mastery near this lets the tutor propose early graduation (LookatStudy NEAR_MASTERED_THRESHOLD). */\nexport const NEAR_MASTERED_THRESHOLD = 0.85\n/** Concepts below this mastery are flagged weak (LookatStudy kcContext). */\nexport const WEAK_CONCEPT_THRESHOLD = 0.7\nconst FRICTION_CAP = 10\n\n/** Fresh empty state for a first run. */\nexport function emptyState(): LearningState {\n return { version: 2, courses: [], mode: 'guide', focus: null, memoryGlobal: null, memoryPatterns: {}, proposals: [], lessonSessions: {} }\n}\n\n/**\n * Resolve the state-file location: explicit config path wins, otherwise\n * `$DSH_HOME ?? ~/.dsh` under a plugin-named subdirectory.\n * @param configured - Config `statePath` (empty means default).\n * @returns absolute state-file path.\n */\nexport function resolveStatePath(configured: string): string {\n if (configured !== '') return configured\n const dshHome = process.env.DSH_HOME ?? join(homedir(), '.dsh')\n return join(dshHome, 'lookatstudy-plugin', 'state.json')\n}\n\n/**\n * Load persisted state; a missing file yields empty state, a corrupt file fails loud.\n * v1 → v2 migration: `completed` lessons become `mastered`, lessons gain `kind`\n * (default `study`). Newer files than this code knows are rejected.\n * @param path - state-file path.\n * @returns the loaded state.\n */\nexport function loadState(path: string): LearningState {\n if (!existsSync(path)) return emptyState()\n let parsed: unknown\n try {\n parsed = JSON.parse(readFileSync(path, 'utf8'))\n } catch (error) {\n throw new Error(`lookatstudy-plugin: state file is not valid JSON: ${path} (${String(error)})`)\n }\n if (typeof parsed !== 'object' || parsed === null || !Array.isArray((parsed as LearningState).courses)) {\n throw new Error(`lookatstudy-plugin: state file has an unexpected shape: ${path}`)\n }\n const raw = parsed as Partial<LearningState> & { version?: number }\n if (raw.version !== undefined && raw.version > 2) {\n throw new Error(`lookatstudy-plugin: state file version ${raw.version} is newer than this plugin supports: ${path}`)\n }\n const courses = raw.courses!.map(course => ({\n ...course,\n sections: course.sections.map(section => ({\n ...section,\n lessons: section.lessons.map(lesson => ({\n ...lesson,\n kind: lesson.kind ?? 'study',\n status: lesson.status === ('completed' as LessonStatus) ? 'mastered' : lesson.status,\n })),\n })),\n }))\n for (const course of courses) {\n // LookatStudy's ensureExamNodesForExistingCourses: courses imported before\n // exam nodes existed gain them at load (appended per section end, so\n // existing lesson ids never move).\n course.sections.forEach((section, si) => {\n const hasExam = section.lessons.some(l => l.kind === 'exam')\n const studyCount = section.lessons.filter(l => l.kind === 'study').length\n if (!hasExam && studyCount >= 2) {\n section.lessons.push({\n ...freshLesson(`${section.title} · 章节测验`, `${section.anchor}#exam`, '', 'exam'),\n id: `${course.id}:${si}:${section.lessons.length}`,\n status: 'available',\n })\n }\n })\n }\n return {\n version: 2,\n courses,\n mode: raw.mode ?? 'guide',\n focus: raw.focus ?? null,\n memoryGlobal: raw.memoryGlobal ?? null,\n memoryPatterns: raw.memoryPatterns ?? {},\n proposals: raw.proposals ?? [],\n lessonSessions: raw.lessonSessions ?? {},\n }\n}\n\n/**\n * Persist state atomically (write a sibling temp file, then rename).\n * @param path - state-file path.\n * @param state - state to persist.\n */\nexport function saveState(path: string, state: LearningState): void {\n mkdirSync(dirname(path), { recursive: true })\n const tmp = `${path}.tmp`\n writeFileSync(tmp, `${JSON.stringify(state, null, 2)}\\n`, 'utf8')\n renameSync(tmp, path)\n}\n\n/**\n * Slugify a course title into an id prefix: lowercase alphanumerics joined by `-`.\n * @param title - course title.\n * @returns slug, at least `course`.\n */\nfunction slugify(title: string): string {\n const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')\n return slug === '' ? 'course' : slug\n}\n\nfunction freshLesson(title: string, anchor: string, body: string, kind: LessonKind = 'study'): LessonState {\n return {\n id: '',\n title,\n anchor,\n body,\n kind,\n status: 'locked',\n concepts: null,\n conceptMastery: null,\n mastery: null,\n attempts: 0,\n correctCount: 0,\n lastAnsweredAt: null,\n completedAt: null,\n sm2: null,\n dueAt: null,\n friction: [],\n memory: null,\n notes: [],\n }\n}\n\n/**\n * Import a parsed course. Idempotent: the id is the title slug, so importing\n * the same source again returns the existing course unchanged (LookatStudy's\n * pasted-markdown contract, applied to every source). Study lessons are gated\n * (first available, rest locked); every study section with ≥2 lessons also\n * gets a 章节测验 exam node (available in state, gated on sibling mastery in\n * the UI — LookatStudy's rule).\n * @param state - state to mutate.\n * @param parsed - course tree from an importer.\n * @param source - import origin.\n * @param sourceRef - markdown/folder/repo reference for display.\n * @returns the imported (or pre-existing) course.\n */\nexport function importCourse(\n state: LearningState,\n parsed: ParsedCourse,\n source: CourseSource,\n sourceRef: string,\n): CourseState {\n const id = slugify(parsed.title)\n const existing = state.courses.find(c => c.id === id)\n if (existing) return existing\n const course: CourseState = {\n id,\n title: parsed.title,\n source,\n sourceRef,\n createdAt: new Date().toISOString(),\n sections: parsed.sections.map(section => {\n const lessons = section.lessons.map(lesson =>\n freshLesson(lesson.title, lesson.anchor, lesson.body, lesson.world === 'practice' ? 'practice' : 'study'))\n if (section.world !== 'practice' && lessons.filter(l => l.kind === 'study').length >= 2) {\n lessons.push(freshLesson(`${section.title} · 章节测验`, `${section.anchor}#exam`, section.examBody ?? '', 'exam'))\n }\n return { title: section.title, anchor: section.anchor, lessons }\n }),\n }\n let first = true\n for (let si = 0; si < course.sections.length; si++) {\n const lessons = course.sections[si]!.lessons\n for (let li = 0; li < lessons.length; li++) {\n const lesson = lessons[li]!\n lesson.id = `${id}:${si}:${li}`\n if (lesson.kind === 'exam' || lesson.kind === 'practice') {\n lesson.status = 'available'\n } else if (first) {\n lesson.status = 'available'\n first = false\n }\n }\n }\n state.courses.push(course)\n return course\n}\n\n/**\n * Drop a course from state; unknown ids fail loud.\n * @param state - state to mutate.\n * @param courseId - course to remove.\n */\nexport function deleteCourse(state: LearningState, courseId: string): void {\n const i = state.courses.findIndex(c => c.id === courseId)\n if (i < 0) throw new Error(`lookatstudy-plugin: unknown course id ${JSON.stringify(courseId)}`)\n state.courses.splice(i, 1)\n state.proposals = state.proposals.filter(p => !p.lessonId.startsWith(`${courseId}:`))\n}\n\n/**\n * Locate a course; unknown ids fail loud.\n * @param state - state to search.\n * @param courseId - course id.\n * @returns the course.\n */\nexport function findCourse(state: LearningState, courseId: string): CourseState {\n const course = state.courses.find(c => c.id === courseId)\n if (!course) throw new Error(`lookatstudy-plugin: unknown course id ${JSON.stringify(courseId)}`)\n return course\n}\n\n/**\n * Locate a lesson by its hierarchical id; unknown ids fail loud.\n * @param state - state to search.\n * @param lessonId - lesson id (`courseId:sectionIndex:lessonIndex`).\n * @returns course/section/lesson references.\n */\nexport function findLesson(state: LearningState, lessonId: string): LessonRef {\n const parts = lessonId.split(':')\n const li = parts.pop()\n const si = parts.pop()\n const courseId = parts.join(':')\n const course = findCourse(state, courseId)\n const sectionIndex = Number.parseInt(si ?? '', 10)\n const lessonIndex = Number.parseInt(li ?? '', 10)\n if (Number.isInteger(sectionIndex) && Number.isInteger(lessonIndex)) {\n const section = course.sections[sectionIndex]\n const lesson = section?.lessons[lessonIndex]\n if (lesson && lesson.id === lessonId) {\n return { course, section: section!, lesson }\n }\n }\n throw new Error(`lookatstudy-plugin: unknown lesson id ${JSON.stringify(lessonId)}`)\n}\n\n/**\n * Find the next STUDY lesson after the given one in flat course order\n * (practice/exam nodes never gate the path).\n * @param course - course to walk.\n * @param lessonId - current lesson id.\n * @returns the next study lesson, or null at the end of the path.\n */\nexport function nextLesson(course: CourseState, lessonId: string): LessonState | null {\n const flat = course.sections.flatMap(s => s.lessons)\n const i = flat.findIndex(l => l.id === lessonId)\n for (let j = i + 1; j < flat.length; j++) {\n if (flat[j]!.kind === 'study') return flat[j]!\n }\n return null\n}\n\n/**\n * LookatStudy's dual-track unlock, fired whenever the current lesson reaches\n * mastery ≥0.5 (which includes the 0.5 seed from the first attempt): unlock\n * (1) the next locked study lesson later in the same section AND (2) the\n * first study lesson of the next section. Only `locked` nodes ever change;\n * nothing re-locks.\n * @param ref - the lesson that reached the threshold.\n * @returns the lessons unlocked by this call.\n */\nfunction unlockAfter(ref: LessonRef): Array<{ id: string; title: string }> {\n const unlocked: Array<{ id: string; title: string }> = []\n const lessons = ref.section.lessons\n const li = lessons.indexOf(ref.lesson)\n for (let i = li + 1; i < lessons.length; i++) {\n const next = lessons[i]!\n if (next.kind !== 'study') continue\n if (next.status === 'locked') {\n next.status = 'available'\n unlocked.push({ id: next.id, title: next.title })\n }\n break\n }\n const si = ref.course.sections.indexOf(ref.section)\n const nextSection = ref.course.sections[si + 1]\n if (nextSection !== undefined) {\n const first = nextSection.lessons.find(l => l.kind === 'study')\n if (first !== undefined && first.status === 'locked') {\n first.status = 'available'\n unlocked.push({ id: first.id, title: first.title })\n }\n }\n return unlocked\n}\n\n/** Recompute lesson mastery as the weakest concept once KCs exist. */\nfunction aggregateMastery(lesson: LessonState): void {\n if (lesson.concepts === null || lesson.conceptMastery === null) return\n const values = lesson.concepts.map((_, i) => lesson.conceptMastery![i] ?? 0.5)\n lesson.mastery = Math.min(...values)\n}\n\n/**\n * Graduate a lesson: mark mastered, seed its SM-2 schedule if absent (first\n * review due tomorrow), and run the dual-track unlock.\n */\nfunction graduate(lesson: LessonState, course: CourseState, now: Date): Array<{ id: string; title: string }> {\n if (lesson.status !== 'mastered') {\n lesson.status = 'mastered'\n lesson.completedAt = now.toISOString()\n if (lesson.sm2 === null) {\n lesson.sm2 = { easeFactor: 2.5, intervalDays: 1, repetitions: 0 }\n lesson.dueAt = new Date(now.getTime() + DAY_MS).toISOString()\n }\n }\n const si = course.sections.findIndex(s => s.lessons.includes(lesson))\n return unlockAfter({ course, section: course.sections[si]!, lesson })\n}\n\n/** Outcome details shared by answer recording and proposal application. */\nexport interface Progression {\n graduated: boolean\n unlocked: Array<{ id: string; title: string }>\n nextDue: string | null\n courseComplete: boolean\n}\n\n/** Whether every study lesson of the course is mastered. */\nfunction courseComplete(course: CourseState): boolean {\n return course.sections.every(s => s.lessons.every(l => l.kind !== 'study' || l.status === 'mastered'))\n}\n\n/** Describe the path effects of a mastery change (early unlock, graduation). */\nfunction applyProgression(ref: LessonRef, now: Date): Progression {\n const before = ref.lesson.status\n const graduated = ref.lesson.mastery !== null && ref.lesson.mastery >= MASTERED_THRESHOLD\n let unlocked: Array<{ id: string; title: string }> = []\n if (graduated && before !== 'mastered') {\n unlocked = graduate(ref.lesson, ref.course, now)\n } else if (ref.lesson.mastery !== null && ref.lesson.mastery >= UNLOCK_THRESHOLD) {\n unlocked = unlockAfter(ref)\n }\n return {\n graduated: graduated && before !== 'mastered',\n unlocked,\n nextDue: ref.lesson.dueAt,\n courseComplete: courseComplete(ref.course),\n }\n}\n\n/** Full result of recording one graded answer. */\nexport interface AnswerResult {\n ref: LessonRef\n concept: { title: string; mastery: number } | null\n prevMastery: number\n newMastery: number\n crown: number\n mastered: boolean\n progression: Progression\n}\n\n/**\n * Open a lesson for study (LookatStudy markNodeAttempted): locked lessons\n * fail loud; the first open of an `available` lesson marks it `in_progress`,\n * seeds mastery at the BKT prior (0.5), and — because 0.5 already meets the\n * unlock threshold — runs the dual-track unlock, so merely starting a lesson\n * lights up the next ones.\n * @param state - state to mutate.\n * @param lessonId - lesson to open.\n * @param now - current time.\n * @returns the lesson ref, whether this open started it, and what unlocked.\n */\nexport function attemptLesson(\n state: LearningState,\n lessonId: string,\n now: Date,\n): { ref: LessonRef; started: boolean; unlocked: Array<{ id: string; title: string }> } {\n const ref = findLesson(state, lessonId)\n if (ref.lesson.status === 'locked') {\n throw new Error(`lookatstudy-plugin: lesson ${JSON.stringify(lessonId)} is locked; complete earlier lessons first`)\n }\n if (ref.lesson.status !== 'available') {\n return { ref, started: false, unlocked: [] }\n }\n ref.lesson.status = 'in_progress'\n ref.lesson.lastAnsweredAt = now.toISOString()\n if (ref.lesson.mastery === null) ref.lesson.mastery = 0.5\n return { ref, started: true, unlocked: unlockAfter(ref) }\n}\n\n/**\n * Record one graded answer against a lesson: attribute it to one knowledge\n * component when named, update BKT (per-KC, aggregated as the weakest),\n * nudge the SM-2 schedule when one exists, and apply mastery-driven\n * progression (early unlock at 0.5, graduation at 0.9). Locked lessons fail\n * loud — open the lesson first (study_lesson does).\n * @param state - state to mutate.\n * @param lessonId - lesson to update.\n * @param correct - whether the learner answered correctly.\n * @param concept - concept title the question tested, when attributable.\n * @param now - current time.\n * @returns mastery transition, KC attribution, and progression effects.\n */\nexport function recordAnswer(\n state: LearningState,\n lessonId: string,\n correct: boolean,\n concept: string | undefined,\n now: Date,\n): AnswerResult {\n const ref = findLesson(state, lessonId)\n if (ref.lesson.status === 'locked') {\n throw new Error(`lookatstudy-plugin: lesson ${JSON.stringify(lessonId)} is locked; open it with study_lesson first`)\n }\n if (ref.lesson.status === 'available') ref.lesson.status = 'in_progress'\n const kcIndex = concept === undefined\n ? undefined\n : ref.lesson.concepts?.findIndex(c => c.title === concept)\n if (concept !== undefined && (ref.lesson.concepts === null || kcIndex === undefined || kcIndex < 0)) {\n throw new Error(\n `lookatstudy-plugin: unknown concept ${JSON.stringify(concept)} on lesson ${JSON.stringify(lessonId)} — define concepts with study_define_concepts first`,\n )\n }\n const prev = ref.lesson.mastery\n if (ref.lesson.concepts !== null && kcIndex !== undefined) {\n const masteries = ref.lesson.conceptMastery ?? {}\n masteries[kcIndex] = updateMastery(masteries[kcIndex], correct)\n ref.lesson.conceptMastery = masteries\n aggregateMastery(ref.lesson)\n } else if (ref.lesson.concepts !== null) {\n // No attribution: conservatively update every concept (LookatStudy semantics).\n const masteries = ref.lesson.conceptMastery ?? {}\n ref.lesson.concepts.forEach((_, i) => {\n masteries[i] = updateMastery(masteries[i], correct)\n })\n ref.lesson.conceptMastery = masteries\n aggregateMastery(ref.lesson)\n } else {\n ref.lesson.mastery = updateMastery(prev, correct)\n }\n ref.lesson.attempts += 1\n if (correct) ref.lesson.correctCount += 1\n ref.lesson.lastAnsweredAt = now.toISOString()\n // BKT↔SRS loop: a graded answer nudges the review schedule (correct→5, wrong→2).\n if (ref.lesson.sm2 !== null) {\n const result = computeSm2(ref.lesson.sm2, (correct ? 5 : 2) as ReviewQuality, now)\n ref.lesson.sm2 = { easeFactor: result.easeFactor, intervalDays: result.intervalDays, repetitions: result.repetitions }\n ref.lesson.dueAt = result.dueAt\n }\n const progression = applyProgression(ref, now)\n return {\n ref,\n concept: kcIndex === undefined ? null : { title: concept!, mastery: ref.lesson.conceptMastery![kcIndex]! },\n prevMastery: prev ?? 0,\n newMastery: ref.lesson.mastery ?? 0,\n crown: masteryToCrown(ref.lesson.mastery),\n mastered: (ref.lesson.mastery ?? 0) >= MASTERED_THRESHOLD,\n progression,\n }\n}\n\n/**\n * Complete a lesson explicitly (the manual path; mastery graduation is the\n * automatic one). Locked lessons fail loud.\n * @param state - state to mutate.\n * @param lessonId - lesson to complete.\n * @param now - current time.\n * @returns completion result including the unlocked lessons.\n */\nexport function completeLesson(\n state: LearningState,\n lessonId: string,\n now: Date,\n): { ref: LessonRef; unlocked: Array<{ id: string; title: string }>; dueAt: string; courseComplete: boolean } {\n const ref = findLesson(state, lessonId)\n if (ref.lesson.status === 'locked') {\n throw new Error(`lookatstudy-plugin: lesson ${JSON.stringify(lessonId)} is locked; complete earlier lessons first`)\n }\n const unlocked = graduate(ref.lesson, ref.course, now)\n return {\n ref,\n unlocked,\n dueAt: ref.lesson.dueAt ?? new Date(now.getTime() + DAY_MS).toISOString(),\n courseComplete: courseComplete(ref.course),\n }\n}\n\n/**\n * Define (or replace) a lesson's knowledge components — the independently\n * quizzable units per-KC mastery tracks. Existing per-KC mastery resets.\n * @param state - state to mutate.\n * @param lessonId - lesson to describe.\n * @param concepts - 2–7 short concepts.\n */\nexport function defineConcepts(state: LearningState, lessonId: string, concepts: ConceptDef[]): void {\n const ref = findLesson(state, lessonId)\n if (concepts.length < 2 || concepts.length > 7) {\n throw new Error(`lookatstudy-plugin: define 2–7 concepts (got ${concepts.length})`)\n }\n for (const def of concepts) {\n if (def.title.trim() === '' || def.description.trim() === '') {\n throw new Error('lookatstudy-plugin: every concept needs a non-empty title and description')\n }\n }\n ref.lesson.concepts = concepts.map(c => ({ title: c.title.trim(), description: c.description.trim() }))\n ref.lesson.conceptMastery = {}\n aggregateMastery(ref.lesson)\n}\n\n/**\n * Log one silent friction event (confusion / block / frustration).\n * @param state - state to mutate.\n * @param lessonId - lesson it happened on, when attributable.\n * @param category - friction category.\n * @param summary - optional one-line description.\n * @param now - current time.\n */\nexport function addFriction(\n state: LearningState,\n lessonId: string | null,\n category: FrictionCategory,\n summary: string | null,\n now: Date,\n): void {\n const entry: FrictionEntry = { category, summary, at: now.toISOString() }\n if (lessonId === null) {\n // Course-less friction still counts toward the global pattern slot material.\n return\n }\n const ref = findLesson(state, lessonId)\n ref.lesson.friction.push(entry)\n if (ref.lesson.friction.length > FRICTION_CAP) ref.lesson.friction.splice(0, ref.lesson.friction.length - FRICTION_CAP)\n}\n\n/**\n * Set a memory slot. The tutor merges mentally before writing (read the\n * current slot, then send the merged 1–3 sentence text).\n * @param state - state to mutate.\n * @param category - which slot.\n * @param lessonId - lesson for the `lesson` slot.\n * @param content - merged slot content.\n * @returns the previous content, for the tutor's merge flow.\n */\nexport function setMemory(\n state: LearningState,\n category: MemoryCategory,\n content: string,\n lessonId?: string,\n): string | null {\n if (category === 'global') {\n const prev = state.memoryGlobal\n state.memoryGlobal = content\n return prev\n }\n if (category === 'pattern') {\n const course = findCourse(state, lessonId === undefined ? '' : lessonId.slice(0, lessonId.lastIndexOf(':')))\n const prev = state.memoryPatterns[course.id] ?? null\n state.memoryPatterns[course.id] = content\n return prev\n }\n if (lessonId === undefined) {\n throw new Error('lookatstudy-plugin: the lesson memory slot needs a lessonId')\n }\n const ref = findLesson(state, lessonId)\n const prev = ref.lesson.memory\n ref.lesson.memory = content\n return prev\n}\n\n/**\n * Add one notebook entry to a lesson's Cornell zones.\n * @param state - state to mutate.\n * @param lessonId - lesson the note belongs to (required: notes anchor to material).\n * @param zone - Cornell zone.\n * @param title - short entry title.\n * @param text - entry body (markdown for the understand zone).\n * @param source - where the content came from.\n * @param quote - verbatim source quote for record-zone notes.\n * @param now - current time.\n * @returns the created note.\n */\nexport function addNote(\n state: LearningState,\n lessonId: string,\n zone: NoteZone,\n title: string,\n text: string,\n source: NoteSource,\n quote: string | null,\n now: Date,\n): LessonNote {\n const ref = findLesson(state, lessonId)\n const note: LessonNote = {\n id: `${lessonId}:n${ref.lesson.notes.length}`,\n zone,\n title,\n text,\n source,\n quote,\n at: now.toISOString(),\n }\n ref.lesson.notes.push(note)\n return note\n}\n\n/**\n * Propose early mastery graduation for the learner to accept or reject in chat.\n * @param state - state to mutate.\n * @param lessonId - lesson judged mastered.\n * @param rationale - why the tutor believes it is mastered.\n * @param now - current time.\n * @returns the pending proposal.\n */\nexport function proposeMastery(state: LearningState, lessonId: string, rationale: string, now: Date): MasteryProposal {\n const ref = findLesson(state, lessonId)\n if (ref.lesson.status === 'locked') {\n throw new Error(`lookatstudy-plugin: lesson ${JSON.stringify(lessonId)} is locked`)\n }\n const pending = state.proposals.find(p => p.lessonId === lessonId && p.status === 'pending')\n if (pending) return pending\n const proposal: MasteryProposal = {\n id: `prop-${randomBytes(3).toString('hex')}`,\n lessonId,\n rationale,\n status: 'pending',\n createdAt: now.toISOString(),\n }\n state.proposals.push(proposal)\n return proposal\n}\n\n/**\n * Resolve a pending proposal: acceptance floors every concept (and the\n * lesson) to 0.95 and graduates; rejection changes nothing.\n * @param state - state to mutate.\n * @param proposalId - proposal to resolve.\n * @param accept - learner's decision.\n * @param now - current time.\n * @returns the resolved proposal.\n */\nexport function resolveProposal(state: LearningState, proposalId: string, accept: boolean, now: Date): MasteryProposal {\n const proposal = state.proposals.find(p => p.id === proposalId)\n if (!proposal) throw new Error(`lookatstudy-plugin: unknown proposal id ${JSON.stringify(proposalId)}`)\n if (proposal.status !== 'pending') {\n throw new Error(`lookatstudy-plugin: proposal ${JSON.stringify(proposalId)} is already ${proposal.status}`)\n }\n if (accept) {\n const ref = findLesson(state, proposal.lessonId)\n if (ref.lesson.concepts !== null && ref.lesson.conceptMastery !== null) {\n for (let i = 0; i < ref.lesson.concepts.length; i++) {\n ref.lesson.conceptMastery[i] = Math.max(ref.lesson.conceptMastery[i] ?? 0, 0.95)\n }\n aggregateMastery(ref.lesson)\n } else {\n ref.lesson.mastery = Math.max(ref.lesson.mastery ?? 0, 0.95)\n }\n graduate(ref.lesson, ref.course, now)\n // LookatStudy's manual-apply side effect: the graduation also counts as a\n // correct SM-2 review when a schedule already exists.\n if (ref.lesson.sm2 !== null) {\n const review = computeSm2(ref.lesson.sm2, 5, now)\n ref.lesson.sm2 = { easeFactor: review.easeFactor, intervalDays: review.intervalDays, repetitions: review.repetitions }\n ref.lesson.dueAt = review.dueAt\n }\n }\n proposal.status = accept ? 'applied' : 'rejected'\n return proposal\n}\n\n/**\n * Record an SM-2 review grade and advance the schedule.\n * @param state - state to mutate.\n * @param lessonId - lesson being reviewed.\n * @param quality - SM-2 quality grade 0–5.\n * @param now - current time.\n * @returns the advanced schedule.\n */\nexport function recordReview(\n state: LearningState,\n lessonId: string,\n quality: ReviewQuality,\n now: Date,\n): { ref: LessonRef; intervalDays: number; repetitions: number; easeFactor: number; dueAt: string } {\n const ref = findLesson(state, lessonId)\n if (!ref.lesson.sm2) {\n throw new Error(`lookatstudy-plugin: lesson ${JSON.stringify(lessonId)} has no review schedule; complete it first`)\n }\n const result = computeSm2(ref.lesson.sm2, quality, now)\n ref.lesson.sm2 = { easeFactor: result.easeFactor, intervalDays: result.intervalDays, repetitions: result.repetitions }\n ref.lesson.dueAt = result.dueAt\n return {\n ref,\n intervalDays: result.intervalDays,\n repetitions: result.repetitions,\n easeFactor: result.easeFactor,\n dueAt: result.dueAt,\n }\n}\n\n/** One due review item, flattened for tool output. */\nexport interface DueReview {\n lessonId: string\n courseId: string\n courseTitle: string\n lessonTitle: string\n dueAt: string\n overdueDays: number\n}\n\n/**\n * List mastered lessons whose SM-2 review is due, oldest first.\n * @param state - state to scan.\n * @param courseId - restrict to one course when provided.\n * @param now - current time.\n * @returns due items across the requested scope.\n */\nexport function dueReviews(state: LearningState, courseId: string | undefined, now: Date): DueReview[] {\n const courses = courseId ? [findCourse(state, courseId)] : state.courses\n const due: DueReview[] = []\n for (const course of courses) {\n for (const lesson of course.sections.flatMap(s => s.lessons)) {\n if (lesson.status !== 'mastered' || lesson.dueAt === null) continue\n if (Date.parse(lesson.dueAt) > now.getTime()) continue\n due.push({\n lessonId: lesson.id,\n courseId: course.id,\n courseTitle: course.title,\n lessonTitle: lesson.title,\n dueAt: lesson.dueAt,\n overdueDays: Math.floor((now.getTime() - Date.parse(lesson.dueAt)) / DAY_MS),\n })\n }\n }\n due.sort((a, b) => Date.parse(a.dueAt) - Date.parse(b.dueAt))\n return due\n}\n\n/** Aggregate course progress for listings. */\nexport interface CourseSummary {\n courseId: string\n title: string\n source: CourseSource\n createdAt: string\n total: number\n mastered: number\n available: number\n avgMasteryPct: number | null\n dueCount: number\n currentLessonId: string | null\n}\n\n/**\n * Summarize every course: counts, average mastery, due reviews, and the\n * current (first not-yet-mastered study) lesson.\n * @param state - state to summarize.\n * @param now - current time.\n * @returns one summary per course, in import order.\n */\nexport function courseSummaries(state: LearningState, now: Date): CourseSummary[] {\n return state.courses.map((course) => {\n const lessons = course.sections.flatMap(s => s.lessons)\n const answered = lessons.filter(l => l.mastery !== null)\n const due = dueReviews({ ...emptyState(), courses: [course] }, course.id, now)\n const current = lessons.find(l => l.kind === 'study' && l.status !== 'mastered') ?? null\n const avg = answered.length === 0\n ? null\n : answered.reduce((sum, l) => sum + (l.mastery ?? 0), 0) / answered.length\n return {\n courseId: course.id,\n title: course.title,\n source: course.source,\n createdAt: course.createdAt,\n total: lessons.length,\n mastered: lessons.filter(l => l.status === 'mastered').length,\n available: lessons.filter(l => l.status === 'available').length,\n avgMasteryPct: avg === null ? null : Math.round(avg * 100),\n dueCount: due.length,\n currentLessonId: current?.id ?? null,\n }\n })\n}\n\n/**\n * Teaching-strategy band for a mastery level (LookatStudy learner-model bands).\n * @param mastery - lesson mastery, null before any answer.\n * @returns the strategy instruction for the tutor.\n */\nexport function strategyBand(mastery: number | null): string {\n if (mastery === null || mastery < 0.1) {\n return '先建立直觉再讲细节:用类比引入概念,分步骤引导,不堆术语。'\n }\n if (mastery < 0.4) {\n return '用提问检验理解,发现误解时立即纠正,多给实际例子。'\n }\n if (mastery < 0.7) {\n return '深化理解:对比相似概念的区别,考察边界情况,可以出有迷惑性的问题。'\n }\n return '综合应用阶段:让学习者尝试用自己的话教回来(费曼技巧),考虑提议标记掌握。'\n}\n\n/** Weak-concept view of one lesson for maps and snapshots. */\nexport interface ConceptView {\n title: string\n masteryPct: number\n weak: boolean\n tested: number\n}\n\n/**\n * Project a lesson's concepts with mastery and weak flags.\n * @param lesson - lesson to project.\n * @returns concept views in definition order, or null before concepts exist.\n */\nexport function conceptViews(lesson: LessonState): ConceptView[] | null {\n if (lesson.concepts === null) return null\n return lesson.concepts.map((c, i) => {\n const mastery = lesson.conceptMastery?.[i] ?? 0.5\n return {\n title: c.title,\n masteryPct: Math.round(mastery * 100),\n weak: mastery < WEAK_CONCEPT_THRESHOLD,\n tested: lesson.conceptMastery !== null && i in lesson.conceptMastery ? 1 : 0,\n }\n })\n}\n\n/** The four consolidation starters attached to a lesson (LookatStudy templates). */\nexport function starterPrompts(lessonTitle: string): Array<{ label: string; message: string; effect: 'mastery' | 'friction' | 'none' }> {\n return [\n { label: '🔬 深入这点', message: `帮我深入讲讲「${lessonTitle}」刚才那个核心点——展开它的结构、细节和容易忽略的边界。`, effect: 'none' },\n { label: '💡 举个例子', message: `给我一个「${lessonTitle}」的实际例子或用法,让我更具体地理解。`, effect: 'none' },\n { label: '📝 考考我', message: `出一道关于「${lessonTitle}」的应用题考考我,看我是否真懂了——我答完请判断对错。`, effect: 'mastery' },\n { label: '🤔 我没太懂', message: `关于「${lessonTitle}」,我有地方不太懂,帮我理一理——先问我是哪里不清楚。`, effect: 'friction' },\n ]\n}\n\n/** Structured learner snapshot for prompt injection (one home, pure read). */\nexport interface LearnerSnapshot {\n focus: { lessonId: string; courseTitle: string; lessonTitle: string; masteryPct: number | null; status: LessonStatus } | null\n strategy: string | null\n concepts: ConceptView[] | null\n friction: FrictionEntry[]\n memoryGlobal: string | null\n memoryLesson: string | null\n memoryPattern: string | null\n dueCount: number\n pendingProposal: MasteryProposal | null\n}\n\n/**\n * Compose the learner snapshot for the focused lesson (or course-wide when\n * no focus): strategy band, weak concepts, recent friction, memory slots,\n * due count, pending proposal. The tutor persona's volatile tail.\n * @param state - state to read.\n * @param now - current time.\n * @returns the snapshot value.\n */\nexport function learnerSnapshot(state: LearningState, now: Date): LearnerSnapshot {\n let ref: LessonRef | null = state.focus === null ? null : tryFindLesson(state, state.focus.lessonId)\n if (ref === null && state.courses.length > 0) {\n const lessons = state.courses[0]!.sections.flatMap(s => s.lessons)\n const current = lessons.find(l => l.kind === 'study' && l.status === 'in_progress')\n ?? lessons.find(l => l.kind === 'study' && l.status === 'available')\n ?? null\n ref = current === null ? null : { course: state.courses[0]!, section: state.courses[0]!.sections.find(s => s.lessons.includes(current))!, lesson: current }\n }\n return {\n focus: ref === null ? null : {\n lessonId: ref.lesson.id,\n courseTitle: ref.course.title,\n lessonTitle: ref.lesson.title,\n masteryPct: ref.lesson.mastery === null ? null : Math.round(ref.lesson.mastery * 100),\n status: ref.lesson.status,\n },\n strategy: ref === null ? null : strategyBand(ref.lesson.mastery),\n concepts: ref === null ? null : conceptViews(ref.lesson),\n friction: ref === null ? [] : ref.lesson.friction.slice(-5),\n memoryGlobal: state.memoryGlobal,\n memoryLesson: ref?.lesson.memory ?? null,\n memoryPattern: ref === null ? null : (state.memoryPatterns[ref.course.id] ?? null),\n dueCount: dueReviews(state, undefined, now).length,\n pendingProposal: state.proposals.find(p => p.status === 'pending') ?? null,\n }\n}\n\n/** findLesson that returns null instead of throwing (snapshot focus may be stale). */\nfunction tryFindLesson(state: LearningState, lessonId: string): LessonRef | null {\n try {\n return findLesson(state, lessonId)\n } catch {\n return null\n }\n}\n","/**\n * The study tab's HTTP API under `/lookatstudy/api/*`: the polled state feed\n * and the tab's write actions (focus, mode, lesson-session binding, course\n * deletion, study-workspace path), reading the same live plugin state the\n * tutor tools write. The v0.3 standalone workbench page and its reverse\n * message channel were removed once the in-client study tab superseded them.\n * @module dsh-plugin-lookatstudy/dashboard\n */\n\nimport { renderMarkdown } from './markdown.ts'\nimport {\n conceptViews,\n deleteCourse,\n dueReviews,\n findCourse,\n findLesson,\n learnerSnapshot,\n starterPrompts,\n strategyBand,\n type LearningState,\n} from './state.ts'\n\n/** State access shared with the tools (same live object). */\nexport interface DashboardStore {\n get(): LearningState\n save(): void\n}\n\n/** Wiring handed in by `apply`. */\nexport interface DashboardDeps {\n store: DashboardStore\n /** Directory the one-click starter adopts as the study workspace (apply ensures it exists). */\n studyAreaPath: string\n}\n\n/** Structural slice of the dsh `webServer` service, for testability. */\nexport interface RouteRegistry {\n register(route: { kind: 'exact' | 'prefix'; path: string; handler: (req: RequestLike, res: ResponseLike) => void | Promise<void> }): () => void\n}\n\n/** Structural `IncomingMessage`. */\nexport interface RequestLike {\n method?: string\n url?: string\n}\n\n/** Structural `ServerResponse` the handlers write to. */\nexport interface ResponseLike {\n headersSent: boolean\n writeHead(status: number, headers?: Record<string, string>): ResponseLike\n end(chunk?: string): ResponseLike\n on(event: 'data', listener: (chunk: Buffer) => void): void\n on(event: 'end', listener: () => void): void\n}\n\n/** One course's map for the left rail. */\nexport interface WorkbenchCourse {\n courseId: string\n title: string\n mastered: number\n total: number\n avgMasteryPct: number | null\n sections: Array<{\n title: string\n index: number\n lessons: Array<{\n id: string\n title: string\n kind: string\n status: string\n masteryPct: number | null\n weakConcepts: number\n frictionCount: number\n due: boolean\n focus: boolean\n }>\n }>\n}\n\n/** The focus lesson's 讲解 view. */\nexport interface WorkbenchLesson {\n lessonId: string\n courseTitle: string\n sectionTitle: string\n title: string\n status: string\n masteryPct: number | null\n strategy: string\n concepts: Array<{ title: string; masteryPct: number; weak: boolean }>\n starters: Array<{ label: string; message: string }>\n notes: Array<{ id: string; zone: string; title: string; text: string; source: string; quote: string | null }>\n html: string\n}\n\n/** Whole workbench state for the page. */\nexport interface WorkbenchState {\n mode: string\n courses: WorkbenchCourse[]\n focusLessonId: string | null\n lesson: WorkbenchLesson | null\n dueCount: number\n due: Array<{ lessonId: string; lessonTitle: string; courseTitle: string; overdueDays: number }>\n pendingProposals: Array<{ id: string; lessonTitle: string; rationale: string }>\n memory: { global: string | null; lesson: string | null; pattern: string | null }\n /** Lesson id → dsh session id (one session per lesson node). */\n lessonSessions: Record<string, string>\n}\n\n/**\n * Assemble the whole workbench state (pure read; the lesson HTML is rendered\n * server-side from the sanitized markdown pipeline).\n * @param state - live learning state.\n * @param now - current time.\n * @returns the page's data contract.\n */\nexport function workbenchState(state: LearningState, now: Date): WorkbenchState {\n const focusId = state.focus?.lessonId ?? null\n const dueIds = new Set(dueReviews(state, undefined, now).map(d => d.lessonId))\n const courses: WorkbenchCourse[] = state.courses.map((course) => {\n const lessons = course.sections.flatMap(s => s.lessons)\n const answered = lessons.filter(l => l.mastery !== null)\n return {\n courseId: course.id,\n title: course.title,\n mastered: lessons.filter(l => l.status === 'mastered').length,\n total: lessons.length,\n avgMasteryPct: answered.length === 0\n ? null\n : Math.round(answered.reduce((sum, l) => sum + (l.mastery ?? 0), 0) / answered.length * 100),\n sections: course.sections.map((section, index) => ({\n title: section.title,\n index,\n lessons: section.lessons.map(lesson => ({\n id: lesson.id,\n title: lesson.title,\n kind: lesson.kind,\n status: lesson.status,\n masteryPct: lesson.mastery === null ? null : Math.round(lesson.mastery * 100),\n weakConcepts: (conceptViews(lesson) ?? []).filter(c => c.weak).length,\n frictionCount: lesson.friction.length,\n due: dueIds.has(lesson.id),\n focus: lesson.id === focusId,\n })),\n })),\n }\n })\n let lesson: WorkbenchLesson | null = null\n if (focusId !== null) {\n try {\n const ref = findLesson(state, focusId)\n lesson = {\n lessonId: ref.lesson.id,\n courseTitle: ref.course.title,\n sectionTitle: ref.section.title,\n title: ref.lesson.title,\n status: ref.lesson.status,\n masteryPct: ref.lesson.mastery === null ? null : Math.round(ref.lesson.mastery * 100),\n strategy: strategyBand(ref.lesson.mastery),\n concepts: conceptViews(ref.lesson) ?? [],\n starters: starterPrompts(ref.lesson.title).map(s => ({ label: s.label, message: s.message })),\n notes: ref.lesson.notes.map(n => ({\n id: n.id,\n zone: n.zone,\n title: n.title,\n text: n.text,\n source: n.source,\n quote: n.quote,\n })),\n html: renderMarkdown(ref.lesson.body),\n }\n } catch {\n lesson = null\n }\n }\n const due = dueReviews(state, undefined, now)\n return {\n mode: state.mode,\n courses,\n focusLessonId: focusId,\n lesson,\n dueCount: due.length,\n due: due.map(d => ({ lessonId: d.lessonId, lessonTitle: d.lessonTitle, courseTitle: d.courseTitle, overdueDays: d.overdueDays })),\n pendingProposals: state.proposals\n .filter(p => p.status === 'pending')\n .map(p => {\n try {\n return { id: p.id, lessonTitle: findLesson(state, p.lessonId).lesson.title, rationale: p.rationale }\n } catch {\n return { id: p.id, lessonTitle: p.lessonId, rationale: p.rationale }\n }\n }),\n memory: (() => {\n const snap = learnerSnapshot(state, now)\n return { global: snap.memoryGlobal, lesson: snap.memoryLesson, pattern: snap.memoryPattern }\n })(),\n lessonSessions: state.lessonSessions,\n }\n}\n\nconst JSON_HEADERS = { 'content-type': 'application/json; charset=utf-8' }\n\nfunction sendJson(res: ResponseLike, status: number, value: unknown): void {\n res.writeHead(status, JSON_HEADERS).end(JSON.stringify(value))\n}\n\n/**\n * Read one JSON body, answering 400 on malformed or oversized input so the\n * handler never throws into the HTTP layer.\n * @returns the parsed value, or undefined when the response is already sent.\n */\nasync function readJsonBodySafe(req: RequestLike, res: ResponseLike): Promise<unknown | undefined> {\n try {\n return await readJsonBody(req as never)\n } catch (error) {\n sendJson(res, 400, { ok: false, error: error instanceof Error ? error.message : 'bad request' })\n return undefined\n }\n}\n\n/** Read one JSON request body with a hard 64 kB cap; malformed bodies reject. */\nfunction readJsonBody(req: RequestLike & { on(event: 'data' | 'end', listener: (...args: never[]) => void): void }): Promise<unknown> {\n return new Promise((resolve, reject) => {\n const chunks: Buffer[] = []\n req.on('data', (chunk: Buffer) => {\n chunks.push(chunk)\n if (chunks.reduce((n, c) => n + c.length, 0) > 65_536) {\n reject(new Error('request body too large'))\n return\n }\n })\n req.on('end', () => {\n try {\n resolve(JSON.parse(Buffer.concat(chunks).toString('utf8')))\n } catch {\n reject(new Error('request body is not valid JSON'))\n }\n })\n })\n}\n\n/**\n * Register the study tab's API routes under `/lookatstudy/api/*`: the polling\n * state feed plus the tab's write actions.\n * @param webServer - the composed webserver's route registry.\n * @param deps - store plus the study-workspace directory.\n * @returns the disposer removing every route.\n */\nexport function registerDashboard(webServer: RouteRegistry, deps: DashboardDeps): () => void {\n const disposeRoutes = webServer.register({\n kind: 'prefix',\n path: '/lookatstudy',\n handler: async (req, res) => {\n const pathname = new URL(req.url ?? '/', 'http://x').pathname\n if (req.method === 'GET' && pathname === '/lookatstudy/api/state') {\n sendJson(res, 200, workbenchState(deps.store.get(), new Date()))\n return\n }\n if (req.method === 'POST' && pathname === '/lookatstudy/api/focus') {\n const body = await readJsonBodySafe(req, res)\n if (body === undefined) return\n if (typeof body.lessonId !== 'string') {\n sendJson(res, 400, { ok: false, error: 'lessonId (string) required' })\n return\n }\n try {\n const ref = findLesson(deps.store.get(), body.lessonId)\n deps.store.get().focus = { lessonId: ref.lesson.id }\n deps.store.save()\n sendJson(res, 200, { ok: true })\n } catch (error) {\n sendJson(res, 404, { ok: false, error: error instanceof Error ? error.message : String(error) })\n }\n return\n }\n if (req.method === 'GET' && pathname === '/lookatstudy/api/study-workspace') {\n sendJson(res, 200, { ok: true, path: deps.studyAreaPath })\n return\n }\n if (req.method === 'POST' && pathname === '/lookatstudy/api/course/delete') {\n const body = await readJsonBodySafe(req, res)\n if (body === undefined) return\n if (typeof body.courseId !== 'string') {\n sendJson(res, 400, { ok: false, error: 'courseId (string) required' })\n return\n }\n try {\n const course = findCourse(deps.store.get(), body.courseId)\n deleteCourse(deps.store.get(), course.id)\n if (deps.store.get().focus?.lessonId.startsWith(`${course.id}:`)) {\n deps.store.get().focus = null\n }\n deps.store.save()\n sendJson(res, 200, { ok: true })\n } catch (error) {\n sendJson(res, 404, { ok: false, error: error instanceof Error ? error.message : String(error) })\n }\n return\n }\n if (req.method === 'POST' && pathname === '/lookatstudy/api/lesson-session') {\n const body = await readJsonBodySafe(req, res)\n if (body === undefined) return\n if (typeof body.lessonId !== 'string' || typeof body.sessionId !== 'string') {\n sendJson(res, 400, { ok: false, error: 'lessonId and sessionId (strings) required' })\n return\n }\n deps.store.get().lessonSessions[body.lessonId] = body.sessionId\n deps.store.save()\n sendJson(res, 200, { ok: true })\n return\n }\n if (req.method === 'POST' && pathname === '/lookatstudy/api/mode') {\n const body = await readJsonBodySafe(req, res)\n if (body === undefined) return\n if (body.mode !== 'direct' && body.mode !== 'guide' && body.mode !== 'practice') {\n sendJson(res, 400, { ok: false, error: 'mode must be direct | guide | practice' })\n return\n }\n deps.store.get().mode = body.mode\n deps.store.save()\n sendJson(res, 200, { ok: true, mode: body.mode })\n return\n }\n sendJson(res, 404, { ok: false, error: 'not found' })\n },\n })\n return () => { disposeRoutes() }\n}\n","// Vendored from LookatStudy src/main/services/pure/markdown-course.ts (MIT License, https://github.com/kaiji/LookatStudy).\r\n// Unmodified except this provenance header. PDF/PPTX branches resolve unavailable optional libs and are skipped per upstream try/catch.\r\n/**\r\n * Markdown → 课程树 纯解析器(M4 Course Generator 阶段 A 的可测核心)。\r\n *\r\n * 把一份 README.md 解析成 section/lesson 两层结构:\r\n * - H2(## )→ section\r\n * - H3(### )→ section 下的 lesson\r\n * - 锚点从标题生成(GitHub 风格:小写、空格变 -、去标点)\r\n *\r\n * 零依赖,可被测试直接 import 真实源码(VERIFICATION §3.1)。\r\n * LLM 部分(讲解生成、质量优化)在 course-generator.ts,非确定性,不在本文件测。\r\n */\r\n\r\nexport interface ParsedLesson {\r\n title: string;\r\n anchor: string;\r\n /** 该 H3 下到下一个 H3/H2 之间的正文(逐字) */\r\n body: string;\r\n /** 文件分类器标记该课来源不确定(规则无法确定是课时正文),LLM 结构化时应优先判断 keep/skip */\r\n uncertain?: boolean;\r\n /** 原始文件路径(如 lessons/3-NN/03-Perceptron/README.md),用于翻译匹配/图片关联 */\r\n sourceFilePath?: string;\r\n /** 两个世界: null=未定(LLM 判), \"study\"=学习讲解, \"practice\"=实操练习 */\r\n world?: \"study\" | \"practice\" | null;\r\n}\r\n\r\nexport interface ParsedSection {\r\n title: string;\r\n anchor: string;\r\n /** 两个世界: null=未定, \"study\"/\"practice\" 由子节点多数决定或 LLM 判 */\r\n world?: \"study\" | \"practice\" | null;\r\n lessons: ParsedLesson[];\r\n /** 章节测验引子(考试内容始终由导师按本节课时即时生成,这里只放说明性开场;由 state 层消费) */\r\n examBody?: string;\r\n}\r\n\r\nexport interface ParsedCourse {\r\n /** 第一个 H1 作为课程标题,没有则 \"(untitled)\" */\r\n title: string;\r\n sections: ParsedSection[];\r\n}\r\n\r\n/**\r\n * GitHub 风格的 anchor 生成:小写、去一组标点(保留中文等 unicode)、每个空格单独转 -。\r\n * 注意:**不合并多 -**(\"A & B\" → 去 & 留两空格 → \"a--b\",与 GitHub slugger 一致)。\r\n * 与 seed.ts 的锚点对齐。\r\n */\r\nexport function titleToAnchor(title: string): string {\r\n return title\r\n .toLowerCase()\r\n .trim()\r\n // GitHub slugger 移除的标点集(保留字母/数字/中文/下划线/连字符)\r\n .replace(/[!\"#$%&'()*+,.\\/:;<=>?@[\\\\\\]^`{|}~]/g, \"\")\r\n .replace(/ /g, \"-\") // 每个空格单独转 -(不合并)\r\n .replace(/^-|-$/g, \"\"); // 去首尾 -\r\n}\r\n\r\n/**\r\n * 清洗课时/章节标题 — 去 emoji、多余空格、markdown 格式符号。\r\n * \"🛠 The Modern FDE Stack\" → \"The Modern FDE Stack\"\r\n * \"## [Pre-lecture quiz](url)\" → \"Pre-lecture quiz\"\r\n */\r\nexport function cleanTitle(raw: string): string {\r\n return raw\r\n // 去 emoji(Unicode emoji 范围)\r\n .replace(/[\\u{1F000}-\\u{1FFFF}\\u{2600}-\\u{27BF}\\u{2190}-\\u{21FF}\\u{2B00}-\\u{2BFF}]/gu, \"\")\r\n // 去 markdown 链接格式 [text](url) → text\r\n .replace(/\\[([^\\]]*)\\]\\([^)]*\\)/g, \"$1\")\r\n // 去 markdown 标题符号\r\n .replace(/^#+\\s*/, \"\")\r\n // 去多余空格\r\n .trim()\r\n .replace(/\\s+/g, \" \")\r\n // 去首尾标点\r\n .replace(/^[·\\-\\.\\s]+|[·\\-\\.\\s]+$/g, \"\")\r\n .trim();\r\n}\r\n\r\n/**\r\n * 解析 markdown 为课程树。\r\n * 容错:H3 出现在任何 H2 之前 → 归到一个 \"(前言)\" section。\r\n */\r\nexport function parseMarkdownToCourse(md: string): ParsedCourse {\r\n const lines = md.split(/\\r?\\n/);\r\n const sections: ParsedSection[] = [];\r\n let title = \"(untitled)\";\r\n let currentSection: ParsedSection | null = null;\r\n let bodyBuffer: string[] = [];\r\n let inCodeFence = false; // ``` 或 ~~~ 围栏状态——代码块内的 #/##/### 是注释不是标题\r\n\r\n const flushLessonBody = () => {\r\n if (currentSection && currentSection.lessons.length > 0) {\r\n currentSection.lessons[currentSection.lessons.length - 1].body =\r\n bodyBuffer.join(\"\\n\").trim();\r\n }\r\n bodyBuffer = [];\r\n };\r\n\r\n for (const line of lines) {\r\n // 代码围栏状态机(必须在标题检测之前)\r\n if (/^(\\s*)(```|~~~)/.test(line)) {\r\n inCodeFence = !inCodeFence;\r\n bodyBuffer.push(line);\r\n continue;\r\n }\r\n // 代码块内:原样保留,不当标题处理\r\n if (inCodeFence) {\r\n bodyBuffer.push(line);\r\n continue;\r\n }\r\n // H1 → 课程标题(取第一个)\r\n if (/^#\\s+/.test(line) && title === \"(untitled)\") {\r\n title = cleanTitle(line.replace(/^#\\s+/, \"\").trim());\r\n continue;\r\n }\r\n // H2 → 新 section\r\n if (/^##\\s+/.test(line)) {\r\n flushLessonBody();\r\n const sectionTitle = cleanTitle(line.replace(/^##\\s+/, \"\").trim());\r\n currentSection = {\r\n title: sectionTitle,\r\n anchor: titleToAnchor(sectionTitle),\r\n lessons: [],\r\n };\r\n sections.push(currentSection);\r\n continue;\r\n }\r\n // H3 → 当前 section 下新 lesson\r\n if (/^###\\s+/.test(line)) {\r\n flushLessonBody();\r\n const lessonTitle = cleanTitle(line.replace(/^###\\s+/, \"\").trim());\r\n if (!currentSection) {\r\n // H3 在 H2 前:建前言 section\r\n currentSection = {\r\n title: \"(前言)\",\r\n anchor: titleToAnchor(\"前言\"),\r\n lessons: [],\r\n };\r\n sections.push(currentSection);\r\n }\r\n currentSection.lessons.push({\r\n title: lessonTitle,\r\n anchor: titleToAnchor(lessonTitle),\r\n body: \"\",\r\n });\r\n continue;\r\n }\r\n // 其他行 → 当前 lesson 的 body\r\n bodyBuffer.push(line);\r\n }\r\n flushLessonBody();\r\n\r\n return { title, sections };\r\n}\r\n\r\n/* ---------- LabType 检测 ---------- */\r\n\r\nexport type LabType = \"doc\" | \"code\" | \"notebook\";\r\n\r\n/**\r\n * 从 markdown 内容推断 LabType(决定 AI 能否动手操作)。\r\n *\r\n * 改进(原则:规则管确定性):\r\n * - notebook: 正文有 ≥3 个代码块 + 含 jupyter/notebook/.ipynb 关键词 → notebook\r\n * (单纯提及 \"jupyter\" 不够,必须同时有大量代码块)\r\n * - code: 正文有 ≥5 个代码块(多代码块 = 实操课程)\r\n * (单个代码块不够——很多理论课有一个示例代码)\r\n * - doc: 其他(纯阅读课程)\r\n */\r\nexport function detectLabType(md: string): LabType {\r\n const lower = md.toLowerCase();\r\n // 数代码块数量\r\n const codeBlockCount = (md.match(/```/g) || []).length / 2; // 开闭成对\r\n // notebook: 大量代码 + notebook 关键词\r\n if (codeBlockCount >= 3 && /\\.ipynb|jupyter\\s*notebook|colab notebook/.test(lower)) {\r\n return \"notebook\";\r\n }\r\n // code: 代码块多(≥5 个 = 实操导向)\r\n if (codeBlockCount >= 5) {\r\n return \"code\";\r\n }\r\n return \"doc\";\r\n}\r\n","// Vendored from LookatStudy src/main/services/pure/local-folder-scanner.ts (MIT License, https://github.com/kaiji/LookatStudy).\n// Local modification (documented): dedupKey() includes the file's directory so\n// per-lesson README.md files in different directories are NOT collapsed into one\n// (upstream keys on basename alone, dropping every nested README after the first —\n// fatal for course repos whose lessons live in per-directory READMEs).\n// PDF/PPTX branches resolve unavailable optional libs and are skipped per upstream try/catch.\n/**\n * 本地文件夹通用扫描器 —— 把任意课程文件夹(如 Coursera 下载包)递归扫描成文档清单。\n *\n * 设计原则:通用,不硬编码某一种文件夹结构。\n * - 扫描文档类:.txt/.md/.mdx/.markdown/.html/.htm/.pdf/.ipynb/.rst/.rmd/.org/.adoc/.asciidoc\n * - 扫描代码类:.py/.js/.ts/.go/.rs/.java/.c/.cpp/.rb/.sh/.lua/.sql/.r/.jl/.dart/... (30+ 语言, code-parser 转 markdown)\n * - 图片文件:.png/.jpg/.jpeg/.gif/.webp/.svg/.bmp/.avif/.ico/.tiff/.heic(多模态 flag on 时收集)\n * - 中文优先去重(同内容 .zh-CN 和 .en 只留中文)\n * - 按文件名 NN_ 前缀排序\n * - HTML 去标签转纯文本(<co-content> 富文本质量足够)\n * - PDF 用 pdf-renderer 提取文字 + 图片(纯文字/纯图片/混合自动分类)\n *\n * 纯函数为主(htmlToText/标题推断/去重/图片引用解析),便于 verify 脚本测。\n * scanFolder 本身用 fs(异步),verify 用临时目录造文件测。\n */\nimport { readFile, readdir } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join, relative, sep, basename, dirname } from \"node:path\";\n\nexport interface ScannedDoc {\n /** 相对根目录的路径(如 calculus/week1/lesson1/06_motivation.zh-CN.txt),用 / 分隔 */\n path: string;\n /** 从路径/文件名推断的标题(去数字前缀/扩展名/语言后缀) */\n title: string;\n /** 提取的纯文本内容 */\n content: string;\n /** 语言(zh/en/other),用于去重 */\n lang: \"zh\" | \"en\" | \"other\";\n /** 文件类型 */\n kind: \"txt\" | \"md\" | \"html\" | \"pdf\" | \"ipynb\" | \"rst\" | \"rmd\" | \"org\" | \"adoc\" | \"code\" | \"pptx\";\n}\n\n/** 扫描到的图片资源(独立图片文件 / markdown 引用 / PDF 页面渲染图) */\nexport interface ScannedImage {\n /** 相对根目录的路径(用 / 分隔) */\n path: string;\n /** 绝对路径(落库时复制到 assets 用);buffer 型(PDF 提取)为空串 */\n absPath: string;\n /** 从文件名推断的标题/描述 */\n title: string;\n /** MIME 类型 */\n mime: string;\n /** 来源:独立文件 / markdown 引用 / PDF 页面渲染图 */\n source: \"image_file\" | \"markdown_ref\" | \"pdf_page\";\n /** markdown ![](x) 的 alt 文本(独立文件时 = title) */\n altText: string;\n /** PDF 提取的图片二进制(有 buffer 时 absPath 可空);独立文件时为 undefined */\n buffer?: Buffer;\n /** PDF 来源页码(1-based);非 PDF 为 undefined */\n pageNumber?: number;\n}\n\n/** 支持的扩展名 → kind 映射 */\nconst EXT_KIND: Record<string, ScannedDoc[\"kind\"]> = {\n txt: \"txt\",\n md: \"md\",\n mdx: \"md\",\n markdown: \"md\",\n html: \"html\",\n htm: \"html\",\n pdf: \"pdf\",\n pptx: \"pptx\",\n ipynb: \"ipynb\",\n rst: \"rst\",\n rmd: \"rmd\",\n org: \"org\",\n adoc: \"adoc\",\n asciidoc: \"adoc\",\n // 代码文件 → code kind (代码即教学内容)\n py: \"code\", js: \"code\", jsx: \"code\", ts: \"code\", tsx: \"code\", mjs: \"code\", cjs: \"code\",\n go: \"code\", rs: \"code\", java: \"code\", kt: \"code\", kts: \"code\", scala: \"code\",\n c: \"code\", h: \"code\", cpp: \"code\", cc: \"code\", cxx: \"code\", hpp: \"code\",\n cs: \"code\", rb: \"code\", php: \"code\", swift: \"code\",\n sh: \"code\", bash: \"code\", zsh: \"code\", ps1: \"code\",\n lua: \"code\", r: \"code\", jl: \"code\", dart: \"code\",\n clj: \"code\", ex: \"code\", exs: \"code\", erl: \"code\", hs: \"code\", ml: \"code\", fs: \"code\",\n sql: \"code\", pl: \"code\", elm: \"code\",\n};\n\n/** 图片扩展名 → MIME 映射 */\nconst IMAGE_EXT_MIME: Record<string, string> = {\n png: \"image/png\",\n jpg: \"image/jpeg\",\n jpeg: \"image/jpeg\",\n gif: \"image/gif\",\n webp: \"image/webp\",\n svg: \"image/svg+xml\",\n bmp: \"image/bmp\",\n avif: \"image/avif\",\n ico: \"image/x-icon\",\n tiff: \"image/tiff\",\n tif: \"image/tiff\",\n heic: \"image/heic\",\n};\n\n/** 排除的目录(非教学内容) */\nconst EXCLUDED_DIRS = new Set([\n \"node_modules\", \".git\", \".svn\", \"dist\", \"build\", \"__pycache__\",\n \".DS_Store\", \"translations\",\n \".venv\", \"venv\", \"env\", \"vendor\", \"target\", \"out\", \"coverage\",\n \".next\", \".nuxt\", \".gradle\", \".idea\", \".vscode\", \".cache\",\n \".pytest_cache\", \".mypy_cache\", \".turbo\", \".svelte-kit\",\n \"bin\", \"obj\", \"__pypackages__\", \".docusaurus\",\n]);\n\n/** HTML 转纯文本:去 script/style,标签转段落,<li> 加 •,decode 常见实体。纯函数,可测。 */\nexport function htmlToText(html: string): string {\n let s = html;\n // 去 script/style 整块\n s = s.replace(/<script[\\s\\S]*?<\\/script>/gi, \"\");\n s = s.replace(/<style[\\s\\S]*?<\\/style>/gi, \"\");\n s = s.replace(/<head[\\s\\S]*?<\\/head>/gi, \"\");\n // 块级标签 → 换行\n s = s.replace(/<\\/(p|div|section|article|h[1-6]|li|tr|br)>/gi, \"\\n\");\n s = s.replace(/<br\\s*\\/?>/gi, \"\\n\");\n // <li> → 项目符号\n s = s.replace(/<li[^>]*>/gi, \"• \");\n // 表格单元格分隔\n s = s.replace(/<\\/td>/gi, \"\\t\");\n s = s.replace(/<\\/th>/gi, \"\\t\");\n // 去所有剩余标签\n s = s.replace(/<[^>]+>/g, \"\");\n // decode 常见 HTML 实体\n s = s\n .replace(/&nbsp;/g, \" \")\n .replace(/&amp;/g, \"&\")\n .replace(/&lt;/g, \"<\")\n .replace(/&gt;/g, \">\")\n .replace(/&quot;/g, '\"')\n .replace(/&#39;/g, \"'\")\n .replace(/&hellip;/g, \"…\")\n .replace(/&mdash;/g, \"—\");\n // 压缩多余空白(保留段落分隔)\n s = s.replace(/[ \\t]+/g, \" \");\n s = s.replace(/\\n[ \\t]+/g, \"\\n\");\n s = s.replace(/\\n{3,}/g, \"\\n\\n\");\n return s.trim();\n}\n\n/** 从文件名推断语言(用于中文优先去重)。 */\nexport function detectLang(filename: string): \"zh\" | \"en\" | \"other\" {\n const lower = filename.toLowerCase();\n if (/\\.zh[-_]?cn\\./.test(lower) || /\\.zh[-_]?hans\\./.test(lower) || /\\.zh[-_]?tw\\./.test(lower) || /\\.zh[-_]?hant\\./.test(lower) || /\\.zh\\./.test(lower)) return \"zh\";\n if (/\\.en[-_]?us\\./.test(lower) || /\\.en[-_]?gb\\./.test(lower) || /\\.en\\./.test(lower)) return \"en\";\n if (/\\.ja\\./.test(lower) || /\\.ko\\./.test(lower) || /\\.de\\./.test(lower) || /\\.fr\\./.test(lower) || /\\.es\\./.test(lower) || /\\.pt[-_]?br\\./.test(lower) || /\\.pt\\./.test(lower) || /\\.it\\./.test(lower) || /\\.ru\\./.test(lower) || /\\.ar\\./.test(lower)) return \"other\";\n return \"other\";\n}\n\n/** 从路径推断标题:\n * 07_derivatives-and-tangents.zh-CN.txt → \"Derivatives And Tangents\"\n * 01_lesson-1-intro/README.md → \"Lesson 1 Intro\"\n * 去数字前缀 + 扩展名 + 语言后缀,- _ 转空格,首字母大写。纯函数,可测。 */\nexport function inferTitle(relPath: string): string {\n const filename = basename(relPath);\n // 去扩展名\n let name = filename.replace(/\\.(txt|md|mdx|markdown|html?|pdf|ipynb|rst|rmd|org|adoc|asciidoc|py|js|jsx|ts|tsx|mjs|cjs|go|rs|java|kt|kts|scala|c|h|cpp|cc|cxx|hpp|cs|rb|php|swift|sh|bash|zsh|ps1|lua|r|jl|dart|clj|ex|exs|erl|hs|ml|fs|sql|pl|elm)$/i, \"\");\n // 去语言后缀(.zh-CN / .en / .en-US 等)\n name = name.replace(/\\.(zh[-_]?cn|zh[-_]?hans|zh|en[-_]?us|en)$/i, \"\");\n // README / index → 用父目录名\n if (/^(readme|index)$/i.test(name)) {\n const parts = relPath.split(\"/\").filter(Boolean);\n const parent = parts[parts.length - 2];\n if (parent) name = parent;\n }\n // 去开头数字前缀(01_ / 02-)\n name = name.replace(/^(\\d+[_-]\\s*)/, \"\");\n // - 和 _ 转空格\n name = name.replace(/[-_]+/g, \" \").trim();\n // 首字母大写(英文),中文不受影响\n if (/^[a-z]/.test(name)) name = name.charAt(0).toUpperCase() + name.slice(1);\n return name || filename;\n}\n\n/** 算 basename 的去重 key(去掉语言后缀 + 扩展名)。\n * 06_motivation.en.txt 和 06_motivation.zh-CN.txt → key \"06_motivation\" */\nexport function dedupKey(relPath: string): string {\n const dir = dirname(relPath).toLowerCase();\n const filename = basename(relPath);\n let name = filename.replace(/\\.(txt|md|mdx|markdown|html?|pdf|ipynb|rst|rmd|org|adoc|asciidoc|py|js|jsx|ts|tsx|mjs|cjs|go|rs|java|kt|kts|scala|c|h|cpp|cc|cxx|hpp|cs|rb|php|swift|sh|bash|zsh|ps1|lua|r|jl|dart|clj|ex|exs|erl|hs|ml|fs|sql|pl|elm)$/i, \"\");\n name = name.replace(/\\.(zh[-_]?cn|zh[-_]?hans|zh|en[-_]?us|en)$/i, \"\");\n return (dir === \".\" ? \"\" : dir + \"/\") + name.toLowerCase();\n}\n\n/**\n * 递归扫描一个目录,返回所有文本类文档(可选:同时收集图片)。\n * 中文优先去重:同 dedupKey 的多语言文件只保留中文(.zh 优先于 .en/other)。\n * 按相对路径排序(保持目录顺序 + 文件名 NN_ 前缀)。\n *\n * @param rootDir 根目录绝对路径\n * @param onProgress 可选进度回调(已扫文件数,当前路径)\n * @param options.collectImages true 时同时收集图片文件 + markdown 图片引用(多模态 flag)\n * @returns 文档数组,或 { docs, images }(collectImages=true 时)\n */\nexport async function scanFolder(\n rootDir: string,\n onProgress?: (scanned: number, currentPath: string) => void,\n options?: { collectImages?: boolean },\n): Promise<ScannedDoc[] | { docs: ScannedDoc[]; images: ScannedImage[] }> {\n const allFiles: { absPath: string; relPath: string; isImage: boolean }[] = [];\n await walkDir(rootDir, rootDir, allFiles);\n\n // 按相对路径排序(目录顺序 + 文件名数字前缀)\n allFiles.sort((a, b) => naturalPathCompare(a.relPath, b.relPath));\n\n const docFiles = allFiles.filter((f) => !f.isImage);\n const imageFiles = allFiles.filter((f) => f.isImage);\n\n // 读所有文档文件,按 kind 提取内容\n const docs: ScannedDoc[] = [];\n let count = 0;\n for (const f of docFiles) {\n onProgress?.(++count, f.relPath);\n const ext = f.relPath.toLowerCase().match(/\\.([^.]+)$/)?.[1] ?? \"\";\n const kind = EXT_KIND[ext];\n if (!kind) continue;\n try {\n const content = await readFileWithKind(f.absPath, kind);\n if (!content || content.trim().length < 5) continue; // 跳过空/太短文件(中文 4-5 字也算有效)\n const lang = detectLang(f.relPath);\n docs.push({\n path: f.relPath,\n title: inferTitle(f.relPath),\n content,\n lang,\n kind,\n });\n } catch {\n // 单文件失败跳过(如损坏 PDF),不阻塞整体扫描\n }\n }\n\n // 中文优先去重:同 dedupKey 的文件,优先级 zh > en > other\n const dedupedDocs = dedupByLang(docs);\n\n // 不收图 → 直接返回(向后兼容)\n if (!options?.collectImages) {\n return dedupedDocs;\n }\n\n // === 收图 ===\n\n // 1. 独立图片文件\n const fileImages: ScannedImage[] = imageFiles.map((f) => {\n const ext = f.relPath.toLowerCase().match(/\\.([^.]+)$/)?.[1] ?? \"\";\n return {\n path: f.relPath,\n absPath: f.absPath,\n title: inferImageTitle(f.relPath),\n mime: IMAGE_EXT_MIME[ext] ?? \"image/png\",\n source: \"image_file\" as const,\n altText: inferImageTitle(f.relPath),\n };\n });\n\n // 2. markdown 图片引用(从 .md/.html 文档正文解析)\n const refImages: ScannedImage[] = [];\n for (const doc of dedupedDocs) {\n // 所有格式解析后都已转成 markdown,图片引用统一用 ![](path) 或 <img> 语法\n // txt 可能含裸路径但不常见,跳过;html 走 htmlToText 后图片标签已丢\n if (doc.kind === \"txt\" || doc.kind === \"html\") continue;\n const refs = extractImageRefs(doc.content);\n for (const ref of refs) {\n const resolvedPath = resolveImageRef(ref.refPath, doc.path);\n // 跳过已被独立文件覆盖的(去重后做)\n const ext = resolvedPath.toLowerCase().match(/\\.([^.]+)$/)?.[1] ?? \"\";\n refImages.push({\n path: resolvedPath,\n absPath: join(rootDir, resolvedPath),\n title: ref.alt || inferImageTitle(resolvedPath),\n mime: IMAGE_EXT_MIME[ext] ?? \"image/png\",\n source: \"markdown_ref\" as const,\n altText: ref.alt || inferImageTitle(resolvedPath),\n });\n }\n }\n\n // 去重:同 path 只留一份(file 优先)\n const dedupedFileAndRefImages = dedupImages(fileImages, refImages);\n\n // 3. PDF 内嵌图片提取(纯文字 PDF 无图;混合/纯图片 PDF 有图)\n const pdfImages: ScannedImage[] = [];\n for (const doc of dedupedDocs) {\n if (doc.kind !== \"pdf\") continue;\n try {\n const { processPdf } = await import(\"../../lib/pdf-renderer.js\");\n const pdfBuf = await readFile(join(rootDir, doc.path));\n const result = await processPdf(pdfBuf);\n for (const img of result.images) {\n pdfImages.push({\n path: `${doc.path}#page${img.pageNumber}.png`,\n absPath: \"\", // buffer 型,无源文件\n title: `${doc.title} - 图(第${img.pageNumber}页)`,\n mime: img.mimeType,\n source: \"pdf_page\" as const,\n altText: `${doc.title} 第${img.pageNumber}页`,\n buffer: img.buffer,\n pageNumber: img.pageNumber,\n });\n }\n } catch {\n // PDF 图片提取失败跳过(文字已在 doc.content 里)\n }\n }\n\n // 3b. PPTX 内嵌图片提取(每 slide 的图片对象)\n // 复用 source=\"pdf_page\"(都是 buffer 提取的文档图 + 带 page/slide 号);不新增\n // \"pptx_slide\" kind —— schema.sql node_assets 有 CHECK 约束, 改了存量 DB 迁移不了。\n // pageNumber 存 slideNumber。语义小瑕疵(slide 复用 pdf_page 标签)用此注释说明。\n const pptxImages: ScannedImage[] = [];\n for (const doc of dedupedDocs) {\n if (doc.kind !== \"pptx\") continue;\n try {\n const { parsePptx } = await import(\"../../lib/pptx-parser.js\");\n const pptxBuf = await readFile(join(rootDir, doc.path));\n const result = await parsePptx(pptxBuf);\n for (const img of result.images) {\n pptxImages.push({\n path: `${doc.path}#slide${img.slideNumber}.png`,\n absPath: \"\", // buffer 型, 无源文件\n title: `${doc.title} - 图(第${img.slideNumber}页)`,\n mime: img.mimeType,\n source: \"pdf_page\" as const, // 复用(见上注释)\n altText: `${doc.title} 第${img.slideNumber}页`,\n buffer: img.buffer,\n pageNumber: img.slideNumber,\n });\n }\n } catch {\n // PPTX 图片提取失败跳过(文字已在 doc.content 里)\n }\n }\n\n // 4. ipynb output 图片提取(notebook 的 code cell 执行输出图)\n const notebookImages: ScannedImage[] = [];\n for (const doc of dedupedDocs) {\n if (!doc.path.toLowerCase().endsWith(\".ipynb\")) continue;\n try {\n const { parseNotebook } = await import(\"./notebook-parser.js\");\n const nbRaw = await readFile(join(rootDir, doc.path), \"utf8\");\n const nbResult = parseNotebook(nbRaw);\n for (const img of nbResult.images) {\n const buf = Buffer.from(img.base64, \"base64\");\n notebookImages.push({\n path: `${doc.path}#cell${img.cellIndex}.png`,\n absPath: \"\", // buffer 型\n title: `${doc.title} - 输出图(cell ${img.cellIndex})`,\n mime: img.mimeType,\n source: \"image_file\" as const, // 复用 image_file 类型(buffer 型)\n altText: img.altText,\n buffer: buf,\n });\n }\n } catch {\n // notebook 图片提取失败跳过(文字已在 doc.content 里)\n }\n }\n\n // 全部图片合并(PDF/notebook 图用唯一 path,不会和文件图冲突)\n const images = [...dedupedFileAndRefImages, ...pdfImages, ...pptxImages, ...notebookImages];\n\n return { docs: dedupedDocs, images };\n}\n\n/**\n * 同语言类别内部去重(保留双语配对)。\n *\n * 历史:旧版是跨语言的\"中文优先\"(同 dedupKey 只留 zh)——那是翻译管线诞生前的\n * hack,xxx.en.txt / xxx.zh-CN.txt 成对时英文原稿被直接丢掉,双语信息在扫描层\n * 就没了,翻译管线永远拿不到配对。现在分类层(excludeSuffixTranslations 规则\n * 分流 + LLM translation 角色)负责把成对双语分流为 原文+翻译,所以扫描器必须\n * 把配对双方都保留,只合并同一语言类别内部的真重复(如 08.en.txt vs 08.en.md)。\n */\nexport function dedupByLang(docs: ScannedDoc[]): ScannedDoc[] {\n const byKey = new Map<string, ScannedDoc>();\n for (const d of docs) {\n const key = `${dedupKey(d.path)}|${d.lang}`;\n if (!byKey.has(key)) byKey.set(key, d); // 同语言同 key 保留首个(docs 已按自然序排好)\n }\n // 保持原顺序\n return docs.filter((d) => byKey.get(`${dedupKey(d.path)}|${d.lang}`) === d);\n}\n\n/* ============================================================\n * 图片收集(多模态 flag on 时启用)\n * ============================================================ */\n\n/** markdown 图片引用提取结果 */\nexport interface MarkdownImageRef {\n /** 原始 alt 文本 */\n alt: string;\n /** 引用路径(markdown 里的原始写法,如 ./img.png 或 ../assets/fig.png) */\n refPath: string;\n}\n\n/**\n * 从 markdown 内容里提取图片引用 ![alt](path)。\n * 纯函数,便于测试。\n *\n * 解析规则:\n * - 匹配 ![可选alt](路径) 格式\n * - 去掉路径里的锚点和查询参数后缀\n * - 只保留图片扩展名(.png/.jpg/.jpeg/.gif/.webp/.svg/.bmp)\n * - 跳过 http(s) 绝对 URL(这些是外部资源,本地没有文件)\n * - 跳过 data: URL\n */\nexport function extractImageRefs(md: string): MarkdownImageRef[] {\n const refs: MarkdownImageRef[] = [];\n const seen = new Set<string>();\n\n // 1. Markdown 语法 ![alt](url)\n const mdPattern = /!\\[([^\\]]*)\\]\\(([^)]+)\\)/g;\n let m: RegExpExecArray | null;\n while ((m = mdPattern.exec(md)) !== null) {\n const alt = m[1].trim();\n let url = m[2].trim();\n // 去空格和标题(如 ![alt](path \"title\"))\n const titleMatch = url.match(/\\s+\"[^\"]*\"$/);\n if (titleMatch) url = url.slice(0, titleMatch.index).trim();\n // 去锚点\n url = url.split(\"#\")[0];\n // 跳过外部 URL 和 data URL\n if (!url || url.startsWith(\"http://\") || url.startsWith(\"https://\") || url.startsWith(\"data:\")) continue;\n // 只留图片扩展名\n const ext = url.toLowerCase().match(/\\.([^.]+)$/)?.[1] ?? \"\";\n if (!(ext in IMAGE_EXT_MIME)) continue;\n const key = alt + \"|\" + url;\n if (seen.has(key)) continue;\n seen.add(key);\n refs.push({ alt, refPath: url });\n }\n\n // 2. HTML <img> 标签(src='...' 或 src=\"...\")\n // 覆盖微软课程仓库常见的 <img src='images/xxx.png' alt='描述'/>\n // 两步法:先提取 <img ...> 整标签,再独立提取 src 和 alt(属性顺序无关)\n const htmlPattern = /<img\\s+[^>]*>/gi;\n let hm: RegExpExecArray | null;\n while ((hm = htmlPattern.exec(md)) !== null) {\n const tag = hm[0];\n const url = (tag.match(/src=['\"]([^'\"]+)['\"]/i)?.[1] ?? \"\").trim().split(\"#\")[0];\n const alt = (tag.match(/alt=['\"]([^'\"]*)['\"]/i)?.[1] ?? \"\").trim();\n if (!url || url.startsWith(\"http://\") || url.startsWith(\"https://\") || url.startsWith(\"data:\")) continue;\n const ext = url.toLowerCase().match(/\\.([^.]+)$/)?.[1] ?? \"\";\n if (!(ext in IMAGE_EXT_MIME)) continue;\n const key = alt + \"|\" + url;\n if (seen.has(key)) continue;\n seen.add(key);\n refs.push({ alt: alt || (url.split(\"/\").pop() ?? url), refPath: url });\n }\n\n return refs;\n}\n\n/**\n * 把 markdown 图片引用解析成相对于扫描根目录的路径。\n * 处理 ./ ../ 等相对引用。\n *\n * @param refPath markdown 里的原始引用(如 ./img.png)\n * @param docRelPath 引用所在文档的相对路径(如 ch1/lesson1/notes.md)\n * @returns 相对根目录的标准化路径(如 ch1/lesson1/img.png),用 / 分隔\n *\n * 纯函数,便于测试。\n */\nexport function resolveImageRef(refPath: string, docRelPath: string): string {\n const docDir = dirname(docRelPath).replace(/\\\\/g, \"/\");\n // 统一用 / 分隔(Windows \\ 路径归一)\n const normalized = refPath.replace(/\\\\/g, \"/\").replace(/^\\.\\//, \"\");\n // 相对引用(含 ./ ../ 纯文件名 子目录)→ 相对 docDir 解析。\n // 用纯字符串拼接(不依赖 node:path 的盘符行为,跨平台一致)。\n const parts = docDir === \".\" ? [] : docDir.split(\"/\").filter(Boolean);\n const refParts = normalized.split(\"/\");\n for (const p of refParts) {\n if (p === \"..\") parts.pop();\n else if (p !== \".\" && p !== \"\") parts.push(p);\n }\n return parts.join(\"/\");\n}\n\n/** 从图片文件名推断 alt 文本(去扩展名 + 数字前缀) */\nexport function inferImageTitle(filename: string): string {\n let name = basename(filename);\n name = name.replace(/\\.(png|jpe?g|gif|webp|svg|bmp)$/i, \"\");\n name = name.replace(/^(\\d+[_-]\\s*)/, \"\");\n name = name.replace(/[-_]+/g, \" \").trim();\n if (/^[a-z]/.test(name)) name = name.charAt(0).toUpperCase() + name.slice(1);\n return name || basename(filename);\n}\n\n/**\n * 把独立图片文件 + markdown 引用合并去重。\n * 去重规则:按相对根目录路径归一。同一图既被 .md 引用又是独立文件 → 只留一份(image_file 优先,因为它肯定存在)。\n *\n * 纯函数,便于测试。\n */\nexport function dedupImages(\n fileImages: ScannedImage[],\n refImages: ScannedImage[],\n): ScannedImage[] {\n const seen = new Map<string, ScannedImage>();\n // 先放 file(优先),再放 ref(补充未匹配的)\n for (const img of fileImages) {\n if (!seen.has(img.path)) seen.set(img.path, img);\n }\n for (const img of refImages) {\n if (!seen.has(img.path)) seen.set(img.path, img);\n }\n return Array.from(seen.values());\n}\n\n/* ---------- 内部辅助 ---------- */\n\nasync function walkDir(root: string, current: string, acc: { absPath: string; relPath: string; isImage: boolean }[]): Promise<void> {\n let entries: import(\"node:fs\").Dirent[];\n try {\n entries = await readdir(current, { withFileTypes: true });\n } catch {\n return;\n }\n for (const entry of entries) {\n if (EXCLUDED_DIRS.has(entry.name)) continue;\n const abs = join(current, entry.name);\n if (entry.isDirectory()) {\n await walkDir(root, abs, acc);\n } else if (entry.isFile()) {\n const ext = entry.name.toLowerCase().match(/\\.([^.]+)$/)?.[1] ?? \"\";\n if (ext in EXT_KIND) {\n const rel = relative(root, abs).split(sep).join(\"/\");\n acc.push({ absPath: abs, relPath: rel, isImage: false });\n } else if (ext in IMAGE_EXT_MIME) {\n const rel = relative(root, abs).split(sep).join(\"/\");\n acc.push({ absPath: abs, relPath: rel, isImage: true });\n }\n }\n }\n}\n\nasync function readFileWithKind(absPath: string, kind: ScannedDoc[\"kind\"]): Promise<string> {\n if (kind === \"pdf\") {\n // 优先 pdf-inspector(layout-aware markdown), 失败/平台不支持回退 pdf-parse。\n // 路由 + 兜底集中在 lib/pdf-text.ts(平台缺预编译时 require 会抛, 不能让导入挂)。\n const buf = await readFile(absPath);\n const { parsePdfText } = await import(\"../../lib/pdf-text.js\");\n return parsePdfText(buf);\n }\n if (kind === \"pptx\") {\n // .pptx → officeparser AST → markdown(每 slide 一个 ##, 讲者备注随 slide 走)。\n // 现有导入管线按 ## 切, 自动每 slide 一节课。图片在下面 pptxImages 循环单独提取。\n const buf = await readFile(absPath);\n const { parsePptx } = await import(\"../../lib/pptx-parser.js\");\n return (await parsePptx(buf)).markdown;\n }\n if (kind === \"ipynb\") {\n // .ipynb 是 JSON,用 notebook-parser 转成 markdown(markdown cell + code block)\n const raw = await readFile(absPath, \"utf8\");\n const { parseNotebook } = await import(\"./notebook-parser.js\");\n const result = parseNotebook(raw);\n return result.markdown;\n }\n if (kind === \"rst\" || kind === \"rmd\" || kind === \"org\" || kind === \"adoc\") {\n // 非 markdown 标记格式 → 用各自解析器转 markdown\n const raw = await readFile(absPath, \"utf8\");\n const parser = { rst: \"rst-parser\", rmd: \"rmd-parser\", org: \"org-parser\", adoc: \"adoc-parser\" }[kind];\n if (parser) {\n try {\n const mod = await import(`./${parser}.js`);\n const fn = mod.parseRst ?? mod.parseRmd ?? mod.parseOrg ?? mod.parseAdoc;\n return fn(raw).markdown;\n } catch {\n return raw; // 解析失败 → 当纯文本\n }\n }\n return raw;\n }\n if (kind === \"code\") {\n // 代码文件 → code-parser 转 markdown(docstring + 代码围栏)\n const raw = await readFile(absPath, \"utf8\");\n const ext = absPath.toLowerCase().match(/\\.([^.]+)$/)?.[1] ?? \"\";\n try {\n const { parseCode } = await import(\"./code-parser.js\");\n return parseCode(raw, ext).markdown;\n } catch {\n return \"```\\n\" + raw + \"\\n```\"; // 解析失败 → 纯代码围栏\n }\n }\n const raw = await readFile(absPath, \"utf8\");\n return kind === \"html\" ? htmlToText(raw) : raw;\n}\n\n/** 路径自然排序:按段拆分,数字段按数值比较(02_ 在 10_ 前,不是字典序)。 */\nfunction naturalPathCompare(a: string, b: string): number {\n const pa = a.split(\"/\");\n const pb = b.split(\"/\");\n for (let i = 0; i < Math.min(pa.length, pb.length); i++) {\n const na = pa[i]!.match(/^(\\d+)/)?.[1];\n const nb = pb[i]!.match(/^(\\d+)/)?.[1];\n if (na && nb && na !== nb) return Number(na) - Number(nb);\n if (pa[i] !== pb[i]) return pa[i]! < pb[i]! ? -1 : 1;\n }\n return pa.length - pb.length;\n}\n\n/* ============================================================\n * 本地导入清点 (buildLocalInventory) —— 供新 5 步管线的 Step 1\n *\n * scanFolder 只管扫描文档+图片(不含 translations/)。\n * buildLocalInventory 在此基础上补全:\n * - translations/{lang}/ 扫描 → 翻译文件 + 检测到的语言\n * - README 检测(根目录 README.md / index.md / 首个 md)\n * - fullTree(所有路径,给 LLM 看仓库结构)\n * - standaloneImages(不被任何 md 引用的独立图片文件)\n * ============================================================ */\n\n/** 本地导入清点结果 */\nexport interface LocalInventory {\n /** 文档(非翻译,已去重) */\n docs: ScannedDoc[];\n /** 图片(独立文件 + md 引用 + PDF/notebook 提取) */\n images: ScannedImage[];\n /** 翻译文件(path = translations/{lang}/{原路径}) */\n translations: ScannedDoc[];\n /** 检测到的翻译语言代码(如 [\"zh-CN\", \"ja\"]) */\n translationLangs: string[];\n /** README 全文(根目录 README.md/index.md,无则首个 md,再无则 \"\") */\n readmeMd: string;\n /** 完整目录树(所有文件路径,给 LLM 看结构) */\n fullTree: string[];\n /** 不被任何文档引用的独立图片文件(给 LLM Step4 关联到 lesson 用) */\n standaloneImages: ScannedImage[];\n}\n\n/**\n * 为新管线构建本地清点:scanFolder + translations + README + fullTree + standaloneImages。\n *\n * 和 GitHub 的 fetchRepoInventory 对齐:产出 readmeMd + fileList(隐含在 docs 里) +\n * fullTree,供 classifyFileRoles + designCourseStructure 使用。\n */\nexport async function buildLocalInventory(\n rootDir: string,\n onProgress?: (scanned: number, currentPath: string) => void,\n): Promise<LocalInventory> {\n // 1. 扫描文档 + 图片(scanFolder 内部排除 translations/,不影响)\n const scanResult = await scanFolder(rootDir, onProgress, { collectImages: true });\n // collectImages:true → 返回 { docs, images }(不是 ScannedDoc[])\n const { docs, images } = Array.isArray(scanResult) ? { docs: scanResult, images: [] } : scanResult;\n\n // 2. 扫描 translations/ 目录(单独扫,不进 docs)\n const { translations, translationLangs } = await scanTranslationsDir(rootDir);\n\n // 3. README 检测\n const readmeMd = findReadmeContent(docs);\n\n // 4. fullTree(所有文件路径,含翻译 + 图片)\n const fullTree = [\n ...docs.map((d) => d.path),\n ...images.map((i) => i.path),\n ...translations.map((t) => t.path),\n ];\n\n // 5. 不被引用的独立图片\n const standaloneImages = findStandaloneImages(images, docs);\n\n return { docs, images, translations, translationLangs, readmeMd, fullTree, standaloneImages };\n}\n\n/**\n * 扫描 translations/{lang}/ 目录。\n * 每个 lang 子目录对应一种翻译语言,其下的文件按原目录结构保留。\n * path = translations/{lang}/{相对 lang 目录的路径}。\n */\nasync function scanTranslationsDir(\n rootDir: string,\n): Promise<{ translations: ScannedDoc[]; translationLangs: string[] }> {\n const translationsDir = join(rootDir, \"translations\");\n if (!existsSync(translationsDir)) {\n return { translations: [], translationLangs: [] };\n }\n\n let langEntries: import(\"node:fs\").Dirent[];\n try {\n langEntries = await readdir(translationsDir, { withFileTypes: true });\n } catch {\n return { translations: [], translationLangs: [] };\n }\n\n const langs = langEntries.filter((e) => e.isDirectory()).map((e) => e.name);\n const translations: ScannedDoc[] = [];\n\n for (const lang of langs) {\n const langDir = join(translationsDir, lang);\n const transFiles: { absPath: string; relPath: string; isImage: boolean }[] = [];\n await walkDir(langDir, langDir, transFiles);\n\n for (const f of transFiles) {\n if (f.isImage) continue;\n const ext = f.relPath.toLowerCase().match(/\\.([^.]+)$/)?.[1] ?? \"\";\n const kind = EXT_KIND[ext];\n if (!kind) continue;\n try {\n const content = await readFileWithKind(f.absPath, kind);\n if (!content || content.trim().length < 5) continue;\n translations.push({\n path: `translations/${lang}/${f.relPath}`,\n title: inferTitle(f.relPath),\n content,\n lang: detectLang(f.relPath),\n kind,\n });\n } catch {\n // 单文件失败跳过\n }\n }\n }\n\n return { translations, translationLangs: langs };\n}\n\n/**\n * 从已扫描文档里找 README 全文。\n * 优先根目录 README.md/README.markdown,其次 index.md,再首个 md,都没有返回 \"\"。\n */\nfunction findReadmeContent(docs: ScannedDoc[]): string {\n // 根目录 README.md / README.markdown\n const readme = docs.find((d) => {\n const parts = d.path.split(\"/\");\n return parts.length === 1 && /^readme\\.(md|markdown)$/i.test(parts[0]!);\n });\n if (readme) return readme.content;\n\n // 根目录 index.md\n const index = docs.find((d) => {\n const parts = d.path.split(\"/\");\n return parts.length === 1 && /^index\\.(md|markdown)$/i.test(parts[0]!);\n });\n if (index) return index.content;\n\n // 首个 md 文档\n const firstMd = docs.find((d) => d.kind === \"md\");\n return firstMd?.content ?? \"\";\n}\n\n/**\n * 找出不被任何文档引用的独立图片文件。\n * 这些是\"孤儿\"图片,需要 LLM 在 Step 4 关联到最相关的 lesson。\n *\n * 判定:source=image_file(独立文件,非 PDF/notebook 提取) + 有 absPath(磁盘文件) +\n * 不在任何文档的图片引用路径里。\n */\nexport function findStandaloneImages(images: ScannedImage[], docs: ScannedDoc[]): ScannedImage[] {\n // 收集所有文档引用的图片路径\n const referencedPaths = new Set<string>();\n for (const doc of docs) {\n if (doc.kind === \"txt\" || doc.kind === \"html\") continue;\n const refs = extractImageRefs(doc.content);\n for (const ref of refs) {\n const resolved = resolveImageRef(ref.refPath, doc.path);\n referencedPaths.add(resolved);\n }\n }\n\n return images.filter(\n (img) => img.source === \"image_file\" && img.absPath && !referencedPaths.has(img.path),\n );\n}\n","// Vendored from LookatStudy src/main/services/pure/file-classifier.ts (MIT License, https://github.com/kaiji/LookatStudy).\n// Unmodified except this provenance header. PDF/PPTX branches resolve unavailable optional libs and are skipped per upstream try/catch.\n/**\r\n * 课时文件分类器 —— 规则引擎 + LLM 兜底两阶段分类。\r\n *\r\n * 设计理念(见 dev-docs 讨论与种子构建经验):\r\n * - 规则只判**高置信度**的(路径明确的 lab/翻译/notebook/license 等)\r\n * - 不确定的标 `uncertain`,`keepAsLesson: true`,显式交给 LLM 在\r\n * `analyzeCourseStructure` 里先分类(keep/skip)再排结构\r\n * - 不做死规则覆盖一切——边界 case 太多时规则会臃肿且脆弱\r\n *\r\n * 级联模式镜像 `classifyLlmError`(llm-client.ts)—— first-match-wins。\r\n *\r\n * 零依赖(纯函数),归 services/pure/。\r\n */\r\n\r\n/** 文件角色(分类标签) */\r\nexport type FileRole =\r\n | \"lesson\" // 确定的课时正文\r\n | \"notebook\" // Jupyter notebook(独立成 lesson 不合适,但正文有代码价值)\r\n | \"lab\" // 配套练习/作业\r\n | \"section-intro\" // 章节介绍页(同 section 有更深的 lesson)\r\n | \"translation\" // 翻译副本\r\n | \"meta\" // 仓库元数据(LICENSE/CONTRIBUTING 等)\r\n | \"example\" // 示例代码\r\n | \"uncertain\"; // 规则无法确定,交给 LLM\r\n\r\n/** 置信度 */\r\nexport type Confidence = \"high\" | \"low\";\r\n\r\n/** 两个世界(与 shared/types.ts World 对齐,这里独立声明避免环引用) */\r\nexport type World = \"study\" | \"practice\";\r\n\r\n/** 分类结果 */\r\nexport interface FileClassification {\r\n role: FileRole;\r\n confidence: Confidence;\r\n /** 人话解释,供审计/调试/进度提示 */\r\n reason: string;\r\n /**\r\n * 是否进 lesson 列表。\r\n * - 高置信度 lesson / uncertain → true(uncertain 先留,让 LLM 定)\r\n * - 高置信度噪声(translation/meta/lab/example/notebook/section-intro)→ false\r\n */\r\n keepAsLesson: boolean;\r\n /**\r\n * 属于哪个世界。\r\n * - null = 未定(uncertain),由 LLM 在 course-structure-service 判\r\n * - \"study\" = 高置信度判定为学习讲解(section-intro 等)\r\n * - \"practice\" = (当前规则不直接判 practice,留给 LLM)\r\n */\r\n world: World | null;\r\n}\r\n\r\n/** 分类上下文:同一批次所有文件的路径(用于 section-intro 判断) */\r\nexport interface ClassifyContext {\r\n siblingPaths: string[];\r\n}\r\n\r\n/** 仓库元数据文件名(忽略大小写,匹配文件名 stem) */\r\nconst META_FILE_NAMES = new Set([\r\n \"license\", \"licence\", \"contributing\", \"code_of_conduct\", \"security\", \"changelog\",\r\n \"authors\", \"maintainers\",\r\n \"pull_request_template\", \"issue_template\", \"support\", \"citation\",\r\n]);\r\n\r\n/** 配套练习目录/文件名关键词(路径含这些子串即判定) */\r\nconst LAB_KEYWORDS = [\"/lab/\", \"/labs/\", \"/exercise/\", \"/exercises/\", \"/assignment/\", \"/assignments/\", \"/quiz/\", \"/quizzes/\", \"/homework/\", \"/practice/\", \"/solution/\", \"labs/\", \"exercises/\", \"assignments/\"];\r\n\r\n/** 示例代码目录关键词(路径含这些子串即判定,含根目录开头) */\r\nconst EXAMPLE_KEYWORDS = [\"/examples/\", \"/example/\", \"/demo/\", \"/demos/\", \"/samples/\", \"/sample/\", \"examples/\", \"example/\", \"demo/\", \"demos/\", \"samples/\"];\r\n\r\n/**\r\n * 判断一个文件是否是 section-intro:它是某个 section 的 README.md,\r\n * 且同 section 下有**更深一级的 README.md lesson**(不是 lab/notebook)。\r\n *\r\n * 例:\r\n * `lessons/3-NN/README.md` 是 section-intro ← 因为有 `lessons/3-NN/03-Perceptron/README.md`\r\n * `lessons/3-NN/03-Perceptron/README.md` 不是 ← 虽然 03-Perceptron/ 下有 lab/README.md,\r\n * 但 lab 不是 lesson,不能用来判定 lesson 是 intro\r\n */\r\nfunction isSectionIntro(path: string, siblingPaths: string[]): boolean {\r\n const parts = path.split(\"/\").filter(Boolean);\r\n const last = parts[parts.length - 1];\r\n // 必须是 README.md / index.md\r\n if (!last || !(/^readme/i.test(last) || last === \"index.md\")) return false;\r\n // 当前深度\r\n const myDepth = parts.length;\r\n if (myDepth < 3) return false; // 太浅不可能是 section-intro\r\n // section 前缀(去掉末尾 README)\r\n const prefix = parts.slice(0, -1).join(\"/\");\r\n // 找同 section 下更深一级的**真正 lesson README**(排除 lab/notebook/exercise 等噪声)\r\n const hasDeeperLesson = siblingPaths.some((sib) => {\r\n if (sib === path) return false;\r\n const sibLower = sib.toLowerCase();\r\n // 排除噪声路径\r\n if (sibLower.includes(\"/lab/\") || sibLower.includes(\"/exercise/\") || sibLower.includes(\"/assignment/\")) return false;\r\n if (sibLower.endsWith(\".ipynb\")) return false;\r\n const sibParts = sib.split(\"/\").filter(Boolean);\r\n const sibPrefix = sibParts.slice(0, -1).join(\"/\");\r\n // 同 section 且更深(`prefix/NN-Lesson/README.md` vs `prefix/README.md`)\r\n return sibPrefix.startsWith(prefix + \"/\") && sibParts.length > myDepth;\r\n });\r\n return hasDeeperLesson;\r\n}\r\n\r\n/**\r\n * 主分类函数:first-match-wins 级联规则。\r\n *\r\n * @param path 文件路径(相对 repo 根,/ 分隔)\r\n * @param md 文件正文(已转成 markdown)\r\n * @param context 分类上下文(siblingPaths = 同批次所有文件路径)\r\n */\r\nexport function classifyFile(\r\n path: string,\r\n _md: string,\r\n context: ClassifyContext,\r\n): FileClassification {\r\n const lowerPath = path.toLowerCase();\r\n const parts = path.split(\"/\").filter(Boolean);\r\n const filename = parts[parts.length - 1] ?? path;\r\n const stem = filename.replace(/\\.[^.]+$/, \"\").toLowerCase();\r\n\r\n // ── 规则 1: 翻译副本 ──\r\n if (lowerPath.includes(\"translations/\") || lowerPath.includes(\"translated_images/\")) {\r\n return { role: \"translation\", confidence: \"high\", keepAsLesson: false, world: null,\r\n reason: \"路径含 translations/,是翻译副本\" };\r\n }\r\n\r\n // ── 规则 2: 仓库元数据 ──\r\n if (META_FILE_NAMES.has(stem)) {\r\n return { role: \"meta\", confidence: \"high\", keepAsLesson: false, world: null,\r\n reason: `文件名 ${stem} 是仓库元数据` };\r\n }\r\n\r\n // ── 规则 3: Jupyter notebook → uncertain(notebook 可能是主课程)──\r\n if (lowerPath.endsWith(\".ipynb\")) {\r\n return { role: \"uncertain\", confidence: \"low\", keepAsLesson: true, world: null,\r\n reason: \".ipynb notebook——可能是主课程(fast.ai/d2l 风格)也可能是补充代码,交给 LLM 判断\" };\r\n }\r\n\r\n // ── 规则 4: 配套练习 → uncertain(exercise 可能就是课时正文)──\r\n for (const kw of LAB_KEYWORDS) {\r\n if (lowerPath.includes(kw)) {\r\n return { role: \"uncertain\", confidence: \"low\", keepAsLesson: true, world: null,\r\n reason: `路径含 ${kw}——可能是配套练习也可能是课时正文,交给 LLM 判断` };\r\n }\r\n }\r\n\r\n // ── 规则 5: 示例代码 → uncertain(example 可能就是课时正文)──\r\n for (const kw of EXAMPLE_KEYWORDS) {\r\n if (lowerPath.includes(kw)) {\r\n return { role: \"uncertain\", confidence: \"low\", keepAsLesson: true, world: null,\r\n reason: `路径含 ${kw}——可能是示例代码也可能是课时正文,交给 LLM 判断` };\r\n }\r\n }\r\n\r\n // ── 规则 6: section-intro(章节介绍页)──\r\n if (isSectionIntro(path, context.siblingPaths)) {\r\n return { role: \"section-intro\", confidence: \"high\", keepAsLesson: false, world: \"study\",\r\n reason: \"章节介绍页(同 section 有更深的 lesson 文件)\" };\r\n }\r\n\r\n // ── fallback: 不确定,交给 LLM ──\r\n // 所有未被高置信度规则命中的文件,统一标 uncertain 交给 LLM 判断。\r\n // 不再用 proseChars<200 阈值细分——那个分支和 fallback 返回完全一样,是死代码。\r\n return { role: \"uncertain\", confidence: \"low\", keepAsLesson: true, world: null,\r\n reason: \"规则未命中高置信度分类,交给 LLM 判断\" };\r\n}\r\n\r\n/**\r\n * 批量分类便捷函数:一次性给所有文件分类(siblingPaths 自动填充)。\r\n */\r\nexport function classifyFiles(\r\n files: { path: string; md: string }[],\r\n): Array<{ path: string; md: string; classification: FileClassification }> {\r\n const allPaths = files.map((f) => f.path);\r\n return files.map((f) => ({\r\n path: f.path,\r\n md: f.md,\r\n classification: classifyFile(f.path, f.md, { siblingPaths: allPaths }),\r\n }));\r\n}\r\n\r\n/**\r\n * 统计分类结果(供进度提示 / 调试)。\r\n */\r\nexport function summarizeClassifications(\r\n classifications: FileClassification[],\r\n): { byRole: Record<string, number>; keepCount: number; skipCount: number; uncertainCount: number } {\r\n const byRole: Record<string, number> = {};\r\n let keepCount = 0;\r\n let skipCount = 0;\r\n let uncertainCount = 0;\r\n for (const c of classifications) {\r\n byRole[c.role] = (byRole[c.role] ?? 0) + 1;\r\n if (c.keepAsLesson) keepCount++;\r\n else skipCount++;\r\n if (c.role === \"uncertain\") uncertainCount++;\r\n }\r\n return { byRole, keepCount, skipCount, uncertainCount };\r\n}\r\n","// Vendored from LookatStudy src/main/services/pure/repo-fetcher.ts (MIT License, https://github.com/kaiji/LookatStudy).\n// Unmodified except this provenance header. PDF/PPTX branches resolve unavailable optional libs and are skipped per upstream try/catch.\n/**\r\n * 仓库导入器 —— 从学习型 GitHub 仓库构建课程结构。\r\n *\r\n * 核心策略:不依赖文件列表 API(api.github.com / api.jsdelivr.net 在很多网络环境下不可达),\r\n * 而是从 README.md 的 markdown 内部链接发现课程结构。\r\n *\r\n * 学习仓库的 README 通常有完整的课程大纲,链接指向每个课时:\r\n * - 形态 A(课程型): 链接指向 lessons/N-Topic/README.md + .ipynb\r\n * - 形态 B(单文件型): README 本身是超长文档,无子文件链接\r\n *\r\n * 数据源: cdn.jsdelivr.net/gh/{owner}/{repo}@{branch}/{path}(全球 CDN,无速率限制,\r\n * 在大多数网络环境下可用,包括 raw.githubusercontent.com 被墙的情况)\r\n *\r\n * 纯函数设计: fetchFn 由调用方注入(生产用 global fetch,测试用 mock)。\r\n */\r\nimport { parseMarkdownToCourse, type ParsedCourse, type ParsedSection, type ParsedLesson } from \"./markdown-course.js\";\r\nimport { classifyFile, type FileClassification } from \"./file-classifier.js\";\r\nimport https from \"node:https\";\r\n\r\n/** 仓库文件条目(从 README 链接发现) */\r\nexport interface DiscoveredFile {\r\n path: string;\r\n /** 链接文本(课时标题) */\r\n title: string;\r\n /** 文件类型: md 正文 / ipynb notebook / rst / rmd / org / adoc / code / other */\r\n kind: \"md\" | \"ipynb\" | \"rst\" | \"rmd\" | \"org\" | \"adoc\" | \"code\" | \"other\";\r\n}\r\n\r\n/** 仓库检测结果 */\r\nexport type RepoPattern = \"course\" | \"well-organized\" | \"single-file\" | \"docs-rich\" | \"unsupported\";\r\n\r\nexport interface DetectionResult {\r\n pattern: RepoPattern;\r\n reason: string;\r\n /** course 模式: 从 README 链接发现的课时文件 */\r\n lessonFiles?: DiscoveredFile[];\r\n /** 单文件模式: README 本身的正文长度 */\r\n readmeLength?: number;\r\n}\r\n\r\n/** 拉取结果 */\r\nexport interface FetchedFile {\r\n path: string;\r\n title: string;\r\n md: string;\r\n /** 文件分类(由 classifyFile 填充,buildCourseFromFiles 用于决定是否进 lesson 列表) */\r\n classification?: FileClassification;\r\n}\r\n\r\nexport interface FetchResult {\r\n ok: FetchedFile[];\r\n failed: { path: string; error: string }[];\r\n}\r\n\r\n/** CDN URL 构造 */\r\nexport function cdnUrl(owner: string, repo: string, branch: string, path: string): string {\r\n const cleanPath = path.replace(/^\\.\\//, \"\").replace(/^\\//, \"\");\r\n return `https://cdn.jsdelivr.net/gh/${owner}/${repo}@${branch}/${cleanPath}`;\r\n}\r\n\r\n/** 代码文件扩展名(代码即教学内容) */\r\nconst CODE_EXTENSIONS = [\r\n \".py\", \".js\", \".jsx\", \".ts\", \".tsx\", \".mjs\", \".cjs\",\r\n \".go\", \".rs\", \".java\", \".kt\", \".kts\", \".scala\",\r\n \".c\", \".h\", \".cpp\", \".cc\", \".cxx\", \".hpp\",\r\n \".cs\", \".rb\", \".php\", \".swift\",\r\n \".sh\", \".bash\", \".zsh\", \".ps1\",\r\n \".lua\", \".r\", \".jl\", \".dart\",\r\n \".clj\", \".ex\", \".exs\", \".erl\", \".hs\", \".ml\", \".fs\",\r\n \".sql\", \".pl\", \".elm\",\r\n];\r\n\r\n/**\r\n * 从 README 的 markdown 链接提取内部文件引用。\r\n * 只看相对路径(非 http/锚点),且指向 .md/.ipynb 文件。\r\n */\r\nexport function extractInternalLinks(readmeMd: string): DiscoveredFile[] {\r\n const linkPattern = /\\[([^\\]]*)\\]\\(([^)]+)\\)/g;\r\n const seen = new Set<string>();\r\n const files: DiscoveredFile[] = [];\r\n let m;\r\n while ((m = linkPattern.exec(readmeMd)) !== null) {\r\n const title = m[1].trim();\r\n let href = m[2].trim();\r\n // 去掉锚点部分\r\n href = href.split(\"#\")[0];\r\n // 只看相对路径\r\n if (!href || href.startsWith(\"http\") || href.startsWith(\"mailto:\")) continue;\r\n // 去掉 ./ 前缀\r\n href = href.replace(/^\\.\\//, \"\");\r\n // 收文档 + 代码文件\r\n let kind: DiscoveredFile[\"kind\"] = \"other\";\r\n if (href.endsWith(\".md\") || href.endsWith(\".mdx\")) kind = \"md\";\r\n else if (href.endsWith(\".ipynb\")) kind = \"ipynb\";\r\n else if (href.endsWith(\".rst\")) kind = \"rst\";\r\n else if (href.endsWith(\".rmd\")) kind = \"rmd\";\r\n else if (href.endsWith(\".org\")) kind = \"org\";\r\n else if (href.endsWith(\".adoc\") || href.endsWith(\".asciidoc\")) kind = \"adoc\";\r\n else if (CODE_EXTENSIONS.some((ext) => href.endsWith(ext))) kind = \"code\";\r\n else continue;\r\n // 去重\r\n if (seen.has(href)) continue;\r\n seen.add(href);\r\n files.push({ path: href, title: title || href, kind });\r\n }\r\n return files;\r\n}\r\n\r\n/**\r\n * 过滤:只保留像课时文件的(排除 translations/、lab/、translations、LICENSE 等)\r\n */\r\nexport function filterLessonFiles(files: DiscoveredFile[]): DiscoveredFile[] {\r\n return files.filter((f) => {\r\n const p = f.path.toLowerCase();\r\n // 排除翻译目录\r\n if (p.includes(\"translations/\")) return false;\r\n // 排除常见非教学内容\r\n if (p.endsWith(\"license.md\") || p.endsWith(\"contributing.md\") || p.endsWith(\"code_of_conduct.md\"))\r\n return false;\r\n // 排除 lab/ 目录(是配套练习说明,不是课时正文)\r\n // 注意:保留,但后面处理时区分对待\r\n return true;\r\n });\r\n}\r\n\r\n/**\r\n * 规则高置信度检测:仓库是否已用编号目录组织好课程结构。\r\n *\r\n * 判定依据:文件路径里有 ≥3 个不同的编号顶层目录(如 lessons/1-Intro/,\r\n * lessons/2-Symbolic/, lessons/3-NeuralNetworks/)。编号前缀 = 作者刻意组织。\r\n *\r\n * 这是确定性判断(规则管),不交给 LLM。\r\n * 命中 → pattern: \"well-organized\",下游只判 world 不重组章节。\r\n */\r\nexport function detectWellOrganized(files: { path: string }[]): boolean {\r\n const topicDirs = new Set<string>();\r\n // 已知的课程组织目录名前缀(前缀 + 数字/分隔符,不含纯复数如 lessons/chapters)\r\n const ORGANIZED_PREFIXES = /^(week|unit|part|topic|lecture|session|day|step)(\\d|[-_])/i;\r\n for (const f of files) {\r\n const parts = f.path.split(\"/\").filter(Boolean);\r\n for (const part of parts) {\r\n if (part.includes(\".\")) continue; // 是文件名不是目录名\r\n // 编号目录 (1-Intro, 02_Symbolic, 03-Perceptron)\r\n const m = part.match(/^(\\d+[-_])/i);\r\n if (m) {\r\n topicDirs.add(part.toLowerCase());\r\n break;\r\n }\r\n // 已知课程组织目录 (week1, unit-2, topic-a, lecture3, etc.)\r\n if (ORGANIZED_PREFIXES.test(part)) {\r\n topicDirs.add(part.toLowerCase());\r\n break;\r\n }\r\n }\r\n }\r\n return topicDirs.size >= 3;\r\n}\r\n\r\n/**\r\n * 检测仓库形态。\r\n *\r\n * 原则:规则管确定性,不确定的给 LLM 兜底(通过下游 analyzeCourseStructure)。\r\n *\r\n * - well-organized: README 链接 ≥1 个且路径有编号/组织目录(数字/week/unit/topic) → 保留原始结构\r\n * - course: README 链接里有 ≥1 个课程文件(.md/.ipynb/.py 等) → LLM 重组\r\n * - single-file: 无子文件链接但 README 有实质教学正文(prose >1000 字)\r\n * - docs-rich: README 无链接但文件树可能有内容 → 不急着拒绝,让 fetchRepoInventory 用文件树补全\r\n * - unsupported: awesome-list(外链占比>60%且正文极少)\r\n */\r\nexport function detectRepoPattern(readmeMd: string): DetectionResult {\r\n const allLinks = extractInternalLinks(readmeMd);\r\n const lessonLinks = filterLessonFiles(allLinks).filter((f) => f.kind !== \"other\");\r\n\r\n // 课程型: 有 ≥1 个子文件链接 → 尝试课程型(文件树会补全更多文件)\r\n if (lessonLinks.length >= 1) {\r\n // 高置信度检测:仓库是否已用编号目录组织好(如 lessons/1-Intro/...)\r\n if (detectWellOrganized(lessonLinks)) {\r\n return {\r\n pattern: \"well-organized\",\r\n reason: `README 含 ${lessonLinks.length} 个文件,路径有编号目录组织,判定为已组织好的课程仓库`,\r\n lessonFiles: lessonLinks,\r\n };\r\n }\r\n return {\r\n pattern: \"course\",\r\n reason: `README 含 ${lessonLinks.length} 个内部课程文件链接,判定为课程型仓库`,\r\n lessonFiles: lessonLinks,\r\n };\r\n }\r\n\r\n // 计算\"实质正文\"字符数(去徽章/HTML/链接语法后的纯文字)\r\n const proseChars = readmeMd\r\n .replace(/!\\[[^\\]]*\\]\\([^)]*\\)/g, \"\") // 去图片\r\n .replace(/\\[([^\\]]*)\\]\\([^)]*\\)/g, \"$1\") // 去链接语法保留文字\r\n .replace(/<[^>]+>/g, \"\") // 去 HTML 标签\r\n .replace(/^---[\\s\\S]*?---/m, \"\") // 去 YAML front matter\r\n .replace(/\\s/g, \"\").length;\r\n\r\n // 单文件型: 无子文件链接,但 README 有实质教学正文\r\n if (proseChars > 1000) {\r\n return {\r\n pattern: \"single-file\",\r\n reason: `README 无子文件链接,但实质正文 ${proseChars} 字,判定为单文件型`,\r\n readmeLength: readmeMd.length,\r\n };\r\n }\r\n\r\n // awesome-list 检测:外链占比极高 + 正文极少 → unsupported\r\n const externalLinks = (readmeMd.match(/\\]\\(https?:\\/\\//g) || []).length;\r\n const totalLinks = (readmeMd.match(/\\]\\(/g) || []).length;\r\n if (totalLinks > 10 && externalLinks / totalLinks > 0.6 && proseChars < 500) {\r\n return {\r\n pattern: \"unsupported\",\r\n reason: `README 外链占比 ${(externalLinks / totalLinks * 100).toFixed(0)}%,实质正文仅 ${proseChars} 字,疑似 awesome-list 资源索引(非课程)`,\r\n };\r\n }\r\n\r\n // docs-rich: README 无链接但可能 docs/ 下有大量内容 → 让 fetchRepoInventory 用文件树补全\r\n // 不在这里抛 unsupported,给文件树一个机会\r\n return {\r\n pattern: \"docs-rich\",\r\n reason: `README 无课程文件链接,实质正文 ${proseChars} 字 → 将用文件树补全课程文件`,\r\n };\r\n}\r\n\r\n/**\r\n * 并发拉取多个 markdown 文件(5 并发,防 CDN 过载)。\r\n *\r\n * @param files 要拉取的文件列表\r\n * @param owner repo owner\r\n * @param repo repo name\r\n * @param branch 分支名\r\n * @param fetchFn 注入的 fetch 函数\r\n * @param onProgress 进度回调 (done, total, currentPath)\r\n */\r\nexport async function fetchMarkdownContents(\r\n files: DiscoveredFile[],\r\n owner: string,\r\n repo: string,\r\n branch: string,\r\n fetchFn: typeof fetch,\r\n onProgress?: (done: number, total: number, currentPath: string) => void,\r\n): Promise<FetchResult> {\r\n const ok: FetchedFile[] = [];\r\n const failed: { path: string; error: string }[] = [];\r\n const CONCURRENCY = 5;\r\n let done = 0;\r\n\r\n // 分批并发\r\n for (let i = 0; i < files.length; i += CONCURRENCY) {\r\n const batch = files.slice(i, i + CONCURRENCY);\r\n const results = await Promise.allSettled(\r\n batch.map(async (f) => {\r\n const url = cdnUrl(owner, repo, branch, f.path);\r\n const r = await fetchFn(url);\r\n if (!r.ok) throw new Error(`HTTP ${r.status}`);\r\n const text = await r.text();\r\n // .ipynb → 用 notebook-parser 转成 markdown(markdown cell + code block)\r\n if (f.path.toLowerCase().endsWith(\".ipynb\")) {\r\n try {\r\n const { parseNotebook } = await import(\"./notebook-parser.js\");\r\n const nbResult = parseNotebook(text);\r\n return { path: f.path, title: f.title, md: nbResult.markdown };\r\n } catch {\r\n return { path: f.path, title: f.title, md: text };\r\n }\r\n }\r\n // .rst/.rmd/.org/.adoc → 用各自解析器转 markdown\r\n const lowerPath = f.path.toLowerCase();\r\n if (lowerPath.endsWith(\".rst\") || lowerPath.endsWith(\".rmd\") || lowerPath.endsWith(\".org\") || lowerPath.endsWith(\".adoc\") || lowerPath.endsWith(\".asciidoc\")) {\r\n const parserMap: Record<string, string> = {\r\n \".rst\": \"rst-parser\", \".rmd\": \"rmd-parser\", \".org\": \"org-parser\",\r\n \".adoc\": \"adoc-parser\", \".asciidoc\": \"adoc-parser\",\r\n };\r\n const ext = lowerPath.match(/\\.[^.]+$/)?.[0] ?? \"\";\r\n const parserName = parserMap[ext];\r\n if (parserName) {\r\n try {\r\n const mod = await import(`./${parserName}.js`);\r\n const fn = mod.parseRst ?? mod.parseRmd ?? mod.parseOrg ?? mod.parseAdoc;\r\n return { path: f.path, title: f.title, md: fn(text).markdown };\r\n } catch {\r\n return { path: f.path, title: f.title, md: text };\r\n }\r\n }\r\n }\r\n // 代码文件 → code-parser 转 markdown (docstring + 代码围栏)\r\n if (CODE_EXTENSIONS.some((ext) => lowerPath.endsWith(ext))) {\r\n const ext = lowerPath.split(\".\").pop() ?? \"\";\r\n try {\r\n const { parseCode } = await import(\"./code-parser.js\");\r\n return { path: f.path, title: f.title, md: parseCode(text, ext).markdown };\r\n } catch {\r\n return { path: f.path, title: f.title, md: \"```\\n\" + text + \"\\n```\" };\r\n }\r\n }\r\n return { path: f.path, title: f.title, md: text };\r\n }),\r\n );\r\n for (let j = 0; j < results.length; j++) {\r\n done++;\r\n const file = batch[j];\r\n const result = results[j];\r\n if (file) onProgress?.(done, files.length, file.path);\r\n if (result && result.status === \"fulfilled\") {\r\n ok.push(result.value);\r\n } else if (result && result.status === \"rejected\") {\r\n failed.push({\r\n path: file?.path ?? \"(unknown)\",\r\n error: result.reason instanceof Error ? result.reason.message : String(result.reason),\r\n });\r\n }\r\n }\r\n }\r\n\r\n return { ok, failed };\r\n}\r\n\r\n/**\r\n * 把课程型仓库的多个课时文件合并成 ParsedCourse 结构。\r\n *\r\n * v3 改进:集成 file-classifier 规则引擎。\r\n * - 先对每个文件调 classifyFile 判定角色(lesson/notebook/lab/section-intro/uncertain 等)\r\n * - keepAsLesson=false 的文件(translation/meta/notebook/lab/example/section-intro)不进 lesson 列表\r\n * - section-intro 的正文追加到同 section 摘要(作为章节概述)\r\n * - uncertain 的文件进 lesson 列表但标 uncertain=true,后续 LLM 结构化时优先判断 keep/skip\r\n *\r\n * 分组策略保留 v2 的\"第一个非通用目录\"启发式(减少碎片)。\r\n *\r\n * 每个文件的内部 H2/H3 → 该 section 下的 lessons;无 H2/H3 则整个文件作一个 lesson。\r\n */\r\nexport function buildCourseFromFiles(\r\n courseTitle: string,\r\n files: FetchedFile[],\r\n): ParsedCourse {\r\n // 第 0 步:对每个文件分类(siblingPaths = 全部文件路径)\r\n const allPaths = files.map((f) => f.path);\r\n for (const file of files) {\r\n if (!file.classification) {\r\n file.classification = classifyFile(file.path, file.md, { siblingPaths: allPaths });\r\n }\r\n }\r\n\r\n // 第一步:给每个 keepAsLesson 文件算\"分组键\"和\"lesson 候选\"\r\n // 非课时文件(notebook/lab/example/section-intro)的正文不丢弃——\r\n // notebook/lab/example 追加到同目录 lesson 的正文末尾(作为\"代码/练习补充\"),\r\n // section-intro 追加到 section 第一个 lesson 的正文开头(作为\"章节概述\")。\r\n interface FileGroup {\r\n sectionTitle: string;\r\n orderKey: string; // 用于排序(保持原路径顺序)\r\n lessons: ParsedLesson[];\r\n /** 待追加到第一个 lesson 的章节概述正文 */\r\n pendingIntro?: string;\r\n }\r\n const groupMap = new Map<string, FileGroup>();\r\n const groupOrder: string[] = [];\r\n\r\n const GENERIC_DIRS = new Set([\"lessons\", \"docs\", \"doc\", \"src\", \"content\", \"modules\", \"chapters\", \"tutorials\", \"guide\", \"week\", \"unit\", \"part\", \"topic\", \"lecture\", \"session\", \"day\", \"step\"]);\r\n\r\n /**\r\n * 计算文件的 section 分组键(和 lesson 用同一个逻辑)。\r\n */\r\n function sectionKeyOf(path: string): { groupKey: string; sectionTitle: string } {\r\n const parts = path.split(\"/\").filter(Boolean);\r\n const dirParts = parts[parts.length - 1]?.match(/^readme/i) || parts[parts.length - 1] === \"index.md\"\r\n ? parts.slice(0, -1)\r\n : parts;\r\n const specificDir = dirParts.find((p) => !GENERIC_DIRS.has(p.toLowerCase()) && !/\\.(md|mdx)$/i.test(p));\r\n if (dirParts.length >= 2 && specificDir) {\r\n const gk = specificDir.replace(/\\.md$/i, \"\");\r\n return { groupKey: gk, sectionTitle: gk };\r\n } else if (dirParts.length === 1) {\r\n return { groupKey: path, sectionTitle: dirParts[0]!.replace(/\\.md$/i, \"\") };\r\n }\r\n return { groupKey: path, sectionTitle: parts[parts.length - 1] ?? path };\r\n }\r\n\r\n // 先按路径排序,保证同目录的 notebook 在 lesson 之后(这样 lesson 先建好,notebook 能追加到它)\r\n const sortedFiles = [...files].sort((a, b) => a.path.localeCompare(b.path));\r\n\r\n for (const file of sortedFiles) {\r\n const classification = file.classification!;\r\n const { groupKey, sectionTitle } = sectionKeyOf(file.path);\r\n\r\n // 确保分组存在\r\n if (!groupMap.has(groupKey)) {\r\n groupMap.set(groupKey, { sectionTitle, orderKey: file.path, lessons: [] });\r\n if (!groupOrder.includes(groupKey)) groupOrder.push(groupKey);\r\n }\r\n const group = groupMap.get(groupKey)!;\r\n\r\n // ---- 非课时文件:正文合并到同目录 lesson ----\r\n if (!classification.keepAsLesson) {\r\n if (classification.role === \"section-intro\") {\r\n // section-intro → 追加到 section 第一个 lesson 开头\r\n group.pendingIntro = file.md;\r\n } else {\r\n // translation/meta → 不合并,直接跳过\r\n }\r\n continue;\r\n }\r\n\r\n // ---- uncertain 文件(notebook/lab/example):建独立 practice 节点 ----\r\n // 两个世界设计:不再把 notebook/lab/example 合并进 study lesson 正文,\r\n // 而是作为独立 practice lesson 入组(world=null,LLM 判 study/practice)。\r\n // 这样 LLM 能看到它们并判 world,用户也能在实操世界独立探索。\r\n const lowerP = file.path.toLowerCase();\r\n const isNotebook = lowerP.endsWith(\".ipynb\");\r\n const isLab = /\\/lab\\//.test(lowerP) || /\\/labs\\//.test(lowerP) || /\\/exercise/.test(lowerP);\r\n const isExample = /\\/examples?\\//.test(lowerP) || /\\/demos?\\//.test(lowerP);\r\n if (isNotebook || isLab || isExample) {\r\n // notebook/lab/example → 独立 practice 节点(world=null 等 LLM 判)\r\n const h1Match = file.md.match(/^#\\s+(.+)$/m);\r\n const lessonTitle = h1Match ? h1Match[1]!.trim() : file.title;\r\n group.lessons.push({\r\n title: lessonTitle,\r\n anchor: file.path.toLowerCase().replace(/[^a-z0-9]+/g, \"-\"),\r\n body: file.md,\r\n uncertain: true,\r\n sourceFilePath: file.path,\r\n world: null, // LLM 在 course-structure-service 判 study/practice\r\n });\r\n continue;\r\n }\r\n\r\n // ---- 课时文件:正常进 lesson 列表 ----\r\n const parsed = parseMarkdownToCourse(file.md);\r\n const parsedLessonCount = parsed.sections.reduce((sum, s) => sum + s.lessons.length, 0);\r\n const isUncertain = classification.role === \"uncertain\";\r\n const fileWorld = classification.world; // null for uncertain, \"study\" for section-intro\r\n const lessonCandidates: ParsedLesson[] =\r\n parsedLessonCount > 0\r\n ? parsed.sections\r\n .filter((s) => s.lessons.length > 0)\r\n .flatMap((s) => s.lessons.map((l) => ({\r\n title: l.title,\r\n anchor: l.title.toLowerCase().replace(/\\s+/g, \"-\"),\r\n body: l.body,\r\n uncertain: isUncertain,\r\n sourceFilePath: file.path,\r\n world: fileWorld,\r\n })))\r\n : (() => {\r\n const h1Match = file.md.match(/^#\\s+(.+)$/m);\r\n const lessonTitle = h1Match ? h1Match[1]!.trim() : file.title;\r\n return [{\r\n title: lessonTitle,\r\n anchor: lessonTitle.toLowerCase().replace(/\\s+/g, \"-\"),\r\n body: file.md,\r\n uncertain: isUncertain,\r\n sourceFilePath: file.path,\r\n world: fileWorld,\r\n }];\r\n })();\r\n\r\n group.lessons.push(...lessonCandidates);\r\n }\r\n\r\n // 第二步:把 pendingIntro(section-intro 正文)追加到每个 section 第一个 lesson 开头\r\n for (const key of groupOrder) {\r\n const g = groupMap.get(key)!;\r\n if (g.pendingIntro && g.lessons.length > 0) {\r\n g.lessons[0]!.body = `> **📖 章节概述**\\n>\\n> ${g.pendingIntro.replace(/\\n/g, \"\\n> \")}\\n\\n---\\n\\n${g.lessons[0]!.body}`;\r\n }\r\n }\r\n\r\n // 第三步:每个分组 → 一个 section(去掉空 section)\r\n // section.world: 全 practice 子节点 → practice, 否则 study(混或全 study)\r\n const sections: ParsedSection[] = groupOrder\r\n .filter((key) => groupMap.get(key)!.lessons.length > 0)\r\n .map((key) => {\r\n const g = groupMap.get(key)!;\r\n const practiceCount = g.lessons.filter((l) => l.world === \"practice\").length;\r\n const studyCount = g.lessons.filter((l) => l.world === \"study\").length;\r\n return {\r\n title: g.sectionTitle,\r\n anchor: g.sectionTitle.toLowerCase().replace(/\\s+/g, \"-\"),\r\n world: practiceCount > 0 && studyCount === 0 ? \"practice\" as const : \"study\" as const,\r\n lessons: g.lessons,\r\n };\r\n });\r\n\r\n return { title: courseTitle, sections };\r\n}\r\n\r\n/* ============================================================\r\n * 文件发现:GitHub Tree API(主)→ jsdelivr 文件列表(fallback)→ README 链接(兜底)\r\n *\r\n * 用户网络只是偶尔不稳,不屏蔽 API。设计以最优方式为主,降级防抖。\r\n * ============================================================ */\r\n\r\n/** 文件发现的来源标记(供进度提示 + 测试断言)。 */\r\nexport type FileDiscoverySource = \"github-tree-api\" | \"jsdelivr-list\" | \"readme-links\" | \"none\";\r\n\r\nexport interface DiscoveredTree {\r\n paths: string[];\r\n source: FileDiscoverySource;\r\n}\r\n\r\n/** 从 .md 路径列表构造 DiscoveredFile[](复用 filterLessonFiles 排除规则 + 标题推断)。 */\r\nexport function pathsToDiscoveredFiles(paths: string[]): DiscoveredFile[] {\r\n const files: DiscoveredFile[] = [];\r\n const seen = new Set<string>();\r\n for (const p of paths) {\r\n const lower = p.toLowerCase();\r\n if (seen.has(p)) continue;\r\n let kind: DiscoveredFile[\"kind\"] = \"other\";\r\n if (lower.endsWith(\".md\") || lower.endsWith(\".mdx\")) kind = \"md\";\r\n else if (lower.endsWith(\".ipynb\")) kind = \"ipynb\";\r\n else if (lower.endsWith(\".rst\")) kind = \"rst\";\r\n else if (lower.endsWith(\".rmd\")) kind = \"rmd\";\r\n else if (lower.endsWith(\".org\")) kind = \"org\";\r\n else if (lower.endsWith(\".adoc\") || lower.endsWith(\".asciidoc\")) kind = \"adoc\";\r\n else if (CODE_EXTENSIONS.some((ext) => lower.endsWith(ext))) kind = \"code\";\r\n else continue;\r\n // 排除非教学内容\r\n if (lower.includes(\"node_modules/\") || lower.startsWith(\".git/\") || lower.includes(\"translations/\")) continue;\r\n if (lower.endsWith(\"license.md\") || lower.endsWith(\"contributing.md\") || lower.endsWith(\"code_of_conduct.md\")) continue;\r\n seen.add(p);\r\n // 标题用文件名(去扩展名)或最后一层目录名\r\n const parts = p.split(\"/\").filter(Boolean);\r\n const last = parts[parts.length - 1] ?? p;\r\n const title = last.replace(/\\.(md|mdx|ipynb|rst|rmd|org|adoc|asciidoc|py|js|jsx|ts|tsx|go|rs|java|c|cpp|rb|sh|sql|lua|r|jl|dart|scala|kt|cs|php|swift|hs|clj|ex|erl|ml|fs|pl|elm)$/i, \"\").replace(/^readme$/i, parts[parts.length - 2] ?? last);\r\n files.push({ path: p, title, kind });\r\n }\r\n return files;\r\n}\r\n\r\n/**\r\n * 从本地扫描器(buildLocalInventory)已解析的 docs 直接构造 DiscoveredFile[]。\r\n *\r\n * 为什么本地路径不走 pathsToDiscoveredFiles:后者是面向 GitHub 文件树的过滤器,\r\n * 只保留 .md/.ipynb/.rst/.rmd/.org/.adoc + 代码扩展名,会 `else continue` 静默丢弃\r\n * .txt/.html/.htm/.pdf/.pptx。而本地扫描器按 EXT_KIND 接受并解析好这些格式了\r\n * (html→htmlToText / pdf→parsePdfText / pptx→parsePptx / txt→原文),\r\n * 再过一遍 pathsToDiscoveredFiles 等于把已解析的内容全扔掉 → 分类空 → 空课程\r\n * (见 scripts/verify-local-filelist.mjs 锁定的回归)。\r\n *\r\n * DiscoveredFile.kind 在下游分类 / 结构设计链路(classifyFileRoles、parseRoleResult、\r\n * parseStructureDesignResult、fallbackStructure)均不读取(只读 path),故统一填 \"other\"。\r\n */\r\nexport function docsToDiscoveredFiles(docs: { path: string; title?: string }[]): DiscoveredFile[] {\r\n const seen = new Set<string>();\r\n const files: DiscoveredFile[] = [];\r\n for (const d of docs) {\r\n if (!d.path || seen.has(d.path)) continue;\r\n seen.add(d.path);\r\n const parts = d.path.split(\"/\").filter(Boolean);\r\n const last = parts[parts.length - 1] ?? d.path;\r\n const title = d.title?.trim() || last.replace(/\\.[^.]+$/, \"\") || d.path;\r\n files.push({ path: d.path, title, kind: \"other\" });\r\n }\r\n return files;\r\n}\r\n\r\n/**\r\n * 主方式:GitHub Tree API 一次拿全仓文件树。\r\n * https://api.github.com/repos/{owner}/{repo}/git/trees/{branch}?recursive=1\r\n * 返回 { tree: [{ path, type }] }。筛 blob + .md/.ipynb。\r\n * 网络失败/限流 → 抛错(由调用方降级)。\r\n */\r\n/**\r\n * 用 Node 的 https 模块拉取(可单独控制 SSL 验证)。\r\n * GitHub Tree API 的证书链在部分环境(Node 内置 CA)验证失败(中间证书缺失),\r\n * 对这一个获取公开文件树的请求用 rejectUnauthorized:false 绕过。\r\n * 风险可控:获取的是公开文件路径列表(无敏感数据),且只用于此请求。\r\n */\r\nfunction httpsGet(url: string, opts: { rejectUnauthorized?: boolean; headers?: Record<string, string> } = {}): Promise<{ ok: boolean; status?: number; body?: string; error?: string }> {\r\n return new Promise((resolve) => {\r\n const req = https.get(url, {\r\n headers: { \"User-Agent\": \"lookatstudy-import\", ...opts.headers },\r\n rejectUnauthorized: opts.rejectUnauthorized ?? true,\r\n timeout: 20000,\r\n }, (res) => {\r\n let body = \"\";\r\n res.on(\"data\", (d: Buffer) => { body += d.toString(); });\r\n res.on(\"end\", () => resolve({ ok: res.statusCode === 200, status: res.statusCode, body }));\r\n });\r\n req.on(\"error\", (e: Error) => resolve({ ok: false, error: e.message }));\r\n req.on(\"timeout\", () => { req.destroy(); resolve({ ok: false, error: \"timeout\" }); });\r\n });\r\n}\r\n\r\nexport async function fetchRepoFileTree(\r\n owner: string,\r\n repo: string,\r\n branch: string,\r\n _fetchFn?: typeof fetch, // 保留签名兼容,实际用内部 httpsGet(可控制 SSL)\r\n): Promise<DiscoveredTree> {\r\n // GitHub Tree API(唯一可靠源:recursive=1 给全部文件,含 translations/、代码、图片等)\r\n // jsdelivr 文件列表已证明不可行(仓库大就 403 \"Package size exceeded limit\")\r\n const apiUrl = `https://api.github.com/repos/${owner}/${repo}/git/trees/${branch}?recursive=1`;\r\n try {\r\n const r = await httpsGet(apiUrl, { rejectUnauthorized: false });\r\n console.error(`[import] GitHub Tree API: HTTP ${r.status ?? r.error}`);\r\n if (r.ok && r.body) {\r\n const data = JSON.parse(r.body) as { tree?: Array<{ path: string; type: string }> };\r\n const paths = (data.tree ?? []).filter((n) => n.type === \"blob\").map((n) => n.path);\r\n if (paths.length > 0) return { paths, source: \"github-tree-api\" };\r\n }\r\n } catch (e) {\r\n console.error(`[import] GitHub Tree API 异常: ${e instanceof Error ? e.message : e}`);\r\n }\r\n return { paths: [], source: \"none\" };\r\n}\r\n\r\n/**\r\n * 兜底:从 README 链接发现 + 一层递归(读到的 .md 文件内部再找链接)。\r\n * 用于 Tree API + jsdelivr 都失败时,或网络不稳的场景。\r\n */\r\nexport async function discoverFromReadmeRecursively(\r\n readmeMd: string,\r\n owner: string,\r\n repo: string,\r\n branch: string,\r\n fetchFn: typeof fetch,\r\n maxDepth = 1,\r\n onProgress?: (msg: string) => void,\r\n): Promise<DiscoveredTree> {\r\n const direct = filterLessonFiles(extractInternalLinks(readmeMd)).filter((f) => f.kind === \"md\");\r\n if (direct.length === 0) return { paths: [], source: \"readme-links\" };\r\n\r\n const allPaths = new Set<string>(direct.map((f) => f.path));\r\n\r\n // 一层递归:拉取直接链接的文件,从其内部再找 .md 链接\r\n if (maxDepth >= 1) {\r\n onProgress?.(`README 发现 ${direct.length} 个文件,递归扫描子链接…`);\r\n const fetched = await fetchMarkdownContents(direct, owner, repo, branch, fetchFn);\r\n for (const f of fetched.ok) {\r\n const subLinks = filterLessonFiles(extractInternalLinks(f.md)).filter((s) => s.kind === \"md\");\r\n for (const s of subLinks) {\r\n if (!allPaths.has(s.path)) allPaths.add(s.path);\r\n }\r\n }\r\n }\r\n\r\n return { paths: Array.from(allPaths), source: \"readme-links\" };\r\n}\r\n\r\n/* ============================================================\r\n * v0.8 多模态:GitHub 导入图片收集\r\n * 从已拉取的 .md 内容里解析 ![](img.png) 引用,从 CDN 下载图片二进制。\r\n * ============================================================ */\r\n\r\n/** 图片扩展名集合(与 local-folder-scanner 保持一致) */\r\nconst IMAGE_EXTS = new Set([\"png\", \"jpg\", \"jpeg\", \"gif\", \"webp\", \"svg\", \"bmp\", \"avif\", \"ico\", \"tiff\", \"tif\", \"heic\"]);\r\n\r\n/** ext → MIME */\r\nconst EXT_TO_MIME: Record<string, string> = {\r\n png: \"image/png\",\r\n jpg: \"image/jpeg\",\r\n jpeg: \"image/jpeg\",\r\n gif: \"image/gif\",\r\n webp: \"image/webp\",\r\n svg: \"image/svg+xml\",\r\n bmp: \"image/bmp\",\r\n};\r\n\r\n/** 从 markdown 文本提取图片引用 ![alt](path) + <img src='...'>,只收相对路径的图片扩展名 */\r\nexport function extractImageRefsFromMd(md: string): { alt: string; path: string }[] {\r\n const refs: { alt: string; path: string }[] = [];\r\n const seen = new Set<string>();\r\n\r\n // 1. Markdown 语法 ![alt](url)\r\n const mdPattern = /!\\[([^\\]]*)\\]\\(([^)]+)\\)/g;\r\n let m: RegExpExecArray | null;\r\n while ((m = mdPattern.exec(md)) !== null) {\r\n const alt = m[1].trim();\r\n let url = m[2].trim();\r\n const titleMatch = url.match(/\\s+\"[^\"]*\"$/);\r\n if (titleMatch) url = url.slice(0, titleMatch.index).trim();\r\n url = url.split(\"#\")[0];\r\n if (!url || url.startsWith(\"http://\") || url.startsWith(\"https://\") || url.startsWith(\"data:\")) continue;\r\n url = url.replace(/^\\.\\//, \"\");\r\n const ext = url.toLowerCase().match(/\\.([^.]+)$/)?.[1] ?? \"\";\r\n if (!IMAGE_EXTS.has(ext)) continue;\r\n if (seen.has(url)) continue;\r\n seen.add(url);\r\n refs.push({ alt, path: url });\r\n }\r\n\r\n // 2. HTML <img> 标签(覆盖微软课程仓库 <img src='images/xxx.png'/>)\r\n // 两步法:先提取 <img ...> 整标签,再独立提取 src 和 alt(属性顺序无关)\r\n const htmlPattern = /<img\\s+[^>]*>/gi;\r\n let hm: RegExpExecArray | null;\r\n while ((hm = htmlPattern.exec(md)) !== null) {\r\n const tag = hm[0];\r\n let url = (tag.match(/src=['\"]([^'\"]+)['\"]/i)?.[1] ?? \"\").trim().split(\"#\")[0];\r\n const alt = (tag.match(/alt=['\"]([^'\"]*)['\"]/i)?.[1] ?? \"\").trim();\r\n if (!url || url.startsWith(\"http://\") || url.startsWith(\"https://\") || url.startsWith(\"data:\")) continue;\r\n url = url.replace(/^\\.\\//, \"\");\r\n const ext = url.toLowerCase().match(/\\.([^.]+)$/)?.[1] ?? \"\";\r\n if (!IMAGE_EXTS.has(ext)) continue;\r\n if (seen.has(url)) continue;\r\n seen.add(url);\r\n refs.push({ alt: alt || (url.split(\"/\").pop() ?? url), path: url });\r\n }\r\n\r\n return refs;\r\n}\r\n\r\n/** 从 .md 文件路径解析图片引用的绝对仓库路径(相对 doc 所在目录) */\r\nfunction resolveRepoImgPath(imgRef: string, docPath: string): string {\r\n const docDir = docPath.includes(\"/\") ? docPath.slice(0, docPath.lastIndexOf(\"/\")) : \"\";\r\n const parts = docDir ? docDir.split(\"/\") : [];\r\n for (const p of imgRef.split(\"/\")) {\r\n if (p === \"..\") parts.pop();\r\n else if (p !== \".\" && p !== \"\") parts.push(p);\r\n }\r\n return parts.join(\"/\");\r\n}\r\n\r\n/** 下载的图片结果 */\r\nexport interface DownloadedImage {\r\n /** 仓库内的相对路径(用作 sourcePath) */\r\n repoPath: string;\r\n /** 关联的 doc 路径(用于 nodeId 匹配) */\r\n docPath: string;\r\n /** 图片二进制 */\r\n buffer: Buffer;\r\n /** MIME */\r\n mimeType: string;\r\n /** alt 文本 */\r\n altText: string;\r\n}\r\n\r\n/**\r\n * 从已拉取的 markdown 文件里收集图片引用,从 CDN 下载二进制。\r\n * 5 并发,防 CDN 过载。单个失败跳过不阻塞。\r\n *\r\n * @param files 已拉取的 .md 文件(ok 列表)\r\n * @param owner repo owner\r\n * @param repo repo name\r\n * @param branch 分支\r\n * @param fetchFn 注入的 fetch\r\n * @param onProgress 进度回调\r\n * @returns 下载成功的图片列表\r\n */\r\nexport async function fetchRepoImages(\r\n files: FetchedFile[],\r\n owner: string,\r\n repo: string,\r\n branch: string,\r\n fetchFn: typeof fetch,\r\n onProgress?: (done: number, total: number, path: string) => void,\r\n): Promise<DownloadedImage[]> {\r\n // 1. 从所有 .md 文件收集图片引用(去重)\r\n const allRefs = new Map<string, { repoPath: string; docPath: string; alt: string }>();\r\n for (const file of files) {\r\n const refs = extractImageRefsFromMd(file.md);\r\n for (const ref of refs) {\r\n const repoPath = resolveRepoImgPath(ref.path, file.path);\r\n if (!allRefs.has(repoPath)) {\r\n allRefs.set(repoPath, { repoPath, docPath: file.path, alt: ref.alt });\r\n }\r\n }\r\n }\r\n\r\n if (allRefs.size === 0) return [];\r\n const refList = Array.from(allRefs.values());\r\n const downloaded: DownloadedImage[] = [];\r\n const CONCURRENCY = 5;\r\n\r\n // 2. 并发下载(分批)\r\n for (let i = 0; i < refList.length; i += CONCURRENCY) {\r\n const batch = refList.slice(i, i + CONCURRENCY);\r\n const results = await Promise.allSettled(\r\n batch.map(async (ref) => {\r\n const url = cdnUrl(owner, repo, branch, ref.repoPath);\r\n const r = await fetchFn(url);\r\n if (!r.ok) throw new Error(`HTTP ${r.status}`);\r\n const buf = Buffer.from(await r.arrayBuffer());\r\n const ext = ref.repoPath.toLowerCase().match(/\\.([^.]+)$/)?.[1] ?? \"png\";\r\n return {\r\n repoPath: ref.repoPath,\r\n docPath: ref.docPath,\r\n buffer: buf,\r\n mimeType: EXT_TO_MIME[ext] ?? \"image/png\",\r\n altText: ref.alt || ref.repoPath.split(\"/\").pop() || ref.repoPath,\r\n } satisfies DownloadedImage;\r\n }),\r\n );\r\n for (let j = 0; j < results.length; j++) {\r\n const done = i + j + 1;\r\n const ref = batch[j];\r\n onProgress?.(done, refList.length, ref?.repoPath ?? \"\");\r\n const result = results[j];\r\n if (result && result.status === \"fulfilled\") {\r\n downloaded.push(result.value);\r\n }\r\n }\r\n }\r\n\r\n return downloaded;\r\n}\r\n\r\n/* ============================================================\r\n * 顶层编排:从 GitHub repo URL → ParsedCourse(纯函数,不落库)\r\n *\r\n * 提取自 ipc/index.ts 的 importFromRepo handler 的纯逻辑部分。\r\n * IPC handler / 种子脚本 / 未来 CLI 都复用本函数。\r\n * ============================================================ */\r\n\r\n/** importRepoToParsedCourse 的返回结果 */\r\nexport interface ImportRepoResult {\r\n /** 构建好的课程结构(含 classification 标签) */\r\n course: ParsedCourse;\r\n /** 仓库检测结果 */\r\n detection: DetectionResult;\r\n /** 拉取的文件(含 classification,供图像收集等后续步骤用) */\r\n fetchedFiles: FetchedFile[];\r\n /** README 实际用的分支(main 或 master) */\r\n readmeBranch: string;\r\n /** README 全文(供 single-file 降级用) */\r\n readmeMd: string;\r\n}\r\n\r\n/** 文件数上限(防爆,和 IPC handler 一致) */\r\nconst MAX_FILES = 500;\r\n\r\n/**\r\n * 从 GitHub 仓库构建课程结构 —— 纯编排函数。\r\n *\r\n * 流程: fetch README → detectRepoPattern → 发现文件树 → fetchMarkdownContents\r\n * → classifyFile(在 buildCourseFromFiles 内)→ buildCourseFromFiles\r\n *\r\n * 不落库、不发进度事件(onProgress 回调只传消息字符串,由调用方决定怎么用)。\r\n *\r\n * @param owner GitHub owner\r\n * @param repo GitHub repo\r\n * @param branch 起始分支(README 先试 main 再试 master)\r\n * @param fetchFn 注入的 fetch(生产用 global fetch,测试用 mock)\r\n * @param onProgress 进度回调(可选)\r\n */\r\nexport async function importRepoToParsedCourse(\r\n owner: string,\r\n repo: string,\r\n branch: string,\r\n fetchFn: typeof fetch,\r\n onProgress?: (msg: string) => void,\r\n): Promise<ImportRepoResult> {\r\n const send = (msg: string) => onProgress?.(msg);\r\n\r\n // 1. 拉 README(试 main/master 两个分支)\r\n send(\"正在拉取 README…\");\r\n const branches = branch === \"master\" ? [\"master\", \"main\"] : [\"main\", \"master\"];\r\n let readmeMd: string | null = null;\r\n let readmeBranch = branch;\r\n for (const br of branches) {\r\n try {\r\n const r = await fetchFn(cdnUrl(owner, repo, br, \"README.md\"));\r\n if (r.ok) {\r\n readmeMd = await r.text();\r\n readmeBranch = br;\r\n break;\r\n }\r\n } catch {\r\n // 网络错误,试下一个分支\r\n }\r\n }\r\n if (!readmeMd) throw new Error(`无法拉取 README(试过分支: ${branches.join(\", \")})`);\r\n send(`README 拉取成功(${readmeMd.length} 字符,分支 ${readmeBranch})`);\r\n\r\n // 2. 检测仓库形态\r\n const detection = detectRepoPattern(readmeMd);\r\n if (detection.pattern === \"unsupported\") {\r\n throw new Error(`仓库不支持: ${detection.reason}`);\r\n }\r\n\r\n // single-file: 直接返回(调用方用 generateCourseFromMarkdown 处理)\r\n if (detection.pattern === \"single-file\") {\r\n return {\r\n course: parseMarkdownToCourse(readmeMd),\r\n detection,\r\n fetchedFiles: [],\r\n readmeBranch,\r\n readmeMd,\r\n };\r\n }\r\n\r\n // 3. course 型: 发现文件\r\n // 策略:README 链接是人工策展的(作者选了真正重要的文件),优先用它。\r\n // 文件树是穷举的(含草稿/翻译/内部文档),只在 README 链接太少时才补充。\r\n let lessonFiles = filterLessonFiles(detection.lessonFiles ?? []);\r\n const readmeLinkCount = lessonFiles.length;\r\n\r\n // 只在 README 链接很少(<5)时才尝试文件树补充\r\n if (readmeLinkCount < 5) {\r\n try {\r\n send(\"README 链接较少,扫描文件树补充…\");\r\n const tree = await fetchRepoFileTree(owner, repo, readmeBranch, fetchFn);\r\n if (tree.paths.length > 0) {\r\n const treeFiles = pathsToDiscoveredFiles(tree.paths);\r\n const treeLessonFiles = filterLessonFiles(treeFiles).filter((f) => f.kind !== \"other\");\r\n if (treeLessonFiles.length > lessonFiles.length) {\r\n lessonFiles = treeLessonFiles;\r\n send(`文件树发现 ${lessonFiles.length} 个课时文件(来源: ${tree.source})`);\r\n }\r\n }\r\n } catch {\r\n send(\"文件树拉取失败,使用 README 链接发现\");\r\n }\r\n } else {\r\n send(`README 链接发现 ${readmeLinkCount} 个课时文件(人工策展,优先使用)`);\r\n }\r\n\r\n if (lessonFiles.length === 0) {\r\n // 没有子文件,降级为 single-file\r\n send(\"未发现课时文件,降级为单文件导入\");\r\n return {\r\n course: parseMarkdownToCourse(readmeMd),\r\n detection: { ...detection, pattern: \"single-file\", reason: \"无课时文件,降级\" },\r\n fetchedFiles: [],\r\n readmeBranch,\r\n readmeMd,\r\n };\r\n }\r\n\r\n // 上限:超过 MAX_FILES 时,优先保留 README 链接的文件(人工策展),\r\n // 从文件树补充的文件按路径排序截断(保留编号靠前的课时,通常是基础课)\r\n if (lessonFiles.length > MAX_FILES) {\r\n send(`文件数 ${lessonFiles.length} 超过上限 ${MAX_FILES},截断`);\r\n if (readmeLinkCount > 0 && readmeLinkCount < MAX_FILES) {\r\n // 保留所有 README 链接文件 + 文件树文件按路径排序填充剩余空间\r\n const readmePaths = new Set(filterLessonFiles(detection.lessonFiles ?? []).map((f) => f.path));\r\n const fromReadme = lessonFiles.filter((f) => readmePaths.has(f.path));\r\n const fromTree = lessonFiles.filter((f) => !readmePaths.has(f.path)).slice(0, MAX_FILES - fromReadme.length);\r\n lessonFiles = [...fromReadme, ...fromTree];\r\n } else {\r\n lessonFiles = lessonFiles.slice(0, MAX_FILES);\r\n }\r\n }\r\n\r\n // 4. 拉取正文\r\n send(`检测到课程型仓库(${lessonFiles.length} 个文件),开始拉取…`);\r\n const fetchResult = await fetchMarkdownContents(\r\n lessonFiles, owner, repo, readmeBranch, fetchFn,\r\n (done, total, path) => send(`拉取 ${done}/${total}: ${path}`),\r\n );\r\n\r\n if (fetchResult.ok.length === 0) {\r\n // 所有文件拉取失败 → 抛错让用户知道(而不是静默用 README 伪造课程)\r\n throw new Error(\r\n `检测到 ${lessonFiles.length} 个课时文件,但全部拉取失败。` +\r\n `可能是网络受限。请稍后重试或改用「粘贴 Markdown」方式手动导入。`,\r\n );\r\n }\r\n\r\n // 5. 构建课程(buildCourseFromFiles 内部会调 classifyFile 做分类)\r\n const h1Match = readmeMd.match(/^#\\s+(.+)$/m);\r\n const courseTitle = h1Match ? h1Match[1]!.trim() : repo;\r\n const course = buildCourseFromFiles(courseTitle, fetchResult.ok);\r\n send(`解析完成:${course.sections.length} 章节,构建课程…`);\r\n\r\n return {\r\n course,\r\n detection,\r\n fetchedFiles: fetchResult.ok,\r\n readmeBranch,\r\n readmeMd,\r\n };\r\n}\r\n\r\n/* ============================================================\r\n * 多语言:从 README 检测翻译语言 + 拉取翻译版课程内容\r\n * ============================================================ */\r\n\r\n/** 从 markdown 链接中提取翻译语言列表 */\r\nexport function extractLanguagesFromReadme(readmeMd: string): { code: string; name: string }[] {\r\n // 匹配 [语言名](./translations/xx-XX/README.md) 或 [语言名](translations/xx-XX/README.md)\r\n const pattern = /\\[([^\\]]+)\\]\\(\\.?\\/?translations\\/([^/)]+)\\/README\\.md\\)/g;\r\n const langs: { code: string; name: string }[] = [];\r\n const seen = new Set<string>();\r\n let m;\r\n while ((m = pattern.exec(readmeMd)) !== null) {\r\n const name = m[1]!.trim();\r\n const code = m[2]!.trim();\r\n if (!seen.has(code)) {\r\n seen.add(code);\r\n langs.push({ code, name });\r\n }\r\n }\r\n return langs;\r\n}\r\n\r\n/**\r\n * 从 GitHub 仓库检测可用翻译语言。\r\n * 拉根 README → 提取翻译链接 → 返回语言列表(空 = 无翻译)。\r\n */\r\nexport async function detectRepoLanguages(\r\n owner: string,\r\n repo: string,\r\n branch: string,\r\n fetchFn: typeof fetch,\r\n): Promise<{ code: string; name: string }[]> {\r\n // 试 main + master\r\n const branches = branch === \"master\" ? [\"master\", \"main\"] : [\"main\", \"master\"];\r\n for (const br of branches) {\r\n try {\r\n const r = await fetchFn(cdnUrl(owner, repo, br, \"README.md\"));\r\n if (r.ok) {\r\n const readme = await r.text();\r\n return extractLanguagesFromReadme(readme);\r\n }\r\n } catch {\r\n // 试下一个\r\n }\r\n }\r\n return [];\r\n}\r\n\r\n/** 翻译版文件条目 */\r\nexport interface TranslatedFile {\r\n /** 原文路径(用于和 content_nodes 对齐) */\r\n originalPath: string;\r\n /** 翻译版路径(translations/<code>/...) */\r\n translatedPath: string;\r\n title: string;\r\n md: string;\r\n}\r\n\r\n/**\r\n * 净化翻译版 markdown —— 翻译内容是 CDN 原样拉取的,未经原文管道的\r\n * code-fence-aware parser 处理,可能含畸形结构导致 react-markdown 崩溃。\r\n *\r\n * 处理:\r\n * 1. 未闭合代码围栏:统计 ``` 和 ~~~ 数量,奇数则补一个闭合围栏\r\n * 2. 去除 <script>/<style>/<iframe> 等危险 HTML(防 XSS + 防渲染崩溃)\r\n * 3. 去 BOM、统一换行\r\n *\r\n * 这是确定性规则处理(高置信度),不是 LLM 判断 —— 格式修复是规则擅长的。\r\n */\r\nexport function sanitizeTranslatedMarkdown(md: string): string {\r\n let s = md.replace(/^\\uFEFF/, \"\"); // BOM\r\n s = s.replace(/\\r\\n/g, \"\\n\").replace(/\\r/g, \"\\n\"); // 统一换行\r\n\r\n // 1. 代码围栏平衡:逐行状态机检测未闭合围栏\r\n const lines = s.split(\"\\n\");\r\n let fence: string | null = null; // 当前围栏类型(\"```\" 或 \"~~~\")\r\n for (const line of lines) {\r\n const m = line.match(/^\\s*(```|~~~)/);\r\n if (m) {\r\n fence = fence ? null : m[1]!; // 切换状态\r\n }\r\n }\r\n if (fence) {\r\n // 围栏没闭合 → 补一个\r\n s = s + \"\\n\" + fence + \"\\n\";\r\n }\r\n\r\n // 2. 去除危险 HTML 标签(script/style/iframe/object/embed)\r\n // react-markdown 默认不渲染 raw HTML(除非 rehype-raw),但保险起见仍剥离\r\n s = s.replace(/<script\\b[^<]*(?:(?!<\\/script>)<[^<]*)*<\\/script>/gi, \"\");\r\n s = s.replace(/<style\\b[^<]*(?:(?!<\\/style>)<[^<]*)*<\\/style>/gi, \"\");\r\n s = s.replace(/<\\/?(iframe|object|embed)\\b[^>]*>/gi, \"\");\r\n\r\n return s.trim();\r\n}\r\n\r\n/**\r\n * 拉取翻译版课程内容。\r\n *\r\n * 策略:不依赖翻译版 README 的链接(很多翻译只翻译了大纲,README 里没有 lesson 链接,\r\n * 或链接指向其他语言翻译)。直接用原文课程的文件路径,在前面加 translations/<code>/\r\n * 前缀去探测翻译版是否存在。5 并发拉取,404 跳过(该课无翻译)。\r\n *\r\n * @returns Map<originalPath, { title, content }> — key 是原文路径\r\n */\r\nexport async function fetchTranslatedContent(\r\n owner: string,\r\n repo: string,\r\n branch: string,\r\n langCode: string,\r\n originalFiles: FetchedFile[],\r\n fetchFn: typeof fetch,\r\n onProgress?: (msg: string) => void,\r\n): Promise<Map<string, { title: string; content: string }>> {\r\n const send = (msg: string) => onProgress?.(msg);\r\n const result = new Map<string, { title: string; content: string }>();\r\n\r\n // 先确认翻译版 README 存在(不存在说明该语言完全没翻译)\r\n const transReadmeUrl = cdnUrl(owner, repo, branch, `translations/${langCode}/README.md`);\r\n send(`检查翻译版 README (${langCode})…`);\r\n try {\r\n const r = await fetchFn(transReadmeUrl);\r\n if (!r.ok) {\r\n send(`翻译版不存在 (${r.status}),跳过`);\r\n return result;\r\n }\r\n } catch {\r\n send(\"翻译版检查失败,跳过\");\r\n return result;\r\n }\r\n\r\n // 只对 .md 文件探测翻译版(.ipynb 通常不翻译)\r\n const mdFiles = originalFiles.filter((f) => !f.path.toLowerCase().endsWith(\".ipynb\"));\r\n send(`探测 ${mdFiles.length} 个文件的翻译版(${langCode})…`);\r\n\r\n const transPrefix = `translations/${langCode}/`;\r\n const CONCURRENCY = 5;\r\n let done = 0;\r\n\r\n for (let i = 0; i < mdFiles.length; i += CONCURRENCY) {\r\n const batch = mdFiles.slice(i, i + CONCURRENCY);\r\n const results = await Promise.allSettled(\r\n batch.map(async (file) => {\r\n const transPath = transPrefix + file.path;\r\n const url = cdnUrl(owner, repo, branch, transPath);\r\n const r = await fetchFn(url);\r\n if (!r.ok) return null; // 该文件无翻译\r\n const md = await r.text();\r\n return { originalPath: file.path, title: file.title, content: sanitizeTranslatedMarkdown(md) };\r\n }),\r\n );\r\n for (const res of results) {\r\n done++;\r\n if (res.status === \"fulfilled\" && res.value) {\r\n result.set(res.value.originalPath, { title: res.value.title, content: res.value.content });\r\n }\r\n }\r\n if (done % 10 === 0 || done === mdFiles.length) {\r\n send(`翻译探测 ${done}/${mdFiles.length}(命中 ${result.size})`);\r\n }\r\n }\r\n\r\n send(`翻译版拉取完成: ${result.size}/${mdFiles.length} 文件有翻译`);\r\n return result;\r\n}\r\n\r\n/* ============================================================\r\n * 新智能导入管线: Step 1 (fetchRepoInventory) + Step 3 (fetchFileOutlines)\r\n * ============================================================ */\r\n\r\n/** 仓库清单 —— Step 1 的输出 */\r\nexport interface RepoInventory {\r\n /** README 全文 */\r\n readmeMd: string;\r\n /** 课程文件路径列表(供 Step 3+5 拉正文用,已过滤翻译/元数据) */\r\n fileList: DiscoveredFile[];\r\n /** 完整目录树(所有文件路径,含 translations/images/lab 等,供 LLM 看) */\r\n fullTree: string[];\r\n /** README 实际使用的分支 */\r\n branch: string;\r\n /** 仓库检测结果 */\r\n detection: DetectionResult;\r\n}\r\n\r\n/**\r\n * Step 1: 拉取仓库清单 —— README 全文 + 完整目录树 + 课程文件列表。\r\n *\r\n * 三样东西:\r\n * 1. README 全文 → 给 LLM 看课程大纲\r\n * 2. 完整目录树(所有路径) → 给 LLM 看仓库结构(translations/、images/、lab/ 等)\r\n * 3. 课程文件列表(filterLessonFiles 过滤后) → 供 Step 3+5 拉正文用\r\n *\r\n * 不拉正文。\r\n */\r\nexport async function fetchRepoInventory(\r\n owner: string,\r\n repo: string,\r\n branch: string,\r\n fetchFn: typeof fetch,\r\n onProgress?: (msg: string) => void,\r\n): Promise<RepoInventory> {\r\n const send = (msg: string) => onProgress?.(msg);\r\n\r\n // 1. 拉 README(多候选文件名 + 多分支)\r\n send(\"正在拉取 README…\");\r\n const branches = branch === \"master\" ? [\"master\", \"main\", \"develop\", \"gh-pages\"]\r\n : branch === \"main\" ? [\"main\", \"master\", \"develop\", \"gh-pages\"]\r\n : [branch, \"main\", \"master\"];\r\n // README 候选文件名(按优先级)\r\n const readmeCandidates = [\"README.md\", \"readme.md\", \"README.MD\", \"README.rst\", \"README.adoc\", \"index.md\", \"home.md\", \"SUMMARY.md\"];\r\n let readmeMd: string | null = null;\r\n let readmeBranch = branch;\r\n outer: for (const br of branches) {\r\n for (const candidate of readmeCandidates) {\r\n try {\r\n const r = await fetchFn(cdnUrl(owner, repo, br, candidate));\r\n if (r.ok) {\r\n readmeMd = await r.text();\r\n readmeBranch = br;\r\n break outer;\r\n }\r\n } catch {\r\n // network error, try next\r\n }\r\n }\r\n }\r\n if (!readmeMd) throw new Error(`无法拉取 README(试过分支: ${branches.join(\", \")},文件名: ${readmeCandidates.join(\", \")})`);\r\n send(`README 拉取成功(${readmeMd.length} 字符,分支 ${readmeBranch})`);\r\n\r\n // 2. 检测形态\r\n const detection = detectRepoPattern(readmeMd);\r\n if (detection.pattern === \"unsupported\") {\r\n throw new Error(`仓库不支持: ${detection.reason}`);\r\n }\r\n\r\n // 3. 课程文件列表(初始:README 链接发现)\r\n let fileList = filterLessonFiles(detection.lessonFiles ?? []);\r\n send(`README 链接发现 ${fileList.length} 个课程文件`);\r\n\r\n // 4. 完整目录树 + 用文件树补全 fileList(README 链接会漏文件)\r\n // 总是拉取文件树:既供 LLM 看仓库结构,又补全 README 表格没列全的课程文件\r\n let fullTree: string[] = fileList.map((f) => f.path);\r\n try {\r\n send(\"扫描仓库完整目录结构…\");\r\n const tree = await fetchRepoFileTree(owner, repo, readmeBranch, fetchFn);\r\n if (tree.paths.length > 0) {\r\n fullTree = tree.paths;\r\n // 用文件树的内容文件补全 fileList(README 表格可能没列全所有 .md/.ipynb)\r\n const treeFiles = pathsToDiscoveredFiles(tree.paths);\r\n const treeLessonFiles = filterLessonFiles(treeFiles).filter((f) => f.kind !== \"other\");\r\n const existing = new Set(fileList.map((f) => f.path));\r\n const added = treeLessonFiles.filter((f) => !existing.has(f.path));\r\n if (added.length > 0) {\r\n fileList = [...fileList, ...added];\r\n send(`文件树补充 ${added.length} 个,共 ${fileList.length} 个课程文件`);\r\n }\r\n send(`目录树: ${fullTree.length} 个文件/目录`);\r\n }\r\n } catch {\r\n send(\"目录树拉取失败,使用 README 链接列表\");\r\n }\r\n\r\n // docs-rich 模式下文件树也没找到课程文件 → 不支持\r\n if (fileList.length === 0) {\r\n throw new Error(`未找到课程文件(README 无链接且文件树无可识别的文档/代码文件)`);\r\n }\r\n\r\n // 上限\r\n if (fileList.length > MAX_FILES) {\r\n send(`文件数 ${fileList.length} 超过上限 ${MAX_FILES},截断`);\r\n fileList = fileList.slice(0, MAX_FILES);\r\n }\r\n\r\n return { readmeMd, fileList, fullTree, branch: readmeBranch, detection };\r\n}\r\n\r\n/** 文件标题大纲 —— Step 3 的输出 */\r\nexport interface FileOutline {\r\n /** 文件 H1 标题(第一个 # 开头的行) */\r\n h1: string;\r\n /** 文件总字符数(全文,含正文)—— 长文件拆分决策依据 */\r\n totalChars: number;\r\n /** H2/H3 标题列表(不含正文)+ 每段字符数(到下一个同级或更高级标题) */\r\n headings: { level: number; title: string; chars: number }[];\r\n}\r\n\r\n/**\r\n * Step 3: 批量提取文件的标题大纲(H1/H2/H3 + 每段字符数,不含正文)。\r\n * 拉取完整文件文本(不只前 N 行),因为字符数统计需要全文。\r\n * 并发度 5,同 fetchMarkdownContents。\r\n */\r\nexport async function fetchFileOutlines(\r\n filePaths: string[],\r\n owner: string,\r\n repo: string,\r\n branch: string,\r\n fetchFn: typeof fetch,\r\n onProgress?: (done: number, total: number, path: string) => void,\r\n): Promise<Map<string, FileOutline>> {\r\n const result = new Map<string, FileOutline>();\r\n const CONCURRENCY = 5;\r\n\r\n for (let i = 0; i < filePaths.length; i += CONCURRENCY) {\r\n const batch = filePaths.slice(i, i + CONCURRENCY);\r\n const results = await Promise.allSettled(\r\n batch.map(async (filePath) => {\r\n const url = cdnUrl(owner, repo, branch, filePath);\r\n const r = await fetchFn(url);\r\n if (!r.ok) return null;\r\n const text = await r.text();\r\n const outline = extractOutlineWithCharCounts(text, filePath);\r\n return { path: filePath, outline };\r\n }),\r\n );\r\n for (let j = 0; j < results.length; j++) {\r\n const res = results[j];\r\n if (res.status === \"fulfilled\" && res.value) {\r\n result.set(res.value.path, res.value.outline);\r\n }\r\n onProgress?.(i + j + 1, filePaths.length, batch[j] ?? \"\");\r\n }\r\n }\r\n\r\n return result;\r\n}\r\n\r\n/**\r\n * 从 markdown 文本提取 H1/H2/H3 标题 + 每段字符数。\r\n * 字符数 = 该标题行到下一个同级或更高级标题之间的字符数。\r\n * H2 边界:下一个 H1/H2;H3 边界:下一个 H1/H2/H3。\r\n * 代码块内的 # 不算标题。\r\n */\r\nexport function extractOutlineWithCharCounts(text: string, filePath: string): FileOutline {\r\n const lines = text.split(/\\r?\\n/);\r\n const totalChars = text.length;\r\n let h1 = \"\";\r\n // 先收集所有标题行(带行号)\r\n const rawHeadings: { level: number; title: string; line: number }[] = [];\r\n let inCodeFence = false;\r\n for (let i = 0; i < lines.length; i++) {\r\n const line = lines[i]!;\r\n if (/^(\\s*)(```|~~~)/.test(line)) { inCodeFence = !inCodeFence; continue; }\r\n if (inCodeFence) continue;\r\n const h1Match = line.match(/^#\\s+(.+)$/);\r\n if (h1Match) {\r\n if (!h1) h1 = h1Match[1]!.trim();\r\n rawHeadings.push({ level: 1, title: h1Match[1]!.trim(), line: i });\r\n continue;\r\n }\r\n const h2Match = line.match(/^##\\s+(.+)$/);\r\n if (h2Match) { rawHeadings.push({ level: 2, title: h2Match[1]!.trim(), line: i }); continue; }\r\n const h3Match = line.match(/^###\\s+(.+)$/);\r\n if (h3Match) { rawHeadings.push({ level: 3, title: h3Match[1]!.trim(), line: i }); continue; }\r\n }\r\n // 计算每个 H2/H3 的字符数(到下一个同级或更高级标题)\r\n const headings: { level: number; title: string; chars: number }[] = [];\r\n for (let idx = 0; idx < rawHeadings.length; idx++) {\r\n const h = rawHeadings[idx]!;\r\n if (h.level === 1) continue; // H1 不进 headings\r\n // 找下一个 level <= h.level 的标题行号\r\n let endLine = lines.length;\r\n for (let j = idx + 1; j < rawHeadings.length; j++) {\r\n if (rawHeadings[j]!.level <= h.level) { endLine = rawHeadings[j]!.line; break; }\r\n }\r\n const sectionText = lines.slice(h.line, endLine).join(\"\\n\");\r\n headings.push({ level: h.level, title: h.title, chars: sectionText.length });\r\n }\r\n // .ipynb: 没有 markdown 标题,用文件名\r\n if (!h1 && filePath.endsWith(\".ipynb\")) {\r\n h1 = filePath.split(\"/\").pop()?.replace(/\\.ipynb$/i, \"\") ?? filePath;\r\n }\r\n return { h1: h1 || (filePath.split(\"/\").pop() ?? filePath), totalChars, headings };\r\n}\r\n\r\n/**\r\n * 拉取单个文件的完整正文(Step 5a 用)。\r\n * 复用 fetchMarkdownContents 的解析逻辑(.ipynb → parseNotebook, .rst → rst-parser 等),\r\n * 但只拉一个文件,不做批量。\r\n */\r\nexport async function fetchSingleFileContent(\r\n filePath: string,\r\n owner: string,\r\n repo: string,\r\n branch: string,\r\n fetchFn: typeof fetch,\r\n): Promise<string | null> {\r\n try {\r\n const url = cdnUrl(owner, repo, branch, filePath);\r\n const r = await fetchFn(url);\r\n if (!r.ok) return null;\r\n const lower = filePath.toLowerCase();\r\n if (lower.endsWith(\".ipynb\")) {\r\n const { parseNotebook } = await import(\"./notebook-parser.js\");\r\n const jsonText = await r.text();\r\n const nbResult = parseNotebook(jsonText);\r\n return nbResult.markdown;\r\n }\r\n const text = await r.text();\r\n if (lower.endsWith(\".rst\")) {\r\n const { parseRst } = await import(\"./rst-parser.js\");\r\n return parseRst(text).markdown;\r\n }\r\n if (lower.endsWith(\".rmd\")) {\r\n const { parseRmd } = await import(\"./rmd-parser.js\");\r\n return parseRmd(text).markdown;\r\n }\r\n if (lower.endsWith(\".org\")) {\r\n const { parseOrg } = await import(\"./org-parser.js\");\r\n return parseOrg(text).markdown;\r\n }\r\n if (lower.endsWith(\".adoc\")) {\r\n const { parseAdoc } = await import(\"./adoc-parser.js\");\r\n return parseAdoc(text).markdown;\r\n }\r\n // 代码文件 → code-parser 转 markdown\r\n if (CODE_EXTENSIONS.some((ext) => lower.endsWith(ext))) {\r\n const ext = lower.split(\".\").pop() ?? \"\";\r\n const { parseCode } = await import(\"./code-parser.js\");\r\n return parseCode(text, ext).markdown;\r\n }\r\n return text;\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\n/**\r\n * 下载图片并转 base64 data-url(Step 5b 用)。\r\n * 返回 data:url 或 null(下载失败/太大)。\r\n */\r\nexport async function fetchImageAsDataUrl(\r\n imgPath: string,\r\n owner: string,\r\n repo: string,\r\n branch: string,\r\n fetchFn: typeof fetch,\r\n maxBytes = 200_000,\r\n): Promise<string | null> {\r\n try {\r\n const url = cdnUrl(owner, repo, branch, imgPath);\r\n const r = await fetchFn(url);\r\n if (!r.ok) return null;\r\n const buf = Buffer.from(await r.arrayBuffer());\r\n if (buf.length > maxBytes) return null; // 太大不内联\r\n const ext = imgPath.split(\".\").pop()?.toLowerCase() ?? \"png\";\r\n const mime =\r\n ext === \"png\" ? \"image/png\"\r\n : ext === \"jpg\" || ext === \"jpeg\" ? \"image/jpeg\"\r\n : ext === \"gif\" ? \"image/gif\"\r\n : ext === \"webp\" ? \"image/webp\"\r\n : ext === \"svg\" ? \"image/svg+xml\"\r\n : ext === \"bmp\" ? \"image/bmp\"\r\n : \"image/png\";\r\n return `data:${mime};base64,${buf.toString(\"base64\")}`;\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\n\r\n","/**\n * Pure UI projections for the study tools: human-readable display lines\n * derived from canonical tool values. Shared by `output.render` text and\n * `presentationMeta`/`presentResult` cards; no IO, no clock, no randomness.\n * @module dsh-plugin-lookatstudy/cards\n */\n\n/** Canonical value of the three import tools. */\nexport interface ImportValue {\n courseId: string\n title: string\n sections: number\n lessons: number\n firstLessonId: string | null\n firstLessonTitle: string | null\n}\n\n/** Canonical value of `study_map`. */\nexport interface MapValue {\n courseId: string\n title: string\n counts: { total: number; mastered: number; available: number }\n tree: Array<{\n title: string\n lessons: Array<{\n id: string\n title: string\n kind: string\n status: string\n masteryPct: number | null\n crown: number\n weakConcepts: number\n frictionCount: number\n }>\n }>\n}\n\n/** Canonical value of `study_record_answer`. */\nexport interface AnswerValue {\n lessonTitle: string\n correct: boolean\n prevMasteryPct: number\n newMasteryPct: number\n crown: number\n mastered: boolean\n}\n\n/** Canonical value of `study_due_reviews`. */\nexport interface DueValue {\n total: number\n due: Array<{ lessonId: string; courseTitle: string; lessonTitle: string; dueAt: string; overdueDays: number }>\n}\n\n/** Canonical value of `study_record_review`. */\nexport interface ReviewValue {\n lessonTitle: string\n quality: number\n intervalDays: number\n repetitions: number\n dueAt: string\n}\n\n/** Canonical value of `study_complete_lesson`. */\nexport interface CompleteValue {\n lessonTitle: string\n unlockedLessonTitles: string[]\n reviewDueAt: string\n courseComplete: boolean\n}\n\n/**\n * Status glyph for one lesson line on the map (LookatStudy's map icons).\n * @param kind - lesson kind.\n * @param status - lesson status.\n * @returns the glyph prefix.\n */\nfunction statusGlyph(kind: string, status: string): string {\n if (kind === 'exam') return '🎯'\n if (status === 'mastered') return '👑'\n if (status === 'in_progress') return '📖'\n if (status === 'available') return '⭐'\n return '🔒'\n}\n\n/**\n * Display lines for an import result.\n * @param value - import tool value.\n * @returns card lines.\n */\nexport function importLines(value: ImportValue): string[] {\n const lines = [\n `📘 ${value.title}`,\n `${value.sections} sections · ${value.lessons} lessons · id ${value.courseId}`,\n ]\n if (value.firstLessonId !== null) {\n lines.push(`Start at “${value.firstLessonTitle}” (${value.firstLessonId})`)\n }\n return lines\n}\n\n/**\n * Display lines for the skill-tree map.\n * @param value - map tool value.\n * @returns card lines.\n */\nexport function mapLines(value: MapValue): string[] {\n const lines = [\n `🗺 ${value.title} — ${value.counts.mastered}/${value.counts.total} mastered`,\n ]\n for (const section of value.tree) {\n lines.push(`▍${section.title}`)\n for (const lesson of section.lessons) {\n const mastery = lesson.masteryPct === null ? '' : ` · ${lesson.masteryPct}%${lesson.crown >= 4 ? ' 👑' : ''}`\n const weak = lesson.weakConcepts > 0 ? ` · ⚡${lesson.weakConcepts}` : ''\n const friction = lesson.frictionCount > 0 ? ` · 😣${lesson.frictionCount}` : ''\n lines.push(` ${statusGlyph(lesson.kind, lesson.status)} ${lesson.title}${mastery}${weak}${friction}`)\n }\n }\n return lines\n}\n\n/**\n * Display line for a graded answer.\n * @param value - answer tool value.\n * @returns single feedback line.\n */\nexport function answerLine(value: AnswerValue): string {\n const mark = value.correct ? '✓ correct' : '✗ incorrect'\n const crown = value.mastered ? ' · 👑 mastered' : ''\n return `${mark} — mastery ${value.prevMasteryPct}% → ${value.newMasteryPct}% (crown ${value.crown})${crown}`\n}\n\n/**\n * Display lines for the due-review list.\n * @param value - due tool value.\n * @returns card lines.\n */\nexport function dueLines(value: DueValue): string[] {\n if (value.total === 0) return ['🎉 No reviews due — everything is scheduled ahead.']\n const lines = [`🔁 ${value.total} due`]\n for (const item of value.due) {\n const overdue = item.overdueDays > 0 ? ` · ${item.overdueDays}d overdue` : ''\n lines.push(` ⏰ ${item.lessonTitle} — ${item.courseTitle}${overdue}`)\n }\n return lines\n}\n\n/**\n * Display line for a recorded review grade.\n * @param value - review tool value.\n * @returns single schedule line.\n */\nexport function reviewLine(value: ReviewValue): string {\n return `🔁 quality ${value.quality}/5 — next review in ${value.intervalDays}d (${value.repetitions} in a row), due ${value.dueAt.slice(0, 10)}`\n}\n\n/**\n * Display lines for a completed lesson.\n * @param value - complete tool value.\n * @returns card lines.\n */\nexport function completeLines(value: CompleteValue): string[] {\n const lines = [`🎓 Mastered “${value.lessonTitle}”`]\n for (const title of value.unlockedLessonTitles) {\n lines.push(`🔓 Unlocked “${title}”`)\n }\n lines.push(`🔁 First review due ${value.reviewDueAt.slice(0, 10)}`)\n if (value.courseComplete) lines.push('🏁 Course complete!')\n return lines\n}\n","/**\n * The `study_*` tool surface, ported from LookatStudy's agent contract:\n * import (markdown / folder / GitHub), course map, lesson content with\n * concepts/starters/memory, KC-attributed answer recording with\n * mastery-driven progression, spaced reviews, mastery proposals, friction\n * logging, learner memory, Cornell notes, and soul switching. All state\n * mutations persist synchronously through the shared store.\n * @module dsh-plugin-lookatstudy/tools\n */\n\nimport { existsSync } from 'node:fs'\nimport { basename } from 'node:path'\nimport { defineTool } from '@deepseek-ai/dsh-tools'\nimport type { ToolDefinition } from '@deepseek-ai/dsh-tools'\nimport { parseMarkdownToCourse } from './vendor/markdown-course.ts'\nimport type { ParsedCourse } from './vendor/markdown-course.ts'\nimport { scanFolder } from './vendor/local-folder-scanner.ts'\nimport { buildCourseFromFiles, importRepoToParsedCourse } from './vendor/repo-fetcher.ts'\nimport type { FetchedFile } from './vendor/repo-fetcher.ts'\nimport type { ReviewQuality } from './vendor/sm2.ts'\nimport { masteryToCrown } from './vendor/bkt.ts'\nimport * as cards from './cards.ts'\nimport {\n NEAR_MASTERED_THRESHOLD,\n addFriction,\n addNote,\n attemptLesson,\n completeLesson,\n conceptViews,\n courseSummaries,\n deleteCourse,\n dueReviews,\n findCourse,\n findLesson,\n importCourse,\n nextLesson,\n proposeMastery,\n recordAnswer,\n recordReview,\n resolveProposal,\n setMemory,\n starterPrompts,\n defineConcepts as defineConceptsState,\n strategyBand,\n type CourseState,\n type LearningState,\n type LessonRef,\n} from './state.ts'\n\n/** State access handed in by `apply`; every mutation persists via {@link StudyStore.save}. */\nexport interface StudyStore {\n /** Live learning state. */\n get(): LearningState\n /** Persist the current state to disk. */\n save(): void\n}\n\n/** SM-2 quality grades, shared by the parameter enum and the state layer. */\nconst QUALITIES = [0, 1, 2, 3, 4, 5] as const\nconst LESSON_STATUSES = ['locked', 'available', 'in_progress', 'mastered'] as const\nconst LESSON_KINDS = ['study', 'practice', 'exam'] as const\nconst FRICTION_CATEGORIES = ['confused', 'blocked', 'frustrated'] as const\nconst MEMORY_CATEGORIES = ['global', 'pattern', 'lesson'] as const\nconst NOTE_ZONES = ['understand', 'record', 'practice'] as const\nconst NOTE_SOURCES = ['ai', 'content', 'chat'] as const\nconst MODES = ['direct', 'guide', 'practice'] as const\n\nconst nullableInteger = { oneOf: [{ type: 'integer' as const }, { type: 'null' as const }] }\nconst nullableString = { oneOf: [{ type: 'string' as const }, { type: 'null' as const }] }\n\n/**\n * Fail loud when an importer produced no lessons — a course with an empty\n * path is useless and hides upstream parsing problems. Runs before any state\n * mutation so a failed import leaves persisted state untouched.\n * @param parsed - parsed course about to be imported.\n */\nfunction requireParsedLessons(parsed: ParsedCourse): void {\n const count = parsed.sections.reduce((n, s) => n + s.lessons.length, 0)\n if (count === 0) {\n throw new Error(\n 'lookatstudy-plugin: import produced 0 lessons — the source needs ## sections containing ### lessons, or lesson-like files in a folder',\n )\n }\n}\n\n/** Canonical value shared by the three import tools. */\nfunction toImportValue(course: CourseState): cards.ImportValue {\n const lessons = course.sections.flatMap(s => s.lessons)\n const first = lessons.find(l => l.status === 'available') ?? lessons[0]!\n return {\n courseId: course.id,\n title: course.title,\n sections: course.sections.length,\n lessons: lessons.length,\n firstLessonId: first.id,\n firstLessonTitle: first.title,\n }\n}\n\n/** Canonical value of `study_map`. */\nfunction toMapValue(course: CourseState): cards.MapValue {\n const lessons = course.sections.flatMap(s => s.lessons)\n return {\n courseId: course.id,\n title: course.title,\n counts: {\n total: lessons.length,\n mastered: lessons.filter(l => l.status === 'mastered').length,\n available: lessons.filter(l => l.status === 'available').length,\n },\n tree: course.sections.map(section => ({\n title: section.title,\n lessons: section.lessons.map(lesson => ({\n id: lesson.id,\n title: lesson.title,\n kind: lesson.kind,\n status: lesson.status,\n masteryPct: lesson.mastery === null ? null : Math.round(lesson.mastery * 100),\n crown: masteryToCrown(lesson.mastery),\n weakConcepts: (conceptViews(lesson) ?? []).filter(c => c.weak).length,\n frictionCount: lesson.friction.length,\n })),\n })),\n }\n}\n\n/** Canonical value of `study_lesson`. */\nfunction toLessonValue(ref: LessonRef, state: LearningState) {\n const next = nextLesson(ref.course, ref.lesson.id)\n const pending = state.proposals.find(p => p.lessonId === ref.lesson.id && p.status === 'pending')\n return {\n lessonId: ref.lesson.id,\n courseId: ref.course.id,\n courseTitle: ref.course.title,\n sectionTitle: ref.section.title,\n title: ref.lesson.title,\n kind: ref.lesson.kind,\n status: ref.lesson.status,\n body: ref.lesson.body,\n masteryPct: ref.lesson.mastery === null ? null : Math.round(ref.lesson.mastery * 100),\n crown: masteryToCrown(ref.lesson.mastery),\n attempts: ref.lesson.attempts,\n correctCount: ref.lesson.correctCount,\n strategy: strategyBand(ref.lesson.mastery),\n concepts: conceptViews(ref.lesson),\n starters: starterPrompts(ref.lesson.title),\n memory: {\n lesson: ref.lesson.memory,\n global: state.memoryGlobal,\n pattern: state.memoryPatterns[ref.course.id] ?? null,\n },\n noteCount: ref.lesson.notes.length,\n pendingProposal: pending === undefined ? null : { id: pending.id, rationale: pending.rationale },\n nextLessonId: next?.id ?? null,\n }\n}\n\n/**\n * Parse a GitHub repository URL into owner/repo.\n * @param url - `https://github.com/<owner>/<repo>` (`.git` suffix and subpaths tolerated).\n * @returns owner and repo.\n */\nfunction parseGithubUrl(url: string): { owner: string; repo: string } {\n const match = url.match(/^(?:https?:\\/\\/)?github\\.com\\/([A-Za-z0-9_.-]+)\\/([A-Za-z0-9_.-]+?)(?:\\.git)?(?:[/?#].*)?$/)\n if (!match) {\n throw new Error(`lookatstudy-plugin: not a GitHub repository URL: ${JSON.stringify(url)} (expected https://github.com/<owner>/<repo>)`)\n }\n return { owner: match[1]!, repo: match[2]! }\n}\n\n/** Wrap `fetch` so cancellation of the tool call aborts in-flight repo fetches. */\nfunction signalFetch(signal: AbortSignal): typeof fetch {\n return (input, init) => fetch(input, { ...init, signal })\n}\n\n/** Total over a missing `meta` (events logged before a presentationMeta existed): renders nothing instead of throwing into the presenter fallback. */\nconst textBlocks = (lines: readonly string[] | undefined | null): Array<{ type: 'text'; text: string }> => (lines ?? []).map(text => ({ type: 'text', text }) as const)\n\n/**\n * Build the full study tool set over one store.\n * @param store - state store owned by `apply`.\n * @returns tool definitions ready for `ctx.tools.register`.\n */\nexport function studyTools(store: StudyStore): ToolDefinition[] {\n /** Run a mutating state operation and persist. */\n const mutate = <T>(fn: (state: LearningState) => T): T => {\n const result = fn(store.get())\n store.save()\n return result\n }\n\n const importOutput = {\n schema: {\n type: 'object',\n additionalProperties: false,\n properties: {\n courseId: { type: 'string', required: true },\n title: { type: 'string', required: true },\n sections: { type: 'integer', required: true },\n lessons: { type: 'integer', required: true },\n firstLessonId: { type: 'string', required: true },\n firstLessonTitle: { type: 'string', required: true },\n },\n },\n }\n const importPresent = {\n presentationMeta: (_args: unknown, value: cards.ImportValue) => cards.importLines(value),\n presentResult: (_args: unknown, result: { meta: unknown }) => ({\n card: 'generic',\n content: textBlocks(result.meta as string[]),\n }),\n }\n\n const importMarkdown = defineTool({\n name: 'study_import_markdown',\n description:\n 'Import pasted markdown as a structured course: H2 (##) becomes a section, H3 (###) a lesson. '\n + 'Use for notes, single long documents, or content fetched by other means.',\n parameters: {\n markdown: { type: 'string', required: true, description: 'The full markdown source of the course.' },\n title: { type: 'string', description: 'Optional course title overriding the first H1.' },\n },\n output: {\n ...importOutput,\n render: (_args, value) => [{\n type: 'text',\n text: `Imported course “${value.title}” (${value.sections} sections, ${value.lessons} lessons). `\n + `First lesson: “${value.firstLessonTitle}” (id ${value.firstLessonId}).`,\n }],\n },\n async execute(args) {\n const parsed = parseMarkdownToCourse(args.markdown)\n if (args.title !== undefined) parsed.title = args.title\n requireParsedLessons(parsed)\n return mutate(state => toImportValue(importCourse(state, parsed, 'markdown', 'pasted markdown')))\n },\n presentCall: args => ({ card: 'generic', title: `Import markdown course${args.title === undefined ? '' : `: ${args.title}`}`, kind: 'read' }),\n ...importPresent,\n })\n\n const importFolder = defineTool({\n name: 'study_import_folder',\n description:\n 'Import a local folder as a course: markdown, txt, html, Jupyter notebooks, rst/Rmd/org/adoc, '\n + 'and 30+ code file types become lessons grouped into sections by directory (code is teaching material too). '\n + 'PDF/PPTX are not supported in this edition.',\n parameters: {\n path: { type: 'string', required: true, description: 'Absolute path of the folder to scan.' },\n title: { type: 'string', description: 'Optional course title overriding the folder name.' },\n },\n output: {\n ...importOutput,\n render: (_args, value) => [{\n type: 'text',\n text: `Imported folder course “${value.title}” (${value.sections} sections, ${value.lessons} lessons). `\n + `First lesson: “${value.firstLessonTitle}” (id ${value.firstLessonId}).`,\n }],\n },\n async execute(args) {\n if (!existsSync(args.path)) {\n throw new Error(`lookatstudy-plugin: folder does not exist: ${args.path}`)\n }\n const docs = await scanFolder(args.path)\n const files: FetchedFile[] = docs.map(doc => ({ path: doc.path, title: doc.title, md: doc.content }))\n const title = args.title ?? basename(args.path.replaceAll('\\\\', '/'))\n const parsed = buildCourseFromFiles(title, files)\n requireParsedLessons(parsed)\n return mutate(state => toImportValue(importCourse(state, parsed, 'folder', args.path)))\n },\n timeoutMs: 60_000,\n presentCall: args => ({ card: 'generic', title: `Scan folder: ${args.path}`, kind: 'read', rawInput: args.path }),\n ...importPresent,\n })\n\n const importGithub = defineTool({\n name: 'study_import_github',\n description:\n 'Import a GitHub learning repository as a course. Discovery follows the README outline, files are '\n + 'fetched through the jsDelivr CDN (works where github.com is unreachable). Best for curated '\n + 'curricula (e.g. microsoft/AI-For-Beginners); awesome-lists are rejected.',\n parameters: {\n url: { type: 'string', required: true, description: 'Repository URL, e.g. https://github.com/microsoft/AI-For-Beginners.' },\n branch: { type: 'string', description: 'Branch to read (main tried, then master); defaults to main.' },\n },\n output: {\n ...importOutput,\n render: (_args, value) => [{\n type: 'text',\n text: `Imported GitHub course “${value.title}” (${value.sections} sections, ${value.lessons} lessons). `\n + `First lesson: “${value.firstLessonTitle}” (id ${value.firstLessonId}).`,\n }],\n },\n async execute(args, exec) {\n const { owner, repo } = parseGithubUrl(args.url)\n const branch = args.branch ?? 'main'\n const result = await importRepoToParsedCourse(owner, repo, branch, signalFetch(exec.signal))\n requireParsedLessons(result.course)\n return mutate(state => toImportValue(importCourse(state, result.course, 'github', args.url)))\n },\n timeoutMs: 180_000,\n presentCall: args => ({ card: 'generic', title: `Import GitHub course: ${args.url}`, kind: 'fetch' }),\n ...importPresent,\n })\n\n const listCourses = defineTool({\n name: 'study_courses',\n description: 'List imported courses with progress, average mastery, due reviews, and the current lesson id.',\n parameters: {},\n output: {\n schema: {\n type: 'object',\n additionalProperties: false,\n properties: {\n total: { type: 'integer', required: true },\n courses: {\n type: 'array',\n required: true,\n items: {\n type: 'object',\n additionalProperties: false,\n properties: {\n courseId: { type: 'string', required: true },\n title: { type: 'string', required: true },\n source: { type: 'string', required: true, enum: ['markdown', 'folder', 'github'] },\n total: { type: 'integer', required: true },\n mastered: { type: 'integer', required: true },\n avgMasteryPct: { ...nullableInteger, required: true },\n dueCount: { type: 'integer', required: true },\n currentLessonId: { ...nullableString, required: true },\n },\n },\n },\n },\n },\n render: (_args, value) => [{\n type: 'text',\n text: value.courses.length === 0\n ? 'No courses imported yet. Import one with study_import_markdown, study_import_folder, or study_import_github.'\n : value.courses.map(c =>\n `“${c.title}” (${c.source}) — ${c.mastered}/${c.total} lessons mastered`\n + `${c.avgMasteryPct === null ? '' : `, avg mastery ${c.avgMasteryPct}%`}`\n + `${c.dueCount === 0 ? '' : `, ${c.dueCount} reviews due`}`\n + `${c.currentLessonId === null ? '' : `, current lesson ${c.currentLessonId}`}`,\n ).join('\\n'),\n }],\n },\n async execute() {\n const summaries = courseSummaries(store.get(), new Date())\n return {\n total: summaries.length,\n courses: summaries.map(s => ({\n courseId: s.courseId,\n title: s.title,\n source: s.source,\n total: s.total,\n mastered: s.mastered,\n avgMasteryPct: s.avgMasteryPct,\n dueCount: s.dueCount,\n currentLessonId: s.currentLessonId,\n })),\n }\n },\n isConcurrencySafe: () => true,\n presentCall: () => ({ card: 'generic', title: 'List courses', kind: 'read' }),\n })\n\n const courseMap = defineTool({\n name: 'study_map',\n description:\n 'Show one course\\'s skill tree: sections, lessons with locked/available/in_progress/mastered status, mastery, '\n + 'weak-concept count (⚡), and friction count — the weak spots to target.',\n parameters: {\n courseId: { type: 'string', required: true, description: 'Course id from an import result or study_courses.' },\n },\n output: {\n schema: {\n type: 'object',\n additionalProperties: false,\n properties: {\n courseId: { type: 'string', required: true },\n title: { type: 'string', required: true },\n counts: {\n type: 'object',\n required: true,\n additionalProperties: false,\n properties: {\n total: { type: 'integer', required: true },\n mastered: { type: 'integer', required: true },\n available: { type: 'integer', required: true },\n },\n },\n tree: {\n type: 'array',\n required: true,\n items: {\n type: 'object',\n additionalProperties: false,\n properties: {\n title: { type: 'string', required: true },\n lessons: {\n type: 'array',\n required: true,\n items: {\n type: 'object',\n additionalProperties: false,\n properties: {\n id: { type: 'string', required: true },\n title: { type: 'string', required: true },\n kind: { type: 'string', required: true, enum: [...LESSON_KINDS] },\n status: { type: 'string', required: true, enum: [...LESSON_STATUSES] },\n masteryPct: { ...nullableInteger, required: true },\n crown: { type: 'integer', required: true },\n weakConcepts: { type: 'integer', required: true },\n frictionCount: { type: 'integer', required: true },\n },\n },\n },\n },\n },\n },\n },\n },\n render: (_args, value) => textBlocks(cards.mapLines(value)),\n },\n async execute(args) {\n return toMapValue(findCourse(store.get(), args.courseId))\n },\n isConcurrencySafe: () => true,\n presentCall: args => ({ card: 'generic', title: `Course map: ${args.courseId}`, kind: 'read' }),\n presentationMeta: (_args, value) => cards.mapLines(value),\n presentResult: (_args, result) => ({ card: 'generic', content: textBlocks(result.meta as string[]) }),\n })\n\n const lessonContent = defineTool({\n name: 'study_lesson',\n description:\n 'Open one lesson and make it the focus: returns its markdown content (the source of truth to teach '\n + 'from), teaching strategy band, knowledge concepts with mastery/weak flags, four consolidation '\n + 'starters, memory slots, and any pending mastery proposal.',\n parameters: {\n lessonId: { type: 'string', required: true, description: 'Lesson id from a map, import, or courses call.' },\n },\n output: {\n schema: {\n type: 'object',\n additionalProperties: false,\n properties: {\n lessonId: { type: 'string', required: true },\n courseId: { type: 'string', required: true },\n courseTitle: { type: 'string', required: true },\n sectionTitle: { type: 'string', required: true },\n title: { type: 'string', required: true },\n kind: { type: 'string', required: true, enum: [...LESSON_KINDS] },\n status: { type: 'string', required: true, enum: [...LESSON_STATUSES] },\n body: { type: 'string', required: true },\n masteryPct: { ...nullableInteger, required: true },\n crown: { type: 'integer', required: true },\n attempts: { type: 'integer', required: true },\n correctCount: { type: 'integer', required: true },\n strategy: { type: 'string', required: true },\n concepts: { oneOf: [{ type: 'null' }, {\n type: 'array',\n items: {\n type: 'object',\n additionalProperties: false,\n properties: {\n title: { type: 'string', required: true },\n masteryPct: { type: 'integer', required: true },\n weak: { type: 'boolean', required: true },\n /** 1 once this concept has been quizzed at least once, else 0 (ConceptView.tested). */\n tested: { type: 'integer', required: true },\n },\n },\n }], required: true },\n starters: {\n type: 'array',\n required: true,\n items: {\n type: 'object',\n additionalProperties: false,\n properties: {\n label: { type: 'string', required: true },\n message: { type: 'string', required: true },\n effect: { type: 'string', required: true, enum: ['mastery', 'friction', 'none'] },\n },\n },\n },\n memory: {\n type: 'object',\n required: true,\n additionalProperties: false,\n properties: {\n lesson: { ...nullableString, required: true },\n global: { ...nullableString, required: true },\n pattern: { ...nullableString, required: true },\n },\n },\n noteCount: { type: 'integer', required: true },\n pendingProposal: { oneOf: [{ type: 'null' }, {\n type: 'object',\n additionalProperties: false,\n properties: { id: { type: 'string', required: true }, rationale: { type: 'string', required: true } },\n }], required: true },\n nextLessonId: { ...nullableString, required: true },\n },\n },\n render: (_args, value) => [{\n type: 'text',\n text: `Lesson “${value.title}” — ${value.courseTitle} / ${value.sectionTitle}\\n`\n + `status ${value.status}${value.masteryPct === null ? '' : `, mastery ${value.masteryPct}%`}, `\n + `${value.correctCount}/${value.attempts} answers correct\\n`\n + `strategy: ${value.strategy}\\n`\n + (value.concepts === null ? '' : `concepts: ${value.concepts.map(c => `${c.title} ${c.masteryPct}%${c.weak ? ' ⚡weak' : ''}`).join(' · ')}\\n`)\n + `starters: ${value.starters.map(s => s.label).join(' / ')}\\n\\n${value.body}`\n + `${value.nextLessonId === null ? '\\n\\n(this is the last lesson)' : `\\n\\n(next lesson: ${value.nextLessonId})`}`,\n }],\n },\n async execute(args) {\n return mutate((state) => {\n // Opening IS attempting (LookatStudy markNodeAttempted): first open\n // marks in_progress, seeds mastery 0.5, and runs the dual-track unlock.\n const { ref } = attemptLesson(state, args.lessonId, new Date())\n state.focus = { lessonId: ref.lesson.id }\n return toLessonValue(ref, state)\n })\n },\n presentCall: args => ({ card: 'generic', title: `Open lesson: ${args.lessonId}`, kind: 'read' }),\n })\n\n const recordAnswerTool = defineTool({\n name: 'study_record_answer',\n description:\n 'Record one graded answer and update mastery — call after EVERY learner answer to a scored question. '\n + 'Name the `concept` the question tested (from study_lesson / study_define_concepts) so per-concept '\n + 'mastery stays accurate; lesson mastery is the WEAKEST concept. Mastery ≥50% unlocks the next lesson '\n + 'early; ≥90% graduates automatically and schedules the first review. Also pass the question text and '\n + 'the learner\\'s answer to keep a practice log.',\n parameters: {\n lessonId: { type: 'string', required: true, description: 'Lesson the question tested.' },\n correct: { type: 'boolean', required: true, description: 'Whether the learner answered correctly.' },\n concept: { type: 'string', description: 'Concept title the question tested (required once concepts are defined).' },\n rationale: { type: 'string', description: 'One line: why you graded it this way.' },\n question: { type: 'string', description: 'The question text, for the practice log.' },\n givenAnswer: { type: 'string', description: 'The learner\\'s answer, for the practice log.' },\n },\n output: {\n schema: {\n type: 'object',\n additionalProperties: false,\n properties: {\n lessonId: { type: 'string', required: true },\n lessonTitle: { type: 'string', required: true },\n correct: { type: 'boolean', required: true },\n concept: { oneOf: [{ type: 'null' }, {\n type: 'object',\n additionalProperties: false,\n properties: { title: { type: 'string', required: true }, masteryPct: { type: 'integer', required: true }, weak: { type: 'boolean', required: true } },\n }], required: true },\n prevMasteryPct: { type: 'integer', required: true },\n newMasteryPct: { type: 'integer', required: true },\n crown: { type: 'integer', required: true },\n mastered: { type: 'boolean', required: true },\n attempts: { type: 'integer', required: true },\n correctCount: { type: 'integer', required: true },\n graduated: { type: 'boolean', required: true },\n unlockedLessonIds: { type: 'array', required: true, items: { type: 'string' } },\n reviewDueAt: { ...nullableString, required: true },\n },\n },\n render: (_args, value) => [{\n type: 'text',\n text: cards.answerLine(value)\n + (value.concept === null ? '' : `\\nconcept: ${value.concept.title} ${value.concept.masteryPct}%${value.concept.weak ? ' ⚡weak' : ''}`)\n + (value.graduated ? '\\n🎓 mastery ≥90% — lesson graduated, first review scheduled.' : '')\n + (value.unlockedLessonIds.length === 0 ? '' : `\\n🔓 unlocked: ${value.unlockedLessonIds.join(', ')}`),\n }],\n },\n async execute(args) {\n return mutate((state) => {\n const r = recordAnswer(state, args.lessonId, args.correct, args.concept, new Date())\n if (args.question !== undefined) {\n addNote(\n state,\n args.lessonId,\n 'practice',\n args.question.slice(0, 80),\n `${args.question}\\n\\nlearner answered: ${args.givenAnswer ?? '(not recorded)'} — ${args.correct ? '✓ correct' : '✗ incorrect'}${args.rationale === undefined ? '' : `\\nrationale: ${args.rationale}`}`,\n 'ai',\n null,\n new Date(),\n )\n }\n return {\n lessonId: r.ref.lesson.id,\n lessonTitle: r.ref.lesson.title,\n correct: args.correct,\n concept: r.concept === null ? null : {\n title: r.concept.title,\n masteryPct: Math.round(r.concept.mastery * 100),\n weak: r.concept.mastery < 0.7,\n },\n prevMasteryPct: Math.round(r.prevMastery * 100),\n newMasteryPct: Math.round(r.newMastery * 100),\n crown: r.crown,\n mastered: r.mastered,\n attempts: r.ref.lesson.attempts,\n correctCount: r.ref.lesson.correctCount,\n graduated: r.progression.graduated,\n unlockedLessonIds: r.progression.unlocked.map(u => u.id),\n reviewDueAt: r.progression.nextDue,\n }\n })\n },\n presentCall: args => ({\n card: 'generic',\n title: `Record answer (${args.correct ? 'correct' : 'incorrect'}): ${args.lessonId}`,\n }),\n presentationMeta: (_args, value) => [cards.answerLine(value)],\n presentResult: (_args, result) => ({ card: 'generic', content: textBlocks(result.meta as string[]) }),\n })\n\n const completeLessonTool = defineTool({\n name: 'study_complete_lesson',\n description:\n 'Mark a lesson mastered manually (graduation at 90% mastery is the automatic path — this is the '\n + 'override). Unlocks the next lesson and schedules the first spaced review for tomorrow. Call only '\n + 'when the learner has genuinely worked through the lesson.',\n parameters: {\n lessonId: { type: 'string', required: true, description: 'Lesson to complete.' },\n },\n output: {\n schema: {\n type: 'object',\n additionalProperties: false,\n properties: {\n lessonId: { type: 'string', required: true },\n lessonTitle: { type: 'string', required: true },\n unlockedLessonIds: { type: 'array', required: true, items: { type: 'string' } },\n unlockedLessonTitles: { type: 'array', required: true, items: { type: 'string' } },\n reviewDueAt: { type: 'string', required: true },\n courseComplete: { type: 'boolean', required: true },\n },\n },\n render: (_args, value) => [{\n type: 'text',\n text: cards.completeLines(value).join('\\n'),\n }],\n },\n async execute(args) {\n return mutate((state) => {\n const r = completeLesson(state, args.lessonId, new Date())\n return {\n lessonId: r.ref.lesson.id,\n lessonTitle: r.ref.lesson.title,\n unlockedLessonIds: r.unlocked.map(u => u.id),\n unlockedLessonTitles: r.unlocked.map(u => u.title),\n reviewDueAt: r.dueAt,\n courseComplete: r.courseComplete,\n }\n })\n },\n presentCall: args => ({ card: 'generic', title: `Complete lesson: ${args.lessonId}` }),\n presentationMeta: (_args, value) => cards.completeLines(value),\n presentResult: (_args, result) => ({ card: 'generic', content: textBlocks(result.meta as string[]) }),\n })\n\n const dueReviewsTool = defineTool({\n name: 'study_due_reviews',\n description: 'List mastered lessons whose spaced-repetition review is due (optionally within one course), oldest first. Start every session here.',\n parameters: {\n courseId: { type: 'string', description: 'Restrict to one course; omit to scan all courses.' },\n },\n output: {\n schema: {\n type: 'object',\n additionalProperties: false,\n properties: {\n total: { type: 'integer', required: true },\n due: {\n type: 'array',\n required: true,\n items: {\n type: 'object',\n additionalProperties: false,\n properties: {\n lessonId: { type: 'string', required: true },\n courseTitle: { type: 'string', required: true },\n lessonTitle: { type: 'string', required: true },\n dueAt: { type: 'string', required: true },\n overdueDays: { type: 'integer', required: true },\n },\n },\n },\n },\n },\n render: (_args, value) => textBlocks(cards.dueLines(value)),\n },\n async execute(args) {\n const due = dueReviews(store.get(), args.courseId, new Date())\n return {\n total: due.length,\n due: due.map(d => ({\n lessonId: d.lessonId,\n courseTitle: d.courseTitle,\n lessonTitle: d.lessonTitle,\n dueAt: d.dueAt,\n overdueDays: d.overdueDays,\n })),\n }\n },\n isConcurrencySafe: () => true,\n presentCall: () => ({ card: 'generic', title: 'List due reviews', kind: 'search' }),\n presentationMeta: (_args, value) => cards.dueLines(value),\n presentResult: (_args, result) => ({ card: 'generic', content: textBlocks(result.meta as string[]) }),\n })\n\n const recordReviewTool = defineTool({\n name: 'study_record_review',\n description:\n 'Record an SM-2 review grade for a mastered lesson and advance its schedule. Grade how well the '\n + 'learner recalled the material: 5 perfect, 4 hesitant, 3 recalled with effort, 2 incorrect but '\n + 'recognized, 1 incorrect, 0 complete blackout. Target weak concepts (⚡) first.',\n parameters: {\n lessonId: { type: 'string', required: true, description: 'Lesson being reviewed.' },\n quality: { type: 'integer', required: true, enum: [...QUALITIES], description: 'SM-2 recall quality, 0 (blackout) to 5 (perfect).' },\n },\n output: {\n schema: {\n type: 'object',\n additionalProperties: false,\n properties: {\n lessonId: { type: 'string', required: true },\n lessonTitle: { type: 'string', required: true },\n quality: { type: 'integer', required: true },\n intervalDays: { type: 'integer', required: true },\n repetitions: { type: 'integer', required: true },\n easeFactor: { type: 'number', required: true },\n dueAt: { type: 'string', required: true },\n },\n },\n render: (_args, value) => [{ type: 'text', text: cards.reviewLine(value) }],\n },\n async execute(args) {\n return mutate((state) => {\n const r = recordReview(state, args.lessonId, args.quality as ReviewQuality, new Date())\n return {\n lessonId: r.ref.lesson.id,\n lessonTitle: r.ref.lesson.title,\n quality: args.quality,\n intervalDays: r.intervalDays,\n repetitions: r.repetitions,\n easeFactor: r.easeFactor,\n dueAt: r.dueAt,\n }\n })\n },\n presentCall: args => ({ card: 'generic', title: `Record review (quality ${args.quality}): ${args.lessonId}` }),\n presentationMeta: (_args, value) => [cards.reviewLine(value)],\n presentResult: (_args, result) => ({ card: 'generic', content: textBlocks(result.meta as string[]) }),\n })\n\n const deleteCourseTool = defineTool({\n name: 'study_delete_course',\n description: 'Delete one course and all its progress. Ask the learner before calling.',\n parameters: {\n courseId: { type: 'string', required: true, description: 'Course to delete.' },\n },\n output: {\n schema: {\n type: 'object',\n additionalProperties: false,\n properties: {\n deletedCourseId: { type: 'string', required: true },\n remaining: { type: 'integer', required: true },\n },\n },\n render: (_args, value) => [{\n type: 'text',\n text: `Deleted course ${value.deletedCourseId}. ${value.remaining} courses remain.`,\n }],\n },\n async execute(args) {\n return mutate((state) => {\n findCourse(state, args.courseId)\n deleteCourse(state, args.courseId)\n return { deletedCourseId: args.courseId, remaining: state.courses.length }\n })\n },\n presentCall: args => ({ card: 'generic', title: `Delete course: ${args.courseId}`, kind: 'delete', rawInput: args.courseId }),\n })\n\n const defineConceptsTool = defineTool({\n name: 'study_define_concepts',\n description:\n 'Define a lesson\\'s knowledge components — the 2–7 independently quizzable units mastery tracks. '\n + 'Call this the FIRST time you teach a lesson, derived from its content. Titles ≤10 characters; '\n + 'descriptions say what understanding this concept means. Lesson mastery is the WEAKEST concept; '\n + 'cover weak ones (⚡) first when quizzing.',\n parameters: {\n lessonId: { type: 'string', required: true, description: 'Lesson to describe.' },\n concepts: {\n type: 'array',\n required: true,\n description: '2–7 concepts.',\n items: {\n type: 'object',\n additionalProperties: false,\n properties: {\n title: { type: 'string', required: true, description: 'Short concept title (≤10 chars).' },\n description: { type: 'string', required: true, description: 'What understanding this concept means.' },\n },\n },\n },\n },\n output: {\n schema: {\n type: 'object',\n additionalProperties: false,\n properties: {\n lessonId: { type: 'string', required: true },\n concepts: {\n type: 'array',\n required: true,\n items: {\n type: 'object',\n additionalProperties: false,\n properties: { title: { type: 'string', required: true }, masteryPct: { type: 'integer', required: true } },\n },\n },\n },\n },\n render: (_args, value) => [{\n type: 'text',\n text: `Concepts defined: ${value.concepts.map(c => `${c.title} (${c.masteryPct}%)`).join(' · ')}. Attribute quiz answers with the \\`concept\\` parameter.`,\n }],\n },\n async execute(args) {\n return mutate((state) => {\n defineConceptsState(state, args.lessonId, args.concepts)\n const ref = findLesson(state, args.lessonId)\n return {\n lessonId: ref.lesson.id,\n concepts: (conceptViews(ref.lesson) ?? []).map(c => ({ title: c.title, masteryPct: c.masteryPct })),\n }\n })\n },\n presentCall: args => ({ card: 'generic', title: `Define concepts: ${args.lessonId}` }),\n })\n\n const proposeMasteryTool = defineTool({\n name: 'study_propose_mastery',\n description:\n 'Propose graduating a lesson as mastered ahead of the 90% threshold — use when mastery is ≥85% and '\n + 'the learner has convincingly demonstrated understanding (e.g. a Feynman-style explanation back to '\n + 'you). Creates a PENDING proposal: present it with your rationale and WAIT for the learner\\'s '\n + 'decision, then resolve with study_resolve_proposal. Never apply it yourself.',\n parameters: {\n lessonId: { type: 'string', required: true, description: 'Lesson judged mastered.' },\n rationale: { type: 'string', required: true, description: 'Why you believe it is mastered — the learner reads this.' },\n },\n output: {\n schema: {\n type: 'object',\n additionalProperties: false,\n properties: {\n proposalId: { type: 'string', required: true },\n lessonTitle: { type: 'string', required: true },\n status: { type: 'string', required: true, enum: ['pending', 'applied', 'rejected'] },\n rationale: { type: 'string', required: true },\n },\n },\n render: (_args, value) => [{\n type: 'text',\n text: `Proposal ${value.proposalId} (${value.status}): “${value.lessonTitle}” — ${value.rationale}\\nPresent this to the learner and wait; resolve via study_resolve_proposal.`,\n }],\n },\n async execute(args) {\n return mutate((state) => {\n const ref = findLesson(state, args.lessonId)\n const proposal = proposeMastery(state, args.lessonId, args.rationale, new Date())\n return { proposalId: proposal.id, lessonTitle: ref.lesson.title, status: proposal.status, rationale: proposal.rationale }\n })\n },\n presentCall: args => ({ card: 'generic', title: `Propose mastery: ${args.lessonId}` }),\n presentationMeta: (_args, value) => ({\n kind: 'study-proposal-created',\n proposalId: value.proposalId,\n lessonTitle: value.lessonTitle,\n rationale: value.rationale,\n }),\n presentResult: (_args, result) => ({\n card: 'generic',\n content: textBlocks([`🎓 Proposed mastery for “${(result.meta as { lessonTitle?: string } | undefined)?.lessonTitle ?? 'lesson'}”: ${(result.meta as { rationale?: string } | undefined)?.rationale ?? ''}`]),\n }),\n })\n\n const resolveProposalTool = defineTool({\n name: 'study_resolve_proposal',\n description:\n 'Resolve a pending mastery proposal with the learner\\'s explicit decision (they said yes / no in chat). '\n + 'Accepting floors every concept to 95%, graduates the lesson, and unlocks the next one.',\n parameters: {\n proposalId: { type: 'string', required: true, description: 'Proposal id from study_propose_mastery.' },\n accept: { type: 'boolean', required: true, description: 'The learner\\'s decision.' },\n },\n output: {\n schema: {\n type: 'object',\n additionalProperties: false,\n properties: {\n proposalId: { type: 'string', required: true },\n lessonId: { type: 'string', required: true },\n status: { type: 'string', required: true, enum: ['applied', 'rejected'] },\n },\n },\n render: (_args, value) => [{\n type: 'text',\n text: value.status === 'applied'\n ? `🎓 Proposal applied — lesson ${value.lessonId} mastered (all concepts ≥95%), next lesson unlocked, review scheduled.`\n : `Proposal rejected — continuing practice on ${value.lessonId}.`,\n }],\n },\n async execute(args) {\n return mutate((state) => {\n const proposal = resolveProposal(state, args.proposalId, args.accept, new Date())\n return { proposalId: proposal.id, lessonId: proposal.lessonId, status: proposal.status }\n })\n },\n presentCall: args => ({ card: 'generic', title: `Resolve proposal: ${args.proposalId}` }),\n presentationMeta: (_args, value) => ({\n kind: 'study-proposal-resolved',\n proposalId: value.proposalId,\n status: value.status,\n }),\n presentResult: (_args, result) => ({\n card: 'generic',\n content: textBlocks([`Proposal ${(result.meta as { proposalId?: string } | undefined)?.proposalId ?? '?'} ${(result.meta as { status?: string } | undefined)?.status ?? ''}.`]),\n }),\n })\n\n const reportFrictionTool = defineTool({\n name: 'study_report_friction',\n description:\n 'SILENTLY log a learning-friction moment — call when the learner seems confused (糊涂), stuck (卡住), '\n + 'or frustrated (受挫), or when they say \"我没太懂\". One short line. Never mention that you logged it; '\n + 'it feeds the weak-spot map and adapts difficulty.',\n parameters: {\n category: { type: 'string', required: true, enum: [...FRICTION_CATEGORIES], description: 'confused | blocked | frustrated.' },\n summary: { type: 'string', description: 'One short line: what specifically is hard.' },\n lessonId: { type: 'string', description: 'Lesson it happened on, when known.' },\n },\n output: {\n schema: {\n type: 'object',\n additionalProperties: false,\n properties: { logged: { type: 'boolean', required: true } },\n },\n render: () => [{ type: 'text', text: 'Noted.' }],\n },\n async execute(args) {\n return mutate((state) => {\n addFriction(state, args.lessonId ?? null, args.category, args.summary ?? null, new Date())\n return { logged: true }\n })\n },\n presentCall: () => ({ card: 'generic', title: 'Log friction' }),\n })\n\n const rememberTool = defineTool({\n name: 'study_remember',\n description:\n 'Write a learner-memory slot — call only when you learn something worth keeping across sessions '\n + '(how they best learn, a recurring pattern, a specific gap). NOT for transient chat. To merge: '\n + 'read the current slot first (study_lesson\\'s memory field), then send the merged 1–3 sentence '\n + 'version — this REPLACES the slot.',\n parameters: {\n category: { type: 'string', required: true, enum: [...MEMORY_CATEGORIES], description: 'global (cross-course style) | pattern (per-course recurring pattern) | lesson (this lesson\\'s specific gap).' },\n content: { type: 'string', required: true, description: 'The merged 1–3 sentence slot content.' },\n lessonId: { type: 'string', description: 'Lesson (for the lesson slot) or any lesson of the course (for the pattern slot).' },\n },\n output: {\n schema: {\n type: 'object',\n additionalProperties: false,\n properties: {\n previous: { ...nullableString, required: true },\n stored: { type: 'string', required: true },\n },\n },\n render: () => [{ type: 'text', text: 'Remembered.' }],\n },\n async execute(args) {\n return mutate(state => ({\n previous: setMemory(state, args.category, args.content, args.lessonId),\n stored: args.content,\n }))\n },\n presentCall: () => ({ card: 'generic', title: 'Update learner memory' }),\n })\n\n const noteSaveTool = defineTool({\n name: 'study_note_save',\n description:\n 'Save an entry to the learner\\'s Cornell notebook. Zones: `understand` (knowledge structures you '\n + 'generated — concept maps as mermaid, compare tables, diagrams; sediment your best structures here '\n + 'after showing them), `record` (the learner\\'s own words — when they ask to take a note, or when '\n + 'they write something worth keeping, with the verbatim `quote`), `practice` (quiz log — normally '\n + 'written automatically by study_record_answer).',\n parameters: {\n lessonId: { type: 'string', required: true, description: 'Lesson the note belongs to.' },\n zone: { type: 'string', required: true, enum: [...NOTE_ZONES], description: 'understand | record | practice.' },\n title: { type: 'string', required: true, description: 'Short entry title.' },\n text: { type: 'string', required: true, description: 'Entry body — markdown for the understand zone.' },\n source: { type: 'string', required: true, enum: [...NOTE_SOURCES], description: 'ai (you generated) | content (quoted from lesson) | chat (quoted from conversation).' },\n quote: { type: 'string', description: 'Verbatim source quote, for record-zone notes.' },\n },\n output: {\n schema: {\n type: 'object',\n additionalProperties: false,\n properties: { noteId: { type: 'string', required: true }, zone: { type: 'string', required: true } },\n },\n render: (_args, value) => [{ type: 'text', text: `Saved ${value.zone}-zone note ${value.noteId}.` }],\n },\n async execute(args) {\n return mutate((state) => {\n const note = addNote(state, args.lessonId, args.zone, args.title, args.text, args.source, args.quote ?? null, new Date())\n return { noteId: note.id, zone: note.zone }\n })\n },\n presentCall: args => ({ card: 'generic', title: `Save ${args.zone} note: ${args.title}` }),\n })\n\n const notesTool = defineTool({\n name: 'study_notes',\n description: 'Read the learner\\'s Cornell notebook: three zones per lesson (understand structures, learner records, practice log).',\n parameters: {\n lessonId: { type: 'string', description: 'One lesson\\'s notes; omit for all lessons (most recent last).' },\n },\n output: {\n schema: {\n type: 'object',\n additionalProperties: false,\n properties: {\n total: { type: 'integer', required: true },\n notes: {\n type: 'array',\n required: true,\n items: {\n type: 'object',\n additionalProperties: false,\n properties: {\n id: { type: 'string', required: true },\n lessonTitle: { type: 'string', required: true },\n zone: { type: 'string', required: true, enum: [...NOTE_ZONES] },\n title: { type: 'string', required: true },\n text: { type: 'string', required: true },\n source: { type: 'string', required: true, enum: [...NOTE_SOURCES] },\n quote: { ...nullableString, required: true },\n },\n },\n },\n },\n },\n render: (_args, value) => [{\n type: 'text',\n text: value.total === 0\n ? 'Notebook is empty.'\n : value.notes.map(n => `[${n.zone}] ${n.lessonTitle} — ${n.title}${n.quote === null ? '' : ` (quote: “${n.quote.slice(0, 60)}”)`}`).join('\\n'),\n }],\n },\n async execute(args) {\n const state = store.get()\n const lessons = args.lessonId === undefined\n ? state.courses.flatMap(c => c.sections.flatMap(s => s.lessons))\n : [findLesson(state, args.lessonId).lesson]\n const notes = lessons.flatMap(l => l.notes.map(n => ({\n id: n.id,\n lessonTitle: l.title,\n zone: n.zone,\n title: n.title,\n text: n.text,\n source: n.source,\n quote: n.quote,\n })))\n return { total: notes.length, notes }\n },\n isConcurrencySafe: () => true,\n presentCall: () => ({ card: 'generic', title: 'Read notebook', kind: 'read' }),\n })\n\n const setModeTool = defineTool({\n name: 'study_set_mode',\n description:\n 'Switch the tutoring soul when the learner asks for a different style: `direct` 精讲 (explain first, '\n + 'then verify), `guide` 引导 (questions first, hand over steps), `practice` 实战 (learn inside real, '\n + 'messy problems). Takes effect from the next reply.',\n parameters: {\n mode: { type: 'string', required: true, enum: [...MODES], description: 'direct | guide | practice.' },\n },\n output: {\n schema: {\n type: 'object',\n additionalProperties: false,\n properties: { mode: { type: 'string', required: true, enum: [...MODES] } },\n },\n render: (_args, value) => [{ type: 'text', text: `Tutoring soul switched to ${value.mode} (effective next reply).` }],\n },\n async execute(args) {\n return mutate((state) => {\n state.mode = args.mode\n return { mode: state.mode }\n })\n },\n presentCall: args => ({ card: 'generic', title: `Switch soul: ${args.mode}` }),\n })\n\n return [\n importMarkdown,\n importFolder,\n importGithub,\n listCourses,\n courseMap,\n lessonContent,\n recordAnswerTool,\n completeLessonTool,\n dueReviewsTool,\n recordReviewTool,\n deleteCourseTool,\n defineConceptsTool,\n proposeMasteryTool,\n resolveProposalTool,\n reportFrictionTool,\n rememberTool,\n noteSaveTool,\n notesTool,\n setModeTool,\n ]\n}\n","/**\n * dsh-plugin-lookatstudy — turn any markdown, local folder, or GitHub learning\n * repo into a guided course inside DeepSeek Harness. Registers the `study_*`\n * tool surface (ported from LookatStudy's agent contract), a stable tutor\n * persona plus a switchable soul section, and a dynamic learner-snapshot\n * context. Learning state persists in one JSON file shared across sessions.\n * @module dsh-plugin-lookatstudy\n */\n\nimport { existsSync, mkdirSync } from 'node:fs'\nimport { dirname, join } from 'node:path'\nimport type { Context } from '@deepseek-ai/cordis'\nimport { Config } from './config.ts'\nimport { registerDashboard } from './dashboard.ts'\nimport { learnerSnapshot, loadState, resolveStatePath, saveState } from './state.ts'\nimport type { StudyMode } from './state.ts'\nimport { studyTools } from './tools.ts'\n\nexport const name = 'lookatstudy-plugin'\nexport const inject = ['tools', 'systemPrompt']\n\n/**\n * Stable tutor core (ported from LookatStudy's BASE_AGENT_PROMPT plus its\n * tool contract). Deliberately static so the prefix hits the provider's\n * prompt cache; volatile facts live in the learner-snapshot context below.\n */\nconst TUTOR_CORE = `## Study tutor (lookatstudy-plugin)\n\nYou are the learner's AI study tutor for a course imported via the study tools. Your job is genuine understanding, not reciting the material. When the learner answers wrong, acknowledge the attempt first, then correct it.\n\n### Grounding (hard rule)\nTeach strictly from the current lesson's content (study_lesson's body is the source of truth). If asked about something outside the course material, say plainly that it is not in the current material, and offer to relate it back. Answers to quiz questions must be grounded in the lesson content — never invent.\n\n### Vague confusion\nWhen the learner says \"我不懂 / 不太理解\" without specifics, ask which concept is unclear, or list the lesson's 2–3 core concepts and let them pick. Log it silently with study_report_friction.\n\n### Interaction form\n- ONE question or interactive block per reply — never a wall of quiz questions.\n- Structure answers with markdown (headings, lists, GFM tables); for structures prefer visuals: concept maps and flow diagrams as mermaid code blocks, comparisons as GFM tables, code walkthroughs as fenced code with line-referenced annotations.\n- When the learner quotes text in「」, treat it as quote-to-explain: explain that specific passage in the lesson's context.\n- Opening a brand-new lesson: start with a short hook and one fun two-option guess (curiosity-driven, NOT scored, revealed next turn) — no opening lecture, no scored question.\n- After opening a lesson, offer its four starters (from study_lesson) as suggestions.\n- Celebrate graduations and crowns briefly — earned joy, no confetti spam.\n\n### The tutoring loop\n1. Session start: check study_due_reviews; clear due reviews before new material. Open the focus lesson with study_lesson. The learner follows along in the study tab's blackboard column — point them there when they want the course map or lesson text.\n2. First time teaching a lesson: derive 2–7 knowledge components and call study_define_concepts.\n3. Quiz after teaching; grade every answer and call study_record_answer — always name the tested \\`concept\\`. Lesson mastery is the WEAKEST concept, so target ⚡weak ones first.\n4. Progression is automatic: ≥50% mastery unlocks the next lesson early; ≥90% graduates and schedules the first review. study_complete_lesson is only the manual override.\n5. Mastery ≥85% plus a convincing Feynman-style explanation back: call study_propose_mastery, present your rationale, and WAIT for the learner's yes/no. Resolve only with their explicit answer via study_resolve_proposal. You never graduate a lesson on your own judgment alone.\n6. Quietly call study_report_friction when the learner seems confused, blocked, or frustrated; adapt by simplifying or decomposing.\n7. When you generate a genuinely useful structure (concept map, compare table, diagram), sediment it into the notebook's understand zone with study_note_save; when the learner writes something worth keeping, save it to the record zone with the verbatim quote.\n8. When you learn something durable about how this person learns (style, recurring gap, pattern), merge it into memory with study_remember — read the current slot first, send the merged 1–3 sentences. No transient chat.\n\n### Quiz quality\n3–4 questions per quiz block is best (5 max), 4 options each. Distractors must come from real misconceptions, not absurd fillers. Test understanding, not recall: \"in scenario Y, use X or Z?\" rather than \"define X\". Every question carries an explanation of why the right answer is right. One scored block at a time.\n\n### Integrity\nNever claim progress you did not record through the tools. Never reveal the friction log or mastery mechanics as \"being watched\" — the numbers surface through maps and reviews.\n`\n\n/** The three builtin souls, verbatim from LookatStudy (direct/guide/practice). */\nconst SOULS: Record<StudyMode, string> = {\n direct:\n `### Soul: direct 精讲\n你是讲解型教练。核心原则:**先讲清楚,再确认懂没懂**。\n1. 学习者问什么,先用一两句把核心讲透——不绕弯子、不反问让他猜。给定义时配一个最小例子。\n2. 讲完一个点,立刻出一个轻量确认题(是非/选择,不计掌握度),答对再往下;答错针对性补一句,不换题海。\n3. 抽象概念优先给完整范例(worked example),再让他在范例上动手改一个数。\n4. 他说\"懂了\"时,让他用自己的话复述一遍(费曼检验)——复述不出就再讲。\n5. 一次只推进一个核心点。讲透一个,不扫过一片。`,\n guide:\n `### Soul: guide 引导\n你是引导型教练。核心原则:**让他自己往前推一步,你只递台阶**。\n1. 学习者问\"X 是什么/为什么\",不直接给答案,先抛一个引导性问题让他用已有知识推。\n2. 推对往深推一层;推偏给更具体的提示(不是答案),让他再试。\n3. 连着两次推不动、或他明说\"直接告诉我\",才给答案——给时附一句\"为什么\",建因果链。\n4. 检测到他连续答对三次,主动提议进入更深的子主题(不让他停舒适区)。\n5. 鼓励他费曼式复述刚推出的结论,验是否真懂。`,\n practice:\n `### Soul: practice 实战\n你是实战型 mentor。核心原则:**在真实世界的乱问题里学,不在干净的练习题里学**。\n1. 每个概念落在一个真实的、边界模糊的问题上——不是\"已知 A 求 B\",而是\"给你一笔预算/一个真实场景/一堆乱数据,你怎么决策\"这类没有标准答案的问题。\n2. 先让他面对问题自己想思路(哪怕错),再把他卡住的地方和刚学的概念连起来——概念是工具,问题是主。\n3. 他卡住时给\"下一步具体动作\"(如\"先把你要的变量列出来\"),不给完整解;做完一步再推进。\n4. 一个问题走完,要求他复盘:哪步用了哪个概念、重来会怎么改。复盘比答对更重要。\n5. 主动串联:把当前问题和已学概念织成网,让他看到知识点在真实任务里怎么协作。`,\n}\n\n/**\n * Render the learner snapshot (LookatStudy's per-turn volatile tail) as the\n * dynamic runtime context: focus, strategy band, concepts with weak flags,\n * recent friction, memory slots, due count, pending proposal.\n */\nfunction snapshotText(store: { get(): ReturnType<typeof loadState> }): string {\n const snap = learnerSnapshot(store.get(), new Date())\n if (snap.focus === null) {\n return snap.dueCount === 0 ? '' : `【学习者当前状态】\\n今日待复习: ${snap.dueCount} 项(study_due_reviews)`\n }\n const lines: string[] = ['【学习者当前状态】']\n lines.push(`焦点: ${snap.focus.courseTitle} / ${snap.focus.lessonTitle}(${snap.focus.status}${snap.focus.masteryPct === null ? '' : `, 掌握度 ${snap.focus.masteryPct}%`})`)\n if (snap.strategy !== null) lines.push(`教学策略: ${snap.strategy}`)\n if (snap.concepts !== null && snap.concepts.length > 0) {\n lines.push(`知识点(课级掌握度 = 最薄弱知识点): ${snap.concepts.map(c => `${c.title} ${c.masteryPct}%${c.weak ? ' ⚡薄弱' : ''}`).join(' · ')}`)\n }\n if (snap.friction.length > 0) {\n lines.push(`近期卡点(共 ${snap.friction.length} 条): ${snap.friction.map(f => `${f.category}${f.summary === null ? '' : `: ${f.summary}`}`).join(' / ')}`)\n }\n const memory = [\n snap.memoryGlobal === null ? '' : `整体: ${snap.memoryGlobal}`,\n snap.memoryLesson === null ? '' : `本课: ${snap.memoryLesson}`,\n snap.memoryPattern === null ? '' : `模式: ${snap.memoryPattern}`,\n ].filter(Boolean)\n if (memory.length > 0) lines.push(`记忆: ${memory.join(' | ')}`)\n if (snap.dueCount > 0) lines.push(`今日待复习: ${snap.dueCount} 项`)\n if (snap.pendingProposal !== null) lines.push(`待决提案 ${snap.pendingProposal.id}: ${snap.pendingProposal.rationale}(等学习者表态)`)\n return lines.join('\\n')\n}\n\n/**\n * Register the study tools, the tutor persona (stable core + soul), and the\n * dynamic learner-snapshot context.\n * @param ctx - plugin context carrying the tool registry and system prompt.\n * @param config - validated plugin configuration.\n */\nexport function apply(ctx: Context, config: Config): void {\n const statePath = resolveStatePath(config.statePath)\n const fresh = !existsSync(statePath)\n const state = loadState(statePath)\n // Config seeds the initial soul; afterwards the persisted choice (switchable\n // via study_set_mode) wins.\n if (fresh) state.mode = config.mode\n const store = {\n get: () => state,\n save: () => saveState(statePath, state),\n }\n for (const tool of studyTools(store)) {\n ctx.tools.register(tool)\n }\n ctx.systemPrompt.section({\n name: 'lookatstudy:tutor-core',\n order: 120,\n text: TUTOR_CORE,\n })\n ctx.systemPrompt.section({\n name: 'lookatstudy:soul',\n order: 121,\n text: () => SOULS[store.get().mode],\n })\n ctx.systemPrompt.context({\n name: 'lookatstudy:learner-snapshot',\n order: 50,\n text: () => snapshotText(store),\n })\n // The study tab's HTTP API and its dedicated workspace directory exist only\n // in compositions carrying a webserver (web profile); headless assemblies\n // keep the plain tool surface.\n ctx.inject(['webServer'], (webCtx) => {\n // The one-click starter's dedicated workspace directory: a sibling of the\n // state file, created eagerly so the client can adopt it as a workspace.\n const studyAreaPath = join(dirname(statePath), 'study-area')\n mkdirSync(studyAreaPath, { recursive: true })\n const disposeDashboard = registerDashboard(webCtx.webServer, { store, studyAreaPath })\n webCtx.effect(() => disposeDashboard, 'lookatstudy.dashboard()')\n })\n}\n\nexport { Config }\n"],"mappings":"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"}