@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,109 @@
|
|
|
1
|
+
// 100-complete-grounding-trace.test.mjs — a declared-complete grounding SAYS
|
|
2
|
+
// SO in the rationale.
|
|
3
|
+
//
|
|
4
|
+
// THE GAP THIS CLOSES. `pipeline.ts` ends the derivation when the winning
|
|
5
|
+
// grounding carries `MechanismResult.complete` — the mechanism's own claim that
|
|
6
|
+
// the query IS a stored context, so its continuation is the whole read-out and
|
|
7
|
+
// a further pivot could only chain PAST the fact that produced the answer. The
|
|
8
|
+
// decision was correct and SILENT: no step, no note. A reader of the rationale
|
|
9
|
+
// therefore could not tell
|
|
10
|
+
//
|
|
11
|
+
// "the chain stopped because the query WAS the context" (complete)
|
|
12
|
+
//
|
|
13
|
+
// apart from
|
|
14
|
+
//
|
|
15
|
+
// "nothing followed" (no continuation)
|
|
16
|
+
//
|
|
17
|
+
// which are different claims about the same answer. AGENTS §6 makes that an
|
|
18
|
+
// instrumentation defect rather than a documentation gap: a bound that
|
|
19
|
+
// truncates must be reportable AT THE POINT it truncates, through the one
|
|
20
|
+
// surface that already exists.
|
|
21
|
+
//
|
|
22
|
+
// WHAT IS PINNED HERE.
|
|
23
|
+
// 1. the stop is REPORTED (step `completeGrounding`) when it happens;
|
|
24
|
+
// 2. the stop is REAL — the post-grounding extension did not run (no pivot or
|
|
25
|
+
// forward-absorb step), so the report cannot rot into a lie;
|
|
26
|
+
// 3. the report does NOT appear for an ordinary grounding, so it names a
|
|
27
|
+
// decision rather than decorating every response.
|
|
28
|
+
//
|
|
29
|
+
// The fixture is test/76's CARRIED frame: the continuation quotes its filler
|
|
30
|
+
// (`Run gcc <X>`), which is the shape the reference mechanism binds and
|
|
31
|
+
// declares complete.
|
|
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 CARRIED = [
|
|
39
|
+
["How do I compile hello.c?", "Run gcc hello.c"],
|
|
40
|
+
["How do I compile server.c?", "Run gcc server.c"],
|
|
41
|
+
["How do I compile parser.c?", "Run gcc parser.c"],
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
/** The frame fixture — the same one test/76 pins the binding on. */
|
|
45
|
+
async function frame() {
|
|
46
|
+
const m = new Mind({ seed: 7, store: new SQliteStore({ path: ":memory:" }) });
|
|
47
|
+
await m.ingest(CARRIED);
|
|
48
|
+
return m;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** A grounding that is NOT declared complete: one plain learnt edge. */
|
|
52
|
+
async function plainChain() {
|
|
53
|
+
const m = new Mind({ seed: 7, store: new SQliteStore({ path: ":memory:" }) });
|
|
54
|
+
await m.ingest([["Eva director", "The director of Eva is Gustaf Molander."]]);
|
|
55
|
+
return m;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const moves = (steps) => steps.map((s) => s.mechanism.at(-1));
|
|
59
|
+
|
|
60
|
+
test("a binding that declares itself complete reports the stop", async () => {
|
|
61
|
+
const m = await frame();
|
|
62
|
+
const steps = [];
|
|
63
|
+
await m.respondText("How do I compile main.c?", (s) => steps.push(s));
|
|
64
|
+
|
|
65
|
+
const stop = steps.find((s) => s.mechanism.at(-1) === "completeGrounding");
|
|
66
|
+
assert.ok(stop, "the rationale must report the declared-complete stop");
|
|
67
|
+
assert.match(
|
|
68
|
+
stop.note,
|
|
69
|
+
/declared complete/,
|
|
70
|
+
"the note must carry the CLAIM, not a description of the answer",
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("the reported stop is real: the extension did not run", async () => {
|
|
75
|
+
const m = await frame();
|
|
76
|
+
const steps = [];
|
|
77
|
+
const answer = await m.respondText(
|
|
78
|
+
"How do I compile main.c?",
|
|
79
|
+
(s) => steps.push(s),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// The binding spliced the asker's referent (behaviour under test, asserted
|
|
83
|
+
// WITHOUT a trace below — a trace can change an answer, so the step and the
|
|
84
|
+
// bytes are pinned separately).
|
|
85
|
+
assert.ok(answer.length > 0, "the binding must answer");
|
|
86
|
+
const seen = moves(steps);
|
|
87
|
+
assert.equal(
|
|
88
|
+
seen.includes("pivotStep") || seen.includes("absorbForward"),
|
|
89
|
+
false,
|
|
90
|
+
"a complete grounding must not be extended",
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("the non-traced answer is the spliced one", async () => {
|
|
95
|
+
const m = await frame();
|
|
96
|
+
const answer = await m.respondText("How do I compile main.c?");
|
|
97
|
+
assert.equal(answer.replace(/\0+/g, "").trim(), "Run gcc main.c");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("an ordinary grounding reports no complete-grounding stop", async () => {
|
|
101
|
+
const m = await plainChain();
|
|
102
|
+
const steps = [];
|
|
103
|
+
await m.respondText("Eva director", (s) => steps.push(s));
|
|
104
|
+
assert.equal(
|
|
105
|
+
moves(steps).includes("completeGrounding"),
|
|
106
|
+
false,
|
|
107
|
+
"the step names a decision, not every response",
|
|
108
|
+
);
|
|
109
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// 101-alignment-gap-bound.test.mjs — the alignment's gap bound SAYS SO when it
|
|
2
|
+
// bites.
|
|
3
|
+
//
|
|
4
|
+
// THE GAP THIS CLOSES. `alignAround` matches a query against a stored context
|
|
5
|
+
// by sweeping for the next common run of ≥ W bytes, with each side's gap
|
|
6
|
+
// bounded by `chainReach(W)` = W². When the sweep ends with material still
|
|
7
|
+
// unmatched on BOTH sides, the alignment stopped because of that BOUND — and
|
|
8
|
+
// nothing reported it: `match.ts` emitted no trace at all. A reader of the
|
|
9
|
+
// rationale therefore saw a substituted answer (the frame filled with ANOTHER
|
|
10
|
+
// instance's filler) with no way to tell that the answer's own span had been
|
|
11
|
+
// refused by a cap.
|
|
12
|
+
//
|
|
13
|
+
// MEASURED BOUNDARY (W = 4, cap = 16). Against a learned
|
|
14
|
+
// `Book a table at <filler> tonight.` → `Your table at <filler> is booked.`
|
|
15
|
+
// frame, a NOVEL filler binds while it fits the cap and is replaced by a stored
|
|
16
|
+
// instance's filler once it does not:
|
|
17
|
+
//
|
|
18
|
+
// filler 7 B → answer quotes the asker's filler (bound)
|
|
19
|
+
// filler 12 B → answer quotes the asker's filler (bound)
|
|
20
|
+
// filler 18 B → answer carries ANOTHER filler (substituted)
|
|
21
|
+
// filler 30 B → answer carries ANOTHER filler (substituted)
|
|
22
|
+
//
|
|
23
|
+
// THIS FILE PINS THE REPORT, not the substitution: the step must exist and must
|
|
24
|
+
// carry the derived cap, so the bound is auditable at the point it truncates
|
|
25
|
+
// (AGENTS §6). The substitution itself is the scope defect a later lot fixes;
|
|
26
|
+
// when it is fixed, this file's behavioural control (a filler inside the cap is
|
|
27
|
+
// quoted back) stays true and the report stays reachable.
|
|
28
|
+
|
|
29
|
+
import { test } from "node:test";
|
|
30
|
+
import assert from "node:assert/strict";
|
|
31
|
+
import { Mind } from "../dist/src/index.js";
|
|
32
|
+
import { alignAround } from "../dist/src/mind/match.js";
|
|
33
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
34
|
+
|
|
35
|
+
const WORDS =
|
|
36
|
+
("alpha bravo charlie delta echo foxtrot golf hotel india juliet kilo lima " +
|
|
37
|
+
"mike november oscar papa quebec romeo sierra tango uniform victor whiskey " +
|
|
38
|
+
"xray yankee zulu amber bronze copper dahlia ember fjord gossamer harbour " +
|
|
39
|
+
"indigo jasmine kestrel lantern marigold nectar opal pewter quartz ripple " +
|
|
40
|
+
"saffron thistle umber violet willow xenon yarrow").split(" ");
|
|
41
|
+
|
|
42
|
+
const short = (i) => `${WORDS[i % 50]}${i}`;
|
|
43
|
+
const long = (i) =>
|
|
44
|
+
`${WORDS[i % 50]} ${WORDS[(i * 3) % 50]} ${WORDS[(i * 7) % 50]} street ${i}`;
|
|
45
|
+
|
|
46
|
+
/** The frame whose continuation quotes its filler, with mixed filler widths. */
|
|
47
|
+
async function frame(n = 60, opts = {}) {
|
|
48
|
+
const store = new SQliteStore({ path: ":memory:", D: 1024 });
|
|
49
|
+
const mind = new Mind({ seed: 7, store, ...opts });
|
|
50
|
+
const pairs = [];
|
|
51
|
+
for (let i = 0; i < n; i++) {
|
|
52
|
+
const f = i % 3 === 0 ? long(i) : short(i);
|
|
53
|
+
pairs.push([
|
|
54
|
+
`Book a table at ${f} tonight.`,
|
|
55
|
+
`Your table at ${f} is booked.`,
|
|
56
|
+
]);
|
|
57
|
+
}
|
|
58
|
+
await mind.ingest(pairs);
|
|
59
|
+
return { store, mind };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** A novel filler of exactly `len` bytes, never ingested. */
|
|
63
|
+
const novel = (len) => "zephyr quartz lantern ".repeat(3).slice(0, len).trim();
|
|
64
|
+
|
|
65
|
+
// THE LAW'S ASSERTION, straight on the aligner. There is no bound to report
|
|
66
|
+
// any more: the sweep's work is proportional to the bytes a run spans, so a
|
|
67
|
+
// divergence far past the old arity bound (chainReach(W) = 16) is simply
|
|
68
|
+
// bridged, on both sides, by finding the next common run.
|
|
69
|
+
test("a divergence far past the old arity bound is bridged, not truncated", () => {
|
|
70
|
+
const enc = new TextEncoder();
|
|
71
|
+
const head = "the common head of the frame ";
|
|
72
|
+
const tail = " and the common tail of the frame";
|
|
73
|
+
const q = enc.encode(head + "a".repeat(60) + tail);
|
|
74
|
+
const c = enc.encode(head + "b".repeat(60) + tail);
|
|
75
|
+
const at = head.length - 1; // the seed: the shared head's own boundary
|
|
76
|
+
const { matched, gaps } = alignAround(
|
|
77
|
+
{ space: { maxGroup: 4 } },
|
|
78
|
+
q,
|
|
79
|
+
c,
|
|
80
|
+
at,
|
|
81
|
+
at,
|
|
82
|
+
);
|
|
83
|
+
assert.equal(
|
|
84
|
+
matched.length,
|
|
85
|
+
2,
|
|
86
|
+
"both common runs must be found: " + JSON.stringify(matched),
|
|
87
|
+
);
|
|
88
|
+
assert.equal(gaps.length, 1, "with exactly one substitution between them");
|
|
89
|
+
const g = gaps[0];
|
|
90
|
+
assert.ok(
|
|
91
|
+
g.qe - g.qs >= 60 && g.ce - g.cs >= 60,
|
|
92
|
+
"the substitution's extent is the pair's own: " + JSON.stringify(g),
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("control: a filler inside the cap is quoted back, and the fixture is live", async () => {
|
|
97
|
+
const filler = novel(12);
|
|
98
|
+
const { mind } = await frame();
|
|
99
|
+
const answer = (await mind.respondText(
|
|
100
|
+
`Book a table at ${filler} tonight.`,
|
|
101
|
+
)).replace(/\0+/g, "").trim();
|
|
102
|
+
assert.ok(
|
|
103
|
+
answer.includes(filler),
|
|
104
|
+
"inside the cap the frame binds the asker's own filler",
|
|
105
|
+
);
|
|
106
|
+
});
|
|
@@ -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,137 @@
|
|
|
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
|
+
// …and the bytes it actually tried, EXACTLY. The `text` rendering cannot
|
|
77
|
+
// stand in for them: it decodes UTF-8 and DROPS NUL bytes, so a key carrying
|
|
78
|
+
// one is unrecoverable from it — which is why a refusal could not be tested
|
|
79
|
+
// exactly without re-encoding, and why this assertion reads BYTES.
|
|
80
|
+
//
|
|
81
|
+
// The relation is the rule's own: key = fact ‖ prefix(tail).
|
|
82
|
+
const carried = got.filter((s) => {
|
|
83
|
+
const raw = (s.inputs ?? []).map((i) => i.bytes);
|
|
84
|
+
if (raw.length < 3) return false;
|
|
85
|
+
const [fact, tail, key] = raw;
|
|
86
|
+
if (![fact, tail, key].every((b) => b instanceof Uint8Array)) return false;
|
|
87
|
+
if (key.length < fact.length) return false;
|
|
88
|
+
if (!fact.every((b, k) => key[k] === b)) return false;
|
|
89
|
+
const rest = key.subarray(fact.length);
|
|
90
|
+
return rest.every((b, k) => tail[k] === b);
|
|
91
|
+
});
|
|
92
|
+
assert.equal(
|
|
93
|
+
carried.length,
|
|
94
|
+
got.length,
|
|
95
|
+
`every refusal must carry its OWN bytes (key = fact ‖ prefix of tail), not ` +
|
|
96
|
+
`only a rendering — ${
|
|
97
|
+
got.length - carried.length
|
|
98
|
+
} of ${got.length} did not`,
|
|
99
|
+
);
|
|
100
|
+
await mind.store.close();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("a report per LETTER is noise: only forms are reported", async () => {
|
|
104
|
+
const mind = await chain();
|
|
105
|
+
const steps = [];
|
|
106
|
+
await mind.respond("eva director country capital", (s) => steps.push(s));
|
|
107
|
+
const got = misses(steps);
|
|
108
|
+
assert.ok(got.length > 0, "the real fact's refusal is still reported");
|
|
109
|
+
// STRUCTURAL, not a count: a report is only made for a FORM, so every report
|
|
110
|
+
// carries a piece at least one window long — no one-byte out reports a miss.
|
|
111
|
+
// `W` is the fixture's own geometry (the Mind's default maxGroup), the same
|
|
112
|
+
// line the rule uses.
|
|
113
|
+
const W = 4;
|
|
114
|
+
for (const m of got) {
|
|
115
|
+
const longest = Math.max(
|
|
116
|
+
0,
|
|
117
|
+
...(m.inputs ?? []).map((i) => String(i.text ?? "").length),
|
|
118
|
+
);
|
|
119
|
+
assert.ok(
|
|
120
|
+
longest >= W,
|
|
121
|
+
`a report must be about a form, got pieces of at most ${longest} bytes`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
await mind.store.close();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("a fully covered query is not a refusal", async () => {
|
|
128
|
+
const mind = await chain();
|
|
129
|
+
const steps = [];
|
|
130
|
+
await mind.respond("eva director", (s) => steps.push(s));
|
|
131
|
+
assert.equal(
|
|
132
|
+
misses(steps).length,
|
|
133
|
+
0,
|
|
134
|
+
"with no tail there is nothing to join through — not a miss",
|
|
135
|
+
);
|
|
136
|
+
await mind.store.close();
|
|
137
|
+
});
|