From 197dad7175052599cf6a6cb64cc059cd2280171c Mon Sep 17 00:00:00 2001 From: jony376 Date: Sun, 31 May 2026 11:07:54 -0700 Subject: [PATCH] refactor(sources): extract declared source-type inventory helper --- lib/sources.ts | 44 +++++++++++++++++++++++++++---------- tests/unit/sources.test.ts | 45 +++++++++++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 13 deletions(-) diff --git a/lib/sources.ts b/lib/sources.ts index 4eda699..f10484d 100644 --- a/lib/sources.ts +++ b/lib/sources.ts @@ -156,6 +156,33 @@ function inferTypeFromFilename(name: string): string | null { return null; } +export type DeclaredSourceTypesInput = { + mdManifest: ManifestSource[] | null; + manifest: SourcesManifest | null; + sourceFiles: { name: string; size: number; isText: boolean }[]; +}; + +/** + * Centralized source-type inventory with precedence: + * 1) product MD manifest + * 2) sources.yaml manifest + * 3) filename inference fallback + */ +export function collectDeclaredSourceTypes(input: DeclaredSourceTypesInput): Set { + if (input.mdManifest) { + return new Set(input.mdManifest.map((s) => s.type).filter(Boolean)); + } + if (input.manifest) { + return new Set((input.manifest.sources ?? []).map((s) => s.type).filter(Boolean)); + } + return new Set( + input.sourceFiles + .filter((f) => f.name.endsWith(".pdf")) + .map((f) => inferTypeFromFilename(f.name)) + .filter((t): t is string => t !== null) + ); +} + function coerceDates(v: any): any { if (v instanceof Date) return v.toISOString(); if (Array.isArray(v)) return v.map(coerceDates); @@ -223,18 +250,11 @@ function summarize(absDir: string, relDir: string): SourcesSummary | null { const vendor = manifest?.product?.vendor ?? parsed.vendor; const { rule, required } = requiredTypesForVendor(vendor); - // The MD manifest is the canonical index per `_base.md`; prefer its declared types. - // Fall back to sources.yaml types, then to filename inference for legacy dirs. - const declaredTypes = new Set( - mdManifest - ? mdManifest.map((s) => s.type) - : manifest - ? (manifest.sources ?? []).map((s) => s.type) - : sourceFiles - .filter((f) => f.name.endsWith(".pdf")) - .map((f) => inferTypeFromFilename(f.name)) - .filter((t): t is string => t !== null) - ); + const declaredTypes = collectDeclaredSourceTypes({ + mdManifest, + manifest, + sourceFiles + }); const present = required.filter((t) => declaredTypes.has(t)); const missing = required.filter((t) => !declaredTypes.has(t)); diff --git a/tests/unit/sources.test.ts b/tests/unit/sources.test.ts index 98574d1..2c4959e 100644 --- a/tests/unit/sources.test.ts +++ b/tests/unit/sources.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { classifyScope, formatBytes } from "@/lib/sources"; +import { classifyScope, collectDeclaredSourceTypes, formatBytes } from "@/lib/sources"; describe("classifyScope (manifest local: → scope)", () => { it("classifies bare or source-prefixed paths as product scope", () => { @@ -45,3 +45,46 @@ describe("formatBytes", () => { expect(formatBytes(3.25 * 1024 ** 3)).toBe("3.25 GB"); }); }); + +describe("collectDeclaredSourceTypes", () => { + it("prefers md manifest types when available", () => { + const result = collectDeclaredSourceTypes({ + mdManifest: [ + { scope: "product", local: "source/a.pdf", type: "spec-sheet" }, + { scope: "line", local: "../source/b.pdf", type: "tech-guide" }, + ], + manifest: { + sources: [{ filename: "x.pdf", type: "other" }], + }, + sourceFiles: [{ name: "spec-sheet.pdf", size: 1, isText: false }], + }); + expect(Array.from(result).sort()).toEqual(["spec-sheet", "tech-guide"]); + }); + + it("falls back to sources.yaml manifest when md manifest is absent", () => { + const result = collectDeclaredSourceTypes({ + mdManifest: null, + manifest: { + sources: [ + { filename: "a.pdf", type: "spec-sheet" }, + { filename: "b.pdf", type: "tech-guide" }, + ], + }, + sourceFiles: [{ name: "quickspecs.pdf", size: 1, isText: false }], + }); + expect(Array.from(result).sort()).toEqual(["spec-sheet", "tech-guide"]); + }); + + it("falls back to filename inference when no manifest data exists", () => { + const result = collectDeclaredSourceTypes({ + mdManifest: null, + manifest: null, + sourceFiles: [ + { name: "technical-guide.pdf", size: 1, isText: false }, + { name: "quickspecs.pdf", size: 1, isText: false }, + { name: "notes.txt", size: 1, isText: true }, + ], + }); + expect(Array.from(result).sort()).toEqual(["spec-sheet", "tech-guide"]); + }); +});