@hviana/sema 0.7.8 → 0.8.0
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/mind/graph-search.js +29 -9
- package/dist/src/mind/mind.d.ts +5 -0
- package/dist/src/mind/mind.js +8 -1
- package/dist/src/mind/recognition.js +30 -15
- package/dist/src/mind/types.d.ts +6 -0
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/mind/graph-search.ts +34 -10
- package/src/mind/mind.ts +9 -0
- package/src/mind/recognition.ts +28 -13
- package/src/mind/types.ts +6 -0
- package/test/46-recognise-multibyte-edge.test.mjs +33 -0
- package/test/99-fact-join.test.mjs +38 -0
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// So this engine stays decoupled: it knows the graph's shape, never how the
|
|
17
17
|
// graph was learnt.
|
|
18
18
|
import { lightestDerivation, } from "../derive/src/index.js";
|
|
19
|
-
import { bytesEqual, concat2, concatBytes, latin1 } from "../bytes.js";
|
|
19
|
+
import { bytesEqual, concat2, concatBytes, indexOf, latin1 } from "../bytes.js";
|
|
20
20
|
import { ALL } from "./types.js";
|
|
21
21
|
// The cost ladder is a strict ORDERING, not tuned magic:
|
|
22
22
|
// • Coverage dominates everything: leaving one query byte unrecognised (PASS)
|
|
@@ -894,14 +894,34 @@ export class GraphSearch {
|
|
|
894
894
|
// The entity candidates are the forms the fact's own bytes CONTAIN — the
|
|
895
895
|
// same recogniser the query went through, so the evidence standard is the
|
|
896
896
|
// query's. A byte atom is never a subject; the fact's own node is the span
|
|
897
|
-
// itself, not an entity inside it.
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
897
|
+
// itself, not an entity inside it. The admission predicate has ONE
|
|
898
|
+
// definition — `traverse.ts`'s `leadsSomewhere` (edge or halo); the host
|
|
899
|
+
// LENDS it when it can (Mind does, with the response-scoped struct cache),
|
|
900
|
+
// and a bare host falls back to the raw-store probe, so the search stays
|
|
901
|
+
// host-based.
|
|
902
|
+
const leading = this.host.recogniseSpan(fact.bytes).sites.filter((s) => s.payload >= 0 && s.payload !== fact.node &&
|
|
903
|
+
(this.host.leadsSomewhere !== undefined
|
|
904
|
+
? this.host.leadsSomewhere(s.payload)
|
|
905
|
+
: this.store.hasNext(s.payload) || this.store.hasHalo(s.payload)));
|
|
906
|
+
// …then prefer the entity the query did NOT name, and the MAXIMAL one. The
|
|
907
|
+
// join exists to reach the subject the query never wrote, so:
|
|
908
|
+
// • a candidate the query already contains is the query's OWN subject, and
|
|
909
|
+
// following its fact answers about that subject instead of the inferred
|
|
910
|
+
// one (measured: "Gustaf Molander spouse date of death" gave Gustaf's
|
|
911
|
+
// date of death, not his spouse's);
|
|
912
|
+
// • a candidate contained in a longer one is not the entity the fact
|
|
913
|
+
// introduces ("Timur" must not win over "Timur Bekmambetov").
|
|
914
|
+
// Byte work over bytes already read, and the pruning REMOVES the
|
|
915
|
+
// resolve()/nextFirst() probes these candidates would have paid.
|
|
916
|
+
const candidates = leading
|
|
917
|
+
.map((s) => ({
|
|
918
|
+
payload: s.payload,
|
|
919
|
+
bytes: this.store.bytesPrefix(s.payload, ALL),
|
|
920
|
+
}))
|
|
921
|
+
.filter((c) => indexOf(queryBytes, c.bytes, 0) < 0)
|
|
922
|
+
.filter((c, _i, all) => !all.some((o) => o.bytes.length > c.bytes.length && indexOf(o.bytes, c.bytes, 0) >= 0));
|
|
923
|
+
for (const c of candidates) {
|
|
924
|
+
const key = this.host.resolve(concat2(c.bytes, tail));
|
|
905
925
|
if (key === null)
|
|
906
926
|
continue;
|
|
907
927
|
const nx = this.store.nextFirst(key, 1);
|
package/dist/src/mind/mind.d.ts
CHANGED
|
@@ -150,6 +150,11 @@ export declare class Mind implements MindContext {
|
|
|
150
150
|
private _conversations;
|
|
151
151
|
/** Canonical node id of a byte span. Required by GraphSearchHost & MindContext. */
|
|
152
152
|
resolve(bytes: Uint8Array): number | null;
|
|
153
|
+
/** Whether a node leads somewhere — the admission predicate, delegating to
|
|
154
|
+
* `traverse.ts`'s ONE definition (edge or halo, with its response-scoped
|
|
155
|
+
* cache). The search holds a bare Store and cannot reach that cache itself,
|
|
156
|
+
* so it asks through this hook; a bare host keeps its raw-store fallback. */
|
|
157
|
+
leadsSomewhere(id: number): boolean;
|
|
153
158
|
recogniseSpan(bytes: Uint8Array): {
|
|
154
159
|
sites: ReadonlyArray<Site>;
|
|
155
160
|
leaves: ReadonlyArray<Leaf>;
|
package/dist/src/mind/mind.js
CHANGED
|
@@ -20,7 +20,7 @@ import { GraphSearch, } from "./graph-search.js";
|
|
|
20
20
|
import { Alu } from "../alu/src/index.js";
|
|
21
21
|
import { decodeText, Rationale, } from "./rationale.js";
|
|
22
22
|
import { gistOf, inputBytes, perceive as perceiveImpl, perceiveKey, resolve as resolveImpl, } from "./primitives.js";
|
|
23
|
-
import { chooseNext, edgeAncestors as edgeAncestorsFn, invalidateStructuralCaches, } from "./traverse.js";
|
|
23
|
+
import { chooseNext, edgeAncestors as edgeAncestorsFn, invalidateStructuralCaches, leadsSomewhere, } from "./traverse.js";
|
|
24
24
|
import { invalidateJunctionCache } from "./junction.js";
|
|
25
25
|
import { follow } from "./match.js";
|
|
26
26
|
import { recognise, segment } from "./recognition.js";
|
|
@@ -112,6 +112,13 @@ export class Mind {
|
|
|
112
112
|
resolve(bytes) {
|
|
113
113
|
return resolveImpl(this, bytes);
|
|
114
114
|
}
|
|
115
|
+
/** Whether a node leads somewhere — the admission predicate, delegating to
|
|
116
|
+
* `traverse.ts`'s ONE definition (edge or halo, with its response-scoped
|
|
117
|
+
* cache). The search holds a bare Store and cannot reach that cache itself,
|
|
118
|
+
* so it asks through this hook; a bare host keeps its raw-store fallback. */
|
|
119
|
+
leadsSomewhere(id) {
|
|
120
|
+
return leadsSomewhere(this, id);
|
|
121
|
+
}
|
|
115
122
|
// recogniseSpan wraps recognise
|
|
116
123
|
recogniseSpan(bytes) {
|
|
117
124
|
const r = recognise(this, bytes);
|
|
@@ -514,18 +514,21 @@ function recogniseImpl(ctx, bytes) {
|
|
|
514
514
|
// starts on a fold cut nor ends on a node edge was unreachable by either
|
|
515
515
|
// tier — the exact site whose loss `tryChain`'s own note records as "the
|
|
516
516
|
// pivot dies with the site and multi-hop goes silent". The interior
|
|
517
|
-
// pass below spends the same budget on those pairs.
|
|
517
|
+
// pass below spends the same budget on those pairs. Returns whether it
|
|
518
|
+
// emitted, so a caller can retry a trimmed edge on the miss path only.
|
|
518
519
|
if (end - start < W)
|
|
519
|
-
return;
|
|
520
|
+
return false;
|
|
520
521
|
if (flatProbe(start, end) === null) {
|
|
521
522
|
if (!canonBudget)
|
|
522
|
-
return;
|
|
523
|
+
return false;
|
|
523
524
|
if (!canonAdmits(start, end))
|
|
524
|
-
return;
|
|
525
|
+
return false;
|
|
525
526
|
}
|
|
526
527
|
const id = resolveSpan(start, end);
|
|
527
|
-
if (id
|
|
528
|
-
|
|
528
|
+
if (id === null)
|
|
529
|
+
return false;
|
|
530
|
+
emit(start, end, id);
|
|
531
|
+
return true;
|
|
529
532
|
};
|
|
530
533
|
// A CUMULATIVE BYTE BUDGET, SPENT SHORTEST-SPAN-FIRST.
|
|
531
534
|
//
|
|
@@ -562,10 +565,7 @@ function recogniseImpl(ctx, bytes) {
|
|
|
562
565
|
const afford = span <= budget;
|
|
563
566
|
if (afford)
|
|
564
567
|
budget -= span;
|
|
565
|
-
probe(start, end, afford);
|
|
566
|
-
// Always keep walking: the exact route is unbudgeted, so running out
|
|
567
|
-
// of canon budget must not stop the scan.
|
|
568
|
-
return true;
|
|
568
|
+
return probe(start, end, afford);
|
|
569
569
|
};
|
|
570
570
|
const prefixes = ordered.filter((e) => e > 0).sort((a, b) => a - b);
|
|
571
571
|
const suffixes = ordered
|
|
@@ -573,11 +573,26 @@ function recogniseImpl(ctx, bytes) {
|
|
|
573
573
|
.sort((a, b) => b - a);
|
|
574
574
|
for (let i = 0; i < Math.max(prefixes.length, suffixes.length); i++) {
|
|
575
575
|
// Interleaved so neither edge starves the other when the budget runs
|
|
576
|
-
// out — a query can carry a trained form at either end.
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
if (i <
|
|
580
|
-
|
|
576
|
+
// out — a query can carry a trained form at either end. The scan never
|
|
577
|
+
// stops on a miss: the exact route is unbudgeted, so running out of
|
|
578
|
+
// canon budget must not stop it.
|
|
579
|
+
if (i < prefixes.length)
|
|
580
|
+
spend(0, prefixes[i]);
|
|
581
|
+
if (i < suffixes.length) {
|
|
582
|
+
const s = suffixes[i];
|
|
583
|
+
// An edge form can end ONE byte before the edge does: a query often
|
|
584
|
+
// closes with a separator ("…is Timur Bekmambetov.") that belongs
|
|
585
|
+
// BETWEEN forms, and the text canonicalizer passes punctuation through,
|
|
586
|
+
// so the full-edge probe can never match it. Retry the trimmed edge
|
|
587
|
+
// ON THE MISS PATH ONLY — self-verifying (resolve decides, so a wrong
|
|
588
|
+
// trim can never emit), the same ±1 edge-trim discipline the canon-miss
|
|
589
|
+
// fallback above already trusts, and the hit path pays nothing. A form
|
|
590
|
+
// LONGER than `chainReach` at the sentence end (measured: "Timur
|
|
591
|
+
// Bekmambetov", 17 bytes) is recovered only here — the interior pass is
|
|
592
|
+
// capped at `chainReach`.
|
|
593
|
+
if (!spend(s, bytes.length))
|
|
594
|
+
spend(s, bytes.length - 1);
|
|
595
|
+
}
|
|
581
596
|
}
|
|
582
597
|
// INTERIOR pairs within the same `chainReach(W)` span bound the chain
|
|
583
598
|
// trusts — the dead zone the gate above used to leave: a form that neither
|
package/dist/src/mind/types.d.ts
CHANGED
|
@@ -35,6 +35,12 @@ export interface GraphSearchHost {
|
|
|
35
35
|
starts: ReadonlySet<number>;
|
|
36
36
|
};
|
|
37
37
|
chooseNext?(node: number): number | undefined;
|
|
38
|
+
/** The admission predicate — `traverse.ts`'s `leadsSomewhere`, its ONE
|
|
39
|
+
* definition: does this node bear an edge or a halo? Optional, so a bare
|
|
40
|
+
* host (a raw Store and nothing else) still works; when present, the search
|
|
41
|
+
* uses it rather than re-probing the store, which keeps the predicate
|
|
42
|
+
* single-defined AND memoised on the response-scoped struct cache. */
|
|
43
|
+
leadsSomewhere?(id: number): boolean;
|
|
38
44
|
}
|
|
39
45
|
export interface Recognition {
|
|
40
46
|
/** Forms that can lead somewhere — they have an edge or a halo. */
|
package/jsr.json
CHANGED
package/package.json
CHANGED
package/src/mind/graph-search.ts
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
type Rule,
|
|
24
24
|
} from "../derive/src/index.js";
|
|
25
25
|
import type { Store } from "../store.js";
|
|
26
|
-
import { bytesEqual, concat2, concatBytes, latin1 } from "../bytes.js";
|
|
26
|
+
import { bytesEqual, concat2, concatBytes, indexOf, latin1 } from "../bytes.js";
|
|
27
27
|
import type { GraphSearchHost } from "./types.js";
|
|
28
28
|
|
|
29
29
|
import { ALL } from "./types.js";
|
|
@@ -1165,16 +1165,40 @@ export class GraphSearch {
|
|
|
1165
1165
|
// The entity candidates are the forms the fact's own bytes CONTAIN — the
|
|
1166
1166
|
// same recogniser the query went through, so the evidence standard is the
|
|
1167
1167
|
// query's. A byte atom is never a subject; the fact's own node is the span
|
|
1168
|
-
// itself, not an entity inside it.
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1168
|
+
// itself, not an entity inside it. The admission predicate has ONE
|
|
1169
|
+
// definition — `traverse.ts`'s `leadsSomewhere` (edge or halo); the host
|
|
1170
|
+
// LENDS it when it can (Mind does, with the response-scoped struct cache),
|
|
1171
|
+
// and a bare host falls back to the raw-store probe, so the search stays
|
|
1172
|
+
// host-based.
|
|
1173
|
+
const leading = this.host.recogniseSpan(fact.bytes).sites.filter((s) =>
|
|
1174
|
+
s.payload >= 0 && s.payload !== fact.node &&
|
|
1175
|
+
(this.host.leadsSomewhere !== undefined
|
|
1176
|
+
? this.host.leadsSomewhere(s.payload)
|
|
1177
|
+
: this.store.hasNext(s.payload) || this.store.hasHalo(s.payload))
|
|
1178
|
+
);
|
|
1179
|
+
// …then prefer the entity the query did NOT name, and the MAXIMAL one. The
|
|
1180
|
+
// join exists to reach the subject the query never wrote, so:
|
|
1181
|
+
// • a candidate the query already contains is the query's OWN subject, and
|
|
1182
|
+
// following its fact answers about that subject instead of the inferred
|
|
1183
|
+
// one (measured: "Gustaf Molander spouse date of death" gave Gustaf's
|
|
1184
|
+
// date of death, not his spouse's);
|
|
1185
|
+
// • a candidate contained in a longer one is not the entity the fact
|
|
1186
|
+
// introduces ("Timur" must not win over "Timur Bekmambetov").
|
|
1187
|
+
// Byte work over bytes already read, and the pruning REMOVES the
|
|
1188
|
+
// resolve()/nextFirst() probes these candidates would have paid.
|
|
1189
|
+
const candidates = leading
|
|
1190
|
+
.map((s) => ({
|
|
1191
|
+
payload: s.payload,
|
|
1192
|
+
bytes: this.store.bytesPrefix(s.payload, ALL),
|
|
1193
|
+
}))
|
|
1194
|
+
.filter((c) => indexOf(queryBytes, c.bytes, 0) < 0)
|
|
1195
|
+
.filter((c, _i, all) =>
|
|
1196
|
+
!all.some((o) =>
|
|
1197
|
+
o.bytes.length > c.bytes.length && indexOf(o.bytes, c.bytes, 0) >= 0
|
|
1198
|
+
)
|
|
1177
1199
|
);
|
|
1200
|
+
for (const c of candidates) {
|
|
1201
|
+
const key = this.host.resolve(concat2(c.bytes, tail));
|
|
1178
1202
|
if (key === null) continue;
|
|
1179
1203
|
const nx = this.store.nextFirst(key, 1);
|
|
1180
1204
|
if (nx.length === 0) continue;
|
package/src/mind/mind.ts
CHANGED
|
@@ -159,6 +159,7 @@ import {
|
|
|
159
159
|
chooseNext,
|
|
160
160
|
edgeAncestors as edgeAncestorsFn,
|
|
161
161
|
invalidateStructuralCaches,
|
|
162
|
+
leadsSomewhere,
|
|
162
163
|
} from "./traverse.js";
|
|
163
164
|
import { invalidateJunctionCache } from "./junction.js";
|
|
164
165
|
import { follow } from "./match.js";
|
|
@@ -343,6 +344,14 @@ export class Mind implements MindContext {
|
|
|
343
344
|
return resolveImpl(this, bytes);
|
|
344
345
|
}
|
|
345
346
|
|
|
347
|
+
/** Whether a node leads somewhere — the admission predicate, delegating to
|
|
348
|
+
* `traverse.ts`'s ONE definition (edge or halo, with its response-scoped
|
|
349
|
+
* cache). The search holds a bare Store and cannot reach that cache itself,
|
|
350
|
+
* so it asks through this hook; a bare host keeps its raw-store fallback. */
|
|
351
|
+
leadsSomewhere(id: number): boolean {
|
|
352
|
+
return leadsSomewhere(this, id);
|
|
353
|
+
}
|
|
354
|
+
|
|
346
355
|
// recogniseSpan wraps recognise
|
|
347
356
|
recogniseSpan(bytes: Uint8Array): {
|
|
348
357
|
sites: ReadonlyArray<Site>;
|
package/src/mind/recognition.ts
CHANGED
|
@@ -517,7 +517,7 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
|
|
|
517
517
|
start: number,
|
|
518
518
|
end: number,
|
|
519
519
|
canonBudget: boolean,
|
|
520
|
-
):
|
|
520
|
+
): boolean => {
|
|
521
521
|
// Any span at least one river window wide is worth a probe. This used
|
|
522
522
|
// to stop at `chainReach(W)` — "the chain already covers anything that
|
|
523
523
|
// short" — and that premise does not hold for every embedded form: the
|
|
@@ -531,14 +531,17 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
|
|
|
531
531
|
// starts on a fold cut nor ends on a node edge was unreachable by either
|
|
532
532
|
// tier — the exact site whose loss `tryChain`'s own note records as "the
|
|
533
533
|
// pivot dies with the site and multi-hop goes silent". The interior
|
|
534
|
-
// pass below spends the same budget on those pairs.
|
|
535
|
-
|
|
534
|
+
// pass below spends the same budget on those pairs. Returns whether it
|
|
535
|
+
// emitted, so a caller can retry a trimmed edge on the miss path only.
|
|
536
|
+
if (end - start < W) return false;
|
|
536
537
|
if (flatProbe(start, end) === null) {
|
|
537
|
-
if (!canonBudget) return;
|
|
538
|
-
if (!canonAdmits(start, end)) return;
|
|
538
|
+
if (!canonBudget) return false;
|
|
539
|
+
if (!canonAdmits(start, end)) return false;
|
|
539
540
|
}
|
|
540
541
|
const id = resolveSpan(start, end);
|
|
541
|
-
if (id
|
|
542
|
+
if (id === null) return false;
|
|
543
|
+
emit(start, end, id);
|
|
544
|
+
return true;
|
|
542
545
|
};
|
|
543
546
|
// A CUMULATIVE BYTE BUDGET, SPENT SHORTEST-SPAN-FIRST.
|
|
544
547
|
//
|
|
@@ -574,10 +577,7 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
|
|
|
574
577
|
const span = end - start;
|
|
575
578
|
const afford = span <= budget;
|
|
576
579
|
if (afford) budget -= span;
|
|
577
|
-
probe(start, end, afford);
|
|
578
|
-
// Always keep walking: the exact route is unbudgeted, so running out
|
|
579
|
-
// of canon budget must not stop the scan.
|
|
580
|
-
return true;
|
|
580
|
+
return probe(start, end, afford);
|
|
581
581
|
};
|
|
582
582
|
const prefixes = ordered.filter((e) => e > 0).sort((a, b) => a - b);
|
|
583
583
|
const suffixes = ordered
|
|
@@ -585,9 +585,24 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
|
|
|
585
585
|
.sort((a, b) => b - a);
|
|
586
586
|
for (let i = 0; i < Math.max(prefixes.length, suffixes.length); i++) {
|
|
587
587
|
// Interleaved so neither edge starves the other when the budget runs
|
|
588
|
-
// out — a query can carry a trained form at either end.
|
|
589
|
-
|
|
590
|
-
|
|
588
|
+
// out — a query can carry a trained form at either end. The scan never
|
|
589
|
+
// stops on a miss: the exact route is unbudgeted, so running out of
|
|
590
|
+
// canon budget must not stop it.
|
|
591
|
+
if (i < prefixes.length) spend(0, prefixes[i]);
|
|
592
|
+
if (i < suffixes.length) {
|
|
593
|
+
const s = suffixes[i];
|
|
594
|
+
// An edge form can end ONE byte before the edge does: a query often
|
|
595
|
+
// closes with a separator ("…is Timur Bekmambetov.") that belongs
|
|
596
|
+
// BETWEEN forms, and the text canonicalizer passes punctuation through,
|
|
597
|
+
// so the full-edge probe can never match it. Retry the trimmed edge
|
|
598
|
+
// ON THE MISS PATH ONLY — self-verifying (resolve decides, so a wrong
|
|
599
|
+
// trim can never emit), the same ±1 edge-trim discipline the canon-miss
|
|
600
|
+
// fallback above already trusts, and the hit path pays nothing. A form
|
|
601
|
+
// LONGER than `chainReach` at the sentence end (measured: "Timur
|
|
602
|
+
// Bekmambetov", 17 bytes) is recovered only here — the interior pass is
|
|
603
|
+
// capped at `chainReach`.
|
|
604
|
+
if (!spend(s, bytes.length)) spend(s, bytes.length - 1);
|
|
605
|
+
}
|
|
591
606
|
}
|
|
592
607
|
// INTERIOR pairs within the same `chainReach(W)` span bound the chain
|
|
593
608
|
// trusts — the dead zone the gate above used to leave: a form that neither
|
package/src/mind/types.ts
CHANGED
|
@@ -65,6 +65,12 @@ export interface GraphSearchHost {
|
|
|
65
65
|
starts: ReadonlySet<number>;
|
|
66
66
|
};
|
|
67
67
|
chooseNext?(node: number): number | undefined;
|
|
68
|
+
/** The admission predicate — `traverse.ts`'s `leadsSomewhere`, its ONE
|
|
69
|
+
* definition: does this node bear an edge or a halo? Optional, so a bare
|
|
70
|
+
* host (a raw Store and nothing else) still works; when present, the search
|
|
71
|
+
* uses it rather than re-probing the store, which keeps the predicate
|
|
72
|
+
* single-defined AND memoised on the response-scoped struct cache. */
|
|
73
|
+
leadsSomewhere?(id: number): boolean;
|
|
68
74
|
}
|
|
69
75
|
|
|
70
76
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -113,3 +113,36 @@ test("recognise(): an INTERIOR form at a non-cut offset is recovered, not only a
|
|
|
113
113
|
assert.deepEqual([hit.start, hit.end], [23, 38]);
|
|
114
114
|
await m.store.close();
|
|
115
115
|
});
|
|
116
|
+
|
|
117
|
+
test("recognise(): a form LONGER than chainReach is recovered past a trailing separator", async () => {
|
|
118
|
+
// A 17-byte object (17 > chainReach(W) = 16) at the end of a sentence is
|
|
119
|
+
// outside the interior pass's span bound, and it is not a suffix of the span
|
|
120
|
+
// because a sentence-final separator (".") follows it — and the text
|
|
121
|
+
// canonicalizer passes punctuation through, so the full-edge probe can never
|
|
122
|
+
// match. MEASURED on the pre-change tree: no site for the object (only the
|
|
123
|
+
// shorter sub-form "Timur" surfaced); without the period the object WAS a
|
|
124
|
+
// suffix and was found. The edge tier now retries the trimmed edge on the
|
|
125
|
+
// MISS path only, so the hit path pays nothing.
|
|
126
|
+
const m = new Mind({ seed: 7, store: new SQliteStore({ path: ":memory:" }) });
|
|
127
|
+
await m.ingest([
|
|
128
|
+
["xavier director", "The director of xavier is averyverylongname."],
|
|
129
|
+
["averyverylongname", "The spouse of averyverylongname is zoe."],
|
|
130
|
+
]);
|
|
131
|
+
const expected = resolve(m, enc("averyverylongname"));
|
|
132
|
+
assert.ok(
|
|
133
|
+
expected !== null,
|
|
134
|
+
"sanity: the object must resolve standalone",
|
|
135
|
+
);
|
|
136
|
+
assert.ok(
|
|
137
|
+
"averyverylongname".length > 16,
|
|
138
|
+
"sanity: the object must be longer than chainReach(W)=16",
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const rec = recognise(m, enc("The director of xavier is averyverylongname."));
|
|
142
|
+
assert.ok(
|
|
143
|
+
rec.sites.some((s) => s.payload === expected),
|
|
144
|
+
`expected a site for the >chainReach object past the trailing separator, got: ` +
|
|
145
|
+
JSON.stringify(rec.sites.map((s) => [s.start, s.end])),
|
|
146
|
+
);
|
|
147
|
+
await m.store.close();
|
|
148
|
+
});
|
|
@@ -97,3 +97,41 @@ test("naming the intermediate does not need derive-through — the cover reads i
|
|
|
97
97
|
);
|
|
98
98
|
await store.close();
|
|
99
99
|
});
|
|
100
|
+
|
|
101
|
+
/** The TRAP fixture: the query's OWN subject carries the asked relation too, so
|
|
102
|
+
* a join that follows the subject answers about the subject. */
|
|
103
|
+
const TRAP_CHAIN = [
|
|
104
|
+
["Eiffel Tower country", "The country of Eiffel Tower is France."],
|
|
105
|
+
["France capital", "The capital of France is Paris."],
|
|
106
|
+
["France", "The capital of France is Paris."],
|
|
107
|
+
["Eiffel Tower capital", "The capital of Eiffel Tower is Berlin."],
|
|
108
|
+
["Eiffel Tower", "The country of Eiffel Tower is France."],
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
async function trappedFlip() {
|
|
112
|
+
const store = new SQliteStore({ path: ":memory:", D: 1024 });
|
|
113
|
+
const mind = new Mind({ seed: 7, store });
|
|
114
|
+
await mind.ingest(TRAP_CHAIN);
|
|
115
|
+
await mind.ingest(Array.from({ length: 4300 }, (_, i) => filler(i)));
|
|
116
|
+
return { store, mind };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
test("the join answers about the entity the query did NOT name, not the query's own subject", async () => {
|
|
120
|
+
// MEASURED on the pre-change tree: the answer was "The capital of Eiffel
|
|
121
|
+
// Tower country is Berlin." — the join followed the query's OWN subject
|
|
122
|
+
// (Eiffel Tower), which carries a "capital" fact of its own, instead of the
|
|
123
|
+
// entity the produced fact introduces (France). Both candidates are contexts
|
|
124
|
+
// joined by the same relation, so only the preference decides.
|
|
125
|
+
const { store, mind } = await trappedFlip();
|
|
126
|
+
const moves = [];
|
|
127
|
+
const out = await mind.respondText(
|
|
128
|
+
"Eiffel Tower country capital",
|
|
129
|
+
(s) => moves.push(s.mechanism[s.mechanism.length - 1]),
|
|
130
|
+
);
|
|
131
|
+
assert.equal(out.trim(), "The capital of France is Paris.");
|
|
132
|
+
assert.ok(
|
|
133
|
+
!out.includes("Eiffel Tower"),
|
|
134
|
+
`the answer must be about the inferred entity, got ${JSON.stringify(out)}`,
|
|
135
|
+
);
|
|
136
|
+
await store.close();
|
|
137
|
+
});
|