@calcit/procs 0.13.46 → 0.13.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.
Files changed (29) hide show
  1. package/.yarn/install-state.gz +0 -0
  2. package/README.md +1 -1
  3. package/RFCs/08-23-option-result-binding-macros-rfc.md +5 -1
  4. package/RFCs/08-23-typed-option-query-ergonomics-rfc.md +5 -1
  5. package/RFCs/README.md +3 -2
  6. package/build.rs +32 -0
  7. package/editing-history/20260826-1234-native-let-macro-lowering.md +21 -0
  8. package/editing-history/20260826-1407-native-map-macro-lowering.md +19 -0
  9. package/editing-history/20260826-1645-prune-cold-core-macros.md +11 -0
  10. package/editing-history/20260826-1905-flipped-dynamic-callable-boundary.md +11 -0
  11. package/editing-history/20260826-1940-prevent-js-module-self-imports.md +8 -0
  12. package/editing-history/20260826-1950-js-nullish-unit-boundary.md +10 -0
  13. package/editing-history/20260826-2140-compact-runtime-call-shape.md +32 -0
  14. package/editing-history/202608262220-result-file-effects.md +26 -0
  15. package/editing-history/202608262228-fix-result-method-doc-chain.md +5 -0
  16. package/editing-history/202608262237-result-file-review.md +8 -0
  17. package/editing-history/202608262327-release-0.13.47.md +7 -0
  18. package/editing-history/202608270059-fix-stable-rust-build.md +7 -0
  19. package/editing-history/2026082701-release-0.13.48.md +7 -0
  20. package/editing-history/202608270132-reject-compact-snapshots.md +8 -0
  21. package/editing-history/202608270154-review-compact-migration-paths.md +6 -0
  22. package/editing-history/202608270214-ffi-build-handshake.md +15 -0
  23. package/editing-history/202608270231-ffi-build-review-followup.md +5 -0
  24. package/editing-history/202608270238-release-0.13.49.md +5 -0
  25. package/lib/calcit.procs.d.mts +6 -0
  26. package/lib/calcit.procs.mjs +29 -5
  27. package/lib/package.json +1 -1
  28. package/package.json +1 -1
  29. package/ts-src/calcit.procs.mts +36 -5
Binary file
package/README.md CHANGED
@@ -59,7 +59,7 @@ Evaluate snippets:
59
59
  ```bash
60
60
  calcit eval 'range 100'
61
61
 
62
- calcit eval 'thread-first 100 range (map $ \ * % %)'
62
+ calcit eval -- '-> 100 range (map $ \ * % %)'
63
63
  ```
64
64
 
65
65
  Run with a runtime snapshot such as `calcit.cirru` (legacy filename: `compact.cirru`):
@@ -1,8 +1,12 @@
1
1
  # Option / Result 顺序绑定宏 RFC
2
2
 
3
- 状态:Experimental
3
+ 状态:Partially retained(2026-08-26;保留 `option:let`,移除 `result:let`)
4
4
  日期:2026-08-23
5
5
 
6
+ > 历史说明:`option:let` 在 Respo 中有实际使用,因此继续保留;`result:let`
7
+ > 没有生态采用且会隐藏 Result 链,已从 core 移除。Result 代码应直接使用
8
+ > receiver-first `.and-then`。下文保留原始实验设计供追溯。
9
+
6
10
  ## 目标
7
11
 
8
12
  在不增加 parser syntax、提前返回控制流或隐式 unwrap 的前提下,简化连续的
@@ -1,8 +1,12 @@
1
1
  # 类型安全的 Option 查询终点 RFC
2
2
 
3
- 状态:Implemented(待发布与生态迁移)
3
+ 状态:Withdrawn(2026-08-26;六个 `*-or` 宏已从 core 移除)
4
4
  日期:2026-08-23
5
5
 
6
+ > 历史说明:实际生态采用率为零,而这些宏只隐藏了一次明确的
7
+ > `option:unwrap-or` 调用。当前代码应直接调用查询函数并对返回的 `Option`
8
+ > 使用 `.unwrap-or`;下文保留为设计与撤回记录。
9
+
6
10
  ## 背景与生态证据
7
11
 
8
12
  `08-05-systematic-nil-reduction-rfc.md` 把公开缺失值统一为 `Option<T>`,边界已经比旧的
package/RFCs/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # RFC 整理索引
2
2
 
3
- 更新时间:2026-08-23
3
+ 更新时间:2026-08-26
4
4
 
5
5
  ## 目录原则
6
6
 
@@ -48,7 +48,8 @@
48
48
  | `08-21-type-quality-ci-adoption-rfc.md` | Draft | 统一使用原生 `analyze quality` 与按 definition baseline,定义生态 CI 层级并禁止各项目重复实现 JS 汇总脚本。 |
49
49
  | `08-21-js-ffi-runtime-contract-validation-rfc.md` | Draft | 在现有 typed JS FFI 声明之上增加 host guard、decoder、runtime contract tests 与 unsafe evidence。 |
50
50
  | `08-21-static-type-system-evolution-roadmap.md` | Draft | 借鉴 Rust/MoonBit 推进 Unknown/Dynamic 分离、穷尽性、局部推断、trait coherence 与框架类型化。 |
51
- | `08-23-typed-option-query-ergonomics-rfc.md` | Implemented | 保持 Option 边界,新增 `get-or` 等类型安全查询终点,减少业务代码中的机械 unwrap |
51
+ | `08-23-typed-option-query-ergonomics-rfc.md` | Withdrawn | 生态采用率为零;`get-or` 等六个宏已移除,直接使用查询返回的 `Option` 与 `.unwrap-or`。 |
52
+ | `08-23-option-result-binding-macros-rfc.md` | Partial | 保留已有 Respo 使用的 `option:let`;移除未采用的 `result:let`,Result 链直接使用 `.and-then`。 |
52
53
 
53
54
  ## 已执行的清理
54
55
 
package/build.rs CHANGED
@@ -5,6 +5,7 @@ use std::collections::HashMap;
5
5
  use std::env;
6
6
  use std::fs;
7
7
  use std::path::Path;
8
+ use std::process::Command;
8
9
 
9
10
  #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
10
11
  #[serde(rename_all = "lowercase")]
@@ -558,8 +559,39 @@ fn parse_files(edn: Edn) -> Result<HashMap<String, FileInSnapShot>, String> {
558
559
  }
559
560
  }
560
561
 
562
+ fn rustc_verbose_field<'a>(output: &'a str, name: &str) -> &'a str {
563
+ output
564
+ .lines()
565
+ .find_map(|line| line.strip_prefix(name).map(str::trim))
566
+ .unwrap_or_else(|| panic!("`rustc --version --verbose` did not report `{name}`"))
567
+ }
568
+
569
+ fn ffi_build_id() -> String {
570
+ let rustc = env::var("RUSTC").unwrap_or_else(|_| "rustc".to_owned());
571
+ let output = Command::new(&rustc)
572
+ .args(["--version", "--verbose"])
573
+ .output()
574
+ .unwrap_or_else(|error| panic!("failed to run `{rustc} --version --verbose`: {error}"));
575
+ if !output.status.success() {
576
+ panic!("`{rustc} --version --verbose` failed with {}", output.status);
577
+ }
578
+ let verbose = String::from_utf8(output.stdout).expect("rustc verbose version must be UTF-8");
579
+ let release = rustc_verbose_field(&verbose, "release:");
580
+ let commit = rustc_verbose_field(&verbose, "commit-hash:");
581
+ let target = env::var("TARGET").expect("Cargo must provide TARGET to build scripts");
582
+ let debug_assertions = env::var_os("CARGO_CFG_DEBUG_ASSERTIONS").is_some();
583
+ let panic_strategy = env::var("CARGO_CFG_PANIC").expect("Cargo must provide CARGO_CFG_PANIC to build scripts");
584
+
585
+ format!("rustc={release}:{commit};target={target};debug-assertions={debug_assertions};panic={panic_strategy}")
586
+ }
587
+
561
588
  fn main() {
562
589
  println!("cargo:rerun-if-changed=src/cirru/calcit-core.cirru");
590
+ println!("cargo:rerun-if-env-changed=RUSTC");
591
+ println!("cargo:rerun-if-env-changed=TARGET");
592
+ println!("cargo:rerun-if-env-changed=CARGO_CFG_DEBUG_ASSERTIONS");
593
+ println!("cargo:rerun-if-env-changed=CARGO_CFG_PANIC");
594
+ println!("cargo:rustc-env=CALCIT_FFI_BUILD_ID={}", ffi_build_id());
563
595
 
564
596
  let out_dir = env::var_os("OUT_DIR").unwrap();
565
597
  let dest_path = Path::new(&out_dir).join("calcit-core.rmp");
@@ -0,0 +1,21 @@
1
+ # Native lowering for the core let macro
2
+
3
+ - Added a compiler-native lowering for the strict, capability-free
4
+ `calcit.core/let` macro. It builds the same nested `&let` tree as the macro's
5
+ recursive quasiquote implementation, avoiding general evaluator execution
6
+ for valid binding-pair lists.
7
+ - Invalid outer binding shapes deliberately remain on the ordinary macro path,
8
+ preserving the established macro diagnostics and error behavior.
9
+ - After review, native lowering was narrowed to the exact `&let` binding
10
+ contract: `()` or a two-item list headed by a symbol. Singleton, oversized,
11
+ and non-symbol-headed lists therefore retain the ordinary macro path.
12
+ - Macro metrics now explicitly classify this route as `native-fast-path`, not
13
+ as a general-evaluator fallback or a cache candidate.
14
+ - Latest Respo main release-binary metrics changed from 1,418 expansions / about
15
+ 29.04 ms evaluator time to 1,255 expansions / about 22.55 ms evaluator time.
16
+ The `let` evaluator time became zero while its required post-preprocessing
17
+ still runs.
18
+ - On Apple arm64, 20 alternating paired Respo `--check-only` processes against
19
+ a same-source 0.13.46 baseline improved the median from 246.12 ms to
20
+ 237.40 ms; paired median change was about -2.96%. This is a process-level
21
+ check improvement, not an application-runtime claim.
@@ -0,0 +1,19 @@
1
+ # Native lowering for the core map macro
2
+
3
+ - Continued issue #436 after the merged native `let` fast path by lowering the
4
+ strict core `{}` macro directly to `CalcitProc::NativeMap`.
5
+ - The fast path accepts only the macro's valid two-item list pairs, flattens
6
+ them in source order, and leaves malformed pairs on the normal macro path so
7
+ existing diagnostic text, locations, and metrics remain intact.
8
+ - Latest Respo main showed 96 `{}` expansions and about 2.91 ms spent in the
9
+ general macro evaluator, making this compact, semantics-preserving lowering
10
+ the next lower-risk target after `let`.
11
+ - Against the same-source #459 release binary on Apple arm64, latest Respo
12
+ `--check-only` changed from a 240.10 ms median to 237.99 ms across 20
13
+ alternating process pairs; paired median change was -1.43%. The macro metric
14
+ evaluator total was about 35.0 ms before and 20.1 ms after, while `{}` itself
15
+ moved from about 2.91 ms to zero. This remains compile/check performance,
16
+ not an application-runtime claim.
17
+ - Full Calcit tests and `yarn check-all` passed. Latest Respo passed 27 tests
18
+ and JS check-only; latest Recollect passed 9 native tests and emitted/ran its
19
+ JS test entry successfully.
@@ -0,0 +1,11 @@
1
+ # Prune cold core macros and compatibility stubs
2
+
3
+ - Removed 23 unused or superseded macros from the bundled core, reducing the audited macro inventory from 86 to 63: `\\.`, `%<-`, `<-`, `[,]`, `[][]`, `thread-as`, `thread-first`, `thread-last`, `call-w-log`, `call-wo-log`, `defn-w-log`, `defn-wo-log`, `field-match`, `calcit.internal/&field-match-internal`, `first-or`, `last-or`, `nth-or`, `get-or`, `get-in-or`, `get-env-or`, `result:let`, `record-match`, and `record-with`.
4
+ - Removed the already non-functional `record-struct` and `record?` compatibility stubs, plus their dedicated migration-diagnostic cases. Generic legacy `&record:*` and tuple diagnostics remain until their lower-level compatibility removal is handled separately.
5
+ - Use `->`, `->>`, and `->%` instead of their aliases. Reverse the form order explicitly when migrating `<-` or `%<-`.
6
+ - Use ordinary `[]` construction instead of `[,]`; build a list of lists explicitly instead of `[][]`; write explicit nested `fn` forms instead of `\\.`.
7
+ - Use `fn`/`defn`, `w-log`/`wo-log`, or explicit logging instead of the removed call/definition logging wrappers.
8
+ - Call `.unwrap-or` on the `Option` returned by `get`, `get-in`, `get-env`, `first`, `last`, or `nth` instead of the six `*-or` macros.
9
+ - Chain Result values with receiver-first `.and-then` instead of `result:let`. `option:let` remains because current Respo code uses it.
10
+ - Replace `field-match` with an explicit field read followed by `case`/`match`; use `struct-match` for nominal structs. Replace the removed record names with `struct-match`, `struct-with`, `struct-definition`, `struct?`, or `struct-def?` as appropriate.
11
+ - Migrated the Calcit runtime fixtures and current documentation to the canonical forms. Historical RFCs are retained with explicit withdrawn/partial status notes; editing-history records remain unchanged.
@@ -0,0 +1,11 @@
1
+ # Keep `flipped` compatible with host callables
2
+
3
+ - Relaxed `flipped`'s first macro input from source schema `Fn` (represented
4
+ internally as `DynFn`) to `Dynamic`.
5
+ - The macro only reverses call syntax; JavaScript host symbols such as
6
+ `js/setTimeout` are intentionally dynamic and cannot satisfy the native
7
+ callable schema before host lowering.
8
+ - Kept the rest arguments and expansion result explicitly dynamic, while the
9
+ macro remains compile-time pure and phase-aware.
10
+ - Validated the change against current Respo UI usage, which relies on
11
+ `flipped js/setTimeout ...`.
@@ -0,0 +1,8 @@
1
+ # Prevent generated JavaScript module self-imports
2
+
3
+ - Keep resolved definitions from the current namespace local even when a
4
+ type-directed rewrite conservatively marks them as referred imports.
5
+ - Add a codegen regression test proving that the local definition remains a
6
+ direct identifier and does not enter the ESM import collection.
7
+ - Validated with skir's same-namespace `Request` and `Response` struct
8
+ constructors, which previously produced duplicate declaration errors.
@@ -0,0 +1,10 @@
1
+ # Keep JS nullish checks aligned with `&unit`
2
+
3
+ - Treat both `nil` (JavaScript `null`) and `&unit` (JavaScript `undefined`) as
4
+ nullish at `JsNullish<T>` boundaries.
5
+ - Define `js-present?` as the complement of `js-nullish?` so the two helpers
6
+ cannot drift apart again.
7
+ - Route `js-nullish->option` through the same predicate so JavaScript
8
+ `undefined` becomes `%none` instead of `%some &unit`.
9
+ - Add examples covering both runtime representations. This prevents optional
10
+ host fields whose value is `undefined` from being dereferenced as present.
@@ -0,0 +1,32 @@
1
+ # Cache compact native function call shape
2
+
3
+ - Continued issue #464 with a field-consumer and layout audit before changing
4
+ the runtime representation. On Apple arm64, `Calcit` is 88 bytes,
5
+ `CalcitFn` 176 bytes, `CalcitLocal` 48 bytes, and `CalcitScope` 24 bytes.
6
+ - Function values already share `Arc<CalcitFn>`, so ordinary calls do not clone
7
+ generic, where-bound, return, or argument annotation graphs. Those fields are
8
+ cold resident metadata rather than per-call allocation traffic. Local
9
+ metadata is hot in the preprocessed tree, but shrinking `CalcitLocal` alone
10
+ cannot shrink the 88-byte `Calcit` enum; a later material memory experiment
11
+ needs a genuinely separate execution expression.
12
+ - Added a 6-byte `CalcitFnCallShape` that fills existing `CalcitFn` alignment
13
+ padding, leaving `size_of::<CalcitFn>()` unchanged at 176 bytes. It caches the
14
+ fixed parameter count, continuous trailing `Option` count, and combined
15
+ marked/typed rest evidence after preprocessing.
16
+ - Native `run_fn` and `run_fn_owned` now consult the compact shape before
17
+ optional-argument completion instead of scanning `CalcitFnArgs` and the full
18
+ argument annotation list on every function call. Full schema metadata remains
19
+ available to preprocessing, codegen, queries, effects analysis, and errors.
20
+ - Against main `86c47a93`, an alternating 30-pair native fold benchmark with
21
+ 200,000 callback calls changed from a 119.280 ms median to 117.647 ms; paired
22
+ median change was -1.34%. A 15-pair 1,000,000-call run changed from 607.412 ms
23
+ to 594.369 ms; paired median change was -2.09%.
24
+ - Latest Respo main (`27bb6304`) retained all 27 native tests; its 15-sample
25
+ test-body median changed from 30.867 ms to 31.255 ms. Latest Recollect main
26
+ (`e0903a6d`) retained all 9 native tests and stayed at a 9.520 ms median.
27
+ These project-level differences are treated as noise rather than claimed
28
+ speedups. Median peak RSS moved from 48,201,728 to 47,726,592 bytes for Respo
29
+ and from 70,172,672 to 68,927,488 bytes for Recollect, but process-level RSS
30
+ variance is too high to attribute that reduction to this zero-size cache.
31
+ - The release binary changed from 9,212,896 to 9,213,136 bytes (+240 bytes).
32
+ No new heap allocation is introduced by the cached shape.
@@ -0,0 +1,26 @@
1
+ # Result-based file effects
2
+
3
+ ## Summary
4
+
5
+ - Added String methods `.read-file`, `.read-dir`, and `.write-file` that convert recoverable file failures into `Result<..., String>` while retaining the raising raw procedures for compatibility.
6
+ - Added native and generated-JavaScript regression coverage for missing paths and static Result contracts.
7
+ - Completed the JavaScript host boundary for file effects: `read_file` now returns and validates injected content, `read_dir` validates/sorts host paths, and missing injections raise stable errors that the safe methods can catch.
8
+ - Updated type guidance and file-operation examples to prefer receiver methods.
9
+ - Fixed a Rust 1.97 Clippy finding in the previously landed native-call-shape test so the required `-D warnings` gate remains clean.
10
+
11
+ ## Knowledge retained
12
+
13
+ - A language-level `try` wrapper is sufficient for Result-returning effects and keeps raw primitives ABI-compatible. Because generated JavaScript `try` normalizes host exceptions to String, the same core wrapper works on native and JS.
14
+ - Core String methods are registered in `&core-string-methods`; adding a method there makes receiver typing and method discovery reuse the wrapper function schema.
15
+ - Node file effects are host capabilities supplied through `globalThis.__calcit_injections__`. Validate values at this Dynamic boundary before returning them to typed Calcit code. Browser `read-file` keeps its localStorage compatibility path but now raises on a missing key, matching the recoverable failure contract.
16
+ - `read-dir` must sort injected JavaScript paths to match the native procedure's deterministic contract.
17
+ - WASM currently skips `try`-based file wrappers and does not expose host file procedures; keep this backend limit explicit in API docs and regression output.
18
+
19
+ ## Validation
20
+
21
+ - `cargo fmt --all`
22
+ - `cargo clippy --all-targets -- -D warnings`
23
+ - `cargo test`
24
+ - `yarn check-all`
25
+ - Latest Respo main: 27/27 tests, 111/111 documentation blocks, JS codegen
26
+ - Latest Recollect main: 9/9 native tests, generated JS tests, production build with Node 24.4.1, required WASM checks
@@ -0,0 +1,5 @@
1
+ # Fix Result method chaining in file examples
2
+
3
+ The Markdown checker exposed a Cirru association pitfall in the new examples: `path .read-file .and-then` passes `.and-then` and its callback as extra arguments to `.read-file`. Parenthesize the first receiver call as `(path .read-file) .and-then` so the Result method receives the completed `Result` value. The checked example keeps its callback body direct to avoid unrelated nested callback inference obscuring the file-effect contract.
4
+
5
+ Validated with the repository-wide Markdown check.
@@ -0,0 +1,8 @@
1
+ # Address Result file-effect review
2
+
3
+ - Grouped both receiver expressions in the documented Result chain so Cirru does not pass later method tokens as extra arguments.
4
+ - Made `.read-dir` preserve the default non-recursive call without reintroducing a legacy nil parameter: its trailing recursion flag is `Option<Bool>`, omission supplies `%none`, and explicit recursion uses `%some true` or `%some false`.
5
+ - Added omitted and explicit Option cases to native and generated-JavaScript tests.
6
+ - Repeated the native/JavaScript support and WASM host-effect boundary at each public documentation entry.
7
+
8
+ Validated with targeted examples/tests, native and generated JavaScript runs, and the full Markdown checker.
@@ -0,0 +1,7 @@
1
+ # Release 0.13.47
2
+
3
+ - Publishes the recoverable String file-effect methods merged in #472.
4
+ - Includes the matching JavaScript procs exports for `read_file`, `read_dir`,
5
+ and `write_file`; this removes unresolved `$procs.read_dir` imports in real
6
+ JS consumers upgraded from the previously published 0.13.46 package.
7
+ - Keeps Cargo and npm package versions aligned at 0.13.47.
@@ -0,0 +1,7 @@
1
+ # Keep the release path buildable on stable Rust
2
+
3
+ - Replaced three unstable `if let` match guards with equivalent nested stable
4
+ control flow.
5
+ - Kept enum and function-schema resolution order unchanged.
6
+ - Changed the regular CI toolchain from nightly to stable so it matches the
7
+ publish workflow and catches stable-channel build blockers before release.
@@ -0,0 +1,7 @@
1
+ # Release 0.13.48
2
+
3
+ - Publishes the stable-Rust build fix after the 0.13.47 tag was created from a
4
+ revision that only compiled under the nightly test workflow.
5
+ - Keeps Cargo and npm package versions aligned.
6
+ - Uses the stable toolchain in normal CI so future release blockers are caught
7
+ before a tag is created.
@@ -0,0 +1,8 @@
1
+ # Reject retired compact.cirru snapshots
2
+
3
+ - Reject `compact.cirru` before deserialization instead of silently running it
4
+ as a default or module fallback.
5
+ - Point users to the canonical copy/rename, format, and check-only sequence.
6
+ - Record 0.13.48 as the final compatibility release for the old filename.
7
+ - Remove the obsolete formatter warning because formatting a retired filename
8
+ is no longer an accepted operation.
@@ -0,0 +1,6 @@
1
+ # Resolve compact snapshot migration review
2
+
3
+ - Keep retired `compact.cirru` files out of normal module candidate resolution.
4
+ - Detect compact-only modules separately so they retain actionable migration guidance.
5
+ - Preserve nested module directories in the canonical destination and validation commands.
6
+ - Correct the remaining singular snapshot wording in the agent guide.
@@ -0,0 +1,15 @@
1
+ # Guard transitional Rust-native FFI with a C-safe build handshake
2
+
3
+ - Embed the exact rustc release/commit, target, debug-assertion mode, and panic
4
+ strategy in the Calcit host build.
5
+ - Read an optional static C string from dylibs before invoking legacy Rust ABI
6
+ version or business symbols.
7
+ - Reject mismatched identities deterministically; require identity metadata for
8
+ debug hosts while retaining a warned release-only migration path.
9
+ - Apply the same check to runtime calls and `caps` native verification, and
10
+ include the identity in native realization receipts and cache keys.
11
+ - Add `calcit --ffi-build-id` and document the extension-side build script and
12
+ export.
13
+ - Reproduce the former debug-host/release-dylib abort with calcit_wasmtime: the
14
+ guarded host now rejects before `abi_version`; matching release builds still
15
+ return `27`.
@@ -0,0 +1,5 @@
1
+ # FFI build handshake review follow-up
2
+
3
+ - Updated the introductory Rust FFI examples to use Rust 2024-compatible `#[unsafe(no_mangle)]` attributes.
4
+ - Included the required `build.rs` identity generator in the documented upgrade staging command.
5
+ - Kept the original `202608270214` history timestamp because it records Asia/Shanghai local time, matching the repository workflow and actual commit time.
@@ -0,0 +1,5 @@
1
+ # Release 0.13.49
2
+
3
+ - Published the strict `compact.cirru` filename cutoff merged in #475.
4
+ - Published the transitional Rust-native FFI build-identity handshake merged in #476.
5
+ - Kept Cargo and npm package versions synchronized and refreshed the workspace lockfile.
@@ -249,7 +249,13 @@ export declare let struct_$q_: (x: CalcitValue) => boolean;
249
249
  export declare let enum_$q_: (x: CalcitValue) => boolean;
250
250
  export declare let buffer_$q_: (x: CalcitValue) => boolean;
251
251
  export declare let _$n_str_$o_escape: (x: string) => string;
252
+ export type CalcitFileInjections = {
253
+ read_file?: (path: string) => unknown;
254
+ read_dir?: (path: string, recursive: boolean) => unknown;
255
+ write_file?: (path: string, content: string) => unknown;
256
+ };
252
257
  export declare let read_file: (path: string) => string;
258
+ export declare let read_dir: (path: string, recursive: boolean) => CalcitSliceList;
253
259
  export declare let write_file: (path: string, content: string) => void;
254
260
  export declare let parse_cirru: (code: string) => CalcitCirruQuote;
255
261
  export declare let parse_cirru_list: (code: string) => CalcitList;
@@ -1538,20 +1538,44 @@ export let buffer_$q_ = (x) => {
1538
1538
  return false;
1539
1539
  };
1540
1540
  export let _$n_str_$o_escape = (x) => JSON.stringify(x);
1541
+ const get_file_injection = (name) => {
1542
+ const injections = globalThis.__calcit_injections__;
1543
+ const injection = injections?.[name];
1544
+ if (typeof injection !== "function") {
1545
+ throw new Error(`${name} is unavailable: the JavaScript host did not provide a __calcit_injections__.${name} function`);
1546
+ }
1547
+ return injection;
1548
+ };
1541
1549
  export let read_file = (path) => {
1542
1550
  if (inNodeJs) {
1543
- // TODO
1544
- globalThis["__calcit_injections__"].read_file(path);
1551
+ const content = get_file_injection("read_file")(path);
1552
+ if (typeof content !== "string") {
1553
+ throw new TypeError(`read_file injection expected a string result, got ${typeof content}`);
1554
+ }
1555
+ return content;
1545
1556
  }
1546
1557
  else {
1547
1558
  // no actual File API in browser
1548
- return localStorage.get(path) ?? "";
1559
+ const content = localStorage.getItem(path);
1560
+ if (content === null) {
1561
+ throw new Error(`read-file failed at ${path}: key not found in localStorage`);
1562
+ }
1563
+ return content;
1564
+ }
1565
+ };
1566
+ export let read_dir = (path, recursive) => {
1567
+ if (!inNodeJs) {
1568
+ throw new Error("read_dir is unavailable in browser JavaScript hosts");
1569
+ }
1570
+ const paths = get_file_injection("read_dir")(path, recursive);
1571
+ if (!Array.isArray(paths) || !paths.every((item) => typeof item === "string")) {
1572
+ throw new TypeError("read_dir injection expected an array of strings");
1549
1573
  }
1574
+ return new CalcitSliceList([...paths].sort());
1550
1575
  };
1551
1576
  export let write_file = (path, content) => {
1552
1577
  if (inNodeJs) {
1553
- // TODO
1554
- globalThis["__calcit_injections__"].write_file(path, content);
1578
+ get_file_injection("write_file")(path, content);
1555
1579
  }
1556
1580
  else {
1557
1581
  // no actual File API in browser
package/lib/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@calcit/procs",
3
- "version": "0.13.46",
3
+ "version": "0.13.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.13.46",
3
+ "version": "0.13.49",
4
4
  "main": "./lib/calcit.procs.mjs",
5
5
  "devDependencies": {
6
6
  "@types/node": "^25.7.0",
@@ -1676,19 +1676,50 @@ export let buffer_$q_ = (x: CalcitValue): boolean => {
1676
1676
 
1677
1677
  export let _$n_str_$o_escape = (x: string) => JSON.stringify(x);
1678
1678
 
1679
+ export type CalcitFileInjections = {
1680
+ read_file?: (path: string) => unknown;
1681
+ read_dir?: (path: string, recursive: boolean) => unknown;
1682
+ write_file?: (path: string, content: string) => unknown;
1683
+ };
1684
+
1685
+ const get_file_injection = <K extends keyof CalcitFileInjections>(name: K): NonNullable<CalcitFileInjections[K]> => {
1686
+ const injections = (globalThis as { __calcit_injections__?: CalcitFileInjections }).__calcit_injections__;
1687
+ const injection = injections?.[name];
1688
+ if (typeof injection !== "function") {
1689
+ throw new Error(`${name} is unavailable: the JavaScript host did not provide a __calcit_injections__.${name} function`);
1690
+ }
1691
+ return injection as NonNullable<CalcitFileInjections[K]>;
1692
+ };
1693
+
1679
1694
  export let read_file = (path: string): string => {
1680
1695
  if (inNodeJs) {
1681
- // TODO
1682
- (globalThis as any)["__calcit_injections__"].read_file(path);
1696
+ const content = get_file_injection("read_file")(path);
1697
+ if (typeof content !== "string") {
1698
+ throw new TypeError(`read_file injection expected a string result, got ${typeof content}`);
1699
+ }
1700
+ return content;
1683
1701
  } else {
1684
1702
  // no actual File API in browser
1685
- return localStorage.get(path) ?? "";
1703
+ const content = localStorage.getItem(path);
1704
+ if (content === null) {
1705
+ throw new Error(`read-file failed at ${path}: key not found in localStorage`);
1706
+ }
1707
+ return content;
1708
+ }
1709
+ };
1710
+ export let read_dir = (path: string, recursive: boolean): CalcitSliceList => {
1711
+ if (!inNodeJs) {
1712
+ throw new Error("read_dir is unavailable in browser JavaScript hosts");
1713
+ }
1714
+ const paths = get_file_injection("read_dir")(path, recursive);
1715
+ if (!Array.isArray(paths) || !paths.every((item) => typeof item === "string")) {
1716
+ throw new TypeError("read_dir injection expected an array of strings");
1686
1717
  }
1718
+ return new CalcitSliceList([...paths].sort());
1687
1719
  };
1688
1720
  export let write_file = (path: string, content: string): void => {
1689
1721
  if (inNodeJs) {
1690
- // TODO
1691
- (globalThis as any)["__calcit_injections__"].write_file(path, content);
1722
+ get_file_injection("write_file")(path, content);
1692
1723
  } else {
1693
1724
  // no actual File API in browser
1694
1725
  localStorage.setItem(path, content);