@hviana/sema 0.1.0 → 0.1.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 (131) hide show
  1. package/dist/example/demo.d.ts +1 -0
  2. package/dist/example/demo.js +39 -0
  3. package/dist/example/train_base.d.ts +87 -0
  4. package/dist/example/train_base.js +2216 -0
  5. package/dist/src/alphabet.d.ts +7 -0
  6. package/dist/src/alphabet.js +33 -0
  7. package/dist/src/alu/src/alu.d.ts +185 -0
  8. package/dist/src/alu/src/alu.js +440 -0
  9. package/dist/src/alu/src/expr.d.ts +61 -0
  10. package/dist/src/alu/src/expr.js +318 -0
  11. package/dist/src/alu/src/index.d.ts +11 -0
  12. package/dist/src/alu/src/index.js +19 -0
  13. package/dist/src/alu/src/kernel-arith.d.ts +16 -0
  14. package/dist/src/alu/src/kernel-arith.js +264 -0
  15. package/dist/src/alu/src/kernel-bits.d.ts +19 -0
  16. package/dist/src/alu/src/kernel-bits.js +152 -0
  17. package/dist/src/alu/src/kernel-logic.d.ts +4 -0
  18. package/dist/src/alu/src/kernel-logic.js +60 -0
  19. package/dist/src/alu/src/kernel-nd.d.ts +3 -0
  20. package/dist/src/alu/src/kernel-nd.js +208 -0
  21. package/dist/src/alu/src/kernel-numeric.d.ts +54 -0
  22. package/dist/src/alu/src/kernel-numeric.js +366 -0
  23. package/dist/src/alu/src/operation.d.ts +168 -0
  24. package/dist/src/alu/src/operation.js +189 -0
  25. package/dist/src/alu/src/parser.d.ts +212 -0
  26. package/dist/src/alu/src/parser.js +469 -0
  27. package/dist/src/alu/src/resonance.d.ts +55 -0
  28. package/dist/src/alu/src/resonance.js +126 -0
  29. package/dist/src/alu/src/text.d.ts +31 -0
  30. package/dist/src/alu/src/text.js +73 -0
  31. package/dist/src/alu/src/value.d.ts +109 -0
  32. package/dist/src/alu/src/value.js +300 -0
  33. package/dist/src/alu/test/alu.test.d.ts +1 -0
  34. package/dist/src/alu/test/alu.test.js +764 -0
  35. package/dist/src/bytes.d.ts +14 -0
  36. package/dist/src/bytes.js +59 -0
  37. package/dist/src/config.d.ts +114 -0
  38. package/dist/src/config.js +96 -0
  39. package/dist/src/derive/src/deduction.d.ts +125 -0
  40. package/dist/src/derive/src/deduction.js +155 -0
  41. package/dist/src/derive/src/index.d.ts +7 -0
  42. package/dist/src/derive/src/index.js +11 -0
  43. package/dist/src/derive/src/priority-queue.d.ts +20 -0
  44. package/dist/src/derive/src/priority-queue.js +73 -0
  45. package/dist/src/derive/src/rewrite.d.ts +56 -0
  46. package/dist/src/derive/src/rewrite.js +100 -0
  47. package/dist/src/derive/src/trie.d.ts +90 -0
  48. package/dist/src/derive/src/trie.js +217 -0
  49. package/dist/src/derive/test/derive.test.d.ts +1 -0
  50. package/dist/src/derive/test/derive.test.js +122 -0
  51. package/dist/src/extension.d.ts +37 -0
  52. package/dist/src/extension.js +7 -0
  53. package/dist/src/geometry.d.ts +137 -0
  54. package/dist/src/geometry.js +430 -0
  55. package/dist/src/index.d.ts +15 -0
  56. package/dist/src/index.js +21 -0
  57. package/dist/src/ingest-cache.d.ts +41 -0
  58. package/dist/src/ingest-cache.js +161 -0
  59. package/dist/src/mind/articulation.d.ts +6 -0
  60. package/dist/src/mind/articulation.js +99 -0
  61. package/dist/src/mind/attention.d.ts +72 -0
  62. package/dist/src/mind/attention.js +894 -0
  63. package/dist/src/mind/canonical.d.ts +29 -0
  64. package/dist/src/mind/canonical.js +88 -0
  65. package/dist/src/mind/graph-search.d.ts +270 -0
  66. package/dist/src/mind/graph-search.js +847 -0
  67. package/dist/src/mind/index.d.ts +5 -0
  68. package/dist/src/mind/index.js +5 -0
  69. package/dist/src/mind/junction.d.ts +95 -0
  70. package/dist/src/mind/junction.js +262 -0
  71. package/dist/src/mind/learning.d.ts +47 -0
  72. package/dist/src/mind/learning.js +201 -0
  73. package/dist/src/mind/match.d.ts +111 -0
  74. package/dist/src/mind/match.js +422 -0
  75. package/dist/src/mind/mechanisms/alu.d.ts +4 -0
  76. package/dist/src/mind/mechanisms/alu.js +29 -0
  77. package/dist/src/mind/mechanisms/cast.d.ts +35 -0
  78. package/dist/src/mind/mechanisms/cast.js +447 -0
  79. package/dist/src/mind/mechanisms/confluence.d.ts +24 -0
  80. package/dist/src/mind/mechanisms/confluence.js +213 -0
  81. package/dist/src/mind/mechanisms/cover.d.ts +6 -0
  82. package/dist/src/mind/mechanisms/cover.js +179 -0
  83. package/dist/src/mind/mechanisms/extraction.d.ts +67 -0
  84. package/dist/src/mind/mechanisms/extraction.js +342 -0
  85. package/dist/src/mind/mechanisms/recall.d.ts +13 -0
  86. package/dist/src/mind/mechanisms/recall.js +151 -0
  87. package/dist/src/mind/mind.d.ts +147 -0
  88. package/dist/src/mind/mind.js +300 -0
  89. package/dist/src/mind/pipeline-mechanism.d.ts +142 -0
  90. package/dist/src/mind/pipeline-mechanism.js +213 -0
  91. package/dist/src/mind/pipeline.d.ts +20 -0
  92. package/dist/src/mind/pipeline.js +185 -0
  93. package/dist/src/mind/primitives.d.ts +43 -0
  94. package/dist/src/mind/primitives.js +162 -0
  95. package/dist/src/mind/rationale.d.ts +134 -0
  96. package/dist/src/mind/rationale.js +162 -0
  97. package/dist/src/mind/reasoning.d.ts +15 -0
  98. package/dist/src/mind/reasoning.js +162 -0
  99. package/dist/src/mind/recognition.d.ts +20 -0
  100. package/dist/src/mind/recognition.js +223 -0
  101. package/dist/src/mind/resonance.d.ts +23 -0
  102. package/dist/src/mind/resonance.js +0 -0
  103. package/dist/src/mind/trace.d.ts +15 -0
  104. package/dist/src/mind/trace.js +73 -0
  105. package/dist/src/mind/traverse.d.ts +100 -0
  106. package/dist/src/mind/traverse.js +447 -0
  107. package/dist/src/mind/types.d.ts +174 -0
  108. package/dist/src/mind/types.js +84 -0
  109. package/dist/src/rabitq-hnsw/src/database.d.ts +200 -0
  110. package/dist/src/rabitq-hnsw/src/database.js +388 -0
  111. package/dist/src/rabitq-hnsw/src/heap.d.ts +22 -0
  112. package/dist/src/rabitq-hnsw/src/heap.js +89 -0
  113. package/dist/src/rabitq-hnsw/src/hnsw.d.ts +125 -0
  114. package/dist/src/rabitq-hnsw/src/hnsw.js +474 -0
  115. package/dist/src/rabitq-hnsw/src/index.d.ts +10 -0
  116. package/dist/src/rabitq-hnsw/src/index.js +6 -0
  117. package/dist/src/rabitq-hnsw/src/prng.d.ts +19 -0
  118. package/dist/src/rabitq-hnsw/src/prng.js +36 -0
  119. package/dist/src/rabitq-hnsw/src/rabitq.d.ts +95 -0
  120. package/dist/src/rabitq-hnsw/src/rabitq.js +283 -0
  121. package/dist/src/rabitq-hnsw/src/store.d.ts +162 -0
  122. package/dist/src/rabitq-hnsw/src/store.js +825 -0
  123. package/dist/src/rabitq-hnsw/test/hnsw.test.d.ts +1 -0
  124. package/dist/src/rabitq-hnsw/test/hnsw.test.js +948 -0
  125. package/dist/src/store-sqlite.d.ts +149 -0
  126. package/dist/src/store-sqlite.js +702 -0
  127. package/dist/src/store.d.ts +638 -0
  128. package/dist/src/store.js +1618 -0
  129. package/dist/src/vec.d.ts +31 -0
  130. package/dist/src/vec.js +109 -0
  131. package/package.json +1 -1
@@ -0,0 +1,73 @@
1
+ // trace.ts — trace instrumentation + persistence (Section 9 of the mind).
2
+ //
3
+ // rItem, rNode, rDeriv — build RationaleItems from bytes/nodes/derivations
4
+ // traceDerivation — trace a full derivation proof tree
5
+ // MOVE_NOTE — human-readable names for each derivation move
6
+ import { read } from "./primitives.js";
7
+ import { decodeText } from "./rationale.js";
8
+ export function rItem(bytes, role, node, span) {
9
+ return {
10
+ text: decodeText(bytes),
11
+ role,
12
+ node: node ?? undefined,
13
+ span,
14
+ };
15
+ }
16
+ export function rNode(ctx, id, role, score) {
17
+ return {
18
+ text: decodeText(read(ctx, id)),
19
+ node: id,
20
+ role,
21
+ score,
22
+ };
23
+ }
24
+ export function rDeriv(ctx, it, role) {
25
+ const text = it.bytes
26
+ ? decodeText(it.bytes)
27
+ : it.node !== undefined
28
+ ? decodeText(read(ctx, it.node))
29
+ : it.kind === "cover"
30
+ ? `cover@${it.span[0]}`
31
+ : `[${it.span[0]},${it.span[1]})`;
32
+ return { text, role: role ?? it.kind, node: it.node, span: it.span };
33
+ }
34
+ /** The standard FALL-THROUGH closer every self-gating mechanism ends with:
35
+ * close the open scope with no outputs and the reason, and return null so
36
+ * the caller can `return fail("…")` in one expression. `t` is the scope an
37
+ * enclosing `ctx.trace?.enter(...)` returned (undefined when not tracing). */
38
+ export function traceFail(t) {
39
+ return (note) => {
40
+ t?.done([], note);
41
+ return null;
42
+ };
43
+ }
44
+ export const MOVE_NOTE = {
45
+ "follow-edge": "follow a learned continuation edge — 'what follows what'",
46
+ "concept-hop": "jump a concept (halo) link — a synonym's edge",
47
+ "voice": "emit the asker's own wording for this form (articulation)",
48
+ "ground": "a chain reached its terminal answer",
49
+ "splice-connector": "splice a learnt connector between two rewrites",
50
+ "split": "cut a span at a sub-leaf form boundary so a form can be reached",
51
+ "fuse": "fuse adjacent fragments toward a deeper learned form",
52
+ "recompose": "recompose fused parts into a learned whole that leads on",
53
+ "bridge": "advance the cover frontier across this span",
54
+ "pool-vote": "pool independent regions' evidence for a shared anchor (sum, not shortest path)",
55
+ "axiom": "a seed: a perceived leaf, recognised form, or computed result",
56
+ "step": "a derivation step",
57
+ };
58
+ export function traceDerivation(ctx, steps) {
59
+ const t = ctx.trace;
60
+ if (!t)
61
+ return;
62
+ const indexOfOrder = new Map();
63
+ for (const s of steps) {
64
+ const note = MOVE_NOTE[s.move] ?? s.move;
65
+ const deps = s.producers
66
+ .map((o) => indexOfOrder.get(o))
67
+ .filter((x) => x !== undefined);
68
+ const premises = s.premises.map((p) => rDeriv(ctx, p));
69
+ const conclusion = [rDeriv(ctx, s.conclusion)];
70
+ const index = t.step(s.move, premises, conclusion, s.cost > 0 ? `${note} (cost ${s.cost})` : note, deps.length > 0 ? deps : undefined);
71
+ indexOfOrder.set(s.order, index);
72
+ }
73
+ }
@@ -0,0 +1,100 @@
1
+ import { Vec } from "../vec.js";
2
+ import type { AncestorReach, MindContext } from "./types.js";
3
+ /** Climb the structural DAG from a node to its edge-bearing ancestor contexts.
4
+ * Ascent stops at hub nodes (parents > √N) — their reach is non-discriminative.
5
+ * When the start node has no structural parents, climbs from containment parents
6
+ * (sub-span flat branches inheriting their chunks' context).
7
+ *
8
+ * `memo`, when given, caches whole climbs by start id for the duration of ONE
9
+ * query (the store is read-only while a query is in flight, so a climb is a
10
+ * pure function of the id). The consensus pipeline climbs the SAME anchors
11
+ * repeatedly — regions sharing a chunk, and canonicalChunkId probing each
12
+ * chunk's prefixes — so without the memo every repeat re-pays the full
13
+ * fan-out reads. */
14
+ export declare function edgeAncestors(ctx: MindContext, id: number, contextCount: number, memo?: Map<number, AncestorReach>): AncestorReach;
15
+ /** Convenience: forward edges of a node. */
16
+ export declare function nextOf(ctx: MindContext, id: number): number[];
17
+ /** Convenience: reverse edges of a node. */
18
+ export declare function prevOf(ctx: MindContext, id: number): number[];
19
+ /** Whether a node LEADS SOMEWHERE — it bears a continuation edge or a halo.
20
+ * The admission predicate recognition filters sites with (HOW_IT_WORKS
21
+ * §15.3): a form that leads nowhere contributes nothing to any derivation.
22
+ * Runs once per candidate span on the recognition hot path — `hasNext` is
23
+ * cached per response (the same flat-branch ids are probed across prefix
24
+ * variants by canonicalChunkId). `hasHalo` is not cached: it's a single
25
+ * indexed point probe per candidate, and the candidates that reach this
26
+ * check have already been filtered by hasNext above in edgeAncestors. */
27
+ export declare function leadsSomewhere(ctx: MindContext, id: number): boolean;
28
+ /** The structural IDF read of ONE node: how many distinct learnt contexts
29
+ * its containment/edge climb reaches, or Infinity when it reaches none or
30
+ * saturates (no usable identity evidence). The number every
31
+ * discriminative-vs-scaffolding decision derives from — paired with the
32
+ * half-dominance convention (geometry.dominates(reach, N)): content
33
+ * reaching a corpus MINORITY of contexts discriminates (an entity, a
34
+ * filler); content reaching a majority is frame scaffolding. */
35
+ export declare function reachOf(ctx: MindContext, id: number, contextCount: number, memo?: Map<number, AncestorReach>): number;
36
+ /** The corpus scale N — the count of DISTINCT learnt contexts, floored at 2
37
+ * so its derived readings (ln N in the consensus floor, √N in the hub bound)
38
+ * stay meaningful on a near-empty store. The one definition every consumer
39
+ * of "how big is this corpus?" reads. */
40
+ export declare function corpusN(ctx: MindContext): number;
41
+ /** The hub bound √N itself (≥ 2 always, since N is floored at 2) — for
42
+ * consumers that pass it to the store's LIMITed reads instead of capping a
43
+ * materialised list. {@link hubCap} is the list-side reading of the same
44
+ * convention. */
45
+ export declare function hubBound(ctx: MindContext): number;
46
+ /** Cap a candidate list at the hub bound √N (insertion order) — the ONE
47
+ * fan-out convention every walk and disambiguation uses (see HOW_IT_WORKS
48
+ * §8.6). A node connected to more than √N others is a hub whose individual
49
+ * connections carry ~no discriminative information; materialising or scoring
50
+ * them all would make single decisions scale with the corpus. */
51
+ export declare function hubCap<T>(ctx: MindContext, ids: readonly T[]): readonly T[];
52
+ /** Whether `descendant` lies within `ancestor`'s subtree — a structural DAG
53
+ * relation read off the hash-consed `kids` lists, by a bounded explicit-stack
54
+ * descent. Used by articulation to keep a voice from revoicing a fragment
55
+ * OF that voice. */
56
+ export declare function contains(ctx: MindContext, ancestor: number, descendant: number): boolean;
57
+ /** The best-scoring item by cosine against `query`, among items scoring at
58
+ * or above `threshold` — the shared arg-max every Pattern-A "which of these
59
+ * resonates best" decision reduces to. `strict` picks the tie-break a
60
+ * caller needs: `true` keeps the first-seen leader on a tie (`>`), the
61
+ * default lets a later equal score take it (`>=`). */
62
+ export declare function argmaxBy<T>(items: Iterable<T>, scoreOf: (item: T) => number, threshold: number, strict?: boolean): {
63
+ item: T;
64
+ score: number;
65
+ } | null;
66
+ export declare function argmaxCosine<T>(query: Vec, items: Iterable<T>, vecOf: (item: T) => Vec | null | undefined, threshold: number, strict?: boolean): {
67
+ item: T;
68
+ score: number;
69
+ } | null;
70
+ /** The guided-or-first continuation of a node, as answer-shaped bytes source:
71
+ * chooseNext under the response guide, falling back to the FIRST-inserted
72
+ * edge — the one no-guide convention chooseNext, project() and the search's
73
+ * formRules all share. undefined when the node has no continuation. */
74
+ export declare function guidedFirst(ctx: MindContext, id: number): number | undefined;
75
+ export declare function guidedNext(ctx: MindContext, node: number): number | undefined;
76
+ /** Disambiguate among a node's learnt continuations by distributional
77
+ * support. NOTE the `guide` contract: its VALUE is deliberately unused —
78
+ * only its PRESENCE gates disambiguation (a null guide means no query is in
79
+ * flight, so structural walkers keep plain first-edge behaviour). The
80
+ * gist-cosine of short answer candidates against a query guide is dominated
81
+ * by accidental byte-pattern correlations, not semantic relatedness, so the
82
+ * evidence consulted is structural: each candidate's reverse-edge support
83
+ * count (see below). Contrast {@link chooseAmong}, the REVERSE-direction
84
+ * disambiguator, whose candidates are whole learnt contexts — long enough
85
+ * that their perceived gists ARE semantically meaningful — and which
86
+ * therefore scores by guide cosine. The two directions consult different
87
+ * halves of the evidence on purpose. */
88
+ export declare function chooseNext(ctx: MindContext, id: number, guide?: Vec | null): number | undefined;
89
+ /** The perceived gist of a candidate node, through the session gist cache.
90
+ * Re-gisting a candidate is a full river fold of its bytes — the measured
91
+ * recall bottleneck (a hub context offers up to √N continuations, EACH
92
+ * re-perceived per pick). A node's bytes are immutable and perception is
93
+ * pure, so the cached gist is valid for the store's lifetime. Exported for
94
+ * every "score node ids against a guide" decision (chooseAmong here, the
95
+ * bridge's junction pick) so they share ONE cache and one convention. */
96
+ export declare function candidateGist(ctx: MindContext, c: number): Vec | null;
97
+ export declare function chooseAmong(ctx: MindContext, candidates: readonly number[], guide: Vec): {
98
+ id: number;
99
+ score: number;
100
+ };
@@ -0,0 +1,447 @@
1
+ // traverse.ts — Traverse primitives + disambiguation (Sections 1 & 6 of the mind).
2
+ //
3
+ // Traverse — node → nodes (edgeAncestors, nextOf, prevOf, contains,
4
+ // guidedNext, chooseNext, chooseAmong, hubCap)
5
+ //
6
+ // The PROJECTIONS built on these walks (follow, conceptHop, reverseContext,
7
+ // project) live in match.ts — the elementary match-and-project operation.
8
+ import { cosine } from "../vec.js";
9
+ import { gistOf, read } from "./primitives.js";
10
+ const structCaches = new WeakMap();
11
+ function getStructCache(ctx) {
12
+ if (ctx.climbMemo === null)
13
+ return null;
14
+ let c = structCaches.get(ctx.climbMemo);
15
+ if (c === undefined) {
16
+ structCaches.set(ctx.climbMemo, c = {
17
+ hasNext: new Map(),
18
+ prevCount: new Map(),
19
+ hasParents: new Map(),
20
+ });
21
+ }
22
+ return c;
23
+ }
24
+ /** Cached {@link Store.hasNext} — pure during one respond(). */
25
+ function cachedHasNext(ctx, id, cache) {
26
+ if (cache === null)
27
+ return ctx.store.hasNext(id);
28
+ let v = cache.hasNext.get(id);
29
+ if (v === undefined) {
30
+ v = ctx.store.hasNext(id);
31
+ cache.hasNext.set(id, v);
32
+ }
33
+ return v;
34
+ }
35
+ /** Cached {@link Store.prevCount} — pure during one respond(). */
36
+ function cachedPrevCount(ctx, id, cache) {
37
+ if (cache === null)
38
+ return ctx.store.prevCount(id);
39
+ let v = cache.prevCount.get(id);
40
+ if (v === undefined) {
41
+ v = ctx.store.prevCount(id);
42
+ cache.prevCount.set(id, v);
43
+ }
44
+ return v;
45
+ }
46
+ /** Cached {@link Store.hasParents} — pure during one respond(). */
47
+ function cachedHasParents(ctx, id, cache) {
48
+ if (cache === null)
49
+ return ctx.store.hasParents(id);
50
+ let v = cache.hasParents.get(id);
51
+ if (v === undefined) {
52
+ v = ctx.store.hasParents(id);
53
+ cache.hasParents.set(id, v);
54
+ }
55
+ return v;
56
+ }
57
+ // ── Graph climbing ───────────────────────────────────────────────────────
58
+ /** Climb the structural DAG from a node to its edge-bearing ancestor contexts.
59
+ * Ascent stops at hub nodes (parents > √N) — their reach is non-discriminative.
60
+ * When the start node has no structural parents, climbs from containment parents
61
+ * (sub-span flat branches inheriting their chunks' context).
62
+ *
63
+ * `memo`, when given, caches whole climbs by start id for the duration of ONE
64
+ * query (the store is read-only while a query is in flight, so a climb is a
65
+ * pure function of the id). The consensus pipeline climbs the SAME anchors
66
+ * repeatedly — regions sharing a chunk, and canonicalChunkId probing each
67
+ * chunk's prefixes — so without the memo every repeat re-pays the full
68
+ * fan-out reads. */
69
+ export function edgeAncestors(ctx, id, contextCount, memo) {
70
+ const hit = memo?.get(id);
71
+ if (hit !== undefined)
72
+ return hit;
73
+ const bound = Math.ceil(Math.sqrt(contextCount));
74
+ const roots = [];
75
+ const seen = new Set([id]);
76
+ const ctxSeen = new Set();
77
+ let saturated = false;
78
+ // EXPAND-UNTIL-DECIDED: a reach is consumed either as a VOTE (which needs
79
+ // contextsReached exactly, and only while ≤ √N — beyond that the region is
80
+ // non-discriminative) or as an ABSTENTION (saturated — whose roots and
81
+ // counts no consumer reads). So the climb may STOP the moment the answer
82
+ // is decided:
83
+ // • a node whose prev fan-in alone exceeds √N decides it (its
84
+ // predecessors are √N+ distinct contexts) — no read needed, prevCount
85
+ // is an indexed O(1);
86
+ // • distinct contexts crossing √N decides it;
87
+ // • a node with more than √N parents decides its own expansion (the
88
+ // classic hub guard; the walk aborts rather than continue, which no
89
+ // consumer can distinguish — saturated reaches are never voted).
90
+ // Below every decision threshold the walk is EXACT — identical roots and
91
+ // contexts to the unbounded climb — because prevFirst(√N) IS the full prev
92
+ // list and parentsFirst(√N+1) IS the full parent list whenever they do not
93
+ // decide. Work is bounded by √N contexts × the climb's local structure,
94
+ // never by the corpus.
95
+ const structCache = getStructCache(ctx);
96
+ // LATERAL-BRANCH ACCOUNT — the cumulative dual of the per-node hub guard.
97
+ // Within one deposit the ascent is a CHAIN (each node's first parent);
98
+ // every parent BEYOND a node's first is an entry into another containing
99
+ // structure (hash-consing: a shared subtree's extra parents are other
100
+ // deposits' chunks). The per-node guard already declares a node with more
101
+ // than √N parents non-discriminative; a climb whose ACCUMULATED lateral
102
+ // entries exceed √N has spread across just as many distinct containing
103
+ // structures — the same commonness, distributed along the cone instead of
104
+ // concentrated at one node — and is decided: saturated. A deep chain in
105
+ // ONE structure accrues no laterals, so legitimate deep scaffolding (a
106
+ // fragment far down a long cumulative context) still climbs to its root
107
+ // at any depth; what dies is the cross-structure drift that visited tens
108
+ // of thousands of edge-free interiors (profiled on a 17.7M-node store:
109
+ // ~20K distinct nodes per climb family, >95% unique — not memoisable)
110
+ // while the context account never decided.
111
+ let lateral = 0;
112
+ const visit = (x) => {
113
+ const hasNx = cachedHasNext(ctx, x, structCache);
114
+ const pc = cachedPrevCount(ctx, x, structCache);
115
+ if (hasNx || pc > 0) {
116
+ roots.push(x);
117
+ if (hasNx)
118
+ ctxSeen.add(x);
119
+ if (pc > bound)
120
+ return false; // decided: ≥ pc > √N distinct contexts
121
+ for (const p of ctx.store.prevFirst(x, bound))
122
+ ctxSeen.add(p);
123
+ if (ctxSeen.size > bound)
124
+ return false; // decided
125
+ }
126
+ const parents = ctx.store.parentsFirst(x, bound + 1);
127
+ if (parents.length > bound)
128
+ return false; // decided: hub
129
+ let fresh = 0;
130
+ for (const p of parents) {
131
+ if (!seen.has(p)) {
132
+ seen.add(p);
133
+ stack.push(p);
134
+ fresh++;
135
+ }
136
+ }
137
+ if (fresh > 1) {
138
+ lateral += fresh - 1;
139
+ if (lateral > bound)
140
+ return false; // decided: cone-wide hub
141
+ }
142
+ return true;
143
+ };
144
+ const stack = [];
145
+ const containment = !cachedHasParents(ctx, id, structCache);
146
+ if (!containment)
147
+ stack.push(id);
148
+ // The containment seed is STREAMED in pages of √N: a distinctive window's
149
+ // containers (which converge on one or two contexts, however many chunks
150
+ // of one deposit repeat it) are walked IN FULL — exact — while a common
151
+ // window's corpus-sized container list is abandoned at the first decision
152
+ // above, after O(√N) pages at most (each page adds containers whose climbs
153
+ // add contexts; √N distinct contexts decide).
154
+ let containerOff = 0;
155
+ let containersExhausted = !containment;
156
+ climb: for (;;) {
157
+ if (stack.length === 0) {
158
+ if (containersExhausted)
159
+ break;
160
+ const page = ctx.store.containersSlice(id, containerOff, bound);
161
+ containerOff += page.length;
162
+ if (page.length < bound)
163
+ containersExhausted = true;
164
+ for (const c of page) {
165
+ if (!seen.has(c)) {
166
+ seen.add(c);
167
+ stack.push(c);
168
+ }
169
+ }
170
+ if (stack.length === 0) {
171
+ if (containerOff === 0)
172
+ stack.push(id); // no containers at all
173
+ else
174
+ break;
175
+ }
176
+ }
177
+ while (stack.length > 0) {
178
+ let x = stack.pop();
179
+ // TRANSPARENT-CHAIN HOP: a node with no edges in or out and exactly one
180
+ // parent contributes nothing here — no root, no context, no lateral
181
+ // entry — so the run to its first non-transparent ancestor is skipped
182
+ // in ONE store read (Store.chainRun) instead of three probes per node.
183
+ // The interior nodes still enter `seen`, exactly as a node-at-a-time
184
+ // ascent would have recorded them at push time, so sibling entries into
185
+ // the same chain keep identical fresh/lateral accounting; and if the
186
+ // terminal was already seen (another chain merged into this one first),
187
+ // it is not visited twice — the same dedup the push-time seen-check
188
+ // used to provide.
189
+ const run = ctx.store.chainRun(x);
190
+ if (run.length > 1) {
191
+ const top = run[run.length - 1];
192
+ const dup = seen.has(top);
193
+ for (let i = 1; i < run.length; i++)
194
+ seen.add(run[i]);
195
+ if (dup)
196
+ continue;
197
+ x = top;
198
+ }
199
+ if (!visit(x)) {
200
+ saturated = true;
201
+ break climb;
202
+ }
203
+ }
204
+ }
205
+ const reach = { roots, contextsReached: ctxSeen.size, saturated };
206
+ memo?.set(id, reach);
207
+ return reach;
208
+ }
209
+ /** Convenience: forward edges of a node. */
210
+ export function nextOf(ctx, id) {
211
+ return ctx.store.next(id);
212
+ }
213
+ /** Convenience: reverse edges of a node. */
214
+ export function prevOf(ctx, id) {
215
+ return ctx.store.prev(id);
216
+ }
217
+ /** Whether a node LEADS SOMEWHERE — it bears a continuation edge or a halo.
218
+ * The admission predicate recognition filters sites with (HOW_IT_WORKS
219
+ * §15.3): a form that leads nowhere contributes nothing to any derivation.
220
+ * Runs once per candidate span on the recognition hot path — `hasNext` is
221
+ * cached per response (the same flat-branch ids are probed across prefix
222
+ * variants by canonicalChunkId). `hasHalo` is not cached: it's a single
223
+ * indexed point probe per candidate, and the candidates that reach this
224
+ * check have already been filtered by hasNext above in edgeAncestors. */
225
+ export function leadsSomewhere(ctx, id) {
226
+ const memo = getStructCache(ctx);
227
+ if (cachedHasNext(ctx, id, memo))
228
+ return true;
229
+ return ctx.store.hasHalo(id);
230
+ }
231
+ /** The structural IDF read of ONE node: how many distinct learnt contexts
232
+ * its containment/edge climb reaches, or Infinity when it reaches none or
233
+ * saturates (no usable identity evidence). The number every
234
+ * discriminative-vs-scaffolding decision derives from — paired with the
235
+ * half-dominance convention (geometry.dominates(reach, N)): content
236
+ * reaching a corpus MINORITY of contexts discriminates (an entity, a
237
+ * filler); content reaching a majority is frame scaffolding. */
238
+ export function reachOf(ctx, id, contextCount, memo) {
239
+ const r = edgeAncestors(ctx, id, contextCount, memo);
240
+ if (r.saturated || r.roots.length === 0)
241
+ return Infinity;
242
+ return Math.max(1, r.contextsReached);
243
+ }
244
+ /** The corpus scale N — the count of DISTINCT learnt contexts, floored at 2
245
+ * so its derived readings (ln N in the consensus floor, √N in the hub bound)
246
+ * stay meaningful on a near-empty store. The one definition every consumer
247
+ * of "how big is this corpus?" reads. */
248
+ export function corpusN(ctx) {
249
+ return Math.max(2, ctx.store.edgeSourceCount());
250
+ }
251
+ /** The hub bound √N itself (≥ 2 always, since N is floored at 2) — for
252
+ * consumers that pass it to the store's LIMITed reads instead of capping a
253
+ * materialised list. {@link hubCap} is the list-side reading of the same
254
+ * convention. */
255
+ export function hubBound(ctx) {
256
+ return Math.ceil(Math.sqrt(corpusN(ctx)));
257
+ }
258
+ /** Cap a candidate list at the hub bound √N (insertion order) — the ONE
259
+ * fan-out convention every walk and disambiguation uses (see HOW_IT_WORKS
260
+ * §8.6). A node connected to more than √N others is a hub whose individual
261
+ * connections carry ~no discriminative information; materialising or scoring
262
+ * them all would make single decisions scale with the corpus. */
263
+ export function hubCap(ctx, ids) {
264
+ const bound = hubBound(ctx);
265
+ return ids.length > bound ? ids.slice(0, bound) : ids;
266
+ }
267
+ /** Whether `descendant` lies within `ancestor`'s subtree — a structural DAG
268
+ * relation read off the hash-consed `kids` lists, by a bounded explicit-stack
269
+ * descent. Used by articulation to keep a voice from revoicing a fragment
270
+ * OF that voice. */
271
+ export function contains(ctx, ancestor, descendant) {
272
+ if (ancestor === descendant)
273
+ return true;
274
+ const seen = new Set([ancestor]);
275
+ const stack = [ancestor];
276
+ while (stack.length > 0) {
277
+ const rec = ctx.store.get(stack.pop());
278
+ if (!rec?.kids)
279
+ continue;
280
+ for (const k of rec.kids) {
281
+ if (k === descendant)
282
+ return true;
283
+ if (!seen.has(k)) {
284
+ seen.add(k);
285
+ stack.push(k);
286
+ }
287
+ }
288
+ }
289
+ return false;
290
+ }
291
+ // ── Edge disambiguation (Section 6) ──────────────────────────────────────
292
+ /** The best-scoring item by cosine against `query`, among items scoring at
293
+ * or above `threshold` — the shared arg-max every Pattern-A "which of these
294
+ * resonates best" decision reduces to. `strict` picks the tie-break a
295
+ * caller needs: `true` keeps the first-seen leader on a tie (`>`), the
296
+ * default lets a later equal score take it (`>=`). */
297
+ export function argmaxBy(items, scoreOf, threshold, strict = false) {
298
+ let best = null;
299
+ for (const item of items) {
300
+ const score = scoreOf(item);
301
+ const bar = best?.score ?? threshold;
302
+ if (strict ? score > bar : score >= bar)
303
+ best = { item, score };
304
+ }
305
+ return best;
306
+ }
307
+ export function argmaxCosine(query, items, vecOf, threshold, strict = false) {
308
+ return argmaxBy(items, (item) => {
309
+ const v = vecOf(item);
310
+ return v ? cosine(query, v) : -Infinity;
311
+ }, threshold, strict);
312
+ }
313
+ /** The guided-or-first continuation of a node, as answer-shaped bytes source:
314
+ * chooseNext under the response guide, falling back to the FIRST-inserted
315
+ * edge — the one no-guide convention chooseNext, project() and the search's
316
+ * formRules all share. undefined when the node has no continuation. */
317
+ export function guidedFirst(ctx, id) {
318
+ const pick = guidedNext(ctx, id);
319
+ if (pick !== undefined)
320
+ return pick;
321
+ // No guide in flight (or nothing chosen): the first-inserted edge, read
322
+ // with LIMIT 1 — never the full fan-out.
323
+ const nx = ctx.store.nextFirst(id, 1);
324
+ return nx.length > 0 ? nx[0] : undefined;
325
+ }
326
+ export function guidedNext(ctx, node) {
327
+ if (ctx._edgeGuide === null)
328
+ return undefined;
329
+ // The pick memo is BYPASSED while a rationale trace is attached — the same
330
+ // policy climbMemo and recogniseMemo follow (every mechanism must emit its
331
+ // own steps; a memo hit would swallow the repeat's `disambiguate` step).
332
+ // Consistency does not need the memo: chooseNext is a pure function of the
333
+ // (read-only) store and the guide, so recomputation yields the same pick.
334
+ if (!ctx.trace) {
335
+ const memo = ctx._edgeChoice.get(node);
336
+ if (memo !== undefined)
337
+ return memo === -1 ? undefined : memo;
338
+ }
339
+ const pick = chooseNext(ctx, node, ctx._edgeGuide);
340
+ if (!ctx.trace)
341
+ ctx._edgeChoice.set(node, pick ?? -1);
342
+ return pick;
343
+ }
344
+ /** Disambiguate among a node's learnt continuations by distributional
345
+ * support. NOTE the `guide` contract: its VALUE is deliberately unused —
346
+ * only its PRESENCE gates disambiguation (a null guide means no query is in
347
+ * flight, so structural walkers keep plain first-edge behaviour). The
348
+ * gist-cosine of short answer candidates against a query guide is dominated
349
+ * by accidental byte-pattern correlations, not semantic relatedness, so the
350
+ * evidence consulted is structural: each candidate's reverse-edge support
351
+ * count (see below). Contrast {@link chooseAmong}, the REVERSE-direction
352
+ * disambiguator, whose candidates are whole learnt contexts — long enough
353
+ * that their perceived gists ARE semantically meaningful — and which
354
+ * therefore scores by guide cosine. The two directions consult different
355
+ * halves of the evidence on purpose. */
356
+ export function chooseNext(ctx, id, guide) {
357
+ // CAPPED read: only the first √N continuations are ever candidates (the
358
+ // documented hub trade), so only they are read — a hub context's full
359
+ // fan-out is corpus-sized and must never be materialised. hubBound ≥ 2,
360
+ // so the single-continuation fast path below stays exact.
361
+ const nx = ctx.store.nextFirst(id, hubBound(ctx));
362
+ if (nx.length === 0)
363
+ return undefined;
364
+ if (nx.length === 1 || !guide)
365
+ return nx[0];
366
+ // Cap candidates at √N — the same bound the original chooseAmong used.
367
+ // A hub context can accumulate thousands of continuations; the best-fit
368
+ // one is among the first √N by insertion order (edges are never deleted,
369
+ // so the oldest are the most established). A strongly-supported edge
370
+ // inserted beyond the cap is invisible here — the deliberate trade
371
+ // against paying O(fan-out) count reads on every disambiguation.
372
+ const capped = nx; // already the hub-capped prefix, by the read above
373
+ // Distributional-evidence disambiguation, consulting BOTH read-outs of the
374
+ // evidence the training poured:
375
+ // 1. prevCount — how many DISTINCT contexts predict this candidate (one
376
+ // indexed COUNT; never a materialisation — a common continuation's
377
+ // reverse fan-in is corpus-sized). Diversity of independent evidence
378
+ // is the primary signal: three different formulations agreeing beat
379
+ // one formulation repeated.
380
+ // 2. haloMass — how many episode signatures were poured into the
381
+ // candidate's halo (repetition counts). The tie-break among equally
382
+ // diverse candidates: a fact reinforced across many episodes is more
383
+ // corroborated than one seen once, and this is the DIRECT measure of
384
+ // that — consulting only the structural count would leave poured
385
+ // evidence on the table.
386
+ // When both are equal, first-inserted wins (backward compatible).
387
+ let best = capped[0];
388
+ let bestSupport = ctx.store.prevCount(best);
389
+ let bestMass = ctx.store.haloMass(best);
390
+ for (let i = 1; i < capped.length; i++) {
391
+ const support = ctx.store.prevCount(capped[i]);
392
+ if (support < bestSupport)
393
+ continue;
394
+ const mass = ctx.store.haloMass(capped[i]);
395
+ if (support > bestSupport || mass > bestMass) {
396
+ best = capped[i];
397
+ bestSupport = support;
398
+ bestMass = mass;
399
+ }
400
+ }
401
+ // Trace is built lazily — the filter + map below only execute when a
402
+ // trace listener is attached, so the common (no-trace) path pays only
403
+ // for the prevCount calls in the loop above, never for extra rItemShort
404
+ // byte-reads.
405
+ if (ctx.trace) {
406
+ const others = capped.filter((c) => c !== best);
407
+ ctx.trace.step("disambiguate", [rItemShort(ctx, best, "halo-evidence", bestSupport)], others.map((c) => rItemShort(ctx, c, "candidate", ctx.store.prevCount(c))), `${capped.length} continuations — distributional evidence selects ` +
408
+ `the most corroborated (distinct contexts ${bestSupport}, ` +
409
+ `poured mass ${bestMass})`);
410
+ }
411
+ return best;
412
+ }
413
+ /** The perceived gist of a candidate node, through the session gist cache.
414
+ * Re-gisting a candidate is a full river fold of its bytes — the measured
415
+ * recall bottleneck (a hub context offers up to √N continuations, EACH
416
+ * re-perceived per pick). A node's bytes are immutable and perception is
417
+ * pure, so the cached gist is valid for the store's lifetime. Exported for
418
+ * every "score node ids against a guide" decision (chooseAmong here, the
419
+ * bridge's junction pick) so they share ONE cache and one convention. */
420
+ export function candidateGist(ctx, c) {
421
+ const hit = ctx._gistCache.get(c);
422
+ if (hit !== undefined)
423
+ return hit;
424
+ const b = read(ctx, c);
425
+ if (b.length === 0)
426
+ return null;
427
+ const g = gistOf(ctx, b);
428
+ ctx._gistCache.set(c, g);
429
+ return g;
430
+ }
431
+ export function chooseAmong(ctx, candidates, guide) {
432
+ const capped = hubCap(ctx, candidates);
433
+ const found = argmaxCosine(guide, capped, (c) => candidateGist(ctx, c), -Infinity, true);
434
+ return found
435
+ ? { id: found.item, score: found.score }
436
+ : { id: candidates[0], score: -Infinity };
437
+ }
438
+ // ── Trace shim (used by chooseNext before trace module is loaded) ────────
439
+ import { decodeText } from "./rationale.js";
440
+ function rItemShort(ctx, id, role, score) {
441
+ return {
442
+ text: decodeText(read(ctx, id)),
443
+ node: id,
444
+ role,
445
+ score,
446
+ };
447
+ }