diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e83f763b..9adff6cc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 ()`, 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: — ` 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: check(s) blocked by the environment (), 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`): `/projects//pregate/runs//` (0700; `--run ` names it, else `--<4 hex>`) holds the secrets scan's added-line copies (deleted after the scan), `lanes/lanes.log`, `guards/.log` and the remote gate's log and file list, never `os.tmpdir()`. `pregate.json` records it as `scratch`, the table prints `scratch: `, `--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`): `/projects//pregate/runs//` (0700; `--run ` names it, else `--<4 hex>`) holds the secrets scan's added-line copies (deleted after the scan), `lanes/lanes.log`, `guards/.log` and the remote gate's log and file list, never `os.tmpdir()`. `pregate.json` records it as `scratch`, the table prints `scratch: `, `--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`. diff --git a/lib/pregate/checks/guards.ts b/lib/pregate/checks/guards.ts index c5690917e..4ab35054e 100644 --- a/lib/pregate/checks/guards.ts +++ b/lib/pregate/checks/guards.ts @@ -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'; diff --git a/lib/pregate/checks/lanes.ts b/lib/pregate/checks/lanes.ts index 303128462..80f406935 100644 --- a/lib/pregate/checks/lanes.ts +++ b/lib/pregate/checks/lanes.ts @@ -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'; diff --git a/lib/pregate/checks/secrets.ts b/lib/pregate/checks/secrets.ts index 19dd94664..5083002b7 100644 --- a/lib/pregate/checks/secrets.ts +++ b/lib/pregate/checks/secrets.ts @@ -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'); diff --git a/lib/pregate/context.ts b/lib/pregate/context.ts index b73e30a17..02829b208 100644 --- a/lib/pregate/context.ts +++ b/lib/pregate/context.ts @@ -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'); } -/** `/projects//pregate/runs`: where every run's scratch directory lives (lib/pregate/scratch.ts), from the caller's environment. */ +/** `/projects//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'); diff --git a/lib/pregate/scratch.ts b/lib/pregate/run-scratch.ts similarity index 97% rename from lib/pregate/scratch.ts rename to lib/pregate/run-scratch.ts index 789b5512d..1532ea186 100644 --- a/lib/pregate/scratch.ts +++ b/lib/pregate/run-scratch.ts @@ -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): `/projects//pregate/runs//`. * 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 diff --git a/lib/pregate/types.ts b/lib/pregate/types.ts index ef483d8f5..4489bf9ab 100644 --- a/lib/pregate/types.ts +++ b/lib/pregate/types.ts @@ -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; diff --git a/test/pregate-scratch.test.ts b/test/pregate-scratch.test.ts index 39c3f2634..9d47b568e 100644 --- a/test/pregate-scratch.test.ts +++ b/test/pregate-scratch.test.ts @@ -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[] = []; diff --git a/test/pregate-starter.test.ts b/test/pregate-starter.test.ts index 4bd5aef82..a6dad6a11 100644 --- a/test/pregate-starter.test.ts +++ b/test/pregate-starter.test.ts @@ -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([]); + }); +});