mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-30 11:49:16 +00:00
Two-sided ratchets with zero slack made parallel tasks conflict on shared numbers, recompute them after every rebase and hit a ceiling because a neighbour merged first (#689 after #691). - Core lines (test/core-file-budget.test.mjs): a branch may grow up to CORE_BAND = 50 lines over the beta ceiling; shrinking no longer fails it. - Bundle graphs (bundle-budget.mjs): initial View and lazy graphs fail only above ceiling + 2 000 B; below the ceiling is not a branch finding. The absolute INITIAL_VIEW_GZIP_BUDGET stays the wall. - Monolith numbers (monolith-metrics.mjs, unused-locals-gate.mjs): METRIC_BANDS — 5 for delegates, port members and privates, 25 for host. refs, 2 000 B for dist/; a lower number is reported, not failed. - Browser mutation guards: 200 is a guideline — mutation-gate --check warns above it instead of failing; every guard still needs its reason line. - scripts/ratchets.mjs: `report [--warn]` and `tighten` — on the beta candidate the release manager sets every ceiling to the fact in one commit; release:prerelease prints loose ceilings as a warning. Canon: PROCESS.md §3 (browser guards, monolith numbers) and §8 «Храповики»; docs/TESTING.md. Issue: #699 User-Visible: no Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018qZfe7YS4rqEMKoVeS3GKd
108 lines
6.0 KiB
JavaScript
108 lines
6.0 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` больше своей полосы
|
|
* (`METRIC_BANDS`, #699). Снижение задачу не красит: базу до факта опускает
|
|
* бета (`node scripts/ratchets.mjs tighten`), иначе выигрыш монолит отыграет
|
|
* обратно. Рост сверх полосы допускается только с явной записью в 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, METRIC_BANDS,
|
|
} 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}`
|
|
+ ` — больше полосы ${METRIC_BANDS[g.name] ?? 0}; вернуть или обосновать в issue и поднять базу тем же коммитом`);
|
|
}
|
|
}
|
|
if (shrunk.length) {
|
|
// #699: снижение задачу не красит — базу до факта опускает бета
|
|
// (`node scripts/ratchets.mjs tighten`). Храповик по-прежнему двусторонний,
|
|
// но вторая сторона живёт на бете, а не в каждой ветке.
|
|
for (const s of shrunk) lines.push(`info связность ниже базы: ${s.name} ${s.base} → ${s.now} — базу опустит бета`);
|
|
}
|
|
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);
|
|
}
|