Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .specify/feature.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"feature_directory": "specs/006-voyager-about-tab"
"feature_directory": "specs/007-voyager-er-pure-fk"
}
2 changes: 1 addition & 1 deletion .specify/integrations/claude.manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"integration": "claude",
"version": "0.11.3",
"installed_at": "2026-06-20T02:31:44.756825+00:00",
"installed_at": "2026-07-03T06:11:58.021218+00:00",
"files": {
".claude/skills/speckit-analyze/SKILL.md": "0c6d0525b97b523851a3ea8fc0efdcf477dfa65bef8c4d976b6da2f220f32c86",
".claude/skills/speckit-clarify/SKILL.md": "eb8907c57e9af2c54952370a45e3fb66cb21a64dc7b3d361615bab914c13becf",
Expand Down
36 changes: 36 additions & 0 deletions specs/007-voyager-er-pure-fk/checklists/requirements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# 规格质量检查清单:Voyager ER 图 —— Hide Reverse Relationships 连线模式

**用途**:在进入 plan 阶段前,对 spec.md 的完整性与质量做一次校验
**创建日期**:2026-07-03
**对应功能**:[spec.md](../spec.md)

## 内容质量

- [x] 不包含实现细节(语言、框架、API、文件路径)——仅必要的现有命名引用作为上下文锚点
- [x] 聚焦用户价值与业务需求
- [x] 面向非技术干系人可读
- [x] 所有必填章节已填写

## 需求完整性

- [x] spec.md 中无残留的 `[NEEDS CLARIFICATION]` 标记
- [x] 所有功能需求(FR-001 ~ FR-014)可测试、无歧义
- [x] 成功标准(SC-001 ~ SC-005)可衡量
- [x] 成功标准未夹带实现细节(无框架/语言/数据库/工具名)
- [x] 用户故事的验收场景全部已定义(Story 1 八条、Story 2 五条)
- [x] 边界情况已识别(10 条)
- [x] 功能范围有清晰边界(仅裁剪 ER-diagram 模式下的连线,不干涉字段展示/侧边栏/其他模式)
- [x] 依赖与假设已在"假设"区块列出

## 功能就绪度

- [x] 每条功能需求都有对应的验收场景支撑
- [x] 用户故事覆盖主要使用流程(首次勾选、持久化、与其他选项正交)
- [x] 功能可达成"成功标准"中描述的可衡量结果
- [x] 实现细节未泄漏到规格中(连线的"如何区分 FK vs Relationship"留给 plan 阶段)

## 备注

- Phase 0 八步访谈区块已省略:本特性是 Voyager 工具自身的 UI 增强,不涉及 nexusx 业务建模(无业务实体、聚合根、Service 切分、DB 选型)。处理方式与上一同源特性 `006-voyager-about-tab` 一致。
- 关键决策点(连线裁剪范围、默认状态、持久化策略)已在 "澄清记录 → Session 2026-07-03" 中以 Q&A 形式固化,spec 主体不再重复 NEEDS CLARIFICATION。
- 本清单所有项已通过;spec 可直接进入 `/speckit-clarify`(如需追加澄清)或 `/speckit-plan`(开始设计实现方案)。
115 changes: 115 additions & 0 deletions specs/007-voyager-er-pure-fk/contracts/er-diagram-payload-extension.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# 契约:ErDiagramPayload / ErDiagramSubgraphPayload 字段扩展

**功能**:[spec.md](../spec.md) · **数据模型**:[data-model.md](../data-model.md) · **过滤契约**:[hide-reverse-filter.md](./hide-reverse-filter.md)

**位置**:`src/nexusx/voyager/create_voyager.py`(`ErDiagramPayload` 第 64 行、`ErDiagramSubgraphPayload` 第 72 行)

---

## 字段定义

### `ErDiagramPayload`(`POST /er-diagram` 请求体)

新增字段:

```python
class ErDiagramPayload(PydanticModel):
# ... 现有字段保持不变
show_module: bool = True
better_cluster_display: bool = False
show_methods: bool = True
hide_reverse_relationships: bool = False # 新增(本期)
```

### `ErDiagramSubgraphPayload`(`POST /er-diagram-subgraph` 请求体,spec 005)

新增同名字段:

```python
class ErDiagramSubgraphPayload(PydanticModel):
"""Spec 005 — request body for POST /er-diagram-subgraph.

Same rendering fields as :class:`ErDiagramPayload`, plus the required
schema_name.
"""
schema_name: str
# ... 现有渲染字段保持不变(与 ErDiagramPayload 对齐)
show_module: bool = True
better_cluster_display: bool = False
show_methods: bool = True
hide_reverse_relationships: bool = False # 新增(本期)
```

---

## 请求示例

### 主图请求

```http
POST /er-diagram HTTP/1.1
Content-Type: application/json

{
"schema": "demo.enterprise_voyager",
"show_module": true,
"better_cluster_display": false,
"show_methods": true,
"hide_reverse_relationships": true
}
```

### 子图请求(spec 005)

```http
POST /er-diagram-subgraph HTTP/1.1
Content-Type: application/json

{
"schema": "demo.enterprise_voyager",
"schema_name": "demo.enterprise_voyager.models.Post",
"show_module": true,
"better_cluster_display": false,
"show_methods": true,
"hide_reverse_relationships": true
}
```

---

## 校验规则

- **类型**:`bool`,Pydantic 自动校验。非 bool 值(如 `"true"` 字符串、`1` 整数)按 Pydantic 默认行为转换;转换失败返回 `422 Unprocessable Entity`,与现有 bool 字段(`show_module` 等)一致。
- **必填性**:可选(默认 `False`)。老客户端不传该字段时行为与现状完全一致(向后兼容)。
- **取值语义**:
- `false`(默认):行为与现状完全一致——所有方向(MANYTOONE / ONETOMANY / MANYTOMANY)的 relationship 连线都生成。
- `true`:进入 Pure FK 模式——`_add_relationship_link` 跳过 `direction == 'ONETOMANY'` 的 relationship,详见 [hide-reverse-filter.md](./hide-reverse-filter.md)。

---

## 响应 shape

**完全不变**——`/er-diagram` 仍返回 `{dot: str, links: [...], schemas: {...}}`,`/er-diagram-subgraph` 仍返回 spec 005 定义的子图响应 shape。`hide_reverse_relationships: true` 时唯一可观察的差异是 `dot` 字符串和 `links` 数组中 ONETOMANY 方向的边缺失。

---

## 向后兼容性

- **老客户端**(包括已发布的 voyager 前端缓存版本、第三方调用者)不传该字段时,Pydantic 默认 `False`,行为完全一致。
- **service worker 缓存**(`web/sw.js`):本期不修改缓存键策略;如果 sw 把请求体作为缓存键的一部分(需在实现时确认),新增字段会导致老缓存失效、新缓存重新填充——属于一次性开销,无需特殊处理。
- **路由处理函数**:`@router.post("/er-diagram")` 与 `@router.post("/er-diagram-subgraph")` 的处理逻辑**无需修改**——FastAPI 自动把请求体映射到 `voyager_context.get_er_diagram(payload)` 等方法,新增字段通过 `payload.get("hide_reverse_relationships", False)` 在 `voyager_context.py` 内透传到 `ErDiagramDotBuilder` 构造函数。

---

## 与现有 toggle 字段的对齐表

| 字段 | localStorage key | 默认值 | 作用 |
|------|-----------------|--------|------|
| `show_module` | `show_module_cluster` | `True` | 按模块聚类显示 |
| `better_cluster_display` | `better_cluster_display` | `False` | 改进的聚类显示 |
| `show_methods` | (仅会话内) | `True` | 显示实体方法 |
| `brief` | `brief_mode` | `False` | 简短模式(仅 tag → schema) |
| `pydantic_resolve_meta` | `pydantic_resolve_meta` | `False` | 显示 Pydantic resolve 元数据 |
| **`hide_reverse_relationships`**(本期新增) | **`hide_reverse_relationships`** | **`False`** | **隐藏 ONETOMANY 反向镜像** |

新增字段在命名、默认值、localStorage 持久化模式上与现有字段完全对齐。
160 changes: 160 additions & 0 deletions specs/007-voyager-er-pure-fk/contracts/frontend-toggle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# 契约:前端 store + UI 集成

**功能**:[spec.md](../spec.md) · **数据模型**:[data-model.md](../data-model.md) · **Payload 契约**:[er-diagram-payload-extension.md](./er-diagram-payload-extension.md)

**位置**:
- `src/nexusx/voyager/web/store.js`(state + toggle 函数)
- `src/nexusx/voyager/web/vue-main.js`(初始化 + fetch 透传)
- `src/nexusx/voyager/web/component/schema-code-display.js`(显示选项面板 checkbox)

---

## store.js 变更

### 新增 state 字段

在 `state.filter` 对象内新增:

```javascript
state.filter = {
// ... 现有字段保持不变
hideReverseRelationships: false, // 新增(本期)
}
```

初始值 `false`,与 spec FR-002"默认未勾选"一致。

### 新增 toggle 函数

紧邻现有 `toggleBetterClusterDisplay`(约第 467-475 行)之后新增:

```javascript
/**
* Toggle hide reverse relationships mode.
* When enabled, only MANYTOONE and MANYTOMANY direction relationship edges
* are rendered (FK-holder side preserved); ONETOMANY reverse mirrors hidden.
* Persists to localStorage; calls onGenerate to trigger graph re-render.
* @param {boolean} val - New value
* @param {Function} onGenerate - Callback to regenerate graph
*/
toggleHideReverseRelationships(val, onGenerate) {
state.filter.hideReverseRelationships = val
try {
localStorage.setItem("hide_reverse_relationships", JSON.stringify(val))
} catch (e) {
console.warn("Failed to save hide_reverse_relationships to localStorage", e)
}
onGenerate()
},
```

签名、错误降级(`console.warn` 不阻塞)、`onGenerate()` 调用模式与现有 `toggleBetterClusterDisplay` / `toggleBrief` / `togglePydanticResolveMeta` 完全一致。

---

## vue-main.js 变更

### 初始化时读取 localStorage

在现有 `loadToggleState("pydantic_resolve_meta", ...)`(约第 55 行)之后新增:

```javascript
store.state.filter.hideReverseRelationships = loadToggleState(
"hide_reverse_relationships",
false,
)
```

`loadToggleState(key, defaultValue)` 是项目内已有的辅助函数(与 `show_module_cluster` / `better_cluster_display` / `brief_mode` / `pydantic_resolve_meta` 复用同一函数),自动处理 JSON 解析失败、隐私模式禁用 localStorage 等降级场景(spec FR-011 / Story 2 验收场景 4-5)。

### fetch `/er-diagram` 时透传字段

约第 189-200 行,现有 `fetch("er-diagram", { body: JSON.stringify({...}) })` 的 body 内新增一行:

```javascript
const res = await fetch("er-diagram", {
// ... method / headers 不变
body: JSON.stringify({
// ... 现有字段保持不变
schema: store.state.graph.schema,
show_module: store.state.filter.showModule,
better_cluster_display: store.state.filter.betterClusterDisplay,
show_methods: store.state.modeControl.showMethodsEnabled,
// ... 其他现有字段
hide_reverse_relationships: store.state.filter.hideReverseRelationships, // 新增(本期)
}),
})
```

### fetch `/er-diagram-subgraph` 时透传字段

`/er-diagram-subgraph` 请求体由 `src/nexusx/voyager/web/store.js::buildErDiagramSubgraphPayload(schemaName)` 构造(spec 005 引入,约第 623-632 行)——这是该端点请求体的**唯一构造点**,被 `store.js::fetchRelatedEntities`(约第 638 行)调用、由 `component/related-entities-display.js` 触发。

在本函数返回对象内新增字段:

```javascript
buildErDiagramSubgraphPayload(schemaName) {
return {
schema_name: schemaName,
show_fields: state.filter.showFields,
show_module: state.filter.showModule,
better_cluster_display: state.filter.showModule && state.filter.betterClusterDisplay,
edge_minlen: state.filter.edgeMinlen,
show_methods: state.filter.showMethods,
hide_reverse_relationships: state.filter.hideReverseRelationships, // 新增(本期)
}
},
```

**为什么直接透传而不做条件包装**:与 `better_cluster_display` 不同(其在 subgraph 中受 `state.filter.showModule && ...` 条件包装,因为 cluster display 依赖 module 聚类),`hide_reverse_relationships` 是独立的渲染配置、无前置依赖,直接透传即可——与 spec FR-007"子图跟随主图渲染配置"原则一致。

---

## 显示选项面板变更(component/schema-code-display.js 或同等位置)

### 新增 Quasar checkbox

紧邻现有 `better cluster display` / `brief mode` / `pydantic resolve meta` toggle 之后新增:

```html
<q-checkbox
:model-value="store.state.filter.hideReverseRelationships"
@update:model-value="(val) => store.toggleHideReverseRelationships(val, onGenerate)"
label="Hide Reverse Relationships"
/>
```

### 仅在 ER-diagram 模式下可见

`schema-code-display.js` 已有按 `store.state.mode === 'er-diagram'` 条件渲染面板的逻辑(spec FR-001)——新增 checkbox 沿用同一条件,无需新增判断。

### 键盘可达性(spec FR-004)

Quasar `q-checkbox` 默认键盘可达(Tab 聚焦、Space 切换),与现有 `q-checkbox` 一致,无需额外配置。

### 选项位置(plan 阶段不固化)

spec FR-001 仅要求"与现有显示选项位于同一交互区域",具体顺序(出现在 brief mode 之前还是之后)、分组(独立还是合并到"relationship display"子组)属于 UI 实现细节,由 tasks 阶段决定。建议放在 `better cluster display` 之后、`brief mode` 之前(语义上"裁剪连线"接近"聚类显示")。

---

## 与项目内现有 toggle 的对齐表

| Toggle | state.filter 字段 | localStorage key | toggle 函数 | Payload 字段 |
|--------|------------------|------------------|-------------|--------------|
| Show Module Cluster | `showModule` | `show_module_cluster` | `toggleShowModule` | `show_module` |
| Better Cluster Display | `betterClusterDisplay` | `better_cluster_display` | `toggleBetterClusterDisplay` | `better_cluster_display` |
| Brief Mode | `brief` | `brief_mode` | `toggleBrief` | `brief` |
| Pydantic Resolve Meta | `pydanticResolveMetaEnabled`* | `pydantic_resolve_meta` | `togglePydanticResolveMeta` | `show_pydantic_resolve_meta` |
| **Hide Reverse Relationships**(本期) | **`hideReverseRelationships`** | **`hide_reverse_relationships`** | **`toggleHideReverseRelationships`** | **`hide_reverse_relationships`** |

*`pydanticResolveMetaEnabled` 在 `state.modeControl` 而非 `state.filter`,因为它影响"是否生成 Pydantic resolve 元数据"而非"如何渲染图"。本期 Pure FK 模式属于"如何渲染图"范畴,归 `state.filter`,与 cluster display / brief mode 同侧。

---

## 不变量

1. **URL 不含状态**:toggle 函数不修改 URL 参数(spec FR-012);分享 URL 时接收方按自己 localStorage 偏好渲染。
2. **不破坏现有 toggle**:Pure FK 与 cluster display / brief mode / show fields / pydantic resolve meta 等正交(spec FR-013)——各 toggle 独立写入 localStorage、独立透传到请求体、后端独立处理。
3. **localStorage 不可用降级**:`loadToggleState` 与 `localStorage.setItem` 的 try/catch 已覆盖隐私模式、配额满、JSON 解析失败等场景(spec FR-011 / Story 2 验收场景 4-5)。
4. **勾选即生效**:`toggleHideReverseRelationships` 调用 `onGenerate()` 触发 ER 图重新生成(spec FR-003),与现有 toggle 一致——不需要用户额外点击"应用"或"刷新"。
Loading