@hviana/sema 0.6.0 → 0.7.2

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 (42) hide show
  1. package/.github/workflows/release.yml +80 -0
  2. package/AGENTS.md +53 -9
  3. package/HOW_IT_WORKS.md +17 -16
  4. package/dist/src/meter.d.ts +14 -4
  5. package/dist/src/meter.js +27 -3
  6. package/dist/src/mind/attention.js +22 -20
  7. package/dist/src/mind/graph-search.d.ts +43 -9
  8. package/dist/src/mind/graph-search.js +82 -15
  9. package/dist/src/mind/junction.d.ts +13 -0
  10. package/dist/src/mind/junction.js +26 -1
  11. package/dist/src/mind/mechanisms/cover.js +23 -2
  12. package/dist/src/mind/mechanisms/prefix-completion.d.ts +2 -1
  13. package/dist/src/mind/mechanisms/prefix-completion.js +40 -20
  14. package/dist/src/mind/mechanisms/recall.js +8 -4
  15. package/dist/src/mind/pipeline-mechanism.d.ts +0 -24
  16. package/dist/src/mind/pipeline-mechanism.js +13 -36
  17. package/dist/src/mind/pipeline.d.ts +24 -0
  18. package/dist/src/mind/pipeline.js +71 -5
  19. package/dist/src/mind/recognition.d.ts +15 -1
  20. package/dist/src/mind/recognition.js +15 -1
  21. package/dist/src/mind/resonance.js +54 -12
  22. package/dist/src/store.js +22 -1
  23. package/jsr.json +1 -1
  24. package/package.json +7 -2
  25. package/src/meter.ts +27 -4
  26. package/src/mind/attention.ts +22 -19
  27. package/src/mind/graph-search.ts +93 -16
  28. package/src/mind/junction.ts +25 -1
  29. package/src/mind/mechanisms/cover.ts +23 -4
  30. package/src/mind/mechanisms/prefix-completion.ts +40 -20
  31. package/src/mind/mechanisms/recall.ts +8 -4
  32. package/src/mind/pipeline-mechanism.ts +13 -42
  33. package/src/mind/pipeline.ts +106 -5
  34. package/src/mind/recognition.ts +19 -2
  35. package/src/mind/resonance.ts +84 -49
  36. package/src/store.ts +21 -1
  37. package/test/89-completion-recursion.test.mjs +230 -0
  38. package/test/90-connector-read-cap.test.mjs +130 -0
  39. package/test/91-branch-bytes-cache.test.mjs +152 -0
  40. package/test/93-regime-prediction.test.mjs +148 -0
  41. package/test/94-cross-region-budget.test.mjs +67 -0
  42. package/test/95-wide-resonance-removed.test.mjs +109 -0
package/dist/src/store.js CHANGED
@@ -723,7 +723,28 @@ export class AbstractStore {
723
723
  parts.push(child);
724
724
  got += child.length;
725
725
  }
726
- return concat(parts);
726
+ const out = concat(parts);
727
+ // Cache the BRANCH too, not just the leaf above. Reconstruction is a pure
728
+ // function of the store, so this is a transparent cache in the strict sense
729
+ // — an eviction costs a re-walk and nothing else — which is exactly what
730
+ // `_bytesCache`'s "smallest"/"clock" configuration is for.
731
+ //
732
+ // Caching only leaves made every branch re-walk its whole subtree on every
733
+ // request, and the DAG is hash-consed, so the same children recur under many
734
+ // parents. Measured on the 18.9M-node store, ONE 1,314-byte query:
735
+ // 20,021,474 `_prefix` calls over 469,083 distinct ids (42.7x reuse) to
736
+ // produce 87,789 results — 97.7% of the work re-derived bytes it had already
737
+ // built. One single-byte leaf was reconstructed 2,599,984 times. Measuring
738
+ // reuse at the TOP level only shows 1.1x and hides all of it.
739
+ //
740
+ // Only a COMPLETE reconstruction may be cached: `_prefix` is also called
741
+ // with a cap, and a truncated prefix stored under `id` would be served as
742
+ // if it were the node's whole content by the `_bytesCache` hit above.
743
+ // `got < maxLen` is that proof — the walk ran out of children before it ran
744
+ // out of budget, so nothing below was truncated either.
745
+ if (got < maxLen)
746
+ this._bytesCache.set(id, out);
747
+ return out;
727
748
  }
728
749
  contentLen(id, cap = Infinity) {
729
750
  if (this.meter)
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://jsr.io/schema/config-file.v1.json",
3
3
  "name": "@hviana/sema",
4
- "version": "0.6.0",
4
+ "version": "0.7.2",
5
5
  "exports": "./src/index.ts"
6
6
  }
package/package.json CHANGED
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "name": "@hviana/sema",
3
- "version": "0.6.0",
3
+ "version": "0.7.2",
4
4
  "description": "Sema: a non-parametric, instance-based reasoning system.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/hviana/sema.git"
8
+ },
5
9
  "type": "module",
6
10
  "main": "dist/src/index.js",
7
11
  "types": "dist/src/index.d.ts",
@@ -14,7 +18,8 @@
14
18
  "scripts": {
15
19
  "build": "tsc",
16
20
  "demo": "tsc && node dist/example/demo.js",
17
- "test": "tsc && node --test test/**/*.test.mjs"
21
+ "test": "tsc && node --test test/**/*.test.mjs",
22
+ "prepublishOnly": "npm test"
18
23
  },
19
24
  "license": "PolyForm-Noncommercial-1.0.0",
20
25
  "devDependencies": {
package/src/meter.ts CHANGED
@@ -32,7 +32,7 @@
32
32
  * PHASES NEST, AND ARE NOT DISJOINT. `think` contains every mechanism
33
33
  * phase; a mechanism's `floor` contains whatever shared analysis it
34
34
  * first-touched (`attention`, `weave`); `recall.run` contains
35
- * `substitutionBridge`, which contains `recall.exhaustiveResonate`. Read a
35
+ * `substitutionBridge`. Read a
36
36
  * phase as "wall-clock spent inside this, inclusive" — never sum them and
37
37
  * expect the total. `CostReport.elapsedMs` is the only whole.
38
38
  *
@@ -176,6 +176,13 @@ export class Meter {
176
176
  /** Nodes popped by those ascents, against their √N·W budget — the counter
177
177
  * that shows whether the walks are deciding early or burning the budget. */
178
178
  junctionPops = 0;
179
+ /** Ascents that ended by EXHAUSTING the expansion budget rather than by
180
+ * deciding — the walk abstained and the caller silently fell through to a
181
+ * lower tier of the ladder (§2.13: a degradation nothing else reports).
182
+ * It rises the moment a SHARED budget is drained by an earlier walk, which
183
+ * is what makes "this tier answered nothing" distinguishable from "this
184
+ * tier never got to look". */
185
+ junctionBudgetExhausted = 0;
179
186
  /** Arbitrary byte spans whose distributional company was VSA-bundled from
180
187
  * existing episode halos. */
181
188
  spanHalos = 0;
@@ -198,9 +205,6 @@ export class Meter {
198
205
  mechanismRuns = 0;
199
206
  /** Candidates the decider weighed. */
200
207
  candidates = 0;
201
- /** Candidates refused before the competition for explaining less than 1/W
202
- * of the query — the honesty-density floor (see pipeline.ts `consider`). */
203
- thinRejects = 0;
204
208
 
205
209
  // ── Phases ──────────────────────────────────────────────────────────────
206
210
 
@@ -234,6 +238,25 @@ export class Meter {
234
238
  }
235
239
  }
236
240
 
241
+ /** Time one SYNCHRONOUS phase. The sync/async seam (§2.10) is a real
242
+ * contract — perception, recognition and the graph search are synchronous —
243
+ * so a synchronous layer must not be wrapped in `time`'s promise just to be
244
+ * measured: that would make the profiled path await where the unprofiled
245
+ * one does not, and a meter never changes what a layer computes. */
246
+ timeSync<T>(phase: string, fn: () => T): T {
247
+ const before = this.snapshot();
248
+ const t = performance.now();
249
+ try {
250
+ return fn();
251
+ } finally {
252
+ const ms = performance.now() - t;
253
+ const after = this.snapshot();
254
+ const delta: Record<string, number> = {};
255
+ for (const k of Object.keys(after)) delta[k] = after[k] - before[k];
256
+ this.charge(phase, ms, delta);
257
+ }
258
+ }
259
+
237
260
  /** Time one async phase and attribute the work done inside it. Returns
238
261
  * the awaited value untouched — a meter never changes what a layer
239
262
  * computes, only what is known about it. */
@@ -45,6 +45,7 @@ import {
45
45
  import { recognise } from "./recognition.js";
46
46
  import { leafIdRun } from "./canonical.js";
47
47
  import {
48
+ atomIsHub,
48
49
  corpusN,
49
50
  edgeAncestors,
50
51
  hubBound,
@@ -2647,26 +2648,28 @@ async function crossRegionVotes(
2647
2648
  // the same container (or a sub-container of it) twice.
2648
2649
  const consumed = new Set<number>();
2649
2650
  let probes = 0;
2650
- // Once atoms themselves are hubs (N > W²), the cross-region analysis gets
2651
- // one k·W walk allowance per evidence tier. Without a shared allowance,
2652
- // each of k candidate pairs spends the full corpus-derived budget and a
2653
- // cumulative dialogue multiplies bounded work into tens of seconds. Small
2654
- // corpora retain exhaustive exact traversal: below this same scale the
2655
- // budget would be smaller than the structures the tests deliberately build.
2651
+ // When atoms themselves are hubs (atomIsHub a single byte reaches ≥ √N
2652
+ // contexts, §2.8's own predicate), the corpus is large enough that the
2653
+ // cross-region junction walks are dominated by the drift through common
2654
+ // content's ancestry. Each of k candidate pairs otherwise spends its own
2655
+ // √N·W budget (profiled: 160,210 junction pops, 31% of think at
2656
+ // N = 325,608), and a cumulative dialogue multiplies bounded work into tens
2657
+ // of seconds. The structural walk is therefore given ONE k·W allowance per
2658
+ // evidence tier, shared across every pair — k pairs × W phrase-scale levels,
2659
+ // the minimal exact check; a pair whose container is not reached within it
2660
+ // falls through to the resonance tier (the ANN proposes what the shallow
2661
+ // walk no longer exhaustively scans, §2.3).
2656
2662
  //
2657
- // MEASURED 2026-07-29, NOT YET RESOLVED. This gate never engages at real
2658
- // scale: on the trained store N = 325,608 with k = 24 and W = 4, so the
2659
- // threshold is 96³ = 884,736 and a third of a million contexts still runs
2660
- // unbudgeted at hubBound·W = 2,280 pops PER PAIR 160,210 junction pops,
2661
- // 5.9s, 31% of think. Sharing one hubBound·W allowance across all pairs
2662
- // instead cuts that to 22,418 pops and 2.6s (think −19%), but is measurably
2663
- // too tight below ~10³ contexts: test/36 (N = 8, budget 8) loses the
2664
- // `red circle` binding root and test/14 (N = 120, budget 40) recalls 39/40.
2665
- // The sharing is the right shape; hubBound·W is the wrong size for it, and
2666
- // fitting a size to those two points would repeat the mistake the cube
2667
- // already makes — pricing the gate on the synthetic corpora.
2668
- const marketScale = k * ctx.space.maxGroup;
2669
- const corpusScale = N > marketScale ** 3;
2663
+ // Below atomIsHub the store is small and atoms still discriminate, so the
2664
+ // walks keep exhaustive exact traversal (per-walk √N·W) the shared budget
2665
+ // would otherwise be smaller than the structures the tests deliberately
2666
+ // build. The gate is the SAME derived predicate the climb already uses for
2667
+ // byte atoms, not a separate corpus-size knob: an earlier `N > (k·W)³` cube
2668
+ // never engaged at real scale (96³ = 884,736 > 325,608), and a "share one
2669
+ // √N·W" experiment was too tight below ~10³ contexts (test/36, test/14)
2670
+ // both are the same mistake of pricing the gate on corpus size instead of on
2671
+ // the atom-hub scale.
2672
+ const corpusScale = atomIsHub(ctx, N);
2670
2673
  const exactBudget = corpusScale ? { n: k * ctx.space.maxGroup } : undefined;
2671
2674
  const synonymBudget = corpusScale ? { n: k * ctx.space.maxGroup } : undefined;
2672
2675
 
@@ -108,6 +108,13 @@ export type GItem =
108
108
  * because subtraction is more "the point" of its query). See
109
109
  * {@link liftAnswer}. */
110
110
  computed?: boolean;
111
+ /** Set on the out emitted at a chain's GENUINE FIXPOINT — the one span kind a
112
+ * recursive re-cover ({@link GraphSearch.recompleteNode}) may deepen. The
113
+ * re-cover does NOT run here: this marks the span as eligible, and
114
+ * {@link GraphSearch.deepen} runs it afterwards on the spans the lightest
115
+ * derivation actually CHOSE. Part of {@link key}, because it decides
116
+ * whether the span's final bytes may still change. */
117
+ fix?: boolean;
111
118
  };
112
119
  type OutItem = Extract<GItem, { kind: "out" }>;
113
120
 
@@ -153,6 +160,10 @@ export interface Seg {
153
160
  /** See the `computed` field of the "out" {@link GItem} — set only for an
154
161
  * extension's derived value, never a genuinely recognised learned form. */
155
162
  computed?: boolean;
163
+ /** See the `fix` field of the "out" {@link GItem} — this span ended a chain at
164
+ * a genuine fixpoint, so it is the one span kind a recursive re-cover may
165
+ * still deepen. Consumed by {@link GraphSearch.deepen}. */
166
+ fix?: boolean;
156
167
  }
157
168
 
158
169
  /** Read the chosen spans back off a derivation: the goal is a chain of bridge
@@ -171,6 +182,7 @@ function readCover(derivation: Derivation<GItem>): Seg[] {
171
182
  rec: out.rec,
172
183
  node: out.node,
173
184
  computed: out.computed,
185
+ fix: out.fix,
174
186
  });
175
187
  }
176
188
  node = node.premises[0];
@@ -472,10 +484,32 @@ export class GraphSearch {
472
484
  onDerivation(readDerivation(derivation, substitutions !== undefined));
473
485
  }
474
486
  return derivation
475
- ? { segs: readCover(derivation), cost: derivation.cost }
487
+ ? { segs: this.deepen(readCover(derivation)), cost: derivation.cost }
476
488
  : null;
477
489
  }
478
490
 
491
+ /** Re-cover the CHOSEN fixpoint spans, in place.
492
+ *
493
+ * Completion is still "cover, recursively" — it just runs on the answer
494
+ * instead of on the exploration. A cover chooses O(segs) spans, so a level
495
+ * pays O(answer) re-covers however densely the corpus interconnects the forms
496
+ * the search passed through on the way. That is the bound
497
+ * {@link recompleteNode}'s contract always claimed and, running per fixpoint
498
+ * REACHED, never had.
499
+ *
500
+ * Deepening cannot change which cover won: the derivation is already final
501
+ * and every span keeps its i..j and its cost. It only replaces a chosen
502
+ * span's bytes with the deeper learnt form they rewrite to — what the
503
+ * recursion was always for. */
504
+ private deepen(segs: Seg[]): Seg[] {
505
+ for (const s of segs) {
506
+ if (!s.fix || s.node === undefined) continue;
507
+ const deeper = this.recompleteNode(s.node);
508
+ if (deeper !== null) s.bytes = deeper;
509
+ }
510
+ return segs;
511
+ }
512
+
479
513
  /** The weighted deduction system the graph exploration solves (the four
480
514
  * reductions of adapted A*LD live in {@link lightestDerivation}; this only states the
481
515
  * items, axioms, goal, and rules — see {@link GItem} for the item kinds).
@@ -541,8 +575,8 @@ export class GraphSearch {
541
575
  }`;
542
576
  }
543
577
  return `o${it.i}.${it.j}.${it.cover ? 1 : 0}.${it.rec ? 1 : 0}.${
544
- it.node ?? -1
545
- }.${latin1(it.bytes)}`;
578
+ it.fix ? 1 : 0
579
+ }.${it.node ?? -1}.${latin1(it.bytes)}`;
546
580
  },
547
581
  *axioms() {
548
582
  yield { item: { kind: "cover", p: 0 }, cost: 0 };
@@ -855,17 +889,29 @@ export class GraphSearch {
855
889
  // actual end, never per intermediate stop — so its cost tracks the
856
890
  // ANSWER's own structure, not how densely the corpus interconnects
857
891
  // the nodes passed through on the way there.
858
- const deeper = this.recompleteNode(it.node);
892
+ // MARK the fixpoint; do not re-cover it here. Re-covering at this point
893
+ // pays a full {@link recompleteNode} — a recognition of the node's whole
894
+ // bytes — for every fixpoint the exploration REACHES, and how many it
895
+ // reaches is set by how densely the corpus interconnects the forms passed
896
+ // through. Measured on an 18.9M-node store, a 2-byte query: 12,000
897
+ // re-covers inside ONE cover, folding 95,258 distinct spans, ~2.4 GB and
898
+ // climbing to a V8 fatal. The answer needs a handful.
899
+ //
900
+ // {@link deepen} runs it instead on the spans the lightest derivation
901
+ // CHOSE. The ladder is untouched — this out still costs 0, so a genuine
902
+ // fixpoint still beats any premature stop at the same depth, exactly as
903
+ // the ordering above states.
859
904
  yield {
860
905
  premises: [it],
861
906
  conclusion: {
862
907
  kind: "out",
863
908
  i: it.i,
864
909
  j: it.j,
865
- bytes: deeper ?? nodeBytes(it.node),
910
+ bytes: nodeBytes(it.node),
866
911
  cover: true,
867
912
  rec: true,
868
913
  node: it.node,
914
+ fix: true,
869
915
  },
870
916
  cost: 0,
871
917
  };
@@ -936,18 +982,45 @@ export class GraphSearch {
936
982
  * ({@link resolve}) — the graph itself gates against re-expanding a contained
937
983
  * form ("ice is cold" ⊅→ "ice is cold is cold").
938
984
  *
939
- * Termination is INTRINSIC, not a depth limit: a node already on the
940
- * completion stack ({@link recompleteOpen}) is not re-entered a self-
941
- * referential recomposition is a cycle that can yield nothing new, so it
942
- * stops there, exactly as {@link completeForward} stops on a revisited edge.
943
- * Distinct node ids are finite and each finished completion is memoised, so a
944
- * legitimate chain runs as deep as the graph licenses and no further. */
985
+ * Termination is STRUCTURAL: a produced node is re-covered once, never inside
986
+ * another re-cover (see the guard below), so one cover pays at most one
987
+ * nested {@link solve} per distinct produced node it actually reaches, and
988
+ * {@link recompleteMemo} collapses a repeat to nothing.
989
+ *
990
+ * This comment used to argue termination from "distinct node ids are finite
991
+ * and each finished completion is memoised". That is a bound of N — the one
992
+ * AGENTS §2.8 forbids — and it was load-bearing, not pedantic: nested, the
993
+ * recursion reached depth 331 and 9.1 GB on an 18.9M-node store for a 2-byte
994
+ * query and did not terminate, which is what killed a 5 h training run at its
995
+ * checkpoint recall. Guard: test/89-completion-recursion.test.mjs. */
945
996
  private recompleteNode(node: number): Uint8Array | null {
946
997
  if (!this.host.recogniseSpan) return null;
947
998
  const memo = this.recompleteMemo;
948
999
  if (memo.has(node)) return memo.get(node) ?? null;
949
- // Cycle guard: a node being completed must not recurse back into itself.
950
- if (this.recompleteOpen.has(node)) return null;
1000
+ // ONE re-cover per produced node never a re-cover inside a re-cover.
1001
+ //
1002
+ // Re-covering is how a PRODUCED node's bytes enter the search at all: the
1003
+ // cover machinery otherwise only ever sees the QUERY's spans. That is
1004
+ // needed once. The alternation of decomposition and recomposition that
1005
+ // follows — parts rewriting several times, siblings fusing, a recomposition
1006
+ // feeding another — is the main search's own fuse/`rcmp` work, not this
1007
+ // recursion's: 15-decomposition-gap §9–§12 all pass with this method
1008
+ // disabled outright, and only §6 (the produced composite "p1 p2", whose
1009
+ // bytes nothing else brings in) needs it.
1010
+ //
1011
+ // Nesting it was the defect. Each level is a full {@link solve} with its
1012
+ // own agenda and chart, exploring from a node the answer never asked about,
1013
+ // so per-query cost tracked how densely the corpus interconnects the forms
1014
+ // passed through — the growth AGENTS §2.8 forbids. Measured on an
1015
+ // 18.9M-node store: depth 331 and 9.1 GB for a 2-byte query, not
1016
+ // terminating; and on the guard corpus every one of 125 nested re-covers
1017
+ // was REJECTED by the resolve() gate below, expanding a 70-byte node into a
1018
+ // 374-byte concatenation that names nothing. All of it was waste.
1019
+ //
1020
+ // `recompleteOpen` is that stack, so a non-empty stack means we are already
1021
+ // inside one. This subsumes the old cycle guard: a node cannot recurse
1022
+ // back into itself when nothing recurses at all.
1023
+ if (this.recompleteOpen.size > 0) return null;
951
1024
 
952
1025
  // A leaf or single-child node has no parts to recompose; skip before the
953
1026
  // costly recognition so a plain terminal answer pays nothing.
@@ -985,9 +1058,13 @@ export class GraphSearch {
985
1058
  * outs of a long query re-cover each distinct node at most once); reset at the
986
1059
  * top of {@link cover}. */
987
1060
  private recompleteMemo = new Map<number, Uint8Array | null>();
988
- /** The nodes currently being re-completed — the recursion stack. A node in
989
- * this set is not re-entered, so a cyclic recomposition terminates naturally
990
- * (the same cycle guard {@link completeForward} uses), with no depth cap. */
1061
+ /** The node currently being re-completed — the recursion stack, and so also
1062
+ * the nesting depth. {@link recompleteNode} refuses to start while it is
1063
+ * non-empty (one re-cover per produced node, never one inside another), which
1064
+ * is what keeps a query's cost proportional to its answer rather than to the
1065
+ * corpus; it therefore holds at most one id. A Set, not a flag, because it
1066
+ * states WHICH node is open — the invariant a reader needs to check the
1067
+ * guard, and what makes the old cycle-guard reading still hold. */
991
1068
  private recompleteOpen = new Set<number>();
992
1069
 
993
1070
  /** out(i,j,bytes,…): index it for the binary rules, then offer splicing a
@@ -207,6 +207,19 @@ function cachedContainers(
207
207
  * edgeAncestors' question and wrong for this one: a junction container
208
208
  * is legitimately reached across many containing structures. Half the
209
209
  * successful junctions would be lost.
210
+ *
211
+ * REFUTED EARLY-STOP (side-cone exhaustion, §2.17's "real saturation"):
212
+ * stopping the walk the moment ONE side's upward cone is emptied is wrong,
213
+ * in both a hub-guarded form and a hub-flagged form. The junction test is
214
+ * a BYTE containment over the UNION of the two cones, and a junction can be
215
+ * structurally reachable from only ONE side — the side whose seed is a
216
+ * FOLD sub-node of the container (test/16: "cold or hot" is reached from
217
+ * the window "cold", but the 3-byte answer "hot" is not a 4-byte window of
218
+ * it, so "hot"'s cone is empty while the junction still lies ahead in
219
+ * "cold"'s cone; test/34's n-ary binding fails the hub-guarded form the
220
+ * same way). "One cone exhausted" therefore never proves "no junction
221
+ * left", and the walk must keep the √N·W budget as its NET after the
222
+ * per-node saturations below.
210
223
  * • per-node hub guards — parent fan-outs beyond √N are hubs (not
211
224
  * expanded); each node contributes at most one √N page of containers;
212
225
  * √N collected candidates decide. */
@@ -251,7 +264,18 @@ export function junctionContainersFrom(
251
264
  id,
252
265
  d: 0,
253
266
  }));
254
- while (stack.length > 0 && out.length < bound && b.n-- > 0) {
267
+ while (stack.length > 0 && out.length < bound) {
268
+ // BUDGET EXHAUSTION IS AN ABSTENTION, AND IT MUST BE VISIBLE (§2.13). The
269
+ // walk stops with work still on the stack, the caller reads "no container"
270
+ // and falls through to a lower ladder rung — indistinguishable, from the
271
+ // outside, from a walk that looked everywhere and found nothing. With a
272
+ // SHARED budget (cross-region's one k·W allowance per tier) an EARLIER
273
+ // pair can drain it, so a later pair's exact tier may never run at all;
274
+ // this counter is the only thing that says so.
275
+ if (b.n-- <= 0) {
276
+ if (ctx.meter) ctx.meter.junctionBudgetExhausted++;
277
+ break;
278
+ }
255
279
  const { id: x, d } = stack.pop()!;
256
280
  if (ctx.meter) ctx.meter.junctionPops++;
257
281
  const f = cachedRead(ctx, cache, x, maxContainer);
@@ -59,7 +59,9 @@ export async function resolveConnectors(
59
59
  // transcript evidence: cover still needs the site for structural context,
60
60
  // but liftAnswer will trim that continuation as already answered. Building
61
61
  // pairwise/n-ary bridges for it can only create connectors that are later
62
- // discarded, and on cumulative dialogue that dominated the whole search.
62
+ // discarded a semantically neutral gate (it removes work whose product
63
+ // liftAnswer throws away), and a cumulative (multi-turn) query is exactly
64
+ // where such already-answered continuations recur.
63
65
  let answered = 0;
64
66
  const ordered = [...sites]
65
67
  .sort((a, b) => a.start - b.start)
@@ -72,9 +74,26 @@ export async function resolveConnectors(
72
74
  if (span && span[0] <= s.start && s.end <= span[1]) return false;
73
75
  if (query === undefined || ctx.answeredSpans.length === 0) return true;
74
76
  const continuations = ctx.store.nextFirst(s.payload, hubBound(ctx));
75
- return !continuations.some((answer) =>
76
- indexOf(query, read(ctx, answer), 0) >= 0
77
- );
77
+ return !continuations.some((answer) => {
78
+ // PREFIX-CAPPED (AGENTS §2.8): a candidate longer than the query cannot
79
+ // occur INSIDE it, so read one byte past the query's length — enough to
80
+ // detect the overflow — and reject without reconstructing the rest.
81
+ // The `+ 1` is what makes the test exact rather than a truncation: a
82
+ // result of exactly `query.length + 1` bytes is known to be too long,
83
+ // and anything shorter is the candidate's COMPLETE content, so the
84
+ // substring test below is the same test as before. (The same overflow
85
+ // probe bridge.ts:256 already uses.)
86
+ //
87
+ // This loop runs up to hubBound(ctx) = √N reads PER SITE, and only on a
88
+ // multi-turn response — `answeredSpans` is empty for a plain respond(),
89
+ // so the probe does not execute there. The cap cannot reduce the read
90
+ // COUNT — only a semantic change to the "already answered" test could —
91
+ // but it bounds each read by the query instead of by the corpus, which
92
+ // is what §2.8 asks for and what rescues a SHORT query: at 3 bytes this
93
+ // reads 4 bytes per candidate instead of the ~231 it averaged before.
94
+ const bytes = read(ctx, answer, query.length + 1);
95
+ return bytes.length <= query.length && indexOf(query, bytes, 0) >= 0;
96
+ });
78
97
  });
79
98
  const bridgePair = async (l: number, r: number) => {
80
99
  if (l === r || links.has(l + "," + r)) return;
@@ -45,15 +45,23 @@
45
45
  // from `resonate(k)` at k = 24, 256 AND 2048 — while forms scoring LOWER
46
46
  // (Germany 0.5670, Yemen 0.5591) are returned. `k` only reorders WITHIN
47
47
  // the IVF clusters already probed, exactly as Store.resonate's doc warns,
48
- // so no k recovers it. With `exhaustive` it ranks 8.
48
+ // so no k recovers it.
49
49
  //
50
- // So this is a RETRIEVABILITY gap, not a semantic one, and it is repaired by
51
- // reading the candidate list recall's refusal path has ALREADY fetched
52
- // exhaustively for the substitution bridgenever by resonating on its own.
53
- // Measured cost of the scan over those 570 candidates: 2.9 ms warm, 20.4 ms
54
- // cold, against a ~700 ms refusal path. Issuing a FRESH exhaustive call would
55
- // cost 490 ms median against 13 ms non-exhaustive (36×), which is why this tier
56
- // takes the candidate list as an argument and adds nothing to it.
50
+ // So this is a RETRIEVABILITY gap, not a semantic one, and the ANN is the wrong
51
+ // instrument for it: a proper prefix's gist cannot rank its own continuation.
52
+ // The repair is CONTENT-ADDRESSED (§2.3)`formsOpenedBy` (traverse.ts) reads
53
+ // the leaf-id WINDOW index the write side already maintains and answers "which
54
+ // trained forms does this byte run open?" in a bounded √N walk. That is this
55
+ // mechanism's first supply. The response's memoised top-k `resonance()` is the
56
+ // second, for prefixes long enough that the gist still ranks the form; it is
57
+ // read, never re-issued.
58
+ //
59
+ // AN EXHAUSTIVE ANN LIST IS NOT A SUPPLY HERE, AND WAS REMOVED. This tier once
60
+ // read `Precomputed.wideResonance()` — a full-index `resonate(guide, √N,
61
+ // exhaustive)` — on the argument that the target "ranks 8 with `exhaustive`".
62
+ // It bought an O(k) need at O(index) cost (measured: 244K annVectorReads per
63
+ // refusing query, ~1.5 s) for candidates the window index proposes directly.
64
+ // See pipeline-mechanism.ts's REMOVED note; test/95 pins its absence.
57
65
  //
58
66
  // THREE GUARDS, each falsified into existence by measurement — do not drop any:
59
67
  //
@@ -114,7 +122,8 @@ export interface PrefixCompletion {
114
122
  * it, when the continuation is sub-quantum, when a candidate's continuation
115
123
  * cannot be read through, or when the candidates disagree.
116
124
  *
117
- * `ranked` must be a list the caller has ALREADY fetched; this mechanism never
125
+ * `ranked` must be a list the caller has ALREADY fetched (the write side's
126
+ * window index, or the response's memoised top-k); this mechanism never
118
127
  * resonates on its own (see the header's cost note). */
119
128
  export function prefixCompletion(
120
129
  ctx: MindContext,
@@ -251,9 +260,9 @@ export const prefixMechanism: PipelineMechanism = {
251
260
  provenance: "prefix",
252
261
  async floor(ctx, query, _pre, worthRunning) {
253
262
  // One projection: the form is voiced whole, nothing is substituted.
254
- // INVESTMENT DISCIPLINE — the supplies below are the response's wide
255
- // candidate list and a bounded √N walk, so neither is touched until the
256
- // bound can still beat the incumbent.
263
+ // INVESTMENT DISCIPLINE — the supplies below are a bounded √N window walk
264
+ // and the response's memoised top-k resonance read, so neither is touched
265
+ // until the bound can still beat the incumbent.
257
266
  if (!worthRunning(STEP)) return STEP;
258
267
  // A query with no room for a perceivable continuation inside the phrase
259
268
  // cap cannot clear guard 2, so it is not worth a single read.
@@ -264,14 +273,25 @@ export const prefixMechanism: PipelineMechanism = {
264
273
  return STEP;
265
274
  },
266
275
  async run(ctx, query, pre) {
267
- // The response's shared wide list first; only when it supplies nothing does
268
- // the write side's window index propose. That ordering is the whole cost
269
- // story: a query the ranked list can already explain pays not one extra
270
- // read, and the bounded walk is spent only where the alternative is an
271
- // empty answer. A second SUPPLY, not a second mechanism — the same three
272
- // guards decide either way.
273
- const completed = prefixCompletion(ctx, query, await pre.wideResonance()) ??
274
- prefixCompletion(ctx, query, formsOpenedBy(ctx, query));
276
+ // ONE SUPPLY PASS, not a two-tier `??`. The window index (exact,
277
+ // content-addressed) and the response's memoised top-k (approximate) are
278
+ // concatenated and the three guards decide ONCE over the union. A
279
+ // first-then-fallback chain would let the APPROXIMATE tier override the
280
+ // EXACT one (§2.3): when formsOpenedBy finds two continuations, guard 3
281
+ // returns null and the fallback re-runs the guards on resonance's top-k
282
+ // alone which, seeing only one of the two forms, would voice it. That is
283
+ // precisely the disagreement-suppression guard 3 exists to prevent, and it
284
+ // is the exact tier's ambiguity being washed away by the approximate tier.
285
+ // Evaluating the union means a disagreement the window index saw can never
286
+ // be hidden by what the ANN happens to rank. The ANN read is the
287
+ // response's ONE memoised top-k (§2.11), already paid by recall's refusal
288
+ // path on the queries where this mechanism fires, so reading it here is not
289
+ // a second index scan.
290
+ const ids = [
291
+ ...formsOpenedBy(ctx, query),
292
+ ...(await pre.resonance()).map((h) => h.id),
293
+ ];
294
+ const completed = prefixCompletion(ctx, query, ids);
275
295
  if (completed === null) return [];
276
296
  return [{
277
297
  bytes: completed.form,
@@ -412,10 +412,14 @@ export async function recallByResonance(
412
412
  }
413
413
  }
414
414
  // 3b. Corroborated-substitution bridge — refusal-path only (bridge.ts).
415
- // The WIDE candidate list every past-the-top-k mechanism reads lives on
416
- // Precomputed (see wideResonance): shared across the whole response, so the
417
- // exhaustive branch runs at most once whoever first-touches it.
418
- const wideIds = () => pre.wideResonance();
415
+ // The bridge's proposal source is the response's ONE top-k read the same
416
+ // list recall already ranked above never an exhaustive √N scan. The
417
+ // bridge's own candidate cap is 2·recallQueryK, so top-k proposals are
418
+ // exactly the budget it can consume, and every proposal is byte-verified
419
+ // downstream (§2.3). Reuse the memoised `resonance()`; scanning every IVF
420
+ // cluster here once made every honest refusal cost hundreds of ms regardless
421
+ // of k.
422
+ const wideIds = async () => (await pre.resonance()).map((h) => h.id);
419
423
 
420
424
  // Every gist-based tier has failed; before refusing, align the query
421
425
  // byte-for-byte against the trained contexts its own stored windows