From bbf7d2e92ad696d787bca671fd1341b6b55644b2 Mon Sep 17 00:00:00 2001 From: lyf <2514544224@qq.com> Date: Tue, 30 Jun 2026 01:37:17 +0800 Subject: [PATCH] fix: harden SGS hall POI matching diagnostics Co-Authored-By: Claude Opus 4.8 (1M context) --- .superpowers/sdd/task-2-report.md | 14 +++-- scripts/check-sgs-hall-poi-adapter.mjs | 81 ++++++++++++++++++------- src/data/adapters/sgsSdkGuideAdapter.ts | 41 ++++++++++--- 3 files changed, 100 insertions(+), 36 deletions(-) diff --git a/.superpowers/sdd/task-2-report.md b/.superpowers/sdd/task-2-report.md index 25bd3d4..ed1ea02 100644 --- a/.superpowers/sdd/task-2-report.md +++ b/.superpowers/sdd/task-2-report.md @@ -4,20 +4,22 @@ Status: DONE Files changed: - `src/data/adapters/sgsSdkGuideAdapter.ts` +- `scripts/check-sgs-hall-poi-adapter.mjs` - `.superpowers/sdd/task-2-report.md` Commands run: - `node scripts/check-sgs-hall-poi-adapter.mjs` - - Output: `SGS hall POI adapter smoke check passed` + - Output: command completed successfully with no stdout; all assertions passed. - `pnpm type-check` - - Output summary: `vue-tsc --noEmit` completed successfully. + - Output summary: failed in the worktree because `vue-tsc` was not resolvable there (`node_modules` / local bin unavailable in the isolated worktree). Commit: -- `18385be` `fix: keep SGS hall fallback metadata preview-only` +- Pending Notes: -- POI-level `sourcePlaceId` now remains unset for space-center fallback halls; entrance-specific `sourcePlaceId`/`routeNodeId` stay on `entrances` until a route-capable entrance upgrades the POI. -- Space-center fallback halls now use matched-space floor metadata; route-capable entrance upgrades replace POI floor metadata with entrance floor metadata and set `sourceConfidence` to `backend-sgs-sdk-hall-entrance`. +- Hall-space matching now ranks candidates by specificity: exact normalized name match first, then longest normalized overlapping space name. +- Place-only fallback hall IDs now include floor identity, preventing cross-floor collisions for same-named halls. +- `skippedSpaceCount` now checks whether each eligible space has its canonical `hall-{space.id}` POI, so place-only halls no longer distort diagnostics. Concerns: -- The targeted smoke script is a lightweight regression harness, not a full test runner. +- `pnpm type-check` could not complete inside the isolated worktree because the local `vue-tsc` binary was unavailable there. diff --git a/scripts/check-sgs-hall-poi-adapter.mjs b/scripts/check-sgs-hall-poi-adapter.mjs index 48d41d4..b1950ab 100644 --- a/scripts/check-sgs-hall-poi-adapter.mjs +++ b/scripts/check-sgs-hall-poi-adapter.mjs @@ -76,10 +76,28 @@ const findMatchedHallSpace = (place, spaces) => { const placeName = normalizedHallName(placeHallName(place)) if (!placeName) return undefined - return spaces.find((space) => { - const spaceName = normalizedHallName(space.name) - return spaceName && (placeName.includes(spaceName) || spaceName.includes(placeName)) - }) + const candidates = spaces + .map((space) => { + const spaceName = normalizedHallName(space.name) + if (!spaceName) return null + + const exact = placeName === spaceName + const overlaps = exact || placeName.includes(spaceName) || spaceName.includes(placeName) + if (!overlaps) return null + + return { + space, + exact, + specificity: spaceName.length + } + }) + .filter(Boolean) + .sort((a, b) => { + if (a.exact !== b.exact) return a.exact ? -1 : 1 + return b.specificity - a.specificity + }) + + return candidates[0]?.space } const normalizePositionSource = (source) => { @@ -101,8 +119,9 @@ const normalizeSpaceCenter = (space) => normalizePositionSource(space.center || const createHallPoiId = (space, place, fallbackIndex) => { if (space?.id) return `hall-${space.id}` const ownerName = normalizedHallName(placeHallName(place)) - if (ownerName) return `hall-place-${ownerName}` - return `hall-place-${place.id || fallbackIndex + 1}` + const floorId = normalizedHallName(String(place.floorId || 'unknown-floor')) + if (ownerName) return `hall-place-${floorId}-${ownerName}` + return `hall-place-${floorId}-${place.id || fallbackIndex + 1}` } const toHallPois = (spaces, navigablePlaces, fallbackFloorId) => { @@ -166,42 +185,51 @@ const toHallPois = (spaces, navigablePlaces, fallbackFloorId) => { return Array.from(halls.values()) } +const createHallPoiDiagnostics = (spaces, navigablePlaces, hallPois) => { + const hallSpaces = spaces.filter(isEligiblePublicExhibitionSpace) + const hallPlaceCount = navigablePlaces.filter(isExhibitionHallNavigablePlace).length + const hallPoiIds = new Set(hallPois.map((poi) => poi.id)) + + return { + skippedSpaceCount: hallSpaces.filter((space) => !hallPoiIds.has(`hall-${space.id}`)).length, + skippedPlaceCount: Math.max(hallPlaceCount - hallPois.reduce((count, poi) => count + (poi.entrances?.length || 0), 0), 0) + } +} + const spaces = [ { id: 1, name: '展厅1宇宙厅', type: 'exhibition_hall', floorId: 'B2', center: { x: 10, y: 0, z: 20 } }, { id: 2, name: '巨幕影院', type: 'theater', floorId: 'L1', center: { x: 30, y: 0, z: 40 } }, { id: 3, name: '博物馆之友活动室', type: 'education_activity', floorId: 'L4', center: { x: 50, y: 0, z: 60 } }, { id: 4, name: '展览坡道', type: 'ramp', floorId: 'L1.5', center: { x: 70, y: 0, z: 80 } }, { id: 5, name: '茶水间', type: 'service', floorId: 'L1', center: { x: 90, y: 0, z: 100 } }, - // Finding 3: unmatched exhibition space with center coordinates emits fallback POI { id: 6, name: '自然科学展厅', type: 'exhibition_hall', floorId: 'L2', center: { x: 110, y: 0, z: 120 } }, - // Finding 4: space that will match when entrance has no coordinates { id: 7, name: '地球科学展厅', type: 'exhibition_hall', floorId: 'L3', center: { x: 130, y: 0, z: 140 } }, - // Finding 1: space matched by entrance with nodeId for sourceConfidence assertion - { id: 8, name: '古生物展厅', type: 'exhibition_hall', floorId: 'L2', center: { x: 150, y: 0, z: 160 } } + { id: 8, name: '古生物展厅', type: 'exhibition_hall', floorId: 'L2', center: { x: 150, y: 0, z: 160 } }, + { id: 9, name: '古生物展厅A', type: 'exhibition_hall', floorId: 'L2', center: { x: 170, y: 0, z: 180 } }, + { id: 10, name: '星空展厅', type: 'exhibition_hall', floorId: 'L5', center: { x: 190, y: 0, z: 200 } } ] const places = [ { id: 101, name: '展厅1宇宙厅 出入口 1', ownerName: '展厅1宇宙厅', floorId: 'B2', position: { x: 11, y: 0, z: 21 }, nodeId: 'n101' }, { id: 102, name: '巨幕影院 出入口 1', ownerName: '巨幕影院', floorId: 'L1', position: { x: 31, y: 0, z: 41 }, nodeId: 'n102' }, - // Finding 3: entrance with missing coordinates falls back to matched space.center (creates POI with space-center sourceConfidence) { id: 104, name: '地球科学展厅 出入口', ownerName: '地球科学展厅', floorId: 'L3' }, - // Finding 1: entrance-backed hall (with nodeId) must have backend-sgs-sdk-hall-entrance { id: 105, name: '古生物展厅 出入口', ownerName: '古生物展厅', floorId: 'L2', position: { x: 151, y: 0, z: 161 }, nodeId: 'n105' }, - // Finding 2: entrance WITHOUT nodeId that matches a space should use space-center semantics (no position provided) { id: 107, name: '自然科学展厅 出入口', ownerName: '自然科学展厅', floorId: 'L2' }, - // Finding 2: place with coordinates but no nodeId should match space center, not entrance position { id: 108, name: '自然科学展厅【主】出入口1', ownerName: '自然科学展厅', floorId: 'L2', position: { x: 111, y: 0, z: 121 } }, - // Finding 1: later entrance with BOTH nodeId and position upgrades POI from space-center to entrance-backed - { id: 109, name: '地球 科学 展厅 (主)出入口 1', ownerName: '地球科学展厅', floorId: 'L3', position: { x: 131, y: 0, z: 141 }, nodeId: 'n109' } + { id: 109, name: '地球 科学 展厅 (主)出入口 1', ownerName: '地球科学展厅', floorId: 'L3', position: { x: 131, y: 0, z: 141 }, nodeId: 'n109' }, + { id: 110, name: '古生物展厅A 出入口', ownerName: '古生物展厅A', floorId: 'L2', position: { x: 171, y: 0, z: 181 }, nodeId: 'n110' }, + { id: 111, name: '共享展厅 出入口', ownerName: '共享展厅', floorId: 'L6', position: { x: 210, y: 0, z: 220 }, nodeId: 'n111' }, + { id: 112, name: '共享展厅 出入口', ownerName: '共享展厅', floorId: 'L7', position: { x: 230, y: 0, z: 240 }, nodeId: 'n112' } ] const result = toHallPois(spaces, places, 'B2') +const diagnostics = createHallPoiDiagnostics(spaces, places, result) const byName = new Map(result.map((poi) => [poi.name, poi])) const byId = new Map(result.map((poi) => [poi.id, poi])) -// All fixtures merge into existing POIs, maintaining 7 unique halls (verified by ID-based map) -assert.equal(result.length, 7, 'result contains 7 POIs') -assert.equal(byId.size, 7, 'all POI IDs are unique') +// All fixtures merge into existing POIs, maintaining 11 unique halls (verified by ID-based map) +assert.equal(result.length, 11, 'result contains 11 POIs') +assert.equal(byId.size, 11, 'all POI IDs are unique') assert.deepEqual(byId.get('hall-1').positionGltf, [11, 0, 21]) assert.deepEqual(byId.get('hall-2').positionGltf, [31, 0, 41]) assert.deepEqual(byId.get('hall-3').positionGltf, [50, 0, 60]) @@ -263,7 +291,16 @@ assert.equal(earthSciencePoi.entrances.length, 2, 'both place 104 and 109 entran assert.equal(earthSciencePoi.entrances.some((e) => e.name === '地球科学展厅 出入口'), true, 'place 104 no-coordinate entrance recorded') assert.equal(earthSciencePoi.entrances.some((e) => e.name === '地球 科学 展厅 (主)出入口 1'), true, 'place 109 normalized entrance recorded') -// Issue 4: keep unmatched exhibition space fallback coverage -assert.equal(byName.has('自然科学展厅'), true, 'unmatched space-center POI still emitted') +// Finding 2 re-review: prefer exact match over shorter substring matches +assert.equal(byId.get('hall-9').name, '古生物展厅A', 'exact normalized match beats overlapping shorter hall names') +assert.deepEqual(byId.get('hall-9').positionGltf, [171, 0, 181], 'exact matched hall uses its own entrance position') + +// Finding 3 re-review: place-only fallback IDs include floor identity to avoid collisions across floors +assert.equal(byId.has('hall-place-L6-共享展厅'), true, 'floor-scoped fallback ID created for L6 place-only hall') +assert.equal(byId.has('hall-place-L7-共享展厅'), true, 'floor-scoped fallback ID created for L7 place-only hall') +assert.notEqual(byId.get('hall-place-L6-共享展厅').id, byId.get('hall-place-L7-共享展厅').id, 'same-named place-only halls on different floors stay distinct') + +// Finding 1 re-review: diagnostics count skipped spaces by missing hall-{space.id}, not by total hall POI count +assert.equal(diagnostics.skippedSpaceCount, 0, 'place-only halls do not inflate skipped-space diagnostics when all eligible spaces have hall-{space.id}') +assert.equal(diagnostics.skippedPlaceCount, 0, 'all hall places are represented by entrances') -console.log('SGS hall POI adapter smoke check passed') diff --git a/src/data/adapters/sgsSdkGuideAdapter.ts b/src/data/adapters/sgsSdkGuideAdapter.ts index dae692a..c00e67e 100644 --- a/src/data/adapters/sgsSdkGuideAdapter.ts +++ b/src/data/adapters/sgsSdkGuideAdapter.ts @@ -367,10 +367,32 @@ const findMatchedHallSpace = ( const placeName = normalizedHallName(placeHallName(place)) if (!placeName) return undefined - return spaces.find((space) => { - const spaceName = normalizedHallName(space.name) - return spaceName && (placeName.includes(spaceName) || spaceName.includes(placeName)) - }) + const rankedMatches = spaces + .map((space) => { + const spaceName = normalizedHallName(space.name) + if (!spaceName) return null + + const exact = placeName === spaceName + const overlaps = exact || placeName.includes(spaceName) || spaceName.includes(placeName) + if (!overlaps) return null + + return { + space, + exact, + specificity: spaceName.length + } + }) + .filter((match): match is { + space: SgsSpacePayload + exact: boolean + specificity: number + } => Boolean(match)) + .sort((left, right) => { + if (left.exact !== right.exact) return left.exact ? -1 : 1 + return right.specificity - left.specificity + }) + + return rankedMatches[0]?.space } const floorLabelForSgs = ( @@ -416,9 +438,10 @@ const createHallPoiId = ( if (spaceId) return `hall-${spaceId}` const ownerName = normalizedHallName(placeHallName(place)) - if (ownerName) return `hall-place-${ownerName}` + const floorIdentity = normalizedHallName(stringifyId(place.floorId) || place.floorCode || 'unknown-floor') + if (ownerName) return `hall-place-${floorIdentity}-${ownerName}` - return `hall-place-${stringifyId(place.id) || fallbackIndex + 1}` + return `hall-place-${floorIdentity}-${stringifyId(place.id) || fallbackIndex + 1}` } const createSpaceFallbackHallPoi = ( @@ -556,8 +579,10 @@ export const createSgsHallPoiDiagnostics = ( navigablePlaces: SgsNavigablePlacePayload[], hallPois: MuseumPoi[] ): SgsHallPoiDiagnostics => { - const eligibleSpaceCount = spaces.filter(isExhibitionHallSpace).length + const eligibleHallSpaces = spaces.filter(isExhibitionHallSpace) + const eligibleSpaceCount = eligibleHallSpaces.length const hallPlaceCount = navigablePlaces.filter(isExhibitionHallNavigablePlace).length + const hallPoiIds = new Set(hallPois.map((poi) => poi.id)) const hallPoiWithPositionCount = hallPois.filter((poi) => Boolean(poi.positionGltf)).length return { @@ -567,7 +592,7 @@ export const createSgsHallPoiDiagnostics = ( hallPlaceCount, hallPoiCount: hallPois.length, hallPoiWithPositionCount, - skippedSpaceCount: Math.max(eligibleSpaceCount - hallPois.length, 0), + skippedSpaceCount: eligibleHallSpaces.filter((space) => !hallPoiIds.has(`hall-${stringifyId(space.id)}`)).length, skippedPlaceCount: Math.max(hallPlaceCount - hallPois.reduce((count, poi) => count + (poi.entrances?.length || 0), 0), 0) } }