* chore(#3059): close the ESLint glob-coverage escape and guard it 62 tracked source files matched no `files:` glob in eslint.config.mjs, so ESLint skipped them entirely while `eslint .` still exited 0 — including all 26 files under hooks/, the enforcement machinery itself. Covers 56 of them (eslint-rules/, hooks/, bin/lib/, pi/, examples/, vscode/, the plugin shims, root *.mjs) and allowlists the 6 deliberate must-not-compile brand-typing fixtures with a recorded reason each. hooks/** is covered with n/no-process-exit deliberately off: a hook's whole contract is its exit code, several exits are load-bearing stdin-timeout guards where nothing else terminates the process, and ADR-0012/0174 scope the no-process-exit convention to the Command Routing Hub. bin/lib/ui-safety-gate.cjs is dual-mode, so it keeps the rule live and takes two targeted disables in its require.main===module tail instead. Adds scripts/lint-eslint-glob-coverage.cjs + a node:test drift guard so the class cannot regrow: allowlist entries require a non-empty reason, the list ratchets down only (a stale entry fails), and a tracked-count floor means a broken `git ls-files` fails rather than reporting clean. Closes #3059 * chore(#3059): apply review findings — correct the changeset count, add parser properties Isolated adversarial review, confirmed by rebuilding a byte-for-byte replica of the pre-change eslint.config.mjs: the changeset claimed 44 previously- unlinted files. The real figure is 56 (56 covered + 6 allowlisted = 62). That was user-facing CHANGELOG text and was wrong; corrected, along with three consequential figures in the design record. CLAUDE.md requires a fast-check property test for parsers and budget limits, and listTrackedSourceFiles is a parser. The standards review called this "satisfied in spirit"; it is not. Adds three properties driving the real exported parser through an injected execFile: extension totality/soundness including a trailing terminator, backslash-normalization totality, and CRLF/LF equivalence — the invariant the repo's recurring CRLF defect class breaks. Also de-duplicates the anchor rows onto one shared resolver, kept deliberately independent of the guard's own resolveFileCoverage so an anchor still fails if that resolution regresses, and records in the guard's header why the bin/install.js family is NOT allowlisted: it resolves to 2 rules under ADR-1703, so an entry would trip the allowlist_stale ratchet. * fix(#3059): make the coverage guard's git call container-safe The remote runner reported the guard degrading to `git_failed` on both Node lanes: fatal: detected dubious ownership in repository at '/work' The runner executes in a container where the repo is owned by a different UID, so git refuses to operate on it. The guard's degraded-verdict path worked exactly as designed — it reported the failure instead of throwing or falsely reporting clean — but a guard that cannot run in CI is not a gate. `git ls-files` is now invoked as `git -c safe.directory=* ls-files`. `-c` scopes the override to the single invocation and mutates no config file, and the wildcard is appropriate because this command only enumerates tracked paths in the repository it is already executing inside. Adds a regression test that captures the argv through the injected execFile seam and asserts `-c safe.directory=*` precedes `ls-files`, so the container case is pinned behaviorally rather than by reading the script's source. * chore(#3059): backfill changeset PR number pr:0 placeholder replaced with the real number now that #3277 exists. --------- Co-authored-by: sim <sim@local>
110 lines
3.9 KiB
JavaScript
110 lines
3.9 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* UI Safety Gate — shell-free implementation (#3706, #3718)
|
|
*
|
|
* Replaces the bash shell-based one-liner that silently degraded on Windows
|
|
* PowerShell / cmd.exe because the locale env-var prefix was not recognised.
|
|
* This module runs inside Node.js — no shell dependency, works identically
|
|
* on bash, Git-Bash, PowerShell, and cmd.exe.
|
|
*
|
|
* Word-boundary anchoring:
|
|
* (^|[^a-zA-Z0-9])(TOKEN)([^a-zA-Z0-9]|$)
|
|
* Equivalent to POSIX ERE [^[:alnum:]] — matches tokens only when they are not
|
|
* interior substrings of alphanumeric compound words (e.g. "microfrontend" is NOT
|
|
* matched; "micro-frontend" and "micro frontend" ARE matched).
|
|
*
|
|
* Public API:
|
|
* checkUiPresence(text: string): { hasUI: boolean, tokens: string[] }
|
|
*
|
|
* CLI usage — reads phase-section text from STDIN to avoid ARG_MAX limits:
|
|
* echo "$PHASE_SECTION" | node bin/lib/ui-safety-gate.cjs
|
|
* echo $? → 0 if UI tokens found, 1 if not, 2 on usage error
|
|
*
|
|
* Exit codes mirror grep: 0 = match found, 1 = no match, 2 = usage error.
|
|
*/
|
|
|
|
const UI_TOKENS = [
|
|
'UI',
|
|
'interface',
|
|
'frontend',
|
|
'component',
|
|
'layout',
|
|
'page',
|
|
'screen',
|
|
'view',
|
|
'form',
|
|
'dashboard',
|
|
'widget',
|
|
];
|
|
|
|
/**
|
|
* Built once at module load — no per-call compilation overhead.
|
|
* ASCII word boundaries — matches the original ASCII-grep intent of #3706.
|
|
* Note: JS [a-zA-Z0-9] is ASCII-only and NOT equivalent to POSIX [[:alnum:]],
|
|
* which is locale-sensitive and includes accented characters.
|
|
*/
|
|
const UI_GATE_PATTERN = new RegExp(
|
|
'(^|[^a-zA-Z0-9])(' + UI_TOKENS.join('|') + ')([^a-zA-Z0-9]|$)',
|
|
'i'
|
|
);
|
|
|
|
// Global-flagged variant for extracting ALL matches per line (matchAll).
|
|
const UI_GATE_PATTERN_GLOBAL = new RegExp(UI_GATE_PATTERN.source, 'gi');
|
|
|
|
/**
|
|
* Check a roadmap phase section string for frontend UI indicators.
|
|
*
|
|
* @param {string} text - The roadmap phase section content (may be multi-line, CRLF or LF).
|
|
* @returns {{ hasUI: boolean, tokens: string[] }}
|
|
* hasUI — true if any UI token was matched as a standalone word.
|
|
* tokens — matched token strings (lowercased), deduplicated.
|
|
*/
|
|
function checkUiPresence(text) {
|
|
if (typeof text !== 'string') {
|
|
return { hasUI: false, tokens: [] };
|
|
}
|
|
|
|
// Normalise CRLF so the pattern sees consistent line boundaries.
|
|
const normalised = text.replace(/\r\n/g, '\n');
|
|
|
|
const found = new Set();
|
|
for (const line of normalised.split('\n')) {
|
|
// Reset lastIndex before each line so the global pattern restarts from 0.
|
|
UI_GATE_PATTERN_GLOBAL.lastIndex = 0;
|
|
for (const m of line.matchAll(UI_GATE_PATTERN_GLOBAL)) {
|
|
found.add(m[2].toLowerCase());
|
|
}
|
|
}
|
|
|
|
return { hasUI: found.size > 0, tokens: [...found] };
|
|
}
|
|
|
|
module.exports = { checkUiPresence, UI_TOKENS };
|
|
|
|
// ── CLI entry point ─────────────────────────────────────────────────────────
|
|
// Reads phase-section text from STDIN (not argv) to avoid OS ARG_MAX limits.
|
|
// Invoked by workflow .md bash blocks as: echo "$PHASE_SECTION" | node bin/lib/ui-safety-gate.cjs
|
|
// Exit 0 = UI found, 1 = no UI, 2 = startup error.
|
|
|
|
if (require.main === module) {
|
|
// Collect stdin chunks asynchronously.
|
|
const chunks = [];
|
|
process.stdin.setEncoding('utf-8');
|
|
|
|
process.stdin.on('data', (chunk) => chunks.push(chunk));
|
|
|
|
process.stdin.on('end', () => {
|
|
const input = chunks.join('');
|
|
const result = checkUiPresence(input);
|
|
// eslint-disable-next-line n/no-process-exit -- CLI entry point (require.main===module): async stdin handler, nothing else terminates the process (#3059)
|
|
process.exit(result.hasUI ? 0 : 1);
|
|
});
|
|
|
|
process.stdin.on('error', (err) => {
|
|
process.stderr.write(`ERROR: ui-safety-gate.cjs stdin read failed: ${err.message}\n`);
|
|
// eslint-disable-next-line n/no-process-exit -- CLI entry point (require.main===module): async stdin handler, nothing else terminates the process (#3059)
|
|
process.exit(2);
|
|
});
|
|
}
|