@calcit/procs 0.13.18 → 0.13.20

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
package/README.md CHANGED
@@ -135,9 +135,10 @@ Related examples and workflows:
135
135
  ```
136
136
 
137
137
  Run `caps` to resolve the recursive dependency graph and install it. Immutable revisions are stored under
138
- `~/.config/calcit/modules/.store/`, while the current project receives links under `.calcit/modules/`.
138
+ `~/.config/calcit/module-caches/`, while the current project receives links under `.calcit/modules/`.
139
139
  Different projects can therefore use different revisions without switching a shared checkout. Existing
140
- `~/.config/calcit/modules/<repo>/` checkouts remain a runtime fallback during migration.
140
+ project module links are the only runtime source for package-style module paths; explicit relative and
141
+ absolute paths retain their normal direct resolution.
141
142
 
142
143
  Published SemVer tags are preferred. Branch refs remain supported for development, but `caps` warns with
143
144
  the resolved commit. When a graph requests several SemVer tags for one repository, the highest requested
@@ -157,8 +158,8 @@ To load modules, use `:modules` configuration and the runtime snapshot file `cal
157
158
  :modules $ [] |memof/calcit.cirru |lilac/
158
159
  ```
159
160
 
160
- Paths defined in `:modules` first load from the snapshot directory's `.calcit/modules/`, then fall back to
161
- `~/.config/calcit/modules/`, i.e. `.calcit/modules/memof/calcit.cirru` or the legacy global path.
161
+ Paths defined in `:modules` load from the snapshot directory's `.calcit/modules/`, e.g.
162
+ `.calcit/modules/memof/calcit.cirru`. Run `caps` to materialize or refresh that project-local view.
162
163
 
163
164
  Modules ending with `/` are automatically suffixed with `calcit.cirru`, and still fall back to `compact.cirru` for compatibility.
164
165
 
@@ -50,21 +50,24 @@ Calcit 继续使用 GitHub repository + Git ref 管理模块,不建设 registr
50
50
 
51
51
  ## 3. 目录模型
52
52
 
53
- 当前正式路径是 `~/.config/calcit/modules/`。新 store 放在它下面,避免再引入
54
- 一套顶级目录;路径解析应集中到一个 helper,后续再考虑 XDG 或其他平台差异。
53
+ `~/.config/calcit/modules/` 保留给 legacy module root;新的不可变 store 使用它的
54
+ 同级路径 `~/.config/calcit/module-caches/`。路径解析应集中到一个 helper,后续再
55
+ 考虑 XDG 或其他平台差异。
55
56
 
56
57
  ```text
57
- ~/.config/calcit/modules/
58
- .store/git/<owner>/<repo>/<commit>/source/
59
- .store/git/<owner>/<repo>/<commit>/realizations/<build-key>/
60
- <repo>/ # legacy clone,迁移期保留
58
+ ~/.config/calcit/
59
+ modules/
60
+ <repo>/ # legacy clone,迁移期保留
61
+ module-caches/
62
+ git/<owner>/<repo>/<commit>/source/
63
+ git/<owner>/<repo>/<commit>/realizations/<build-key>/
61
64
 
62
65
  <project>/
63
66
  deps.cirru
64
67
  .calcit/
65
68
  caps-state.cirru
66
69
  modules/
67
- <repo> -> ~/.config/calcit/modules/.store/.../source/
70
+ <repo> -> ~/.config/calcit/module-caches/.../source/
68
71
  <native-repo> -> .../realizations/<build-key>/
69
72
  tmp/
70
73
  ```
@@ -99,7 +102,7 @@ basename 建立项目链接。如果依赖图里出现 `owner-a/utils` 与 `owne
99
102
 
100
103
  `caps` 按以下顺序修改本机状态:
101
104
 
102
- 1. 在 store 的 `.store/tmp/` 下创建临时目录,解析 ref、下载 source(与 store 同文件系统,保证 rename 原子);项目侧临时 project view 写入 `.calcit/tmp/`;
105
+ 1. 在 store 的 `~/.config/calcit/module-caches/tmp/` 下创建临时目录,解析 ref、下载 source(与 store 同文件系统,保证 rename 原子);项目侧临时 project view 写入 `.calcit/tmp/`;
103
106
  2. 完成递归依赖图和版本选择;
104
107
  3. 完成需要的 native realization 与验证;
105
108
  4. 写入临时 project view;
@@ -0,0 +1,993 @@
1
+ # RFC:以 Calcit 现有类型系统约束 JavaScript FFI 边界
2
+
3
+ 状态:Draft
4
+
5
+ 日期:2026-08-18
6
+
7
+ ## 摘要
8
+
9
+ Calcit 现有的 `Struct`、`Enum`、trait、泛型函数 schema、`JsObject`、
10
+ `JsNullish<T>`、`:features #{:js-ffi}` 与 external-object trait 已经足以构成
11
+ JavaScript FFI 的主体模型。这里不建议引入一套 TypeScript 式的结构类型、联合类型、
12
+ 重载和 DOM 继承树。
13
+
14
+ 仍然需要进一步调整,重点不是增加更多 JS 专用类型,而是补齐以下能力:
15
+
16
+ 1. 所有原始 JS 操作必须经过统一的 `:js-ffi` capability gate;当前实现只覆盖部分
17
+ JS syntax proc,裸 `js/*` 解析路径尚未统一检查。
18
+ 2. `:features` 作为实现体 capability 元数据应在解析、查询和检查阶段保留,但不进入
19
+ 普通函数类型匹配、泛型统一或 trait method candidate selection。
20
+ 3. external-object trait 只在 definition 显式带有
21
+ `:ffi {:backend :js :kind :external-object}` 时启用宿主 lowering;普通 trait 的匹配和
22
+ 推断保持不变。
23
+ 4. 宿主字段写入可以使用可选的 writable lowering 元数据;默认 external field 只读,
24
+ 但这不向核心 trait 系统增加 mutability 语义。
25
+ 5. FFI wrapper 应尽快把宿主值规范化为 Calcit `Struct`、`Enum`、`Option`、`Result`
26
+ 和 `Unit`,避免把 `Dynamic`、裸 Map 或 `JsObject` 扩散到业务代码。
27
+
28
+ 本 RFC 是 Calcit 编译器的渐进改进方向,也同时规定 `js-ffi` 仓库应遵循的建模原则。
29
+ MVP 不增加 generic trait、associated type 或任何 FFI 专用的核心 type variant。
30
+
31
+ ## 背景与现状
32
+
33
+ Calcit 当前已经具备:
34
+
35
+ - `JsObject`:不透明宿主值,不自动匹配 Calcit 数据类型;
36
+ - `JsNullish<T>`:只表示 JavaScript `null`/`undefined` 边界,与 Calcit `Option<T>`
37
+ 分离;
38
+ - `:features $ #{} :js-ffi`:声明函数体允许使用 JavaScript interop;
39
+ - external-object trait:用 tag member 描述宿主字段,用 method member 描述宿主方法;
40
+ - `CodeEntry :ffi`:保存 backend、host name 和 lowering 信息,不污染普通类型 schema;
41
+ - `:names`:将 Calcit member name 映射到实际 JavaScript property name;
42
+ - generic `Fn`、generic `Struct`/`Enum`、trait `:where` bound;
43
+ - `js-get`/`js-set` 对 external field 的类型推断与写入类型检查;
44
+ - `--warn-dyn-method`:定位裸 `JsObject` 上未声明契约的字段或方法访问。
45
+
46
+ 这些能力说明总体方向已经正确:普通数据使用 Calcit nominal data type,宿主对象使用
47
+ capability trait,原始宿主值保持 opaque。
48
+
49
+ 当前仍有几个具体缺口。
50
+
51
+ ### 原始 JS 能力门禁不完整
52
+
53
+ 预处理器对 `is_js_syntax_procs` 中的调用检查 `:js-ffi`,但 `js/foo` 会在 namespace
54
+ qualified symbol 分支直接变成 `RawCode(Js, ...)`。因此门禁并未覆盖全部 raw JS 路径。
55
+
56
+ 同样需要统一纳入检查的还有:
57
+
58
+ - `js/*` global/property/call;
59
+ - `.-`、`.!`、`.?-`、`.?!`;
60
+ - `aget`、`aset`、`js-get`、`js-set`、`js-delete`;
61
+ - `js-object`、`js-array`、`new`;
62
+ - `js-await`、`js-for-await`;
63
+ - JS `set!`;
64
+ - external-object trait 最终 lowering 成的 property/method 操作;
65
+ - `unsafe-coerce` 从 `JsObject`/`JsNullish<_>` 到 external trait 的可信断言。
66
+
67
+ 门禁应基于“预处理后的 host operation kind”,而不是维护若干互相遗漏的字符串列表。
68
+
69
+ ### `:features` 的保存与检查路径不完整
70
+
71
+ `CalcitFnTypeAnnotation` 已保存 `features`,但当前函数 signature matching 不比较
72
+ features,函数类型的 `Ord`/`Hash` 也未包含 features,部分从运行时函数重建 schema 的
73
+ 路径会回到空 feature set。
74
+
75
+ MVP 不把这个差异直接升级为函数类型兼容规则。需要解决的是:函数实现体在 capability
76
+ validation 时能够找到它声明的 feature,且 query/analyze 不误报。普通函数签名匹配、
77
+ trait method matching、泛型统一和缓存中的核心类型身份不应因 FFI feature 改变。
78
+
79
+ ### 宿主泛型关系不在本 RFC 中扩展
80
+
81
+ 当前 trait 可组合、可作为 `:where` bound,但 trait definition 自身没有类型参数或
82
+ associated type。以下常见关系无法稳定表达:
83
+
84
+ - `Promise<T>` 的 resolve value;
85
+ - `Event<Target>` 的 target;
86
+ - `Iterator<T>` / `AsyncIterator<T>` 的 item;
87
+ - `MapLike<K, V>` 的 key/value;
88
+ - typed array 的 element type;
89
+ - stream 的 chunk type。
90
+
91
+ 本 RFC 选择不为这些关系扩展 trait。adapter 应在边界内消费 Promise、iterator、event
92
+ 或 map-like object,再使用现有 `Fn`、`List<T>`、`Map<K, V>`、Struct、Enum、Option 和
93
+ Result 表达公共 API。只有 generic trait 本身获得足够多非 FFI 用例时,才应在独立的
94
+ Calcit RFC 中讨论;不能由 JavaScript FFI 单独驱动核心 trait 表示和推断复杂化。
95
+
96
+ ### 字段可写性没有进入契约
97
+
98
+ external trait field 已能声明字段类型,`js-set` 也能检查赋值类型,但契约尚未区分
99
+ read-only 与 writable。DOM、Node 和 npm API 中大量属性只读;仅检查 value type 仍可能
100
+ 生成无效写入。
101
+
102
+ ## 设计原则
103
+
104
+ ### Calcit-owned data 与 host object 分离
105
+
106
+ 使用如下映射:
107
+
108
+ | 语义 | Calcit 表示 |
109
+ | --- | --- |
110
+ | 已转换、由 Calcit 拥有的数据 | `Struct` |
111
+ | 有限状态、有限分支、错误类别 | `Enum` |
112
+ | 宿主对象可提供的字段/方法能力 | external-object trait |
113
+ | 未建立契约的宿主值 | `JsObject` |
114
+ | JavaScript 可空宿主值 | `JsNullish<T>` |
115
+ | 业务缺失 | `Option<T>` |
116
+ | 可恢复错误或异常 | `Result<T, E>` |
117
+ | 只有副作用、无业务返回值 | `Unit` |
118
+
119
+ 不得把 JavaScript object 直接伪装成 Calcit `Struct`。Struct 表示已经转换并满足 Calcit
120
+ 数据不变量的值;external trait 才表示“仍由宿主拥有,但我们信任它具有这些能力”。
121
+
122
+ ### 不复制 TypeScript 类型系统
123
+
124
+ 本 RFC 不引入:
125
+
126
+ - 通用 structural object type;
127
+ - union/intersection/conditional type;
128
+ - overload resolution;
129
+ - 完整 DOM interface inheritance;
130
+ - JavaScript prototype 或 Proxy 建模;
131
+ - 依赖任意字符串字面量的 dependent typing。
132
+
133
+ 常见替代方式:
134
+
135
+ - union 用命名 Enum;
136
+ - overload 用多个明确 wrapper;
137
+ - 字符串常量集合用 Enum,并在 FFI adapter 中转换;
138
+ - 复杂宿主返回值由 decoder 转为 Struct/Enum;
139
+ - 只为项目实际使用的成员定义小型 capability trait。
140
+
141
+ ### `:schema` 与 `:ffi` 元数据正交
142
+
143
+ `:schema` 只描述 Calcit 看见的类型。`:ffi` 只描述 backend lowering、host name、target
144
+ 和 writable 等宿主事实。不要引入新的 `Ffi<T>` schema wrapper。
145
+
146
+ ### 核心类型与推断的复杂度预算
147
+
148
+ 本 RFC 的实现必须满足以下硬约束:
149
+
150
+ - 不新增 `JsPromise<T>`、`JsArray<T>`、`JsEvent<T>`、`JsIterator<T>` 等核心 type
151
+ variant;
152
+ - 不修改 `CalcitTypeAnnotation::matches_with_bindings` 来解释 `:ffi` 或 `:features`;
153
+ - 不为 FFI 增加普通 trait 的 structural satisfaction、自动 impl 或特殊 where-bound;
154
+ - 不改变普通 trait 的 method candidate、requires、impl-traits 与 generic unification;
155
+ - external-object 分支只在 trait definition 显式具有
156
+ `:ffi {:backend :js :kind :external-object}` 时启用;
157
+ - `:ffi` 始终是 lowering metadata,不参与 schema equality、subtyping 或 trait
158
+ satisfaction;
159
+ - 未带 `:ffi` 的程序在推断候选、诊断和 codegen 上保持原行为。
160
+
161
+ external-object trait 复用 trait 的 member type 描述,但不自动获得普通 runtime impl,
162
+ 也不能因为字段或方法同名而结构化满足另一个 trait。FFI 检查应是核心类型检查之后的独立
163
+ capability/target validation;它可以拒绝一个已经类型正确但未授权或 target 不兼容的
164
+ 程序,却不能制造新的类型匹配结果。
165
+
166
+ ## `js-ffi` 仓库的类型层次
167
+
168
+ 建议将定义分为三层。
169
+
170
+ ### 第一层:raw adapters
171
+
172
+ raw adapter 是唯一允许出现原始 JS syntax、`unsafe-coerce` 和 host import 的层。每个
173
+ 函数必须有完整 Fn schema,并标记:
174
+
175
+ ```cirru.no-check
176
+ :features $ #{} :js-ffi
177
+ ```
178
+
179
+ raw adapter 可以返回 `JsObject`、`JsNullish<Trait>` 或 external trait,但不得使用
180
+ `Dynamic` 隐藏已知关系。
181
+
182
+ ### 第二层:typed host capabilities
183
+
184
+ 以小型 external trait 表示宿主对象。示意:
185
+
186
+ ```cirru
187
+ deftrait StorageHost
188
+ .get-item $ :: 'Fn
189
+ {}
190
+ :args $ [] 'js-ffi.browser/StorageHost 'String
191
+ :return $ :: 'JsNullish 'String
192
+ .set-item! $ :: 'Fn
193
+ {}
194
+ :args $ [] 'js-ffi.browser/StorageHost 'String 'String
195
+ :return 'Unit
196
+ .remove-item! $ :: 'Fn
197
+ {}
198
+ :args $ [] 'js-ffi.browser/StorageHost 'String
199
+ :return 'Unit
200
+ ```
201
+
202
+ trait 的 `CodeEntry :ffi` 声明 `:backend :js`、`:kind :external-object` 和必要的
203
+ `:names`。业务代码不直接接收裸 `JsObject`。
204
+
205
+ ### 第三层:normalized Calcit API
206
+
207
+ 公共 API 优先返回 Calcit-owned data:
208
+
209
+ ```cirru
210
+ defenum Runtime (:browser) (:node)
211
+
212
+ defstruct JsError
213
+ :name 'String
214
+ :message 'String
215
+ :stack $ :: 'Option 'String
216
+
217
+ defstruct BrowserProbe
218
+ :runtime 'js-ffi.types/Runtime
219
+ :document? 'Bool
220
+ :storage $ :: 'Result 'String 'js-ffi.types/JsError
221
+
222
+ defstruct NodeProbe
223
+ :runtime 'js-ffi.types/Runtime
224
+ :cwd 'String
225
+ :argv-count 'Number
226
+ ```
227
+
228
+ 当前 probe 使用 `Map<Tag, Dynamic>`,应迁移成命名 Struct。Storage mutation、console、
229
+ timer registration 等只有副作用的 wrapper 应显式以 `nil` 收尾并声明返回 `Unit`,不因
230
+ JavaScript 返回 `undefined` 就暴露 `Dynamic`。
231
+
232
+ ## 类型表达案例目录
233
+
234
+ 本节把常见 JavaScript API 分成三种视图:
235
+
236
+ | 层次 | 目的 | 可以出现的类型 |
237
+ | --- | --- | --- |
238
+ | raw boundary | 如实表示宿主不确定性 | `JsObject`、`JsNullish<T>`、external trait |
239
+ | typed host API | 保留宿主 identity,同时提供成员类型 | external trait、`Option<Trait>`、typed `Fn` |
240
+ | normalized API | 供普通 Calcit 业务代码使用 | Struct、Enum、List、Map、Option、Result、Unit |
241
+
242
+ 不是每个 API 都必须同时提供三层。默认优先提供 normalized API;只有调用者确实需要继续
243
+ 操作宿主对象时,才公开 typed host API。
244
+
245
+ ### 案例 1:确定的 primitive 返回值
246
+
247
+ 例如 `window.innerWidth` 的宿主契约明确是 number。raw adapter 在一个位置完成断言:
248
+
249
+ ```cirru.no-check
250
+ defn viewport-width ()
251
+ unsafe-coerce js/window.innerWidth Number
252
+ ```
253
+
254
+ 对应 schema:
255
+
256
+ ```cirru.no-check
257
+ :: 'Fn $ {}
258
+ :args $ []
259
+ :return 'Number
260
+ :features $ #{} :js-ffi
261
+ ```
262
+
263
+ 公共函数可以继续返回 `Number`。调用者不需要 `:js-ffi`,因为 raw property access 已封装
264
+ 在 adapter 内。相同规则适用于可靠的 `Bool`、`String` 和 `Buffer` 返回值。
265
+
266
+ 如果宿主值可能为 `null`/`undefined`,不能直接断言 primitive,应先使用
267
+ `JsNullish<Primitive>`,再转换为 Option 或 Result。
268
+
269
+ ### 案例 2:JavaScript nullish 与业务缺失
270
+
271
+ `document.querySelector` 的原始结果是宿主 nullish:
272
+
273
+ ```cirru.no-check
274
+ :: 'Fn $ {}
275
+ :args $ [] 'String
276
+ :return $ :: 'JsNullish 'js-ffi.browser/DomElement
277
+ :features $ #{} :js-ffi
278
+ ```
279
+
280
+ 低层 typed host wrapper 可以转换为:
281
+
282
+ ```cirru.no-check
283
+ :: 'Fn $ {}
284
+ :args $ [] 'String
285
+ :return $ :: 'Option 'js-ffi.browser/DomElement
286
+ :features $ #{} :js-ffi
287
+ ```
288
+
289
+ 这里 Option 中仍然装着宿主对象。调用者若继续读取 external field 或调用 external method,
290
+ 该访问所在函数仍需要 `:js-ffi`。
291
+
292
+ 完全 normalized 的版本应复制所需数据:
293
+
294
+ ```cirru
295
+ defstruct ElementSnapshot
296
+ :id 'String
297
+ :text 'String
298
+ :visible? 'Bool
299
+ ```
300
+
301
+ ```cirru.no-check
302
+ :: 'Fn $ {}
303
+ :args $ [] 'String
304
+ :return $ :: 'Option 'js-ffi.browser/ElementSnapshot
305
+ :features $ #{} :js-ffi
306
+ ```
307
+
308
+ 业务层拿到 `Option<ElementSnapshot>` 后只处理 Calcit 数据,不再依赖宿主 identity。
309
+
310
+ ### 案例 3:错误、异常与 Result
311
+
312
+ 不要把可能抛异常的 API 仅表示为返回值。例如 localStorage 在隐私模式或 quota 满时可能
313
+ 抛出异常。先定义 normalized error:
314
+
315
+ ```cirru
316
+ defenum JsErrorKind
317
+ :exception
318
+ :type-error
319
+ :range-error
320
+ :permission
321
+ :quota
322
+ :unknown
323
+
324
+ defstruct JsError
325
+ :kind 'js-ffi.types/JsErrorKind
326
+ :name 'String
327
+ :message 'String
328
+ :stack $ :: 'Option 'String
329
+ ```
330
+
331
+ Storage read 的公共类型应为:
332
+
333
+ ```cirru.no-check
334
+ :: 'Fn $ {}
335
+ :args $ [] 'String
336
+ :return $ :: 'Result
337
+ :: 'Option 'String
338
+ , 'js-ffi.types/JsError
339
+ :features $ #{} :js-ffi
340
+ ```
341
+
342
+ 三层语义分别为:
343
+
344
+ - key 不存在:`Result.ok Option.none`;
345
+ - key 存在:`Result.ok (Option.some value)`;
346
+ - host exception:`Result.err JsError`。
347
+
348
+ 不能用空字符串同时表示“不存在”和“读取失败”。
349
+
350
+ ### 案例 4:只有副作用的 API
351
+
352
+ `console.log`、`localStorage.setItem`、`removeItem`、`focus` 和 `process.exit` 不应因为
353
+ JavaScript 返回 `undefined` 而声明为 `Dynamic`。wrapper 应显式以 `nil` 收尾并返回
354
+ `Unit`:
355
+
356
+ ```cirru.no-check
357
+ defn console-log! (message)
358
+ js/console.log message
359
+ , nil
360
+ ```
361
+
362
+ ```cirru.no-check
363
+ :: 'Fn $ {}
364
+ :args $ [] 'String
365
+ :return 'Unit
366
+ :features $ #{} :js-ffi
367
+ ```
368
+
369
+ 如果 effect 可能失败,则使用 `Result<Unit, JsError>`。
370
+
371
+ ### 案例 5:external field 与 method
372
+
373
+ DOM input 不建模为 Struct,因为对象仍由浏览器拥有。使用 capability trait:
374
+
375
+ ```cirru.no-check
376
+ deftrait DomInput
377
+ :value 'String
378
+ :checked 'Bool
379
+ :disabled 'Bool
380
+ .focus! $ :: 'Fn
381
+ {}
382
+ :args $ [] 'js-ffi.browser/DomInput
383
+ :return 'Unit
384
+ ```
385
+
386
+ 对应 lowering metadata:
387
+
388
+ ```cirru.no-check
389
+ :ffi $ {}
390
+ :backend :js
391
+ :kind :external-object
392
+ :names $ {}
393
+ :focus! |focus
394
+ :writable $ #{} :value :checked :disabled
395
+ ```
396
+
397
+ 类型行为:
398
+
399
+ ```cirru.no-check
400
+ input :value
401
+ ; => String
402
+
403
+ input .focus!
404
+ ; => Unit
405
+
406
+ js-get input :value
407
+ ; => JsNullish<String>, because raw property semantics still admit absence
408
+
409
+ js-set input :value |next
410
+ ; => String or Unit according to the chosen set operation contract
411
+ ```
412
+
413
+ `:value` 的普通 typed tag access 使用可信 external trait 契约;`js-get` 保留 raw
414
+ JavaScript nullish 语义。两者不应混成同一个推断规则。
415
+
416
+ ### 案例 6:事件与 callback target
417
+
418
+ 直接公开宿主事件时,事件和 target 都使用 external trait:
419
+
420
+ ```cirru.no-check
421
+ deftrait ClickEvent
422
+ :target $ :: 'JsNullish 'js-ffi.browser/DomElement
423
+ :meta-key 'Bool
424
+ :ctrl-key 'Bool
425
+ :shift-key 'Bool
426
+ .prevent-default! $ :: 'Fn
427
+ {}
428
+ :args $ [] 'js-ffi.browser/ClickEvent
429
+ :return 'Unit
430
+ ```
431
+
432
+ 低层 listener schema:
433
+
434
+ ```cirru.no-check
435
+ :: 'Fn $ {}
436
+ :args $ []
437
+ 'js-ffi.browser/DomElement
438
+ :: 'Fn $ {}
439
+ :args $ [] 'js-ffi.browser/ClickEvent
440
+ :return 'Unit
441
+ :features $ #{} :js-ffi
442
+ :return 'Unit
443
+ :features $ #{} :js-ffi
444
+ ```
445
+
446
+ 如果业务 callback 不需要宿主操作,应先转换事件:
447
+
448
+ ```cirru
449
+ defstruct ClickInfo
450
+ :target-id $ :: 'Option 'String
451
+ :meta? 'Bool
452
+ :ctrl? 'Bool
453
+ :shift? 'Bool
454
+ ```
455
+
456
+ 然后暴露纯 callback:
457
+
458
+ ```cirru.no-check
459
+ :: 'Fn $ {}
460
+ :args $ []
461
+ 'js-ffi.browser/DomElement
462
+ :: 'Fn $ {}
463
+ :args $ [] 'js-ffi.browser/ClickInfo
464
+ :return 'Unit
465
+ :return 'Unit
466
+ :features $ #{} :js-ffi
467
+ ```
468
+
469
+ event name 与 event type 的关联不使用 dependent string type。分别提供 `on-click!`、
470
+ `on-input!`、`on-keydown!` 等 wrapper;共享配置使用 Enum。
471
+
472
+ 上述 callback schema 中的 `:features` 只用于检查 callback 自己的函数体。高阶函数接收
473
+ callback 时仍按现有 Fn 参数与返回值匹配,不根据 feature set 改变泛型绑定或 trait
474
+ method candidate。纯 callback 和包含宿主操作的 callback 可以共用同一个业务签名,后者
475
+ 只需在自身实现体通过 capability validation。
476
+
477
+ ### 案例 7:字符串常量、状态与 union
478
+
479
+ JavaScript API 常返回有限字符串,例如 document ready state。使用 Enum:
480
+
481
+ ```cirru
482
+ defenum DocumentReadyState
483
+ :loading
484
+ :interactive
485
+ :complete
486
+ :unknown 'String
487
+ ```
488
+
489
+ raw adapter 先得到 String,decoder 再返回
490
+ `Result<DocumentReadyState, JsError>`,或在未来兼容未知值时使用 `:unknown String`。
491
+
492
+ `"GET" | "POST" | "PUT"` 同样定义 `HttpMethod` Enum。进入 fetch adapter 时转换为 JS
493
+ string;不要给业务函数暴露任意 String 后再依赖运行时约定。
494
+
495
+ ### 案例 8:数组、iterable 与同质集合
496
+
497
+ 原始 JavaScript Array 首先是 `JsObject`。若 adapter 会遍历并验证元素,应返回:
498
+
499
+ ```cirru.no-check
500
+ :: 'Fn $ {}
501
+ :generics $ [] 'T
502
+ :args $ []
503
+ 'JsObject
504
+ :: 'Fn $ {}
505
+ :args $ [] 'JsObject
506
+ :return $ :: 'Result 'T 'js-ffi.types/JsError
507
+ :return $ :: 'Result
508
+ :: 'List 'T
509
+ , 'js-ffi.types/JsError
510
+ :features $ #{} :js-ffi
511
+ ```
512
+
513
+ 这保留了 decoder input/output 的类型关系。不要用裸 `List` 或 `List<Dynamic>`。
514
+
515
+ 需要 lazy/async host iteration 时,也不在本 RFC 中引入 `JsIterator<T>` 或
516
+ `JsAsyncIterator<T>`。adapter 应消费 iterable 后返回 `List<T>`,或者接受明确的 typed
517
+ callback,逐项推送已经验证的 item。若需要暂停、取消或错误状态,使用命名 Struct/Enum
518
+ 表达 adapter 协议,而不是扩展核心 trait。
519
+
520
+ ### 案例 9:JavaScript dictionary 与 Calcit Map
521
+
522
+ 动态 key 的 host object 不能因为“看起来像 map”就声明为 Calcit Map。
523
+
524
+ - 仍由宿主持有、通过 `aget` 访问:`JsObject`;
525
+ - 读取时可能缺失:`JsNullish<T>`;
526
+ - 已复制并验证所有 key/value:`Map<K, V>`;
527
+ - 固定字段集合:Struct;
528
+ - 有方法或 identity:external trait。
529
+
530
+ 例如 `process.env` 的公共读取 API:
531
+
532
+ ```cirru.no-check
533
+ :: 'Fn $ {}
534
+ :args $ [] 'String
535
+ :return $ :: 'Option 'String
536
+ :features $ #{} :js-ffi
537
+ ```
538
+
539
+ 只有需要快照所有环境变量时才返回 `Map<String, String>`;不要暴露
540
+ `Map<String, JsNullish<JsObject>>` 给普通业务层。
541
+
542
+ ### 案例 10:ES module function import
543
+
544
+ npm named export 在 import 点首先是 opaque host value。adapter 一次性断言完整 Fn
545
+ schema:
546
+
547
+ ```cirru.no-check
548
+ defn make-id (size)
549
+ let
550
+ generate $ unsafe-coerce nanoid $ :: 'Fn
551
+ {}
552
+ :args $ [] 'Number
553
+ :return 'String
554
+ generate size
555
+ ```
556
+
557
+ `make-id` 自身的 schema:
558
+
559
+ ```cirru.no-check
560
+ :: 'Fn $ {}
561
+ :args $ [] 'Number
562
+ :return 'String
563
+ :features $ #{} :js-ffi
564
+ ```
565
+
566
+ 业务层只看到 `Number -> String`。如果 module export 是带 receiver 的对象方法,应定义
567
+ external trait method,不能把 method 取出来后当作普通 function,以免丢失 JavaScript
568
+ `this`。
569
+
570
+ ### 案例 11:Promise 与 async
571
+
572
+ 当前可实现的推荐 API是在 adapter 内 await 并捕获 rejection:
573
+
574
+ ```cirru.no-check
575
+ :: 'Fn $ {}
576
+ :args $ [] 'String
577
+ :return $ :: 'Result 'String 'js-ffi.types/JsError
578
+ :features $ #{} :js-ffi :async
579
+ ```
580
+
581
+ 如果返回值在 JavaScript backend 上仍然是 Promise,`:async`/lowering metadata 负责调用
582
+ 约定;逻辑返回类型描述 await 后的 Calcit value,不再暴露无类型 `JsObject`。
583
+
584
+ 本 RFC 不公开未 await 的 Promise,也不引入 `JsPromise<T>`。需要组合 Promise 的 API
585
+ 应在 JS adapter 内完成组合,并把最终逻辑结果返回为 `T`、`Option<T>` 或
586
+ `Result<T, E>`。如果将来 Calcit 因非 FFI 场景获得通用 async/task abstraction,它可以
587
+ 由独立 RFC 统一承载;这里不预先复制 JavaScript Promise 类型。
588
+
589
+ ### 案例 12:Node process 与 filesystem
590
+
591
+ Node API 经过 adapter 后应使用普通 Calcit 类型:
592
+
593
+ | JavaScript API | raw boundary | 公共类型 |
594
+ | --- | --- | --- |
595
+ | `process.cwd()` | trusted host String | `String` |
596
+ | `process.argv` | host Array | `List<String>` |
597
+ | `process.env[key]` | nullish host String | `Option<String>` |
598
+ | `fs.existsSync(path)` | trusted Bool | `Bool` |
599
+ | `fs.readFile` | callback/Promise + Buffer | `Result<Buffer, FsError>` |
600
+ | `process.exit(code)` | non-returning effect | `Unit`,未来可增加 `Never` |
601
+
602
+ Filesystem error 使用 Enum + Struct,而不是 String:
603
+
604
+ ```cirru
605
+ defenum FsErrorKind
606
+ :not-found
607
+ :permission
608
+ :already-exists
609
+ :invalid-path
610
+ :io
611
+ :unknown 'String
612
+
613
+ defstruct FsError
614
+ :kind 'js-ffi.node/FsErrorKind
615
+ :message 'String
616
+ :path $ :: 'Option 'String
617
+ :code $ :: 'Option 'String
618
+ ```
619
+
620
+ ### 案例 13:constructor 与有 identity 的实例
621
+
622
+ `new js/Date` 的结果不是 Calcit Struct。低层使用 external trait:
623
+
624
+ ```cirru
625
+ deftrait DateHost
626
+ .timestamp $ :: 'Fn
627
+ {}
628
+ :args $ [] 'js-ffi.shared/DateHost
629
+ :return 'Number
630
+ .to-iso-string $ :: 'Fn
631
+ {}
632
+ :args $ [] 'js-ffi.shared/DateHost
633
+ :return 'String
634
+ ```
635
+
636
+ 若业务只需要不可变数据,转换为:
637
+
638
+ ```cirru
639
+ defstruct DateSnapshot
640
+ :timestamp 'Number
641
+ :iso 'String
642
+ ```
643
+
644
+ 同样适用于 URL、Request、Response、AbortController 和 Node class instances。
645
+
646
+ ### 案例 14:timer handle 的 target 差异
647
+
648
+ 浏览器 `setTimeout` 常返回 number,Node 返回 object。不能为了共享 API 把二者都声明为
649
+ `Number` 或 `Dynamic`。
650
+
651
+ 建议低层分开:
652
+
653
+ ```cirru.no-check
654
+ js-ffi.browser/BrowserTimerId
655
+ js-ffi.node/NodeTimerHandle
656
+ ```
657
+
658
+ browser 与 Node namespace 各自提供匹配的 `set-timeout!`/`clear-timeout!`。共享代码若不
659
+ 需要取消 timer,就不返回 handle;若必须共享,使用 entry type slot 或未来 nominal opaque
660
+ newtype 绑定具体 handle,而不是暴露 backend representation。
661
+
662
+ ### 案例 15:重载与配置对象
663
+
664
+ 对于 JavaScript overload:
665
+
666
+ ```text
667
+ fetch(url)
668
+ fetch(url, options)
669
+ fetch(request)
670
+ ```
671
+
672
+ Calcit adapter 提供多个明确函数:
673
+
674
+ ```cirru.no-check
675
+ fetch-text url
676
+ fetch-text-with url options
677
+ fetch-request request
678
+ ```
679
+
680
+ 配置使用 Struct,有限选项使用 Enum:
681
+
682
+ ```cirru
683
+ defstruct FetchOptions
684
+ :method 'js-ffi.http/HttpMethod
685
+ :headers $ :: 'Map 'String 'String
686
+ :body $ :: 'Option 'String
687
+ :timeout-ms $ :: 'Option 'Number
688
+ ```
689
+
690
+ adapter 负责把 FetchOptions 转成 JS object。不要直接把开放 `JsObject` options 传播到
691
+ 业务层,也不需要在类型系统中实现 JavaScript overload resolution。
692
+
693
+ ### 案例 16:动态 escape hatch
694
+
695
+ 确实无法静态描述的 plugin/global object 可以保留:
696
+
697
+ ```cirru.no-check
698
+ :: 'Fn $ {}
699
+ :args $ [] 'String
700
+ :return $ :: 'JsNullish 'JsObject
701
+ :features $ #{} :js-ffi
702
+ ```
703
+
704
+ 约束是:
705
+
706
+ - 只存在于 adapter namespace;
707
+ - 每次离开 adapter 前尽量 decode;
708
+ - literal key access 在 `--warn-dyn-method` 下可见;
709
+ - 不把 `Dynamic` 当作 `JsObject` 的别名;
710
+ - 不允许 raw host value 静默满足 Struct、Enum 或 external trait。
711
+
712
+ ### 类型选择速查
713
+
714
+ | 遇到的宿主值 | 首选表达 |
715
+ | --- | --- |
716
+ | 明确 primitive | `String` / `Number` / `Bool` / `Buffer` |
717
+ | 可能 null/undefined 的宿主值 | `JsNullish<T>` |
718
+ | 已转换的业务缺失 | `Option<T>` |
719
+ | 可能异常/reject/decode 失败 | `Result<T, E>` |
720
+ | 固定数据字段 | Struct |
721
+ | 有限状态或分支 | Enum |
722
+ | 有 identity、字段和方法的宿主对象 | external-object trait |
723
+ | 同质 JS Array 已完成验证 | `List<T>` |
724
+ | 动态 dictionary 已完成复制验证 | `Map<K, V>` |
725
+ | 未知宿主对象 | `JsObject` |
726
+ | 回调 | 完整 `Fn` 参数与返回值;实现体 capability 另行检查 |
727
+ | Promise/Iterator/Event 类型关系 | 由 adapter 消费,转换为现有 Fn/List/Struct/Enum/Result |
728
+ | 纯副作用 | `Unit` |
729
+ | 跨 target handle | target-specific external trait 或 type slot |
730
+
731
+ ## 编译器调整
732
+
733
+ ### 统一 host operation 分类
734
+
735
+ 预处理阶段增加统一分类,例如:
736
+
737
+ ```text
738
+ HostOperation
739
+ JsGlobal
740
+ JsPropertyRead
741
+ JsPropertyWrite
742
+ JsMethodCall
743
+ JsConstruct
744
+ JsAwait
745
+ JsImport
746
+ ExternalFieldRead
747
+ ExternalFieldWrite
748
+ ExternalMethodCall
749
+ UnsafeHostAssertion
750
+ ```
751
+
752
+ 所有会 lowering 成 JavaScript host operation 的节点都调用同一个检查:
753
+
754
+ ```text
755
+ require_feature(:js-ffi, operation, current_fn)
756
+ ```
757
+
758
+ 建议实现顺序固定为:
759
+
760
+ ```text
761
+ parse/preprocess
762
+ -> existing schema inference and trait resolution
763
+ -> collect explicitly marked host operations
764
+ -> capability and target validation
765
+ -> backend lowering/codegen
766
+ ```
767
+
768
+ `collect` 只能从 raw JS syntax 或显式 external-object `:ffi` metadata 产生 host
769
+ operation。它读取已经完成的类型结果,但不得回写 type binding、增加 trait candidate 或
770
+ 让失败的普通类型匹配变为成功。这样新增 validation 可以独立开启、记录 warning,并在
771
+ 成熟后升级为 error,而不改变 trait solver 的收敛行为。
772
+
773
+ 第一阶段产生稳定 warning;strict mode 与下一次 breaking release 中升级为 error。
774
+
775
+ 建议诊断码:
776
+
777
+ - `E_JS_FFI_FEATURE_REQUIRED`:raw/typed host operation 位于未授权函数体;
778
+ - `E_JS_FFI_TARGET_MISMATCH`:Node binding 用于 browser target,或反之;
779
+ - `E_JS_FFI_UNSAFE_ASSERTION`:host assertion 不在受信任 adapter;
780
+ - `E_JS_FFI_FIELD_READONLY`:写入未声明 writable 的 external field;
781
+
782
+ ### lexical function scope
783
+
784
+ 门禁以实际执行 host operation 的函数体为边界:
785
+
786
+ - 普通函数调用一个已经封装好的 typed FFI wrapper,不需要标记 `:js-ffi`;
787
+ - wrapper 自己必须标记;
788
+ - 匿名函数若包含 raw JS,也必须具有带 `:js-ffi` 的 `hint-fn` schema;
789
+ - 外层函数有 feature 不应自动授权一个可逃逸的匿名 closure;
790
+ - 宏不能把 raw JS 静默注入到无 feature 的调用者;检查应基于展开后的归属位置。
791
+
792
+ 这样 `:js-ffi` 表示“这个实现体包含宿主操作”,而不是把能力沿普通调用链传播到整个
793
+ 应用。
794
+
795
+ ### 独立保存并验证函数 features
796
+
797
+ 所有从 schema、函数实现体、import 或 callback hint 建立 capability context 的路径都应
798
+ 保留 features,query/analyze 也应能报告它们。但 MVP 不把 feature set 加入
799
+ `CalcitTypeAnnotation::matches_with_bindings`、`matches_signature`、trait method selection
800
+ 或 generic unification;也不要求因 FFI 修改核心类型的 `Eq`、`Ord` 和 `Hash` 语义。
801
+
802
+ 编译器在普通类型检查完成后运行独立的 capability validation:遍历已经归属到 lexical
803
+ function body 的 `HostOperation`,检查该 body 的 feature set。这样 feature 丢失不会被
804
+ 类型匹配“补救”,而是由保存实现体归属和 capability metadata 的专用路径解决。
805
+
806
+ callback 若自身包含 raw JS,它自己的实现体必须有 `:js-ffi`。把 callback 传入高阶函数
807
+ 时,MVP 只按现有 Fn 参数与返回值规则进行类型匹配;不把 callback feature compatibility
808
+ 加入 Fn subtype/unification。未来若确实需要限制 callback 可执行的 capability,应作为
809
+ 类型检查后的单独 capability contract 提案,且不能改变 trait candidate 或泛型绑定结果。
810
+
811
+ 普通 direct call 不要求 caller 拥有 callee 的 features;raw operation 只发生在 callee
812
+ 已标记的实现体内。若未来需要表达传递副作用,应使用独立的 `:effects`,不要把
813
+ `:features` 同时解释成 effect row。
814
+
815
+ ### strict mode 与 target policy
816
+
817
+ 建议 entry 增加渐进配置:
818
+
819
+ ```cirru.no-check
820
+ :feature-policy $ {}
821
+ :js-ffi :error
822
+ ```
823
+
824
+ 策略值可以是 `:allow`、`:warn`、`:error`。`js-ffi` 仓库自身和新项目直接使用
825
+ `:error`;旧项目可先使用 `:warn`。
826
+
827
+ `:ffi :backend` 或 definition target metadata 用于检查 browser/Node/native/WASM
828
+ availability。Backend 信息不参与普通 schema type matching,但调用不可用 binding 时必须
829
+ 在 codegen 前失败。
830
+
831
+ 当前实现把 entry target 独立保存,避免把 `:mode :js` 误当成 Node 或 browser:
832
+
833
+ ```cirru.no-check
834
+ :browser $ {} (:mode :js) (:target :browser)
835
+ :node $ {} (:mode :native) (:target :node)
836
+ ```
837
+
838
+ definition 的 `:ffi` metadata 可以声明 `(:target :browser)` 或
839
+ `(:target :node)`。typed external-object operation 和带该 metadata 的 raw host
840
+ wrapper 会在 codegen 前检查 selected entry;缺少 entry target 的旧项目保持兼容,暂不做
841
+ target-specific validation。`cr query def ns/name --json` 同时暴露 `ffi` metadata,包含
842
+ host name mapping,供 adapter 审计使用。
843
+
844
+ ## external trait 的有限补充
845
+
846
+ ### 不由 FFI 推动参数化 trait
847
+
848
+ 本 RFC 不修改 `CalcitTrait` 的表示,不增加 applied-trait annotation,也不改变 `:where`、
849
+ TraitSet、member lookup、method selection 或 codegen hint 的泛型逻辑。`Promise<T>`、
850
+ `Iterator<T>`、`Event<Target>` 与 `MapLike<K, V>` 由 adapter 消费并规范化。
851
+
852
+ 这是刻意的边界,而不是临时缺失:generic trait 或 associated type 会影响所有 Calcit
853
+ 代码的 trait identity、impl coherence、method candidate 和泛型求解,不能作为 JS FFI
854
+ 的附属功能引入。若以后独立 RFC 证明这类能力对普通 Calcit abstraction 同样必要,FFI
855
+ 库可以随后复用,但本 RFC 的 gate、target validation 和 external lowering 不依赖它。
856
+
857
+ ### writable fields
858
+
859
+ external trait field 默认 read-only。确实需要直接 mutation 时,在 `:ffi` 中声明:
860
+
861
+ ```cirru.no-check
862
+ :ffi $ {}
863
+ :backend :js
864
+ :kind :external-object
865
+ :writable $ #{} :value :checked
866
+ ```
867
+
868
+ `js-set` 或 typed set lowering 必须同时满足:
869
+
870
+ 1. 字段存在;
871
+ 2. 字段列在 `:writable`;
872
+ 3. value type 匹配字段类型;
873
+ 4. 当前函数有 `:js-ffi`。
874
+
875
+ 如果 API 可以通过方法表达 mutation,优先定义 `.set-value!` wrapper,而不是开放字段写入。
876
+ `:writable` 只由 external-object lowering 和 capability validation 读取。普通 trait inference
877
+ 仍然只看到 field type;不得为它增加 mutable field type、setter trait、variance 或新的
878
+ trait satisfaction 规则。
879
+
880
+ ## 暂不扩展的类型
881
+
882
+ 以下能力暂不增加专用 Calcit type:
883
+
884
+ - `JsArray<T>`:优先在边界转换成 `List<T>`;需要保留 identity 时使用非泛型的小型
885
+ external trait,并通过 typed adapter 读取元素;
886
+ - `JsPromise<T>`:在 adapter 中 await 后返回 `T` 或 `Result<T, E>`;
887
+ - `JsFunction`:使用完整 `Fn` schema;未知签名才使用裸 `Fn`;
888
+ - `null` 与 `undefined` 的类型级区分:默认继续使用 `JsNullish<T>`,只有确有业务语义时
889
+ 通过显式 predicate 转换为 Calcit Enum;
890
+ - JavaScript number 的整数/浮点细分:除非 Calcit 自身增加 numeric type,否则由
891
+ decoder 做范围检查;
892
+ - TypeScript literal/union:使用 Enum 与 wrapper conversion。
893
+
894
+ ## `unsafe-coerce` 与验证
895
+
896
+ `unsafe-coerce` 只提供 trusted evidence,不提供 runtime validation。规则如下:
897
+
898
+ - 从 `JsObject`/`JsNullish<_>` 到 primitive、Struct、Enum 或 external trait 的 coercion
899
+ 只能出现在 `:js-ffi` adapter;
900
+ - coercion 到 Struct/Enum 仅用于已经由 decoder 验证过的值;否则 wrapper 应返回
901
+ `Result<T, JsError>`;
902
+ - query/analyze 应能报告 host coercion 的 source type、target type 和 location;
903
+ - 普通业务 namespace 不直接出现 host coercion。
904
+
905
+ 未来如果增加 `:unsafe` feature,应要求 host coercion 同时具有 `:js-ffi` 与 `:unsafe`;
906
+ 在此之前先由 `:js-ffi` 统一管理。
907
+
908
+ ## 迁移方案
909
+
910
+ ### Phase 0:仅使用现有类型整理 `js-ffi` 库
911
+
912
+ - 新增 `js-ffi.types`;
913
+ - 将 probe Map 改为 Struct;
914
+ - effect wrapper 从 `Dynamic` 改为 `Unit`;
915
+ - nullable host result 用 `JsNullish<T>`,对外转换为 `Option<T>`;
916
+ - exception/rejection 转为 `Result<T, JsError>`;
917
+ - 为 Storage、Document、Location、Process 等稳定能力声明非泛型 external trait;
918
+ - 开启 `--warn-dyn-method`,消除可建模的裸字段访问。
919
+
920
+ 本阶段不修改核心 type annotation、trait identity、trait matching 或泛型推断。
921
+
922
+ ### Phase 1:完整 capability gate
923
+
924
+ - 引入统一 `HostOperation` 分类;
925
+ - 覆盖裸 `js/*`、native member access、construct/await 和 external trait lowering;
926
+ - 增加 `:feature-policy`;
927
+ - 先 warning,再在 `js-ffi` 自身切到 error。
928
+
929
+ HostOperation 分类与 `:js-ffi` 检查是独立 validation,不进入
930
+ `matches_with_bindings`、trait candidate 或 generic unification。
931
+
932
+ ### Phase 2:capability metadata 与 target 检查
933
+
934
+ - runtime function 与 anonymous callback 的实现体归属保留 features;
935
+ - query/analyze 报告 feature、host operation 与 target;
936
+ - browser/Node/native/WASM binding 在 codegen 前进行 availability validation;
937
+ - 如需 callback capability contract,单独设计 post-type-check 规则;
938
+ - effects graph 单独记录传递 effects,不改变 `:features` 的 body capability 语义。
939
+
940
+ ### Phase 3:有限 lowering 完善
941
+
942
+ - writable field metadata;
943
+ - external member name 与 target availability 的查询输出;
944
+ - 保持 external-object 分支由显式 `:ffi` 元数据触发。
945
+
946
+ generic trait、associated type、effect row 和 FFI 专用 type constructor 都不属于本 RFC
947
+ 的迁移阶段。
948
+
949
+ ## 验收标准
950
+
951
+ 1. 未标记函数中的任何 raw `js/*`、`.!`、`.-`、construct、await 或 external lowering
952
+ 都产生 `E_JS_FFI_FEATURE_REQUIRED`。
953
+ 2. `js-ffi` 公共 wrapper 不向业务代码暴露无理由的 `Dynamic`。
954
+ 3. probe、错误和状态结果使用命名 Struct/Enum。
955
+ 4. `JsNullish<T>` 不会静默匹配 `Option<T>`,存在性检查后仍保留 payload type。
956
+ 5. external field/method 的类型和 host name 可由 `cr query` 查询。
957
+ 6. 未声明 writable 的 external field 无法被 typed write。
958
+ 7. 带 `:js-ffi` 的匿名 callback 能保留实现体归属,capability validation 能检查其 raw
959
+ operation;Fn 类型匹配结果不因 feature 改变。
960
+ 8. browser binding 在 Node target、Node binding 在 browser target 时于 codegen 前失败。
961
+ 9. 所有 RFC 示例可由 Markdown/Cirru 检查工具解析,Node 与浏览器 smoke test 继续通过。
962
+ 10. 没有 `:ffi` metadata 的普通 trait 不进入 external-object lowering。
963
+ 11. 增加 FFI metadata 不改变普通 trait satisfaction、method candidate 或泛型绑定结果。
964
+ 12. 不使用 FFI 的现有 fixtures 在类型推断、诊断与 codegen 上保持行为一致。
965
+
966
+ ## 决策
967
+
968
+ 本 RFC 建议立即接受:
969
+
970
+ - 继续以 Struct/Enum/trait/Fn schema 为主,不新增 TypeScript 式结构类型;
971
+ - raw JS 统一受 `:js-ffi` gate 管控;
972
+ - external field 默认只读;
973
+ - FFI wrapper 对外优先返回 normalized Calcit data;
974
+ - `:features` 与未来 `:effects` 分离;
975
+ - capability/target validation 与核心类型匹配分离;
976
+ - generic trait、associated type 与 FFI 专用核心类型不属于本 RFC。
977
+
978
+ 建议先验证真实用例再接受:
979
+
980
+ - target-specific opaque wrapper 或 type slot;
981
+ - `:unsafe` 独立 capability;
982
+ - callback capability contract。
983
+
984
+ generic trait 与 associated type 若要重新讨论,必须进入独立、面向 Calcit 通用类型系统
985
+ 的 RFC,并提供非 FFI 用例与 trait inference 回归证明;不作为本 RFC 的后续 phase。
986
+
987
+ ## 相关资料
988
+
989
+ - Calcit `RFCs/07-08-ffi-features-and-js-object-type-rfc.md`
990
+ - Calcit `RFCs/08-08-cross-backend-host-ffi-contracts-rfc.md`
991
+ - Calcit `RFCs/07-31-unsafe-coerce-driven-static-type-boundary-plan.md`
992
+ - Calcit `docs/features/js-interop.md`
993
+ - `js-ffi` README 与 `calcit.cirru`
package/RFCs/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # RFC 整理索引
2
2
 
3
- 更新时间:2026-08-14
3
+ 更新时间:2026-08-18
4
4
 
5
5
  ## 目录原则
6
6
 
@@ -44,6 +44,7 @@
44
44
  | `08-04-strict-cirru-edn-decoding-rfc.md` | Implemented | Phase 1:`parse-cirru-edn-as` 严格类型化反序列化、无 Dynamic 的 `EdnDecoderGraph`、名义身份与 Native/JS 一致性。 |
45
45
  | `08-05-systematic-nil-reduction-rfc.md` | Partial | 类型驱动减少 nil:先拆分可省略参数与 nullable 值,再迁移至 Option/Result 并逐步收紧 typed code。 |
46
46
  | `08-08-cross-backend-host-ffi-contracts-rfc.md` | Draft | 统一 JS/native/WASM/WASI 的逻辑 FFI 契约与诊断,ABI transport 保持 backend-specific;首个完整 shape consumer 为 JS/DOM。 |
47
+ | `08-18-calcit-typed-js-ffi-boundary-rfc.md` | Draft | 在现有 Struct/Enum/Fn/trait 上补齐 JS capability gate 与 target validation;FFI metadata 不进入普通 trait 匹配和泛型推断。 |
47
48
 
48
49
  ## 已执行的清理
49
50
 
package/build.rs CHANGED
@@ -57,6 +57,10 @@ pub struct SnapshotEntry {
57
57
  pub modules: Vec<String>,
58
58
  #[serde(default, rename = "type-slots")]
59
59
  pub type_slots: HashMap<String, String>,
60
+ #[serde(default, rename = "feature-policy")]
61
+ pub feature_policy: HashMap<String, String>,
62
+ #[serde(default)]
63
+ pub target: Option<String>,
60
64
  }
61
65
 
62
66
  #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
@@ -139,9 +143,27 @@ fn parse_snapshot_entry(data: Edn) -> Result<SnapshotEntry, String> {
139
143
  description: from_edn(map.get_or_nil("description")).map_err(|e| format!("entry `:description`: {e}"))?,
140
144
  modules: from_edn(map.get_or_nil("modules")).map_err(|e| format!("entry `:modules`: {e}"))?,
141
145
  type_slots: from_edn(map.get_or_nil("type-slots")).map_err(|e| format!("entry `:type-slots`: {e}"))?,
146
+ feature_policy: match map.get_or_nil("feature-policy") {
147
+ Edn::Nil => HashMap::new(),
148
+ value => parse_feature_policy(value)?,
149
+ },
150
+ target: match map.get_or_nil("target") {
151
+ Edn::Nil => None,
152
+ value => Some(from_edn(value).map_err(|e| format!("entry `:target`: {e}"))?),
153
+ },
142
154
  })
143
155
  }
144
156
 
157
+ fn parse_feature_policy(data: Edn) -> Result<HashMap<String, String>, String> {
158
+ let map = data.view_map().map_err(|e| format!("entry `:feature-policy` must be a map: {e}"))?;
159
+ let text = |value: &Edn| match value {
160
+ Edn::Tag(tag) => Ok(tag.ref_str().to_owned()),
161
+ Edn::Str(value) | Edn::Symbol(value) => Ok(value.to_string().trim_start_matches(':').to_owned()),
162
+ other => Err(format!("entry `:feature-policy` expects tag/string/symbol values, got {other:?}")),
163
+ };
164
+ map.0.iter().map(|(key, value)| Ok((text(key)?, text(value)?))).collect()
165
+ }
166
+
145
167
  fn parse_entries(data: Edn) -> Result<HashMap<String, SnapshotEntry>, String> {
146
168
  let map = data.view_map().map_err(|e| format!("entries must be a map: {e}"))?;
147
169
  let mut entries = HashMap::with_capacity(map.0.len());
@@ -576,6 +598,8 @@ fn main() {
576
598
  description: String::new(),
577
599
  modules: configs.modules.clone(),
578
600
  type_slots: configs.type_slots.clone(),
601
+ feature_policy: HashMap::new(),
602
+ target: None,
579
603
  },
580
604
  );
581
605
  }
@@ -0,0 +1,6 @@
1
+ # Project module runtime resolution
2
+
3
+ - `caps` already stores immutable dependency revisions in `~/.config/calcit/modules/.store/` and builds the project's `.calcit/modules/` link view.
4
+ - Runtime loaders must consume only that project view. Retaining a global module fallback can silently run a dependency revision that was not selected by the project's dependency graph.
5
+ - Centralize the project module directory calculation in `calcit::project_module_folder`, then use it for normal execution, WASM codegen, queries, configuration inspection, markdown checks, call-graph comparison, and the Cirru integration harness.
6
+ - Keep explicitly relative and absolute module paths working; only package-style module paths are constrained to the project view.
@@ -0,0 +1,6 @@
1
+ # Visible module versions and cache cleanup
2
+
3
+ - Store immutable module revisions under `~/.config/calcit/modules/versions/` instead of a hidden `.store/` directory, retaining the owner, repository, and resolved commit in the path.
4
+ - Write `metadata.txt` beside each cached revision so the requested reference remains inspectable and global cleanup can select the highest SemVer release without a network request. When one commit is observed through multiple refs, retain the highest observed SemVer ref for cleanup ordering.
5
+ - Add `caps clean` as an explicit global cleanup command. It preserves one newest revision per module (SemVer takes precedence; materialization time breaks non-SemVer ties) and removes older revision directories.
6
+ - Rewrite `~/.config/calcit/modules/AGENTS.md` on each `caps` invocation. The guide treats `versions/` as immutable cache data and directs dependency changes through a repository commit, new tag, dependency update, and reinstall.
@@ -0,0 +1,5 @@
1
+ # Module caches root
2
+
3
+ - Move the global immutable cache from the nested `modules/versions/` proposal to the clearer sibling path `~/.config/calcit/module-caches/`.
4
+ - Keep `~/.config/calcit/modules/` reserved for the legacy module root and per-project link vocabulary; `module-caches/AGENTS.md` is the authoritative generated guidance for cached revisions.
5
+ - Derive the cache root as a sibling of `CALCIT_MODULES_DIR`, so custom test and CI roots preserve the same `modules/` plus `module-caches/` layout.
@@ -0,0 +1,5 @@
1
+ # Module cache review follow-up
2
+
3
+ - Scope `cr docs` module documentation and scope discovery to the current project's `.calcit/modules/` directory, removing the final global fallback.
4
+ - Register each successfully materialized project module view in `module-caches/projects/`; global cleanup preserves every cached revision that remains linked by one of those views.
5
+ - When a cached commit is later resolved through a SemVer tag, refresh its metadata with the highest observed SemVer ref so cleanup ordering stays correct.
@@ -0,0 +1,4 @@
1
+ # Release 0.13.19
2
+
3
+ - Publish the project-scoped module resolution, visible `module-caches` store, safe cleanup, and documentation fixes from PR #364.
4
+ - Keep Cargo and npm package versions synchronized at `0.13.19` for the direct PR-branch release.
@@ -0,0 +1,4 @@
1
+ # Module path resolution documentation
2
+
3
+ - Clarify that project-local `.calcit/modules/` links are mandatory only for package-style module paths.
4
+ - Preserve the documented behavior of explicit relative paths (resolved from the snapshot) and absolute paths (resolved directly).
@@ -0,0 +1,4 @@
1
+ # Update release dependencies
2
+
3
+ - Refresh `md-5` and `wasm-encoder` to the requested current compatible releases.
4
+ - Refresh the transitive `generic-array` lockfile entry where the resolver permits, then validate the compiler, code generators, and tests.
@@ -0,0 +1,5 @@
1
+ # Module cache review follow-up
2
+
3
+ - Added a shared inter-process metadata lock for cache revision metadata updates and cleanup scans, preventing concurrent `caps` processes from losing the highest observed SemVer reference.
4
+ - Made project-view installation transactional across module links and `caps-state.cirru`; registration failures now restore the previous view and state before returning an error.
5
+ - Added concurrency and rollback regression tests, and routed module documentation tests through project-root-aware resolver helpers so the production path is covered.
@@ -0,0 +1,5 @@
1
+ # Module cache locking follow-up
2
+
3
+ - Replaced removable lock files with OS-managed `fs4` locks, so ownership is released when the handle or process exits on every supported platform.
4
+ - Held the cache lock across revision installation and metadata publication, and added a project-scoped lock spanning module view activation, state replacement, registration, rollback, and cleanup.
5
+ - Isolated the documentation fallback regression test against the actual `$HOME/.config/calcit/modules` layout without leaking the test environment.
@@ -0,0 +1,4 @@
1
+ # 2026-08-17 14:38 — Serialize documentation HOME access
2
+
3
+ - Shared the documentation test HOME mutex between `TestHome` and tests that read the guidebook through the process environment.
4
+ - Guarded `collect_docs_for_query_uses_guidebook_without_module` so it cannot race with tests that temporarily replace `HOME`.
@@ -0,0 +1,5 @@
1
+ # Release 0.13.20
2
+
3
+ - Publish the typed JavaScript FFI capability boundary and browser/Node target validation from PR #367.
4
+ - Reorganize and expand the JavaScript FFI documentation, including adapter design, diagnostics, and release checks.
5
+ - Keep Cargo and npm package versions synchronized at `0.13.20` for the direct PR-branch release.
@@ -0,0 +1,21 @@
1
+ # Make dependency-boundary audits discoverable to agents
2
+
3
+ ## Knowledge points
4
+
5
+ - `:dependencies`, `:dev-dependencies`, configured entry modules, and statically reachable source paths
6
+ answer different questions. An installed module must not be classified as runtime-only from its presence
7
+ under `.calcit/modules/`.
8
+ - Inspect root declaration intent in `deps.cirru`; use `caps tree` and `caps why <owner/repo>` to explain
9
+ the resolved recursive graph. The current display combines both root groups, so it cannot be the
10
+ authority for runtime versus development declaration intent.
11
+ - Use `cr config modules [--entry <name>]` to inspect each selected entry's module configuration, then
12
+ run `cr --check-only` for default and every release/CI entry. Named entries do not inherit default
13
+ modules.
14
+ - `docs check-md` defaults to modules from the default entry. Documentation-only dependencies require
15
+ appropriate explicit module paths through repeatable `--dep`; these checks, like entry preprocessing,
16
+ are static evidence and do not prove dynamic loading or external consumer use.
17
+
18
+ ## Validation
19
+
20
+ - `cr docs check-md docs/run/load-deps.md --entry calcit/test.cirru --failures-only`
21
+ - `cr docs check-md docs/CalcitAgent.md --entry calcit/test.cirru --failures-only`
@@ -0,0 +1,4 @@
1
+ # Address dependency audit documentation review
2
+
3
+ - Clarified that `docs check-md --entry` takes a snapshot filename, unlike named-entry selection in `config modules` and top-level `--check-only`.
4
+ - Recorded Markdown validation for both changed dependency-audit documents.
package/lib/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calcit/procs",
3
- "version": "0.13.18",
3
+ "version": "0.13.20",
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.13.18",
3
+ "version": "0.13.20",
4
4
  "main": "./lib/calcit.procs.mjs",
5
5
  "devDependencies": {
6
6
  "@types/node": "^25.7.0",