From 4eae3ef27d0e7a3f6a10366f16cd985508df9133 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Fri, 30 Oct 2026 19:58:48 -0700 Subject: [PATCH] v1.91.348.0 pregate: map tests to dotfiles named in quoted literals (GST-246) (#3661) * pregate: map tests to dotfiles named in quoted literals The quoted-literal pattern required [A-Za-z0-9_@] after the relative prefix, so "../.gitleaks.toml" never matched, and a dot-led literal resolved beside the test instead of from the repo root. A branch touching only the dotfile ended in PREGATE_NO_LANE (GST-246, garrytan/truther#956). Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * pregate: dotfile literals as their own pass; dot-led literals also resolve from the naming file's folder Review fixes for #3661: the optional dot in QUOTED let a closing quote followed by .method( consume the next literal's opening quote, dropping an edge main found; and join(__dirname, '.github', ...) lost its fixture edge. Regression test fails at acd2274. 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> --- CHANGELOG.md | 4 +++ VERSION | 2 +- agents-digest/gstack-AGENTS.md | 2 +- lib/pregate/import-graph.ts | 8 +++-- package.json | 2 +- test/pregate-import-graph-paths.test.ts | 40 +++++++++++++++++++++++++ 6 files changed, 53 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6e7f0802..7023f690a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [1.91.348.0] - 2026-10-31 + +- **Pregate now maps tests to dotfiles they name.** A test that reads `../.gitleaks.toml`, `.gitleaks.toml` or `join('.github', 'workflows', 'ci.yml')` used to get no edge to that file, so a branch touching only the dotfile ended in `PREGATE_NO_LANE`. Quoted literals may now start with a dot, and a dot-led literal that isn't `./` or `../` resolves from both the repository root and the folder of the file that names it, so a fixture's own `.github/` keeps its edge. A literal still forms an edge only when it names a file in the repo, so strings like `'.ts'` add nothing. On gstack this adds 179 edges (`buildGraph` over this repository: 16,934 on main 2127729, 17,113 with this change, none removed), all to real dotfiles such as `.github/workflows/*.yml` and `.gstack/*.json`. (GST-246, reported from garrytan/truther#956.) + ## [1.91.347.0] - 2026-10-31 **A Captain archives only its own crew's threads (R39, amended). A thread outside every crew belongs to the owner and stays as it is, even when the Captain's own automation created it.** diff --git a/VERSION b/VERSION index 63abd749d..f47070e4a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.91.347.0 +1.91.348.0 diff --git a/agents-digest/gstack-AGENTS.md b/agents-digest/gstack-AGENTS.md index 393061062..5ba10b586 100644 --- a/agents-digest/gstack-AGENTS.md +++ b/agents-digest/gstack-AGENTS.md @@ -1,4 +1,4 @@ -# gstack digest v1.91.347.0 — regenerate/re-copy after upgrading gstack +# gstack digest v1.91.348.0 — regenerate/re-copy after upgrading gstack Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed for agent hosts without a full skill install. The full skills add workflows, diff --git a/lib/pregate/import-graph.ts b/lib/pregate/import-graph.ts index 6f6dc13b7..2445448c6 100644 --- a/lib/pregate/import-graph.ts +++ b/lib/pregate/import-graph.ts @@ -56,6 +56,9 @@ const scanner = new Bun.Transpiler({ loader: 'tsx' }); // names it, like a relative import; without it a test reading a template by // relative path had no edge to the template (GSTA-121's test/ship-publication-gates.test.ts). const QUOTED = /['"`]((?:\.\.?\/)*[A-Za-z0-9_@][A-Za-z0-9_./@()[\]+-]*)['"`]/g; +// A dot-led literal (`'.gitleaks.toml'`, `'../.github/x.yml'`) runs as its own pass and leaves its +// closing quote unconsumed, so a closing quote followed by `.method(` never eats the next literal's quote. +const DOTFILE = /['"`]((?:\.\.?\/)*\.[A-Za-z0-9_@][A-Za-z0-9_./@()[\]+-]*)(?=['"`])/g; const JOIN = /\bjoin\(([^()]*)\)/g; const RESOLVE_EXT = ['', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts', '/index.ts', '/index.tsx', '/index.js', '/index.jsx', '/index.mjs']; // A statement starts a line or follows `;` or a compound statement's `:`. The module spec is @@ -291,11 +294,11 @@ function resolveCandidate(files: Set, dirs: Set, c: string, lite */ function resolveRefs(files: Set, dirs: Set, fromFile: string, ref: string, literal = false, config: PathAliases = NO_ALIASES): string[] { if (!ref || ref.startsWith('node:') || ref.startsWith('bun:')) return []; - if (ref.startsWith('.')) { const t = resolveCandidate(files, dirs, normalize(path.posix.join(path.posix.dirname(fromFile), ref)), literal); return t ? [t] : []; } + if (/^\.\.?(?:\/|$)/.test(ref)) { const t = resolveCandidate(files, dirs, normalize(path.posix.join(path.posix.dirname(fromFile), ref)), literal); return t ? [t] : []; } const { chosen, others } = matchAliases(config.aliases, ref); for (const c of chosen ?? []) { const t = resolveCandidate(files, dirs, c, literal); if (t) return [t]; } if (!chosen && others.length === 0 && ref.startsWith('@') && !ref.includes('/')) return []; - const plausible = [...others, ...(config.baseUrl ? [normalize(path.posix.join(config.baseUrl, ref))] : []), normalize(ref)]; + const plausible = [...others, ...(config.baseUrl ? [normalize(path.posix.join(config.baseUrl, ref))] : []), normalize(ref), ...(literal && ref.startsWith('.') ? [normalize(path.posix.join(path.posix.dirname(fromFile), ref))] : [])]; return [...new Set(plausible.map(c => resolveCandidate(files, dirs, c, literal)).filter((t): t is string => t !== null))]; } @@ -429,6 +432,7 @@ export function referencesIn(files: Set, dirs: Set, rel: string, // sources (a test spawns a bin; a bash bin calls a sibling). Prose and // manifests name everything and would make every file a hub. if (!scansLiterals(rel)) return out; + for (const m of source.matchAll(DOTFILE)) for (const target of resolveRefs(files, dirs, rel, m[1]!, true, config)) out.push({ target, via: 'literal' }); for (const m of source.matchAll(QUOTED)) { const targets = resolveRefs(files, dirs, rel, m[1]!, true, config); for (const target of targets) out.push({ target, via: 'literal' }); diff --git a/package.json b/package.json index d389f8fff..ea893d536 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gstack", - "version": "1.91.347", + "version": "1.91.348", "description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.", "license": "MIT", "type": "module", diff --git a/test/pregate-import-graph-paths.test.ts b/test/pregate-import-graph-paths.test.ts index 4631a3662..2f9a98293 100644 --- a/test/pregate-import-graph-paths.test.ts +++ b/test/pregate-import-graph-paths.test.ts @@ -48,6 +48,46 @@ describe('import graph: route-group / dynamic-segment paths and tsconfig path al expect(testsFor(buildGraph(repo.work), 'ship/sections/pr-body.md.tmpl').tests).toEqual(['test/publication.test.ts']); }); + test('a dotfile named by a quoted literal or a join() maps, relative or from the repo root', () => { + const repo = fixture(); + commit(repo, { + '.gitleaks.toml': 'title = "x"\n', + '.github/workflows/ci.yml': 'on: push\n', + 'test/relative.test.ts': "import { readFileSync } from 'node:fs';\nreadFileSync(new URL('../.gitleaks.toml', import.meta.url), 'utf8');\n", + 'test/rooted.test.ts': "import { readFileSync } from 'node:fs';\nreadFileSync('.gitleaks.toml', 'utf8');\n", + 'test/joined.test.ts': "import { join } from 'node:path';\nimport { readFileSync } from 'node:fs';\nreadFileSync(join('.github', 'workflows', 'ci.yml'), 'utf8');\n", + }); + const g = buildGraph(repo.work); + expect(testsFor(g, '.gitleaks.toml').tests).toEqual(['test/relative.test.ts', 'test/rooted.test.ts']); + expect(testsFor(g, '.github/workflows/ci.yml').tests).toEqual(['test/joined.test.ts']); + }); + + test('a dot-led literal keeps the edges the base found: no swallowed quote, fixture dotfiles still map', () => { + const repo = fixture(); + commit(repo, { + 'test/fixtures/x.md': 'x\n', + '.github/workflows/ci.yml': 'on: push\n', + 'test/.github/workflows/ci.yml': 'on: push\n', + 'test/method.test.ts': "const d = 'a';\nexport const hit = `${d}`.includes('test/fixtures/x.md');\n", + 'test/config.test.ts': "import { join } from 'node:path';\nimport { readFileSync } from 'node:fs';\nreadFileSync(join(__dirname, '.github', 'workflows', 'ci.yml'), 'utf8');\n", + }); + const g = buildGraph(repo.work); + expect(testsFor(g, 'test/fixtures/x.md').tests).toEqual(['test/method.test.ts']); + expect(testsFor(g, 'test/.github/workflows/ci.yml').tests).toEqual(['test/config.test.ts']); + expect(testsFor(g, '.github/workflows/ci.yml').tests).toEqual(['test/config.test.ts']); + }); + + test('a dot-led literal that names no repo file adds no edge (extensions, missing dotfiles)', () => { + const repo = fixture(); + commit(repo, { + 'lib/plan.ts': 'export const plan = 1;\n', + 'test/ext.test.ts': "const exts = ['.ts', '.md', '.env', '../.missing.toml', '.github'];\nexport { exts };\n", + }); + const g = buildGraph(repo.work); + expect(testsFor(g, 'lib/plan.ts').tests).toEqual([]); + expect([...g.reverse.values()].flat().filter(e => e.from === 'test/ext.test.ts')).toEqual([]); + }); + test('a relative import of a bracketed path already maps (control: the import scanner is not the bug)', () => { const repo = fixture(); commit(repo, {