diff --git a/tools/hygiene/check-bash-retirement-inventory.test.ts b/tools/hygiene/check-bash-retirement-inventory.test.ts index d2024c8d97..1a9ff99ad0 100644 --- a/tools/hygiene/check-bash-retirement-inventory.test.ts +++ b/tools/hygiene/check-bash-retirement-inventory.test.ts @@ -31,6 +31,7 @@ describe("buildInventoryReport", () => { expect(report.allowlistIntegrity.duplicateEntries).toEqual([]); expect(report.allowlistIntegrity.orderViolations).toEqual([]); expect(report.allowlistIntegrity.uncategorizedEntries).toEqual([]); + expect(report.allowlistIntegrity.staleCategoryEntries).toEqual([]); expect(report.retained).toHaveLength(EXPECTED_RETAINED_SHELL.length); expect(report.drift.unexpected).toEqual([]); expect(report.drift.missingRetained).toEqual([]); @@ -72,6 +73,7 @@ describe("buildInventoryReport", () => { expect(report.allowlistIntegrity.duplicateEntries).toEqual([duplicate]); expect(report.allowlistIntegrity.orderViolations).toEqual([]); expect(report.allowlistIntegrity.uncategorizedEntries).toEqual([]); + expect(report.allowlistIntegrity.staleCategoryEntries).toEqual([]); expect(report.drift.unexpected).toEqual([]); expect(report.drift.missingRetained).toEqual([]); }); @@ -84,6 +86,20 @@ describe("buildInventoryReport", () => { expect(report.allowlistIntegrity.duplicateEntries).toEqual([]); expect(report.allowlistIntegrity.orderViolations).toEqual([{ index: 1, previous: second, current: first }]); expect(report.allowlistIntegrity.uncategorizedEntries).toEqual([]); + expect(report.allowlistIntegrity.staleCategoryEntries).toEqual([]); + expect(report.drift.unexpected).toEqual([]); + expect(report.drift.missingRetained).toEqual([]); + }); + + test("flags category metadata entries that are no longer retained", () => { + const [stale, rest] = splitExpectedRetained(); + const report = buildInventoryReport(rest, rest); + + expect(hasDrift(report)).toBe(true); + expect(report.allowlistIntegrity.duplicateEntries).toEqual([]); + expect(report.allowlistIntegrity.orderViolations).toEqual([]); + expect(report.allowlistIntegrity.uncategorizedEntries).toEqual([]); + expect(report.allowlistIntegrity.staleCategoryEntries).toEqual([stale]); expect(report.drift.unexpected).toEqual([]); expect(report.drift.missingRetained).toEqual([]); }); @@ -161,4 +177,15 @@ describe("renderReport", () => { expect(rendered).toContain(duplicate); expect(rendered).not.toContain("## Unexpected non-Lean shell files"); }); + + test("renders stale category map entries as allowlist integrity errors", () => { + const [stale, rest] = splitExpectedRetained(); + const rendered = renderReport(buildInventoryReport(rest, rest)); + + expect(rendered).toContain("## Retained shell allowlist integrity errors"); + expect(rendered).toContain("free of stale category metadata"); + expect(rendered).toContain("### Stale category entries"); + expect(rendered).toContain(stale); + expect(rendered).not.toContain("## Unexpected non-Lean shell files"); + }); }); diff --git a/tools/hygiene/check-bash-retirement-inventory.ts b/tools/hygiene/check-bash-retirement-inventory.ts index 19fecddaa4..06d78882fc 100644 --- a/tools/hygiene/check-bash-retirement-inventory.ts +++ b/tools/hygiene/check-bash-retirement-inventory.ts @@ -40,6 +40,7 @@ interface AllowlistIntegrity { readonly duplicateEntries: readonly string[]; readonly orderViolations: readonly AllowlistOrderViolation[]; readonly uncategorizedEntries: readonly string[]; + readonly staleCategoryEntries: readonly string[]; } export type RetainedShellCategory = @@ -180,6 +181,7 @@ export const trackedNonLeanBashFilesFromGit = trackedNonLeanShellFilesFromGit; function inspectAllowlistIntegrity(expectedRetained: readonly string[]): AllowlistIntegrity { const counts = new Map(); + const expectedSet = new Set(expectedRetained); const orderViolations: AllowlistOrderViolation[] = []; const uncategorizedEntries = new Set(); @@ -200,10 +202,15 @@ function inspectAllowlistIntegrity(expectedRetained: readonly string[]): Allowli .map(([file]) => file) .sort((a, b) => a.localeCompare(b)); + const staleCategoryEntries = Object.keys(RETAINED_SHELL_CATEGORY_BY_FILE) + .filter((file) => !expectedSet.has(file)) + .sort((a, b) => a.localeCompare(b)); + return { duplicateEntries, orderViolations, uncategorizedEntries: [...uncategorizedEntries].sort((a, b) => a.localeCompare(b)), + staleCategoryEntries, }; } @@ -211,7 +218,8 @@ function hasAllowlistIntegrityDrift(integrity: AllowlistIntegrity): boolean { return ( integrity.duplicateEntries.length > 0 || integrity.orderViolations.length > 0 || - integrity.uncategorizedEntries.length > 0 + integrity.uncategorizedEntries.length > 0 || + integrity.staleCategoryEntries.length > 0 ); } @@ -282,6 +290,7 @@ export function renderReport(report: InventoryReport): string { lines.push(`allowlist_duplicates: ${String(report.allowlistIntegrity.duplicateEntries.length)}`); lines.push(`allowlist_order_violations: ${String(report.allowlistIntegrity.orderViolations.length)}`); lines.push(`allowlist_uncategorized: ${String(report.allowlistIntegrity.uncategorizedEntries.length)}`); + lines.push(`allowlist_stale_category_entries: ${String(report.allowlistIntegrity.staleCategoryEntries.length)}`); lines.push(`unexpected: ${String(report.drift.unexpected.length)}`); lines.push(`missing_retained: ${String(report.drift.missingRetained.length)}`); lines.push(""); @@ -298,7 +307,9 @@ export function renderReport(report: InventoryReport): string { if (hasAllowlistIntegrityDrift(report.allowlistIntegrity)) { lines.push("## Retained shell allowlist integrity errors"); lines.push(""); - lines.push("The retained shell allowlist must be unique and sorted before repo shell drift is classified."); + lines.push( + "The retained shell allowlist must be unique, sorted, fully categorized, and free of stale category metadata before repo shell drift is classified.", + ); lines.push(""); if (report.allowlistIntegrity.duplicateEntries.length > 0) { lines.push("### Duplicate entries"); @@ -320,6 +331,12 @@ export function renderReport(report: InventoryReport): string { for (const file of report.allowlistIntegrity.uncategorizedEntries) lines.push(`- ${file}`); lines.push(""); } + if (report.allowlistIntegrity.staleCategoryEntries.length > 0) { + lines.push("### Stale category entries"); + lines.push(""); + for (const file of report.allowlistIntegrity.staleCategoryEntries) lines.push(`- ${file}`); + lines.push(""); + } return `${lines.join("\n")}\n`; } if (report.drift.unexpected.length > 0) {