From b811086de6efae9ae21629600fe1642a91fb957b Mon Sep 17 00:00:00 2001 From: openhands Date: Thu, 20 Aug 2026 14:57:40 +0200 Subject: [PATCH] fix: normalize cross-section duplicate ids in furnidata reconcile Classnames listed in both roomitemtypes and wallitemtypes keep a canonical copy with the correct id/offerid while the stray wall copy can carry a legacy wrong id. reconcileFurniDataWithItemsBase now normalizes those stray copies to the offerid whenever the offerid maps to an items_base row whose classname matches exactly, so id === offerid === sprite id holds for every uniquely-mapped classname. The sandbox rehearsal asserts this invariant and the live database has been reconciled (76 ids fixed, 0 remaining). --- src/lib/services/catalog-repair-live.test.ts | 30 ++++++++++++++++++++ src/lib/services/furni-data.ts | 18 ++++++++++++ 2 files changed, 48 insertions(+) diff --git a/src/lib/services/catalog-repair-live.test.ts b/src/lib/services/catalog-repair-live.test.ts index defa4949..58e39453 100644 --- a/src/lib/services/catalog-repair-live.test.ts +++ b/src/lib/services/catalog-repair-live.test.ts @@ -230,6 +230,36 @@ describeGuard("catalog studio live repair rehearsal (sandbox)", () => { const offer = await reconcileImportedOfferIds(); console.log("REHEARSAL offer reconcile:", JSON.stringify(offer)); + // Every furnidata entry whose classname maps to a unique items_base row + // must have id === offerid === db id (incl. cross-section copies). + const [uniqueRows] = (await db.execute(sql` + SELECT id, item_name FROM items_base + `)) as unknown as [Array<{ id: number; item_name: string }>, unknown]; + const dbById = new Map(); + for (const r of uniqueRows) dbById.set(Number(r.id), r.item_name); + const byName = new Map(); + for (const r of uniqueRows) + byName.set(r.item_name, (byName.get(r.item_name) ?? 0) + 1); + const finalFd = JSON.parse( + await fs.readFile( + path.join(gamedataDir, "config", "FurnitureData.json"), + "utf8", + ), + ) as Record> }>; + let badCrossSection = 0; + for (const section of ["roomitemtypes", "wallitemtypes"] as const) { + for (const e of finalFd[section]?.furnitype ?? []) { + const cn = String(e.classname ?? ""); + if (byName.get(cn) !== 1) continue; + const offer = Number(e.offerid); + if (!Number.isFinite(offer) || offer <= 0) continue; + if (dbById.get(offer) !== cn) continue; + if (Number(e.id) !== offer) badCrossSection++; + } + } + console.log("REHEARSAL cross-section id mismatches:", badCrossSection); + expect(badCrossSection).toBe(0); + const afterState = await audit(); console.log("REHEARSAL after:", JSON.stringify(afterState)); diff --git a/src/lib/services/furni-data.ts b/src/lib/services/furni-data.ts index d8e45ea3..e09f1805 100644 --- a/src/lib/services/furni-data.ts +++ b/src/lib/services/furni-data.ts @@ -536,6 +536,24 @@ export async function reconcileFurniDataWithItemsBase(): Promise(); + for (const row of rows) dbById.set(row.id, row.itemName); + for (const section of ["roomitemtypes", "wallitemtypes"] as const) { + for (const e of furniData[section]?.furnitype ?? []) { + const offer = entryOfferId(e); + if (offer <= 0 || Number(e.id) === offer) continue; + if (dbById.get(offer) !== e.classname) continue; + e.id = offer; + fixedIds++; + } + } + if (fixedIds > 0 || fixedOfferIds > 0) { await writeFurniData(furniData as Record); }