diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bb368b9f..af00c45f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [1.91.188.0] - 2026-10-24 + +**Pregate maps extensionless .jsx imports to their tests (#3340).** On a Next.js or webpack JSX repo, `gstack-pregate` failed `lanes` with "has no test lane" for every component imported without an extension, so /ship's receipt refused and the only way out was shipping without one. The import resolver tried `.ts`, `.tsx`, `.js`, `.mjs` and `.cjs` but never `.jsx`, and a directory import found `index.ts` or `index.js` but not `index.jsx`, `index.tsx` or `index.mjs` (it fell back to selecting on the whole directory). Reported by adam-balazs. + +### Fixed +- **`lib/pregate/import-graph.ts`**: `RESOLVE_EXT` adds `.jsx`, `.mts`, `.cts`, `/index.tsx`, `/index.jsx` and `/index.mjs`. The existing entries keep their order, so a `.js` file still wins over a `.jsx` sibling. Relative, `paths` alias and `baseUrl` imports share the list, so all three resolve the new forms. + +### Tests +- `test/pregate-selection.test.ts` has three new cases: an extensionless import of a `.jsx`, `.mts` and `.cts` file; a directory import that reaches `index.jsx`, `index.tsx` and `index.mjs` and leaves the directory's other files unselected; and a jsconfig `paths` alias to a `.jsx` file, with a `.js` sibling that still wins. All three failed before the change. + +### Docs +- `docs/pregate.md` lists the extensions and index forms an extensionless import tries, in order. + ## [1.91.187.0] - 2026-10-24 **Four docs now say what the code does: the unattended run contract, the gate reply grammar, the session-kind override and voice triggers.** All four were reported by readers who checked the docs against the code. `docs/unattended.md` promised that every workflow skill ends an unattended run with a run directory and a `GSTACK_RESULT` line, but only `/autoplan` has the steps and helpers that write one (#3281, thanks @exGeni). `gstack-gate --help` still taught the reply grammar from before 1.91.107.0, without `rec` or `as-is`, while the render it relays leads with `rec` (#3282, thanks @exGeni). `docs/OPENCLAW.md` said only `spawned` is honored as a `GSTACK_SESSION_KIND` override, but `unattended` is honored too (#3287, thanks @exGeni). The README said natural phrases activate the right skill, without saying that only `voice-triggers:` aliases reach a host and that the plain `triggers:` list does not (#2118, thanks @jinzheio). diff --git a/VERSION b/VERSION index 7143a7b3a..b1d2af98e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.91.187.0 +1.91.188.0 diff --git a/agents-digest/gstack-AGENTS.md b/agents-digest/gstack-AGENTS.md index 31778cde5..39d35a768 100644 --- a/agents-digest/gstack-AGENTS.md +++ b/agents-digest/gstack-AGENTS.md @@ -1,4 +1,4 @@ -# gstack digest v1.91.187.0 — regenerate/re-copy after upgrading gstack +# gstack digest v1.91.188.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/docs/pregate.md b/docs/pregate.md index 8e7660cd2..22a661b98 100644 --- a/docs/pregate.md +++ b/docs/pregate.md @@ -137,6 +137,10 @@ Two stages with separate clocks, both printed: included, are source code, so a `dependencies` `[]` declaration clears them and `empty_lanes` does not), transitively through non-test files; deleted and renamed files select the tests that named them. + An extensionless JS/TS import (relative, a tsconfig/jsconfig `paths` alias or + `baseUrl`) tries the exact path, then `.ts`, `.tsx`, `.js`, `.jsx`, `.mjs`, + `.cjs`, `.mts`, `.cts`, then `index.ts`, `index.tsx`, `index.js`, `index.jsx` + and `index.mjs` inside the directory; the first file that exists wins. A touched source file with no lane is `FAIL lanes — has no test lane`, never a pass. Markdown that no test depends on is a docs/no-tests empty lane instead (`docs/no-tests: N documentation file(s)`), including when the base diff --git a/lib/pregate/import-graph.ts b/lib/pregate/import-graph.ts index a50595e7f..52da30f32 100644 --- a/lib/pregate/import-graph.ts +++ b/lib/pregate/import-graph.ts @@ -47,7 +47,7 @@ const scanner = new Bun.Transpiler({ loader: 'tsx' }); // that matches more text only gains an edge when it resolves to a repo file. const QUOTED = /['"`]([A-Za-z0-9_@][A-Za-z0-9_./@()[\]+-]*)['"`]/g; const JOIN = /\bjoin\(([^()]*)\)/g; -const RESOLVE_EXT = ['', '.ts', '.tsx', '.js', '.mjs', '.cjs', '/index.ts', '/index.js']; +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 // non-empty and starts with a dot or a word character, so no two repetitions can share a dot or a space. const PY_IMPORT = /(?:^|[;:])[ \t\f]*(?:from[ \t]+(\.+(?:\w[\w.]*)?|\w[\w.]*)[ \t]+import[ \t]+(\([^)]*\)|[^\n;]+)|import[ \t]+([^\n;]+))/gm; diff --git a/package.json b/package.json index 923c205ef..8e28e51fe 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gstack", - "version": "1.91.187", + "version": "1.91.188", "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-selection.test.ts b/test/pregate-selection.test.ts index 59404fd5f..3891b2ef8 100644 --- a/test/pregate-selection.test.ts +++ b/test/pregate-selection.test.ts @@ -82,6 +82,55 @@ describe('import graph (static imports ∪ repo-path literals ∪ bin convention expect(testsFor(g, 'notes/scratch.md')).toMatchObject({ tests: [], declaredNone: false }); }); + test('#3340: an extensionless import of a .jsx, .mts or .cts file maps to that file', () => { + const repo = fixture(); + commit(repo, { + 'src/components/Foo.jsx': 'export default function Foo() { return null; }\n', + 'src/lib/m.mts': 'export const m = 1;\n', + 'src/lib/c.cts': 'export const c = 1;\n', + 'test/foo.test.jsx': "import Foo from '../src/components/Foo';\nimport { m } from '../src/lib/m';\nimport { c } from '../src/lib/c';\n", + }); + const g = buildGraph(repo.work); + for (const f of ['src/components/Foo.jsx', 'src/lib/m.mts', 'src/lib/c.cts']) { + expect(testsFor(g, f).tests).toEqual(['test/foo.test.jsx']); + expect(testsFor(g, f).provenance[0]).toContain('(import)'); + } + }); + + test('#3340: a directory import resolves to its index.jsx, index.tsx or index.mjs, not only the dir/** fallback', () => { + const repo = fixture(); + commit(repo, { + 'src/Bar/index.jsx': 'export default 1;\n', + 'src/Bar/other.js': 'export default 2;\n', + 'src/Baz/index.tsx': 'export default 1;\n', + 'src/Baz/other.js': 'export default 2;\n', + 'src/Qux/index.mjs': 'export default 1;\n', + 'src/Qux/other.js': 'export default 2;\n', + 'test/dirs.test.js': "import Bar from '../src/Bar';\nimport Baz from '../src/Baz';\nimport Qux from '../src/Qux';\n", + }); + const g = buildGraph(repo.work); + for (const f of ['src/Bar/index.jsx', 'src/Baz/index.tsx', 'src/Qux/index.mjs']) { + expect(testsFor(g, f).tests).toEqual(['test/dirs.test.js']); + expect(testsFor(g, f).provenance[0]).not.toContain('/**'); + } + for (const f of ['src/Bar/other.js', 'src/Baz/other.js', 'src/Qux/other.js']) expect(testsFor(g, f).tests).toEqual([]); + }); + + test('#3340: an import through a jsconfig paths alias reaches a .jsx file; an existing .js sibling still wins', () => { + const repo = fixture(); + commit(repo, { + 'jsconfig.json': '{ "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["src/*"] } } }\n', + 'src/ui/Button.jsx': 'export default function Button() { return null; }\n', + 'src/ui/Card.js': 'export default 1;\n', + 'src/ui/Card.jsx': 'export default 2;\n', + 'test/ui.test.jsx': "import Button from '@/ui/Button';\nimport Card from '@/ui/Card';\n", + }); + const g = buildGraph(repo.work); + expect(testsFor(g, 'src/ui/Button.jsx').tests).toEqual(['test/ui.test.jsx']); + expect(testsFor(g, 'src/ui/Card.js').tests).toEqual(['test/ui.test.jsx']); + expect(testsFor(g, 'src/ui/Card.jsx').tests).toEqual([]); + }); + test('python: absolute, from-import submodule, relative and parenthesised imports and mock.patch targets map; stdlib, comments and docstrings do not', () => { const repo = fixture(); commit(repo, {