# RFC: `cr analyze program-diff <GIT REF>`

状态：Draft
日期：2026-05-12

---

## 1. 概要

新增命令：

- `cr analyze program-diff <GIT REF>`

该命令从 Git 中读取指定版本的完整运行时快照文件（默认沿用当前 `cr` 的输入文件路径，优先 `calcit.cirru`，兼容旧文件名 `compact.cirru`），确保**历史版本**与**当前工作区版本**都能被正确解析为 Calcit snapshot，然后做**结构化 diff**。

输出目标不是纯文本行 diff，而是面向程序结构的树形 diff：

- 顶层展示 `package/configs/entries/files` 等结构；
- 文件层展示 `ns/defs`；
- definition 层展示 `doc/schema/examples/code`；
- `code/examples/ns.code` 内部继续深入到 Cirru 表达式节点级别；
- 无变化分支允许灰色折叠；
- 变更分支默认展开；
- 增加、删除、修改分别用不同符号和颜色表示。

## 2. 动机

当前针对运行时快照文件的版本对比主要依赖 Git 行 diff，但它有几个明显问题：

1. **对结构不友好**：`calcit.cirru`（兼容旧文件名 `compact.cirru`）是机器生成的 snapshot，行 diff 容易受格式、字段顺序和长表达式影响；
2. **对表达式内部变化不友好**：definition 代码改动往往是局部子树变化，行 diff 很难快速看出 AST 层面的增删改；
3. **对 LLM / CLI 阅读不友好**：调用 `git diff` 后很难一眼区分“只是某个 def 变了”还是“整个 namespace 被搬动了”；
4. **缺少 parse guard**：历史版本文件如果本身损坏，应该先明确报解析失败，而不是直接进入错误 diff。

因此需要一个基于 Calcit 自身 snapshot/Cirru 结构的 diff 命令。

## 3. 命令定义

```/dev/null/rfc.txt#L1-2
cr analyze program-diff <GIT REF>
```

语义：

- `<GIT REF>` 可以是 `HEAD~1`、tag、branch、commit SHA 等；
- 当前侧使用 `cr` 输入路径（默认优先 `calcit.cirru`，兼容旧文件名 `compact.cirru`）；
- 历史侧使用 `git show <ref>:<repo-relative-input-path>` 读取文件内容；
- 两边都必须先经过：
  1. `cirru_edn::parse`
  2. `snapshot::load_snapshot_data`
- 任一侧失败都直接返回错误并停止 diff。

## 4. 输出设计

### 4.1 总体结构

输出分 2 段：

1. Summary
2. Tree Diff

Summary 示例：

```/dev/null/rfc.txt#L1-6
# Program Diff
- ref: HEAD~1
- file: calcit.cirru
- parsed: ok / ok
- changes: ~12 +3 -1
```

### 4.2 树形 diff 约定

参考 `cr analyze call-graph` 的树形展示风格，继续使用：

- `├──`
- `└──`
- `│`

在此基础上增加状态符号：

- `=` unchanged（灰色）
- `~` modified（黄色）
- `+` added（绿色）
- `-` removed（红色）

### 4.3 折叠策略

- `Unchanged` 子树默认只显示一行概要，并灰色折叠；
- `Modified` / `Added` / `Removed` 子树默认展开；
- root 总是展开；
- 叶子节点直接显示 old/new 摘要。

### 4.4 表达式内部 diff

对于 `code/examples/ns.code` 内部的 `Cirru`：

- 叶子变更显示 old/new；
- list 节点按子节点序列做结构化比对；
- 插入/删除节点要保留索引位置和节点摘要；
- 同位置替换优先显示为 `modified`，而不是简单拆成 delete+add；
- 整棵新增/删除表达式应继续展开其子结构。

## 5. 范围

本次只在 `calcit` 当前仓库内实现，不先抽到 `cirru_parser`：

- 先满足 CLI 工作流；
- diff 数据结构先放在当前项目中，便于快速迭代；
- 后续如果算法稳定，再考虑抽取成 Cirru parser / edn 侧通用模块。

## 6. 实现方案

### 6.1 新模块

新增库模块：

- `src/program_diff.rs`

职责：

1. 读取当前 snapshot 文件；
2. 从 git ref 读取历史 snapshot 文件；
3. 校验两边都能 parse/load；
4. 构建结构化 diff tree；
5. 生成适合终端的彩色树形输出。

### 6.2 CLI 接入

- `src/cli_args.rs`
  - 为 `AnalyzeSubcommand` 增加 `ProgramDiff`；
- `src/bin/cr.rs`
  - 将该命令作为 standalone analyze 命令优先处理；
- `src/bin/cli_handlers/command_echo.rs`
  - 增加 command echo 渲染；
- `src/bin/cli_handlers/mod.rs`
  - 导出 handler。

### 6.3 Git 文件读取

流程：

1. 用 `git rev-parse --show-toplevel` 找 repo root；
2. 将当前 `input` 解析成 repo-relative path；
3. 用 `git show <ref>:<relative-path>` 读取历史内容；
4. 若文件不存在、ref 不存在、或命令失败，则直接返回错误。

### 6.4 Diff 层级

优先按 snapshot 语义层比较：

1. `package`
2. `about`
3. `configs`
4. `entries`
5. `files`
6. `file.ns`
7. `file.defs`
8. `def.doc`
9. `def.schema`
10. `def.examples`
11. `def.code`
12. `Cirru` 子树

这样可以避免直接把整个运行时快照文件当作匿名 EDN 树来比，输出会更稳定、更可读。

### 6.5 Cirru 序列 diff

list 内部子节点对比采用“带替换代价的序列对齐”策略：

- 完全相等 => `match`
- 不相等但对位更合理 => `replace`
- 仅旧侧存在 => `remove`
- 仅新侧存在 => `insert`

这比单纯按索引逐位比较更适合表达式中间插入/删除节点的场景。

## 7. 预期收益

- 更适合 review `calcit.cirru`（兼容旧文件名 `compact.cirru`）的语义变化；
- 更容易理解大型 definition 的局部 AST 改动；
- 对 agent/LLM 更友好，能直接拿到层级化变化；
- 为后续抽象成 Cirru diff 模块提供实现样本。

## 8. 暂不处理

本 RFC 本轮不覆盖：

- 三方 merge；
- patch 应用；
- HTML/TUI 交互界面；
- 抽取到 `cirru_parser` crate；
- 和 `cr tree show` / `cr query def` 的统一 chunk/diff UI。

## 9. 后续可能扩展

后续可考虑继续增加：

- `--format json`
- `--no-color`
- `--expand-all`
- `--only <path-prefix>`
- `--git-path <path>`（脱离当前 input）
- 将 Cirru diff 算法下沉到 parser 层
