Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ docs/TradingView级别像素对齐技术.md

# Agents
.agents/
.worktrees/

# Next.js examples
examples/next-app/.next/
Expand Down
15 changes: 6 additions & 9 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,12 +177,9 @@ Never guess at Effect patterns - check the guide first.

## Comment Style

- Language: body in Chinese; technical terms keep English (Signal, batch, computed, Action, etc.)
- No Markdown symbols: no backticks, bold, or arrows. Use natural language for code references.
- Prefer JSDoc tags: `@remarks` (detailed explanation), `@param`, `@returns`, `@typeParam`, `@example`.
- Inline comments: `/** brief */` or `// brief`, no tags needed.
- **Say what it is, not what to do with it**
- **Never invent Chinese jargon**
- **表述直接,不说倒装话**:说"合并后写入 X",不说"写入 X 前先在此合并"。注释要一眼看完就能理解,不要兜圈子
- **One sentence if possible**
- **分支注释写在 `if` 上一行**,不要写到分支内部
- 每个文件必须有头部注释,说明文件用途。
- 每个函数必须有注释,说明其职责、参数和返回值;简单函数可使用简短注释。
- 关键代码必须有注释,说明实现意图、业务规则或不直观的处理逻辑。
- 每个测试用例必须有中文注释,说明验证的行为和场景。
- 注释正文使用中文,技术术语保留英文。
- 注释必须简单明了,直接说明代码是什么或为什么这样实现,尽量使用一句话,避免冗长和重复代码本身的含义。
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Instance Market Session Registry 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:** Add strict chart-instance market session resolution and normalize gotdx symbols into the unified market model.

**Architecture:** `SymbolSpec.market` is required. Each Chart owns a `MarketSessionRegistry`; time-share activation resolves its session before changing chart state. gotdx search converts private market/category metadata into `CN` or `HK`; core never examines fetcher params.

**Tech Stack:** TypeScript, Vitest, Effect, pnpm workspace

---

### Task 1: Market Session Registry

**Files:**
- Create: `packages/core/src/engine/market/marketSessionRegistry.ts`
- Create: `packages/core/src/engine/market/__tests__/marketSessionRegistry.test.ts`
- Modify: `packages/core/src/index.ts`

- [ ] Write tests proving built-ins resolve, unknown IDs throw, invalid configs throw, and two registries are isolated.
- [ ] Run the focused test and confirm failure because the registry module is missing.
- [ ] Implement `MarketSessionRegistry`, built-in CN/HK/US entries, validation, and `getRequired`.
- [ ] Export the registry and market session types from core.
- [ ] Run the focused test and confirm it passes.

### Task 2: Strict Unified Symbol Market

**Files:**
- Modify: `packages/core/src/controllers/types.ts`
- Modify: all repository `SymbolSpec` and `SymbolInfo` construction sites reported by type-check.
- Test: `packages/core/src/engine/market/__tests__/marketSessionRegistry.test.ts`

- [ ] Add compile/runtime tests showing blank market is rejected.
- [ ] Make `market` required on `SymbolSpec` and `SymbolInfo`.
- [ ] Update fixtures and explicit inline/custom symbol construction with a deliberate market; do not infer from exchange or code.
- [ ] Run `pnpm type-check` and resolve only missing unified-market construction errors.

### Task 3: Chart Instance Integration

**Files:**
- Modify: `packages/core/src/controllers/types.ts` (`ChartMountOptions`)
- Modify: `packages/core/src/controllers/createChartController.ts`
- Modify: `packages/core/src/engine/chart.ts`
- Test: `packages/core/src/engine/modes/__tests__/timeShareMode.test.ts` or a focused Chart test

- [ ] Write failing tests proving HK symbols select `HK_MARKET_SESSION`, unknown markets throw before fetch, and two Chart registries do not share overrides.
- [ ] Add `marketSessions` to mount options and instantiate one registry per Chart.
- [ ] In `Chart.setSymbols`, validate market and resolve/apply the time-share session before `setActiveMode` and data loading.
- [ ] Run focused tests and confirm pass.

### Task 4: gotdx Market Normalization

**Files:**
- Modify: `packages/core/src/data/gotdx.ts`
- Modify: `packages/core/src/data/types.ts`
- Test: `packages/core/src/data/__tests__/gotdx.test.ts`

- [ ] Write failing tests for main-market to `CN`, HK category entries to `HK`, and unsupported metadata rejection.
- [ ] Extend `SearchResult` with required unified `market`.
- [ ] Normalize the raw gotdx response inside `searchGotdx`; preserve private params unchanged.
- [ ] Run gotdx tests and confirm pass.

### Task 5: UI Search Propagation

**Files:**
- Modify: `packages/vue/src/composables/useSymbolSearch.ts`
- Modify: Vue symbol conversion sites in `packages/vue/src/components/KLineChart.vue`
- Test: `packages/vue/src/composables/__tests__/useSymbolSearch.test.ts`

- [ ] Write failing tests proving `market` survives catalog/search selection into `SymbolSpec`.
- [ ] Propagate the already-normalized field without deriving it from exchange or params.
- [ ] Run Vue focused tests and confirm pass.

### Task 6: Verification

- [ ] Run `pnpm --filter @363045841yyt/klinechart-core test`.
- [ ] Run `pnpm --filter @363045841yyt/klinechart test`.
- [ ] Run `pnpm type-check`.
- [ ] Run `pnpm test:packages` if focused suites and type-check pass.
- [ ] Inspect diffs in both repositories and report any unrelated existing changes without modifying them.
268 changes: 268 additions & 0 deletions docs/superpowers/plans/2026-07-28-symbol-chip-fetch-error-title.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
# 品种 Chip 拉取错误 Title 实现计划

> **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:** 主品种 K 线 Effect 显式失败时,品种 chip 悬停通过原生 `title` 显示失败原因。

**Architecture:** `DataBuffer` 持有 `lastError` 只读信号;失败写入、成功/换品种/dispose 清空。`ChartDataManager`/`Chart`/`ChartController` 暴露 `dataError`。Vue 订阅后把文案传到 `SymbolSelector` 的 `title`。

**Tech Stack:** TypeScript、Effect、vitest、Vue 3 SFC

**Spec:** `docs/superpowers/specs/2026-07-28-symbol-chip-fetch-error-title-design.md`

---

### Task 1: DataBuffer.lastError

**Files:**
- Modify: `packages/core/src/data/dataBufferTypes.ts`
- Modify: `packages/core/src/data/dataBuffer.ts`
- Test: `packages/core/src/data/__tests__/dataBuffer.test.ts`

- [ ] **Step 1: 写失败测试**

在 `dataBuffer.test.ts` 增加(`defaultSpec` 补 `market: 'CN'` 若测试需要):

```ts
it('records lastError when fetch fails after retries', async () => {
const fetcher: DataFetcher = async () => {
throw new Error('[gotdx] stock/kline-by-date failed: 500')
}
buffer.setFetcher(fetcher)
buffer.setSymbol({ ...defaultSpec, market: 'CN' })

await vi.waitFor(() => expect(buffer.loading()).toBe(false), { timeout: 10_000 })
expect(buffer.lastError()).toBe('[gotdx] stock/kline-by-date failed: 500')
})

it('clears lastError on successful fetch', async () => {
let fail = true
const fetcher: DataFetcher = async () => {
if (fail) throw new Error('offline')
return [makeKLine(Date.now())]
}
buffer.setFetcher(fetcher)
buffer.setSymbol({ ...defaultSpec, market: 'CN' })
await vi.waitFor(() => expect(buffer.lastError()).toBe('offline'), { timeout: 10_000 })

fail = false
buffer.setSymbol({ ...defaultSpec, market: 'CN', symbol: 'sh.600001' })
await vi.waitFor(() => {
expect(buffer.loading()).toBe(false)
expect(buffer.data().data.length).toBe(1)
})
expect(buffer.lastError()).toBeNull()
})

it('does not set lastError for successful empty data', async () => {
buffer.setFetcher(async () => [])
buffer.setSymbol({ ...defaultSpec, market: 'CN' })
await vi.waitFor(() => expect(buffer.loading()).toBe(false))
expect(buffer.lastError()).toBeNull()
})

it('clears lastError on setInlineData', async () => {
buffer.setFetcher(async () => {
throw new Error('boom')
})
buffer.setSymbol({ ...defaultSpec, market: 'CN' })
await vi.waitFor(() => expect(buffer.lastError()).toBe('boom'), { timeout: 10_000 })
buffer.setInlineData([makeKLine(Date.now())])
expect(buffer.lastError()).toBeNull()
})
```

- [ ] **Step 2: 跑测试确认 RED**

```bash
pnpm exec vitest run src/data/__tests__/dataBuffer.test.ts
```

Expected: FAIL — `lastError` 不存在

- [ ] **Step 3: 最小实现**

`dataBufferTypes.ts` 的 `DataBufferLike` / `KLineBuffer` 增加:

```ts
readonly lastError: ReadonlySignal<string | null>
```

`dataBuffer.ts`:

```ts
private _lastError = createSignal<string | null>(null)

get lastError(): ReadonlySignal<string | null> {
return this._lastError
}
```

- `setSymbol` / `setInlineData` / `dispose`:`this._lastError.set(null)`
- `_fetchAndMerge` 成功 merge 后:`this._lastError.set(null)`
- `.catch(err)`:若 `requestVersion === this._requestVersion`,
`this._lastError.set(err instanceof Error && err.message ? err.message : err ? String(err) : '加载失败')`

注意:`FetchScheduler.run` 的 catch 目前丢弃 err;需把 `run` 的 reject 原因传到外层 catch,或在 task 内 try/catch 写入 lastError。优先在 `_fetchAndMerge` 的 task 内 try/catch:

```ts
this._scheduler
.run(async () => {
try {
const incoming = await fetchEffect()
if (disposed() || requestVersion !== this._requestVersion) return
this._lastError.set(null)
// merge ...
} catch (err) {
if (disposed() || requestVersion !== this._requestVersion) return
const message =
err instanceof Error && err.message.trim()
? err.message
: err != null && String(err).trim()
? String(err)
: '加载失败'
this._lastError.set(message)
this._inflightBoundary = null
this._pendingRequestStartTs = null
}
})
```

- [ ] **Step 4: 跑测试确认 GREEN**

```bash
pnpm exec vitest run src/data/__tests__/dataBuffer.test.ts
```

Expected: PASS

- [ ] **Step 5: Commit**

```bash
git add packages/core/src/data/dataBuffer.ts packages/core/src/data/dataBufferTypes.ts packages/core/src/data/__tests__/dataBuffer.test.ts
git commit -m "feat(core): record DataBuffer lastError on fetch failure"
```

---

### Task 2: Chart / Controller 暴露 dataError

**Files:**
- Modify: `packages/core/src/engine/data/chartDataManager.ts`
- Modify: `packages/core/src/engine/chart.ts`
- Modify: `packages/core/src/controllers/types.ts`
- Modify: `packages/core/src/controllers/createChartController.ts`
- Test: `packages/core/src/engine/data/__tests__/chartDataManager.incrementalLoad.test.ts` 或新增小测试

- [ ] **Step 1: 写失败测试**

在 incrementalLoad 或新建测试中:失败 fetcher 后 `manager`/`chart` 的 `dataError.peek()` 等于错误 message;成功后为 null。

- [ ] **Step 2: RED**

- [ ] **Step 3: 实现**

`ChartDataManager`:

```ts
get dataError(): ReadonlySignal<string | null> {
const buf = this.getActiveDataBuffer()
return (buf?.lastError ?? createSignal<string | null>(null)) as ReadonlySignal<string | null>
}
```

注意:active buffer 切换时,若直接返回 buffer 信号引用会变。更稳妥:在 `dataState` 增加 `error: string | null`,在 `publishBufferSnapshot` / loading/data 事件时同步 `buf.lastError.peek()`;或 ChartDataManager 维护桥接 signal,在 bindActiveBuffer 时订阅 `buf.lastError`。

推荐桥接(与 loading 镜像一致):

- `bindActiveBuffer` 额外 `buf.lastError.subscribe` → 写入 `_dataState.actions.setError(...)`
- 或独立 `_errorSignal` 在 chartDataManager 内

最小路径:`Chart.dataError` 每次 `get` 返回 active buffer 的 `lastError`;Vue 在 `dataLoading` 订阅回调里 `peek()` 一次即可。但 `ChartController` 需要稳定 `ReadonlySignal`。

稳定方案:

```ts
// chartDataManager
private _dataError = createSignal<string | null>(null)

private syncDataErrorFromBuffer(buf: KLineBuffer | TimeShareBuffer | null): void {
const err =
buf && 'lastError' in buf
? ((buf as KLineBuffer).lastError?.peek() ?? null)
: null
this._dataError.set(err)
}

// 在 publishBufferSnapshot / bind / handle loading|data 后调用 sync
get dataError(): ReadonlySignal<string | null> {
return this._dataError
}
```

`Chart`:`get dataError() { return this.dataManager.dataError }`
`ChartController`:`readonly dataError: ReadonlySignal<string | null>`
`createChartController`:`dataError: chart.dataError`

- [ ] **Step 4: GREEN + commit**

```bash
git commit -m "feat(core): expose chart dataError signal"
```

---

### Task 3: Vue chip title 接线

**Files:**
- Modify: `packages/vue/src/components/SymbolSelector.vue`
- Modify: `packages/vue/src/components/TopToolbar.vue`
- Modify: `packages/vue/src/components/KLineChart.vue`
- Test: 新增 `packages/vue/src/components/__tests__/SymbolSelector.errorTitle.test.ts`(若 vue 包已有 component test 模式);否则用纯函数/小测验证 title 计算,或 mount SymbolSelector

- [ ] **Step 1: SymbolSelector 失败测试**

```ts
// title 计算:error && errorMessage ? errorMessage : displayText
it('uses errorMessage as title when error is true', () => {
// mount SymbolSelector with error=true, errorMessage='offline', symbol display
// expect button title === 'offline'
})
```

- [ ] **Step 2: RED → 实现 props `errorMessage?: string`,`:title="error && errorMessage ? errorMessage : displayText"`**

- [ ] **Step 3: TopToolbar 增加 `symbolErrorMessage?: string` 传给 SymbolSelector**

- [ ] **Step 4: KLineChart 订阅 `ctrl.dataError`,维护 `symbolErrorMessage`,传给 TopToolbar**

在现有 `unsubscribeDataLoading` 旁:

```ts
symbolErrorMessage.value = ctrl.dataError.peek()
const unsubscribeDataError = ctrl.dataError.subscribe(() => {
symbolErrorMessage.value = ctrl.dataError.peek()
})
```

destroy 时 unsub。

- [ ] **Step 5: GREEN + commit**

```bash
git commit -m "feat(vue): show fetch error reason on symbol chip title"
```

---

### Task 4: 回归验证

- [ ] `pnpm --filter @363045841yyt/klinechart-core test`
- [ ] `pnpm --filter @363045841yyt/klinechart-core build`
- [ ] 相关 vue 测试(若有)
- [ ] 提交中文设计/计划文档(若尚未提交)

```bash
git add docs/superpowers/specs/2026-07-28-symbol-chip-fetch-error-title-design.md docs/superpowers/plans/2026-07-28-symbol-chip-fetch-error-title.md
git commit -m "docs: 中文 chip 错误 title 设计与实现计划"
```
Loading
Loading