Harness Engineering 搭建指南(六):总指南 + 自动化层 — 全局编织与自检闭环

概述

系列定位:在项目中要搭建出完整的Harness Engineering四层文档体系,基本分为6步,接下来我会通过本系列共 6 篇文章,指导从零搭建 Harness Engineering 四层文档体系,最终实现从 BRD/PRD 到代码交付的一站式 AI 开发与自检闭环。

本篇定位:第 6 步(最终步)------编写统领全局的 AI Coding 指南,建立 Linter 自动化拦截和自动化治理脚本,实现从 BRD/PRD 到 PR 交付的完整自检闭环。


1. 为什么最后一步是"总指南 + 自动化层"

1.1 本步要解决的两个问题

问题 解决方式 产出
四层文档体系分散,缺乏全局视角 编写 AI Coding 总指南,编织四层文档的全景索引和协作流程 docs/ai-coding-guide.md
AI Agent 的产出仅靠主观判断,缺乏客观验证 建立 Linter 自动拦截 + 自动化治理脚本,实现客观数据驱动的自检闭环 build/linter/ + build/automation/scripts/

1.2 为什么必须最后做

复制代码
Step 1~5 产出约 30 个文档/文件
         │
         ▼
Step 6: 将这 30 个文件编织成一个全局索引
        + 建立自动化机制验证这些文件的执行效果
  • 总指南需要引用全部已有文档:如果在其他文档未就位时编写,索引会不完整
  • Linter 规则需要与 Rules 对齐:Rules 层(Step 2)定义了约束,Linter 配置是这些约束的自动化执行
  • 自动化脚本需要引用全部四层:文档一致性检测需要扫描全部 .md/.yaml/.ps1/.xml 文件

2. 需要创建的文件清单

复制代码
docs/
└── ai-coding-guide.md                  # AI Coding 规范化开发总指南(全局索引)

build/
├── linter/
│   ├── pmd-exchange-ruleset.xml        # PMD 代码质量规则集
│   └── checkstyle-exchange.xml        # Checkstyle 代码风格规则集
│
└── automation/
    └── scripts/
        ├── pre-check.ps1               # 环境前置检查
        ├── worktree-manager.ps1        # Git Worktree 自动化管理
        ├── collect-sidecar.ps1          # 客观侧性数据采集
        ├── decay-detector.ps1           # 自动衰减检测
        ├── governance-report.ps1        # 持续治理报告生成
        └── index-sync-check.ps1        # 文档全景索引一致性检测

共 8 个文件(1 个总指南 + 2 个 Linter 配置 + 5~6 个脚本)。


3. 第一部分:AI Coding 总指南

3.1 docs/ai-coding-guide.md -人类参考文档

定位

这是整个 Harness Engineering 体系的顶层文档 ,核心定位是"人类参考文档 ",不是"AI 执行文档"。它不重复四层文档的内容,而是编织四层的协作关系:

  • §1 Harness 理念:核心思想、设计目标
  • §2 四层文档体系:四层的关系图和职责划分
  • §3 PRD 驱动开发全链路:7 步流程的详细说明和按需加载清单
  • §4 按需加载策略:每步加载哪些文档、预估 token
  • §5 文档全景索引:全部文件的目录树(5.1~5.5)
  • §6 核心技术约束速查always-on.md 的速查表
  • §7 实战示例:以一个完整案例展示端到端流程
  • §8 文档元信息规范:front matter 格式
  • §9 协作机制总结:开发者与 AI Agent 的交互流程图

3.2 必须包含的章节

§1 Harness Engineering 理念
markdown 复制代码
## 1. Harness Engineering 理念

### 1.1 核心思想
Harness Engineering(驾驭式工程化)是一种让 AI Agent 在严格约束下精准产出代码
的工程方法论。其核心原则是:**AI 生成,人类驾驭**------通过分层文档体系为 AI 提供
充分的上下文知识和硬性约束,同时通过人工确认门禁确保关键决策由人类把控。

### 1.2 设计目标
| 目标 | 实现方式 |
|------|---------|
| 精准性 | Rules 层硬性约束 + Skills 层操作手册 |
| 可控性 | PRD → 设计 → 评审 → 编码 → 自检 全链路门禁 |
| 高效性 | 分步按需加载策略,单步 token ≤ 3000 |
| 一致性 | docs/ 知识层统一管理,人机共读 |
§2 四层文档体系

用 ASCII 图展示四层关系:

markdown 复制代码
## 2. 四层文档体系

┌─────────────────────────────────────────────────────┐
│                  AGENTS.md                          │
│         项目入口 · 技术栈基线 · 快速导航              │
├─────────────┬───────────────┬───────────────────────┤
│  Rules 层    │  Skills 层    │  Wiki 层 (docs/)      │
│  硬性约束    │  操作手册      │  知识库               │
│  .qoder/     │  .qoder/      │  docs/                │
│  rules/      │  skills/      │                       │
├─────────────┴───────────────┴───────────────────────┤
│              Changes 层 (docs/changes/)              │
│         变更管理 · 影响分析 · PR 自检                │
└─────────────────────────────────────────────────────┘

每层用表格列出文件清单、路径和约束内容(引用 Step 1~5 已创建的文件)。

§3 PRD 驱动开发全链路

这是总指南的核心章节,描述从 PRD 输入到交付的完整 7 步流程:

markdown 复制代码
## 3. PRD 驱动开发全链路

### 3.1 流程总览

Step 0: 环境检查与输入校验 → Step 1: PRD解析 → Step 2: 影响分析 → Step 3: 技术设计文档
→ Step 4: 设计评审(人工门禁)→ Step 5: 代码生成 → Step 5.5: 编译验证(含测试代码)→ Step 6: 配置与文档同步
→ Step 7: 交付前自检 → 交付

> **铁律**:禁止跳过环境检查与输入校验;禁止跳过技术设计文档直接写代码;禁止跳过设计评审直接生成代码;
> 禁止跳过自检直接交付。

### 3.2 分步详解与文档调用清单

#### Step 1: PRD 解析与上下文加载
| 文档 | 用途 |
|------|------|
| AGENTS.md | 获取项目概览与技术栈基线 |
| docs/reference/prd-template.md | 对照 PRD 模板解析需求结构 |
| docs/reference/brd-to-prd-guide.md | (条件)BRD→PRD 转换参考 |

产出:需求解析摘要

{...Step 2~7 类似}
§4 按需加载策略
markdown 复制代码
## 4. 按需加载策略

### 4.1 设计原则
- Rules 层:始终在上下文中(IDE 自动注入)
- Skills 层:仅在对应 Step 触发时加载
- Wiki 层:按步骤加载该步骤所需文档
- 单步控制:单个 Step 的按需加载总量控制在 3000 token 以内

### 4.2 各步骤加载清单与预估 token

| 步骤 | 按需加载 | 预估 token |
|------|---------|-----------|
| Step 1 PRD解析 | AGENTS.md + prd-template.md | ~1200 |
| Step 2 影响分析 | boundaries.md + impact-analysis.md | ~2000 |
| Step 3 设计文档 | _template.md + data-flow.md + feature-*.md | ~3000 |
| Step 4 设计评审 | design-review.md | ~1000 |
| Step 5 代码生成 | 按需触发 add-*.md | ~800 |
| Step 6 文档同步 | error-codes.md + api.spec.yaml | ~1500 |
| Step 7 自检 | auto-governance.md + pr-checklist.md | ~1600 |
§5 文档全景索引

这是总指南最重要的维护章节,列出全部文件的目录树:

markdown 复制代码
## 5. 文档全景索引

### 5.1 指令层(.qoder/)
{列出 .qoder/rules/ 和 .qoder/skills/ 下的全部文件}

### 5.2 知识层(docs/)
{列出 docs/ 下的全部 .md 和 .yaml 文件}

### 5.3 项目入口
{列出项目根目录下的 AGENTS.md、README.md、CHANGELOG.md}

### 5.4 自动化层脚本
{列出 build/automation/scripts/ 下的全部 .ps1 文件}

### 5.5 Linter 规则配置
{列出 build/linter/ 下的全部 .xml 文件}

关键规则:凡是新增或删除 .md/.yaml/.ps1/.xml 文件时,必须同步更新此章节的文件树。

§7 实战示例

以一个完整案例(如"新增供应商对接")展示端到端流程:

markdown 复制代码
## 7. 实战示例:新增供应商对接

### 7.1 用户提供 PRD
开发者参照 prd-template.md 编写 PRD(业务人员填 Part A)。

### 7.2 AI Agent 执行流程

Step 1: 加载 AGENTS.md + prd-template.md → 解析 PRD
        ↓
Step 2: 加载 boundaries.md + impact-analysis.md → 输出影响分析
        ↓
Step 3: 加载 _template.md + data-flow.md → 生成设计文档 (draft)
        ↓
Step 4: 加载 design-review.md → 7 维度评审
        → ⚠️ 暂停,等待用户确认
        → 用户确认「通过」→ status 改为 active
        ↓
Step 5: 加载 add-vendor-adapter.md → 按序生成代码
        ↓
Step 5.5: 编译验证(含测试代码)
        ↓
Step 6: 加载 error-codes.md + api.spec.yaml → 同步文档
        ↓
Step 7: 加载 auto-governance.md + pr-checklist.md → 自检
        → 交付
§9 协作机制总结

用 ASCII 时序图展示开发者与 AI Agent 的交互流程:

markdown 复制代码
## 9. 协作机制总结

开发者                    AI Agent                    文档体系
  │                          │                          │
  │  1. 提交 PRD             │                          │
  │ ──────────────────────▶ │                          │
  │                          │  2. 加载文档(按需)       │
  │                          │ ◀────────────────────── │
  │                          │  3. 生成设计文档          │
  │                          │ ──────────────────────▶ │
  │  4. 评审报告 + 设计摘要   │                          │
  │ ◀────────────────────── │                          │
  │  5. 人工确认「通过」      │                          │
  │ ──────────────────────▶ │                          │
  │                          │  6. 生成代码 + 测试       │
  │                          │  7. 同步文档 + 自检       │
  │  8. 自检结果 + 交付       │                          │
  │ ◀────────────────────── │                          │

3.3 编写要点

  1. 总指南是"索引"不是"副本":引用四层文档,不重复其内容
  2. 全景索引必须完整:每新增/删除文件都要更新 §5
  3. 实战示例要端到端:从 PRD 输入到 PR 交付的完整流程
  4. 面向人而非AI:核心定位是"人类参考文档",不是"AI 执行文档"

实际示例

通过agent生成

markdown 复制代码
---
last_updated: 2026-06-29
status: active
owner: @zhangsan
---

# AI Coding 规范化开发指南

> **定位**:本文档是 app-exchange-port 项目使用 AI Coding 进行一站式规范化开发的总体指南,
> 基于 Harness Engineering 理念,将 PRD 驱动开发全链路中涉及的所有 .md 文档统一编排,
> 为开发者提供从需求输入到代码交付的完整操作手册。

---

## 1. Harness Engineering 理念

### 1.1 核心思想

Harness Engineering(驾驭式工程化)是一种让 AI Agent 在严格约束下精准产出代码的工程方法论。
其核心原则是:**AI 生成,人类驾驭**------通过分层文档体系为 AI 提供充分的上下文知识和硬性约束,
同时通过人工确认门禁确保关键决策由人类把控。

### 1.2 设计目标

| 目标 | 实现方式 |
|------|---------|
| **精准性** --- AI 生成的代码符合项目技术栈和架构规范 | Rules 层硬性约束 + Skills 层操作手册 |
| **可控性** --- 开发流程可追溯、可审查、可回退 | PRD → 设计 → 评审 → 编码 → 自检 全链路门禁 |
| **高效性** --- 减少 AI 上下文膨胀,按需加载文档 | 分步按需加载策略,单步 token ≤ 3000 |
| **一致性** --- 人机共读同一套知识库 | docs/ 知识层统一管理架构、规范、设计文档 |

---

## 2. 四层文档体系

项目文档分为四层,各层职责明确、协同运作:


\```
┌─────────────────────────────────────────────────────┐
│                  AGENTS.md                          │
│         项目入口 · 技术栈基线 · 快速导航              │
├─────────────┬───────────────┬───────────────────────┤
│  Rules 层    │  Skills 层    │  Wiki 层 (docs/)      │
│  硬性约束    │  操作手册      │  知识库               │
│  .qoder/     │  .qoder/      │  docs/                │
│  rules/      │  skills/      │                       │
├─────────────┴───────────────┴───────────────────────┤
│              Changes 层 (docs/changes/)              │
│         变更管理 · 影响分析 · PR 自检                │
└─────────────────────────────────────────────────────┘
\```

### 2.1 Rules 层 --- 硬性约束(始终生效)

Rules 层文件由 IDE 自动注入 AI Agent 上下文,无需手动加载,在所有任务中始终生效。

| 文件 | 路径 | 约束内容 |
|------|------|---------|
| 技术硬约束 | `.qoder/rules/always-on.md` | 技术栈锁定、模块依赖方向、依赖注入、日志、HTTP、数据库、线程池、限流、traceId、事务、配置、注释语言、代码行数限制(共 13 条) |
| Agent 工作流 | `.qoder/rules/agent-workflow.md` | 强制流程顺序(含 BRD→PRD)、设计文档要求、评审门禁、按需加载策略、代码生成约束、交付自检、文档同步、自动化层自我验证(共 8 节) |
| Java 代码风格 | `.qoder/rules/java-code-style.md` | 包结构、类命名(18 类型)、Lombok 使用、异常处理、import 规范、供应商扩展规范 |
| 测试规则 | `.qoder/rules/java-testing.md` | 测试分类(单元/集成/架构)、JUnit 5 + Mockito + ArchUnit 框架、命名规范、Mock 策略、集成测试规则、架构测试规则、覆盖要求、禁止项 |

### 2.2 Skills 层 --- 操作手册(按需触发)

Skills 层是 AI Agent 的操作手册,仅在对应开发步骤触发时加载。

| 技能 | 路径 | 触发场景 |
|------|------|---------|
| PRD 转代码(主技能) | `.qoder/skills/prd-to-code.md` | 用户提供 PRD 要求开发时 |
| 技术设计评审 | `.qoder/skills/design-review.md` | 设计文档生成后、代码生成前 |
| 新增供应商适配器 | `.qoder/skills/add-vendor-adapter.md` | PRD 要求对接新供应商时 |
| 新增 Entity/Mapper/Manager | `.qoder/skills/add-entity-mapper.md` | PRD 要求新增数据库表时 |
| 新增 API 接口 | `.qoder/skills/add-api-endpoint.md` | PRD 要求新增对外接口时 |
| 文档健康度扫描 | `.qoder/skills/doc-gardening.md` | 提交前检查文档健康度时 |

### 2.3 Wiki 层 --- 知识库(人机共读)

Wiki 层位于 `docs/` 目录,提供项目架构、编码规范、设计文档等上下文知识,供 AI Agent 按需加载。

| 子目录 | 内容 | 关键文件 |
|--------|------|---------|
| `docs/architecture/` | 系统架构 | `overview.md`(架构概览)、`boundaries.md`(模块边界)、`data-flow.md`(数据流转) |
| `docs/conventions/` | 编码规范 | `README.md`(总览)、`naming.md`(命名)、`error-handling.md`(错误处理)、`testing.md`(测试)、`logging.md`(日志) |
| `docs/design/` | 功能设计文档 | `_template.md`(设计模板)、`feature-*.md`(各功能设计文档) |
| `docs/reference/` | 参考资料库 | `prd-template.md`(PRD 模板,Part A/B 分层)、`brd-to-prd-guide.md`(BRD→PRD 转换指南)、`api.spec.yaml`(API 规范)、`error-codes.md`(错误码) |
| `docs/plans/` | 迭代计划 | `current-sprint.md`(当前迭代)、`backlog.md`(待办) |

### 2.4 Changes 层 --- 变更管理

Changes 层确保代码变更的可追溯性、一致性和安全性。

| 文件 | 路径 | 用途 |
|------|------|------|
| PR 自检清单 | `docs/changes/pr-checklist.md` | 代码生成后逐项自检(规则合规、代码质量、测试、文档同步、提交规范) |
| 影响分析指南 | `docs/changes/impact-analysis.md` | 变更前分析影响范围(模块影响矩阵、变更类型、文档关联检查) |
| 变更记录 | `CHANGELOG.md`(项目根) | 记录每次变更历史 |

---

## 3. PRD 驱动开发全链路

### 3.1 流程总览

\```
Step 0: 环境检查与输入校验 → Step 1: PRD解析 → Step 2: 影响分析 → Step 3: 技术设计文档
→ Step 4: 设计评审(人工门禁)→ Step 5: 代码生成 → Step 5.5: 编译验证(含测试代码)→ Step 6: 配置与文档同步
→ Step 7: 交付前自检 → 交付
\```

> **铁律**:禁止跳过环境检查与输入校验;禁止跳过技术设计文档直接写代码;禁止跳过设计评审直接生成代码;禁止跳过自检直接交付。

### 3.2 分步详解与文档调用清单

#### Step 0: 环境检查与输入校验

**目标**:检查环境就绪,创建隔离工作区,校验输入文档完整性(含 BRD→PRD 转换)。

**按需加载文档**:

| 文档 | 用途 |
|------|------|
| `build/automation/scripts/pre-check.ps1` | JDK/Maven/Git 环境校验 |
| `docs/reference/prd-template.md` 第 0 节 | PRD 最低完整性要求校验 |
| `docs/reference/brd-to-prd-guide.md` | (条件)如输入为 BRD,转换为 PRD 结构 |

**产出**:就绪环境 + 隔离 Git worktree + 校验通过的 PRD。PRD 必填项缺失则暂停等待用户补充。

---

#### Step 1: PRD 解析与上下文加载

**目标**:解析 PRD 需求,提取功能目标、输入输出、业务规则。

**按需加载文档**:

| 文档 | 用途 |
|------|------|
| `AGENTS.md` | 获取项目概览与技术栈基线 |
| `docs/reference/prd-template.md` | 对照 PRD 模板(Part A/B 分层)解析需求结构 |
| `docs/reference/brd-to-prd-guide.md` | (条件)如 PRD 中出现技术方案关键词,参照指南判断是否需移至设计阶段 |

**产出**:需求解析摘要,标记缺失信息为"待补充"。如 PRD Part B 留空,标记为"AI Agent 推导"。

---

#### Step 2: 影响分析

**目标**:确定功能归属模块,列出新增/修改文件清单。

**按需加载文档**:

| 文档 | 用途 |
|------|------|
| `docs/architecture/boundaries.md` | 模块边界与依赖方向约束 |
| `docs/changes/impact-analysis.md` | 影响分析模板与模块影响矩阵 |
| `docs/reference/api.spec.yaml`(条件) | 仅 PRD 涉及 API 变更时读取相关段 |

**产出**:影响分析报告(变更模块、新增文件、修改文件、数据库变更、配置变更、文档同步、回归风险)。

---

#### Step 3: 技术设计文档生成

**目标**:基于模板生成技术设计文档,保存到 `docs/design/feature-{功能名}.md`。

**按需加载文档**:

| 文档 | 用途 |
|------|------|
| `docs/design/_template.md` | 技术设计文档模板 |
| `docs/architecture/data-flow.md` | 数据流转路径参考 |
| `docs/design/feature-*.md`(1~2 个相关) | 相似功能的设计参考 |

**产出**:`docs/design/feature-{功能名}.md`,`status: draft`,包含:目标、范围、分层设计、数据库约束、API 约定、配置项、测试要求、风险与对策、自检清单。

---

#### Step 4: 设计评审(人工确认门禁)

**目标**:对技术设计文档进行 7 维度评审,通过后方可进入代码生成。

**按需加载文档**:

| 文档 | 用途 |
|------|------|
| `.qoder/skills/design-review.md` | 评审维度与检查清单 |

**评审 7 维度**:

1. **架构合规性** --- 模块归属、依赖方向、跨层引用
2. **类设计合理性** --- 基类复用、注解规范、命名规范
3. **数据库设计** --- 表名前缀、标准字段、索引、DDL 完整性
4. **API 设计** --- 路径风格、DTO 结构、错误码、文档同步标注
5. **测试方案** --- 单元测试/集成测试/架构测试覆盖、Mock 策略、命名规范
6. **配置与文档同步** --- Nacos 配置、文档更新标注
7. **风险评估** --- 技术风险、回归风险、缓解措施

> **人工确认门禁**:评审完成后必须暂停,将设计文档摘要 + 评审报告呈现给用户,等待明确确认:
> - 用户确认「通过」→ 设计文档 `status` 改为 `active`,继续 Step 5
> - 用户要求修改 → 修复后重新提交评审
> - 用户要求重新设计 → 返回 Step 3
>
> **禁止在未获得用户确认前自动进入代码生成阶段。**

---

#### Step 5: 代码生成

**目标**:按模块依赖方向自底向上生成代码,严格遵守 Rules 层全部规则。

**按需加载文档**(按需触发对应 Skill):

| 条件 | 加载 Skill |
|------|-----------|
| 新增数据库表 | `.qoder/skills/add-entity-mapper.md` |
| 新增供应商对接 | `.qoder/skills/add-vendor-adapter.md` |
| 新增 API 接口 | `.qoder/skills/add-api-endpoint.md` |

**代码生成顺序**(严格遵守依赖方向):

\```
1. dal 层     Entity → Mapper → Manager → Mapper XML
2. business 层  接口/抽象类 → 核心实现(@Component + @Slf4j)
3. ext 层      供应商适配器(InputConvertor / HttpResultParser / PortResultConvertor)
4. app 层      配置注册(BizConfiguration.preInitAppConfig)+ Trigger 编排
5. runner/soa 层  Controller / Service 暴露
6. 测试代码    JUnit 5 + Mockito 单元测试
\```

**代码生成约束**:

- 每个 Java 文件必须有中文类注释 + `@author` 标注
- 必须同步生成 JUnit 5 + Mockito 单元测试
- 测试覆盖:正常路径、参数校验失败、异常路径、边界条件
- 禁止使用 `@Transactional`,使用 `TransactionTemplate` 编程式事务
- 禁止直接注入 Mapper,使用 `DalManager.of(beanName)`
- JSON 统一使用 FastJSON,HTTP 统一使用 OkHttp3 + HttpClientFactory
- 代码生成后运行 Linter 自动检查:`mvn pmd:check checkstyle:check`,报错消息含三要素(问题/修复/文档),AI Agent 可自动修复
- 架构依赖校验运行 ArchUnit 测试:`mvn test -pl exchange-test -Dtest=ArchitectureRulesTest`

---

#### Step 6: 配置与文档同步

**目标**:同步更新受影响的配置和文档。

**按需加载文档**:

| 文档 | 用途 |
|------|------|
| `docs/reference/error-codes.md` | 登记新增错误码 |
| `docs/reference/api.spec.yaml` | 新增/修改 API 定义 |

**同步清单**:

| 变更类型 | 需更新的文档 |
|---------|-------------|
| 新增/修改错误码 | `docs/reference/error-codes.md` + `CodeEnum.java` |
| 新增/修改 API | `docs/reference/api.spec.yaml` |
| 新增功能 | `docs/design/feature-*.md`(status → active)+ `CHANGELOG.md` |
| 修改模块结构 | `docs/architecture/boundaries.md` |
| 修改数据流转 | `docs/architecture/data-flow.md` |

---

#### Step 7: 交付前自检

**目标**:逐项执行 PR 自检清单,确认通过后方可交付。

**按需加载文档**:

| 文档 | 用途 |
|------|------|
| `docs/changes/pr-checklist.md` | PR 提交前自检清单 |

**自检维度**:

1. **规则合规** --- 依赖方向、日志、注入、Entity 注解、JSON、HTTP、数据库、线程池、事务、限流、traceId、配置
2. **代码质量** --- 类注释、方法注释、空 catch、异常日志格式、import、单文件行数
3. **测试检查** --- JUnit 5 + Mockito、命名、覆盖场景、Mock 策略、无外部依赖
4. **文档同步** --- 错误码、API、设计文档、last_updated、CHANGELOG
5. **提交规范** --- commit message、PR 粒度、Git hooks

---

## 4. 按需加载策略

### 4.1 设计原则

为避免 AI Agent 上下文膨胀导致 token 浪费和生成质量下降,采用分步按需加载策略:

- **Rules 层**:始终在上下文中(IDE 自动注入),不计入按需加载
- **Skills 层**:仅在对应 Step 触发时加载
- **Wiki 层**:按步骤加载该步骤所需文档,禁止预读后续步骤文档
- **单步控制**:单个 Step 的按需加载总量控制在 3000 token 以内

### 4.2 各步骤加载清单与预估 token

| 步骤 | 按需加载 | 预估 token |
|------|---------|-----------|
| Step 1 PRD 解析 | `AGENTS.md` + `prd-template.md` +(条件)`brd-to-prd-guide.md` | ~800~1200 |
| Step 2 影响分析 | `boundaries.md` + `impact-analysis.md` +(条件)`api.spec.yaml` | ~1500~3500 |
| Step 3 设计文档 | `_template.md` + `data-flow.md` + 1~2 个 `feature-*.md` | ~3000 |
| Step 4 设计评审 | `design-review.md` | ~1000 |
| Step 5 代码生成 | 按需触发 `add-*.md`(如需) | ~800 |
| Step 6 文档同步 | `error-codes.md` + `api.spec.yaml`(相关段) | ~1500 |
| Step 7 自检 | `pr-checklist.md` | ~600 |

### 4.3 加载规则

1. 禁止在 Step 1 预读 Step 2~7 的文档
2. 每个 Step 完成后,可释放前序步骤的文档上下文
3. 如某 Step 发现需要回溯前序文档,按需补读即可
4. Rules 层始终在上下文中(IDE 自动注入),不计入按需加载
5. Skills 层仅在对应 Step 触发时加载

---

## 5. 文档全景索引

### 5.1 指令层(.qoder/)

\```
.qoder/
├── rules/                          # Rules 层 --- 始终生效
│   ├── always-on.md                # 技术硬约束(13 条)
│   ├── agent-workflow.md           # AI Agent 工作流(8 节,含自我验证)
│   ├── java-code-style.md          # Java 代码风格
│   └── java-testing.md             # 测试规则
└── skills/                         # Skills 层 --- 按需触发
    ├── prd-to-code.md              # PRD 转代码(主技能,7 步流程 + Step 5.5 编译门禁)
    ├── flow-orchestration.md      # 全流程编排与断点恢复(流程图 + 断点恢复 + 人工介入点 + 失败处理总表)
    ├── design-review.md            # 设计评审(7 维度门禁)
    ├── add-vendor-adapter.md       # 新增供应商适配器(8 步)
    ├── add-entity-mapper.md        # 新增 Entity/Mapper/Manager(7 步)
    ├── add-api-endpoint.md         # 新增 API 接口(2 场景)
    ├── doc-gardening.md            # 文档健康度扫描(5 步)
    └── auto-governance.md          # 自动治理巡检(5 步:采集→检测→自动修复→建议→报告)
\```

### 5.2 知识层(docs/)

\```
docs/
├── ai-coding-guide.md              # AI Coding 规范化开发指南(本文档)
├── architecture/                   # 架构知识
│   ├── overview.md                 # 系统架构概览
│   ├── boundaries.md               # 模块边界与依赖约束
│   ├── data-flow.md                # 数据流转(同步/异步/文件)
│   └── code-assets.md             # 代码资产清单(基类/接口/工厂 + Manager 双模式决策树)
├── conventions/                    # 编码规范
│   ├── README.md                   # 规范总览
│   ├── naming.md                   # 命名规范
│   ├── error-handling.md           # 错误处理规范
│   ├── testing.md                  # 测试规范
│   ├── logging.md                  # 日志规范
│   └── linter-rules.md             # Linter 自动化规则集(PMD/Checkstyle/ArchUnit/Maven Enforcer)
├── design/                         # 功能设计文档
│   ├── _template.md                # 设计文档模板
│   ├── feature-sync-single-request.md      # 同步单笔请求
│   ├── feature-async-batch-sharding.md     # 异步批量分片
│   ├── feature-vendor-adapter.md           # 供应商适配器
│   ├── feature-rate-limiting.md            # 限流
│   ├── feature-scheduler-framework.md      # 调度框架
│   ├── feature-trace-propagation.md        # traceId 传播
│   ├── feature-high-concurrency-query.md   # 高并发查询
│   └── feature-automation-layer.md          # 自动化层设计文档
├── reference/                      # 参考资料
│   ├── prd-template.md             # PRD 输入模板(Part A 业务层 + Part B 技术层)
│   ├── brd-to-prd-guide.md         # BRD→PRD 转换指南(What vs How)
│   ├── api.spec.yaml               # API 规范(OpenAPI 3.0)
│   ├── error-codes.md              # 错误码定义
│   └── prd-high-Single-request.md  # 高并发查询 PRD 示例(正确填写示范)
├── changes/                        # 变更管理
│   ├── README.md                   # 变更管理层说明
│   ├── pr-checklist.md             # PR 自检清单
│   └── impact-analysis.md          # 影响分析指南
├── plans/                          # 迭代计划
│   ├── current-sprint.md           # 当前迭代
│   └── backlog.md                  # 待办列表
└── guides/                         # Harness Engineering 搭建指南
    ├── harness-overview.md         # 搭建指南总览
    ├── harness-step1-project-entry-and-architecture.md
    ├── harness-step2-rules-layer.md
    ├── harness-step3-wiki-layer.md
    ├── harness-step4-skills-layer.md
    ├── harness-step5-changes-layer.md
    └── harness-step6-guide-and-automation.md
\```

### 5.3 项目入口

\```
项目根/
├── AGENTS.md                       # AI Agent 项目入口(技术栈基线 + 快速导航)
├── README.md                       # 项目整体说明
└── CHANGELOG.md                    # 变更记录
\```

### 5.4 自动化层脚本(build/automation/scripts/)

\```
build/automation/scripts/
├── pre-check.ps1                  # 环境前置检查(JDK/Maven/Git/项目结构 5 项检查)
├── worktree-manager.ps1           # Git Worktree 自动化管理(create/list/clean/status)
├── collect-sidecar.ps1             # 客观侧性数据采集(构建/测试/Linter 结果 → JSON)
├── decay-detector.ps1             # 自动衰减检测(文档过期/代码-文档漂移/规则命中率/技术栈对齐)
├── governance-report.ps1          # 持续治理报告生成(汇总侧性数据 + 衰减报告 → Markdown)
└── index-sync-check.ps1           # 文档全景索引一致性检测(磁盘文件 vs §5 索引比对)
\```

### 5.5 Linter 规则配置(build/linter/)

\```
build/linter/
├── pmd-exchange-ruleset.xml       # PMD 代码质量规则集(16 条规则,对应 linter-rules.md)
└── checkstyle-exchange.xml        # Checkstyle 代码风格规则集(11 条规则,对应 linter-rules.md)
\```

---

## 6. 核心技术约束速查

以下约束在 `always-on.md` 中完整定义,AI Agent 在代码生成时必须严格遵守:

| 约束项 | 规则 | 禁止 |
|--------|------|------|
| 技术栈 | JDK 1.8 / Spring Boot 2.3.12 / MyBatis-Plus 3.4.3 | Java 9+ 语法、Spring 3.x、MP 3.5+ |
| JSON | FastJSON 1.2.83 | Jackson / Gson |
| HTTP | OkHttp3 + HttpClientFactory | RestTemplate / Apache HttpClient |
| 数据库 | DalManager.of(beanName) | 直接注入 Mapper |
| 事务 | TransactionTemplate 编程式 | @Transactional 注解 |
| 日志 | @Slf4j + 占位符格式 | LoggerFactory / System.out |
| 依赖注入 | @Resource / 构造器注入 | 字段级 @Autowired |
| 线程池 | BatchExecutorTemplate / ThreadPoolExecutorBuilder | 裸 new ThreadPoolExecutor |
| 限流 | Resilience4j + RateLimitFactory | 自行实现限流 |
| traceId | @MDC + ContextHolder / MDCTemplate | 线程切换丢失 traceId |
| 配置 | Nacos | 硬编码环境地址 / 密钥 |
| 模块依赖 | common → dal → business → ext → app → runner | 反向依赖 / 跨层引用 |

---

## 7. 实战示例:新增供应商对接

以下以"新增一个供应商适配器"为例,展示完整的 AI Coding 开发流程:

### 7.1 用户提供 PRD

开发者参照 `docs/reference/prd-template.md` 编写 PRD(业务人员填 Part A,技术细节留空由 AI Agent 推导),可参考 `docs/reference/brd-to-prd-guide.md` 了解 BRD→PRD 转换要点。编写完成后提交给 AI Agent。

### 7.2 AI Agent 执行流程

\```
Step 1: 加载 AGENTS.md + prd-template.md → 解析 PRD
        ↓
Step 2: 加载 boundaries.md + impact-analysis.md → 输出影响分析
        · 确定需新增 exchange-ext-{供应商} 子模块
        · 确定需修改 BizConfiguration.preInitAppConfig()
        ↓
Step 3: 加载 _template.md + data-flow.md + feature-vendor-adapter.md
        → 生成 docs/design/feature-{供应商}.md (status: draft)
        ↓
Step 4: 加载 design-review.md → 7 维度评审
        → ⚠️ 暂停,输出评审报告,等待用户确认
        → 用户确认「通过」→ status 改为 active
        ↓
Step 5: 加载 add-vendor-adapter.md → 按序生成代码
        · dal 层: Entity → Mapper → Manager(如需自有结果表)
        · ext 层: InputConvertor → HttpResultParser → PortResultConvertor
        · app 层: BizConfiguration 注册 PortConfig
        · 测试: JUnit 5 + Mockito 单元测试
        ↓
Step 6: 加载 error-codes.md + api.spec.yaml → 同步文档
        · 新增错误码登记
        · 新增 API 定义(如需)
        · 更新 CHANGELOG.md
        ↓
Step 7: 加载 pr-checklist.md → 逐项自检
        · 规则合规 ✓ · 代码质量 ✓ · 测试覆盖 ✓ · 文档同步 ✓ · 提交规范 ✓
        → 交付
\```

### 7.3 提交后

执行 `/doc-gardening` 检查文档健康度,确保所有 .md 文件的 front matter 完整、日期更新。

---

## 8. 文档元信息规范

所有项目 .md 文件头部必须包含 YAML front matter:

```yaml
---
last_updated: YYYY-MM-DD
status: active          # active | deprecated | draft
owner: @username
---
\```

| 字段 | 说明 |
|------|------|
| `last_updated` | 最后更新日期,格式 YYYY-MM-DD |
| `status` | 文档状态:`active`(生效中)/ `draft`(草稿)/ `deprecated`(已废弃) |
| `owner` | 文档负责人 |

**过期检测规则**(由 `/doc-gardening` 执行):

- `last_updated` 距今 > 90 天 → 标记为 **stale**(过期)
- `last_updated` 距今 > 180 天 → 标记为 **critical**(严重过期)
- `status: draft` → 标记为 **待发布**
- `status: deprecated` → 标记为 **已废弃**
- `owner` 缺失 → 标记为 **orphan**(无主文档)

---

## 9. 协作机制总结

\```
开发者                    AI Agent                    文档体系
  │                          │                          │
  │  1. 提交 PRD             │                          │
  │ ──────────────────────▶ │                          │
  │                          │  2. 加载 AGENTS.md       │
  │                          │     + prd-template.md    │
  │                          │ ◀────────────────────── │
  │                          │  3. 加载 boundaries.md   │
  │                          │     + impact-analysis.md │
  │                          │ ◀────────────────────── │
  │                          │  4. 加载 _template.md    │
  │                          │     + data-flow.md       │
  │                          │ ◀────────────────────── │
  │                          │  5. 生成设计文档          │
  │                          │ ──────────────────────▶ │
  │                          │  6. 加载 design-review   │
  │                          │ ◀────────────────────── │
  │  7. 评审报告 + 设计摘要   │                          │
  │ ◀────────────────────── │                          │
  │                          │                          │
  │  8. 人工确认「通过」      │                          │
  │ ──────────────────────▶ │                          │
  │                          │  9. 加载 add-*.md skill  │
  │                          │ ◀────────────────────── │
  │                          │ 10. 生成代码 + 测试       │
  │                          │ 11. 同步文档              │
  │                          │ ──────────────────────▶ │
  │                          │ 12. 加载 pr-checklist    │
  │                          │ ◀────────────────────── │
  │  13. 自检结果 + 交付      │                          │
  │ ◀────────────────────── │                          │
\```

**核心设计**:AI Agent 负责加载文档、生成代码和同步文档;开发者负责提供 PRD、确认设计评审和验收交付。文档体系是双方共享的知识桥梁,Rules 层约束 AI 行为,Skills 层指导 AI 操作,Wiki 层提供上下文,Changes 层保障变更质量。

4. 第二部分:Linter 自动化规则配置

4.1 定位

Linter 配置是 Rules 层硬性约束的自动化执行器always-on.md 中定义的规则,通过 PMD/Checkstyle/ArchUnit/Maven Enforcer 自动拦截。

4.2 PMD 代码质量规则集(pmd-exchange-ruleset.xml)

always-on.md 中的规则映射为 PMD 规则:

always-on.md 规则 PMD 规则 阻塞级别
禁止 System.out.println SystemPrintln error
禁止空 catch 块 EmptyCatchBlock error
禁止字符串拼接日志 GuardLogStatement warning
单文件 ≤ 300 行 ExcessiveLength warning
单方法 ≤ 80 行 ExcessiveMethodLength warning
禁止未使用 import UnusedImports error
XML 模板
xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<ruleset name="Exchange Ruleset"
         xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0
         https://pmd.sourceforge.io/ruleset_2_0_0.xsd">

    <description>Exchange project PMD rules</description>

    <!-- 禁止 System.out.println -->
    <rule ref="category/java/bestpractices.xml/SystemPrintln">
        <priority>1</priority>
    </rule>

    <!-- 禁止空 catch 块 -->
    <rule ref="category/java/errorprone.xml/EmptyCatchBlock">
        <priority>1</priority>
    </rule>

    <!-- 单文件 ≤ 300 行 -->
    <rule ref="category/java/design.xml/ExcessiveLength">
        <properties>
            <property name="minimum" value="300"/>
        </properties>
        <priority>2</priority>
    </rule>

    <!-- 单方法 ≤ 80 行 -->
    <rule ref="category/java/design.xml/ExcessiveMethodLength">
        <properties>
            <property name="minimum" value="80"/>
        </properties>
        <priority>2</priority>
    </rule>
</ruleset>
实际示例

通过agent生成

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!--
  PMD 代码质量规则集 --- app-exchange-port 项目
  基于 .qoder/rules/always-on.md + java-code-style.md + java-testing.md + docs/conventions/ 中的禁止规则
  每条规则的 message 遵循三要素格式:问题是什么 | 怎么修 | 去哪看文档
  当 AI Agent 看到 PMD 报错时,无需额外提示即可根据 message 自动修复
-->
<ruleset name="exchange-pmd-ruleset"
         xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 http://pmd.sourceforge.net/ruleset_2_0_0.xsd">

    <description>
        app-exchange-port 项目 PMD 代码质量规则集。
        覆盖日志规范、依赖注入、HTTP 调用、数据库访问、线程池、事务、异常处理、Lombok、测试规范共 9 大类 16 条规则。
        规则 ID 格式:EXCHANGE-{类别}-{序号},对应 .md 文档中的禁止规则。
    </description>

    <!-- ================================================================ -->
    <!-- 一、日志规范(来源:always-on.md#4, docs/conventions/logging.md)   -->
    <!-- ================================================================ -->

    <!-- EXCHANGE-LOG-001: 禁止 LoggerFactory.getLogger() -->
    <rule name="ExchangeNoLoggerFactory"
          language="java"
          message="[EXCHANGE-LOG-001] 禁止使用 LoggerFactory.getLogger()。问题:违反日志规范,项目统一使用 Lombok @Slf4j,禁止手动创建 Logger 实例。修复:1.在类上添加 @Slf4j 注解(import lombok.extern.slf4j.Slf4j) 2.删除 private static final Logger xxx = LoggerFactory.getLogger(...) 声明 3.将类中所有 xxx.error/warn/info/debug(...) 调用替换为 log.error/warn/info/debug(...) 4.删除 org.slf4j.Logger 和 org.slf4j.LoggerFactory 的 import。文档:.qoder/rules/always-on.md 第4节日志规范, docs/conventions/logging.md"
          class="net.sourceforge.pmd.lang.rule.XPathRule">
        <description>禁止使用 LoggerFactory.getLogger() 创建 Logger,必须使用 Lombok @Slf4j 注解</description>
        <priority>1</priority>
        <properties>
            <property name="xpath">
                <value><![CDATA[
                    //PrimaryExpression/PrimaryPrefix/Name[
                        @Image='LoggerFactory.getLogger'
                        or ends-with(@Image, '.LoggerFactory.getLogger')
                    ]
                ]]></value>
            </property>
        </properties>
    </rule>

    <!-- EXCHANGE-LOG-002: 禁止 System.out.println() / System.err.println() -->
    <rule name="ExchangeNoSystemOut"
          language="java"
          message="[EXCHANGE-LOG-002] 禁止使用 System.out.println() 或 System.err.println()。问题:违反日志规范,必须使用 @Slf4j 的 log.info/warn/error 记录日志,System.out 不走日志框架、无法关联 traceId、生产环境无法控制级别。修复:1.确认类上有 @Slf4j 注解 2.将 System.out.println(xxx) 替换为 log.info(&quot;描述,param={}&quot;, xxx) 3.将 System.err.println(xxx) 替换为 log.error(&quot;描述,param={}&quot;, xxx)。文档:.qoder/rules/always-on.md 第4节, docs/conventions/logging.md"
          class="net.sourceforge.pmd.lang.rule.XPathRule">
        <description>禁止使用 System.out.println() 和 System.err.println()</description>
        <priority>1</priority>
        <properties>
            <property name="xpath">
                <value><![CDATA[
                    //PrimaryExpression/PrimaryPrefix/Name[
                        starts-with(@Image, 'System.out')
                        or starts-with(@Image, 'System.err')
                    ]
                ]]></value>
            </property>
        </properties>
    </rule>

    <!-- EXCHANGE-LOG-003: 禁止 e.printStackTrace() -->
    <rule name="ExchangeNoPrintStackTrace"
          language="java"
          message="[EXCHANGE-LOG-003] 禁止使用 e.printStackTrace()。问题:违反异常处理规范,printStackTrace 输出到 stderr 不走日志框架、无法关联 traceId、生产环境无法收集。修复:1.确认类上有 @Slf4j 注解 2.将 e.printStackTrace() 替换为 log.error(&quot;描述,param={}&quot;, param, e) --- 异常对象 e 必须作为最后一个参数。文档:.qoder/rules/always-on.md 第4节, docs/conventions/error-handling.md"
          class="net.sourceforge.pmd.lang.rule.XPathRule">
        <description>禁止使用 e.printStackTrace(),必须使用 log.error 记录异常</description>
        <priority>1</priority>
        <properties>
            <property name="xpath">
                <value><![CDATA[
                    //PrimaryExpression/PrimaryPrefix/Name[
                        @Image='printStackTrace'
                        or ends-with(@Image, '.printStackTrace')
                    ]
                ]]></value>
            </property>
        </properties>
    </rule>

    <!-- EXCHANGE-LOG-004: 禁止日志字符串拼接 -->
    <rule name="ExchangeNoLogStringConcat"
          language="java"
          message="[EXCHANGE-LOG-004] 禁止日志中使用字符串拼接(+)。问题:违反日志规范,必须使用占位符 {} 格式,字符串拼接在日志级别关闭时仍会执行拼接造成性能浪费。修复:将 log.xxx(&quot;planId:&quot; + planId + &quot; 失败&quot;) 替换为 log.xxx(&quot;planId={}, 失败&quot;, planId)。文档:.qoder/rules/always-on.md 第4节, docs/conventions/logging.md"
          class="net.sourceforge.pmd.lang.rule.XPathRule">
        <description>禁止在日志调用中使用字符串拼接(+),必须使用 {} 占位符</description>
        <priority>2</priority>
        <properties>
            <property name="xpath">
                <value><![CDATA[
                    //PrimaryExpression[
                        PrimaryPrefix/Name[@Image='log.error' or @Image='log.warn' or @Image='log.info' or @Image='log.debug'
                            or ends-with(@Image, '.log.error') or ends-with(@Image, '.log.warn')
                            or ends-with(@Image, '.log.info') or ends-with(@Image, '.log.debug')]
                        and PrimarySuffix/Arguments//AdditiveExpression[@Operator='+']
                    ]
                ]]></value>
            </property>
        </properties>
    </rule>

    <!-- ================================================================ -->
    <!-- 二、依赖注入(来源:always-on.md#3)                                  -->
    <!-- ================================================================ -->

    <!-- EXCHANGE-DI-001: 禁止字段级 @Autowired -->
    <rule name="ExchangeNoFieldAutowired"
          language="java"
          message="[EXCHANGE-DI-001] 禁止在字段上使用 @Autowired。问题:违反依赖注入规范,字段注入导致无法声明 final、难以单元测试、隐藏依赖关系。修复:方案一(推荐):改为构造器注入,将字段声明为 private final,删除 @Autowired,在构造器上无需加 @Autowired(Spring 4.3+ 单构造器自动注入)。方案二:将 @Autowired 替换为 @Resource(javax.annotation.Resource)。文档:.qoder/rules/always-on.md 第3节依赖注入"
          class="net.sourceforge.pmd.lang.rule.XPathRule">
        <description>禁止在字段声明上使用 @Autowired 注解</description>
        <priority>1</priority>
        <properties>
            <property name="xpath">
                <value><![CDATA[
                    //FieldDeclaration//Annotation/Name[
                        @Image='Autowired'
                        or @Image='org.springframework.beans.factory.annotation.Autowired'
                    ]
                ]]></value>
            </property>
        </properties>
    </rule>

    <!-- EXCHANGE-DI-002: 禁止构造器上的 @Autowired -->
    <rule name="ExchangeNoConstructorAutowired"
          language="java"
          message="[EXCHANGE-DI-002] 禁止在构造器上使用 @Autowired。问题:Spring 4.3+ 单构造器自动注入,@Autowired 注解冗余。修复:直接删除构造器上的 @Autowired 注解即可。文档:.qoder/rules/always-on.md 第3节依赖注入"
          class="net.sourceforge.pmd.lang.rule.XPathRule">
        <description>禁止在构造器上使用 @Autowired 注解(Spring 4.3+ 自动注入)</description>
        <priority>2</priority>
        <properties>
            <property name="xpath">
                <value><![CDATA[
                    //ConstructorDeclaration//Annotation/Name[
                        @Image='Autowired'
                        or @Image='org.springframework.beans.factory.annotation.Autowired'
                    ]
                ]]></value>
            </property>
        </properties>
    </rule>

    <!-- ================================================================ -->
    <!-- 三、HTTP 调用(来源:always-on.md#5)                                 -->
    <!-- ================================================================ -->

    <!-- EXCHANGE-HTTP-001: 禁止 new OkHttpClient() -->
    <rule name="ExchangeNoNewOkHttpClient"
          language="java"
          message="[EXCHANGE-HTTP-001] 禁止直接 new OkHttpClient()。问题:违反 HTTP 调用规范,必须通过 HttpClientFactory 获取 OkHttp 客户端,直接 new 会创建无法复用连接池的实例、缺少监控拦截器。修复:1.注入 HttpClientFactory(@Resource private HttpClientFactory httpClientFactory) 2.将 new OkHttpClient() 替换为 httpClientFactory.getClient() 或 httpClientFactory.getMonitorClient()。文档:.qoder/rules/always-on.md 第5节HTTP调用, docs/architecture/boundaries.md"
          class="net.sourceforge.pmd.lang.rule.XPathRule">
        <description>禁止直接 new OkHttpClient(),必须通过 HttpClientFactory 获取</description>
        <priority>1</priority>
        <properties>
            <property name="xpath">
                <value><![CDATA[
                    //PrimaryExpression/PrimaryPrefix/AllocationExpression/ClassOrInterfaceType[
                        @Image='OkHttpClient'
                    ]
                ]]></value>
            </property>
        </properties>
    </rule>

    <!-- ================================================================ -->
    <!-- 四、数据库访问(来源:always-on.md#6, docs/conventions/error-handling.md) -->
    <!-- ================================================================ -->

    <!-- EXCHANGE-DB-001: 禁止 @Transactional -->
    <rule name="ExchangeNoTransactional"
          language="java"
          message="[EXCHANGE-DB-001] 禁止使用 @Transactional 注解。问题:@Transactional 与动态数据源(Dynamic Datasource)不兼容,导致数据源切换失效。修复:1.删除 @Transactional 注解 2.注入 TransactionTemplate(@Resource private TransactionTemplate transactionTemplate) 3.使用编程式事务:transactionTemplate.execute(transaction -> { try { // 业务操作 } catch (Exception e) { transaction.setRollbackOnly(); log.error(&quot;描述&quot;, e); return false; } return true; })。文档:.qoder/rules/always-on.md 第10节事务, docs/conventions/error-handling.md#事务处理"
          class="net.sourceforge.pmd.lang.rule.XPathRule">
        <description>禁止使用 @Transactional 注解,必须使用 TransactionTemplate 编程式事务</description>
        <priority>1</priority>
        <properties>
            <property name="xpath">
                <value><![CDATA[
                    //Annotation/Name[
                        @Image='Transactional'
                        or @Image='org.springframework.transaction.annotation.Transactional'
                    ]
                ]]></value>
            </property>
        </properties>
    </rule>

    <!-- ================================================================ -->
    <!-- 五、线程池(来源:always-on.md#7)                                    -->
    <!-- ================================================================ -->

    <!-- EXCHANGE-THREAD-001: 禁止裸 new ThreadPoolExecutor -->
    <rule name="ExchangeNoNewThreadPoolExecutor"
          language="java"
          message="[EXCHANGE-THREAD-001] 禁止直接 new ThreadPoolExecutor()。问题:违反线程池规范,必须使用 BatchExecutorTemplate 或 ThreadPoolExecutorBuilder(exchange-common/juc),裸 new 无法统一监控、无法优雅停机。修复:方案一(批量任务):注入 BatchExecutorTemplate,使用 batchExecutorTemplate.execute(tasks, callback)。方案二(自定义线程池):使用 ThreadPoolExecutorBuilder.builder().name(&quot;xxx&quot;).coreSize(n).maxSize(n).build()。文档:.qoder/rules/always-on.md 第7节线程池, docs/architecture/boundaries.md#exchange-common"
          class="net.sourceforge.pmd.lang.rule.XPathRule">
        <description>禁止直接 new ThreadPoolExecutor(),必须使用 BatchExecutorTemplate 或 ThreadPoolExecutorBuilder</description>
        <priority>1</priority>
        <properties>
            <property name="xpath">
                <value><![CDATA[
                    //PrimaryExpression/PrimaryPrefix/AllocationExpression/ClassOrInterfaceType[
                        @Image='ThreadPoolExecutor'
                    ]
                ]]></value>
            </property>
        </properties>
    </rule>

    <!-- EXCHANGE-THREAD-002: 禁止 new ForkJoinPool -->
    <rule name="ExchangeNoNewForkJoinPool"
          language="java"
          message="[EXCHANGE-THREAD-002] 禁止直接 new ForkJoinPool()。问题:违反线程池规范,ForkJoinPool 不受项目统一管理、无法关联 traceId、无法优雅停机。修复:使用 ThreadPoolExecutorBuilder(exchange-common/juc) 创建受管线程池,或使用 BatchExecutorTemplate。文档:.qoder/rules/always-on.md 第7节线程池"
          class="net.sourceforge.pmd.lang.rule.XPathRule">
        <description>禁止直接 new ForkJoinPool(),必须使用项目统一线程池</description>
        <priority>1</priority>
        <properties>
            <property name="xpath">
                <value><![CDATA[
                    //PrimaryExpression/PrimaryPrefix/AllocationExpression/ClassOrInterfaceType[
                        @Image='ForkJoinPool'
                    ]
                ]]></value>
            </property>
        </properties>
    </rule>

    <!-- ================================================================ -->
    <!-- 六、异常处理(来源:java-code-style.md, docs/conventions/error-handling.md) -->
    <!-- ================================================================ -->

    <!-- EXCHANGE-CATCH-001: 禁止空 catch 块 --- 使用 PMD 内置规则 -->
    <rule ref="category/java/errorprone.xml/EmptyCatchBlock">
        <message>[EXCHANGE-CATCH-001] 禁止空 catch 块。问题:违反异常处理规范,空 catch 会吞掉异常导致问题难以排查。修复:方案一(记录日志):catch 块中添加 log.error(&quot;描述,param={}&quot;, param, e)。方案二(向上抛出):catch 块中 throw new RuntimeException(e) 或自定义业务异常。方案三(返回错误码):catch 块中 return ResponseDTO.errorInstance(CodeEnum.UNKNOWN, e.getMessage())。文档:.qoder/rules/java-code-style.md#异常处理, docs/conventions/error-handling.md#异常处理原则</message>
    </rule>

    <!-- ================================================================ -->
    <!-- 七、Lombok 使用(来源:java-code-style.md)                           -->
    <!-- ================================================================ -->

    <!-- EXCHANGE-LOMBOK-001: 禁止 @AllArgsConstructor + @NoArgsConstructor 组合 -->
    <rule name="ExchangeNoAllArgsNoArgsCombo"
          language="java"
          message="[EXCHANGE-LOMBOK-001] 检测到 @AllArgsConstructor,请确认是否同时使用了 @NoArgsConstructor。问题:禁止使用 @AllArgsConstructor + @NoArgsConstructor 组合替代 @Data,该组合无法生成 toString/equals/hashCode 且语义不清。修复:1.删除 @AllArgsConstructor 和 @NoArgsConstructor 2.在类上添加 @Data 注解 3.如需构建器,添加 @Builder + @Builder.Default。文档:.qoder/rules/java-code-style.md#Lombok-使用"
          class="net.sourceforge.pmd.lang.rule.XPathRule">
        <description>检测 @AllArgsConstructor 使用,提示确认是否违反 @Data 替代规范</description>
        <priority>3</priority>
        <properties>
            <property name="xpath">
                <value><![CDATA[
                    //Annotation/Name[
                        @Image='AllArgsConstructor'
                        or @Image='lombok.AllArgsConstructor'
                    ]
                ]]></value>
            </property>
        </properties>
    </rule>

    <!-- ================================================================ -->
    <!-- 八、测试规范(来源:java-testing.md)                                 -->
    <!-- ================================================================ -->

    <!-- EXCHANGE-TEST-001: 禁止 JUnit 4 @RunWith -->
    <rule name="ExchangeNoRunWith"
          language="java"
          message="[EXCHANGE-TEST-001] 禁止使用 JUnit 4 的 @RunWith 注解。问题:违反测试规范,项目使用 JUnit 5 (Jupiter),@RunWith 是 JUnit 4 注解不兼容。修复:1.删除 @RunWith(Xxx.class) 2.添加 @ExtendWith(MockitoExtension.class)(import org.junit.jupiter.api.extension.ExtendWith) 3.将 import org.junit.runner.RunWith 替换为 import org.junit.jupiter.api.extension.ExtendWith 4.将 org.junit.Test 替换为 org.junit.jupiter.api.Test。文档:.qoder/rules/java-testing.md#框架"
          class="net.sourceforge.pmd.lang.rule.XPathRule">
        <description>禁止使用 JUnit 4 的 @RunWith 注解,必须使用 JUnit 5 的 @ExtendWith</description>
        <priority>1</priority>
        <properties>
            <property name="xpath">
                <value><![CDATA[
                    //Annotation/Name[
                        @Image='RunWith'
                        or @Image='org.junit.runner.RunWith'
                    ]
                ]]></value>
            </property>
        </properties>
    </rule>

    <!-- EXCHANGE-TEST-002: 禁止 Thread.sleep() -->
    <rule name="ExchangeNoThreadSleep"
          language="java"
          message="[EXCHANGE-TEST-002] 禁止在测试中使用 Thread.sleep()。问题:违反测试规范,Thread.sleep 会导致测试不稳定(时序依赖)、执行缓慢。修复:方案一(等待异步结果):使用 CountDownLatch + await() 替代 Thread.sleep。方案二(同步执行):将异步逻辑改为同步调用直接验证结果。文档:.qoder/rules/java-testing.md#禁止"
          class="net.sourceforge.pmd.lang.rule.XPathRule">
        <description>禁止在测试代码中使用 Thread.sleep(),必须使用 CountDownLatch 或同步执行</description>
        <priority>1</priority>
        <properties>
            <property name="xpath">
                <value><![CDATA[
                    //PrimaryExpression/PrimaryPrefix/Name[
                        @Image='Thread.sleep'
                        or ends-with(@Image, '.Thread.sleep')
                    ]
                ]]></value>
            </property>
        </properties>
    </rule>

    <!-- EXCHANGE-TEST-003: 禁止直接注入 Mapper(在非 dal 模块中) -->
    <rule name="ExchangeNoDirectMapperInjection"
          language="java"
          message="[EXCHANGE-TEST-003] 检测到直接注入 Mapper。问题:违反数据库访问规范,必须通过 DalManager.of(beanName) 获取 Manager 实例,禁止直接注入 Mapper。修复:1.删除 @Resource/@Autowired private XxxMapper xxxMapper 2.使用 DalManager.of(&quot;xxxManagerImpl&quot;) 获取 Manager 3.将 xxxMapper.selectById(id) 替换为 manager.getById(id)。文档:.qoder/rules/always-on.md 第6节数据库访问, docs/architecture/boundaries.md#跨模块约束"
          class="net.sourceforge.pmd.lang.rule.XPathRule">
        <description>检测直接注入 Mapper(应通过 DalManager.of() 获取 Manager)</description>
        <priority>2</priority>
        <properties>
            <property name="xpath">
                <value><![CDATA[
                    //FieldDeclaration[
                        Type/ReferenceType/ClassOrInterfaceType[
                            ends-with(@Image, 'Mapper')
                            and not(starts-with(@Image, 'Base'))
                        ]
                    ]
                ]]></value>
            </property>
        </properties>
    </rule>

    <!-- ================================================================ -->
    <!-- 九、代码质量(来源:conventions/README.md, pr-checklist.md)           -->
    <!-- ================================================================ -->

    <!-- EXCHANGE-QUALITY-001: 禁止 System.exit() 在非启动类中调用 -->
    <rule name="ExchangeNoSystemExit"
          language="java"
          message="[EXCHANGE-QUALITY-001] 禁止在业务代码中使用 System.exit()。问题:System.exit 会强制终止 JVM,影响其他正在执行的线程和请求。修复:1.删除 System.exit() 调用 2.使用异常或返回值通知调用方处理 3.如需优雅停机,使用 exchange-common/shutdown 模块。文档:.qoder/rules/always-on.md, docs/changes/pr-checklist.md"
          class="net.sourceforge.pmd.lang.rule.XPathRule">
        <description>禁止在业务代码中使用 System.exit()</description>
        <priority>2</priority>
        <properties>
            <property name="xpath">
                <value><![CDATA[
                    //PrimaryExpression/PrimaryPrefix/Name[
                        @Image='System.exit'
                        or ends-with(@Image, '.System.exit')
                    ]
                ]]></value>
            </property>
        </properties>
    </rule>

</ruleset>

4.3 Checkstyle 代码风格规则集(checkstyle-exchange.xml)

java-code-style.md 中的规则映射为 Checkstyle 规则:

规则 Checkstyle 模块 阻塞级别
类注释必须存在 JavadocType error
方法注释必须存在 JavadocMethod error
禁止未使用 import UnusedImports error
import 排序规范 CustomImportOrder warning
实际示例

通过agent生成

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE module PUBLIC
    "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
    "https://checkstyle.org/dtds/configuration_1_3.dtd">
<!--
  Checkstyle 代码风格配置 --- app-exchange-port 项目
  基于 .qoder/rules/always-on.md + java-code-style.md 中的 import 禁止规则和命名规范
  每条规则的 message 遵循三要素格式:问题是什么 | 怎么修 | 去哪看文档
  当 AI Agent 看到 Checkstyle 报错时,无需额外提示即可根据 message 自动修复
-->
<module name="Checker">

    <property name="charset" value="UTF-8"/>
    <property name="severity" value="error"/>
    <property name="fileExtensions" value="java"/>

    <!-- ================================================================ -->
    <!-- 文件级检查                                                          -->
    <!-- ================================================================ -->

    <!-- EXCHANGE-CS-FILE-001: 文件行数不超过 300 行 -->
    <module name="FileLength">
        <property name="max" value="300"/>
        <message key="maxLen.fileLength"
                 value="[EXCHANGE-CS-FILE-001] 文件行数超过 300 行限制(当前 {0} 行)。问题:违反代码质量规范,单文件超过 300 行难以维护。修复:1.按职责拆分为多个类 2.将策略逻辑提取为独立的策略类 3.将通用方法提取为工具类。文档:always-on.md#13, docs/conventions/README.md#通用原则"/>
    </module>

    <!-- ================================================================ -->
    <!-- Import 检查(来源:always-on.md#1 FastJSON 唯一, always-on.md#5 HTTP) -->
    <!-- ================================================================ -->
    <module name="TreeWalker">

        <!-- EXCHANGE-CS-001: 禁止 import Jackson -->
        <module name="IllegalImport">
            <property name="illegalPkgs" value="com.fasterxml.jackson, org.codehaus.jackson"/>
            <message key="import.illegal"
                     value="[EXCHANGE-CS-001] 禁止 import Jackson({0})。问题:违反技术栈规范,项目统一使用 FastJSON(com.alibaba.fastjson.JSON) 作为唯一 JSON 序列化库,禁止引入 Jackson。修复:1.删除该 import 语句 2.将 ObjectMapper 替换为 com.alibaba.fastjson.JSON 3.将 @JsonProperty 替换为 @JSONField 4.将 @JsonIgnore 替换为 @JSONField(serialize=false)。文档:.qoder/rules/always-on.md 第1节技术栈锁定"/>
        </module>

        <!-- EXCHANGE-CS-002: 禁止 import Gson -->
        <module name="IllegalImport">
            <property name="illegalPkgs" value="com.google.gson"/>
            <message key="import.illegal"
                     value="[EXCHANGE-CS-002] 禁止 import Gson({0})。问题:违反技术栈规范,项目统一使用 FastJSON(com.alibaba.fastjson.JSON) 作为唯一 JSON 序列化库,禁止引入 Gson。修复:1.删除该 import 语句 2.将 Gson() 替换为 com.alibaba.fastjson.JSON 3.将 fromJson(str, Cls.class) 替换为 JSON.parseObject(str, Cls.class) 4.将 toJson(obj) 替换为 JSON.toJSONString(obj)。文档:.qoder/rules/always-on.md 第1节技术栈锁定"/>
        </module>

        <!-- EXCHANGE-CS-003: 禁止 import RestTemplate -->
        <module name="IllegalImport">
            <property name="illegalPkgs" value="org.springframework.web.client.RestTemplate"/>
            <message key="import.illegal"
                     value="[EXCHANGE-CS-003] 禁止 import RestTemplate({0})。问题:违反 HTTP 调用规范,项目统一使用 OkHttp3 + HttpClientFactory(exchange-common/http),禁止使用 RestTemplate。修复:1.删除该 import 语句 2.注入 HttpClientFactory(@Resource private HttpClientFactory httpClientFactory) 3.使用 httpClientFactory.getClient() 获取 OkHttpClient 4.构建 Request + 使用 client.newCall(request).execute() 发起请求。文档:.qoder/rules/always-on.md 第5节HTTP调用"/>
        </module>

        <!-- EXCHANGE-CS-004: 禁止 import Apache HttpClient -->
        <module name="IllegalImport">
            <property name="illegalPkgs" value="org.apache.http.client, org.apache.http.impl"/>
            <message key="import.illegal"
                     value="[EXCHANGE-CS-004] 禁止 import Apache HttpClient({0})。问题:违反 HTTP 调用规范,项目统一使用 OkHttp3 + HttpClientFactory(exchange-common/http),禁止使用 Apache HttpClient。修复:1.删除该 import 语句 2.注入 HttpClientFactory 3.使用 OkHttp3 API 替换 Apache HttpClient API。文档:.qoder/rules/always-on.md 第5节HTTP调用"/>
        </module>

        <!-- EXCHANGE-CS-005: 禁止 import java.util.logging -->
        <module name="IllegalImport">
            <property name="illegalPkgs" value="java.util.logging"/>
            <message key="import.illegal"
                     value="[EXCHANGE-CS-005] 禁止 import java.util.logging({0})。问题:违反日志规范,项目统一使用 Lombok @Slf4j(Logback 实现),禁止使用 JUL。修复:1.删除该 import 语句 2.在类上添加 @Slf4j 注解 3.使用 log.info/warn/error 替代 Logger.info/warning/severe。文档:docs/conventions/logging.md#日志框架"/>
        </module>

        <!-- EXCHANGE-CS-006: 禁止 import org.junit.runner(JUnit 4) -->
        <module name="IllegalImport">
            <property name="illegalPkgs" value="org.junit.runner, org.junit.runners"/>
            <message key="import.illegal"
                     value="[EXCHANGE-CS-006] 禁止 import JUnit 4 包({0})。问题:违反测试规范,项目使用 JUnit 5 (Jupiter),禁止使用 JUnit 4。修复:1.删除该 import 语句 2.将 org.junit.runner.RunWith 替换为 org.junit.jupiter.api.extension.ExtendWith 3.将 org.junit.Test 替换为 org.junit.jupiter.api.Test 4.将 org.junit.Assert 替换为 org.junit.jupiter.api.Assertions。文档:.qoder/rules/java-testing.md#框架"/>
        </module>

        <!-- EXCHANGE-CS-007: 禁止 import org.junit.Assert(JUnit 4 断言) -->
        <module name="IllegalImport">
            <property name="illegalPkgs" value="org.junit.Assert"/>
            <message key="import.illegal"
                     value="[EXCHANGE-CS-007] 禁止 import org.junit.Assert({0})。问题:违反测试规范,JUnit 5 使用 org.junit.jupiter.api.Assertions 进行断言。修复:1.删除 import org.junit.Assert 2.添加 import org.junit.jupiter.api.Assertions 3.将 Assert.assertEquals(a, b) 替换为 Assertions.assertEquals(a, b) 4.将 Assert.assertNotNull(x) 替换为 Assertions.assertNotNull(x)。文档:.qoder/rules/java-testing.md#框架"/>
        </module>

        <!-- EXCHANGE-CS-008: 检测未使用的 import -->
        <module name="UnusedImports">
            <message key="import.unused"
                     value="[EXCHANGE-CS-008] 存在未使用的 import({0})。问题:违反代码质量规范,未使用的 import 会增加代码噪音。修复:直接删除该 import 语句。文档:docs/changes/pr-checklist.md#代码质量检查"/>
        </module>

        <!-- EXCHANGE-CS-009: Import 排序规范 -->
        <module name="CustomImportOrder">
            <property name="customImportOrderRules"
                      value="STATIC###STANDARD_JAVA_PACKAGE###THIRD_PARTY_PACKAGE"/>
            <property name="sortImportsInGroupAlphabetically" value="true"/>
            <property name="separateLineBetweenGroups" value="true"/>
            <message key="custom.import.order"
                     value="[EXCHANGE-CS-009] Import 顺序不符合规范。问题:违反代码风格规范,import 应按 静态导入 ### 标准Java包 ### 第三方包 分组排列,组内字母排序,组间空行分隔。修复:1.将 static import 放在最前 2.将 java.* / javax.* 放在第二组 3.将 com.* / org.* 等第三方包放在第三组 4.组内按字母排序 5.组间添加空行。文档:docs/conventions/README.md"/>
        </module>

        <!-- EXCHANGE-CS-010: 禁止通配符 import -->
        <module name="AvoidStarImport">
            <message key="import.avoidStar"
                     value="[EXCHANGE-CS-010] 禁止通配符 import({0})。问题:违反代码风格规范,通配符 import 会隐藏依赖关系并导致冲突。修复:将 import xxx.* 替换为逐个显式 import,只导入实际使用的类。文档:java-code-style.md#import 规范"/>
        </module>

    </module>

</module>

4.4 ArchUnit 架构测试

使用 ArchUnit 自动验证模块依赖方向:

java 复制代码
@AnalyzeClasses(packages = "com.jzsk.exchange")
public class ArchitectureRulesTest {

    @ArchTest
    static final ArchRule 依赖方向不可逆 = layeredArchitecture()
        .layer("common").definedBy("..common..")
        .layer("dal").definedBy("..dal..")
        .layer("business").definedBy("..business..")
        .layer("ext").definedBy("..ext..")
        .layer("app").definedBy("..app..")
        .layer("runner").definedBy("..runner..")
        .whereLayer("common").mayNotAccessAnyLayer()
        .whereLayer("dal").mayOnlyAccessLayers("common")
        .whereLayer("business").mayOnlyAccessLayers("dal", "common")
        .whereLayer("ext").mayOnlyAccessLayers("business")
        .whereLayer("app").mayOnlyAccessLayers("ext", "business", "dal", "common")
        .whereLayer("runner").mayOnlyAccessLayers("app");
}

4.5 Maven Enforcer 依赖管理

使用 Maven Enforcer Plugin 强制依赖方向:

xml 复制代码
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-enforcer-plugin</artifactId>
    <version>3.1.0</version>
    <executions>
        <execution>
            <id>enforce-dependency-direction</id>
            <goals><goal>enforce</goal></goals>
            <configuration>
                <rules>
                    <bannedDependencies>
                        <excludes>
                            <!-- common 不允许依赖任何项目内模块 -->
                            <exclude>com.jzsk.exchange:*</exclude>
                        </excludes>
                    </bannedDependencies>
                </rules>
            </configuration>
        </execution>
    </executions>
</plugin>

5. 第三部分:自动化治理脚本

5.1 pre-check.ps1 --- 环境前置检查

触发时机prd-to-code.md Step 0.1

检查项

检查项 检查方式 不通过时
JDK 版本 java -version 阻断,提示安装 JDK 1.8
Maven 版本 mvn -version 阻断,提示安装 Maven 3.6+
Git 已配置 git config user.name 阻断,提示配置 Git
项目结构 检查 pom.xml 存在 阻断,提示目录错误
.qoder/rules/ 存在 检查目录 警告,提示 Rules 层未建立

输出格式:JSON,包含每项检查的 pass/fail 结果。

实际示例

通过agent生成

bash 复制代码
<#
.SYNOPSIS
  环境前置检查脚本 --- 在 prd-to-code Step 0 中执行,确保环境就绪
.DESCRIPTION
  Harness 自动化层组件之五:检查 JDK 1.8、Maven 3.6+、Git 是否可用,
  检查项目是否可编译(快速编译检查),检查 build/automation/scripts/ 脚本是否存在。
  全部通过返回 exit 0,任一项不通过返回 exit 1 并输出缺失项。
.EXAMPLE
  .\pre-check.ps1
#>

$ErrorActionPreference = "Stop"
Write-Host "[PRECHECK] 开始环境前置检查..." -ForegroundColor Cyan
$allPass = $true

# ── 1. JDK 检查 ──
Write-Host "[PRECHECK] [1/5] JDK 检查..." -NoNewline
$javaVersion = & java -version 2>&1 | Out-String
if ($javaVersion -match 'version "1\.8') {
    Write-Host " PASS (JDK 1.8)" -ForegroundColor Green
} else {
    Write-Host " FAIL" -ForegroundColor Red
    Write-Host "  期望: JDK 1.8, 实际: $($javaVersion.Split("`n")[0])" -ForegroundColor Red
    Write-Host "  修复: 安装 JDK 1.8 并设置 JAVA_HOME" -ForegroundColor Yellow
    $allPass = $false
}

# ── 2. Maven 检查 ──
Write-Host "[PRECHECK] [2/5] Maven 检查..." -NoNewline
$mvnVersion = & mvn --version 2>&1 | Out-String
if ($mvnVersion -match 'Apache Maven (\d+\.\d+)') {
    $mvnVer = $Matches[1]
    if ([double]$mvnVer -ge 3.6) {
        Write-Host " PASS (Maven $mvnVer)" -ForegroundColor Green
    } else {
        Write-Host " FAIL (Maven $mvnVer < 3.6)" -ForegroundColor Red
        Write-Host "  修复: 升级 Maven 到 3.6+" -ForegroundColor Yellow
        $allPass = $false
    }
} else {
    Write-Host " FAIL (未找到 Maven)" -ForegroundColor Red
    Write-Host "  修复: 安装 Maven 并添加到 PATH" -ForegroundColor Yellow
    $allPass = $false
}

# ── 3. Git 检查 ──
Write-Host "[PRECHECK] [3/5] Git 检查..." -NoNewline
$gitVersion = & git --version 2>&1 | Out-String
if ($gitVersion -match 'git version') {
    Write-Host " PASS ($($gitVersion.Trim()))" -ForegroundColor Green
} else {
    Write-Host " FAIL" -ForegroundColor Red
    Write-Host "  修复: 安装 Git 并添加到 PATH" -ForegroundColor Yellow
    $allPass = $false
}

# ── 4. 项目根目录检查 ──
Write-Host "[PRECHECK] [4/5] 项目结构检查..." -NoNewline
$repoRoot = (git rev-parse --show-toplevel 2>$null | Out-String).Trim()
if (-not $repoRoot -or -not (Test-Path (Join-Path $repoRoot "pom.xml"))) {
    Write-Host " FAIL (未找到 pom.xml)" -ForegroundColor Red
    Write-Host "  修复: 请在 Git 仓库根目录执行" -ForegroundColor Yellow
    $allPass = $false
} else {
    Write-Host " PASS ($repoRoot)" -ForegroundColor Green
}

# ── 5. 自动化脚本检查 ──
Write-Host "[PRECHECK] [5/5] 自动化脚本检查..." -NoNewline
$requiredScripts = @(
    "build\automation\scripts\collect-sidecar.ps1",
    "build\automation\scripts\decay-detector.ps1",
    "build\automation\scripts\governance-report.ps1",
    "build\automation\scripts\worktree-manager.ps1"
)
$missingScripts = @()
foreach ($script in $requiredScripts) {
    $fullPath = Join-Path $repoRoot $script
    if (-not (Test-Path $fullPath)) { $missingScripts += $script }
}
if ($missingScripts.Count -eq 0) {
    Write-Host " PASS (4/4 脚本就绪)" -ForegroundColor Green
} else {
    Write-Host " FAIL ($($missingScripts.Count) 个缺失)" -ForegroundColor Red
    foreach ($s in $missingScripts) { Write-Host "  缺失: $s" -ForegroundColor Yellow }
    $allPass = $false
}

# ── 汇总 ──
Write-Host ""
if ($allPass) {
    Write-Host "[PRECHECK] 全部检查通过,环境就绪。" -ForegroundColor Green
    exit 0
} else {
    Write-Host "[PRECHECK] 检查未通过,请修复上述问题后重试。" -ForegroundColor Red
    exit 1
}

5.2 worktree-manager.ps1 --- Git Worktree 自动化

触发时机prd-to-code.md Step 0.2

功能

命令 功能
create "feature-{name}" 创建隔离 worktree
list 列出所有 worktree
clean 清理已合并的 worktree
status 检查 worktree 合并就绪状态

实际示例

通过agent生成

bash 复制代码
<#
.SYNOPSIS
  Git Worktree 自动化管理脚本 --- 为 AI Agent 任务创建隔离工作区
.DESCRIPTION
  Harness 自动化层组件之一:管理 Git Worktree 的完整生命周期。
  - create: 基于 PRD 名称创建隔离 worktree + 分支
  - list:   列出所有活跃 worktree
  - clean:  清理过期 worktree(默认 >7 天)
  - status: 检查 worktree 是否合并就绪(Linter + 测试通过)
.EXAMPLE
  .\worktree-manager.ps1 create "feature-umeng-query"
  .\worktree-manager.ps1 list
  .\worktree-manager.ps1 clean 7
  .\worktree-manager.ps1 status "feature-umeng-query"
#>

param(
    [Parameter(Position = 0)]
    [ValidateSet("create", "list", "clean", "status")]
    [string]$Action = "list",

    [Parameter(Position = 1)]
    [string]$BranchName,

    [Parameter(Position = 2)]
    [int]$MaxDays = 7
)

$ErrorActionPreference = "Stop"
$repoRoot = (git rev-parse --show-toplevel 2>$null | Out-String).Trim()
if (-not $repoRoot) {
    Write-Error "[WORKTREE] 无法确定 Git 仓库根目录。请在仓库内执行此脚本。"
    exit 1
}

$worktreeBase = Join-Path $repoRoot "..\.worktrees"
if (-not (Test-Path $worktreeBase)) {
    New-Item -ItemType Directory -Path $worktreeBase -Force | Out-Null
}

function New-Worktree {
    param([string]$Name)
    if (-not $Name) {
        Write-Error "[WORKTREE] 用法: worktree-manager.ps1 create <branch-name>"
        exit 1
    }
    $timestamp = Get-Date -Format "yyyyMMdd-HHmm"
    $fullBranch = "ai-task/$Name-$timestamp"
    $worktreePath = Join-Path $worktreeBase ($Name -replace '[^a-zA-Z0-9-]', '-')
    if (Test-Path $worktreePath) {
        $worktreePath = "${worktreePath}-$timestamp"
    }
    Write-Host "[WORKTREE] 创建隔离工作区:" -ForegroundColor Cyan
    Write-Host "  分支: $fullBranch"
    Write-Host "  路径: $worktreePath"
    git worktree add -b $fullBranch $worktreePath 2>&1 | ForEach-Object { Write-Host "  $_" }
    if ($LASTEXITCODE -eq 0) {
        $meta = @{
            branch = $fullBranch
            path = $worktreePath
            createdAt = (Get-Date).ToString("o")
            task = $Name
        }
        $meta | ConvertTo-Json | Set-Content (Join-Path $worktreePath ".worktree-meta.json")
        Write-Host "[WORKTREE] 创建成功。AI Agent 可在 $worktreePath 中工作。" -ForegroundColor Green
        Write-Host "[WORKTREE] 元信息已写入 .worktree-meta.json" -ForegroundColor DarkGray
    } else {
        Write-Error "[WORKTREE] 创建失败,请检查 Git 状态。"
    }
}

function Show-Worktrees {
    Write-Host "[WORKTREE] 活跃工作区列表:" -ForegroundColor Cyan
    $output = git worktree list --porcelain 2>$null
    if (-not $output) {
        Write-Host "  (无活跃 worktree)"
        return
    }
    $current = @{}
    foreach ($line in ($output -split "`n")) {
        if ($line -match '^worktree\s+(.+)') { $current.path = $Matches[1] }
        elseif ($line -match '^branch\s+(.+)') { $current.branch = $Matches[1] }
        elseif ($line -match '^HEAD\s+(.+)') {
            $current.head = $Matches[1].Substring(0, 8)
            $metaFile = Join-Path $current.path ".worktree-meta.json"
            $task = "-"
            $age = "-"
            if (Test-Path $metaFile) {
                $meta = Get-Content $metaFile | ConvertFrom-Json
                $task = $meta.task
                $created = [DateTime]::Parse($meta.createdAt)
                $age = [math]::Round(((Get-Date) - $created).TotalDays, 1)
            }
            Write-Host ("  {0,-40} {1,-45} age={2}d  task={3}" -f $current.branch, $current.path, $age, $task)
            $current = @{}
        }
    }
}

function Remove-StaleWorktrees {
    param([int]$Days)
    Write-Host "[WORKTREE] 清理 ${Days} 天前的过期工作区:" -ForegroundColor Yellow
    $output = git worktree list --porcelain 2>$null
    if (-not $output) { Write-Host "  (无活跃 worktree)"; return }
    $worktrees = @()
    $current = @{}
    foreach ($line in ($output -split "`n")) {
        if ($line -match '^worktree\s+(.+)') { $current.path = $Matches[1] }
        elseif ($line -match '^branch\s+(.+)') { $current.branch = $Matches[1] }
        elseif ($line -match '^HEAD\s+(.+)') {
            $current.head = $Matches[1]
            $worktrees += [PSCustomObject]$current
            $current = @{}
        }
    }
    $cleaned = 0
    foreach ($wt in $worktrees) {
        $metaFile = Join-Path $wt.path ".worktree-meta.json"
        $shouldClean = $false
        $task = "unknown"
        if (Test-Path $metaFile) {
            $meta = Get-Content $metaFile | ConvertFrom-Json
            $task = $meta.task
            $created = [DateTime]::Parse($meta.createdAt)
            $age = ((Get-Date) - $created).TotalDays
            if ($age -gt $Days) { $shouldClean = $true }
        }
        if ($shouldClean) {
            Write-Host "  清理: $($wt.branch) (task=$task, age=$([math]::Round($age,1))d)" -ForegroundColor Yellow
            git worktree remove --force $wt.path 2>&1 | Out-Null
            git branch -D $wt.branch 2>&1 | Out-Null
            $cleaned++
        }
    }
    if ($cleaned -eq 0) { Write-Host "  无过期工作区需要清理。" -ForegroundColor Green }
    else { Write-Host "[WORKTREE] 已清理 $cleaned 个过期工作区。" -ForegroundColor Green }
}

function Test-MergeReady {
    param([string]$Name)
    $output = git worktree list --porcelain 2>$null
    $targetPath = $null
    $current = @{}
    foreach ($line in ($output -split "`n")) {
        if ($line -match '^worktree\s+(.+)') { $current.path = $Matches[1] }
        elseif ($line -match '^branch\s+(.+)') { $current.branch = $Matches[1] }
        elseif ($line -match '^HEAD\s+(.+)') {
            if ($current.branch -like "*$Name*") { $targetPath = $current.path; break }
            $current = @{}
        }
    }
    if (-not $targetPath) {
        Write-Error "[WORKTREE] 未找到匹配 '$Name' 的工作区。"
        exit 1
    }
    Write-Host "[WORKTREE] 检查合并就绪状态: $Name" -ForegroundColor Cyan
    Write-Host "  路径: $targetPath"
    $checks = @()
    Push-Location $targetPath
    try {
        Write-Host "  [1/3] 编译检查..." -NoNewline
        $compileResult = mvn compile -q -f $repoRoot 2>&1
        if ($LASTEXITCODE -eq 0) { Write-Host " PASS" -ForegroundColor Green; $checks += "compile:PASS" }
        else { Write-Host " FAIL" -ForegroundColor Red; $checks += "compile:FAIL" }
        Write-Host "  [2/3] Linter 检查..." -NoNewline
        $pmdResult = mvn pmd:check -q -f $repoRoot 2>&1
        if ($LASTEXITCODE -eq 0) { Write-Host " PASS" -ForegroundColor Green; $checks += "pmd:PASS" }
        else { Write-Host " FAIL" -ForegroundColor Red; $checks += "pmd:FAIL" }
        Write-Host "  [3/3] 测试检查..." -NoNewline
        $testResult = mvn test -q -f $repoRoot -DskipTests=false 2>&1
        if ($LASTEXITCODE -eq 0) { Write-Host " PASS" -ForegroundColor Green; $checks += "test:PASS" }
        else { Write-Host " FAIL" -ForegroundColor Red; $checks += "test:FAIL" }
    } finally { Pop-Location }
    $allPass = ($checks | Where-Object { $_ -like "*:FAIL" }).Count -eq 0
    if ($allPass) { Write-Host "[WORKTREE] 合并就绪: 全部检查通过" -ForegroundColor Green }
    else { Write-Host "[WORKTREE] 合并就绪: 有未通过检查,请修复后再合并" -ForegroundColor Red }
    return $allPass
}

switch ($Action) {
    "create" { New-Worktree -Name $BranchName }
    "list"   { Show-Worktrees }
    "clean"  { Remove-StaleWorktrees -Days $MaxDays }
    "status" { Test-MergeReady -Name $BranchName }
}

5.3 collect-sidecar.ps1 --- 客观侧性数据采集

触发时机prd-to-code.md Step 7 / auto-governance.md Step 1

采集内容

数据项 采集方式 输出文件
编译结果 mvn compile -q build-status.json
测试结果 mvn test test-results.json
Linter 结果 mvn pmd:check checkstyle:check linter-results.json
汇总 合并以上 sidecar-summary.json

关键设计:sidecar-summary.json 包含 timestamp 字段,超过 24 小时视为过期。

实际示例

通过agent生成

bash 复制代码
<#
.SYNOPSIS
  Git Worktree 自动化管理脚本 --- 为 AI Agent 任务创建隔离工作区
.DESCRIPTION
  Harness 自动化层组件之一:管理 Git Worktree 的完整生命周期。
  - create: 基于 PRD 名称创建隔离 worktree + 分支
  - list:   列出所有活跃 worktree
  - clean:  清理过期 worktree(默认 >7 天)
  - status: 检查 worktree 是否合并就绪(Linter + 测试通过)
.EXAMPLE
  .\worktree-manager.ps1 create "feature-umeng-query"
  .\worktree-manager.ps1 list
  .\worktree-manager.ps1 clean 7
  .\worktree-manager.ps1 status "feature-umeng-query"
#>

param(
    [Parameter(Position = 0)]
    [ValidateSet("create", "list", "clean", "status")]
    [string]$Action = "list",

    [Parameter(Position = 1)]
    [string]$BranchName,

    [Parameter(Position = 2)]
    [int]$MaxDays = 7
)

$ErrorActionPreference = "Stop"
$repoRoot = (git rev-parse --show-toplevel 2>$null | Out-String).Trim()
if (-not $repoRoot) {
    Write-Error "[WORKTREE] 无法确定 Git 仓库根目录。请在仓库内执行此脚本。"
    exit 1
}

$worktreeBase = Join-Path $repoRoot "..\.worktrees"
if (-not (Test-Path $worktreeBase)) {
    New-Item -ItemType Directory -Path $worktreeBase -Force | Out-Null
}

function New-Worktree {
    param([string]$Name)
    if (-not $Name) {
        Write-Error "[WORKTREE] 用法: worktree-manager.ps1 create <branch-name>"
        exit 1
    }
    $timestamp = Get-Date -Format "yyyyMMdd-HHmm"
    $fullBranch = "ai-task/$Name-$timestamp"
    $worktreePath = Join-Path $worktreeBase ($Name -replace '[^a-zA-Z0-9-]', '-')
    if (Test-Path $worktreePath) {
        $worktreePath = "${worktreePath}-$timestamp"
    }
    Write-Host "[WORKTREE] 创建隔离工作区:" -ForegroundColor Cyan
    Write-Host "  分支: $fullBranch"
    Write-Host "  路径: $worktreePath"
    git worktree add -b $fullBranch $worktreePath 2>&1 | ForEach-Object { Write-Host "  $_" }
    if ($LASTEXITCODE -eq 0) {
        $meta = @{
            branch = $fullBranch
            path = $worktreePath
            createdAt = (Get-Date).ToString("o")
            task = $Name
        }
        $meta | ConvertTo-Json | Set-Content (Join-Path $worktreePath ".worktree-meta.json")
        Write-Host "[WORKTREE] 创建成功。AI Agent 可在 $worktreePath 中工作。" -ForegroundColor Green
        Write-Host "[WORKTREE] 元信息已写入 .worktree-meta.json" -ForegroundColor DarkGray
    } else {
        Write-Error "[WORKTREE] 创建失败,请检查 Git 状态。"
    }
}

function Show-Worktrees {
    Write-Host "[WORKTREE] 活跃工作区列表:" -ForegroundColor Cyan
    $output = git worktree list --porcelain 2>$null
    if (-not $output) {
        Write-Host "  (无活跃 worktree)"
        return
    }
    $current = @{}
    foreach ($line in ($output -split "`n")) {
        if ($line -match '^worktree\s+(.+)') { $current.path = $Matches[1] }
        elseif ($line -match '^branch\s+(.+)') { $current.branch = $Matches[1] }
        elseif ($line -match '^HEAD\s+(.+)') {
            $current.head = $Matches[1].Substring(0, 8)
            $metaFile = Join-Path $current.path ".worktree-meta.json"
            $task = "-"
            $age = "-"
            if (Test-Path $metaFile) {
                $meta = Get-Content $metaFile | ConvertFrom-Json
                $task = $meta.task
                $created = [DateTime]::Parse($meta.createdAt)
                $age = [math]::Round(((Get-Date) - $created).TotalDays, 1)
            }
            Write-Host ("  {0,-40} {1,-45} age={2}d  task={3}" -f $current.branch, $current.path, $age, $task)
            $current = @{}
        }
    }
}

function Remove-StaleWorktrees {
    param([int]$Days)
    Write-Host "[WORKTREE] 清理 ${Days} 天前的过期工作区:" -ForegroundColor Yellow
    $output = git worktree list --porcelain 2>$null
    if (-not $output) { Write-Host "  (无活跃 worktree)"; return }
    $worktrees = @()
    $current = @{}
    foreach ($line in ($output -split "`n")) {
        if ($line -match '^worktree\s+(.+)') { $current.path = $Matches[1] }
        elseif ($line -match '^branch\s+(.+)') { $current.branch = $Matches[1] }
        elseif ($line -match '^HEAD\s+(.+)') {
            $current.head = $Matches[1]
            $worktrees += [PSCustomObject]$current
            $current = @{}
        }
    }
    $cleaned = 0
    foreach ($wt in $worktrees) {
        $metaFile = Join-Path $wt.path ".worktree-meta.json"
        $shouldClean = $false
        $task = "unknown"
        if (Test-Path $metaFile) {
            $meta = Get-Content $metaFile | ConvertFrom-Json
            $task = $meta.task
            $created = [DateTime]::Parse($meta.createdAt)
            $age = ((Get-Date) - $created).TotalDays
            if ($age -gt $Days) { $shouldClean = $true }
        }
        if ($shouldClean) {
            Write-Host "  清理: $($wt.branch) (task=$task, age=$([math]::Round($age,1))d)" -ForegroundColor Yellow
            git worktree remove --force $wt.path 2>&1 | Out-Null
            git branch -D $wt.branch 2>&1 | Out-Null
            $cleaned++
        }
    }
    if ($cleaned -eq 0) { Write-Host "  无过期工作区需要清理。" -ForegroundColor Green }
    else { Write-Host "[WORKTREE] 已清理 $cleaned 个过期工作区。" -ForegroundColor Green }
}

function Test-MergeReady {
    param([string]$Name)
    $output = git worktree list --porcelain 2>$null
    $targetPath = $null
    $current = @{}
    foreach ($line in ($output -split "`n")) {
        if ($line -match '^worktree\s+(.+)') { $current.path = $Matches[1] }
        elseif ($line -match '^branch\s+(.+)') { $current.branch = $Matches[1] }
        elseif ($line -match '^HEAD\s+(.+)') {
            if ($current.branch -like "*$Name*") { $targetPath = $current.path; break }
            $current = @{}
        }
    }
    if (-not $targetPath) {
        Write-Error "[WORKTREE] 未找到匹配 '$Name' 的工作区。"
        exit 1
    }
    Write-Host "[WORKTREE] 检查合并就绪状态: $Name" -ForegroundColor Cyan
    Write-Host "  路径: $targetPath"
    $checks = @()
    Push-Location $targetPath
    try {
        Write-Host "  [1/3] 编译检查..." -NoNewline
        $compileResult = mvn compile -q -f $repoRoot 2>&1
        if ($LASTEXITCODE -eq 0) { Write-Host " PASS" -ForegroundColor Green; $checks += "compile:PASS" }
        else { Write-Host " FAIL" -ForegroundColor Red; $checks += "compile:FAIL" }
        Write-Host "  [2/3] Linter 检查..." -NoNewline
        $pmdResult = mvn pmd:check -q -f $repoRoot 2>&1
        if ($LASTEXITCODE -eq 0) { Write-Host " PASS" -ForegroundColor Green; $checks += "pmd:PASS" }
        else { Write-Host " FAIL" -ForegroundColor Red; $checks += "pmd:FAIL" }
        Write-Host "  [3/3] 测试检查..." -NoNewline
        $testResult = mvn test -q -f $repoRoot -DskipTests=false 2>&1
        if ($LASTEXITCODE -eq 0) { Write-Host " PASS" -ForegroundColor Green; $checks += "test:PASS" }
        else { Write-Host " FAIL" -ForegroundColor Red; $checks += "test:FAIL" }
    } finally { Pop-Location }
    $allPass = ($checks | Where-Object { $_ -like "*:FAIL" }).Count -eq 0
    if ($allPass) { Write-Host "[WORKTREE] 合并就绪: 全部检查通过" -ForegroundColor Green }
    else { Write-Host "[WORKTREE] 合并就绪: 有未通过检查,请修复后再合并" -ForegroundColor Red }
    return $allPass
}

switch ($Action) {
    "create" { New-Worktree -Name $BranchName }
    "list"   { Show-Worktrees }
    "clean"  { Remove-StaleWorktrees -Days $MaxDays }
    "status" { Test-MergeReady -Name $BranchName }
}

5.4 decay-detector.ps1 --- 自动衰减检测

触发时机auto-governance.md Step 2

检测维度

维度 检测方式 衰减信号
文档过期 last_updated 距今 > 90 天 stale / critical
代码-文档漂移 文件存在但全景索引未注册 orphan
规则命中率 Linter 规则近 30 天触发次数 = 0 dead-rule
技术栈对齐 pom.xml 版本 vs always-on.md 声明 mismatch

输出:decay-report.json,包含 overallScore(0~100)。

实际示例

通过agent生成

bash 复制代码
<#
.SYNOPSIS
  自动衰减检测脚本 --- 检测文档/规则/约束随时间推移的可信度下降
.DESCRIPTION
  Harness 自动化层组件之三:检测四个维度的衰减:
  1. 文档过期:last_updated 超过 90 天的 .md 文件
  2. 代码-文档漂移:源码变更但对应设计文档未同步更新
  3. 规则零命中:Linter 规则从未被触发(可能已过时或覆盖不足)
  4. 技术栈基线对齐:AGENTS.md 技术栈与 pom.xml 实际版本是否一致
  输出 decay-report.json 供 AI Agent 读取决策。
.EXAMPLE
  .\decay-detector.ps1
  .\decay-detector.ps1 -Verbose
#>

param([switch]$Verbose = $false)
$ErrorActionPreference = "Continue"
$repoRoot = (git rev-parse --show-toplevel 2>$null | Out-String).Trim()
if (-not $repoRoot) { Write-Error "[DECAY] 请在 Git 仓库内执行"; exit 1 }

$sidecarDir = Join-Path $repoRoot "build\automation\sidecar"
if (-not (Test-Path $sidecarDir)) { New-Item -ItemType Directory -Path $sidecarDir -Force | Out-Null }
$timestamp = (Get-Date).ToString("o")
$now = Get-Date
$staleThreshold = 90
$driftThreshold = 30

# ── 1. 文档过期检测 ──
Write-Host "[DECAY] [1/4] 文档过期检测..." -ForegroundColor Cyan
$staleDocs = @()
$mdFiles = Get-ChildItem -Path $repoRoot -Filter "*.md" -Recurse -ErrorAction SilentlyContinue |
    Where-Object { $_.FullName -notmatch '\\target\\' -and $_.FullName -notmatch '\\\.git\\' -and $_.FullName -notmatch '\\\.qoder\\cache\\' }
foreach ($md in $mdFiles) {
    $content = Get-Content $md.FullName -Raw -ErrorAction SilentlyContinue
    if (-not $content) { continue }
    $lastUpdated = $null
    if ($content -match 'last_updated:\s*(\d{4}-\d{2}-\d{2})') {
        $lastUpdated = [DateTime]::ParseExact($Matches[1], "yyyy-MM-dd", $null)
        $age = ($now - $lastUpdated).TotalDays
        if ($age -gt $staleThreshold) {
            $staleDocs += @{
                file = $md.FullName.Replace($repoRoot + "\", "").Replace("\", "/")
                lastUpdated = $Matches[1]
                daysOld = [math]::Round($age, 0)
            }
            if ($Verbose) { Write-Host "  STALE: $($md.Name) ($([math]::Round($age,0))d old)" -ForegroundColor Yellow }
        }
    }
}
Write-Host "  过期文档: $($staleDocs.Count) 个" -ForegroundColor $(if ($staleDocs.Count -eq 0) { "Green" } else { "Yellow" })

# ── 2. 代码-文档漂移检测 ──
Write-Host "[DECAY] [2/4] 代码-文档漂移检测..." -ForegroundColor Cyan
$drifts = @()
# 源码→设计文档映射规则
$docMappings = @(
    @{ pattern = 'SyncSingle|sync.*single'; docs = @('docs/design/feature-sync-single-request.md') },
    @{ pattern = 'async.*batch|sharding|partition'; docs = @('docs/design/feature-async-batch-sharding.md') },
    @{ pattern = 'Vendor|vendor|ext.*tc|ext.*ym|ext.*bh|tianchuang|youmeng|baihang'; docs = @('docs/design/feature-vendor-adapter.md') },
    @{ pattern = 'RateLimit|rateLimit|Sentinel|sentinel'; docs = @('docs/design/feature-rate-limiting.md') },
    @{ pattern = 'TraceId|traceId|MDC|trace'; docs = @('docs/design/feature-trace-propagation.md') },
    @{ pattern = 'Scheduler|scheduler|XXL|xxl|ScheduleTask'; docs = @('docs/design/feature-scheduler-framework.md') }
)
$javaFiles = Get-ChildItem -Path $repoRoot -Filter "*.java" -Recurse -ErrorAction SilentlyContinue |
    Where-Object { $_.FullName -notmatch '\\target\\' -and $_.FullName -notmatch '\\test\\' }
$checkedFiles = 0
foreach ($java in $javaFiles) {
    $relativePath = $java.FullName.Replace($repoRoot + "\", "").Replace("\", "/")
    # 获取源码最后修改日期(git log)
    $lastCommitDate = git log -1 --format="%ci" -- $relativePath 2>$null | Out-String
    if (-not $lastCommitDate.Trim()) { continue }
    try { $codeDate = [DateTime]::Parse($lastCommitDate.Trim()) } catch { continue }
    $checkedFiles++
    # 匹配对应文档
    foreach ($mapping in $docMappings) {
        if ($java.Name -match $mapping.pattern -or $relativePath -match $mapping.pattern) {
            foreach ($docPath in $mapping.docs) {
                $fullDocPath = Join-Path $repoRoot ($docPath -replace '/', '\')
                if (Test-Path $fullDocPath) {
                    $docContent = Get-Content $fullDocPath -Raw -ErrorAction SilentlyContinue
                    if ($docContent -match 'last_updated:\s*(\d{4}-\d{2}-\d{2})') {
                        $docDate = [DateTime]::ParseExact($Matches[1], "yyyy-MM-dd", $null)
                        $driftDays = ($codeDate - $docDate).TotalDays
                        if ($driftDays -gt $driftThreshold) {
                            $drifts += @{
                                codeFile = $relativePath
                                codeLastCommit = $codeDate.ToString("yyyy-MM-dd")
                                linkedDoc = $docPath
                                docLastUpdated = $Matches[1]
                                driftDays = [math]::Round($driftDays, 0)
                            }
                            if ($Verbose) { Write-Host "  DRIFT: $relativePath → $docPath (drift=$([math]::Round($driftDays,0))d)" -ForegroundColor Yellow }
                        }
                    }
                }
            }
            break
        }
    }
}
# 去重(同一个 doc 可能被多个 java 文件触发)
$drifts = $drifts | Sort-Object linkedDoc, driftDays -Descending | Group-Object linkedDoc | ForEach-Object { $_.Group[0] }
Write-Host "  检查源码文件: $checkedFiles 个" -ForegroundColor DarkGray
Write-Host "  代码-文档漂移: $($drifts.Count) 处" -ForegroundColor $(if ($drifts.Count -eq 0) { "Green" } else { "Yellow" })

# ── 3. Linter 规则零命中检测 ──
Write-Host "[DECAY] [3/4] Linter 规则命中率检测..." -ForegroundColor Cyan
$ruleCoverage = @{ totalRules = 35; hitRules = 0; zeroHitRules = @(); coverage = 0 }
$pmdViolationsFile = Join-Path $sidecarDir "linter-violations.json"
$csViolationsFile = Join-Path $sidecarDir "checkstyle-violations.json"
$hitRules = @{}
if (Test-Path $pmdViolationsFile) {
    $pmdData = Get-Content $pmdViolationsFile -Raw | ConvertFrom-Json
    if ($pmdData.byRule) {
        foreach ($key in $pmdData.byRule.PSObject.Properties.Name) { $hitRules[$key] = $true }
    }
}
if (Test-Path $csViolationsFile) {
    $csData = Get-Content $csViolationsFile -Raw | ConvertFrom-Json
    if ($csData.byRule) {
        foreach ($key in $csData.byRule.PSObject.Properties.Name) {
            $ruleName = if ($key -match 'EXCHANGE') { $key } else { "EXCHANGE-CS-$key" }
            $hitRules[$ruleName] = $true
        }
    }
}
$ruleCoverage.hitRules = $hitRules.Count
$ruleCoverage.coverage = [math]::Round($hitRules.Count / $ruleCoverage.totalRules, 4)
# 已知规则列表(与 pmd-exchange-ruleset.xml + checkstyle-exchange.xml 对齐)
$allPmdRules = @(
    "ExchangeNoLoggerFactory","ExchangeNoSystemOut","ExchangeNoPrintStackTrace","ExchangeNoLogStringConcat",
    "ExchangeNoFieldAutowired","ExchangeNoConstructorAutowired","ExchangeNoNewOkHttpClient",
    "ExchangeNoTransactional","ExchangeNoThreadPoolExecutor","ExchangeNoForkJoinPool",
    "EmptyCatchBlock","ExchangeNoAllArgsConstructor","ExchangeNoRunWith","ExchangeNoThreadSleep",
    "ExchangeNoMapperFieldInjection","ExchangeNoSystemExit"
)
$allCsRules = @("IllegalImport","FileLength","UnusedImports","CustomImportOrder")
foreach ($r in $allPmdRules) { if (-not $hitRules.ContainsKey($r)) { $ruleCoverage.zeroHitRules += $r } }
foreach ($r in $allCsRules) { if (-not $hitRules.ContainsKey($r)) { $ruleCoverage.zeroHitRules += $r } }
Write-Host "  命中规则: $($ruleCoverage.hitRules)/$($ruleCoverage.totalRules) (覆盖率=$([math]::Round($ruleCoverage.coverage*100,1))%)" -ForegroundColor $(if ($ruleCoverage.coverage -ge 0.5) { "Green" } else { "Yellow" })
if ($ruleCoverage.zeroHitRules.Count -gt 0 -and $Verbose) {
    Write-Host "  零命中规则:" -ForegroundColor DarkGray
    foreach ($r in $ruleCoverage.zeroHitRules) { Write-Host "    - $r" -ForegroundColor DarkGray }
}

# ── 4. 技术栈基线对齐检测 ──
Write-Host "[DECAY] [4/4] 技术栈基线对齐检测..." -ForegroundColor Cyan
$techStack = @{ aligned = $true; mismatches = @() }
$agentsContent = Get-Content (Join-Path $repoRoot "AGENTS.md") -Raw -ErrorAction SilentlyContinue
$pomContent = Get-Content (Join-Path $repoRoot "pom.xml") -Raw -ErrorAction SilentlyContinue
# 从 pom.xml 提取实际版本
$versionChecks = @(
    @{ name = "JDK"; agentsValue = "1.8"; pomPattern = '<maven\.compiler\.source>([^<]+)</maven\.compiler\.source>' },
    @{ name = "Spring Boot"; agentsValue = "2.3.12.RELEASE"; pomPattern = '<spring-boot\.version>([^<]+)</spring-boot\.version>' },
    @{ name = "MyBatis-Plus"; agentsValue = "3.4.3"; pomPattern = '<mybatis-plus\.version>([^<]+)</mybatis-plus\.version>' },
    @{ name = "FastJSON"; agentsValue = "1.2.83"; pomPattern = '<fastjson\.version>([^<]+)</fastjson\.version>' },
    @{ name = "OkHttp3"; agentsValue = "3.14.9"; pomPattern = '<okhttp\.version>([^<]+)</okhttp\.version>' },
    @{ name = "JUnit"; agentsValue = "5.6.3"; pomPattern = '<junit\.version>([^<]+)</junit\.version>' },
    @{ name = "Mockito"; agentsValue = "3.12.4"; pomPattern = '<mockito\.version>([^<]+)</mockito\.version>' }
)
foreach ($check in $versionChecks) {
    $pomVersion = $null
    if ($pomContent -match $check.pomPattern) { $pomVersion = $Matches[1] }
    if ($pomVersion -and $pomVersion -ne $check.agentsValue) {
        $techStack.aligned = $false
        $techStack.mismatches += @{
            component = $check.name
            agentsValue = $check.agentsValue
            pomValue = $pomVersion
        }
        Write-Host "  MISMATCH: $($check.name) --- AGENTS.md=$($check.agentsValue), pom.xml=$pomVersion" -ForegroundColor Yellow
    }
}
if ($techStack.aligned) { Write-Host "  技术栈基线一致" -ForegroundColor Green }

# ── 衰减评分 ──
$docScore = 100 - [math]::Min($staleDocs.Count * 10, 100)
$driftScore = 100 - [math]::Min($drifts.Count * 15, 100)
$coverageScore = [math]::Round($ruleCoverage.coverage * 100, 0)
$stackScore = if ($techStack.aligned) { 100 } else { 100 - $techStack.mismatches.Count * 20 }
$overallScore = [math]::Round(($docScore + $driftScore + $coverageScore + $stackScore) / 4, 0)

$decayReport = @{
    timestamp = $timestamp
    overallScore = $overallScore
    dimensions = @{
        docFreshness = @{ score = $docScore; staleCount = $staleDocs.Count; threshold = "$staleThreshold days" }
        codeDocAlignment = @{ score = $driftScore; driftCount = $drifts.Count; threshold = "$driftThreshold days drift" }
        ruleCoverage = @{ score = $coverageScore; hitRules = $ruleCoverage.hitRules; totalRules = $ruleCoverage.totalRules; zeroHitRules = $ruleCoverage.zeroHitRules }
        techStackAlignment = @{ score = $stackScore; aligned = $techStack.aligned; mismatches = $techStack.mismatches }
    }
    staleDocs = $staleDocs
    codeDocDrift = $drifts
    ruleCoverage = $ruleCoverage
    techStackAlignment = $techStack
}
$decayReport | ConvertTo-Json -Depth 4 | Set-Content (Join-Path $sidecarDir "decay-report.json")

Write-Host ""
Write-Host "[DECAY] 衰减检测完成" -ForegroundColor Green
Write-Host "  整体评分: $overallScore / 100" -ForegroundColor $(if ($overallScore -ge 80) { "Green" } elseif ($overallScore -ge 60) { "Yellow" } else { "Red" })
Write-Host "  文档新鲜度:   $docScore ($($staleDocs.Count) 个过期)"
Write-Host "  代码-文档对齐: $driftScore ($($drifts.Count) 处漂移)"
Write-Host "  规则覆盖率:   $coverageScore ($($ruleCoverage.hitRules)/$($ruleCoverage.totalRules) 命中)"
Write-Host "  技术栈对齐:   $stackScore ($($techStack.mismatches.Count) 个不一致)"
Write-Host ""
Write-Host "[DECAY] 报告已写入: $sidecarDir\decay-report.json"

5.5 governance-report.ps1 --- 持续治理报告

触发时机auto-governance.md Step 5

功能:汇总 sidecar 数据 + 衰减检测,生成 Markdown 治理报告。

报告内容

  1. 构建状态(编译/测试/Linter 通过率)
  2. 文档健康度(stale/critical/orphan 统计)
  3. 衰减评分(overallScore + 各维度明细)
  4. 建议清单(需人工处理的问题)

实际示例

通过agent生成

bash 复制代码
<#
.SYNOPSIS
  持续治理报告生成脚本 --- 汇总侧性数据 + 衰减报告,生成 Markdown 治理周报
.DESCRIPTION
  Harness 自动化层组件之四:读取 build/automation/sidecar/ 下的所有 JSON 数据,
  结合衰减检测结果,生成结构化的治理报告(Markdown),保存到 docs/changes/ 目录。
  AI Agent 可读取此报告了解项目整体健康状态并制定修复计划。
  也可手动运行用于定期治理巡检。
.EXAMPLE
  .\governance-report.ps1                # 生成报告
  .\governance-report.ps1 -RunCollect     # 先采集侧性数据再生成报告
#>

param([switch]$RunCollect = $false)
$ErrorActionPreference = "Continue"
$repoRoot = (git rev-parse --show-toplevel 2>$null | Out-String).Trim()
if (-not $repoRoot) { Write-Error "[REPORT] 请在 Git 仓库内执行"; exit 1 }

$sidecarDir = Join-Path $repoRoot "build\automation\sidecar"
$reportDir = Join-Path $repoRoot "docs\changes"
$reportDate = Get-Date -Format "yyyy-MM-dd"
$reportPath = Join-Path $reportDir "governance-report-$reportDate.md"

# 可选:先运行采集
if ($RunCollect) {
    Write-Host "[REPORT] 先运行侧性数据采集..." -ForegroundColor Cyan
    $collectScript = Join-Path $repoRoot "build\automation\scripts\collect-sidecar.ps1"
    if (Test-Path $collectScript) { & powershell -File $collectScript }
    $decayScript = Join-Path $repoRoot "build\automation\scripts\decay-detector.ps1"
    if (Test-Path $decayScript) { & powershell -File $decayScript }
}

# 加载侧性数据
function Read-Json($path) {
    if (Test-Path $path) { return Get-Content $path -Raw | ConvertFrom-Json }
    return $null
}
$buildStatus = Read-Json (Join-Path $sidecarDir "build-status.json")
$testResults = Read-Json (Join-Path $sidecarDir "test-results.json")
$pmdData = Read-Json (Join-Path $sidecarDir "linter-violations.json")
$csData = Read-Json (Join-Path $sidecarDir "checkstyle-violations.json")
$decay = Read-Json (Join-Path $sidecarDir "decay-report.json")

# 历史趋势(读取上一个报告)
$prevReports = Get-ChildItem -Path $reportDir -Filter "governance-report-*.md" | Sort-Object Name -Descending
$prevScore = $null
if ($prevReports.Count -gt 1) {
    $prevContent = Get-Content $prevReports[1].FullName -Raw
    if ($prevContent -match '整体评分.*?(\d+)\s*/\s*100') { $prevScore = [int]$Matches[1] }
}

# ── 生成 Markdown 报告 ──
$sb = [System.Text.StringBuilder]::new()
[void]$sb.AppendLine("---")
[void]$sb.AppendLine("last_updated: $reportDate")
[void]$sb.AppendLine("status: active")
[void]$sb.AppendLine("owner: @auto-governance")
[void]$sb.AppendLine("---")
[void]$sb.AppendLine("")
[void]$sb.AppendLine("# 治理报告 $reportDate")
[void]$sb.AppendLine("")
[void]$sb.AppendLine("> 本报告由 `governance-report.ps1` 自动生成,基于客观数据(构建/测试/Linter/衰减检测)。")
[void]$sb.AppendLine("> AI Agent 可读取此报告制定修复计划。")
[void]$sb.AppendLine("")

# ── 1. 执行摘要 ──
[void]$sb.AppendLine("## 执行摘要")
[void]$sb.AppendLine("")
$score = if ($decay) { $decay.overallScore } else { "N/A" }
$scoreColor = if ($score -is [int]) { if ($score -ge 80) { "健康" } elseif ($score -ge 60) { "关注" } else { "风险" } } else { "未知" }
[void]$sb.AppendLine("| 指标 | 值 | 状态 |")
[void]$sb.AppendLine("|------|-----|------|")
if ($prevScore) {
    $trend = $score - $prevScore
    $trendStr = if ($trend -gt 0) { "↑ +$trend" } elseif ($trend -lt 0) { "↓ $trend" } else { "→ 持平" }
    [void]$sb.AppendLine("| 整体评分 | $score / 100 | $scoreColor (上周: $prevScore, $trendStr) |")
} else {
    [void]$sb.AppendLine("| 整体评分 | $score / 100 | $scoreColor |")
}
$buildStr = if ($buildStatus) { if ($buildStatus.compileSuccess) { "PASS" } else { "FAIL ($($buildStatus.errorCount) errors)" } } else { "N/A" }
[void]$sb.AppendLine("| 编译状态 | $buildStr | $(if ($buildStatus.compileSuccess) { '正常' } else { '异常' }) |")
$testStr = if ($testResults) { "$($testResults.passed)/$($testResults.totalTests) (通过率=$([math]::Round($testResults.passRate*100,1))%)" } else { "N/A" }
[void]$sb.AppendLine("| 测试通过率 | $testStr | $(if ($testResults -and $testResults.failed -eq 0) { '正常' } else { '异常' }) |")
$pmdStr = if ($pmdData) { $pmdData.total } else { "N/A" }
[void]$sb.AppendLine("| PMD 违规 | $pmdStr | $(if ($pmdData -and $pmdData.total -eq 0) { '正常' } else { '需修复' }) |")
$csStr = if ($csData) { $csData.total } else { "N/A" }
[void]$sb.AppendLine("| Checkstyle 违规 | $csStr | $(if ($csData -and $csData.total -eq 0) { '正常' } else { '需修复' }) |")
$staleStr = if ($decay) { $decay.dimensions.docFreshness.staleCount } else { "N/A" }
[void]$sb.AppendLine("| 过期文档 | $staleStr | $(if ($staleStr -eq 0) { '正常' } else { '需更新' }) |")
$driftStr = if ($decay) { $decay.dimensions.codeDocAlignment.driftCount } else { "N/A" }
[void]$sb.AppendLine("| 代码-文档漂移 | $driftStr | $(if ($driftStr -eq 0) { '正常' } else { '需同步' }) |")
[void]$sb.AppendLine("")

# ── 2. 衰减详情 ──
if ($decay) {
    [void]$sb.AppendLine("## 衰减检测")
    [void]$sb.AppendLine("")
    [void]$sb.AppendLine("| 维度 | 评分 | 详情 |")
    [void]$sb.AppendLine("|------|------|------|")
    [void]$sb.AppendLine("| 文档新鲜度 | $($decay.dimensions.docFreshness.score) | $($decay.dimensions.docFreshness.staleCount) 个过期 |")
    [void]$sb.AppendLine("| 代码-文档对齐 | $($decay.dimensions.codeDocAlignment.score) | $($decay.dimensions.codeDocAlignment.driftCount) 处漂移 |")
    [void]$sb.AppendLine("| 规则覆盖率 | $($decay.dimensions.ruleCoverage.score) | $($decay.dimensions.ruleCoverage.hitRules)/$($decay.dimensions.ruleCoverage.totalRules) 命中 |")
    [void]$sb.AppendLine("| 技术栈对齐 | $($decay.dimensions.techStackAlignment.score) | $(if ($decay.dimensions.techStackAlignment.aligned) { '一致' } else { '不一致' }) |")
    [void]$sb.AppendLine("")
    # 过期文档列表
    if ($decay.staleDocs -and $decay.staleDocs.Count -gt 0) {
        [void]$sb.AppendLine("### 过期文档(>90 天)")
        [void]$sb.AppendLine("")
        [void]$sb.AppendLine("| 文档 | 最后更新 | 过期天数 |")
        [void]$sb.AppendLine("|------|---------|---------|")
        foreach ($d in $decay.staleDocs) { [void]$sb.AppendLine("| $($d.file) | $($d.lastUpdated) | $($d.daysOld)d |") }
        [void]$sb.AppendLine("")
    }
    # 代码-文档漂移列表
    if ($decay.codeDocDrift -and $decay.codeDocDrift.Count -gt 0) {
        [void]$sb.AppendLine("### 代码-文档漂移")
        [void]$sb.AppendLine("")
        [void]$sb.AppendLine("| 源码文件 | 代码最后提交 | 关联文档 | 文档最后更新 | 漂移天数 |")
        [void]$sb.AppendLine("|---------|------------|---------|------------|---------|")
        foreach ($dr in $decay.codeDocDrift) { [void]$sb.AppendLine("| $($dr.codeFile) | $($dr.codeLastCommit) | $($dr.linkedDoc) | $($dr.docLastUpdated) | $($dr.driftDays)d |") }
        [void]$sb.AppendLine("")
    }
    # 零命中规则
    if ($decay.ruleCoverage.zeroHitRules -and $decay.ruleCoverage.zeroHitRules.Count -gt 0) {
        [void]$sb.AppendLine("### 零命中 Linter 规则(需观察是否过时)")
        [void]$sb.AppendLine("")
        foreach ($r in $decay.ruleCoverage.zeroHitRules) { [void]$sb.AppendLine("- `$r`") }
        [void]$sb.AppendLine("")
    }
    # 技术栈不一致
    if (-not $decay.techStackAlignment.aligned) {
        [void]$sb.AppendLine("### 技术栈不一致")
        [void]$sb.AppendLine("")
        [void]$sb.AppendLine("| 组件 | AGENTS.md | pom.xml |")
        [void]$sb.AppendLine("|------|-----------|---------|")
        foreach ($m in $decay.techStackAlignment.mismatches) { [void]$sb.AppendLine("| $($m.component) | $($m.agentsValue) | $($m.pomValue) |") }
        [void]$sb.AppendLine("")
    }
}

# ── 3. Linter 违规详情 ──
if ($pmdData -and $pmdData.total -gt 0) {
    [void]$sb.AppendLine("## PMD 违规详情")
    [void]$sb.AppendLine("")
    [void]$sb.AppendLine("| 规则 | 数量 |")
    [void]$sb.AppendLine("|------|------|")
    foreach ($prop in $pmdData.byRule.PSObject.Properties) { [void]$sb.AppendLine("| $($prop.Name) | $($prop.Value) |") }
    [void]$sb.AppendLine("")
    if ($pmdData.violations -and $pmdData.violations.Count -gt 0) {
        [void]$sb.AppendLine("### 违规明细(前 10 条)")
        [void]$sb.AppendLine("")
        [void]$sb.AppendLine("| 文件 | 行 | 规则 | 消息 |")
        [void]$sb.AppendLine("|------|-----|------|------|")
        foreach ($v in ($pmdData.violations | Select-Object -First 10)) {
            $msg = if ($v.message) { ($v.message -replace '\|', '/' -replace "`n", " ").Substring(0, [math]::Min($v.message.Length, 80)) } else { "" }
            [void]$sb.AppendLine("| $($v.file) | $($v.line) | $($v.rule) | $msg |")
        }
        [void]$sb.AppendLine("")
    }
}

# ── 4. 测试失败详情 ──
if ($testResults -and $testResults.failed -gt 0) {
    [void]$sb.AppendLine("## 测试失败详情")
    [void]$sb.AppendLine("")
    [void]$sb.AppendLine("| 测试类 | 方法 | 消息 |")
    [void]$sb.AppendLine("|--------|------|------|")
    foreach ($f in $testResults.failures) {
        $msg = if ($f.message) { ($f.message -replace '\|', '/' -replace "`n", " ").Substring(0, [math]::Min($f.message.Length, 100)) } else { "" }
        [void]$sb.AppendLine("| $($f.className) | $($f.methodName) | $msg |")
    }
    [void]$sb.AppendLine("")
}

# ── 5. 行动项 ──
[void]$sb.AppendLine("## 行动项")
[void]$sb.AppendLine("")
$actionIdx = 1
if ($buildStatus -and -not $buildStatus.compileSuccess) { [void]$sb.AppendLine("$actionIdx. **[紧急]** 修复编译错误($($buildStatus.errorCount) 个)"); $actionIdx++ }
if ($testResults -and $testResults.failed -gt 0) { [void]$sb.AppendLine("$actionIdx. **[紧急]** 修复失败测试($($testResults.failed) 个)"); $actionIdx++ }
if ($pmdData -and $pmdData.total -gt 0) { [void]$sb.AppendLine("$actionIdx. **[高]** 修复 PMD 违规($($pmdData.total) 个)--- 参见 docs/conventions/linter-rules.md"); $actionIdx++ }
if ($csData -and $csData.total -gt 0) { [void]$sb.AppendLine("$actionIdx. **[高]** 修复 Checkstyle 违规($($csData.total) 个)--- 参见 docs/conventions/linter-rules.md"); $actionIdx++ }
if ($decay -and $decay.staleDocs.Count -gt 0) { [void]$sb.AppendLine("$actionIdx. **[中]** 更新过期文档($($decay.staleDocs.Count) 个)--- 执行 `/doc-gardening`"); $actionIdx++ }
if ($decay -and $decay.codeDocDrift.Count -gt 0) { [void]$sb.AppendLine("$actionIdx. **[中]** 同步代码-文档漂移($($decay.codeDocDrift.Count) 处)--- 源码已变更但文档未更新"); $actionIdx++ }
if ($decay -and -not $decay.techStackAlignment.aligned) { [void]$sb.AppendLine("$actionIdx. **[中]** 对齐技术栈基线($($decay.techStackAlignment.mismatches.Count) 个不一致)--- 更新 AGENTS.md 或 pom.xml"); $actionIdx++ }
if ($decay -and $decay.ruleCoverage.zeroHitRules.Count -gt 5) { [void]$sb.AppendLine("$actionIdx. **[低]** 评估零命中 Linter 规则($($decay.ruleCoverage.zeroHitRules.Count) 条)--- 确认是否需要保留"); $actionIdx++ }
if ($actionIdx -eq 1) { [void]$sb.AppendLine("无待办行动项,项目健康状态良好。") }
[void]$sb.AppendLine("")
[void]$sb.AppendLine("---")
[void]$sb.AppendLine("*生成时间: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') | 数据来源: build/automation/sidecar/*")

# 写入文件
$sb.ToString() | Set-Content $reportPath -Encoding UTF8
Write-Host "[REPORT] 治理报告已生成: $reportPath" -ForegroundColor Green
Write-Host "[REPORT] 整体评分: $score / 100" -ForegroundColor $(if ($score -ge 80) { "Green" } elseif ($score -ge 60) { "Yellow" } else { "Red" })

5.6 index-sync-check.ps1 --- 文档全景索引一致性检测

触发时机:PR 自检清单中的"文档同步检查"项

检测逻辑

  1. 扫描磁盘上的 .md/.yaml/.ps1/.xml 文件
  2. 解析 ai-coding-guide.md §5 全景索引
  3. 对比磁盘文件 vs 索引文件
  4. 输出差异:磁盘有但索引无 / 索引有但磁盘无

输出格式

json 复制代码
{
  "diskFiles": 35,
  "indexFiles": 33,
  "missingInIndex": ["docs/new-file.md"],
  "missingInDisk": [],
  "consistent": false
}

实际示例

通过agent生成

bash 复制代码
<#
.SYNOPSIS
  文档全景索引一致性检测脚本 --- 确保 ai-coding-guide.md §5 索引与磁盘实际文件一致
.DESCRIPTION
  Harness 自动化层组件:扫描 .qoder/ 和 docs/ 目录下的实际 .md/.yaml 文件,
  与 docs/ai-coding-guide.md §5「文档全景索引」中的文件条目进行比对,
  输出缺失项(磁盘有但索引无)和幽灵项(索引有但磁盘无)。
  输出 index-sync-report.json 供 AI Agent 和 doc-gardening 读取决策。
.EXAMPLE
  .\index-sync-check.ps1
  .\index-sync-check.ps1 -Verbose
#>

param([switch]$Verbose = $false)
$ErrorActionPreference = "Continue"
$repoRoot = (git rev-parse --show-toplevel 2>$null | Out-String).Trim()
if (-not $repoRoot) { Write-Error "[INDEX-SYNC] 请在 Git 仓库内执行"; exit 1 }

$sidecarDir = Join-Path $repoRoot "build\automation\sidecar"
if (-not (Test-Path $sidecarDir)) { New-Item -ItemType Directory -Path $sidecarDir -Force | Out-Null }
$timestamp = (Get-Date).ToString("o")

# ── 1. 收集磁盘实际文件 ──
Write-Host "[INDEX-SYNC] [1/3] 收集磁盘实际文件..." -ForegroundColor Cyan

$actualFiles = @()

# .qoder/rules/*.md
$qoderRulesDir = Join-Path $repoRoot ".qoder\rules"
if (Test-Path $qoderRulesDir) {
    Get-ChildItem -Path $qoderRulesDir -Filter "*.md" | ForEach-Object {
        $actualFiles += $_.Name
    }
}

# .qoder/skills/*.md
$qoderSkillsDir = Join-Path $repoRoot ".qoder\skills"
if (Test-Path $qoderSkillsDir) {
    Get-ChildItem -Path $qoderSkillsDir -Filter "*.md" | ForEach-Object {
        $actualFiles += $_.Name
    }
}

# docs/**/*.md and docs/**/*.yaml
$docsDir = Join-Path $repoRoot "docs"
if (Test-Path $docsDir) {
    Get-ChildItem -Path $docsDir -Recurse -Include "*.md","*.yaml" -ErrorAction SilentlyContinue | ForEach-Object {
        $actualFiles += $_.Name
    }
}

# Project root files (§5.3)
foreach ($rootFile in @("AGENTS.md", "README.md", "CHANGELOG.md")) {
    $fullPath = Join-Path $repoRoot $rootFile
    if (Test-Path $fullPath) {
        $actualFiles += $rootFile
    }
}

# build/automation/scripts/*.ps1 (§5.4)
$scriptsDir = Join-Path $repoRoot "build\automation\scripts"
if (Test-Path $scriptsDir) {
    Get-ChildItem -Path $scriptsDir -Filter "*.ps1" | ForEach-Object {
        $actualFiles += $_.Name
    }
}

# build/linter/*.xml (§5.5)
$linterDir = Join-Path $repoRoot "build\linter"
if (Test-Path $linterDir) {
    Get-ChildItem -Path $linterDir -Filter "*.xml" | ForEach-Object {
        $actualFiles += $_.Name
    }
}

# 去重并排序
$actualFiles = $actualFiles | Sort-Object -Unique
Write-Host "  磁盘文件数: $($actualFiles.Count) 个" -ForegroundColor DarkGray

# ── 2. 解析 ai-coding-guide.md §5 索引 ──
Write-Host "[INDEX-SYNC] [2/3] 解析 ai-coding-guide.md §5 文档全景索引..." -ForegroundColor Cyan

$guidePath = Join-Path $repoRoot "docs\ai-coding-guide.md"
if (-not (Test-Path $guidePath)) {
    Write-Host "  ERROR: docs/ai-coding-guide.md 不存在" -ForegroundColor Red
    exit 1
}

$guideContent = Get-Content $guidePath -Raw -Encoding UTF8

# 提取 §5 章节(从 "## 5. 文档全景索引" 到下一个 "## " 或文件末尾)
$sectionPattern = '(?s)## 5\. 文档全景索引(.*?)(?=\n## |\Z)'
$sectionMatch = [regex]::Match($guideContent, $sectionPattern)
if (-not $sectionMatch.Success) {
    Write-Host "  ERROR: 未找到 §5 文档全景索引章节" -ForegroundColor Red
    exit 1
}

$sectionContent = $sectionMatch.Groups[1].Value

# 从代码块中提取文件名(匹配 .md 和 .yaml 文件)
# 匹配模式:行中包含 ├── 或 └── 后跟文件名
$indexedFiles = @()
$lines = $sectionContent -split "`n"
foreach ($line in $lines) {
    # 匹配代码块中的文件条目:├── filename.md / .yaml / .ps1 / .xml
    if ($line -match '[├└]──\s+(\S+\.(?:md|yaml|ps1|xml))') {
        $fileName = $Matches[1]
        # 排除目录名(以 / 结尾的被前面的正则排除,因为不以 .md/.yaml 结尾)
        $indexedFiles += $fileName
    }
}

# 去重并排序
$indexedFiles = $indexedFiles | Sort-Object -Unique
Write-Host "  索引文件数: $($indexedFiles.Count) 个" -ForegroundColor DarkGray

# ── 3. 比对差异 ──
Write-Host "[INDEX-SYNC] [3/3] 比对差异..." -ForegroundColor Cyan

$actualSet = [System.Collections.Generic.HashSet[string]]::new([string[]]$actualFiles)
$indexedSet = [System.Collections.Generic.HashSet[string]]::new([string[]]$indexedFiles)

# 磁盘有但索引无 → 缺失项
$missingFromIndex = @()
foreach ($f in $actualFiles) {
    if (-not $indexedSet.Contains($f)) {
        $missingFromIndex += $f
    }
}

# 索引有但磁盘无 → 幽灵项
$ghostEntries = @()
foreach ($f in $indexedFiles) {
    if (-not $actualSet.Contains($f)) {
        $ghostEntries += $f
    }
}

# 输出结果
$synced = ($missingFromIndex.Count -eq 0 -and $ghostEntries.Count -eq 0)

if ($synced) {
    Write-Host "  ✅ 索引与磁盘文件完全一致" -ForegroundColor Green
} else {
    if ($missingFromIndex.Count -gt 0) {
        Write-Host "  ⚠ 缺失项(磁盘有但索引无): $($missingFromIndex.Count) 个" -ForegroundColor Yellow
        foreach ($f in $missingFromIndex) {
            Write-Host "    - $f" -ForegroundColor Yellow
        }
    }
    if ($ghostEntries.Count -gt 0) {
        Write-Host "  ⚠ 幽灵项(索引有但磁盘无): $($ghostEntries.Count) 个" -ForegroundColor Yellow
        foreach ($f in $ghostEntries) {
            Write-Host "    - $f" -ForegroundColor Yellow
        }
    }
}

# ── 生成 JSON 报告 ──
$report = @{
    timestamp = $timestamp
    synced = $synced
    actualFileCount = $actualFiles.Count
    indexedFileCount = $indexedFiles.Count
    missingFromIndex = $missingFromIndex
    ghostEntries = $ghostEntries
    actualFiles = $actualFiles
    indexedFiles = $indexedFiles
}

$reportPath = Join-Path $sidecarDir "index-sync-report.json"
$report | ConvertTo-Json -Depth 3 | Set-Content $reportPath

Write-Host ""
Write-Host "[INDEX-SYNC] 检测完成" -ForegroundColor $(if ($synced) { "Green" } else { "Yellow" })
Write-Host "  实际文件: $($actualFiles.Count) | 索引文件: $($indexedFiles.Count) | 缺失: $($missingFromIndex.Count) | 幽灵: $($ghostEntries.Count)"
Write-Host "  报告路径: $reportPath"
Write-Host ""

if (-not $synced) { exit 2 } else { exit 0 }

6. 自动化层的门禁闭环

6.1 完整的门禁流程

复制代码
代码生成完成
    │
    ▼
Step 5.5: 编译验证(mvn test-compile)
    │ 失败 → 返回修复
    │ 通过 ↓
    ▼
Step 7.1: 自动治理巡检
    │
    ├─ collect-sidecar.ps1(采集客观数据)
    │   └─ build-status.json(编译结果)
    │   └─ test-results.json(测试结果)
    │   └─ linter-results.json(Linter 结果)
    │
    ├─ decay-detector.ps1(衰减检测)
    │   └─ decay-report.json(overallScore)
    │
    ├─ 门禁判断
    │   ├─ 编译失败 → 阻断交付
    │   ├─ 测试失败 → 阻断交付
    │   ├─ 衰减评分 < 60 → 人工确认
    │   └─ sidecar 过期 → 重新采集
    │
    ▼
Step 7.2: PR 自检清单
    │
    ├─ 规则合规检查(对照 always-on.md)
    ├─ 代码质量检查
    ├─ 测试检查
    ├─ 文档同步检查(含 index-sync-check.ps1)
    └─ 提交规范检查
    │
    ▼
交付

6.2 客观侧性优先原则

核心原则:当 AI Agent 的主观判断与 sidecar JSON 客观数据冲突时,以客观数据为准。

AI 主观判断 sidecar 客观数据 决策
"代码没问题" compileSuccess = false 以客观数据为准,修复编译错误
"测试应该通过" test-results failed > 0 以客观数据为准,修复失败测试
"文档是完整的" index-sync-check inconsistent 以客观数据为准,更新索引

7. 完整的端到端流程

从 BRD 到 PR 的完整路径:

复制代码
1. 业务方编写 BRD
   │
   ▼
2. 产品经理参照 brd-to-prd-guide.md 将 BRD 转为 PRD(参照 prd-template.md)
   │
   ▼
3. 提交 PRD 给 AI Agent
   │
   ▼
4. AI Agent 执行 prd-to-code 技能(7 步流程)
   │
   ├─ Step 0: 环境检查 + Worktree + PRD 校验
   ├─ Step 1: PRD 解析(加载 AGENTS.md + prd-template.md)
   ├─ Step 2: 影响分析(加载 boundaries.md + impact-analysis.md)
   ├─ Step 3: 设计文档生成(加载 _template.md + data-flow.md)
   ├─ Step 4: 设计评审(加载 design-review.md)
   │         ⚠️ 暂停 → 人工确认「通过」
   ├─ Step 5: 代码生成(加载 add-*.md)
   │         → 遵守 always-on.md 全部规则
   │         → 遵守 java-code-style.md 代码风格
   │         → 同步生成 JUnit 5 + Mockito 测试
   ├─ Step 5.5: 编译验证(mvn test-compile)
   ├─ Step 6: 文档同步(更新 error-codes.md + api.spec.yaml + CHANGELOG.md)
   └─ Step 7: 交付前自检
             ├─ 自动治理巡检(collect-sidecar + decay-detector)
             ├─ 门禁检查(编译/测试/衰减评分)
             └─ PR 自检清单(6 维度逐项检查)
   │
   ▼
5. 交付(生成 PR)
   │
   ▼
6. 提交后
   ├─ 执行 /doc-gardening(文档健康度扫描)
   └─ 更新 current-sprint.md 任务状态

8. 完成验证清单

完成本步后,逐项检查:

总指南

  • docs/ai-coding-guide.md --- 包含 §1~§9 全部章节
  • §2 四层文档体系图与实际文件一致
  • §3 八步流程的按需加载清单与 agent-workflow.md §4 一致
  • §5 文档全景索引包含全部已创建的文件(磁盘文件数 = 索引文件数)
  • §7 实战示例覆盖从 PRD 到交付的完整流程
  • §9 协作机制时序图包含开发者与 AI Agent 的完整交互

Linter 配置

  • build/linter/pmd-exchange-ruleset.xml --- 包含 always-on.md 对应的 PMD 代码质量规则
  • build/linter/checkstyle-exchange.xml --- 包含 java-code-style.md 对应的 Checkstyle 代码风格规则
  • ArchUnit 测试覆盖模块依赖方向(与 boundaries.md 一致)
  • Maven Enforcer 强制依赖方向约束

自动化脚本

  • pre-check.ps1 --- 检查 JDK / Maven / Git / 项目结构
  • worktree-manager.ps1 --- 支持 create / list / clean / status 命令
  • collect-sidecar.ps1 --- 采集编译/测试/Linter 结果到 JSON
  • decay-detector.ps1 --- 检测文档过期/漂移/规则命中率/技术栈对齐
  • governance-report.ps1 --- 生成 Markdown 治理报告
  • index-sync-check.ps1 --- 对比磁盘文件 vs 全景索引,输出差异

全局一致性

  • AGENTS.md 快速导航表包含全部文件(Rules 4 + Skills 8 + Wiki 11 + Changes 3 + 项目入口 3 + 脚本 6 + Linter 2)
  • 运行 index-sync-check.ps1 通过(磁盘文件数 = 索引文件数)
  • ai-coding-guide.md §5 全景索引与磁盘实际文件一致
  • pr-checklist.md 的自动化门禁检查项与脚本产出对应

9. 四层体系全景总结

完成全部 6 步后,你的项目应具备以下完整的 Harness Engineering 文档体系:

复制代码
项目根/
├── AGENTS.md                              ← Step 1: 项目入口
├── README.md                              ← Step 1: 项目说明
├── CHANGELOG.md                           ← Step 5: 变更记录
│
├── .qoder/
│   ├── rules/                             ← Step 2: Rules 层(4 文件)
│   │   ├── always-on.md
│   │   ├── agent-workflow.md
│   │   ├── java-code-style.md
│   │   └── java-testing.md
│   │
│   └── skills/                            ← Step 4: Skills 层(8 文件)
│       ├── prd-to-code.md
│       ├── flow-orchestration.md
│       ├── design-review.md
│       ├── add-vendor-adapter.md
│       ├── add-entity-mapper.md
│       ├── add-api-endpoint.md
│       ├── doc-gardening.md
│       └── auto-governance.md
│
├── docs/
│   ├── ai-coding-guide.md                 ← Step 6: 总指南
│   │
│   ├── architecture/                      ← Step 1: 架构基线(4 文件)
│   │   ├── overview.md
│   │   ├── boundaries.md
│   │   ├── data-flow.md
│   │   └── code-assets.md
│   │
│   ├── conventions/                       ← Step 3: 编码规范(6 文件)
│   │   ├── README.md
│   │   ├── naming.md
│   │   ├── error-handling.md
│   │   ├── testing.md
│   │   ├── logging.md
│   │   └── linter-rules.md
│   │
│   ├── design/                            ← Step 3: 设计模板(1 文件 + 按需)
│   │   └── _template.md
│   │
│   ├── reference/                         ← Step 3: 参考资料(4 文件)
│   │   ├── prd-template.md
│   │   ├── brd-to-prd-guide.md
│   │   ├── api.spec.yaml
│   │   └── error-codes.md
│   │
│   ├── changes/                           ← Step 5: 变更管理(3 文件)
│   │   ├── README.md
│   │   ├── pr-checklist.md
│   │   └── impact-analysis.md
│   │
│   ├── plans/                             ← Step 5: 迭代计划(2 文件)
│   │   ├── current-sprint.md
│   │   └── backlog.md
│   │
│   └── guides/                            ← 本系列 6 篇指南
│       ├── harness-step1-project-entry-and-architecture.md
│       ├── harness-step2-rules-layer.md
│       ├── harness-step3-wiki-layer.md
│       ├── harness-step4-skills-layer.md
│       ├── harness-step5-changes-layer.md
│       └── harness-step6-guide-and-automation.md
│
└── build/
    ├── linter/                             ← Step 6: Linter 配置(2 文件)
    │   ├── pmd-exchange-ruleset.xml
    │   └── checkstyle-exchange.xml
    │
    └── automation/scripts/                ← Step 6: 自动化脚本(6 文件)
        ├── pre-check.ps1
        ├── worktree-manager.ps1
        ├── collect-sidecar.ps1
        ├── decay-detector.ps1
        ├── governance-report.ps1
        └── index-sync-check.ps1

总计约 40 个文件(含本系列 6 篇指南),覆盖:

  • 4 个 Rules 文件:始终生效的硬性约束
  • 8 个 Skills 文件:按需触发的操作手册
  • ~17 个 Wiki 文件:人机共读的知识库
  • 3 个 Changes 文件:变更管理机制
  • 3 个项目入口文件AGENTS.md / README.md / CHANGELOG.md
  • 1 个总指南:全局编织
  • 2 个 Linter 配置:自动化规则拦截
  • 6 个自动化脚本:客观数据采集与治理
  • 6 篇搭建指南:本系列文档

10. 体系运转的核心机制

完成搭建后,体系通过以下机制持续运转:

10.1 PRD 驱动闭环

复制代码
(BRD) → PRD → 设计 → 评审(人工门禁)→ 编码 → 编译验证 → 文档同步 → 自检(自动门禁)→ PR

10.2 人工确认门禁

在设计评审环节,AI Agent 必须暂停等待人类确认。这是"AI 生成,人类驾驭"的核心体现。

10.3 客观数据门禁

在交付前自检环节,AI Agent 必须读取 sidecar JSON 做客观决策。编译失败/测试失败时禁止交付

10.4 文档一致性保障

复制代码
新增/删除文件 → 更新 ai-coding-guide.md §5 全景索引 → index-sync-check.ps1 验证一致性

10.5 文档健康度保障

复制代码
doc-gardening.md → 检测过期/orphan/索引不一致 → 标记并修复
decay-detector.ps1 → 检测代码-文档漂移/规则命中率 → 生成衰减报告

10.6 持续治理

复制代码
collect-sidecar.ps1 → 采集客观数据 → governance-report.ps1 → 生成治理报告 → 团队 review

至此,Harness Engineering 四层文档体系搭建完成。团队可以通过提交 PRD,由 AI Agent 在严格约束下完成从设计到交付的全流程,同时通过自动化门禁确保产出质量。