@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,60 @@
1
+ // 109-the-pivot-is-counted.test.mjs — how far the reasoner hopped is a
2
+ // BEHAVIOUR, so it is observable without a rationale.
3
+ //
4
+ // WHY THIS EXISTS. EXTENSION (the study's gap 3): the pivot does not stop when
5
+ // the question is already satisfied — it hops on while the answer still contains
6
+ // an unconsumed learnt context (`reasoning.ts`: the loop breaks only on a null
7
+ // pivot, a null forward step, an unchanged answer, or a restatement). That is a
8
+ // property of how far the chain went, and until now it was visible only through
9
+ // `pivotStep` in the rationale — which PERTURBS the search (measured: appending
10
+ // text to a refusal note changed a traced answer). `meter.ts` is the untraced
11
+ // view and the one home for a counter name (AGENTS §6).
12
+ //
13
+ // WHAT IS PINNED. The pivot is counted on the fixture where the multi-hop
14
+ // genuinely needs it (test/23's two-fact chain), untraced. The assertion is
15
+ // "at least one", so it stays true whether or not the EXTENSION fix changes how
16
+ // many hops a satisfied question takes — the drift itself gets its own test
17
+ // once the criterion for "satisfied" is measured.
18
+
19
+ import { test } from "node:test";
20
+ import assert from "node:assert/strict";
21
+ import { Mind } from "../dist/src/index.js";
22
+ import { SQliteStore } from "../dist/src/store-sqlite.js";
23
+
24
+ /** test/23's two-fact chain: the pivot is what crosses the hop. */
25
+ async function pivotFixture() {
26
+ const store = new SQliteStore({ path: ":memory:" });
27
+ const mind = new Mind({ seed: 7, store, profile: true });
28
+ await mind.ingest([
29
+ ["What is the capital of France", "The capital of France is Paris"],
30
+ ["Paris", "Paris is famous for the Eiffel Tower"],
31
+ ]);
32
+ return mind;
33
+ }
34
+
35
+ test("the pivot is counted, with no rationale attached", async () => {
36
+ const mind = await pivotFixture();
37
+ const out = await mind.respondText(
38
+ "What is the capital of France famous for",
39
+ );
40
+ assert.ok(
41
+ out.includes("Eiffel"),
42
+ `the multi-hop must still chain, got ${JSON.stringify(out)}`,
43
+ );
44
+ assert.ok(
45
+ (mind.lastCost?.counters.pivotSteps ?? 0) > 0,
46
+ "the hop was taken, so it must be counted",
47
+ );
48
+ await mind.store.close();
49
+ });
50
+
51
+ test("a query that needs no hop counts none", async () => {
52
+ const mind = await pivotFixture();
53
+ await mind.respondText("What is the capital of France");
54
+ assert.equal(
55
+ mind.lastCost?.counters.pivotSteps ?? 0,
56
+ 0,
57
+ "a directly answered question pivots nowhere",
58
+ );
59
+ await mind.store.close();
60
+ });
@@ -0,0 +1,91 @@
1
+ // 110-the-reasoner-stops-when-the-question-is-answered.test.mjs — EXTENSION: a
2
+ // further chain link must not change the answer to a question already answered.
3
+ //
4
+ // THE GAP THIS CLOSES. Every stopping condition in the pivot loop judged the
5
+ // ANSWER; none asked whether the QUESTION was satisfied (reasoning.ts says so
6
+ // in its own words). Measured on a three-link chain, the reasoner took ONE hop
7
+ // MORE than the question needed and REPLACED the satisfying answer:
8
+ //
9
+ // elos=2 pivots=1 → "Paris is famous for the Eiffel Tower" (the answer)
10
+ // elos=3 pivots=2 → "the Eiffel Tower is in Paris" (one hop past it)
11
+ //
12
+ // THE INVARIANT (and the acceptance test, relational — no magic string):
13
+ //
14
+ // adding a link to the corpus must NOT change the answer to a question the
15
+ // shorter corpus already answered.
16
+ //
17
+ // WHAT MAKES IT PRINCIPLED, and what each candidate cost. Eight formulations
18
+ // were measured and refuted before this one — including a count of extensions,
19
+ // which is not a reason. This one uses two data the mind already has:
20
+ // • the cost ladder's own `unaccounted` spans (what the grounding left
21
+ // uncovered) — passed to the reasoner by the pipeline;
22
+ // • the mind's own line between chance and evidence — one W-byte window —
23
+ // so an extension counts as progress only if it carries a window of the
24
+ // uncovered material;
25
+ // • and the pipeline's EXISTING condition for who owns a shape: "only a
26
+ // mechanism carrying its own `used` set (cast/join) gets this" — which is
27
+ // exactly `voiced.length > 0`, since `voiced` is what the mechanism
28
+ // withheld. A producer owns its answer (test/29 C3's hop inside the
29
+ // comparison's seat fires); the reasoner's own extensions do not.
30
+ //
31
+ // It terminates by a real argument — the uncovered material is finite and each
32
+ // taken extension must carry some of it — never by a depth limit.
33
+
34
+ import { test } from "node:test";
35
+ import assert from "node:assert/strict";
36
+ import { Mind } from "../dist/src/index.js";
37
+ import { SQliteStore } from "../dist/src/store-sqlite.js";
38
+
39
+ const QUERY = "What is the capital of France famous for";
40
+
41
+ /** The chain with `n` links: each fact's subject continues into the next. */
42
+ const LINKS = [
43
+ ["What is the capital of France", "The capital of France is Paris"],
44
+ ["Paris", "Paris is famous for the Eiffel Tower"],
45
+ ["the Eiffel Tower", "the Eiffel Tower is in Paris"],
46
+ ];
47
+
48
+ async function chain(n) {
49
+ const store = new SQliteStore({ path: ":memory:" });
50
+ const mind = new Mind({ seed: 7, store, profile: true });
51
+ await mind.ingest(LINKS.slice(0, n));
52
+ return mind;
53
+ }
54
+
55
+ const text = (resp) => new TextDecoder().decode(resp.bytes).replace(/\0+/g, "");
56
+
57
+ test("a longer corpus does not change an answer already given", async () => {
58
+ const short = await chain(2);
59
+ const long = await chain(3);
60
+ const answered = text(await short.respond(QUERY)).trim();
61
+ const extended = text(await long.respond(QUERY)).trim();
62
+ assert.equal(
63
+ extended,
64
+ answered,
65
+ "adding a link must not push the chain past the answer",
66
+ );
67
+ // The expected answer is the fixture's OWN second link — the relation the
68
+ // question asked for — not a string copied into the test.
69
+ assert.equal(answered, LINKS[1][1]);
70
+ await short.store.close();
71
+ await long.store.close();
72
+ });
73
+
74
+ test("the counters agree: one hop, not two — with no rationale attached", async () => {
75
+ const mind = await chain(3);
76
+ await mind.respond(QUERY);
77
+ assert.equal(
78
+ mind.lastCost?.counters.pivotSteps ?? 0,
79
+ 1,
80
+ "the satisfying hop is taken and the drift is not",
81
+ );
82
+ await mind.store.close();
83
+ });
84
+
85
+ test("a question the first link already answers still takes no hop", async () => {
86
+ const mind = await chain(3);
87
+ const out = text(await mind.respond("What is the capital of France"));
88
+ assert.equal(out.trim(), "The capital of France is Paris");
89
+ assert.equal(mind.lastCost?.counters.pivotSteps ?? 0, 0);
90
+ await mind.store.close();
91
+ });
@@ -0,0 +1,74 @@
1
+ // 111-the-cover-assembly-is-counted.test.mjs — the connector assembly is
2
+ // countable without a rationale.
3
+ //
4
+ // WHY THIS EXISTS. LIMIT (the study's gap 4): a hub query's `cover.run` is 91%
5
+ // of its time (`"Hello."`: 2.7 s of 3.0 s) and holds its ~270 MB peak, and none
6
+ // of it was countable — `searchPushes` (3130) and `candidates` (2) do not see
7
+ // the connector assembly, and the rationale perturbs the search. `meter.ts` is
8
+ // the untraced view and the one home for a counter name (AGENTS §6).
9
+ //
10
+ // WHAT THE COUNTERS IMMEDIATELY SETTLED, and it is why they exist: `"Hello."`
11
+ // makes TWO bridge calls with 59 bytes of allowance — three orders of magnitude
12
+ // below its ~270 MB peak — so the connector assembly is NOT that peak. A
13
+ // hypothesis dies to a counter instead of to an afternoon of profiling.
14
+ //
15
+ // WHAT IS PINNED. Both counters are published, exercised by a fixture small
16
+ // enough to be a fixture (two touching contexts), and deterministic across
17
+ // identical calls. They stay true whatever the LIMIT fix turns out to be.
18
+
19
+ import { test } from "node:test";
20
+ import assert from "node:assert/strict";
21
+ import { Mind } from "../dist/src/index.js";
22
+ import { SQliteStore } from "../dist/src/store-sqlite.js";
23
+
24
+ async function fixture(pairs) {
25
+ const store = new SQliteStore({ path: ":memory:" });
26
+ const mind = new Mind({ seed: 7, store, profile: true });
27
+ await mind.ingest(pairs);
28
+ return mind;
29
+ }
30
+
31
+ test("the pairwise assembly is counted, with no rationale attached", async () => {
32
+ // Two contexts that TOUCH in the query (`ab`|`cd`), so the cover must bridge
33
+ // them pairwise.
34
+ const mind = await fixture([
35
+ ["ab", "x"],
36
+ ["cd", "y"],
37
+ ]);
38
+ const out = await mind.respondText("abcd");
39
+ const c = mind.lastCost?.counters ?? {};
40
+ assert.ok(
41
+ (c.coverBridges ?? 0) > 0,
42
+ "the cover bridged touching sites, so the calls must be counted",
43
+ );
44
+ assert.ok(out.length > 0, "and the answer still composes");
45
+ await mind.store.close();
46
+ });
47
+
48
+ test("the n-ary interior's allowance is counted", async () => {
49
+ // Three sites in one query: the n-ary path passes an allowance that grows
50
+ // with the intermediate answers' bytes — the quantity that would grow with a
51
+ // hub query's answers.
52
+ const mind = await fixture([
53
+ ["ab", "x"],
54
+ ["cd", "y"],
55
+ ["ab cd", "z"],
56
+ ]);
57
+ await mind.respondText("ab cd");
58
+ const c = mind.lastCost?.counters ?? {};
59
+ assert.ok(
60
+ (c.coverAllowanceBytes ?? 0) > 0,
61
+ "the n-ary interior ran, so its allowance must be counted",
62
+ );
63
+ await mind.store.close();
64
+ });
65
+
66
+ test("the counters are deterministic across identical calls", async () => {
67
+ const a = await fixture([["ab", "x"], ["cd", "y"]]);
68
+ const b = await fixture([["ab", "x"], ["cd", "y"]]);
69
+ await a.respondText("abcd");
70
+ await b.respondText("abcd");
71
+ assert.deepEqual(a.lastCost?.counters, b.lastCost?.counters);
72
+ await a.store.close();
73
+ await b.store.close();
74
+ });
@@ -0,0 +1,89 @@
1
+ // 112-the-exploration-does-not-grow-with-the-hub.test.mjs
2
+ //
3
+ // THE OFFER IS THE CORPUS'S STRUCTURE. A chain hop offers the continuations the
4
+ // corpus holds, and the search pays for exploring them on the ladder (every hop
5
+ // costs STEP). What must NOT happen is a combinatorial explosion: the work must
6
+ // not grow with the SQUARE of the hub's degree.
7
+ //
8
+ // This file used to pin an offer CAP (`exploreCap`, with its invented
9
+ // `PLURALITY` floor). That cap is gone: it was a short-circuit — it bounded what
10
+ // a hop could OFFER instead of charging for it — and it was not needed. In the
11
+ // regime where it used to bite (`hubBound = ceil(√N)` greater than the hub's
12
+ // degree, reached here by choosing the degree below √N, so no trained store is
13
+ // needed) the measured shape without it is LINEAR: degrees 35/70/120 gave offers
14
+ // 52/84/120, pushes 262/296/332, perceptions 530/592/757 — while the peak was
15
+ // identical with and without the cap (218/415/689 MB against 215/410/662),
16
+ // because the peak is set by the store, not by the fan-out. What made that hop
17
+ // expensive was never the breadth of the offer: it was per-offer work, two
18
+ // duplicate/oversized computations since removed.
19
+ //
20
+ // The residual, stated: the trained store's hub (degree 1 083) is an
21
+ // EXTRAPOLATION from this linear shape, not a measurement.
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
+ const QUERY = "alpha beta";
29
+
30
+ /** `alpha` leads to `beta`, and `beta` is a hub of `degree` continuations.
31
+ *
32
+ * The filler keeps `edgeSourceCount()` above the square of the degree, so the
33
+ * READ bound (`hubBound = ceil(√N)`) exceeds the hub's degree — the regime in
34
+ * which the removed cap used to change the offer. */
35
+ async function hub(degree) {
36
+ const store = new SQliteStore({ path: ":memory:", D: 1024 });
37
+ const mind = new Mind({ seed: 7, store, profile: true });
38
+ const pairs = [
39
+ ["alpha", "beta"],
40
+ ["beta", "gamma"],
41
+ ];
42
+ for (let i = 0; i < degree; i++) {
43
+ pairs.push(["beta", `continuation number ${i} of this hub`]);
44
+ }
45
+ const filler = Math.ceil(degree * degree * 1.4);
46
+ for (let i = 0; i < filler; i++) {
47
+ pairs.push([`filler ${i} note`, `unrelated filler body ${i} here`]);
48
+ }
49
+ await mind.ingest(pairs);
50
+ return mind;
51
+ }
52
+
53
+ const pushes = (mind) => mind.lastCost?.counters.searchPushes ?? 0;
54
+ const offers = (mind) => mind.lastCost?.counters.chainOffers ?? 0;
55
+
56
+ test("the work does not explode with the hub's degree", async () => {
57
+ const small = await hub(20);
58
+ const big = await hub(80);
59
+ await small.respond(QUERY);
60
+ await big.respond(QUERY);
61
+ const a = pushes(small);
62
+ const b = pushes(big);
63
+ // The degree grows 4x. An explosion multiplies the work by the SQUARE of
64
+ // that ratio — 16x — so the bound needs no invented number: it is the ratio's
65
+ // own square, and the measured shape is linear (about 4x).
66
+ assert.ok(
67
+ offers(big) > offers(small),
68
+ "the offer must follow the corpus's continuations: " +
69
+ `${offers(small)} → ${offers(big)}`,
70
+ );
71
+ assert.ok(
72
+ b < a * 16,
73
+ "work must not explode with the degree (4x degree would be 16x work): " +
74
+ `${a} → ${b}`,
75
+ );
76
+ await small.store.close();
77
+ await big.store.close();
78
+ });
79
+
80
+ test("and the hub still leads somewhere the chain can use", async () => {
81
+ const mind = await hub(40);
82
+ const out = (await mind.respondText(QUERY)).replace(/\0+/g, "").trim();
83
+ assert.ok(
84
+ out.length > 0,
85
+ `the hub must still be reachable, got ${JSON.stringify(out)}`,
86
+ );
87
+ assert.ok(offers(mind) > 0, "and the chain hop must have been taken");
88
+ await mind.store.close();
89
+ });
@@ -0,0 +1,84 @@
1
+ // 113-the-rationale-payload-is-bounded.test.mjs — a step reports a BOUNDED
2
+ // sample and the COUNT, never the whole field.
3
+ //
4
+ // THE GAP THIS CLOSES. The study measured a rationale step carrying 1082 items
5
+ // and another 1559 — the hub's degree and `hubBound`'s own size, both on the
6
+ // trained store. The step that does it is `disambiguate`: it weighed a hub's
7
+ // continuations and emitted EVERY candidate it considered as an output item.
8
+ // A rationale that carries the entire field is not an explanation, it is a dump:
9
+ // what explains the choice is the pick, the count that says how wide the field
10
+ // was, and a bounded sample of what it was chosen from.
11
+ //
12
+ // WHAT IS PINNED, and neither number is invented here:
13
+ // 1. the sample is bounded by the DECLARED candidate budget (`recallQueryK`,
14
+ // config.ts — a capacity/budget, which is all that file holds);
15
+ // 2. the note still carries the TOTAL, so nothing is lost — measured in the
16
+ // fixture below: 27 continuations weighed, 12 itemised;
17
+ // 3. the choice itself is untouched (the inputs still name the pick).
18
+
19
+ import { test } from "node:test";
20
+ import assert from "node:assert/strict";
21
+ import { Mind } from "../dist/src/index.js";
22
+ import { SQliteStore } from "../dist/src/store-sqlite.js";
23
+ import { DEFAULT_CONFIG } from "../dist/src/config.js";
24
+
25
+ // The DECLARED capacity, read from the config — no number is copied here.
26
+ const BUDGET = DEFAULT_CONFIG.rationaleSampleK;
27
+
28
+ const WORDS =
29
+ ("alpha bravo charlie delta echo foxtrot golf hotel india juliet kilo lima " +
30
+ "mike november oscar papa quebec romeo sierra tango uniform victor whiskey " +
31
+ "xray yankee zulu amber bronze copper dahlia ember fjord gossamer harbour " +
32
+ "indigo jasmine kestrel lantern marigold nectar opal pewter quartz ripple " +
33
+ "saffron thistle umber violet willow xenon yarrow").split(" ");
34
+
35
+ /** A hub of 40 continuations, plus filler so `hubBound` exceeds the budget. */
36
+ async function hub() {
37
+ const store = new SQliteStore({ path: ":memory:", D: 1024 });
38
+ // RECALL's budget is deliberately enormous: the rationale's sample must not
39
+ // follow it (found by an adversarial review — sharing `recallQueryK` meant
40
+ // this very setting un-bounded the payload).
41
+ const mind = new Mind({
42
+ seed: 7,
43
+ store,
44
+ profile: true,
45
+ recallQueryK: 100000,
46
+ });
47
+ const w = (i, n) => WORDS[(i * 7 + n * 13) % WORDS.length];
48
+ const pairs = [];
49
+ for (let i = 0; i < 40; i++) {
50
+ pairs.push(["beta", `continuation number ${i} of this hub`]);
51
+ }
52
+ for (let i = 0; i < 900; i++) {
53
+ pairs.push([
54
+ `${w(i, 1)} ${w(i, 2)} ${w(i, 3)} ${i}`,
55
+ `${w(i, 4)} ${w(i, 5)} ${w(i, 6)} ${w(i, 7)} ${i}`,
56
+ ]);
57
+ }
58
+ await mind.ingest(pairs);
59
+ return mind;
60
+ }
61
+
62
+ test("a step reports a bounded sample, and the count of the whole field", async () => {
63
+ const mind = await hub();
64
+ const steps = [];
65
+ await mind.respondText("beta", (s) => steps.push(s));
66
+ const dis = steps.filter((s) => s.mechanism.at(-1) === "disambiguate");
67
+ assert.ok(dis.length > 0, "the fixture must make the decider weigh a hub");
68
+ const step = dis[0];
69
+ const outs = step.outputs ?? [];
70
+ assert.ok(
71
+ outs.length <= BUDGET,
72
+ `the sample must fit the declared budget, got ${outs.length}`,
73
+ );
74
+ const total = Number(
75
+ /(\d+)\s+continuations/.exec(String(step.note))?.[1] ?? 0,
76
+ );
77
+ assert.ok(total > 0, "the note must say how wide the field was");
78
+ assert.ok(
79
+ total >= outs.length,
80
+ "the count is the WHOLE field, not the sample",
81
+ );
82
+ assert.equal((step.inputs ?? []).length, 1, "the pick is still named");
83
+ await mind.store.close();
84
+ });
@@ -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
+ });