@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,93 @@
1
+ // 114-alignment-budget-is-per-sweep.test.mjs — each alignment sweep owns its
2
+ // budget, so the NEAREST continuation survives either side's exhaustion.
3
+ //
4
+ // THE DEFECT THIS CLOSES (found by an adversarial review of the E3 commit).
5
+ // `alignAround` bounds the work it may spend on (queryGap, contextGap) pairs.
6
+ // The counter was shared between the RIGHT sweep and the LEFT sweep, so once the
7
+ // right side spent the whole budget the left loop's guard was false on entry:
8
+ // zero iterations, and the NEAREST left continuation was lost — the exact
9
+ // opposite of the law the commit states ("an exhausted budget drops the FAR
10
+ // continuations and never the near ones"). It was a regression against the
11
+ // pre-change tree, which bounded each sweep independently.
12
+ //
13
+ // WHAT IS PINNED, structurally and with no magic number: a pair whose two
14
+ // divergent flanks both exceed the work budget still aligns on BOTH sides of the
15
+ // anchor — the left match is present as its own query span, and so is the right.
16
+ // Before the fix the left one is missing, whatever the budget.
17
+
18
+ import { test } from "node:test";
19
+ import assert from "node:assert/strict";
20
+ import { alignAround } from "../dist/src/mind/match.js";
21
+
22
+ const enc = new TextEncoder();
23
+ const dec = new TextDecoder();
24
+
25
+ /** The minimal context `alignAround` reads. There is no budget to inject any
26
+ * more: the sweep's work is proportional to the bytes a run spans. */
27
+ const ctxWith = () => ({ space: { maxGroup: 4 } });
28
+
29
+ /** A shared head, a shared anchor, and divergent flanks on both sides. */
30
+ function pair(flankLen) {
31
+ const q = `MATCH${"a".repeat(6)}SEED${"Q".repeat(flankLen)}`;
32
+ const c = `MATCH${"b".repeat(11)}SEED${"W".repeat(flankLen)}`;
33
+ return { q, c, at: q.indexOf("SEED") };
34
+ }
35
+
36
+ const spansOf = (res, text, needle) => {
37
+ const target = enc.encode(needle);
38
+ return res.matched.filter(([s, e]) => {
39
+ const got = enc.encode(text).subarray(s, e);
40
+ return got.length >= target.length &&
41
+ dec.decode(got).includes(needle);
42
+ });
43
+ };
44
+
45
+ test("both sides of the anchor align even when each flank exceeds the budget", () => {
46
+ // The budget must be reachable for the NEAR left continuation (its own
47
+ // 6+11-byte gap costs on the order of a hundred pair-explorations) and must be
48
+ // EXHAUSTED by the right flank (40x40 divergent bytes cost many hundreds).
49
+ // 512 sits between the two: it is the range where a shared counter starves the
50
+ // left sweep and a per-sweep one does not.
51
+ const { q, c, at } = pair(40);
52
+ for (const budget of [512, 4096]) {
53
+ const res = alignAround(
54
+ ctxWith(budget),
55
+ enc.encode(q),
56
+ enc.encode(c),
57
+ at,
58
+ at,
59
+ );
60
+ assert.ok(
61
+ spansOf(res, q, "MATCH").length > 0,
62
+ `the nearest LEFT continuation must survive the right sweep's budget ` +
63
+ `(budget ${budget}, matched ${JSON.stringify(res.matched)})`,
64
+ );
65
+ assert.ok(
66
+ spansOf(res, q, "SEED").length > 0,
67
+ "and the anchor itself is of course still matched",
68
+ );
69
+ }
70
+ });
71
+
72
+ test("the run chosen is the one with the smallest total gap", () => {
73
+ // The criterion the enumeration used to compute, now computed by the walk:
74
+ // smallest qGap + cGap, ties to the smaller query gap. Two continuations are
75
+ // offered at different totals and the NEARER one must win.
76
+ const enc = new TextEncoder();
77
+ const head = "common head ";
78
+ const far = "FAR";
79
+ const near = "NEAR";
80
+ const q = enc.encode(
81
+ head + "x".repeat(4) + near + "q" + "z".repeat(30) + far,
82
+ );
83
+ const c = enc.encode(head + "y".repeat(9) + near + "c");
84
+ const at = head.length - 1;
85
+ const { matched } = alignAround(ctxWith(), q, c, at, at);
86
+ const got = matched.map(([a, b]) =>
87
+ new TextDecoder().decode(q.subarray(a, b))
88
+ );
89
+ assert.ok(
90
+ got.includes("NEAR"),
91
+ "the nearest continuation must be found: " + JSON.stringify(got),
92
+ );
93
+ });
@@ -0,0 +1,100 @@
1
+ // 116-the-extension-is-gated-by-the-pipelines-own-remainder.test.mjs — EXTENSION
2
+ // judges the question by the pipeline's remainder, with the pipeline's W floor.
3
+ //
4
+ // TWO CORRECTIONS FROM THE ADVERSARIAL REVIEW, in one definition:
5
+ //
6
+ // M3 — the reasoner's `uncovered` was built from the ladder's `accounted`, a
7
+ // COST quantity. The pipeline itself documents that `accounted` can be EMPTY
8
+ // while nothing is unexplained (a query fully explained by one computed span
9
+ // plus bridged connectors), and it already builds the genuine remainder as
10
+ // `[...decided.accounted, ...pre.computed]`. The reasoner now uses that same
11
+ // reading — computed once, used by both the extension gate and the fuse gate.
12
+ //
13
+ // M4 — the boundary. The pipeline treats a remainder under W as bridging
14
+ // punctuation, and the reasoner inherits that floor. The subtlety the review
15
+ // raised is that a one-word tail CARRIES ITS SEPARATOR: `" why"` is exactly W
16
+ // bytes, so it IS a remainder and licenses no extension, while `" famous for"`
17
+ // is well over and does. Both sides are pinned here, because the boundary is
18
+ // where the reviewer's own demonstration sat.
19
+
20
+ import { test } from "node:test";
21
+ import assert from "node:assert/strict";
22
+ import { Mind } from "../dist/src/index.js";
23
+ import { SQliteStore } from "../dist/src/store-sqlite.js";
24
+
25
+ const LINKS = [
26
+ ["What is the capital of France", "The capital of France is Paris"],
27
+ ["Paris", "Paris is famous for the Eiffel Tower"],
28
+ ];
29
+
30
+ /** The test/110 chain: the second link is reachable only by hopping. */
31
+ async function chain() {
32
+ const store = new SQliteStore({ path: ":memory:", D: 1024 });
33
+ const mind = new Mind({ seed: 7, store, profile: true });
34
+ await mind.ingest(LINKS);
35
+ return mind;
36
+ }
37
+
38
+ const text = (resp) =>
39
+ new TextDecoder().decode(resp.bytes).replace(/\0+/g, "").trim();
40
+
41
+ test("a remainder the pipeline calls real licenses the hop", async () => {
42
+ const mind = await chain();
43
+ const out = text(
44
+ await mind.respond("What is the capital of France famous for"),
45
+ );
46
+ assert.equal(
47
+ out,
48
+ LINKS[1][1],
49
+ "a remainder well over one window must still reach the next link",
50
+ );
51
+ await mind.store.close();
52
+ });
53
+
54
+ test("a one-word remainder is exactly one window, and licenses nothing", async () => {
55
+ // `" why"` is W bytes once its separator is counted: per the pipeline's own
56
+ // floor it is a remainder, and per the extension law a remainder the step
57
+ // cannot carry licenses no hop — so the grounded fact stands, AND the refusal
58
+ // is visible: a brake that leaves no trace is the silent cut AGENTS §6
59
+ // forbids, so the rationale must say the extension was declined for want of
60
+ // question material.
61
+ const mind = await chain();
62
+ const steps = [];
63
+ const out = text(
64
+ await mind.respond(
65
+ "What is the capital of France why",
66
+ (s) => steps.push(s),
67
+ ),
68
+ );
69
+ assert.equal(
70
+ out,
71
+ LINKS[0][1],
72
+ "a step carrying none of the remainder must not be taken",
73
+ );
74
+ const refused = steps.find((s) => s.mechanism.at(-1) === "pivotRefused");
75
+ assert.ok(refused, "the refusal must be reported, not silent");
76
+ assert.match(
77
+ String(refused.note),
78
+ /carries none of the question material/,
79
+ "and it must say why it refused",
80
+ );
81
+ assert.ok(
82
+ (refused.outputs ?? []).length > 0,
83
+ "and name the material it left uncovered",
84
+ );
85
+ await mind.store.close();
86
+ });
87
+
88
+ test("a computed-span query has no phantom remainder (M3)", async () => {
89
+ // A pure computation: the ladder's `accounted` is not a coverage reading, so
90
+ // the reasoner must not see a remainder the pipeline says is not there. The
91
+ // check is that the answer is the computed one and the engine is not dragged
92
+ // into an unrelated chain.
93
+ const mind = await chain();
94
+ const out = text(await mind.respond("2+2"));
95
+ assert.ok(
96
+ out.length > 0,
97
+ `a computation must still be answered, got ${JSON.stringify(out)}`,
98
+ );
99
+ await mind.store.close();
100
+ });
@@ -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
+ });
@@ -0,0 +1,74 @@
1
+ // 118-the-join-reaches-a-key-off-the-cut.test.mjs — a deposited, CONTINUING key
2
+ // must be reachable even when its boundary is not a fold cut of the tail.
3
+ //
4
+ // WHY THIS EXISTS. The join builds `key = fact.bytes ‖ tail[0..len]` and used to
5
+ // try only the tail's own fold boundaries (plus its end). A boundary is not a
6
+ // proxy for a stored key's end: a deposited member's end is the end of ITS OWN
7
+ // stream, and the fold never emits a cut at a stream's end (geometry's `emit`
8
+ // guards `at >= n`) — so the boundary that names a real stored key can sit
9
+ // between cuts, or beyond every one of them. Measured on the five-fact chain
10
+ // below: the fourth hop's key "stockholm mayor" exists (resolve() finds it), its
11
+ // continuation is the mayor fact, and its boundary (p = 6) is NOT among the
12
+ // tail's cuts ([4,7]) — so the join could not try it and the answer stopped at
13
+ // the third fact.
14
+ //
15
+ // The candidate ends are now the prefixes that ARE stored nodes, ascending, from
16
+ // the host's `contentKeyEnds` — so the shortest name still wins, and every
17
+ // deposited key is reachable.
18
+ //
19
+ // WHAT IS PINNED. The chain reaches the FIFTH fact, and the join fires at least
20
+ // four times. The counters are the untraced view (AGENTS §6: profile without a
21
+ // trace attached).
22
+ import { test } from "node:test";
23
+ import assert from "node:assert/strict";
24
+ import { Mind } from "../dist/src/index.js";
25
+ import { SQliteStore } from "../dist/src/store-sqlite.js";
26
+
27
+ const F1 = "The director of Eva is Gustaf Molander.";
28
+ const F2 = "The country of Gustaf Molander is Sweden.";
29
+ const F3 = "The capital of Sweden is Stockholm.";
30
+ const F4 = "The mayor of Stockholm is Karin Wanngard.";
31
+ const F5 = "The party of Karin Wanngard is the Social Democrats.";
32
+
33
+ async function chain() {
34
+ const store = new SQliteStore({ path: ":memory:" });
35
+ const mind = new Mind({ seed: 7, store, profile: true });
36
+ await mind.ingest([
37
+ ["eva", F1],
38
+ ["eva director", F1],
39
+ ["gustaf molander", F2],
40
+ ["gustaf molander country", F2],
41
+ ["sweden", F3],
42
+ ["sweden capital", F3],
43
+ ["stockholm", F4],
44
+ ["stockholm mayor", F4],
45
+ ["karin wanngard", F5],
46
+ ["karin wanngard party", F5],
47
+ ]);
48
+ return { mind, store };
49
+ }
50
+
51
+ const text = (resp) => new TextDecoder().decode(resp.bytes).replace(/\0+/g, "");
52
+
53
+ test("a five-fact chain reaches the fifth fact, through four joins", async () => {
54
+ const { mind, store } = await chain();
55
+ const out = text(
56
+ await mind.respond("eva director country capital mayor party"),
57
+ ).trim();
58
+ const fired = mind.lastCost?.counters.joinFired ?? 0;
59
+ await store.close();
60
+
61
+ assert.equal(
62
+ out,
63
+ F5,
64
+ `the chain must reach the fifth fact. A key that EXISTS and LEADS ON ` +
65
+ `("stockholm mayor" → the mayor fact) was not tried because its boundary ` +
66
+ `is not a cut of the tail alone — the join was asked to extend from the ` +
67
+ `third fact and stopped (joinFired=${fired})`,
68
+ );
69
+ assert.ok(
70
+ fired >= 4,
71
+ `the join must fire once per licensed hop (measured ${fired}); four hops ` +
72
+ `were available: eva→gustaf molander→sweden→stockholm→karin wanngard`,
73
+ );
74
+ });
@@ -0,0 +1,122 @@
1
+ // 119-the-work-does-not-grow-with-the-corpus.test.mjs — the derivation's work
2
+ // must be FLAT in the corpus for a byte-identical answer.
3
+ //
4
+ // THE LAW. bounded-reads.md forbids per-query work that grows with N: a bigger
5
+ // store may answer differently (a better derivation can exist), but it must not
6
+ // buy arbitrary COMPUTATION for the same answer. test/89 measures this for the
7
+ // completion recursion against real prose, test/14 for recall reads; this closes
8
+ // the loop for the derivation as a whole after the F3/F4 migrations, where the
9
+ // hop allowance stopped deciding depth and the pivot's probe sweep got its own
10
+ // capacity.
11
+ //
12
+ // THE BARS ARE THE REPO'S OWN, not new numbers (trap 4). test/89 asserts
13
+ // `k < 0.6` for its `searches` (nested solve() calls — the recursion, which its
14
+ // fix governs end to end; measured 1.28 unfixed → 0.38 fixed) and `k < 1` for
15
+ // agenda pops — "only outright linear growth is the forbidden case … so this
16
+ // asserts the law itself", measured 1.40 unfixed → 0.54 fixed. Same helper as
17
+ // test/14 (`logLogSlope`), copied because suites here are standalone.
18
+ //
19
+ // MEASURED BEFORE THIS TEST EXISTED, on the fixture below, one call per size:
20
+ // pops 338 / 338 / 338 / 338 (k = 0.000), searches 2 everywhere, one pivot, and
21
+ // a byte-identical answer — flat, and flat by a wide margin.
22
+
23
+ import { test } from "node:test";
24
+ import assert from "node:assert/strict";
25
+ import { Mind } from "../dist/src/index.js";
26
+ import { SQliteStore } from "../dist/src/store-sqlite.js";
27
+
28
+ /** Power-law exponent k in t ≈ c·n^k, by log–log least squares.
29
+ * k≈0 flat · k≈1 linear · k≈2 quadratic. (test/14's helper.) */
30
+ function logLogSlope(sizes, times) {
31
+ const n = sizes.length;
32
+ const xs = sizes.map(Math.log), ys = times.map(Math.log);
33
+ const mx = xs.reduce((a, b) => a + b, 0) / n;
34
+ const my = ys.reduce((a, b) => a + b, 0) / n;
35
+ let num = 0, den = 0;
36
+ for (let i = 0; i < n; i++) {
37
+ num += (xs[i] - mx) * (ys[i] - my);
38
+ den += (xs[i] - mx) ** 2;
39
+ }
40
+ return num / den;
41
+ }
42
+
43
+ const WORDS =
44
+ ("alpha bravo charlie delta echo foxtrot golf hotel india juliet kilo lima " +
45
+ "mike november oscar papa").split(" ");
46
+
47
+ /** A three-fact chain the query closes through (test/110's LINKS), plus `n`
48
+ * unrelated pairs so the corpus grows without touching the derivation. */
49
+ const QUERY = "What is the capital of France famous for";
50
+
51
+ async function measure(n) {
52
+ const store = new SQliteStore({ path: ":memory:" });
53
+ const mind = new Mind({ seed: 7, store, profile: true });
54
+ const pairs = [
55
+ ["What is the capital of France", "The capital of France is Paris"],
56
+ ["Paris", "Paris is famous for the Eiffel Tower"],
57
+ ["the Eiffel Tower", "the Eiffel Tower is in Paris"],
58
+ ];
59
+ for (let i = 0; i < n; i++) {
60
+ pairs.push([
61
+ `note ${i} about ${WORDS[i % 16]} ${WORDS[(i * 3) % 16]}`,
62
+ `the ${WORDS[i % 16]} of ${WORDS[(i * 5) % 16]} is ${
63
+ WORDS[(i * 7) % 16]
64
+ } ${i}`,
65
+ ]);
66
+ }
67
+ await mind.ingest(pairs);
68
+ const answer = String(await mind.respondText(QUERY)).trim();
69
+ const c = mind.lastCost.counters;
70
+ await store.close();
71
+ return { answer, pops: c.searchPops ?? 0, searches: c.searches ?? 0 };
72
+ }
73
+
74
+ test("a bigger corpus does not buy more work for the same answer", async () => {
75
+ const base = 250;
76
+ const sizes = [];
77
+ const pops = [];
78
+ const searches = [];
79
+ const answers = [];
80
+ for (const mult of [1, 2, 4, 8]) {
81
+ const n = base * mult;
82
+ const r = await measure(n);
83
+ sizes.push(n);
84
+ pops.push(r.pops);
85
+ searches.push(r.searches);
86
+ answers.push(r.answer);
87
+ console.log(
88
+ ` N=${n} → pops ${r.pops}, searches ${r.searches}, ` +
89
+ `answer "${r.answer.slice(0, 32)}"`,
90
+ );
91
+ }
92
+
93
+ // The answer is the CONTROL: same answer, eight times the corpus. Without
94
+ // this the exponents below could be flat because the query stopped being
95
+ // answered at all.
96
+ assert.equal(
97
+ new Set(answers).size,
98
+ 1,
99
+ `the answer must not move with the corpus (got ${JSON.stringify(answers)})`,
100
+ );
101
+
102
+ const kSearches = logLogSlope(sizes, searches);
103
+ const kPops = logLogSlope(sizes, pops);
104
+ console.log(
105
+ ` growth exponents over N→8N: searches k=${kSearches.toFixed(2)}, ` +
106
+ `pops k=${kPops.toFixed(2)}`,
107
+ );
108
+
109
+ assert.ok(
110
+ kSearches < 0.6,
111
+ `nested searches grew with exponent k=${kSearches.toFixed(2)} in corpus ` +
112
+ `size (${searches.join(" → ")}) for a byte-identical answer`,
113
+ );
114
+ assert.ok(
115
+ kPops < 1,
116
+ `agenda pops grew with exponent k=${kPops.toFixed(2)} in corpus size ` +
117
+ `(${
118
+ pops.join(" → ")
119
+ }) for a byte-identical answer — k≈1 is work LINEAR ` +
120
+ `in the corpus, which bounded-reads.md forbids outright`,
121
+ );
122
+ });