@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/src/mind/mind.ts
CHANGED
|
@@ -155,6 +155,9 @@ import { aluToMechanism, defaultMechanisms, think } from "./pipeline.js";
|
|
|
155
155
|
import { articulate } from "./articulation.js";
|
|
156
156
|
import { ingest } from "./learning.js";
|
|
157
157
|
import { rItem } from "./trace.js";
|
|
158
|
+
// The work meter is exported from src/index.ts (via src/meter.ts) — the one
|
|
159
|
+
// definition; the Mind only consumes it.
|
|
160
|
+
import { type CostReport, Meter } from "../meter.js";
|
|
158
161
|
|
|
159
162
|
// ── MindOptions ───────────────────────────────────────────────────────────
|
|
160
163
|
|
|
@@ -174,6 +177,15 @@ export interface MindOptions {
|
|
|
174
177
|
mechanismFactories?: ((
|
|
175
178
|
host: import("../extension.js").ExtensionHost,
|
|
176
179
|
) => import("./pipeline-mechanism.js").PipelineMechanism)[];
|
|
180
|
+
/** Measure the computational usage of every inference call — see
|
|
181
|
+
* src/meter.ts. Off by default and free when off (one null check per
|
|
182
|
+
* store read); on, each `respond`/`respondTurn` leaves a {@link
|
|
183
|
+
* Mind.lastCost} report behind. Counters are deterministic, so two runs
|
|
184
|
+
* of the same query on the same store are diffable; the millisecond
|
|
185
|
+
* fields are not. Profiling NEVER changes an answer — but note that
|
|
186
|
+
* attaching a RATIONALE does (traced responses bypass the ctx memos,
|
|
187
|
+
* AGENTS §2.11), so profile without a trace. */
|
|
188
|
+
profile?: boolean;
|
|
177
189
|
/** Content canonicalizer applied to EVERY response (any modality) for
|
|
178
190
|
* equivalence-class resolution — see src/canon.ts. Text entry points
|
|
179
191
|
* ({@link Mind.respondText}, {@link Mind.respondTurnText}) inject the
|
|
@@ -215,6 +227,22 @@ export class Mind implements MindContext {
|
|
|
215
227
|
* point decide (text entry points inject {@link textCanon}). */
|
|
216
228
|
private _canonOpt: Canon | false | null = null;
|
|
217
229
|
|
|
230
|
+
/** The work accumulator for the inference call in flight — see
|
|
231
|
+
* {@link MindContext.meter}. Non-null only between beginResponse and
|
|
232
|
+
* endResponse, and only when the Mind was constructed with
|
|
233
|
+
* `{ profile: true }`. */
|
|
234
|
+
meter: Meter | null = null;
|
|
235
|
+
|
|
236
|
+
/** Whether {@link MindOptions.profile} was set. */
|
|
237
|
+
private _profile = false;
|
|
238
|
+
|
|
239
|
+
/** The computational-usage report of the LAST completed inference call, or
|
|
240
|
+
* null when profiling is off (or nothing has been asked yet). Overwritten
|
|
241
|
+
* by every `respond`/`respondTurn`; copy it if you are aggregating. See
|
|
242
|
+
* {@link import("../meter.js").CostReport} and `sumReports`/`formatReport`
|
|
243
|
+
* for battery-level aggregation. */
|
|
244
|
+
lastCost: CostReport | null = null;
|
|
245
|
+
|
|
218
246
|
/** Memo of the consensus climb — content-keyed. See {@link MindContext.climbMemo}. */
|
|
219
247
|
climbMemo: Map<string, Map<string, AttentionRead>> | null = null;
|
|
220
248
|
|
|
@@ -326,9 +354,11 @@ export class Mind implements MindContext {
|
|
|
326
354
|
mechanisms: userMechs,
|
|
327
355
|
mechanismFactories: userFacts,
|
|
328
356
|
canon: optsCanon,
|
|
357
|
+
profile: optsProfile,
|
|
329
358
|
...rest
|
|
330
359
|
} = (optsOrCfg ?? {}) as MindOptions;
|
|
331
360
|
this._canonOpt = optsCanon ?? null;
|
|
361
|
+
this._profile = optsProfile === true;
|
|
332
362
|
this.cfg = resolveConfig(rest as Partial<MindConfig>);
|
|
333
363
|
this.store = optsStore ?? new SQliteStore({
|
|
334
364
|
maxGroup: this.cfg.geometry.maxGroup,
|
|
@@ -417,19 +447,48 @@ export class Mind implements MindContext {
|
|
|
417
447
|
return perceiveImpl(this, input, leafAt, lookup);
|
|
418
448
|
}
|
|
419
449
|
|
|
420
|
-
/** Open one response's transient state — the tracer
|
|
421
|
-
* memos
|
|
422
|
-
*
|
|
450
|
+
/** Open one response's transient state — the tracer, the per-response
|
|
451
|
+
* memos, the work meter. The ONE place this state is created, and it
|
|
452
|
+
* serves BOTH entry points: `respond` takes fresh per-response memos,
|
|
453
|
+
* `respondTurn` passes its conversation, whose memos persist across turns
|
|
454
|
+
* (content-keyed, so the previous turn's results are found by this turn's
|
|
455
|
+
* sub-span calls) and whose `resolvedSubtrees` makes foldTree O(suffix)
|
|
456
|
+
* instead of O(context). respondTurn used to inline its own copy of this
|
|
457
|
+
* and of {@link endResponse}; the two drifted (a memo added to one was
|
|
458
|
+
* silently absent from the other), so there is exactly one pair now. */
|
|
423
459
|
private beginResponse(
|
|
424
460
|
inspectRationale?: InspectRationale,
|
|
425
461
|
canon?: Canon | null,
|
|
462
|
+
conv?: ConversationData,
|
|
426
463
|
): void {
|
|
427
464
|
this.trace = inspectRationale ? new Rationale(inspectRationale) : null;
|
|
428
|
-
this.climbMemo = new Map();
|
|
429
|
-
this.recogniseMemo = new Map();
|
|
430
|
-
this.perceiveMemo = new Map();
|
|
465
|
+
this.climbMemo = conv ? conv.climbMemo : new Map();
|
|
466
|
+
this.recogniseMemo = conv ? conv.recogniseMemo : new Map();
|
|
467
|
+
this.perceiveMemo = conv ? conv.perceiveMemo : new Map();
|
|
468
|
+
this._resolvedSubtrees = conv ? conv.resolvedSubtrees : null;
|
|
431
469
|
this.canon = canon ?? null;
|
|
432
470
|
this.canonMemo = canon ? new Map() : null;
|
|
471
|
+
this._beginMeter();
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** Open (or leave closed) the response's work accumulator. Separate from
|
|
475
|
+
* {@link beginResponse} because {@link respondTurn} keeps its own
|
|
476
|
+
* conversation-scoped lifecycle and must not create fresh per-response
|
|
477
|
+
* memos — but it DOES meter, through this same pair. */
|
|
478
|
+
private _beginMeter(): void {
|
|
479
|
+
if (!this._profile) return;
|
|
480
|
+
this.meter = new Meter();
|
|
481
|
+
this.store.meter = this.meter;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/** Close the accumulator and publish its report. Detaching from the store
|
|
485
|
+
* matters: a Mind that shares a store with another Mind must not keep
|
|
486
|
+
* charging that store's reads to a finished response. */
|
|
487
|
+
private _endMeter(queryBytes: number): void {
|
|
488
|
+
if (this.meter === null) return;
|
|
489
|
+
this.lastCost = this.meter.report(queryBytes);
|
|
490
|
+
this.store.meter = null;
|
|
491
|
+
this.meter = null;
|
|
433
492
|
}
|
|
434
493
|
|
|
435
494
|
/** The canonicalizer a response should carry: the Mind-level option when
|
|
@@ -441,12 +500,20 @@ export class Mind implements MindContext {
|
|
|
441
500
|
}
|
|
442
501
|
|
|
443
502
|
/** Close one response's transient state — every per-response field, incl.
|
|
444
|
-
* the edge guide/choices `think` sets mid-flight.
|
|
445
|
-
|
|
503
|
+
* the edge guide/choices `think` sets mid-flight, and the meter's report.
|
|
504
|
+
*
|
|
505
|
+
* A conversation's memo MAPS were mutated in place, so `data.*` still
|
|
506
|
+
* points at them and there is nothing to save back. Clearing the Mind's
|
|
507
|
+
* references is what matters: a concurrently-started `respond()` swaps its
|
|
508
|
+
* own fresh maps into these pointers, and copying back from them here
|
|
509
|
+
* would inject a foreign response's memos into the conversation. */
|
|
510
|
+
private endResponse(queryBytes: number): void {
|
|
511
|
+
this._endMeter(queryBytes);
|
|
446
512
|
this.trace = null;
|
|
447
513
|
this.climbMemo = null;
|
|
448
514
|
this.recogniseMemo = null;
|
|
449
515
|
this.perceiveMemo = null;
|
|
516
|
+
this._resolvedSubtrees = null;
|
|
450
517
|
this.canon = null;
|
|
451
518
|
this.canonMemo = null;
|
|
452
519
|
this._edgeGuide = null;
|
|
@@ -464,30 +531,51 @@ export class Mind implements MindContext {
|
|
|
464
531
|
): Promise<Response> {
|
|
465
532
|
this.beginResponse(inspectRationale, canon);
|
|
466
533
|
try {
|
|
467
|
-
|
|
468
|
-
rItem(queryBytes, "query"),
|
|
469
|
-
]);
|
|
470
|
-
const thought = await think(this, queryBytes, this.mechanisms);
|
|
471
|
-
if (thought === null) {
|
|
472
|
-
top?.done([], "nothing to perceive or an empty store — no answer");
|
|
473
|
-
return { v: null, bytes: new Uint8Array(0) };
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
const voiced = await articulate(this, thought.bytes, queryBytes);
|
|
477
|
-
top?.done(
|
|
478
|
-
[rItem(voiced, "answer", resolveImpl(this, voiced) ?? undefined)],
|
|
479
|
-
"the answer, re-voiced in the asker's words",
|
|
480
|
-
);
|
|
481
|
-
return {
|
|
482
|
-
v: gistOf(this, voiced),
|
|
483
|
-
bytes: voiced,
|
|
484
|
-
provenance: thought.provenance,
|
|
485
|
-
};
|
|
534
|
+
return await this._groundAndVoice(queryBytes, traceLabel);
|
|
486
535
|
} finally {
|
|
487
|
-
this.endResponse();
|
|
536
|
+
this.endResponse(queryBytes.length);
|
|
488
537
|
}
|
|
489
538
|
}
|
|
490
539
|
|
|
540
|
+
/** The ONE path from query bytes to a voiced answer: ground (think), then
|
|
541
|
+
* re-voice in the asker's words (articulate). Both entry points run
|
|
542
|
+
* exactly this — they differ only in the LIFECYCLE around it (fresh
|
|
543
|
+
* per-response memos vs. a conversation's persistent ones) and in what
|
|
544
|
+
* they do with the answer afterwards. It must be called between
|
|
545
|
+
* {@link beginResponse} and {@link endResponse}. */
|
|
546
|
+
private async _groundAndVoice(
|
|
547
|
+
queryBytes: Uint8Array,
|
|
548
|
+
traceLabel: string,
|
|
549
|
+
): Promise<Response> {
|
|
550
|
+
const top = this.trace?.enter(traceLabel, [rItem(queryBytes, "query")]);
|
|
551
|
+
const meter = this.meter;
|
|
552
|
+
const thought = meter
|
|
553
|
+
? await meter.time(
|
|
554
|
+
"think",
|
|
555
|
+
() => think(this, queryBytes, this.mechanisms),
|
|
556
|
+
)
|
|
557
|
+
: await think(this, queryBytes, this.mechanisms);
|
|
558
|
+
if (thought === null) {
|
|
559
|
+
top?.done([], "nothing to perceive or an empty store — no answer");
|
|
560
|
+
return { v: null, bytes: new Uint8Array(0) };
|
|
561
|
+
}
|
|
562
|
+
const voiced = meter
|
|
563
|
+
? await meter.time(
|
|
564
|
+
"articulate",
|
|
565
|
+
() => articulate(this, thought.bytes, queryBytes),
|
|
566
|
+
)
|
|
567
|
+
: await articulate(this, thought.bytes, queryBytes);
|
|
568
|
+
top?.done(
|
|
569
|
+
[rItem(voiced, "answer", resolveImpl(this, voiced) ?? undefined)],
|
|
570
|
+
"the answer, re-voiced in the asker's words",
|
|
571
|
+
);
|
|
572
|
+
return {
|
|
573
|
+
v: gistOf(this, voiced),
|
|
574
|
+
bytes: voiced,
|
|
575
|
+
provenance: thought.provenance,
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
491
579
|
async respond(
|
|
492
580
|
input: Input,
|
|
493
581
|
inspectRationale?: InspectRationale,
|
|
@@ -642,55 +730,30 @@ export class Mind implements MindContext {
|
|
|
642
730
|
|
|
643
731
|
const turnBytes = inputBytes(this, turn);
|
|
644
732
|
// Incremental perception — O(turn) instead of O(context).
|
|
645
|
-
|
|
733
|
+
this._growContext(data, turnBytes);
|
|
646
734
|
const newContext = data.bytes;
|
|
647
735
|
|
|
648
|
-
//
|
|
649
|
-
//
|
|
650
|
-
//
|
|
651
|
-
//
|
|
652
|
-
// the current turn's sub-span calls.
|
|
653
|
-
// _resolvedSubtrees — Sema-node-keyed; when the pyramid reuses
|
|
654
|
-
// prefix subtrees (identical objects), foldTree returns their
|
|
655
|
-
// ids immediately — O(suffix) instead of O(context).
|
|
656
|
-
this.perceiveMemo = data.perceiveMemo;
|
|
657
|
-
this.recogniseMemo = data.recogniseMemo;
|
|
658
|
-
this.climbMemo = data.climbMemo;
|
|
659
|
-
this._resolvedSubtrees = data.resolvedSubtrees;
|
|
660
|
-
this.trace = inspectRationale ? new Rationale(inspectRationale) : null;
|
|
661
|
-
// A string turn is text by nature — carry the text equivalence, same as
|
|
736
|
+
// The conversation's persistent memos and subtree cache are swapped in
|
|
737
|
+
// by beginResponse (see there) — the SAME lifecycle respond() uses, so a
|
|
738
|
+
// memo added in one place can never be missing from the other. A string
|
|
739
|
+
// turn is text by nature and carries the text equivalence, same as
|
|
662
740
|
// respond() (see _canonFor).
|
|
663
|
-
|
|
664
|
-
|
|
741
|
+
//
|
|
742
|
+
// No recognise-memo pre-seeding here: that used to be necessary because
|
|
743
|
+
// the flat/positional fold lost visibility into an earlier turn's own
|
|
744
|
+
// structure once later bytes shifted its position (foldTree no longer
|
|
745
|
+
// visited the turn's root node). The STABLE-PREFIX fold (see {@link
|
|
746
|
+
// ConversationData}) makes every turn's subtree independent of what
|
|
747
|
+
// follows it by construction, so recognise() finds it correctly on its
|
|
748
|
+
// own, first-touch, exactly once per turn.
|
|
749
|
+
this.beginResponse(
|
|
750
|
+
inspectRationale,
|
|
751
|
+
this._canonFor(typeof turn === "string" ? textCanon : null),
|
|
752
|
+
data,
|
|
753
|
+
);
|
|
665
754
|
|
|
666
755
|
try {
|
|
667
|
-
|
|
668
|
-
// because the flat/positional fold lost visibility into an earlier
|
|
669
|
-
// turn's own structure once later bytes shifted its position
|
|
670
|
-
// (foldTree no longer visited the turn's root node). The
|
|
671
|
-
// STABLE-PREFIX fold (see {@link ConversationData}) makes every
|
|
672
|
-
// turn's subtree independent of what follows it by construction, so
|
|
673
|
-
// recognise() finds it correctly on its own, first-touch, exactly
|
|
674
|
-
// once per turn — no workaround needed, and none pre-empting
|
|
675
|
-
// recognise()'s own memo with a partial result.
|
|
676
|
-
const top = this.trace?.enter("respondTurn", [
|
|
677
|
-
rItem(newContext, "query"),
|
|
678
|
-
]);
|
|
679
|
-
|
|
680
|
-
const thought = await think(this, newContext, this.mechanisms);
|
|
681
|
-
if (thought === null) {
|
|
682
|
-
top?.done([], "nothing to perceive or an empty store — no answer");
|
|
683
|
-
return {
|
|
684
|
-
response: { v: null, bytes: new Uint8Array(0) },
|
|
685
|
-
state: this.conversationState(conv)!,
|
|
686
|
-
};
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
const voiced = await articulate(this, thought.bytes, newContext);
|
|
690
|
-
top?.done(
|
|
691
|
-
[rItem(voiced, "answer", resolveImpl(this, voiced) ?? undefined)],
|
|
692
|
-
"the answer, re-voiced in the asker's words",
|
|
693
|
-
);
|
|
756
|
+
const response = await this._groundAndVoice(newContext, "respondTurn");
|
|
694
757
|
|
|
695
758
|
// The REPLY joins the accumulated context the same way a turn does
|
|
696
759
|
// ({@link addTurn}): raw byte append plus a boundary offset — never a
|
|
@@ -698,33 +761,11 @@ export class Mind implements MindContext {
|
|
|
698
761
|
// the cumulative continuous shape multi-turn training deposits, so a
|
|
699
762
|
// later turn can refer to what was ANSWERED ("which of those two…"),
|
|
700
763
|
// not only to what was asked.
|
|
701
|
-
if (
|
|
702
|
-
|
|
703
|
-
return {
|
|
704
|
-
response: {
|
|
705
|
-
v: gistOf(this, voiced),
|
|
706
|
-
bytes: voiced,
|
|
707
|
-
provenance: thought.provenance,
|
|
708
|
-
},
|
|
709
|
-
state: this.conversationState(conv)!,
|
|
710
|
-
};
|
|
764
|
+
if (response.bytes.length > 0) this.addTurn(conv, response.bytes);
|
|
765
|
+
|
|
766
|
+
return { response, state: this.conversationState(conv)! };
|
|
711
767
|
} finally {
|
|
712
|
-
|
|
713
|
-
// `data.*` still points at them. Re-assigning from the Mind-level
|
|
714
|
-
// pointers here was a no-op in the single-flight case and, under a
|
|
715
|
-
// concurrently-started respond() (which swaps its own fresh maps into
|
|
716
|
-
// those pointers), would inject a FOREIGN response's memos into this
|
|
717
|
-
// conversation. Clear Mind references — non-conversation respond()
|
|
718
|
-
// calls get fresh per-response memos.
|
|
719
|
-
this.trace = null;
|
|
720
|
-
this.perceiveMemo = null;
|
|
721
|
-
this.recogniseMemo = null;
|
|
722
|
-
this.climbMemo = null;
|
|
723
|
-
this.canon = null;
|
|
724
|
-
this.canonMemo = null;
|
|
725
|
-
this._resolvedSubtrees = null;
|
|
726
|
-
this._edgeGuide = null;
|
|
727
|
-
this._edgeChoice.clear();
|
|
768
|
+
this.endResponse(newContext.length);
|
|
728
769
|
}
|
|
729
770
|
}
|
|
730
771
|
|
|
@@ -17,11 +17,13 @@ import type { AncestorReach, MindContext, Recognition } from "./types.js";
|
|
|
17
17
|
import type { AttentionRead } from "./types.js";
|
|
18
18
|
import type { ComputedSpan } from "../extension.js";
|
|
19
19
|
import type { Vec } from "../vec.js";
|
|
20
|
+
import { indexOf } from "../bytes.js";
|
|
21
|
+
import { dominates } from "../geometry.js";
|
|
20
22
|
import { windowIds } from "./canonical.js";
|
|
21
23
|
import { read, resolve } from "./primitives.js";
|
|
22
|
-
import { alignGraded, type GradedRun } from "./match.js";
|
|
24
|
+
import { alignGraded, type GradedRun, skillExemplar } from "./match.js";
|
|
23
25
|
import { climbAttentionAll } from "./attention.js";
|
|
24
|
-
import {
|
|
26
|
+
import { sharedReachMemo } from "./traverse.js";
|
|
25
27
|
|
|
26
28
|
// ── Precomputed ──────────────────────────────────────────────────────────────
|
|
27
29
|
//
|
|
@@ -94,9 +96,16 @@ export class Precomputed {
|
|
|
94
96
|
}
|
|
95
97
|
|
|
96
98
|
/** Shared memo for {@link reachOf} (structural-IDF reads): a window's
|
|
97
|
-
* ancestor reach is a pure function of the read-only store, so one
|
|
98
|
-
*
|
|
99
|
-
|
|
99
|
+
* ancestor reach is a pure function of the read-only store, so one memo
|
|
100
|
+
* serves every mechanism that prices commonality — AND the consensus
|
|
101
|
+
* climb, which is the largest consumer and used to build its own. The
|
|
102
|
+
* ONE definition of its lifetime lives in traverse.ts
|
|
103
|
+
* ({@link sharedReachMemo}): response-scoped for respond(),
|
|
104
|
+
* conversation-scoped across turns, always cold under a trace. */
|
|
105
|
+
private _reach?: Map<number, AncestorReach>;
|
|
106
|
+
get reachMemo(): Map<number, AncestorReach> {
|
|
107
|
+
return this._reach ??= sharedReachMemo(this.ctx);
|
|
108
|
+
}
|
|
100
109
|
|
|
101
110
|
// ── Expensive lazy analyses ───────────────────────────────────────────
|
|
102
111
|
//
|
|
@@ -105,16 +114,26 @@ export class Precomputed {
|
|
|
105
114
|
// mechanism MUST check its cheap floor gates and the pipeline's
|
|
106
115
|
// `worthRunning` predicate before first-touching one of these.
|
|
107
116
|
|
|
117
|
+
/** Charge a lazily-shared analysis to its OWN phase rather than to the
|
|
118
|
+
* mechanism that happened to first-touch it. Without this the profile
|
|
119
|
+
* reads as "cast.floor costs 2 s" when what actually cost 2 s is the
|
|
120
|
+
* consensus climb — which cast merely paid for on everyone's behalf, and
|
|
121
|
+
* which every later consumer then got free. Attribution must follow the
|
|
122
|
+
* work, not the caller. */
|
|
123
|
+
private shared<T>(phase: string, fn: () => Promise<T>): Promise<T> {
|
|
124
|
+
const meter = this.ctx.meter;
|
|
125
|
+
return meter ? meter.time(phase, fn) : fn();
|
|
126
|
+
}
|
|
127
|
+
|
|
108
128
|
private _attention?: Promise<AttentionRead>;
|
|
109
129
|
/** The full consensus climb (roots + ranked anchors) — the query-level
|
|
110
130
|
* evidence CAST, confluence, extraction, recall's scaffolding tier, and
|
|
111
131
|
* fusion all share. Computed on first access; a query no mechanism
|
|
112
132
|
* climbs for (e.g. one an extension decided outright) never pays for it. */
|
|
113
133
|
attention(): Promise<AttentionRead> {
|
|
114
|
-
return this._attention ??=
|
|
115
|
-
|
|
116
|
-
this.query,
|
|
117
|
-
this.k,
|
|
134
|
+
return this._attention ??= this.shared(
|
|
135
|
+
"attention",
|
|
136
|
+
() => climbAttentionAll(this.ctx, this.query, this.k),
|
|
118
137
|
);
|
|
119
138
|
}
|
|
120
139
|
|
|
@@ -124,7 +143,10 @@ export class Precomputed {
|
|
|
124
143
|
* mechanism doing analogical transfer. */
|
|
125
144
|
weave(): Promise<WeaveInfo> {
|
|
126
145
|
return this._weave ??= this.attention().then((climb) =>
|
|
127
|
-
|
|
146
|
+
this.shared(
|
|
147
|
+
"weave",
|
|
148
|
+
async () => computeWeave(this.ctx, this.query, this, climb),
|
|
149
|
+
)
|
|
128
150
|
);
|
|
129
151
|
}
|
|
130
152
|
|
|
@@ -140,7 +162,10 @@ export class Precomputed {
|
|
|
140
162
|
spanShapedOf(anchor: number): Promise<SkillInfo | null> {
|
|
141
163
|
let p = this._spanShaped.get(anchor);
|
|
142
164
|
if (p === undefined) {
|
|
143
|
-
p =
|
|
165
|
+
p = this.shared(
|
|
166
|
+
"spanShaped",
|
|
167
|
+
() => skillExemplar(this.ctx, anchor, this.guide),
|
|
168
|
+
);
|
|
144
169
|
this._spanShaped.set(anchor, p);
|
|
145
170
|
}
|
|
146
171
|
return p;
|
|
@@ -192,6 +217,7 @@ function computeWeave(
|
|
|
192
217
|
const rankedCapped = ranked.length > pre.k ? ranked.slice(0, pre.k) : ranked;
|
|
193
218
|
const depth = new Float64Array(query.length);
|
|
194
219
|
const points: WeaveInfo["points"] = [];
|
|
220
|
+
const byAnchor = new Map<number, WeaveInfo["points"][number]>();
|
|
195
221
|
|
|
196
222
|
// WEAVE-SCALE anchors only: CAST transfers structure between things the
|
|
197
223
|
// QUERY weaves together — query-scale structures. A context an order of
|
|
@@ -207,6 +233,38 @@ function computeWeave(
|
|
|
207
233
|
// uncapped weaves spent 5–8s per query recognising conversation-length
|
|
208
234
|
// anchors that could never form a weave point.
|
|
209
235
|
const capBytes = query.length * quantum;
|
|
236
|
+
// EXCLUSIVITY IS ARBITRATED BY THE CLIMB'S VOTE ORDER, DELIBERATELY. A query
|
|
237
|
+
// byte can only be independent evidence for ONE point, so points are built in
|
|
238
|
+
// ranked order and each new point's runs are trimmed against every point
|
|
239
|
+
// already accepted; a point left with no run of a full quantum drops out of
|
|
240
|
+
// the weave.
|
|
241
|
+
//
|
|
242
|
+
// That reads like first-come-wins — a point that merely ranked higher taking
|
|
243
|
+
// a span from the point that actually explains it — and arbitrating by LOCAL
|
|
244
|
+
// evidence instead (ownership of each byte to the longest covering run, then
|
|
245
|
+
// the heavier weight, then rank) was implemented and MEASURED: test/29 went
|
|
246
|
+
// 9/2 to 7/4, and the new failures name the reason. CAST requires the weave
|
|
247
|
+
// to touch a COMMITTED point of attention ("2 aligned structure(s), but none
|
|
248
|
+
// is one of the climb's 1 committed root(s)"), and it was precisely the vote
|
|
249
|
+
// order that kept the committed root's own point alive in the weave. Local
|
|
250
|
+
// run length knows nothing about what the climb settled on, so it evicted the
|
|
251
|
+
// root's evidence and left CAST refusing on its own consistency check.
|
|
252
|
+
//
|
|
253
|
+
// So the vote order here is not an accident of construction — it is what
|
|
254
|
+
// holds the weave and the climb to the same conclusion. Weave-local
|
|
255
|
+
// measures decide what is FRAME inside the weave (see the frame gates in
|
|
256
|
+
// cast.ts); which structures are in the weave at all stays the climb's call.
|
|
257
|
+
//
|
|
258
|
+
// TWO PASSES. `depth` — how much of the weave agrees on each query byte, and
|
|
259
|
+
// therefore what counts as FRAME — must be the whole weave's, not "whatever
|
|
260
|
+
// has been processed so far": read in one pass it made a candidate's own
|
|
261
|
+
// frame reading depend on its rank, and the proposed-run gate below needs the
|
|
262
|
+
// real thing.
|
|
263
|
+
const cands: Array<{
|
|
264
|
+
cand: (typeof rankedCapped)[number];
|
|
265
|
+
ctxBytes: Uint8Array;
|
|
266
|
+
raw: GradedRun[];
|
|
267
|
+
}> = [];
|
|
210
268
|
for (const cand of rankedCapped) {
|
|
211
269
|
const ctxBytes = read(ctx, cand.anchor, capBytes + 1);
|
|
212
270
|
if (ctxBytes.length === 0 || ctxBytes.length > capBytes) continue;
|
|
@@ -215,6 +273,9 @@ function computeWeave(
|
|
|
215
273
|
for (const r of raw) {
|
|
216
274
|
for (let i = r.qs; i < r.qe; i++) depth[i] += r.weight;
|
|
217
275
|
}
|
|
276
|
+
cands.push({ cand, ctxBytes, raw });
|
|
277
|
+
}
|
|
278
|
+
for (const { cand, ctxBytes, raw } of cands) {
|
|
218
279
|
const free: GradedRun[] = [];
|
|
219
280
|
for (const r of raw) {
|
|
220
281
|
let { qs, qe, cs, weight } = r;
|
|
@@ -236,12 +297,122 @@ function computeWeave(
|
|
|
236
297
|
}
|
|
237
298
|
}
|
|
238
299
|
if (free.length > 0) {
|
|
239
|
-
|
|
300
|
+
const pt = {
|
|
240
301
|
anchor: cand.anchor,
|
|
241
302
|
vote: cand.vote,
|
|
242
303
|
ctx: ctxBytes,
|
|
243
304
|
runs: free,
|
|
244
|
-
}
|
|
305
|
+
};
|
|
306
|
+
byAnchor.set(cand.anchor, pt);
|
|
307
|
+
points.push(pt);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// A byte is FRAME when more than half the weave shares it, and a SPAN is
|
|
312
|
+
// frame when more than half its bytes are — the same two-level
|
|
313
|
+
// half-dominance reading cast.ts's own frame gate uses, over the same
|
|
314
|
+
// `depth`. Read against the accepted POINTS (as cast.ts does), so it is
|
|
315
|
+
// only meaningful once phase 1 has run.
|
|
316
|
+
const framed = (from: number, to: number): boolean => {
|
|
317
|
+
let n = 0;
|
|
318
|
+
for (let i = from; i < to; i++) if (dominates(depth[i], points.length)) n++;
|
|
319
|
+
return dominates(n, to - from);
|
|
320
|
+
};
|
|
321
|
+
// PHASE 2 — THE CLIMB'S OWN CONCLUSION IS AN ALIGNMENT THE LITERAL MATCHER
|
|
322
|
+
// CANNOT SEE. `alignRuns` seeds on W-grams, so two forms differing by a
|
|
323
|
+
// single byte share no run at all: on `How is ice like steel?` against a
|
|
324
|
+
// store holding `Ice is cold`, the query's `ice` and the stored `Ice` agree
|
|
325
|
+
// on only `ce ` — three bytes, never seeded — so that structure entered the
|
|
326
|
+
// weave carrying nothing but the ` is ` scaffolding every exemplar shares,
|
|
327
|
+
// lost it to the first point that claimed it, and vanished. The climb had
|
|
328
|
+
// ALREADY identified it: its resonance elected `Ice is cold` from the query
|
|
329
|
+
// span `ce l` and `Steel is hard` from `stee`, two disjoint spans each naming
|
|
330
|
+
// its own structure, weighed through the region's contrastive margin and its
|
|
331
|
+
// IDF — gates the aligner has no equivalent of.
|
|
332
|
+
//
|
|
333
|
+
// So the climb PROPOSES the pairing (which structure, which query span) and
|
|
334
|
+
// bytes DECIDE its terms (§2.3). Three gates, each one measured:
|
|
335
|
+
//
|
|
336
|
+
// • it may only take query bytes NO literal run claimed. Run inline with
|
|
337
|
+
// phase 1 this did the opposite of "exact decides" — a higher-ranked
|
|
338
|
+
// candidate's proposal trimmed a lower-ranked candidate's byte-for-byte
|
|
339
|
+
// match out of existence (`he W`, proposed for `a nickname meaning the
|
|
340
|
+
// divine one`, cut the literal `The ` out of `The Starry Night was
|
|
341
|
+
// painted by Vincent van Gogh.` and CAST's redirection lost its
|
|
342
|
+
// dominant — test/29 C4). Hence a second pass, after every literal run
|
|
343
|
+
// is placed.
|
|
344
|
+
// • the literal agreement must DOMINATE the span. A climb vote is not by
|
|
345
|
+
// itself an alignment: on `The Persistence of Memory was painted by
|
|
346
|
+
// Salvador Dali.` the climb elects `The Starry Night…` from the span
|
|
347
|
+
// ` Dali.`, which shares barely a byte with it — the resonance was
|
|
348
|
+
// carried by the frame those exemplars share. Admitting it let CAST
|
|
349
|
+
// weave points out of pure scaffolding and out-account the correct
|
|
350
|
+
// extraction (test/00, test/24). Where the proposal is real the
|
|
351
|
+
// agreement is overwhelming: both C1 spans agree on three of four bytes.
|
|
352
|
+
// • and the span must not be FRAME. Literal dominance alone is too weak
|
|
353
|
+
// at this scale — a 4-byte span agrees three-of-four with half the
|
|
354
|
+
// corpus by accident (`he W` against `a nickname meaning the divine
|
|
355
|
+
// one`). Frame is the weave-local measure of exactly that.
|
|
356
|
+
const claimed = new Uint8Array(query.length);
|
|
357
|
+
for (const p of points) {
|
|
358
|
+
for (const r of p.runs) claimed.fill(1, r.qs, r.qe);
|
|
359
|
+
}
|
|
360
|
+
for (const { cand, ctxBytes } of cands) {
|
|
361
|
+
if (cand.end > cand.start) {
|
|
362
|
+
let qs = cand.start;
|
|
363
|
+
let qe = cand.end;
|
|
364
|
+
while (qs < qe && claimed[qs]) qs++;
|
|
365
|
+
while (qe > qs && claimed[qe - 1]) qe--;
|
|
366
|
+
let clear = true;
|
|
367
|
+
for (let i = qs; i < qe; i++) if (claimed[i]) clear = false;
|
|
368
|
+
if (clear && qe - qs >= Math.min(quantum, ctxBytes.length)) {
|
|
369
|
+
// The gate only asks whether the agreement DOMINATES the span, so
|
|
370
|
+
// search DOWNWARD from the whole span and stop at the first hit: the
|
|
371
|
+
// first length found is both the longest agreement and, by
|
|
372
|
+
// construction, already past the dominance bar. At most O(W²) bounded
|
|
373
|
+
// substring probes — a span is one segment (≤ 2W) — where a full
|
|
374
|
+
// longest-common-substring scan would be O(|span|² · |ctx|) against a
|
|
375
|
+
// context that may be W× the query.
|
|
376
|
+
const span = query.subarray(qs, qe);
|
|
377
|
+
const bar = Math.floor(span.length / 2) + 1; // dominates(bar, length)
|
|
378
|
+
let bestLen = 0;
|
|
379
|
+
let bestCs = 0;
|
|
380
|
+
for (let len = span.length; len >= bar && bestLen === 0; len--) {
|
|
381
|
+
for (let off = 0; off + len <= span.length; off++) {
|
|
382
|
+
const at = indexOf(ctxBytes, span.subarray(off, off + len), 0);
|
|
383
|
+
if (at < 0) continue;
|
|
384
|
+
bestLen = len;
|
|
385
|
+
// Where the span's FIRST byte lands, so `cs` means the same thing
|
|
386
|
+
// it does for a literal run: the context offset the run starts at.
|
|
387
|
+
bestCs = Math.max(0, at - off);
|
|
388
|
+
break;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (bestLen > 0 && !framed(qs, qe)) {
|
|
392
|
+
const run = {
|
|
393
|
+
qs,
|
|
394
|
+
qe,
|
|
395
|
+
cs: bestCs,
|
|
396
|
+
weight: bestLen / (qe - qs),
|
|
397
|
+
proposed: true,
|
|
398
|
+
};
|
|
399
|
+
claimed.fill(1, qs, qe);
|
|
400
|
+
const pt = byAnchor.get(cand.anchor);
|
|
401
|
+
if (!pt) {
|
|
402
|
+
const made = {
|
|
403
|
+
anchor: cand.anchor,
|
|
404
|
+
vote: cand.vote,
|
|
405
|
+
ctx: ctxBytes,
|
|
406
|
+
runs: [run],
|
|
407
|
+
};
|
|
408
|
+
byAnchor.set(cand.anchor, made);
|
|
409
|
+
points.push(made);
|
|
410
|
+
} else {
|
|
411
|
+
pt.runs.push(run);
|
|
412
|
+
pt.runs.sort((x, y) => x.qs - y.qs);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
245
416
|
}
|
|
246
417
|
}
|
|
247
418
|
return { points, depth };
|
|
@@ -277,6 +448,25 @@ export interface MechanismResult {
|
|
|
277
448
|
/** Override the mechanism's default provenance for this result.
|
|
278
449
|
* When absent, the pipeline uses `mech.provenance`. */
|
|
279
450
|
provenance?: string;
|
|
451
|
+
|
|
452
|
+
/** This grounding is a COMPLETE trained answer — post-grounding must not
|
|
453
|
+
* extend it. Declared by the mechanism about its own result, exactly like
|
|
454
|
+
* `accounted`/`used`/`unexplained`; the decider honours the property and
|
|
455
|
+
* never asks which mechanism set it, so the market stays uniform.
|
|
456
|
+
*
|
|
457
|
+
* Set it only when the answer is a stored form's OWN continuation reached
|
|
458
|
+
* through an identity claim about the query — i.e. the query IS some
|
|
459
|
+
* trained context, so its continuation is the whole read-out and a further
|
|
460
|
+
* multi-hop pivot would chain PAST the fact that produced the answer.
|
|
461
|
+
* That is the same reasoning `reason`'s echo guard already applies to a
|
|
462
|
+
* query that resolves exactly (see reasoning.ts); this carries the claim
|
|
463
|
+
* for the mechanisms that establish the identity by another route.
|
|
464
|
+
*
|
|
465
|
+
* Observed without it: the correct "What is the process of
|
|
466
|
+
* photosynthesis?" grounding was pivoted forward four times, out of the
|
|
467
|
+
* fact that answered it and into an unrelated "Hello! How can I assist you
|
|
468
|
+
* today?" conversational turn. */
|
|
469
|
+
complete?: boolean;
|
|
280
470
|
}
|
|
281
471
|
|
|
282
472
|
// ── PipelineMechanism ────────────────────────────────────────────────────────
|