@hviana/sema 0.4.7 → 0.5.1

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 (66) hide show
  1. package/AGENTS.md +290 -77
  2. package/HOW_IT_WORKS.md +2170 -735
  3. package/dist/example/train_base.d.ts +9 -3
  4. package/dist/example/train_base.js +21 -4
  5. package/dist/src/canon.d.ts +19 -0
  6. package/dist/src/canon.js +28 -0
  7. package/dist/src/geometry.d.ts +52 -0
  8. package/dist/src/geometry.js +87 -1
  9. package/dist/src/mind/attention.d.ts +15 -10
  10. package/dist/src/mind/attention.js +15 -10
  11. package/dist/src/mind/bridge.js +27 -1
  12. package/dist/src/mind/frame-filler.d.ts +15 -0
  13. package/dist/src/mind/frame-filler.js +535 -0
  14. package/dist/src/mind/learning.js +6 -11
  15. package/dist/src/mind/mechanisms/cast.js +72 -2
  16. package/dist/src/mind/mechanisms/cover.js +6 -1
  17. package/dist/src/mind/mechanisms/extraction.js +27 -0
  18. package/dist/src/mind/mechanisms/recall.js +214 -34
  19. package/dist/src/mind/mind.d.ts +52 -3
  20. package/dist/src/mind/mind.js +140 -12
  21. package/dist/src/mind/pipeline-mechanism.d.ts +7 -0
  22. package/dist/src/mind/pipeline.js +29 -1
  23. package/dist/src/mind/prefix-completion.d.ts +59 -0
  24. package/dist/src/mind/prefix-completion.js +270 -0
  25. package/dist/src/mind/primitives.d.ts +29 -10
  26. package/dist/src/mind/primitives.js +98 -71
  27. package/dist/src/mind/recognition.js +153 -26
  28. package/dist/src/mind/traverse.d.ts +32 -0
  29. package/dist/src/mind/traverse.js +52 -0
  30. package/dist/src/mind/types.d.ts +61 -18
  31. package/dist/src/mind/types.js +68 -19
  32. package/dist/src/store.d.ts +21 -0
  33. package/dist/src/store.js +21 -0
  34. package/example/train_base.ts +21 -4
  35. package/package.json +1 -1
  36. package/src/canon.ts +28 -0
  37. package/src/geometry.ts +100 -1
  38. package/src/mind/attention.ts +15 -10
  39. package/src/mind/bridge.ts +34 -0
  40. package/src/mind/frame-filler.ts +604 -0
  41. package/src/mind/learning.ts +5 -9
  42. package/src/mind/mechanisms/cast.ts +70 -2
  43. package/src/mind/mechanisms/cover.ts +6 -1
  44. package/src/mind/mechanisms/extraction.ts +27 -0
  45. package/src/mind/mechanisms/recall.ts +236 -37
  46. package/src/mind/mind.ts +166 -18
  47. package/src/mind/pipeline-mechanism.ts +7 -0
  48. package/src/mind/pipeline.ts +33 -1
  49. package/src/mind/prefix-completion.ts +314 -0
  50. package/src/mind/primitives.ts +105 -80
  51. package/src/mind/recognition.ts +151 -23
  52. package/src/mind/traverse.ts +52 -0
  53. package/src/mind/types.ts +104 -44
  54. package/src/store.ts +25 -0
  55. package/test/13-conversation.test.mjs +13 -0
  56. package/test/57-fusion-order.test.mjs +65 -0
  57. package/test/66-query-edge-whitespace.test.mjs +99 -0
  58. package/test/67-climb-anchor-breadth.test.mjs +113 -0
  59. package/test/68-extraction-unanchored.test.mjs +79 -0
  60. package/test/69-frame-filler.test.mjs +115 -0
  61. package/test/70-prefix-completion.test.mjs +170 -0
  62. package/test/71-embedded-canon-equivalence.test.mjs +121 -0
  63. package/test/72-prefix-candidate-supply.test.mjs +114 -0
  64. package/test/73-scaffolding-only-bridge-abstains.test.mjs +178 -0
  65. package/test/74-prefix-trap-not-sprung-early.test.mjs +114 -0
  66. package/test/75-multiturn-context-optimisation.test.mjs +1334 -0
@@ -10,16 +10,16 @@
10
10
  // Implementation split across src/mind/*.ts — this file assembles the Mind class.
11
11
  import { makeKeyring, rng, setVecConfig } from "../vec.js";
12
12
  import { Alphabet } from "../alphabet.js";
13
- import { bytesToTree, reachThreshold, } from "../geometry.js";
13
+ import { contentFoldIncremental, reachThreshold, } from "../geometry.js";
14
14
  import { BoundedMap } from "../store.js";
15
15
  import { SQliteStore } from "../store-sqlite.js";
16
16
  import { resolveConfig } from "../config.js";
17
- import { canonHash, textCanon } from "../canon.js";
17
+ import { canonHash, textCanon, textEdgeTrim } from "../canon.js";
18
18
  import { bytesEqual, concat2 } from "../bytes.js";
19
19
  import { GraphSearch, } from "./graph-search.js";
20
20
  import { Alu } from "../alu/src/index.js";
21
21
  import { decodeText, Rationale, } from "./rationale.js";
22
- import { gistOf, inputBytes, latin1Key, perceive as perceiveImpl, resolve as resolveImpl, } from "./primitives.js";
22
+ import { gistOf, inputBytes, perceive as perceiveImpl, perceiveKey, resolve as resolveImpl, } from "./primitives.js";
23
23
  import { chooseNext, edgeAncestors as edgeAncestorsFn, invalidateStructuralCaches, } from "./traverse.js";
24
24
  import { invalidateJunctionCache } from "./junction.js";
25
25
  import { follow } from "./match.js";
@@ -206,8 +206,9 @@ export class Mind {
206
206
  * serves BOTH entry points: `respond` takes fresh per-response memos,
207
207
  * `respondTurn` passes its conversation, whose memos persist across turns
208
208
  * (content-keyed, so the previous turn's results are found by this turn's
209
- * sub-span calls) and whose `resolvedSubtrees` makes foldTree O(suffix)
210
- * instead of O(context). respondTurn used to inline its own copy of this
209
+ * sub-span calls) and whose `resolvedSubtrees` spares foldTree the store
210
+ * probes for every prefix subtree and, for walks that pass no visitor,
211
+ * the descent as well. respondTurn used to inline its own copy of this
211
212
  * and of {@link endResponse}; the two drifted (a memo added to one was
212
213
  * silently absent from the other), so there is exactly one pair now. */
213
214
  beginResponse(inspectRationale, canon, conv) {
@@ -322,12 +323,58 @@ export class Mind {
322
323
  provenance: thought.provenance,
323
324
  };
324
325
  }
326
+ /** Answer ONE self-contained input.
327
+ *
328
+ * A MULTI-TURN context is not that, and this is the wrong entry point for
329
+ * it. `respond` folds the bytes it is handed with no boundary set, because
330
+ * nothing in a flat byte string says where one turn ended — only the caller
331
+ * who assembled it knows, which is the whole reason `boundaries` is a
332
+ * parameter of {@link perceiveImpl} and never inferred from content. A
333
+ * conversation deposited through {@link ingest} folds its contexts over
334
+ * those turn boundaries, so a hand-concatenated transcript passed here
335
+ * folds differently from the way it was learnt and reaches the trained
336
+ * context node only by luck (measured on a 7-turn conversation: 5/7 here
337
+ * against 7/7 through {@link respondTurn}, same bytes). Use
338
+ * {@link beginConversation} + {@link respondTurn}, or {@link addTurn} to
339
+ * replay turns the Mind should hear but not answer. */
325
340
  async respond(input, inspectRationale) {
326
341
  // A STRING input is text by nature: it carries the text equivalence even
327
342
  // through the generic entry point. Raw bytes / grids carry only the
328
343
  // Mind-level canon option, if any.
329
344
  const canon = this._canonFor(typeof input === "string" ? textCanon : null);
330
- return this._respondImpl(inputBytes(this, input), inspectRationale, "respond", canon);
345
+ // EDGE WHITESPACE IS NOT PART OF THE QUESTION — trim it once, here, so
346
+ // every mechanism downstream sees the same question regardless of how the
347
+ // caller spaced it. See canon.ts's textEdgeTrim for why the outer edges of a
348
+ // whole input are exactly where canon.ts's no-trimming hazard cannot arise.
349
+ // Gated on the SAME modality test as the canonicalizer above: for bytes and
350
+ // grids 0x20 is content, and nothing is trimmed.
351
+ //
352
+ // Measured on the 15.7M-node store: without this, one leading space took
353
+ // `Who wrote Romeo and Juliet?` and `What is the chemical symbol for
354
+ // water?` from answered to silent, because a shift re-seats every fold
355
+ // boundary — the whole of analyze_training.ts's K2 phase-robustness gap.
356
+ // The caller's EXACT bytes are tried first and the trim is a RETRY, not a
357
+ // pre-filter. Trimming up front is asymmetric — it normalises the query but
358
+ // not the stored forms — so it breaks byte-exact identity for a form trained
359
+ // WITH edge whitespace: test/04 deposits [" ice ", "cold"] and asks
360
+ // " ice ", which must keep answering. Retrying preserves that (the raw
361
+ // query resolves on the first pass) while still reaching the padded case
362
+ // (the raw query grounds nothing, the trimmed one does).
363
+ //
364
+ // COST: nothing on any answering path. The retry needs BOTH silence AND
365
+ // edge whitespace on the query, the same "only on the already-failed path"
366
+ // discipline test/44 and the bridge's own trim retry use. The conversation
367
+ // entry point (respondTurn) is deliberately NOT trimmed — it tracks
368
+ // turn-boundary offsets into its accumulated context, and shifting the bytes
369
+ // under those offsets would desync them.
370
+ const bytes = inputBytes(this, input);
371
+ const first = await this._respondImpl(bytes, inspectRationale, "respond", canon);
372
+ if (first.bytes.length > 0 || typeof input !== "string")
373
+ return first;
374
+ const trimmed = textEdgeTrim(bytes);
375
+ if (trimmed.length === bytes.length || trimmed.length === 0)
376
+ return first;
377
+ return this._respondImpl(trimmed, inspectRationale, "respond", canon);
331
378
  }
332
379
  /** Text view of {@link respond}. NUL bytes (0x00) are stripped before
333
380
  * decoding — they are structural padding in text answers. LOSSY for a
@@ -354,15 +401,35 @@ export class Mind {
354
401
  beginConversation(state) {
355
402
  const id = this._nextConvId++;
356
403
  const initBytes = state?.context ?? new Uint8Array(0);
357
- const initBoundaries = state?.boundaries ? [...state.boundaries] : [];
404
+ // NORMALISE CALLER-SUPPLIED BOUNDARIES. `boundaries` is documented
405
+ // strictly increasing and every boundary this class produces is (they are
406
+ // appended as the context grows), but a restored {@link ConversationState}
407
+ // comes from OUTSIDE — hand-built, migrated, or round-tripped through a
408
+ // store that did not preserve order. The folds consume boundaries with a
409
+ // sequential `b > prev` filter, so an out-of-order entry is silently
410
+ // DROPPED rather than rejected, and the conversation would then fold over
411
+ // a different cut set than the one the caller believes it restored.
412
+ // `bytesToTree` used to sort on the way in and absorbed this; the
413
+ // incremental fold this now calls does not, so the normalisation belongs
414
+ // here, at the one public door untrusted boundaries come through.
415
+ const initBoundaries = state?.boundaries
416
+ ? [...new Set(state.boundaries)]
417
+ .filter((b) => b > 0 && b < initBytes.length)
418
+ .sort((a, b) => a - b)
419
+ : [];
358
420
  const initAnswered = state?.answeredSpans
359
421
  ? state.answeredSpans.map(([start, end]) => [start, end])
360
422
  : initBoundaries.flatMap((start, i, cuts) => i % 2 === 0 && i + 1 < cuts.length
361
423
  ? [[start, cuts[i + 1]]]
362
424
  : []);
363
- const tree = bytesToTree(this.space, this.alphabet, initBytes, undefined, undefined, initBoundaries.length > 0 ? initBoundaries : undefined);
425
+ // The same incremental fold `_growContext` uses, so a RESTORED
426
+ // conversation starts with segment state its next turn can reuse — a
427
+ // resumed conversation is otherwise identical to a live one and must not
428
+ // pay a full re-fold on every turn for the rest of its life.
429
+ const restored = contentFoldIncremental(this.space, this.alphabet, initBytes);
364
430
  this._conversations.set(id, {
365
- tree,
431
+ tree: restored.tree,
432
+ content: restored.fold,
366
433
  bytes: initBytes,
367
434
  boundaries: initBoundaries,
368
435
  answeredSpans: initAnswered,
@@ -397,7 +464,41 @@ export class Mind {
397
464
  * This is the primitive for turns the Mind should hear but not answer:
398
465
  * replaying a transcript, feeding the OTHER speaker's line in a
399
466
  * prediction harness, or restoring context piecewise. {@link
400
- * respondTurn} = addTurn + think + its own reply appended the same way. */
467
+ * respondTurn} = addTurn + think + its own reply appended the same way.
468
+ *
469
+ * ── ON SEPARATORS: THERE IS NO SEPARATOR QUESTION ────────────────────
470
+ *
471
+ * "Never a separator" above says what this method DOES — it appends the
472
+ * bytes you give it and records an OFFSET — not that separator bytes are
473
+ * forbidden, unsupported, or something the engine must be taught about.
474
+ * Sema is agnostic to them, and reviewers keep mistaking that agnosticism
475
+ * for a constraint. To be explicit, because the mistake is easy:
476
+ *
477
+ * 1. A turn boundary is an OFFSET, held here, in `boundaries`. It is
478
+ * never a character the geometry scans for. Nothing downstream asks
479
+ * "what byte separates two turns?" because nothing downstream finds
480
+ * boundaries by looking at content at all.
481
+ * 2. A separator in a CORPUS is ordinary content. If a trainer joins
482
+ * turns with "\n" (example/train_base.ts does), those newlines are
483
+ * simply bytes inside the stream, folded like every other byte. They
484
+ * are a property of that corpus, not of this API and not of the fold.
485
+ * 3. This API can therefore reproduce ANY corpus exactly, with no
486
+ * convention to agree on: replaying a "\n"-joined corpus means passing
487
+ * `"\n" + turnText` as the turn. The separator rides along IN the
488
+ * turn bytes, where it belongs. There is nothing to configure and no
489
+ * mode to select.
490
+ * 4. Inference is not exact-match anyway. Recognition works over
491
+ * sub-spans, canonical equivalence and resonance, so a query that
492
+ * differs from the trained bytes by punctuation or whitespace still
493
+ * reaches the trained forms; it degrades, it does not fail closed.
494
+ *
495
+ * What follows from 1–4: differing separator bytes between a corpus and a
496
+ * query is an ordinary CONTENT difference — the same kind as any other
497
+ * wording difference — and it is measured the same way. It is NOT an
498
+ * incompatibility between the trainer and this API, and it does NOT
499
+ * require choosing a project-wide separator convention. A review that
500
+ * concludes otherwise (this one did, before being corrected) has mistaken
501
+ * its own harness feeding untrained bytes for an architectural defect. */
401
502
  addTurn(conv, turn) {
402
503
  const data = this._conversations.get(conv.id);
403
504
  if (!data)
@@ -422,10 +523,37 @@ export class Mind {
422
523
  const grown = prevLen > 0 ? concat2(data.bytes, turnBytes) : turnBytes;
423
524
  if (prevLen > 0)
424
525
  data.boundaries.push(prevLen);
425
- const tree = bytesToTree(this.space, this.alphabet, grown, undefined, undefined, data.boundaries.length > 0 ? data.boundaries : undefined);
526
+ // THE PLAIN FOLD, INCREMENTALLY. No boundary set is imposed here: the
527
+ // tree is exactly the tree `perceive(grown)` builds for these bytes, which
528
+ // is exactly the tree the DEPOSIT path folded when it learnt them. That
529
+ // agreement is the whole point — it is what lets a cumulative context
530
+ // resolve to its trained node, and when it was absent the alignment family
531
+ // went quadratic (measured: 5.2M cells on a 476-byte context, against 0
532
+ // when the two sides agree).
533
+ //
534
+ // The optimisation is unaffected by dropping the boundaries, because it
535
+ // never came from them: content cuts are stable under append, so the
536
+ // incremental fold reuses every segment left of the new turn as the SAME
537
+ // object (see contentFoldIncremental). That object identity is what
538
+ // `resolvedSubtrees` — a WeakMap keyed by node identity — needs in order
539
+ // to hit at all. Measured against the stable-prefix fold it replaces:
540
+ // ~40 rebuilt nodes per turn either way, flat as the context grows
541
+ // sevenfold, and ~92% of nodes reused by identity in both.
542
+ //
543
+ // `data.boundaries` is still tracked, and is still exact — it is API
544
+ // metadata (ConversationState, answeredSpans, currentTurnStart), not a
545
+ // fold instruction.
546
+ const folded = contentFoldIncremental(this.space, this.alphabet, grown, data.content);
547
+ const tree = folded.tree;
548
+ data.content = folded.fold;
426
549
  data.tree = tree;
427
550
  data.bytes = grown;
428
- data.perceiveMemo.set(latin1Key(grown), tree);
551
+ // Seeded under the PLAIN content key, and that is now the only key there
552
+ // is: with no boundary set imposed, this tree IS what `perceive(grown)`
553
+ // computes, so the memo entry is an ordinary cache hit rather than the
554
+ // deliberate alias it had to be while the two folds differed. The entry
555
+ // saves the pipeline re-folding the context it was just handed.
556
+ data.perceiveMemo.set(perceiveKey(grown), tree);
429
557
  return tree;
430
558
  }
431
559
  /** Process one turn of a conversation.
@@ -122,6 +122,13 @@ export interface MechanismResult {
122
122
  unexplained: string;
123
123
  /** Explicit weight override. When absent, weight = moves + PASS·unaccounted. */
124
124
  weight?: number;
125
+ /** Bytes of `bytes` that came from spans nothing recognised — the asker's
126
+ * own words carried through verbatim rather than derived (see
127
+ * {@link liftedScaffolding}). Reported, not priced: the ladder prices what
128
+ * a candidate leaves UNACCOUNTED, and this orders candidates that tie on
129
+ * exactly that. Omit when a mechanism composes its answer entirely from
130
+ * recognised material, which is the usual case. */
131
+ scaffolding?: number;
125
132
  /** Override the mechanism's default provenance for this result.
126
133
  * When absent, the pipeline uses `mech.provenance`. */
127
134
  provenance?: string;
@@ -108,7 +108,34 @@ export async function think(ctx, query, mechs) {
108
108
  if (ctx.meter)
109
109
  ctx.meter.candidates++;
110
110
  candidates.push(c);
111
- if (best === null || grade(c.weight) < grade(best.weight))
111
+ if (best === null) {
112
+ best = c;
113
+ return;
114
+ }
115
+ const g = grade(c.weight), gb = grade(best.weight);
116
+ if (g < gb) {
117
+ best = c;
118
+ return;
119
+ }
120
+ // TIE-BREAK: AT EQUAL GRADE, PREFER THE ANSWER THAT INVENTS LESS.
121
+ //
122
+ // The ladder prices what a candidate leaves UNACCOUNTED, which is the
123
+ // right primary question but cannot separate two candidates that leave
124
+ // the same bytes unaccounted — and then the winner is whichever mechanism
125
+ // happened to be considered first, which is not a reason.
126
+ //
127
+ // What still separates them is what they DID with those bytes. A
128
+ // candidate that carries an unexplained span into its answer is passing
129
+ // the asker's own words back as if they were derived; one that leaves
130
+ // them out has made a smaller, honest claim. Measured on test/22's
131
+ // two-fact chain: cover and recall both graded 11001 over 11 unexplained
132
+ // bytes, cover answering "The capital of France is Paris famous for" (11
133
+ // bytes of scaffolding) against recall's crossing of the hop (0). Order
134
+ // alone decided it, and the shallower reading won.
135
+ //
136
+ // This never overrides the ladder — it only orders within one grade, so
137
+ // coverage and moves still dominate exactly as before.
138
+ if (g === gb && (c.scaffolding ?? 0) < (best.scaffolding ?? 0))
112
139
  best = c;
113
140
  };
114
141
  const worthRunning = (floor) => best === null || grade(floor) < grade(best.weight);
@@ -150,6 +177,7 @@ export async function think(ctx, query, mechs) {
150
177
  accounted: r.accounted,
151
178
  unexplained: r.unexplained,
152
179
  complete: r.complete,
180
+ scaffolding: r.scaffolding,
153
181
  });
154
182
  }
155
183
  }
@@ -0,0 +1,59 @@
1
+ import type { MindContext } from "./types.js";
2
+ /** Trained forms the query may OPEN, proposed from the write side's own
3
+ * leaf-id window index — the supply of last resort for {@link
4
+ * prefixCompletion}.
5
+ *
6
+ * WHY A SECOND SUPPLY EXISTS. The ranked list this mechanism normally reads
7
+ * is a resonance list, and resonance cannot rank a proper prefix: measured on
8
+ * the trained store, cos(prefix, form) falls from 0.9629 at a one-byte
9
+ * truncation to 0.6206 at three bytes, against a reachThreshold of 0.8750.
10
+ * Three bytes of truncation put the answer out of reach on GEOMETRY, not on a
11
+ * bug, so no k and no re-ranking recovers it.
12
+ *
13
+ * WHY THIS ROUTE WORKS WHERE THE FOLD DOES NOT. A query's own fold is
14
+ * useless here: content addressing is not phrase-position-invariant, so a
15
+ * standalone prefix folds to a DIFFERENT node than the same bytes sitting
16
+ * inside a longer deposit, and neither the prefix's own node nor its
17
+ * ancestors lead to the deposit (measured: the 22-byte prefix of the
18
+ * photosynthesis form resolves, is shared by 6 contexts, and does not have
19
+ * the form among its ancestors). Leaf ids ARE position-invariant — they are
20
+ * content-addressed on single bytes — and `indexSubSpans` already interns a
21
+ * flat branch over every canonical WINDOW of a deposit's leaf-id stream, with
22
+ * containment edges to the chunks that window spans. A query that is a
23
+ * prefix therefore shares those window nodes exactly, and reaches the deposit
24
+ * by climbing containment then parents. Nothing is added to the write side;
25
+ * this reads an index training already built.
26
+ *
27
+ * BOUNDED (§2.8), AND WITH NO NEW THRESHOLD. The window whose containment is
28
+ * SMALLEST carries the most evidence, and one saturated at `hubBound` carries
29
+ * none — that is the same √N reading of "hub" the rest of the mind uses, not
30
+ * a tuned knob. The upward walk spends a budget of `hubBound` nodes and
31
+ * fans out by W, so a hub query enumerates nothing and the caller stays
32
+ * silent rather than guessing (§2.13). Measured on the trained store: the
33
+ * photosynthesis form at a one-byte truncation picks a window with 52
34
+ * containers, visits 446 nodes, and yields exactly ONE candidate that
35
+ * survives the caller's byte compare — the form itself.
36
+ *
37
+ * These are PROPOSALS only. Every candidate still faces the byte-exact
38
+ * prefix compare and all three guards below, so a wrong proposal costs one
39
+ * bounded read and can never be voiced (§2.3). */
40
+ export declare function prefixCandidates(ctx: MindContext, query: Uint8Array): number[];
41
+ /** A trained form the query opens, and the bytes by which it continues. */
42
+ export interface PrefixCompletion {
43
+ /** The trained form whose opening the query is — the answer, voiced whole. */
44
+ id: number;
45
+ /** The form's own bytes. The mechanism grounds a FORM, never a slice of
46
+ * one: slicing at the query's end would cut at an offset the geometry has
47
+ * no reason to treat as a boundary. */
48
+ form: Uint8Array;
49
+ /** The bytes past the query — carried for the rationale and for the
50
+ * uniqueness comparison, not voiced on its own. */
51
+ continuation: Uint8Array;
52
+ }
53
+ /** The sole trained form the query opens — or null when no candidate opens with
54
+ * it, when the continuation is sub-quantum, when a candidate's continuation
55
+ * cannot be read through, or when the candidates disagree.
56
+ *
57
+ * `ranked` must be a list the caller has ALREADY fetched; this mechanism never
58
+ * resonates on its own (see the header's cost note). */
59
+ export declare function prefixCompletion(ctx: MindContext, query: Uint8Array, ranked: ReadonlyArray<number>): PrefixCompletion | null;
@@ -0,0 +1,270 @@
1
+ // prefix-completion.ts — Grounding a query that IS the opening of a trained
2
+ // form.
3
+ //
4
+ // THE SHAPE. `The capital of France is` grounds nothing, while
5
+ // `The capital of France is Paris.` is trained and reads back byte-exact. The
6
+ // query is not SIMILAR to that form, it is a PROPER PREFIX of it: every query
7
+ // byte is a literal match, in order, from offset zero. That is the strongest
8
+ // grounding relation in the store — stronger than the bridge's corroborated
9
+ // substitution, which pays a CONCEPT per substituted span, and stronger than
10
+ // resonance, which only claims an angle. Nothing is invented: the answer IS a
11
+ // trained form, voiced whole.
12
+ //
13
+ // NO NOTION OF TEXT. This mechanism reads bytes and geometry only. It has no
14
+ // separator, no character class, no "word": the only structural quantity it
15
+ // uses is W, the river's grouping window, which is the same capacity the
16
+ // perception tree groups by and the same bar the argument-binding tier holds
17
+ // its constituents to. A completion shorter than one grouping window carries
18
+ // no structure the geometry can perceive, whatever the modality — that is a
19
+ // statement about the fold, not about punctuation. Presentation (what is
20
+ // "spacing", what is "case") belongs to the injected canon and to the modality
21
+ // entry point, never here; see src/canon.ts.
22
+ //
23
+ // WHY THE EARLIER TIERS CANNOT DO IT. Two independent reasons, both measured:
24
+ //
25
+ // 1. `resolve(prefix)` is null. A proper prefix of a deposited stream has no
26
+ // branch of its own unless it was itself deposited, so the exact tiers
27
+ // have nothing to find.
28
+ // 2. The form is not among the resonance candidates AT ALL. Measured on the
29
+ // trained store: cos(query, that form) = 0.5752, yet the form is absent
30
+ // from `resonate(k)` at k = 24, 256 AND 2048 — while forms scoring LOWER
31
+ // (Germany 0.5670, Yemen 0.5591) are returned. `k` only reorders WITHIN
32
+ // the IVF clusters already probed, exactly as Store.resonate's doc warns,
33
+ // so no k recovers it. With `exhaustive` it ranks 8.
34
+ //
35
+ // So this is a RETRIEVABILITY gap, not a semantic one, and it is repaired by
36
+ // reading the candidate list recall's refusal path has ALREADY fetched
37
+ // exhaustively for the substitution bridge — never by resonating on its own.
38
+ // Measured cost of the scan over those 570 candidates: 2.9 ms warm, 20.4 ms
39
+ // cold, against a ~700 ms refusal path. Issuing a FRESH exhaustive call would
40
+ // cost 490 ms median against 13 ms non-exhaustive (36×), which is why this tier
41
+ // takes the candidate list as an argument and adds nothing to it.
42
+ //
43
+ // THREE GUARDS, each falsified into existence by measurement — do not drop any:
44
+ //
45
+ // 1. AN UNREADABLE CONTINUATION VETOES. Reads are bounded (a stored span can
46
+ // run to hundreds of kilobytes), so a candidate that opens with the query
47
+ // but SATURATES the read continues in a way nobody can see. It is a
48
+ // standing disagreement: if any such candidate exists, nothing is grounded.
49
+ // It must NOT be quietly skipped, and that is not a stylistic point — the
50
+ // skip is what MANUFACTURES a fragment. Measured on a one-deposit fixture
51
+ // whose form exceeds the cap: the query matched BOTH the whole 138-byte
52
+ // form (saturating) AND an interior fold node of 34 bytes (unsaturated,
53
+ // continuing `" Paris, an"`). Skipping the saturated candidate removed the
54
+ // only evidence that disagreed, uniqueness then passed on the interior
55
+ // node, and a mid-form slice was voiced as an answer. Suppressing the
56
+ // disagreement is what created the fabrication.
57
+ // (Testing instead whether a candidate is a "complete form" via the fold
58
+ // does NOT work and was measured: content addressing makes an interior
59
+ // node resolve to ITSELF, so self-resolution says nothing about
60
+ // completeness.)
61
+ // 2. THE CONTINUATION MUST REACH ONE GROUPING WINDOW. A trained
62
+ // `What is the capital of France??` opens with `What is the capital of
63
+ // France?` and continues by a single byte. Below W the continuation is
64
+ // sub-quantum — the fold groups nothing from it — and voicing it produces
65
+ // the degenerate reply that is a known failure smell.
66
+ // 3. UNIQUENESS. Several trained forms may open with the query and continue
67
+ // differently, and then the corpus does not say which continuation the
68
+ // asker means. Distinct continuations ⇒ refuse. This is the documented
69
+ // PREFIX TRAP, and it is real — just not for every prefix. Measured: of
70
+ // 15 battery probes exactly ONE yields a unique continuation, and all
71
+ // three honest-silence probes yield none (including `What is the capital
72
+ // of Zamunda?`, whose top hit scores 0.83).
73
+ //
74
+ // Uniqueness is judged on the continuation BYTES, not on the candidate id: the
75
+ // same continuation reached through two trained forms is one answer, not an
76
+ // ambiguity.
77
+ import { bytesEqual } from "../bytes.js";
78
+ import { rItem } from "./trace.js";
79
+ import { canonicalWindows, leafIdPrefix } from "./canonical.js";
80
+ import { hubBound } from "./traverse.js";
81
+ /** Trained forms the query may OPEN, proposed from the write side's own
82
+ * leaf-id window index — the supply of last resort for {@link
83
+ * prefixCompletion}.
84
+ *
85
+ * WHY A SECOND SUPPLY EXISTS. The ranked list this mechanism normally reads
86
+ * is a resonance list, and resonance cannot rank a proper prefix: measured on
87
+ * the trained store, cos(prefix, form) falls from 0.9629 at a one-byte
88
+ * truncation to 0.6206 at three bytes, against a reachThreshold of 0.8750.
89
+ * Three bytes of truncation put the answer out of reach on GEOMETRY, not on a
90
+ * bug, so no k and no re-ranking recovers it.
91
+ *
92
+ * WHY THIS ROUTE WORKS WHERE THE FOLD DOES NOT. A query's own fold is
93
+ * useless here: content addressing is not phrase-position-invariant, so a
94
+ * standalone prefix folds to a DIFFERENT node than the same bytes sitting
95
+ * inside a longer deposit, and neither the prefix's own node nor its
96
+ * ancestors lead to the deposit (measured: the 22-byte prefix of the
97
+ * photosynthesis form resolves, is shared by 6 contexts, and does not have
98
+ * the form among its ancestors). Leaf ids ARE position-invariant — they are
99
+ * content-addressed on single bytes — and `indexSubSpans` already interns a
100
+ * flat branch over every canonical WINDOW of a deposit's leaf-id stream, with
101
+ * containment edges to the chunks that window spans. A query that is a
102
+ * prefix therefore shares those window nodes exactly, and reaches the deposit
103
+ * by climbing containment then parents. Nothing is added to the write side;
104
+ * this reads an index training already built.
105
+ *
106
+ * BOUNDED (§2.8), AND WITH NO NEW THRESHOLD. The window whose containment is
107
+ * SMALLEST carries the most evidence, and one saturated at `hubBound` carries
108
+ * none — that is the same √N reading of "hub" the rest of the mind uses, not
109
+ * a tuned knob. The upward walk spends a budget of `hubBound` nodes and
110
+ * fans out by W, so a hub query enumerates nothing and the caller stays
111
+ * silent rather than guessing (§2.13). Measured on the trained store: the
112
+ * photosynthesis form at a one-byte truncation picks a window with 52
113
+ * containers, visits 446 nodes, and yields exactly ONE candidate that
114
+ * survives the caller's byte compare — the form itself.
115
+ *
116
+ * These are PROPOSALS only. Every candidate still faces the byte-exact
117
+ * prefix compare and all three guards below, so a wrong proposal costs one
118
+ * bounded read and can never be voiced (§2.3). */
119
+ export function prefixCandidates(ctx, query) {
120
+ const store = ctx.store;
121
+ const W = ctx.space.maxGroup;
122
+ const run = leafIdPrefix(ctx, query);
123
+ // The widest canonical window is the most discriminative one the write side
124
+ // ever interned; a query too short to spell one carries no window evidence.
125
+ const len = canonicalWindows(W)[1];
126
+ if (run.length < len)
127
+ return [];
128
+ const bound = hubBound(ctx);
129
+ let best = null;
130
+ let bestN = 0;
131
+ for (let off = 0; off + len <= run.length; off++) {
132
+ const wid = store.findBranch(run.slice(off, off + len));
133
+ if (wid === null)
134
+ continue;
135
+ const n = store.containersSlice(wid, 0, bound).length;
136
+ // Empty says the window spans no chunk; saturated says it is a hub, whose
137
+ // containment discriminates nothing. Neither is evidence.
138
+ if (n === 0 || n >= bound)
139
+ continue;
140
+ if (best === null || n < bestN) {
141
+ best = wid;
142
+ bestN = n;
143
+ }
144
+ }
145
+ if (best === null)
146
+ return [];
147
+ let frontier = store.containersSlice(best, 0, bound);
148
+ const seen = new Set(frontier);
149
+ let budget = bound;
150
+ while (frontier.length > 0 && budget > 0) {
151
+ const next = [];
152
+ for (const f of frontier) {
153
+ if (budget-- <= 0)
154
+ break;
155
+ for (const p of store.parentsFirst(f, W)) {
156
+ if (seen.has(p))
157
+ continue;
158
+ seen.add(p);
159
+ next.push(p);
160
+ }
161
+ }
162
+ frontier = next;
163
+ }
164
+ return [...seen];
165
+ }
166
+ /** The sole trained form the query opens — or null when no candidate opens with
167
+ * it, when the continuation is sub-quantum, when a candidate's continuation
168
+ * cannot be read through, or when the candidates disagree.
169
+ *
170
+ * `ranked` must be a list the caller has ALREADY fetched; this mechanism never
171
+ * resonates on its own (see the header's cost note). */
172
+ export function prefixCompletion(ctx, query, ranked) {
173
+ const W = ctx.space.maxGroup;
174
+ const t = ctx.trace?.enter("prefixCompletion", [rItem(query, "query")]);
175
+ const done = (hit, note, data) => {
176
+ t?.done(hit === null ? [] : [rItem(hit.continuation, "continuation", hit.id)], note, data);
177
+ return hit;
178
+ };
179
+ // Reads are bounded to phrase scale, the same bound the frame filler uses.
180
+ // A query with no room for a whole grouping window past its own length
181
+ // cannot clear guard 2, so it is not worth a single read.
182
+ const cap = query.length * W;
183
+ if (query.length === 0 || cap < query.length + W) {
184
+ return done(null, "no room for a perceivable continuation within the cap");
185
+ }
186
+ // Distinct continuations, each with the first form that offered it. Held as
187
+ // a list, not a byte-keyed map: candidates that open with the query are few
188
+ // (measured: 1 on the trained store's winning query), and a linear byte
189
+ // compare needs no string encoding of content. Uniqueness (guard 3) is
190
+ // decided over this list, so the scan cannot stop early — a second
191
+ // continuation IS the refusal, and finding it is the point.
192
+ const found = [];
193
+ let opened = 0;
194
+ let unreadable = 0;
195
+ let subQuantum = 0;
196
+ for (const id of ranked) {
197
+ const form = ctx.store.bytesPrefix(id, cap);
198
+ if (form.length <= query.length)
199
+ continue;
200
+ let opens = true;
201
+ for (let i = 0; i < query.length; i++) {
202
+ if (form[i] !== query[i]) {
203
+ opens = false;
204
+ break;
205
+ }
206
+ }
207
+ if (!opens)
208
+ continue;
209
+ opened++;
210
+ // Guard 1: a saturated read continues out of sight — a disagreement that
211
+ // cannot be resolved, so it ends the search rather than being skipped.
212
+ if (form.length >= cap) {
213
+ unreadable++;
214
+ continue;
215
+ }
216
+ const rest = form.subarray(query.length);
217
+ // Guard 2: below one grouping window there is no structure to voice.
218
+ if (rest.length < W) {
219
+ subQuantum++;
220
+ continue;
221
+ }
222
+ if (!found.some((f) => bytesEqual(f.continuation, rest))) {
223
+ found.push({ id, form, continuation: rest });
224
+ }
225
+ }
226
+ const data = {
227
+ candidates: ranked.length,
228
+ opened,
229
+ unreadable,
230
+ subQuantum,
231
+ distinctContinuations: found.length,
232
+ };
233
+ if (unreadable > 0 && found.length > 0) {
234
+ return done(null, "a form opens with this query but continues past the read bound — " +
235
+ "its continuation cannot be read, so none is licensed", data);
236
+ }
237
+ // Guard 2b: A SUB-QUANTUM CONTINUATION IS STILL A DISAGREEMENT. Guard 2
238
+ // refuses to VOICE a below-window continuation, and rightly — there is no
239
+ // structure there to speak. But dropping such a candidate from the
240
+ // uniqueness tally silently converts "the corpus offers many continuations,
241
+ // most of them unvoiceable" into "the corpus offers exactly one", and
242
+ // guard 3 then passes VACUOUSLY on the sole survivor. That is precisely
243
+ // the failure guard 1 documents for unreadable continuations — suppressing
244
+ // the disagreement is what manufactures the answer — so it is answered the
245
+ // same way, and for the same reason.
246
+ //
247
+ // Measured on a 4,300-fact fixture of "what is the value of <i>?": the
248
+ // query "what is the value of" drew candidates continuing " 0?", " 4?",
249
+ // " 8?" (3 bytes, sub-quantum at W=4) and " 10?" (4 bytes). The first
250
+ // three were dropped, leaving one survivor, and the mechanism reported
251
+ // "exactly one trained form" and voiced "the value of 10 is 20" — an
252
+ // arbitrary pick from thousands of equally-good readings, with the
253
+ // evidence of ambiguity discarded on the way.
254
+ //
255
+ // Note this can only ever cause SILENCE, never a different answer: it
256
+ // withholds a completion the corpus does not uniquely license.
257
+ if (subQuantum > 0 && found.length > 0) {
258
+ return done(null, "other trained forms open with this query but continue below one " +
259
+ "grouping window — the corpus offers competing readings, so no " +
260
+ "single completion is licensed", data);
261
+ }
262
+ // Guard 3: the corpus must agree on ONE continuation.
263
+ if (found.length !== 1) {
264
+ return done(null, found.length === 0
265
+ ? "no trained form opens with this query and continues perceivably"
266
+ : "trained forms open with this query but continue differently — " +
267
+ "the corpus does not say which continuation is meant", data);
268
+ }
269
+ return done(found[0], "one trained form opens with this query, and continues perceivably", data);
270
+ }