Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 32 additions & 12 deletions lib/sources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> {
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);
Expand Down Expand Up @@ -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<string>(
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));

Expand Down
45 changes: 44 additions & 1 deletion tests/unit/sources.test.ts
Original file line number Diff line number Diff line change
@@ -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", () => {
Expand Down Expand Up @@ -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"]);
});
});