mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
Карточка и редакторский рантайм держали ≈380 неиспользуемых импортов, 56 мёртвых объявлений и дублей типов (warm-boot, LS_*, GLOW_*, debounce, navigate, lruRead — копии карточки в рантайме) и 112 приватных членов карточки, которых не читал никто — делегаты `_editorRuntimeOrThrow()._x()`, оставшиеся от выноса #425, и аксессоры glow-состояния. Всё это снято; в 9 других файлах — по одиночной ошибке. Делегаты и поля, которых касаются браузерные смоки (`card._x(...)`), оставлены и посчитаны отдельно. Гейт `npm run lint:unused` (scripts/unused-locals-gate.mjs, в gate:small и Validate после сборки): `tsc --noUnusedLocals` чист, кроме приватных членов карточки из порта HouseplanEditorHostPort / `host.` (portPrivates) и членов, которых зовёт харнесс (harnessPrivates); храповик по шести числам scripts/monolith-metrics.mjs против scripts/monolith-baseline.json — delegates 260→159, portMembers 350, hostRefs 4948, portPrivates 96, harnessPrivates 107, bundleBytes 2 510 141→2 500 387. `npm run inventory` печатает те же числа. Заморозка 54 тестов, читающих монолит как текст (test/monolith-text-anchors.test.mjs); PROCESS.md §2.7 — правило. Логический исходник для контрактных тестов (test/houseplan-source.mjs) дописывает члены рантайма без делегата в карточке — контракт продукта не зависит от наличия заглушки. Потолки ядер и initial gzip опущены на выигрыш (292 000 → 290 400). Бандл пересобран, три копии синхронны. Issue: #624 User-Visible: no
107 lines
5.9 KiB
JavaScript
107 lines
5.9 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* Гейт «мёртвый код и связность монолита» (#624): `npm run lint:unused`.
|
||
*
|
||
* node scripts/unused-locals-gate.mjs # проверить против базы
|
||
* node scripts/unused-locals-gate.mjs --update # записать текущие числа в базу
|
||
* node scripts/unused-locals-gate.mjs --json # числа и разбор в JSON
|
||
*
|
||
* Две проверки, оба ответа — в одном прогоне:
|
||
*
|
||
* 1. `tsc --noUnusedLocals` по всему проекту чист, кроме приватных членов
|
||
* карточки, которых карточка не читает, но читает рантайм через порт
|
||
* (`portPrivates`) или зовут браузерные смоки (`harnessPrivates`). Любая
|
||
* другая диагностика — неиспользуемый импорт, мёртвая константа, локальная
|
||
* переменная, приватный член вне порта и харнесса — красный: это код,
|
||
* который компилятор доказал мёртвым.
|
||
*
|
||
* 2. Храповик: шесть чисел `scripts/monolith-metrics.mjs` не растут
|
||
* относительно `scripts/monolith-baseline.json` (`bundleBytes` — с полосой
|
||
* ±2 000 Б, как gzip-потолок #438; остальные — точно). Снижение — не ошибка, но
|
||
* база обязана быть опущена тем же коммитом (`--update`): незафиксированный
|
||
* выигрыш монолит отыграет обратно первой же правкой. Рост допускается
|
||
* только с явной записью в issue задачи и правкой базы в том же коммите —
|
||
* гейт печатает, какое число и на сколько.
|
||
*
|
||
* `bundleBytes` требует собранного `dist/`: гейт стоит после `npm run build`
|
||
* (в `gate:small` и в job `frontend` Validate). Без сборки число не судится,
|
||
* и гейт говорит об этом явно, а не молчит зелёным.
|
||
*/
|
||
import { writeFileSync } from 'node:fs';
|
||
import { resolve } from 'node:path';
|
||
import { isMainModule } from './spawn-portable.mjs';
|
||
import {
|
||
BASELINE_FILE, METRIC_NAMES, collectMetrics, compareWithBaseline, formatMetrics, readBaseline,
|
||
} from './monolith-metrics.mjs';
|
||
|
||
/**
|
||
* Чистое решение гейта: код выхода и строки отчёта по замеру и базе.
|
||
* `violations` — диагностики вне разрешённых; `grown` — выросшие числа.
|
||
*/
|
||
export function decide({ metrics, violations, baseline }) {
|
||
const lines = [];
|
||
let fail = false;
|
||
lines.push(`monolith: ${formatMetrics(metrics)}`);
|
||
if (violations.length) {
|
||
fail = true;
|
||
lines.push(`FAIL мёртвый код — ${violations.length} диагностик(и) noUnusedLocals вне порта и харнесса:`);
|
||
for (const v of violations.slice(0, 40)) lines.push(` ${v.file}:${v.line} TS${v.code} ${v.message}`);
|
||
if (violations.length > 40) lines.push(` … ещё ${violations.length - 40}`);
|
||
} else {
|
||
lines.push('ok мёртвого кода нет: единственные непрочитанные члены — порт и харнесс');
|
||
}
|
||
if (!baseline) {
|
||
fail = true;
|
||
lines.push(`FAIL базы ${BASELINE_FILE} нет — создать: node scripts/unused-locals-gate.mjs --update`);
|
||
return { fail, lines, grown: [], shrunk: [] };
|
||
}
|
||
const { grown, shrunk } = compareWithBaseline(metrics, baseline);
|
||
if (grown.length) {
|
||
fail = true;
|
||
for (const g of grown) {
|
||
lines.push(`FAIL связность выросла: ${g.name} ${g.base ?? 'нет в базе'} → ${g.now}`
|
||
+ ' — вернуть или обосновать в issue и поднять базу тем же коммитом');
|
||
}
|
||
}
|
||
if (shrunk.length) {
|
||
// Снижение без записи в базу — тоже отказ: храповик работает в обе стороны.
|
||
fail = true;
|
||
for (const s of shrunk) lines.push(`FAIL связность упала, база не опущена: ${s.name} ${s.base} → ${s.now} — node scripts/unused-locals-gate.mjs --update`);
|
||
}
|
||
if (!grown.length && !shrunk.length) lines.push('ok все числа равны базе');
|
||
if (metrics.bundleBytes == null) {
|
||
fail = true;
|
||
lines.push('FAIL dist/ не собран — bundleBytes не судится; сначала npm run build');
|
||
}
|
||
return { fail, lines, grown, shrunk };
|
||
}
|
||
|
||
export function baselineFrom(metrics) {
|
||
const out = {};
|
||
for (const name of METRIC_NAMES) out[name] = metrics[name];
|
||
return out;
|
||
}
|
||
|
||
if (isMainModule(import.meta.url)) {
|
||
const root = process.cwd();
|
||
const update = process.argv.includes('--update');
|
||
const json = process.argv.includes('--json');
|
||
const result = collectMetrics(root);
|
||
if (update) {
|
||
if (result.metrics.bundleBytes == null) {
|
||
console.error('dist/ не собран — база без bundleBytes была бы ложью; сначала npm run build');
|
||
process.exit(1);
|
||
}
|
||
writeFileSync(resolve(root, BASELINE_FILE), `${JSON.stringify(baselineFrom(result.metrics), null, 2)}\n`);
|
||
console.log(`${BASELINE_FILE} записан: ${formatMetrics(result.metrics)}`);
|
||
}
|
||
const baseline = readBaseline(root);
|
||
const decision = decide({ metrics: result.metrics, violations: result.violations, baseline });
|
||
if (json) {
|
||
console.log(JSON.stringify({ metrics: result.metrics, allowed: result.allowed, violations: result.violations, ...decision }, null, 2));
|
||
} else {
|
||
for (const line of decision.lines) console.log(line);
|
||
}
|
||
process.exit(decision.fail && !update ? 1 : 0);
|
||
}
|