# 架构说明：面向 Java 开发者

## 先建立心智模型

这是一个典型的 ports-and-adapters 分层应用。核心业务不知道 PI API，也不知道
真实文件系统的具体实现；PI adapter 只把事件翻译成 application 调用。

![pi-collision-guard 工作机制](pi-collision-guard-mechanism.svg)

| TypeScript 目录                 | 职责                                           | Java 类比                                   |
| ------------------------------- | ---------------------------------------------- | ------------------------------------------- |
| `src/domain`                    | claim 数据、owner 身份、lease 决策             | domain entity、value object、domain service |
| `src/application`               | acquire、heartbeat、release、status 的用例编排 | application service / use case              |
| `src/infrastructure`            | 文件存储、路径规范化、进程探测、系统时钟       | repository adapter、gateway implementation  |
| `src/extension`                 | PI 事件、命令、状态栏、heartbeat timer         | controller / framework adapter              |
| `extensions/collision-guard.ts` | PI package 加载入口                            | 很薄的 framework bootstrap                  |

## TypeScript 与 Java 对照

### `interface` 与 `class`

`interface` 类似 Java interface，用来描述端口，例如 `ClaimStore`、
`ProcessProbe`、`Clock`。区别是 TypeScript 采用 structural typing：
对象只要形状匹配就可以实现接口，不必写 `implements`。`interface` 编译后不会
保留到 JavaScript runtime。

`class` 与 Java class 类似，构造器负责接收依赖，私有字段使用 `#field`。
这里没有 Spring 容器；对象在 composition root 中显式创建。

### `Promise<T>` 与 `CompletionStage<T>`

`Promise<T>` 可类比 `CompletionStage<T>`：成功时产生 `T`，失败时 rejection。
调用处用 `await` 写成顺序代码：

```ts
const result = await collisionGuard.acquire(request);
```

它没有 Java checked exception；错误通过 rejection/`throw` 传播。PI adapter
在 preflight 边界捕获错误，并返回 `{ block: true }`，实现 fail-closed。

### ESM

项目使用 ECMAScript Modules（`"type": "module"`），对应显式
`import`/`export`。源码内部相对导入写 `.js` 后缀，例如：

```ts
import { CollisionGuard } from "../application/collision-guard.js";
```

TypeScript 在 `NodeNext` 模式下把它解析到 `.ts` 源文件，构建产物则真的使用
`.js` 路径。这不是文件名写错，而是 Node ESM 的要求。

### 依赖注入

`CollisionGuard` 构造器接收五个端口：

- `ClaimStore`
- `LeasePolicy`
- `Clock`
- `ProcessProbe`
- `PathCanonicalizer`

生产环境注入 Node 实现；测试注入 fake clock 和 fake process probe。
这相当于手写 constructor injection，避免 domain/application 依赖 PI 或
Node 全局状态。

## 各层职责

### Domain

- `ClaimOwner`：`sessionId + ownerToken + pid`，三者共同标识一个 runtime owner。
- `ClaimRecord`：持久化 canonical path、owner 和时间戳。
- `DefaultLeasePolicy`：只根据进程状态、heartbeat 和 grace period 做纯决策。
- `decision.ts`：用 discriminated union 表达 acquired、conflict、reclaimed 等
  封闭结果，类似 Java sealed interface + record。

### Application

`CollisionGuard` 是 application service：

- canonicalize path；
- 在单路径 transaction 中读取 claim；
- 调用 policy 判断 active/stale；
- 保存、刷新或删除 claim；
- 提供 list/status/force release。

它不创建 timer，也不订阅 PI 事件。heartbeat 和 release 必须由调用方显式驱动。

### Infrastructure

`FileSystemClaimStore` 用
`SHA-256(canonicalPath).json` 表示单路径 claim，并用单独的 lock directory
串行化同一路径的 transaction。claim 先写入同目录临时文件，再 `rename` 到目标，
避免读到半份 JSON。

`NodePathCanonicalizer` 对现有路径执行 `realpath`；目标不存在时，从最近的现有父
目录开始还原剩余 segment。`NodeProcessProbe` 使用 `process.kill(pid, 0)`
判断进程 alive/dead/unknown。

### Extension adapter

`PiCollisionGuardAdapter` 只负责 framework concern：

- 把 `session_start` 转成 timer/status 初始化；
- 把 built-in `edit` / `write` 的 `tool_call` 转成 `acquire`；
- 把 application 的 conflict/error 转成 PI 的 block result；
- 把 `agent_settled` / `session_shutdown` 转成 release；
- 把 `/collision` 命令转成 list/force release；
- 更新 PI 状态栏和通知。

## 为什么 PI adapter 要薄

如果 stale 判断、文件 transaction 或 canonicalization 写进 PI handler：

1. 核心规则只能通过伪造大量 PI context 测试；
2. framework event 生命周期会和 lease 规则耦合；
3. 未来更换存储或嵌入其他调用方时需要复制逻辑。

现在 adapter 只做“协议翻译”，22 项测试可以分别验证核心规则和 PI wiring。
这是必要的隔离，不是为了增加抽象层数。

## 从 `tool_call` 到 release 的短 trace

成功路径：

```text
PI tool_call(edit/write)
  -> PiCollisionGuardAdapter.onToolCall
  -> CollisionGuard.acquire
  -> NodePathCanonicalizer.canonicalize
  -> FileSystemClaimStore.transact
       -> 获取该 claim ID 的 operation lock
       -> 读取 snapshot
       -> DefaultLeasePolicy 判断是否可获取/接管
       -> 原子写入 ClaimRecord
  -> adapter 记录 held path
  -> 返回 undefined，PI 继续执行 built-in tool

每 5 秒
  -> adapter heartbeat timer
  -> CollisionGuard.heartbeat
  -> 刷新当前 owner 的 heartbeatAt

PI agent_settled
  -> CollisionGuard.releaseOwner
  -> 仅删除 owner 三元组完全匹配的 claims
  -> 清空 adapter held paths
```

冲突路径只在 acquire 决策处不同：

```text
发现其他 active owner
  -> CollisionGuard 返回 conflict
  -> adapter 返回 { block: true, reason }
  -> built-in edit/write 不执行
```

`session_shutdown` 是兜底 release；如果进程崩溃，后续 acquire 会根据 PID 与
30 秒 stale 规则恢复。损坏 claim 则先经过 30 秒 corrupt grace。

## 设计边界

该架构保证的是参与者之间的 cooperative coordination，不是 OS 文件锁或安全
边界。bash、Git、IDE、未加载扩展的 PI、不同用户/主机、NFS、hardlink 与
TOCTOU 都不经过 application service，因此不在保证范围内。完整边界和恢复流程见
[README](../README.md)。
