Stop the pre-push check flagging real files whose names start with tmp or scratch (GST-1944) (#4177)

* Stop pregate flagging real files whose names start with tmp or scratch (GST-1944)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Make the stray-name test safe on case-insensitive disks and record the tmp-word tradeoff

Review-Finding: p1/specialist-batched#1 reader=testing
Review-Finding: p1/red-team#2 reader=red-team
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Describe the new scratch-name rule in the PREGATE_STRAYS troubleshooting entry

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-11-08 21:10:22 +00:00
1 parent 199931029e
commit b6155921a5
5 files changed
+37 -7

No files matched your search

@@ -0,0 +1,4 @@
---
important: blocker
---
- **pregate's `strays` check no longer flags real files whose names only start with `tmp` or `scratch`.** It treated `tmp` and `scratch` as name prefixes, so a real module such as `lib/server/disk-space/tmp-watch.js` failed with `PREGATE_STRAYS`, and `tmpfs-mount.js` or `scratchpad.ts` would have too. A branch can't allowlist its own strays, so nothing on the branch could clear it. `tmp` and `scratch` now count only as the whole name stem, optionally numbered: `tmp`, `tmp.js`, `tmp2.js`, `tmp_1.txt`, `TMP.js`, `scratch.md` and `scratch2.ts` are still flagged, and so is every `zz-*` name and editor leftover. The tradeoff: a stem followed by a word, such as `tmp-repro.ts` or `scratch_notes.md`, is no longer caught either, so name a scratch file `zz-*` to keep it flagged.
+3 -1
View File
@@ -220,7 +220,9 @@ Two stages with separate clocks, both printed:
file is read whole from disk. A file over the size limit, or whose full diff
does not parse, is scanned on its added lines alone, without that context.
- `strays`: a new file at the repository root, a new top-level directory, or a
scratch name (`zz-*`, `tmp*`, `scratch*`) or editor leftover (`*.orig`,
scratch name (`zz-*`, or `tmp` or `scratch` as the whole name stem, optionally
numbered: `tmp.js`, `tmp2.js`, `tmp_1.txt`, `scratch.md`, but not
`tmp-watch.js`, `tmpfs-mount.js`, `scratchpad.ts` or `tmp-repro.ts`) or editor leftover (`*.orig`,
`*.rej`, `*.swp`, `*.swo`, `*.bak`, `*.tmp`, `*~`, `.DS_Store`, `Thumbs.db`)
anywhere.
- `literals`: for every changed exported constant or named timeout/retry
+3 -1
View File
@@ -4241,7 +4241,9 @@ anchored pattern and a reason ([docs/pregate.md](pregate.md#allowlist));
### `PREGATE_STRAYS`
**Meaning.** A file was added at the repository root, in a new top-level
directory, or under a scratch name (`zz-*`, `tmp*`, `scratch*`) or an editor
directory, or under a scratch name (`zz-*`, or `tmp` or `scratch` as the whole
name stem, optionally numbered: `tmp.js`, `tmp2.js`, `tmp_1.txt`, `scratch.md`,
but not `tmp-watch.js`, `tmpfs-mount.js`, `scratchpad.ts` or `tmp-repro.ts`) or an editor
leftover name (`*.orig`, `*.rej`, `*.swp`, `*.swo`, `*.bak`, `*.tmp`, `*~`,
`.DS_Store`, `Thumbs.db`).
+4 -3
View File
@@ -1,8 +1,9 @@
/**
* pregate check `strays` (tier 1, preflight): files the branch added at the
* repository root, in a top-level directory the base branch does not have,
* or anywhere under a scratch name (`zz-*`, `tmp*`, `scratch*`, editor
* leftovers). Incident p0-4-scratch-script: a `zz-probe.sh` shipped in a
* or anywhere under a scratch name (`zz-*`; `tmp` or `scratch` as the whole
* stem, optionally numbered, so `tmp2.js` and `scratch_1.md` but not
* `tmp-watch.js`; editor leftovers). Incident p0-4-scratch-script: a `zz-probe.sh` shipped in a
* release PR because nothing looked at the file list.
*/
import { spawnSync } from 'node:child_process';
@@ -12,7 +13,7 @@ import type { CheckDef, CheckResult, PregateContext } from '../types';
/** Conventional root files a repo adds without a declaration. */
export const ROOT_OK = /^(?:README|LICENSE|LICENCE|NOTICE|CHANGELOG|CONTRIBUTING|CODE_OF_CONDUCT|SECURITY|AUTHORS|VERSION|Makefile|Dockerfile|Procfile|Gemfile|Rakefile|Cargo\.(?:toml|lock)|go\.(?:mod|sum)|package(?:-lock)?\.json|bun\.lockb?|pnpm-lock\.yaml|yarn\.lock|tsconfig[\w.-]*\.json|bunfig\.toml|pyproject\.toml|pubspec\.(?:yaml|lock)|analysis_options\.yaml|dart_test\.yaml|requirements[\w.-]*\.txt|setup\.(?:py|cfg)|\.[\w.-]+|[\w.-]+\.config\.[cm]?[jt]s|[\w.-]+\.md)$/i;
export const STRAY_NAME = /^(?:zz-|tmp|scratch)|\.(?:orig|rej|swp|swo|bak|tmp)$|~$|^\.DS_Store$|^Thumbs\.db$/i;
export const STRAY_NAME = /^(?:zz-|(?:tmp|scratch)\d*(?:[-_]\d+)?(?:\.|$))|\.(?:orig|rej|swp|swo|bak|tmp)$|~$|^\.DS_Store$|^Thumbs\.db$/i;
function run(ctx: PregateContext): CheckResult {
const inputs = baseInputs(ctx);
+23 -2
View File
@@ -20,6 +20,7 @@ import { DEFAULT_CONFIG, validateConfig } from '../lib/pregate/config';
import { isDocsEmptyLane } from '../lib/pregate/docs-lane';
import { buildGraph, testsFor } from '../lib/pregate/import-graph';
import { CHECK_IDS } from '../lib/pregate/index';
import { STRAY_NAME } from '../lib/pregate/checks/strays';
import type { PregateContext } from '../lib/pregate/types';
import { parseWorkflow } from '../lib/pregate/workflows';
import { BASE_PREGATE, cleanup, commit, makeFixtureRepo, runBin, write, type FixtureRepo } from './helpers/pregate-fixture';
@@ -536,15 +537,35 @@ describe('gstack-pregate (bin)', () => {
const ok = pregate(repo, ['--stage', 'preflight', '--only', 'strays']);
expect(ok.status).toBe(EXIT.ok);
expect(ok.out).toMatch(/^preflight {2}strays {4}pass /m);
commit(repo, { 'docs/zz-notes.md': '# n\n', 'lib/tmp-x.ts': 'export {};\n', 'test/scratch.py': 'pass\n', 'lib/util.js.orig': 'x\n' });
commit(repo, { 'docs/zz-notes.md': '# n\n', 'lib/tmp.ts': 'export {};\n', 'test/scratch.py': 'pass\n', 'lib/util.js.orig': 'x\n' });
const r = pregate(repo, ['--stage', 'preflight', '--only', 'strays']);
expect(r.status).toBe(EXIT.fail);
expect(r.out).toMatch(/^preflight {2}strays {4}fail .*4 stray file\(s\)/m);
for (const f of ['docs/zz-notes.md', 'lib/tmp-x.ts', 'test/scratch.py', 'lib/util.js.orig']) expect(r.out).toContain(`strays: ${f}: scratch name (${f.split('/').pop()})`);
for (const f of ['docs/zz-notes.md', 'lib/tmp.ts', 'test/scratch.py', 'lib/util.js.orig']) expect(r.out).toContain(`strays: ${f}: scratch name (${f.split('/').pop()})`);
expect(r.out).not.toContain('probe.py: scratch name');
expect(r.out).not.toContain('temporal-graph-shipped.test.ts: scratch name');
});
// Value: protects=real modules whose names only start with tmp/scratch (tmp-watch.js, tmpfs-mount.js, scratchpad.ts) pass strays; fails_when=STRAY_NAME treats tmp or scratch as a bare prefix again
// why_new=GST-1211 covers zz-*, probe.py and temporal-*, never a tmp- or scratch-prefixed real module (GST-1944, ALPH-74); seam=none
test('GST-1944: tmp and scratch match as a whole stem only; tmp-watch.js, tmpfs-mount.js and scratchpad.ts are not scratch names', () => {
for (const name of ['tmp-watch.js', 'tmpfs-mount.js', 'tmpl.ts', 'scratchpad.ts', 'scratch-card.tsx', 'tmp_cleanup.py', 'tmp-repro.ts', 'scratch-probe.sh']) expect([name, STRAY_NAME.test(name)]).toEqual([name, false]);
for (const name of ['tmp', 'tmp.js', 'tmp2.js', 'tmp_1.txt', 'tmp-1.js', 'TMP.js', 'Tmp.md', 'scratch', 'scratch.md', 'scratch2.ts', 'scratch_3.py', 'SCRATCH.txt', 'zz-anything', 'zz-watch.js', 'notes.tmp', 'util.js.orig']) expect([name, STRAY_NAME.test(name)]).toEqual([name, true]);
const repo = fixture({ pregate: { ...BASE_PREGATE, warn: [] } });
commit(repo, { 'lib/server/disk-space/tmp-watch.js': 'module.exports = 1;\n', 'lib/server/disk-space/tmpfs-mount.js': 'module.exports = 2;\n', 'lib/scratchpad.js': 'module.exports = 3;\n' });
const ok = pregate(repo, ['--stage', 'preflight', '--only', 'strays']);
expect(ok.status).toBe(EXIT.ok);
expect(ok.out).toMatch(/^preflight {2}strays {4}pass /m);
commit(repo, { 'lib/tmp.js': 'x\n', 'lib/tmp2.js': 'x\n', 'lib/tmp_1.txt': 'x\n', 'docs/TMP.js': 'x\n', 'docs/scratch.md': 'x\n', 'lib/scratch2.ts': 'x\n', 'lib/zz-anything': 'x\n', 'lib/tmp': 'x\n' });
const r = pregate(repo, ['--stage', 'preflight', '--only', 'strays']);
expect(r.status).toBe(EXIT.fail);
expect(r.out).toMatch(/^preflight {2}strays {4}fail .*8 stray file\(s\)/m);
for (const f of ['lib/tmp.js', 'lib/tmp2.js', 'lib/tmp_1.txt', 'docs/TMP.js', 'docs/scratch.md', 'lib/scratch2.ts', 'lib/zz-anything', 'lib/tmp']) expect(r.out).toContain(`strays: ${f}: scratch name (${f.split('/').pop()})`);
expect(r.out).not.toContain('tmp-watch.js: scratch name');
expect(r.out).not.toContain('tmpfs-mount.js: scratch name');
expect(r.out).not.toContain('scratchpad.js: scratch name');
});
test('p0-4-heavy-old-constant: a nightly-only test still asserting the old literal fails literals with file:line', () => {
const repo = fixture();
commit(repo, { 'lib/retry.js': "exports.MAX_RETRIES = 3;\nexports.TIMEOUT_MS = 9000;\nexports.retry = (fn) => fn();\n" });