给定一次代码变更(本地未提交改动 / 两次提交 / 分支 / PR·MR),分析它对其他程序模块的影响, 并给出:影响路径、影响原因、代码位置、影响类型、风险等级、分析置信度、建议验证的测试。
扫描范围只有“其他程序”。 本程序内部的调用方、模块、测试覆盖和传播链不会进入影响结果。 因此每次分析至少需要选择一个被扫描的其他程序。
支持的程序来源:本地目录、本地 Git 仓库、GitHub、GitLab、任意 Git 远程仓库。 分析对象以 Java + Maven 多模块项目为主,同时解析 MyBatis / JPA / Spring MVC / 配置文件, 以及 Kafka / RabbitMQ / RocketMQ 的消息契约,因此同一仓库内的模块、通过 HTTP、数据库、 配置或消息主题相连的服务都能被串起来。
一次真实运行的界面:
user-service的isActive判定条件被改动,报告指出order-service、payment-service受影响,并给出每条的路径、原因、风险与置信度。
- 1. 它到底做了什么
- 2. 快速开始(Linux / macOS · Windows)
- 3. 命令行 / CI 集成
- 4. 分析引擎是怎么工作的
- 5. 界面
- 6. 数据与存储
- 7. 已知边界
- 8. 项目结构
- 9. AI 审核(可选,默认关闭)
- 10. 验收测试
- 11. 贡献与反馈
- 12. 许可证
代码来源(本地 / Git / GitHub / GitLab)
│
▼ ① 变更解析:Git diff → AST 语义差异(不是文本比较)
│ 方法签名 / 实现 / 分支条件 / 字段类型 / 枚举 / 接口契约 / SQL / DDL / 配置
▼
② 代码知识图谱:模块依赖 · 方法调用 · 类型继承 · 接口实现 · 数据流用法
│ · REST 端点与消费方 · MyBatis/JPA 表与列 · 配置项引用
│ · Kafka/RabbitMQ/RocketMQ 主题、路由、消息 Schema · 测试关联
▼
③ 影响分析引擎:反向可达性传播 + 用法语义过滤 + 接口/数据/配置专项传播
▼
④ 风险与置信度:两个独立维度,可解释的风险因子分解
▼
⑤ 结果展示:跨程序契约地图 · 其他程序影响明细 · 变更清单 · 路径与证据 · 代码差异
跨程序传播时同样区分使用方式。对方程序里的调用方如何使用被调用方法的返回值,
(CONDITION 参与分支 / ASSIGNMENT 赋值后继续传递 / ARGUMENT 作为实参 / RETURN 继续返回 /
LOG_ONLY 仅打日志 / IGNORED 被忽略),并据此降低或提高行为影响的权重:
// 返回值控制分支 → 行为影响权重 1.0
boolean active = userService.isActive(userId);
if (!active) { throw new IllegalStateException("user is not active"); }
// 返回值只进日志 → 行为影响权重 0.2
boolean active = userService.isActive(userId);
log.info("audit active={}", active);同一个 UserService.isActive 的变更,前者被评为 HIGH,后者显著更低——这正是减少误报的关键。
需要 JDK 21+,Linux / macOS / Windows 三平台都支持。
mvn -q -DskipTests package
./start.sh # 后台启动,默认 http://127.0.0.1:9996
./start.sh 8080 # 指定端口
PORT=8080 ./start.sh # 同上,环境变量方式
./stop.sh # 停止
./stop.sh 8080 # 停止 8080 上的实例start.sh 会把进程从当前 shell 完全脱离(PPID=1、独立进程组),关掉终端不会带走服务;
应用日志写到 logs/code-association.log(按天滚动、保留 7 天),进程原始输出在 logs/console.log,
PID 写到 .code-association.pid。日志目录、事件名与排查命令见 日志规范。
脱离方式按平台自动选择:Linux 用 setsid,macOS 没有 setsid 时用 python3 双 fork,
两者都没有则退化为 nohup 并提示。同理,健康检查与查监听进程也不硬依赖 curl / lsof,
见 INSTALL.md 的兼容表。
mvn -q -DskipTests package
start.bat :: 后台启动,默认 http://127.0.0.1:9996
start.bat 8080 :: 指定端口
stop.bat :: 停止start.bat / stop.bat 是薄封装,内部带 -ExecutionPolicy Bypass 调用同目录的
start.ps1 / stop.ps1,所以不需要修改系统的 PowerShell 执行策略。
Windows 上以隐藏窗口后台启动,进程原始输出写到 logs/console.log,
启动期错误单独写到 logs/console.err.log(cmd 不允许两个重定向写同一文件);
应用日志与 Linux / macOS 相同,都是 logs/code-association.log。
java -jar target/code-association.jar --server.port=9996界面里:右上角「新建分析」→ 选择代码源与分析模式 → 开始分析。首次使用先在「代码源」页导入项目。
| 模式 | 含义 | 典型场景 |
|---|---|---|
GIT_DIFF |
比较两个提交 / 分支 / tag(端点对端点) | 合并前评估、分支对比、按「起始..结束」出报告 |
COMMIT_RANGE |
勾选分支上要分析的提交,可用提交日期缩小列表 | 评审一批提交、只审其中几个 |
WORKTREE_DIFF |
工作区(含未提交改动)对比 HEAD | 提交前自查"我这次改动会影响谁" |
PULL_REQUEST |
拉取 PR / MR 的 head,与目标分支的 merge-base 比较 | CI 中拦截高风险变更 |
不填基线时,GIT_DIFF 自动取目标提交的父提交(即"最近一次提交改了什么")。
提交范围(COMMIT_RANGE):这个模式问的是"这次要审哪些提交",所以它先要一条分支和一个时间窗口,
再由你勾出要分析的提交:
- 分支 / 引用(可留空,留空 = 该代码源的默认分支):候选提交取自这条分支的历史。
- 起始日期 / 结束日期(都可留空):按提交时间过滤候选列表,
yyyy-MM-dd、两端都含。 服务端按自己的本地时区把一天当作整天,所以"上周五"不会漏掉周五晚上的提交。 不填日期时,列出该分支最新的 100 个提交。 - 提交清单:每行一个提交(短 SHA + 标题 + 作者 · 时间),勾中的才进这次分析,
至少勾一个;
全选/清空一次处理整屏。换分支或改日期后,被筛出列表的提交 会同时取消勾选——界面上看不见的提交不应该还藏在请求里。
列表只装得下最新的 100 个(服务端上限 500)。还有更早的匹配提交时,列表上方的提示会写明 "只列出最新的 100 个,可用时间范围缩小",按日期收窄即可看到它们。
报告里也会列出这些提交——文件清单只能说明"改了什么",说明不了"这次要评审多少东西": 同样 3 个文件,跨 2 个提交和跨 40 个提交是完全不同的评审量。
要按「起始提交(不含).. 结束提交(含)」两端分析,用 GIT_DIFF:它比较的就是两个端点,
基线留空时取目标提交的父提交。网页表单不再要求填起始提交。
GIT_DIFF 与 PULL_REQUEST 也会带上本次范围内的提交列表,口径一致。
提交列表最多列 200 个(按提交时间倒序)。超出时报告会注明"只列出最新的 N 个", 而不是把截断后的列表当作全部。
勾选就是请求本身。 勾中的提交才进这次分析;同一个时间窗口里没勾的提交不会混进来。
这和"这个分支相对基线累计改了什么"问的不是同一个问题——四十个提交里你关心的可能只有三个,
其余三十七个只会稀释报告,而那在 GIT_DIFF 里仍然是一次完整的端点对比。
- 选中提交时,基线取最旧那个选中提交的父提交,目标树是这些提交依次叠加的结果,
所以 diff 恰好只包含它们碰过的文件。中间的提交不会混进来。选到仓库的首次提交时
它没有父提交,基线按空树处理,不会因此拒绝这次分析——与
GIT_DIFF把首次提交 当作目标时的处理一致。 - 选择是集合,不是点击顺序:勾选同时包含某个提交和它的祖先时,按提交拓扑从旧到新叠加, 否则旧提交的树会覆盖新提交对同一文件的改动。
- 选中的提交必须落在同一条提交线上。跨分支的选择会描述一个从未存在过的树, 因此直接拒绝并指出哪个提交不在线上。
- 报告里的「本次包含的提交」就是选中的那几个,
selectedCommits与rangeNote("只分析了选中的 N 个提交")说明这次问的是哪一种问题。 - 接口
GET /api/sources/{id}/commits仍接受base+target,只返回该区间内的提交; 网页表单只用分支与日期,把区间限定留给脚本与集成。
| 类型 | 位置写法 | 说明 |
|---|---|---|
LOCAL_DIR |
/path/to/project |
直接读取本地目录(工作区即目标版本) |
LOCAL_GIT |
/path/to/repo |
本地 Git 仓库,可做提交对比 / 未提交对比 / PR |
GITHUB |
https://github.com/org/repo.git |
克隆到本地数据目录,PR ref 为 refs/pull/N/head |
GITLAB |
https://gitlab.com/group/repo.git |
MR ref 为 refs/merge-requests/N/head |
界面的「来源类型」只提供上述四种。通用 Git 远程地址(如自建 Git 服务、Bitbucket)没有对应的
下拉项,需要时用命令行 --type GIT 或直接调用 API 创建,能力与远程仓库一致,
可用 --base/--target 指定分支。
私有仓库有两条凭据路径:
- 设置页「GitLab 私有仓库」个人访问令牌(推荐)。 粘贴令牌保存即生效,不必重启;只对来源类型
为
GITLAB的代码源生效,scope 至少read_repository。令牌存放在{data-dir}/gitlab-settings.json(POSIX 权限 600),不写入浏览器、数据库、报告与导出。详见 5.10。 - 代码源上的「凭据环境变量」。 填变量名(如
GITHUB_TOKEN、GITLAB_TOKEN),服务从环境变量 读取 token,不落库、不写入报告;GitLab 的这个变量名有值时,优先于设置页令牌。
两种方式最终都按 oauth2:<token> 使用令牌:GitHub 与 GitLab 的个人访问令牌都适用。
--peer 是必填的:扫描范围只有其他程序,没有对比对象就没有可报告的内容。
# 分析最近一次提交对其他程序的影响,把高风险当作失败(可用于流水线门禁)
java -jar code-association.jar analyze \
--location /path/to/repo --base HEAD~1 --target HEAD \
--peer /path/to/order-service --peer https://github.com/org/billing.git \
--out impact-report.json --fail-on HIGH
# 本地未提交改动
java -jar code-association.jar analyze --location . --mode WORKTREE_DIFF \
--peer /path/to/other-service --out report.json
# GitHub PR(第 42 号)
java -jar code-association.jar analyze --location https://github.com/org/repo.git \
--type GITHUB --pr 42 --peer /path/to/consumer --out report.json --fail-on HIGH
# 只评审一批提交(命令行按两端解析:起始提交不含、结束提交含)
java -jar code-association.jar analyze --location . --range origin/main..HEAD \
--peer /path/to/other-service --out report.json --fail-on HIGH--range 与 --base + --target 等价,只是把两端写成一个参数——从评审工具或 CI 变量里
直接粘 $BASE..$HEAD 是常见用法,手工拆开正是两端写反的地方。
命令行的 COMMIT_RANGE 语义没有变:两端必填,且起始提交必须是结束提交的祖先(并行分支会被
直接拒绝并说明原因),产出的是两个端点之间的变更。网页表单里"勾选提交"是另一条入口,
不改变这里的行为。
退出码:0 通过,1 命中 --fail-on 阈值,2 参数或仓库解析失败(含缺少 --peer)。
analyze 子命令不会启动 Web 服务。
java -jar code-association.jar analyze --location . --base origin/main --target HEAD \
--format markdown --out impact.md--format markdown 输出一段可直接贴到 GitHub PR / GitLab MR 的评论:结论、其他程序影响表、
逐条影响(位置 / 原因 / 影响路径 / 建议验证的测试)以及分析边界。例如:
## 跨模块变更影响分析
**结论:** 存在高风险跨模块影响,建议人工评审后再合并
| 模块 | 风险 | 影响符号 | 最深层级 | 影响类型 |
| order-service | HIGH 75 | 6 | 5 | 行为 / 数据 |
1. HIGH 75 · 行为 · 置信度 HIGH
- 位置:order-service OrderService#createOrder(Long, Money) — .../OrderService.java:18
- 原因:branch condition changed. OrderService#createOrder(Long, Money) calls
UserService#isActive(Long). The return value controls a branch condition ...
- 影响路径:UserService#isActive(Long) -> OrderService#createOrder [CONDITION] -> OrderController#create [IGNORED]
- 建议验证:UserServiceTest#testIsActive()
| 参数 | 说明 |
|---|---|
--location |
本地路径或 Git 地址(必填) |
--peer |
被扫描的其他程序,可重复(必填,至少一个) |
--type |
LOCAL_DIR / LOCAL_GIT / GITHUB / GITLAB / GIT,省略时按地址推断 |
--mode |
GIT_DIFF(默认)/ COMMIT_RANGE / WORKTREE_DIFF / PULL_REQUEST |
--base --target |
基线 / 目标引用,--base 省略时取目标提交的父提交 |
--range |
<起始>..<结束>,等价于 --base + --target 并切到 COMMIT_RANGE |
--pr |
PR / MR 编号(自动切换为 PULL_REQUEST) |
--token-env |
存放访问令牌的环境变量名;不写时 GITLAB 来源使用设置页令牌或 code-association.gitlab.token(默认读 GITLAB_TOKEN) |
--out --format |
输出文件与格式(json / markdown) |
--fail-on |
CRITICAL / HIGH / MEDIUM / LOW,命中阈值时退出码为 1 |
--ai |
同时生成 AI 评审说明(见第 9 节);未配置时只提示,不改变退出码 |
GitLab CI 示例:
impact:
image: eclipse-temurin:21-jre
script:
- java -jar code-association.jar analyze
--location "$CI_PROJECT_DIR" --base "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
--target "$CI_COMMIT_SHA" --peer "$PEER_DIR" --out impact.json --fail-on HIGH
artifacts:
paths: [impact.json]
when: always私有 GitLab 项目:在 CI/CD Variables 里定义 GITLAB_TOKEN(Masked)即可,不必再写 --token-env;
变量名不同时用 --token-env 指定。优先级与设置页的关系见
命令行与 CI。
文本 diff 只能告诉你"这一行变了",引擎要回答"什么变了、属于哪类变化"。
每个变更都带稳定的符号标识:模块::全限定名#方法名(参数类型)。
| 变更类型 | 判定依据 | 主要传播方式 |
|---|---|---|
METHOD_SIGNATURE_CHANGED |
参数或返回类型哈希变化 | 编译影响 + 调用方 |
METHOD_CONDITION_CHANGED |
分支/返回条件指纹变化 | 行为影响(控制依赖) |
METHOD_BODY_CHANGED |
方法体哈希变化 | 行为影响(较弱) |
METHOD_REMOVED / FIELD_REMOVED |
符号在 head 缺失 | 编译影响 |
FIELD_TYPE_CHANGED |
字段类型变化 | 类型 / 序列化影响 |
ENUM_CONSTANT_REMOVED |
枚举常量缺失 | 编译 + 契约 |
API_CONTRACT_CHANGED |
同一端点响应类型变化 | 接口契约 → 消费方 |
SQL_CHANGED / DDL_CHANGED |
SQL 体或 DDL 语句变化(一条多动作 DDL 按动作拆成多条记录) | 数据影响 → Mapper/Entity |
CONFIG_CHANGED |
配置键值变化 | 配置影响 → 读取方 |
TOPIC_REMOVED / TOPIC_ADDED |
消息主题缺失 / 新增 | 消息契约 → 生产者、消费者 |
MESSAGE_ROUTE_CHANGED |
Topic、Exchange、Queue、Routing Key 或 Tag 变化 | 投递路径 → 两端代码 |
MESSAGE_REQUIRED_CHANGED / MESSAGE_FIELD_* |
消息 DTO 必填性、字段、类型变化 | 消息契约 → 生产与消费端 |
MESSAGE_SCHEMA_CHANGED |
JSON / Avro / Protobuf 等消息 Schema 变化 | 消息契约 → 两端代码 |
BUILD_CHANGED |
pom.xml 变化 | 依赖 / 版本影响 |
DEPENDS_ON 模块 → 模块(pom.xml,真实构建模型)
CALLS 方法 → 方法(含接口/抽象方法的分派边)
INSTANTIATES 方法 → 构造器
REFERENCES_TYPE 类型 → 类型(字段/参数/返回/局部变量)
EXTENDS / IMPLEMENTS / OVERRIDES 类型层次
EXPOSES_API / CALLS_API 端点提供方 / 消费方(Spring MVC、Feign、RestTemplate、OpenAPI)
EXECUTES_SQL / READS_TABLE / WRITES_TABLE / READS_COLUMN / WRITES_COLUMN / MAPS_COLUMN
USES_CONFIG 代码 → 配置项(@Value、@ConfigurationProperties、@ConditionalOnProperty)
PUBLISHES_TO / SUBSCRIBES_TO 生产者/消费者 → 消息投递通道
ROUTES_TO Topic / Exchange → 实际投递通道
USES_MESSAGE_SCHEMA 代码 → 消息体契约
FILTERS_BY 消费者 → RocketMQ Tag / RabbitMQ Routing Key
TESTS 测试用例 → 被测符号
每条边都记录 confidence 与 evidence(静态解析 / 方法引用 / 分派 / 配置推导),
无法解析的调用会进入盲区清单,而不是被悄悄忽略。
I₁ = 直接调用方/引用方(反向边,一次跳转)
Iₙ₊₁ = Iₙ ∪ Pred(Iₙ) 直到深度上限或置信度衰减到阈值以下
实现要点:
- 在 base 与 head 合并后的图上反向遍历:被删除的符号只存在于 base 图,其调用方只存在于 head 图。
- 每个节点保留"最短且置信度最高"的那条路径(避免路径爆炸)。
- 传播权重 = 边置信度 × 关系系数 × 用法系数;
TESTS边不参与传播(测试是验证手段,不是被影响方)。 - 首跳决定影响类型(
COMPILE/BEHAVIOR/API_CONTRACT/DATA/CONFIG/TYPE)。 - 消息变更使用
MESSAGE_CONTRACT;从消息入口方法继续沿其下游调用展开,避免只找到消费者本身。
R = 100 × (0.25·S + 0.20·D + 0.20·B + 0.20·C + 0.15·U)
S 变更本身的风险 D 传播深度衰减 B 行为变化可能性(含用法权重)
C 影响对象重要程度 U 分析不确定性(= 1 - 置信度)
等级:CRITICAL ≥80、HIGH ≥60、MEDIUM ≥40、LOW ≥20、INFO <20。
风险高但置信度低是完全正常且必须展示的组合:影响可能很严重,但静态分析没把握, 此时结论是"需要人工确认",而不是"确定受影响"。报告中两个数字分开显示,并在详情面板给出风险因子分解。
左侧是一条可折叠侧栏:展开时图标 + 文字菜单一起显示主功能区、代码源、状态分组、计数与最近分析; 收起后只留图标列,内容区拿回宽度(见 5.7)。 右侧工作区按任务优先级分配宽度,主任务占更大面积,辅助信息放右侧。
| 工作区 | 内容 |
|---|---|
| 总览 | 合并后的其他程序影响卡(按程序汇总 + 风险前 6 条)、审核进度环、风险分布、近期事件时间线与最近分析 |
| 代码源 | 导入表单(含标签)、四步导入进度、工作原理说明、最近导入记录(标签 pill 即过滤) |
| 影响审核 | 发现列表(顶部按对比程序汇总),待审核 / 已确认 / 已忽略,右侧契约命中、证据链与引用行所在方法 / 代码块的源码,确认 / 忽略操作 |
| 影响详情 | 变更 → 传播 → 影响三段式结论、影响路径、证据、风险因子分解、建议验证的测试、代码差异 |
| 复核队列 | 高风险待复核、复核建议、处理进度 |
| 模块地图 | 放大版依赖图与关系类型统计 |
选中代码源与加载报告是两件事:点击左侧代码源即把工作区切换到它,即使该源还没有任何分析。 此时各页显示「这个代码源还没有分析记录」并引导新建分析,而不是保留上一个代码源的结果。
- 模块影响地图:Cytoscape 依赖图,节点大小=风险分,颜色=风险等级,紫色描边=本次变更模块, 实线=调用/引用/接口/SQL/配置,虚线=Maven 模块依赖。点击节点筛选影响明细。
- 模块影响明细:每个受影响模块的最高风险、影响符号数、最深层级、影响类型与主要原因。
- 变更清单:语义变更列表,点击查看变更前后、标签与对应代码差异。
- 影响详情抽屉:路径从"变更起点"开始逐跳展示,每一跳标注调用方的使用方式(
CONDITION/LOG_ONLY等)。
改界面之前先读 界面规范。 它是工作台视觉与交互的唯一权威规范 (令牌、排版、布局、组件、状态、动效、无障碍、文案), 界面实现映射 记录规范到文件与类名的对应关系,
UiContractTest把关键契约固化成断言——规范与实现漂移会让测试替错误背书。
界面支持浅色 / 深色两套配色,并默认跟随操作系统。
- 切换位置:侧栏底部的月亮 / 太阳按钮,或「设置 → 外观主题」。
- 三种偏好:跟随系统(默认)/ 浅色 / 深色,选择保存在浏览器
localStorage的ca.theme。 - 无闪烁:
index.html在样式表之前先解析偏好并写好data-theme,深色会话首帧即为深色。 - 跟随系统:偏好为「跟随系统」时监听
prefers-color-scheme,系统切换会实时生效。
实现上所有颜色都收敛为 style.css 中的设计令牌::root 定义浅色值,
:root[data-theme="dark"] 只覆盖令牌、不改动任何布局规则。依赖图(Cytoscape)
把颜色烘进画布,无法继承 CSS 变量,因此由 app.js 的 themeColor() 读取令牌
的计算值,并在主题切换时重建图实例。
复核状态是本地状态。 「确认 / 忽略」的复核结果保存在浏览器
localStorage, 只影响当前浏览器的界面展示,不会写回服务端分析结果。分析数据本身始终来自服务端。
- 分析边界与盲区:无法解析的调用点(反射、动态代理、生成的代码、动态 SQL 等),
以及数据库字段级匹配里的「字段盲区」(对方用
SELECT *、列名无法静态确定)。 - 代码差异:按文件折叠的 unified diff。
添加「本地目录 / 本地 Git 仓库」类型的代码源时,位置一栏右侧有「浏览…」按钮, 打开文件夹树直接选择,不必手输路径。
名称不需要填写:它只是一个展示标签,由位置自动推导——本地目录取文件夹名,
远程仓库取程序名(URL 的最后一段,不含 owner / group 前缀)。表单里的「名称」是只读预览,
随位置实时更新;推导逻辑在 SourceNames,服务端与命令行共用同一套规则,
因此 REST / CLI 不传 name 也能正常创建。
名称不是唯一标识:两个 group 下的同名程序会显示成同一个名称,用位置区分即可;
旧版留下的 owner/repo 名称会在升级后首次启动时自动收敛为程序名。
浏览器的文件选择框只会返回相对文件名、拿不到绝对路径,所以选择器由本地服务驱动
(GET /api/fs/dirs)。它只列目录、不读文件内容,并给每个目录标注 git(含 .git)
与 maven(含 pom.xml),方便识别该选哪个。
- 支持直接输入路径回车跳转、上一级、主目录 / 根目录快捷入口、显示隐藏目录。
- 默认分支自动读取:填写位置后自动列出该仓库的分支与标签供选择,不需要手输。
本地仓库直接读取;远程仓库用
ls-remote查询(不克隆,10 秒超时),读不到时留空由服务端自动选择。 - 选定后自动回填位置;若该目录是 Git 工作副本,来源类型会自动切到「本地 Git 仓库」。
- 保存前服务端会校验:本地来源必须指向已存在的目录,
LOCAL_GIT还必须含.git, 否则返回 400 并给出具体原因。
列表每行的编辑按钮与删除按钮并排(悬停显示,触屏常驻),点开的是同一个弹窗,
只是标题变成「编辑代码源」、保存按钮变成「保存修改」:类型、位置、默认分支、
凭据环境变量、标签都在这里改,保存走 PUT /api/sources/{id}。
- 标签是整体替换:弹窗里保留的几个标签就是保存后的全部标签,删掉即移除; 想要保留的标签必须留在表单里——部分更新会把没带上的标签当作要删除。
- 保存后立即生效:列表、侧栏标签分组与过滤结果当场刷新,不需要重启或重新导入。
- 位置仍要过校验:本地来源必须指向已存在的目录,
LOCAL_GIT还必须含.git。 - 过期状态会重新判定:导入时目录不存在而留下「目录缺失」的来源,改到正确位置后 状态随之复位;反之目录在保存前消失也会当场标回缺失——状态描述的是「现在能不能用」, 不是「当初导入时能不能用」。
- 分析期间改的东西不会被覆盖:分析在跑时改名、改标签、换默认分支都可以, 分析收尾只写回同步状态三列,不会用提交时的快照覆盖这几分钟的编辑。
「代码源」页的「最近导入」列表里,每行右侧有删除按钮(悬停显示,触屏常驻)。 删除是不可撤销的,所以会先弹出确认框,要求输入代码源名称才能点「删除」。
删除是级联的:
- 该代码源的分析记录一并删除——分析里存的是名称快照,留着会继续展示已被移除的代码库。
- 远程仓库的本地克隆缓存(
.code-association/repos/<id>-<name>-<hash>)一并清理。 - 用户配置的位置本身永远不会被删除,只删服务自己创建的副本。
还有分析在跑时拒绝删除(返回 400),避免后台任务在记录删除后又把结果写回去。
单个仓库的分析只能看到仓库内部。但一个改动常常越过仓库边界:改了接口,调用方在另一个服务里; 改了某个字段的写入或表结构,读写这一列的是另一个程序。
在「新建分析」弹窗底部勾选同时比较其他程序,就会把本次变更与选中的程序对齐:
| 契约 | 对齐方式 |
|---|---|
| HTTP 接口 | 按规范化路径模板匹配,调用方写的是具体路径也能对上(/api/users/42 ↔ /api/users/{id},"/api/users/" + id 拼接写法同样识别) |
| 数据库字段 | 契约单位是 表.字段。先确认两侧声明的是同一数据库对象(JDBC URL / 显式 schema),再按「变更可被观察」与「类型兼容」准入:删列 / 收窄命中读写与映射;表重命名按旧表名的表级契约命中;NOT NULL 且无默认值的加列会命中省略该列的 INSERT 写方(自动填充列除外);加列、建表、删除迁移脚本、类型放宽不扩散;写入字段集合新增列只命中读方,停止写某列只计数而不按删列传播;字段删除只认迁移脚本里显式的 DROP COLUMN;一条 ALTER 的多个动作逐动作报告;SELECT * 与动态 SQL 计入字段盲区。详见 数据库影响判断 |
| 配置项 | 按 key 匹配,a.b.c 的改动也会命中绑定 a.b 前缀的代码 |
| Maven 依赖 | 对方声明了对本程序模块的依赖,即视为编译期契约 |
| 消息主题 | Kafka / RocketMQ Topic、RabbitMQ Queue 路由按规范化标识匹配 |
| 消息 Schema | 同一中间件下的消息类型、字段、必填性和枚举指纹一致 |
命中之后,影响会在对方程序的调用图里继续传播,所以看到的不只是直接调用方,还有它的上游。 每个条目都给出契约、使用点、文件行号和置信度,并复用与本地分析相同的风险与置信度模型。
两个限制值得说明:
- 只比较本次真正改动过的契约。共享接口没被改动就不算影响,否则结果会退化成一份清单。
- 对方程序按当前工作区索引;无法读取时只在该条目上给出原因,不会让整次分析失败。
- 数据库契约还会校验两侧的数据源身份与类型兼容性:同名表在不同库不命中;表重命名按旧表名命中读写与映射;加列、建表、删除迁移脚本、类型放宽不扩散,只计数并在报告中说明;
NOT NULL且无默认值的加列会命中省略该列的 INSERT 写方(自动填充列除外),并单独计数而不是并入「不可观察」。未知项进入「字段盲区」,不会被当成干净结果。 - 字段删除只认显式迁移动作:迁移脚本里的
ALTER TABLE ... DROP COLUMN(或列重命名的旧列名)才命中对方读写与映射;「本程序停止写某列」和「CREATE TABLE快照里不再列某列」都只计数,不按删除传播。 - 实体字段删除 / Java 类型变化不改变数据库列:即使对比程序也映射同一列,也只给低置信度「映射提示」,不报 SQL 读写影响;双方共用同一模型类时才按 Maven 依赖的编译契约另外确认。
一次大变更可能产生数十万条跨程序影响。报告不会把它们全部写进 JSON:按风险保留前 N 条
(code-association.max-report-impacts,默认 5000,名额按对比程序公平分配),其余只在
impactsTotal / impactsTruncated 里计数。这样做有两个原因:整份报告要序列化成一份 JSON 落库,
条目太多会超过 Jackson 的 2 GiB 文本缓冲上限,让整次分析在最后一步失败;即使写得下,
几万条的审核列表也没法逐条读。影响审核页会在发现列表上方标注这次截断了多少条,
页头的「N 项影响」始终是全量计数。需要逐条看全部时,调大
code-association.max-report-impacts 后重新运行分析。
结果展示在总览和影响审核两页:总览用一张合并卡先给每个对比程序一枚汇总 pill (契约数 / 影响处数),再按风险列出前 6 条影响;影响审核页把同一层程序汇总压在发现列表标题下, 逐条影响只出现一次,右侧在未选中发现时给出完整契约命中与对方使用点,选中后切换成逐跳证据链。 两个页面都放,是因为总览回答"要不要通知另一个团队",审核页回答"到底改哪里"。
实现上不是把两个仓库的图合并——它们本来就不共享调用图。做法是:先用契约把两边接上, 再以对方命中的那个节点为起点,在对方自己的图里跑一遍普通的影响传播。 所以跨程序的影响条目在评分、置信度和解释文案上与本地分析完全一致,没有第二套算法。
扫描范围只有其他程序。 本程序内部的调用方、模块、测试与传播链不会进入结果:
引擎不会对自身索引做传播,报告里的模块地图、盲区与计数也全部来自对方程序。
因此每次分析至少选择一个被扫描的其他程序;命令行同样要求 --peer,缺省会直接报错退出,
而不是产出一份空报告让 --fail-on 静默通过。
不必等人点按钮:每天定时检查所有已注册的程序,只对产生了新提交的程序发起分析。
| 配置项 | 默认值 | 说明 |
|---|---|---|
code-association.schedule.enabled |
true |
总开关 |
code-association.schedule.cron |
0 30 2 * * * |
每天 02:30 |
code-association.schedule.run-on-startup |
false |
启动后补跑一次,用于补上停机期间错过的扫描 |
code-association.schedule.max-programs-per-run |
20 |
单次扫描的程序上限 |
code-association.schedule.directory-rescan-minutes |
1440 |
无提交可比的普通目录,按时间间隔重扫 |
判定逻辑:解析程序当前提交,与该程序上一次成功分析所记录的提交比较。 相同则跳过,不同则分析;每次分析都以其余所有程序为对比对象。 此外还有三种情况会跳过而不是反复重试:已有分析在进行中、位置不可访问、没有其他程序可比较。
运维接口:
curl http://127.0.0.1:9996/api/schedule # 配置、下次时间、上次每个程序的判定结果
curl -X POST http://127.0.0.1:9996/api/schedule/run # 立即执行一次(异步,结果回上面查)两个可靠性细节值得说明:
- 孤儿任务恢复:进程被杀死时正在运行的分析会永远停在 RUNNING,后续扫描会一直把它当作 「忙碌」而跳过。启动时会把这些行标记为失败,让调度自愈。
- 提交要落库:分析实际比较的提交在完成时写回数据库。否则调度无法区分「已分析过这个提交」 与「从未分析」,每天都会把所有程序重扫一遍。
图标与文字菜单现在是同一条侧栏,默认收起,只留图标列;展开后显示完整菜单,把宽度让给工作区。 三个入口等价:
| 入口 | 位置 |
|---|---|
| 折叠按钮 | 侧栏顶部品牌行右侧的箭头 |
| 展开按钮 | 同一个位置,收起后出现 |
| 快捷键 | Ctrl + B / ⌘ + B |
收起后只剩 72px 的图标列,六个主功能区仍然一点即达;搜索、代码源、状态与最近分析这类
细节退出布局。因为折叠按钮在常驻面板里,它不会跟着被隐藏,也就不需要"图标栏底部"的第二个入口。
选择存在 localStorage 的 ca.nav,刷新后保持,且首帧就按记下的状态渲染。
三个容易忽略的细节:
- 收起时文字标签是
display:none,图标仍在文档流里:Tab 会停在可见的图标按钮上, 不会停在 0px 宽的隐藏文字上。两个按钮的aria-expanded同步反转,读屏不会读到过期状态。 - 默认收起,展开是用户选择:第一帧就是图标列,不会先展开再折起;展开后写入
localStorage,刷新保持。窄屏(≤860px)仍可手动展开,收起不是能力上限。 - 快捷键在输入框 / 文本域 / 下拉框里自动让位,不会吃掉正常输入。
一个代码源可以打多个标签,用来分组和检索。标签是纯展示元数据:分析引擎不读它, 它也不会影响契约匹配、风险评分或报告内容。
| 位置 | 交互 |
|---|---|
| 添加表单 / 新建弹窗 | 「标签」输入框:按 Enter 或逗号确认,退格在空输入时删最后一个,粘贴 a,b,c 一次拆成多个 |
| 候选池 | 输入框下方列出已在用的标签(来自 GET /api/tags),点一下即添加 |
| 代码源列表 | 每行显示标签 pill,点 pill 即按该标签过滤;再次点击或点「清除」恢复 |
| 左侧侧栏 | 代码源按标签分组,可逐组折叠(状态记在 localStorage 的 ca.navtags) |
一个源属于多个标签时会出现在每个标签组里——这正是多标签与单分类的区别; 没有标签的源归入「未分类」,永远排在最后。
归一化规则在服务端(SourceTags),Web、CLI 与导入文件因此得到同一个答案:
- 按英文逗号与中文逗号拆分,trim,丢弃空串;
- 大小写不敏感去重,保留首次出现的写法(
Payment与payment视为同一个); - 单个标签最多 24 个字符,一个代码源最多 12 个标签;
- 永不抛异常——标签是检索辅助,格式不对只收敛、不报错。
tags 字段接受 JSON 数组或逗号分隔字符串两种写法(见 SourceTagsDeserializer),
因为表单和手写 curl 习惯不同,为此回 400 没有意义。
设置页有「代码源导入导出」卡片,用于换机器迁移、团队共享清单与备份。
导出(GET /api/sources/export)下载一个 JSON,文件名带时间戳:
{
"kind": "code-association.sources",
"version": 1,
"exportedAt": "2026-10-26T08:41:58.488818Z",
"sources": [
{ "name": "alpha", "type": "LOCAL_DIR", "location": "/work/alpha",
"defaultRef": "main", "credentialEnv": null, "tags": ["payment", "core"] }
]
}不导出本机运行时字段:id、status、message、createdAt、lastSyncedAt。
这些只对产生它们的那台机器有意义,带过去只会让导入方覆盖掉自己的健康状态。
凭据本身从不入库,导出的只是环境变量名。
导入(POST /api/sources/import)按 type + location 判定是否为同一个代码源:
| 情况 | 行为 |
|---|---|
| 已存在 | 更新:标签取并集,defaultRef / credentialEnv 仅在传入值非空时覆盖 |
| 不存在 | 新建,name 为空时按位置推导 |
| 单条非法 | 记入 errors 并继续处理其余条目,不中断整份文件 |
| 本地目录不存在 | 仍然导入,但置 status = MISSING,列表显示「目录缺失」 |
位置比较会忽略尾部斜杠与首尾空白(a/b/ 与 a/b 是同一个源),
否则手工改过路径的文件每导一次就会复制出一份重复的代码源。
导入结果返回计数与逐条错误:
{ "total": 3, "created": 1, "updated": 1, "skipped": 1,
"errors": [{ "index": 2, "name": "broken", "reason": "不支持的 type:NOT_A_TYPE" }] }「导入你刚导出的文件」是幂等的:应全部计为 updated,标签与数据都不变。
设置页在「AI 审核」与「代码源导入导出」之间有一张「GitLab 私有仓库」卡片,用来在本机保存 GitLab 个人访问令牌(PAT)。这是日常使用的方式:粘贴令牌、保存,立即生效,不必重启, 也不必先导出环境变量再启动服务。
| 项 | 行为 |
|---|---|
| 生效范围 | 只对来源类型 GITLAB 的代码源生效;GITHUB / GIT / LOCAL_* 不会附加这个令牌 |
| 优先级 | 代码源「凭据环境变量」指向的变量有值 → 用它;否则用本卡片的令牌 |
| scope | 至少 read_repository;能读仓库即可克隆、fetch、列分支 |
| 存放 | {data-dir}/gitlab-settings.json,原子写、POSIX 权限 600;卡片底部显示绝对路径 |
| 回显 | 输入框永远不预填明文,占位符显示「已保存 glpa…cdef · 留空保持不变」;GET /api/gitlab 只回掩码 |
| 保存 / 清除 | 留空保存表示不修改;只有「清除」才删掉令牌(清除本身就是一次保存) |
| 不写入 | 浏览器存储、SQLite、报告;只有勾选「包含密钥」的配置导出才带上它(见 5.11) |
配置来源与覆盖关系。 application.yml 里的 code-association.gitlab.token 通常写成
${GITLAB_TOKEN:},即从环境变量读取;这是部署形态的基线。一旦在设置页保存过,
gitlab-settings.json 里的快照就会覆盖它(包括清除后的空值)。想退回文件配置,
删掉 gitlab-settings.json 重启即可;GET /api/gitlab 的 configuredBy 会说明当前值来自
file 还是 application.yml。设置页保存的令牌对同一台机器、同一数据目录上的 CLI 同样生效。
自建 GitLab。 地址可以是 https://gitlab.example.com/group/repo.git,也可以是只提供 HTTP 的
http://gitlab.example.com/group/repo.git——两种协议都会把令牌拼进 URL。HTTP 会让令牌以明文
经过网络,能用 HTTPS 就用 HTTPS。SSH 地址(git@...)不支持:这个 JGit 构建没有 SSH 传输,
填 HTTPS 克隆地址即可。
令牌类型。 面向个人访问令牌。部署令牌(deploy token)需要配对的用户名,本功能只按
oauth2:<token> 使用令牌,因此不适用。
怎么确认生效。 添加或编辑代码源时,来源类型选 GITLAB、填仓库地址,分支下拉能自动列出
远端分支,就说明令牌被接受;列不出或提示认证失败时见
故障排查。
设置页有「配置导入导出」卡片,把设置项(AI 审核 + GitLab 私有仓库令牌)与全部代码源 装进一个 JSON 文件,用于换机器、迁移与整机备份。
与 5.9 代码源导入导出 的分工很清楚:5.9 只搬代码源注册表,文件是给同事看的 「一份清单」;5.11 搬的是「这台机器是怎么配的」,包括那些根本不在代码源里的设置——只带走代码源, 新机器上还得把 AI 接口与令牌重新配一遍。
密钥默认不在文件里。 「包含密钥」勾选框默认不勾,此时导出文件里没有 ai.apiKey 与
gitlab.token,secretsIncluded 为 false。备份文件会经过聊天工具、工单附件与 U 盘,
默认把密钥写进去,等于让一次「备份」变成一次泄露;确实要连密钥一起搬(比如整机替换)时再勾上。
导入不会删掉本机的密钥。 文件里没有 apiKey / token 时,本机已有的密钥保持原样——
一份不含密钥的备份被导回来,不该顺手把令牌清空。
导出(GET /api/settings/export)下载一个 JSON,文件名带时间戳
code-association-config-<yyyyMMdd-HHmmss>.json;加 ?secrets=1 才包含密钥。
响应带 Cache-Control: no-store:这份响应可能装着密钥,不该留在浏览器或代理的缓存里。
{
"kind": "code-association.config",
"version": 1,
"exportedAt": "2026-10-30T03:20:00Z",
"secretsIncluded": false,
"settings": {
"ai": { "enabled": true, "baseUrl": "https://api.deepseek.com/v1", "model": "deepseek-chat",
"maxImpacts": 12, "timeoutSeconds": 60, "defaultForRuns": true,
"comments": true, "maxComments": 12, "reflection": true },
"gitlab": {}
},
"sources": [
{ "name": "alpha", "type": "LOCAL_DIR", "location": "/work/alpha",
"defaultRef": "main", "credentialEnv": null, "tags": ["core"] }
]
}gitlab 是空对象有两种情况:没勾「包含密钥」,或这台机器本来就没保存过令牌。勾选「包含密钥」
(或带 ?secrets=1)时,ai 里多出 apiKey、gitlab 里多出 token,secretsIncluded 为 true。
代码源部分与 5.9 的导出完全同构:不带 id / status / message / createdAt / lastSyncedAt
这些只对产生它们的那台机器有意义的运行时字段。
导入(POST /api/settings/import)请求体就是解析后的这份 JSON:
{
"kind": "code-association.config",
"version": 1,
"settings": {
"applied": ["ai"],
"errors": [],
"secretsImported": false
},
"sources": { "total": 3, "created": 1, "updated": 2, "skipped": 0,
"errors": [{ "index": 2, "name": "broken", "reason": "不支持的 type:NOT_A_TYPE" }] }
}空的分组不应用,所以 applied 通常只有 ai。 不勾「包含密钥」导出的文件里 settings.gitlab
是 {},导入时它算「没有可应用内容」:不写本机的 gitlab-settings.json,也不进 applied。
只有文件里带了 token 才出现 gitlab。这不是洁癖——一份不含密钥的共享文件如果在导入方机器上
凭空写出一份 GitLab 快照,就会把对方 application.yml 里的令牌配置盖成空:一次「参考一下同事的
配置」变成了静默改配置。
部分成功是常态,也仍然是 HTTP 200。 设置项按分组(ai / gitlab)独立校验、独立应用:
一组校验不过只写进 settings.errors(section + 中文 reason,例如
{"section":"ai","reason":"接口地址需以 http:// 或 https:// 开头"}),其余分组与全部
代码源照常生效——因为一个填错的接口地址,没有理由让整份备份里能用的部分一起作废。
整份文件回 400 只有两种情况:kind 不认识、version 比 1 新;那时什么都没导入,
因为连「这是什么文件」都还没确定。
文件里没写的字段保持本机现值。 导入是「按文件里写明的项覆盖」,不是「以文件为准整块替换」: 旧版本导出的文件少了后来新增的字段,不该把它们重置成默认值。
设置项导入后立即生效,不必重启。 它走的是与设置页保存同一条路径,导入完成就能用新的接口地址、 模型与令牌发起分析。
也接受 5.9 的代码源文件。 kind 为 code-association.sources 时 settings.applied 为空,
只导入代码源——手里只有一份代码源清单时,不必先拼一个完整配置才能用。
幂等。 导入刚导出的文件,有内容的设置分组以同样的值重写一遍(空的 gitlab 直接跳过),
代码源全部计为 updated,标签与配置不变——这也是判断「合并规则有没有写错」最省事的信号。
代码源的判定与合并(type + location 认同一个源、标签取并集、非空值才覆盖、坏条目逐条报告)
与 5.9 完全一致。
SQLite(默认 ./.code-association/code-association.db):
repo_source 代码源注册表(类型、位置、默认分支、凭据环境变量名、标签 JSON 数组)
analysis 每次分析:模式、基线/目标提交、状态、耗时、摘要 JSON、完整报告 JSON
代码索引不落库:每次分析从快照重建(毫秒级),避免索引与代码漂移。
远程仓库克隆到 .code-association/repos/<id>-<name>-<urlHash>(同一地址复用同一缓存,
不会与同名仓库冲突)。
本机设置快照也在数据目录,都是原子写、POSIX 权限 600;删掉文件重启即退回 application.yml:
| 文件 | 内容 |
|---|---|
ai-settings.json |
AI 审核配置完整快照(见 9.2) |
gitlab-settings.json |
GitLab 个人令牌,仅本人可读(见 5.10) |
令牌只存在这份文件里,不进数据库与报告;配置导出默认也不含它,只有勾选「包含密钥」时 才会写进导出文件(见 5.11)。
界面在选中代码源后会调用 GET /api/sources/{id}/refs 拉取真实分支与标签,作为基线/目标的候选值。
REST 接口:
| 方法 | 路径 | 作用 |
|---|---|---|
GET |
/api/health |
运行状态与配置 |
GET POST PUT DELETE |
/api/sources /api/sources/{id} |
代码源管理 |
GET |
/api/sources/{id}/refs |
分支 / 标签列表 |
GET |
/api/sources/{id}/commits |
提交候选列表(ref / base + target / limit / since / until) |
GET |
/api/tags |
全部在用标签及计数(侧栏分组与输入候选) |
GET |
/api/sources/export |
导出全部代码源为 JSON 文件 |
POST |
/api/sources/import |
导入代码源(合并标签,逐条报错) |
POST |
/api/analyses |
提交分析(异步,立即返回 id) |
GET |
/api/analyses /api/analyses/{id} |
历史与状态 |
GET |
/api/analyses/{id}/report |
完整报告(未完成时返回 202) |
GET PUT |
/api/ai |
读取 / 保存 AI 审核配置(Key 只回显掩码) |
GET PUT |
/api/gitlab |
读取 / 保存 GitLab 个人令牌(只回显掩码;clearToken 清除) |
POST |
/api/ai/test |
测试连接(始终 200,用 ok 表达结果) |
POST |
/api/analyses/{id}/ai-review |
为历史报告补生成 / 重新生成 AI 审核 |
GET |
/api/update |
当前版本、最新版本、历史版本列表 |
GET |
/api/update/history |
本机版本记录(见过 / 下载过 / 应用过 / 回滚过) |
GET |
/api/update/progress |
后台下载/应用的实时进度(供进度条轮询,比 /api/update 轻) |
GET |
/api/update/restart |
热重启状态(供重启期间轻量轮询) |
POST |
/api/update/check |
强制向 GitHub 重新检查 |
POST |
/api/update/download |
下载指定版本的 jar(校验后暂存) |
POST |
/api/update/apply |
用暂存的 jar 替换当前 jar(备份旧的,随后热重启) |
POST |
/api/update/rollback |
从备份回滚(同样热重启) |
PUT |
/api/update/settings |
运行时开关:自动下载 / 自动应用 / 自动重启 |
配置项(application.yml):
server:
port: 9996
code-association:
data-dir: ./.code-association
gitlab:
token: ${GITLAB_TOKEN:} # 私有 GitLab 个人令牌;设置页快照优先
max-files: 20000 # 单仓库解析文件上限
max-file-mb: 20 # 单文件读取上限(MB)
max-depth: 8 # 影响传播最大深度max-file-mb 是单个文件的读取上限:源码通常只有几 KB,接近这个数的基本是打包进仓库的
SDK、数据库导出或改了扩展名的 jar。超过上限的文件不读取、不索引,在报告「跨程序分析边界」
里逐个列出文件名与大小,而不是让整次分析失败。设为 0 表示不限制。
为什么必须有这一项:JGit 读超大对象时抛出的是
LargeObjectException(RuntimeException), 不会被catch (IOException)拦住。仓库里只要有一个这样的文件,该程序的每一次分析都会失败, 且错误信息只有Object unknown object exceeds 20,971,520 limit,看不出是哪个文件造成的。
日志。 应用日志在 logs/ 下:code-association.log(INFO 及以上,一次运行的完整时间线)、
code-association-error.log(只收 ERROR)、archive/*.log.gz(滚动归档,保留 7 天);
进程原始输出在 logs/console.log。目录由 code-association.log-dir 决定(默认 ./logs,
环境变量 LOG_DIR 可覆盖)。它刻意与数据目录 .code-association 分开:数据丢了要重新分析,
日志随时可以清空。事件名、字段与排查命令见 日志规范。
服务会定时向 GitHub 查询 release 与 git tag,设置页能直接看到「当前版本 / 最新版本 / 历史版本」。 发现新版本时界面顶部会出现提示横幅,侧栏的设置按钮也会亮起红点——不必主动进设置页。
检查是自动的,替换 jar 仍然需要你点确认。 但「应用更新」之后不再需要你手动执行
stop.sh / start.sh:一个独立的守护脚本会等旧进程退出、拉起新 jar、轮询健康检查,
起不来就自动用备份 jar 回滚并重新拉起(见下方「热部署」)。
code-association:
update:
enabled: true
repo: geek-xin/code-association
token: ${GITHUB_TOKEN:} # 可选,见下方限流说明
check-interval-minutes: 360 # 6 小时
cron: "0 0 * * * *" # 每小时一次定时检查(命中缓存时只读内存)
max-releases: 20 # 历史版本列表最多返回多少条
include-prerelease: false
include-tags: true # 同时读 git tag,不只读 release
auto-download: false # 发现新版本后自动下载 jar(仍然不会自动应用)
auto-restart: true # 应用后由守护脚本热重启(默认开)
auto-apply: false # 全自动:发现即下载 → 应用 → 重启(默认关)
restart-timeout-seconds: 90 # 新进程健康检查超时,超时即回滚
# ---- 网络容错(GitHub 直连不通时) ----
api-base-urls: # 依次尝试;第一个是官方源,其余为镜像
- https://api.github.com
download-mirror: "" # 附件下载镜像前缀,例如 https://ghfast.top
proxy: ${UPDATE_PROXY:} # 仅更新流量走的代理:host:port 或 http://user:pass@host:port
retries: 2 # 每个请求失败后的重试次数
retry-base-delay-ms: 500 # 指数退避基数:500 → 1000 → 2000
respect-rate-limit: true # 额度用尽后等到重置时间,不做无谓重试
check-on-startup: true # 启动后立即检查一次为什么同时读 tag。 一个版本通常既是 tag 又是 release,但维护者常常先 push tag、
稍后才写 release(或者干脆忘了写)。只读 /releases 的结果是这类版本在历史里根本不存在,
用户会以为版本没发布。所以默认还会读 /tags 并与 release 合并:同一版本以 release 为准
(release 有更新说明和附件),只在 tags 里出现的版本标记为「仅标签」,能看见但通常不能就地下载。
/tags 请求失败不会让整次检查失败——release 的结果照常返回,失败原因进 warnings 显示在界面上。
「有新版本」与「有可安装的新版本」是两件事。 最新 tag 可能没有对应 release,也就没有 jar。
状态里因此有两个字段:updateAvailable(GitHub 上存在更新的版本)与
installableUpdateAvailable(那个版本真的能下载安装)。界面横幅与红点只由后者触发,
避免把用户引到一个点了必然失败的下载按钮;每个 releases[] 条目也带 downloadable 标记。
关于 GitHub 限流。 未认证调用 api.github.com 每小时只有 60 次。默认 6 小时检查一次,
一天 4 次,够用;但如果你频繁点「立即检查」,就会撞上 403。此时界面会明确提示是限流,
并给出额度重置时间。设置 GITHUB_TOKEN(一个只读的 classic token 即可,不需要任何 scope)
可把上限提到每小时 5000 次:
export GITHUB_TOKEN=ghp_xxx
./start.sh更新流程。 点「下载此版本」→ 下载到 {data-dir}/updates/,边下边算 SHA-256,
下完再校验文件头确实是 jar(PK\x03\x04),不是就删掉并报错;然后点「应用更新」→
当前 jar 先备份成 <jar>.bak-<版本>,再被新 jar 覆盖,随后交给守护脚本热重启(见下节)。
后台自动下载是有进度的,不再是黑盒。 开了 auto-download / auto-apply 之后,
守护线程会在你不知情的时候拉一个几十 MB 的 jar;此前界面只有一个 busy 布尔值,
下载慢和下载卡死看起来完全一样,失败更是只进日志。现在:
- 顶部横幅会显示「后台自动更新 · 正在下载 0.6.0」+ 字节数 + 百分比,并明确标注这次不是你点的; 设置页版本卡片上方也有一条同样的进度条,会依次走「下载 → 校验 → 替换 jar → 重启」。
- 总大小未知时(代理剥掉
Content-Length)不编造百分比:进度条走不确定态条纹,只显示已接收字节数。 - 成功后停留 5 秒再淡出;失败会留下一条红色横幅,写明原因(
error),不会自动消失—— 可在设置页重试,或查看logs/code-association.log里event=update.download/event=update.apply那几行。 - 进度来自轻量的
GET /api/update/progress(运行中每 2 秒一次,结束后停止;另有 15 秒一次的 发现探测,用来察觉页面打开后才开始的后台更新),不反复拉取整个版本列表。
三种情况下「应用更新」会被拒绝,并说明原因。原因文案由后端给出(applyUnsupportedReason),
不是前端猜的——所以「为什么不行」和「接下来该跑什么」都跟实际启动方式对得上:
| 情况 | 原因 | 下一步 |
|---|---|---|
开发模式:mvn spring-boot:run 或 IDE 里跑 target/classes(不是 java -jar) |
没有可替换的 jar,CodeSource 是目录而不是文件 | 用 mvn -q -DskipTests package && ./start.sh 以打包好的 jar 启动 |
| Windows | 运行中的 jar 被系统锁定,无法覆盖 | 手动 stop.bat → 替换 jar → start.bat |
| 没先下载 | 没有暂存文件可应用 | 先点「下载此版本」 |
设置页会把判定结果直接写在「启动方式」一行里(打包后的 jar / 开发模式 / 未打包运行), 并在「当前环境不支持自动应用」下面给出可复制的命令,而不是只留一句「可能是在 IDE 里启动」。
开发模式下「当前运行版本」不再是 0.0.0-dev。 以前版本号只从 jar 的 manifest 读,
而 mvn spring-boot:run 根本没有 jar,于是只能显示占位符。现在构建会生成
META-INF/build-info.properties,dev 模式也读它,显示的就是 pom.xml 里的真实版本。
但要在界面里用「应用更新」自动替换 jar,仍然必须以打包后的 jar 运行:
mvn -q -DskipTests package && ./start.sh回滚用同一套机制:POST /api/update/rollback 从最近的 .bak-* 恢复,同样会触发热重启。
难点不在「启动 java」,而在干这件事的进程正是必须死掉的那个。所以重启不是在本进程里做的:
apply()把新 jar 覆盖到运行位置,并留下<jar>.bak-<版本>备份;- 本进程把一个 POSIX 守护脚本写到
{data-dir}/restart/,脱离终端启动它; - 脚本
while kill -0 <旧PID>等旧进程真的退出(端口因此自然释放),再拉起新 jar; - 脚本轮询
GET /api/health,restart-timeout-seconds内没起来就用备份 jar 覆盖回去并再拉起一次; - 本进程延迟约 1.5 秒后
System.exit(0)—— 延迟是为了让「更新成功」这个 HTTP 响应先写回浏览器。
每一步的状态都写进 {data-dir}/restart-status.json,GET /api/update 的 restart 块把它和内存状态合并后
返回,所以重启完成后新进程仍能报告「上一次是成功了还是回滚了」,而不是装作什么都没发生。
前端在应用更新后每 3 秒轮询一次,见到 phase=succeeded 自动刷新页面,见到 rolled-back 显示红色告警与日志路径。
| 配置 | 默认 | 作用 |
|---|---|---|
auto-restart |
true |
应用/回滚后自动热重启。关掉则退回「提示你手动执行 stop.sh/start.sh」 |
auto-apply |
false |
全自动:发现新版本即下载→应用→重启。要求 auto-restart 同时为真 |
restart-timeout-seconds |
90 |
新进程健康检查超时,超时即回滚 |
为什么
auto-apply要求auto-restart也为真:没有热重启的自动应用等于「自动把服务停掉等人来开」, 这不叫更新,叫故障。所以这个组合会被PUT /api/update/settings直接拒绝并说明原因。
为什么三个开关是运行时状态而不落盘:它们描述的是「这个实例从现在起怎么行为」,下次启动仍以
application.yml为准。落盘会造出第二个真相来源,和运维部署的文件悄悄不一致。
自动更新最容易失败的地方不是代码,是网络。四种情况各自有对应处理:
| 症状 | 处理 |
|---|---|
api.github.com 被墙 / 超时 |
api-base-urls 按顺序回退到镜像;成功时状态里 network.activeApiBase 会显示实际用的源 |
| API 通但附件 CDN 不通 | download-mirror 给下载 URL 加前缀(ghproxy 一类镜像的通用约定) |
| 必须走代理 | proxy 只作用于更新流量,不影响其他 HTTP;留空则沿用 JVM 的 -Dhttps.proxyHost / HTTPS_PROXY |
| 临时抖动 / 5xx / 截断 | 指数退避重试 retries 次;404、401 这类不会重试(重试也不能变成 200) |
回退只发生在「换台机器也许能行」的失败上。 404(仓库不存在)和 401(token 无效)会立即结束,
不会去问镜像——那只会把真正的问题藏起来。每个 host 的失败原因都会折叠进 warnings 展示,
所以「备用源也试过了」这件事在界面上是可见的。
限流有一把全进程共享的闸门。 额度用尽时 GitHub 会在响应头里给出重置时间,这个时间被记进
RateLimitGate,检查、重试、下载都查同一把闸门。没有它的话,三个各自退避的调用方会合起来
把这一小时一直撞在限流上。注意附件下载走的是 CDN,不消耗 API 额度,所以限流期间下载仍然可用
(状态里 network.downloadBlocked 恒为 false,就是为了避免把这两件事混为一谈)。
本机版本记录。 GET /api/update/history 读的是落在 {data-dir}/update-history.json 的账本,
记录每个版本第一次见到 / 最近见到 / 下载过 / 应用过 / 回滚过的时间。这份记录跨重启保留,
所以「这台机器上的 0.3.0 是哪天下载的」可以直接查,而不必去翻文件时间戳。
账本写坏或没权限时只会退化成空账本并记一条日志——它绝不能连带让版本检查失败。
下载状态是算出来的,不是记下来的。 releases[].downloaded 每次都由
{data-dir}/updates/code-association-<版本>.jar 是否真的存在决定。
手动删掉 jar 后界面会如实回到「未下载」,不会因为账本写着下载过就骗人。
发现新版本时不会只在设置页里提示。 页面顶部会出现提示横幅,侧栏的设置按钮亮红点, 并可直接「查看」或「忽略此版本」(忽略只作用于当前浏览器,记在 localStorage)。 这样自动检查才真的「自动」——否则用户不点进设置页就永远不知道有新版本。
静态分析无法在一般情况下完整证明跨模块行为是否变化,系统如实标注能力边界:
| 场景 | 现状 |
|---|---|
| 多态 / 接口调用 | 记录声明方法 + 已知实现的分派边(置信度较低) |
| Spring 依赖注入 | 通过字段/构造器类型解析,不还原 Bean 装配图 |
| 反射 / 动态代理 / 代码生成 | 无法解析,进入盲区清单 |
| Feign / RestTemplate / OpenAPI | 按路径模板匹配端点,动态拼接的 URL 可能漏报 |
动态 SQL(<if> 拼接) |
解析静态部分,动态部分进入盲区 |
SELECT * / 列名无法解析 |
数据库契约按字段匹配,这类语句不命中字段级契约,会计数并标注「字段盲区」 |
| 数据库身份 | 从 JDBC URL 与 SQL/DDL 的显式 schema 推断;读不到时按未知处理(保留匹配、降低置信度),不连接数据库核实 |
| 必填列 / 自动填充列 | NOT NULL 且无默认值的加列会命中省略该列的 INSERT;AUTO_INCREMENT / GENERATED / IDENTITY 列由数据库补值,不算必填(只识别 ALTER 列定义文本,触发器 / 序列等其他机制不识别) |
| 值语义(如 status 1 → 2) | 写入列集合不变,不产生数据库 delta;当前不检测,属于已知漏报 |
| 配置中心(Nacos/Apollo) | 仅分析仓库内配置与代码引用 |
| 跨仓库依赖 | 需分别登记代码源;单次分析在同一仓库内完成 |
| 框架入口(Controller / Listener / 定时任务) | 由容器调用,仓库内没有静态调用方;报告会单独计数并说明 |
| 消息中间件 | 支持 Spring Kafka、Spring AMQP、RocketMQ Spring/原生客户端;不读取 Broker 中的真实消息 |
| 动态消息目标 | Topic、Queue、Routing Key、Tag 来自运行时参数或配置中心时进入盲区,不猜测具体值 |
| 消息 Schema | 支持 JSON DTO 及 Avro / Protobuf 文件识别;Schema Registry 在线拉取不在首版范围 |
| 消息框架 | Kafka Streams 拓扑 DSL、Spring Cloud Stream 动态 Binding 不在首版范围 |
| 首次提交(无父提交) | 基线为空快照,所有符号都算新增;报告会明确标注 |
| 影响条目极多 | 报告按风险只保留前 N 条(code-association.max-report-impacts,默认 5000,按对比程序公平分配),其余只计数;审核页会标注截断条数,风险与影响计数始终完整 |
src/main/java/com/geek/association/
├── source/ 快照抽象:DirSource / GitTreeSource / RepositoryPreparer(本地·Git·GitHub·GitLab·PR)
├── parse/ MavenParser · JavaIndexer · CallGraphBuilder · TypeResolver
│ SqlAnalyzer(MyBatis/JPA/DDL)· ApiAnalyzer(Spring MVC/Feign/OpenAPI)
│ ConfigAnalyzer · TestLinker · CodeIndexer
├── diff/ FileDiffCollector · ChangeDetector(语义变更分类)· LineDiff
├── impact/ ImpactGraph(base+head 合并)· PropagationRules · ImpactEngine · RiskScorer
├── report/ ImpactReport(输出模型)· ReportBuilder · MarkdownReport · AiExplainer
├── service/ AnalysisService(异步编排)
├── store/ Store(SQLite)
├── web/ ApiController(REST)
├── cli/ CliRunner(CI 无头模式 + 退出码)
└── config/ AppProperties
src/main/resources/static/ 零依赖前端(原生 JS + Cytoscape 本地内置)
src/test/resources/fixture/ 四模块 Java 示例项目(base/ 与 head/ 两个版本)
src/test/java/ 按关注点分组的用例(见第 10 节)
docs/wiki/ 深度文档:核心概念 · 契约匹配 · 评分模型 · 架构 · 排错
.github/ Issue 模板与 PR 模板
start.sh / stop.sh Linux · macOS 启停脚本(bash,逐项探测可用工具)
start.ps1 / stop.ps1 Windows 启停脚本(PowerShell 实现)
start.bat / stop.bat Windows 薄封装(自带 -ExecutionPolicy Bypass)
release.sh 打包并发布 tar.gz / zip 到 GitHub Release(含两个平台脚本)
同一个 jar、同一份数据目录,三个平台各有对应的启停脚本,参数契约完全一致:
| 平台 | 启动 | 停止 | 端口参数 |
|---|---|---|---|
| Linux / macOS | ./start.sh |
./stop.sh |
./start.sh 8080 · PORT=8080 |
| Windows | start.bat |
stop.bat |
start.bat 8080 · -Port 8080 · set PORT=8080 |
start.sh / stop.sh 不硬依赖任何单一工具,缺什么就换实现:
| 能力 | 首选 | 备选 | 都没有 |
|---|---|---|---|
| 脱离终端 | setsid(Linux) |
python3 双 fork(macOS) |
nohup + 提示 |
| 健康检查 | curl |
bash /dev/tcp |
纯 sleep 轮询 |
| 查监听 PID | lsof |
ss / netstat -tlnp |
记录的 $! / 命令行匹配 |
Windows 侧以隐藏窗口后台启动,.ps1 保存为 UTF-8 with BOM、
.bat 为纯 ASCII、统一 CRLF —— PowerShell 5.1 会按 ANSI 解码无 BOM 文件,
不这样处理中文注释会乱码甚至语法报错。发布脚本对这些编码做了字节级校验。
默认关闭。关闭时报告里没有 ai 字段、分析流程与改动前一致、也不会向任何外部服务发出请求;
界面只在设置页多一张卡片(/api/health 另有一个只读的 aiReady 字段,供集成判断,值为 false)。
设计原则是"先由静态分析产出事实,再让模型解释事实",而不是把仓库丢给模型猜:
- 事实先算完。 哪些符号变了、影响路径逐跳怎么走、每一跳对方怎么消费这个值、风险与置信度、 哪些调用点解析不了——这些都在调用模型之前已经由引擎得出。
- 模型看不到源码全文。 送给模型的是这些结论的摘要,以及在启用「逐条评审意见」时的变更片段, 不是文件内容(见 9.6)。
- 模型不决定"影响了谁"。 影响清单由静态分析产出;模型只被要求用评审语言复述事实、 排出人工确认的优先顺序,并被明确禁止编造事实里不存在的类、方法或调用关系, 不确定时必须写"需人工确认"。
- 解释与证据分区存放。 输出落在
report.ai,界面单独一张卡片,正文之上有一条「待验证」说明 (随内容一起存进report.ai.disclaimer,所以标签跟着内容走,不只跟着界面走)。 读者任何时候都能分清哪部分是静态分析的证据、哪部分是模型的解释。 - 逐条意见的行号由本机算。 可选的第二段调用产出
report.ai.comments:每条带严重级、标题、正文、 建议、代码引用与文件:行。模型只逐字引用代码,行号由三层定位(hunk 匹配 → 整文件扫描 → 模型重定位) 在本机算出;定位不到的评论直接丢弃,不猜位置(见 9.7)。
「设置 → AI 审核」卡片是日常使用的方式:改完点「保存」立即生效,不必重启。
| 卡片上的项 | 默认 | 说明 |
|---|---|---|
| 启用 AI 审核 | 关 | 总开关 |
| 接口地址 | 空 | OpenAI 兼容端点,需以 http:// 或 https:// 开头 |
| 模型 | 空 | 端点上的模型名,如 deepseek-chat |
| API Key | 空 | 本地模型通常不需要,可以留空 |
| 每条分析最多送审影响 | 12 | 1..50;送给模型的影响条目上限,避免提示词随报告规模失控 |
| 请求超时(秒) | 60 | 5..300;推理型模型通常需要更长时间 |
| 生成逐条评审意见 | 开 | 独立的第二段调用;发送变更片段而不是源码全文 |
| 每条分析最多评论 | 12 | 1..50;超出上限按严重级截断 |
| 用反思模型过滤误报 | 开 | 额外一次模型调用;只删除与代码事实矛盾的评论 |
| 默认在每次分析时生成 AI 审核 | 开 | 决定分析弹窗里那个勾选默认是否勾上 |
- 可测试连接。 「测试连接」发一个最小请求(只要求模型回复
pong),返回实际调用的端点与耗时。 配置错误的代价从"等一次分析失败"降到一次点击;测试用的是表单里当前填的草稿,不必先保存。 - Key 只回显掩码。 保存后再打开设置页,输入框是空的、占位符写着「已保存 sk-a…3f9c · 留空保持不变」; 留空保存表示不修改,只有「清除」才真正清空(清除本身就是一次保存)。浏览器里从头到尾没有明文 Key, 也就不存在把掩码当明文回传、把 Key 悄悄写坏的机会。
- 保存到数据目录。 配置写在
{data-dir}/ai-settings.json(默认./.code-association/), 原子写入,POSIX 平台上尽量设为仅本人可读。文件损坏或没有权限时不报错,退回application.yml的配置并记一条日志——一份坏掉的设置文件不应该让服务起不来。卡片底部显示这个文件的绝对路径。
「是否就绪」只有一个口径:已启用 + 接口地址非空 + 模型非空。API Key 不算在内,
因为本地 Ollama 一类的端点本来就没有 key。未就绪时状态行给出缺了哪一项的中文原因。
GET /api/health 的 aiReady 与 aiEnabled 报告同一件事(是否就绪),前者语义更明确,
后者为兼容既有集成保留。
部署时统一配置用文件与环境变量,不必进界面:
code-association:
ai:
enabled: true
base-url: https://api.deepseek.com/v1 # 任意 OpenAI 兼容端点
api-key: ${AI_API_KEY:} # 从环境变量读,不要写进仓库;缺省为空
model: deepseek-chat
max-impacts: 12 # 1..50
timeout-seconds: 60 # 5..300,单次请求超时
comments: true # 逐条评审意见(发送变更片段)
max-comments: 12 # 1..50
reflection: true # 反思模型过滤事实错误
default-for-runs: trueexport AI_API_KEY=sk-...两者的关系是"基线 + 快照覆盖"。 application.yml 是基线;一旦在设置页保存过,
ai-settings.json 里那份完整快照就会覆盖它(包括把某一项清空)。想退回文件配置,
删掉 ai-settings.json 重启即可。当前生效值来自哪里,GET /api/ai 的 configuredBy
会如实回答(file 或 application.yml)。
这样取舍是因为两种配置服务不同场景:yml 属于部署形态,一份配置对所有实例生效;设置页属于 这台机器上的即时调整。让快照覆盖基线,"界面上改了却没生效"才不会发生;而删文件即回退, 则保证部署配置始终可复原。
AI 审核是可选项,三个层次都可以控制:
| 层次 | 怎么控制 | 缺省行为 |
|---|---|---|
| 单次分析 | 分析弹窗里的「AI 审核」勾选 | 勾选状态取「默认在每次分析时生成」 |
| 默认值 | 设置页的「默认在每次分析时生成」 | 已配置时默认勾选 |
| 历史报告 | 分析页 AI 卡片上的「生成 AI 评审」/「重新生成」/「重试」 | 不自动补生成 |
- 请求体字段
aiReview缺失时按「默认值 && 就绪」决定;显式false表示这次不生成。 - 未就绪时勾了也不会失败:跳过并记一条日志,分析本身必须成功。
- 历史报告随时可以补生成,调用
POST /api/analyses/{id}/ai-review,结果写回该报告并同步内存缓存 (不同步的话,刷新页面看到的还是旧内容)。 - AI 未就绪且这份报告里没有 AI 内容时,分析页的 AI 卡片整张隐藏——没启用的功能不该在每份报告上 留一个空位。报告里已有历史 AI 内容时照常显示,只是不再提供生成入口。
- 弹窗打开时读不到
/api/ai,这一行整行隐藏——配置接口不该阻塞发起分析。
任意 OpenAI 兼容的 /chat/completions 端点。接口地址会去掉尾部 /,缺 /chat/completions
时自动补上,所以填到 /v1 就够了。
| 服务 | 接口地址 | 模型 | 备注 |
|---|---|---|---|
| DeepSeek | https://api.deepseek.com/v1 |
deepseek-chat |
需要 API Key |
| OpenAI | https://api.openai.com/v1 |
gpt-4o-mini |
需要 API Key |
| Ollama 本地 | http://localhost:11434/v1 |
qwen2.5:7b |
Key 可留空;模型名以 ollama list 为准 |
设置页的三个快捷填充按钮只填接口地址与模型,不动 Key——Key 属于账号凭据,不该被模板覆盖。
发送(都已经是分析结论,不是源码):
| 内容 | 具体字段 | 上限 |
|---|---|---|
| 变更清单 | 变更类型、模块、符号全名与签名、一句话摘要 | 15 条 |
| 影响条目 | 风险等级与分数、目标模块与符号、影响类型、置信度、深度、原因、逐跳路径与每跳用法 | max-impacts(默认 12) |
| 盲区 | 文件:行、无法解析的表达式、原因 | 8 条 |
| 变更片段(仅「逐条评审意见」开启) | diff 里的变更行与上下文、文件路径与行号 | 20 文件 / 单文件约 4000 tokens / 单 chunk 约 8000 tokens |
摘要与逐条意见是两段调用、两次授权:关掉「生成逐条评审意见」就回到"只发摘要"。
不发送:源码全文(未变更部分)与仓库其它文件、仓库地址与远端 URL、Git 凭据。 "整文件扫描"这一层由本机定位代码读取新版本文件,只为把引用的代码换算成行号,文件内容不进提示词; 补生成历史报告时这一层直接跳过。
API Key 只出现在 Authorization 请求头里,不会出现在接口响应、日志或错误信息中——设置页读回的永远是掩码。
需要留意的是,发送内容里仍然有内部类名、方法名与模块名。它们不是源码,但足以暴露系统结构。 对代码结构本身敏感的场景,应在确认合规后再启用;这也是它默认关闭的原因。
除了整段说明,还能给出逐条意见:每条带严重级、类别、标题、正文、建议、代码引用和 文件:行。
行号不由模型写。 模型看到 diff 文本,自己数行会错(+ 前缀、上下文行、被截断的 hunk 都会让它偏移),
而"看起来对但位置错了"的评论比没有评论更糟。所以协议里没有行号字段:模型逐字引用 existing_code,
行号由本机定位代码去 diff 与文件里找;找不到就丢弃(计入统计),不猜一个接近的位置。
| 层 | 在哪找 | 何时使用 | anchor |
|---|---|---|---|
| ① hunk 文本匹配 | 本次 diff 的新侧;删除行回退旧侧 | 引用的是变更行或 hunk 上下文 | HUNK |
| ② 整文件扫描 | 本机读取的新版本文件 | 引用的是文件里未变更的部分 | FILE |
| ③ 模型重定位 | 再问一次模型"这段代码在哪里" | 前两层都没命中,最多 6 次调用 | RELOCATED |
| 失败 | — | 三层都失败,丢弃并计数 | 不进报告 |
匹配按"宁可丢、不可错":空白归一化、忽略空行、多行要求连续命中、新侧优先、歧义不猜。
误报过滤分两层。 第一层是确定性过滤(不花钱、可复现):文件必须属于本次送审、证据编号必须存在、 行号范围自洽、严重级与类别在白名单内、标题与正文非空、同位置相似标题合并保留更严重者、按上限截断。 第二层是可关的反思调用:只问"哪条评论与代码事实矛盾",可删理由只有代码不存在、片段反驳论断、 引用的符号不存在;"价值低""无法验证"不是删除理由——把过滤器变成第二个评审者,只会把误报换成漏报。 反思失败或解析失败时保留全部评论。
每一步都有 token 上限:最多 20 个文件、单文件约 4000 tokens、单 chunk 约 8000 tokens、
每 chunk 最多 10 条评论、重定位最多 6 次、总数受 max-comments(默认 12,上限 50)截断。
跳过的文件与丢弃的评论都写进 report.ai.reviewStats(filesReviewed / filesSkipped /
commentsKept / commentsDroppedUnlocated / commentsDroppedFiltered / commentsDroppedReflection /
commentsMerged / relocationCalls / reflectionRan / estimatedTokens / notes),
所以"评论很少"可以解释而不是只能猜。逐条评审整体失败时另有可选的 reviewError,摘要照常保留。
界面在摘要下按严重级渲染评论(彩色左边界、严重级标签、文件:行、正文、建议、证据编号、定位方式、
置信度、等宽代码引用);评论为空时显示「本次变更未发现需要评论的问题」,老报告没有 comments 字段时
不新增任何区块。Markdown 报告多一节「AI 逐条评审意见(待验证)」,没有评论时输出与旧版本一致。
AI 是解释层,它的失败不能影响证据层:
| 情况 | 结果 |
|---|---|
| 模型返回 401 / 超时 / 响应无法解析 | 该次 AI 卡片显示原因,报告其它部分与分析结论不变 |
| 未就绪却要求生成 | 跳过并记日志,分析照常完成 |
| 历史报告补生成失败 | 接口返回 generated: false 与原因(不是 5xx),页面显示错误并可重试 |
--ai 但未配置 |
命令行打印一行提示,退出码不变 |
最后一条是硬约束:AI 不可用绝不能让流水线变红,--fail-on 的判定只由静态分析结论决定。
java -jar code-association.jar analyze --location . --peer /path/to/other-service \
--ai --out report.json --fail-on HIGH--ai 生成的内容写进 JSON 的 ai 字段;Markdown 报告则多一个独立小节:
### AI 评审说明(待验证)
> AI 生成内容,仅基于下列静态分析事实,不是已验证的结论。
<正文>
<sub>模型 deepseek-chat · 生成于 …</sub>有逐条意见时再加一节「AI 逐条评审意见(待验证)」,逐条给出位置、正文、建议与代码,末尾一行统计; 没有评论时这一节不出现。
更细的说明(配置项语义、接口结构、隐私边界与排错)见 AI 审核。
mvn test全部用例必须通过,没有例外。
用例按关注点分组,每条规则都有对应断言:
| 测试 | 覆盖内容 |
|---|---|
ImpactAnalysisTest |
端到端:四模块工程的条件变更、SQL 变化、配置翻转、接口契约变化 |
PeerImpactTest |
跨程序契约匹配:HTTP / 数据库表 / 配置项,以及无关程序不误报 |
ScheduledScanTest |
定时扫描的判定:首次扫描、无新提交跳过、位置丢失跳过、孤儿任务恢复 |
MessagingAnalysisTest |
Kafka / RabbitMQ / RocketMQ 主题、路由与消息 Schema 契约 |
RulesTest |
风险与置信度分离、评分归一化、用法权重 |
ApiPathMatchTest |
路径模板匹配:具体路径、拼接写法、不相关路径不误判 |
PathBrowserTest / RefDiscoveryTest / SourceNamesTest / AnalysisProgressTest |
目录选择、分支读取、自动命名、进度持久化 |
GitLabSettingsTest / GitSupportTest |
GitLab 令牌持久化与 600 权限、掩码与 clearToken、http(s) 地址凭据拼接、环境变量优先 |
ImpactAnalysisTest 用一个真实结构的多模块工程验证端到端行为:
- head 版把
UserService.isActive从"存在且 status==1"改为"仅存在"; - 同时改窄
selectById的查询列、翻转一个配置开关、把GET /api/users/{id}的响应类型改为UserSummary。
断言:条件变更被识别为 METHOD_CONDITION_CHANGED;order-service 的 createOrder(分支消费)
被判为 HIGH 且高于 auditOrder(仅日志消费);payment-service.validateUser 被间接命中;
控制器作为 2 层以上间接影响出现;接口契约变更产生 API_CONTRACT 影响;报告含模块依赖边。
- 问题与建议:Issues
- 变更记录:Releases
- 深度文档:geek-xin.github.io/code-association(源码在
docs/)
提交代码前请确认:
mvn test # 全部用例必须通过
mvn -q -DskipTests package新增分析能力时,请同时补一个用例:引擎的每一条传播规则都应当有可复现的断言, 否则"这次改动为什么不影响对方"无法被验证。
改动界面时,请先读 界面规范,改完同步 界面实现映射。
UiContractTest 会校验规范里的关键令牌、类名双向契约、断点、焦点与 reduced-motion、
首帧防闪脚本是否仍在代码中——它不检查"好不好看",只保证规范与实现没有走散。
改动日志时,请先读 日志规范:事件名、字段与级别都在那里定义, 新增重要节点要同时更新事件目录与故障排查里的 grep 配方。
改动启停脚本时请注意:start.sh / stop.sh 必须同时能在 Linux 与 macOS 跑,
Windows 的 start.ps1 / stop.ps1 保持 UTF-8 BOM + CRLF、start.bat / stop.bat 保持纯 ASCII + CRLF。
release.sh 会在发布前校验这些编码约束,改坏了会直接失败而不是发出坏包。
MIT © geek-xin




