functionalscript 0.38.0 → 0.40.0

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 (137) hide show
  1. package/README.md +1 -1
  2. package/fjs/bnf/descent/module.f.d.ts +43 -2
  3. package/fjs/bnf/descent/module.f.js +37 -12
  4. package/fjs/bnf/descent/proof.f.d.ts +1 -0
  5. package/fjs/bnf/descent/proof.f.js +73 -32
  6. package/fjs/bnf/ll1/module.f.js +2 -2
  7. package/fjs/bnf/token_symbol/module.f.d.ts +38 -0
  8. package/fjs/bnf/token_symbol/module.f.js +48 -0
  9. package/fjs/bnf/token_symbol/proof.f.d.ts +10 -0
  10. package/fjs/bnf/token_symbol/proof.f.js +37 -0
  11. package/fjs/cas/cli/module.f.js +6 -9
  12. package/fjs/cas/evo/module.f.d.ts +77 -13
  13. package/fjs/cas/evo/module.f.js +130 -18
  14. package/fjs/cas/evo/proof.f.d.ts +7 -0
  15. package/fjs/cas/evo/proof.f.js +120 -1
  16. package/fjs/cas/module.f.js +64 -37
  17. package/fjs/cas/proof.f.d.ts +3 -9
  18. package/fjs/cas/proof.f.js +94 -74
  19. package/fjs/ci/bun/module.f.js +3 -3
  20. package/fjs/ci/common/module.f.d.ts +1 -2
  21. package/fjs/ci/common/module.f.js +1 -5
  22. package/fjs/ci/config/module.f.d.ts +11 -8
  23. package/fjs/ci/config/module.f.js +24 -9
  24. package/fjs/ci/deno/module.f.js +3 -3
  25. package/fjs/ci/module.f.js +13 -7
  26. package/fjs/ci/nix/module.f.d.ts +61 -0
  27. package/fjs/ci/nix/module.f.js +92 -0
  28. package/fjs/ci/nix/proof.f.d.ts +23 -0
  29. package/fjs/ci/nix/proof.f.js +109 -0
  30. package/fjs/ci/node/module.f.d.ts +23 -1
  31. package/fjs/ci/node/module.f.js +58 -12
  32. package/fjs/ci/proof.f.d.ts +2 -0
  33. package/fjs/ci/proof.f.js +49 -9
  34. package/fjs/cli/module.f.js +2 -2
  35. package/fjs/common/monoid/module.f.d.ts +46 -2
  36. package/fjs/common/monoid/module.f.js +44 -0
  37. package/fjs/common/monoid/proof.f.d.ts +5 -0
  38. package/fjs/common/monoid/proof.f.js +27 -1
  39. package/fjs/dev/module.f.d.ts +1 -0
  40. package/fjs/dev/module.f.js +38 -27
  41. package/fjs/dev/update/module.f.d.ts +11 -0
  42. package/fjs/dev/update/module.f.js +20 -0
  43. package/fjs/dev/update/proof.f.d.ts +7 -0
  44. package/fjs/dev/update/proof.f.js +35 -0
  45. package/fjs/djs/ast/module.f.d.ts +47 -0
  46. package/fjs/djs/ast/module.f.js +9 -0
  47. package/fjs/djs/module.f.js +5 -8
  48. package/fjs/djs/parser/module.f.d.ts +3 -0
  49. package/fjs/djs/parser/module.f.js +17 -0
  50. package/fjs/djs/tokenizer/module.f.js +2 -2
  51. package/fjs/djs/tokenizer/proof.f.d.ts +1 -0
  52. package/fjs/djs/tokenizer/proof.f.js +70 -16
  53. package/fjs/djs/transpiler/module.f.js +6 -6
  54. package/fjs/effects/eff/module.f.d.ts +72 -0
  55. package/fjs/effects/eff/module.f.js +46 -0
  56. package/fjs/effects/eff/proof.f.d.ts +11 -0
  57. package/fjs/effects/eff/proof.f.js +58 -0
  58. package/fjs/effects/list/module.f.d.ts +11 -5
  59. package/fjs/effects/list/module.f.js +2 -1
  60. package/fjs/effects/memory/proof.f.js +7 -8
  61. package/fjs/effects/module.f.d.ts +334 -52
  62. package/fjs/effects/module.f.js +253 -45
  63. package/fjs/effects/node/memory/proof.js +3 -2
  64. package/fjs/effects/node/module.d.ts +3 -3
  65. package/fjs/effects/node/module.f.d.ts +18 -8
  66. package/fjs/effects/node/module.f.js +31 -11
  67. package/fjs/effects/node/module.js +12 -12
  68. package/fjs/effects/node/proof.f.d.ts +1 -0
  69. package/fjs/effects/node/proof.f.js +32 -11
  70. package/fjs/effects/node/virtual/module.f.js +1 -1
  71. package/fjs/effects/proof.f.d.ts +32 -6
  72. package/fjs/effects/proof.f.js +131 -39
  73. package/fjs/emergent_testing/all.test.js +2 -1
  74. package/fjs/emergent_testing/module.f.d.ts +3 -3
  75. package/fjs/emergent_testing/module.f.js +63 -49
  76. package/fjs/emergent_testing/proof.f.d.ts +4 -0
  77. package/fjs/emergent_testing/proof.f.js +54 -21
  78. package/fjs/emergent_testing/scenarios/thenable.pass.js +1 -1
  79. package/fjs/fsc/module.f.js +4 -4
  80. package/fjs/fsm/module.f.js +1 -1
  81. package/fjs/js/tokenizer/module.f.d.ts +1 -0
  82. package/fjs/js/tokenizer/module.f.js +13 -6
  83. package/fjs/{cas/mcp → mcp/cas}/module.f.d.ts +3 -25
  84. package/fjs/{cas/mcp → mcp/cas}/module.f.js +26 -64
  85. package/fjs/mcp/evo/module.f.d.ts +27 -0
  86. package/fjs/mcp/evo/module.f.js +94 -0
  87. package/fjs/{cas/evo/mcp → mcp/evo}/proof.f.d.ts +2 -0
  88. package/fjs/{cas/evo/mcp → mcp/evo}/proof.f.js +40 -9
  89. package/fjs/mcp/module.f.d.ts +54 -237
  90. package/fjs/mcp/module.f.js +55 -258
  91. package/fjs/mcp/proof.f.d.ts +43 -32
  92. package/fjs/mcp/proof.f.js +509 -200
  93. package/fjs/media/nix/module.f.d.ts +30 -0
  94. package/fjs/media/nix/module.f.js +166 -0
  95. package/fjs/media/nix/proof.f.d.ts +32 -0
  96. package/fjs/media/nix/proof.f.js +127 -0
  97. package/fjs/media/type/module.f.js +2 -2
  98. package/fjs/media/type/proof.f.js +7 -10
  99. package/fjs/module.f.js +5 -9
  100. package/fjs/protocol/json_rpc/module.f.d.ts +114 -0
  101. package/fjs/{media/json/rpc → protocol/json_rpc}/module.f.js +3 -3
  102. package/fjs/{media/json/rpc → protocol/json_rpc}/proof.f.js +3 -3
  103. package/fjs/protocol/mcp/module.f.d.ts +239 -0
  104. package/fjs/protocol/mcp/module.f.js +272 -0
  105. package/fjs/protocol/mcp/proof.f.d.ts +34 -0
  106. package/fjs/protocol/mcp/proof.f.js +208 -0
  107. package/fjs/{mcp → protocol/mcp}/stdio/module.f.d.ts +7 -7
  108. package/fjs/{mcp → protocol/mcp}/stdio/module.f.js +22 -22
  109. package/fjs/{mcp → protocol/mcp}/stdio/proof.f.js +9 -9
  110. package/fjs/types/bigint/module.f.js +4 -3
  111. package/fjs/types/bit_vec/module.f.js +5 -15
  112. package/fjs/types/btree/remove/module.f.d.ts +5 -0
  113. package/fjs/types/btree/remove/module.f.js +12 -0
  114. package/fjs/types/list/module.f.d.ts +21 -0
  115. package/fjs/types/list/module.f.js +17 -0
  116. package/fjs/types/list/proof.f.d.ts +1 -0
  117. package/fjs/types/list/proof.f.js +24 -1
  118. package/fjs/types/number/module.f.js +2 -1
  119. package/fjs/types/range_map/module.f.d.ts +14 -13
  120. package/fjs/types/range_map/module.f.js +18 -13
  121. package/fjs/types/range_map/proof.f.js +26 -39
  122. package/fjs/types/range_set/module.f.d.ts +5 -0
  123. package/fjs/types/range_set/module.f.js +16 -0
  124. package/fjs/types/range_set/proof.f.d.ts +1 -0
  125. package/fjs/types/range_set/proof.f.js +19 -0
  126. package/fjs/types/string/module.f.js +10 -2
  127. package/fjs/website/module.f.js +2 -3
  128. package/package.json +5 -5
  129. package/fjs/cas/evo/mcp/module.f.d.ts +0 -19
  130. package/fjs/cas/evo/mcp/module.f.js +0 -56
  131. package/fjs/cas/mcp/proof.f.d.ts +0 -45
  132. package/fjs/cas/mcp/proof.f.js +0 -545
  133. package/fjs/ci/playwright/module.f.d.ts +0 -2
  134. package/fjs/ci/playwright/module.f.js +0 -25
  135. package/fjs/media/json/rpc/module.f.d.ts +0 -114
  136. /package/fjs/{media/json/rpc → protocol/json_rpc}/proof.f.d.ts +0 -0
  137. /package/fjs/{mcp → protocol/mcp}/stdio/proof.f.d.ts +0 -0
@@ -7,7 +7,8 @@ import { sha256 } from '../crypto/sha2/module.f.js';
7
7
  import { join, normalize, parse } from '../path/module.f.js';
8
8
  import { empty, length, maxLength, maxLengthBytes, msb, vec } from '../types/bit_vec/module.f.js';
9
9
  import { cBase32ToVec, vecToCBase32 } from '../basen/cbase32/module.f.js';
10
- import { foldStep, forEachStep, okStep, pure } from '../effects/module.f.js';
10
+ import { foldStep, forEachStep, history, historyStep, okStep, pure, step } from '../effects/module.f.js';
11
+ import { eff } from '../effects/eff/module.f.js';
11
12
  import { access, createExclusive, isNotFound, mkdir, now, randomInt, readBytes, readdir, rename, rm, stat, writeBytes, } from '../effects/node/module.f.js';
12
13
  import { toOption } from '../types/nullable/module.f.js';
13
14
  import { error, ok, unwrap } from '../types/result/module.f.js';
@@ -32,7 +33,8 @@ export const toPath = (key) => {
32
33
  * process.
33
34
  */
34
35
  export const collectRead = (stream) => {
35
- const loop = (acc) => (s) => s.step((node) => {
36
+ const loop = (acc) => (s) => eff(s)
37
+ .step((node) => {
36
38
  if (node === undefined) {
37
39
  return pure(ok(acc));
38
40
  }
@@ -45,7 +47,8 @@ export const collectRead = (stream) => {
45
47
  return pure(error(`cas blob exceeds maximum vector length of ${maxLength} bits`));
46
48
  }
47
49
  return loop(msb.concat(acc)(v))(tail);
48
- });
50
+ })
51
+ .value;
49
52
  return loop(empty)(stream);
50
53
  };
51
54
  /** Maximum chunk size for streaming reads: the largest `Vec` the runtime allows. */
@@ -75,13 +78,18 @@ const deadlineOf = (name) => Number(name.slice(0, name.indexOf('-')));
75
78
  * still-live lease is left alone; the fencing rename keeps even a misjudged
76
79
  * reclaim fail-safe (worst case: that upload restarts).
77
80
  */
78
- const gcStage = (stageDir) => now().step(t => readdir(stageDir, {}).step(([k, v]) => {
81
+ const gcStage = (stageDir) => eff(now())
82
+ .step(() => readdir(stageDir, {}))
83
+ .step(([k, v], t) => {
79
84
  if (k === 'error') {
80
85
  return pure(undefined);
81
86
  }
82
87
  const expired = v.flatMap(d => d.isFile && deadlineOf(d.name) < t ? [d.name] : []);
83
- return forEachStep((name) => rm(join(stageDir, name)).step(() => pure(undefined)))(expired);
84
- }));
88
+ return forEachStep(pure(expired), name => eff(rm(join(stageDir, name)))
89
+ .step(() => pure(undefined))
90
+ .value);
91
+ })
92
+ .value;
85
93
  /**
86
94
  * Builds a content-addressable storage facade from a SHA-2 implementation.
87
95
  */
@@ -92,7 +100,7 @@ export const fileCas = (sha2) => (path) => {
92
100
  return {
93
101
  read: (hash) => {
94
102
  const p = join(path, toPath(hash));
95
- const loop = (offset) => readBytes(p, offset, chunkBytes)
103
+ const loop = (offset) => eff(readBytes(p, offset, chunkBytes))
96
104
  .step((result) => {
97
105
  const [t, v] = result;
98
106
  // A missing shard or read error is an explicit error item, never EOF.
@@ -104,7 +112,8 @@ export const fileCas = (sha2) => (path) => {
104
112
  return length(v) === 0n
105
113
  ? elEmpty()
106
114
  : nonEmpty(ok(v), loop(offset + chunkBytes));
107
- });
115
+ })
116
+ .value;
108
117
  return loop(0);
109
118
  },
110
119
  // Lock-free staging upload (issues/cas/staging-lease.md): stream each chunk straight
@@ -125,17 +134,20 @@ export const fileCas = (sha2) => (path) => {
125
134
  const rel = toPath(hash);
126
135
  const dst = join(path, rel);
127
136
  const dstDir = join(path, ...parse(rel).slice(0, -1));
128
- return mkdir(dstDir, { recursive: true })
137
+ return eff(mkdir(dstDir, { recursive: true }))
129
138
  .step(() => rename(curPath, dst))
130
139
  .step(() => rm(curPath))
131
140
  .step(() => stat(dst))
132
- .step(st => pure(st[0] === 'ok' && st[1].size === offset ? ok(hash) : error('publish size mismatch')));
141
+ .step(st => pure(st[0] === 'ok' && st[1].size === offset ? ok(hash) : error('publish size mismatch')))
142
+ .value;
133
143
  };
134
144
  // Any streaming error fails closed: delete the partial file, return the error.
135
- const fail = (curPath, e) => rm(curPath).step(() => pure(error(e)));
136
- return gcStage(stageDir).step(() => random256.step(rnd => {
145
+ const fail = (curPath, e) => eff(rm(curPath))
146
+ .step(() => pure(error(e)))
147
+ .value;
148
+ return eff(gcStage(stageDir)).step(() => eff(random256).step(rnd => {
137
149
  const rndStr = vecToCBase32(rnd);
138
- const loop = (state, offset, curPath) => (stream) => stream
150
+ const loop = (state, offset, curPath) => (stream) => eff(stream)
139
151
  .step((node) => {
140
152
  if (node === undefined) {
141
153
  return publish(state, offset, curPath);
@@ -145,7 +157,7 @@ export const fileCas = (sha2) => (path) => {
145
157
  return fail(curPath, first[1]);
146
158
  }
147
159
  const chunk = first[1];
148
- return writeBytes(curPath, offset, chunk)
160
+ return eff(writeBytes(curPath, offset, chunk))
149
161
  .step(wb => {
150
162
  if (wb[0] === 'error') {
151
163
  return fail(curPath, wb[1]);
@@ -153,49 +165,59 @@ export const fileCas = (sha2) => (path) => {
153
165
  const newState = sha2.append(chunk)(state);
154
166
  const newOffset = offset + Number(length(chunk) / 8n);
155
167
  // Renew the lease: rename to a fresh deadline (keeps `delta` constant).
156
- return now()
157
- .step(t => {
158
- const next = join(stageDir, stageName(t + leaseDelta, rndStr));
159
- return rename(curPath, next).step(([t, v]) => t === 'error'
160
- ? fail(curPath, v)
161
- : loop(newState, newOffset, next)(tail));
162
- });
163
- });
164
- });
165
- return mkdir(stageDir, { recursive: true })
166
- .step(() => now()
168
+ // The new path is still needed after the rename, to recurse with,
169
+ // so the rename captures it rather than closing over it.
170
+ const nextPath = step(now(), t => pure(join(stageDir, stageName(t + leaseDelta, rndStr))));
171
+ const renamed = historyStep(history(nextPath), next => rename(curPath, next));
172
+ return step(renamed, ([[rt, v], next]) => rt === 'error'
173
+ ? fail(curPath, v)
174
+ : loop(newState, newOffset, next)(tail));
175
+ })
176
+ .value;
177
+ })
178
+ .value;
179
+ return eff(mkdir(stageDir, { recursive: true }))
180
+ .step(() => now())
167
181
  .step(t0 => {
168
182
  const path0 = join(stageDir, stageName(t0 + leaseDelta, rndStr));
169
- return createExclusive(path0)
170
- .step(okStep(() => loop(sha2.init, 0, path0)(payload)));
171
- }));
172
- }));
183
+ return eff(createExclusive(path0))
184
+ .step(okStep(() => loop(sha2.init, 0, path0)(payload)))
185
+ .value;
186
+ })
187
+ .value;
188
+ }).value).value;
173
189
  },
174
190
  list: () =>
175
191
  // A fresh store has no `.cas` directory yet. Treat *only* that case as an
176
192
  // empty store, mirroring how `read` maps a missing shard to an error item.
177
193
  // A `.cas` that exists but cannot be read (permissions, corruption) is a
178
194
  // genuine storage error and is surfaced, not masked as "no hashes".
179
- access(storePrefix).step(a => {
195
+ eff(access(storePrefix))
196
+ .step(a => {
180
197
  if (a[0] === 'error') {
181
198
  if (isNotFound(a[1])) {
182
199
  return pure([]);
183
200
  }
184
201
  throw a[1];
185
202
  }
186
- return readdir(storePrefix, { recursive: true })
203
+ return eff(readdir(storePrefix, { recursive: true }))
187
204
  .step(r => pure(unwrap(r).flatMap(({ name, parentPath, isFile }) => toOption(isFile
188
205
  ? cBase32ToVec(normalize(parentPath).substring(normalizedStorePrefix.length).replaceAll('/', '') + name)
189
- : null))));
190
- }),
206
+ : null))))
207
+ .value;
208
+ })
209
+ .value,
191
210
  url: (hash) => join(path, toPath(hash))
192
211
  };
193
212
  };
194
213
  /** 256-bit random `Vec` built from 8 sequential `randomInt` (32-bit) calls. */
195
- const random256 = foldStep((_) => (acc) => randomInt().step(r => pure(msb.concat(acc)(vec(32n)(BigInt(r))))))(empty)([0, 1, 2, 3, 4, 5, 6, 7]);
214
+ const random256 = foldStep(pure([0, 1, 2, 3, 4, 5, 6, 7]), empty, () => (acc) => eff(randomInt())
215
+ .step(r => pure(msb.concat(acc)(vec(32n)(BigInt(r)))))
216
+ .value);
196
217
  /** Streams any file at `filePath` in `<=128 KiB` chunks as a `ListEffect` of `ok` items. */
197
218
  const streamFile = (filePath) => {
198
- const loop = (offset) => readBytes(filePath, offset, chunkBytes).step((result) => {
219
+ const loop = (offset) => eff(readBytes(filePath, offset, chunkBytes))
220
+ .step((result) => {
199
221
  if (result[0] === 'error') {
200
222
  return nonEmpty(result, elEmpty());
201
223
  }
@@ -203,7 +225,8 @@ const streamFile = (filePath) => {
203
225
  return length(chunk) === 0n
204
226
  ? elEmpty()
205
227
  : nonEmpty(ok(chunk), loop(offset + chunkBytes));
206
- });
228
+ })
229
+ .value;
207
230
  return loop(0);
208
231
  };
209
232
  /**
@@ -224,5 +247,9 @@ cas.write(streamFile(path));
224
247
  export const casUpload = (home) => (fileName) => {
225
248
  const src = join(home, 'cas_upload', fileName);
226
249
  const c = fileCas(sha256)(home);
227
- return casAddFile(c)(src).step(okStep(v => rm(src).step(() => pure(ok(v)))));
250
+ return eff(casAddFile(c)(src))
251
+ .step(okStep(v => eff(rm(src))
252
+ .step(() => pure(ok(v)))
253
+ .value))
254
+ .value;
228
255
  };
@@ -1,12 +1,6 @@
1
- import { type FileCasOperation } from './module.f.ts';
2
- import { type Effect } from '../effects/module.f.ts';
3
- import { type ReadFile, type WriteFile, type Rm, type Mkdir } from '../effects/node/module.f.ts';
4
- type TestOp = FileCasOperation | WriteFile | ReadFile | Rm | Mkdir;
5
- declare const testAddBigFile: () => Effect<TestOp, void>;
6
- declare const testAddAndGetBigFile: () => Effect<TestOp, void>;
7
1
  export declare const proof: {
8
- addBigFile: typeof testAddBigFile;
9
- addAndGetBigFile: typeof testAddAndGetBigFile;
2
+ addBigFile: () => void;
3
+ addAndGetBigFile: () => void;
10
4
  casWriteRead: () => void;
11
5
  casReadMissingShard: () => void;
12
6
  casWriteMultiChunk: () => void;
@@ -14,6 +8,7 @@ export declare const proof: {
14
8
  casWriteErrorItemAborts: () => void;
15
9
  casWriteReadExceedsMaxLength: () => void;
16
10
  casWriteGcReclaimsExpired: () => void;
11
+ casWriteGcSkipsLiveLease: () => void;
17
12
  casUploadSuccess: () => void;
18
13
  casUploadFailureKeepsSource: () => void;
19
14
  collectReadDrainsChunks: () => void;
@@ -21,4 +16,3 @@ export declare const proof: {
21
16
  collectReadOverflow: () => void;
22
17
  casListPropagatesNonNotFoundAccessError: () => void;
23
18
  };
24
- export {};
@@ -2,7 +2,7 @@ import { length, maxLength, msb, vec, vec8 } from '../types/bit_vec/module.f.js'
2
2
  import { cBase32ToVec, vecToCBase32 } from '../basen/cbase32/module.f.js';
3
3
  import { computeSync, sha256 } from '../crypto/sha2/module.f.js';
4
4
  import { fileCas, casAddFile, collectRead, casUpload } from './module.f.js';
5
- import { decode, pure } from '../effects/module.f.js';
5
+ import { match, pure, runPure, step } from '../effects/module.f.js';
6
6
  import { mkdir, writeFile, rm, readFile, access } from '../effects/node/module.f.js';
7
7
  import { error, ok } from '../types/result/module.f.js';
8
8
  import { emptyState, virtual } from '../effects/node/virtual/module.f.js';
@@ -10,6 +10,23 @@ import { join } from '../path/module.f.js';
10
10
  import { nonEmpty, empty } from '../effects/list/module.f.js';
11
11
  import { assert, assertEq, assertNotNullish } from '../asserts/module.f.js';
12
12
  const testDir = './test-cas-cli';
13
+ // Names the command a `FileCasOperation` effect stops at, so a proof can assert
14
+ // on it and resume the continuation without reading the `Do` layout. The map
15
+ // has to list every operation the CAS can perform — that is what makes it total,
16
+ // and what makes a new operation a compile error here rather than a silent gap.
17
+ const casCommand = match({
18
+ access: () => 'access',
19
+ createExclusive: () => 'createExclusive',
20
+ mkdir: () => 'mkdir',
21
+ now: () => 'now',
22
+ randomInt: () => 'randomInt',
23
+ readBytes: () => 'readBytes',
24
+ readdir: () => 'readdir',
25
+ rename: () => 'rename',
26
+ rm: () => 'rm',
27
+ stat: () => 'stat',
28
+ writeBytes: () => 'writeBytes',
29
+ });
13
30
  // Create a 128 KiB big file content (at the max Vec size limit)
14
31
  // This tests the boundary where files are at the chunk size limit
15
32
  const createBigFileContent = () => {
@@ -18,68 +35,54 @@ const createBigFileContent = () => {
18
35
  return vec(byteCount * 8n)(0x42424242n);
19
36
  };
20
37
  // Test adding a big file and verifying the hash
21
- const testAddBigFile = () => mkdir(testDir, { recursive: true }).step(() => {
22
- const bigContent = createBigFileContent();
38
+ const testAddBigFile = () => {
23
39
  const bigFilePath = `${testDir}/big-file.bin`;
24
- return writeFile(bigFilePath, bigContent).step(writeRes => {
25
- if (writeRes[0] === 'error') {
26
- throw new Error(`Failed to write test file: ${writeRes[1]}`);
27
- }
28
- const cas = fileCas(sha256)(testDir);
29
- return casAddFile(cas)(bigFilePath).step(addRes => {
30
- if (addRes[0] === 'error') {
31
- throw new Error(`Failed to add file to CAS: ${addRes[1]}`);
32
- }
33
- const hash = addRes[1];
34
- // Verify hash is 256 bits (SHA-256)
35
- if (length(hash) !== 256n) {
36
- throw new Error(`Expected hash length 256 bits, got ${length(hash)}`);
37
- }
38
- // Verify hash can be encoded/decoded
39
- const hashCBase32 = vecToCBase32(hash);
40
- assertNotNullish(cBase32ToVec(hashCBase32), new Error('Failed to decode hash from base32'));
41
- return rm(testDir).step(() => pure(undefined));
42
- });
40
+ const cas = fileCas(sha256)(testDir);
41
+ const x0 = step(mkdir(testDir, { recursive: true }), () => writeFile(bigFilePath, createBigFileContent()));
42
+ const x1 = step(x0, writeRes => {
43
+ assert(writeRes[0] === 'ok', ['failed to write test file', writeRes]);
44
+ return casAddFile(cas)(bigFilePath);
43
45
  });
44
- });
46
+ const x2 = step(x1, addRes => {
47
+ assert(addRes[0] === 'ok', ['failed to add file to CAS', addRes]);
48
+ const hash = addRes[1];
49
+ // Verify hash is 256 bits (SHA-256)
50
+ assertEq(length(hash), 256n, ['expected 256-bit hash', length(hash)]);
51
+ // Verify hash can be encoded/decoded
52
+ assertNotNullish(cBase32ToVec(vecToCBase32(hash)), 'failed to decode hash from cBase32');
53
+ return rm(testDir);
54
+ });
55
+ return step(x2, () => pure(undefined));
56
+ };
45
57
  // Test adding and retrieving a big file
46
- const testAddAndGetBigFile = () => mkdir(testDir, { recursive: true })
47
- .step(() => {
58
+ const testAddAndGetBigFile = () => {
48
59
  const bigContent = createBigFileContent();
49
60
  const bigFilePath = `${testDir}/big-file.bin`;
50
- return writeFile(bigFilePath, bigContent)
51
- .step(writeRes => {
52
- if (writeRes[0] === 'error') {
53
- throw new Error(`Failed to write test file: ${writeRes[1]}`);
54
- }
55
- const cas = fileCas(sha256)(testDir);
56
- return casAddFile(cas)(bigFilePath)
57
- .step(addRes => {
58
- if (addRes[0] === 'error') {
59
- throw new Error(`Failed to add file to CAS: ${addRes[1]}`);
60
- }
61
- const hash = addRes[1];
62
- const storedPath = cas.url(hash);
63
- // Verify file is stored at the expected location
64
- return readFile(storedPath).step(readRes => {
65
- if (readRes[0] === 'error') {
66
- throw new Error(`Failed to read stored file: ${readRes[1]}`);
67
- }
68
- const storedContent = readRes[1];
69
- // Verify content is the same size as original
70
- const storedLen = length(storedContent);
71
- const originalLen = length(bigContent);
72
- if (storedLen !== originalLen) {
73
- throw new Error(`Content size mismatch: stored ${storedLen} bits, expected ${originalLen} bits`);
74
- }
75
- return rm(testDir).step(() => pure(undefined));
76
- });
77
- });
61
+ const cas = fileCas(sha256)(testDir);
62
+ const x0 = step(mkdir(testDir, { recursive: true }), () => writeFile(bigFilePath, bigContent));
63
+ const x1 = step(x0, writeRes => {
64
+ assert(writeRes[0] === 'ok', ['failed to write test file', writeRes]);
65
+ return casAddFile(cas)(bigFilePath);
78
66
  });
79
- });
67
+ // Verify the file is stored at the expected location
68
+ const x2 = step(x1, addRes => {
69
+ assert(addRes[0] === 'ok', ['failed to add file to CAS', addRes]);
70
+ return readFile(cas.url(addRes[1]));
71
+ });
72
+ const x3 = step(x2, readRes => {
73
+ assert(readRes[0] === 'ok', ['failed to read stored file', readRes]);
74
+ // Verify content is the same size as original
75
+ assertEq(length(readRes[1]), length(bigContent), 'stored content size mismatch');
76
+ return rm(testDir);
77
+ });
78
+ return step(x3, () => pure(undefined));
79
+ };
80
80
  export const proof = {
81
- addBigFile: testAddBigFile,
82
- addAndGetBigFile: testAddAndGetBigFile,
81
+ // Both effects must be interpreted, not merely built: an `Effect` is inert
82
+ // data, so returning one from a proof runs none of its continuations and
83
+ // asserts nothing.
84
+ addBigFile: () => { virtual(emptyState)(testAddBigFile()); },
85
+ addAndGetBigFile: () => { virtual(emptyState)(testAddAndGetBigFile()); },
83
86
  //
84
87
  casWriteRead: () => {
85
88
  // Round-trip a single-chunk payload through the real streaming CAS: `write` returns
@@ -92,7 +95,7 @@ export const proof = {
92
95
  const hash = writeResult[1];
93
96
  assertEq(length(hash), 256n, ['expected 256-bit hash', length(hash)]);
94
97
  assertEq(msb.cmp(hash)(computeSync(sha256)([content])), 0, 'write hash mismatch');
95
- const drain = (acc) => (stream) => stream.step((node) => {
98
+ const drain = (acc) => (stream) => step(stream, (node) => {
96
99
  if (node === undefined) {
97
100
  return pure(ok(acc));
98
101
  }
@@ -125,7 +128,7 @@ export const proof = {
125
128
  assert(writeResult[0] === 'ok', ['expected write ok', writeResult]);
126
129
  const hash = writeResult[1];
127
130
  assertEq(msb.cmp(hash)(computeSync(sha256)(chunks)), 0, 'multi-chunk write hash mismatch');
128
- const drain = (acc) => (stream) => stream.step((node) => {
131
+ const drain = (acc) => (stream) => step(stream, (node) => {
129
132
  if (node === undefined) {
130
133
  return pure(ok(acc));
131
134
  }
@@ -181,7 +184,7 @@ export const proof = {
181
184
  const hash = w[1];
182
185
  assertEq(msb.cmp(hash)(computeSync(sha256)(chunks)), 0, 'oversized write hash mismatch');
183
186
  // Fold the read stream straight into a fresh SHA-2 state — never one `Vec`.
184
- const rehash = (state) => (stream) => stream.step((node) => {
187
+ const rehash = (state) => (stream) => step(stream, (node) => {
185
188
  if (node === undefined) {
186
189
  return pure(ok(sha256.end(state)));
187
190
  }
@@ -212,6 +215,23 @@ export const proof = {
212
215
  const [, present] = virtual(state1)(access(stalePath));
213
216
  assert(present[0] === 'error', 'expected GC to reclaim the expired staging file');
214
217
  },
218
+ casWriteGcSkipsLiveLease: () => {
219
+ // A staging file whose deadline is still in the future is left alone by the GC
220
+ // that `write` runs before staging its own file.
221
+ const livePath = join('.', '.cas', '_stage', '0000000000002000000-live');
222
+ const state0 = {
223
+ ...emptyState,
224
+ epochNs: 1_000_000,
225
+ root: { '.cas': { '_stage': { '0000000000002000000-live': [vec8(0x99n)] } } },
226
+ };
227
+ const content = vec8(0x2an);
228
+ const c = fileCas(sha256)('.');
229
+ const x = c.write(nonEmpty(ok(content), empty()));
230
+ const [state1, w] = virtual(state0)(x);
231
+ assert(w[0] === 'ok', ['expected write ok', w]);
232
+ const [, present] = virtual(state1)(access(livePath));
233
+ assert(present[0] === 'ok', 'expected GC to leave the live staging file alone');
234
+ },
215
235
  casUploadSuccess: () => {
216
236
  // A successful upload returns the hash and deletes the source file from cas_upload/.
217
237
  const content = vec8(0x2an);
@@ -239,18 +259,19 @@ export const proof = {
239
259
  // The common path: every chunk is `ok`, so collectRead concatenates them all
240
260
  // and returns the whole blob as one `Vec`.
241
261
  const stream = nonEmpty(ok(vec8(0x11n)), nonEmpty(ok(vec8(0x22n)), empty()));
242
- const d = decode(collectRead(stream));
243
- assert(d.done, 'expected collectRead to finish without issuing a command');
244
- assertEq(d.result[0], 'ok');
262
+ const o = runPure(collectRead(stream));
263
+ assert(o.length === 1, 'expected collectRead to finish without issuing a command');
264
+ assertEq(o[0][0], 'ok');
245
265
  },
246
266
  collectReadPropagatesErrorItem: () => {
247
267
  // An error item mid-stream short-circuits collectRead with that same error.
248
268
  const boom = error('boom');
249
269
  const stream = nonEmpty(ok(vec8(0x11n)), nonEmpty(boom, empty()));
250
- const d = decode(collectRead(stream));
251
- assert(d.done, 'expected collectRead to finish without issuing a command');
252
- assertEq(d.result[0], 'error');
253
- assertEq(d.result[1], 'boom');
270
+ const o = runPure(collectRead(stream));
271
+ assert(o.length === 1, 'expected collectRead to finish without issuing a command');
272
+ const [r] = o;
273
+ assertEq(r[0], 'error');
274
+ assertEq(r[1], 'boom');
254
275
  },
255
276
  // A single `Vec` cannot exceed `maxLength` bits — feed a pure stream whose second
256
277
  // chunk pushes the running total just over the limit so the overflow guard fires
@@ -260,22 +281,21 @@ export const proof = {
260
281
  const v1 = vec(half)(0n);
261
282
  const v2 = vec(half + 1n)(0n);
262
283
  const stream = nonEmpty(ok(v1), nonEmpty(ok(v2), empty()));
263
- const d = decode(collectRead(stream));
264
- assert(d.done, 'expected collectRead to finish without issuing a command');
265
- assertEq(d.result[0], 'error');
284
+ const o = runPure(collectRead(stream));
285
+ assert(o.length === 1, 'expected collectRead to finish without issuing a command');
286
+ assertEq(o[0][0], 'error');
266
287
  },
267
288
  casListPropagatesNonNotFoundAccessError: () => {
268
289
  // A non-ENOENT `access` failure (permissions, corruption) is a genuine storage
269
290
  // error and must propagate out of `list`, not be swallowed as an empty store.
270
291
  const c = fileCas(sha256)('.');
271
292
  const boom = { code: 'EACCES' };
272
- const d = decode(c.list());
273
- assert(!d.done, 'expected list() to issue an access command first');
274
- assertEq(d.command, 'access');
275
- const continuation = d.continuation;
293
+ const r = casCommand(c.list());
294
+ assert(r[0] === 'cont', 'expected list() to issue an access command first');
295
+ assertEq(r[1], 'access');
276
296
  let threw;
277
297
  try {
278
- continuation(error(boom));
298
+ r[2](error(boom));
279
299
  }
280
300
  catch (e) {
281
301
  threw = e;
@@ -5,11 +5,11 @@
5
5
  * @module
6
6
  */
7
7
  import { bun } from '../config/module.f.js';
8
- import { clean, install, test, uses } from '../common/module.f.js';
9
- export const bunSteps = (version) => clean([
8
+ import { install, test, uses } from '../common/module.f.js';
9
+ export const bunSteps = (version) => [
10
10
  install(uses('oven-sh/setup-bun', { 'bun-version': bun })),
11
11
  install({ run: `bun install -g functionalscript@${version}` }),
12
12
  test({ run: 'bun install --frozen-lockfile' }),
13
13
  test({ run: `bunx functionalscript@${version} t` }),
14
14
  test({ run: 'bun test --coverage' }),
15
- ]);
15
+ ];
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Shared CI types and helpers: GitHub Actions step/job RTTI schemas, the
3
3
  * `MetaStep` representation used by tool-specific modules, and assemblers like
4
- * `install`, `test`, `clean`, `ubuntu`, and `toSteps`.
4
+ * `install`, `test`, `ubuntu`, and `toSteps`.
5
5
  *
6
6
  * @module
7
7
  */
@@ -83,7 +83,6 @@ export type MetaStep = {
83
83
  export declare const uses: (name: keyof typeof actions, w?: Record<string, string>) => Step;
84
84
  export declare const install: (step: Step) => MetaStep;
85
85
  export declare const test: (step: Step) => MetaStep;
86
- export declare const clean: (steps: readonly MetaStep[]) => readonly MetaStep[];
87
86
  export declare const toSteps: (m: readonly MetaStep[]) => readonly Step[];
88
87
  export declare const ubuntu: (ms: readonly MetaStep[]) => Job;
89
88
  export declare const ubuntuArm: (ms: readonly MetaStep[]) => Job;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Shared CI types and helpers: GitHub Actions step/job RTTI schemas, the
3
3
  * `MetaStep` representation used by tool-specific modules, and assemblers like
4
- * `install`, `test`, `clean`, `ubuntu`, and `toSteps`.
4
+ * `install`, `test`, `ubuntu`, and `toSteps`.
5
5
  *
6
6
  * @module
7
7
  */
@@ -37,10 +37,6 @@ export const uses = (name, w) => ({
37
37
  });
38
38
  export const install = (step) => ({ type: 'install', step });
39
39
  export const test = (step) => ({ type: 'test', step });
40
- export const clean = (steps) => [
41
- ...steps,
42
- test({ run: 'git reset --hard HEAD && git clean -fdx' })
43
- ];
44
40
  export const toSteps = (m) => {
45
41
  const filter = (st) => m.flatMap((mt) => mt.type === st ? [mt.step] : []);
46
42
  const aptGet = m.flatMap(v => v.type === 'apt-get' ? [v.package] : []).join(' ');
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * Centralized version pins and OS images used by the CI generator: runner
3
- * images, tool versions (Bun, Deno, Playwright, Rust, Node, Wasmtime, Wasmer,
4
- * TSGO).
3
+ * images, tool versions (Bun, Deno, Rust, Node, Wasmtime, Wasmer, TSGO).
5
4
  *
6
5
  * @module
7
6
  */
@@ -19,24 +18,28 @@ export declare const images: {
19
18
  readonly arm: 'windows-11-arm';
20
19
  };
21
20
  };
22
- export declare const functionalscript: '0.37.0';
21
+ export declare const functionalscript: '0.38.0';
23
22
  export declare const bun = "1.3.14";
24
- export declare const deno = "2.9.3";
25
- export declare const playwright = "1.61.1";
23
+ export declare const deno = "2.9.4";
26
24
  export declare const node: {
27
- readonly default: '26.5.0';
25
+ readonly default: '26.5.1';
28
26
  readonly node22: '22.23.1';
29
27
  readonly node24: '24.18.0';
30
28
  };
29
+ export declare const nixpkgs: {
30
+ readonly ref: 'nixos-26.05';
31
+ readonly commit: '21ea275a7c46aef9d4d6ddc962e6d562e9d94183';
32
+ };
31
33
  export declare const wasmtime = "47.0.2";
32
- export declare const wasmer = "7.2.0";
34
+ export declare const wasmer = "7.2.1";
33
35
  export declare const actions: {
34
- readonly 'actions/checkout': 'v7';
36
+ readonly 'actions/checkout': 'v7.0.1';
35
37
  readonly 'actions/setup-node': 'v7.0.0';
36
38
  readonly 'actions/cache': 'v6.1.0';
37
39
  readonly 'denoland/setup-deno': 'v2.0.5';
38
40
  readonly 'oven-sh/setup-bun': 'v2.2.0';
39
41
  readonly 'bytecodealliance/actions/wasmtime/setup': 'v1.1.3';
40
42
  readonly 'wasmerio/setup-wasmer': 'v3.1';
43
+ readonly 'cachix/install-nix-action': 'v31.11.0';
41
44
  readonly 'dtolnay/rust-toolchain': '1.97.1';
42
45
  };
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * Centralized version pins and OS images used by the CI generator: runner
3
- * images, tool versions (Bun, Deno, Playwright, Rust, Node, Wasmtime, Wasmer,
4
- * TSGO).
3
+ * images, tool versions (Bun, Deno, Rust, Node, Wasmtime, Wasmer, TSGO).
5
4
  *
6
5
  * @module
7
6
  */
@@ -24,30 +23,43 @@ export const images = {
24
23
  // published FunctionalScript release; do not tie it to package.json's current
25
24
  // in-repo version.
26
25
  // https://www.npmjs.com/package/functionalscript
27
- export const functionalscript = '0.37.0';
26
+ export const functionalscript = '0.38.0';
28
27
  // https://bun.sh/
29
28
  export const bun = '1.3.14';
30
29
  // https://deno.com/
31
- export const deno = '2.9.3';
32
- // https://www.npmjs.com/package/playwright
33
- export const playwright = '1.61.1';
30
+ export const deno = '2.9.4';
31
+ // The Node versions the pinned Nixpkgs snapshot below provides — read from
32
+ // `pkgs/development/web/nodejs/v{22,24,26}.nix` at that commit. Every runtime
33
+ // uses these: `setup-node` on the GitHub-hosted runners and the generated
34
+ // flakes on the Nix jobs, which assert the version they actually get. Nixpkgs
35
+ // usually trails nodejs.org, so bump the snapshot first and copy the versions
36
+ // it offers rather than the latest release.
34
37
  // https://nodejs.org/en/download
35
38
  export const node = {
36
- default: '26.5.0',
39
+ default: '26.5.1',
37
40
  node22: '22.23.1',
38
41
  node24: '24.18.0',
39
42
  };
43
+ // Official Nixpkgs snapshot used by the generated CI flakes. `ref` is the
44
+ // stable channel the commit is accepted from; `commit` is the exact revision
45
+ // every generated `flake.nix` pins. The Node versions above come from this
46
+ // snapshot, so the two move together.
47
+ // https://channels.nixos.org/nixos-26.05/git-revision
48
+ export const nixpkgs = {
49
+ ref: 'nixos-26.05',
50
+ commit: '21ea275a7c46aef9d4d6ddc962e6d562e9d94183',
51
+ };
40
52
  // https://github.com/bytecodealliance/wasmtime/releases
41
53
  export const wasmtime = '47.0.2';
42
54
  // https://github.com/wasmerio/wasmer/releases
43
- export const wasmer = '7.2.0';
55
+ export const wasmer = '7.2.1';
44
56
  // GitHub Action versions used by CI step builders. The key is the action
45
57
  // `owner/name`; call sites compose the full ref as
46
58
  // `` `${name}@${actions[name]}` ``.
47
59
  // Note: dtolnay/rust-toolchain value is a Rust version, not an action version.
48
60
  export const actions = {
49
61
  // https://github.com/marketplace/actions/checkout
50
- 'actions/checkout': 'v7',
62
+ 'actions/checkout': 'v7.0.1',
51
63
  // https://github.com/marketplace/actions/setup-node-js-environment
52
64
  'actions/setup-node': 'v7.0.0',
53
65
  // https://github.com/marketplace/actions/cache
@@ -60,6 +72,9 @@ export const actions = {
60
72
  'bytecodealliance/actions/wasmtime/setup': 'v1.1.3',
61
73
  // https://github.com/wasmerio/setup-wasmer
62
74
  'wasmerio/setup-wasmer': 'v3.1',
75
+ // https://github.com/marketplace/actions/install-nix
76
+ // Enables the `nix-command` and `flakes` experimental features by default.
77
+ 'cachix/install-nix-action': 'v31.11.0',
63
78
  // https://rust-lang.org/ - value is Rust version, not action version
64
79
  'dtolnay/rust-toolchain': '1.97.1',
65
80
  };