@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.
- package/AGENTS.md +22 -1
- package/DATASETS.md +1 -1
- package/dist/example/train_base/config.js +2 -2
- package/dist/example/train_base/corpora/massive.js +1 -1
- package/dist/example/train_base/readers.js +1 -1
- package/dist/src/config.d.ts +17 -0
- package/dist/src/config.js +18 -0
- package/dist/src/geometry.d.ts +10 -10
- package/dist/src/geometry.js +25 -24
- package/dist/src/meter.d.ts +29 -12
- package/dist/src/meter.js +58 -14
- package/dist/src/mind/attention.js +12 -12
- package/dist/src/mind/bridge.d.ts +8 -8
- package/dist/src/mind/bridge.js +33 -32
- 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 -8
- package/dist/src/mind/graph-search.js +244 -32
- package/dist/src/mind/index.d.ts +3 -1
- package/dist/src/mind/index.js +1 -0
- package/dist/src/mind/junction.d.ts +1 -1
- package/dist/src/mind/junction.js +8 -8
- package/dist/src/mind/learning.js +36 -35
- package/dist/src/mind/match.d.ts +8 -3
- package/dist/src/mind/match.js +156 -71
- package/dist/src/mind/mechanisms/cast.js +18 -2
- package/dist/src/mind/mechanisms/cover.js +19 -12
- package/dist/src/mind/mechanisms/prefix-completion.js +24 -24
- package/dist/src/mind/mechanisms/recall.js +38 -40
- package/dist/src/mind/mechanisms/reference.js +16 -16
- package/dist/src/mind/mind.d.ts +61 -7
- package/dist/src/mind/mind.js +72 -2
- package/dist/src/mind/pipeline-mechanism.d.ts +10 -8
- package/dist/src/mind/pipeline-mechanism.js +25 -21
- package/dist/src/mind/pipeline.d.ts +9 -9
- package/dist/src/mind/pipeline.js +49 -29
- package/dist/src/mind/primitives.d.ts +5 -5
- package/dist/src/mind/primitives.js +5 -5
- package/dist/src/mind/reasoning.d.ts +5 -1
- package/dist/src/mind/reasoning.js +54 -1
- package/dist/src/mind/recognition.d.ts +14 -13
- package/dist/src/mind/recognition.js +23 -23
- package/dist/src/mind/resonance.js +21 -21
- package/dist/src/mind/traverse.d.ts +54 -52
- package/dist/src/mind/traverse.js +83 -73
- package/dist/src/mind/types.d.ts +26 -4
- package/dist/src/store.d.ts +12 -12
- package/dist/src/store.js +12 -12
- package/docs/INDEX.md +2 -2
- package/docs/architecture/exact-vs-approximate.md +2 -1
- package/docs/architecture/fold-contract.md +1 -1
- package/docs/failures/tempting-but-wrong.md +33 -5
- package/docs/harness/gates.md +7 -7
- package/example/train_base/config.ts +2 -2
- package/example/train_base/corpora/massive.ts +1 -1
- package/example/train_base/readers.ts +1 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/config.ts +35 -0
- package/src/geometry.ts +25 -24
- package/src/meter.ts +61 -14
- package/src/mind/attention.ts +12 -12
- package/src/mind/bridge.ts +33 -32
- package/src/mind/corpus.ts +202 -0
- package/src/mind/graph-search.ts +261 -31
- package/src/mind/index.ts +8 -1
- package/src/mind/junction.ts +8 -8
- package/src/mind/learning.ts +36 -35
- package/src/mind/match.ts +163 -73
- package/src/mind/mechanisms/cast.ts +17 -1
- package/src/mind/mechanisms/cover.ts +18 -12
- package/src/mind/mechanisms/prefix-completion.ts +24 -24
- package/src/mind/mechanisms/recall.ts +38 -40
- package/src/mind/mechanisms/reference.ts +16 -16
- package/src/mind/mind.ts +129 -7
- package/src/mind/pipeline-mechanism.ts +25 -21
- package/src/mind/pipeline.ts +63 -38
- package/src/mind/primitives.ts +5 -5
- package/src/mind/reasoning.ts +55 -0
- package/src/mind/recognition.ts +23 -23
- package/src/mind/resonance.ts +21 -21
- package/src/mind/traverse.ts +83 -73
- package/src/mind/types.ts +30 -4
- package/src/store.ts +20 -20
- package/test/08-storage.test.mjs +1 -1
- 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 +113 -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/14-scaling.test.mjs +10 -7
- package/test/35-prefix-edge.test.mjs +1 -1
- package/test/40-choosenext-scale-guard.test.mjs +16 -17
- package/test/56-bridge-identity-admission.test.mjs +6 -6
- package/test/70-prefix-completion.test.mjs +4 -3
- package/test/72-prefix-candidate-supply.test.mjs +3 -3
- package/test/73-scaffolding-only-bridge-abstains.test.mjs +6 -6
- package/test/75-multiturn-context-optimisation.test.mjs +5 -5
- package/test/76-reference-binding.test.mjs +6 -1
- package/test/84-composed-answer-honesty.test.mjs +5 -6
- package/test/88-dependency-footprint.test.mjs +1 -1
- package/test/89-completion-recursion.test.mjs +47 -19
- package/test/90-connector-read-cap.test.mjs +10 -8
- package/test/93-regime-prediction.test.mjs +10 -10
- package/test/94-cross-region-budget.test.mjs +2 -2
- package/test/95-wide-resonance-removed.test.mjs +8 -7
- package/test/96-bytes-walk-termination.test.mjs +3 -3
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// 102-production-composes-at-scale.test.mjs — a produced composite is
|
|
2
|
+
// decomposed by ITS OWN TREE, at every corpus size.
|
|
3
|
+
//
|
|
4
|
+
// THE GAP THIS CLOSES. `recompleteNode` re-covers a node an edge produced, and
|
|
5
|
+
// it seeds that re-cover from `recogniseSpan(bytes)` filtered to the node's own
|
|
6
|
+
// kids. Above the `atomIsHub` flip the recognition of a produced span returns
|
|
7
|
+
// the WHOLE and deliberately suppresses its atoms (the off-boundary
|
|
8
|
+
// suppression), so the kid filter admitted nothing and the chain ended at the
|
|
9
|
+
// intermediate composite. Measured on
|
|
10
|
+
//
|
|
11
|
+
// seed → "p q" → (p→r, q→s) → "r s" → "m n"
|
|
12
|
+
//
|
|
13
|
+
// below the flip it reached "m n" with `fuse`+`recompose`; above it stopped at
|
|
14
|
+
// "p q" with neither, and the trace showed `recognise("p q") ⇒ form "p q"`
|
|
15
|
+
// alone. The node's own kids ARE its decomposition — the machine that built
|
|
16
|
+
// the composite stated its parts — so they seed the re-cover directly, and a
|
|
17
|
+
// kid recognition already offers is skipped (below the flip the seed set is
|
|
18
|
+
// byte-identical to what it was).
|
|
19
|
+
//
|
|
20
|
+
// WHY THE FIXTURE CROSSES THE FLIP. The property was never false below it: the
|
|
21
|
+
// whole point is that composition must not depend on corpus size. The flip sits
|
|
22
|
+
// between N = 3 658 (composes) and N = 4 174 (did not), so the fixture ingests
|
|
23
|
+
// 4 400 filler pairs, exactly as test/99 does for the join.
|
|
24
|
+
//
|
|
25
|
+
// WHAT IS PINNED.
|
|
26
|
+
// 1. the answer above the flip EQUALS the answer below it (scale invariance,
|
|
27
|
+
// asserted as one comparison rather than two magic strings);
|
|
28
|
+
// 2. above the flip the composition is reported — `fuse` and `recompose`
|
|
29
|
+
// appear in the rationale, so the fix is visible in the rationale and not
|
|
30
|
+
// just in the bytes;
|
|
31
|
+
// 3. the chain still ends where the graph ends ("m n"), so the fix does not
|
|
32
|
+
// invent depth.
|
|
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 CHAIN = [
|
|
40
|
+
["seed", "p q"],
|
|
41
|
+
["p", "r"],
|
|
42
|
+
["q", "s"],
|
|
43
|
+
["r s", "m n"],
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const WORDS =
|
|
47
|
+
("alpha bravo charlie delta echo foxtrot golf hotel india juliet kilo lima " +
|
|
48
|
+
"mike november oscar papa quebec romeo sierra tango uniform victor whiskey " +
|
|
49
|
+
"xray yankee zulu amber bronze copper dahlia ember fjord gossamer harbour " +
|
|
50
|
+
"indigo jasmine kestrel lantern marigold nectar opal pewter quartz ripple " +
|
|
51
|
+
"saffron thistle umber violet willow xenon yarrow").split(" ");
|
|
52
|
+
|
|
53
|
+
/** One store, ingested past the atomIsHub flip (N > 4 174). */
|
|
54
|
+
async function atScale(fillers = 4400) {
|
|
55
|
+
const store = new SQliteStore({ path: ":memory:", D: 1024 });
|
|
56
|
+
const mind = new Mind({ seed: 7, store });
|
|
57
|
+
const w = (i, n) => WORDS[(i * 7 + n * 13) % WORDS.length];
|
|
58
|
+
await mind.ingest([
|
|
59
|
+
...CHAIN,
|
|
60
|
+
...Array.from({ length: fillers }, (_, i) => [
|
|
61
|
+
`${w(i, 1)} ${w(i, 2)} ${w(i, 3)} ${i}`,
|
|
62
|
+
`${w(i, 4)} ${w(i, 5)} ${w(i, 6)} ${w(i, 7)} ${i}`,
|
|
63
|
+
]),
|
|
64
|
+
]);
|
|
65
|
+
return { store, mind };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The same chain BELOW the flip — the behaviour the scaled store must match. */
|
|
69
|
+
async function belowFlip() {
|
|
70
|
+
const store = new SQliteStore({ path: ":memory:", D: 1024 });
|
|
71
|
+
const mind = new Mind({ seed: 7, store });
|
|
72
|
+
await mind.ingest(CHAIN);
|
|
73
|
+
return { store, mind };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const text = (b) => b.replace(/\0+/g, "").trim();
|
|
77
|
+
|
|
78
|
+
test("a produced composite composes identically below and above the flip", async () => {
|
|
79
|
+
const small = await belowFlip();
|
|
80
|
+
const big = await atScale();
|
|
81
|
+
const deep = text(await big.mind.respondText("seed"));
|
|
82
|
+
const shallow = text(await small.mind.respondText("seed"));
|
|
83
|
+
|
|
84
|
+
// The property: depth is a fact about the graph, not about the corpus size.
|
|
85
|
+
assert.equal(deep, shallow, "scale must not change how deep the chain goes");
|
|
86
|
+
assert.equal(
|
|
87
|
+
deep,
|
|
88
|
+
"m n",
|
|
89
|
+
"and the graph ends at the composite's continuation",
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("above the flip the composition is reported, not just performed", async () => {
|
|
94
|
+
const { mind } = await atScale();
|
|
95
|
+
const steps = [];
|
|
96
|
+
await mind.respondText("seed", (s) => steps.push(s));
|
|
97
|
+
const moves = new Set(steps.map((s) => s.mechanism.at(-1)));
|
|
98
|
+
assert.ok(moves.has("fuse"), "the fused pair must appear in the rationale");
|
|
99
|
+
assert.ok(
|
|
100
|
+
moves.has("recompose"),
|
|
101
|
+
"and the recomposition that names the deeper form",
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("the fix does not invent depth: the chain still ends at the graph's end", async () => {
|
|
106
|
+
const { mind } = await atScale();
|
|
107
|
+
const answer = text(await mind.respondText("seed"));
|
|
108
|
+
assert.notEqual(answer, "r s", "it must not stop at the parts it followed");
|
|
109
|
+
assert.notEqual(answer, "z", "nor reach a form the graph never licensed");
|
|
110
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// 103-alignment-gap-budget.test.mjs — a learned frame's slot is bounded by the
|
|
2
|
+
// PAIR, not by the write side's arity; the WORK is bounded by a declared budget.
|
|
3
|
+
//
|
|
4
|
+
// THE GAP THIS CLOSES. `alignAround` bounded each side's gap by
|
|
5
|
+
// `chainReach(W)` = W². A learnt frame whose slot is longer than that could not
|
|
6
|
+
// be aligned, so the frame binding produced no candidate and a cheaper
|
|
7
|
+
// mechanism answered instead — measured on a
|
|
8
|
+
// `Book a table at <filler> tonight.` → `Your table at <filler> is booked.`
|
|
9
|
+
// frame: at 18, 24, 30 and 36 bytes `bindReference` reported the cap and the
|
|
10
|
+
// answer came back carrying ANOTHER instance's filler ("bravo1").
|
|
11
|
+
//
|
|
12
|
+
// Bounding the work instead is the fix, and the two questions are genuinely
|
|
13
|
+
// different: the gap's LENGTH is how far the two byte strings actually diverge
|
|
14
|
+
// (a fact about the pair), while the WORK is how many (queryGap, contextGap)
|
|
15
|
+
// pairs the sweep examines, which is what a cost bound must limit — reaching a
|
|
16
|
+
// gap of size G costs about G²/2 pairs, so a length bound is a work bound only
|
|
17
|
+
// by accident, and at the wrong scale.
|
|
18
|
+
//
|
|
19
|
+
// WHAT IS PINNED: the asker's OWN filler comes back. The old defect used to be
|
|
20
|
+
// reproduced by injecting a tiny budget; that knob is gone because there is no
|
|
21
|
+
// budget any more — the sweep indexes the context's windows and walks the
|
|
22
|
+
// query's, so its work is proportional to the bytes a run spans and its reach is
|
|
23
|
+
// the bytes' own. There is nothing to make artificially small, and nothing that
|
|
24
|
+
// silently drops a far continuation:
|
|
25
|
+
//
|
|
26
|
+
// filler 18/20/24 B → the asker's own filler (the pair's own extent spans it)
|
|
27
|
+
//
|
|
28
|
+
// Beyond what the frame's own instances can span (measured: from ~30 bytes) a
|
|
29
|
+
// DIFFERENT cause takes over — the frame inventory drops to a single instance,
|
|
30
|
+
// and the licence correctly refuses to voice from one exemplar. That is the
|
|
31
|
+
// reference mechanism's own gate, not this bound, and it is not asserted here.
|
|
32
|
+
|
|
33
|
+
import { test } from "node:test";
|
|
34
|
+
import assert from "node:assert/strict";
|
|
35
|
+
import { Mind } from "../dist/src/index.js";
|
|
36
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
37
|
+
|
|
38
|
+
const WORDS =
|
|
39
|
+
("alpha bravo charlie delta echo foxtrot golf hotel india juliet kilo lima " +
|
|
40
|
+
"mike november oscar papa quebec romeo sierra tango uniform victor whiskey " +
|
|
41
|
+
"xray yankee zulu amber bronze copper dahlia ember fjord gossamer harbour " +
|
|
42
|
+
"indigo jasmine kestrel lantern marigold nectar opal pewter quartz ripple " +
|
|
43
|
+
"saffron thistle umber violet willow xenon yarrow").split(" ");
|
|
44
|
+
|
|
45
|
+
const short = (i) => `${WORDS[i % 50]}${i}`;
|
|
46
|
+
const long = (i) =>
|
|
47
|
+
`${WORDS[i % 50]} ${WORDS[(i * 3) % 50]} ${WORDS[(i * 7) % 50]} street ${i}`;
|
|
48
|
+
|
|
49
|
+
/** A frame whose continuation quotes its filler, plus the Mind options. */
|
|
50
|
+
async function frame(opts = {}, n = 60) {
|
|
51
|
+
const store = new SQliteStore({ path: ":memory:", D: 1024 });
|
|
52
|
+
const mind = new Mind({ seed: 7, store, ...opts });
|
|
53
|
+
const pairs = [];
|
|
54
|
+
for (let i = 0; i < n; i++) {
|
|
55
|
+
const f = i % 3 === 0 ? long(i) : short(i);
|
|
56
|
+
pairs.push([
|
|
57
|
+
`Book a table at ${f} tonight.`,
|
|
58
|
+
`Your table at ${f} is booked.`,
|
|
59
|
+
]);
|
|
60
|
+
}
|
|
61
|
+
await mind.ingest(pairs);
|
|
62
|
+
return { store, mind };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A novel filler of exactly `len` bytes, never ingested. */
|
|
66
|
+
const novel = (len) => "zephyr quartz lantern ".repeat(4).slice(0, len).trim();
|
|
67
|
+
|
|
68
|
+
const answer = async (mind, filler) =>
|
|
69
|
+
(await mind.respondText(`Book a table at ${filler} tonight.`))
|
|
70
|
+
.replace(/\0+/g, "").trim();
|
|
71
|
+
|
|
72
|
+
test("the default budget spans it, and the answer quotes the ASKER's filler", async () => {
|
|
73
|
+
const filler = novel(24);
|
|
74
|
+
const { mind } = await frame();
|
|
75
|
+
const got = await answer(mind, filler);
|
|
76
|
+
assert.ok(
|
|
77
|
+
got.includes(filler),
|
|
78
|
+
"the pair's own extent must be reachable: the asker's bytes, not the corpus's",
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("an 18-byte slot is spanned too — the boundary moved off W²", async () => {
|
|
83
|
+
const { mind } = await frame();
|
|
84
|
+
for (const len of [18, 20, 24]) {
|
|
85
|
+
const filler = novel(len);
|
|
86
|
+
const got = await answer(mind, filler);
|
|
87
|
+
assert.ok(got.includes(filler), `${len} B must be spanned`);
|
|
88
|
+
}
|
|
89
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// 104-composition-is-reported.test.mjs — every composition of an answer is
|
|
2
|
+
// VISIBLE in the rationale.
|
|
3
|
+
//
|
|
4
|
+
// THE GAP THIS CLOSES. `joinWithBridge` (resonance.ts) is the composition step
|
|
5
|
+
// every out-of-search assembly shares: it asks the corpus for a learnt
|
|
6
|
+
// connector between two pieces, and on a miss it joins them BARE and emits a
|
|
7
|
+
// `bridgeMiss` step — never silent. CAST's projection-to-continuation join
|
|
8
|
+
// bypassed it with a bare `concat2`, and that single bypass is the whole of the
|
|
9
|
+
// gluing the study measured: `"Steel is hard"` + `"wet"` came back as
|
|
10
|
+
// `"hardwet"`, `"eva director father"` + `"The father of Gustaf Molander…"` as
|
|
11
|
+
// `"fatherThe"`, the real store's `"Dutch.na"`. The house rule is NOT "never
|
|
12
|
+
// join bare" — it is "joined bare, and REPORTED", so that a reader of the
|
|
13
|
+
// rationale can see the seam and its pieces. The bypass made the seam
|
|
14
|
+
// invisible, which is why the gluing survived an audit that read rationales.
|
|
15
|
+
//
|
|
16
|
+
// WHAT IS PINNED. On the fixture below the join IS a miss (this tiny corpus
|
|
17
|
+
// attests no connector between the two pieces), so the step must exist and must
|
|
18
|
+
// name BOTH pieces. The answer must still USE the followed content — test/39
|
|
19
|
+
// pins that the follow is unaffected, and a fix that refuses the join instead
|
|
20
|
+
// of reporting it would silently drop the evidence.
|
|
21
|
+
|
|
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
|
+
/** test/39's corpus: a projection that must follow onto genuinely new content. */
|
|
28
|
+
async function fixture() {
|
|
29
|
+
const store = new SQliteStore({ path: ":memory:" });
|
|
30
|
+
const mind = new Mind({ seed: 7, store });
|
|
31
|
+
await mind.ingest([
|
|
32
|
+
["Ice is cold", "cold"],
|
|
33
|
+
["Fire is hot", "hot"],
|
|
34
|
+
["Steel is hard", "hard"],
|
|
35
|
+
["Water is wet", "wet"],
|
|
36
|
+
["Ice is cold", "Water is wet"],
|
|
37
|
+
["Something else entirely", "Water is wet"],
|
|
38
|
+
]);
|
|
39
|
+
return mind;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const text = (resp) => new TextDecoder().decode(resp.bytes).replace(/\0+/g, "");
|
|
43
|
+
|
|
44
|
+
test("a composed seam is reported: the miss names its two pieces", async () => {
|
|
45
|
+
const mind = await fixture();
|
|
46
|
+
const steps = [];
|
|
47
|
+
const answer = text(
|
|
48
|
+
await mind.respond("What if steel were cold?", (s) => steps.push(s)),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const misses = steps.filter((s) => s.mechanism.at(-1) === "bridgeMiss");
|
|
52
|
+
assert.ok(
|
|
53
|
+
misses.length > 0,
|
|
54
|
+
"the composition must be visible: the shared joiner reports a miss",
|
|
55
|
+
);
|
|
56
|
+
// The seam this lot exists for: the followed continuation is joined on.
|
|
57
|
+
// The INTERNAL seam — the join the mechanism itself made — names the two
|
|
58
|
+
// pieces as they were BEFORE the join: the projection and the followed
|
|
59
|
+
// continuation, side by side. The answer-level assembly can only report the
|
|
60
|
+
// bytes it is handed, so this step is what makes the gluing auditable.
|
|
61
|
+
const internal = misses.some((s) => {
|
|
62
|
+
const parts = (s.inputs ?? []).map((i) => String(i.text));
|
|
63
|
+
return parts.some((t) => t.trim() === "wet") &&
|
|
64
|
+
parts.some((t) => t.includes("cold"));
|
|
65
|
+
});
|
|
66
|
+
assert.ok(internal, "the internal seam must be named, pre-join");
|
|
67
|
+
// Reporting, not refusing: the follow's content is still used (test/39).
|
|
68
|
+
assert.ok(
|
|
69
|
+
answer.includes("wet"),
|
|
70
|
+
`the followed content must still be used, got "${answer}"`,
|
|
71
|
+
);
|
|
72
|
+
await mind.store.close();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("an in-search composition keeps its own trace (no report invented)", async () => {
|
|
76
|
+
// A query the corpus answers directly composes nothing that needs a bridge:
|
|
77
|
+
// cover's own steps are the report, so no `bridgeMiss` is fabricated.
|
|
78
|
+
const store = new SQliteStore({ path: ":memory:" });
|
|
79
|
+
const mind = new Mind({ seed: 7, store });
|
|
80
|
+
await mind.ingest([["Ice is cold", "Water is wet"]]);
|
|
81
|
+
const steps = [];
|
|
82
|
+
await mind.respond("Ice is cold", (s) => steps.push(s));
|
|
83
|
+
const misses = steps.filter((s) => s.mechanism.at(-1) === "bridgeMiss");
|
|
84
|
+
assert.equal(misses.length, 0, "a stored answer is not a composition");
|
|
85
|
+
assert.ok(
|
|
86
|
+
steps.some((s) => s.mechanism.at(-1) === "ground"),
|
|
87
|
+
"and the derivation that did answer is still traced",
|
|
88
|
+
);
|
|
89
|
+
await mind.store.close();
|
|
90
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// 105-derive-through-reports-its-refusal.test.mjs — the join rule says WHY it
|
|
2
|
+
// did not join.
|
|
3
|
+
//
|
|
4
|
+
// THE GAP THIS CLOSES. DIRECTION (the study's gap 2): a produced fact carries
|
|
5
|
+
// the subject the query never wrote, and the query's remaining tail names the
|
|
6
|
+
// relation to follow FROM it — but on the measured chain the rule never fired
|
|
7
|
+
// and nothing said why. `deriveThrough` yields no rule when it refuses, and a
|
|
8
|
+
// rule that yields nothing leaves no step, so the gate was invisible from
|
|
9
|
+
// outside: four fixtures changed the SYMPTOM (`"eva director country"` answered
|
|
10
|
+
// with the intermediate KEY's bytes glued to the fact) without ever reaching the
|
|
11
|
+
// rule. AGENTS §6: a gap in instrumentation is a defect IN the instrumentation
|
|
12
|
+
// — close it there, once, through the rationale, never a channel of its own.
|
|
13
|
+
//
|
|
14
|
+
// WHAT IS PINNED.
|
|
15
|
+
// 1. the refusal is REPORTED, and it names the two pieces it tried (the fact
|
|
16
|
+
// and the tail), so the gate is readable in the rationale;
|
|
17
|
+
// 2. it is reported ONLY for a form: the search asks this rule for every
|
|
18
|
+
// finalized out with a node, one-byte outs included — measured, 68
|
|
19
|
+
// refusals for a single 3-relation query, all letters. W is the line the
|
|
20
|
+
// mind already draws between a chance overlap and a form, and a report per
|
|
21
|
+
// letter is noise, not instrumentation;
|
|
22
|
+
// 3. a fully covered query (no tail) is not a refusal at all — the rule does
|
|
23
|
+
// not apply, and it must not be reported as a miss.
|
|
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
|
+
/** The measured chain: every relation filed under BOTH the entity and its key,
|
|
35
|
+
* which is how the real store files a fact (the study's round-3 dump). */
|
|
36
|
+
async function chain() {
|
|
37
|
+
const store = new SQliteStore({ path: ":memory:" });
|
|
38
|
+
const mind = new Mind({ seed: 7, store });
|
|
39
|
+
await mind.ingest([
|
|
40
|
+
["eva", F1],
|
|
41
|
+
["eva director", F1],
|
|
42
|
+
["gustaf molander", F2],
|
|
43
|
+
["gustaf molander country", F2],
|
|
44
|
+
["sweden", F3],
|
|
45
|
+
["sweden capital", F3],
|
|
46
|
+
]);
|
|
47
|
+
return mind;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const misses = (steps) =>
|
|
51
|
+
steps.filter((s) => s.mechanism.at(-1) === "deriveThroughMiss");
|
|
52
|
+
|
|
53
|
+
test("the join's refusal is reported, naming the candidate and tail it tried", async () => {
|
|
54
|
+
const mind = await chain();
|
|
55
|
+
// The THREE-relation query: the second join is still refused (the rule
|
|
56
|
+
// concludes terminal — the study's other half), so this is where the refusal
|
|
57
|
+
// is observable. The two-relation one now JOINS (measured, and pinned by
|
|
58
|
+
// test/99's spec), which is why this test moved here.
|
|
59
|
+
const steps = [];
|
|
60
|
+
await mind.respond("eva director country capital", (s) => steps.push(s));
|
|
61
|
+
const got = misses(steps);
|
|
62
|
+
assert.ok(got.length > 0, "the join must say why it did not join");
|
|
63
|
+
const named = got.some((s) => {
|
|
64
|
+
const parts = (s.inputs ?? []).map((i) => String(i.text));
|
|
65
|
+
return parts.some((t) => t.includes("gustaf molander")) &&
|
|
66
|
+
parts.some((t) => t.includes("country"));
|
|
67
|
+
});
|
|
68
|
+
assert.ok(named, "the report must name the candidate and the tail it tried");
|
|
69
|
+
// …and WHERE the candidate came from: a refusal that names only bytes leaves
|
|
70
|
+
// the next reader guessing which proposal path produced them (three chained-
|
|
71
|
+
// join attempts were spent fixing paths that never proposed the offender).
|
|
72
|
+
assert.ok(
|
|
73
|
+
got.some((s) => /#\d+, from the .* source/.test(String(s.note))),
|
|
74
|
+
"the report must name the candidate's node and its source",
|
|
75
|
+
);
|
|
76
|
+
await mind.store.close();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("a report per LETTER is noise: only forms are reported", async () => {
|
|
80
|
+
const mind = await chain();
|
|
81
|
+
const steps = [];
|
|
82
|
+
await mind.respond("eva director country capital", (s) => steps.push(s));
|
|
83
|
+
const got = misses(steps);
|
|
84
|
+
assert.ok(got.length > 0, "the real fact's refusal is still reported");
|
|
85
|
+
// STRUCTURAL, not a count: a report is only made for a FORM, so every report
|
|
86
|
+
// carries a piece at least one window long — no one-byte out reports a miss.
|
|
87
|
+
// `W` is the fixture's own geometry (the Mind's default maxGroup), the same
|
|
88
|
+
// line the rule uses.
|
|
89
|
+
const W = 4;
|
|
90
|
+
for (const m of got) {
|
|
91
|
+
const longest = Math.max(
|
|
92
|
+
0,
|
|
93
|
+
...(m.inputs ?? []).map((i) => String(i.text ?? "").length),
|
|
94
|
+
);
|
|
95
|
+
assert.ok(
|
|
96
|
+
longest >= W,
|
|
97
|
+
`a report must be about a form, got pieces of at most ${longest} bytes`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
await mind.store.close();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("a fully covered query is not a refusal", async () => {
|
|
104
|
+
const mind = await chain();
|
|
105
|
+
const steps = [];
|
|
106
|
+
await mind.respond("eva director", (s) => steps.push(s));
|
|
107
|
+
assert.equal(
|
|
108
|
+
misses(steps).length,
|
|
109
|
+
0,
|
|
110
|
+
"with no tail there is nothing to join through — not a miss",
|
|
111
|
+
);
|
|
112
|
+
await mind.store.close();
|
|
113
|
+
});
|
|
@@ -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
|
+
});
|