@calcit/procs 0.12.48 → 0.12.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Binary file
@@ -0,0 +1,569 @@
1
+ # RFC: 语义化树形导航与编辑
2
+
3
+ 状态:Draft
4
+ 日期:2026-07-06
5
+ 关联:`cr tree search-replace`、`cr tree show`、`cr query search`、`03-18-query-def-tree-show-chunked-display-plan.md`
6
+
7
+ ---
8
+
9
+ ## 1. 概要
10
+
11
+ 当前 `cr tree` 系列编辑命令在定位子表达式时依赖纯数字点号路径(如 `-p '0.3.2.1'`)。对于人类而言手动数坐标已经不方便,对于 LLM 而言更是结构性难题——LLM 在精确计数方面的可靠性与人类手动数行号相当。
12
+
13
+ 本 RFC 提出 **四层互补方案**,从近到远逐步提升编辑体验:
14
+
15
+ | 层级 | 方案 | 技术路径 |
16
+ | ------ | ------------------- | ------------------------------------------------------------------------------------- |
17
+ | **L1** | 展示时自动标注路径 | `tree show` 输出中为每个 list 表达式末尾追加 `; "previous node path: 1.2.3"` 注释节点 |
18
+ | **L2** | 多候选交互确认 | `search-replace` 多匹配时列出候选而非报错 |
19
+ | **L3** | 锚点 + 相对偏移搜索 | `search-replace --at ... --child N` 先定位父节点再做子节点替换 |
20
+ | **L4** | 结构化查询语言 | 语义路径表达式:`path` 开头,裸叶子 + `heading` + `nth` 三步导航 |
21
+
22
+ 核心设计原则:
23
+
24
+ - **所有查询表达式均使用 Cirru 语法**(无 Lisp 风格外层括号)
25
+ - **路径表达式中叶子字面量直接表示严格匹配**,表达式取首 token 作为语义算子(`heading` / `nth`)
26
+ - **路径注释使用 `; "previous node path: 1.2.3"` 格式**,作为 list 末尾的注释节点追加,不改变已有子节点索引
27
+ - **锚点使用已有 `calcit.core/noted` macro**,格式为 `noted @anchor:<name> expr`
28
+
29
+ ---
30
+
31
+ ## 2. 动机
32
+
33
+ ### 2.1 现状问题
34
+
35
+ 当前 LLM 编辑 Calcit 代码的标准工作流:
36
+
37
+ ```
38
+ 1. cr tree show 'app.main/main!' → 查看代码结构
39
+ 2. LLM 自己数目标表达式的坐标 → 容易数错
40
+ 3. cr tree replace 'app.main/main!' -p '...' → 可能用错路径
41
+ 4. 出错后重新数、重新试 → 迭代成本高
42
+ ```
43
+
44
+ 或者用 `search-replace`:
45
+
46
+ ```
47
+ 1. cr tree search-replace 'app.main/main!' --pattern 'old-expr' ...
48
+ → 报错:"Found 3 matches"
49
+ 2. LLM 需要切回 tree show 手动辨别是哪个匹配
50
+ 3. 回到手动数坐标模式
51
+ ```
52
+
53
+ ### 2.2 目标工作流
54
+
55
+ ```
56
+ 1. cr tree show 'app.main/main!' → 输出自动带路径注释
57
+ 2. LLM 直接从注释中复制路径 → 不需要数
58
+ 3. cr tree replace 'app.main/main!' -p '复制来的路径' ...
59
+ → 一次成功
60
+ ```
61
+
62
+ 或者在多匹配场景:
63
+
64
+ ```
65
+ 1. cr tree search-replace ... --pattern '...'
66
+ → 列出 3 个候选(带路径和上下文)
67
+ 2. LLM 用 --pick 0 或 --path 指定
68
+ → 一次成功
69
+ ```
70
+
71
+ ---
72
+
73
+ ## 3. L1:展示时自动标注路径
74
+
75
+ ### 3.1 方案
76
+
77
+ 在 `cr tree show` 的输出中,为每个 **list 节点**的末尾追加一条路径注释。注释放在末尾而非开头,避免插入前导节点导致已有子节点索引偏移。格式为 Cirru 行注释语法:
78
+
79
+ ```cirru
80
+ defn add (a b)
81
+ &+ a b (; "previous node path: 3.2")
82
+ ; "previous node path: 3"
83
+ ```
84
+
85
+ 实现方式:**在 AST 层面操作**,为每个 `Cirru::List` 的 children 末尾 `push` 一个 comment 节点——即 `Cirru::List`,其首子节点为 `Cirru::Leaf(";")`,后续子节点为注释内容(如 `Cirru::Leaf("previous node path: 1.2.3")`)。格式化时该 list 自然渲染为行注释 `; "previous node path: 1.2.3"`。
86
+
87
+ ### 3.2 命令
88
+
89
+ ```bash
90
+ # 默认行为:纯代码展示,无路径注释
91
+ cr tree show 'app.main/main!'
92
+
93
+ # 开启路径标注(所有嵌套层级末尾标注路径)
94
+ cr tree show 'app.main/main!' --path-annotations
95
+ ```
96
+
97
+ 当展示的节点包含较多子节点(如超过阈值)时,在输出底部提示可用选项:
98
+
99
+ ```
100
+ Tip: This node has 15 children. Use --path-annotations to annotate each child
101
+ with its path index for easier editing. Use --chunked to split large
102
+ subtrees into fragments.
103
+ ```
104
+
105
+ ### 3.3 输出示例
106
+
107
+ 对于如下源码:
108
+
109
+ ```cirru
110
+ defn process (xs)
111
+ let
112
+ ys $ map xs inc
113
+ zs $ filter ys even?
114
+ foldl zs 0 add
115
+ ```
116
+
117
+ 默认 `cr tree show` 输出(无标注,保持旧行为):
118
+
119
+ ```cirru
120
+ defn process (xs)
121
+ let
122
+ ys $ map xs inc
123
+ zs $ filter ys even?
124
+ foldl zs 0 add
125
+ ```
126
+
127
+ `--path-annotations` 时(每个嵌套 list 末尾都追加路径注释):
128
+
129
+ ```cirru
130
+ defn process (xs)
131
+ let
132
+ ys $ map xs inc
133
+ ; "previous node path: 3.0.0.1.2"
134
+ ; "previous node path: 3.0.0"
135
+ zs $ filter ys even?
136
+ ; "previous node path: 3.0.1.1.2"
137
+ ; "previous node path: 3.0.1"
138
+ foldl zs 0 add
139
+ ; "previous node path: 3.2"
140
+ ; "previous node path: 3.3"
141
+ ```
142
+
143
+ ### 3.4 实现要点
144
+
145
+ - **AST 层面操作**:调用 `children.push(Cirru::List([Cirru::Leaf(";"), Cirru::Leaf(path_string)]))` 追加注释节点,而非字符串拼接
146
+ - 注释节点格式:`Cirru::List` 首子节点为 `Cirru::Leaf(";")`,后续为内容叶子(如 `"previous node path: 1.2.3"`),渲染为 `; "previous node path: 1.2.3"`
147
+ - `--path-annotations`:递归为所有嵌套 list 末尾追加注释(flag,无参数)
148
+ - 默认不追加任何注释节点,保持旧行为
149
+ - 当展示的节点子节点较多时,底部输出 tip 提示可开启 `--path-annotations` 或 `--chunked`
150
+ - 注释中的路径数字为相对于当前 `-p` 定位 path 的索引
151
+ - 使用 dimmed 颜色渲染注释行,不干扰代码阅读
152
+ - 根节点的 path 为空字符串, 不用显示
153
+ - **末尾追加不改变索引**:注释节点是最后一个 child,不影响已有子节点的相对位置
154
+
155
+ ### 3.5 与 chunked display 的关系
156
+
157
+ `--path-annotations` 与 `--chunked` 可组合使用,两者独立运作:
158
+
159
+ - `--chunked`:表达式过大时拆分展示,便于人类阅读整体结构
160
+ - `--path-annotations`:在 chunk 内部或普通展示中标注每个节点的路径坐标
161
+
162
+ 同时启用时:先分片,再在每个 fragment 内部标注路径注释。默认两个都不启用。
163
+
164
+ ---
165
+
166
+ ## 4. L2:多候选交互确认
167
+
168
+ ### 4.1 方案
169
+
170
+ 当 `search-replace` 遇到多个匹配时,**不直接报错退出**,而是:
171
+
172
+ 1. 列出所有候选匹配(带路径、上下文预览、序号)
173
+ 2. 允许用户/LLM 通过 `--pick <index>` 或 `--path <path>` 精确指定
174
+ 3. 默认行为(无 `--pick` 也无 `--path`)保持不变:报错并要求指定
175
+
176
+ ### 4.2 命令
177
+
178
+ ```bash
179
+ # 多匹配时列出候选
180
+ cr tree search-replace 'app.main/main!' \
181
+ --pattern 'old-name' \
182
+ --code 'new-name'
183
+
184
+ # 输出候选列表后,选择第 2 个候选
185
+ cr tree search-replace 'app.main/main!' \
186
+ --pattern 'old-name' \
187
+ --code 'new-name' \
188
+ --pick 2
189
+
190
+ # 或直接用路径指定
191
+ cr tree search-replace 'app.main/main!' \
192
+ --pattern 'old-name' \
193
+ --code 'new-name' \
194
+ --at '1.3.0'
195
+ ```
196
+
197
+ ### 4.3 候选展示格式
198
+
199
+ 当匹配数 > 1 且未指定 `--pick`/`--at` 时:
200
+
201
+ ```
202
+ Found 3 matches for pattern "old-name":
203
+
204
+ [0] Path [1.3.0]: "old-name"
205
+ Context: defn update $ old-name new-name
206
+ Command: cr tree search-replace 'app.main/main!' --pattern 'old-name' ... --pick 0
207
+
208
+ [1] Path [2.5.2]: "old-name"
209
+ Context: let $ old-name x $ do-something old-name
210
+ Command: cr tree search-replace 'app.main/main!' --pattern 'old-name' ... --pick 1
211
+
212
+ [2] Path [3.0.1]: "old-name"
213
+ Context: cond $ = old-name nil $ handle-nil old-name
214
+ Command: cr tree search-replace 'app.main/main!' --pattern 'old-name' ... --pick 2
215
+
216
+ Use --pick <index> to select a candidate, or --at '<path>' to specify directly.
217
+ ```
218
+
219
+ ### 4.4 实现要点
220
+
221
+ - `--pick` 和 `--at` 互斥,同时指定时报错
222
+ - 候选按路径深度优先排序(与当前遍历顺序一致)
223
+ - `--pick` 从 0 开始
224
+ - 最多展示 20 个候选,超出部分显示 `... and N more`
225
+ - 该行为同样适用于 `search-replace` 的 list-node 匹配(非仅 leaf)
226
+
227
+ ---
228
+
229
+ ## 5. L3:锚点搜索替换(`search-replace --at`)
230
+
231
+ ### 5.1 方案
232
+
233
+ 扩展 `search-replace`,允许先通过内容匹配定位一个**父节点(锚点)**,然后在锚点的第 N 个子节点中做替换。这样 LLM 只需描述"在哪个定义/哪个 let 里面改",不需要知道锚点的全局坐标。
234
+
235
+ ### 5.2 命令
236
+
237
+ ```bash
238
+ # 基本形式:在匹配 anchor 的节点的第 N 个子节点中做搜索替换
239
+ cr tree search-replace 'app.main/main!' \
240
+ --at 'defn add' \
241
+ --child 2 \
242
+ --pattern 'old-call' \
243
+ --code 'new-call'
244
+
245
+ # 多层锚定:在 anchor 内的第 N 个子节点中再锚定
246
+ cr tree search-replace 'app.main/main!' \
247
+ --at 'let' \
248
+ --child 0 \
249
+ --at 'cond' \
250
+ --child 1 \
251
+ --pattern 'old-branch' \
252
+ --code 'new-branch'
253
+ ```
254
+
255
+ ### 5.3 语义
256
+
257
+ `--at` 与 `--child` 的语义:
258
+
259
+ - `--at <quoted-code>`:在当前范围内搜索匹配该内容的节点。相当于先做 `search-replace` 的匹配逻辑,**但不替换**。
260
+ - `--child <N>`:从匹配到的锚点进入第 N 个子节点,缩小搜索范围。
261
+
262
+ 组合效果:
263
+
264
+ ```
265
+ search-replace target --at A --child 0 --at B --child 1 --pattern P --code R
266
+ ```
267
+
268
+ 等价于:
269
+
270
+ 1. 在 target 中搜索匹配 A 的节点 → 锚点 a
271
+ 2. 取 a 的第 0 个子节点 → scope₁
272
+ 3. 在 scope₁ 中搜索匹配 B 的节点 → 锚点 b
273
+ 4. 取 b 的第 1 个子节点 → scope₂
274
+ 5. 在 scope₂ 中搜索匹配 P 的节点 → 替换为 R
275
+
276
+ ### 5.4 约束
277
+
278
+ - `--at` 在目标范围内**必须唯一匹配**,否则报错列出候选(沿用 L2 的交互逻辑)
279
+ - `--child` 索引从 0 开始
280
+ - 多个 `--at` / `--child` 按出现顺序链式执行
281
+ - `--at` 的参数使用与 L4 路径表达式相同的语义:裸叶子表示严格匹配叶子,表达式取首 token 作为算子(见 §6)
282
+
283
+ ---
284
+
285
+ ## 6. L4:结构化查询语言(Cirru Path Expression)
286
+
287
+ ### 6.1 设计目标
288
+
289
+ 提供一种**完全用 Cirru 语法书写**的语义路径表达式,替代纯数字坐标,让 LLM 和人类都能直观描述"我要编辑哪个节点"。
290
+
291
+ 核心设计:
292
+
293
+ - 表达式以 `path` 开头
294
+ - **叶子字面量直接表示严格匹配**:`x`、`|hello`、`42` 等裸叶子值表示"当前节点必须是这个叶子"
295
+ - **表达式取首 token 作为语义算子**:`heading`、`nth`
296
+ - 从左到右链式执行,每个选择器在上一个匹配结果上继续缩小范围
297
+
298
+ 选择器只有三种:裸叶子精确匹配叶子值,`heading` 按前缀匹配 list 节点,`nth` 按索引进入子节点。配合 L1 的路径标注(`--path-annotations`)显示各子节点索引,无需额外的搜索型选择器。
299
+
300
+ ### 6.2 选择器类型
301
+
302
+ #### 6.2.1 叶子严格匹配(裸字面量)
303
+
304
+ 叶子节点直接书写,表示"当前节点必须精确等于此值":
305
+
306
+ ```cirru
307
+ path defn add
308
+ ```
309
+
310
+ 语义:先匹配叶子 `defn`,再匹配叶子 `add`。等价于在 AST 中找连续两个叶子 `defn` `add` 的位置。
311
+
312
+ - 标识符:`x`、`defn`、`add`
313
+ - 字符串:`|hello`
314
+ - 数字:`42`
315
+ - tag:`:name`
316
+
317
+ #### 6.2.2 `heading` — 表达式前缀匹配
318
+
319
+ 唯一的 list 节点内容匹配选择器:
320
+
321
+ ```cirru
322
+ path
323
+ heading def {} :name |add
324
+ ```
325
+
326
+ 语义:匹配任何 **前 N 个子节点** 与给定模式一致的 list 节点,允许后面有更多子节点。当无多余子节点时等同于精确匹配。
327
+
328
+ 示例:
329
+
330
+ ```cirru
331
+ ; 匹配任意以 def 开头的表达式(def, defn, defrecord, defenum ...)
332
+ path
333
+ heading def
334
+
335
+ ; 匹配 defn add (a b) (&+ a b)——有 or 没有多余子节点都命中
336
+ path
337
+ heading defn add (a b)
338
+ &+ a b
339
+ ```
340
+
341
+ 嵌套表达式内的 `(a b)` 递归地用相同规则匹配。
342
+
343
+ #### 6.2.3 `nth` — 位置导航
344
+
345
+ ```cirru
346
+ path
347
+ heading defn add (a b)
348
+ &+ a b
349
+ nth 2
350
+ ```
351
+
352
+ 语义:匹配目标后,进入其第 2 个子节点。
353
+
354
+ - `nth N`:进入当前匹配节点的第 N 个子节点(从 0 计数)
355
+
356
+ ### 6.3 组合示例
357
+
358
+ 定位 add 函数的第一个 let 绑定:
359
+
360
+ ```cirru
361
+ path
362
+ heading def {} :name |add
363
+ nth 2
364
+ heading let
365
+ nth 0
366
+ ```
367
+
368
+ 等价于:
369
+
370
+ 1. 匹配任意 `def {} :name |add ...` 表达式 → 定位 add 函数
371
+ 2. 进入第 2 个子节点(body)
372
+ 3. 匹配以 `let` 开头的表达式
373
+ 4. 进入第 0 个子节点(bindings)
374
+
375
+ ### 6.4 完整语法规范
376
+
377
+ ```
378
+ path-expr = "path" selector*
379
+
380
+ selector = leaf-literal ; 叶子严格匹配
381
+ | list-selector ; 表达式语义选择器
382
+
383
+ list-selector = "heading" children ; 表达式前缀匹配(含精确匹配)
384
+ | "nth" integer ; 位置导航
385
+
386
+ children = (leaf-literal | nested-expr)*
387
+ nested-expr = "(" children ")" ; 在 Cirru 中即缩进嵌套的 list
388
+
389
+ leaf-literal = identifier | string-literal | number | tag
390
+ integer = /\d+/
391
+ ```
392
+
393
+ ### 6.5 使用场景
394
+
395
+ #### 场景 A:定位定义
396
+
397
+ ```bash
398
+ # 搜索 add 函数的 def,拿到路径
399
+ cr query path 'app.main' \
400
+ --selector 'path
401
+ heading def {} :name |add'
402
+
403
+ # 输出: 0 (def 的顶层索引)
404
+ ```
405
+
406
+ #### 场景 B:编辑深层子表达式
407
+
408
+ ```bash
409
+ # 在 add 函数的第一个 let 绑定中搜索并替换
410
+ cr tree search-replace 'app.main/main!' \
411
+ --path-selector 'path
412
+ heading def {} :name |add
413
+ nth 2
414
+ heading let
415
+ nth 0' \
416
+ --pattern 'old-var' \
417
+ --code 'new-var'
418
+ ```
419
+
420
+ #### 场景 C:批量脚本
421
+
422
+ ```bash
423
+ # 获取路径后用于后续编辑
424
+ PATH=$(cr query path 'app.main' --selector 'path heading def {} :name |init-fn $ nth 2')
425
+ cr tree replace 'app.main/main!' -p "$PATH" --code '...'
426
+ ```
427
+
428
+ ### 6.6 与现有路径的互操作
429
+
430
+ - `cr query path` 输出标准数字路径(如 `1.3.0`),可直接用于 `-p`
431
+ - `--path-selector` 是 `-p` 的超集替代,内部先解析为数字路径再执行
432
+ - 解析失败时给出明确错误信息(哪一步匹配失败、已匹配到的范围、剩余选择器是什么)
433
+
434
+ ### 6.7 选择器语义对比
435
+
436
+ | 选择器 | 匹配方式 | 示例 |
437
+ | ------------- | ------------------------------- | ---------------------- |
438
+ | 裸叶子 `x` | 当前节点必须是叶子 `x` | `path defn add` |
439
+ | `heading ...` | 当前 list 的前 N 个子节点匹配 | `heading def {} :name` |
440
+ | `nth N` | 导航到当前 list 的第 N 个子节点 | `nth 2` |
441
+
442
+ ---
443
+
444
+ ## 7. 锚点注释(Source Annotations)
445
+
446
+ ### 7.1 方案
447
+
448
+ 使用已有的 `calcit.core/noted` macro 在源码中定义**命名锚点**,作为编辑时的稳定引用:
449
+
450
+ ```cirru
451
+ defn main! ()
452
+ noted @anchor:init-state
453
+ let
454
+ state $ load-initial-state
455
+ ; ...
456
+ ```
457
+
458
+ `noted` 是已有 macro,接受 tag 和表达式两个参数。`@anchor:<name>` 作为 tag 标记该表达式,`noted` 在运行时透传表达式的值,锚点信息不参与运行时语义。
459
+
460
+ 锚点附着在表达式上,表达式被移动/复制时锚点跟随。`cr query anchors` 遍历 AST 中所有 `noted` 调用,提取 `@anchor:` 前缀的 tag 及其路径。
461
+
462
+ ### 7.2 命令
463
+
464
+ ```bash
465
+ # 列出所有锚点
466
+ cr query anchors 'app.main'
467
+
468
+ # 输出:
469
+ # @anchor:init-state → app.main/main! [1]
470
+ # @anchor:render-loop → app.main/main! [4.2]
471
+
472
+ # 用锚点定位
473
+ cr tree show 'app.main/main!' --anchor 'init-state'
474
+
475
+ # 用锚点编辑:在锚点后插入
476
+ cr tree insert-after 'app.main/main!' \
477
+ --anchor 'init-state' \
478
+ --code 'println |loaded'
479
+ ```
480
+
481
+ ### 7.3 约束
482
+
483
+ - 锚点 tag 以 `@anchor:` 前缀标识,在同一 namespace 内必须唯一(不唯一时报错)
484
+ - `noted` 在运行时透传表达式值,锚点不参与运行时语义
485
+ - 锚点跟随表达式移动:`tree delete` / `tree insert` 等操作后,锚点随 `noted` 节点自然位移
486
+ - `cr query anchors` 遍历 AST 中所有 `noted` 调用,提取路径和名称
487
+
488
+ ### 7.4 锚点与路径的对比
489
+
490
+ | 特性 | 路径 (`-p '1.3.0'`) | 锚点 (`--anchor 'init-state'`) |
491
+ | ---------- | ---------------------- | ------------------------------ |
492
+ | 稳定性 | 编辑后可能失效 | 跟随代码移动,基本稳定 |
493
+ | 可读性 | 无意义数字 | 语义化名称 |
494
+ | 设置成本 | 零(自动标注) | 需要手动添加注释 |
495
+ | LLM 友好度 | 低(需要数坐标或复制) | 高(语义化引用) |
496
+
497
+ ---
498
+
499
+ ## 8. 实施路线
500
+
501
+ ### Phase 1(最小可行):L1 + L2
502
+
503
+ - `tree show --path-annotations`:opt-in 标志,开启后所有嵌套层级标注路径
504
+ - 大节点底部 tip 提示可开启标注或分片
505
+ - `search-replace` 多匹配候选展示(不改默认行为,仅在匹配 > 1 时改进输出)
506
+
507
+ **预计工作量**:~2-3 天
508
+ **收益**:LLM 可直接从 show 输出复制路径,多匹配时给出可操作建议
509
+
510
+ ### Phase 2:L3 锚点搜索
511
+
512
+ - `search-replace --at` / `--child` 链式锚定
513
+ - 将 `search-replace` 从仅 leaf 匹配扩展到 list-node 匹配
514
+
515
+ **预计工作量**:~3-5 天
516
+ **收益**:LLM 可用语义描述定位,不再依赖数字路径
517
+
518
+ ### Phase 3:L4 结构化查询语言
519
+
520
+ - `path` 选择器解析器
521
+ - `cr query path` 命令
522
+ - `--path-selector` 替代 `-p` 的编辑命令集成
523
+
524
+ **预计工作量**:~5-7 天
525
+ **收益**:完整的语义化树形查询能力
526
+
527
+ ### Phase 4:锚点注释
528
+
529
+ - `noted @anchor:<name>` 的识别与提取
530
+ - `cr query anchors` 命令
531
+ - `--anchor` 参数集成到编辑命令
532
+
533
+ **预计工作量**:~4-6 天
534
+ **收益**:跨编辑会话的稳定引用
535
+
536
+ ---
537
+
538
+ ## 9. 兼容性
539
+
540
+ - 所有新参数均为 **opt-in**,现有行为完全保留
541
+ - `--path-annotations` 是 flag,默认关闭,传即开启
542
+ - `--chunked` 默认关闭,需手动开启
543
+ - `--pick` 和 `--path-selector` 与现有 `-p` 互斥
544
+ - 锚点使用已有 `noted` macro,不引入新语法,对现有解析无影响
545
+
546
+ ---
547
+
548
+ ## 10. 开放问题
549
+
550
+ 1. **`--path-annotations` 是否应该默认开启?**
551
+ - 关闭(当前决定):保持旧行为,大节点时底部 tip 引导开启
552
+ - 默认开启:对 LLM 更友好,但改变默认输出
553
+ - 建议默认关闭 + tip 引导,后续根据使用反馈决定是否改为默认开启
554
+
555
+ 2. **`heading` 是否总是够用?**
556
+ - `heading` 匹配前 N 个子节点,无多余子节点时等同于精确匹配
557
+ - 裸叶子处理单值精确匹配
558
+ - 不提供独立的 `exact` 选择器——链式导航模型中没有它的位置
559
+ - 后续评估是否需要 `ends-with`、`contains` 等变体
560
+
561
+ 3. **是否需要支持通配符叶子?**
562
+ - 例:用 `_` 匹配任意单个叶子,`...` 匹配任意剩余子节点
563
+ - `path heading def _ :name` 匹配任意名称的 def
564
+ - 当前暂不支持,可作为后续增强
565
+
566
+ 4. **锚点应缓存还是每次遍历 AST?**
567
+ - 缓存:`cr query anchors` 首次解析后缓存到 snapshot 元数据,编辑后失效重算
568
+ - 实时遍历:更简单,无需维护缓存一致性
569
+ - 由于 `noted` 节点在 AST 中自然存在,遍历成本可控,建议先实时遍历
@@ -0,0 +1,223 @@
1
+ # RFC: `:features` 函数能力标记 + `:js-object` 类型收敛
2
+
3
+ 状态:Draft
4
+ 日期:2026-07-08
5
+
6
+ ---
7
+
8
+ ## 1. 概要
9
+
10
+ 在函数 schema 中新增 `:features` 属性(HashSet),标记当前函数可使用的语言能力子集。默认空集合。使用 JS interop 语法(`aget`、`&js-object`、`new` 等)要求函数显式标记 `:features $ #{} :js-ffi`,从类型层面将 FFI 隔离在有限的 binding 函数中。
11
+
12
+ 同步新增 `:js-object` 类型注解,表示 JavaScript FFI 操作产生的外部 opaque 数据,阻断 JS 对象向纯 Calcit 函数的无意识泄露。
13
+
14
+ ## 2. 动机
15
+
16
+ ### 问题
17
+
18
+ 当前 JS FFI 函数(`is_js_syntax_procs` 收录 21 个)在 `codegen::codegen_mode()` 下全局放行,任何函数都可以随意调用:
19
+
20
+ ```cirru
21
+ ;; 任意函数都能直接调用 FFI,无任何约束
22
+ defn my-pure-fn (x)
23
+ aget x |someProp ;; 静默通过
24
+ &js-object $ {} (:a 1) ;; 静默通过
25
+ ```
26
+
27
+ 这导致:
28
+
29
+ - **边界模糊**:无法从代码中区分"纯 Calcit 函数"和"FFI binding 函数"
30
+ - **审查困难**:排查 JS interop 使用点需要全文搜索 21 个函数名
31
+ - **类型不安全**:JS 对象可以自由流经所有函数,静态分析无法追踪外部数据
32
+ - **重构风险**:修改 FFI 层时无法确定影响范围
33
+
34
+ ### 目标
35
+
36
+ | 目标 | 说明 |
37
+ | -------- | ------------------------------------------------------------------------ |
38
+ | 权限收敛 | 只有 schema 中声明 `:js-ffi` 的函数才能调用 JS FFI |
39
+ | 类型隔离 | `:js-object` 类型标注 JS 对象,跨非 FFI 边界时产生告警 |
40
+ | 渐进迁移 | 先 warning 后 error,给生态适配时间 |
41
+ | 可扩展 | `:features` 设计预留 `:side-effects`、`:async`、`:unsafe` 等未来能力标记 |
42
+
43
+ ## 3. 设计
44
+
45
+ ### 3.1 `:features` — Schema 属性
46
+
47
+ 在 `CalcitFnTypeAnnotation` 中新增 `features` 字段:
48
+
49
+ ```rust
50
+ pub struct CalcitFnTypeAnnotation {
51
+ pub generics: Arc<Vec<Arc<str>>>,
52
+ pub where_bounds: Arc<Vec<CalcitGenericBound>>,
53
+ pub arg_types: Vec<Arc<CalcitTypeAnnotation>>,
54
+ pub return_type: Arc<CalcitTypeAnnotation>,
55
+ pub fn_kind: SchemaKind,
56
+ pub rest_type: Option<Arc<CalcitTypeAnnotation>>,
57
+ /// Feature flags declared in schema, e.g. `:features $ #{} :js-ffi`
58
+ pub features: Arc<HashSet<EdnTag>>,
59
+ }
60
+ ```
61
+
62
+ ### 3.2 `:js-object` — 类型注解
63
+
64
+ 在 `CalcitTypeAnnotation` 枚举中新增变体:
65
+
66
+ ```rust
67
+ pub enum CalcitTypeAnnotation {
68
+ // ... 现有变体不变 ...
69
+ /// JavaScript FFI external object (opaque to Calcit type system)
70
+ JsObject,
71
+ }
72
+ ```
73
+
74
+ EDN 序列化:tag 名为 `:js-object`。
75
+
76
+ ### 3.3 Canonical Cirru EDN 形态
77
+
78
+ ```cirru
79
+ %{} :CodeEntry
80
+ :doc "|FFI binding for DOM event handling"
81
+ :code $ quote
82
+ defn handle-click (event)
83
+ let
84
+ target $ aget event |target
85
+ value $ aget target |value
86
+ println value
87
+ :schema $ :: :fn
88
+ {}
89
+ :args $ [] :js-object
90
+ :return :unit
91
+ :features $ #{} :js-ffi
92
+ ```
93
+
94
+ 一行 Cirru EDN 示例:
95
+
96
+ ```cirru
97
+ (:: :fn ({} (:args ([] :js-object)) (:return :unit) (:features (#{} :js-ffi))))
98
+ ```
99
+
100
+ ### 3.4 校验流程
101
+
102
+ ```mermaid
103
+ flowchart TD
104
+ A[preprocess_expr 符号解析] --> B{调用目标是 JS FFI 函数?}
105
+ B -->|否| C[正常预处理]
106
+ B -->|是| D{当前正在编译的 def 有 :js-ffi?}
107
+ D -->|是| C
108
+ D -->|否| E[产生 Error/Warning]
109
+
110
+ F[函数 A 返回 JsObject] --> G[函数 B 接收 JsObject 作为参数]
111
+ G --> H{B 的 schema 有 :js-ffi?}
112
+ H -->|否| I[类型告警: JsObject 流入非 FFI 函数]
113
+ H -->|是| J[放行]
114
+ ```
115
+
116
+ ### 3.5 校验位置
117
+
118
+ | 校验点 | 位置 | 触发条件 |
119
+ | ----------------- | ------------------------------------------ | ----------------------------------------------------------------------------------- |
120
+ | FFI 函数调用 | `preprocess_expr` → `preprocess_list_call` | head 解析为 `is_js_syntax_procs` 且当前编译上下文中 `defn` 无 `:js-ffi` |
121
+ | `ns js/...` 语法 | `preprocess_expr` 中 `RawCode(Js, _)` 分支 | 同上的 features 检查 |
122
+ | `JsObject` 跨边界 | `check_user_fn_arg_types` | 实参类型为 `JsObject` 且形参 schema 非 `JsObject` 且函数无 `:js-ffi` |
123
+ | `JsObject` 返回值 | `check_function_return_type` | 返回值推断为 `JsObject` 且函数 schema 的 `:return` 非 `JsObject` 且函数无 `:js-ffi` |
124
+
125
+ ### 3.6 默认值与兼容
126
+
127
+ - `:features` 默认 `Arc::new(HashSet::new())`,即空集合 = 无任何特殊能力
128
+ - `:js-object` 类型的默认行为:在无 `:js-ffi` 的函数中作为 `:dynamic` 等效处理(宽松模式),或产生类型告警(严格模式)
129
+ - 第一阶段以 **warning** 形式上线,通过 `--warn-ffi` flag 控制,后续升级为 error
130
+
131
+ ## 4. FFI 函数清单
132
+
133
+ 当前 `is_js_syntax_procs()` 收录的函数,全部受 `:js-ffi` 管控:
134
+
135
+ | 函数 | 用途 | 返回类型建议 |
136
+ | --------------------------------------- | -------------------- | -------------------------- |
137
+ | `aget` / `js-get` | JS 属性读取 | `:js-object` 或 `:dynamic` |
138
+ | `aset` / `js-set` | JS 属性写入 | `:unit` |
139
+ | `js-delete` | JS 属性删除 | `:unit` |
140
+ | `exists?` | JS 属性存在性检查 | `:bool` |
141
+ | `instance?` | JS instanceof | `:bool` |
142
+ | `&js-object` | 创建 JS 对象 | `:js-object` |
143
+ | `js-array` | 创建 JS 数组 | `:js-object` |
144
+ | `js-await` / `js-for-await` | Promise/异步迭代 | `:js-object` |
145
+ | `new` | JS new 构造 | `:js-object` |
146
+ | `set!` | JS 赋值 | `:unit` |
147
+ | `to-js-data` | Calcit → JS 转换 | `:js-object` |
148
+ | `to-calcit-data` | JS → Calcit 转换 | `:dynamic` |
149
+ | `extract-cirru-edn` / `to-cirru-edn` | EDN 序列化 | `:string` / `:dynamic` |
150
+ | `&raw-code` | 原始 JS 代码 | `:js-object` |
151
+ | `timeout-call` | 异步定时 | `:js-object` |
152
+ | `foldl` | fold-left(JS 实现) | 依赖元素类型 |
153
+ | `load-console-formatter!` / `printable` | 控制台输出 | `:unit` |
154
+
155
+ > **注**:`foldl` 虽然在 JS FFI 列表中,但它是纯函数式的 fold 操作,未来可考虑移出 FFI 列表或单独归类。
156
+
157
+ ## 5. 实现计划
158
+
159
+ ### Phase 1: 数据层(~2h)
160
+
161
+ - [ ] `CalcitFnTypeAnnotation.features` 字段新增
162
+ - [ ] EDN 序列化 / 反序列化(`to_schema_edn` / `parse_loaded_schema_annotation`)
163
+ - [ ] `CalcitTypeAnnotation::JsObject` 变体新增 + 全量 match 补齐
164
+ - [ ] `JsObject` 的 EDN tag 解析(`:js-object`)
165
+
166
+ ### Phase 2: 校验层(~4h)
167
+
168
+ - [ ] `preprocess_list_call` 中 FFI 调用权限校验
169
+ - [ ] `preprocess_expr` 中 `ns js/...` 语法的 FFI 权限校验
170
+ - [ ] 编译上下文传递:在 `ensure_ns_def_preprocessed` 链路中追踪当前 `defn` 的 features
171
+ - [ ] `check_user_fn_arg_types` 中 `JsObject` 跨边界告警
172
+ - [ ] `check_function_return_type` 中 `JsObject` 返回值告警
173
+ - [ ] `--warn-ffi` CLI flag(默认开启 warning 模式)
174
+
175
+ ### Phase 3: 类型推断(~2h)
176
+
177
+ - [ ] `&js-object`、`js-array`、`new` 等 FFI 函数的返回值推断为 `JsObject`
178
+ - [ ] `aget` 对 `JsObject` 参数的接收校验
179
+ - [ ] `to-calcit-data` 作为 `JsObject → Dynamic` 的转换边界
180
+
181
+ ### Phase 4: 迁移与测试(~3h)
182
+
183
+ - [ ] 现有 `calcit/` 下使用 JS FFI 的测试文件 schema 迁移
184
+ - [ ] 新增 `calcit/test-ffi-features.cirru` 测试用例:
185
+ - 无 `:js-ffi` 调用 `aget` → 预期 warning/error
186
+ - 有 `:js-ffi` 调用 `aget` → 预期通过
187
+ - 无 `:js-ffi` 接收 `JsObject` 参数 → 预期类型告警
188
+ - 有 `:js-ffi` 接收 `JsObject` → 预期通过
189
+ - `to-calcit-data` 边界转换后不再需要 `:js-ffi`
190
+ - [ ] 文档更新(`docs/features.md`)
191
+
192
+ ### Phase 5: 严格模式(后续版本)
193
+
194
+ - [ ] 缺少 `:js-ffi` 的 FFI 调用从 warning 升级为 hard error
195
+ - [ ] `:js-object` 可能扩展到区分 `:js-object` vs `:js-array` vs `:js-promise`
196
+
197
+ ## 6. 风险与缓解
198
+
199
+ | 风险 | 影响 | 缓解 |
200
+ | --------------------------------------- | ------------------ | ---------------------------------------------------- |
201
+ | 现有 Cirru 代码大量使用 FFI 但无 schema | 迁移工作量大 | 先 warning 后 error;提供迁移脚本 |
202
+ | `foldl` 列入 FFI 引起误报 | 纯函数被标记为 FFI | 考虑将 `foldl` 移出 `is_js_syntax_procs` 或单独分类 |
203
+ | 宏生成的函数自动获取 `:js-ffi` | 权限继承不明确 | 宏生成函数的 features 从宏定义处显式声明,不自动继承 |
204
+ | 第三方库适配周期长 | 生态断裂 | 预留至少一个大版本的 warning-only 过渡期 |
205
+
206
+ ## 7. 未来扩展
207
+
208
+ `:features` 设计为 `HashSet<EdnTag>`,天然支持未来新增能力标记:
209
+
210
+ | 标记 | 含义 |
211
+ | --------------- | ------------------------------------------------ |
212
+ | `:js-ffi` | 可使用 JS interop 语法(本次实现) |
213
+ | `:side-effects` | 函数可能产生副作用(未来:配合 effects graph) |
214
+ | `:async` | 函数内部使用异步操作(未来:配合 effect 系统) |
215
+ | `:unsafe` | 函数绕过了类型检查(未来:配合 unsafe block) |
216
+ | `:host` | 函数需要宿主环境 API(未来:配合多 target 编译) |
217
+
218
+ ## 8. 参考资料
219
+
220
+ - 现有 JS FFI 函数定义:`src/builtins.rs:649` `is_js_syntax_procs()`
221
+ - 现有 Schema 结构:`src/calcit/type_annotation.rs:3826` `CalcitFnTypeAnnotation`
222
+ - 现有预处理入口:`src/runner/preprocess/mod.rs` `preprocess_expr()`
223
+ - 相关 RFC:[Type Slot 机制](./04-13-type-slot-mechanism-rfc.md)、[泛型 `:where` 约束](./05-31-generic-where-bounds-mfs.md)
package/RFCs/README.md CHANGED
@@ -29,6 +29,7 @@
29
29
  | `05-31-generic-where-bounds-mfs.md` | Active | 函数 schema 泛型 `:where` 约束的最小功能规格,先作为主链路开发基线。 |
30
30
  | `06-15-effects-graph-rfc.md` | Draft | `cr analyze effects-graph`:State/Transform/Effect 语义分解图与类型驱动 effect 标注路线。 |
31
31
  | `06-29-cr-exec-cli-builtins-rfc.md` | **Active** | `cr exec` + `calcit.cli/*` 内建函数:绕过 Shell 转义的 Cirru 函数调用方案。 |
32
+ | `07-06-semantic-tree-navigation-rfc.md` | Draft | 语义化树形导航与编辑:路径标注、多候选交互、锚点搜索替换、结构化查询语言。 |
32
33
 
33
34
  ## 已执行的清理
34
35
 
@@ -0,0 +1,49 @@
1
+ # 语义化树形导航实现 (2026-07-07)
2
+
3
+ ## 变更概要
4
+
5
+ 实现 RFC `07-06-semantic-tree-navigation-rfc.md` 全部四个 Phase。
6
+
7
+ ## 实现功能
8
+
9
+ ### L1: `--path-annotations`
10
+ - `cr tree show` 新增 `--path-annotations` flag
11
+ - 开启后递归为所有嵌套 `Cirru::List` 末尾追加 `; "previous node path: X.Y.Z"` 注释节点
12
+ - AST 层面操作:`children.push(Cirru::List([Cirru::Leaf(";"), Cirru::Leaf(path_string)]))`
13
+ - 子节点 > 8 时底部 tip 提示可开启
14
+
15
+ ### L2: `--pick` 多候选选择
16
+ - `search-replace` 多匹配时展示带路径、上下文的候选列表
17
+ - `--pick <N>` 直接选择第 N 个候选执行替换
18
+
19
+ ### L4: 语义路径表达式
20
+ - `cr query path <ns> --selector 'path heading ... nth ...'`
21
+ - 三个选择器:裸叶子(精确匹配)、`heading`(前缀匹配)、`nth`(位置导航)
22
+ - `resolve_path_expression` 函数解析并返回数字路径
23
+
24
+ ### L4: `--selector` 限定搜索范围
25
+ - `search-replace --selector 'path ...'` 在语义路径定位的子树内搜索替换
26
+ - 与 `cr query path` 共用同一套选择器语法
27
+
28
+ ### Phase 4: `cr query anchors`
29
+ - 遍历 AST 中 `noted @anchor:<name>` 调用,输出锚点名称和路径
30
+
31
+ ## 修改文件
32
+
33
+ | 文件 | 变更 |
34
+ |------|------|
35
+ | `src/cli_args.rs` | 新增 `path_annotations`, `pick`, `selector` 字段;新增 `QueryPathCommand`, `QueryAnchorsCommand` |
36
+ | `src/bin/cli_handlers/tree.rs` | `annotate_paths` 函数;`handle_show` 路径标注 + tip;`handle_search_replace` 多候选 + selector |
37
+ | `src/bin/cli_handlers/query.rs` | `resolve_path_expression`(pub), `starts_with_pattern`, `handle_query_path`, `handle_query_anchors`, `find_anchors` |
38
+ | `src/bin/cli_handlers/command_echo.rs` | Path/Anchors 子命令 echo 支持 |
39
+ | `docs/CalcitAgent.md` | 精简引用新功能 |
40
+ | `docs/run/agent-advanced.md` | 详细示例文档 |
41
+ | `RFCs/07-06-semantic-tree-navigation-rfc.md` | 新增 RFC |
42
+
43
+ ## 设计决策
44
+
45
+ - `--at`/`--child` 链式语法被 `--selector` 替代,代码中已移除
46
+ - `exact` 选择器不纳入——链式导航模型不需要独立精确匹配,`heading` 无多余子节点时等同
47
+ - `sub-path` 选择器不纳入——L1 路径标注让 `nth` 足够
48
+ - 路径注释放在 list 末尾(不破坏已有子节点索引)
49
+ - `Cirru::Leaf` 存原始内容,格式化时自动加引号
@@ -0,0 +1,25 @@
1
+ # `:features` function schema + `:js-object` type + CLI simplification
2
+
3
+ ## Changes
4
+
5
+ ### Feature: `:features` and `:js-object` type system
6
+ - Added `features: Arc<HashSet<EdnTag>>` to `CalcitFnTypeAnnotation` for marking function capabilities
7
+ - Added `CalcitTypeAnnotation::JsObject` variant for opaque JS FFI data
8
+ - EDN round-trip for both (`features_to_edn` + `parse_fn_features_from_form`)
9
+ - Manually implemented `PartialOrd`/`Ord`/`Hash` for `CalcitFnTypeAnnotation`
10
+ - `CURRENT_FN_FEATURES` thread-local + `lookup_def_schema` fallback for FFI checking
11
+ - `as_fn()` helper on `CalcitTypeAnnotation`
12
+ - `#{}` hashset support in `schema_cirru_to_edn`
13
+ - `:features` field validation in `validate_schema_for_write`
14
+ - All direct FFI callers marked with `:features $ #{} :js-ffi`
15
+
16
+ ### Refactor: CLI simplification
17
+ - Removed `--json` option and `--json-input` switch from all edit/tree subcommands
18
+ - Auto-detect JSON (`[` prefix) vs Cirru EDN (`quote` prefix) in `parse_input_to_cirru`
19
+ - Simplified `read_code_input` to `(file, code)` params; stdin fallback
20
+ - Removed `json()`/`json_input()` from `InsertOperation` trait
21
+ - Simplified `CodeInputParts` to only carry `file`+`code`
22
+ - Added stdin mention to all edit subcommand help texts
23
+ - Updated agent docs (CalcitAgent.md, agent-advanced.md, cirru-syntax.md, edit-tree.md)
24
+ - Cleaned up stale short flag mentions (`-j`, `-J`, `-e`)
25
+ - Changed js-interop doc blocks to `no-check` to avoid FFI warnings
package/lib/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calcit/procs",
3
- "version": "0.12.48",
3
+ "version": "0.12.49",
4
4
  "main": "./lib/calcit.procs.mjs",
5
5
  "devDependencies": {
6
6
  "@types/node": "^25.7.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calcit/procs",
3
- "version": "0.12.48",
3
+ "version": "0.12.49",
4
4
  "main": "./lib/calcit.procs.mjs",
5
5
  "devDependencies": {
6
6
  "@types/node": "^25.7.0",