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:
1 parent
bee55e1fd4
commit
6fc14b9b84
4 files changed
+384
-1
No files matched your search
@@ -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?.({
|
||||
|
||||
Reference in new issue
Block a user