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.
- package/README.md +1 -1
- package/fjs/bnf/descent/module.f.d.ts +43 -2
- package/fjs/bnf/descent/module.f.js +37 -12
- package/fjs/bnf/descent/proof.f.d.ts +1 -0
- package/fjs/bnf/descent/proof.f.js +73 -32
- package/fjs/bnf/ll1/module.f.js +2 -2
- package/fjs/bnf/token_symbol/module.f.d.ts +38 -0
- package/fjs/bnf/token_symbol/module.f.js +48 -0
- package/fjs/bnf/token_symbol/proof.f.d.ts +10 -0
- package/fjs/bnf/token_symbol/proof.f.js +37 -0
- package/fjs/cas/cli/module.f.js +6 -9
- package/fjs/cas/evo/module.f.d.ts +77 -13
- package/fjs/cas/evo/module.f.js +130 -18
- package/fjs/cas/evo/proof.f.d.ts +7 -0
- package/fjs/cas/evo/proof.f.js +120 -1
- package/fjs/cas/module.f.js +64 -37
- package/fjs/cas/proof.f.d.ts +3 -9
- package/fjs/cas/proof.f.js +94 -74
- package/fjs/ci/bun/module.f.js +3 -3
- package/fjs/ci/common/module.f.d.ts +1 -2
- package/fjs/ci/common/module.f.js +1 -5
- package/fjs/ci/config/module.f.d.ts +11 -8
- package/fjs/ci/config/module.f.js +24 -9
- package/fjs/ci/deno/module.f.js +3 -3
- package/fjs/ci/module.f.js +13 -7
- package/fjs/ci/nix/module.f.d.ts +61 -0
- package/fjs/ci/nix/module.f.js +92 -0
- package/fjs/ci/nix/proof.f.d.ts +23 -0
- package/fjs/ci/nix/proof.f.js +109 -0
- package/fjs/ci/node/module.f.d.ts +23 -1
- package/fjs/ci/node/module.f.js +58 -12
- package/fjs/ci/proof.f.d.ts +2 -0
- package/fjs/ci/proof.f.js +49 -9
- package/fjs/cli/module.f.js +2 -2
- package/fjs/common/monoid/module.f.d.ts +46 -2
- package/fjs/common/monoid/module.f.js +44 -0
- package/fjs/common/monoid/proof.f.d.ts +5 -0
- package/fjs/common/monoid/proof.f.js +27 -1
- package/fjs/dev/module.f.d.ts +1 -0
- package/fjs/dev/module.f.js +38 -27
- package/fjs/dev/update/module.f.d.ts +11 -0
- package/fjs/dev/update/module.f.js +20 -0
- package/fjs/dev/update/proof.f.d.ts +7 -0
- package/fjs/dev/update/proof.f.js +35 -0
- package/fjs/djs/ast/module.f.d.ts +47 -0
- package/fjs/djs/ast/module.f.js +9 -0
- package/fjs/djs/module.f.js +5 -8
- package/fjs/djs/parser/module.f.d.ts +3 -0
- package/fjs/djs/parser/module.f.js +17 -0
- package/fjs/djs/tokenizer/module.f.js +2 -2
- package/fjs/djs/tokenizer/proof.f.d.ts +1 -0
- package/fjs/djs/tokenizer/proof.f.js +70 -16
- package/fjs/djs/transpiler/module.f.js +6 -6
- package/fjs/effects/eff/module.f.d.ts +72 -0
- package/fjs/effects/eff/module.f.js +46 -0
- package/fjs/effects/eff/proof.f.d.ts +11 -0
- package/fjs/effects/eff/proof.f.js +58 -0
- package/fjs/effects/list/module.f.d.ts +11 -5
- package/fjs/effects/list/module.f.js +2 -1
- package/fjs/effects/memory/proof.f.js +7 -8
- package/fjs/effects/module.f.d.ts +334 -52
- package/fjs/effects/module.f.js +253 -45
- package/fjs/effects/node/memory/proof.js +3 -2
- package/fjs/effects/node/module.d.ts +3 -3
- package/fjs/effects/node/module.f.d.ts +18 -8
- package/fjs/effects/node/module.f.js +31 -11
- package/fjs/effects/node/module.js +12 -12
- package/fjs/effects/node/proof.f.d.ts +1 -0
- package/fjs/effects/node/proof.f.js +32 -11
- package/fjs/effects/node/virtual/module.f.js +1 -1
- package/fjs/effects/proof.f.d.ts +32 -6
- package/fjs/effects/proof.f.js +131 -39
- package/fjs/emergent_testing/all.test.js +2 -1
- package/fjs/emergent_testing/module.f.d.ts +3 -3
- package/fjs/emergent_testing/module.f.js +63 -49
- package/fjs/emergent_testing/proof.f.d.ts +4 -0
- package/fjs/emergent_testing/proof.f.js +54 -21
- package/fjs/emergent_testing/scenarios/thenable.pass.js +1 -1
- package/fjs/fsc/module.f.js +4 -4
- package/fjs/fsm/module.f.js +1 -1
- package/fjs/js/tokenizer/module.f.d.ts +1 -0
- package/fjs/js/tokenizer/module.f.js +13 -6
- package/fjs/{cas/mcp → mcp/cas}/module.f.d.ts +3 -25
- package/fjs/{cas/mcp → mcp/cas}/module.f.js +26 -64
- package/fjs/mcp/evo/module.f.d.ts +27 -0
- package/fjs/mcp/evo/module.f.js +94 -0
- package/fjs/{cas/evo/mcp → mcp/evo}/proof.f.d.ts +2 -0
- package/fjs/{cas/evo/mcp → mcp/evo}/proof.f.js +40 -9
- package/fjs/mcp/module.f.d.ts +54 -237
- package/fjs/mcp/module.f.js +55 -258
- package/fjs/mcp/proof.f.d.ts +43 -32
- package/fjs/mcp/proof.f.js +509 -200
- package/fjs/media/nix/module.f.d.ts +30 -0
- package/fjs/media/nix/module.f.js +166 -0
- package/fjs/media/nix/proof.f.d.ts +32 -0
- package/fjs/media/nix/proof.f.js +127 -0
- package/fjs/media/type/module.f.js +2 -2
- package/fjs/media/type/proof.f.js +7 -10
- package/fjs/module.f.js +5 -9
- package/fjs/protocol/json_rpc/module.f.d.ts +114 -0
- package/fjs/{media/json/rpc → protocol/json_rpc}/module.f.js +3 -3
- package/fjs/{media/json/rpc → protocol/json_rpc}/proof.f.js +3 -3
- package/fjs/protocol/mcp/module.f.d.ts +239 -0
- package/fjs/protocol/mcp/module.f.js +272 -0
- package/fjs/protocol/mcp/proof.f.d.ts +34 -0
- package/fjs/protocol/mcp/proof.f.js +208 -0
- package/fjs/{mcp → protocol/mcp}/stdio/module.f.d.ts +7 -7
- package/fjs/{mcp → protocol/mcp}/stdio/module.f.js +22 -22
- package/fjs/{mcp → protocol/mcp}/stdio/proof.f.js +9 -9
- package/fjs/types/bigint/module.f.js +4 -3
- package/fjs/types/bit_vec/module.f.js +5 -15
- package/fjs/types/btree/remove/module.f.d.ts +5 -0
- package/fjs/types/btree/remove/module.f.js +12 -0
- package/fjs/types/list/module.f.d.ts +21 -0
- package/fjs/types/list/module.f.js +17 -0
- package/fjs/types/list/proof.f.d.ts +1 -0
- package/fjs/types/list/proof.f.js +24 -1
- package/fjs/types/number/module.f.js +2 -1
- package/fjs/types/range_map/module.f.d.ts +14 -13
- package/fjs/types/range_map/module.f.js +18 -13
- package/fjs/types/range_map/proof.f.js +26 -39
- package/fjs/types/range_set/module.f.d.ts +5 -0
- package/fjs/types/range_set/module.f.js +16 -0
- package/fjs/types/range_set/proof.f.d.ts +1 -0
- package/fjs/types/range_set/proof.f.js +19 -0
- package/fjs/types/string/module.f.js +10 -2
- package/fjs/website/module.f.js +2 -3
- package/package.json +5 -5
- package/fjs/cas/evo/mcp/module.f.d.ts +0 -19
- package/fjs/cas/evo/mcp/module.f.js +0 -56
- package/fjs/cas/mcp/proof.f.d.ts +0 -45
- package/fjs/cas/mcp/proof.f.js +0 -545
- package/fjs/ci/playwright/module.f.d.ts +0 -2
- package/fjs/ci/playwright/module.f.js +0 -25
- package/fjs/media/json/rpc/module.f.d.ts +0 -114
- /package/fjs/{media/json/rpc → protocol/json_rpc}/proof.f.d.ts +0 -0
- /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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
|
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(
|
|
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
|
|
251
|
-
const [inlineNames] = runner([])(registerModule(
|
|
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
|
|
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.
|
package/fjs/fsc/module.f.js
CHANGED
|
@@ -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(
|
|
44
|
+
return v => c => x(i)(c)(v)(c);
|
|
45
45
|
};
|
|
46
46
|
export const terminal = -1;
|
|
47
47
|
const toInit = () => () => [[], init];
|
package/fjs/fsm/module.f.js
CHANGED
|
@@ -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(
|
|
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)(
|
|
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)(
|
|
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
|
|
127
|
-
|
|
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
|
|
4
|
-
import { type
|
|
5
|
-
import { type
|
|
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
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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`.
|
|
10
|
-
* `fjs/
|
|
11
|
-
* `fjs/
|
|
12
|
-
*
|
|
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
|
|
17
|
-
*
|
|
18
|
-
* | `cas_add`
|
|
19
|
-
* | `cas_get`
|
|
20
|
-
* | `cas_list`
|
|
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/
|
|
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 {
|
|
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
|
|
120
|
-
import {} from '../../
|
|
121
|
-
import {
|
|
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 {
|
|
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()))
|
|
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)
|
|
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))
|
|
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))
|
|
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))
|
|
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()
|
|
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 '
|
|
2
|
-
import { fileCas } from '../../module.f.js';
|
|
3
|
-
import { sha256 } from '
|
|
4
|
-
import { emptyState, virtual } from '
|
|
5
|
-
import { vec8 } from '
|
|
6
|
-
import { vecToCBase32 } from '
|
|
7
|
-
import { initEvo, evo } from '
|
|
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 = {
|
|
23
|
-
|
|
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: () => {
|