Files
gstack/lib/pregate/init-draft.ts
T
5ebda5999f v1.91.161.0 fix(pregate): Django, Dart, asset and tool-config lanes; init drafts from the repo (#3266-#3269) (#3306)
* fix(pregate): Dart import graph and test lanes (#3285)

Port of arlophoenix's #3285. The import graph reads Dart import/export/part
directives and resolves package: URIs through every tracked pubspec.yaml;
_test.dart files under test/ are tests. lanes runs each Dart package's
selected tests from its own root with flutter test or dart test (through fvm
when the package pins it). flutter/dart/melos/very_good CI jobs are lanes,
and run-step test paths resolve against working-directory and literal cd.
A missing Dart SDK or a .dart file outside any package leaves the lane
incomplete with a fix line; device tests (integration_test/, test_driver/)
are not unit lanes.

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

* fix(pregate): a --base that names a commit gets a branch fix, not an impossible fetch (#3269)

gstack-pregate --base <sha> refused with "fix: git fetch origin <sha>
--depth=1". That fetch can never create origin/<sha>: a short SHA fails
with "couldn't find remote ref", and a full SHA only reaches FETCH_HEAD.
A fork clone hits the same wall with --base upstream/main.

When origin/<base> does not resolve but <base> names a commit here and
no local branch (a SHA, a tag, another remote's ref), the refusal now
says --base takes a branch name on origin and suggests origin/HEAD's
branch. A local branch, or a name that resolves nowhere, keeps the
fetch fix: that is the unfetched or shallow checkout it repairs. Same
code, same exit 3, and pin files are still read only from the reviewed
origin/<base>.

Fixes #3269

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(pregate): Django, asset and tool-config lanes; init drafts from the repo (#3266, #3267, #3268)

- Django: settings modules, settings strings, include(), app migrations,
  admin, templates, static files and project templates get framework edges
  (lib/pregate/django.ts).
- Assets, stylesheets and dotfile tool config take a no-tests empty lane;
  licence text under any name is documentation.
- gstack-pregate init drafts languages, runner notes and nearest-test
  dependencies; never writes [].
- Review fixes on the #3285 port: untracked shell scopes leave a lane
  unknown; Dart directives read to their semicolon; nested comments.

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

* test(pregate): init draft API is exercised directly, so init-draft.ts has a lane

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

* docs(changelog): restore the history a truncated rewrite dropped

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

* fix(pregate): outside-voice review fixes for the non-JS lanes

- Tool config is an allowlist: unknown dotfiles (.rspec, .husky hooks) and
  stylesheets still need a lane; snapshot, golden and fixture trees and
  .gstack/ are never inert.
- Django edges need Django evidence (manage.py or code importing django)
  and settings assignments in code, not strings or comments.
- A cd after ||, &, control flow or braces leaves a lane unknown.
- Dart directives allow metadata and quoted semicolons.
- init drafts at most 256 dependencies (the schema limit); the rest are TODO.

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

* test(windows): pregate-dart and pregate-non-js join the POSIX-run pregate fixtures

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

* chore: release v1.91.161.0

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

---------

Co-authored-by: arlophoenix <arlo.phoenix@gmail.com>
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
Co-authored-by: y$un_ <forrest.sun527@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 01:38:26 +00:00

101 lines
5.5 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. */
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. */
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('.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`;
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').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;
}