From f74999d0cf6f4aa2371ce457914d2f2ec2c545f8 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sat, 6 Jun 2026 17:36:03 +0000 Subject: [PATCH] Exclude nested skill trees from parent inventories Co-Authored-By: Paperclip --- .../__tests__/company-skills-service.test.ts | 30 +++++++++++++++++++ server/src/services/company-skills.ts | 13 ++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/server/src/__tests__/company-skills-service.test.ts b/server/src/__tests__/company-skills-service.test.ts index ae931cb9a2c..2c1498512d5 100644 --- a/server/src/__tests__/company-skills-service.test.ts +++ b/server/src/__tests__/company-skills-service.test.ts @@ -170,6 +170,36 @@ describeEmbeddedPostgres("companySkillService.list", () => { ]); }); + it("does not include nested skill trees in a root skill inventory", async () => { + const companyId = await createCompany(); + const rootDir = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-root-with-nested-skill-")); + cleanupDirs.add(rootDir); + await fs.mkdir(path.join(rootDir, "references"), { recursive: true }); + await fs.mkdir(path.join(rootDir, "skills", "nested", "references"), { recursive: true }); + await fs.writeFile(path.join(rootDir, "SKILL.md"), "---\nname: Root Skill\n---\n\n# Root\n", "utf8"); + await fs.writeFile(path.join(rootDir, "references", "root.md"), "# Root Reference\n", "utf8"); + await fs.writeFile( + path.join(rootDir, "skills", "nested", "SKILL.md"), + "---\nname: Nested Skill\n---\n\n# Nested\n", + "utf8", + ); + await fs.writeFile(path.join(rootDir, "skills", "nested", "references", "nested.md"), "# Nested Reference\n", "utf8"); + + const result = await svc.importFromSource(companyId, rootDir); + const rootSkill = result.imported.find((skill) => skill.slug === "root-skill"); + const nestedSkill = result.imported.find((skill) => skill.slug === "nested-skill"); + + expect(result.imported).toHaveLength(2); + expect(rootSkill?.fileInventory).toEqual([ + { path: "references/root.md", kind: "reference" }, + { path: "SKILL.md", kind: "skill" }, + ]); + expect(nestedSkill?.fileInventory).toEqual([ + { path: "references/nested.md", kind: "reference" }, + { path: "SKILL.md", kind: "skill" }, + ]); + }); + it("rejects skill inventory refresh for a missing company", async () => { await expect(svc.list(randomUUID())).rejects.toMatchObject({ status: 404, diff --git a/server/src/services/company-skills.ts b/server/src/services/company-skills.ts index 9823c0bc2dd..a280d7fc4ae 100644 --- a/server/src/services/company-skills.ts +++ b/server/src/services/company-skills.ts @@ -1119,10 +1119,19 @@ async function readLocalSkillImports(companyId: string, sourcePath: string): Pro for (const skillPath of skillPaths) { const skillDir = path.posix.dirname(skillPath); const skillInventoryRoot = skillDir === "." ? "" : skillDir; + const nestedSkillRoots = skillPaths + .map((entry) => { + const nestedDir = path.posix.dirname(entry); + return nestedDir === "." ? "" : nestedDir; + }) + .filter((entry) => + entry !== skillInventoryRoot + && (skillInventoryRoot ? entry.startsWith(`${skillInventoryRoot}/`) : entry.length > 0)); const inventory = allFiles .filter((entry) => - entry === skillPath - || (skillInventoryRoot ? entry.startsWith(`${skillInventoryRoot}/`) : true)) + (entry === skillPath || (skillInventoryRoot ? entry.startsWith(`${skillInventoryRoot}/`) : true)) + && !nestedSkillRoots.some((nestedRoot) => + entry === `${nestedRoot}/SKILL.md` || entry.startsWith(`${nestedRoot}/`))) .map((entry) => { const relative = entry === skillPath ? "SKILL.md"