Skip to content

Commit 5fb2941

Browse files
authored
[200_69] 修复 gfproject.json 合并逻辑,支持单命令回退 (#697)
* [200_69] 修复 gfproject.json 合并逻辑,支持单命令回退 * wip
1 parent cdb48fe commit 5fb2941

4 files changed

Lines changed: 551 additions & 84 deletions

File tree

devel/200_69.md

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,100 @@
11
# [200_63] 命令行工具注册系统规范
22

3+
## 如何测试
4+
5+
测试项一:字段级覆盖 description,不影响命令实现
6+
1. 在任意临时目录创建 `gfproject.json`
7+
2. 写入如下内容,只覆盖 `test.description`
8+
9+
```json
10+
{
11+
"tools": {
12+
"test": {
13+
"description": {
14+
"en_US": "Run project-specific tests",
15+
"zh_CN": "运行当前项目的测试"
16+
}
17+
}
18+
}
19+
}
20+
```
21+
22+
3. 在该目录执行 `gf help`
23+
4. 确认 `test` 的描述变成 `Run project-specific tests`
24+
5. 确认 `help``version``doc``source` 等其他命令仍然正常显示
25+
6. 执行 `gf test`
26+
7. 确认 `gf test` 仍然走原有实现,不会报缺少 `organization/module`
27+
28+
测试项二:只覆盖嵌套字段,未覆盖的 description 子字段仍然保留
29+
1. 在任意临时目录创建 `gfproject.json`
30+
2. 写入如下内容,只覆盖 `test.description.zh_CN`
31+
32+
```json
33+
{
34+
"tools": {
35+
"test": {
36+
"description": {
37+
"zh_CN": "运行测试(本地覆盖)"
38+
}
39+
}
40+
}
41+
}
42+
```
43+
44+
3. 在该目录执行 `gf eval '(display (g_gfproject-load-config))'`
45+
4. 确认输出中的 `tools.test.organization` 仍然是 `liii`
46+
5. 确认输出中的 `tools.test.module` 仍然是 `goldtest`
47+
6. 确认输出中的 `tools.test.description.en_US` 仍然保留内置值
48+
7. 确认输出中的 `tools.test.description.zh_CN` 变成 `运行测试(本地覆盖)`
49+
50+
测试项三:本地把某个命令配置坏时,只回退该命令
51+
1. 在任意临时目录创建 `gfproject.json`
52+
2. 写入如下内容,故意把 `version.module` 改成不存在的模块
53+
54+
```json
55+
{
56+
"tools": {
57+
"version": {
58+
"module": "goldversion_missing"
59+
}
60+
}
61+
}
62+
```
63+
64+
3. 在该目录执行 `gf version`
65+
4. 确认仍然输出正常版本信息,如 `Goldfish Scheme 17.11.48 by LiiiLabs`
66+
5. 执行 `gf help`
67+
6. 确认 `help` 仍然正常输出命令列表
68+
7. 确认不会出现动态模块导入失败导致整个命令系统不可用
69+
70+
测试项四:help 命令通过 glue 复用 C++ 侧的合并结果
71+
1. 在任意临时目录创建 `gfproject.json`
72+
2. 写入如下内容
73+
74+
```json
75+
{
76+
"tools": {
77+
"test": {
78+
"description": {
79+
"en_US": "Run project-specific tests"
80+
}
81+
}
82+
}
83+
}
84+
```
85+
86+
3. 在该目录执行 `gf eval '(display (g_gfproject-load-config))'`
87+
4. 记录输出中 `tools.test.description.en_US` 的值
88+
5. 在同一目录执行 `gf help`
89+
6. 确认 `help` 里显示的 `test` 描述与上一步 glue 输出一致
90+
91+
测试项五:源码树运行时也能找到内置 gfproject.json
92+
1. 进入 Goldfish 源码根目录之外的任意临时目录
93+
2. 不创建本地 `gfproject.json`
94+
3. 执行 `/path/to/goldfish/bin/gf eval '(display (g_gfproject-load-config))'`
95+
4. 确认输出中存在内置工具配置,如 `help``test``version`
96+
5. 确认不是空的 `{"tools":{}}`
97+
398
## 背景
499

5100
目前 `gf` 的子命令 `doc``source``help` 是在 C++ 代码中硬编码的。为了支持通过 Scheme 实现更多的子命令,需要设计一个工具注册系统。
@@ -211,3 +306,94 @@ v17.11.47 打包后的版本所有子命令都无法使用,因为:
211306
5. **Commit 5 - 动态注册**:通过 `gfproject.json` 动态注册 help/source/doc 命令,移除 C++ 硬编码逻辑
212307
6. **Commit 6 - version 子命令**:迁移 `version` 命令为 Scheme 实现
213308
7. **Commit 7 - help 优化**:优化 help 命令输出格式,`gf help <command>` 直接显示 `tools/<command>/README.md`
309+
310+
## 2026/04/13 gfproject.json 改为 C++ 字段级深度合并,Scheme 通过 glue 复用
311+
312+
### What
313+
`gfproject.json` 的合并逻辑统一收敛到 C++ 侧实现,并通过 glue 暴露给 Scheme:
314+
315+
1. C++ 同时读取内置 `gfproject.json` 和当前目录 `gfproject.json`
316+
2. `tools.<command>` 不再整对象覆盖,改为字段级深度合并
317+
3. 嵌套对象字段继续递归合并,例如 `description.en_US``description.zh_CN`
318+
4. `goldhelp.scm` 不再自己读文件和手写合并规则,而是通过 `g_gfproject-load-config` 直接消费 C++ 产出的最终结果
319+
320+
### Why
321+
之前的实现存在三个关键问题:
322+
323+
1. 当前目录存在 `gfproject.json` 时,会把内置配置整体遮掉,不是真正的“合并”
324+
2. 即使开始尝试合并,也只是 `tools.<command>` 的整对象覆盖;本地只改 `description` 时,会把内置的 `organization/module` 一起覆盖掉
325+
3. `goldhelp.scm` 自己维护一套独立的读取和合并逻辑,容易和 C++ 运行时语义漂移
326+
327+
这会导致:
328+
329+
- 本地只想改帮助描述,却把命令实现配坏
330+
- `gf help` 看到的结果和真实命令执行时的解析结果不一致
331+
- 在源码树运行时,`gfproject.json` 的 lib 路径还可能定位错误
332+
333+
### How
334+
`src/goldfish.hpp` 中新增并整理以下能力:
335+
336+
- `find_local_gfproject_json`:定位当前目录 `gfproject.json`
337+
- `find_lib_gfproject_json`:定位内置 `gfproject.json`,同时支持 `gf_lib``gf_lib` 的父目录
338+
- `load_json_file_or_empty`:安全读取 JSON object
339+
- `gfproject_extract_tools`:统一提取 `tools`
340+
- `gfproject_deep_merge_value`:对 `tools.<command>` 做字段级深度合并
341+
- `load_gfproject_config_bundle`:同时保留 lib/local/merged 三份配置
342+
- `load_gfproject_config`:返回最终 merged 配置
343+
344+
`glue_goldfish` 中新增 `g_gfproject-load-config`,返回合并后的 `gfproject.json` 字符串。
345+
346+
`tools/help/liii/goldhelp.scm` 中:
347+
348+
- 删除本地文件查找和手写合并逻辑
349+
- `load-gfproject` 改为直接调用 `g_gfproject-load-config`
350+
351+
---
352+
353+
## 2026/04/13 工具执行改为单命令回退,避免局部坏配置拖垮整个命令系统
354+
355+
### What
356+
调整动态工具执行逻辑:
357+
358+
1. 本地 `gfproject.json` 覆盖某个命令后,先尝试执行该命令的 merged 配置
359+
2. 如果 merged 配置非法或无法导入,只回退这个命令到 lib 配置
360+
3.`help``version``eval``load``repl``run` 等本来就有内置实现的命令,若动态工具无法执行,再继续回退到内置逻辑
361+
4. 新增 `help``version` 的普通子命令 fallback,不再只依赖 `--help/-h``--version/-v`
362+
363+
### Why
364+
之前的问题不是单纯“合并错了”,而是失败粒度也不对:
365+
366+
1. 本地把某个命令的字段改坏时,错误会在动态加载阶段直接暴露,用户看到的是整个命令失败
367+
2. `eval``load``run` 这类本来应该走内置逻辑的命令,只要被 `gfproject.json` 命中,就可能在动态工具分发阶段提前报错
368+
3. `help``version` 缺少普通子命令的明确 fallback,回退链条不完整
369+
370+
我们真正需要的是:
371+
372+
- 本地配置优先
373+
- 单命令出错时只影响该命令
374+
- 该命令还能安全退回 lib 或内置实现
375+
376+
### How
377+
`src/goldfish.hpp` 中新增并重构以下执行辅助函数:
378+
379+
- `resolve_gfproject_tool`:解析某个命令的 local/lib/merged 状态
380+
- `goldfish_prepare_tool_main`:校验配置并准备动态模块执行
381+
- `goldfish_run_tool_with_config`:用一份具体配置尝试执行一个命令
382+
- `goldfish_finish_tool_error` / `goldfish_finish_tool_success`:统一处理命令执行后的收尾
383+
- `goldfish_reset_captured_error_port`:在 fallback 前清理错误输出缓冲
384+
385+
主流程改为:
386+
387+
1. 动态工具分发先尝试 merged 配置
388+
2. merged 配置失败时,只对当前命令回退到 lib 配置
389+
3. lib 配置仍不适用且该命令有内置实现时,返回 `-1`,让主命令分发继续走内置 `help``version``eval``load``repl``run`
390+
391+
对应回归测试新增在:
392+
393+
- `tools/help/tests/liii/goldhelp/load-gfproject-test.scm`
394+
395+
覆盖场景包括:
396+
397+
1. 本地只覆盖 `description` 时,`organization/module` 仍保留
398+
2. 本地只覆盖 `description.zh_CN` 时,`description.en_US` 仍保留
399+
3. 本地把 `version.module` 改坏时,`gf version` 仍然能回退输出版本信息

0 commit comments

Comments
 (0)