From db1721ca75d3a54506765b389e9455edb0c095f5 Mon Sep 17 00:00:00 2001 From: eanzhao Date: Mon, 27 Apr 2026 13:30:54 +0800 Subject: [PATCH 1/2] Rename docs/decisions to docs/adr and add architecture vocabulary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Align docs structure and terminology with the deepening / Ports & Adapters vocabulary used by the improve-codebase-architecture skill. - docs/decisions/ → docs/adr/ (matches skill convention; ADR is the wider industry term and is what tooling expects) - Renumber duplicate ADRs introduced by parallel branches: 0011-agui-sse-projection-session-pipeline → 0015 (lark-nyx kept 0011) 0012-studio-member-first-published-service → 0016 (channel-credential kept 0012) - Update CLAUDE.md, repo README, tools/docs/lint.sh, tools/docs/build-index.sh, test/tools/test_docs_tools.sh, plus in-doc cross-links in design / history / ADR files. - lint.sh now also rejects duplicate ADR numbers so the same collision can't happen again. - Add docs/canon/architecture-vocabulary.md mapping the skill's vocabulary (Module / Interface / Depth / Seam / Adapter / Leverage / Locality) onto aevatar's existing terms (Actor / GAgent / Port / ReadModel / Projection) with anti-aliases for "boundary" / "service" / "API" / "component" so arch reviews stay coherent. - Regenerate docs/README.md. Out of scope (follow-up): top-level CONTEXT.md / CONTEXT-MAP.md to satisfy the skill's domain-language input — aevatar already uses docs/canon/ as a multi-file context surface and a single-file CONTEXT redirect needs a separate decision. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 8 +- README.md | 2 +- docs/README.md | 24 ++-- .../0001-project-split-strategy.md | 0 .../0002-mainnet-architecture.md | 0 .../0003-kafka-transport.md | 0 .../0006-multi-agent-evolution.md | 0 .../{decisions => adr}/0007-stream-forward.md | 0 ...008-channel-runtime-multi-token-routing.md | 0 .../0009-channel-bot-callback-architecture.md | 0 ...0010-channel-phase0-provider-validation.md | 0 .../0011-lark-nyx-relay-webhook.md | 0 ...012-channel-runtime-credential-boundary.md | 0 .../0013-unified-channel-inbound-backbone.md | 0 .../0014-interactive-reply-abstraction.md | 2 +- ...5-agui-sse-projection-session-pipeline.md} | 2 +- ...-studio-member-first-published-service.md} | 2 +- docs/canon/architecture-vocabulary.md | 108 ++++++++++++++++++ ...-first-backend-implementation-checklist.md | 4 +- ...first-frontend-implementation-checklist.md | 4 +- ...-204-agui-sse-projection-session-design.md | 2 +- test/tools/test_docs_tools.sh | 6 +- tools/docs/build-index.sh | 6 +- tools/docs/lint.sh | 27 +++-- 24 files changed, 161 insertions(+), 36 deletions(-) rename docs/{decisions => adr}/0001-project-split-strategy.md (100%) rename docs/{decisions => adr}/0002-mainnet-architecture.md (100%) rename docs/{decisions => adr}/0003-kafka-transport.md (100%) rename docs/{decisions => adr}/0006-multi-agent-evolution.md (100%) rename docs/{decisions => adr}/0007-stream-forward.md (100%) rename docs/{decisions => adr}/0008-channel-runtime-multi-token-routing.md (100%) rename docs/{decisions => adr}/0009-channel-bot-callback-architecture.md (100%) rename docs/{decisions => adr}/0010-channel-phase0-provider-validation.md (100%) rename docs/{decisions => adr}/0011-lark-nyx-relay-webhook.md (100%) rename docs/{decisions => adr}/0012-channel-runtime-credential-boundary.md (100%) rename docs/{decisions => adr}/0013-unified-channel-inbound-backbone.md (100%) rename docs/{decisions => adr}/0014-interactive-reply-abstraction.md (99%) rename docs/{decisions/0011-agui-sse-projection-session-pipeline.md => adr/0015-agui-sse-projection-session-pipeline.md} (98%) rename docs/{decisions/0012-studio-member-first-published-service.md => adr/0016-studio-member-first-published-service.md} (99%) create mode 100644 docs/canon/architecture-vocabulary.md diff --git a/CLAUDE.md b/CLAUDE.md index e67519ff5..b3911c3d8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -125,11 +125,11 @@ - 新增状态/事件/持久化载荷:先定义 `.proto` 并生成类型,再接入实现;禁止先写临时结构后补 Protobuf。 ## 文档系统(强制) -- `docs/canon/` 是唯一权威参考;一个 topic 一个文件,不可有重复。 -- `docs/decisions/` 是 ADR(Architecture Decision Records),不可变,只可被新决策 supersede。 +- `docs/canon/` 是唯一权威参考;一个 topic 一个文件,不可有重复。架构评审与重构讨论统一用 [docs/canon/architecture-vocabulary.md](docs/canon/architecture-vocabulary.md) 的词汇(Module / Interface / Depth / Seam / Adapter / Leverage / Locality)。 +- `docs/adr/` 是 ADR(Architecture Decision Records),不可变,只可被新决策 supersede。文件名 `NNNN-slug.md`,编号唯一不可重用。 - `docs/history/` 存放已归档的思考快照,按月份组织,明确标记非权威。 - AI 生成的设计文档在会话结束后默认不保留到 `docs/`;需要保留的必须添加 frontmatter(title/status/owner)并放入对应目录。 -- 所有 `docs/canon/` 和 `docs/decisions/` 文件必须有 YAML frontmatter,包含 `title`、`status`、`owner` 字段。 +- 所有 `docs/canon/` 和 `docs/adr/` 文件必须有 YAML frontmatter,包含 `title`、`status`、`owner` 字段。 - Lint 操作由 `tools/docs/lint.sh` 执行,已集成到 CI 门禁。 - 根目录允许的 `.md` 文件:`CLAUDE.md`、`README.md`、`CHANGELOG.md`、`LICENSE`、`AGENTS.md`。`src/` 下各项目允许自身 `README.md`。 - `docs/README.md` 由 `tools/docs/build-index.sh` 自动生成,不手动编辑。 @@ -137,7 +137,7 @@ ## 项目结构 - `src/`:生产代码(`Aevatar.Foundation.*`、`Aevatar.AI.*`、`Aevatar.CQRS.Projection.Core.Abstractions/Runtime/Stores.Abstractions`、`src/workflow/Aevatar.Workflow.*`、`Aevatar.Host.*`)。 - `test/`:对应测试项目(单元、集成、API)。 -- `docs/`:架构文档(`canon/` 权威参考、`decisions/` ADR、`history/` 归档、`audit-scorecard/` 审计)。 +- `docs/`:架构文档(`canon/` 权威参考、`adr/` ADR、`history/` 归档、`audit-scorecard/` 审计)。 - `workflows/`:YAML 工作流定义;`tools/`:开发工具;`demos/`:示例程序。 - **CLI 项目**:`tools/Aevatar.Tools.Cli`——提到"CLI 项目"或"cli 项目"时,均指此路径。 diff --git a/README.md b/README.md index 765c3d209..d3874e5e4 100644 --- a/README.md +++ b/README.md @@ -353,7 +353,7 @@ sequenceDiagram - **Event Sourcing**: [docs/canon/event-sourcing.md](docs/canon/event-sourcing.md) — 如何开启事件溯源。 - **Connector 配置详解**: [docs/canon/connector.md](docs/canon/connector.md) — 配置格式与示例。 - **Maker 示例**: [demos/Aevatar.Demos.Maker](demos/Aevatar.Demos.Maker) — 自定义步骤类型与 MAKER 工作流。 -- **项目拆分策略**: [docs/decisions/0001-project-split-strategy.md](docs/decisions/0001-project-split-strategy.md) — 分片与拆仓路径。 +- **项目拆分策略**: [docs/adr/0001-project-split-strategy.md](docs/adr/0001-project-split-strategy.md) — 分片与拆仓路径。 --- diff --git a/docs/README.md b/docs/README.md index acb6c25b5..67b97ef76 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,6 +7,7 @@ Authoritative architecture and developer guides. Each covers one topic. - [[RFC] Aevatar Chat — Multi-Channel Adapter Architecture](canon/aevatar-channel-architecture.md) +- [Architecture Vocabulary](canon/architecture-vocabulary.md) - [Aevatar Foundation](canon/architecture.md) - [Workflow Chat API 能力说明(框架层)](canon/chat-api.md) - [Connector 配置与执行逻辑](canon/connector.md) @@ -26,15 +27,20 @@ Authoritative architecture and developer guides. Each covers one topic. Immutable records of architectural choices and their rationale. -- [Aevatar 项目拆分策略(2026-02-21)](decisions/0001-project-split-strategy.md) -- [Aevatar Mainnet 架构说明](decisions/0002-mainnet-architecture.md) -- [Orleans Kafka Provider Backend Architecture](decisions/0003-kafka-transport.md) -- [Workflow 调度 Actor 化 & 多智能体协作演进方案](decisions/0006-multi-agent-evolution.md) -- [Aevatar Stream Forward 架构说明(2026-02-22)](decisions/0007-stream-forward.md) -- [Channel Runtime Multi-Token Credential Routing](decisions/0008-channel-runtime-multi-token-routing.md) -- [Channel Bot Callback Architecture — Lessons from Lark Integration](decisions/0009-channel-bot-callback-architecture.md) -- [Channel Phase 0 Persistent Provider Validation Result](decisions/0010-channel-phase0-provider-validation.md) -- [AGUI / SSE Projection Session Pipeline](decisions/0011-agui-sse-projection-session-pipeline.md) +- [Aevatar 项目拆分策略(2026-02-21)](adr/0001-project-split-strategy.md) +- [Aevatar Mainnet 架构说明](adr/0002-mainnet-architecture.md) +- [Orleans Kafka Provider Backend Architecture](adr/0003-kafka-transport.md) +- [Workflow 调度 Actor 化 & 多智能体协作演进方案](adr/0006-multi-agent-evolution.md) +- [Aevatar Stream Forward 架构说明(2026-02-22)](adr/0007-stream-forward.md) +- [Channel Runtime Multi-Token Credential Routing](adr/0008-channel-runtime-multi-token-routing.md) +- [Channel Bot Callback Architecture — Lessons from Lark Integration](adr/0009-channel-bot-callback-architecture.md) +- [Channel Phase 0 Persistent Provider Validation Result](adr/0010-channel-phase0-provider-validation.md) +- [Lark Nyx Relay Webhook Topology](adr/0011-lark-nyx-relay-webhook.md) +- [Channel Runtime Credential Boundary](adr/0012-channel-runtime-credential-boundary.md) +- [Unified Channel Inbound Backbone](adr/0013-unified-channel-inbound-backbone.md) +- [Channel Interactive Reply Abstraction](adr/0014-interactive-reply-abstraction.md) +- [AGUI / SSE Projection Session Pipeline](adr/0015-agui-sse-projection-session-pipeline.md) +- [Studio Member-First Published Service Identity](adr/0016-studio-member-first-published-service.md) ## History diff --git a/docs/decisions/0001-project-split-strategy.md b/docs/adr/0001-project-split-strategy.md similarity index 100% rename from docs/decisions/0001-project-split-strategy.md rename to docs/adr/0001-project-split-strategy.md diff --git a/docs/decisions/0002-mainnet-architecture.md b/docs/adr/0002-mainnet-architecture.md similarity index 100% rename from docs/decisions/0002-mainnet-architecture.md rename to docs/adr/0002-mainnet-architecture.md diff --git a/docs/decisions/0003-kafka-transport.md b/docs/adr/0003-kafka-transport.md similarity index 100% rename from docs/decisions/0003-kafka-transport.md rename to docs/adr/0003-kafka-transport.md diff --git a/docs/decisions/0006-multi-agent-evolution.md b/docs/adr/0006-multi-agent-evolution.md similarity index 100% rename from docs/decisions/0006-multi-agent-evolution.md rename to docs/adr/0006-multi-agent-evolution.md diff --git a/docs/decisions/0007-stream-forward.md b/docs/adr/0007-stream-forward.md similarity index 100% rename from docs/decisions/0007-stream-forward.md rename to docs/adr/0007-stream-forward.md diff --git a/docs/decisions/0008-channel-runtime-multi-token-routing.md b/docs/adr/0008-channel-runtime-multi-token-routing.md similarity index 100% rename from docs/decisions/0008-channel-runtime-multi-token-routing.md rename to docs/adr/0008-channel-runtime-multi-token-routing.md diff --git a/docs/decisions/0009-channel-bot-callback-architecture.md b/docs/adr/0009-channel-bot-callback-architecture.md similarity index 100% rename from docs/decisions/0009-channel-bot-callback-architecture.md rename to docs/adr/0009-channel-bot-callback-architecture.md diff --git a/docs/decisions/0010-channel-phase0-provider-validation.md b/docs/adr/0010-channel-phase0-provider-validation.md similarity index 100% rename from docs/decisions/0010-channel-phase0-provider-validation.md rename to docs/adr/0010-channel-phase0-provider-validation.md diff --git a/docs/decisions/0011-lark-nyx-relay-webhook.md b/docs/adr/0011-lark-nyx-relay-webhook.md similarity index 100% rename from docs/decisions/0011-lark-nyx-relay-webhook.md rename to docs/adr/0011-lark-nyx-relay-webhook.md diff --git a/docs/decisions/0012-channel-runtime-credential-boundary.md b/docs/adr/0012-channel-runtime-credential-boundary.md similarity index 100% rename from docs/decisions/0012-channel-runtime-credential-boundary.md rename to docs/adr/0012-channel-runtime-credential-boundary.md diff --git a/docs/decisions/0013-unified-channel-inbound-backbone.md b/docs/adr/0013-unified-channel-inbound-backbone.md similarity index 100% rename from docs/decisions/0013-unified-channel-inbound-backbone.md rename to docs/adr/0013-unified-channel-inbound-backbone.md diff --git a/docs/decisions/0014-interactive-reply-abstraction.md b/docs/adr/0014-interactive-reply-abstraction.md similarity index 99% rename from docs/decisions/0014-interactive-reply-abstraction.md rename to docs/adr/0014-interactive-reply-abstraction.md index 03f91b4b3..a99fe3101 100644 --- a/docs/decisions/0014-interactive-reply-abstraction.md +++ b/docs/adr/0014-interactive-reply-abstraction.md @@ -173,5 +173,5 @@ behaviour without redeploying. - Issue `#328` — unified inbound trunk; this ADR's outbound abstraction slots into the same `IChannelOutboundPort` surface once #328 lands. - PR `#324` — inbound `card.action.trigger` processing; this ADR is its outbound dual. -- ADR-0011 (`docs/decisions/0011-lark-nyx-relay-webhook.md`) — relay webhook topology; +- ADR-0011 (`docs/adr/0011-lark-nyx-relay-webhook.md`) — relay webhook topology; not superseded, outbound completeness augments it. diff --git a/docs/decisions/0011-agui-sse-projection-session-pipeline.md b/docs/adr/0015-agui-sse-projection-session-pipeline.md similarity index 98% rename from docs/decisions/0011-agui-sse-projection-session-pipeline.md rename to docs/adr/0015-agui-sse-projection-session-pipeline.md index e5278392f..4e0b82288 100644 --- a/docs/decisions/0011-agui-sse-projection-session-pipeline.md +++ b/docs/adr/0015-agui-sse-projection-session-pipeline.md @@ -4,7 +4,7 @@ status: active owner: liyingpei --- -# ADR-0011: AGUI / SSE Projection Session Pipeline +# ADR-0015: AGUI / SSE Projection Session Pipeline ## Context diff --git a/docs/decisions/0012-studio-member-first-published-service.md b/docs/adr/0016-studio-member-first-published-service.md similarity index 99% rename from docs/decisions/0012-studio-member-first-published-service.md rename to docs/adr/0016-studio-member-first-published-service.md index 19b77a869..ff4226edc 100644 --- a/docs/decisions/0012-studio-member-first-published-service.md +++ b/docs/adr/0016-studio-member-first-published-service.md @@ -4,7 +4,7 @@ status: accepted owner: codex --- -# ADR-0012: Studio Member-First Published Service Identity +# ADR-0016: Studio Member-First Published Service Identity ## Context diff --git a/docs/canon/architecture-vocabulary.md b/docs/canon/architecture-vocabulary.md new file mode 100644 index 000000000..df4f17424 --- /dev/null +++ b/docs/canon/architecture-vocabulary.md @@ -0,0 +1,108 @@ +--- +title: "Architecture Vocabulary" +status: active +owner: eanzhao +--- + +# Architecture Vocabulary + +本文是架构评审与重构讨论时使用的统一词汇表。它把外部的 deepening / Ports & Adapters 词汇与 aevatar 内部已有的术语对齐,避免在 review 时同一个概念出现多种叫法。 + +适用场景: + +- `arch-audit`、`improve-codebase-architecture` 等架构评审 skill 的产出 +- ADR 的 Context / Decision 文段 +- PR 描述中讨论"重构动机"时 +- code review 中评估"接口该不该这样切"时 + +如果只是讲业务功能("member-first", "channel relay", "scripting"),用领域词汇即可,不必套用本表。 + +## 1. 核心词汇映射 + +| 通用词汇 | aevatar 已有术语 | 含义与口径 | +|---|---|---| +| Module | Actor / GAgent / 项目(`Aevatar..`) | 任何"接口 + 实现"的单元,尺度无关;在 aevatar 主线上最常落到 Actor / GAgent / 一个 .csproj。 | +| Interface | Port + 命令/事件 proto + ReadModel 查询契约 | "调用方必须知道才能正确使用"的全部事实:类型签名、不变式(invariant)、顺序(ordering)、错误模式、配置、性能特性。**不只是 C# `interface` 关键字。** | +| Implementation | Actor body / Adapter 内部 / `*.cs` 实现文件 | 接口背后的代码主体。**与 Adapter 区分**:一个 thing 可以是"小 adapter + 大 implementation"(比如真实 ES 仓储),也可以是"大 adapter + 小 implementation"(比如 in-memory fake)。 | +| Depth(深度) | 业务实体内聚("Actor 即业务实体"原则);"删除优先"留下来的模块 | 接口杠杆率:接口很小、覆盖的行为很多 → **deep**;接口几乎和实现一样宽 → **shallow**。aevatar 的"Actor 即业务实体(数据 + 方法同住)"就是 depth 的具体实例。 | +| Seam(接缝) | Port(如 `IActorDispatchPort`、`IEventPublisher`)+ 命令分发契约 | 一个**可替换实现的位置**:能在不改这里代码的前提下换掉行为。在 aevatar 中,Port 就是 seam。 | +| Adapter | 同义。e.g. `LocalActorPublisher`、`RuntimeBackedActorRuntime`、各种 `InMemory*` | 满足某 seam 上 Interface 的具体实现。描述**角色**(填哪个槽),不描述**实质**(里面是什么)。 | +| Leverage | "一处 Port,N 处调用 + M 处测试都获益" | Depth 给调用方的回报:一份实现服务多个调用点和测试。 | +| Locality | "单一权威拥有者"原则、"事实源唯一" | Depth 给维护者的回报:变更、bug、知识、验证集中在一个位置;改一处 → 各处自动修复。 | + +### 1.1 已经存在但容易和上面混淆的词 + +| aevatar 术语 | **不**等价于 | 区分 | +|---|---|---| +| 边界(如"Actor 边界"、"权威源边界") | Seam | "边界"在 aevatar 是**所有权 / 责任范围**(接近 DDD bounded context)。Seam 是**可替换实现的位置**。一个 actor 的边界不是 seam;它周围的 Port 才是 seam。 | +| ReadModel | Interface | ReadModel 是**查询副本**(actor-scoped current-state replica)。它的查询契约(IXxxQueryPort)才是 Interface / Seam。 | +| Projection Pipeline | Adapter | Projection 是**物化机制**(committed event → readmodel),不是某个 seam 上的具体实现。"Projection 通道下的具体投影器"才类比 Adapter。 | +| Service(如 `WriteService`、`QueryService`) | Module(无脑套用) | aevatar 的应用层契约必须承载业务语义、不是纯转发空壳;一个 "Service" 是不是 Module 取决于它的 Interface 是不是足够 deep。多数情况下,正确的形态是更窄的 `IXxxQueryPort` / `IActorDispatchPort`,而不是泛 `Service`。 | + +## 2. 关键原则(与 CLAUDE.md 已有规则的映射) + +### 2.1 Deletion test(删除测试) + +> 想象删掉这个模块。如果复杂度消失了,它就是 pass-through;如果复杂度在 N 个调用方处重新出现,它就是有价值的。 + +对应 CLAUDE.md: + +- **删除优先**:空转发、重复抽象、无业务价值代码直接删除,不保留兼容空壳。 +- **抽象一旦能被滥用即设计未完成**:允许绕过读写分离 / actor 边界 / 权威源的通用接口须继续收窄。 + +> 落地口径:在评审一个新增"中间层"时,先做删除测试。如果删除后调用方各自要重新写出同样的逻辑,保留;如果调用方只是少调一层、自己已能完成,删除。 + +### 2.2 The interface is the test surface + +> 调用方和测试穿过同一个 seam。如果想要测"接口背后"的内容,模块的形状大概率是错的。 + +对应 CLAUDE.md: + +- **读写分离**:查询走 readmodel;不暴露 actor 内部 state 或 event replay 作为查询主路径。 +- **Actor 测试通过 inbox / 行为契约**:不通过 reflection 拆 actor 内部状态字段。 + +### 2.3 One adapter = hypothetical seam, two adapters = real seam + +> 只有一个 adapter 时,seam 是想象出来的;至少要有两个 adapter(通常是 production + test)才值得引入 Port。 + +对应 CLAUDE.md: + +- **禁止预留兼容空壳**。 +- **本地可用不等于分布式正确**:仅本地 runtime 偶然细节才成立的实现视为未完成设计。 + +> 落地口径:新增 Port 时必须同时给出至少两个 Adapter(典型:runtime-backed + in-memory test fake),且两个都被实际使用。只有"未来可能要换"的 Port 不能落地。 + +### 2.4 Deep over shallow(深 vs 浅) + +> 模块要"深"——大量行为藏在小接口背后;不要"浅"——接口几乎和实现一样复杂。 + +对应 CLAUDE.md: + +- **Actor 即业务实体**:一个 actor = 一个业务实体(数据 + 方法同住);禁止按技术功能(读 / 写 / 投影)拆分同一业务实体为多个 actor。 +- **命名跟随职责**:接口 / 类型 / 目录命名描述职责边界,不泄露 `runtime/stream/protocol` 偶然细节。 + +## 3. 使用约定 + +1. **领域语言** vs **架构语言** 分开使用: + - 描述业务("member 是 Studio 的唯一主语")→ 用 `docs/canon/role-model.md` 等领域文档里的词汇。 + - 描述结构("这个 module 太 shallow,应该 deepen 进 GAgent")→ 用本表词汇。 +2. ADR 的 Context 段落用本表词汇描述当前结构问题;Decision 段落仍可使用领域语言落地。 +3. **不要混用**:同一段话里"边界"和"seam"含义清晰可分;但"boundary 边界"如果用来指可替换位置就是错的,应该改成"port"或"seam"。 +4. 中文写作时优先用括号显式给出英文术语:例如"接缝(seam)"、"深度(depth)"、"接缝(port)",避免"边界"被随意替代。 + +## 4. 词汇拒绝清单 + +下列词在架构讨论中**不要单独使用**,因为它们已经被业务或基础设施语义占用 / 含糊: + +- "boundary / 边界" 用于"可替换位置"——改用 **port / seam**。 +- "service" 用于泛指一个模块——改用 **module / port** 或具体业务名(`*GAgent`、`*Projector`)。 +- "API" 仅指类型签名——架构层面要谈 **interface**(含不变式与错误模式)。 +- "component" 指 UI 组件以外的概念——通常是 **module** 或 **adapter**。 + +## 5. 参考 + +- [Mattpocock skills - improve-codebase-architecture/LANGUAGE.md](https://github.com/mattpocock/skills/blob/main/improve-codebase-architecture/LANGUAGE.md) — 本表的外部出处。 +- Michael Feathers, *Working Effectively with Legacy Code* — seam 概念原始出处。 +- [overview.md](overview.md) — aevatar 项目主架构。 +- [architecture.md](architecture.md) — Foundation 层接口与运行时模型。 +- [cqrs-projection.md](cqrs-projection.md) — 读写分离与 Projection Pipeline。 diff --git a/docs/design/2026-04-23-studio-member-first-backend-implementation-checklist.md b/docs/design/2026-04-23-studio-member-first-backend-implementation-checklist.md index ddad74fe1..8edb4e929 100644 --- a/docs/design/2026-04-23-studio-member-first-backend-implementation-checklist.md +++ b/docs/design/2026-04-23-studio-member-first-backend-implementation-checklist.md @@ -4,7 +4,7 @@ status: draft owner: tbd last_updated: 2026-04-23 references: - - "../decisions/0012-studio-member-first-published-service.md" + - "../adr/0016-studio-member-first-published-service.md" - "./2026-04-22-team-member-first-prd.md" - "./2026-04-22-studio-member-lifecycle-spec.md" --- @@ -13,7 +13,7 @@ references: ## 0. 目标 -把 [ADR-0012](../decisions/0012-studio-member-first-published-service.md) 落成可执行的后端实施清单。 +把 [ADR-0016](../adr/0016-studio-member-first-published-service.md) 落成可执行的后端实施清单。 这份 checklist 锁定的不是视觉设计,而是后端事实模型: diff --git a/docs/design/2026-04-23-studio-member-first-frontend-implementation-checklist.md b/docs/design/2026-04-23-studio-member-first-frontend-implementation-checklist.md index c0db76ac2..b2fc0f8d8 100644 --- a/docs/design/2026-04-23-studio-member-first-frontend-implementation-checklist.md +++ b/docs/design/2026-04-23-studio-member-first-frontend-implementation-checklist.md @@ -4,7 +4,7 @@ status: draft owner: tbd last_updated: 2026-04-23 references: - - "../decisions/0012-studio-member-first-published-service.md" + - "../adr/0016-studio-member-first-published-service.md" - "./2026-04-22-studio-member-lifecycle-spec.md" - "./2026-04-23-studio-member-first-backend-implementation-checklist.md" --- @@ -13,7 +13,7 @@ references: ## 0. 目标 -把 [ADR-0012](../decisions/0012-studio-member-first-published-service.md) 转成可执行的前端实施清单。 +把 [ADR-0016](../adr/0016-studio-member-first-published-service.md) 转成可执行的前端实施清单。 这份 checklist 的核心目标有两个: diff --git a/docs/history/2026-04/2026-04-17-issue-204-agui-sse-projection-session-design.md b/docs/history/2026-04/2026-04-17-issue-204-agui-sse-projection-session-design.md index 11e40f5c6..62ab681ad 100644 --- a/docs/history/2026-04/2026-04-17-issue-204-agui-sse-projection-session-design.md +++ b/docs/history/2026-04/2026-04-17-issue-204-agui-sse-projection-session-design.md @@ -6,7 +6,7 @@ owner: liyingpei # Issue 204:AGUI / SSE Projection Session Pipeline 落地说明 -> 本文档位于 `docs/history/2026-04/`,保留的是一份“落地后回看版”说明,不再保留最初那版大而全的设计推演。当前权威口径仍以 [ADR-0011:AGUI / SSE Projection Session Pipeline](../../decisions/0011-agui-sse-projection-session-pipeline.md) 为准;本文只回答三件事:原来哪里有问题,这次代码具体怎么收了,哪些还没做。 +> 本文档位于 `docs/history/2026-04/`,保留的是一份“落地后回看版”说明,不再保留最初那版大而全的设计推演。当前权威口径仍以 [ADR-0015:AGUI / SSE Projection Session Pipeline](../../adr/0015-agui-sse-projection-session-pipeline.md) 为准;本文只回答三件事:原来哪里有问题,这次代码具体怎么收了,哪些还没做。 ## 1. 这次到底在修什么 diff --git a/test/tools/test_docs_tools.sh b/test/tools/test_docs_tools.sh index b72544650..4355a651d 100755 --- a/test/tools/test_docs_tools.sh +++ b/test/tools/test_docs_tools.sh @@ -14,7 +14,7 @@ trap "rm -rf $TMPDIR_TEST" EXIT setup_test_docs() { rm -rf "$TMPDIR_TEST/docs" - mkdir -p "$TMPDIR_TEST/docs/canon" "$TMPDIR_TEST/docs/decisions" + mkdir -p "$TMPDIR_TEST/docs/canon" "$TMPDIR_TEST/docs/adr" } run_lint() { @@ -45,7 +45,7 @@ assert_pass() { CHECKED=$((CHECKED+1)) } for file in "$DOCS_DIR"/canon/*.md; do [ -f "$file" ] && check_frontmatter "$file"; done 2>/dev/null - for file in "$DOCS_DIR"/decisions/*.md; do [ -f "$file" ] && check_frontmatter "$file"; done 2>/dev/null + for file in "$DOCS_DIR"/adr/*.md; do [ -f "$file" ] && check_frontmatter "$file"; done 2>/dev/null exit $ERRORS ' ) > /dev/null 2>&1 @@ -77,7 +77,7 @@ assert_fail() { CHECKED=$((CHECKED+1)) } for file in "$DOCS_DIR"/canon/*.md; do [ -f "$file" ] && check_frontmatter "$file"; done 2>/dev/null - for file in "$DOCS_DIR"/decisions/*.md; do [ -f "$file" ] && check_frontmatter "$file"; done 2>/dev/null + for file in "$DOCS_DIR"/adr/*.md; do [ -f "$file" ] && check_frontmatter "$file"; done 2>/dev/null exit $ERRORS ' ) > /dev/null 2>&1 diff --git a/tools/docs/build-index.sh b/tools/docs/build-index.sh index 25abb0ed7..a3f349313 100755 --- a/tools/docs/build-index.sh +++ b/tools/docs/build-index.sh @@ -42,13 +42,13 @@ extract_field() { echo "" echo "Immutable records of architectural choices and their rationale." echo "" - if [ -d "$DOCS_DIR/decisions" ]; then - for file in "$DOCS_DIR"/decisions/*.md; do + if [ -d "$DOCS_DIR/adr" ]; then + for file in "$DOCS_DIR"/adr/*.md; do [ -f "$file" ] || continue basename=$(basename "$file") title=$(extract_field "$file" "title") [ -z "$title" ] && title="$basename" - echo "- [${title}](decisions/${basename})" + echo "- [${title}](adr/${basename})" done else echo "_No decision records yet._" diff --git a/tools/docs/lint.sh b/tools/docs/lint.sh index 7129cd8eb..3b6b85e46 100755 --- a/tools/docs/lint.sh +++ b/tools/docs/lint.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # tools/docs/lint.sh — Validate docs frontmatter -# Checks: all docs/canon/ and docs/decisions/ files have required frontmatter fields +# Checks: all docs/canon/ and docs/adr/ files have required frontmatter fields # Required fields: title, status, owner # Exit 1 on first failure (CI gate mode) or accumulate all errors (report mode) set -euo pipefail @@ -46,22 +46,33 @@ if [ -d "$DOCS_DIR/canon" ]; then done fi -# Lint decision docs -if [ -d "$DOCS_DIR/decisions" ]; then - for file in "$DOCS_DIR"/decisions/*.md; do +# Lint ADR docs +if [ -d "$DOCS_DIR/adr" ]; then + for file in "$DOCS_DIR"/adr/*.md; do [ -f "$file" ] || continue check_frontmatter "$file" done fi -# Check decision file naming: must start with NNNN- -if [ -d "$DOCS_DIR/decisions" ]; then - for file in "$DOCS_DIR"/decisions/*.md; do +# Check ADR file naming: must start with NNNN- and numbers must be unique +if [ -d "$DOCS_DIR/adr" ]; then + numbers_file=$(mktemp) + trap 'rm -f "$numbers_file"' EXIT + for file in "$DOCS_DIR"/adr/*.md; do [ -f "$file" ] || continue basename=$(basename "$file") if ! echo "$basename" | grep -qE '^[0-9]{4}-'; then - echo "FAIL: docs/decisions/$basename — must start with NNNN- (e.g., 0001-topic.md)" + echo "FAIL: docs/adr/$basename — must start with NNNN- (e.g., 0001-topic.md)" ERRORS=$((ERRORS + 1)) + continue + fi + number="${basename:0:4}" + existing=$(grep "^${number} " "$numbers_file" 2>/dev/null | cut -d' ' -f2- || true) + if [ -n "$existing" ]; then + echo "FAIL: docs/adr/$basename — duplicate ADR number $number (also: $existing)" + ERRORS=$((ERRORS + 1)) + else + echo "$number $basename" >> "$numbers_file" fi done fi From d83f449ddf95d1d2ad7d92e4a47b5050b0d72ff8 Mon Sep 17 00:00:00 2001 From: eanzhao Date: Mon, 27 Apr 2026 13:59:48 +0800 Subject: [PATCH 2/2] Address PR 431 review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - vocabulary: split "接缝(seam)/ 端口(port)" so seam (concept) and port (aevatar landing form) are not aliased. - lint.sh: reject legacy docs/decisions/ directory with a git-mv hint, so branches that re-introduce the old path fail the gate instead of being silently ignored. - lint.sh + build-index.sh: honour DOCS_DIR env override so tests can drive the real scripts instead of an in-test reimplementation. - test_docs_tools.sh: invoke the real tools/docs/lint.sh via DOCS_DIR; add cases for unique ADR numbers (passes), duplicate ADR numbers (fails), missing NNNN- prefix (fails), legacy docs/decisions/ present (fails), and canonical date-prefix rejection. Build-index test now drives the real script as well. 12/12 pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/canon/architecture-vocabulary.md | 2 +- test/tools/test_docs_tools.sh | 188 +++++++++++++++----------- tools/docs/build-index.sh | 3 +- tools/docs/lint.sh | 10 +- 4 files changed, 123 insertions(+), 80 deletions(-) diff --git a/docs/canon/architecture-vocabulary.md b/docs/canon/architecture-vocabulary.md index df4f17424..16ba9108e 100644 --- a/docs/canon/architecture-vocabulary.md +++ b/docs/canon/architecture-vocabulary.md @@ -88,7 +88,7 @@ owner: eanzhao - 描述结构("这个 module 太 shallow,应该 deepen 进 GAgent")→ 用本表词汇。 2. ADR 的 Context 段落用本表词汇描述当前结构问题;Decision 段落仍可使用领域语言落地。 3. **不要混用**:同一段话里"边界"和"seam"含义清晰可分;但"boundary 边界"如果用来指可替换位置就是错的,应该改成"port"或"seam"。 -4. 中文写作时优先用括号显式给出英文术语:例如"接缝(seam)"、"深度(depth)"、"接缝(port)",避免"边界"被随意替代。 +4. 中文写作时优先用括号显式给出英文术语:例如"接缝(seam)"、"深度(depth)"、"端口(port)"。`seam` 是概念(可替换实现的位置),`port` 是这个概念在 aevatar 里最常见的落地形态——两者不要互换使用,避免"边界"被随意替代。 ## 4. 词汇拒绝清单 diff --git a/test/tools/test_docs_tools.sh b/test/tools/test_docs_tools.sh index 4355a651d..1a4bf3510 100755 --- a/test/tools/test_docs_tools.sh +++ b/test/tools/test_docs_tools.sh @@ -1,5 +1,9 @@ #!/usr/bin/env bash # test/tools/test_docs_tools.sh — Tests for tools/docs/lint.sh and tools/docs/build-index.sh +# +# Tests invoke the real scripts via DOCS_DIR override so CI guards stay in sync +# with what we exercise here. Adding a rule to lint.sh without a test below will +# leave the rule unverified. set -uo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" @@ -18,76 +22,28 @@ setup_test_docs() { } run_lint() { - # Override REPO_ROOT for lint.sh by creating a wrapper - (cd "$TMPDIR_TEST" && REPO_ROOT="$TMPDIR_TEST" bash -c ' - SCRIPT_DIR="'"$REPO_ROOT"'/tools/docs" - REPO_ROOT="'"$TMPDIR_TEST"'" - DOCS_DIR="$REPO_ROOT/docs" - source '"$LINT"' - ' 2>&1) || true + DOCS_DIR="$TMPDIR_TEST/docs" bash "$LINT" > /dev/null 2>&1 } -assert_pass() { +assert_lint_passes() { local test_name="$1" - local exit_code=0 - ( - cd "$TMPDIR_TEST" - bash -c ' - DOCS_DIR="'"$TMPDIR_TEST"'/docs" - ERRORS=0; CHECKED=0 - check_frontmatter() { - local file="$1" - if ! head -1 "$file" | grep -q "^---$"; then ERRORS=$((ERRORS+1)); return; fi - local fm=$(awk "NR==1{next} /^---$/{exit} {print}" "$file") - for field in title status owner; do - if ! echo "$fm" | grep -q "^${field}:"; then ERRORS=$((ERRORS+1)); fi - done - CHECKED=$((CHECKED+1)) - } - for file in "$DOCS_DIR"/canon/*.md; do [ -f "$file" ] && check_frontmatter "$file"; done 2>/dev/null - for file in "$DOCS_DIR"/adr/*.md; do [ -f "$file" ] && check_frontmatter "$file"; done 2>/dev/null - exit $ERRORS - ' - ) > /dev/null 2>&1 - exit_code=$? - if [ "$exit_code" -eq 0 ]; then + if run_lint; then echo " PASS: $test_name" PASS=$((PASS + 1)) else - echo " FAIL: $test_name (expected pass, got exit $exit_code)" + echo " FAIL: $test_name (expected lint pass, got fail)" FAIL=$((FAIL + 1)) fi } -assert_fail() { +assert_lint_fails() { local test_name="$1" - local exit_code=0 - ( - cd "$TMPDIR_TEST" - bash -c ' - DOCS_DIR="'"$TMPDIR_TEST"'/docs" - ERRORS=0; CHECKED=0 - check_frontmatter() { - local file="$1" - if ! head -1 "$file" | grep -q "^---$"; then ERRORS=$((ERRORS+1)); return; fi - local fm=$(awk "NR==1{next} /^---$/{exit} {print}" "$file") - for field in title status owner; do - if ! echo "$fm" | grep -q "^${field}:"; then ERRORS=$((ERRORS+1)); fi - done - CHECKED=$((CHECKED+1)) - } - for file in "$DOCS_DIR"/canon/*.md; do [ -f "$file" ] && check_frontmatter "$file"; done 2>/dev/null - for file in "$DOCS_DIR"/adr/*.md; do [ -f "$file" ] && check_frontmatter "$file"; done 2>/dev/null - exit $ERRORS - ' - ) > /dev/null 2>&1 - exit_code=$? - if [ "$exit_code" -ne 0 ]; then + if run_lint; then + echo " FAIL: $test_name (expected lint fail, got pass)" + FAIL=$((FAIL + 1)) + else echo " PASS: $test_name" PASS=$((PASS + 1)) - else - echo " FAIL: $test_name (expected fail, got pass)" - FAIL=$((FAIL + 1)) fi } @@ -106,7 +62,7 @@ owner: testuser # Test EOF -assert_pass "canonical doc with complete frontmatter" +assert_lint_passes "canonical doc with complete frontmatter" # Test 2: Missing title → FAIL echo "Test 2: Missing title fails" @@ -119,7 +75,7 @@ owner: testuser # Test EOF -assert_fail "canonical doc missing title" +assert_lint_fails "canonical doc missing title" # Test 3: Missing status → FAIL echo "Test 3: Missing status fails" @@ -132,7 +88,7 @@ owner: testuser # Test EOF -assert_fail "canonical doc missing status" +assert_lint_fails "canonical doc missing status" # Test 4: Missing owner → FAIL echo "Test 4: Missing owner fails" @@ -145,7 +101,7 @@ status: active # Test EOF -assert_fail "canonical doc missing owner" +assert_lint_fails "canonical doc missing owner" # Test 5: No frontmatter at all → FAIL echo "Test 5: No frontmatter fails" @@ -155,7 +111,7 @@ cat > "$TMPDIR_TEST/docs/canon/test.md" << 'EOF' No frontmatter here. EOF -assert_fail "canonical doc with no frontmatter" +assert_lint_fails "canonical doc with no frontmatter" # Test 6: History files without frontmatter → PASS (not checked) echo "Test 6: History files not checked" @@ -164,10 +120,99 @@ mkdir -p "$TMPDIR_TEST/docs/history/2026-03" cat > "$TMPDIR_TEST/docs/history/2026-03/test.md" << 'EOF' # Historical doc without frontmatter EOF -assert_pass "history files are not lint-checked" +assert_lint_passes "history files are not lint-checked" + +# Test 7: ADR with valid frontmatter and unique number → PASS +echo "Test 7: ADR with unique number passes" +setup_test_docs +cat > "$TMPDIR_TEST/docs/adr/0001-foo.md" << 'EOF' +--- +title: "Foo" +status: accepted +owner: testuser +--- + +# ADR-0001: Foo +EOF +cat > "$TMPDIR_TEST/docs/adr/0002-bar.md" << 'EOF' +--- +title: "Bar" +status: accepted +owner: testuser +--- + +# ADR-0002: Bar +EOF +assert_lint_passes "two ADRs with distinct numbers" + +# Test 8: Duplicate ADR numbers → FAIL +echo "Test 8: Duplicate ADR numbers fail" +setup_test_docs +cat > "$TMPDIR_TEST/docs/adr/0017-foo.md" << 'EOF' +--- +title: "Foo" +status: accepted +owner: testuser +--- + +# ADR-0017: Foo +EOF +cat > "$TMPDIR_TEST/docs/adr/0017-bar.md" << 'EOF' +--- +title: "Bar" +status: accepted +owner: testuser +--- + +# ADR-0017: Bar +EOF +assert_lint_fails "two ADRs with the same number 0017" + +# Test 9: ADR file without NNNN- prefix → FAIL +echo "Test 9: ADR without numeric prefix fails" +setup_test_docs +cat > "$TMPDIR_TEST/docs/adr/no-prefix.md" << 'EOF' +--- +title: "No prefix" +status: accepted +owner: testuser +--- + +# No prefix +EOF +assert_lint_fails "ADR without NNNN- prefix" + +# Test 10: Legacy docs/decisions/ directory → FAIL +echo "Test 10: Legacy docs/decisions/ rejected" +setup_test_docs +mkdir -p "$TMPDIR_TEST/docs/decisions" +cat > "$TMPDIR_TEST/docs/decisions/0001-old.md" << 'EOF' +--- +title: "Old" +status: accepted +owner: testuser +--- + +# ADR-0001: Old +EOF +assert_lint_fails "legacy docs/decisions/ directory present" + +# Test 11: Canonical doc with date prefix → FAIL +echo "Test 11: Canonical doc with date prefix fails" +setup_test_docs +cat > "$TMPDIR_TEST/docs/canon/2026-04-27-bad.md" << 'EOF' +--- +title: "Bad" +status: active +owner: testuser +--- + +# Bad +EOF +assert_lint_fails "canonical doc with date prefix" -# Test 7: build-index generates README -echo "Test 7: build-index generates README" +# Test 12: build-index produces README with overridden DOCS_DIR +echo "Test 12: build-index honours DOCS_DIR override" setup_test_docs cat > "$TMPDIR_TEST/docs/canon/test.md" << 'EOF' --- @@ -178,18 +223,7 @@ owner: testuser # Test EOF -# Run build-index with modified paths -( - DOCS_DIR="$TMPDIR_TEST/docs" - OUTPUT="$DOCS_DIR/README.md" - echo "# Aevatar Documentation" > "$OUTPUT" - echo "" >> "$OUTPUT" - for file in "$DOCS_DIR"/canon/*.md; do - [ -f "$file" ] || continue - title=$(sed -n '2,/^---$/p' "$file" | grep "^title:" | sed 's/^title: *//' | sed 's/^"//' | sed 's/"$//') - echo "- [$title](canon/$(basename "$file"))" >> "$OUTPUT" - done -) +DOCS_DIR="$TMPDIR_TEST/docs" bash "$BUILD_INDEX" > /dev/null 2>&1 if [ -f "$TMPDIR_TEST/docs/README.md" ] && grep -q "My Test Doc" "$TMPDIR_TEST/docs/README.md"; then echo " PASS: build-index generates README with doc titles" PASS=$((PASS + 1)) diff --git a/tools/docs/build-index.sh b/tools/docs/build-index.sh index a3f349313..bd2c6f732 100755 --- a/tools/docs/build-index.sh +++ b/tools/docs/build-index.sh @@ -4,7 +4,8 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -DOCS_DIR="$REPO_ROOT/docs" +# DOCS_DIR can be overridden for testing; defaults to repo's docs/ +DOCS_DIR="${DOCS_DIR:-$REPO_ROOT/docs}" OUTPUT="$DOCS_DIR/README.md" extract_field() { diff --git a/tools/docs/lint.sh b/tools/docs/lint.sh index 3b6b85e46..fb7767450 100755 --- a/tools/docs/lint.sh +++ b/tools/docs/lint.sh @@ -7,7 +7,8 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -DOCS_DIR="$REPO_ROOT/docs" +# DOCS_DIR can be overridden for testing; defaults to repo's docs/ +DOCS_DIR="${DOCS_DIR:-$REPO_ROOT/docs}" ERRORS=0 CHECKED=0 @@ -38,6 +39,13 @@ check_frontmatter() { CHECKED=$((CHECKED + 1)) } +# Reject legacy docs/decisions/ — must migrate to docs/adr/ +if [ -d "$DOCS_DIR/decisions" ]; then + echo "FAIL: docs/decisions/ still exists — ADRs moved to docs/adr/." + echo " Migrate with: git mv docs/decisions/.md docs/adr/.md" + ERRORS=$((ERRORS + 1)) +fi + # Lint canonical docs if [ -d "$DOCS_DIR/canon" ]; then for file in "$DOCS_DIR"/canon/*.md; do