@hviana/sema 0.2.8 → 0.3.0
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/attention.d.ts +15 -2
- package/dist/src/mind/attention.js +16 -8
- package/dist/src/mind/index.d.ts +2 -0
- package/dist/src/mind/learning.d.ts +32 -4
- package/dist/src/mind/learning.js +26 -4
- package/dist/src/mind/mind.d.ts +4 -1
- package/dist/src/mind/mind.js +5 -2
- package/dist/src/mind/pipeline.d.ts +29 -0
- package/dist/src/mind/pipeline.js +27 -11
- package/dist/src/mind/traverse.js +30 -2
- package/dist/src/mind/types.d.ts +11 -0
- package/package.json +1 -1
- package/src/mind/attention.ts +34 -12
- package/src/mind/index.ts +6 -0
- package/src/mind/learning.ts +46 -5
- package/src/mind/mind.ts +5 -1
- package/src/mind/pipeline.ts +79 -28
- package/src/mind/traverse.ts +30 -3
- package/src/mind/types.ts +11 -0
- package/test/54-evidence-k-instrumentation.test.mjs +175 -0
|
@@ -63,6 +63,11 @@ export interface ConsensusReachTrace {
|
|
|
63
63
|
contextsReached: number;
|
|
64
64
|
saturated: boolean;
|
|
65
65
|
saturation?: SaturationStop;
|
|
66
|
+
/** Nodes the climb processed — see {@link AncestorReach.visited}. Absent
|
|
67
|
+
* on payloads recorded before this field existed. */
|
|
68
|
+
visited?: number;
|
|
69
|
+
/** Maximum ascent distance — see {@link AncestorReach.maxDepth}. */
|
|
70
|
+
maxDepth?: number;
|
|
66
71
|
}
|
|
67
72
|
export type AnchorRejectionReason = "below-natural-break" | "below-consensus-floor" | "leading-saturation";
|
|
68
73
|
export interface ConsensusAnchorTrace {
|
|
@@ -358,6 +363,14 @@ export interface StructuralResonanceProposal {
|
|
|
358
363
|
leftSiblingId?: number;
|
|
359
364
|
rightSiblingId?: number;
|
|
360
365
|
}
|
|
366
|
+
/** A sibling gist cached in the shared, climb-wide memo alongside the
|
|
367
|
+
* COMPLETE stored byte length it was reconstructed from — the length is
|
|
368
|
+
* required to tell whether a cache hit is still valid under a probe whose
|
|
369
|
+
* `maxSiblingBytes` bound is smaller than the one that first cached it. */
|
|
370
|
+
interface CachedSiblingGist {
|
|
371
|
+
gist: Vec;
|
|
372
|
+
length: number;
|
|
373
|
+
}
|
|
361
374
|
/** Build, bound and order every mandatory structural variant (§7-8): the
|
|
362
375
|
* exact/exact composition is always kept; up to `ctx.cfg.haloQueryK`
|
|
363
376
|
* synonym variants (single- and double-synonym combined, one shared
|
|
@@ -366,7 +379,7 @@ export interface StructuralResonanceProposal {
|
|
|
366
379
|
* buildStructuralVariantSpecs}); a sibling's bytes are read and perceived
|
|
367
380
|
* only for specs actually retained, and at most once per sibling id per
|
|
368
381
|
* climb via `siblingGistMemo`. */
|
|
369
|
-
export declare function buildStructuralVariants(ctx: MindContext, ra: Region, rb: Region, sides: JunctionSynonymSides, siblingGistMemo: Map<number,
|
|
382
|
+
export declare function buildStructuralVariants(ctx: MindContext, ra: Region, rb: Region, sides: JunctionSynonymSides, siblingGistMemo: Map<number, CachedSiblingGist>): {
|
|
370
383
|
variants: StructuralVariant[];
|
|
371
384
|
exactLeft: StructuralPart;
|
|
372
385
|
exactRight: StructuralPart;
|
|
@@ -375,7 +388,7 @@ export declare function buildStructuralVariants(ctx: MindContext, ra: Region, rb
|
|
|
375
388
|
* ANN-query each, merge proposals by candidate id, and validate the winner
|
|
376
389
|
* through the SAME structural gates every other tier answers to (saturation,
|
|
377
390
|
* roots, IDF, contrastive margin). Returns null when nothing survives. */
|
|
378
|
-
export declare function structuralResonance(ctx: MindContext, query: Uint8Array, ra: Region, rb: Region, sides: JunctionSynonymSides, siblingGistMemo: Map<number,
|
|
391
|
+
export declare function structuralResonance(ctx: MindContext, query: Uint8Array, ra: Region, rb: Region, sides: JunctionSynonymSides, siblingGistMemo: Map<number, CachedSiblingGist>, k: number, N: number, reachMemo: Map<number, AncestorReach>,
|
|
379
392
|
/** Each side's OWN individual climb roots (from voteRegions), when it cast
|
|
380
393
|
* one — the self-evidence backstop structural-resonance needs and the
|
|
381
394
|
* exact tier gets for free from literal byte containment (§11's whole
|
|
@@ -38,6 +38,9 @@ function serialiseReaches(reachMemo) {
|
|
|
38
38
|
contextsReached: r.contextsReached,
|
|
39
39
|
saturated: r.saturated,
|
|
40
40
|
...(r.saturation ? { saturation: r.saturation } : {}),
|
|
41
|
+
...(r.visited !== undefined
|
|
42
|
+
? { visited: r.visited, maxDepth: r.maxDepth }
|
|
43
|
+
: {}),
|
|
41
44
|
});
|
|
42
45
|
}
|
|
43
46
|
return out;
|
|
@@ -962,19 +965,24 @@ function buildStructuralVariantSpecs(sides) {
|
|
|
962
965
|
/** A halo sibling's structural gist, bounded to `maxBytes` of stored content
|
|
963
966
|
* and reused across the whole climb. `positiveMemo` (shared across every
|
|
964
967
|
* probe in the climb, passed in by the caller) remembers only successfully
|
|
965
|
-
* reconstructed complete gists
|
|
966
|
-
* large for THIS pair's phrase-scale
|
|
967
|
-
* larger-spanning pair later, so a
|
|
968
|
-
*
|
|
969
|
-
*
|
|
968
|
+
* reconstructed complete gists together with their complete byte length —
|
|
969
|
+
* a sibling rejected here for being too large for THIS pair's phrase-scale
|
|
970
|
+
* bound may still be admissible for a larger-spanning pair later, so a
|
|
971
|
+
* rejection is never memoized globally, and a sibling cached by a LARGER
|
|
972
|
+
* probe is only reused here when its length still fits THIS probe's
|
|
973
|
+
* (possibly smaller) bound — eligibility must never depend on which probe
|
|
974
|
+
* happened to cache the sibling first. `localMemo` is scoped to one
|
|
975
|
+
* `buildStructuralVariants` call, where every variant shares the same
|
|
976
|
+
* bound, so a `null` there is safe to reuse. */
|
|
970
977
|
function loadBoundedSiblingGist(ctx, id, maxBytes, positiveMemo, localMemo) {
|
|
971
978
|
if (localMemo.has(id)) {
|
|
972
979
|
return localMemo.get(id) ?? null;
|
|
973
980
|
}
|
|
974
981
|
const cached = positiveMemo.get(id);
|
|
975
982
|
if (cached !== undefined) {
|
|
976
|
-
|
|
977
|
-
|
|
983
|
+
const result = cached.length <= maxBytes ? cached.gist : null;
|
|
984
|
+
localMemo.set(id, result);
|
|
985
|
+
return result;
|
|
978
986
|
}
|
|
979
987
|
const length = ctx.store.contentLen(id, maxBytes + 1);
|
|
980
988
|
if (length <= 0 || length > maxBytes) {
|
|
@@ -987,7 +995,7 @@ function loadBoundedSiblingGist(ctx, id, maxBytes, positiveMemo, localMemo) {
|
|
|
987
995
|
return null;
|
|
988
996
|
}
|
|
989
997
|
const gist = gistOf(ctx, bytes);
|
|
990
|
-
positiveMemo.set(id, gist);
|
|
998
|
+
positiveMemo.set(id, { gist, length });
|
|
991
999
|
localMemo.set(id, gist);
|
|
992
1000
|
return gist;
|
|
993
1001
|
}
|
package/dist/src/mind/index.d.ts
CHANGED
|
@@ -5,3 +5,5 @@ export type { MechanismResult, PipelineMechanism, Precomputed, } from "./pipelin
|
|
|
5
5
|
export type { InspectRationale, RationaleItem, RationaleStep, } from "./rationale.js";
|
|
6
6
|
export type { AnchorRejectionReason, ClimbConsensusData, ConsensusAnchorTrace, ConsensusReachTrace, ConsensusRegionTrace, CrossRegionTier, JunctionVoteTrace, RegionOutcome, } from "./attention.js";
|
|
7
7
|
export type { AncestorReach, AttentionRead, SaturationReason, SaturationStop, } from "./types.js";
|
|
8
|
+
export type { DepositReport } from "./learning.js";
|
|
9
|
+
export type { DecideGroundingData, NarrowDecisionData, Provenance, } from "./pipeline.js";
|
|
@@ -28,8 +28,30 @@ export declare function deposit(ctx: MindContext, input: Input, track: boolean,
|
|
|
28
28
|
export declare function ingestOne(ctx: MindContext, input: Input): Promise<Sema & {
|
|
29
29
|
id: number;
|
|
30
30
|
}>;
|
|
31
|
-
/**
|
|
32
|
-
|
|
31
|
+
/** What one ingested item deposited — reported through {@link ingest}'s
|
|
32
|
+
* optional `onDeposit` callback. Pure provenance read-out: node ids are
|
|
33
|
+
* content-addressed, so two byte-identical items report the SAME ids (that
|
|
34
|
+
* is content addressing working, not an error), and the callback observes
|
|
35
|
+
* the deposit without influencing it. */
|
|
36
|
+
export interface DepositReport {
|
|
37
|
+
/** Zero-based position of the item in the ingested input (0 for a scalar
|
|
38
|
+
* or single pair). */
|
|
39
|
+
index: number;
|
|
40
|
+
/** Whether the item was a bare experience or a (context, continuation)
|
|
41
|
+
* pair. */
|
|
42
|
+
kind: "one" | "pair";
|
|
43
|
+
/** Root node id of the item's (context) bytes. */
|
|
44
|
+
contextId: number;
|
|
45
|
+
/** Root node id of the continuation bytes — pairs only. */
|
|
46
|
+
continuationId?: number;
|
|
47
|
+
}
|
|
48
|
+
/** Ingest a pair (context, continuation) — learn an edge and pour halos.
|
|
49
|
+
* Returns the deposited root ids (context, continuation) — a pure
|
|
50
|
+
* read-out; callers that ignore it behave exactly as before. */
|
|
51
|
+
export declare function ingestPair(ctx: MindContext, ctxInput: Input, cont: Input): Promise<{
|
|
52
|
+
ctxId: number;
|
|
53
|
+
contId: number;
|
|
54
|
+
}>;
|
|
33
55
|
/** Dispatch the public ingest input shapes onto one-input / pair handlers —
|
|
34
56
|
* THE one reading of ingest's polymorphic surface (scalar, (context,
|
|
35
57
|
* continuation) pair, or a list mixing bare inputs and pairs). Both ingest
|
|
@@ -41,7 +63,13 @@ export declare function dispatchIngest(input: Input | (Input | [Input, Input])[]
|
|
|
41
63
|
}>, onPair: (ctxInput: Input, cont: Input) => Promise<void>): Promise<(Sema & {
|
|
42
64
|
id: number;
|
|
43
65
|
}) | undefined>;
|
|
44
|
-
/** Ingest an input or array of inputs/pairs. The public ingest entry point.
|
|
45
|
-
|
|
66
|
+
/** Ingest an input or array of inputs/pairs. The public ingest entry point.
|
|
67
|
+
*
|
|
68
|
+
* `onDeposit`, when given, is invoked once per ingested item with the
|
|
69
|
+
* deposited root node ids ({@link DepositReport}) — item-level provenance
|
|
70
|
+
* for tooling that needs to know which stored node an ingested item became.
|
|
71
|
+
* Purely observational: the callback runs after the item's deposit
|
|
72
|
+
* completed and nothing reads its result. */
|
|
73
|
+
export declare function ingest(ctx: MindContext, input: Input | (Input | [Input, Input])[], second?: Input, onDeposit?: (report: DepositReport) => void): Promise<(Sema & {
|
|
46
74
|
id: number;
|
|
47
75
|
}) | undefined>;
|
|
@@ -188,7 +188,9 @@ async function propagateSuffixes(ctx, src, dst) {
|
|
|
188
188
|
await ctx.store.link(id, dst);
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
|
-
/** Ingest a pair (context, continuation) — learn an edge and pour halos.
|
|
191
|
+
/** Ingest a pair (context, continuation) — learn an edge and pour halos.
|
|
192
|
+
* Returns the deposited root ids (context, continuation) — a pure
|
|
193
|
+
* read-out; callers that ignore it behave exactly as before. */
|
|
192
194
|
export async function ingestPair(ctx, ctxInput, cont) {
|
|
193
195
|
const c = await deposit(ctx, ctxInput, true, true);
|
|
194
196
|
const cont_ = await deposit(ctx, cont, false);
|
|
@@ -213,6 +215,7 @@ export async function ingestPair(ctx, ctxInput, cont) {
|
|
|
213
215
|
await ctx.store.pourHalo(partId, contSeat);
|
|
214
216
|
await ctx.store.pourHalo(contId, bindSeat(ctx.space, companySignature(ctx.space, partId), 0));
|
|
215
217
|
}
|
|
218
|
+
return { ctxId, contId };
|
|
216
219
|
}
|
|
217
220
|
/** Dispatch the public ingest input shapes onto one-input / pair handlers —
|
|
218
221
|
* THE one reading of ingest's polymorphic surface (scalar, (context,
|
|
@@ -242,7 +245,26 @@ export async function dispatchIngest(input, second, onOne, onPair) {
|
|
|
242
245
|
await onPair(input, second);
|
|
243
246
|
return undefined;
|
|
244
247
|
}
|
|
245
|
-
/** Ingest an input or array of inputs/pairs. The public ingest entry point.
|
|
246
|
-
|
|
247
|
-
|
|
248
|
+
/** Ingest an input or array of inputs/pairs. The public ingest entry point.
|
|
249
|
+
*
|
|
250
|
+
* `onDeposit`, when given, is invoked once per ingested item with the
|
|
251
|
+
* deposited root node ids ({@link DepositReport}) — item-level provenance
|
|
252
|
+
* for tooling that needs to know which stored node an ingested item became.
|
|
253
|
+
* Purely observational: the callback runs after the item's deposit
|
|
254
|
+
* completed and nothing reads its result. */
|
|
255
|
+
export async function ingest(ctx, input, second, onDeposit) {
|
|
256
|
+
let index = 0;
|
|
257
|
+
return dispatchIngest(input, second, async (i) => {
|
|
258
|
+
const r = await ingestOne(ctx, i);
|
|
259
|
+
onDeposit?.({ index: index++, kind: "one", contextId: r.id });
|
|
260
|
+
return r;
|
|
261
|
+
}, async (a, b) => {
|
|
262
|
+
const { ctxId, contId } = await ingestPair(ctx, a, b);
|
|
263
|
+
onDeposit?.({
|
|
264
|
+
index: index++,
|
|
265
|
+
kind: "pair",
|
|
266
|
+
contextId: ctxId,
|
|
267
|
+
continuationId: contId,
|
|
268
|
+
});
|
|
269
|
+
});
|
|
248
270
|
}
|
package/dist/src/mind/mind.d.ts
CHANGED
|
@@ -235,7 +235,10 @@ export declare class Mind implements MindContext {
|
|
|
235
235
|
* read-out direction of the same operation, without recall's grounding
|
|
236
236
|
* ladder. If either side's acceptance rule changes, revisit the other. */
|
|
237
237
|
express(idOrV: number | Vec): Promise<Uint8Array>;
|
|
238
|
-
|
|
238
|
+
/** See {@link import("./learning.js").ingest} — `onDeposit`, when given,
|
|
239
|
+
* reports each ingested item's deposited root node ids
|
|
240
|
+
* ({@link DepositReport}); purely observational. */
|
|
241
|
+
ingest(input: Input | (Input | [Input, Input])[], second?: Input, onDeposit?: (report: import("./learning.js").DepositReport) => void): Promise<(Sema & {
|
|
239
242
|
id: number;
|
|
240
243
|
}) | undefined>;
|
|
241
244
|
private extensionHost;
|
package/dist/src/mind/mind.js
CHANGED
|
@@ -464,8 +464,11 @@ export class Mind {
|
|
|
464
464
|
return new Uint8Array(0);
|
|
465
465
|
}
|
|
466
466
|
// ── Learning ─────────────────────────────────────────────────────────────
|
|
467
|
-
|
|
468
|
-
|
|
467
|
+
/** See {@link import("./learning.js").ingest} — `onDeposit`, when given,
|
|
468
|
+
* reports each ingested item's deposited root node ids
|
|
469
|
+
* ({@link DepositReport}); purely observational. */
|
|
470
|
+
async ingest(input, second, onDeposit) {
|
|
471
|
+
return ingest(this, input, second, onDeposit);
|
|
469
472
|
}
|
|
470
473
|
// ── Extension Surface ────────────────────────────────────────────────────
|
|
471
474
|
extensionHost() {
|
|
@@ -8,6 +8,35 @@ export interface Thought {
|
|
|
8
8
|
bytes: Uint8Array;
|
|
9
9
|
provenance: Provenance;
|
|
10
10
|
}
|
|
11
|
+
/** Structured payload of the "decideGrounding" rationale step — the same
|
|
12
|
+
* numbers the human-readable candidate labels already carry, exposed as
|
|
13
|
+
* data so a downstream tool need not parse free text. Purely additive
|
|
14
|
+
* instrumentation: built only under `ctx.trace?.` (optional chaining
|
|
15
|
+
* short-circuits its arguments), never read by inference. */
|
|
16
|
+
export interface DecideGroundingData {
|
|
17
|
+
version: 1;
|
|
18
|
+
/** Every grounding candidate weighed, in consideration order. */
|
|
19
|
+
candidates: Array<{
|
|
20
|
+
provenance: string;
|
|
21
|
+
/** The candidate's exact weight in the one cost ladder. */
|
|
22
|
+
weight: number;
|
|
23
|
+
/** The DISCRETE grade the decision actually compares (floor(weight/STEP)). */
|
|
24
|
+
grade: number;
|
|
25
|
+
/** Query bytes the candidate's accounted spans leave unexplained. */
|
|
26
|
+
unexplainedBytes: number;
|
|
27
|
+
/** Whether this candidate won the decision. */
|
|
28
|
+
decided: boolean;
|
|
29
|
+
}>;
|
|
30
|
+
/** Grade margin between the winner and the runner-up, when both exist —
|
|
31
|
+
* the same quantity the "narrowDecision" step reports as narrow when
|
|
32
|
+
* ≤ 1. Absent for a single-candidate decision. */
|
|
33
|
+
runnerUpMargin?: number;
|
|
34
|
+
}
|
|
35
|
+
/** Structured payload of the "narrowDecision" rationale step. */
|
|
36
|
+
export interface NarrowDecisionData {
|
|
37
|
+
version: 1;
|
|
38
|
+
margin: number;
|
|
39
|
+
}
|
|
11
40
|
/** Think: a single lightest-derivation exploration of the Sema graph.
|
|
12
41
|
*
|
|
13
42
|
* Every answer travels the same path:
|
|
@@ -132,9 +132,12 @@ export async function think(ctx, query, mechs) {
|
|
|
132
132
|
// initial null, so the read-back needs the assertion.)
|
|
133
133
|
const decided = best;
|
|
134
134
|
if (candidates.length > 1) {
|
|
135
|
-
|
|
135
|
+
// The runner-up is computed BEFORE the decideGrounding step so its grade
|
|
136
|
+
// margin can ride along in the step's structured data payload; the
|
|
137
|
+
// computation itself is pure and was always unconditional — only its
|
|
138
|
+
// position moved.
|
|
139
|
+
let runnerUp = null;
|
|
136
140
|
if (decided !== null) {
|
|
137
|
-
let runnerUp = null;
|
|
138
141
|
for (const c of candidates) {
|
|
139
142
|
if (c === decided)
|
|
140
143
|
continue;
|
|
@@ -142,15 +145,28 @@ export async function think(ctx, query, mechs) {
|
|
|
142
145
|
runnerUp = c;
|
|
143
146
|
}
|
|
144
147
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
148
|
+
}
|
|
149
|
+
const margin = decided !== null && runnerUp !== null
|
|
150
|
+
? grade(runnerUp.weight) - grade(decided.weight)
|
|
151
|
+
: null;
|
|
152
|
+
ctx.trace?.step("decideGrounding", candidates.map((c) => rItem(c.bytes, `${c.provenance} (weight ${c.weight.toFixed(3)}${c.unexplained ? `, unexplained: "${c.unexplained}"` : ""})`)), decided ? [rItem(decided.bytes, decided.provenance)] : [], "the lightest grounding derivation wins — every mechanism weighed in the one cost ladder", undefined, {
|
|
153
|
+
version: 1,
|
|
154
|
+
candidates: candidates.map((c) => ({
|
|
155
|
+
provenance: c.provenance,
|
|
156
|
+
weight: c.weight,
|
|
157
|
+
grade: grade(c.weight),
|
|
158
|
+
unexplainedBytes: unaccounted(c.accounted),
|
|
159
|
+
decided: c === decided,
|
|
160
|
+
})),
|
|
161
|
+
...(margin !== null ? { runnerUpMargin: margin } : {}),
|
|
162
|
+
});
|
|
163
|
+
if (decided !== null && runnerUp !== null && margin !== null) {
|
|
164
|
+
if (margin <= 1) {
|
|
165
|
+
ctx.trace?.step("narrowDecision", [
|
|
166
|
+
rItem(decided.bytes, `${decided.provenance} (weight ${decided.weight.toFixed(3)})`),
|
|
167
|
+
], [
|
|
168
|
+
rItem(runnerUp.bytes, `${runnerUp.provenance} (weight ${runnerUp.weight.toFixed(3)})`),
|
|
169
|
+
], `margin ${margin} grade-unit(s) — the decision could change with one more training fact`, undefined, { version: 1, margin });
|
|
154
170
|
}
|
|
155
171
|
}
|
|
156
172
|
}
|
|
@@ -102,6 +102,8 @@ export function edgeAncestors(ctx, id, contextCount, memo) {
|
|
|
102
102
|
observed: atomReach(ctx, contextCount),
|
|
103
103
|
limit: bound0,
|
|
104
104
|
},
|
|
105
|
+
visited: 0,
|
|
106
|
+
maxDepth: 0,
|
|
105
107
|
}
|
|
106
108
|
: {}),
|
|
107
109
|
};
|
|
@@ -151,7 +153,20 @@ export function edgeAncestors(ctx, id, contextCount, memo) {
|
|
|
151
153
|
// ~20K distinct nodes per climb family, >95% unique — not memoisable)
|
|
152
154
|
// while the context account never decided.
|
|
153
155
|
let lateral = 0;
|
|
156
|
+
// CLIMB READ-OUT (pure instrumentation, same contract as satStop): the
|
|
157
|
+
// parallel `depths` stack mirrors every push/pop of `stack`, so a node's
|
|
158
|
+
// ascent distance is known at its pop. Allocated only when a trace is
|
|
159
|
+
// requested; the climb itself never reads any of these back.
|
|
160
|
+
const depths = ctx.trace ? [] : null;
|
|
161
|
+
let curDepth = 0;
|
|
162
|
+
let visitedCount = 0;
|
|
163
|
+
let maxDepth = 0;
|
|
154
164
|
const visit = (x) => {
|
|
165
|
+
if (depths) {
|
|
166
|
+
visitedCount++;
|
|
167
|
+
if (curDepth > maxDepth)
|
|
168
|
+
maxDepth = curDepth;
|
|
169
|
+
}
|
|
155
170
|
const hasNx = cachedHasNext(ctx, x, structCache);
|
|
156
171
|
const pc = cachedPrevCount(ctx, x, structCache);
|
|
157
172
|
if (hasNx || pc > 0) {
|
|
@@ -203,6 +218,7 @@ export function edgeAncestors(ctx, id, contextCount, memo) {
|
|
|
203
218
|
if (!seen.has(p)) {
|
|
204
219
|
seen.add(p);
|
|
205
220
|
stack.push(p);
|
|
221
|
+
depths?.push(curDepth + 1);
|
|
206
222
|
fresh++;
|
|
207
223
|
}
|
|
208
224
|
}
|
|
@@ -225,8 +241,10 @@ export function edgeAncestors(ctx, id, contextCount, memo) {
|
|
|
225
241
|
};
|
|
226
242
|
const stack = [];
|
|
227
243
|
const containment = !cachedHasParents(ctx, id, structCache);
|
|
228
|
-
if (!containment)
|
|
244
|
+
if (!containment) {
|
|
229
245
|
stack.push(id);
|
|
246
|
+
depths?.push(0);
|
|
247
|
+
}
|
|
230
248
|
// The containment seed is STREAMED in pages of √N: a distinctive window's
|
|
231
249
|
// containers (which converge on one or two contexts, however many chunks
|
|
232
250
|
// of one deposit repeat it) are walked IN FULL — exact — while a common
|
|
@@ -247,17 +265,22 @@ export function edgeAncestors(ctx, id, contextCount, memo) {
|
|
|
247
265
|
if (!seen.has(c)) {
|
|
248
266
|
seen.add(c);
|
|
249
267
|
stack.push(c);
|
|
268
|
+
depths?.push(1);
|
|
250
269
|
}
|
|
251
270
|
}
|
|
252
271
|
if (stack.length === 0) {
|
|
253
|
-
if (containerOff === 0)
|
|
272
|
+
if (containerOff === 0) {
|
|
254
273
|
stack.push(id); // no containers at all
|
|
274
|
+
depths?.push(0);
|
|
275
|
+
}
|
|
255
276
|
else
|
|
256
277
|
break;
|
|
257
278
|
}
|
|
258
279
|
}
|
|
259
280
|
while (stack.length > 0) {
|
|
260
281
|
let x = stack.pop();
|
|
282
|
+
if (depths)
|
|
283
|
+
curDepth = depths.pop();
|
|
261
284
|
// TRANSPARENT-CHAIN HOP: a node with no edges in or out and exactly one
|
|
262
285
|
// parent contributes nothing here — no root, no context, no lateral
|
|
263
286
|
// entry — so the run to its first non-transparent ancestor is skipped
|
|
@@ -277,6 +300,10 @@ export function edgeAncestors(ctx, id, contextCount, memo) {
|
|
|
277
300
|
if (dup)
|
|
278
301
|
continue;
|
|
279
302
|
x = top;
|
|
303
|
+
// The chain's interior hops are part of the terminal's ascent
|
|
304
|
+
// distance — count them exactly as a node-at-a-time ascent would.
|
|
305
|
+
if (depths)
|
|
306
|
+
curDepth += run.length - 1;
|
|
280
307
|
}
|
|
281
308
|
if (!visit(x)) {
|
|
282
309
|
saturated = true;
|
|
@@ -289,6 +316,7 @@ export function edgeAncestors(ctx, id, contextCount, memo) {
|
|
|
289
316
|
contextsReached: ctxSeen.size,
|
|
290
317
|
saturated,
|
|
291
318
|
...(saturated && satStop ? { saturation: satStop } : {}),
|
|
319
|
+
...(depths ? { visited: visitedCount, maxDepth } : {}),
|
|
292
320
|
};
|
|
293
321
|
memo?.set(id, reach);
|
|
294
322
|
return reach;
|
package/dist/src/mind/types.d.ts
CHANGED
|
@@ -156,6 +156,17 @@ export interface AncestorReach {
|
|
|
156
156
|
* a non-saturated reach, and absent (even when saturated) when no trace
|
|
157
157
|
* was requested — instrumentation must not allocate when tracing is off. */
|
|
158
158
|
saturation?: SaturationStop;
|
|
159
|
+
/** The number of nodes the climb actually PROCESSED (popped and examined
|
|
160
|
+
* by its visit step; a transparent chain counts as its one terminal).
|
|
161
|
+
* Present only when a trace was requested — same contract as
|
|
162
|
+
* {@link saturation}: instrumentation must not allocate when tracing is
|
|
163
|
+
* off. Purely a read-out; the climb never consults it. */
|
|
164
|
+
visited?: number;
|
|
165
|
+
/** The maximum structural ascent distance (in parent/containment hops,
|
|
166
|
+
* transparent-chain interiors counted) from the start node among the
|
|
167
|
+
* processed nodes. Present only when a trace was requested — see
|
|
168
|
+
* {@link visited}. */
|
|
169
|
+
maxDepth?: number;
|
|
159
170
|
}
|
|
160
171
|
/** Saturated-interval information for the noise-drop gate. */
|
|
161
172
|
export interface SaturationInfo {
|
package/package.json
CHANGED
package/src/mind/attention.ts
CHANGED
|
@@ -149,6 +149,11 @@ export interface ConsensusReachTrace {
|
|
|
149
149
|
contextsReached: number;
|
|
150
150
|
saturated: boolean;
|
|
151
151
|
saturation?: SaturationStop;
|
|
152
|
+
/** Nodes the climb processed — see {@link AncestorReach.visited}. Absent
|
|
153
|
+
* on payloads recorded before this field existed. */
|
|
154
|
+
visited?: number;
|
|
155
|
+
/** Maximum ascent distance — see {@link AncestorReach.maxDepth}. */
|
|
156
|
+
maxDepth?: number;
|
|
152
157
|
}
|
|
153
158
|
|
|
154
159
|
export type AnchorRejectionReason =
|
|
@@ -419,6 +424,9 @@ function serialiseReaches(
|
|
|
419
424
|
contextsReached: r.contextsReached,
|
|
420
425
|
saturated: r.saturated,
|
|
421
426
|
...(r.saturation ? { saturation: r.saturation } : {}),
|
|
427
|
+
...(r.visited !== undefined
|
|
428
|
+
? { visited: r.visited, maxDepth: r.maxDepth }
|
|
429
|
+
: {}),
|
|
422
430
|
});
|
|
423
431
|
}
|
|
424
432
|
return out;
|
|
@@ -1613,19 +1621,32 @@ function buildStructuralVariantSpecs(
|
|
|
1613
1621
|
return specs;
|
|
1614
1622
|
}
|
|
1615
1623
|
|
|
1624
|
+
/** A sibling gist cached in the shared, climb-wide memo alongside the
|
|
1625
|
+
* COMPLETE stored byte length it was reconstructed from — the length is
|
|
1626
|
+
* required to tell whether a cache hit is still valid under a probe whose
|
|
1627
|
+
* `maxSiblingBytes` bound is smaller than the one that first cached it. */
|
|
1628
|
+
interface CachedSiblingGist {
|
|
1629
|
+
gist: Vec;
|
|
1630
|
+
length: number;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1616
1633
|
/** A halo sibling's structural gist, bounded to `maxBytes` of stored content
|
|
1617
1634
|
* and reused across the whole climb. `positiveMemo` (shared across every
|
|
1618
1635
|
* probe in the climb, passed in by the caller) remembers only successfully
|
|
1619
|
-
* reconstructed complete gists
|
|
1620
|
-
* large for THIS pair's phrase-scale
|
|
1621
|
-
* larger-spanning pair later, so a
|
|
1622
|
-
*
|
|
1623
|
-
*
|
|
1636
|
+
* reconstructed complete gists together with their complete byte length —
|
|
1637
|
+
* a sibling rejected here for being too large for THIS pair's phrase-scale
|
|
1638
|
+
* bound may still be admissible for a larger-spanning pair later, so a
|
|
1639
|
+
* rejection is never memoized globally, and a sibling cached by a LARGER
|
|
1640
|
+
* probe is only reused here when its length still fits THIS probe's
|
|
1641
|
+
* (possibly smaller) bound — eligibility must never depend on which probe
|
|
1642
|
+
* happened to cache the sibling first. `localMemo` is scoped to one
|
|
1643
|
+
* `buildStructuralVariants` call, where every variant shares the same
|
|
1644
|
+
* bound, so a `null` there is safe to reuse. */
|
|
1624
1645
|
function loadBoundedSiblingGist(
|
|
1625
1646
|
ctx: MindContext,
|
|
1626
1647
|
id: number,
|
|
1627
1648
|
maxBytes: number,
|
|
1628
|
-
positiveMemo: Map<number,
|
|
1649
|
+
positiveMemo: Map<number, CachedSiblingGist>,
|
|
1629
1650
|
localMemo: Map<number, Vec | null>,
|
|
1630
1651
|
): Vec | null {
|
|
1631
1652
|
if (localMemo.has(id)) {
|
|
@@ -1634,8 +1655,9 @@ function loadBoundedSiblingGist(
|
|
|
1634
1655
|
|
|
1635
1656
|
const cached = positiveMemo.get(id);
|
|
1636
1657
|
if (cached !== undefined) {
|
|
1637
|
-
|
|
1638
|
-
|
|
1658
|
+
const result = cached.length <= maxBytes ? cached.gist : null;
|
|
1659
|
+
localMemo.set(id, result);
|
|
1660
|
+
return result;
|
|
1639
1661
|
}
|
|
1640
1662
|
|
|
1641
1663
|
const length = ctx.store.contentLen(id, maxBytes + 1);
|
|
@@ -1651,7 +1673,7 @@ function loadBoundedSiblingGist(
|
|
|
1651
1673
|
}
|
|
1652
1674
|
|
|
1653
1675
|
const gist = gistOf(ctx, bytes);
|
|
1654
|
-
positiveMemo.set(id, gist);
|
|
1676
|
+
positiveMemo.set(id, { gist, length });
|
|
1655
1677
|
localMemo.set(id, gist);
|
|
1656
1678
|
return gist;
|
|
1657
1679
|
}
|
|
@@ -1669,7 +1691,7 @@ export function buildStructuralVariants(
|
|
|
1669
1691
|
ra: Region,
|
|
1670
1692
|
rb: Region,
|
|
1671
1693
|
sides: JunctionSynonymSides,
|
|
1672
|
-
siblingGistMemo: Map<number,
|
|
1694
|
+
siblingGistMemo: Map<number, CachedSiblingGist>,
|
|
1673
1695
|
): {
|
|
1674
1696
|
variants: StructuralVariant[];
|
|
1675
1697
|
exactLeft: StructuralPart;
|
|
@@ -1778,7 +1800,7 @@ export async function structuralResonance(
|
|
|
1778
1800
|
ra: Region,
|
|
1779
1801
|
rb: Region,
|
|
1780
1802
|
sides: JunctionSynonymSides,
|
|
1781
|
-
siblingGistMemo: Map<number,
|
|
1803
|
+
siblingGistMemo: Map<number, CachedSiblingGist>,
|
|
1782
1804
|
k: number,
|
|
1783
1805
|
N: number,
|
|
1784
1806
|
reachMemo: Map<number, AncestorReach>,
|
|
@@ -1977,7 +1999,7 @@ async function crossRegionVotes(
|
|
|
1977
1999
|
// Shared across every cross-region probe in this climb: a sibling
|
|
1978
2000
|
// successfully reconstructed while probing one pair must not be read and
|
|
1979
2001
|
// perceived again while probing another pair in the same climb.
|
|
1980
|
-
const siblingGistMemo = new Map<number,
|
|
2002
|
+
const siblingGistMemo = new Map<number, CachedSiblingGist>();
|
|
1981
2003
|
const votedSpans = new Set<string>();
|
|
1982
2004
|
for (const rv of rvs.votes) votedSpans.add(`${rv.start},${rv.end}`);
|
|
1983
2005
|
const seen = new Set<string>();
|
package/src/mind/index.ts
CHANGED
package/src/mind/learning.ts
CHANGED
|
@@ -215,12 +215,32 @@ async function propagateSuffixes(
|
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
/**
|
|
218
|
+
/** What one ingested item deposited — reported through {@link ingest}'s
|
|
219
|
+
* optional `onDeposit` callback. Pure provenance read-out: node ids are
|
|
220
|
+
* content-addressed, so two byte-identical items report the SAME ids (that
|
|
221
|
+
* is content addressing working, not an error), and the callback observes
|
|
222
|
+
* the deposit without influencing it. */
|
|
223
|
+
export interface DepositReport {
|
|
224
|
+
/** Zero-based position of the item in the ingested input (0 for a scalar
|
|
225
|
+
* or single pair). */
|
|
226
|
+
index: number;
|
|
227
|
+
/** Whether the item was a bare experience or a (context, continuation)
|
|
228
|
+
* pair. */
|
|
229
|
+
kind: "one" | "pair";
|
|
230
|
+
/** Root node id of the item's (context) bytes. */
|
|
231
|
+
contextId: number;
|
|
232
|
+
/** Root node id of the continuation bytes — pairs only. */
|
|
233
|
+
continuationId?: number;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Ingest a pair (context, continuation) — learn an edge and pour halos.
|
|
237
|
+
* Returns the deposited root ids (context, continuation) — a pure
|
|
238
|
+
* read-out; callers that ignore it behave exactly as before. */
|
|
219
239
|
export async function ingestPair(
|
|
220
240
|
ctx: MindContext,
|
|
221
241
|
ctxInput: Input,
|
|
222
242
|
cont: Input,
|
|
223
|
-
): Promise<
|
|
243
|
+
): Promise<{ ctxId: number; contId: number }> {
|
|
224
244
|
const c = await deposit(ctx, ctxInput, true, true);
|
|
225
245
|
const cont_ = await deposit(ctx, cont, false);
|
|
226
246
|
const ctxId = c.rootId, contId = cont_.rootId;
|
|
@@ -249,6 +269,7 @@ export async function ingestPair(
|
|
|
249
269
|
bindSeat(ctx.space, companySignature(ctx.space, partId), 0),
|
|
250
270
|
);
|
|
251
271
|
}
|
|
272
|
+
return { ctxId, contId };
|
|
252
273
|
}
|
|
253
274
|
|
|
254
275
|
/** Dispatch the public ingest input shapes onto one-input / pair handlers —
|
|
@@ -286,16 +307,36 @@ export async function dispatchIngest(
|
|
|
286
307
|
return undefined;
|
|
287
308
|
}
|
|
288
309
|
|
|
289
|
-
/** Ingest an input or array of inputs/pairs. The public ingest entry point.
|
|
310
|
+
/** Ingest an input or array of inputs/pairs. The public ingest entry point.
|
|
311
|
+
*
|
|
312
|
+
* `onDeposit`, when given, is invoked once per ingested item with the
|
|
313
|
+
* deposited root node ids ({@link DepositReport}) — item-level provenance
|
|
314
|
+
* for tooling that needs to know which stored node an ingested item became.
|
|
315
|
+
* Purely observational: the callback runs after the item's deposit
|
|
316
|
+
* completed and nothing reads its result. */
|
|
290
317
|
export async function ingest(
|
|
291
318
|
ctx: MindContext,
|
|
292
319
|
input: Input | (Input | [Input, Input])[],
|
|
293
320
|
second?: Input,
|
|
321
|
+
onDeposit?: (report: DepositReport) => void,
|
|
294
322
|
): Promise<(Sema & { id: number }) | undefined> {
|
|
323
|
+
let index = 0;
|
|
295
324
|
return dispatchIngest(
|
|
296
325
|
input,
|
|
297
326
|
second,
|
|
298
|
-
(i) =>
|
|
299
|
-
|
|
327
|
+
async (i) => {
|
|
328
|
+
const r = await ingestOne(ctx, i);
|
|
329
|
+
onDeposit?.({ index: index++, kind: "one", contextId: r.id });
|
|
330
|
+
return r;
|
|
331
|
+
},
|
|
332
|
+
async (a, b) => {
|
|
333
|
+
const { ctxId, contId } = await ingestPair(ctx, a, b);
|
|
334
|
+
onDeposit?.({
|
|
335
|
+
index: index++,
|
|
336
|
+
kind: "pair",
|
|
337
|
+
contextId: ctxId,
|
|
338
|
+
continuationId: contId,
|
|
339
|
+
});
|
|
340
|
+
},
|
|
300
341
|
);
|
|
301
342
|
}
|
package/src/mind/mind.ts
CHANGED
|
@@ -767,11 +767,15 @@ export class Mind implements MindContext {
|
|
|
767
767
|
|
|
768
768
|
// ── Learning ─────────────────────────────────────────────────────────────
|
|
769
769
|
|
|
770
|
+
/** See {@link import("./learning.js").ingest} — `onDeposit`, when given,
|
|
771
|
+
* reports each ingested item's deposited root node ids
|
|
772
|
+
* ({@link DepositReport}); purely observational. */
|
|
770
773
|
async ingest(
|
|
771
774
|
input: Input | (Input | [Input, Input])[],
|
|
772
775
|
second?: Input,
|
|
776
|
+
onDeposit?: (report: import("./learning.js").DepositReport) => void,
|
|
773
777
|
): Promise<(Sema & { id: number }) | undefined> {
|
|
774
|
-
return ingest(this, input, second);
|
|
778
|
+
return ingest(this, input, second, onDeposit);
|
|
775
779
|
}
|
|
776
780
|
|
|
777
781
|
// ── Extension Surface ────────────────────────────────────────────────────
|
package/src/mind/pipeline.ts
CHANGED
|
@@ -77,6 +77,37 @@ export interface Thought {
|
|
|
77
77
|
provenance: Provenance;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/** Structured payload of the "decideGrounding" rationale step — the same
|
|
81
|
+
* numbers the human-readable candidate labels already carry, exposed as
|
|
82
|
+
* data so a downstream tool need not parse free text. Purely additive
|
|
83
|
+
* instrumentation: built only under `ctx.trace?.` (optional chaining
|
|
84
|
+
* short-circuits its arguments), never read by inference. */
|
|
85
|
+
export interface DecideGroundingData {
|
|
86
|
+
version: 1;
|
|
87
|
+
/** Every grounding candidate weighed, in consideration order. */
|
|
88
|
+
candidates: Array<{
|
|
89
|
+
provenance: string;
|
|
90
|
+
/** The candidate's exact weight in the one cost ladder. */
|
|
91
|
+
weight: number;
|
|
92
|
+
/** The DISCRETE grade the decision actually compares (floor(weight/STEP)). */
|
|
93
|
+
grade: number;
|
|
94
|
+
/** Query bytes the candidate's accounted spans leave unexplained. */
|
|
95
|
+
unexplainedBytes: number;
|
|
96
|
+
/** Whether this candidate won the decision. */
|
|
97
|
+
decided: boolean;
|
|
98
|
+
}>;
|
|
99
|
+
/** Grade margin between the winner and the runner-up, when both exist —
|
|
100
|
+
* the same quantity the "narrowDecision" step reports as narrow when
|
|
101
|
+
* ≤ 1. Absent for a single-candidate decision. */
|
|
102
|
+
runnerUpMargin?: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Structured payload of the "narrowDecision" rationale step. */
|
|
106
|
+
export interface NarrowDecisionData {
|
|
107
|
+
version: 1;
|
|
108
|
+
margin: number;
|
|
109
|
+
}
|
|
110
|
+
|
|
80
111
|
/** Think: a single lightest-derivation exploration of the Sema graph.
|
|
81
112
|
*
|
|
82
113
|
* Every answer travels the same path:
|
|
@@ -211,6 +242,22 @@ export async function think(
|
|
|
211
242
|
// initial null, so the read-back needs the assertion.)
|
|
212
243
|
const decided = best as Candidate | null;
|
|
213
244
|
if (candidates.length > 1) {
|
|
245
|
+
// The runner-up is computed BEFORE the decideGrounding step so its grade
|
|
246
|
+
// margin can ride along in the step's structured data payload; the
|
|
247
|
+
// computation itself is pure and was always unconditional — only its
|
|
248
|
+
// position moved.
|
|
249
|
+
let runnerUp: Candidate | null = null;
|
|
250
|
+
if (decided !== null) {
|
|
251
|
+
for (const c of candidates) {
|
|
252
|
+
if (c === decided) continue;
|
|
253
|
+
if (runnerUp === null || grade(c.weight) < grade(runnerUp.weight)) {
|
|
254
|
+
runnerUp = c;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const margin = decided !== null && runnerUp !== null
|
|
259
|
+
? grade(runnerUp.weight) - grade(decided.weight)
|
|
260
|
+
: null;
|
|
214
261
|
ctx.trace?.step(
|
|
215
262
|
"decideGrounding",
|
|
216
263
|
candidates.map((c) =>
|
|
@@ -223,35 +270,39 @@ export async function think(
|
|
|
223
270
|
),
|
|
224
271
|
decided ? [rItem(decided.bytes, decided.provenance)] : [],
|
|
225
272
|
"the lightest grounding derivation wins — every mechanism weighed in the one cost ladder",
|
|
273
|
+
undefined,
|
|
274
|
+
{
|
|
275
|
+
version: 1,
|
|
276
|
+
candidates: candidates.map((c) => ({
|
|
277
|
+
provenance: c.provenance,
|
|
278
|
+
weight: c.weight,
|
|
279
|
+
grade: grade(c.weight),
|
|
280
|
+
unexplainedBytes: unaccounted(c.accounted),
|
|
281
|
+
decided: c === decided,
|
|
282
|
+
})),
|
|
283
|
+
...(margin !== null ? { runnerUpMargin: margin } : {}),
|
|
284
|
+
} satisfies DecideGroundingData,
|
|
226
285
|
);
|
|
227
|
-
if (decided !== null) {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
rItem(
|
|
248
|
-
runnerUp.bytes,
|
|
249
|
-
`${runnerUp.provenance} (weight ${runnerUp.weight.toFixed(3)})`,
|
|
250
|
-
),
|
|
251
|
-
],
|
|
252
|
-
`margin ${margin} grade-unit(s) — the decision could change with one more training fact`,
|
|
253
|
-
);
|
|
254
|
-
}
|
|
286
|
+
if (decided !== null && runnerUp !== null && margin !== null) {
|
|
287
|
+
if (margin <= 1) {
|
|
288
|
+
ctx.trace?.step(
|
|
289
|
+
"narrowDecision",
|
|
290
|
+
[
|
|
291
|
+
rItem(
|
|
292
|
+
decided.bytes,
|
|
293
|
+
`${decided.provenance} (weight ${decided.weight.toFixed(3)})`,
|
|
294
|
+
),
|
|
295
|
+
],
|
|
296
|
+
[
|
|
297
|
+
rItem(
|
|
298
|
+
runnerUp.bytes,
|
|
299
|
+
`${runnerUp.provenance} (weight ${runnerUp.weight.toFixed(3)})`,
|
|
300
|
+
),
|
|
301
|
+
],
|
|
302
|
+
`margin ${margin} grade-unit(s) — the decision could change with one more training fact`,
|
|
303
|
+
undefined,
|
|
304
|
+
{ version: 1, margin } satisfies NarrowDecisionData,
|
|
305
|
+
);
|
|
255
306
|
}
|
|
256
307
|
}
|
|
257
308
|
}
|
package/src/mind/traverse.ts
CHANGED
|
@@ -147,6 +147,8 @@ export function edgeAncestors(
|
|
|
147
147
|
observed: atomReach(ctx, contextCount),
|
|
148
148
|
limit: bound0,
|
|
149
149
|
},
|
|
150
|
+
visited: 0,
|
|
151
|
+
maxDepth: 0,
|
|
150
152
|
}
|
|
151
153
|
: {}),
|
|
152
154
|
};
|
|
@@ -201,7 +203,20 @@ export function edgeAncestors(
|
|
|
201
203
|
// while the context account never decided.
|
|
202
204
|
let lateral = 0;
|
|
203
205
|
|
|
206
|
+
// CLIMB READ-OUT (pure instrumentation, same contract as satStop): the
|
|
207
|
+
// parallel `depths` stack mirrors every push/pop of `stack`, so a node's
|
|
208
|
+
// ascent distance is known at its pop. Allocated only when a trace is
|
|
209
|
+
// requested; the climb itself never reads any of these back.
|
|
210
|
+
const depths: number[] | null = ctx.trace ? [] : null;
|
|
211
|
+
let curDepth = 0;
|
|
212
|
+
let visitedCount = 0;
|
|
213
|
+
let maxDepth = 0;
|
|
214
|
+
|
|
204
215
|
const visit = (x: number): boolean => {
|
|
216
|
+
if (depths) {
|
|
217
|
+
visitedCount++;
|
|
218
|
+
if (curDepth > maxDepth) maxDepth = curDepth;
|
|
219
|
+
}
|
|
205
220
|
const hasNx = cachedHasNext(ctx, x, structCache);
|
|
206
221
|
const pc = cachedPrevCount(ctx, x, structCache);
|
|
207
222
|
if (hasNx || pc > 0) {
|
|
@@ -251,6 +266,7 @@ export function edgeAncestors(
|
|
|
251
266
|
if (!seen.has(p)) {
|
|
252
267
|
seen.add(p);
|
|
253
268
|
stack.push(p);
|
|
269
|
+
depths?.push(curDepth + 1);
|
|
254
270
|
fresh++;
|
|
255
271
|
}
|
|
256
272
|
}
|
|
@@ -274,7 +290,10 @@ export function edgeAncestors(
|
|
|
274
290
|
|
|
275
291
|
const stack: number[] = [];
|
|
276
292
|
const containment = !cachedHasParents(ctx, id, structCache);
|
|
277
|
-
if (!containment)
|
|
293
|
+
if (!containment) {
|
|
294
|
+
stack.push(id);
|
|
295
|
+
depths?.push(0);
|
|
296
|
+
}
|
|
278
297
|
|
|
279
298
|
// The containment seed is STREAMED in pages of √N: a distinctive window's
|
|
280
299
|
// containers (which converge on one or two contexts, however many chunks
|
|
@@ -295,15 +314,19 @@ export function edgeAncestors(
|
|
|
295
314
|
if (!seen.has(c)) {
|
|
296
315
|
seen.add(c);
|
|
297
316
|
stack.push(c);
|
|
317
|
+
depths?.push(1);
|
|
298
318
|
}
|
|
299
319
|
}
|
|
300
320
|
if (stack.length === 0) {
|
|
301
|
-
if (containerOff === 0)
|
|
302
|
-
|
|
321
|
+
if (containerOff === 0) {
|
|
322
|
+
stack.push(id); // no containers at all
|
|
323
|
+
depths?.push(0);
|
|
324
|
+
} else break;
|
|
303
325
|
}
|
|
304
326
|
}
|
|
305
327
|
while (stack.length > 0) {
|
|
306
328
|
let x = stack.pop()!;
|
|
329
|
+
if (depths) curDepth = depths.pop()!;
|
|
307
330
|
// TRANSPARENT-CHAIN HOP: a node with no edges in or out and exactly one
|
|
308
331
|
// parent contributes nothing here — no root, no context, no lateral
|
|
309
332
|
// entry — so the run to its first non-transparent ancestor is skipped
|
|
@@ -321,6 +344,9 @@ export function edgeAncestors(
|
|
|
321
344
|
for (let i = 1; i < run.length; i++) seen.add(run[i]);
|
|
322
345
|
if (dup) continue;
|
|
323
346
|
x = top;
|
|
347
|
+
// The chain's interior hops are part of the terminal's ascent
|
|
348
|
+
// distance — count them exactly as a node-at-a-time ascent would.
|
|
349
|
+
if (depths) curDepth += run.length - 1;
|
|
324
350
|
}
|
|
325
351
|
if (!visit(x)) {
|
|
326
352
|
saturated = true;
|
|
@@ -334,6 +360,7 @@ export function edgeAncestors(
|
|
|
334
360
|
contextsReached: ctxSeen.size,
|
|
335
361
|
saturated,
|
|
336
362
|
...(saturated && satStop ? { saturation: satStop } : {}),
|
|
363
|
+
...(depths ? { visited: visitedCount, maxDepth } : {}),
|
|
337
364
|
};
|
|
338
365
|
memo?.set(id, reach);
|
|
339
366
|
return reach;
|
package/src/mind/types.ts
CHANGED
|
@@ -205,6 +205,17 @@ export interface AncestorReach {
|
|
|
205
205
|
* a non-saturated reach, and absent (even when saturated) when no trace
|
|
206
206
|
* was requested — instrumentation must not allocate when tracing is off. */
|
|
207
207
|
saturation?: SaturationStop;
|
|
208
|
+
/** The number of nodes the climb actually PROCESSED (popped and examined
|
|
209
|
+
* by its visit step; a transparent chain counts as its one terminal).
|
|
210
|
+
* Present only when a trace was requested — same contract as
|
|
211
|
+
* {@link saturation}: instrumentation must not allocate when tracing is
|
|
212
|
+
* off. Purely a read-out; the climb never consults it. */
|
|
213
|
+
visited?: number;
|
|
214
|
+
/** The maximum structural ascent distance (in parent/containment hops,
|
|
215
|
+
* transparent-chain interiors counted) from the start node among the
|
|
216
|
+
* processed nodes. Present only when a trace was requested — see
|
|
217
|
+
* {@link visited}. */
|
|
218
|
+
maxDepth?: number;
|
|
208
219
|
}
|
|
209
220
|
|
|
210
221
|
/** Saturated-interval information for the noise-drop gate. */
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// 54-evidence-k-instrumentation.test.mjs — the three purely-additive
|
|
2
|
+
// read-outs added for evidence-breadth measurement tooling:
|
|
3
|
+
// 1. AncestorReach.visited / maxDepth (trace-gated, serialised into
|
|
4
|
+
// ClimbConsensusData.reaches),
|
|
5
|
+
// 2. structured data payloads on "decideGrounding" / "narrowDecision",
|
|
6
|
+
// 3. ingest()'s optional onDeposit provenance callback.
|
|
7
|
+
//
|
|
8
|
+
// Every assertion here checks a READ-OUT; the final test pins that none of
|
|
9
|
+
// them changes the answer (the same invariant tests 52 §6 / 53 §9 pin for
|
|
10
|
+
// the climb instrumentation).
|
|
11
|
+
|
|
12
|
+
import { test } from "node:test";
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import { decodeText, Mind } from "../dist/src/index.js";
|
|
15
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
16
|
+
|
|
17
|
+
const mk = (seed = 1) =>
|
|
18
|
+
new Mind({ seed, store: new SQliteStore({ path: ":memory:" }) });
|
|
19
|
+
|
|
20
|
+
const CORPUS = [
|
|
21
|
+
["red", "is a color"],
|
|
22
|
+
["blue", "is a color"],
|
|
23
|
+
["circle", "is a shape"],
|
|
24
|
+
["square", "is a shape"],
|
|
25
|
+
["red circle", "answer alpha"],
|
|
26
|
+
["red square", "answer beta"],
|
|
27
|
+
["blue circle", "answer gamma"],
|
|
28
|
+
["blue square", "answer delta"],
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
async function trace(mind, q) {
|
|
32
|
+
const steps = [];
|
|
33
|
+
const ans = await mind.respondText(q, (s) => steps.push(s));
|
|
34
|
+
return { steps, ans };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
38
|
+
// 1. climb read-out: visited / maxDepth
|
|
39
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
40
|
+
|
|
41
|
+
test("1. traced climb reaches carry visited/maxDepth; untraced ones do not", async () => {
|
|
42
|
+
const m = mk(1);
|
|
43
|
+
await m.ingest(CORPUS);
|
|
44
|
+
|
|
45
|
+
const { steps } = await trace(m, "red then circle");
|
|
46
|
+
const climb = steps.find((s) => s.mechanism.at(-1) === "climbConsensus");
|
|
47
|
+
assert.ok(climb?.data?.reaches?.length > 0, "expected reach traces");
|
|
48
|
+
for (const r of climb.data.reaches) {
|
|
49
|
+
assert.equal(typeof r.visited, "number");
|
|
50
|
+
assert.equal(typeof r.maxDepth, "number");
|
|
51
|
+
assert.ok(r.visited >= 0);
|
|
52
|
+
assert.ok(r.maxDepth >= 0);
|
|
53
|
+
// A climb that processed no node cannot have ascended. (One processed
|
|
54
|
+
// node can already sit at depth 1: containment seeds start one hop up.)
|
|
55
|
+
if (r.visited === 0) assert.equal(r.maxDepth, 0);
|
|
56
|
+
// Depth is bounded by the number of processed nodes plus any
|
|
57
|
+
// transparent-chain interiors — sanity only: never negative, and zero
|
|
58
|
+
// whenever nothing was climbed.
|
|
59
|
+
assert.ok(r.maxDepth >= 0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Untraced (no respond in flight): the same contract as `saturation` —
|
|
63
|
+
// instrumentation fields absent when no trace was requested.
|
|
64
|
+
const someNode = climb.data.reaches[0].node;
|
|
65
|
+
const reach = m.edgeAncestors(someNode, 8);
|
|
66
|
+
assert.equal(reach.visited, undefined);
|
|
67
|
+
assert.equal(reach.maxDepth, undefined);
|
|
68
|
+
await m.store.close();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
72
|
+
// 2. decideGrounding / narrowDecision data payloads
|
|
73
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
74
|
+
|
|
75
|
+
test("2. decideGrounding carries every candidate's weight/grade as data, exactly one decided", async () => {
|
|
76
|
+
const m = mk(1);
|
|
77
|
+
await m.ingest(CORPUS);
|
|
78
|
+
const { steps, ans } = await trace(m, "red then circle");
|
|
79
|
+
await m.store.close();
|
|
80
|
+
|
|
81
|
+
const dg = steps.find((s) => s.mechanism.at(-1) === "decideGrounding");
|
|
82
|
+
assert.ok(dg, "expected a decideGrounding step (≥2 candidates)");
|
|
83
|
+
assert.ok(dg.data, "decideGrounding must carry a data payload");
|
|
84
|
+
assert.equal(dg.data.version, 1);
|
|
85
|
+
assert.equal(dg.data.candidates.length, dg.inputs.length);
|
|
86
|
+
for (const c of dg.data.candidates) {
|
|
87
|
+
assert.equal(typeof c.provenance, "string");
|
|
88
|
+
assert.equal(typeof c.weight, "number");
|
|
89
|
+
assert.equal(typeof c.grade, "number");
|
|
90
|
+
assert.equal(typeof c.unexplainedBytes, "number");
|
|
91
|
+
assert.ok(c.unexplainedBytes >= 0);
|
|
92
|
+
}
|
|
93
|
+
const decided = dg.data.candidates.filter((c) => c.decided);
|
|
94
|
+
assert.equal(decided.length, 1, "exactly one candidate wins");
|
|
95
|
+
// The winner's grade is minimal — the data restates the decision rule.
|
|
96
|
+
const minGrade = Math.min(...dg.data.candidates.map((c) => c.grade));
|
|
97
|
+
assert.equal(decided[0].grade, minGrade);
|
|
98
|
+
assert.ok(ans.length > 0);
|
|
99
|
+
|
|
100
|
+
if (dg.data.runnerUpMargin !== undefined) {
|
|
101
|
+
assert.ok(dg.data.runnerUpMargin >= 0);
|
|
102
|
+
const nd = steps.find((s) => s.mechanism.at(-1) === "narrowDecision");
|
|
103
|
+
if (dg.data.runnerUpMargin <= 1) {
|
|
104
|
+
assert.ok(nd, "margin ≤ 1 must emit narrowDecision");
|
|
105
|
+
assert.equal(nd.data.version, 1);
|
|
106
|
+
assert.equal(nd.data.margin, dg.data.runnerUpMargin);
|
|
107
|
+
} else {
|
|
108
|
+
assert.equal(nd, undefined);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
114
|
+
// 3. onDeposit provenance callback
|
|
115
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
116
|
+
|
|
117
|
+
test("3. onDeposit reports one item-indexed record per ingested item, ids content-addressed", async () => {
|
|
118
|
+
const m = mk(1);
|
|
119
|
+
const reports = [];
|
|
120
|
+
await m.ingest(CORPUS, undefined, (r) => reports.push(r));
|
|
121
|
+
|
|
122
|
+
assert.equal(reports.length, CORPUS.length);
|
|
123
|
+
reports.forEach((r, i) => {
|
|
124
|
+
assert.equal(r.index, i);
|
|
125
|
+
assert.equal(r.kind, "pair");
|
|
126
|
+
assert.equal(typeof r.contextId, "number");
|
|
127
|
+
assert.equal(typeof r.continuationId, "number");
|
|
128
|
+
// The reported ids ARE the items' content-addressed nodes.
|
|
129
|
+
assert.equal(decodeText(m.store.bytes(r.contextId)), CORPUS[i][0]);
|
|
130
|
+
assert.equal(decodeText(m.store.bytes(r.continuationId)), CORPUS[i][1]);
|
|
131
|
+
});
|
|
132
|
+
// Content addressing: identical continuations share one node id.
|
|
133
|
+
const colorIds = [reports[0], reports[1]].map((r) => r.continuationId);
|
|
134
|
+
assert.equal(colorIds[0], colorIds[1]);
|
|
135
|
+
await m.store.close();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("3b. onDeposit reports bare items as kind 'one'; omitting it changes nothing", async () => {
|
|
139
|
+
const m = mk(1);
|
|
140
|
+
const reports = [];
|
|
141
|
+
await m.ingest(
|
|
142
|
+
["standalone note", ["ctx", "cont"]],
|
|
143
|
+
undefined,
|
|
144
|
+
(r) => reports.push(r),
|
|
145
|
+
);
|
|
146
|
+
assert.deepEqual(reports.map((r) => [r.index, r.kind]), [[0, "one"], [
|
|
147
|
+
1,
|
|
148
|
+
"pair",
|
|
149
|
+
]]);
|
|
150
|
+
assert.equal(reports[1].continuationId !== undefined, true);
|
|
151
|
+
assert.equal(reports[0].continuationId, undefined);
|
|
152
|
+
await m.store.close();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
156
|
+
// 4. none of the read-outs changes the answer
|
|
157
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
158
|
+
|
|
159
|
+
test("4. answers are identical with and without the new read-outs attached", async () => {
|
|
160
|
+
const ask = async (withHooks) => {
|
|
161
|
+
const m = mk(7);
|
|
162
|
+
await m.ingest(CORPUS, undefined, withHooks ? () => {} : undefined);
|
|
163
|
+
const out = [];
|
|
164
|
+
for (
|
|
165
|
+
const q of ["red then circle", "red circle blue square", "blue square"]
|
|
166
|
+
) {
|
|
167
|
+
out.push(
|
|
168
|
+
withHooks ? await m.respondText(q, () => {}) : await m.respondText(q),
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
await m.store.close();
|
|
172
|
+
return out;
|
|
173
|
+
};
|
|
174
|
+
assert.deepEqual(await ask(true), await ask(false));
|
|
175
|
+
});
|