@hviana/sema 0.3.0 → 0.4.1
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/AGENTS.md +71 -5
- package/CONTRIBUTING.md +92 -10
- package/LICENSE.md +2 -2
- package/dist/src/derive/src/deduction.d.ts +12 -1
- package/dist/src/derive/src/deduction.js +5 -1
- package/dist/src/derive/src/index.d.ts +1 -0
- package/dist/src/geometry.d.ts +3 -30
- package/dist/src/geometry.js +330 -82
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/meter.d.ts +171 -0
- package/dist/src/meter.js +269 -0
- package/dist/src/mind/attention.d.ts +0 -4
- package/dist/src/mind/attention.js +251 -23
- package/dist/src/mind/bridge.d.ts +10 -1
- package/dist/src/mind/bridge.js +179 -10
- package/dist/src/mind/canonical.d.ts +6 -1
- package/dist/src/mind/canonical.js +6 -1
- package/dist/src/mind/graph-search.d.ts +9 -0
- package/dist/src/mind/graph-search.js +59 -19
- package/dist/src/mind/junction.d.ts +10 -0
- package/dist/src/mind/junction.js +14 -0
- package/dist/src/mind/match.d.ts +40 -0
- package/dist/src/mind/match.js +125 -1
- package/dist/src/mind/mechanisms/cast.js +63 -6
- package/dist/src/mind/mechanisms/extraction.d.ts +0 -34
- package/dist/src/mind/mechanisms/extraction.js +1 -88
- package/dist/src/mind/mechanisms/recall.d.ts +3 -0
- package/dist/src/mind/mechanisms/recall.js +77 -14
- package/dist/src/mind/mind.d.ts +55 -4
- package/dist/src/mind/mind.js +110 -91
- package/dist/src/mind/pipeline-mechanism.d.ts +33 -3
- package/dist/src/mind/pipeline-mechanism.js +179 -10
- package/dist/src/mind/pipeline.js +52 -10
- package/dist/src/mind/primitives.d.ts +11 -15
- package/dist/src/mind/primitives.js +47 -28
- package/dist/src/mind/reasoning.d.ts +7 -1
- package/dist/src/mind/reasoning.js +40 -8
- package/dist/src/mind/recognition.js +93 -20
- package/dist/src/mind/traverse.d.ts +11 -0
- package/dist/src/mind/traverse.js +58 -5
- package/dist/src/mind/types.d.ts +28 -5
- package/dist/src/store.d.ts +15 -0
- package/dist/src/store.js +91 -6
- package/package.json +1 -1
- package/src/derive/src/deduction.ts +15 -0
- package/src/derive/src/index.ts +1 -0
- package/src/geometry.ts +350 -122
- package/src/index.ts +1 -0
- package/src/meter.ts +333 -0
- package/src/mind/attention.ts +268 -31
- package/src/mind/bridge.ts +187 -10
- package/src/mind/canonical.ts +6 -1
- package/src/mind/graph-search.ts +60 -21
- package/src/mind/junction.ts +12 -0
- package/src/mind/match.ts +146 -1
- package/src/mind/mechanisms/cast.ts +62 -6
- package/src/mind/mechanisms/extraction.ts +2 -103
- package/src/mind/mechanisms/recall.ts +84 -17
- package/src/mind/mind.ts +139 -98
- package/src/mind/pipeline-mechanism.ts +203 -13
- package/src/mind/pipeline.ts +65 -8
- package/src/mind/primitives.ts +49 -33
- package/src/mind/reasoning.ts +39 -7
- package/src/mind/recognition.ts +89 -19
- package/src/mind/traverse.ts +59 -5
- package/src/mind/types.ts +28 -5
- package/src/store.ts +75 -6
- package/test/14-scaling.test.mjs +17 -7
- package/test/31-audit.test.mjs +4 -1
- package/test/33-multi-candidate.test.mjs +13 -1
- package/test/36-already-answered-fusion.test.mjs +10 -3
- package/test/46-recognise-multibyte-edge.test.mjs +3 -3
- package/test/53-cross-region-probe-instrumentation.test.mjs +36 -6
- package/test/55-cost-meter.test.mjs +284 -0
- package/test/56-bridge-identity-admission.test.mjs +209 -0
- package/test/57-fusion-order.test.mjs +104 -0
- package/test/58-subquantum-sites.test.mjs +112 -0
- package/test/59-fold-invariance.test.mjs +226 -0
|
@@ -8,6 +8,49 @@
|
|
|
8
8
|
import { cosine } from "../vec.js";
|
|
9
9
|
import { gistOf, read } from "./primitives.js";
|
|
10
10
|
const structCaches = new WeakMap();
|
|
11
|
+
// ── The shared ancestor-reach memo ──────────────────────────────────────
|
|
12
|
+
//
|
|
13
|
+
// `edgeAncestors` is a pure function of (node, N) over a read-only store —
|
|
14
|
+
// asking never writes — so its result is reusable for as long as the store
|
|
15
|
+
// is not written to. There used to be TWO memos and they never met: the
|
|
16
|
+
// climb built a private one per call (computeAttention), while
|
|
17
|
+
// `Precomputed.reachMemo` — documented as "one response-scoped memo serves
|
|
18
|
+
// every mechanism that prices commonality" — was reached only by confluence.
|
|
19
|
+
// The climb is by far the biggest consumer.
|
|
20
|
+
//
|
|
21
|
+
// Keyed off `ctx.climbMemo`'s OBJECT IDENTITY, exactly like the struct cache
|
|
22
|
+
// above, which buys the right lifetime for free: a plain respond() has a
|
|
23
|
+
// fresh climbMemo, so the memo is response-scoped; a conversation turn has
|
|
24
|
+
// the conversation's persistent one, so it is conversation-scoped. That
|
|
25
|
+
// matters — the stable-prefix fold makes each turn's subtree independent of
|
|
26
|
+
// what follows, so 59–70% of a later turn's climb regions are byte-identical
|
|
27
|
+
// repeats of an earlier turn's (measured on a 4-turn session), and every one
|
|
28
|
+
// of them used to re-climb from cold.
|
|
29
|
+
//
|
|
30
|
+
// Budgeted, not unbounded (AGENTS §2.12): past the cap the whole map is
|
|
31
|
+
// dropped and re-derived, costing a cold climb and never a wrong answer.
|
|
32
|
+
const REACH_MEMO_MAX = 100_000;
|
|
33
|
+
const reachCaches = new WeakMap();
|
|
34
|
+
/** The reach memo this response should use — see the note above.
|
|
35
|
+
*
|
|
36
|
+
* A TRACED response always gets a fresh, empty one. `AncestorReach`'s
|
|
37
|
+
* `visited`/`maxDepth`/`saturation` fields are populated only when a trace
|
|
38
|
+
* is attached, so an entry deposited by an untraced earlier turn would
|
|
39
|
+
* silently black out the reach detail of a later traced one; and the trace's
|
|
40
|
+
* reach payload is serialised by ITERATING this map, which must therefore
|
|
41
|
+
* hold what THIS climb consulted, not the whole conversation's history.
|
|
42
|
+
* Consistent with AGENTS §2.11: a traced response is a different machine —
|
|
43
|
+
* never benchmark with a trace attached. */
|
|
44
|
+
export function sharedReachMemo(ctx) {
|
|
45
|
+
if (ctx.trace !== null || ctx.climbMemo === null)
|
|
46
|
+
return new Map();
|
|
47
|
+
let m = reachCaches.get(ctx.climbMemo);
|
|
48
|
+
if (m === undefined)
|
|
49
|
+
reachCaches.set(ctx.climbMemo, m = new Map());
|
|
50
|
+
else if (m.size >= REACH_MEMO_MAX)
|
|
51
|
+
m.clear();
|
|
52
|
+
return m;
|
|
53
|
+
}
|
|
11
54
|
function getStructCache(ctx) {
|
|
12
55
|
if (ctx.climbMemo === null)
|
|
13
56
|
return null;
|
|
@@ -89,7 +132,7 @@ export function edgeAncestors(ctx, id, contextCount, memo) {
|
|
|
89
132
|
// is withdrawn. On a small store the floor stays ≤ √N and the atom
|
|
90
133
|
// climbs exactly as before, so single-letter facts keep working.
|
|
91
134
|
if (id < 0 && atomIsHub(ctx, contextCount)) {
|
|
92
|
-
const bound0 =
|
|
135
|
+
const bound0 = boundFor(contextCount);
|
|
93
136
|
const reach = {
|
|
94
137
|
roots: [],
|
|
95
138
|
contextsReached: 0,
|
|
@@ -110,7 +153,7 @@ export function edgeAncestors(ctx, id, contextCount, memo) {
|
|
|
110
153
|
memo?.set(id, reach);
|
|
111
154
|
return reach;
|
|
112
155
|
}
|
|
113
|
-
const bound =
|
|
156
|
+
const bound = boundFor(contextCount);
|
|
114
157
|
const roots = [];
|
|
115
158
|
const seen = new Set([id]);
|
|
116
159
|
const ctxSeen = new Set();
|
|
@@ -162,6 +205,8 @@ export function edgeAncestors(ctx, id, contextCount, memo) {
|
|
|
162
205
|
let visitedCount = 0;
|
|
163
206
|
let maxDepth = 0;
|
|
164
207
|
const visit = (x) => {
|
|
208
|
+
if (ctx.meter)
|
|
209
|
+
ctx.meter.ancestorVisits++;
|
|
165
210
|
if (depths) {
|
|
166
211
|
visitedCount++;
|
|
167
212
|
if (curDepth > maxDepth)
|
|
@@ -344,8 +389,7 @@ export function atomReach(ctx, contextCount) {
|
|
|
344
389
|
* atom votes and is recognised exactly as any stored form; above it the
|
|
345
390
|
* alphabet is scaffolding everywhere and abstains. */
|
|
346
391
|
export function atomIsHub(ctx, contextCount) {
|
|
347
|
-
return atomReach(ctx, contextCount) >
|
|
348
|
-
Math.ceil(Math.sqrt(Math.max(2, contextCount)));
|
|
392
|
+
return atomReach(ctx, contextCount) > boundFor(contextCount);
|
|
349
393
|
}
|
|
350
394
|
/** Whether a node LEADS SOMEWHERE — it bears a continuation edge or a halo.
|
|
351
395
|
* The admission predicate recognition filters sites with (HOW_IT_WORKS
|
|
@@ -386,7 +430,16 @@ export function corpusN(ctx) {
|
|
|
386
430
|
* materialised list. {@link hubCap} is the list-side reading of the same
|
|
387
431
|
* convention. */
|
|
388
432
|
export function hubBound(ctx) {
|
|
389
|
-
return
|
|
433
|
+
return boundFor(corpusN(ctx));
|
|
434
|
+
}
|
|
435
|
+
/** √N for an EXPLICIT context count — the ctx-free reading of {@link
|
|
436
|
+
* hubBound}, for the callers inside this module that are handed a count
|
|
437
|
+
* rather than a context ({@link edgeAncestors}, {@link atomIsHub}). The
|
|
438
|
+
* floor at 2 matches {@link corpusN}'s, so both readings agree for every
|
|
439
|
+
* input: the two used to be spelled out inline, once WITH the floor and
|
|
440
|
+
* once without, in the same function. */
|
|
441
|
+
function boundFor(contextCount) {
|
|
442
|
+
return Math.ceil(Math.sqrt(Math.max(2, contextCount)));
|
|
390
443
|
}
|
|
391
444
|
/** Cap a candidate list at the hub bound √N (insertion order) — the ONE
|
|
392
445
|
* fan-out convention every walk and disambiguation uses (see HOW_IT_WORKS
|
package/dist/src/mind/types.d.ts
CHANGED
|
@@ -4,17 +4,16 @@ import type { BoundedMap, Store } from "../store.js";
|
|
|
4
4
|
import type { Space } from "../sema.js";
|
|
5
5
|
import type { Alphabet } from "../alphabet.js";
|
|
6
6
|
import type { MindConfig } from "../config.js";
|
|
7
|
+
import type { Meter } from "../meter.js";
|
|
7
8
|
import type { GraphSearch, Leaf, Seg, Site } from "./graph-search.js";
|
|
8
9
|
import type { Rationale } from "./rationale.js";
|
|
9
|
-
import type {
|
|
10
|
+
import type { Grid, StableFold } from "../geometry.js";
|
|
10
11
|
/** One {@link MindContext._depositTrees} entry — see that field's doc. */
|
|
11
12
|
export interface DepositCacheEntry {
|
|
12
13
|
/** Turn boundaries accumulated over this content's deposit chain —
|
|
13
14
|
* strictly increasing proper offsets, each a previously-deposited
|
|
14
15
|
* whole-context length. Empty for a first-seen (single-turn) input. */
|
|
15
16
|
boundaries: number[];
|
|
16
|
-
/** Plain-fold pyramid (first-seen inputs only). */
|
|
17
|
-
pyramid?: FoldPyramid;
|
|
18
17
|
/** Stable-prefix segment folds (grown-context inputs only). */
|
|
19
18
|
stable?: StableFold;
|
|
20
19
|
/** The continuation bytes this ctxInput was paired with in ingestPair, if
|
|
@@ -29,6 +28,10 @@ export type Input = string | Uint8Array | Grid | Grid[];
|
|
|
29
28
|
/** The host capabilities GraphSearch consults during a cover. MindContext
|
|
30
29
|
* extends this so the Mind can pass itself as the host. */
|
|
31
30
|
export interface GraphSearchHost {
|
|
31
|
+
/** Work accumulator, or null/absent when nothing is profiling — see
|
|
32
|
+
* src/meter.ts. Declared here (not only on MindContext) so the graph
|
|
33
|
+
* search can report its chart effort without importing mind code. */
|
|
34
|
+
readonly meter?: Meter | null;
|
|
32
35
|
resolve(bytes: Uint8Array): number | null;
|
|
33
36
|
recogniseSpan?(bytes: Uint8Array): {
|
|
34
37
|
sites: ReadonlyArray<Site>;
|
|
@@ -132,6 +135,19 @@ export interface RegionVote {
|
|
|
132
135
|
* to "one region" just because it collapsed to one pooled axiom.
|
|
133
136
|
* Defaults to 1 when absent. */
|
|
134
137
|
absorbed?: number;
|
|
138
|
+
/** The SEPARATE query places this vote's evidence occupies, when that is
|
|
139
|
+
* more than the one contiguous run [start, end]. A cross-region junction
|
|
140
|
+
* vote is pooled as a single synthetic region spanning its endpoints and
|
|
141
|
+
* the gap between them, so `[start, end]` reads as ONE place — yet the
|
|
142
|
+
* vote exists precisely because two non-adjacent regions each voted and
|
|
143
|
+
* only their conjunction resolved. Cluster counting (Attention.clusters)
|
|
144
|
+
* asks "how many separate places in the query corroborate this?", and
|
|
145
|
+
* answering it from the merged span makes every joint binding look like a
|
|
146
|
+
* single local neighbourhood; fusion's dispersion gate then drops it
|
|
147
|
+
* unless it also explains most of the whole query, which a binding inside
|
|
148
|
+
* a MULTI-topic query structurally cannot. Absent for an ordinary
|
|
149
|
+
* per-region vote, where the merged span already is the truth. */
|
|
150
|
+
parts?: readonly (readonly [number, number])[];
|
|
135
151
|
}
|
|
136
152
|
/** The structural gate that first decided an {@link edgeAncestors} climb was
|
|
137
153
|
* saturated (an abstention, not a discriminative conclusion) — pure
|
|
@@ -190,6 +206,12 @@ export type AItem = {
|
|
|
190
206
|
};
|
|
191
207
|
export interface MindContext extends GraphSearchHost {
|
|
192
208
|
store: Store;
|
|
209
|
+
/** The work accumulator for the inference call in flight, or null when
|
|
210
|
+
* nothing is profiling — see src/meter.ts. WRITE-ONLY from the engine's
|
|
211
|
+
* point of view: no inference decision may read a counter, or the
|
|
212
|
+
* determinism contract (AGENTS §2.1) is gone. Every call site is
|
|
213
|
+
* `ctx.meter?.x++`, so an unprofiled response allocates nothing. */
|
|
214
|
+
meter: Meter | null;
|
|
193
215
|
space: Space;
|
|
194
216
|
alphabet: Alphabet;
|
|
195
217
|
cfg: MindConfig;
|
|
@@ -250,8 +272,9 @@ export interface MindContext extends GraphSearchHost {
|
|
|
250
272
|
* folds with the SAME stable-prefix fold query-time perception uses
|
|
251
273
|
* (structural train/inference agreement, load-bearing for recall),
|
|
252
274
|
* reusing every already-folded segment via `stable` (see StableFold) —
|
|
253
|
-
* O(turn) per deposit instead of O(context). A first-seen input
|
|
254
|
-
*
|
|
275
|
+
* O(turn) per deposit instead of O(context). A first-seen input takes the
|
|
276
|
+
* same fold with no boundaries at all, and caches the segments it produced
|
|
277
|
+
* so a later turn of the same conversation reuses them. Purely a
|
|
255
278
|
* performance cache for the FOLD STATE; the boundaries are semantic but
|
|
256
279
|
* derived only from the deposit sequence itself (an evicted chain falls
|
|
257
280
|
* back to plain-fold behavior, exactly the pre-boundary shape). */
|
package/dist/src/store.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Vec } from "./vec.js";
|
|
2
2
|
import { type StoreConfig } from "./config.js";
|
|
3
|
+
import type { Meter } from "./meter.js";
|
|
3
4
|
/** A node id: a dense, non-negative integer assigned in creation order. */
|
|
4
5
|
export type NodeId = number;
|
|
5
6
|
/** A node in the graph. Exactly one of `leaf` / `kids` is set. */
|
|
@@ -62,6 +63,13 @@ export declare class BoundedMap<K, V> {
|
|
|
62
63
|
}
|
|
63
64
|
export interface Store {
|
|
64
65
|
readonly D: number;
|
|
66
|
+
/** The work accumulator for the inference call in flight, or null. The
|
|
67
|
+
* Mind attaches one per profiled response and detaches it after (see
|
|
68
|
+
* src/meter.ts). A store MUST only ever write to it — no read may reach
|
|
69
|
+
* a decision, or determinism is gone. An adapter that adds reads of its
|
|
70
|
+
* own should bump the matching counter; one that does not simply reports
|
|
71
|
+
* less, never wrongly. */
|
|
72
|
+
meter: Meter | null;
|
|
65
73
|
/** Insert a leaf, returning its content id. Idempotent. */
|
|
66
74
|
putLeaf(bytes: Uint8Array, gist: Vec): Promise<NodeId>;
|
|
67
75
|
/** Insert a branch over child ids, returning its content id. Idempotent. */
|
|
@@ -508,6 +516,10 @@ export declare abstract class AbstractStore implements Store {
|
|
|
508
516
|
* outlive their records — the read degrades safely to empty bytes, this
|
|
509
517
|
* counter is what keeps that degradation observable. */
|
|
510
518
|
danglingReads: number;
|
|
519
|
+
/** {@link Store.meter} — the per-response work accumulator, or null when
|
|
520
|
+
* nothing is profiling. Every read below bumps it through `?.`, so an
|
|
521
|
+
* unprofiled store pays one null check per read and allocates nothing. */
|
|
522
|
+
meter: Meter | null;
|
|
511
523
|
bytes(id: NodeId): Uint8Array;
|
|
512
524
|
/** First `maxLen` bytes of a node. Walks only the leftmost branch,
|
|
513
525
|
* stopping at `maxLen` — so a 1 MB document root costs the same as a
|
|
@@ -517,6 +529,9 @@ export declare abstract class AbstractStore implements Store {
|
|
|
517
529
|
* may be shared with the byte cache and with other callers — treat them
|
|
518
530
|
* as read-only. Mutating one would corrupt every subsequent read. */
|
|
519
531
|
bytesPrefix(id: NodeId, maxLen: number): Uint8Array;
|
|
532
|
+
/** {@link bytesPrefix}'s recursive body — uncharged; see the metering
|
|
533
|
+
* boundary note there. */
|
|
534
|
+
private _prefix;
|
|
520
535
|
contentLen(id: NodeId, cap?: number): number;
|
|
521
536
|
findLeaf(bytes: Uint8Array): NodeId | null;
|
|
522
537
|
findBranch(kids: NodeId[]): NodeId | null;
|
package/dist/src/store.js
CHANGED
|
@@ -516,6 +516,8 @@ export class AbstractStore {
|
|
|
516
516
|
}
|
|
517
517
|
// ── DAG traversal ──────────────────────────────────────────────────────
|
|
518
518
|
get(id) {
|
|
519
|
+
if (this.meter)
|
|
520
|
+
this.meter.nodeRecords++;
|
|
519
521
|
// Byte leaves are implicit — fabricate from the id.
|
|
520
522
|
if (id < 0) {
|
|
521
523
|
return { id, leaf: new Uint8Array([-(id + 1)]), kids: null };
|
|
@@ -536,7 +538,20 @@ export class AbstractStore {
|
|
|
536
538
|
* outlive their records — the read degrades safely to empty bytes, this
|
|
537
539
|
* counter is what keeps that degradation observable. */
|
|
538
540
|
danglingReads = 0;
|
|
541
|
+
/** {@link Store.meter} — the per-response work accumulator, or null when
|
|
542
|
+
* nothing is profiling. Every read below bumps it through `?.`, so an
|
|
543
|
+
* unprofiled store pays one null check per read and allocates nothing. */
|
|
544
|
+
meter = null;
|
|
539
545
|
bytes(id) {
|
|
546
|
+
if (this.meter) {
|
|
547
|
+
this.meter.byteReads++;
|
|
548
|
+
// Charged BEFORE the fast path returns, and from the cache entry when
|
|
549
|
+
// there is one: the volume a caller pulled through is the cost signal
|
|
550
|
+
// (an unbounded read is unbounded whether or not it was cached).
|
|
551
|
+
const c = this._bytesCache.get(id);
|
|
552
|
+
if (c)
|
|
553
|
+
this.meter.bytesRead += c.length;
|
|
554
|
+
}
|
|
540
555
|
// Fast path.
|
|
541
556
|
const hit = this._bytesCache.get(id);
|
|
542
557
|
if (hit)
|
|
@@ -583,7 +598,10 @@ export class AbstractStore {
|
|
|
583
598
|
const out = concat(kids.map((k) => cache.get(k)));
|
|
584
599
|
cache.set(nid, out);
|
|
585
600
|
}
|
|
586
|
-
|
|
601
|
+
const out = cache.get(id) ?? _ZERO;
|
|
602
|
+
if (this.meter)
|
|
603
|
+
this.meter.bytesRead += out.length;
|
|
604
|
+
return out;
|
|
587
605
|
}
|
|
588
606
|
/** First `maxLen` bytes of a node. Walks only the leftmost branch,
|
|
589
607
|
* stopping at `maxLen` — so a 1 MB document root costs the same as a
|
|
@@ -593,8 +611,6 @@ export class AbstractStore {
|
|
|
593
611
|
* may be shared with the byte cache and with other callers — treat them
|
|
594
612
|
* as read-only. Mutating one would corrupt every subsequent read. */
|
|
595
613
|
bytesPrefix(id, maxLen) {
|
|
596
|
-
if (maxLen <= 0)
|
|
597
|
-
return _ZERO;
|
|
598
614
|
// A FULL read (the ALL sentinel) routes through bytes(), whose
|
|
599
615
|
// reconstruction enters the byte-budget cache. Without this, the mind's
|
|
600
616
|
// read() — which always passes ALL — re-walked the DAG and re-concatenated
|
|
@@ -602,6 +618,23 @@ export class AbstractStore {
|
|
|
602
618
|
// exists precisely for those reconstructions.
|
|
603
619
|
if (maxLen >= 0x7fffffff)
|
|
604
620
|
return this.bytes(id);
|
|
621
|
+
// METERING BOUNDARY: this public entry point is charged ONCE per logical
|
|
622
|
+
// read; the walk below recurses through `_prefix`, which is not charged.
|
|
623
|
+
// Counting the recursion instead made a single read of an N-byte branch
|
|
624
|
+
// report as N reads (one per node descended), so `byteReads` measured
|
|
625
|
+
// tree size rather than read requests — it read as ~1 byte per read.
|
|
626
|
+
if (this.meter)
|
|
627
|
+
this.meter.byteReads++;
|
|
628
|
+
const out = this._prefix(id, maxLen);
|
|
629
|
+
if (this.meter)
|
|
630
|
+
this.meter.bytesRead += out.length;
|
|
631
|
+
return out;
|
|
632
|
+
}
|
|
633
|
+
/** {@link bytesPrefix}'s recursive body — uncharged; see the metering
|
|
634
|
+
* boundary note there. */
|
|
635
|
+
_prefix(id, maxLen) {
|
|
636
|
+
if (maxLen <= 0)
|
|
637
|
+
return _ZERO;
|
|
605
638
|
// Full-cache hit: bytes() already reconstructed the whole node.
|
|
606
639
|
const full = this._bytesCache.get(id);
|
|
607
640
|
if (full)
|
|
@@ -625,13 +658,15 @@ export class AbstractStore {
|
|
|
625
658
|
for (const k of kids) {
|
|
626
659
|
if (got >= maxLen)
|
|
627
660
|
break;
|
|
628
|
-
const child = this.
|
|
661
|
+
const child = this._prefix(k, maxLen - got);
|
|
629
662
|
parts.push(child);
|
|
630
663
|
got += child.length;
|
|
631
664
|
}
|
|
632
665
|
return concat(parts);
|
|
633
666
|
}
|
|
634
667
|
contentLen(id, cap = Infinity) {
|
|
668
|
+
if (this.meter)
|
|
669
|
+
this.meter.lenReads++;
|
|
635
670
|
if (id < 0)
|
|
636
671
|
return 1; // implicit single-byte leaf
|
|
637
672
|
const hit = this._lenCache.get(id);
|
|
@@ -661,6 +696,8 @@ export class AbstractStore {
|
|
|
661
696
|
}
|
|
662
697
|
// ── Content-addressed lookup ───────────────────────────────────────────
|
|
663
698
|
findLeaf(bytes) {
|
|
699
|
+
if (this.meter)
|
|
700
|
+
this.meter.leafLookups++;
|
|
664
701
|
if (bytes.length === 1)
|
|
665
702
|
return -(bytes[0] + 1);
|
|
666
703
|
const key = keyOf(bytes);
|
|
@@ -673,6 +710,8 @@ export class AbstractStore {
|
|
|
673
710
|
return id;
|
|
674
711
|
}
|
|
675
712
|
findBranch(kids) {
|
|
713
|
+
if (this.meter)
|
|
714
|
+
this.meter.branchLookups++;
|
|
676
715
|
const key = kids.join(",");
|
|
677
716
|
const cached = this._branchKey.get(key);
|
|
678
717
|
if (cached !== undefined)
|
|
@@ -693,15 +732,23 @@ export class AbstractStore {
|
|
|
693
732
|
}
|
|
694
733
|
// ── Structural parents ─────────────────────────────────────────────────
|
|
695
734
|
parents(id) {
|
|
735
|
+
if (this.meter)
|
|
736
|
+
this.meter.parentReads++;
|
|
696
737
|
return this._dbGetParents(id);
|
|
697
738
|
}
|
|
698
739
|
parentsFirst(id, limit) {
|
|
740
|
+
if (this.meter)
|
|
741
|
+
this.meter.parentReads++;
|
|
699
742
|
return this._dbGetParentsFirst(id, limit);
|
|
700
743
|
}
|
|
701
744
|
hasParents(id) {
|
|
745
|
+
if (this.meter)
|
|
746
|
+
this.meter.parentProbes++;
|
|
702
747
|
return this._dbGetParentsFirst(id, 1).length > 0;
|
|
703
748
|
}
|
|
704
749
|
chainRun(id) {
|
|
750
|
+
if (this.meter)
|
|
751
|
+
this.meter.chainRuns++;
|
|
705
752
|
const hit = this._chainMemo.get(id);
|
|
706
753
|
if (hit !== undefined)
|
|
707
754
|
return hit;
|
|
@@ -736,12 +783,16 @@ export class AbstractStore {
|
|
|
736
783
|
set.add(parent);
|
|
737
784
|
}
|
|
738
785
|
hasContainers(child) {
|
|
786
|
+
if (this.meter)
|
|
787
|
+
this.meter.containerProbes++;
|
|
739
788
|
if (this._dbContainExists(child))
|
|
740
789
|
return true;
|
|
741
790
|
const buf = this._containBuf.get(child);
|
|
742
791
|
return buf !== undefined && buf.size > 0;
|
|
743
792
|
}
|
|
744
793
|
containersSlice(child, offset, limit) {
|
|
794
|
+
if (this.meter)
|
|
795
|
+
this.meter.containerReads++;
|
|
745
796
|
const out = this._dbGetContainParentsSlice(child, offset, limit);
|
|
746
797
|
if (out.length >= limit)
|
|
747
798
|
return out;
|
|
@@ -765,6 +816,8 @@ export class AbstractStore {
|
|
|
765
816
|
return out;
|
|
766
817
|
}
|
|
767
818
|
containers(child) {
|
|
819
|
+
if (this.meter)
|
|
820
|
+
this.meter.containerReads++;
|
|
768
821
|
const stored = this._dbGetContainParents(child);
|
|
769
822
|
const buf = this._containBuf.get(child);
|
|
770
823
|
if (stored.length === 0)
|
|
@@ -1152,9 +1205,14 @@ export class AbstractStore {
|
|
|
1152
1205
|
const cache = this._resonateCache;
|
|
1153
1206
|
if (cache) {
|
|
1154
1207
|
const hit = cache.get(rk);
|
|
1155
|
-
if (hit !== undefined)
|
|
1208
|
+
if (hit !== undefined) {
|
|
1209
|
+
if (this.meter)
|
|
1210
|
+
this.meter.annCacheHits++;
|
|
1156
1211
|
return hit;
|
|
1212
|
+
}
|
|
1157
1213
|
}
|
|
1214
|
+
if (this.meter)
|
|
1215
|
+
this.meter.annQueries++;
|
|
1158
1216
|
const clusters = this._vecContentClusterCount();
|
|
1159
1217
|
const results = this._vecContentQuery(normalize(copy(v)), k * this.overfetch,
|
|
1160
1218
|
// Exhaustive: probe every cluster (ef ≥ 4·clusters guarantees the
|
|
@@ -1169,6 +1227,10 @@ export class AbstractStore {
|
|
|
1169
1227
|
if (out.length >= k)
|
|
1170
1228
|
break;
|
|
1171
1229
|
}
|
|
1230
|
+
// Vectors the index actually scored for THIS descent — the counter that
|
|
1231
|
+
// exposes a query whose ANN cost is growing with the corpus.
|
|
1232
|
+
if (this.meter)
|
|
1233
|
+
this.meter.annVectorReads += this._vecContentLastReads();
|
|
1172
1234
|
const rc = this._resonateCache ??= new Map();
|
|
1173
1235
|
if (rc.size >= RESONATE_CACHE_MAX)
|
|
1174
1236
|
rc.clear();
|
|
@@ -1390,24 +1452,36 @@ export class AbstractStore {
|
|
|
1390
1452
|
this._dbInsertEdge(from, to);
|
|
1391
1453
|
}
|
|
1392
1454
|
next(id) {
|
|
1455
|
+
if (this.meter)
|
|
1456
|
+
this.meter.edgeReads++;
|
|
1393
1457
|
return this._dbGetNextEdges(id);
|
|
1394
1458
|
}
|
|
1395
1459
|
/** {@link Store.hasNext} — one indexed point probe, never a range read. */
|
|
1396
1460
|
hasNext(id) {
|
|
1461
|
+
if (this.meter)
|
|
1462
|
+
this.meter.edgeProbes++;
|
|
1397
1463
|
return this._dbEdgeSrcExists(id);
|
|
1398
1464
|
}
|
|
1399
1465
|
prev(id) {
|
|
1466
|
+
if (this.meter)
|
|
1467
|
+
this.meter.prevReads++;
|
|
1400
1468
|
return this._dbGetPrevEdges(id);
|
|
1401
1469
|
}
|
|
1402
1470
|
nextFirst(id, limit) {
|
|
1471
|
+
if (this.meter)
|
|
1472
|
+
this.meter.edgeReads++;
|
|
1403
1473
|
return this._dbGetNextEdgesFirst(id, limit);
|
|
1404
1474
|
}
|
|
1405
1475
|
prevFirst(id, limit) {
|
|
1476
|
+
if (this.meter)
|
|
1477
|
+
this.meter.prevReads++;
|
|
1406
1478
|
return this._dbGetPrevEdgesFirst(id, limit);
|
|
1407
1479
|
}
|
|
1408
1480
|
/** {@link Store.prevCount}. Subclasses with an indexed reverse-edge count
|
|
1409
1481
|
* should override; this default materialises (correct, not optimal). */
|
|
1410
1482
|
prevCount(id) {
|
|
1483
|
+
if (this.meter)
|
|
1484
|
+
this.meter.prevProbes++;
|
|
1411
1485
|
return this._dbGetPrevEdges(id).length;
|
|
1412
1486
|
}
|
|
1413
1487
|
edgeSourceCount() {
|
|
@@ -1422,10 +1496,14 @@ export class AbstractStore {
|
|
|
1422
1496
|
}
|
|
1423
1497
|
// ── Halos ──────────────────────────────────────────────────────────────
|
|
1424
1498
|
haloMass(id) {
|
|
1499
|
+
if (this.meter)
|
|
1500
|
+
this.meter.haloProbes++;
|
|
1425
1501
|
const r = this._dbGetHalo(id);
|
|
1426
1502
|
return r ? r.mass : 0;
|
|
1427
1503
|
}
|
|
1428
1504
|
halo(id) {
|
|
1505
|
+
if (this.meter)
|
|
1506
|
+
this.meter.haloReads++;
|
|
1429
1507
|
const cached = this._haloNorm.get(id);
|
|
1430
1508
|
if (cached !== undefined)
|
|
1431
1509
|
return copy(cached);
|
|
@@ -1440,6 +1518,8 @@ export class AbstractStore {
|
|
|
1440
1518
|
/** {@link Store.hasHalo} — MUST mirror {@link halo}'s null condition
|
|
1441
1519
|
* exactly (row present AND mass ≥ minHaloMass), minus the decode. */
|
|
1442
1520
|
hasHalo(id) {
|
|
1521
|
+
if (this.meter)
|
|
1522
|
+
this.meter.haloProbes++;
|
|
1443
1523
|
const r = this._dbGetHalo(id);
|
|
1444
1524
|
return r !== null && r.mass >= this.minHaloMass;
|
|
1445
1525
|
}
|
|
@@ -1473,9 +1553,14 @@ export class AbstractStore {
|
|
|
1473
1553
|
const cache = this._resonateHaloCache;
|
|
1474
1554
|
if (cache) {
|
|
1475
1555
|
const hit = cache.get(rk);
|
|
1476
|
-
if (hit !== undefined)
|
|
1556
|
+
if (hit !== undefined) {
|
|
1557
|
+
if (this.meter)
|
|
1558
|
+
this.meter.annCacheHits++;
|
|
1477
1559
|
return hit;
|
|
1560
|
+
}
|
|
1478
1561
|
}
|
|
1562
|
+
if (this.meter)
|
|
1563
|
+
this.meter.haloQueries++;
|
|
1479
1564
|
const results = this._vecHaloQuery(normalize(copy(v)), k * this.overfetch, this.efFor(this._vecHaloClusterCount()));
|
|
1480
1565
|
const out = [];
|
|
1481
1566
|
for (const r of results) {
|
package/package.json
CHANGED
|
@@ -128,12 +128,25 @@ export interface Derivation<I> {
|
|
|
128
128
|
premises: Array<Derivation<I>>;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
/** Optional search-effort read-out, filled in place while the search runs.
|
|
132
|
+
* A plain structural record so the sublibrary stays firewalled (it imports
|
|
133
|
+
* nothing from Sema, and Sema copies these into its own accounting). Purely
|
|
134
|
+
* observational: the search never reads it back. */
|
|
135
|
+
export interface SearchStats {
|
|
136
|
+
/** Agenda entries popped, stale lazy-deletion entries included — the chart
|
|
137
|
+
* work actually done. */
|
|
138
|
+
pops: number;
|
|
139
|
+
/** Agenda entries pushed by a successful relaxation. */
|
|
140
|
+
pushes: number;
|
|
141
|
+
}
|
|
142
|
+
|
|
131
143
|
/**
|
|
132
144
|
* Find a lightest derivation of a goal item, or `null` if none exists.
|
|
133
145
|
* `cost` on the returned root is the total derivation cost.
|
|
134
146
|
*/
|
|
135
147
|
export function lightestDerivation<I>(
|
|
136
148
|
system: DeductionSystem<I>,
|
|
149
|
+
stats?: SearchStats,
|
|
137
150
|
): Derivation<I> | null {
|
|
138
151
|
const g = new Map<string, number>(); // best known cost per item
|
|
139
152
|
const proof = new Map<string, Rule<I> | null>(); // producing rule per item
|
|
@@ -177,6 +190,7 @@ export function lightestDerivation<I>(
|
|
|
177
190
|
g.set(key, cost);
|
|
178
191
|
proof.set(key, rule);
|
|
179
192
|
items.set(key, item);
|
|
193
|
+
if (stats) stats.pushes++;
|
|
180
194
|
agenda.push(cost + h(item, key), { key, g: cost });
|
|
181
195
|
}
|
|
182
196
|
};
|
|
@@ -185,6 +199,7 @@ export function lightestDerivation<I>(
|
|
|
185
199
|
|
|
186
200
|
while (agenda.size > 0) {
|
|
187
201
|
const { value } = agenda.pop() as { value: { key: string; g: number } };
|
|
202
|
+
if (stats) stats.pops++;
|
|
188
203
|
const key = value.key;
|
|
189
204
|
// Lazy deletion: an entry is stale if a cheaper derivation has since been
|
|
190
205
|
// recorded for the same item.
|
package/src/derive/src/index.ts
CHANGED