* feat(#3907): gates report no-input instead of asserting a verdict they never reached The three stdin-reading gates bound 2 to a stdin read error only, with no arm for stdin closed at zero bytes - so empty input flowed into the detector, found nothing, and exited 1, which each module's own comment defines as a negative verdict. An unset PHASE_SECTION made the UI gate assert the phase has no UI. Empty and whitespace-only input now exit NO_INPUT, and a read error exits UNAVAILABLE rather than a locally-invented 2, both resolved through the registry and delivered by terminateNow. The exit code was only half of it: under --json the same input emitted {detected:false}, byte-identical to the fabricated payload #3909 exists to fix, and the blocking coverage gate reads that payload. Empty input now emits the in-tree {skipped:true,reason} form with no detected key at all. teams-status is excluded: it never reads stdin and has no invented 2, so the four-module framing in the issue and ADR is wrong. The dead root bin/lib/ui-safety-gate.cjs is deleted - no installer reference, no workflow invocation, and the live fallback chains are for other modules. Its removal restores the unit tests to the module that actually ships; they had been asserting the stale copy's two-field shape, which is why it drifted unnoticed. * fix(#3907): drive gate tests through the process seam, and make removed-but-needed basename-precise CONTRIBUTING requires every subprocess go through tests/helpers/process-seam.cjs; two of the three gate suites hand-rolled spawnSync while the third, added in the same change, used runNode correctly for the identical injection case. Converted the blocks this change added, leaving pre-existing ones alone. Deleting one of two files sharing a basename made lint-removed-but-needed report 14 references that were all to the surviving canonical module - the false-positive class its own docstring names. It now matches on the deleted file's full path when a surviving file shares its basename, which is more precise rather than weaker: a genuine full-path reference still fails, and behaviour is unchanged when no basename collides. It immediately caught a docstring on this branch that spelled the deleted path. * test(#3907): update the one existing assertion that pinned the old empty-stdin verdict A pre-existing test asserted exit 1 on empty stdin - the defect this phase removes - and was missed because the change added new blocks without auditing existing ones pinning the old contract. Audited the rest: the other three status-1 assertions in that file all feed real input and are the genuine-negative controls that must keep returning 1, so exactly one was stale. The retired 2 is gone from the describe's contract comment too. * chore(#3907): backfill changeset pr number --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -1,109 +0,0 @@
|
||||
'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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user