@hviana/sema 0.9.2 → 0.9.4

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.
Files changed (41) hide show
  1. package/dist/src/meter.d.ts +6 -0
  2. package/dist/src/meter.js +6 -0
  3. package/dist/src/mind/evidence.d.ts +6 -0
  4. package/dist/src/mind/evidence.js +19 -7
  5. package/dist/src/mind/mechanisms/cast.d.ts +1 -1
  6. package/dist/src/mind/mechanisms/cast.js +73 -12
  7. package/dist/src/mind/mechanisms/cover.js +4 -20
  8. package/dist/src/mind/mechanisms/recall.js +84 -8
  9. package/dist/src/mind/mechanisms/reference.js +25 -0
  10. package/dist/src/mind/mind.d.ts +1 -0
  11. package/dist/src/mind/pipeline-mechanism.js +15 -1
  12. package/dist/src/mind/pipeline.js +5 -4
  13. package/dist/src/mind/reasoning.js +28 -16
  14. package/dist/src/mind/resonance.d.ts +3 -2
  15. package/dist/src/mind/resonance.js +3 -2
  16. package/dist/src/mind/traverse.d.ts +52 -0
  17. package/dist/src/mind/traverse.js +334 -7
  18. package/dist/src/mind/types.d.ts +4 -1
  19. package/docs/INDEX.md +21 -17
  20. package/docs/INVARIANTS.md +1 -1
  21. package/docs/PHILOSOPHY.md +317 -0
  22. package/docs/architecture/evidence.md +172 -29
  23. package/jsr.json +1 -1
  24. package/package.json +1 -1
  25. package/src/meter.ts +6 -0
  26. package/src/mind/evidence.ts +22 -4
  27. package/src/mind/mechanisms/cast.ts +77 -3
  28. package/src/mind/mechanisms/cover.ts +6 -22
  29. package/src/mind/mechanisms/recall.ts +107 -9
  30. package/src/mind/mechanisms/reference.ts +24 -1
  31. package/src/mind/mind.ts +3 -1
  32. package/src/mind/pipeline-mechanism.ts +15 -1
  33. package/src/mind/pipeline.ts +5 -4
  34. package/src/mind/reasoning.ts +33 -15
  35. package/src/mind/resonance.ts +3 -2
  36. package/src/mind/traverse.ts +338 -6
  37. package/src/mind/types.ts +6 -2
  38. package/test/154-the-question-names-the-step.test.mjs +88 -1
  39. package/test/155-the-relation-read-off-another-instance.test.mjs +376 -0
  40. package/test/29-counterfactual.test.mjs +21 -11
  41. package/test/76-reference-binding.test.mjs +32 -0
@@ -8,7 +8,7 @@
8
8
 
9
9
  import { cosine, Vec } from "../vec.js";
10
10
  import type { AncestorReach, MindContext, SaturationStop } from "./types.js";
11
- import { gistOf, read } from "./primitives.js";
11
+ import { gistOf, read, resolve } from "./primitives.js";
12
12
  import {
13
13
  canonicalWindows,
14
14
  chainReach,
@@ -21,7 +21,7 @@ import {
21
21
  // file's line order never decides load order. The note described an intention
22
22
  // the runtime does not honour; the imports move and the claim goes.
23
23
  import { decodeText } from "./rationale.js";
24
- import { latin1 } from "../bytes.js";
24
+ import { concatBytes, indexOf, latin1 } from "../bytes.js";
25
25
  import { type WindowIndex, windowIndex, witness } from "./evidence.js";
26
26
  import type { RationaleItem } from "./rationale.js";
27
27
 
@@ -693,7 +693,9 @@ export function guidedNext(
693
693
  // policy climbMemo and recogniseMemo follow (every mechanism must emit its
694
694
  // own steps; a memo hit would swallow the repeat's `disambiguate` step).
695
695
  // Consistency does not need the memo: chooseNext is a pure function of the
696
- // (read-only) store and the guide, so recomputation yields the same pick.
696
+ // (read-only) store, the guide and the consensus climb's points once they
697
+ // exist (the memo is cleared when they arrive), so recomputation yields the
698
+ // same pick.
697
699
  if (!ctx.trace) {
698
700
  const memo = ctx._edgeChoice.get(node);
699
701
  if (memo !== undefined) return memo === -1 ? undefined : memo;
@@ -839,6 +841,31 @@ export function offsetCanon(ctx: MindContext, bytes: Uint8Array): Uint8Array {
839
841
  return c.length === bytes.length ? c : bytes;
840
842
  }
841
843
 
844
+ /** A FRAGMENT ANSWERS OTHER QUESTIONS. A recognised form that sits inside
845
+ * other forms (it has structural parents or containers) holds continuations
846
+ * because the forms it is a piece of were asked — suffix inheritance gives
847
+ * `f death` the answer of every `… place of death` question, and `director`
848
+ * the answer of every `… director` one. Choosing one of several such
849
+ * continuations by popularity voices some other question's answer. A
850
+ * fragment with several continuations leads somewhere FOR THIS QUESTION only
851
+ * when the question names one (the exact tier below). A form that is
852
+ * (nearly) the whole question is not a piece of it: the question says
853
+ * nothing beyond it, so its continuations answer THIS question. Read by
854
+ * every mechanism that projects through a recognised site (cover's sites,
855
+ * recall's argument binding). */
856
+ export function answersOtherQuestions(
857
+ ctx: MindContext,
858
+ id: number,
859
+ queryLen: number,
860
+ siteLen: number,
861
+ ): boolean {
862
+ const asked = ctx._edgeAsked;
863
+ if (asked === null || queryLen - siteLen < ctx.space.maxGroup) return false;
864
+ if (!(ctx.store.hasParents(id) || ctx.store.hasContainers(id))) return false;
865
+ if (ctx.store.nextFirst(id, 2).length < 2) return false;
866
+ return namedContinuations(ctx, id, asked) === null;
867
+ }
868
+
842
869
  /** The continuations of `id` that `asked` NAMES (see {@link
843
870
  * askedContinuations}) — for a caller holding material other than the whole
844
871
  * question: the multi-hop walk asks with what of the question no product has
@@ -914,7 +941,11 @@ function askedEntry(
914
941
  const hit = memo.get(id);
915
942
  if (hit !== undefined && !ctx.trace) return hit;
916
943
  const entry = askedContinuationsImpl(ctx, id, nx, asked);
917
- memo.set(id, entry);
944
+ // Nothing named before the climb has run is provisional: a co-instance
945
+ // read from its points may still name one.
946
+ if (entry.named !== null || ctx._edgeAsked?.points !== undefined) {
947
+ memo.set(id, entry);
948
+ }
918
949
  return entry;
919
950
  }
920
951
 
@@ -963,7 +994,8 @@ function askedContinuationsImpl(
963
994
  // reply established by hundreds of contexts would otherwise spend the whole
964
995
  // allowance alone. The order changes what is READ, never what wins: scores
965
996
  // are compared afterwards in the continuations' own order.
966
- let budget = Math.max(hubBound(ctx), chainReach(W));
997
+ const allowance = Math.max(hubBound(ctx), chainReach(W));
998
+ let budget = allowance;
967
999
  const order = nx
968
1000
  .map((n, at) => ({ n, at, support: cachedPrevCount(ctx, n, cache) }))
969
1001
  .filter((c) => c.support >= 2) // only `id` establishes the rest
@@ -995,6 +1027,16 @@ function askedContinuationsImpl(
995
1027
  const head = offsetCanon(ctx, read(ctx, c, W));
996
1028
  if (head.length < W) continue;
997
1029
  if (!indexes.some((ix) => ix.has(latin1(head)))) continue;
1030
+ // AN ESTABLISHING CONTEXT IS A DEPOSITED ONE. A span interned inside
1031
+ // bigger forms inherits their edges (the fragment ` born?` leads to the
1032
+ // fact of every `Where was … born?` question), so witnessing it names
1033
+ // every one of them — measured on the 2Wiki fixture with one-hop
1034
+ // questions deposited, ` born?` named nine strangers' birthplaces. The
1035
+ // structural predicate pivotInto already reads: a form with parents or
1036
+ // containers was never a context on its own.
1037
+ if (
1038
+ cachedHasParents(ctx, c, cache) || ctx.store.hasContainers(c)
1039
+ ) continue;
998
1040
  const form = read(ctx, c, formCap + 1);
999
1041
  if (form.length < W || form.length > formCap) continue;
1000
1042
  const w = witness(offsetCanon(ctx, form), indexes, W);
@@ -1018,7 +1060,9 @@ function askedContinuationsImpl(
1018
1060
  witnessed = by;
1019
1061
  } else if (score === bestBytes) best.push(n);
1020
1062
  }
1021
- if (best.length === 0) return none;
1063
+ if (best.length === 0) {
1064
+ return byCoInstance(ctx, id, nx, asked, formCap, allowance);
1065
+ }
1022
1066
  if (ctx.meter) ctx.meter.askedContinuations++;
1023
1067
  if (ctx.trace && witnessed !== null) {
1024
1068
  ctx.trace.step(
@@ -1035,6 +1079,294 @@ function askedContinuationsImpl(
1035
1079
  return { named: best, spans: evidence };
1036
1080
  }
1037
1081
 
1082
+ /** The climb's points the question HOLDS under the response's equivalence —
1083
+ * stored forms whose canonical bytes occur in the question's, as sites. The
1084
+ * recognition walk matches bytes and the fold's own boundaries, so a stored
1085
+ * `Man at Bath` inside `… of film Man At Bath work at?` is no site; the
1086
+ * consensus climb reaches it through the windows the two spellings share, and
1087
+ * containment under the canon (offset-preserving, so the span is the
1088
+ * question's own) decides. Approximate proposes, exact decides. Read off
1089
+ * the `chainReach(W)` most corroborated points; empty before the climb. */
1090
+ export function canonHeldPoints(
1091
+ ctx: MindContext,
1092
+ minLen: number,
1093
+ ): Array<{ start: number; end: number; payload: number }> {
1094
+ const asked = ctx._edgeAsked;
1095
+ if (asked === null || asked.points === undefined || ctx.canon === null) {
1096
+ return [];
1097
+ }
1098
+ const W = ctx.space.maxGroup;
1099
+ const out: Array<{ start: number; end: number; payload: number }> = [];
1100
+ for (const id of asked.points.slice(0, chainReach(W))) {
1101
+ const raw = read(ctx, id, asked.bytes.length);
1102
+ if (raw.length < minLen || raw.length >= asked.bytes.length) continue;
1103
+ const form = ctx.canon(raw);
1104
+ if (form.length !== raw.length) continue;
1105
+ const at = indexOf(asked.bytes, form, 0);
1106
+ if (at >= 0) out.push({ start: at, end: at + raw.length, payload: id });
1107
+ }
1108
+ return out;
1109
+ }
1110
+
1111
+ /** A CO-INSTANCE of the question: a stored form that shares the question's
1112
+ * FRAME — the same opening and the same close, byte for byte under the
1113
+ * response's equivalence — around ONE different filler. `Where was Peter
1114
+ * Jackson born?` against `Where was the director of film Beat Girl born?`
1115
+ * shares `Where was ` and ` born?`, and leaves `Peter Jackson` against `the
1116
+ * director of film Beat Girl`: another instance of the same question, never
1117
+ * this one. It is the ordered limit of witnessing: a frame is not a bag of
1118
+ * windows (an order-free reading lets `Lyon is a city in France` pass for an
1119
+ * instance of `what if the capital of France were Lyon?`, which NAMES Lyon;
1120
+ * and a coincidental window inside a filler splits it).
1121
+ *
1122
+ * The frame must reach one window, every window inside it must be held by
1123
+ * the material (so on the walk, a frame a product already said cannot name a
1124
+ * second step), and the form's filler must be a thing the corpus knows
1125
+ * (below) — a record that differs from the question only inside the frame
1126
+ * by a description (`converts sunlight into chemical energy` against
1127
+ * `works`) is the same question about the frame's own subject. (Requiring
1128
+ * the two fillers to share no window as well was measured redundant with
1129
+ * this and cost two answers on the 2Wiki fixture.) Returns the form's filler span in `raw` and the
1130
+ * question spans of the frame, or null. One reading for every consumer: the
1131
+ * exact tier transfers the relation through it, and the readers that would
1132
+ * VOICE such a form's own continuation refuse it. */
1133
+ export function coInstanceFiller(
1134
+ ctx: MindContext,
1135
+ raw: Uint8Array,
1136
+ indexes: ReadonlyArray<WindowIndex>,
1137
+ question: Uint8Array,
1138
+ ): { span: [number, number]; spans: Array<[number, number]> } | null {
1139
+ const W = ctx.space.maxGroup;
1140
+ const form = offsetCanon(ctx, raw);
1141
+ const max = Math.min(form.length, question.length);
1142
+ let a = 0;
1143
+ while (a < max && form[a] === question[a]) a++;
1144
+ let b = 0;
1145
+ while (
1146
+ b < max - a &&
1147
+ form[form.length - 1 - b] === question[question.length - 1 - b]
1148
+ ) b++;
1149
+ const fe = form.length - b;
1150
+ if (a + b < W || fe <= a || question.length - b <= a) return null;
1151
+ // Scaffolding is nobody's evidence, so a frame window that is a hub need not
1152
+ // be unsaid (` is ` in `Which country Leo Mittler is from?`, which the first
1153
+ // hop's `… is Arshad Khan.` already said).
1154
+ const hub = hubWindows(ctx, raw);
1155
+ const held = (o: number): boolean => {
1156
+ if (hub[o]) return true;
1157
+ const key = latin1(form.subarray(o, o + W));
1158
+ return indexes.some((ix) => ix.has(key));
1159
+ };
1160
+ for (let o = 0; o + W <= a; o++) if (!held(o)) return null;
1161
+ for (let o = fe; o + W <= form.length; o++) if (!held(o)) return null;
1162
+ // THE FILLER IS A THING THE CORPUS KNOWS: a stored context with
1163
+ // continuations of its own, opening where the slot opens (up to one window
1164
+ // earlier — a filler byte can sit in a window the question holds by chance:
1165
+ // the `T` of `Taika` inside `as t` of `was the`). `Peter Jackson` is one;
1166
+ // `converts sunlight into chemical energy`, the slot of `Explain how
1167
+ // photosynthesis …`, is a description of the frame's own subject, and the
1168
+ // record answers the question. The longest such context is the filler.
1169
+ // One content-addressed probe per byte (the `keyEnds` reading); only a run
1170
+ // some deposit spelled whole is looked up.
1171
+ const ids = leafIdPrefix(ctx, raw);
1172
+ let best: [number, number] | null = null;
1173
+ const cache = getStructCache(ctx);
1174
+ const reach = Math.min(ids.length, fe + W - 1);
1175
+ for (let st = Math.max(0, a - W + 1); st <= a; st++) {
1176
+ const run: number[] = [];
1177
+ for (let en = st + 1; en <= reach; en++) {
1178
+ run.push(ids[en - 1]);
1179
+ if (en - st < W) continue;
1180
+ // The flat branch over the whole run exists only where some deposit
1181
+ // spelled exactly these bytes — the cheap filter; the context it names
1182
+ // is then looked up by content.
1183
+ if (ctx.store.findBranch(run) === null) continue;
1184
+ if (best !== null && en - st <= best[1] - best[0]) continue;
1185
+ const id = resolve(ctx, raw.subarray(st, en));
1186
+ if (id !== null && cachedHasNext(ctx, id, cache)) best = [st, en];
1187
+ }
1188
+ }
1189
+ if (best === null) return null;
1190
+ const spans: Array<[number, number]> = [];
1191
+ if (a > 0) spans.push([0, a]);
1192
+ if (b > 0) spans.push([question.length - b, question.length]);
1193
+ return { span: best, spans };
1194
+ }
1195
+
1196
+ /** THE RELATION, READ OFF ANOTHER INSTANCE — the exact tier's second reading,
1197
+ * asked only when no establishing context of `id`'s continuations is
1198
+ * witnessed outright.
1199
+ *
1200
+ * `When was the director of film Jinpa born?` reaches `Pema Tseden`, whose
1201
+ * date-of-birth fact is established by `Pema Tseden date of birth`: the
1202
+ * question says `born`, the corpus says `date of birth`, and no window joins
1203
+ * them. What can join them is ANOTHER INSTANCE of the same question. A
1204
+ * stored context the question witnesses in every byte but ONE contiguous span
1205
+ * (`When was Peter Jackson born?`, residue `Peter Jackson`) is the question's
1206
+ * own frame around a different filler — a CO-INSTANCE. Its continuation is
1207
+ * established by other contexts too (`Peter Jackson date of birth`), and one
1208
+ * that holds the filler is the relation spelled the way the corpus spells it.
1209
+ * Put this node where the filler was and look the result up by content:
1210
+ * `Pema Tseden date of birth` exists, and its continuation is named.
1211
+ *
1212
+ * Nothing here is stored as a unit — not `born` ≈ `date of birth`, not the
1213
+ * frame — and nothing is approximate: the co-instance is witnessed, the
1214
+ * substitution is bytes, the target is an exact lookup that either exists
1215
+ * with this node's continuation or does not. A frame the question shares only
1216
+ * partly (`Where was … born?` against `When was …`) leaves two residues and
1217
+ * is no co-instance.
1218
+ *
1219
+ * BOUNDED: the frames are a property of the question and are read once per
1220
+ * question (`relationFrames`); co-instances are proposed by the edge-bearing
1221
+ * ancestors of its windows (`edgeAncestors`, the climb's own memoised reach;
1222
+ * a saturated window proposes nothing), rarest first, at most `allowance` of
1223
+ * them, each read once. A node pays one exact lookup per frame. */
1224
+ function byCoInstance(
1225
+ ctx: MindContext,
1226
+ id: number,
1227
+ nx: readonly number[],
1228
+ asked: { bytes: Uint8Array; index: WindowIndex },
1229
+ formCap: number,
1230
+ allowance: number,
1231
+ ): AskedEntry {
1232
+ const none: AskedEntry = { named: null, spans: new Map() };
1233
+ const frames = relationFrames(ctx, asked, allowance);
1234
+ if (frames.length === 0) return none;
1235
+ const node = read(ctx, id, formCap);
1236
+ const support = new Map<number, Set<number>>();
1237
+ const spans = new Map<number, Array<[number, number]>>();
1238
+ const via = new Map<number, { q: number; t: number }>();
1239
+ for (const fr of frames) {
1240
+ const t = resolve(ctx, concatBytes([fr.prefix, node, fr.suffix]));
1241
+ if (t === null || t === id) continue;
1242
+ for (const n of ctx.store.nextFirst(t, allowance)) {
1243
+ if (!nx.includes(n)) continue;
1244
+ let by = support.get(n);
1245
+ if (by === undefined) support.set(n, by = new Set());
1246
+ for (const q of fr.by) by.add(q);
1247
+ if (!spans.has(n)) {
1248
+ spans.set(n, fr.spans);
1249
+ via.set(n, { q: fr.by[0], t });
1250
+ }
1251
+ }
1252
+ }
1253
+ let best: number[] = [];
1254
+ let most = 0;
1255
+ for (const n of nx) {
1256
+ const k = support.get(n)?.size ?? 0;
1257
+ if (k === 0) continue;
1258
+ if (k > most) {
1259
+ best = [n];
1260
+ most = k;
1261
+ } else if (k === most) best.push(n);
1262
+ }
1263
+ if (best.length === 0) return none;
1264
+ if (ctx.meter) ctx.meter.coInstanceNamings++;
1265
+ const first = via.get(best[0]);
1266
+ if (ctx.trace && first !== undefined) {
1267
+ ctx.trace.step(
1268
+ "askedByCoInstance",
1269
+ [
1270
+ rItemShort(ctx, id, "node"),
1271
+ rItemShort(ctx, first.q, "co-instance"),
1272
+ rItemShort(ctx, first.t, "asked"),
1273
+ ],
1274
+ best.map((n) => rItemShort(ctx, n, "named")),
1275
+ `${nx.length} continuations — no establishing context is witnessed; ` +
1276
+ `${most} co-instance(s) of the question carry the relation to ` +
1277
+ `${best.length === 1 ? "one" : best.length} of them`,
1278
+ );
1279
+ }
1280
+ const evidence = new Map<number, Array<[number, number]>>();
1281
+ for (const n of best) evidence.set(n, spans.get(n) ?? []);
1282
+ return { named: best, spans: evidence };
1283
+ }
1284
+
1285
+ /** One RELATION FRAME read off a co-instance: an establishing context of the
1286
+ * co-instance's continuation with the filler cut out (`Peter Jackson place
1287
+ * of birth` → `` + · + ` place of birth`). Any node put in the gap spells
1288
+ * that node's context for the same relation. */
1289
+ interface RelationFrame {
1290
+ /** The co-instances that spell the relation this way (first one traced). */
1291
+ by: number[];
1292
+ prefix: Uint8Array;
1293
+ suffix: Uint8Array;
1294
+ spans: Array<[number, number]>;
1295
+ }
1296
+
1297
+ /** The relation frames the question's co-instances carry — a property of the
1298
+ * QUESTION, not of the node asked about, so it is read once per question
1299
+ * (and once per walk material) and every node only pays the exact lookups.
1300
+ * A question the corpus holds verbatim is its own instance and reads none. */
1301
+ function relationFrames(
1302
+ ctx: MindContext,
1303
+ asked: { bytes: Uint8Array; index: WindowIndex },
1304
+ allowance: number,
1305
+ ): RelationFrame[] {
1306
+ // A co-instance is the question's frame around ONE other filler, so it is
1307
+ // read at most to twice the question's length.
1308
+ const formCap = 2 * asked.bytes.length;
1309
+ const hit = frameMemo.get(asked);
1310
+ if (hit !== undefined && !ctx.trace) return hit;
1311
+ const whole = ctx._edgeAsked;
1312
+ // THE PROPOSALS ARE THE CLIMB'S. The consensus climb already scored the
1313
+ // stored forms the question's regions reach — measured on the 2Wiki
1314
+ // fixture with one-hop questions, the co-instances were among its points —
1315
+ // so the tier reads co-instances from those and climbs nothing of its own.
1316
+ // Before the climb has run there is nothing to read, and nothing is
1317
+ // remembered: a later ask, after the climb, reads its points.
1318
+ const points = whole?.points;
1319
+ if (points === undefined) return [];
1320
+ const frames: RelationFrame[] = [];
1321
+ frameMemo.set(asked, frames);
1322
+ if (whole !== null && resolve(ctx, whole.bytes) !== null) return frames;
1323
+ const W = ctx.space.maxGroup;
1324
+ const cache = getStructCache(ctx);
1325
+ const deposited = (c: number): boolean =>
1326
+ !cachedHasParents(ctx, c, cache) && !ctx.store.hasContainers(c);
1327
+ const spelled = new Map<string, RelationFrame>();
1328
+ // Most corroborated first (the climb's own ranking), at most
1329
+ // `chainReach(W)` read — the exact tier's own floor.
1330
+ const proposals = points.filter((c) => cachedHasNext(ctx, c, cache))
1331
+ .slice(0, Math.min(allowance, chainReach(W)));
1332
+ for (const q of proposals) {
1333
+ if (!deposited(q)) continue;
1334
+ const raw = read(ctx, q, formCap + 1);
1335
+ if (raw.length > formCap) continue;
1336
+ if (ctx.meter) ctx.meter.coInstanceReads++;
1337
+ const co = coInstanceFiller(ctx, raw, [asked.index], asked.bytes);
1338
+ if (co === null) continue;
1339
+ const [fs, fe] = co.span;
1340
+ const filler = raw.subarray(fs, fe);
1341
+ for (const f of ctx.store.nextFirst(q, W)) {
1342
+ for (const c of ctx.store.prevFirst(f, allowance)) {
1343
+ if (c === q || !deposited(c)) continue;
1344
+ const cb = read(ctx, c, formCap + 1);
1345
+ const at = indexOf(cb, filler, 0);
1346
+ if (at < 0) continue;
1347
+ const prefix = cb.subarray(0, at);
1348
+ const suffix = cb.subarray(at + filler.length);
1349
+ if (prefix.length + suffix.length === 0) continue;
1350
+ const key = latin1(prefix) + "\u0000" + latin1(suffix);
1351
+ const known = spelled.get(key);
1352
+ if (known === undefined) {
1353
+ spelled.set(key, { by: [q], prefix, suffix, spans: co.spans });
1354
+ } else if (!known.by.includes(q)) known.by.push(q);
1355
+ }
1356
+ }
1357
+ }
1358
+ // ONE INSTANCE AGREES WITH NOTHING. A relation is read off the corpus only
1359
+ // where two co-instances spell it the same way — the agreement `reference`
1360
+ // demands of a frame (MIN_INSTANCES), for the same reason: a single
1361
+ // coincidental alignment is not evidence of what the frame means. Measured
1362
+ // on the trained store, long dialogue turns proposed hundreds of one-off
1363
+ // "frames", each an exact lookup for every node asked about.
1364
+ for (const fr of spelled.values()) if (fr.by.length >= 2) frames.push(fr);
1365
+ frames.sort((a, b) => b.by.length - a.by.length);
1366
+ return frames;
1367
+ }
1368
+ const frameMemo = new WeakMap<object, RelationFrame[]>();
1369
+
1038
1370
  /** The perceived gist of a candidate node, through the session gist cache.
1039
1371
  * Re-gisting a candidate is a full river fold of its bytes — the measured
1040
1372
  * recall bottleneck (a hub context offers up to √N continuations, EACH
package/src/mind/types.ts CHANGED
@@ -415,8 +415,12 @@ export interface MindContext extends GraphSearchHost {
415
415
  /** The question currently being answered, as the material `chooseNext`'s
416
416
  * exact tier witnesses a continuation's establishing contexts against —
417
417
  * its bytes (canonical when the response's canon preserves offsets) and
418
- * their window index. Set and cleared with `_edgeGuide`. */
419
- _edgeAsked: { bytes: Uint8Array; index: WindowIndex } | null;
418
+ * their window index. Set and cleared with `_edgeGuide`. `points` are the
419
+ * anchors the response's consensus climb scored, once it has climbed —
420
+ * the proposals the exact tier reads co-instances from. */
421
+ _edgeAsked:
422
+ | { bytes: Uint8Array; index: WindowIndex; points?: readonly number[] }
423
+ | null;
420
424
  _edgeChoice: Map<number, number>;
421
425
  _prevSeen: Set<number> | null;
422
426
  /** Session cache of node-id → perceived gist, for candidate scoring
@@ -31,7 +31,16 @@
31
31
  // that named the first hop cannot name a second;
32
32
  // 154.5 a fragment (`director`, inside every `… director` question) answers
33
33
  // other questions: the cover voices none of its continuations unless the
34
- // question names one.
34
+ // question names one;
35
+ // 154.6 …and neither does recall's argument binding, when the fragment is the
36
+ // query's sole argument;
37
+ // 154.7 a comparison needs the question to name two things: an analog
38
+ // evidenced only by windows the dominant holds, or by scaffolding, is
39
+ // not compared (it used to glue its bare question onto the answer);
40
+ // 154.8 an argument the question holds only under the response's
41
+ // equivalence (`Man At Bath` for the stored `Man at Bath`, inside the
42
+ // question) still binds: the climb proposes it, canonical containment
43
+ // decides.
35
44
 
36
45
  import { test } from "node:test";
37
46
  import assert from "node:assert/strict";
@@ -279,3 +288,81 @@ test("154.5 a fragment answers other questions unless the question names one", a
279
288
  assert.ok(mind.lastCost.counters.unaskedFragments > 0);
280
289
  await mind.store.close();
281
290
  });
291
+
292
+ test("154.6 recall's argument binding does not bind a fragment the question names nothing of", async () => {
293
+ const mind = await fixture();
294
+ // Subjects ending in a parenthesis (2Wiki's own titles) let the fold intern
295
+ // `director` as a node of its own, which inherits every director fact.
296
+ await mind.ingest(deposits([
297
+ ["Vasantha Sena (1967 film)", "director", "B. S. Ranga"],
298
+ ["Guilty of Treason (film)", "director", "Felix E. Feist"],
299
+ ]));
300
+ const steps = [];
301
+ const answer = await mind.respondText(
302
+ "When was the director of film Jinpa born?",
303
+ (st) => {
304
+ if (st.mechanism.includes("recallByResonance")) {
305
+ steps.push({
306
+ name: st.mechanism.at(-1),
307
+ outputs: st.outputs.map((o) => o.text ?? ""),
308
+ });
309
+ }
310
+ },
311
+ );
312
+ // `director` — the only constituent long enough to bind — leads to every
313
+ // director fact, and the binding voiced the most-poured stranger's.
314
+ assert.ok(
315
+ steps.some((s) => s.name === "unaskedFragments"),
316
+ "the binding is refused",
317
+ );
318
+ for (const s of steps.filter((s) => s.name === "recallByResonance")) {
319
+ for (const o of s.outputs) assert.doesNotMatch(o, /Vasantha|Guilty/);
320
+ }
321
+ assert.equal(answer, "The director of Jinpa is Pema Tseden.");
322
+ await mind.store.close();
323
+ });
324
+
325
+ test("154.7 a comparison needs the question to name two things", async () => {
326
+ const mind = await fixture();
327
+ const notes = [];
328
+ const answer = await mind.respondText(
329
+ "Tell me the father of Frederick II.",
330
+ (st) => {
331
+ if (st.mechanism.at(-1) === "validateAnalogy") notes.push(st.note ?? "");
332
+ },
333
+ );
334
+ // `Peter III of Aragon father` is reached through ` father` (the dominant
335
+ // `Frederick II father` holds it) and ` of ` (scaffolding): one structure
336
+ // named, so nothing is compared.
337
+ assert.ok(
338
+ notes.some((n) => /names one structure, not two/.test(n)),
339
+ "the comparison is refused",
340
+ );
341
+ assert.equal(answer, FATHER);
342
+ await mind.store.close();
343
+ });
344
+
345
+ test("154.8 an argument held only under the response's equivalence still binds", async () => {
346
+ const mind = await fixture();
347
+ await mind.ingest(deposits([
348
+ ["Man at Bath", "director", "Christophe Honoré"],
349
+ ["At the End of the Tunnel", "director", "Rodrigo Grande"],
350
+ ]));
351
+ // The trainer builds the canonical-form index after training (progress.ts).
352
+ await mind.buildCanonIndex();
353
+ // 2Wiki title-cases its questions; the title sits inside the question,
354
+ // where no fold boundary of the query lines up with the stored form.
355
+ assert.equal(
356
+ await mind.respondText(
357
+ "Where does the director of film Man At Bath work at?",
358
+ ),
359
+ "The director of Man at Bath is Christophe Honoré.",
360
+ );
361
+ assert.equal(
362
+ await mind.respondText(
363
+ "Where did the director of film At The End Of The Tunnel work?",
364
+ ),
365
+ "The director of At the End of the Tunnel is Rodrigo Grande.",
366
+ );
367
+ await mind.store.close();
368
+ });