From 1f445180f0d201644274932a107b432acbfe8e36 Mon Sep 17 00:00:00 2001 From: TheCryptoDonkey Date: Wed, 23 Sep 2026 01:47:05 +0100 Subject: [PATCH 1/3] feat: plan class, interface, type alias and enum declarations A plan anchor on a field or signature line, as in a walkthrough on an external repository, previously found no supported block. --- docs/WORKER-PACKETS.md | 7 +++++-- packages/context-tools/src/source-packet.mjs | 5 +++++ test/worker-packet.test.mjs | 12 ++++++++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/docs/WORKER-PACKETS.md b/docs/WORKER-PACKETS.md index 942edcb..feea28f 100644 --- a/docs/WORKER-PACKETS.md +++ b/docs/WORKER-PACKETS.md @@ -121,8 +121,11 @@ with the task receipt; inspect it before dispatch. Repeated anchors do not repea the same source lines in the packet. The planner selects the smallest enclosing supported function, method, -constructor, accessor, named arrow/function-expression owner, or statement-level -call with a direct function callback. A callback call is a syntactic category, +constructor, accessor, named arrow/function-expression owner, statement-level +call with a direct function callback, named class, interface, type alias or +enum. A field or signature line therefore selects its whole declaration, while a +line inside a method still selects only the method; imports and other top-level +statements are not planned. A callback call is a syntactic category, not a guarantee that it is a test: nested `it(...)` inside `describe(...)` selects the `it` call, but an anchor inside a smaller callback-bearing helper may select that helper. Anchor the outer declaration/call when its whole block is needed. diff --git a/packages/context-tools/src/source-packet.mjs b/packages/context-tools/src/source-packet.mjs index 93ec4d3..7abea4a 100644 --- a/packages/context-tools/src/source-packet.mjs +++ b/packages/context-tools/src/source-packet.mjs @@ -321,6 +321,11 @@ function plannerCandidates(source) { add(property, 'property', source.text[property.end] === ',' ? property.end + 1 : property.end); } if (ts.isExpressionStatement(node) && ts.isCallExpression(node.expression) && node.expression.arguments.some((argument) => ts.isArrowFunction(argument) || ts.isFunctionExpression(argument))) add(node, 'callbackCall'); + // Declarations own the lines between their members (fields, signatures, enum members). + if (ts.isClassDeclaration(node) && node.name) add(node, 'class'); + if (ts.isInterfaceDeclaration(node)) add(node, 'interface'); + if (ts.isTypeAliasDeclaration(node)) add(node, 'type'); + if (ts.isEnumDeclaration(node)) add(node, 'enum'); ts.forEachChild(node, visit); }; visit(source); diff --git a/test/worker-packet.test.mjs b/test/worker-packet.test.mjs index 2642866..c87f810 100644 --- a/test/worker-packet.test.mjs +++ b/test/worker-packet.test.mjs @@ -91,6 +91,18 @@ test('planner rejects unsupported, malformed, signature-only and ambiguous same- await assert.rejects(planPacket({ root, spec: await spec(root, planBase({ sources: [{ path: 'bad.ts', line: 1 }] })) }), /parse errors/); }); +test('planner selects class, interface, type and enum declarations for lines outside any function', async () => { + const root = await fixture(); + await writeFile(join(root, 'src.ts'), `import { a } from './a';\n/** Doc. */\nexport class Owner extends Base {\n field = 1;\n method() {\n return a;\n }\n}\ninterface Shape {\n size: number;\n}\ntype Pair = {\n left: string;\n};\nenum Mode {\n On,\n}\n`); + const result = await planPacket({ root, spec: await spec(root, planBase({ sources: [ + { path: 'src.ts', line: 4 }, { path: 'src.ts', line: 6 }, { path: 'src.ts', line: 10 }, { path: 'src.ts', line: 13 }, { path: 'src.ts', line: 16 }, + ] })) }); + assert.deepEqual(result.coverage.resolutions.map(({ kind, startLine, endLine }) => ({ kind, startLine, endLine })), [ + { kind: 'class', startLine: 2, endLine: 8 }, { kind: 'method', startLine: 5, endLine: 7 }, { kind: 'interface', startLine: 9, endLine: 11 }, { kind: 'type', startLine: 12, endLine: 14 }, { kind: 'enum', startLine: 15, endLine: 17 }, + ]); + await assert.rejects(planPacket({ root, spec: await spec(root, planBase({ sources: [{ path: 'src.ts', line: 1 }] })) }), /no supported/); +}); + test('plan CLI writes the v1 packet and reports coverage', async () => { const root = await fixture(); await writeFile(join(root, 'src.ts'), 'const named = () => {\n return 1;\n};\n'); From e53803b219bbac923c9c4dde7b0576d46d52f98c Mon Sep 17 00:00:00 2001 From: TheCryptoDonkey Date: Wed, 23 Sep 2026 01:49:09 +0100 Subject: [PATCH 2/3] fix: strip Git hook variables from context-tools tests Run from a pre-push hook in a linked worktree, repository-packet fixtures inherited GIT_DIR and wrote to the pushed repository: a fixture commit on the branch, core.bare=true and a fixture identity in the shared config. --- packages/context-tools/src/repository-packet-mcp.test.ts | 6 ++++++ packages/context-tools/vitest.config.ts | 2 +- packages/context-tools/vitest.setup.mjs | 4 ++++ 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 packages/context-tools/vitest.setup.mjs diff --git a/packages/context-tools/src/repository-packet-mcp.test.ts b/packages/context-tools/src/repository-packet-mcp.test.ts index 29f7c37..eaaa6ba 100644 --- a/packages/context-tools/src/repository-packet-mcp.test.ts +++ b/packages/context-tools/src/repository-packet-mcp.test.ts @@ -289,3 +289,9 @@ describe('repository packet compact rendering', () => { } finally { await connection.close() } }) }) + +describe('fixture repository isolation', () => { + it('runs without inherited Git hook variables', () => { + expect(Object.keys(process.env).filter((key) => key.startsWith('GIT_'))).toEqual([]) + }) +}) diff --git a/packages/context-tools/vitest.config.ts b/packages/context-tools/vitest.config.ts index be4f7e5..c888c6b 100644 --- a/packages/context-tools/vitest.config.ts +++ b/packages/context-tools/vitest.config.ts @@ -1,2 +1,2 @@ import { defineConfig } from 'vitest/config' -export default defineConfig({ test: { include: ['src/**/*.test.ts'], environment: 'node' } }) +export default defineConfig({ test: { include: ['src/**/*.test.ts'], environment: 'node', setupFiles: ['./vitest.setup.mjs'] } }) diff --git a/packages/context-tools/vitest.setup.mjs b/packages/context-tools/vitest.setup.mjs new file mode 100644 index 0000000..f6dae99 --- /dev/null +++ b/packages/context-tools/vitest.setup.mjs @@ -0,0 +1,4 @@ +// Git exports GIT_DIR, GIT_INDEX_FILE and similar into hooks. Fixture repositories +// created by `git -C ` would otherwise write to the repository being pushed, +// including its shared config, so every test runs without them. +for (const key of Object.keys(process.env)) if (key.startsWith('GIT_')) delete process.env[key] From 735c33596173b2fa23877cc49f039efb58386cae Mon Sep 17 00:00:00 2001 From: TheCryptoDonkey Date: Wed, 23 Sep 2026 01:51:04 +0100 Subject: [PATCH 3/3] fix: strip Git hook variables from ecosystem snapshot tests --- test/test_ecosystem_snapshot.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/test_ecosystem_snapshot.py b/test/test_ecosystem_snapshot.py index 6d23092..075550e 100644 --- a/test/test_ecosystem_snapshot.py +++ b/test/test_ecosystem_snapshot.py @@ -13,6 +13,10 @@ import ecosystem_snapshot as snapshot CLI = Path(snapshot.__file__).resolve() +# Git exports GIT_DIR and similar into hooks; fixture repositories must not +# inherit them, or `git -C ` writes to the repository being pushed. +for _key in [key for key in os.environ if key.startswith('GIT_')]: + del os.environ[_key] class SnapshotBoundaryTests(unittest.TestCase):