@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
package/dist/src/mind/mind.js
CHANGED
|
@@ -29,6 +29,9 @@ import { aluToMechanism, defaultMechanisms, think } from "./pipeline.js";
|
|
|
29
29
|
import { articulate } from "./articulation.js";
|
|
30
30
|
import { ingest } from "./learning.js";
|
|
31
31
|
import { rItem } from "./trace.js";
|
|
32
|
+
// The work meter is exported from src/index.ts (via src/meter.ts) — the one
|
|
33
|
+
// definition; the Mind only consumes it.
|
|
34
|
+
import { Meter } from "../meter.js";
|
|
32
35
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
33
36
|
// THE MIND
|
|
34
37
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -53,6 +56,19 @@ export class Mind {
|
|
|
53
56
|
* `false` to disable canonical resolution, or null to let each entry
|
|
54
57
|
* point decide (text entry points inject {@link textCanon}). */
|
|
55
58
|
_canonOpt = null;
|
|
59
|
+
/** The work accumulator for the inference call in flight — see
|
|
60
|
+
* {@link MindContext.meter}. Non-null only between beginResponse and
|
|
61
|
+
* endResponse, and only when the Mind was constructed with
|
|
62
|
+
* `{ profile: true }`. */
|
|
63
|
+
meter = null;
|
|
64
|
+
/** Whether {@link MindOptions.profile} was set. */
|
|
65
|
+
_profile = false;
|
|
66
|
+
/** The computational-usage report of the LAST completed inference call, or
|
|
67
|
+
* null when profiling is off (or nothing has been asked yet). Overwritten
|
|
68
|
+
* by every `respond`/`respondTurn`; copy it if you are aggregating. See
|
|
69
|
+
* {@link import("../meter.js").CostReport} and `sumReports`/`formatReport`
|
|
70
|
+
* for battery-level aggregation. */
|
|
71
|
+
lastCost = null;
|
|
56
72
|
/** Memo of the consensus climb — content-keyed. See {@link MindContext.climbMemo}. */
|
|
57
73
|
climbMemo = null;
|
|
58
74
|
/** Memo of recognise() — content-keyed. See {@link MindContext.recogniseMemo}. */
|
|
@@ -120,8 +136,9 @@ export class Mind {
|
|
|
120
136
|
this.store = storeArg;
|
|
121
137
|
}
|
|
122
138
|
else {
|
|
123
|
-
const { store: optsStore, mechanisms: userMechs, mechanismFactories: userFacts, canon: optsCanon, ...rest } = (optsOrCfg ?? {});
|
|
139
|
+
const { store: optsStore, mechanisms: userMechs, mechanismFactories: userFacts, canon: optsCanon, profile: optsProfile, ...rest } = (optsOrCfg ?? {});
|
|
124
140
|
this._canonOpt = optsCanon ?? null;
|
|
141
|
+
this._profile = optsProfile === true;
|
|
125
142
|
this.cfg = resolveConfig(rest);
|
|
126
143
|
this.store = optsStore ?? new SQliteStore({
|
|
127
144
|
maxGroup: this.cfg.geometry.maxGroup,
|
|
@@ -180,16 +197,44 @@ export class Mind {
|
|
|
180
197
|
perceive(input, leafAt, lookup) {
|
|
181
198
|
return perceiveImpl(this, input, leafAt, lookup);
|
|
182
199
|
}
|
|
183
|
-
/** Open one response's transient state — the tracer
|
|
184
|
-
* memos
|
|
185
|
-
*
|
|
186
|
-
|
|
200
|
+
/** Open one response's transient state — the tracer, the per-response
|
|
201
|
+
* memos, the work meter. The ONE place this state is created, and it
|
|
202
|
+
* serves BOTH entry points: `respond` takes fresh per-response memos,
|
|
203
|
+
* `respondTurn` passes its conversation, whose memos persist across turns
|
|
204
|
+
* (content-keyed, so the previous turn's results are found by this turn's
|
|
205
|
+
* sub-span calls) and whose `resolvedSubtrees` makes foldTree O(suffix)
|
|
206
|
+
* instead of O(context). respondTurn used to inline its own copy of this
|
|
207
|
+
* and of {@link endResponse}; the two drifted (a memo added to one was
|
|
208
|
+
* silently absent from the other), so there is exactly one pair now. */
|
|
209
|
+
beginResponse(inspectRationale, canon, conv) {
|
|
187
210
|
this.trace = inspectRationale ? new Rationale(inspectRationale) : null;
|
|
188
|
-
this.climbMemo = new Map();
|
|
189
|
-
this.recogniseMemo = new Map();
|
|
190
|
-
this.perceiveMemo = new Map();
|
|
211
|
+
this.climbMemo = conv ? conv.climbMemo : new Map();
|
|
212
|
+
this.recogniseMemo = conv ? conv.recogniseMemo : new Map();
|
|
213
|
+
this.perceiveMemo = conv ? conv.perceiveMemo : new Map();
|
|
214
|
+
this._resolvedSubtrees = conv ? conv.resolvedSubtrees : null;
|
|
191
215
|
this.canon = canon ?? null;
|
|
192
216
|
this.canonMemo = canon ? new Map() : null;
|
|
217
|
+
this._beginMeter();
|
|
218
|
+
}
|
|
219
|
+
/** Open (or leave closed) the response's work accumulator. Separate from
|
|
220
|
+
* {@link beginResponse} because {@link respondTurn} keeps its own
|
|
221
|
+
* conversation-scoped lifecycle and must not create fresh per-response
|
|
222
|
+
* memos — but it DOES meter, through this same pair. */
|
|
223
|
+
_beginMeter() {
|
|
224
|
+
if (!this._profile)
|
|
225
|
+
return;
|
|
226
|
+
this.meter = new Meter();
|
|
227
|
+
this.store.meter = this.meter;
|
|
228
|
+
}
|
|
229
|
+
/** Close the accumulator and publish its report. Detaching from the store
|
|
230
|
+
* matters: a Mind that shares a store with another Mind must not keep
|
|
231
|
+
* charging that store's reads to a finished response. */
|
|
232
|
+
_endMeter(queryBytes) {
|
|
233
|
+
if (this.meter === null)
|
|
234
|
+
return;
|
|
235
|
+
this.lastCost = this.meter.report(queryBytes);
|
|
236
|
+
this.store.meter = null;
|
|
237
|
+
this.meter = null;
|
|
193
238
|
}
|
|
194
239
|
/** The canonicalizer a response should carry: the Mind-level option when
|
|
195
240
|
* set (or none when explicitly disabled), else the entry point's own
|
|
@@ -200,12 +245,20 @@ export class Mind {
|
|
|
200
245
|
return this._canonOpt ?? entryDefault;
|
|
201
246
|
}
|
|
202
247
|
/** Close one response's transient state — every per-response field, incl.
|
|
203
|
-
* the edge guide/choices `think` sets mid-flight.
|
|
204
|
-
|
|
248
|
+
* the edge guide/choices `think` sets mid-flight, and the meter's report.
|
|
249
|
+
*
|
|
250
|
+
* A conversation's memo MAPS were mutated in place, so `data.*` still
|
|
251
|
+
* points at them and there is nothing to save back. Clearing the Mind's
|
|
252
|
+
* references is what matters: a concurrently-started `respond()` swaps its
|
|
253
|
+
* own fresh maps into these pointers, and copying back from them here
|
|
254
|
+
* would inject a foreign response's memos into the conversation. */
|
|
255
|
+
endResponse(queryBytes) {
|
|
256
|
+
this._endMeter(queryBytes);
|
|
205
257
|
this.trace = null;
|
|
206
258
|
this.climbMemo = null;
|
|
207
259
|
this.recogniseMemo = null;
|
|
208
260
|
this.perceiveMemo = null;
|
|
261
|
+
this._resolvedSubtrees = null;
|
|
209
262
|
this.canon = null;
|
|
210
263
|
this.canonMemo = null;
|
|
211
264
|
this._edgeGuide = null;
|
|
@@ -217,26 +270,38 @@ export class Mind {
|
|
|
217
270
|
async _respondImpl(queryBytes, inspectRationale, traceLabel = "respond", canon = null) {
|
|
218
271
|
this.beginResponse(inspectRationale, canon);
|
|
219
272
|
try {
|
|
220
|
-
|
|
221
|
-
rItem(queryBytes, "query"),
|
|
222
|
-
]);
|
|
223
|
-
const thought = await think(this, queryBytes, this.mechanisms);
|
|
224
|
-
if (thought === null) {
|
|
225
|
-
top?.done([], "nothing to perceive or an empty store — no answer");
|
|
226
|
-
return { v: null, bytes: new Uint8Array(0) };
|
|
227
|
-
}
|
|
228
|
-
const voiced = await articulate(this, thought.bytes, queryBytes);
|
|
229
|
-
top?.done([rItem(voiced, "answer", resolveImpl(this, voiced) ?? undefined)], "the answer, re-voiced in the asker's words");
|
|
230
|
-
return {
|
|
231
|
-
v: gistOf(this, voiced),
|
|
232
|
-
bytes: voiced,
|
|
233
|
-
provenance: thought.provenance,
|
|
234
|
-
};
|
|
273
|
+
return await this._groundAndVoice(queryBytes, traceLabel);
|
|
235
274
|
}
|
|
236
275
|
finally {
|
|
237
|
-
this.endResponse();
|
|
276
|
+
this.endResponse(queryBytes.length);
|
|
238
277
|
}
|
|
239
278
|
}
|
|
279
|
+
/** The ONE path from query bytes to a voiced answer: ground (think), then
|
|
280
|
+
* re-voice in the asker's words (articulate). Both entry points run
|
|
281
|
+
* exactly this — they differ only in the LIFECYCLE around it (fresh
|
|
282
|
+
* per-response memos vs. a conversation's persistent ones) and in what
|
|
283
|
+
* they do with the answer afterwards. It must be called between
|
|
284
|
+
* {@link beginResponse} and {@link endResponse}. */
|
|
285
|
+
async _groundAndVoice(queryBytes, traceLabel) {
|
|
286
|
+
const top = this.trace?.enter(traceLabel, [rItem(queryBytes, "query")]);
|
|
287
|
+
const meter = this.meter;
|
|
288
|
+
const thought = meter
|
|
289
|
+
? await meter.time("think", () => think(this, queryBytes, this.mechanisms))
|
|
290
|
+
: await think(this, queryBytes, this.mechanisms);
|
|
291
|
+
if (thought === null) {
|
|
292
|
+
top?.done([], "nothing to perceive or an empty store — no answer");
|
|
293
|
+
return { v: null, bytes: new Uint8Array(0) };
|
|
294
|
+
}
|
|
295
|
+
const voiced = meter
|
|
296
|
+
? await meter.time("articulate", () => articulate(this, thought.bytes, queryBytes))
|
|
297
|
+
: await articulate(this, thought.bytes, queryBytes);
|
|
298
|
+
top?.done([rItem(voiced, "answer", resolveImpl(this, voiced) ?? undefined)], "the answer, re-voiced in the asker's words");
|
|
299
|
+
return {
|
|
300
|
+
v: gistOf(this, voiced),
|
|
301
|
+
bytes: voiced,
|
|
302
|
+
provenance: thought.provenance,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
240
305
|
async respond(input, inspectRationale) {
|
|
241
306
|
// A STRING input is text by nature: it carries the text equivalence even
|
|
242
307
|
// through the generic entry point. Raw bytes / grids carry only the
|
|
@@ -358,82 +423,36 @@ export class Mind {
|
|
|
358
423
|
throw new Error(`Conversation ${conv.id} not found`);
|
|
359
424
|
const turnBytes = inputBytes(this, turn);
|
|
360
425
|
// Incremental perception — O(turn) instead of O(context).
|
|
361
|
-
|
|
426
|
+
this._growContext(data, turnBytes);
|
|
362
427
|
const newContext = data.bytes;
|
|
363
|
-
//
|
|
364
|
-
//
|
|
365
|
-
//
|
|
366
|
-
//
|
|
367
|
-
// the current turn's sub-span calls.
|
|
368
|
-
// _resolvedSubtrees — Sema-node-keyed; when the pyramid reuses
|
|
369
|
-
// prefix subtrees (identical objects), foldTree returns their
|
|
370
|
-
// ids immediately — O(suffix) instead of O(context).
|
|
371
|
-
this.perceiveMemo = data.perceiveMemo;
|
|
372
|
-
this.recogniseMemo = data.recogniseMemo;
|
|
373
|
-
this.climbMemo = data.climbMemo;
|
|
374
|
-
this._resolvedSubtrees = data.resolvedSubtrees;
|
|
375
|
-
this.trace = inspectRationale ? new Rationale(inspectRationale) : null;
|
|
376
|
-
// A string turn is text by nature — carry the text equivalence, same as
|
|
428
|
+
// The conversation's persistent memos and subtree cache are swapped in
|
|
429
|
+
// by beginResponse (see there) — the SAME lifecycle respond() uses, so a
|
|
430
|
+
// memo added in one place can never be missing from the other. A string
|
|
431
|
+
// turn is text by nature and carries the text equivalence, same as
|
|
377
432
|
// respond() (see _canonFor).
|
|
378
|
-
|
|
379
|
-
|
|
433
|
+
//
|
|
434
|
+
// No recognise-memo pre-seeding here: that used to be necessary because
|
|
435
|
+
// the flat/positional fold lost visibility into an earlier turn's own
|
|
436
|
+
// structure once later bytes shifted its position (foldTree no longer
|
|
437
|
+
// visited the turn's root node). The STABLE-PREFIX fold (see {@link
|
|
438
|
+
// ConversationData}) makes every turn's subtree independent of what
|
|
439
|
+
// follows it by construction, so recognise() finds it correctly on its
|
|
440
|
+
// own, first-touch, exactly once per turn.
|
|
441
|
+
this.beginResponse(inspectRationale, this._canonFor(typeof turn === "string" ? textCanon : null), data);
|
|
380
442
|
try {
|
|
381
|
-
|
|
382
|
-
// because the flat/positional fold lost visibility into an earlier
|
|
383
|
-
// turn's own structure once later bytes shifted its position
|
|
384
|
-
// (foldTree no longer visited the turn's root node). The
|
|
385
|
-
// STABLE-PREFIX fold (see {@link ConversationData}) makes every
|
|
386
|
-
// turn's subtree independent of what follows it by construction, so
|
|
387
|
-
// recognise() finds it correctly on its own, first-touch, exactly
|
|
388
|
-
// once per turn — no workaround needed, and none pre-empting
|
|
389
|
-
// recognise()'s own memo with a partial result.
|
|
390
|
-
const top = this.trace?.enter("respondTurn", [
|
|
391
|
-
rItem(newContext, "query"),
|
|
392
|
-
]);
|
|
393
|
-
const thought = await think(this, newContext, this.mechanisms);
|
|
394
|
-
if (thought === null) {
|
|
395
|
-
top?.done([], "nothing to perceive or an empty store — no answer");
|
|
396
|
-
return {
|
|
397
|
-
response: { v: null, bytes: new Uint8Array(0) },
|
|
398
|
-
state: this.conversationState(conv),
|
|
399
|
-
};
|
|
400
|
-
}
|
|
401
|
-
const voiced = await articulate(this, thought.bytes, newContext);
|
|
402
|
-
top?.done([rItem(voiced, "answer", resolveImpl(this, voiced) ?? undefined)], "the answer, re-voiced in the asker's words");
|
|
443
|
+
const response = await this._groundAndVoice(newContext, "respondTurn");
|
|
403
444
|
// The REPLY joins the accumulated context the same way a turn does
|
|
404
445
|
// ({@link addTurn}): raw byte append plus a boundary offset — never a
|
|
405
446
|
// separator. A conversation's context is the full exchange, exactly
|
|
406
447
|
// the cumulative continuous shape multi-turn training deposits, so a
|
|
407
448
|
// later turn can refer to what was ANSWERED ("which of those two…"),
|
|
408
449
|
// not only to what was asked.
|
|
409
|
-
if (
|
|
410
|
-
this.addTurn(conv,
|
|
411
|
-
return {
|
|
412
|
-
response: {
|
|
413
|
-
v: gistOf(this, voiced),
|
|
414
|
-
bytes: voiced,
|
|
415
|
-
provenance: thought.provenance,
|
|
416
|
-
},
|
|
417
|
-
state: this.conversationState(conv),
|
|
418
|
-
};
|
|
450
|
+
if (response.bytes.length > 0)
|
|
451
|
+
this.addTurn(conv, response.bytes);
|
|
452
|
+
return { response, state: this.conversationState(conv) };
|
|
419
453
|
}
|
|
420
454
|
finally {
|
|
421
|
-
|
|
422
|
-
// `data.*` still points at them. Re-assigning from the Mind-level
|
|
423
|
-
// pointers here was a no-op in the single-flight case and, under a
|
|
424
|
-
// concurrently-started respond() (which swaps its own fresh maps into
|
|
425
|
-
// those pointers), would inject a FOREIGN response's memos into this
|
|
426
|
-
// conversation. Clear Mind references — non-conversation respond()
|
|
427
|
-
// calls get fresh per-response memos.
|
|
428
|
-
this.trace = null;
|
|
429
|
-
this.perceiveMemo = null;
|
|
430
|
-
this.recogniseMemo = null;
|
|
431
|
-
this.climbMemo = null;
|
|
432
|
-
this.canon = null;
|
|
433
|
-
this.canonMemo = null;
|
|
434
|
-
this._resolvedSubtrees = null;
|
|
435
|
-
this._edgeGuide = null;
|
|
436
|
-
this._edgeChoice.clear();
|
|
455
|
+
this.endResponse(newContext.length);
|
|
437
456
|
}
|
|
438
457
|
}
|
|
439
458
|
/** Text view of {@link respondTurn}. See {@link respondText} for the
|
|
@@ -37,9 +37,21 @@ export declare class Precomputed {
|
|
|
37
37
|
* any future identity-based mechanism reads the same cache. */
|
|
38
38
|
windowsOf(anchor: number): Map<number, number>;
|
|
39
39
|
/** Shared memo for {@link reachOf} (structural-IDF reads): a window's
|
|
40
|
-
* ancestor reach is a pure function of the read-only store, so one
|
|
41
|
-
*
|
|
42
|
-
|
|
40
|
+
* ancestor reach is a pure function of the read-only store, so one memo
|
|
41
|
+
* serves every mechanism that prices commonality — AND the consensus
|
|
42
|
+
* climb, which is the largest consumer and used to build its own. The
|
|
43
|
+
* ONE definition of its lifetime lives in traverse.ts
|
|
44
|
+
* ({@link sharedReachMemo}): response-scoped for respond(),
|
|
45
|
+
* conversation-scoped across turns, always cold under a trace. */
|
|
46
|
+
private _reach?;
|
|
47
|
+
get reachMemo(): Map<number, AncestorReach>;
|
|
48
|
+
/** Charge a lazily-shared analysis to its OWN phase rather than to the
|
|
49
|
+
* mechanism that happened to first-touch it. Without this the profile
|
|
50
|
+
* reads as "cast.floor costs 2 s" when what actually cost 2 s is the
|
|
51
|
+
* consensus climb — which cast merely paid for on everyone's behalf, and
|
|
52
|
+
* which every later consumer then got free. Attribution must follow the
|
|
53
|
+
* work, not the caller. */
|
|
54
|
+
private shared;
|
|
43
55
|
private _attention?;
|
|
44
56
|
/** The full consensus climb (roots + ranked anchors) — the query-level
|
|
45
57
|
* evidence CAST, confluence, extraction, recall's scaffolding tier, and
|
|
@@ -109,6 +121,24 @@ export interface MechanismResult {
|
|
|
109
121
|
/** Override the mechanism's default provenance for this result.
|
|
110
122
|
* When absent, the pipeline uses `mech.provenance`. */
|
|
111
123
|
provenance?: string;
|
|
124
|
+
/** This grounding is a COMPLETE trained answer — post-grounding must not
|
|
125
|
+
* extend it. Declared by the mechanism about its own result, exactly like
|
|
126
|
+
* `accounted`/`used`/`unexplained`; the decider honours the property and
|
|
127
|
+
* never asks which mechanism set it, so the market stays uniform.
|
|
128
|
+
*
|
|
129
|
+
* Set it only when the answer is a stored form's OWN continuation reached
|
|
130
|
+
* through an identity claim about the query — i.e. the query IS some
|
|
131
|
+
* trained context, so its continuation is the whole read-out and a further
|
|
132
|
+
* multi-hop pivot would chain PAST the fact that produced the answer.
|
|
133
|
+
* That is the same reasoning `reason`'s echo guard already applies to a
|
|
134
|
+
* query that resolves exactly (see reasoning.ts); this carries the claim
|
|
135
|
+
* for the mechanisms that establish the identity by another route.
|
|
136
|
+
*
|
|
137
|
+
* Observed without it: the correct "What is the process of
|
|
138
|
+
* photosynthesis?" grounding was pivoted forward four times, out of the
|
|
139
|
+
* fact that answered it and into an unrelated "Hello! How can I assist you
|
|
140
|
+
* today?" conversational turn. */
|
|
141
|
+
complete?: boolean;
|
|
112
142
|
}
|
|
113
143
|
export interface PipelineMechanism {
|
|
114
144
|
/** Stable identifier for trace/debug. */
|
|
@@ -12,11 +12,13 @@
|
|
|
12
12
|
// 3. VISIBLE BUDGET — every mechanism carries its own caps internally (√N, k).
|
|
13
13
|
// 4. TRAVELING EVIDENCE — run() returns MechanismResult with accounted, moves,
|
|
14
14
|
// and unexplained. The pipeline computes the weight.
|
|
15
|
+
import { indexOf } from "../bytes.js";
|
|
16
|
+
import { dominates } from "../geometry.js";
|
|
15
17
|
import { windowIds } from "./canonical.js";
|
|
16
18
|
import { read, resolve } from "./primitives.js";
|
|
17
|
-
import { alignGraded } from "./match.js";
|
|
19
|
+
import { alignGraded, skillExemplar } from "./match.js";
|
|
18
20
|
import { climbAttentionAll } from "./attention.js";
|
|
19
|
-
import {
|
|
21
|
+
import { sharedReachMemo } from "./traverse.js";
|
|
20
22
|
// ── Precomputed ──────────────────────────────────────────────────────────────
|
|
21
23
|
//
|
|
22
24
|
// Precomputed is a LAZY container for structural analyses of the query — the
|
|
@@ -88,29 +90,46 @@ export class Precomputed {
|
|
|
88
90
|
return w;
|
|
89
91
|
}
|
|
90
92
|
/** Shared memo for {@link reachOf} (structural-IDF reads): a window's
|
|
91
|
-
* ancestor reach is a pure function of the read-only store, so one
|
|
92
|
-
*
|
|
93
|
-
|
|
93
|
+
* ancestor reach is a pure function of the read-only store, so one memo
|
|
94
|
+
* serves every mechanism that prices commonality — AND the consensus
|
|
95
|
+
* climb, which is the largest consumer and used to build its own. The
|
|
96
|
+
* ONE definition of its lifetime lives in traverse.ts
|
|
97
|
+
* ({@link sharedReachMemo}): response-scoped for respond(),
|
|
98
|
+
* conversation-scoped across turns, always cold under a trace. */
|
|
99
|
+
_reach;
|
|
100
|
+
get reachMemo() {
|
|
101
|
+
return this._reach ??= sharedReachMemo(this.ctx);
|
|
102
|
+
}
|
|
94
103
|
// ── Expensive lazy analyses ───────────────────────────────────────────
|
|
95
104
|
//
|
|
96
105
|
// Async, cached-by-promise: the first caller starts the computation, every
|
|
97
106
|
// later caller (any mechanism, any phase) awaits the same promise. A
|
|
98
107
|
// mechanism MUST check its cheap floor gates and the pipeline's
|
|
99
108
|
// `worthRunning` predicate before first-touching one of these.
|
|
109
|
+
/** Charge a lazily-shared analysis to its OWN phase rather than to the
|
|
110
|
+
* mechanism that happened to first-touch it. Without this the profile
|
|
111
|
+
* reads as "cast.floor costs 2 s" when what actually cost 2 s is the
|
|
112
|
+
* consensus climb — which cast merely paid for on everyone's behalf, and
|
|
113
|
+
* which every later consumer then got free. Attribution must follow the
|
|
114
|
+
* work, not the caller. */
|
|
115
|
+
shared(phase, fn) {
|
|
116
|
+
const meter = this.ctx.meter;
|
|
117
|
+
return meter ? meter.time(phase, fn) : fn();
|
|
118
|
+
}
|
|
100
119
|
_attention;
|
|
101
120
|
/** The full consensus climb (roots + ranked anchors) — the query-level
|
|
102
121
|
* evidence CAST, confluence, extraction, recall's scaffolding tier, and
|
|
103
122
|
* fusion all share. Computed on first access; a query no mechanism
|
|
104
123
|
* climbs for (e.g. one an extension decided outright) never pays for it. */
|
|
105
124
|
attention() {
|
|
106
|
-
return this._attention ??= climbAttentionAll(this.ctx, this.query, this.k);
|
|
125
|
+
return this._attention ??= this.shared("attention", () => climbAttentionAll(this.ctx, this.query, this.k));
|
|
107
126
|
}
|
|
108
127
|
_weave;
|
|
109
128
|
/** Result of {@link alignGraded} for the first k ranked anchors —
|
|
110
129
|
* O(k · |query| · |ctx|). Consumed by CAST; reusable by any future
|
|
111
130
|
* mechanism doing analogical transfer. */
|
|
112
131
|
weave() {
|
|
113
|
-
return this._weave ??= this.attention().then((climb) => computeWeave(this.ctx, this.query, this, climb));
|
|
132
|
+
return this._weave ??= this.attention().then((climb) => this.shared("weave", async () => computeWeave(this.ctx, this.query, this, climb)));
|
|
114
133
|
}
|
|
115
134
|
/** Span-shaped classification of one ranked anchor, memoised per anchor id
|
|
116
135
|
* so repeated calls (extraction's own early-exit scan, any future
|
|
@@ -124,7 +143,7 @@ export class Precomputed {
|
|
|
124
143
|
spanShapedOf(anchor) {
|
|
125
144
|
let p = this._spanShaped.get(anchor);
|
|
126
145
|
if (p === undefined) {
|
|
127
|
-
p = skillExemplar(this.ctx, anchor, this.guide);
|
|
146
|
+
p = this.shared("spanShaped", () => skillExemplar(this.ctx, anchor, this.guide));
|
|
128
147
|
this._spanShaped.set(anchor, p);
|
|
129
148
|
}
|
|
130
149
|
return p;
|
|
@@ -152,6 +171,7 @@ function computeWeave(ctx, query, pre, climb) {
|
|
|
152
171
|
const rankedCapped = ranked.length > pre.k ? ranked.slice(0, pre.k) : ranked;
|
|
153
172
|
const depth = new Float64Array(query.length);
|
|
154
173
|
const points = [];
|
|
174
|
+
const byAnchor = new Map();
|
|
155
175
|
// WEAVE-SCALE anchors only: CAST transfers structure between things the
|
|
156
176
|
// QUERY weaves together — query-scale structures. A context an order of
|
|
157
177
|
// magnitude beyond the query is not woven BY the query (the query can at
|
|
@@ -166,6 +186,34 @@ function computeWeave(ctx, query, pre, climb) {
|
|
|
166
186
|
// uncapped weaves spent 5–8s per query recognising conversation-length
|
|
167
187
|
// anchors that could never form a weave point.
|
|
168
188
|
const capBytes = query.length * quantum;
|
|
189
|
+
// EXCLUSIVITY IS ARBITRATED BY THE CLIMB'S VOTE ORDER, DELIBERATELY. A query
|
|
190
|
+
// byte can only be independent evidence for ONE point, so points are built in
|
|
191
|
+
// ranked order and each new point's runs are trimmed against every point
|
|
192
|
+
// already accepted; a point left with no run of a full quantum drops out of
|
|
193
|
+
// the weave.
|
|
194
|
+
//
|
|
195
|
+
// That reads like first-come-wins — a point that merely ranked higher taking
|
|
196
|
+
// a span from the point that actually explains it — and arbitrating by LOCAL
|
|
197
|
+
// evidence instead (ownership of each byte to the longest covering run, then
|
|
198
|
+
// the heavier weight, then rank) was implemented and MEASURED: test/29 went
|
|
199
|
+
// 9/2 to 7/4, and the new failures name the reason. CAST requires the weave
|
|
200
|
+
// to touch a COMMITTED point of attention ("2 aligned structure(s), but none
|
|
201
|
+
// is one of the climb's 1 committed root(s)"), and it was precisely the vote
|
|
202
|
+
// order that kept the committed root's own point alive in the weave. Local
|
|
203
|
+
// run length knows nothing about what the climb settled on, so it evicted the
|
|
204
|
+
// root's evidence and left CAST refusing on its own consistency check.
|
|
205
|
+
//
|
|
206
|
+
// So the vote order here is not an accident of construction — it is what
|
|
207
|
+
// holds the weave and the climb to the same conclusion. Weave-local
|
|
208
|
+
// measures decide what is FRAME inside the weave (see the frame gates in
|
|
209
|
+
// cast.ts); which structures are in the weave at all stays the climb's call.
|
|
210
|
+
//
|
|
211
|
+
// TWO PASSES. `depth` — how much of the weave agrees on each query byte, and
|
|
212
|
+
// therefore what counts as FRAME — must be the whole weave's, not "whatever
|
|
213
|
+
// has been processed so far": read in one pass it made a candidate's own
|
|
214
|
+
// frame reading depend on its rank, and the proposed-run gate below needs the
|
|
215
|
+
// real thing.
|
|
216
|
+
const cands = [];
|
|
169
217
|
for (const cand of rankedCapped) {
|
|
170
218
|
const ctxBytes = read(ctx, cand.anchor, capBytes + 1);
|
|
171
219
|
if (ctxBytes.length === 0 || ctxBytes.length > capBytes)
|
|
@@ -177,6 +225,9 @@ function computeWeave(ctx, query, pre, climb) {
|
|
|
177
225
|
for (let i = r.qs; i < r.qe; i++)
|
|
178
226
|
depth[i] += r.weight;
|
|
179
227
|
}
|
|
228
|
+
cands.push({ cand, ctxBytes, raw });
|
|
229
|
+
}
|
|
230
|
+
for (const { cand, ctxBytes, raw } of cands) {
|
|
180
231
|
const free = [];
|
|
181
232
|
for (const r of raw) {
|
|
182
233
|
let { qs, qe, cs, weight } = r;
|
|
@@ -201,12 +252,130 @@ function computeWeave(ctx, query, pre, climb) {
|
|
|
201
252
|
}
|
|
202
253
|
}
|
|
203
254
|
if (free.length > 0) {
|
|
204
|
-
|
|
255
|
+
const pt = {
|
|
205
256
|
anchor: cand.anchor,
|
|
206
257
|
vote: cand.vote,
|
|
207
258
|
ctx: ctxBytes,
|
|
208
259
|
runs: free,
|
|
209
|
-
}
|
|
260
|
+
};
|
|
261
|
+
byAnchor.set(cand.anchor, pt);
|
|
262
|
+
points.push(pt);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// A byte is FRAME when more than half the weave shares it, and a SPAN is
|
|
266
|
+
// frame when more than half its bytes are — the same two-level
|
|
267
|
+
// half-dominance reading cast.ts's own frame gate uses, over the same
|
|
268
|
+
// `depth`. Read against the accepted POINTS (as cast.ts does), so it is
|
|
269
|
+
// only meaningful once phase 1 has run.
|
|
270
|
+
const framed = (from, to) => {
|
|
271
|
+
let n = 0;
|
|
272
|
+
for (let i = from; i < to; i++)
|
|
273
|
+
if (dominates(depth[i], points.length))
|
|
274
|
+
n++;
|
|
275
|
+
return dominates(n, to - from);
|
|
276
|
+
};
|
|
277
|
+
// PHASE 2 — THE CLIMB'S OWN CONCLUSION IS AN ALIGNMENT THE LITERAL MATCHER
|
|
278
|
+
// CANNOT SEE. `alignRuns` seeds on W-grams, so two forms differing by a
|
|
279
|
+
// single byte share no run at all: on `How is ice like steel?` against a
|
|
280
|
+
// store holding `Ice is cold`, the query's `ice` and the stored `Ice` agree
|
|
281
|
+
// on only `ce ` — three bytes, never seeded — so that structure entered the
|
|
282
|
+
// weave carrying nothing but the ` is ` scaffolding every exemplar shares,
|
|
283
|
+
// lost it to the first point that claimed it, and vanished. The climb had
|
|
284
|
+
// ALREADY identified it: its resonance elected `Ice is cold` from the query
|
|
285
|
+
// span `ce l` and `Steel is hard` from `stee`, two disjoint spans each naming
|
|
286
|
+
// its own structure, weighed through the region's contrastive margin and its
|
|
287
|
+
// IDF — gates the aligner has no equivalent of.
|
|
288
|
+
//
|
|
289
|
+
// So the climb PROPOSES the pairing (which structure, which query span) and
|
|
290
|
+
// bytes DECIDE its terms (§2.3). Three gates, each one measured:
|
|
291
|
+
//
|
|
292
|
+
// • it may only take query bytes NO literal run claimed. Run inline with
|
|
293
|
+
// phase 1 this did the opposite of "exact decides" — a higher-ranked
|
|
294
|
+
// candidate's proposal trimmed a lower-ranked candidate's byte-for-byte
|
|
295
|
+
// match out of existence (`he W`, proposed for `a nickname meaning the
|
|
296
|
+
// divine one`, cut the literal `The ` out of `The Starry Night was
|
|
297
|
+
// painted by Vincent van Gogh.` and CAST's redirection lost its
|
|
298
|
+
// dominant — test/29 C4). Hence a second pass, after every literal run
|
|
299
|
+
// is placed.
|
|
300
|
+
// • the literal agreement must DOMINATE the span. A climb vote is not by
|
|
301
|
+
// itself an alignment: on `The Persistence of Memory was painted by
|
|
302
|
+
// Salvador Dali.` the climb elects `The Starry Night…` from the span
|
|
303
|
+
// ` Dali.`, which shares barely a byte with it — the resonance was
|
|
304
|
+
// carried by the frame those exemplars share. Admitting it let CAST
|
|
305
|
+
// weave points out of pure scaffolding and out-account the correct
|
|
306
|
+
// extraction (test/00, test/24). Where the proposal is real the
|
|
307
|
+
// agreement is overwhelming: both C1 spans agree on three of four bytes.
|
|
308
|
+
// • and the span must not be FRAME. Literal dominance alone is too weak
|
|
309
|
+
// at this scale — a 4-byte span agrees three-of-four with half the
|
|
310
|
+
// corpus by accident (`he W` against `a nickname meaning the divine
|
|
311
|
+
// one`). Frame is the weave-local measure of exactly that.
|
|
312
|
+
const claimed = new Uint8Array(query.length);
|
|
313
|
+
for (const p of points) {
|
|
314
|
+
for (const r of p.runs)
|
|
315
|
+
claimed.fill(1, r.qs, r.qe);
|
|
316
|
+
}
|
|
317
|
+
for (const { cand, ctxBytes } of cands) {
|
|
318
|
+
if (cand.end > cand.start) {
|
|
319
|
+
let qs = cand.start;
|
|
320
|
+
let qe = cand.end;
|
|
321
|
+
while (qs < qe && claimed[qs])
|
|
322
|
+
qs++;
|
|
323
|
+
while (qe > qs && claimed[qe - 1])
|
|
324
|
+
qe--;
|
|
325
|
+
let clear = true;
|
|
326
|
+
for (let i = qs; i < qe; i++)
|
|
327
|
+
if (claimed[i])
|
|
328
|
+
clear = false;
|
|
329
|
+
if (clear && qe - qs >= Math.min(quantum, ctxBytes.length)) {
|
|
330
|
+
// The gate only asks whether the agreement DOMINATES the span, so
|
|
331
|
+
// search DOWNWARD from the whole span and stop at the first hit: the
|
|
332
|
+
// first length found is both the longest agreement and, by
|
|
333
|
+
// construction, already past the dominance bar. At most O(W²) bounded
|
|
334
|
+
// substring probes — a span is one segment (≤ 2W) — where a full
|
|
335
|
+
// longest-common-substring scan would be O(|span|² · |ctx|) against a
|
|
336
|
+
// context that may be W× the query.
|
|
337
|
+
const span = query.subarray(qs, qe);
|
|
338
|
+
const bar = Math.floor(span.length / 2) + 1; // dominates(bar, length)
|
|
339
|
+
let bestLen = 0;
|
|
340
|
+
let bestCs = 0;
|
|
341
|
+
for (let len = span.length; len >= bar && bestLen === 0; len--) {
|
|
342
|
+
for (let off = 0; off + len <= span.length; off++) {
|
|
343
|
+
const at = indexOf(ctxBytes, span.subarray(off, off + len), 0);
|
|
344
|
+
if (at < 0)
|
|
345
|
+
continue;
|
|
346
|
+
bestLen = len;
|
|
347
|
+
// Where the span's FIRST byte lands, so `cs` means the same thing
|
|
348
|
+
// it does for a literal run: the context offset the run starts at.
|
|
349
|
+
bestCs = Math.max(0, at - off);
|
|
350
|
+
break;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (bestLen > 0 && !framed(qs, qe)) {
|
|
354
|
+
const run = {
|
|
355
|
+
qs,
|
|
356
|
+
qe,
|
|
357
|
+
cs: bestCs,
|
|
358
|
+
weight: bestLen / (qe - qs),
|
|
359
|
+
proposed: true,
|
|
360
|
+
};
|
|
361
|
+
claimed.fill(1, qs, qe);
|
|
362
|
+
const pt = byAnchor.get(cand.anchor);
|
|
363
|
+
if (!pt) {
|
|
364
|
+
const made = {
|
|
365
|
+
anchor: cand.anchor,
|
|
366
|
+
vote: cand.vote,
|
|
367
|
+
ctx: ctxBytes,
|
|
368
|
+
runs: [run],
|
|
369
|
+
};
|
|
370
|
+
byAnchor.set(cand.anchor, made);
|
|
371
|
+
points.push(made);
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
pt.runs.push(run);
|
|
375
|
+
pt.runs.sort((x, y) => x.qs - y.qs);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
210
379
|
}
|
|
211
380
|
}
|
|
212
381
|
return { points, depth };
|