@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.
- package/AGENTS.md +29 -29
- package/TRADEMARKS.md +0 -1
- package/dist/src/config.d.ts +28 -0
- package/dist/src/config.js +20 -0
- package/dist/src/geometry.d.ts +21 -0
- package/dist/src/geometry.js +21 -0
- package/dist/src/meter.d.ts +76 -0
- package/dist/src/meter.js +95 -0
- package/dist/src/mind/attention.d.ts +4 -0
- package/dist/src/mind/attention.js +165 -16
- package/dist/src/mind/canonical.d.ts +16 -0
- package/dist/src/mind/canonical.js +41 -0
- package/dist/src/mind/corpus.d.ts +40 -0
- package/dist/src/mind/corpus.js +149 -0
- package/dist/src/mind/graph-search.d.ts +7 -0
- package/dist/src/mind/graph-search.js +254 -24
- package/dist/src/mind/index.d.ts +3 -1
- package/dist/src/mind/index.js +1 -0
- package/dist/src/mind/match.d.ts +9 -4
- package/dist/src/mind/match.js +147 -61
- package/dist/src/mind/mechanisms/cast.js +19 -3
- package/dist/src/mind/mechanisms/confluence.js +24 -0
- package/dist/src/mind/mechanisms/cover.js +6 -0
- package/dist/src/mind/mechanisms/recall.js +32 -4
- package/dist/src/mind/mind.d.ts +57 -0
- package/dist/src/mind/mind.js +72 -1
- package/dist/src/mind/pipeline-mechanism.d.ts +7 -0
- package/dist/src/mind/pipeline.js +66 -20
- package/dist/src/mind/primitives.js +9 -1
- package/dist/src/mind/rationale.d.ts +28 -1
- package/dist/src/mind/rationale.js +22 -1
- package/dist/src/mind/reasoning.d.ts +25 -3
- package/dist/src/mind/reasoning.js +125 -20
- package/dist/src/mind/recognition.js +4 -8
- package/dist/src/mind/resonance.js +20 -1
- package/dist/src/mind/trace.js +1 -0
- package/dist/src/mind/traverse.js +15 -3
- package/dist/src/mind/types.d.ts +49 -4
- package/docs/INVARIANTS.md +2 -2
- package/docs/architecture/bounded-reads.md +1 -1
- package/docs/architecture/commonality.md +2 -2
- package/docs/architecture/cost-model.md +2 -2
- package/docs/architecture/determinism.md +7 -7
- package/docs/architecture/match-project.md +2 -3
- package/docs/architecture/mechanism-market.md +10 -10
- package/docs/architecture/meter.md +5 -5
- package/docs/architecture/store.md +3 -3
- package/docs/failures/tempting-but-wrong.md +34 -6
- package/docs/harness/gates.md +2 -2
- package/docs/mechanisms/cast.md +2 -2
- package/docs/mechanisms/cover.md +2 -3
- package/docs/mechanisms/extraction.md +7 -7
- package/docs/mechanisms/recall.md +8 -9
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/alu/README.md +11 -12
- package/src/config.ts +48 -0
- package/src/geometry.ts +21 -0
- package/src/meter.ts +98 -0
- package/src/mind/attention.ts +167 -16
- package/src/mind/canonical.ts +43 -0
- package/src/mind/corpus.ts +202 -0
- package/src/mind/graph-search.ts +277 -23
- package/src/mind/index.ts +8 -1
- package/src/mind/match.ts +148 -57
- package/src/mind/mechanisms/cast.ts +20 -2
- package/src/mind/mechanisms/confluence.ts +24 -0
- package/src/mind/mechanisms/cover.ts +5 -0
- package/src/mind/mechanisms/recall.ts +32 -4
- package/src/mind/mind.ts +125 -0
- package/src/mind/pipeline-mechanism.ts +7 -0
- package/src/mind/pipeline.ts +79 -22
- package/src/mind/primitives.ts +9 -1
- package/src/mind/rationale.ts +35 -1
- package/src/mind/reasoning.ts +145 -13
- package/src/mind/recognition.ts +4 -8
- package/src/mind/resonance.ts +19 -1
- package/src/mind/trace.ts +1 -0
- package/src/mind/traverse.ts +16 -6
- package/src/mind/types.ts +53 -4
- package/test/100-complete-grounding-trace.test.mjs +109 -0
- package/test/101-alignment-gap-bound.test.mjs +106 -0
- package/test/102-production-composes-at-scale.test.mjs +110 -0
- package/test/103-alignment-gap-budget.test.mjs +89 -0
- package/test/104-composition-is-reported.test.mjs +90 -0
- package/test/105-derive-through-reports-its-refusal.test.mjs +137 -0
- package/test/106-the-join-fires.test.mjs +94 -0
- package/test/107-the-join-is-counted.test.mjs +81 -0
- package/test/108-the-join-chains.test.mjs +78 -0
- package/test/109-the-pivot-is-counted.test.mjs +60 -0
- package/test/110-the-reasoner-stops-when-the-question-is-answered.test.mjs +91 -0
- package/test/111-the-cover-assembly-is-counted.test.mjs +74 -0
- package/test/112-the-exploration-does-not-grow-with-the-hub.test.mjs +89 -0
- package/test/113-the-rationale-payload-is-bounded.test.mjs +84 -0
- package/test/114-alignment-budget-is-per-sweep.test.mjs +93 -0
- package/test/116-the-extension-is-gated-by-the-pipelines-own-remainder.test.mjs +100 -0
- package/test/117-corpus-search.test.mjs +171 -0
- package/test/118-the-join-reaches-a-key-off-the-cut.test.mjs +74 -0
- package/test/119-the-work-does-not-grow-with-the-corpus.test.mjs +122 -0
- package/test/120-composition-is-consequence.test.mjs +132 -0
- package/test/121-the-extension-does-not-grow-with-the-corpus.test.mjs +128 -0
- package/test/122-the-climb-search-does-not-grow-with-the-corpus.test.mjs +117 -0
- package/test/123-the-paired-formulas-agree.test.mjs +90 -0
- package/test/125-the-post-grounding-branch-publishes-its-operand.test.mjs +51 -0
- package/test/126-the-pipeline-does-not-name-mechanisms.test.mjs +42 -0
- package/test/128-the-leads-somewhere-pair-agrees.test.mjs +83 -0
- package/test/129-the-trace-payload-shape.test.mjs +164 -0
- package/test/14-scaling.test.mjs +10 -7
- package/test/32-confluence.test.mjs +68 -0
- package/test/38-reason-restate-guard.test.mjs +8 -2
- package/test/43-cast-analog-seat.test.mjs +10 -0
- package/test/55-cost-meter.test.mjs +859 -0
- package/test/76-reference-binding.test.mjs +6 -1
- package/test/89-completion-recursion.test.mjs +30 -5
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// 106-the-join-fires.test.mjs — DIRECTION: the join reaches the subject the
|
|
2
|
+
// query never wrote.
|
|
3
|
+
//
|
|
4
|
+
// THE GAP THIS CLOSES. `deriveThrough` derives the answer THROUGH a produced
|
|
5
|
+
// fact: the fact carries the subject the query never named, and the query's
|
|
6
|
+
// remaining tail names the relation to follow FROM that subject. Measured, the
|
|
7
|
+
// rule never fired — and the report added for it (test/105) named the gate:
|
|
8
|
+
// `leading.length === 0`. The cause was then measured exactly: the recognition
|
|
9
|
+
// of the fact's bytes returns EXACTLY ONE site — the fact itself — because a
|
|
10
|
+
// STORED WHOLE stops the sweep, so the interior entity the join exists for was
|
|
11
|
+
// never proposed, and the query was answered by gluing the intermediate KEY to
|
|
12
|
+
// the fact (`The director of Eva is Gustaf Molander.gustaf molander country`).
|
|
13
|
+
//
|
|
14
|
+
// WHAT IS PINNED.
|
|
15
|
+
// 1. the join FIRES: the answer is the second fact, reached through the
|
|
16
|
+
// subject the query never wrote (`The country of Gustaf Molander is
|
|
17
|
+
// Sweden.`);
|
|
18
|
+
// 2. the rationale says so (`derive-through` is the move that reached it);
|
|
19
|
+
// 3. the intermediate KEY never appears in the answer — the glued reading is
|
|
20
|
+
// what the query got before, and it is not an answer;
|
|
21
|
+
// 4. a query with no tail is not a join at all: it is answered directly, and
|
|
22
|
+
// the move must not be claimed.
|
|
23
|
+
//
|
|
24
|
+
// The `canonicalQueryNodes` exclusion (the query's OWN subject, canonically) is
|
|
25
|
+
// what keeps test/99's TRAP refused: its `Eiffel Tower country` is a deposited
|
|
26
|
+
// node in canonical form while the query writes it capitalised, so a raw byte
|
|
27
|
+
// test re-admitted it and the answer came back about Eiffel Tower.
|
|
28
|
+
|
|
29
|
+
import { test } from "node:test";
|
|
30
|
+
import assert from "node:assert/strict";
|
|
31
|
+
import { Mind } from "../dist/src/index.js";
|
|
32
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
33
|
+
|
|
34
|
+
const F1 = "The director of Eva is Gustaf Molander.";
|
|
35
|
+
const F2 = "The country of Gustaf Molander is Sweden.";
|
|
36
|
+
|
|
37
|
+
/** The measured chain, filed the way the real store files a fact: under BOTH
|
|
38
|
+
* the entity and the key it was learnt with. */
|
|
39
|
+
async function chain() {
|
|
40
|
+
const store = new SQliteStore({ path: ":memory:" });
|
|
41
|
+
const mind = new Mind({ seed: 7, store });
|
|
42
|
+
await mind.ingest([
|
|
43
|
+
["eva", F1],
|
|
44
|
+
["eva director", F1],
|
|
45
|
+
["gustaf molander", F2],
|
|
46
|
+
["gustaf molander country", F2],
|
|
47
|
+
]);
|
|
48
|
+
return mind;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const text = (resp) => new TextDecoder().decode(resp.bytes).replace(/\0+/g, "");
|
|
52
|
+
|
|
53
|
+
test("the join reaches the subject the query never wrote", async () => {
|
|
54
|
+
const mind = await chain();
|
|
55
|
+
const moves = [];
|
|
56
|
+
const out = text(
|
|
57
|
+
await mind.respond(
|
|
58
|
+
"eva director country",
|
|
59
|
+
(s) => moves.push(s.mechanism.at(-1)),
|
|
60
|
+
),
|
|
61
|
+
);
|
|
62
|
+
assert.equal(out.trim(), F2);
|
|
63
|
+
assert.ok(
|
|
64
|
+
moves.includes("derive-through"),
|
|
65
|
+
`expected the join to be the move that reached it, got ${
|
|
66
|
+
[...new Set(moves)].join(", ")
|
|
67
|
+
}`,
|
|
68
|
+
);
|
|
69
|
+
await mind.store.close();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("the intermediate key is never the answer", async () => {
|
|
73
|
+
const mind = await chain();
|
|
74
|
+
const out = text(await mind.respond("eva director country"));
|
|
75
|
+
assert.ok(
|
|
76
|
+
!out.includes("gustaf molander country"),
|
|
77
|
+
`the key is not an answer, got ${JSON.stringify(out)}`,
|
|
78
|
+
);
|
|
79
|
+
await mind.store.close();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("a query with no tail is answered directly, not by a join", async () => {
|
|
83
|
+
const mind = await chain();
|
|
84
|
+
const moves = [];
|
|
85
|
+
const out = text(
|
|
86
|
+
await mind.respond("eva director", (s) => moves.push(s.mechanism.at(-1))),
|
|
87
|
+
);
|
|
88
|
+
assert.equal(out.trim(), F1);
|
|
89
|
+
assert.ok(
|
|
90
|
+
!moves.includes("derive-through"),
|
|
91
|
+
"there is nothing to join through when the query ends at the fact",
|
|
92
|
+
);
|
|
93
|
+
await mind.store.close();
|
|
94
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// 107-the-join-is-counted.test.mjs — the join's outcome is observable WITHOUT a
|
|
2
|
+
// rationale.
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS EXISTS. Everything known about the fact join's refusals came from
|
|
5
|
+
// the rationale, and the rationale PERTURBS the search: appending text to a
|
|
6
|
+
// refusal note changed the answer of a traced response (measured, 386293e), and
|
|
7
|
+
// the "key leads nowhere" refusal chased for four rounds turned out to be a
|
|
8
|
+
// trace artifact — untraced, every refusal is a missing KEY, never a key whose
|
|
9
|
+
// continuation is missing: a key that leads nowhere is not the relation at all,
|
|
10
|
+
// so the scan moves on and there is no counter for it (the branch that reported
|
|
11
|
+
// it was unreachable, and an adversarial review said so).
|
|
12
|
+
// A work counter is the untraced view, and `meter.ts` is its one home
|
|
13
|
+
// (AGENTS §6: a counter name exists in exactly one place).
|
|
14
|
+
//
|
|
15
|
+
// WHAT IS PINNED. The four outcomes are published in the cost report with no
|
|
16
|
+
// rationale attached. The assertions stay TRUE when the join is fixed — they
|
|
17
|
+
// check that the counters exist, that a join that fires is counted as fired, and
|
|
18
|
+
// that a query whose join does not fire reports WHY in the counters — never that
|
|
19
|
+
// a particular refusal still happens.
|
|
20
|
+
|
|
21
|
+
import { test } from "node:test";
|
|
22
|
+
import assert from "node:assert/strict";
|
|
23
|
+
import { Mind } from "../dist/src/index.js";
|
|
24
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
25
|
+
|
|
26
|
+
const F1 = "The director of Eva is Gustaf Molander.";
|
|
27
|
+
const F2 = "The country of Gustaf Molander is Sweden.";
|
|
28
|
+
const F3 = "The capital of Sweden is Stockholm.";
|
|
29
|
+
|
|
30
|
+
async function chain() {
|
|
31
|
+
const store = new SQliteStore({ path: ":memory:" });
|
|
32
|
+
const mind = new Mind({ seed: 7, store, profile: true });
|
|
33
|
+
await mind.ingest([
|
|
34
|
+
["eva", F1],
|
|
35
|
+
["eva director", F1],
|
|
36
|
+
["gustaf molander", F2],
|
|
37
|
+
["gustaf molander country", F2],
|
|
38
|
+
["sweden", F3],
|
|
39
|
+
["sweden capital", F3],
|
|
40
|
+
]);
|
|
41
|
+
return mind;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const counters = (mind) => mind.lastCost?.counters ?? {};
|
|
45
|
+
|
|
46
|
+
test("a join that fires is counted, with no rationale attached", async () => {
|
|
47
|
+
const mind = await chain();
|
|
48
|
+
await mind.respondText("eva director country"); // no inspectRationale
|
|
49
|
+
const c = counters(mind);
|
|
50
|
+
assert.ok(
|
|
51
|
+
(c.joinFired ?? 0) > 0,
|
|
52
|
+
"the join reached the second fact, so it must be counted as fired",
|
|
53
|
+
);
|
|
54
|
+
await mind.store.close();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("a query whose join does not fire reports WHY, untraced", async () => {
|
|
58
|
+
const mind = await chain();
|
|
59
|
+
await mind.respondText("eva director country capital"); // no rationale
|
|
60
|
+
const c = counters(mind);
|
|
61
|
+
const refusals = (c.joinNoKey ?? 0) + (c.joinNoEntity ?? 0);
|
|
62
|
+
assert.ok(
|
|
63
|
+
refusals > 0,
|
|
64
|
+
"a join that did not fire must say so in the counters",
|
|
65
|
+
);
|
|
66
|
+
await mind.store.close();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("the counters are deterministic across identical calls", async () => {
|
|
70
|
+
const a = await chain();
|
|
71
|
+
const b = await chain();
|
|
72
|
+
await a.respondText("eva director country capital");
|
|
73
|
+
await b.respondText("eva director country capital");
|
|
74
|
+
assert.deepEqual(
|
|
75
|
+
counters(a),
|
|
76
|
+
counters(b),
|
|
77
|
+
"same query + same store ⇒ same counted work",
|
|
78
|
+
);
|
|
79
|
+
await a.store.close();
|
|
80
|
+
await b.store.close();
|
|
81
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// 108-the-join-chains.test.mjs — DIRECTION, the other half: the result of one
|
|
2
|
+
// join feeds the next.
|
|
3
|
+
//
|
|
4
|
+
// THE GAP THIS CLOSES. The join's conclusion covered the WHOLE remaining tail
|
|
5
|
+
// (`j = queryLen`), so it consumed everything and could only ever conclude: a
|
|
6
|
+
// three-relation query got one join and stopped. The whole-tail key was the
|
|
7
|
+
// same defect seen from the key side — a two-relation tail names no learnt key
|
|
8
|
+
// at all.
|
|
9
|
+
//
|
|
10
|
+
// THE FIX, and the measurement that found the last piece of it. The key is the
|
|
11
|
+
// SHORTEST tail prefix that BOTH resolves and leads somewhere, and the
|
|
12
|
+
// conclusion covers only that prefix, leaving the rest of the tail for the next
|
|
13
|
+
// step. Resolving is not enough: a dry run of these very primitives showed the
|
|
14
|
+
// first prefix that resolves for the candidate `Sweden` is the key `Sweden `
|
|
15
|
+
// (trailing space) — which leads NOWHERE — while the key that names the fact
|
|
16
|
+
// (`sweden capital`) sat one prefix further; accepting the first resolving key
|
|
17
|
+
// refused the join.
|
|
18
|
+
//
|
|
19
|
+
// WHAT IS PINNED, untraced (a rationale perturbs the search — test/107):
|
|
20
|
+
// 1. the answer is the THIRD fact, reached through two joins;
|
|
21
|
+
// 2. `joinFired` counts exactly two — the counters are the untraced view;
|
|
22
|
+
// 3. the two-relation query still counts exactly one, so chaining did not
|
|
23
|
+
// turn every join into a chain.
|
|
24
|
+
|
|
25
|
+
import { test } from "node:test";
|
|
26
|
+
import assert from "node:assert/strict";
|
|
27
|
+
import { Mind } from "../dist/src/index.js";
|
|
28
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
29
|
+
|
|
30
|
+
const F1 = "The director of Eva is Gustaf Molander.";
|
|
31
|
+
const F2 = "The country of Gustaf Molander is Sweden.";
|
|
32
|
+
const F3 = "The capital of Sweden is Stockholm.";
|
|
33
|
+
|
|
34
|
+
async function chain() {
|
|
35
|
+
const store = new SQliteStore({ path: ":memory:" });
|
|
36
|
+
const mind = new Mind({ seed: 7, store, profile: true });
|
|
37
|
+
await mind.ingest([
|
|
38
|
+
["eva", F1],
|
|
39
|
+
["eva director", F1],
|
|
40
|
+
["gustaf molander", F2],
|
|
41
|
+
["gustaf molander country", F2],
|
|
42
|
+
["sweden", F3],
|
|
43
|
+
["sweden capital", F3],
|
|
44
|
+
]);
|
|
45
|
+
return mind;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const text = (resp) => new TextDecoder().decode(resp.bytes).replace(/\0+/g, "");
|
|
49
|
+
|
|
50
|
+
test("a three-relation query reaches the third fact, through two joins", async () => {
|
|
51
|
+
const mind = await chain();
|
|
52
|
+
const out = text(await mind.respond("eva director country capital"));
|
|
53
|
+
assert.equal(out.trim(), F3, "the chain must reach the capital fact");
|
|
54
|
+
await mind.store.close();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("the counters agree, with no rationale attached", async () => {
|
|
58
|
+
const mind = await chain();
|
|
59
|
+
await mind.respond("eva director country capital");
|
|
60
|
+
assert.equal(
|
|
61
|
+
mind.lastCost?.counters.joinFired ?? 0,
|
|
62
|
+
2,
|
|
63
|
+
"two joins reached it — the untraced view must agree with the answer",
|
|
64
|
+
);
|
|
65
|
+
await mind.store.close();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("a two-relation query still joins exactly once", async () => {
|
|
69
|
+
const mind = await chain();
|
|
70
|
+
const out = text(await mind.respond("eva director country"));
|
|
71
|
+
assert.equal(out.trim(), F2);
|
|
72
|
+
assert.equal(
|
|
73
|
+
mind.lastCost?.counters.joinFired ?? 0,
|
|
74
|
+
1,
|
|
75
|
+
"chaining must not turn a single join into a chain",
|
|
76
|
+
);
|
|
77
|
+
await mind.store.close();
|
|
78
|
+
});
|
|
@@ -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
|
+
});
|