@hviana/sema 0.2.1 → 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/geometry.d.ts +28 -0
- package/dist/src/geometry.js +35 -0
- package/dist/src/mind/articulation.js +1 -1
- package/dist/src/mind/attention.d.ts +4 -0
- package/dist/src/mind/attention.js +65 -12
- package/dist/src/mind/graph-search.d.ts +16 -1
- package/dist/src/mind/graph-search.js +34 -5
- package/dist/src/mind/learning.d.ts +1 -1
- package/dist/src/mind/learning.js +20 -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 +40 -1
- package/dist/src/mind/mechanisms/extraction.js +75 -30
- package/dist/src/mind/mechanisms/recall.js +26 -2
- package/dist/src/mind/mind.d.ts +3 -2
- package/dist/src/mind/mind.js +28 -54
- package/dist/src/mind/pipeline.js +34 -2
- package/dist/src/mind/primitives.d.ts +16 -9
- package/dist/src/mind/primitives.js +58 -22
- 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 +58 -10
- package/dist/src/mind/types.js +15 -0
- package/package.json +1 -1
- package/src/geometry.ts +61 -1
- package/src/mind/articulation.ts +1 -0
- package/src/mind/attention.ts +80 -12
- package/src/mind/graph-search.ts +59 -2
- package/src/mind/learning.ts +20 -3
- package/src/mind/mechanisms/alu.ts +8 -1
- package/src/mind/mechanisms/cast.ts +46 -8
- package/src/mind/mechanisms/cover.ts +46 -0
- package/src/mind/mechanisms/extraction.ts +101 -40
- package/src/mind/mechanisms/recall.ts +30 -2
- package/src/mind/mind.ts +38 -61
- package/src/mind/pipeline.ts +37 -2
- package/src/mind/primitives.ts +71 -29
- 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 +73 -10
- package/test/35-attention-confidence.test.mjs +139 -0
|
@@ -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
|
@@ -6,7 +6,25 @@ import type { Alphabet } from "../alphabet.js";
|
|
|
6
6
|
import type { MindConfig } from "../config.js";
|
|
7
7
|
import type { GraphSearch, Leaf, Seg, Site } from "./graph-search.js";
|
|
8
8
|
import type { Rationale } from "./rationale.js";
|
|
9
|
-
import type { FoldPyramid, Grid } from "../geometry.js";
|
|
9
|
+
import type { FoldPyramid, Grid, StableFold } from "../geometry.js";
|
|
10
|
+
/** One {@link MindContext._depositTrees} entry — see that field's doc. */
|
|
11
|
+
export interface DepositCacheEntry {
|
|
12
|
+
/** Turn boundaries accumulated over this content's deposit chain —
|
|
13
|
+
* strictly increasing proper offsets, each a previously-deposited
|
|
14
|
+
* whole-context length. Empty for a first-seen (single-turn) input. */
|
|
15
|
+
boundaries: number[];
|
|
16
|
+
/** Plain-fold pyramid (first-seen inputs only). */
|
|
17
|
+
pyramid?: FoldPyramid;
|
|
18
|
+
/** Stable-prefix segment folds (grown-context inputs only). */
|
|
19
|
+
stable?: StableFold;
|
|
20
|
+
/** The continuation bytes this ctxInput was paired with in ingestPair, if
|
|
21
|
+
* any — the ONLY thing that makes a later, longer ctxInput a genuine next
|
|
22
|
+
* TURN of the same conversation rather than an unrelated fact that
|
|
23
|
+
* happens to share this one's byte prefix (e.g. "2+2" vs. "2+2=5"). A
|
|
24
|
+
* later deposit only takes this entry as its stable-prefix `prev` when
|
|
25
|
+
* its own suffix bytes-equal this exactly. */
|
|
26
|
+
nextBytes?: Uint8Array;
|
|
27
|
+
}
|
|
10
28
|
export type Input = string | Uint8Array | Grid | Grid[];
|
|
11
29
|
/** The host capabilities GraphSearch consults during a cover. MindContext
|
|
12
30
|
* extends this so the Mind can pass itself as the host. */
|
|
@@ -16,6 +34,7 @@ export interface GraphSearchHost {
|
|
|
16
34
|
sites: ReadonlyArray<Site>;
|
|
17
35
|
leaves: ReadonlyArray<Leaf>;
|
|
18
36
|
splits: ReadonlySet<number>;
|
|
37
|
+
starts: ReadonlySet<number>;
|
|
19
38
|
};
|
|
20
39
|
chooseNext?(node: number): number | undefined;
|
|
21
40
|
}
|
|
@@ -26,6 +45,13 @@ export interface Recognition {
|
|
|
26
45
|
leaves: Leaf[];
|
|
27
46
|
/** Sub-leaf positions where a form boundary falls between leaf edges. */
|
|
28
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>;
|
|
29
55
|
}
|
|
30
56
|
/** How the consensus climb weights a region's Document-Frequency reach. */
|
|
31
57
|
export type DFMode = "inverse" | "direct" | "combined";
|
|
@@ -38,6 +64,16 @@ export interface Attention {
|
|
|
38
64
|
/** The union of the query byte-spans whose evidence supports this point. */
|
|
39
65
|
start: number;
|
|
40
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;
|
|
41
77
|
}
|
|
42
78
|
/** Both read-outs of one consensus climb. */
|
|
43
79
|
export interface AttentionRead {
|
|
@@ -71,6 +107,14 @@ export interface RegionVote {
|
|
|
71
107
|
roots: readonly number[];
|
|
72
108
|
w: number;
|
|
73
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;
|
|
74
118
|
}
|
|
75
119
|
/** The edge-bearing contexts reached by climbing from a node, plus saturation info. */
|
|
76
120
|
export interface AncestorReach {
|
|
@@ -153,15 +197,19 @@ export interface MindContext extends GraphSearchHost {
|
|
|
153
197
|
* never invalidated. Bounded LRU (byte-sized); a miss only re-perceives,
|
|
154
198
|
* never a correctness risk. */
|
|
155
199
|
_gistCache: BoundedMap<number, Vec>;
|
|
156
|
-
/** DEPOSIT-path
|
|
157
|
-
*
|
|
158
|
-
* whose
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
|
|
200
|
+
/** DEPOSIT-path perception cache: content key (latin1) of a deposited
|
|
201
|
+
* input → its accumulated turn BOUNDARIES plus reusable fold state. A
|
|
202
|
+
* deposit whose content extends a cached entry IS a conversation context
|
|
203
|
+
* grown by one turn — the cached length is the new boundary — so it
|
|
204
|
+
* folds with the SAME stable-prefix fold query-time perception uses
|
|
205
|
+
* (structural train/inference agreement, load-bearing for recall),
|
|
206
|
+
* reusing every already-folded segment via `stable` (see StableFold) —
|
|
207
|
+
* O(turn) per deposit instead of O(context). A first-seen input keeps
|
|
208
|
+
* the plain fold and caches its `pyramid` (see FoldPyramid). Purely a
|
|
209
|
+
* performance cache for the FOLD STATE; the boundaries are semantic but
|
|
210
|
+
* derived only from the deposit sequence itself (an evicted chain falls
|
|
211
|
+
* back to plain-fold behavior, exactly the pre-boundary shape). */
|
|
212
|
+
_depositTrees: BoundedMap<string, DepositCacheEntry>;
|
|
165
213
|
/** The byte lengths present in {@link _depositTrees} — the candidate
|
|
166
214
|
* prefix lengths probed (longest first). Drifts on eviction (a stale
|
|
167
215
|
* length only costs a miss); cleared with the map when it outgrows the
|
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/geometry.ts
CHANGED
|
@@ -145,7 +145,7 @@ export function coverageBar(_maxGroup: number, D: number): number {
|
|
|
145
145
|
|
|
146
146
|
// ---- types ----
|
|
147
147
|
|
|
148
|
-
interface Folded {
|
|
148
|
+
export interface Folded {
|
|
149
149
|
tree: Sema;
|
|
150
150
|
/** Byte length of the subtree — carried incrementally so the stable-prefix
|
|
151
151
|
* boundary scan never re-walks subtrees (the old per-level walk was
|
|
@@ -383,6 +383,66 @@ function stablePrefixFold(
|
|
|
383
383
|
return cur.tree;
|
|
384
384
|
}
|
|
385
385
|
|
|
386
|
+
/** A stable-prefix fold's reusable state: the segment edge offsets and each
|
|
387
|
+
* segment's independently-folded root ({@link riverFoldRaw} output). A
|
|
388
|
+
* grown stream whose boundary set EXTENDS a previous fold's reuses every
|
|
389
|
+
* matching segment's Folded unchanged (segments fold independently by
|
|
390
|
+
* construction, so reuse is bit-identical to refolding) and folds only the
|
|
391
|
+
* new right-edge segment — O(turn) per extension, the stable-prefix
|
|
392
|
+
* counterpart of {@link FoldPyramid}. Purely a cache: the produced tree
|
|
393
|
+
* never depends on cache state. */
|
|
394
|
+
export interface StableFold {
|
|
395
|
+
edges: number[];
|
|
396
|
+
segs: Folded[];
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** {@link stablePrefixFold} with incremental segment reuse — same cuts, same
|
|
400
|
+
* segment folds, same left-nested join, same single root normalize; `prev`
|
|
401
|
+
* only elides recomputing segments whose [start,end) offsets it already
|
|
402
|
+
* folded over a byte-identical prefix (the caller keys the cache by
|
|
403
|
+
* content). Requires a non-empty effective boundary set. */
|
|
404
|
+
export function stablePrefixFoldIncremental(
|
|
405
|
+
space: Space,
|
|
406
|
+
alphabet: Alphabet,
|
|
407
|
+
bytes: Uint8Array,
|
|
408
|
+
boundaries: readonly number[],
|
|
409
|
+
prev?: StableFold,
|
|
410
|
+
): { tree: Sema; fold: StableFold } {
|
|
411
|
+
const cuts: number[] = [];
|
|
412
|
+
let prevB = 0;
|
|
413
|
+
for (const b of boundaries) {
|
|
414
|
+
if (b > prevB && b < bytes.length) {
|
|
415
|
+
cuts.push(b);
|
|
416
|
+
prevB = b;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
const edges = [0, ...cuts, bytes.length];
|
|
420
|
+
const segs: Folded[] = [];
|
|
421
|
+
for (let i = 0; i + 1 < edges.length; i++) {
|
|
422
|
+
const hit = prev !== undefined && prev.edges[i] === edges[i] &&
|
|
423
|
+
prev.edges[i + 1] === edges[i + 1]
|
|
424
|
+
? prev.segs[i]
|
|
425
|
+
: undefined;
|
|
426
|
+
segs.push(
|
|
427
|
+
hit ??
|
|
428
|
+
riverFoldRaw(
|
|
429
|
+
space,
|
|
430
|
+
bytesToLeaves(alphabet, bytes.subarray(edges[i], edges[i + 1])),
|
|
431
|
+
),
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
if (segs.length === 1) {
|
|
435
|
+
// Degenerate boundary set — the plain fold, as stablePrefixFold does.
|
|
436
|
+
const tree =
|
|
437
|
+
riverFold(space, bytesToLeaves(alphabet, bytes), bytes.length).tree;
|
|
438
|
+
return { tree, fold: { edges, segs } };
|
|
439
|
+
}
|
|
440
|
+
let cur = segs[0];
|
|
441
|
+
for (let i = 1; i < segs.length; i++) cur = fold2(space, cur, segs[i]);
|
|
442
|
+
normalize(cur.tree.v);
|
|
443
|
+
return { tree: cur.tree, fold: { edges, segs } };
|
|
444
|
+
}
|
|
445
|
+
|
|
386
446
|
/** Join two folded items as one 2-kid branch — the top-level join of the
|
|
387
447
|
* stable-prefix fold, identical FP ops to foldSlice's seat-bound
|
|
388
448
|
* accumulation over a group of two. Unnormalized (interior). */
|
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
|
|
|
@@ -867,6 +886,28 @@ async function crossRegionVotes(
|
|
|
867
886
|
const rb = regions[cand[b]];
|
|
868
887
|
if (!strong.has(cand[a]) && !strong.has(cand[b])) continue;
|
|
869
888
|
if (ra.end >= rb.start) continue; // overlap or adjacent — nothing between
|
|
889
|
+
// Candidates strictly BETWEEN ra and rb (cand is sorted by start, so
|
|
890
|
+
// that is exactly cand[a+1 .. b-1]) that already cast their OWN vote —
|
|
891
|
+
// genuine, individually-corroborated evidence about what fills the gap
|
|
892
|
+
// — gate the container search below: a joint container is binding
|
|
893
|
+
// evidence only when it is CONSISTENT with that evidence, i.e. its own
|
|
894
|
+
// bytes actually contain what the between-region says. This is the
|
|
895
|
+
// n-ary composition's normal shape (a between-attribute's bytes DO
|
|
896
|
+
// recur inside the joint container, credited as an "extra" below) as
|
|
897
|
+
// opposed to a container that silently substitutes something else for
|
|
898
|
+
// it (e.g. bridging past "Italy" to a container whose interior is
|
|
899
|
+
// "Japan" — a different, contradicting learnt whole).
|
|
900
|
+
// Only a KNOWN (content-addressed, exact) between-region qualifies —
|
|
901
|
+
// an approximate region's resonance climbing "somewhere" is ordinary
|
|
902
|
+
// noise (any ANN query returns SOME nearest neighbour), not evidence
|
|
903
|
+
// this specific gap already means something specific.
|
|
904
|
+
const between: number[] = [];
|
|
905
|
+
for (let m = a + 1; m < b; m++) {
|
|
906
|
+
if (
|
|
907
|
+
strong.has(cand[m]) && !consumed.has(cand[m]) &&
|
|
908
|
+
regions[cand[m]].known
|
|
909
|
+
) between.push(cand[m]);
|
|
910
|
+
}
|
|
870
911
|
// A single KNOWN region covering both: the whole form is already a
|
|
871
912
|
// stored identity that votes directly; its pieces add nothing.
|
|
872
913
|
if (
|
|
@@ -932,6 +973,22 @@ async function crossRegionVotes(
|
|
|
932
973
|
);
|
|
933
974
|
if (indexOf(query, joined, 0) >= 0) continue; // query says it itself
|
|
934
975
|
}
|
|
976
|
+
// CONTRADICTION GUARD: a between-region already carrying its own
|
|
977
|
+
// vote must actually recur in this container's bytes — otherwise
|
|
978
|
+
// the container is a different learnt whole that happens to share
|
|
979
|
+
// ra/rb, and letting it stand in for the gap would silently
|
|
980
|
+
// override evidence the query itself already resolved there.
|
|
981
|
+
if (
|
|
982
|
+
between.some((bi) =>
|
|
983
|
+
indexOf(
|
|
984
|
+
bytes,
|
|
985
|
+
query.subarray(regions[bi].start, regions[bi].end),
|
|
986
|
+
0,
|
|
987
|
+
) < 0
|
|
988
|
+
)
|
|
989
|
+
) {
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
935
992
|
let cov = left.length + right.length;
|
|
936
993
|
const extras: number[] = [];
|
|
937
994
|
for (const ei of cand) {
|
|
@@ -978,14 +1035,6 @@ async function crossRegionVotes(
|
|
|
978
1035
|
spanStart = Math.min(spanStart, regions[ei].start);
|
|
979
1036
|
spanEnd = Math.max(spanEnd, regions[ei].end);
|
|
980
1037
|
}
|
|
981
|
-
out.push({
|
|
982
|
-
start: spanStart,
|
|
983
|
-
end: spanEnd,
|
|
984
|
-
canonicalFailed: false, // content-addressed: never saturation-masked
|
|
985
|
-
roots: reach.roots,
|
|
986
|
-
w,
|
|
987
|
-
wFocus: w,
|
|
988
|
-
});
|
|
989
1038
|
consumed.add(cand[a]);
|
|
990
1039
|
consumed.add(cand[b]);
|
|
991
1040
|
for (const ei of bestExtras) consumed.add(ei);
|
|
@@ -995,14 +1044,33 @@ async function crossRegionVotes(
|
|
|
995
1044
|
// vote's bytes are literally part of the learnt whole), and FULL root
|
|
996
1045
|
// disjointness is the disagreement test: a vote sharing even one root
|
|
997
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.
|
|
998
1052
|
const containerBytes = cachedRead(ctx, cache, best.id, cap);
|
|
999
1053
|
const jointRoots = new Set(reach.roots);
|
|
1054
|
+
let explainedAway = 0;
|
|
1000
1055
|
for (const rv of rvs.votes) {
|
|
1001
1056
|
if (rv.roots.some((r) => jointRoots.has(r))) continue;
|
|
1002
1057
|
const bytes = query.subarray(rv.start, rv.end);
|
|
1003
|
-
if (indexOf(containerBytes, bytes, 0) >= 0
|
|
1058
|
+
if (indexOf(containerBytes, bytes, 0) >= 0 && !superseded.has(rv)) {
|
|
1059
|
+
superseded.add(rv);
|
|
1060
|
+
explainedAway++;
|
|
1061
|
+
}
|
|
1004
1062
|
}
|
|
1005
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
|
+
|
|
1006
1074
|
const label = [cand[a], cand[b], ...bestExtras]
|
|
1007
1075
|
.sort((x, y) => regions[x].start - regions[y].start)
|
|
1008
1076
|
.map((ri) => dec(query.subarray(regions[ri].start, regions[ri].end)))
|