Compare commits
4 Commits
4caf502908
...
13d1fea5fb
| Author | SHA1 | Date | |
|---|---|---|---|
| 13d1fea5fb | |||
| 7dc3a270ae | |||
| bc54f8352a | |||
| 1e3269380e |
139
DEVELOPMENT.md
139
DEVELOPMENT.md
@@ -30,21 +30,22 @@ src/
|
|||||||
static.ts 生产模式静态资源服务(SPA fallback、Content-Type 映射、immutable 缓存)
|
static.ts 生产模式静态资源服务(SPA fallback、Content-Type 映射、immutable 缓存)
|
||||||
helpers.ts 共享响应格式化工具(见下方函数清单)
|
helpers.ts 共享响应格式化工具(见下方函数清单)
|
||||||
middleware.ts API 参数校验中间件(validateIdParam、validatePagination、validateTimeRange)
|
middleware.ts API 参数校验中间件(validateIdParam、validatePagination、validateTimeRange)
|
||||||
routes/ API 路由 handler(按端点拆分)
|
routes/ API 路由 handler(按端点拆分)
|
||||||
health.ts GET /health
|
meta.ts GET /api/meta
|
||||||
shared/
|
version.ts 运行时版本号读取(从 package.json 读取并验证)
|
||||||
api.ts 前后端共享 TypeScript 类型
|
shared/
|
||||||
app.ts 应用全局常量(name、title、subtitle、description、version)
|
api.ts 前后端共享 TypeScript 类型
|
||||||
|
app.ts 应用全局常量(name、title、subtitle、description)
|
||||||
web/ React 前端(通过 Vite 构建)
|
web/ React 前端(通过 Vite 构建)
|
||||||
index.html HTML 入口
|
index.html HTML 入口
|
||||||
app.tsx 根组件(Admin 布局:Header + Sidebar + Content)
|
app.tsx 根组件(Admin 布局:Header + Sidebar + Content + 版本号展示)
|
||||||
main.tsx 入口(BrowserRouter + QueryClient 挂载 + ErrorBoundary + ReactQueryDevtools + TDesign CSS 导入)
|
main.tsx 入口(BrowserRouter + QueryClient 挂载 + ErrorBoundary + ReactQueryDevtools + TDesign CSS 导入)
|
||||||
routes.tsx 路由配置(定义所有页面路由)
|
routes.tsx 路由配置(定义所有页面路由)
|
||||||
styles.css 全局样式与自定义 CSS 变量
|
styles.css 全局样式与自定义 CSS 变量
|
||||||
css.d.ts CSS 模块类型声明
|
css.d.ts CSS 模块类型声明
|
||||||
pages/ 页面组件
|
pages/ 页面组件
|
||||||
dashboard/
|
dashboard/
|
||||||
index.tsx 仪表盘页(欢迎语 + /health 联调示例)
|
index.tsx 仪表盘页(欢迎语 + /api/meta 联调示例)
|
||||||
users/
|
users/
|
||||||
index.tsx 用户管理页(占位)
|
index.tsx 用户管理页(占位)
|
||||||
settings/
|
settings/
|
||||||
@@ -54,7 +55,7 @@ src/
|
|||||||
components/ UI 组件
|
components/ UI 组件
|
||||||
ErrorBoundary.tsx React 错误边界,捕获渲染异常并展示降级 UI
|
ErrorBoundary.tsx React 错误边界,捕获渲染异常并展示降级 UI
|
||||||
Sidebar/
|
Sidebar/
|
||||||
index.tsx 侧边栏菜单组件(TDesign Menu + 折叠控制)
|
index.tsx 侧边栏菜单组件(TDesign Menu + 底部折叠按钮)
|
||||||
hooks/ React hooks
|
hooks/ React hooks
|
||||||
use-theme-preference.ts 主题偏好 hook(system/light/dark,localStorage 记忆 + matchMedia 监听)
|
use-theme-preference.ts 主题偏好 hook(system/light/dark,localStorage 记忆 + matchMedia 监听)
|
||||||
use-sidebar-collapsed.ts 侧边栏折叠状态 hook(localStorage 记忆)
|
use-sidebar-collapsed.ts 侧边栏折叠状态 hook(localStorage 记忆)
|
||||||
@@ -62,10 +63,12 @@ src/
|
|||||||
time.ts 时间处理(formatCountdown、formatDurationUnit、formatRelativeTime、isOlderThan、subtractHours)
|
time.ts 时间处理(formatCountdown、formatDurationUnit、formatRelativeTime、isOlderThan、subtractHours)
|
||||||
menu.tsx 菜单配置(路由与菜单项统一数据源)
|
menu.tsx 菜单配置(路由与菜单项统一数据源)
|
||||||
routes.tsx 路由配置(定义所有页面路由)
|
routes.tsx 路由配置(定义所有页面路由)
|
||||||
scripts/
|
scripts/
|
||||||
dev.ts 双进程开发服务(Bun API server + Vite dev server)
|
dev.ts 双进程开发服务(Bun API server + Vite dev server)
|
||||||
build.ts Vite → codegen → Bun compile 三步构建流水线
|
build.ts Vite → codegen → Bun compile 三步构建流水线(含版本号注入)
|
||||||
clean.ts 清理构建产物与临时文件
|
bump-version-logic.ts 纯版本管理逻辑(parse、validate、bump、format)
|
||||||
|
bump-version.ts 版本升迁 CLI 脚本
|
||||||
|
clean.ts 清理构建产物与临时文件
|
||||||
tests/ Bun test 测试(结构镜像 src 目录)
|
tests/ Bun test 测试(结构镜像 src 目录)
|
||||||
setup.ts 全局测试配置(jsdom、polyfill)
|
setup.ts 全局测试配置(jsdom、polyfill)
|
||||||
helpers.ts 测试辅助工具(rmRetry)
|
helpers.ts 测试辅助工具(rmRetry)
|
||||||
@@ -77,7 +80,7 @@ tests/ Bun test 测试(结构镜像 src 目录)
|
|||||||
web/ 前端测试
|
web/ 前端测试
|
||||||
App.test.tsx
|
App.test.tsx
|
||||||
test-utils.tsx
|
test-utils.tsx
|
||||||
openspec/ OpenSpec 变更与规格文档
|
openspec/ OpenSpec 变更、规格文档与 fast-drive workflow schema
|
||||||
config.example.yaml 配置文件示例
|
config.example.yaml 配置文件示例
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -85,7 +88,7 @@ config.example.yaml 配置文件示例
|
|||||||
|
|
||||||
## 前后端边界
|
## 前后端边界
|
||||||
|
|
||||||
前端只通过 HTTP 调用后端,API 路径为 `/api/*` 和 `/health`。共享类型放在 `src/shared`,前端不得 `import src/server` 的运行时实现。
|
前端只通过 HTTP 调用后端,API 路径为 `/api/*`。共享类型放在 `src/shared`,前端不得 `import src/server` 的运行时实现。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -129,8 +132,8 @@ HTTP 请求:
|
|||||||
// server.ts 中的路由注册
|
// server.ts 中的路由注册
|
||||||
routes: {
|
routes: {
|
||||||
"/api/*": () => jsonResponse(createApiError("API route not found", 404), { mode, status: 404 }),
|
"/api/*": () => jsonResponse(createApiError("API route not found", 404), { mode, status: 404 }),
|
||||||
"/health": {
|
"/api/meta": {
|
||||||
GET: () => handleHealth(mode),
|
GET: async () => handleMeta(mode, await resolveVersion()),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -138,8 +141,8 @@ routes: {
|
|||||||
Handler 函数签名:
|
Handler 函数签名:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// 无依赖的路由
|
// 带版本号参数的路由
|
||||||
export function handleHealth(mode: RuntimeMode): Response;
|
export function handleMeta(mode: RuntimeMode, version: string): Response;
|
||||||
```
|
```
|
||||||
|
|
||||||
**请求处理流程**:
|
**请求处理流程**:
|
||||||
@@ -161,7 +164,7 @@ export function handleHealth(mode: RuntimeMode): Response;
|
|||||||
- **`helpers.ts`**:跨路由共用的响应工具函数
|
- **`helpers.ts`**:跨路由共用的响应工具函数
|
||||||
- `createApiError(error, status)` — 构造 API 错误体
|
- `createApiError(error, status)` — 构造 API 错误体
|
||||||
- `createHeaders(mode, init)` — 创建响应 Headers(生产模式附加安全头:`X-Content-Type-Options`、`Referrer-Policy`)
|
- `createHeaders(mode, init)` — 创建响应 Headers(生产模式附加安全头:`X-Content-Type-Options`、`Referrer-Policy`)
|
||||||
- `createHealthResponse()` — 构造健康检查响应 `{ ok: true, service, timestamp }`
|
- `createMetaResponse(version)` — 构造应用元信息响应 `{ ok: true, service, timestamp, version }`
|
||||||
- `formatDuration(ms)` — 毫秒转为可读时长字符串
|
- `formatDuration(ms)` — 毫秒转为可读时长字符串
|
||||||
- `jsonResponse(body, options)` — JSON 响应构造
|
- `jsonResponse(body, options)` — JSON 响应构造
|
||||||
|
|
||||||
@@ -177,10 +180,11 @@ export function handleHealth(mode: RuntimeMode): Response;
|
|||||||
### 1.5 类型定义规范
|
### 1.5 类型定义规范
|
||||||
|
|
||||||
- **共享类型**以 `src/shared/api.ts` 为唯一源头,前后端共同引用
|
- **共享类型**以 `src/shared/api.ts` 为唯一源头,前后端共同引用
|
||||||
- **应用常量**以 `src/shared/app.ts` 为唯一源头,定义 `APP` 对象(name、title、subtitle、description、version),前后端及构建脚本共同引用
|
- **应用常量**以 `src/shared/app.ts` 为唯一源头,定义 `APP` 对象(name、title、subtitle、description),前后端及构建脚本共同引用
|
||||||
|
- **版本号**以 `package.json.version` 为唯一源头,通过 `src/server/version.ts` 运行时读取或构建时注入字面量
|
||||||
- 前端不得 `import src/server/` 下的任何文件
|
- 前端不得 `import src/server/` 下的任何文件
|
||||||
- **严格联合类型**优先于宽类型:如 `RuntimeMode: "development" | "production" | "test"` 而非 `RuntimeMode: string`
|
- **严格联合类型**优先于宽类型:如 `RuntimeMode: "development" | "production" | "test"` 而非 `RuntimeMode: string`
|
||||||
- API 响应类型(`ApiErrorResponse`、`HealthResponse`)定义在 shared 中
|
- API 响应类型(`ApiErrorResponse`、`MetaResponse`)定义在 shared 中
|
||||||
|
|
||||||
### 1.6 配置文件规范
|
### 1.6 配置文件规范
|
||||||
|
|
||||||
@@ -208,7 +212,29 @@ server:
|
|||||||
port: 3000
|
port: 3000
|
||||||
```
|
```
|
||||||
|
|
||||||
### 1.7 错误模式
|
### 1.7 版本管理
|
||||||
|
|
||||||
|
项目使用 `package.json.version` 作为版本号唯一来源,严格 `MAJOR.MINOR.PATCH` 格式。
|
||||||
|
|
||||||
|
**版本获取方式**:
|
||||||
|
|
||||||
|
- 开发模式:`src/server/version.ts` 运行时从 `package.json` 读取版本号
|
||||||
|
- 生产模式:`scripts/build.ts` 在构建时将版本号烘焙为 `APP_VERSION` 字面量注入 `server-entry.ts`
|
||||||
|
|
||||||
|
**版本升迁命令**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun run version:patch # 升迁 patch 版本(0.1.0 → 0.1.1)
|
||||||
|
bun run version:minor # 升迁 minor 版本(0.1.0 → 0.2.0)
|
||||||
|
bun run version:major # 升迁 major 版本(0.1.0 → 1.0.0)
|
||||||
|
bun run version:set 2.0.0 # 显式设置版本号
|
||||||
|
```
|
||||||
|
|
||||||
|
版本升迁仅更新 `package.json.version`,不自动创建 git commit、tag 或 changelog。
|
||||||
|
|
||||||
|
**API 暴露**:`GET /api/meta` 返回 `{ ok, service, timestamp, version }`,前端通过此接口获取并展示版本号。
|
||||||
|
|
||||||
|
### 1.8 错误模式
|
||||||
|
|
||||||
- **API 错误**:`{ error: "描述", status: <code> }`,状态码 400/404
|
- **API 错误**:`{ error: "描述", status: <code> }`,状态码 400/404
|
||||||
- **日志**:非致命异常用 `console.warn`,启动失败用 `console.error` + `process.exit(1)`
|
- **日志**:非致命异常用 `console.warn`,启动失败用 `console.error` + `process.exit(1)`
|
||||||
@@ -242,13 +268,13 @@ main.tsx
|
|||||||
│ ├── useThemePreference() ── Header 主题模式 RadioGroup(系统/明亮/黑暗)
|
│ ├── useThemePreference() ── Header 主题模式 RadioGroup(系统/明亮/黑暗)
|
||||||
│ ├── useSidebarCollapsed() ── 侧边栏折叠状态(localStorage 记忆)
|
│ ├── useSidebarCollapsed() ── 侧边栏折叠状态(localStorage 记忆)
|
||||||
│ ├── Layout
|
│ ├── Layout
|
||||||
│ │ ├── Header(折叠按钮 + 品牌名 + 页标题 + 主题切换)
|
│ │ ├── Header(品牌名 + 版本号 + 页标题 + 主题切换)
|
||||||
│ │ └── Layout(嵌套)
|
│ │ └── Layout(嵌套)
|
||||||
│ │ ├── Aside
|
│ │ ├── Aside
|
||||||
│ │ │ └── Sidebar(TDesign Menu,菜单项点击导航)
|
│ │ │ └── Sidebar(TDesign Menu + 底部折叠按钮,菜单项点击导航)
|
||||||
│ │ └── Content
|
│ │ └── Content
|
||||||
│ │ └── AppRoutes(路由配置)
|
│ │ └── AppRoutes(路由配置)
|
||||||
│ │ ├── / → DashboardPage(欢迎语 + /health 联调)
|
│ │ ├── / → DashboardPage(欢迎语 + /api/meta 联调)
|
||||||
│ │ ├── /users → UsersPage(占位)
|
│ │ ├── /users → UsersPage(占位)
|
||||||
│ │ ├── /settings → SettingsPage(占位)
|
│ │ ├── /settings → SettingsPage(占位)
|
||||||
│ │ └── * → NotFoundPage(404)
|
│ │ └── * → NotFoundPage(404)
|
||||||
@@ -288,7 +314,7 @@ utils/menu-config.ts(路由与菜单统一数据源)
|
|||||||
```typescript
|
```typescript
|
||||||
// 使用 structured array(非字符串),以便精确匹配和按 prefix 失效
|
// 使用 structured array(非字符串),以便精确匹配和按 prefix 失效
|
||||||
const queryKeys = {
|
const queryKeys = {
|
||||||
health: () => ["health"] as const,
|
meta: () => ["meta"] as const,
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -300,8 +326,8 @@ const queryKeys = {
|
|||||||
```typescript
|
```typescript
|
||||||
// 全局级查询(需要持续刷新)
|
// 全局级查询(需要持续刷新)
|
||||||
useQuery({
|
useQuery({
|
||||||
queryKey: queryKeys.health(),
|
queryKey: queryKeys.meta(),
|
||||||
queryFn: () => fetchJson<HealthResponse>("/health"),
|
queryFn: () => fetchJson<MetaResponse>("/api/meta"),
|
||||||
refetchInterval: 30000, // 30s 轮询
|
refetchInterval: 30000, // 30s 轮询
|
||||||
refetchIntervalInBackground: false, // 切后台不轮询
|
refetchIntervalInBackground: false, // 切后台不轮询
|
||||||
staleTime: 5000, // 5s 内视为 fresh
|
staleTime: 5000, // 5s 内视为 fresh
|
||||||
@@ -344,7 +370,7 @@ new QueryClient({
|
|||||||
- 类型从 `../shared/api` 导入,使用 `type` 导入(`import type { ... }`)
|
- 类型从 `../shared/api` 导入,使用 `type` 导入(`import type { ... }`)
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import type { HealthResponse } from "../shared/api";
|
import type { MetaResponse } from "../shared/api";
|
||||||
|
|
||||||
interface AppProps {
|
interface AppProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
@@ -409,7 +435,7 @@ bun run dev [config.yaml]
|
|||||||
- **Bun API server**(端口 3000):后端 API 服务,`--watch` 监听后端文件变更自动重启
|
- **Bun API server**(端口 3000):后端 API 服务,`--watch` 监听后端文件变更自动重启
|
||||||
- **Vite dev server**(端口 5173):前端 SPA + HMR 热更新
|
- **Vite dev server**(端口 5173):前端 SPA + HMR 热更新
|
||||||
|
|
||||||
开发时访问 `http://127.0.0.1:5173`,Vite 自动将 `/api` 和 `/health` 请求代理到后端。
|
开发时访问 `http://127.0.0.1:5173`,Vite 自动将 `/api` 请求代理到后端。
|
||||||
|
|
||||||
也可以单独启动:
|
也可以单独启动:
|
||||||
|
|
||||||
@@ -426,7 +452,7 @@ bun run dev:web # 仅启动 Vite dev server
|
|||||||
|
|
||||||
- Vite dev server 负责前端 SPA、HMR、模块热替换
|
- Vite dev server 负责前端 SPA、HMR、模块热替换
|
||||||
- Bun API server 负责后端 API 路由
|
- Bun API server 负责后端 API 路由
|
||||||
- Vite 通过 proxy 配置将 `/api/*` 和 `/health` 转发到 Bun
|
- Vite 通过 proxy 配置将 `/api/*` 转发到 Bun
|
||||||
|
|
||||||
#### 生产模式架构
|
#### 生产模式架构
|
||||||
|
|
||||||
@@ -444,16 +470,16 @@ const server = Bun.serve({
|
|||||||
},
|
},
|
||||||
routes: {
|
routes: {
|
||||||
"/api/*": () => ...,
|
"/api/*": () => ...,
|
||||||
"/health": { GET: () => handleHealth(mode) },
|
"/api/meta": { GET: async () => handleMeta(mode, await resolveVersion()) },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 路由优先级
|
#### 路由优先级
|
||||||
|
|
||||||
Bun routes 的匹配规则:具体路径 > 通配符。`/health` 优先于 `/*`。
|
Bun routes 的匹配规则:具体路径 > 通配符。`/api/meta` 优先于 `/api/*`。
|
||||||
|
|
||||||
未匹配 method 的请求(如 POST /health)会落入 `/api/*` 通配符返回 404;若无该通配符会落入 fetch fallback。
|
未匹配 method 的请求(如 POST /api/meta)会落入 `/api/*` 通配符返回 404;若无该通配符会落入 fetch fallback。
|
||||||
|
|
||||||
非 API 路径由 fetch fallback 处理:有文件扩展名的返回对应静态资源或 404,无扩展名的返回 SPA index.html。
|
非 API 路径由 fetch fallback 处理:有文件扩展名的返回对应静态资源或 404,无扩展名的返回 SPA index.html。
|
||||||
|
|
||||||
@@ -471,8 +497,8 @@ bun run build
|
|||||||
|
|
||||||
```
|
```
|
||||||
1. Vite build → dist/web/ (前端静态资源,含 code splitting)
|
1. Vite build → dist/web/ (前端静态资源,含 code splitting)
|
||||||
2. Code generation → .build/static-assets.ts + .build/server-entry.ts
|
2. Code generation → .build/static-assets.ts + .build/server-entry.ts(含版本号字面量注入)
|
||||||
3. Bun compile → dist/dial-server (单可执行文件)
|
3. Bun compile → dist/my-app (单可执行文件)
|
||||||
```
|
```
|
||||||
|
|
||||||
- Vite 构建前端资源到 `dist/web/`,自动 code splitting(vendor-react、vendor-tdesign、vendor-chart)
|
- Vite 构建前端资源到 `dist/web/`,自动 code splitting(vendor-react、vendor-tdesign、vendor-chart)
|
||||||
@@ -482,10 +508,10 @@ bun run build
|
|||||||
|
|
||||||
#### 产物
|
#### 产物
|
||||||
|
|
||||||
| 产物 | 用途 |
|
| 产物 | 用途 |
|
||||||
| ------------------ | ---------------------------------------- |
|
| ------------- | ---------------------------------------- |
|
||||||
| `dist/dial-server` | 生产可执行文件(含前端资源,单文件部署) |
|
| `dist/my-app` | 生产可执行文件(含前端资源,单文件部署) |
|
||||||
| `dist/web/` | Vite 构建的前端资源(构建中间产物) |
|
| `dist/web/` | Vite 构建的前端资源(构建中间产物) |
|
||||||
|
|
||||||
#### 构建参数
|
#### 构建参数
|
||||||
|
|
||||||
@@ -496,14 +522,13 @@ bun run build
|
|||||||
#### 运行可执行文件
|
#### 运行可执行文件
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./dist/dial-server [config.yaml]
|
./dist/my-app [config.yaml]
|
||||||
```
|
```
|
||||||
|
|
||||||
启动后:
|
启动后:
|
||||||
|
|
||||||
- 访问 `http://127.0.0.1:3000/` → 返回前端 SPA
|
- 访问 `http://127.0.0.1:3000/` → 返回前端 SPA
|
||||||
- 访问 `http://127.0.0.1:3000/api/*` → 返回后端 API
|
- 访问 `http://127.0.0.1:3000/api/meta` → 返回应用元信息 JSON(含版本号)
|
||||||
- 访问 `http://127.0.0.1:3000/health` → 返回健康检查 JSON
|
|
||||||
|
|
||||||
#### 清理
|
#### 清理
|
||||||
|
|
||||||
@@ -534,13 +559,17 @@ bun run verify
|
|||||||
|
|
||||||
### 3.5 脚本说明
|
### 3.5 脚本说明
|
||||||
|
|
||||||
| 脚本 | 文件 | 说明 |
|
| 脚本 | 文件 | 说明 |
|
||||||
| -------------------- | ------------------- | ---------------------------------------- |
|
| ----------------------- | ------------------------- | ---------------------------------------- |
|
||||||
| `bun run dev` | `scripts/dev.ts` | 双进程开发服务(Vite :5173 + API :3000) |
|
| `bun run dev` | `scripts/dev.ts` | 双进程开发服务(Vite :5173 + API :3000) |
|
||||||
| `bun run dev:server` | `src/server/dev.ts` | 仅启动后端 API server(--watch 模式) |
|
| `bun run dev:server` | `src/server/dev.ts` | 仅启动后端 API server(--watch 模式) |
|
||||||
| `bun run dev:web` | Vite CLI | 仅启动 Vite dev server |
|
| `bun run dev:web` | Vite CLI | 仅启动 Vite dev server |
|
||||||
| `bun run build` | `scripts/build.ts` | Vite → codegen → Bun compile 三步构建 |
|
| `bun run build` | `scripts/build.ts` | Vite → codegen → Bun compile 三步构建 |
|
||||||
| `bun run clean` | `scripts/clean.ts` | 清理构建缓存与临时文件 |
|
| `bun run clean` | `scripts/clean.ts` | 清理构建缓存与临时文件 |
|
||||||
|
| `bun run version:patch` | `scripts/bump-version.ts` | 升迁 patch 版本(x.y.Z) |
|
||||||
|
| `bun run version:minor` | `scripts/bump-version.ts` | 升迁 minor 版本(x.Y.0) |
|
||||||
|
| `bun run version:major` | `scripts/bump-version.ts` | 升迁 major 版本(X.0.0) |
|
||||||
|
| `bun run version:set` | `scripts/bump-version.ts` | 显式设置版本号 |
|
||||||
|
|
||||||
### 3.6 环境变量
|
### 3.6 环境变量
|
||||||
|
|
||||||
@@ -668,10 +697,10 @@ CI 或正式提交前执行完整验证(类型检查 + lint + 格式 + 测试
|
|||||||
|
|
||||||
### 测试分层
|
### 测试分层
|
||||||
|
|
||||||
| 层级 | 覆盖范围 | 位置 | 命令 |
|
| 层级 | 覆盖范围 | 位置 | 命令 |
|
||||||
| -------- | ---------------------- | ------------------------------------------------------------------- | --------------------------------------------- |
|
| -------- | ---------------------- | ------------------------------------------------------------------------------------------------- | --------------------------------------------- |
|
||||||
| 单元测试 | 后端函数、纯函数、常量 | `tests/server/**/*.test.ts`、`tests/web/{utils,hooks}/**/*.test.ts` | `bun test tests/server`、`bun test tests/web` |
|
| 单元测试 | 后端函数、纯函数、常量 | `tests/server/**/*.test.ts`、`tests/scripts/**/*.test.ts`、`tests/web/{utils,hooks}/**/*.test.ts` | `bun test tests/server`、`bun test tests/web` |
|
||||||
| 组件测试 | React 组件渲染和交互 | `tests/web/components/**/*.test.tsx` | `bun test tests/web` |
|
| 组件测试 | React 组件渲染和交互 | `tests/web/components/**/*.test.tsx` | `bun test tests/web` |
|
||||||
|
|
||||||
### 运行命令
|
### 运行命令
|
||||||
|
|
||||||
|
|||||||
50
README.md
50
README.md
@@ -35,11 +35,10 @@ export const APP = {
|
|||||||
title: "Your App", // 人类可读标题
|
title: "Your App", // 人类可读标题
|
||||||
subtitle: "你的副标题", // 副标题
|
subtitle: "你的副标题", // 副标题
|
||||||
description: "应用描述", // SEO meta 描述
|
description: "应用描述", // SEO meta 描述
|
||||||
version: "0.1.0", // 版本号
|
|
||||||
} as const;
|
} as const;
|
||||||
```
|
```
|
||||||
|
|
||||||
同时修改 `package.json` 的 `name` 字段保持一致。
|
同时修改 `package.json` 的 `name` 字段保持一致,`version` 字段管理应用版本号。
|
||||||
|
|
||||||
> **注意**:localStorage key 已从 `"my-app.theme.preference"` 变更为 `"theme.preference"`。如果从旧版本升级,用户的主题偏好设置将丢失,需重新选择。
|
> **注意**:localStorage key 已从 `"my-app.theme.preference"` 变更为 `"theme.preference"`。如果从旧版本升级,用户的主题偏好设置将丢失,需重新选择。
|
||||||
|
|
||||||
@@ -52,7 +51,7 @@ rm -rf openspec/specs/*
|
|||||||
rm -rf openspec/changes/*
|
rm -rf openspec/changes/*
|
||||||
```
|
```
|
||||||
|
|
||||||
> `openspec/config.yaml` 需要保留,其中包含项目开发规范配置。
|
> `openspec/config.yaml` 和 `openspec/schemas/fast-drive/` 需要保留,其中包含项目开发规范配置与自定义 OpenSpec workflow schema。
|
||||||
|
|
||||||
### 4. 安装依赖
|
### 4. 安装依赖
|
||||||
|
|
||||||
@@ -68,19 +67,23 @@ bun run dev
|
|||||||
|
|
||||||
## 项目管理
|
## 项目管理
|
||||||
|
|
||||||
| 命令 | 说明 |
|
| 命令 | 说明 |
|
||||||
| -------------------- | ---------------------------------------------------------- |
|
| ----------------------- | ---------------------------------------------------------- |
|
||||||
| `bun run dev` | 启动开发模式(并行启动后端 + 前端 Vite 开发服务器) |
|
| `bun run dev` | 启动开发模式(并行启动后端 + 前端 Vite 开发服务器) |
|
||||||
| `bun run dev:server` | 仅启动后端开发服务(`--watch` 热重载) |
|
| `bun run dev:server` | 仅启动后端开发服务(`--watch` 热重载) |
|
||||||
| `bun run dev:web` | 仅启动前端 Vite 开发服务器 |
|
| `bun run dev:web` | 仅启动前端 Vite 开发服务器 |
|
||||||
| `bun run build` | 生产构建(Vite 打包前端 → Bun compile 生成独立可执行文件) |
|
| `bun run build` | 生产构建(Vite 打包前端 → Bun compile 生成独立可执行文件) |
|
||||||
| `bun test` | 运行全部测试 |
|
| `bun test` | 运行全部测试 |
|
||||||
| `bun run lint` | ESLint 代码风格检查 |
|
| `bun run lint` | ESLint 代码风格检查 |
|
||||||
| `bun run format` | Prettier 代码格式化 |
|
| `bun run format` | Prettier 代码格式化 |
|
||||||
| `bun run typecheck` | TypeScript 类型检查 |
|
| `bun run typecheck` | TypeScript 类型检查 |
|
||||||
| `bun run check` | 完整质量检查:typecheck + lint + test |
|
| `bun run check` | 完整质量检查:typecheck + lint + test |
|
||||||
| `bun run verify` | 验证构建流程:check + build |
|
| `bun run verify` | 验证构建流程:check + build |
|
||||||
| `bun run clean` | 清理构建产物和临时文件 |
|
| `bun run clean` | 清理构建产物和临时文件 |
|
||||||
|
| `bun run version:patch` | 升迁 patch 版本(x.y.Z) |
|
||||||
|
| `bun run version:minor` | 升迁 minor 版本(x.Y.0) |
|
||||||
|
| `bun run version:major` | 升迁 major 版本(X.0.0) |
|
||||||
|
| `bun run version:set` | 显式设置版本号 |
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
@@ -96,7 +99,9 @@ bun run dev
|
|||||||
├── .lintstagedrc.json # lint-staged 暂存区检查配置
|
├── .lintstagedrc.json # lint-staged 暂存区检查配置
|
||||||
├── scripts/
|
├── scripts/
|
||||||
│ ├── dev.ts # 开发启动脚本(并行启动 API + Vite)
|
│ ├── dev.ts # 开发启动脚本(并行启动 API + Vite)
|
||||||
│ ├── build.ts # 生产构建脚本(Vite → 代码生成 → Bun compile)
|
│ ├── build.ts # 生产构建脚本(Vite → 代码生成 → Bun compile,含版本号注入)
|
||||||
|
│ ├── bump-version-logic.ts # 纯版本管理逻辑(parse、validate、bump、format)
|
||||||
|
│ ├── bump-version.ts # 版本升迁 CLI 脚本
|
||||||
│ └── clean.ts # 清理脚本
|
│ └── clean.ts # 清理脚本
|
||||||
├── src/
|
├── src/
|
||||||
│ ├── server/ # 后端代码
|
│ ├── server/ # 后端代码
|
||||||
@@ -108,15 +113,16 @@ bun run dev
|
|||||||
│ │ ├── helpers.ts # 共享响应工具(健康检查、JSON 响应)
|
│ │ ├── helpers.ts # 共享响应工具(健康检查、JSON 响应)
|
||||||
│ │ ├── middleware.ts # API 参数校验中间件
|
│ │ ├── middleware.ts # API 参数校验中间件
|
||||||
│ │ ├── static.ts # 静态资源服务
|
│ │ ├── static.ts # 静态资源服务
|
||||||
│ │ └── routes/ # API 路由处理器
|
│ │ └── routes/ # API 路由处理器
|
||||||
│ │ └── health.ts # 健康检查端点
|
│ │ └── meta.ts # 应用元信息端点(GET /api/meta)
|
||||||
|
│ │ version.ts # 版本号读取
|
||||||
│ ├── shared/
|
│ ├── shared/
|
||||||
│ │ ├── api.ts # 前后端共享 TypeScript 类型定义
|
│ │ ├── api.ts # 前后端共享 TypeScript 类型定义
|
||||||
│ │ └── app.ts # 应用全局常量(name、title、version 等)
|
│ │ └── app.ts # 应用全局常量(name、title、subtitle、description)
|
||||||
│ └── web/ # 前端代码
|
│ └── web/ # 前端代码
|
||||||
│ ├── index.html # HTML 入口
|
│ ├── index.html # HTML 入口
|
||||||
│ ├── main.tsx # React 入口(BrowserRouter + QueryClient + ErrorBoundary)
|
│ ├── main.tsx # React 入口(BrowserRouter + QueryClient + ErrorBoundary)
|
||||||
│ ├── app.tsx # 根组件(Admin 布局:Header + Sidebar + Content)
|
│ ├── app.tsx # 根组件(Admin 布局:Header + Sidebar + Content + 版本号展示)
|
||||||
│ ├── routes.tsx # 路由配置
|
│ ├── routes.tsx # 路由配置
|
||||||
│ ├── styles.css # 全局样式
|
│ ├── styles.css # 全局样式
|
||||||
│ ├── css.d.ts # CSS 模块类型声明
|
│ ├── css.d.ts # CSS 模块类型声明
|
||||||
@@ -133,7 +139,7 @@ bun run dev
|
|||||||
│ ├── menu.tsx # 菜单配置
|
│ ├── menu.tsx # 菜单配置
|
||||||
│ └── routes.tsx # 路由配置
|
│ └── routes.tsx # 路由配置
|
||||||
├── tests/ # 测试文件(镜像 src 目录结构)
|
├── tests/ # 测试文件(镜像 src 目录结构)
|
||||||
├── openspec/ # OpenSpec 规格与变更管理
|
├── openspec/ # OpenSpec 规格、变更与 fast-drive workflow schema
|
||||||
└── docs/ # 项目文档
|
└── docs/ # 项目文档
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -4,12 +4,12 @@
|
|||||||
|
|
||||||
## 提示词
|
## 提示词
|
||||||
|
|
||||||
| 文件 | 用途 |
|
| 文件 | 用途 |
|
||||||
| ------------------------------------------------------ | ------------------------------------------------------------------------ |
|
| ------------------------------------------------------ | ------------------------------------------------------------------------- |
|
||||||
| [prompt-smart-merge.md](prompt-smart-merge.md) | 批量合并 `dev*` 分支到目标分支,含规则探测、依赖分析、冲突处理、安全回退 |
|
| [prompt-smart-merge.md](prompt-smart-merge.md) | 批量合并 `dev*` 分支到目标分支,含规则探测、依赖分析、冲突处理、安全回退 |
|
||||||
| [prompt-spec-review.md](prompt-spec-review.md) | 审查和整理 `openspec/specs/` 下的稳定规范,提升可检索性和一致性 |
|
| [prompt-spec-review.md](prompt-spec-review.md) | 审查和整理 `openspec/specs/` 下的稳定规范,提升可检索性和一致性 |
|
||||||
| [prompt-proposal-review.md](prompt-proposal-review.md) | 审查 proposal/design/tasks/specs 与讨论、代码现状、OpenSpec 规范的一致性 |
|
| [prompt-proposal-review.md](prompt-proposal-review.md) | 审查 fast-drive design/tasks 与讨论、实际状态、OpenSpec workflow 的一致性 |
|
||||||
| [prompt-apply-review.md](prompt-apply-review.md) | 审查 apply 后代码、测试、变更文档的一致性,并补齐遗漏或回写文档 |
|
| [prompt-apply-review.md](prompt-apply-review.md) | 审查 apply 后实际产物、验证、design/tasks 的一致性,并补齐遗漏或回写文档 |
|
||||||
|
|
||||||
## 设计目标
|
## 设计目标
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
审查 OpenSpec apply 完成后以及后续手动修补后的实际实现,判断代码、测试、变更文档是否一致,识别偏离、漏记和可优化点,并将确认后的实际变更同步回变更文档,按以下流程执行。
|
审查 OpenSpec apply 完成后以及后续手动修补后的实际变更,判断实际产物、验证结果和变更文档是否与 `design.md` source of truth 一致,识别偏离、漏记和可优化点,并将确认后的实际变更同步回变更文档,按以下流程执行。
|
||||||
|
|
||||||
## 约束
|
## 约束
|
||||||
|
|
||||||
- 先审查再修复;未经用户确认,不修改代码或变更文档
|
- 先审查再修复;未经用户确认,不修改实际产物或变更文档
|
||||||
- 默认按 `spec-driven` workflow 审查;识别 change 后先确认 `schemaName`;若实际 schema 不同,说明差异,仅对实际存在的 artifacts 做审查
|
- 默认按 `fast-drive` workflow 审查;识别 change 后先确认 `schemaName`;若实际 schema 不同,说明差异,仅对实际存在的 artifacts 做审查
|
||||||
- 禁止同步或修改 `openspec/specs/` 下的主规范——主规范同步属于 archive 阶段,不在此提示词范围内;本次 change 目录下的 `specs/*.md`、代码、测试、README 等均可修改
|
- 在 `fast-drive` workflow 下,核心 artifacts 是 `design.md` 和 `tasks.md`;不要要求存在 `proposal.md` 或 `specs/*.md`
|
||||||
- 优先使用当前会话中的实现说明、测试结论、手动修补记录和已生成的变更文档;仅在无法明确 change、`schemaName`、改动范围或修补来源时,再用提问工具或 OpenSpec 命令补充定位
|
- 在 `fast-drive` workflow 下,`design.md` 是 scope、requirements、decisions、guardrails、execution direction 和 verification expectations 的 source of truth,`tasks.md` 是 apply 进度和验证闭环的 tracking 文件
|
||||||
- 不要因为代码已经存在就自动以代码为准;先判断差异属于"文档要求未实现"、"测试后新增修补"还是"意外偏离/回归"
|
- 禁止同步或修改 `openspec/specs/` 下的主规范;若实际 schema 包含 `specs/*.md`,也只允许修改本次 change 目录下实际存在的 spec artifacts,主规范同步属于 archive 阶段,不在此提示词范围内
|
||||||
- 每批代码或文档修改执行前用提问工具获得用户确认
|
- 优先使用当前会话中的执行说明、验证结论、手动修补记录和已生成的变更文档;仅在无法明确 change、`schemaName`、改动范围或修补来源时,再用提问工具或 OpenSpec 命令补充定位
|
||||||
|
- 不要因为实际产物已经存在就自动以实际产物为准;先判断差异属于“design 要求未完成”、“验证后新增修补”、“合理落地细化”还是“意外偏离/回归”
|
||||||
|
- 每批实际产物或文档修改执行前用提问工具获得用户确认
|
||||||
- 删除/重写前用提问工具获得用户确认,并先备份原文件为 `{file}.bak.{timestamp}`
|
- 删除/重写前用提问工具获得用户确认,并先备份原文件为 `{file}.bak.{timestamp}`
|
||||||
- 若修改代码涉及新逻辑、模块结构、API、实体或用户可见行为,同步更新测试、相关变更文档和 README
|
- 若修改实际产物涉及新行为、流程、接口、内容、数据、配置、责任边界或用户可见结果,同步更新验证材料、相关变更文档和必要的文档/沟通材料
|
||||||
|
|
||||||
## 1. 收集
|
## 1. 收集
|
||||||
|
|
||||||
@@ -22,18 +24,34 @@
|
|||||||
|
|
||||||
a) 先并行读取核心入口和配置,确定范围:
|
a) 先并行读取核心入口和配置,确定范围:
|
||||||
|
|
||||||
- 本次 change 的 `proposal.md`
|
- 本次 change 的 `design.md`
|
||||||
- `openspec/config.yaml`
|
- 本次 change 的 `tasks.md`
|
||||||
|
- workflow context/configuration,例如存在时读取 `openspec/config.yaml`
|
||||||
|
- 若可定位到 schema,读取对应 schema;`fast-drive` 下优先读取 `openspec/schemas/fast-drive/schema.yaml`
|
||||||
|
|
||||||
b) 从 `proposal.md` 提取 `Capabilities` / `Modified Capabilities`,确定 proposal 已声明的 spec 列表
|
b) 从 `design.md` 提取审查基准:
|
||||||
|
|
||||||
c) 并行获取改动范围:`git diff --name-only`、`git diff --name-only --cached`;若工作区已干净,再结合文档和代码模块反推
|
- `Context`
|
||||||
|
- `Discussion Notes`
|
||||||
|
- `Requirements`
|
||||||
|
- `Goals / Non-Goals`
|
||||||
|
- `Execution Guardrails`
|
||||||
|
- `Affected Areas`
|
||||||
|
- `Decisions`
|
||||||
|
- `Execution Plan`
|
||||||
|
- `Verification Plan`
|
||||||
|
- `Risks / Trade-offs`
|
||||||
|
- `Open Questions`
|
||||||
|
|
||||||
d) 对比 git diff 涉及的模块与 proposal 已声明的 spec,识别 apply 阶段新增改动触及但 proposal 未覆盖的现有 spec,合并为完整 spec 读取列表;相关性来源还包括:手动修补涉及的受影响能力、`design.md` Impact 中提到的模块、相关代码对应的现有能力
|
c) 从 `tasks.md` 提取任务状态、已完成项、未完成项、验证任务和文档/沟通任务;重点记录所有已标记完成的 `- [x]` 或等价完成状态。
|
||||||
|
|
||||||
e) 并行读取完整 spec 列表和其余 artifacts(`design.md`、`tasks.md`、相关源码、测试文件、README、架构文档)
|
d) 获取实际改动范围:若在版本控制工作区中,优先使用 `git diff --name-only`、`git diff --name-only --cached`;若工作区已干净或不适用版本控制,再结合 `design.md`、`tasks.md`、验证记录和执行记录反推。
|
||||||
|
|
||||||
f) 收集当前会话中与本次变更相关的实现说明、apply 过程中的偏离、测试失败、手动修补原因、待确认事项
|
e) 并行读取实际改动范围、`Affected Areas`、`Execution Plan`、`Verification Plan` 涉及的实际产物、参考材料、验证材料、流程说明、配置、文档或沟通材料。
|
||||||
|
|
||||||
|
f) 收集当前会话中与本次变更相关的执行说明、apply 过程中的偏离、验证失败、手动修补原因、验证命令或检查结果、待确认事项。
|
||||||
|
|
||||||
|
g) 若实际 schema 不是 `fast-drive`,只读取实际存在的 artifacts;若存在 `proposal.md`、`specs/*.md`,再按该 schema 的要求补充读取和审查。
|
||||||
|
|
||||||
若当前上下文无法明确 change 或文档路径:
|
若当前上下文无法明确 change 或文档路径:
|
||||||
|
|
||||||
@@ -42,66 +60,75 @@ f) 收集当前会话中与本次变更相关的实现说明、apply 过程中
|
|||||||
|
|
||||||
若已明确 change,但尚未确认 `schemaName`,先读取 change 元数据或执行 `openspec status --change "{name}" --json` 确认。
|
若已明确 change,但尚未确认 `schemaName`,先读取 change 元数据或执行 `openspec status --change "{name}" --json` 确认。
|
||||||
|
|
||||||
若缺少测试结果或手动修补记录,明确说明本次无法可靠判断部分差异的来源,仅能基于代码与文档现状审查。
|
若缺少验证结果或手动修补记录,明确说明本次无法可靠判断部分差异的来源,仅能基于实际产物与文档现状审查。
|
||||||
|
|
||||||
## 2. 分析
|
## 2. 分析
|
||||||
|
|
||||||
按以下优先级检查:
|
按以下优先级检查:
|
||||||
|
|
||||||
| 优先级 | 维度 | 检查点 |
|
| 优先级 | 维度 | 检查点 |
|
||||||
| ------ | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------ | ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| P0 | 实际实现与测试结论 | 当前代码的真实行为是什么;apply 后是否有手动改动或测试后修补;测试是否证明这些实现有效;若缺少测试结果,标记相关结论为"未验证";检查是否存在回归、未覆盖场景或被掩盖的问题 |
|
| P0 | 实际变更与验证结论 | 当前实际产物的真实状态是什么;apply 后是否有手动改动或验证后修补;验证是否证明这些变更有效;若缺少验证结果,标记相关结论为“未验证”;检查是否存在回归、未覆盖场景或被掩盖的问题 |
|
||||||
| P1 | 文档同步性 | 对本次 change 目录下实际存在的 artifacts 检查:已落地的实现、测试后新增修补、边界处理、异常路径、验证结论是否已同步回变更文档;若影响模块结构、API、实体或用户可见行为,再检查 README 是否同步 |
|
| P1 | `design.md` 一致性 | 实际变更是否符合 `Requirements`、`Goals / Non-Goals`、`Execution Guardrails`、`Decisions`、`Execution Plan` 和 `Verification Plan`;`Open Questions` 是否已明确区分 blocking / non-blocking 或写出 `None`;是否违反被明确否决的方案、用户偏好或约束 |
|
||||||
| P2 | Spec 覆盖完整性 | 对比实际代码改动范围与 proposal 中定义的 `Capabilities` / `Modified Capabilities`,识别 apply 阶段新增的功能是否触及了更多现有 spec;若触及,列入补充清单,在本次 change 的 specs 中新增对应的 spec 文件,并更新 `proposal.md` 的 `Modified Capabilities` |
|
| P2 | `tasks.md` 真实性 | 已完成任务是否真的完成并完成必要验证;未完成任务是否仍然必要;apply 或手动修补是否引入了需要补充的新任务、验证任务或文档/沟通任务 |
|
||||||
| P3 | 文档要求覆盖 | 对实际存在的 artifacts 检查:文档中承诺的目标、方案、Requirement、Scenario 是否都已实现;在 `spec-driven` 下重点检查 `proposal.md`、`design.md`、`specs/*.md`、`tasks.md` |
|
| P3 | 文档回写完整性 | 已落地的实际变更、验证后新增修补、边界处理、异常路径、验证结论、实际触达产物是否已同步回 `design.md` 和 `tasks.md`;若影响行为、流程、接口、内容、数据、配置、责任边界或用户可见结果,再检查必要的文档/沟通材料是否同步 |
|
||||||
| P4 | 实现质量 | 代码结构、复用、命名、复杂度、错误处理、测试质量、与项目现有模式的一致性是否存在明显问题或可优化点 |
|
| P4 | 质量与可维护性 | 实际产物的结构、清晰度、一致性、可维护性、风险处理、移交质量、验证质量、与现有模式的一致性是否存在明显问题或可优化点 |
|
||||||
|
| P5 | Schema 兼容性 | 对实际存在的 artifacts 检查是否符合其 schema;若不是 `fast-drive`,仅按实际 artifacts 检查,不凭空要求 `fast-drive` 专属结构;最终 artifacts 是否仍保留模板注释、空表格行或占位任务文本 |
|
||||||
|
|
||||||
分析时区分三类差异:
|
分析时区分四类差异:
|
||||||
|
|
||||||
- 文档要求已明确,但代码未实现或实现不完整 → 需补充代码或测试
|
- `design.md` 要求已明确,但实际变更未完成或完成不充分 → 需补充实际工作或验证
|
||||||
- 代码因测试暴露问题、手动修补或合理落地细化而新增/变更 → 需回写文档
|
- 实际变更因验证暴露问题、手动修补或合理落地细化而新增/变更 → 需回写 `design.md` 和/或 `tasks.md`
|
||||||
- 代码与文档不一致,且无法判断应以哪边为准 → 列入待确认清单
|
- 实际变更与 `design.md` 不一致,且无法判断应以哪边为准 → 列入待确认清单
|
||||||
|
- `tasks.md` 状态与实际完成情况或验证结果不一致 → 修正任务状态或补充任务
|
||||||
|
|
||||||
不要把以下情况直接视为合理修补:
|
不要把以下情况直接视为合理修补:
|
||||||
|
|
||||||
- 通过 `skip`、`only`、弱化断言、绕过错误处理来让测试通过
|
- 通过跳过、弱化或绕过验证来声称变更完成
|
||||||
- 为了贴合现有代码而降低已确认的 Requirement 或行为约束
|
- 为了贴合当前实际产物而降低已确认的 requirement、acceptance criteria 或 guardrail
|
||||||
- 未经过讨论和验证就扩大功能范围
|
- 未经过讨论和验证就扩大功能、流程、内容或责任范围
|
||||||
|
- 违反 `Execution Guardrails`、被拒绝方案或 `Open Questions` 中尚未解决的 blocker
|
||||||
|
|
||||||
重点识别:
|
重点识别:
|
||||||
|
|
||||||
- 文档要求但未落地的功能、场景、异常处理或验证步骤
|
- `design.md` 要求但未落地的结果、流程、内容、场景、异常处理、文档/沟通更新或验证步骤
|
||||||
- apply 完成后新增的代码修补、边界处理、接口调整、行为变化未同步到文档
|
- 实际变更偏离 `Goals / Non-Goals`、`Execution Guardrails`、`Decisions` 或 `Execution Plan` 的地方
|
||||||
- `tasks.md` 标记完成,但代码、测试或文档未闭环
|
- apply 完成后新增的修补、边界处理、接口调整、行为变化、流程变化或内容变化未同步到 `design.md`
|
||||||
- `Modified Capabilities` 在本次 change 的 specs 中是否已更新(注意:仅更新 change 目录下的 specs,不动 `openspec/specs/`);apply 阶段新增功能触及的未覆盖 spec 是否已补充到本次 change 的 `specs/` 目录
|
- `Affected Areas` 与实际改动范围不一致,导致新会话无法复盘真实影响面
|
||||||
- 代码存在明显的重复、复杂度过高、命名不清、错误处理薄弱、测试质量不足等问题
|
- `Verification Plan` 中要求的验证、质量检查、审阅、批准、沟通检查或 manual checks 未执行或未记录
|
||||||
|
- `tasks.md` 标记完成,但实际产物、验证、文档或沟通未闭环
|
||||||
|
- `design.md` 或 `tasks.md` 仍保留 `<!-- ... -->` 模板注释、空表格行、`Replace with...`、`TBD`、`TODO` 等未解决占位内容
|
||||||
|
- 必要的文档/沟通材料未同步影响行为、流程、接口、内容、数据、配置、责任边界或用户可见结果的变更
|
||||||
|
- 实际产物存在明显的重复、复杂度过高、表达不清、责任不明、风险处理薄弱、验证质量不足等问题
|
||||||
|
- `fast-drive` change 中仍错误依赖 `proposal.md`、`specs/*.md`、`Capabilities` 或 `Modified Capabilities` 的内容
|
||||||
|
|
||||||
输出审查结果:
|
输出审查结果:
|
||||||
|
|
||||||
1. **问题总览表**:问题类型 × 涉及文件数
|
1. **问题总览表**:问题类型 × 涉及文件数
|
||||||
2. **实际改动与修补清单**:本次实现中已落地的主要功能、后续修补和验证结论;若缺少测试结果,对未验证部分单独标记
|
2. **实际变更与修补清单**:本次已落地的主要变更、后续修补和验证结论;若缺少验证结果,对未验证部分单独标记
|
||||||
3. **未覆盖清单**:文档要求但未在代码中实现或未充分验证的内容
|
3. **Design 偏离清单**:实际变更未完成、完成不充分或偏离 `design.md` 的内容
|
||||||
4. **需回写文档清单**:代码和测试中已确认、但文档未体现的实现、修补或约束变化
|
4. **需回写文档清单**:实际产物和验证中已确认、但 `design.md`、`tasks.md` 或相关文档/沟通材料未体现的变更、修补或约束变化
|
||||||
5. **方向待确认清单**:代码与文档不一致,且无法判断应以哪边为准的事项
|
5. **方向待确认清单**:实际变更与 `design.md` 不一致,且无法判断应以哪边为准的事项
|
||||||
6. **Spec 补充清单**:apply 阶段新增功能触及但 proposal 未覆盖的现有 spec,列出需新增的 spec 文件名、对应的主 spec 路径、需描述的变更内容
|
6. **任务状态问题清单**:未真正完成、状态错误或需补充的新任务
|
||||||
7. **任务状态问题清单**:未真正完成、状态错误或需补充的新任务
|
7. **验证问题清单**:缺失覆盖、掩盖错误、验证不足或修补后未回归验证的问题
|
||||||
8. **测试问题清单**:缺失覆盖、掩盖错误、验证不足或修补后未回归验证的测试问题
|
8. **质量/优化清单**:可优化的实际产物问题和建议
|
||||||
9. **代码质量/优化清单**:可优化的实现问题和建议
|
9. **Schema 差异清单**:实际 schema 与默认 `fast-drive` 不同时,列出因此跳过或改按实际 artifacts 审查的内容
|
||||||
10. **逐项分析**:每个问题说明位置、问题、影响、建议和建议修复方向
|
10. **逐项分析**:每个问题说明位置、问题、影响、建议和建议修复方向
|
||||||
|
|
||||||
若所有清单均为空,输出"审查通过,未发现问题",跳至步骤 5。
|
若所有清单均为空,输出“审查通过,未发现问题”,跳至步骤 5。
|
||||||
|
|
||||||
## 3. 计划(用户确认)
|
## 3. 计划(用户确认)
|
||||||
|
|
||||||
先针对"方向待确认清单"用提问工具逐项向用户确认。
|
先针对“方向待确认清单”用提问工具逐项向用户确认。
|
||||||
|
|
||||||
再整理完整修复方案,按类别列出:
|
再整理完整修复方案,按类别列出:
|
||||||
|
|
||||||
- 代码或测试补充:补实现、补异常处理、补回归测试、修复掩盖错误的测试
|
- 实际工作或验证补充:补完成、补异常处理、补回归验证、修复被弱化或绕过的验证
|
||||||
- 文档回写:同步本次 change 目录下的 `proposal.md`、`design.md`、`tasks.md`、`specs/*.md`、README 中遗漏或过时的内容(禁止同步到 `openspec/specs/`)
|
- Design 回写:同步 `design.md` 中遗漏或过时的 requirements、guardrails、affected areas、decisions、execution plan、verification plan、risks 或 open questions
|
||||||
- Spec 补充:将 apply 阶段新增功能触及的现有 spec 复制到本次 change 的 `specs/` 目录并更新,同步更新 `proposal.md` 的 `Modified Capabilities`
|
|
||||||
- 任务状态修正:修正已完成/未完成状态,补充 apply 后新增但已完成的修补任务或验证任务
|
- 任务状态修正:修正已完成/未完成状态,补充 apply 后新增但已完成的修补任务或验证任务
|
||||||
- 代码质量优化:在不改变目标行为的前提下优化结构、复用、命名或可维护性
|
- 文档/沟通同步:同步行为、流程、接口、内容、数据、配置、责任边界或用户可见结果变化
|
||||||
|
- 质量优化:在不改变目标结果的前提下优化结构、表达、一致性、可维护性或移交质量
|
||||||
|
- Schema 兼容处理:若实际 schema 不是 `fast-drive`,按实际存在的 artifacts 说明额外文档同步项
|
||||||
|
|
||||||
对每个拟修改的文件说明:
|
对每个拟修改的文件说明:
|
||||||
|
|
||||||
@@ -115,36 +142,38 @@ f) 收集当前会话中与本次变更相关的实现说明、apply 过程中
|
|||||||
|
|
||||||
## 4. 执行
|
## 4. 执行
|
||||||
|
|
||||||
逐项执行已确认的代码、测试和文档修复。
|
逐项执行已确认的实际产物、验证和文档修复。
|
||||||
|
|
||||||
若涉及删除或重写:
|
若涉及删除或重写:
|
||||||
|
|
||||||
- 先创建备份文件 `{file}.bak.{timestamp}`
|
- 先创建备份文件 `{file}.bak.{timestamp}`
|
||||||
- 再执行修改
|
- 再执行修改
|
||||||
|
|
||||||
若修改了代码或测试:
|
若修改了实际产物或验证材料:
|
||||||
|
|
||||||
- 同步更新相关变更文档;若影响模块结构、API、实体或用户可见行为,再同步 README
|
- 同步更新相关变更文档;若影响行为、流程、接口、内容、数据、配置、责任边界或用户可见结果,再同步必要的文档/沟通材料
|
||||||
- 运行相关测试;若修补影响范围较大,再补充执行受影响的回归测试
|
- 运行或执行相关验证;若修补影响范围较大,再补充执行受影响的回归验证
|
||||||
|
|
||||||
若修改了文档:
|
若修改了文档:
|
||||||
|
|
||||||
- 确认本次 change 目录下的变更文档之间保持一致;重点检查 `proposal.md`、`design.md`、`tasks.md`、`specs/*.md`
|
- 在 `fast-drive` workflow 下,确认 `design.md` 仍是 source of truth,`tasks.md` 仍从 `design.md` 派生
|
||||||
- 若 apply 后新增修补改变了能力边界或行为约束,同步更新本次 change 的 `Capabilities` / `Modified Capabilities`
|
- 确认 `design.md` 的 requirements、guardrails、affected areas、decisions、execution plan、verification plan、risks 和 open questions 与实际变更一致
|
||||||
- 若"Spec 补充清单"中有需新增的 spec:先从 `openspec/specs/` 复制对应的原 spec 到本次 change 的 `specs/` 目录,再基于实际改动更新其内容;禁止修改 `openspec/specs/` 下的原文件
|
- 确认 `tasks.md` 每个完成任务都有对应实际产物和必要验证,新增修补已补充任务或记录在合适任务中
|
||||||
- 禁止将本次 change 的 specs 同步到 `openspec/specs/`,该操作属于 archive 阶段
|
- 禁止将本次 change 内容同步到 `openspec/specs/`,该操作属于 archive 阶段
|
||||||
|
- 在 `fast-drive` workflow 下不创建 `proposal.md` 或 `specs/*.md`;若实际 schema 不是 `fast-drive`,则按实际 schema 的 required artifacts 创建或更新本次 change 目录下的 artifacts
|
||||||
|
|
||||||
执行后重新读取所有被修改的代码、测试和文档,并复核:
|
执行后重新读取所有被修改的实际产物、验证材料和文档,并复核:
|
||||||
|
|
||||||
- "未覆盖清单" 是否已清空或已标注保留原因
|
- “Design 偏离清单” 是否已清空或已标注保留原因
|
||||||
- "需回写文档清单" 是否已清空
|
- “需回写文档清单” 是否已清空
|
||||||
- "Spec 补充清单" 是否已清空或已标注保留原因
|
- “方向待确认清单” 是否已清空或已记录用户决策
|
||||||
- "方向待确认清单" 是否已清空或已记录用户决策
|
- “任务状态问题清单” 和 “验证问题清单” 是否已清空或已标注残留原因
|
||||||
- "任务状态问题清单" 和 "测试问题清单" 是否已清空或已标注残留原因
|
- “质量/优化清单” 中哪些已处理,哪些有意延期
|
||||||
- "代码质量/优化清单" 中哪些已处理,哪些有意延期
|
- 必要的文档/沟通材料是否已按影响范围同步
|
||||||
|
- 所有模板注释、空表格行和占位文本是否已清空或替换为有效内容
|
||||||
|
|
||||||
## 5. 收尾
|
## 5. 收尾
|
||||||
|
|
||||||
列出所有修改的文件、备份文件、测试命令与结果、文档同步摘要和剩余风险。
|
列出所有修改的文件、备份文件、验证命令或检查结果、文档同步摘要和剩余风险。
|
||||||
|
|
||||||
若本次因缺少测试结果、修补记录或上下文而降级执行,或有问题因信息不足暂未处理,单独说明。
|
若本次因缺少验证结果、修补记录或上下文而降级执行,或有问题因信息不足暂未处理,单独说明。
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
审查本次 OpenSpec 变更文档是否与前序讨论、当前代码现状和 OpenSpec 文档规范一致,识别遗漏、冲突和不合理假设,并给出可执行的补充建议,按以下流程执行。
|
审查本次 OpenSpec 变更文档是否与前序讨论、当前实际状态和实际 OpenSpec workflow 一致,重点检查 `fast-drive` workflow 下的 `design.md` 是否足以在上下文压缩或新会话中指导后续 `apply`,并识别遗漏、冲突和不合理假设,给出可执行的补充建议,按以下流程执行。
|
||||||
|
|
||||||
## 约束
|
## 约束
|
||||||
|
|
||||||
- 仅修改本次变更文档,不修改源码
|
- 仅修改本次变更文档,不修改实际产物
|
||||||
- 默认按 `spec-driven` workflow 审查;识别 change 后先确认 `schemaName`;若实际 schema 不同,说明差异,仅对实际存在的 artifacts 做审查
|
- 默认按 `fast-drive` workflow 审查;识别 change 后先确认 `schemaName`;若实际 schema 不同,说明差异,仅对实际存在的 artifacts 做审查
|
||||||
- 优先使用当前会话中的讨论和已生成的变更文档;仅在无法明确 change、`schemaName` 或文档范围时,再用提问工具或 OpenSpec 命令补充定位
|
- 在 `fast-drive` workflow 下,核心 artifacts 是 `design.md` 和 `tasks.md`;不要要求存在 `proposal.md` 或 `specs/*.md`
|
||||||
|
- 在 `fast-drive` workflow 下,`design.md` 是 scope、requirements、decisions、guardrails、execution direction 和 verification expectations 的 source of truth,`tasks.md` 必须从 `design.md` 派生
|
||||||
|
- 优先使用当前会话中的讨论、explore/propose 阶段结论和已生成的变更文档;仅在无法明确 change、`schemaName` 或文档范围时,再用提问工具或 OpenSpec 命令补充定位
|
||||||
- 每批文档修改建议执行前用提问工具获得用户确认
|
- 每批文档修改建议执行前用提问工具获得用户确认
|
||||||
- 删除/重写前用提问工具获得用户确认,并先备份原文件为 `{file}.bak.{timestamp}`
|
- 删除/重写前用提问工具获得用户确认,并先备份原文件为 `{file}.bak.{timestamp}`
|
||||||
|
|
||||||
@@ -19,12 +21,28 @@
|
|||||||
|
|
||||||
a) 先并行读取核心入口和配置,确定范围:
|
a) 先并行读取核心入口和配置,确定范围:
|
||||||
|
|
||||||
- 本次 change 的 `proposal.md`
|
- 本次 change 的 `design.md`
|
||||||
- `openspec/config.yaml`
|
- 本次 change 的 `tasks.md`
|
||||||
|
- workflow context/configuration,例如存在时读取 `openspec/config.yaml`
|
||||||
|
- 若可定位到 schema,读取对应 schema;`fast-drive` 下优先读取 `openspec/schemas/fast-drive/schema.yaml`
|
||||||
|
|
||||||
b) 从 `proposal.md` 提取 `Capabilities` / `Modified Capabilities`,确定需要读取的 spec 列表;相关性来源还包括:讨论中提到的受影响能力、`design.md` Impact 中提到的模块、相关代码对应的现有能力
|
b) 从 `design.md` 提取审查基准:
|
||||||
|
|
||||||
c) 并行读取已确定的 spec 和其余 artifacts(`design.md`、`tasks.md`、相关源码、测试、README、架构文档)
|
- `Context`
|
||||||
|
- `Discussion Notes`
|
||||||
|
- `Requirements`
|
||||||
|
- `Goals / Non-Goals`
|
||||||
|
- `Execution Guardrails`
|
||||||
|
- `Affected Areas`
|
||||||
|
- `Decisions`
|
||||||
|
- `Execution Plan`
|
||||||
|
- `Verification Plan`
|
||||||
|
- `Risks / Trade-offs`
|
||||||
|
- `Open Questions`
|
||||||
|
|
||||||
|
c) 基于 `Affected Areas`、`Execution Plan`、`Verification Plan`、讨论中提到的受影响范围,并行读取相关实际产物、参考材料、验证材料、流程说明、配置、文档或沟通材料,确认文档是否贴合当前实际状态。
|
||||||
|
|
||||||
|
d) 若实际 schema 不是 `fast-drive`,只读取实际存在的 artifacts;若存在 `proposal.md`、`specs/*.md`,再按该 schema 的要求补充读取和审查。
|
||||||
|
|
||||||
若当前上下文无法明确 change 或文档路径:
|
若当前上下文无法明确 change 或文档路径:
|
||||||
|
|
||||||
@@ -33,47 +51,55 @@ c) 并行读取已确定的 spec 和其余 artifacts(`design.md`、`tasks.md`
|
|||||||
|
|
||||||
若已明确 change,但尚未确认 `schemaName`,先读取 change 元数据或执行 `openspec status --change "{name}" --json` 确认。
|
若已明确 change,但尚未确认 `schemaName`,先读取 change 元数据或执行 `openspec status --change "{name}" --json` 确认。
|
||||||
|
|
||||||
若缺少讨论记录,明确说明本次降级为"文档 + 代码现状审查",不做讨论一致性结论。
|
若缺少讨论记录,明确说明本次降级为“文档 + 当前实际状态审查”,不做讨论一致性结论。
|
||||||
|
|
||||||
## 2. 分析
|
## 2. 分析
|
||||||
|
|
||||||
按以下优先级检查:
|
按以下优先级检查:
|
||||||
|
|
||||||
| 优先级 | 维度 | 检查点 |
|
| 优先级 | 维度 | 检查点 |
|
||||||
| ------ | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| ------ | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| P0 | 讨论一致性 | 仅在存在讨论记录时检查:文档是否完整覆盖已确认的目标、范围、非目标、约束、边界条件、风险、决策点、待办事项;若无讨论记录,标记为"跳过" |
|
| P0 | 讨论承接性 | 仅在存在讨论记录时检查:`design.md` 是否完整记录已确认的目标、非目标、用户偏好、约束、边界条件、风险、关键决策、被否决方案和待澄清事项;若无讨论记录,标记为“跳过” |
|
||||||
| P1 | 代码现实性 | 文档对当前模块、接口、数据结构、命名、依赖、目录结构、复用路径的描述是否准确;是否把"计划变更"误写成"当前现状";是否遗漏真实受影响的现有能力 |
|
| P1 | `design.md` 自包含性 | `design.md` 是否足以让看不到前序对话的执行者继续工作;是否包含完整 required sections;`Open Questions` 是否明确区分 blocking / non-blocking 或写出 `None`;是否存在依赖未记录聊天上下文的隐含要求 |
|
||||||
| P2 | 文档内部一致性 | 对实际存在的 artifacts 检查是否互相支撑;在 `spec-driven` 下重点检查 `proposal.md`、`design.md`、`tasks.md`、`specs/*.md`;`Capabilities` / `Modified Capabilities` 是否完整;每个 capability 是否有对应 spec;`tasks.md` 是否覆盖 `design.md` 和 `specs/*.md` |
|
| P2 | 当前状态真实性 | `design.md` 对当前实际产物、流程、接口、内容、数据、配置、依赖、责任边界、参考材料和验证入口的描述是否准确;是否把“计划变更”误写成“当前现状”;`Affected Areas` 是否遗漏真实受影响区域 |
|
||||||
| P3 | OpenSpec 合规性 | 对实际存在的 artifacts 检查是否遵循 OpenSpec 格式和术语;`specs/*.md` 是否只描述行为与约束、不混入实现细节;`tasks.md` 是否一行一个任务;是否混入 git 操作任务 |
|
| P3 | `tasks.md` 派生性 | `tasks.md` 是否从 `design.md` 派生;是否覆盖 requirements、guardrails、decisions、execution plan 和 verification plan;是否依赖 `proposal.md` 或 `specs/*.md` 中未写入 `design.md` 的内容 |
|
||||||
|
| P4 | OpenSpec 合规性 | 对实际存在的 artifacts 检查是否遵循对应 schema 和 OpenSpec 术语;`tasks.md` 是否一行一个 `- [ ]` checkbox 任务、按 `##` numbered headings 分组、无无关的仓库/版本控制/发布操作任务;`design.md` 是否避免 task checkbox;最终 artifacts 是否仍保留模板注释、空表格行或占位任务文本 |
|
||||||
|
|
||||||
分析时区分两类情况:
|
分析时区分两类情况:
|
||||||
|
|
||||||
- 文档对当前代码现状的描述错误
|
- 文档对当前实际状态的描述错误
|
||||||
- 文档描述的是预期变更,本来就应当与当前代码不同
|
- 文档描述的是预期变更,本来就应当与当前状态不同
|
||||||
|
|
||||||
重点识别:
|
重点识别:
|
||||||
|
|
||||||
- 讨论中已确定但文档未记录的内容
|
- 讨论中已确定但 `design.md` 未记录的内容
|
||||||
- 文档基于错误现状做出的设计或任务拆分
|
- `design.md` 中缺失或含糊的 requirements、acceptance criteria、guardrails、decisions、verification expectations
|
||||||
- 文档之间相互冲突的目标、方案、约束、任务
|
- `Open Questions` 未明确区分 blocking / non-blocking、与 `tasks.md` 冲突,或包含 apply 前必须解决的 blocker
|
||||||
- `Modified Capabilities` 在本次 change 的 specs 中是否已更新(注意:仅更新 change 目录下的 specs,不动 `openspec/specs/`)
|
- `tasks.md` 未覆盖 `design.md` 的要求、约束、执行计划、验证计划或文档/沟通更新要求
|
||||||
|
- `tasks.md` 标记了无法验证、跨行、过大、顺序错误或包含无关仓库/版本控制/发布操作的任务
|
||||||
|
- 文档仍保留 `<!-- ... -->` 模板注释、空表格行、`Replace with...`、`TBD`、`TODO` 等未解决占位内容
|
||||||
|
- 文档基于错误当前状态做出的设计或任务拆分
|
||||||
|
- 文档之间相互冲突的目标、方案、约束、任务和验证要求
|
||||||
|
- `fast-drive` change 中仍错误依赖 `proposal.md`、`specs/*.md`、`Capabilities` 或 `Modified Capabilities` 的内容
|
||||||
|
|
||||||
输出审查结果:
|
输出审查结果:
|
||||||
|
|
||||||
1. **问题总览表**:问题类型 × 涉及文档数
|
1. **问题总览表**:问题类型 × 涉及文档数
|
||||||
2. **讨论遗漏清单**:讨论已确定但文档未体现的内容;若缺少讨论记录,标记为"未审查"
|
2. **讨论遗漏清单**:讨论已确定但 `design.md` 未体现的内容;若缺少讨论记录,标记为“未审查”
|
||||||
3. **现实性问题清单**:与当前代码现状不符的描述、假设或影响分析
|
3. **Design 自包含性问题清单**:缺失、含糊或无法指导新会话 apply 的内容
|
||||||
4. **文档冲突清单**:proposal、design、tasks、specs 之间的不一致
|
4. **当前状态问题清单**:与当前实际状态不符的描述、假设或影响分析
|
||||||
5. **OpenSpec 规范问题清单**:格式、术语、结构问题
|
5. **Tasks 派生与覆盖问题清单**:`tasks.md` 未从 `design.md` 正确派生或覆盖不足的内容
|
||||||
6. **待澄清清单**:仅靠讨论和代码仍无法判断的事项
|
6. **文档冲突清单**:`design.md`、`tasks.md` 和实际存在的其他 artifacts 之间的不一致
|
||||||
7. **逐项分析**:每个问题说明位置、问题、影响、建议
|
7. **OpenSpec 规范问题清单**:格式、术语、结构问题
|
||||||
8. **补充建议方案**:按文件列出建议补充/修正的内容、原因和可选方案
|
8. **待澄清清单**:仅靠讨论和当前状态仍无法判断的事项
|
||||||
|
9. **逐项分析**:每个问题说明位置、问题、影响、建议
|
||||||
|
10. **补充建议方案**:按文件列出建议补充/修正的内容、原因和可选方案
|
||||||
|
|
||||||
若所有清单均为空,输出"审查通过,未发现问题",跳至步骤 5。
|
若所有清单均为空,输出“审查通过,未发现问题”,跳至步骤 5。
|
||||||
|
|
||||||
## 3. 计划(用户确认)
|
## 3. 计划(用户确认)
|
||||||
|
|
||||||
先针对"待澄清清单"用提问工具逐项向用户确认。
|
先针对“待澄清清单”用提问工具逐项向用户确认。
|
||||||
|
|
||||||
再整理完整修复方案,按文件列出:
|
再整理完整修复方案,按文件列出:
|
||||||
|
|
||||||
@@ -86,7 +112,9 @@ c) 并行读取已确定的 spec 和其余 artifacts(`design.md`、`tasks.md`
|
|||||||
|
|
||||||
## 4. 执行
|
## 4. 执行
|
||||||
|
|
||||||
逐项修改已确认的变更文档,不修改源码。
|
逐项修改已确认的变更文档,不修改实际产物。
|
||||||
|
|
||||||
|
在 `fast-drive` workflow 下,通常只修改本次 change 的 `design.md` 和 `tasks.md`;若实际 schema 存在其他 artifacts,仅在确有必要且用户确认后修改实际存在的 artifacts。
|
||||||
|
|
||||||
若涉及删除或重写:
|
若涉及删除或重写:
|
||||||
|
|
||||||
@@ -95,11 +123,14 @@ c) 并行读取已确定的 spec 和其余 artifacts(`design.md`、`tasks.md`
|
|||||||
|
|
||||||
执行后重新读取所有被修改的文档,并复核:
|
执行后重新读取所有被修改的文档,并复核:
|
||||||
|
|
||||||
- "讨论遗漏清单" 是否已清空或已标注保留原因
|
- “讨论遗漏清单” 是否已清空或已标注保留原因
|
||||||
- "现实性问题清单" 是否已清空或已标注为预期变更
|
- “Design 自包含性问题清单” 是否已清空
|
||||||
- "文档冲突清单" 是否已清空
|
- “当前状态问题清单” 是否已清空或已标注为预期变更
|
||||||
- "OpenSpec 规范问题清单" 是否已清空
|
- “Tasks 派生与覆盖问题清单” 是否已清空
|
||||||
- "待澄清清单" 是否已清空或已记录用户决策
|
- “文档冲突清单” 是否已清空
|
||||||
|
- “OpenSpec 规范问题清单” 是否已清空
|
||||||
|
- “待澄清清单” 是否已清空或已记录用户决策
|
||||||
|
- 所有模板注释、空表格行和占位文本是否已清空或替换为有效内容
|
||||||
|
|
||||||
## 5. 收尾
|
## 5. 收尾
|
||||||
|
|
||||||
|
|||||||
@@ -1,142 +0,0 @@
|
|||||||
请审查并整理 `openspec/specs/` 下的稳定规范,使其成为可搜索、边界清晰、无冗余、与当前业务一致的能力索引,按以下流程执行。
|
|
||||||
|
|
||||||
## 约束
|
|
||||||
|
|
||||||
- `openspec/specs/` 描述长期稳定的业务能力、规则和外部行为,不记录变更过程、迁移说明、实现路径、内部类型名、组件 props、样式数值、层级分层等实现细节
|
|
||||||
- 用户可感知或对外暴露的契约可以保留:公开 API 路径、请求/响应字段、协议名、错误码、数据约束、交互结果
|
|
||||||
- `Requirement` 和 `Scenario` 应描述业务能力、外部行为或稳定约束,不以“使用某层/某组件/某库实现”作为标题或核心表述
|
|
||||||
- 不把当前代码自动视为唯一真相;若代码、README、现有 spec 冲突且无法判断应以哪边为准,列入待确认清单,不直接改写规范
|
|
||||||
- 仅删除内容已被其他规范完整覆盖且无独立检索价值的规范;非冗余内容仅迁移、合并、拆分或重命名
|
|
||||||
- 每批重构执行前用提问工具获得用户确认;删除或重写前先备份原文件为 `{file}.bak.{timestamp}`
|
|
||||||
- 命名、Purpose、Requirement 标题都必须保留用户下一次最可能搜索的业务关键词
|
|
||||||
|
|
||||||
## 1. 收集
|
|
||||||
|
|
||||||
并行读取:
|
|
||||||
|
|
||||||
- `openspec/config.yaml`
|
|
||||||
- `README.md`,以及与模块结构、API、架构相关的 README 或文档
|
|
||||||
- `openspec/specs/*/spec.md`
|
|
||||||
|
|
||||||
默认不读取 `openspec/changes/**`、历史 proposal/design/tasks 作为稳定规范整理依据;仅在用户明确要求“连同历史变更一起校对”时再纳入。
|
|
||||||
|
|
||||||
先建立索引,不直接开始改写:
|
|
||||||
|
|
||||||
| 索引 | 内容 |
|
|
||||||
| -------------- | ----------------------------------------------------------------------------- |
|
|
||||||
| `spec_index[]` | 每个 spec 的目录名、Purpose、Requirement 摘要、关键词、外部契约、疑似重叠对象 |
|
|
||||||
| `domain_map[]` | 从 README、API、模块文档中提炼的核心业务域、横切能力和术语 |
|
|
||||||
| `term_map[]` | 同义词、旧名、缩写和推荐标准术语 |
|
|
||||||
| `suspects[]` | 需要进一步对照代码或测试确认的 spec |
|
|
||||||
|
|
||||||
仅对 `suspects[]` 做定向读取:
|
|
||||||
|
|
||||||
- 读取与该 spec 对应的源码、测试、README 或架构文档
|
|
||||||
- 不对 `backend/`、`frontend/` 做无差别逐文件扫描
|
|
||||||
|
|
||||||
判定依据优先级:
|
|
||||||
|
|
||||||
- 当前稳定 spec 与 README 共同支持的事实,可直接视为高置信度
|
|
||||||
- 仅代码可见但 README 和 spec 未体现的内容,先判断它是稳定外部行为还是临时实现细节
|
|
||||||
- 代码、README、现有 spec 互相冲突且无法自动定夺时,进入 `待确认清单`
|
|
||||||
|
|
||||||
## 2. 审查
|
|
||||||
|
|
||||||
按 spec、Requirement、Scenario 三层检查:
|
|
||||||
|
|
||||||
| 维度 | 检查点 |
|
|
||||||
| --------- | --------------------------------------------------------------------------------- |
|
|
||||||
| 过时 | 描述的能力、术语、外部契约是否仍成立;是否存在 `TBD`、`TODO`、占位说明 |
|
|
||||||
| 冲突 | 不同规范是否对同一行为给出不同约束、命名或边界 |
|
|
||||||
| 重复/重叠 | 是否在文件级、Requirement 级、Scenario 级重复描述同一能力 |
|
|
||||||
| 错位 | 内容是否放错能力域;横切规则是否混入实体规范;平台实现是否混入通用能力规范 |
|
|
||||||
| 粒度 | 是否过大导致难检索,或过碎导致回答一个问题必须同时打开多个 spec |
|
|
||||||
| 术语 | 同一概念是否混用多个名字;旧名、别名、缩写是否需要归一并保留检索入口 |
|
|
||||||
| 命名/检索 | 目录名、Purpose、Requirement 标题是否准确;是否能被 README、API、业务术语直接命中 |
|
|
||||||
| 规范性 | 是否使用 SHALL/WHEN/THEN;是否混入变更记录、迁移说明、内部实现或 UI/代码细节 |
|
|
||||||
| 完整性 | Purpose 是否明确;是否存在空目录、非 spec 噪音文件、无清晰归属的孤立规范 |
|
|
||||||
|
|
||||||
重构判定规则:
|
|
||||||
|
|
||||||
- 若两个 spec 回答的是同一个核心问题,或其中一个只是另一个的子集,优先合并
|
|
||||||
- 若一个 spec 混合多个独立检索意图,或同时包含横切规则与业务流程,优先拆分
|
|
||||||
- 若内容正确但目录名、Purpose 或 Requirement 标题不利于检索,优先重命名或改写标题
|
|
||||||
- 若多个术语指向同一概念,统一到一个标准术语,并在 Purpose 或 Requirement 中保留必要别名以支持搜索
|
|
||||||
- 若某段内容只是内部实现细节,且不影响外部行为理解,删除该段而不是为其单独保留 spec
|
|
||||||
- 若某个具体值同时属于外部契约与内部实现,按“是否对调用方可见、是否影响兼容性”判断是否保留
|
|
||||||
|
|
||||||
### 命名约定
|
|
||||||
|
|
||||||
命名优先复用仓库已存在的稳定术语,如 `provider`、`model`、`stats`、`protocol`、`proxy`、`logging`、`validation`、`migration`、`frontend`、`desktop`、`mysql`。
|
|
||||||
|
|
||||||
| 类型 | 模式 | 示例 |
|
|
||||||
| ------------ | ---------------------------------------------------------- | -------------------------------------------------- |
|
|
||||||
| 实体生命周期 | `{entity}-management` | `provider-management`、`model-management` |
|
|
||||||
| 横切能力 | `{concern}` 或 `{concern}-{qualifier}` | `error-handling`、`structured-logging` |
|
|
||||||
| 协议/适配 | `{protocol}-{capability}` 或 `protocol-adapter-{protocol}` | `openai-protocol-proxy`、`protocol-adapter-openai` |
|
|
||||||
| 运行面/入口 | `{surface}` 或 `{surface}-{capability}` | `frontend`、`desktop-app` |
|
|
||||||
| 基础设施 | `{resource}-{operation}` | `database-migration`、`mysql-driver` |
|
|
||||||
|
|
||||||
命名原则:
|
|
||||||
|
|
||||||
- 1-4 个词,保持单一主题
|
|
||||||
- 优先使用业务名词,不使用 `basic`、`general`、`misc`、`info`、`data` 等泛化词
|
|
||||||
- 不使用 `crud`、`list`、`table`、`display` 等实现模式词,除非它本身就是外部契约的一部分
|
|
||||||
- 同一主题的命名模式保持一致,不同时混用多套前后缀
|
|
||||||
|
|
||||||
## 3. 报告
|
|
||||||
|
|
||||||
输出分析结果:
|
|
||||||
|
|
||||||
1. **问题总览表**:问题类型 × 涉及规范数
|
|
||||||
2. **规范关系表**:每个 spec 的主主题、重叠对象、冲突对象、建议动作
|
|
||||||
3. **术语归一表**:旧术语 / 别名 / 缩写 → 推荐标准术语
|
|
||||||
4. **逐项分析**:每个有问题的规范说明位置、问题、影响、建议和目标规范
|
|
||||||
5. **待确认清单**:代码、README、现有 spec 冲突且无法自动定夺的事项
|
|
||||||
6. **重构方案**:按优先级分批
|
|
||||||
7. **重构后目录结构**:预期的新 `openspec/specs/` 目录树
|
|
||||||
|
|
||||||
优先级建议:
|
|
||||||
|
|
||||||
- P0:删除空目录、非 spec 噪音文件、占位内容
|
|
||||||
- P1:删除完全冗余规范;将其内容映射到主规范
|
|
||||||
- P2:合并重复/子集规范;拆分错位或过大规范
|
|
||||||
- P3:重命名目录、改写 Purpose 和 Requirement 标题以提升检索性
|
|
||||||
- P4:修正过时描述,清理实现细节、迁移说明和变更记录
|
|
||||||
|
|
||||||
若所有问题清单为空,输出“审查通过,未发现问题”,跳至步骤 5。
|
|
||||||
|
|
||||||
## 4. 计划(用户确认)
|
|
||||||
|
|
||||||
先针对 `待确认清单` 用提问工具逐项向用户确认。
|
|
||||||
|
|
||||||
再按批次展示完整重构计划,每批必须包含:
|
|
||||||
|
|
||||||
- 操作类型:删除、重命名、迁移、合并、拆分、改写
|
|
||||||
- 路径变化:源路径 → 目标路径
|
|
||||||
- 内容映射:源 spec 的 Requirement / Scenario 将迁移到哪里
|
|
||||||
- 术语处理:哪些旧词保留为检索入口,哪些词统一替换
|
|
||||||
- 执行原因:为什么这样做更利于检索、去重和边界清晰
|
|
||||||
- 验证方式:如何确认没有丢失约束或引入新的冲突
|
|
||||||
|
|
||||||
用提问工具获得当前批次确认后再执行。
|
|
||||||
|
|
||||||
## 5. 执行
|
|
||||||
|
|
||||||
按 P0 → P4 逐批执行已确认的重构。
|
|
||||||
|
|
||||||
执行要求:
|
|
||||||
|
|
||||||
- 合并或拆分时先写目标 spec,再删除或重命名源 spec
|
|
||||||
- 删除前确认其 Requirement 和 Scenario 已被完整保留、迁移或判定为纯冗余
|
|
||||||
- 每批执行后重新读取受影响的 spec,并复核结构和内容
|
|
||||||
|
|
||||||
每批执行后至少验证:
|
|
||||||
|
|
||||||
- 目录结构完整,`openspec/specs/*/spec.md` 可正常读取
|
|
||||||
- 不存在未承接的 Requirement 或 Scenario
|
|
||||||
- Purpose、Requirement 标题、目录名可以直接表达主能力
|
|
||||||
- 不再包含 `TBD`、变更记录、迁移说明、内部实现细节或噪音文件
|
|
||||||
- 若本批涉及代码对照项,相关外部契约描述与当前仓库现状一致,或已列入残留待确认
|
|
||||||
|
|
||||||
收尾时输出:修改文件清单、备份文件清单、最终目录树、残留待确认事项和整理摘要。
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
schema: spec-driven
|
|
||||||
created: 2026-05-20
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
## Context
|
|
||||||
|
|
||||||
当前前端为单页面应用,`app.tsx` 直接渲染 Header + Content,无路由、无侧边栏。技术栈为 React 19 + TDesign React + TanStack Query + Vite。后端使用 Bun.serve,已支持 SPA fallback(无扩展名路径返回 index.html)。
|
|
||||||
|
|
||||||
目标:重构为经典企业 Admin 后台布局,引入路由,支持多页面导航。
|
|
||||||
|
|
||||||
约束:
|
|
||||||
- 前端样式开发优先使用 TDesign 组件和 CSS tokens,禁止内联 style、硬编码色值
|
|
||||||
- 新增代码需编写完善测试
|
|
||||||
- 后端无需改动
|
|
||||||
|
|
||||||
## Goals / Non-Goals
|
|
||||||
|
|
||||||
**Goals:**
|
|
||||||
- 引入 React Router v7 (Declarative mode) 实现 SPA 路由
|
|
||||||
- 实现企业 Admin 后台布局:Header + 侧边栏 + 内容区
|
|
||||||
- 侧边栏支持折叠/展开,状态持久化到 localStorage
|
|
||||||
- 路由定义与菜单配置统一为单一数据源
|
|
||||||
- 提供示例页面(仪表盘、用户管理、系统设置、404)
|
|
||||||
|
|
||||||
**Non-Goals:**
|
|
||||||
- 不实现路由守卫/权限控制(预留接口但不启用)
|
|
||||||
- 不实现懒加载(页面少,暂不需要)
|
|
||||||
- 不实现面包屑导航
|
|
||||||
- 不实现用户认证
|
|
||||||
|
|
||||||
## Decisions
|
|
||||||
|
|
||||||
### 1. 路由方案:React Router v7 Declarative mode
|
|
||||||
|
|
||||||
**选择**:`react-router` v7,使用 Declarative mode(`BrowserRouter` + `Routes` + `Route`)
|
|
||||||
|
|
||||||
**理由**:
|
|
||||||
- v7 统一了 `react-router` 和 `react-router-dom`,单包导入
|
|
||||||
- Declarative mode 满足当前需求,无需 framework mode 的文件系统路由
|
|
||||||
- 社区成熟,文档完善
|
|
||||||
|
|
||||||
**替代方案**:
|
|
||||||
- TanStack Router:类型安全强,但较新、API 变动风险
|
|
||||||
- 自实现:零依赖但功能有限,扩展成本高
|
|
||||||
|
|
||||||
### 2. Routes 定义位置:独立文件
|
|
||||||
|
|
||||||
**选择**:抽离为 `src/web/routes.tsx`,`app.tsx` 引用
|
|
||||||
|
|
||||||
**理由**:
|
|
||||||
- 路由增删时改动范围小,不污染 app.tsx
|
|
||||||
- 便于后续扩展路由守卫、懒加载
|
|
||||||
|
|
||||||
### 3. 侧边栏折叠按钮位置:Header 左侧
|
|
||||||
|
|
||||||
**选择**:折叠按钮放在 Header 左侧,Sidebar 不使用 Menu 的 operations prop
|
|
||||||
|
|
||||||
**理由**:
|
|
||||||
- Sidebar 折叠变窄后,底部按钮变小且易被忽略
|
|
||||||
- Header 上的按钮始终可见且易触达
|
|
||||||
- 符合 Ant Design Pro 等主流企业后台习惯
|
|
||||||
|
|
||||||
### 4. 页面标题来源:复用 menu label
|
|
||||||
|
|
||||||
**选择**:Header 页面标题直接使用 `menu.tsx` 中的 `label` 字段
|
|
||||||
|
|
||||||
**理由**:
|
|
||||||
- 简单直接,侧边栏标签即页面标题
|
|
||||||
- 如需差异化,后续加 `title` 字段即可
|
|
||||||
|
|
||||||
### 5. Layout 嵌套:保留嵌套结构
|
|
||||||
|
|
||||||
**选择**:`Layout > (Header + Layout > (Aside + Content))`
|
|
||||||
|
|
||||||
**理由**:
|
|
||||||
- 为 Footer 预留位置,将来加 Footer 无需重构
|
|
||||||
- 符合 TDesign 官方组合导航布局示例
|
|
||||||
|
|
||||||
### 6. Vite code splitting:react-router 单独分组
|
|
||||||
|
|
||||||
**选择**:新增 `vendor-router` 组,包含 `react-router`
|
|
||||||
|
|
||||||
**理由**:
|
|
||||||
- 路由库独立于 React 核心,更新节奏不同
|
|
||||||
- 便于缓存隔离
|
|
||||||
|
|
||||||
### 7. 菜单与路由数据源:menu.tsx
|
|
||||||
|
|
||||||
**选择**:`src/web/menu.tsx` 定义菜单项配置,Sidebar 和 Routes 共同引用
|
|
||||||
|
|
||||||
**理由**:
|
|
||||||
- 单一数据源,保证菜单项和路由同步
|
|
||||||
- 便于类型安全(`as const`)
|
|
||||||
|
|
||||||
## Risks / Trade-offs
|
|
||||||
|
|
||||||
- **路由与菜单配置一致性**:需人工保证 `menu.tsx` 与 `routes.tsx` 中 Route 定义一致 → 测试覆盖路由跳转和菜单点击
|
|
||||||
- **CSS 类名迁移影响**:`.dashboard-*` → `.app-*` 可能影响测试选择器 → 全局搜索确认影响范围,更新测试
|
|
||||||
- **React Router v7 新版本风险**:v7 较新,可能存在未发现的 bug → 使用成熟 API(BrowserRouter/Routes/Route),避免实验性特性
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
## Why
|
|
||||||
|
|
||||||
当前前端为单页面应用,仅有 Header + Content 的简单布局,无路由、无侧边栏,页面结构单一,无法支撑企业级 Admin 后台的多页面导航需求。作为 Bun 全栈应用模板,需要提供更完善的前端布局范例,便于使用者在此基础上扩展业务页面。
|
|
||||||
|
|
||||||
## What Changes
|
|
||||||
|
|
||||||
- 引入 React Router v7 (Declarative mode) 实现前端 SPA 路由
|
|
||||||
- 重构 Layout 为经典企业 Admin 后台布局:Header + 侧边栏 + 内容区
|
|
||||||
- 新增侧边栏菜单组件,支持折叠/展开,折叠状态持久化到 localStorage
|
|
||||||
- 新增多个示例页面(仪表盘、用户管理、系统设置、404)
|
|
||||||
- 将路由定义与菜单配置统一为单一数据源,保证两者同步
|
|
||||||
- 新增 `react-router` 依赖,Vite code splitting 单独分组
|
|
||||||
|
|
||||||
**BREAKING**: 原 `app.tsx` 的 Content 内容迁移至 Dashboard 页面;CSS 类名 `.dashboard-*` 变更为 `.app-*`
|
|
||||||
|
|
||||||
## Capabilities
|
|
||||||
|
|
||||||
### New Capabilities
|
|
||||||
|
|
||||||
- `frontend-routing`: 前端 SPA 路由能力,基于 React Router v7,支持多页面导航、路由匹配、404 处理
|
|
||||||
- `admin-layout`: 企业 Admin 后台布局能力,Header + 侧边栏 + 内容区,侧边栏可折叠
|
|
||||||
|
|
||||||
### Modified Capabilities
|
|
||||||
|
|
||||||
- `app-constants`: 新增 localStorage key `"sidebar.collapsed"` 用于持久化侧边栏折叠状态
|
|
||||||
|
|
||||||
## Impact
|
|
||||||
|
|
||||||
- 新增依赖:`react-router`
|
|
||||||
- 新增目录:`src/web/pages/`、`src/web/components/Sidebar/`
|
|
||||||
- 新增文件:`src/web/routes.tsx`、`src/web/menu.tsx`、`src/web/hooks/use-sidebar-collapsed.ts`
|
|
||||||
- 修改文件:`src/web/app.tsx`(重构 Layout)、`src/web/main.tsx`(+ BrowserRouter)、`src/web/styles.css`(布局样式重写)
|
|
||||||
- 修改配置:`vite.config.ts`(code splitting 新增 vendor-router 组)
|
|
||||||
- 更新文档:`DEVELOPMENT.md`、`README.md`
|
|
||||||
- 新增测试:Sidebar 组件测试、各页面测试、test-utils 增强
|
|
||||||
- 后端无需改动(SPA fallback 已支持)
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
## ADDED Requirements
|
|
||||||
|
|
||||||
### Requirement: 企业 Admin 后台布局
|
|
||||||
|
|
||||||
系统 SHALL 实现 Header + 侧边栏 + 内容区的企业 Admin 后台布局,使用 TDesign Layout 组件。
|
|
||||||
|
|
||||||
#### Scenario: 布局结构
|
|
||||||
|
|
||||||
- **WHEN** 用户访问应用
|
|
||||||
- **THEN** 系统 SHALL 渲染包含 Header、Aside、Content 的 Layout 结构
|
|
||||||
|
|
||||||
#### Scenario: Layout 嵌套结构
|
|
||||||
|
|
||||||
- **WHEN** 布局渲染
|
|
||||||
- **THEN** 系统 SHALL 使用嵌套 Layout 结构:`Layout > (Header + Layout > (Aside + Content))`
|
|
||||||
|
|
||||||
### Requirement: Header 布局
|
|
||||||
|
|
||||||
Header SHALL 包含折叠按钮、页面标题、主题切换控件。
|
|
||||||
|
|
||||||
#### Scenario: Header 左侧折叠按钮
|
|
||||||
|
|
||||||
- **WHEN** Header 渲染
|
|
||||||
- **THEN** 系统 SHALL 在左侧显示侧边栏折叠/展开按钮
|
|
||||||
|
|
||||||
#### Scenario: Header 页面标题
|
|
||||||
|
|
||||||
- **WHEN** Header 渲染
|
|
||||||
- **THEN** 系统 SHALL 显示当前路由对应的页面标题(从 menu 获取)
|
|
||||||
|
|
||||||
#### Scenario: Header 右侧主题切换
|
|
||||||
|
|
||||||
- **WHEN** Header 渲染
|
|
||||||
- **THEN** 系统 SHALL 在右侧显示主题切换 RadioGroup(系统/明亮/黑暗)
|
|
||||||
|
|
||||||
### Requirement: 侧边栏菜单
|
|
||||||
|
|
||||||
系统 SHALL 提供侧边栏菜单组件(Sidebar),使用 TDesign Menu 组件,支持折叠/展开。
|
|
||||||
|
|
||||||
#### Scenario: 侧边栏渲染菜单项
|
|
||||||
|
|
||||||
- **WHEN** 侧边栏渲染
|
|
||||||
- **THEN** 系统 SHALL 根据 `menu.tsx` 渲染所有菜单项
|
|
||||||
|
|
||||||
#### Scenario: 菜单项点击导航
|
|
||||||
|
|
||||||
- **WHEN** 用户点击菜单项
|
|
||||||
- **THEN** 系统 SHALL 使用 React Router 的 `navigate` 跳转到对应路径
|
|
||||||
|
|
||||||
#### Scenario: 菜单项激活状态
|
|
||||||
|
|
||||||
- **WHEN** 当前路由与菜单项路径匹配
|
|
||||||
- **THEN** 系统 SHALL 高亮显示该菜单项
|
|
||||||
|
|
||||||
### Requirement: 侧边栏折叠
|
|
||||||
|
|
||||||
侧边栏 SHALL 支持折叠/展开,折叠状态持久化到 localStorage。
|
|
||||||
|
|
||||||
#### Scenario: 侧边栏折叠交互
|
|
||||||
|
|
||||||
- **WHEN** 用户点击 Header 左侧的折叠按钮
|
|
||||||
- **THEN** 系统 SHALL 切换侧边栏折叠状态
|
|
||||||
|
|
||||||
#### Scenario: 侧边栏折叠宽度
|
|
||||||
|
|
||||||
- **WHEN** 侧边栏折叠
|
|
||||||
- **THEN** Aside 宽度 SHALL 变为 80px,Menu 仅显示图标
|
|
||||||
|
|
||||||
#### Scenario: 侧边栏展开宽度
|
|
||||||
|
|
||||||
- **WHEN** 侧边栏展开
|
|
||||||
- **THEN** Aside 宽度 SHALL 变为 232px,Menu 显示图标和文字
|
|
||||||
|
|
||||||
#### Scenario: 折叠状态持久化
|
|
||||||
|
|
||||||
- **WHEN** 用户切换侧边栏折叠状态
|
|
||||||
- **THEN** 系统 SHALL 将状态存储到 localStorage key `"sidebar.collapsed"`
|
|
||||||
|
|
||||||
#### Scenario: 折叠状态恢复
|
|
||||||
|
|
||||||
- **WHEN** 应用初始化
|
|
||||||
- **THEN** 系统 SHALL 从 localStorage 读取 `"sidebar.collapsed"` 并恢复折叠状态
|
|
||||||
|
|
||||||
### Requirement: 侧边栏主题跟随全局
|
|
||||||
|
|
||||||
侧边栏 Menu 的主题 SHALL 跟随全局主题设置,不单独设置 `theme` prop。
|
|
||||||
|
|
||||||
#### Scenario: 侧边栏跟随全局主题
|
|
||||||
|
|
||||||
- **WHEN** 全局主题切换为 dark
|
|
||||||
- **THEN** 侧边栏 SHALL 自动应用 dark 主题样式
|
|
||||||
|
|
||||||
### Requirement: 菜单配置单一数据源
|
|
||||||
|
|
||||||
系统 SHALL 在 `src/web/menu.tsx` 定义菜单项配置,包含 `value`、`label`、`path`、`icon` 字段。
|
|
||||||
|
|
||||||
#### Scenario: 菜单配置类型安全
|
|
||||||
|
|
||||||
- **WHEN** 定义菜单配置
|
|
||||||
- **THEN** 系统 SHALL 使用 `as const` 保证字面量类型推断
|
|
||||||
|
|
||||||
#### Scenario: Sidebar 引用菜单配置
|
|
||||||
|
|
||||||
- **WHEN** Sidebar 渲染
|
|
||||||
- **THEN** 系统 SHALL 遍历 `MENU_ITEMS` 渲染 MenuItem
|
|
||||||
|
|
||||||
### Requirement: 示例页面
|
|
||||||
|
|
||||||
系统 SHALL 提供示例页面组件:Dashboard、Users、Settings、NotFound。
|
|
||||||
|
|
||||||
#### Scenario: Dashboard 页面
|
|
||||||
|
|
||||||
- **WHEN** 用户访问 `/`
|
|
||||||
- **THEN** 系统 SHALL 渲染 Dashboard 页面(包含原 app.tsx 的欢迎语和 /health 展示)
|
|
||||||
|
|
||||||
#### Scenario: Users 页面占位
|
|
||||||
|
|
||||||
- **WHEN** 用户访问 `/users`
|
|
||||||
- **THEN** 系统 SHALL 渲染用户管理占位页面
|
|
||||||
|
|
||||||
#### Scenario: Settings 页面占位
|
|
||||||
|
|
||||||
- **WHEN** 用户访问 `/settings`
|
|
||||||
- **THEN** 系统 SHALL 渲染系统设置占位页面
|
|
||||||
|
|
||||||
#### Scenario: NotFound 页面
|
|
||||||
|
|
||||||
- **WHEN** 用户访问未定义路径
|
|
||||||
- **THEN** 系统 SHALL 渲染 404 页面,提供返回首页按钮
|
|
||||||
|
|
||||||
### Requirement: CSS 类名迁移
|
|
||||||
|
|
||||||
原 `.dashboard-*` CSS 类名 SHALL 变更为 `.app-*`。
|
|
||||||
|
|
||||||
#### Scenario: 布局类名
|
|
||||||
|
|
||||||
- **WHEN** 样式应用
|
|
||||||
- **THEN** 系统 SHALL 使用 `.app-layout`、`.app-header`、`.app-content`、`.app-sidebar` 类名
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
## ADDED Requirements
|
|
||||||
|
|
||||||
### Requirement: 侧边栏折叠状态 localStorage key
|
|
||||||
|
|
||||||
侧边栏折叠状态存储 key SHALL 为 `"sidebar.collapsed"`,不包含应用名前缀。
|
|
||||||
|
|
||||||
#### Scenario: 折叠状态持久化
|
|
||||||
|
|
||||||
- **WHEN** 用户切换侧边栏折叠状态
|
|
||||||
- **THEN** 系统 SHALL 将状态存储到 localStorage key `"sidebar.collapsed"`
|
|
||||||
|
|
||||||
#### Scenario: 折叠状态读取
|
|
||||||
|
|
||||||
- **WHEN** 应用初始化时读取侧边栏折叠状态
|
|
||||||
- **THEN** 系统 SHALL 从 localStorage key `"sidebar.collapsed"` 读取
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
## ADDED Requirements
|
|
||||||
|
|
||||||
### Requirement: 前端 SPA 路由
|
|
||||||
|
|
||||||
系统 SHALL 使用 React Router v7 (Declarative mode) 实现前端 SPA 路由,支持多页面导航。
|
|
||||||
|
|
||||||
#### Scenario: 应用启动时初始化路由
|
|
||||||
|
|
||||||
- **WHEN** 应用启动
|
|
||||||
- **THEN** 系统 SHALL 在 `main.tsx` 中使用 `BrowserRouter` 包裹根组件
|
|
||||||
|
|
||||||
#### Scenario: 路由匹配渲染对应页面
|
|
||||||
|
|
||||||
- **WHEN** 用户访问路径 `/`
|
|
||||||
- **THEN** 系统 SHALL 渲染 Dashboard 页面
|
|
||||||
|
|
||||||
#### Scenario: 用户管理页面路由
|
|
||||||
|
|
||||||
- **WHEN** 用户访问路径 `/users`
|
|
||||||
- **THEN** 系统 SHALL 渲染用户管理页面
|
|
||||||
|
|
||||||
#### Scenario: 系统设置页面路由
|
|
||||||
|
|
||||||
- **WHEN** 用户访问路径 `/settings`
|
|
||||||
- **THEN** 系统 SHALL 渲染系统设置页面
|
|
||||||
|
|
||||||
#### Scenario: 未知路径返回 404 页面
|
|
||||||
|
|
||||||
- **WHEN** 用户访问未定义的路径(如 `/unknown`)
|
|
||||||
- **THEN** 系统 SHALL 渲染 NotFound 页面
|
|
||||||
|
|
||||||
### Requirement: 路由定义独立文件
|
|
||||||
|
|
||||||
路由定义 SHALL 抽离为独立文件 `src/web/routes.tsx`,`app.tsx` 引用该文件。
|
|
||||||
|
|
||||||
#### Scenario: 路由配置集中管理
|
|
||||||
|
|
||||||
- **WHEN** 开发者需要新增或修改路由
|
|
||||||
- **THEN** 系统 SHALL 在 `routes.tsx` 中统一管理所有 `<Route>` 定义
|
|
||||||
|
|
||||||
### Requirement: 路由守卫预留
|
|
||||||
|
|
||||||
系统 SHALL 预留路由守卫接口(`ProtectedRoute` 组件),但暂不实现认证逻辑。
|
|
||||||
|
|
||||||
#### Scenario: 路由守卫组件存在
|
|
||||||
|
|
||||||
- **WHEN** 需要实现认证保护
|
|
||||||
- **THEN** 系统 SHALL 提供 `ProtectedRoute` wrapper 组件供 Routes 使用
|
|
||||||
|
|
||||||
### Requirement: Vite code splitting 包含路由库
|
|
||||||
|
|
||||||
`vite.config.ts` 的 code splitting 配置 SHALL 新增 `vendor-router` 组,包含 `react-router`。
|
|
||||||
|
|
||||||
#### Scenario: 路由库独立分包
|
|
||||||
|
|
||||||
- **WHEN** 执行生产构建
|
|
||||||
- **THEN** `react-router` SHALL 被打包到独立的 `vendor-router` chunk 中
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
## 1. 依赖与配置
|
|
||||||
|
|
||||||
- [x] 1.1 安装 react-router 依赖
|
|
||||||
- [x] 1.2 更新 vite.config.ts code splitting 配置,新增 vendor-router 组
|
|
||||||
|
|
||||||
## 2. 工具与 Hook
|
|
||||||
|
|
||||||
- [x] 2.1 创建 src/web/menu.tsx,定义菜单项配置
|
|
||||||
- [x] 2.2 创建 src/web/hooks/use-sidebar-collapsed.ts,实现侧边栏折叠状态 hook
|
|
||||||
|
|
||||||
## 3. 页面组件
|
|
||||||
|
|
||||||
- [x] 3.1 创建 src/web/pages/dashboard/index.tsx,迁移原 app.tsx 内容区逻辑
|
|
||||||
- [x] 3.2 创建 src/web/pages/users/index.tsx,实现用户管理占位页面
|
|
||||||
- [x] 3.3 创建 src/web/pages/settings/index.tsx,实现系统设置占位页面
|
|
||||||
- [x] 3.4 创建 src/web/pages/404/index.tsx,实现 404 页面
|
|
||||||
|
|
||||||
## 4. 侧边栏组件
|
|
||||||
|
|
||||||
- [x] 4.1 创建 src/web/components/Sidebar/index.tsx,实现侧边栏菜单组件
|
|
||||||
|
|
||||||
## 5. 路由配置
|
|
||||||
|
|
||||||
- [x] 5.1 创建 src/web/routes.tsx,定义所有路由
|
|
||||||
|
|
||||||
## 6. 根组件重构
|
|
||||||
|
|
||||||
- [x] 6.1 重构 src/web/app.tsx,实现 Header + Aside + Content 布局
|
|
||||||
- [x] 6.2 修改 src/web/main.tsx,添加 BrowserRouter 包裹
|
|
||||||
|
|
||||||
## 7. 样式更新
|
|
||||||
|
|
||||||
- [x] 7.1 更新 src/web/styles.css,迁移 .dashboard-* 为 .app-*,新增布局样式
|
|
||||||
|
|
||||||
## 8. 测试工具增强
|
|
||||||
|
|
||||||
- [x] 8.1 增强 tests/web/test-utils.tsx,提供 renderWithProviders 函数
|
|
||||||
|
|
||||||
## 9. 组件测试
|
|
||||||
|
|
||||||
- [x] 9.1 创建 tests/web/components/Sidebar/index.test.tsx
|
|
||||||
- [x] 9.2 创建 tests/web/routes/dashboard.test.tsx
|
|
||||||
- [x] 9.3 创建 tests/web/routes/users.test.tsx
|
|
||||||
- [x] 9.4 创建 tests/web/routes/settings.test.tsx
|
|
||||||
- [x] 9.5 创建 tests/web/routes/404.test.tsx
|
|
||||||
- [x] 9.6 更新 tests/web/App.test.tsx 适配 BrowserRouter
|
|
||||||
|
|
||||||
## 10. 文档更新
|
|
||||||
|
|
||||||
- [x] 10.1 更新 DEVELOPMENT.md,更新技术栈表格、组件树、目录结构说明
|
|
||||||
- [x] 10.2 更新 README.md,更新技术栈表格、项目结构说明
|
|
||||||
|
|
||||||
## 11. 质量保障
|
|
||||||
|
|
||||||
- [x] 11.1 运行 bun run check 确保类型检查、lint、测试通过
|
|
||||||
- [x] 11.2 运行 bun run build 验证构建成功
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
schema: spec-driven
|
schema: fast-drive
|
||||||
|
|
||||||
context: |
|
context: |
|
||||||
- 使用中文(注释、文档、交流),面向中文开发者
|
- 使用中文(注释、文档、交流),面向中文开发者
|
||||||
@@ -20,8 +20,8 @@ context: |
|
|||||||
- 本项目为 Bun 全栈应用模板,README.md记录模板使用方法,DEVELOPMENT.md记录模板使用的技术细节
|
- 本项目为 Bun 全栈应用模板,README.md记录模板使用方法,DEVELOPMENT.md记录模板使用的技术细节
|
||||||
|
|
||||||
rules:
|
rules:
|
||||||
proposal:
|
explore:
|
||||||
- 仔细审查每一个过往spec判断是否存在Modified Capabilities
|
- 本项目openspec使用fast-drive自定义schema,变更文档只包含design.md和tasks.md,无proposal.md和specs
|
||||||
design:
|
design:
|
||||||
- 先前的讨论技术方案要尽可能体现在设计文档中,便于指导实现阶段不偏离已定的技术路线
|
- 先前的讨论技术方案要尽可能体现在设计文档中,便于指导实现阶段不偏离已定的技术路线
|
||||||
tasks:
|
tasks:
|
||||||
|
|||||||
181
openspec/schemas/fast-drive/schema.yaml
Normal file
181
openspec/schemas/fast-drive/schema.yaml
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
name: fast-drive
|
||||||
|
version: 1
|
||||||
|
description: Fast OpenSpec workflow - design -> tasks -> apply
|
||||||
|
artifacts:
|
||||||
|
- id: design
|
||||||
|
generates: design.md
|
||||||
|
description: Self-contained solution brief and execution plan
|
||||||
|
template: design.md
|
||||||
|
instruction: |
|
||||||
|
Create design.md as the self-contained source of truth for what will
|
||||||
|
change, why it is changing, and how the work will be executed.
|
||||||
|
|
||||||
|
This workflow does not use proposal or specs artifacts. design.md MUST
|
||||||
|
preserve the important outcomes from prior exploration and user
|
||||||
|
discussion so a later apply phase can proceed correctly even after
|
||||||
|
context compression or in a new session.
|
||||||
|
|
||||||
|
Write for someone who cannot see the earlier conversation. Keep simple
|
||||||
|
changes concise, but include enough detail to make execution
|
||||||
|
unambiguous. Add more detail when any apply:
|
||||||
|
|
||||||
|
- Cross-cutting change across multiple systems, teams, workstreams, or
|
||||||
|
artifacts
|
||||||
|
|
||||||
|
- New dependency, integration, vendor, tool, policy, or external input
|
||||||
|
|
||||||
|
- Significant information model, process model, data model, or ownership
|
||||||
|
changes
|
||||||
|
|
||||||
|
- Security, privacy, compliance, performance, operational, or migration
|
||||||
|
complexity
|
||||||
|
|
||||||
|
- Ambiguity that benefits from decisions before execution
|
||||||
|
|
||||||
|
- Prior discussion settled non-obvious requirements, constraints, or
|
||||||
|
rejected alternatives
|
||||||
|
|
||||||
|
Required sections:
|
||||||
|
|
||||||
|
- **Context**: Problem, current state, relevant references, and the user
|
||||||
|
request that triggered this change
|
||||||
|
|
||||||
|
- **Discussion Notes**: Key points from exploration or prior discussion
|
||||||
|
that must not be lost. Include agreed conclusions, user preferences,
|
||||||
|
constraints, and important rejected ideas.
|
||||||
|
|
||||||
|
- **Requirements**: Expected outcomes, behavior/process/interface/content
|
||||||
|
changes, continuity expectations, and acceptance criteria.
|
||||||
|
|
||||||
|
- **Goals / Non-Goals**: What this change will achieve and what is
|
||||||
|
explicitly out of scope.
|
||||||
|
|
||||||
|
- **Execution Guardrails**: Must-follow constraints, forbidden approaches,
|
||||||
|
preserved behavior/processes, dependency limits, and project- or
|
||||||
|
workflow-specific boundaries.
|
||||||
|
|
||||||
|
- **Affected Areas**: Concrete artifacts, references, stakeholders,
|
||||||
|
systems, workstreams, documents, configurations, assets, or handoffs that
|
||||||
|
are relevant to the change.
|
||||||
|
|
||||||
|
- **Decisions**: Key choices with rationale (why X over Y?). For each
|
||||||
|
important decision, include alternatives considered and why they were not
|
||||||
|
chosen.
|
||||||
|
|
||||||
|
- **Execution Plan**: Main workstreams or artifacts to change, integration
|
||||||
|
or handoff points, sequencing, and any rollout notes.
|
||||||
|
|
||||||
|
- **Verification Plan**: Validation checks, reviews, approvals,
|
||||||
|
acceptance checks, documentation checks, communication checks, and manual
|
||||||
|
checks needed to prove the change is complete.
|
||||||
|
|
||||||
|
- **Risks / Trade-offs**: Known limitations and things that could go
|
||||||
|
wrong.
|
||||||
|
Format: [Risk] -> Mitigation
|
||||||
|
|
||||||
|
- **Open Questions**: Outstanding decisions, assumptions, or unknowns to
|
||||||
|
resolve before execution. Separate blocking questions that must pause
|
||||||
|
apply from non-blocking follow-ups. Use "None" if there are no open
|
||||||
|
questions.
|
||||||
|
|
||||||
|
Optional sections when relevant:
|
||||||
|
|
||||||
|
- **Migration / Rollout Plan**: Rollout steps, communication, ownership,
|
||||||
|
rollback, or continuity strategy.
|
||||||
|
|
||||||
|
Focus on preserving requirements, rationale, constraints, and approach.
|
||||||
|
Avoid line-by-line or step-by-step details unless a detail is a deliberate
|
||||||
|
decision from the discussion.
|
||||||
|
|
||||||
|
Prefer durable summaries over chat transcripts. Use concrete artifact
|
||||||
|
names, data/information shapes, examples, stakeholders, ownership, and
|
||||||
|
edge cases when they affect execution.
|
||||||
|
|
||||||
|
Do not use task checkboxes in design.md; checkboxes belong only in
|
||||||
|
tasks.md.
|
||||||
|
|
||||||
|
Final design.md must not contain unresolved template comments, empty
|
||||||
|
table rows, or placeholder text.
|
||||||
|
|
||||||
|
If information is missing, state assumptions and open questions instead
|
||||||
|
of inventing hidden requirements. Do not rely on unstated chat context.
|
||||||
|
requires: []
|
||||||
|
- id: tasks
|
||||||
|
generates: tasks.md
|
||||||
|
description: Trackable execution checklist derived from design.md
|
||||||
|
template: tasks.md
|
||||||
|
instruction: |
|
||||||
|
Create tasks.md by breaking design.md into executable work.
|
||||||
|
|
||||||
|
**IMPORTANT: Follow the template below exactly.** The apply phase parses
|
||||||
|
checkbox format to track progress. Tasks not using `- [ ]` will not be
|
||||||
|
tracked.
|
||||||
|
|
||||||
|
Guidelines:
|
||||||
|
|
||||||
|
- Derive tasks from design.md. Do not depend on proposal.md or specs
|
||||||
|
artifacts; any relevant prior discussion must already be captured in
|
||||||
|
design.md.
|
||||||
|
|
||||||
|
- Group related tasks under `##` numbered headings
|
||||||
|
|
||||||
|
- Each task MUST be a single-line checkbox: `- [ ] X.Y Task description`
|
||||||
|
|
||||||
|
- Tasks should be small enough to complete in one session
|
||||||
|
|
||||||
|
- Order tasks by dependency (what must be done first?)
|
||||||
|
|
||||||
|
- Start with context review tasks when execution depends on guardrails,
|
||||||
|
affected areas, or open questions
|
||||||
|
|
||||||
|
- Include validation tasks for checks, reviews, approvals, acceptance,
|
||||||
|
documentation, communication, and manual checks when required
|
||||||
|
|
||||||
|
- Do not include repository, version-control, or release operation tasks
|
||||||
|
unless they are explicitly part of the change scope
|
||||||
|
|
||||||
|
- Final tasks.md must not contain unresolved template comments, empty
|
||||||
|
table rows, or placeholder task text
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
## 1. Context Review
|
||||||
|
|
||||||
|
- [ ] 1.1 Read design.md and identify scope, requirements, decisions, guardrails, and open questions
|
||||||
|
- [ ] 1.2 Review relevant artifacts and references listed in Affected Areas
|
||||||
|
|
||||||
|
## 2. Execution
|
||||||
|
|
||||||
|
- [ ] 2.1 Execute first concrete work item from design.md
|
||||||
|
- [ ] 2.2 Execute next concrete work item from design.md
|
||||||
|
|
||||||
|
## 3. Validation
|
||||||
|
|
||||||
|
- [ ] 3.1 Run required validation from Verification Plan
|
||||||
|
- [ ] 3.2 Perform quality checks required by the project or workflow
|
||||||
|
- [ ] 3.3 Perform required manual review or acceptance checks from Verification Plan
|
||||||
|
|
||||||
|
## 4. Documentation / Communication
|
||||||
|
|
||||||
|
- [ ] 4.1 Update relevant documentation, runbooks, communication materials, or project references if behavior, process, interface, configuration, or usage changed
|
||||||
|
```
|
||||||
|
|
||||||
|
Reference design.md for scope, requirements, decisions, execution
|
||||||
|
direction, and verification expectations.
|
||||||
|
|
||||||
|
Each task should be verifiable: it must be clear when the task is done.
|
||||||
|
requires:
|
||||||
|
- design
|
||||||
|
apply:
|
||||||
|
requires:
|
||||||
|
- design
|
||||||
|
- tasks
|
||||||
|
tracks: tasks.md
|
||||||
|
instruction: |
|
||||||
|
Read design.md first, then tasks.md.
|
||||||
|
Also follow workflow context/configuration, such as openspec/config.yaml when available, and any relevant project or workflow documentation referenced by design.md.
|
||||||
|
Treat design.md as the source of truth for scope, requirements, decisions, guardrails, execution direction, and verification expectations.
|
||||||
|
Work through pending tasks in dependency order and mark complete as you go.
|
||||||
|
Mark a task complete only after its execution and required verification are done.
|
||||||
|
Pause if tasks conflict with design.md, if design.md has blocking open questions, or if clarification is needed.
|
||||||
77
openspec/schemas/fast-drive/templates/design.md
Normal file
77
openspec/schemas/fast-drive/templates/design.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
<!-- Problem, current state, relevant references, and triggering user request -->
|
||||||
|
|
||||||
|
## Discussion Notes
|
||||||
|
|
||||||
|
<!-- Key conclusions from exploration or prior discussion that apply must preserve -->
|
||||||
|
|
||||||
|
- Agreed conclusions:
|
||||||
|
- User preferences:
|
||||||
|
- Constraints:
|
||||||
|
- Rejected ideas:
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
<!-- Expected outcomes, behavior/process/interface/content changes, continuity expectations, and acceptance criteria -->
|
||||||
|
|
||||||
|
| Requirement | Acceptance Criteria |
|
||||||
|
| ----------- | ------------------- |
|
||||||
|
| | |
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
<!-- What this design aims to achieve -->
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
<!-- What is explicitly out of scope -->
|
||||||
|
|
||||||
|
## Execution Guardrails
|
||||||
|
|
||||||
|
<!-- Must-follow constraints, forbidden approaches, preserved behavior/processes, dependency limits, and project- or workflow-specific boundaries -->
|
||||||
|
|
||||||
|
- Dependencies:
|
||||||
|
- Constraints:
|
||||||
|
- Quality Bar:
|
||||||
|
- Stakeholders:
|
||||||
|
- Documentation / Communication:
|
||||||
|
- Compatibility / Continuity:
|
||||||
|
|
||||||
|
## Affected Areas
|
||||||
|
|
||||||
|
<!-- Concrete artifacts, references, stakeholders, systems, workstreams, documents, configurations, assets, or handoffs relevant to this change -->
|
||||||
|
|
||||||
|
| Area | Artifacts / References | Expected Change | Notes |
|
||||||
|
| ---- | ---------------------- | --------------- | ----- |
|
||||||
|
| <!-- Area --> | <!-- Artifacts / References --> | <!-- Expected Change --> | <!-- Notes --> |
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
<!-- Key decisions, rationale, and alternatives considered -->
|
||||||
|
|
||||||
|
| Decision | Rationale | Alternatives Rejected |
|
||||||
|
| -------- | --------- | --------------------- |
|
||||||
|
| | | |
|
||||||
|
|
||||||
|
## Execution Plan
|
||||||
|
|
||||||
|
<!-- Main workstreams or artifacts to change, integration or handoff points, sequencing, and rollout notes -->
|
||||||
|
|
||||||
|
## Verification Plan
|
||||||
|
|
||||||
|
<!-- Validation checks, reviews, approvals, acceptance checks, documentation checks, communication checks, and manual checks needed -->
|
||||||
|
|
||||||
|
| Requirement / Risk | Verification |
|
||||||
|
| ------------------ | ------------ |
|
||||||
|
| | |
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
<!-- Format: [Risk] -> Mitigation -->
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
| Status | Question | Decision Needed |
|
||||||
|
| ------ | -------- | --------------- |
|
||||||
|
| None | No open questions. | None |
|
||||||
19
openspec/schemas/fast-drive/templates/tasks.md
Normal file
19
openspec/schemas/fast-drive/templates/tasks.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
## 1. Context Review
|
||||||
|
|
||||||
|
- [ ] 1.1 Read design.md and identify scope, requirements, decisions, guardrails, and open questions
|
||||||
|
- [ ] 1.2 Review relevant artifacts and references listed in Affected Areas
|
||||||
|
|
||||||
|
## 2. Execution
|
||||||
|
|
||||||
|
- [ ] 2.1 Execute first concrete work item from design.md
|
||||||
|
- [ ] 2.2 Execute next concrete work item from design.md
|
||||||
|
|
||||||
|
## 3. Validation
|
||||||
|
|
||||||
|
- [ ] 3.1 Run required validation from Verification Plan
|
||||||
|
- [ ] 3.2 Perform quality checks required by the project or workflow
|
||||||
|
- [ ] 3.3 Perform required manual review or acceptance checks from Verification Plan
|
||||||
|
|
||||||
|
## 4. Documentation / Communication
|
||||||
|
|
||||||
|
- [ ] 4.1 Update relevant documentation, runbooks, communication materials, or project references if behavior, process, interface, configuration, or usage changed
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
## Purpose
|
|
||||||
|
|
||||||
定义应用全局常量,作为应用元信息(name、title、subtitle、description、version)的唯一真实来源,供前后端及构建脚本统一引用,消除硬编码散落。
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
### Requirement: 应用元信息唯一来源
|
|
||||||
|
|
||||||
系统 SHALL 在 `src/shared/app.ts` 中定义应用全局常量 `APP`,包含以下字段:
|
|
||||||
- `name`:机器标识(kebab-case 格式)
|
|
||||||
- `title`:人类可读标题
|
|
||||||
- `subtitle`:副标题
|
|
||||||
- `description`:应用描述(用于 SEO meta)
|
|
||||||
- `version`:语义版本号
|
|
||||||
|
|
||||||
`APP` SHALL 使用 `as const` 声明,保证字面量类型推断。
|
|
||||||
|
|
||||||
#### Scenario: 后端引用应用名称
|
|
||||||
|
|
||||||
- **WHEN** 后端代码需要应用名称(如 CLI 帮助文本、health 响应、启动日志)
|
|
||||||
- **THEN** 系统 SHALL 从 `src/shared/app.ts` 导入 `APP.name`
|
|
||||||
|
|
||||||
#### Scenario: 前端引用应用标题
|
|
||||||
|
|
||||||
- **WHEN** 前端代码需要应用标题(如 Header logo、欢迎文本)
|
|
||||||
- **THEN** 系统 SHALL 从 `src/shared/app.ts` 导入 `APP.title`
|
|
||||||
|
|
||||||
#### Scenario: 构建脚本引用应用名称
|
|
||||||
|
|
||||||
- **WHEN** 构建脚本需要确定可执行文件名
|
|
||||||
- **THEN** 系统 SHALL 从 `src/shared/app.ts` 导入 `APP.name`
|
|
||||||
|
|
||||||
### Requirement: 前端 HTML 元信息动态设置
|
|
||||||
|
|
||||||
系统 SHALL 在 React 应用挂载时动态设置 HTML 元信息:
|
|
||||||
- `document.title` SHALL 设置为 `APP.title`
|
|
||||||
- `<meta name="description">` 内容 SHALL 设置为 `APP.description`
|
|
||||||
|
|
||||||
#### Scenario: 页面标题显示应用名称
|
|
||||||
|
|
||||||
- **WHEN** 用户访问应用
|
|
||||||
- **THEN** 浏览器标签页标题 SHALL 显示 `APP.title`
|
|
||||||
|
|
||||||
#### Scenario: meta description 设置
|
|
||||||
|
|
||||||
- **WHEN** 搜索引擎爬取页面
|
|
||||||
- **THEN** meta description SHALL 包含 `APP.description`
|
|
||||||
|
|
||||||
### Requirement: localStorage key 语义化命名
|
|
||||||
|
|
||||||
主题偏好存储 key SHALL 为 `"theme.preference"`,不包含应用名前缀。
|
|
||||||
|
|
||||||
#### Scenario: 主题偏好持久化
|
|
||||||
|
|
||||||
- **WHEN** 用户选择主题偏好(system/light/dark)
|
|
||||||
- **THEN** 系统 SHALL 将偏好值存储到 localStorage key `"theme.preference"`
|
|
||||||
|
|
||||||
#### Scenario: 主题偏好读取
|
|
||||||
|
|
||||||
- **WHEN** 应用初始化时读取用户主题偏好
|
|
||||||
- **THEN** 系统 SHALL 从 localStorage key `"theme.preference"` 读取
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "my-app",
|
"name": "my-app",
|
||||||
|
"version": "0.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -14,7 +15,11 @@
|
|||||||
"test": "bun test",
|
"test": "bun test",
|
||||||
"clean": "bun run scripts/clean.ts",
|
"clean": "bun run scripts/clean.ts",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"prepare": "husky"
|
"prepare": "husky",
|
||||||
|
"version:patch": "bun run scripts/bump-version.ts patch",
|
||||||
|
"version:minor": "bun run scripts/bump-version.ts minor",
|
||||||
|
"version:major": "bun run scripts/bump-version.ts major",
|
||||||
|
"version:set": "bun run scripts/bump-version.ts set"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "^21.0.1",
|
"@commitlint/cli": "^21.0.1",
|
||||||
|
|||||||
@@ -3,11 +3,13 @@ import { join, relative, sep } from "node:path";
|
|||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
import { APP } from "../src/shared/app";
|
import { APP } from "../src/shared/app";
|
||||||
|
import { validateVersion } from "./bump-version-logic";
|
||||||
|
|
||||||
const projectRoot = fileURLToPath(new URL("..", import.meta.url));
|
const projectRoot = fileURLToPath(new URL("..", import.meta.url));
|
||||||
const distWebDir = join(projectRoot, "dist/web");
|
const distWebDir = join(projectRoot, "dist/web");
|
||||||
const buildDir = join(projectRoot, ".build");
|
const buildDir = join(projectRoot, ".build");
|
||||||
const executablePath = join(projectRoot, `dist/${APP.name}`);
|
const executablePath = join(projectRoot, `dist/${APP.name}`);
|
||||||
|
const packageJsonPath = join(projectRoot, "package.json");
|
||||||
|
|
||||||
async function build() {
|
async function build() {
|
||||||
try {
|
try {
|
||||||
@@ -62,6 +64,14 @@ async function codeGeneration() {
|
|||||||
await rm(buildDir, { force: true, recursive: true });
|
await rm(buildDir, { force: true, recursive: true });
|
||||||
await Bun.write(join(buildDir, ".gitkeep"), "");
|
await Bun.write(join(buildDir, ".gitkeep"), "");
|
||||||
|
|
||||||
|
const packageJson = (await Bun.file(packageJsonPath).json()) as { version: string };
|
||||||
|
const version = packageJson.version;
|
||||||
|
if (typeof version !== "string") {
|
||||||
|
console.error("package.json does not have a valid version field");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
validateVersion(version);
|
||||||
|
|
||||||
const allFiles = await scanDir(distWebDir, "/");
|
const allFiles = await scanDir(distWebDir, "/");
|
||||||
const importLines: string[] = [];
|
const importLines: string[] = [];
|
||||||
const fileEntries: string[] = [];
|
const fileEntries: string[] = [];
|
||||||
@@ -106,9 +116,11 @@ async function codeGeneration() {
|
|||||||
`import { parseRuntimeArgs } from "../src/server/config";`,
|
`import { parseRuntimeArgs } from "../src/server/config";`,
|
||||||
`import { staticAssets } from "./static-assets";`,
|
`import { staticAssets } from "./static-assets";`,
|
||||||
"",
|
"",
|
||||||
|
`const APP_VERSION = "${version}" as const;`,
|
||||||
|
"",
|
||||||
`async function main() {`,
|
`async function main() {`,
|
||||||
` const { configPath } = parseRuntimeArgs();`,
|
` const { configPath } = parseRuntimeArgs();`,
|
||||||
` await bootstrap({ configPath, mode: "production", staticAssets });`,
|
` await bootstrap({ configPath, mode: "production", staticAssets, version: APP_VERSION });`,
|
||||||
`}`,
|
`}`,
|
||||||
"",
|
"",
|
||||||
`void main().catch((error) => {`,
|
`void main().catch((error) => {`,
|
||||||
|
|||||||
40
scripts/bump-version-logic.ts
Normal file
40
scripts/bump-version-logic.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
const VERSION_REGEX = /^\d+\.\d+\.\d+$/;
|
||||||
|
|
||||||
|
export function bumpVersion(current: string, command: "major" | "minor" | "patch" | "set", target?: string): string {
|
||||||
|
validateVersion(current);
|
||||||
|
const [major, minor, patch] = parseVersion(current);
|
||||||
|
|
||||||
|
switch (command) {
|
||||||
|
case "major":
|
||||||
|
return formatVersion(major + 1, 0, 0);
|
||||||
|
case "minor":
|
||||||
|
return formatVersion(major, minor + 1, 0);
|
||||||
|
case "patch":
|
||||||
|
return formatVersion(major, minor, patch + 1);
|
||||||
|
case "set": {
|
||||||
|
if (!target) {
|
||||||
|
throw new Error("set command requires a target version");
|
||||||
|
}
|
||||||
|
validateVersion(target);
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatVersion(major: number, minor: number, patch: number): string {
|
||||||
|
return `${major}.${minor}.${patch}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseVersion(version: string): [number, number, number] {
|
||||||
|
const parts = version.split(".").map((p) => parseInt(p, 10));
|
||||||
|
if (parts.length !== 3 || parts.some(isNaN)) {
|
||||||
|
throw new Error(`Invalid version format: ${version}`);
|
||||||
|
}
|
||||||
|
return [parts[0]!, parts[1]!, parts[2]!];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateVersion(version: string): void {
|
||||||
|
if (!VERSION_REGEX.test(version)) {
|
||||||
|
throw new Error(`Invalid version format: ${version}. Expected MAJOR.MINOR.PATCH (e.g., 0.1.0)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
45
scripts/bump-version.ts
Normal file
45
scripts/bump-version.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { writeFileSync } from "node:fs";
|
||||||
|
import { resolve } from "node:path";
|
||||||
|
|
||||||
|
import { bumpVersion, validateVersion } from "./bump-version-logic";
|
||||||
|
|
||||||
|
const PACKAGE_JSON_PATH = resolve(import.meta.dir, "..", "package.json");
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
if (args.length === 0) {
|
||||||
|
console.error("Usage: bun run bump-version.ts <patch|minor|major|set> [version]");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const command = args[0];
|
||||||
|
if (command !== "patch" && command !== "minor" && command !== "major" && command !== "set") {
|
||||||
|
console.error(`Unknown command: ${command}. Expected patch, minor, major, or set`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command === "set" && args.length < 2) {
|
||||||
|
console.error("Usage: bun run bump-version.ts set <version>");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const packageJson = (await Bun.file(PACKAGE_JSON_PATH).json()) as { version: string };
|
||||||
|
const currentVersion = packageJson.version;
|
||||||
|
|
||||||
|
if (typeof currentVersion !== "string") {
|
||||||
|
console.error("package.json does not have a valid version field");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
validateVersion(currentVersion);
|
||||||
|
|
||||||
|
const targetVersion = command === "set" ? args[1] : undefined;
|
||||||
|
const nextVersion = bumpVersion(currentVersion, command, targetVersion);
|
||||||
|
|
||||||
|
packageJson.version = nextVersion;
|
||||||
|
writeFileSync(PACKAGE_JSON_PATH, JSON.stringify(packageJson, null, 2) + "\n");
|
||||||
|
|
||||||
|
console.log(`${currentVersion} -> ${nextVersion}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main();
|
||||||
@@ -17,6 +17,7 @@ export interface BootstrapOptions {
|
|||||||
configPath?: string;
|
configPath?: string;
|
||||||
mode: RuntimeMode;
|
mode: RuntimeMode;
|
||||||
staticAssets?: StartServerOptions["staticAssets"];
|
staticAssets?: StartServerOptions["staticAssets"];
|
||||||
|
version?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function bootstrap(options: BootstrapOptions, dependencies: BootstrapDependencies = {}): Promise<void> {
|
export async function bootstrap(options: BootstrapOptions, dependencies: BootstrapDependencies = {}): Promise<void> {
|
||||||
@@ -38,7 +39,7 @@ export async function bootstrap(options: BootstrapOptions, dependencies: Bootstr
|
|||||||
onSignal("SIGINT", shutdown);
|
onSignal("SIGINT", shutdown);
|
||||||
onSignal("SIGTERM", shutdown);
|
onSignal("SIGTERM", shutdown);
|
||||||
|
|
||||||
serve({ config, mode: options.mode, staticAssets: options.staticAssets });
|
serve({ config, mode: options.mode, staticAssets: options.staticAssets, version: options.version });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError("启动失败:", error instanceof Error ? error.message : error);
|
logError("启动失败:", error instanceof Error ? error.message : error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ApiErrorResponse, HealthResponse, RuntimeMode } from "../shared/api";
|
import type { ApiErrorResponse, MetaResponse, RuntimeMode } from "../shared/api";
|
||||||
|
|
||||||
import { APP } from "../shared/app";
|
import { APP } from "../shared/app";
|
||||||
|
|
||||||
@@ -17,11 +17,12 @@ export function createHeaders(mode: RuntimeMode, init: HeadersInit): Headers {
|
|||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createHealthResponse(): HealthResponse {
|
export function createMetaResponse(version: string): MetaResponse {
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
service: APP.name,
|
service: APP.name,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
|
version,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
import type { RuntimeMode } from "../../shared/api";
|
|
||||||
|
|
||||||
import { createHealthResponse, jsonResponse } from "../helpers";
|
|
||||||
|
|
||||||
export function handleHealth(mode: RuntimeMode): Response {
|
|
||||||
return jsonResponse(createHealthResponse(), { mode });
|
|
||||||
}
|
|
||||||
7
src/server/routes/meta.ts
Normal file
7
src/server/routes/meta.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import type { RuntimeMode } from "../../shared/api";
|
||||||
|
|
||||||
|
import { createMetaResponse, jsonResponse } from "../helpers";
|
||||||
|
|
||||||
|
export function handleMeta(mode: RuntimeMode, version: string): Response {
|
||||||
|
return jsonResponse(createMetaResponse(version), { mode });
|
||||||
|
}
|
||||||
@@ -4,17 +4,24 @@ import type { StaticAssets } from "./static";
|
|||||||
|
|
||||||
import { APP } from "../shared/app";
|
import { APP } from "../shared/app";
|
||||||
import { createApiError, jsonResponse } from "./helpers";
|
import { createApiError, jsonResponse } from "./helpers";
|
||||||
import { handleHealth } from "./routes/health";
|
import { handleMeta } from "./routes/meta";
|
||||||
import { serveStaticAsset } from "./static";
|
import { serveStaticAsset } from "./static";
|
||||||
|
import { readAppVersion } from "./version";
|
||||||
|
|
||||||
export interface StartServerOptions {
|
export interface StartServerOptions {
|
||||||
config: ServerConfig;
|
config: ServerConfig;
|
||||||
mode: RuntimeMode;
|
mode: RuntimeMode;
|
||||||
staticAssets?: StaticAssets;
|
staticAssets?: StaticAssets;
|
||||||
|
version?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function startServer(options: StartServerOptions) {
|
export function startServer(options: StartServerOptions) {
|
||||||
const { config, mode, staticAssets } = options;
|
const { config, mode, staticAssets, version } = options;
|
||||||
|
|
||||||
|
const resolveVersion = (): Promise<string> => {
|
||||||
|
if (version) return Promise.resolve(version);
|
||||||
|
return readAppVersion();
|
||||||
|
};
|
||||||
|
|
||||||
const server = Bun.serve({
|
const server = Bun.serve({
|
||||||
fetch(req) {
|
fetch(req) {
|
||||||
@@ -27,8 +34,11 @@ export function startServer(options: StartServerOptions) {
|
|||||||
port: config.port,
|
port: config.port,
|
||||||
routes: {
|
routes: {
|
||||||
"/api/*": () => jsonResponse(createApiError("API route not found", 404), { mode, status: 404 }),
|
"/api/*": () => jsonResponse(createApiError("API route not found", 404), { mode, status: 404 }),
|
||||||
"/health": {
|
"/api/meta": {
|
||||||
GET: () => handleHealth(mode),
|
GET: async () => {
|
||||||
|
const resolvedVersion = await resolveVersion();
|
||||||
|
return handleMeta(mode, resolvedVersion);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
17
src/server/version.ts
Normal file
17
src/server/version.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { resolve } from "node:path";
|
||||||
|
|
||||||
|
import { validateVersion } from "../../scripts/bump-version-logic";
|
||||||
|
|
||||||
|
const PACKAGE_JSON_PATH = resolve(import.meta.dir, "..", "..", "package.json");
|
||||||
|
|
||||||
|
export async function readAppVersion(): Promise<string> {
|
||||||
|
const packageJson = (await Bun.file(PACKAGE_JSON_PATH).json()) as { version: string };
|
||||||
|
const version = packageJson.version;
|
||||||
|
|
||||||
|
if (typeof version !== "string") {
|
||||||
|
throw new Error("package.json does not have a valid version field");
|
||||||
|
}
|
||||||
|
|
||||||
|
validateVersion(version);
|
||||||
|
return version;
|
||||||
|
}
|
||||||
@@ -3,10 +3,11 @@ export interface ApiErrorResponse {
|
|||||||
status: number;
|
status: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HealthResponse {
|
export interface MetaResponse {
|
||||||
ok: true;
|
ok: true;
|
||||||
service: string;
|
service: string;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
|
version: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RuntimeMode = "development" | "production" | "test";
|
export type RuntimeMode = "development" | "production" | "test";
|
||||||
|
|||||||
@@ -3,5 +3,4 @@ export const APP = {
|
|||||||
name: "my-app",
|
name: "my-app",
|
||||||
subtitle: "Bun 全栈应用",
|
subtitle: "Bun 全栈应用",
|
||||||
title: "My App",
|
title: "My App",
|
||||||
version: "0.1.0",
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useLocation } from "react-router";
|
import { useLocation } from "react-router";
|
||||||
import { ChevronLeftIcon, ChevronRightIcon } from "tdesign-icons-react";
|
import { Layout, RadioGroup } from "tdesign-react";
|
||||||
import { Button, Layout, RadioGroup } from "tdesign-react";
|
|
||||||
|
import type { MetaResponse } from "../shared/api";
|
||||||
|
|
||||||
import { APP } from "../shared/app";
|
import { APP } from "../shared/app";
|
||||||
import { Sidebar } from "./components/Sidebar";
|
import { Sidebar } from "./components/Sidebar";
|
||||||
@@ -22,6 +24,12 @@ export function App() {
|
|||||||
const { preference: themePreference, setPreference: setThemePreference } = useThemePreference();
|
const { preference: themePreference, setPreference: setThemePreference } = useThemePreference();
|
||||||
const { collapsed, toggleCollapsed } = useSidebarCollapsed();
|
const { collapsed, toggleCollapsed } = useSidebarCollapsed();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
const { data: meta } = useQuery({
|
||||||
|
queryFn: fetchMeta,
|
||||||
|
queryKey: ["meta"],
|
||||||
|
refetchInterval: 30000,
|
||||||
|
staleTime: 5000,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = APP.title;
|
document.title = APP.title;
|
||||||
@@ -35,15 +43,16 @@ export function App() {
|
|||||||
const currentPath = location.pathname;
|
const currentPath = location.pathname;
|
||||||
const currentItem = MENU_ITEMS.find((item) => item.path === currentPath);
|
const currentItem = MENU_ITEMS.find((item) => item.path === currentPath);
|
||||||
const pageTitle = currentItem?.label ?? APP.title;
|
const pageTitle = currentItem?.label ?? APP.title;
|
||||||
|
const versionDisplay = meta?.version ? `v${meta.version}` : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout className="app-layout">
|
<Layout className="app-layout">
|
||||||
<Header className="app-header">
|
<Header className="app-header">
|
||||||
<div className="app-header-left">
|
<div className="app-header-left">
|
||||||
<Button className="app-sidebar-toggle" onClick={toggleCollapsed} shape="square" variant="text">
|
<span className="app-brand-group">
|
||||||
{collapsed ? <ChevronRightIcon /> : <ChevronLeftIcon />}
|
<span className="app-brand">{APP.title}</span>
|
||||||
</Button>
|
{versionDisplay && <span className="app-version">{versionDisplay}</span>}
|
||||||
<span className="app-brand">{APP.title}</span>
|
</span>
|
||||||
<span className="app-page-title">{pageTitle}</span>
|
<span className="app-page-title">{pageTitle}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="app-header-right">
|
<div className="app-header-right">
|
||||||
@@ -57,8 +66,8 @@ export function App() {
|
|||||||
</div>
|
</div>
|
||||||
</Header>
|
</Header>
|
||||||
<Layout>
|
<Layout>
|
||||||
<Aside className="app-sidebar" width={collapsed ? "80px" : "232px"}>
|
<Aside className="app-sidebar" width={collapsed ? "64px" : "232px"}>
|
||||||
<Sidebar collapsed={collapsed} />
|
<Sidebar collapsed={collapsed} onToggleCollapsed={toggleCollapsed} />
|
||||||
</Aside>
|
</Aside>
|
||||||
<Layout>
|
<Layout>
|
||||||
<Content className="app-content">
|
<Content className="app-content">
|
||||||
@@ -69,3 +78,9 @@ export function App() {
|
|||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchMeta(): Promise<MetaResponse> {
|
||||||
|
const response = await fetch("/api/meta");
|
||||||
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
|
return response.json() as Promise<MetaResponse>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useLocation, useNavigate } from "react-router";
|
import { useLocation, useNavigate } from "react-router";
|
||||||
import { Menu } from "tdesign-react";
|
import { ChevronLeftIcon, ChevronRightIcon } from "tdesign-icons-react";
|
||||||
|
import { Button, Menu } from "tdesign-react";
|
||||||
|
|
||||||
import { MENU_ITEMS } from "../../menu";
|
import { MENU_ITEMS } from "../../menu";
|
||||||
|
|
||||||
@@ -7,9 +8,10 @@ const { MenuItem } = Menu;
|
|||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
|
onToggleCollapsed: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Sidebar({ collapsed }: SidebarProps) {
|
export function Sidebar({ collapsed, onToggleCollapsed }: SidebarProps) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
|
|
||||||
@@ -29,8 +31,17 @@ export function Sidebar({ collapsed }: SidebarProps) {
|
|||||||
className="app-sidebar-menu"
|
className="app-sidebar-menu"
|
||||||
collapsed={collapsed}
|
collapsed={collapsed}
|
||||||
onChange={handleMenuChange}
|
onChange={handleMenuChange}
|
||||||
|
operations={
|
||||||
|
<Button
|
||||||
|
className="app-sidebar-collapse-btn"
|
||||||
|
icon={collapsed ? <ChevronRightIcon /> : <ChevronLeftIcon />}
|
||||||
|
onClick={onToggleCollapsed}
|
||||||
|
shape="square"
|
||||||
|
variant="text"
|
||||||
|
/>
|
||||||
|
}
|
||||||
value={activeValue}
|
value={activeValue}
|
||||||
width={collapsed ? "80px" : "232px"}
|
width={collapsed ? "64px" : "232px"}
|
||||||
>
|
>
|
||||||
{MENU_ITEMS.map((item) => (
|
{MENU_ITEMS.map((item) => (
|
||||||
<MenuItem icon={item.icon} key={item.value} value={item.value}>
|
<MenuItem icon={item.icon} key={item.value} value={item.value}>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export function NotFoundPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Space align="center" className="not-found-page" direction="vertical" size="large">
|
<Space align="center" className="not-found-page" direction="vertical" size="large">
|
||||||
<ErrorCircleIcon size="64px" style={{ color: "var(--td-warning-color)" }} />
|
<ErrorCircleIcon className="not-found-icon" size="64px" />
|
||||||
<h1>404</h1>
|
<h1>404</h1>
|
||||||
<p>您访问的页面不存在</p>
|
<p>您访问的页面不存在</p>
|
||||||
<Button onClick={handleGoHome} theme="primary">
|
<Button onClick={handleGoHome} theme="primary">
|
||||||
|
|||||||
@@ -1,29 +1,29 @@
|
|||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { Space } from "tdesign-react";
|
import { Space } from "tdesign-react";
|
||||||
|
|
||||||
import type { HealthResponse } from "../../../shared/api";
|
import type { MetaResponse } from "../../../shared/api";
|
||||||
|
|
||||||
import { APP } from "../../../shared/app";
|
import { APP } from "../../../shared/app";
|
||||||
|
|
||||||
export function DashboardPage() {
|
export function DashboardPage() {
|
||||||
const { data: health } = useQuery({
|
const { data: meta } = useQuery({
|
||||||
queryFn: fetchHealth,
|
queryFn: fetchMeta,
|
||||||
queryKey: ["health"],
|
queryKey: ["meta"],
|
||||||
refetchInterval: 30000,
|
refetchInterval: 30000,
|
||||||
staleTime: 5000,
|
staleTime: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space direction="vertical" size="large" style={{ width: "100%" }}>
|
<Space className="full-width-space" direction="vertical" size="large">
|
||||||
<h2>欢迎使用 {APP.title}</h2>
|
<h2>欢迎使用 {APP.title}</h2>
|
||||||
<p>在此构建你的应用。以下是 /health API 的返回数据(前后端联调示例):</p>
|
<p>在此构建你的应用。以下是 /api/meta 的返回数据(前后端联调示例):</p>
|
||||||
{health && <pre className="health-response">{JSON.stringify(health, null, 2)}</pre>}
|
{meta && <pre className="meta-response">{JSON.stringify(meta, null, 2)}</pre>}
|
||||||
</Space>
|
</Space>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchHealth(): Promise<HealthResponse> {
|
async function fetchMeta(): Promise<MetaResponse> {
|
||||||
const response = await fetch("/health");
|
const response = await fetch("/api/meta");
|
||||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||||
return response.json() as Promise<HealthResponse>;
|
return response.json() as Promise<MetaResponse>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Card, Space } from "tdesign-react";
|
|||||||
|
|
||||||
export function SettingsPage() {
|
export function SettingsPage() {
|
||||||
return (
|
return (
|
||||||
<Space direction="vertical" size="large" style={{ width: "100%" }}>
|
<Space className="full-width-space" direction="vertical" size="large">
|
||||||
<h2>系统设置</h2>
|
<h2>系统设置</h2>
|
||||||
<Card>
|
<Card>
|
||||||
<p>页面建设中...</p>
|
<p>页面建设中...</p>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Card, Space } from "tdesign-react";
|
|||||||
|
|
||||||
export function UsersPage() {
|
export function UsersPage() {
|
||||||
return (
|
return (
|
||||||
<Space direction="vertical" size="large" style={{ width: "100%" }}>
|
<Space className="full-width-space" direction="vertical" size="large">
|
||||||
<h2>用户管理</h2>
|
<h2>用户管理</h2>
|
||||||
<Card>
|
<Card>
|
||||||
<p>页面建设中...</p>
|
<p>页面建设中...</p>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
import { Route, Routes } from "react-router";
|
import { Route, Routes } from "react-router";
|
||||||
|
|
||||||
import { NotFoundPage } from "./pages/404";
|
import { NotFoundPage } from "./pages/404";
|
||||||
@@ -15,3 +17,7 @@ export function AppRoutes() {
|
|||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ProtectedRoute({ children }: { children: ReactNode }) {
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|||||||
@@ -30,8 +30,10 @@
|
|||||||
gap: var(--td-comp-margin-s);
|
gap: var(--td-comp-margin-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-sidebar-toggle {
|
.app-brand-group {
|
||||||
padding: var(--td-comp-paddingTB-s) var(--td-comp-paddingLR-s);
|
display: inline-flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: var(--td-comp-margin-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-brand {
|
.app-brand {
|
||||||
@@ -39,6 +41,20 @@
|
|||||||
color: var(--td-text-color-primary);
|
color: var(--td-text-color-primary);
|
||||||
font-size: calc(var(--td-font-size-title-large) + 6px);
|
font-size: calc(var(--td-font-size-title-large) + 6px);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-version {
|
||||||
|
color: var(--td-text-color-placeholder);
|
||||||
|
font-size: var(--td-font-size-body-small);
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-sidebar-collapse-btn {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--td-text-color-secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-page-title {
|
.app-page-title {
|
||||||
@@ -64,7 +80,7 @@
|
|||||||
min-height: calc(100vh - 64px);
|
min-height: calc(100vh - 64px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.health-response {
|
.meta-response {
|
||||||
background: var(--td-bg-color-component);
|
background: var(--td-bg-color-component);
|
||||||
border-radius: var(--td-radius-default);
|
border-radius: var(--td-radius-default);
|
||||||
padding: var(--td-comp-paddingTB-l) var(--td-comp-paddingLR-l);
|
padding: var(--td-comp-paddingTB-l) var(--td-comp-paddingLR-l);
|
||||||
@@ -86,6 +102,14 @@
|
|||||||
color: var(--td-text-color-disabled);
|
color: var(--td-text-color-disabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.full-width-space {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.not-found-icon {
|
||||||
|
color: var(--td-warning-color);
|
||||||
|
}
|
||||||
|
|
||||||
.tabular-nums {
|
.tabular-nums {
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
}
|
}
|
||||||
|
|||||||
48
tests/scripts/build.test.ts
Normal file
48
tests/scripts/build.test.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
|
||||||
|
import { validateVersion } from "../../scripts/bump-version-logic";
|
||||||
|
|
||||||
|
describe("build 版本注入", () => {
|
||||||
|
test("validateVersion 接受有效版本", () => {
|
||||||
|
expect(() => validateVersion("0.1.0")).not.toThrow();
|
||||||
|
expect(() => validateVersion("1.2.3")).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("validateVersion 拒绝无效版本", () => {
|
||||||
|
expect(() => validateVersion("invalid")).toThrow();
|
||||||
|
expect(() => validateVersion("1.0.0-beta.1")).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("生成的 server-entry 包含版本字面量", () => {
|
||||||
|
const version = "0.1.0";
|
||||||
|
const serverEntryTs = [
|
||||||
|
`import { bootstrap } from "../src/server/bootstrap";`,
|
||||||
|
`import { parseRuntimeArgs } from "../src/server/config";`,
|
||||||
|
`import { staticAssets } from "./static-assets";`,
|
||||||
|
"",
|
||||||
|
`const APP_VERSION = "${version}" as const;`,
|
||||||
|
"",
|
||||||
|
`async function main() {`,
|
||||||
|
` const { configPath } = parseRuntimeArgs();`,
|
||||||
|
` await bootstrap({ configPath, mode: "production", staticAssets, version: APP_VERSION });`,
|
||||||
|
`}`,
|
||||||
|
"",
|
||||||
|
`void main().catch((error) => {`,
|
||||||
|
` console.error("启动失败:", error instanceof Error ? error.message : error);`,
|
||||||
|
` process.exit(1);`,
|
||||||
|
`});`,
|
||||||
|
"",
|
||||||
|
].join("\n");
|
||||||
|
|
||||||
|
expect(serverEntryTs).toContain(`const APP_VERSION = "${version}"`);
|
||||||
|
expect(serverEntryTs).toContain("version: APP_VERSION");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("版本字面量不依赖外部 package.json", () => {
|
||||||
|
const serverEntryTs = [`const APP_VERSION = "0.1.0" as const;`].join("\n");
|
||||||
|
|
||||||
|
expect(serverEntryTs).not.toContain("package.json");
|
||||||
|
expect(serverEntryTs).not.toContain("Bun.file");
|
||||||
|
expect(serverEntryTs).toContain('"0.1.0"');
|
||||||
|
});
|
||||||
|
});
|
||||||
73
tests/scripts/bump-version.test.ts
Normal file
73
tests/scripts/bump-version.test.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import { describe, expect, test } from "bun:test";
|
||||||
|
|
||||||
|
import { bumpVersion, formatVersion, parseVersion, validateVersion } from "../../scripts/bump-version-logic";
|
||||||
|
|
||||||
|
describe("版本解析与校验", () => {
|
||||||
|
test("parseVersion 解析有效版本", () => {
|
||||||
|
expect(parseVersion("0.1.0")).toEqual([0, 1, 0]);
|
||||||
|
expect(parseVersion("1.2.3")).toEqual([1, 2, 3]);
|
||||||
|
expect(parseVersion("10.20.30")).toEqual([10, 20, 30]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parseVersion 拒绝无效版本", () => {
|
||||||
|
expect(() => parseVersion("invalid")).toThrow();
|
||||||
|
expect(() => parseVersion("1.2")).toThrow();
|
||||||
|
expect(() => parseVersion("1.2.3.4")).toThrow();
|
||||||
|
expect(() => parseVersion("1.2.a")).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("validateVersion 接受有效版本", () => {
|
||||||
|
expect(() => validateVersion("0.1.0")).not.toThrow();
|
||||||
|
expect(() => validateVersion("1.2.3")).not.toThrow();
|
||||||
|
expect(() => validateVersion("10.20.30")).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("validateVersion 拒绝无效版本", () => {
|
||||||
|
expect(() => validateVersion("")).toThrow();
|
||||||
|
expect(() => validateVersion("invalid")).toThrow();
|
||||||
|
expect(() => validateVersion("1.2")).toThrow();
|
||||||
|
expect(() => validateVersion("1.2.3.4")).toThrow();
|
||||||
|
expect(() => validateVersion("1.0.0-beta.1")).toThrow();
|
||||||
|
expect(() => validateVersion("v1.0.0")).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("formatVersion 格式化版本", () => {
|
||||||
|
expect(formatVersion(0, 1, 0)).toBe("0.1.0");
|
||||||
|
expect(formatVersion(1, 2, 3)).toBe("1.2.3");
|
||||||
|
expect(formatVersion(10, 20, 30)).toBe("10.20.30");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("版本升迁逻辑", () => {
|
||||||
|
test("bumpVersion patch 升迁", () => {
|
||||||
|
expect(bumpVersion("1.2.3", "patch")).toBe("1.2.4");
|
||||||
|
expect(bumpVersion("0.1.0", "patch")).toBe("0.1.1");
|
||||||
|
expect(bumpVersion("0.0.1", "patch")).toBe("0.0.2");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bumpVersion minor 升迁", () => {
|
||||||
|
expect(bumpVersion("1.2.3", "minor")).toBe("1.3.0");
|
||||||
|
expect(bumpVersion("0.1.0", "minor")).toBe("0.2.0");
|
||||||
|
expect(bumpVersion("0.0.1", "minor")).toBe("0.1.0");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bumpVersion major 升迁", () => {
|
||||||
|
expect(bumpVersion("1.2.3", "major")).toBe("2.0.0");
|
||||||
|
expect(bumpVersion("0.1.0", "major")).toBe("1.0.0");
|
||||||
|
expect(bumpVersion("0.0.1", "major")).toBe("1.0.0");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bumpVersion set 设置版本", () => {
|
||||||
|
expect(bumpVersion("1.2.3", "set", "2.0.0")).toBe("2.0.0");
|
||||||
|
expect(bumpVersion("0.1.0", "set", "0.2.0")).toBe("0.2.0");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bumpVersion set 拒绝无效版本", () => {
|
||||||
|
expect(() => bumpVersion("1.2.3", "set", "invalid")).toThrow();
|
||||||
|
expect(() => bumpVersion("1.2.3", "set", "1.0.0-beta.1")).toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("bumpVersion set 缺少目标版本报错", () => {
|
||||||
|
expect(() => bumpVersion("1.2.3", "set")).toThrow("set command requires a target version");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -21,6 +21,7 @@ describe("bootstrap", () => {
|
|||||||
signalRegistered = true;
|
signalRegistered = true;
|
||||||
};
|
};
|
||||||
const mockStartServer = (_options: StartServerOptions) => {
|
const mockStartServer = (_options: StartServerOptions) => {
|
||||||
|
expect(_options.version).toBeUndefined();
|
||||||
started = true;
|
started = true;
|
||||||
return {};
|
return {};
|
||||||
};
|
};
|
||||||
@@ -38,6 +39,24 @@ describe("bootstrap", () => {
|
|||||||
expect(signalRegistered).toBe(true);
|
expect(signalRegistered).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("传递 version 给 startServer", async () => {
|
||||||
|
let receivedVersion: string | undefined;
|
||||||
|
|
||||||
|
const deps: BootstrapDependencies = {
|
||||||
|
loadConfig: async () => ({ host: "127.0.0.1", port: 0 }),
|
||||||
|
logError: () => {},
|
||||||
|
onSignal: () => {},
|
||||||
|
startServer: (options: StartServerOptions) => {
|
||||||
|
receivedVersion = options.version;
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await bootstrap({ mode: "production", version: "1.2.3" }, deps);
|
||||||
|
|
||||||
|
expect(receivedVersion).toBe("1.2.3");
|
||||||
|
});
|
||||||
|
|
||||||
test("启动失败时调用 logError", async () => {
|
test("启动失败时调用 logError", async () => {
|
||||||
let errorLogged = false;
|
let errorLogged = false;
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,13 @@ import { renderWithProviders } from "./test-utils";
|
|||||||
describe("App", () => {
|
describe("App", () => {
|
||||||
test("渲染 Layout 骨架和品牌名", () => {
|
test("渲染 Layout 骨架和品牌名", () => {
|
||||||
window.fetch = (async () => {
|
window.fetch = (async () => {
|
||||||
return new Response(JSON.stringify({ ok: true, service: "test-app", timestamp: new Date().toISOString() }), {
|
return new Response(
|
||||||
headers: { "Content-Type": "application/json" },
|
JSON.stringify({ ok: true, service: "test-app", timestamp: new Date().toISOString(), version: "0.1.0" }),
|
||||||
status: 200,
|
{
|
||||||
});
|
headers: { "Content-Type": "application/json" },
|
||||||
|
status: 200,
|
||||||
|
},
|
||||||
|
);
|
||||||
}) as unknown as typeof fetch;
|
}) as unknown as typeof fetch;
|
||||||
|
|
||||||
renderWithProviders(createElement(App));
|
renderWithProviders(createElement(App));
|
||||||
@@ -26,10 +29,13 @@ describe("App", () => {
|
|||||||
|
|
||||||
test("渲染侧边栏菜单项", () => {
|
test("渲染侧边栏菜单项", () => {
|
||||||
window.fetch = (async () => {
|
window.fetch = (async () => {
|
||||||
return new Response(JSON.stringify({ ok: true, service: "test-app", timestamp: new Date().toISOString() }), {
|
return new Response(
|
||||||
headers: { "Content-Type": "application/json" },
|
JSON.stringify({ ok: true, service: "test-app", timestamp: new Date().toISOString(), version: "0.1.0" }),
|
||||||
status: 200,
|
{
|
||||||
});
|
headers: { "Content-Type": "application/json" },
|
||||||
|
status: 200,
|
||||||
|
},
|
||||||
|
);
|
||||||
}) as unknown as typeof fetch;
|
}) as unknown as typeof fetch;
|
||||||
|
|
||||||
renderWithProviders(createElement(App));
|
renderWithProviders(createElement(App));
|
||||||
@@ -38,4 +44,21 @@ describe("App", () => {
|
|||||||
expect(screen.getAllByText("用户管理").length).toBeGreaterThan(0);
|
expect(screen.getAllByText("用户管理").length).toBeGreaterThan(0);
|
||||||
expect(screen.getAllByText("系统设置").length).toBeGreaterThan(0);
|
expect(screen.getAllByText("系统设置").length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Header 不包含侧边栏折叠按钮", () => {
|
||||||
|
window.fetch = (async () => {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ ok: true, service: "test-app", timestamp: new Date().toISOString(), version: "0.1.0" }),
|
||||||
|
{
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
status: 200,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}) as unknown as typeof fetch;
|
||||||
|
|
||||||
|
renderWithProviders(createElement(App));
|
||||||
|
|
||||||
|
const toggleButtons = document.querySelectorAll(".app-sidebar-toggle");
|
||||||
|
expect(toggleButtons.length).toBe(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||||
import { screen } from "@testing-library/react";
|
import { screen } from "@testing-library/react";
|
||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import { createElement } from "react";
|
import { createElement } from "react";
|
||||||
@@ -7,7 +8,7 @@ import { renderWithProviders } from "../../test-utils";
|
|||||||
|
|
||||||
describe("Sidebar", () => {
|
describe("Sidebar", () => {
|
||||||
test("渲染菜单项", () => {
|
test("渲染菜单项", () => {
|
||||||
renderWithProviders(createElement(Sidebar, { collapsed: false }));
|
renderWithProviders(createElement(Sidebar, { collapsed: false, onToggleCollapsed: () => {} }));
|
||||||
|
|
||||||
expect(screen.getByText("仪表盘")).not.toBeNull();
|
expect(screen.getByText("仪表盘")).not.toBeNull();
|
||||||
expect(screen.getByText("用户管理")).not.toBeNull();
|
expect(screen.getByText("用户管理")).not.toBeNull();
|
||||||
@@ -15,10 +16,53 @@ describe("Sidebar", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("折叠状态下仍渲染菜单项", () => {
|
test("折叠状态下仍渲染菜单项", () => {
|
||||||
renderWithProviders(createElement(Sidebar, { collapsed: true }));
|
renderWithProviders(createElement(Sidebar, { collapsed: true, onToggleCollapsed: () => {} }));
|
||||||
|
|
||||||
expect(screen.getByText("仪表盘")).not.toBeNull();
|
expect(screen.getByText("仪表盘")).not.toBeNull();
|
||||||
expect(screen.getByText("用户管理")).not.toBeNull();
|
expect(screen.getByText("用户管理")).not.toBeNull();
|
||||||
expect(screen.getByText("系统设置")).not.toBeNull();
|
expect(screen.getByText("系统设置")).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("高亮当前路由对应的菜单项", () => {
|
||||||
|
renderWithProviders(createElement(Sidebar, { collapsed: false, onToggleCollapsed: () => {} }), {
|
||||||
|
initialRoute: "/users",
|
||||||
|
});
|
||||||
|
|
||||||
|
const activeItem = document.querySelector(".t-is-active");
|
||||||
|
expect(activeItem).not.toBeNull();
|
||||||
|
expect(activeItem?.textContent).toContain("用户管理");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("展开态底部渲染折叠按钮", () => {
|
||||||
|
renderWithProviders(createElement(Sidebar, { collapsed: false, onToggleCollapsed: () => {} }));
|
||||||
|
|
||||||
|
const collapseBtn = document.querySelector(".app-sidebar-collapse-btn");
|
||||||
|
expect(collapseBtn).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("点击底部按钮调用 onToggleCollapsed", () => {
|
||||||
|
let called = false;
|
||||||
|
const onToggle = () => {
|
||||||
|
called = true;
|
||||||
|
};
|
||||||
|
renderWithProviders(createElement(Sidebar, { collapsed: false, onToggleCollapsed: onToggle }));
|
||||||
|
|
||||||
|
const btn = document.querySelector<HTMLButtonElement>(".app-sidebar-collapse-btn");
|
||||||
|
expect(btn).not.toBeNull();
|
||||||
|
btn!.click();
|
||||||
|
expect(called).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("折叠态底部按钮仍渲染且菜单项高亮不变", () => {
|
||||||
|
renderWithProviders(createElement(Sidebar, { collapsed: true, onToggleCollapsed: () => {} }), {
|
||||||
|
initialRoute: "/users",
|
||||||
|
});
|
||||||
|
|
||||||
|
const collapseBtn = document.querySelector(".app-sidebar-collapse-btn");
|
||||||
|
expect(collapseBtn).not.toBeNull();
|
||||||
|
|
||||||
|
const activeItem = document.querySelector(".t-is-active");
|
||||||
|
expect(activeItem).not.toBeNull();
|
||||||
|
expect(activeItem?.textContent).toContain("用户管理");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,4 +13,12 @@ describe("NotFoundPage", () => {
|
|||||||
expect(screen.getByText("您访问的页面不存在")).not.toBeNull();
|
expect(screen.getByText("您访问的页面不存在")).not.toBeNull();
|
||||||
expect(screen.getByText("返回首页")).not.toBeNull();
|
expect(screen.getByText("返回首页")).not.toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("返回首页按钮存在且可点击", () => {
|
||||||
|
renderWithProviders(createElement(NotFoundPage));
|
||||||
|
|
||||||
|
const button = screen.getByText("返回首页");
|
||||||
|
expect(button).not.toBeNull();
|
||||||
|
expect(button.closest("button")).not.toBeNull();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,10 +9,13 @@ import { renderWithProviders } from "../test-utils";
|
|||||||
describe("DashboardPage", () => {
|
describe("DashboardPage", () => {
|
||||||
test("渲染欢迎信息", () => {
|
test("渲染欢迎信息", () => {
|
||||||
window.fetch = (async () => {
|
window.fetch = (async () => {
|
||||||
return new Response(JSON.stringify({ ok: true, service: "test-app", timestamp: new Date().toISOString() }), {
|
return new Response(
|
||||||
headers: { "Content-Type": "application/json" },
|
JSON.stringify({ ok: true, service: "test-app", timestamp: new Date().toISOString(), version: "0.1.0" }),
|
||||||
status: 200,
|
{
|
||||||
});
|
headers: { "Content-Type": "application/json" },
|
||||||
|
status: 200,
|
||||||
|
},
|
||||||
|
);
|
||||||
}) as unknown as typeof fetch;
|
}) as unknown as typeof fetch;
|
||||||
|
|
||||||
renderWithProviders(createElement(DashboardPage));
|
renderWithProviders(createElement(DashboardPage));
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
proxy: {
|
proxy: {
|
||||||
"/api": "http://127.0.0.1:3000",
|
"/api": "http://127.0.0.1:3000",
|
||||||
"/health": "http://127.0.0.1:3000",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user