# SGS Hall POI Display Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Make SGS SDK mode generate and display exhibition hall/public exhibition-space POIs reliably in the single-floor 3D model, with diagnostics for silent data failures. **Architecture:** Keep the existing provider/adapter/repository/renderer boundary. Expand SGS `spaces + navigablePlaces` adaptation inside `sgsSdkGuideAdapter.ts`, keep `GuideModelRepository` as the model-POI source, and keep `ThreeMap` consuming `GuideRenderPoi` without reading SGS payloads directly. **Tech Stack:** Vue 3, uni-app H5, TypeScript 5.3, Three.js 0.163, pnpm, `vue-tsc`, ESLint. ## Global Constraints - Default runtime target is H5; do not add mini-program/mp-weixin work. - Pages and presentation components must not import or parse `/app-api/gis/sdk/*` response shapes. - Keep guide capability wording as “位置预览” / “查看位置”; do not claim certified navigation. - `spaces` represent museum spaces; `navigablePlaces` represent entrances/reachable destinations; route planning can only use entrance route nodes. - Do not add dependencies. - Avoid unrelated refactors and broad data cleanup. - Validation baseline after code changes: `pnpm type-check`, `pnpm lint`, `pnpm build:h5`. --- ## File Structure - Modify: `src/data/adapters/sgsSdkGuideAdapter.ts` - Owns SGS payload-to-domain conversion. - Add public helpers for eligible exhibition-space classification, hall-place matching, space center fallback, and hall POI diagnostics. - Keep raw SGS fields contained in this adapter. - Modify: `src/repositories/GuideModelRepository.ts` - Owns model render package and floor render POI loading. - Add development diagnostics around failed SGS floor data requests and hall POI generation counts. - Continue returning only `GuideRenderPoi[]` to `ThreeMap`. - Modify: `src/components/map/ThreeMap.vue` - Owns 3D presentation and POI visibility. - Confirm floor/detail mode does not hide valid hall POIs through distance/limit pruning. If necessary, add a focused guard so hall POIs remain visible in floor mode only. - Create: `scripts/check-sgs-hall-poi-adapter.mjs` - Node-based smoke script for the adapter algorithm using representative SGS payload fixtures. - It does not import TypeScript project files, so no new test runner is required. - It verifies expected hall/public-space POI outputs, classification behavior, entrance-vs-space-center source semantics, entrance-anchor upgrade behavior, and ID-based assertions. - Reference: `docs/superpowers/specs/2026-06-29-sgs-hall-poi-display-design.md` - Implementation must match this spec. --- ### Task 1: Adapter classification and fallback smoke coverage **Files:** - Create: `scripts/check-sgs-hall-poi-adapter.mjs` **Interfaces:** - Consumes: No project imports. Uses local JS functions mirroring the intended adapter behavior. - Produces: A runnable smoke command: `node scripts/check-sgs-hall-poi-adapter.mjs`. - [ ] **Step 1: Create the failing smoke script** Create `scripts/check-sgs-hall-poi-adapter.mjs` with this exact content: ```js const assert = require('node:assert/strict') const exhibitionSpaceTypeWhitelist = new Set([ 'exhibition_hall', 'theater', 'education_activity', 'ramp' ]) const exhibitionSpaceKeywords = [ '展厅', '临展', '展览', '影院', '球幕', '巨幕', '报告厅', '活动室', '科普', '实验室', '展览坡道', '宇宙', '地球', '演化', '恐龙', '人类', '生物', '生态', '家园' ] const normalizedText = (value) => (value || '').trim() const normalizedHallName = (value) => normalizedText(value).replace(/[\s()()【】\[\]_-]/g, '') const hasExhibitionKeyword = (value) => exhibitionSpaceKeywords.some((keyword) => normalizedText(value).includes(keyword)) const isEligiblePublicExhibitionSpace = (space) => { const type = normalizedText(space.type).toLowerCase() const name = normalizedText(space.name) if (type === 'exhibition_hall') return true if (type === 'theater') return hasExhibitionKeyword(name) if (type === 'education_activity') return hasExhibitionKeyword(name) if (type === 'ramp') return name.includes('展览坡道') return false } const placeHallName = (place) => ( normalizedText(place.ownerName) || normalizedText(place.name) .replace(/(?:主)?(?:出入口|入口|出口|门)\s*[A-Za-z0-9一二三四五六七八九十号-]*$/u, '') .trim() || normalizedText(place.name) ) const isExhibitionHallNavigablePlace = (place) => { const searchableText = [ place.name, place.ownerName, place.category, place.type, place.typeCode, place.typeName ] .map(normalizedText) .filter(Boolean) .join(' ') return hasExhibitionKeyword(searchableText) } 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 normalizePositionSource = (source) => { const x = Number(source?.x) const y = Number(source?.y) const z = Number(source?.z) if (![x, y, z].every(Number.isFinite)) return undefined return [x, y, z] } const normalizePlacePosition = (place) => normalizePositionSource( place.position ? { x: place.position.x, y: place.position.y ?? 0, z: place.position.z } : { x: place.x, y: place.y ?? 0, z: place.z } ) 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 toHallPois = (spaces, navigablePlaces, fallbackFloorId) => { const hallSpaces = spaces.filter(isEligiblePublicExhibitionSpace) const hallPlaces = navigablePlaces.filter(isExhibitionHallNavigablePlace) const halls = new Map() hallPlaces.forEach((place, index) => { const matchedSpace = findMatchedHallSpace(place, hallSpaces) const entrancePosition = normalizePlacePosition(place) const fallbackPosition = matchedSpace ? normalizeSpaceCenter(matchedSpace) : undefined const position = entrancePosition || fallbackPosition const poiId = createHallPoiId(matchedSpace, place, index) if (!position) return const existing = halls.get(poiId) if (existing) { existing.entrances.push({ name: place.name, positionGltf: entrancePosition }) return } halls.set(poiId, { id: poiId, name: matchedSpace?.name || placeHallName(place), floorId: String(place.floorId || matchedSpace?.floorId || fallbackFloorId), primaryCategory: 'exhibition_hall', positionGltf: position, sourceConfidence: entrancePosition ? 'backend-sgs-sdk-hall-entrance' : 'backend-sgs-sdk-space-center', entrances: [{ name: place.name, positionGltf: entrancePosition }] }) }) hallSpaces.forEach((space, index) => { const poiId = `hall-${space.id}` if (halls.has(poiId)) return const position = normalizeSpaceCenter(space) if (!position) return halls.set(poiId, { id: poiId, name: space.name, floorId: String(space.floorId || fallbackFloorId), primaryCategory: 'exhibition_hall', positionGltf: position, sourceConfidence: 'backend-sgs-sdk-space-center', entrances: [] }) }) return Array.from(halls.values()) } 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 } } ] 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' } ] const result = toHallPois(spaces, places, 'B2') const byName = new Map(result.map((poi) => [poi.name, poi])) assert.equal(result.length, 4) assert.deepEqual(byName.get('展厅1宇宙厅').positionGltf, [11, 0, 21]) assert.deepEqual(byName.get('巨幕影院').positionGltf, [31, 0, 41]) assert.deepEqual(byName.get('博物馆之友活动室').positionGltf, [50, 0, 60]) assert.deepEqual(byName.get('展览坡道').positionGltf, [70, 0, 80]) assert.equal(byName.has('茶水间'), false) assert.equal(byName.get('博物馆之友活动室').sourceConfidence, 'backend-sgs-sdk-space-center') console.log('SGS hall POI adapter smoke check passed') ``` - [ ] **Step 2: Run the smoke script and verify current intended behavior is captured** Run: ```powershell node scripts/check-sgs-hall-poi-adapter.mjs ``` Expected output: ```text SGS hall POI adapter smoke check passed ``` - [ ] **Step 3: Commit the smoke coverage** ```powershell git add scripts/check-sgs-hall-poi-adapter.mjs docs/superpowers/specs/2026-06-29-sgs-hall-poi-display-design.md git commit -m "test: cover SGS hall POI display rules" ``` If the working tree already contains unrelated user changes, commit only the two listed files. --- ### Task 2: Extend SGS hall-space adapter behavior **Files:** - Modify: `src/data/adapters/sgsSdkGuideAdapter.ts:122-428` - Test: `scripts/check-sgs-hall-poi-adapter.mjs` **Interfaces:** - Consumes: `SgsSpacePayload.center?: SgsPositionPayload | null`, `SgsNavigablePlacePayload`, and existing `toMuseumHallPoisFromSgs(spaces, navigablePlaces, floors, fallbackFloorId): MuseumPoi[]`. - Produces: - `isExhibitionHallSpace(space: SgsSpacePayload): boolean` now returns true for public exhibition spaces. - `toMuseumHallPoisFromSgs(...)` generates hall POIs from entrance coordinates and space-center fallback. - `createSgsHallPoiDiagnostics(spaces, navigablePlaces, hallPois): SgsHallPoiDiagnostics` for repository diagnostics. - [ ] **Step 1: Add classification constants and diagnostics types** In `src/data/adapters/sgsSdkGuideAdapter.ts`, replace the current `exhibitionHallKeywords` block at lines around `263-274` with: ```ts const exhibitionSpaceTypeWhitelist = new Set([ 'exhibition_hall', 'theater', 'education_activity', 'ramp' ]) const exhibitionHallKeywords = [ '展厅', '临展', '展览', '影院', '球幕', '巨幕', '报告厅', '活动室', '科普', '实验室', '展览坡道', '宇宙', '地球', '演化', '恐龙', '人类', '生物', '生态', '家园' ] export interface SgsHallPoiDiagnostics { spaceCount: number eligibleSpaceCount: number navigablePlaceCount: number hallPlaceCount: number hallPoiCount: number hallPoiWithPositionCount: number skippedSpaceCount: number skippedPlaceCount: number } ``` - [ ] **Step 2: Add keyword and center helpers** Immediately after the constants from Step 1, add: ```ts const hasExhibitionKeyword = (value?: string | null) => ( exhibitionHallKeywords.some((keyword) => normalizedText(value).includes(keyword)) ) const searchableSpaceText = (space: SgsSpacePayload) => [ space.name, space.type, space.sourceNodeName ] .map((value) => normalizedText(value)) .filter(Boolean) .join(' ') const normalizeSpaceCenter = (space: SgsSpacePayload): [number, number, number] | undefined => normalizePositionSource( space.center || { x: undefined, y: undefined, z: undefined } ) ``` - [ ] **Step 3: Replace `isExhibitionHallSpace` implementation** Replace the existing function: ```ts export const isExhibitionHallSpace = (space: SgsSpacePayload) => ( normalizedText(space.type).toLowerCase() === 'exhibition_hall' ) ``` with: ```ts export const isExhibitionHallSpace = (space: SgsSpacePayload) => { const type = normalizedText(space.type).toLowerCase() const name = normalizedText(space.name) const searchableText = searchableSpaceText(space) if (!exhibitionSpaceTypeWhitelist.has(type)) return false if (type === 'exhibition_hall') return true if (type === 'ramp') return name.includes('展览坡道') return hasExhibitionKeyword(searchableText) } ``` This keeps non-public service spaces such as 茶水间 out of hall POIs. - [ ] **Step 4: Update `isExhibitionHallNavigablePlace` to use the shared keyword helper** Inside `isExhibitionHallNavigablePlace`, replace: ```ts return exhibitionHallKeywords.some((keyword) => searchableText.includes(keyword)) ``` with: ```ts return hasExhibitionKeyword(searchableText) ``` - [ ] **Step 5: Add space fallback hall POI helper** After `createHallPoiId(...)`, add: ```ts const createSpaceFallbackHallPoi = ( space: SgsSpacePayload, floors: SgsSdkFloorSummaryPayload[], fallbackFloorId: string ): MuseumPoi | null => { const floorId = stringifyId(space.floorId) || fallbackFloorId const position = normalizeSpaceCenter(space) const hallId = stringifyId(space.id) if (!hallId || !normalizedText(space.name) || !position) return null return { id: `hall-${hallId}`, name: normalizedText(space.name), floorId, floorLabel: floorLabelForSgs(floorId, floors), primaryCategory: hallCategory, categories: [ hallCategory ], positionGltf: position, sourceObjectName: space.sourceNodeName || undefined, sourceConfidence: 'backend-sgs-sdk-space-center', navigationReadiness: '位置预览', accessible: false, kind: 'hall', hallId, hallName: normalizedText(space.name), spaceId: hallId, sourceSpaceId: hallId, entrances: [] } } ``` - [ ] **Step 6: Update `toMuseumHallPoisFromSgs` to use fallback positions and space-only POIs** Inside `toMuseumHallPoisFromSgs`, keep the existing function signature. Replace the body with: ```ts const hallSpaces = spaces.filter(isExhibitionHallSpace) const hallPlaces = navigablePlaces.filter(isExhibitionHallNavigablePlace) const halls = new Map() hallPlaces.forEach((place, index) => { const matchedSpace = findMatchedHallSpace(place, hallSpaces) const entrance = createHallEntrance(place, floors, fallbackFloorId) const fallbackPosition = matchedSpace ? normalizeSpaceCenter(matchedSpace) : undefined const floorId = entrance.floorId || stringifyId(matchedSpace?.floorId) || fallbackFloorId const hallId = stringifyId(matchedSpace?.id) || undefined const poiId = createHallPoiId(matchedSpace, place, index) const hallName = normalizedText(matchedSpace?.name) || placeHallName(place) || entrance.name const existing = halls.get(poiId) if (existing) { existing.entrances = [ ...(existing.entrances || []), entrance ] if (!existing.positionGltf && (entrance.positionGltf || fallbackPosition)) { existing.positionGltf = entrance.positionGltf || fallbackPosition existing.floorId = floorId existing.floorLabel = entrance.floorLabel existing.sourceObjectName = entrance.sourceObjectName || matchedSpace?.sourceNodeName || undefined existing.sourcePlaceId = entrance.sourcePlaceId } return } const positionGltf = entrance.positionGltf || fallbackPosition if (!positionGltf) return halls.set(poiId, { id: poiId, name: hallName, floorId, floorLabel: entrance.floorLabel, primaryCategory: hallCategory, categories: [ hallCategory, hallEntranceCategory ], positionGltf, sourceObjectName: entrance.sourceObjectName || matchedSpace?.sourceNodeName || undefined, sourceConfidence: entrance.positionGltf ? 'backend-sgs-sdk-hall-entrance' : 'backend-sgs-sdk-space-center', navigationReadiness: '位置预览', accessible: false, kind: 'hall', hallId, hallName, spaceId: hallId, sourcePlaceId: entrance.sourcePlaceId, sourceSpaceId: hallId, entrances: [entrance] }) }) hallSpaces.forEach((space) => { const hallId = stringifyId(space.id) if (!hallId || halls.has(`hall-${hallId}`)) return const fallbackPoi = createSpaceFallbackHallPoi(space, floors, fallbackFloorId) if (fallbackPoi) { halls.set(fallbackPoi.id, fallbackPoi) } }) return Array.from(halls.values()) .filter((poi) => poi.id && poi.name && poi.positionGltf) ``` - [ ] **Step 7: Add diagnostics helper** After `toMuseumHallPoisFromSgs`, add: ```ts export const createSgsHallPoiDiagnostics = ( spaces: SgsSpacePayload[], navigablePlaces: SgsNavigablePlacePayload[], hallPois: MuseumPoi[] ): SgsHallPoiDiagnostics => { const eligibleSpaceCount = spaces.filter(isExhibitionHallSpace).length const hallPlaceCount = navigablePlaces.filter(isExhibitionHallNavigablePlace).length const hallPoiWithPositionCount = hallPois.filter((poi) => Boolean(poi.positionGltf)).length return { spaceCount: spaces.length, eligibleSpaceCount, navigablePlaceCount: navigablePlaces.length, hallPlaceCount, hallPoiCount: hallPois.length, hallPoiWithPositionCount, skippedSpaceCount: Math.max(eligibleSpaceCount - hallPois.length, 0), skippedPlaceCount: Math.max(hallPlaceCount - hallPois.reduce((count, poi) => count + (poi.entrances?.length || 0), 0), 0) } } ``` - [ ] **Step 8: Run the adapter smoke script** Run: ```powershell node scripts/check-sgs-hall-poi-adapter.mjs ``` Expected output: ```text SGS hall POI adapter smoke check passed ``` - [ ] **Step 9: Run type-check for adapter changes** Run: ```powershell pnpm type-check ``` Expected: exits 0. If it fails, fix TypeScript errors in `sgsSdkGuideAdapter.ts` only before continuing. - [ ] **Step 10: Commit adapter behavior** ```powershell git add src/data/adapters/sgsSdkGuideAdapter.ts scripts/check-sgs-hall-poi-adapter.mjs git commit -m "feat: expand SGS hall POI adaptation" ``` --- ### Task 3: Add SGS model POI diagnostics in repository **Files:** - Modify: `src/repositories/GuideModelRepository.ts:14-244` - Test: `scripts/check-sgs-hall-poi-adapter.mjs` **Interfaces:** - Consumes: `createSgsHallPoiDiagnostics(spaces, navigablePlaces, hallPois)` from `sgsSdkGuideAdapter.ts`. - Produces: Development-only console warnings for failed SGS floor data endpoints and suspicious empty hall POI generation. - [ ] **Step 1: Update imports** In `src/repositories/GuideModelRepository.ts`, replace: ```ts import { formatSgsFloorLabel, toMuseumHallPoisFromSgs, toMuseumPoiFromSgs } from '@/data/adapters/sgsSdkGuideAdapter' ``` with: ```ts import { createSgsHallPoiDiagnostics, formatSgsFloorLabel, toMuseumHallPoisFromSgs, toMuseumPoiFromSgs } from '@/data/adapters/sgsSdkGuideAdapter' ``` - [ ] **Step 2: Add development warning helper** After `isSharedSgsModelAsset(...)`, add: ```ts const warnSgsGuideModelDiagnostics = ( message: string, payload: Record ) => { if (!import.meta.env.DEV) return console.warn(`[SGS guide model] ${message}`, payload) } ``` - [ ] **Step 3: Add safe request helper inside `loadFloorPois`** Inside `SgsSdkGuideModelRepository.loadFloorPois`, immediately after: ```ts const floorId = String(matchedFloor.floorId) ``` add this local helper: ```ts const loadFloorData = async ( endpoint: 'pois' | 'spaces' | 'navigablePlaces', loader: () => Promise ) => { try { return await loader() } catch (error) { warnSgsGuideModelDiagnostics('floor data request failed', { floorId, endpoint, error: error instanceof Error ? error.message : String(error) }) return [] } } ``` - [ ] **Step 4: Replace silent catch requests** Replace: ```ts const [pois, spaces, navigablePlaces] = await Promise.all([ this.provider.getFloorPois(floorId).catch(() => []), this.provider.getFloorSpaces(floorId).catch(() => []), this.provider.getNavigablePlaces(floorId).catch(() => []) ]) ``` with: ```ts const [pois, spaces, navigablePlaces] = await Promise.all([ loadFloorData('pois', () => this.provider.getFloorPois(floorId)), loadFloorData('spaces', () => this.provider.getFloorSpaces(floorId)), loadFloorData('navigablePlaces', () => this.provider.getNavigablePlaces(floorId)) ]) ``` - [ ] **Step 5: Add hall generation diagnostics** Replace: ```ts const ordinaryPois = pois.map((poi) => toSgsRenderPoi(toMuseumPoiFromSgs(poi, manifest.floors))) const hallPois = toMuseumHallPoisFromSgs(spaces, navigablePlaces, manifest.floors, floorId) .map(toSgsRenderPoi) return dedupeRenderPoisById([ ...hallPois, ...ordinaryPois ]) .filter((poi) => Array.isArray(poi.positionGltf) && poi.positionGltf.length === 3) ``` with: ```ts const ordinaryPois = pois.map((poi) => toSgsRenderPoi(toMuseumPoiFromSgs(poi, manifest.floors))) const museumHallPois = toMuseumHallPoisFromSgs(spaces, navigablePlaces, manifest.floors, floorId) const hallDiagnostics = createSgsHallPoiDiagnostics(spaces, navigablePlaces, museumHallPois) if (hallDiagnostics.eligibleSpaceCount > 0 && hallDiagnostics.hallPoiWithPositionCount === 0) { warnSgsGuideModelDiagnostics('eligible hall spaces produced no renderable hall POIs', { floorId, ...hallDiagnostics }) } const hallPois = museumHallPois.map(toSgsRenderPoi) const renderPois = dedupeRenderPoisById([ ...hallPois, ...ordinaryPois ]) .filter((poi) => Array.isArray(poi.positionGltf) && poi.positionGltf.length === 3) if (hallDiagnostics.hallPoiWithPositionCount > 0 && !renderPois.some((poi) => poi.primaryCategory === 'exhibition_hall')) { warnSgsGuideModelDiagnostics('render POI filter removed all hall POIs', { floorId, ...hallDiagnostics, renderPoiCount: renderPois.length }) } return renderPois ``` - [ ] **Step 6: Run smoke and type-check** Run: ```powershell node scripts/check-sgs-hall-poi-adapter.mjs pnpm type-check ``` Expected: ```text SGS hall POI adapter smoke check passed ``` and `pnpm type-check` exits 0. - [ ] **Step 7: Commit repository diagnostics** ```powershell git add src/repositories/GuideModelRepository.ts src/data/adapters/sgsSdkGuideAdapter.ts scripts/check-sgs-hall-poi-adapter.mjs git commit -m "chore: expose SGS hall POI diagnostics" ``` --- ### Task 4: Confirm floor-mode hall POI visibility **Files:** - Modify: `src/components/map/ThreeMap.vue:650-749` only if source inspection shows floor mode can still hide hall POIs. - Test: `scripts/check-sgs-hall-poi-adapter.mjs` **Interfaces:** - Consumes: `RenderPoi.primaryCategory === 'exhibition_hall'` / `'exhibition_hall_entrance'`. - Produces: In floor/detail mode, valid hall POIs are not hidden by distance/limit pruning. - [ ] **Step 1: Inspect current floor visibility behavior** Open `src/components/map/ThreeMap.vue` around `getPoiVisibilityTier` and confirm this current behavior exists: ```ts const getPoiVisibilityTier = (): PoiVisibilityTier => { if (activeView.value === 'floor') return 'full' if (!controls || !activeModel) return 'full' ``` If this is still present, no code change is required for floor-mode pruning because `full` makes `getPoiVisibilityLimit()` return infinity and `getPoiScreenSpacing()` return 0. - [ ] **Step 2: Only if floor mode no longer returns `full`, add hall visibility guard** If Step 1 shows floor mode can be `tight` or `balanced`, update `shouldShowPoiAtDistance` to begin with: ```ts if (activeView.value === 'floor' && ( poi.primaryCategory === 'exhibition_hall' || poi.primaryCategory === 'exhibition_hall_entrance' )) { return true } ``` Do not add this guard if `activeView.value === 'floor'` already returns `full`. - [ ] **Step 3: Run type-check** Run: ```powershell pnpm type-check ``` Expected: exits 0. - [ ] **Step 4: Commit visibility confirmation or change** If no code change was needed, do not create an empty commit. Record the finding in the final implementation summary. If code changed, run: ```powershell git add src/components/map/ThreeMap.vue git commit -m "fix: keep hall POIs visible in floor mode" ``` --- ### Task 5: Final validation and handoff **Files:** - Modify: none unless validation reveals a failure in files changed by Tasks 2-4. - Test: whole changed scope. **Interfaces:** - Consumes: all previous task outputs. - Produces: Verified working tree state and final summary. - [ ] **Step 1: Run adapter smoke check** ```powershell node scripts/check-sgs-hall-poi-adapter.mjs ``` Expected: ```text SGS hall POI adapter smoke check passed ``` - [ ] **Step 2: Run type-check** ```powershell pnpm type-check ``` Expected: exits 0. - [ ] **Step 3: Run lint** ```powershell pnpm lint ``` Expected: exits 0. If warnings appear with exit 0, record them in the final summary. - [ ] **Step 4: Run H5 build** This writes `dist`, which is expected for build validation. ```powershell pnpm build:h5 ``` Expected: exits 0. - [ ] **Step 5: Inspect changed files** ```powershell git diff -- src/data/adapters/sgsSdkGuideAdapter.ts src/repositories/GuideModelRepository.ts src/components/map/ThreeMap.vue scripts/check-sgs-hall-poi-adapter.mjs docs/superpowers/specs/2026-06-29-sgs-hall-poi-display-design.md ``` Expected: diff is limited to the planned files and contains no unrelated refactor. - [ ] **Step 6: Commit final validation notes if a docs update was needed** If no docs changed after validation, do not commit. If validation required updating this plan or spec, commit only those docs: ```powershell git add docs/superpowers/specs/2026-06-29-sgs-hall-poi-display-design.md docs/superpowers/plans/2026-06-29-sgs-hall-poi-display.md git commit -m "docs: record SGS hall POI validation" ``` - [ ] **Step 7: Final response** Report: - Files changed. - How hall/public exhibition spaces are now classified. - Whether `ThreeMap` required a change. - Exact validation commands and outcomes. - Any skipped validation and why. --- ## Self-Review ### Spec coverage - Expanded `exhibition_hall`-only classification: Task 2. - Added theater / education_activity / ramp handling: Task 2. - Added entrance-first and space-center fallback: Task 2. - Added diagnostics for silent request/adaptation failure: Task 3. - Preserved ThreeMap boundary and floor-mode visibility: Task 4. - Validation commands and H5 build: Task 5. ### Placeholder scan This plan contains no TBD/TODO placeholders and every code-changing step includes exact code or exact conditional instructions. ### Type consistency - `createSgsHallPoiDiagnostics` is defined in Task 2 and imported in Task 3 with the same name. - `SgsHallPoiDiagnostics` fields match Task 3 diagnostic spreading. - `toMuseumHallPoisFromSgs` signature is unchanged for existing callers.