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
@@ -218,41 +218,72 @@ export const githubReporterOutput = () => {
218
218
  assertEq(exit, 1);
219
219
  assertEq(stderr, '::error file=./s.proof.f.ts,line=1,title=import("./s.proof.f.ts").proof["a%3Ab%2Cc%25d"]()::oops\n');
220
220
  };
221
- // registerModule appends ' ...' for inline runners (Bun/Playwright).
222
- // Uses a minimal synchronous mock for the Test/All/Await effect operations.
223
- export const registerSuffixes = () => {
221
+ const registerNoopCtx = { test: (_n, _o, _f) => Promise.resolve() };
222
+ /**
223
+ * Builds a synchronous mock runner for `registerModule`'s `Test`/`All`/`Await`
224
+ * effect operations. Only the `test` op varies between call sites (whether it
225
+ * invokes the registered callback), so `all`/`await` are shared here.
226
+ */
227
+ const makeRegisterRunner = (testOp) => {
224
228
  let runner;
225
- const noopCtx = { test: (_n, _o, _f) => Promise.resolve() };
226
- const makeRunner = () => mockRun({
227
- test: (_ctx, name, _xf, _fn) => (s) => [[...s, name], undefined],
228
- all: (...effects) => (s) => {
229
- let st = s;
230
- const rs = [];
231
- for (const e of effects) {
232
- const [ns, r] = runner(st)(e);
233
- st = ns;
234
- rs.push(r);
235
- }
236
- return [st, rs];
237
- },
238
- await: p => (s) => [s, [p]],
229
+ runner = mockRun({
230
+ test: (ctx, name, xf, fn) => testOp(runner, ctx, name, xf, fn),
231
+ all: (...effects) => (s) => effects.reduce(([st, rs], e) => {
232
+ const [ns, r] = runner(st)(e);
233
+ return [ns, [...rs, r]];
234
+ }, [s, []]),
235
+ await: (p) => (s) => [s, [p]],
239
236
  });
240
- runner = makeRunner();
237
+ return runner;
238
+ };
239
+ // registerModule appends ' ...' for inline runners (Bun).
240
+ // This mock never invokes the registered callback; it only records names.
241
+ export const registerSuffixes = () => {
242
+ const runner = makeRegisterRunner((_runner, _ctx, name, _xf, _fn) => (s) => [[...s, name], undefined]);
241
243
  const proof = {
242
244
  ok: () => { },
243
245
  throw: { a: () => { throw 'expected'; } },
244
246
  };
245
247
  // Node (star = ''): no suffixes
246
- const [nodeNames] = runner([])(registerModule(noopCtx, './a.f.ts', proof, ''));
248
+ const [nodeNames] = runner([])(registerModule(registerNoopCtx, './a.f.ts', proof, ''));
247
249
  assertEq(nodeNames.length, 2);
248
250
  assert(nodeNames[0] === 'import("./a.f.ts").proof.ok()');
249
251
  assertEq(nodeNames[1], 'import("./a.f.ts").proof.throw.a()');
250
- // Bun/Playwright (star = ' ...'): ... on normal tests, path shows throw for throw-tests
251
- const [inlineNames] = runner([])(registerModule(noopCtx, './a.f.ts', proof, ' ...'));
252
+ // Bun (star = ' ...'): ... on normal tests, path shows throw for throw-tests
253
+ const [inlineNames] = runner([])(registerModule(registerNoopCtx, './a.f.ts', proof, ' ...'));
252
254
  assertEq(inlineNames.length, 2);
253
255
  assert(inlineNames[0] === 'import("./a.f.ts").proof.ok() ...');
254
256
  assertEq(inlineNames[1], 'import("./a.f.ts").proof.throw.a()');
255
257
  };
258
+ // A `throw`-tagged test whose function completes without throwing (the
259
+ // external framework, not this module, is responsible for turning that into
260
+ // a failure via `expectFailure`). registerModule's own callback must still
261
+ // short-circuit before walking the returned value for sub-tests: it invokes
262
+ // the callback and returns without recursing, rather than treating the
263
+ // returned object as a sub-tree.
264
+ export const registerThrowsWithoutThrowing = () => {
265
+ // Unlike registerSuffixes' mock, this one actually invokes the registered
266
+ // callback so registerOne's inner `.step` body runs, and asserts the
267
+ // callback is registered with `expectFailure: true`.
268
+ const runner = makeRegisterRunner((runner, ctx, name, xf, fn) => (s) => {
269
+ assert(xf);
270
+ const [ns] = runner(s)(fn(ctx));
271
+ return [[...ns, name], undefined];
272
+ });
273
+ // Returns a sub-tree that would register more tests if it were walked.
274
+ const proof = { throw: { a: () => ({ sub: () => { } }) } };
275
+ const [names] = runner([])(registerModule(registerNoopCtx, './a.f.ts', proof, ''));
276
+ // Only the throw-test itself is registered; `sub` is never reached.
277
+ assertEq(names.length, 1);
278
+ assertEq(names[0], 'import("./a.f.ts").proof.throw.a()');
279
+ };
280
+ // registerModule with an empty proof object registers zero tests and
281
+ // returns without invoking the mock's `test` op at all.
282
+ export const registerEmptyProof = () => {
283
+ const runner = makeRegisterRunner((_runner, _ctx, name, _xf, _fn) => (s) => [[...s, name], undefined]);
284
+ const [names] = runner([])(registerModule(registerNoopCtx, './a.f.ts', {}, ''));
285
+ assertEq(names.length, 0);
286
+ };
256
287
  // direct unit tests for the pure path-format helpers
257
288
  export const helpers = {
258
289
  isInteger: () => {
@@ -361,6 +392,8 @@ export const proof = {
361
392
  defaultReporterFailOutput,
362
393
  githubReporterOutput,
363
394
  registerSuffixes,
395
+ registerThrowsWithoutThrowing,
396
+ registerEmptyProof,
364
397
  defaultReporterExpectedToThrow,
365
398
  helpers
366
399
  };
@@ -1,6 +1,6 @@
1
1
  // A test that returns a thenable (Promise-like object, not a real Promise).
2
2
  // Per FunctionalScript convention, thenables are treated as plain values —
3
- // not awaited. Both sandbox (fjs) and registerModule (node/bun/deno/playwright)
3
+ // not awaited. Both sandbox (fjs) and registerModule (node/bun/deno)
4
4
  // must exit 0: the thenable object is walked as a sub-tree whose only key
5
5
  // `then` is a function with parameters, so no leaf tests are found and the
6
6
  // test trivially passes.
@@ -7,7 +7,7 @@ import { strictEqual } from '../types/function/operator/module.f.js';
7
7
  import { merge as rangeMapMerge, fromRange, get, } from '../types/range_map/module.f.js';
8
8
  import { reduce as listReduce, toArray, map } from '../types/list/module.f.js';
9
9
  import { range as asciiRange } from '../text/ascii/module.f.js';
10
- import { fn } from '../types/function/module.f.js';
10
+ import { flip, fn } from '../types/function/module.f.js';
11
11
  import { one } from '../types/range/module.f.js';
12
12
  import { assertEq } from '../asserts/module.f.js';
13
13
  const fromCharCode = String.fromCharCode;
@@ -31,17 +31,17 @@ const reduce = (a) => {
31
31
  });
32
32
  return toArray(listReduce(merge)(empty)(a));
33
33
  };
34
- const codePointRange = fromRange(def);
34
+ const codePointRange = flip(fromRange(def));
35
35
  const range = fn(asciiRange).map(codePointRange).result;
36
36
  const rangeSet = (l) => (f) => {
37
- const codePointRange = fromRange(def);
37
+ const codePointRange = flip(fromRange(def));
38
38
  const g = r => codePointRange(asciiRange(r))(f);
39
39
  return reduce(map(g)(l));
40
40
  };
41
41
  const create = (a) => {
42
42
  const i = reduce(a);
43
43
  const x = get(def);
44
- return v => c => x(c)(i)(v)(c);
44
+ return v => c => x(i)(c)(v)(c);
45
45
  };
46
46
  export const terminal = -1;
47
47
  const toInit = () => () => [[], init];
@@ -51,5 +51,5 @@ const initialState = [''];
51
51
  const initialStateStringify = stringifyIdentity(initialState);
52
52
  export const dfa = grammar => addEntry(grammar)(initialState)({});
53
53
  const get = rangeMapGet(emptyStateStringify);
54
- const runOp = dfa => input => s => get(input)(dfa[s] ?? []);
54
+ const runOp = dfa => input => s => get(dfa[s] ?? [])(input);
55
55
  export const run = (dfa) => (input) => foldScan(runOp(dfa))(initialStateStringify)(input);
@@ -96,6 +96,7 @@ type ErrorMessage = '" are missing' | 'unescaped character' | 'invalid hex value
96
96
  export declare const isKeywordToken: (token: JsToken) => boolean;
97
97
  export declare const tokenize: (input: List<number>) => (path: string) => List<JsTokenWithMetadata>;
98
98
  export declare const proof: {
99
+ getOperatorTokenInvalid: () => void;
99
100
  throw: {
100
101
  unionConflict: () => void;
101
102
  };
@@ -27,7 +27,7 @@ leftSquareBracket, reverseSolidus, rightSquareBracket, lowLine,
27
27
  latinSmallLetterRange, latinSmallLetterA, latinSmallLetterB, latinSmallLetterE, latinSmallLetterF, latinSmallLetterN, latinSmallLetterR, latinSmallLetterT, latinSmallLetterU,
28
28
  //
29
29
  leftCurlyBracket, rightCurlyBracket, dollarSign } from '../../text/ascii/module.f.js';
30
- import { todo } from '../../asserts/module.f.js';
30
+ import { todo, assertEq } from '../../asserts/module.f.js';
31
31
  const { fromCharCode } = String;
32
32
  const rangeOneNine = range('19');
33
33
  const rangeSetNewLine = [
@@ -111,21 +111,20 @@ const rangeMapMerge = def => merge({
111
111
  equal: strictEqual,
112
112
  def,
113
113
  });
114
- const rangeFunc = r => f => def => fromRange(def)(r)(f);
114
+ const rangeFunc = r => f => def => fromRange(def)(f)(r);
115
115
  const scanRangeOp = def => f => [f(def), scanRangeOp(def)];
116
116
  const reduceRangeMap = def => a => {
117
117
  const rm = scan(scanRangeOp(def))(a);
118
118
  return toArray(listReduce(rangeMapMerge(def))(empty)(rm));
119
119
  };
120
- const scanRangeSetOp = def => f => r => [fromRange(def)(r)(f), scanRangeSetOp(def)(f)];
120
+ const scanRangeSetOp = def => f => r => [fromRange(def)(f)(r), scanRangeSetOp(def)(f)];
121
121
  const rangeSetFunc = rs => f => def => {
122
122
  const rm = scan(scanRangeSetOp(def)(f))(rs);
123
123
  return toArray(listReduce(rangeMapMerge(def))(empty)(rm));
124
124
  };
125
125
  const create = (def) => (a) => {
126
- const i = reduceRangeMap(def)(a);
127
- const x = get(def);
128
- return v => c => x(c)(i)(v)(c);
126
+ const x = get(def)(reduceRangeMap(def)(a));
127
+ return v => c => x(c)(v)(c);
129
128
  };
130
129
  const digitToBigInt = d => BigInt(d - digit0);
131
130
  const startNumber = digit => ({ s: 1n, m: digitToBigInt(digit), f: 0, es: 1, e: 0 });
@@ -527,6 +526,14 @@ export const tokenize = input => path => {
527
526
  return flat(scan(flat([input /*satisfies List<CharCodeOrEof>*/, [null]])));
528
527
  };
529
528
  export const proof = {
529
+ // `getOperatorToken` is only ever called with a value already confirmed to be
530
+ // a known operator (`hasOperatorToken`, or a single char from `rangeOpStart`),
531
+ // so its `??` fallback is unreachable through `tokenize`. Call it directly
532
+ // with a non-operator string to cover that branch.
533
+ getOperatorTokenInvalid: () => {
534
+ const result = getOperatorToken('@');
535
+ assertEq(result.kind, 'error');
536
+ },
530
537
  throw: {
531
538
  // union throws when two distinct non-default handlers are merged for the same range;
532
539
  // this path is unreachable through the public API (no overlapping ranges in practice).
@@ -1,9 +1,7 @@
1
- import { type Effect } from '../../effects/module.f.ts';
2
1
  import { type MemOp } from '../../effects/memory/module.f.ts';
3
- import { type Read, type Write } from '../../effects/node/module.f.ts';
4
- import { type McpConfig, type McpHandlers, type ToolEntry } from '../../mcp/module.f.ts';
5
- import { type FileCasOperation } from '../module.f.ts';
6
- import { type Cache } from '../evo/module.f.ts';
2
+ import { type ToolEntry } from '../../protocol/mcp/module.f.ts';
3
+ import { type FileCasOperation } from '../../cas/module.f.ts';
4
+ import { type Cache } from '../../cas/evo/module.f.ts';
7
5
  import type { Key } from '../../effects/memory/module.f.ts';
8
6
  /** Arguments for `cas_add`: content to store, with optional encoding type. */
9
7
  export declare const casAddArgs: {
@@ -27,23 +25,3 @@ export declare const casListArgs: {};
27
25
  * rescan.
28
26
  */
29
27
  export declare const casToolRegistry: (home: string) => (cacheKey: Key<Cache>) => readonly ToolEntry<FileCasOperation | MemOp>[];
30
- /**
31
- * MCP handlers for `FileCas` plus the Evo API (`fjs/cas/evo`) layered on it,
32
- * bound to `home` and an already-built Evo cache slot (see `initEvo`).
33
- */
34
- export declare const casMcpHandlers: (home: string) => (cacheKey: Key<Cache>) => McpHandlers<FileCasOperation | MemOp>;
35
- /**
36
- * Static MCP configuration for the CAS server: advertises the `tools`
37
- * capability, identifies the server, and pins the protocol version.
38
- */
39
- export declare const casConfig: McpConfig;
40
- /**
41
- * Runs the combined CAS + Evo MCP server over stdio: scans `~/.cas/` once to
42
- * build the Evo subject/head cache (`initEvo`), allocates the session-state
43
- * slot, builds the `mcpStep` for the merged tool registry, and drives the
44
- * read → parse → dispatch → write loop until stdin EOF.
45
- */
46
- export declare const casMcpServer: (home: string) => Effect<Read | Write | MemOp | FileCasOperation, void>;
47
- export declare const proof: {
48
- casMcpServer: () => void;
49
- };
@@ -1,26 +1,21 @@
1
1
  /**
2
- * MCP adapter for the content-addressable store and the Evo API layered on
3
- * top of it.
4
- *
5
- * Maps `Cas<O>` operations onto MCP tools, so an agent that speaks MCP can
6
- * store a blob and get back its hash, fetch a blob by hash, and enumerate
7
- * what is stored — without shelling out to the `cas` CLI. The store itself
2
+ * The `cas_add` / `cas_get` / `cas_list` tool registry: maps `Cas<O>`
3
+ * operations onto MCP tools, so an agent that speaks MCP can store a blob and
4
+ * get back its hash, fetch a blob by hash, and enumerate what is stored —
5
+ * without shelling out to the `cas` CLI. The store itself
8
6
  * (`fjs/cas/module.f.ts`) stays transport-agnostic; this is an additional
9
- * front end alongside the CLI `main`. The same server also exposes
10
- * `fjs/cas/evo`'s subject/head API (`evo_list`/`evo_head`/`evo_add`,
11
- * `fjs/cas/evo/mcp`) one process, one `~/.cas/` store, one in-memory Evo
12
- * cache scanned once at startup (`initEvo`).
7
+ * front end alongside the CLI `main`. This registry is one of the tool sets
8
+ * `fjs/mcp/module.f.ts` composes into the FJS MCP server — see that module
9
+ * for the full tool table (including `fjs/mcp/evo`'s `evo_*` tools) and the
10
+ * server entry point.
13
11
  *
14
12
  * ## Tools
15
13
  *
16
- * | Tool | args | action | result |
17
- * |-------------|-------------------------------------------------|---------------|--------------------------------------|
18
- * | `cas_add` | `{ content, type? }` | `c.write(...)`| hash (cBase32) |
19
- * | `cas_get` | `{ hash, content?: boolean }` | `c.read(key)` | JSON `{length,mimeType,type[,uri][,text\|blob]}` |
20
- * | `cas_list` | `{}` | `c.list()` | hashes, one per line |
21
- * | `evo_list` | `{}` | `e.list()` | subjects, one per line |
22
- * | `evo_head` | `{ subject }` | `e.head(...)` | head hashes, one per line |
23
- * | `evo_add` | `{ parents, snapshot?, subject?, archived? }` | `e.add(...)` | hash (cBase32) |
14
+ * | Tool | args | action | result |
15
+ * |----------------|--------------------------------|------------------|--------------------------------------|
16
+ * | `cas_add` | `{ content, type? }` | `c.write(...)` | hash (cBase32) |
17
+ * | `cas_get` | `{ hash, content?: boolean }` | `c.read(key)` | JSON `{length,mimeType,type[,uri][,text\|blob]}` |
18
+ * | `cas_list` | `{}` | `c.list()` | hashes, one per line |
24
19
  *
25
20
  * ## `cas_add` input encoding
26
21
  *
@@ -33,7 +28,7 @@
33
28
  *
34
29
  * Inline content (either encoding) is capped at 128 KiB (`maxLength`). There is
35
30
  * no MCP route for larger content — the server never opens a local path named by
36
- * the client (see the invariant in `fjs/cas/mcp/README.md`); large files go
31
+ * the client (see the invariant in `fjs/mcp/README.md`); large files go
37
32
  * through the `cas` CLI (`cas add <path>`) instead, run directly by the user.
38
33
  *
39
34
  * ## `cas_get` output
@@ -108,25 +103,22 @@
108
103
  */
109
104
  import { string, option, or, boolean } from '../../types/rtti/module.f.js';
110
105
  import { stringify } from '../../media/json/module.f.js';
111
- import { pure } from '../../effects/module.f.js';
112
- import { create } from '../../effects/memory/module.f.js';
106
+ import { pure, step } from '../../effects/module.f.js';
107
+ import {} from '../../effects/memory/module.f.js';
113
108
  import { cBase32ToVec, vecToCBase32 } from '../../basen/cbase32/module.f.js';
114
109
  import { decode as base64Decode, encode as base64Encode } from '../../basen/base64/module.f.js';
115
110
  import { tryUtf8 } from '../../text/module.f.js';
116
111
  import { detectStream } from '../../media/type/module.f.js';
117
112
  import { detect as detectDialect } from '../../media/module.f.js';
118
113
  import { maxLengthBytes } from '../../types/bit_vec/module.f.js';
119
- import { ok, error } from '../../types/result/module.f.js';
120
- import {} from '../../effects/node/module.f.js';
121
- import { stdioTransport } from '../../mcp/stdio/module.f.js';
122
- import { mcpStep, uninitializedState, toolEntry, fromRegistry, errorResult, okResult, } from '../../mcp/module.f.js';
123
- import { collectRead, fileCas } from '../module.f.js';
114
+ import { ok } from '../../types/result/module.f.js';
115
+ import { toolEntry, errorResult, okResult, } from '../../protocol/mcp/module.f.js';
116
+ import { collectRead, fileCas } from '../../cas/module.f.js';
124
117
  import { fromVec } from '../../text/utf8/module.f.js';
125
118
  import { identity } from '../../types/function/module.f.js';
126
119
  import { sha256 } from '../../crypto/sha2/module.f.js';
127
120
  import { nonEmpty, empty as elEmpty } from '../../effects/list/module.f.js';
128
- import { initEvo, evo, syncRevision } from '../evo/module.f.js';
129
- import { evoToolRegistry } from '../evo/mcp/module.f.js';
121
+ import { syncRevision } from '../../cas/evo/module.f.js';
130
122
  // ── Argument schemas (declared once, used for both inputSchema and validate) ─────
131
123
  /** Arguments for `cas_add`: content to store, with optional encoding type. */
132
124
  export const casAddArgs = {
@@ -162,12 +154,12 @@ export const casToolRegistry = (home) => (cacheKey) => {
162
154
  return x === null
163
155
  ? pure(errorResult('too large or malformed — for large content, run `npx functionalscript cas add <path>` (or have the user run it) instead'))
164
156
  // The resolved content fits in one chunk; feed it as a single-item stream.
165
- : c.write(nonEmpty(ok(x), elEmpty())).step((writeResult) => {
157
+ : step(c.write(nonEmpty(ok(x), elEmpty())), (writeResult) => {
166
158
  if (writeResult[0] === 'error') {
167
159
  return pure(errorResult('write'));
168
160
  }
169
161
  const hash = writeResult[1];
170
- return syncRevision(cacheKey)(hash)(x).step(() => pure(okResult(vecToCBase32(hash))));
162
+ return step(syncRevision(cacheKey)(hash)(x), () => pure(okResult(vecToCBase32(hash))));
171
163
  });
172
164
  }),
173
165
  toolEntry('cas_get', 'Inspect a blob by hash. Always returns JSON {length,mimeType,type[,uri]} where type is "text" or "base64". Pass content:true to also include the inline payload as text (type:"text") or blob (type:"base64"), but content is capped at 128 KiB (131072 bytes) — a larger blob is rejected with an error. To download a blob, prefer the uri field returned in the result instead of requesting inline content.', casGetArgs, r => {
@@ -176,7 +168,7 @@ export const casToolRegistry = (home) => (cacheKey) => {
176
168
  return pure(errorResult(`invalid cBase32 hash: ${r.hash}`));
177
169
  }
178
170
  const uri = c.url(key);
179
- return detectStream(c.read(key)).step(([tag, detected]) => {
171
+ return step(detectStream(c.read(key)), ([tag, detected]) => {
180
172
  if (tag === 'error') {
181
173
  return pure(errorResult(`no such hash: ${r.hash}`));
182
174
  }
@@ -189,7 +181,7 @@ export const casToolRegistry = (home) => (cacheKey) => {
189
181
  if (type !== 'text' || length > maxLengthBytes) {
190
182
  return pure(okResult(toJson(meta)));
191
183
  }
192
- return collectRead(c.read(key)).step(([collectTag, value]) => {
184
+ return step(collectRead(c.read(key)), ([collectTag, value]) => {
193
185
  // Already known to fit from the streaming pass above, so an
194
186
  // error here means the hash vanished between reads; fall back
195
187
  // to the streaming verdict rather than fail the whole request.
@@ -207,7 +199,7 @@ export const casToolRegistry = (home) => (cacheKey) => {
207
199
  if (length > maxLengthBytes) {
208
200
  return pure(errorResult(`blob too large to fetch inline (${length} bytes, limit ${maxLengthBytes} bytes); use the uri field (${uri}) or omit content for metadata`));
209
201
  }
210
- return collectRead(c.read(key)).step(([collectTag, value]) => {
202
+ return step(collectRead(c.read(key)), ([collectTag, value]) => {
211
203
  if (collectTag === 'error') {
212
204
  return pure(errorResult(`no such hash: ${r.hash}`));
213
205
  }
@@ -232,36 +224,6 @@ export const casToolRegistry = (home) => (cacheKey) => {
232
224
  });
233
225
  });
234
226
  }),
235
- toolEntry('cas_list', 'List all stored content hashes (cBase32), one per line.', casListArgs, () => c.list().step(hashes => pure(okResult(hashes.map(vecToCBase32).join('\n'))))),
227
+ toolEntry('cas_list', 'List all stored content hashes (cBase32), one per line.', casListArgs, () => step(c.list(), hashes => pure(okResult(hashes.map(vecToCBase32).join('\n'))))),
236
228
  ];
237
229
  };
238
- // ── Handlers ────────────────────────────────────────────────────────────────────
239
- /**
240
- * MCP handlers for `FileCas` plus the Evo API (`fjs/cas/evo`) layered on it,
241
- * bound to `home` and an already-built Evo cache slot (see `initEvo`).
242
- */
243
- export const casMcpHandlers = (home) => (cacheKey) => fromRegistry([...casToolRegistry(home)(cacheKey), ...evoToolRegistry(evo(fileCas(sha256)(home))(cacheKey))]);
244
- // ── Session configuration ───────────────────────────────────────────────────────
245
- /**
246
- * Static MCP configuration for the CAS server: advertises the `tools`
247
- * capability, identifies the server, and pins the protocol version.
248
- */
249
- export const casConfig = {
250
- serverInfo: { name: 'functionalscript-cas', version: '0.30.0' },
251
- capabilities: { tools: {} },
252
- protocolVersion: '2024-11-05',
253
- };
254
- // ── Server ──────────────────────────────────────────────────────────────────────
255
- /**
256
- * Runs the combined CAS + Evo MCP server over stdio: scans `~/.cas/` once to
257
- * build the Evo subject/head cache (`initEvo`), allocates the session-state
258
- * slot, builds the `mcpStep` for the merged tool registry, and drives the
259
- * read → parse → dispatch → write loop until stdin EOF.
260
- */
261
- export const casMcpServer = (home) => initEvo(fileCas(sha256)(home)).step(cacheKey => create(uninitializedState).step(sessionKey => stdioTransport(mcpStep(casConfig)(casMcpHandlers(home)(cacheKey))(sessionKey))));
262
- // ── Tests ────────────────────────────────────────────────────────────────────
263
- export const proof = {
264
- // casMcpServer is never called in integration tests because it drives a
265
- // real stdio server; call it here to cover its Effect-building body.
266
- casMcpServer: () => { casMcpServer('/'); },
267
- };
@@ -0,0 +1,27 @@
1
+ import { type Operation } from '../../effects/module.f.ts';
2
+ import { type MemOp } from '../../effects/memory/module.f.ts';
3
+ import { type ToolEntry } from '../../protocol/mcp/module.f.ts';
4
+ import { type Evo } from '../../cas/evo/module.f.ts';
5
+ /** Arguments for `evo_list`: none. */
6
+ export declare const evoListArgs: {};
7
+ /** Arguments for `evo_head`: the subject whose current heads are requested. */
8
+ export declare const evoHeadArgs: {
9
+ readonly subject: import("../../types/rtti/module.f.ts").String;
10
+ };
11
+ /** Arguments for `evo_revision`: the hash of the revision to read. */
12
+ export declare const evoRevisionArgs: {
13
+ readonly hash: import("../../types/rtti/module.f.ts").String;
14
+ };
15
+ /**
16
+ * Arguments for `evo_add`: a new revision, per `fjs/cas/evo`'s
17
+ * `RevisionData` — every field of it the caller supplies, i.e. all but
18
+ * `generation`, which the server computes.
19
+ */
20
+ export declare const evoAddArgs: {
21
+ readonly parents: import("../../types/rtti/module.f.ts").Type1<"array", import("../../types/rtti/module.f.ts").String>;
22
+ readonly snapshot: import("../../types/rtti/module.f.ts").Or<readonly [import("../../types/rtti/module.f.ts").String, undefined]>;
23
+ readonly subject: import("../../types/rtti/module.f.ts").Or<readonly [import("../../types/rtti/module.f.ts").String, undefined]>;
24
+ readonly archived: import("../../types/rtti/module.f.ts").Or<readonly [true, undefined]>;
25
+ };
26
+ /** Registry of all Evo tools, bound to an `Evo<O>`. */
27
+ export declare const evoToolRegistry: <O extends Operation>(e: Evo<O>) => readonly ToolEntry<O | MemOp>[];
@@ -0,0 +1,94 @@
1
+ /**
2
+ * MCP tool definitions for the Evo API (`fjs/cas/evo/module.f.ts`): subjects,
3
+ * revision heads, and the typed read of a single revision over the
4
+ * content-addressable store, backed by the in-memory cache the core module
5
+ * maintains.
6
+ *
7
+ * ## Tools
8
+ *
9
+ * | Tool | args | action | result |
10
+ * |----------------|-----------------------------------------------|-------------------|--------------------------------------|
11
+ * | `evo_list` | `{}` | `e.list()` | subjects, as a JSON array of strings |
12
+ * | `evo_head` | `{ subject }` | `e.head(...)` | head hashes, one per line |
13
+ * | `evo_revision` | `{ hash }` | `e.revision(...)` | the revision, as JSON `RevisionData` |
14
+ * | `evo_add` | `{ parents, snapshot?, subject?, archived? }` | `e.add(...)` | hash (cBase32) |
15
+ *
16
+ * `evo_add` and `evo_revision` speak the same structure — `fjs/cas/evo`'s
17
+ * `RevisionData` — in opposite directions, so a revision read back can be
18
+ * added again as-is. `evo_add`'s advertised arguments stay as they are: the
19
+ * one field `evo_revision` returns that `evo_add` does not accept is
20
+ * `generation`, which the server computes, and rtti's struct validation
21
+ * ignores properties the schema does not name, so a whole `evo_revision`
22
+ * result can be passed straight back to `evo_add`.
23
+ *
24
+ * ## Result size
25
+ *
26
+ * `evo_list` and `evo_revision` answer with JSON carried as MCP *text*
27
+ * content, so the JSON-RPC serializer escapes it a second time on the way
28
+ * out and a modest result can encode to a much longer line (a subject of
29
+ * quote characters is the worst case). A response whose encoded line exceeds
30
+ * the transport cap is not lost: `fjs/protocol/mcp/stdio` retries with a small
31
+ * `-32603` body carrying the request's `id`, so every request still gets a
32
+ * response and the process never crashes. That is the transport's contract
33
+ * for every tool — `cas_get` has proofs for the same double-escaping path —
34
+ * and no tool here can pre-empt it: whether the encoded response fits is
35
+ * known only by encoding it, which is the transport's job, and guessing from
36
+ * an unencoded size is exactly the size estimate that must never be made.
37
+ *
38
+ * These tools are not served by their own process: `fjs/mcp` (the same
39
+ * server as `cas_add`/`cas_get`/`cas_list`) builds one `Evo<O>` from its own
40
+ * `Cas<O>` and cache slot, concatenates `evoToolRegistry` onto its own
41
+ * registry, and serves everything from that single process — one `~/.cas/`
42
+ * store, one Evo cache, one server.
43
+ *
44
+ * @module
45
+ */
46
+ import { string, option, array } from '../../types/rtti/module.f.js';
47
+ import { pure, step } from '../../effects/module.f.js';
48
+ import {} from '../../effects/memory/module.f.js';
49
+ import { toolEntry, errorResult, okResult, } from '../../protocol/mcp/module.f.js';
50
+ import { stringify } from '../../media/json/module.f.js';
51
+ import { identity } from '../../types/function/module.f.js';
52
+ import {} from '../../cas/evo/module.f.js';
53
+ // ── Argument schemas (declared once, used for both inputSchema and validate) ─────
54
+ /** Arguments for `evo_list`: none. */
55
+ export const evoListArgs = {};
56
+ /** Arguments for `evo_head`: the subject whose current heads are requested. */
57
+ export const evoHeadArgs = {
58
+ subject: string,
59
+ };
60
+ /** Arguments for `evo_revision`: the hash of the revision to read. */
61
+ export const evoRevisionArgs = {
62
+ hash: string,
63
+ };
64
+ /**
65
+ * Arguments for `evo_add`: a new revision, per `fjs/cas/evo`'s
66
+ * `RevisionData` — every field of it the caller supplies, i.e. all but
67
+ * `generation`, which the server computes.
68
+ */
69
+ export const evoAddArgs = {
70
+ parents: array(string),
71
+ snapshot: option(string),
72
+ subject: option(string),
73
+ archived: option(true),
74
+ };
75
+ // ── Tool registry ────────────────────────────────────────────────────────────────
76
+ /** Canonical JSON encoder for the `evo_list` and `evo_revision` results. */
77
+ const toJson = stringify(identity);
78
+ /** Registry of all Evo tools, bound to an `Evo<O>`. */
79
+ export const evoToolRegistry = (e) => [
80
+ toolEntry('evo_list', 'List all subjects with at least one stored revision, as a JSON array of strings.', evoListArgs,
81
+ // Subjects are arbitrary caller-supplied strings (unlike hashes, not
82
+ // constrained to a newline-free alphabet), so a `join('\n')` line
83
+ // format could not represent an empty subject or one containing a
84
+ // newline without ambiguity — JSON encoding can.
85
+ () => step(e.list(), subjects => pure(okResult(toJson(subjects))))),
86
+ toolEntry('evo_head', 'List the current head hashes (cBase32) of a subject, one per line. Empty when the subject is unknown.', evoHeadArgs, ({ subject }) => step(e.head(subject), heads => pure(okResult(heads.join('\n'))))),
87
+ toolEntry('evo_revision', 'Read one revision by hash, as JSON: `{ subject, parents, snapshot, generation, archived? }`. `parents[0]` is the mainline parent and every further entry is a merged-in branch; `parents` and `snapshot` come back in their canonical cBase32 spelling, so they compare directly against `evo_head` output. Errors when the hash is not cBase32, is not present in the store, could not be read, or does not hold a `vnd.fjs.revision` blob — use `cas_get` for raw bytes of non-revision content.', evoRevisionArgs,
88
+ // The revision goes out as JSON in a text content item, like
89
+ // `evo_list`'s. An encoded response that outgrows the transport cap is
90
+ // the transport's `-32603`, not a tool-level error — see "Result size"
91
+ // in the module doc.
92
+ ({ hash }) => step(e.revision(hash), result => pure(result[0] === 'error' ? errorResult(result[1]) : okResult(toJson(result[1]))))),
93
+ toolEntry('evo_add', 'Add a new revision (a `vnd.fjs.revision` blob) and return its hash (cBase32). `subject` is required unless there is exactly one parent, from which it is inherited. `snapshot`, when omitted, is resolved from the parents (zero parents → `subject`, one parent → the parent\'s snapshot; a merge requires an explicit `snapshot`) and written explicitly. `generation` is computed by the server.', evoAddArgs, (input) => step(e.add(input), result => pure(result[0] === 'error' ? errorResult(result[1]) : okResult(result[1])))),
94
+ ];
@@ -4,6 +4,8 @@ export declare const proof: {
4
4
  evoListEncodesArbitrarySubjectsAsJson: () => void;
5
5
  evoHeadReflectsTheCache: () => void;
6
6
  evoHeadMissingSubjectIsInvalidArguments: () => void;
7
+ evoRevisionReturnsRevisionJson: () => void;
8
+ evoRevisionDomainErrorIsError: () => void;
7
9
  evoAddSuccessReturnsHash: () => void;
8
10
  evoAddDomainErrorIsError: () => void;
9
11
  };
@@ -1,10 +1,10 @@
1
- import { assert, assertEq } from '../../../asserts/module.f.js';
2
- import { fileCas } from '../../module.f.js';
3
- import { sha256 } from '../../../crypto/sha2/module.f.js';
4
- import { emptyState, virtual } from '../../../effects/node/virtual/module.f.js';
5
- import { vec8 } from '../../../types/bit_vec/module.f.js';
6
- import { vecToCBase32 } from '../../../basen/cbase32/module.f.js';
7
- import { initEvo, evo } from '../module.f.js';
1
+ import { assert, assertEq } from '../../asserts/module.f.js';
2
+ import { fileCas } from '../../cas/module.f.js';
3
+ import { sha256 } from '../../crypto/sha2/module.f.js';
4
+ import { emptyState, virtual } from '../../effects/node/virtual/module.f.js';
5
+ import { vec8 } from '../../types/bit_vec/module.f.js';
6
+ import { vecToCBase32 } from '../../basen/cbase32/module.f.js';
7
+ import { initEvo, evo } from '../../cas/evo/module.f.js';
8
8
  import { evoToolRegistry } from './module.f.js';
9
9
  const home = '/home/user';
10
10
  const findEntry = (registry, name) => {
@@ -19,8 +19,13 @@ const textOf = (result) => {
19
19
  };
20
20
  export const proof = {
21
21
  toolNamesMatchTheDesign: () => {
22
- const e = { list: () => { throw 'unused'; }, head: () => { throw 'unused'; }, add: () => { throw 'unused'; } };
23
- assertEq(evoToolRegistry(e).map(entry => entry.name).join(','), 'evo_list,evo_head,evo_add');
22
+ const e = {
23
+ list: () => { throw 'unused'; },
24
+ head: () => { throw 'unused'; },
25
+ add: () => { throw 'unused'; },
26
+ revision: () => { throw 'unused'; },
27
+ };
28
+ assertEq(evoToolRegistry(e).map(entry => entry.name).join(','), 'evo_list,evo_head,evo_revision,evo_add');
24
29
  },
25
30
  evoListReflectsTheCache: () => {
26
31
  const c = fileCas(sha256)(home);
@@ -69,6 +74,32 @@ export const proof = {
69
74
  const [, result] = virtual(state0)(entry.handle({}));
70
75
  assertEq(result.isError, true);
71
76
  },
77
+ // Covers evo_revision's success branch: the stored revision comes back as
78
+ // the JSON of `RevisionData` — `dialect` dropped, `generation` and the
79
+ // resolved `snapshot` included.
80
+ evoRevisionReturnsRevisionJson: () => {
81
+ const c = fileCas(sha256)(home);
82
+ const [state0, cacheKey] = virtual(emptyState)(initEvo(c));
83
+ const e = evo(c)(cacheKey);
84
+ const subject = vecToCBase32(vec8(0x3n));
85
+ const [state1, added] = virtual(state0)(e.add({ parents: [], subject }));
86
+ assert(added[0] === 'ok', ['expected add ok', added]);
87
+ const entry = findEntry(evoToolRegistry(e), 'evo_revision');
88
+ const [, result] = virtual(state1)(entry.handle({ hash: added[1] }));
89
+ assert(!result.isError);
90
+ assertEq(textOf(result), `{"subject":"${subject}","parents":[],"snapshot":"${subject}","generation":0}`);
91
+ },
92
+ // Covers evo_revision's error branch: a domain-level failure (a hash the
93
+ // store has nothing under) is surfaced as isError with the message.
94
+ evoRevisionDomainErrorIsError: () => {
95
+ const c = fileCas(sha256)(home);
96
+ const [state0, cacheKey] = virtual(emptyState)(initEvo(c));
97
+ const e = evo(c)(cacheKey);
98
+ const entry = findEntry(evoToolRegistry(e), 'evo_revision');
99
+ const [, result] = virtual(state0)(entry.handle({ hash: vecToCBase32(vec8(0x4n)) }));
100
+ assertEq(result.isError, true);
101
+ assert(textOf(result).includes('revision not found'));
102
+ },
72
103
  // Covers evo_add's success branch: a valid revision is stored and its
73
104
  // hash comes back as plain, non-error text.
74
105
  evoAddSuccessReturnsHash: () => {