feat: catalog audit can repair orphaned refs and duplicate classnames

- add repairOrphanedCatalog: remove catalog_items rows whose item_ids only
  reference missing items_base entries, strip orphaned ids from mixed rows
- add repairDuplicateClassnames: merge items_base duplicates into one
  canonical row per classname, remap references, delete duplicate rows
- add 'repair structural issues' checkbox + result display in audit UI
This commit is contained in:
openhands committed 2026-08-03 18:08:45 +02:00
1 parent bee55e1fd4
commit 6fc14b9b84
4 files changed
+384 -1

No files matched your search

+96
View File
@@ -9,7 +9,9 @@ import {
applyCatalogSql,
generateCatalogSql,
type MissingCatalogEntry,
repairDuplicateClassnames,
repairFurniData,
repairOrphanedCatalog,
} from "./catalog-repair";
import { ensureDirectories } from "./furni-import";
import { repairMissingIcons } from "./repair-icons";
@@ -25,6 +27,7 @@ export interface AuditEvent {
| "sql_ready"
| "sql_applied"
| "furnidata_repair_complete"
| "structure_repair_complete"
| "batch_complete"
| "error";
message?: string;
@@ -41,6 +44,13 @@ export interface AuditEvent {
applied?: number;
sqlFailed?: number;
furniData?: { added: number; removedDuplicates: number };
structure?: {
orphanedRemoved: number;
orphanedCleaned: number;
duplicatesMerged: number;
duplicateRowsRemoved: number;
remappedReferences: number;
};
}
export interface RepairStats {
@@ -84,6 +94,11 @@ export interface AuditSummary {
catalogSqlApplied: number;
furniDataAdded: number;
furniDataDuplicatesRemoved: number;
orphanedRemoved: number;
orphanedCleaned: number;
duplicatesMerged: number;
duplicateRowsRemoved: number;
remappedReferences: number;
}
export interface CatalogAuditOptions {
@@ -92,6 +107,7 @@ export interface CatalogAuditOptions {
sql?: boolean;
applySql?: boolean;
repairFurniData?: boolean;
repairStructure?: boolean;
}
export async function runCatalogAudit(
@@ -450,6 +466,81 @@ export async function runCatalogAudit(
}
}
// ── Structural repair: orphaned catalog refs + duplicate classnames ──
let orphanedRemoved = 0;
let orphanedCleaned = 0;
let duplicatesMerged = 0;
let duplicateRowsRemoved = 0;
let remappedReferences = 0;
if (options?.repairStructure) {
onEvent?.({
type: "progress",
message: "Repairing structural issues (orphaned refs + duplicates)…",
});
try {
const orphanResult = await repairOrphanedCatalog();
orphanedRemoved = orphanResult.removed;
orphanedCleaned = orphanResult.cleaned;
// Re-derive orphaned_catalog issues from the fixed data.
const [afterCatalog] = (await db.execute(sql`
SELECT id, item_ids, catalog_name FROM catalog_items ORDER BY id
`)) as unknown as [
Array<{ id: number; item_ids: string; catalog_name: string }>,
unknown,
];
const orphanedCatalogIds = new Set<number>();
for (const ci of afterCatalog) {
const ids = ci.item_ids.split(";").map(Number).filter(Boolean);
for (const id of ids) {
if (!itemById.has(id)) orphanedCatalogIds.add(ci.id);
}
}
issues = issues.filter(
(i) =>
i.type !== "orphaned_catalog" ||
orphanedCatalogIds.has(i.catalogId ?? -1),
);
const duplicateResult = await repairDuplicateClassnames();
duplicatesMerged = duplicateResult.merged;
duplicateRowsRemoved = duplicateResult.rowsRemoved;
remappedReferences = duplicateResult.remapped;
// Re-derive duplicate_classname issues from the fixed data.
const [afterItems] = (await db.execute(sql`
SELECT item_name FROM items_base
`)) as unknown as [Array<{ item_name: string }>, unknown];
const afterCount = new Map<string, number>();
for (const row of afterItems) {
afterCount.set(row.item_name, (afterCount.get(row.item_name) ?? 0) + 1);
}
const dupNames = new Set(
[...afterCount].filter(([, count]) => count > 1).map(([name]) => name),
);
issues = issues.filter(
(i) =>
i.type !== "duplicate_classname" || dupNames.has(i.classname ?? ""),
);
onEvent?.({
type: "structure_repair_complete",
structure: {
orphanedRemoved,
orphanedCleaned,
duplicatesMerged,
duplicateRowsRemoved,
remappedReferences,
},
});
} catch (err) {
onEvent?.({
type: "error",
message: `Structural repair failed: ${(err as Error).message}`,
});
}
}
// ── Catalog SQL generation / apply ────────────────────────────────
let catalogSqlCount = 0;
let catalogSqlApplied = 0;
@@ -557,6 +648,11 @@ export async function runCatalogAudit(
catalogSqlApplied,
furniDataAdded,
furniDataDuplicatesRemoved,
orphanedRemoved,
orphanedCleaned,
duplicatesMerged,
duplicateRowsRemoved,
remappedReferences,
};
onEvent?.({