Skip to content

Commit ad20edd

Browse files
committed
feat(specs): add readonly specs browser
1 parent 2ae688a commit ad20edd

32 files changed

Lines changed: 1520 additions & 46 deletions

File tree

guidelines/IPC.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@ keywords: [ipc, electron, preload, channels, contracts]
8787
- 入参 schema 位于 `src/shared/schemas/ipc/overview.ts`,要求 `projectId` 为非空字符串;DTO 类型位于 `src/shared/types/overview.ts`。handler 位于 `src/main/ipc/overview.ts`,bridge 与 renderer 薄封装分别位于 `src/preload/api/overview.ts``src/renderer/src/api/overview.ts`
8888
- 主进程内部允许 overview service 并行聚合仓库扫描、git 查询与 lineage 投影,但 IPC handler 仍只负责 `validate -> resolveProjectPath -> service`,不得把文件系统扫描、git 调用或 lineage 投影逻辑写在 handler 中。
8989

90+
## Specs Browser Channels
91+
92+
- `specs:getSpecsBrowser`:入参 `{ projectId }`,主进程解析 `projectId → projectPath` 后读取 `openspec/specs/*/spec.md`,返回 `IpcResponse<SpecsBrowserOverview>`。该 channel 是能力规约只读浏览页的数据入口,只返回 capability id、Purpose、源路径、更新时间、Requirement/Scenario 计数与解析后的 Requirement/Scenario 分组;不得返回 capability title、family、familyLabel 或 anchors。
93+
- 入参 schema 位于 `src/shared/schemas/ipc/specs.ts`,要求 `projectId` 为非空字符串;DTO 类型位于 `src/shared/types/specs.ts`。handler 位于 `src/main/ipc/specs.ts`,bridge 与 renderer 薄封装分别位于 `src/preload/api/specs.ts``src/renderer/src/api/specs.ts`
94+
- 主进程 handler 只负责 `validate -> resolveProjectPath -> getSpecsBrowser`。目录扫描、markdown heading 解析、排序和 `spec.md` 缺失容错都属于 `src/main/services/specs/**`
95+
9096
## Bundled MCP Env
9197

9298
- 主进程向 bundled MCP server 启动描述符注入 `FYLLO_MCP_EVENT_DIR = mcpEventsDir(projectPath)`,供 `fyllo-specs``create-proposal` 写出 proposal 事件文件。该目录按主项目 `projectPath` 计算,不随 linked worktree 变化。

guidelines/RendererProcess.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ keywords: [renderer, vue, pinia, routing, ui]
6565
- Good: `src/renderer/src/stores/integration.providers.ts` 作为 settings 与 `/integration` 页共享的 provider 状态入口。
6666
- Good: `src/renderer/src/api/project.ts` 仅返回 `window.api.project.*` 的类型化 Promise,而不承担缓存或 toast 逻辑。
6767
- Good: `src/renderer/src/stores/overview.ts` 通过 `src/renderer/src/api/overview.ts` 读取单一 `ProjectOverview` DTO,并在 store 内集中维护 loading/error/null data 状态,页面只负责三态编排。
68+
- Good: `src/renderer/src/stores/specs.ts` 通过 `src/renderer/src/api/specs.ts` 读取 `SpecsBrowserOverview``/specs` 页面只消费 store 数据并派生选中项与 Requirement 快速定位状态。
6869
- Good: `src/renderer/src/bootstrap/tasks/projects.ts` 调用 store 的 `ensureInitialized()` 预热 persisted project 列表。
6970
- Good: 组件里用 `<a target="_blank" rel="noreferrer">``UButton as="a"` 打开外站,让 `setWindowOpenHandler` 统一走系统浏览器。
7071
- Good: `src/renderer/src/components/task/TaskCard.vue` 的删除确认通过 `useConfirmDialog()` 发起,只在用户确认后继续 emit 删除事件。
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
schema: spec-driven
2+
created: 2026-06-25T00:00:00.000Z
3+
status: applying
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
## Context
2+
3+
当前 `ProjectOverview.stats.specsCount` 只统计 `openspec/specs/` 下目录数,`OverviewStatsBar` 的「能力规约」卡片在规范中被定义为纯展示。用户已经在主工作区验证了一个静态 UI 原型,并确认最终方向:只读、从概览卡片下钻、不做编辑、不做 family/category 分类、不依赖 spec 中不存在的 title 字段、不把 anchors 作为 schema 字段。
4+
5+
现有 `ProposalMarkdownContent.vue` 使用 `MarkStream` 渲染 markdown,这可以复用于 Requirement body 与 Scenario body。但 specs 浏览页不应整篇渲染 `spec.md`,否则 `# title``## Purpose` 会与详情 header 重复,且大量 Requirements 缺少快速定位。
6+
7+
## Goals / Non-Goals
8+
9+
**Goals:**
10+
11+
- 提供 `/specs` 只读页面,展示当前项目 `openspec/specs/*/spec.md`
12+
- 从 markdown 解析出 `purpose`、Requirement groups、Scenario groups、source path、更新时间与统计数量。
13+
- 左侧 capability 列表只显示 id 和 Purpose 单行摘要。
14+
- 右侧详情 header 紧凑展示 id、purpose、source path、更新时间、Requirements/Scenarios 统计。
15+
- 右侧正文以结构化 Requirement/Scenario 展示:Requirement 快速定位目录 + 平铺阅读内容 + Scenario timeline 视觉。
16+
- Requirement/Scenario body 继续用 `MarkStream` 渲染 markdown,保留 bold、code、list 等 markdown 语义。
17+
18+
**Non-Goals:**
19+
20+
- 不提供编辑、新建、删除、保存或 archive specs 的能力。
21+
- 不引入 spec category/family/title/anchors 等当前 `spec.md` 没有的持久字段。
22+
- 不把 `/specs` 加入 ActivityBar 主导航。
23+
- 不改变 OpenSpec 文件格式;解析只消费当前 markdown 结构。
24+
25+
## Decisions
26+
27+
### Decision: 新增 specs browser IPC,而不是扩展 ProjectOverview DTO
28+
29+
`ProjectOverview` 目前是概览页聚合 DTO,适合数量、趋势和最近脉络,不适合承载所有 `spec.md` 内容。新增 `specs:getSpecsBrowser` IPC,可保持概览加载轻量,也避免每次进入概览页都读取并解析大量 markdown。
30+
31+
主进程新增 `src/main/services/specs/specs-browser-service.ts`,读取 `openspec/specs` 下一级目录中的 `spec.md`。renderer 通过 `src/renderer/src/api/specs.ts``useSpecsStore` 获取数据,页面不直接访问 `window.api`
32+
33+
### Decision: 解析 markdown 结构,但正文仍交给 MarkStream
34+
35+
解析器只负责结构边界:
36+
37+
- `# ...`:忽略,不进入 DTO。
38+
- `## Purpose`:提取为 `purpose`,详情 header 展示,不在正文重复渲染。
39+
- `### Requirement: <name>``### 要求:<name>`:创建 `RequirementGroup.title`
40+
- `#### Scenario: <name>``#### 场景:<name>`:创建 `ScenarioGroup.title`
41+
- Requirement 标题之后、首个 Scenario 之前的 markdown 归入 `RequirementGroup.body`
42+
- Scenario 标题之后、下一个 Scenario/Requirement 之前的 markdown 归入 `ScenarioGroup.body`
43+
44+
Vue 组件负责结构和布局;`MarkStream` 只渲染 body 字段,避免丢失 `**WHEN**`、inline code、列表和未来可能出现的 mermaid/katex 等 markdown 能力。
45+
46+
### Decision: Requirement 快速定位由前端从 requirementGroups 派生
47+
48+
不在 DTO 中增加 `anchors` 字段。页面左侧 capability 列表负责能力切换;详情内部增加 Requirements 快速索引栏,直接遍历 `selectedSpec.requirementGroups`,点击后滚动到对应 DOM id。这样目录不会与数据源漂移,也避免把纯 UI 派生状态固化进 shared type。
49+
50+
### Decision: UI 延续静态原型的三栏结构
51+
52+
页面主体使用现有应用 shell 内的 `bg-elevated space-x-2` 模式:
53+
54+
- 左栏:`w-80` capability 列表,使用 `UiSurface as="button" variant="flat" padding="none"`,每项只显示 id + Purpose 单行摘要。
55+
- 中栏:`w-64` Requirement 快速索引,来源为当前 spec 的 requirementGroups。
56+
- 右侧:详情阅读区,header 在顶部,正文滚动。Requirement 不使用 `UiSurface` 卡片;Requirement 之间用分割线,Scenario 用纵向 timeline 线串联。
57+
58+
## Risks / Trade-offs
59+
60+
- Markdown 解析边界不完整 → 解析器只支持当前 OpenSpec 标准结构,并为缺失 Purpose、缺失 Scenario 的文件提供降级展示和测试覆盖。
61+
- 大量 specs 或大型 spec.md 导致首次加载慢 → specs browser 数据按进入页面时加载,概览不预取;初版不做缓存,后续根据实测再加 TTL 或 store-level 缓存。
62+
- 双语 header 兼容风险 → 解析器同时支持 `Requirement:` / `要求:``Scenario:` / `场景:`,避免当前历史文件混用中英文时无法展示。
63+
- 当前 main worktree 有静态原型残留 → 本 change 的 implementation 应在 linked worktree 内以复制过来的原型为基线,后续是否清理 main prototype 由用户另行决定。
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
## Why
2+
3+
项目概览页已经展示「能力规约」数量,但用户无法在应用内查看 `openspec/specs/*/spec.md` 的具体契约内容,统计卡的下钻价值不足。能力规约是归档后稳定下来的行为契约,需要一个只读浏览入口帮助用户快速查看当前项目能力、Requirement 与 Scenario。
4+
5+
## What Changes
6+
7+
- 新增只读能力规约浏览页 `/specs`,从概览页「能力规约」统计卡下钻进入,不加入 ActivityBar 主导航。
8+
- 新增 specs 读取与解析能力:读取 `openspec/specs/<capability>/spec.md`,提取 capability id、Purpose、Requirement、Scenario、源路径、更新时间与统计数量。
9+
- specs 详情页不展示 `#` 一级标题和 `## Purpose` 原文重复内容;顶部展示 id、purpose、source path、更新时间、Requirement/Scenario 统计,下方仅展示 Requirements 及其 Scenarios。
10+
- Requirement / Scenario 结构由解析结果驱动;Requirement 标题和 Scenario 标题用 Vue 文本渲染,Requirement body 和 Scenario body 继续用 `MarkStream` 渲染 markdown。
11+
- Requirement 快速定位目录从解析出的 `requirementGroups` 派生,不作为持久 schema 字段;点击目录项滚动到对应 Requirement。
12+
- 左侧 capability 列表只展示 capability id 和 Purpose 单行摘要;不提供 family/category 分类,不从 spec 里派生不存在的 title 字段。
13+
- 修改概览页「能力规约」卡片为可点击入口,跳转到 `/specs`;「项目准则」「溯源覆盖」仍保持纯展示。
14+
15+
## Capabilities
16+
17+
### New Capabilities
18+
19+
- `specs-browser`: 定义只读能力规约浏览页、spec.md 解析输出、Requirement/Scenario 阅读结构与错误/空态行为。
20+
21+
### Modified Capabilities
22+
23+
- `project-overview`: 将概览页「能力规约」统计卡从纯展示改为 `/specs` 下钻入口,并保留其它非入口卡片的纯展示行为。
24+
- `app-shell-routing`: 将 `/specs` 定义为项目作用域应用路由,受共享应用外壳和当前项目访问约束保护,但不加入 ActivityBar 主导航。
25+
26+
## Impact
27+
28+
- 主进程:新增 specs 读取/解析 service 与 IPC handler,读取 `openspec/specs/*/spec.md` 并返回只读 DTO。
29+
- 共享契约:新增 specs browser DTO、IPC channel 常量与 schema。
30+
- preload:暴露 `specs` API 薄封装。
31+
- renderer:新增 `/specs` 页面、renderer api/store、概览卡入口调整、Requirement/Scenario 结构化展示。
32+
- 测试:新增 main service/IPC 测试、renderer store/page 测试,并更新 overview 页面测试。
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
## MODIFIED Requirements
2+
3+
### 要求:非欢迎页共享路由级应用外壳
4+
5+
系统 SHALL 为所有应用页面提供路由级父页面,该父页面 SHALL 渲染带有专用 header、侧边导航和主内容区域的共享应用外壳布局。
6+
7+
#### 场景:共享外壳包裹应用页面
8+
9+
- **WHEN** 用户导航到 `/chat``/workflow``/integration``/proposal``/proposal/:id``/specs``/settings`
10+
- **THEN** 路由在共享应用外壳内渲染
11+
- **AND** 页面专属内容在外壳主区域渲染
12+
13+
### 要求:应用页面需要当前项目
14+
15+
系统 SHALL 在无当前项目时阻止访问项目作用域的应用路由,改为渲染 WelcomeView。
16+
17+
#### 场景:无项目时访问项目作用域路由
18+
19+
- **WHEN** 用户在无当前项目的情况下直接导航到 `/chat``/workflow``/integration``/proposal``/proposal/:id``/specs`
20+
- **THEN** 应用在主内容区域渲染 WelcomeView
21+
22+
### Requirement: ActivityBar 主导航不再包含提案入口
23+
24+
ActivityBar 中部主导航菜单 SHALL NOT 包含「提案」入口。提案区域(`/proposal` 列表页)SHALL 改由概览页进入,而非通过 ActivityBar 导航。ActivityBar 中部主导航菜单 SHALL NOT 包含「能力规约」或指向 `/specs` 的入口。能力规约浏览页 SHALL 改由概览页进入,而非通过 ActivityBar 导航。`/proposal``/proposal/:id``/specs` 路由本身 SHALL 保留,仍受共享外壳与项目作用域约束保护。
25+
26+
#### Scenario: 主导航不显示提案或能力规约入口
27+
28+
- **WHEN** 应用在共享外壳中渲染 ActivityBar 主导航菜单
29+
- **THEN** 菜单中不出现指向 `/proposal` 的「提案」入口
30+
- **AND** 菜单中不出现指向 `/specs` 的「能力规约」入口
31+
- **AND** 其余主导航入口(概览、对话、任务、工作流、定时、集成)正常显示
32+
33+
#### Scenario: 提案和 specs 路由仍可直接访问
34+
35+
- **WHEN** 用户通过概览页入口或直接导航到 `/proposal``/proposal/:id``/specs`
36+
- **THEN** 对应页面在共享应用外壳内正常渲染
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
## MODIFIED Requirements
2+
3+
### Requirement: 概览页归档提案卡提供提案列表入口
4+
5+
概览页 `OverviewStatsBar` 中的「归档提案」统计卡(`key: "archives"`)SHALL 为可点击交互元素,点击后路由跳转至 `/proposal` 列表页。概览页 `OverviewStatsBar` 中的「能力规约」统计卡(`key: "specs"`)SHALL 为可点击交互元素,点击后路由跳转至 `/specs` 只读能力规约浏览页。这两个可点击卡片 SHALL 提供一致的 hover 视觉反馈与无障碍语义(可聚焦、键盘可触发)。「项目准则」与「溯源覆盖」统计卡 SHALL 保持纯展示,无点击交互。
6+
7+
#### Scenario: 点击归档提案卡进入列表页
8+
9+
- **WHEN** 用户点击概览页 `OverviewStatsBar` 的「归档提案」统计卡
10+
- **THEN** 路由跳转至 `/proposal` 列表页
11+
12+
#### Scenario: 点击能力规约卡进入 specs 浏览页
13+
14+
- **WHEN** 用户点击概览页 `OverviewStatsBar` 的「能力规约」统计卡
15+
- **THEN** 路由跳转至 `/specs` 只读能力规约浏览页
16+
17+
#### Scenario: 其他统计卡无跳转
18+
19+
- **WHEN** 用户点击「项目准则」或「溯源覆盖」统计卡
20+
- **THEN** 不触发路由跳转
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
## ADDED Requirements
2+
3+
### Requirement: 只读能力规约浏览页
4+
5+
系统 SHALL 提供 `/specs` 只读页面,用于浏览当前项目 `openspec/specs/<capability>/spec.md` 中的能力规约。该页面 SHALL 从概览页「能力规约」统计卡下钻进入,且 SHALL NOT 提供编辑、新建、删除、保存或归档 spec 的操作入口。
6+
7+
#### Scenario: 从概览进入 specs 页面
8+
9+
- **WHEN** 用户在已打开项目的情况下点击概览页「能力规约」统计卡
10+
- **THEN** 路由跳转到 `/specs`
11+
- **AND** 页面展示当前项目的能力规约浏览界面
12+
- **AND** 页面不展示任何编辑、新建、删除、保存或归档 spec 的按钮
13+
14+
#### Scenario: specs 页面不加入 ActivityBar
15+
16+
- **WHEN** 应用渲染 ActivityBar 主导航
17+
- **THEN** 主导航不出现指向 `/specs` 的入口
18+
- **AND** 用户仍可通过概览页下钻或直接访问 `/specs` 打开页面
19+
20+
### Requirement: specs browser 数据读取与解析
21+
22+
系统 SHALL 读取当前项目 `openspec/specs/` 下每个一级子目录中的 `spec.md`,并为每个文件返回只读 `SpecBrowserItem`。每个 item SHALL 包含 capability `id``purpose``sourcePath``updatedAt``requirementsCount``scenariosCount``requirementGroups`。系统 SHALL NOT 返回 spec 中不存在的 `title``family``familyLabel` 或持久化 `anchors` 字段。
23+
24+
#### Scenario: 标准 spec.md 被解析为 item
25+
26+
- **WHEN** 项目存在 `openspec/specs/project-overview/spec.md`
27+
- **AND** 文件包含 `## Purpose``### Requirement:``#### Scenario:` 段落
28+
- **THEN** specs browser 数据中包含 `id === "project-overview"` 的 item
29+
- **AND** `purpose` 来自 `## Purpose` 段落内容
30+
- **AND** `requirementsCount` 等于解析出的 Requirement 数量
31+
- **AND** `scenariosCount` 等于所有 Requirement 下 Scenario 数量之和
32+
- **AND** `requirementGroups` 包含每个 Requirement 的标题、body 与 scenarios
33+
34+
#### Scenario: 不返回派生 UI 字段
35+
36+
- **WHEN** renderer 获取 specs browser 数据
37+
- **THEN** 每个 item 不包含 `title`
38+
- **AND** 每个 item 不包含 `family``familyLabel`
39+
- **AND** 每个 item 不包含 `anchors`
40+
41+
#### Scenario: openspec specs 目录缺失
42+
43+
- **WHEN** 当前项目不存在 `openspec/specs/`
44+
- **THEN** specs browser API 返回空列表
45+
- **AND** renderer 展示空状态而不是错误状态
46+
47+
### Requirement: Requirement 与 Scenario 结构化展示
48+
49+
Specs 详情页 SHALL 在 header 中展示 capability id、purpose、source path、更新时间、Requirements 数量与 Scenarios 数量。详情正文 SHALL 跳过 `#` 一级标题和 `## Purpose` 原文,只展示 Requirements 及其 Scenarios。Requirement 标题与 Scenario 标题 SHALL 由 Vue 文本渲染;Requirement body 与 Scenario body SHALL 使用 `MarkStream` 渲染 markdown。
50+
51+
#### Scenario: 详情 header 展示元数据
52+
53+
- **WHEN** 用户选中某个 capability
54+
- **THEN** 详情 header 显示该 capability 的 id
55+
- **AND** 显示 purpose
56+
- **AND** 显示 source path
57+
- **AND** 显示 updatedAt
58+
- **AND** 显示 Requirements 和 Scenarios 两个统计数字
59+
60+
#### Scenario: 正文跳过重复标题和 Purpose
61+
62+
- **WHEN** specs 页面渲染某个 capability 的详情正文
63+
- **THEN** 正文不渲染 `# <capability> 规范`
64+
- **AND** 正文不重复渲染 `## Purpose` 段落
65+
- **AND** 正文只展示解析后的 Requirement 和 Scenario 内容
66+
67+
#### Scenario: Scenario markdown 保留格式
68+
69+
- **WHEN** Scenario body 包含 `- **WHEN**``- **THEN**` 或 inline code
70+
- **THEN** 页面使用 `MarkStream` 渲染 Scenario body
71+
- **AND** 加粗、列表和 inline code 语义被保留
72+
73+
### Requirement: Requirement 快速定位与 Scenario timeline
74+
75+
Specs 详情页 SHALL 为当前 capability 提供 Requirement 快速定位栏。该栏 SHALL 从 `selectedSpec.requirementGroups` 派生,不依赖 DTO 中的 anchors 字段。点击某个 Requirement 项 SHALL 滚动到对应 Requirement。每个 Requirement 下的 Scenarios SHALL 采用纵向 timeline 视觉,以一条线串联所有 Scenario 节点。
76+
77+
#### Scenario: 点击 Requirement 索引定位
78+
79+
- **WHEN** 用户点击详情内 Requirement 快速定位栏中的某一项
80+
- **THEN** 阅读区滚动到对应 Requirement
81+
- **AND** 被点击的索引项显示高亮状态
82+
83+
#### Scenario: Scenario 使用 timeline 视觉
84+
85+
- **WHEN** 某个 Requirement 下存在多个 Scenario
86+
- **THEN** 页面以纵向线条串联这些 Scenario
87+
- **AND** 每个 Scenario 显示一个节点圆点
88+
- **AND** Scenario 标题和 markdown body 仍按原顺序展示

0 commit comments

Comments
 (0)