Files
houseplan-card/test/core-file-budget.test.mjs
T
Claude b76f3e5756 refactor: вынести четыре диалога настроек в модули (#592)
Шаг 0 эпика #591. Видимых изменений нет — это подготовка: оба ядра стояли в
трёх строках от жёсткого потолка, а разметка диалогов была крупнейшей
перемещаемой частью редакторского рантайма.

Перенос механический и проверяемый. Тела всех четырёх методов уехали
побайтово: тип `this` объявлен параметром функции, поэтому ни одна строка
разметки, ни один обработчик и ни один якорь мутанта не переписаны, а в классе
остались однострочные делегаты. Сверка при подготовке коммита: 43 617, 16 795,
10 590 и 6 719 символов совпали посимвольно с оригиналом.

  src/editors/marker-dialog.ts            диалог устройства на плане
  src/editors/space-settings-dialog.ts    диалог пространства
  src/editors/general-settings-dialog.ts  общие настройки
  src/editors/room-settings-dialog.ts     настройки комнаты

Имена файлов пространства и комнаты уточнены против ТЗ: рядом уже живёт
`src/space-dialog.ts` с логикой того же диалога, и два `space-dialog.ts` в
одном дереве путали бы обоих — и человека, и grep.

Попутно из рантайма уехали два словаря ярлыков режима отображения (их
единственный потребитель — диалог устройства) и границы шага сетки: последние
теперь объявлены в модуле, который рисует поле, а кламп записи импортирует их
оттуда. Направление импорта то же, что у самой функции рисования, цикла нет.

`_radarSetup` стал публичным: диалог устройства обращается к контроллеру из
своего модуля, а остальной класс и так живёт на `public _x`.

Потолок ядра опущен 14100 → 12810 — ровно на 1290 вынесенных строк, запас над
фактом прежний. Незафиксированный выигрыш ядро отыграло бы первой же правкой.

Сборщик логического исходника для контрактных тестов (`test/houseplan-source.mjs`)
научен подставлять тело вынесенной функции вместо делегата: шесть тестов,
читающих разметку, продолжают видеть ровно то же, что и до переноса, — иначе
их пришлось бы переучивать на новые пути в тот самый момент, когда они должны
доказать, что перенос ничего не изменил.

Issue: #592
User-Visible: no
2026-09-18 19:36:47 +03:00

128 lines
7.7 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
// #425, заменяет #34. Декомпозиция фронтенда идёт попутно и продолжит идти, но
// два ядра всё равно прибавляют по 500–1000 строк за релиз: новое уезжает в
// новые модули, а из старых ничего не уходит. Проект «разложить всё по
// каталогам» не начался за 25 дней и не начнётся; вместо него — потолок,
// который делает рост осознанным.
//
// Правило простое: хочешь добавить в ядро — вынеси столько же. Число в дифе,
// решение в ревью.
// Мера — `split('\n').length`, то есть строки плюс завершающий перевод.
// Та же функция и для потолков, и для измерения: две разные меры разошлись бы
// на единицу, и гейт краснел бы на пустом месте (проверено при написании).
const SLACK = 250;
// Потолки. Меняются только вручную и только вместе с объяснением в ревью:
// потолок, который вычисляется от текущего размера, потолком не является.
const CAPS = {
// #485 adds the View-side subscription/render integration seams; the live
// model and rendering themselves remain in dedicated modules.
// #500 moved config/layout identity and the adoption sequence into
// src/config-adoption.ts; the ratchet keeps that win.
// 2026-09-11, #534: +1 for `import { keyed }`. The line buys back the 80 ms
// that #525 put on every space switch, and there is nothing to move out —
// the render itself did not grow by a character.
// 2026-09-14, #564: the geometry/index/latch/DOM ownership implementation is
// isolated in device-hit-owner.ts. These 81 lines are the remaining card
// lifecycle invalidations and the presentation/action integration seams.
'src/houseplan-card.ts': 13732,
// #478 removed the persisted room-draft editor branch. Keep that reduction.
// #485 keeps its large setup surface in editors/radar-section.ts; these are
// only the dialog state/save seam and the thin lazy-render adapter.
// 2026-09-18, #592: разметка четырёх диалогов настроек уехала в
// src/editors/{marker,space-settings,general-settings,room-settings}-dialog.ts
// — 1290 строк. Потолок опущен ровно на эту величину (14100 → 12810), запас
// над фактом остался прежним, три строки: выигрыш зафиксирован, иначе ядро
// отыграет его обратно первой же правкой диалога.
'src/houseplan-editor-runtime.ts': 12810,
};
/**
* Храповик: наверх не пускает, вниз — требует зафиксировать выигрыш.
*
* Вторая половина важнее первой. Без неё вынос двух тысяч строк ничего не
* изменит: потолок останется прежним, и через полгода ядро дорастёт до него
* обратно — молча и «в рамках бюджета».
*/
export function coreBudgetViolations(sizes, caps, slack = SLACK) {
const problems = [];
for (const [file, cap] of Object.entries(caps)) {
const lines = sizes[file];
if (typeof lines !== 'number') {
problems.push({ file, kind: 'missing', text: `${file}: файл не измерен` });
continue;
}
if (lines > cap) {
problems.push({
file, kind: 'grew', over: lines - cap,
text: `${file}: ${lines} строк при потолке ${cap} — выросло на ${lines - cap}.`
+ ' Вынесите столько же в отдельный модуль либо поднимите потолок'
+ ' отдельным решением, объяснив его в ревью.',
});
} else if (lines < cap - slack) {
problems.push({
file, kind: 'shrank', under: cap - lines,
text: `${file}: ${lines} строк при потолке ${cap} — на ${cap - lines} меньше.`
+ ' Опустите потолок: незафиксированный выигрыш ядро отыграет обратно.',
});
}
}
return problems;
}
const measure = (file) => readFileSync(new URL(`../${file}`, import.meta.url), 'utf8').split('\n').length;
test('ядра не выросли выше потолка и не опустились ниже него молча', () => {
const sizes = Object.fromEntries(Object.keys(CAPS).map((file) => [file, measure(file)]));
const problems = coreBudgetViolations(sizes, CAPS);
assert.deepEqual(problems.map((p) => p.text), [], problems.map((p) => p.text).join('\n'));
});
test('рост выше потолка становится нарушением с числом', () => {
const [problem] = coreBudgetViolations({ 'a.ts': 1300 }, { 'a.ts': 1000 });
assert.equal(problem.kind, 'grew');
assert.equal(problem.over, 300);
assert.match(problem.text, /выросло на 300/);
});
test('заметное уменьшение требует опустить потолок', () => {
const [problem] = coreBudgetViolations({ 'a.ts': 700 }, { 'a.ts': 1000 });
assert.equal(problem.kind, 'shrank');
assert.equal(problem.under, 300);
assert.match(problem.text, /Опустите потолок/);
});
test('изменение в пределах люфта не трогает никого', () => {
assert.deepEqual(coreBudgetViolations({ 'a.ts': 1000 }, { 'a.ts': 1000 }), []);
assert.deepEqual(coreBudgetViolations({ 'a.ts': 800 }, { 'a.ts': 1000 }), []);
assert.deepEqual(coreBudgetViolations({ 'a.ts': 751 }, { 'a.ts': 1000 }), []);
});
test('границы включительно: ровно потолок и ровно люфт нарушением не считаются', () => {
assert.deepEqual(coreBudgetViolations({ 'a.ts': 1000 }, { 'a.ts': 1000 }), []);
assert.deepEqual(coreBudgetViolations({ 'a.ts': 750 }, { 'a.ts': 1000 }), []);
assert.equal(coreBudgetViolations({ 'a.ts': 1001 }, { 'a.ts': 1000 })[0].kind, 'grew');
assert.equal(coreBudgetViolations({ 'a.ts': 749 }, { 'a.ts': 1000 })[0].kind, 'shrank');
});
test('потолки заданы для двух ядер и ни для чего больше', () => {
assert.deepEqual(Object.keys(CAPS).sort(),
['src/houseplan-card.ts', 'src/houseplan-editor-runtime.ts']);
});
test('потолки — числа в этом файле, а не вычисление от текущего размера', () => {
// AC4. Потолок, который считается от того, что сейчас на диске, разрешает
// любой рост и запрещает только уменьшение — то есть работает наоборот.
const source = readFileSync(new URL('./core-file-budget.test.mjs', import.meta.url), 'utf8');
const caps = source.slice(source.indexOf('const CAPS'), source.indexOf('};', source.indexOf('const CAPS')));
assert.match(caps, /'src\/houseplan-card\.ts': \d+/);
assert.doesNotMatch(caps, /measure|readFileSync|process\.env/);
for (const problem of coreBudgetViolations({ 'a.ts': 10 }, { 'a.ts': 10 })) {
assert.fail(`функция обязана принимать потолки аргументом: ${problem.text}`);
}
});