From a708c51db0cc004878b9ddb6b1c8df783fc3ea68 Mon Sep 17 00:00:00 2001 From: Steven Marcus <72649+srnm@users.noreply.github.com> Date: Mon, 15 Jun 2026 22:22:32 +0700 Subject: [PATCH 1/2] fix(mapper): preserve uv workspace root context --- src/mapper.test.ts | 60 ++++++++++++++++++++++++++++++++++ src/mappers/python.ts | 75 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 124 insertions(+), 11 deletions(-) diff --git a/src/mapper.test.ts b/src/mapper.test.ts index a8fb208..494213f 100644 --- a/src/mapper.test.ts +++ b/src/mapper.test.ts @@ -12700,6 +12700,32 @@ exclude = ["packages/legacy"] ).toBe(false); }); + it("adds workspace root runtime metadata to uv workspace member features", async () => { + const root = await fixtureRoot("clawpatch-python-uv-workspace-runtime-context-"); + await writeFixture( + root, + "pyproject.toml", + '[project]\nname = "workspace-root"\n\n[tool.uv.workspace]\nmembers = ["packages/backend"]\n', + ); + await writeFixture(root, ".python-version", "3.14\n"); + await writeFixture(root, "packages/backend/pyproject.toml", '[project]\nname = "backend"\n'); + await writeFixture(root, "packages/backend/src/backend/app.py", "def run():\n pass\n"); + + const project = await detectProject(root); + const result = await mapFeatures(root, project, []); + const backendSource = result.features.find( + (feature) => feature.title === "Python source packages/backend/src", + ); + + expect(backendSource?.contextFiles).toEqual( + expect.arrayContaining([ + { path: "packages/backend/pyproject.toml", reason: "python target runtime metadata" }, + { path: "pyproject.toml", reason: "python target runtime metadata" }, + { path: ".python-version", reason: "python target runtime metadata" }, + ]), + ); + }); + it("does not duplicate uv workspace members from root-level Python mapping", async () => { const root = await fixtureRoot("clawpatch-python-uv-workspace-root-dedupe-"); await writeFixture( @@ -12728,6 +12754,40 @@ exclude = ["packages/legacy"] ).toBe(false); }); + it("preserves root routes that only touch uv members through associated tests", async () => { + const root = await fixtureRoot("clawpatch-python-uv-workspace-root-route-tests-"); + await writeFixture( + root, + "pyproject.toml", + '[project]\nname = "workspace-root"\ndependencies = ["fastapi"]\n\n[tool.uv.workspace]\nmembers = ["packages/backend"]\n', + ); + await writeFixture(root, "packages/__init__.py", ""); + await writeFixture( + root, + "packages/shared.py", + "from fastapi import FastAPI\n\napp = FastAPI()\n\n@app.get('/shared')\ndef shared():\n return {'ok': True}\n", + ); + await writeFixture(root, "packages/backend/pyproject.toml", '[project]\nname = "backend"\n'); + await writeFixture( + root, + "packages/backend/tests/test_member.py", + "def test_member():\n pass\n", + ); + + const project = await detectProject(root); + const result = await mapFeatures(root, project, []); + const route = result.features.find((feature) => feature.title === "FastAPI route GET /shared"); + + expect(route?.ownedFiles).toEqual([ + { path: "packages/shared.py", reason: "FastAPI route handler shared" }, + ]); + expect(route?.tests).toEqual([]); + expect(route?.contextFiles).not.toContainEqual({ + path: "packages/backend/tests/test_member.py", + reason: "associated test", + }); + }); + it("preserves non-member files from mixed uv workspace root source groups", async () => { const root = await fixtureRoot("clawpatch-python-uv-workspace-root-mixed-"); await writeFixture( diff --git a/src/mappers/python.ts b/src/mappers/python.ts index 31c8849..4abc830 100644 --- a/src/mappers/python.ts +++ b/src/mappers/python.ts @@ -98,7 +98,9 @@ export async function pythonSeeds(root: string): Promise { const memberSeeds = await pythonProjectSeeds(join(root, member), { testCommandOverride: uvWorkspaceMemberTestCommand(member), }); - seeds.push(...memberSeeds.map((seed) => workspaceMemberSeed(seed, member))); + for (const seed of memberSeeds) { + seeds.push(await workspaceMemberSeed(root, seed, member)); + } } return seeds; } @@ -256,7 +258,10 @@ function pruneRootSeedUvMemberPaths( (seed.source !== "python-source-group" && seed.source !== "python-test-suite") || seed.ownedFiles === undefined ) { - return null; + if (seedOwnedOrEntryTouchesUvMember(seed, members)) { + return null; + } + return pruneSeedUvMemberReferences(seed, members); } const ownedFiles = seed.ownedFiles.filter((file) => !pathTouchesUvMember(file.path, members)); if (ownedFiles.length === 0) { @@ -301,6 +306,31 @@ function pruneRootSeedUvMemberPaths( return pruned; } +function seedOwnedOrEntryTouchesUvMember(seed: FeatureSeed, members: readonly string[]): boolean { + return ( + pathTouchesUvMember(seed.entryPath, members) || + (seed.ownedFiles?.some((file) => pathTouchesUvMember(file.path, members)) ?? false) + ); +} + +function pruneSeedUvMemberReferences(seed: FeatureSeed, members: readonly string[]): FeatureSeed { + const pruned: FeatureSeed = { ...seed }; + if (seed.contextFiles !== undefined) { + pruned.contextFiles = seed.contextFiles.filter( + (file) => !pathTouchesUvMember(file.path, members), + ); + } + if (seed.tests !== undefined) { + pruned.tests = seed.tests.filter((test) => !pathTouchesUvMember(test.path, members)); + } + if (seed.testPrefixes !== undefined) { + pruned.testPrefixes = seed.testPrefixes.filter( + (prefix) => !pathTouchesUvMember(prefix, members), + ); + } + return pruned; +} + function pathTouchesUvMember(path: string, members: readonly string[]): boolean { return members.some((member) => pathMatchesPrefix(path, member)); } @@ -315,11 +345,28 @@ function seedRepoPaths(seed: FeatureSeed): string[] { ]); } -function workspaceMemberSeed(seed: FeatureSeed, member: string): FeatureSeed { +async function workspaceMemberSeed( + workspaceRoot: string, + seed: FeatureSeed, + member: string, +): Promise { const prefixPath = (path: string): string => `${member}/${path}`; const genericSource = seed.source === "python-source-group"; const genericTestSuite = seed.source === "python-test-suite"; const entryPath = prefixPath(seed.entryPath); + const contextFiles = + seed.contextFiles === undefined + ? undefined + : seed.contextFiles.map((file) => ({ + ...file, + path: prefixPath(file.path), + })); + const workspaceRuntimeContext = await pythonRuntimeContextFiles( + workspaceRoot, + seed.ownedFiles === undefined + ? [entryPath] + : seed.ownedFiles.map((file) => prefixPath(file.path)), + ); return { ...seed, title: genericSource @@ -337,14 +384,7 @@ function workspaceMemberSeed(seed: FeatureSeed, member: string): FeatureSeed { ...(seed.ownedFiles === undefined ? {} : { ownedFiles: seed.ownedFiles.map((file) => ({ ...file, path: prefixPath(file.path) })) }), - ...(seed.contextFiles === undefined - ? {} - : { - contextFiles: seed.contextFiles.map((file) => ({ - ...file, - path: prefixPath(file.path), - })), - }), + contextFiles: uniqueSeedFileRefs([...(contextFiles ?? []), ...workspaceRuntimeContext]), ...(seed.tests === undefined ? {} : { tests: seed.tests.map((test) => ({ ...test, path: prefixPath(test.path) })) }), @@ -352,6 +392,19 @@ function workspaceMemberSeed(seed: FeatureSeed, member: string): FeatureSeed { }; } +function uniqueSeedFileRefs(refs: SeedFileRef[]): SeedFileRef[] { + const seen = new Set(); + const output: SeedFileRef[] = []; + for (const ref of refs) { + if (seen.has(ref.path)) { + continue; + } + seen.add(ref.path); + output.push(ref); + } + return output; +} + function workspaceMemberSummary(seed: FeatureSeed, member: string): string { if (seed.source === "python-source-group") { return prefixedPythonSourceSummary(seed, member); From 546e25b4e5b20ec2bd5d378eae81fc0f99f31b13 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 15 Jun 2026 17:19:45 -0400 Subject: [PATCH 2/2] docs(changelog): credit uv workspace fix --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b1dc18..0caeda0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Removed the direct MiniMax HTTP provider and its transport dependency; provider integrations are now explicitly limited to coding harnesses and agent CLIs. - Added uv workspace member mapping with repository-relative paths and member-local test commands while preserving mixed root source and test groups, thanks @srnm. +- Fixed uv workspace mapping to preserve root features with member-associated tests and include workspace-root runtime metadata in member features, thanks @srnm. ## 0.6.0 - 2026-06-11