diff --git a/lib/pipeline/sources.ts b/lib/pipeline/sources.ts index 090284e..4dbe1df 100644 --- a/lib/pipeline/sources.ts +++ b/lib/pipeline/sources.ts @@ -446,9 +446,8 @@ export async function approveCandidate( }; } - // Write to {product_dir}/source/{stable_filename} const filename = stableFilenameFor(cand.doc_type); - const sourceDir = path.join(ctx.product_dir, "source"); + const sourceDir = sourceDirForScope(ctx, cand.scope); fs.mkdirSync(sourceDir, { recursive: true }); const targetAbs = path.join(sourceDir, filename); const tmp = `${targetAbs}.tmp-${process.pid}-${Date.now()}`; @@ -579,7 +578,18 @@ function sourcesYamlPathForScope( return path.join(ctx.product_dir, "sources.yaml"); } -function relForMd(scope: string, filename: string): string { +export function sourceDirForScope(ctx: ProductContext, scope: string): string { + const dataDir = path.resolve(env.PRODUCT_MCP_DATA_DIR); + if (scope === "category") { + return path.join(dataDir, ctx.category, ctx.vendor, "source"); + } + if (scope === "line") { + return path.join(dataDir, ctx.category, ctx.vendor, ctx.product_line, "source"); + } + return path.join(ctx.product_dir, "source"); +} + +export function relForMd(scope: string, filename: string): string { if (scope === "category") return `../../source/${filename}`; if (scope === "line") return `../source/${filename}`; return `source/${filename}`; @@ -729,7 +739,7 @@ export async function manualUpload( const scope = input.scope ?? "own"; const filename = input.filename ?? stableFilenameFor(input.docType); - const sourceDir = path.join(ctx.product_dir, "source"); + const sourceDir = sourceDirForScope(ctx, scope); fs.mkdirSync(sourceDir, { recursive: true }); const targetAbs = path.join(sourceDir, filename); const tmp = `${targetAbs}.tmp-${process.pid}-${Date.now()}`; diff --git a/tests/unit/pipeline-sources.test.ts b/tests/unit/pipeline-sources.test.ts new file mode 100644 index 0000000..1e3a110 --- /dev/null +++ b/tests/unit/pipeline-sources.test.ts @@ -0,0 +1,53 @@ +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { relForMd, sourceDirForScope, type ProductContext } from "@/lib/pipeline/sources"; + +function productContext(root: string): ProductContext { + return { + vendor: "dell", + product_line: "poweredge", + category: "server", + slug: "r770", + canonical_name: "Dell PowerEdge R770", + product_dir: path.join(root, "server", "dell", "poweredge", "r770"), + product_md: path.join(root, "server", "dell", "poweredge", "r770", "r770.md"), + }; +} + +describe("scoped source paths", () => { + it("stores own-scope files in the product source directory", () => { + const root = path.resolve(process.env.PRODUCT_MCP_DATA_DIR ?? "./data/sample"); + const ctx = productContext(root); + + expect(sourceDirForScope(ctx, "own")).toBe( + path.join(root, "server", "dell", "poweredge", "r770", "source"), + ); + expect(relForMd("own", "spec-sheet.pdf")).toBe("source/spec-sheet.pdf"); + }); + + it("stores line-scope files where ../source manifest paths resolve", () => { + const root = path.resolve(process.env.PRODUCT_MCP_DATA_DIR ?? "./data/sample"); + const ctx = productContext(root); + const filename = "technical-guide.pdf"; + + expect(sourceDirForScope(ctx, "line")).toBe( + path.join(root, "server", "dell", "poweredge", "source"), + ); + expect(path.resolve(ctx.product_dir, relForMd("line", filename))).toBe( + path.join(root, "server", "dell", "poweredge", "source", filename), + ); + }); + + it("stores category-scope files where ../../source manifest paths resolve", () => { + const root = path.resolve(process.env.PRODUCT_MCP_DATA_DIR ?? "./data/sample"); + const ctx = productContext(root); + const filename = "portfolio.pdf"; + + expect(sourceDirForScope(ctx, "category")).toBe( + path.join(root, "server", "dell", "source"), + ); + expect(path.resolve(ctx.product_dir, relForMd("category", filename))).toBe( + path.join(root, "server", "dell", "source", filename), + ); + }); +});