pregate: rename run-scratch module (strays rule) and map starter.ts to its test

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-26 15:07:24 +00:00
1 parent 0232f78e91
commit e9f9c217d9
9 files changed
+20 -9

No files matched your search

+1 -1
View File
@@ -8,7 +8,7 @@
- **No policy → warn plus a starter, never a hard `PREGATE_NO_LANE`** (`lib/pregate/starter.ts`, `lib/pregate/checks/lanes.ts`). When the base branch carries no `.gstack/pregate.json` and no user-owned policy exists, a touched file with no lane is a `warn` row under the new code `PREGATE_NO_POLICY`; the selected tests still run (a red lane is still `fail`, an unknown platform matrix is still a `requires-remote` obligation), and the table prints a starter policy drafted from the repository: the detected runner, the test directories, the CI lanes (naming a planner-computed matrix that needs `selection`), the JSON between `PREGATE_STARTER: begin` and `end` with a `dependencies` entry per unmapped touched file mapped to the tests nearest it or sharing its stem (never `[]`), and a `TODO` line per file nothing covers. `run` writes nothing; `gstack-pregate init` is the explicit write. Once any policy exists (base copy, user-owned file, or `--policy-from head`), an unmapped file is `PREGATE_NO_LANE` as before.
- **`allowlist` in `.gstack/pregate.json`** (`lib/pregate/allowlist.ts`, `lib/pregate/config.ts`, `lib/pregate/checks/secrets.ts`): `[{ finding, paths?, pattern?, reason }]`, where `finding` is a gstack-redact finding id or one of the categories `pii`, `internal`, `hygiene`, `legal`; `paths` are contained globs; `pattern` is a regex matched at the finding's position in the added line and ending at a token boundary; one scope is required and `reason` always is (64 entries at most). The `secrets` row says `N allowlisted (<ids>)`, lists each allowlisted finding with its entry and reason, and records the entries' digest as `inputs.allowlist`. **`env.value` (a credential-named environment variable's real value in the added lines, #3449) and the whole `secret` category can never be allowlisted:** validation refuses the entry (`PREGATE_CONFIG_INVALID` at `allowlist[i].finding`) and the matcher never consults the list for `env.value`, so a stale or hand-edited policy cannot silence it. An unparsed scanner report keeps its exit-code verdict.
- **Environment failures reported apart from diff failures** (`lib/pregate/environment.ts`; new code `PREGATE_ENVIRONMENT` with the troubleshooting anchor `pregate-environment`). A runner or guard binary missing from PATH (exit 127, `command not found`, a spawn `ENOENT`, a tool `not on PATH`), a network error or a quota refusal before any test was counted, a full disk, an unverified runtime or gstack's own scanner not finishing is `incomplete` with `cause: "environment"` and `environment: { kind, evidence }` in `pregate.json`, an `environment: <kind> — <evidence>` table line and a fix that says the diff is not at fault; `PREGATE_LANE_FAILED`, `PREGATE_GUARD_FAILED` and `PREGATE_SECRETS` carry `cause: "diff"`. The classifier is conservative: a local lane that exited non-zero is read from its output only when its runner reported no test summary; a counted failure is the diff's whatever its log says; a remote gate is environment only when it printed no `PREGATE_REMOTE:` line; stalls and timeouts keep their codes. `gstack-pregate verify` prints `PREGATE_ENVIRONMENT: <n> check(s) blocked by the environment (<kinds>), not by the diff` when every incomplete check is environment-caused and names the kinds beside `PREGATE_INCOMPLETE` otherwise, so a lane waives by cause, never by test name.
- **One scratch directory per run under the state root** (`lib/pregate/scratch.ts`): `<state root>/projects/<slug>/pregate/runs/<run id>/` (0700; `--run <id>` names it, else `<UTC stamp>-<pid>-<4 hex>`) holds the secrets scan's added-line copies (deleted after the scan), `lanes/lanes.log`, `guards/<id>.log` and the remote gate's log and file list, never `os.tmpdir()`. `pregate.json` records it as `scratch`, the table prints `scratch: <dir>`, `--explain` names it; runs older than 7 days under the same project are pruned when a new run starts; an unwritable state root falls back to a private per-run directory under `os.tmpdir()` and says so.
- **One scratch directory per run under the state root** (`lib/pregate/run-scratch.ts`): `<state root>/projects/<slug>/pregate/runs/<run id>/` (0700; `--run <id>` names it, else `<UTC stamp>-<pid>-<4 hex>`) holds the secrets scan's added-line copies (deleted after the scan), `lanes/lanes.log`, `guards/<id>.log` and the remote gate's log and file list, never `os.tmpdir()`. `pregate.json` records it as `scratch`, the table prints `scratch: <dir>`, `--explain` names it; runs older than 7 days under the same project are pruned when a new run starts; an unwritable state root falls back to a private per-run directory under `os.tmpdir()` and says so.
### Changed
- `docs/pregate.md`: the `allowlist` row, the "No policy yet" paragraph, and three sections (allowlist, environment failures, scratch). `docs/troubleshooting.md`: `PREGATE_NO_POLICY` and `PREGATE_ENVIRONMENT` anchors; `PREGATE_NO_LANE` and `PREGATE_INCOMPLETE` point at them. `lib/headless-artifacts.ts`: the pregate schema accepts `cause`, `environment` and `scratch` (additive). `gstack-pregate --help` documents `--run`.
+1 -1
View File
@@ -19,7 +19,7 @@ import type { Guard } from '../config';
import { baseInputs, sha256 } from '../context';
import { classifyEnvironment, environmentLine, environmentResult } from '../environment';
import { launchRuntime } from '../runtime';
import { checkScratch } from '../scratch';
import { checkScratch } from '../run-scratch';
import { readRange, supervise } from '../supervise';
import type { EnvironmentCause } from '../types';
import type { CheckDef, CheckResult, PregateContext } from '../types';
+1 -1
View File
@@ -19,7 +19,7 @@ import * as path from 'node:path';
import { baseInputs, sha256 } from '../context';
import { PREGATE_CONFIG_PATH } from '../config';
import { classifyEnvironment, environmentLine, environmentResult } from '../environment';
import { checkScratch } from '../scratch';
import { checkScratch } from '../run-scratch';
import { starterPolicy } from '../starter';
import { isDeviceDart, listRepoFiles, readRepoFile } from '../import-graph';
import { selectLanes, type Selection } from '../lane-selection';
+1 -1
View File
@@ -11,7 +11,7 @@ import { runScriptSync } from '../../script-invocation';
import { allowlistDigest, applyAllowlist, describeAllowlisted, type AllowlistFinding } from '../allowlist';
import { baseInputs, sha256 } from '../context';
import { environmentResult } from '../environment';
import { checkScratch } from '../scratch';
import { checkScratch } from '../run-scratch';
import type { CheckDef, CheckResult, PregateContext } from '../types';
const BIN_DIR = path.resolve(path.dirname(Bun.fileURLToPath(import.meta.url)), '..', '..', '..', 'bin');
+2 -2
View File
@@ -21,7 +21,7 @@ import { policyUnavailableLine, resolveBaseBranch } from '../ship-policy';
import { PREGATE_CONFIG_PATH, isConfigUnavailable, loadConfigFromBase, loadConfigFromHead, loadConfigFromUser, type ConfigError, type ConfigLoad, type PregateConfig } from './config';
import type { CheckResult, PregateContext, TouchedFile } from './types';
import { discoverLanes } from './workflows';
import { createScratch, scratchRunId } from './scratch';
import { createScratch, scratchRunId } from './run-scratch';
import type { RegistryError } from '../regen';
export interface BuildContextOptions {
@@ -179,7 +179,7 @@ export function defaultUserConfigPath(root: string): string {
return path.join(resolveStateRoot(), 'projects', slugFromEnvironment(undefined, root), 'pregate.json');
}
/** `<state root>/projects/<slug>/pregate/runs`: where every run's scratch directory lives (lib/pregate/scratch.ts), from the caller's environment. */
/** `<state root>/projects/<slug>/pregate/runs`: where every run's scratch directory lives (lib/pregate/run-scratch.ts), from the caller's environment. */
export function defaultScratchRoot(root: string, env: NodeJS.ProcessEnv = process.env): string {
const stateRoot = resolveStateRoot(env);
return path.join(stateRoot, 'projects', slugFromEnvironment(stateRoot, root), 'pregate', 'runs');
@@ -1,5 +1,5 @@
/**
* pregate/scratch — one scratch directory per run under the state root
* pregate/run-scratch — one scratch directory per run under the state root
* (Lane D, GSTA-112): `<state root>/projects/<slug>/pregate/runs/<run id>/`.
* Every check keeps its working files there (the secrets scan's added-line
* copies, the lane and guard logs, the remote gate's file list) instead of
+1 -1
View File
@@ -35,7 +35,7 @@ export interface PregateContext {
configLabel: string;
/** Where the policy came from: the base branch, the unreviewed head, the user-owned file, or nowhere (defaults). */
configKind: 'base' | 'head' | 'user' | 'none';
/** This run's scratch directory under the state root (lib/pregate/scratch.ts): logs and scan files, never os.tmpdir(). */
/** This run's scratch directory under the state root (lib/pregate/run-scratch.ts): logs and scan files, never os.tmpdir(). */
scratch: string;
/** The policy file to edit: the user-owned path when it is the active policy, else `.gstack/pregate.json`. */
configFile: string;
+1 -1
View File
@@ -11,7 +11,7 @@ import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { EXIT } from '../lib/headless-artifacts';
import { pruneScratch, scratchRunId } from '../lib/pregate/scratch';
import { pruneScratch, scratchRunId } from '../lib/pregate/run-scratch';
import { BASE_PREGATE, cleanup, commit, makeFixtureRepo, runBin, type FixtureRepo } from './helpers/pregate-fixture';
const repos: FixtureRepo[] = [];
+11
View File
@@ -13,6 +13,7 @@ import * as path from 'node:path';
import { EXIT } from '../lib/headless-artifacts';
import { validateConfig } from '../lib/pregate/config';
import { CHECK_IDS } from '../lib/pregate/index';
import { nearestTestsFor } from '../lib/pregate/starter';
import { BASE_PREGATE, cleanup, commit, makeFixtureRepo, runBin, type FixtureRepo } from './helpers/pregate-fixture';
const repos: FixtureRepo[] = [];
@@ -119,3 +120,13 @@ describe('no policy anywhere: warn plus a printed starter (never PREGATE_NO_LANE
expect(BASE_PREGATE.runner).toBe('node --test {files}');
});
});
describe('nearestTestsFor: the nearest directory with tests, else the tests sharing the stem', () => {
const tests = ['lib/render/a.test.ts', 'lib/render/b.test.ts', 'test/report.test.js', 'test/report-golden.test.js', 'test/reporter.test.js'];
test('a file under a directory with tests takes those tests; one with none takes its stem; a stem with no match is empty', () => {
expect(nearestTestsFor(tests, 'lib/render/draw.ts')).toEqual(['lib/render/a.test.ts', 'lib/render/b.test.ts']);
expect(nearestTestsFor(tests, 'src/report.js')).toEqual(['test/report.test.js', 'test/report-golden.test.js']);
expect(nearestTestsFor(tests, 'src/orphan.js')).toEqual([]);
expect(nearestTestsFor(tests, 'README')).toEqual([]);
});
});