@hviana/sema 0.2.2 → 0.2.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.
- package/dist/src/mind/articulation.js +1 -1
- package/dist/src/mind/attention.d.ts +4 -0
- package/dist/src/mind/attention.js +36 -12
- package/dist/src/mind/graph-search.d.ts +16 -1
- package/dist/src/mind/graph-search.js +34 -5
- package/dist/src/mind/mechanisms/alu.js +8 -1
- package/dist/src/mind/mechanisms/cast.js +35 -11
- package/dist/src/mind/mechanisms/cover.js +8 -1
- package/dist/src/mind/mechanisms/extraction.js +75 -30
- package/dist/src/mind/mind.d.ts +1 -0
- package/dist/src/mind/mind.js +6 -1
- package/dist/src/mind/pipeline.js +34 -2
- package/dist/src/mind/reasoning.d.ts +9 -1
- package/dist/src/mind/reasoning.js +26 -4
- package/dist/src/mind/recognition.js +30 -6
- package/dist/src/mind/traverse.js +15 -0
- package/dist/src/mind/types.d.ts +26 -0
- package/dist/src/mind/types.js +15 -0
- package/package.json +1 -1
- package/src/mind/articulation.ts +1 -0
- package/src/mind/attention.ts +42 -12
- package/src/mind/graph-search.ts +59 -2
- package/src/mind/mechanisms/alu.ts +8 -1
- package/src/mind/mechanisms/cast.ts +46 -8
- package/src/mind/mechanisms/cover.ts +8 -0
- package/src/mind/mechanisms/extraction.ts +101 -40
- package/src/mind/mind.ts +7 -1
- package/src/mind/pipeline.ts +37 -2
- package/src/mind/reasoning.ts +26 -3
- package/src/mind/recognition.ts +28 -5
- package/src/mind/traverse.ts +18 -0
- package/src/mind/types.ts +40 -0
- package/test/35-attention-confidence.test.mjs +139 -0
|
@@ -111,7 +111,15 @@ export async function reason(ctx, query, answer, preConsumed, pre) {
|
|
|
111
111
|
* When the consensus climb finds more than one dominant point, each
|
|
112
112
|
* independent point grounds its own answer; they are bridged together
|
|
113
113
|
* by any learnt connector the graph holds between them. */
|
|
114
|
-
export async function fuseAttention(ctx, query, primary, pre
|
|
114
|
+
export async function fuseAttention(ctx, query, primary, pre,
|
|
115
|
+
/** True when `primary` never touched the consensus climb at all — e.g. a
|
|
116
|
+
* pure ALU computation, which has no anchor of its own. commitVotes
|
|
117
|
+
* ALWAYS admits the dominant root regardless of its vote (attention.ts:
|
|
118
|
+
* "roots.length === 0 || …") on the assumption a lone root already IS
|
|
119
|
+
* primary's own source; that assumption is exactly backwards when
|
|
120
|
+
* primary is unclimbed. Absent or false preserves the original
|
|
121
|
+
* behaviour exactly. */
|
|
122
|
+
unclimbed = false) {
|
|
115
123
|
// When the answer is structurally drawn from the query itself
|
|
116
124
|
// (extraction), it already spans all the query's pieces — fusion
|
|
117
125
|
// would only add noise from unrelated stored contexts. The gate is
|
|
@@ -125,17 +133,31 @@ export async function fuseAttention(ctx, query, primary, pre) {
|
|
|
125
133
|
// query, same k, same DF mode) — read them from Precomputed instead of
|
|
126
134
|
// re-climbing, so even a traced response pays for the climb once.
|
|
127
135
|
const forest = (await pre.attention()).roots;
|
|
128
|
-
|
|
136
|
+
// A LONE root is ordinarily primary's own source — nothing to fuse. But
|
|
137
|
+
// when primary is unclimbed, the lone root was never checked against
|
|
138
|
+
// anything: it is admitted by commitVotes unconditionally, so it may be
|
|
139
|
+
// genuine consensus (Attention.breadth dominates — most of the query's
|
|
140
|
+
// OWN regions corroborate it) or a coincidental echo (breadth does not
|
|
141
|
+
// dominate — see test/35-attention-confidence). breadth is the SCALE-
|
|
142
|
+
// INVARIANT read of exactly this question: the raw IDF vote cannot serve
|
|
143
|
+
// here, since it is an absolute ln(N)-scaled quantity (a genuine root on
|
|
144
|
+
// a large store can score BELOW its own floor while a coincidental echo
|
|
145
|
+
// on a small one scores comfortably above its own, smaller, floor).
|
|
146
|
+
const lonePromotes = unclimbed && forest.length === 1 &&
|
|
147
|
+
forest[0].breadth > 0.5;
|
|
148
|
+
if (forest.length === 0 || (forest.length <= 1 && !lonePromotes)) {
|
|
129
149
|
return primary;
|
|
150
|
+
}
|
|
130
151
|
const pieces = [
|
|
131
152
|
{ start: forest[0].start, bytes: primary },
|
|
132
153
|
];
|
|
133
154
|
const qv = pre.guide; // once, not per root
|
|
155
|
+
const rest = lonePromotes ? forest : forest.slice(1);
|
|
134
156
|
const t = ctx.trace?.enter("fuseAttention", [
|
|
135
157
|
rItem(primary, "primary"),
|
|
136
|
-
...
|
|
158
|
+
...rest.map((r) => rNode(ctx, r.anchor, "point", r.vote)),
|
|
137
159
|
]);
|
|
138
|
-
for (const root of
|
|
160
|
+
for (const root of rest) {
|
|
139
161
|
const g = await project(ctx, root.anchor, qv);
|
|
140
162
|
if (g === null || g.length === 0)
|
|
141
163
|
continue;
|
|
@@ -41,8 +41,9 @@ function recogniseImpl(ctx, bytes) {
|
|
|
41
41
|
const sites = [];
|
|
42
42
|
const leaves = [];
|
|
43
43
|
const splits = new Set();
|
|
44
|
+
const starts = new Set();
|
|
44
45
|
if (bytes.length === 0)
|
|
45
|
-
return { sites, leaves, splits };
|
|
46
|
+
return { sites, leaves, splits, starts };
|
|
46
47
|
// Span-resolve memo for THIS call: the structural pass (sub-runs inside
|
|
47
48
|
// leaf-parents) and the canonical pass (leaf-id chains) probe overlapping
|
|
48
49
|
// spans, and each resolve() is a full fold of the sub-span (fresh subarray
|
|
@@ -78,7 +79,6 @@ function recogniseImpl(ctx, bytes) {
|
|
|
78
79
|
}
|
|
79
80
|
};
|
|
80
81
|
// ── structural: the query's own perceived tree ──────────────────────
|
|
81
|
-
const starts = new Set();
|
|
82
82
|
starts.add(0);
|
|
83
83
|
foldTree(ctx, perceive(ctx, bytes), 0, (n, start, end, node) => {
|
|
84
84
|
if (n.kids === null) {
|
|
@@ -105,7 +105,16 @@ function recogniseImpl(ctx, bytes) {
|
|
|
105
105
|
leafOffsets.push(off);
|
|
106
106
|
off += k.leaf?.length ?? 0;
|
|
107
107
|
}
|
|
108
|
+
// Sub-spans starting at i > 0 begin INSIDE the chunk, at an offset the
|
|
109
|
+
// query's own fold did not itself choose as a boundary — the same
|
|
110
|
+
// opportunistic byte-atom-chain risk `tryChain`'s `boundary` gate
|
|
111
|
+
// guards below (see its comment). Only the chunk's own left edge
|
|
112
|
+
// (i === 0, already registered in `starts` above) carries the fold's
|
|
113
|
+
// evidence; interior sub-starts are exempt from the guard only while
|
|
114
|
+
// atoms themselves still discriminate at this corpus scale.
|
|
108
115
|
for (let i = 0; i < n.kids.length; i++) {
|
|
116
|
+
if (i > 0 && atomsAreHubs)
|
|
117
|
+
break;
|
|
109
118
|
const subIds = [];
|
|
110
119
|
for (let j = i; j < n.kids.length; j++) {
|
|
111
120
|
const kj = n.kids[j];
|
|
@@ -148,7 +157,20 @@ function recogniseImpl(ctx, bytes) {
|
|
|
148
157
|
chunkEnd[p] = chunkLimit;
|
|
149
158
|
}
|
|
150
159
|
}
|
|
151
|
-
|
|
160
|
+
// A chain rebuilt from a NON-boundary offset (the query's own perceived
|
|
161
|
+
// cut, `starts`, never chose to segment here) is opportunistic: the same
|
|
162
|
+
// byte-atom coincidence the hub guard above already exists for, just
|
|
163
|
+
// spelled over 2+ leaves instead of 1. At small corpus scale that's fine
|
|
164
|
+
// — coincidence is rare and every chain is real evidence (see `atomIsHub`).
|
|
165
|
+
// Past the scale where atoms themselves stop discriminating, the same
|
|
166
|
+
// uniform-expectation argument bounds a CHAIN'S commonality too: it is at
|
|
167
|
+
// least as rare as its rarest atom, so a store where atoms are hubs makes
|
|
168
|
+
// interior chain reconstructions no more trustworthy than the atoms they
|
|
169
|
+
// are built from ("hi" resolving out of "W[hi]ch" is exactly this: two
|
|
170
|
+
// hub-scale atoms, chained at an offset nothing in the query's own fold
|
|
171
|
+
// selected). Chains that start ON a boundary carry the fold's own
|
|
172
|
+
// evidence instead and are exempt.
|
|
173
|
+
const tryChain = (p, maxIds, boundary) => {
|
|
152
174
|
const first = leafFrom(p);
|
|
153
175
|
if (!first)
|
|
154
176
|
return;
|
|
@@ -164,6 +186,8 @@ function recogniseImpl(ctx, bytes) {
|
|
|
164
186
|
pos = nx.end;
|
|
165
187
|
if (store.findBranch(ids) === null)
|
|
166
188
|
continue;
|
|
189
|
+
if (!boundary && atomsAreHubs)
|
|
190
|
+
continue;
|
|
167
191
|
const id = resolveSpan(p, pos);
|
|
168
192
|
if (id === null || id === prevId)
|
|
169
193
|
continue;
|
|
@@ -173,11 +197,11 @@ function recogniseImpl(ctx, bytes) {
|
|
|
173
197
|
};
|
|
174
198
|
for (let p = 0; p < bytes.length; p++) {
|
|
175
199
|
if (starts.has(p)) {
|
|
176
|
-
tryChain(p, chainReach(W)); // boundary start — full reach
|
|
200
|
+
tryChain(p, chainReach(W), true); // boundary start — full reach
|
|
177
201
|
}
|
|
178
202
|
else {
|
|
179
203
|
const limit = chunkEnd[p] + W;
|
|
180
|
-
tryChain(p, Math.min(limit - p, chainReach(W)));
|
|
204
|
+
tryChain(p, Math.min(limit - p, chainReach(W)), false);
|
|
181
205
|
}
|
|
182
206
|
}
|
|
183
207
|
// ── splits: a form boundary that does not fall on a leaf edge ────────
|
|
@@ -195,7 +219,7 @@ function recogniseImpl(ctx, bytes) {
|
|
|
195
219
|
s.end,
|
|
196
220
|
])), `decompose the query into ${sites.length} learnt form(s) that lead somewhere` +
|
|
197
221
|
` (over ${leaves.length} perceived leaves)`);
|
|
198
|
-
return { sites, leaves, splits };
|
|
222
|
+
return { sites, leaves, splits, starts };
|
|
199
223
|
}
|
|
200
224
|
/** Segment bytes using the geometry's own groupings — leaf-parent
|
|
201
225
|
* nodes from the perceived tree, with consecutive bare leaves merged
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// The PROJECTIONS built on these walks (follow, conceptHop, reverseContext,
|
|
7
7
|
// project) live in match.ts — the elementary match-and-project operation.
|
|
8
8
|
import { cosine } from "../vec.js";
|
|
9
|
+
import { consensusFloor } from "../geometry.js";
|
|
9
10
|
import { gistOf, read } from "./primitives.js";
|
|
10
11
|
const structCaches = new WeakMap();
|
|
11
12
|
function getStructCache(ctx) {
|
|
@@ -439,6 +440,20 @@ export function chooseNext(ctx, id, guide) {
|
|
|
439
440
|
bestMass = mass;
|
|
440
441
|
}
|
|
441
442
|
}
|
|
443
|
+
// A pick among GENUINELY competing continuations still needs to clear the
|
|
444
|
+
// same genuine-corroboration floor {@link consensusFloor} draws for every
|
|
445
|
+
// other consumer that turns distinct-context support into a vote (the
|
|
446
|
+
// climb's recallByResonance/commitVotes) — below it, "most corroborated"
|
|
447
|
+
// is only the least-thin echo among several, not real evidence. Gated to
|
|
448
|
+
// corpus scale large enough that this echo is even possible (the same
|
|
449
|
+
// scale {@link atomIsHub} already switches on for the identical reason:
|
|
450
|
+
// at small N every edge IS the evidence there is). A hub id has no
|
|
451
|
+
// meaningful "distinct context" reading at all (id < 0 atoms are excluded
|
|
452
|
+
// from this path already, since chooseNext only ever sees real edges).
|
|
453
|
+
const N = corpusN(ctx);
|
|
454
|
+
if (capped.length > 1 && atomIsHub(ctx, N) && bestSupport < consensusFloor(N)) {
|
|
455
|
+
return undefined;
|
|
456
|
+
}
|
|
442
457
|
// Trace is built lazily — the filter + map below only execute when a
|
|
443
458
|
// trace listener is attached, so the common (no-trace) path pays only
|
|
444
459
|
// for the prevCount calls in the loop above, never for extra rItemShort
|
package/dist/src/mind/types.d.ts
CHANGED
|
@@ -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,16 @@ 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;
|
|
59
77
|
}
|
|
60
78
|
/** Both read-outs of one consensus climb. */
|
|
61
79
|
export interface AttentionRead {
|
|
@@ -89,6 +107,14 @@ export interface RegionVote {
|
|
|
89
107
|
roots: readonly number[];
|
|
90
108
|
w: number;
|
|
91
109
|
wFocus: number;
|
|
110
|
+
/** How many of the query's ORIGINAL regions this one vote's evidence
|
|
111
|
+
* accounts for. 1 for an ordinary per-region vote (itself); for a
|
|
112
|
+
* cross-region junction vote, 1 (itself) plus however many individual
|
|
113
|
+
* votes it explained away (see crossRegionVotes) — the junction speaks
|
|
114
|
+
* for all of them at once, and breadth accounting must not undercount it
|
|
115
|
+
* to "one region" just because it collapsed to one pooled axiom.
|
|
116
|
+
* Defaults to 1 when absent. */
|
|
117
|
+
absorbed?: number;
|
|
92
118
|
}
|
|
93
119
|
/** The edge-bearing contexts reached by climbing from a node, plus saturation info. */
|
|
94
120
|
export interface AncestorReach {
|
package/dist/src/mind/types.js
CHANGED
|
@@ -26,6 +26,21 @@ export function liftAnswer(segs, queryLen) {
|
|
|
26
26
|
return null;
|
|
27
27
|
if (recognised.length === 1) {
|
|
28
28
|
const s = segs[recognised[0]];
|
|
29
|
+
// A COMPUTED span's query-side width is operand digit-count, not
|
|
30
|
+
// evidence of how much of the query's meaning it accounts for — the
|
|
31
|
+
// half-dominance check below (built for a genuinely RECOGNISED learned
|
|
32
|
+
// form) is not a valid framing signal for it (see the `computed` field
|
|
33
|
+
// doc on Seg/GItem): "1000 - 421" outweighs "what is …?" by width only
|
|
34
|
+
// because the operands are big, not because the framing matters less.
|
|
35
|
+
// A LITERAL PREFIX before a computed span is unambiguous framing
|
|
36
|
+
// regardless of width — an arithmetic expression is never itself
|
|
37
|
+
// preceded by more literal computed content, so anything literal before
|
|
38
|
+
// it is question wording ("what is ", "compute ") to lift clear of.
|
|
39
|
+
// With no prefix (s.i === 0) the span is judged by the ordinary
|
|
40
|
+
// half-dominance rule below, which already correctly keeps a short
|
|
41
|
+
// trailing glue byte ("2+2." → "4.", the span dominates a 4-byte query).
|
|
42
|
+
if (s.computed && s.i > 0)
|
|
43
|
+
return s.bytes;
|
|
29
44
|
if (dominates(s.j - s.i, queryLen)) {
|
|
30
45
|
return concatBytes(segs.map((x) => x.bytes));
|
|
31
46
|
}
|
package/package.json
CHANGED
package/src/mind/articulation.ts
CHANGED
package/src/mind/attention.ts
CHANGED
|
@@ -417,6 +417,9 @@ export function poolVotes(
|
|
|
417
417
|
votes: Map<number, number>;
|
|
418
418
|
votesIdf: Map<number, number>;
|
|
419
419
|
support: Map<number, { start: number; end: number; w: number }>;
|
|
420
|
+
/** Per-anchor SCALE-INVARIANT support: Σ RegionVote.absorbed over the
|
|
421
|
+
* distinct contributing regions — see Attention.breadth. */
|
|
422
|
+
regionSupport: Map<number, number>;
|
|
420
423
|
steps: DerivationStep[];
|
|
421
424
|
} {
|
|
422
425
|
const eligible: number[] = [];
|
|
@@ -496,6 +499,7 @@ export function poolVotes(
|
|
|
496
499
|
number,
|
|
497
500
|
{ start: number; end: number; w: number }
|
|
498
501
|
>();
|
|
502
|
+
const regionSupport = new Map<number, number>();
|
|
499
503
|
const steps: DerivationStep[] = [];
|
|
500
504
|
let order = 0;
|
|
501
505
|
for (const pc of pool.values()) {
|
|
@@ -503,13 +507,16 @@ export function poolVotes(
|
|
|
503
507
|
votes.set(pc.item.id, pc.cost);
|
|
504
508
|
const premises: DerivationItem[] = [];
|
|
505
509
|
const seenRi = new Set<number>();
|
|
510
|
+
let breadthSum = 0;
|
|
506
511
|
for (const c of pc.contributions) {
|
|
507
512
|
const p0 = c.premises[0].item;
|
|
508
513
|
if (p0.kind !== "region" || seenRi.has(p0.ri)) continue;
|
|
509
514
|
seenRi.add(p0.ri);
|
|
510
515
|
const rv = regionVotes[p0.ri];
|
|
516
|
+
breadthSum += rv.absorbed ?? 1;
|
|
511
517
|
premises.push({ kind: "form", span: [rv.start, rv.end] });
|
|
512
518
|
}
|
|
519
|
+
regionSupport.set(pc.item.id, breadthSum);
|
|
513
520
|
steps.push({
|
|
514
521
|
order: order++,
|
|
515
522
|
move: "pool-vote",
|
|
@@ -536,7 +543,7 @@ export function poolVotes(
|
|
|
536
543
|
}
|
|
537
544
|
}
|
|
538
545
|
}
|
|
539
|
-
return { votes, votesIdf, support, steps };
|
|
546
|
+
return { votes, votesIdf, support, regionSupport, steps };
|
|
540
547
|
}
|
|
541
548
|
|
|
542
549
|
export function commitVotes(
|
|
@@ -545,6 +552,7 @@ export function commitVotes(
|
|
|
545
552
|
votes: Map<number, number>;
|
|
546
553
|
votesIdf: Map<number, number>;
|
|
547
554
|
support: Map<number, { start: number; end: number; w: number }>;
|
|
555
|
+
regionSupport: Map<number, number>;
|
|
548
556
|
steps: DerivationStep[];
|
|
549
557
|
},
|
|
550
558
|
sat: SaturationInfo,
|
|
@@ -552,16 +560,27 @@ export function commitVotes(
|
|
|
552
560
|
regionVoter: ReadonlyArray<{ id: number; score: number; w: number } | null>,
|
|
553
561
|
N: number,
|
|
554
562
|
): AttentionRead {
|
|
555
|
-
const { votes, votesIdf, support, steps } = pooled;
|
|
563
|
+
const { votes, votesIdf, support, regionSupport, steps } = pooled;
|
|
556
564
|
if (votes.size === 0) {
|
|
557
565
|
traceAttention(ctx, regions, regionVoter, [], steps);
|
|
558
566
|
return { roots: [], ranked: [] };
|
|
559
567
|
}
|
|
560
568
|
|
|
569
|
+
// SCALE-INVARIANT confidence — see Attention.breadth's doc. regions.length
|
|
570
|
+
// is the query's OWN full candidate count (most never vote at all), the
|
|
571
|
+
// same denominator the "N of M sub-regions voted" rationale text already
|
|
572
|
+
// reports; regionSupport is that same accounting read PER ANCHOR.
|
|
573
|
+
const totalRegions = Math.max(1, regions.length);
|
|
561
574
|
const ranked = [...votes.entries()]
|
|
562
575
|
.map(([anchor, vote]) => {
|
|
563
576
|
const s = support.get(anchor)!;
|
|
564
|
-
return {
|
|
577
|
+
return {
|
|
578
|
+
anchor,
|
|
579
|
+
vote,
|
|
580
|
+
start: s.start,
|
|
581
|
+
end: s.end,
|
|
582
|
+
breadth: (regionSupport.get(anchor) ?? 0) / totalRegions,
|
|
583
|
+
};
|
|
565
584
|
})
|
|
566
585
|
.sort((a, b) => b.vote - a.vote);
|
|
567
586
|
|
|
@@ -1016,14 +1035,6 @@ async function crossRegionVotes(
|
|
|
1016
1035
|
spanStart = Math.min(spanStart, regions[ei].start);
|
|
1017
1036
|
spanEnd = Math.max(spanEnd, regions[ei].end);
|
|
1018
1037
|
}
|
|
1019
|
-
out.push({
|
|
1020
|
-
start: spanStart,
|
|
1021
|
-
end: spanEnd,
|
|
1022
|
-
canonicalFailed: false, // content-addressed: never saturation-masked
|
|
1023
|
-
roots: reach.roots,
|
|
1024
|
-
w,
|
|
1025
|
-
wFocus: w,
|
|
1026
|
-
});
|
|
1027
1038
|
consumed.add(cand[a]);
|
|
1028
1039
|
consumed.add(cand[b]);
|
|
1029
1040
|
for (const ei of bestExtras) consumed.add(ei);
|
|
@@ -1033,14 +1044,33 @@ async function crossRegionVotes(
|
|
|
1033
1044
|
// vote's bytes are literally part of the learnt whole), and FULL root
|
|
1034
1045
|
// disjointness is the disagreement test: a vote sharing even one root
|
|
1035
1046
|
// with the junction corroborates it and keeps its say elsewhere.
|
|
1047
|
+
// Counted BEFORE pushing the junction's own vote below: each ORIGINAL
|
|
1048
|
+
// region this ascent explains away is evidence the junction speaks
|
|
1049
|
+
// for, not evidence lost — `absorbed` (RegionVote's breadth-accounting
|
|
1050
|
+
// field) must credit the junction with all of it, not just the ONE
|
|
1051
|
+
// pooled axiom it collapses to.
|
|
1036
1052
|
const containerBytes = cachedRead(ctx, cache, best.id, cap);
|
|
1037
1053
|
const jointRoots = new Set(reach.roots);
|
|
1054
|
+
let explainedAway = 0;
|
|
1038
1055
|
for (const rv of rvs.votes) {
|
|
1039
1056
|
if (rv.roots.some((r) => jointRoots.has(r))) continue;
|
|
1040
1057
|
const bytes = query.subarray(rv.start, rv.end);
|
|
1041
|
-
if (indexOf(containerBytes, bytes, 0) >= 0
|
|
1058
|
+
if (indexOf(containerBytes, bytes, 0) >= 0 && !superseded.has(rv)) {
|
|
1059
|
+
superseded.add(rv);
|
|
1060
|
+
explainedAway++;
|
|
1061
|
+
}
|
|
1042
1062
|
}
|
|
1043
1063
|
|
|
1064
|
+
out.push({
|
|
1065
|
+
start: spanStart,
|
|
1066
|
+
end: spanEnd,
|
|
1067
|
+
canonicalFailed: false, // content-addressed: never saturation-masked
|
|
1068
|
+
roots: reach.roots,
|
|
1069
|
+
w,
|
|
1070
|
+
wFocus: w,
|
|
1071
|
+
absorbed: 1 + explainedAway,
|
|
1072
|
+
});
|
|
1073
|
+
|
|
1044
1074
|
const label = [cand[a], cand[b], ...bestExtras]
|
|
1045
1075
|
.sort((x, y) => regions[x].start - regions[y].start)
|
|
1046
1076
|
.map((ri) => dec(query.subarray(regions[ri].start, regions[ri].end)))
|
package/src/mind/graph-search.ts
CHANGED
|
@@ -96,6 +96,18 @@ export type GItem =
|
|
|
96
96
|
cover: boolean;
|
|
97
97
|
rec: boolean;
|
|
98
98
|
node?: number;
|
|
99
|
+
/** Set only for a {@link ComputedResult} axiom (an extension's derived
|
|
100
|
+
* value, e.g. ALU arithmetic) — as opposed to a genuinely RECOGNISED
|
|
101
|
+
* learned form. Both set `rec: true` (a computation bridges the cover
|
|
102
|
+
* exactly like a learned terminal answer), but they mean different
|
|
103
|
+
* things for `i..j`'s WIDTH: a recognised form's query-span width is
|
|
104
|
+
* real evidence of how much of the query's meaning it accounts for
|
|
105
|
+
* (liftAnswer's half-dominance framing decision); a computed span's
|
|
106
|
+
* width is operand digit-count, uncorrelated with meaning ("1000 - 421"
|
|
107
|
+
* is wider than "15 * 7" only because the numbers are bigger, not
|
|
108
|
+
* because subtraction is more "the point" of its query). See
|
|
109
|
+
* {@link liftAnswer}. */
|
|
110
|
+
computed?: boolean;
|
|
99
111
|
};
|
|
100
112
|
type OutItem = Extract<GItem, { kind: "out" }>;
|
|
101
113
|
|
|
@@ -138,6 +150,9 @@ export interface Seg {
|
|
|
138
150
|
bytes: Uint8Array;
|
|
139
151
|
rec: boolean;
|
|
140
152
|
node?: number;
|
|
153
|
+
/** See the `computed` field of the "out" {@link GItem} — set only for an
|
|
154
|
+
* extension's derived value, never a genuinely recognised learned form. */
|
|
155
|
+
computed?: boolean;
|
|
141
156
|
}
|
|
142
157
|
|
|
143
158
|
/** Read the chosen spans back off a derivation: the goal is a chain of bridge
|
|
@@ -155,6 +170,7 @@ function readCover(derivation: Derivation<GItem>): Seg[] {
|
|
|
155
170
|
bytes: out.bytes,
|
|
156
171
|
rec: out.rec,
|
|
157
172
|
node: out.node,
|
|
173
|
+
computed: out.computed,
|
|
158
174
|
});
|
|
159
175
|
}
|
|
160
176
|
node = node.premises[0];
|
|
@@ -356,6 +372,7 @@ export class GraphSearch {
|
|
|
356
372
|
conceptTarget: ReadonlyMap<number, number>,
|
|
357
373
|
leaves: ReadonlyArray<Leaf>,
|
|
358
374
|
splits: ReadonlySet<number>,
|
|
375
|
+
starts: ReadonlySet<number>,
|
|
359
376
|
substitutions?: ReadonlyMap<number, Uint8Array>,
|
|
360
377
|
connectors?: ReadonlyMap<string, Uint8Array>,
|
|
361
378
|
computedResults?: ReadonlyArray<ComputedResult>,
|
|
@@ -377,6 +394,7 @@ export class GraphSearch {
|
|
|
377
394
|
sites,
|
|
378
395
|
leaves,
|
|
379
396
|
splits,
|
|
397
|
+
starts,
|
|
380
398
|
},
|
|
381
399
|
conceptTarget,
|
|
382
400
|
substitutions,
|
|
@@ -405,6 +423,7 @@ export class GraphSearch {
|
|
|
405
423
|
sites: ReadonlyArray<Site>;
|
|
406
424
|
leaves: ReadonlyArray<Leaf>;
|
|
407
425
|
splits: ReadonlySet<number>;
|
|
426
|
+
starts: ReadonlySet<number>;
|
|
408
427
|
},
|
|
409
428
|
conceptTarget: ReadonlyMap<number, number>,
|
|
410
429
|
substitutions?: ReadonlyMap<number, Uint8Array>,
|
|
@@ -418,6 +437,7 @@ export class GraphSearch {
|
|
|
418
437
|
conceptTarget,
|
|
419
438
|
recognition.leaves,
|
|
420
439
|
recognition.splits,
|
|
440
|
+
recognition.starts,
|
|
421
441
|
substitutions,
|
|
422
442
|
connectors,
|
|
423
443
|
computedResults,
|
|
@@ -448,11 +468,22 @@ export class GraphSearch {
|
|
|
448
468
|
conceptTarget: ReadonlyMap<number, number>,
|
|
449
469
|
leaves: ReadonlyArray<Leaf>,
|
|
450
470
|
splits: ReadonlySet<number>,
|
|
471
|
+
starts: ReadonlySet<number>,
|
|
451
472
|
substitutions?: ReadonlyMap<number, Uint8Array>,
|
|
452
473
|
connectors?: ReadonlyMap<string, Uint8Array>,
|
|
453
474
|
computedResults?: ReadonlyArray<ComputedResult>,
|
|
454
475
|
): DeductionSystem<GItem> {
|
|
455
476
|
const W = this.maxGroup; // fusible span ceiling (shortest composite bound)
|
|
477
|
+
// Same corpus-scale hub floor {@link atomIsHub}/{@link atomReach} (traverse.ts)
|
|
478
|
+
// derive for byte atoms — duplicated here rather than imported because this
|
|
479
|
+
// module is deliberately host-based (no MindContext), and both inputs
|
|
480
|
+
// (maxGroup, edgeSourceCount) are already in scope with no ctx needed. If
|
|
481
|
+
// the formula ever changes, it must change in BOTH places (see canonical.ts's
|
|
482
|
+
// header for the same write/read-side duplication convention).
|
|
483
|
+
const atomsAreHubs = Math.max(
|
|
484
|
+
1,
|
|
485
|
+
Math.ceil((this.store.edgeSourceCount() * W) / 256),
|
|
486
|
+
) > Math.ceil(Math.sqrt(Math.max(2, this.store.edgeSourceCount())));
|
|
456
487
|
const nodeBytes = (n: number) => this.store.bytesPrefix(n, ALL);
|
|
457
488
|
// Content-addressed probes over the store's hash-cons maps — the same keys
|
|
458
489
|
// training filled. No byte-by-byte trie walk.
|
|
@@ -550,6 +581,7 @@ export class GraphSearch {
|
|
|
550
581
|
cover: true,
|
|
551
582
|
rec: true,
|
|
552
583
|
node: u.node,
|
|
584
|
+
computed: true,
|
|
553
585
|
},
|
|
554
586
|
cost: STEP,
|
|
555
587
|
};
|
|
@@ -588,6 +620,8 @@ export class GraphSearch {
|
|
|
588
620
|
return this.outRules(it, {
|
|
589
621
|
W,
|
|
590
622
|
splits,
|
|
623
|
+
starts,
|
|
624
|
+
atomsAreHubs,
|
|
591
625
|
coversDone,
|
|
592
626
|
outsByStart,
|
|
593
627
|
outsByEnd,
|
|
@@ -949,6 +983,8 @@ export class GraphSearch {
|
|
|
949
983
|
ctx: {
|
|
950
984
|
W: number;
|
|
951
985
|
splits: ReadonlySet<number>;
|
|
986
|
+
starts: ReadonlySet<number>;
|
|
987
|
+
atomsAreHubs: boolean;
|
|
952
988
|
coversDone: Set<number>;
|
|
953
989
|
outsByStart: Map<number, OutItem[]>;
|
|
954
990
|
outsByEnd: Map<number, OutItem[]>;
|
|
@@ -1095,12 +1131,30 @@ export class GraphSearch {
|
|
|
1095
1131
|
r: OutItem,
|
|
1096
1132
|
ctx: {
|
|
1097
1133
|
W: number;
|
|
1134
|
+
starts: ReadonlySet<number>;
|
|
1135
|
+
atomsAreHubs: boolean;
|
|
1098
1136
|
findLeafU: (b: Uint8Array) => number | undefined;
|
|
1099
1137
|
findBranchU: (k: number[]) => number | undefined;
|
|
1100
1138
|
},
|
|
1101
1139
|
): Iterable<Rule<GItem>> {
|
|
1102
1140
|
const bytes = concat2(l.bytes, r.bytes);
|
|
1103
|
-
|
|
1141
|
+
// A PURE leaf-leaf fuse (neither side already a recognised completion)
|
|
1142
|
+
// is opportunistic cross-leaf recovery exactly like recognition.ts's own
|
|
1143
|
+
// canonical chain — findLeaf/findBranch here have no idea WHY these two
|
|
1144
|
+
// leaves are adjacent, only that their concatenation happens to spell a
|
|
1145
|
+
// trained form ("hi" recovered from "W[hi]ch"). The same corpus-scale
|
|
1146
|
+
// caution recognition.ts's `boundary` gate applies: trust it fully when
|
|
1147
|
+
// `l.i` is a position the query's OWN fold chose as a boundary (real
|
|
1148
|
+
// structural evidence); past the scale where atoms themselves stop
|
|
1149
|
+
// discriminating, an interior offset's opportunistic match is noise, not
|
|
1150
|
+
// a genuine cross-leaf recovery. A completion-involved fuse (l.rec ||
|
|
1151
|
+
// r.rec) is a different, legitimate case — a rewrite growing into its
|
|
1152
|
+
// neighbour — and is exempt, same as recognition.ts's rec-derived sites.
|
|
1153
|
+
const trusted = l.rec || r.rec || ctx.starts.has(l.i) ||
|
|
1154
|
+
!ctx.atomsAreHubs;
|
|
1155
|
+
let node = (trusted && bytes.length <= ctx.W)
|
|
1156
|
+
? ctx.findLeafU(bytes)
|
|
1157
|
+
: undefined;
|
|
1104
1158
|
// Whether this pair ACTUALLY forms a 2-child branch — the hard evidence
|
|
1105
1159
|
// that the fused bytes are a learned form worth keeping alive. Derived
|
|
1106
1160
|
// from the same findBranchU probe that sets `node`; when false, the pair
|
|
@@ -1108,7 +1162,10 @@ export class GraphSearch {
|
|
|
1108
1162
|
// node cannot contribute to any further fusion (findBranch needs two
|
|
1109
1163
|
// nodes, resolve needs a completion, and findLeaf already had its chance).
|
|
1110
1164
|
let pairFormsBranch = false;
|
|
1111
|
-
if (
|
|
1165
|
+
if (
|
|
1166
|
+
trusted && node === undefined && l.node !== undefined &&
|
|
1167
|
+
r.node !== undefined
|
|
1168
|
+
) {
|
|
1112
1169
|
node = ctx.findBranchU([l.node, r.node]);
|
|
1113
1170
|
pairFormsBranch = node !== undefined;
|
|
1114
1171
|
}
|
|
@@ -16,7 +16,14 @@ import type { PipelineMechanism } from "../pipeline-mechanism.js";
|
|
|
16
16
|
export function aluToMechanism(alu: Alu): PipelineMechanism {
|
|
17
17
|
return {
|
|
18
18
|
name: "alu",
|
|
19
|
-
|
|
19
|
+
// Not a cover derivation: cover.ts composes an answer by walking
|
|
20
|
+
// recognised query STRUCTURE; the ALU evaluates a recognised expression
|
|
21
|
+
// to its authoritative result and hands the bytes back untouched. It
|
|
22
|
+
// shares cover's near-zero floor (computation always wins, masked into
|
|
23
|
+
// cover's own search — see mechanisms/cover.ts), but the candidate this
|
|
24
|
+
// produces is not one of cover's derivations, so it carries its own
|
|
25
|
+
// honest label, the same way extract/cast/recall each carry theirs.
|
|
26
|
+
provenance: "alu",
|
|
20
27
|
parse: (query) => alu.parse(query),
|
|
21
28
|
async floor(_ctx, _query, pre, _worthRunning) {
|
|
22
29
|
return pre.computed.length > 0 ? 0 : null;
|
|
@@ -102,18 +102,43 @@ export async function counterfactualTransfer(
|
|
|
102
102
|
query: Uint8Array,
|
|
103
103
|
pre: Precomputed,
|
|
104
104
|
): Promise<CastResult[]> {
|
|
105
|
+
// Opened unconditionally, at entry — the same convention recall.ts's
|
|
106
|
+
// recallByResonance and extraction.ts's extractBySkill use, so every exit
|
|
107
|
+
// path (five gates below, then the schemas themselves) closes through
|
|
108
|
+
// ONE scope and inspectRationale never hits a silent dead end. Only the
|
|
109
|
+
// first two gates duplicate floor()'s own admissible bound (query length,
|
|
110
|
+
// ranked anchor count) — required to stay in sync per this function's own
|
|
111
|
+
// doc comment above, and effectively dead through the ordinary pipeline
|
|
112
|
+
// (floor() returning null already stops run() from being called at all),
|
|
113
|
+
// but this function is also exported and callable directly, so they stay
|
|
114
|
+
// and get the same honest trace as everything past them.
|
|
115
|
+
const t = ctx.trace?.enter("counterfactual", [rItem(query, "query")]);
|
|
116
|
+
const fail = (note: string): CastResult[] => {
|
|
117
|
+
t?.done([], note);
|
|
118
|
+
return [];
|
|
119
|
+
};
|
|
120
|
+
|
|
105
121
|
const quantum = ctx.space.maxGroup;
|
|
106
122
|
if (query.length < 2 * quantum || ctx.store.edgeSourceCount() === 0) {
|
|
107
|
-
return
|
|
123
|
+
return fail("query below the two-quantum floor, or no edges learnt yet");
|
|
108
124
|
}
|
|
109
125
|
const { roots, ranked } = await pre.attention();
|
|
110
|
-
if (ranked.length < 2)
|
|
126
|
+
if (ranked.length < 2) {
|
|
127
|
+
return fail(
|
|
128
|
+
`only ${ranked.length} ranked anchor(s) — CAST needs at least two`,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
111
131
|
|
|
112
132
|
const weave = await pre.weave();
|
|
113
133
|
const points = weave.points;
|
|
114
134
|
const depth = weave.depth;
|
|
115
135
|
const aligned = points.length;
|
|
116
|
-
if (aligned < 2)
|
|
136
|
+
if (aligned < 2) {
|
|
137
|
+
return fail(
|
|
138
|
+
`only ${aligned} structure(s) aligned across the query — CAST needs ` +
|
|
139
|
+
`at least two to transfer between`,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
117
142
|
|
|
118
143
|
type Point = typeof points[0];
|
|
119
144
|
|
|
@@ -154,18 +179,31 @@ export async function counterfactualTransfer(
|
|
|
154
179
|
const isRoot = (id: number) => roots.some((r) => r.anchor === id);
|
|
155
180
|
// The weave must touch a COMMITTED point of attention: the dominant
|
|
156
181
|
// structure itself, or another aligned point the climb committed to.
|
|
157
|
-
if (!points.some((p) => isRoot(p.anchor)))
|
|
182
|
+
if (!points.some((p) => isRoot(p.anchor))) {
|
|
183
|
+
t?.done(
|
|
184
|
+
[
|
|
185
|
+
...points.map((p) => rNode(ctx, p.anchor, "aligned")),
|
|
186
|
+
...roots.map((r) => rNode(ctx, r.anchor, "committed-root")),
|
|
187
|
+
],
|
|
188
|
+
`${points.length} aligned structure(s), but none is one of the climb's ` +
|
|
189
|
+
`${roots.length} committed root(s) — CAST refuses to transfer through ` +
|
|
190
|
+
`content the climb itself never settled on`,
|
|
191
|
+
);
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
158
194
|
|
|
159
195
|
const woven = points.some((p) =>
|
|
160
196
|
p.runs.some((r) =>
|
|
161
197
|
!pre.rec.sites.some((s) => r.qs >= s.start && r.qe <= s.end)
|
|
162
198
|
)
|
|
163
199
|
);
|
|
164
|
-
if (!woven)
|
|
200
|
+
if (!woven) {
|
|
201
|
+
return fail(
|
|
202
|
+
`every aligned run restates a recognised query site — nothing was ` +
|
|
203
|
+
`actually WOVEN across structures, so there is nothing to transfer`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
165
206
|
|
|
166
|
-
const t = ctx.trace?.enter("counterfactual", [
|
|
167
|
-
rItem(query, "query"),
|
|
168
|
-
]);
|
|
169
207
|
// Each schema tried below RECORDS its candidate (when it fires) rather than
|
|
170
208
|
// returning immediately — every schema that succeeds contributes its own
|
|
171
209
|
// candidate, and the grounding decider's own weight comparison (not CAST's
|