feat(exam): Phase 3 — классификатор tag-exam-textbook.js (100% math9, 800/800)
Детерминированная эвристика: subtopic → кандидатные §, keyword-scoring по тексту. Карта subtopic→primary § по PLAN.md. Флаги: --exam, --dry-run, --report. Результат: 800 задач math9 размечены без единого null (algebra-8-ch2#8 и др.). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,644 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* tag-exam-textbook.js — heuristic per-task textbook-paragraph classifier
|
||||
* for exam_tasks (math9 and future exams).
|
||||
*
|
||||
* For each task:
|
||||
* 1. subtopic → candidate chapters/paragraphs (primary map from PLAN.md)
|
||||
* 2. keyword scoring on stripped text → best § match
|
||||
* 3. fallback: primary § for the subtopic
|
||||
*
|
||||
* Writes exam_tasks.textbook_slug / textbook_paragraph.
|
||||
*
|
||||
* Usage:
|
||||
* node backend/scripts/tag-exam-textbook.js --exam math9 # tag live DB
|
||||
* node backend/scripts/tag-exam-textbook.js --exam math9 --dry-run # no writes
|
||||
* node backend/scripts/tag-exam-textbook.js --exam math9 --report # show distribution
|
||||
* node backend/scripts/tag-exam-textbook.js --exam math9 --dry-run --report
|
||||
*
|
||||
* Idempotent: overwrites existing textbook_slug/paragraph on every run.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const db = require('../src/db/db');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const flags = new Set(args.filter(a => a.startsWith('--')));
|
||||
const vals = args.filter(a => !a.startsWith('--'));
|
||||
|
||||
const DRY = flags.has('--dry-run') || flags.has('--dry');
|
||||
const REPORT = flags.has('--report');
|
||||
const VERBOSE = flags.has('--verbose') || flags.has('-v');
|
||||
|
||||
// --exam math9 or positional first arg
|
||||
let examKey = 'math9';
|
||||
const examIdx = args.indexOf('--exam');
|
||||
if (examIdx !== -1 && args[examIdx + 1]) {
|
||||
examKey = args[examIdx + 1];
|
||||
} else if (vals.length) {
|
||||
examKey = vals[0];
|
||||
}
|
||||
|
||||
/* ── Taxonomy ─────────────────────────────────────────────────── */
|
||||
const taxonomy = require('./data/g9_textbook_sections.json');
|
||||
|
||||
// Build lookup: book -> [ {chapter_slug, para_id, num, title} ]
|
||||
// and flat: chapter_slug+para_id -> para number (integer)
|
||||
const byBook = new Map();
|
||||
const paraNum = new Map(); // key: `${chapter_slug}:${para_id}` -> integer N
|
||||
|
||||
for (const sec of taxonomy) {
|
||||
if (!sec.para_id) continue; // math-5/6 engine-based, skip for slug lookup
|
||||
const n = parseInt((sec.num || '').replace(/[^\d]/g, ''), 10);
|
||||
if (!isNaN(n)) {
|
||||
paraNum.set(`${sec.chapter_slug}:${sec.para_id}`, n);
|
||||
}
|
||||
if (!byBook.has(sec.book)) byBook.set(sec.book, []);
|
||||
byBook.get(sec.book).push(sec);
|
||||
}
|
||||
|
||||
/* Helper: resolve (chapter_slug, para_id) → integer paragraph number.
|
||||
For math-5/6 (engine-based, no para_id in JSON) we use para_id directly
|
||||
parsed as an integer if it's like "p7" → 7. */
|
||||
function paraToInt(chapter_slug, para_id) {
|
||||
const key = `${chapter_slug}:${para_id}`;
|
||||
if (paraNum.has(key)) return paraNum.get(key);
|
||||
// fallback: parse "pN" → N
|
||||
const m = String(para_id || '').match(/^p(\d+)$/);
|
||||
return m ? parseInt(m[1], 10) : null;
|
||||
}
|
||||
|
||||
/* ── Text normalization ───────────────────────────────────────── */
|
||||
function stripText(html) {
|
||||
return String(html || '')
|
||||
.replace(/<svg[\s\S]*?<\/svg>/gi, ' ')
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/\\dfrac\{([^}]+)\}\{([^}]+)\}/g, '($1)/($2)')
|
||||
.replace(/\\frac\{([^}]+)\}\{([^}]+)\}/g, '($1)/($2)')
|
||||
.replace(/\\sqrt\{([^}]+)\}/g, 'sqrt($1)')
|
||||
.replace(/\\cdot/g, '*').replace(/\\leq/g, '<=').replace(/\\geq/g, '>=')
|
||||
.replace(/\\lt/g, '<').replace(/\\gt/g, '>')
|
||||
.replace(/\\[a-zA-Z]+/g, ' ')
|
||||
.replace(/[{}$]/g, ' ')
|
||||
.replace(/&[a-z]+;/gi, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim();
|
||||
}
|
||||
|
||||
/* ── Candidate rules per subtopic ─────────────────────────────── */
|
||||
/*
|
||||
Each entry: { slug, paragraph, keywords: [regexp,...], boost }
|
||||
Scoring: sum of keyword matches (each match = 1 point + boost if set).
|
||||
The entry with the highest score wins; ties: first (most specific first).
|
||||
Fallback: first entry (primary §).
|
||||
*/
|
||||
|
||||
const SUBTOPIC_RULES = {
|
||||
|
||||
/* ── alg-numbers ──────────────────────────────────────────── */
|
||||
'alg-numbers': [
|
||||
// Rational numbers → math-6-ch4
|
||||
{ slug: 'math-6-ch4', paragraph: null,
|
||||
kw: [/рациональн|отрицательн|положительн|противоположн|модул[ья]/i] },
|
||||
// Natural numbers, divisibility → math-5-ch1
|
||||
{ slug: 'math-5-ch1', paragraph: null,
|
||||
kw: [/натуральн|НОД|НОК|делитель|кратное|простое\s+число|составное/i] },
|
||||
// Sets / intervals → algebra-8-ch1 §5
|
||||
{ slug: 'algebra-8-ch1', paragraph: 5,
|
||||
kw: [/промежуток|объединение|пересечение|принадлежит\s+числ/i] },
|
||||
// fallback
|
||||
{ slug: 'math-6-ch4', paragraph: null, kw: [] },
|
||||
],
|
||||
|
||||
/* ── alg-arithmetic ──────────────────────────────────────── */
|
||||
'alg-arithmetic': [
|
||||
// Decimals → math-6-ch1
|
||||
{ slug: 'math-6-ch1', paragraph: null,
|
||||
kw: [/десятичн[ая]\s+дроб|запятой/i] },
|
||||
// Fractions (ordinary) → math-5-ch3
|
||||
{ slug: 'math-5-ch3', paragraph: null,
|
||||
kw: [/обыкновенн[ая]\s+дроб|числитель|знаменатель.*не.*алгебр/i] },
|
||||
// Calculation → math-5-ch2
|
||||
{ slug: 'math-5-ch2', paragraph: null,
|
||||
kw: [/вычисл|выражени[ея]\s+со\s+скобк|порядок\s+действий/i] },
|
||||
// fallback
|
||||
{ slug: 'math-6-ch1', paragraph: null, kw: [] },
|
||||
],
|
||||
|
||||
/* ── alg-powers ──────────────────────────────────────────── */
|
||||
'alg-powers': [
|
||||
// Standard form → algebra-7-ch1 §3
|
||||
{ slug: 'algebra-7-ch1', paragraph: 3,
|
||||
kw: [/стандартн[ый]*\s+вид|A\s*[·x*]\s*10\^?\s*[\-{n]|\d+\s*[·*]\s*10\^/i] },
|
||||
// Integer exponent → algebra-7-ch1 §2
|
||||
{ slug: 'algebra-7-ch1', paragraph: 2,
|
||||
kw: [/целый?\s+показател|отрицательн[ый]*\s+показател|степен[ьи]\s+с\s+(?:целым|отрицательным)/i] },
|
||||
// Square root → algebra-8-ch1 §1
|
||||
{ slug: 'algebra-8-ch1', paragraph: 1,
|
||||
kw: [/квадратн[ый]*\s+корен|арифметическ[ий]*\s+корен|извлеч[её]нь?|sqrt\(/i] },
|
||||
// Sqrt properties → algebra-8-ch1 §3
|
||||
{ slug: 'algebra-8-ch1', paragraph: 3,
|
||||
kw: [/свойств[ао]\s+(?:квадратных\s+)?корн[ей]|упрост[иь].*sqrt|вынеси.*за\s+знак|внес[иь].*под\s+знак/i] },
|
||||
// Sqrt application → algebra-8-ch1 §4
|
||||
{ slug: 'algebra-8-ch1', paragraph: 4,
|
||||
kw: [/приблиз[иь].*корн|применен[ие]\s+свойств\s+корн/i] },
|
||||
// Natural exponent → algebra-7-ch1 §1 (fallback to ch1)
|
||||
{ slug: 'algebra-7-ch1', paragraph: 1,
|
||||
kw: [/натуральн[ый]*\s+показател|степен[ьи]\s+числа\s+[a-z]/i] },
|
||||
// fallback
|
||||
{ slug: 'algebra-7-ch1', paragraph: 1, kw: [] },
|
||||
],
|
||||
|
||||
/* ── alg-expressions ─────────────────────────────────────── */
|
||||
'alg-expressions': [
|
||||
// Rational expression transform → algebra-9-ch1 §5
|
||||
{ slug: 'algebra-9-ch1', paragraph: 5,
|
||||
kw: [/рациональн[ое]*\s+выражени|преобразован[ие]\s+выражени|упростите\s+выражени|тождественно\s+равн/i] },
|
||||
// Variables / substitution → algebra-7-ch2 §4
|
||||
{ slug: 'algebra-7-ch2', paragraph: 4,
|
||||
kw: [/с\s+переменн|числовое\s+значение\s+при|значение\s+выражения\s+при\s+[a-z]/i] },
|
||||
// Identity → algebra-7-ch2 §5
|
||||
{ slug: 'algebra-7-ch2', paragraph: 5,
|
||||
kw: [/тождество|тождественн/i] },
|
||||
// fallback
|
||||
{ slug: 'algebra-9-ch1', paragraph: 5, kw: [] },
|
||||
],
|
||||
|
||||
/* ── alg-polynomials ─────────────────────────────────────── */
|
||||
'alg-polynomials': [
|
||||
// Factoring → algebra-7-ch2 §14
|
||||
{ slug: 'algebra-7-ch2', paragraph: 14,
|
||||
kw: [/разложите\s+на\s+множител|вынесите.*за\s+скобк|разложение.*множител/i] },
|
||||
// Difference of squares → algebra-7-ch2 §13
|
||||
{ slug: 'algebra-7-ch2', paragraph: 13,
|
||||
kw: [/разность\s+квадрат|a\s*[\-−]\s*b.*a\s*\+\s*b/i] },
|
||||
// Square of sum/diff → algebra-7-ch2 §12
|
||||
{ slug: 'algebra-7-ch2', paragraph: 12,
|
||||
kw: [/квадрат\s+сумм|квадрат\s+разност|формул[а-я]*\s+сокращённ/i] },
|
||||
// Multiply polynomials → algebra-7-ch2 §11
|
||||
{ slug: 'algebra-7-ch2', paragraph: 11,
|
||||
kw: [/перемнож[иь]|умножени[ея]\s+многочлен|произведени[ея]\s+многочлен/i] },
|
||||
// Monomial operations → algebra-7-ch2 §7
|
||||
{ slug: 'algebra-7-ch2', paragraph: 7,
|
||||
kw: [/одночлен.*умн|умн.*одночлен|одночлен.*дел|действия\s+с\s+одночлен/i] },
|
||||
// Polynomial standard form → algebra-7-ch2 §8
|
||||
{ slug: 'algebra-7-ch2', paragraph: 8,
|
||||
kw: [/многочлен[.!,\s]|стандартн[ый]*\s+вид.*многочлен|степень\s+многочлен/i] },
|
||||
// Monomial standard form → algebra-7-ch2 §6
|
||||
{ slug: 'algebra-7-ch2', paragraph: 6,
|
||||
kw: [/одночлен[.!,\s]|стандартн[ый]*\s+вид.*одночлен|степень\s+одночлен/i] },
|
||||
// Add/subtract polynomials → algebra-7-ch2 §9
|
||||
{ slug: 'algebra-7-ch2', paragraph: 9,
|
||||
kw: [/сложени[ея]\s+многочлен|вычитани[ея]\s+многочлен|подобные\s+слагаем/i] },
|
||||
// Divide/multiply by monomial → algebra-7-ch2 §10
|
||||
{ slug: 'algebra-7-ch2', paragraph: 10,
|
||||
kw: [/умножени[ея].*одночлен|деление.*одночлен|на\s+одночлен/i] },
|
||||
// quadratic trinomial factoring → algebra-8-ch2 §10
|
||||
{ slug: 'algebra-8-ch2', paragraph: 10,
|
||||
kw: [/квадратн[ый]*\s+трёхчлен|разложени[ея].*трёхчлен/i] },
|
||||
// fallback
|
||||
{ slug: 'algebra-7-ch2', paragraph: 14, kw: [] },
|
||||
],
|
||||
|
||||
/* ── alg-fractions ───────────────────────────────────────── */
|
||||
'alg-fractions': [
|
||||
// Rational fraction transform → algebra-9-ch1 §5
|
||||
{ slug: 'algebra-9-ch1', paragraph: 5,
|
||||
kw: [/преобразован[ие]\s+(?:рациональн[ых]?\s+)?выражени|упростите|тождественно\s+равн/i] },
|
||||
// Multiply/divide rational → algebra-9-ch1 §4
|
||||
{ slug: 'algebra-9-ch1', paragraph: 4,
|
||||
kw: [/умножени[ея]\s+(?:и\s+делени[ея]\s+)?(?:рациональн[ых]?\s+)?дроб|делени[ея]\s+дроб|произведени[ея].*дроб|частное.*дроб/i] },
|
||||
// Add/subtract rational → algebra-9-ch1 §3
|
||||
{ slug: 'algebra-9-ch1', paragraph: 3,
|
||||
kw: [/сложени[ея]\s+(?:и\s+вычитани[ея]\s+)?(?:рациональн[ых]?\s+)?дроб|вычитани[ея]\s+дроб|сумм[а-я]*.*дроб/i] },
|
||||
// Main property of fraction → algebra-9-ch1 §2
|
||||
{ slug: 'algebra-9-ch1', paragraph: 2,
|
||||
kw: [/основное\s+свойство\s+дроб|сократите\s+дробь|сокращени[ея]\s+дроб/i] },
|
||||
// Rational fraction definition → algebra-9-ch1 §1
|
||||
{ slug: 'algebra-9-ch1', paragraph: 1,
|
||||
kw: [/рациональная\s+дробь|область.*допустим|ОДЗ|не\s+имеет\s+смысла/i] },
|
||||
// Ordinary fractions → math-6-ch1 (decimals/ordinary)
|
||||
{ slug: 'math-5-ch3', paragraph: null,
|
||||
kw: [/обыкновенн[ая]\s+дроб|числитель\s+и\s+знаменатель(?!\s+дроб)/i] },
|
||||
// fallback
|
||||
{ slug: 'algebra-9-ch1', paragraph: 3, kw: [] },
|
||||
],
|
||||
|
||||
/* ── alg-equations ───────────────────────────────────────── */
|
||||
'alg-equations': [
|
||||
// Fractional-rational equations → algebra-9-ch3 §10
|
||||
{ slug: 'algebra-9-ch3', paragraph: 10,
|
||||
kw: [/дробно[-\s]?рациональн[ое]*\s+уравнен|дроб.*знаменател.*уравнен|уравнени[ея].*с\s+дроб/i] },
|
||||
// Systems of nonlinear → algebra-9-ch3 §11
|
||||
{ slug: 'algebra-9-ch3', paragraph: 11,
|
||||
kw: [/систем[а-я]*\s+нелинейн|нелинейн[ая]*\s+систем/i] },
|
||||
// Systems of linear (method) → algebra-7-ch4 §24
|
||||
{ slug: 'algebra-7-ch4', paragraph: 24,
|
||||
kw: [/метод\s+(?:подстановки|сложени[ея])|подстановк[и]*\s+(?:в\s+)?систем|способ[а-я]*\s+решени[ея]\s+систем/i] },
|
||||
// Systems of linear (definition) → algebra-7-ch4 §23
|
||||
{ slug: 'algebra-7-ch4', paragraph: 23,
|
||||
kw: [/систем[а-я]*\s+(?:линейных\s+)?уравнен|решит[ье]?\s+систем/i] },
|
||||
// Quadratic (reduced to) → algebra-8-ch2 §12
|
||||
{ slug: 'algebra-8-ch2', paragraph: 12,
|
||||
kw: [/(?:целые|целое|рациональн[ое]*)\s+уравнен[ие]*.*(?:сводящ|квадратн)|сводящ[ееся]*\s+к\s+квадратн/i] },
|
||||
// Quadratic by Vieta → algebra-8-ch2 §9
|
||||
{ slug: 'algebra-8-ch2', paragraph: 9,
|
||||
kw: [/теорема\s+Виета|Виета|по\s+формуле\s+Виета|сумм[а-я]*\s+корн[ей].*произведен|произведен.*корн[ей].*сумм/i] },
|
||||
// Quadratic by discriminant → algebra-8-ch2 §8
|
||||
{ slug: 'algebra-8-ch2', paragraph: 8,
|
||||
kw: [/дискриминант|формул[а-я]*\s+корн[ей]\s+квадратн|D\s*=\s*b\^2\s*-\s*4ac/i] },
|
||||
// Incomplete quadratic → algebra-8-ch2 §7
|
||||
{ slug: 'algebra-8-ch2', paragraph: 7,
|
||||
kw: [/неполн[ое]*\s+квадратн[ое]*\s+уравнен|квадратное\s+уравнение(?!\s+.*(?:Виета|дискриминант))/i] },
|
||||
// Linear equation → algebra-7-ch3 §15
|
||||
{ slug: 'algebra-7-ch3', paragraph: 15,
|
||||
kw: [/линейное\s+уравнение\s+с\s+одной|линейн[ое]*\s+уравнени[ею]/i] },
|
||||
// fallback: quadratic by discriminant (most common in math9)
|
||||
{ slug: 'algebra-8-ch2', paragraph: 8, kw: [] },
|
||||
],
|
||||
|
||||
/* ── alg-inequalities ────────────────────────────────────── */
|
||||
'alg-inequalities': [
|
||||
// Fractional-rational inequalities → algebra-8-ch3 §18
|
||||
{ slug: 'algebra-8-ch3', paragraph: 18,
|
||||
kw: [/дробно[-\s]?рациональн[ое]*\s+неравенств|дроб.*знаменател.*неравенств/i] },
|
||||
// Method of intervals → algebra-9-ch3 §13 (primary) or algebra-8-ch3 §17
|
||||
{ slug: 'algebra-9-ch3', paragraph: 13,
|
||||
kw: [/метод\s+интервалов|знак[а-я]*\s+на\s+(?:каждом|промежутке)\s+(?:каждого\s+)?промежутк|числовая\s+ось.*интервал/i] },
|
||||
{ slug: 'algebra-8-ch3', paragraph: 17,
|
||||
kw: [/квадратное\s+неравенств|квадратн[а-я]+\s+неравенств|ax\^2|a\s*x\^2/i] },
|
||||
// Systems of linear inequalities → algebra-8-ch3 §16 (or algebra-7-ch3 §18)
|
||||
{ slug: 'algebra-8-ch3', paragraph: 16,
|
||||
kw: [/систем[а-я]*\s+неравенств|совокупност[а-я]*\s+неравенств|двойное\s+неравенств/i] },
|
||||
// Linear inequalities → algebra-8-ch3 §15
|
||||
{ slug: 'algebra-8-ch3', paragraph: 15,
|
||||
kw: [/линейн[ое]*\s+неравенств|linear.*неравенств/i] },
|
||||
// Numeric inequalities props → algebra-8-ch3 §13
|
||||
{ slug: 'algebra-8-ch3', paragraph: 13,
|
||||
kw: [/свойств[а-я]*\s+неравенств|числовы[ех]*\s+неравенств/i] },
|
||||
// fallback
|
||||
{ slug: 'algebra-8-ch3', paragraph: 17, kw: [] },
|
||||
],
|
||||
|
||||
/* ── alg-functions ───────────────────────────────────────── */
|
||||
'alg-functions': [
|
||||
// Graph shifts → algebra-9-ch2 §9
|
||||
{ slug: 'algebra-9-ch2', paragraph: 9,
|
||||
kw: [/сдвиг[и]*\s+график|параллельн[ый]*\s+перенос\s+график|y\s*=\s*f\(\s*x\s*(?:[+\-]|−)|y\s*=\s*f\(\s*x\s*\)\s*(?:[+\-]|−)/i] },
|
||||
// Even/odd → algebra-9-ch2 §8
|
||||
{ slug: 'algebra-9-ch2', paragraph: 8,
|
||||
kw: [/чётн[а-я]*\s+функци[и]*|нечётн[а-я]*\s+функци[и]*/i] },
|
||||
// Properties (monotone, zeros) → algebra-9-ch2 §7
|
||||
{ slug: 'algebra-9-ch2', paragraph: 7,
|
||||
kw: [/свойств[а-я]*\s+функци[и]*|возрастан[ие]*|убыван[ие]*|нул[иь]\s+функци[и]*|наибольшее\s+значение\s+функци/i] },
|
||||
// Function definition → algebra-9-ch2 §6
|
||||
{ slug: 'algebra-9-ch2', paragraph: 6,
|
||||
kw: [/область\s+(?:определения|значений)|функция\s+числового\s+аргумент|область\s+допустим/i] },
|
||||
// Linear function → algebra-7-ch3 §20
|
||||
{ slug: 'algebra-7-ch3', paragraph: 20,
|
||||
kw: [/линейная\s+функция|y\s*=\s*kx\s*(?:[+\-]|−)\s*b|прямая.*пропорциональн|k\s*>\s*0.*возрастает/i] },
|
||||
// Function basics → algebra-7-ch3 §19
|
||||
{ slug: 'algebra-7-ch3', paragraph: 19,
|
||||
kw: [/понятие\s+функции|аргумент[а-я]*\s+и\s+значени[ея]|f\(\s*[a-z]\s*\)\s*=|соответств[ие]*.*каждому.*значению/i] },
|
||||
// Parabola → algebra-9-ch2 §7 (properties) or §9 (shifts)
|
||||
{ slug: 'algebra-9-ch2', paragraph: 9,
|
||||
kw: [/параболы?\s+y\s*=.*\(\s*x|вершин[а-я]*\s+параболы?/i] },
|
||||
// fallback
|
||||
{ slug: 'algebra-9-ch2', paragraph: 6, kw: [] },
|
||||
],
|
||||
|
||||
/* ── alg-progressions ────────────────────────────────────── */
|
||||
'alg-progressions': [
|
||||
// Infinite geometric (|q|<1) → algebra-9-ch4 §19
|
||||
{ slug: 'algebra-9-ch4', paragraph: 19,
|
||||
kw: [/бесконечно\s+убывающ|сумм[а-я]*\s+бесконечн|знаменател[ья]*\s*<\s*1|\|q\|\s*<\s*1/i] },
|
||||
// Sum of geometric → algebra-9-ch4 §18
|
||||
{ slug: 'algebra-9-ch4', paragraph: 18,
|
||||
kw: [/сумм[а-я]*\s+(?:первых\s+n\s+членов\s+)?геометрическ|S_n.*геометрическ|геометрическ.*S_n/i] },
|
||||
// Geometric progression → algebra-9-ch4 §17
|
||||
{ slug: 'algebra-9-ch4', paragraph: 17,
|
||||
kw: [/геометрическ[а-я]*\s+прогресс|знаменатель\s+прогресс|знаменател[ья]*\s+геометрическ|q\s*=\s*(?:[b-z]|\d)/i] },
|
||||
// Sum of arithmetic → algebra-9-ch4 §16
|
||||
{ slug: 'algebra-9-ch4', paragraph: 16,
|
||||
kw: [/сумм[а-я]*\s+(?:первых\s+n\s+членов\s+)?арифметическ|S_n.*арифметическ|арифметическ.*S_n/i] },
|
||||
// Arithmetic progression → algebra-9-ch4 §15
|
||||
{ slug: 'algebra-9-ch4', paragraph: 15,
|
||||
kw: [/арифметическ[а-я]*\s+прогресс|разность\s+прогресс|разност[ья]*\s+арифметическ|d\s*=\s*(?:[b-z]|\d)/i] },
|
||||
// Sequence → algebra-9-ch4 §14
|
||||
{ slug: 'algebra-9-ch4', paragraph: 14,
|
||||
kw: [/числовая?\s+последовательност|формул[а-я]*\s+(?:общего\s+)?члена|a_n\s*=|заданной\s+формулой/i] },
|
||||
// fallback: arithmetic (most common)
|
||||
{ slug: 'algebra-9-ch4', paragraph: 15, kw: [] },
|
||||
],
|
||||
|
||||
/* ── alg-word-problems ───────────────────────────────────── */
|
||||
'alg-word-problems': [
|
||||
// Percents/proportions → math-6-ch2
|
||||
{ slug: 'math-6-ch2', paragraph: null,
|
||||
kw: [/процент|%|пропорци[иья]/i] },
|
||||
// System of equations → algebra-7-ch4 §25
|
||||
{ slug: 'algebra-7-ch4', paragraph: 25,
|
||||
kw: [/систем[а-я]*.*задач|задач.*систем|двое\s+рабочих|две\s+бригады|двое\s+работн/i] },
|
||||
// Quadratic equation → algebra-8-ch2 §11
|
||||
{ slug: 'algebra-8-ch2', paragraph: 11,
|
||||
kw: [/квадратн[ое]*\s+уравнени[ею]\s+(?:для\s+)?задач|задач.*квадратн|квадратн.*задач|площадь.*прямоугольника.*уравнен/i] },
|
||||
// Linear equation text problems → algebra-7-ch3 §16
|
||||
{ slug: 'algebra-7-ch3', paragraph: 16,
|
||||
kw: [/линейн[ое]*\s+уравнени[ею]\s+(?:для\s+)?задач|текстов[ые]*\s+задач.*уравнени/i] },
|
||||
// Scale/map → math-6-ch2
|
||||
{ slug: 'math-6-ch2', paragraph: null,
|
||||
kw: [/масштаб|карт[ае]\s+изображ/i] },
|
||||
// fallback: percentages (most common in word problems)
|
||||
{ slug: 'math-6-ch2', paragraph: null, kw: [] },
|
||||
],
|
||||
|
||||
/* ── geom-triangles ──────────────────────────────────────── */
|
||||
'geom-triangles': [
|
||||
// Trig (sin/cos law) in any triangle → geometry-9-ch3
|
||||
{ slug: 'geometry-9-ch3', paragraph: 10,
|
||||
kw: [/теорема\s+синусов|law\s+of\s+sine/i] },
|
||||
{ slug: 'geometry-9-ch3', paragraph: 11,
|
||||
kw: [/теорема\s+косинусов|law\s+of\s+cosine/i] },
|
||||
{ slug: 'geometry-9-ch3', paragraph: 12,
|
||||
kw: [/формула\s+Герона|решени[ея]\s+треугольника/i] },
|
||||
// Right triangle trig → geometry-9-ch1
|
||||
{ slug: 'geometry-9-ch1', paragraph: 2,
|
||||
kw: [/прямоугольн[ый]*\s+треугольник.*(?:sin|cos|tg|ctg|катет|гипотенуз)|(?:sin|cos|tg|ctg).*прямоугольн[ый]*\s+треугольник/i] },
|
||||
{ slug: 'geometry-9-ch1', paragraph: 1,
|
||||
kw: [/\bsin\s+[A-ZА-Я]|\bcos\s+[A-ZА-Я]|\btg\s+[A-ZА-Я]|\bctg\s+[A-ZА-Я]|синус|косинус|тангенс|котангенс/i] },
|
||||
{ slug: 'geometry-9-ch1', paragraph: 5,
|
||||
kw: [/площадь.*синус|S\s*=\s*(?:\()?[0-9.]*?[ab1]/i] },
|
||||
// Similar triangles → geometry-8-ch3
|
||||
{ slug: 'geometry-8-ch3', paragraph: 3,
|
||||
kw: [/подобн[ы]*\s+треугольник|коэффициент\s+подобия|отношение.*сторон.*подобн/i] },
|
||||
{ slug: 'geometry-8-ch3', paragraph: 5,
|
||||
kw: [/первый\s+признак\s+подобия/i] },
|
||||
{ slug: 'geometry-8-ch3', paragraph: 6,
|
||||
kw: [/второй\s+признак\s+подобия/i] },
|
||||
{ slug: 'geometry-8-ch3', paragraph: 7,
|
||||
kw: [/третий\s+признак\s+подобия/i] },
|
||||
// Pythagorean theorem → geometry-8-ch2 §11
|
||||
{ slug: 'geometry-8-ch2', paragraph: 11,
|
||||
kw: [/пифагор|a\^2\s*[+\-]\s*b\^2|гипотенузы?\^2/i] },
|
||||
// Isosceles triangle → geometry-7-ch2 §11–12
|
||||
{ slug: 'geometry-7-ch2', paragraph: 11,
|
||||
kw: [/равнобедренн[ый]*\s+треугольник|основание\s+и\s+боковые\s+стороны/i] },
|
||||
// Triangle congruence → geometry-7-ch2 §9
|
||||
{ slug: 'geometry-7-ch2', paragraph: 9,
|
||||
kw: [/признаки?\s+равенства\s+треугольников|первый\s+признак\s+равенства|второй\s+признак\s+равенства/i] },
|
||||
{ slug: 'geometry-7-ch2', paragraph: 13,
|
||||
kw: [/третий\s+признак\s+равенства\s+треугольников/i] },
|
||||
// Right triangle special → geometry-7-ch4 §23–26
|
||||
{ slug: 'geometry-7-ch4', paragraph: 23,
|
||||
kw: [/прямоугольн[ый]*\s+треугольник(?!.*подобн|.*пифагор|.*sin|.*cos)/i] },
|
||||
{ slug: 'geometry-7-ch4', paragraph: 26,
|
||||
kw: [/катет.*30|угол\s+30|30°/i] },
|
||||
// Median, altitude, bisector → geometry-7-ch2 §10
|
||||
{ slug: 'geometry-7-ch2', paragraph: 10,
|
||||
kw: [/высота\s+треугольника|медиана\s+треугольника|биссектриса\s+треугольника/i] },
|
||||
// Triangle angles sum → geometry-7-ch4 §19–20
|
||||
{ slug: 'geometry-7-ch4', paragraph: 19,
|
||||
kw: [/сумма\s+углов\s+треугольника/i] },
|
||||
{ slug: 'geometry-7-ch4', paragraph: 20,
|
||||
kw: [/внешний\s+угол\s+треугольника/i] },
|
||||
// fallback
|
||||
{ slug: 'geometry-7-ch2', paragraph: 9, kw: [] },
|
||||
],
|
||||
|
||||
/* ── geom-quadrilaterals ─────────────────────────────────── */
|
||||
'geom-quadrilaterals': [
|
||||
// Inscribed/circumscribed quadrilaterals → geometry-9-ch2 §9
|
||||
{ slug: 'geometry-9-ch2', paragraph: 9,
|
||||
kw: [/вписанн[ый]*\s+четырёхугольник|описанн[ый]*\s+четырёхугольник|вписан.*четырёхуголь/i] },
|
||||
// Regular polygon → geometry-9-ch4
|
||||
{ slug: 'geometry-9-ch4', paragraph: 13,
|
||||
kw: [/правильн[ый]*\s+(?:многоугольник|шестиугольник|пятиугольник|восьмиугольник)/i] },
|
||||
// Trapezoid → geometry-8-ch1 §14–16
|
||||
{ slug: 'geometry-8-ch1', paragraph: 14,
|
||||
kw: [/трапеци[ия]|средняя\s+линия\s+трапеци/i] },
|
||||
{ slug: 'geometry-8-ch1', paragraph: 15,
|
||||
kw: [/равнобедренн[ая]*\s+трапеци[ия]\s+(?:свойств|признак)/i] },
|
||||
// Rhombus → geometry-8-ch1 §9
|
||||
{ slug: 'geometry-8-ch1', paragraph: 9,
|
||||
kw: [/ромб[а-я]*(?:\s|,|\.)/i] },
|
||||
// Rectangle → geometry-8-ch1 §7
|
||||
{ slug: 'geometry-8-ch1', paragraph: 7,
|
||||
kw: [/прямоугольник[а-я]*(?:\s+свойств|.*диагональ)/i] },
|
||||
// Square → geometry-8-ch1 §10
|
||||
{ slug: 'geometry-8-ch1', paragraph: 10,
|
||||
kw: [/квадрат[а-я]*\s+(?:свойств|признак|ABCD|со\s+стороной)/i] },
|
||||
// Parallelogram properties → geometry-8-ch1 §5
|
||||
{ slug: 'geometry-8-ch1', paragraph: 5,
|
||||
kw: [/свойства?\s+параллелограмма/i] },
|
||||
// Parallelogram signs → geometry-8-ch1 §6
|
||||
{ slug: 'geometry-8-ch1', paragraph: 6,
|
||||
kw: [/признаки?\s+параллелограмма/i] },
|
||||
// Parallelogram definition → geometry-8-ch1 §4
|
||||
{ slug: 'geometry-8-ch1', paragraph: 4,
|
||||
kw: [/параллелограмм/i] },
|
||||
// Sum of angles polygon → geometry-8-ch1 §2
|
||||
{ slug: 'geometry-8-ch1', paragraph: 2,
|
||||
kw: [/сумма\s+углов.*многоугольника|сумм[а-я]*\s+внутренних\s+углов/i] },
|
||||
// fallback
|
||||
{ slug: 'geometry-8-ch1', paragraph: 4, kw: [] },
|
||||
],
|
||||
|
||||
/* ── geom-circle ─────────────────────────────────────────── */
|
||||
'geom-circle': [
|
||||
// Circumference and area of circle → geometry-9-ch4 §16
|
||||
{ slug: 'geometry-9-ch4', paragraph: 16,
|
||||
kw: [/длина\s+окружности|площадь\s+круга|2\s*\*?\s*pi\s*\*?\s*r|π\s*r\^?2/i] },
|
||||
// Inscribed/circumscribed circles in triangles → geometry-9-ch2 §7–8
|
||||
{ slug: 'geometry-9-ch2', paragraph: 7,
|
||||
kw: [/описанн[ая]*\s+окружность.*треугольн|вписанн[ая]*\s+окружность.*треугольн/i] },
|
||||
{ slug: 'geometry-9-ch2', paragraph: 8,
|
||||
kw: [/окружност[ьи]*\s+прямоугольного\s+треугольника/i] },
|
||||
// Inscribed angle on diameter → geometry-8-ch4 §11
|
||||
{ slug: 'geometry-8-ch4', paragraph: 11,
|
||||
kw: [/вписанн[ый]*\s+угол.*диаметр|угол.*полуокружн|90°.*диаметр|опирается.*диаметр/i] },
|
||||
// Inscribed angle property → geometry-8-ch4 §9
|
||||
{ slug: 'geometry-8-ch4', paragraph: 9,
|
||||
kw: [/свойство\s+вписанного\s+угла|вписанн[ый]*\s+угол\s+равен\s+половине/i] },
|
||||
// Central angle → geometry-8-ch4 §8
|
||||
{ slug: 'geometry-8-ch4', paragraph: 8,
|
||||
kw: [/центральн[ый]*\s+угол|градусная\s+мера\s+дуги/i] },
|
||||
// Tangent from point → geometry-8-ch4 §3
|
||||
{ slug: 'geometry-8-ch4', paragraph: 3,
|
||||
kw: [/касательн[ые]*\s+из\s+(?:одной\s+)?точки|от\s+внешней\s+точки.*касательн/i] },
|
||||
// Tangent recognition → geometry-8-ch4 §1
|
||||
{ slug: 'geometry-8-ch4', paragraph: 1,
|
||||
kw: [/признак\s+касательной|касательная.*перпендикулярна\s+радиусу/i] },
|
||||
// Chord properties → geometry-8-ch4 §15
|
||||
{ slug: 'geometry-8-ch4', paragraph: 15,
|
||||
kw: [/пересекающихся\s+хорд|произведен.*хорд/i] },
|
||||
// Tangent-chord angle → geometry-8-ch4 §12
|
||||
{ slug: 'geometry-8-ch4', paragraph: 12,
|
||||
kw: [/угол\s+между\s+касательной\s+и\s+хордой/i] },
|
||||
// Circle (basic, g7) → geometry-7-ch1 §4
|
||||
{ slug: 'geometry-7-ch1', paragraph: 4,
|
||||
kw: [/окружность\s+и\s+круг|радиус\s+и\s+диаметр(?!\s+.*центральн)/i] },
|
||||
// fallback
|
||||
{ slug: 'geometry-8-ch4', paragraph: 8, kw: [] },
|
||||
],
|
||||
|
||||
/* ── geom-coordinates ────────────────────────────────────── */
|
||||
'geom-coordinates': [
|
||||
// Circle equation → algebra-9-ch3 §12
|
||||
{ slug: 'algebra-9-ch3', paragraph: 12,
|
||||
kw: [/уравнение\s+окружности|\(x\s*[-−]\s*a\)\^2\s*[+\-]\s*\(y\s*[-−]\s*b\)\^2|центр\s+окружности.*уравнен/i] },
|
||||
// Coordinate plane → math-6-ch5
|
||||
{ slug: 'math-6-ch5', paragraph: null,
|
||||
kw: [/координатная\s+плоскость|ось\s+(?:OX|OY|абсцисс|ординат)|XOY/i] },
|
||||
// Vectors / linear equation → algebra-7-ch4 §21–22
|
||||
{ slug: 'algebra-7-ch4', paragraph: 22,
|
||||
kw: [/уравнение\s+прямой|ax\s*\+\s*by\s*=\s*c|линейное\s+уравнение.*двумя\s+переменными.*график/i] },
|
||||
{ slug: 'algebra-7-ch4', paragraph: 21,
|
||||
kw: [/линейное\s+уравнение\s+с\s+двумя\s+переменными/i] },
|
||||
// fallback
|
||||
{ slug: 'algebra-9-ch3', paragraph: 12, kw: [] },
|
||||
],
|
||||
|
||||
/* ── theory-statements ───────────────────────────────────── */
|
||||
'theory-statements': [
|
||||
// Try to categorise by content — same keywords as main subtopics
|
||||
{ slug: 'algebra-9-ch4', paragraph: 15,
|
||||
kw: [/прогресс|последовательност/i] },
|
||||
{ slug: 'algebra-8-ch3', paragraph: 17,
|
||||
kw: [/неравенств/i] },
|
||||
{ slug: 'algebra-8-ch2', paragraph: 8,
|
||||
kw: [/уравнен/i] },
|
||||
{ slug: 'algebra-9-ch2', paragraph: 7,
|
||||
kw: [/функц|график/i] },
|
||||
{ slug: 'geometry-8-ch4', paragraph: 8,
|
||||
kw: [/окружност|дуг|вписанн|касательн|хорд/i] },
|
||||
{ slug: 'geometry-8-ch1', paragraph: 4,
|
||||
kw: [/параллелограмм|трапеци|ромб|прямоугольник/i] },
|
||||
{ slug: 'geometry-7-ch2', paragraph: 9,
|
||||
kw: [/треугольник|подобн|пифагор|синус|косинус/i] },
|
||||
{ slug: 'algebra-9-ch1', paragraph: 5,
|
||||
kw: [/дробь|алгебраическ|многочлен/i] },
|
||||
// If no match → null (leave for subtopic fallback in controller)
|
||||
{ slug: null, paragraph: null, kw: [] },
|
||||
],
|
||||
};
|
||||
|
||||
/* ── Classifier ───────────────────────────────────────────── */
|
||||
function classify(task) {
|
||||
const subtopic = task.subtopic;
|
||||
if (!subtopic) return { slug: null, para: null };
|
||||
|
||||
const rules = SUBTOPIC_RULES[subtopic];
|
||||
if (!rules || !rules.length) return { slug: null, para: null };
|
||||
|
||||
const txt = stripText(task.text_html);
|
||||
|
||||
let bestScore = -1;
|
||||
let bestRule = rules[rules.length - 1]; // last = fallback
|
||||
|
||||
for (const rule of rules) {
|
||||
if (!rule.kw.length) continue; // skip fallback entries in scoring
|
||||
let score = 0;
|
||||
for (const re of rule.kw) {
|
||||
if (re.test(txt)) score++;
|
||||
}
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
bestRule = rule;
|
||||
}
|
||||
}
|
||||
|
||||
// If nothing matched (bestScore == -1 and bestRule is fallback), use first entry (primary)
|
||||
if (bestScore < 0) {
|
||||
bestRule = rules[rules.length - 1]; // explicit fallback
|
||||
}
|
||||
|
||||
return { slug: bestRule.slug, para: bestRule.paragraph };
|
||||
}
|
||||
|
||||
/* ── Main ─────────────────────────────────────────────────── */
|
||||
function main() {
|
||||
const tasks = db.prepare(`
|
||||
SELECT id, variant, task_idx, task_type, subtopic, text_html
|
||||
FROM exam_tasks
|
||||
WHERE exam_key = ?
|
||||
ORDER BY variant, task_idx
|
||||
`).all(examKey);
|
||||
|
||||
if (!tasks.length) {
|
||||
console.error(`No tasks found for exam_key='${examKey}'. Check DB.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const upd = db.prepare(`
|
||||
UPDATE exam_tasks
|
||||
SET textbook_slug = ?, textbook_paragraph = ?
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
const stats = {
|
||||
total: tasks.length,
|
||||
tagged: 0,
|
||||
null_slug: 0,
|
||||
bySub: new Map(),
|
||||
bySlug: new Map(),
|
||||
};
|
||||
|
||||
const updates = [];
|
||||
for (const t of tasks) {
|
||||
const { slug, para } = classify(t);
|
||||
updates.push({ id: t.id, slug, para, subtopic: t.subtopic });
|
||||
if (slug) {
|
||||
stats.tagged++;
|
||||
const key = `${slug}#${para ?? 'hub'}`;
|
||||
stats.bySlug.set(key, (stats.bySlug.get(key) || 0) + 1);
|
||||
} else {
|
||||
stats.null_slug++;
|
||||
}
|
||||
if (t.subtopic) {
|
||||
if (!stats.bySub.has(t.subtopic)) stats.bySub.set(t.subtopic, { tagged: 0, null: 0 });
|
||||
const s = stats.bySub.get(t.subtopic);
|
||||
if (slug) s.tagged++; else s.null++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!DRY) {
|
||||
db.transaction(() => {
|
||||
for (const u of updates) upd.run(u.slug, u.para, u.id);
|
||||
})();
|
||||
}
|
||||
|
||||
if (VERBOSE) {
|
||||
for (const u of updates) {
|
||||
console.log(` id=${u.id} subtopic=${u.subtopic} -> ${u.slug}#${u.para}`);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Report ── */
|
||||
const pct = n => ((n / stats.total) * 100).toFixed(1) + '%';
|
||||
if (REPORT || true) { // always print summary
|
||||
console.log(`\n=== ${examKey} textbook-tagging${DRY ? ' (DRY RUN)' : ''} ===`);
|
||||
console.log(`Total tasks : ${stats.total}`);
|
||||
console.log(`Tagged : ${stats.tagged} (${pct(stats.tagged)})`);
|
||||
console.log(`Null/hub : ${stats.null_slug} (${pct(stats.null_slug)})`);
|
||||
|
||||
console.log(`\nBy subtopic:`);
|
||||
for (const [sub, s] of [...stats.bySub.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
||||
const tot = s.tagged + s.null;
|
||||
console.log(` ${sub.padEnd(22)} tagged=${String(s.tagged).padStart(4)}/${tot} null=${s.null}`);
|
||||
}
|
||||
|
||||
if (REPORT) {
|
||||
console.log(`\nBy chapter#paragraph (top 40):`);
|
||||
const sorted = [...stats.bySlug.entries()].sort((a, b) => b[1] - a[1]).slice(0, 40);
|
||||
for (const [key, n] of sorted) {
|
||||
console.log(` ${key.padEnd(35)} ${String(n).padStart(4)} (${pct(n)})`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (DRY) console.log(`\n[DRY RUN] No changes written.`);
|
||||
else console.log(`\nDone. ${stats.tagged} tasks tagged, ${stats.null_slug} left without slug.`);
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user