@hviana/sema 0.5.7 → 0.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/AGENTS.md +23 -0
  2. package/DATASETS.md +159 -0
  3. package/HOW_IT_WORKS.md +74 -0
  4. package/README.md +12 -0
  5. package/dist/example/train_base.d.ts +73 -3
  6. package/dist/example/train_base.js +1000 -49
  7. package/dist/src/geometry.d.ts +20 -0
  8. package/dist/src/geometry.js +22 -0
  9. package/dist/src/mind/articulation.js +15 -2
  10. package/dist/src/mind/attention.d.ts +6 -0
  11. package/dist/src/mind/attention.js +44 -4
  12. package/dist/src/mind/learning.js +250 -3
  13. package/dist/src/mind/mechanisms/cast.js +45 -1
  14. package/dist/src/mind/mind.d.ts +6 -1
  15. package/dist/src/mind/mind.js +14 -2
  16. package/dist/src/mind/reasoning.js +59 -5
  17. package/dist/src/mind/recognition.js +29 -3
  18. package/dist/src/mind/traverse.d.ts +34 -0
  19. package/dist/src/mind/traverse.js +42 -0
  20. package/dist/src/store-sqlite.d.ts +4 -0
  21. package/dist/src/store-sqlite.js +47 -0
  22. package/dist/src/store.d.ts +7 -0
  23. package/example/train_base.ts +1193 -46
  24. package/jsr.json +1 -1
  25. package/package.json +1 -1
  26. package/src/geometry.ts +23 -0
  27. package/src/mind/articulation.ts +16 -2
  28. package/src/mind/attention.ts +54 -1
  29. package/src/mind/learning.ts +253 -4
  30. package/src/mind/mechanisms/cast.ts +48 -1
  31. package/src/mind/mind.ts +12 -1
  32. package/src/mind/reasoning.ts +64 -5
  33. package/src/mind/recognition.ts +29 -3
  34. package/src/mind/traverse.ts +48 -0
  35. package/src/store-sqlite.ts +53 -0
  36. package/src/store.ts +28 -0
  37. package/test/29-counterfactual.test.mjs +43 -6
  38. package/test/76-type-level-company.test.mjs +342 -0
  39. package/test/77-company-saturation.test.mjs +302 -0
  40. package/test/78-atom-hub-recognition-cliff.test.mjs +135 -0
  41. package/test/84-composed-answer-honesty.test.mjs +136 -0
  42. package/test/85-answered-directly.test.mjs +126 -0
  43. package/test/86-cast-voices-committed.test.mjs +164 -0
  44. package/test/87-codominant-commitment.test.mjs +250 -0
@@ -0,0 +1,126 @@
1
+ // 85-answered-directly.test.mjs — a question that was answered DIRECTLY must not
2
+ // run on to a further hop.
3
+ //
4
+ // THE DEFECT THIS PINS. `reason()` loops to a fixpoint, extending while the
5
+ // current answer offers anywhere to go. Every stopping condition it had was
6
+ // about the ANSWER — `consumed`, `restatesQuery`, `bytesEqual` — and none asked
7
+ // whether the QUESTION had already been satisfied. So a single-hop question
8
+ // whose answer happens to name another learnt context stepped past a correct
9
+ // answer and replaced it with the next hop's:
10
+ //
11
+ // asked : "<subject> father"
12
+ // hop 1 : "The father of <subject> is Ernest I of Anhalt-Dessau." <- correct
13
+ // pivot : "Ernest I of Anhalt-Dessau" <- a learnt context too
14
+ // got : "The date of death of Ernest I of Anhalt-Dessau is 12 June 1516."
15
+ //
16
+ // Any store holding a bare-entity context beside a relation fact has that shape.
17
+ //
18
+ // THE FIX is the echo guard's other half, and the same principle: the QUERY's
19
+ // own position in the graph says the read-out is complete. The echo guard
20
+ // handles a query that is itself a learnt CONTINUATION; this handles a query
21
+ // that is a learnt CONTEXT whose grounded answer is one of its own
22
+ // continuations. A genuine multi-hop query is not a deposited context at all —
23
+ // "What is the capital of the country of Eiffel Tower?" resolves to nothing —
24
+ // so the guard can never gate a real chain, which the last test asserts,
25
+ // because a fix that bought single-hop correctness by killing composition would
26
+ // be no fix at all.
27
+ //
28
+ // A REFUTED ALTERNATIVE, recorded so it is not retried: gating the pivot on
29
+ // "the QUERY still contains an unconsumed learnt context" (symmetric to
30
+ // `pivotInto` on the answer) looks natural and BREAKS composition — a two-hop
31
+ // query does not literally contain a learnt context, so the gate fires on every
32
+ // real chain.
33
+
34
+ import { test } from "node:test";
35
+ import assert from "node:assert/strict";
36
+ import { Mind } from "../dist/src/index.js";
37
+
38
+ const SUBJECT = "John V of Anhalt-Zerbst";
39
+ const HOP1 = `The father of ${SUBJECT} is Ernest I of Anhalt-Dessau.`;
40
+ const HOP2 = "The date of death of Ernest I of Anhalt-Dessau is 12 June 1516.";
41
+
42
+ // Two chained facts, each deposited as a relation fact AND a bare-entity fact —
43
+ // the shape that makes the entity pivotable, and the run-on possible.
44
+ const DEPOSITS = [
45
+ [`${SUBJECT} father`, HOP1],
46
+ [SUBJECT, HOP1],
47
+ ["Ernest I of Anhalt-Dessau date of death", HOP2],
48
+ ["Ernest I of Anhalt-Dessau", HOP2],
49
+ ];
50
+
51
+ async function chainStore() {
52
+ const mind = new Mind({ seed: 7, D: 1024 });
53
+ await mind.ingest(DEPOSITS);
54
+ return mind;
55
+ }
56
+
57
+ test("a single-hop question keeps its own answer", async () => {
58
+ // The regression itself. Without the guard this returns HOP2.
59
+ const mind = await chainStore();
60
+ const answer = await mind.respondText(`${SUBJECT} father`);
61
+ assert.ok(
62
+ answer.includes("Ernest I of Anhalt-Dessau"),
63
+ `lost the answer entirely: ${JSON.stringify(answer)}`,
64
+ );
65
+ assert.ok(
66
+ !answer.includes("12 June 1516"),
67
+ `ran on to the second hop and replaced a correct answer: ${
68
+ JSON.stringify(answer)
69
+ }`,
70
+ );
71
+ });
72
+
73
+ test("the bare-entity fact answers its own hop, and stops", async () => {
74
+ // The bare-entity deposit is a context too, so it must behave the same way:
75
+ // asking the entity yields ITS continuation, not the one after.
76
+ const mind = await chainStore();
77
+ const answer = await mind.respondText(SUBJECT);
78
+ assert.ok(
79
+ answer.includes("Ernest I of Anhalt-Dessau"),
80
+ `lost the answer: ${JSON.stringify(answer)}`,
81
+ );
82
+ assert.ok(
83
+ !answer.includes("12 June 1516"),
84
+ `bare-entity context ran on a hop: ${JSON.stringify(answer)}`,
85
+ );
86
+ });
87
+
88
+ test("the second hop is still reachable when actually asked", async () => {
89
+ // The guard must not make hop 2 unreachable — it is a deposited fact and a
90
+ // direct question about it must answer.
91
+ const mind = await chainStore();
92
+ const answer = await mind.respondText(
93
+ "Ernest I of Anhalt-Dessau date of death",
94
+ );
95
+ assert.ok(
96
+ answer.includes("12 June 1516"),
97
+ `hop 2 became unreachable: ${JSON.stringify(answer)}`,
98
+ );
99
+ });
100
+
101
+ test("a genuine two-hop question still composes", async () => {
102
+ // THE OTHER HALF OF THE CONTRACT. The guard keys on the query being a
103
+ // deposited context; a real multi-hop question is not one, so composition
104
+ // must be untouched. If this fails, the guard is over-broad.
105
+ const mind = new Mind({ seed: 7, D: 1024 });
106
+ await mind.ingest([
107
+ ["Eiffel Tower country", "The country of Eiffel Tower is France."],
108
+ ["France capital", "The capital of France is Paris."],
109
+ ["France", "The capital of France is Paris."],
110
+ ]);
111
+ const steps = [];
112
+ const answer = await mind.respondText(
113
+ "What is the capital of the country of Eiffel Tower?",
114
+ (s) => steps.push(s.mechanism[s.mechanism.length - 1]),
115
+ );
116
+ assert.ok(
117
+ steps.includes("pivotStep"),
118
+ `the guard suppressed a genuine chain; answer was ${
119
+ JSON.stringify(answer)
120
+ }`,
121
+ );
122
+ assert.ok(
123
+ answer.includes("Paris"),
124
+ `two-hop chain did not compose: ${JSON.stringify(answer)}`,
125
+ );
126
+ });
@@ -0,0 +1,164 @@
1
+ // 86-cast-voices-committed.test.mjs — CAST may only SPEAK through a structure
2
+ // the climb committed to, or one the query itself named.
3
+ //
4
+ // THE INVARIANT. cast.ts already states it, in the refusal note its own weave
5
+ // gate emits: "CAST refuses to transfer through content the climb itself never
6
+ // settled on". That gate asks only that the weave TOUCH a committed point
7
+ // (`points.some(isRoot)`) — the right question for MEMBERSHIP, since a weave
8
+ // needs uncommitted structure to compare against; that is what an analogy IS.
9
+ // It is the wrong question for VOICING: satisfied by any committed bystander,
10
+ // it licensed every OTHER aligned point to put its own learnt content into the
11
+ // answer while a committed root that contributed nothing held the door open.
12
+ //
13
+ // WHAT THAT COST, measured on the fixture below (N ~ 103, consensusFloor 5.13):
14
+ //
15
+ // climbConsensus committed ONE root: "Eiffel Tower country" #5 vote 8.13
16
+ // projectCounterfactual voiced:
17
+ // filler "What is th" #535 vote 0.15 <- a FILLER deposit
18
+ // displaced-structure "France capital" #90 vote 0.57
19
+ // answer "What is the capitalThe capital of France is Paris." [cast]
20
+ //
21
+ // Neither voiced structure was committed and both scored 9-34x BELOW the floor,
22
+ // while the root that licensed the weave supplied no bytes at all.
23
+ //
24
+ // WHY THE TEST READS THE TRACE AND NOT THE ANSWER. The answer text is the wrong
25
+ // probe: both the pre-fix and post-fix answers on this fixture are malformed
26
+ // concatenations, and which one happens to contain the right substring is
27
+ // cosmetic. That remaining garbling is a SEPARATE defect — CAST bidding at all
28
+ // on a plain factual question — and this file deliberately asserts nothing
29
+ // about it. The invariant is structural, so it is asserted structurally: the
30
+ // node CAST voices must be a node the climb committed.
31
+ //
32
+ // THE OTHER HALF OF THE CONTRACT is tests 2 and 3. Commitment is not the only
33
+ // warrant — a structure the asker QUOTED is content the query did ask about —
34
+ // and an analogy must still transfer from a structure the query never names,
35
+ // provided the climb settled on it. A gate that bought the invariant by killing
36
+ // either would be no fix at all.
37
+
38
+ import { test } from "node:test";
39
+ import assert from "node:assert/strict";
40
+ import { Mind } from "../dist/src/index.js";
41
+ import { SQliteStore } from "../dist/src/store-sqlite.js";
42
+
43
+ const CHAIN = [
44
+ ["Eiffel Tower country", "The country of Eiffel Tower is France."],
45
+ ["France capital", "The capital of France is Paris."],
46
+ ["France", "The capital of France is Paris."],
47
+ ];
48
+ const TWO_HOP = "What is the capital of the country of Eiffel Tower?";
49
+ // The SHAPE matters: this filler family reproduces the defect at every N from
50
+ // 50 up, across seeds 7/42/99 and D 512/1024, and survives a full entity
51
+ // rename — so it is a structural fixture, not a byte-pattern one.
52
+ const filler = (i) => [
53
+ `What is the status of my request ${i}?`,
54
+ `I have token number ${i} waiting.`,
55
+ ];
56
+
57
+ /** Run one query; collect the committed anchors and the structures CAST voiced. */
58
+ async function voicedVsCommitted(query, train) {
59
+ const store = new SQliteStore({ path: ":memory:", D: 1024 });
60
+ const mind = new Mind({ seed: 7, store });
61
+ await mind.ingest(train);
62
+ const committed = new Set();
63
+ const voiced = [];
64
+ await mind.respondText(query, (s) => {
65
+ const name = s.mechanism[s.mechanism.length - 1];
66
+ if (name === "climbConsensus") {
67
+ for (const o of s.outputs ?? []) {
68
+ if (o.role === "anchor" && o.node !== undefined) committed.add(o.node);
69
+ }
70
+ }
71
+ // What each schema transfers THROUGH: substitution voices the displaced
72
+ // structure's tail plus its own continuation; redirection voices the named
73
+ // substitute's own fact.
74
+ if (name === "projectCounterfactual") {
75
+ for (const i of s.inputs ?? []) {
76
+ if (
77
+ (i.role === "displaced-structure" || i.role === "substitute") &&
78
+ i.node !== undefined
79
+ ) voiced.push({ node: i.node, text: String(i.text ?? "") });
80
+ }
81
+ }
82
+ });
83
+ await store.close();
84
+ return { committed, voiced };
85
+ }
86
+
87
+ test("CAST voices only a structure the climb committed to", async () => {
88
+ // THE REGRESSION. Without the gate the displaced structure is #90
89
+ // ("France capital", uncommitted, vote 0.57) while the climb's only
90
+ // committed root is #5 — so `voiced` is not a subset of `committed`.
91
+ const { committed, voiced } = await voicedVsCommitted(TWO_HOP, [
92
+ ...CHAIN,
93
+ ...Array.from({ length: 50 }, (_, i) => filler(i)),
94
+ ]);
95
+ assert.ok(
96
+ committed.size > 0,
97
+ "fixture no longer reaches the consensus climb",
98
+ );
99
+ assert.ok(
100
+ voiced.length > 0,
101
+ "fixture no longer fires a CAST projection — it no longer covers the defect",
102
+ );
103
+ for (const v of voiced) {
104
+ assert.ok(
105
+ committed.has(v.node),
106
+ `CAST voiced #${v.node} ${
107
+ JSON.stringify(v.text.slice(0, 48))
108
+ }, which the climb never committed (committed: ${
109
+ [...committed].join(", ")
110
+ })`,
111
+ );
112
+ }
113
+ });
114
+
115
+ test("a substitute the query NAMES may still be voiced", async () => {
116
+ // OVER-CORRECTION GUARD, and the reason `voiceable` is a disjunction.
117
+ // "what if the capital of France were Lyon?" names Lyon outright; the climb
118
+ // need not have committed it for the asker to have asked about it. Gating on
119
+ // commitment alone refuses this — it is test/29 B3, reproduced here so the
120
+ // dependency is visible from the file that introduces the gate.
121
+ const store = new SQliteStore({ path: ":memory:", D: 1024 });
122
+ const mind = new Mind({ seed: 7, store });
123
+ await mind.ingest([
124
+ ["what is the capital of France?", "Paris is the capital of France"],
125
+ ["what is the capital of Italy?", "Rome is the capital of Italy"],
126
+ ["Lyon is a city in France", "Lyon is known for its cuisine"],
127
+ ]);
128
+ const got = await mind.respondText(
129
+ "what if the capital of France were Lyon?",
130
+ );
131
+ await store.close();
132
+ assert.ok(
133
+ /Lyon/i.test(got) && !/Paris/i.test(got),
134
+ `the named substitute was refused — expected Lyon, not Paris: ${
135
+ JSON.stringify(got)
136
+ }`,
137
+ );
138
+ });
139
+
140
+ test("an analogy still transfers from a structure the query never names", async () => {
141
+ // THE CAPABILITY THE GATE MUST NOT COST. "steel is frigid" names neither the
142
+ // water context nor its property; transferring from it is exactly what CAST
143
+ // exists to do, and it is licensed here because the climb COMMITTED that
144
+ // structure (test/29 D1 is the same assertion from the other direction).
145
+ const store = new SQliteStore({ path: ":memory:", D: 1024 });
146
+ const mind = new Mind({ seed: 7, store });
147
+ await mind.ingest([
148
+ ["ice is cold so ice is brittle", "brittle"],
149
+ ["steel is hard so steel is strong", "strong"],
150
+ ["water is frigid so water is freezing", "freezing"],
151
+ ]);
152
+ const r = await mind.respond("steel is frigid");
153
+ const got = new TextDecoder().decode(r.bytes ?? new Uint8Array());
154
+ await store.close();
155
+ assert.equal(
156
+ r.provenance,
157
+ "cast",
158
+ `CAST must still fire — got ${r.provenance}`,
159
+ );
160
+ assert.ok(
161
+ /freezing/i.test(got),
162
+ `property transfer lost — expected "freezing", got ${JSON.stringify(got)}`,
163
+ );
164
+ });
@@ -0,0 +1,250 @@
1
+ // 87-codominant-commitment.test.mjs — when the estimator cannot separate two
2
+ // anchors, the climb must not pick one of them by coin flip.
3
+ //
4
+ // THE DEFECT THIS PINS. `commitVotes` exempts the DOMINANT anchor from both
5
+ // vote gates ("the first non-overlapping root is dominant and bypasses the two
6
+ // vote thresholds — it always grounds"), and holds every later anchor to
7
+ // `naturalBreak` AND an absolute `consensusFloor(N) = ln N + 1/2`. Which anchor
8
+ // gets the exemption is decided by a sort over ESTIMATED quantities. When the
9
+ // two are within the estimator's own resolution that sort is a coin flip — and
10
+ // the loser is then refused by a floor the winner never had to clear.
11
+ //
12
+ // MEASURED on the corpus below, 60 seeds per D. True separation of the two
13
+ // anchors is 0.54s / 0.75s / 1.04s (s = estimatorNoise(D) = 1/sqrt(D)):
14
+ //
15
+ // D s=1/sqrt(D) vote SD (estimated anchor) SD/s top flips
16
+ // 256 0.0625 0.0561 0.90 19/60
17
+ // 1024 0.0313 0.0268 0.86 12/60
18
+ // 4096 0.0156 0.0074 0.48 2/60
19
+ //
20
+ // The SD tracks 1/sqrt(D) and the flip rate collapses with it, so the
21
+ // reordering is the ESTIMATOR's, not the corpus's. One anchor's vote is
22
+ // constant across all 60 seeds (exact, content-addressed evidence); the other's
23
+ // varies. Meanwhile `consensusFloor(3) = 1.599` and neither anchor exceeds
24
+ // ~1.01, so the runner-up could NEVER commit: the query was allowed exactly one
25
+ // point of attention, chosen by noise.
26
+ //
27
+ // THE FIX is the co-dominant band: an anchor whose margin from the dominant is
28
+ // inside sqrt(k)*s inherits the dominant's exemption. sqrt(k) because a vote is
29
+ // a SUM over the anchor's k contributing regions, so its noise grows as
30
+ // sqrt(k) — a bare s would be the same category error as pricing an
31
+ // N-invariant count against an N-growing threshold. Both quantities are already
32
+ // in hand (`regionAxioms`, `estimatorNoise(D)`), so no constant is introduced.
33
+ //
34
+ // WHAT THIS FILE ASSERTS, AND WHY NOT THE OUTCOME. test/29 D2 asserts
35
+ // `provenance === "cast"` — a PROXY. It passed while the property its own title
36
+ // names ("site-aware climb is seed-independent") was false, because CAST voiced
37
+ // the runner-up regardless of what the climb committed. So this file asserts
38
+ // the commitment itself, read from the trace.
39
+ //
40
+ // NOT "the same anchors commit at every seed" — that was tried and it is FALSE,
41
+ // which is worth recording. At seed 7 the noise puts the two anchors 0.049
42
+ // apart while the runner-up's band is sqrt(2)*s = 0.044, so it is genuinely
43
+ // separated and correctly rejected. The band is a statement about resolution,
44
+ // not a promise of a fixed root set. The exact invariant is the one below: an
45
+ // anchor inside its own band of the dominant is never rejected by a gate the
46
+ // dominant was exempt from.
47
+
48
+ import { test } from "node:test";
49
+ import assert from "node:assert/strict";
50
+ import { Mind } from "../dist/src/index.js";
51
+ import { SQliteStore } from "../dist/src/store-sqlite.js";
52
+
53
+ const D = 1024;
54
+ const SIGMA = 1 / Math.sqrt(D); // estimatorNoise(D), by its own definition
55
+
56
+ // test/29 D1's corpus: "steel is frigid" aligns "steel is " with one context
57
+ // and "frigid" with another, and the two score within noise of each other.
58
+ const ANALOGY = [
59
+ ["ice is cold so ice is brittle", "brittle"],
60
+ ["steel is hard so steel is strong", "strong"],
61
+ ["water is frigid so water is freezing", "freezing"],
62
+ ];
63
+
64
+ /** The climb's per-anchor commit record for one query. */
65
+ async function commitRecord(seed, train, query) {
66
+ const store = new SQliteStore({ path: ":memory:", D });
67
+ const mind = new Mind({ seed, store });
68
+ await mind.ingest(train);
69
+ let anchors = [];
70
+ await mind.respondText(query, (s) => {
71
+ if (s.mechanism[s.mechanism.length - 1] !== "climbConsensus") return;
72
+ anchors = s.data?.anchors ?? anchors;
73
+ });
74
+ await store.close();
75
+ return anchors;
76
+ }
77
+
78
+ /** The anchor's own band: sqrt(k) * sigma, k = its contributing-vote count. */
79
+ const bandOf = (a) => Math.sqrt(Math.max(1, a.contributingVotes ?? 1)) * SIGMA;
80
+
81
+ test("an anchor inside its band of the dominant is never refused by the floor", async () => {
82
+ // THE REGRESSION, stated exactly. Without the band, seed 1 rejects the
83
+ // runner-up with ["below-natural-break","below-consensus-floor"] at a margin
84
+ // of 0.013 — a third of its own resolution — while the dominant that beat it
85
+ // by that margin was exempt from both gates. Measured at 6 of 24 seeds.
86
+ let rescued = 0;
87
+ for (const seed of [1, 7, 8, 18, 20, 22, 23, 42]) {
88
+ const anchors = await commitRecord(seed, ANALOGY, "steel is frigid");
89
+ const dom = anchors.find((a) => a.commit?.dominant);
90
+ assert.ok(dom !== undefined, `seed ${seed}: no dominant anchor recorded`);
91
+ for (const a of anchors) {
92
+ if (a === dom || a.commit?.status === "overlap") continue;
93
+ const margin = (dom.idfVote ?? 0) - (a.idfVote ?? 0);
94
+ if (margin >= bandOf(a)) continue; // genuinely separated — may be rejected
95
+ rescued++;
96
+ assert.equal(
97
+ a.commit?.status,
98
+ "root",
99
+ `seed ${seed}: anchor #${a.anchor} sits ${
100
+ margin.toFixed(5)
101
+ } from the ` +
102
+ `dominant — inside its own band of ${
103
+ bandOf(a).toFixed(5)
104
+ } — yet was ` +
105
+ `${a.commit?.status} for ${
106
+ JSON.stringify(a.commit?.rejectionReasons)
107
+ }, gates the dominant never had to clear`,
108
+ );
109
+ }
110
+ }
111
+ assert.ok(
112
+ rescued > 0,
113
+ "no anchor in the sweep landed inside its band — fixture no longer covers the defect",
114
+ );
115
+ });
116
+
117
+ test("a band-admitted root is genuinely inside its own sqrt(k)*sigma", async () => {
118
+ // WIDTH GUARD. The band must be exactly what it claims. Recomputed here from
119
+ // the trace's OWN recorded numbers, so a widened band cannot pass silently.
120
+ let checked = 0;
121
+ for (const seed of [1, 7, 20, 22]) {
122
+ const anchors = await commitRecord(seed, ANALOGY, "steel is frigid");
123
+ const dom = anchors.find((a) => a.commit?.dominant);
124
+ if (dom === undefined) continue;
125
+ for (const a of anchors) {
126
+ if (!a.commit?.tiedWithDominant) continue;
127
+ checked++;
128
+ const margin = (dom.idfVote ?? 0) - (a.idfVote ?? 0);
129
+ assert.ok(
130
+ margin < bandOf(a),
131
+ `seed ${seed}: anchor #${a.anchor} admitted as tied with margin ${
132
+ margin.toFixed(5)
133
+ } but its band is only ${bandOf(a).toFixed(5)}`,
134
+ );
135
+ }
136
+ }
137
+ assert.ok(
138
+ checked > 0,
139
+ "no root was admitted by the band — fixture no longer covers it",
140
+ );
141
+ });
142
+
143
+ test("band admissions are bounded by the QUERY, not by the corpus", async () => {
144
+ // THE COST CONTRACT. The band's direct cost is O(1) per anchor, but admitting
145
+ // a root is not free downstream: the weave, the schemas and fusion all read
146
+ // `roots`. So the question that matters is whether the number of admissions
147
+ // can grow with the corpus. It cannot, and the bound is structural rather
148
+ // than a cap: an anchor overlapping one already placed is ABSORBED before any
149
+ // vote gate is consulted, so rivals elected from the same query span can
150
+ // never all commit — the ceiling is the query's own count of pairwise
151
+ // non-overlapping spans.
152
+ //
153
+ // Adversarial fixture: K structures made near-identical with respect to the
154
+ // query, so their votes cluster inside each other's bands. Measured at
155
+ // K = 1, 2, 4, 8, 16 over seeds 1/7/42 — committed roots never exceeded 2 and
156
+ // did not rise with K; the extra candidates came back `overlap`, or never
157
+ // entered the ranked list at all.
158
+ const SUBJ = [
159
+ "water",
160
+ "juice",
161
+ "milk",
162
+ "cider",
163
+ "broth",
164
+ "syrup",
165
+ "cream",
166
+ "nectar",
167
+ "brine",
168
+ "tonic",
169
+ "lager",
170
+ "cocoa",
171
+ "gravy",
172
+ "toddy",
173
+ "mead",
174
+ "punch",
175
+ ];
176
+ const PROP = [
177
+ "freezing",
178
+ "icy",
179
+ "cold",
180
+ "chilled",
181
+ "frosty",
182
+ "numbing",
183
+ "glacial",
184
+ "bitter",
185
+ "raw",
186
+ "biting",
187
+ "harsh",
188
+ "sharp",
189
+ "stinging",
190
+ "crisp",
191
+ "keen",
192
+ "brisk",
193
+ ];
194
+ const rootsAt = async (K) => {
195
+ const train = [["steel is hard so steel is strong", "strong"]];
196
+ for (let i = 0; i < K; i++) {
197
+ train.push([
198
+ `${SUBJ[i]} is frigid so ${SUBJ[i]} is ${PROP[i]}`,
199
+ PROP[i],
200
+ ]);
201
+ }
202
+ const anchors = await commitRecord(7, train, "steel is frigid");
203
+ return anchors.filter((a) => a.commit?.status === "root").length;
204
+ };
205
+ const small = await rootsAt(1);
206
+ for (const K of [2, 4, 8, 16]) {
207
+ const n = await rootsAt(K);
208
+ assert.ok(
209
+ n <= Math.max(small, 2),
210
+ `${K} near-tied rivals produced ${n} committed roots — admissions are ` +
211
+ `growing with the corpus, not with the query`,
212
+ );
213
+ }
214
+ });
215
+
216
+ test("the band does not admit a clearly separated anchor", async () => {
217
+ // OVER-CORRECTION GUARD. The band is about indistinguishability, not
218
+ // generosity. Measured on this fixture the climb commits ONE root at vote
219
+ // 8.13 while its rivals sit at 0.57 and 0.15 — separations of 240s and 255s.
220
+ // If this ever commits more than one root, the band has become a blanket
221
+ // admission.
222
+ const chain = [
223
+ ["Eiffel Tower country", "The country of Eiffel Tower is France."],
224
+ ["France capital", "The capital of France is Paris."],
225
+ ["France", "The capital of France is Paris."],
226
+ ];
227
+ const filler = Array.from({ length: 50 }, (_, i) => [
228
+ `What is the status of my request ${i}?`,
229
+ `I have token number ${i} waiting.`,
230
+ ]);
231
+ const anchors = await commitRecord(
232
+ 7,
233
+ [...chain, ...filler],
234
+ "What is the capital of the country of Eiffel Tower?",
235
+ );
236
+ const roots = anchors.filter((a) => a.commit?.status === "root");
237
+ assert.equal(
238
+ roots.length,
239
+ 1,
240
+ `expected the clearly dominant anchor alone; got ${
241
+ roots.map((r) => `#${r.anchor}@${Number(r.idfVote).toFixed(3)}`).join(
242
+ ", ",
243
+ )
244
+ }`,
245
+ );
246
+ assert.ok(
247
+ !roots[0].commit?.tiedWithDominant,
248
+ "the dominant root must not be marked as tied with itself",
249
+ );
250
+ });