# Prompt 管理重构 · 方案A 终版实施方案（v2，已折叠评审）

> **执行摘要**：把 21 个内置 agent 的 prompt 散文从 `specs.py` 的 Python 三引号常量外置为 `prompts/<spec.name>.md` 文件，用 import 期确定性加载器 `load_prompt` 注入；建立 `SPECS` 名册与 `SCHEMA_CATALOG`（Pydantic 类型永留 Python）；新增 `SpecResolver` 统一内置/用户 skill 的只读解析入口。**核心约束**：缓存断点前块（不变量 #9）字节级不变——金标准 fixture 必须取自 **AST literal_eval 的运行时字符串值**（而非源码文本），因为现有常量大量使用反斜杠折行，物理换行 ≠ 运行时换行。
>
> **预估工作量**：3–4 人日（@llm 步骤1–3 约 1.5 日 · @backend 步骤4–5 约 1 日 · @qa 集成回归约 0.5 日 · @devops 打包配置 + `.gitattributes` + CI wheel 冒烟约 0.5 日）。纯重构、零功能变更、零 schema 变更（前端无需 `pnpm gen:api`）。
>
> **风险等级**：**中**。唯一真正的 CRITICAL 是缓存字节稳定性（反斜杠折行 + 尾换行 + BOM + Unicode 归一化四类漂移源），已在本版用「运行时值金标准 + 文件层契约 + CI 防护」三道锁覆盖。其余为可控的协作/打包/接线问题。

> 文档类型：架构设计（**只设计、不实现**）· 语言：中文 · 适用包：`packages/agents`（@llm）、`packages/skills`（@backend）、`packages/core/orchestrator`（@llm，本波不改）、repo-root config（@devops）。
> 上游锚点：`ARCHITECTURE.md §5.1`（AgentSpec）/ `§5.6`（Skill registry）/ `§4.6`（缓存断点）；不变量 #2/#3/#9。
> 既定方向（不推翻，仅细化）：① prompt 散文外置 `.md`，spec 声明留 Python，`system_prompt = load_prompt(name)`（import 时读盘 + 内存缓存 + 确定性 + fail-fast）；② 集中注册表 `SPECS: dict[name, AgentSpec]`；③ 统一 `SpecResolver.get(name)` 内置与用户 skill 同路径，`output_schema` 这类 Pydantic 类型永远留 Python，按 name 查 `SCHEMA_CATALOG`。

---

## 0. 现状基线（重构前事实，已逐字复核）

- `specs.py` 共 804 行 = `AgentSpec` 类（39–53）+ 21 个 `*_SYSTEM_PROMPT` 多行字符串常量 + 21 个 `*_spec` 实例。
- **CRITICAL 事实（决定整套字节稳定设计）**：21 个 `*_SYSTEM_PROMPT` 常量**大量使用反斜杠行延续**（行尾 `\`）。已逐字核验 `CONTINUITY_SYSTEM_PROMPT`（行56–85）：源码看似多行，但 `\<newline>` 在 Python 解析时塌缩为空——**源码里的物理换行 ≠ 运行时字符串里的换行**。所有常量**无 f-string / `.format` / 占位符**（纯静态文本，已核验）。
- 所有 21 个 spec：`input_schema=None`（入参统一为 assemble 后的序列化文本，无结构化契约）；`scope="builtin"`；`output_schema` 为真 Pydantic 类（20 个有，`refiner` 唯一 `None`）。
- 易错命名（已核验）：`style_drift_spec`（变量名）的 `name="style"`（行270–271，非 `"style_drift"`）；文件须按 **spec.name** 命名 → `style.md`、`character-gen.md`、`golden-finger.md` 等连字符名。
- **完整消费方矩阵（6 个直接 import 点，跨 @llm / @backend 两个 owner）** —— 评审 HIGH 修正，原稿遗漏 3 处 apps/api 路由：

  | # | 文件 | import 的 spec | owner | 本波处理 |
  |---|---|---|---|---|
  | 1 | `packages/core/.../orchestrator/graph.py` `REVIEW_SPECS`（四审硬编码元组） | continuity/foreshadow/style/pace | @llm | **不改**（兼容期复用同一实例） |
  | 2 | `packages/core/.../orchestrator/generation_node.py`（§6.5 入库前 continuity 校验） | `continuity_spec` | @llm | **不改** |
  | 3 | `packages/core/.../orchestrator/chain/graph.py`（`review_specs` 默认参数 `from ..graph import REVIEW_SPECS`） | 四审 | @llm | **不改** |
  | 4 | `packages/skills/.../toolbox_registry.py`（`GeneratorTool.spec`） | 8 个生成器 `*_spec` | @backend | **改**：走 `SPECS["<name>"]` |
  | 5 | `apps/api/.../routers/toolbox.py:22` | `continuity_spec`（world-entity 入库 precheck） | @backend | **本波保留 `*_spec`**（兼容期不动），后续波次再切 |
  | 6 | `apps/api/.../routers/outline.py:23` + `style.py:25` | `outliner_spec` / `refiner_spec, style_extract_spec` | @backend | **本波保留 `*_spec`**（兼容期不动） |

  > 关键结论修正：因为 #5/#6 是 apps/api（@backend 写）import specs.py（@llm 写）的产物，**「加 agent 要改 4 处」的散落比原稿估计更广**。本波**不强行清空 `*_spec` 导出**；步骤6 的删除前置条件改为「`grep -rn '_spec' apps/api packages` 确认零消费方」（见 §5）。
- 用户 skill 已有按 name 查表机制：`SkillRegistry.get(name) -> AgentSpec`（`_to_spec` 把 DB 行转 spec，`input_schema`/`output_schema` 强制 `None`），加载时 `validate_declaration` 校验 reads/writes ⊆ `KNOWN_TABLES`。**内置 spec 未走此路径——这是要弥合的缺口。**

「加一个 agent 要改 4 处」指：①写 prompt 常量 ②写 spec 实例 ③`__init__.py` 导出 ④下游 import 注册。方案A 把 ①→文件、②③→注册表，消除 ①②③ 的散落（④ 的 apps/api 路由因兼容期保留 `*_spec` 而本波不消除，列为后续波次）。

---

## 1. 设计原则

1. **只外置「会变的散文」，不外置「类型」。** prompt 是自然语言、迭代频繁、与代码逻辑正交，外置成 `.md` 让非工程改稿不碰 Python、diff 干净。而 `output_schema`/`input_schema` 是 Pydantic 类型，承载结构化解析与 `isinstance` 校验（`generation_node.py:154`、`outline_node.py:68`、`style_extract_node.py:71`、`review_node.py:122`），**无法从文本推导、无法运行时安全构造**，必须留 Python，由 `SCHEMA_CATALOG` 作 name→type 的**唯一真相源**（见原则6）。

2. **一个 name 一个真相。** name 是全局稳定标识（LangGraph 节点 key、`state['reviews']` key、`chapter_reviews` 落库列、日志标签）。重构后 name 仍是唯一主键：`SPECS[name]`、`prompts/<name>.md`、`SCHEMA_CATALOG[name]` 三者由 name 对齐，**任何一方缺失即 fail-fast**。**name 匹配语义锁死为精确字符串相等**——无大小写折叠、无连字符归一、无模糊匹配（评审 MEDIUM）。

3. **import 时确定性加载，绝不运行时惊喜。** `load_prompt` 在模块 import 期一次性读盘 + 内存缓存：保证 `spec.system_prompt` 是完整字符串（缓存断点前块 `cache=True` 要求字节稳定，不变量 #9），且单测可复现。文件缺失 = 启动失败（fail-fast），不 fallback、不用占位符。**`load_prompt` 不做任何动态插值（无 `.format`）；任何未来需要运行期占位符的 prompt 不走 `load_prompt`，明确划在本波范围外**（评审 MEDIUM：声明此约束）。

4. **内置与用户 skill 同构、同读接口，但权限边界不同。** 二者都产出 `AgentSpec`。`SpecResolver` 提供统一 `.get(name)`：内置走纯内存 `SPECS`（**零 DB 依赖**），用户走 `SkillRegistry`（DB，必过 `validate_declaration`）。统一**读接口**，不统一**信任级别**。**`get(name)` 命中内置 name 时绝不触发任何 DB 调用**（评审 HIGH，§3.5 + §6#7 断言）。

5. **不动缓存原理，只换 prompt 来源。** 字节稳定性的责任边界不变：`system_prompt` 仍整块进 `system` 且 `cache=True`，volatile（latest_state/outline）仍在断点后。改的只是「这块文本从常量来」→「从文件来」。

6. **缓存字节稳定 = 运行时值金标准 + 文件层契约（CRITICAL，本版新增的核心原则）。** 因为常量含反斜杠折行，**金标准 fixture 必须从 AST `literal_eval` 的运行时字符串值计算 sha256，绝不从源码文本计算**。`prompts/<name>.md` 的**物理换行必须 ≡ 运行时字符串的换行**——即：导出时，凡运行时无换行处，`.md` 里就是同一物理长行（放弃反斜杠折行带来的「源码可读性」，换取「文件即运行时真相」）。这是字节稳定的根。

---

## 2. 目标目录结构（before / after）

```
packages/agents/ww_agents/
  __init__.py                  # before: 导出 21 个 *_spec + 22 schema 类 + AgentSpec
                               # after:  导出 AgentSpec(re-export from spec_model) + SPECS
                               #         + load_prompt + SCHEMA_CATALOG + schema 类
                               #         + 兼容期保留 *_spec（用 __all__/as 显式 re-export 避 F401）
  specs.py          (804行)    # before: AgentSpec 类 + 21 prompt 常量 + 21 spec 实例
- ─────────────────────────────────────────────────────────────────────
  spec_model.py     (新, ~30)  # after:  仅 AgentSpec 类定义（frozen Pydantic）
  prompt_loader.py  (新, ~55)  # after:  load_prompt(name) + PROMPTS_DIR + 缓存 + 规整 + fail-fast
  schema_catalog.py (新, ~40)  # after:  SCHEMA_CATALOG: dict[name, type|None]（唯一真相源）
  specs.py          (~140)     # after:  21 个 spec 声明（system_prompt=load_prompt(name),
                               #         output_schema=SCHEMA_CATALOG[name]）+ SPECS dict
  prompts/                     # after:  21 个外置散文（文件名 = spec.name）
    continuity.md  outliner.md  foreshadow.md  pace.md  style_extract.md
    style.md        # ← spec.name="style"（非 style_drift），易错点，单测覆盖
    refiner.md  worldbuilder.md
    character-gen.md  # ← 连字符 name，非变量名 character_gen_spec
    brainstorm.md  book-title.md  blurb.md  name.md  golden-finger.md
    glossary.md  opening.md  fine-outline.md  continue.md  expand.md
    de-ai.md  teardown.md

packages/skills/ww_skills/
  skill_registry.py            # 改：加载/入库校验期拒绝与内置 name 同名的 skill（保留命名空间前移，见 §3.5）
  spec_resolver.py  (新, ~60)  # after:  SpecResolver：合并 SPECS(内置,内存) + SkillRegistry(用户,DB)
  toolbox_registry.py          # 改：GeneratorTool.spec 改按 SPECS 取，不直接 import *_spec

# repo-root（@devops owns）
  .gitattributes               # 新增/改：prompts/*.md text eol=lf（防 CRLF 漂移）
  packages/agents/pyproject... # 改：wheel/sdist include prompts/*.md（数据文件随包分发）
  .github/workflows/ci.yml     # 改：新增「build wheel→裸装→import SPECS」冒烟步骤
```

**文件命名铁律**：`prompts/<spec.name>.md`，name 用 spec 的**连字符 name**，不是 Python 变量名。`style` agent 的 spec.name 是 `"style"` → `style.md`——已知易错点，单测必须覆盖。

---

## 3. 关键接口签名与归属包

### 3.1 `AgentSpec` 模型 — `packages/agents/ww_agents/spec_model.py`（@llm）

从 `specs.py` 原样抽出，**字段不变**（保持 frozen、`system_prompt: str` 仍是 str，不改 Callable）：

```python
class AgentSpec(BaseModel):
    model_config = ConfigDict(frozen=True, arbitrary_types_allowed=True)
    name: str
    tier: Tier
    system_prompt: str            # 仍是 str；值在 import 时由 load_prompt 填入
    input_schema: type[BaseModel] | None = None
    output_schema: type[BaseModel] | None = None
    reads: list[str] = Field(default_factory=list)
    writes: list[str] = Field(default_factory=list)
    genre: str | None = None
    scope: str = "builtin"
```

> 决策：`system_prompt` 保持 `str`（import 时求值），不改 `Callable`——缓存断点前块要确定字符串；改 Callable 会破坏 frozen/序列化，且违背确定性加载。
> **`__init__.py` re-export 纪律**（评审 MEDIUM）：`AgentSpec` 改从 `spec_model` re-export；现有 `from ww_agents import AgentSpec` 的全部消费方（`skill_registry`/`skill_permissions`/`graph`/`generation_node`/`toolbox` 路由等）**不受影响**（re-export 后 import 路径不变）。`__init__.py` 必须用显式 `__all__` 或 `import ... as ...` 重导出，避免 ruff `F401 unused-import`。§6 增 import-smoke 断言这些路径仍可用。

### 3.2 `load_prompt` — `packages/agents/ww_agents/prompt_loader.py`（@llm）

```python
PROMPTS_DIR: Final = Path(__file__).parent / "prompts"
_CACHE: dict[str, str] = {}

def load_prompt(name: str) -> str:
    """按 spec.name 读 prompts/<name>.md → 规整 → 缓存 → 返回完整 UTF-8 文本。"""
```

**规整规则（锁死缓存字节，决策级——已从「建议」升级，评审 HIGH/MEDIUM）**，依次：

1. **去 BOM**：用 `encoding="utf-8-sig"` 读（或读后剥 `﻿`）——`utf-8` **不会**自动剥 BOM，带 BOM 的 `.md` 会污染字符串首字节破缓存（评审 missingItem）。
2. **行尾归一**：`.replace("\r\n", "\n").replace("\r", "\n")`（CRLF/CR → LF）。
3. **Unicode 归一化**：`unicodedata.normalize("NFC", text)`——中文/全角标点存在 NFC/NFD 跨平台归一差异的理论缝隙，显式假定并强制 **NFC**（评审 missingItem）。
4. **尾换行策略（决策，方案 B 顺生态）**：返回 `text.rstrip("\n")`（吞掉文件尾部所有 LF，**不补回**）。这样 `.md` 文件**允许**带 ≤1 个尾换行（顺 Prettier / editorconfig / `end-of-file-fixer` 默认），而运行时字符串无尾换行（与旧常量逐字等价，旧常量均以「。」结尾、无尾换行）。

**字节稳定的双层契约（评审 HIGH：堵住「loader rstrip 吞掉差异后文件与字符串解耦」的静默漂移）**：
- **真相源** = `load_prompt` 规整后的返回值（即 `spec.system_prompt`），由 §6#5 sha256 金标准锁死。
- **文件层** 额外加一条独立断言：每个 `prompts/*.md` 的磁盘原始字节里，**尾部 LF 数量 ≤ 1**（§6#13）。这样即便有人误补多个尾换行，文件层测试先红，不会被 loader 静默吞掉。

**fail-fast**：文件不存在 → `PromptNotFoundError`（import 期崩，不 fallback、不返回 `""`）。
**`.gitattributes`**（@devops 写）：`prompts/*.md text eol=lf` —— 管行尾风格；尾换行有无由上面文件层断言守。

### 3.3 `SCHEMA_CATALOG` — `packages/agents/ww_agents/schema_catalog.py`（@llm）

**name→output type 的唯一真相源**（评审 LOW：消除 spec 字段与 catalog 双真相）。原稿的 `(input, output)` 元组**收敛为只存 output**（评审 KISS/YAGNI：21 个 input 恒 None、本波不启用，不造尚不存在的结构化入参槽）：

```python
# name → output_schema（input 全 None，本波不建入参槽——YAGNI）
SCHEMA_CATALOG: Final[dict[str, type[BaseModel] | None]] = {
    "continuity": ContinuityReview,
    "outliner":   OutlineResult,
    # ...其余 18 个真类...
    "refiner":    None,   # 唯一纯文本 writer，None 是合法值
}

def output_schema_for(name: str) -> type[BaseModel] | None:
    return SCHEMA_CATALOG[name]
```

> 未来若真出现结构化入参，再新建 `INPUT_SCHEMA_CATALOG` 或回到元组；理由记入 `decisions.md`。

### 3.4 `SPECS` 注册表 — `packages/agents/ww_agents/specs.py`（@llm）

**单向派生**：`system_prompt` 一律来自 `load_prompt(name)`，`output_schema` 一律来自 `SCHEMA_CATALOG[name]`（不再写「或直接 ContinuityReview」的二义来源，评审 LOW）：

```python
continuity_spec = AgentSpec(
    name="continuity", tier="analyst",
    system_prompt=load_prompt("continuity"),         # ← 唯一来自 loader
    output_schema=SCHEMA_CATALOG["continuity"],      # ← 唯一来自 catalog
    reads=["chapter_digests", "characters", "world_entities"], writes=[],
)
# ...其余 20 个同形...

SPECS: Final[dict[str, AgentSpec]] = {s.name: s for s in (
    continuity_spec, outliner_spec, foreshadow_spec, pace_spec, style_extract_spec,
    style_drift_spec, refiner_spec, worldbuilder_spec, character_gen_spec,
    brainstorm_spec, book_title_spec, blurb_spec, name_spec, golden_finger_spec,
    glossary_spec, opening_spec, fine_outline_spec, continue_spec, expand_spec,
    de_ai_spec, teardown_spec,
)}
assert len(SPECS) == 21, "SPECS name 冲突或缺失"   # 唯一性 + 数量自检（import 期）

# 四审受信保留名 —— 独立显式白名单（评审 MEDIUM），安全边界锚在此，不依附派生集合
REVIEW_RESERVED_NAMES: Final[frozenset[str]] = frozenset(
    {"continuity", "foreshadow", "style", "pace"}
)
```

> `*_spec` 模块变量在兼容期保留（toolbox / 节点 / apps/api 路由旧 import 不破）。**`SPECS[name] is *_spec`（同一实例）是不变量**（评审 LOW / missingItem）：兼容期绝不允许 `SPECS["continuity"]` 与 `continuity_spec` 是两个对象——否则缓存断点/日志 name 出现双真相。§6 锁定。

### 3.5 `SpecResolver` — `packages/skills/ww_skills/spec_resolver.py`（@backend）

**依赖方向**：resolver 在 @backend，向上依赖 @llm 的 `SPECS` + 自包 `SkillRegistry`。**不反向**（已核验 `ww_agents` 不依赖任何下游）。

```python
class SpecResolver:
    """内置(SPECS, 纯内存) + 用户 skill(SkillRegistry, DB) 的统一只读解析入口。"""
    def __init__(self, builtin: dict[str, AgentSpec], skills: SkillRegistry) -> None: ...

    @classmethod
    def build(cls, skills: SkillRegistry) -> "SpecResolver":
        return cls(builtin=dict(SPECS), skills=skills)   # 纯合并，不做冲突校验（已前移，见下）

    def get(self, name: str) -> AgentSpec:
        """先查内置 SPECS（纯内存、零 DB）；未命中才查 SkillRegistry；都无 → NOT_FOUND。
        内置 name 永不触发 DB 调用。"""

    def output_schema_for(self, name: str) -> type[BaseModel] | None:
        """命中内置 → SCHEMA_CATALOG[name]；纯用户 skill → None（精确 name，无模糊命中）。"""

    def names(self) -> list[str]: ...
    def list_scope(self, scope: str) -> list[AgentSpec]: ...
```

**override / 优先级裁决 —— 守卫前移（评审 HIGH，核心修正）**：
- 「内置 name 为保留命名空间、用户 skill 不可覆盖」的校验**前移到 SkillRegistry 的 skill 入库/加载校验处**（与 `validate_declaration` 同处），**不放在 `resolver.build` 的读路径**。
- 理由：放读路径会让一个坏 skill（恰好叫 `continuity`）令**每次** `build` 抛错、把安全校验耦合进写章/审稿热路径、并可能让正常请求 500；而且 `build` 若每请求重建，「拒绝成败取决于请求时 DB 状态」违背原则3 的确定性。
- 前移后：`get(name)` 对内置永远是**纯内存查表**（零 DB、零运行时分叉、确定性）；坏 skill 在**入库时**即被拒（`AppError(VALIDATION)`，错误归属清晰、早 fail-fast）。
- 安全断言锚在 **`REVIEW_RESERVED_NAMES` 显式白名单**（§3.4），不依附会随重构漂移的 `set(SPECS)`。

> **`output_schema_for`（原 `bind_output_schema`）接线说明（评审 HIGH：原稿是悬空 API）**：当前 8 个工具箱新工具的 `spec` **本就是内置实例**（`output_schema` 非 None），故「内置 name 补绑」这条路径**本波实际无触发场景**。因此：本波**保留 `output_schema_for` 作为 resolver 的只读查询方法**，但**不接线进 `run_generator`、不在 §6 断言其被调用**，明确标注「为后续『用户 skill 复用内置 schema』路径留缝」。纯用户自定义（仅 DB JSON Schema dict，无 Python 类）→ 恒 `None`，运行期 JSON-Schema→Pydantic 动态构造**明确划出本波范围**（不引入不可信类型构造）。

**编排器本波不接 resolver**：`graph.py`/`review_node.py`（@llm）当前直接用 `REVIEW_SPECS`（内置元组），本波不强制改它走 resolver——避免本波跨两个 owner 大改。resolver 首个消费方是 @backend 的 `toolbox_registry`（按 name 取 spec）。

---

## 4. 逐文件改动清单

**packages/agents（@llm，sole writer）**
- 新增 `spec_model.py`：抽出 `AgentSpec`（零行为变化）。
- 新增 `prompt_loader.py`：`PROMPTS_DIR` + `load_prompt` + `_CACHE` + `PromptNotFoundError`（去BOM / LF / NFC / rstrip尾LF / fail-fast）。
- 新增 `schema_catalog.py`：`SCHEMA_CATALOG: dict[name, type|None]`（21 项，唯一真相源）+ `output_schema_for`。
- 新增 `prompts/<name>.md` ×21：每个文件 = 对应常量**运行时字符串值**的逐字外迁（程序化导出，物理换行 ≡ 运行时换行）。
- 修改 `specs.py`：删 21 常量 + `AgentSpec` 类；`system_prompt=load_prompt(name)` + `output_schema=SCHEMA_CATALOG[name]`；建 `SPECS` + assert + `REVIEW_RESERVED_NAMES`；从 `spec_model` 重导出 `AgentSpec`。降至 ~140 行。
- 修改 `__init__.py`：显式 `__all__` 重导出 `AgentSpec`/`SPECS`/`load_prompt`/`SCHEMA_CATALOG`/schema 类 + 兼容期 `*_spec`（避 F401）。

**packages/skills（@backend，sole writer）**
- 新增 `spec_resolver.py`：`SpecResolver`（build/get/output_schema_for/names/list_scope）。
- 修改 `skill_registry.py`：**入库/加载校验期拒绝与内置 name（含 `REVIEW_RESERVED_NAMES`）同名的 skill**（保留命名空间前移）。`_to_spec` 仍 `output_schema=None` 不变。
- 修改 `toolbox_registry.py`：`GeneratorTool.spec` 改按 `SPECS["<name>"]` 取，删对 8 个生成器 `*_spec` 的直接 import；legacy `spec=None` 工具不变。

**packages/core/orchestrator（@llm）** — 本波**不改**（`REVIEW_SPECS` / 节点 / §6.5 precheck / chain 仍直接用内置 spec 实例，行为不变）。文档标注后续波次可改走 resolver。

**apps/api routers（@backend）** — 本波**不改**（`toolbox.py`/`outline.py`/`style.py` 兼容期继续 `from ww_agents import *_spec`；因 `SPECS[name] is *_spec`，拿到的是同一实例）。后续波次再切 `SPECS`。

**repo-root config（@devops，跨 owner——需 @backend/@llm 在 PROGRESS 开请求项，不可自行写）**
- 新增 `.gitattributes`：`prompts/*.md text eol=lf`（仓库当前**无** `.gitattributes`，由 @devops 新建）。
- 修改 `packages/agents` 构建配置：wheel/sdist `force-include`/package-data 显式纳入 `prompts/*.md`。
- 修改 `.github/workflows/ci.yml`：新增「build wheel → 裸环境安装 → `python -c "import ww_agents; assert ww_agents.SPECS"`」冒烟步骤。

**tests（@qa）/ 各 owner 单测** — 见 §6。

---

## 5. 有序迁移步骤（每步独立过门禁 = ruff + format + **mypy** + pytest，评审 missingItem 补 mypy）

> 原则：先建能力且与旧常量**并存等价**，验证零字节差后再删常量，最后桥接消费方。

**步骤 0 — 运行时值金标准（防漏/防孤儿/防字节漂移的锚，CRITICAL 修正）**
- 一次性脚本/测试：对每个 `*_SYSTEM_PROMPT` 取 **AST `literal_eval` 的运行时字符串值**（**不是源码文本**，因含反斜杠折行），dump `{name: sha256(runtime_value)}` 到 `tests/fixtures/prompt_hashes.json`。这是迁移正确性的唯一金标准。
- **TDD RED**：此 fixture 测试在 `prompt_loader` 实现前先提交并 RED（`load_prompt` 未实现 → ImportError 红），严格满足 RED→GREEN（评审 LOW）。

**步骤 1 — 抽 `AgentSpec` 到 `spec_model.py`**
- 纯搬移 + `specs.py`/`__init__.py` 改 import（含 `__all__` 重导出避 F401）。门禁绿（行为零变化）。

**步骤 2 — 落 `prompt_loader.py` + `prompts/*.md`（与常量并存）**
- 用脚本把每个常量的**运行时值**写入 `prompts/<name>.md`（程序化导出，物理换行 ≡ 运行时换行，杜绝手抄漂移）。
- 加测试 A：`for name: sha256(load_prompt(name)) == fixture[name]`（对齐步骤0 金标准）。
- **加测试 B（步骤2 当步守卫，评审 MEDIUM）**：`{p.stem for p in PROMPTS_DIR.glob("*.md")} == set(fixture.keys())` —— 用 fixture 的 name 全集当锚，**当步**拦住多写/少写/拼错 `.md`（不等到步骤3 建 SPECS）。
- 加测试 C（文件层）：每个 `.md` 尾部 LF ≤ 1。此刻 specs.py 仍用常量——**先证 loader≡常量逐字等价**。

**步骤 3 — 切 specs.py 到 `load_prompt` + 建 `SPECS` + `SCHEMA_CATALOG` + `REVIEW_RESERVED_NAMES`**
- 删 21 常量；`system_prompt=load_prompt(name)`、`output_schema=SCHEMA_CATALOG[name]`；建 `SPECS` + assert。
- 回归：`sha256(SPECS[name].system_prompt) == fixture[name]`（证删常量后零字节差）；`SPECS["<x>"] is <x>_spec`（同一实例）；`all(SPECS[n].output_schema is SCHEMA_CATALOG[n])`。门禁绿。

**步骤 4 — `SpecResolver` + SkillRegistry 守卫前移（skills 包）**
- 实现 resolver（内置纯内存）+ SkillRegistry 入库期拒绝同名内置。单测：内置/用户对齐、内置 `get` 零 DB、入库同名拒绝。门禁绿。

**步骤 5 — 桥接 `toolbox_registry`**
- `GeneratorTool.spec` 改走 `SPECS`；删 8 个生成器 `*_spec` 的直接 import。前端契约不变（GeneratorTool 描述符字段不变），无需 `pnpm gen:api`。门禁绿。

**步骤 6 — 清理（前置条件已修正）**
- 前置条件：`grep -rn '_spec' apps/api packages` 确认**无任何消费方**依赖 `*_spec`（含 apps/api 3 路由 + 编排器）。
- 因 apps/api 路由本波保留 `*_spec`，**本波不删 `*_spec` 导出**——仅更新 `ARCHITECTURE.md §5.1`、`memory/decisions.md`、`PROGRESS.md`，把「apps/api 路由 + 编排器切 resolver」列为后续波次。

**防孤儿 / 防漏（贯穿）**：步骤2 测试B（文件集 == fixture name 全集）+ 步骤3 三集合恒等 + 程序化导出（杜绝手抄）+ 运行时值 sha256 金标准（杜绝字节漂移）。

---

## 6. 测试计划（TDD，全程 mock，不触真 LLM）

| # | 测试 | 类型 | 断言 |
|---|---|---|---|
| 1 | 注册表唯一性 | unit | `len(SPECS)==21`；无重复 name；import `specs` 不抛 |
| 2 | md ↔ spec ↔ catalog 一一对应 | unit | `set(SPECS) == {p.stem for p in PROMPTS_DIR.glob("*.md")} == set(SCHEMA_CATALOG)`（覆盖 `style.md`/`character-gen.md` 连字符） |
| 3 | `load_prompt` 正常 + 缓存 | unit | 已知 name 返非空 UTF-8；二次调用命中 `_CACHE` |
| 4 | `load_prompt` fail-fast | unit | 未知 name → `PromptNotFoundError`；不 fallback、不返 `""` |
| 5 | **缓存前缀字节回归（核心，CRITICAL）** | unit | `sha256(SPECS[name].system_prompt) == fixture[name]`（全21），**fixture 取自 AST literal_eval 运行时值** |
| 6 | 行尾确定性 | unit | 含 CRLF/CR 的临时 md 经 `load_prompt` 规整为 LF，hash 与 LF 版一致 |
| 6b | **BOM + NFC 规整** | unit | 带 BOM 的 md → 首字节无 `﻿`；NFD 全角标点 md → 与 NFC 版 hash 一致 |
| 7 | resolver 内置/用户对齐 + **零 DB** | unit | `get("continuity")` 命中内置且**注入 fake registry 断言其未被调用**；`get("<用户skill>")` 命中 DB；都无 → `NOT_FOUND` |
| 8 | **内置保留名守卫前移** | unit | SkillRegistry **入库**与 `REVIEW_RESERVED_NAMES`/内置同名 skill → `VALIDATION`；resolver.build **不**因此抛 |
| 9 | `output_schema_for` | unit | 内置 name → `SCHEMA_CATALOG[name]` 真类；`refiner` → `None`；纯用户 skill → `None`；**拼错近似 name（`character_gen`）→ None 不抛、不误命中 `character-gen`** |
| 10 | 边界：refiner 纯文本 | unit | `SPECS["refiner"].output_schema is None`；`SCHEMA_CATALOG["refiner"] is None`；§2 一致性不因 None 误判 |
| 11 | toolbox 绑定 | unit | 每个新工具 `GeneratorTool.spec is SPECS[key]`；legacy 工具 `spec is None` 不变 |
| 12 | 编排器无回归 | integration | review/generation/outline/style_extract 节点用 `SPECS` 内置 spec，mock gateway，system 块文本 == 旧行为 |
| 13 | **文件层尾换行契约** | unit | 每个 `prompts/*.md` 磁盘原始字节尾部 LF ≤ 1 |
| 14 | **同一实例不变量** | unit | `SPECS["continuity"] is continuity_spec`；`REVIEW_RESERVED_NAMES ⊆ set(SPECS)`，且这4个 `scope=="builtin"` 且 `writes==[]` |
| 15 | **import-smoke（apps/api + 编排器）** | integration(@backend/@qa) | `from ww_agents import AgentSpec/outliner_spec/refiner_spec/style_extract_spec/continuity_spec` 不抛，且拿到的对象 `is SPECS[name]` |
| 16 | **打包冒烟** | CI(@devops) | build wheel → 裸装 → `import ww_agents; assert ww_agents.SPECS` 不 `PromptNotFoundError`（防 `.md` 漏带，源码树 pytest 测不出） |

覆盖率沿用全局 ≥80%。所有 LLM 调用注 fake gateway（CLAUDE.md TDD 纪律）。

---

## 7. 风险表与缓解

| 风险 | 等级 | 说明 | 缓解 |
|---|---|---|---|
| **反斜杠折行 → 运行时换行塌缩 → 金标准取错来源** | CRITICAL | 常量含 `\<newline>`，源码物理换行 ≠ 运行时换行；若 fixture 从源码文本算 hash，会「看起来对、字节不对」静默破缓存 | 金标准 fixture 取 **AST `literal_eval` 运行时值**；导出时 `.md` 物理换行 ≡ 运行时换行（§6#5/原则6） |
| 缓存字节漂移（行尾/BOM/NFC/尾换行） | CRITICAL | CRLF/BOM/NFD/尾空行使 `system_prompt` 字节变化 → 缓存失配/语义微变（不变量#9） | `load_prompt` 去BOM(`utf-8-sig`)+LF归一+NFC+rstrip尾LF；§6#5/#6/#6b/#13；`.gitattributes eol=lf`（@devops） |
| 尾换行被编辑器/Prettier/end-of-file-fixer 注入 | HIGH | 工具默认补尾 `\n`，与「字节稳定」对抗 | 决策方案B（顺生态）：md 允许 ≤1 尾换行，loader `rstrip("\n")`；**双层契约**——文件层 §6#13 守「尾LF≤1」、运行时 §6#5 守真相源，二者解耦的静默漂移被堵 |
| SpecResolver 引入运行时不确定性 / 安全校验耦合热路径 | HIGH | build 每请求重建 + 读路径校验同名 → 解析结果依赖 DB 当下、坏 skill 让正常请求 500 | 守卫**前移**到 SkillRegistry 入库校验；`get` 内置纯内存零 DB（§3.5 / §6#7/#8） |
| `output_schema` 误外置 / bind 悬空无接线 | HIGH | 类型无法从文本推导；`output_schema_for` 原稿无消费方 | schema 永留 `SCHEMA_CATALOG`（唯一真相）；`output_schema_for` 本波**不接线、不断言被调用**，明标为后续留缝 |
| 消费方遗漏（apps/api 3 路由 + §6.5 precheck） | HIGH | 原稿漏点 → 步骤6 删 `*_spec` 会破 apps/api import | §0 补全 6 点矩阵；apps/api 本波保留 `*_spec`；步骤6 前置改为全仓 grep；§6#15 import-smoke |
| name 漂移破坏落库映射 / 节点 key | HIGH | 改名或文件名拼错（`style`≠`style_drift`、连字符） | name 冻结 + 精确匹配；§6#2 三集合恒等；程序化导出按 spec.name；`REVIEW_SPECS` 不改序 |
| 用户 skill 覆盖内置 name 偷换四审 prompt | HIGH（安全） | 不可信输入冒用 `continuity` 等 | SkillRegistry 入库期拒同名（锚 `REVIEW_RESERVED_NAMES` 显式白名单，非派生集合）；§6#8/#14 |
| **打包遗漏 `.md`（源码树 pytest 假绿）** | HIGH（升级） | wheel 默认不打非-.py 数据文件；CI 用源码树 pytest 测不出，fail-fast 仅在裸装时触发 | @devops 在构建配置 force-include `prompts/*.md`；CI 加「build wheel→裸装→import」冒烟（§4 / §6#16） |
| 跨 owner 改动（@llm specs ↔ @backend resolver/toolbox ↔ @devops config） | MEDIUM | 依赖方向 + `.gitattributes`/打包/CI 属 @devops | 依赖单向（skills→agents）；`.gitattributes`/pyproject/CI 由 @devops 实施，@llm/@backend 在 PROGRESS 开请求项 |
| 迁移分步中途半成品 | MEDIUM | 步骤2/3 之间 | 步骤2 先证 loader≡常量（运行时值 sha256）+ 目录完整性守卫（测试B），步骤3 才删常量；每步独立过门禁（含 mypy） |
| git 历史追溯 | LOW | 删 prompt 行后 `git log -p specs.py` 追不到旧演进 | 迁移 commit body 注明「prompt 逐字外迁至 prompts/<name>.md，运行时值 sha256 见 fixture」；后续改动落 `prompts/*.md` 更聚焦 |

---

## 8. 协作落点（多 agent 纪律）

**`memory/contracts.md`**（C6 Agent specification 下登记）：
- `SpecResolver.get(name) -> AgentSpec` **是契约**（@backend 供 resolver + 入库守卫，@llm 供 `SPECS`/`SCHEMA_CATALOG`/`REVIEW_RESERVED_NAMES`/`load_prompt`）。约束：① 内置 name 为保留命名空间、不可被用户 skill 覆盖（守卫在 **SkillRegistry 入库期**）；② `get` 内置纯内存、零 DB；③ `load_prompt` import 期确定性（去BOM/LF/NFC/rstrip尾LF/fail-fast）；④ `SPECS[name] is *_spec` 兼容期同一实例。标 `稳定` 后 toolbox / 后续波次才动。
- C1（LLM 网关）**不改**（`LlmRequest`/`Block.cache` 不动；`system_prompt` 仍整块 `cache=True`）。

**`memory/decisions.md`**（append，一条一事实）：
- 「prompt 外置方案A：散文→`prompts/<spec.name>.md`；`load_prompt` import 期读盘+缓存+去BOM(utf-8-sig)+LF归一+NFC+`rstrip('\n')`+fail-fast；**金标准 fixture 取 AST literal_eval 运行时值**（因常量含反斜杠折行，源码物理换行≠运行时换行）；`.md` 物理换行≡运行时换行；尾换行决策=方案B（md 允许≤1 尾LF，loader rstrip 不补回，文件层断言守 尾LF≤1）。」
- 「`SCHEMA_CATALOG` 收敛为 `dict[name, type|None]`（去掉恒 None 的 input 槽，YAGNI）；是 name→output 唯一真相源，`SPECS[name].output_schema` 由它派生。」
- 「内置 name（含 `REVIEW_RESERVED_NAMES`=continuity/foreshadow/style/pace）为保留命名空间，守卫**前移至 SkillRegistry 入库校验**（非 resolver 读路径），用户 skill 同名 → `VALIDATION`（安全边界，呼应不变量#3）。」
- 「`load_prompt` 不支持运行期占位符/插值；需插值的 prompt 不走此路径，属本波范围外。」

**`memory/gotchas.md`**：`prompts/*.md` 文件名按 **spec.name（连字符）**，非 Python 变量名；`style` agent 的 name 是 `"style"` → `style.md`；常量含反斜杠折行——**外迁/比对必须用运行时值**，别用源码文本；`.gitattributes` 锁 `prompts/*.md text eol=lf`；wheel 必须 force-include `prompts/*.md`，否则裸装 import 崩（源码树 pytest 测不出）。

**`PROGRESS.md` 任务拆分**（新增一个 wave，每个 `🔵→✅` 独立过门禁，含 mypy）：
1. `@llm` — 步骤 0–3（fixture 取运行时值 / spec_model / loader / prompts / SPECS / SCHEMA_CATALOG / REVIEW_RESERVED_NAMES；删常量；sha256 回归）。
2. `@backend` — 步骤 4–5（SpecResolver + SkillRegistry 入库守卫前移 + toolbox 桥接）。**依赖** 第1项 `SPECS` 稳定后开工。
3. `@qa` — §6#12/#15 集成回归（编排器无回归 + apps/api import-smoke）。
4. `@devops` — `.gitattributes` + wheel package-data + CI wheel 冒烟（§6#16）。**与第1项并行可起，CI 冒烟依赖第1项 prompts 落盘。**
5. `@llm`/`@backend`（**可选后续波**）— apps/api 3 路由 + 编排器节点改走 resolver，然后才删 `*_spec` 导出。

**跨所有权请求项（评审 missingItem，原稿漏）**：本方案**需要 @devops 触碰 repo-root config**（`.gitattributes` 新建 + `packages/agents` 打包配置 + CI）——@llm/@backend **不可自行写**这些文件，须在 `PROGRESS.md` 给 @devops 开请求项。@llm 只动 `packages/agents`，@backend 只动 `packages/skills`。对 `packages/shared`/OpenAPI **零触碰**（GeneratorTool 描述符字段不变 → 前端无需 `pnpm gen:api`）。

---

## 9. 边界情形显式处理

1. **`refiner`（无 output_schema 纯文本 writer）**：走 `load_prompt("refiner")` + `SPECS["refiner"]`；`output_schema=None` 合法，`SCHEMA_CATALOG["refiner"]=None`。§6#2 把 key 存在当断言（不断言值非 None）；节点侧 `if expected is not None: isinstance(...)` 逻辑不变。
2. **input_schema 全 None**：21 个 `input_schema` 恒 None（入参为序列化文本）。`SCHEMA_CATALOG` **本波只存 output**（不造尚不存在的 input 槽，YAGNI）；未来需要时另建 `INPUT_SCHEMA_CATALOG`，理由记 decisions。
3. **toolbox `GeneratorTool` 绑定**：8 个新工具 `spec` 改指 `SPECS[key]`（同一实例，§6#11）；3 个 legacy `spec=None` 不变。
4. **用户 skill 与内置 schema 对齐**：用户 skill 经 `SkillRegistry` 出 `output_schema=None`；`output_schema_for(name)` 仅当 name **精确命中**内置 `SCHEMA_CATALOG` 时返真类，否则 None。**name 精确匹配**（无大小写/连字符归一）：拼错的近似 name（如 `character_gen` vs `character-gen`）→ 返 None 不报错，**这是预期行为非 bug**（§6#9）。本波该方法**不接线**，纯自定义 JSON-Schema→Pydantic 动态构造划出本波范围。
5. **name 比较语义统一**：入库守卫（拒同名）与 `output_schema_for`（补绑）用**同一套精确字符串相等**比较，无二义。

---

## 10. 评审回应（逐条处理 CRITICAL / HIGH findings）

裁决：架构不变量+缓存=`changes_requested` · 项目规约=`approve` · 完备性=`changes_requested`。下列意见已全部折叠进 v2。

### 视角一：架构不变量 + 缓存前缀字节稳定性

**[CRITICAL] 反斜杠行延续 → 运行时换行塌缩，金标准取错来源** —— **采纳（核心修正）**。已核验 `CONTINUITY_SYSTEM_PROMPT` 含 `\<newline>`，源码物理换行 ≠ 运行时换行。处理：① 新增**原则6**「运行时值金标准 + 文件层契约」；② 步骤0 fixture 改为取 **AST `literal_eval` 运行时值**算 sha256（§5 步骤0）；③ 锁死「`.md` 物理换行 ≡ 运行时字符串换行」（导出时凡运行时无换行处即同一物理长行，放弃反斜杠折行的源码可读性）；④ §6#5 断言明确「fixture 取自运行时值」；⑤ 风险表升为头号 CRITICAL；⑥ decisions/gotchas 记明。

**[HIGH] 尾换行策略与生态工具冲突** —— **采纳（选方案B）**。处理：① §3.2 尾换行从「建议」升级为**决策**——md 允许 ≤1 尾换行、loader `rstrip("\n")` 不补回；② **双层契约**：文件层 §6#13 断言「尾LF≤1」+ 运行时 §6#5 守真相源，堵住「rstrip 吞差异后文件与字符串解耦」的静默漂移；③ 风险表第3行 + decisions 记明。

**[HIGH] SpecResolver 引入运行时不确定性 / 安全校验耦合热路径** —— **采纳（守卫前移）**。处理：① §3.5 把「内置保留名守卫」**前移到 SkillRegistry 入库校验**，`resolver.build` 仅纯合并不校验；② `get` 内置纯内存、零 DB；③ §6#7 加「内置 get 注入 fake registry 断言其未被调用」；④ §6#8 断言入库期拒绝、build 不抛；⑤ §4 `skill_registry.py` 列入改动。

**[MEDIUM] 四审受信 name 缺独立显式白名单** —— **采纳**。§3.4 新增 `REVIEW_RESERVED_NAMES` 显式常量；安全校验锚此（非 `set(SPECS)`）；§6#14 断言 `⊆ set(SPECS)` 且这4个 `scope=="builtin"`、`writes==[]`。

**[MEDIUM] 打包分发风险被低估 / 源码树 pytest 假绿** —— **采纳（升级为 HIGH + 开工前必做）**。风险表升级；§4 列 @devops 在构建配置 force-include `prompts/*.md`；§6#16 加「build wheel→裸装→import」CI 冒烟；§8 列 @devops 请求项。

**[LOW] schema_catalog 与 spec 实例双真相** —— **采纳**。§3.3/§3.4 定死单向派生：`SPECS[name].output_schema` 一律来自 `SCHEMA_CATALOG[name]`，catalog 为唯一真相；§6 步骤3 断言 `all(SPECS[n].output_schema is SCHEMA_CATALOG[n])`。

**[missingItem] BOM / Unicode NFC 归一化** —— **采纳**。§3.2 规整加 `utf-8-sig` 去BOM + `unicodedata.normalize("NFC")`；§6#6b 断言；decisions 声明假定 NFC。

### 视角二：项目规约审查（verdict: approve）

**[LOW] 全部 6 条（目录所有权/行数/TDD/命名不可变/协作/KISS-YAGNI/fail-fast 时机）** —— 合规，**采纳其改进建议**：① TDD RED——步骤0 fixture 在 loader 前先提交 RED（§5 步骤0）；② SCHEMA_CATALOG 收敛为只存 output dict（去恒 None 的 input 槽，§3.3，YAGNI）；③ 确认 catalog 非第二真相源（§3.3/§3.4 单向派生）；④ fail-fast 时机——守卫前移入库期（§3.5，与视角一 HIGH 同向）。

**[missingItem] mypy 未点名** —— **采纳**：§5「每步独立过门禁」明确含 `mypy packages apps`。
**[missingItem] `__init__.py` re-export 的 F401** —— **采纳**：§3.1 + §4 要求显式 `__all__`/`as` 重导出避 F401。
**[missingItem] `SPECS[name] is *_spec` 同一实例（覆盖 REVIEW_SPECS）** —— **采纳**：§3.4 列为不变量，§6#14 断言。
**[missingItem] `.gitattributes` 跨 owner（@devops）** —— **采纳**：§4/§8 明确 `.gitattributes`/打包/CI 归 @devops，@llm/@backend 开 PROGRESS 请求项（仓库当前无 `.gitattributes`，已核验）。

### 视角三：完备性批评（漏了什么）

**[HIGH] 消费方清单漏 3 个 apps/api 路由** —— **采纳**。已核验 `toolbox.py:22`/`outline.py:23`/`style.py:25` import 内置 `*_spec`。处理：① §0 补全 **6 点消费方矩阵**（含 owner + 本波处理）；② 修正「无需跨 owner」——apps/api 路由切换需 @backend 自有目录实施，本波**保留 `*_spec` 不动**；③ 步骤6 删除前置条件改为「全仓 grep（含 apps/api）确认零 `*_spec` import」；④ §6#15 import-smoke。

**[HIGH] `bind_output_schema` 悬空 API 无接线** —— **采纳**。处理：① 重命名为 `output_schema_for`（去掉「bind」暗示的写语义）；② §3.5 明确本波**无触发场景**（8 个新工具的 spec 本就是内置实例、output 非 None），故**保留为只读查询但不接线进 `run_generator`、不断言被调用**，标为后续留缝；③ §9.4 同步。

**[MEDIUM] name 精确匹配语义 + 拼错近似 name 失败模式** —— **采纳**。原则2 + §9.4/§9.5 声明**精确字符串相等**（无大小写/连字符归一）；§9.4 列「拼错近似 name → 返 None 不报错，是预期行为」；§6#9 加单测（`character_gen` → None，不误命中 `character-gen`）；入库守卫与补绑用同一套比较。

**[MEDIUM] 步骤2「独立过门禁」缺 prompts 目录完整性守卫** —— **采纳**。§5 步骤2 加测试B：`{md stem} == set(fixture.keys())`（用 fixture name 全集当锚），**当步**拦多写/少写/拼错 md，不等步骤3。

**[MEDIUM] 尾换行策略仍是「建议」+ 未声明无插值假设** —— **采纳**。§3.2 升级为**决策**（方案B，见视角一 HIGH）；原则3 + decisions 声明「`load_prompt` 不支持运行期占位符/插值，需插值的 prompt 属本波外」。

**[LOW] 回归未覆盖 apps/api 路由 + §6.5 precheck continuity 复用 + 同一实例断言** —— **采纳**。§6#14（`SPECS["continuity"] is continuity_spec`）+ §6#15（apps/api import-smoke，拿到对象 `is SPECS[name]`）；已核验 `generation_node.py` §6.5 precheck 复用 `continuity_spec`，因同一实例不变量而无回归。

**[missingItem] `__all__` 大改破坏面** —— **采纳**：§3.1 说明 `AgentSpec` 改 re-export 后 `from ww_agents import AgentSpec` 的全部现有消费方（skill_registry/skill_permissions/graph/generation_node/toolbox 路由）import 路径不变、不受影响；§6#15 import-smoke 覆盖。

---

## 附：相关文件路径（绝对）

- 现状声明：`packages/agents/ww_agents/specs.py`、`.../__init__.py`、`.../schemas.py`
- 用户 skill（resolver 对齐基准 + 守卫前移落点）：`packages/skills/ww_skills/skill_registry.py`、`.../skill_permissions.py`、`.../toolbox.py`、`.../toolbox_registry.py`
- 编排器消费点（本波不改）：`packages/core/ww_core/orchestrator/{graph.py,review_node.py,generation_node.py,outline_node.py,style_extract_node.py,collect.py}`、`.../chain/graph.py`
- apps/api 路由消费点（本波保留 `*_spec`，评审补全）：`apps/api/ww_api/routers/{toolbox.py,outline.py,style.py}`
- 待外置目标目录（新建）：`packages/agents/ww_agents/prompts/`
- @devops 配置落点：`.gitattributes`（新建）、`packages/agents` 打包配置、`.github/workflows/ci.yml`
- 协作回写：`memory/{contracts.md,decisions.md,gotchas.md}`、`PROGRESS.md`、`ARCHITECTURE.md`（§5.1）
