infra: portable Windows gate, CI toolchain pins, task packet, deterministic wait, gate:small --smokes

Windows portability (the three red tests on the owner's machine at green CI):
- scripts/spawn-portable.mjs: isMainModule via pathToFileURL (the
  `file://${argv[1]}` form gives file:///C:/C:/... and the CLI stays silent);
  portableCommand — a shell only for npm/npx/.cmd, node and git run directly
  (spawn via shell dropped the quotes of `node -e "…"`). Applied to
  classify-changes, mutation-gate-report, review-doc-guard, check-inputs,
  merge-candidate, gate-small, rebase-on-dev.
- the rebase-on-dev test pins core.autocrlf=false / core.eol=lf through
  GIT_CONFIG_* for its temp repository instead of touching the user's git
  config; test/windows-portability.test.mjs forbids both anti-patterns.

Pins: scripts/toolchain-pins.mjs reads Node/Python from validate.yml, the HA
stack from tests_backend/requirements.txt, Playwright/Chromium from the
lockfile — no second dictionary; `npm run toolchain:check` compares the
machine; .nvmrc/.python-version are derived and tested equal;
scripts/wsl-setup.sh provisions WSL/Linux with those pins.

scripts/task-packet.mjs: one derived view of an issue (status, rights, owner
decisions, branch vs dev, Validate on the tip, previous verdict with recorded
tree, AC → evidence, unwitnessed). scripts/wait-verdict.mjs: polls labels,
pipeline comments and optionally Validate, prints only on state change, exit
0/3/4, writes nothing.

gate:small --smokes: after build the browser phase runs bundle-sync and then
the directly matched and registered smokes, two at a time; broad matches stay
with the reviewer. package.json changed, so the bundle is rebuilt here.

Issue: #496
User-Visible: no
This commit is contained in:
Claude
2026-09-09 01:11:24 +03:00
parent 6e95c649e6
commit f0c8ae24ba
69 changed files with 1327 additions and 365 deletions
+3 -1
View File
@@ -24,6 +24,7 @@
import { execFileSync } from 'node:child_process';
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
import { dirname, join, posix, relative, resolve } from 'node:path';
import { isMainModule } from './spawn-portable.mjs';
/** Корни, внутри которых файл считается исполняемым входом (§5.2). */
export const EXECUTABLE_ROOTS = ['scripts', 'demo', 'test', 'tests_backend', '.github', 'custom_components', 'src'];
@@ -56,6 +57,7 @@ export const NOT_AN_INPUT = [
['scripts/sh3d-convert/cli.mjs', 'CLI конвертера для человека'],
['scripts/sh3d-convert/make-fixtures.mjs', 'генератор фикстур конвертера, ручной'],
['scripts/support-relay/deploy/**', 'деплой relay на стенд'],
['scripts/wsl-setup.sh', 'установка локального Linux/WSL-контура с пинами CI (#496), ручной запуск'],
['.github/workflows/*.yml', 'другие workflow: у каждого свой запуск; validate.yml — вход toolchain всех проверок, объявлен явно'],
['.github/ISSUE_TEMPLATE/**', 'шаблоны issue GitHub, не исполняются'],
['.githooks/**', 'локальные хуки'],
@@ -378,7 +380,7 @@ export function coverage(root = process.cwd(), options = {}) {
// ---------------------------------------------------------------------------
// CLI: `node scripts/check-inputs.mjs --check=backend` печатает входы;
// `--coverage` — лист покрытия; `--affected` читает список файлов из stdin.
const invokedDirectly = process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).href;
const invokedDirectly = isMainModule(import.meta.url); // #496: переносимо для Windows
if (invokedDirectly) {
const argv = process.argv.slice(2);
const root = process.cwd();