mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-01 22:45:02 +01:00
* pregate: starter policy, benign-finding allowlist, environment failures, per-run scratch
A repository with no .gstack/pregate.json now gets a warn (PREGATE_NO_POLICY)
with a starter policy printed, instead of PREGATE_NO_LANE on every touched
file; gstack-pregate init remains the explicit write. The policy gains an
allowlist for benign secrets/pii findings (env.value and secret never
allowlistable). Checks that cannot run because of the machine report
PREGATE_ENVIRONMENT (incomplete, with kind and evidence) apart from diff
failures. Every run writes its scratch under the state root, per run id.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* 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>
* pregate literals: match a module as a path fragment, require the constant name for a one-character value
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: pre-landing review fixes
guards: an exited guard is the machine's only on exit 127, never from its output;
secrets: clear an interrupted run's added-line copies before scanning;
environment: one fix table per kind, one ANSI strip, hoisted not-found vocabulary;
starter returns its lines; allowlistDigest reuses sha256; stale headers and the
PREGATE_INCOMPLETE fix clause updated; tests for the mixed verify branch, disk
precedence, allowlist boundaries, a positionless finding, head-policy strictness,
run-id edges and the unwritable state root.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* pregate: count a module's full file name without a directory as a consumer
consumersOf anchored both the stem and the full basename on a path
separator, so run('install.sh') or bash install.sh without a directory
stopped counting for a multi-character old value. Only the bare stem
needs the separator; the full file name is specific on its own.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* pregate: a diff cannot clear itself as an environment failure; a taken --run id gets its own directory
Adversarial review of the environment classifier and the scratch layout:
- exit 127 and 'command not found' are missing-binary only when the
command's own binary is off PATH (runner, guard and remote gate alike);
a script the diff controls that exits 127 is the diff's failure
- a local lane with no test summary is classified from its output only
when the runner was not seen starting a test file; a remote gate only
from its first 40 lines, before any test output could stream
- environment.proof records launch (certain) or output (inferred); an
output-inferred cause never says 'the diff is not at fault', its line
says '(inferred from output)', and verify names such checks
- a runner killed by a signal is PREGATE_INCOMPLETE, not missing-binary
- a --run id whose directory exists takes <id>-2 .. -9 instead of
sharing (and truncating) a live run's logs
- engine.* findings are never allowlistable; a pattern entry the scanner
gave no position for is reported as not tested instead of silently inert
- docs: synopsis --run, no-policy lanes wording, verify's code list,
guard exit 127 carve-out, PREGATE_SECRETS allowlist pointer, scratch
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* docs(pregate): PREGATE_INCOMPLETE names a signal-killed runner; CHANGELOG lists every doc edit
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>
102 lines
5.7 KiB
TypeScript
102 lines
5.7 KiB
TypeScript
/**
|
|
* pregate/init-draft — `gstack-pregate init` writes a policy drafted from the
|
|
* repo (#3268) instead of a fixed example: the languages it holds, what the
|
|
* lanes check would run, and a `dependencies` entry for each file the import
|
|
* graph maps to no test.
|
|
*
|
|
* A drafted entry only ever adds tests: it maps an unmapped file to the tests
|
|
* of the nearest directory above it that has any. An empty list (`[]`) would
|
|
* clear the file without a test, so the draft never writes one; a file with no
|
|
* nearby test, or past the schema's entry limit, is printed as a TODO for the
|
|
* owner to decide. Documentation,
|
|
* assets and tool config already clear without a policy (docs-lane.ts), and
|
|
* Dart packages run per package root, so `runner` stays null unless no
|
|
* runner can be detected.
|
|
*/
|
|
import * as path from 'node:path';
|
|
import { detectRunner } from './checks/lanes';
|
|
import { DEFAULT_CONFIG, MAX_LIST, PREGATE_CONFIG_PATH, PREGATE_CONFIG_SCHEMA } from './config';
|
|
import { isDocsEmptyLane, isInertNoTests } from './docs-lane';
|
|
import { buildGraph, listRepoFiles, testsFor } from './import-graph';
|
|
import { GENERATED_PATH, isRegistryUnavailable, loadRegistryFromHead } from '../regen';
|
|
import { matchesAny } from '../ship-policy';
|
|
import { discoverLanes } from './workflows';
|
|
|
|
const LANGUAGES: Array<[string, RegExp]> = [
|
|
['typescript', /\.[cm]?tsx?$/], ['javascript', /\.[cm]?jsx?$/], ['python', /\.py$/], ['dart', /\.dart$/],
|
|
['go', /\.go$/], ['ruby', /\.rb$/], ['rust', /\.rs$/], ['shell', /\.(?:sh|bash|bats)$/],
|
|
];
|
|
/** Above this many tests a directory's tests are no longer a useful suggestion. */
|
|
export const MAX_SUGGESTED = 25;
|
|
const MAX_TODO_LINES = 50;
|
|
|
|
export interface InitDraft {
|
|
json: string;
|
|
languages: Array<{ language: string; files: number }>;
|
|
runnerNote: string;
|
|
drafted: number;
|
|
todo: string[];
|
|
selectionTodo: string[];
|
|
}
|
|
|
|
/** The tests under the nearest ancestor directory of `file` (not the repo root) that has 1..MAX_SUGGESTED tests. */
|
|
export function nearestTests(tests: readonly string[], file: string): string[] {
|
|
for (let dir = path.posix.dirname(file); dir !== '.' && dir !== ''; dir = path.posix.dirname(dir)) {
|
|
const under = tests.filter(t => t.startsWith(`${dir}/`));
|
|
if (under.length > MAX_SUGGESTED) return [];
|
|
if (under.length) return under;
|
|
}
|
|
return [];
|
|
}
|
|
|
|
export function draftConfig(repoRoot: string): InitDraft {
|
|
const files = listRepoFiles(repoRoot);
|
|
const graph = buildGraph(repoRoot);
|
|
const tests = [...graph.tests].sort();
|
|
const languages = LANGUAGES.map(([language, re]) => ({ language, files: files.filter(f => re.test(f)).length })).filter(l => l.files > 0).sort((a, b) => b.files - a.files);
|
|
|
|
const dependencies: Record<string, string[]> = {};
|
|
const todo: string[] = [];
|
|
let drafted = 0;
|
|
const registry = loadRegistryFromHead(repoRoot);
|
|
const generated = isRegistryUnavailable(registry) ? [] : registry.registry.entries.flatMap(e => [...e.inputs, ...e.outputs]);
|
|
for (const file of files) {
|
|
if (matchesAny(generated, file)) continue;
|
|
if (graph.tests.has(file) || file === PREGATE_CONFIG_PATH || file === GENERATED_PATH) continue;
|
|
if (isDocsEmptyLane(file) || isInertNoTests(file)) continue;
|
|
if (testsFor(graph, file).tests.length) continue;
|
|
const suggested = nearestTests(tests, file);
|
|
if (!suggested.length || drafted >= MAX_LIST) { todo.push(file); continue; }
|
|
dependencies[file] = suggested;
|
|
drafted++;
|
|
}
|
|
|
|
const groups = new Map<string, string[]>();
|
|
for (const t of tests) { const lang = t.endsWith('.dart') ? 'dart' : t.endsWith('.rs') ? 'rust' : t.endsWith('.py') ? 'python' : /\.[cm]?[jt]sx?$/.test(t) ? 'javascript' : 'other'; groups.set(lang, [...(groups.get(lang) ?? []), t]); }
|
|
const notes = [...groups].map(([lang, list]) => {
|
|
if (lang === 'dart') return `${list.length} Dart test file(s) run per package with flutter test or dart test`;
|
|
if (lang === 'rust') return `${list.length} Rust test file(s) run per crate with cargo test -p`;
|
|
const runner = detectRunner({ repoRoot, config: DEFAULT_CONFIG }, list);
|
|
return runner ? `${lang} tests detected at run time: ${runner}` : `no runner detected for ${list.length} ${lang} test file(s); set runner (with {files})`;
|
|
});
|
|
const mixed = [...groups.keys()].filter(l => l !== 'dart' && l !== 'rust').length > 1 ? '; a selection mixing these languages needs runner set' : '';
|
|
const runnerNote = notes.length ? notes.join('; ') + mixed : 'no test files found';
|
|
|
|
const selectionTodo = discoverLanes(repoRoot).filter(l => l.runsTests && l.matrixExpression && l.testFiles.length === 0).map(l => l.id);
|
|
const config = { $schema: PREGATE_CONFIG_SCHEMA, runner: null, dependencies, empty_lanes: [], selection: {}, warn: [], strays: { allow: [] } };
|
|
return { json: JSON.stringify(config, null, 2) + '\n', languages, runnerNote, drafted, todo, selectionTodo };
|
|
}
|
|
|
|
/** The lines `init` prints after writing the draft. */
|
|
export function describeDraft(d: InitDraft): string[] {
|
|
const lines = [
|
|
`PREGATE_INIT: languages ${d.languages.map(l => `${l.language} ${l.files}`).join(', ') || '(none recognised)'}`,
|
|
`PREGATE_INIT: runner null (${d.runnerNote})`,
|
|
`PREGATE_INIT: ${d.drafted} file(s) with no test lane mapped to the tests nearest them under dependencies; review each entry`,
|
|
];
|
|
for (const id of d.selectionTodo) lines.push(`TODO selection: ${id} computes its matrix; declare the command that lists its test files under selection`);
|
|
for (const f of d.todo.slice(0, MAX_TODO_LINES)) lines.push(`TODO ${f}: no test lane and no nearby test; add a test, map it under dependencies, or declare [] if no test can cover it`);
|
|
if (d.todo.length > MAX_TODO_LINES) lines.push(`TODO: +${d.todo.length - MAX_TODO_LINES} more file(s) with no test lane`);
|
|
return lines;
|
|
}
|