Files
Learn_System/backend/scripts/redesign_p8_ch1_2.cjs
Maxim Dolgolyov cd14e1326f fix(phys8 ch1): Phase 1.2 redo — CRLF-aware stub replace
Предыдущий коммит eaee79d удалил builders §3, §5, §6, §8 из-за
greedy regex, который пересекал границы параграфов. Фактически
жалкие 211 КБ файла вместо 280 КБ.

redesign_p8_ch1_2.cjs переписан:
- Использует точный stub-text per-paragraph (с 'Новый интерактив §N'
  в title — уникальный маркер).
- Нормализует CRLF/LF (ch1.html на диске CRLF, шаблон — LF).
- Делает простой h.replace(stubText, widget) без regex с greedy.
- Sanity-чек: все 11 builders должны остаться на месте после patch.

Восстановлены §3 Heat Conductor Bench, §6 Heat Mixer, §8 Phase
Diagram T(t) — full IV-6 interactives с drag/scrubbers/Anim.raf.
Размер ch1: 295851 байт. Все 11 builders + 5 IVs in каждом + IV-6
flagship в §1, §3, §6, §8.
2026-05-30 10:08:49 +03:00

370 lines
21 KiB
JavaScript
Raw Permalink 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.
// Phase 1.2 — Заменяет IV-6 stubs в §3, §6, §8 на полноценные интерактивы.
// Использует точный per-paragraph anchor — текст 'Новый интерактив §N' — для
// замены ровно одного стуба за раз. Без greedy match через границы.
'use strict';
const fs = require('fs');
const path = require('path');
const DST = path.join(__dirname, '..', '..', 'frontend', 'textbooks', 'physics_8_ch1.html');
let h = fs.readFileSync(DST, 'utf8');
// Stub-HTML per paragraph N (та же форма что в redesign_p8_ch1.cjs).
// Заменяем эту точную строку на новый widgetHtml.
function makeStubText(n) {
return `/* IV6 — flagship интерактив (заглушка Phase 1, наполнение в Phase 1.${n}) */
h += '<div class="wg p8-iv6">'
+'<div class="wg-header"><span class="wg-badge p8-badge p8-badge-thermal">IV-6</span><div class="wg-title">Новый интерактив §${n}</div></div>'
+'<div class="wg-help">Готовится: интерактивная визуализация с drag-and-drop для углубления темы. Скоро будет доступна.</div>'
+'<div style="padding:30px;text-align:center;color:var(--p8-muted);font-style:italic">'
+'<svg viewBox="0 0 24 24" style="width:32px;height:32px;stroke:currentColor;fill:none;stroke-width:1.5;opacity:.4"><circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/></svg>'
+'<div style="margin-top:8px;font-size:.86rem">Phase 1.${n} — coming soon</div>'
+'</div>'
+'</div>';`;
}
function replaceStub(pid, n, widgetHtml, initFn) {
// File uses CRLF, my template uses LF — normalize stub to file's EOL style.
const stubLF = makeStubText(n);
const stubCRLF = stubLF.replace(/\n/g, '\r\n');
let stubText = null;
if (h.includes(stubLF)) stubText = stubLF;
else if (h.includes(stubCRLF)) stubText = stubCRLF;
if (!stubText) {
console.warn(`${pid}: stub text not found in file`);
return false;
}
const eol = stubText === stubCRLF ? '\r\n' : '\n';
const widget = widgetHtml.trim().replace(/\n/g, eol);
h = h.replace(stubText, widget);
// Add init call after wireReadBtn
h = h.replace(`wireReadBtn('${pid}');`, `wireReadBtn('${pid}');\n _init${pid.toUpperCase()}_iv6();`);
// Append init function after build_pN
const fnStart = h.indexOf(`function build_${pid}()`);
const fnEnd = h.indexOf('\n}\n', fnStart);
h = h.slice(0, fnEnd + 3) + '\n' + initFn.trim() + '\n' + h.slice(fnEnd + 3);
console.log(`${pid}: replaced stub with real IV-6`);
return true;
}
// === §3 — Heat Conductor Bench ===
const P3_HTML = `/* IV6 — Heat Conductor Bench (Phase 1.2) */
h += '<div class="wg p8-iv6">'
+'<div class="wg-header"><span class="wg-badge p8-badge p8-badge-thermal">IV-6</span><div class="wg-title">Тепловая лавочка — какой материал быстрее проводит тепло?</div></div>'
+'<div class="wg-help">Перетащи один из стержней (медь, дерево, стекло, серебро) на горелку. Цветовая карта покажет, как тепло движется по стержню. Чем больше λ — тем быстрее.</div>'
+'<div class="p8-sandbox" id="p3-iv6-sandbox" style="height:300px"></div>'
+'<div style="margin-top:10px;display:flex;gap:10px;flex-wrap:wrap">'
+'<div class="p8-readout"><span class="p8-readout-label">Материал</span><span class="p8-readout-value" id="p3-iv6-mat">—</span></div>'
+'<div class="p8-readout"><span class="p8-readout-label">λ</span><span class="p8-readout-value" id="p3-iv6-lam">—</span><span class="p8-readout-unit">Вт/(м·К)</span></div>'
+'<div class="p8-readout"><span class="p8-readout-label">T дальнего конца</span><span class="p8-readout-value" id="p3-iv6-tend">—</span><span class="p8-readout-unit">°C</span></div>'
+'</div>'
+'</div>';`;
const P3_INIT = `
function _initP3_iv6(){
const sb = document.getElementById('p3-iv6-sandbox');
if (!sb || !window.P8Helpers || !window.P8Drag || !window.P8Anim) return;
const svg = P8Helpers.svg.create(560, 300);
svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block';
sb.appendChild(svg);
const burner = P8Helpers.svg.el('g', { transform: 'translate(80, 240)' });
burner.appendChild(P8Helpers.svg.el('rect', { x:-32, y:-8, width:64, height:32, rx:4, fill:'#475569' }));
burner.appendChild(P8Helpers.svg.el('rect', { x:-26, y:-22, width:52, height:14, rx:7, fill:'#dc2626' }));
burner.appendChild(P8Helpers.svg.el('text', { x:0, y:48, 'font-family':"'Inter',sans-serif", 'font-size':11, 'font-weight':700, fill:'#475569', 'text-anchor':'middle', text:'Горелка (drop)' }));
svg.appendChild(burner);
const rods = [
{ name:'Медь', lam:400, color:'#b45309', x:200, y:50 },
{ name:'Серебро', lam:430, color:'#9ca3af', x:300, y:50 },
{ name:'Стекло', lam:0.8, color:'#bae6fd', x:400, y:50 },
{ name:'Дерево', lam:0.15,color:'#a16207', x:500, y:50 }
];
const rodEls = [];
rods.forEach(rod => {
const g = P8Helpers.svg.el('g', { transform: 'translate('+rod.x+','+rod.y+')' });
const segments = 12;
const segs = [];
for (let s = 0; s < segments; s++) {
const r = P8Helpers.svg.el('rect', {
x: -55 + s * (110/segments), y: -10, width: 110/segments, height: 20,
fill: rod.color, stroke: 'none'
});
g.appendChild(r);
segs.push(r);
}
g.appendChild(P8Helpers.svg.el('rect', { x:-55, y:-10, width:110, height:20, rx:3, fill:'none', stroke:'#0f172a', 'stroke-width':1.5 }));
g.appendChild(P8Helpers.svg.el('text', { x:0, y:-18, 'font-family':"'Inter',sans-serif", 'font-size':11, 'font-weight':700, fill:'#0f172a', 'text-anchor':'middle', text: rod.name }));
g.appendChild(P8Helpers.svg.el('text', { x:0, y:30, 'font-family':"'JetBrains Mono',monospace", 'font-size':9, 'font-weight':600, fill:'var(--p8-muted, #64748b)', 'text-anchor':'middle', text: 'λ='+rod.lam }));
svg.appendChild(g);
rodEls.push({ rod, g, segs, x: rod.x, y: rod.y });
});
let simLoop = null;
let simTime = 0;
const matEl = document.getElementById('p3-iv6-mat');
const lamEl = document.getElementById('p3-iv6-lam');
const tendEl = document.getElementById('p3-iv6-tend');
function resetColors(rodObj){ rodObj.segs.forEach(s => s.setAttribute('fill', rodObj.rod.color)); }
function startSim(rodObj){
if (simLoop) simLoop.stop();
simTime = 0;
const lamNorm = Math.min(1, Math.log10(rodObj.rod.lam + 1) / Math.log10(500));
simLoop = P8Anim.raf(dt => {
simTime += dt;
const speed = lamNorm * 0.8 + 0.04;
rodObj.segs.forEach((seg, i) => {
const pos = i / (rodObj.segs.length - 1);
const heat = Math.max(0, Math.min(1, speed * simTime - pos));
seg.setAttribute('fill', P8Helpers.thermal.tempColor(heat * 0.85 + 0.1));
});
const endHeat = Math.max(0, Math.min(1, speed * simTime - 0.95));
const tEnd = Math.round(20 + endHeat * 80);
if (tendEl) tendEl.textContent = tEnd;
if (simTime > 30) simLoop.stop();
});
simLoop.start();
if (matEl) matEl.textContent = rodObj.rod.name;
if (lamEl) lamEl.textContent = rodObj.rod.lam;
}
rodEls.forEach((rodObj, i) => {
P8Drag.attach(rodObj.g, {
container: svg,
onMove: (ev, pos) => {
rodObj.x = pos.x;
rodObj.y = pos.y;
rodObj.g.setAttribute('transform', 'translate('+rodObj.x+','+rodObj.y+')');
},
onEnd: (ev, pos) => {
if (Math.abs(pos.x - 80) < 70 && Math.abs(pos.y - 240) < 50) {
rodEls.forEach((other, j) => { if (j !== i) resetColors(other); });
startSim(rodObj);
if (window.addXp) addXp(10, 'p3-iv6-conduct');
}
}
});
});
svg.appendChild(P8Helpers.svg.el('text', {
x: 280, y: 290,
'font-family': "'Inter', sans-serif", 'font-size': 10,
fill: 'var(--p8-muted, #64748b)', 'text-anchor': 'middle',
text: 'Перетащи стержень на горелку • Чем выше λ — тем быстрее цвет дойдёт до конца'
}));
}
`;
replaceStub('p3', 3, P3_HTML, P3_INIT);
// === §6 — Heat Mixer ===
const P6_HTML = `/* IV6 — Heat Mixer (Phase 1.2) */
h += '<div class="wg p8-iv6">'
+'<div class="wg-header"><span class="wg-badge p8-badge p8-badge-thermal">IV-6</span><div class="wg-title">Смесь двух жидкостей — рассчитай конечную T</div></div>'
+'<div class="wg-help">Установи массы и начальные T двух ёмкостей скрубберами, нажми «Смешать» и наблюдай за итоговой температурой по уравнению теплового баланса $c m_1 (T_1 - T) = c m_2 (T - T_2)$.</div>'
+'<div class="p8-sandbox" id="p6-iv6-sandbox" style="height:240px"></div>'
+'<div style="margin-top:12px;display:grid;grid-template-columns:1fr 1fr;gap:10px">'
+'<div class="p8-scrubber"><span class="p8-scrubber-label">m₁</span><input type="range" id="p6-iv6-m1" min="0.1" max="2" step="0.1" value="0.5"><span class="p8-scrubber-value"><span id="p6-iv6-m1-val">0.5</span><span class="p8-unit">кг</span></span></div>'
+'<div class="p8-scrubber"><span class="p8-scrubber-label">T₁</span><input type="range" id="p6-iv6-t1" min="0" max="100" step="1" value="80"><span class="p8-scrubber-value"><span id="p6-iv6-t1-val">80</span><span class="p8-unit">°C</span></span></div>'
+'<div class="p8-scrubber"><span class="p8-scrubber-label">m₂</span><input type="range" id="p6-iv6-m2" min="0.1" max="2" step="0.1" value="1"><span class="p8-scrubber-value"><span id="p6-iv6-m2-val">1.0</span><span class="p8-unit">кг</span></span></div>'
+'<div class="p8-scrubber"><span class="p8-scrubber-label">T₂</span><input type="range" id="p6-iv6-t2" min="0" max="100" step="1" value="20"><span class="p8-scrubber-value"><span id="p6-iv6-t2-val">20</span><span class="p8-unit">°C</span></span></div>'
+'</div>'
+'<div style="margin-top:10px;display:flex;gap:10px;flex-wrap:wrap">'
+'<div class="p8-readout"><span class="p8-readout-label">T_итог</span><span class="p8-readout-value" id="p6-iv6-tf">—</span><span class="p8-readout-unit">°C</span></div>'
+'<button class="btn primary" id="p6-iv6-mix">Смешать</button>'
+'<button class="btn" id="p6-iv6-reset">Сброс</button>'
+'</div>'
+'</div>';`;
const P6_INIT = `
function _initP6_iv6(){
const sb = document.getElementById('p6-iv6-sandbox');
if (!sb || !window.P8Helpers || !window.P8Anim) return;
const svg = P8Helpers.svg.create(560, 240);
svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block';
sb.appendChild(svg);
const v1 = { x: 140, y: 130, m: 0.5, T: 80 };
const v2 = { x: 420, y: 130, m: 1.0, T: 20 };
const finalState = { active: false, T: 50 };
function drawVessel(x, y, m, T){
const g = P8Helpers.svg.el('g', { transform: 'translate('+x+','+y+')' });
const ht = 30 + m * 50; const w = 70;
g.appendChild(P8Helpers.svg.el('rect', { x:-w/2, y:-ht, width:w, height:ht, rx:6, fill:'rgba(255,255,255,.6)', stroke:'#0f172a', 'stroke-width':1.5 }));
g.appendChild(P8Helpers.svg.el('rect', { x:-w/2+3, y:-ht+5, width:w-6, height:ht-8, rx:4, fill: P8Helpers.thermal.tempColor(T/100) }));
g.appendChild(P8Helpers.svg.el('text', { x:0, y:18, 'font-family':"'JetBrains Mono',monospace", 'font-size':11, 'font-weight':700, fill:'#0f172a', 'text-anchor':'middle', text: 'm='+m.toFixed(1)+' кг' }));
g.appendChild(P8Helpers.svg.el('text', { x:0, y:32, 'font-family':"'JetBrains Mono',monospace", 'font-size':11, 'font-weight':700, fill:'#0f172a', 'text-anchor':'middle', text: 'T='+Math.round(T)+'°C' }));
return g;
}
function redraw(){
svg.innerHTML = '';
if (!finalState.active) {
svg.appendChild(drawVessel(v1.x, v1.y, v1.m, v1.T));
svg.appendChild(drawVessel(v2.x, v2.y, v2.m, v2.T));
} else {
svg.appendChild(drawVessel(280, 130, v1.m + v2.m, finalState.T));
svg.appendChild(P8Helpers.svg.el('text', { x:280, y:60, 'font-family':"'Unbounded',sans-serif", 'font-size':14, 'font-weight':800, fill:'var(--th-mid,#f97316)', 'text-anchor':'middle', text: 'T_итог = '+Math.round(finalState.T)+' °C' }));
}
}
function bindScrub(inputId, valId, obj, prop){
const input = document.getElementById(inputId);
const lab = document.getElementById(valId);
if (!input || !lab) return;
input.addEventListener('input', () => {
const v = parseFloat(input.value);
obj[prop] = v;
lab.textContent = v.toFixed(prop === 'm' ? 1 : 0);
if (finalState.active) { finalState.active = false; document.getElementById('p6-iv6-tf').textContent = '—'; }
redraw();
});
}
bindScrub('p6-iv6-m1', 'p6-iv6-m1-val', v1, 'm');
bindScrub('p6-iv6-t1', 'p6-iv6-t1-val', v1, 'T');
bindScrub('p6-iv6-m2', 'p6-iv6-m2-val', v2, 'm');
bindScrub('p6-iv6-t2', 'p6-iv6-t2-val', v2, 'T');
document.getElementById('p6-iv6-mix').onclick = () => {
const T = (v1.m * v1.T + v2.m * v2.T) / (v1.m + v2.m);
finalState.active = true;
P8Anim.tween({
from: v1.T, to: T, duration: 1200, easing: 'cubicInOut',
onUpdate: t => { finalState.T = t; redraw(); document.getElementById('p6-iv6-tf').textContent = Math.round(t); }
});
if (window.addXp) addXp(10, 'p6-iv6-mix');
};
document.getElementById('p6-iv6-reset').onclick = () => {
finalState.active = false; document.getElementById('p6-iv6-tf').textContent = '—'; redraw();
};
redraw();
}
`;
replaceStub('p6', 6, P6_HTML, P6_INIT);
// === §8 — Phase Diagram T(t) ===
const P8_HTML = `/* IV6 — Phase Diagram T(t) (Phase 1.2) */
h += '<div class="wg p8-iv6">'
+'<div class="wg-header"><span class="wg-badge p8-badge p8-badge-thermal">IV-6</span><div class="wg-title">График плавления — почему T не растёт?</div></div>'
+'<div class="wg-help">Запусти нагрев льда и наблюдай T(t). При плавлении энергия идёт на разрушение решётки — T держится постоянной (плато при 0°C). Двигай мощность нагревателя — крутизна меняется.</div>'
+'<div class="p8-sandbox" id="p8-iv6-sandbox" style="height:280px"></div>'
+'<div style="margin-top:12px;display:flex;gap:14px;flex-wrap:wrap">'
+'<div class="p8-scrubber" style="flex:1;min-width:200px"><span class="p8-scrubber-label">Мощность</span><input type="range" id="p8-iv6-pwr" min="100" max="2000" step="50" value="500"><span class="p8-scrubber-value"><span id="p8-iv6-pwr-val">500</span><span class="p8-unit">Вт</span></span></div>'
+'<div class="p8-readout"><span class="p8-readout-label">Фаза</span><span class="p8-readout-value" id="p8-iv6-phase">лёд</span></div>'
+'<div class="p8-readout"><span class="p8-readout-label">T</span><span class="p8-readout-value" id="p8-iv6-temp">-20</span><span class="p8-readout-unit">°C</span></div>'
+'<button class="btn primary" id="p8-iv6-play">Старт</button>'
+'<button class="btn" id="p8-iv6-reset">Сброс</button>'
+'</div>'
+'</div>';`;
const P8_INIT = `
function _initP8_iv6(){
const sb = document.getElementById('p8-iv6-sandbox');
if (!sb || !window.P8Helpers || !window.P8Anim) return;
const W = 560, H = 280;
const svg = P8Helpers.svg.create(W, H);
svg.setAttribute('width','100%'); svg.setAttribute('height','100%'); svg.style.display='block';
sb.appendChild(svg);
const m = 0.5;
const c_ice = 2100, c_water = 4200, lambda = 330000, r_vap = 2300000;
let power = 500, energyAccumulated = 0, running = false;
let points = [{ t: 0, T: -20 }];
const pad = { l: 50, r: 18, t: 22, b: 32 };
const plotW = W - pad.l - pad.r;
const plotH = H - pad.t - pad.b;
svg.appendChild(P8Helpers.svg.el('rect', { x: pad.l, y: pad.t, width: plotW, height: plotH, fill: '#fafafa', stroke: '#e5e7eb' }));
const yMin = -20, yMax = 120;
function yToPx(T) { return pad.t + plotH * (1 - (T - yMin) / (yMax - yMin)); }
function tToPx(t) { return pad.l + plotW * Math.min(1, t / 300); }
[-20, 0, 20, 40, 60, 80, 100, 120].forEach(t => {
const y = yToPx(t);
svg.appendChild(P8Helpers.svg.el('line', { x1: pad.l, y1: y, x2: pad.l + plotW, y2: y, stroke: '#e5e7eb' }));
svg.appendChild(P8Helpers.svg.el('text', { x: pad.l - 6, y: y + 3, 'font-family':"'JetBrains Mono',monospace", 'font-size': 10, fill: 'var(--p8-muted,#64748b)', 'text-anchor':'end', text: t+'°' }));
});
const phaseRegions = [
{ from: -20, to: 0, fill: '#bfdbfe', name: 'лёд' },
{ from: 0, to: 100, fill: '#7dd3fc', name: 'вода' },
{ from: 100, to: 120, fill: '#fde68a', name: 'пар' }
];
phaseRegions.forEach(r => {
const y1 = yToPx(r.from), y2 = yToPx(r.to);
svg.appendChild(P8Helpers.svg.el('rect', { x: pad.l, y: y2, width: plotW, height: y1 - y2, fill: r.fill, opacity: 0.18 }));
svg.appendChild(P8Helpers.svg.el('text', { x: pad.l + plotW - 6, y: (y1 + y2) / 2 + 3, 'font-family':"'Inter',sans-serif", 'font-size': 10, 'font-weight': 700, fill: 'var(--p8-text)', 'text-anchor': 'end', text: r.name }));
});
svg.appendChild(P8Helpers.svg.el('line', { x1: pad.l, y1: yToPx(0), x2: pad.l + plotW, y2: yToPx(0), stroke: '#0f172a', 'stroke-width': 1, 'stroke-dasharray': '3 3' }));
svg.appendChild(P8Helpers.svg.el('line', { x1: pad.l, y1: yToPx(100), x2: pad.l + plotW, y2: yToPx(100), stroke: '#0f172a', 'stroke-width': 1, 'stroke-dasharray': '3 3' }));
svg.appendChild(P8Helpers.svg.el('line', { x1: pad.l, y1: pad.t + plotH, x2: pad.l + plotW, y2: pad.t + plotH, stroke: '#0f172a' }));
svg.appendChild(P8Helpers.svg.el('text', { x: pad.l + plotW / 2, y: H - 6, 'font-family':"'Inter',sans-serif", 'font-size': 11, 'font-weight': 700, fill: 'var(--p8-text)', 'text-anchor': 'middle', text: 'Время, с' }));
const path = P8Helpers.svg.el('path', { d: '', fill: 'none', stroke: 'var(--th-mid, #f97316)', 'stroke-width': 3, 'stroke-linejoin': 'round', 'stroke-linecap': 'round' });
svg.appendChild(path);
function updatePath(){
if (!points.length) return;
const d = points.map((p, i) => (i === 0 ? 'M' : 'L') + tToPx(p.t).toFixed(1) + ',' + yToPx(p.T).toFixed(1)).join(' ');
path.setAttribute('d', d);
}
function currentT(){ return points[points.length-1].T; }
function currentPhase(T){
if (T < 0) return 'лёд';
if (T < 0.5 && energyAccumulated < lambda * m) return 'плавление';
if (T < 100) return 'вода';
if (T < 100.5 && energyAccumulated < (lambda + r_vap) * m) return 'кипение';
return 'пар';
}
function tick(dt){
if (!running) return;
const energy = power * dt;
let T = currentT();
let newT = T;
if (T < 0) {
const dT = energy / (c_ice * m);
newT = T + dT;
if (newT > 0) newT = 0;
} else if (T < 0.5 && energyAccumulated < lambda * m) {
energyAccumulated += energy;
newT = 0;
if (energyAccumulated >= lambda * m) newT = 0.5;
} else if (T < 100) {
const dT = energy / (c_water * m);
newT = T + dT;
if (newT > 100) newT = 100;
} else if (T < 100.5 && energyAccumulated < (lambda + r_vap) * m) {
energyAccumulated += energy;
newT = 100;
if (energyAccumulated >= (lambda + r_vap) * m) newT = 100.5;
} else if (T < 120) {
const dT = energy / (c_water * m);
newT = T + dT;
if (newT > 120) newT = 120;
} else { running = false; }
const lastP = points[points.length-1];
points.push({ t: lastP.t + dt, T: newT });
if (points.length > 600) points.shift();
updatePath();
document.getElementById('p8-iv6-temp').textContent = Math.round(newT);
document.getElementById('p8-iv6-phase').textContent = currentPhase(newT);
if (lastP.t > 300) running = false;
}
const raf = P8Anim.raf(dt => tick(Math.min(dt * 4, 0.5)));
const pwrInp = document.getElementById('p8-iv6-pwr');
const pwrLab = document.getElementById('p8-iv6-pwr-val');
pwrInp.oninput = () => { power = +pwrInp.value; pwrLab.textContent = power; };
document.getElementById('p8-iv6-play').onclick = () => {
if (!running) { running = true; raf.start(); if (window.addXp) addXp(10, 'p8-iv6-melt'); }
};
document.getElementById('p8-iv6-reset').onclick = () => {
running = false; raf.stop();
energyAccumulated = 0;
points = [{ t: 0, T: -20 }];
updatePath();
document.getElementById('p8-iv6-temp').textContent = '-20';
document.getElementById('p8-iv6-phase').textContent = 'лёд';
};
updatePath();
}
`;
replaceStub('p8', 8, P8_HTML, P8_INIT);
fs.writeFileSync(DST, h);
console.log('ch1 final size:', h.length);
const scripts = [...h.matchAll(/<script>([\s\S]*?)<\/script>/g)];
for (const m of scripts) {
try { new Function(m[1]); }
catch (e) { console.error('JS PARSE FAIL:', e.message.slice(0, 150)); process.exit(1); }
}
console.log('inline JS parses OK');
// Verify all 11 builders still present
const fns = [...h.matchAll(/function build_p(\d+)\(\)/g)].map(m => parseInt(m[1]));
console.log('Builders after:', fns.length, fns);
if (fns.length !== 11) { console.error('LOST BUILDERS!'); process.exit(1); }