Define Architecture
为 TypeScript 全栈应用定义持久、易于变更的架构默认值,产出一份可强制执行的架构简报(architecture brief)。
适用(IS):文件夹结构、模块契约、中间件管线、前端/后端边界;架构简报;在既有代码库中进行领域驱动的深化;面向编码代理(agent)的代码库护栏。
不适用(IS NOT):搭建新仓库(scaffold-nextjs 用于 Next.js turborepo,scaffold-cli 用于 TypeScript CLI)、多租户领域/隔离/路由(multi-tenant-architecture)、对本地 diff 的结构性审查(pr-reviewer)。
工作流与参考文档
跟踪以下清单:
架构进度:
- [ ] 步骤 1:选择工作流(新代码库:Setup;既有代码库:Adoption)
- [ ] 步骤 2:端到端运行该工作流
- [ ] 步骤 3:撰写简报(输出模板)
- [ ] 步骤 4:运行校验循环,将结果记录进简报
- [ ] 步骤 5:修复未通过的检查项,重新运行循环
仅在条件满足时加载参考文档:
| 参考文档 | 何时阅读 |
|---|---|
| references/stack-defaults.md | 选择库、工具链或部署目标时 |
| references/api-design.md | 设计端点、模块契约或请求上下文;审查 API 表面变更时 |
| references/distributed-correctness.md | 设计调用外部系统、消费 webhook、需要重试、需要审计追踪或涉及资金流动(计费、额度、支付)的流程时 |
| references/deepening-existing.md | 运行 Adoption 工作流(领域映射、机会模式、输出模板)时 |
| references/craftsmanship.md | 撰写团队约定、测试或质量门槛章节时 |
| references/agent-friendly-codebase.md | 为编码代理准备代码库:护栏工具、不变量棘轮、遗留标记、生成式契约、验证层级 |
| references/shipping-practices.md | 撰写发布与回滚章节时 |
Setup 工作流(新代码库)
- 约束优先:产品范围、团队规模、合规/安全、预期规模、部署目标、必需集成以及质量门槛。
- 选择仓库形态:
- apps/ 用于可部署表面(api、web、admin)。
- packages/ 用于共享库(shared、ui、icons、auth、proto)。
- 定义后端模块契约,每项都需指明其强制手段(lint 边界规则或类型检查):
- handler:仅负责传输层。
- service:业务编排。
- dao:仅负责数据库访问。
- mapper:DB/proto/领域之间的转换。
- constants 与 types:模块本地契约。
- 定义请求上下文与中间件:
- 在由 AsyncLocalStorage 支持的 RequestContext 中携带 tenantId、userId 和 traceId,在每个入口点(RPC、HTTP、任务、CLI)初始化,并通过 getContext() 读取;绝不要把 ctx 参数贯穿业务函数。实现见 references/api-design.md。
- 在注册时要求每个 RPC 方法都有显式鉴权策略;没有策略的方法注册失败,绝不默认开放。
- 将鉴权、日志、错误和上下文放在共享中间件中,而非每个 handler 的代码里。
- 定义前端边界:
- 默认使用 Server Components;仅在需要交互性的叶子组件上添加 "use client"。
- 服务端状态放在 TanStack/Connect Query;客户端状态放在组件状态;MobX 仅用于两者都不适合的横切客户端状态。
- 每份数据有且仅有一个所有者。绝不把服务端数据镜像进 useState,也不要用 useEffect 同步两个 store;两者都是所有权不清的红旗信号。
- 测试与发布:
- 单元测试保持不依赖数据库;集成/E2E 并行运行,使用动态生成的 ID,使运行之间不会在夹具(fixture)上冲突。
- 以小型、完整、可逆的垂直切片发布,每个变更都有回滚方案。
- 一个切片只有在可靠性、错误路径、可观测性和面向用户的状态都被覆盖时才算完成;不要把它们推迟到后续打磨阶段。
- 代理护栏:按 references/agent-friendly-codebase.md 将死代码、重复、边界和文件大小检查接入 pre-commit 与 CI。
Adoption 工作流(既有代码库)
目标:领域驱动的深化,而非重写。加载 references/deepening-existing.md 获取分析方法、机会模式和输出模板。
- 映射领域语言与决策。如存在,阅读 CONTEXT.md、docs/adr/ 或本地等效文档,然后阅读代码以识别团队所命名的实体、动作和有界上下文。记录分歧(一个概念三个名字,或一个名字三个概念)。
- 寻找深化机会。寻找贫血概念、浅模块、边界泄漏、命名分歧、重复概念、基本类型偏执、错位逻辑,以及被迫绕过公共接口的测试。每条都记录文件路径,绝不写模糊的坏味道。
- 按杠杆排序。优先选择能通过删除测试、将具名的未来变更局部化、churn 低、满足当前需求且拥有可行测试接缝的机会。先对候选排序,再设计目标接口;丢弃投机性的清理。
- 先迁移一个垂直切片。在泛化之前,先端到端验证最高杠杆的动作通过一个切片。
- 添加护栏。用 lint、类型或测试检查强制新边界,使其不会退化,然后逐模块推广。针对代理专用护栏(死代码、重复、遗留标记、文件大小上限)加载 references/agent-friendly-codebase.md。
校验循环
在定稿前运行;将结果记录进简报(Open risks)。每项检查都需要证据;"看起来一致"不算通过。
- 一致性:命名、模块边界和中间件规则在每个服务中读起来相同。证据:零发现的矛盾扫描。
- 可强制性:每个契约都指明其 lint 规则、类型检查或测试。证据:每个契约都有强制列或说明。
- 可操作性:每个可部署表面都有可观测性、健康检查和回滚路径。证据:发布章节逐项列出。
- 质量门禁(仅当代码变更时,例如 Adoption 切片):运行仓库的 lint、类型检查和针对性测试(npm run lint、npm run check-types、npm run test --workspace=<pkg> 或等效命令)。证据:通过的输出。
失败时:修复简报或约定,然后重新运行循环。
输出模板
使用以下结构:
Architecture brief
Context and constraints
Repo shape
Backend module contracts
Request context and middleware policy
Frontend boundaries
Testing strategy
Quality bar and surface-area budget
Rollout and rollback plan
Open risks and follow-ups
相关技能
- scaffold-nextjs 或 scaffold-cli:简报达成一致后搭建仓库。
- multi-tenant-architecture:租户识别、隔离与领域策略。
- pr-reviewer:实现后对本地 diff 进行结构性审查。
- planning:将 Adoption 机会转化为实现计划,然后进行压力测试。
- agents-md:审计并重构 AGENTS.md 文件本身;本技能决定什么应属于工具化。
- tidy:限定 diff 范围的清理;references/agent-friendly-codebase.md 涵盖让这些清理保持小范围的仓库级护栏设置。
易错点(Gotchas)
- 5 人以下团队不要默认微服务:每个服务都会增加部署管线、契约版本管理和 on-call 表面。从模块化 monorepo 开始;当团队或规模压力证明边界成立时再拆分。
- 不要把应用级依赖放进 monorepo 根 package.json:提升会隐藏缺失的声明,导致应用本地能构建但单独部署时失败。每个应用拥有自己的依赖。
- 不要在无强制手段的情况下定义模块契约(handler/service/dao):未强制的契约会在第一个截止日期前退化。在你写下它的当天就添加导入边界 lint 规则(例如 dao 不可导入 handler)。
- 不要在业务函数中贯穿 ctx 参数而非使用 AsyncLocalStorage:每个签名都会膨胀,之后添加一个字段就要改数百处调用点。
- 不要把 "use client" 放在页面或布局层级:它会把整个子树转换为客户端渲染,丧失流式传输和直接的服务端数据访问。把它推到叶子节点。
- Adoption 期间不要提议大爆炸式重写:先迁移一个垂直切片,验证它,然后泛化。
- 不要过早抽取到 packages/:等到 3 个以上应用需要相同代码时再说;过早的共享包会无谓地耦合发布周期。
- 不要在没有每个变更回滚方案的情况下定稿简报:不可逆的决策在发布前需要一份有文档记录的兜底方案。
- 不要在没有 outbox(或 CDC)的情况下双写数据库与队列/webhook:一侧提交,另一侧失败,你会静默丢失或伪造一条通知。见 references/distributed-correctness.md。
- 不要用构造方式强制一个可被外部强制的不变量(无符号类型、硬 CHECK):当外界强制该状态时,系统会崩溃或钳制,而非记录它。要表示它、事后检测它、显式恢复。
- 不要在 AGENTS.md 中编码 linter 能确定性强制的内容:提示规则会在上下文压力下退化;而 pre-commit 与 CI 中的退出码不会。
- 不要让遗留代码未加标记地留在当前模式旁边:代理会 grep,先找到旧端点,然后在其上构建。用可 grep 的 LEGACY 注释标记它,或删除它(见 references/agent-friendly-codebase.md)。