@hviana/sema 0.8.1 → 0.8.3

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 (114) hide show
  1. package/AGENTS.md +29 -29
  2. package/TRADEMARKS.md +0 -1
  3. package/dist/src/config.d.ts +28 -0
  4. package/dist/src/config.js +20 -0
  5. package/dist/src/geometry.d.ts +21 -0
  6. package/dist/src/geometry.js +21 -0
  7. package/dist/src/meter.d.ts +76 -0
  8. package/dist/src/meter.js +95 -0
  9. package/dist/src/mind/attention.d.ts +4 -0
  10. package/dist/src/mind/attention.js +165 -16
  11. package/dist/src/mind/canonical.d.ts +16 -0
  12. package/dist/src/mind/canonical.js +41 -0
  13. package/dist/src/mind/corpus.d.ts +40 -0
  14. package/dist/src/mind/corpus.js +149 -0
  15. package/dist/src/mind/graph-search.d.ts +7 -0
  16. package/dist/src/mind/graph-search.js +254 -24
  17. package/dist/src/mind/index.d.ts +3 -1
  18. package/dist/src/mind/index.js +1 -0
  19. package/dist/src/mind/match.d.ts +9 -4
  20. package/dist/src/mind/match.js +147 -61
  21. package/dist/src/mind/mechanisms/cast.js +19 -3
  22. package/dist/src/mind/mechanisms/confluence.js +24 -0
  23. package/dist/src/mind/mechanisms/cover.js +6 -0
  24. package/dist/src/mind/mechanisms/recall.js +32 -4
  25. package/dist/src/mind/mind.d.ts +57 -0
  26. package/dist/src/mind/mind.js +72 -1
  27. package/dist/src/mind/pipeline-mechanism.d.ts +7 -0
  28. package/dist/src/mind/pipeline.js +66 -20
  29. package/dist/src/mind/primitives.js +9 -1
  30. package/dist/src/mind/rationale.d.ts +28 -1
  31. package/dist/src/mind/rationale.js +22 -1
  32. package/dist/src/mind/reasoning.d.ts +25 -3
  33. package/dist/src/mind/reasoning.js +125 -20
  34. package/dist/src/mind/recognition.js +4 -8
  35. package/dist/src/mind/resonance.js +20 -1
  36. package/dist/src/mind/trace.js +1 -0
  37. package/dist/src/mind/traverse.js +15 -3
  38. package/dist/src/mind/types.d.ts +49 -4
  39. package/docs/INVARIANTS.md +2 -2
  40. package/docs/architecture/bounded-reads.md +1 -1
  41. package/docs/architecture/commonality.md +2 -2
  42. package/docs/architecture/cost-model.md +2 -2
  43. package/docs/architecture/determinism.md +7 -7
  44. package/docs/architecture/match-project.md +2 -3
  45. package/docs/architecture/mechanism-market.md +10 -10
  46. package/docs/architecture/meter.md +5 -5
  47. package/docs/architecture/store.md +3 -3
  48. package/docs/failures/tempting-but-wrong.md +34 -6
  49. package/docs/harness/gates.md +2 -2
  50. package/docs/mechanisms/cast.md +2 -2
  51. package/docs/mechanisms/cover.md +2 -3
  52. package/docs/mechanisms/extraction.md +7 -7
  53. package/docs/mechanisms/recall.md +8 -9
  54. package/jsr.json +1 -1
  55. package/package.json +1 -1
  56. package/src/alu/README.md +11 -12
  57. package/src/config.ts +48 -0
  58. package/src/geometry.ts +21 -0
  59. package/src/meter.ts +98 -0
  60. package/src/mind/attention.ts +167 -16
  61. package/src/mind/canonical.ts +43 -0
  62. package/src/mind/corpus.ts +202 -0
  63. package/src/mind/graph-search.ts +277 -23
  64. package/src/mind/index.ts +8 -1
  65. package/src/mind/match.ts +148 -57
  66. package/src/mind/mechanisms/cast.ts +20 -2
  67. package/src/mind/mechanisms/confluence.ts +24 -0
  68. package/src/mind/mechanisms/cover.ts +5 -0
  69. package/src/mind/mechanisms/recall.ts +32 -4
  70. package/src/mind/mind.ts +125 -0
  71. package/src/mind/pipeline-mechanism.ts +7 -0
  72. package/src/mind/pipeline.ts +79 -22
  73. package/src/mind/primitives.ts +9 -1
  74. package/src/mind/rationale.ts +35 -1
  75. package/src/mind/reasoning.ts +145 -13
  76. package/src/mind/recognition.ts +4 -8
  77. package/src/mind/resonance.ts +19 -1
  78. package/src/mind/trace.ts +1 -0
  79. package/src/mind/traverse.ts +16 -6
  80. package/src/mind/types.ts +53 -4
  81. package/test/100-complete-grounding-trace.test.mjs +109 -0
  82. package/test/101-alignment-gap-bound.test.mjs +106 -0
  83. package/test/102-production-composes-at-scale.test.mjs +110 -0
  84. package/test/103-alignment-gap-budget.test.mjs +89 -0
  85. package/test/104-composition-is-reported.test.mjs +90 -0
  86. package/test/105-derive-through-reports-its-refusal.test.mjs +137 -0
  87. package/test/106-the-join-fires.test.mjs +94 -0
  88. package/test/107-the-join-is-counted.test.mjs +81 -0
  89. package/test/108-the-join-chains.test.mjs +78 -0
  90. package/test/109-the-pivot-is-counted.test.mjs +60 -0
  91. package/test/110-the-reasoner-stops-when-the-question-is-answered.test.mjs +91 -0
  92. package/test/111-the-cover-assembly-is-counted.test.mjs +74 -0
  93. package/test/112-the-exploration-does-not-grow-with-the-hub.test.mjs +89 -0
  94. package/test/113-the-rationale-payload-is-bounded.test.mjs +84 -0
  95. package/test/114-alignment-budget-is-per-sweep.test.mjs +93 -0
  96. package/test/116-the-extension-is-gated-by-the-pipelines-own-remainder.test.mjs +100 -0
  97. package/test/117-corpus-search.test.mjs +171 -0
  98. package/test/118-the-join-reaches-a-key-off-the-cut.test.mjs +74 -0
  99. package/test/119-the-work-does-not-grow-with-the-corpus.test.mjs +122 -0
  100. package/test/120-composition-is-consequence.test.mjs +132 -0
  101. package/test/121-the-extension-does-not-grow-with-the-corpus.test.mjs +128 -0
  102. package/test/122-the-climb-search-does-not-grow-with-the-corpus.test.mjs +117 -0
  103. package/test/123-the-paired-formulas-agree.test.mjs +90 -0
  104. package/test/125-the-post-grounding-branch-publishes-its-operand.test.mjs +51 -0
  105. package/test/126-the-pipeline-does-not-name-mechanisms.test.mjs +42 -0
  106. package/test/128-the-leads-somewhere-pair-agrees.test.mjs +83 -0
  107. package/test/129-the-trace-payload-shape.test.mjs +164 -0
  108. package/test/14-scaling.test.mjs +10 -7
  109. package/test/32-confluence.test.mjs +68 -0
  110. package/test/38-reason-restate-guard.test.mjs +8 -2
  111. package/test/43-cast-analog-seat.test.mjs +10 -0
  112. package/test/55-cost-meter.test.mjs +859 -0
  113. package/test/76-reference-binding.test.mjs +6 -1
  114. package/test/89-completion-recursion.test.mjs +30 -5
@@ -31,8 +31,8 @@
31
31
  // they gate.
32
32
  import { addInto, cosine, dot, normalize, zeros } from "../vec.js";
33
33
  import { conceptThreshold, dominates, identityBar, significanceBar, } from "../geometry.js";
34
- import { bytesEqual, indexOf } from "../bytes.js";
35
- import { chainReach, leafIdRun } from "./canonical.js";
34
+ import { bytesEqual, indexOf, latin1 } from "../bytes.js";
35
+ import { leafIdRun } from "./canonical.js";
36
36
  import { foldTree, gistOf, perceive, read, resolve } from "./primitives.js";
37
37
  import { argmaxCosine, chooseAmong, chooseNext, corpusN, edgeAncestors, guidedFirst, hubBound, hubCap, sharedReachMemo, } from "./traverse.js";
38
38
  import { recognise, segment } from "./recognition.js";
@@ -237,9 +237,14 @@ export function alignGraded(ctx, query, contextBytes, querySites) {
237
237
  }
238
238
  /** Extend a seed match (query offset qo ↔ candidate offset co) to its maximal
239
239
  * common run, then walk outward in both directions collecting further common
240
- * runs of at least W bytes across bounded mismatch gaps (each side ≤
241
- * chainReach). Returns the matched query spans and the mismatch pairs
242
- * between consecutive runs.
240
+ * runs of at least W bytes across mismatch gaps. Each gap's LENGTH is the
241
+ * pair's own extent (a gap cannot be longer than the bytes it spans) and the
242
+ * sweep's WORK is proportional to the bytes a run spans (the context's windows
243
+ * are indexed once, then the query's are walked) — the arity bound
244
+ * (`chainReach`) used to cap BOTH, and truncated every learned frame whose
245
+ * slot was longer. Each sweep owns its own budget, so an exhausted right
246
+ * sweep never starves the left one. Returns the matched query spans and the
247
+ * mismatch pairs between consecutive runs.
243
248
  *
244
249
  * This is the SEEDED aligner, distinct from {@link alignRuns}: that one finds
245
250
  * every run two structures share anywhere (a weave), this one reads two
@@ -253,7 +258,21 @@ export function alignGraded(ctx, query, contextBytes, querySites) {
253
258
  * window test); {@link frameSlots} takes the other reading. */
254
259
  export function alignAround(ctx, q, c, qo, co) {
255
260
  const W = ctx.space.maxGroup;
256
- const reachCap = chainReach(W);
261
+ // THE GAP LENGTH IS THE PAIR'S OWN EXTENT; THE WORK IS BUDGETED.
262
+ //
263
+ // The sweep walks (queryGap, contextGap) pairs by ASCENDING total, so reaching
264
+ // a gap of size G costs about G²/2 pairs. Bounding the LENGTH by the write
265
+ // side's arity (`chainReach(W)` = 16) therefore truncated every learned frame
266
+ // whose slot is longer — measured: `bindReference` reported the cap at 18, 24,
267
+ // 30 and 36 bytes and `recall` answered with ANOTHER instance's filler — while
268
+ // removing the bound outright took the corpus-cost guard (test/89) from
269
+ // milliseconds to 68 seconds.
270
+ //
271
+ // Bounding the PAIRS keeps a call's cost constant however long the pair is,
272
+ // and the ascending order means an exhausted budget drops the FAR
273
+ // continuations and never the near ones — the same degradation recognition.ts
274
+ // documents for its canon budget. Length and work are different questions;
275
+ // this is the one place they were conflated.
257
276
  // Maximal run around the seed.
258
277
  let qs = qo, ss = co;
259
278
  while (qs > 0 && ss > 0 && q[qs - 1] === c[ss - 1]) {
@@ -267,8 +286,66 @@ export function alignAround(ctx, q, c, qo, co) {
267
286
  }
268
287
  const matched = [[qs, qe]];
269
288
  const gaps = [];
270
- // The next common run of ≥ W bytes past (qi, si), with each side's gap
271
- // bounded by chainReach; smallest total gap wins (nearest continuation).
289
+ // THE SWEEP IS STRUCTURAL, NOT ENUMERATIVE.
290
+ //
291
+ // The criterion is unchanged: the next common run, MINIMUM TOTAL GAP, ties to
292
+ // the smaller query gap. What changed is how it is found. Enumerating
293
+ // (queryGap, contextGap) pairs by ascending total reaches a run at total t in
294
+ // about t²/2 pairs — and that quadratic shape, not the reach, was the cost
295
+ // problem: capping the pairs dropped reach (a legitimate 24-byte slot stopped
296
+ // being found), while leaving them uncapped cost 68 seconds on the corpus
297
+ // guard. Neither is the answer, because the answer is the algorithm.
298
+ //
299
+ // The context's windows are indexed ONCE, for lengths 1..W — W being the
300
+ // geometry's own unit of composition, so nothing is chosen here. Each step
301
+ // then walks the query's windows outward from the anchor: for a given query
302
+ // gap the nearest context gap that continues a run is one O(1) lookup, and the
303
+ // walk stops the moment the query gap alone exceeds the best total already
304
+ // found. So the work is proportional to the bytes the run SPANS. No budget,
305
+ // no cap, no number: a long slot is reached, and its price is already the
306
+ // ladder's (its bytes are unaccounted, so the search pays PASS per byte).
307
+ const index = [];
308
+ for (let len = 1; len <= W; len++) {
309
+ const m = new Map();
310
+ for (let o = 0; o + len <= c.length; o++) {
311
+ const key = latin1(c.subarray(o, o + len));
312
+ const at = m.get(key);
313
+ if (at === undefined)
314
+ m.set(key, [o]);
315
+ else
316
+ at.push(o);
317
+ }
318
+ index.push(m);
319
+ }
320
+ /** Smallest listed offset at or after `from`, or -1. */
321
+ const fromAt = (list, from) => {
322
+ let lo = 0, hi = list.length - 1, best = -1;
323
+ while (lo <= hi) {
324
+ const mid = (lo + hi) >> 1;
325
+ if (list[mid] >= from) {
326
+ best = list[mid];
327
+ hi = mid - 1;
328
+ }
329
+ else
330
+ lo = mid + 1;
331
+ }
332
+ return best;
333
+ };
334
+ /** Largest listed offset at or before `to`, or -1. */
335
+ const toAt = (list, to) => {
336
+ let lo = 0, hi = list.length - 1, best = -1;
337
+ while (lo <= hi) {
338
+ const mid = (lo + hi) >> 1;
339
+ if (list[mid] <= to) {
340
+ best = list[mid];
341
+ lo = mid + 1;
342
+ }
343
+ else
344
+ hi = mid - 1;
345
+ }
346
+ return best;
347
+ };
348
+ /** Length of the common run STARTING at (qi, si). */
272
349
  const runLenAt = (qi, si) => {
273
350
  let n = 0;
274
351
  while (qi + n < q.length && si + n < c.length && q[qi + n] === c[si + n]) {
@@ -276,69 +353,76 @@ export function alignAround(ctx, q, c, qo, co) {
276
353
  }
277
354
  return n;
278
355
  };
279
- // RIGHT sweep.
280
- let qi = qe, si = se;
281
- for (;;) {
282
- let found = false;
283
- for (let total = 1; total <= 2 * reachCap && !found; total++) {
284
- for (let gq = 0; gq <= Math.min(total, reachCap); gq++) {
285
- const gs = total - gq;
286
- if (gs > reachCap)
356
+ /** Length of the common run ENDING at (qi, si). */
357
+ const runLenBefore = (qi, si) => {
358
+ let n = 0;
359
+ while (n < qi && n < si && q[qi - 1 - n] === c[si - 1 - n])
360
+ n++;
361
+ return n;
362
+ };
363
+ /** The next run outward from an anchor, or null when the bytes run out. */
364
+ const nextRun = (qi, si, forward) => {
365
+ const qLim = forward ? q.length - qi : qi;
366
+ let best = null;
367
+ for (let gq = 0; gq < qLim; gq++) {
368
+ // No later query gap can beat a total already found.
369
+ if (best !== null && gq > best.gq + best.gs)
370
+ break;
371
+ const left = qLim - gq;
372
+ // A run of >= W bytes, or — when the query itself ends inside one window —
373
+ // the run that REACHES that end. Exactly the acceptance the sweep had.
374
+ const lens = left >= W ? [W] : [left];
375
+ for (const len of lens) {
376
+ const key = latin1(q.subarray(forward ? qi + gq : qi - gq - len, forward ? qi + gq + len : qi - gq));
377
+ const list = index[len - 1].get(key);
378
+ if (list === undefined)
287
379
  continue;
288
- if (qi + gq >= q.length || si + gs >= c.length)
380
+ const o = forward ? fromAt(list, si) : toAt(list, si - len);
381
+ if (o < 0)
289
382
  continue;
290
- const n = runLenAt(qi + gq, si + gs);
291
- if (n >= W || qi + gq + n === q.length) {
292
- if (n === 0)
293
- continue;
294
- if (gq > 0 || gs > 0) {
295
- gaps.push({ qs: qi, qe: qi + gq, cs: si, ce: si + gs });
383
+ const n = forward
384
+ ? runLenAt(qi + gq, o)
385
+ : runLenBefore(qi - gq, o + len);
386
+ if (n < 1)
387
+ continue;
388
+ if (forward ? n >= W || qi + gq + n === q.length : n >= W || n === qi - gq) {
389
+ const gs = forward ? o - si : si - len - o;
390
+ if (best === null || gq + gs < best.gq + best.gs) {
391
+ best = { gq, gs, n };
296
392
  }
297
- matched.push([qi + gq, qi + gq + n]);
298
- qi = qi + gq + n;
299
- si = si + gs + n;
300
- found = true;
301
393
  break;
302
394
  }
303
395
  }
304
396
  }
305
- if (!found)
397
+ return best;
398
+ };
399
+ // RIGHT sweep.
400
+ let qi = qe, si = se;
401
+ for (;;) {
402
+ const step = nextRun(qi, si, true);
403
+ if (step === null)
306
404
  break;
405
+ if (step.gq > 0 || step.gs > 0) {
406
+ gaps.push({ qs: qi, qe: qi + step.gq, cs: si, ce: si + step.gs });
407
+ }
408
+ matched.push([qi + step.gq, qi + step.gq + step.n]);
409
+ qi = qi + step.gq + step.n;
410
+ si = si + step.gs + step.n;
307
411
  }
308
- // LEFT sweep (mirror).
412
+ // LEFT sweep (mirror): an independent walk, so an exhausted right side can
413
+ // never starve it (pinned by test/114).
309
414
  qi = qs;
310
415
  si = ss;
311
416
  for (;;) {
312
- let found = false;
313
- for (let total = 1; total <= 2 * reachCap && !found; total++) {
314
- for (let gq = 0; gq <= Math.min(total, reachCap); gq++) {
315
- const gs = total - gq;
316
- if (gs > reachCap)
317
- continue;
318
- if (qi - gq <= 0 || si - gs <= 0)
319
- continue;
320
- // Run ENDING at (qi - gq, si - gs).
321
- let n = 0;
322
- while (n < qi - gq && n < si - gs &&
323
- q[qi - gq - 1 - n] === c[si - gs - 1 - n]) {
324
- n++;
325
- }
326
- if (n >= W || n === qi - gq) {
327
- if (n === 0)
328
- continue;
329
- if (gq > 0 || gs > 0) {
330
- gaps.push({ qs: qi - gq, qe: qi, cs: si - gs, ce: si });
331
- }
332
- matched.push([qi - gq - n, qi - gq]);
333
- qi = qi - gq - n;
334
- si = si - gs - n;
335
- found = true;
336
- break;
337
- }
338
- }
339
- }
340
- if (!found)
417
+ const step = nextRun(qi, si, false);
418
+ if (step === null)
341
419
  break;
420
+ if (step.gq > 0 || step.gs > 0) {
421
+ gaps.push({ qs: qi - step.gq, qe: qi, cs: si - step.gs, ce: si });
422
+ }
423
+ matched.push([qi - step.gq - step.n, qi - step.gq]);
424
+ qi = qi - step.gq - step.n;
425
+ si = si - step.gs - step.n;
342
426
  }
343
427
  return { matched, gaps };
344
428
  }
@@ -462,7 +546,7 @@ export function frameSlots(ctx, query, cand, id) {
462
546
  * ANCHOR that the query displaced. Neither implies the other, and the
463
547
  * observed failures pass the restatement guard cleanly.
464
548
  *
465
- * Three conditions, all byte-exact and all necessary:
549
+ * Four conditions, all byte-exact and all necessary:
466
550
  *
467
551
  * 1. the query and the anchor must be ONE STRUCTURE — what they share has to
468
552
  * dominate the query, or the query is not a variant of the anchor at all
@@ -547,8 +631,10 @@ export function substituteAll(hay, pairs) {
547
631
  if (usable.length === 0)
548
632
  return hay;
549
633
  // Longest needle first, so a needle that is a prefix of another can never
550
- // pre-empt it. Ties cannot arise: an instance whose fillers are not
551
- // pairwise distinct is refused by frameSlots.
634
+ // pre-empt it. Ties cannot arise: a consumer that VOICES checks the
635
+ // fillers pairwise with `distinct` and refuses such an instance itself —
636
+ // `frameSlots` reports and does not judge (see its own doc), so the refusal
637
+ // lives with the mechanism that needs it, not here.
552
638
  const order = [...usable].sort((a, b) => b.needle.length - a.needle.length);
553
639
  const out = [];
554
640
  let i = 0;
@@ -16,7 +16,7 @@ import { analogyStrength, follow, project, reverseContext, sharedFrameStrengthOf
16
16
  import { joinWithBridge } from "../resonance.js";
17
17
  import { restatesQuery } from "../reasoning.js";
18
18
  import { CONCEPT, STEP } from "../graph-search.js";
19
- import { concat2, indexOf } from "../../bytes.js";
19
+ import { indexOf } from "../../bytes.js";
20
20
  import { consensusFloor, dominates } from "../../geometry.js";
21
21
  import { unexplainedLabel, unexplainedSpans, } from "../rationale.js";
22
22
  import { rItem, rNode } from "../trace.js";
@@ -523,7 +523,23 @@ export async function counterfactualTransfer(ctx, query, pre) {
523
523
  const fwd = await follow(ctx, proj.anchor, qv);
524
524
  if (fwd !== null && indexOf(answer, fwd, 0) < 0 &&
525
525
  !restatesQuery(query, fwd)) {
526
- answer = concat2(answer, fwd);
526
+ // THROUGH THE SHARED JOINER, not a bare concatenation.
527
+ //
528
+ // `joinWithBridge` is the composition step every out-of-search assembly
529
+ // shares (multi-topic fusion, CAST's substitution and comparison): it
530
+ // asks the corpus for a learnt connector between the pieces and, on a
531
+ // miss, joins them BARE **and says so** — the `bridgeMiss` step (see
532
+ // resonance.ts). This site bypassed it, and that is the whole of the
533
+ // gluing the study measured: `"Steel is hard"` + `"wet"` came back as
534
+ // `"hardwet"`, `"eva director father"` + `"The father of…"` as
535
+ // `"fatherThe"` — compositions no rationale could show, because the one
536
+ // step that made them left no trace.
537
+ //
538
+ // Routing it through the shared joiner is the instrumentation fix that
539
+ // comes first: a bare join stays possible (the house rule is "joined
540
+ // bare, never silent") but it is now VISIBLE, and an attested connector
541
+ // is used when the corpus has one.
542
+ answer = await joinWithBridge(ctx, answer, fwd);
527
543
  }
528
544
  ctx.trace?.step("projectCounterfactual", [
529
545
  rItem(filler, "filler", subj.point.anchor),
@@ -715,7 +731,7 @@ export async function counterfactualTransfer(ctx, query, pre) {
715
731
  // grounds") — fine for ORIENTING mechanisms, not for voicing learnt
716
732
  // content the query never asked about. Computed once here; both the
717
733
  // hub fallback below and the comparison gate consume it.
718
- const rootTrusted = roots.some((r) => r.vote >= consensusFloor(corpusN(ctx)));
734
+ const rootTrusted = roots.some((r) => r.idfVote >= consensusFloor(corpusN(ctx))); // the IDF sum: the bar's own quantity
719
735
  // The context that ESTABLISHES a filler — the same reverse context, under
720
736
  // the same naming test, `seatOfNode` uses to VOICE an analog (a predecessor
721
737
  // whose bytes CONTAIN the node's: it names or describes it, rather than
@@ -98,6 +98,30 @@ export async function confluenceJoin(ctx, query, pre) {
98
98
  // constraints). Shard-bound streams are no constraints, and their meets
99
99
  // are connective debris (". Sure,", "ngul" — observed).
100
100
  const bindsAConstituent = (cover) => cover.some(([cs, ce]) => ce - cs >= 2 * W);
101
+ // THE VOTE ENTERS AS ORDER, NEVER AS A BAR. This is the only one of the
102
+ // climb's four consumers (recall, fuseAttention, cast, here) that uses the
103
+ // evidence's MAGNITUDE without a floor, and it is legitimate by construction:
104
+ // `ranked` answers "which anchor is stronger" — a question about votes, so the
105
+ // comparison stays within one dimension — and the vote is otherwise only
106
+ // REPORTED (Stream.vote travels to the rationale's constraint nodes). What
107
+ // actually SELECTS a constraint is byte-structural and never the magnitude: a
108
+ // run of at least 2W (`bindsAConstituent`, with its accidental-sharing
109
+ // counter-examples above), disjoint covers (`disjoint`), and scaffolding never
110
+ // binds at all (`dominates(reachOf(…), N)`). The MEET such a stream may
111
+ // produce is selected the same way: a span shorter than 2W is rejected, and
112
+ // the winner is the one with the smallest `reach` (ties broken by the longer
113
+ // span) — a corpus quantity and bytes, never the vote, which appears only in
114
+ // the trace item.
115
+ // binds at all (`dominates(reachOf(…), N)`). The only cut in this loop is a
116
+ // BUDGET, and it is measured: stopping the scan at 2W anchors saves 50-70% of
117
+ // confluence's cost on non-conjunctive queries while preserving every genuinely
118
+ // conjunctive case, whose top anchors ARE its constraints.
119
+ // MEASURED (this goal, on THIS file's own conjunctive fixture): the two
120
+ // streams appear at ranks 1 and 4 against a budget of 2W = 8, on a query whose
121
+ // `ranked` is 9 — so the cut IS live (it would have returned null at the 8th
122
+ // anchor) and it does NOT prune the case it exists to protect. The other
123
+ // conjunctive fixture (the Leonardo one) finds them at ranks 0 and 1. Scope:
124
+ // these are the repo's conjunctive fixtures, and no more.
101
125
  const streams = [];
102
126
  const rankedCapped = ranked.length > pre.k ? ranked.slice(0, pre.k) : ranked;
103
127
  // CONJUNCTIVITY EARLY-EXIT: a conjunctive query's top-ranked anchors
@@ -83,6 +83,8 @@ export async function resolveConnectors(ctx, sites, query) {
83
83
  const bridgePair = async (l, r) => {
84
84
  if (l === r || links.has(l + "," + r))
85
85
  return;
86
+ if (ctx.meter)
87
+ ctx.meter.coverBridges++;
86
88
  const link = await bridge(ctx, read(ctx, l), read(ctx, r));
87
89
  if (link !== null)
88
90
  links.set(l + "," + r, link);
@@ -122,6 +124,10 @@ export async function resolveConnectors(ctx, sites, query) {
122
124
  // plus one W-quantum of glue per joint — pass that allowance so the
123
125
  // bridge's phrase-scale cap admits the whole learnt run.
124
126
  const allowance = middleBytes + (m + 1) * W;
127
+ if (ctx.meter) {
128
+ ctx.meter.coverBridges++;
129
+ ctx.meter.coverAllowanceBytes += allowance;
130
+ }
125
131
  const interior = await bridge(ctx, first.bytes, orderedNodes[m].bytes, allowance);
126
132
  if (interior !== null)
127
133
  links.set(key, interior);
@@ -194,9 +194,36 @@ export async function recallByResonance(ctx, query, pre) {
194
194
  // consensus", while breadth is the SCALE-INVARIANT reading — "a point whose
195
195
  // breadth clears `dominates` (> half the query's regions corroborate it) is
196
196
  // real consensus; one that does not is a coincidental single-region echo".
197
- // Attention.peak's contract makes the same point from the other side:
198
- // comparing a POOLED SUM against a floor that prices ONE region's evidence
199
- // is a dimensional error.
197
+ // THIS USED TO CLAIM A DIMENSIONAL ERROR, AND THAT CLAIM WAS FALSE.
198
+ // It read: "comparing a POOLED SUM against a floor that prices ONE region's
199
+ // evidence is a dimensional error." `consensusFloor` is not priced for one
200
+ // region: thresholds.md §2 derives it as the POOLED-vote significance floor —
201
+ // "each region contributes at most ln(N/c) <= ln(N); ln(N)+1/2 demands ..." —
202
+ // and attention.ts says the same where it builds the vote ("the scale
203
+ // consensusFloor is derived for"). The comparison is in ONE dimension, and
204
+ // it is so because the climb WEIGHTS BY IDF: `wf` in voteRegions is
205
+ // `direct ? df : combined ? idf + df : idf`, and the engine only ever runs the
206
+ // last one (DFMode's default "inverse", the mode every non-test caller uses —
207
+ // `direct` and `combined` are exercised by test/24 and test/27 only, and
208
+ // test/24 pins that their votes DO differ). In those two the sum would leave
209
+ // the floor's dimension and the floor would need re-deriving.
210
+ //
211
+ // What the OR below is really for is SCALE, not dimension (the paragraph
212
+ // above says it): a vote that clears ln(N)+1/2 means "strong" on a small store
213
+ // and "weak" on a large one for the same genuine consensus, so the
214
+ // scale-invariant breadth reading is added beside it.
215
+ //
216
+ // AND THE PREMISE IS IDF. The deviation in the other two weighting modes is
217
+ // TWO-SIDED and DERIVED: `direct` DEFLATES a region (ln(1+c) < ln(N/c) for
218
+ // small c) and `combined` INFLATES it (ln N + ln(1+1/c)), both by at most
219
+ // `ln 2` — see `geometry.ts`'s `consensusFloor`, where the bound lives.
220
+ // MEASURED on 8 anchors across 5 queries, running the same climb in all
221
+ // three modes: ZERO gate inversions — every anchor's `vote >= floor` verdict
222
+ // is the same in `inverse`, `direct` and `combined`, even where the readings
223
+ // straddle the floor on opposite sides (#148: inverse 3.39, combined 4.71
224
+ // above it, direct 1.31 below). Pinned by test/55's test 20. The bar is not
225
+ // re-derived for those modes because nothing reachable needs it; the premise
226
+ // is IDF, and that is now written where the gate reads it.
200
227
  //
201
228
  // Measured on the 15.7M-node store (N=325,615, so the old floor was 13.19).
202
229
  // The absolute vote cannot separate right from wrong at this scale, and the
@@ -265,7 +292,7 @@ export async function recallByResonance(ctx, query, pre) {
265
292
  const minVote = consensusFloor(corpusN(ctx));
266
293
  if (forest.length > 0 &&
267
294
  !allWindowsAreScaffolding(ctx, query) &&
268
- (forest[0].vote >= minVote ||
295
+ (forest[0].idfVote >= minVote || // the IDF sum: the bar's own quantity
269
296
  (dominates(forest[0].breadth, 1) && forest[0].peak > Math.LN2))) {
270
297
  const g = await project(ctx, forest[0].anchor, queryGist);
271
298
  // THE ANCHOR'S OCCUPANT IS NOT THE ASKER'S. This tier grounds an anchor
@@ -468,6 +495,7 @@ export const recallMechanism = {
468
495
  moves: r.moves,
469
496
  unexplained: r.unexplained,
470
497
  provenance: r.echoed ? "recall-echo" : "recall",
498
+ used: new Set(),
471
499
  ...(r.complete ? { complete: true } : {}),
472
500
  }];
473
501
  },
@@ -1,5 +1,6 @@
1
1
  import { Vec } from "../vec.js";
2
2
  import { Sema, Space } from "../sema.js";
3
+ import type { CorpusResult } from "./corpus.js";
3
4
  import { Alphabet } from "../alphabet.js";
4
5
  import { Grid } from "../geometry.js";
5
6
  import { BoundedMap, type Store } from "../store.js";
@@ -49,10 +50,42 @@ import type { AttentionRead, MindContext, Recognition } from "./types.js";
49
50
  export type { AnchorRejectionReason, ClimbConsensusData, ConsensusAnchorTrace, ConsensusReachTrace, ConsensusRegionTrace, CrossRegionTier, JunctionVoteTrace, RegionOutcome, } from "./attention.js";
50
51
  export type { AncestorReach, SaturationReason, SaturationStop, } from "./types.js";
51
52
  import { type CostReport, Meter } from "../meter.js";
53
+ /** A stored pair as TEXT — the text helper's view of {@link CorpusPair}. */
54
+ export interface CorpusTextPair {
55
+ context: string;
56
+ continuation: string;
57
+ contextId: number;
58
+ continuationId: number;
59
+ matchedBytes: number;
60
+ contextTruncated: boolean;
61
+ continuationTruncated: boolean;
62
+ }
63
+ /** {@link CorpusResult} as text, plus the prose for why nothing matched. The
64
+ * byte layer reports a STATE; saying it in words belongs to the text layer. */
65
+ export interface CorpusTextResult {
66
+ query: string;
67
+ pairs: CorpusTextPair[];
68
+ resolved: number;
69
+ reached: number;
70
+ totalContexts: number;
71
+ browsed: boolean;
72
+ note?: string;
73
+ }
52
74
  export interface MindOptions {
53
75
  seed?: number;
54
76
  recallQueryK?: number;
55
77
  haloQueryK?: number;
78
+ /** Branch nodes the pivot sweep may probe — see {@link MindConfig}. */
79
+ pivotProbeK?: number;
80
+ /** Items one rationale step may itemise — see {@link MindConfig}. */
81
+ rationaleSampleK?: number;
82
+ /** Corpus-reading capacities and budgets — see {@link MindConfig}. */
83
+ corpusLimitMax?: number;
84
+ corpusClimbs?: number;
85
+ corpusContextsPerClimb?: number;
86
+ corpusSampleProbes?: number;
87
+ corpusPreviewBytes?: number;
88
+ corpusSampleFloorBytes?: number;
56
89
  normalizeEpsilon?: number;
57
90
  cosineEpsilon?: number;
58
91
  geometry?: Partial<import("../config.js").GeometryConfig>;
@@ -153,6 +186,10 @@ export declare class Mind implements MindContext {
153
186
  * `traverse.ts`'s ONE definition (edge or halo, with its response-scoped
154
187
  * cache). The search holds a bare Store and cannot reach that cache itself,
155
188
  * so it asks through this hook; a bare host keeps its raw-store fallback. */
189
+ /** The canonical identity for the search (see GraphSearchHost). */
190
+ canonResolve(bytes: Uint8Array): number | null;
191
+ /** Feed a search refusal into the rationale (see GraphSearchHost). */
192
+ reportSearch(name: string, parts: ReadonlyArray<Uint8Array>, note: string): void;
156
193
  leadsSomewhere(id: number): boolean;
157
194
  recogniseSpan(bytes: Uint8Array): {
158
195
  sites: ReadonlyArray<Site>;
@@ -167,6 +204,8 @@ export declare class Mind implements MindContext {
167
204
  * with the most distributional evidence (highest `prevOf` count — the
168
205
  * structural manifestation of its halo). When evidence is equal the
169
206
  * first-inserted edge wins. */
207
+ /** See {@link GraphSearchHost.contentKeyEnds}. */
208
+ contentKeyEnds(prefix: Uint8Array, tail: Uint8Array): readonly number[];
170
209
  chooseNext(node: number): number | undefined;
171
210
  constructor(opts?: MindOptions);
172
211
  constructor(cfg: MindConfig, store: Store, _fromStore: true);
@@ -249,6 +288,24 @@ export declare class Mind implements MindContext {
249
288
  * as one form, provided the store's canon index is built
250
289
  * ({@link buildCanonIndex}). */
251
290
  respondText(input: string, inspectRationale?: InspectRationale): Promise<string>;
291
+ /** Which stored notes does this query REACH? BYTES in, BYTES out — this
292
+ * method has no notion of text or encoding; the text case is
293
+ * {@link searchCorpusText}, which is one caller of this.
294
+ *
295
+ * Exact content addressing through the machinery an answer already uses
296
+ * (see src/mind/corpus.ts): the query's recognised sites are the resolved
297
+ * subtrees, the climb goes up from the biggest, and a result is a context
298
+ * that carries a learnt continuation. Nothing is written and nothing is
299
+ * indexed. */
300
+ searchCorpus(queryBytes: Uint8Array, limit?: number): CorpusResult;
301
+ /** Browse real pairs. Deterministic: `from` is the caller's own offset in
302
+ * [0,1), so browsing twice with different offsets shows different notes
303
+ * without a random draw. */
304
+ sampleCorpus(limit?: number, from?: number): CorpusResult;
305
+ /** The TEXT case of {@link searchCorpus}: encode, search, decode. The search
306
+ * itself exists once, in the byte layer above; only the rendering lives
307
+ * here, with the rest of this class's text modality. */
308
+ searchCorpusText(query: string, limit?: number): CorpusTextResult;
252
309
  /** Begin a new conversation, optionally restoring from a previously-saved
253
310
  * {@link ConversationState}. The returned handle is required for
254
311
  * {@link respondTurn} and {@link endConversation}.
@@ -9,8 +9,10 @@
9
9
  // Architecture: 4 primitives × 2 patterns = all inference.
10
10
  // Implementation split across src/mind/*.ts — this file assembles the Mind class.
11
11
  import { makeKeyring, rng, setVecConfig } from "../vec.js";
12
+ import { sampleCorpus, searchCorpus } from "./corpus.js";
12
13
  import { Alphabet } from "../alphabet.js";
13
14
  import { contentFoldIncremental, reachThreshold, } from "../geometry.js";
15
+ import { keyEnds } from "./canonical.js";
14
16
  import { BoundedMap } from "../store.js";
15
17
  import { SQliteStore } from "../store-sqlite.js";
16
18
  import { resolveConfig } from "../config.js";
@@ -19,7 +21,7 @@ import { bytesEqual, concat2 } from "../bytes.js";
19
21
  import { GraphSearch, } from "./graph-search.js";
20
22
  import { Alu } from "../alu/src/index.js";
21
23
  import { decodeText, Rationale, } from "./rationale.js";
22
- import { gistOf, inputBytes, perceive as perceiveImpl, perceiveKey, resolve as resolveImpl, } from "./primitives.js";
24
+ import { canonResolve as canonResolveImpl, gistOf, inputBytes, perceive as perceiveImpl, perceiveKey, resolve as resolveImpl, } from "./primitives.js";
23
25
  import { chooseNext, edgeAncestors as edgeAncestorsFn, invalidateStructuralCaches, leadsSomewhere, } from "./traverse.js";
24
26
  import { invalidateJunctionCache } from "./junction.js";
25
27
  import { follow } from "./match.js";
@@ -33,6 +35,21 @@ import { rItem } from "./trace.js";
33
35
  // The work meter is exported from src/index.ts (via src/meter.ts) — the one
34
36
  // definition; the Mind only consumes it.
35
37
  import { Meter } from "../meter.js";
38
+ /** What the text helper says when the byte layer reports a miss. */
39
+ const CORPUS_NOTE = {
40
+ "nothing-resolved": "No trained note sits above the parts of that text the mind recognised. " +
41
+ "It addresses content exactly, so try wording closer to something it was " +
42
+ "actually given — or browse the examples instead.",
43
+ "no-continuations": "That text reaches stored nodes, but none of them carries a learnt " +
44
+ "continuation.",
45
+ };
46
+ /** UTF-8 of bytes for display: reuse {@link decodeText} (the mind's own text
47
+ * conversion), then drop the replacement character a byte-boundary cut leaves
48
+ * behind. Much of a real corpus is non-Latin, so that trailing U+FFFD is the
49
+ * common case, not an exotic one — and it is the ONLY thing added here. */
50
+ function previewCorpusText(bytes) {
51
+ return decodeText(bytes).replace(/\uFFFD+$/, "").replace(/\s+/g, " ").trim();
52
+ }
36
53
  // ═══════════════════════════════════════════════════════════════════════════
37
54
  // THE MIND
38
55
  // ═══════════════════════════════════════════════════════════════════════════
@@ -116,6 +133,14 @@ export class Mind {
116
133
  * `traverse.ts`'s ONE definition (edge or halo, with its response-scoped
117
134
  * cache). The search holds a bare Store and cannot reach that cache itself,
118
135
  * so it asks through this hook; a bare host keeps its raw-store fallback. */
136
+ /** The canonical identity for the search (see GraphSearchHost). */
137
+ canonResolve(bytes) {
138
+ return canonResolveImpl(this, bytes);
139
+ }
140
+ /** Feed a search refusal into the rationale (see GraphSearchHost). */
141
+ reportSearch(name, parts, note) {
142
+ this.trace?.step(name, parts.map((b) => rItem(b)), [], note);
143
+ }
119
144
  leadsSomewhere(id) {
120
145
  return leadsSomewhere(this, id);
121
146
  }
@@ -136,6 +161,10 @@ export class Mind {
136
161
  * with the most distributional evidence (highest `prevOf` count — the
137
162
  * structural manifestation of its halo). When evidence is equal the
138
163
  * first-inserted edge wins. */
164
+ /** See {@link GraphSearchHost.contentKeyEnds}. */
165
+ contentKeyEnds(prefix, tail) {
166
+ return keyEnds(this, prefix, tail);
167
+ }
139
168
  chooseNext(node) {
140
169
  return chooseNext(this, node, this._edgeGuide);
141
170
  }
@@ -411,6 +440,48 @@ export class Mind {
411
440
  const r = await this.respond(input, inspectRationale);
412
441
  return decodeText(r.bytes);
413
442
  }
443
+ // ── Reading the trained memory back ─────────────────────────────────────
444
+ /** Which stored notes does this query REACH? BYTES in, BYTES out — this
445
+ * method has no notion of text or encoding; the text case is
446
+ * {@link searchCorpusText}, which is one caller of this.
447
+ *
448
+ * Exact content addressing through the machinery an answer already uses
449
+ * (see src/mind/corpus.ts): the query's recognised sites are the resolved
450
+ * subtrees, the climb goes up from the biggest, and a result is a context
451
+ * that carries a learnt continuation. Nothing is written and nothing is
452
+ * indexed. */
453
+ searchCorpus(queryBytes, limit) {
454
+ return searchCorpus(this, queryBytes, limit);
455
+ }
456
+ /** Browse real pairs. Deterministic: `from` is the caller's own offset in
457
+ * [0,1), so browsing twice with different offsets shows different notes
458
+ * without a random draw. */
459
+ sampleCorpus(limit, from) {
460
+ return sampleCorpus(this, limit, from);
461
+ }
462
+ /** The TEXT case of {@link searchCorpus}: encode, search, decode. The search
463
+ * itself exists once, in the byte layer above; only the rendering lives
464
+ * here, with the rest of this class's text modality. */
465
+ searchCorpusText(query, limit) {
466
+ const result = this.searchCorpus(new TextEncoder().encode(query), limit);
467
+ return {
468
+ query,
469
+ pairs: result.pairs.map((p) => ({
470
+ context: previewCorpusText(p.context),
471
+ continuation: previewCorpusText(p.continuation),
472
+ contextId: p.contextId,
473
+ continuationId: p.continuationId,
474
+ matchedBytes: p.matchedBytes,
475
+ contextTruncated: p.contextTruncated,
476
+ continuationTruncated: p.continuationTruncated,
477
+ })),
478
+ resolved: result.resolved,
479
+ reached: result.reached,
480
+ totalContexts: result.totalContexts,
481
+ browsed: result.browsed,
482
+ note: result.miss === "matched" ? undefined : CORPUS_NOTE[result.miss],
483
+ };
484
+ }
414
485
  // ── Conversation API ────────────────────────────────────────────────────
415
486
  /** Begin a new conversation, optionally restoring from a previously-saved
416
487
  * {@link ConversationState}. The returned handle is required for
@@ -151,6 +151,13 @@ export interface MechanismResult {
151
151
  bytes: Uint8Array;
152
152
  accounted: Array<[number, number]>;
153
153
  moves: number;
154
+ /** WHAT THIS ANSWER SPEAKS FOR — the anchors it voices, and therefore the
155
+ * content the reasoner must not pivot back through. Declared by the
156
+ * mechanism about its OWN result, exactly like `accounted`/`unexplained`/
157
+ * `complete`: post-grounding honours the property and NEVER ASKS WHICH
158
+ * MECHANISM SET IT, so the market stays uniform. An EMPTY set is a real
159
+ * declaration — "this answer voices nothing" (recall) — and withholds
160
+ * nothing; omit the field and the pipeline re-recognises the answer. */
154
161
  used?: ReadonlySet<number>;
155
162
  unexplained: string;
156
163
  /** Explicit weight override. When absent, weight = moves + PASS·unaccounted. */