@hviana/sema 0.8.1 → 0.8.3

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 (114) hide show
  1. package/AGENTS.md +29 -29
  2. package/TRADEMARKS.md +0 -1
  3. package/dist/src/config.d.ts +28 -0
  4. package/dist/src/config.js +20 -0
  5. package/dist/src/geometry.d.ts +21 -0
  6. package/dist/src/geometry.js +21 -0
  7. package/dist/src/meter.d.ts +76 -0
  8. package/dist/src/meter.js +95 -0
  9. package/dist/src/mind/attention.d.ts +4 -0
  10. package/dist/src/mind/attention.js +165 -16
  11. package/dist/src/mind/canonical.d.ts +16 -0
  12. package/dist/src/mind/canonical.js +41 -0
  13. package/dist/src/mind/corpus.d.ts +40 -0
  14. package/dist/src/mind/corpus.js +149 -0
  15. package/dist/src/mind/graph-search.d.ts +7 -0
  16. package/dist/src/mind/graph-search.js +254 -24
  17. package/dist/src/mind/index.d.ts +3 -1
  18. package/dist/src/mind/index.js +1 -0
  19. package/dist/src/mind/match.d.ts +9 -4
  20. package/dist/src/mind/match.js +147 -61
  21. package/dist/src/mind/mechanisms/cast.js +19 -3
  22. package/dist/src/mind/mechanisms/confluence.js +24 -0
  23. package/dist/src/mind/mechanisms/cover.js +6 -0
  24. package/dist/src/mind/mechanisms/recall.js +32 -4
  25. package/dist/src/mind/mind.d.ts +57 -0
  26. package/dist/src/mind/mind.js +72 -1
  27. package/dist/src/mind/pipeline-mechanism.d.ts +7 -0
  28. package/dist/src/mind/pipeline.js +66 -20
  29. package/dist/src/mind/primitives.js +9 -1
  30. package/dist/src/mind/rationale.d.ts +28 -1
  31. package/dist/src/mind/rationale.js +22 -1
  32. package/dist/src/mind/reasoning.d.ts +25 -3
  33. package/dist/src/mind/reasoning.js +125 -20
  34. package/dist/src/mind/recognition.js +4 -8
  35. package/dist/src/mind/resonance.js +20 -1
  36. package/dist/src/mind/trace.js +1 -0
  37. package/dist/src/mind/traverse.js +15 -3
  38. package/dist/src/mind/types.d.ts +49 -4
  39. package/docs/INVARIANTS.md +2 -2
  40. package/docs/architecture/bounded-reads.md +1 -1
  41. package/docs/architecture/commonality.md +2 -2
  42. package/docs/architecture/cost-model.md +2 -2
  43. package/docs/architecture/determinism.md +7 -7
  44. package/docs/architecture/match-project.md +2 -3
  45. package/docs/architecture/mechanism-market.md +10 -10
  46. package/docs/architecture/meter.md +5 -5
  47. package/docs/architecture/store.md +3 -3
  48. package/docs/failures/tempting-but-wrong.md +34 -6
  49. package/docs/harness/gates.md +2 -2
  50. package/docs/mechanisms/cast.md +2 -2
  51. package/docs/mechanisms/cover.md +2 -3
  52. package/docs/mechanisms/extraction.md +7 -7
  53. package/docs/mechanisms/recall.md +8 -9
  54. package/jsr.json +1 -1
  55. package/package.json +1 -1
  56. package/src/alu/README.md +11 -12
  57. package/src/config.ts +48 -0
  58. package/src/geometry.ts +21 -0
  59. package/src/meter.ts +98 -0
  60. package/src/mind/attention.ts +167 -16
  61. package/src/mind/canonical.ts +43 -0
  62. package/src/mind/corpus.ts +202 -0
  63. package/src/mind/graph-search.ts +277 -23
  64. package/src/mind/index.ts +8 -1
  65. package/src/mind/match.ts +148 -57
  66. package/src/mind/mechanisms/cast.ts +20 -2
  67. package/src/mind/mechanisms/confluence.ts +24 -0
  68. package/src/mind/mechanisms/cover.ts +5 -0
  69. package/src/mind/mechanisms/recall.ts +32 -4
  70. package/src/mind/mind.ts +125 -0
  71. package/src/mind/pipeline-mechanism.ts +7 -0
  72. package/src/mind/pipeline.ts +79 -22
  73. package/src/mind/primitives.ts +9 -1
  74. package/src/mind/rationale.ts +35 -1
  75. package/src/mind/reasoning.ts +145 -13
  76. package/src/mind/recognition.ts +4 -8
  77. package/src/mind/resonance.ts +19 -1
  78. package/src/mind/trace.ts +1 -0
  79. package/src/mind/traverse.ts +16 -6
  80. package/src/mind/types.ts +53 -4
  81. package/test/100-complete-grounding-trace.test.mjs +109 -0
  82. package/test/101-alignment-gap-bound.test.mjs +106 -0
  83. package/test/102-production-composes-at-scale.test.mjs +110 -0
  84. package/test/103-alignment-gap-budget.test.mjs +89 -0
  85. package/test/104-composition-is-reported.test.mjs +90 -0
  86. package/test/105-derive-through-reports-its-refusal.test.mjs +137 -0
  87. package/test/106-the-join-fires.test.mjs +94 -0
  88. package/test/107-the-join-is-counted.test.mjs +81 -0
  89. package/test/108-the-join-chains.test.mjs +78 -0
  90. package/test/109-the-pivot-is-counted.test.mjs +60 -0
  91. package/test/110-the-reasoner-stops-when-the-question-is-answered.test.mjs +91 -0
  92. package/test/111-the-cover-assembly-is-counted.test.mjs +74 -0
  93. package/test/112-the-exploration-does-not-grow-with-the-hub.test.mjs +89 -0
  94. package/test/113-the-rationale-payload-is-bounded.test.mjs +84 -0
  95. package/test/114-alignment-budget-is-per-sweep.test.mjs +93 -0
  96. package/test/116-the-extension-is-gated-by-the-pipelines-own-remainder.test.mjs +100 -0
  97. package/test/117-corpus-search.test.mjs +171 -0
  98. package/test/118-the-join-reaches-a-key-off-the-cut.test.mjs +74 -0
  99. package/test/119-the-work-does-not-grow-with-the-corpus.test.mjs +122 -0
  100. package/test/120-composition-is-consequence.test.mjs +132 -0
  101. package/test/121-the-extension-does-not-grow-with-the-corpus.test.mjs +128 -0
  102. package/test/122-the-climb-search-does-not-grow-with-the-corpus.test.mjs +117 -0
  103. package/test/123-the-paired-formulas-agree.test.mjs +90 -0
  104. package/test/125-the-post-grounding-branch-publishes-its-operand.test.mjs +51 -0
  105. package/test/126-the-pipeline-does-not-name-mechanisms.test.mjs +42 -0
  106. package/test/128-the-leads-somewhere-pair-agrees.test.mjs +83 -0
  107. package/test/129-the-trace-payload-shape.test.mjs +164 -0
  108. package/test/14-scaling.test.mjs +10 -7
  109. package/test/32-confluence.test.mjs +68 -0
  110. package/test/38-reason-restate-guard.test.mjs +8 -2
  111. package/test/43-cast-analog-seat.test.mjs +10 -0
  112. package/test/55-cost-meter.test.mjs +859 -0
  113. package/test/76-reference-binding.test.mjs +6 -1
  114. package/test/89-completion-recursion.test.mjs +30 -5
@@ -0,0 +1,164 @@
1
+ // 129-the-trace-payload-shape.test.mjs — THE PAYLOAD'S SHAPE, PINNED
2
+ //
3
+ // The climb's trace payload (`ClimbConsensusData`, `version: 1`) is built by
4
+ // hand next to the internal `TraceDraft` that feeds it, and the pair has
5
+ // already drifted once (`visited?` — "Absent on payloads recorded before this
6
+ // field existed"). The draft is NOT exported, so a test cannot compare the two
7
+ // side by side (and exporting it just for a test would be new plumbing the
8
+ // architecture forbids). What CAN be pinned from outside is the payload's
9
+ // SHAPE: a closed key set per section, so that a field added, removed or
10
+ // renamed on one side of the pair cannot pass unnoticed.
11
+ //
12
+ // The sets below are MEASURED from a real payload (printed before being
13
+ // written), not chosen. If this test fails, the payload changed: update the
14
+ // section it names AND the draft that feeds it AND the note in attention.ts
15
+ // that says which fields older payloads lack.
16
+ //
17
+ // The sections are optional by design (that is what keeps recorded payloads
18
+ // valid); this query produces ALL of them, and the test asserts that too, so a
19
+ // section silently disappearing is caught rather than tolerated.
20
+
21
+ import { test } from "node:test";
22
+ import assert from "node:assert/strict";
23
+ import { Mind } from "../dist/src/index.js";
24
+ import { SQliteStore } from "../dist/src/store-sqlite.js";
25
+
26
+ const CORPUS = [
27
+ ["red", "is a color"],
28
+ ["blue", "is a color"],
29
+ ["circle", "is a shape"],
30
+ ["square", "is a shape"],
31
+ ["red circle", "answer alpha"],
32
+ ["red square", "answer beta"],
33
+ ["blue circle", "answer gamma"],
34
+ ["blue square", "answer delta"],
35
+ ];
36
+
37
+ const TOP = [
38
+ "anchors",
39
+ "cache",
40
+ "candidates",
41
+ "config",
42
+ "crossRegion",
43
+ "pooling",
44
+ "reaches",
45
+ "regions",
46
+ "result",
47
+ "saturation",
48
+ "version",
49
+ ];
50
+ const REQUIRED = ["cache", "candidates", "config", "result", "version"];
51
+ const SECTIONS = {
52
+ cache: ["detailAvailable", "hit"],
53
+ config: [
54
+ "annK",
55
+ "consensusFloor",
56
+ "corpusN",
57
+ "crossRegionProbeLimit",
58
+ "dimension",
59
+ "estimatorNoise",
60
+ "hubBound",
61
+ "mode",
62
+ "naturalBreak",
63
+ ],
64
+ candidates: ["perceived", "recognised", "total"],
65
+ crossRegion: [
66
+ "eligibleRegions",
67
+ "junctionVotes",
68
+ "maximalRegions",
69
+ "probeLimit",
70
+ "probes",
71
+ "probesAttempted",
72
+ "stopReason",
73
+ "supersededOrdinaryVotes",
74
+ ],
75
+ saturation: ["hasLeading", "leadingEnd", "regionIntervals"],
76
+ pooling: ["eligibleVotes", "inputVotes", "saturationMaskedVotes"],
77
+ };
78
+ const ANCHOR = [
79
+ "anchor",
80
+ "breadth",
81
+ "candidateBreadth",
82
+ "clusters",
83
+ "commit",
84
+ "contributingEvidence",
85
+ "contributingSpans",
86
+ "contributingVotes",
87
+ "idfVote",
88
+ "peak",
89
+ "pooledVote",
90
+ "rank",
91
+ ];
92
+ const REGION = [
93
+ "annHitsExamined",
94
+ "annHitsReturned",
95
+ "annQueried",
96
+ "canonicalFailed",
97
+ "canonicalId",
98
+ "canonicalUsable",
99
+ "chunk",
100
+ "dfWeight",
101
+ "focusWeightPerRoot",
102
+ "idf",
103
+ "index",
104
+ "known",
105
+ "mutualWeight",
106
+ "ordinaryVoteProduced",
107
+ "outcome",
108
+ "reachNode",
109
+ "selected",
110
+ "source",
111
+ "span",
112
+ "superseded",
113
+ "voteWeightPerRoot",
114
+ ];
115
+
116
+ /** The payload, found the way a reader finds it: deep in the recorded steps. */
117
+ function findPayload(steps) {
118
+ const walk = (o, depth) => {
119
+ if (o === null || typeof o !== "object" || depth > 6) return null;
120
+ if (o.version === 1 && o.config && o.result) return o;
121
+ for (const v of Object.values(o)) {
122
+ const hit = walk(v, depth + 1);
123
+ if (hit) return hit;
124
+ }
125
+ return null;
126
+ };
127
+ for (const step of steps) {
128
+ const hit = walk(step.data, 0);
129
+ if (hit) return hit;
130
+ }
131
+ return null;
132
+ }
133
+
134
+ test("the climb trace payload keeps its published shape", async () => {
135
+ const m = new Mind({ seed: 1, store: new SQliteStore({ path: ":memory:" }) });
136
+ await m.ingest(CORPUS);
137
+ const steps = [];
138
+ await m.respondText("red then circle", (step) => steps.push(step));
139
+ const payload = findPayload(steps);
140
+ assert.ok(payload, "no version:1 payload with config+result was recorded");
141
+
142
+ assert.deepEqual(Object.keys(payload).sort(), TOP);
143
+ for (const key of REQUIRED) {
144
+ assert.ok(key in payload, `required section ${key} is missing`);
145
+ }
146
+ for (const [name, keys] of Object.entries(SECTIONS)) {
147
+ assert.ok(payload[name] !== undefined, `section ${name} is absent`);
148
+ assert.deepEqual(
149
+ Object.keys(payload[name]).sort(),
150
+ keys,
151
+ `section ${name} changed shape`,
152
+ );
153
+ }
154
+ assert.ok(payload.anchors.length > 0, "no anchors recorded");
155
+ assert.deepEqual(Object.keys(payload.anchors[0]).sort(), ANCHOR);
156
+ assert.ok(payload.regions.length > 0, "no regions recorded");
157
+ assert.deepEqual(Object.keys(payload.regions[0]).sort(), REGION);
158
+ assert.ok(
159
+ Array.isArray(payload.anchors[0].commit.rejectionReasons),
160
+ "an anchor's commit no longer carries its rejection reasons",
161
+ );
162
+
163
+ await m.store.close();
164
+ });
@@ -158,14 +158,17 @@ test("training: recall work does NOT grow with the store (storage reads, not tim
158
158
  for (let i = from; i < to; i++) await mind.ingest(novelExperience(i, "g"));
159
159
  };
160
160
 
161
- await grow(0, 300);
161
+ // 6x in RATIO is the signal; the absolute size is the cost. 100 -> 600
162
+ // keeps the same 6x step (and a SMALLER N makes the log-N growth relatively
163
+ // LARGER, so the 3x band below is not loosened by shrinking it).
164
+ await grow(0, 100);
162
165
  const small = await readsNow();
163
- await grow(300, 1800); // 6x the corpus
166
+ await grow(100, 600); // 6x the corpus
164
167
  const large = await readsNow();
165
168
  await store.close();
166
169
 
167
170
  console.log(
168
- ` content-index reads for one recall: N=300 → ${small}, N=1800 → ${large}`,
171
+ ` content-index reads for one recall: N=100 → ${small}, N=600 → ${large}`,
169
172
  );
170
173
 
171
174
  // 6x the corpus must not cost anywhere near 6x the reads. A genuinely
@@ -194,7 +197,7 @@ test("training: absolute deposition throughput clears a sane floor", async () =>
194
197
  // not one-time setup.
195
198
  for (let i = 0; i < 200; i++) await mind.ingest(novelExperience(i, "warm"));
196
199
 
197
- const N = 1000;
200
+ const N = 400;
198
201
  let bytes = 0;
199
202
  const t0 = performance.now();
200
203
  for (let i = 0; i < N; i++) {
@@ -322,7 +325,7 @@ test("training: exact recall is preserved at scale", async () => {
322
325
  // growth exponent in corpus size is the proof — it must be well below linear.
323
326
  test("inference: cost is sublinear in corpus size (independent corpora)", async () => {
324
327
  const query = unknownInput(1024);
325
- const sizes = [50, 200, 800, 3200];
328
+ const sizes = [50, 200, 800];
326
329
  const times = [];
327
330
 
328
331
  for (const n of sizes) {
@@ -370,7 +373,7 @@ test("inference: input is processed at a roughly constant KB/s (linear, not quad
370
373
  const mind = new Mind({ seed: 7, store });
371
374
  await mind.ingest(corpus(200, "inflen"));
372
375
 
373
- const kbs = [0.5, 1, 2, 4, 8];
376
+ const kbs = [0.5, 1, 2, 4];
374
377
  const queries = kbs.map((kb) => unknownInput(kb * 1024));
375
378
  const bytes = queries.map((q) => new TextEncoder().encode(q).length);
376
379
 
@@ -452,7 +455,7 @@ test("inference: completion still fires inside long inputs", async () => {
452
455
  });
453
456
  await mind.ingest([["ice", "cold"], ["fire", "hot"], ["2+2", "4"]]);
454
457
 
455
- for (const pad of [16, 64, 256, 1024]) {
458
+ for (const pad of [16, 64, 256]) {
456
459
  const filler = unknownInput(pad);
457
460
  const mid = filler.slice(0, filler.length >> 1);
458
461
  const end = filler.slice(filler.length >> 1);
@@ -171,3 +171,71 @@ test("E1 — the intersection is seed-independent (it is exact, not resonant)",
171
171
  await m.store.close();
172
172
  }
173
173
  });
174
+
175
+ // ═══════════════════════════════════════════════════════════════════════════
176
+ // Section F — the early-exit budget: live, but never pruning
177
+ // ═══════════════════════════════════════════════════════════════════════════
178
+
179
+ test("F1 — the two streams are found well inside the early-exit's budget", async () => {
180
+ // The mechanism scans the climb's ranked anchors with two cuts: `ranked.slice(0,
181
+ // pre.k)` and an early exit after `2W` anchors that returns null when fewer than
182
+ // two constraint streams exist. Both are spec-legal (a capacity, and a formula
183
+ // over W — config.ts's default `maxGroup` is 4) but neither was DERIVED; what
184
+ // makes them legitimate is that they do not prune the case they exist to protect.
185
+ //
186
+ // MEASURED on this corpus: the two streams appear at ranks 1 and 4 against the
187
+ // budget of 8, while `ranked` is 9 — the cut IS live (it would have returned
188
+ // null at the 8th anchor) and it does NOT prune. The last assertion is the
189
+ // anti-vacuity guard: if `ranked` ever fell to 8 or below, this test would be
190
+ // pinning nothing at all.
191
+ const W = 4; // the engine's default quantum: config.ts, geometry.maxGroup
192
+ const m = new Mind({
193
+ seed: 7,
194
+ store: new SQliteStore({ path: ":memory:" }),
195
+ profile: true,
196
+ });
197
+ await m.ingest(materials);
198
+ const steps = [];
199
+ await m.respondText(
200
+ "Which material is translucent and featherlight?",
201
+ (s) => steps.push(s),
202
+ );
203
+ await m.store.close();
204
+
205
+ const findAnchors = (o, depth = 0) => {
206
+ if (o === null || typeof o !== "object" || depth > 5) return null;
207
+ if (Array.isArray(o.anchors) && o.anchors.length) return o;
208
+ for (const v of Object.values(o)) {
209
+ const r = findAnchors(v, depth + 1);
210
+ if (r) return r;
211
+ }
212
+ return null;
213
+ };
214
+ const step = steps.find((s) => findAnchors(s.data ?? s) !== null);
215
+ const td = step ? findAnchors(step.data ?? step) : null;
216
+ assert.ok(td, "the climb must report its anchors");
217
+
218
+ const meet = steps.find((s) =>
219
+ (s.mechanism ?? []).join("/").includes("intersectEvidence")
220
+ );
221
+ assert.ok(meet, "confluence must ENTER on a conjunctive query");
222
+
223
+ const ranks = (meet.inputs ?? [])
224
+ .filter((i) => typeof i.node === "number")
225
+ .map((i) => (td.anchors.find((a) => a.anchor === i.node) ?? {}).rank);
226
+ assert.ok(
227
+ ranks.length >= 2,
228
+ `the meet needs two streams (got ${ranks.length})`,
229
+ );
230
+ assert.ok(
231
+ Math.max(...ranks) < 2 * W,
232
+ `the streams must appear inside the budget (ranks ${
233
+ ranks.join(",")
234
+ } vs 2W=${2 * W})`,
235
+ );
236
+ assert.ok(
237
+ td.anchors.length > 2 * W,
238
+ `and the cut must be live on this fixture, else this test pins nothing ` +
239
+ `(ranked=${td.anchors.length}, 2W=${2 * W})`,
240
+ );
241
+ });
@@ -57,7 +57,10 @@ test("reason(): a forward hop landing on bytes already present in the query is r
57
57
  queryResolved: resolve(m, query),
58
58
  };
59
59
 
60
- const out = await reason(m, query, answer, new Set(), pre);
60
+ // `reason()` reports the WHOLE extension (bytes + what it carried + how many
61
+ // steps) so a caller can price it; these assertions measure the bytes, and
62
+ // that is all this change touches — the law they pin is untouched.
63
+ const out = (await reason(m, query, answer, new Set(), pre)).bytes;
61
64
  assert.equal(
62
65
  dec(out),
63
66
  "bridge context",
@@ -82,7 +85,10 @@ test("reason(): an ordinary forward hop onto genuinely new content is unaffected
82
85
  queryResolved: resolve(m, query),
83
86
  };
84
87
 
85
- const out = await reason(m, query, answer, new Set(), pre);
88
+ // `reason()` reports the WHOLE extension (bytes + what it carried + how many
89
+ // steps) so a caller can price it; these assertions measure the bytes, and
90
+ // that is all this change touches — the law they pin is untouched.
91
+ const out = (await reason(m, query, answer, new Set(), pre)).bytes;
86
92
  assert.equal(
87
93
  dec(out),
88
94
  "a wholly new fact never mentioned in any query",
@@ -65,6 +65,7 @@ test("CAST comparison: a nextOf-descendant analog is seated by its own bytes, no
65
65
  const dominant = {
66
66
  anchor: franceId,
67
67
  vote: 100,
68
+ idfVote: 100, // the fixture builds an Attention: the field has to exist
68
69
  ctx: enc("What is the capital of France?"),
69
70
  runs: [{ qs: 0, qe: 30, cs: 0, weight: 1 }],
70
71
  };
@@ -75,6 +76,7 @@ test("CAST comparison: a nextOf-descendant analog is seated by its own bytes, no
75
76
  const spainPoint = {
76
77
  anchor: spainAnalogSrcId,
77
78
  vote: 50,
79
+ idfVote: 50, // the fixture builds an Attention: the field has to exist
78
80
  ctx: enc("some other prompt"),
79
81
  runs: [{ qs: 32, qe: 62, cs: 0, weight: 1 }],
80
82
  };
@@ -85,6 +87,7 @@ test("CAST comparison: a nextOf-descendant analog is seated by its own bytes, no
85
87
  {
86
88
  anchor: franceId,
87
89
  vote: 100,
90
+ idfVote: 100, // the fixture builds an Attention: the field has to exist
88
91
  start: 0,
89
92
  end: 30,
90
93
  breadth: 1,
@@ -95,6 +98,7 @@ test("CAST comparison: a nextOf-descendant analog is seated by its own bytes, no
95
98
  {
96
99
  anchor: franceId,
97
100
  vote: 100,
101
+ idfVote: 100, // the fixture builds an Attention: the field has to exist
98
102
  start: 0,
99
103
  end: 30,
100
104
  breadth: 1,
@@ -103,6 +107,7 @@ test("CAST comparison: a nextOf-descendant analog is seated by its own bytes, no
103
107
  {
104
108
  anchor: spainAnalogSrcId,
105
109
  vote: 50,
110
+ idfVote: 50, // the fixture builds an Attention: the field has to exist
106
111
  start: 32,
107
112
  end: 62,
108
113
  breadth: 0.5,
@@ -174,6 +179,7 @@ test("CAST comparison: a DIRECTLY aligned analog is never re-projected past its
174
179
  const dominant = {
175
180
  anchor: franceId,
176
181
  vote: 100,
182
+ idfVote: 100, // the fixture builds an Attention: the field has to exist
177
183
  ctx: enc("What is the capital of France?"),
178
184
  runs: [{ qs: 0, qe: 30, cs: 0, weight: 1 }],
179
185
  };
@@ -182,6 +188,7 @@ test("CAST comparison: a DIRECTLY aligned analog is never re-projected past its
182
188
  const japanPoint = {
183
189
  anchor: japanId,
184
190
  vote: 50,
191
+ idfVote: 50, // the fixture builds an Attention: the field has to exist
185
192
  ctx: enc("What is the capital of Japan? Tokyo is the capital of Japan."),
186
193
  runs: [{ qs: 32, qe: 62, cs: 0, weight: 1 }],
187
194
  };
@@ -192,6 +199,7 @@ test("CAST comparison: a DIRECTLY aligned analog is never re-projected past its
192
199
  {
193
200
  anchor: franceId,
194
201
  vote: 100,
202
+ idfVote: 100, // the fixture builds an Attention: the field has to exist
195
203
  start: 0,
196
204
  end: 30,
197
205
  breadth: 1,
@@ -202,6 +210,7 @@ test("CAST comparison: a DIRECTLY aligned analog is never re-projected past its
202
210
  {
203
211
  anchor: franceId,
204
212
  vote: 100,
213
+ idfVote: 100, // the fixture builds an Attention: the field has to exist
205
214
  start: 0,
206
215
  end: 30,
207
216
  breadth: 1,
@@ -210,6 +219,7 @@ test("CAST comparison: a DIRECTLY aligned analog is never re-projected past its
210
219
  {
211
220
  anchor: japanId,
212
221
  vote: 50,
222
+ idfVote: 50, // the fixture builds an Attention: the field has to exist
213
223
  start: 32,
214
224
  end: 62,
215
225
  breadth: 0.5,