@hviana/sema 0.4.6 → 0.5.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.
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/bridge.js +27 -1
  10. package/dist/src/mind/frame-filler.d.ts +15 -0
  11. package/dist/src/mind/frame-filler.js +535 -0
  12. package/dist/src/mind/learning.js +6 -11
  13. package/dist/src/mind/mechanisms/cast.js +72 -2
  14. package/dist/src/mind/mechanisms/cover.js +6 -1
  15. package/dist/src/mind/mechanisms/extraction.js +27 -0
  16. package/dist/src/mind/mechanisms/recall.js +214 -34
  17. package/dist/src/mind/mind.d.ts +49 -1
  18. package/dist/src/mind/mind.js +137 -10
  19. package/dist/src/mind/pipeline-mechanism.d.ts +7 -0
  20. package/dist/src/mind/pipeline.js +29 -1
  21. package/dist/src/mind/prefix-completion.d.ts +59 -0
  22. package/dist/src/mind/prefix-completion.js +270 -0
  23. package/dist/src/mind/primitives.d.ts +29 -10
  24. package/dist/src/mind/primitives.js +52 -61
  25. package/dist/src/mind/recognition.js +119 -9
  26. package/dist/src/mind/traverse.d.ts +32 -0
  27. package/dist/src/mind/traverse.js +52 -0
  28. package/dist/src/mind/types.d.ts +55 -16
  29. package/dist/src/mind/types.js +68 -19
  30. package/dist/src/rabitq-ivf/src/rabitq.js +31 -1
  31. package/dist/src/store.d.ts +21 -0
  32. package/dist/src/store.js +21 -0
  33. package/example/train_base.ts +21 -4
  34. package/package.json +1 -1
  35. package/src/canon.ts +28 -0
  36. package/src/geometry.ts +100 -1
  37. package/src/mind/bridge.ts +34 -0
  38. package/src/mind/frame-filler.ts +604 -0
  39. package/src/mind/learning.ts +5 -9
  40. package/src/mind/mechanisms/cast.ts +70 -2
  41. package/src/mind/mechanisms/cover.ts +6 -1
  42. package/src/mind/mechanisms/extraction.ts +27 -0
  43. package/src/mind/mechanisms/recall.ts +236 -37
  44. package/src/mind/mind.ts +154 -14
  45. package/src/mind/pipeline-mechanism.ts +7 -0
  46. package/src/mind/pipeline.ts +33 -1
  47. package/src/mind/prefix-completion.ts +314 -0
  48. package/src/mind/primitives.ts +59 -70
  49. package/src/mind/recognition.ts +117 -6
  50. package/src/mind/traverse.ts +52 -0
  51. package/src/mind/types.ts +98 -42
  52. package/src/rabitq-ivf/src/rabitq.ts +31 -1
  53. package/src/store.ts +25 -0
  54. package/test/13-conversation.test.mjs +13 -0
  55. package/test/57-fusion-order.test.mjs +65 -0
  56. package/test/65-ann-recall.test.mjs +331 -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 +1082 -0
@@ -12,7 +12,7 @@ import { read, resolve } from "../primitives.js";
12
12
  import { guidedFirst, hubBound } from "../traverse.js";
13
13
  import { conceptHop } from "../match.js";
14
14
  import { bridge } from "../resonance.js";
15
- import { liftAnswer, segRestatesQuery } from "../types.js";
15
+ import { liftAnswer, liftedScaffolding, segRestatesQuery } from "../types.js";
16
16
  import { decodeText, unexplainedLabel } from "../rationale.js";
17
17
  import { indexOf } from "../../bytes.js";
18
18
  import { rItem, rNode, traceDerivation } from "../trace.js";
@@ -222,6 +222,11 @@ export const coverMechanism = {
222
222
  moves: 0,
223
223
  weight: solved.cost, // A*LD derivation's g-value IS the weight
224
224
  unexplained: unexplainedLabel(query, accounted),
225
+ // How much of the composed answer is the asker's own unexplained words
226
+ // (the spans the liftAnswer trace above labels "scaffolding"). Cover is
227
+ // the mechanism that can carry them, because a PASS span still lands in
228
+ // the cover it returns.
229
+ scaffolding: liftedScaffolding(segs, query.length, query, W),
225
230
  }];
226
231
  },
227
232
  };
@@ -92,6 +92,33 @@ export async function extractBySkill(ctx, query, pre) {
92
92
  subQuantum++;
93
93
  continue;
94
94
  }
95
+ // AN UNANCHORED READ IS NOT AN EXTRACTION. This function's contract (see
96
+ // the doc above) is that `accounted` carries "the located frames AND any
97
+ // read span BOUNDED by located frames on both sides", while an open-ended
98
+ // read "remains a guess about where the span stops — it stays unaccounted".
99
+ // EMPTY accounted is the degenerate case of that: NO frame of the exemplar
100
+ // was located in the query at all, so nothing ties the bytes just read to
101
+ // this question — the skill applied its exemplar's geometry to a query it
102
+ // never matched.
103
+ //
104
+ // The live case (analyze_training.ts F, the battery's ONLY wrong non-silent
105
+ // answer): "Which city is France's seat of government?" answered "Which ci"
106
+ // — a fragment of the query itself — from the exemplar "What is dll", with
107
+ // accounted=[] and pieces=1. isSpanShaped is deliberately permissive (a
108
+ // sparse-subsequence check), so it accepts exemplars whose relation to the
109
+ // query is coincidental gap-matching; requiring at least one LOCATED frame
110
+ // is the structural evidence that permissiveness leaves out.
111
+ //
112
+ // Scoped to extraction ON PURPOSE. The same test at the pipeline's
113
+ // post-grounding density check was tried and REVERTED: `accounted` is passed
114
+ // empty BY CONVENTION on recall's own tiers (recall.ts's ground(…, [], …)),
115
+ // so a density veto there refused six legitimate reverse-recall groundings.
116
+ // Here the field is this mechanism's own output and carries its documented
117
+ // meaning, so the test is sound exactly where the convention does not reach.
118
+ if (built.accounted.length === 0) {
119
+ subQuantum++;
120
+ continue;
121
+ }
95
122
  if (shapeMisses > 0 || subQuantum > 0) {
96
123
  ctx.trace?.step("trySkillAnchors", [
97
124
  rItem(query.subarray(0, 0), `skipped ${shapeMisses + subQuantum}`),
@@ -4,15 +4,17 @@
4
4
  // index and grounds the nearest learned form. Four tiers, orderly degrading
5
5
  // from exact self-match to honest echo.
6
6
  import { cosine } from "../../vec.js";
7
- import { conceptThreshold, consensusFloor, identityBar, reachThreshold, significanceBar, } from "../../geometry.js";
7
+ import { conceptThreshold, consensusFloor, dominates, identityBar, reachThreshold, significanceBar, } from "../../geometry.js";
8
8
  import { gistOf, read, resolve } from "../primitives.js";
9
9
  import { bytesEqual, indexOf } from "../../bytes.js";
10
- import { corpusN, hubBound } from "../traverse.js";
10
+ import { allWindowsAreScaffolding, corpusN, hubBound } from "../traverse.js";
11
11
  import { follow, project, reverseContext } from "../match.js";
12
12
  import { CONCEPT, STEP } from "../graph-search.js";
13
13
  import { unexplainedLabel } from "../rationale.js";
14
14
  import { rItem, rNode } from "../trace.js";
15
15
  import { substitutionBridge } from "../bridge.js";
16
+ import { frameFillerSubstitution } from "../frame-filler.js";
17
+ import { prefixCandidates, prefixCompletion } from "../prefix-completion.js";
16
18
  /** Recall the answer by resonating the whole query against the content index. */
17
19
  export async function recallByResonance(ctx, query, pre) {
18
20
  const t = ctx.trace?.enter("recallByResonance", [
@@ -180,13 +182,92 @@ export async function recallByResonance(ctx, query, pre) {
180
182
  Math.sqrt(otherLen / Math.max(1, query.length)));
181
183
  // 2. Scaffolding-dominated.
182
184
  if (top.score >= sig) {
183
- const N = corpusN(ctx);
184
- const minVote = consensusFloor(N);
185
185
  // The committed points of attention ARE the shared climb's roots (same
186
186
  // query, same k, same DF mode) — read them from Precomputed instead of
187
187
  // re-climbing, so even a traced response pays for the climb once.
188
188
  const forest = (await pre.attention()).roots;
189
- if (forest.length > 0 && forest[0].vote >= minVote) {
189
+ // TRUST THE ANCHOR ON ITS BREADTH, NOT ON ITS ABSOLUTE VOTE.
190
+ //
191
+ // This gate read `forest[0].vote >= consensusFloor(N)`. Attention.breadth's
192
+ // own contract (types.ts) says why that is the wrong quantity: the IDF vote
193
+ // is "an absolute, ln(N)-scaled quantity that means 'strong' on a small
194
+ // store and 'weak' on a large one for the SAME degree of genuine
195
+ // consensus", while breadth is the SCALE-INVARIANT reading — "a point whose
196
+ // breadth clears `dominates` (> half the query's regions corroborate it) is
197
+ // real consensus; one that does not is a coincidental single-region echo".
198
+ // Attention.peak's contract makes the same point from the other side:
199
+ // comparing a POOLED SUM against a floor that prices ONE region's evidence
200
+ // is a dimensional error.
201
+ //
202
+ // Measured on the 15.7M-node store (N=325,615, so the old floor was 13.19).
203
+ // The absolute vote cannot separate right from wrong at this scale, and the
204
+ // proof is a probe that must stay SILENT:
205
+ //
206
+ // anchor picked by the climb vote breadth correct?
207
+ // "What is the chemical formula …" 10.60 0.556 RIGHT
208
+ // "Qual é a capital de França?" 8.19 0.667 RIGHT
209
+ // "Who wrote the play Romeo …?" 8.25 0.833 RIGHT
210
+ // "How do you say "good morning" …" 10.77 0.800 RIGHT
211
+ // "What is the commercial capital …" 12.69 0.333 Zamunda — MUST be silent
212
+ // "Menene sunan ginin mafi tsayi …" 12.79 0.214 wrong (Hausa)
213
+ // "Today is the 5th of March …" 10.36 0.000 wrong
214
+ //
215
+ // Zamunda's junk attractor outvotes every correct anchor, so no vote
216
+ // threshold admits the right ones without admitting fabrication — while
217
+ // breadth > ½ admits exactly the four correct anchors and nothing else.
218
+ // The old floor was simply never cleared on a corpus this large: the tier
219
+ // was dead code here, which is why 12 probes fell through to silence.
220
+ //
221
+ // `dominates(breadth, 1)` is the SAME half-dominance predicate used
222
+ // throughout, applied to the fraction — no new constant, and the bar the
223
+ // breadth contract names. COST: none; breadth is already computed and
224
+ // carried on every Attention the climb returns.
225
+ //
226
+ // The two readings are ALTERNATIVES, never a substitution. REPLACING the
227
+ // vote test with the breadth test was tried and broke 7 tests: on a small
228
+ // store ln(N) is low, so the vote bar is the one that legitimately fires
229
+ // there, and — as Attention.clusters' own contract warns — "breadth starves
230
+ // a genuine, evenly-split multi-topic query, since no root in a real N-way
231
+ // split can exceed half the vote" (the two 3.1 two-topic fusion tests are
232
+ // exactly that shape). Each reading is sufficient on its own evidence: a
233
+ // vote that clears the absolute floor is strong enough wherever the corpus
234
+ // is small enough for that to mean something, and a breadth past ½ is real
235
+ // consensus at any scale. ORing them keeps every admission the floor
236
+ // already made and adds only the scale-invariant ones it could never see.
237
+ //
238
+ // BREADTH ALSO NEEDS DISCRIMINATIVENESS. Breadth asks how much of the
239
+ // query corroborates the anchor, never whether the anchor SAYS anything: on
240
+ // a one-context store every region trivially corroborates the only anchor
241
+ // there is, so breadth is 1 while the anchor's IDF is 0 — and test/31 A2
242
+ // ("explain quantum chromodynamics" against a lone cat fact) answered the
243
+ // cat, which is fabrication. A region's IDF contribution for an anchor
244
+ // reached through c of N contexts is ln(N/c), so requiring it to exceed
245
+ // ln 2 is requiring c·2 < N — the SAME half-dominance reading used
246
+ // everywhere, expressed in the IDF's own units rather than as a new bar.
247
+ // `peak` is that per-region contribution, and reading it here is what
248
+ // Attention.peak's contract asks of a consumer gating on this evidence.
249
+ //
250
+ // AND THE QUERY MUST SAY SOMETHING. Both readings above price the
251
+ // ANCHOR's evidence; neither asks whether the QUERY discriminates
252
+ // anything. A query that is entirely corpus-global scaffolding gives the
253
+ // corpus nothing to be held to, and this tier — which exists to serve
254
+ // scaffolding-DOMINATED queries — is exactly where that runs out.
255
+ // Measured on the trained store: "What is the capital " answered "Colombo
256
+ // is the commercial capital of Sri Lanka…" on breadth 0.667 / clusters 1,
257
+ // and every window it spells is a hub ("What":572). See
258
+ // allWindowsAreScaffolding for the full separation, including the probes
259
+ // this tier serves CORRECTLY, which all retain a discriminating window
260
+ // ("what is the capital of france" → "f fr":248).
261
+ //
262
+ // DISPERSION WAS TRIED HERE FIRST AND FALSIFIED — do not retry it: the
263
+ // fabrication and the no-punctuation robustness probe have the IDENTICAL
264
+ // profile (breadth 0.667, clusters 1), so requiring clusters >= 2 silenced
265
+ // "what is the capital of france" too and cost the battery a probe.
266
+ const minVote = consensusFloor(corpusN(ctx));
267
+ if (forest.length > 0 &&
268
+ !allWindowsAreScaffolding(ctx, query) &&
269
+ (forest[0].vote >= minVote ||
270
+ (dominates(forest[0].breadth, 1) && forest[0].peak > Math.LN2))) {
190
271
  const g = await project(ctx, forest[0].anchor, queryGist);
191
272
  // The anchor cleared the consensus floor, but the floor prices the
192
273
  // ANCHOR's evidence, not the projection's: a junk attractor can clear
@@ -212,6 +293,42 @@ export async function recallByResonance(ctx, query, pre) {
212
293
  }
213
294
  }
214
295
  // 3b. Corroborated-substitution bridge — refusal-path only (bridge.ts).
296
+ // MEMOISED ACROSS EVERY REMAINING TIER, and that is load-bearing rather than
297
+ // tidy: the bridge, prefix completion and the frame filler all read the SAME
298
+ // candidate list, so the exhaustive branch runs at most once per response.
299
+ // Without the memo each tier re-issues it — measured at 490 ms median against
300
+ // 13 ms non-exhaustive (36x).
301
+ const wideIdsOnce = async () => {
302
+ // When the top resonance hit is below the concept threshold, the query
303
+ // gist has no concept-level match to any stored form — an exhaustive √N
304
+ // ANN would only score more vectors below the bar (profiled at 38K–40K
305
+ // annVectorReads per refusing query on a 325K-context store). The
306
+ // bridge's structural channels (junction walks, anchor climbs) are the
307
+ // correct proposal source for a query whose gist has no clean match;
308
+ // the ANN cannot propose what the gist cannot rank.
309
+ // The condition above is the SCORE of the top hit, not the size of the
310
+ // corpus. It used to be spelled `corpusN(ctx) <= (k · W)³`, which asks
311
+ // a different question and answers it wrongly at exactly the scale the
312
+ // note was written from: on the trained store N = 325,608 with k = 24
313
+ // and W = 4 puts the cube at 884,736, so that store took the exhaustive
314
+ // branch — the very branch measured here as 38K–40K annVectorReads.
315
+ // Measured cost of the mismatch: substitutionBridge 8,544ms of a
316
+ // 19,548ms think (44%), against 1,248ms and 14,218ms without it, with
317
+ // every answer in the battery byte-identical and the suite unchanged
318
+ // at 445/445. Corpus size was never the discriminator; whether the
319
+ // gist ranks ANYTHING at concept level is.
320
+ //
321
+ // Reading it as the note states also removes a duplicated (k · W)³ —
322
+ // the same cube gates crossRegionVotes' walk budget, where it likewise
323
+ // never engages at real scale (see attention.ts).
324
+ if (whole.length > 0 && whole[0].score >= conceptThreshold(ctx.store.D)) {
325
+ const exhaustive = await ctx.store.resonate(queryGist, hubBound(ctx), true);
326
+ return exhaustive.map((h) => h.id);
327
+ }
328
+ return whole.map((h) => h.id);
329
+ };
330
+ let wide = null;
331
+ const wideIds = () => (wide ??= wideIdsOnce());
215
332
  // Every gist-based tier has failed; before refusing, align the query
216
333
  // byte-for-byte against the trained contexts its own stored windows
217
334
  // anchor, accepting mismatches only as corpus-attested, concept-bar
@@ -232,35 +349,6 @@ export async function recallByResonance(ctx, query, pre) {
232
349
  // exact co-occurrence and bounded anchor ascent are the bridge's structural
233
350
  // proposal channels, while an exhaustive ANN call made every honest
234
351
  // refusal cost hundreds of milliseconds regardless of k.
235
- const wideIds = async () => {
236
- // When the top resonance hit is below the concept threshold, the query
237
- // gist has no concept-level match to any stored form — an exhaustive √N
238
- // ANN would only score more vectors below the bar (profiled at 38K–40K
239
- // annVectorReads per refusing query on a 325K-context store). The
240
- // bridge's structural channels (junction walks, anchor climbs) are the
241
- // correct proposal source for a query whose gist has no clean match;
242
- // the ANN cannot propose what the gist cannot rank.
243
- // The condition above is the SCORE of the top hit, not the size of the
244
- // corpus. It used to be spelled `corpusN(ctx) <= (k · W)³`, which asks
245
- // a different question and answers it wrongly at exactly the scale the
246
- // note was written from: on the trained store N = 325,608 with k = 24
247
- // and W = 4 puts the cube at 884,736, so that store took the exhaustive
248
- // branch — the very branch measured here as 38K–40K annVectorReads.
249
- // Measured cost of the mismatch: substitutionBridge 8,544ms of a
250
- // 19,548ms think (44%), against 1,248ms and 14,218ms without it, with
251
- // every answer in the battery byte-identical and the suite unchanged
252
- // at 445/445. Corpus size was never the discriminator; whether the
253
- // gist ranks ANYTHING at concept level is.
254
- //
255
- // Reading it as the note states also removes a duplicated (k · W)³ —
256
- // the same cube gates crossRegionVotes' walk budget, where it likewise
257
- // never engages at real scale (see attention.ts).
258
- if (whole.length > 0 && whole[0].score >= conceptThreshold(ctx.store.D)) {
259
- const exhaustive = await ctx.store.resonate(queryGist, hubBound(ctx), true);
260
- return exhaustive.map((h) => h.id);
261
- }
262
- return whole.map((h) => h.id);
263
- };
264
352
  const bridged = await substitutionBridge(ctx, query, wideIds);
265
353
  if (bridged !== null) {
266
354
  const g = await project(ctx, bridged.id, queryGist);
@@ -274,7 +362,32 @@ export async function recallByResonance(ctx, query, pre) {
274
362
  const cBytes = ctx.store.bytes(bridged.id);
275
363
  const manufactured = g !== null &&
276
364
  bridged.subs.some((s) => indexOf(cBytes.subarray(s.cs, s.ce), g, 0) >= 0);
365
+ // THE PREFIX TRAP IS NOT THIS TIER'S TO SPRING. With no substitutions
366
+ // the claim is "a trained context IS this query, up to filler". When
367
+ // the query is a STRICT BYTE PREFIX of that context, the claim is false
368
+ // in the one way that matters: the candidate's extra tail is precisely
369
+ // the DISCRIMINATING part, and dismissing it as filler asserts a
370
+ // specification the asker never made. Measured on a 4,300-fact fixture
371
+ // of "what is the value of <i>?": the query "what is the value of"
372
+ // bridged with subs [] to "what is the value of 0?" and answered "the
373
+ // value of 0 is 0" — one arbitrary pick from 4,300 equally-matching
374
+ // contexts, every one of which fits the query exactly as well.
375
+ //
376
+ // The engine ALREADY has the right machinery for this shape:
377
+ // prefixCompletion runs a few lines below and carries the three guards
378
+ // this tier lacks — unreadable-continuation veto, sub-quantum
379
+ // continuation, and UNIQUENESS (distinct continuations ⇒ refuse), which
380
+ // is exactly what 4,300 competing values must trip. So this is not a
381
+ // new rule and not a new threshold: it is deferring a prefix decision to
382
+ // the tier that owns it (§2.5, one factored machinery). Byte-strict on
383
+ // purpose — a candidate differing by case or punctuation ("what is the
384
+ // capital of france" → "What is the capital of France?") is NOT a byte
385
+ // prefix, keeps grounding here, and is unaffected.
386
+ const strictPrefix = g !== null &&
387
+ cBytes.length > query.length &&
388
+ indexOf(cBytes, query, 0) === 0;
277
389
  if (g !== null && g.length > 0 && !restates(g) && !manufactured &&
390
+ !(bridged.subs.length === 0 && strictPrefix) &&
278
391
  !(g.length < query.length && indexOf(query, g, 0) >= 0)) {
279
392
  return ground(g, bridged.subs.length === 0
280
393
  ? `identity bridge — a trained context IS this query, up to ` +
@@ -326,6 +439,73 @@ export async function recallByResonance(ctx, query, pre) {
326
439
  bridged.subs.length === 0);
327
440
  }
328
441
  }
442
+ // 3b′. PREFIX COMPLETION — refusal-path only (prefix-completion.ts).
443
+ // Inside the bridge's block, and deliberately: it consumes `wideIds`, the
444
+ // list the bridge has already fetched, so it costs a bounded byte compare
445
+ // per candidate and not one resonance. The claim it makes is the
446
+ // strongest in the ladder — every query byte is a LITERAL match from
447
+ // offset zero of a trained form — so it needs no projection and no reach
448
+ // gate. It runs after the bridge only because the bridge answers the
449
+ // richer relation when it can; a prefix match that the bridge also
450
+ // explains is the same trained form either way.
451
+ {
452
+ // The resonance list first; only when it supplies nothing does the
453
+ // write side's leaf-id window index propose (prefixCandidates). That
454
+ // ordering is the whole cost story: a query the ranked list can already
455
+ // explain pays not one extra read, and the fallback's bounded walk is
456
+ // spent only where the alternative is an empty answer. It is a second
457
+ // SUPPLY, not a second mechanism — the same three guards decide.
458
+ const completed = prefixCompletion(ctx, query, await wideIds()) ??
459
+ prefixCompletion(ctx, query, prefixCandidates(ctx, query));
460
+ if (completed !== null) {
461
+ return ground(completed.form, "prefix completion — the query IS the opening of exactly one " +
462
+ "trained form, which this grounds whole",
463
+ // Every query byte is literally matched against the form. The
464
+ // completion is the form's own continuation, not a substitution, so
465
+ // there is nothing to be humble about in the accounting — the same
466
+ // reading the IDENTITY bridge above takes.
467
+ whole_, STEP, false,
468
+ // NOT complete: the query is a proper PREFIX, so the form may carry
469
+ // more past the remainder this tier voiced.
470
+ false);
471
+ }
472
+ }
473
+ }
474
+ // 3c. FRAME-FILLER SUBSTITUTION — refusal-path only (frame-filler.ts).
475
+ // The bridge has failed, and for the shape this tier answers it MUST fail:
476
+ // a definite description standing where a proper noun stands is not a
477
+ // similarity relation the bridge can price (raw balance refuses
478
+ // `dominates(6, 37)`, and correctly — that is the France/Spain trap). This
479
+ // tier makes a different claim: not that the two spans resemble each other,
480
+ // but that the store ALREADY HOLDS this query with the filler in the
481
+ // description's place, byte-exactly. A key the store does not hold is
482
+ // discarded, so the answer is always a trained continuation.
483
+ {
484
+ // THE COHORT NEEDS EVIDENCE, AND THE REFUSAL PATH HAS ALREADY BOUGHT IT.
485
+ // This tier reads constituency from what a cohort of exemplars does NOT
486
+ // share, so its resolution is bounded by how many instances of the frame it
487
+ // can see. The top-k resonance hits are too few — on the two-hop probe the
488
+ // exemplars holding the query's discriminative content number TWO, and two
489
+ // structures agree on so little that a whole clause reads as content. The
490
+ // exhaustive list the bridge fetched is the same evidence at ~570 wide, and
491
+ // it is already paid for (memoised above, so this costs no ANN call).
492
+ const filled = frameFillerSubstitution(ctx, query, await wideIds());
493
+ if (filled !== null) {
494
+ const g = await project(ctx, filled.id, queryGist);
495
+ // The same restated-fragment and manufactured-answer guards every tier
496
+ // above applies: a projection contained in the FILLER is the
497
+ // substitution restated as if it were knowledge, not knowledge.
498
+ if (g !== null && g.length > 0 && !restates(g) &&
499
+ indexOf(filled.filler, g, 0) < 0 &&
500
+ !(g.length < query.length && indexOf(query, g, 0) >= 0)) {
501
+ return ground(g, "frame-filler substitution — a trained form IS this query with a " +
502
+ "corroborated filler in the described span's place",
503
+ // The frame is literally matched against the resolved form and the
504
+ // described span is explained by the substitution — the same
505
+ // matched-plus-substituted accounting the bridge reports.
506
+ [[0, query.length]], CONCEPT + STEP);
507
+ }
508
+ }
329
509
  }
330
510
  // The refusal/echo decision. The echo returns a stored form's bytes AS
331
511
  // the answer — a near-identity claim about the query — and identity-grade
@@ -219,6 +219,20 @@ export declare class Mind implements MindContext {
219
219
  * they do with the answer afterwards. It must be called between
220
220
  * {@link beginResponse} and {@link endResponse}. */
221
221
  private _groundAndVoice;
222
+ /** Answer ONE self-contained input.
223
+ *
224
+ * A MULTI-TURN context is not that, and this is the wrong entry point for
225
+ * it. `respond` folds the bytes it is handed with no boundary set, because
226
+ * nothing in a flat byte string says where one turn ended — only the caller
227
+ * who assembled it knows, which is the whole reason `boundaries` is a
228
+ * parameter of {@link perceiveImpl} and never inferred from content. A
229
+ * conversation deposited through {@link ingest} folds its contexts over
230
+ * those turn boundaries, so a hand-concatenated transcript passed here
231
+ * folds differently from the way it was learnt and reaches the trained
232
+ * context node only by luck (measured on a 7-turn conversation: 5/7 here
233
+ * against 7/7 through {@link respondTurn}, same bytes). Use
234
+ * {@link beginConversation} + {@link respondTurn}, or {@link addTurn} to
235
+ * replay turns the Mind should hear but not answer. */
222
236
  respond(input: Input, inspectRationale?: InspectRationale): Promise<Response>;
223
237
  /** Text view of {@link respond}. NUL bytes (0x00) are stripped before
224
238
  * decoding — they are structural padding in text answers. LOSSY for a
@@ -252,7 +266,41 @@ export declare class Mind implements MindContext {
252
266
  * This is the primitive for turns the Mind should hear but not answer:
253
267
  * replaying a transcript, feeding the OTHER speaker's line in a
254
268
  * prediction harness, or restoring context piecewise. {@link
255
- * respondTurn} = addTurn + think + its own reply appended the same way. */
269
+ * respondTurn} = addTurn + think + its own reply appended the same way.
270
+ *
271
+ * ── ON SEPARATORS: THERE IS NO SEPARATOR QUESTION ────────────────────
272
+ *
273
+ * "Never a separator" above says what this method DOES — it appends the
274
+ * bytes you give it and records an OFFSET — not that separator bytes are
275
+ * forbidden, unsupported, or something the engine must be taught about.
276
+ * Sema is agnostic to them, and reviewers keep mistaking that agnosticism
277
+ * for a constraint. To be explicit, because the mistake is easy:
278
+ *
279
+ * 1. A turn boundary is an OFFSET, held here, in `boundaries`. It is
280
+ * never a character the geometry scans for. Nothing downstream asks
281
+ * "what byte separates two turns?" because nothing downstream finds
282
+ * boundaries by looking at content at all.
283
+ * 2. A separator in a CORPUS is ordinary content. If a trainer joins
284
+ * turns with "\n" (example/train_base.ts does), those newlines are
285
+ * simply bytes inside the stream, folded like every other byte. They
286
+ * are a property of that corpus, not of this API and not of the fold.
287
+ * 3. This API can therefore reproduce ANY corpus exactly, with no
288
+ * convention to agree on: replaying a "\n"-joined corpus means passing
289
+ * `"\n" + turnText` as the turn. The separator rides along IN the
290
+ * turn bytes, where it belongs. There is nothing to configure and no
291
+ * mode to select.
292
+ * 4. Inference is not exact-match anyway. Recognition works over
293
+ * sub-spans, canonical equivalence and resonance, so a query that
294
+ * differs from the trained bytes by punctuation or whitespace still
295
+ * reaches the trained forms; it degrades, it does not fail closed.
296
+ *
297
+ * What follows from 1–4: differing separator bytes between a corpus and a
298
+ * query is an ordinary CONTENT difference — the same kind as any other
299
+ * wording difference — and it is measured the same way. It is NOT an
300
+ * incompatibility between the trainer and this API, and it does NOT
301
+ * require choosing a project-wide separator convention. A review that
302
+ * concludes otherwise (this one did, before being corrected) has mistaken
303
+ * its own harness feeding untrained bytes for an architectural defect. */
256
304
  addTurn(conv: Conversation, turn: Input): ConversationState;
257
305
  /** Grow a conversation's accumulated context by one turn's bytes — raw
258
306
  * append plus a boundary offset, pyramid advanced by O(turn), the grown
@@ -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";
@@ -322,12 +322,58 @@ export class Mind {
322
322
  provenance: thought.provenance,
323
323
  };
324
324
  }
325
+ /** Answer ONE self-contained input.
326
+ *
327
+ * A MULTI-TURN context is not that, and this is the wrong entry point for
328
+ * it. `respond` folds the bytes it is handed with no boundary set, because
329
+ * nothing in a flat byte string says where one turn ended — only the caller
330
+ * who assembled it knows, which is the whole reason `boundaries` is a
331
+ * parameter of {@link perceiveImpl} and never inferred from content. A
332
+ * conversation deposited through {@link ingest} folds its contexts over
333
+ * those turn boundaries, so a hand-concatenated transcript passed here
334
+ * folds differently from the way it was learnt and reaches the trained
335
+ * context node only by luck (measured on a 7-turn conversation: 5/7 here
336
+ * against 7/7 through {@link respondTurn}, same bytes). Use
337
+ * {@link beginConversation} + {@link respondTurn}, or {@link addTurn} to
338
+ * replay turns the Mind should hear but not answer. */
325
339
  async respond(input, inspectRationale) {
326
340
  // A STRING input is text by nature: it carries the text equivalence even
327
341
  // through the generic entry point. Raw bytes / grids carry only the
328
342
  // Mind-level canon option, if any.
329
343
  const canon = this._canonFor(typeof input === "string" ? textCanon : null);
330
- return this._respondImpl(inputBytes(this, input), inspectRationale, "respond", canon);
344
+ // EDGE WHITESPACE IS NOT PART OF THE QUESTION — trim it once, here, so
345
+ // every mechanism downstream sees the same question regardless of how the
346
+ // caller spaced it. See canon.ts's textEdgeTrim for why the outer edges of a
347
+ // whole input are exactly where canon.ts's no-trimming hazard cannot arise.
348
+ // Gated on the SAME modality test as the canonicalizer above: for bytes and
349
+ // grids 0x20 is content, and nothing is trimmed.
350
+ //
351
+ // Measured on the 15.7M-node store: without this, one leading space took
352
+ // `Who wrote Romeo and Juliet?` and `What is the chemical symbol for
353
+ // water?` from answered to silent, because a shift re-seats every fold
354
+ // boundary — the whole of analyze_training.ts's K2 phase-robustness gap.
355
+ // The caller's EXACT bytes are tried first and the trim is a RETRY, not a
356
+ // pre-filter. Trimming up front is asymmetric — it normalises the query but
357
+ // not the stored forms — so it breaks byte-exact identity for a form trained
358
+ // WITH edge whitespace: test/04 deposits [" ice ", "cold"] and asks
359
+ // " ice ", which must keep answering. Retrying preserves that (the raw
360
+ // query resolves on the first pass) while still reaching the padded case
361
+ // (the raw query grounds nothing, the trimmed one does).
362
+ //
363
+ // COST: nothing on any answering path. The retry needs BOTH silence AND
364
+ // edge whitespace on the query, the same "only on the already-failed path"
365
+ // discipline test/44 and the bridge's own trim retry use. The conversation
366
+ // entry point (respondTurn) is deliberately NOT trimmed — it tracks
367
+ // turn-boundary offsets into its accumulated context, and shifting the bytes
368
+ // under those offsets would desync them.
369
+ const bytes = inputBytes(this, input);
370
+ const first = await this._respondImpl(bytes, inspectRationale, "respond", canon);
371
+ if (first.bytes.length > 0 || typeof input !== "string")
372
+ return first;
373
+ const trimmed = textEdgeTrim(bytes);
374
+ if (trimmed.length === bytes.length || trimmed.length === 0)
375
+ return first;
376
+ return this._respondImpl(trimmed, inspectRationale, "respond", canon);
331
377
  }
332
378
  /** Text view of {@link respond}. NUL bytes (0x00) are stripped before
333
379
  * decoding — they are structural padding in text answers. LOSSY for a
@@ -354,15 +400,35 @@ export class Mind {
354
400
  beginConversation(state) {
355
401
  const id = this._nextConvId++;
356
402
  const initBytes = state?.context ?? new Uint8Array(0);
357
- const initBoundaries = state?.boundaries ? [...state.boundaries] : [];
403
+ // NORMALISE CALLER-SUPPLIED BOUNDARIES. `boundaries` is documented
404
+ // strictly increasing and every boundary this class produces is (they are
405
+ // appended as the context grows), but a restored {@link ConversationState}
406
+ // comes from OUTSIDE — hand-built, migrated, or round-tripped through a
407
+ // store that did not preserve order. The folds consume boundaries with a
408
+ // sequential `b > prev` filter, so an out-of-order entry is silently
409
+ // DROPPED rather than rejected, and the conversation would then fold over
410
+ // a different cut set than the one the caller believes it restored.
411
+ // `bytesToTree` used to sort on the way in and absorbed this; the
412
+ // incremental fold this now calls does not, so the normalisation belongs
413
+ // here, at the one public door untrusted boundaries come through.
414
+ const initBoundaries = state?.boundaries
415
+ ? [...new Set(state.boundaries)]
416
+ .filter((b) => b > 0 && b < initBytes.length)
417
+ .sort((a, b) => a - b)
418
+ : [];
358
419
  const initAnswered = state?.answeredSpans
359
420
  ? state.answeredSpans.map(([start, end]) => [start, end])
360
421
  : initBoundaries.flatMap((start, i, cuts) => i % 2 === 0 && i + 1 < cuts.length
361
422
  ? [[start, cuts[i + 1]]]
362
423
  : []);
363
- const tree = bytesToTree(this.space, this.alphabet, initBytes, undefined, undefined, initBoundaries.length > 0 ? initBoundaries : undefined);
424
+ // The same incremental fold `_growContext` uses, so a RESTORED
425
+ // conversation starts with segment state its next turn can reuse — a
426
+ // resumed conversation is otherwise identical to a live one and must not
427
+ // pay a full re-fold on every turn for the rest of its life.
428
+ const restored = contentFoldIncremental(this.space, this.alphabet, initBytes);
364
429
  this._conversations.set(id, {
365
- tree,
430
+ tree: restored.tree,
431
+ content: restored.fold,
366
432
  bytes: initBytes,
367
433
  boundaries: initBoundaries,
368
434
  answeredSpans: initAnswered,
@@ -397,7 +463,41 @@ export class Mind {
397
463
  * This is the primitive for turns the Mind should hear but not answer:
398
464
  * replaying a transcript, feeding the OTHER speaker's line in a
399
465
  * prediction harness, or restoring context piecewise. {@link
400
- * respondTurn} = addTurn + think + its own reply appended the same way. */
466
+ * respondTurn} = addTurn + think + its own reply appended the same way.
467
+ *
468
+ * ── ON SEPARATORS: THERE IS NO SEPARATOR QUESTION ────────────────────
469
+ *
470
+ * "Never a separator" above says what this method DOES — it appends the
471
+ * bytes you give it and records an OFFSET — not that separator bytes are
472
+ * forbidden, unsupported, or something the engine must be taught about.
473
+ * Sema is agnostic to them, and reviewers keep mistaking that agnosticism
474
+ * for a constraint. To be explicit, because the mistake is easy:
475
+ *
476
+ * 1. A turn boundary is an OFFSET, held here, in `boundaries`. It is
477
+ * never a character the geometry scans for. Nothing downstream asks
478
+ * "what byte separates two turns?" because nothing downstream finds
479
+ * boundaries by looking at content at all.
480
+ * 2. A separator in a CORPUS is ordinary content. If a trainer joins
481
+ * turns with "\n" (example/train_base.ts does), those newlines are
482
+ * simply bytes inside the stream, folded like every other byte. They
483
+ * are a property of that corpus, not of this API and not of the fold.
484
+ * 3. This API can therefore reproduce ANY corpus exactly, with no
485
+ * convention to agree on: replaying a "\n"-joined corpus means passing
486
+ * `"\n" + turnText` as the turn. The separator rides along IN the
487
+ * turn bytes, where it belongs. There is nothing to configure and no
488
+ * mode to select.
489
+ * 4. Inference is not exact-match anyway. Recognition works over
490
+ * sub-spans, canonical equivalence and resonance, so a query that
491
+ * differs from the trained bytes by punctuation or whitespace still
492
+ * reaches the trained forms; it degrades, it does not fail closed.
493
+ *
494
+ * What follows from 1–4: differing separator bytes between a corpus and a
495
+ * query is an ordinary CONTENT difference — the same kind as any other
496
+ * wording difference — and it is measured the same way. It is NOT an
497
+ * incompatibility between the trainer and this API, and it does NOT
498
+ * require choosing a project-wide separator convention. A review that
499
+ * concludes otherwise (this one did, before being corrected) has mistaken
500
+ * its own harness feeding untrained bytes for an architectural defect. */
401
501
  addTurn(conv, turn) {
402
502
  const data = this._conversations.get(conv.id);
403
503
  if (!data)
@@ -422,10 +522,37 @@ export class Mind {
422
522
  const grown = prevLen > 0 ? concat2(data.bytes, turnBytes) : turnBytes;
423
523
  if (prevLen > 0)
424
524
  data.boundaries.push(prevLen);
425
- const tree = bytesToTree(this.space, this.alphabet, grown, undefined, undefined, data.boundaries.length > 0 ? data.boundaries : undefined);
525
+ // THE PLAIN FOLD, INCREMENTALLY. No boundary set is imposed here: the
526
+ // tree is exactly the tree `perceive(grown)` builds for these bytes, which
527
+ // is exactly the tree the DEPOSIT path folded when it learnt them. That
528
+ // agreement is the whole point — it is what lets a cumulative context
529
+ // resolve to its trained node, and when it was absent the alignment family
530
+ // went quadratic (measured: 5.2M cells on a 476-byte context, against 0
531
+ // when the two sides agree).
532
+ //
533
+ // The optimisation is unaffected by dropping the boundaries, because it
534
+ // never came from them: content cuts are stable under append, so the
535
+ // incremental fold reuses every segment left of the new turn as the SAME
536
+ // object (see contentFoldIncremental). That object identity is what
537
+ // `resolvedSubtrees` — a WeakMap keyed by node identity — needs in order
538
+ // to hit at all. Measured against the stable-prefix fold it replaces:
539
+ // ~40 rebuilt nodes per turn either way, flat as the context grows
540
+ // sevenfold, and ~92% of nodes reused by identity in both.
541
+ //
542
+ // `data.boundaries` is still tracked, and is still exact — it is API
543
+ // metadata (ConversationState, answeredSpans, currentTurnStart), not a
544
+ // fold instruction.
545
+ const folded = contentFoldIncremental(this.space, this.alphabet, grown, data.content);
546
+ const tree = folded.tree;
547
+ data.content = folded.fold;
426
548
  data.tree = tree;
427
549
  data.bytes = grown;
428
- data.perceiveMemo.set(latin1Key(grown), tree);
550
+ // Seeded under the PLAIN content key, and that is now the only key there
551
+ // is: with no boundary set imposed, this tree IS what `perceive(grown)`
552
+ // computes, so the memo entry is an ordinary cache hit rather than the
553
+ // deliberate alias it had to be while the two folds differed. The entry
554
+ // saves the pipeline re-folding the context it was just handed.
555
+ data.perceiveMemo.set(perceiveKey(grown), tree);
429
556
  return tree;
430
557
  }
431
558
  /** 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;