@hviana/sema 0.3.0 → 0.4.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/AGENTS.md +71 -5
- package/dist/src/derive/src/deduction.d.ts +12 -1
- package/dist/src/derive/src/deduction.js +5 -1
- package/dist/src/derive/src/index.d.ts +1 -0
- package/dist/src/geometry.d.ts +3 -30
- package/dist/src/geometry.js +330 -82
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/meter.d.ts +171 -0
- package/dist/src/meter.js +269 -0
- package/dist/src/mind/attention.d.ts +0 -4
- package/dist/src/mind/attention.js +251 -23
- package/dist/src/mind/bridge.d.ts +10 -1
- package/dist/src/mind/bridge.js +179 -10
- package/dist/src/mind/canonical.d.ts +6 -1
- package/dist/src/mind/canonical.js +6 -1
- package/dist/src/mind/graph-search.d.ts +9 -0
- package/dist/src/mind/graph-search.js +59 -19
- package/dist/src/mind/junction.d.ts +10 -0
- package/dist/src/mind/junction.js +14 -0
- package/dist/src/mind/match.d.ts +40 -0
- package/dist/src/mind/match.js +125 -1
- package/dist/src/mind/mechanisms/cast.js +63 -6
- package/dist/src/mind/mechanisms/extraction.d.ts +0 -34
- package/dist/src/mind/mechanisms/extraction.js +1 -88
- package/dist/src/mind/mechanisms/recall.d.ts +3 -0
- package/dist/src/mind/mechanisms/recall.js +77 -14
- package/dist/src/mind/mind.d.ts +55 -4
- package/dist/src/mind/mind.js +110 -91
- package/dist/src/mind/pipeline-mechanism.d.ts +33 -3
- package/dist/src/mind/pipeline-mechanism.js +179 -10
- package/dist/src/mind/pipeline.js +52 -10
- package/dist/src/mind/primitives.d.ts +11 -15
- package/dist/src/mind/primitives.js +47 -28
- package/dist/src/mind/reasoning.d.ts +7 -1
- package/dist/src/mind/reasoning.js +40 -8
- package/dist/src/mind/recognition.js +93 -20
- package/dist/src/mind/traverse.d.ts +11 -0
- package/dist/src/mind/traverse.js +58 -5
- package/dist/src/mind/types.d.ts +28 -5
- package/dist/src/store.d.ts +15 -0
- package/dist/src/store.js +91 -6
- package/package.json +1 -1
- package/src/derive/src/deduction.ts +15 -0
- package/src/derive/src/index.ts +1 -0
- package/src/geometry.ts +350 -122
- package/src/index.ts +1 -0
- package/src/meter.ts +333 -0
- package/src/mind/attention.ts +268 -31
- package/src/mind/bridge.ts +187 -10
- package/src/mind/canonical.ts +6 -1
- package/src/mind/graph-search.ts +60 -21
- package/src/mind/junction.ts +12 -0
- package/src/mind/match.ts +146 -1
- package/src/mind/mechanisms/cast.ts +62 -6
- package/src/mind/mechanisms/extraction.ts +2 -103
- package/src/mind/mechanisms/recall.ts +84 -17
- package/src/mind/mind.ts +139 -98
- package/src/mind/pipeline-mechanism.ts +203 -13
- package/src/mind/pipeline.ts +65 -8
- package/src/mind/primitives.ts +49 -33
- package/src/mind/reasoning.ts +39 -7
- package/src/mind/recognition.ts +89 -19
- package/src/mind/traverse.ts +59 -5
- package/src/mind/types.ts +28 -5
- package/src/store.ts +75 -6
- package/test/14-scaling.test.mjs +17 -7
- package/test/31-audit.test.mjs +4 -1
- package/test/33-multi-candidate.test.mjs +13 -1
- package/test/36-already-answered-fusion.test.mjs +10 -3
- package/test/46-recognise-multibyte-edge.test.mjs +3 -3
- package/test/53-cross-region-probe-instrumentation.test.mjs +36 -6
- package/test/55-cost-meter.test.mjs +284 -0
- package/test/56-bridge-identity-admission.test.mjs +209 -0
- package/test/57-fusion-order.test.mjs +104 -0
- package/test/58-subquantum-sites.test.mjs +112 -0
- package/test/59-fold-invariance.test.mjs +226 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// 58-subquantum-sites.test.mjs — at hub scale, a recognised form shorter
|
|
2
|
+
// than one river window (W) is CHANCE, not evidence.
|
|
3
|
+
//
|
|
4
|
+
// recognition.emit is the single choke point every pass emits sites through
|
|
5
|
+
// (structural subtree, chunk sub-runs, canonical leaf-id chains, edge trims).
|
|
6
|
+
// Below W, byte overlap carries no identity information — the SAME quantum
|
|
7
|
+
// floor identityBar prices ("below one river window, byte overlap is chance")
|
|
8
|
+
// and the bridge's attestedQ applies ("spans shorter than W carry no window of
|
|
9
|
+
// their own and can never substitute"). No new constant.
|
|
10
|
+
//
|
|
11
|
+
// Scoped to hub scale by the same `atomsAreHubs` switch that already guards
|
|
12
|
+
// byte atoms, and for the same reason: on a SMALL store a two-byte fact is
|
|
13
|
+
// genuine learnt content and its site is essential; on a large one every short
|
|
14
|
+
// letter-run of every query becomes a "recognised form" that cover can hang an
|
|
15
|
+
// edge off. A span covering the WHOLE query is exempt — then it is not a
|
|
16
|
+
// coincidental fragment of something longer, it is the question.
|
|
17
|
+
//
|
|
18
|
+
// THE LIVE SHAPE (17.9M-node trained store): "In which country is the Eiffel
|
|
19
|
+
// Tower?" recognised the form "hi" — the i=0 sub-run of the fold chunk "hich"
|
|
20
|
+
// in "In w[hi]ch" — alongside "the" and "Eiffel Tower". The canonical pass's
|
|
21
|
+
// own comment already names this coincidence ('"hi" resolving out of
|
|
22
|
+
// "W[hi]ch"'). After this rule only "Eiffel Tower" survives.
|
|
23
|
+
//
|
|
24
|
+
// NOT claimed here: that removing those sites fixes that query. It does not —
|
|
25
|
+
// see bench/README.md. Cover still grounds a greeting there through a 2-byte
|
|
26
|
+
// span that does NOT come from `sites`. This test pins the site rule only.
|
|
27
|
+
|
|
28
|
+
import { test } from "node:test";
|
|
29
|
+
import assert from "node:assert/strict";
|
|
30
|
+
import { Mind } from "../dist/src/index.js";
|
|
31
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
32
|
+
|
|
33
|
+
const mk = () =>
|
|
34
|
+
new Mind({ seed: 1, store: new SQliteStore({ path: ":memory:" }) });
|
|
35
|
+
|
|
36
|
+
const enc = new TextEncoder();
|
|
37
|
+
|
|
38
|
+
test("1. a sub-quantum coincidental substring is not a recognised site", async () => {
|
|
39
|
+
const mind = mk();
|
|
40
|
+
await mind.ingest([
|
|
41
|
+
["hi", " Hello there, how can I help?"],
|
|
42
|
+
["What is the capital of France?", " The capital of France is Paris."],
|
|
43
|
+
["What is the capital of Spain?", " The capital of Spain is Madrid."],
|
|
44
|
+
["What is the capital of Italy?", " The capital of Italy is Rome."],
|
|
45
|
+
]);
|
|
46
|
+
const { recognise } = await import("../dist/src/mind/recognition.js");
|
|
47
|
+
const { atomIsHub, corpusN } = await import("../dist/src/mind/traverse.js");
|
|
48
|
+
const N = corpusN(mind);
|
|
49
|
+
const W = mind.space.maxGroup;
|
|
50
|
+
if (!atomIsHub(mind, N)) {
|
|
51
|
+
// Below hub scale the rule is deliberately inert — nothing to assert.
|
|
52
|
+
await mind.store.close();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
// "which" contains "hi" at an interior offset.
|
|
56
|
+
const q = enc.encode("In which country is the capital of France?");
|
|
57
|
+
const sites = recognise(mind, q).sites;
|
|
58
|
+
for (const s of sites) {
|
|
59
|
+
assert.ok(
|
|
60
|
+
s.end - s.start >= W || (s.start === 0 && s.end === q.length),
|
|
61
|
+
`sub-quantum site ${
|
|
62
|
+
JSON.stringify(
|
|
63
|
+
new TextDecoder().decode(q.subarray(s.start, s.end)),
|
|
64
|
+
)
|
|
65
|
+
} survived at hub scale`,
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
await mind.store.close();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("2. a whole-query short form is EXEMPT — it is the question", async () => {
|
|
72
|
+
const mind = mk();
|
|
73
|
+
await mind.ingest([
|
|
74
|
+
["hi", " Hello there, how can I help?"],
|
|
75
|
+
["What is the capital of France?", " The capital of France is Paris."],
|
|
76
|
+
["What is the capital of Spain?", " The capital of Spain is Madrid."],
|
|
77
|
+
]);
|
|
78
|
+
const a = await mind.respondText("hi");
|
|
79
|
+
assert.match(
|
|
80
|
+
a,
|
|
81
|
+
/Hello there/,
|
|
82
|
+
"a short form that IS the whole query must still be answerable",
|
|
83
|
+
);
|
|
84
|
+
await mind.store.close();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("3. long forms are untouched", async () => {
|
|
88
|
+
const mind = mk();
|
|
89
|
+
await mind.ingest([
|
|
90
|
+
["What is the capital of France?", " The capital of France is Paris."],
|
|
91
|
+
["What is the capital of Spain?", " The capital of Spain is Madrid."],
|
|
92
|
+
]);
|
|
93
|
+
assert.match(
|
|
94
|
+
await mind.respondText("What is the capital of France?"),
|
|
95
|
+
/Paris/,
|
|
96
|
+
);
|
|
97
|
+
await mind.store.close();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("4. determinism", async () => {
|
|
101
|
+
const run = async () => {
|
|
102
|
+
const m = mk();
|
|
103
|
+
await m.ingest([
|
|
104
|
+
["hi", " Hello there, how can I help?"],
|
|
105
|
+
["What is the capital of France?", " The capital of France is Paris."],
|
|
106
|
+
]);
|
|
107
|
+
const a = await m.respondText("In which country is Paris?");
|
|
108
|
+
await m.store.close();
|
|
109
|
+
return a;
|
|
110
|
+
};
|
|
111
|
+
assert.equal(await run(), await run());
|
|
112
|
+
});
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// 59-fold-invariance.test.mjs — the fold's segmentation must be decided by
|
|
2
|
+
// CONTENT, not by a byte's absolute position.
|
|
3
|
+
//
|
|
4
|
+
// `riverFold` groups fixed arity from byte 0 and permutes item k by seats[k],
|
|
5
|
+
// k = index mod W. Under that rule a byte's contribution — and therefore the
|
|
6
|
+
// subtree its content sits in — is a function of where it happens to fall, so
|
|
7
|
+
// the same content is a different node at a different offset. Measured
|
|
8
|
+
// consequences before this changed: recognition's site set moved with the
|
|
9
|
+
// query's phase (period W); `What is the capital of France?` answered at pad
|
|
10
|
+
// 0, 1, 4 and went silent at 2, 3, 5, 6, 7; and at pad 3 `In which country is
|
|
11
|
+
// the Eiffel Tower?` answered with unrelated Malagasy text instead of
|
|
12
|
+
// abstaining. Semantic identity must not depend on W at all.
|
|
13
|
+
//
|
|
14
|
+
// `contentBoundaries` decides cuts with a rolling hash, so a change upstream
|
|
15
|
+
// moves only the cut it falls inside. These are the properties that buys, and
|
|
16
|
+
// they are cheap to lose by accident — capping the segment length at W, for
|
|
17
|
+
// instance, makes forced cuts dominate and silently restores the grid while
|
|
18
|
+
// LEAVING EVERY OTHER TEST GREEN (it was measured at 14.3%, exactly the grid,
|
|
19
|
+
// with the rest of the suite passing). That is why invariance is asserted
|
|
20
|
+
// here as a number rather than left implicit.
|
|
21
|
+
//
|
|
22
|
+
// Floors are set well under the measured values so ordinary drift does not
|
|
23
|
+
// fail the suite, and far above the grid's 14.3% so a regression to positional
|
|
24
|
+
// segmentation cannot pass. Measured at the time of writing:
|
|
25
|
+
//
|
|
26
|
+
// deposit-like text 96.7% cuts held / 94.5% segments identical
|
|
27
|
+
// non-Latin scripts 97.4% / 95.8%
|
|
28
|
+
// random binary 98.6% / 97.4%
|
|
29
|
+
// the arithmetic grid 14.3% (only shifts of k ≡ 0 mod W survive)
|
|
30
|
+
|
|
31
|
+
import { test } from "node:test";
|
|
32
|
+
import assert from "node:assert/strict";
|
|
33
|
+
import { bytesToTree, contentBoundaries } from "../dist/src/geometry.js";
|
|
34
|
+
import { Alphabet } from "../dist/src/alphabet.js";
|
|
35
|
+
import { makeKeyring, rng } from "../dist/src/vec.js";
|
|
36
|
+
|
|
37
|
+
const D = 256;
|
|
38
|
+
const W = 4;
|
|
39
|
+
const mkSpace = () => ({
|
|
40
|
+
D,
|
|
41
|
+
seats: makeKeyring(D, Math.max(8, W), rng(1)),
|
|
42
|
+
rand: rng(2),
|
|
43
|
+
maxGroup: W,
|
|
44
|
+
});
|
|
45
|
+
const space = mkSpace();
|
|
46
|
+
const alphabet = new Alphabet(7, D, { roughness: 0.65, seedMask: 0xa1fa17 });
|
|
47
|
+
const enc = (s) => new TextEncoder().encode(s);
|
|
48
|
+
|
|
49
|
+
const SAMPLES = [
|
|
50
|
+
"The Eiffel Tower is a wrought iron lattice tower located in Paris, France.",
|
|
51
|
+
"What is the capital of France? The capital of France is Paris.",
|
|
52
|
+
"Photosynthesis is the process by which plants convert light into energy.",
|
|
53
|
+
"Was ist die Hauptstadt von Deutschland? Die Hauptstadt ist Berlin.",
|
|
54
|
+
"Qual é a capital do Brasil? A capital do Brasil é Brasília.",
|
|
55
|
+
"水は水素と酸素からできています。これは化学の基本です。",
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const segmentsOf = (bytes) => {
|
|
59
|
+
const edges = [0, ...contentBoundaries(space, bytes), bytes.length];
|
|
60
|
+
const out = [];
|
|
61
|
+
for (let i = 0; i + 1 < edges.length; i++) {
|
|
62
|
+
out.push(bytes.subarray(edges[i], edges[i + 1]));
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
};
|
|
66
|
+
const keyOf = (b) => Array.from(b).join(",");
|
|
67
|
+
|
|
68
|
+
/** Shift each sample by 1..2W bytes and measure how much of the segmentation
|
|
69
|
+
* downstream of the disturbance survives. */
|
|
70
|
+
function invariance(samples) {
|
|
71
|
+
let cuts = 0, held = 0, segs = 0, segsHeld = 0;
|
|
72
|
+
for (const bytes of samples) {
|
|
73
|
+
const base = contentBoundaries(space, bytes);
|
|
74
|
+
const baseSegs = new Set(segmentsOf(bytes).map(keyOf));
|
|
75
|
+
for (let k = 1; k <= 2 * W; k++) {
|
|
76
|
+
const padded = new Uint8Array(k + bytes.length);
|
|
77
|
+
padded.set(enc(" ".repeat(k)));
|
|
78
|
+
padded.set(bytes, k);
|
|
79
|
+
const moved = new Set(contentBoundaries(space, padded));
|
|
80
|
+
// Only cuts past the disturbed head are expected to survive.
|
|
81
|
+
const expected = base.filter((c) => c > 2 * W).map((c) => c + k);
|
|
82
|
+
cuts += expected.length;
|
|
83
|
+
held += expected.filter((c) => moved.has(c)).length;
|
|
84
|
+
// Segment identity, skipping the two segments covering the change.
|
|
85
|
+
for (const s of segmentsOf(padded).slice(2)) {
|
|
86
|
+
segs++;
|
|
87
|
+
if (baseSegs.has(keyOf(s))) segsHeld++;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { cutRatio: held / cuts, segRatio: segsHeld / segs };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
test("content cuts survive a byte shift; the arithmetic grid does not", () => {
|
|
95
|
+
const { cutRatio, segRatio } = invariance(SAMPLES.map(enc));
|
|
96
|
+
assert.ok(
|
|
97
|
+
cutRatio >= 0.85,
|
|
98
|
+
`downstream cuts held ${
|
|
99
|
+
(cutRatio * 100).toFixed(1)
|
|
100
|
+
}% — below the 85% floor; ` +
|
|
101
|
+
`at 14.3% the fold has degenerated to the arithmetic grid`,
|
|
102
|
+
);
|
|
103
|
+
assert.ok(
|
|
104
|
+
segRatio >= 0.8,
|
|
105
|
+
`segments byte-identical ${
|
|
106
|
+
(segRatio * 100).toFixed(1)
|
|
107
|
+
}% — below the 80% floor`,
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
// The grid, same corpus, as the control this floor is meaningful against.
|
|
111
|
+
const gridCuts = (b) => {
|
|
112
|
+
const out = [];
|
|
113
|
+
for (let i = W; i < b.length; i += W) out.push(i);
|
|
114
|
+
return out;
|
|
115
|
+
};
|
|
116
|
+
let gCuts = 0, gHeld = 0;
|
|
117
|
+
for (const bytes of SAMPLES.map(enc)) {
|
|
118
|
+
const base = gridCuts(bytes);
|
|
119
|
+
for (let k = 1; k <= 2 * W; k++) {
|
|
120
|
+
const padded = new Uint8Array(k + bytes.length);
|
|
121
|
+
padded.set(bytes, k);
|
|
122
|
+
const moved = new Set(gridCuts(padded));
|
|
123
|
+
const expected = base.filter((c) => c > 2 * W).map((c) => c + k);
|
|
124
|
+
gCuts += expected.length;
|
|
125
|
+
gHeld += expected.filter((c) => moved.has(c)).length;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const gridRatio = gHeld / gCuts;
|
|
129
|
+
assert.ok(
|
|
130
|
+
gridRatio < 0.3,
|
|
131
|
+
`control: the grid should lose most cuts under a shift, got ${
|
|
132
|
+
(gridRatio * 100).toFixed(1)
|
|
133
|
+
}%`,
|
|
134
|
+
);
|
|
135
|
+
assert.ok(
|
|
136
|
+
cutRatio > gridRatio * 2,
|
|
137
|
+
`content cuts (${(cutRatio * 100).toFixed(1)}%) must beat the grid ` +
|
|
138
|
+
`(${(gridRatio * 100).toFixed(1)}%) by a wide margin`,
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("segmentation reads bytes, not text — random binary behaves like prose", () => {
|
|
143
|
+
// Mind is not a text engine; the same fold carries grids and any other
|
|
144
|
+
// modality. A boundary rule that only worked on prose would be importing an
|
|
145
|
+
// assumption the architecture rejects.
|
|
146
|
+
let s = 12345;
|
|
147
|
+
const rnd = () => (s = (Math.imul(s, 1103515245) + 12345) >>> 0) / 2 ** 32;
|
|
148
|
+
const binary = Array.from({ length: 40 }, () => {
|
|
149
|
+
const n = 40 + Math.floor(rnd() * 300);
|
|
150
|
+
return Uint8Array.from({ length: n }, () => Math.floor(rnd() * 256));
|
|
151
|
+
});
|
|
152
|
+
const { cutRatio, segRatio } = invariance(binary);
|
|
153
|
+
assert.ok(
|
|
154
|
+
cutRatio >= 0.85,
|
|
155
|
+
`random binary cuts held ${
|
|
156
|
+
(cutRatio * 100).toFixed(1)
|
|
157
|
+
}% — the rule must not depend on text`,
|
|
158
|
+
);
|
|
159
|
+
assert.ok(
|
|
160
|
+
segRatio >= 0.8,
|
|
161
|
+
`random binary segments ${(segRatio * 100).toFixed(1)}%`,
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("segment lengths stay within the write side's declared unit scale", () => {
|
|
166
|
+
// The minimum is W−1 (canonicalWindows' straddle neighbour, the write side's
|
|
167
|
+
// own floor for a unit) and the maximum is the keyring's seat count, because
|
|
168
|
+
// a segment folds as ONE flat node and `fold` has exactly that many seats.
|
|
169
|
+
// A segment outside that range cannot be bound into seats at all.
|
|
170
|
+
const maxLen = space.seats.length;
|
|
171
|
+
for (const bytes of SAMPLES.map(enc)) {
|
|
172
|
+
const segs = segmentsOf(bytes);
|
|
173
|
+
for (const s of segs) {
|
|
174
|
+
assert.ok(
|
|
175
|
+
s.length <= maxLen,
|
|
176
|
+
`segment of ${s.length} bytes exceeds the ${maxLen} available seats`,
|
|
177
|
+
);
|
|
178
|
+
assert.ok(s.length >= 1, "empty segment");
|
|
179
|
+
}
|
|
180
|
+
assert.equal(
|
|
181
|
+
segs.reduce((a, x) => a + x.length, 0),
|
|
182
|
+
bytes.length,
|
|
183
|
+
"segments must tile the stream exactly",
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("the fold stays shallow — no left-nested spine", () => {
|
|
189
|
+
// Joining segments left-nested costs a node per segment on ONE spine: at a
|
|
190
|
+
// cut every ~6 bytes a 3 KB stream became a 450-deep chain of 450 fresh
|
|
191
|
+
// D-vectors, ~1.8 MB for a single deposit, and every walker above inherited
|
|
192
|
+
// the depth. Grouping recurses by cut LEVEL instead, so depth stays
|
|
193
|
+
// logarithmic in the stream length.
|
|
194
|
+
const base = "The Eiffel Tower is a wrought iron lattice tower in Paris. ";
|
|
195
|
+
for (const reps of [1, 8, 40]) {
|
|
196
|
+
const bytes = enc(base.repeat(reps));
|
|
197
|
+
const tree = bytesToTree(space, alphabet, bytes);
|
|
198
|
+
let depth = 0, nodes = 0;
|
|
199
|
+
const stack = [[tree, 1]];
|
|
200
|
+
while (stack.length) {
|
|
201
|
+
const [n, d] = stack.pop();
|
|
202
|
+
nodes++;
|
|
203
|
+
if (d > depth) depth = d;
|
|
204
|
+
if (n.kids !== null) { for (const k of n.kids) stack.push([k, d + 1]); }
|
|
205
|
+
}
|
|
206
|
+
// Generous: 4x the ideal log_W depth still catches a linear spine by an
|
|
207
|
+
// enormous margin (a 2.3 KB stream would spine to ~400).
|
|
208
|
+
const ideal = Math.ceil(Math.log(bytes.length) / Math.log(W));
|
|
209
|
+
assert.ok(
|
|
210
|
+
depth <= ideal * 4,
|
|
211
|
+
`depth ${depth} over ${bytes.length} bytes (river ideal ~${ideal}) — a spine, not a tree`,
|
|
212
|
+
);
|
|
213
|
+
assert.ok(
|
|
214
|
+
nodes < bytes.length * 2,
|
|
215
|
+
`${nodes} nodes for ${bytes.length} bytes — the fold should not multiply nodes`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("the fold is deterministic — identical bytes, identical cuts", () => {
|
|
221
|
+
for (const bytes of SAMPLES.map(enc)) {
|
|
222
|
+
const a = contentBoundaries(space, bytes);
|
|
223
|
+
const b = contentBoundaries(mkSpace(), Uint8Array.from(bytes));
|
|
224
|
+
assert.deepEqual(a, b, "the same bytes must always cut the same way");
|
|
225
|
+
}
|
|
226
|
+
});
|