@hviana/sema 0.8.7 → 0.8.9
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/dist/src/meter.d.ts +6 -0
- package/dist/src/meter.js +6 -0
- package/dist/src/mind/recognition.js +63 -1
- package/dist/src/store.js +2 -2
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/meter.ts +6 -0
- package/src/mind/recognition.ts +57 -1
- package/src/store.ts +1 -1
- package/test/185-composite-continuation.test.mjs +103 -0
- package/test/186-embedded-middle.test.mjs +101 -0
package/dist/src/meter.d.ts
CHANGED
|
@@ -103,6 +103,12 @@ export declare class Meter {
|
|
|
103
103
|
recognisedBytes: number;
|
|
104
104
|
/** `recognise` calls served from the memo. */
|
|
105
105
|
recogniseHits: number;
|
|
106
|
+
/** Offsets the INTERIOR pass of `recognise` considered — the endpoints inside the gaps between consecutive
|
|
107
|
+
* content-defined cuts. A zero says that pass found nothing to offer; a non-zero with no new site says they
|
|
108
|
+
* were offered and refused. */
|
|
109
|
+
recogniseInteriorGaps: number;
|
|
110
|
+
/** Interior pairs that were actually probed (the pairs that passed the span bound and spent the budget). */
|
|
111
|
+
recogniseInteriorPairs: number;
|
|
106
112
|
/** `resolve` — whole-span content-addressed identity requests. */
|
|
107
113
|
resolves: number;
|
|
108
114
|
/** `climbAttentionAll` calls that actually climbed. */
|
package/dist/src/meter.js
CHANGED
|
@@ -97,6 +97,12 @@ export class Meter {
|
|
|
97
97
|
recognisedBytes = 0;
|
|
98
98
|
/** `recognise` calls served from the memo. */
|
|
99
99
|
recogniseHits = 0;
|
|
100
|
+
/** Offsets the INTERIOR pass of `recognise` considered — the endpoints inside the gaps between consecutive
|
|
101
|
+
* content-defined cuts. A zero says that pass found nothing to offer; a non-zero with no new site says they
|
|
102
|
+
* were offered and refused. */
|
|
103
|
+
recogniseInteriorGaps = 0;
|
|
104
|
+
/** Interior pairs that were actually probed (the pairs that passed the span bound and spent the budget). */
|
|
105
|
+
recogniseInteriorPairs = 0;
|
|
100
106
|
/** `resolve` — whole-span content-addressed identity requests. */
|
|
101
107
|
resolves = 0;
|
|
102
108
|
// ── Mind: the consensus climb ───────────────────────────────────────────
|
|
@@ -600,7 +600,14 @@ function recogniseImpl(ctx, bytes) {
|
|
|
600
600
|
// keeps this off the quadratic path the budget note above describes (that
|
|
601
601
|
// one had no span bound at all).
|
|
602
602
|
{
|
|
603
|
-
|
|
603
|
+
// The span bound is W^2, the chain's own limit, PLUS the slack the endpoint set already grants: every endpoint
|
|
604
|
+
// sits within `radius` of a cut, so a pair that names one form may straddle cuts and still be a single form's
|
|
605
|
+
// span. Measured on the composite fixture: W=4 (reach 16), radius 8, and the useful [7,32) is 25 bytes with its
|
|
606
|
+
// edges 2 bytes from cuts 5 and 30 — already IN `ordered`, and excluded only by the upper bound. Both terms are
|
|
607
|
+
// derived (W and the seat count); no new constant enters.
|
|
608
|
+
const reach = chainReach(W) + 2 * radius;
|
|
609
|
+
if (ctx.meter)
|
|
610
|
+
ctx.meter.recogniseInteriorGaps += ordered.length;
|
|
604
611
|
for (const end of ordered) {
|
|
605
612
|
for (const start of ordered) {
|
|
606
613
|
if (start >= end)
|
|
@@ -608,9 +615,64 @@ function recogniseImpl(ctx, bytes) {
|
|
|
608
615
|
const span = end - start;
|
|
609
616
|
if (span < W || span > reach)
|
|
610
617
|
continue;
|
|
618
|
+
if (ctx.meter)
|
|
619
|
+
ctx.meter.recogniseInteriorPairs++;
|
|
611
620
|
spend(start, end);
|
|
612
621
|
}
|
|
613
622
|
}
|
|
623
|
+
// ── THE MIDDLE WAS BLIND: a CUT-PAIR probe, BOUNDED so it stays LINEAR ──────────────
|
|
624
|
+
// A stored form embedded in the MIDDLE of a longer query was named by NO tier: the two
|
|
625
|
+
// edge scans probe only prefixes and suffixes, and the loop above is capped at
|
|
626
|
+
// `reach` = W^2 + 2*radius. Measured on the trained corpus, the SAME real contexts in
|
|
627
|
+
// three positions: opening 12/17, MIDDLE 0/17, end 12/17. Both edges of such a form sit
|
|
628
|
+
// within `radius` of CUTS, so a cut pair plus a small trim names the form exactly
|
|
629
|
+
// (measured: a 64 B form at [27, 91) between cuts 26 and 90, trims +1/+1).
|
|
630
|
+
//
|
|
631
|
+
// THE BOUND IS WHAT KEEPS IT LINEAR. An unbounded cut-pair scan is O(cuts^2) probes, and
|
|
632
|
+
// test/14 rejected exactly that (47 324 ms). Bounded by a CONSTANT span it is
|
|
633
|
+
// O(cuts * const) = O(n), like the loop above — the constant is merely larger. The bound
|
|
634
|
+
// is DERIVED, never pinned: `chainReach(W)` is W^2, "the deepest two-level composite the
|
|
635
|
+
// write side's windows can spell" (canonical.ts), so `chainReach(W) * W * W` is the
|
|
636
|
+
// four-level one; `chainReach(W) * W` is already used in bridge.ts.
|
|
637
|
+
//
|
|
638
|
+
// THE TRIMS ARE THE SAME DISCIPLINE THE EDGE SCANS USE — the suffix scan already probes
|
|
639
|
+
// `spend(s, bytes.length - 1)`, so ±W is a wider version of an existing rule, not a new
|
|
640
|
+
// threshold. Measured price/reach; all four configurations pass test/14, which gates the
|
|
641
|
+
// CLASS (linear) and not the constant:
|
|
642
|
+
// W^3 + ±1 -> 64 B, 8 068 probes (x2.02)
|
|
643
|
+
// W^4 + ±W -> 136 B, 70 888 probes (x6.06) <- this one
|
|
644
|
+
// W^3 + ±W and W^4 + ±1 are DOMINATED: each stops at the other parameter and costs more.
|
|
645
|
+
// The loop above stays, so no candidate that produces a site today is lost. Suite
|
|
646
|
+
// 707/707, and a differential over 24 real corpus questions is byte-identical (0 answers
|
|
647
|
+
// changed, 0 new duplicate sites): the :258 warning that a wider bound can rediscover a
|
|
648
|
+
// smaller subtree's content as a second, overlapping site was read and measured, and it
|
|
649
|
+
// does not materialise here.
|
|
650
|
+
const deepReach = chainReach(W) * W * W;
|
|
651
|
+
for (let ci = 0; ci + 1 < startList.length; ci++) {
|
|
652
|
+
for (let cj = ci + 1; cj < startList.length; cj++) {
|
|
653
|
+
if (startList[cj] - startList[ci] > deepReach + 2 * radius)
|
|
654
|
+
break;
|
|
655
|
+
// The EXACT cut pair first. Measured: with the 81 trims starting at -W the `spend`
|
|
656
|
+
// pool ran dry (canonProbesDenied in the millions) and the canon route was then denied
|
|
657
|
+
// to the candidates that needed it — including forms the byte-exact route SEES
|
|
658
|
+
// (`flatProbe` true) that were still not named. Probing (ci, cj) before any trim puts
|
|
659
|
+
// the common case in front of the famine.
|
|
660
|
+
if (ctx.meter)
|
|
661
|
+
ctx.meter.recogniseInteriorPairs++;
|
|
662
|
+
spend(startList[ci], startList[cj]);
|
|
663
|
+
for (let dl = -W; dl <= W; dl++) {
|
|
664
|
+
for (let dr = -W; dr <= W; dr++) {
|
|
665
|
+
const a = startList[ci] + dl;
|
|
666
|
+
const z = startList[cj] + dr;
|
|
667
|
+
if (a < 0 || z > bytes.length || z - a < W)
|
|
668
|
+
continue;
|
|
669
|
+
if (ctx.meter)
|
|
670
|
+
ctx.meter.recogniseInteriorPairs++;
|
|
671
|
+
spend(a, z);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
614
676
|
}
|
|
615
677
|
}
|
|
616
678
|
}
|
package/dist/src/store.js
CHANGED
|
@@ -822,10 +822,10 @@ export class AbstractStore {
|
|
|
822
822
|
}
|
|
823
823
|
// ── Content-addressed lookup ───────────────────────────────────────────
|
|
824
824
|
findLeaf(bytes) {
|
|
825
|
-
if (this.meter)
|
|
826
|
-
this.meter.leafLookups++;
|
|
827
825
|
if (bytes.length === 1)
|
|
828
826
|
return -(bytes[0] + 1);
|
|
827
|
+
if (this.meter)
|
|
828
|
+
this.meter.leafLookups++;
|
|
829
829
|
const key = keyOf(bytes);
|
|
830
830
|
const cached = this._leafKey.get(key);
|
|
831
831
|
if (cached !== undefined)
|
package/jsr.json
CHANGED
package/package.json
CHANGED
package/src/meter.ts
CHANGED
|
@@ -149,6 +149,12 @@ export class Meter {
|
|
|
149
149
|
recognisedBytes = 0;
|
|
150
150
|
/** `recognise` calls served from the memo. */
|
|
151
151
|
recogniseHits = 0;
|
|
152
|
+
/** Offsets the INTERIOR pass of `recognise` considered — the endpoints inside the gaps between consecutive
|
|
153
|
+
* content-defined cuts. A zero says that pass found nothing to offer; a non-zero with no new site says they
|
|
154
|
+
* were offered and refused. */
|
|
155
|
+
recogniseInteriorGaps = 0;
|
|
156
|
+
/** Interior pairs that were actually probed (the pairs that passed the span bound and spent the budget). */
|
|
157
|
+
recogniseInteriorPairs = 0;
|
|
152
158
|
/** `resolve` — whole-span content-addressed identity requests. */
|
|
153
159
|
resolves = 0;
|
|
154
160
|
|
package/src/mind/recognition.ts
CHANGED
|
@@ -610,15 +610,71 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
|
|
|
610
610
|
// keeps this off the quadratic path the budget note above describes (that
|
|
611
611
|
// one had no span bound at all).
|
|
612
612
|
{
|
|
613
|
-
|
|
613
|
+
// The span bound is W^2, the chain's own limit, PLUS the slack the endpoint set already grants: every endpoint
|
|
614
|
+
// sits within `radius` of a cut, so a pair that names one form may straddle cuts and still be a single form's
|
|
615
|
+
// span. Measured on the composite fixture: W=4 (reach 16), radius 8, and the useful [7,32) is 25 bytes with its
|
|
616
|
+
// edges 2 bytes from cuts 5 and 30 — already IN `ordered`, and excluded only by the upper bound. Both terms are
|
|
617
|
+
// derived (W and the seat count); no new constant enters.
|
|
618
|
+
const reach = chainReach(W) + 2 * radius;
|
|
619
|
+
if (ctx.meter) ctx.meter.recogniseInteriorGaps += ordered.length;
|
|
614
620
|
for (const end of ordered) {
|
|
615
621
|
for (const start of ordered) {
|
|
616
622
|
if (start >= end) continue;
|
|
617
623
|
const span = end - start;
|
|
618
624
|
if (span < W || span > reach) continue;
|
|
625
|
+
if (ctx.meter) ctx.meter.recogniseInteriorPairs++;
|
|
619
626
|
spend(start, end);
|
|
620
627
|
}
|
|
621
628
|
}
|
|
629
|
+
// ── THE MIDDLE WAS BLIND: a CUT-PAIR probe, BOUNDED so it stays LINEAR ──────────────
|
|
630
|
+
// A stored form embedded in the MIDDLE of a longer query was named by NO tier: the two
|
|
631
|
+
// edge scans probe only prefixes and suffixes, and the loop above is capped at
|
|
632
|
+
// `reach` = W^2 + 2*radius. Measured on the trained corpus, the SAME real contexts in
|
|
633
|
+
// three positions: opening 12/17, MIDDLE 0/17, end 12/17. Both edges of such a form sit
|
|
634
|
+
// within `radius` of CUTS, so a cut pair plus a small trim names the form exactly
|
|
635
|
+
// (measured: a 64 B form at [27, 91) between cuts 26 and 90, trims +1/+1).
|
|
636
|
+
//
|
|
637
|
+
// THE BOUND IS WHAT KEEPS IT LINEAR. An unbounded cut-pair scan is O(cuts^2) probes, and
|
|
638
|
+
// test/14 rejected exactly that (47 324 ms). Bounded by a CONSTANT span it is
|
|
639
|
+
// O(cuts * const) = O(n), like the loop above — the constant is merely larger. The bound
|
|
640
|
+
// is DERIVED, never pinned: `chainReach(W)` is W^2, "the deepest two-level composite the
|
|
641
|
+
// write side's windows can spell" (canonical.ts), so `chainReach(W) * W * W` is the
|
|
642
|
+
// four-level one; `chainReach(W) * W` is already used in bridge.ts.
|
|
643
|
+
//
|
|
644
|
+
// THE TRIMS ARE THE SAME DISCIPLINE THE EDGE SCANS USE — the suffix scan already probes
|
|
645
|
+
// `spend(s, bytes.length - 1)`, so ±W is a wider version of an existing rule, not a new
|
|
646
|
+
// threshold. Measured price/reach; all four configurations pass test/14, which gates the
|
|
647
|
+
// CLASS (linear) and not the constant:
|
|
648
|
+
// W^3 + ±1 -> 64 B, 8 068 probes (x2.02)
|
|
649
|
+
// W^4 + ±W -> 136 B, 70 888 probes (x6.06) <- this one
|
|
650
|
+
// W^3 + ±W and W^4 + ±1 are DOMINATED: each stops at the other parameter and costs more.
|
|
651
|
+
// The loop above stays, so no candidate that produces a site today is lost. Suite
|
|
652
|
+
// 707/707, and a differential over 24 real corpus questions is byte-identical (0 answers
|
|
653
|
+
// changed, 0 new duplicate sites): the :258 warning that a wider bound can rediscover a
|
|
654
|
+
// smaller subtree's content as a second, overlapping site was read and measured, and it
|
|
655
|
+
// does not materialise here.
|
|
656
|
+
const deepReach = chainReach(W) * W * W;
|
|
657
|
+
for (let ci = 0; ci + 1 < startList.length; ci++) {
|
|
658
|
+
for (let cj = ci + 1; cj < startList.length; cj++) {
|
|
659
|
+
if (startList[cj] - startList[ci] > deepReach + 2 * radius) break;
|
|
660
|
+
// The EXACT cut pair first. Measured: with the 81 trims starting at -W the `spend`
|
|
661
|
+
// pool ran dry (canonProbesDenied in the millions) and the canon route was then denied
|
|
662
|
+
// to the candidates that needed it — including forms the byte-exact route SEES
|
|
663
|
+
// (`flatProbe` true) that were still not named. Probing (ci, cj) before any trim puts
|
|
664
|
+
// the common case in front of the famine.
|
|
665
|
+
if (ctx.meter) ctx.meter.recogniseInteriorPairs++;
|
|
666
|
+
spend(startList[ci], startList[cj]);
|
|
667
|
+
for (let dl = -W; dl <= W; dl++) {
|
|
668
|
+
for (let dr = -W; dr <= W; dr++) {
|
|
669
|
+
const a = startList[ci] + dl;
|
|
670
|
+
const z = startList[cj] + dr;
|
|
671
|
+
if (a < 0 || z > bytes.length || z - a < W) continue;
|
|
672
|
+
if (ctx.meter) ctx.meter.recogniseInteriorPairs++;
|
|
673
|
+
spend(a, z);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
622
678
|
}
|
|
623
679
|
}
|
|
624
680
|
}
|
package/src/store.ts
CHANGED
|
@@ -1377,8 +1377,8 @@ export abstract class AbstractStore implements Store {
|
|
|
1377
1377
|
// ── Content-addressed lookup ───────────────────────────────────────────
|
|
1378
1378
|
|
|
1379
1379
|
findLeaf(bytes: Uint8Array): NodeId | null {
|
|
1380
|
-
if (this.meter) this.meter.leafLookups++;
|
|
1381
1380
|
if (bytes.length === 1) return -(bytes[0] + 1);
|
|
1381
|
+
if (this.meter) this.meter.leafLookups++;
|
|
1382
1382
|
const key = keyOf(bytes);
|
|
1383
1383
|
const cached = this._leafKey.get(key);
|
|
1384
1384
|
if (cached !== undefined) return cached;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// 185-composite-continuation.test.mjs — the interior-offset probe of `recognise`.
|
|
2
|
+
//
|
|
3
|
+
// THE CAPABILITY. A question that names the PARTS of a stored composite reaches the fact whose SUBJECT is those parts
|
|
4
|
+
// and answers with that fact's own continuation. Before the probe the recogniser proposed only spans aligned to the
|
|
5
|
+
// question's own fold, so a stored form whose two edges are not fold cuts was never offered to the engine: measured, the
|
|
6
|
+
// two-part question below answered "Alderman is a settlement" — the second part's own fact — instead of the composite's
|
|
7
|
+
// continuation, which is the defect recorded in test/181's regime-B entry.
|
|
8
|
+
//
|
|
9
|
+
// HOW. The probe offers the offsets INTERIOR to the gaps between consecutive content-defined cuts, paired on both
|
|
10
|
+
// edges, gated by `findFlatBranch` (the allocation-free branch probe) and confirmed by `canonResolve`/`resolve`.
|
|
11
|
+
// It runs for the node that IS the whole byte stream, once per byte stream.
|
|
12
|
+
//
|
|
13
|
+
// WHAT THIS FILE PINS: the answers themselves, the determinism of the reading, and that the instrumented counters are
|
|
14
|
+
// wired — NOT their magnitudes, which are a cost matter with its own measurement.
|
|
15
|
+
import test from "node:test";
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
import { Mind } from "../dist/src/index.js";
|
|
18
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
19
|
+
|
|
20
|
+
const A = "the founder was born in Alderman";
|
|
21
|
+
const B = "the capital is Brackwater";
|
|
22
|
+
|
|
23
|
+
async function mind() {
|
|
24
|
+
const m = new Mind({
|
|
25
|
+
seed: 7,
|
|
26
|
+
store: new SQliteStore({ path: ":memory:" }),
|
|
27
|
+
profile: true,
|
|
28
|
+
});
|
|
29
|
+
await m.ingest([
|
|
30
|
+
[A, "Alderman is a settlement"],
|
|
31
|
+
[B, "Brackwater is a province"],
|
|
32
|
+
[`${A} and ${B}`, "that settlement is Cordelia"],
|
|
33
|
+
["the note is filed", "the note is in folder"],
|
|
34
|
+
]);
|
|
35
|
+
return m;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
test("recognise(): a question naming the parts of a composite answers with the composite's own continuation", async () => {
|
|
39
|
+
const m = await mind();
|
|
40
|
+
const got = String(await m.respondText(`I ask: ${B}, and ${A}.`, () => {}));
|
|
41
|
+
assert.match(
|
|
42
|
+
got,
|
|
43
|
+
/that settlement is Cordelia/,
|
|
44
|
+
`the composite's continuation must be reachable through its parts, got: ${
|
|
45
|
+
JSON.stringify(got)
|
|
46
|
+
}`,
|
|
47
|
+
);
|
|
48
|
+
assert.match(
|
|
49
|
+
got,
|
|
50
|
+
/Alderman is a settlement/,
|
|
51
|
+
`and the other part of the question stays explained too, got: ${
|
|
52
|
+
JSON.stringify(got)
|
|
53
|
+
}`,
|
|
54
|
+
);
|
|
55
|
+
await m.store.close();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("recognise(): ONE part of the composite is enough to reach it", async () => {
|
|
59
|
+
const m = await mind();
|
|
60
|
+
const got = String(await m.respondText(`I ask: ${B}.`, () => {}));
|
|
61
|
+
assert.equal(got.trim(), "that settlement is Cordelia");
|
|
62
|
+
await m.store.close();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("recognise(): the parts may be named in either order", async () => {
|
|
66
|
+
const m = await mind();
|
|
67
|
+
const got = String(await m.respondText(`I ask: ${A}, and ${B}.`, () => {}));
|
|
68
|
+
assert.match(
|
|
69
|
+
got,
|
|
70
|
+
/that settlement is Cordelia/,
|
|
71
|
+
`the store's order decides, not the question's, got: ${
|
|
72
|
+
JSON.stringify(got)
|
|
73
|
+
}`,
|
|
74
|
+
);
|
|
75
|
+
await m.store.close();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("recognise(): the reading is deterministic across two answers", async () => {
|
|
79
|
+
const m = await mind();
|
|
80
|
+
const one = String(await m.respondText(`I ask: ${B}, and ${A}.`, () => {}));
|
|
81
|
+
const two = String(await m.respondText(`I ask: ${B}, and ${A}.`, () => {}));
|
|
82
|
+
assert.equal(two, one);
|
|
83
|
+
await m.store.close();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("recognise(): the interior tier is instrumented, and the whole-stream guard is what runs it", async () => {
|
|
87
|
+
const m = await mind();
|
|
88
|
+
await m.respondText(`I ask: ${B}, and ${A}.`, () => {});
|
|
89
|
+
const c = m.lastCost?.counters ?? {};
|
|
90
|
+
for (const k of ["recogniseInteriorGaps", "recogniseInteriorPairs"]) {
|
|
91
|
+
assert.ok(
|
|
92
|
+
Object.hasOwn(c, k),
|
|
93
|
+
`the meter must carry ${k} — a cost claim with no counter is a claim with no evidence (got: ${
|
|
94
|
+
Object.keys(c).join(",")
|
|
95
|
+
})`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
assert.ok(
|
|
99
|
+
c.recogniseInteriorPairs > 0,
|
|
100
|
+
"the interior tier must have probed at least one gated pair",
|
|
101
|
+
);
|
|
102
|
+
await m.store.close();
|
|
103
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// 186-embedded-middle.test.mjs — the CUT-PAIR probe of `recognise`: a stored form in the MIDDLE of a query.
|
|
2
|
+
//
|
|
3
|
+
// THE CAPABILITY. A stored form embedded in the MIDDLE of a longer question is named, and its continuation is
|
|
4
|
+
// answered. No tier reached it before: the two edge scans probe only prefixes and suffixes (`spend(0, prefixes[i])`,
|
|
5
|
+
// `spend(s, bytes.length)`), and the interior pass was capped at `reach` = W^2 + 2*radius. Measured on the trained
|
|
6
|
+
// corpus, the SAME real contexts in three positions: opening 12/17, MIDDLE 0/17, end 12/17.
|
|
7
|
+
//
|
|
8
|
+
// HOW. `startList` already holds the content-defined cuts, and both edges of such a form sit within `radius` of a
|
|
9
|
+
// cut. The probe therefore pairs CUTS — the exact pair first, then a ±W trim — under a constant span bound derived
|
|
10
|
+
// from W (`chainReach(W) * W * W`, never a pinned number). The bound is what keeps it linear: an unbounded cut-pair
|
|
11
|
+
// scan is O(cuts^2) and test/14 rejects it.
|
|
12
|
+
//
|
|
13
|
+
// WHAT THIS FILE PINS: the recognition and the ANSWER at all three positions, that an unstored form is still refused
|
|
14
|
+
// (the negative control that keeps the middle assertions from passing for the wrong reason), and the determinism of
|
|
15
|
+
// the reading. NOT magnitudes — those are a cost matter with its own measurement in test/14.
|
|
16
|
+
import test from "node:test";
|
|
17
|
+
import assert from "node:assert/strict";
|
|
18
|
+
import { Mind } from "../dist/src/index.js";
|
|
19
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
20
|
+
import { recognise } from "../dist/src/mind/recognition.js";
|
|
21
|
+
|
|
22
|
+
const dec = new TextDecoder();
|
|
23
|
+
// 64 B, so the form is TWICE `reach` (W^2 + 2*radius) — the dead zone the cut-pair probe exists to close.
|
|
24
|
+
const FORM = "the painter was born in Verano and the river runs through Verano";
|
|
25
|
+
const CONTINUATION = "that region is Calenta";
|
|
26
|
+
const OTHER = "the harbour master";
|
|
27
|
+
const OTHER_CONTINUATION = "the harbour is at Kestrel";
|
|
28
|
+
|
|
29
|
+
async function mind() {
|
|
30
|
+
const m = new Mind({
|
|
31
|
+
seed: 7,
|
|
32
|
+
store: new SQliteStore({ path: ":memory:" }),
|
|
33
|
+
profile: true,
|
|
34
|
+
});
|
|
35
|
+
await m.ingest([
|
|
36
|
+
[FORM, CONTINUATION],
|
|
37
|
+
[OTHER, OTHER_CONTINUATION],
|
|
38
|
+
]);
|
|
39
|
+
return m;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function named(m, query, form) {
|
|
43
|
+
const bytes = new TextEncoder().encode(query);
|
|
44
|
+
return recognise(m, bytes).sites.some((s) =>
|
|
45
|
+
dec.decode(bytes.subarray(s.start, s.end)) === form
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
test("recognise(): a stored form EMBEDDED IN THE MIDDLE of a longer query is named", async () => {
|
|
50
|
+
const m = await mind();
|
|
51
|
+
const query = `I ask: ${OTHER}, ${FORM}, and the note is filed.`;
|
|
52
|
+
assert.equal(named(m, query, FORM), true);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("the middle form is ANSWERED with its own continuation, not the surrounding fact's", async () => {
|
|
56
|
+
const m = await mind();
|
|
57
|
+
const answer = String(
|
|
58
|
+
await m.respondText(
|
|
59
|
+
`I ask: ${OTHER}, ${FORM}, and the note is filed.`,
|
|
60
|
+
() => {},
|
|
61
|
+
),
|
|
62
|
+
);
|
|
63
|
+
assert.match(answer, /Calenta/);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("the same form at the OPENING and at the END is still named — the edge tiers did not regress", async () => {
|
|
67
|
+
const m = await mind();
|
|
68
|
+
assert.equal(named(m, `${FORM}, and the note is filed.`, FORM), true);
|
|
69
|
+
assert.equal(named(m, `I ask: ${OTHER}, and ${FORM}.`, FORM), true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("an UNSTORED form is still refused in the middle — the assertion above is not vacuous", async () => {
|
|
73
|
+
const m = await mind();
|
|
74
|
+
// Same LENGTH by construction (one word swapped), and never deposited: the middle assertions must not
|
|
75
|
+
// pass merely because some longer span happens to cover the region.
|
|
76
|
+
const unstored = FORM.replace("painter", "sculpto");
|
|
77
|
+
assert.equal(
|
|
78
|
+
unstored.length,
|
|
79
|
+
FORM.length,
|
|
80
|
+
"the control must be the same size as the form",
|
|
81
|
+
);
|
|
82
|
+
assert.notEqual(unstored, FORM);
|
|
83
|
+
assert.equal(
|
|
84
|
+
named(m, `I ask: ${OTHER}, ${unstored}, and the note is filed.`, unstored),
|
|
85
|
+
false,
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("the reading is deterministic, and the interior counter is wired", async () => {
|
|
90
|
+
const first = await mind();
|
|
91
|
+
const second = await mind();
|
|
92
|
+
const query = `I ask: ${OTHER}, ${FORM}, and the note is filed.`;
|
|
93
|
+
const a = String(await first.respondText(query, () => {}));
|
|
94
|
+
const b = String(await second.respondText(query, () => {}));
|
|
95
|
+
assert.equal(a, b);
|
|
96
|
+
const counters = first.lastCost?.counters ?? {};
|
|
97
|
+
assert.ok(
|
|
98
|
+
(counters.recogniseInteriorPairs ?? 0) > 0,
|
|
99
|
+
"recogniseInteriorPairs must be wired",
|
|
100
|
+
);
|
|
101
|
+
});
|