@hviana/sema 0.8.0 → 0.8.2

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 (119) hide show
  1. package/AGENTS.md +22 -1
  2. package/DATASETS.md +1 -1
  3. package/dist/example/train_base/config.js +2 -2
  4. package/dist/example/train_base/corpora/massive.js +1 -1
  5. package/dist/example/train_base/readers.js +1 -1
  6. package/dist/src/config.d.ts +17 -0
  7. package/dist/src/config.js +18 -0
  8. package/dist/src/geometry.d.ts +10 -10
  9. package/dist/src/geometry.js +25 -24
  10. package/dist/src/meter.d.ts +29 -12
  11. package/dist/src/meter.js +58 -14
  12. package/dist/src/mind/attention.js +12 -12
  13. package/dist/src/mind/bridge.d.ts +8 -8
  14. package/dist/src/mind/bridge.js +33 -32
  15. package/dist/src/mind/corpus.d.ts +40 -0
  16. package/dist/src/mind/corpus.js +149 -0
  17. package/dist/src/mind/graph-search.d.ts +7 -8
  18. package/dist/src/mind/graph-search.js +244 -32
  19. package/dist/src/mind/index.d.ts +3 -1
  20. package/dist/src/mind/index.js +1 -0
  21. package/dist/src/mind/junction.d.ts +1 -1
  22. package/dist/src/mind/junction.js +8 -8
  23. package/dist/src/mind/learning.js +36 -35
  24. package/dist/src/mind/match.d.ts +8 -3
  25. package/dist/src/mind/match.js +156 -71
  26. package/dist/src/mind/mechanisms/cast.js +18 -2
  27. package/dist/src/mind/mechanisms/cover.js +19 -12
  28. package/dist/src/mind/mechanisms/prefix-completion.js +24 -24
  29. package/dist/src/mind/mechanisms/recall.js +38 -40
  30. package/dist/src/mind/mechanisms/reference.js +16 -16
  31. package/dist/src/mind/mind.d.ts +61 -7
  32. package/dist/src/mind/mind.js +72 -2
  33. package/dist/src/mind/pipeline-mechanism.d.ts +10 -8
  34. package/dist/src/mind/pipeline-mechanism.js +25 -21
  35. package/dist/src/mind/pipeline.d.ts +9 -9
  36. package/dist/src/mind/pipeline.js +49 -29
  37. package/dist/src/mind/primitives.d.ts +5 -5
  38. package/dist/src/mind/primitives.js +5 -5
  39. package/dist/src/mind/reasoning.d.ts +5 -1
  40. package/dist/src/mind/reasoning.js +54 -1
  41. package/dist/src/mind/recognition.d.ts +14 -13
  42. package/dist/src/mind/recognition.js +23 -23
  43. package/dist/src/mind/resonance.js +21 -21
  44. package/dist/src/mind/traverse.d.ts +54 -52
  45. package/dist/src/mind/traverse.js +83 -73
  46. package/dist/src/mind/types.d.ts +26 -4
  47. package/dist/src/store.d.ts +12 -12
  48. package/dist/src/store.js +12 -12
  49. package/docs/INDEX.md +2 -2
  50. package/docs/architecture/exact-vs-approximate.md +2 -1
  51. package/docs/architecture/fold-contract.md +1 -1
  52. package/docs/failures/tempting-but-wrong.md +33 -5
  53. package/docs/harness/gates.md +7 -7
  54. package/example/train_base/config.ts +2 -2
  55. package/example/train_base/corpora/massive.ts +1 -1
  56. package/example/train_base/readers.ts +1 -1
  57. package/jsr.json +1 -1
  58. package/package.json +1 -1
  59. package/src/config.ts +35 -0
  60. package/src/geometry.ts +25 -24
  61. package/src/meter.ts +61 -14
  62. package/src/mind/attention.ts +12 -12
  63. package/src/mind/bridge.ts +33 -32
  64. package/src/mind/corpus.ts +202 -0
  65. package/src/mind/graph-search.ts +261 -31
  66. package/src/mind/index.ts +8 -1
  67. package/src/mind/junction.ts +8 -8
  68. package/src/mind/learning.ts +36 -35
  69. package/src/mind/match.ts +163 -73
  70. package/src/mind/mechanisms/cast.ts +17 -1
  71. package/src/mind/mechanisms/cover.ts +18 -12
  72. package/src/mind/mechanisms/prefix-completion.ts +24 -24
  73. package/src/mind/mechanisms/recall.ts +38 -40
  74. package/src/mind/mechanisms/reference.ts +16 -16
  75. package/src/mind/mind.ts +129 -7
  76. package/src/mind/pipeline-mechanism.ts +25 -21
  77. package/src/mind/pipeline.ts +63 -38
  78. package/src/mind/primitives.ts +5 -5
  79. package/src/mind/reasoning.ts +55 -0
  80. package/src/mind/recognition.ts +23 -23
  81. package/src/mind/resonance.ts +21 -21
  82. package/src/mind/traverse.ts +83 -73
  83. package/src/mind/types.ts +30 -4
  84. package/src/store.ts +20 -20
  85. package/test/08-storage.test.mjs +1 -1
  86. package/test/100-complete-grounding-trace.test.mjs +109 -0
  87. package/test/101-alignment-gap-bound.test.mjs +106 -0
  88. package/test/102-production-composes-at-scale.test.mjs +110 -0
  89. package/test/103-alignment-gap-budget.test.mjs +89 -0
  90. package/test/104-composition-is-reported.test.mjs +90 -0
  91. package/test/105-derive-through-reports-its-refusal.test.mjs +113 -0
  92. package/test/106-the-join-fires.test.mjs +94 -0
  93. package/test/107-the-join-is-counted.test.mjs +81 -0
  94. package/test/108-the-join-chains.test.mjs +78 -0
  95. package/test/109-the-pivot-is-counted.test.mjs +60 -0
  96. package/test/110-the-reasoner-stops-when-the-question-is-answered.test.mjs +91 -0
  97. package/test/111-the-cover-assembly-is-counted.test.mjs +74 -0
  98. package/test/112-the-exploration-does-not-grow-with-the-hub.test.mjs +89 -0
  99. package/test/113-the-rationale-payload-is-bounded.test.mjs +84 -0
  100. package/test/114-alignment-budget-is-per-sweep.test.mjs +93 -0
  101. package/test/116-the-extension-is-gated-by-the-pipelines-own-remainder.test.mjs +100 -0
  102. package/test/117-corpus-search.test.mjs +171 -0
  103. package/test/14-scaling.test.mjs +10 -7
  104. package/test/35-prefix-edge.test.mjs +1 -1
  105. package/test/40-choosenext-scale-guard.test.mjs +16 -17
  106. package/test/56-bridge-identity-admission.test.mjs +6 -6
  107. package/test/70-prefix-completion.test.mjs +4 -3
  108. package/test/72-prefix-candidate-supply.test.mjs +3 -3
  109. package/test/73-scaffolding-only-bridge-abstains.test.mjs +6 -6
  110. package/test/75-multiturn-context-optimisation.test.mjs +5 -5
  111. package/test/76-reference-binding.test.mjs +6 -1
  112. package/test/84-composed-answer-honesty.test.mjs +5 -6
  113. package/test/88-dependency-footprint.test.mjs +1 -1
  114. package/test/89-completion-recursion.test.mjs +47 -19
  115. package/test/90-connector-read-cap.test.mjs +10 -8
  116. package/test/93-regime-prediction.test.mjs +10 -10
  117. package/test/94-cross-region-budget.test.mjs +2 -2
  118. package/test/95-wide-resonance-removed.test.mjs +8 -7
  119. package/test/96-bytes-walk-termination.test.mjs +3 -3
@@ -0,0 +1,171 @@
1
+ // 117-corpus-search.test.mjs — reading the trained memory back out of the DAG.
2
+ //
3
+ // WHAT IS PINNED. Two methods and their division of labour:
4
+ // • `searchCorpus(bytes, limit?)` — MULTIMODAL: bytes in, bytes out, no notion
5
+ // of text or encoding anywhere in it;
6
+ // • `searchCorpusText(text, limit?)` — the text case, which encodes, calls the
7
+ // multimodal one, and decodes. The search itself exists ONCE (src/mind/
8
+ // corpus.ts, over the machinery an answer already uses: `recognise` for the
9
+ // resolved subtrees, `edgeAncestors` for the climb, `nextFirst` for the
10
+ // continuation).
11
+ //
12
+ // Both are deterministic (same seed, same order, same query ⇒ byte-identical
13
+ // results), both report a miss as a STATE in the byte layer and as prose only in
14
+ // the text layer, and browsing takes the caller's own offset instead of a random
15
+ // draw.
16
+
17
+ import { test } from "node:test";
18
+ import assert from "node:assert/strict";
19
+ import { Mind, SQliteStore } from "../dist/src/index.js";
20
+
21
+ const enc = new TextEncoder();
22
+ const dec = new TextDecoder();
23
+
24
+ /** A small deposited corpus: three experience pairs, no trained store needed. */
25
+ async function fixture() {
26
+ const mind = new Mind({
27
+ seed: 7,
28
+ store: new SQliteStore({ path: ":memory:" }),
29
+ });
30
+ await mind.ingest([
31
+ ["the capital of France", "Paris is the capital of France."],
32
+ ["the capital of Portugal", "Lisbon is the capital of Portugal."],
33
+ ["who wrote Hamlet", "Shakespeare wrote Hamlet."],
34
+ ]);
35
+ return mind;
36
+ }
37
+
38
+ const asText = (b) => dec.decode(b).replace(/\0+/g, "").trim();
39
+
40
+ test("the multimodal search takes bytes and returns bytes", async () => {
41
+ const mind = await fixture();
42
+ const result = mind.searchCorpus(enc.encode("the capital of France"));
43
+ assert.ok(result.pairs.length > 0, "the deposited pair must be found");
44
+ const pair = result.pairs[0];
45
+ assert.ok(pair.context instanceof Uint8Array, "context is BYTES, not text");
46
+ assert.ok(pair.continuation instanceof Uint8Array);
47
+ assert.ok(typeof pair.contextId === "number");
48
+ assert.ok(asText(pair.context).includes("capital"));
49
+ assert.equal(result.browsed, false);
50
+ assert.equal(result.miss, "matched");
51
+ assert.ok(result.totalContexts > 0, "the store's own context count is read");
52
+ await mind.store.close();
53
+ });
54
+
55
+ test("the text helper is the SAME search, converted", async () => {
56
+ const mind = await fixture();
57
+ const bytes = mind.searchCorpus(enc.encode("the capital of France"));
58
+ const text = mind.searchCorpusText("the capital of France");
59
+ assert.equal(
60
+ text.pairs.length,
61
+ bytes.pairs.length,
62
+ "one search, two views — the helper must not run a second one",
63
+ );
64
+ assert.equal(text.pairs[0].contextId, bytes.pairs[0].contextId);
65
+ assert.equal(typeof text.pairs[0].context, "string");
66
+ assert.ok(text.pairs[0].context.includes("capital"));
67
+ assert.equal(text.note, undefined, "a match needs no note");
68
+ await mind.store.close();
69
+ });
70
+
71
+ test("both are deterministic across identical calls", async () => {
72
+ const mind = await fixture();
73
+ const a = mind.searchCorpus(enc.encode("the capital of France"));
74
+ const b = mind.searchCorpus(enc.encode("the capital of France"));
75
+ assert.deepEqual(
76
+ a.pairs.map((p) => [p.contextId, p.continuationId, asText(p.context)]),
77
+ b.pairs.map((p) => [p.contextId, p.continuationId, asText(p.context)]),
78
+ "same store + same query ⇒ the same pairs in the same order",
79
+ );
80
+ assert.deepEqual(
81
+ mind.searchCorpusText("the capital of France").pairs,
82
+ mind.searchCorpusText("the capital of France").pairs,
83
+ );
84
+ await mind.store.close();
85
+ });
86
+
87
+ test("a miss is a state in bytes and prose only in text", async () => {
88
+ const mind = await fixture();
89
+ const bytes = mind.searchCorpus(enc.encode("zzzq nothing at all"));
90
+ assert.equal(bytes.pairs.length, 0);
91
+ assert.ok(
92
+ bytes.miss === "nothing-resolved" || bytes.miss === "no-continuations",
93
+ `the byte layer reports a STATE, got ${bytes.miss}`,
94
+ );
95
+ assert.equal(bytes.note, undefined, "no prose in the byte layer");
96
+ const text = mind.searchCorpusText("zzzq nothing at all");
97
+ assert.equal(text.pairs.length, 0);
98
+ assert.equal(typeof text.note, "string", "the text layer says it in words");
99
+ await mind.store.close();
100
+ });
101
+
102
+ test("determinism holds ACROSS instances, not just across calls", async () => {
103
+ // The invariant is about the engine, not about one object: the same seed,
104
+ // the same deposit order and the same query must give byte-identical results
105
+ // from a FRESH Mind over a fresh store built the same way — both for a search
106
+ // (whose ids come from the store's intern order) and for a browse.
107
+ const a = await fixture();
108
+ const b = await fixture();
109
+ const shape = (r) =>
110
+ r.pairs.map((p) => [
111
+ p.contextId,
112
+ p.continuationId,
113
+ p.matchedBytes,
114
+ Array.from(p.context),
115
+ Array.from(p.continuation),
116
+ ]);
117
+ assert.deepEqual(
118
+ shape(a.searchCorpus(enc.encode("the capital of France"))),
119
+ shape(b.searchCorpus(enc.encode("the capital of France"))),
120
+ "two fresh minds over identically-built stores must agree byte for byte",
121
+ );
122
+ assert.deepEqual(
123
+ shape(a.sampleCorpus(2)),
124
+ shape(b.sampleCorpus(2)),
125
+ "and browsing must agree too — no draw from outside the seed",
126
+ );
127
+ assert.deepEqual(
128
+ shape(a.sampleCorpus(2, 0.25)),
129
+ shape(b.sampleCorpus(2, 0.25)),
130
+ );
131
+ await a.store.close();
132
+ await b.store.close();
133
+ });
134
+
135
+ test("a browse never shows the same context twice", async () => {
136
+ // Found by mutating the determinism test: on a store small relative to the
137
+ // probe budget the id stride revisits ids, so a browse returned the SAME pair
138
+ // over and over (measured: six copies of context #4 for `limit: 6`).
139
+ const mind = await fixture();
140
+ const r = mind.sampleCorpus(6);
141
+ const ids = r.pairs.map((p) => p.contextId);
142
+ assert.equal(
143
+ new Set(ids).size,
144
+ ids.length,
145
+ `every browsed pair must be a different context, got ${
146
+ JSON.stringify(ids)
147
+ }`,
148
+ );
149
+ await mind.store.close();
150
+ });
151
+
152
+ test("browsing is deterministic, and `from` moves the window", async () => {
153
+ const mind = await fixture();
154
+ const a = mind.sampleCorpus(2);
155
+ const b = mind.sampleCorpus(2);
156
+ assert.ok(
157
+ a.pairs.length >= 2,
158
+ "the fixture must yield at least two DISTINCT samples, or this proves nothing",
159
+ );
160
+ assert.deepEqual(
161
+ a.pairs.map((p) => [p.contextId, p.continuationId]),
162
+ b.pairs.map((p) => [p.contextId, p.continuationId]),
163
+ "browsing must not draw randomly",
164
+ );
165
+ const other = mind.sampleCorpus(2, 0.5);
166
+ assert.ok(
167
+ other.pairs.every((p) => p.matchedBytes === 0),
168
+ "browse samples carry no query match",
169
+ );
170
+ await mind.store.close();
171
+ });
@@ -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);
@@ -5,7 +5,7 @@
5
5
  // parents, or (halo > 0 ∧ already an edge source). Pure answers do
6
6
  // not qualify — they are destinations, not sources.
7
7
  //
8
- // All phrases verified via instrumentation first (see MISTAKES.md).
8
+ // All phrases verified via instrumentation first.
9
9
 
10
10
  import { test } from "node:test";
11
11
  import assert from "node:assert/strict";
@@ -10,23 +10,22 @@
10
10
  // already requires strict dominance; a tie leaves first-inserted as the
11
11
  // pick, exactly the "no real winner" case a floor would matter for.
12
12
  //
13
- // But chooseNext ALSO gated this pick on `bestSupport < consensusFloor(N)`
14
- // once the corpus scale crosses atomIsHub's threshold (traverse.ts:541-546)
15
- // — reusing the SAME ln(N)+0.5 floor recallByResonance and commitVotes use
16
- // for POOLED, IDF-weighted CLIMB VOTES (each region worth up to ln N, so a
17
- // sum exceeding ln N + 0.5 is more than any one region could say alone —
18
- // HOW_IT_WORKS.md §8.6). `prevCount(candidate)` is a different kind of
19
- // quantity: a raw count of how many training contexts independently
20
- // predicted ONE destination, bounded by how many times that specific fact
21
- // was retold — NOT by corpus size N. Gating an N-invariant count against
22
- // an N-growing threshold guarantees failure once N is large enough
23
- // (verified live: N≈325K gives a floor of ≈13.19, so a genuinely dominant
24
- // but only-doubly-attested fact like "capital of France → Paris" was
25
- // refused, falling back to a noisy concept-hop that produced the wrong
26
- // answer). HOW_IT_WORKS.md's own canonical chooseNext pseudocode (§25)
27
- // has NO such floor — it's undocumented implementation drift, not a
28
- // deliberate design surface. Fix: remove the gate; chooseNext's existing
29
- // strict-dominance loop already IS the "genuinely competing" test.
13
+ // But chooseNext ALSO gated this pick on `bestSupport < consensusFloor(N)` once
14
+ // the corpus scale crosses atomIsHub's threshold (traverse.ts:541-546) —
15
+ // reusing the SAME ln(N)+0.5 floor recallByResonance and commitVotes use for
16
+ // POOLED, IDF-weighted CLIMB VOTES (each region worth up to ln N, so a sum
17
+ // exceeding ln N + 0.5 is more than any one region could say alone —
18
+ // thresholds.md). `prevCount(candidate)` is a different kind of quantity: a raw
19
+ // count of how many training contexts independently predicted ONE destination,
20
+ // bounded by how many times that specific fact was retold — NOT by corpus size
21
+ // N. Gating an N-invariant count against an N-growing threshold guarantees
22
+ // failure once N is large enough (verified live: N≈325K gives a floor of
23
+ // ≈13.19, so a genuinely dominant but only-doubly-attested fact like "capital
24
+ // of France → Paris" was refused, falling back to a noisy concept-hop that
25
+ // produced the wrong answer). The canonical `chooseNext` pseudocode has NO such
26
+ // floor — it is undocumented implementation drift, not a deliberate design
27
+ // surface. Fix: remove the gate; chooseNext's existing strict-dominance loop
28
+ // already IS the "genuinely competing" test.
30
29
 
31
30
  import { test } from "node:test";
32
31
  import assert from "node:assert/strict";
@@ -3,13 +3,13 @@
3
3
  // separated from the query only by material that does not change what the
4
4
  // text SAYS, is the SAME learnt form and grounds through its own edge.
5
5
  //
6
- // "Material that does not change what it says" has ONE definition here, and
7
- // it is read from the corpus, never tuned (AGENTS §2.7, corpus-global
6
+ // "Material that does not change what it says" has ONE definition here, and it
7
+ // is read from the corpus, never tuned (commonality.md, corpus-global
8
8
  // population): a span is EXPLAINED when it is sub-quantum (< W — typographic
9
- // glue) or every W-window in it is COMMON by the store's own climb (the
10
- // ascent saturates, or it reaches a majority of contexts). A window that
11
- // reaches NOTHING is novel content and is never explained — the reading that
12
- // separates a droppable "the process of " from a load-bearing "heavy ".
9
+ // glue) or every W-window in it is COMMON by the store's own climb (the ascent
10
+ // saturates, or it reaches a majority of contexts). A window that reaches
11
+ // NOTHING is novel content and is never explained — the reading that separates
12
+ // a droppable "the process of " from a load-bearing "heavy ".
13
13
  //
14
14
  // THE GAP THIS CLOSES (measured on the 17.9M-node trained store). The query
15
15
  // `Who wrote Romeo and Juliet?` against the trained `Who wrote "Romeo and
@@ -1,9 +1,10 @@
1
1
  // 70-prefix-completion.test.mjs — a query that IS the opening of one trained
2
2
  // form is completed by that form's remainder; anything less is refused.
3
3
  //
4
- // WHAT THE MECHANISM DOES (src/mind/prefix-completion.ts): when every other
5
- // tier has declined, scan the candidate list recall's refusal path has ALREADY
6
- // fetched and look for a trained form whose bytes literally BEGIN with the whole
4
+ // WHAT THE MECHANISM DOES (src/mind/mechanisms/prefix-completion.ts): when
5
+ // every other tier has declined, scan the candidate list recall's refusal path
6
+ // has ALREADY fetched and look for a trained form whose bytes literally BEGIN
7
+ // with the whole
7
8
  // query. The answer is that form's own remainder — never an invention.
8
9
  //
9
10
  // WHY IT IS NEEDED, measured on the 15.7M-node trained store:
@@ -98,9 +98,9 @@ test("a proper prefix reaches its trained form through the window supply", async
98
98
  "a FORM is grounded whole, never a slice cut at the query's end",
99
99
  );
100
100
 
101
- // HONEST DEGRADATION (§2.13). A query with no discriminative window must
102
- // propose nothing rather than guess — silence is the correct answer, and a
103
- // supply that widened until it found something would be the real defect.
101
+ // HONEST DEGRADATION (INVARIANTS.md). A query with no discriminative window
102
+ // must propose nothing rather than guess — silence is the correct answer, and
103
+ // a supply that widened until it found something would be the real defect.
104
104
  const hub = enc("The ");
105
105
  assert.equal(
106
106
  prefixCompletion(m, hub, formsOpenedBy(m, hub)),
@@ -2,16 +2,16 @@
2
2
  // ABSTAIN when every literal span it did not substitute is corpus-global
3
3
  // scaffolding.
4
4
  //
5
- // THE DEFECT THIS PINS. A bridge grounds through the literal spans it did NOT
6
- // substitute; those anchors are the whole of its evidence. The anchor scan
5
+ // THE DEFECT THIS PINS. A bridge grounds through the literal spans it did NOT
6
+ // substitute; those anchors are the whole of its evidence. The anchor scan
7
7
  // ranked them by containment but rejected only the ones with ZERO containers,
8
8
  // so a query made entirely of scaffolding still bridged — the single
9
9
  // substituted span carried the whole semantic load, and the answer was voiced
10
- // with confidence. Measured on the trained store (hubBound 571): "What is the
10
+ // with confidence. Measured on the trained store (hubBound 571): "What is the
11
11
  // capital of" has 19 anchors, ALL saturated ("What":572, "hat ":572, "at i":572
12
- // …), and answered with an unrelated trained context about an integral. That
13
- // breaks honest silence (§2.13), which is worse than a gap: a gap is visible, a
14
- // fabrication is not.
12
+ // …), and answered with an unrelated trained context about an integral. That
13
+ // breaks honest silence (INVARIANTS.md), which is worse than a gap: a gap is
14
+ // visible, a fabrication is not.
15
15
  //
16
16
  // WHY THIS IS NOT A PROBE-SHAPED PATCH. The gate was falsified against the
17
17
  // queries the bridge answers CORRECTLY before it was written, and every one of
@@ -966,11 +966,11 @@ test("F1: every turn of a trained conversation is answered exactly", async () =>
966
966
  test("F1b: attaching a trace changes no answer — the audit layer is inert", async () => {
967
967
  // The mind's ONLY text-shaped code lives in the rationale/trace payloads:
968
968
  // attention.ts's `dec` helper decodes bytes and collapses whitespace so an
969
- // audit line is readable, and frame-filler builds diagnostic strings the
970
- // same way. Neither may ever reach a decision — nothing in the core knows
971
- // what "whitespace" is (see canon.ts's header, and AGENTS §2.11: profile
972
- // and trace must not move an answer). Asserted here rather than assumed,
973
- // because the formatting sits inside the same functions that decide.
969
+ // audit line is readable, and frame-filler builds diagnostic strings the same
970
+ // way. Neither may ever reach a decision — nothing in the core knows what
971
+ // "whitespace" is (see canon.ts's header, and memoization.md: profile and
972
+ // trace must not move an answer). Asserted here rather than assumed, because
973
+ // the formatting sits inside the same functions that decide.
974
974
  const pairs = [
975
975
  [
976
976
  "who painted the weeping woman",
@@ -144,7 +144,12 @@ test("the frame inventory REPORTS without judging", async () => {
144
144
  "the country where the Eiffel Tower is",
145
145
  );
146
146
  assert.equal(clean(inst.slots[0].filler), "France");
147
- assert.equal(inst.covered, 23, "coverage must be reported, not judged");
147
+ // 24, not 23: the alignment's gap bound is the PAIR's extent now (budgeted),
148
+ // so the frame's constant run is no longer cut one byte short by
149
+ // chainReach(W). The contract this test pins — REPORT without judging — is
150
+ // untouched: the substitution is still reported (and still refused by
151
+ // `voiceable` below).
152
+ assert.equal(inst.covered, 24, "coverage must be reported, not judged");
148
153
 
149
154
  // 2. An INSERTION — a real variation, and not a slot anything can carry.
150
155
  const ins = frameSlots(
@@ -32,8 +32,7 @@
32
32
  //
33
33
  // TO REPRODUCE THE REAL FAILURE: build the same chain, then ingest ~6,000
34
34
  // deposits produced by the Taskmaster adapter (example/train_base/corpora/
35
- // taskmaster.ts) from
36
- // TM-2/TM-3/TM-4, and ask the two-hop question. See FINDINGS.md §A1/§A4.
35
+ // taskmaster.ts) from TM-2/TM-3/TM-4, and ask the two-hop question.
37
36
 
38
37
  import { test } from "node:test";
39
38
  import assert from "node:assert/strict";
@@ -93,10 +92,10 @@ test("each hop still answers on its own — the substrate is intact", async () =
93
92
 
94
93
  test("a two-hop query composes or stays silent — it never fabricates", async () => {
95
94
  // THE CONTRACT. Three outcomes are conceivable and only two are acceptable:
96
- // compose -> the answer contains Paris
97
- // silence -> the empty answer, which is honest (AGENTS §2.13)
98
- // fabricate-> an assembly carrying content from an unrelated deposit
99
- // The third is what a store past the real-text ceiling actually does.
95
+ // compose -> the answer contains Paris silence -> the empty answer, which is
96
+ // honest (INVARIANTS.md) fabricate-> an assembly carrying content from an
97
+ // unrelated deposit The third is what a store past the real-text ceiling
98
+ // actually does.
100
99
  const mind = await storeWithDistractors();
101
100
  const answer = await mind.respondText(TWO_HOP);
102
101
 
@@ -1,6 +1,6 @@
1
1
  // 88 — the dependency footprint is a PRODUCT PROPERTY, so it is tested.
2
2
  //
3
- // AGENTS.md §6: "do not add runtime dependencies casually — the near-zero-
3
+ // AGENTS.md §7: "do not add runtime dependencies casually — the near-zero-
4
4
  // dependency footprint is a product feature." A feature stated only in prose
5
5
  // erodes; this suite pins it at the two places it can actually break.
6
6
  //
@@ -1,20 +1,21 @@
1
1
  // 89-completion-recursion.test.mjs — the completion recursion must be
2
2
  // OUTPUT-SENSITIVE.
3
3
  //
4
- // AGENTS §2.8: "No per-query read may grow with the corpus." §2.8 enforces
5
- // that per READ (nextFirst, bytesPrefix, …), and every one of those caps holds.
6
- // What no guard covered is the NUMBER of reads: `recompleteNode`
4
+ // bounded-reads.md: "No per-query read may grow with the corpus." That law is
5
+ // enforced per READ (nextFirst, bytesPrefix, …), and every one of those caps
6
+ // holds. What no guard covered is the NUMBER of reads: `recompleteNode`
7
7
  // (src/mind/graph-search.ts) re-covers a produced node by calling `solve`
8
- // recursively, and each nested solve builds its own agenda and chart. Its own
8
+ // recursively, and each nested solve builds its own agenda and chart. Its own
9
9
  // doc states the intent —
10
10
  //
11
11
  // "its cost tracks the ANSWER's own structure, not how densely the corpus
12
12
  // interconnects the nodes passed through"
13
13
  //
14
14
  // — but argues termination from "Distinct node ids are finite and each finished
15
- // completion is memoised". Finite-in-the-corpus is exactly the bound §2.8
16
- // forbids, and the recursion is emitted at `cost: 0` while the nested cover's
17
- // own `cost` is computed and discarded, so A* has no gradient against depth.
15
+ // completion is memoised". Finite-in-the-corpus is exactly the bound
16
+ // bounded-reads.md forbids, and the recursion is emitted at `cost: 0` while the
17
+ // nested cover's own `cost` is computed and discarded, so A* has no gradient
18
+ // against depth.
18
19
  //
19
20
  // MEASURED on a trained store (18,938,834 nodes, edgeSourceCount 796,528):
20
21
  // `respond("Hi")` reached recursion depth 331 and 9.1 GB RSS in 56 s without
@@ -205,12 +206,37 @@ test("completion recursion: per-query work does not grow with the corpus", async
205
206
  // NO OUTPUT CONFOUND. Work is allowed to grow with the ANSWER. Pinning the
206
207
  // answer byte-for-byte across every size removes that defence entirely: any
207
208
  // growth measured below bought exactly nothing.
209
+ // NO OUTPUT CONFOUND — WITHOUT DEMANDING A CONSTANT ANSWER.
210
+ //
211
+ // This used to require a byte-identical answer across corpus sizes, on the
212
+ // reasoning that with the output moving, any work growth would stop being
213
+ // attributable to the corpus. That was true only while an offer cap froze
214
+ // what a hop could reach: with the cap gone the offer follows the corpus, and
215
+ // a bigger corpus legitimately licenses a different — here CHEAPER —
216
+ // derivation. Measured at the three sizes: the answer moved at the largest
217
+ // (34 B → 34 B → 41 B) while the work did NOT (searches 2/2/2, pops
218
+ // 428/430/384, falling). So the confound worth defending against is not
219
+ // "the answer moved" but "the work grew because the answer grew", and that is
220
+ // removed by dividing the work by the answer it produced — the reading the
221
+ // comment below already allows ("Work is allowed to grow with the ANSWER").
222
+ // The two raw bars below stay exactly as they were; this only ADDS a bar.
223
+ // BYTES, not UTF-16 code units: this law is priced per byte (PASS/byte,
224
+ // bounded-reads.md), so the denominator is the answer's own byte length even
225
+ // though respondText hands back a string.
226
+ const answerBytes = answers.map((a) => new TextEncoder().encode(a).length);
227
+ const perByte = pops.map((p, i) => p / Math.max(1, answerBytes[i]));
228
+ const kPerByte = logLogSlope(SIZES, perByte);
229
+ console.log(
230
+ ` answer-normalised pops/byte: ${
231
+ perByte.map((v) => v.toFixed(2)).join(" → ")
232
+ } · k ≈ ${kPerByte.toFixed(2)}`,
233
+ );
208
234
  assert.ok(
209
- answers.every((a) => a === answers[0]),
210
- `the answer changed across corpus sizes (${
211
- JSON.stringify(answers.map((a) => a.slice(0, 40)))
212
- }) — with the output moving, work growth is no longer attributable to the ` +
213
- `corpus alone`,
235
+ kPerByte < 1,
236
+ `answer-normalised work grew with exponent k=${kPerByte.toFixed(2)} in ` +
237
+ `corpus size — dividing by the answer's own length already removes the ` +
238
+ `output confound, so growth beyond that is work the answer never asked ` +
239
+ `for (bounded-reads.md)`,
214
240
  );
215
241
 
216
242
  const kSearches = logLogSlope(SIZES, searches);
@@ -221,8 +247,9 @@ test("completion recursion: per-query work does not grow with the corpus", async
221
247
  } (agenda pops) — target ≪ 1 (sublinear in the corpus)`,
222
248
  );
223
249
 
224
- // THE LAW. Same answer, more corpus, so cost must not move. k ≈ 0 is flat,
225
- // k ≈ 1 is linear in the corpus — the bound §2.8 forbids outright.
250
+ // THE LAW. Same answer, more corpus, so cost must not move. k ≈ 0 is flat, k
251
+ // ≈
252
+ // 1 is linear in the corpus — the bound bounded-reads.md forbids outright.
226
253
  //
227
254
  // `searches` counts nested solve() calls, which is the recursion itself and
228
255
  // nothing else, so it gets 14-scaling.test.mjs's stricter 0.6 bar. Measured
@@ -234,17 +261,18 @@ test("completion recursion: per-query work does not grow with the corpus", async
234
261
  `nested solve() builds its own agenda and chart, so this is the ` +
235
262
  `completion recursion doing work the answer never asked for`,
236
263
  );
237
- // A LOOSER BAR, FOR A REASON. `searchPops` aggregates the TOP-LEVEL cover's
264
+ // A LOOSER BAR, FOR A REASON. `searchPops` aggregates the TOP-LEVEL cover's
238
265
  // agenda too, and that one legitimately carries some corpus sensitivity: a
239
266
  // bigger store recognises more sites inside the same query, so more items are
240
- // admissible. Only outright linear growth is the forbidden case (§2.8), so
241
- // this asserts the law itself, k < 1, rather than the stricter 0.6 that suits
242
- // a counter the fix governs end to end. Measured 1.40 unfixed, 0.54 fixed.
267
+ // admissible. Only outright linear growth is the forbidden case
268
+ // (bounded-reads.md), so this asserts the law itself, k < 1, rather than the
269
+ // stricter 0.6 that suits a counter the fix governs end to end. Measured 1.40
270
+ // unfixed, 0.54 fixed.
243
271
  assert.ok(
244
272
  kPops < 1,
245
273
  `agenda pops grew with exponent k=${kPops.toFixed(2)} in corpus size (${
246
274
  pops.join(" → ")
247
275
  }) for a byte-identical answer — k≈1 is work LINEAR in the corpus, which ` +
248
- `is the bound §2.8 forbids outright`,
276
+ `is the bound bounded-reads.md forbids outright`,
249
277
  );
250
278
  });
@@ -7,10 +7,10 @@
7
7
  // substring search, so it needs the candidate's bytes; it used to reconstruct
8
8
  // them in FULL via `read(ctx, answer)`, whose maxLen defaults to ALL.
9
9
  //
10
- // AGENTS §2.8, prefix-capped reads: "a candidate that exceeds the cap is
10
+ // bounded-reads.md, prefix-capped reads: "a candidate that exceeds the cap is
11
11
  // rejected without reconstructing it — the weave, the junction walks and the
12
12
  // bridge all read this way, and uncapped reads there cost seconds per query on
13
- // a large store." This probe was the exception, and it runs hubBound(ctx) = √N
13
+ // a large store." This probe was the exception: it runs hubBound(ctx) = √N
14
14
  // times PER SITE.
15
15
  //
16
16
  // The probe's corpus-scale cost was once claimed from a trained-store
@@ -18,14 +18,15 @@
18
18
  // prompt" — but that number was measured on a `respond()` query, where the
19
19
  // probe does NOT execute (`answeredSpans` is empty there, so the enclosing
20
20
  // guard returns first). It is therefore not attributable to the probe and is
21
- // not repeated here (§2.16: a comment asserting a measurement inherits Gate 1).
21
+ // not repeated here (a comment asserting a measurement inherits Gate 1).
22
22
  // The probe runs only on a multi-turn `respondTurn` response; its benefit there
23
23
  // is still unmeasured.
24
24
  //
25
- // WHAT THIS PINS. The cap cannot reduce the read COUNT — only a semantic change
26
- // could (see below). It bounds each read by the QUERY, which is what §2.8 asks
27
- // and what rescues a SHORT query: candidates averaged 231 B reconstructed
28
- // against a 3-byte prompt. So the invariant here is per-read SIZE.
25
+ // WHAT THIS PINS. The cap cannot reduce the read COUNT — only a semantic change
26
+ // could (see below). It bounds each read by the QUERY, which is what
27
+ // bounded-reads.md asks and what rescues a SHORT query: candidates averaged 231
28
+ // B reconstructed against a 3-byte prompt. So the invariant here is per-read
29
+ // SIZE.
29
30
  //
30
31
  // It is measured by calling `resolveConnectors` DIRECTLY and diffing the meter
31
32
  // across it. A whole-response counter cannot express this: `bytesRead` sums
@@ -121,7 +122,8 @@ test("connector probe reads by the query, not by the learnt continuation", async
121
122
  `the connector probe averaged ${perRead.toFixed(0)} B per read for a ` +
122
123
  `${QUERY.length} B query — a candidate longer than the query cannot ` +
123
124
  `occur inside it, so it must be rejected on an overflow probe of ` +
124
- `${QUERY.length + 1} B, not reconstructed in full (AGENTS §2.8)`,
125
+ `${QUERY.length + 1} B, not reconstructed in full ` +
126
+ `(bounded-reads.md)`,
125
127
  );
126
128
  } finally {
127
129
  mind.endResponse();
@@ -1,17 +1,17 @@
1
1
  // 93-regime-prediction.test.mjs — the retrieval/composition regime (R8) is
2
2
  // exposed as a structured trace step, without changing inference.
3
3
  //
4
- // After the FIRST mechanism runs (cover, which §2.6 places first and floors at
5
- // 0), the market's whole outcome is already determined by the one cost ladder:
6
- // the consensus climb runs exactly when `worthRunning(2 * STEP)` is true —
7
- // CAST (floor 2·STEP) is the cheapest mechanism that first-touches it. An
8
- // incumbent at or below that floor prunes CAST and, with it, the climb
4
+ // After the FIRST mechanism runs (cover, which mechanism-market.md places first
5
+ // and floors at 0), the market's whole outcome is already determined by the one
6
+ // cost ladder: the consensus climb runs exactly when `worthRunning(2 * STEP)`
7
+ // is true — CAST (floor 2·STEP) is the cheapest mechanism that first-touches
8
+ // it. An incumbent at or below that floor prunes CAST and, with it, the climb
9
9
  // (retrieval); anything above — or no incumbent — runs the full market and the
10
- // climb (composition). The step is purely observational: it is built only
11
- // under a trace (optional-chaining short-circuits it otherwise), and it never
12
- // alters which candidate wins. The assertions here check the payload's
13
- // STRUCTURE and its consistency with the actual market outcome, never that
14
- // inference itself changed.
10
+ // climb (composition). The step is purely observational: it is built only under
11
+ // a trace (optional-chaining short-circuits it otherwise), and it never alters
12
+ // which candidate wins. The assertions here check the payload's STRUCTURE and
13
+ // its consistency with the actual market outcome, never that inference itself
14
+ // changed.
15
15
 
16
16
  import { test } from "node:test";
17
17
  import assert from "node:assert/strict";
@@ -1,7 +1,7 @@
1
1
  // 94-cross-region-budget.test.mjs — the cross-region junction ladder shares ONE
2
2
  // k·W allowance per evidence tier once atoms are hubs, instead of letting each
3
- // candidate pair spend its own √N·W drift budget (attention.ts crossRegionVotes,
4
- // §2.17's derived gate = traverse.atomIsHub).
3
+ // candidate pair spend its own √N·W drift budget (attention.ts
4
+ // crossRegionVotes, saturation.md's derived gate = traverse.atomIsHub).
5
5
  //
6
6
  // This is a PERFORMANCE regression test, not a behaviour test: the shared
7
7
  // budget is byte-identical at every scale — a pair whose container is not