functionalscript 0.38.0 → 0.39.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/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/mcp/module.f.d.ts +9 -1
- package/fjs/cas/evo/mcp/module.f.js +52 -14
- package/fjs/cas/evo/mcp/proof.f.d.ts +2 -0
- package/fjs/cas/evo/mcp/proof.f.js +33 -2
- package/fjs/cas/evo/module.f.d.ts +76 -12
- package/fjs/cas/evo/module.f.js +129 -17
- package/fjs/cas/evo/proof.f.d.ts +7 -0
- package/fjs/cas/evo/proof.f.js +119 -0
- package/fjs/cas/mcp/module.f.js +22 -20
- package/fjs/cas/mcp/proof.f.d.ts +1 -1
- package/fjs/cas/mcp/proof.f.js +9 -9
- 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 +5 -5
- package/fjs/ci/config/module.f.js +5 -5
- package/fjs/ci/deno/module.f.js +3 -3
- package/fjs/ci/module.f.js +3 -4
- package/fjs/ci/node/module.f.js +11 -9
- package/fjs/ci/proof.f.js +2 -0
- 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.js +25 -25
- 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/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.f.d.ts +1 -1
- package/fjs/effects/node/module.f.js +9 -11
- package/fjs/effects/node/proof.f.js +18 -10
- package/fjs/effects/proof.f.d.ts +32 -6
- package/fjs/effects/proof.f.js +131 -39
- package/fjs/emergent_testing/module.f.js +55 -39
- package/fjs/emergent_testing/proof.f.d.ts +4 -0
- package/fjs/emergent_testing/proof.f.js +53 -20
- package/fjs/mcp/module.f.js +8 -8
- package/fjs/mcp/proof.f.js +5 -4
- package/fjs/mcp/stdio/module.f.d.ts +2 -2
- package/fjs/mcp/stdio/module.f.js +12 -12
- package/fjs/media/type/module.f.js +2 -2
- package/fjs/media/type/proof.f.js +7 -10
- package/fjs/module.f.js +4 -8
- 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/string/module.f.js +10 -2
- package/fjs/website/module.f.js +2 -3
- package/package.json +3 -3
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type Nullable } from '../../types/nullable/module.f.ts';
|
|
2
|
+
/**
|
|
3
|
+
* How many names one encoding holds: every symbol from {@link start} up to but
|
|
4
|
+
* not including `eof` (`0xFFFFFF`, the top of the 24-bit symbol space).
|
|
5
|
+
*/
|
|
6
|
+
export declare const capacity: number;
|
|
7
|
+
/**
|
|
8
|
+
* A bidirectional map between a fixed alphabet of token names and the symbol
|
|
9
|
+
* range reserved for them.
|
|
10
|
+
*/
|
|
11
|
+
export type Encoding<T extends string> = {
|
|
12
|
+
/**
|
|
13
|
+
* The input symbol standing for `name`.
|
|
14
|
+
*
|
|
15
|
+
* The result is a bare symbol, the form a tokenizer emits. Wrap it in
|
|
16
|
+
* `oneEncode` to use it as a terminal of a grammar rule — a symbol and a
|
|
17
|
+
* `TerminalRange` are both plain numbers, so passing one where the other
|
|
18
|
+
* belongs is not a type error.
|
|
19
|
+
*/
|
|
20
|
+
readonly encode: (name: T) => number;
|
|
21
|
+
/**
|
|
22
|
+
* The name a symbol stands for, or `null` when the symbol belongs to no
|
|
23
|
+
* registered name — a code point, `eof`, or a symbol past the end of the
|
|
24
|
+
* alphabet.
|
|
25
|
+
*/
|
|
26
|
+
readonly decode: (symbol: number) => Nullable<T>;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Builds an encoding over the complete list of token names.
|
|
30
|
+
*
|
|
31
|
+
* A name takes the symbol at its index in `names`, so the list is append-only:
|
|
32
|
+
* inserting or reordering names changes the symbols of everything after the
|
|
33
|
+
* edit.
|
|
34
|
+
*
|
|
35
|
+
* @throws When `names` holds more than {@link capacity} entries, or when a name
|
|
36
|
+
* repeats — a repeated name has no single symbol to decode back to.
|
|
37
|
+
*/
|
|
38
|
+
export declare const encoding: <T extends string>(names: readonly T[]) => Encoding<T>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encoding of multi-character token names as single BNF input symbols.
|
|
3
|
+
*
|
|
4
|
+
* A parser built on top of a tokenizer consumes one input symbol per token, so
|
|
5
|
+
* multi-character operators (`>>>=`) and keywords (`instanceof`) each need a
|
|
6
|
+
* symbol of their own. Names are registered as one fixed alphabet and get a
|
|
7
|
+
* symbol from their position in it, above the Unicode range and below `eof`.
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
import { assert } from '../../asserts/module.f.js';
|
|
12
|
+
import { fromUndefined } from '../../types/nullable/module.f.js';
|
|
13
|
+
import { eof, rangeDecode, unicodeRange } from '../module.f.js';
|
|
14
|
+
const [, unicodeLast] = rangeDecode(unicodeRange);
|
|
15
|
+
const [eofSymbol] = rangeDecode(eof);
|
|
16
|
+
/**
|
|
17
|
+
* The symbol of the first registered name: `0x110000`, one past the last
|
|
18
|
+
* Unicode scalar value, so a token symbol can never be mistaken for a code
|
|
19
|
+
* point of the layer below.
|
|
20
|
+
*/
|
|
21
|
+
const start = unicodeLast + 1;
|
|
22
|
+
/**
|
|
23
|
+
* How many names one encoding holds: every symbol from {@link start} up to but
|
|
24
|
+
* not including `eof` (`0xFFFFFF`, the top of the 24-bit symbol space).
|
|
25
|
+
*/
|
|
26
|
+
export const capacity = eofSymbol - start;
|
|
27
|
+
/**
|
|
28
|
+
* Builds an encoding over the complete list of token names.
|
|
29
|
+
*
|
|
30
|
+
* A name takes the symbol at its index in `names`, so the list is append-only:
|
|
31
|
+
* inserting or reordering names changes the symbols of everything after the
|
|
32
|
+
* edit.
|
|
33
|
+
*
|
|
34
|
+
* @throws When `names` holds more than {@link capacity} entries, or when a name
|
|
35
|
+
* repeats — a repeated name has no single symbol to decode back to.
|
|
36
|
+
*/
|
|
37
|
+
export const encoding = (names) => {
|
|
38
|
+
assert(names.length <= capacity, ['too many token names', names.length]);
|
|
39
|
+
assert(new Set(names).size === names.length, ['duplicate token name', names]);
|
|
40
|
+
return {
|
|
41
|
+
encode: name => {
|
|
42
|
+
const index = names.indexOf(name);
|
|
43
|
+
assert(index !== -1, ['unregistered token name', name]);
|
|
44
|
+
return start + index;
|
|
45
|
+
},
|
|
46
|
+
decode: symbol => fromUndefined(names[symbol - start]),
|
|
47
|
+
};
|
|
48
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { assertEq } from '../../asserts/module.f.js';
|
|
2
|
+
import { capacity, encoding } from './module.f.js';
|
|
3
|
+
const names = ['>>', '>>>=', 'instanceof'];
|
|
4
|
+
export const proof = {
|
|
5
|
+
encode: () => {
|
|
6
|
+
const { encode } = encoding(names);
|
|
7
|
+
assertEq(encode('>>'), 0x110000);
|
|
8
|
+
assertEq(encode('instanceof'), 0x110002);
|
|
9
|
+
},
|
|
10
|
+
decode: [
|
|
11
|
+
() => {
|
|
12
|
+
const { decode } = encoding(names);
|
|
13
|
+
assertEq(decode(0x110000), '>>');
|
|
14
|
+
assertEq(decode(0x110002), 'instanceof');
|
|
15
|
+
},
|
|
16
|
+
() => {
|
|
17
|
+
const { decode } = encoding(names);
|
|
18
|
+
// past the end of the alphabet, a code point, and `eof`
|
|
19
|
+
assertEq(decode(0x110003), null);
|
|
20
|
+
assertEq(decode(0x10FFFF), null);
|
|
21
|
+
assertEq(decode(0xFFFFFF), null);
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
roundTrip: () => {
|
|
25
|
+
const { encode, decode } = encoding(names);
|
|
26
|
+
for (const name of names) {
|
|
27
|
+
assertEq(decode(encode(name)), name);
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
throw: {
|
|
31
|
+
duplicateName: () => { encoding(['a', 'b', 'a']); },
|
|
32
|
+
unregisteredName: () => { encoding(['a']).encode('b'); },
|
|
33
|
+
// `capacity` names fit, so one more is the smallest list that doesn't.
|
|
34
|
+
// The array is sparse, so this costs a length, not the strings.
|
|
35
|
+
tooManyNames: () => { encoding(new Array(capacity + 1)); },
|
|
36
|
+
},
|
|
37
|
+
};
|
package/fjs/cas/cli/module.f.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { sha256 } from '../../crypto/sha2/module.f.js';
|
|
7
7
|
import { cBase32ToVec, vecToCBase32 } from '../../basen/cbase32/module.f.js';
|
|
8
|
-
import { forEachStep, pure } from '../../effects/module.f.js';
|
|
8
|
+
import { forEachStep, pure, step } from '../../effects/module.f.js';
|
|
9
9
|
import { errorExit, log, writeFromStream } from '../../effects/node/module.f.js';
|
|
10
10
|
import { dispatch } from '../../cli/module.f.js';
|
|
11
11
|
import {} from '../../effects/memory/module.f.js';
|
|
@@ -19,10 +19,9 @@ export const commands = [
|
|
|
19
19
|
return errorExit("'cas add' expects one parameter");
|
|
20
20
|
}
|
|
21
21
|
const c = fileCas(sha256)(home);
|
|
22
|
-
return casAddFile(c)(path)
|
|
23
|
-
.step(hashResult => hashResult[0] === 'error'
|
|
22
|
+
return step(casAddFile(c)(path), hashResult => hashResult[0] === 'error'
|
|
24
23
|
? pure(1)
|
|
25
|
-
: log(vecToCBase32(hashResult[1]))
|
|
24
|
+
: step(log(vecToCBase32(hashResult[1])), () => pure(0)));
|
|
26
25
|
},
|
|
27
26
|
},
|
|
28
27
|
{
|
|
@@ -38,8 +37,7 @@ export const commands = [
|
|
|
38
37
|
}
|
|
39
38
|
const c = fileCas(sha256)(home);
|
|
40
39
|
const x = c.read(hash);
|
|
41
|
-
return writeFromStream(path, x)
|
|
42
|
-
.step(([r, v]) => r === 'error' ? errorExit(`e: ` + String(v)) : pure(0));
|
|
40
|
+
return step(writeFromStream(path, x), ([r, v]) => r === 'error' ? errorExit(`e: ` + String(v)) : pure(0));
|
|
43
41
|
},
|
|
44
42
|
},
|
|
45
43
|
{
|
|
@@ -47,9 +45,8 @@ export const commands = [
|
|
|
47
45
|
description: 'List all stored content hashes',
|
|
48
46
|
handler: ({ home }) => {
|
|
49
47
|
const c = fileCas(sha256)(home);
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
.step(() => pure(0));
|
|
48
|
+
const x0 = forEachStep(c.list(), j => log(vecToCBase32(j)));
|
|
49
|
+
return step(x0, () => pure(0));
|
|
53
50
|
},
|
|
54
51
|
},
|
|
55
52
|
];
|
|
@@ -8,7 +8,15 @@ export declare const evoListArgs: {};
|
|
|
8
8
|
export declare const evoHeadArgs: {
|
|
9
9
|
readonly subject: import("../../../types/rtti/module.f.ts").String;
|
|
10
10
|
};
|
|
11
|
-
/** Arguments for `
|
|
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
|
+
*/
|
|
12
20
|
export declare const evoAddArgs: {
|
|
13
21
|
readonly parents: import("../../../types/rtti/module.f.ts").Type1<"array", import("../../../types/rtti/module.f.ts").String>;
|
|
14
22
|
readonly snapshot: import("../../../types/rtti/module.f.ts").Or<readonly [import("../../../types/rtti/module.f.ts").String, undefined]>;
|
|
@@ -1,15 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MCP tool definitions for the Evo API (`fjs/cas/evo/module.f.ts`): subjects
|
|
3
|
-
*
|
|
4
|
-
* in-memory cache the core module
|
|
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.
|
|
5
6
|
*
|
|
6
7
|
* ## Tools
|
|
7
8
|
*
|
|
8
|
-
* | Tool
|
|
9
|
-
*
|
|
10
|
-
* | `evo_list`
|
|
11
|
-
* | `evo_head`
|
|
12
|
-
* | `
|
|
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/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.
|
|
13
37
|
*
|
|
14
38
|
* These tools are not served by their own process: `fjs/cas/mcp` (the same
|
|
15
39
|
* server as `cas_add`/`cas_get`/`cas_list`) builds one `Evo<O>` from its own
|
|
@@ -20,7 +44,7 @@
|
|
|
20
44
|
* @module
|
|
21
45
|
*/
|
|
22
46
|
import { string, option, array } from '../../../types/rtti/module.f.js';
|
|
23
|
-
import { pure } from '../../../effects/module.f.js';
|
|
47
|
+
import { pure, step } from '../../../effects/module.f.js';
|
|
24
48
|
import {} from '../../../effects/memory/module.f.js';
|
|
25
49
|
import { toolEntry, errorResult, okResult, } from '../../../mcp/module.f.js';
|
|
26
50
|
import { stringify } from '../../../media/json/module.f.js';
|
|
@@ -33,7 +57,15 @@ export const evoListArgs = {};
|
|
|
33
57
|
export const evoHeadArgs = {
|
|
34
58
|
subject: string,
|
|
35
59
|
};
|
|
36
|
-
/** Arguments for `
|
|
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
|
+
*/
|
|
37
69
|
export const evoAddArgs = {
|
|
38
70
|
parents: array(string),
|
|
39
71
|
snapshot: option(string),
|
|
@@ -41,7 +73,7 @@ export const evoAddArgs = {
|
|
|
41
73
|
archived: option(true),
|
|
42
74
|
};
|
|
43
75
|
// ── Tool registry ────────────────────────────────────────────────────────────────
|
|
44
|
-
/** Canonical JSON encoder for `evo_list`
|
|
76
|
+
/** Canonical JSON encoder for the `evo_list` and `evo_revision` results. */
|
|
45
77
|
const toJson = stringify(identity);
|
|
46
78
|
/** Registry of all Evo tools, bound to an `Evo<O>`. */
|
|
47
79
|
export const evoToolRegistry = (e) => [
|
|
@@ -50,7 +82,13 @@ export const evoToolRegistry = (e) => [
|
|
|
50
82
|
// constrained to a newline-free alphabet), so a `join('\n')` line
|
|
51
83
|
// format could not represent an empty subject or one containing a
|
|
52
84
|
// newline without ambiguity — JSON encoding can.
|
|
53
|
-
() => e.list()
|
|
54
|
-
toolEntry('evo_head', 'List the current head hashes (cBase32) of a subject, one per line. Empty when the subject is unknown.', evoHeadArgs, ({ subject }) => e.head(subject)
|
|
55
|
-
toolEntry('
|
|
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])))),
|
|
56
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
|
};
|
|
@@ -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: () => {
|
|
@@ -30,6 +30,13 @@
|
|
|
30
30
|
* both fold through the same {@link addRevisionToCache}, the former over the
|
|
31
31
|
* whole store at once, the latter incrementally for one new revision.
|
|
32
32
|
*
|
|
33
|
+
* Not everything here is cache-backed: {@link readRevision} answers "what is
|
|
34
|
+
* the revision at this hash" straight from the store, decoded, validated, and
|
|
35
|
+
* with every hash canonicalized ({@link toRevisionData}). It is the typed
|
|
36
|
+
* counterpart of a raw blob read — {@link addRevision} validates on the way
|
|
37
|
+
* in, {@link readRevision} on the way out — and both speak the same
|
|
38
|
+
* {@link RevisionData} vocabulary.
|
|
39
|
+
*
|
|
33
40
|
* @module
|
|
34
41
|
*/
|
|
35
42
|
import { type Effect, type Operation } from '../../effects/module.f.ts';
|
|
@@ -44,21 +51,50 @@ export type Hash = string;
|
|
|
44
51
|
/** The identity of a mutable object whose revisions are being evolved. */
|
|
45
52
|
export type Subject = string;
|
|
46
53
|
/**
|
|
47
|
-
*
|
|
48
|
-
* revision
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
54
|
+
* The semantic content of a revision: the media-level `Revision`
|
|
55
|
+
* (`fjs/media/revision`) minus `dialect`, which is a serialization tag
|
|
56
|
+
* carrying no information once decoding has validated it. One structure is
|
|
57
|
+
* the vocabulary of both directions — {@link addRevision} takes it,
|
|
58
|
+
* {@link readRevision} returns it — so what you add is what you get back, and
|
|
59
|
+
* a value read back can be fed to `add` unchanged, without stripping fields.
|
|
60
|
+
* Every direction-specific field is therefore optional, with the guarantees
|
|
61
|
+
* documented per field rather than typed:
|
|
62
|
+
*
|
|
63
|
+
* - `parents` — required in both directions; index 0 is the mainline parent.
|
|
64
|
+
* On output the entries are canonical cBase32 spellings
|
|
65
|
+
* ({@link canonicalHash}), so they compare directly against {@link Evo.head}
|
|
66
|
+
* output.
|
|
67
|
+
* - `subject` — input: absent means "inherit from my single parent" (see
|
|
68
|
+
* {@link resolveSubject}); output: always present.
|
|
69
|
+
* - `snapshot` — input: absent is a write-boundary convenience resolved from
|
|
70
|
+
* the parents (see {@link resolveSnapshot}), because the stored blob
|
|
71
|
+
* requires it explicitly; output: always present, canonical, and exactly
|
|
72
|
+
* what the stored blob names.
|
|
73
|
+
* - `archived` — genuinely optional in both directions; the only field that
|
|
74
|
+
* can be absent from a read.
|
|
75
|
+
* - `generation` — input: **ignored**, {@link computeGeneration} derives the
|
|
76
|
+
* authoritative value from the parents; output: always present. It exists as
|
|
77
|
+
* an input field only so a read value round-trips into `add` as-is.
|
|
78
|
+
*
|
|
79
|
+
* Relaxing what the format requires is the point, not an oversight. The stored
|
|
80
|
+
* `vnd.fjs.revision` blob requires `subject`, `snapshot` and `generation`, and
|
|
81
|
+
* {@link readRevision} does return all three — but this is not that blob's
|
|
82
|
+
* type. It is the type of the API that *reads and constructs* revisions, and
|
|
83
|
+
* its purpose is to make both easy: `add` asks only for what a caller can
|
|
84
|
+
* actually know, resolving or computing the rest at the write boundary, and a
|
|
85
|
+
* read hands back a value that goes straight into `add` again. A separate
|
|
86
|
+
* output type with `subject`/`snapshot`/`generation` required would state that
|
|
87
|
+
* one direction more precisely, at the cost of splitting the one vocabulary
|
|
88
|
+
* into two — the trade this API declines, deliberately and not by omission.
|
|
89
|
+
* `fjs/media/revision`'s `Revision` remains the all-required type of the
|
|
90
|
+
* stored blob for anyone who wants it.
|
|
56
91
|
*/
|
|
57
|
-
export type
|
|
92
|
+
export type RevisionData = {
|
|
58
93
|
readonly parents: readonly Hash[];
|
|
59
94
|
readonly snapshot?: Hash | undefined;
|
|
60
95
|
readonly subject?: Subject | undefined;
|
|
61
96
|
readonly archived?: true | undefined;
|
|
97
|
+
readonly generation?: number | undefined;
|
|
62
98
|
};
|
|
63
99
|
/**
|
|
64
100
|
* Per-subject bookkeeping: every revision hash seen for the subject, and
|
|
@@ -123,8 +159,27 @@ export declare const syncRevision: (cacheKey: Key<Cache>) => (hash: Vec) => (val
|
|
|
123
159
|
* revision, a blob too large to encode, or a store write failure — is
|
|
124
160
|
* reported as `error(message)` rather than thrown, so a caller (e.g. an MCP
|
|
125
161
|
* tool handler) can surface it without a `throw`/`catch`.
|
|
162
|
+
*
|
|
163
|
+
* `input.generation` is ignored: it exists on {@link RevisionData} only so a
|
|
164
|
+
* value read back by {@link readRevision} round-trips into `add` unchanged,
|
|
165
|
+
* and {@link computeGeneration} always derives the stored value from the
|
|
166
|
+
* resolved parents.
|
|
167
|
+
*/
|
|
168
|
+
export declare const addRevision: <O extends Operation>(cas: Cas<O>) => (cacheKey: Key<Cache>) => (input: RevisionData) => Effect<O | MemOp, Result<Hash, string>>;
|
|
169
|
+
/**
|
|
170
|
+
* Reads the revision at `hash`: decoded, validated, and canonicalized
|
|
171
|
+
* ({@link toRevisionData}). The typed counterpart of a raw `cas.read` — the
|
|
172
|
+
* store's generic "bytes by hash" read stays available for arbitrary content
|
|
173
|
+
* (snapshots and everything else that is not a revision), while this is the
|
|
174
|
+
* view for revisions specifically: `add` validates on the way in, this
|
|
175
|
+
* validates on the way out, so no caller re-implements JSON parsing, schema
|
|
176
|
+
* validation, the `dialect` check, or hash canonicalization.
|
|
177
|
+
*
|
|
178
|
+
* Every way to fail is its own message: `hash` is not cBase32, the store has
|
|
179
|
+
* nothing under it, the store has it but could not deliver it, or what it
|
|
180
|
+
* holds is not a `vnd.fjs.revision` (see {@link decodeReadRevision}).
|
|
126
181
|
*/
|
|
127
|
-
export declare const
|
|
182
|
+
export declare const readRevision: <O extends Operation>(cas: Cas<O>) => (hash: Hash) => Effect<O, Result<RevisionData, string>>;
|
|
128
183
|
/** The Evo API described in `fjs/cas/evo/README.md`, bound to a `Cas<O>` and its cache slot. */
|
|
129
184
|
export type Evo<O extends Operation> = {
|
|
130
185
|
/** Returns every subject with at least one stored revision. */
|
|
@@ -132,7 +187,16 @@ export type Evo<O extends Operation> = {
|
|
|
132
187
|
/** Returns the current head hashes of `subject` (empty if unknown). */
|
|
133
188
|
readonly head: (subject: Subject) => Effect<MemOp, readonly Hash[]>;
|
|
134
189
|
/** Adds a new head; see {@link addRevision}. */
|
|
135
|
-
readonly add: (input:
|
|
190
|
+
readonly add: (input: RevisionData) => Effect<O | MemOp, Result<Hash, string>>;
|
|
191
|
+
/**
|
|
192
|
+
* The revision at `hash`, decoded, validated, and canonicalized; see
|
|
193
|
+
* {@link readRevision}. Served from the store today, so the `MemOp` in the
|
|
194
|
+
* declared operation set is unused — it is there because a revision is
|
|
195
|
+
* immutable and therefore memoizable in the same cache slot the other
|
|
196
|
+
* operations read, which must not become a breaking change to this type
|
|
197
|
+
* when it happens.
|
|
198
|
+
*/
|
|
199
|
+
readonly revision: (hash: Hash) => Effect<O | MemOp, Result<RevisionData, string>>;
|
|
136
200
|
};
|
|
137
201
|
/** Builds the {@link Evo} API over `cas`, backed by the cache at `cacheKey` (see {@link initEvo}). */
|
|
138
202
|
export declare const evo: <O extends Operation>(cas: Cas<O>) => (cacheKey: Key<Cache>) => Evo<O>;
|