Files
Learn_System/backend/scripts/review_geom11.js
T
Maxim Dolgolyov 5381679c68 chore: консолидация незакоммиченной работы (биохимия + System Health + lab/textbooks)
Зафиксирована накопленная незакоммиченная работа рабочего дерева, КРОМЕ файлов
учебника «Химия 7» (migration 046, chemistry_7_*.html, chem7_svg.js, тест —
оставлены незакоммиченными по запросу).

Включает: модуль биохимии (ядро BIO, 3D VSEPR, химдвижок, баланс, challenges,
пути из БД), System Health Level 1 (вердикт/мониторинг), а также frontend-
страницы и lab/textbooks-правки параллельной сессии.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 18:12:55 +03:00

228 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// review_geom11.js - аудит Геометрии 11 (только репорт)
const fs = require('fs');
const path = require('path');
const FILES = [
'frontend/textbooks/geometry_11_hub.html',
'frontend/textbooks/geometry_11_ch1.html',
'frontend/textbooks/geometry_11_ch2.html',
'frontend/textbooks/geometry_11_ch3.html',
'frontend/textbooks/geometry_11_ch4.html',
];
const ROOT = path.join(__dirname, '..', '..');
// Словарь KaTeX-команд (часто встречающиеся)
const KATEX_CMDS = new Set([
'dfrac','frac','tfrac','sqrt','cdot','times','pm','mp','ne','le','ge','approx',
'angle','triangle','square','vec','overline','overrightarrow','overrightarrow',
'sin','cos','tan','cot','arcsin','arccos','arctan','log','ln','lg','exp',
'pi','alpha','beta','gamma','delta','varepsilon','theta','lambda','mu','phi','omega','rho','sigma','tau',
'Delta','Gamma','Omega','Phi','Sigma','Theta','Lambda',
'boxed','underline','mathbf','mathrm','text','textbf','textit',
'sum','prod','int','infty','lim','to','rightarrow','leftarrow','Rightarrow','Leftrightarrow',
'left','right','big','Big','bigg','Bigg','quad','qquad',',','!',
'cap','cup','in','notin','subset','supset','emptyset','varnothing',
'parallel','perp','cong','sim','equiv','neq',
'begin','end','array','matrix','pmatrix','bmatrix','cases','aligned','align',
'displaystyle','textstyle','scriptstyle',
'colon','ldots','cdots','vdots','ddots',
'mathbb','mathcal','mathfrak','operatorname',
'hat','tilde','bar','dot','widehat','widetilde',
'binom','choose','over','atop',
'div','star','ast','circ','bullet',
'forall','exists','land','lor','neg','iff','implies',
'leq','geq','prec','succ','subseteq','supseteq',
'partial','nabla','degree','prime',
'oplus','ominus','otimes','odot',
'mathring','space','phantom','vphantom',
'overset','underset','stackrel',
'color','textcolor','rgb','href',
'rule','kern','mskip','hspace','vspace',
'tag','label','ref',
'lvert','rvert','lVert','rVert','vert','Vert','|',
'oint','iint','iiint',
'because','therefore',
'leftrightarrow','Leftarrow','longleftarrow','longrightarrow',
'mapsto','hookrightarrow',
]);
const EMOJI_CHARS = ['✓','✗','⚠','🔥','📐','✅','❌','🎯','🔴','🟢','🟡','📊','📈','📉','🎓','📚','🧮','🔬','🌟','⭐','💡','🚀','🎉','📝','📖','🗒'];
const AUTHOR_NAMES = ['Латотин','Чеботаревский','Горбунова','Цыбулько','Шлыков','Подгорная'];
function scanFile(rel) {
const fp = path.join(ROOT, rel);
const src = fs.readFileSync(fp, 'utf8');
const out = { file: rel, katex: [], optionsKatex: [], emoji: [], authors: [], pixels: [], g3d: {} };
// 1. KaTeX ошибки: ищем backslash перед буквой (одиночный) в JS template literals и в HTML
// Анализируем строчно
const lines = src.split(/\r?\n/);
// Regex: одиночный \ перед 2+ буквами, не предшествуемый \
const reSingle = /(?<!\\)\\([a-zA-Z]{2,})/g;
// Regex: двойной \\command (нормальный для JS-литералов)
// Чтобы не плодить шум: репортим только когда команда из KATEX_CMDS
// Делим на блоки: внутри <script> и вне
let inScript = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (/<script(\s|>)/i.test(line)) inScript = true;
// Сначала проверим текущую строку
// Внутри <script> — нужен \\cmd (т.к. JS экранирует). Одиночный \cmd = ошибка.
// Вне <script> (чистый HTML, например в title) — \cmd нормально для KaTeX render.
// Найдём все совпадения \cmd
let m;
const matches = [];
const re = /\\+([a-zA-Z]{2,})/g;
while ((m = re.exec(line)) !== null) {
const bsCount = m[0].length - m[1].length; // число backslashes
const cmd = m[1];
if (!KATEX_CMDS.has(cmd)) continue;
matches.push({ cmd, bsCount, idx: m.index, full: m[0] });
}
for (const mt of matches) {
if (inScript) {
// в JS template literals одиночный \X неправильно. Нужно \\X (bsCount>=2)
if (mt.bsCount === 1) {
out.katex.push({ line: i+1, cmd: mt.cmd, snippet: line.trim().slice(0, 140) });
}
} else {
// в чистом HTML \cmd ок
}
}
if (/<\/script>/i.test(line)) inScript = false;
}
// 2. <option> с сырым KaTeX (содержит $ или \\sqrt и т.п.)
const optRe = /<option[^>]*>([^<]*)<\/option>/g;
let om;
while ((om = optRe.exec(src)) !== null) {
const txt = om[1];
if (/\$[^$]+\$/.test(txt) || /\\\\[a-zA-Z]+/.test(txt) || /\\(dfrac|sqrt|frac|sin|cos|pi|angle|vec|cdot|times)/.test(txt)) {
out.optionsKatex.push({ snippet: om[0].slice(0,160) });
}
}
// Также проверим динамический innerHTML на option в JS
const scriptBlocks = [...src.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script>/g)].map(m=>m[1]);
for (const sb of scriptBlocks) {
// ищем <option ...>$...$</option> или \\sqrt в template-literals
const re2 = /<option[^>]*>[^<]*(?:\$[^$]+\$|\\\\(?:dfrac|sqrt|frac|sin|cos|pi|angle|vec|cdot|times))[^<]*<\/option>/g;
let mm;
while ((mm = re2.exec(sb)) !== null) {
out.optionsKatex.push({ snippet: mm[0].slice(0,160), inScript: true });
}
}
// 3. Эмодзи (исключая SVG)
for (const ch of EMOJI_CHARS) {
let idx = 0;
while ((idx = src.indexOf(ch, idx)) !== -1) {
// Найти строку
const upto = src.slice(0, idx);
const lineNum = upto.split('\n').length;
const lineStart = upto.lastIndexOf('\n') + 1;
const lineEnd = src.indexOf('\n', idx);
const lineText = src.slice(lineStart, lineEnd === -1 ? src.length : lineEnd);
out.emoji.push({ char: ch, line: lineNum, snippet: lineText.trim().slice(0, 120) });
idx++;
}
}
// 4. Авторы
for (const name of AUTHOR_NAMES) {
if (src.includes(name)) {
// Найдём строку
const idx = src.indexOf(name);
const upto = src.slice(0, idx);
const lineNum = upto.split('\n').length;
const lineStart = upto.lastIndexOf('\n') + 1;
const lineEnd = src.indexOf('\n', idx);
const lineText = src.slice(lineStart, lineEnd === -1 ? src.length : lineEnd);
out.authors.push({ name, line: lineNum, snippet: lineText.trim().slice(0, 160) });
}
}
// 5. Пиксели в подписях: ищем sliders с min/max, и подписи рядом
// Простая эвристика: <input type="range" min="X" max="Y" ...> + value/label рядом
const sliderRe = /<input[^>]*type=["']range["'][^>]*>/g;
let sm;
while ((sm = sliderRe.exec(src)) !== null) {
const tag = sm[0];
const minM = tag.match(/\bmin=["'](\d+)["']/);
const maxM = tag.match(/\bmax=["'](\d+)["']/);
const idM = tag.match(/\bid=["']([^"']+)["']/);
if (minM && maxM) {
const min = +minM[1], max = +maxM[1];
// подозрительно если min>20 и max>100 (вероятно пиксели)
const suspicious = min >= 20 && max >= 100;
if (suspicious) {
const upto = src.slice(0, sm.index);
const lineNum = upto.split('\n').length;
out.pixels.push({ line: lineNum, id: idM ? idM[1] : '', min, max, tag: tag.slice(0,180) });
}
}
}
// 6. G3D usage
out.g3d.includesScript = /<script[^>]*src=["']\/js\/g3d\.js["'][^>]*>/.test(src);
out.g3d.usesPrism = /G3D\.prismMesh/.test(src);
out.g3d.usesCylinder = /G3D\.cylinderMesh/.test(src);
out.g3d.usesPyramid = /G3D\.pyramidMesh/.test(src);
out.g3d.usesCone = /G3D\.coneMesh/.test(src);
out.g3d.usesSphere = /G3D\.sphereWireframe/.test(src);
out.g3d.usesAttachOrbit = /G3D\.attachOrbit/.test(src);
out.g3d.usesPresetView = /G3D\.presetView/.test(src);
out.g3d.viewButtons = (src.match(/Изо|Спереди|Сверху|Сбоку/g) || []).length;
// 7. Структура: theory cards (3), interactives (4), "Я прочитал"
out.theoryCards = (src.match(/class=["'][^"']*theory[^"']*card/g) || []).length;
out.interactiveCards = (src.match(/class=["'][^"']*interactive[^"']*/g) || []).length;
out.readBtns = (src.match(/Я прочитал|я прочитал/g) || []).length;
out.bossCount = (src.match(/босс|Босс|BOSS/g) || []).length;
out.paragraphCount = (src.match(/§\d+/g) || []).length;
out.finalSections = (src.match(/Финал|финал раздела|Финал раздела/g) || []).length;
out.length = src.length;
return out;
}
const results = FILES.map(scanFile);
// Report
console.log('='.repeat(80));
console.log('AUDIT GEOMETRY 11');
console.log('='.repeat(80));
let totals = { katex: 0, options: 0, emoji: 0, authors: 0, pixels: 0 };
for (const r of results) {
console.log(`\n--- ${r.file} (${(r.length/1024).toFixed(1)} KB) ---`);
console.log(`KaTeX errors: ${r.katex.length}`);
if (r.katex.length) {
r.katex.slice(0, 10).forEach(e => console.log(` L${e.line}: \\${e.cmd} | ${e.snippet}`));
if (r.katex.length > 10) console.log(` ... +${r.katex.length-10} more`);
}
console.log(`<option> KaTeX: ${r.optionsKatex.length}`);
r.optionsKatex.slice(0,5).forEach(o => console.log(` ${o.inScript?'[JS] ':''}${o.snippet}`));
console.log(`Emoji: ${r.emoji.length}`);
r.emoji.slice(0,5).forEach(e => console.log(` L${e.line} '${e.char}': ${e.snippet}`));
console.log(`Authors: ${r.authors.length}`);
r.authors.slice(0,5).forEach(a => console.log(` L${a.line} '${a.name}': ${a.snippet}`));
console.log(`Pixel sliders (susp.): ${r.pixels.length}`);
r.pixels.slice(0,5).forEach(p => console.log(` L${p.line} #${p.id} min=${p.min} max=${p.max}`));
console.log(`G3D: script=${r.g3d.includesScript}, prism=${r.g3d.usesPrism}, cyl=${r.g3d.usesCylinder}, pyr=${r.g3d.usesPyramid}, cone=${r.g3d.usesCone}, sphere=${r.g3d.usesSphere}, orbit=${r.g3d.usesAttachOrbit}, presetView=${r.g3d.usesPresetView}, viewBtnHits=${r.g3d.viewButtons}`);
console.log(`Structure: theoryCards=${r.theoryCards}, interactives=${r.interactiveCards}, readBtns=${r.readBtns}, bossHits=${r.bossCount}, §count=${r.paragraphCount}, finalHits=${r.finalSections}`);
totals.katex += r.katex.length;
totals.options += r.optionsKatex.length;
totals.emoji += r.emoji.length;
totals.authors += r.authors.length;
totals.pixels += r.pixels.length;
}
console.log('\n' + '='.repeat(80));
console.log('TOTALS:', totals);
console.log('='.repeat(80));