diff --git a/presets/nexusx/commands/speckit.plan.md b/presets/nexusx/commands/speckit.plan.md new file mode 100644 index 0000000..6ceebfc --- /dev/null +++ b/presets/nexusx/commands/speckit.plan.md @@ -0,0 +1,143 @@ +--- +description: nexusx preset — plan with DB 选型 / alembic / TS SDK 决策访谈. +handoffs: + - label: Create Tasks + agent: speckit.tasks + prompt: Break the plan into nexusx phase-tagged tasks (Schema → Methods → Service → optional SDK) + send: true + - label: Create Checklist + agent: speckit.checklist + prompt: Create a checklist for the following nexusx domain... +scripts: + sh: scripts/bash/setup-plan.sh --json + ps: scripts/powershell/setup-plan.ps1 -Json +--- + +## User Input + +```text +$ARGUMENTS +``` + +You **MUST** consider the user input before proceeding (if not empty). + +## Pre-Execution Checks + +**Check for extension hooks (before planning)**: +- Check `.specify/extensions.yml` for `hooks.before_plan` entries +- Standard handling: parse YAML, skip if invalid, filter by `enabled`, output per `optional` flag (Automatic → `EXECUTE_COMMAND:`; Optional → prompt display) +- If no `before_plan` hooks, skip silently + +## Outline + +1. **Setup**: Run `{SCRIPT}` from repo root and parse JSON for FEATURE_SPEC, IMPL_PLAN, SPECS_DIR, BRANCH. For single quotes in args like "I'm Groot", use escape syntax: `'I'\''m Groot'` (or double-quote if possible: `"I'm Groot"`). + +2. **Load context**: Read FEATURE_SPEC + `/memory/constitution.md`(nexusx preset 版本,含 8 条硬规则 + V 型验收治理)+ IMPL_PLAN template. + +3. **Verify Phase 0 completeness** *(nexusx preset)*: + - 检查 FEATURE_SPEC 中 `## Phase 0 需求确认纪要` 八步全部填写 + - 特别确认 Step 0-4 Service 切分由用户明确选择(不是 `[NEEDS CLARIFICATION]`) + - 特别确认 Step 0-7 DB 选型由用户明确选定 + - 任一缺失 → ERROR:"Phase 0 不完整,请重新运行 `__SPECKIT_COMMAND_SPECIFY__` 补全访谈" + +4. **Execute plan workflow** *(nexusx preset)*: + - Fill `## Technical Context`(nexusx 字段:Language/Version、Primary Dependencies、DB 选型、async/sync DATABASE_URL、Async DB Driver、是否引入 alembic、Testing、Target Platform) + - Fill `## Phase 决策记录`:从 spec 复制 Service 切分最终方案 + 与用户确认是否生成 TS SDK + 第三方库确认清单 + - Fill `## Constitution Check`:逐条对照 8 条原则 + - Evaluate gates(任何 ❌ 或 ⚠️ 必须在 Complexity Tracking 给理由,否则 ERROR) + - Phase 0: Generate `research.md`(解决 Technical Context 中所有 NEEDS CLARIFICATION;研究 alembic 配置、async driver 选型、跨层数据流 ExposeAs/SendTo/Collector 适用性) + - Phase 1: Generate `data-model.md`(实体定义,含 docstring + Field description)、`contracts/`(REST endpoints + GraphQL SDL)、`quickstart.md` + - Phase 1: Update agent context by running the agent script + - Re-evaluate Constitution Check post-design + +## Mandatory Post-Execution Hooks + +**You MUST complete this section before reporting completion to the user.** + +Check `.specify/extensions.yml` for `hooks.after_plan` entries. Same handling as Pre-Execution: +- Mandatory → emit `EXECUTE_COMMAND: {command}` +- Optional → prompt display + +If no `after_plan` hooks or `.specify/extensions.yml` missing, skip to Completion Report. + +## Completion Report + +Report branch, IMPL_PLAN path, generated artifacts (research.md / data-model.md / contracts/ / quickstart.md), and **nexusx Phase 决策摘要**(Service 切分 / DB 选型 / 是否 TS SDK). + +## Phases + +### Phase 0: DB 选型 + alembic + 第三方库 + TS SDK 决策研究 + +1. **从 spec.md 复制决策**(已在 Phase 0 访谈中由用户裁定): + - DB 选型(in-memory / file sqlite / docker pg / docker mysql / external) + - Service 切分最终方案 + - 第三方库候选 + +2. **填充 plan.md Technical Context 的待定字段**: + - async DATABASE_URL:根据 DB 选型填具体值 + - sync DATABASE_URL_SYNC:持久化场景必填(alembic + load_seed 用);in-memory 留空 + - Async DB Driver:sqlite→aiosqlite、pg→asyncpg、mysql→aiomysql + - 是否 alembic:in-memory 否;其他是 + - Testing:pytest + pytest-asyncio(项目级 `tests/`) + +3. **TS SDK 决策访谈**: + + 向用户提问:"本特性是否需要生成 TypeScript SDK?" + - **是** → plan.md 标记为"是";Phase 4 触发,需 `fe/` 子目录 + `@hey-api/openapi-ts`;前提是 Phase 3 用 `create_use_case_router()` + - **否**(默认)→ plan.md 标记为"否",跳过 Phase 4 + + 决策结果写入 plan.md `## Phase 决策记录 > 是否生成 TS SDK` 区块。 + +4. **alembic 配置研究**(仅持久化场景):参考 `presets/nexusx/reference/phase1.md` 的 alembic 配置部分,记录: + - `alembic/env.py` 必须加 `import src.models # noqa: F401`(否则 autogenerate 空) + - `script.py.mako` 必须加 `import sqlmodel`(否则 NameError) + - SQLite 必须 `render_as_batch=True` + - `alembic.ini` `sqlalchemy.url =` 留空,env.py 覆盖 + +5. **Generate research.md** with format: + - Decision: [选了什么] + - Rationale: [为什么] + - Alternatives considered: [还看了什么] + +**Output**: research.md with all NEEDS CLARIFICATION resolved + +### Phase 1: Design & Contracts + +**Prerequisites**: `research.md` complete + +1. **Extract entities from feature spec + spec.md Phase 0 Step 0-1** → `data-model.md`: + - Entity name + docstring + fields (with description) + - Relationships(全部带 `sa_relationship_kwargs={"lazy": "noload"}`) + - Validation rules from requirements + - State transitions if applicable + +2. **Define interface contracts** → `/contracts/`: + - REST endpoints(每个 service 的方法 → 一个 POST 路由,由 `create_use_case_router()` 自动生成) + - GraphQL SDL(基于 SQLModel 实体 + Phase 2 挂载的 methods) + - MCP tools 4 层(list_apps → describe_compose_schema → describe_compose_method → compose_query) + - 跨层数据流标注(如有 ExposeAs / SendTo / Collector 用法) + +3. **Create quickstart validation guide** → `quickstart.md`: + - 端到端可运行验证场景 + - 包含:prerequisites、setup commands、test/run commands、expected outcomes + - 用链接引用 contracts/ 和 data-model.md,不复制内容 + - 不含完整实现代码、迁移文件、完整测试套件 + +4. **Agent context update**: 更新 `__CONTEXT_FILE__` 中 `` 与 `` 之间的 plan 引用 + +**Output**: data-model.md, /contracts/*, quickstart.md, updated agent context + +## Key rules + +- Use absolute paths for filesystem operations; use project-relative paths for references in docs +- ERROR on Constitution gate failures or unresolved clarifications +- nexusx preset 强制:plan.md 的 Technical Context 必须填 nexusx 字段,不能保留 spec-kit 通用字段 + +## Done When + +- [ ] Phase 0 (spec.md) 完整性验证通过 +- [ ] Plan workflow executed:Technical Context 填 nexusx 字段、Phase 决策记录完整、Constitution Check 全 ✅ 或 Complexity Tracking 给理由 +- [ ] research.md 生成且所有 NEEDS CLARIFICATION 解决 +- [ ] data-model.md / contracts/ / quickstart.md 生成 +- [ ] Extension hooks dispatched or skipped per rules above +- [ ] Completion reported with branch / plan path / artifacts / nexusx Phase 决策摘要 diff --git a/presets/nexusx/commands/speckit.specify.md b/presets/nexusx/commands/speckit.specify.md new file mode 100644 index 0000000..a2d4e0a --- /dev/null +++ b/presets/nexusx/commands/speckit.specify.md @@ -0,0 +1,228 @@ +--- +description: nexusx preset — specify with Phase 0 八步访谈 (entities/关系/聚合根/Service切分/DB选型). +handoffs: + - label: Build Technical Plan + agent: speckit.plan + prompt: Create a plan for the spec. I am building with nexusx (SQLModel + FastAPI + MCP). + - label: Clarify Spec Requirements + agent: speckit.clarify + prompt: Clarify specification requirements + send: true +--- + +## User Input + +```text +$ARGUMENTS +``` + +You **MUST** consider the user input before proceeding (if not empty). + +## Pre-Execution Checks + +**Check for extension hooks (before specification)**: +- Check if `.specify/extensions.yml` exists in the project root. +- If it exists, read it and look for entries under the `hooks.before_specify` key +- If the YAML cannot be parsed or is invalid, skip hook checking silently and continue normally +- Filter out hooks where `enabled` is explicitly `false`. Treat hooks without an `enabled` field as enabled by default. +- For each remaining hook, output based on its `optional` flag per the standard spec-kit format (Automatic Pre-Hook emits `EXECUTE_COMMAND:`; Optional Pre-Hook shows the prompt) +- If no hooks are registered, skip silently + +## Outline + +The text after `__SPECKIT_COMMAND_SPECIFY__` in the triggering message **is** the feature description. + +### 1. Short-name & directory setup + +1. **Generate a concise short name** (2-4 words) for the feature (action-noun format when possible; preserve technical acronyms like OAuth2/JWT) +2. **Branch creation** (optional, via `before_specify` hook): if a hook ran successfully it will have output JSON with `BRANCH_NAME` and `FEATURE_NUM`. Note these for reference +3. **Create the spec feature directory** under `specs/`: + - Resolution order: user-provided `SPECIFY_FEATURE_DIRECTORY` > auto-generated `specs/-` (check `.specify/init-options.json` for `feature_numbering`: `timestamp` → `YYYYMMDD-HHMMSS-`; `sequential` (default) → next 3-digit number) + - `mkdir -p SPECIFY_FEATURE_DIRECTORY` + - Resolve active `spec-template` through the preset stack (equivalent to `specify preset resolve spec-template` — nexusx preset provides this) + - Copy resolved template to `SPECIFY_FEATURE_DIRECTORY/spec.md` + - Set `SPEC_FILE` to `SPECIFY_FEATURE_DIRECTORY/spec.md` + - Persist path to `.specify/feature.json`: `{ "feature_directory": "" }` + - **One feature per invocation**; spec dir name and git branch name are independent + +### 2. Load context + +4. Load the resolved `spec-template` (nexusx preset version) to understand required sections +5. **IF EXISTS**: Load `/memory/constitution.md` for project principles (nexusx preset constitution defines 8 hard rules) + +### 3. Phase 0 八步访谈 *(nexusx preset — interactive)* + + + +按 `spec.md` 模板的 `## Phase 0 需求确认纪要` 区块逐项与用户访谈。**每步等待用户回复后才继续**。 + +#### Step 0-1 术语与实体定义 + +从 feature description 提取候选取实体,**表格化呈现**给用户逐行确认: + +| 实体 | 业务含义(一句话) | 核心字段(名称+类型+语义) | 字段约束 | +|------|----------|----------|----------| +| [候选 1] | ... | ... | ... | + +询问:补充、修改、删除哪些? + +#### Step 0-2 实体关系 + +基于 Step 0-1 实体,绘制文本 ER 图: + +``` +User ──1:N──→ Post +Post ──N:M──→ Tag (中间表: PostTag) +``` + +每条关系标方向(1:N / N:1 / M:N)+ 业务含义 + 是否需要中间实体。**与用户确认关系方向和基数**。 + +#### Step 0-3 聚合根 + 根类型 + +明确每个聚合根是 **SQLModel 实体**还是 **虚拟实体**(普通 `pydantic.BaseModel`): + +| 聚合根 | 类型 | 选用理由 | +|--------|------|----------| +| [候选] | SQLModel 实体 / 虚拟实体 | [判断依据] | + +**判断依据**:根字段全部来自 DB → SQLModel;来自请求上下文(JWT、headers)或聚合多源 → 虚拟。 + +#### Step 0-4 Service 切分候选方案 ⚠️ 强制门 + +**禁止模型自行决定**。必须向用户提出**至少一种候选方案**: + +``` +方案 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 强耦合被拆开 + +方案 C:混合 + ... +``` + +**强制门**:如果未向用户提出候选方案并取得明确选择(A / B / C / 用户的修正),**立即标记 `SPEC_PHASE0_BLOCKED`** 并停在此处输出: + +``` +⚠️ SPEC_PHASE0_BLOCKED +等待用户从 A/B/C 选择 Service 切分方案,或提出修正。 +``` + +收到用户回复后才继续 Step 0-5。 + +#### Step 0-5 GraphQL 定位 + +告知用户:GraphQL 在 nexusx 中是**辅助开发测试**接口(不是正式 API)。业务方法定义在 `service//methods.py`,挂载到 Entity(GraphQL 辅助)+ UseCaseService(REST/MCP 正式)。无需用户确认。 + +#### Step 0-6 第三方库确认 + +列出 feature 涉及的非业务功能领域(认证、实时推送、文件存储、数据迁移等)。对每个: +- 候选方案(成熟第三方库 vs 手写) +- 推荐理由 +- **必须调查用户提到的库的当前维护状态**(避免停更库) + +| 功能领域 | 推荐方案 | 维护状态 | 备注 | +|----------|----------|----------|------| +| 认证 | ... | 活跃/维护中/已停更 | ... | + +#### Step 0-7 DB 选型 + 迁移策略 ⚠️ 强制门 + +向用户呈现 5 选项决策表: + +| 选项 | async URL | 持久化 | Alembic | 适用 | +|------|----------|--------|---------|------| +| in-memory sqlite | `sqlite+aiosqlite://` | ❌ | ❌ | Demo | +| file sqlite | `sqlite+aiosqlite:///./var/X.db` | ✅ 文件 | ✅ | 本地开发 | +| docker pg | `postgresql+asyncpg://...` | ✅ 卷 | ✅ | 团队 | +| docker mysql | `mysql+aiomysql://...` | ✅ 卷 | ✅ | MySQL 偏好 | +| external | 视驱动 | ✅ | ✅ | 已有 DB | + +**强制门**:用户未明确选定(含 async URL + sync URL + 是否 alembic + 是否 docker-compose)前,标记 `SPEC_PHASE0_BLOCKED` 等待。 + +#### Step 0-8 检查清单汇总 + +向用户展示 8 项 checklist 全部 ✅ 后才进入下一步: + +- [ ] 实体和字段完整,约束清晰 +- [ ] 实体关系方向和基数正确 +- [ ] 聚合根明确,每个类型已确认 +- [ ] **Service 切分由用户明确选择**(IV) +- [ ] 核心用例覆盖,逻辑自洽 +- [ ] 第三方库选型确认 +- [ ] **DB 选型 + 迁移策略明确**(影响 Phase 1 alembic) +- [ ] 无遗漏或未讨论的边界 + +### 4. Fill spec.md using Phase 0 answers + +6. Write the specification to `SPEC_FILE`: + - 用 Phase 0 八步访谈的答案填充 `## Phase 0 需求确认纪要` 区块 + - 用 spec-kit core 流程填充 `## User Scenarios & Testing` / `## Requirements` / `## Success Criteria` / `## Assumptions` + - 解析用户描述中的 actors / actions / data / constraints + - 不明确的标记 `[NEEDS CLARIFICATION: specific question]`,**全局最多 3 个** + - 优先级:scope > security/privacy > UX > technical details + - 合理默认:标准 web/mobile 性能、用户友好错误、标准认证模式、项目合适的集成模式 + +### 5. Specification Quality Validation + +7. 创建 `SPECIFY_FEATURE_DIRECTORY/checklists/requirements.md` 验证 checklist,包含: + - **Content Quality**: 无实现细节、聚焦用户价值、面向非技术干系人 + - **Requirement Completeness**: 无残留 `[NEEDS CLARIFICATION]`、需求可测、success criteria 可衡量且与实现无关、边界用例识别 + - **Feature Readiness**: 所有 FR 有清晰验收、用户场景覆盖主流程 + +8. 验证流程: + - **全部通过** → 标记完成,进入 Mandatory Post-Execution Hooks + - **非 NEEDS CLARIFICATION 失败** → 列出问题、更新 spec、重验证(最多 3 轮) + - **残留 NEEDS CLARIFICATION** → 提取每个标记(最多 3 个),用表格向用户提问(Q1/Q2/Q3 同时呈现),等用户回复后替换标记,重新验证 + +## Mandatory Post-Execution Hooks + +**You MUST complete this section before reporting completion to the user.** + +Check `.specify/extensions.yml` for `hooks.after_specify` entries. Apply same logic as Pre-Execution Checks: +- Mandatory hook → emit `EXECUTE_COMMAND: {command}` +- Optional hook → show prompt and command path + +If no `after_specify` hooks or `.specify/extensions.yml` missing, skip to Completion Report. + +## Completion Report + +Report to user: +- `SPECIFY_FEATURE_DIRECTORY` — feature 目录路径 +- `SPEC_FILE` — spec 文件路径 +- Phase 0 八步访谈全部完成(特别提示 Service 切分 + DB 选型已由用户裁定) +- Checklist 验证结果摘要 +- Readiness for next phase (`__SPECKIT_COMMAND_CLARIFY__` 或 `__SPECKIT_COMMAND_PLAN__`) + +**NOTE**: Branch creation 由 `before_specify` hook 处理;spec 目录和文件创建由本命令处理。 + +## Quick Guidelines + +- **Phase 0 访谈阶段**:聚焦业务实体、关系、聚合根、Service 边界、DB 选型——这些决定后续 phase 走向。可以包含 nexusx 技术栈名(SQLModel / FastAPI / MCP),因为本项目就用 nexusx +- **User Scenarios / Requirements / Success Criteria 阶段**:聚焦 WHAT 和 WHY,避免 HOW(具体代码结构、字段名) +- **NEEDS CLARIFICATION** 上限 3 个,仅在多重合理解读且影响范围时使用 + +### Section Requirements + +- **Phase 0 需求确认纪要**:nexusx preset 必填,未完成禁止进入 plan +- **User Scenarios**:必填,每个故事可独立测试 +- **Requirements / Success Criteria / Assumptions**:必填 +- 不适用的 section 整段删除,不留 "N/A" + +## Done When + +- [ ] Phase 0 八步访谈完成(Service 切分 + DB 选型由用户明确裁定) +- [ ] Specification written to `SPEC_FILE`,包含 nexusx Phase 0 区块 + spec-kit core 区块 +- [ ] Validated against quality checklist +- [ ] Extension hooks dispatched or skipped per rules above +- [ ] Completion reported with feature dir / spec path / Phase 0 interview summary diff --git a/presets/nexusx/commands/speckit.tasks.md b/presets/nexusx/commands/speckit.tasks.md new file mode 100644 index 0000000..69e3de0 --- /dev/null +++ b/presets/nexusx/commands/speckit.tasks.md @@ -0,0 +1,198 @@ +--- +description: nexusx preset — tasks with phase 标签 (Schema/Methods/Service/SDK) + reference linking. +handoffs: + - label: Analyze For Consistency + agent: speckit.analyze + prompt: Run a project analysis for consistency + send: true + - label: Implement Project + agent: speckit.implement + prompt: Start the implementation in nexusx phases + send: true +scripts: + sh: scripts/bash/setup-tasks.sh --json + ps: scripts/powershell/setup-tasks.ps1 -Json +--- + +## User Input + +```text +$ARGUMENTS +``` + +You **MUST** consider the user input before proceeding (if not empty). + +## Pre-Execution Checks + +**Check for extension hooks (before tasks generation)**: +- Check `.specify/extensions.yml` for `hooks.before_tasks` entries +- Standard handling: parse YAML, skip if invalid, filter by `enabled`, output per `optional` flag (Automatic → `EXECUTE_COMMAND:`; Optional → prompt display) +- If no `before_tasks` hooks, skip silently + +## Outline + +1. **Setup**: Run `{SCRIPT}` from repo root and parse FEATURE_DIR, TASKS_TEMPLATE, AVAILABLE_DOCS. For single quotes in args use escape syntax: `'I'\''m Groot'` (or `"I'm Groot"`). + +2. **Load design documents** from FEATURE_DIR: + - **Required**: plan.md (nexusx Technical Context + Phase 决策记录 + Constitution Check), spec.md (Phase 0 需求确认纪要 + User Stories) + - **Optional**: data-model.md (entities), contracts/ (interface contracts), research.md (decisions), quickstart.md + - **IF EXISTS**: `/memory/constitution.md` (nexusx 8 硬规则) + - 注:不是所有项目都有所有文档;基于实际可用的生成任务 + +3. **Read nexusx-specific decisions** *(nexusx preset)*: + - 从 plan.md `## Phase 决策记录 > Service 切分最终方案` 读 domain 列表(决定 Phase 2/3 的 service 数量) + - 从 plan.md `## Phase 决策记录 > 是否生成 TS SDK` 读布尔值(决定是否生成 Phase 4) + - 从 spec.md `## Phase 0 需求确认纪要 > Step 0-7 DB 选型` 读是否 alembic(决定 Phase 1 是否包含 alembic setup 任务) + +4. **Execute task generation workflow**: + - Load plan.md 提取 tech stack / libraries / structure + - Load spec.md 提取 user stories 与优先级(P1, P2, P3...) + - Load data-model.md 提取实体并映射到 user stories(跨故事共享实体放 Phase 1 顶部多标签) + - Load contracts/ 映射接口契约到 user stories + - Load research.md 提取决策用于 setup 任务 + - **按 nexusx phase 组织任务**(不是按 user story),见下方规则 + - 生成依赖图(phase 顺序 + phase 内部并行) + - 验证完整性(每个 user story 在所需 phase 都有任务) + +5. **Generate tasks.md**: Read nexusx tasks-template from TASKS_TEMPLATE (or fall back to `.specify/presets/nexusx/templates/tasks-template.md`). Fill with: + - 正确的 feature name(来自 plan.md) + - **Phase 1 Schema**(含 alembic setup 任务,仅当 DB 选型为持久化场景) + - **Phase 2 Methods**(每个 domain 一组 methods.py + mount_method) + - **Phase 3 Service**(每个 domain 一组 dtos.py + service.py + spec.md + main.py 集成) + - **Phase 4 TS SDK**(仅当 plan.md "是否生成 TS SDK" 为"是",否则整段删除) + - **Polish & Cross-Cutting Concerns** + - 所有任务必须遵循 nexusx checklist 格式(见下) + - 每个 phase 顶部链接到 `presets/nexusx/reference/phaseN.md` + - Dependencies section(phase 顺序 + 用户故事跨 phase 追踪) + +## Mandatory Post-Execution Hooks + +**You MUST complete this section before reporting completion to the user.** + +Check `.specify/extensions.yml` for `hooks.after_tasks` entries. Same handling as Pre-Execution: +- Mandatory → emit `EXECUTE_COMMAND: {command}` +- Optional → prompt display + +If no `after_tasks` hooks or `.specify/extensions.yml` missing, skip to Completion Report. + +## Completion Report + +Output path to generated tasks.md and summary: +- Total task count +- Task count per nexusx phase(Phase 1 / 2 / 3 / 4) +- Task count per user story +- Parallel opportunities identified +- Independent test criteria for each story +- Suggested MVP scope(Phase 1 + Phase 2 + Phase 3 of US1) +- Format validation: 所有任务遵循 checklist 格式 + phase 标签 + +Context for task generation: {ARGS} + +The tasks.md should be immediately executable — each task must be specific enough that an LLM can complete it without additional context. + +## Task Generation Rules *(nexusx preset)* + +**CRITICAL**: Tasks MUST be organized by **nexusx phase**(不是 spec-kit 默认的 user-story-first)。每个 task 必须带 `[P1]/[P2]/[P3]/[P4]` phase 标签。 + +**Tests are OPTIONAL**: 仅当 spec.md 明确要求或用户要求 TDD 才生成测试任务。 + +### Checklist Format (REQUIRED) + +Every task MUST strictly follow this format: + +```text +- [ ] [TaskID] [P?] [P] [Story?] Description with file path +``` + +**Format Components**: + +1. **Checkbox**: 总是以 `- [ ]` 开头 +2. **Task ID**: 顺序号 T001, T002, T003... 全局递增(不重置 per phase) +3. **[P] marker**: 仅当可并行(不同文件、无依赖)时包含 +4. **[P1] / [P2] / [P3] / [P4]**: nexusx phase 标签(**必填**) +5. **[USx] label**: 多故事共享任务可标多个 `[US1] [US2]`;单故事任务标一个;非故事任务(如 alembic setup)不标 +6. **Description**: 清晰动作 + 具体文件路径 + +**Examples**: + +- ✅ CORRECT: `- [ ] T001 [P1] Create src/db.py with engine + async session factory` +- ✅ CORRECT: `- [ ] T002 [P1] [US1] [US2] Create shared User entity in src/models.py` +- ✅ CORRECT: `- [ ] T020 [P2] [US1] Implement create_user in src/service/auth/methods.py` +- ❌ WRONG: `- [ ] T001 Create project structure`(缺 phase 标签) +- ❌ WRONG: `- [ ] [US1] Create User model`(缺 ID 和 phase) +- ❌ WRONG: `- [ ] T020 Create service`(缺 phase 标签 + 文件路径) + +### Task Organization + +1. **Phase 1 Schema** *(reference: `presets/nexusx/reference/phase1.md`)*: + - `src/db.py`(engine + session factory) + - `src/models.py`(纯实体,无方法,所有 Relationship 加 `lazy=noload`,每个 Model 加 docstring,每个 Field 加 description) + - `src/database.py`(init_db 策略) + - `src/main.py`(FastAPI lifespan + Voyager ER) + - mock seed data(持久化场景:`var/seed_data.json` + `scripts/load_seed.py`) + - **持久化场景额外**:alembic init、env.py、script.py.mako、baseline migration + +2. **Phase 2 Methods** *(reference: `presets/nexusx/reference/phase2.md`)*: + - `src/service//methods.py`(每个 domain 一份,普通 async def) + - `src/models.py` 末尾的 `mount_method()` 函数(`_mount()` 桥接 + `@functools.wraps`) + - `src/main.py` 在 `GraphQLHandler` 之前调用 `mount_method()` + - `tests/test__methods.py`(项目级 tests/,monkey-patch async_session) + +3. **Phase 3 Service** *(reference: `presets/nexusx/reference/phase3.md`)*: + - `src/service//dtos.py`(DefineSubset,禁 `future annotations`,字段用 DTO 类型) + - `src/service//service.py`(UseCaseService,复用 methods.py,所有方法声明返回类型注解,service.py 不直接操作 DB) + - `src/service//spec.md` + - `src/main.py` 集成:`UseCaseAppConfig` + `create_use_case_router()` + `create_use_case_graphql_mcp_server()` + MCP http_app lifespan 合并 + `create_use_case_voyager()` 补 services + +4. **Phase 4 TS SDK** *(conditional — 仅当 plan.md "是否生成 TS SDK" 为 "是")*: + - `fe/openapi-ts.config.ts` + - `fe/package.json` + - 生成 + 验证 + - **如 plan.md 标"否"**,整段 Phase 4 删除,不保留空骨架 + +5. **Polish & Cross-Cutting Concerns**: + - 文档更新(每个 service spec.md) + - 性能优化 + - Security hardening + - `quickstart.md` 端到端验证 + +### Phase 标签使用规则 + +| Task 类型 | Phase 标签 | 故事标签 | +|------|------|------| +| Schema 实体(共享) | `[P1]` | 多个 `[US1] [US2]` | +| Schema 实体(单故事) | `[P1]` | 单个 `[USx]` | +| Schema 基础设施(db.py / main.py) | `[P1]` | 不标 | +| alembic setup(仅持久化) | `[P1]` | 不标 | +| Methods.py(按 domain) | `[P2]` | `[USx]` | +| mount_method 桥接 | `[P2]` | 不标(所有故事共用) | +| DTO + Service(按 domain) | `[P3]` | `[USx]` | +| main.py router 集成 | `[P3]` | 不标 | +| TS SDK | `[P4]` | 不标 | +| Polish | 不标 | 不标 | + +### Conditional Phase 4 Emission Logic + +``` +if plan.md "## Phase 决策记录 > 是否生成 TS SDK" == "是": + emit Phase 4 tasks +else: + omit Phase 4 entirely +``` + +## Phase Structure + +- **Phase 1 Schema**: 项目初始化 + 实体定义 + DB engine + Voyager ER;完成后**必须暂停**等用户 V 升确认 +- **Phase 2 Methods**: 业务方法实现 + 挂载;完成后**必须暂停** +- **Phase 3 Service**: DTO + UseCaseService + REST + MCP + Voyager services;完成后**必须暂停** +- **Phase 4 TS SDK** *(可选)*: SDK 生成 +- **Polish**: 跨 phase 改进 + +## Done When + +- [ ] tasks.md generated with nexusx phase structure(Schema / Methods / Service / 可选 SDK / Polish) +- [ ] 所有任务带 `[P1]/[P2]/[P3]/[P4]` phase 标签 + 具体文件路径 +- [ ] Phase 4 仅在 plan.md 决策为"是"时存在 +- [ ] 每个 phase 顶部链接到 `presets/nexusx/reference/phaseN.md` +- [ ] Extension hooks dispatched or skipped per rules above +- [ ] Completion reported with task count per phase + per story + MVP scope diff --git a/presets/nexusx/preset.yml b/presets/nexusx/preset.yml new file mode 100644 index 0000000..18a0ffc --- /dev/null +++ b/presets/nexusx/preset.yml @@ -0,0 +1,64 @@ +schema_version: "1.0" + +preset: + id: "nexusx" + name: "nexusx 4-Phase Workflow" + version: "0.1.0" + description: "nexusx 4-phase methodology (Schema → Methods → Service → optional SDK) layered on spec-kit SDD" + author: "tangkikodo" + repository: "https://github.com/KLR-Pattern/nexusx" + license: "MIT" + +requires: + speckit_version: ">=0.6.0" + +provides: + templates: + - type: "template" + name: "constitution-template" + file: "templates/constitution-template.md" + description: "nexusx 8 条硬规则 + V 型验收治理" + replaces: "constitution-template" + + - type: "template" + name: "spec-template" + file: "templates/spec-template.md" + description: "spec-kit 用户故事结构 + nexusx Phase 0 需求确认纪要" + replaces: "spec-template" + + - type: "template" + name: "plan-template" + file: "templates/plan-template.md" + description: "nexusx Technical Context(DB 选型 / service 切分 / TS SDK 决策)" + replaces: "plan-template" + + - type: "template" + name: "tasks-template" + file: "templates/tasks-template.md" + description: "phase-first 混合:Schema → Methods → Service → 可选 SDK" + replaces: "tasks-template" + + - type: "command" + name: "speckit.specify" + file: "commands/speckit.specify.md" + description: "Specify + Phase 0 八步访谈,含 Service 切分强制门" + replaces: "speckit.specify" + + - type: "command" + name: "speckit.plan" + file: "commands/speckit.plan.md" + description: "Plan + DB 选型 / alembic / TS SDK 决策访谈" + replaces: "speckit.plan" + + - type: "command" + name: "speckit.tasks" + file: "commands/speckit.tasks.md" + description: "Tasks + phase 标签 + reference doc 链接 + Phase 4 条件生成" + replaces: "speckit.tasks" + +tags: + - "nexusx" + - "sqlmodel" + - "fastapi" + - "mcp" + - "4phase" diff --git a/presets/nexusx/reference/phase1.md b/presets/nexusx/reference/phase1.md new file mode 100644 index 0000000..accd662 --- /dev/null +++ b/presets/nexusx/reference/phase1.md @@ -0,0 +1,78 @@ + + +# Phase 1: Schema + ER Diagram + mock seed + +**目标**: 定义纯实体模型(字段 + 关系声明)、mock seed data,用 ER diagram 可视化供团队讨论。**不含任何业务方法**。 + +**新增/修改文件**: +- `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"}` +- `database.py` — 启动 hook(FastAPI lifespan 调用)。**实现策略由 Step 0-7 决定**: + - in-memory 场景:`init_db()` 做 `SQLModel.metadata.create_all` + mock seed(讨论样本数据) + - 持久化场景(file sqlite / docker / external):`init_db()` 改为 no-op(保留函数签名,`main.py` lifespan 和 tests/conftest.py 都 import 它),schema 和数据完全由 alembic + `scripts/load_seed.py` 管 +- `main.py` — FastAPI + Voyager(ER diagram 可视化) + +**关键模式**: +- SQLModel 实体 + Relationship 声明关系方向,**不包含任何 @query/@mutation 方法** +- 每个 Model 必须有 docstring 说明业务含义,每个 Field 必须有 `description` 说明字段语义 +- mock seed data 用于讨论数据样本是否合理(数量、关联关系、边界值)。持久化场景下 seed 数据写到 `var/seed_data.json`,由 `scripts/load_seed.py` 灌入 +- Voyager 通过 `create_use_case_voyager(services=[], er_manager=er)` 展示 ER diagram +- Phase 1 无 GraphiQL(无方法可查询),GraphQL 在 Phase 2 方法挂载后可用 +- **如果 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` 中记录以下验收标准: + +| # | 验收项 | 验证方式 | +|---|--------|----------| +| 1 | 每个 Entity 在 Voyager ER 图中正确显示,关系线方向正确 | 浏览器打开 Voyager | +| 2 | `models.py` 中每个 Entity 只包含字段 + Relationship,无任何业务方法 | 检查代码结构 | +| 3 | mock seed 数据样本展示合理的数量、关联关系和边界值 | 编写简单查询验证记录数 | +| 4 | (持久化场景)alembic baseline 迁移生成并 upgrade 成功,DB 中表结构与 models 一致 | `alembic upgrade head` + 查 `alembic_version` 表 | + +**实现:** +编写 `db.py` → `models.py`(纯实体,无方法) → `database.py` → `main.py` + +**如果 Step 0-7 选了持久化 DB(file sqlite / docker / external),还需要:** + +1. **`pyproject.toml` 加 `alembic>=1.13` 依赖**,按 DB 类型加 async driver(postgresql → `asyncpg`;mysql → `aiomysql`) +2. **`alembic init alembic`** +3. **改 `alembic/env.py`**: + - 顶部加 `import os`、`from sqlmodel import SQLModel`、`import src.models # noqa: F401`(**关键:不导入则 SQLModel.metadata 为空,autogenerate 会生成空迁移**) + - `target_metadata = SQLModel.metadata` + - URL 从 env var 读,与 app 用同一文件但走 **sync 驱动**(alembic 默认 sync 连接): + ```python + sync_url = os.getenv("DATABASE_URL_SYNC", "sqlite:///./var/.db") + config.set_main_option("sqlalchemy.url", sync_url) + ``` + - SQLite 场景在 offline / online 两个 `context.configure(...)` 都加 `render_as_batch=True`(SQLite 不支持大多数 ALTER,必须 batch) +4. **改 `alembic/script.py.mako`** 加 `import sqlmodel`(SQLModel 的 `AutoString` 类型在生成的迁移里会被引用,缺这个 import 会 NameError) +5. **`alembic.ini` 的 `sqlalchemy.url =` 留空**,env.py 覆盖 +6. **`.gitignore` 加 `var/`**(file sqlite 场景) +7. **`alembic revision --autogenerate -m "init schema"`** → 打开生成的迁移文件检查表结构正确(特别是自引用 FK)→ **`alembic upgrade head`** +8. **(可选)`scripts/load_seed.py`**:一次性把 mock seed 数据灌入文件 DB,保留 ID 和时间戳 + +**踩坑预警:** +- ❌ `alembic revision --autogenerate` 给出空 `upgrade()`:`env.py` 漏了 `import src.models` +- ❌ `alembic upgrade` 报 `NameError: name 'sqlmodel' is not defined`:`script.py.mako` 漏了 `import sqlmodel` +- ❌ uvicorn `--reload` 模式下,改 `db.py` URL 后会立即 reload,老的 `init_db()` 可能跑了一次 create_all 把表建到新文件里 → 后续 autogenerate 看到表已存在生成空迁移。**解决**:先 dump 数据 → 删 DB 文件 → 改代码 → autogenerate → upgrade → load_seed + +**V 升 — 逐条回查验收:** +按验收标准逐条验证,用户确认后才写入 `spec/phase1.md`: + +- [ ] 1. Voyager ER 图:实体节点、关系线、聚合根高亮 +- [ ] 2. Entity 纯字段:无 @query/@mutation 方法,无 `nexusx` 导入 +- [ ] 3. mock seed:数据量合理、关联关系正确、包含边界用例 +- [ ] 4. (持久化场景)alembic baseline 已 upgrade,`alembic_version` 表记录了 revision id + +## 踩坑经验 + +1. **engine/session 必须独立为 `db.py`** — `models.py` 需要 `async_session`,`database.py` 需要 models,放在同一文件会导致循环导入。`db.py` 只放 engine + session_factory,不导入任何 model +2. **`pyproject.toml` 必须配置 `packages = ["src"]`** — hatchling 默认按项目名找目录,`src/` 布局需要显式指定 `[tool.hatch.build.targets.wheel]` +3. **目录命名不能以数字开头** — Python 模块名限制 +4. **每个 Model 必须有 docstring,每个 Field 必须有 description** — Phase 1 就要确保语义清晰,description 会传递到 OpenAPI spec +5. **所有 Relationship 加 `sa_relationship_kwargs={"lazy": "noload"}`** — 项目通过显式查询 + Resolver DataLoader 加载关系数据,不依赖 ORM lazy-load。`noload` 使 relationship 属性直接返回默认值(`None`/`[]`),避免 session 关闭后 `model_validate(entity)` 访问 relationship descriptor 触发 DetachedInstanceError +6. **in-memory SQLite 是进程级的,跨进程连不上** — uvicorn 跑起来后内存数据只活在那一个进程里,重启即丢。如果中途想从 in-memory 切到 file sqlite,必须**先通过 MCP/HTTP 接口把当前数据 dump 出来**(写 `var/seed_data.json`),再改 `db.py` URL 和引入 alembic,最后用 `scripts/load_seed.py` 灌回。直接连内存库 dump 是不可能的 +7. **uvicorn `--reload` 模式下编辑代码会立即重启** — 在做 DB 迁移这类需要"先 dump 再改代码"的操作时,必须 Phase A(dump)和 Phase B+(改代码)严格分开:dump 落盘确认之前不要碰任何 `*.py`/`*.toml`/`*.ini` 文件,否则进程一重启数据就没了 +8. **拆分 `models/` 多文件时,不要把关系注解写成 `from __future__ import annotations` + `list["OtherModel"]` / `list[OtherModel]`** — 我已做最小复现验证:这种写法在 `SQLModel.metadata.create_all()` 阶段可能不报错,但一旦实例化或触发 mapper 配置,SQLAlchemy 会把关系目标解析成字符串 `"list[Hero]"`,随后抛 `InvalidRequestError`。稳妥写法是:**去掉 `from __future__ import annotations`,显式 `from typing import List, Optional`,关系统一写成 `children: List["Child"]`、`parent: Optional["Parent"]`**。另外在 `models/__init__.py` 中集中导入所有 model,确保模块扫描和注册顺序稳定 +9. **拆分 `models/` 后必须有聚合导入出口** — 推荐 `src/models/__init__.py` 显式 `from .user import User`、`from .post import Post` 这类导入,并提供 `__all__`。原因不是给类型检查器看,而是确保:① app 启动时所有 SQLModel 子类都注册进 `SQLModel.metadata`;② 字符串关系名(如 `"User"`)能在 mapper 初始化前进入类注册表;③ 依赖自动发现/扫描的工具(ER、Resolver、Voyager、migration import side effect)拿到完整模型集。若只 import 单个子模块,另一个模块里的关系目标可能尚未注册,后续在第一次实例化实体时才炸 diff --git a/presets/nexusx/reference/phase2.md b/presets/nexusx/reference/phase2.md new file mode 100644 index 0000000..177f55b --- /dev/null +++ b/presets/nexusx/reference/phase2.md @@ -0,0 +1,76 @@ + + +# Phase 2: 方法实现 + Entity 挂载 + +**目标**: 按业务域实现独立方法,挂载到 Entity 的 @query/@mutation,GraphQL 可查询。 + +**新增/修改文件**: +- `service//methods.py` — 独立业务方法实现(核心逻辑,不含 @query/@mutation 装饰器) +- `models.py` — 新增 `mount_method()` 函数,延迟 import methods 并通过 `_mount()` 挂载到 Entity + +**关键模式**: +- 业务方法在 `service//methods.py` 中定义,为普通 async 函数(不含 `cls` 参数,非 classmethod) +- `models.py` 通过 `mount_method()` 函数挂载(桥接 classmethod 协议)。函数体内做延迟 import 避免循环依赖,`main.py` 中显式调用: + ```python + # models.py 底部 + def mount_method(): + """挂载 service methods 到 entity classes。需在外部显式调用。""" + import functools + from nexusx import mutation, query + from src.service.user.methods import list_users, create_user + + def _mount(entity, fn, decorator): + @functools.wraps(fn) + async def wrapper(cls, *args, **kwargs): + return await fn(*args, **kwargs) + setattr(entity, fn.__name__, decorator(wrapper)) + + _mount(User, list_users, query) + _mount(User, create_user, mutation) + ``` + ```python + # main.py(在 graphql_handler 创建之前调用) + from src.models import mount_method + mount_method() + graphql_handler = GraphQLHandler(base=BaseEntity, session_factory=async_session) + ``` +- `_mount()` 用 `fn.__name__` 作为挂载属性名,`@functools.wraps(fn)` 保留 docstring 确保 GraphQL SDL 正确生成描述 +- GraphQL 作为辅助测试接口,`@query`/`@mutation` 装饰器在挂载时应用 +- `mount_method()` 定义放在 Entity class 之后、ErManager 之前 + +**V 降 — 定义验收标准:** +进入 Phase 2 编码之前,先与用户确认测试验收集并写入 `spec/phase2.md`: + +| # | 方法 | 测试场景 | 预期结果 | 验证方式 | +|---|------|----------|----------|----------| +| 1 | create_user | 正常创建 | 返回新用户对象,含关联关系 | GraphiQL mutation | +| 2 | create_user | 重复邮箱 | 返回错误提示 | GraphiQL mutation | +| 3 | list_users | 分页查询 | 返回正确分页数据 | GraphiQL query | +| ... | ... | ... | ... | ... | + +验收标准要求: +- 每个 `@query`/`@mutation` 至少覆盖:**一个正常场景 + 一个边界/异常场景** +- 异常场景的预期结果必须是可观察的(不写"不出错",写"返回 status: error, message: xx") +- 验证方式统一通过 GraphQL query/mutation 在 GraphiQL 中执行 + +**实现:** +编写 `service//methods.py` → `models.py` 挂载 + +**V 升 — 逐条回查验收:** +启动服务,在 GraphiQL 中逐一执行验收表,同时运行自动化测试: + +- [ ] 1. create_user(正常)→ 返回新用户数据,关系字段正确 +- [ ] 2. create_user(重复)→ 返回预期错误信息 +- [ ] 3. list_users(分页)→ 分页参数生效 +- [ ] ...(每条对标验收表) +- [ ] 确认 seed 数据仍可查询,Loader 行为符合预期 +- [ ] 自动化测试全部通过:`pytest tests/` + +## 踩坑经验 + +1. **methods.py 函数需通过 `_mount()` 桥接 classmethod 协议** — `query()`/`mutation()` 返回 `classmethod`,会自动注入 `cls` 参数。methods.py 中的独立函数不含 `cls`,直接用 `query(fn)` 挂载到 Entity 后调用会 TypeError。使用 `_mount()` 辅助函数包装一层 `async def wrapper(cls, *args, **kwargs): return await fn(*args, **kwargs)` 来桥接。`@functools.wraps(fn)` 保留 docstring,确保 GraphQL SDL 正确生成描述 +2. **`GraphQLHandler` 必须在 `mount_method()` 之后创建** — `GraphQLHandler` 在初始化时扫描 BaseEntity 子类的 `@query`/`@mutation` 方法构建 schema。如果先创建 handler 再挂载方法,GraphQL schema 会为空。`main.py` 中必须先调用 `mount_method()` 再创建 `graphql_handler` +3. **`mount_method()` 定义在 `models.py` 中,`main.py` 显式调用** — 挂载逻辑和 entity 定义放在一起,减少文件跳转。函数体内做延迟 import(`from src.service.xxx.methods import ...`)避免循环依赖。`main.py` 中 `from src.models import mount_method` + `mount_method()` 显式调用,比 import 副作用更清晰 +4. **列表关系需要 order_by** — 分页功能要求 `sa_relationship_kwargs={"order_by": "Entity.column"}` +5. **测试需 monkey-patch 每个 methods 模块的 `async_session`** — methods.py 执行 `from src.db import async_session` 时已绑定原始值,运行时 patch `src.db.async_session` 不会影响已导入的局部绑定。必须同时 patch `src.db` 和每个 methods 模块:`monkeypatch.setattr(mod, "async_session", test_factory)` +6. **测试放在项目级 `tests/` 目录** — 不放在 `service/*/` 子目录,避免循环导入(tests 导入 src.models,而 models.py 底部导入 service methods)。每个业务域一个 `test__methods.py` 文件 diff --git a/presets/nexusx/reference/phase3.md b/presets/nexusx/reference/phase3.md new file mode 100644 index 0000000..c77f813 --- /dev/null +++ b/presets/nexusx/reference/phase3.md @@ -0,0 +1,165 @@ + + +# Phase 3: UseCase 响应组装 + MCP + +**目标**: 按 API 用例组装响应结构。DefineSubset 隐藏内部字段,UseCaseService 统一业务入口。 + +**新增/修改文件**: +- `service//spec.md` — 服务目的、用途、需求、变更记录 +- `service//dtos.py` — DefineSubset DTOs +- `service//service.py` — UseCaseService +- `main.py` — 用 `create_use_case_router()` 挂载 REST + MCP + Voyager 补充 services + +**关键模式**: +- `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`) +- `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)` — 子层结果聚合回父层 +- `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 复用 `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)` + - **mutation 方法**:同 get 单条模式,调用 methods.py 获取 Model 后转换 + - **service.py 不直接操作数据库**(无 `async_session`)。如需直接查询构建 DTO,使用 `build_dto_select(entity, dto_type)` 生成 SQL SELECT 并通过 `dict(row._mapping)` 构造 DTO +- **`build_dto_select(Entity, DtoType)` 查询构建** — 根据 DTO 的 `__subset__` 字段自动生成 SQL SELECT 语句,只查询 DTO 需要的列,避免 `SELECT *` +- **`create_use_case_router()` 自动生成 REST 路由** — 从 UseCaseAppConfig 生成 POST 路由,自动提取 `response_model`、构建 request body model、注册路由。不需要手写 `router/` 目录 + ```python + from nexusx import UseCaseAppConfig, create_use_case_router + use_case_router = create_use_case_router( + UseCaseAppConfig( + name="project", + services=[WorkspaceService, AgentService, ChatService], + ), + ) + app.include_router(use_case_router) + ``` +- **`create_jsonrpc_router()` JSON-RPC 2.0 路由** — 替代 REST 路由的方案,方法命名为 `ServiceName.method_name`。适用于需要轻量 RPC 协议的场景 + ```python + from nexusx import create_jsonrpc_router + app.include_router(create_jsonrpc_router(use_case_config)) + ``` +- **`create_use_case_cli()` 生成 Typer CLI** — 将 UseCaseService 方法暴露为 CLI 命令,每个 service 成为一个命令组,每个方法成为子命令 + ```python + from nexusx import create_use_case_cli + cli = create_use_case_cli(use_case_config) + cli() # python -m myapp user-service list-users + ``` +- `create_use_case_voyager()` 可视化服务结构 +- **`create_use_case_graphql_mcp_server()` (3.0+) — UseCase GraphQL MCP**:4 层渐进披露,Layer 3 (`compose_query`) 接收标准 GraphQL 查询字符串: + - Layer 0: `list_apps` — 应用发现 + - Layer 1: `describe_compose_schema(app_name)` — service+方法紧凑列表 + - Layer 2: `describe_compose_method(app_name, service_name, method_name)` — 参数表 + 返回类型 + SDL 片段 + - Layer 3: `compose_query(app_name, query)` — GraphQL 字符串执行,返回 `{data, errors}`;**拒绝内省查询**(`__schema`/`__type`/`__typename`),引导用 Layer 1/2 + ```python + from nexusx import create_use_case_graphql_mcp_server, UseCaseAppConfig + mcp = create_use_case_graphql_mcp_server(apps=[app_config], name="API") + ``` +- **可选: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 + from nexusx import UseCaseAppConfig, build_compose_schema + from nexusx.graphiql import GRAPHIQL_HTML + from nexusx.use_case.compose_executor import ( + compose_introspect, + execute_compose_query, + is_introspection_query, + ) + ``` + + GraphiQL 启动页 HTML 直接用 `nexusx.graphiql.GRAPHIQL_HTML`(不必自己手写)。参考 `demo/use_case/graphql_server.py` +- 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 决定取舍,按需启用): + | 出口 | 入口 | 适用场景 | + |------|------|---------| + | 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 / 服务结构可视化 | +- **main.py 典型模式 — REST + MCP + Voyager**(按需扩展 GraphQL HTTP / JSON-RPC / CLI): + ```python + from nexusx import ( + UseCaseAppConfig, create_use_case_router, + create_use_case_graphql_mcp_server, create_use_case_voyager, + GraphQLHandler, + ) + + app_config = UseCaseAppConfig( + name="project", + services=[UserService, TaskService, SprintService], + ) + + # REST(自动路由,OpenAPI spec) + app.include_router(create_use_case_router(app_config)) + + # JSON-RPC(替代 REST 的轻量方案,二选一) + # app.include_router(create_jsonrpc_router(app_config)) + + # CLI(可选,生成 Typer CLI 命令行工具) + # cli = create_use_case_cli(app_config) + + # GraphQL(基于 SQLModel 实体的辅助开发测试 API) + graphql_handler = GraphQLHandler(base=Base, session_factory=async_session) + + # MCP(UseCase GraphQL MCP — 4 层渐进披露,Layer 3 接收 GraphQL 字符串) + mcp = create_use_case_graphql_mcp_server(apps=[app_config], name="API") + + # Voyager 可视化(注意:services 是 UseCaseService 子类列表,不是 apps/UseCaseAppConfig) + voyager = create_use_case_voyager( + services=app_config.services, + er_manager=er, + ) + app.mount("/voyager", voyager) + ``` + +**V 降 — 定义验收标准:** +进入 Phase 3 编码之前,先与用户确认以下验收项并写入 `spec/phase3.md`: + +| # | 验收项 | 验证方式 | +|---|--------|----------| +| 1 | 每个 REST 端点返回的响应字段符合 DTO 定义(FK 字段隐藏、关系字段包含) | curl POST endpoint | +| 2 | Voyager 中 service 树展示完整(每个服务的方法可见) | 浏览器打开 Voyager | +| 3 | MCP 4 层工具可用:list_apps → describe_compose_schema → describe_compose_method → compose_query | MCP 客户端调用 | +| 4 | POST body 参数校验生效(参数缺少返回 422) | curl 发送非法请求 | + +**实现:** +编写 `dtos.py` → `service.py` → `main.py` 挂载 + +**V 升 — 逐条回查验收:** + +- [ ] 1. REST 响应:`curl /api/sprint_service/list_sprints -X POST` 返回字段符合 DTO +- [ ] 2. FK 隐藏:返回数据中不包含 FK 字段(如 `owner_id`) +- [ ] 3. Voyager:service 节点和 method 方法都可见 +- [ ] 4. MCP 4 层工具: + - `list_apps` 返回应用列表 + - `describe_compose_schema(app_name=...)` 返回 services + methods 紧凑列表 + - `describe_compose_method(app_name=..., service_name=..., method_name=...)` 返回参数表 + 返回类型 + SDL 片段 + - `compose_query(app_name=..., query="{ ServiceName { method_name { fields } } }")` 返回 `{data, errors}`,含字段投影 +- [ ] 5. 参数校验:缺少必填参数返回 422 + +## 踩坑经验 + +1. **不要在 DefineSubset 文件中使用 `from __future__ import annotations`** — 会使类型注解变字符串,SubsetMeta 无法检测 Annotated 元数据 +2. **DTO 字段类型必须用 DTO 类型** — 不能直接用 SQLModel 实体,3.0 起 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` +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 响应紧凑 +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/presets/nexusx/templates/constitution-template.md b/presets/nexusx/templates/constitution-template.md new file mode 100644 index 0000000..273a104 --- /dev/null +++ b/presets/nexusx/templates/constitution-template.md @@ -0,0 +1,83 @@ +# [PROJECT_NAME] Constitution + +> 本 constitution 由 nexusx preset 强制;记录 nexusx 项目的硬规则与 V 型验收治理。所有 spec / plan / tasks 必须符合以下原则,违反需要在 plan 的 Complexity Tracking 中显式记录理由。 + +## 核心原则 + +### I. 关系懒加载强制(不可协商) + +所有 SQLModel `Relationship` 必须显式声明 `sa_relationship_kwargs={"lazy": "noload"}`。 + +**理由**:项目通过显式查询 + Resolver DataLoader 加载关系数据,不依赖 ORM lazy-load。`noload` 防止 session 关闭后 `model_validate(entity)` 访问 relationship descriptor 触发 `DetachedInstanceError`。违反即视为 Phase 1 未完成。 + +### II. 实体层零业务依赖 + +`models.py` / `models/` 子模块禁止 `import nexusx` 或导入任何 `service/` 模块;实体文件只含字段与关系声明,**不含 `@query` / `@mutation` 方法**。 + +**理由**:业务方法在 `service//methods.py` 中实现,通过 `mount_method()` 延迟挂载。混入会破坏 phase 1 → phase 2 的清晰边界。 + +### III. 模型与字段的自描述强制 + +每个 SQLModel 类必须有 docstring 说明业务含义;每个 `Field` 必须有 `description` 参数。 + +**理由**:description 会传递到 OpenAPI spec 和 Voyager ER 图;缺失会导致自动生成的对外接口文档失去语义。 + +### IV. Service 切分必须由用户裁定(不可协商) + +**禁止模型自行决定 Service 切分方案**。必须在 Phase 0(specify 阶段)向用户提出至少一种候选方案并取得明确选择,否则禁止进入 Phase 1。 + +**理由**:Service 切分直接影响目录结构、Phase 2 methods.py 粒度、Phase 3 UseCaseService 类划分、MCP / REST 入口组织。自行决定会迫使后续 phase 围绕错误的边界展开。 + +### V. methods.py 挂载时序与桥接(不可协商) + +- `mount_method()` 必须在创建 `GraphQLHandler` 之前调用 +- `service//methods.py` 中业务方法必须是普通 `async def`(不含 `cls`) +- 挂载桥接函数必须使用 `@functools.wraps(fn)` 保留 docstring + +**理由**:`GraphQLHandler` 初始化时扫描 BaseEntity 子类构建 schema,错序会导致 schema 为空;缺失 `wraps` 会导致 SDL 描述丢失。 + +### VI. DTO 类型纯净性 + +- DefineSubset DTO 字段必须使用 DTO 类型,禁止直接使用 SQLModel 实体(3.0+ 会抛 `SQLModelInDtoFieldError`) +- DTO 文件禁用 `from __future__ import annotations`(会导致 compose schema 无法检测 `Annotated` 元数据) + +**理由**:DTO 是响应契约,混入 ORM 实体会破坏字段选择与序列化语义;字符串化的类型注解在 SubsetMeta 扫描时不可见。 + +### VII. UseCaseService 返回类型强制 + +UseCaseService 的每个 `@query` / `@mutation` 方法必须声明返回类型注解(如 `-> list[X]`、`-> X | None`)。 + +**理由**:3.0 起 compose schema 生成器(`build_compose_schema`)强校验,缺注解的方法在 MCP server 构造时抛 `MissingReturnAnnotationError`;同时 `create_use_case_router()` 也通过 `get_type_hints(method).get("return")` 提取响应类型作为 OpenAPI `response_model`,缺失会让 spec 显示 unknown、Phase 4 TS SDK 无法生成有效类型。 + +### VIII. REST 路由与 MCP 传输协议(不可协商) + +- REST 路由**必须**通过 `create_use_case_router()` 自动生成,禁止手写 `router/` +- MCP `http_app` 必须使用 `transport="streamable-http", stateless_http=True`,并将 MCP lifespan 嵌套进 FastAPI lifespan(`async with mcp_http.lifespan(mcp_http):`) + +**理由**:手写路由无法声明 `response_model`,导致 OpenAPI spec 响应类型为 unknown;MCP http_app 缺少正确的传输 / lifespan 配置会抛 `Task group is not initialized`。 + +## Phase 闸门规则 + +### V 型验收(贯穿所有 phase) + +每个 Phase 必须遵循三段式: + +1. **V 降**:进入 Phase 实现之前,先在 `specs/-/phaseN.md` 中定义可观察、可操作的验收标准("GraphiQL 中执行 X query 返回 Y",而非"代码健壮") +2. **实现**:按验收标准编写代码 +3. **V 升**:逐条对照验收标准,用户确认后才可进入下一 Phase + +### Phase 间暂停 + +每个 Phase 实现完成后**必须暂停**,展示验收结果,等用户明确确认后才进入下一阶段。**禁止**连续执行多个 Phase 不暂停。 + +### Phase 4 条件触发 + +Phase 4(TS SDK 生成)**仅当** plan.md 的"是否生成 TS SDK"字段为 `是` 时才执行;默认 `否`。Phase 4 依赖 Phase 3 使用 `create_use_case_router()`,否则 OpenAPI spec 响应类型为 unknown,SDK 类型生成失败。 + +## 治理 + +- Constitution 优先级高于 spec / plan / tasks 中的便利性选择;冲突时以本文件为准 +- 修改本 constitution 需要在 commit message 中说明理由,并同步更新 `presets/nexusx/templates/constitution-template.md` 源文件 +- 复杂度豁免:如果某项原则需要被违反,必须在 plan.md 的 `## Complexity Tracking` 区块中显式记录"违反项 / 为什么需要 / 拒绝的简化方案",否则视为未通过 Constitution Check + +**Version**: 0.1.0 | **Ratified**: 2026-07-01 | **Last Amended**: 2026-07-01 diff --git a/presets/nexusx/templates/plan-template.md b/presets/nexusx/templates/plan-template.md new file mode 100644 index 0000000..1b7636e --- /dev/null +++ b/presets/nexusx/templates/plan-template.md @@ -0,0 +1,133 @@ +# Implementation Plan: [FEATURE] + +**Branch**: `[###-feature-name]` | **Date**: [DATE] | **Spec**: [link] + +**Input**: Feature specification from `/specs/[###-feature-name]/spec.md` + +**Note**: 本模板由 `__SPECKIT_COMMAND_PLAN__` 命令填充。详见 `.specify/templates/plan-template.md` 的执行工作流。 + +## Summary + +[从 spec.md 提取:核心需求 + nexusx 技术方案要点(DB 选型、service 切分、是否 TS SDK)] + +## Technical Context *(nexusx preset)* + + + +**Language/Version**: Python ≥ 3.10(具体版本:[e.g., 3.12 或 NEEDS CLARIFICATION]) + +**Primary Dependencies**: nexusx>=3.2, fastapi, uvicorn, sqlmodel, pydantic>=2.0, aiodataloader + +**DB 选型**: [从 spec.md Step 0-7 确认结论复制:in-memory sqlite / file sqlite / docker pg / docker mysql / external ___] + +**async DATABASE_URL**: [e.g., `sqlite+aiosqlite://` 或 `postgresql+asyncpg://user:pwd@localhost:5432/db` 或 NEEDS CLARIFICATION] + +**sync DATABASE_URL_SYNC** *(持久化场景必填,供 alembic + load_seed 使用)*: [e.g., `sqlite:///./var/.db` 或 NEEDS CLARIFICATION] + +**Async DB Driver**: +- in-memory / file sqlite → `aiosqlite` +- postgresql → `asyncpg` +- mysql → `aiomysql` + +**是否引入 alembic**: +- ✅ 持久化场景(file sqlite / docker / external):**必须**引入,加 `alembic>=1.13` 依赖 +- ❌ in-memory sqlite:不引入 + +**Testing**: pytest + pytest-asyncio(`tests/` 在项目根,不放 `service/*/` 子目录避免循环导入) + +**Target Platform**: [e.g., Linux server / Docker / 本地开发] + +**Project Type**: web-service(FastAPI + nexusx) + +**Performance Goals**: [domain-specific 或 NEEDS CLARIFICATION] + +**Constraints**: [e.g., <200ms p95 或 NEEDS CLARIFICATION] + +**Scale/Scope**: [e.g., 10k users, N entities, M services] + +## Phase 决策记录 + +### Service 切分最终方案 + +[从 spec.md Step 0-4 复制用户最终选择,并列出每个 service 的方法清单] + +``` +auth/ → register, login +chat/ → create_conversation, list_messages, send_message +``` + +### 是否生成 TS SDK + +- [ ] **是** —— Phase 4 触发,需 `fe/` 子目录,使用 `@hey-api/openapi-ts` 从 `http://localhost:8000/openapi.json` 生成 +- [x] **否**(默认)—— 不生成 SDK,跳过 Phase 4 + +**注意**:选"是"的前提是 Phase 3 使用 `create_use_case_router()` 自动生成 REST 路由(Constitution Principle VIII),否则 OpenAPI spec 响应类型为 unknown,SDK 类型生成失败。 + +### 第三方库确认清单 + +| 功能领域 | 选定方案 | 版本 | 维护状态 | 备注 | +|----------|----------|------|----------|------| +| 认证 | [候选] | [版本] | [活跃/维护中/已停更] | [集成要点] | +| 实时推送 | [候选] | [版本] | [活跃/维护中/已停更] | [集成要点] | + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +逐条对照 `presets/nexusx/templates/constitution-template.md` 的 8 条核心原则: + +| # | 原则 | 状态 | 备注 | +|---|------|------|------| +| I | 关系懒加载强制 | ✅ / ⚠️ / ❌ | [备注] | +| II | 实体层零业务依赖 | ✅ / ⚠️ / ❌ | [备注] | +| III | 模型与字段自描述 | ✅ / ⚠️ / ❌ | [备注] | +| IV | Service 切分用户裁定 | ✅ / ⚠️ / ❌ | [备注] | +| V | methods.py 挂载时序 | ✅ / ⚠️ / ❌ | [备注] | +| VI | DTO 类型纯净性 | ✅ / ⚠️ / ❌ | [备注] | +| VII | UseCaseService 返回类型强制 | ✅ / ⚠️ / ❌ | [备注] | +| VIII | REST 路由与 MCP 传输协议 | ✅ / ⚠️ / ❌ | [备注] | + +任何 ❌ 或 ⚠️ 都必须在下方 Complexity Tracking 中给出理由。 + +## Project Structure + +```text +src/ +├── models.py # Phase 1 纯实体 → Phase 2 通过 mount_method() 挂载方法 +├── db.py # Phase 1(engine + session factory,URL 由 DB 选型决定) +├── database.py # Phase 1(in-memory: create_all+seed;持久化: no-op,schema 由 alembic 管) +├── service/ # Phase 2 新增 methods.py,Phase 3 补充 service.py/dtos.py +│ └── / # 按业务域划分(非按实体),具体见上方 Service 切分最终方案 +│ ├── methods.py # Phase 2: 独立业务方法(普通 async def) +│ ├── dtos.py # Phase 3: DefineSubset DTO +│ ├── service.py # Phase 3: UseCaseService +│ ├── test.py # Phase 3: unittest +│ └── spec.md # Phase 3: 服务说明 +├── main.py # 逐步扩展(voyager → graphql → create_use_case_router → mcp) +alembic/ # Phase 1 持久化场景才引入(file sqlite / docker / external) +├── env.py # import src.models + sync URL + render_as_batch(sqlite) +├── script.py.mako # 模板加 import sqlmodel +└── versions/ +scripts/ # Phase 1 持久化场景 +└── load_seed.py # 一次性把 var/seed_data.json 灌入文件 DB +var/ # gitignored(file sqlite 场景) +├── .db +└── seed_data.json +fe/ # Phase 4 仅在"是否生成 TS SDK"为"是"时引入 +├── openapi-ts.config.ts +├── package.json +└── src/sdk/ +``` + +**REST 路由通过 `create_use_case_router(use_case_config)` 自动生成**,不需要手写 `router/` 目录。 + +## Complexity Tracking + +> **仅在 Constitution Check 有 ⚠️ 或 ❌ 时填写** + +| 违反项 | 为什么需要 | 拒绝的简化方案及理由 | +|--------|------------|---------------------| +| [e.g., 第 N 条] | [当前需求] | [为什么简化方案不行] | diff --git a/presets/nexusx/templates/spec-template.md b/presets/nexusx/templates/spec-template.md new file mode 100644 index 0000000..e534b3d --- /dev/null +++ b/presets/nexusx/templates/spec-template.md @@ -0,0 +1,195 @@ +# Feature Specification: [FEATURE NAME] + +**Feature Branch**: `[###-feature-name]` + +**Created**: [DATE] + +**Status**: Draft + +**Input**: User description: "$ARGUMENTS" + +## Phase 0 需求确认纪要 *(nexusx preset mandatory)* + + + +### Step 0-1 术语与实体定义 + +逐一列出本特性涉及的业务实体。每个实体说明业务含义、核心字段、字段约束。 + +| 实体 | 业务含义 | 核心字段(名称+类型+语义) | 字段约束 | +|------|----------|-----------------------------|----------| +| [Entity 1] | [一句话业务含义] | [关键属性,不要穷举] | [唯一/非空/枚举/联合唯一] | + +### Step 0-2 实体关系 + +用文本 ER 图展示实体间关系,每条关系标明方向、基数、业务含义、是否需要中间实体。 + +``` +User ──1:N──→ Post +Post ──N:M──→ Tag (中间表: PostTag) +``` + +- **关系 1**: [方向] [业务含义] +- **关系 2**: [方向] [业务含义] + +### Step 0-3 聚合根 + +明确本特性的聚合根(业务入口实体)。每个聚合根必须明确是 **SQLModel 实体**还是 **虚拟实体(普通 `pydantic.BaseModel`,不落表)**。 + +| 聚合根 | 类型 | 数据持久化 | 选用理由 | +|--------|------|------------|----------| +| [Aggregate 1] | SQLModel 实体 / 虚拟实体 | 落表 / 不落表 | [判断依据:字段全部来自 DB → SQLModel;来自请求上下文或聚合多源 → 虚拟] | + +**判断依据**:如果根字段全部来自数据库表 → SQLModel;如果字段来自请求上下文(JWT、headers)或聚合多个源 → 虚拟实体。 + +### Step 0-4 Service 切分候选方案 ⚠️ 用户裁定 + + + +**候选方案 A:按业务功能域** + +``` +auth/ → [methods] +chat/ → [methods] +``` + +- 优势:[业务内聚] +- 劣势:[可能过大] + +**候选方案 B:按聚合根** + +``` +user/ → [methods] +conversation/ → [methods] +``` + +- 优势:[每 service 粒度均匀] +- 劣势:[强耦合被拆开] + +**候选方案 C:混合(功能域 + 独立聚合)** + +``` +[domains...] +``` + +**用户最终选择**: [NEEDS CLARIFICATION: 待用户从 A/B/C 中选择或提出修正方案] + +### Step 0-5 GraphQL 定位 + +GraphQL 在本项目中是**辅助开发测试与 AI 测试**接口,不是正式 API。业务方法定义在 `service//methods.py`,挂载到 Entity(GraphQL 辅助)与 UseCaseService(REST + MCP 正式接口)。 + +### Step 0-6 第三方库确认 + +| 功能领域 | 推荐方案 | 维护状态已调查 | 备注 | +|----------|----------|----------------|------| +| 认证 | [候选] | [是/否] | [兼容性说明] | +| 实时推送 | [候选] | [是/否] | [兼容性说明] | +| 文件存储 | [候选] | [是/否] | [兼容性说明] | +| 数据迁移 | [候选] | [是/否] | nexusx 已覆盖 ORM/GraphQL/MCP,不在此讨论 | + +**注意**:用户指定的库必须先调查维护状态;发现问题需告知用户并提供替代方案。 + +### Step 0-7 DB 选型 + 迁移策略 + +从下表选择一种 DB 与迁移策略。**此决策决定 Phase 1 是否引入 alembic、`database.py` 的 `init_db()` 策略**。 + +| 选项 | async DB URL | 持久化 | Alembic | 适用场景 | +|------|--------------|--------|---------|----------| +| In-memory SQLite | `sqlite+aiosqlite://` | ❌ 进程退出即丢 | ❌ | 纯原型 / Demo / 讨论 | +| File-backed SQLite | `sqlite+aiosqlite:///./var/.db` | ✅ 文件 | ✅ 必须 | 本地开发 / 单人项目 | +| Docker PostgreSQL | `postgresql+asyncpg://...` | ✅ 容器卷 | ✅ 必须 | 团队开发 / 生产前演练 | +| Docker MySQL | `mysql+aiomysql://...` | ✅ 容器卷 | ✅ 必须 | 团队偏好 MySQL | +| External DB | 视驱动 | ✅ | ✅ 必须 | 已有 DB 基础设施 | + +**用户最终选择**: [NEEDS CLARIFICATION: 待用户明确选定] + +### Step 0-8 检查清单 + +进入 Phase 1(plan 阶段)之前,以下必须全部 ✅: + +- [ ] 所有实体和字段完整,约束清晰 +- [ ] 实体关系方向和基数正确 +- [ ] 聚合根明确,每个聚合根类型(SQLModel / 虚拟)已确认 +- [ ] **Service 切分方案由用户明确选择**(Constitution Principle IV) +- [ ] 核心用例覆盖主要业务场景,逻辑自洽 +- [ ] 第三方库选型确认,维护状态已调查 +- [ ] **DB 选型 + 迁移策略由用户明确选定**(影响 Phase 1 alembic) +- [ ] 无明显遗漏或未讨论的边界情况 + +--- + +## User Scenarios & Testing *(mandatory)* + + + +### User Story 1 - [Brief Title] (Priority: P1) + +[Describe this user journey in plain language] + +**Why this priority**: [Explain the value and why it has this priority level] + +**Independent Test**: [Describe how this can be tested independently] + +**Acceptance Scenarios**: + +1. **Given** [initial state], **When** [action], **Then** [expected outcome] +2. **Given** [initial state], **When** [action], **Then** [expected outcome] + +--- + +### User Story 2 - [Brief Title] (Priority: P2) + +[Describe this user journey in plain language] + +**Why this priority**: [Explain] + +**Independent Test**: [How to test independently] + +**Acceptance Scenarios**: + +1. **Given** [initial state], **When** [action], **Then** [expected outcome] + +--- + +### Edge Cases + +- What happens when [boundary condition]? +- How does system handle [error scenario]? + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: System MUST [specific capability] +- **FR-002**: System MUST [specific capability] + +*Marking unclear requirements:* + +- **FR-XXX**: System MUST [NEEDS CLARIFICATION: reason] + +### Key Entities *(include if feature involves data)* + +- **[Entity 1]**: [What it represents, key attributes without implementation] +- **[Entity 2]**: [Relationships to other entities] + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: [Measurable metric, e.g., "Users can complete X in under Y minutes"] +- **SC-002**: [Measurable metric] + +## Assumptions + +- [Assumption about target users] +- [Assumption about scope boundaries] +- [Assumption about data/environment] diff --git a/presets/nexusx/templates/tasks-template.md b/presets/nexusx/templates/tasks-template.md new file mode 100644 index 0000000..4dce3df --- /dev/null +++ b/presets/nexusx/templates/tasks-template.md @@ -0,0 +1,219 @@ +--- + +description: "nexusx preset 任务模板:phase-first 混合组织(Schema → Methods → Service → 可选 SDK)" +--- + +# Tasks: [FEATURE NAME] + +**Input**: Design documents from `/specs/[###-feature-name]/` + +**Prerequisites**: plan.md (required), spec.md (required for user stories), data-model.md, research.md, contracts/ + +**Tests**: 示例中包含测试任务。仅当 spec.md 明确要求时才生成测试任务。 + +## 组织原则 *(nexusx preset)* + +**Phase-first 混合**:顶层按 nexusx Phase 组织(Schema → Methods → Service → 可选 SDK),同一 Phase 内的任务用 `[USx]` 标签标注所属用户故事。 + +**理由**:nexusx 的硬约束是 phase 顺序 + V 型验收 + phase 间暂停。用户故事优先布局会把 Schema 工作散到 US1/US2/US3,掩盖"Schema 完成后暂停"的闸门。在 nexusx 中 Phase 3 之前无可交付物,所以"每故事 MVP 独立性"弱于 phase 推进。 + +**跨故事共享实体**(如 `User` 被 US1+US2 共用)放在 Phase 1 顶部,标注 `[US1] [US2]` 多标签。 + +## Format: `[ID] [P?] [P] [US?] Description` + +- **[P]**: 可并行(不同文件,无依赖) +- **[P1] / [P2] / [P3] / [P4]**: nexusx phase 标签(必填) +- **[USx]**: 用户故事标签(多故事共享任务可标多个 `[US1] [US2]`) +- 任务描述必须包含具体文件路径 + +## Path Conventions + +- 单项目:仓库根 `src/`、`tests/` +- 路径假设为单项目布局;如 plan.md 选择其他结构需相应调整 + + + +## Phase 1: Schema (Entities + DB + ER + mock seed) + +**Goal**: 定义纯实体模型(字段 + 关系声明)+ DB engine + mock seed data + Voyager ER 可视化 + +**Reference**: `presets/nexusx/reference/phase1.md`(包含 alembic 详细配置、lazy=noload 强制、踩坑经验) + +**V 降验收标准** *(写入 `specs/-/phase1.md`)*: +- [ ] 1. 每个 Entity 在 Voyager ER 图中正确显示 +- [ ] 2. `models.py` 中每个 Entity 只包含字段 + Relationship,无 `@query/@mutation` 方法 +- [ ] 3. mock seed 数据样本合理(数量、关联、边界值) +- [ ] 4. (持久化场景)alembic baseline 生成 + upgrade 成功 + +**Tasks**: + +- [ ] T001 [P1] 创建 `src/db.py`:engine + async session factory(不导入 models,避免循环依赖;URL 取自 plan.md DB 选型) +- [ ] T002 [P1] [US1] [US2] 创建共享实体 `src/models.py`:定义被多个故事使用的实体(如 User),所有 Relationship 加 `sa_relationship_kwargs={"lazy": "noload"}`,每个 Model 加 docstring,每个 Field 加 description +- [ ] T003 [P1] [US1] 创建 US1 专属实体(在 `src/models.py` 或 `src/models/.py`) +- [ ] T004 [P1] [US2] 创建 US2 专属实体 +- [ ] T005 [P1] 创建 `src/database.py`:in-memory → `init_db()` 做 create_all + mock seed;持久化 → no-op +- [ ] T006 [P1] 创建 `src/main.py`:FastAPI lifespan + Voyager ER 可视化(`create_use_case_voyager(services=[], er_manager=er)`) +- [ ] T007 [P1] 编写 mock seed data(持久化场景:写到 `var/seed_data.json`,由 `scripts/load_seed.py` 灌入) + +**持久化场景额外任务**(plan.md DB 选型为 file sqlite / docker / external 时): + +- [ ] T008 [P1] `pyproject.toml` 加 `alembic>=1.13` + 对应 async driver(`asyncpg` / `aiomysql`) +- [ ] T009 [P1] `alembic init alembic` +- [ ] T010 [P1] 改 `alembic/env.py`:顶部 `import src.models # noqa: F401`、`target_metadata = SQLModel.metadata`、sync URL 从 env var 读、SQLite 加 `render_as_batch=True` +- [ ] T011 [P1] 改 `alembic/script.py.mako` 加 `import sqlmodel` +- [ ] T012 [P1] `alembic.ini` 的 `sqlalchemy.url =` 留空(env.py 覆盖) +- [ ] T013 [P1] `.gitignore` 加 `var/` +- [ ] T014 [P1] `alembic revision --autogenerate -m "init schema"` → 检查迁移文件 → `alembic upgrade head` +- [ ] T015 [P1] 编写 `scripts/load_seed.py`:把 mock seed 一次性灌入文件 DB(保留 ID) + +**V 升 — 用户确认**: +- [ ] 在 Voyager 中确认 ER 图、实体纯字段、mock seed 合理、alembic(如有)已 upgrade + +--- + +## Phase 2: Methods (业务逻辑 + Entity 挂载,GraphQL 可查询) + +**Goal**: 按业务域在 `service//methods.py` 中实现独立 async 方法,通过 `mount_method()` 挂载到 Entity + +**Reference**: `presets/nexusx/reference/phase2.md`(包含 `_mount()` 桥接、`@functools.wraps` 必要性、测试 monkey-patch、踩坑经验) + +**V 降验收标准** *(写入 `specs/-/phase2.md`)*: +- [ ] 每个 `@query`/`@mutation` 覆盖正常 + 异常场景,验证方式为 GraphiQL 执行 +- [ ] `mount_method()` 在 `GraphQLHandler` 之前调用 +- [ ] `pytest tests/` 通过 + +**Tasks** *(每个 domain 一组)*: + +- [ ] T020 [P2] [US1] 创建 `src/service//methods.py`:实现 US1 相关业务方法(普通 `async def`,无 `cls`,无装饰器) +- [ ] T021 [P2] [US2] 创建 `src/service//methods.py`(不同 domain 可并行) +- [ ] T022 [P2] 在 `src/models.py` 末尾添加 `mount_method()` 函数:延迟 import methods + `_mount()` 桥接 classmethod 协议 + `@functools.wraps(fn)` 保留 docstring +- [ ] T023 [P2] 在 `src/main.py` 中 `GraphQLHandler` 创建之前显式调用 `mount_method()` +- [ ] T024 [P2] [US1] 编写 `tests/test__methods.py`(项目级 `tests/`,避免循环导入;monkey-patch `async_session`) +- [ ] T025 [P2] [US2] 编写 `tests/test__methods.py` + +**V 升 — 用户确认**: +- [ ] 在 GraphiQL 中逐条执行验收表(每个方法的正常 + 异常场景) +- [ ] `pytest tests/` 全部通过 + +--- + +## Phase 3: Service (DTO + UseCaseService + REST + MCP + Voyager) + +**Goal**: DefineSubset DTO + UseCaseService + 自动 REST 路由 + MCP server + Voyager services 可视化 + +**Reference**: `presets/nexusx/reference/phase3.md`(包含 DefineSubset 模式、跨层数据流 ExposeAs/SendTo/Collector、MCP http_app lifespan 合并、踩坑经验) + +**V 降验收标准** *(写入 `specs/-/phase3.md`)*: +- [ ] 1. 每个 REST 端点返回字段符合 DTO 定义(FK 隐藏、关系包含) +- [ ] 2. Voyager 中 service 树展示完整 +- [ ] 3. MCP 4 层工具可用(list_apps → describe_compose_schema → describe_compose_method → compose_query) +- [ ] 4. POST body 参数校验生效(缺参 422) + +**Tasks** *(每个 service 一组)*: + +- [ ] T030 [P3] [US1] 创建 `src/service//dtos.py`:DefineSubset DTO(禁 `from __future__ import annotations`,字段用 DTO 类型不用 SQLModel 实体,关系字段名匹配 ORM relationship 自动加载) +- [ ] T031 [P3] [US1] 创建 `src/service//service.py`:UseCaseService 复用 methods.py 逻辑(query 方法:methods → model_validate → Resolver().resolve;mutation 方法:同单条 get 模式),所有方法声明返回类型注解,service.py 不直接操作 DB +- [ ] T032 [P3] [US1] 创建 `src/service//spec.md`:服务目的、用途、需求、变更记录 +- [ ] T033 [P3] [US2] 创建 `` 的 dtos.py + service.py + spec.md(与 US1 并行) + +**Main.py 集成**: + +- [ ] T040 [P3] 在 `src/main.py` 添加: + - `UseCaseAppConfig` 聚合所有 Service + - `app.include_router(create_use_case_router(app_config))` 自动生成 REST 路由 + - `mcp = create_use_case_graphql_mcp_server(apps=[app_config], name="API")` 创建 MCP server + - MCP http_app 配置:`transport="streamable-http", stateless_http=True`,在 FastAPI lifespan 中嵌套 `async with mcp_http.lifespan(mcp_http):` + - `create_use_case_voyager(services=app_config.services, er_manager=er)` 补充 services 可视化(注意:services 是 UseCaseService 子类列表,不是 apps/UseCaseAppConfig) + +**V 升 — 用户确认**: +- [ ] curl `/api//` 返回字段符合 DTO +- [ ] Voyager 中 services 节点可见 +- [ ] MCP 客户端走 4 层工具链路完整 +- [ ] 缺参请求返回 422 + +--- + +## Phase 4: TS SDK (可选 — 仅当 plan.md "是否生成 TS SDK" 为 "是") + + + +**Goal**: 从 FastAPI OpenAPI spec 生成 TypeScript SDK + +**Reference**: `presets/nexusx/reference/phase3.md` 第 4 节 + 历史 `skill/phases/phase4.md` + +**前提**: Phase 3 必须使用 `create_use_case_router()`(Constitution Principle VIII),否则 OpenAPI 响应类型为 unknown。 + +**Tasks**: + +- [ ] T050 [P4] 创建 `fe/openapi-ts.config.ts`(`@hey-api/openapi-ts` + `operations: { strategy: 'byTags' }`) +- [ ] T051 [P4] 创建 `fe/package.json`(`generate-client` 脚本 + `@hey-api/openapi-ts` 依赖) +- [ ] T052 [P4] `cd fe && npm install && npm run generate-client` +- [ ] T053 [P4] 验证:每个 DTO 字段有 TS 类型、snake_case 字段名原样映射、嵌套关系递归结构正确 + +--- + +## Polish & Cross-Cutting Concerns + +**Purpose**: 跨多个 phase 的改进 + +- [ ] T060 [P] 文档更新(每个 service 的 spec.md 补充变更记录) +- [ ] T061 [P] 性能优化(DataLoader 缓存命中率、SQL 列裁剪) +- [ ] T062 [P] Security hardening(认证集成、租户隔离) +- [ ] T063 [P] Run `quickstart.md` 端到端验证 + +--- + +## Dependencies & Execution Order + +### Phase 依赖 + +- **Phase 1 Schema**: 无依赖,立即开始。完成后**必须暂停**等用户 V 升确认 +- **Phase 2 Methods**: 依赖 Phase 1 完成。完成后**必须暂停** +- **Phase 3 Service**: 依赖 Phase 2 完成。完成后**必须暂停** +- **Phase 4 SDK** *(可选)*: 依赖 Phase 3 完成 +- **Polish**: 依赖前面所有 phase + +### 用户故事依赖 + +跨 phase 追踪同一用户故事:扫所有 phase 标题找 `[USx]` 标签。共享实体放在 Phase 1 顶部多标签。 + +### Parallel Opportunities + +- 同 phase 内 `[P]` 标记任务可并行 +- 不同 domain 的 methods.py / service.py 可跨 phase 并行(但需先完成对应 phase 的共享基础设施) + +--- + +## Implementation Strategy + +### Sequential Phase (默认) + +1. Phase 1 Schema → V 升确认 → 暂停 +2. Phase 2 Methods → V 升确认 → 暂停 +3. Phase 3 Service → V 升确认 → 暂停 +4. (可选)Phase 4 SDK +5. Polish + +**关键**:nexusx 的硬约束是 phase 间暂停(Constitution "Phase 闸门规则"),禁止连续执行多个 phase 不暂停。 + +--- + +## Notes + +- 每个任务必须含具体文件路径 +- 每个 phase 完成后在 `specs/-/phaseN.md` 写入 V 降 + V 升 结果 +- 交付前校验所有 phaseN.md 非空(空文件 = 未完成) diff --git a/scripts/preset-reinstall.sh b/scripts/preset-reinstall.sh new file mode 100755 index 0000000..8dd638c --- /dev/null +++ b/scripts/preset-reinstall.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# nexusx preset 重新安装脚本 +# +# spec-kit 的 `specify preset add --dev` 是 copy-based(非 symlink), +# 每次编辑 preset 源文件后必须重新安装才能在 .claude/commands/ 等位置生效。 +# 本脚本封装 remove + add 流程,让测试迭代一轮一次按键。 +# +# Usage: bash scripts/preset-reinstall.sh + +set -euo pipefail + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +PRESET_DIR="${REPO_ROOT}/presets/nexusx" + +if [ ! -f "${PRESET_DIR}/preset.yml" ]; then + echo "ERROR: ${PRESET_DIR}/preset.yml 不存在" >&2 + exit 1 +fi + +echo "==> Removing existing nexusx preset (if installed)" +specify preset remove nexusx 2>/dev/null || echo " (was not installed)" + +echo "==> Installing nexusx preset from ${PRESET_DIR}" +specify preset add --dev "${PRESET_DIR}" + +echo "==> Installed presets:" +specify preset list + +echo +echo "==> Verify template resolution (nexusx should win over core):" +for tpl in spec-template plan-template tasks-template constitution-template; do + resolved=$(specify preset resolve "${tpl}" 2>/dev/null || echo "FAILED") + echo " ${tpl} → ${resolved}" +done + +echo " (command overrides registered via .claude/skills/ — check with: ls .claude/skills/speckit-*/SKILL.md)" + +echo +echo "Done. 现在可以在 nexusx 项目中运行 /speckit-specify 测试。"