@hviana/sema 0.6.0 → 0.7.2
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/.github/workflows/release.yml +80 -0
- package/AGENTS.md +53 -9
- package/HOW_IT_WORKS.md +17 -16
- package/dist/src/meter.d.ts +14 -4
- package/dist/src/meter.js +27 -3
- package/dist/src/mind/attention.js +22 -20
- package/dist/src/mind/graph-search.d.ts +43 -9
- package/dist/src/mind/graph-search.js +82 -15
- package/dist/src/mind/junction.d.ts +13 -0
- package/dist/src/mind/junction.js +26 -1
- package/dist/src/mind/mechanisms/cover.js +23 -2
- package/dist/src/mind/mechanisms/prefix-completion.d.ts +2 -1
- package/dist/src/mind/mechanisms/prefix-completion.js +40 -20
- package/dist/src/mind/mechanisms/recall.js +8 -4
- package/dist/src/mind/pipeline-mechanism.d.ts +0 -24
- package/dist/src/mind/pipeline-mechanism.js +13 -36
- package/dist/src/mind/pipeline.d.ts +24 -0
- package/dist/src/mind/pipeline.js +71 -5
- package/dist/src/mind/recognition.d.ts +15 -1
- package/dist/src/mind/recognition.js +15 -1
- package/dist/src/mind/resonance.js +54 -12
- package/dist/src/store.js +22 -1
- package/jsr.json +1 -1
- package/package.json +7 -2
- package/src/meter.ts +27 -4
- package/src/mind/attention.ts +22 -19
- package/src/mind/graph-search.ts +93 -16
- package/src/mind/junction.ts +25 -1
- package/src/mind/mechanisms/cover.ts +23 -4
- package/src/mind/mechanisms/prefix-completion.ts +40 -20
- package/src/mind/mechanisms/recall.ts +8 -4
- package/src/mind/pipeline-mechanism.ts +13 -42
- package/src/mind/pipeline.ts +106 -5
- package/src/mind/recognition.ts +19 -2
- package/src/mind/resonance.ts +84 -49
- package/src/store.ts +21 -1
- package/test/89-completion-recursion.test.mjs +230 -0
- package/test/90-connector-read-cap.test.mjs +130 -0
- package/test/91-branch-bytes-cache.test.mjs +152 -0
- package/test/93-regime-prediction.test.mjs +148 -0
- package/test/94-cross-region-budget.test.mjs +67 -0
- package/test/95-wide-resonance-removed.test.mjs +109 -0
package/dist/src/store.js
CHANGED
|
@@ -723,7 +723,28 @@ export class AbstractStore {
|
|
|
723
723
|
parts.push(child);
|
|
724
724
|
got += child.length;
|
|
725
725
|
}
|
|
726
|
-
|
|
726
|
+
const out = concat(parts);
|
|
727
|
+
// Cache the BRANCH too, not just the leaf above. Reconstruction is a pure
|
|
728
|
+
// function of the store, so this is a transparent cache in the strict sense
|
|
729
|
+
// — an eviction costs a re-walk and nothing else — which is exactly what
|
|
730
|
+
// `_bytesCache`'s "smallest"/"clock" configuration is for.
|
|
731
|
+
//
|
|
732
|
+
// Caching only leaves made every branch re-walk its whole subtree on every
|
|
733
|
+
// request, and the DAG is hash-consed, so the same children recur under many
|
|
734
|
+
// parents. Measured on the 18.9M-node store, ONE 1,314-byte query:
|
|
735
|
+
// 20,021,474 `_prefix` calls over 469,083 distinct ids (42.7x reuse) to
|
|
736
|
+
// produce 87,789 results — 97.7% of the work re-derived bytes it had already
|
|
737
|
+
// built. One single-byte leaf was reconstructed 2,599,984 times. Measuring
|
|
738
|
+
// reuse at the TOP level only shows 1.1x and hides all of it.
|
|
739
|
+
//
|
|
740
|
+
// Only a COMPLETE reconstruction may be cached: `_prefix` is also called
|
|
741
|
+
// with a cap, and a truncated prefix stored under `id` would be served as
|
|
742
|
+
// if it were the node's whole content by the `_bytesCache` hit above.
|
|
743
|
+
// `got < maxLen` is that proof — the walk ran out of children before it ran
|
|
744
|
+
// out of budget, so nothing below was truncated either.
|
|
745
|
+
if (got < maxLen)
|
|
746
|
+
this._bytesCache.set(id, out);
|
|
747
|
+
return out;
|
|
727
748
|
}
|
|
728
749
|
contentLen(id, cap = Infinity) {
|
|
729
750
|
if (this.meter)
|
package/jsr.json
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hviana/sema",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Sema: a non-parametric, instance-based reasoning system.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/hviana/sema.git"
|
|
8
|
+
},
|
|
5
9
|
"type": "module",
|
|
6
10
|
"main": "dist/src/index.js",
|
|
7
11
|
"types": "dist/src/index.d.ts",
|
|
@@ -14,7 +18,8 @@
|
|
|
14
18
|
"scripts": {
|
|
15
19
|
"build": "tsc",
|
|
16
20
|
"demo": "tsc && node dist/example/demo.js",
|
|
17
|
-
"test": "tsc && node --test test/**/*.test.mjs"
|
|
21
|
+
"test": "tsc && node --test test/**/*.test.mjs",
|
|
22
|
+
"prepublishOnly": "npm test"
|
|
18
23
|
},
|
|
19
24
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
20
25
|
"devDependencies": {
|
package/src/meter.ts
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
* PHASES NEST, AND ARE NOT DISJOINT. `think` contains every mechanism
|
|
33
33
|
* phase; a mechanism's `floor` contains whatever shared analysis it
|
|
34
34
|
* first-touched (`attention`, `weave`); `recall.run` contains
|
|
35
|
-
* `substitutionBridge
|
|
35
|
+
* `substitutionBridge`. Read a
|
|
36
36
|
* phase as "wall-clock spent inside this, inclusive" — never sum them and
|
|
37
37
|
* expect the total. `CostReport.elapsedMs` is the only whole.
|
|
38
38
|
*
|
|
@@ -176,6 +176,13 @@ export class Meter {
|
|
|
176
176
|
/** Nodes popped by those ascents, against their √N·W budget — the counter
|
|
177
177
|
* that shows whether the walks are deciding early or burning the budget. */
|
|
178
178
|
junctionPops = 0;
|
|
179
|
+
/** Ascents that ended by EXHAUSTING the expansion budget rather than by
|
|
180
|
+
* deciding — the walk abstained and the caller silently fell through to a
|
|
181
|
+
* lower tier of the ladder (§2.13: a degradation nothing else reports).
|
|
182
|
+
* It rises the moment a SHARED budget is drained by an earlier walk, which
|
|
183
|
+
* is what makes "this tier answered nothing" distinguishable from "this
|
|
184
|
+
* tier never got to look". */
|
|
185
|
+
junctionBudgetExhausted = 0;
|
|
179
186
|
/** Arbitrary byte spans whose distributional company was VSA-bundled from
|
|
180
187
|
* existing episode halos. */
|
|
181
188
|
spanHalos = 0;
|
|
@@ -198,9 +205,6 @@ export class Meter {
|
|
|
198
205
|
mechanismRuns = 0;
|
|
199
206
|
/** Candidates the decider weighed. */
|
|
200
207
|
candidates = 0;
|
|
201
|
-
/** Candidates refused before the competition for explaining less than 1/W
|
|
202
|
-
* of the query — the honesty-density floor (see pipeline.ts `consider`). */
|
|
203
|
-
thinRejects = 0;
|
|
204
208
|
|
|
205
209
|
// ── Phases ──────────────────────────────────────────────────────────────
|
|
206
210
|
|
|
@@ -234,6 +238,25 @@ export class Meter {
|
|
|
234
238
|
}
|
|
235
239
|
}
|
|
236
240
|
|
|
241
|
+
/** Time one SYNCHRONOUS phase. The sync/async seam (§2.10) is a real
|
|
242
|
+
* contract — perception, recognition and the graph search are synchronous —
|
|
243
|
+
* so a synchronous layer must not be wrapped in `time`'s promise just to be
|
|
244
|
+
* measured: that would make the profiled path await where the unprofiled
|
|
245
|
+
* one does not, and a meter never changes what a layer computes. */
|
|
246
|
+
timeSync<T>(phase: string, fn: () => T): T {
|
|
247
|
+
const before = this.snapshot();
|
|
248
|
+
const t = performance.now();
|
|
249
|
+
try {
|
|
250
|
+
return fn();
|
|
251
|
+
} finally {
|
|
252
|
+
const ms = performance.now() - t;
|
|
253
|
+
const after = this.snapshot();
|
|
254
|
+
const delta: Record<string, number> = {};
|
|
255
|
+
for (const k of Object.keys(after)) delta[k] = after[k] - before[k];
|
|
256
|
+
this.charge(phase, ms, delta);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
237
260
|
/** Time one async phase and attribute the work done inside it. Returns
|
|
238
261
|
* the awaited value untouched — a meter never changes what a layer
|
|
239
262
|
* computes, only what is known about it. */
|
package/src/mind/attention.ts
CHANGED
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
import { recognise } from "./recognition.js";
|
|
46
46
|
import { leafIdRun } from "./canonical.js";
|
|
47
47
|
import {
|
|
48
|
+
atomIsHub,
|
|
48
49
|
corpusN,
|
|
49
50
|
edgeAncestors,
|
|
50
51
|
hubBound,
|
|
@@ -2647,26 +2648,28 @@ async function crossRegionVotes(
|
|
|
2647
2648
|
// the same container (or a sub-container of it) twice.
|
|
2648
2649
|
const consumed = new Set<number>();
|
|
2649
2650
|
let probes = 0;
|
|
2650
|
-
//
|
|
2651
|
-
//
|
|
2652
|
-
//
|
|
2653
|
-
//
|
|
2654
|
-
//
|
|
2655
|
-
//
|
|
2651
|
+
// When atoms themselves are hubs (atomIsHub — a single byte reaches ≥ √N
|
|
2652
|
+
// contexts, §2.8's own predicate), the corpus is large enough that the
|
|
2653
|
+
// cross-region junction walks are dominated by the drift through common
|
|
2654
|
+
// content's ancestry. Each of k candidate pairs otherwise spends its own
|
|
2655
|
+
// √N·W budget (profiled: 160,210 junction pops, 31% of think at
|
|
2656
|
+
// N = 325,608), and a cumulative dialogue multiplies bounded work into tens
|
|
2657
|
+
// of seconds. The structural walk is therefore given ONE k·W allowance per
|
|
2658
|
+
// evidence tier, shared across every pair — k pairs × W phrase-scale levels,
|
|
2659
|
+
// the minimal exact check; a pair whose container is not reached within it
|
|
2660
|
+
// falls through to the resonance tier (the ANN proposes what the shallow
|
|
2661
|
+
// walk no longer exhaustively scans, §2.3).
|
|
2656
2662
|
//
|
|
2657
|
-
//
|
|
2658
|
-
//
|
|
2659
|
-
//
|
|
2660
|
-
//
|
|
2661
|
-
//
|
|
2662
|
-
//
|
|
2663
|
-
// too tight below ~10³ contexts
|
|
2664
|
-
//
|
|
2665
|
-
//
|
|
2666
|
-
|
|
2667
|
-
// already makes — pricing the gate on the synthetic corpora.
|
|
2668
|
-
const marketScale = k * ctx.space.maxGroup;
|
|
2669
|
-
const corpusScale = N > marketScale ** 3;
|
|
2663
|
+
// Below atomIsHub the store is small and atoms still discriminate, so the
|
|
2664
|
+
// walks keep exhaustive exact traversal (per-walk √N·W) — the shared budget
|
|
2665
|
+
// would otherwise be smaller than the structures the tests deliberately
|
|
2666
|
+
// build. The gate is the SAME derived predicate the climb already uses for
|
|
2667
|
+
// byte atoms, not a separate corpus-size knob: an earlier `N > (k·W)³` cube
|
|
2668
|
+
// never engaged at real scale (96³ = 884,736 > 325,608), and a "share one
|
|
2669
|
+
// √N·W" experiment was too tight below ~10³ contexts (test/36, test/14) —
|
|
2670
|
+
// both are the same mistake of pricing the gate on corpus size instead of on
|
|
2671
|
+
// the atom-hub scale.
|
|
2672
|
+
const corpusScale = atomIsHub(ctx, N);
|
|
2670
2673
|
const exactBudget = corpusScale ? { n: k * ctx.space.maxGroup } : undefined;
|
|
2671
2674
|
const synonymBudget = corpusScale ? { n: k * ctx.space.maxGroup } : undefined;
|
|
2672
2675
|
|
package/src/mind/graph-search.ts
CHANGED
|
@@ -108,6 +108,13 @@ export type GItem =
|
|
|
108
108
|
* because subtraction is more "the point" of its query). See
|
|
109
109
|
* {@link liftAnswer}. */
|
|
110
110
|
computed?: boolean;
|
|
111
|
+
/** Set on the out emitted at a chain's GENUINE FIXPOINT — the one span kind a
|
|
112
|
+
* recursive re-cover ({@link GraphSearch.recompleteNode}) may deepen. The
|
|
113
|
+
* re-cover does NOT run here: this marks the span as eligible, and
|
|
114
|
+
* {@link GraphSearch.deepen} runs it afterwards on the spans the lightest
|
|
115
|
+
* derivation actually CHOSE. Part of {@link key}, because it decides
|
|
116
|
+
* whether the span's final bytes may still change. */
|
|
117
|
+
fix?: boolean;
|
|
111
118
|
};
|
|
112
119
|
type OutItem = Extract<GItem, { kind: "out" }>;
|
|
113
120
|
|
|
@@ -153,6 +160,10 @@ export interface Seg {
|
|
|
153
160
|
/** See the `computed` field of the "out" {@link GItem} — set only for an
|
|
154
161
|
* extension's derived value, never a genuinely recognised learned form. */
|
|
155
162
|
computed?: boolean;
|
|
163
|
+
/** See the `fix` field of the "out" {@link GItem} — this span ended a chain at
|
|
164
|
+
* a genuine fixpoint, so it is the one span kind a recursive re-cover may
|
|
165
|
+
* still deepen. Consumed by {@link GraphSearch.deepen}. */
|
|
166
|
+
fix?: boolean;
|
|
156
167
|
}
|
|
157
168
|
|
|
158
169
|
/** Read the chosen spans back off a derivation: the goal is a chain of bridge
|
|
@@ -171,6 +182,7 @@ function readCover(derivation: Derivation<GItem>): Seg[] {
|
|
|
171
182
|
rec: out.rec,
|
|
172
183
|
node: out.node,
|
|
173
184
|
computed: out.computed,
|
|
185
|
+
fix: out.fix,
|
|
174
186
|
});
|
|
175
187
|
}
|
|
176
188
|
node = node.premises[0];
|
|
@@ -472,10 +484,32 @@ export class GraphSearch {
|
|
|
472
484
|
onDerivation(readDerivation(derivation, substitutions !== undefined));
|
|
473
485
|
}
|
|
474
486
|
return derivation
|
|
475
|
-
? { segs: readCover(derivation), cost: derivation.cost }
|
|
487
|
+
? { segs: this.deepen(readCover(derivation)), cost: derivation.cost }
|
|
476
488
|
: null;
|
|
477
489
|
}
|
|
478
490
|
|
|
491
|
+
/** Re-cover the CHOSEN fixpoint spans, in place.
|
|
492
|
+
*
|
|
493
|
+
* Completion is still "cover, recursively" — it just runs on the answer
|
|
494
|
+
* instead of on the exploration. A cover chooses O(segs) spans, so a level
|
|
495
|
+
* pays O(answer) re-covers however densely the corpus interconnects the forms
|
|
496
|
+
* the search passed through on the way. That is the bound
|
|
497
|
+
* {@link recompleteNode}'s contract always claimed and, running per fixpoint
|
|
498
|
+
* REACHED, never had.
|
|
499
|
+
*
|
|
500
|
+
* Deepening cannot change which cover won: the derivation is already final
|
|
501
|
+
* and every span keeps its i..j and its cost. It only replaces a chosen
|
|
502
|
+
* span's bytes with the deeper learnt form they rewrite to — what the
|
|
503
|
+
* recursion was always for. */
|
|
504
|
+
private deepen(segs: Seg[]): Seg[] {
|
|
505
|
+
for (const s of segs) {
|
|
506
|
+
if (!s.fix || s.node === undefined) continue;
|
|
507
|
+
const deeper = this.recompleteNode(s.node);
|
|
508
|
+
if (deeper !== null) s.bytes = deeper;
|
|
509
|
+
}
|
|
510
|
+
return segs;
|
|
511
|
+
}
|
|
512
|
+
|
|
479
513
|
/** The weighted deduction system the graph exploration solves (the four
|
|
480
514
|
* reductions of adapted A*LD live in {@link lightestDerivation}; this only states the
|
|
481
515
|
* items, axioms, goal, and rules — see {@link GItem} for the item kinds).
|
|
@@ -541,8 +575,8 @@ export class GraphSearch {
|
|
|
541
575
|
}`;
|
|
542
576
|
}
|
|
543
577
|
return `o${it.i}.${it.j}.${it.cover ? 1 : 0}.${it.rec ? 1 : 0}.${
|
|
544
|
-
it.
|
|
545
|
-
}.${latin1(it.bytes)}`;
|
|
578
|
+
it.fix ? 1 : 0
|
|
579
|
+
}.${it.node ?? -1}.${latin1(it.bytes)}`;
|
|
546
580
|
},
|
|
547
581
|
*axioms() {
|
|
548
582
|
yield { item: { kind: "cover", p: 0 }, cost: 0 };
|
|
@@ -855,17 +889,29 @@ export class GraphSearch {
|
|
|
855
889
|
// actual end, never per intermediate stop — so its cost tracks the
|
|
856
890
|
// ANSWER's own structure, not how densely the corpus interconnects
|
|
857
891
|
// the nodes passed through on the way there.
|
|
858
|
-
|
|
892
|
+
// MARK the fixpoint; do not re-cover it here. Re-covering at this point
|
|
893
|
+
// pays a full {@link recompleteNode} — a recognition of the node's whole
|
|
894
|
+
// bytes — for every fixpoint the exploration REACHES, and how many it
|
|
895
|
+
// reaches is set by how densely the corpus interconnects the forms passed
|
|
896
|
+
// through. Measured on an 18.9M-node store, a 2-byte query: 12,000
|
|
897
|
+
// re-covers inside ONE cover, folding 95,258 distinct spans, ~2.4 GB and
|
|
898
|
+
// climbing to a V8 fatal. The answer needs a handful.
|
|
899
|
+
//
|
|
900
|
+
// {@link deepen} runs it instead on the spans the lightest derivation
|
|
901
|
+
// CHOSE. The ladder is untouched — this out still costs 0, so a genuine
|
|
902
|
+
// fixpoint still beats any premature stop at the same depth, exactly as
|
|
903
|
+
// the ordering above states.
|
|
859
904
|
yield {
|
|
860
905
|
premises: [it],
|
|
861
906
|
conclusion: {
|
|
862
907
|
kind: "out",
|
|
863
908
|
i: it.i,
|
|
864
909
|
j: it.j,
|
|
865
|
-
bytes:
|
|
910
|
+
bytes: nodeBytes(it.node),
|
|
866
911
|
cover: true,
|
|
867
912
|
rec: true,
|
|
868
913
|
node: it.node,
|
|
914
|
+
fix: true,
|
|
869
915
|
},
|
|
870
916
|
cost: 0,
|
|
871
917
|
};
|
|
@@ -936,18 +982,45 @@ export class GraphSearch {
|
|
|
936
982
|
* ({@link resolve}) — the graph itself gates against re-expanding a contained
|
|
937
983
|
* form ("ice is cold" ⊅→ "ice is cold is cold").
|
|
938
984
|
*
|
|
939
|
-
* Termination is
|
|
940
|
-
*
|
|
941
|
-
*
|
|
942
|
-
*
|
|
943
|
-
*
|
|
944
|
-
*
|
|
985
|
+
* Termination is STRUCTURAL: a produced node is re-covered once, never inside
|
|
986
|
+
* another re-cover (see the guard below), so one cover pays at most one
|
|
987
|
+
* nested {@link solve} per distinct produced node it actually reaches, and
|
|
988
|
+
* {@link recompleteMemo} collapses a repeat to nothing.
|
|
989
|
+
*
|
|
990
|
+
* This comment used to argue termination from "distinct node ids are finite
|
|
991
|
+
* and each finished completion is memoised". That is a bound of N — the one
|
|
992
|
+
* AGENTS §2.8 forbids — and it was load-bearing, not pedantic: nested, the
|
|
993
|
+
* recursion reached depth 331 and 9.1 GB on an 18.9M-node store for a 2-byte
|
|
994
|
+
* query and did not terminate, which is what killed a 5 h training run at its
|
|
995
|
+
* checkpoint recall. Guard: test/89-completion-recursion.test.mjs. */
|
|
945
996
|
private recompleteNode(node: number): Uint8Array | null {
|
|
946
997
|
if (!this.host.recogniseSpan) return null;
|
|
947
998
|
const memo = this.recompleteMemo;
|
|
948
999
|
if (memo.has(node)) return memo.get(node) ?? null;
|
|
949
|
-
//
|
|
950
|
-
|
|
1000
|
+
// ONE re-cover per produced node — never a re-cover inside a re-cover.
|
|
1001
|
+
//
|
|
1002
|
+
// Re-covering is how a PRODUCED node's bytes enter the search at all: the
|
|
1003
|
+
// cover machinery otherwise only ever sees the QUERY's spans. That is
|
|
1004
|
+
// needed once. The alternation of decomposition and recomposition that
|
|
1005
|
+
// follows — parts rewriting several times, siblings fusing, a recomposition
|
|
1006
|
+
// feeding another — is the main search's own fuse/`rcmp` work, not this
|
|
1007
|
+
// recursion's: 15-decomposition-gap §9–§12 all pass with this method
|
|
1008
|
+
// disabled outright, and only §6 (the produced composite "p1 p2", whose
|
|
1009
|
+
// bytes nothing else brings in) needs it.
|
|
1010
|
+
//
|
|
1011
|
+
// Nesting it was the defect. Each level is a full {@link solve} with its
|
|
1012
|
+
// own agenda and chart, exploring from a node the answer never asked about,
|
|
1013
|
+
// so per-query cost tracked how densely the corpus interconnects the forms
|
|
1014
|
+
// passed through — the growth AGENTS §2.8 forbids. Measured on an
|
|
1015
|
+
// 18.9M-node store: depth 331 and 9.1 GB for a 2-byte query, not
|
|
1016
|
+
// terminating; and on the guard corpus every one of 125 nested re-covers
|
|
1017
|
+
// was REJECTED by the resolve() gate below, expanding a 70-byte node into a
|
|
1018
|
+
// 374-byte concatenation that names nothing. All of it was waste.
|
|
1019
|
+
//
|
|
1020
|
+
// `recompleteOpen` is that stack, so a non-empty stack means we are already
|
|
1021
|
+
// inside one. This subsumes the old cycle guard: a node cannot recurse
|
|
1022
|
+
// back into itself when nothing recurses at all.
|
|
1023
|
+
if (this.recompleteOpen.size > 0) return null;
|
|
951
1024
|
|
|
952
1025
|
// A leaf or single-child node has no parts to recompose; skip before the
|
|
953
1026
|
// costly recognition so a plain terminal answer pays nothing.
|
|
@@ -985,9 +1058,13 @@ export class GraphSearch {
|
|
|
985
1058
|
* outs of a long query re-cover each distinct node at most once); reset at the
|
|
986
1059
|
* top of {@link cover}. */
|
|
987
1060
|
private recompleteMemo = new Map<number, Uint8Array | null>();
|
|
988
|
-
/** The
|
|
989
|
-
*
|
|
990
|
-
* (
|
|
1061
|
+
/** The node currently being re-completed — the recursion stack, and so also
|
|
1062
|
+
* the nesting depth. {@link recompleteNode} refuses to start while it is
|
|
1063
|
+
* non-empty (one re-cover per produced node, never one inside another), which
|
|
1064
|
+
* is what keeps a query's cost proportional to its answer rather than to the
|
|
1065
|
+
* corpus; it therefore holds at most one id. A Set, not a flag, because it
|
|
1066
|
+
* states WHICH node is open — the invariant a reader needs to check the
|
|
1067
|
+
* guard, and what makes the old cycle-guard reading still hold. */
|
|
991
1068
|
private recompleteOpen = new Set<number>();
|
|
992
1069
|
|
|
993
1070
|
/** out(i,j,bytes,…): index it for the binary rules, then offer splicing a
|
package/src/mind/junction.ts
CHANGED
|
@@ -207,6 +207,19 @@ function cachedContainers(
|
|
|
207
207
|
* edgeAncestors' question and wrong for this one: a junction container
|
|
208
208
|
* is legitimately reached across many containing structures. Half the
|
|
209
209
|
* successful junctions would be lost.
|
|
210
|
+
*
|
|
211
|
+
* REFUTED EARLY-STOP (side-cone exhaustion, §2.17's "real saturation"):
|
|
212
|
+
* stopping the walk the moment ONE side's upward cone is emptied is wrong,
|
|
213
|
+
* in both a hub-guarded form and a hub-flagged form. The junction test is
|
|
214
|
+
* a BYTE containment over the UNION of the two cones, and a junction can be
|
|
215
|
+
* structurally reachable from only ONE side — the side whose seed is a
|
|
216
|
+
* FOLD sub-node of the container (test/16: "cold or hot" is reached from
|
|
217
|
+
* the window "cold", but the 3-byte answer "hot" is not a 4-byte window of
|
|
218
|
+
* it, so "hot"'s cone is empty while the junction still lies ahead in
|
|
219
|
+
* "cold"'s cone; test/34's n-ary binding fails the hub-guarded form the
|
|
220
|
+
* same way). "One cone exhausted" therefore never proves "no junction
|
|
221
|
+
* left", and the walk must keep the √N·W budget as its NET after the
|
|
222
|
+
* per-node saturations below.
|
|
210
223
|
* • per-node hub guards — parent fan-outs beyond √N are hubs (not
|
|
211
224
|
* expanded); each node contributes at most one √N page of containers;
|
|
212
225
|
* √N collected candidates decide. */
|
|
@@ -251,7 +264,18 @@ export function junctionContainersFrom(
|
|
|
251
264
|
id,
|
|
252
265
|
d: 0,
|
|
253
266
|
}));
|
|
254
|
-
while (stack.length > 0 && out.length < bound
|
|
267
|
+
while (stack.length > 0 && out.length < bound) {
|
|
268
|
+
// BUDGET EXHAUSTION IS AN ABSTENTION, AND IT MUST BE VISIBLE (§2.13). The
|
|
269
|
+
// walk stops with work still on the stack, the caller reads "no container"
|
|
270
|
+
// and falls through to a lower ladder rung — indistinguishable, from the
|
|
271
|
+
// outside, from a walk that looked everywhere and found nothing. With a
|
|
272
|
+
// SHARED budget (cross-region's one k·W allowance per tier) an EARLIER
|
|
273
|
+
// pair can drain it, so a later pair's exact tier may never run at all;
|
|
274
|
+
// this counter is the only thing that says so.
|
|
275
|
+
if (b.n-- <= 0) {
|
|
276
|
+
if (ctx.meter) ctx.meter.junctionBudgetExhausted++;
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
255
279
|
const { id: x, d } = stack.pop()!;
|
|
256
280
|
if (ctx.meter) ctx.meter.junctionPops++;
|
|
257
281
|
const f = cachedRead(ctx, cache, x, maxContainer);
|
|
@@ -59,7 +59,9 @@ export async function resolveConnectors(
|
|
|
59
59
|
// transcript evidence: cover still needs the site for structural context,
|
|
60
60
|
// but liftAnswer will trim that continuation as already answered. Building
|
|
61
61
|
// pairwise/n-ary bridges for it can only create connectors that are later
|
|
62
|
-
// discarded
|
|
62
|
+
// discarded — a semantically neutral gate (it removes work whose product
|
|
63
|
+
// liftAnswer throws away), and a cumulative (multi-turn) query is exactly
|
|
64
|
+
// where such already-answered continuations recur.
|
|
63
65
|
let answered = 0;
|
|
64
66
|
const ordered = [...sites]
|
|
65
67
|
.sort((a, b) => a.start - b.start)
|
|
@@ -72,9 +74,26 @@ export async function resolveConnectors(
|
|
|
72
74
|
if (span && span[0] <= s.start && s.end <= span[1]) return false;
|
|
73
75
|
if (query === undefined || ctx.answeredSpans.length === 0) return true;
|
|
74
76
|
const continuations = ctx.store.nextFirst(s.payload, hubBound(ctx));
|
|
75
|
-
return !continuations.some((answer) =>
|
|
76
|
-
|
|
77
|
-
|
|
77
|
+
return !continuations.some((answer) => {
|
|
78
|
+
// PREFIX-CAPPED (AGENTS §2.8): a candidate longer than the query cannot
|
|
79
|
+
// occur INSIDE it, so read one byte past the query's length — enough to
|
|
80
|
+
// detect the overflow — and reject without reconstructing the rest.
|
|
81
|
+
// The `+ 1` is what makes the test exact rather than a truncation: a
|
|
82
|
+
// result of exactly `query.length + 1` bytes is known to be too long,
|
|
83
|
+
// and anything shorter is the candidate's COMPLETE content, so the
|
|
84
|
+
// substring test below is the same test as before. (The same overflow
|
|
85
|
+
// probe bridge.ts:256 already uses.)
|
|
86
|
+
//
|
|
87
|
+
// This loop runs up to hubBound(ctx) = √N reads PER SITE, and only on a
|
|
88
|
+
// multi-turn response — `answeredSpans` is empty for a plain respond(),
|
|
89
|
+
// so the probe does not execute there. The cap cannot reduce the read
|
|
90
|
+
// COUNT — only a semantic change to the "already answered" test could —
|
|
91
|
+
// but it bounds each read by the query instead of by the corpus, which
|
|
92
|
+
// is what §2.8 asks for and what rescues a SHORT query: at 3 bytes this
|
|
93
|
+
// reads 4 bytes per candidate instead of the ~231 it averaged before.
|
|
94
|
+
const bytes = read(ctx, answer, query.length + 1);
|
|
95
|
+
return bytes.length <= query.length && indexOf(query, bytes, 0) >= 0;
|
|
96
|
+
});
|
|
78
97
|
});
|
|
79
98
|
const bridgePair = async (l: number, r: number) => {
|
|
80
99
|
if (l === r || links.has(l + "," + r)) return;
|
|
@@ -45,15 +45,23 @@
|
|
|
45
45
|
// from `resonate(k)` at k = 24, 256 AND 2048 — while forms scoring LOWER
|
|
46
46
|
// (Germany 0.5670, Yemen 0.5591) are returned. `k` only reorders WITHIN
|
|
47
47
|
// the IVF clusters already probed, exactly as Store.resonate's doc warns,
|
|
48
|
-
// so no k recovers it.
|
|
48
|
+
// so no k recovers it.
|
|
49
49
|
//
|
|
50
|
-
// So this is a RETRIEVABILITY gap, not a semantic one, and
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
50
|
+
// So this is a RETRIEVABILITY gap, not a semantic one, and the ANN is the wrong
|
|
51
|
+
// instrument for it: a proper prefix's gist cannot rank its own continuation.
|
|
52
|
+
// The repair is CONTENT-ADDRESSED (§2.3) — `formsOpenedBy` (traverse.ts) reads
|
|
53
|
+
// the leaf-id WINDOW index the write side already maintains and answers "which
|
|
54
|
+
// trained forms does this byte run open?" in a bounded √N walk. That is this
|
|
55
|
+
// mechanism's first supply. The response's memoised top-k `resonance()` is the
|
|
56
|
+
// second, for prefixes long enough that the gist still ranks the form; it is
|
|
57
|
+
// read, never re-issued.
|
|
58
|
+
//
|
|
59
|
+
// AN EXHAUSTIVE ANN LIST IS NOT A SUPPLY HERE, AND WAS REMOVED. This tier once
|
|
60
|
+
// read `Precomputed.wideResonance()` — a full-index `resonate(guide, √N,
|
|
61
|
+
// exhaustive)` — on the argument that the target "ranks 8 with `exhaustive`".
|
|
62
|
+
// It bought an O(k) need at O(index) cost (measured: 244K annVectorReads per
|
|
63
|
+
// refusing query, ~1.5 s) for candidates the window index proposes directly.
|
|
64
|
+
// See pipeline-mechanism.ts's REMOVED note; test/95 pins its absence.
|
|
57
65
|
//
|
|
58
66
|
// THREE GUARDS, each falsified into existence by measurement — do not drop any:
|
|
59
67
|
//
|
|
@@ -114,7 +122,8 @@ export interface PrefixCompletion {
|
|
|
114
122
|
* it, when the continuation is sub-quantum, when a candidate's continuation
|
|
115
123
|
* cannot be read through, or when the candidates disagree.
|
|
116
124
|
*
|
|
117
|
-
* `ranked` must be a list the caller has ALREADY fetched
|
|
125
|
+
* `ranked` must be a list the caller has ALREADY fetched (the write side's
|
|
126
|
+
* window index, or the response's memoised top-k); this mechanism never
|
|
118
127
|
* resonates on its own (see the header's cost note). */
|
|
119
128
|
export function prefixCompletion(
|
|
120
129
|
ctx: MindContext,
|
|
@@ -251,9 +260,9 @@ export const prefixMechanism: PipelineMechanism = {
|
|
|
251
260
|
provenance: "prefix",
|
|
252
261
|
async floor(ctx, query, _pre, worthRunning) {
|
|
253
262
|
// One projection: the form is voiced whole, nothing is substituted.
|
|
254
|
-
// INVESTMENT DISCIPLINE — the supplies below are
|
|
255
|
-
//
|
|
256
|
-
// bound can still beat the incumbent.
|
|
263
|
+
// INVESTMENT DISCIPLINE — the supplies below are a bounded √N window walk
|
|
264
|
+
// and the response's memoised top-k resonance read, so neither is touched
|
|
265
|
+
// until the bound can still beat the incumbent.
|
|
257
266
|
if (!worthRunning(STEP)) return STEP;
|
|
258
267
|
// A query with no room for a perceivable continuation inside the phrase
|
|
259
268
|
// cap cannot clear guard 2, so it is not worth a single read.
|
|
@@ -264,14 +273,25 @@ export const prefixMechanism: PipelineMechanism = {
|
|
|
264
273
|
return STEP;
|
|
265
274
|
},
|
|
266
275
|
async run(ctx, query, pre) {
|
|
267
|
-
//
|
|
268
|
-
// the
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
//
|
|
272
|
-
// guards
|
|
273
|
-
|
|
274
|
-
|
|
276
|
+
// ONE SUPPLY PASS, not a two-tier `??`. The window index (exact,
|
|
277
|
+
// content-addressed) and the response's memoised top-k (approximate) are
|
|
278
|
+
// concatenated and the three guards decide ONCE over the union. A
|
|
279
|
+
// first-then-fallback chain would let the APPROXIMATE tier override the
|
|
280
|
+
// EXACT one (§2.3): when formsOpenedBy finds two continuations, guard 3
|
|
281
|
+
// returns null and the fallback re-runs the guards on resonance's top-k
|
|
282
|
+
// alone — which, seeing only one of the two forms, would voice it. That is
|
|
283
|
+
// precisely the disagreement-suppression guard 3 exists to prevent, and it
|
|
284
|
+
// is the exact tier's ambiguity being washed away by the approximate tier.
|
|
285
|
+
// Evaluating the union means a disagreement the window index saw can never
|
|
286
|
+
// be hidden by what the ANN happens to rank. The ANN read is the
|
|
287
|
+
// response's ONE memoised top-k (§2.11), already paid by recall's refusal
|
|
288
|
+
// path on the queries where this mechanism fires, so reading it here is not
|
|
289
|
+
// a second index scan.
|
|
290
|
+
const ids = [
|
|
291
|
+
...formsOpenedBy(ctx, query),
|
|
292
|
+
...(await pre.resonance()).map((h) => h.id),
|
|
293
|
+
];
|
|
294
|
+
const completed = prefixCompletion(ctx, query, ids);
|
|
275
295
|
if (completed === null) return [];
|
|
276
296
|
return [{
|
|
277
297
|
bytes: completed.form,
|
|
@@ -412,10 +412,14 @@ export async function recallByResonance(
|
|
|
412
412
|
}
|
|
413
413
|
}
|
|
414
414
|
// 3b. Corroborated-substitution bridge — refusal-path only (bridge.ts).
|
|
415
|
-
// The
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
|
|
415
|
+
// The bridge's proposal source is the response's ONE top-k read — the same
|
|
416
|
+
// list recall already ranked above — never an exhaustive √N scan. The
|
|
417
|
+
// bridge's own candidate cap is 2·recallQueryK, so top-k proposals are
|
|
418
|
+
// exactly the budget it can consume, and every proposal is byte-verified
|
|
419
|
+
// downstream (§2.3). Reuse the memoised `resonance()`; scanning every IVF
|
|
420
|
+
// cluster here once made every honest refusal cost hundreds of ms regardless
|
|
421
|
+
// of k.
|
|
422
|
+
const wideIds = async () => (await pre.resonance()).map((h) => h.id);
|
|
419
423
|
|
|
420
424
|
// Every gist-based tier has failed; before refusing, align the query
|
|
421
425
|
// byte-for-byte against the trained contexts its own stored windows
|