1
0

refactor: 规范审查与重组,合并细粒度规范,清理过时内容

- 合并 20+ 细粒度 spec 为粗粒度主题规范:dashboard、data-store、probe-engine、probe-api、probe-config 等
- 删除完全冗余规范:data-retention(被 probe-engine+data-store 覆盖)、backend-code-quality(DEVELOPMENT.md 已记录)
- 补充 http-checker 规范至完整标准(配置+执行+expect+校验+observation),匹配代码 440 行实现
- 清理 tcp/udp/llm checker 规范中已废弃 defaults 配置段的残留 Scenario
- 清理 checker-cohesion-structure 中的实现路径引用(src/server/...)
- 统一所有 spec 格式(## Purpose 开头,去除 # Capability/Title 形式)
- 更新 prompt-spec-review.md 审查提示文档
This commit is contained in:
2026-05-22 18:55:18 +08:00
parent cf847ccd7a
commit cfca03b4d6
40 changed files with 1368 additions and 2134 deletions

View File

@@ -1,59 +0,0 @@
## Purpose
定义后端 API 路由的组织规范:按端点拆分为独立 handler、共享响应工具集中管理、路径参数由 Bun routes 解析,静态资源服务由 HTML import manifest 管理。
## Requirements
### Requirement: 路由按职责拆分
系统 SHALL 将 HTTP 路由处理逻辑按 API 端点拆分为独立模块,每个模块导出 route handler 函数供 routes 对象统一注册。
#### Scenario: health 端点独立路由
- **WHEN** 客户端请求 `GET /health`
- **THEN** `routes/health.ts` 导出的 handler 负责处理,返回 HealthResponse JSON
#### Scenario: summary 端点独立路由
- **WHEN** 客户端请求 `GET /api/summary`
- **THEN** `routes/summary.ts` 导出的 handler 负责处理,委托 store 查询并返回 SummaryResponse JSON
#### Scenario: targets 端点独立路由
- **WHEN** 客户端请求 `GET /api/targets`
- **THEN** `routes/targets.ts` 导出的 handler 负责处理,委托 store 查询并返回 TargetStatus[] JSON
#### Scenario: history 端点独立路由
- **WHEN** 客户端请求 `GET /api/targets/:id/history?from=ISO&to=ISO`
- **THEN** `routes/history.ts` 导出的 handler 负责处理,通过 `req.params.id` 获取路径参数包含参数校验、store 查询和 HistoryResponse 返回
#### Scenario: trend 端点独立路由
- **WHEN** 客户端请求 `GET /api/targets/:id/trend?from=ISO&to=ISO`
- **THEN** `routes/trend.ts` 导出的 handler 负责处理,通过 `req.params.id` 获取路径参数包含参数校验、store 查询和 TrendPoint[] 返回
### Requirement: 共享辅助函数集中管理
系统 SHALL 将跨路由共享的响应格式化函数抽取到 helpers.ts 模块,单一职责、集中管理。
#### Scenario: createApiError 集中定义
- **WHEN** 任意路由需要返回 API 错误响应
- **THEN** 从 `helpers.ts` 导入 `createApiError` 函数,提供错误信息和状态码
#### Scenario: jsonResponse 集中定义
- **WHEN** 任意路由需要返回 JSON 响应
- **THEN** 从 `helpers.ts` 导入 `jsonResponse` 函数,处理 HEAD 方法、Content-Type 和安全头
#### Scenario: mapCheckResult 集中定义
- **WHEN** 需要将 StoredCheckResult 映射为 API CheckResult
- **THEN** 从 `helpers.ts` 导入 `mapCheckResult` 函数,处理 failure JSON 解析和格式转换
### Requirement: 静态资源服务独立管理
系统 SHALL 将 SPA fallback 逻辑交给 routes 对象中的 HTML import 通配符处理,静态资源服务由 Bun 内置 manifest 机制自动处理。
#### Scenario: 根路径返回 index.html
- **WHEN** 客户端请求 `/`
- **THEN** routes 中注册的 HTML import 自动返回 index.html
#### Scenario: 资源文件返回正确 Content-Type
- **WHEN** 客户端请求构建后的静态资源
- **THEN** Bun 内置 manifest 机制自动返回正确的 Content-Type 和缓存头
#### Scenario: SPA fallback
- **WHEN** 客户端请求非 API、非资源的路径`/dashboard`
- **THEN** routes 中注册的 `"/*"` HTML import 通配符返回 index.html 实现 SPA 的客户端路由

View File

@@ -1,136 +0,0 @@
## Purpose
定义后端代码中 es-toolkit 和 Bun 内置 API 的使用规范:类型判断、空值检测、深度比较、错误判断、并发控制、集合分组和 Web API 标准方法,替代手写实现落实库使用优先级规则。
## Requirements
### Requirement: 使用 es-toolkit 进行类型判断
系统 SHALL 使用 es-toolkit 的 `isPlainObject` 替代手写的对象类型判断函数,用于 expect 校验中区分纯值(原始值)和操作符对象。
#### Scenario: 识别纯对象为操作符
- **WHEN** body 校验规则中 expected 配置为 `{ equals: "value" }`(纯对象操作符)
- **THEN** `isPlainObject(expected)` SHALL 返回 true系统按操作符语义处理
#### Scenario: 排除非纯对象作为操作符
- **WHEN** body 校验规则中 expected 为原始值如 `"value"` 或数字 `200`
- **THEN** `isPlainObject(expected)` SHALL 返回 false系统按 equals 默认操作符处理
### Requirement: 使用 es-toolkit 进行空值检测
系统 SHALL 使用 es-toolkit 的 `isNil` 替代手写的 `actual === null || actual === undefined` 检测,用于 expect 中 `empty` 操作符的空值判断。
#### Scenario: null 值判定为空
- **WHEN** 校验值为 null
- **THEN** `isNil(null)` SHALL 返回 true
#### Scenario: undefined 值判定为空
- **WHEN** 校验值为 undefined
- **THEN** `isNil(undefined)` SHALL 返回 true
#### Scenario: 非空值判定为非空
- **WHEN** 校验值为 0、"false"、空数组 `[]` 等非 nil 值
- **THEN** `isNil(value)` SHALL 返回 false
### Requirement: 使用 es-toolkit 进行空对象检测
系统 SHALL 使用 es-toolkit 的 `isEmptyObject` 替代手写的 `typeof actual === "object" && Object.keys(actual).length === 0` 检测,用于 expect 中 `empty` 操作符的空对象判断。
#### Scenario: 空对象判定为空
- **WHEN** 校验值为 `{}`
- **THEN** `isEmptyObject({})` SHALL 返回 true
#### Scenario: 非空对象判定为非空
- **WHEN** 校验值为 `{ key: "val" }`
- **THEN** `isEmptyObject({ key: "val" })` SHALL 返回 false
#### Scenario: null 不是空对象
- **WHEN** 校验值为 null
- **THEN** `isEmptyObject(null)` SHALL 返回 false空值由 isNil 前置处理)
### Requirement: 使用 es-toolkit 进行深度相等比较
系统 SHALL 使用 es-toolkit 的 `isEqual` 替代 `!==` 浅比较,用于 expect 中 `equals` 操作符的值比较,支持对象和数组的深度比较。
#### Scenario: 原始值浅比较
- **WHEN** expected 和 actual 均为原始值字符串、数字、布尔值、null
- **THEN** `isEqual(actual, expected)` 的行为 SHALL 与 `actual === expected` 一致
#### Scenario: 对象深度比较
- **WHEN** expected 和 actual 均为对象(如从 JSONPath 提取的结构化数据)
- **THEN** `isEqual(actual, expected)` SHALL 递归比较所有属性值,而非引用比较
### Requirement: 使用 es-toolkit 进行错误类型判断
系统 SHALL 使用 es-toolkit 的 `isError` 替代 `error instanceof Error`,用于 HTTP runner 和 cmd runner 中的错误类型判断。
#### Scenario: Error 实例识别
- **WHEN** 错误对象为 `new Error("msg")`
- **THEN** `isError(error)` SHALL 返回 true
#### Scenario: Error 子类识别
- **WHEN** 错误对象为继承 Error 的自定义类型
- **THEN** `isError(error)` SHALL 返回 true
#### Scenario: 非 Error 对象识别
- **WHEN** 错误对象为字符串或普通对象
- **THEN** `isError(error)` SHALL 返回 false
### Requirement: 使用 es-toolkit Semaphore 实现并发控制
系统 SHALL 使用 es-toolkit 的 `Semaphore` 类替代手写的信号量实现(计数器 + Promise 队列),用于 ProbeEngine 中的组内并发拨测控制。
#### Scenario: 获取并发槽位
- **WHEN** 当前并发数未达上限
- **THEN** `semaphore.acquire()` SHALL 立即返回,不阻塞
#### Scenario: 等待并发槽位
- **WHEN** 当前并发数已达上限 maxConcurrentChecks
- **THEN** `semaphore.acquire()` SHALL 阻塞等待,直到其他任务调用 `semaphore.release()`
#### Scenario: 释放并发槽位
- **WHEN** 调用 `semaphore.release()`
- **THEN** 系统 SHALL 唤醒一个等待中的 acquire() 调用
### Requirement: 使用 es-toolkit groupBy 实现 target 分组
系统 SHALL 使用 es-toolkit 的 `groupBy` 函数替代手写的 Map 循环分组,用于 ProbeEngine 中按 interval 分组拨测目标。
#### Scenario: 按 interval 分组
- **WHEN** 输入包含不同 intervalMs 值的多个 target
- **THEN** `groupBy(targets, t => t.intervalMs)` SHALL 返回 key 为 intervalMs 值的分组对象,值为对应 target 数组
### Requirement: 使用原生 API 进行数组类型判断
系统 SHALL 使用原生 `Array.isArray()` 替代 `es-toolkit/compat``isArray`,用于 checker 模块中所有数组类型判断场景。
#### Scenario: 数组值判定为数组
- **WHEN** 校验值为数组(如 `[1, 2, 3]`
- **THEN** `Array.isArray(value)` SHALL 返回 true
#### Scenario: 非数组值判定为非数组
- **WHEN** 校验值为对象 `{}`、字符串 `"abc"`、数字 `123`、null 等
- **THEN** `Array.isArray(value)` SHALL 返回 false
### Requirement: 使用原生 API 进行对象类型判断
系统 SHALL 使用原生 `typeof x === 'object' && x !== null` 替代 `es-toolkit/compat``isObject`,用于 checker 模块中需要判断值为对象类型(排除 null的场景。
#### Scenario: 普通对象判定为对象
- **WHEN** 值为普通对象 `{ key: "val" }`
- **THEN** `typeof value === 'object' && value !== null` SHALL 返回 true
#### Scenario: 数组判定为对象
- **WHEN** 值为数组 `[1, 2, 3]`
- **THEN** `typeof value === 'object' && value !== null` SHALL 返回 true
#### Scenario: Headers 实例判定为对象
- **WHEN** 值为 `Headers` 实例
- **THEN** `typeof value === 'object' && value !== null` SHALL 返回 true
#### Scenario: null 判定为非对象
- **WHEN** 值为 null
- **THEN** `typeof value === 'object' && value !== null` SHALL 返回 false
#### Scenario: 原始值判定为非对象
- **WHEN** 值为字符串、数字、布尔值、undefined
- **THEN** `typeof value === 'object' && value !== null` SHALL 返回 false
### Requirement: 使用 Bun 内置 API 进行 Headers 转换
系统 SHALL 使用 `Object.fromEntries(headers)` 标准 Web API 替代手写的 `headersToRecord` 函数,用于将 Fetch API 的 Headers 对象转换为键值对。
#### Scenario: 转换响应头
- **WHEN** HTTP runner 获取到 response headers
- **THEN** `Object.fromEntries(response.headers)` SHALL 返回以 header 名称为 key、header 值为 value 的对象

View File

@@ -1,92 +0,0 @@
## Purpose
定义 ProbeStore 的批量查询方法getLatestChecksMap、getAllTargetStats以及 getSummary 和 createTargetsResponse 的 N+1 查询优化规范。同时约定单次查询操作使用 db.query() 利用内置缓存。
## Requirements
### Requirement: 批量查询最新检查结果
系统 SHALL 提供 `getLatestChecksMap` 方法,通过单次 SQL 查询获取所有 target 的最新一次 check 结果,返回 Map 结构供调用方按 target_id 索引。
#### Scenario: 获取所有目标的最新检查
- **WHEN** 调用 `getLatestChecksMap()`
- **THEN** 系统 SHALL 执行子查询找到每个 target_id 的 MAX(timestamp),再 JOIN 回 check_results 获取完整行,返回 `Map<number, StoredCheckResult | null>`
#### Scenario: 目标无历史记录
- **WHEN** 某 target 在 check_results 表中无任何记录
- **THEN** 该 target_id 在返回的 Map 中 SHALL 不存在对应的 key
### Requirement: 批量查询目标统计
系统 SHALL 提供 `getAllTargetStats` 方法,通过单次 SQL GROUP BY 聚合查询获取所有 target 的拨测统计totalChecks 和 availability。availability 计算精度 SHALL 与 `getTargetStats` 一致,统一使用 `Math.round(value * 100) / 100` 保留两位小数。
#### Scenario: 获取所有目标的聚合统计
- **WHEN** 调用 `getAllTargetStats()`
- **THEN** 系统 SHALL 执行 `SELECT target_id, COUNT(*), SUM(CASE WHEN matched=1 THEN 1 ELSE 0 END) FROM check_results GROUP BY target_id`,在内存中计算 availability 并返回 `Map<number, { totalChecks, availability }>`
#### Scenario: 目标无历史记录
- **WHEN** 某 target 在 check_results 表中无任何记录
- **THEN** 该 target_id 在返回的 Map 中 SHALL 不存在对应的 key
#### Scenario: availability 精度
- **WHEN** 计算 availabilityupCount / totalChecks * 100
- **THEN** 结果 SHALL 使用 `Math.round(value * 100) / 100` 四舍五入保留两位小数,与 `getTargetStats` 方法一致
### Requirement: summary 查询使用批量方法
`getSummary` 方法 SHALL 使用 `getLatestChecksMap` 一次性获取所有 target 的最新检查结果,而非对每个 target 逐条查询。
#### Scenario: 统计总览使用批量查询
- **WHEN** 调用 `store.getSummary()`
- **THEN** 系统 SHALL 调用 `getLatestChecksMap()` 一次获取所有最新结果,在内存中遍历统计 up/down 数量,而非循环 N 次调用 `getLatestCheck()`
### Requirement: targets 列表使用批量方法
`handleTargets`routes/targets.ts 中生成 TargetStatus[] 的逻辑SHALL 使用 `getLatestChecksMap``getAllTargetStats``getAllRecentSamples` 替代逐目标查询,消除 N+1 查询。
#### Scenario: 目标列表使用批量查询
- **WHEN** 处理 `GET /api/targets` 请求
- **THEN** 系统 SHALL 分别调用 `getLatestChecksMap()``getAllTargetStats()``getAllRecentSamples(30)` 批量获取数据,在内存中组装 TargetStatus 数组,而非对每个 target 逐条查询数据库
#### Scenario: 目标无采样数据
- **WHEN** 某 target 在 getAllRecentSamples 返回的 Map 中不存在
- **THEN** 该 target 的 recentSamples SHALL 为空数组
### Requirement: 批量查询所有目标的最近采样数据
系统 SHALL 提供 `getAllRecentSamples(limit: number)` 方法,通过单次 SQL 查询获取所有 target 的最近 N 条采样数据,返回 `Map<number, Array<{ timestamp: string; duration_ms: number | null; matched: number }>>` 结构。
#### Scenario: 获取所有目标的最近采样
- **WHEN** 调用 `getAllRecentSamples(30)`
- **THEN** 系统 SHALL 通过单次 SQL 查询获取每个 target 最近 30 条记录,返回按 target_id 索引的 Map
#### Scenario: 目标无历史记录
- **WHEN** 某 target 在 check_results 表中无任何记录
- **THEN** 该 target_id 在返回的 Map 中 SHALL 不存在对应的 key
#### Scenario: 采样数据排序
- **WHEN** 获取采样数据
- **THEN** 每个 target 的记录 SHALL 按 timestamp 降序排列(最新在前)
### Requirement: prepared statement 使用 query() 缓存
ProbeStore 中不涉及事务内复用的单次读/写操作 SHALL 使用 `this.db.query()` 而非 `this.db.prepare()`,利用 bun:sqlite 内置的 statement 缓存机制。
#### Scenario: insertCheckResult 使用 query
- **WHEN** 写入一条检查结果
- **THEN** `insertCheckResult` SHALL 使用 `this.db.query("INSERT INTO ...").run(...)` 而非 `this.db.prepare("INSERT INTO ...").run(...)`
#### Scenario: getHistory 查询使用 query
- **WHEN** 查询历史记录(包括 COUNT 和分页查询)
- **THEN** `getHistory` SHALL 使用 `this.db.query(...)` 而非 `this.db.prepare(...)`
#### Scenario: getTargetStats 查询使用 query
- **WHEN** 查询单目标统计
- **THEN** `getTargetStats` SHALL 使用 `this.db.query(...)` 而非 `this.db.prepare(...)`
#### Scenario: getTrend 查询使用 query
- **WHEN** 查询趋势数据
- **THEN** `getTrend` SHALL 使用 `this.db.query(...)` 而非 `this.db.prepare(...)`
#### Scenario: getRecentSamples 查询使用 query
- **WHEN** 查询采样数据
- **THEN** `getRecentSamples` SHALL 使用 `this.db.query(...)` 而非 `this.db.prepare(...)`
#### Scenario: syncTargets 事务保持 prepare例外
- **WHEN** 同步 targets 配置(事务内多次复用 insertStmt/updateStmt/deleteStmt
- **THEN** `syncTargets` 方法 SHALL 保持使用 `this.db.prepare()`,因需要在事务闭包内持有引用

View File

@@ -1,49 +0,0 @@
## Purpose
定义基于 Bun.serve `routes` 对象的全栈声明式路由注册、路径参数、HTTP method 声明和 fallback 行为。
## Requirements
### Requirement: 声明式路由注册
系统 SHALL 使用 Bun.serve 的 `routes` 对象以声明式方式注册 API 端点路由,非 API 请求由 `fetch` fallback 处理。
#### Scenario: API 端点路由注册
- **WHEN** server 启动时
- **THEN** 系统 SHALL 将所有 API 端点以 method handler 对象形式注册到 `routes` 对象
#### Scenario: 非 API 请求处理
- **WHEN** 请求路径不匹配任何 `routes` 中注册的路由
- **THEN** `fetch` fallback SHALL 将请求交给静态资源服务处理production或返回提示文本development
### Requirement: 路径参数支持
系统 SHALL 使用 routes 对象的 `:param` 语法声明路径参数,替代手动 regex 匹配。
#### Scenario: 带路径参数的 API 路由
- **WHEN** 客户端请求 `/api/targets/123/history`
- **THEN** 系统 SHALL 通过 `routes` 中注册的 `/api/targets/:id/history` 匹配,并通过 `req.params.id` 获取参数值 `"123"`
#### Scenario: 路径参数类型
- **WHEN** route handler 接收到路径参数
- **THEN** 参数值 SHALL 为字符串类型handler 负责进行类型转换和校验
### Requirement: HTTP Method 声明
系统 SHALL 在 routes 对象中为每个 API 端点以 per-method handler 形式声明支持的 HTTP method未匹配 method 的 API 请求 SHALL 落入 `/api/*` 通配符并返回 JSON 404。
#### Scenario: 单 method 端点
- **WHEN** API 端点只支持 GET 方法
- **THEN** 该端点 SHALL 以 `{ GET(req) { ... } }` 形式注册
#### Scenario: 不支持的 method 请求
- **WHEN** 客户端使用未声明的 method 请求 API 端点
- **THEN** `/api/*` 通配符 SHALL 返回 JSON 格式的 404 错误响应
### Requirement: Fetch Fallback 处理
系统 SHALL 使用 `fetch` handler 作为非 API 请求的入口,负责静态资源服务和 SPA fallback。
#### Scenario: Production 模式 fetch fallback
- **WHEN** production 模式下请求未匹配 routes 中的 API 路由
- **THEN** `fetch` handler SHALL 调用 `serveStaticAsset` 返回对应静态资源或 SPA fallback
#### Scenario: Development 模式 fetch fallback
- **WHEN** development 模式下请求未匹配 routes 中的 API 路由
- **THEN** `fetch` handler SHALL 返回提示文本,引导开发者通过 Vite dev server 访问前端

View File

@@ -1,107 +1,25 @@
## Purpose
定义 checker 模块的内聚化组织结构,确保每个 checker 以独立目录形式存在包含其全部类型定义、schema 声明、语义校验、执行逻辑和断言逻辑。同时定义共享的 expect/ 和 schema/ 基础设施,以及严格的依赖方向约束。
定义 checker 模块的内聚化组织结构,确保每个 checker 以独立目录形式存在包含其全部类型定义、schema 声明、语义校验、执行逻辑和断言逻辑。同时定义严格的依赖方向约束、Checker 接口定义、CheckerRegistry 注册中心、配置契约片段、配置校验 issue、引擎调度和服务注册委托
## Requirements
### Requirement: Checker 目录内聚结构
每个 checker SHALL 以独立目录形式存在于 `src/server/checker/runner/<type>/`,目录内 SHALL 包含该 checker 的全部类型定义、schema 声明、语义校验、执行逻辑和断言逻辑。
每个 checker SHALL 以独立目录形式存在于 checker runner 目录下,目录内 SHALL 包含该 checker 的全部类型定义、schema 声明、语义校验、执行逻辑和断言逻辑。
#### Scenario: HTTP checker 目录完整性
- **WHEN** 开发者查看 `src/server/checker/runner/http/` 目录
- **THEN** 该目录 SHALL 包含 `index.ts``types.ts``schema.ts``execute.ts``expect.ts``body.ts``validate.ts`
#### Scenario: Cmd checker 目录完整性
- **WHEN** 开发者查看 `src/server/checker/runner/cmd/` 目录
- **THEN** 该目录 SHALL 包含 `index.ts``types.ts``schema.ts``execute.ts``expect.ts``text.ts``validate.ts`
#### Scenario: checker 目录完整性
- **WHEN** 开发者查看某个 checker 目录
- **THEN** 该目录 SHALL 包含该 checker 的全部类型定义、schema 声明、语义校验、执行逻辑和断言逻辑
#### Scenario: 新增 checker 最小改动
- **WHEN** 开发者新增一个 checker 类型(如 dns
- **THEN** 开发者 SHALL 只需创建 `src/server/checker/runner/dns/` 目录及其内部文件,并在 `runner/index.ts` 注册列表中添加一行 import 和一行数组项
- **THEN** 开发者 SHALL 只需创建 checker 目录及其内部文件,并在注册列表中添加一行 import 和一行数组项
### Requirement: Checker 目录文件职责
每个 checker 目录内的文件 SHALL 遵循统一的职责划分
### Requirement: 断言基础设施
系统 SHALL 提供所有 checker 共享的断言基础设施,使用 Raw/Resolved expectation 术语和 value/content/keyed/status/headers 模块边界
#### Scenario: index.ts 仅做 re-export
- **WHEN** 开发者查看某 checker 的 `index.ts`
- **THEN** 该文件 SHALL 仅包含对 `execute.ts` 中 Checker 类的 re-export不包含任何逻辑
#### Scenario: types.ts 包含该 checker 全部专属类型
- **WHEN** 开发者需要该 checker 的配置类型、resolved 类型或 expect 类型
- **THEN** 这些类型 SHALL 全部定义在该 checker 目录的 `types.ts` 中,不在顶层 `types.ts`
#### Scenario: schema.ts 包含 TypeBox schema 定义
- **WHEN** 开发者需要该 checker 的 config 或 expect schema
- **THEN** 这些 schema SHALL 定义在该 checker 目录的 `schema.ts`
#### Scenario: execute.ts 包含 Checker 类实现
- **WHEN** 开发者需要查看该 checker 的执行逻辑
- **THEN** Checker 类(实现 CheckerDefinition 接口SHALL 定义在 `execute.ts`
#### Scenario: validate.ts 包含该 checker 全部语义校验
- **WHEN** 开发者需要查看该 checker 的配置校验逻辑
- **THEN** 该 checker 专属的语义校验函数 SHALL 全部定义在 `validate.ts`
#### Scenario: expect.ts 包含该 checker 专属断言
- **WHEN** 开发者需要查看该 checker 的断言逻辑
- **THEN** 该 checker 专属的断言函数 SHALL 定义在 `expect.ts`
### Requirement: 断言基础设施目录
系统 SHALL 在 `src/server/checker/expect/` 目录中提供所有 checker 共享的断言基础设施。共享 expect 目录 SHALL 使用 Raw/Resolved expectation 术语和 value/content/keyed/status/headers 模块边界。
#### Scenario: expect 共享类型位置
- **WHEN** 任何 checker 需要使用断言相关的共享类型(如 `ExpectationResult``ValueExpectation``ContentExpectations``KeyedExpectations`
- **THEN** 这些类型 SHALL 从 `src/server/checker/expect/types.ts` 导入
#### Scenario: value 断言引擎位置
- **WHEN** 任何 checker 需要使用 `applyValueMatcher``evaluateJsonPath``resolveValueExpectation``checkValueExpectation`
- **THEN** 这些函数 SHALL 从 `src/server/checker/expect/value.ts` 导入
#### Scenario: content 和 keyed 断言位置
- **WHEN** 任何 checker 需要执行内容数组或键值表 expectation
- **THEN** SHALL 分别从 `src/server/checker/expect/content.ts``src/server/checker/expect/keyed.ts` 导入共享函数
#### Scenario: failure 构造器位置
- **WHEN** 任何 checker 需要使用 `errorFailure``mismatchFailure`
- **THEN** 这些函数 SHALL 从 `src/server/checker/expect/failure.ts` 导入
#### Scenario: expectation 校验位置
- **WHEN** 任何 checker 的 validate 需要校验 Raw value、Raw content 或 Raw keyed expectation
- **THEN** 对应函数 SHALL 从 `src/server/checker/expect/validate.ts` 导入
#### Scenario: ExpectationResult 类型位置
- **WHEN** 任何 checker 需要使用 `ExpectationResult` 类型
- **THEN** 该类型 SHALL 从 `src/server/checker/expect/types.ts` 导入
### Requirement: Schema 目录结构
系统 SHALL 在 `src/server/checker/schema/` 目录中组织配置 schema 体系,替代原 `config-contract/` 目录。
#### Scenario: schema 目录包含 builder
- **WHEN** 系统需要从 registry 动态构建整体配置 schema
- **THEN** 该逻辑 SHALL 位于 `src/server/checker/schema/builder.ts`
#### Scenario: schema 目录包含 fragments
- **WHEN** checker 的 schema.ts 需要引用共享 schema 片段(如 durationSchema、sizeSchema
- **THEN** 这些片段 SHALL 从 `src/server/checker/schema/fragments.ts` 导入
#### Scenario: schema 目录包含 Ajv 校验入口
- **WHEN** config-loader 需要执行契约校验
- **THEN** 校验入口 SHALL 位于 `src/server/checker/schema/validate.ts`
#### Scenario: schema 目录包含 issue 工具
- **WHEN** 任何校验逻辑需要构造 ConfigValidationIssue
- **THEN** issue 类型和工具函数 SHALL 从 `src/server/checker/schema/issues.ts` 导入
### Requirement: 工具函数归集
系统 SHALL 在 `src/server/checker/utils.ts` 中提供纯工具函数。
#### Scenario: parseSize 位置
- **WHEN** 任何模块需要解析 size 字符串(如 "100MB"
- **THEN** `parseSize` SHALL 从 `src/server/checker/utils.ts` 导入
#### Scenario: parseDuration 位置
- **WHEN** 任何模块需要解析 duration 字符串(如 "30s"
- **THEN** `parseDuration` SHALL 从 `src/server/checker/utils.ts` 导入
### Requirement: Schema 体系
系统 SHALL 通过 schema 体系组织配置校验、契约片段和 issue 报告,支持从 registry 动态构建整体配置 schema、共享 schema 片段引用、Ajv 校验入口和 ConfigValidationIssue 构造。
### Requirement: 依赖方向约束
checker 系统内的模块依赖 SHALL 遵循严格的分层方向。
@@ -119,35 +37,248 @@ checker 系统内的模块依赖 SHALL 遵循严格的分层方向。
- **THEN** `schema/` 中的文件 SHALL 仅通过 `CheckerDefinition` 接口与 checker 交互SHALL NOT 直接导入具体 checker 目录
### Requirement: 显式注册列表
系统 SHALL 在 `src/server/checker/runner/index.ts` 中使用显式 import 列表注册所有 checker。
系统 SHALL 在 checker 注册入口文件中使用显式 import 列表注册所有 checker。
#### Scenario: 注册入口结构
- **WHEN** 开发者查看 `runner/index.ts`
- **WHEN** 开发者查看 checker 注册入口文件
- **THEN** 该文件 SHALL 包含所有 checker 的静态 import 和一个 checker 实例数组,通过循环调用 `registry.register()` 完成注册
#### Scenario: 新增 checker 注册
- **WHEN** 开发者新增一个 checker
- **THEN** 开发者 SHALL 在 `runner/index.ts` 中添加一行 import 和一行数组项,无需修改其他文件
- **THEN** 开发者 SHALL 在注册入口文件中添加一行 import 和一行数组项,无需修改其他文件
### Requirement: 公共类型文件瘦身
顶层 `src/server/checker/types.ts` SHALL 仅保留跨 checker 共享的 base 类型和存储相关类型expect 专属类型 SHALL 放在 `src/server/checker/expect/types.ts`
### Requirement: Checker 配置契约片段
系统 SHALL 支持 checker 提供自身 TypeBox 配置契约片段,用于描述该 checker 的 target 领域分组和 expect 分组。checker 契约 SHALL 区分 Authoring schema 与 Normalized schema。Authoring schema SHALL 描述用户 YAML 可书写形式,包括变量引用和 expect 简写Normalized schema SHALL 描述 `normalizeAuthoringConfig()` 输出形式,不接受变量引用、不接受 expect primitive 简写。公共配置加载模块 SHALL 通过 registry 获取已注册 checker 的契约片段,并按用途组合为运行时 Ajv 契约校验流程和外部 `probe-config.schema.json` 导出流程
#### Scenario: types.ts 不包含 checker 或 expect 专属类型
- **WHEN** 开发者查看顶层 `types.ts`
- **THEN** 该文件 SHALL NOT 包含 `HttpTargetConfig``ResolvedHttpTarget``RawCommandExpectConfig``ContentExpectation` 等 checker 专属或 expect 专属类型
#### Scenario: HTTP checker 提供契约片段
- **WHEN** HTTP checker 被注册
- **THEN** registry SHALL 能提供 HTTP target 和 HTTP expect 的 TypeBox 契约片段
#### Scenario: types.ts 保留 base 类型
- **WHEN** 开发者查看顶层 `types.ts`
- **THEN** 该文件 SHALL 包含 `ResolvedTargetBase``RawTargetConfig``DefaultsConfig``CheckResult``CheckFailure``StoredTarget``StoredCheckResult``JsonValue` 等公共类型
#### Scenario: Cmd checker 提供契约片段
- **WHEN** Cmd checker 被注册
- **THEN** registry SHALL 能提供 Cmd target 和 Cmd expect 的 TypeBox 契约片段
#### Scenario: ResolvedTargetBase 替代联合类型
- **WHEN** engine、store、config-loader 需要引用 resolved target 类型
- **THEN** 这些模块 SHALL 使用 `ResolvedTargetBase` interface不再使用硬编码联合类型
#### Scenario: 新 checker 只维护自身契约
- **WHEN** 开发者新增一个 checker 类型
- **THEN** 该 checker SHALL 提供自身 TypeBox 配置契约和语义 validator而不需要把 checker 专属字段写入中央手工校验逻辑
#### Scenario: DefaultsConfig 为宽松 base 形式
- **WHEN** 开发者查看顶层 `types.ts` 中的 `DefaultsConfig`
- **THEN** 该 interface SHALL 仅包含公共字段(`interval?``timeout?`)和 index signature`[checkerKey: string]: unknown`SHALL NOT 包含 `cmd?``http?` 等 checker 专属字段
#### Scenario: 外部 schema 通过 registry 生成
- **WHEN** 系统生成 `probe-config.schema.json`
- **THEN** 生成流程 SHALL 从 registry 获取已注册 checker 的 Authoring 契约片段,并将其组合进完整配置 schema
#### Scenario: 各 checker validate 自行 narrow defaults
- **WHEN** checker 的 `validate()` 方法需要访问自身的 defaults 配置
- **THEN** checker SHALL 从 `DefaultsConfig` 中通过 `defaults[configKey]` 获取并自行 narrow 为具体类型
#### Scenario: 运行时 schema 通过 registry 生成
- **WHEN** config-loader 执行运行时 AJV 契约校验
- **THEN** 校验流程 SHALL 从 registry 获取已注册 checker 的 Normalized 契约片段,并将其组合进完整配置 schema
#### Scenario: 契约组装不依赖全局 singleton
- **WHEN** 测试或 schema 生成流程需要组装配置契约
- **THEN** 系统 SHALL 支持传入 fresh CheckerRegistry 实例完成契约组装,避免重复注册或全局状态污染
### Requirement: Checker 启动期语义校验
系统 SHALL 支持 checker 提供启动期语义 validator用于校验 TypeBox/Ajv 契约不适合表达或需要 checker 业务知识判断的配置规则。语义 validator MUST 在 resolver 填充最终 ResolvedTarget 之前执行,并 MUST 返回 `ConfigValidationIssue[]`
#### Scenario: checker 语义校验先于 resolve
- **WHEN** config-loader 准备解析一个 target
- **THEN** 系统 SHALL 先完成该 target 的 checker 语义校验,再调用 checker.resolve()
#### Scenario: 语义校验失败阻止启动
- **WHEN** checker 语义 validator 发现非法配置
- **THEN** 系统 SHALL 以配置错误退出,不进入 checker 执行阶段
### Requirement: 结构化配置校验 issue
系统 SHALL 使用统一 `ConfigValidationIssue` 表示配置校验问题,至少包含 `code``path``message`,并支持可选 `targetName`。契约校验和 checker 语义校验都 SHALL 产出该结构,由配置加载模块统一渲染为中文错误。
#### Scenario: Ajv 错误转换为 issue
- **WHEN** Ajv 校验发现 required、type 或 additionalProperties 错误
- **THEN** 系统 SHALL 将该错误转换为 `ConfigValidationIssue`,保留配置路径和可读 message
#### Scenario: checker validator 返回 issue
- **WHEN** checker 语义 validator 发现非法 XPath 或正则表达式
- **THEN** checker SHALL 返回 `ConfigValidationIssue`,而不是直接抛出最终用户错误字符串
### Requirement: Checker 接口定义
系统 SHALL 定义面向扩展的泛型 `CheckerDefinition<TResolved extends ResolvedTargetBase = ResolvedTargetBase>`,包含 `type``configKey`、Authoring/Normalized TypeBox 配置契约、启动期语义校验、`resolve``execute``serialize``buildDetail` 成员。泛型参数 SHALL 约束 `execute``serialize` 方法的 target 参数类型,使 checker 实现内部获得编译期类型安全。默认泛型参数 `= ResolvedTargetBase` 保证中间层registry、engine、config-loader无需指定泛型。
#### Scenario: Checker 接口包含必要方法
- **WHEN** 开发者实现一个新的 Checker
- **THEN** 该实现 MUST 提供 `type`(字符串标识)、`configKey`配置分组名、Authoring/Normalized TypeBox 配置契约、启动期语义校验、`resolve(target, context)`(解析配置并填充默认值)、`execute(target, ctx)`(执行探测返回 CheckResult`serialize(target)`(返回 target 展示文本和 config JSON`buildDetail(observation)`(从 observation 构造人可读摘要)
#### Scenario: CheckerContext 注入 signal
- **WHEN** 引擎调用 `checker.execute(target, ctx)`
- **THEN** `ctx.signal` SHALL 是一个由引擎创建的 `AbortSignal`,在超时或引擎关闭时 abort
#### Scenario: resolve 不承担通用契约校验
- **WHEN** config-loader 调用 checker.resolve()
- **THEN** checker.resolve() SHALL 假定配置已经通过 Normalized TypeBox/Ajv 契约校验和启动期语义校验,只负责默认值填充、路径解析和领域配置转换
#### Scenario: resolve 接收 Normalized target
- **WHEN** config-loader 调用 checker.resolve()
- **THEN** 传入的 target SHALL 已通过 Normalized schema 和语义校验且不包含变量引用、Authoring expect primitive 简写或 Raw content/keyed DSL
#### Scenario: type 与 configKey 默认一致
- **WHEN** checker 定义 `type: "tcp"`
- **THEN** checker 的 `configKey` SHALL 默认使用 `"tcp"`,对应 target 的 `tcp` 分组
#### Scenario: 接口方法使用泛型约束
- **WHEN** 开发者查看 `CheckerDefinition<TResolved>` 接口签名
- **THEN** `resolve` 的返回值 SHALL 为 `TResolved``execute` 的参数 SHALL 为 `TResolved``serialize` 的参数 SHALL 为 `TResolved`
#### Scenario: checker 实现无需手动断言
- **WHEN** HttpChecker 实现 `CheckerDefinition<ResolvedHttpTarget>`
- **THEN** `execute` 方法的 target 参数类型 SHALL 直接为 `ResolvedHttpTarget`,无需在方法内部使用 `as` 类型断言
#### Scenario: registry 使用默认泛型参数
- **WHEN** CheckerRegistry 存储和返回 checker 实例
- **THEN** registry 内部 SHALL 使用 `CheckerDefinition`(等价于 `CheckerDefinition<ResolvedTargetBase>`),实现类型擦除
#### Scenario: buildDetail 方法签名
- **WHEN** 开发者实现 buildDetail 方法
- **THEN** 方法签名 SHALL 为 `buildDetail(observation: Record<string, unknown>): string | null`,接收 observation 对象并返回人可读摘要字符串或 null
#### Scenario: buildDetail 由 API 层调用
- **WHEN** API 序列化 CheckResult
- **THEN** API 层 SHALL 通过 registry 获取对应 checker 并调用 buildDetail而非由 execute 方法直接生成 detail
### Requirement: CheckerRegistry 注册中心
系统 SHALL 提供 `CheckerRegistry` 类,支持 `register(checker)``get(type)``supportedTypes`。重复注册同一 type SHALL 抛出错误。registry 内部 SHALL 存储 `CheckerDefinition`(使用默认泛型参数),对外提供类型擦除后的接口。
#### Scenario: 注册并获取 Checker
- **WHEN** 调用 `registry.register(new HttpChecker())` 后再调用 `registry.get("http")`
- **THEN** 返回的 SHALL 是之前注册的 HttpChecker 实例(类型为 `CheckerDefinition`
#### Scenario: 获取未注册的 type
- **WHEN** 调用 `registry.get("unknown")` 且未注册对应 type 的 checker
- **THEN** 系统 SHALL 抛出错误,提示不支持的 probe type
#### Scenario: 重复注册
- **WHEN** 同一 type 值被重复 `register()`
- **THEN** 系统 SHALL 抛出错误,提示该 type 已注册
#### Scenario: 查询支持的 type 列表
- **WHEN** 注册了 "http" 和 "cmd" 两个 checker 后查询 `registry.supportedTypes`
- **THEN** 返回的数组 SHALL 包含 `["http", "cmd"]`(按注册顺序)
### Requirement: 引擎通过 registry 调度 checker
系统 SHALL 在引擎执行检查时通过 `checkerRegistry.get(target.type).execute(target, ctx)` 调度检查,替代原有的 `switch/case` 分支。
#### Scenario: 引擎使用 registry 调度
- **WHEN** engine 需要执行一个 type 为 "http" 的 target
- **THEN** engine SHALL 从 `checkerRegistry` 中获取对应 checker 并调用其 `execute()` 方法,不再使用 `switch/case`
#### Scenario: 引擎注入超时 signal
- **WHEN** engine 调度一次 checker 执行
- **THEN** engine SHALL 创建 `AbortController`,设置超时定时器,将 `controller.signal` 注入 `CheckerContext`,执行完成后清理定时器
### Requirement: 配置解析通过 registry 委托 checker
系统 SHALL 在配置加载流程中通过 `checkerRegistry` 发现已注册 checker组合公共 TypeBox 契约与 checker 契约,并将 checker 专属语义校验和解析委托给对应 checker。公共配置校验 SHALL 仅保留公共语义校验name 非空、name 不重复、group 类型、type 已注册等)和契约调度职责,不包含 checker 专属字段校验。
#### Scenario: 配置契约通过 registry 组合
- **WHEN** config-loader 校验配置文件
- **THEN** config-loader SHALL 从 `checkerRegistry` 获取已注册 checker 的契约片段,并用于校验 defaults 与 targets 中对应 checker 的配置形状
#### Scenario: Authoring 契约通过 registry 组合
- **WHEN** 系统导出用户配置 JSON Schema
- **THEN** 配置 builder SHALL 从 `checkerRegistry` 获取已注册 checker 的 Authoring 契约片段
#### Scenario: Normalized 契约通过 registry 组合
- **WHEN** config-loader 校验 normalized 配置对象
- **THEN** config-loader SHALL 从 `checkerRegistry` 获取已注册 checker 的 Normalized 契约片段
#### Scenario: 配置解析委托 checker
- **WHEN** config-loader 解析一个 type 为 "cmd" 的 target
- **THEN** config-loader SHALL 调用 `checkerRegistry.get("cmd")` 获取对应 checker并委托该 checker 执行语义校验和 resolve
#### Scenario: 通用字段校验保留在 config-loader
- **WHEN** YAML 配置中某个 target 缺少 name 或 type 字段
- **THEN** config-loader 的公共校验流程 SHALL 仍负责校验这些通用字段
#### Scenario: type 专属校验下沉到 checker
- **WHEN** YAML 配置中 HTTP target 缺少 `http.url`
- **THEN** HTTP checker 的契约或语义校验 SHALL 抛出校验错误,提示缺少必填字段
#### Scenario: HTTP method 非法校验
- **WHEN** YAML 配置中 HTTP target 的 `http.method` 不是大写合法方法枚举值
- **THEN** HTTP checker 契约或语义校验 SHALL 抛出校验错误,提示 method 不合法
#### Scenario: URL 格式校验
- **WHEN** YAML 配置中 HTTP target 的 `http.url` 不以 `http://``https://` 开头
- **THEN** HttpChecker 的语义校验 SHALL 抛出校验错误,提示 URL 格式不合法
### Requirement: 存储序列化通过 registry 获取展示格式
系统 SHALL 在存储同步 targets 时通过 `checkerRegistry.get(t.type).serialize(t)` 获取每个 target 的展示摘要和配置 JSON替代函数中的类型分支。系统 SHALL 将 `targets.expect` 持久化为 null不依赖 Raw expect 或 Resolved expect。
#### Scenario: 序列化委托 checker
- **WHEN** store 同步 targets 表
- **THEN** store SHALL 对每个 target 调用对应 checker 的 `serialize()` 方法获取 `{ target, config }`
#### Scenario: expect 持久化不依赖 rawExpect
- **WHEN** store 同步带 expect 的 target 到 targets 表
- **THEN** store SHALL 将 `targets.expect` 写入 NULLMUST NOT 依赖 `rawExpect` 或 Raw expect 快照
### Requirement: Checker resolve 只接收已去糖配置
每个 checker 的 `resolve()` SHALL 接收已通过 Normalized schema 和语义校验的配置不再包含变量引用、Authoring expect primitive 简写或 Raw content/keyed DSL。`config-loader` SHALL 继续通过 registry 委托 checker resolveMUST NOT 在中间层理解 checker 专属 expect 字段。
#### Scenario: resolve 不再展开 Raw expect
- **WHEN** config-loader 解析一个带 `expect.durationMs: {equals: 1000}` 的 target
- **THEN** 对应 checker 的 resolved target SHALL 直接使用 Normalized expect 中的 `{equals: 1000}`resolve 只负责默认值和运行期配置转换
#### Scenario: 中间层不感知 checker expect 字段
- **WHEN** 新增 checker 定义自己的 Raw/Resolved expect 字段
- **THEN** config-loader SHALL 只调用该 checker 的 `validate()``resolve()`,不新增 checker 类型分支
### Requirement: 共享 expect 断言函数
系统 SHALL 提供可被多个 checker 复用的 expect 基础设施。共享基础设施 SHALL 包含 value expectation、content expectations、keyed expectations、status code 断言、headers keyed 断言、failure 构造和 ReDoS 校验。checker 专用的状态类断言 SHALL 保留在对应 checker 目录,或在多个 checker 复用时移动到共享模块。仅被单个 checker 使用且不属于通用 value/content/keyed/status/header 模型的断言模块 SHALL 位于该 checker 目录内。
#### Scenario: 共享 ValueExpectation 断言
- **WHEN** 任何 checker 需要对数字、字符串、布尔或 JSON value 执行 matcher 匹配
- **THEN** SHALL 调用共享 value expectation 工具执行 `equals``contains``regex``exists``empty``gt``gte``lt``lte` 语义
#### Scenario: 共享 ContentExpectations 断言
- **WHEN** HTTP body、LLM output、Cmd stdout/stderr、UDP response 或 TCP banner 需要执行返回内容校验
- **THEN** SHALL 调用共享 content expectations 工具,而不是在 checker 目录内复制 contains/regex/json/css/xpath 逻辑
#### Scenario: 共享 KeyedExpectations 断言
- **WHEN** HTTP 或 LLM checker 需要校验响应 headers或 DB checker 需要校验 rows 中的列值
- **THEN** SHALL 调用共享 keyed expectations 工具,并按调用方规则决定 key 是否大小写敏感
#### Scenario: 共享 headers 断言
- **WHEN** HTTP 或 LLM checker 需要校验响应 headers
- **THEN** SHALL 调用共享 header expectation 包装函数,确保 header key 大小写不敏感
#### Scenario: 共享 regex ReDoS 校验
- **WHEN** 任一 matcher 或 content expectation 配置 `regex`
- **THEN** SHALL 调用共享 ReDoS 校验工具在启动期拒绝危险正则
#### Scenario: 共享 failure 构造
- **WHEN** 任何 checker 需要构造 CheckFailure 对象
- **THEN** SHALL 调用共享的 `errorFailure()``mismatchFailure()` 构造 CheckFailure并保留 actual 截断策略
#### Scenario: 共享 status 断言
- **WHEN** HTTP 或 LLM checker 需要校验响应状态码
- **THEN** SHALL 复用共享 status code 断言函数,支持精确状态码和 `1xx``5xx` 范围模式
### Requirement: 超时控制由引擎注入 signal
Checker 实现的 `execute()` MUST 使用 `ctx.signal` 感知超时,不得自行创建 `AbortController``setTimeout` 用于超时控制。Cmd checker 和 ping checker 可在 signal abort 时 `proc.kill()` 以确保子进程被终止。
#### Scenario: HTTP checker 使用 signal
- **WHEN** HttpChecker 执行 HTTP 请求
- **THEN** SHALL 将 `ctx.signal` 传入 `fetch()``signal` 选项,不自行创建 `AbortController`
#### Scenario: Cmd checker 响应 signal
- **WHEN** CommandChecker 执行命令且 signal 被 abort
- **THEN** SHALL 调用 `proc.kill()` 终止子进程,并在 CheckResult 中记录超时错误
#### Scenario: Ping checker 响应 signal
- **WHEN** IcmpChecker 执行 ping 命令且 signal 被 abort
- **THEN** SHALL 调用 `proc.kill()` 终止 ping 子进程,并在 CheckResult 中记录超时错误
### Requirement: CheckFailure.phase 使用 string 类型
`CheckFailure.phase` 的类型 SHALL 定义为 `string`,替代原有的硬编码联合类型 `"status" | "duration" | "headers" | "body" | "exitCode" | "stdout" | "stderr"`
#### Scenario: phase 支持 checker 专用值
- **WHEN** cmd checker 在执行失败spawn error时生成 failure
- **THEN** `failure.phase` SHALL 可以是 `"spawn"` 等任意字符串值,类型系统 SHALL 不报错
#### Scenario: 前端展示 phase 不依赖硬编码类型
- **WHEN** 前端收到任意 phase 字符串值
- **THEN** 前端 SHALL 直接展示而不做类型判断

View File

@@ -48,14 +48,14 @@ UDP checker 的 observation SHALL 包含 respondedboolean、durationMsn
- **WHEN** UDP 发送数据后超时未收到响应
- **THEN** observation SHALL 包含 responded=false
### Requirement: Ping Checker Observation
Ping/ICMP checker 的 observation SHALL 包含 aliveboolean、transmittednumber、receivednumber、packetLossnumber、avgLatencyMsnumber | null、maxLatencyMsnumber | null、minLatencyMsnumber | null、errorstring | nullAPI registry type SHALL 仍为 `ping`
### Requirement: ICMP Checker Observation
ICMP checker 的 observation SHALL 包含 aliveboolean、transmittednumber、receivednumber、packetLossnumber、avgLatencyMsnumber | null、maxLatencyMsnumber | null、minLatencyMsnumber | null、errorstring | null
#### Scenario: Ping 正常返回统计
#### Scenario: ICMP 正常返回统计
- **WHEN** ping 命令成功执行并解析出统计数据
- **THEN** observation SHALL 包含完整的 PingStats 字段
- **THEN** observation SHALL 包含完整的 ICMPStats 字段
#### Scenario: Ping 命令失败
#### Scenario: ICMP 命令失败
- **WHEN** ping 命令未找到或超时
- **THEN** observation SHALL 为 null 或包含 error 字段
@@ -155,4 +155,4 @@ CheckResult SHALL 包含 `detail: string | null` 字段(替代原 statusDetail
#### Scenario: 各 checker 的 detail 格式
- **WHEN** 各 checker 的 buildDetail 被调用
- **THEN** HTTP SHALL 返回 `"HTTP {statusCode}"` 格式TCP SHALL 返回连接状态和 banner 摘要UDP SHALL 返回响应状态和大小摘要Ping SHALL 返回存活状态、平均延迟和丢包率摘要DB SHALL 返回连接状态或行数摘要CMD SHALL 返回 `"exitCode={N}"` 格式LLM SHALL 返回 provider、mode、状态码、finish 原因、输出长度和 token 用量摘要
- **THEN** HTTP SHALL 返回 `"HTTP {statusCode}"` 格式TCP SHALL 返回连接状态和 banner 摘要UDP SHALL 返回响应状态和大小摘要Ping SHALL 返回存活状态、平均延迟和丢包率摘要;ICMP SHALL 返回存活状态、平均延迟和丢包率摘要;DB SHALL 返回连接状态或行数摘要CMD SHALL 返回 `"exitCode={N}"` 格式LLM SHALL 返回 provider、mode、状态码、finish 原因、输出长度和 token 用量摘要

View File

@@ -1,241 +0,0 @@
## Purpose
定义 Checker 接口规范、注册机制、CheckerContext 上下文注入,以及共享 expect 断言函数的职责边界。此 capability 是 checker 系统的架构基础,不定义任何具体 checker 类型的业务行为。
## Requirements
### Requirement: Checker 配置契约片段
系统 SHALL 支持 checker 提供自身 TypeBox 配置契约片段,用于描述该 checker 的 target 领域分组和 expect 分组。checker 契约 SHALL 区分 Authoring schema 与 Normalized schema。Authoring schema SHALL 描述用户 YAML 可书写形式,包括变量引用和 expect 简写Normalized schema SHALL 描述 `normalizeAuthoringConfig()` 输出形式,不接受变量引用、不接受 expect primitive 简写。公共配置加载模块 SHALL 通过 registry 获取已注册 checker 的契约片段,并按用途组合为运行时 Ajv 契约校验流程和外部 `probe-config.schema.json` 导出流程。
#### Scenario: HTTP checker 提供契约片段
- **WHEN** HTTP checker 被注册
- **THEN** registry SHALL 能提供 HTTP target 和 HTTP expect 的 TypeBox 契约片段
#### Scenario: Cmd checker 提供契约片段
- **WHEN** Cmd checker 被注册
- **THEN** registry SHALL 能提供 Cmd target 和 Cmd expect 的 TypeBox 契约片段
#### Scenario: 新 checker 只维护自身契约
- **WHEN** 开发者新增一个 checker 类型
- **THEN** 该 checker SHALL 提供自身 TypeBox 配置契约和语义 validator而不需要把 checker 专属字段写入中央手工校验逻辑
#### Scenario: 外部 schema 通过 registry 生成
- **WHEN** 系统生成 `probe-config.schema.json`
- **THEN** 生成流程 SHALL 从 registry 获取已注册 checker 的 Authoring 契约片段,并将其组合进完整配置 schema
#### Scenario: 运行时 schema 通过 registry 生成
- **WHEN** config-loader 执行运行时 AJV 契约校验
- **THEN** 校验流程 SHALL 从 registry 获取已注册 checker 的 Normalized 契约片段,并将其组合进完整配置 schema
#### Scenario: 契约组装不依赖全局 singleton
- **WHEN** 测试或 schema 生成流程需要组装配置契约
- **THEN** 系统 SHALL 支持传入 fresh CheckerRegistry 实例完成契约组装,避免重复注册或全局状态污染
### Requirement: Checker 启动期语义校验
系统 SHALL 支持 checker 提供启动期语义 validator用于校验 TypeBox/Ajv 契约不适合表达或需要 checker 业务知识判断的配置规则。语义 validator MUST 在 resolver 填充最终 ResolvedTarget 之前执行,并 MUST 返回 `ConfigValidationIssue[]`
#### Scenario: checker 语义校验先于 resolve
- **WHEN** config-loader 准备解析一个 target
- **THEN** 系统 SHALL 先完成该 target 的 checker 语义校验,再调用 checker.resolve()
#### Scenario: 语义校验失败阻止启动
- **WHEN** checker 语义 validator 发现非法配置
- **THEN** 系统 SHALL 以配置错误退出,不进入 checker 执行阶段
### Requirement: 结构化配置校验 issue
系统 SHALL 使用统一 `ConfigValidationIssue` 表示配置校验问题,至少包含 `code``path``message`,并支持可选 `targetName`。契约校验和 checker 语义校验都 SHALL 产出该结构,由配置加载模块统一渲染为中文错误。
#### Scenario: Ajv 错误转换为 issue
- **WHEN** Ajv 校验发现 required、type 或 additionalProperties 错误
- **THEN** 系统 SHALL 将该错误转换为 `ConfigValidationIssue`,保留配置路径和可读 message
#### Scenario: checker validator 返回 issue
- **WHEN** checker 语义 validator 发现非法 XPath 或正则表达式
- **THEN** checker SHALL 返回 `ConfigValidationIssue`,而不是直接抛出最终用户错误字符串
### Requirement: Checker 接口定义
系统 SHALL 在 `src/server/checker/runner/types.ts` 中定义面向扩展的泛型 `CheckerDefinition<TResolved extends ResolvedTargetBase = ResolvedTargetBase>`,包含 `type``configKey`、Authoring/Normalized TypeBox 配置契约、启动期语义校验、`resolve``execute``serialize``buildDetail` 成员。泛型参数 SHALL 约束 `execute``serialize` 方法的 target 参数类型,使 checker 实现内部获得编译期类型安全。默认泛型参数 `= ResolvedTargetBase` 保证中间层registry、engine、config-loader无需指定泛型。
#### Scenario: Checker 接口包含必要方法
- **WHEN** 开发者实现一个新的 Checker
- **THEN** 该实现 MUST 提供 `type`(字符串标识)、`configKey`配置分组名、Authoring/Normalized TypeBox 配置契约、启动期语义校验、`resolve(target, context)`(解析配置并填充默认值)、`execute(target, ctx)`(执行探测返回 CheckResult`serialize(target)`(返回 target 展示文本和 config JSON`buildDetail(observation)`(从 observation 构造人可读摘要)
#### Scenario: CheckerContext 注入 signal
- **WHEN** 引擎调用 `checker.execute(target, ctx)`
- **THEN** `ctx.signal` SHALL 是一个由引擎创建的 `AbortSignal`,在超时或引擎关闭时 abort
#### Scenario: resolve 不承担通用契约校验
- **WHEN** config-loader 调用 checker.resolve()
- **THEN** checker.resolve() SHALL 假定配置已经通过 Normalized TypeBox/Ajv 契约校验和启动期语义校验,只负责默认值填充、路径解析和领域配置转换
#### Scenario: resolve 接收 Normalized target
- **WHEN** config-loader 调用 checker.resolve()
- **THEN** 传入的 target SHALL 已通过 Normalized schema 和语义校验且不包含变量引用、Authoring expect primitive 简写或 Raw content/keyed DSL
#### Scenario: type 与 configKey 默认一致
- **WHEN** checker 定义 `type: "tcp"`
- **THEN** checker 的 `configKey` SHALL 默认使用 `"tcp"`,对应 target 的 `tcp` 分组和 defaults.tcp 分组
#### Scenario: 接口方法使用泛型约束
- **WHEN** 开发者查看 `CheckerDefinition<TResolved>` 接口签名
- **THEN** `resolve` 的返回值 SHALL 为 `TResolved``execute` 的参数 SHALL 为 `TResolved``serialize` 的参数 SHALL 为 `TResolved`
#### Scenario: checker 实现无需手动断言
- **WHEN** HttpChecker 实现 `CheckerDefinition<ResolvedHttpTarget>`
- **THEN** `execute` 方法的 target 参数类型 SHALL 直接为 `ResolvedHttpTarget`,无需在方法内部使用 `as` 类型断言
#### Scenario: registry 使用默认泛型参数
- **WHEN** CheckerRegistry 存储和返回 checker 实例
- **THEN** registry 内部 SHALL 使用 `CheckerDefinition`(等价于 `CheckerDefinition<ResolvedTargetBase>`),实现类型擦除
#### Scenario: buildDetail 方法签名
- **WHEN** 开发者实现 buildDetail 方法
- **THEN** 方法签名 SHALL 为 `buildDetail(observation: Record<string, unknown>): string | null`,接收 observation 对象并返回人可读摘要字符串或 null
#### Scenario: buildDetail 由 API 层调用
- **WHEN** API 序列化 CheckResult
- **THEN** API 层 SHALL 通过 registry 获取对应 checker 并调用 buildDetail而非由 execute 方法直接生成 detail
### Requirement: CheckerRegistry 注册中心
系统 SHALL 在 `src/server/checker/runner/registry.ts` 中提供 `CheckerRegistry` 类,支持 `register(checker)``get(type)``supportedTypes`。重复注册同一 type SHALL 抛出错误。registry 内部 SHALL 存储 `CheckerDefinition`(使用默认泛型参数),对外提供类型擦除后的接口。
#### Scenario: 注册并获取 Checker
- **WHEN** 调用 `registry.register(new HttpChecker())` 后再调用 `registry.get("http")`
- **THEN** 返回的 SHALL 是之前注册的 HttpChecker 实例(类型为 `CheckerDefinition`
#### Scenario: 获取未注册的 type
- **WHEN** 调用 `registry.get("unknown")` 且未注册对应 type 的 checker
- **THEN** 系统 SHALL 抛出错误,提示不支持的 probe type
#### Scenario: 重复注册
- **WHEN** 同一 type 值被重复 `register()`
- **THEN** 系统 SHALL 抛出错误,提示该 type 已注册
#### Scenario: 查询支持的 type 列表
- **WHEN** 注册了 "http" 和 "cmd" 两个 checker 后查询 `registry.supportedTypes`
- **THEN** 返回的数组 SHALL 包含 `["http", "cmd"]`(按注册顺序)
### Requirement: 引擎通过 registry 调度 checker
系统 SHALL 在 `ProbeEngine.runCheck()` 中通过 `checkerRegistry.get(target.type).execute(target, ctx)` 调度检查,替代原有的 `switch/case` 分支。
#### Scenario: 引擎使用 registry 调度
- **WHEN** engine 需要执行一个 type 为 "http" 的 target
- **THEN** engine SHALL 从 `checkerRegistry` 中获取对应 checker 并调用其 `execute()` 方法,不再使用 `switch/case`
#### Scenario: 引擎注入超时 signal
- **WHEN** engine 调度一次 checker 执行
- **THEN** engine SHALL 创建 `AbortController`,设置超时定时器,将 `controller.signal` 注入 `CheckerContext`,执行完成后清理定时器
### Requirement: 配置解析通过 registry 委托 checker
系统 SHALL 在 `config-loader.ts` 的配置加载流程中通过 `checkerRegistry` 发现已注册 checker组合公共 TypeBox 契约与 checker 契约,并将 checker 专属语义校验和解析委托给对应 checker。`validateConfig()` SHALL 仅保留公共语义校验name 非空、name 不重复、group 类型、type 已注册等)和契约调度职责,不包含 checker 专属字段校验。
#### Scenario: 配置契约通过 registry 组合
- **WHEN** config-loader 校验配置文件
- **THEN** config-loader SHALL 从 `checkerRegistry` 获取已注册 checker 的契约片段,并用于校验 defaults 与 targets 中对应 checker 的配置形状
#### Scenario: Authoring 契约通过 registry 组合
- **WHEN** 系统导出用户配置 JSON Schema
- **THEN** 配置 builder SHALL 从 `checkerRegistry` 获取已注册 checker 的 Authoring 契约片段
#### Scenario: Normalized 契约通过 registry 组合
- **WHEN** config-loader 校验 normalized 配置对象
- **THEN** config-loader SHALL 从 `checkerRegistry` 获取已注册 checker 的 Normalized 契约片段
#### Scenario: 配置解析委托 checker
- **WHEN** config-loader 解析一个 type 为 "cmd" 的 target
- **THEN** config-loader SHALL 调用 `checkerRegistry.get("cmd")` 获取对应 checker并委托该 checker 执行语义校验和 resolve
#### Scenario: 通用字段校验保留在 config-loader
- **WHEN** YAML 配置中某个 target 缺少 name 或 type 字段
- **THEN** config-loader 的公共校验流程 SHALL 仍负责校验这些通用字段
#### Scenario: type 专属校验下沉到 checker
- **WHEN** YAML 配置中 HTTP target 缺少 `http.url`
- **THEN** HTTP checker 的契约或语义校验 SHALL 抛出校验错误,提示缺少必填字段
#### Scenario: HTTP method 非法校验
- **WHEN** YAML 配置中 HTTP target 的 `http.method` 不是大写合法方法枚举值
- **THEN** HTTP checker 契约或语义校验 SHALL 抛出校验错误,提示 method 不合法
#### Scenario: URL 格式校验
- **WHEN** YAML 配置中 HTTP target 的 `http.url` 不以 `http://``https://` 开头
- **THEN** HttpChecker 的语义校验 SHALL 抛出校验错误,提示 URL 格式不合法
### Requirement: 存储序列化通过 registry 获取展示格式
系统 SHALL 在 `ProbeStore.syncTargets()` 中通过 `checkerRegistry.get(t.type).serialize(t)` 获取每个 target 的展示摘要(`target` 列)和配置 JSON`config` 列),替代 `buildTargetDisplay()` / `buildTargetConfig()` 中的类型分支。系统 SHALL 将 `targets.expect` 持久化为 null不依赖 Raw expect 或 Resolved expect。
#### Scenario: 序列化委托 checker
- **WHEN** store 同步 targets 表
- **THEN** store SHALL 对每个 target 调用对应 checker 的 `serialize()` 方法获取 `{ target, config }`
#### Scenario: expect 持久化不依赖 rawExpect
- **WHEN** store 同步带 expect 的 target 到 targets 表
- **THEN** store SHALL 将 `targets.expect` 写入 NULLMUST NOT 依赖 `rawExpect` 或 Raw expect 快照
### Requirement: Checker resolve 只接收已去糖配置
每个 checker 的 `resolve()` SHALL 接收已通过 Normalized schema 和语义校验的配置不再包含变量引用、Authoring expect primitive 简写或 Raw content/keyed DSL。`config-loader` SHALL 继续通过 registry 委托 checker resolveMUST NOT 在中间层理解 checker 专属 expect 字段。
#### Scenario: resolve 不再展开 Raw expect
- **WHEN** config-loader 解析一个带 `expect.durationMs: {equals: 1000}` 的 target
- **THEN** 对应 checker 的 resolved target SHALL 直接使用 Normalized expect 中的 `{equals: 1000}`resolve 只负责默认值和运行期配置转换
#### Scenario: 中间层不感知 checker expect 字段
- **WHEN** 新增 checker 定义自己的 Raw/Resolved expect 字段
- **THEN** config-loader SHALL 只调用该 checker 的 `validate()``resolve()`,不新增 checker 类型分支
### Requirement: 共享 expect 断言函数
系统 SHALL 在 `src/server/checker/expect/` 中提供可被多个 checker 复用的 expect 基础设施。共享基础设施 SHALL 包含 value expectation、content expectations、keyed expectations、status code 断言、headers keyed 断言、failure 构造和 ReDoS 校验。checker 专用的状态类断言 SHALL 保留在对应 checker 目录,或在多个 checker 复用时移动到共享模块。仅被单个 checker 使用且不属于通用 value/content/keyed/status/header 模型的断言模块 SHALL 位于该 checker 目录内。
#### Scenario: 共享 ValueExpectation 断言
- **WHEN** 任何 checker 需要对数字、字符串、布尔或 JSON value 执行 matcher 匹配
- **THEN** SHALL 调用共享 value expectation 工具执行 `equals``contains``regex``exists``empty``gt``gte``lt``lte` 语义
#### Scenario: 共享 ContentExpectations 断言
- **WHEN** HTTP body、LLM output、Cmd stdout/stderr、UDP response 或 TCP banner 需要执行返回内容校验
- **THEN** SHALL 调用共享 content expectations 工具,而不是在 checker 目录内复制 contains/regex/json/css/xpath 逻辑
#### Scenario: 共享 KeyedExpectations 断言
- **WHEN** HTTP 或 LLM checker 需要校验响应 headers或 DB checker 需要校验 rows 中的列值
- **THEN** SHALL 调用共享 keyed expectations 工具,并按调用方规则决定 key 是否大小写敏感
#### Scenario: 共享 headers 断言
- **WHEN** HTTP 或 LLM checker 需要校验响应 headers
- **THEN** SHALL 调用共享 header expectation 包装函数,确保 header key 大小写不敏感
#### Scenario: 共享 regex ReDoS 校验
- **WHEN** 任一 matcher 或 content expectation 配置 `regex`
- **THEN** SHALL 调用共享 ReDoS 校验工具在启动期拒绝危险正则
#### Scenario: 共享 failure 构造
- **WHEN** 任何 checker 需要构造 CheckFailure 对象
- **THEN** SHALL 调用 `expect/failure.ts` 中的 `errorFailure()``mismatchFailure()`,并保留 actual 截断策略
#### Scenario: 共享 status 断言
- **WHEN** HTTP 或 LLM checker 需要校验响应状态码
- **THEN** SHALL 复用共享 status code 断言函数,支持精确状态码和 `1xx``5xx` 范围模式
### Requirement: 超时控制由引擎注入 signal
Checker 实现的 `execute()` MUST 使用 `ctx.signal` 感知超时,不得自行创建 `AbortController``setTimeout` 用于超时控制。Cmd checker 和 ping checker 可在 signal abort 时 `proc.kill()` 以确保子进程被终止。
#### Scenario: HTTP checker 使用 signal
- **WHEN** HttpChecker 执行 HTTP 请求
- **THEN** SHALL 将 `ctx.signal` 传入 `fetch()``signal` 选项,不自行创建 `AbortController`
#### Scenario: Cmd checker 响应 signal
- **WHEN** CommandChecker 执行命令且 signal 被 abort
- **THEN** SHALL 调用 `proc.kill()` 终止子进程,并在 CheckResult 中记录超时错误
#### Scenario: Ping checker 响应 signal
- **WHEN** IcmpChecker 执行 ping 命令且 signal 被 abort
- **THEN** SHALL 调用 `proc.kill()` 终止 ping 子进程,并在 CheckResult 中记录超时错误
### Requirement: CheckFailure.phase 使用 string 类型
`shared/api.ts``CheckFailure.phase` 的类型 SHALL 定义为 `string`,替代原有的硬编码联合类型 `"status" | "duration" | "headers" | "body" | "exitCode" | "stdout" | "stderr"`
#### Scenario: phase 支持 checker 专用值
- **WHEN** cmd checker 在执行失败spawn error时生成 failure
- **THEN** `failure.phase` SHALL 可以是 `"spawn"` 等任意字符串值,类型系统 SHALL 不报错
#### Scenario: 前端展示 phase 不依赖硬编码类型
- **WHEN** 前端收到任意 phase 字符串值
- **THEN** 前端 SHALL 直接展示而不做类型判断

View File

@@ -1,6 +1,6 @@
## Purpose
定义项目代码质量门禁、格式化检查、快速检查和完整验证命令的行为要求,确保开发者可以通过文档化命令稳定验证源码质量、基础测试和生产构建。
定义项目代码质量门禁、格式化检查、Git hooks 自动化质量门禁、提交信息格式校验、快速检查和完整验证命令的行为要求,确保开发者可以通过文档化命令稳定验证源码质量、基础测试和生产构建。
## Requirements
@@ -55,6 +55,52 @@
- **WHEN** 不同开发者在不同操作系统上运行格式化(通过 ESLint 或独立 Prettier
- **THEN** 由于所有格式化参数均显式定义,产物 SHALL 完全一致
### Requirement: pre-commit 自动质量检查
项目 SHALL 通过 husky 和 lint-staged 在 git commit 前自动对变更文件运行 ESLint含 Prettier 格式)检查。由于 eslint-plugin-prettier 已集成格式检查lint-staged SHALL 仅运行 `eslint --fix` 即可同时修复代码质量和格式问题。
#### Scenario: 变更 TypeScript 文件后提交
- **WHEN** 开发者 stage 了 `.ts``.tsx` 文件并执行 `git commit`
- **THEN** lint-staged SHALL 自动对变更文件运行 `eslint --fix`(含格式修复),修复后继续提交
#### Scenario: 变更 Markdown 或 JSON 文件后提交
- **WHEN** 开发者 stage 了 `.md``.json``.yaml``.yml` 文件并执行 `git commit`
- **THEN** lint-staged SHALL 自动对变更文件运行 `prettier --write`
#### Scenario: lint 检查失败阻止提交
- **WHEN** 变更文件存在无法自动修复的 ESLint 错误(含格式错误)
- **THEN** pre-commit hook MUST 以非零状态退出,阻止提交
#### Scenario: 无变更文件提交
- **WHEN** 开发者执行 `git commit` 但无 stage 文件
- **THEN** lint-staged SHALL 正常通过,不阻止提交
### Requirement: 提交信息格式校验
项目 SHALL 通过 commitlint 在 git commit 时校验提交信息必须符合 "类型: 简短描述" 格式,类型限定为 feat/fix/refactor/docs/style/test/chore。
#### Scenario: 有效的中文提交信息
- **WHEN** 开发者提交信息为 "feat: 新增导入排序功能"
- **THEN** commit-msg hook SHALL 通过校验
#### Scenario: 缺少类型前缀的提交信息
- **WHEN** 开发者提交信息为 "新增导入排序功能"(无 "feat:" 前缀)
- **THEN** commit-msg hook MUST 以非零状态退出,提示正确格式
#### Scenario: 无效的提交类型
- **WHEN** 开发者提交信息使用不在允许列表中的类型(如 "update: 修改配置"
- **THEN** commit-msg hook MUST 以非零状态退出,提示可用类型
### Requirement: husky 初始化自动化
项目 SHALL 通过 `prepare` 生命周期脚本在 `bun install` 时自动初始化 husky。
#### Scenario: 首次安装依赖
- **WHEN** 开发者运行 `bun install`
- **THEN** husky SHALL 自动初始化,安装 pre-commit 和 commit-msg hooks
#### Scenario: 已有 husky 配置时安装
- **WHEN** 开发者运行 `bun install` 且 husky 已初始化
- **THEN** husky 初始化 SHALL 跳过,不覆盖已有配置
### Requirement: TypeScript 未使用变量检测
项目 SHALL 启用 TypeScript `noUnusedLocals` 编译选项,将未使用的局部变量检测为编译错误。
@@ -115,7 +161,7 @@
- **THEN** 系统 SHALL 执行 `tests/` 目录下所有 `*.test.ts``*.test.tsx` 文件
### Requirement: 完整验证命令
项目 SHALL 提供完整 `verify` 命令,用于提交前或发布前验证当前源码、测试和生产构建。原 executable smoke test 暂时移除,后续通过独立变更重新设计。
项目 SHALL 提供完整 `verify` 命令,用于提交前或发布前验证当前源码、测试和生产构建。
#### Scenario: 运行完整验证
- **WHEN** 开发者运行 `bun run verify`

View File

@@ -1,51 +0,0 @@
## Purpose
定义 Git hooks 自动化质量门禁行为,在 pre-commit 阶段自动运行代码检查和格式化,在 commit-msg 阶段校验提交信息格式。
## Requirements
### Requirement: pre-commit 自动质量检查
项目 SHALL 通过 husky 和 lint-staged 在 git commit 前自动对变更文件运行 ESLint含 Prettier 格式)检查。由于 eslint-plugin-prettier 已集成格式检查lint-staged SHALL 仅运行 `eslint --fix` 即可同时修复代码质量和格式问题。
#### Scenario: 变更 TypeScript 文件后提交
- **WHEN** 开发者 stage 了 `.ts``.tsx` 文件并执行 `git commit`
- **THEN** lint-staged SHALL 自动对变更文件运行 `eslint --fix`(含格式修复),修复后继续提交
#### Scenario: 变更 Markdown 或 JSON 文件后提交
- **WHEN** 开发者 stage 了 `.md``.json``.yaml``.yml` 文件并执行 `git commit`
- **THEN** lint-staged SHALL 自动对变更文件运行 `prettier --write`
#### Scenario: lint 检查失败阻止提交
- **WHEN** 变更文件存在无法自动修复的 ESLint 错误(含格式错误)
- **THEN** pre-commit hook MUST 以非零状态退出,阻止提交
#### Scenario: 无变更文件提交
- **WHEN** 开发者执行 `git commit` 但无 stage 文件
- **THEN** lint-staged SHALL 正常通过,不阻止提交
### Requirement: 提交信息格式校验
项目 SHALL 通过 commitlint 在 git commit 时校验提交信息必须符合 "类型: 简短描述" 格式,类型限定为 feat/fix/refactor/docs/style/test/chore。
#### Scenario: 有效的中文提交信息
- **WHEN** 开发者提交信息为 "feat: 新增导入排序功能"
- **THEN** commit-msg hook SHALL 通过校验
#### Scenario: 缺少类型前缀的提交信息
- **WHEN** 开发者提交信息为 "新增导入排序功能"(无 "feat:" 前缀)
- **THEN** commit-msg hook MUST 以非零状态退出,提示正确格式
#### Scenario: 无效的提交类型
- **WHEN** 开发者提交信息使用不在允许列表中的类型(如 "update: 修改配置"
- **THEN** commit-msg hook MUST 以非零状态退出,提示可用类型
### Requirement: husky 初始化自动化
项目 SHALL 通过 `prepare` 生命周期脚本在 `bun install` 时自动初始化 husky。
#### Scenario: 首次安装依赖
- **WHEN** 开发者运行 `bun install`
- **THEN** husky SHALL 自动初始化,安装 pre-commit 和 commit-msg hooks
#### Scenario: 已有 husky 配置时安装
- **WHEN** 开发者运行 `bun install` 且 husky 已初始化
- **THEN** husky 初始化 SHALL 跳过,不覆盖已有配置

View File

@@ -1,5 +1,3 @@
# Container Image Packaging
## Purpose
Provide Alpine-based multi-stage Docker container image packaging for DiAL, enabling containerized deployment with musl executables, minimal runtime dependencies, and documented build/run workflows.

View File

@@ -1,140 +0,0 @@
## Purpose
定义 styles.css 中集中管理的前端样式工具类和 CSS 自定义属性,供 TDesign 组件之外的自定义组件StatusDot、StatusBar 等)引用。
## Requirements
### Requirement: 状态色 CSS 类
styles.css SHALL 定义状态指示相关的 CSS 类,颜色使用 TDesign tokens。
#### Scenario: StatusDot 颜色类
- **WHEN** StatusDot 组件渲染
- **THEN** 组件 SHALL 使用 `.status-dot` 基础类 + `.status-dot--up`background: `--td-success-color`)或 `.status-dot--down`background: `--td-error-color`)修饰类,不使用内联 style
#### Scenario: StatusDot 发光阴影
- **WHEN** StatusDot 组件渲染
- **THEN** `.status-dot--up` SHALL 定义 `box-shadow` 使用 `--td-success-color``.status-dot--down` SHALL 定义 `box-shadow` 使用 `--td-error-color`
#### Scenario: StatusBar 色块类
- **WHEN** StatusBar 组件渲染色块
- **THEN** 组件 SHALL 使用 `.status-bar-block` 基础类 + `.status-bar-block--up`background: `--td-success-color`)、`.status-bar-block--down`background: `--td-error-color`)或 `.status-bar-block--empty`background: `--td-bg-color-component-disabled`)修饰类,不使用内联 style
### Requirement: 可用率色阶 CSS 变量
styles.css SHALL 定义 10 级可用率色阶 CSS 自定义属性,使用项目自定义色值。
#### Scenario: 色阶变量定义
- **WHEN** 可用率进度条渲染
- **THEN** 色阶 SHALL 通过 CSS 自定义属性 `--avail-0``--avail-9` 定义,值为项目自定义色值(`#d54941``#3dba60`
#### Scenario: 色阶渐变方向
- **WHEN** 色阶变量被引用
- **THEN** 色阶 SHALL 从红色0-30%经橙色30-60%过渡到绿色60-100%
### Requirement: 辅助工具类
styles.css SHALL 定义前端组件复用的工具类,包含页面布局相关类。
#### Scenario: 文本禁用色类
- **WHEN** 延迟列无数据需要显示占位符
- **THEN** 组件 SHALL 使用 `.text-disabled`color: `--td-text-color-disabled`
#### Scenario: 等宽数字类
- **WHEN** 数值需要等宽显示
- **THEN** 组件 SHALL 使用 `.tabular-nums`font-variant-numeric: tabular-nums
#### Scenario: 延迟色值类
- **WHEN** 延迟数值渲染
- **THEN** 组件 SHALL 使用 `.latency-ok``.latency-warn``.latency-error`
#### Scenario: 延迟值容器类
- **WHEN** 延迟数值需要固定宽度对齐
- **THEN** 组件 SHALL 使用 `.latency-value`display: inline-block; min-width: 7ch; white-space: nowrap
#### Scenario: 全宽布局类
- **WHEN** 组件需要占满父容器宽度
- **THEN** 组件 SHALL 使用 `.full-width`width: 100%
#### Scenario: 可点击表格类
- **WHEN** PrimaryTable 行支持点击交互
- **THEN** 表格 SHALL 使用 `.clickable-table`cursor: pointer
#### Scenario: Tab 面板内边距类
- **WHEN** Drawer 内 Tabs 面板需要内边距
- **THEN** TabPanel SHALL 使用 `className="tab-panel-padded"` prop 传入类名
#### Scenario: 内容区居中类
- **WHEN** Dashboard 内容区需要居中且限制最大宽度
- **THEN** 内容区 SHALL 使用 `.dashboard-content`max-width: 1400px; margin: 0 auto; padding: var(--td-comp-paddingTB-xl) var(--td-comp-paddingLR-xl)
#### Scenario: 页面背景色
- **WHEN** Dashboard 页面渲染
- **THEN** `.dashboard` 类 SHALL 设置 background: var(--td-bg-color-page)min-height: 100vhwidth: 100%
#### Scenario: 品牌标识类
- **WHEN** HeadMenu logo 区域渲染品牌名和副标题
- **THEN** 品牌 SHALL 使用 `.dashboard-brand`display: inline-flex; align-items: baseline; gap: var(--td-comp-margin-s)),品牌名 SHALL 使用 `.dashboard-logo`font-size: calc(var(--td-font-size-title-large) + 6px); font-weight: 700副标题 SHALL 使用 `.dashboard-subtitle`font-size: var(--td-font-size-body-medium); color: var(--td-text-color-secondary)
#### Scenario: Header 右侧操作区类
- **WHEN** HeadMenu operations 区域渲染主题模式选择器、刷新频率选择器和倒计时/按钮
- **THEN** 容器 SHALL 使用 `.dashboard-header-controls`display: inline-flex; align-items: center; gap: var(--td-comp-margin-s); margin-right: var(--td-comp-margin-xxl)
#### Scenario: Header 右侧操作区单行布局
- **WHEN** Header 右侧操作区渲染
- **THEN** `.dashboard-header-controls` SHALL 保持桌面单行水平布局,不为该区域新增窄屏换行或收纳规则
#### Scenario: 倒计时文本类
- **WHEN** 倒计时文本或刷新按钮渲染
- **THEN** 容器 SHALL 使用 `.dashboard-countdown`display: inline-flex; align-items: center; font-variant-numeric: tabular-nums; min-width: 5ch确保数字等宽且格式切换不抖动
#### Scenario: SummaryCard 居中类
- **WHEN** SummaryCards 内 Statistic 需要居中
- **THEN** Statistic 所在的 Col SHALL 使用 `.summary-stat-col`text-align: center
#### Scenario: SummaryCards 行间距类
- **WHEN** SummaryCards 容器需要与下方内容保持间距
- **THEN** 容器 SHALL 使用 `.summary-cards-row`margin-bottom: var(--td-comp-margin-xl)
#### Scenario: Drawer 时间控件单行类
- **WHEN** Drawer 时间选择器需要单行布局
- **THEN** 控件容器 SHALL 使用 `.drawer-time-controls`display: flex; align-items: center; gap: var(--td-comp-margin-m); width: 100%),日期选择器 SHALL 使用 `.drawer-date-range`flex: 1; min-width: 360px
#### Scenario: Drawer 时间控件响应式
- **WHEN** 视口宽度 ≤ 768px
- **THEN** `.drawer-time-controls` SHALL 启用 flex-wrap`.drawer-date-range` min-width 改为 100%
#### Scenario: 概览统计卡片类
- **WHEN** Drawer 概览统计区渲染
- **THEN** 统计卡片 SHALL 使用 `.overview-stat-card`background: var(--td-bg-color-container-hover)),并使用 TDesign Statistic 组件自带的上下布局title 在上、value 在下),通过 `.summary-stat-col`text-align: center实现内容居中。系统 SHALL NOT 使用已移除的 `.overview-stat-item``.overview-stat-value` 类。
### Requirement: NumberFlow 倒计时样式类
styles.css SHALL 定义 NumberFlow 倒计时相关样式类,供 Header 倒计时组件使用。样式 SHALL 继承 TDesign 文本颜色或使用 TDesign CSS tokens不得使用组件内联 `style`、硬编码色值、`!important` 或覆盖 TDesign 内部类名。
#### Scenario: 倒计时滚动容器类
- **WHEN** Header 自动刷新倒计时以 NumberFlow 形式渲染
- **THEN** 倒计时 SHALL 使用集中定义的滚动容器类,保持 inline-flex、baseline 对齐、nowrap 和 tabular-nums
#### Scenario: 倒计时数字类
- **WHEN** NumberFlow 数字渲染
- **THEN** 数字 SHALL 使用集中定义的数字类配置 line-height 和 NumberFlow mask CSS 变量,减少滚动边缘突兀感
#### Scenario: 倒计时单位类
- **WHEN** 分钟或秒单位文本渲染
- **THEN** 单位 SHALL 使用集中定义的单位类与数字保持基线对齐,并继承当前 TDesign 文本色
#### Scenario: 不使用内联样式
- **WHEN** RefreshCountdown 组件渲染 NumberFlow 倒计时
- **THEN** 组件 SHALL 通过 `className` 引用 styles.css 中的样式类,不得通过 React `style` prop 设置 NumberFlow 展示样式
### Requirement: 异常行背景类
styles.css SHALL 定义 DOWN 行的背景色和左侧竖线,使用安全选择器且不使用 `!important`
#### Scenario: DOWN 行背景色
- **WHEN** 表格行标记为 DOWN 状态
- **THEN** 行 SHALL 通过 `.t-table tr.row-down` 选择器获得浅红色背景
#### Scenario: DOWN 行左侧竖线
- **WHEN** 表格行标记为 DOWN 状态
- **THEN** 行 SHALL 通过 `.t-table tr.row-down` 选择器获得 `border-left: 3px solid var(--td-error-color)`
#### Scenario: DOWN 行 hover 状态
- **WHEN** 鼠标悬停在 DOWN 行上
- **THEN** 行背景 SHALL 通过 `.t-table--hoverable tbody tr.row-down:hover` 选择器显示 hover 状态色

View File

@@ -1,51 +0,0 @@
## Purpose
定义 Dashboard 页面骨架布局:顶部导航栏(含品牌标识、主题模式选择器、刷新频率选择器和倒计时控件)、内容区域居中与最大宽度、页面背景色。
## Requirements
### Requirement: 页面骨架布局
Dashboard SHALL 使用 TDesign Layout 组件体系构建页面骨架,包含顶部导航栏和内容区域。
#### Scenario: Layout 结构
- **WHEN** Dashboard 页面渲染
- **THEN** 页面 SHALL 使用 TDesign `Layout` 组件包裹 `Layout.Header``Layout.Content`
#### Scenario: 顶部导航栏
- **WHEN** Dashboard 页面渲染
- **THEN** `Layout.Header` SHALL 内嵌 TDesign `HeadMenu` 组件,`logo` prop 渲染品牌名 "DiAL" 和副标题 "统一拨测平台"(水平排列),`operations` prop 渲染主题模式选择器、刷新频率选择器和倒计时/刷新按钮组合控件
#### Scenario: Header 右侧操作区
- **WHEN** Dashboard 页面渲染
- **THEN** HeadMenu operations 区域 SHALL 包含主题模式 RadioGroup、刷新频率 RadioGroup 和倒计时文本(或手动刷新按钮),三者水平排列并垂直居中
#### Scenario: 主题选择器位置
- **WHEN** HeadMenu operations 区域渲染
- **THEN** 主题模式 RadioGroup SHALL 位于刷新频率 RadioGroup 前面
#### Scenario: Header 右侧操作区位置
- **WHEN** HeadMenu 渲染
- **THEN** operations 区域 SHALL 使用右侧 margin 向内收缩,避免紧贴浏览器右边缘
#### Scenario: 内容区域居中
- **WHEN** Dashboard 内容区渲染
- **THEN** `Layout.Content` 内部 SHALL 使用 CSS 类限制最大宽度max-width: 1400px并水平居中
#### Scenario: 页面背景色
- **WHEN** Dashboard 页面渲染
- **THEN** 页面背景色 SHALL 使用 `var(--td-bg-color-page)`,内容卡片浮于当前 TDesign 主题背景之上
### Requirement: Header 版本号展示
Dashboard SHALL 在顶部导航栏品牌区域展示当前运行实例的应用版本号,版本号 SHALL 使用 `/api/meta` 返回的 `version` 字段,并以 `v` 前缀显示。
#### Scenario: Meta 数据已加载
- **WHEN** Dashboard 成功获取 `/api/meta` 且返回 `version: "0.1.0"`
- **THEN** Header 品牌区域 SHALL 展示 `v0.1.0`
#### Scenario: Meta 数据尚未加载或请求失败
- **WHEN** Dashboard 尚未获取到有效 `version`
- **THEN** Header SHALL 保持可用并省略版本号占位,不影响品牌名、主题模式选择器、刷新频率选择器和倒计时/刷新按钮渲染
#### Scenario: 版本号视觉层级
- **WHEN** Header 展示版本号
- **THEN** 版本号 SHALL 使用次级文本样式弱展示,不得使用内联 style、硬编码色值、`!important` 或覆盖 TDesign 内部类名

View File

@@ -0,0 +1,327 @@
## Purpose
定义拨测系统前端 Dashboard 页面页面骨架布局、刷新频率控制与倒计时、CSS 工具类基础设施、总览统计卡片、Dashboard 数据查询、加载和错误状态处理。分组表格布局见 `target-table`,目标详情 Drawer 见 `target-detail-drawer`,数据轮询和缓存见 `tanstack-query-data-layer`
## Requirements
### Requirement: 页面骨架布局
Dashboard SHALL 使用 TDesign Layout 组件体系构建页面骨架,包含顶部导航栏和内容区域。
#### Scenario: Layout 结构
- **WHEN** Dashboard 页面渲染
- **THEN** 页面 SHALL 使用 TDesign `Layout` 组件包裹 `Layout.Header``Layout.Content`
#### Scenario: 顶部导航栏
- **WHEN** Dashboard 页面渲染
- **THEN** `Layout.Header` SHALL 内嵌 TDesign `HeadMenu` 组件,`logo` prop 渲染品牌名 "DiAL" 和副标题 "统一拨测平台"(水平排列),`operations` prop 渲染主题模式选择器、刷新频率选择器和倒计时/刷新按钮组合控件
#### Scenario: Header 右侧操作区
- **WHEN** Dashboard 页面渲染
- **THEN** HeadMenu operations 区域 SHALL 包含主题模式 RadioGroup、刷新频率 RadioGroup 和倒计时文本(或手动刷新按钮),三者水平排列并垂直居中
#### Scenario: 主题选择器位置
- **WHEN** HeadMenu operations 区域渲染
- **THEN** 主题模式 RadioGroup SHALL 位于刷新频率 RadioGroup 前面
#### Scenario: Header 右侧操作区位置
- **WHEN** HeadMenu 渲染
- **THEN** operations 区域 SHALL 使用右侧 margin 向内收缩,避免紧贴浏览器右边缘
#### Scenario: 内容区域居中
- **WHEN** Dashboard 内容区渲染
- **THEN** `Layout.Content` 内部 SHALL 使用 CSS 类限制最大宽度max-width: 1400px并水平居中
#### Scenario: 页面背景色
- **WHEN** Dashboard 页面渲染
- **THEN** 页面背景色 SHALL 使用 `var(--td-bg-color-page)`,内容卡片浮于当前 TDesign 主题背景之上
### Requirement: Header 版本号展示
Dashboard SHALL 在顶部导航栏品牌区域展示当前运行实例的应用版本号,版本号 SHALL 使用 `/api/meta` 返回的 `version` 字段,并以 `v` 前缀显示。
#### Scenario: Meta 数据已加载
- **WHEN** Dashboard 成功获取 `/api/meta` 且返回 `version: "0.1.0"`
- **THEN** Header 品牌区域 SHALL 展示 `v0.1.0`
#### Scenario: Meta 数据尚未加载或请求失败
- **WHEN** Dashboard 尚未获取到有效 `version`
- **THEN** Header SHALL 保持可用并省略版本号占位,不影响品牌名、主题模式选择器、刷新频率选择器和倒计时/刷新按钮渲染
#### Scenario: 版本号视觉层级
- **WHEN** Header 展示版本号
- **THEN** 版本号 SHALL 使用次级文本样式弱展示,不得使用内联 style、硬编码色值、`!important` 或覆盖 TDesign 内部类名
### Requirement: 刷新频率选择器
HeadMenu operations 区域 SHALL 提供 RadioGroup 组件供用户选择刷新频率。
#### Scenario: RadioGroup 渲染
- **WHEN** Dashboard 页面渲染
- **THEN** HeadMenu operations 区域 SHALL 显示 RadioGrouptheme="button", variant="default-filled"选项为手动、10秒、30秒、1分钟、5分钟
#### Scenario: 默认选中
- **WHEN** 页面首次加载
- **THEN** RadioGroup SHALL 默认选中"30秒"
#### Scenario: 切换频率立即刷新
- **WHEN** 用户切换刷新频率选项
- **THEN** 系统 SHALL 立即触发一次数据刷新,然后应用新的刷新间隔
### Requirement: 倒计时显示
RadioGroup 右侧 SHALL 显示距下次自动刷新的倒计时。倒计时逻辑 SHALL 封装在独立的 `RefreshCountdown` 组件中App 组件 SHALL NOT 持有每秒更新的 `now` state。自动倒计时数字 SHALL 使用 `@number-flow/react` 提供滚动过渡,非倒计时状态 SHALL 保持普通文本或按钮语义。
#### Scenario: RefreshCountdown 组件封装
- **WHEN** Dashboard 页面渲染
- **THEN** 倒计时显示 SHALL 由独立的 `RefreshCountdown` 组件负责,该组件内部持有 `now` state 和每秒 `setInterval`,渲染边界限制在该组件内部
#### Scenario: RefreshCountdown props
- **WHEN** RefreshCountdown 组件渲染
- **THEN** 组件 SHALL 接收 `dashboardUpdatedAt: number``refreshInterval: number``isFetching: boolean``isManualRefresh: boolean``onRefresh: () => void` 作为 props
#### Scenario: NumberFlow 数字滚动
- **WHEN** 自动刷新模式下已完成首次刷新且当前未处于刷新中状态
- **THEN** 倒计时数字 SHALL 使用 `@number-flow/react``NumberFlow` 渲染,并使用向下滚动趋势表达倒计时递减
#### Scenario: 秒级间隔格式
- **WHEN** 自动刷新间隔小于 60 秒
- **THEN** 倒计时 SHALL 显示为"xx秒"格式(如"26秒"
#### Scenario: 分钟级稳定格式
- **WHEN** 自动刷新间隔大于等于 60 秒
- **THEN** 倒计时 SHALL 显示为"x分xx秒"格式,秒数 SHALL 固定为两位(如"4分30秒"、"0分09秒"
#### Scenario: 时间数字边界
- **WHEN** 分钟级倒计时中的秒数在 59 到 00 边界变化
- **THEN** 秒数十位 SHALL 按时间显示规则限制在 0 到 5 之间滚动
#### Scenario: 无前缀
- **WHEN** 倒计时显示
- **THEN** 可见倒计时文本 SHALL 不包含任何前缀(如"下一次刷新:"),直接显示时间
#### Scenario: 可访问文本
- **WHEN** NumberFlow 倒计时渲染
- **THEN** 倒计时容器 SHALL 暴露与当前倒计时等价的可访问文本,供测试和辅助技术读取
#### Scenario: 刷新中状态
- **WHEN** 数据正在刷新isFetching=true 且 isLoading=false
- **THEN** 倒计时文本 SHALL 显示为"刷新中..."
#### Scenario: 等待首次刷新状态
- **WHEN** 自动刷新模式下尚未完成首次刷新
- **THEN** 倒计时文本 SHALL 显示为"等待首次刷新"
### Requirement: App 组件渲染隔离
App 组件 SHALL NOT 持有任何高频更新的 state如每秒更新的时钟确保 App 的重渲染频率与数据刷新频率一致(默认 30 秒一次)。
#### Scenario: App 无 now state
- **WHEN** App 组件渲染
- **THEN** App SHALL NOT 包含 `useState` 管理的时钟 state也 SHALL NOT 包含每秒触发的 `setInterval`
#### Scenario: App 重渲染频率
- **WHEN** Dashboard 处于自动刷新模式
- **THEN** App 组件的重渲染 SHALL 仅由 TanStack Query 的 refetch 触发(频率等于用户选择的刷新间隔),而非每秒触发
### Requirement: 手动刷新按钮
选择"手动"模式时,倒计时区域 SHALL 替换为刷新按钮。
#### Scenario: 手动模式显示按钮
- **WHEN** 用户选择"手动"刷新频率
- **THEN** 倒计时区域 SHALL 替换为刷新图标按钮
#### Scenario: 点击刷新
- **WHEN** 用户点击刷新按钮
- **THEN** 系统 SHALL 触发一次数据刷新
#### Scenario: 刷新中禁用
- **WHEN** 数据正在刷新
- **THEN** 刷新按钮 SHALL 显示 loading 状态且 disabled防止连续点击
### Requirement: 布局稳定性
倒计时/按钮容器 SHALL 保持布局稳定避免内容变化导致的抖动。NumberFlow 倒计时 SHALL 通过分组同步和等宽数字样式降低位数、单位和动画变化带来的布局偏移。
#### Scenario: 数字等宽
- **WHEN** 倒计时数字变化
- **THEN** 容器和 NumberFlow 倒计时 SHALL 使用 tabular-nums 字体特性,确保数字等宽不抖动
#### Scenario: NumberFlow 分组同步
- **WHEN** 分钟级倒计时同时渲染分钟和秒数
- **THEN** 分钟和秒数 SHALL 使用 `NumberFlowGroup` 同步布局变化
#### Scenario: 格式切换不抖动
- **WHEN** 倒计时在按钮、秒级文本和分钟级文本之间切换
- **THEN** 容器 SHALL 使用 min-width 确保最小宽度,避免 RadioGroup 位移
### Requirement: 状态色 CSS 类
styles.css SHALL 定义状态指示相关的 CSS 类,颜色使用 TDesign tokens。
#### Scenario: StatusDot 颜色类
- **WHEN** StatusDot 组件渲染
- **THEN** 组件 SHALL 使用 `.status-dot` 基础类 + `.status-dot--up`background: `--td-success-color`)或 `.status-dot--down`background: `--td-error-color`)修饰类,不使用内联 style
#### Scenario: StatusDot 发光阴影
- **WHEN** StatusDot 组件渲染
- **THEN** `.status-dot--up` SHALL 定义 `box-shadow` 使用 `--td-success-color``.status-dot--down` SHALL 定义 `box-shadow` 使用 `--td-error-color`
#### Scenario: StatusBar 色块类
- **WHEN** StatusBar 组件渲染色块
- **THEN** 组件 SHALL 使用 `.status-bar-block` 基础类 + `.status-bar-block--up`background: `--td-success-color`)、`.status-bar-block--down`background: `--td-error-color`)或 `.status-bar-block--empty`background: `--td-bg-color-component-disabled`)修饰类,不使用内联 style
### Requirement: 可用率色阶 CSS 变量
styles.css SHALL 定义 10 级可用率色阶 CSS 自定义属性,使用项目自定义色值。
#### Scenario: 色阶变量定义
- **WHEN** 可用率进度条渲染
- **THEN** 色阶 SHALL 通过 CSS 自定义属性 `--avail-0``--avail-9` 定义,值为项目自定义色值(`#d54941``#3dba60`
#### Scenario: 色阶渐变方向
- **WHEN** 色阶变量被引用
- **THEN** 色阶 SHALL 从红色0-30%经橙色30-60%过渡到绿色60-100%
### Requirement: 辅助工具类
styles.css SHALL 定义前端组件复用的工具类,包含页面布局相关类。
#### Scenario: 文本禁用色类
- **WHEN** 延迟列无数据需要显示占位符
- **THEN** 组件 SHALL 使用 `.text-disabled`color: `--td-text-color-disabled`
#### Scenario: 等宽数字类
- **WHEN** 数值需要等宽显示
- **THEN** 组件 SHALL 使用 `.tabular-nums`font-variant-numeric: tabular-nums
#### Scenario: 延迟色值类
- **WHEN** 延迟数值渲染
- **THEN** 组件 SHALL 使用 `.latency-ok``.latency-warn``.latency-error`
#### Scenario: 延迟值容器类
- **WHEN** 延迟数值需要固定宽度对齐
- **THEN** 组件 SHALL 使用 `.latency-value`display: inline-block; min-width: 7ch; white-space: nowrap
#### Scenario: 全宽布局类
- **WHEN** 组件需要占满父容器宽度
- **THEN** 组件 SHALL 使用 `.full-width`width: 100%
#### Scenario: 可点击表格类
- **WHEN** PrimaryTable 行支持点击交互
- **THEN** 表格 SHALL 使用 `.clickable-table`cursor: pointer
#### Scenario: Tab 面板内边距类
- **WHEN** Drawer 内 Tabs 面板需要内边距
- **THEN** TabPanel SHALL 使用 `className="tab-panel-padded"` prop 传入类名
#### Scenario: 内容区居中类
- **WHEN** Dashboard 内容区需要居中且限制最大宽度
- **THEN** 内容区 SHALL 使用 `.dashboard-content`max-width: 1400px; margin: 0 auto; padding: var(--td-comp-paddingTB-xl) var(--td-comp-paddingLR-xl)
#### Scenario: 页面背景色
- **WHEN** Dashboard 页面渲染
- **THEN** `.dashboard` 类 SHALL 设置 background: var(--td-bg-color-page)min-height: 100vhwidth: 100%
#### Scenario: 品牌标识类
- **WHEN** HeadMenu logo 区域渲染品牌名和副标题
- **THEN** 品牌 SHALL 使用 `.dashboard-brand`display: inline-flex; align-items: baseline; gap: var(--td-comp-margin-s)),品牌名 SHALL 使用 `.dashboard-logo`font-size: calc(var(--td-font-size-title-large) + 6px); font-weight: 700副标题 SHALL 使用 `.dashboard-subtitle`font-size: var(--td-font-size-body-medium); color: var(--td-text-color-secondary)
#### Scenario: Header 右侧操作区类
- **WHEN** HeadMenu operations 区域渲染主题模式选择器、刷新频率选择器和倒计时/按钮
- **THEN** 容器 SHALL 使用 `.dashboard-header-controls`display: inline-flex; align-items: center; gap: var(--td-comp-margin-s); margin-right: var(--td-comp-margin-xxl)
#### Scenario: Header 右侧操作区单行布局
- **WHEN** Header 右侧操作区渲染
- **THEN** `.dashboard-header-controls` SHALL 保持桌面单行水平布局,不为该区域新增窄屏换行或收纳规则
#### Scenario: 倒计时文本类
- **WHEN** 倒计时文本或刷新按钮渲染
- **THEN** 容器 SHALL 使用 `.dashboard-countdown`display: inline-flex; align-items: center; font-variant-numeric: tabular-nums; min-width: 5ch确保数字等宽且格式切换不抖动
#### Scenario: SummaryCard 居中类
- **WHEN** SummaryCards 内 Statistic 需要居中
- **THEN** Statistic 所在的 Col SHALL 使用 `.summary-stat-col`text-align: center
#### Scenario: SummaryCards 行间距类
- **WHEN** SummaryCards 容器需要与下方内容保持间距
- **THEN** 容器 SHALL 使用 `.summary-cards-row`margin-bottom: var(--td-comp-margin-xl)
#### Scenario: Drawer 时间控件单行类
- **WHEN** Drawer 时间选择器需要单行布局
- **THEN** 控件容器 SHALL 使用 `.drawer-time-controls`display: flex; align-items: center; gap: var(--td-comp-margin-m); width: 100%),日期选择器 SHALL 使用 `.drawer-date-range`flex: 1; min-width: 360px
#### Scenario: Drawer 时间控件响应式
- **WHEN** 视口宽度 ≤ 768px
- **THEN** `.drawer-time-controls` SHALL 启用 flex-wrap`.drawer-date-range` min-width 改为 100%
#### Scenario: 概览统计卡片类
- **WHEN** Drawer 概览统计区渲染
- **THEN** 统计卡片 SHALL 使用 `.overview-stat-card`background: var(--td-bg-color-container-hover)),并使用 TDesign Statistic 组件自带的上下布局title 在上、value 在下),通过 `.summary-stat-col`text-align: center实现内容居中。系统 SHALL NOT 使用已移除的 `.overview-stat-item``.overview-stat-value` 类。
### Requirement: NumberFlow 倒计时样式类
styles.css SHALL 定义 NumberFlow 倒计时相关样式类,供 Header 倒计时组件使用。样式 SHALL 继承 TDesign 文本颜色或使用 TDesign CSS tokens不得使用组件内联 `style`、硬编码色值、`!important` 或覆盖 TDesign 内部类名。
#### Scenario: 倒计时滚动容器类
- **WHEN** Header 自动刷新倒计时以 NumberFlow 形式渲染
- **THEN** 倒计时 SHALL 使用集中定义的滚动容器类,保持 inline-flex、baseline 对齐、nowrap 和 tabular-nums
#### Scenario: 倒计时数字类
- **WHEN** NumberFlow 数字渲染
- **THEN** 数字 SHALL 使用集中定义的数字类配置 line-height 和 NumberFlow mask CSS 变量,减少滚动边缘突兀感
#### Scenario: 倒计时单位类
- **WHEN** 分钟或秒单位文本渲染
- **THEN** 单位 SHALL 使用集中定义的单位类与数字保持基线对齐,并继承当前 TDesign 文本色
#### Scenario: 不使用内联样式
- **WHEN** RefreshCountdown 组件渲染 NumberFlow 倒计时
- **THEN** 组件 SHALL 通过 `className` 引用 styles.css 中的样式类,不得通过 React `style` prop 设置 NumberFlow 展示样式
### Requirement: 异常行背景类
styles.css SHALL 定义 DOWN 行的背景色和左侧竖线,使用安全选择器且不使用 `!important`
#### Scenario: DOWN 行背景色
- **WHEN** 表格行标记为 DOWN 状态
- **THEN** 行 SHALL 通过 `.t-table tr.row-down` 选择器获得浅红色背景
#### Scenario: DOWN 行左侧竖线
- **WHEN** 表格行标记为 DOWN 状态
- **THEN** 行 SHALL 通过 `.t-table tr.row-down` 选择器获得 `border-left: 3px solid var(--td-error-color)`
#### Scenario: DOWN 行 hover 状态
- **WHEN** 鼠标悬停在 DOWN 行上
- **THEN** 行背景 SHALL 通过 `.t-table--hoverable tbody tr.row-down:hover` 选择器显示 hover 状态色
### Requirement: Dashboard 数据查询
Dashboard SHALL 通过 `GET /api/dashboard` 获取首屏总览统计和目标列表数据。
#### Scenario: 查询 Dashboard 数据
- **WHEN** 页面处于打开状态
- **THEN** 前端 SHALL 使用 TanStack Query 请求 `GET /api/dashboard?window=24h&recentLimit=30`
#### Scenario: 统计数据自动刷新
- **WHEN** 页面处于打开状态
- **THEN** Dashboard 数据 SHALL 通过 TanStack Query 的 refetchInterval=8000 自动刷新
#### Scenario: 元信息独立查询
- **WHEN** 页面需要 checker 类型列表
- **THEN** 前端 SHALL 继续通过 `GET /api/meta` 独立查询 checkerTypes
### Requirement: 总览统计卡片
Dashboard SHALL 在页面顶部使用单个 TDesign Card 组件内嵌一行居中的 Statistic 展示总览统计,包含总目标数、正常数、异常数和窗口异常事件数。
#### Scenario: 展示统计卡片
- **WHEN** 用户打开 Dashboard 页面
- **THEN** 页面顶部 SHALL 使用单个 TDesign Card无 shadow、无 bordered内嵌 TDesign Row/Col 布局展示 4 个居中的 Statistic全部目标数color=blue、正常目标数color=green、异常目标数color=red、24h 异常事件数color=orange
#### Scenario: 指标居中显示
- **WHEN** SummaryCards 渲染
- **THEN** 每个 Statistic 所在的 Col SHALL 使用 `.summary-stat-col` 类实现标题和数字居中对齐
#### Scenario: 异常事件数据来源
- **WHEN** SummaryCards 渲染 24h 异常事件数
- **THEN** 该数值 SHALL 使用 DashboardResponse.summary.incidents 字段,标题 SHALL 基于当前 window 展示为"24h 异常事件数"
### Requirement: 页面加载与错误状态
Dashboard SHALL 使用 TDesign Skeleton 组件处理首次加载状态,使用 Alert 处理错误。
#### Scenario: 首次加载
- **WHEN** 页面首次加载且数据尚未返回
- **THEN** 页面 SHALL 使用 TDesign Skeleton 组件animation="gradient")展示页面骨架,模拟 Summary 区域和 Table 区域的大致结构
#### Scenario: API 请求失败
- **WHEN** 前端 API 请求失败
- **THEN** 页面 SHALL 使用 TDesign Alert 组件theme=error显示错误提示

View File

@@ -1,47 +0,0 @@
## Purpose
定义历史拨测数据的自动清理机制:可配置的保留时长和定时清理调度。
## Requirements
### Requirement: 数据保留配置
系统 SHALL 支持通过 `server.storage.retention` 配置项指定历史数据保留时长,格式为持续时间字符串(`<数字><单位>`,单位支持 `d`/`h`/`m`)。
#### Scenario: 配置 7 天保留
- **WHEN** 配置文件中 `server.storage.retention` 设置为 `"7d"`
- **THEN** 系统 SHALL 保留最近 7 天的检查结果,清理更早的数据
#### Scenario: 配置小时级保留
- **WHEN** 配置文件中 `server.storage.retention` 设置为 `"24h"`
- **THEN** 系统 SHALL 保留最近 24 小时的检查结果
#### Scenario: 未配置 retention
- **WHEN** 配置文件中未指定 `server.storage.retention`
- **THEN** 系统 SHALL 使用默认值 `"7d"`
#### Scenario: 无效 retention 格式
- **WHEN** 配置文件中 `server.storage.retention` 格式不合法(如 `"abc"``"7x"`
- **THEN** 系统 SHALL 在配置校验阶段报错,拒绝启动
### Requirement: 定时清理调度
系统 SHALL 以固定间隔1 小时)定期执行数据清理,删除超过保留时长的历史检查结果,并清理已无关联检查结果的非活跃目标行。
#### Scenario: 引擎启动后首次清理
- **WHEN** ProbeEngine 启动
- **THEN** 系统 SHALL 立即执行一次清理,然后每隔 1 小时再次执行
#### Scenario: 清理执行
- **WHEN** 清理定时器触发
- **THEN** 系统 SHALL 删除 `check_results` 表中 `timestamp` 早于 `now - retentionMs` 的所有记录
#### Scenario: 清理空壳非活跃目标
- **WHEN** 清理定时器触发且 check_results 过期清理执行完毕
- **THEN** 系统 SHALL 删除 `targets` 表中 `active = 0` 且在 `check_results` 表中不存在任何关联记录的目标行
#### Scenario: 引擎停止时清除定时器
- **WHEN** ProbeEngine.stop() 被调用
- **THEN** 系统 SHALL 清除清理定时器,不再执行后续清理
#### Scenario: retention 为 0 时不清理
- **WHEN** 配置的 retention 解析为 0 毫秒
- **THEN** 系统 SHALL 不注册清理定时器,数据永久保留

View File

@@ -1,6 +1,6 @@
## Purpose
定义基于 SQLite 的拨测数据持久化存储targets 同步含分组信息、check_results 追加写入、Dashboard 和 Metrics 数据查询支持、延迟百分位取数、时间范围和分页查询、索引与聚合查询。
定义基于 SQLite 的拨测数据持久化存储targets 同步含分组信息、check_results 追加写入、Dashboard 和 Metrics 数据查询支持、延迟百分位取数、时间范围和分页查询、索引与聚合查询、批量查询方法getLatestChecksMap、getAllTargetStats、getAllRecentSamples、N+1 查询优化,以及 prepared statement 使用 query() 缓存
## Requirements
@@ -225,3 +225,89 @@ ProbeStore SHALL 提供 `prune(retentionMs: number)` 方法,删除超过保留
#### Scenario: 活跃目标永不清理
- **WHEN** 存在 active=1 的 target 且该 target 在 check_results 表中无关联记录
- **THEN** 系统 SHALL NOT 删除该 target 行
### Requirement: 批量查询最新检查结果
系统 SHALL 提供 `getLatestChecksMap` 方法,通过单次 SQL 查询获取所有 target 的最新一次 check 结果,返回 Map 结构供调用方按 target_id 索引。
#### Scenario: 获取所有目标的最新检查
- **WHEN** 调用 `getLatestChecksMap()`
- **THEN** 系统 SHALL 执行子查询找到每个 target_id 的 MAX(timestamp),再 JOIN 回 check_results 获取完整行,返回 `Map<number, StoredCheckResult | null>`
#### Scenario: 批量查询目标无历史记录
- **WHEN** 某 target 在 check_results 表中无任何记录
- **THEN** 该 target_id 在返回的 Map 中 SHALL 不存在对应的 key
### Requirement: 批量查询目标统计
系统 SHALL 提供 `getAllTargetStats` 方法,通过单次 SQL GROUP BY 聚合查询获取所有 target 的拨测统计totalChecks 和 availability。availability 计算精度 SHALL 与 `getTargetStats` 一致,统一使用 `Math.round(value * 100) / 100` 保留两位小数。
#### Scenario: 获取所有目标的聚合统计
- **WHEN** 调用 `getAllTargetStats()`
- **THEN** 系统 SHALL 执行 `SELECT target_id, COUNT(*), SUM(CASE WHEN matched=1 THEN 1 ELSE 0 END) FROM check_results GROUP BY target_id`,在内存中计算 availability 并返回 `Map<number, { totalChecks, availability }>`
#### Scenario: 统计查询目标无历史记录
- **WHEN** 某 target 在 check_results 表中无任何记录
- **THEN** 该 target_id 在返回的 Map 中 SHALL 不存在对应的 key
#### Scenario: availability 精度
- **WHEN** 计算 availabilityupCount / totalChecks * 100
- **THEN** 结果 SHALL 使用 `Math.round(value * 100) / 100` 四舍五入保留两位小数,与 `getTargetStats` 方法一致
### Requirement: 批量查询所有目标的最近采样数据
系统 SHALL 提供 `getAllRecentSamples(limit: number)` 方法,通过单次 SQL 查询获取所有 target 的最近 N 条采样数据,返回 `Map<number, Array<{ timestamp: string; duration_ms: number | null; matched: number }>>` 结构。
#### Scenario: 获取所有目标的最近采样
- **WHEN** 调用 `getAllRecentSamples(30)`
- **THEN** 系统 SHALL 通过单次 SQL 查询获取每个 target 最近 30 条记录,返回按 target_id 索引的 Map
#### Scenario: 采样查询目标无历史记录
- **WHEN** 某 target 在 check_results 表中无任何记录
- **THEN** 该 target_id 在返回的 Map 中 SHALL 不存在对应的 key
#### Scenario: 采样数据排序
- **WHEN** 获取采样数据
- **THEN** 每个 target 的记录 SHALL 按 timestamp 降序排列(最新在前)
#### Scenario: 采样目标无数据返回空数组
- **WHEN** 某 target 在 getAllRecentSamples 返回的 Map 中不存在
- **THEN** 该 target 的 recentSamples SHALL 为空数组
### Requirement: summary 查询使用批量方法
`getSummary` 方法 SHALL 使用 `getLatestChecksMap` 一次性获取所有 target 的最新检查结果,而非对每个 target 逐条查询。
#### Scenario: 统计总览使用批量查询
- **WHEN** 调用 `store.getSummary()`
- **THEN** 系统 SHALL 调用 `getLatestChecksMap()` 一次获取所有最新结果,在内存中遍历统计 up/down 数量,而非循环 N 次调用 `getLatestCheck()`
### Requirement: targets 列表使用批量方法
`handleTargets`routes/targets.ts 中生成 TargetStatus[] 的逻辑SHALL 使用 `getLatestChecksMap``getAllTargetStats``getAllRecentSamples` 替代逐目标查询,消除 N+1 查询。
#### Scenario: 目标列表使用批量查询
- **WHEN** 处理 `GET /api/targets` 请求
- **THEN** 系统 SHALL 分别调用 `getLatestChecksMap()``getAllTargetStats()``getAllRecentSamples(30)` 批量获取数据,在内存中组装 TargetStatus 数组,而非对每个 target 逐条查询数据库
### Requirement: prepared statement 使用 query() 缓存
ProbeStore 中不涉及事务内复用的单次读/写操作 SHALL 使用 `this.db.query()` 而非 `this.db.prepare()`,利用 bun:sqlite 内置的 statement 缓存机制。
#### Scenario: insertCheckResult 使用 query
- **WHEN** 写入一条检查结果
- **THEN** `insertCheckResult` SHALL 使用 `this.db.query("INSERT INTO ...").run(...)` 而非 `this.db.prepare("INSERT INTO ...").run(...)`
#### Scenario: getHistory 查询使用 query
- **WHEN** 查询历史记录(包括 COUNT 和分页查询)
- **THEN** `getHistory` SHALL 使用 `this.db.query(...)` 而非 `this.db.prepare(...)`
#### Scenario: getTargetStats 查询使用 query
- **WHEN** 查询单目标统计
- **THEN** `getTargetStats` SHALL 使用 `this.db.query(...)` 而非 `this.db.prepare(...)`
#### Scenario: getTrend 查询使用 query
- **WHEN** 查询趋势数据
- **THEN** `getTrend` SHALL 使用 `this.db.query(...)` 而非 `this.db.prepare(...)`
#### Scenario: getRecentSamples 查询使用 query
- **WHEN** 查询采样数据
- **THEN** `getRecentSamples` SHALL 使用 `this.db.query(...)` 而非 `this.db.prepare(...)`
#### Scenario: syncTargets 事务保持 prepare例外
- **WHEN** 同步 targets 配置(事务内多次复用 insertStmt/updateStmt/deleteStmt
- **THEN** `syncTargets` 方法 SHALL 保持使用 `this.db.prepare()`,因需要在事务闭包内持有引用

View File

@@ -1,25 +0,0 @@
## Purpose
定义 ESLint 与 Prettier 集成方案,通过 eslint-plugin-prettier 将 Prettier 格式检查统一纳入 ESLint 工作流,减少独立工具调用,提升开发体验。
## Requirements
### Requirement: ESLint 集成 Prettier 格式检查
项目 SHALL 通过 `eslint-plugin-prettier` 将 Prettier 格式规则集成为 ESLint 规则,使单次 `eslint .` 运行同时报告代码质量问题和格式问题。ESLint 配置 SHALL 注册 `plugin:prettier/recommended` 规则集,该规则集自动加载 Prettier 配置并禁用与 Prettier 冲突的 ESLint 规则。
#### Scenario: lint 运行同时检查格式
- **WHEN** 开发者运行 `bun run lint`
- **THEN** ESLint SHALL 同时报告代码质量违规和 Prettier 格式违规
#### Scenario: lint --fix 自动格式化
- **WHEN** 开发者运行 `eslint --fix` 或 lint-staged 自动触发 `eslint --fix`
- **THEN** ESLint SHALL 对可自动修复的代码质量问题和格式问题一并修正,包括调用 Prettier 重写文件格式
#### Scenario: Prettier 配置被正确读取
- **WHEN** ESLint 通过 eslint-plugin-prettier 检查代码格式
- **THEN** 检查结果 SHALL 与独立运行 `prettier --check` 的结果完全一致
#### Scenario: 不因集成降低 lint 性能
- **WHEN** 开发者运行 `bun run lint`
- **THEN** 运行时间 SHOULD 不超过原 `eslint .` + `prettier --check` 总耗时的 120%

View File

@@ -1,209 +0,0 @@
## Purpose
定义 HTTP 拨测中响应体校验方法集contains/regex/json/css/xpath、操作符系统和响应头校验的行为规范。
## Requirements
### Requirement: 响应体多种校验方法
系统 SHALL 支持通过共享 `ContentExpectations` 对 HTTP 响应体进行有序内容校验。`expect.body` MUST 为 expectation 数组。每个 expectation SHALL 为直接 `ValueMatcher`,或 `json``css``xpath` extractor expectation 之一。直接 matcher SHALL 作用于完整响应体文本。`json` SHALL 解析响应体为 JSON 并用 JSONPath 子集提取值。`css` SHALL 使用 CSS selector 从 HTML 中提取元素文本或属性。`xpath` SHALL 使用 XPath 从 XML/HTML 中提取节点值。Extractor 未配置 matcher 时resolve 阶段 SHALL 将其 matcher 物化为 `{exists: true}`
#### Scenario: contains 子串匹配
- **WHEN** HTTP target 配置 `expect.body: [{contains: "healthy"}]`,且响应体包含 `"healthy"`
- **THEN** 系统 SHALL 判定该 body expectation 通过
#### Scenario: regex 正则匹配
- **WHEN** HTTP target 配置 `expect.body: [{regex: '"status"\\s*:\\s*"ok"'}]`,且响应体匹配该正则
- **THEN** 系统 SHALL 判定该 body expectation 通过
#### Scenario: json JSONPath 等值匹配
- **WHEN** HTTP target 配置 `expect.body: [{json: {path: "$.status", equals: "ok"}}]`,且响应 JSON 中 `$.status` 值为 `"ok"`
- **THEN** 系统 SHALL 判定该 body expectation 通过
#### Scenario: json JSONPath 存在性匹配
- **WHEN** HTTP target 配置 `expect.body: [{json: {path: "$.status"}}]`,且响应 JSON 中存在 `$.status`
- **THEN** resolve 阶段 SHALL 将该 expectation 按 `exists: true` 语义物化并在运行期判定通过
#### Scenario: css 选择器匹配
- **WHEN** HTTP target 配置 `expect.body: [{css: {selector: "div#health", equals: "OK"}}]`,且 HTML 中存在 `div#health` 元素文本为 `"OK"`
- **THEN** 系统 SHALL 判定该 body expectation 通过
#### Scenario: css 选择器匹配属性值
- **WHEN** HTTP target 配置 css expectation 带 `attr: "content"` 用于提取属性,且属性值匹配期望
- **THEN** 系统 SHALL 判定该 body expectation 通过
#### Scenario: xpath 表达式匹配
- **WHEN** HTTP target 配置 `expect.body: [{xpath: {path: "/root/status/text()", equals: "ok"}}]`,且 XML 中 `/root/status` 节点文本为 `"ok"`
- **THEN** 系统 SHALL 判定该 body expectation 通过
#### Scenario: 提取器无匹配目标失败
- **WHEN** HTTP target 配置了 json、css 或 xpath expectation 且对应路径、元素或节点不存在,并且 expectation 未配置 `exists: false`
- **THEN** 系统 SHALL 判定 matched 为 false
### Requirement: 多种 body 校验方法 AND 组合
系统 SHALL 支持在 `expect.body` 数组中同时配置多条内容 expectation所有 expectation 均通过时 matched 方为 true。系统 SHALL 按数组顺序执行 expectation任一 expectation 失败后 MUST NOT 继续执行后续 expectation。
#### Scenario: 多种方法全部通过
- **WHEN** HTTP target 的 `expect.body` 数组依次配置 contains、json、regex且全部通过
- **THEN** 系统 SHALL 判定 matched 为 true
#### Scenario: 多种方法任一失败
- **WHEN** HTTP target 的 `expect.body` 数组第一条 contains 不通过,后续还有 json expectation
- **THEN** 系统 SHALL 判定 matched 为 false且不再检查后续 json expectation
#### Scenario: 直接 matcher 多字段组合
- **WHEN** HTTP target 配置 `expect.body: [{contains: "ok", regex: "status"}]`,且响应体同时满足 contains 和 regex
- **THEN** 系统 SHALL 判定该 expectation 通过
### Requirement: 操作符系统
系统 SHALL 支持通过共享 `ValueMatcher` 对提取值和文本值进行比较:`equals`(深度等值)、`contains`(子串包含)、`regex`(正则匹配)、`empty`(空值判断)、`exists`(存在性判断)、`gte`/`lte`/`gt`/`lt`(数值比较)。系统 MUST NOT 支持旧 `match` 字段。
#### Scenario: equals 匹配 JSON value
- **WHEN** 配置 `{equals: {status: "ok"}}`,且实际值为相同 JSON object
- **THEN** 系统 SHALL 使用深度相等判定通过
#### Scenario: 显式 contains matcher
- **WHEN** 配置 `{contains: "success"}`,且实际值字符串化后包含 `"success"`
- **THEN** 系统 SHALL 判定该规则通过
#### Scenario: 显式 regex matcher
- **WHEN** 配置 `{regex: '\\d+\\.\\d+\\.\\d+'}`,且实际值字符串化后匹配该正则
- **THEN** 系统 SHALL 判定该规则通过
#### Scenario: empty matcher 判断为空
- **WHEN** 配置 `{empty: true}`,且实际值为空数组 `[]`
- **THEN** 系统 SHALL 判定该规则通过
#### Scenario: exists matcher 判断不存在
- **WHEN** 配置 `{exists: false}`,且实际值为 `undefined`
- **THEN** 系统 SHALL 判定该规则通过
#### Scenario: 数值比较 matcher
- **WHEN** 配置 `{gt: 0, lt: 1000}`,且实际值为 `500`
- **THEN** 系统 SHALL 对同一字段进行多 matcher 复合比较,全部通过则该规则通过
### Requirement: 响应头校验
系统 SHALL 支持通过共享 `KeyedExpectations` 配置 `expect.headers` 对 HTTP 响应头进行键值断言header 名称匹配 MUST 不区分大小写。header 期望值 MUST 为 `RawValueExpectation`primitive 字面量 SHALL 在 resolve 阶段等价为 `{equals: <value>}`
#### Scenario: 响应头字面量匹配
- **WHEN** HTTP target 配置 `expect.headers: {"Content-Type": "application/json"}`,且响应包含该 header 且值精确匹配
- **THEN** 系统 SHALL 判定 headers 阶段通过
#### Scenario: 响应头 matcher 匹配
- **WHEN** HTTP target 配置 `expect.headers: {"Content-Type": {contains: "application/json"}}`,且响应 header 值包含该文本
- **THEN** 系统 SHALL 判定 headers 阶段通过
#### Scenario: 响应头缺失
- **WHEN** HTTP target 配置了某个 header 但响应中不存在该 header且未配置 `exists: false`
- **THEN** 系统 SHALL 判定 matched 为 false
### Requirement: 结构化 expect 失败信息
系统 SHALL 在任一 expect 规则失败时生成结构化 failure用于标识失败阶段、规则路径、期望值、实际值和可读错误信息。actual 值 SHALL 在构造时截断至不超过 200 字符超出部分以省略标记和总字符数替代。expected 值不截断。
#### Scenario: body 规则失败信息
- **WHEN** HTTP target 的 `expect.body[1].json` 规则失败
- **THEN** failure SHALL 包含 kind=`mismatch`、phase=`body`、path 指向 `expect.body[1]`,并包含 message
#### Scenario: actual 值截断
- **WHEN** 失败规则的实际值为字符串且长度超过 200 字符
- **THEN** failure.actual SHALL 为前 200 字符加 `…(共 N 字符)` 后缀,其中 N 为原始总字符数
#### Scenario: actual 值未超限
- **WHEN** 失败规则的实际值为字符串且长度不超过 200 字符
- **THEN** failure.actual SHALL 保留完整原始值,不做截断
#### Scenario: actual 值为对象或数组
- **WHEN** 失败规则的实际值为对象或数组,且 JSON 序列化后长度超过 200 字符
- **THEN** failure.actual SHALL 为序列化后前 200 字符加 `…(共 N 字符)` 后缀
#### Scenario: actual 值为标量
- **WHEN** 失败规则的实际值为 number、boolean、null 或 undefined
- **THEN** failure.actual SHALL 保留原始值,不做截断
### Requirement: 状态码范围匹配
系统 SHALL 支持在 `expect.status` 数组中使用范围模式字符串(`"1xx"``"2xx"``"3xx"``"4xx"``"5xx"`),与精确数字混合使用。范围模式 SHALL 匹配对应百位段内的所有状态码。其他范围模式 SHALL 在启动期配置校验失败。
#### Scenario: 1xx 范围匹配
- **WHEN** HTTP target 配置 `expect.status: ["1xx"]`,且响应状态码为 101
- **THEN** 系统 SHALL 判定状态码匹配
#### Scenario: 2xx 范围匹配
- **WHEN** HTTP target 配置 `expect.status: ["2xx"]`,且响应状态码为 200
- **THEN** 系统 SHALL 判定状态码匹配
#### Scenario: 2xx 范围匹配 204
- **WHEN** HTTP target 配置 `expect.status: ["2xx"]`,且响应状态码为 204
- **THEN** 系统 SHALL 判定状态码匹配
#### Scenario: 2xx 范围不匹配 301
- **WHEN** HTTP target 配置 `expect.status: ["2xx"]`,且响应状态码为 301
- **THEN** 系统 SHALL 判定状态码不匹配
#### Scenario: 混合精确值与范围模式
- **WHEN** HTTP target 配置 `expect.status: ["2xx", 301]`,且响应状态码为 301
- **THEN** 系统 SHALL 判定状态码匹配(精确值 301 匹配)
#### Scenario: 混合精确值与范围模式范围命中
- **WHEN** HTTP target 配置 `expect.status: ["2xx", 301]`,且响应状态码为 204
- **THEN** 系统 SHALL 判定状态码匹配2xx 范围命中)
#### Scenario: 5xx 范围匹配
- **WHEN** HTTP target 配置 `expect.status: ["5xx"]`,且响应状态码为 503
- **THEN** 系统 SHALL 判定状态码匹配
#### Scenario: 非 HTTP 范围模式启动失败
- **WHEN** HTTP target 配置 `expect.status: ["6xx"]`
- **THEN** 系统 SHALL 在启动期配置校验失败
### Requirement: HTTP expect 规则启动期校验
系统 SHALL 在启动期校验 HTTP expect 中已支持字段的类型、格式、未知字段和可编译表达式。HTTP expect SHALL 只允许 `status``headers``body``durationMs` 字段。`expect.body` MUST 为 `RawContentExpectations` 数组。直接 `ValueMatcher` 对象 MUST 至少包含一个合法 matcher。Extractor expectation MUST 只包含 `json``css``xpath` 中的一种 extractor。Extractor 内部可以不配置 matcher并 SHALL 在 resolve 阶段以存在性 matcher 作为通过语义。`equals` matcher SHALL 支持任意 JSON value包括数组和对象。系统 SHALL 在启动期对所有 `regex` 执行静态 ReDoS 检测。语义校验 MUST NOT 修改 Raw HTTP expect 输入。
#### Scenario: body rule 使用 regex 字段
- **WHEN** HTTP target 配置 `expect.body: [{regex: "ok|healthy"}]` 且 regex 可编译且无 ReDoS 风险
- **THEN** 系统 SHALL 接受该配置,并在运行期按 regex expectation 匹配响应体
#### Scenario: body rule 不支持 match 字段
- **WHEN** HTTP target 配置 `expect.body: [{match: "ok"}]`
- **THEN** 系统 SHALL 在启动期配置校验失败,提示 `match` 是未知字段或不支持字段
#### Scenario: body rule 多 extractor 非法
- **WHEN** HTTP target 的同一条 body expectation 同时配置 `json``css`
- **THEN** 系统 SHALL 在启动期配置校验失败
#### Scenario: matcher regex 正则非法
- **WHEN** HTTP target 的 expect.headers、body 直接 matcher 或 extractor 内部 matcher 配置了不可编译的 regex
- **THEN** 系统 SHALL 在启动期配置校验失败
#### Scenario: matcher 数值比较类型非法
- **WHEN** HTTP target 的 matcher 配置 gt、gte、lt 或 lte且对应值不是有限数字
- **THEN** 系统 SHALL 在启动期配置校验失败
#### Scenario: matcher 布尔类型非法
- **WHEN** HTTP target 的 matcher 配置 empty 或 exists且对应值不是布尔值
- **THEN** 系统 SHALL 在启动期配置校验失败
#### Scenario: JSONPath 子集非法
- **WHEN** HTTP target 的 json body expectation path 不符合系统支持的 JSONPath 子集
- **THEN** 系统 SHALL 在启动期配置校验失败
#### Scenario: matcher 未知字段非法
- **WHEN** HTTP target 的 matcher 配置了 `foo: "bar"` 等未知字段
- **THEN** 系统 SHALL 在启动期配置校验失败
#### Scenario: durationMs matcher 非法
- **WHEN** HTTP target 配置 `expect.durationMs` 不是合法 `RawValueExpectation` 或其中数值 matcher 不是有限数字
- **THEN** 系统 SHALL 在启动期配置校验失败
### Requirement: HTTP body 运行期失败结构化
系统 SHALL 将 HTTP body 运行期失败记录为结构化 CheckFailure并保留与具体 expectation 相关的 phase 和 path。响应内容不符合配置 SHALL 记录为 mismatch响应内容无法按配置解析或解码 SHALL 记录为 error。
#### Scenario: JSON 响应不是合法 JSON
- **WHEN** HTTP target 配置 json body expectation但响应体不是合法 JSON
- **THEN** 系统 SHALL 记录 `failure.kind="error"``failure.phase="body"`,且 failure.path SHALL 指向对应 json expectation
#### Scenario: CSS selector 无匹配元素
- **WHEN** HTTP target 配置 css body expectation但响应 HTML 中无匹配元素
- **THEN** 系统 SHALL 记录 `failure.kind="mismatch"``failure.phase="body"`,且 failure.path SHALL 指向对应 css expectation
#### Scenario: XPath 无匹配节点
- **WHEN** HTTP target 配置 xpath body expectation但响应 XML/HTML 中无匹配节点
- **THEN** 系统 SHALL 记录 `failure.kind="mismatch"``failure.phase="body"`,且 failure.path SHALL 指向对应 xpath expectation

View File

@@ -1,6 +1,6 @@
## Purpose
定义共享 expect 断言规则系统的核心概念和基础设施ValueMatcher 统一匹配器、ContentExpectations 内容断言数组、KeyedExpectations 键控断言数组、以及相关的启动期校验和失败路径规范。
定义共享 expect 断言规则系统的核心概念和基础设施ValueMatcher 统一匹配器、ContentExpectations 内容断言数组、KeyedExpectations 键控断言数组、HTTP 场景特有规则状态码范围匹配、body 运行期失败结构化)、以及相关的启动期校验和失败路径规范。
## Requirements
@@ -249,3 +249,53 @@
#### Scenario: actual 截断
- **WHEN** matcher 失败时 actual 字符串长度超过 200 字符
- **THEN** 系统 SHALL 使用现有截断策略保存 failure.actual避免历史记录写入过长内容
### Requirement: 状态码范围匹配
系统 SHALL 支持在 `expect.status` 数组中使用范围模式字符串(`"1xx"``"2xx"``"3xx"``"4xx"``"5xx"`),与精确数字混合使用。范围模式 SHALL 匹配对应百位段内的所有状态码。其他范围模式 SHALL 在启动期配置校验失败。
#### Scenario: 1xx 范围匹配
- **WHEN** HTTP target 配置 `expect.status: ["1xx"]`,且响应状态码为 101
- **THEN** 系统 SHALL 判定状态码匹配
#### Scenario: 2xx 范围匹配
- **WHEN** HTTP target 配置 `expect.status: ["2xx"]`,且响应状态码为 200
- **THEN** 系统 SHALL 判定状态码匹配
#### Scenario: 2xx 范围匹配 204
- **WHEN** HTTP target 配置 `expect.status: ["2xx"]`,且响应状态码为 204
- **THEN** 系统 SHALL 判定状态码匹配
#### Scenario: 2xx 范围不匹配 301
- **WHEN** HTTP target 配置 `expect.status: ["2xx"]`,且响应状态码为 301
- **THEN** 系统 SHALL 判定状态码不匹配
#### Scenario: 混合精确值与范围模式
- **WHEN** HTTP target 配置 `expect.status: ["2xx", 301]`,且响应状态码为 301
- **THEN** 系统 SHALL 判定状态码匹配(精确值 301 匹配)
#### Scenario: 混合精确值与范围模式范围命中
- **WHEN** HTTP target 配置 `expect.status: ["2xx", 301]`,且响应状态码为 204
- **THEN** 系统 SHALL 判定状态码匹配2xx 范围命中)
#### Scenario: 5xx 范围匹配
- **WHEN** HTTP target 配置 `expect.status: ["5xx"]`,且响应状态码为 503
- **THEN** 系统 SHALL 判定状态码匹配
#### Scenario: 非 HTTP 范围模式启动失败
- **WHEN** HTTP target 配置 `expect.status: ["6xx"]`
- **THEN** 系统 SHALL 在启动期配置校验失败
### Requirement: HTTP body 运行期失败结构化
系统 SHALL 将 HTTP body 运行期失败记录为结构化 CheckFailure并保留与具体 expectation 相关的 phase 和 path。响应内容不符合配置 SHALL 记录为 mismatch响应内容无法按配置解析或解码 SHALL 记录为 error。
#### Scenario: JSON 响应不是合法 JSON
- **WHEN** HTTP target 配置 json body expectation但响应体不是合法 JSON
- **THEN** 系统 SHALL 记录 `failure.kind="error"``failure.phase="body"`,且 failure.path SHALL 指向对应 json expectation
#### Scenario: CSS selector 无匹配元素
- **WHEN** HTTP target 配置 css body expectation但响应 HTML 中无匹配元素
- **THEN** 系统 SHALL 记录 `failure.kind="mismatch"``failure.phase="body"`,且 failure.path SHALL 指向对应 css expectation
#### Scenario: XPath 无匹配节点
- **WHEN** HTTP target 配置 xpath body expectation但响应 XML/HTML 中无匹配节点
- **THEN** 系统 SHALL 记录 `failure.kind="mismatch"``failure.phase="body"`,且 failure.path SHALL 指向对应 xpath expectation

View File

@@ -1,6 +1,6 @@
## Purpose
定义基于 Vite dev server + Bun API server 的前端开发工作流、开发期 API 访问和共享契约的行为要求。
定义基于 Vite dev server + Bun API server 的前端开发工作流、生产前端构建与 code splitting 策略、开发期 API 访问和共享契约的行为要求。
## Requirements
@@ -15,6 +15,66 @@
- **WHEN** 开发者运行前端生产构建命令
- **THEN** 系统 SHALL 通过 Vite buildRolldown产出优化的前端静态资源到 `dist/web/`
### Requirement: Vite 前端构建配置
系统 SHALL 使用 Vite 作为前端构建工具,配置文件位于项目根目录 `vite.config.ts`,以 `src/web` 为 root产出到 `dist/web/`
#### Scenario: 运行 Vite 生产构建
- **WHEN** 构建脚本执行 `bunx --bun vite build`
- **THEN** Vite SHALL 将 `src/web/index.html` 及其引用的所有模块构建到 `dist/web/` 目录,包含 `index.html``assets/` 子目录
#### Scenario: 产出文件名包含 content hash
- **WHEN** Vite 构建完成
- **THEN** `assets/` 目录下的 JS 和 CSS 文件名 SHALL 包含 content hash`index-a1b2c3.js`
### Requirement: Code Splitting 策略
系统 SHALL 配置 Vite 的 Rolldown code splitting将 vendor 库分离为独立 chunks并通过 `React.lazy()` 动态导入实现按需加载。
#### Scenario: React 相关库分离
- **WHEN** Vite 构建完成
- **THEN** `react``react-dom``scheduler` SHALL 被打包到名为 `vendor-react` 的独立 chunk
#### Scenario: TDesign 相关库分离
- **WHEN** Vite 构建完成
- **THEN** `tdesign-react``tdesign-icons-react` 相关模块 SHALL 被打包到名为 `vendor-tdesign` 的独立 chunk
#### Scenario: 图表库分离
- **WHEN** Vite 构建完成
- **THEN** `recharts``d3-*` 相关模块 SHALL 被打包到名为 `vendor-chart` 的独立 chunk
#### Scenario: TargetDetailDrawer 延迟加载
- **WHEN** Vite 构建完成
- **THEN** `TargetDetailDrawer` 及其依赖recharts、D3、DateRangePicker 等SHALL 通过 `React.lazy()` 动态导入,被 Rolldown 自动拆分为异步 chunk不包含在初始加载的 JS 中
#### Scenario: Drawer 首次渲染无闪烁
- **WHEN** 用户首次点击目标触发 Drawer 渲染
- **THEN** Drawer SHALL 通过 `<Suspense fallback={null}>` 包裹,利用其默认 visible=false 状态避免加载期间的视觉闪烁
### Requirement: CSS 处理
系统 SHALL 通过 Vite 处理 CSS 导入,产出独立的 CSS 文件。TDesign 组件样式 SHALL 保持全量导入方式。
#### Scenario: CSS 文件产出
- **WHEN** Vite 构建完成
- **THEN** 所有 CSS 导入 SHALL 被提取为独立的 `.css` 文件到 `assets/` 目录
#### Scenario: CSS 压缩
- **WHEN** Vite 执行生产构建
- **THEN** 产出的 CSS 文件 SHALL 经过压缩处理
#### Scenario: TDesign CSS 全量导入
- **WHEN** 前端入口文件初始化样式
- **THEN** 系统 SHALL 通过 `tdesign-react/dist/reset.css``tdesign-react/dist/tdesign.min.css` 全量导入 TDesign 组件样式
### Requirement: 前端构建产物拆分
前端生产构建 SHALL 将 vendor 依赖拆分为独立 chunk利用浏览器并行加载和长期缓存。
#### Scenario: vendor chunk 拆分
- **WHEN** 执行前端生产构建
- **THEN** 构建产物 SHALL 包含独立的 vendor chunkreact、tdesign、recharts 各自独立),而非单个 bundle
#### Scenario: 业务代码变更不影响 vendor 缓存
- **WHEN** 仅修改业务代码src/web/ 下非 node_modules 文件)并重新构建
- **THEN** vendor chunk 的文件名(含 hashSHALL 保持不变,浏览器缓存 SHALL 继续有效
### Requirement: 前端开发期 API 代理
前端开发服务器 SHALL 通过 Vite proxy 配置将 API 请求转发到后端 server。

View File

@@ -1,6 +1,6 @@
## Purpose
定义基于 Vite + Bun 的全栈应用运行时,包括 HTTP server、API 命名空间、健康检查、生产静态资源服务和 SPA fallback 行为。
定义基于 Vite + Bun 的全栈应用运行时,包括统一服务 bootstrap、声明式 Bun routes、API 路由组织、HTTP server、API 命名空间、健康检查、生产静态资源服务和 SPA fallback 行为。
## Requirements
@@ -113,3 +113,108 @@
#### Scenario: SIGINT/SIGTERM 处理
- **WHEN** 开发模式进程收到 SIGINT 或 SIGTERM 信号
- **THEN** 系统 SHALL 调用 engine.stop() 停止调度、调用 store.close() 关闭数据库连接后退出进程
### Requirement: 路径参数支持
系统 SHALL 使用 routes 对象的 `:param` 语法声明路径参数,替代手动 regex 匹配。
#### Scenario: 带路径参数的 API 路由
- **WHEN** 客户端请求 `/api/targets/123/history`
- **THEN** 系统 SHALL 通过 `routes` 中注册的 `/api/targets/:id/history` 匹配,并通过 `req.params.id` 获取参数值 `"123"`
#### Scenario: 路径参数类型
- **WHEN** route handler 接收到路径参数
- **THEN** 参数值 SHALL 为字符串类型handler 负责进行类型转换和校验
### Requirement: HTTP Method 声明
系统 SHALL 在 routes 对象中为每个 API 端点以 per-method handler 形式声明支持的 HTTP method未匹配 method 的 API 请求 SHALL 落入 `/api/*` 通配符并返回 JSON 404。
#### Scenario: 单 method 端点
- **WHEN** API 端点只支持 GET 方法
- **THEN** 该端点 SHALL 以 `{ GET(req) { ... } }` 形式注册
#### Scenario: 不支持的 method 请求
- **WHEN** 客户端使用未声明的 method 请求 API 端点
- **THEN** `/api/*` 通配符 SHALL 返回 JSON 格式的 404 错误响应
### Requirement: 路由按职责拆分
系统 SHALL 将 HTTP 路由处理逻辑按 API 端点拆分为独立模块,每个模块导出 route handler 函数供 routes 对象统一注册。
#### Scenario: health 端点独立路由
- **WHEN** 客户端请求 `GET /health`
- **THEN** `routes/health.ts` 导出的 handler 负责处理,返回 HealthResponse JSON
#### Scenario: summary 端点独立路由
- **WHEN** 客户端请求 `GET /api/summary`
- **THEN** `routes/summary.ts` 导出的 handler 负责处理,委托 store 查询并返回 SummaryResponse JSON
#### Scenario: targets 端点独立路由
- **WHEN** 客户端请求 `GET /api/targets`
- **THEN** `routes/targets.ts` 导出的 handler 负责处理,委托 store 查询并返回 TargetStatus[] JSON
#### Scenario: history 端点独立路由
- **WHEN** 客户端请求 `GET /api/targets/:id/history?from=ISO&to=ISO`
- **THEN** `routes/history.ts` 导出的 handler 负责处理,通过 `req.params.id` 获取路径参数包含参数校验、store 查询和 HistoryResponse 返回
#### Scenario: trend 端点独立路由
- **WHEN** 客户端请求 `GET /api/targets/:id/trend?from=ISO&to=ISO`
- **THEN** `routes/trend.ts` 导出的 handler 负责处理,通过 `req.params.id` 获取路径参数包含参数校验、store 查询和 TrendPoint[] 返回
### Requirement: 共享辅助函数集中管理
系统 SHALL 将跨路由共享的响应格式化函数抽取到 helpers.ts 模块,单一职责、集中管理。
#### Scenario: createApiError 集中定义
- **WHEN** 任意路由需要返回 API 错误响应
- **THEN** 从 `helpers.ts` 导入 `createApiError` 函数,提供错误信息和状态码
#### Scenario: jsonResponse 集中定义
- **WHEN** 任意路由需要返回 JSON 响应
- **THEN** 从 `helpers.ts` 导入 `jsonResponse` 函数,处理 HEAD 方法、Content-Type 和安全头
#### Scenario: mapCheckResult 集中定义
- **WHEN** 需要将 StoredCheckResult 映射为 API CheckResult
- **THEN** 从 `helpers.ts` 导入 `mapCheckResult` 函数,处理 failure JSON 解析和格式转换
### Requirement: 统一启动引导函数
系统 SHALL 提供 `src/server/bootstrap.ts` 导出 `bootstrap(options: BootstrapOptions)` 函数,封装完整的服务启动序列:加载配置、初始化正式 logger、创建 store、同步 targets、创建并启动 engine、启动 HTTP server、注册 shutdown handler。配置加载失败发生在正式 logger 初始化之前,系统 SHALL 使用 console fallback 输出启动失败信息。配置加载成功后的启动失败 SHALL 使用正式 logger 输出 `fatal` 后退出。
#### Scenario: 开发模式启动
- **WHEN** `dev.ts` 调用 `bootstrap({ configPath, mode: "development" })`
- **THEN** 系统 SHALL 完成完整启动序列,不传入 staticAssets并初始化运行时 logger
#### Scenario: 生产模式启动(带静态资源)
- **WHEN** code-generated entry 调用 `bootstrap({ configPath, mode: "production", staticAssets })`
- **THEN** 系统 SHALL 完成完整启动序列,将 staticAssets 传递给 startServer并初始化运行时 logger
#### Scenario: 配置加载失败处理
- **WHEN** 配置文件读取、YAML 解析或配置校验失败
- **THEN** 系统 SHALL 通过 console fallback 输出错误信息并以非零退出码退出进程
#### Scenario: 配置加载后的启动失败处理
- **WHEN** logger、store、engine 或 HTTP server 初始化失败
- **THEN** 系统 SHALL 通过正式 logger 输出 `fatal` 日志并以非零退出码退出进程
#### Scenario: 优雅关机
- **WHEN** 进程收到 SIGINT 或 SIGTERM 信号
- **THEN** bootstrap 注册的 shutdown handler SHALL 调用 engine.stop()、store.close() 和 logger.flush() 后退出
### Requirement: BootstrapOptions 接口
`bootstrap` 函数 SHALL 接受 `BootstrapOptions` 参数,包含 `configPath: string``mode: RuntimeMode` 和可选的 `staticAssets?: StaticAssets`
#### Scenario: 最小配置(开发模式)
- **WHEN** 仅传入 configPath 和 mode
- **THEN** 系统 SHALL 正常启动startServer 不接收 staticAssets 参数
#### Scenario: 生产模式配置
- **WHEN** 传入 configPath、mode 和 staticAssets
- **THEN** 系统 SHALL 将 staticAssets 传递给 startServer
### Requirement: dev.ts 和生产入口使用 bootstrap
`dev.ts``src/server/main.ts` SHALL 调用 `bootstrap()` 而非各自维护启动序列。
#### Scenario: dev.ts 调用 bootstrap
- **WHEN** 开发者运行 `bun run dev`
- **THEN** `dev.ts` SHALL 调用 `bootstrap` 完成启动
#### Scenario: main.ts 调用 bootstrap
- **WHEN** 生产可执行文件启动
- **THEN** `main.ts` SHALL 调用 `bootstrap` 完成启动

View File

@@ -25,8 +25,172 @@
#### Scenario: HTTP target 覆盖 headers
- **WHEN** YAML 中某个 HTTP target 显式配置 `http.headers`
- **THEN** 该 target SHALL 使用自身 headers 字段的值,不再与规则文件级 defaults 合并
- **THEN** 该 target SHALL 使用自身 headers 字段的值
#### Scenario: HTTP target 配置 ignoreSSL
- **WHEN** YAML 中 HTTP target 设置 `http.ignoreSSL: true`
- **THEN** 系统 SHALL 解析该字段并在执行时跳过 SSL 证书校验
#### Scenario: HTTP target 配置 maxRedirects
- **WHEN** YAML 中 HTTP target 设置 `http.maxRedirects: 5`
- **THEN** 系统 SHALL 解析该字段并在执行时允许最多跟随 5 次重定向
#### Scenario: HTTP 序列化展示摘要
- **WHEN** 系统同步 HTTP target 到 targets 表
- **THEN** `target` 展示摘要 SHALL 为 HTTP URL`config` JSON SHALL 包含 resolved 后的 url、method、headers、body、ignoreSSL、maxBodyBytes 和 maxRedirects
### Requirement: HTTP checker 执行
系统 SHALL 按 HTTP target 配置发起 HTTP 请求,使用引擎注入的 `ctx.signal` 响应超时。系统 SHALL 支持手动重定向跟随(`redirect: "manual"`),在 `maxRedirects` 限制内跟随 301/302/303/307/308 重定向。系统 SHALL 记录完整执行耗时和 HTTP observation并在网络错误、超时、响应体超限或字符编码不支持时产生结构化失败信息。
#### Scenario: HTTP 请求成功
- **WHEN** HTTP target 指向可正常响应的 HTTP 服务,且未配置 expect 或 `expect.status``[200]`
- **THEN** 系统 SHALL 记录 `matched=true``durationMs` 和包含 statusCode、headers、bodyPreview、contentType、contentLength 的 observation
#### Scenario: 使用 ctx.signal 响应超时
- **WHEN** 引擎注入的 `ctx.signal` 在 HTTP 请求过程中 abort
- **THEN** 系统 SHALL 记录 `matched=false`observation SHALL 为 nullfailure 的 kind 为 `error`phase 为 `request`message 包含超时信息
#### Scenario: 网络错误
- **WHEN** HTTP 请求因网络错误DNS 解析失败、连接被拒绝等)失败
- **THEN** 系统 SHALL 记录 `matched=false`observation SHALL 为 nullfailure 的 kind 为 `error`phase 为 `request`
#### Scenario: 重定向跟随
- **WHEN** HTTP target 配置 `http.maxRedirects: 3` 且服务端返回 301/302/303/307/308 重定向
- **THEN** 系统 SHALL 在 maxRedirects 限制内自动跟随重定向,并返回最终响应供 expect 校验
#### Scenario: 重定向次数耗尽
- **WHEN** HTTP target 配置 `http.maxRedirects: 1` 且服务端返回超过 1 次重定向
- **THEN** 系统 SHALL 将最后一次重定向响应作为最终结果返回,不继续跟随
#### Scenario: POST 重定向为 GET
- **WHEN** HTTP target 使用 POST 方法且收到 301 或 302 重定向,或收到 303 重定向
- **THEN** 系统 SHALL 将后续请求方法改为 GET移除 Content-Type 和 Content-Length 请求头,并移除 body
#### Scenario: 跨域重定向移除敏感头
- **WHEN** 重定向到不同 origin 的目标
- **THEN** 系统 SHALL 移除 Authorization 和 Cookie 请求头
#### Scenario: 响应体读取与大小限制
- **WHEN** HTTP target 配置了 body expect 且响应体未超过 `maxBodyBytes`
- **THEN** 系统 SHALL 读取完整响应体,按 Content-Type 字符编码解码,并用于 body expect 校验
#### Scenario: 响应体超过大小限制
- **WHEN** HTTP target 配置了 body expect 且响应体超过 `maxBodyBytes`
- **THEN** 系统 SHALL 停止读取,记录 `matched=false`failure 的 kind 为 `error`phase 为 `body`message 包含响应体超过大小限制的信息
#### Scenario: 不支持的字符编码
- **WHEN** HTTP 响应的 Content-Type 指定了不支持的 charset
- **THEN** 系统 SHALL 记录 `matched=false`failure 的 kind 为 `error`phase 为 `body`message 包含不支持的字符编码信息
#### Scenario: 无 body expect 时不读取响应体
- **WHEN** HTTP target 未配置 `expect.body` 或 body expect 为空数组
- **THEN** 系统 SHALL NOT 读取响应体内容bodyPreview SHALL 为 null
#### Scenario: GET/HEAD 请求不发送 body
- **WHEN** HTTP target 配置 `http.method: GET``http.method: HEAD`
- **THEN** 系统 SHALL NOT 发送请求体,即使配置了 `http.body`
### Requirement: HTTP expect 校验
系统 SHALL 支持 HTTP 专属 expect包括 `status``headers``body``durationMs`,并按 status、headers、early-timeout仅当配置了 body expect 时、body、durationMs 的阶段顺序快速失败。`status` SHALL 保持状态码数组语义支持精确数字100-599和范围模式`1xx``5xx`),未配置时在 Resolved expect 中默认 `[200]``headers` SHALL 使用共享 `RawKeyedExpectations` 输入并在运行期使用 `KeyedExpectations`header key 大小写不敏感。`body` MUST 使用共享 `RawContentExpectations` 数组输入并在运行期使用 `ContentExpectations`,支持直接 ValueMatcher 以及 json/css/xpath 提取器。`durationMs` SHALL 使用共享 `RawValueExpectation` 输入并在运行期使用 `ValueExpectation` 校验完整执行耗时。
#### Scenario: 默认 status 成功语义
- **WHEN** HTTP target 未显式配置 `expect.status`
- **THEN** 系统 SHALL 在 Resolved HTTP expect 中使用默认 `status: [200]` 进行校验
#### Scenario: status 精确值匹配
- **WHEN** HTTP target 配置 `expect.status: [200, 201]` 且响应状态码为 201
- **THEN** 系统 SHALL 判定 status 阶段通过,继续后续 expect 阶段
#### Scenario: status 范围模式匹配
- **WHEN** HTTP target 配置 `expect.status: ["2xx"]` 且响应状态码为 204
- **THEN** 系统 SHALL 判定 status 阶段通过
#### Scenario: status 不匹配快速失败
- **WHEN** HTTP target 配置 `expect.status: [200]` 且响应状态码为 500
- **THEN** 系统 SHALL 立即返回 `matched=false`failure 的 phase 为 `status`
#### Scenario: headers 校验通过
- **WHEN** HTTP target 配置 `expect.headers: {Content-Type: {contains: "application/json"}}` 且响应包含对应 header
- **THEN** 系统 SHALL 判定 headers 阶段通过header key 匹配大小写不敏感
#### Scenario: headers 校验失败
- **WHEN** HTTP target 配置 `expect.headers: {X-Custom: {exists: true}}` 且响应不包含该 header
- **THEN** 系统 SHALL 返回 `matched=false`failure 的 phase 为 `headers`
#### Scenario: body ContentExpectations 校验通过
- **WHEN** HTTP target 配置 `expect.body: [{contains: "ok"}, {json: {path: "$.status", equals: "success"}}]` 且响应体满足全部条件
- **THEN** 系统 SHALL 判定 body 阶段通过
#### Scenario: body ContentExpectations 快速失败
- **WHEN** HTTP target 配置两条 body expectation 且第一条失败
- **THEN** 系统 SHALL 返回第一条失败 expectation 的 failurephase 为 `body`,并 MUST NOT 执行第二条 expectation
#### Scenario: body 非 JSON 响应触发 JSONPath 失败
- **WHEN** HTTP target 配置 json body expectation 但响应体不是合法 JSON
- **THEN** 系统 SHALL 记录 `matched=false`failure 的 kind 为 `error`phase 为 `body`
#### Scenario: early-timeout 快速失败
- **WHEN** HTTP target 配置了 body expect 和 `expect.durationMs: {lte: 500}`,且请求状态码和 headers 通过后已耗时超过 500ms
- **THEN** 系统 SHALL 在读取响应体之前判定 durationMs 超限,不读取响应体,直接返回 `matched=false`failure 的 phase 为 `duration`
#### Scenario: durationMs 校验
- **WHEN** HTTP target 配置 `expect.durationMs: {lte: 1000}` 且完整执行耗时超过 1000ms
- **THEN** 系统 SHALL 返回 `matched=false`failure 的 phase 为 `duration`
### Requirement: HTTP checker 启动期配置校验
系统 SHALL 在启动期对 HTTP checker 的配置契约和语义执行严格校验。HTTP target 的 `http` 分组 SHALL 只允许 `url``method``headers``body``ignoreSSL``maxBodyBytes``maxRedirects` 字段。HTTP expect SHALL 只允许 `status``headers``body``durationMs` 字段。未知字段、非法类型、非法 URL 协议、非法 status 值、非法 maxBodyBytes、非法 ContentExpectations 和不可编译正则 MUST 导致启动期配置错误。语义校验 MUST NOT 修改 Raw HTTP expect 输入。
#### Scenario: http.url 为空字符串
- **WHEN** YAML 中 HTTP target 配置 `http.url: ""`
- **THEN** 系统 SHALL 以配置错误退出,提示缺少 http.url 字段
#### Scenario: http.url 协议非法
- **WHEN** YAML 中 HTTP target 的 `http.url` 不以 `http://``https://` 开头
- **THEN** 系统 SHALL 以配置错误退出,提示 URL 格式不合法
#### Scenario: http.url 格式非法
- **WHEN** YAML 中 HTTP target 的 `http.url` 不是合法 URL
- **THEN** 系统 SHALL 以配置错误退出,提示 URL 格式不合法
#### Scenario: http.maxBodyBytes 格式非法
- **WHEN** YAML 中 HTTP target 的 `http.maxBodyBytes` 不是合法 size 值
- **THEN** 系统 SHALL 以配置错误退出,提示 maxBodyBytes 格式错误
#### Scenario: http 分组未知字段失败
- **WHEN** YAML 中 HTTP target 的 `http` 分组包含未知字段
- **THEN** 系统 SHALL 以配置错误退出,提示 http 分组包含未知字段
#### Scenario: expect.status 数字非法
- **WHEN** YAML 中 HTTP target 的 `expect.status` 包含非整数或不在 100-599 范围内的数字
- **THEN** 系统 SHALL 以配置错误退出,提示 status 数字不合法
#### Scenario: expect.status 模式非法
- **WHEN** YAML 中 HTTP target 的 `expect.status` 包含不符合 `1xx``5xx` 格式的字符串
- **THEN** 系统 SHALL 以配置错误退出,提示 status 模式不合法
#### Scenario: expect.body 必须为数组
- **WHEN** YAML 中 HTTP target 的 `expect.body` 已配置但不是数组
- **THEN** 系统 SHALL 以配置错误退出,提示 expect.body 必须为数组
#### Scenario: HTTP expect 未知字段失败
- **WHEN** YAML 中 HTTP target 的 expect 包含 `connected``exitCode``maxDurationMs` 或其他非 HTTP expect 字段
- **THEN** 系统 SHALL 以配置错误退出,提示 expect 包含未知字段
### Requirement: HTTP observation 与 detail
HTTP checker SHALL 在 observation 中记录 statusCodenumber、headersRecord<string, string>,截断至前 20 个、bodyPreviewstring | null截断至前 1024 字符、contentTypestring | null、contentLengthnumber | null。API detail SHALL 由 `buildDetail` 从 observation 动态构造,格式为 `HTTP {statusCode}`。网络错误或超时导致无法收集 observation 时observation SHALL 为 nulldetail SHALL 为 null。
#### Scenario: 正常响应 observation
- **WHEN** HTTP 请求成功返回
- **THEN** observation SHALL 包含 statusCode、截断后的 headers、截断后的 bodyPreview、contentType 和 contentLength
#### Scenario: 未读取响应体时 bodyPreview 为 null
- **WHEN** HTTP target 未配置 body expect
- **THEN** observation.bodyPreview SHALL 为 null
#### Scenario: 请求失败 observation 为 null
- **WHEN** HTTP 请求因网络错误或超时失败
- **THEN** observation SHALL 为 nulldetail SHALL 为 null
#### Scenario: detail 格式
- **WHEN** API 序列化 HTTP CheckResult
- **THEN** detail SHALL 为 `HTTP {statusCode}` 格式(如 `HTTP 200`

View File

@@ -5,14 +5,14 @@
## Requirements
### Requirement: LLM Checker 注册与模块结构
系统 SHALL 提供 `type: llm` checker用于大模型服务的应用层拨测。LLM checker MUST 位于 `src/server/checker/runner/llm/` 自包含目录,并通过 `src/server/checker/runner/index.ts` 注册到 `CheckerRegistry`。LLM checker SHALL 复用现有 checker 抽象、配置 schema 组装、启动期语义校验、引擎调度、存储序列化和共享 expect 基础设施。
系统 SHALL 提供 `type: llm` checker用于大模型服务的应用层拨测。LLM checker MUST 位于自包含目录,并通过 checker 注册入口注册到 `CheckerRegistry`。LLM checker SHALL 复用现有 checker 抽象、配置 schema 组装、启动期语义校验、引擎调度、存储序列化和共享 expect 基础设施。
#### Scenario: 注册 LLM checker
- **WHEN** 系统初始化默认 checker registry
- **THEN** registry SHALL 包含 `llm` 类型,且 `/api/meta` 返回的 `checkerTypes` SHALL 包含 `llm`
#### Scenario: LLM checker 目录自包含
- **WHEN** 开发者查看 `src/server/checker/runner/llm/` 目录
- **WHEN** 开发者查看 LLM checker 目录
- **THEN** 该目录 SHALL 包含 LLM checker 的类型、schema、语义校验、provider 创建、observation 构建、expect 断言、执行逻辑和模块入口
#### Scenario: 不扩展存储和 API 结构
@@ -53,18 +53,6 @@ LLM checker SHALL 解析 `llm.provider`、`llm.url`、`llm.model`、`llm.prompt`
- **WHEN** 系统读取只包含 `type: llm` 以及 `llm.provider``llm.url``llm.model``llm.prompt` 的 target
- **THEN** 系统 SHALL 解析为 LLM target并填充 `mode=http``key=""``ignoreSSL=false``options.maxOutputTokens=16``options.temperature=0`
#### Scenario: headers 默认值合并
- **WHEN** `defaults.llm.headers` 和 target `llm.headers` 同时配置同名 header
- **THEN** LLM checker SHALL 按原始 header key 浅合并 headers并由 target `llm.headers` 覆盖 defaults 中同名 key
#### Scenario: options 默认值合并
- **WHEN** `defaults.llm.options` 和 target `llm.options` 同时配置同名 option
- **THEN** LLM checker SHALL 浅合并 options并由 target `llm.options` 覆盖 defaults 中同名字段
#### Scenario: providerOptions 默认值合并
- **WHEN** `defaults.llm.providerOptions` 和 target `llm.providerOptions` 同时配置同名 provider namespace
- **THEN** LLM checker SHALL 按 provider namespace 浅合并 providerOptions并由 target namespace 覆盖 defaults 中同名 namespace
#### Scenario: Anthropic Bearer token
- **WHEN** target 配置 `llm.provider: anthropic` 和非空 `llm.authToken`
- **THEN** LLM checker SHALL 将 `authToken` 映射到 Anthropic SDK 的 Bearer token 认证字段

View File

@@ -1,31 +0,0 @@
## Purpose
定义系统运行时元数据 APIchecker 类型列表、应用版本号等元信息的对外暴露方式。
## Requirements
### Requirement: Meta 信息 API
系统 SHALL 提供 `GET /api/meta` 端点,返回系统运行时元数据,包括应用版本号和 checker 类型列表。未匹配 method SHALL 按 API 通配符处理为 JSON 404不再保留自定义 HEAD/405 语义。
#### Scenario: 获取 checker 类型列表和版本号
- **WHEN** 客户端请求 `GET /api/meta`
- **THEN** 系统 SHALL 返回 JSON `{ checkerTypes: string[], version: string }`,其中 `checkerTypes` 包含所有已注册的 checker 类型标识符(如 `["http", "cmd"]``version` 为当前运行实例的 `MAJOR.MINOR.PATCH` 应用版本
#### Scenario: 类型列表来源
- **WHEN** 系统启动并注册了 checker
- **THEN** `/api/meta` 返回的 `checkerTypes` SHALL 与 `CheckerRegistry.supportedTypes` 完全一致
#### Scenario: 版本号来源
- **WHEN** 系统启动并确定应用版本
- **THEN** `/api/meta` 返回的 `version` SHALL 与启动时注入的应用版本完全一致
#### Scenario: 不支持的 method 请求
- **WHEN** 客户端使用 POST/PUT/DELETE/HEAD 等未声明 method 请求 `/api/meta`
- **THEN** `/api/*` 通配符 SHALL 返回 JSON 404 响应
### Requirement: MetaResponse 共享类型
系统 SHALL 在 `src/shared/api.ts` 中定义 `MetaResponse` 类型。
#### Scenario: MetaResponse 类型定义
- **WHEN** 前后端引用 `MetaResponse` 类型
- **THEN** 该类型 SHALL 包含 `checkerTypes: string[]``version: string` 字段

View File

@@ -1,6 +1,6 @@
## Purpose
定义拨测系统的 REST API 端点Dashboard 聚合 API、单目标指标 API、带时间范围和分页的历史记录、共享类型定义和 API 错误处理。
定义拨测系统的 REST API 端点Dashboard 聚合 API、单目标指标 API、历史记录 API、Meta 信息 API、共享类型定义和 API 错误处理。
## Requirements
@@ -75,11 +75,11 @@ Dashboard API SHALL 返回基于时间窗口计算的目标统计和连续状态
- **THEN** 每个 recentSamples 元素 SHALL 包含 timestamp、durationMs、up 字段,其中 up 为 boolean 且等于 matched
### Requirement: 单目标指标 API
系统 SHALL 提供 `GET /api/targets/:id/metrics` 端点,返回 Drawer 概览所需的单目标统计和趋势数据。仅活跃目标的指标 SHALL 可查询。端点的详细计算规则P95/P99、MTTR、故障分析、趋势分桶等定义在 `target-metrics-api` 能力中。
系统 SHALL 提供 `GET /api/targets/:id/metrics` 端点,返回单个目标在指定时间窗口内的概览统计和趋势数据。仅活跃目标的指标 SHALL 可查询。
#### Scenario: 指定时间范围查询指标
#### Scenario: 获取目标指标
- **WHEN** 客户端请求 `GET /api/targets/1/metrics?from=ISO&to=ISO&bucket=1h` 且该目标为活跃目标
- **THEN** 系统 SHALL 返回 targetId、window、stats、trend 字段
- **THEN** 系统 SHALL 返回 JSON 对象包含 targetId、window、stats、trend 字段
#### Scenario: from 或 to 参数缺失
- **WHEN** 客户端请求 `GET /api/targets/1/metrics` 未提供 from 或 to 参数
@@ -101,6 +101,134 @@ Dashboard API SHALL 返回基于时间窗口计算的目标统计和连续状态
- **WHEN** 客户端请求 `GET /api/targets/1/metrics?from=ISO&to=ISO&bucket=5m`
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
### Requirement: 指标统计字段
单目标指标 API SHALL 返回基于时间窗口计算的完整统计字段。
#### Scenario: stats 字段
- **WHEN** metrics 响应包含 stats
- **THEN** stats SHALL 包含 totalChecks、upChecks、downChecks、availability、avgDurationMs、p95DurationMs、p99DurationMs、mttr、longestOutage、incidentCount、currentStreak 字段
#### Scenario: trend 字段
- **WHEN** metrics 响应包含 trend
- **THEN** trend SHALL 为数组,每个元素包含 bucketStart、avgDurationMs、minDurationMs、maxDurationMs、availability、totalChecks、upChecks、downChecks 字段
### Requirement: P95/P99 延迟计算
系统 SHALL 在后端应用层计算 P95 和 P99 延迟百分位数。
#### Scenario: 正常计算 P95
- **WHEN** 时间窗口内存在成功检查记录matched=1 且 duration_ms 不为 null
- **THEN** 系统 SHALL 取出所有成功检查的 duration_ms在后端应用层排序后取第 95 百分位值返回为 p95DurationMs
#### Scenario: 正常计算 P99
- **WHEN** 时间窗口内存在成功检查记录
- **THEN** 系统 SHALL 取第 99 百分位值返回为 p99DurationMs
#### Scenario: 无成功检查记录
- **WHEN** 时间窗口内无 matched=1 且 duration_ms 不为 null 的记录
- **THEN** p95DurationMs 和 p99DurationMs SHALL 返回 null
#### Scenario: 百分位计算方法
- **WHEN** 计算第 N 百分位
- **THEN** 系统 SHALL 将 duration_ms 升序排列,取 index = ceil(count * N / 100) - 1 位置的值
### Requirement: MTTR 计算
系统 SHALL 在后端应用层计算平均恢复时间Mean Time To Recovery
#### Scenario: 存在已恢复的故障段
- **WHEN** 时间窗口内存在至少一个已恢复的故障段(连续 matched=0 后跟 matched=1
- **THEN** 系统 SHALL 计算所有已恢复故障段的平均持续时间(从首个 matched=0 的 timestamp 到恢复后首个 matched=1 的 timestamp 之差),返回为 mttr毫秒
#### Scenario: 无已恢复的故障段
- **WHEN** 时间窗口内无已恢复的故障段(全部正常,或当前仍在故障中且无历史恢复)
- **THEN** mttr SHALL 返回 null
#### Scenario: 当前正在故障中
- **WHEN** 时间窗口内最后一段故障尚未恢复
- **THEN** 该未恢复的故障段 SHALL 不计入 MTTR 平均值
#### Scenario: 窗口起始即为故障且后续恢复
- **WHEN** 时间窗口内第一条记录即为 matched=0故障跨越了 from 边界),且该故障段在窗口内恢复
- **THEN** 该故障段 SHALL 不计入 MTTR 平均值(因无法确定真实故障开始时间),但 SHALL 计入 incidentCount
### Requirement: 最长故障时长
系统 SHALL 在后端应用层计算时间窗口内最长的单次故障持续时间。
#### Scenario: 存在故障段
- **WHEN** 时间窗口内存在故障段
- **THEN** 系统 SHALL 返回最长故障段的持续时间为 longestOutage毫秒
#### Scenario: 无故障
- **WHEN** 时间窗口内无 matched=0 的记录
- **THEN** longestOutage SHALL 返回 null
#### Scenario: 窗口起始即为故障
- **WHEN** 时间窗口内第一条记录即为 matched=0
- **THEN** 该故障段的持续时间 SHALL 从 from 参数开始计算
#### Scenario: 当前正在故障中
- **WHEN** 最后一段故障尚未恢复
- **THEN** 该故障段的持续时间 SHALL 计算为从故障开始到时间窗口 to 参数的时间差
### Requirement: 故障事件计数
系统 SHALL 在后端应用层计算时间窗口内的故障事件次数。
#### Scenario: 计算故障事件数
- **WHEN** 时间窗口内存在状态翻转matched 从 1 变为 0
- **THEN** 系统 SHALL 返回翻转次数为 incidentCount
#### Scenario: 无故障事件
- **WHEN** 时间窗口内所有检查均为 matched=1
- **THEN** incidentCount SHALL 返回 0
#### Scenario: 窗口起始即为故障
- **WHEN** 时间窗口内第一条记录即为 matched=0 且无前序记录可判断翻转
- **THEN** 该故障 SHALL 计为 1 次事件
#### Scenario: 连续异常只计一次
- **WHEN** 某目标连续 10 次 matched=0
- **THEN** 该连续异常段 SHALL 仅计为 1 次事件
### Requirement: 当前连续状态
系统 SHALL 返回目标当前的连续状态信息。
#### Scenario: 当前连续正常
- **WHEN** 目标最近的检查记录连续为 matched=1
- **THEN** currentStreak SHALL 返回 `{ up: true, count: N }`N 为连续正常的检查次数
#### Scenario: 当前连续异常
- **WHEN** 目标最近的检查记录连续为 matched=0
- **THEN** currentStreak SHALL 返回 `{ up: false, count: N }`N 为连续异常的检查次数
#### Scenario: 连续状态达到取数上限
- **WHEN** 连续状态次数达到后端取数或计算上限
- **THEN** currentStreak SHALL 返回 `{ up: boolean, count: N, capped: true }`,前端据此展示上限标记
#### Scenario: 无检查记录
- **WHEN** 目标没有任何检查记录
- **THEN** currentStreak SHALL 返回 null
### Requirement: 趋势数据应用层分桶
系统 SHALL 在后端应用层按 UTC 小时分桶生成趋势数据。
#### Scenario: 按小时生成趋势
- **WHEN** metrics 请求 bucket=`1h`
- **THEN** 系统 SHALL 按 UTC 小时生成 trend 数组,每个点包含该小时内的 totalChecks、upChecks、downChecks、availability、avgDurationMs、minDurationMs、maxDurationMs
#### Scenario: 小时内无成功检查
- **WHEN** 某小时内存在检查记录但无成功检查记录
- **THEN** avgDurationMs、minDurationMs、maxDurationMs SHALL 返回 nullavailability SHALL 基于 upChecks/totalChecks 返回 0
#### Scenario: 小时内无检查记录
- **WHEN** 某小时内没有任何检查记录
- **THEN** 系统 MAY 不返回该小时对应的 trend 点
### Requirement: 无数据口径
系统 SHALL 对无数据窗口返回稳定的空指标口径。
#### Scenario: 窗口内无检查记录
- **WHEN** 指定时间窗口内没有任何检查记录
- **THEN** stats SHALL 返回 totalChecks=0、upChecks=0、downChecks=0、availability=0、avgDurationMs=null、p95DurationMs=null、p99DurationMs=null、mttr=null、longestOutage=null、incidentCount=0、currentStreak=nulltrend SHALL 返回空数组
### Requirement: 历史记录 API
系统 SHALL 保留 `GET /api/targets/:id/history` 端点,支持时间范围筛选和分页返回指定目标的拨测记录。仅活跃目标的历史记录 SHALL 可查询。
@@ -221,16 +349,20 @@ Dashboard API SHALL 返回基于时间窗口计算的目标统计和连续状态
- **THEN** API SHALL 返回 failure 为 null
### Requirement: Meta 信息 API
系统 SHALL 提供 `GET /api/meta` 端点,返回系统运行时元数据。未匹配 method SHALL 按 API 通配符处理为 JSON 404,不再保留自定义 HEAD/405 语义
系统 SHALL 提供 `GET /api/meta` 端点,返回系统运行时元数据,包括应用版本号和 checker 类型列表。未匹配 method SHALL 按 API 通配符处理为 JSON 404。
#### Scenario: 获取 checker 类型列表
#### Scenario: 获取 checker 类型列表和版本号
- **WHEN** 客户端请求 `GET /api/meta`
- **THEN** 系统 SHALL 返回 JSON `{ checkerTypes: string[] }`,包含所有已注册的 checker 类型标识符
- **THEN** 系统 SHALL 返回 JSON `{ checkerTypes: string[], version: string }`,其中 `checkerTypes` 包含所有已注册的 checker 类型标识符,`version` 为当前运行实例的 `MAJOR.MINOR.PATCH` 应用版本
#### Scenario: 类型列表来源
- **WHEN** 系统启动并注册了 checker
- **THEN** `/api/meta` 返回的 `checkerTypes` SHALL 与 `CheckerRegistry.supportedTypes` 完全一致
#### Scenario: 版本号来源
- **WHEN** 系统启动并确定应用版本
- **THEN** `/api/meta` 返回的 `version` SHALL 与启动时注入的应用版本完全一致
#### Scenario: 不支持的 method 请求
- **WHEN** 客户端使用 POST/PUT/DELETE/HEAD 等未声明 method 请求 `/api/meta`
- **THEN** `/api/*` 通配符 SHALL 返回 JSON 404 响应
@@ -240,4 +372,4 @@ Dashboard API SHALL 返回基于时间窗口计算的目标统计和连续状态
#### Scenario: MetaResponse 类型定义
- **WHEN** 前后端引用 `MetaResponse` 类型
- **THEN** 该类型 SHALL 包含 `checkerTypes: string[]` 字段
- **THEN** 该类型 SHALL 包含 `checkerTypes: string[]``version: string` 字段

View File

@@ -447,12 +447,40 @@ JSON Schema 契约 SHALL 覆盖业务无关的结构规则,包括字段类型
- **THEN** 系统 SHALL 将默认值 `./data` 基于 configDir 解析为绝对路径
### Requirement: target 通用元信息字段约束
系统 SHALL 在 YAML target 通用字段中支持 `description` 字段,并`id``name``description` 执行契约校验。`id` MUST 为 1 到 30 个字符`name` MUST 为 null 或 1 到 30 个字符的字符串,且语义校验 SHALL 拒绝仅包含空白字符的 name。`description` MUST 为 null 或不超过 500 个字符的字符串,且 MAY 为空字符串。
系统 SHALL 在 YAML target 通用字段中对 `id``name``description` 执行契约校验。`id` MUST 为 1 到 30 个字符,符合 `[a-zA-Z0-9][a-zA-Z0-9_-]*` 命名规则MUST 在所有 targets 中全局唯一MUST NOT 参与变量替换。`name` MUST 为 null 或 1 到 30 个字符的字符串支持变量替换MUST NOT 要求全局唯一MUST NOT 参与 target 唯一性判定,语义校验 SHALL 拒绝仅包含空白字符的 name。`description` MUST 为 null 或不超过 500 个字符的字符串,支持变量替换,且 MAY 为空字符串MUST NOT 参与 target 唯一性判定。`name` 为 null 时前端展示 SHALL 使用 `name ?? id` 作为目标名称文案,但该 fallback MUST NOT 改变 target 本身的 name 值
#### Scenario: description 字段解析
- **WHEN** 系统读取包含 `description: "检查生产 API 健康状态"` 的 target
- **THEN** 系统 SHALL 将该字段解析为 target 的目标说明
#### Scenario: id 包含下划线和连字符
- **WHEN** target 配置 `id: "db_check-01"`
- **THEN** 系统 SHALL 使用该 id 作为 target 的唯一标识符
#### Scenario: id 不合法报错
- **WHEN** target 配置 `id: "_invalid"``id: "-start"``id: "has space"`
- **THEN** 系统 SHALL 以配置错误退出,提示 id 不符合命名规则
#### Scenario: id 重复报错
- **WHEN** 两个 target 配置相同的 `id: "api-health"`
- **THEN** 系统 SHALL 以配置错误退出,提示 id 重复
#### Scenario: id 不参与变量替换
- **WHEN** target 配置 `id: "${VAR}"` 形式的变量引用
- **THEN** 系统 SHALL NOT 对 id 执行变量替换
#### Scenario: name 使用变量
- **WHEN** target 配置 `name: "${env} API 健康检查"` 且 variables 中 `env: "生产"`
- **THEN** 系统 SHALL 将 name 解析为 "生产 API 健康检查"
#### Scenario: name 显式 null
- **WHEN** target 配置 `id: "api-health"``name: null`
- **THEN** 系统 SHALL 接受该配置,并在解析、存储和 API 响应中保留 name 为 null
#### Scenario: name 为 null 时前端展示 fallback
- **WHEN** 前端展示 name 为 null 的 target
- **THEN** 前端 SHALL 显示该 target 的 id 作为目标名称文案
#### Scenario: name 为 null 通过校验
- **WHEN** 系统读取包含 `name: null` 或省略 `name` 的 target
- **THEN** 系统 SHALL 接受该配置
@@ -489,6 +517,33 @@ JSON Schema 契约 SHALL 覆盖业务无关的结构规则,包括字段类型
- **WHEN** target 的 `description` 通过变量替换后超过 500 个字符
- **THEN** 系统 SHALL 在契约校验阶段以错误退出,提示 description 字段长度错误
### Requirement: target 分组
系统 SHALL 支持在每个 target 上配置可选的 `group` 字段用于将目标归类到不同分组。未指定 `group` 的 target SHALL 归入 `"default"` 分组。系统 SHALL 保证 "default" 分组始终排在最前面,其余分组按配置文件中首次出现的顺序排列。系统 SHALL 在 API 响应中返回每个 target 的分组信息。系统 SHALL 在数据库 targets 表中持久化每个 target 的分组信息。
#### Scenario: 配置分组名称
- **WHEN** YAML 配置中某个 target 指定 `group: "搜索引擎"`
- **THEN** 系统 SHALL 将该 target 归类到 "搜索引擎" 分组
#### Scenario: 不配置分组
- **WHEN** YAML 配置中某个 target 未指定 `group` 字段
- **THEN** 系统 SHALL 将该 target 归类到 "default" 分组
#### Scenario: default 分组排最前
- **WHEN** 配置中存在多个分组(包括 "default" 和自定义分组)
- **THEN** API 返回的目标列表中 "default" 分组的目标 SHALL 排在其他分组之前
#### Scenario: 自定义分组按出现顺序
- **WHEN** 配置中 "搜索引擎" 分组在 "后端服务" 分组之前首次出现
- **THEN** API 返回中 "搜索引擎" 分组 SHALL 排在 "后端服务" 分组之前
#### Scenario: targets 列表包含分组
- **WHEN** 客户端请求 `GET /api/targets`
- **THEN** 响应中每个目标 SHALL 包含 `group` 字段,值为该目标所属的分组名称
#### Scenario: 持久化分组信息
- **WHEN** 系统同步 targets 到数据库
- **THEN** 每个 target 的 `grp` 列 SHALL 存储其分组名称,未配置分组的存储 `"default"`
### Requirement: 配置 schema 导出包含 target 元信息约束
系统 SHALL 在导出的 Authoring `probe-config.schema.json` 中包含 target `id``name``description` 的长度约束和可空类型,用于编辑器提示和外部校验。导出 schema SHALL 面向用户可书写规则文件,因此还 SHALL 接受支持变量替换字段中的完整变量引用字符串和 expect 简写。

View File

@@ -1,57 +0,0 @@
## Purpose
定义拨测系统前端 Dashboard 页面总览统计卡片、Dashboard 数据查询、加载和错误状态处理。页面骨架布局见 `dashboard-layout`,分组表格布局见 `target-table`,目标详情 Drawer 见 `target-detail-drawer`,数据轮询和缓存见 `tanstack-query-data-layer`
## Requirements
### Requirement: Dashboard 数据查询
Dashboard SHALL 通过 `GET /api/dashboard` 获取首屏总览统计和目标列表数据。
#### Scenario: 查询 Dashboard 数据
- **WHEN** 页面处于打开状态
- **THEN** 前端 SHALL 使用 TanStack Query 请求 `GET /api/dashboard?window=24h&recentLimit=30`
#### Scenario: 统计数据自动刷新
- **WHEN** 页面处于打开状态
- **THEN** Dashboard 数据 SHALL 通过 TanStack Query 的 refetchInterval=8000 自动刷新
#### Scenario: 元信息独立查询
- **WHEN** 页面需要 checker 类型列表
- **THEN** 前端 SHALL 继续通过 `GET /api/meta` 独立查询 checkerTypes
### Requirement: 总览统计卡片
Dashboard SHALL 在页面顶部使用单个 TDesign Card 组件内嵌一行居中的 Statistic 展示总览统计,包含总目标数、正常数、异常数和窗口异常事件数。
#### Scenario: 展示统计卡片
- **WHEN** 用户打开 Dashboard 页面
- **THEN** 页面顶部 SHALL 使用单个 TDesign Card无 shadow、无 bordered内嵌 TDesign Row/Col 布局展示 4 个居中的 Statistic全部目标数color=blue、正常目标数color=green、异常目标数color=red、24h 异常事件数color=orange
#### Scenario: 指标居中显示
- **WHEN** SummaryCards 渲染
- **THEN** 每个 Statistic 所在的 Col SHALL 使用 `.summary-stat-col` 类实现标题和数字居中对齐
#### Scenario: 异常事件数据来源
- **WHEN** SummaryCards 渲染 24h 异常事件数
- **THEN** 该数值 SHALL 使用 DashboardResponse.summary.incidents 字段,标题 SHALL 基于当前 window 展示为"24h 异常事件数"
### Requirement: 页面加载与错误状态
Dashboard SHALL 使用 TDesign Skeleton 组件处理首次加载状态,使用 Alert 处理错误。
#### Scenario: 首次加载
- **WHEN** 页面首次加载且数据尚未返回
- **THEN** 页面 SHALL 使用 TDesign Skeleton 组件animation="gradient")展示页面骨架,模拟 Summary 区域和 Table 区域的大致结构
#### Scenario: API 请求失败
- **WHEN** 前端 API 请求失败
- **THEN** 页面 SHALL 使用 TDesign Alert 组件theme=error显示错误提示
### Requirement: 前端构建产物拆分
前端生产构建 SHALL 将 vendor 依赖拆分为独立 chunk利用浏览器并行加载和长期缓存。
#### Scenario: vendor chunk 拆分
- **WHEN** 执行前端生产构建
- **THEN** 构建产物 SHALL 包含独立的 vendor chunkreact、tdesign、recharts 各自独立),而非单个 bundle
#### Scenario: 业务代码变更不影响 vendor 缓存
- **WHEN** 仅修改业务代码src/web/ 下非 node_modules 文件)并重新构建
- **THEN** vendor chunk 的文件名(含 hashSHALL 保持不变,浏览器缓存 SHALL 继续有效

View File

@@ -1,6 +1,6 @@
## Purpose
定义拨测调度引擎的行为:按 interval 分组定时、组内并发拨测、expect 结果校验结果持久化。
定义拨测调度引擎的行为:按 interval 分组定时、组内并发拨测、通用 expect 校验结果持久化和定期数据清理。各 checker 类型的执行语义和专属 expect 校验规则定义在各自 checker 的规范中
## Requirements
@@ -38,181 +38,24 @@
- **WHEN** 调度器同时触发 10 个目标且 probes.execution.maxConcurrentChecks 为 3
- **THEN** 系统 MUST 同时最多运行 3 个检查,其余检查等待并发槽位释放
### Requirement: HTTP 拨测执行
系统 SHALL 对 `type: http` 的目标执行 HTTP 请求,支持 GET、POST、PUT、DELETE、PATCH、HEAD、OPTIONS 方法,并携带 `http.headers``http.body`。系统 SHALL 支持 `http.ignoreSSL` 配置跳过 SSL 证书校验,支持 `http.maxRedirects` 配置控制重定向行为。HTTP response body 读取 SHALL 受 `http.maxBodyBytes` 流式限制,重定向跟随 SHALL 释放被跟随响应的 body。
#### Scenario: 执行 GET 请求
- **WHEN** 目标配置 method 为 GET
- **THEN** 系统 SHALL 发送 GET 请求到目标 URL
#### Scenario: 执行 POST 请求带 body
- **WHEN** 目标配置 method 为 POST 且指定了 body 和 Content-Type header
- **THEN** 系统 SHALL 发送带指定 body 的 POST 请求
#### Scenario: 携带自定义 headers
- **WHEN** 目标配置了 headers如 Authorization
- **THEN** 系统 SHALL 在请求中包含所有配置的 headers
#### Scenario: HTTP body 读取上限
- **WHEN** HTTP response body 超过该 target 的 maxBodyBytes
- **THEN** 系统 MUST 停止继续读取 response body并记录 `matched=false``failure.kind="error"``failure.phase="body"` 的结构化输出超限错误
#### Scenario: HTTP body 大小等于上限
- **WHEN** HTTP response body 的字节数等于该 target 的 maxBodyBytes
- **THEN** 系统 SHALL 允许该 body 进入后续解码和 expect 校验
#### Scenario: HTTP body 上限为 0
- **WHEN** HTTP target 配置 maxBodyBytes 为 0 且响应体非空
- **THEN** 系统 SHALL 停止读取并记录 body 超限错误
#### Scenario: 忽略 SSL 证书校验
- **WHEN** 目标配置 `http.ignoreSSL: true` 且目标 URL 为 HTTPS
- **THEN** 系统 SHALL 跳过 SSL 证书校验,即使证书无效也正常完成请求
#### Scenario: 不忽略 SSL 证书校验
- **WHEN** 目标未配置 `http.ignoreSSL` 或配置为 `false`,且目标 URL 使用自签名证书
- **THEN** 系统 SHALL 因 SSL 证书校验失败而记录请求错误
#### Scenario: 默认不跟随重定向
- **WHEN** 目标未配置 `http.maxRedirects` 或配置为 0且服务端返回 301/302
- **THEN** 系统 SHALL 不跟随重定向,直接返回 301/302 的响应状态码和响应头
#### Scenario: 配置跟随重定向
- **WHEN** 目标配置 `http.maxRedirects: 5` 且服务端返回重定向
- **THEN** 系统 SHALL 跟随重定向,最多跟随 5 次,并在跟随前释放当前重定向响应的 body
#### Scenario: 超过最大重定向次数
- **WHEN** 目标配置 `http.maxRedirects: 1` 且服务端连续返回两次重定向
- **THEN** 系统 SHALL 只跟随第一次重定向,并返回第二次重定向响应的状态码和响应头
#### Scenario: POST 重定向改 GET
- **WHEN** POST 请求遇到 301/302 或任意方法请求遇到 303且系统决定按 GET 跟随重定向
- **THEN** 系统 SHALL 移除请求 body并清理 content-type、content-length 等 body 相关 headers 后发起后续 GET 请求
#### Scenario: 跨 origin 重定向敏感 header
- **WHEN** HTTP 请求跟随重定向到不同 origin
- **THEN** 系统 SHALL NOT 将 authorization、cookie 等敏感 headers 转发到新的 origin
#### Scenario: 响应体编码自动检测
- **WHEN** HTTP 响应的 `Content-Type` header 包含 `charset=gbk``charset="gbk"`
- **THEN** 系统 SHALL 使用 GBK 编码解码响应体,而非硬编码 UTF-8
#### Scenario: 响应体编码回退 UTF-8
- **WHEN** HTTP 响应的 `Content-Type` header 未指定 charset
- **THEN** 系统 SHALL 使用 UTF-8 编码解码响应体
#### Scenario: 响应体编码不支持
- **WHEN** HTTP 响应的 `Content-Type` header 指定了当前运行时不支持的 charset
- **THEN** 系统 SHALL 记录 `matched=false``failure.kind="error"``failure.phase="body"` 的解码失败结果
### Requirement: 请求超时控制
系统 SHALL 对每次 checker 执行实施超时控制,超时时间使用目标配置的 timeout 值。
系统 SHALL 对每次 checker 执行实施超时控制,超时时间使用目标配置的 timeout 值。引擎 SHALL 通过 AbortController 向 checker 注入超时 signal。
#### Scenario: HTTP 请求超时
- **WHEN** HTTP 请求在 timeout 时间内未收到响应
- **THEN** 系统 SHALL 中止该请求,记录为失败并标注超时错误
#### Scenario: cmd 执行超时
- **WHEN** cmd 进程在 timeout 时间内未退出
- **THEN** 系统 MUST 终止该子进程,记录为失败并标注超时错误
#### Scenario: 请求在超时前完成
#### Scenario: checker 在超时前完成
- **WHEN** checker 在超时前完成执行
- **THEN** 系统 SHALL 正常记录执行结果并进入 expect 校验
#### Scenario: checker 执行超时
- **WHEN** checker 在 timeout 时间内未完成执行
- **THEN** 系统 SHALL 中止该检查,记录为失败并标注超时错误
### Requirement: expect 校验
系统 SHALL 在 checker 执行完成后根据目标类型的 Resolved expect 执行计划校验观测结果,校验结果和首个失败原因记入 check result。HTTP checker `durationMs` SHALL 表示完整 checker 执行耗时,包括重定向、响应体读取、响应体解码和 expect 校验。HTTP `expect.durationMs` SHALL 使用 `RawValueExpectation` 输入并在 resolve 阶段转换为运行期 `ValueExpectation`;旧 `expect.maxDurationMs` MUST NOT 再作为运行期耗时阈值使用
#### Scenario: HTTP 默认状态码
- **WHEN** HTTP target 未配置 `expect.status`
- **THEN** 系统 SHALL 在 Resolved HTTP expect 中物化默认 `status: [200]` 并按该语义校验响应状态码
#### Scenario: 校验 HTTP 状态码精确值
- **WHEN** HTTP target 配置了 `expect.status: [200, 201]`
- **THEN** 系统 SHALL 检查响应状态码是否在列表中,将匹配结果记录到 matched 字段
#### Scenario: 校验 HTTP 状态码范围模式
- **WHEN** HTTP target 配置了 `expect.status: ["2xx"]`
- **THEN** 系统 SHALL 检查响应状态码是否在 200-299 范围内
#### Scenario: 校验 HTTP 状态码混合模式
- **WHEN** HTTP target 配置了 `expect.status: ["2xx", 301]`,且响应状态码为 204
- **THEN** 系统 SHALL 判定状态码匹配204 属于 2xx 范围)
#### Scenario: 校验 HTTP 响应头
- **WHEN** HTTP target 配置了 `expect.headers: {"Content-Type": {contains: "application/json"}}`
- **THEN** 系统 SHALL 检查响应头是否符合指定规则,全部匹配时继续后续阶段
#### Scenario: 校验 HTTP 响应体
- **WHEN** HTTP target 配置了有序 `expect.body` ContentExpectations 数组
- **THEN** 系统 SHALL 按数组顺序执行 body expectations任一失败立即记录 failure 并停止后续 expectation
#### Scenario: 校验 HTTP 完整耗时阈值
- **WHEN** 目标配置了 `expect.durationMs: {lte: 1000}`,且 HTTP checker 完整执行含重定向、body 读取、解码和 expect后的 durationMs 超过阈值
- **THEN** 系统 SHALL 判定 duration 不匹配,记录完整 durationMs 和 duration failure
#### Scenario: HTTP body 前耗时已不可能满足 durationMs 上界
- **WHEN** HTTP target 配置了 body 校验和 `expect.durationMs` 上界 matcher`{lte: 1000}`),且进入 body 读取前的已耗时已使该 matcher 不可能通过
- **THEN** 系统 SHALL 直接返回 duration failure且 MUST NOT 读取 response body
#### Scenario: HTTP body 失败优先于后续 duration 检查
- **WHEN** HTTP target 配置了 body 校验和 `expect.durationMs: {lte: 1000}`body 阶段存在失败,且完整执行后 duration 也超过阈值
- **THEN** 系统 SHALL 返回 body 阶段的失败首个失败为准durationMs SHALL 记录完整耗时
#### Scenario: HTTP 慢响应体计入耗时
- **WHEN** HTTP target 配置了 body 校验和 `expect.durationMs: {lte: 1000}`,且响应头很快返回但响应体读取导致完整执行耗时超过阈值
- **THEN** 系统 SHALL 判定 duration 不匹配并记录完整 durationMs
系统 SHALL 在 checker 执行完成后根据目标类型的 Resolved expect 执行计划校验观测结果,校验结果和首个失败原因记入 check result。 checker 类型 SHALL 定义各自的 expect 执行顺序、默认状态语义和快速失败策略。`durationMs` SHALL 表示完整 checker 执行耗时
#### Scenario: 多条 expect 规则
- **WHEN** 目标同时配置状态、duration、元数据和内容 expectations
- **THEN** 系统 SHALL 所有 expectations 全部通过时 matched 为 true任一不通过则为 false 并记录首个失败原因
#### Scenario: cmd 默认 exitCode
- **WHEN** cmd target 未配置 `expect.exitCode`
- **THEN** 系统 SHALL 在 Resolved cmd expect 中物化默认 `exitCode: [0]` 并按该语义校验命令退出码
#### Scenario: 校验 cmd stdout
- **WHEN** cmd target 配置了有序 `expect.stdout` ContentExpectations 数组
- **THEN** 系统 SHALL 按数组顺序执行 stdout expectations任一失败立即记录 failure 并停止后续 expectation
### Requirement: Body 校验按需解析
系统 SHALL 仅在 HTTP target 配置了 body 校验,且 status、headers 阶段均通过,并且进入 body 前未确定 `expect.durationMs` 已失败时才读取并解析响应体避免不必要的读取和解析开销。HTTP target 未配置 body 校验时,系统 SHALL NOT 读取 response body。仅当 Resolved `durationMs` 包含上界 matcher 且当前已耗时已经使其不可能通过时,系统 MAY 在读取 body 前返回 duration failure其他 duration matcher SHALL 在完整执行耗时可用后校验。
#### Scenario: status 失败时不读取 body
- **WHEN** HTTP target 的 status 阶段不匹配
- **THEN** 系统 SHALL 立即返回 matched=false且 MUST NOT 读取 response body
#### Scenario: headers 失败时不读取 body
- **WHEN** HTTP target 的 status 阶段匹配但 headers 阶段不匹配
- **THEN** 系统 SHALL 立即返回 matched=false且 MUST NOT 读取 response body
#### Scenario: 进入 body 前 durationMs 上界已失败时不读取 body
- **WHEN** HTTP target 已配置 `expect.durationMs` 上界 matcher且进入 body 读取前的已耗时已经使该 matcher 不可能通过
- **THEN** 系统 SHALL 返回 duration failure且 MUST NOT 读取 response body
#### Scenario: 仅配置 contains 时不解析 JSON
- **WHEN** HTTP target 仅配置 body contains expectation 而未配置 json/css/xpath expectation
- **THEN** 系统 SHALL 不执行 JSON.parse 或 HTML/XML 解析
#### Scenario: 配置 json 时解析 JSON 失败
- **WHEN** HTTP target 配置了 body json expectation 但响应体不是合法 JSON
- **THEN** 系统 SHALL 判定 matched 为 false并记录 json expectation 对应的 failure.path
### Requirement: HTTP 运行期错误归属
HTTP checker SHALL 将运行期失败归属到实际失败阶段。请求、网络、TLS 和 timeout 错误 SHALL 记录为 request 阶段错误body 超限、响应体解码失败、响应内容解析失败 SHALL 记录为 body 阶段错误expect 不匹配 SHALL 记录为对应 mismatch 阶段。
#### Scenario: 请求错误归属 request
- **WHEN** HTTP 请求因为网络、TLS 或 timeout 失败
- **THEN** 系统 SHALL 记录 `matched=false``failure.kind="error"``failure.phase="request"`
#### Scenario: body 超限归属 body
- **WHEN** HTTP response body 超过 maxBodyBytes
- **THEN** 系统 SHALL 记录 `failure.kind="error"``failure.phase="body"``failure.path="body"`
#### Scenario: body 解析错误归属 body
- **WHEN** HTTP response body 已读取但解码、JSON 解析、CSS 解析或 XPath 解析失败
- **THEN** 系统 SHALL 记录 `failure.phase="body"`,且 SHALL NOT 将该失败记录为 request 错误
### Requirement: 拨测结果记录
系统 SHALL 在每次 checker 完成后,将结果写入 SQLite 数据存储,包含 target_id、timestamp、matched、duration_ms、observation、failure 字段。detail SHALL 为 API 层派生字段,不写入存储层;系统 SHALL NOT 写入 status_detail 字段。
@@ -229,15 +72,11 @@ HTTP checker SHALL 将运行期失败归属到实际失败阶段。请求、网
- **THEN** 系统 SHALL 记录 matched=false、observation、failure.kind="mismatch" 和具体不匹配信息
### Requirement: runner 选择
系统 SHALL 根据 target.type 选择对应 runner 执行检查。
系统 SHALL 根据 target.type 通过 CheckerRegistry 选择对应 checker 执行检查。
#### Scenario: 选择 HTTP runner
- **WHEN** target.type 为 `http`
- **THEN** 系统 SHALL 使用 HTTP runner 执行该目标
#### Scenario: 选择 cmd runner
- **WHEN** target.type 为 `cmd`
- **THEN** 系统 SHALL 使用 cmd runner 执行该目标
#### Scenario: 根据 type 选择 checker
- **WHEN** target.type 为已注册的 checker 类型
- **THEN** 系统 SHALL 通过 `checkerRegistry.get(type)` 获取对应 checker 执行
### Requirement: 定期数据清理
ProbeEngine SHALL 在启动时注册数据清理定时器,定期调用 ProbeStore.prune() 清理过期数据和空壳非活跃目标。

View File

@@ -1,104 +0,0 @@
## Purpose
定义 Header 刷新频率选择器组件的交互行为:频率切换、倒计时显示、手动刷新按钮、布局稳定性。
## Requirements
### Requirement: 刷新频率选择器
HeadMenu operations 区域 SHALL 提供 RadioGroup 组件供用户选择刷新频率。
#### Scenario: RadioGroup 渲染
- **WHEN** Dashboard 页面渲染
- **THEN** HeadMenu operations 区域 SHALL 显示 RadioGrouptheme="button", variant="default-filled"选项为手动、10秒、30秒、1分钟、5分钟
#### Scenario: 默认选中
- **WHEN** 页面首次加载
- **THEN** RadioGroup SHALL 默认选中"30秒"
#### Scenario: 切换频率立即刷新
- **WHEN** 用户切换刷新频率选项
- **THEN** 系统 SHALL 立即触发一次数据刷新,然后应用新的刷新间隔
### Requirement: 倒计时显示
RadioGroup 右侧 SHALL 显示距下次自动刷新的倒计时。倒计时逻辑 SHALL 封装在独立的 `RefreshCountdown` 组件中App 组件 SHALL NOT 持有每秒更新的 `now` state。自动倒计时数字 SHALL 使用 `@number-flow/react` 提供滚动过渡,非倒计时状态 SHALL 保持普通文本或按钮语义。
#### Scenario: RefreshCountdown 组件封装
- **WHEN** Dashboard 页面渲染
- **THEN** 倒计时显示 SHALL 由独立的 `RefreshCountdown` 组件负责,该组件内部持有 `now` state 和每秒 `setInterval`,渲染边界限制在该组件内部
#### Scenario: RefreshCountdown props
- **WHEN** RefreshCountdown 组件渲染
- **THEN** 组件 SHALL 接收 `dashboardUpdatedAt: number``refreshInterval: number``isFetching: boolean``isManualRefresh: boolean``onRefresh: () => void` 作为 props
#### Scenario: NumberFlow 数字滚动
- **WHEN** 自动刷新模式下已完成首次刷新且当前未处于刷新中状态
- **THEN** 倒计时数字 SHALL 使用 `@number-flow/react``NumberFlow` 渲染,并使用向下滚动趋势表达倒计时递减
#### Scenario: 秒级间隔格式
- **WHEN** 自动刷新间隔小于 60 秒
- **THEN** 倒计时 SHALL 显示为"xx秒"格式(如"26秒"
#### Scenario: 分钟级稳定格式
- **WHEN** 自动刷新间隔大于等于 60 秒
- **THEN** 倒计时 SHALL 显示为"x分xx秒"格式,秒数 SHALL 固定为两位(如"4分30秒"、"0分09秒"
#### Scenario: 时间数字边界
- **WHEN** 分钟级倒计时中的秒数在 59 到 00 边界变化
- **THEN** 秒数十位 SHALL 按时间显示规则限制在 0 到 5 之间滚动
#### Scenario: 无前缀
- **WHEN** 倒计时显示
- **THEN** 可见倒计时文本 SHALL 不包含任何前缀(如"下一次刷新:"),直接显示时间
#### Scenario: 可访问文本
- **WHEN** NumberFlow 倒计时渲染
- **THEN** 倒计时容器 SHALL 暴露与当前倒计时等价的可访问文本,供测试和辅助技术读取
#### Scenario: 刷新中状态
- **WHEN** 数据正在刷新isFetching=true 且 isLoading=false
- **THEN** 倒计时文本 SHALL 显示为"刷新中..."
#### Scenario: 等待首次刷新状态
- **WHEN** 自动刷新模式下尚未完成首次刷新
- **THEN** 倒计时文本 SHALL 显示为"等待首次刷新"
### Requirement: App 组件渲染隔离
App 组件 SHALL NOT 持有任何高频更新的 state如每秒更新的时钟确保 App 的重渲染频率与数据刷新频率一致(默认 30 秒一次)。
#### Scenario: App 无 now state
- **WHEN** App 组件渲染
- **THEN** App SHALL NOT 包含 `useState` 管理的时钟 state也 SHALL NOT 包含每秒触发的 `setInterval`
#### Scenario: App 重渲染频率
- **WHEN** Dashboard 处于自动刷新模式
- **THEN** App 组件的重渲染 SHALL 仅由 TanStack Query 的 refetch 触发(频率等于用户选择的刷新间隔),而非每秒触发
### Requirement: 手动刷新按钮
选择"手动"模式时,倒计时区域 SHALL 替换为刷新按钮。
#### Scenario: 手动模式显示按钮
- **WHEN** 用户选择"手动"刷新频率
- **THEN** 倒计时区域 SHALL 替换为刷新图标按钮
#### Scenario: 点击刷新
- **WHEN** 用户点击刷新按钮
- **THEN** 系统 SHALL 触发一次数据刷新
#### Scenario: 刷新中禁用
- **WHEN** 数据正在刷新
- **THEN** 刷新按钮 SHALL 显示 loading 状态且 disabled防止连续点击
### Requirement: 布局稳定性
倒计时/按钮容器 SHALL 保持布局稳定避免内容变化导致的抖动。NumberFlow 倒计时 SHALL 通过分组同步和等宽数字样式降低位数、单位和动画变化带来的布局偏移。
#### Scenario: 数字等宽
- **WHEN** 倒计时数字变化
- **THEN** 容器和 NumberFlow 倒计时 SHALL 使用 tabular-nums 字体特性,确保数字等宽不抖动
#### Scenario: NumberFlow 分组同步
- **WHEN** 分钟级倒计时同时渲染分钟和秒数
- **THEN** 分钟和秒数 SHALL 使用 `NumberFlowGroup` 同步布局变化
#### Scenario: 格式切换不抖动
- **WHEN** 倒计时在按钮、秒级文本和分钟级文本之间切换
- **THEN** 容器 SHALL 使用 min-width 确保最小宽度,避免 RadioGroup 位移

View File

@@ -1,50 +0,0 @@
## Purpose
TBD - 统一服务启动引导函数,封装开发和生产模式的完整启动序列。
## Requirements
### Requirement: 统一启动引导函数
系统 SHALL 提供 `src/server/bootstrap.ts` 导出 `bootstrap(options: BootstrapOptions)` 函数,封装完整的服务启动序列:加载配置、初始化正式 logger、创建 store、同步 targets、创建并启动 engine、启动 HTTP server、注册 shutdown handler。配置加载失败发生在正式 logger 初始化之前,系统 SHALL 使用 console fallback 输出启动失败信息。配置加载成功后的启动失败 SHALL 使用正式 logger 输出 `fatal` 后退出。
#### Scenario: 开发模式启动
- **WHEN** `dev.ts` 调用 `bootstrap({ configPath, mode: "development" })`
- **THEN** 系统 SHALL 完成完整启动序列,不传入 staticAssets并初始化运行时 logger
#### Scenario: 生产模式启动(带静态资源)
- **WHEN** code-generated entry 调用 `bootstrap({ configPath, mode: "production", staticAssets })`
- **THEN** 系统 SHALL 完成完整启动序列,将 staticAssets 传递给 startServer并初始化运行时 logger
#### Scenario: 配置加载失败处理
- **WHEN** 配置文件读取、YAML 解析或配置校验失败
- **THEN** 系统 SHALL 通过 console fallback 输出错误信息并以非零退出码退出进程
#### Scenario: 配置加载后的启动失败处理
- **WHEN** logger、store、engine 或 HTTP server 初始化失败
- **THEN** 系统 SHALL 通过正式 logger 输出 `fatal` 日志并以非零退出码退出进程
#### Scenario: 优雅关机
- **WHEN** 进程收到 SIGINT 或 SIGTERM 信号
- **THEN** bootstrap 注册的 shutdown handler SHALL 调用 engine.stop()、store.close() 和 logger.flush() 后退出
### Requirement: BootstrapOptions 接口
`bootstrap` 函数 SHALL 接受 `BootstrapOptions` 参数,包含 `configPath: string``mode: RuntimeMode` 和可选的 `staticAssets?: StaticAssets`
#### Scenario: 最小配置(开发模式)
- **WHEN** 仅传入 configPath 和 mode
- **THEN** 系统 SHALL 正常启动startServer 不接收 staticAssets 参数
#### Scenario: 生产模式配置
- **WHEN** 传入 configPath、mode 和 staticAssets
- **THEN** 系统 SHALL 将 staticAssets 传递给 startServer
### Requirement: dev.ts 和生产入口使用 bootstrap
`dev.ts``src/server/main.ts` SHALL 调用 `bootstrap()` 而非各自维护启动序列。
#### Scenario: dev.ts 调用 bootstrap
- **WHEN** 开发者运行 `bun run dev`
- **THEN** `dev.ts` SHALL 调用 `bootstrap` 完成启动
#### Scenario: main.ts 调用 bootstrap
- **WHEN** 生产可执行文件启动
- **THEN** `main.ts` SHALL 调用 `bootstrap` 完成启动

View File

@@ -1,6 +1,6 @@
## Purpose
定义将 Vite 构建的前端资源通过 code generation 嵌入 Bun 后端打包为单个 standalone executable 的生产构建、运行配置和验证要求。
定义将 Vite 构建的前端资源通过 code generation 嵌入 Bun 后端、静态资源服务与 Content-Type 处理、打包为单个 standalone executable 的生产构建、运行配置和验证要求。
## Requirements
@@ -89,3 +89,63 @@ executable MUST 将环境相关运行时配置保留在嵌入的前端和 server
#### Scenario: 升迁后重新构建
- **WHEN** 开发者先升迁 `package.json.version` 再运行生产构建命令
- **THEN** 新生成的 standalone executable SHALL 返回升迁后的版本号
### Requirement: 构建时资源扫描与 Code Generation
构建脚本 SHALL 在 Vite build 完成后扫描 `dist/web/` 目录,自动生成 TypeScript 文件,为每个静态资源创建 `import ... with { type: "file" }` 声明。
#### Scenario: 生成资源导入文件
- **WHEN** 构建脚本扫描 `dist/web/` 目录
- **THEN** 系统 SHALL 在 `.build/static-assets.ts` 中为每个文件生成 `import fN from "<path>" with { type: "file" }` 语句,并导出 `StaticAssets` 对象
#### Scenario: StaticAssets 对象结构
- **WHEN** `static-assets.ts` 被生成
- **THEN** 导出的对象 SHALL 包含 `indexHtml: Blob``files: Record<string, Blob>` 两个字段,其中 files 的 key 为 URL 路径(如 `/assets/index-a1b2c3.js`
#### Scenario: 生成 production server entry
- **WHEN** 构建脚本生成资源导入文件后
- **THEN** 系统 SHALL 在 `.build/server-entry.ts` 中生成 production 入口import bootstrap、config 和 staticAssets 并调用 bootstrap
### Requirement: 运行时静态资源服务
系统 SHALL 提供 `serveStaticAsset` 函数,根据请求路径从 StaticAssets 中查找并返回对应资源。
#### Scenario: 请求根路径
- **WHEN** 请求路径为 `/`
- **THEN** 系统 SHALL 返回 `indexHtml`Content-Type 为 `text/html; charset=utf-8`Cache-Control 为 `no-cache`
#### Scenario: 请求已知静态资源
- **WHEN** 请求路径匹配 `files` 中的某个 key
- **THEN** 系统 SHALL 返回对应 BlobContent-Type 根据文件扩展名推断Cache-Control 为 `public, max-age=31536000, immutable`
#### Scenario: 请求未知带扩展名路径
- **WHEN** 请求路径包含文件扩展名但未匹配任何已知资源
- **THEN** 系统 SHALL 返回 404 响应
#### Scenario: SPA Fallback
- **WHEN** 请求路径不包含文件扩展名且不以 `/api/` 开头
- **THEN** 系统 SHALL 返回 `indexHtml`SPA fallback
### Requirement: Content-Type 推断
系统 SHALL 根据文件扩展名推断正确的 Content-Type header。
#### Scenario: JavaScript 文件
- **WHEN** 请求路径以 `.js``.mjs` 结尾
- **THEN** Content-Type SHALL 为 `text/javascript; charset=utf-8`
#### Scenario: CSS 文件
- **WHEN** 请求路径以 `.css` 结尾
- **THEN** Content-Type SHALL 为 `text/css; charset=utf-8`
#### Scenario: SVG 文件
- **WHEN** 请求路径以 `.svg` 结尾
- **THEN** Content-Type SHALL 为 `image/svg+xml`
### Requirement: 静态资源 import specifier SHALL 使用平台无关分隔符
构建时静态资源 code generation SHALL 将文件系统相对路径转换为 ESM import specifier并确保生成的 import 路径在 Windows、macOS、Linux 开发环境下都使用 `/` 作为分隔符。
#### Scenario: Windows 相对路径转换为 import specifier
- **WHEN** code generation 将 Windows 文件系统相对路径 `..\\dist\\web\\assets\\app.js` 转换为静态资源 import specifier
- **THEN** 生成的 import specifier SHALL 为 `../dist/web/assets/app.js`,且 MUST NOT 包含 `\\`
#### Scenario: POSIX 相对路径保持 import specifier 形式
- **WHEN** code generation 将 POSIX 文件系统相对路径 `../dist/web/assets/app.js` 转换为静态资源 import specifier
- **THEN** 生成的 import specifier SHALL 保持为 `../dist/web/assets/app.js`

View File

@@ -1,69 +0,0 @@
# Static Asset Embedding
定义构建时将 Vite 产出的前端静态资源嵌入 Bun 可执行文件的 code generation 流程和运行时静态资源服务逻辑。
## Purpose
支持将 Vite 构建的前端资源通过 `import with { type: "file" }` 嵌入 Bun 可执行文件,实现单文件交付的同时保持正确的缓存策略和 Content-Type 处理。
## Requirements
### Requirement: 构建时资源扫描与 Code Generation
构建脚本 SHALL 在 Vite build 完成后扫描 `dist/web/` 目录,自动生成 TypeScript 文件,为每个静态资源创建 `import ... with { type: "file" }` 声明。
#### Scenario: 生成资源导入文件
- **WHEN** 构建脚本扫描 `dist/web/` 目录
- **THEN** 系统 SHALL 在 `.build/static-assets.ts` 中为每个文件生成 `import fN from "<path>" with { type: "file" }` 语句,并导出 `StaticAssets` 对象
#### Scenario: StaticAssets 对象结构
- **WHEN** `static-assets.ts` 被生成
- **THEN** 导出的对象 SHALL 包含 `indexHtml: Blob``files: Record<string, Blob>` 两个字段,其中 files 的 key 为 URL 路径(如 `/assets/index-a1b2c3.js`
#### Scenario: 生成 production server entry
- **WHEN** 构建脚本生成资源导入文件后
- **THEN** 系统 SHALL 在 `.build/server-entry.ts` 中生成 production 入口import bootstrap、config 和 staticAssets 并调用 bootstrap
### Requirement: 运行时静态资源服务
系统 SHALL 提供 `serveStaticAsset` 函数,根据请求路径从 StaticAssets 中查找并返回对应资源。
#### Scenario: 请求根路径
- **WHEN** 请求路径为 `/`
- **THEN** 系统 SHALL 返回 `indexHtml`Content-Type 为 `text/html; charset=utf-8`Cache-Control 为 `no-cache`
#### Scenario: 请求已知静态资源
- **WHEN** 请求路径匹配 `files` 中的某个 key
- **THEN** 系统 SHALL 返回对应 BlobContent-Type 根据文件扩展名推断Cache-Control 为 `public, max-age=31536000, immutable`
#### Scenario: 请求未知带扩展名路径
- **WHEN** 请求路径包含文件扩展名但未匹配任何已知资源
- **THEN** 系统 SHALL 返回 404 响应
#### Scenario: SPA Fallback
- **WHEN** 请求路径不包含文件扩展名且不以 `/api/` 开头
- **THEN** 系统 SHALL 返回 `indexHtml`SPA fallback
### Requirement: Content-Type 推断
系统 SHALL 根据文件扩展名推断正确的 Content-Type header。
#### Scenario: JavaScript 文件
- **WHEN** 请求路径以 `.js``.mjs` 结尾
- **THEN** Content-Type SHALL 为 `text/javascript; charset=utf-8`
#### Scenario: CSS 文件
- **WHEN** 请求路径以 `.css` 结尾
- **THEN** Content-Type SHALL 为 `text/css; charset=utf-8`
#### Scenario: SVG 文件
- **WHEN** 请求路径以 `.svg` 结尾
- **THEN** Content-Type SHALL 为 `image/svg+xml`
### Requirement: 静态资源 import specifier SHALL 使用平台无关分隔符
构建时静态资源 code generation SHALL 将文件系统相对路径转换为 ESM import specifier并确保生成的 import 路径在 Windows、macOS、Linux 开发环境下都使用 `/` 作为分隔符。
#### Scenario: Windows 相对路径转换为 import specifier
- **WHEN** code generation 将 Windows 文件系统相对路径 `..\\dist\\web\\assets\\app.js` 转换为静态资源 import specifier
- **THEN** 生成的 import specifier SHALL 为 `../dist/web/assets/app.js`,且 MUST NOT 包含 `\\`
#### Scenario: POSIX 相对路径保持 import specifier 形式
- **WHEN** code generation 将 POSIX 文件系统相对路径 `../dist/web/assets/app.js` 转换为静态资源 import specifier
- **THEN** 生成的 import specifier SHALL 保持为 `../dist/web/assets/app.js`

View File

@@ -1,45 +0,0 @@
## Purpose
定义 target 分组能力YAML 配置中的 group 字段、后端存储、API 传递和前端分组排序。
## Requirements
### Requirement: target 分组配置
系统 SHALL 支持在每个 target 上配置可选的 `group` 字段,用于将目标归类到不同分组。未指定 `group` 的 target SHALL 归入 `"default"` 分组。
#### Scenario: 配置分组名称
- **WHEN** YAML 配置中某个 target 指定 `group: "搜索引擎"`
- **THEN** 系统 SHALL 将该 target 归类到 "搜索引擎" 分组
#### Scenario: 不配置分组
- **WHEN** YAML 配置中某个 target 未指定 `group` 字段
- **THEN** 系统 SHALL 将该 target 归类到 "default" 分组
#### Scenario: group 字段类型校验
- **WHEN** YAML 配置中某个 target 的 `group` 字段不是字符串
- **THEN** 系统 SHALL 以错误退出并提示 group 字段类型错误
### Requirement: 分组排序
系统 SHALL 保证 "default" 分组始终排在最前面,其余分组按配置文件中首次出现的顺序排列。
#### Scenario: default 分组排最前
- **WHEN** 配置中存在多个分组(包括 "default" 和自定义分组)
- **THEN** API 返回的目标列表中 "default" 分组的目标 SHALL 排在其他分组之前
#### Scenario: 自定义分组按出现顺序
- **WHEN** 配置中 "搜索引擎" 分组在 "后端服务" 分组之前首次出现
- **THEN** API 返回中 "搜索引擎" 分组 SHALL 排在 "后端服务" 分组之前
### Requirement: 分组信息 API 传递
系统 SHALL 在 API 响应中返回每个 target 的分组信息。
#### Scenario: targets 列表包含分组
- **WHEN** 客户端请求 `GET /api/targets`
- **THEN** 响应中每个目标 SHALL 包含 `group` 字段,值为该目标所属的分组名称
### Requirement: 分组存储
系统 SHALL 在数据库 targets 表中持久化每个 target 的分组信息。
#### Scenario: 持久化分组信息
- **WHEN** 系统同步 targets 到数据库
- **THEN** 每个 target 的 `grp` 列 SHALL 存储其分组名称,未配置分组的存储 `"default"`

View File

@@ -1,110 +0,0 @@
## Purpose
定义 target 的 id/name 双字段标识体系id 作为唯一标识符name 作为可选展示名称。
## Requirements
### Requirement: target id 字段
每个 target SHALL 包含必填的 `id` 字段作为唯一标识符。`id` SHALL 符合 `[a-zA-Z0-9][a-zA-Z0-9_-]*` 命名规则。`id` 长度 MUST 为 1 到 30 个字符。`id` MUST 在所有 targets 中全局唯一。`id` MUST NOT 参与变量替换。
#### Scenario: 合法 id
- **WHEN** target 配置 `id: "api-health"`
- **THEN** 系统 SHALL 使用该 id 作为 target 的唯一标识符
#### Scenario: id 包含下划线和连字符
- **WHEN** target 配置 `id: "db_check-01"`
- **THEN** 系统 SHALL 使用该 id 作为 target 的唯一标识符
#### Scenario: id 缺失报错
- **WHEN** target 未配置 `id` 字段
- **THEN** 系统 SHALL 以配置错误退出,提示该 target 缺少 id 字段
#### Scenario: id 为空字符串报错
- **WHEN** target 配置 `id: ""`
- **THEN** 系统 SHALL 以配置错误退出,提示 id 不能为空
#### Scenario: id 超过最大长度报错
- **WHEN** target 配置超过 30 个字符的 `id`
- **THEN** 系统 SHALL 以配置错误退出,提示 id 长度不合法
#### Scenario: id 不合法报错
- **WHEN** target 配置 `id: "_invalid"``id: "-start"``id: "has space"`
- **THEN** 系统 SHALL 以配置错误退出,提示 id 不符合命名规则
#### Scenario: id 重复报错
- **WHEN** 两个 target 配置相同的 `id: "api-health"`
- **THEN** 系统 SHALL 以配置错误退出,提示 id 重复
### Requirement: target name 字段
每个 target SHALL 支持可选的 `name` 字段作为展示名称元信息。`name` 缺省或显式配置为 `null` 时 SHALL 在配置解析、运行时模型、存储和 API 中保留为 null。前端展示目标名称时 SHALL 使用 `name ?? id`,但该 fallback MUST NOT 改变 target 本身的 name 值。显式配置的 `name` MUST 为长度 1 到 30 个字符的字符串,且去除首尾空白后 MUST 不为空。`name` SHALL 支持变量替换。`name` MUST NOT 要求全局唯一MUST NOT 参与 target 唯一性判定。
#### Scenario: 配置 name
- **WHEN** target 配置 `id: "api-health"``name: "API 健康检查"`
- **THEN** 系统 SHALL 在解析后保留 name 为 "API 健康检查"
#### Scenario: name 使用变量
- **WHEN** target 配置 `name: "${env} API 健康检查"` 且 variables 中 `env: "生产"`
- **THEN** 系统 SHALL 将 name 解析为 "生产 API 健康检查"
#### Scenario: name 缺省保留为 null
- **WHEN** target 配置 `id: "api-health"` 但未配置 `name`
- **THEN** 系统 SHALL 在解析、存储和 API 响应中保留 name 为 null
#### Scenario: name 显式 null
- **WHEN** target 配置 `id: "api-health"``name: null`
- **THEN** 系统 SHALL 接受该配置,并在解析、存储和 API 响应中保留 name 为 null
#### Scenario: name 空 YAML 值
- **WHEN** target 配置 `id: "api-health"``name:` 后不提供值
- **THEN** 系统 SHALL 将该 name 按 null 处理,并接受该配置
#### Scenario: name 为 null 时展示 fallback
- **WHEN** 前端展示 name 为 null 的 target
- **THEN** 前端 SHALL 显示该 target 的 id 作为目标名称文案
#### Scenario: name 为空字符串报错
- **WHEN** target 配置 `name: ""`
- **THEN** 系统 SHALL 以配置错误退出,提示 name 不能为空
#### Scenario: name 仅包含空白字符报错
- **WHEN** target 配置 `name: " "`
- **THEN** 系统 SHALL 以配置错误退出,提示 name 不能为空
#### Scenario: name 超过最大长度报错
- **WHEN** target 配置超过 30 个字符的 `name`
- **THEN** 系统 SHALL 以配置错误退出,提示 name 长度不合法
#### Scenario: 多个 target 使用相同 name
- **WHEN** 两个 target 配置不同 id 但相同 `name: "健康检查"`
- **THEN** 系统 SHALL 接受该配置不报错name 不要求全局唯一)
### Requirement: target description 字段
每个 target SHALL 支持可选的 `description` 字段作为目标说明。`description` 缺省或显式配置为 `null` 时 SHALL 在配置解析、运行时模型、存储和 API 中保留为 null。`description` SHALL 支持变量替换。`description` 字符串长度 MUST 不超过 500 个字符,且允许为空字符串。`description` MUST NOT 参与 target 唯一性判定。
#### Scenario: 配置 description
- **WHEN** target 配置 `description: "检查生产 API 健康状态"`
- **THEN** 系统 SHALL 使用该值作为目标说明
#### Scenario: description 使用变量
- **WHEN** target 配置 `description: "${env} 环境健康检查"` 且 variables 中 `env: "生产"`
- **THEN** 系统 SHALL 将目标说明解析为 "生产 环境健康检查"
#### Scenario: description 缺省
- **WHEN** target 未配置 `description`
- **THEN** 系统 SHALL 接受该配置,且目标说明为 null
#### Scenario: description 显式 null
- **WHEN** target 配置 `description: null`
- **THEN** 系统 SHALL 接受该配置,且目标说明为 null
#### Scenario: description 空 YAML 值
- **WHEN** target 配置 `description:` 后不提供值
- **THEN** 系统 SHALL 将该 description 按 null 处理,并接受该配置
#### Scenario: description 为空字符串
- **WHEN** target 配置 `description: ""`
- **THEN** 系统 SHALL 接受该配置,且目标说明为空字符串
#### Scenario: description 超过最大长度报错
- **WHEN** target 配置超过 500 个字符的 `description`
- **THEN** 系统 SHALL 以配置错误退出,提示 description 长度不合法

View File

@@ -1,164 +0,0 @@
## Purpose
定义单目标指标 API 的端点、响应类型、延迟百分位计算、MTTR 计算、最长故障时长计算、故障事件计数、当前连续状态、趋势数据应用层分桶和无数据口径。
## Requirements
### Requirement: 单目标指标 API
系统 SHALL 提供 `GET /api/targets/:id/metrics` 端点,返回单个目标在指定时间窗口内的概览统计和趋势数据。
#### Scenario: 获取目标指标
- **WHEN** 客户端请求 `GET /api/targets/1/metrics?from=ISO&to=ISO&bucket=1h`
- **THEN** 系统 SHALL 返回 JSON 对象包含 targetId、window、stats、trend 字段
#### Scenario: from 或 to 参数缺失
- **WHEN** 客户端请求 `GET /api/targets/1/metrics` 未提供 from 或 to 参数
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
#### Scenario: 目标不存在
- **WHEN** 客户端请求 `GET /api/targets/999/metrics?from=ISO&to=ISO`
- **THEN** 系统 SHALL 返回 404 状态码和错误信息
#### Scenario: 无效的目标 ID
- **WHEN** 客户端请求 `GET /api/targets/abc/metrics?from=ISO&to=ISO`
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
#### Scenario: bucket 参数缺失
- **WHEN** 客户端请求 `GET /api/targets/1/metrics?from=ISO&to=ISO` 未提供 bucket 参数
- **THEN** 系统 SHALL 默认使用 bucket=`1h`
#### Scenario: 不支持的 bucket 参数
- **WHEN** 客户端请求 `GET /api/targets/1/metrics?from=ISO&to=ISO&bucket=5m`
- **THEN** 系统 SHALL 返回 400 状态码和错误信息
### Requirement: TargetMetricsResponse 共享类型
系统 SHALL 在 `src/shared/api.ts` 中定义 `TargetMetricsResponse` 类型。
#### Scenario: 类型定义
- **WHEN** 前后端引用 `TargetMetricsResponse` 类型
- **THEN** 该类型 SHALL 包含 targetIdnumber、windowfrom/to/bucket、stats 和 trend 字段
#### Scenario: stats 字段
- **WHEN** metrics 响应包含 stats
- **THEN** stats SHALL 包含 totalChecks、upChecks、downChecks、availability、avgDurationMs、p95DurationMs、p99DurationMs、mttr、longestOutage、incidentCount、currentStreak 字段
#### Scenario: trend 字段
- **WHEN** metrics 响应包含 trend
- **THEN** trend SHALL 为数组,每个元素包含 bucketStart、avgDurationMs、minDurationMs、maxDurationMs、availability、totalChecks、upChecks、downChecks 字段
### Requirement: P95/P99 延迟计算
系统 SHALL 在后端应用层计算 P95 和 P99 延迟百分位数。
#### Scenario: 正常计算 P95
- **WHEN** 时间窗口内存在成功检查记录matched=1 且 duration_ms 不为 null
- **THEN** 系统 SHALL 取出所有成功检查的 duration_ms在后端应用层排序后取第 95 百分位值返回为 p95DurationMs
#### Scenario: 正常计算 P99
- **WHEN** 时间窗口内存在成功检查记录
- **THEN** 系统 SHALL 取第 99 百分位值返回为 p99DurationMs
#### Scenario: 无成功检查记录
- **WHEN** 时间窗口内无 matched=1 且 duration_ms 不为 null 的记录
- **THEN** p95DurationMs 和 p99DurationMs SHALL 返回 null
#### Scenario: 百分位计算方法
- **WHEN** 计算第 N 百分位
- **THEN** 系统 SHALL 将 duration_ms 升序排列,取 index = ceil(count * N / 100) - 1 位置的值
### Requirement: MTTR 计算
系统 SHALL 在后端应用层计算平均恢复时间Mean Time To Recovery
#### Scenario: 存在已恢复的故障段
- **WHEN** 时间窗口内存在至少一个已恢复的故障段(连续 matched=0 后跟 matched=1
- **THEN** 系统 SHALL 计算所有已恢复故障段的平均持续时间(从首个 matched=0 的 timestamp 到恢复后首个 matched=1 的 timestamp 之差),返回为 mttr毫秒
#### Scenario: 无已恢复的故障段
- **WHEN** 时间窗口内无已恢复的故障段(全部正常,或当前仍在故障中且无历史恢复)
- **THEN** mttr SHALL 返回 null
#### Scenario: 当前正在故障中
- **WHEN** 时间窗口内最后一段故障尚未恢复
- **THEN** 该未恢复的故障段 SHALL 不计入 MTTR 平均值
#### Scenario: 窗口起始即为故障且后续恢复
- **WHEN** 时间窗口内第一条记录即为 matched=0故障跨越了 from 边界),且该故障段在窗口内恢复
- **THEN** 该故障段 SHALL 不计入 MTTR 平均值(因无法确定真实故障开始时间),但 SHALL 计入 incidentCount
### Requirement: 最长故障时长
系统 SHALL 在后端应用层计算时间窗口内最长的单次故障持续时间。
#### Scenario: 存在故障段
- **WHEN** 时间窗口内存在故障段
- **THEN** 系统 SHALL 返回最长故障段的持续时间为 longestOutage毫秒
#### Scenario: 无故障
- **WHEN** 时间窗口内无 matched=0 的记录
- **THEN** longestOutage SHALL 返回 null
#### Scenario: 窗口起始即为故障
- **WHEN** 时间窗口内第一条记录即为 matched=0
- **THEN** 该故障段的持续时间 SHALL 从 from 参数开始计算
#### Scenario: 当前正在故障中
- **WHEN** 最后一段故障尚未恢复
- **THEN** 该故障段的持续时间 SHALL 计算为从故障开始到时间窗口 to 参数的时间差
### Requirement: 故障事件计数
系统 SHALL 在后端应用层计算时间窗口内的故障事件次数。
#### Scenario: 计算故障事件数
- **WHEN** 时间窗口内存在状态翻转matched 从 1 变为 0
- **THEN** 系统 SHALL 返回翻转次数为 incidentCount
#### Scenario: 无故障事件
- **WHEN** 时间窗口内所有检查均为 matched=1
- **THEN** incidentCount SHALL 返回 0
#### Scenario: 窗口起始即为故障
- **WHEN** 时间窗口内第一条记录即为 matched=0 且无前序记录可判断翻转
- **THEN** 该故障 SHALL 计为 1 次事件
#### Scenario: 连续异常只计一次
- **WHEN** 某目标连续 10 次 matched=0
- **THEN** 该连续异常段 SHALL 仅计为 1 次事件
### Requirement: 当前连续状态
系统 SHALL 返回目标当前的连续状态信息。
#### Scenario: 当前连续正常
- **WHEN** 目标最近的检查记录连续为 matched=1
- **THEN** currentStreak SHALL 返回 `{ up: true, count: N }`N 为连续正常的检查次数
#### Scenario: 当前连续异常
- **WHEN** 目标最近的检查记录连续为 matched=0
- **THEN** currentStreak SHALL 返回 `{ up: false, count: N }`N 为连续异常的检查次数
#### Scenario: 连续状态达到取数上限
- **WHEN** 连续状态次数达到后端取数或计算上限
- **THEN** currentStreak SHALL 返回 `{ up: boolean, count: N, capped: true }`,前端据此展示上限标记
#### Scenario: 无检查记录
- **WHEN** 目标没有任何检查记录
- **THEN** currentStreak SHALL 返回 null
### Requirement: 趋势数据应用层分桶
系统 SHALL 在后端应用层按 UTC 小时分桶生成趋势数据。
#### Scenario: 按小时生成趋势
- **WHEN** metrics 请求 bucket=`1h`
- **THEN** 系统 SHALL 按 UTC 小时生成 trend 数组,每个点包含该小时内的 totalChecks、upChecks、downChecks、availability、avgDurationMs、minDurationMs、maxDurationMs
#### Scenario: 小时内无成功检查
- **WHEN** 某小时内存在检查记录但无成功检查记录
- **THEN** avgDurationMs、minDurationMs、maxDurationMs SHALL 返回 nullavailability SHALL 基于 upChecks/totalChecks 返回 0
#### Scenario: 小时内无检查记录
- **WHEN** 某小时内没有任何检查记录
- **THEN** 系统 MAY 不返回该小时对应的 trend 点
### Requirement: 无数据口径
系统 SHALL 对无数据窗口返回稳定的空指标口径。
#### Scenario: 窗口内无检查记录
- **WHEN** 指定时间窗口内没有任何检查记录
- **THEN** stats SHALL 返回 totalChecks=0、upChecks=0、downChecks=0、availability=0、avgDurationMs=null、p95DurationMs=null、p99DurationMs=null、mttr=null、longestOutage=null、incidentCount=0、currentStreak=nulltrend SHALL 返回空数组

View File

@@ -23,14 +23,6 @@
- **WHEN** YAML 中 tcp target 的 `tcp.port` 不是 1 到 65535 之间的整数
- **THEN** 系统 SHALL 以配置错误退出,并提示 tcp.port 必须为合法 TCP 端口
#### Scenario: tcp defaults 覆盖 banner 参数
- **WHEN** YAML 中配置 `defaults.tcp.bannerReadTimeout: 1000``defaults.tcp.maxBannerBytes: "8KB"`
- **THEN** 未显式配置对应字段的 tcp target SHALL 使用 defaults.tcp 中的值
#### Scenario: per-target banner 参数覆盖 defaults
- **WHEN** defaults.tcp 配置了 banner 参数,且某个 tcp target 显式配置 `tcp.bannerReadTimeout``tcp.maxBannerBytes`
- **THEN** 该 target SHALL 使用自身 tcp 分组中的值
#### Scenario: tcp 序列化展示摘要
- **WHEN** 系统同步 tcp target 到 targets 表
- **THEN** `target` 展示摘要 SHALL 为 `<host>:<port>``config` JSON SHALL 包含 resolved 后的 host、port、readBanner、bannerReadTimeout 和 maxBannerBytes

View File

@@ -1,5 +1,3 @@
# Test Output Cleanliness
## Purpose
确保测试运行时输出干净、无噪音,便于开发者快速定位问题。

View File

@@ -27,14 +27,6 @@
- **WHEN** YAML 中 udp target 未配置 `udp.payload`
- **THEN** 系统 SHALL 使用空字符串作为 payload并在执行时发送零长度 UDP datagram
#### Scenario: udp defaults 覆盖通用 UDP 参数
- **WHEN** YAML 中配置 `defaults.udp.encoding: "hex"``defaults.udp.responseEncoding: "hex"``defaults.udp.maxResponseBytes: "8KB"`
- **THEN** 未显式配置对应字段的 udp target SHALL 使用 defaults.udp 中的值
#### Scenario: per-target UDP 参数覆盖 defaults
- **WHEN** defaults.udp 配置了 encoding、responseEncoding 或 maxResponseBytes且某个 udp target 显式配置对应字段
- **THEN** 该 target SHALL 使用自身 udp 分组中的值
#### Scenario: udp 分组未知字段失败
- **WHEN** YAML 中 udp target 的 `udp` 分组包含 `dnsQuery``expectResponse` 或其他未知字段
- **THEN** 系统 SHALL 以配置错误退出,提示 udp 分组包含未知字段
@@ -129,7 +121,7 @@
- **THEN** 系统 SHALL 返回 `matched=false`failure 的 kind 为 `error`phase 为 `response`message 包含响应被截断的信息
#### Scenario: maxResponseBytes 格式非法
- **WHEN** YAML 中 udp target 或 defaults.udp `maxResponseBytes` 不是非负整数或合法 size string
- **WHEN** YAML 中 udp target 的 `maxResponseBytes` 不是非负整数或合法 size string
- **THEN** 系统 SHALL 以配置错误退出,提示 maxResponseBytes 格式错误
### Requirement: udp expect 校验

View File

@@ -1,58 +0,0 @@
# Vite Frontend Bundling
定义 Vite 作为前端构建工具的配置、产出结构和优化策略。
## Purpose
使用 Vite 的 Rolldown 引擎完成前端打包,实现 code splitting、vendor chunk 分离和 CSS 优化,解决 Bun bundler 前端性能问题。
## Requirements
### Requirement: Vite 前端构建配置
系统 SHALL 使用 Vite 作为前端构建工具,配置文件位于项目根目录 `vite.config.ts`,以 `src/web` 为 root产出到 `dist/web/`
#### Scenario: 运行 Vite 生产构建
- **WHEN** 构建脚本执行 `bunx --bun vite build`
- **THEN** Vite SHALL 将 `src/web/index.html` 及其引用的所有模块构建到 `dist/web/` 目录,包含 `index.html``assets/` 子目录
#### Scenario: 产出文件名包含 content hash
- **WHEN** Vite 构建完成
- **THEN** `assets/` 目录下的 JS 和 CSS 文件名 SHALL 包含 content hash`index-a1b2c3.js`
### Requirement: Code Splitting 策略
系统 SHALL 配置 Vite 的 Rolldown code splitting将 vendor 库分离为独立 chunks并通过 `React.lazy()` 动态导入实现按需加载。
#### Scenario: React 相关库分离
- **WHEN** Vite 构建完成
- **THEN** `react``react-dom``scheduler` SHALL 被打包到名为 `vendor-react` 的独立 chunk
#### Scenario: TDesign 相关库分离
- **WHEN** Vite 构建完成
- **THEN** `tdesign-react``tdesign-icons-react` 相关模块 SHALL 被打包到名为 `vendor-tdesign` 的独立 chunk
#### Scenario: 图表库分离
- **WHEN** Vite 构建完成
- **THEN** `recharts``d3-*` 相关模块 SHALL 被打包到名为 `vendor-chart` 的独立 chunk
#### Scenario: TargetDetailDrawer 延迟加载
- **WHEN** Vite 构建完成
- **THEN** `TargetDetailDrawer` 及其依赖recharts、D3、DateRangePicker 等SHALL 通过 `React.lazy()` 动态导入,被 Rolldown 自动拆分为异步 chunk不包含在初始加载的 JS 中
#### Scenario: Drawer 首次渲染无闪烁
- **WHEN** 用户首次点击目标触发 Drawer 渲染
- **THEN** Drawer SHALL 通过 `<Suspense fallback={null}>` 包裹,利用其默认 visible=false 状态避免加载期间的视觉闪烁
### Requirement: CSS 处理
系统 SHALL 通过 Vite 处理 CSS 导入,产出独立的 CSS 文件。TDesign 组件样式 SHALL 保持全量导入方式。
#### Scenario: CSS 文件产出
- **WHEN** Vite 构建完成
- **THEN** 所有 CSS 导入 SHALL 被提取为独立的 `.css` 文件到 `assets/` 目录
#### Scenario: CSS 压缩
- **WHEN** Vite 执行生产构建
- **THEN** 产出的 CSS 文件 SHALL 经过压缩处理
#### Scenario: TDesign CSS 全量导入
- **WHEN** 前端入口文件初始化样式
- **THEN** 系统 SHALL 通过 `tdesign-react/dist/reset.css``tdesign-react/dist/tdesign.min.css` 全量导入 TDesign 组件样式

View File

@@ -1,5 +1,3 @@
# Capability: windows-test-compat
## Purpose
确保测试在 Windows、macOS、Linux 平台上的兼容性,包括文件句柄释放后的目录清理重试机制和跨平台命令测试约定。