feat(biochem): Фаза 2.1/2.2/2.4 — серверный chem.js + /analyze + подсказки валентности

- biochem-core.js dual-export (browser window.BIO + Node module.exports), без дублей
- BIO.valency: подробные подсказки валентности (2.4), общие для редактора и сервера
- services/chem.js: серверный анализ поверх того же ядра (analyze/validate)
- POST /api/biochem/analyze (2.2); /validate переведён на ядро (+фикс формата связей)
- api.js: LS.biochemAnalyze

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-30 22:37:59 +03:00
parent 8b5d9238b5
commit b67fac6407
7 changed files with 125 additions and 26 deletions
+3 -8
View File
@@ -683,13 +683,8 @@ function getBondSum(id) {
return bonds.reduce((s,b) => s + (b.from===id||b.to===id ? (b.order||b.o||1) : 0), 0);
}
function getIssues() {
return atoms.filter(a => {
const used = bonds.reduce((s,b)=>(b.from===a.id||b.to===a.id)?s+(b.order||1):s,0);
return used > (ELEMENTS[a.s]?.maxV ?? 4);
}).map(a => {
const used = bonds.reduce((s,b)=>(b.from===a.id||b.to===a.id)?s+(b.order||1):s,0);
return { id:a.id, s:a.s, used, max: ELEMENTS[a.s]?.maxV??4 };
});
// Единая проверка валентности из ядра (с подсказками, Фаза 2.4)
return BIO.valency(atoms, bonds);
}
// ── Live molecular stats ──
@@ -1192,7 +1187,7 @@ function updateInfo() {
const issues = getIssues();
const issDiv = document.getElementById('bp-issues');
if (issues.length) {
issDiv.innerHTML = issues.map(i => `<div class="bp-issue"><svg class="ic" viewBox="0 0 24 24"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg> ${i.s}: ${i.used}/${i.max} связей</div>`).join('');
issDiv.innerHTML = issues.map(i => `<div class="bp-issue"><svg class="ic" viewBox="0 0 24 24"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg> ${i.msg}</div>`).join('');
} else if (formula) {
issDiv.innerHTML = '<div class="bp-ok"><svg class="ic" viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg> Валентность в норме</div>';
} else {
+42 -4
View File
@@ -911,15 +911,53 @@
},
};
/* ── Экспорт ──────────────────────────────────────────────────────────── */
global.BIO = {
/* ── Валентность: подробная проверка с подсказками (Фаза 2.4) ──────────
* Возвращает массив проблем-«перевалентностей» с готовым человекочитаемым
* текстом: [{ id, symbol, name, used, max, over, kind:'error', msg }].
* Работает с обоими форматами связей (from/to/order и f/t/o) через bF/bT/bO.
*/
function _bondWord(n) {
const m10 = n % 10, m100 = n % 100;
if (m10 === 1 && m100 !== 11) return 'связь';
if (m10 >= 2 && m10 <= 4 && (m100 < 12 || m100 > 14)) return 'связи';
return 'связей';
}
function valency(atoms, bonds) {
if (!atoms || !atoms.length) return [];
const sum = {};
for (const b of (bonds || [])) {
const f = bF(b), t = bT(b), o = bO(b);
sum[f] = (sum[f] || 0) + o;
sum[t] = (sum[t] || 0) + o;
}
const out = [];
for (const a of atoms) {
const e = el(a.s);
const used = sum[a.id] || 0;
const max = e.maxV != null ? e.maxV : 4;
if (used > max) {
const over = used - max;
out.push({
id: a.id, symbol: a.s, name: e.name, used, max, over, kind: 'error',
msg: e.name + ' (' + a.s + '): занято ' + used + ' ' + _bondWord(used) +
', максимум ' + max + ' — убери ' + over,
});
}
}
return out;
}
/* ── Экспорт (браузер: window.BIO; Node: module.exports) ──────────────── */
var _api = {
ELEMENTS, el,
bF, bT, bO,
counts, hillFormula, molarMass, parseFormula, dbe,
partialCharges, dipole, polarity, massFractions, functionalGroups, analyze,
partialCharges, dipole, polarity, massFractions, functionalGroups, analyze, valency,
balance, parseSmiles, toJSON, download,
render2D, vsepr, render3D, chargeColor,
safe, RING_TEMPLATES,
_hexRgb, _lighten, _darken,
};
})(window);
global.BIO = _api;
if (typeof module !== 'undefined' && module.exports) module.exports = _api;
})(typeof window !== 'undefined' ? window : globalThis);