Files
frontend-miniapp/docs/superpowers/plans/2026-06-25-sgs-sdk-data-switch-viewmodels.md
2026-06-26 04:25:08 +08:00

871 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SGS SDK Data Switch ViewModel Convergence 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:** Complete the SGS SDK data-source switch by keeping `static/api/sdk` mode behavior intact while moving remaining page-level mapping into the existing adapter/view-model middle layers.
**Architecture:** Keep the existing split: providers/adapters map source payloads to domain models, repositories/use cases expose business operations, and `src/view-models/*` maps domain models into presentation props. The implementation does not rewrite the SGS provider, repository, renderer, or pages; it only closes remaining mapping leaks and adds observable mode diagnostics.
**Tech Stack:** uni-app H5, Vue 3 `<script setup>`, TypeScript, `vue-tsc`, ESLint, existing `GuideUseCase`, `GuideRepository`, `GuideMapShell`, `ThreeMap`, and `SgsMapRenderer`.
---
## Current State Summary
Already present and should be preserved:
```text
VITE_DATA_SOURCE_MODE=static -> StaticGuideRepository + ThreeMap
VITE_DATA_SOURCE_MODE=api -> SgsSdkGuideRepository + ThreeMap
VITE_DATA_SOURCE_MODE=sdk -> SgsSdkGuideRepository + SgsMapRenderer
```
Evidence from source audit:
- `src/config/dataSource.ts` supports `static | api | sdk` and centralizes SGS API/SDK config.
- `src/repositories/createGuideRepository.ts` selects `SgsSdkGuideRepository` for `api` and `sdk`.
- `src/components/navigation/GuideMapShell.vue` selects `SgsMapRenderer` only when `isSgsSdkMode()` is true.
- `src/data/adapters/sgsSdkGuideAdapter.ts` maps SGS payloads to `MuseumFloor`, `MuseumPoi`, diagnostics, and route readiness.
- `src/view-models/guideViewModels.ts` and `src/view-models/explainViewModels.ts` already exist, but `src/pages/index/index.vue` still owns some route/explain/card mapping.
## File Structure
Modify these files only:
- `src/view-models/guideViewModels.ts`
- Add guide presentation view models for selected POI cards and route-point picker options.
- Add conversion helpers so pages do not build route picker props from domain models inline.
- `src/view-models/explainViewModels.ts`
- Move explain hall selector item mapping from `src/pages/index/index.vue` into the view-model layer.
- `src/components/navigation/RoutePointPicker.vue`
- Consume `RoutePointOptionViewModel` from `src/view-models/guideViewModels.ts` and re-export it as `RoutePointOption` for existing imports.
- `src/components/navigation/RoutePlannerPanel.vue`
- Consume the same `RoutePointOption` alias exported by `RoutePointPicker.vue`; no behavior change.
- `src/components/explain/ExplainHallSelect.vue`
- Consume `ExplainHallSelectItem` from `src/view-models/explainViewModels.ts` instead of defining presentation item shape locally.
- `src/pages/index/index.vue`
- Replace inline mapping helpers with imports from view-models.
- Keep page state and user interactions in the page.
- Keep the existing `GuideUseCase`/`guideRouteUseCase` calls.
- `src/config/dataSource.ts`
- Add a small diagnostics helper that reports selected data source and renderer mode.
- `src/components/navigation/GuideMapShell.vue`
- Log the mode diagnostic once in dev/H5 so QA can confirm `static/api/sdk` mode without inspecting code.
Do not modify:
- `src/data/providers/sgsSdkApiProvider.ts`
- `src/data/adapters/sgsSdkGuideAdapter.ts`
- `src/repositories/GuideRepository.ts`
- `src/repositories/createGuideRepository.ts`
- `src/components/map/SgsMapRenderer.vue`
- `src/services/sgs/SgsMapService.ts`
- `src/services/sgs/SgsMapScriptLoader.ts`
---
### Task 1: Add Guide ViewModel Mappers
**Files:**
- Modify: `src/view-models/guideViewModels.ts`
- [ ] **Step 1: Add imports for route target and renderer POI types**
At the top of `src/view-models/guideViewModels.ts`, replace the existing imports with:
```ts
import type {
GuideLocationPreview,
GuideLocationPreviewPlan,
GuideRouteTarget,
MuseumFloor,
MuseumPoi
} from '@/domain/museum'
import type {
GuideRenderPoi
} from '@/domain/guideModel'
```
Expected: the existing `GuideLocationPreview`, `GuideLocationPreviewPlan`, and `MuseumPoi` usages still compile, and the file can now define route picker and selected POI card mappers.
- [ ] **Step 2: Add route point and selected POI card view-model interfaces**
After `TargetPoiFocusRequestViewModel`, add:
```ts
export interface RoutePointOptionViewModel {
poiId: string
name: string
floorId: string
floorLabel: string
categoryLabel?: string
}
export interface SelectedGuidePoiCardViewModel {
id: string
name: string
subtitle: string
collapsedSubtitle: string
}
```
Expected: these types become the presentation contract for `RoutePointPicker`, `RoutePlannerPanel`, and the selected POI card in `src/pages/index/index.vue`.
- [ ] **Step 3: Add mappers for route targets and selected POI cards**
After `toTargetFocusRequestViewModel`, add:
```ts
export const toRoutePointOptionViewModel = (target: GuideRouteTarget): RoutePointOptionViewModel => ({
poiId: target.poiId,
name: target.name,
floorId: target.floorId,
floorLabel: target.floorLabel,
categoryLabel: target.categoryLabel
})
export const toSelectedGuidePoiCardViewModel = (
poi: GuideRenderPoi,
floors: MuseumFloor[]
): SelectedGuidePoiCardViewModel => {
const floor = floors.find((item) => item.id === poi.floorId)
const floorLabel = floor?.label || poi.floorId
const categoryLabel = poi.primaryCategoryZh || '位置预览'
return {
id: poi.id,
name: poi.name,
subtitle: `${floorLabel} · ${categoryLabel}`,
collapsedSubtitle: '位置预览 · 点按展开操作'
}
}
```
Expected: `src/pages/index/index.vue` no longer needs to know how to format selected POI subtitles or route-point picker items.
- [ ] **Step 4: Run type-check for this isolated change**
Run:
```powershell
pnpm type-check
```
Expected: PASS. If it fails because imports are unused, continue to Task 2 and Task 5 before re-running.
- [ ] **Step 5: Commit if authorized**
Do not commit unless the user explicitly authorizes commits in this session. If authorized, run:
```powershell
git add src/view-models/guideViewModels.ts
git commit -m @'
refactor: add guide presentation view models
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'@
```
Expected: one commit containing only `src/view-models/guideViewModels.ts`.
---
### Task 2: Move Route Picker Presentation Type To ViewModel Layer
**Files:**
- Modify: `src/components/navigation/RoutePointPicker.vue`
- Modify: `src/components/navigation/RoutePlannerPanel.vue`
- [ ] **Step 1: Update RoutePointPicker type source**
In `src/components/navigation/RoutePointPicker.vue`, replace the local `RoutePointOption` interface with this import/export block immediately after `import { computed, ref, watch } from 'vue'`:
```ts
import type {
RoutePointOptionViewModel
} from '@/view-models/guideViewModels'
export type RoutePointOption = RoutePointOptionViewModel
```
Remove this old block from the same file:
```ts
export interface RoutePointOption {
poiId: string
name: string
floorId: string
floorLabel: string
categoryLabel?: string
}
```
Expected: `RoutePointPicker` still accepts the same prop shape, but the contract is owned by `src/view-models/guideViewModels.ts`.
- [ ] **Step 2: Confirm RoutePlannerPanel remains compatible**
Leave this import in `src/components/navigation/RoutePlannerPanel.vue` unchanged:
```ts
import RoutePointPicker, {
type RoutePointOption
} from '@/components/navigation/RoutePointPicker.vue'
```
Expected: because `RoutePointPicker.vue` re-exports the view-model alias, no RoutePlannerPanel behavior changes.
- [ ] **Step 3: Run type-check for picker type migration**
Run:
```powershell
pnpm type-check
```
Expected: PASS or only failures from later not-yet-updated page imports if Task 5 has not been applied.
- [ ] **Step 4: Commit if authorized**
Do not commit unless the user explicitly authorizes commits in this session. If authorized, run:
```powershell
git add src/components/navigation/RoutePointPicker.vue src/components/navigation/RoutePlannerPanel.vue
git commit -m @'
refactor: route picker uses guide view model type
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'@
```
Expected: one commit containing the route picker type ownership change.
---
### Task 3: Add Explain Hall Selector ViewModel Mapper
**Files:**
- Modify: `src/view-models/explainViewModels.ts`
- Modify: `src/components/explain/ExplainHallSelect.vue`
- [ ] **Step 1: Add ExplainHallSelectItem interface to explain view models**
In `src/view-models/explainViewModels.ts`, after `HallDetailViewModel`, add:
```ts
export interface ExplainHallSelectItem {
id: string
name: string
floorLabel?: string
image?: string
explainCount: number
searchText: string
}
```
Expected: the hall selector item presentation contract is no longer owned by the component file.
- [ ] **Step 2: Add hall-selector mapping helpers**
In `src/view-models/explainViewModels.ts`, after `toExplainSearchResultViewModel`, add:
```ts
const normalizeExplainKeyword = (value?: string) => (value || '').trim().toLowerCase()
export const toExplainHallSelectItems = (
halls: MuseumHall[],
exhibits: MuseumExhibit[]
): ExplainHallSelectItem[] => {
const countsByHallId = new Map<string, number>()
const countsByHallName = new Map<string, number>()
const keywordsByHallId = new Map<string, Set<string>>()
const keywordsByHallName = new Map<string, Set<string>>()
exhibits.forEach((exhibit) => {
const keywordParts = [
exhibit.name,
exhibit.hallName,
exhibit.floorLabel,
exhibit.description,
exhibit.guideTitle,
exhibit.guideText,
...(exhibit.tags || [])
].filter(Boolean) as string[]
if (exhibit.hallId) {
countsByHallId.set(exhibit.hallId, (countsByHallId.get(exhibit.hallId) || 0) + 1)
const keywords = keywordsByHallId.get(exhibit.hallId) || new Set<string>()
keywordParts.forEach((part) => keywords.add(part))
keywordsByHallId.set(exhibit.hallId, keywords)
}
if (exhibit.hallName) {
countsByHallName.set(exhibit.hallName, (countsByHallName.get(exhibit.hallName) || 0) + 1)
const keywords = keywordsByHallName.get(exhibit.hallName) || new Set<string>()
keywordParts.forEach((part) => keywords.add(part))
keywordsByHallName.set(exhibit.hallName, keywords)
}
})
return halls.map((hall) => {
const explainCount = countsByHallId.get(hall.id)
|| countsByHallName.get(hall.name)
|| hall.exhibitCount
|| 0
const keywordParts = [
hall.name,
hall.floorLabel,
hall.description,
hall.area,
...Array.from(keywordsByHallId.get(hall.id) || []),
...Array.from(keywordsByHallName.get(hall.name) || [])
].filter(Boolean) as string[]
return {
id: hall.id,
name: hall.name,
floorLabel: hall.floorLabel,
image: hall.image,
explainCount,
searchText: normalizeExplainKeyword(keywordParts.join(' '))
}
})
}
```
Expected: the exact logic currently in `src/pages/index/index.vue` is moved without behavior changes.
- [ ] **Step 3: Update ExplainHallSelect component type ownership**
In `src/components/explain/ExplainHallSelect.vue`, replace the local `ExplainHallSelectItem` interface with this import/export block after `import { computed } from 'vue'`:
```ts
import type {
ExplainHallSelectItem as ExplainHallSelectItemViewModel
} from '@/view-models/explainViewModels'
export type ExplainHallSelectItem = ExplainHallSelectItemViewModel
```
Remove this old interface from the same file:
```ts
export interface ExplainHallSelectItem {
id: string
name: string
floorLabel?: string
image?: string
explainCount: number
searchText: string
}
```
Expected: component props retain the same shape, but the presentation contract is defined in `src/view-models/explainViewModels.ts`.
- [ ] **Step 4: Run type-check for explain view-model migration**
Run:
```powershell
pnpm type-check
```
Expected: PASS or only failures from `src/pages/index/index.vue` until Task 5 is applied.
- [ ] **Step 5: Commit if authorized**
Do not commit unless the user explicitly authorizes commits in this session. If authorized, run:
```powershell
git add src/view-models/explainViewModels.ts src/components/explain/ExplainHallSelect.vue
git commit -m @'
refactor: move explain hall selector mapping to view models
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'@
```
Expected: one commit containing explain hall selector view-model ownership.
---
### Task 4: Add Data-Source Mode Diagnostics
**Files:**
- Modify: `src/config/dataSource.ts`
- Modify: `src/components/navigation/GuideMapShell.vue`
- [ ] **Step 1: Add diagnostics helper to dataSource config**
At the end of `src/config/dataSource.ts`, after `getSgsMapTargetOrigin`, add:
```ts
export const getGuideDataSourceDiagnostics = () => {
const usesSgsRepository = dataSourceConfig.mode === 'api' || dataSourceConfig.mode === 'sdk'
const usesSgsRenderer = dataSourceConfig.mode === 'sdk'
return {
mode: dataSourceConfig.mode,
guideRepository: usesSgsRepository ? 'SgsSdkGuideRepository' : 'StaticGuideRepository',
mapRenderer: usesSgsRenderer ? 'SgsMapRenderer' : 'ThreeMap',
apiBaseUrl: usesSgsRepository ? dataSourceConfig.sgsApiBaseUrl : '',
mapId: usesSgsRepository ? dataSourceConfig.sgsMapId : '',
sdkScriptUrl: usesSgsRenderer ? dataSourceConfig.sgsSdkScriptUrl : '',
sdkEngineUrl: usesSgsRenderer ? dataSourceConfig.sgsMapEngineUrl : ''
}
}
```
Expected: diagnostics are centralized with mode parsing and do not read raw env vars in page/component code.
- [ ] **Step 2: Import diagnostics helper in GuideMapShell**
In `src/components/navigation/GuideMapShell.vue`, change the H5 config import from:
```ts
import {
isSgsSdkMode
} from '@/config/dataSource'
```
to:
```ts
import {
getGuideDataSourceDiagnostics,
isSgsSdkMode
} from '@/config/dataSource'
```
Also change the Vue import from:
```ts
import { computed, ref } from 'vue'
```
to:
```ts
import { computed, onMounted, ref } from 'vue'
```
Expected: `GuideMapShell` can log diagnostics in H5 without changing mode-selection behavior.
- [ ] **Step 3: Log selected mode once in dev H5**
In `src/components/navigation/GuideMapShell.vue`, after `const useSgsMapRenderer = computed(() => isSgsSdkMode())`, add:
```ts
onMounted(() => {
if (!import.meta.env.DEV) return
const diagnostics = getGuideDataSourceDiagnostics()
console.info('[guide-data-source]', diagnostics)
})
```
Expected in dev console:
```text
[guide-data-source] { mode: 'sdk', guideRepository: 'SgsSdkGuideRepository', mapRenderer: 'SgsMapRenderer', ... }
```
Expected in production build: no diagnostic log.
- [ ] **Step 4: Run type-check for diagnostics change**
Run:
```powershell
pnpm type-check
```
Expected: PASS or only failures from `src/pages/index/index.vue` until Task 5 is applied.
- [ ] **Step 5: Commit if authorized**
Do not commit unless the user explicitly authorizes commits in this session. If authorized, run:
```powershell
git add src/config/dataSource.ts src/components/navigation/GuideMapShell.vue
git commit -m @'
feat: expose guide data source diagnostics
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'@
```
Expected: one commit containing diagnostics only.
---
### Task 5: Replace Remaining Page-Level Mapping In Index Page
**Files:**
- Modify: `src/pages/index/index.vue`
- [ ] **Step 1: Update imports**
In `src/pages/index/index.vue`, remove this import block:
```ts
import type {
RoutePointOption
} from '@/components/navigation/RoutePointPicker.vue'
import ExplainHallSelect, {
type ExplainHallSelectItem
} from '@/components/explain/ExplainHallSelect.vue'
```
Replace it with:
```ts
import type {
RoutePointOption
} from '@/components/navigation/RoutePointPicker.vue'
import ExplainHallSelect from '@/components/explain/ExplainHallSelect.vue'
```
Then replace the existing domain import:
```ts
import type {
GuideRouteResult,
GuideRouteTarget,
MuseumExhibit,
MuseumFloor
} from '@/domain/museum'
```
with:
```ts
import type {
GuideRouteResult,
MuseumFloor
} from '@/domain/museum'
```
Finally add this view-model import near the other app imports:
```ts
import {
toExplainHallSelectItems,
type ExplainHallSelectItem
} from '@/view-models/explainViewModels'
import {
toRoutePointOptionViewModel,
toSelectedGuidePoiCardViewModel
} from '@/view-models/guideViewModels'
```
Expected: page no longer imports `GuideRouteTarget` or `ExplainHallSelectItem` from presentation components.
- [ ] **Step 2: Replace selected POI subtitle computed with card view model**
In `src/pages/index/index.vue`, replace this computed block:
```ts
const selectedGuidePoiSubtitle = computed(() => {
const poi = selectedGuidePoi.value
if (!poi) return ''
const floor = guideFloors.value.find((item) => item.id === poi.floorId)
const floorLabel = floor?.label || poi.floorId
return `${floorLabel} · ${poi.primaryCategoryZh || '位置预览'}`
})
```
with:
```ts
const selectedGuidePoiCard = computed(() => (
selectedGuidePoi.value
? toSelectedGuidePoiCardViewModel(selectedGuidePoi.value, guideFloors.value)
: null
))
```
Expected: page still owns selected state, but no longer formats selected POI display text inline.
- [ ] **Step 3: Update selected POI card template bindings**
In the selected POI card template near the top of `src/pages/index/index.vue`, replace:
```vue
<text class="poi-card-title">{{ selectedGuidePoi.name }}</text>
<text class="poi-card-subtitle">位置预览 · 点按展开操作</text>
```
with:
```vue
<text class="poi-card-title">{{ selectedGuidePoiCard?.name }}</text>
<text class="poi-card-subtitle">{{ selectedGuidePoiCard?.collapsedSubtitle }}</text>
```
Also replace:
```vue
<text class="poi-card-title">{{ selectedGuidePoi.name }}</text>
<text class="poi-card-subtitle">{{ selectedGuidePoiSubtitle }}</text>
```
with:
```vue
<text class="poi-card-title">{{ selectedGuidePoiCard?.name }}</text>
<text class="poi-card-subtitle">{{ selectedGuidePoiCard?.subtitle }}</text>
```
Expected: user-facing text remains identical.
- [ ] **Step 4: Remove inline route point mapper**
Delete this function from `src/pages/index/index.vue`:
```ts
const toRoutePointOption = (target: GuideRouteTarget): RoutePointOption => ({
poiId: target.poiId,
name: target.name,
floorId: target.floorId,
floorLabel: target.floorLabel,
categoryLabel: target.categoryLabel
})
```
Expected: route-point option mapping comes from `src/view-models/guideViewModels.ts`.
- [ ] **Step 5: Use guide view-model route mapper**
In `src/pages/index/index.vue`, replace:
```ts
const options = targets.map(toRoutePointOption)
```
with:
```ts
const options: RoutePointOption[] = targets.map(toRoutePointOptionViewModel)
```
Expected: route planner point list remains unchanged.
- [ ] **Step 6: Remove inline explain hall mapper helpers**
Delete these functions from `src/pages/index/index.vue`:
```ts
const normalizeExplainKeyword = (value?: string) => (value || '').trim().toLowerCase()
const buildExplainHallItems = (
halls: Awaited<ReturnType<typeof explainUseCase.listHalls>>,
exhibits: MuseumExhibit[]
): ExplainHallSelectItem[] => {
const countsByHallId = new Map<string, number>()
const countsByHallName = new Map<string, number>()
const keywordsByHallId = new Map<string, Set<string>>()
const keywordsByHallName = new Map<string, Set<string>>()
exhibits.forEach((exhibit) => {
const keywordParts = [
exhibit.name,
exhibit.hallName,
exhibit.floorLabel,
exhibit.description,
exhibit.guideTitle,
exhibit.guideText,
...(exhibit.tags || [])
].filter(Boolean) as string[]
if (exhibit.hallId) {
countsByHallId.set(exhibit.hallId, (countsByHallId.get(exhibit.hallId) || 0) + 1)
const keywords = keywordsByHallId.get(exhibit.hallId) || new Set<string>()
keywordParts.forEach((part) => keywords.add(part))
keywordsByHallId.set(exhibit.hallId, keywords)
}
if (exhibit.hallName) {
countsByHallName.set(exhibit.hallName, (countsByHallName.get(exhibit.hallName) || 0) + 1)
const keywords = keywordsByHallName.get(exhibit.hallName) || new Set<string>()
keywordParts.forEach((part) => keywords.add(part))
keywordsByHallName.set(exhibit.hallName, keywords)
}
})
return halls.map((hall) => {
const explainCount = countsByHallId.get(hall.id)
|| countsByHallName.get(hall.name)
|| hall.exhibitCount
|| 0
const keywordParts = [
hall.name,
hall.floorLabel,
hall.description,
hall.area,
...Array.from(keywordsByHallId.get(hall.id) || []),
...Array.from(keywordsByHallName.get(hall.name) || [])
].filter(Boolean) as string[]
return {
id: hall.id,
name: hall.name,
floorLabel: hall.floorLabel,
image: hall.image,
explainCount,
searchText: normalizeExplainKeyword(keywordParts.join(' '))
}
})
}
```
Expected: `src/pages/index/index.vue` no longer owns explain hall card mapping.
- [ ] **Step 7: Use explain view-model hall mapper**
In `loadExplainExhibits`, replace:
```ts
explainHallItems.value = buildExplainHallItems(halls, exhibits)
```
with:
```ts
explainHallItems.value = toExplainHallSelectItems(halls, exhibits)
```
Expected: explain hall selector still shows the same halls and counts.
- [ ] **Step 8: Run type-check after index page migration**
Run:
```powershell
pnpm type-check
```
Expected: PASS.
- [ ] **Step 9: Commit if authorized**
Do not commit unless the user explicitly authorizes commits in this session. If authorized, run:
```powershell
git add src/pages/index/index.vue
git commit -m @'
refactor: use view models on guide index page
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'@
```
Expected: one commit containing only index page mapping removal.
---
### Task 6: Verify Data-Source Switch Modes
**Files:**
- No source edits expected.
- [ ] **Step 1: Verify no forbidden page imports exist**
Run these searches:
```powershell
Select-String -Path "src/pages/**/*.vue" -Pattern "SGSMapSDK|SgsMapService|sgsSdkApiProvider|/app-api/gis/sdk" -CaseSensitive
Select-String -Path "src/components/**/*.vue" -Pattern "sgsSdkApiProvider|/app-api/gis/sdk" -CaseSensitive
```
Expected: no matches in pages. Component matches should not include generic UI components; `SgsMapRenderer.vue` may import `SgsMapService`, which is allowed because it is the SDK renderer boundary.
- [ ] **Step 2: Verify mode switch source code**
Run:
```powershell
Select-String -Path "src/repositories/createGuideRepository.ts","src/components/navigation/GuideMapShell.vue","src/config/dataSource.ts" -Pattern "dataSourceConfig.mode|isSgsSdkMode|SgsSdkGuideRepository|StaticGuideRepository|SgsMapRenderer|ThreeMap" -CaseSensitive
```
Expected evidence:
```text
createGuideRepository.ts: api/sdk -> SgsSdkGuideRepository, static -> StaticGuideRepository
GuideMapShell.vue: sdk -> SgsMapRenderer, otherwise -> ThreeMap
config/dataSource.ts: mode supports static/api/sdk
```
- [ ] **Step 3: Run type-check**
Run:
```powershell
pnpm type-check
```
Expected: PASS.
- [ ] **Step 4: Run lint because shell/page/view-model files changed**
Run:
```powershell
pnpm lint
```
Expected: PASS. If warnings appear but exit code is 0, record them as debt in the final report.
- [ ] **Step 5: Run H5 build because shell/page UI files changed**
This command writes `dist`.
Run:
```powershell
pnpm build:h5
```
Expected: PASS and `dist/build/h5` or the projects configured H5 output is updated.
- [ ] **Step 6: Commit verification-related source edits if authorized**
If Task 6 made no source edits, do not commit. If a small verification fix was required and the user has authorized commits, run:
```powershell
git add src
git commit -m @'
chore: verify guide data source switching
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
'@
```
Expected: commit only source fixes made during verification.
---
## Acceptance Criteria
- `VITE_DATA_SOURCE_MODE=static` continues to use `StaticGuideRepository` and `ThreeMap`.
- `VITE_DATA_SOURCE_MODE=api` continues to use `SgsSdkGuideRepository` and `ThreeMap`.
- `VITE_DATA_SOURCE_MODE=sdk` continues to use `SgsSdkGuideRepository` and `SgsMapRenderer`.
- Pages and ordinary presentation components do not directly import `SGSMapSDK`, `SgsMapService`, `sgsSdkApiProvider`, or raw `/app-api/gis/sdk/*` URLs.
- Remaining route picker, selected POI card, and explain hall selector mapping is owned by `src/view-models/*` instead of `src/pages/index/index.vue`.
- Route readiness gate remains unchanged; no user-facing copy claims certified route navigation.
- `pnpm type-check` passes.
- `pnpm lint` passes.
- `pnpm build:h5` passes.
## Self-Review
- Spec coverage: plan covers data-source switching, renderer switching preservation, middle-layer/view-model convergence, forbidden direct imports, diagnostics, and quality gates.
- Placeholder scan: no TBD/TODO/fill-in-later instructions remain; all code snippets are concrete.
- Type consistency: `RoutePointOptionViewModel` is aliased as `RoutePointOption`, `ExplainHallSelectItem` is exported from the view-model layer and aliased by the component, and the page imports the mapper names defined in the tasks.