@hviana/sema 0.4.1 → 0.4.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 (45) hide show
  1. package/dist/src/ingest-cache.js +4 -0
  2. package/dist/src/meter.d.ts +5 -0
  3. package/dist/src/meter.js +5 -0
  4. package/dist/src/mind/attention.js +19 -2
  5. package/dist/src/mind/bridge.js +265 -56
  6. package/dist/src/mind/junction.d.ts +7 -4
  7. package/dist/src/mind/junction.js +16 -5
  8. package/dist/src/mind/match.d.ts +15 -0
  9. package/dist/src/mind/match.js +92 -2
  10. package/dist/src/mind/mechanisms/cast.js +12 -1
  11. package/dist/src/mind/mechanisms/confluence.js +31 -1
  12. package/dist/src/mind/mechanisms/cover.d.ts +1 -1
  13. package/dist/src/mind/mechanisms/cover.js +29 -5
  14. package/dist/src/mind/mechanisms/recall.js +17 -41
  15. package/dist/src/mind/mind.d.ts +7 -0
  16. package/dist/src/mind/mind.js +25 -2
  17. package/dist/src/mind/pipeline-mechanism.d.ts +2 -2
  18. package/dist/src/mind/pipeline-mechanism.js +87 -4
  19. package/dist/src/mind/pipeline.js +1 -1
  20. package/dist/src/mind/reasoning.js +19 -11
  21. package/dist/src/mind/recognition.js +41 -0
  22. package/dist/src/mind/resonance.js +0 -0
  23. package/dist/src/mind/traverse.d.ts +3 -1
  24. package/dist/src/mind/traverse.js +14 -13
  25. package/dist/src/mind/types.d.ts +10 -0
  26. package/package.json +1 -1
  27. package/src/ingest-cache.ts +4 -0
  28. package/src/meter.ts +5 -0
  29. package/src/mind/attention.ts +18 -1
  30. package/src/mind/bridge.ts +292 -54
  31. package/src/mind/junction.ts +21 -7
  32. package/src/mind/match.ts +92 -1
  33. package/src/mind/mechanisms/cast.ts +12 -0
  34. package/src/mind/mechanisms/confluence.ts +30 -1
  35. package/src/mind/mechanisms/cover.ts +36 -4
  36. package/src/mind/mechanisms/recall.ts +21 -44
  37. package/src/mind/mind.ts +39 -2
  38. package/src/mind/pipeline-mechanism.ts +86 -4
  39. package/src/mind/pipeline.ts +1 -1
  40. package/src/mind/reasoning.ts +15 -8
  41. package/src/mind/recognition.ts +40 -0
  42. package/src/mind/resonance.ts +0 -0
  43. package/src/mind/traverse.ts +17 -15
  44. package/src/mind/types.ts +10 -0
  45. package/test/49-natural-units-synonym-bridge.test.mjs +56 -15
@@ -46,7 +46,7 @@ async function collectComputed(ctx, mechanisms, query) {
46
46
  // invests in its own precomputation. CAST's and confluence's floors (2*STEP,
47
47
  // 3*STEP) then fail `worthRunning` and are skipped by the SAME admissible-
48
48
  // floor pruning every mechanism is already subject to — not by asking
49
- // "is this an extension?". Grade TIES keep the earlier candidate, so this
49
+ // "is this an extension?". Grade TIES keep the earlier candidate, so this
50
50
  // order is also the tie-break priority: cover, cast, confluence, extraction,
51
51
  // recall.
52
52
  export const defaultMechanisms = [
@@ -65,17 +65,25 @@ export async function reason(ctx, query, answer, preConsumed, pre) {
65
65
  // grounding can pre-consume one node per recognised site, O(query length));
66
66
  // nodes past the cap are still consumed directly, they just skip the
67
67
  // synonym expansion.
68
- let haloSweeps = 0;
69
- for (const id of preConsumed) {
70
- consumeNode(id);
71
- if (haloSweeps >= ctx.cfg.haloQueryK)
72
- continue;
73
- const h = ctx.store.halo(id);
74
- if (!h)
75
- continue;
76
- haloSweeps++;
77
- for (const sib of await haloSiblings(ctx, id, h))
78
- consumeNode(sib.id);
68
+ const preconsume = async () => {
69
+ let haloSweeps = 0;
70
+ for (const id of preConsumed) {
71
+ consumeNode(id);
72
+ if (haloSweeps >= ctx.cfg.haloQueryK)
73
+ continue;
74
+ const h = ctx.store.halo(id);
75
+ if (!h)
76
+ continue;
77
+ haloSweeps++;
78
+ for (const sib of await haloSiblings(ctx, id, h))
79
+ consumeNode(sib.id);
80
+ }
81
+ };
82
+ if (ctx.meter) {
83
+ await ctx.meter.time("reason.preconsumeHalos", preconsume);
84
+ }
85
+ else {
86
+ await preconsume();
79
87
  }
80
88
  let cur = answer;
81
89
  const qv = pre.guide; // the response-wide guide IS the query's gist
@@ -333,6 +333,47 @@ function recogniseImpl(ctx, bytes) {
333
333
  return null;
334
334
  return singleLeaf[p];
335
335
  };
336
+ // ── exact query-edge forms beyond the canonical chain reach ─────────
337
+ //
338
+ // At corpus scale off-boundary atom chains are deliberately suppressed,
339
+ // but a whole trained form can be longer than chainReach(W) and sit at a
340
+ // query edge without being a subtree of the query's larger root. Appending
341
+ // another topic demonstrates the failure: the exact 30-byte trained
342
+ // question `What is the capital of France?` ends inside the larger query's
343
+ // [27,33) content segment, so neither the structural walk nor a W² chain
344
+ // can name it.
345
+ //
346
+ // Probe only prefix/suffix endpoints within one maximum segment of the
347
+ // query's own content cuts. The flat-branch lookup is byte-exact and runs
348
+ // before resolveSpan pays for a fold; approximate evidence never enters.
349
+ // This tier is needed only where atom chains are suppressed. Small stores
350
+ // retain their existing decomposition unchanged.
351
+ if (atomsAreHubs) {
352
+ const allLeafIds = singleLeaf.map((x) => x?.id ?? null);
353
+ if (allLeafIds.every((x) => x !== null)) {
354
+ const radius = ctx.space.seats.length;
355
+ const endpoints = new Set([0, bytes.length]);
356
+ for (const cut of startList) {
357
+ for (let p = Math.max(0, cut - radius); p <= Math.min(bytes.length, cut + radius); p++)
358
+ endpoints.add(p);
359
+ }
360
+ const ordered = [...endpoints].sort((a, b) => a - b);
361
+ const probe = (start, end) => {
362
+ if (end - start < W || end - start <= chainReach(W))
363
+ return;
364
+ const ids = allLeafIds.slice(start, end);
365
+ if (store.findBranch(ids) === null)
366
+ return;
367
+ const id = resolveSpan(start, end);
368
+ if (id !== null)
369
+ emit(start, end, id);
370
+ };
371
+ for (const end of ordered)
372
+ probe(0, end);
373
+ for (const start of ordered)
374
+ probe(start, bytes.length);
375
+ }
376
+ }
336
377
  const chunkEnd = new Uint32Array(bytes.length);
337
378
  const sorted = [...starts].sort((a, b) => a - b);
338
379
  for (let si = 0; si < sorted.length; si++) {
Binary file
@@ -1,6 +1,6 @@
1
1
  import { Vec } from "../vec.js";
2
2
  import type { AncestorReach, MindContext } from "./types.js";
3
- /** The reach memo this response should use — see the note above.
3
+ /** The reach memo this ask should use — see the note above.
4
4
  *
5
5
  * A TRACED response always gets a fresh, empty one. `AncestorReach`'s
6
6
  * `visited`/`maxDepth`/`saturation` fields are populated only when a trace
@@ -11,6 +11,8 @@ import type { AncestorReach, MindContext } from "./types.js";
11
11
  * Consistent with AGENTS §2.11: a traced response is a different machine —
12
12
  * never benchmark with a trace attached. */
13
13
  export declare function sharedReachMemo(ctx: MindContext): Map<number, AncestorReach>;
14
+ /** Invalidate every session-lifetime structural read after a write. */
15
+ export declare function invalidateStructuralCaches(ctx: MindContext): void;
14
16
  /** Climb the structural DAG from a node to its edge-bearing ancestor contexts.
15
17
  * Ascent stops at hub nodes (parents > √N) — their reach is non-discriminative.
16
18
  * When the start node has no structural parents, climbs from containment parents
@@ -18,20 +18,16 @@ const structCaches = new WeakMap();
18
18
  // every mechanism that prices commonality" — was reached only by confluence.
19
19
  // The climb is by far the biggest consumer.
20
20
  //
21
- // Keyed off `ctx.climbMemo`'s OBJECT IDENTITY, exactly like the struct cache
22
- // above, which buys the right lifetime for free: a plain respond() has a
23
- // fresh climbMemo, so the memo is response-scoped; a conversation turn has
24
- // the conversation's persistent one, so it is conversation-scoped. That
25
- // matters — the stable-prefix fold makes each turn's subtree independent of
26
- // what follows, so 59–70% of a later turn's climb regions are byte-identical
27
- // repeats of an earlier turn's (measured on a 4-turn session), and every one
28
- // of them used to re-climb from cold.
21
+ // Keyed by the Mind's structural lifecycle identity: ordinary and
22
+ // conversational asks share it, and every ingest invalidates it. A real
23
+ // battery repeatedly reaches the same corpus scaffolding even when its
24
+ // surface questions differ.
29
25
  //
30
26
  // Budgeted, not unbounded (AGENTS §2.12): past the cap the whole map is
31
27
  // dropped and re-derived, costing a cold climb and never a wrong answer.
32
28
  const REACH_MEMO_MAX = 100_000;
33
29
  const reachCaches = new WeakMap();
34
- /** The reach memo this response should use — see the note above.
30
+ /** The reach memo this ask should use — see the note above.
35
31
  *
36
32
  * A TRACED response always gets a fresh, empty one. `AncestorReach`'s
37
33
  * `visited`/`maxDepth`/`saturation` fields are populated only when a trace
@@ -44,9 +40,9 @@ const reachCaches = new WeakMap();
44
40
  export function sharedReachMemo(ctx) {
45
41
  if (ctx.trace !== null || ctx.climbMemo === null)
46
42
  return new Map();
47
- let m = reachCaches.get(ctx.climbMemo);
43
+ let m = reachCaches.get(ctx._structMemoKey);
48
44
  if (m === undefined)
49
- reachCaches.set(ctx.climbMemo, m = new Map());
45
+ reachCaches.set(ctx._structMemoKey, m = new Map());
50
46
  else if (m.size >= REACH_MEMO_MAX)
51
47
  m.clear();
52
48
  return m;
@@ -54,9 +50,9 @@ export function sharedReachMemo(ctx) {
54
50
  function getStructCache(ctx) {
55
51
  if (ctx.climbMemo === null)
56
52
  return null;
57
- let c = structCaches.get(ctx.climbMemo);
53
+ let c = structCaches.get(ctx._structMemoKey);
58
54
  if (c === undefined) {
59
- structCaches.set(ctx.climbMemo, c = {
55
+ structCaches.set(ctx._structMemoKey, c = {
60
56
  hasNext: new Map(),
61
57
  prevCount: new Map(),
62
58
  hasParents: new Map(),
@@ -64,6 +60,11 @@ function getStructCache(ctx) {
64
60
  }
65
61
  return c;
66
62
  }
63
+ /** Invalidate every session-lifetime structural read after a write. */
64
+ export function invalidateStructuralCaches(ctx) {
65
+ reachCaches.delete(ctx._structMemoKey);
66
+ structCaches.delete(ctx._structMemoKey);
67
+ }
67
68
  /** Cached {@link Store.hasNext} — pure during one respond(). */
68
69
  function cachedHasNext(ctx, id, cache) {
69
70
  if (cache === null)
@@ -232,6 +232,9 @@ export interface MindContext extends GraphSearchHost {
232
232
  * Null outside respond(); during respondTurn() the conversation's
233
233
  * persistent map is swapped in. */
234
234
  climbMemo: Map<string, Map<string, AttentionRead>> | null;
235
+ /** Stable identity for session-lifetime, write-invalidated structural
236
+ * caches. Query-level climb results remain on climbMemo. */
237
+ _structMemoKey: object;
235
238
  /** Memo of {@link recognise} — content-keyed (latin1) so recognised
236
239
  * forms carry forward across conversation turns. Bypassed while a
237
240
  * trace is attached. Null outside respond(). */
@@ -251,6 +254,13 @@ export interface MindContext extends GraphSearchHost {
251
254
  id: number;
252
255
  len: number;
253
256
  }> | null;
257
+ /** Completed assistant-turn byte spans in the current cumulative query.
258
+ * Empty for ordinary respond(); response-scoped structural context for
259
+ * mechanisms that must not re-derive already-produced replies. */
260
+ answeredSpans: ReadonlyArray<readonly [number, number]>;
261
+ /** Start offset of the user turn currently being answered. Zero for an
262
+ * ordinary respond() and for the first turn of a conversation. */
263
+ currentTurnStart: number;
254
264
  _edgeGuide: Vec | null;
255
265
  _edgeChoice: Map<number, number>;
256
266
  _prevSeen: Set<number> | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hviana/sema",
3
- "version": "0.4.1",
3
+ "version": "0.4.3",
4
4
  "description": "Sema: a non-parametric, instance-based reasoning system.",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",
@@ -42,6 +42,8 @@ import { bindSeat, companySignature, type Sema } from "./sema.js";
42
42
  import type { Input } from "./mind/index.js";
43
43
  import { BoundedMap } from "./store.js";
44
44
  import type { Vec } from "./vec.js";
45
+ import { invalidateStructuralCaches } from "./mind/traverse.js";
46
+ import { invalidateJunctionCache } from "./mind/junction.js";
45
47
 
46
48
  /** The interned result of perceiving + interning ONE input.
47
49
  *
@@ -122,6 +124,8 @@ export class CachedIngest {
122
124
  input: Input | (Input | [Input, Input])[],
123
125
  second?: Input,
124
126
  ): Promise<(Sema & { id: number }) | undefined> {
127
+ invalidateStructuralCaches(this.mind);
128
+ invalidateJunctionCache(this.mind);
125
129
  // One shape-reading for both ingest paths — see {@link dispatchIngest}.
126
130
  return dispatchIngest(
127
131
  input,
package/src/meter.ts CHANGED
@@ -176,6 +176,11 @@ 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
+ /** Arbitrary byte spans whose distributional company was VSA-bundled from
180
+ * existing episode halos. */
181
+ spanHalos = 0;
182
+ /** Canonical W-windows examined while composing those span halos. */
183
+ spanHaloWindows = 0;
179
184
 
180
185
  /** `lightestDerivation` searches started. */
181
186
  searches = 0;
@@ -2275,6 +2275,16 @@ async function crossRegionVotes(
2275
2275
  // the same container (or a sub-container of it) twice.
2276
2276
  const consumed = new Set<number>();
2277
2277
  let probes = 0;
2278
+ // Once atoms themselves are hubs (N > W²), the cross-region analysis gets
2279
+ // one k·W walk allowance per evidence tier. Without a shared allowance,
2280
+ // each of k candidate pairs spends the full corpus-derived budget and a
2281
+ // cumulative dialogue multiplies bounded work into tens of seconds. Small
2282
+ // corpora retain exhaustive exact traversal: below this same scale the
2283
+ // budget would be smaller than the structures the tests deliberately build.
2284
+ const marketScale = k * ctx.space.maxGroup;
2285
+ const corpusScale = N > marketScale ** 3;
2286
+ const exactBudget = corpusScale ? { n: k * ctx.space.maxGroup } : undefined;
2287
+ const synonymBudget = corpusScale ? { n: k * ctx.space.maxGroup } : undefined;
2278
2288
 
2279
2289
  for (let a = 0; a < cand.length && probes < k; a++) {
2280
2290
  if (consumed.has(cand[a])) continue;
@@ -2284,6 +2294,12 @@ async function crossRegionVotes(
2284
2294
  const rb = regions[cand[b]];
2285
2295
  if (!strong.has(cand[a]) && !strong.has(cand[b])) continue;
2286
2296
  if (ra.end >= rb.start) continue; // overlap or adjacent — nothing between
2297
+ // In a cumulative conversation, an old↔old interaction cannot explain
2298
+ // the user turn currently being answered; it was already available
2299
+ // before that turn existed. Keep old↔current pairs (the current turn may
2300
+ // refer to a prior answer), but do not repeatedly spend the junction
2301
+ // budget recomposing two regions wholly before the current boundary.
2302
+ if (ctx.currentTurnStart > 0 && rb.end <= ctx.currentTurnStart) continue;
2287
2303
  // Candidates strictly BETWEEN ra and rb (cand is sorted by start, so
2288
2304
  // that is exactly cand[a+1 .. b-1]) that already cast their OWN vote —
2289
2305
  // genuine, individually-corroborated evidence about what fills the gap
@@ -2364,7 +2380,7 @@ async function crossRegionVotes(
2364
2380
  cap,
2365
2381
  seedsOf(cand[a]),
2366
2382
  seedsOf(cand[b]),
2367
- undefined,
2383
+ exactBudget,
2368
2384
  true,
2369
2385
  );
2370
2386
  if (probe) {
@@ -2384,6 +2400,7 @@ async function crossRegionVotes(
2384
2400
  maxInterior,
2385
2401
  true,
2386
2402
  sides,
2403
+ synonymBudget,
2387
2404
  );
2388
2405
  if (probe) {
2389
2406
  const singleAttempted = sides.leftSiblings.length > 0 ||