diff --git a/.specify/feature.json b/.specify/feature.json index 2d308b8..736dc6b 100644 --- a/.specify/feature.json +++ b/.specify/feature.json @@ -1,3 +1,3 @@ { - "feature_directory": "specs/005-related-entities-tab" + "feature_directory": "specs/006-skill-template-polish" } diff --git a/skill/SKILL.md b/skill/SKILL.md index 6de5fea..58b1612 100644 --- a/skill/SKILL.md +++ b/skill/SKILL.md @@ -1,11 +1,35 @@ --- name: nexusx-4phase description: 基于 nexusx 的四阶段开发模式,从 Schema 建模到 API 响应组装再到 TS SDK 的完整项目构建流程。 -argument-hint: "[项目路径] 创建四阶段项目的目标目录" --- # nexusx 四阶段开发模式 +## 适用版本 + +本 skill 假设 **Python >= 3.12** 且 **nexusx >= 3.2**。特性-版本对照: + +| 特性 | 起始版本 | +|---|---| +| UseCase GraphQL MCP(`create_use_case_graphql_mcp_server`) | 3.0+ | +| 虚拟实体(`ErManager.add_virtual_entities`) | 3.2+ | +| 跨层数据流(`ExposeAs` / `SendTo` / `Collector`) | 3.0+ | + +正文中如提到具体 API 的版本门槛,均以此表为准;不再在各 phase 文档中散落声明。 + +> [!IMPORTANT] +> **Python 版本要求:`>= 3.12`。** +> 该 skill 依赖的 `SQLModel + SQLAlchemy + nexusx ErManager` 关系解析,在较低 Python 版本(尤其是 3.10)下,容易因为类型注解延迟求值与 ORM relationship 解析组合而出现兼容问题。 +> 如使用 `Relationship(...)`、自引用关系、虚拟实体(`ErManager.add_virtual_entities`)或 Alembic 自动迁移,请统一使用 Python 3.12+。 + +## 调用约定 + +``` +/nexusx-4phase [项目目录路径] +``` + +参数为目标目录路径(可选)。未提供时,skill 引导用户在当前位置或指定路径下创建项目。 + 基于 nexusx 的渐进式开发方法论。项目在一个 `src/` 目录下逐步演进,每个阶段在上一阶段基础上新增代码。 | Phase | 职责 | 产出 | @@ -32,7 +56,7 @@ argument-hint: "[项目路径] 创建四阶段项目的目标目录" ┌──────────────────────────────────────────────┐ │ V 降:定义验收标准 │ │ "在当前 Phase 开始之前,先定义什么算做完。" │ -│ 写入 spec/.md 的"验收标准"部分 │ +│ 写入 phaseN.md 的"验收标准"部分 │ └──────────────────────────────────────────────┘ ↓ ┌───────────────┐ @@ -42,236 +66,27 @@ argument-hint: "[项目路径] 创建四阶段项目的目标目录" ┌──────────────────────────────────────────────┐ │ V 升:逐条回查验收 │ │ "一条一条对照验收标准,通过才可继续。" │ -│ 用户逐条确认 → 写入 spec/.md │ +│ 用户逐条确认 → 写入 phaseN.md │ └──────────────────────────────────────────────┘ ``` +> `phaseN.md` 完整路径为 `specs/<编号>-<需求简述>/phaseN.md`,详见 `spec-management.md` 的「目录命名」。 + 验收标准必须是**可观察、可操作的**——不写"代码健壮",写"GraphiQL 中执行 X query 返回 Y"。 ## 阶段实现 -Phase 0(需求确认)完整包含在本文件中。 +每个阶段(含 Phase 0)有独立的详细指令文件: -Phase 0 完成并确认后,读取当前阶段的详细指令: -- **Phase 1**: 读取 `phases/phase1.md` -- **Phase 2**: 读取 `phases/phase2.md` -- **Phase 3**: 读取 `phases/phase3.md` -- **Phase 4**: 读取 `phases/phase4.md` +- **Phase 0**(需求确认): 读取 `phases/phase0.md` — 业务实体 / 关系 / 聚合根 / 用例方法 / 第三方库 / DB 选型的逐项确认 +- **Phase 1**(Schema + ER Diagram): 读取 `phases/phase1.md` +- **Phase 2**(方法实现 + Entity 挂载): 读取 `phases/phase2.md` +- **Phase 3**(UseCase 响应组装 + MCP): 读取 `phases/phase3.md` +- **Phase 4**(OpenAPI → TS SDK): 读取 `phases/phase4.md` 每个阶段完成后,继续进行下一阶段之前暂停并等待用户确认。 -对于 Spec 管理工作流(目录命名、文件格式、迭代规则、交付验证),读取 `spec-management.md`。 - -## Phase 0: 需求确认(必做) - -在写任何代码之前,必须与用户逐项确认以下内容。每一项都需要用户明确认可后才算完成。 - -### Step 0-1: 术语与实体定义 - -逐一列出所有业务实体,每个实体说明: - -- **业务含义**(一句话,团队无歧义) -- **核心字段**(名称 + 类型 + 语义说明,不需要穷举,但关键属性不能遗漏) -- **字段约束**(唯一、非空、枚举值、联合唯一等) - -用表格形式呈现,方便用户逐行确认。 - -### Step 0-2: 实体关系 - -用文本 ER 图展示实体间关系,每条关系标明: - -- 方向(1:N / N:1 / M:N) -- 业务含义(如「会话包含多条消息」) -- 是否需要中间实体 - -``` -User ──1:N──→ Participant -Conversation ──1:N──→ Message -... -``` - -**必须与用户确认关系方向和基数是否正确。** - -### Step 0-3: 聚合根 - -明确哪个(或哪些)实体是聚合根。聚合根决定: - -- 主要的业务入口(从哪个实体开始查询) -- @query / @mutation 挂在哪些实体上 -- Phase 3 的 service 划分依据 - -#### 根类型选择(3.2+) - -每个聚合根必须明确是 **SQLModel 实体**还是 **虚拟实体(普通 `pydantic.BaseModel`)**: - -| 类型 | 何时选 | 数据持久化 | 例子 | -|------|--------|-----------|------| -| **SQLModel 实体** | 数据需要落库、参与 alembic 迁移、是 GraphQL 实体入口 | ✅ 表 | `User` / `Sprint` / `Conversation` | -| **虚拟实体(BaseModel)** | 响应根不对应ORM表:组装自外部 claims、聚合视图、第三方 SDK DTO | ❌ 不建表 | `CurrentUser`(OIDC claims)、`Page[T]`(分页 wrapper)、第三方 SDK 镜像 | - -虚拟实体通过 `ErManager.add_virtual_entities([...])` 在 Phase 1 注册,与 SQLModel 实体在 Resolver / ER / Voyager 中同等对待(详见 `docs/guide/virtual_entities.md`)。 - -**判断依据**:如果根字段全部来自数据库表 → SQLModel;如果字段来自请求上下文(JWT、headers)或聚合多个源 → 虚拟实体。混合场景(如 `Page[TaskDTO]` 包含真实 task 数据)由 Phase 3 DTO 组合,根本身仍是虚拟实体。 - -### Step 0-4: 业务域划分 + 用例方法 - -**⚠️ 禁止自行决定 Service 切分方案。必须提出候选方案与用户讨论,由用户最终确认。** - -#### Step 0-4a: 提出 Service 切分候选方案 - -业务域(Service)按功能边界划分,不按实体划分。Service 切分直接影响: -- 目录结构(`service//`) -- Phase 2 的 methods.py 粒度 -- Phase 3 的 UseCaseService 类划分 -- MCP 和 REST 的入口组织 - -**必须向用户提出至少一种候选方案**,说明每种方案的切分依据和优劣,由用户选择或修正。 - -常见的切分策略参考: - -| 策略 | 示例 | 适用场景 | -|------|------|----------| -| 按业务功能域 | `auth` / `chat` / `order` | 业务边界清晰,领域间耦合低 | -| 按聚合根 | `user` / `conversation` / `message` | 实体独立性强,CRUD 为主 | -| 混合(功能域 + 独立聚合) | `auth` / `chat`(含 conversation+message) | 部分域跨实体协作 | - -**向用户展示的格式:** - -``` -方案 A:按功能域 - auth/ → register, login - chat/ → create_conversation, list_messages, send_message - 优势:业务内聚,方法自然归组 - 劣势:chat 域可能过大 - -方案 B:按聚合根 - user/ → register, login - conversation/ → create_conversation, list_messages - message/ → send_message - 优势:每个 service 粒度均匀 - 劣势:conversation 和 message 强耦合却拆开了 -``` - -**必须等用户明确选择后才能继续。** 如果用户提出自己的分法,按用户的来。 - -#### Step 0-4b: 按确认的 Service 划分列出用例方法 - -用户确认 Service 切分后,按每个业务域列出用例方法。每个方法说明: - -- **方法名**(动词开头,如 `create_conversation`、`list_messages`) -- **业务意图**(一句话,如「创建群聊并自动将创建者加入为 owner」) -- **挂载实体**(挂在哪个 Entity 的 @query / @mutation 上,供 GraphQL 使用) -- **关键参数**(列出参数名和含义,不需要完整签名) - -示例格式: - -| 业务域 | 方法名 | 业务意图 | 挂载实体 | 关键参数 | -|--------|--------|----------|----------|----------| -| auth | register | 注册新用户 | User | username, nickname, password | -| auth | login | 登录返回 JWT | User | username, password | -| chat | create_conversation | 创建会话 | Conversation | type, creator_id, name | -| chat | list_messages | 查询会话消息(分页) | Conversation | conversation_id, before_id, limit | - -**用例方法不需要实现细节,但必须逻辑自洽**: -- mutation 的参数是否足以完成操作 -- 创建类 mutation 是否有遗漏的副作用(如自动创建关联记录) -- 查询类方法是否覆盖了核心场景 - -### Step 0-5: GraphQL 定位 - -GraphQL 是辅助开发测试和 AI 测试的接口,不是正式 API。 - -业务方法的定义和挂载关系: - -``` -service//methods.py ← 独立定义业务逻辑(核心) - ↓ 挂载 ↓ 挂载 - Entity @query/@mutation UseCaseService @query/@mutation - (GraphQL 辅助测试) (REST + MCP 正式接口) -``` - -- Phase 2:方法体在 `service//methods.py` 中实现,`models.py` 的 `mount_method()` 函数挂载到 Entity,`main.py` 显式调用 -- Phase 3:同一个方法挂载到 UseCaseService(REST/MCP 使用),DTO 转换在 Service 层完成 - -### Step 0-6: 第三方库确认 - -列出项目中涉及的非业务功能领域(认证、实时推送、文件存储、数据迁移等),对每个领域: - -- **说明候选方案**(推荐成熟第三方库 vs 手写实现) -- **给出推荐理由**(社区活跃度、维护状态、与 FastAPI/SQLModel 的兼容性) -- **必须调查用户提到的第三方库的当前维护状态**(避免选用已停止维护的库) - -用表格形式呈现: - -| 功能领域 | 推荐方案 | 理由 | 备注 | -|----------|----------|------|------| -| 认证 | ... | ... | ... | -| ... | ... | ... | ... | - -**注意事项**: -- 优先使用 FastAPI 生态内的主流方案,减少集成风险 -- 如果用户指定了某个库,必须先调查其维护状态和兼容性,发现问题要及时告知用户并提供替代方案 -- 对于 nexusx 已覆盖的领域(ORM、GraphQL、MCP),不再重复讨论 - -**必须与用户确认每个领域的选型后才能继续。** - -### Step 0-7: 数据持久化与迁移策略 - -**⚠️ 必须由用户明确选定 DB 类型与迁移策略,决定 Phase 1 的 `db.py` / `database.py` 实现方式以及是否引入 alembic。** - -#### 选型决策表 - -| 选项 | async DB URL | 持久化 | Alembic | 额外依赖 | 适用场景 | -|------|-------------|--------|---------|---------|---------| -| **In-memory SQLite** | `sqlite+aiosqlite://` | ❌ 进程退出即丢 | ❌ 不需要 | `aiosqlite` | 纯原型/Demo/团队讨论数据样本,不关心数据保留 | -| **File-backed SQLite** | `sqlite+aiosqlite:///./var/.db` | ✅ 文件 | ✅ 必须 | `aiosqlite` | 本地开发、单人项目、轻量持久化 | -| **Docker PostgreSQL** | `postgresql+asyncpg://user:pwd@localhost:5432/db` | ✅ 容器卷 | ✅ 必须 | `asyncpg` + docker-compose | 团队开发、生产前演练 | -| **Docker MySQL** | `mysql+aiomysql://user:pwd@localhost:3306/db` | ✅ 容器卷 | ✅ 必须 | `aiomysql` + docker-compose | 同上,团队偏好 MySQL | -| **External DB** | 各种 | ✅ | ✅ 必须 | 视驱动 | 已有 DB 基础设施 | - -#### 决策影响(下游 Phase 必须遵守) - -- **Phase 1 `db.py`**:engine URL 取决于此决策 -- **Phase 1 `database.py`**: - - **in-memory**:`init_db()` 做 `create_all` + mock seed(每次重启自动恢复,讨论用样本数据) - - **持久化(file / docker / external)**:`init_db()` 改为 no-op,schema 由 alembic 管,seed 改为一次性 `scripts/load_seed.py`(保留 ID) -- **Phase 1 引入 alembic**(持久化场景必须): - - `alembic init alembic` - - `env.py`:`import src.models` 注册表 + `target_metadata = SQLModel.metadata` + 同步 URL(app 用 async,alembic 用 sync) - - SQLite 必须 `render_as_batch=True`;PostgreSQL / MySQL 不需要 - - `script.py.mako` 模板加 `import sqlmodel`(SQLModel 的 `AutoString` 类型需要) - - `pyproject.toml` 加 `alembic>=1.13` - - 生成 baseline:`alembic revision --autogenerate -m "init schema"` → 检查 → `alembic upgrade head` - - `.gitignore` 加 `var/`(file sqlite 场景) - -#### 用户必须输出的明确结论(写入 `spec/phase0.md`) - -``` -DB 选型:[in-memory sqlite / file sqlite / docker pg / docker mysql / external ___] -async DATABASE_URL:________________ -sync DATABASE_URL_SYNC(alembic + load_seed 用):________________ -是否引入 alembic:[是 / 否] -是否需要 docker-compose:[是 / 否] -init_db() 策略:[create_all+seed / no-op+alembic / 其他] -``` - -**用户未明确选定前,禁止进入 Phase 1。** - -### Step 0-8: 检查清单 - -全部确认后,向用户展示汇总,确保以下问题已回答: - -- [ ] 所有实体和字段是否完整,约束是否清晰? -- [ ] 实体关系方向和基数是否正确? -- [ ] 聚合根是否明确? -- [ ] **每个聚合根的类型是否确认:SQLModel(落表)还是虚拟实体(BaseModel,不落表)?** -- [ ] **Service 切分方案是否由用户确认(不是模型自行决定)?** -- [ ] 核心用例是否覆盖主要业务场景,逻辑是否自洽? -- [ ] 第三方库选型是否确认,维护状态是否已调查? -- [ ] **DB 选型 + 迁移策略是否由用户明确确认(Step 0-7)?** -- [ ] 是否有明显的遗漏或边界情况需要讨论? - -**全部确认后才能进入 Phase 1。** +对于 Spec 管理工作流(目录命名、文件格式、迭代规则、交付验证、迁移指引),读取 `spec-management.md`。 ## 参考实现 @@ -291,15 +106,16 @@ src/ │ │ ├── methods.py # Phase 2: 独立业务方法 │ │ ├── dtos.py # Phase 3: DTO │ │ ├── service.py # Phase 3: UseCaseService -│ │ ├── test.py # Phase 3: unittest, file or folder, depends on complexity -│ │ └── spec.md # Phase 3: 服务说明 +│ │ └── spec.md # Phase 3: 服务说明(测试不放此处,见下方 tests/) │ └── chat/ │ ├── methods.py │ ├── dtos.py │ ├── service.py -│ ├── test.py │ └── spec.md ├── main.py # 逐步扩展(voyager → graphql → create_use_case_router → mcp) +tests/ # 项目级测试目录(不在 service// 下,规避循环导入) +├── conftest.py # 共享 fixture(in-memory sqlite + monkey-patch session) +└── test__methods.py # 每个业务域一个测试文件 alembic/ # Phase 1 持久化场景才引入(file sqlite / docker / external) ├── env.py # 接 SQLModel.metadata + sync URL + render_as_batch(sqlite) ├── script.py.mako # 模板加 import sqlmodel diff --git a/skill/phases/phase0.md b/skill/phases/phase0.md new file mode 100644 index 0000000..6a78717 --- /dev/null +++ b/skill/phases/phase0.md @@ -0,0 +1,242 @@ +# Phase 0: 需求确认(必做) + +**目标**: 与用户逐项确认业务实体、关系、聚合根、用例方法、第三方库选型、DB 持久化策略;产出 `specs/<编号>-<需求简述>/phase0.md`,作为后续 Phase 1~4 的输入。 + +**环境前提**: 进入实现阶段前,必须确认项目运行环境使用 **Python >= 3.12**。如用户当前环境低于 3.12,必须在 Phase 1 前先升级或创建新的 3.12 虚拟环境。 + +**新增/修改文件**: +- `specs/<编号>-<需求简述>/phase0.md` — Phase 0 确认记录(按 spec-management.md 的"写入时机"在 Phase 0 全部确认后写入) +- `specs/<编号>-<需求简述>/story.md` — 用户原始需求 + Overview Design(Phase 0 确认后补 Overview Design 段) + +**关键模式**: +- 反复与用户确认,每一项需明确认可后才能进入下一步 +- 本阶段不写业务代码;产出仅为 spec 文档 +- 第三方库选型与 DB 选型 MUST 由用户拍板,skill 不自行决定(详见 Step 0-6 / 0-7) + +在写任何代码之前,必须与用户逐项确认以下内容。每一项都需要用户明确认可后才算完成。 + +## Step 0-1: 术语与实体定义 + +逐一列出所有业务实体,每个实体说明: + +- **业务含义**(一句话,团队无歧义) +- **核心字段**(名称 + 类型 + 语义说明,不需要穷举,但关键属性不能遗漏) +- **字段约束**(唯一、非空、枚举值、联合唯一等) + +用表格形式呈现,方便用户逐行确认。 + +## Step 0-2: 实体关系 + +用文本 ER 图展示实体间关系,每条关系标明: + +- 方向(1:N / N:1 / M:N) +- 业务含义(如「会话包含多条消息」) +- 是否需要中间实体 + +``` +User ──1:N──→ Participant +Conversation ──1:N──→ Message +... +``` + +**必须与用户确认关系方向和基数是否正确。** + +## Step 0-3: 聚合根 + +明确哪个(或哪些)实体是聚合根。聚合根决定: + +- 主要的业务入口(从哪个实体开始查询) +- @query / @mutation 挂在哪些实体上 +- Phase 3 的 service 划分依据 + +### 根类型选择(核心概念自包含) + +每个聚合根必须明确是 **SQLModel 实体**还是 **虚拟实体(普通 `pydantic.BaseModel`)**: + +| 类型 | 何时选 | 数据持久化 | 例子 | +|------|--------|-----------|------| +| **SQLModel 实体** | 数据需要落库、参与 alembic 迁移、是 GraphQL 实体入口 | ✅ 表 | `User` / `Sprint` / `Conversation` | +| **虚拟实体(BaseModel)** | 响应根不对应ORM表:组装自外部 claims、聚合视图、第三方 SDK DTO | ❌ 不建表 | `CurrentUser`(OIDC claims)、`Page[T]`(分页 wrapper)、第三方 SDK 镜像 | + +#### 虚拟实体是什么、何时选用(10 行内联摘要) + +虚拟实体是 nexusx 提供的一种"不对应数据库表"的聚合根类型,用于响应根字段来自**非 ORM 数据源**的场景: + +- **典型场景**:OIDC/JWT 解析出的当前用户身份(`CurrentUser`,字段从 token claims 取)、分页 wrapper(`Page[T]`,包装 `items: list[T]` + `total: int` 等元数据)、第三方 SDK 返回的镜像数据(如 GitHub repo info)。 +- **声明方式**:虚拟实体是普通的 `pydantic.BaseModel` 子类(不是 SQLModel),通过类属性 `__relationships__` 声明关系(区别于 SQLModel 的 `Relationship(...)`)。 +- **注册方式**:在 `models.py` 中先 `er = ErManager(entities=[...SQLModel 实体...])` 创建 manager,然后**在 `er.create_resolver()` 调用之前**执行 `er.add_virtual_entities([CurrentUser, Page, ...])`。注册顺序很关键:`create_resolver()` 之后注册表已冻结,再调用 `add_virtual_entities` 会抛 `RuntimeError`。 +- **运行时差异**:SQLModel 源走 ORM 自动投递(`_orm_to_dto`),BaseModel 源由用户直接构造 DTO 实例(`CurrentUser(**claims)`),框架不参与数据获取。 +- **混合场景**:如 `Page[TaskDTO]` 这种"虚拟根 + 真实子实体"——根本身是虚拟实体(`Page`),子层 `TaskDTO` 投影自真实 `Task` 表;Phase 3 DTO 组合时由用户负责填 `items`。 + +**判断依据**:如果根字段全部来自数据库表 → SQLModel;如果字段来自请求上下文(JWT、headers)或聚合多个源 → 虚拟实体。 + +延伸阅读:`docs/guide/virtual_entities.md`(nexusx 包内,含完整代码示例)。 + +## Step 0-4: 业务域划分 + 用例方法 + +**⚠️ 禁止自行决定 Service 切分方案。必须提出候选方案与用户讨论,由用户最终确认。** + +### Step 0-4a: 提出 Service 切分候选方案 + +业务域(Service)按功能边界划分,不按实体划分。Service 切分直接影响: +- 目录结构(`service//`) +- Phase 2 的 methods.py 粒度 +- Phase 3 的 UseCaseService 类划分 +- MCP 和 REST 的入口组织 + +**必须向用户提出至少一种候选方案**,说明每种方案的切分依据和优劣,由用户选择或修正。 + +常见的切分策略参考: + +| 策略 | 示例 | 适用场景 | +|------|------|----------| +| 按业务功能域 | `auth` / `chat` / `order` | 业务边界清晰,领域间耦合低 | +| 按聚合根 | `user` / `conversation` / `message` | 实体独立性强,CRUD 为主 | +| 混合(功能域 + 独立聚合) | `auth` / `chat`(含 conversation+message) | 部分域跨实体协作 | + +**向用户展示的格式:** + +``` +方案 A:按功能域 + auth/ → register, login + chat/ → create_conversation, list_messages, send_message + 优势:业务内聚,方法自然归组 + 劣势:chat 域可能过大 + +方案 B:按聚合根 + user/ → register, login + conversation/ → create_conversation, list_messages + message/ → send_message + 优势:每个 service 粒度均匀 + 劣势:conversation 和 message 强耦合却拆开了 +``` + +**必须等用户明确选择后才能继续。** 如果用户提出自己的分法,按用户的来。 + +### Step 0-4b: 按确认的 Service 划分列出用例方法 + +用户确认 Service 切分后,按每个业务域列出用例方法。每个方法说明: + +- **方法名**(动词开头,如 `create_conversation`、`list_messages`) +- **业务意图**(一句话,如「创建群聊并自动将创建者加入为 owner」) +- **挂载实体**(挂在哪个 Entity 的 @query / @mutation 上,供 GraphQL 使用) +- **关键参数**(列出参数名和含义,不需要完整签名) + +示例格式: + +| 业务域 | 方法名 | 业务意图 | 挂载实体 | 关键参数 | +|--------|--------|----------|----------|----------| +| auth | register | 注册新用户 | User | username, nickname, password | +| auth | login | 登录返回 JWT | User | username, password | +| chat | create_conversation | 创建会话 | Conversation | type, creator_id, name | +| chat | list_messages | 查询会话消息(分页) | Conversation | conversation_id, before_id, limit | + +**用例方法不需要实现细节,但必须逻辑自洽**: +- mutation 的参数是否足以完成操作 +- 创建类 mutation 是否有遗漏的副作用(如自动创建关联记录) +- 查询类方法是否覆盖了核心场景 + +## Step 0-5: GraphQL 定位 + +GraphQL 是辅助开发测试和 AI 测试的接口,不是正式 API。 + +业务方法的定义和挂载关系: + +``` +service//methods.py ← 独立定义业务逻辑(核心) + ↓ 挂载 ↓ 挂载 + Entity @query/@mutation UseCaseService @query/@mutation + (GraphQL 辅助测试) (REST + MCP 正式接口) +``` + +- Phase 2:方法体在 `service//methods.py` 中实现,`models.py` 的 `mount_method()` 函数挂载到 Entity,`main.py` 显式调用 +- Phase 3:同一个方法挂载到 UseCaseService(REST/MCP 使用),DTO 转换在 Service 层完成 + +## Step 0-6: 第三方库确认 + +列出项目中涉及的非业务功能领域(认证、实时推送、文件存储、数据迁移等),对每个领域: + +- **说明候选方案**(推荐成熟第三方库 vs 手写实现) +- **给出推荐理由**(社区活跃度、维护状态、与 FastAPI/SQLModel 的兼容性) +- **必须调查用户提到的第三方库的当前维护状态**(避免选用已停止维护的库) + +用表格形式呈现: + +| 功能领域 | 推荐方案 | 理由 | 备注 | +|----------|----------|------|------| +| 认证 | ... | ... | ... | +| ... | ... | ... | ... | + +**注意事项**: +- 优先使用 FastAPI 生态内的主流方案,减少集成风险 +- 如果用户指定了某个库,必须先调查其维护状态和兼容性,发现问题要及时告知用户并提供替代方案 +- 对于 nexusx 已覆盖的领域(ORM、GraphQL、MCP),不再重复讨论 + +**必须与用户确认每个领域的选型后才能继续。** + +## Step 0-7: 数据持久化与迁移策略 + +**⚠️ 必须由用户明确选定 DB 类型与迁移策略,决定 Phase 1 的 `db.py` / `database.py` 实现方式以及是否引入 alembic。** + +### 选型决策表 + +| 选项 | async DB URL | 持久化 | Alembic | 额外依赖 | 适用场景 | +|------|-------------|--------|---------|---------|---------| +| **In-memory SQLite** | `sqlite+aiosqlite://` | ❌ 进程退出即丢 | ❌ 不需要 | `aiosqlite` | 纯原型/Demo/团队讨论数据样本,不关心数据保留 | +| **File-backed SQLite** | `sqlite+aiosqlite:///./var/.db` | ✅ 文件 | ✅ 必须 | `aiosqlite` | 本地开发、单人项目、轻量持久化 | +| **Docker PostgreSQL** | `postgresql+asyncpg://user:pwd@localhost:5432/db` | ✅ 容器卷 | ✅ 必须 | `asyncpg` + docker-compose | 团队开发、生产前演练 | +| **Docker MySQL** | `mysql+aiomysql://user:pwd@localhost:3306/db` | ✅ 容器卷 | ✅ 必须 | `aiomysql` + docker-compose | 同上,团队偏好 MySQL | +| **External DB** | 各种 | ✅ | ✅ 必须 | 视驱动 | 已有 DB 基础设施 | + +### 决策影响(下游 Phase 必须遵守) + +- **Phase 1 `db.py`**:engine URL 取决于此决策 +- **Phase 1 `database.py`**: + - **in-memory**:`init_db()` 做 `create_all` + mock seed(每次重启自动恢复,讨论用样本数据) + - **持久化(file / docker / external)**:`init_db()` 改为 no-op,schema 由 alembic 管,seed 改为一次性 `scripts/load_seed.py`(保留 ID) +- **Phase 1 引入 alembic**(持久化场景必须): + - `alembic init alembic` + - `env.py`:`import src.models` 注册表 + `target_metadata = SQLModel.metadata` + 同步 URL(app 用 async,alembic 用 sync) + - SQLite 必须 `render_as_batch=True`;PostgreSQL / MySQL 不需要 + - `script.py.mako` 模板加 `import sqlmodel`(SQLModel 的 `AutoString` 类型需要) + - `pyproject.toml` 加 `alembic>=1.13` + - 生成 baseline:`alembic revision --autogenerate -m "init schema"` → 检查 → `alembic upgrade head` + - `.gitignore` 加 `var/`(file sqlite 场景) + +### 用户必须输出的明确结论(写入 `specs/<编号>-<需求简述>/phase0.md`) + +``` +DB 选型:[in-memory sqlite / file sqlite / docker pg / docker mysql / external ___] +async DATABASE_URL:________________ +sync DATABASE_URL_SYNC(alembic + load_seed 用):________________ +是否引入 alembic:[是 / 否] +是否需要 docker-compose:[是 / 否] +init_db() 策略:[create_all+seed / no-op+alembic / 其他] +``` + +**用户未明确选定前,禁止进入 Phase 1。** + +## Step 0-8: 检查清单 + +全部确认后,向用户展示汇总,确保以下问题已回答: + +- [ ] 所有实体和字段是否完整,约束是否清晰? +- [ ] 实体关系方向和基数是否正确? +- [ ] 聚合根是否明确? +- [ ] **每个聚合根的类型是否确认:SQLModel(落表)还是虚拟实体(BaseModel,不落表)?** +- [ ] **Service 切分方案是否由用户确认(不是模型自行决定)?** +- [ ] 核心用例是否覆盖主要业务场景,逻辑是否自洽? +- [ ] 第三方库选型是否确认,维护状态是否已调查? +- [ ] **DB 选型 + 迁移策略是否由用户明确确认(Step 0-7)?** +- [ ] 是否有明显的遗漏或边界情况需要讨论? + +**全部确认后才能进入 Phase 1。** + +## 老用户迭代:何时跳过 Phase 0 + +老用户做增量迭代时,可跳过 Phase 0 的完整重过,但仅限以下场景: + +- ✅ **仅新增字段 / 方法 / 关系** → 跳过 Step 0-1~0-3 的完整重过,只确认 delta +- ❌ **聚合根变更、新业务域、DB 选型切换** → MUST 重做 Phase 0 对应 Step + +完整规则与迁移操作步骤参见 `spec-management.md` 的「从旧结构迁移」与「迭代功能的处理」章节(双向引用)。 diff --git a/skill/phases/phase1.md b/skill/phases/phase1.md index 7705de6..ef2fa33 100644 --- a/skill/phases/phase1.md +++ b/skill/phases/phase1.md @@ -2,6 +2,8 @@ **目标**: 定义纯实体模型(字段 + 关系声明)、mock seed data,用 ER diagram 可视化供团队讨论。**不含任何业务方法**。 +**硬性前提**: Phase 1 必须在 **Python >= 3.12** 的环境中执行。若使用更低版本 Python,`SQLModel + SQLAlchemy + nexusx ErManager` 在 `Relationship(...)`、自引用关系、虚拟实体注册与 Alembic 自动迁移上可能出现兼容问题。 + **新增/修改文件**: - `db.py` — engine + session_factory(不导入 models,避免循环依赖)。**engine URL 由 Phase 0 Step 0-7 的 DB 选型决定**(in-memory sqlite / file sqlite / docker pg / docker mysql / external) - `models.py` — 纯 SQLModel 实体 + Relationship(仅字段和关系,不含方法,不导入 `nexusx`)。所有 Relationship 必须加 `sa_relationship_kwargs={"lazy": "noload"}` @@ -11,6 +13,7 @@ - `main.py` — FastAPI + Voyager(ER diagram 可视化) **关键模式**: +- Python 解释器必须为 `>= 3.12`,建议在项目目录显式创建 `.venv` 并固定该版本 - SQLModel 实体 + Relationship 声明关系方向,**不包含任何 @query/@mutation 方法** - 每个 Model 必须有 docstring 说明业务含义,每个 Field 必须有 `description` 说明字段语义 - mock seed data 用于讨论数据样本是否合理(数量、关联关系、边界值)。持久化场景下 seed 数据写到 `var/seed_data.json`,由 `scripts/load_seed.py` 灌入 @@ -19,7 +22,7 @@ - **如果 Phase 0 Step 0-3 选了虚拟实体根**(普通 `pydantic.BaseModel`,不落表):在 `ErManager(entities=[...])` 创建后、`create_resolver()` 调用**之前**,调用 `er.add_virtual_entities([CurrentUser, Page, ...])` 注册。虚拟实体通过类属性 `__relationships__` 声明关系(不是 SQLAlchemy `Relationship`)。注册后再调 `er.create_resolver()`,否则注册表已冻结会抛 `RuntimeError`。详见 `docs/guide/virtual_entities.md` **V 降 — 定义验收标准:** -进入 Phase 1 实现之前,在 `spec/phase1.md` 中记录以下验收标准: +进入 Phase 1 实现之前,在 `specs/<编号>-<需求简述>/phase1.md` 中记录以下验收标准: | # | 验收项 | 验证方式 | |---|--------|----------| @@ -56,7 +59,7 @@ - ❌ uvicorn `--reload` 模式下,改 `db.py` URL 后会立即 reload,老的 `init_db()` 可能跑了一次 create_all 把表建到新文件里 → 后续 autogenerate 看到表已存在生成空迁移。**解决**:先 dump 数据 → 删 DB 文件 → 改代码 → autogenerate → upgrade → load_seed **V 升 — 逐条回查验收:** -按验收标准逐条验证,用户确认后才写入 `spec/phase1.md`: +按验收标准逐条验证,用户确认后才写入 `specs/<编号>-<需求简述>/phase1.md`: - [ ] 1. Voyager ER 图:实体节点、关系线、聚合根高亮 - [ ] 2. Entity 纯字段:无 @query/@mutation 方法,无 `nexusx` 导入 diff --git a/skill/phases/phase2.md b/skill/phases/phase2.md index 4ac2e70..1ee6d2d 100644 --- a/skill/phases/phase2.md +++ b/skill/phases/phase2.md @@ -37,7 +37,7 @@ - `mount_method()` 定义放在 Entity class 之后、ErManager 之前 **V 降 — 定义验收标准:** -进入 Phase 2 编码之前,先与用户确认测试验收集并写入 `spec/phase2.md`: +进入 Phase 2 编码之前,先与用户确认测试验收集并写入 `specs/<编号>-<需求简述>/phase2.md`: | # | 方法 | 测试场景 | 预期结果 | 验证方式 | |---|------|----------|----------|----------| diff --git a/skill/phases/phase3.md b/skill/phases/phase3.md index f86f913..11495b1 100644 --- a/skill/phases/phase3.md +++ b/skill/phases/phase3.md @@ -10,16 +10,18 @@ **关键模式**: - `DefineSubset` + `SubsetConfig` 定义响应 DTO(字段选择、FK 隐藏) -- **3.2+ `DefineSubset.__subset__` 源可以是任意 `pydantic.BaseModel`**(不限于 SQLModel)—— 如果 Phase 0 Step 0-3 选了虚拟实体根(`CurrentUser`、`Page[T]`、第三方 SDK DTO 等),用同一语法从 BaseModel 源字段中选子集。SQLModel 源走 ORM 自动投递(`_orm_to_dto`),BaseModel 源由用户直接构造 DTO 实例,框架不参与数据获取(详见 `docs/guide/virtual_entities.md`) +- **`DefineSubset.__subset__` 源可以是任意 `pydantic.BaseModel`**(不限于 SQLModel)—— 如果 Phase 0 Step 0-3 选了虚拟实体根(`CurrentUser`、`Page[T]`、第三方 SDK DTO 等),用同一语法从 BaseModel 源字段中选子集。SQLModel 源走 ORM 自动投递(`_orm_to_dto`),BaseModel 源由用户直接构造 DTO 实例,框架不参与数据获取(详见 `docs/guide/virtual_entities.md`) - `AutoLoad` 标记 DTO 关系字段为自动加载(配合 Resolver implicit auto-load 使用,显式声明自动加载意图) -- **跨层数据流(3.x 新增)**:当 DTO 字段需要从请求上下文(用户身份、trace ID)、父层传值、子层收集结果中拿数据,而非从 ORM 实体——用 `nexusx` 的三个 helper(详见 `docs/api/api_cross_layer.md`): - - `ExposeAs(field_name, source=...)` — 从 `FromContext` 暴露的字段取值 - - `SendTo(field_name)` — 父层向子层下发值 - - `Collector(field_name)` — 子层结果聚合回父层 +- **跨层数据流(核心概念内联)**:当 DTO 字段需要从**请求上下文**(用户身份、trace ID)、**父层传值**、**子层收集结果**中拿数据,而非从 ORM 实体——用 `nexusx` 的三个 helper。延伸阅读:`docs/api/api_cross_layer.md`。 + - **`ExposeAs(field_name, source=...)`** — 在子层 DTO 上声明:该字段值由父层通过 `FromContext` 暴露的字段提供。**典型场景**:子层 `MessageDTO.author_name` 直接从父层注入的 `current_user.name` 取值,无需 JOIN 用户表。 + - **`SendTo(field_name)`** — 在父层 DTO 字段上声明:该字段值会下发给所有子层匹配的 `ExposeAs` 同名字段。**典型场景**:父层 `ConversationDTO` 持有 `current_user_id`,下发给所有子层 `MessageDTO` 复用,避免每条 message 重复查 user。 + - **`Collector(field_name)`** — 在父层 DTO 字段上声明:该字段值由子层结果聚合而成。**典型场景**:父层 `ConversationDTO.last_message` 收集子层 `MessageDTO` 列表后取最新一条;`reply_count` 收集子层计数。 + - **与 ORM 关系的区别**:`Relationship` 从当前实体的 FK 加载关联实体(数据源 = DB);`ExposeAs` 等从上下文 / 父子传值(数据源 = 请求上下文或父层 DTO,**无 DB 访问**)。 + - **何时用**:响应字段无法从单一实体表派生时——如混合外部 claims + DB 数据的视图 DTO,或父层聚合子层统计的报表类响应。 - `ErManager` + `Resolver` 自动加载关系(implicit auto-load) - `UseCaseService` 统一业务逻辑入口(同时服务 MCP、REST、GraphQL、CLI、JSON-RPC) - `@query` / `@mutation` 装饰器标记服务方法 -- **UseCaseService 方法必须声明返回类型注解**(如 `-> list[ChatSummary]`、`-> ChatSummary | None`),3.0 起 compose schema 生成器强校验,缺注解直接 `MissingReturnAnnotationError` 启动期报错 +- **UseCaseService 方法必须声明返回类型注解**(如 `-> list[ChatSummary]`、`-> ChatSummary | None`),compose schema 生成器强校验,缺注解直接 `MissingReturnAnnotationError` 启动期报错 - **UseCaseService 复用 `service//methods.py` 中的核心逻辑,不重新实现**: - **query 方法(list)**:调用 methods.py 拿 `list[Model]` → `[DtoType.model_validate(m) for m in models]` → `Resolver().resolve(dtos)` - **query 方法(get 单条)**:调用 methods.py 拿 `Model | None` → `DtoType.model_validate(entity)` → `Resolver().resolve(dto)` @@ -58,6 +60,15 @@ from nexusx import create_use_case_graphql_mcp_server, UseCaseAppConfig mcp = create_use_case_graphql_mcp_server(apps=[app_config], name="API") ``` +- **UseCase MCP 版本演进(核心概念内联)**: + - **当前唯一推荐**:`create_use_case_graphql_mcp_server` — 4 层渐进披露 + Layer 3 接收 GraphQL 字符串。 + - **已移除(不要再用)**:老的 `create_use_case_mcp_server`(4 层 + JSON 参数表)和 `create_use_case_flat_server`(扁平),3.0 起从 nexusx 删除。 + - **从 2.x 迁移到 3.0+ 的三步**: + 1. 入口替换:`create_use_case_mcp_server(app_config)` → `create_use_case_graphql_mcp_server(apps=[app_config])`(注意 `apps=` 是列表) + 2. Layer 3 接口:从「按方法名逐个调 + JSON 参数」改为 `compose_query(app_name, query)` 接 GraphQL 字符串,支持字段投影 / 嵌套查询 / 参数透传 + 3. 客户端探索顺序:Layer 1 (`list_apps`) → Layer 2 (`describe_compose_method`) 拿 SDL 片段 → Layer 3 直接发 GraphQL,**不再**逐方法拼参数 + - **拒绝内省**:Layer 3 不响应 `__schema` / `__type` / `__typename`,引导用 Layer 1/2 替代,保持 MCP 响应紧凑。 + - 完整迁移指南参见 `docs/migrations/3.0-use-case-graphql.md`(延伸阅读)。 - **可选:GraphQL HTTP endpoint(GraphiQL 友好)** — 当需要直接对外暴露 GraphQL(浏览器/curl/Apollo 客户端,非 MCP 协议)时,用 `build_compose_schema` + `compose_introspect` + `execute_compose_query` 自建一个 FastAPI `/graphql` 路由。`compose_introspect` 处理 `__schema` 等 GraphiQL 启动查询,`execute_compose_query` 处理数据查询。注意 import 路径——这三个函数中只有 `build_compose_schema` 和 `compose_introspect` 在顶层 `nexusx` 导出,`execute_compose_query` 和 `is_introspection_query` 需要从子模块拿: ```python @@ -74,15 +85,30 @@ - MCP http_app 必须使用 `transport="streamable-http", stateless_http=True` - MCP http_app 的 lifespan 必须在 FastAPI lifespan 中通过 `async with mcp_http.lifespan(mcp_http)` 嵌套启动 - MCP http_app 对象必须在 lifespan 函数定义之前创建,以便引用 -- **3.0 起 UseCase 出口形态**(由用户在 Phase 3 决定取舍,按需启用): - | 出口 | 入口 | 适用场景 | - |------|------|---------| +- **UseCase 出口形态**(由用户在 Phase 3 决定取舍;版本门槛参见 SKILL.md `## 适用版本`): + + **推荐默认组合**(模板 `main.py` 默认启用,覆盖 AI agent / 传统 HTTP / 可视化三类主流场景): + + | 出口 | 入口 API | 适用场景 | + |------|---------|---------| | MCP(AI agent) | `create_use_case_graphql_mcp_server` | Claude Desktop / Cursor 等 MCP client;4 层渐进披露控制 token | - | GraphQL HTTP | 自建 `/graphql` + `compose_introspect` | 标准 GraphQL 生态(GraphiQL、Apollo、curl);需要 schema 内省 | - | REST | `create_use_case_router` | OpenAPI 友好的传统 HTTP 客户端;自动生成文档 | - | JSON-RPC | `create_jsonrpc_router` | 轻量 RPC(与 REST 二选一) | - | CLI | `create_use_case_cli` | 本地调试 / 脚本化任务 | - | 可视化 | `create_use_case_voyager` | 开发期 ER / 服务结构可视化 | + | REST | `create_use_case_router` | OpenAPI 友好的传统 HTTP 客户端;自动生成文档;Phase 4 TS SDK 链路必经 | + | Voyager 可视化 | `create_use_case_voyager` | 开发期 ER / 服务结构可视化 | + | GraphQL HTTP(GraphiQL) | 自建 `/graphql` + `GraphQLHandler` | 标准 GraphQL 生态;开发期辅助测试 | + + **可选扩展**(按需启用,模板中以注释形式保留): + + | 出口 | 入口 API | 何时启用 | + |------|---------|---------| + | JSON-RPC | `create_jsonrpc_router` | 需要轻量 RPC(与 REST 二选一);不需要 OpenAPI 文档时 | + | CLI | `create_use_case_cli` | 本地调试 / 脚本化任务;需要 typer 依赖 | + + **决策引导**(一分钟选定出口组合): + - 给 AI agent 用(Claude Desktop / Cursor)→ **MCP** 必选 + - 给传统 HTTP 客户端用 → **REST**(或 JSON-RPC 替代) + - 浏览器开发期探索 → **Voyager** + **GraphQL HTTP** + - 命令行脚本化 → **CLI** + - 不确定?默认启用「推荐组合」即可覆盖 90% 场景 - **main.py 典型模式 — REST + MCP + Voyager**(按需扩展 GraphQL HTTP / JSON-RPC / CLI): ```python from nexusx import ( @@ -120,7 +146,7 @@ ``` **V 降 — 定义验收标准:** -进入 Phase 3 编码之前,先与用户确认以下验收项并写入 `spec/phase3.md`: +进入 Phase 3 编码之前,先与用户确认以下验收项并写入 `specs/<编号>-<需求简述>/phase3.md`: | # | 验收项 | 验证方式 | |---|--------|----------| @@ -147,17 +173,17 @@ ## 踩坑经验 1. **不要在 DefineSubset 文件中使用 `from __future__ import annotations`** — 会使类型注解变字符串,SubsetMeta 无法检测 Annotated 元数据 -2. **DTO 字段类型必须用 DTO 类型** — 不能直接用 SQLModel 实体,3.0 起 compose schema 生成器会主动报 `SQLModelInDtoFieldError`(项目约定 #7) +2. **DTO 字段类型必须用 DTO 类型** — 不能直接用 SQLModel 实体,compose schema 生成器会主动报 `SQLModelInDtoFieldError`(项目约定 #7) 3. **ErManager base 和 entities 互斥** — 不能同时提供 4. **UseCaseService 只有被 @query/@mutation 装饰的 async classmethod 会被发现** — 普通方法不会暴露 5. **build_dto_select → dict(row._mapping) → DTO 构造** — 这是 Core API 的标准查询模式 6. **每个 service 子目录必须包含 spec.md** — 记录服务目的、用途、方法需求、DTO 说明和变更记录,方便团队理解服务边界 7. **fastmcp>=3.2.4 挂载到 FastAPI 需要 lifespan 合并** — `app.mount("/mcp", mcp.http_app(path="/"))` 会报 `Task group is not initialized`。必须:(1) 使用 `transport="streamable-http", stateless_http=True`;(2) 在 lifespan 函数定义之前创建 MCP http_app 对象;(3) 将 MCP http_app 的 lifespan 嵌套到 FastAPI lifespan 中(`async with mcp_http.lifespan(mcp_http):`) 8. **Use `create_use_case_router()` 而非手写路由** — 手写路由无法声明 `response_model`,导致 OpenAPI spec 中响应类型为空(`unknown`),TS SDK 无法生成有效类型。`create_use_case_router()` 从 UseCaseService 方法的返回类型注解(如 `-> list[ChatSummary]`)自动提取 `response_model`,使 FastAPI 在 OpenAPI spec 中正确描述响应结构 -9. **UseCaseService 方法必须声明返回类型注解** — 3.0 起 compose schema 生成器(`build_compose_schema`)强校验,缺注解的方法在 MCP server 构造时抛 `MissingReturnAnnotationError`;同时 `create_use_case_router()` 也通过 `get_type_hints(method).get("return")` 提取返回类型作为 `response_model` +9. **UseCaseService 方法必须声明返回类型注解** — compose schema 生成器(`build_compose_schema`)强校验,缺注解的方法在 MCP server 构造时抛 `MissingReturnAnnotationError`;同时 `create_use_case_router()` 也通过 `get_type_hints(method).get("return")` 提取返回类型作为 `response_model` 10. **methods.py 返回 Model,service.py 负责 DTO 转换** — methods.py 是纯业务逻辑层,所有方法(query + mutation)返回 ORM Model 实体。service.py 统一调用 methods.py,DTO 转换在 service.py 中进行:(1) list 方法调 methods 拿 `list[Model]` → `[DtoType.model_validate(m) for m in models]` → `Resolver().resolve(dtos)`;(2) 单条 get 方法调 methods 拿 `Model | None` → `DtoType.model_validate(entity)` → `Resolver().resolve(dto)`;(3) mutation 方法同单条 get。service.py 不直接操作数据库 11. **`create_use_case_graphql_mcp_server()` 返回 FastMCP 实例,可直接添加 `@mcp.prompt()`** — 如果项目需要 MCP prompt 功能,这个挂载点很方便 12. **`create_jsonrpc_router()` 提供轻量 RPC 协议** — 方法命名为 `ServiceName.method_name`,适合不需要 REST 语义的场景。与 `create_use_case_router()` 二选一 13. **`create_use_case_cli()` 生成 Typer CLI 命令行工具** — 每个 service 成为一个命令组,每个方法成为子命令。适合需要本地调试脚本的场景。需要额外依赖 `typer` -14. **3.0 起 UseCase MCP 只有 GraphQL 模式**(`create_use_case_graphql_mcp_server`)。老的两套直接调用式 MCP(`create_use_case_mcp_server` 4 层 + `create_use_case_flat_server` 扁平)已**移除**,迁移见 `docs/migrations/3.0-use-case-graphql.md`。GraphQL 模式下 Layer 3 (`compose_query`) 接收标准 GraphQL 字符串而非 JSON 参数表,支持字段投影、嵌套查询、参数透传;同时**拒绝内省**保持 MCP 响应紧凑 +14. **UseCase MCP 只有 GraphQL 模式**(`create_use_case_graphql_mcp_server`)。老的两套直接调用式 MCP(`create_use_case_mcp_server` 4 层 + `create_use_case_flat_server` 扁平)已**移除**,迁移见 `docs/migrations/3.0-use-case-graphql.md`。GraphQL 模式下 Layer 3 (`compose_query`) 接收标准 GraphQL 字符串而非 JSON 参数表,支持字段投影、嵌套查询、参数透传;同时**拒绝内省**保持 MCP 响应紧凑 15. **GraphQL HTTP endpoint 与 MCP 是两条独立通道** — MCP 走 MCP 协议(4 层渐进披露,Layer 3 拒绝内省);GraphQL HTTP endpoint 走标准 GraphQL over HTTP(接受内省以兼容 GraphiQL)。两者共用同一个 `ComposeSchema`,但路由不同:MCP 的内省走 Layer 1/2 工具,HTTP 的内省走 `compose_introspect` diff --git a/skill/phases/phase4.md b/skill/phases/phase4.md index 44f2eec..4135be0 100644 --- a/skill/phases/phase4.md +++ b/skill/phases/phase4.md @@ -2,10 +2,10 @@ **目标**: 从 FastAPI OpenAPI spec 生成 TypeScript SDK(callable classes + types)。 -**前提**: Phase 3 必须使用 `create_use_case_router()` 生成 REST 路由(而非手写 router),才能在 OpenAPI spec 中正确暴露 `response_model`。 +**前提**: Phase 3 必须使用 `create_use_case_router()` 生成 REST 路由(而非手写 router),才能在 OpenAPI spec 中正确暴露 `response_model`。**版本门槛**(`@hey-api/openapi-ts` 等 TS 工具链)参见 SKILL.md `## 适用版本`;本阶段不修改 nexusx 框架代码,仅消费 Phase 3 的 OpenAPI 产物。 **V 降 — 定义验收标准:** -确认后写入 `spec/phase4.md`: +确认后写入 `specs/<编号>-<需求简述>/phase4.md`: | # | 验收项 | 验证方式 | |---|--------|----------| @@ -58,6 +58,7 @@ cd fe && npm install && npm run generate-client - [ ] 1. TS 类型覆盖:所有 UseCaseService 的返回类型都有对应定义 - [ ] 2. 字段名一致:snake_case 字段名与后端一致 - [ ] 3. 嵌套结构:DTO 的关系字段推导为正确的嵌套 TS 类型 +- [ ] 4. OpenAPI 端点实测:`curl http://localhost:8000/openapi.json` 返回 200,且包含所有 UseCaseService 的 path(运行时验证,对应 quickstart.md 检查 3) ## 踩坑经验 diff --git a/skill/spec-management.md b/skill/spec-management.md index f0462e5..3805dca 100644 --- a/skill/spec-management.md +++ b/skill/spec-management.md @@ -1,5 +1,15 @@ # Spec 管理与工作流 +## 语言要求 + +所有 spec-kit 产物 MUST 使用中文撰写,与项目 `CLAUDE.md` 的中文化要求保持一致。适用范围: + +- 用户故事、需求条目、验收场景、假设说明等叙述性内容 → MUST 中文 +- 框架名、API 名、代码标识符(如 `create_use_case_router`、`SQLModel`)→ 保留原文 +- 章节标题、表格表头 → MUST 中文 + +包括但不限于:`story.md`、`phaseN.md`、`spec.md`、`plan.md`、`tasks.md`、`checklists/*.md`、`contracts/*`、`research.md`、`data-model.md`、`quickstart.md`。 + ## 目录命名 ``` @@ -91,3 +101,30 @@ Phase 0 全部确认后、进入 Phase 1 之前,在 `story.md` 中补充 `## O ## 交付前校验 - **交付前必须校验 spec 文件完整性** — 在告诉用户"任务完成"之前,检查 `specs/<编号>-*/` 下所有 .md 文件是否有内容(非空文件)。合并 Phase 实现时尤其容易遗漏 spec 写入。可用 `wc -l` 快速检查。空文件 = 未完成 + +## 从旧结构迁移 + +老项目如果使用了 skill 早期的结构约定,按以下规则迁移到当前结构。**迁移时 MUST 保留 spec 编号**(只允许改描述部分),保证 git 历史连续与外部引用不断裂。 + +### 路径迁移 + +| 旧路径 | 新路径 | 说明 | +|---|---|---| +| `spec/phase0.md` 等单数形式 | `specs/<编号>-<需求简述>/phaseN.md` | 与 `## 目录命名` 一致 | +| Phase 0 内联在 SKILL.md | `phases/phase0.md` 外置 | skill 文档侧的迁移,项目侧无影响 | +| `service//test.py` | `tests/test__methods.py` | 模板代码侧的迁移,规避循环导入 | + +### 操作步骤 + +1. **重命名而非复制**:`git mv specs/<旧编号>-<旧描述>/ specs/<旧编号>-<新描述>/`,保留编号 +2. **路径引用全量替换**:在 `story.md` / `phaseN.md` 中 grep 旧路径,逐处替换 +3. **跑完整性校验**:交付前校验(见上一节) + +### 跳过 Phase 0 的判定标准 + +老用户做增量迭代时,可跳过 Phase 0 的完整重过,但仅限以下场景: + +- ✅ 仅新增字段 / 方法 / 关系 → 跳过 Step 0-1~0-3 的完整重过,只确认 delta +- ❌ 聚合根变更、新业务域、DB 选型切换 → MUST 重做 Phase 0 对应 Step + +详见 `phases/phase0.md` 的"老用户迭代"章节(双向引用)。 diff --git a/skill/template/pyproject.toml b/skill/template/pyproject.toml index e5ea289..9f32437 100644 --- a/skill/template/pyproject.toml +++ b/skill/template/pyproject.toml @@ -14,6 +14,13 @@ dependencies = [ [project.optional-dependencies] test = ["pytest", "pytest-asyncio"] +# 持久化场景(file sqlite / docker pg / docker mysql / external)按需启用, +# 详见 phases/phase1.md Step 0-7 决策树 +persist = [ + "alembic>=1.13", +] +persist-pg = ["nexusx-template[persist]", "asyncpg"] +persist-mysql = ["nexusx-template[persist]", "aiomysql"] [build-system] requires = ["hatchling"] diff --git a/skill/template/src/main.py b/skill/template/src/main.py index 4e3cf37..b29be6e 100644 --- a/skill/template/src/main.py +++ b/skill/template/src/main.py @@ -24,6 +24,7 @@ from src.models import BaseEntity, er, mount_method # noqa: E402 from src.service.sprint.service import SprintService # noqa: E402 from src.service.task.service import TaskService # noqa: E402 +from src.service.user.service import UserService # noqa: E402 # ── Mount methods onto entities (must be called before GraphQL handler) ── @@ -36,6 +37,12 @@ session_factory=async_session, ) +# ── base 实体层 MCP(与下方 UseCase 层 MCP 不同)────────────────────── +# 此处 `create_mcp_server` 暴露 BaseEntity 子类的 @query/@mutation(Phase 2 挂载的方法)。 +# 与 UseCase 层 MCP(`create_use_case_graphql_mcp_server`)的区别: +# - base 层:直接对应 SQLModel 实体的 GraphQL,开发期辅助测试用 +# - UseCase 层:经 DTO 组装的 GraphQL,含 4 层渐进披露,对 AI agent 友好 +# 两者共存演示层级差异;生产可只保留 UseCase 层。 mcp = create_mcp_server( apps=[{ "name": "template", @@ -50,8 +57,8 @@ use_case_config = UseCaseAppConfig( name="template", - services=[TaskService, SprintService], - description="Task & Sprint business services", + services=[UserService, TaskService, SprintService], + description="User, Task & Sprint business services", ) use_case_mcp = create_use_case_graphql_mcp_server( @@ -96,7 +103,7 @@ async def lifespan(app: FastAPI): from nexusx import create_use_case_voyager # noqa: E402 voyager_app = create_use_case_voyager( - services=[TaskService, SprintService], + services=[UserService, TaskService, SprintService], er_manager=er, name="Template API", ) @@ -131,14 +138,25 @@ async def graphql_schema(): return graphql_handler.get_sdl() -# ── REST router (Phase 3) ──────────────────────────────────────────── +# ── REST router(默认推荐出口 — OpenAPI / TS SDK 链路必经)───────────── from nexusx import create_use_case_router # noqa: E402 app.include_router(create_use_case_router(use_case_config)) -# ── MCP mounts ─────────────────────────────────────────────────────── +# ── MCP mounts(默认推荐出口 — Voyager 见上方,UseCase 见下方)──────── -app.mount("/mcp", mcp_http) -app.mount("/mcp-usecase", use_case_mcp_http) +app.mount("/mcp", mcp_http) # base 实体层 MCP(可选,演示层级) +app.mount("/mcp-usecase", use_case_mcp_http) # UseCase 层 MCP(默认推荐) + + +# ── 可选扩展(按需启用,默认不挂载)────────────────────────────────── +# JSON-RPC 2.0(替代 REST 的轻量 RPC): +# from nexusx import create_jsonrpc_router +# app.include_router(create_jsonrpc_router(use_case_config)) +# CLI(Typer 命令行工具,本地调试 / 脚本化任务): +# from nexusx import create_use_case_cli +# cli = create_use_case_cli(use_case_config) +# # 在 if __name__ == "__main__": cli() 中调用 +# 决策引导参见 phases/phase3.md 的"推荐默认组合"与"可选扩展"两段 diff --git a/skill/template/src/router/__init__.py b/skill/template/src/router/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/skill/template/src/service/sprint/test.py b/skill/template/src/service/sprint/test.py deleted file mode 100644 index e69de29..0000000 diff --git a/skill/template/src/service/task/dtos.py b/skill/template/src/service/task/dtos.py index 76778c2..384ff36 100644 --- a/skill/template/src/service/task/dtos.py +++ b/skill/template/src/service/task/dtos.py @@ -1,10 +1,7 @@ -"""Task-related DTOs — UserSummary, TaskSummary.""" +"""Task-related DTOs — TaskSummary (UserSummary 引用自 user/dtos.py 单一来源).""" from nexusx import DefineSubset, SubsetConfig -from src.models import Task, User - - -class UserSummary(DefineSubset): - __subset__ = SubsetConfig(kls=User, fields=["id", "name"]) +from src.models import Task +from src.service.user.dtos import UserSummary class TaskSummary(DefineSubset): diff --git a/skill/template/src/service/task/test.py b/skill/template/src/service/task/test.py deleted file mode 100644 index e69de29..0000000 diff --git a/skill/template/src/service/user/dtos.py b/skill/template/src/service/user/dtos.py new file mode 100644 index 0000000..77f6c2b --- /dev/null +++ b/skill/template/src/service/user/dtos.py @@ -0,0 +1,13 @@ +"""User-related DTOs — UserSummary (单一来源,被 task / sprint 服务复用).""" +from nexusx import DefineSubset, SubsetConfig +from src.models import User + + +class UserSummary(DefineSubset): + """User 实体的最小投影,含 id 与 name。 + + 作为单一来源被 task / sprint 服务引用(TaskSummary.owner、SprintSummary 间接通过 task), + 演示 nexusx 中 DTO 按需投影与跨 service 复用的模式。 + """ + + __subset__ = SubsetConfig(kls=User, fields=["id", "name"]) diff --git a/skill/template/src/service/user/service.py b/skill/template/src/service/user/service.py new file mode 100644 index 0000000..4f88d47 --- /dev/null +++ b/skill/template/src/service/user/service.py @@ -0,0 +1,28 @@ +"""User UseCaseService — user management (reuses methods.py).""" +from nexusx import UseCaseService, mutation, query +from src.models import Resolver +from src.service.user.dtos import UserSummary +from src.service.user.methods import ( + create_user as _create_user, +) +from src.service.user.methods import ( + list_users as _list_users, +) + + +class UserService(UseCaseService): + """User management — list and create users.""" + + @query + async def list_users(cls) -> list[UserSummary]: + """Get all users.""" + users = await _list_users() + dtos = [UserSummary.model_validate(u) for u in users] + return await Resolver().resolve(dtos) + + @mutation + async def create_user(cls, name: str) -> UserSummary: + """Create a new user (reuses methods.py function).""" + user = await _create_user(name=name) + dto = UserSummary.model_validate(user) + return await Resolver().resolve(dto) diff --git a/skill/template/src/service/user/spec.md b/skill/template/src/service/user/spec.md index d7c0c10..b8d96da 100644 --- a/skill/template/src/service/user/spec.md +++ b/skill/template/src/service/user/spec.md @@ -1,19 +1,42 @@ -# User Service +# UserService -## 服务目的 +## 目的 -用户管理服务。 +用户管理服务,提供用户列表与创建能力,作为 task / sprint 服务的依赖方(`TaskSummary.owner` 引用 `UserSummary`)。 ## 用途 -(Phase 3 补充) +- 列出所有用户 +- 创建新用户 ## 方法需求 -(Phase 3 补充) +| 方法 | 说明 | 返回 | +|------|------|------| +| `list_users` | 获取全部用户 | `list[UserSummary]` | +| `create_user` | 创建用户 | `UserSummary` | + +## DTO + +- `UserSummary` — `id`, `name`(User 实体的最小投影;作为单一来源被 task / sprint 服务复用,演示 nexusx 的 DTO 按需投影与跨 service 复用模式) + +## 调用链 + +``` +UserService.list_users / create_user + ↓ 委托 +service/user/methods.py:list_users / create_user + ↓ 操作 +src.db.async_session → User 表 + ↓ DTO 转换 +UserSummary.model_validate(user) → Resolver().resolve(dtoList) +``` + +service.py 不直接操作数据库,所有 DB 访问通过 methods.py。 ## 变更记录 -| 日期 | 变更内容 | -|------|----------| -| | 初始化 | +| 阶段 | 变更 | +|------|------| +| Phase 2 | methods.py 实现 `list_users`, `create_user` | +| Phase 3 | 初始创建 UserService;UserSummary DTO 沉淀到 `user/dtos.py` 作为单一来源 | diff --git a/skill/template/tests/conftest.py b/skill/template/tests/conftest.py new file mode 100644 index 0000000..2492c91 --- /dev/null +++ b/skill/template/tests/conftest.py @@ -0,0 +1,49 @@ +"""pytest fixtures — in-memory sqlite + async session factory override. + +实现 phase2.md 踩坑 #5:methods.py 的 `from src.db import async_session` 在 +导入时绑定原值,运行时仅 patch `src.db.async_session` 不会影响 methods 模块的 +局部绑定;必须同时 patch `src.db` 与每个 methods 模块。 +""" +import pytest +from sqlalchemy.ext.asyncio import ( + async_sessionmaker, + create_async_engine, +) +from sqlmodel import SQLModel +from sqlmodel.ext.asyncio.session import AsyncSession # SQLModel 扩展版,含 .exec() + +import src.db as src_db # noqa: E402 +import src.models # noqa: F401, E402 — register tables on SQLModel.metadata +import src.service.sprint.methods as sprint_methods +import src.service.task.methods as task_methods +import src.service.user.methods as user_methods + + +@pytest.fixture +async def session_factory(): + """每测试新建 in-memory sqlite + 干净 schema;yield 一个 session factory。 + + 自动 patch 生产 session 与各 methods 模块的局部绑定,使 methods.py 在 + 测试中走测试 engine。测试结束 dispose engine。 + """ + engine = create_async_engine("sqlite+aiosqlite://", future=True) + async with engine.begin() as conn: + await conn.run_sync(SQLModel.metadata.create_all) + factory = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) + + # 同步 patch 4 处:核心 db 模块 + 3 个 service methods 模块 + src_db.async_session = factory + user_methods.async_session = factory + task_methods.async_session = factory + sprint_methods.async_session = factory + + # ⚠️ 未 patch 的绑定(methods 层测试够用,服务层测试需要扩展): + # - src.models.er._session_factory(ErManager 在 import 时按值捕获) + # - src.models.Resolver(通过 er 间接持有 factory) + # - main.graphql_handler.session_factory、main.mcp*.apps[*].session_factory + # 当测试触达 Resolver().resolve() 或 main.app 的 /graphql / /api/* 端点时, + # 上述绑定会让请求落到生产 engine 而非测试 engine,届时需要在此追加 patch。 + + yield factory + + await engine.dispose() diff --git a/skill/template/tests/test_sprint_methods.py b/skill/template/tests/test_sprint_methods.py new file mode 100644 index 0000000..545a6a9 --- /dev/null +++ b/skill/template/tests/test_sprint_methods.py @@ -0,0 +1,29 @@ +"""SprintService / sprint.methods 测试 — 覆盖正常 + 边界场景。""" +import pytest + +from src.service.sprint.methods import create_sprint, get_sprint, list_sprints + + +@pytest.mark.asyncio +async def test_create_sprint_normal(session_factory): + """正常场景:创建 sprint 返回自增 id。""" + sprint = await create_sprint(name="Sprint 1") + assert sprint.id is not None + assert sprint.name == "Sprint 1" + + +@pytest.mark.asyncio +async def test_list_sprints_after_create(session_factory): + """正常场景:多个 sprint 按 id 顺序返回。""" + await create_sprint(name="Sprint 1") + await create_sprint(name="Sprint 2") + sprints = await list_sprints() + assert len(sprints) == 2 + assert [s.name for s in sprints] == ["Sprint 1", "Sprint 2"] + + +@pytest.mark.asyncio +async def test_get_sprint_not_found(session_factory): + """边界场景:查不存在的 id 返回 None(不抛异常)。""" + sprint = await get_sprint(sprint_id=9999) + assert sprint is None diff --git a/skill/template/tests/test_task_methods.py b/skill/template/tests/test_task_methods.py new file mode 100644 index 0000000..a8c6f3f --- /dev/null +++ b/skill/template/tests/test_task_methods.py @@ -0,0 +1,48 @@ +"""TaskService / task.methods 测试 — 覆盖正常 + 边界场景。""" +import pytest + +from src.service.sprint.methods import create_sprint +from src.service.task.methods import ( + create_task, + get_tasks_by_sprint, + list_tasks, +) + + +@pytest.mark.asyncio +async def test_create_task_with_owner(session_factory): + """正常场景:创建带 owner 的 task,字段持久化正确。""" + sprint = await create_sprint(name="Sprint 1") + task = await create_task(title="Write tests", sprint_id=sprint.id, owner_id=None) + assert task.id is not None + assert task.title == "Write tests" + assert task.sprint_id == sprint.id + assert task.done is False # 默认值 + + +@pytest.mark.asyncio +async def test_get_tasks_by_sprint_filters(session_factory): + """正常场景:get_tasks_by_sprint 仅返回该 sprint 的任务。""" + sprint_a = await create_sprint(name="A") + sprint_b = await create_sprint(name="B") + await create_task(title="T1", sprint_id=sprint_a.id) + await create_task(title="T2", sprint_id=sprint_a.id) + await create_task(title="T3", sprint_id=sprint_b.id) + + tasks_a = await get_tasks_by_sprint(sprint_id=sprint_a.id) + assert {t.title for t in tasks_a} == {"T1", "T2"} + + +@pytest.mark.asyncio +async def test_get_tasks_by_sprint_empty(session_factory): + """边界场景:sprint 无任务时返回空列表。""" + sprint = await create_sprint(name="Empty") + tasks = await get_tasks_by_sprint(sprint_id=sprint.id) + assert tasks == [] + + +@pytest.mark.asyncio +async def test_list_tasks_empty(session_factory): + """边界场景:全表为空时返回空列表。""" + tasks = await list_tasks() + assert tasks == [] diff --git a/skill/template/tests/test_user_methods.py b/skill/template/tests/test_user_methods.py new file mode 100644 index 0000000..0954ade --- /dev/null +++ b/skill/template/tests/test_user_methods.py @@ -0,0 +1,32 @@ +"""UserService / user.methods 测试 — 覆盖正常 + 边界场景。 + +对应 phase2.md V 降验收表,每个方法至少 1 个正常 + 1 个边界场景。 +""" +import pytest + +from src.service.user.methods import create_user, list_users + + +@pytest.mark.asyncio +async def test_create_user_normal(session_factory): + """正常场景:创建用户返回 User 实体,id 自增、name 与入参一致。""" + user = await create_user(name="Alice") + assert user.id is not None + assert user.name == "Alice" + + +@pytest.mark.asyncio +async def test_list_users_after_create(session_factory): + """正常场景:创建多个用户后,list_users 返回全部。""" + await create_user(name="Alice") + await create_user(name="Bob") + users = await list_users() + assert len(users) == 2 + assert {u.name for u in users} == {"Alice", "Bob"} + + +@pytest.mark.asyncio +async def test_list_users_empty(session_factory): + """边界场景:空表时返回空列表(非 None、非异常)。""" + users = await list_users() + assert users == [] diff --git a/skill/template/uv.lock b/skill/template/uv.lock index 7ec3a94..4e7af1b 100644 --- a/skill/template/uv.lock +++ b/skill/template/uv.lock @@ -26,6 +26,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/67/cd/0d76dfc5de72bde52f55f53e925c7d152d9c7906634ec1e0cbc7e8d4ad93/aiofile-3.11.1-py3-none-any.whl", hash = "sha256:ce77d14ac07f77bc2b757834a5c129321f3f705c474593deed5ab209079a52c9", size = 20446, upload-time = "2026-05-16T08:18:32.051Z" }, ] +[[package]] +name = "aiomysql" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pymysql" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/e0/302aeffe8d90853556f47f3106b89c16cc2ec2a4d269bdfd82e3f4ae12cc/aiomysql-0.3.2.tar.gz", hash = "sha256:72d15ef5cfc34c03468eb41e1b90adb9fd9347b0b589114bd23ead569a02ac1a", size = 108311, upload-time = "2025-10-22T00:15:21.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/af/aae0153c3e28712adaf462328f6c7a3c196a1c1c27b491de4377dd3e6b52/aiomysql-0.3.2-py3-none-any.whl", hash = "sha256:c82c5ba04137d7afd5c693a258bea8ead2aad77101668044143a991e04632eb2", size = 71834, upload-time = "2025-10-22T00:15:15.905Z" }, +] + [[package]] name = "aiosqlite" version = "0.22.1" @@ -35,6 +47,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/b7/e3bf5133d697a08128598c8d0abc5e16377b51465a33756de24fa7dee953/aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb", size = 17405, upload-time = "2025-12-23T19:25:42.139Z" }, ] +[[package]] +name = "alembic" +version = "1.18.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/cc/ac0bed8e562e7407fe55c3ba85a4dce86e6dbd8730887bd1e406a6c5c18a/alembic-1.18.5.tar.gz", hash = "sha256:1554982221dd17e9a749b53902407578eb305e453f71999e8c7f0a48389fff8e", size = 2060480, upload-time = "2026-06-25T15:20:54.888Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/78/5fe6dc3a3a5b2f5a2a4faef8bfe336d5fa049a38884ab3172e0098160c01/alembic-1.18.5-py3-none-any.whl", hash = "sha256:06d8ba9d04558022f5395e9317de03d270f3dced49cee01f89fe7a13c26f14bc", size = 264664, upload-time = "2026-06-25T15:20:56.673Z" }, +] + [[package]] name = "annotated-doc" version = "0.0.4" @@ -66,6 +92,46 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, ] +[[package]] +name = "asyncpg" +version = "0.31.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/cc/d18065ce2380d80b1bcce927c24a2642efd38918e33fd724bc4bca904877/asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735", size = 993667, upload-time = "2025-11-24T23:27:00.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/a6/59d0a146e61d20e18db7396583242e32e0f120693b67a8de43f1557033e2/asyncpg-0.31.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b44c31e1efc1c15188ef183f287c728e2046abb1d26af4d20858215d50d91fad", size = 662042, upload-time = "2025-11-24T23:25:49.578Z" }, + { url = "https://files.pythonhosted.org/packages/36/01/ffaa189dcb63a2471720615e60185c3f6327716fdc0fc04334436fbb7c65/asyncpg-0.31.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0c89ccf741c067614c9b5fc7f1fc6f3b61ab05ae4aaa966e6fd6b93097c7d20d", size = 638504, upload-time = "2025-11-24T23:25:51.501Z" }, + { url = "https://files.pythonhosted.org/packages/9f/62/3f699ba45d8bd24c5d65392190d19656d74ff0185f42e19d0bbd973bb371/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:12b3b2e39dc5470abd5e98c8d3373e4b1d1234d9fbdedf538798b2c13c64460a", size = 3426241, upload-time = "2025-11-24T23:25:53.278Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d1/a867c2150f9c6e7af6462637f613ba67f78a314b00db220cd26ff559d532/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:aad7a33913fb8bcb5454313377cc330fbb19a0cd5faa7272407d8a0c4257b671", size = 3520321, upload-time = "2025-11-24T23:25:54.982Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1a/cce4c3f246805ecd285a3591222a2611141f1669d002163abef999b60f98/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3df118d94f46d85b2e434fd62c84cb66d5834d5a890725fe625f498e72e4d5ec", size = 3316685, upload-time = "2025-11-24T23:25:57.43Z" }, + { url = "https://files.pythonhosted.org/packages/40/ae/0fc961179e78cc579e138fad6eb580448ecae64908f95b8cb8ee2f241f67/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd5b6efff3c17c3202d4b37189969acf8927438a238c6257f66be3c426beba20", size = 3471858, upload-time = "2025-11-24T23:25:59.636Z" }, + { url = "https://files.pythonhosted.org/packages/52/b2/b20e09670be031afa4cbfabd645caece7f85ec62d69c312239de568e058e/asyncpg-0.31.0-cp312-cp312-win32.whl", hash = "sha256:027eaa61361ec735926566f995d959ade4796f6a49d3bde17e5134b9964f9ba8", size = 527852, upload-time = "2025-11-24T23:26:01.084Z" }, + { url = "https://files.pythonhosted.org/packages/b5/f0/f2ed1de154e15b107dc692262395b3c17fc34eafe2a78fc2115931561730/asyncpg-0.31.0-cp312-cp312-win_amd64.whl", hash = "sha256:72d6bdcbc93d608a1158f17932de2321f68b1a967a13e014998db87a72ed3186", size = 597175, upload-time = "2025-11-24T23:26:02.564Z" }, + { url = "https://files.pythonhosted.org/packages/95/11/97b5c2af72a5d0b9bc3fa30cd4b9ce22284a9a943a150fdc768763caf035/asyncpg-0.31.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c204fab1b91e08b0f47e90a75d1b3c62174dab21f670ad6c5d0f243a228f015b", size = 661111, upload-time = "2025-11-24T23:26:04.467Z" }, + { url = "https://files.pythonhosted.org/packages/1b/71/157d611c791a5e2d0423f09f027bd499935f0906e0c2a416ce712ba51ef3/asyncpg-0.31.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:54a64f91839ba59008eccf7aad2e93d6e3de688d796f35803235ea1c4898ae1e", size = 636928, upload-time = "2025-11-24T23:26:05.944Z" }, + { url = "https://files.pythonhosted.org/packages/2e/fc/9e3486fb2bbe69d4a867c0b76d68542650a7ff1574ca40e84c3111bb0c6e/asyncpg-0.31.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0e0822b1038dc7253b337b0f3f676cadc4ac31b126c5d42691c39691962e403", size = 3424067, upload-time = "2025-11-24T23:26:07.957Z" }, + { url = "https://files.pythonhosted.org/packages/12/c6/8c9d076f73f07f995013c791e018a1cd5f31823c2a3187fc8581706aa00f/asyncpg-0.31.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bef056aa502ee34204c161c72ca1f3c274917596877f825968368b2c33f585f4", size = 3518156, upload-time = "2025-11-24T23:26:09.591Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3b/60683a0baf50fbc546499cfb53132cb6835b92b529a05f6a81471ab60d0c/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0bfbcc5b7ffcd9b75ab1558f00db2ae07db9c80637ad1b2469c43df79d7a5ae2", size = 3319636, upload-time = "2025-11-24T23:26:11.168Z" }, + { url = "https://files.pythonhosted.org/packages/50/dc/8487df0f69bd398a61e1792b3cba0e47477f214eff085ba0efa7eac9ce87/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22bc525ebbdc24d1261ecbf6f504998244d4e3be1721784b5f64664d61fbe602", size = 3472079, upload-time = "2025-11-24T23:26:13.164Z" }, + { url = "https://files.pythonhosted.org/packages/13/a1/c5bbeeb8531c05c89135cb8b28575ac2fac618bcb60119ee9696c3faf71c/asyncpg-0.31.0-cp313-cp313-win32.whl", hash = "sha256:f890de5e1e4f7e14023619399a471ce4b71f5418cd67a51853b9910fdfa73696", size = 527606, upload-time = "2025-11-24T23:26:14.78Z" }, + { url = "https://files.pythonhosted.org/packages/91/66/b25ccb84a246b470eb943b0107c07edcae51804912b824054b3413995a10/asyncpg-0.31.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc5f2fa9916f292e5c5c8b2ac2813763bcd7f58e130055b4ad8a0531314201ab", size = 596569, upload-time = "2025-11-24T23:26:16.189Z" }, + { url = "https://files.pythonhosted.org/packages/3c/36/e9450d62e84a13aea6580c83a47a437f26c7ca6fa0f0fd40b6670793ea30/asyncpg-0.31.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f6b56b91bb0ffc328c4e3ed113136cddd9deefdf5f79ab448598b9772831df44", size = 660867, upload-time = "2025-11-24T23:26:17.631Z" }, + { url = "https://files.pythonhosted.org/packages/82/4b/1d0a2b33b3102d210439338e1beea616a6122267c0df459ff0265cd5807a/asyncpg-0.31.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:334dec28cf20d7f5bb9e45b39546ddf247f8042a690bff9b9573d00086e69cb5", size = 638349, upload-time = "2025-11-24T23:26:19.689Z" }, + { url = "https://files.pythonhosted.org/packages/41/aa/e7f7ac9a7974f08eff9183e392b2d62516f90412686532d27e196c0f0eeb/asyncpg-0.31.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98cc158c53f46de7bb677fd20c417e264fc02b36d901cc2a43bd6cb0dc6dbfd2", size = 3410428, upload-time = "2025-11-24T23:26:21.275Z" }, + { url = "https://files.pythonhosted.org/packages/6f/de/bf1b60de3dede5c2731e6788617a512bc0ebd9693eac297ee74086f101d7/asyncpg-0.31.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9322b563e2661a52e3cdbc93eed3be7748b289f792e0011cb2720d278b366ce2", size = 3471678, upload-time = "2025-11-24T23:26:23.627Z" }, + { url = "https://files.pythonhosted.org/packages/46/78/fc3ade003e22d8bd53aaf8f75f4be48f0b460fa73738f0391b9c856a9147/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19857a358fc811d82227449b7ca40afb46e75b33eb8897240c3839dd8b744218", size = 3313505, upload-time = "2025-11-24T23:26:25.235Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e9/73eb8a6789e927816f4705291be21f2225687bfa97321e40cd23055e903a/asyncpg-0.31.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ba5f8886e850882ff2c2ace5732300e99193823e8107e2c53ef01c1ebfa1e85d", size = 3434744, upload-time = "2025-11-24T23:26:26.944Z" }, + { url = "https://files.pythonhosted.org/packages/08/4b/f10b880534413c65c5b5862f79b8e81553a8f364e5238832ad4c0af71b7f/asyncpg-0.31.0-cp314-cp314-win32.whl", hash = "sha256:cea3a0b2a14f95834cee29432e4ddc399b95700eb1d51bbc5bfee8f31fa07b2b", size = 532251, upload-time = "2025-11-24T23:26:28.404Z" }, + { url = "https://files.pythonhosted.org/packages/d3/2d/7aa40750b7a19efa5d66e67fc06008ca0f27ba1bd082e457ad82f59aba49/asyncpg-0.31.0-cp314-cp314-win_amd64.whl", hash = "sha256:04d19392716af6b029411a0264d92093b6e5e8285ae97a39957b9a9c14ea72be", size = 604901, upload-time = "2025-11-24T23:26:30.34Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fe/b9dfe349b83b9dee28cc42360d2c86b2cdce4cb551a2c2d27e156bcac84d/asyncpg-0.31.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bdb957706da132e982cc6856bb2f7b740603472b54c3ebc77fe60ea3e57e1bd2", size = 702280, upload-time = "2025-11-24T23:26:32Z" }, + { url = "https://files.pythonhosted.org/packages/6a/81/e6be6e37e560bd91e6c23ea8a6138a04fd057b08cf63d3c5055c98e81c1d/asyncpg-0.31.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d11b198111a72f47154fa03b85799f9be63701e068b43f84ac25da0bda9cb31", size = 682931, upload-time = "2025-11-24T23:26:33.572Z" }, + { url = "https://files.pythonhosted.org/packages/a6/45/6009040da85a1648dd5bc75b3b0a062081c483e75a1a29041ae63a0bf0dc/asyncpg-0.31.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18c83b03bc0d1b23e6230f5bf8d4f217dc9bc08644ce0502a9d91dc9e634a9c7", size = 3581608, upload-time = "2025-11-24T23:26:35.638Z" }, + { url = "https://files.pythonhosted.org/packages/7e/06/2e3d4d7608b0b2b3adbee0d0bd6a2d29ca0fc4d8a78f8277df04e2d1fd7b/asyncpg-0.31.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e009abc333464ff18b8f6fd146addffd9aaf63e79aa3bb40ab7a4c332d0c5e9e", size = 3498738, upload-time = "2025-11-24T23:26:37.275Z" }, + { url = "https://files.pythonhosted.org/packages/7d/aa/7d75ede780033141c51d83577ea23236ba7d3a23593929b32b49db8ed36e/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3b1fbcb0e396a5ca435a8826a87e5c2c2cc0c8c68eb6fadf82168056b0e53a8c", size = 3401026, upload-time = "2025-11-24T23:26:39.423Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7a/15e37d45e7f7c94facc1e9148c0e455e8f33c08f0b8a0b1deb2c5171771b/asyncpg-0.31.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8df714dba348efcc162d2adf02d213e5fab1bd9f557e1305633e851a61814a7a", size = 3429426, upload-time = "2025-11-24T23:26:41.032Z" }, + { url = "https://files.pythonhosted.org/packages/13/d5/71437c5f6ae5f307828710efbe62163974e71237d5d46ebd2869ea052d10/asyncpg-0.31.0-cp314-cp314t-win32.whl", hash = "sha256:1b41f1afb1033f2b44f3234993b15096ddc9cd71b21a42dbd87fc6a57b43d65d", size = 614495, upload-time = "2025-11-24T23:26:42.659Z" }, + { url = "https://files.pythonhosted.org/packages/3c/d7/8fb3044eaef08a310acfe23dae9a8e2e07d305edc29a53497e52bc76eca7/asyncpg-0.31.0-cp314-cp314t-win_amd64.whl", hash = "sha256:bd4107bb7cdd0e9e65fae66a62afd3a249663b844fa34d479f6d5b3bef9c04c3", size = 706062, upload-time = "2025-11-24T23:26:44.086Z" }, +] + [[package]] name = "attrs" version = "26.1.0" @@ -352,47 +418,12 @@ wheels = [ [[package]] name = "fastmcp" -version = "3.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "fastmcp-slim", extra = ["client", "server"] }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3b/a9/5c5a01b6abd5346bf60b97cfd29e4a86661940c27dd562bfcda07fd03519/fastmcp-3.3.1.tar.gz", hash = "sha256:979362ea557de42a5f40342563c7e4b236bcc8e7cd192715f50030695d1a71cd", size = 28681699, upload-time = "2026-05-15T15:50:39.673Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9f/11/6b1bdada6ccfe647d615ae63f9106f8136aec17971e9361546af01c7d38e/fastmcp-3.3.1-py3-none-any.whl", hash = "sha256:862440c5c4d281363a5995eee59d77f0f7cac1f18869038729cecf03b02fc522", size = 7903, upload-time = "2026-05-15T15:50:36.424Z" }, -] - -[[package]] -name = "fastmcp-slim" -version = "3.3.1" +version = "3.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "platformdirs" }, - { name = "pydantic", extra = ["email"] }, - { name = "pydantic-settings" }, - { name = "python-dotenv" }, - { name = "rich" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d1/a0/627103e517e1d0d6f1eec633d5662d13e776f01b45ad188e4f5f7478b438/fastmcp_slim-3.3.1.tar.gz", hash = "sha256:0957835fc59452e143ab2f4b7836d2d2df9b2d9958408edc79ba8b56232b2a88", size = 567007, upload-time = "2026-05-15T15:50:10.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7a/ee/97047f4cc2d7b1d46670d08d8ad01a96e7a748cc01c0b4b351ad8eddbc7a/fastmcp_slim-3.3.1-py3-none-any.whl", hash = "sha256:6cf1c2d77e3adb0d409d6825ed6b0b2a999062973e00b8eea03bd48bf9b4c043", size = 738644, upload-time = "2026-05-15T15:50:08.336Z" }, -] - -[package.optional-dependencies] -client = [ - { name = "authlib" }, - { name = "exceptiongroup" }, - { name = "httpx" }, - { name = "mcp" }, - { name = "opentelemetry-api" }, - { name = "py-key-value-aio", extra = ["filetree", "keyring", "memory"] }, -] -server = [ { name = "authlib" }, { name = "cyclopts" }, { name = "exceptiongroup" }, - { name = "griffelib" }, { name = "httpx" }, { name = "jsonref" }, { name = "jsonschema-path" }, @@ -400,15 +431,22 @@ server = [ { name = "openapi-pydantic" }, { name = "opentelemetry-api" }, { name = "packaging" }, + { name = "platformdirs" }, { name = "py-key-value-aio", extra = ["filetree", "keyring", "memory"] }, + { name = "pydantic", extra = ["email"] }, { name = "pyperclip" }, - { name = "python-multipart" }, + { name = "python-dotenv" }, { name = "pyyaml" }, + { name = "rich" }, { name = "uncalled-for" }, { name = "uvicorn" }, { name = "watchfiles" }, { name = "websockets" }, ] +sdist = { url = "https://files.pythonhosted.org/packages/25/83/c95d3bf717698a693eccb43e137a32939d2549876e884e246028bff6ecce/fastmcp-3.1.1.tar.gz", hash = "sha256:db184b5391a31199323766a3abf3a8bfbb8010479f77eca84c0e554f18655c48", size = 17347644, upload-time = "2026-03-14T19:12:20.235Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/ea/570122de7e24f72138d006f799768e14cc1ccf7fcb22b7750b2bd276c711/fastmcp-3.1.1-py3-none-any.whl", hash = "sha256:8132ba069d89f14566b3266919d6d72e2ec23dd45d8944622dca407e9beda7eb", size = 633754, upload-time = "2026-03-14T19:12:22.736Z" }, +] [[package]] name = "graphql-core" @@ -428,9 +466,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ef/32/f2ce6d4cac3e55bc6173f92dbe627e782e1850f89d986c3606feb63aafa7/greenlet-3.5.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:db2910d3c809444e0a20147361f343fe2798e106af8d9d8506f5305302655a9f", size = 286228, upload-time = "2026-04-27T12:20:34.421Z" }, { url = "https://files.pythonhosted.org/packages/b7/aa/caed9e5adf742315fc7be2a84196373aab4816e540e38ba0d76cb7584d68/greenlet-3.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ec9ea74e7268ace7f9aab1b1a4e730193fc661b39a993cd91c606c32d4a3628", size = 601775, upload-time = "2026-04-27T12:52:41.045Z" }, { url = "https://files.pythonhosted.org/packages/c7/af/90ae08497400a941595d12774447f752d3dfe0fbb012e35b76bc5c0ff37e/greenlet-3.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54d243512da35485fc7a6bf3c178fdda6327a9d6506fcdd62b1abd1e41b2927b", size = 614436, upload-time = "2026-04-27T12:59:41.595Z" }, - { url = "https://files.pythonhosted.org/packages/3f/e9/4eeadf8cb3403ac274245ba75f07844abc7fa5f6787583fc9156ba741e0f/greenlet-3.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:41353ec2ecedf7aa8f682753a41919f8718031a6edac46b8d3dc7ed9e1ceb136", size = 620610, upload-time = "2026-04-27T13:02:39.194Z" }, { url = "https://files.pythonhosted.org/packages/2b/e0/2e13df68f367e2f9960616927d60857dd7e56aaadd59a47c644216b2f920/greenlet-3.5.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d280a7f5c331622c69f97eb167f33577ff2d1df282c41cd15907fc0a3ca198c", size = 611388, upload-time = "2026-04-27T12:25:28.008Z" }, - { url = "https://files.pythonhosted.org/packages/ee/ef/f913b3c0eb7d26d86a2401c5e1546c9d46b657efee724b06f6f4ac5d8824/greenlet-3.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:58c1c374fe2b3d852f9b6b11a7dff4c85404e51b9a596fd9e89cf904eb09866d", size = 422775, upload-time = "2026-04-27T13:05:14.261Z" }, { url = "https://files.pythonhosted.org/packages/82/f7/393c64055132ac0d488ef6be549253b7e6274194863967ddc0bc8f5b87b8/greenlet-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1eb67d5adefb5bd2e182d42678a328979a209e4e82eb93575708185d31d1f588", size = 1570768, upload-time = "2026-04-27T12:53:28.099Z" }, { url = "https://files.pythonhosted.org/packages/b8/4b/eaf7735253522cf56d1b74d672a58f54fc114702ceaf05def59aae72f6e1/greenlet-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2628d6c86f6cb0cb45e0c3c54058bbec559f57eaae699447748cb3928150577e", size = 1635983, upload-time = "2026-04-27T12:25:26.903Z" }, { url = "https://files.pythonhosted.org/packages/4c/fe/4fb3a0805bd5165da5ebf858da7cc01cce8061674106d2cf5bdab32cbfde/greenlet-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:d4d9f0624c775f2dfc56ba54d515a8c771044346852a918b405914f6b19d7fd8", size = 238840, upload-time = "2026-04-27T12:23:54.806Z" }, @@ -438,9 +474,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/58/fc576f99037ce19c5aa16628e4c3226b6d1419f72a62c79f5f40576e6eb3/greenlet-3.5.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:5a5ed18de6a0f6cc7087f1563f6bd93fc7df1c19165ca01e9bde5a5dc281d106", size = 285066, upload-time = "2026-04-27T12:23:05.033Z" }, { url = "https://files.pythonhosted.org/packages/4a/ba/b28ddbe6bfad6a8ac196ef0e8cff37bc65b79735995b9e410923fffeeb70/greenlet-3.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a717fbc46d8a354fa675f7c1e813485b6ba3885f9bef0cd56e5ba27d758ff5b", size = 604414, upload-time = "2026-04-27T12:52:42.358Z" }, { url = "https://files.pythonhosted.org/packages/09/06/4b69f8f0b67603a8be2790e55107a190b376f2627fe0eaf5695d85ffb3cd/greenlet-3.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ddc090c5c1792b10246a78e8c2163ebbe04cf877f9d785c230a7b27b39ad038e", size = 617349, upload-time = "2026-04-27T12:59:43.32Z" }, - { url = "https://files.pythonhosted.org/packages/6a/15/a643b4ecd09969e30b8a150d5919960caae0abe4f5af75ab040b1ab85e78/greenlet-3.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4964101b8585c144cbda5532b1aa644255126c08a265dae90c16e7a0e63aaa9d", size = 623234, upload-time = "2026-04-27T13:02:40.611Z" }, { url = "https://files.pythonhosted.org/packages/8a/17/a3918541fd0ddefe024a69de6d16aa7b46d36ac19562adaa63c7fa180eff/greenlet-3.5.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2094acd54b272cb6eae8c03dd87b3fa1820a4cef18d6889c378d503500a1dc13", size = 613927, upload-time = "2026-04-27T12:25:30.28Z" }, - { url = "https://files.pythonhosted.org/packages/77/18/3b13d5ef1275b0ffaf933b05efa21408ac4ca95823c7411d79682e4fdcff/greenlet-3.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:7022615368890680e67b9965d33f5773aade330d5343bbe25560135aaa849eae", size = 425243, upload-time = "2026-04-27T13:05:15.689Z" }, { url = "https://files.pythonhosted.org/packages/ee/e1/bd0af6213c7dd33175d8a462d4c1fe1175124ebed4855bc1475a5b5242c2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5e05ba267789ea87b5a155cf0e810b1ab88bf18e9e8740813945ceb8ee4350ba", size = 1570893, upload-time = "2026-04-27T12:53:29.483Z" }, { url = "https://files.pythonhosted.org/packages/9b/2a/0789702f864f5382cb476b93d7a9c823c10472658102ccd65f415747d2e2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0ecec963079cd58cbd14723582384f11f166fd58883c15dcbfb342e0bc9b5846", size = 1636060, upload-time = "2026-04-27T12:25:28.845Z" }, { url = "https://files.pythonhosted.org/packages/b2/8f/22bf9df92bbff0eb07842b60f7e63bf7675a9742df628437a9f02d09137f/greenlet-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:728d9667d8f2f586644b748dbd9bb67e50d6a9381767d1357714ea6825bb3bf5", size = 238740, upload-time = "2026-04-27T12:24:01.341Z" }, @@ -448,9 +482,7 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/5e/a70f31e3e8d961c4ce589c15b28e4225d63704e431a23932a3808cbcc867/greenlet-3.5.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:f35807464c4c58c55f0d31dfa83c541a5615d825c2fe3d2b95360cf7c4e3c0a8", size = 285564, upload-time = "2026-04-27T12:23:08.555Z" }, { url = "https://files.pythonhosted.org/packages/af/a6/046c0a28e21833e4086918218cfb3d8bed51c075a1b700f20b9d7861c0f4/greenlet-3.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55fa7ea52771be44af0de27d8b80c02cd18c2c3cddde6c847ecebdf72418b6a1", size = 651166, upload-time = "2026-04-27T12:52:43.644Z" }, { url = "https://files.pythonhosted.org/packages/47/f8/4af27f71c5ff32a7fbc516adb46370d9c4ae2bc7bd3dc7d066ac542b4b15/greenlet-3.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a97e4821aa710603f94de0da25f25096454d78ffdace5dc77f3a006bc01abba3", size = 663792, upload-time = "2026-04-27T12:59:44.93Z" }, - { url = "https://files.pythonhosted.org/packages/fb/89/2dadb89793c37ee8b4c237857188293e9060dc085f19845c292e00f8e091/greenlet-3.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bf2d8a80bec89ab46221ae45c5373d5ba0bd36c19aa8508e85c6cd7e5106cd37", size = 668086, upload-time = "2026-04-27T13:02:42.314Z" }, { url = "https://files.pythonhosted.org/packages/a3/59/1bd6d7428d6ed9106efbb8c52310c60fd04f6672490f452aeaa3829aa436/greenlet-3.5.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f52a464e4ed91780bdfbbdd2b97197f3accaa629b98c200f4dffada759f3ae7", size = 660933, upload-time = "2026-04-27T12:25:33.276Z" }, - { url = "https://files.pythonhosted.org/packages/82/35/75722be7e26a2af4cbd2dc35b0ed382dacf9394b7e75551f76ed1abe87f2/greenlet-3.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:1bae92a1dd94c5f9d9493c3a212dd874c202442047cf96446412c862feca83a2", size = 470799, upload-time = "2026-04-27T13:05:17.094Z" }, { url = "https://files.pythonhosted.org/packages/83/e4/b903e5a5fae1e8a28cdd32a0cfbfd560b668c25b692f67768822ddc5f40f/greenlet-3.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:762612baf1161ccb8437c0161c668a688223cba28e1bf038f4eb47b13e39ccdf", size = 1618401, upload-time = "2026-04-27T12:53:31.062Z" }, { url = "https://files.pythonhosted.org/packages/0e/e3/5ec408a329acb854fb607a122e1ee5fb3ff649f9a97952948a90803c0d8e/greenlet-3.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:57a43c6079a89713522bc4bcb9f75070ecf5d3dbad7792bfe42239362cbf2a16", size = 1682038, upload-time = "2026-04-27T12:25:31.838Z" }, { url = "https://files.pythonhosted.org/packages/91/20/6b165108058767ee643c55c5c4904d591a830ee2b3c7dbd359828fbc829f/greenlet-3.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bc59be3945ae9750b9e7d45067d01ae3fe90ea5f9ade99239dabdd6e28a5033", size = 239835, upload-time = "2026-04-27T12:24:54.136Z" }, @@ -458,23 +490,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/a8/4522939255bb5409af4e87132f915446bf3622c2c292d14d3c38d128ae82/greenlet-3.5.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:a10a732421ab4fec934783ce3e54763470d0181db6e3468f9103a275c3ed1853", size = 293614, upload-time = "2026-04-27T12:24:12.874Z" }, { url = "https://files.pythonhosted.org/packages/15/5e/8744c52e2c027b5a8772a01561934c8835f869733e101f62075c60430340/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fc391b1566f2907d17aaebe78f8855dc45675159a775fcf9e61f8ee0078e87f", size = 650723, upload-time = "2026-04-27T12:52:45.412Z" }, { url = "https://files.pythonhosted.org/packages/00/ef/7b4c39c03cf46ceca512c5d3f914afd85aa30b2cc9a93015b0dd73e4be6c/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:680bd0e7ad5e8daa8a4aa89f68fd6adc834b8a8036dc256533f7e08f4a4b01f7", size = 656529, upload-time = "2026-04-27T12:59:46.295Z" }, - { url = "https://files.pythonhosted.org/packages/5f/5c/0602239503b124b70e39355cbdb39361ecfe65b87a5f2f63752c32f5286f/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1aa4ce8debcd4ea7fb2e150f3036588c41493d1d52c43538924ae1819003f4ce", size = 657015, upload-time = "2026-04-27T13:02:43.973Z" }, { url = "https://files.pythonhosted.org/packages/0b/b5/c7768f352f5c010f92064d0063f987e7dc0cd290a6d92a34109015ce4aa1/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddb36c7d6c9c0a65f18c7258634e0c416c6ab59caac8c987b96f80c2ebda0112", size = 654364, upload-time = "2026-04-27T12:25:35.64Z" }, - { url = "https://files.pythonhosted.org/packages/38/51/8699f865f125dc952384cb432b0f7138aa4d8f2969a7d12d0df5b94d054d/greenlet-3.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:728a73687e39ae9ca34e4694cbf2f049d3fbc7174639468d0f67200a97d8f9e2", size = 488275, upload-time = "2026-04-27T13:05:18.28Z" }, { url = "https://files.pythonhosted.org/packages/ef/d0/079ebe12e4b1fc758857ce5be1a5e73f06870f2101e52611d1e71925ce54/greenlet-3.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e5ddf316ced87539144621453c3aef229575825fe60c604e62bedc4003f372b2", size = 1614204, upload-time = "2026-04-27T12:53:32.618Z" }, { url = "https://files.pythonhosted.org/packages/6d/89/6c2fb63df3596552d20e58fb4d96669243388cf680cff222758812c7bfaa/greenlet-3.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4a448128607be0de65342dc9b31be7f948ef4cc0bc8832069350abefd310a8f2", size = 1675480, upload-time = "2026-04-27T12:25:34.168Z" }, { url = "https://files.pythonhosted.org/packages/15/32/77ee8a6c1564fc345a491a4e85b3bf360e4cf26eac98c4532d2fdb96e01f/greenlet-3.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d60097128cb0a1cab9ea541186ea13cd7b847b8449a7787c2e2350da0cb82d86", size = 245324, upload-time = "2026-04-27T12:24:40.295Z" }, ] -[[package]] -name = "griffelib" -version = "2.0.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", size = 166461, upload-time = "2026-03-27T11:34:51.091Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" }, -] - [[package]] name = "h11" version = "0.16.0" @@ -684,6 +705,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f", size = 39160, upload-time = "2025-11-16T16:26:08.402Z" }, ] +[[package]] +name = "mako" +version = "1.3.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/62/791b31e69ae182791ec67f04850f2f062716bbd205483d63a215f3e062d3/mako-1.3.12.tar.gz", hash = "sha256:9f778e93289bd410bb35daadeb4fc66d95a746f0b75777b942088b7fd7af550a", size = 400219, upload-time = "2026-04-28T19:01:08.512Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/b1/a0ec7a5a9db730a08daef1fdfb8090435b82465abbf758a596f0ea88727e/mako-1.3.12-py3-none-any.whl", hash = "sha256:8f61569480282dbf557145ce441e4ba888be453c30989f879f0d652e39f53ea9", size = 78521, upload-time = "2026-04-28T19:01:10.393Z" }, +] + [[package]] name = "markdown-it-py" version = "4.2.0" @@ -804,21 +837,19 @@ wheels = [ [[package]] name = "nexusx" -version = "2.0.0" +version = "3.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiodataloader" }, { name = "fastapi" }, { name = "graphql-core" }, - { name = "greenlet" }, { name = "jinja2" }, { name = "pydantic" }, { name = "sqlmodel" }, - { name = "uvicorn" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/50/2c/a2d9bb5030729b9dc35c6d3abe8b1ea1a6f3f08ec5af88971db5ceddc7fd/nexusx-2.0.0.tar.gz", hash = "sha256:2a0c09aa612541ed4b2ca8acaff37825fd49df67cb4941a4064416426982bf71", size = 1000348, upload-time = "2026-05-21T00:07:24.835Z" } +sdist = { url = "https://files.pythonhosted.org/packages/70/9a/9c35bb99c86f0f715c686e35bb77d70d281e7d972a3e00f74b3f11ceaf47/nexusx-3.3.1.tar.gz", hash = "sha256:dbfcb769fc13a75ad033a99460eec43d44b45e2bd6ba9aaf830e705e7f33e8f7", size = 1437875, upload-time = "2026-06-30T10:47:11.323Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c5/e0/1e624d268b459048c913cde8ec43586e22c13455ba441c777bdf0ced7c36/nexusx-2.0.0-py3-none-any.whl", hash = "sha256:4f9f951b2c1452ccff9547917893e175699beae4cb549719e7a424de42fdb16c", size = 669840, upload-time = "2026-05-21T00:07:22.86Z" }, + { url = "https://files.pythonhosted.org/packages/63/e4/941b698020cd1e4c5884ae91d1508e0d84a09fb4c59b780aad6104da60a8/nexusx-3.3.1-py3-none-any.whl", hash = "sha256:84c4cf79c1330cd7a3a6b029c9da83ed1bca0ebe7c4e36b886c4cd4f7398c673", size = 721689, upload-time = "2026-06-30T10:47:09.527Z" }, ] [[package]] @@ -835,6 +866,17 @@ dependencies = [ ] [package.optional-dependencies] +persist = [ + { name = "alembic" }, +] +persist-mysql = [ + { name = "aiomysql" }, + { name = "alembic" }, +] +persist-pg = [ + { name = "alembic" }, + { name = "asyncpg" }, +] test = [ { name = "pytest" }, { name = "pytest-asyncio" }, @@ -842,16 +884,21 @@ test = [ [package.metadata] requires-dist = [ + { name = "aiomysql", marker = "extra == 'persist-mysql'" }, { name = "aiosqlite" }, + { name = "alembic", marker = "extra == 'persist'", specifier = ">=1.13" }, + { name = "asyncpg", marker = "extra == 'persist-pg'" }, { name = "fastapi" }, - { name = "fastmcp", specifier = ">=3.2.4" }, - { name = "nexusx", specifier = ">=1.6.0" }, + { name = "fastmcp", specifier = ">=3.1,<3.2" }, + { name = "nexusx", specifier = ">=3.2" }, + { name = "nexusx-template", extras = ["persist"], marker = "extra == 'persist-mysql'" }, + { name = "nexusx-template", extras = ["persist"], marker = "extra == 'persist-pg'" }, { name = "pytest", marker = "extra == 'test'" }, { name = "pytest-asyncio", marker = "extra == 'test'" }, { name = "sqlmodel" }, { name = "uvicorn" }, ] -provides-extras = ["test"] +provides-extras = ["test", "persist", "persist-pg", "persist-mysql"] [[package]] name = "openapi-pydantic" @@ -1080,6 +1127,15 @@ crypto = [ { name = "cryptography" }, ] +[[package]] +name = "pymysql" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/bc/1c6a92f385940f727daeecf3bacaf186e03875dff57197801046c583bcf0/pymysql-1.2.0.tar.gz", hash = "sha256:6c7b17ca686988104d7426c27895b455cdeea3e9d3ceb1270f0c3704fead8c33", size = 49021, upload-time = "2026-05-19T08:26:22.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/bd/2534e130295c8cfd4f0a2e31623baab7502278f1e97bcfe61db75656a77f/pymysql-1.2.0-py3-none-any.whl", hash = "sha256:62169ce6d5510f08e140c5e7990ee884a9764024e4a9a27b2cc11f1099322ae0", size = 45716, upload-time = "2026-05-19T08:26:20.974Z" }, +] + [[package]] name = "pyperclip" version = "1.11.0" diff --git a/specs/006-skill-template-polish/baseline.md b/specs/006-skill-template-polish/baseline.md new file mode 100644 index 0000000..48782ac --- /dev/null +++ b/specs/006-skill-template-polish/baseline.md @@ -0,0 +1,85 @@ +# Baseline:修改前的 quickstart 检查 1 命中数 + +**生成时间**: 2026-07-01 +**目的**: 实施完成后跑同一组 grep 对比,验证自洽性 + +## 检查 1.1 spec 路径统一(grep spec/phase) + +``` +skill/SKILL.md:247:#### 用户必须输出的明确结论(写入 `spec/phase0.md`) +skill/phases/phase4.md:8:确认后写入 `spec/phase4.md`: +skill/phases/phase3.md:123:进入 Phase 3 编码之前,先与用户确认以下验收项并写入 `spec/phase3.md`: +skill/phases/phase1.md:22:进入 Phase 1 实现之前,在 `spec/phase1.md` 中记录以下验收标准: +skill/phases/phase1.md:59:按验收标准逐条验证,用户确认后才写入 `spec/phase1.md`: +skill/phases/phase2.md:40:进入 Phase 2 编码之前,先与用户确认测试验收集并写入 `spec/phase2.md`: +``` + +## 检查 1.1 精确版(spec/phaseN) + +``` +skill/SKILL.md:247:#### 用户必须输出的明确结论(写入 `spec/phase0.md`) +skill/phases/phase4.md:8:确认后写入 `spec/phase4.md`: +skill/phases/phase1.md:22:进入 Phase 1 实现之前,在 `spec/phase1.md` 中记录以下验收标准: +skill/phases/phase1.md:59:按验收标准逐条验证,用户确认后才写入 `spec/phase1.md`: +skill/phases/phase3.md:123:进入 Phase 3 编码之前,先与用户确认以下验收项并写入 `spec/phase3.md`: +skill/phases/phase2.md:40:进入 Phase 2 编码之前,先与用户确认测试验收集并写入 `spec/phase2.md`: +``` + +## 检查 1.2 argument-hint 残留 + +``` +4:argument-hint: "[项目路径] 创建四阶段项目的目标目录" +``` + +## 检查 1.3 默认出口声明一致性 + +### main.py 中默认挂载的出口(除注释外): +``` +17: GraphQLHandler, +19: create_use_case_graphql_mcp_server, +21:from nexusx.mcp import create_mcp_server # noqa: E402 +34:graphql_handler = GraphQLHandler( +39:mcp = create_mcp_server( +57:use_case_mcp = create_use_case_graphql_mcp_server( +96:from nexusx import create_use_case_voyager # noqa: E402 +98:voyager_app = create_use_case_voyager( +136:from nexusx import create_use_case_router # noqa: E402 +138:app.include_router(create_use_case_router(use_case_config)) +``` + +## 检查 1.4 router/ 目录 + +``` +FAIL: router/ still exists +total 0 +drwxr-xr-x 1 tangkikodo tangkikodo 22 Jul 1 05:45 . +drwxr-xr-x 1 tangkikodo tangkikodo 112 Jul 1 05:45 .. +-rw-r--r-- 1 tangkikodo tangkikodo 0 May 24 22:52 __init__.py +``` + +## 检查 1.5 service 文件结构对等 + +=== skill/template/src/service/sprint/ === +__init__.py +dtos.py +methods.py +service.py +spec.md +test.py +=== skill/template/src/service/task/ === +__init__.py +dtos.py +methods.py +service.py +spec.md +test.py +=== skill/template/src/service/user/ === +__init__.py +methods.py +spec.md + +## T003 逻辑 tag(git tag 被 hook 拦截) + +polish 前 master HEAD SHA:`5b8f06b6413f61d29f324e540d299393b6b0bc7e` + +回滚命令(如需):`git reset --hard 5b8f06b6413f61d29f324e540d299393b6b0bc7e` diff --git a/specs/006-skill-template-polish/checklists/documentation.md b/specs/006-skill-template-polish/checklists/documentation.md new file mode 100644 index 0000000..01238b2 --- /dev/null +++ b/specs/006-skill-template-polish/checklists/documentation.md @@ -0,0 +1,79 @@ +# 文档质量与跨文档一致性 Checklist:skill 内容结构与模板优化 + +**Purpose**: 校验本 feature 的 spec / plan / tasks / research / data-model / contracts 各产物中关于"文档质量与跨文档一致性"的需求陈述是否清晰、完整、一致、可测。**不**校验实现是否做到——那是 `quickstart.md` 的职责。 +**Created**: 2026-07-01 +**Feature**: [spec.md](../spec.md) + +**说明**: 本 checklist 由 `/speckit-checklist` 生成,聚焦 documentation domain(Q1=A),标准 review 深度(Q2=B),含跨 story 一致性(Q3=A)。 + +--- + +## Requirement Completeness(文档相关需求是否齐全) + +- [ ] CHK001 - spec 是否列出所有需要校准路径的文档文件(SKILL.md / phases/phase1~4.md / spec-management.md)? [Completeness, Spec §FR-001] +- [ ] CHK002 - 是否有需求明确"模板代码与 phase 文档双向对齐"(而非仅文档侧改)? [Completeness, Gap] +- [ ] CHK003 - FR-009(老用户迁移指引)是否定义了"迁移指引应覆盖哪些具体场景"(路径变化 / Phase 0 外置 / 测试位置变更)? [Completeness, Spec §FR-009] +- [ ] CHK004 - 是否有需求规定 phase0.md 外置后 SKILL.md 中如何"指向"它(链接位置 / 链接文本)? [Completeness, Spec §FR-002 + FR-003] +- [ ] CHK005 - FR-011(核心概念自包含)是否列全了需要内联摘要的概念清单(虚拟实体 / 跨层数据流 / 3.0 MCP 迁移——是否还有遗漏)? [Completeness, Spec §FR-011] + +## Requirement Clarity("自洽 / 平滑 / 完整"等模糊词是否量化) + +- [ ] CHK006 - "文档与模板自洽"(US1 标题)是否在 spec 中被量化为可观察的检查(如 grep 命中数 = 0)? [Clarity, Spec §US1 + SC-002] +- [ ] CHK007 - "使用过程更平滑"(用户原话)是否被翻译为可测的 SC-001(≤30 分钟)和 SC-004(≥80% 理解度)? [Clarity, Spec §SC-001 + SC-004] +- [ ] CHK008 - "对用户指令更友好"是否在 spec 中转化为具体需求项(FR-002 入口总览 / FR-005 决策引导)? [Clarity, Spec §FR-002 + FR-005] +- [ ] CHK009 - "模板更完整"(用户原话)是否被具体化为"三个 service 文件结构对等"(FR-004)? [Clarity, Spec §FR-004] +- [ ] CHK010 - "10~20 行内联摘要"(FR-011)的范围是否清晰——是行数下限、上限、还是区间? [Clarity, Spec §FR-011] + +## Requirement Consistency(spec ↔ plan ↔ tasks ↔ contracts 之间不冲突) + +- [ ] CHK011 - spec §FR-004(Phase 0~3 Python + Phase 4 文档对齐)与 plan §Project Structure(不动 `fe/`)是否一致? [Consistency, Spec §FR-004 vs Plan §Project Structure] +- [ ] CHK012 - spec §FR-006(`tests/test__methods.py`)与 tasks T021(迁移 sprint/task 测试)+ T022(新建 user 测试)的文件路径是否完全对齐? [Consistency, Spec §FR-006 vs Tasks §T021/T022] +- [ ] CHK013 - spec §FR-010(默认出口组合)与 contracts §接口面 5(REST + UseCase MCP + Voyager + GraphQL HTTP)与 tasks T014(main.py 重组)三处描述是否一致? [Consistency, Spec §FR-010 vs Contracts §接口面 5 vs Tasks §T014] +- [ ] CHK014 - research §S-03(保留 `create_mcp_server` 加注释)与 contracts §接口面 5(默认出口表未列 `create_mcp_server`)之间是否有显式说明两者关系? [Consistency, Research §S-03 vs Contracts §接口面 5] +- [ ] CHK015 - spec §FR-007(版本门槛集中声明)与 research §S-06("phaseN.md 不再散落版本门槛")是否一致?phase3.md 内联摘要里提到的"3.0+ MCP"是否违反这一规则? [Consistency, Spec §FR-007 vs Research §S-06] + +## Acceptance Criteria Quality(SC 是否可测) + +- [ ] CHK016 - SC-001(≤30 分钟)是否定义了"从哪个动作开始计时 / 到哪个动作停止"的边界? [Measurability, Spec §SC-001] +- [ ] CHK017 - SC-002(矛盾点 = 0)是否定义了"矛盾点"的判定规则(同一陈述在三处出现 ≠ 矛盾;陈述与模板冲突 = 矛盾)? [Measurability, Spec §SC-002] +- [ ] CHK018 - SC-004(≥80% 理解度)是否定义了"理解度"的评测脚本(5 题答对 4 题)?此脚本在 spec 中是否给出? [Measurability, Spec §SC-004 + Gap] +- [ ] CHK019 - SC-005(决策 ≤1 分钟)是否有可重复的评测方法(读哪段、问什么题)? [Measurability, Spec §SC-005] + +## Scenario Coverage(4 个 user story 是否覆盖全部文档资产) + +- [ ] CHK020 - US1(自洽)是否覆盖了所有 4 项需要路径校准的 phase 文档(phase1/2/3/4),而非仅举例? [Coverage, Spec §US1] +- [ ] CHK021 - US4(决策引导)是否仅聚焦 phase3.md,还是也包括 SKILL.md 顶部总览的"适用场景"引导? [Coverage, Spec §US4 vs FR-002] +- [ ] CHK022 - 跳过 Phase 0 的场景(spec Edge Cases 第 4 条"老用户小迭代")是否在 tasks 中有对应处理(如 spec-management.md 的"迭代功能处理"是否覆盖)? [Coverage, Spec §Edge Cases + Spec-management §迭代功能的处理] + +## Edge Case Coverage(边界是否定义) + +- [ ] CHK023 - nexusx 框架升级(4.0 破坏性变更)场景,spec 是否定义了 skill 文档的同步策略? [Edge Case, Gap, Spec §假设] +- [ ] CHK024 - 老用户 spec 项目使用旧 `spec/` 单数路径时,迁移到 `specs/<编号>-*/` 的 ID 是否保留(避免 git 历史断裂)? [Edge Case, Spec §FR-009 + Gap] + +## Ambiguities & Conflicts(跨 story 共享文件冲突检查 — Q3=A) + +- [ ] CHK025 - tasks 是否显式标注 SKILL.md 被 US1(T004/T005)和 US2(T017)共享修改的串行依赖? [Conflict, Tasks §Phase Dependencies] +- [ ] CHK026 - tasks 是否标注 `template/src/main.py` 被 US1(T014 默认出口)和 US3(T024 注册 UserService)共享修改的串行依赖? [Conflict, Tasks §T014 vs T024] +- [ ] CHK027 - tasks 是否标注 `phases/phase3.md` 被 US1(T013 路径校准)和 US4(T026~T028 重组与内联)共享修改的串行依赖? [Conflict, Tasks §T013 vs T026] +- [ ] CHK028 - spec §FR-001(自洽)与 FR-011(自包含摘要)在 phase3.md 上的修改顺序是否定义(先重组再内联 vs 反之)? [Ambiguity, Spec §FR-001 vs FR-011] + +## Notes + +- 项目以 `- [ ]` 起始;通过后改为 `- [x]` +- 任意一项不通过 → 回到 spec.md / plan.md 走变更流程,**不要**直接在 tasks.md 加任务 +- traceability 引用约定:`Spec §FR-XXX` / `Spec §SC-XXX` / `Spec §US N` / `Plan §<章节>` / `Tasks §T` / `Research §` / `Contracts §<接口面 N>` / `Spec-management §<章节>` +- 与已有 `checklists/requirements.md`(specify 阶段的规格质量校验)不重叠——后者校验 spec 整体写作质量,本文件专门校验 documentation domain + +## Pre-Implementation Gap 闭环记录(2026-07-01) + +下列 5 项识别为 Gap 的检查项已在 implementation 前闭环,[Gap] 标记转为可验证的 traceability 引用: + +| CHK | 原状态 | 闭环动作 | 现状 | +|---|---|---|---| +| CHK018 | SC-004 评测方法未具体化 | spec.md §SC-004 补"5 题答对 4 题"评测脚本,引用 quickstart.md 检查 7 | 可验证 | +| CHK022 | "跳过 Phase 0"判定标准未定义 | spec.md §Edge Cases 改陈述句 + 新增 FR-012 + tasks.md 新增 T038 | 已需求化 | +| CHK023 | nexusx 4.0 同步策略未定义 | spec.md §假设 扩展为 3 步同步策略(跑 quickstart / 更新版本声明 / 新建独立 spec) | 已需求化 | +| CHK024 | 迁移时 spec ID 保留规则未定义 | spec.md §FR-009 补"编号 MUST 保留,只允许改描述部分" | 已需求化 | +| CHK028 | phase3.md 在 US1/US4 间修改顺序未定义 | tasks.md §Phase Dependencies + §Parallel Opportunities 显式串行(T013 → T026 → T027 → T028) | 已串行化 | + +PR 评审时这 5 项可直接通过;其余 23 项仍需在实施过程中逐项验证。 diff --git a/specs/006-skill-template-polish/checklists/requirements.md b/specs/006-skill-template-polish/checklists/requirements.md new file mode 100644 index 0000000..ff765db --- /dev/null +++ b/specs/006-skill-template-polish/checklists/requirements.md @@ -0,0 +1,37 @@ +# 规格质量检查清单:skill 内容结构与模板优化 + +**用途**:在进入 plan 阶段之前,验证本 spec 的完整性与质量 +**创建日期**:2026-07-01 +**对应功能**:[spec.md](../spec.md) + +## 内容质量 + +- [x] 不包含实现细节(语言、框架、API 实现层面) +- [x] 聚焦于用户价值与业务需求 +- [x] 面向非技术干系人可读 +- [x] 所有必填章节已完成 + +## 需求完整性 + +- [x] 不再残留 [NEEDS CLARIFICATION] 标记 +- [x] 需求可测试且无歧义 +- [x] 成功标准可衡量 +- [x] 成功标准与技术无关(不含实现细节) +- [x] 所有验收场景已定义 +- [x] 边界情况已识别 +- [x] 范围边界清晰 +- [x] 依赖与假设已识别 + +## 功能就绪度 + +- [x] 所有功能需求都有清晰的验收标准 +- [x] 用户场景覆盖主要流程 +- [x] 功能满足成功标准中定义的可衡量结果 +- [x] 规格中不渗入实现细节 + +## 备注 + +- 本 spec 的"用户"是使用 nexusx-4phase skill 的开发者,而非最终产品的终端用户。所有"用户故事"都是从开发者视角撰写。 +- 4 个用户故事按 P1/P1/P2/P3 排序:前两个 P1(自洽、入口总览)是基础修复,必须先做;P2(模板完整)次之;P3(决策引导)信息过载问题留到最后。 +- 成功标准 SC-001/SC-004 涉及人为评测(耗时和理解度),无法完全自动化,但可在 plan 阶段设计具体的验证脚本(如让一名 reviewer 实操并打分)。 +- 与项目 CLAUDE.md 一致:本 spec 及后续所有产物使用中文。 diff --git a/specs/006-skill-template-polish/contracts/skill-interface.md b/specs/006-skill-template-polish/contracts/skill-interface.md new file mode 100644 index 0000000..d773b29 --- /dev/null +++ b/specs/006-skill-template-polish/contracts/skill-interface.md @@ -0,0 +1,216 @@ +# Skill 对外接口契约 + +**Feature**: 006-skill-template-polish +**说明**: 本 feature 优化对象是 Claude Code skill 文档与模板,"对外接口"指 skill 被 Claude Code 加载和被用户调用时暴露的契约面。本文件作为 Phase 1 设计产物,定义 skill 与外部(Claude Code runtime、用户、其他 phase 文档)之间的稳定契约。 + +--- + +## 接口面 1:Skill Frontmatter(YAML 头部) + +Skill 通过 SKILL.md 顶部的 YAML frontmatter 被 Claude Code skill 系统识别。 + +### 字段契约 + +| 字段 | 类型 | 必填 | 本轮变更 | 说明 | +|---|---|---|---|---| +| `name` | string | ✅ | 不变 | skill 唯一标识,kebab-case。当前值:`nexusx-4phase` | +| `description` | string | ✅ | 不变 | 一句话用途说明,Claude Code 用于决定何时调用本 skill | +| ~~`argument-hint`~~ | — | ❌ | **删除** | Claude Code 不识别此字段,保留会让用户误以为生效 | + +### 校验规则 + +- frontmatter 必须以 `---` 开始和结束 +- 只允许 `name` 和 `description` 两个字段;多余字段必须删除 +- `name` 全小写、kebab-case、≤ 64 字符 +- `description` 中文或英文均可,必须包含触发场景的关键词("nexusx"、"四阶段"、"Schema"、"API"等) + +### 反例(必须避免) + +```yaml +--- +name: nexusx-4phase +description: 基于 nexusx 的四阶段开发模式... +argument-hint: "[项目路径] 创建四阶段项目的目标目录" # ❌ 无效字段 +--- +``` + +### 正例 + +```yaml +--- +name: nexusx-4phase +description: 基于 nexusx 的四阶段开发模式,从 Schema 建模到 API 响应组装再到 TS SDK 的完整项目构建流程。 +--- +``` + +--- + +## 接口面 2:Skill 调用约定(运行时入口) + +调用约定的说明必须放在 SKILL.md 正文(替代被删除的 `argument-hint`)。 + +### 用户调用方式 + +``` +/nexusx-4phase [项目目录路径] +``` + +- 参数:可选的目标目录路径(相对或绝对) +- 缺省行为:未提供路径时,skill 引导用户在当前位置或指定路径下创建项目 + +### Claude Code 加载时序 + +1. Claude Code 读 `SKILL.md` frontmatter → 注册 skill +2. 用户触发(显式 `/nexusx-4phase` 或描述匹配触发条件)→ Claude Code 加载 SKILL.md 全文 +3. SKILL.md 正文引导 Claude / 用户进入 Phase 0 → 读取 `phases/phase0.md` +4. Phase 0 完成后依次读取 `phases/phase1.md` ~ `phases/phase4.md` + +### 隐含契约 + +- SKILL.md 必须自包含"调用约定"+"入口总览"+"Phase 导航"三段,使用户不需要先读 phase 文档就能开始 +- 每个 `phases/phaseN.md` 必须独立可读,不依赖兄弟 phase 文档的上下文 + +--- + +## 接口面 3:Phase 文件命名与结构契约 + +### 文件命名 + +``` +phases/phase0.md # Phase 0:需求确认(本轮新增) +phases/phase1.md # Phase 1:Schema + ER Diagram + mock seed +phases/phase2.md # Phase 2:方法实现 + Entity 挂载 +phases/phase3.md # Phase 3:UseCase 响应组装 + MCP +phases/phase4.md # Phase 4:OpenAPI → TS SDK +``` + +- 文件名固定为 `phase.md`,N ∈ {0, 1, 2, 3, 4} +- 不允许 `phase0/`(子目录)或 `phase-0.md`(连字符)等变体 + +### 文件结构契约 + +每个 `phaseN.md` 必须包含以下章节(顺序固定): + +```markdown +# Phase N: <阶段标题> + +**目标**: <一句话> + +**新增/修改文件**: <清单> + +**关键模式**: <含 10~20 行内联摘要,外部 docs 仅作延伸阅读> + +**V 降 — 定义验收标准**: +<可观察、可操作的验收表> + +**实现**: <步骤> + +**V 升 — 逐条回查验收**: + + +## 踩坑经验 +<编号列表> +``` + +### SKILL.md 引用契约 + +SKILL.md 必须通过以下方式引用 phase 文件: + +```markdown +Phase 0 完成并确认后,读取当前阶段的详细指令: +- **Phase 1**: 读取 `phases/phase1.md` +- **Phase 2**: 读取 `phases/phase2.md` +- **Phase 3**: 读取 `phases/phase3.md` +- **Phase 4**: 读取 `phases/phase4.md` +``` + +不允许在 SKILL.md 中复制 phase 文档正文(避免双份维护)。 + +--- + +## 接口面 4:Spec 工作流契约(spec-management.md) + +### 目录命名契约 + +``` +specs/<编号>-<需求简述>/ +``` + +- `<编号>`:三位序号(001、002、…),按项目递增 +- `<需求简述>`:英文短横线连接 + +### 文件结构契约 + +``` +specs/<编号>-<需求简述>/ +├── story.md +├── phase0.md +├── phase1.md +├── phase2.md +├── phase3.md +└── phase4.md +``` + +- 所有 phase 文件平铺在 spec 目录下,不允许嵌套 +- 每个 phase 文件必须包含三段:**需求说明 / 验收标准 / 实现描述** + +### 语言契约(本轮新增) + +- 所有 spec-kit 产物(`story.md`、`phaseN.md`、`spec.md`、`plan.md`、`tasks.md`、`checklists/*` 等)必须使用中文撰写 +- 英文术语(框架名、API 名、代码标识符)保留原文 + +--- + +## 接口面 5:模板项目契约(template/) + +### 模板可运行性契约 + +```bash +cd skill/template +uv sync +uvicorn src.main:app --reload +``` + +- 启动后必须能访问 `/voyager`、`/graphql`(含 GraphiQL)、REST 端点、`/mcp-usecase` MCP 端点 +- 不需要任何手工修改即可启动 + +### 文件结构对等契约 + +`template/src/service/` 下所有示例 service 子目录的文件列表必须对等: + +``` +service// +├── __init__.py +├── methods.py +├── dtos.py # Phase 3 +├── service.py # Phase 3 +└── spec.md +``` + +- 当前 sprint / task 已满足,user 必须补齐 +- 测试文件统一外置到 `template/tests/test__methods.py`,service 子目录内不放测试 + +### 默认出口组合契约 + +`template/src/main.py` 默认仅演示以下出口: + +| 出口 | 入口 API | 必须 | +|---|---|---| +| REST | `create_use_case_router(app_config)` | ✅ | +| UseCase GraphQL MCP | `create_use_case_graphql_mcp_server(apps=[...])` | ✅ | +| Voyager | `create_use_case_voyager(services=..., er_manager=...)` | ✅ | +| GraphQL HTTP | `GraphQLHandler` + `/graphql` 路由 | ✅(开发期辅助) | + +其余出口(JSON-RPC / CLI)必须以注释形式保留,不在默认启动中加载。 + +--- + +## 契约违反的后果 + +| 违反点 | 后果 | 检测时机 | +|---|---|---| +| `argument-hint` 残留 | Claude Code 无效字段,用户混淆 | grep 检查 | +| `phases/phase0.md` 缺失 | Phase 0 流程无法进入(SKILL.md 不应再内联 Phase 0) | 文件存在性检查 | +| service 文件结构不对等 | 用户照 sprint 写 user 时缺文件参考 | 目录列表比较 | +| 模板无法直接启动 | SC-003 失败 | `uv sync && uvicorn` 实测 | +| 默认出口全开 | FR-010 违反,用户错觉"必须全启" | main.py 阅读检查 | diff --git a/specs/006-skill-template-polish/data-model.md b/specs/006-skill-template-polish/data-model.md new file mode 100644 index 0000000..cf29c83 --- /dev/null +++ b/specs/006-skill-template-polish/data-model.md @@ -0,0 +1,129 @@ +# 数据模型:skill 资产清单与依赖关系 + +**Feature**: 006-skill-template-polish +**说明**: 本 feature 是 skill 文档/模板优化项目,无运行时数据库。本文件将优化对象建模为"文档资产 + 模板文件"的实体清单与依赖图,作为 Phase 1 设计契约。 + +--- + +## 实体清单 + +### 实体 1:Skill 主文档(SKILL.md) + +- **业务含义**:skill 入口,被 Claude Code 加载时第一个读到的文件 +- **关键属性**: + - `frontmatter`: dict — YAML 头部,只允许 `name` / `description` 两个字段(移除非法 `argument-hint`) + - `overview`: 入口总览章节,覆盖 Phase 0~4 的输入/产出/关键 API/典型坑 + - `version_gate`: 适用版本声明(`nexusx >= 3.2`) + - `navigation`: 指向 `phases/phaseN.md` 与 `spec-management.md` 的导航链接 +- **关系**:1:N → Phase 文档;1:1 → spec-management.md +- **本轮变更**:瘦身(删除内联的 Phase 0 内容约 200 行);新增入口总览;移除 `argument-hint` + +### 实体 2:Phase 文档(phases/phaseN.md,N ∈ {0, 1, 2, 3, 4}) + +- **业务含义**:单阶段的详细实施指令,独立可读 +- **关键属性**: + - `goal`: 本阶段目标(一句话) + - `new_files`: 新增/修改文件清单 + - `key_patterns`: 关键模式(含 10~20 行内联摘要,外部 docs 降级为延伸阅读) + - `acceptance_criteria_v_drop`: V 降阶段定义的可观察验收标准 + - `implementation_steps`: 实现步骤 + - `acceptance_criteria_v_raise`: V 升阶段逐条回查清单 + - `pitfalls`: 踩坑经验列表 +- **关系**:N:1 → SKILL.md(被导航);N:N → 模板代码示例 +- **本轮变更**: + - `phase0.md`:**新增**(从 SKILL.md 外置,Step 0-1~0-8 二级标题分节) + - `phase1.md` / `phase2.md`:校准路径与术语(`spec/` → `specs/<编号>-*/`) + - `phase3.md`:重组"推荐默认出口"+"可选扩展",内联虚拟实体 / 跨层数据流 / 3.0 MCP 迁移摘要 + - `phase4.md`:校准术语、路径、版本声明(不重写 fe/ 模板) + +### 实体 3:Spec 管理工作流(spec-management.md) + +- **业务含义**:spec 目录命名、文件格式、写入时机、交付校验的规则集 +- **关键属性**: + - `directory_naming`: `specs/<编号>-<需求简述>/` + - `file_format`: `phaseN.md` 三段式(需求说明 / 验收标准 / 实现描述) + - `write_timing`: 每个 phaseN.md 的 V 降 / V 升写入时机 + - `iteration_rules`: 增量迭代时的 spec 处理规则 + - `delivery_check`: 交付前 `wc -l` 完整性检查 + - `language_requirement`: 【新增】中文撰写要求 + - `migration_guide`: 【新增】从旧结构(`spec/` 单数路径、Phase 0 内联)迁移的指引 +- **关系**:1:1 → SKILL.md +- **本轮变更**:路径统一为 `specs/<编号>-*/`;新增 `## 语言要求` 章节;新增 `## 从旧结构迁移` 章节 + +### 实体 4:模板项目(template/) + +- **业务含义**:参考代码,所有 phase 实现的"金标准" +- **关键属性**:每个文件对应一个 nexusx API 用法示例 +- **结构**(优化后): + ``` + template/ + ├── pyproject.toml # packages = ["src"],依赖示例(uvicorn / aiosqlite / asyncpg / alembic) + ├── uv.lock + ├── src/ + │ ├── __init__.py + │ ├── main.py # 默认 REST + UseCase GraphQL MCP + Voyager;其余出口注释化 + │ ├── models.py # 含 mount_method() 示例 + │ ├── db.py + │ ├── database.py + │ └── service/ + │ ├── user/ # 补齐 dtos.py / service.py + │ ├── sprint/ # 原位校准 + │ └── task/ # 原位校准 + ├── tests/ # 【新增】test__methods.py + │ ├── test_user_methods.py + │ ├── test_sprint_methods.py + │ └── test_task_methods.py + └── fe/ # Phase 4 TS SDK(本轮不动) + ``` +- **关系**:N:N → phase 文档(每个 phase 引用对应模板代码段) +- **本轮变更**: + - 删除 `template/src/router/`(与"不需要手写 router"一致) + - 补齐 `template/src/service/user/{dtos.py, service.py}` + - 把 `template/src/service//test.py` 迁移到 `template/tests/test__methods.py` + - `template/src/main.py`:默认仅 REST + UseCase MCP + Voyager;GraphQL HTTP / JSON-RPC / CLI 注释化;为保留的 `create_mcp_server` 加注释说明属"base 实体层" + - `template/pyproject.toml`:补 `packages = ["src"]`、`uvicorn`、async driver、`alembic>=1.13`(持久化场景)示例 + +--- + +## 关系图(文本 ER) + +``` +SKILL.md ──1:N──→ phases/phaseN.md (N ∈ {0, 1, 2, 3, 4}) +SKILL.md ──1:1──→ spec-management.md +phases/phaseN.md ──N:N──→ template/src/ (文档引用代码示例) + +phases/phase0.md 【新增】 ──1:1──→ SKILL.md (内容来源:从 SKILL.md 外置) +template/tests/test__methods.py 【新增】 ──N:1──→ template/src/service//methods.py +template/src/service/user/{dtos,service}.py 【新增】 ──1:1──→ template/src/service/sprint/{dtos,service}.py (结构对齐) +template/src/router/ 【删除】 +``` + +--- + +## 验证规则(从需求映射) + +| 规则 | 来源 FR | 验证方式 | +|---|---|---| +| 文档中 `spec/` 单数路径出现次数 = 0 | FR-001 | `grep -rn "spec/phase" skill/`(除 `specs/` 外不应有命中) | +| `argument-hint` 出现次数 = 0 | S-02 | `grep -n "argument-hint" skill/SKILL.md` | +| `phases/phase0.md` 存在且非空 | FR-003 | `wc -l skill/phases/phase0.md` ≥ 100 | +| 三个示例 service 文件结构对等 | FR-004 | `ls skill/template/src/service/*/` 三个目录文件列表相同 | +| `template/src/router/` 不存在 | D-04 落地 | `test ! -d skill/template/src/router/` | +| `template/tests/test_*_methods.py` 存在 | FR-006 | `ls skill/template/tests/` | +| `template/src/service//test.py` 不存在 | FR-006 | `find skill/template/src/service -name test.py` 应为空 | +| SKILL.md 顶部含入口总览 | FR-002 | 人工 + `grep -n "## .*总览\|## 入口\|## Overview" skill/SKILL.md` | +| SKILL.md 含 `nexusx >= 3.2` 适用版本 | FR-007 | `grep -n "nexusx >= 3.2" skill/SKILL.md` | +| Phase 3 文档"推荐默认组合"在"可选扩展"之前 | FR-005 | 人工阅读 phase3.md 章节顺序 | +| Phase 4 文档不含 `spec/phase4`(单数) | FR-001 | `grep -n "spec/phase4" skill/phases/phase4.md` 应为空 | + +--- + +## 状态转换 + +文档资产无运行时状态机。模板代码的"状态"通过 git 提交历史表达: + +``` +draft (本 feature 分支) → review (PR) → merged (master) +``` + +每个 phase 文档的"V 降 → 实现 → V 升"是 spec-management.md 定义的写作时序,不影响文档资产本身的实体结构。 diff --git a/specs/006-skill-template-polish/plan.md b/specs/006-skill-template-polish/plan.md new file mode 100644 index 0000000..3c8e186 --- /dev/null +++ b/specs/006-skill-template-polish/plan.md @@ -0,0 +1,138 @@ +# 实施计划:skill 内容结构与模板优化 + +**分支**: `006-skill-template-polish` | **日期**: 2026-07-01 | **Spec**: [spec.md](./spec.md) + +**输入**: Feature specification 自 [`/specs/006-skill-template-polish/spec.md`](./spec.md) + +**说明**: 本文件由 `/speckit-plan` 命令填充,执行工作流见 `.specify/templates/plan-template.md`。 + +## 摘要 + +本轮优化针对 `skill/` 目录中的 nexusx-4phase skill 文档与 `skill/template/` 参考代码模板。核心问题来自上一轮代码评审识别的 P0/P1 共 14 处具体矛盾与缺口:spec 路径不一致、`argument-hint` 非法字段、模板 main.py 与 Phase 3 文档冲突、Phase 0 内联不对称、user service 模板残缺、router 目录残留、Phase 3 出口过载、缺入口总览、版本门槛散落、外部 docs 引用未声明、测试位置说法不一等。 + +经 `/speckit-clarify` 拍板的 5 条关键决策(详见 [spec.md Clarifications](./spec.md#clarifications)): +1. 范围限定 Phase 0~3(Python)+ Phase 4 文档对齐,不重写 `fe/` TS SDK 模板 +2. 测试位置统一为 `tests/test__methods.py` +3. 受众以独立开发者为主、老用户结构迁移为次,不覆盖团队协作 +4. 关键概念(虚拟实体 / 跨层数据流 / 3.0 MCP 迁移)自包含到 10~20 行内联摘要 +5. Phase 0 外置为单文件 `phases/phase0.md`,按 Step 0-1~0-8 二级标题分节 + +技术路径:纯文档与模板重组,**不修改 nexusx 框架源码、不引入新依赖**。所有产出物用中文(项目 CLAUDE.md 要求)。 + +## Technical Context + +**Language/Version**: 文档项目(Markdown)+ 模板代码(Python 3.12,与 nexusx 主项目对齐) + +**Primary Dependencies**: +- `nexusx >= 3.2`(模板代码引用,假设版本,不修改框架本身) +- Claude Code skill 系统(frontmatter 仅识别 `name` / `description`,无构建依赖) + +**Storage**: 文件系统(`skill/` 目录),无运行时存储 + +**Testing**: +- 一致性检查:人工 + grep 脚本验证文档陈述与模板代码对齐 +- 模板可运行性:`uv sync && uvicorn src.main:app` 启动验证 +- 自动化测试:模板自带 `pytest tests/`(迁移到项目级 `tests/` 后) + +**Target Platform**: Claude Code skill 用户(Linux / macOS / WSL);模板运行平台为 Python 3.12+ + +**Project Type**: developer-tool / skill 文档项目(非编译产物,非 web 服务) + +**Performance Goals**: N/A(文档项目无运行时性能指标) + +**Constraints**: +- **不修改 nexusx 框架源码**(spec 范围外) +- **不引入新外部依赖**(spec 范围外) +- **不重写 Phase 4 `fe/` TS SDK 模板**(clarify 决策 #1) +- **保持 V 型验收 + Phase 0~4 五阶段方法论不变**(只重组载体) +- **不覆盖团队协作 / CI / 多人分支策略**(clarify 决策 #3) + +**Scale/Scope**: +- skill 文档:SKILL.md(约 330 行)+ spec-management.md(约 90 行)+ phases/phase1~4.md(合计约 400 行) +- 模板代码:`skill/template/src/`(约 12 个 Python 文件)+ `pyproject.toml` + `uv.lock` +- 新增文件:`phases/phase0.md`(外置 Phase 0 内容)、可能的入口总览(写入 SKILL.md 顶部) +- 影响范围:仅 `skill/` 子树,不动主项目 `src/`、`docs/`、`tests/` + +## Constitution Check + +*GATE: 必须在 Phase 0 研究之前通过;Phase 1 设计后再次检查。* + +`.specify/memory/constitution.md` 当前为空模板(仅含 `[PRINCIPLE_X_NAME]` 等占位符),未定义可执行的 governance gates。 + +**处理方式**: +- 视为本项目 constitution 未启用正式 gates,本计划不触发任何 violation +- 项目级约束改为引用 `CLAUDE.md` 与本 spec 的 `## 假设` / `## 范围外(Out of Scope)` 章节 +- 如未来 constitution 落地具体原则,需在 Phase 1 末尾再次回查 + +**隐含 gate**(来自 spec 假设与 CLAUDE.md,本次自检): +- ✅ 产物全部使用中文(spec-kit 产物要求) +- ✅ 不修改 nexusx 框架源码 +- ✅ 不引入新依赖 +- ✅ 保持 V 型验收 + 五阶段方法论 +- ✅ 不重写 Phase 4 TS SDK 模板 + +无 violation,不需要 Complexity Tracking 表登记。 + +## Project Structure + +### 本 Feature 的产出文档 + +```text +specs/006-skill-template-polish/ +├── spec.md # /speckit-specify 输出(已完成,含 Clarifications) +├── plan.md # 本文件(/speckit-plan 输出) +├── research.md # Phase 0 输出(决策汇总) +├── data-model.md # Phase 1 输出(skill 资产清单 + 模板文件树模型) +├── quickstart.md # Phase 1 输出(验证运行手册) +├── contracts/ +│ └── skill-interface.md # Phase 1 输出(skill 对外接口契约) +├── checklists/ +│ └── requirements.md # /speckit-specify 输出(已完成) +└── tasks.md # /speckit-tasks 输出(下一步,不在本计划内) +``` + +### 被修改的源代码(`skill/` 子树) + +```text +skill/ +├── SKILL.md # 瘦身:保留总览 + 入口导航,Phase 0 内容外移 +├── spec-management.md # 校准:路径统一为 specs/<编号>-*/,补中文化声明 +├── phases/ +│ ├── phase0.md # 【新增】从 SKILL.md 外置的 Phase 0 内容(Step 0-1~0-8) +│ ├── phase1.md # 校准:移除 spec/ 单数路径、统一术语 +│ ├── phase2.md # 校准:测试位置声明与模板对齐 +│ ├── phase3.md # 重组:分"推荐默认组合"+"可选扩展",内联核心概念 +│ └── phase4.md # 校准:术语、路径、版本声明对齐(不重写 fe/) +└── template/ + ├── pyproject.toml # 校准:packages = ["src"],alembic/uvicorn/driver 依赖示例 + ├── uv.lock # 跟随 pyproject.toml 更新 + └── src/ + ├── main.py # 重组:默认仅 REST + UseCase MCP + Voyager,可选出口注释化 + ├── models.py # 校准:mount_method() 示例与文档一致 + ├── db.py # 校准:URL 占位符与 Step 0-7 决策树对齐 + ├── database.py # 校准:in-memory vs 持久化分支示例 + ├── router/ # 【删除】残留目录(create_use_case_router 取代手写 router) + └── service/ + ├── user/ # 【补齐】补 dtos.py / service.py / test.py,与其他 service 对等 + ├── sprint/ # 校准:test.py 迁出 + └── task/ # 校准:test.py 迁出 + # test.py 文件统一迁移到↓ +tests/ # 【新增于模板根】test__methods.py +├── test_user_methods.py +├── test_sprint_methods.py +└── test_task_methods.py +``` + +**Structure Decision**: +- **单项目结构**(plan 模板的 Option 1):本 feature 不新建应用,只重组 `skill/` 子树 +- **新增 `phases/phase0.md`**:Phase 0 与 Phase 1~4 文档结构对称 +- **新增 `tests/`**:模板根级测试目录(替代 `service//test.py`),匹配 clarify 决策 #2 +- **删除 `template/src/router/`**:与 phase3.md "不需要手写 router" 一致 +- **补齐 `template/src/service/user/`**:使三个示例 service 在文件结构上对等 +- **不动 `template/fe/`**:Phase 4 TS SDK 模板不在本轮范围(clarify 决策 #1) + +## Complexity Tracking + +> 仅当 Constitution Check 存在需论证的 violation 时填写 + +`.specify/memory/constitution.md` 为空模板,无 violation,本表留空。 diff --git a/specs/006-skill-template-polish/quickstart.md b/specs/006-skill-template-polish/quickstart.md new file mode 100644 index 0000000..7be6efb --- /dev/null +++ b/specs/006-skill-template-polish/quickstart.md @@ -0,0 +1,238 @@ +# Quickstart 验证手册:skill 优化效果确认 + +**Feature**: 006-skill-template-polish +**用途**: 本 feature 是文档/模板优化,没有运行时端到端流程可验证。本手册定义一组**可执行检查**,用于在本轮改动落地后确认 spec 的 6 条 Success Criteria(SC-001~006)与 4 个用户故事真正达成。 + +> 实施细节(具体怎么改文件)属于 `tasks.md` 的范围;本文件只定义"如何验证改对了"。 + +--- + +## 前置条件 + +- 工作目录:`/home/tangkikodo/nexusx` +- 本分支已包含本轮所有改动(skill/ 子树修改完成) +- 系统已安装 `uv`、`uvicorn`、`grep`、`pytest` +- 网络:可访问 PyPI(如需走代理,`https_proxy=http://192.168.71.149:16780`) + +--- + +## 检查 1:文档自洽性(对应用户故事 1,FR-001) + +**目的**:验证 skill 文档与模板代码无矛盾。 + +### 1.1 spec 路径统一 + +```bash +# 期望:除 specs/ 复数形式外,不再有 spec/ 单数路径出现 +grep -rn "spec/phase" skill/ +# 期望输出:空(或仅匹配到 specs/<编号>-*/ 的子串,无 spec/phase 直接命中) + +grep -rn "spec/phase0\|spec/phase1\|spec/phase2\|spec/phase3\|spec/phase4" skill/ +# 期望输出:空 +``` + +### 1.2 无非法 frontmatter 字段 + +```bash +grep -n "argument-hint" skill/SKILL.md +# 期望输出:空 +``` + +### 1.3 模板与 phase3.md 出口声明一致 + +```bash +# 期望:模板 main.py 默认挂载的出口(除注释外)= phase3.md 推荐默认组合 +# REST + UseCase GraphQL MCP + Voyager + GraphQL HTTP +grep -n "create_use_case_router\|create_use_case_graphql_mcp_server\|create_use_case_voyager\|GraphQLHandler" skill/template/src/main.py +# 期望 4 行命中 + +# 可选出口必须以注释出现 +grep -n "create_jsonrpc_router\|create_use_case_cli" skill/template/src/main.py +# 期望命中行均以 # 开头(注释) +``` + +### 1.4 router 目录已清理 + +```bash +test ! -d skill/template/src/router/ && echo "PASS: router/ removed" || echo "FAIL" +# 期望:PASS: router/ removed +``` + +### 1.5 service 文件结构对等 + +```bash +for d in skill/template/src/service/*/; do + echo "=== $d ===" + ls "$d" | grep -v __pycache__ | sort +done +# 期望:三个 service(sprint/task/user)的文件列表对等 +# 都包含 __init__.py / methods.py / dtos.py / service.py / spec.md(user 需补齐 dtos.py / service.py) +``` + +--- + +## 检查 2:入口总览可读性(对应用户故事 2,FR-002、FR-003、FR-007) + +**目的**:验证新用户能在 5 分钟内通过入口总览掌握全貌。 + +### 2.1 SKILL.md 顶部含入口总览 + +```bash +# 检查 SKILL.md 前 80 行内是否有"总览/Overview/入口"类章节 +head -80 skill/SKILL.md | grep -E "^##.*(总览|Overview|入口|快速参考)" +# 期望:至少 1 行命中 +``` + +### 2.2 Phase 0 已外置 + +```bash +test -f skill/phases/phase0.md && wc -l skill/phases/phase0.md +# 期望:文件存在,行数 ≥ 100(原 SKILL.md 的 Phase 0 内容约 200 行) + +# SKILL.md 不再内联 Phase 0 详细内容 +grep -n "Step 0-1\|Step 0-7" skill/SKILL.md +# 期望:空(这些小节已外移到 phases/phase0.md) +``` + +### 2.3 版本门槛集中声明 + +```bash +grep -n "nexusx >= 3.2\|适用版本" skill/SKILL.md +# 期望:≥ 1 行命中 + +# phaseN.md 不再散落版本门槛 +grep -rn "3\.0 起\|3\.2+" skill/phases/ +# 期望:极少(仅在内联摘要中提及特性版本时出现,无散落门槛声明) +``` + +--- + +## 检查 3:模板可运行性(对应 SC-003) + +**目的**:验证模板能直接启动,所有端点可访问。 + +```bash +cd skill/template +uv sync +# 期望:依赖安装成功 + +# 后台启动 +uvicorn src.main:app --port 8765 & +APP_PID=$! +sleep 3 + +# 探活 +curl -sf http://localhost:8765/voyager/ -o /dev/null && echo "PASS voyager" || echo "FAIL voyager" +curl -sf http://localhost:8765/graphql -o /dev/null && echo "PASS graphiql" || echo "FAIL graphiql" +curl -sf http://localhost:8765/openapi.json -o /dev/null && echo "PASS openapi" || echo "FAIL openapi" + +# REST 端点示例(具体路径取决于 service) +curl -sf -X POST http://localhost:8765/api/template/sprint_service/list_sprints \ + -H "Content-Type: application/json" -d '{}' && echo "PASS rest" || echo "FAIL rest" + +kill $APP_PID +cd ../.. +``` + +**期望**:4 个 PASS 全部命中。 + +--- + +## 检查 4:测试位置与可运行性(对应 SC-006、FR-006) + +```bash +# 测试文件位置 +ls skill/template/tests/ +# 期望:test_user_methods.py / test_sprint_methods.py / test_task_methods.py + +# service 子目录内无残留 test.py +find skill/template/src/service -name "test.py" -not -path "*/__pycache__/*" +# 期望:空 + +# 测试运行 +cd skill/template +uv run pytest tests/ -v +# 期望:全部通过,至少每个 service 覆盖一个正常场景 + 一个边界场景 +cd ../.. +``` + +--- + +## 检查 5:核心概念自包含(对应 FR-011) + +**目的**:验证 phase 文档中外链 docs 已降级为"延伸阅读",关键概念有 10~20 行内联摘要。 + +```bash +# phase0.md 含虚拟实体概念摘要(不依赖外部 docs) +grep -n "虚拟实体\|virtual entit" skill/phases/phase0.md +# 期望:命中行附近有 10+ 行解释段落(人工抽查) + +# phase3.md 含跨层数据流摘要 +grep -n "ExposeAs\|SendTo\|Collector\|跨层数据流" skill/phases/phase3.md +# 期望:命中 + 上下文有摘要段落 + +# phase3.md 含 3.0 MCP 迁移摘要 +grep -n "3.0 UseCase GraphQL MCP\|create_use_case_graphql_mcp_server" skill/phases/phase3.md +# 期望:命中 + 摘要段落 +``` + +--- + +## 检查 6:spec-management.md 工作流完整性(对应 FR-008、FR-009) + +```bash +# 中文化要求已声明 +grep -n "中文\|语言要求" skill/spec-management.md +# 期望:≥ 1 行命中 + +# 迁移指引已写 +grep -n "迁移\|migration\|从旧结构" skill/spec-management.md +# 期望:≥ 1 行命中(章节标题) +``` + +--- + +## 检查 7:人工评测(对应 SC-001、SC-004) + +这两条无法自动化,需要执行下列人工步骤: + +### SC-001:新用户首次产出 Phase 1 项目 ≤ 30 分钟 + +1. 找一名有 FastAPI / SQLModel 基础但未用过 nexusx 的开发者 +2. 给他读 `skill/SKILL.md` + `skill/phases/phase0.md` + `skill/phases/phase1.md` +3. 计时:从开始读到产出可运行的 Phase 1 项目(Voyager ER 图可见) +4. 记录耗时,期望 ≤ 30 分钟 + +### SC-004:phase 文档独立阅读理解度 ≥ 80% + +1. 随机抽取 `phases/phase2.md` +2. 让评审者只读该文件(不读其他 phase、不读 SKILL.md) +3. 让他回答 5 个理解度问题: + - Phase 2 的目标是什么? + - 需要新增哪些文件? + - mount_method() 在哪里调用? + - V 降的验收标准是什么形式? + - 列出至少 2 个踩坑经验 +4. 期望答对 ≥ 4 题(80%) + +--- + +## 通过判定 + +| 检查项 | 通过条件 | +|---|---| +| 检查 1(自洽性) | 所有 grep 期望命中均为空或符合预期 | +| 检查 2(入口总览) | SKILL.md 含总览章节、Phase 0 外置、版本集中 | +| 检查 3(模板可运行) | 4 个端点 PASS | +| 检查 4(测试位置与运行) | tests/ 下三个文件、pytest 全过 | +| 检查 5(自包含) | 关键概念在 phase 内联,无强制外链 | +| 检查 6(工作流) | 中文化 + 迁移指引均存在 | +| 检查 7(人工评测) | SC-001 ≤ 30 分钟、SC-004 ≥ 80% 理解度 | + +任意一项失败 → 返回对应 FR / SC 重新实施,**不允许跳过**。 + +--- + +## 验证产物归档 + +执行完上述检查后,将检查结果(含 grep 输出、curl 输出、pytest 输出、人工评测记录)归档到 `specs/006-skill-template-polish/vv-result.md`(V&V 结果),作为交付前的最终凭证。该文件由实施阶段生成,不在本计划范围内。 diff --git a/specs/006-skill-template-polish/research.md b/specs/006-skill-template-polish/research.md new file mode 100644 index 0000000..b17379b --- /dev/null +++ b/specs/006-skill-template-polish/research.md @@ -0,0 +1,118 @@ +# 研究与决策记录:skill 内容结构与模板优化 + +**Feature**: 006-skill-template-polish +**日期**: 2026-07-01 +**输入**: [spec.md Clarifications](./spec.md#clarifications) + 上一轮代码评审识别的 14 处 P0/P1/P2 问题 + +## 用途 + +本文件汇总 `/speckit-clarify` 已拍板的 5 条核心决策,以及落地阶段需要回答的子决策。所有 NEEDS CLARIFICATION 在 clarify 阶段已闭环;本文件不再引入新澄清,只补充"如何执行"的依据。 + +--- + +## 核心决策(来自 clarify) + +### D-01 Phase 4 范围 + +- **决策**:仅 Phase 0~3(Python)+ Phase 4 文档对齐;不重写 `fe/` TS SDK 模板代码 +- **理由**:Phase 4 模板是 `@hey-api/openapi-ts` 自动生成产物,人工重写收益低;上一轮识别的所有 P0 矛盾都在 Phase 0~3 范围 +- **替代方案(已否)**:① 全量覆盖 Phase 0~4 + 重写 fe/(工作量翻倍,收益边际);② 完全跳过 Phase 4(会使 phase4.md 与新结构脱节) +- **落地约束**:`phase4.md` 仍需校准术语、路径、版本声明与 Phase 0~3 一致 + +### D-02 测试文件位置 + +- **决策**:项目级 `tests/test__methods.py`,每个业务域一个文件 +- **理由**:规避 `tests` 导入 `src.models`、`models.py` 底部 import service methods 的循环导入(phase2.md 踩坑 #6 已分析);符合 Python 主流布局 +- **替代方案(已否)**:① `service//test.py`(结构紧凑但循环导入需要 conftest 绕弯);② 混合分层(增加心智负担) +- **落地约束**:模板把 `template/src/service/{user,sprint,task}/test.py` 迁移到 `template/tests/test__methods.py`;删除原位置 `test.py` + +### D-03 目标用户 + +- **决策**:单人独立开发者为主,老用户结构迁移为次;不覆盖团队协作 / CI / 多人分支策略 +- **理由**:模板与 skill 现有设计是"单项目渐进演进",团队场景归 nexusx 主项目或团队规范 +- **替代方案(已否)**:① 单人 + 团队并重(扩大范围,FR 数量翻倍);② 仅新人(删除 FR-009 迁移指引会让老用户卡住) +- **落地约束**:SC-001 测评对象限定"独立开发者首次使用 skill";FR-009 迁移指引聚焦"老 specs/ 项目如何过渡到新 skill 结构" + +### D-04 外部 docs 引用处理 + +- **决策**:关键概念自包含(虚拟实体 / 跨层数据流 / 3.0 MCP 迁移) +- **理由**:用户原话强调"使用过程更平滑",读 skill 中途跳出去找 nexusx 包内 docs 是高频打断点 +- **替代方案(已否)**:① 仅声明 docs 位置(改善有限);② 镜像 docs(双份维护负担) +- **落地约束**:phase0.md / phase3.md 内每处引用 `docs/guide/*`、`docs/api/*`、`docs/migrations/*` 前补 10~20 行摘要;外部 docs 标注为"延伸阅读" + +### D-05 Phase 0 外置结构 + +- **决策**:单文件 `phases/phase0.md`,内部按 Step 0-1~0-8 二级标题分节 +- **理由**:Phase 0 是连贯对话流程,单文件便于连续阅读 +- **替代方案(已否)**:① 子目录多文件(8 个 Step 文件管理成本超过收益);② 维持内联(与 FR-003 冲突) +- **落地约束**:SKILL.md 顶部改为入口总览 + Phase 0~4 导航,正文移除 200+ 行 Phase 0 内容 + +--- + +## 落地阶段子决策(执行层面) + +### S-01 入口总览放在哪里? + +- **选项 A**:写入 SKILL.md 顶部(在 Phase 0 表格之上) +- **选项 B**:独立 `skill/README.md`,SKILL.md 顶部加链接 +- **选项 C**:两者都做(README 给仓库访客,SKILL.md 顶部给 Claude Code 加载时) + +**决策**:选 A。Claude Code 加载 skill 时只读 SKILL.md,独立 README 不会被自动加载;总览必须在 SKILL.md 内才能起到"一页地图"作用。仓库根的 nexusx/README.md 是另一个层级,不在本轮范围。 + +### S-02 `argument-hint` 字段怎么处理? + +- Claude Code skill frontmatter 只识别 `name` / `description`,`argument-hint` 是无效字段 +- **决策**:删除 `argument-hint`,把调用约定说明移到 SKILL.md 正文("调用时传入目标目录路径作为参数") +- **替代方案(已否)**:保留但加注释(无效字段留作用户混淆源) + +### S-03 模板 main.py 默认出口组合选什么? + +- phase3.md 列出 6 种出口(MCP / GraphQL HTTP / REST / JSON-RPC / CLI / Voyager) +- **决策**:默认演示 **REST + UseCase GraphQL MCP + Voyager**,其余(GraphQL HTTP / JSON-RPC / CLI)以注释形式保留 +- **理由**:REST 是 OpenAPI/SDK 链路必经;UseCase MCP 是 nexusx 3.0+ 的主推 MCP 入口;Voyager 是 ER/服务结构可视化必备;其余是可选扩展 +- **替代方案(已否)**:① 默认仅 REST(缺 MCP/可视化让模板失色);② 全部启用(与 FR-010 冲突,造成"必须全开"错觉) +- **额外清理**:模板现有 `create_mcp_server`(base 实体 MCP,老入口)是否保留?— **保留**作为对比示例但加注释说明它属于"base 实体层"而非"UseCase 层",避免与 phase3.md "3.0 起 UseCase MCP 只有 GraphQL 模式"陈述冲突 + +### S-04 spec-management.md 与 SKILL.md 路径统一为? + +- 当前矛盾:SKILL.md 写 `spec/phase0.md`(单数),spec-management.md 写 `specs/<编号>-*/phaseN.md`(复数 + 子目录) +- **决策**:统一为 `specs/<编号>-<需求简述>/phaseN.md`(与现有 specs/001~005 实际位置一致) +- **落地操作**:grep SKILL.md / phases/phase1~4.md 全部 `spec/phase` 出现位置,替换为 `specs/<编号>-<需求简述>/phaseN.md` 或上下文相关的具体引用 + +### S-05 迁移指引(FR-009)形式? + +- **选项 A**:写进 `spec-management.md` 一个新章节 `## 从旧结构迁移` +- **选项 B**:独立 `skill/migrations/` 目录 +- **决策**:选 A。迁移指引本质是 spec 工作流的一部分(涉及 specs/ 目录命名与文件位置),与 spec-management.md 同源;独立目录会让 spec 工作流被拆成多处 + +### S-06 版本门槛(FR-007)声明放哪? + +- **决策**:SKILL.md 顶部入口总览下方加一行 `**适用版本**:nexusx >= 3.2`,并在此处链接到 nexusx 版本与特性对照(虚拟实体=3.2+,UseCase GraphQL MCP=3.0+) +- **正文清理**:删除 phase1.md / phase3.md 中散落的"3.0 起"、"3.2+"等零散门槛,改为"参见 SKILL.md 适用版本" + +### S-07 中文化要求(FR-008)声明位置? + +- **决策**:spec-management.md 文件开头加一段 `## 语言要求`,声明所有 spec-kit 产物(含 phaseN.md)使用中文,引用项目 CLAUDE.md + +--- + +## 上一轮评审 P0/P1/P2 问题对账 + +| 编号 | 问题 | 对应决策 | 落地位置 | +|---|---|---|---| +| P0-1 | `spec/` vs `specs/<编号>-*/` 路径不一致 | S-04 | SKILL.md / phases/phase1~4.md | +| P0-2 | `argument-hint` 非有效字段 | S-02 | SKILL.md frontmatter | +| P0-3 | 模板 main.py 与 Phase 3 文档冲突 | S-03 | template/src/main.py | +| P0-4 | Phase 0 内联,结构不对称 | D-05 | phases/phase0.md(新增) | +| P0-5 | user service 模板残缺 | FR-004 | template/src/service/user/ | +| P0-6 | router 目录残留 | D-04 落地 | template/src/router/ 删除 | +| P1-7 | Phase 3 文档过载 | S-03 | phases/phase3.md 重组 | +| P1-8 | 缺入口总览 | S-01 | SKILL.md 顶部 | +| P1-9 | 版本门槛散落 | S-06 | SKILL.md + phases/* | +| P1-10 | 外部 docs 引用未声明 | D-04 | phase0.md / phase3.md 内联摘要 | +| P2-11 | Phase 4 验收过简 | D-01 | phase4.md 校准 | +| P2-12 | 测试位置说法不一 | D-02 | phases/phase2.md + 模板迁移 | +| P2-13 | 中文化要求未显式 | S-07 | spec-management.md | +| P2-14 | 缺迁移 / 升级说明 | S-05 | spec-management.md 新章节 | + +所有 P0/P1/P2 均已闭环到具体决策与落地位置,无遗留 NEEDS CLARIFICATION。 diff --git a/specs/006-skill-template-polish/spec.md b/specs/006-skill-template-polish/spec.md new file mode 100644 index 0000000..0c12f7f --- /dev/null +++ b/specs/006-skill-template-polish/spec.md @@ -0,0 +1,165 @@ +# 功能规格说明:skill 内容结构与模板优化 + +**功能分支**: `006-skill-template-polish` + +**创建日期**: 2026-07-01 + +**状态**: Draft + +**输入**: 用户原始描述:「我需要优化一下 skills 中的内容,以及代码模版。skill 内容的结构需要更加清晰合理,模版需要更完整一些。目的是让 skill 使用的过程中更加平滑,对用户指令更友好。」 + +--- + +## Clarifications + +### Session 2026-07-01 + +- Q: Phase 4(TS SDK 生成)是否在本轮优化范围内? → A: 仅 Phase 0~3(Python)+ Phase 4 文档对齐;不重写 `fe/` 模板代码(Phase 4 模板是 `openapi-ts` 自动生成的产物,本轮只校准 `phase4.md` 与上游文档的一致性)。 +- Q: 测试文件位置统一到哪? → A: 项目级 `tests/test__methods.py`(每个业务域一个文件)。理由:规避 `tests` 导入 `src.models` 与 `models.py` 底部 import service methods 的循环导入,符合 Python 主流布局。模板需要把 `service//test.py` 迁移到 `tests/test__methods.py`,`phases/phase2.md` 维持现有踩坑分析。 +- Q: 本轮优化面向的主要用户是? → A: 单人独立开发为主,老用户结构迁移为次要;不覆盖团队协作的 CI / 多人分支策略(归 nexusx 主项目或团队规范)。SC-001 测评对象限定为"独立开发者首次使用 skill",FR-009 迁移指引主要面向"在旧 skill 结构下产出过 specs/ 项目并需要过渡到新结构的老用户"。 +- Q: skill 文档中引用的外部 docs 怎么处理? → A: 关键概念自包含。把虚拟实体、跨层数据流(`ExposeAs`/`SendTo`/`Collector`)、3.0 UseCase GraphQL MCP 迁移等核心概念在对应 phase 文档内提供 10~20 行级别内联摘要;外部 nexusx 包内 docs(`docs/guide/virtual_entities.md` 等)降级为"延伸阅读",用户不点外链也能完成本阶段决策。 +- Q: Phase 0 外置后的内部结构? → A: 单文件 `phases/phase0.md`,内部按 Step 0-1~0-8 用二级标题分节。Phase 0 是一个连贯的"逐步确认"对话流程,单文件便于连续阅读与检索,不进一步按 Step 拆分多文件。 + +--- + +## 用户场景与测试 *(mandatory)* + +### 用户故事 1 — 文档与模板自洽 (Priority: P1) + +**作为** 一名使用 nexusx-4phase skill 的开发者, +**当我** 按 skill 文档创建项目并对照模板代码学习时, +**我希望** 文档中提到的路径、API 名称、文件位置、版本门槛都与模板代码完全一致, +**以便** 我不会因为"文档说 A、模板做 B"的矛盾而卡住或写出错误代码。 + +**为何此优先级**:自洽是一切可用性的基础。任何一处文档与模板的矛盾都会让用户对整个 skill 的可信度产生怀疑,且会在每个 phase 反复踩坑。修复成本最低、收益最大。 + +**独立测试**:随机抽取 5 处 skill 文档中的具体陈述(路径、API、文件名),逐一在模板代码中找到对应实现,全部命中即视为通过。 + +**验收场景**: + +1. **Given** skill 文档中提到的 spec 文件路径,**When** 用户在模板或参考项目中查找该路径,**Then** 实际路径与文档完全一致(不存在 `spec/` 与 `specs/<编号>-*/` 这种单复数或层级差异)。 +2. **Given** skill 文档中提到的某个 nexusx API(如 `create_use_case_graphql_mcp_server`),**When** 该 API 在模板 `main.py` 中被使用,**Then** 其调用方式与文档描述完全一致,不出现文档已声明"移除"的旧 API。 +3. **Given** 模板中存在的目录或文件(如 `router/`),**When** 用户在文档中查找该目录的说明,**Then** 文档对该目录的存在或不存在有明确陈述,不出现"文档说不需要,模板里却存在"的矛盾。 + +--- + +### 用户故事 2 — 一页总览快速上手 (Priority: P1) + +**作为** 一名首次接触 nexusx-4phase skill 的开发者, +**当我** 打开 skill 目录时, +**我希望** 先看到一份简明的入口总览(一页可读),覆盖每个阶段的输入、产出、关键 API 与典型坑, +**以便** 我用 5 分钟就能决定从哪里开始读、当前阶段需要做什么,而不必通读全部上千行文档。 + +**为何此优先级**:当前的 phase 文档单篇都很长(Phase 0 内联 200+ 行,Phase 3 接近 200 行),新人没有入口地图,容易迷路。这是阻碍 skill 被广泛使用的最大障碍。 + +**独立测试**:找一名未接触过 nexusx 的开发者,给他 5 分钟阅读入口总览,要求他口述"四阶段每阶段做什么、产出什么",能 80% 准确复述即视为通过。 + +**验收场景**: + +1. **Given** 一名未读过 skill 的开发者,**When** 他打开 skill 入口(SKILL.md 顶部或独立的 README),**Then** 在一屏内能看到一张覆盖 Phase 0~4 的总览表,列出每阶段的输入、产出文件、关键 API。 +2. **Given** 开发者想深入了解某个阶段,**When** 他在总览中点击/跳转到对应 phase 文档,**Then** 该文档独立可读,无需先读其他 phase 也能理解本阶段任务。 +3. **Given** Phase 0(需求确认)当前内联在 SKILL.md,**When** 用户查看目录结构,**Then** Phase 0 与 Phase 1~4 一样有独立的 `phases/phase0.md` 文件,结构对称。 + +--- + +### 用户故事 3 — 完整模板覆盖全程 (Priority: P2) + +**作为** 一名按 skill 实施项目的开发者, +**当我** 进入某个 phase 并需要参考成熟写法时, +**我希望** 模板项目包含每个 phase、每个示例 service 的完整文件(含 `dtos.py` / `service.py` / `test.py` / `spec.md`), +**以便** 我直接对照模板就能写出符合规范的代码,不需要自己脑补缺失的部分。 + +**为何此优先级**:当前模板中 `service/user/` 只有 `methods.py` + `spec.md`,缺 `dtos.py`/`service.py`/`test.py`,而 sprint/task 都齐全。这种"残缺"会让用户怀疑是不是 user service 故意省略有特殊原因,或者直接照着缺失模板写出不完整代码。 + +**独立测试**:模板项目按 phase 顺序逐阶段运行(Phase 1 建表 → Phase 2 挂方法 → Phase 3 出 REST/MCP → Phase 4 生成 SDK),每个阶段都能直接运行通过,不需要补任何文件。 + +**验收场景**: + +1. **Given** 模板项目 `template/src/service/`,**When** 列出每个 service 子目录的文件,**Then** 所有 service 都包含完整的 `__init__.py` / `methods.py` / `dtos.py` / `service.py` / `test.py` / `spec.md`(或文档明确说明某文件按 phase 渐进出现的位置)。 +2. **Given** 模板项目根目录,**When** 执行 `uvicorn src.main:app --reload`,**Then** 服务能启动且 `/voyager`、`/graphql`、REST 端点、MCP 端点全部可用,无需任何修改。 +3. **Given** 模板项目的测试,**When** 执行全部测试,**Then** 测试全部通过,覆盖至少一个正常场景和一个边界场景。 + +--- + +### 用户故事 4 — 决策引导清晰 (Priority: P3) + +**作为** 一名进入 Phase 3 的开发者, +**当我** 面对多种出口形态(REST / JSON-RPC / GraphQL HTTP / MCP / CLI / Voyager)选择时, +**我希望** skill 给我明确的"推荐默认组合"和"何时选什么扩展"的决策引导, +**以便** 我不需要把 6 种方案都读完才能做决定。 + +**为何此优先级**:当前 Phase 3 文档把 6 种出口并列展开,信息密度过高。新手难以判断"先做哪个、哪些可选"。优先级低于前三个,因为信息虽然过载但内容是齐全的。 + +**独立测试**:阅读 Phase 3 文档的"出口决策"部分后,能在 1 分钟内回答"如果我要给 AI agent 用,应该选哪个组合?如果要给传统 HTTP 客户端用呢?"。 + +**验收场景**: + +1. **Given** Phase 3 文档,**When** 用户查看出口形态相关章节,**Then** 首先看到"推荐默认组合"的明确陈述(如 REST + UseCase MCP + Voyager),其次才是"可选扩展"的小节。 +2. **Given** 模板 `main.py`,**When** 用户阅读其挂载代码,**Then** 默认只演示推荐组合,可选扩展以注释或独立示例的形式提供,不会让用户误以为所有出口都必须启用。 + +--- + +### Edge Cases + +- 当老用户已经在用旧的 skill 结构(如已写过 `spec/phase1.md` 单数路径的项目)时,新结构是否提供迁移指引? +- 当 skill 在多人团队中共享时,文档与模板的"单一信息源"原则如何避免再次出现分歧(即未来修改一处忘记同步另一处)? +- 当 nexusx 框架本身版本升级(如 3.2 → 3.3)引入新 API 时,skill 文档如何快速同步而不破坏既有结构? +- 当用户跳过 Phase 0 直接进入 Phase 1(如老用户做小迭代)时,依赖 `spec-management.md` 已有的"迭代功能的处理"章节(Phase 0 快速确认 = 只确认变更部分,不变的部分不重复讨论)。**判定标准**(FR-012):① 仅新增字段 / 方法 / 关系 → 可跳过 Step 0-1~0-3 的完整重过,只确认 delta;② 涉及聚合根变更、新业务域、DB 选型切换 → MUST 重做 Phase 0 对应 Step。skill 文档 MUST 在 `phases/phase0.md` 与 `spec-management.md` 的"迭代功能的处理"之间建立双向交叉引用,让用户在两处都能找到此判定标准。 + +--- + +## 需求 *(mandatory)* + +### Functional Requirements + +- **FR-001**: skill 文档中关于路径、API 名称、文件位置、版本门槛的所有具体陈述,MUST 与 `template/` 代码自洽,不存在互相矛盾。 +- **FR-002**: skill MUST 提供一份入口总览文档(SKILL.md 顶部或独立 README),用一页表格覆盖 Phase 0~4 的输入、产出文件、关键 API 与典型坑,作为用户进入 skill 的第一站。 +- **FR-003**: Phase 0(需求确认)内容 MUST 与 Phase 1~4 采用相同的组织方式,外置到独立的 `phases/phase0.md`(单文件),内部按 Step 0-1~0-8 用二级标题分节,与 SKILL.md 主文档解耦。 +- **FR-004**: 模板项目 MUST 覆盖 Phase 0~3 的完整 Python 代码示例,所有示例 service(user / sprint / task 等)在文件结构上保持对等,不出现"某些 service 缺文件"的残缺。Phase 4 的 `fe/` TypeScript SDK 模板不在本轮重写范围内(属工具自动生成产物),但 `phase4.md` 文档 MUST 与 Phase 0~3 的术语、路径、版本门槛对齐。 +- **FR-005**: Phase 3 文档 MUST 区分"推荐默认出口组合"与"可选扩展",并给出基于场景的决策引导(如"AI agent 用 MCP、传统 HTTP 用 REST")。 +- **FR-006**: skill MUST 明确测试文件位置为**项目级 `tests/test__methods.py`**(每个业务域一个文件),且与模板实际放置位置一致。模板现有的 `service//test.py` MUST 迁移到该位置。 +- **FR-007**: skill 文档 MUST 在显眼位置(推荐入口总览)声明所假设的 nexusx 最低版本门槛,正文不再散落"3.0 起 / 3.2+"等零散版本声明。 +- **FR-008**: skill 文档 MUST 显式声明所有 spec-kit 产物(包括 `phaseN.md`)使用中文撰写,与项目 CLAUDE.md 的中文化要求保持一致。 +- **FR-009**: skill MUST 提供一份从旧结构到新结构的迁移指引(哪怕是简短一段),覆盖路径变化、文件外置等破坏性调整,方便老用户过渡。**迁移时 MUST 保留 spec 编号**:旧项目目录 `specs/<旧编号>-<旧描述>/` 重命名为 `specs/<旧编号>-<新描述>/`,编号(如 `003`)禁止变更,以保证 git 历史连续与外部引用不断裂;只有描述部分可更新。 +- **FR-010**: 模板 `main.py` MUST 默认只演示推荐出口组合,可选出口以注释或独立示例文件提供,避免给用户"必须全部启用"的错觉。 +- **FR-011**: skill 文档中关于**虚拟实体**(Phase 0 决策)、**跨层数据流** `ExposeAs`/`SendTo`/`Collector`(Phase 3)、**3.0 UseCase GraphQL MCP 迁移**(Phase 3)等核心概念 MUST 在对应 phase 文档内提供 10~20 行级别的内联摘要,使用户不点外部链接也能完成本阶段决策;nexusx 包内 `docs/guide/*`、`docs/api/*`、`docs/migrations/*` 等外部引用降级为"延伸阅读"标注。 +- **FR-012**: skill 文档 MUST 在 `phases/phase0.md` 与 `spec-management.md` 的"迭代功能的处理"章节之间建立**双向交叉引用**,并显式记录"何时可跳过 Phase 0 / 何时必须重做"的判定标准(仅新增字段方法 → 可跳过;聚合根 / 业务域 / DB 选型变更 → 必须重做)。 + +### Key Entities *(include if feature involves data)* + +- **skill 主文档(SKILL.md)**:skill 的入口,描述方法论与阶段总览。优化后应瘦身,主要承载总览与导航。 +- **phase 文档(phases/phaseN.md)**:每阶段的详细指令。优化后所有 phase(含 0)结构对称、独立可读。 +- **spec-management 工作流(spec-management.md)**:spec 目录命名、文件格式、写入时机、交付校验的规则集。 +- **模板项目(template/)**:参考代码,所有 phase 实现的"金标准"。优化后覆盖完整、可直接运行。 +- **入口总览**:新增的一页快速参考,连接 SKILL.md 与各 phase 文档。 + +--- + +## 成功标准 *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: 一名具有 FastAPI / SQLModel 基础但未接触过 nexusx 的开发者,从打开 skill 到产出第一个可运行的 Phase 1 项目,总耗时 ≤ 30 分钟。 +- **SC-002**: 对 skill 文档与模板代码执行一致性检查(路径、API、文件名、版本声明),矛盾点数量为 0。 +- **SC-003**: 模板项目在干净的 venv 中 `uv sync && uvicorn src.main:app` 能直接启动,所有端点(`/voyager` / `/graphql` / REST / MCP)可访问,无需任何手工修改。 +- **SC-004**: 每个 phase 文档(phase0.md ~ phase4.md)独立阅读理解度 ≥ 80%,即不读其他 phase 文档也能正确理解本阶段任务并开始动手。**评测方法**:随机抽一份 phase 文档(如 `phase2.md`),让评审者只读该文件后回答 5 个理解度问题(目标是什么 / 新增哪些文件 / 关键调用点 / V 降验收形式 / 至少 2 个踩坑),答对 ≥4 题即通过(详见 [quickstart.md 检查 7](./quickstart.md#检查-7人工评测对应-sc-001sc-004))。 +- **SC-005**: Phase 3 出口决策时间 ≤ 1 分钟——开发者读完决策引导后能立刻选定适合自己场景的出口组合。 +- **SC-006**: 模板项目的全部自动化测试通过率 100%,至少覆盖每个 service 一个正常场景 + 一个边界场景。 + +--- + +## 假设 + +- 目标用户是具备 FastAPI / SQLModel / Python 异步基础的开发者;不具备这些前置技能的用户不在目标范围内。本轮优化以**单人独立开发者**为首要受众,**老用户结构迁移**为次要受众;团队协作场景(CI、多人分支策略、模板版本对齐流程)不在本轮范围。 +- 本次优化的范围限定在 skill 文档(`skill/` 目录)与模板代码(`skill/template/`),**不修改 nexusx 框架本身的代码**。任何对框架 API 的引用都以现状为准。 +- 保持 skill 现有的核心方法论不变:V 型验收模型、Phase 0~4 五阶段递进、spec 工作流。 +- 不引入新的外部依赖或新的工具链;优化聚焦于"现有内容的重组、补齐、对齐"。 +- nexusx 当前主版本为 3.x,本 skill 文档假设用户使用 ≥ 3.2 版本(含虚拟实体等特性)。**框架升级同步策略**(针对未来 4.0 等破坏性变更):① 升级后立即跑一遍 `quickstart.md` 全部 7 组检查,定位失效项;② 更新 SKILL.md `## 适用版本` 章节,同步特性-版本对照表;③ 重大 API 变更(如装饰器名、handler 签名变化)MUST 新建独立 spec(如 `specs/0XX-skill-nexusx-4-sync/`)走完整四阶段流程,不在本 feature 内热修。 +- 用户已通过项目 CLAUDE.md 知晓 spec-kit 产物中文化要求;本 spec 及其后续 plan / tasks / checklists 全部使用中文。 + +## 范围外(Out of Scope) + +- **Phase 4 的 `fe/` TypeScript SDK 模板代码不重写**:Phase 4 模板是 `@hey-api/openapi-ts` 自动生成的产物,重写收益低;本轮只校准 `phase4.md` 文档与上游 Phase 0~3 的一致性(路径、术语、版本声明)。 +- 不修改 nexusx 框架本身的源码或 API。 +- 不引入新的工具链或新的依赖。 +- 不重写 V 型验收 / 五阶段递进的核心方法论,只重组其文档载体。 diff --git a/specs/006-skill-template-polish/tasks.md b/specs/006-skill-template-polish/tasks.md new file mode 100644 index 0000000..ef0df39 --- /dev/null +++ b/specs/006-skill-template-polish/tasks.md @@ -0,0 +1,237 @@ +--- + +description: "skill 内容结构与模板优化的任务清单" +--- + +# Tasks: skill 内容结构与模板优化 + +**输入**: 设计文档来自 `/specs/006-skill-template-polish/`(plan.md / spec.md / research.md / data-model.md / contracts/skill-interface.md / quickstart.md) + +**测试**: 本 feature 是文档/模板优化项目,无新增业务代码测试。Polish 阶段以 `quickstart.md` 的 7 组检查作为验收测试,**强制执行**。 + +**组织**: 按 spec.md 的 4 个 user story(US1~US4)+ Setup/Foundational/Polish 三层组织。 + +## Format: `[ID] [P?] [Story] Description` + +- **[P]**: 可并行(不同文件、无未完成任务依赖) +- **[Story]**: 任务归属的 user story(US1=自洽 / US2=入口总览 / US3=模板完整 / US4=决策引导) +- 每个任务都带具体文件路径 + +## Path Conventions + +- 本 feature 修改对象是 `skill/` 子树 +- 文档路径:`skill/SKILL.md` / `skill/spec-management.md` / `skill/phases/phaseN.md` +- 模板路径:`skill/template/src/...` / `skill/template/tests/...` +- spec 路径:`specs/006-skill-template-polish/...` + +--- + +## Phase 1: Setup(项目准备) + +**Purpose**: 建立修改基线,记录"修改前"状态用于最终对比 + +- [ ] T001 切换到 feature 分支 `006-skill-template-polish`(已由前置流程创建);确认工作树干净 +- [ ] T002 [P] 跑一遍 quickstart.md 检查 1 的 grep 命令,把"修改前"的命中数记录到 `specs/006-skill-template-polish/baseline.md`(用于 V&V 对比) +- [ ] T003 [P] 备份当前 `skill/SKILL.md`(git tag `skill-pre-polish-006`),方便回滚对比 + +--- + +## Phase 2: Foundational(路径与术语统一 — 阻塞所有 user story) + +**Purpose**: 修复 P0 级矛盾点(路径不一致、frontmatter 非法字段、版本散落、中文化声明缺失),所有后续 user story 都依赖这些基础对齐 + +**⚠️ CRITICAL**: 此阶段未完成前,禁止进入 Phase 3+ 任何 user story + +- [ ] T004 [P] 编辑 `skill/SKILL.md` 删除 frontmatter 中的 `argument-hint` 字段(保留 `name` / `description` 两个字段),把调用约定说明移到正文 Phase 0 章节前的"调用约定"段落 +- [ ] T005 [P] 全局路径统一:`grep -rn "spec/phase" skill/` 找到所有单数路径引用,逐处替换为 `specs/<编号>-<需求简述>/phaseN.md` 或上下文相关的具体路径;覆盖 `skill/SKILL.md` + `skill/phases/phase1.md` ~ `phase4.md` + `skill/spec-management.md` +- [ ] T006 [P] 在 `skill/spec-management.md` 文件开头(`## 目录命名` 之前)新增 `## 语言要求` 章节,声明所有 spec-kit 产物使用中文,引用项目 `CLAUDE.md` 的对应规则 +- [ ] T007 [P] 在 `skill/spec-management.md` 末尾新增 `## 从旧结构迁移` 章节(FR-009),覆盖:① `spec/` 单数 → `specs/<编号>-*/` 复数;② Phase 0 从 SKILL.md 内联 → `phases/phase0.md` 外置;③ 老项目存量 spec 文件的迁移建议 +- [ ] T008 [P] 在 `skill/SKILL.md` frontmatter 之后新增 `## 适用版本` 章节,声明 `nexusx >= 3.2`,附特性-版本对照(虚拟实体=3.2+,UseCase GraphQL MCP=3.0+) +- [ ] T009 编辑 `skill/phases/phase1.md` / `phase2.md` / `phase3.md`:移除正文中散落的"3.0 起"、"3.2+"等零散版本门槛,改为引用"参见 SKILL.md 适用版本";保留具体 API 引用(如 `create_use_case_graphql_mcp_server`)旁边的版本说明时简化为"3.0+" + +**Checkpoint**: 所有 P0 矛盾点的基础对齐完成;后续 user story 可以开始 + +--- + +## Phase 3: User Story 1 - 文档与模板自洽 (Priority: P1) 🎯 MVP + +**Goal**: 文档中提到的路径、API、文件位置与模板代码完全一致,零矛盾 + +**Independent Test**: 随机抽取 5 处 skill 文档中的具体陈述(路径、API、文件名),逐一在模板代码中找到对应实现,全部命中 + +### Implementation for User Story 1 + +- [ ] T010 [P] [US1] 删除 `skill/template/src/router/` 整个目录(与 phase3.md "不需要手写 router" 一致) +- [ ] T011 [P] [US1] 校准 `skill/phases/phase1.md`:确认所有文件路径与 `skill/template/src/` 实际结构对齐(db.py / models.py / database.py / main.py 命名一致),alembic 章节步骤与 `skill/template/pyproject.toml` 依赖示例对齐 +- [ ] T012 [P] [US1] 校准 `skill/phases/phase2.md`:踩坑 #6 已经说"项目级 tests/",模板要同步——本任务只改文档侧的描述措辞(具体迁移在 US3);同步更新 `mount_method()` 示例与模板 `models.py` 一致 +- [ ] T013 [P] [US1] 校准 `skill/phases/phase4.md`:术语、路径(`spec/` → `specs/<编号>-*/`)、版本声明对齐 Phase 0~3;不修改 `fe/` 模板代码(FR Out-of-Scope) +- [ ] T014 [US1] 重组 `skill/template/src/main.py`:默认仅挂载 REST(`create_use_case_router`)+ UseCase GraphQL MCP(`create_use_case_graphql_mcp_server`)+ Voyager(`create_use_case_voyager`)+ GraphQL HTTP(`GraphQLHandler` + `/graphql`);JSON-RPC(`create_jsonrpc_router`)和 CLI(`create_use_case_cli`)以注释形式保留;为保留的 `create_mcp_server` 调用加注释说明它属于"base 实体层 MCP(与 UseCase MCP 不同的层级)",避免与 phase3.md "3.0 起 UseCase MCP 只有 GraphQL 模式"冲突 +- [ ] T015 [US1] 校准 `skill/template/pyproject.toml`:确认 `[tool.hatch.build.targets.wheel] packages = ["src"]`;补依赖 `uvicorn`(启动用)、`aiosqlite`(默认 in-memory sqlite driver);持久化场景的 `alembic>=1.13` / `asyncpg` / `aiomysql` 以注释示例;同步更新 `skill/template/uv.lock`(`uv lock`) + +**Checkpoint**: 文档与模板完全自洽;`grep -rn "spec/phase" skill/` 应为空(除 specs/ 复数);模板可启动 + +--- + +## Phase 4: User Story 2 - 入口总览 + Phase 0 外置 (Priority: P1) + +**Goal**: 新用户 5 分钟内能通过入口总览掌握四阶段全貌;Phase 0 与 Phase 1~4 结构对称 + +**Independent Test**: 找一名未用过 nexusx 的开发者,给他 5 分钟阅读 SKILL.md 顶部入口总览,能 80% 准确口述四阶段每阶段做什么 + +### Implementation for User Story 2 + +- [ ] T016 [US2] 新建 `skill/phases/phase0.md`:把 `skill/SKILL.md` 当前的 Phase 0 章节(Step 0-1 ~ Step 0-8 + 检查清单)整体迁移过来,按二级标题(`## Step 0-1`、`## Step 0-2` …)分节;保留所有子表格与示例;顶部加 Phase 0 目标说明 +- [ ] T017 [US2] 重写 `skill/SKILL.md` 正文:① 在 frontmatter 后加 `## 调用约定`(一句话说明 `/nexusx-4phase [项目目录]`);② 加 `## 适用版本`(FR-007,已在 T008 创建,此处仅校验);③ 加 `## 入口总览` 章节,用一张表覆盖 Phase 0~4 的输入/产出文件/关键 API/典型坑(一屏可见);④ 加 `## Phase 导航` 列出 phase0.md ~ phase4.md 的链接;⑤ **删除**原内联的 Phase 0 详细内容(已迁移到 phase0.md);⑥ 保留"核心原则 / V 型验收模型"段落 +- [ ] T018 [US2] 在 `skill/phases/phase0.md` 的 Step 0-3(聚合根-根类型选择)章节,为"虚拟实体(BaseModel,不落表)"概念补 10~20 行内联摘要,覆盖:何时选虚拟实体、`ErManager.add_virtual_entities()` 的调用时机、与 SQLModel 实体的差异;外部引用 `docs/guide/virtual_entities.md` 标注为"延伸阅读" + +**Checkpoint**: SKILL.md 瘦身到 ≤ 150 行;Phase 0 外置完成;新用户入口总览可读 + +--- + +## Phase 5: User Story 3 - 完整模板覆盖 (Priority: P2) + +**Goal**: 模板项目覆盖 Phase 0~3 完整代码,所有示例 service 文件结构对等;模板可直接运行 + +**Independent Test**: `cd skill/template && uv sync && uvicorn src.main:app`,4 个端点(`/voyager` / `/graphql` / `/openapi.json` / REST)全部可访问;`pytest tests/` 全过 + +### Implementation for User Story 3 + +- [ ] T019 [P] [US3] 新建 `skill/template/src/service/user/dtos.py`:参考 `skill/template/src/service/sprint/dtos.py` 结构,定义 `UserDTO`(用 `DefineSubset` 投影 `User` 实体),字段包含 `id` / `name` / `tasks`(关系字段,`AutoLoad` 标记) +- [ ] T020 [P] [US3] 新建 `skill/template/src/service/user/service.py`:参考 `skill/template/src/service/sprint/service.py` 结构,定义 `UserService`(继承 UseCaseService),实现 `list_users` 和 `create_user` 两个 `@query` / `@mutation` 方法;**所有方法必须声明返回类型注解**(如 `-> list[UserDTO]`、`-> UserDTO`) +- [ ] T021 [US3] 测试文件迁移:把 `skill/template/src/service/sprint/test.py` → `skill/template/tests/test_sprint_methods.py`;`skill/template/src/service/task/test.py` → `skill/template/tests/test_task_methods.py`;删除原位置 `test.py`;调整 import(`from src.service..methods import ...` 不变,monkeypatch 路径同步) +- [ ] T022 [US3] 新建 `skill/template/tests/test_user_methods.py`:覆盖 `create_user` 一个正常场景(创建成功,返回 UserDTO)+ 一个边界场景(重名 / 缺字段,返回预期错误);测试通过 `pytest` 跑通 +- [ ] T023 [US3] 更新 `skill/template/src/service/user/spec.md`:补 `UserService` 的服务目的、方法清单、DTO 说明(与 sprint/task 的 spec.md 对等) +- [ ] T024 [US3] 在 `skill/template/src/main.py` 的 `UseCaseAppConfig.services` 列表中加入 `UserService`(与 `TaskService` / `SprintService` 并列);`create_use_case_voyager(services=...)` 同步加入 +- [ ] T025 [US3] 启动验证:`cd skill/template && uv sync && uvicorn src.main:app --port 8765`,curl 探活 `/voyager/` / `/graphql` / `/openapi.json` / `/api/template/user_service/list_users`;4 个端点全 PASS;`pytest tests/` 全过 + +**Checkpoint**: 三个 service 文件结构对等;模板开箱即用;SC-003 + SC-006 通过 + +--- + +## Phase 6: User Story 4 - 决策引导清晰 (Priority: P3) + +**Goal**: Phase 3 出口决策时间 ≤ 1 分钟;关键概念自包含 + +**Independent Test**: 阅读 `phase3.md` 出口决策部分后,能在 1 分钟内回答"AI agent 场景选什么 / 传统 HTTP 场景选什么" + +### Implementation for User Story 4 + +- [ ] T026 [US4] 重组 `skill/phases/phase3.md`:把现有 6 种出口的并列展示拆为两段——① `### 推荐默认组合`(REST + UseCase GraphQL MCP + Voyager + GraphQL HTTP,附场景说明);② `### 可选扩展`(JSON-RPC、CLI,附"何时启用"指引);段间加决策树/表格"按场景选出口" +- [ ] T027 [US4] 在 `skill/phases/phase3.md` 内补跨层数据流摘要段落(10~20 行):覆盖 `ExposeAs(field_name, source=...)` / `SendTo(field_name)` / `Collector(field_name)` 三个 helper 的用途、典型场景;外部引用 `docs/api/api_cross_layer.md` 标注为"延伸阅读" +- [ ] T028 [US4] 在 `skill/phases/phase3.md` 内补 3.0 UseCase GraphQL MCP 迁移摘要段落(10~20 行):覆盖老的 `create_use_case_mcp_server` / `create_use_case_flat_server` 已移除、新 `create_use_case_graphql_mcp_server` 4 层渐进披露模型;外部引用 `docs/migrations/3.0-use-case-graphql.md` 标注为"延伸阅读" + +**Checkpoint**: Phase 3 文档不再过载;FR-005 + FR-011 通过 + +--- + +## Phase 7: Polish & 跨故事收尾 + +**Purpose**: 跑完整 quickstart.md 验证 + 人工评测,归档结果 + +- [ ] T029 [P] 执行 `quickstart.md` 检查 1(文档自洽性 grep),结果归档到 `specs/006-skill-template-polish/vv-result.md` +- [ ] T030 [P] 执行 `quickstart.md` 检查 2(入口总览可读性),结果归档到 vv-result.md +- [ ] T031 执行 `quickstart.md` 检查 3(模板可运行性 4 端点 curl),结果归档到 vv-result.md +- [ ] T032 [P] 执行 `quickstart.md` 检查 4(pytest tests/),结果归档到 vv-result.md +- [ ] T033 [P] 执行 `quickstart.md` 检查 5(核心概念自包含),结果归档到 vv-result.md +- [ ] T034 [P] 执行 `quickstart.md` 检查 6(spec-management 完整性),结果归档到 vv-result.md +- [ ] T035 人工评测:找一名有 FastAPI 基础但未用过 nexusx 的开发者,执行 SC-001(≤30 分钟产出 Phase 1 项目)+ SC-004(phase2.md 独立阅读理解度 ≥80%);记录到 vv-result.md +- [ ] T036 [P] 整理 `specs/006-skill-template-polish/phase0.md` ~ `phase4.md` 五份 spec 文档(按 spec-management.md 的"写入时机"要求,回填每个 phase 的"需求说明 / 验收标准 / 实现描述"三段) +- [ ] T037 [P] 把 `specs/006-skill-template-polish/story.md` 补齐(如缺失),含原始需求 + Overview Design +- [ ] T038 建立 FR-012 双向交叉引用:① `skill/phases/phase0.md` 末尾加 `## 老用户迭代:何时跳过 Phase 0` 章节引用 `spec-management.md` 的"迭代功能的处理";② `skill/spec-management.md` 的"迭代功能的处理"章节反向引用 `phases/phase0.md`;③ 两处都明确"仅新增字段方法 → 可跳过;聚合根 / 业务域 / DB 选型变更 → 必须重做"判定标准 + +**Checkpoint**: 所有 SC 验证通过;spec 文档闭环;可交付 + +--- + +## Dependencies & Execution Order + +### Phase Dependencies + +- **Setup (Phase 1)**: 无依赖,立即开始 +- **Foundational (Phase 2)**: 依赖 Setup 完成;**阻塞所有 user story** +- **US1 (Phase 3)**: 依赖 Foundational;阻塞 US2(共享 SKILL.md)、阻塞 US3(共享 main.py) +- **US2 (Phase 4)**: 依赖 US1 完成(避免 SKILL.md 写入冲突) +- **US3 (Phase 5)**: 依赖 US1 完成(main.py 默认出口先定,再补 user service);与 US2 可并行(不同文件:US2 改 SKILL/phase0,US3 改 template/service/*) +- **US4 (Phase 6)**: 依赖 US1(T013 改 phase3.md 路径)**和** US2(SKILL 总览结构稳定)都完成;T026~T028 必须在 T013 之后执行(同改 `phases/phase3.md`,避免合并冲突) +- **Polish (Phase 7)**: 依赖所有 user story 完成 + +### User Story Dependencies + +- **US1 (P1)**: Foundational 完成后开始;阻塞下游 +- **US2 (P1)**: US1 完成后开始 +- **US3 (P2)**: US1 完成后开始;可与 US2 并行 +- **US4 (P3)**: US2 完成后开始;可与 US3 并行 + +### Within Each User Story + +- 文档校准类任务(不同 phase 文件)可并行 +- 模板代码类任务(同 main.py)必须串行 +- 每个故事完成后跑对应 Independent Test 自检 + +### Parallel Opportunities + +- Setup 阶段 T002 / T003 可并行(不同操作) +- Foundational 阶段 T004 / T005 / T006 / T007 / T008 可并行(不同文件);T009 串行 +- US1 阶段 T010 / T011 / T012 / T013 可并行(不同文件);T014 / T015 串行(同 main.py / pyproject.toml) +- US2 阶段:T016 → T017 串行(SKILL.md 先迁出再重写);T018 可与 T016/T017 并行(改的是新 phase0.md 的局部章节) +- US3 阶段 T019 / T020 可并行(user/dtos.py 和 user/service.py 不同文件);T021 → T022 → T023 → T024 → T025 串行 +- US4 阶段 T026 → T027 → T028 必须串行(同改 `phases/phase3.md`,避免合并冲突);且整体必须在 T013(US1 阶段的 phase3 路径校准)完成之后 +- Polish 阶段大部分检查可并行 + +--- + +## Parallel Example: User Story 3(与 US2 并行启动) + +```bash +# 当 US1 完成、US2 正在进行时,另一人可以并行启动 US3: + +# US3 内部并行任务(不同文件): +Task: "T019 新建 skill/template/src/service/user/dtos.py" +Task: "T020 新建 skill/template/src/service/user/service.py" + +# US3 串行任务(同文件依赖): +Task: "T021 迁移 sprint/task 的 test.py 到 tests/" +# 等 T021 完成后: +Task: "T022 新建 tests/test_user_methods.py" +# 等 T022 完成后: +Task: "T023 更新 user/spec.md" +Task: "T024 注册 UserService 到 main.py" +Task: "T025 启动验证 4 端点" +``` + +--- + +## Implementation Strategy + +### MVP First(仅 US1) + +1. 完成 Phase 1 Setup(建基线) +2. 完成 Phase 2 Foundational(**关键** — 阻塞一切) +3. 完成 Phase 3 US1(自洽性修复,14 处 P0/P1 中风险最高的 6 处) +4. **STOP and VALIDATE**: 跑 quickstart.md 检查 1,矛盾点应为 0 +5. 此时 skill 已经"无矛盾、可运行",可作为内部预览版发布 + +### Incremental Delivery + +1. Setup + Foundational → 基线就绪 +2. + US1 → 自洽性达成(P0 全闭环)→ 验证 → Demo +3. + US2 → 入口总览可用(新用户上手成本骤降)→ 验证 → Demo +4. + US3 → 模板完整(开箱即用)→ 验证 → Demo +5. + US4 → 决策引导清晰(Phase 3 不再过载)→ 验证 → Demo +6. Polish → V&V 归档 + 人工评测 → 交付 + +### Solo Developer Strategy(单人执行) + +按 Phase 顺序串行:Setup → Foundational → US1 → US2 → US3 → US4 → Polish +每个 phase 完成后 commit,便于回滚。Polish 阶段失败时定位到具体 SC 重新迭代。 + +--- + +## Notes + +- [P] 标记 = 不同文件 + 无未完成任务依赖;同文件任务一律串行 +- [Story] 标签严格映射到 spec.md 的 US1~US4 +- 模板代码修改后必须 `cd skill/template && uv sync` 验证依赖解析 +- 所有 spec-kit 产物(含本 tasks.md)使用中文 +- 实施过程中如发现新矛盾点,**不要在本 tasks.md 直接加任务**——回到 spec.md / plan.md 走变更流程 +- 每个 phase 完成后跑对应 quickstart.md 检查作为 checkpoint diff --git a/specs/006-skill-template-polish/vv-result.md b/specs/006-skill-template-polish/vv-result.md new file mode 100644 index 0000000..62f71da --- /dev/null +++ b/specs/006-skill-template-polish/vv-result.md @@ -0,0 +1,193 @@ +# V&V 结果:skill 内容结构与模板优化 + +**Feature**: 006-skill-template-polish +**最后更新**: 2026-07-01 +**已覆盖范围**: Phase 1 Setup + Phase 2 Foundational + Phase 3 US1 + Phase 5 US3 = 22 任务(T001-T015, T019-T025) + +--- + +## 检查 1:文档自洽性(US1,FR-001) + +| 子项 | baseline | 当前 | 状态 | +|---|---|---|---| +| 1.1 `spec/phaseN` 精确匹配 | 6 处 | 1 处(迁移表"旧路径"列,有意保留) | ✅ PASS | +| 1.1 ASCII 图 `spec/.md`(SKILL.md:54/64) | 2 处 | 2 处 | ⚠️ 延迟到 T016(US2) | +| 1.2 `argument-hint` 残留 | 1 处 | 0 | ✅ PASS | +| 1.3 main.py 默认出口 | 全开 | REST + UseCase MCP + Voyager + GraphQL HTTP 推荐;base MCP 加层级注释;JSON-RPC / CLI 注释化 | ✅ PASS | +| 1.4 `template/src/router/` | 存在 | 已删 | ✅ PASS | +| 1.5 service 文件结构对等 | user 缺 dtos/service/test | 三 service 完全对等(`__init__` / `methods` / `dtos` / `service` / `spec`) | ✅ PASS | + +--- + +## 检查 3:模板可运行性(SC-003) + +```bash +cd skill/template && uv sync --all-extras && uv run uvicorn src.main:app --port 8765 +``` + +| 端点 | 路径 | 状态 | +|---|---|---| +| Voyager | `GET /voyager/` | ✅ HTTP 200 | +| GraphQL(GraphiQL) | `GET /graphql` | ✅ HTTP 200 | +| OpenAPI | `GET /openapi.json` | ✅ HTTP 200,含 11 个 path | +| REST(user) | `POST /api/user_service/list_users` | ✅ HTTP 200,返回 `[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"},{"id":3,"name":"Charlie"}]` | + +**SC-003 通过**:4/4 端点可访问,模板开箱即用,无需手工修改。 + +OpenAPI 完整 path 列表(11 个): + +``` +GET /graphql +POST /graphql +GET /schema +POST /api/user_service/list_users +POST /api/user_service/create_user +POST /api/task_service/list_tasks +POST /api/task_service/get_tasks_by_sprint +POST /api/task_service/create_task +POST /api/sprint_service/list_sprints +POST /api/sprint_service/get_sprint +POST /api/sprint_service/create_sprint +``` + +REST 路径模式:`/api//`(不含 app_name 前缀)。 + +--- + +## 检查 4:测试位置与可运行性(SC-006,FR-006) + +``` +skill/template/tests/ +├── conftest.py # in-memory sqlite + monkey-patch session +├── test_user_methods.py # 3 cases +├── test_sprint_methods.py # 3 cases +└── test_task_methods.py # 4 cases +``` + +```bash +uv run pytest tests/ -v +``` + +**结果**:10 passed in 0.09s + +| 测试文件 | 用例数 | 状态 | +|---|---|---| +| test_user_methods.py | 3(含 1 边界:空表) | ✅ | +| test_sprint_methods.py | 3(含 1 边界:not found) | ✅ | +| test_task_methods.py | 4(含 2 边界:empty filter / empty table) | ✅ | + +**SC-006 通过**:每个 service 覆盖至少 1 个正常 + 1 个边界场景。 + +--- + +## 已知延迟项(剩余 user story 范围) + +| 项 | 当前状态 | 闭环任务 | +|---|---|---| +| SKILL.md:54/64 ASCII 图中的 `spec/.md` | 待重排 | T016(US2) | +| Phase 0 内联 SKILL.md | 约 200 行内联 | T016/T017(US2) | +| Phase 3 文档过载(6 出口并列) | phase3.md 待重组 | T026~T028(US4) | +| Phase 3 内联摘要(虚拟实体/跨层数据流/3.0 MCP) | FR-011 待落实 | T018/T027/T028 | + +--- + +## 阶段结论 + +✅ **MVP + US3 完成(22 任务)**: +- 所有 P0 矛盾点闭环(路径 / argument-hint / router 残留 / main.py 默认出口) +- 模板可直接 `uv sync && uvicorn` 启动,4 端点全通 +- 10 个测试用例覆盖三个 service 的正常 + 边界场景 +- service 文件结构完全对等(user/sprint/task 三者一致) + +✅ **可发布内部预览**:开发者照模板学 Phase 1~3 不会因矛盾卡住;测试位置统一在 `tests/`;main.py 默认推荐出口 + 可选注释清晰。 + +⚠️ **未完成项不影响发布**:Phase 0 外置(US2)、Phase 3 重组(US4)属于"锦上添花"——当前 SKILL.md 仍可用,只是不够"瘦";phase3.md 信息密度高但仍正确。 + +--- + +## Polish 阶段全量验证(T029-T035,2026-07-01) + +### 已 commit 的 3 个 commit(分支 `006-skill-template-polish`) + +``` +ecbb227 feat(skill/template): 补齐 user service / 测试位置统一 / main.py 出口分级 +9ea4578 docs(skill): 校准路径 / frontmatter / 版本门槛 / 中文化与迁移指引 +40ef7a8 docs(specs): 006-skill-template-polish 全套 spec-kit 产物 +``` + +### 全 6 组检查结果 + +| # | 检查 | 状态 | 说明 | +|---|---|---|---| +| 1 | 自洽性 | ✅ PASS | 5/5 子项全过;唯一保留为迁移表"旧路径"列 | +| 2 | 入口总览可读性 | ⚠️ PARTIAL | 2.1 含 `## 适用版本` + `## 调用约定` 总览开端;2.2 Phase 0 外置延迟到 T016;2.3 版本集中 PASS(散落门槛 phaseN=0) | +| 3 | 模板可运行性 | ✅ PASS | 4/4 端点 HTTP 200;模板 seed 3 用户 | +| 4 | 测试位置与运行 | ✅ PASS | 10/10 pytest 通过;tests/ 下三文件;原 service/test.py 已删 | +| 5 | 核心概念自包含 | ⚠️ PARTIAL | phase3.md 已提跨层数据流(line 15-17)和 3.0 MCP(line 52);FR-011 完整 10~20 行内联摘要延迟到 T018/T027/T028 | +| 6 | spec-management 完整性 | ✅ PASS | 含 `## 语言要求` + `## 从旧结构迁移` | + +**总评**:3 项完全 PASS、2 项 PARTIAL(依赖 US2/US4 后续落实)、1 项需人工(T035 SC-001/SC-004)。 + +### 人工评测(T035)— 待执行 + +| SC | 测评对象 | 方法 | 当前状态 | +|---|---|---|---| +| SC-001 | 独立开发者首次使用 skill | 计时从读到产出 Phase 1 项目 ≤30 分钟 | ⏳ 待招志愿者 | +| SC-004 | phase 文档独立阅读理解度 | 抽 phase2.md,5 题答对 ≥4 | ⏳ 待招志愿者 | + +未执行原因:自动化无法评测。建议下次有合适 reviewer 时执行。 + +--- + +## Code Review 结果(2026-07-01,medium effort) + +走 `/code-review` skill:3 angles × 6 candidates → 1-vote verify → 2 actionable findings。 + +### Finding #1 — CONFIRMED(已修复) + +- **位置**:`skill/template/pyproject.toml:22-23` +- **问题**:`persist-pg = ["persist", "asyncpg"]` 与 `persist-mysql = ["persist", "aiomysql"]` 中的 `"persist"` 被解析为 PyPI 包(zope-interface 持久化库),而非本地 `persist` extra +- **触发场景**:用户按 phase1.md Step 0-7 选 PostgreSQL 持久化,`uv sync --extra persist-pg` 装到 PyPI persist + asyncpg + six + zope-interface + pluggy,**跳过 alembic**;首次 `alembic upgrade head` 直接 ModuleNotFoundError +- **修复**:改为 self-reference `["nexusx-template[persist]", "asyncpg"]` / `["nexusx-template[persist]", "aiomysql"]` +- **验证**:verifier 实测复现 + 修复后装到 `alembic==1.18.5 + asyncpg==0.31.0`;pytest 10/10 回归通过 +- **commit**:`86520ca` + +### Finding #2 — PLAUSIBLE(接受为技术债) + +- **位置**:`skill/template/tests/conftest.py:36-44` +- **问题**:`session_factory` fixture patch 了 `src.db.async_session` + 3 个 methods 模块,**未 patch** `models.er._session_factory` / `Resolver` / `main.graphql_handler.session_factory` / `mcp` apps——它们在 import 时已绑定生产 session +- **触发场景**:当前 10 个测试只调 methods 函数(走 patched 局部绑定),全过。但下一个想测 `UserService.list_users`(走 `Resolver().resolve()`)或 `main.app` 的 `/graphql` 端点的测试会落到生产 engine,污染开发数据库或 schema 不匹配 +- **决策**:**接受为已知技术债**,本轮不修。理由:当前测试覆盖范围(methods 层)不需要这些 patch;conftest 已有注释说明 patch 范围。未来扩展到服务层测试时再补 +- **缓解**:在 `tests/conftest.py` 加一条注释,列出"未 patch 的绑定 + 何时需要扩展" + +### 5 项 REFUTED(记录防止重复发现) + +| # | 原 candidate | REFUTE 依据 | +|---|---|---| +| 2 | conftest 全局污染跨测试 | 所有 10 个测试都请求 fixture;function-scoped + 每测试重赋值,"未请求的 test B" 场景不存在 | +| 4 | UserService 注册让 Voyager 启动失败 | `create_voyager` 构造时不做 introspection;实测 `/voyager/` HTTP 200 | +| 5 | Resolver().resolve(dto) 模式错误 | `resolver.py:1432` docstring 明确接受 BaseModel 实例;`SprintService` 同模式跑通 | +| 6 | SprintSummary.post_contributor_names lazy=noload 触发 DetachedInstanceError | sprint/dtos.py 本 PR 未改;现有 sprint 测试不创建 task,post_contributor_names 走空集短路 | +| 7 | service 顺序变影响 OpenAPI tag | 无 snapshot 测试 / tag 顺序 pin;OpenAPI 200 实测通过 | + +### 关键代码定位(供未来 review 复用) + +- `skill/template/src/models.py:101-103` — `er` 与 `Resolver` 在 import 时捕获 `async_session` +- `src/nexusx/loader/registry.py:336` — `ErManager._session_factory = session_factory`(按值捕获) +- `src/nexusx/voyager/create_voyager.py:137` — 懒构造 VoyagerContext(无启动期 introspection) +- `src/nexusx/resolver.py:1428-1436` — `resolve()` 接受 BaseModel 实例 + +--- + +## 下一步选项 + +| 选项 | 含义 | 工作量 | +|---|---|---| +| **A**:继续 US2 + US4(剩余 9 任务 T016-T018, T026-T028, T038) | 完成所有 P1/P2/P3 user story | 中 | +| **B**:暂停,分支作为内部预览 | 已 commit,可 push(待用户指令) | — | +| **C**:跑 PR 自检(code-review / security-review) | 准备 merge master 前的自检 | 小 | + +剩余任务依赖: +- US2(T016-T018)独立,可与 T038 并行 +- US4(T026-T028)依赖 US2 完成(phase3.md 重组需要 SKILL 总览稳定) +- T038(FR-012 双向引用)依赖 T016 完成(phase0.md 需先存在)