@hviana/sema 0.2.4 → 0.2.5
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 +26 -0
- package/dist/src/geometry.js +32 -3
- package/dist/src/mind/attention.d.ts +232 -5
- package/dist/src/mind/attention.js +718 -166
- package/dist/src/mind/index.d.ts +2 -0
- package/dist/src/mind/junction.d.ts +30 -1
- package/dist/src/mind/junction.js +73 -18
- package/dist/src/mind/mind.d.ts +2 -0
- package/dist/src/mind/rationale.d.ts +7 -2
- package/dist/src/mind/rationale.js +6 -5
- package/dist/src/mind/traverse.js +74 -10
- package/dist/src/mind/types.d.ts +18 -0
- package/package.json +1 -1
- package/src/geometry.ts +52 -3
- package/src/mind/attention.ts +1134 -121
- package/src/mind/index.ts +16 -0
- package/src/mind/junction.ts +125 -16
- package/src/mind/mind.ts +15 -0
- package/src/mind/rationale.ts +12 -4
- package/src/mind/traverse.ts +75 -7
- package/src/mind/types.ts +25 -0
- package/test/51-structural-resonance-ladder.test.mjs +552 -0
- package/test/52-climb-consensus-instrumentation.test.mjs +324 -0
package/dist/src/mind/index.d.ts
CHANGED
|
@@ -3,3 +3,5 @@ export type { Input, Response } from "./mind.js";
|
|
|
3
3
|
export type { ComputedSpan, ExtensionHost } from "./mind.js";
|
|
4
4
|
export type { MechanismResult, PipelineMechanism, Precomputed, } from "./pipeline-mechanism.js";
|
|
5
5
|
export type { InspectRationale, RationaleItem, RationaleStep, } from "./rationale.js";
|
|
6
|
+
export type { AnchorRejectionReason, ClimbConsensusData, ConsensusAnchorTrace, ConsensusReachTrace, ConsensusRegionTrace, CrossRegionTier, JunctionVoteTrace, RegionOutcome, } from "./attention.js";
|
|
7
|
+
export type { AncestorReach, AttentionRead, SaturationReason, SaturationStop, } from "./types.js";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Hit } from "../store.js";
|
|
1
2
|
import type { MindContext } from "./types.js";
|
|
2
3
|
export interface Junction {
|
|
3
4
|
/** The node whose learnt bytes evidence this junction (a container form,
|
|
@@ -6,6 +7,34 @@ export interface Junction {
|
|
|
6
7
|
/** The bytes that belong between left and right. */
|
|
7
8
|
interior: Uint8Array;
|
|
8
9
|
}
|
|
10
|
+
/** Which relaxation produced a {@link SynonymJunction}: one side replaced by
|
|
11
|
+
* a distributional halo sibling (`single-synonym`), or both (`double-
|
|
12
|
+
* synonym`) — the two remaining rungs of the graded ladder below exact DAG
|
|
13
|
+
* containment (see the module doc atop {@link junctionSynonyms}). */
|
|
14
|
+
export type SynonymJunctionTier = "single-synonym" | "double-synonym";
|
|
15
|
+
export interface SynonymJunction extends Junction {
|
|
16
|
+
tier: SynonymJunctionTier;
|
|
17
|
+
/** Sibling score for a single-synonym junction; min(left, right) sibling
|
|
18
|
+
* score for a double-synonym junction. */
|
|
19
|
+
confidence: number;
|
|
20
|
+
}
|
|
21
|
+
/** The exact node ids and halo siblings resolved for one junction call's two
|
|
22
|
+
* sides — computed ONCE and reused by every ladder rung that needs them
|
|
23
|
+
* (junctionSynonyms' two tiers, and the structural-resonance tier beyond
|
|
24
|
+
* it). A failed synonym junction means only "no common DAG container was
|
|
25
|
+
* proven" — it does NOT mean the loaded siblings stop being useful. */
|
|
26
|
+
export interface JunctionSynonymSides {
|
|
27
|
+
leftId: number | null;
|
|
28
|
+
rightId: number | null;
|
|
29
|
+
leftSiblings: Hit[];
|
|
30
|
+
rightSiblings: Hit[];
|
|
31
|
+
}
|
|
32
|
+
/** Resolve `left`/`right` to their exact node ids (when known) and load each
|
|
33
|
+
* resolved side's halo siblings once — deterministic (haloSiblings already
|
|
34
|
+
* ranks nearest-first) and shared by every ladder rung that consults
|
|
35
|
+
* siblings, so no ladder rung repeats a halo ANN query the previous one
|
|
36
|
+
* already paid for. */
|
|
37
|
+
export declare function loadJunctionSynonymSides(ctx: MindContext, left: Uint8Array, right: Uint8Array): Promise<JunctionSynonymSides>;
|
|
9
38
|
/** Seed node ids to ascend from for one side of a junction: the side's own
|
|
10
39
|
* node when it is a stored form, plus — when the node has no structural
|
|
11
40
|
* parents — its canonical window ids. A non-W-aligned node may have no
|
|
@@ -92,4 +121,4 @@ export declare function junctionContainers(ctx: MindContext, left: Uint8Array, r
|
|
|
92
121
|
* cost is bounded at √N·W pops total regardless of how many siblings are
|
|
93
122
|
* tried. A sibling whose bytes exceed `maxInterior` is skipped (it
|
|
94
123
|
* cannot be junction-sized). */
|
|
95
|
-
export declare function junctionSynonyms(ctx: MindContext, left: Uint8Array, right: Uint8Array, maxInterior: number, unordered?: boolean): Promise<
|
|
124
|
+
export declare function junctionSynonyms(ctx: MindContext, left: Uint8Array, right: Uint8Array, maxInterior: number, unordered?: boolean, sides?: JunctionSynonymSides): Promise<SynonymJunction[]>;
|
|
@@ -16,6 +16,20 @@ import { windowIds } from "./canonical.js";
|
|
|
16
16
|
import { hubBound } from "./traverse.js";
|
|
17
17
|
import { haloSiblings } from "./match.js";
|
|
18
18
|
import { indexOf } from "../bytes.js";
|
|
19
|
+
/** Resolve `left`/`right` to their exact node ids (when known) and load each
|
|
20
|
+
* resolved side's halo siblings once — deterministic (haloSiblings already
|
|
21
|
+
* ranks nearest-first) and shared by every ladder rung that consults
|
|
22
|
+
* siblings, so no ladder rung repeats a halo ANN query the previous one
|
|
23
|
+
* already paid for. */
|
|
24
|
+
export async function loadJunctionSynonymSides(ctx, left, right) {
|
|
25
|
+
const leftId = resolve(ctx, left);
|
|
26
|
+
const rightId = resolve(ctx, right);
|
|
27
|
+
const leftSiblings = leftId !== null ? await haloSiblings(ctx, leftId) : [];
|
|
28
|
+
const rightSiblings = rightId !== null
|
|
29
|
+
? await haloSiblings(ctx, rightId)
|
|
30
|
+
: [];
|
|
31
|
+
return { leftId, rightId, leftSiblings, rightSiblings };
|
|
32
|
+
}
|
|
19
33
|
/** Seed node ids to ascend from for one side of a junction: the side's own
|
|
20
34
|
* node when it is a stored form, plus — when the node has no structural
|
|
21
35
|
* parents — its canonical window ids. A non-W-aligned node may have no
|
|
@@ -225,38 +239,79 @@ export function junctionContainers(ctx, left, right, maxContainer, unordered = f
|
|
|
225
239
|
* cost is bounded at √N·W pops total regardless of how many siblings are
|
|
226
240
|
* tried. A sibling whose bytes exceed `maxInterior` is skipped (it
|
|
227
241
|
* cannot be junction-sized). */
|
|
228
|
-
export async function junctionSynonyms(ctx, left, right, maxInterior, unordered = false) {
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const
|
|
242
|
+
export async function junctionSynonyms(ctx, left, right, maxInterior, unordered = false, sides) {
|
|
243
|
+
const s = sides ?? await loadJunctionSynonymSides(ctx, left, right);
|
|
244
|
+
if (s.leftId === null && s.rightId === null)
|
|
245
|
+
return [];
|
|
246
|
+
// ── Tier 2.5a: single-synonym — one side replaced by a halo sibling ──────
|
|
247
|
+
// ONE shared expansion budget across BOTH directions of this tier.
|
|
248
|
+
const singleBudget = { n: hubBound(ctx) * ctx.space.maxGroup };
|
|
249
|
+
const singleOut = new Map();
|
|
250
|
+
const keepBest = (map, j, tier, confidence) => {
|
|
251
|
+
const prev = map.get(j.id);
|
|
252
|
+
if (prev === undefined || confidence > prev.confidence) {
|
|
253
|
+
map.set(j.id, { ...j, tier, confidence });
|
|
254
|
+
}
|
|
255
|
+
};
|
|
235
256
|
// Left-side synonyms: containers of sibling+right. `right`'s seeds are
|
|
236
257
|
// FIXED across every sibling this loop tries.
|
|
237
|
-
if (
|
|
258
|
+
if (s.leftId !== null) {
|
|
238
259
|
const rightSeeds = junctionSeeds(ctx, right);
|
|
239
|
-
for (const sib of
|
|
260
|
+
for (const sib of s.leftSiblings) {
|
|
240
261
|
const sibBytes = read(ctx, sib.id, maxInterior + 1);
|
|
241
262
|
if (sibBytes.length === 0 || sibBytes.length > maxInterior)
|
|
242
263
|
continue;
|
|
243
|
-
const containers = junctionContainersFrom(ctx, sibBytes, right, sibBytes.length + right.length + maxInterior, junctionSeeds(ctx, sibBytes), rightSeeds,
|
|
244
|
-
for (const c of containers)
|
|
245
|
-
|
|
264
|
+
const containers = junctionContainersFrom(ctx, sibBytes, right, sibBytes.length + right.length + maxInterior, junctionSeeds(ctx, sibBytes), rightSeeds, singleBudget, unordered);
|
|
265
|
+
for (const c of containers) {
|
|
266
|
+
keepBest(singleOut, c, "single-synonym", sib.score);
|
|
267
|
+
}
|
|
246
268
|
}
|
|
247
269
|
}
|
|
248
270
|
// Right-side synonyms: containers of left+sibling. `left`'s seeds are
|
|
249
271
|
// likewise fixed across this loop.
|
|
250
|
-
if (
|
|
272
|
+
if (s.rightId !== null) {
|
|
251
273
|
const leftSeeds = junctionSeeds(ctx, left);
|
|
252
|
-
for (const sib of
|
|
274
|
+
for (const sib of s.rightSiblings) {
|
|
253
275
|
const sibBytes = read(ctx, sib.id, maxInterior + 1);
|
|
254
276
|
if (sibBytes.length === 0 || sibBytes.length > maxInterior)
|
|
255
277
|
continue;
|
|
256
|
-
const containers = junctionContainersFrom(ctx, left, sibBytes, left.length + sibBytes.length + maxInterior, leftSeeds, junctionSeeds(ctx, sibBytes),
|
|
257
|
-
for (const c of containers)
|
|
258
|
-
|
|
278
|
+
const containers = junctionContainersFrom(ctx, left, sibBytes, left.length + sibBytes.length + maxInterior, leftSeeds, junctionSeeds(ctx, sibBytes), singleBudget, unordered);
|
|
279
|
+
for (const c of containers) {
|
|
280
|
+
keepBest(singleOut, c, "single-synonym", sib.score);
|
|
281
|
+
}
|
|
259
282
|
}
|
|
260
283
|
}
|
|
261
|
-
|
|
284
|
+
if (singleOut.size > 0)
|
|
285
|
+
return [...singleOut.values()];
|
|
286
|
+
// ── Tier 2.5b: double-synonym — BOTH sides replaced, tried only when
|
|
287
|
+
// single-synonym found NOTHING. Every (leftSibling, rightSibling) pair,
|
|
288
|
+
// sorted deterministically, bounded to haloQueryK pairs total, ONE fresh
|
|
289
|
+
// shared budget for the whole tier. ─────────────────────────────────────
|
|
290
|
+
if (s.leftSiblings.length === 0 || s.rightSiblings.length === 0)
|
|
291
|
+
return [];
|
|
292
|
+
const pairs = [];
|
|
293
|
+
for (const l of s.leftSiblings) {
|
|
294
|
+
for (const r of s.rightSiblings) {
|
|
295
|
+
pairs.push({ l, r, confidence: Math.min(l.score, r.score) });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
pairs.sort((a, b) => b.confidence - a.confidence ||
|
|
299
|
+
a.l.id - b.l.id ||
|
|
300
|
+
a.r.id - b.r.id);
|
|
301
|
+
const doubleOut = new Map();
|
|
302
|
+
const budget = { n: hubBound(ctx) * ctx.space.maxGroup };
|
|
303
|
+
const tries = Math.min(pairs.length, ctx.cfg.haloQueryK);
|
|
304
|
+
for (let i = 0; i < tries; i++) {
|
|
305
|
+
const { l, r, confidence } = pairs[i];
|
|
306
|
+
const lBytes = read(ctx, l.id, maxInterior + 1);
|
|
307
|
+
const rBytes = read(ctx, r.id, maxInterior + 1);
|
|
308
|
+
if (lBytes.length === 0 || lBytes.length > maxInterior ||
|
|
309
|
+
rBytes.length === 0 || rBytes.length > maxInterior)
|
|
310
|
+
continue;
|
|
311
|
+
const containers = junctionContainersFrom(ctx, lBytes, rBytes, lBytes.length + rBytes.length + maxInterior, junctionSeeds(ctx, lBytes), junctionSeeds(ctx, rBytes), budget, unordered);
|
|
312
|
+
for (const c of containers) {
|
|
313
|
+
keepBest(doubleOut, c, "double-synonym", confidence);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return [...doubleOut.values()];
|
|
262
317
|
}
|
package/dist/src/mind/mind.d.ts
CHANGED
|
@@ -42,6 +42,8 @@ export interface Conversation {
|
|
|
42
42
|
readonly id: number;
|
|
43
43
|
}
|
|
44
44
|
import type { AttentionRead, MindContext, Recognition } from "./types.js";
|
|
45
|
+
export type { AnchorRejectionReason, ClimbConsensusData, ConsensusAnchorTrace, ConsensusReachTrace, ConsensusRegionTrace, CrossRegionTier, JunctionVoteTrace, RegionOutcome, } from "./attention.js";
|
|
46
|
+
export type { AncestorReach, SaturationReason, SaturationStop, } from "./types.js";
|
|
45
47
|
export interface MindOptions {
|
|
46
48
|
seed?: number;
|
|
47
49
|
recallQueryK?: number;
|
|
@@ -59,6 +59,11 @@ export interface RationaleStep {
|
|
|
59
59
|
/** A one-line, human account of what the mechanism did and why — the sentence
|
|
60
60
|
* that turns the data into an explanation. */
|
|
61
61
|
note?: string;
|
|
62
|
+
/** Optional structured payload — a mechanism-specific, plain-serialisable
|
|
63
|
+
* shape (no Map/Set/vectors/mutable internals) that carries more than the
|
|
64
|
+
* human-readable `note` can, for a debugger or downstream tool to consume
|
|
65
|
+
* programmatically. Never read by inference; purely additive. */
|
|
66
|
+
data?: unknown;
|
|
62
67
|
}
|
|
63
68
|
/** The callback {@link Mind.respond} / {@link Mind.respondText} accept. It is
|
|
64
69
|
* invoked once per completed step, AS the inference unfolds — never batched at
|
|
@@ -86,7 +91,7 @@ export interface Scope {
|
|
|
86
91
|
/** Close the mechanism: emit its step with these outputs and pop it off the
|
|
87
92
|
* nesting stack. Idempotent — a second call is ignored, so a `finally` that
|
|
88
93
|
* closes after an early return is safe. */
|
|
89
|
-
done(outputs: RationaleItem[], note?: string): void;
|
|
94
|
+
done(outputs: RationaleItem[], note?: string, data?: unknown): void;
|
|
90
95
|
}
|
|
91
96
|
/** The live tracer: a stack of open mechanisms over one {@link Mind.respond}.
|
|
92
97
|
*
|
|
@@ -130,5 +135,5 @@ export declare class Rationale {
|
|
|
130
135
|
enter(name: string, inputs: RationaleItem[], deps?: number[]): Scope;
|
|
131
136
|
/** Record a mechanism that has no sub-steps — its inputs and outputs are both
|
|
132
137
|
* known at the call site. Returns its index, for a later step to depend on. */
|
|
133
|
-
step(name: string, inputs: RationaleItem[], outputs: RationaleItem[], note?: string, deps?: number[]): number;
|
|
138
|
+
step(name: string, inputs: RationaleItem[], outputs: RationaleItem[], note?: string, deps?: number[], data?: unknown): number;
|
|
134
139
|
}
|
|
@@ -109,7 +109,7 @@ export class Rationale {
|
|
|
109
109
|
this.lastByName.set(name, index);
|
|
110
110
|
return index;
|
|
111
111
|
}
|
|
112
|
-
emit(index, mechanism, inputs, outputs, deps, note) {
|
|
112
|
+
emit(index, mechanism, inputs, outputs, deps, note, data) {
|
|
113
113
|
this.sink({
|
|
114
114
|
index,
|
|
115
115
|
mechanism,
|
|
@@ -120,6 +120,7 @@ export class Rationale {
|
|
|
120
120
|
inputs,
|
|
121
121
|
outputs,
|
|
122
122
|
note,
|
|
123
|
+
data,
|
|
123
124
|
});
|
|
124
125
|
}
|
|
125
126
|
/** Enter a mechanism that has sub-steps. Captures its inputs and the nesting
|
|
@@ -141,22 +142,22 @@ export class Rationale {
|
|
|
141
142
|
};
|
|
142
143
|
return {
|
|
143
144
|
index,
|
|
144
|
-
done: (outputs, note) => {
|
|
145
|
+
done: (outputs, note, data) => {
|
|
145
146
|
if (closed)
|
|
146
147
|
return;
|
|
147
148
|
closed = true;
|
|
148
149
|
pop();
|
|
149
|
-
emit(index, mechanism, inputs, outputs, resolvedDeps, note);
|
|
150
|
+
emit(index, mechanism, inputs, outputs, resolvedDeps, note, data);
|
|
150
151
|
},
|
|
151
152
|
};
|
|
152
153
|
}
|
|
153
154
|
/** Record a mechanism that has no sub-steps — its inputs and outputs are both
|
|
154
155
|
* known at the call site. Returns its index, for a later step to depend on. */
|
|
155
|
-
step(name, inputs, outputs, note, deps) {
|
|
156
|
+
step(name, inputs, outputs, note, deps, data) {
|
|
156
157
|
const mechanism = this.path(name);
|
|
157
158
|
const resolvedDeps = deps ?? this.defaultDeps();
|
|
158
159
|
const index = this.reserve(name);
|
|
159
|
-
this.emit(index, mechanism, inputs, outputs, resolvedDeps, note);
|
|
160
|
+
this.emit(index, mechanism, inputs, outputs, resolvedDeps, note, data);
|
|
160
161
|
return index;
|
|
161
162
|
}
|
|
162
163
|
}
|
|
@@ -89,7 +89,22 @@ export function edgeAncestors(ctx, id, contextCount, memo) {
|
|
|
89
89
|
// is withdrawn. On a small store the floor stays ≤ √N and the atom
|
|
90
90
|
// climbs exactly as before, so single-letter facts keep working.
|
|
91
91
|
if (id < 0 && atomIsHub(ctx, contextCount)) {
|
|
92
|
-
const
|
|
92
|
+
const bound0 = Math.ceil(Math.sqrt(Math.max(2, contextCount)));
|
|
93
|
+
const reach = {
|
|
94
|
+
roots: [],
|
|
95
|
+
contextsReached: 0,
|
|
96
|
+
saturated: true,
|
|
97
|
+
...(ctx.trace
|
|
98
|
+
? {
|
|
99
|
+
saturation: {
|
|
100
|
+
reason: "byte-atom-commonality",
|
|
101
|
+
node: id,
|
|
102
|
+
observed: atomReach(ctx, contextCount),
|
|
103
|
+
limit: bound0,
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
: {}),
|
|
107
|
+
};
|
|
93
108
|
memo?.set(id, reach);
|
|
94
109
|
return reach;
|
|
95
110
|
}
|
|
@@ -98,6 +113,10 @@ export function edgeAncestors(ctx, id, contextCount, memo) {
|
|
|
98
113
|
const seen = new Set([id]);
|
|
99
114
|
const ctxSeen = new Set();
|
|
100
115
|
let saturated = false;
|
|
116
|
+
// Provenance of the FIRST decision that saturated this climb — allocated
|
|
117
|
+
// only when a trace is requested (see AncestorReach.saturation's doc); the
|
|
118
|
+
// climb itself never reads it back.
|
|
119
|
+
let satStop;
|
|
101
120
|
// EXPAND-UNTIL-DECIDED: a reach is consumed either as a VOTE (which needs
|
|
102
121
|
// contextsReached exactly, and only while ≤ √N — beyond that the region is
|
|
103
122
|
// non-discriminative) or as an ABSTENTION (saturated — whose roots and
|
|
@@ -139,16 +158,46 @@ export function edgeAncestors(ctx, id, contextCount, memo) {
|
|
|
139
158
|
roots.push(x);
|
|
140
159
|
if (hasNx)
|
|
141
160
|
ctxSeen.add(x);
|
|
142
|
-
if (pc > bound)
|
|
143
|
-
|
|
161
|
+
if (pc > bound) {
|
|
162
|
+
// decided: ≥ pc > √N distinct contexts
|
|
163
|
+
if (ctx.trace) {
|
|
164
|
+
satStop = {
|
|
165
|
+
reason: "predecessor-fan-in",
|
|
166
|
+
node: x,
|
|
167
|
+
observed: pc,
|
|
168
|
+
limit: bound,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
144
173
|
for (const p of ctx.store.prevFirst(x, bound))
|
|
145
174
|
ctxSeen.add(p);
|
|
146
|
-
if (ctxSeen.size > bound)
|
|
147
|
-
|
|
175
|
+
if (ctxSeen.size > bound) {
|
|
176
|
+
// decided
|
|
177
|
+
if (ctx.trace) {
|
|
178
|
+
satStop = {
|
|
179
|
+
reason: "distinct-context-limit",
|
|
180
|
+
node: x,
|
|
181
|
+
observed: ctxSeen.size,
|
|
182
|
+
limit: bound,
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
148
187
|
}
|
|
149
188
|
const parents = ctx.store.parentsFirst(x, bound + 1);
|
|
150
|
-
if (parents.length > bound)
|
|
151
|
-
|
|
189
|
+
if (parents.length > bound) {
|
|
190
|
+
// decided: hub
|
|
191
|
+
if (ctx.trace) {
|
|
192
|
+
satStop = {
|
|
193
|
+
reason: "parent-fan-out",
|
|
194
|
+
node: x,
|
|
195
|
+
observed: parents.length,
|
|
196
|
+
limit: bound,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
152
201
|
let fresh = 0;
|
|
153
202
|
for (const p of parents) {
|
|
154
203
|
if (!seen.has(p)) {
|
|
@@ -159,8 +208,18 @@ export function edgeAncestors(ctx, id, contextCount, memo) {
|
|
|
159
208
|
}
|
|
160
209
|
if (fresh > 1) {
|
|
161
210
|
lateral += fresh - 1;
|
|
162
|
-
if (lateral > bound)
|
|
163
|
-
|
|
211
|
+
if (lateral > bound) {
|
|
212
|
+
// decided: cone-wide hub
|
|
213
|
+
if (ctx.trace) {
|
|
214
|
+
satStop = {
|
|
215
|
+
reason: "lateral-cone-limit",
|
|
216
|
+
node: x,
|
|
217
|
+
observed: lateral,
|
|
218
|
+
limit: bound,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
164
223
|
}
|
|
165
224
|
return true;
|
|
166
225
|
};
|
|
@@ -225,7 +284,12 @@ export function edgeAncestors(ctx, id, contextCount, memo) {
|
|
|
225
284
|
}
|
|
226
285
|
}
|
|
227
286
|
}
|
|
228
|
-
const reach = {
|
|
287
|
+
const reach = {
|
|
288
|
+
roots,
|
|
289
|
+
contextsReached: ctxSeen.size,
|
|
290
|
+
saturated,
|
|
291
|
+
...(saturated && satStop ? { saturation: satStop } : {}),
|
|
292
|
+
};
|
|
229
293
|
memo?.set(id, reach);
|
|
230
294
|
return reach;
|
|
231
295
|
}
|
package/dist/src/mind/types.d.ts
CHANGED
|
@@ -133,11 +133,29 @@ export interface RegionVote {
|
|
|
133
133
|
* Defaults to 1 when absent. */
|
|
134
134
|
absorbed?: number;
|
|
135
135
|
}
|
|
136
|
+
/** The structural gate that first decided an {@link edgeAncestors} climb was
|
|
137
|
+
* saturated (an abstention, not a discriminative conclusion) — pure
|
|
138
|
+
* instrumentation for {@link ClimbConsensusData}'s reach trace; it never
|
|
139
|
+
* feeds back into the climb itself. */
|
|
140
|
+
export type SaturationReason = "byte-atom-commonality" | "predecessor-fan-in" | "distinct-context-limit" | "parent-fan-out" | "lateral-cone-limit";
|
|
141
|
+
/** One saturation stop's provenance: which reason fired, at which node, the
|
|
142
|
+
* observed count against the bound that decided it. */
|
|
143
|
+
export interface SaturationStop {
|
|
144
|
+
reason: SaturationReason;
|
|
145
|
+
node: number;
|
|
146
|
+
observed: number;
|
|
147
|
+
limit: number;
|
|
148
|
+
}
|
|
136
149
|
/** The edge-bearing contexts reached by climbing from a node, plus saturation info. */
|
|
137
150
|
export interface AncestorReach {
|
|
138
151
|
roots: number[];
|
|
139
152
|
contextsReached: number;
|
|
140
153
|
saturated: boolean;
|
|
154
|
+
/** The saturation gate that stopped this climb, when {@link saturated} is
|
|
155
|
+
* true and a trace was requested — see {@link edgeAncestors}. Absent for
|
|
156
|
+
* a non-saturated reach, and absent (even when saturated) when no trace
|
|
157
|
+
* was requested — instrumentation must not allocate when tracing is off. */
|
|
158
|
+
saturation?: SaturationStop;
|
|
141
159
|
}
|
|
142
160
|
/** Saturated-interval information for the noise-drop gate. */
|
|
143
161
|
export interface SaturationInfo {
|
package/package.json
CHANGED
package/src/geometry.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// identically regardless of what follows — pure structural stability.
|
|
8
8
|
// 3. The same rule recurses level after level until one root remains.
|
|
9
9
|
|
|
10
|
-
import { normalize, Vec } from "./vec.js";
|
|
10
|
+
import { addInto, copy, normalize, Vec, zeros } from "./vec.js";
|
|
11
11
|
import { Sema, sema, Space } from "./sema.js";
|
|
12
12
|
import { Alphabet } from "./alphabet.js";
|
|
13
13
|
|
|
@@ -461,8 +461,10 @@ function fold2(space: Space, a: Folded, b: Folded): Folded {
|
|
|
461
461
|
/** Plain river fold WITHOUT the final root normalize — the segment-level
|
|
462
462
|
* building block of {@link stablePrefixFold} (interiors must keep their
|
|
463
463
|
* byte-proportional magnitude; only the whole perception's root is ever
|
|
464
|
-
* normalized).
|
|
465
|
-
|
|
464
|
+
* normalized). Exported so callers that COMPOSE already-existing structural
|
|
465
|
+
* parts into a hypothetical synthetic root (see {@link composeStructuralGist})
|
|
466
|
+
* can feed the same raw primitive instead of duplicating its mathematics. */
|
|
467
|
+
export function riverFoldRaw(space: Space, row: Folded[]): Folded {
|
|
466
468
|
if (row.length === 0) {
|
|
467
469
|
const z = new Float32Array(space.D);
|
|
468
470
|
return { tree: sema(z, new Uint8Array(0), null), len: 0 };
|
|
@@ -477,6 +479,53 @@ function riverFoldRaw(space: Space, row: Folded[]): Folded {
|
|
|
477
479
|
return level[0];
|
|
478
480
|
}
|
|
479
481
|
|
|
482
|
+
// ---- structural composition (synthesize from EXISTING structural parts) ----
|
|
483
|
+
|
|
484
|
+
/** One already-existing structural vector to compose, paired with the byte
|
|
485
|
+
* span (query-slot) length it stands in for. `len`, not the vector's own
|
|
486
|
+
* magnitude, is what {@link composeStructuralGist} restores — the composed
|
|
487
|
+
* slot's NATURAL span, exactly as the linear river fold would carry it. */
|
|
488
|
+
export interface StructuralPart {
|
|
489
|
+
v: Vec;
|
|
490
|
+
len: number;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** Synthesize a hypothetical internal structure from already-existing
|
|
494
|
+
* structural vectors — NOT from bytes. This is the raw positional
|
|
495
|
+
* composition the linear river fold already uses (see the folding header
|
|
496
|
+
* above): each part is positionally bound into its own seat, its natural
|
|
497
|
+
* span magnitude is preserved, the parts are linearly superposed, and only
|
|
498
|
+
* the final synthetic root is normalized. It never calls {@link gistOf}
|
|
499
|
+
* (there is no `gistOf` here — geometry.ts has no store), never perceives a
|
|
500
|
+
* concatenated byte string, and never interns or stores a new node: the
|
|
501
|
+
* result is an opaque, ungrounded Vec for an ANN probe only. */
|
|
502
|
+
export function composeStructuralGist(
|
|
503
|
+
space: Space,
|
|
504
|
+
parts: readonly StructuralPart[],
|
|
505
|
+
): Vec {
|
|
506
|
+
const foldedParts: Folded[] = [];
|
|
507
|
+
|
|
508
|
+
for (const part of parts) {
|
|
509
|
+
if (part.len <= 0) continue;
|
|
510
|
+
|
|
511
|
+
const direction = copy(part.v);
|
|
512
|
+
normalize(direction);
|
|
513
|
+
|
|
514
|
+
const scaled = zeros(space.D);
|
|
515
|
+
addInto(scaled, direction, Math.sqrt(part.len));
|
|
516
|
+
|
|
517
|
+
foldedParts.push({ tree: sema(scaled), len: part.len });
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (foldedParts.length === 0) return zeros(space.D);
|
|
521
|
+
|
|
522
|
+
const rawRoot = riverFoldRaw(space, foldedParts);
|
|
523
|
+
|
|
524
|
+
const result = copy(rawRoot.tree.v);
|
|
525
|
+
normalize(result);
|
|
526
|
+
return result;
|
|
527
|
+
}
|
|
528
|
+
|
|
480
529
|
// ---- pyramid fold (incremental plain perception) ----
|
|
481
530
|
|
|
482
531
|
/** The PLAIN fold's full level pyramid — every level's item list, bottom
|