@hviana/sema 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/src/mind/articulation.js +1 -1
  2. package/dist/src/mind/attention.d.ts +18 -2
  3. package/dist/src/mind/attention.js +88 -18
  4. package/dist/src/mind/bridge.d.ts +30 -0
  5. package/dist/src/mind/bridge.js +569 -0
  6. package/dist/src/mind/graph-search.d.ts +16 -1
  7. package/dist/src/mind/graph-search.js +34 -5
  8. package/dist/src/mind/match.d.ts +15 -2
  9. package/dist/src/mind/match.js +3 -8
  10. package/dist/src/mind/mechanisms/alu.js +8 -1
  11. package/dist/src/mind/mechanisms/cast.d.ts +54 -0
  12. package/dist/src/mind/mechanisms/cast.js +303 -48
  13. package/dist/src/mind/mechanisms/cover.js +24 -32
  14. package/dist/src/mind/mechanisms/extraction.js +75 -30
  15. package/dist/src/mind/mechanisms/recall.js +66 -0
  16. package/dist/src/mind/mind.d.ts +1 -0
  17. package/dist/src/mind/mind.js +6 -1
  18. package/dist/src/mind/pipeline.js +34 -2
  19. package/dist/src/mind/reasoning.d.ts +20 -1
  20. package/dist/src/mind/reasoning.js +84 -6
  21. package/dist/src/mind/recognition.js +157 -13
  22. package/dist/src/mind/traverse.js +16 -0
  23. package/dist/src/mind/types.d.ts +65 -2
  24. package/dist/src/mind/types.js +53 -7
  25. package/dist/src/store.d.ts +12 -3
  26. package/dist/src/store.js +9 -3
  27. package/package.json +1 -1
  28. package/src/mind/articulation.ts +1 -0
  29. package/src/mind/attention.ts +105 -17
  30. package/src/mind/bridge.ts +596 -0
  31. package/src/mind/graph-search.ts +59 -2
  32. package/src/mind/match.ts +19 -5
  33. package/src/mind/mechanisms/alu.ts +8 -1
  34. package/src/mind/mechanisms/cast.ts +336 -46
  35. package/src/mind/mechanisms/cover.ts +31 -36
  36. package/src/mind/mechanisms/extraction.ts +101 -40
  37. package/src/mind/mechanisms/recall.ts +79 -0
  38. package/src/mind/mind.ts +7 -1
  39. package/src/mind/pipeline.ts +37 -2
  40. package/src/mind/reasoning.ts +97 -5
  41. package/src/mind/recognition.ts +160 -12
  42. package/src/mind/traverse.ts +17 -0
  43. package/src/mind/types.ts +110 -6
  44. package/src/store.ts +19 -5
  45. package/test/35-attention-confidence.test.mjs +139 -0
  46. package/test/36-already-answered-fusion.test.mjs +128 -0
  47. package/test/37-cluster-dispersion-fusion.test.mjs +190 -0
  48. package/test/38-reason-restate-guard.test.mjs +94 -0
  49. package/test/39-cast-restate-guard.test.mjs +102 -0
  50. package/test/40-choosenext-scale-guard.test.mjs +75 -0
  51. package/test/41-seatofnode-direction.test.mjs +85 -0
  52. package/test/42-recognise-trace-idempotence.test.mjs +106 -0
  53. package/test/43-cast-analog-seat.test.mjs +244 -0
  54. package/test/44-recognise-edge-whitespace.test.mjs +63 -0
  55. package/test/45-liftanswer-restated-trim.test.mjs +60 -0
  56. package/test/46-recognise-multibyte-edge.test.mjs +85 -0
  57. package/test/47-cast-comparison-coverage.test.mjs +134 -0
  58. package/test/48-recognise-turn-connective.test.mjs +125 -0
  59. package/test/49-natural-units-synonym-bridge.test.mjs +175 -0
  60. package/test/50-cast-analog-consensus-floor.test.mjs +179 -0
@@ -7,7 +7,7 @@
7
7
  import { rItem } from "./trace.js";
8
8
  import { canonResolve, foldTree, gistOf, latin1Key, perceive, resolve, } from "./primitives.js";
9
9
  import { atomIsHub, corpusN, leadsSomewhere } from "./traverse.js";
10
- import { chainReach, leafIdAt } from "./canonical.js";
10
+ import { chainReach, leafIdAt, leafIdRun } from "./canonical.js";
11
11
  import { isChunk } from "../sema.js";
12
12
  /** Decompose a byte stream into every stored form that leads somewhere
13
13
  * (has a continuation edge or a halo). Two complementary readings:
@@ -23,13 +23,37 @@ import { isChunk } from "../sema.js";
23
23
  * Both O(n · maxGroup) bounded O(1) probes — never a scan of the corpus. */
24
24
  export function recognise(ctx, bytes) {
25
25
  // Content-keyed memo — works for both single-turn respond() and multi-turn
26
- // respondTurn() (where the map persists across calls). Skipped while
27
- // tracing so every call still emits its rationale step.
28
- if (ctx.recogniseMemo && !ctx.trace) {
26
+ // respondTurn() (where the map persists across calls). ALWAYS consulted,
27
+ // regardless of tracing matching perceive()'s own memo, which carries no
28
+ // trace gate at all. This memo is NOT an optional accelerator: recogniseImpl
29
+ // walks the query's perceived tree via foldTree, whose subtree-resolution
30
+ // fast path (see primitives.ts) skips invoking `visit` — and therefore
31
+ // skips EMITTING SITES — for any subtree already cached in
32
+ // ctx._resolvedSubtrees. A multi-turn conversation's stable-prefix fold
33
+ // deliberately shares node OBJECTS across turns, so by the second call on
34
+ // the exact same bytes, large swaths of the tree are already cached and
35
+ // foldTree stops short of recursing into them — a second recogniseImpl
36
+ // call on the SAME bytes is not idempotent; it silently finds FEWER sites
37
+ // than the first (observed live: 31 sites → 5 on an immediate repeat
38
+ // call). Skipping this memo "only while tracing" used to mean every
39
+ // traced turn re-ran recogniseImpl from scratch at every one of the many
40
+ // call sites that recognise the same query (cover, reason, articulate...),
41
+ // each subsequent call silently more incomplete than the last — measurably
42
+ // changing which mechanism grounds the answer, not just costing time. The
43
+ // trace step must still fire on every call regardless (a cache hit is not
44
+ // silent), so it is emitted here directly instead of only inside
45
+ // recogniseImpl.
46
+ if (ctx.recogniseMemo) {
29
47
  const key = latin1Key(bytes);
30
48
  const hit = ctx.recogniseMemo.get(key);
31
- if (hit !== undefined)
49
+ if (hit !== undefined) {
50
+ ctx.trace?.step("recognise", [rItem(bytes, "query")], hit.sites.map((s) => rItem(bytes.subarray(s.start, s.end), "form", s.payload, [
51
+ s.start,
52
+ s.end,
53
+ ])), `decompose the query into ${hit.sites.length} learnt form(s) that ` +
54
+ `lead somewhere (over ${hit.leaves.length} perceived leaves) [cached]`);
32
55
  return hit;
56
+ }
33
57
  const fresh = recogniseImpl(ctx, bytes);
34
58
  ctx.recogniseMemo.set(key, fresh);
35
59
  return fresh;
@@ -41,8 +65,9 @@ function recogniseImpl(ctx, bytes) {
41
65
  const sites = [];
42
66
  const leaves = [];
43
67
  const splits = new Set();
68
+ const starts = new Set();
44
69
  if (bytes.length === 0)
45
- return { sites, leaves, splits };
70
+ return { sites, leaves, splits, starts };
46
71
  // Span-resolve memo for THIS call: the structural pass (sub-runs inside
47
72
  // leaf-parents) and the canonical pass (leaf-id chains) probe overlapping
48
73
  // spans, and each resolve() is a full fold of the sub-span (fresh subarray
@@ -70,15 +95,25 @@ function recogniseImpl(ctx, bytes) {
70
95
  // silence. Atoms stay available as leaves (PASS-carried literals) and
71
96
  // through exact tier-0 resolution regardless.
72
97
  const atomsAreHubs = atomIsHub(ctx, corpusN(ctx));
98
+ // Distinct probes (structural exact match, canon fallback, edge trims at
99
+ // several offsets) can legitimately re-derive the SAME (start, end, id)
100
+ // site from different tree nodes — a wide edge-trim search is exactly
101
+ // this on purpose (see below). Duplicate site entries are not wrong
102
+ // evidence, but they double the weight cover's derivation search gives
103
+ // that span, distorting its cost model — the same span must count once.
104
+ const seen = new Set();
73
105
  const emit = (start, end, id) => {
74
106
  if (id < 0 && atomsAreHubs)
75
107
  return;
108
+ const key = start + "," + end + "," + id;
109
+ if (seen.has(key))
110
+ return;
111
+ seen.add(key);
76
112
  if (leadsSomewhere(ctx, id)) {
77
113
  sites.push({ start, end, payload: id });
78
114
  }
79
115
  };
80
116
  // ── structural: the query's own perceived tree ──────────────────────
81
- const starts = new Set();
82
117
  starts.add(0);
83
118
  foldTree(ctx, perceive(ctx, bytes), 0, (n, start, end, node) => {
84
119
  if (n.kids === null) {
@@ -90,11 +125,96 @@ function recogniseImpl(ctx, bytes) {
90
125
  // missed may still be a stored form under the response's equivalence
91
126
  // (case, width, whitespace — whatever the injected canonicalizer says).
92
127
  // O(subtree bytes) per miss, memoised per response; a no-op when no
93
- // canonicalizer was injected or the store has no canon index.
94
- else if (end - start >= 2) {
128
+ // canonicalizer was injected or the store has no canon index. A raw
129
+ // leaf (n.kids === null) is single-byte and handled by the byte-atom
130
+ // path above instead — canon equivalence only applies to composites.
131
+ else if (n.kids !== null) {
95
132
  const cid = canonResolve(ctx, bytes.subarray(start, end));
96
133
  if (cid !== null)
97
134
  emit(start, end, cid);
135
+ // The edge-trim fallbacks below remove 1 byte from a side; the
136
+ // remainder must still be a composite (>= 2 bytes, the same floor
137
+ // n.kids !== null enforces above) rather than degenerate into
138
+ // single-byte-atom territory, which atomIsHub already governs
139
+ // separately.
140
+ else if (end - start - 1 >= 2) {
141
+ // The chunk's own boundary is drawn by content geometry, not by
142
+ // any notion of "form" — it can include one edge byte the query's
143
+ // fold happened to attach here that the trained span never had
144
+ // (e.g. a separator from the preceding chunk). The core has no
145
+ // idea what that byte means; it only knows resolve()/canonResolve
146
+ // are self-verifying (hash-then-verify, same discipline as every
147
+ // content lookup here), so a blind one-byte-shorter guess on
148
+ // either edge costs nothing when wrong and is trustworthy when it
149
+ // hits. Two extra probes, only on the already-failed miss path.
150
+ const left = resolve(ctx, bytes.subarray(start + 1, end));
151
+ if (left !== null)
152
+ emit(start + 1, end, left);
153
+ const right = resolve(ctx, bytes.subarray(start, end - 1));
154
+ if (right !== null)
155
+ emit(start, end - 1, right);
156
+ // A misalignment wider than one byte (e.g. more than one edge
157
+ // separator swallowed) is not itself geometry-quantized — the
158
+ // WRITE side's canonical index (canonicalWindows) interns sliding
159
+ // W−1/W-length windows over leaf ids at EVERY offset, not just
160
+ // radix-aligned ones (see canonical.ts) — so the offset that
161
+ // recovers a trained span can be anything, not a multiple of W.
162
+ // What IS bounded is how far it's worth looking: chainReach(W)=W²,
163
+ // the same reach the canonical pass (tryChain) trusts for a chain
164
+ // rebuilt off the query's own fold. Every candidate offset is
165
+ // gated by store.findBranch(leafIds) first — the SAME cheap,
166
+ // fold-free existence check tryChain already uses — so the extra
167
+ // resolve() fold (the real cost) is only paid when a branch could
168
+ // plausibly exist there, not for every offset. The node itself is
169
+ // also bounded to chunk-scale (end - start <= W²): widening this at
170
+ // whole-query/root scale can rediscover a smaller subtree's own
171
+ // content as a second, overlapping site the structural walk's own
172
+ // finer recursion already emits correctly on its own — a duplicate
173
+ // that downstream derivation can stitch into a wrong answer.
174
+ const W = ctx.space.maxGroup;
175
+ for (let k = 1; end - start <= W * W && k <= W * W && start + k < end - 1; k++) {
176
+ const lIds = leafIdRun(ctx, bytes, start + k, end);
177
+ if (lIds !== null && store.findBranch(lIds) !== null) {
178
+ const eLeft = resolve(ctx, bytes.subarray(start + k, end));
179
+ if (eLeft !== null)
180
+ emit(start + k, end, eLeft);
181
+ }
182
+ const rIds = leafIdRun(ctx, bytes, start, end - k);
183
+ if (rIds !== null && store.findBranch(rIds) !== null) {
184
+ const eRight = resolve(ctx, bytes.subarray(start, end - k));
185
+ if (eRight !== null)
186
+ emit(start, end - k, eRight);
187
+ }
188
+ }
189
+ // A REAL extra word at the left edge (a discourse connective like
190
+ // "And " prepended to a follow-up turn — not boundary noise, actual
191
+ // content the injected canonicalizer has no equivalence for) shows
192
+ // up as a canon-miss too big for the chunk-scale search above: the
193
+ // turn is its OWN segment (stable-prefix folded independently — see
194
+ // mind.ts's _growContext), so it can be turn/segment-scale, not
195
+ // chunk-scale. Widening the size bound itself reopens the root-
196
+ // scale false-positive this module already fixed once (test/46);
197
+ // widening the SEARCH instead — trying every position up to W
198
+ // chunk-widths from the left edge that the query's OWN fold treats
199
+ // as a chunk boundary (`starts`, the same set the canonical pass
200
+ // privileges with full chain reach) — does not: `starts.has(p)` is
201
+ // fold EVIDENCE the query produced on its own (a leaf-parent chunk
202
+ // like "And " is visited, and its start added to `starts`, before
203
+ // this composite ever runs — foldTree is post-order), never a blind
204
+ // guess. Bounded to W candidates, each an O(1) set lookup before
205
+ // paying for the real canonResolve fold — canonResolve, not
206
+ // resolve()/findBranch, because the gap here is often exactly the
207
+ // kind of equivalence (case, in the live trace) canon exists for,
208
+ // not just an exact-content coincidence.
209
+ for (let k = 1; k <= W; k++) {
210
+ const p = start + k * W;
211
+ if (p >= end - 1 || !starts.has(p))
212
+ continue;
213
+ const cid = canonResolve(ctx, bytes.subarray(p, end));
214
+ if (cid !== null)
215
+ emit(p, end, cid);
216
+ }
217
+ }
98
218
  }
99
219
  if (isChunk(n)) {
100
220
  starts.add(start);
@@ -105,7 +225,16 @@ function recogniseImpl(ctx, bytes) {
105
225
  leafOffsets.push(off);
106
226
  off += k.leaf?.length ?? 0;
107
227
  }
228
+ // Sub-spans starting at i > 0 begin INSIDE the chunk, at an offset the
229
+ // query's own fold did not itself choose as a boundary — the same
230
+ // opportunistic byte-atom-chain risk `tryChain`'s `boundary` gate
231
+ // guards below (see its comment). Only the chunk's own left edge
232
+ // (i === 0, already registered in `starts` above) carries the fold's
233
+ // evidence; interior sub-starts are exempt from the guard only while
234
+ // atoms themselves still discriminate at this corpus scale.
108
235
  for (let i = 0; i < n.kids.length; i++) {
236
+ if (i > 0 && atomsAreHubs)
237
+ break;
109
238
  const subIds = [];
110
239
  for (let j = i; j < n.kids.length; j++) {
111
240
  const kj = n.kids[j];
@@ -148,7 +277,20 @@ function recogniseImpl(ctx, bytes) {
148
277
  chunkEnd[p] = chunkLimit;
149
278
  }
150
279
  }
151
- const tryChain = (p, maxIds) => {
280
+ // A chain rebuilt from a NON-boundary offset (the query's own perceived
281
+ // cut, `starts`, never chose to segment here) is opportunistic: the same
282
+ // byte-atom coincidence the hub guard above already exists for, just
283
+ // spelled over 2+ leaves instead of 1. At small corpus scale that's fine
284
+ // — coincidence is rare and every chain is real evidence (see `atomIsHub`).
285
+ // Past the scale where atoms themselves stop discriminating, the same
286
+ // uniform-expectation argument bounds a CHAIN'S commonality too: it is at
287
+ // least as rare as its rarest atom, so a store where atoms are hubs makes
288
+ // interior chain reconstructions no more trustworthy than the atoms they
289
+ // are built from ("hi" resolving out of "W[hi]ch" is exactly this: two
290
+ // hub-scale atoms, chained at an offset nothing in the query's own fold
291
+ // selected). Chains that start ON a boundary carry the fold's own
292
+ // evidence instead and are exempt.
293
+ const tryChain = (p, maxIds, boundary) => {
152
294
  const first = leafFrom(p);
153
295
  if (!first)
154
296
  return;
@@ -164,6 +306,8 @@ function recogniseImpl(ctx, bytes) {
164
306
  pos = nx.end;
165
307
  if (store.findBranch(ids) === null)
166
308
  continue;
309
+ if (!boundary && atomsAreHubs)
310
+ continue;
167
311
  const id = resolveSpan(p, pos);
168
312
  if (id === null || id === prevId)
169
313
  continue;
@@ -173,11 +317,11 @@ function recogniseImpl(ctx, bytes) {
173
317
  };
174
318
  for (let p = 0; p < bytes.length; p++) {
175
319
  if (starts.has(p)) {
176
- tryChain(p, chainReach(W)); // boundary start — full reach
320
+ tryChain(p, chainReach(W), true); // boundary start — full reach
177
321
  }
178
322
  else {
179
323
  const limit = chunkEnd[p] + W;
180
- tryChain(p, Math.min(limit - p, chainReach(W)));
324
+ tryChain(p, Math.min(limit - p, chainReach(W)), false);
181
325
  }
182
326
  }
183
327
  // ── splits: a form boundary that does not fall on a leaf edge ────────
@@ -195,7 +339,7 @@ function recogniseImpl(ctx, bytes) {
195
339
  s.end,
196
340
  ])), `decompose the query into ${sites.length} learnt form(s) that lead somewhere` +
197
341
  ` (over ${leaves.length} perceived leaves)`);
198
- return { sites, leaves, splits };
342
+ return { sites, leaves, splits, starts };
199
343
  }
200
344
  /** Segment bytes using the geometry's own groupings — leaf-parent
201
345
  * nodes from the perceived tree, with consecutive bare leaves merged
@@ -439,6 +439,22 @@ export function chooseNext(ctx, id, guide) {
439
439
  bestMass = mass;
440
440
  }
441
441
  }
442
+ // NO consensusFloor gate here (tried and reverted — see
443
+ // test/40-choosenext-scale-guard.test.mjs): that floor is calibrated for
444
+ // POOLED, IDF-weighted CLIMB VOTES (recallByResonance, commitVotes), where
445
+ // each corroborating region contributes at most ln N and the floor grows
446
+ // with N exactly as that per-region ceiling does (HOW_IT_WORKS.md §8.6).
447
+ // `bestSupport` here is a different kind of quantity — a raw prevCount of
448
+ // how many training contexts predicted ONE destination, bounded by how
449
+ // often that specific fact was retold, never by corpus size N. Gating an
450
+ // N-invariant count against an N-growing threshold guarantees failure
451
+ // once N is large enough, discarding genuinely, structurally dominant
452
+ // edges (observed: a fact corroborated 2-to-1-1-1 refused at N≈325K,
453
+ // falling back to a noisy concept-hop). The loop above already IS the
454
+ // "genuinely competing" test: a tie leaves first-inserted as the pick
455
+ // (test/30's own pinned behaviour); a strict winner is real evidence
456
+ // regardless of corpus scale. Matches HOW_IT_WORKS.md §25's own
457
+ // chooseNext pseudocode, which has no such floor.
442
458
  // Trace is built lazily — the filter + map below only execute when a
443
459
  // trace listener is attached, so the common (no-trace) path pays only
444
460
  // for the prevCount calls in the loop above, never for extra rItemShort
@@ -34,6 +34,7 @@ export interface GraphSearchHost {
34
34
  sites: ReadonlyArray<Site>;
35
35
  leaves: ReadonlyArray<Leaf>;
36
36
  splits: ReadonlySet<number>;
37
+ starts: ReadonlySet<number>;
37
38
  };
38
39
  chooseNext?(node: number): number | undefined;
39
40
  }
@@ -44,6 +45,13 @@ export interface Recognition {
44
45
  leaves: Leaf[];
45
46
  /** Sub-leaf positions where a form boundary falls between leaf edges. */
46
47
  splits: Set<number>;
48
+ /** Leaf-parent (chunk) start positions from the query's OWN perceived
49
+ * fold — the positions the fold itself chose as a grouping boundary, as
50
+ * opposed to an offset a byte-level scan merely happens to land on. The
51
+ * one boundary signal opportunistic cross-leaf recovery (recognition's
52
+ * own canonical chains, the search's `fuse`) can lean on instead of
53
+ * ASCII/word heuristics: see the `boundary` gate in recognition.ts. */
54
+ starts: Set<number>;
47
55
  }
48
56
  /** How the consensus climb weights a region's Document-Frequency reach. */
49
57
  export type DFMode = "inverse" | "direct" | "combined";
@@ -56,6 +64,33 @@ export interface Attention {
56
64
  /** The union of the query byte-spans whose evidence supports this point. */
57
65
  start: number;
58
66
  end: number;
67
+ /** SCALE-INVARIANT confidence: the fraction of the query's OWN regions
68
+ * whose evidence this point accounts for (Σ RegionVote.absorbed among
69
+ * its contributors, over the query's total region count) — read PER-
70
+ * ANCHOR, unlike the raw IDF vote (an absolute, ln(N)-scaled quantity
71
+ * that means "strong" on a small store and "weak" on a large one for
72
+ * the SAME degree of genuine consensus). A point whose breadth clears
73
+ * `dominates` (> half the query's regions corroborate it) is real
74
+ * consensus; one that does not is a coincidental single-region echo —
75
+ * see test/35-attention-confidence.test.mjs. */
76
+ breadth: number;
77
+ /** DISPERSION: the number of distinct clusters this point's contributing
78
+ * regions form, merging any two whose gap is under one river-fold
79
+ * quantum W. Neither breadth NOR raw region count discriminates a
80
+ * genuine further topic from a coincidental echo (both were tried and
81
+ * falsified — breadth starves a genuine, evenly-split multi-topic query,
82
+ * since no root in a real N-way split can exceed half the vote; raw
83
+ * count doesn't separate them either, since a short, structurally simple
84
+ * echo racks up as many corroborating regions as a real topic does).
85
+ * Dispersion asks a different question: not how MUCH evidence, but how
86
+ * many separate PLACES in the query corroborate it. A coincidental
87
+ * match — one local phrase resonating with an unrelated stored form —
88
+ * is structurally confined to ONE cluster no matter how strong its vote;
89
+ * a genuine further topic is named in its own distinctive wording
90
+ * somewhere the query's scaffolding does not reach, always a SEPARATE
91
+ * cluster from whatever else corroborates it. See
92
+ * test/37-cluster-dispersion-fusion.test.mjs. */
93
+ clusters: number;
59
94
  }
60
95
  /** Both read-outs of one consensus climb. */
61
96
  export interface AttentionRead {
@@ -89,6 +124,14 @@ export interface RegionVote {
89
124
  roots: readonly number[];
90
125
  w: number;
91
126
  wFocus: number;
127
+ /** How many of the query's ORIGINAL regions this one vote's evidence
128
+ * accounts for. 1 for an ordinary per-region vote (itself); for a
129
+ * cross-region junction vote, 1 (itself) plus however many individual
130
+ * votes it explained away (see crossRegionVotes) — the junction speaks
131
+ * for all of them at once, and breadth accounting must not undercount it
132
+ * to "one region" just because it collapsed to one pooled axiom.
133
+ * Defaults to 1 when absent. */
134
+ absorbed?: number;
92
135
  }
93
136
  /** The edge-bearing contexts reached by climbing from a node, plus saturation info. */
94
137
  export interface AncestorReach {
@@ -200,9 +243,29 @@ export interface MindContext extends GraphSearchHost {
200
243
  export declare const ALL = 2147483647;
201
244
  /** Splice every chosen span in order — the whole cover as one byte string. */
202
245
  export declare function spliceAll(segs: Seg[]): Uint8Array | null;
246
+ /** Whether a chosen span RESTATES the query rather than answering it: its
247
+ * SUBSTITUTED bytes (an edge followed from a recognised site, not the
248
+ * site's own literal text read back) already occur elsewhere in the query
249
+ * — the same principle recall.ts's tiers apply to a whole-query projection
250
+ * ("a projection that is a proper byte-subspan of the query restates part
251
+ * of the question"). A LITERAL span (the site's own bytes, unchanged) is
252
+ * exempt: naming what's already there at its OWN position is not a
253
+ * substitution. A recognised site that is itself an entire PRIOR TURN of
254
+ * a multi-turn query is exactly this shape: it carries a genuine learnt
255
+ * continuation, but that continuation is something the asker already said
256
+ * moments later in the SAME query, not a new answer. Below one river
257
+ * window, byte overlap is chance, not evidence — the same floor
258
+ * identityBar and reachThreshold hold every other structural-overlap claim
259
+ * to. */
260
+ export declare function segRestatesQuery(s: Seg, query: Uint8Array, queryLen: number, W: number): boolean;
203
261
  /** Lift the answer out of the cover for think: the recognised region, free of
204
- * the asker's surrounding (unrecognised) framing. */
205
- export declare function liftAnswer(segs: Seg[], queryLen: number): Uint8Array | null;
262
+ * the asker's surrounding (unrecognised) framing — and free of any chosen
263
+ * span that only RESTATES content the query already contains (see {@link
264
+ * segRestatesQuery}). A restating span is excluded from both the framing
265
+ * (lo/hi) decision and the final concatenation: it is stale, not a second
266
+ * answer, but the OTHER spans a derivation chose are independent evidence
267
+ * and must not be discarded along with it. */
268
+ export declare function liftAnswer(segs: Seg[], queryLen: number, query: Uint8Array, W: number): Uint8Array | null;
206
269
  /** The CHANGED NODES of a freshly-perceived `tree` against the node ids a previous
207
270
  * tracked deposit interned (`prevSeen`). */
208
271
  export declare function changedNodes(tree: Sema, ids: Map<Sema, number>, prevSeen: Set<number>): Sema[];
@@ -2,7 +2,7 @@
2
2
  //
3
3
  // GraphSearchHost is defined first (minimal imports) so GraphSearch can import
4
4
  // it without pulling in the full MindContext.
5
- import { concatBytes } from "../bytes.js";
5
+ import { bytesEqual, concatBytes, indexOf } from "../bytes.js";
6
6
  import { dominates } from "../geometry.js";
7
7
  // ═══════════════════════════════════════════════════════════════════════════
8
8
  // FREE FUNCTIONS (pure, no state)
@@ -15,25 +15,71 @@ export function spliceAll(segs) {
15
15
  return null;
16
16
  return concatBytes(segs.map((s) => s.bytes));
17
17
  }
18
+ /** Whether a chosen span RESTATES the query rather than answering it: its
19
+ * SUBSTITUTED bytes (an edge followed from a recognised site, not the
20
+ * site's own literal text read back) already occur elsewhere in the query
21
+ * — the same principle recall.ts's tiers apply to a whole-query projection
22
+ * ("a projection that is a proper byte-subspan of the query restates part
23
+ * of the question"). A LITERAL span (the site's own bytes, unchanged) is
24
+ * exempt: naming what's already there at its OWN position is not a
25
+ * substitution. A recognised site that is itself an entire PRIOR TURN of
26
+ * a multi-turn query is exactly this shape: it carries a genuine learnt
27
+ * continuation, but that continuation is something the asker already said
28
+ * moments later in the SAME query, not a new answer. Below one river
29
+ * window, byte overlap is chance, not evidence — the same floor
30
+ * identityBar and reachThreshold hold every other structural-overlap claim
31
+ * to. */
32
+ export function segRestatesQuery(s, query, queryLen, W) {
33
+ if (!s.rec)
34
+ return false;
35
+ const literal = s.j - s.i === s.bytes.length &&
36
+ bytesEqual(s.bytes, query.subarray(s.i, s.j));
37
+ if (literal)
38
+ return false;
39
+ return s.bytes.length >= W && s.bytes.length < queryLen &&
40
+ indexOf(query, s.bytes, 0) >= 0;
41
+ }
18
42
  /** Lift the answer out of the cover for think: the recognised region, free of
19
- * the asker's surrounding (unrecognised) framing. */
20
- export function liftAnswer(segs, queryLen) {
43
+ * the asker's surrounding (unrecognised) framing — and free of any chosen
44
+ * span that only RESTATES content the query already contains (see {@link
45
+ * segRestatesQuery}). A restating span is excluded from both the framing
46
+ * (lo/hi) decision and the final concatenation: it is stale, not a second
47
+ * answer, but the OTHER spans a derivation chose are independent evidence
48
+ * and must not be discarded along with it. */
49
+ export function liftAnswer(segs, queryLen, query, W) {
50
+ const restated = segs.map((s) => segRestatesQuery(s, query, queryLen, W));
21
51
  const recognised = [];
22
- for (let k = 0; k < segs.length; k++)
23
- if (segs[k].rec)
52
+ for (let k = 0; k < segs.length; k++) {
53
+ if (segs[k].rec && !restated[k])
24
54
  recognised.push(k);
55
+ }
25
56
  if (recognised.length === 0)
26
57
  return null;
27
58
  if (recognised.length === 1) {
28
59
  const s = segs[recognised[0]];
60
+ // A COMPUTED span's query-side width is operand digit-count, not
61
+ // evidence of how much of the query's meaning it accounts for — the
62
+ // half-dominance check below (built for a genuinely RECOGNISED learned
63
+ // form) is not a valid framing signal for it (see the `computed` field
64
+ // doc on Seg/GItem): "1000 - 421" outweighs "what is …?" by width only
65
+ // because the operands are big, not because the framing matters less.
66
+ // A LITERAL PREFIX before a computed span is unambiguous framing
67
+ // regardless of width — an arithmetic expression is never itself
68
+ // preceded by more literal computed content, so anything literal before
69
+ // it is question wording ("what is ", "compute ") to lift clear of.
70
+ // With no prefix (s.i === 0) the span is judged by the ordinary
71
+ // half-dominance rule below, which already correctly keeps a short
72
+ // trailing glue byte ("2+2." → "4.", the span dominates a 4-byte query).
73
+ if (s.computed && s.i > 0)
74
+ return s.bytes;
29
75
  if (dominates(s.j - s.i, queryLen)) {
30
- return concatBytes(segs.map((x) => x.bytes));
76
+ return concatBytes(segs.filter((_, k) => !restated[k]).map((x) => x.bytes));
31
77
  }
32
78
  return s.bytes;
33
79
  }
34
80
  const lo = recognised[0];
35
81
  const hi = recognised[recognised.length - 1];
36
- return concatBytes(segs.slice(lo, hi + 1).map((x) => x.bytes));
82
+ return concatBytes(segs.slice(lo, hi + 1).filter((_, k) => !restated[lo + k]).map((x) => x.bytes));
37
83
  }
38
84
  /** The CHANGED NODES of a freshly-perceived `tree` against the node ids a previous
39
85
  * tracked deposit interned (`prevSeen`). */
@@ -139,8 +139,17 @@ export interface Store {
139
139
  * question is decided, with per-page work bounded by `limit`. */
140
140
  containersSlice(child: NodeId, offset: number, limit: number): NodeId[];
141
141
  nodeCount(): number;
142
- /** The k nodes whose gist resonates most with v. */
143
- resonate(v: Vec, k: number): Promise<Hit[]>;
142
+ /** The k nodes whose gist resonates most with v. `exhaustive` widens the
143
+ * IVF probe to every cluster (see {@link AbstractStore.efFor}'s doc)
144
+ * for refusal-path-only callers where an approximate top-√C-clusters
145
+ * search is not the same discriminator as the caller's own byte-exact
146
+ * verification (the substitution bridge's proposal channel): a rarer
147
+ * paraphrase can score lower than hundreds of unrelated hits by pure
148
+ * fold-geometry structural distance (a middle-of-string mismatch
149
+ * perturbs the tree hash far more than a tail mismatch of the same
150
+ * byte length) and so never even reach a probed cluster, no matter how
151
+ * large k is — k only reorders WITHIN the clusters already probed. */
152
+ resonate(v: Vec, k: number, exhaustive?: boolean): Promise<Hit[]>;
144
153
  /** Mark a node as a RESONANCE TARGET — promote its gist into the content
145
154
  * index so {@link resonate} can find it. A node's gist is captured at intern
146
155
  * but indexed LAZILY (only targets are indexed; the ~99.5% intermediate DAG
@@ -593,7 +602,7 @@ export declare abstract class AbstractStore implements Store {
593
602
  *
594
603
  * Iterative explicit-queue walk: the call stack never sees tree depth. */
595
604
  protected indexSubtree(root: NodeId): void;
596
- resonate(v: Vec, k: number): Promise<Hit[]>;
605
+ resonate(v: Vec, k: number, exhaustive?: boolean): Promise<Hit[]>;
597
606
  indexedVectorCount(): number;
598
607
  lastResonateReads(): number;
599
608
  /** How many physical compaction attempts have failed this session. Zero in
package/dist/src/store.js CHANGED
@@ -1135,7 +1135,7 @@ export class AbstractStore {
1135
1135
  }
1136
1136
  }
1137
1137
  // ── Soft resonance ─────────────────────────────────────────────────────
1138
- async resonate(v, k) {
1138
+ async resonate(v, k, exhaustive = false) {
1139
1139
  await this._ensureReady();
1140
1140
  // Synchronous flush of any buffered index writes: the FIRST resonance
1141
1141
  // after a large ingest pays that flush here, so it shows up in respond
@@ -1148,14 +1148,20 @@ export class AbstractStore {
1148
1148
  // same values still hits. Lazy-init: null after any index write; the
1149
1149
  // first miss after a flush recreates it. When voteRegions resonates
1150
1150
  // identical perceived sub-regions, only the first call descends the ANN.
1151
- const rk = vecKey(v) + ":" + k;
1151
+ const rk = vecKey(v) + ":" + k + (exhaustive ? ":x" : "");
1152
1152
  const cache = this._resonateCache;
1153
1153
  if (cache) {
1154
1154
  const hit = cache.get(rk);
1155
1155
  if (hit !== undefined)
1156
1156
  return hit;
1157
1157
  }
1158
- const results = this._vecContentQuery(normalize(copy(v)), k * this.overfetch, this.efFor(this._vecContentClusterCount()));
1158
+ const clusters = this._vecContentClusterCount();
1159
+ const results = this._vecContentQuery(normalize(copy(v)), k * this.overfetch,
1160
+ // Exhaustive: probe every cluster (ef ≥ 4·clusters guarantees the
1161
+ // IVF's own ef→nprobe=ceil(ef/4) mapping reaches all of them) — the
1162
+ // natural ceiling for a search that is ALREADY refusal-path-only and
1163
+ // must not miss a candidate hiding in an unprobed cluster.
1164
+ exhaustive ? 4 * clusters : this.efFor(clusters));
1159
1165
  const out = [];
1160
1166
  for (const r of results) {
1161
1167
  const id = r.id;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hviana/sema",
3
- "version": "0.2.2",
3
+ "version": "0.2.4",
4
4
  "description": "Sema: a non-parametric, instance-based reasoning system.",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",
@@ -105,6 +105,7 @@ export async function articulate(
105
105
  new Map(),
106
106
  ans.leaves,
107
107
  ans.splits,
108
+ ans.starts,
108
109
  substitutions,
109
110
  undefined,
110
111
  undefined,