functionalscript 0.29.1 → 0.31.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 (191) hide show
  1. package/fs/asn.1/module.f.js +18 -22
  2. package/fs/asn.1/proof.f.d.ts +5 -0
  3. package/fs/asn.1/proof.f.js +38 -2
  4. package/fs/base64/module.f.d.ts +9 -0
  5. package/fs/base64/module.f.js +60 -0
  6. package/fs/base64/proof.f.d.ts +11 -0
  7. package/fs/base64/proof.f.js +117 -0
  8. package/fs/base_n/module.f.d.ts +44 -0
  9. package/fs/base_n/module.f.js +54 -0
  10. package/fs/base_n/proof.f.d.ts +10 -0
  11. package/fs/base_n/proof.f.js +86 -0
  12. package/fs/bnf/data/module.f.d.ts +4 -9
  13. package/fs/bnf/data/module.f.js +4 -3
  14. package/fs/bnf/module.f.d.ts +3 -4
  15. package/fs/bnf/module.f.js +3 -2
  16. package/fs/bnf/proof.f.d.ts +9 -0
  17. package/fs/bnf/proof.f.js +98 -1
  18. package/fs/cas/mcp/module.f.d.ts +40 -0
  19. package/fs/cas/mcp/module.f.js +223 -0
  20. package/fs/cas/mcp/proof.f.d.ts +30 -0
  21. package/fs/cas/mcp/proof.f.js +310 -0
  22. package/fs/cas/module.f.d.ts +9 -3
  23. package/fs/cas/module.f.js +104 -69
  24. package/fs/cas/proof.f.d.ts +2 -0
  25. package/fs/cas/proof.f.js +30 -1
  26. package/fs/cbase32/module.f.d.ts +1 -1
  27. package/fs/cbase32/module.f.js +19 -40
  28. package/fs/cbase32/proof.f.d.ts +1 -0
  29. package/fs/cbase32/proof.f.js +15 -0
  30. package/fs/ci/bun/module.f.d.ts +2 -2
  31. package/fs/ci/bun/module.f.js +7 -14
  32. package/fs/ci/common/module.f.d.ts +3 -1
  33. package/fs/ci/common/module.f.js +9 -2
  34. package/fs/ci/config/module.f.d.ts +7 -5
  35. package/fs/ci/config/module.f.js +11 -5
  36. package/fs/ci/deno/module.f.d.ts +1 -1
  37. package/fs/ci/deno/module.f.js +7 -5
  38. package/fs/ci/module.f.d.ts +6 -3
  39. package/fs/ci/module.f.js +24 -54
  40. package/fs/ci/node/module.f.d.ts +3 -3
  41. package/fs/ci/node/module.f.js +30 -26
  42. package/fs/ci/playwright/module.f.js +1 -1
  43. package/fs/ci/proof.f.d.ts +6 -7
  44. package/fs/ci/proof.f.js +63 -36
  45. package/fs/ci/rust/module.f.d.ts +2 -1
  46. package/fs/ci/rust/module.f.js +33 -16
  47. package/fs/cli/module.f.d.ts +3 -1
  48. package/fs/cli/module.f.js +22 -12
  49. package/fs/cli/proof.f.d.ts +5 -0
  50. package/fs/cli/proof.f.js +76 -0
  51. package/fs/crypto/hmac/proof.f.d.ts +1 -0
  52. package/fs/crypto/hmac/proof.f.js +8 -0
  53. package/fs/crypto/secp/proof.f.d.ts +1 -0
  54. package/fs/crypto/secp/proof.f.js +10 -0
  55. package/fs/crypto/sign/module.f.js +1 -4
  56. package/fs/dev/module.f.d.ts +5 -3
  57. package/fs/dev/module.f.js +13 -2
  58. package/fs/dev/package_json/proof.f.d.ts +1 -0
  59. package/fs/dev/package_json/proof.f.js +5 -0
  60. package/fs/dev/proof.f.d.ts +1 -0
  61. package/fs/dev/proof.f.js +12 -1
  62. package/fs/djs/ast/module.f.d.ts +1 -1
  63. package/fs/djs/ast/proof.f.d.ts +5 -0
  64. package/fs/djs/ast/proof.f.js +17 -0
  65. package/fs/djs/module.f.d.ts +1 -1
  66. package/fs/djs/module.f.js +2 -3
  67. package/fs/djs/serializer/module.f.js +24 -11
  68. package/fs/djs/transpiler/module.f.js +3 -4
  69. package/fs/effects/memory/module.f.d.ts +37 -0
  70. package/fs/effects/memory/module.f.js +24 -0
  71. package/fs/effects/memory/proof.f.d.ts +6 -0
  72. package/fs/effects/memory/proof.f.js +64 -0
  73. package/fs/effects/mock/module.f.d.ts +7 -2
  74. package/fs/effects/mock/module.f.js +12 -8
  75. package/fs/effects/module.d.ts +1 -1
  76. package/fs/effects/module.f.d.ts +41 -0
  77. package/fs/effects/module.f.js +25 -0
  78. package/fs/effects/module.js +6 -7
  79. package/fs/effects/node/memory/module.d.ts +20 -0
  80. package/fs/effects/node/memory/module.js +44 -0
  81. package/fs/effects/node/memory/proof.d.ts +10 -0
  82. package/fs/effects/node/memory/proof.js +35 -0
  83. package/fs/effects/node/module.d.ts +1 -1
  84. package/fs/effects/node/module.f.d.ts +54 -8
  85. package/fs/effects/node/module.f.js +55 -4
  86. package/fs/effects/node/module.js +166 -135
  87. package/fs/effects/node/proof.f.d.ts +10 -0
  88. package/fs/effects/node/proof.f.js +88 -21
  89. package/fs/effects/node/virtual/module.f.d.ts +6 -0
  90. package/fs/effects/node/virtual/module.f.js +48 -16
  91. package/fs/effects/node/virtual/proof.f.d.ts +17 -0
  92. package/fs/effects/node/virtual/proof.f.js +79 -0
  93. package/fs/effects/proof.f.d.ts +5 -0
  94. package/fs/effects/proof.f.js +54 -34
  95. package/fs/emergent_testing/module.f.d.ts +6 -1
  96. package/fs/emergent_testing/module.f.js +8 -3
  97. package/fs/emergent_testing/proof.f.d.ts +11 -0
  98. package/fs/emergent_testing/proof.f.js +52 -16
  99. package/fs/fjs/module.f.js +2 -2
  100. package/fs/fjs/proof.f.d.ts +8 -0
  101. package/fs/fjs/proof.f.js +39 -0
  102. package/fs/fsc/module.f.d.ts +5 -0
  103. package/fs/fsc/module.f.js +17 -27
  104. package/fs/fsc/proof.f.d.ts +8 -0
  105. package/fs/fsc/proof.f.js +48 -2
  106. package/fs/fsm/module.f.d.ts +2 -3
  107. package/fs/fsm/module.f.js +1 -1
  108. package/fs/html/module.f.d.ts +2 -3
  109. package/fs/html/module.f.js +7 -6
  110. package/fs/html/proof.f.d.ts +1 -0
  111. package/fs/html/proof.f.js +7 -0
  112. package/fs/js/tokenizer/module.f.js +1 -1
  113. package/fs/js/tokenizer/proof.f.js +7 -0
  114. package/fs/json/module.f.d.ts +9 -2
  115. package/fs/json/module.f.js +9 -7
  116. package/fs/json/parser/module.f.js +4 -6
  117. package/fs/json/parser/proof.f.js +19 -10
  118. package/fs/json/proof.f.d.ts +2 -1
  119. package/fs/json/proof.f.js +33 -6
  120. package/fs/json/rpc/module.f.d.ts +5 -0
  121. package/fs/json/rpc/module.f.js +16 -11
  122. package/fs/json/schema/module.f.d.ts +15 -14
  123. package/fs/json/schema/proof.f.d.ts +3 -0
  124. package/fs/json/schema/proof.f.js +16 -1
  125. package/fs/mcp/module.f.d.ts +103 -5
  126. package/fs/mcp/module.f.js +139 -2
  127. package/fs/mcp/proof.f.d.ts +34 -0
  128. package/fs/mcp/proof.f.js +207 -0
  129. package/fs/mcp/stdio/module.f.d.ts +39 -0
  130. package/fs/mcp/stdio/module.f.js +54 -0
  131. package/fs/mcp/stdio/proof.f.d.ts +10 -0
  132. package/fs/mcp/stdio/proof.f.js +92 -0
  133. package/fs/mime/module.f.d.ts +39 -0
  134. package/fs/mime/module.f.js +78 -0
  135. package/fs/mime/proof.f.d.ts +15 -0
  136. package/fs/mime/proof.f.js +30 -0
  137. package/fs/path/module.f.d.ts +8 -0
  138. package/fs/path/module.f.js +10 -3
  139. package/fs/path/proof.f.d.ts +1 -0
  140. package/fs/path/proof.f.js +40 -2
  141. package/fs/text/sgr/proof.f.js +30 -2
  142. package/fs/text/utf8/module.f.d.ts +7 -0
  143. package/fs/text/utf8/module.f.js +85 -31
  144. package/fs/text/utf8/proof.f.d.ts +1 -0
  145. package/fs/text/utf8/proof.f.js +60 -1
  146. package/fs/types/bigfloat/module.f.d.ts +3 -0
  147. package/fs/types/bigfloat/module.f.js +30 -31
  148. package/fs/types/bigint/module.f.d.ts +24 -2
  149. package/fs/types/bigint/module.f.js +30 -2
  150. package/fs/types/bigint/proof.f.d.ts +7 -0
  151. package/fs/types/bigint/proof.f.js +47 -16
  152. package/fs/types/bit_vec/module.f.d.ts +18 -0
  153. package/fs/types/bit_vec/module.f.js +18 -0
  154. package/fs/types/bit_vec/proof.f.d.ts +1 -0
  155. package/fs/types/bit_vec/proof.f.js +13 -1
  156. package/fs/types/function/operator/proof.f.d.ts +2 -0
  157. package/fs/types/function/operator/proof.f.js +20 -2
  158. package/fs/types/list/module.f.js +1 -4
  159. package/fs/types/list/proof.f.d.ts +1 -1
  160. package/fs/types/list/proof.f.js +14 -6
  161. package/fs/types/map/proof.f.d.ts +1 -0
  162. package/fs/types/map/proof.f.js +10 -1
  163. package/fs/types/nibble_set/module.f.d.ts +13 -0
  164. package/fs/types/nibble_set/module.f.js +16 -0
  165. package/fs/types/object/module.f.d.ts +12 -8
  166. package/fs/types/object/module.f.js +9 -2
  167. package/fs/types/object/proof.f.d.ts +14 -0
  168. package/fs/types/object/proof.f.js +4 -0
  169. package/fs/types/ordered_map/proof.f.d.ts +1 -0
  170. package/fs/types/ordered_map/proof.f.js +39 -79
  171. package/fs/types/phantom/module.f.d.ts +22 -0
  172. package/fs/types/phantom/module.f.js +11 -0
  173. package/fs/types/prime_field/proof.f.d.ts +7 -0
  174. package/fs/types/prime_field/proof.f.js +63 -122
  175. package/fs/types/rtti/common/module.f.d.ts +4 -4
  176. package/fs/types/rtti/common/module.f.js +1 -1
  177. package/fs/types/rtti/module.f.d.ts +2 -3
  178. package/fs/types/rtti/parse/module.f.js +0 -1
  179. package/fs/types/rtti/ts/module.f.d.ts +26 -32
  180. package/fs/types/rtti/ts/module.f.js +2 -2
  181. package/fs/types/rtti/ts/proof.f.js +3 -3
  182. package/fs/types/rtti/validate/module.f.js +0 -1
  183. package/fs/types/sorted_list/module.f.d.ts +6 -0
  184. package/fs/types/sorted_list/module.f.js +11 -0
  185. package/fs/types/sorted_set/module.f.js +2 -8
  186. package/fs/types/ts/module.f.d.ts +1 -1
  187. package/fs/types/ts/module.f.js +3 -1
  188. package/fs/types/ts/proof.f.js +3 -2
  189. package/fs/website/proof.f.d.ts +1 -0
  190. package/fs/website/proof.f.js +8 -1
  191. package/package.json +3 -3
@@ -0,0 +1,39 @@
1
+ /**
2
+ * stdio transport for JSON-RPC / MCP servers.
3
+ *
4
+ * `stdioTransport` wraps a step function — the `mcpStep`-shaped
5
+ * `(value) => Effect<O, Response | null>` from `fs/mcp/module.f.ts` — in the
6
+ * canonical read → parse → dispatch → write loop, expressed as a recursive
7
+ * effect so it stays in the pure effect model and is fully testable against a
8
+ * mock stdin / stdout (see `fs/effects/node/virtual`) with no real process.
9
+ *
10
+ * The loop only consumes `Read` (stdin, one byte at a time, via the pure
11
+ * `readLine` combinator) and emits `Write` (stdout) on top of whatever `O` the
12
+ * step needs (e.g. `MemOp`); it is transport-agnostic and carries no filesystem
13
+ * dependency.
14
+ *
15
+ * Edge cases, matching the issue's spec:
16
+ * - `readLine` yields `null` (EOF) → the loop returns, a clean shutdown.
17
+ * - a malformed JSON line → a JSON-RPC parse-error response (`-32700`, `id`
18
+ * `null`) is written rather than silently discarded, per JSON-RPC 2.0 §5.
19
+ * - the step yields `null` (a notification needing no reply) → nothing is
20
+ * written and the loop continues.
21
+ *
22
+ * @module
23
+ */
24
+ import { type Effect, type Operation } from '../../effects/module.f.ts';
25
+ import { type Read, type Write } from '../../effects/node/module.f.ts';
26
+ import { type Unknown } from '../../json/module.f.ts';
27
+ import { type Response } from '../../json/rpc/module.f.ts';
28
+ /**
29
+ * A transport step: maps one parsed JSON-RPC message to a response, or `null`
30
+ * for a notification that needs no reply. The shape of `mcpStep(config)(handlers)(key)`.
31
+ */
32
+ export type Step<O extends Operation> = (value: Unknown) => Effect<O, Response | null>;
33
+ /**
34
+ * Drives the read-parse-dispatch-write loop for `step` over stdin/stdout.
35
+ *
36
+ * Recurses after each handled line; terminates (resolving to `void`) when
37
+ * `readLine` reports EOF.
38
+ */
39
+ export declare const stdioTransport: <O extends Operation>(step: Step<O>) => Effect<Read | Write | O, void>;
@@ -0,0 +1,54 @@
1
+ /**
2
+ * stdio transport for JSON-RPC / MCP servers.
3
+ *
4
+ * `stdioTransport` wraps a step function — the `mcpStep`-shaped
5
+ * `(value) => Effect<O, Response | null>` from `fs/mcp/module.f.ts` — in the
6
+ * canonical read → parse → dispatch → write loop, expressed as a recursive
7
+ * effect so it stays in the pure effect model and is fully testable against a
8
+ * mock stdin / stdout (see `fs/effects/node/virtual`) with no real process.
9
+ *
10
+ * The loop only consumes `Read` (stdin, one byte at a time, via the pure
11
+ * `readLine` combinator) and emits `Write` (stdout) on top of whatever `O` the
12
+ * step needs (e.g. `MemOp`); it is transport-agnostic and carries no filesystem
13
+ * dependency.
14
+ *
15
+ * Edge cases, matching the issue's spec:
16
+ * - `readLine` yields `null` (EOF) → the loop returns, a clean shutdown.
17
+ * - a malformed JSON line → a JSON-RPC parse-error response (`-32700`, `id`
18
+ * `null`) is written rather than silently discarded, per JSON-RPC 2.0 §5.
19
+ * - the step yields `null` (a notification needing no reply) → nothing is
20
+ * written and the loop continues.
21
+ *
22
+ * @module
23
+ */
24
+ import { pure } from "../../effects/module.f.js";
25
+ import { readLine, write } from "../../effects/node/module.f.js";
26
+ import { utf8 } from "../../text/module.f.js";
27
+ import { stringToList } from "../../text/utf16/module.f.js";
28
+ import { stringify } from "../../json/module.f.js";
29
+ import { tokenize } from "../../json/tokenizer/module.f.js";
30
+ import { parse } from "../../json/parser/module.f.js";
31
+ import { sort } from "../../types/object/module.f.js";
32
+ import { jsonrpc, parseError } from "../../json/rpc/module.f.js";
33
+ const stringifyJson = stringify(sort);
34
+ /** The parse-error response (`-32700`, `id: null`) for a malformed input line. */
35
+ const parseErrorResponse = { jsonrpc, error: parseError, id: null };
36
+ /** Encodes a response as a newline-terminated UTF-8 line and writes it to `stdout`. */
37
+ const writeResponse = (resp) => write('stdout', utf8(stringifyJson(resp) + '\n'));
38
+ /** Writes `resp` when present, otherwise does nothing (notification). */
39
+ const writeMaybe = (resp) => resp === null ? pure(undefined) : writeResponse(resp);
40
+ /**
41
+ * Drives the read-parse-dispatch-write loop for `step` over stdin/stdout.
42
+ *
43
+ * Recurses after each handled line; terminates (resolving to `void`) when
44
+ * `readLine` reports EOF.
45
+ */
46
+ export const stdioTransport = (step) => readLine('stdin').step(line => line === null
47
+ ? pure(undefined)
48
+ : handleLine(step)(line));
49
+ const handleLine = (step) => (line) => {
50
+ const [t, value] = parse(tokenize(stringToList(line)));
51
+ return (t === 'error'
52
+ ? writeResponse(parseErrorResponse)
53
+ : step(value).step(writeMaybe)).step(() => stdioTransport(step));
54
+ };
@@ -0,0 +1,10 @@
1
+ export declare const proof: {
2
+ eofImmediately: () => void;
3
+ requestWritesResponse: () => void;
4
+ requestWithoutTrailingNewline: () => void;
5
+ notificationWritesNothing: () => void;
6
+ malformedJsonWritesParseError: () => void;
7
+ trailingCommaWritesParseError: () => void;
8
+ undefinedFieldOmitted: () => void;
9
+ multipleLines: () => void;
10
+ };
@@ -0,0 +1,92 @@
1
+ import { assertEq } from "../../asserts/module.f.js";
2
+ import { pure } from "../../effects/module.f.js";
3
+ import { emptyState, virtual } from "../../effects/node/virtual/module.f.js";
4
+ import { stringify } from "../../json/module.f.js";
5
+ import { utf8 } from "../../text/module.f.js";
6
+ import { fromVec } from "../../types/uint8array/module.f.js";
7
+ import { sort } from "../../types/object/module.f.js";
8
+ import { jsonrpc, parseError } from "../../json/rpc/module.f.js";
9
+ import { stdioTransport } from "./module.f.js";
10
+ const stringifyJson = stringify(sort);
11
+ // Extracts the request `id` (a request has one; a notification does not).
12
+ const idOf = (value) => value !== null && typeof value === 'object' && !(value instanceof Array)
13
+ ? value.id
14
+ : undefined;
15
+ // A step that mirrors the mcpStep contract closely enough to drive the loop:
16
+ // a message carrying an `id` (a request) gets an echo success response; a
17
+ // message without an `id` (a notification) yields `null` — no reply.
18
+ const echoStep = (value) => {
19
+ const id = idOf(value);
20
+ return pure(id === undefined
21
+ ? null
22
+ : { jsonrpc, result: { ok: true }, id });
23
+ };
24
+ // UTF-8 bytes of `s` as a plain array — the virtual stdin byte stream.
25
+ const toBytes = (s) => [...fromVec(utf8(s))];
26
+ // Run the transport with `step` over `input` fed to stdin one byte at a time;
27
+ // return the final state. `input` is raw text so tests control newline framing.
28
+ const runStep = (step) => (input) => virtual({ ...emptyState, stdin: toBytes(input) })(stdioTransport(step))[0];
29
+ const run = runStep(echoStep);
30
+ const okResponse = (id) => stringifyJson({ jsonrpc, result: { ok: true }, id }) + '\n';
31
+ const parseErrorLine = stringifyJson({ jsonrpc, error: parseError, id: null }) + '\n';
32
+ const ping = (id) => `{"jsonrpc":"2.0","method":"ping","id":${id}}`;
33
+ const notification = '{"jsonrpc":"2.0","method":"notifications/initialized"}';
34
+ export const proof = {
35
+ // EOF on the very first read: clean shutdown, nothing written, no further reads.
36
+ eofImmediately: () => {
37
+ const state = run('');
38
+ assertEq(state.stdout, '');
39
+ assertEq(state.stdin.length, 0);
40
+ },
41
+ // A newline-terminated request is parsed, dispatched, and its response
42
+ // written; stdin is fully drained before the terminating EOF.
43
+ requestWritesResponse: () => {
44
+ const state = run(ping(1) + '\n');
45
+ assertEq(state.stdout, okResponse(1));
46
+ assertEq(state.stdin.length, 0);
47
+ },
48
+ // A final line lacking a trailing newline is still flushed and dispatched
49
+ // (covers `readLine`'s EOF-with-buffered-bytes branch).
50
+ requestWithoutTrailingNewline: () => {
51
+ const state = run(ping(2));
52
+ assertEq(state.stdout, okResponse(2));
53
+ },
54
+ // A notification (no `id`) yields a `null` step result → nothing is written.
55
+ notificationWritesNothing: () => {
56
+ const state = run(notification + '\n');
57
+ assertEq(state.stdout, '');
58
+ },
59
+ // A malformed JSON line produces a JSON-RPC parse-error response (-32700),
60
+ // not a silent discard.
61
+ malformedJsonWritesParseError: () => {
62
+ const state = run('not json\n');
63
+ assertEq(state.stdout, parseErrorLine);
64
+ },
65
+ // A request that is not strict JSON (trailing comma) must be rejected with
66
+ // a parse error, never dispatched to the step.
67
+ trailingCommaWritesParseError: () => {
68
+ const state = run('{"jsonrpc":"2.0","method":"ping","id":1,}\n');
69
+ assertEq(state.stdout, parseErrorLine);
70
+ },
71
+ // A response with an optional field explicitly `undefined` serializes like
72
+ // JSON.stringify — the field is omitted, not a thrown TypeError that would
73
+ // abort the loop.
74
+ undefinedFieldOmitted: () => {
75
+ const step = (value) => {
76
+ const id = idOf(value);
77
+ return pure(id === undefined
78
+ ? null
79
+ : { jsonrpc, result: { ok: true, nextCursor: undefined }, id });
80
+ };
81
+ const state = runStep(step)(ping(1) + '\n');
82
+ assertEq(state.stdout, okResponse(1));
83
+ },
84
+ // A multi-line session interleaving all cases: request, notification, and
85
+ // malformed line, ending with an unterminated request. Order is preserved
86
+ // and the notification contributes nothing.
87
+ multipleLines: () => {
88
+ const state = run([ping(1), notification, 'not json', ping(2)].join('\n'));
89
+ assertEq(state.stdout, okResponse(1) + parseErrorLine + okResponse(2));
90
+ assertEq(state.stdin.length, 0);
91
+ },
92
+ };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Magic-byte MIME type detection.
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.
8
+ *
9
+ * The CAS store is type-agnostic and keeps raw bytes only, so type is never
10
+ * stored; it is recovered on read by sniffing the content. Callers decide what
11
+ * `null` means: the CAS MCP adapter falls back to a plain text result.
12
+ *
13
+ * ## Recognised signatures
14
+ *
15
+ * | MIME type | Leading bytes |
16
+ * |-------------------|----------------------------------------|
17
+ * | `image/png` | `89 50 4E 47 0D 0A 1A 0A` |
18
+ * | `image/jpeg` | `FF D8 FF` |
19
+ * | `image/gif` | `47 49 46 38 37 61` / `…39 61` (`"GIF87a"` / `"GIF89a"`) |
20
+ * | `image/webp` | `52 49 46 46 .. .. .. .. 57 45 42 50` (`"RIFF"…"WEBP"`) |
21
+ * | `application/pdf` | `25 50 44 46 2D` (`"%PDF-"`) |
22
+ * | `application/zip` | `50 4B 03 04` / `05 06` / `07 08` (`"PK"` entry, empty, or spanned) |
23
+ *
24
+ * WebP is the one signature with a gap: the four-byte little-endian file size
25
+ * sits between the `RIFF` and `WEBP` markers, so it is matched as a prefix plus
26
+ * a second marker at byte offset 8 rather than a single contiguous run.
27
+ *
28
+ * @module
29
+ */
30
+ import { type Vec } from '../types/bit_vec/module.f.ts';
31
+ import type { Nullable } from '../types/nullable/module.f.ts';
32
+ /**
33
+ * Detects the MIME type of `bytes` from its leading magic-byte signature.
34
+ *
35
+ * @returns the MIME type string for a recognised format, or `null` when the
36
+ * leading bytes match no known signature (including any `Vec` shorter than
37
+ * the signature it might otherwise match).
38
+ */
39
+ export declare const detect: (bytes: Vec) => Nullable<string>;
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Magic-byte MIME type detection.
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.
8
+ *
9
+ * The CAS store is type-agnostic and keeps raw bytes only, so type is never
10
+ * stored; it is recovered on read by sniffing the content. Callers decide what
11
+ * `null` means: the CAS MCP adapter falls back to a plain text result.
12
+ *
13
+ * ## Recognised signatures
14
+ *
15
+ * | MIME type | Leading bytes |
16
+ * |-------------------|----------------------------------------|
17
+ * | `image/png` | `89 50 4E 47 0D 0A 1A 0A` |
18
+ * | `image/jpeg` | `FF D8 FF` |
19
+ * | `image/gif` | `47 49 46 38 37 61` / `…39 61` (`"GIF87a"` / `"GIF89a"`) |
20
+ * | `image/webp` | `52 49 46 46 .. .. .. .. 57 45 42 50` (`"RIFF"…"WEBP"`) |
21
+ * | `application/pdf` | `25 50 44 46 2D` (`"%PDF-"`) |
22
+ * | `application/zip` | `50 4B 03 04` / `05 06` / `07 08` (`"PK"` entry, empty, or spanned) |
23
+ *
24
+ * WebP is the one signature with a gap: the four-byte little-endian file size
25
+ * sits between the `RIFF` and `WEBP` markers, so it is matched as a prefix plus
26
+ * a second marker at byte offset 8 rather than a single contiguous run.
27
+ *
28
+ * @module
29
+ */
30
+ import { msb, fromSentinel, length } from "../types/bit_vec/module.f.js";
31
+ const { startsWith, removeFront } = msb;
32
+ // Each signature is written as a hex literal whose leading `1` nibble is a
33
+ // sentinel marking the start of the byte run (so leading zero bytes survive)
34
+ // and fixing the length — see `bit_vec` `fromSentinel`.
35
+ const sig = fromSentinel;
36
+ /**
37
+ * Contiguous magic-byte signatures, checked in order; the first prefix match
38
+ * wins. Ordering is irrelevant here — no signature is a prefix of another.
39
+ */
40
+ const table = [
41
+ [sig(0x189504e470d0a1a0an), 'image/png'],
42
+ [sig(0x1ffd8ffn), 'image/jpeg'],
43
+ // Match the full GIF version headers ("GIF87a" / "GIF89a"), not just "GIF8",
44
+ // so opaque bytes that merely start with "GIF8" are not mistyped.
45
+ [sig(0x1474946383761n), 'image/gif'],
46
+ [sig(0x1474946383961n), 'image/gif'],
47
+ [sig(0x1255044462dn), 'application/pdf'],
48
+ // ZIP has three "PK" local-header variants: a normal entry, an empty
49
+ // archive (end-of-central-directory only), and a spanned archive.
50
+ [sig(0x1504b0304n), 'application/zip'],
51
+ [sig(0x1504b0506n), 'application/zip'],
52
+ [sig(0x1504b0708n), 'application/zip'],
53
+ ];
54
+ // WebP: "RIFF" at offset 0, "WEBP" at offset 8 (the 4 bytes between are the
55
+ // file size). 12 bytes = 96 bits is the minimum to carry both markers.
56
+ const riff = sig(0x152494646n);
57
+ const webp = sig(0x157454250n);
58
+ const isWebp = (bytes) => length(bytes) >= 96n
59
+ && startsWith(riff)(bytes)
60
+ && startsWith(webp)(removeFront(64n)(bytes));
61
+ /**
62
+ * Detects the MIME type of `bytes` from its leading magic-byte signature.
63
+ *
64
+ * @returns the MIME type string for a recognised format, or `null` when the
65
+ * leading bytes match no known signature (including any `Vec` shorter than
66
+ * the signature it might otherwise match).
67
+ */
68
+ export const detect = (bytes) => {
69
+ if (isWebp(bytes)) {
70
+ return 'image/webp';
71
+ }
72
+ for (const [s, m] of table) {
73
+ if (startsWith(s)(bytes)) {
74
+ return m;
75
+ }
76
+ }
77
+ return null;
78
+ };
@@ -0,0 +1,15 @@
1
+ export declare const proof: {
2
+ png: () => void;
3
+ jpeg: () => void;
4
+ gif89a: () => void;
5
+ gif87a: () => void;
6
+ gif8NotGif: () => void;
7
+ pdf: () => void;
8
+ zip: () => void;
9
+ emptyZip: () => void;
10
+ webp: () => void;
11
+ riffNotWebp: () => void;
12
+ textIsNull: () => void;
13
+ shortIsNull: () => void;
14
+ emptyIsNull: () => void;
15
+ };
@@ -0,0 +1,30 @@
1
+ import { assertEq } from "../asserts/module.f.js";
2
+ import { msb, u8ListToVec, empty } from "../types/bit_vec/module.f.js";
3
+ import { detect } from "./module.f.js";
4
+ // Builds a big-endian `Vec` from a list of byte values — mirrors how the CAS
5
+ // store would hold the leading bytes of a stored blob.
6
+ const bytes = (...b) => u8ListToVec(msb)(b);
7
+ export const proof = {
8
+ png: () => assertEq(detect(bytes(0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a)), 'image/png'),
9
+ jpeg: () => assertEq(detect(bytes(0xff, 0xd8, 0xff, 0xe0)), 'image/jpeg'),
10
+ gif89a: () => assertEq(detect(bytes(0x47, 0x49, 0x46, 0x38, 0x39, 0x61)), 'image/gif'),
11
+ gif87a: () => assertEq(detect(bytes(0x47, 0x49, 0x46, 0x38, 0x37, 0x61)), 'image/gif'),
12
+ // "GIF8" alone, without a valid version suffix, is not a GIF.
13
+ gif8NotGif: () => assertEq(detect(bytes(0x47, 0x49, 0x46, 0x38, 0x30, 0x30)), null),
14
+ pdf: () =>
15
+ // "%PDF-1.4"
16
+ assertEq(detect(bytes(0x25, 0x50, 0x44, 0x46, 0x2d, 0x31, 0x2e, 0x34)), 'application/pdf'),
17
+ zip: () => assertEq(detect(bytes(0x50, 0x4b, 0x03, 0x04, 0x14, 0x00)), 'application/zip'),
18
+ // An empty ZIP archive starts with the end-of-central-directory record.
19
+ emptyZip: () => assertEq(detect(bytes(0x50, 0x4b, 0x05, 0x06, 0x00, 0x00)), 'application/zip'),
20
+ webp: () =>
21
+ // "RIFF" + 4-byte size + "WEBP"
22
+ assertEq(detect(bytes(0x52, 0x49, 0x46, 0x46, 0x1a, 0x00, 0x00, 0x00, 0x57, 0x45, 0x42, 0x50)), 'image/webp'),
23
+ // "RIFF…" without the "WEBP" marker (e.g. a WAV) is not WebP.
24
+ riffNotWebp: () => assertEq(detect(bytes(0x52, 0x49, 0x46, 0x46, 0x1a, 0x00, 0x00, 0x00, 0x57, 0x41, 0x56, 0x45)), null),
25
+ // Plain ASCII text matches no signature.
26
+ textIsNull: () => assertEq(detect(bytes(0x68, 0x65, 0x6c, 0x6c, 0x6f)), null),
27
+ // A prefix shorter than any signature falls through to null, not a partial match.
28
+ shortIsNull: () => assertEq(detect(bytes(0x89, 0x50)), null),
29
+ emptyIsNull: () => assertEq(detect(empty), null),
30
+ };
@@ -24,6 +24,14 @@ export declare const normalize: Unary<string, string>;
24
24
  * Concatenates two path fragments and returns a normalized path.
25
25
  */
26
26
  export declare const concat: Reduce<string>;
27
+ /**
28
+ * Joins path segments with single POSIX `/` separators, without
29
+ * normalization. Unlike {@link concat}, the result is not parsed/collapsed,
30
+ * so absolute roots and `.`/`..` segments are preserved verbatim. Use this
31
+ * for building paths from already-clean segments (directory walks, store
32
+ * layouts); use {@link concat} when normalization is desired.
33
+ */
34
+ export declare const join: (...list: readonly string[]) => string;
27
35
  /**
28
36
  * Returns `path` relative to `base` with a `./` prefix, or `path` unchanged
29
37
  * if it does not start with `base` or `base` is empty.
@@ -1,6 +1,5 @@
1
1
  import { fold, last, take, length, concat as listConcat, toArray } from "../types/list/module.f.js";
2
- import { join } from "../types/string/module.f.js";
3
- import { concat as stringConcat } from "../types/string/module.f.js";
2
+ import { join as listJoin, concat as stringConcat } from "../types/string/module.f.js";
4
3
  const foldNormalizeOp = input => state => {
5
4
  switch (input) {
6
5
  case '':
@@ -41,7 +40,7 @@ export const parse = (path) => {
41
40
  */
42
41
  export const normalize = path => {
43
42
  const foldResult = parse(path);
44
- return join('/')(foldResult);
43
+ return listJoin('/')(foldResult);
45
44
  };
46
45
  /**
47
46
  * Concatenates two path fragments and returns a normalized path.
@@ -50,6 +49,14 @@ export const concat = a => b => {
50
49
  const s = stringConcat([a, '/', b]);
51
50
  return normalize(s);
52
51
  };
52
+ /**
53
+ * Joins path segments with single POSIX `/` separators, without
54
+ * normalization. Unlike {@link concat}, the result is not parsed/collapsed,
55
+ * so absolute roots and `.`/`..` segments are preserved verbatim. Use this
56
+ * for building paths from already-clean segments (directory walks, store
57
+ * layouts); use {@link concat} when normalization is desired.
58
+ */
59
+ export const join = (...list) => list.join('/');
53
60
  /**
54
61
  * Returns `path` relative to `base` with a `./` prefix, or `path` unchanged
55
62
  * if it does not start with `base` or `base` is empty.
@@ -1,6 +1,7 @@
1
1
  export declare const proof: {
2
2
  normalizeTest: (() => void)[];
3
3
  concatTest: (() => void)[];
4
+ joinTest: (() => void)[];
4
5
  relativizeTest: (() => void)[];
5
6
  toPosixTest: (() => void)[];
6
7
  };
@@ -1,4 +1,4 @@
1
- import { concat, normalize, relativize, toPosix } from "./module.f.js";
1
+ import { concat, join, normalize, relativize, toPosix } from "./module.f.js";
2
2
  const normalizeTest = [
3
3
  () => {
4
4
  const norm = normalize("dir/file.json");
@@ -45,6 +45,44 @@ const concatTest = [
45
45
  }
46
46
  },
47
47
  ];
48
+ const joinTest = [
49
+ () => {
50
+ const r = join('a', 'b');
51
+ if (r !== 'a/b') {
52
+ throw r;
53
+ }
54
+ },
55
+ () => {
56
+ const r = join('/abs/root', 'x');
57
+ if (r !== '/abs/root/x') {
58
+ throw r;
59
+ }
60
+ },
61
+ () => {
62
+ const r = join('a', 'b', 'c', 'd');
63
+ if (r !== 'a/b/c/d') {
64
+ throw r;
65
+ }
66
+ },
67
+ () => {
68
+ const r = join('', 'x');
69
+ if (r !== '/x') {
70
+ throw r;
71
+ }
72
+ },
73
+ () => {
74
+ const r = join();
75
+ if (r !== '') {
76
+ throw r;
77
+ }
78
+ },
79
+ () => {
80
+ const r = join('only');
81
+ if (r !== 'only') {
82
+ throw r;
83
+ }
84
+ },
85
+ ];
48
86
  const relativizeTest = [
49
87
  () => {
50
88
  const r = relativize('/repo', '/repo/fs/a.ts');
@@ -91,4 +129,4 @@ const toPosixTest = [
91
129
  }
92
130
  },
93
131
  ];
94
- export const proof = { normalizeTest, concatTest, relativizeTest, toPosixTest };
132
+ export const proof = { normalizeTest, concatTest, joinTest, relativizeTest, toPosixTest };
@@ -1,4 +1,15 @@
1
- import { fgRed, createConsoleText, backspace } from "./module.f.js";
1
+ import { fgRed, reset, createConsoleText, backspace, csiWrite } from "./module.f.js";
2
+ import { virtual, emptyState } from "../../effects/node/virtual/module.f.js";
3
+ const makeOptions = (isTTY) => ({
4
+ args: [],
5
+ env: {},
6
+ home: '.',
7
+ std: { stdout: { isTTY }, stderr: { isTTY } },
8
+ testContext: { test: async () => { } },
9
+ bunTestContext: { test: async () => { } },
10
+ playwrightTestContext: { test: async () => { } },
11
+ engine: 'node',
12
+ });
2
13
  export const proof = [
3
14
  () => {
4
15
  if (fgRed !== '\x1b[31m') {
@@ -19,5 +30,22 @@ export const proof = [
19
30
  if (output[1] !== expected) {
20
31
  throw output[1];
21
32
  }
22
- }
33
+ },
34
+ () => {
35
+ // csiWrite with isTTY=false strips ANSI SGR sequences
36
+ const writeFn = csiWrite(makeOptions(false))('stdout');
37
+ const [state] = virtual(emptyState)(writeFn(fgRed + 'hello' + reset));
38
+ if (state.stdout !== 'hello') {
39
+ throw ['expected ANSI stripped', state.stdout];
40
+ }
41
+ },
42
+ () => {
43
+ // csiWrite with isTTY=true preserves ANSI SGR sequences
44
+ const writeFn = csiWrite(makeOptions(true))('stdout');
45
+ const [state] = virtual(emptyState)(writeFn(fgRed + 'hello' + reset));
46
+ const expected = fgRed + 'hello' + reset;
47
+ if (state.stdout !== expected) {
48
+ throw ['expected ANSI preserved', state.stdout];
49
+ }
50
+ },
23
51
  ];
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { type List, type Thunk } from '../../types/list/module.f.ts';
7
7
  import type { Array1, Array2, Array3 } from '../../types/array/module.f.ts';
8
+ import { type Vec } from '../../types/bit_vec/module.f.ts';
8
9
  /**
9
10
  * An unsigned 8-bit integer, represents a single byte.
10
11
  */
@@ -41,3 +42,9 @@ export declare const fromCodePointList: (input: List<number>) => Thunk<U8>;
41
42
  * @returns A list of Unicode code points or error codes.
42
43
  */
43
44
  export declare const toCodePointList: (input: List<U8>) => List<I32>;
45
+ /**
46
+ * Returns the decoded string if `v` is valid UTF-8, or `null` otherwise.
47
+ * Rejects non-octet Vecs, invalid byte sequences, surrogates, and out-of-range
48
+ * code points.
49
+ */
50
+ export declare const fromVec: (v: Vec) => string | null;