functionalscript 0.33.0 → 0.34.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 (70) hide show
  1. package/README.md +1 -1
  2. package/fs/asserts/module.f.d.ts +1 -1
  3. package/fs/asserts/module.f.js +4 -1
  4. package/fs/bnf/data/module.f.d.ts +0 -103
  5. package/fs/bnf/data/module.f.js +10 -246
  6. package/fs/bnf/data/proof.f.d.ts +0 -10
  7. package/fs/bnf/data/proof.f.js +2 -642
  8. package/fs/bnf/descent/module.f.d.ts +56 -0
  9. package/fs/bnf/descent/module.f.js +119 -0
  10. package/fs/bnf/descent/proof.f.d.ts +5 -0
  11. package/fs/bnf/descent/proof.f.js +296 -0
  12. package/fs/bnf/ll1/module.f.d.ts +72 -0
  13. package/fs/bnf/ll1/module.f.js +153 -0
  14. package/fs/bnf/ll1/proof.f.d.ts +9 -0
  15. package/fs/bnf/ll1/proof.f.js +358 -0
  16. package/fs/cas/cli/module.f.d.ts +6 -0
  17. package/fs/cas/cli/module.f.js +56 -0
  18. package/fs/cas/cli/proof.f.d.ts +14 -0
  19. package/fs/cas/cli/proof.f.js +149 -0
  20. package/fs/cas/mcp/module.f.d.ts +9 -17
  21. package/fs/cas/mcp/module.f.js +116 -84
  22. package/fs/cas/mcp/proof.f.d.ts +5 -1
  23. package/fs/cas/mcp/proof.f.js +74 -32
  24. package/fs/cas/module.f.d.ts +35 -15
  25. package/fs/cas/module.f.js +168 -131
  26. package/fs/cas/proof.f.d.ts +16 -13
  27. package/fs/cas/proof.f.js +255 -121
  28. package/fs/ci/config/module.f.d.ts +14 -14
  29. package/fs/ci/config/module.f.js +14 -14
  30. package/fs/djs/tokenizer-new/module.f.js +1 -1
  31. package/fs/djs/tokenizer-new/proof.f.d.ts +2 -0
  32. package/fs/djs/tokenizer-new/proof.f.js +357 -229
  33. package/fs/effects/list/module.f.d.ts +31 -0
  34. package/fs/effects/list/module.f.js +16 -0
  35. package/fs/effects/module.f.d.ts +11 -5
  36. package/fs/effects/module.f.js +16 -6
  37. package/fs/effects/node/module.f.d.ts +29 -3
  38. package/fs/effects/node/module.f.js +35 -1
  39. package/fs/effects/node/module.js +32 -18
  40. package/fs/effects/node/virtual/module.f.js +57 -0
  41. package/fs/effects/proof.f.d.ts +4 -0
  42. package/fs/effects/proof.f.js +18 -1
  43. package/fs/fjs/module.f.js +2 -8
  44. package/fs/html/module.f.js +6 -8
  45. package/fs/mcp/module.f.d.ts +1 -1
  46. package/fs/mime/module.f.d.ts +101 -5
  47. package/fs/mime/module.f.js +159 -6
  48. package/fs/mime/proof.f.d.ts +25 -0
  49. package/fs/mime/proof.f.js +193 -3
  50. package/fs/text/code_point/module.f.d.ts +39 -2
  51. package/fs/text/code_point/module.f.js +74 -2
  52. package/fs/text/code_point/proof.f.d.ts +8 -0
  53. package/fs/text/code_point/proof.f.js +72 -0
  54. package/fs/text/utf16/module.f.js +4 -26
  55. package/fs/text/utf8/module.f.d.ts +27 -0
  56. package/fs/text/utf8/module.f.js +8 -14
  57. package/fs/types/function/operator/module.f.d.ts +26 -0
  58. package/fs/types/list/module.f.d.ts +3 -0
  59. package/fs/types/result/module.d.ts +2 -4
  60. package/fs/types/result/module.js +9 -3
  61. package/package.json +3 -3
  62. package/issues/demo/data/data.f.js +0 -12
  63. package/issues/demo/data/shared.f.js +0 -3
  64. package/issues/demo/fs/app.js +0 -4
  65. package/issues/demo/fs/math.f.js +0 -4
  66. package/issues/demo/sample/proof.f.js +0 -13
  67. /package/{issues → todo}/031-json.f.d.ts +0 -0
  68. /package/{issues → todo}/031-json.f.js +0 -0
  69. /package/{issues → todo}/proof.f.d.ts +0 -0
  70. /package/{issues → todo}/proof.f.js +0 -0
@@ -0,0 +1,31 @@
1
+ import { type Effect, type Operation } from "../module.f.ts";
2
+ export type NonEmpty<O extends Operation, T> = {
3
+ readonly first: T;
4
+ readonly tail: List<O, T>;
5
+ };
6
+ /**
7
+ * The payload of a `List` effect: the next cons cell, or `undefined` at
8
+ * end-of-stream.
9
+ *
10
+ * Since `Pure<T>` is now itself a lazy thunk (`() => T`), the effect alone is the
11
+ * suspension point and the cell needs no extra wrapping thunk. `Effect<O, Next<O, T>>`
12
+ * is used directly in places where `List<O, T>` cannot be written as a return type
13
+ * (see {@link empty}).
14
+ */
15
+ export type Next<O extends Operation, T> = NonEmpty<O, T> | undefined;
16
+ export type List<O extends Operation, T> = Effect<O, Next<O, T>>;
17
+ /**
18
+ * The empty `List`: a pure end-of-stream marker (`undefined`).
19
+ *
20
+ * The explicit `Effect<O, Next<O, T>>` return type lets the contextual type drive the
21
+ * check, so the recursive payload type-checks without a cast. Construct streams through
22
+ * these two combinators.
23
+ *
24
+ * Note: we use `Effect<O, Next<O, T>>` because TypeScript can't convert `pure(...)` to
25
+ * `List<O, T>`.
26
+ */
27
+ export declare const empty: <O extends Operation, T>() => Effect<O, Next<O, T>>;
28
+ /**
29
+ * Prepends `head` to a `ListEffect` `tail`, as a pure cons cell. See {@link empty}.
30
+ */
31
+ export declare const nonEmpty: <O extends Operation, T>(first: T, tail: List<O, T>) => Effect<O, Next<O, T>>;
@@ -0,0 +1,16 @@
1
+ import { pure } from "../module.f.js";
2
+ /**
3
+ * The empty `List`: a pure end-of-stream marker (`undefined`).
4
+ *
5
+ * The explicit `Effect<O, Next<O, T>>` return type lets the contextual type drive the
6
+ * check, so the recursive payload type-checks without a cast. Construct streams through
7
+ * these two combinators.
8
+ *
9
+ * Note: we use `Effect<O, Next<O, T>>` because TypeScript can't convert `pure(...)` to
10
+ * `List<O, T>`.
11
+ */
12
+ export const empty = () => pure(undefined);
13
+ /**
14
+ * Prepends `head` to a `ListEffect` `tail`, as a pure cons cell. See {@link empty}.
15
+ */
16
+ export const nonEmpty = (first, tail) => pure({ first, tail });
@@ -10,12 +10,19 @@ export type Effect<O extends Operation, T> = {
10
10
  step: <Q extends Operation, R>(f: (p: T) => Effect<Q, R>) => Effect<O | Q, R>;
11
11
  };
12
12
  export type Value<O extends Operation, T> = Pure<T> | Do<O, T>;
13
- export type Pure<T> = readonly [T];
13
+ export type Pure<T> = () => T;
14
14
  export type DoKPR<O extends Operation, T, K extends string, PR extends readonly [unknown, unknown]> = readonly [K, PR[0], (_: PR[1]) => Effect<O, T>];
15
15
  export type Pr<O extends Operation, K extends O[0]> = O extends readonly [K, (...args: infer P) => infer R] ? readonly [P, R] : never;
16
16
  export type DoK<O extends Operation, T, K extends O[0]> = DoKPR<O, T, K, Pr<O, K>>;
17
17
  export type Do<O extends Operation, T> = DoK<O, T, O[0]>;
18
18
  export declare const pure: <T>(v: T) => Effect<never, T>;
19
+ /**
20
+ * A lazy pure effect. Like {@link pure}, but takes a thunk and evaluates it on
21
+ * demand instead of holding an already-computed value. Use this to produce the
22
+ * next item of a list without instantiating the rest — the thunk runs only when
23
+ * the effect is decoded (or stepped into).
24
+ */
25
+ export declare const lazy: <T>(t: () => T) => Effect<never, T>;
19
26
  export declare const doFull: <O extends Operation, T, K extends O[0]>(cmd: K, param: Pr<O, K>[0], cont: (input: Pr<O, K>[1]) => Effect<O, T>) => Effect<O, T>;
20
27
  export type Param<O extends Operation> = F<O>[0];
21
28
  export type Return<O extends Operation> = F<O>[1];
@@ -53,10 +60,10 @@ export type Decoded<O extends Operation, T> = {
53
60
  /**
54
61
  * Decodes an effect's next step: a pure result, or a command to perform.
55
62
  *
56
- * This is the only function that knows how `Value` is laid out (a length-1
57
- * tuple for `Pure`, a `[command, payload, continuation]` tuple for `Do`).
63
+ * This is the only function that knows how `Value` is laid out (a thunk
64
+ * `() => T` for `Pure`, a `[command, payload, continuation]` tuple for `Do`).
58
65
  * Interpreters and proofs must go through `decode` (or `match`) instead of
59
- * inspecting the tuple, so the representation can change without touching
66
+ * inspecting the value, so the representation can change without touching
60
67
  * them.
61
68
  */
62
69
  export declare const decode: <O extends Operation, T>({ value }: Effect<O, T>) => Decoded<O, T>;
@@ -82,4 +89,3 @@ export type ToAsyncOperationMap<O extends Operation> = {
82
89
  };
83
90
  export type F<O extends Operation> = Pr<O, O[0]>;
84
91
  export type Func<O extends Operation> = (..._: Param<O>) => Effect<O, Return<O>>;
85
- export type ListEffect<O extends Operation, T> = Effect<O, readonly [T, ListEffect<O, T>] | undefined>;
@@ -5,9 +5,19 @@
5
5
  */
6
6
  import { fold } from "../types/list/module.f.js";
7
7
  export const pure = (v) => ({
8
- value: [v],
8
+ value: () => v,
9
9
  step: f => f(v)
10
10
  });
11
+ /**
12
+ * A lazy pure effect. Like {@link pure}, but takes a thunk and evaluates it on
13
+ * demand instead of holding an already-computed value. Use this to produce the
14
+ * next item of a list without instantiating the rest — the thunk runs only when
15
+ * the effect is decoded (or stepped into).
16
+ */
17
+ export const lazy = (t) => ({
18
+ value: t,
19
+ step: f => f(t())
20
+ });
11
21
  export const doFull = (cmd, param, cont) => ({
12
22
  value: [cmd, param, cont],
13
23
  step: (f) => doFull(cmd, param, x => cont(x).step(f)),
@@ -32,14 +42,14 @@ export const forEachStep = (f) => (items) => foldStep((item) => () => f(item))(u
32
42
  /**
33
43
  * Decodes an effect's next step: a pure result, or a command to perform.
34
44
  *
35
- * This is the only function that knows how `Value` is laid out (a length-1
36
- * tuple for `Pure`, a `[command, payload, continuation]` tuple for `Do`).
45
+ * This is the only function that knows how `Value` is laid out (a thunk
46
+ * `() => T` for `Pure`, a `[command, payload, continuation]` tuple for `Do`).
37
47
  * Interpreters and proofs must go through `decode` (or `match`) instead of
38
- * inspecting the tuple, so the representation can change without touching
48
+ * inspecting the value, so the representation can change without touching
39
49
  * them.
40
50
  */
41
- export const decode = ({ value }) => value.length === 1
42
- ? { done: true, result: value[0] }
51
+ export const decode = ({ value }) => typeof value === 'function'
52
+ ? { done: true, result: value() }
43
53
  : { done: false, command: value[0], payload: value[1], continuation: value[2] };
44
54
  /**
45
55
  * Decodes an effect's next step and dispatches its command to `map`,
@@ -1,9 +1,10 @@
1
- import type { Vec } from '../../types/bit_vec/module.f.ts';
1
+ import { type Vec } from '../../types/bit_vec/module.f.ts';
2
2
  import type { MemOp } from '../memory/module.f.ts';
3
3
  import type { Nominal } from '../../types/nominal/module.f.ts';
4
4
  import { type Result } from '../../types/result/module.f.ts';
5
5
  import type { StringMap } from '../../types/object/module.f.ts';
6
6
  import { type Effect, type Func, type Operation, type ToAsyncOperationMap } from '../module.f.ts';
7
+ import type { List } from '../list/module.f.ts';
7
8
  export type IoResult<T> = Result<T, unknown>;
8
9
  /**
9
10
  * True if `e` is a "file or directory does not exist" (`ENOENT`) error.
@@ -81,7 +82,32 @@ export type Exec = readonly ['exec', (command: string, stdin?: string) => IoResu
81
82
  export declare const exec: Func<Exec>;
82
83
  export type Access = readonly ['access', (path: string) => IoResult<void>];
83
84
  export declare const access: Func<Access>;
84
- export type Fs = Mkdir | ReadFile | ReadBytes | Readdir | WriteFile | Rm | Rename | Exec | Access;
85
+ /**
86
+ * Creates `path` as an empty file with `O_CREAT|O_EXCL` — fails if it already
87
+ * exists. This is the exclusive create that claims a staging name in the
88
+ * lock-free upload ([staging-lease.md](../../../issues/cas/staging-lease.md));
89
+ * with 256 random bits in the name `EEXIST` never happens in practice, so it
90
+ * is just a sanity guard.
91
+ */
92
+ export type CreateExclusive = readonly ['createExclusive', (path: string) => IoResult<void>];
93
+ export declare const createExclusive: Func<CreateExclusive>;
94
+ /**
95
+ * Writes the **entire** `data` vector to an **existing** `path` at byte `offset`
96
+ * (positional write). The mirror of {@link readBytes}: it never creates the file
97
+ * (a missing path is `ENOENT`), and it writes every byte or returns an error —
98
+ * the runner loops over short writes — so a later size check can never pass over
99
+ * a hole. Bounded to ≤128 KiB per call, like `readBytes`.
100
+ */
101
+ export type WriteBytes = readonly ['writeBytes', (path: string, offset: number, data: Vec) => IoResult<void>];
102
+ export declare const writeBytes: Func<WriteBytes>;
103
+ export declare const writeFromStream: <O extends Operation>(path: string, e: List<O, IoResult<Vec>>) => Effect<O | WriteBytes | CreateExclusive, IoResult<void>>;
104
+ /** File metadata returned by {@link stat}. Only `size` (in bytes) for now. */
105
+ export type FileStat = {
106
+ readonly size: number;
107
+ };
108
+ export type Stat = readonly ['stat', (path: string) => IoResult<FileStat>];
109
+ export declare const stat: Func<Stat>;
110
+ export type Fs = Mkdir | ReadFile | ReadBytes | Readdir | WriteFile | Rm | Rename | Exec | Access | CreateExclusive | WriteBytes | Stat;
85
111
  export type Server = Nominal<'server', `160855c4f69310fece3273c1853ac32de43dee1eb41bf59d821917f8eebe9272`, unknown>;
86
112
  export type Headers = StringMap<string, string>;
87
113
  export type IncomingMessage = {
@@ -216,7 +242,7 @@ export type TestContext = {
216
242
  /** Effect operation that registers a named test with the active `TestContext`. */
217
243
  export type Test = readonly ['test', (ctx: TestContext, name: string, expectFailure: boolean, test: (t: TestContext) => Effect<Test | All | Await, void>) => void];
218
244
  export declare const test: Func<Test>;
219
- export type NodeOp = All | Await | Fetch | Fs | Http | Forever | Import | MemOp | Now | RandomInt | Read | Sandbox | Write | Test;
245
+ export type NodeOp = Access | All | Await | Fetch | Fs | Http | Forever | Import | MemOp | Now | RandomInt | Read | Sandbox | Write | Test;
220
246
  export type NodeEffect<T> = Effect<NodeOp, T>;
221
247
  /**
222
248
  * Writes an error line to `stderr` and yields exit code `1`. The canonical
@@ -12,7 +12,8 @@ import { utf8, utf8ToString } from "../../text/module.f.js";
12
12
  import { toCodePointList } from "../../text/utf8/module.f.js";
13
13
  import { codePointListToString } from "../../text/utf16/module.f.js";
14
14
  import { reverse } from "../../types/list/module.f.js";
15
- import { ok } from "../../types/result/module.f.js";
15
+ import { length } from "../../types/bit_vec/module.f.js";
16
+ import { ok, error as resultError } from "../../types/result/module.f.js";
16
17
  import { do_, pure } from "../module.f.js";
17
18
  /**
18
19
  * True if `e` is a "file or directory does not exist" (`ENOENT`) error.
@@ -53,6 +54,39 @@ export const readBytes = do_('readBytes');
53
54
  export const randomInt = do_('randomInt');
54
55
  export const exec = do_('exec');
55
56
  export const access = do_('access');
57
+ export const createExclusive = do_('createExclusive');
58
+ export const writeBytes = do_('writeBytes');
59
+ const writeLoop = (path) => {
60
+ const f = (offset, e) => e.step(r => {
61
+ if (r === undefined) {
62
+ return pure(ok(undefined));
63
+ }
64
+ const { first: [t, v], tail } = r;
65
+ if (t === 'error') {
66
+ return pure(resultError(v));
67
+ }
68
+ const lenV = length(v);
69
+ if ((lenV & 7n) !== 0n) {
70
+ return pure(resultError('invalid buffer size'));
71
+ }
72
+ return writeBytes(path, offset, v)
73
+ .step((r) => {
74
+ if (r[0] === 'error') {
75
+ return pure(r);
76
+ }
77
+ return f(offset + Number(lenV >> 3n), tail);
78
+ });
79
+ });
80
+ return f;
81
+ };
82
+ export const writeFromStream = (path, e) => createExclusive(path)
83
+ .step(([r, v]) => {
84
+ if (r === 'error') {
85
+ return pure(resultError(v));
86
+ }
87
+ return writeLoop(path)(0, e);
88
+ });
89
+ export const stat = do_('stat');
56
90
  export const createServer = do_('createServer');
57
91
  export const listen = do_('listen');
58
92
  export const forever = do_('forever');
@@ -35,6 +35,7 @@ import { memoryOperationMap } from "./memory/module.js";
35
35
  import {} from "./module.f.js";
36
36
  import { asBase, asNominal } from "../../types/nominal/module.f.js";
37
37
  import { error, ok } from "../../types/result/module.f.js";
38
+ import { asyncTryCatch } from "../../types/result/module.js";
38
39
  import { fromVec, listToVec, toVec } from "../../types/uint8array/module.f.js";
39
40
  import { maxLengthBytes } from "../../types/bit_vec/module.f.js";
40
41
  /**
@@ -44,14 +45,6 @@ import { maxLengthBytes } from "../../types/bit_vec/module.f.js";
44
45
  * requires them present; this local view keeps the narrowing in one place.
45
46
  */
46
47
  const createServer = http.createServer;
47
- const tc = async (f) => {
48
- try {
49
- return ok(await f());
50
- }
51
- catch (e) {
52
- return error(e);
53
- }
54
- };
55
48
  const collect = async (v) => {
56
49
  let result = [];
57
50
  for await (const a of v) {
@@ -156,15 +149,15 @@ const readStdinByte = async () => {
156
149
  const runNodeEffect = asyncRun({
157
150
  ...memoryOperationMap(),
158
151
  all: async (...effects) => await Promise.all(effects.map(runNodeEffect)),
159
- fetch: async (url) => tc(async () => {
152
+ fetch: async (url) => asyncTryCatch(async () => {
160
153
  const response = await fetch(url);
161
154
  if (!response.ok) {
162
155
  throw new Error(`Fetch error: ${response.status} ${response.statusText}`);
163
156
  }
164
157
  return toVec(new Uint8Array(await response.arrayBuffer()));
165
158
  }),
166
- mkdir: (...p) => tc(async () => { await mkdir(...p); }),
167
- readFile: path => tc(async () => {
159
+ mkdir: (...p) => asyncTryCatch(async () => { await mkdir(...p); }),
160
+ readFile: path => asyncTryCatch(async () => {
168
161
  const fileStats = await stat(path);
169
162
  // if the file is too big, toVec should fail anyway but in this case we don't want to load the file.
170
163
  if (fileStats.size > maxFileSizeBytes) {
@@ -172,16 +165,16 @@ const runNodeEffect = asyncRun({
172
165
  }
173
166
  return toVec(await readFile(path));
174
167
  }),
175
- readdir: (path, r) => tc(async () => (await readdir(path, { ...r, withFileTypes: true }))
168
+ readdir: (path, r) => asyncTryCatch(async () => (await readdir(path, { ...r, withFileTypes: true }))
176
169
  .map(v => ({
177
170
  name: v.name,
178
171
  parentPath: normalize(v.parentPath),
179
172
  isFile: v.isFile()
180
173
  }))),
181
- writeFile: (path, data) => tc(() => writeFile(path, fromVec(data))),
182
- rm: path => tc(() => rm(path)),
183
- rename: (src, dst) => tc(() => rename(src, dst)),
184
- readBytes: (path, offset, size) => tc(async () => {
174
+ writeFile: (path, data) => asyncTryCatch(() => writeFile(path, fromVec(data))),
175
+ rm: path => asyncTryCatch(() => rm(path)),
176
+ rename: (src, dst) => asyncTryCatch(() => rename(src, dst)),
177
+ readBytes: (path, offset, size) => asyncTryCatch(async () => {
185
178
  if (offset < 0) {
186
179
  throw new Error(`Offset ${offset} is negative`);
187
180
  }
@@ -199,8 +192,29 @@ const runNodeEffect = asyncRun({
199
192
  }
200
193
  }),
201
194
  randomInt: async () => crypto.randomInt(2 ** 32),
202
- access: path => tc(() => access(path)),
203
- import: path => tc(() => asyncImport(path)),
195
+ access: path => asyncTryCatch(() => access(path)),
196
+ createExclusive: path => asyncTryCatch(async () => {
197
+ const fh = await open(path, 'wx');
198
+ await fh.close();
199
+ }),
200
+ writeBytes: (path, offset, data) => asyncTryCatch(async () => {
201
+ const fh = await open(path, 'r+');
202
+ try {
203
+ const buffer = fromVec(data);
204
+ // Loop over short writes so the whole Vec lands — a partial pwrite would
205
+ // leave a hole the publish-time size check could pass over.
206
+ let written = 0;
207
+ while (written < buffer.length) {
208
+ const { bytesWritten } = await fh.write(buffer, written, buffer.length - written, offset + written);
209
+ written += bytesWritten;
210
+ }
211
+ }
212
+ finally {
213
+ await fh.close();
214
+ }
215
+ }),
216
+ stat: path => asyncTryCatch(async () => ({ size: (await stat(path)).size })),
217
+ import: path => asyncTryCatch(() => asyncImport(path)),
204
218
  exec: (command, stdin) => new Promise(resolve => {
205
219
  const child = exec(command, (e, stdout, stderr) => resolve(e !== null ? ['error', e] : ok({ stdout, stderr })));
206
220
  child.stdin?.end(stdin);
@@ -290,6 +290,60 @@ const readBytesOp = (path, offset, size) => readOperation((dir, p) => {
290
290
  }
291
291
  return ok(result);
292
292
  })(path);
293
+ /** Total byte size of a chunk-list file (each chunk is byte-aligned). */
294
+ const fileSizeBytes = (chunks) => chunks.reduce((acc, c) => acc + Number(length(c) / 8n), 0);
295
+ /** Absent-path error for an already-existing exclusive create, mirroring `EEXIST`. */
296
+ const eexist = error({ code: 'EEXIST' });
297
+ const createExclusive = operation((dir, path) => {
298
+ if (path.length !== 1) {
299
+ return [dir, invalidPath];
300
+ }
301
+ const [name] = path;
302
+ // O_EXCL: fail if the name is already taken; otherwise create an empty file.
303
+ if (dir[name] !== undefined) {
304
+ return [dir, eexist];
305
+ }
306
+ return [{ ...dir, [name]: [] }, okVoid];
307
+ });
308
+ // The lock-free upload only ever writes sequentially at the current end of the
309
+ // staging file (`offset === size`), so the virtual model implements that append
310
+ // case exactly: it never creates (a missing file is `ENOENT`), never overwrites
311
+ // existing bytes, and never leaves a hole — matching the effect's contract for
312
+ // the one access pattern its callers use.
313
+ const writeBytesOp = (path, offset, data) => operation((dir, p) => {
314
+ if (p.length !== 1) {
315
+ return [dir, enoent];
316
+ }
317
+ const [name] = p;
318
+ const file = dir[name];
319
+ if (file === undefined) {
320
+ return [dir, enoent];
321
+ } // writeBytes never creates
322
+ if (!Array.isArray(file)) {
323
+ return [dir, error(`'${name}' is not a file`)];
324
+ }
325
+ if (!Number.isInteger(offset) || offset < 0) {
326
+ return [dir, error(`Offset ${offset} is invalid`)];
327
+ }
328
+ const chunks = file;
329
+ if (offset !== fileSizeBytes(chunks)) {
330
+ return [dir, error(`writeBytes offset ${offset} must equal the file size (append-only)`)];
331
+ }
332
+ return [{ ...dir, [name]: [...chunks, data] }, okVoid];
333
+ })(path);
334
+ const statOp = readOperation((dir, path) => {
335
+ if (path.length !== 1) {
336
+ return enoent;
337
+ }
338
+ const file = dir[path[0]];
339
+ if (file === undefined) {
340
+ return enoent;
341
+ }
342
+ if (!Array.isArray(file)) {
343
+ return error(`'${path[0]}' is not a file`);
344
+ }
345
+ return ok({ size: fileSizeBytes(file) });
346
+ });
293
347
  const map = {
294
348
  all: (...a) => state => {
295
349
  let e = [];
@@ -330,6 +384,9 @@ const map = {
330
384
  rm,
331
385
  rename,
332
386
  readBytes: readBytesOp,
387
+ createExclusive,
388
+ writeBytes: writeBytesOp,
389
+ stat: statOp,
333
390
  randomInt: () => state => [{ ...state, randomNext: state.randomNext + 1 }, state.randomNext],
334
391
  exec: todo,
335
392
  createServer: todo,
@@ -1,4 +1,8 @@
1
1
  export declare const proof: {
2
+ lazy: {
3
+ value: () => void;
4
+ deferred: () => void;
5
+ };
2
6
  foldStep: {
3
7
  empty: () => void;
4
8
  threadsState: () => void;
@@ -1,4 +1,4 @@
1
- import { decode, do_, foldStep, forEachStep, match, pure } from "./module.f.js";
1
+ import { decode, do_, foldStep, forEachStep, lazy, match, pure } from "./module.f.js";
2
2
  const assertPure = (e, expected) => {
3
3
  const d = decode(e);
4
4
  if (!d.done) {
@@ -10,6 +10,23 @@ const assertPure = (e, expected) => {
10
10
  };
11
11
  const next = match({ add: (a, b) => a + b });
12
12
  export const proof = {
13
+ lazy: {
14
+ value: () => {
15
+ assertPure(lazy(() => 42), 42);
16
+ },
17
+ deferred: () => {
18
+ // The thunk runs only when the effect is decoded, not when `lazy` is called.
19
+ let evaluated = false;
20
+ const e = lazy(() => { evaluated = true; return 7; });
21
+ if (evaluated) {
22
+ throw 'lazy must not evaluate eagerly';
23
+ }
24
+ assertPure(e, 7);
25
+ if (!evaluated) {
26
+ throw 'decode must force the thunk';
27
+ }
28
+ },
29
+ },
13
30
  foldStep: {
14
31
  empty: () => {
15
32
  const e = foldStep((x) => (s) => pure(s + x))(10)([]);
@@ -5,14 +5,11 @@
5
5
  */
6
6
  import { compile } from "../djs/module.f.js";
7
7
  import { main as testMain } from "../emergent_testing/module.f.js";
8
- import { commands as casCommands, fileCas } from "../cas/module.f.js";
8
+ import { commands as casCommands } from "../cas/cli/module.f.js";
9
9
  import { main as ciMain } from "../ci/module.f.js";
10
10
  import { import_ } from "../effects/node/module.f.js";
11
11
  import { dispatch } from "../cli/module.f.js";
12
12
  import { casMcpServer } from "../cas/mcp/module.f.js";
13
- import { toPath } from "../cas/module.f.js";
14
- import { sha256 } from "../crypto/sha2/module.f.js";
15
- import { join } from "../path/module.f.js";
16
13
  import { pure } from "../effects/module.f.js";
17
14
  const commands = [
18
15
  {
@@ -33,10 +30,7 @@ const commands = [
33
30
  {
34
31
  names: ['mcp', 'm'],
35
32
  description: 'Run an MCP server over stdio exposing the CAS as tools',
36
- handler: ({ home }) => {
37
- const c = fileCas(sha256)(home);
38
- return casMcpServer(c, home, hash => join(home, toPath(hash))).step(() => pure(0));
39
- },
33
+ handler: ({ home }) => casMcpServer(home).step(() => pure(0)),
40
34
  },
41
35
  {
42
36
  names: ['ci', 'i'],
@@ -46,15 +46,13 @@ const rawText = [
46
46
  /**
47
47
  * https://stackoverflow.com/questions/7381974/which-characters-need-to-be-escaped-in-html
48
48
  */
49
- const escapeCharCode = (code) => {
50
- switch (code) {
51
- case quotationMark: return '&quot;';
52
- case ampersand: return '&amp;';
53
- case lessThanSign: return '&lt;';
54
- case greaterThanSign: return '&gt;';
55
- default: return fromCharCode(code);
56
- }
49
+ const escapeTable = {
50
+ [quotationMark]: '&quot;',
51
+ [ampersand]: '&amp;',
52
+ [lessThanSign]: '&lt;',
53
+ [greaterThanSign]: '&gt;',
57
54
  };
55
+ const escapeCharCode = (code) => escapeTable[code] ?? fromCharCode(code);
58
56
  const escape = compose(stringToList)(map(escapeCharCode));
59
57
  const node = (n) => typeof n === 'string' ? escape(n) : element(n);
60
58
  const nodes = flatMap(node);
@@ -229,4 +229,4 @@ export type McpConfig = {
229
229
  * - `tools/list` params (an optional pagination `cursor`) are validated and passed
230
230
  * to the handler; invalid params → -32602.
231
231
  */
232
- export declare const mcpStep: <O extends Operation>({ protocolVersion, capabilities, serverInfo, }: McpConfig) => (handlers: McpHandlers<O>) => (stateKey: Key<McpSessionState>) => (value: Unknown) => Effect<MemOp | O, Response | null>;
232
+ export declare const mcpStep: ({ protocolVersion, capabilities, serverInfo, }: McpConfig) => <O extends Operation>(handlers: McpHandlers<O>) => (stateKey: Key<McpSessionState>) => (value: Unknown) => Effect<MemOp | O, Response | null>;
@@ -1,10 +1,15 @@
1
1
  /**
2
2
  * Magic-byte MIME type detection.
3
3
  *
4
- * A pure table lookup over the leading bytes of a `Vec`: no I/O, no
5
- * dependencies beyond `fs/types/bit_vec`. `detect` returns a MIME type string
6
- * for the container formats whose signatures it knows, or `null` for anything
7
- * else — text, unknown binary, or a `Vec` too short to match.
4
+ * `detect` is a pure table lookup over the leading bytes of a `Vec`: it returns a
5
+ * MIME type string for the container formats whose signatures it knows, or `null`
6
+ * for anything else text, unknown binary, or a `Vec` too short to match.
7
+ *
8
+ * Beside it, `detectStream` is the **streaming counterpart**: a byte-accepting
9
+ * state machine (length × magic-byte eliminator × UTF-8 validity DFA) that derives
10
+ * `{ length, mime_type, type }` by folding a CAS read stream in O(1) space, without
11
+ * ever buffering the blob into a single `maxLength`-bounded `Vec`. See the README
12
+ * for the factored design.
8
13
  *
9
14
  * The CAS store is type-agnostic and keeps raw bytes only, so type is never
10
15
  * stored; it is recovered on read by sniffing the content. Callers decide what
@@ -29,11 +34,102 @@
29
34
  */
30
35
  import { type Vec } from '../types/bit_vec/module.f.ts';
31
36
  import type { Nullable } from '../types/nullable/module.f.ts';
37
+ import { type Effect, type Operation } from '../effects/module.f.ts';
38
+ import type { List } from '../effects/list/module.f.ts';
39
+ import type { IoResult } from '../effects/node/module.f.ts';
40
+ import { type Utf8State } from '../text/utf8/module.f.ts';
32
41
  /**
33
42
  * Detects the MIME type of `bytes` from its leading magic-byte signature.
34
43
  *
35
- * @returns the MIME type string for a recognised format, or `null` when the
44
+ * @returns the MIME type string for a recognized format, or `null` when the
36
45
  * leading bytes match no known signature (including any `Vec` shorter than
37
46
  * the signature it might otherwise match).
38
47
  */
39
48
  export declare const detect: (bytes: Vec) => Nullable<string>;
49
+ /**
50
+ * A magic-byte signature as a byte pattern. `null` entries are wildcards (the
51
+ * four little-endian size bytes of WebP, between its `RIFF` and `WEBP` markers).
52
+ */
53
+ type Signature = {
54
+ readonly pattern: readonly Nullable<number>[];
55
+ readonly mime: string;
56
+ };
57
+ /**
58
+ * `A_magic`: signature elimination. `scan` holds the byte offset and the still-viable
59
+ * signatures; a fully matched signature absorbs into `matched`, an empty viable set
60
+ * into `dead`. Settles within 12 bytes — `matched`/`dead` are absorbing.
61
+ */
62
+ type MagicState = {
63
+ readonly tag: 'scan';
64
+ readonly pos: number;
65
+ readonly viable: readonly Signature[];
66
+ } | {
67
+ readonly tag: 'matched';
68
+ readonly mime: string;
69
+ } | {
70
+ readonly tag: 'dead';
71
+ };
72
+ /**
73
+ * `A_utf8`: a streaming UTF-8 validity-and-text check riding the shared
74
+ * `utf8ByteToCodePointOp` decoder. `st` is the decoder's mid-sequence state;
75
+ * `valid` is `false` once an illegal byte, surrogate, or out-of-range code point
76
+ * is seen — `valid: false` is absorbing. A non-null `st` at EOF (a truncated
77
+ * multi-byte sequence) is invalid. `text` is the orthogonal text-ness verdict: it
78
+ * is `false` once a non-text (control) code point is decoded, even though that
79
+ * code point is perfectly well-formed UTF-8 — `text: false` is absorbing too.
80
+ * Keeping the two distinct lets a valid-but-control blob (e.g. NUL) decode
81
+ * cleanly yet still classify as binary.
82
+ */
83
+ type Utf8Detect = {
84
+ readonly st: Utf8State;
85
+ readonly valid: boolean;
86
+ readonly text: boolean;
87
+ };
88
+ /**
89
+ * The product state: running bit length × magic eliminator × UTF-8 validator.
90
+ * The factors never read each other; they meet only in {@link finish}.
91
+ */
92
+ export type DetectState = {
93
+ readonly length: bigint;
94
+ readonly magic: MagicState;
95
+ readonly utf8: Utf8Detect;
96
+ };
97
+ /** The initial detector state `q₀`. */
98
+ export declare const detectInit: DetectState;
99
+ /**
100
+ * Folds one `Vec` chunk into the detector state (`δ` over a whole chunk). Length
101
+ * always advances by the chunk's bit length; per-byte iteration stops as soon as
102
+ * the verdict is fixed (see {@link isSettled}), so large blobs — including large
103
+ * magic-matched ones — cost ≈ length counting.
104
+ */
105
+ export declare const push: (s: DetectState) => (chunk: Vec) => DetectState;
106
+ /** The metadata read off the detector at end-of-stream. */
107
+ export type DetectMeta = {
108
+ readonly length: bigint;
109
+ readonly mime_type: string;
110
+ readonly type: 'text' | 'base64';
111
+ };
112
+ /**
113
+ * Reads the answer off the final state (`λ`). Reproduces the three-way result of
114
+ * the pure path: magic hit → `base64` + detected mime; else whole-blob-valid UTF-8
115
+ * that is also all-text (byte-aligned, no invalidity, no control bytes) → `text` +
116
+ * `text/plain`; else → `base64` + `application/octet-stream`. A valid-but-control
117
+ * blob (NUL, other controls) is well-formed UTF-8 yet falls through to the binary
118
+ * branch.
119
+ */
120
+ export declare const finish: (s: DetectState) => DetectMeta;
121
+ /**
122
+ * Classifies a whole `Vec` with the same state machine as {@link detectStream}.
123
+ * The single-buffer counterpart for callers that already hold the bytes (the
124
+ * `cas_get` `content: true` path materializes the blob anyway): both paths read
125
+ * the three-way `{ length, mime_type, type }` verdict from one machine instead of
126
+ * re-deriving it from `detect` + a separate UTF-8 check.
127
+ */
128
+ export declare const detectVec: (bytes: Vec) => DetectMeta;
129
+ /**
130
+ * Folds a CAS read stream through {@link push} and reads {@link finish} at EOF,
131
+ * deriving `cas_get` metadata without ever materializing the blob. A read `error`
132
+ * item short-circuits into the `IoResult` error.
133
+ */
134
+ export declare const detectStream: <O extends Operation>(stream: List<O, IoResult<Vec>>) => Effect<O, IoResult<DetectMeta>>;
135
+ export {};