@hviana/sema 0.4.7 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +290 -77
- package/HOW_IT_WORKS.md +2170 -735
- package/dist/example/train_base.d.ts +9 -3
- package/dist/example/train_base.js +21 -4
- package/dist/src/canon.d.ts +19 -0
- package/dist/src/canon.js +28 -0
- package/dist/src/geometry.d.ts +52 -0
- package/dist/src/geometry.js +87 -1
- package/dist/src/mind/bridge.js +27 -1
- package/dist/src/mind/frame-filler.d.ts +15 -0
- package/dist/src/mind/frame-filler.js +535 -0
- package/dist/src/mind/learning.js +6 -11
- package/dist/src/mind/mechanisms/cast.js +72 -2
- package/dist/src/mind/mechanisms/cover.js +6 -1
- package/dist/src/mind/mechanisms/extraction.js +27 -0
- package/dist/src/mind/mechanisms/recall.js +214 -34
- package/dist/src/mind/mind.d.ts +49 -1
- package/dist/src/mind/mind.js +137 -10
- package/dist/src/mind/pipeline-mechanism.d.ts +7 -0
- package/dist/src/mind/pipeline.js +29 -1
- package/dist/src/mind/prefix-completion.d.ts +59 -0
- package/dist/src/mind/prefix-completion.js +270 -0
- package/dist/src/mind/primitives.d.ts +29 -10
- package/dist/src/mind/primitives.js +52 -61
- package/dist/src/mind/recognition.js +119 -9
- package/dist/src/mind/traverse.d.ts +32 -0
- package/dist/src/mind/traverse.js +52 -0
- package/dist/src/mind/types.d.ts +55 -16
- package/dist/src/mind/types.js +68 -19
- package/dist/src/store.d.ts +21 -0
- package/dist/src/store.js +21 -0
- package/example/train_base.ts +21 -4
- package/package.json +1 -1
- package/src/canon.ts +28 -0
- package/src/geometry.ts +100 -1
- package/src/mind/bridge.ts +34 -0
- package/src/mind/frame-filler.ts +604 -0
- package/src/mind/learning.ts +5 -9
- package/src/mind/mechanisms/cast.ts +70 -2
- package/src/mind/mechanisms/cover.ts +6 -1
- package/src/mind/mechanisms/extraction.ts +27 -0
- package/src/mind/mechanisms/recall.ts +236 -37
- package/src/mind/mind.ts +154 -14
- package/src/mind/pipeline-mechanism.ts +7 -0
- package/src/mind/pipeline.ts +33 -1
- package/src/mind/prefix-completion.ts +314 -0
- package/src/mind/primitives.ts +59 -70
- package/src/mind/recognition.ts +117 -6
- package/src/mind/traverse.ts +52 -0
- package/src/mind/types.ts +98 -42
- package/src/store.ts +25 -0
- package/test/13-conversation.test.mjs +13 -0
- package/test/57-fusion-order.test.mjs +65 -0
- package/test/66-query-edge-whitespace.test.mjs +99 -0
- package/test/67-climb-anchor-breadth.test.mjs +113 -0
- package/test/68-extraction-unanchored.test.mjs +79 -0
- package/test/69-frame-filler.test.mjs +115 -0
- package/test/70-prefix-completion.test.mjs +170 -0
- package/test/71-embedded-canon-equivalence.test.mjs +121 -0
- package/test/72-prefix-candidate-supply.test.mjs +114 -0
- package/test/73-scaffolding-only-bridge-abstains.test.mjs +178 -0
- package/test/74-prefix-trap-not-sprung-early.test.mjs +114 -0
- package/test/75-multiturn-context-optimisation.test.mjs +1082 -0
package/src/mind/mind.ts
CHANGED
|
@@ -14,16 +14,18 @@ import { bindSeat, fold, Sema, Space } from "../sema.js";
|
|
|
14
14
|
import { Alphabet } from "../alphabet.js";
|
|
15
15
|
import {
|
|
16
16
|
bytesToTree,
|
|
17
|
+
contentFoldIncremental,
|
|
17
18
|
Grid,
|
|
18
19
|
gridToTree,
|
|
19
20
|
hilbertBytes,
|
|
20
21
|
reachThreshold,
|
|
21
22
|
stackGrids,
|
|
22
23
|
} from "../geometry.js";
|
|
24
|
+
import type { ContentFold } from "../geometry.js";
|
|
23
25
|
import { BoundedMap, type Store } from "../store.js";
|
|
24
26
|
import { SQliteStore } from "../store-sqlite.js";
|
|
25
27
|
import { type MindConfig, resolveConfig } from "../config.js";
|
|
26
|
-
import { type Canon, canonHash, textCanon } from "../canon.js";
|
|
28
|
+
import { type Canon, canonHash, textCanon, textEdgeTrim } from "../canon.js";
|
|
27
29
|
import {
|
|
28
30
|
type CandidateSpan,
|
|
29
31
|
coverSequence,
|
|
@@ -114,6 +116,18 @@ interface ConversationData {
|
|
|
114
116
|
tree: Sema;
|
|
115
117
|
bytes: Uint8Array;
|
|
116
118
|
boundaries: number[];
|
|
119
|
+
/** The plain fold's reusable segment state (see {@link ContentFold}). A
|
|
120
|
+
* grown context reuses every content segment it already folded and folds
|
|
121
|
+
* only the new turn — O(turn) instead of O(context) — and, because the
|
|
122
|
+
* reused segments are the SAME Sema objects, `resolvedSubtrees` (keyed by
|
|
123
|
+
* node identity) hits across turns, which is what makes recognition
|
|
124
|
+
* O(suffix). Undefined until the first grow.
|
|
125
|
+
*
|
|
126
|
+
* No turn boundaries are involved: reuse comes from content cuts being
|
|
127
|
+
* stable under append, and imposing boundaries would only change the tree
|
|
128
|
+
* away from what the deposit path folded. `boundaries` beside this field
|
|
129
|
+
* is API metadata, not an input to the fold. */
|
|
130
|
+
content?: ContentFold;
|
|
117
131
|
answeredSpans: Array<[number, number]>;
|
|
118
132
|
perceiveMemo: Map<string, Sema>;
|
|
119
133
|
recogniseMemo: Map<string, Recognition>;
|
|
@@ -130,6 +144,7 @@ import {
|
|
|
130
144
|
inputBytes,
|
|
131
145
|
latin1Key,
|
|
132
146
|
perceive as perceiveImpl,
|
|
147
|
+
perceiveKey,
|
|
133
148
|
read,
|
|
134
149
|
resolve as resolveImpl,
|
|
135
150
|
} from "./primitives.js";
|
|
@@ -605,6 +620,20 @@ export class Mind implements MindContext {
|
|
|
605
620
|
};
|
|
606
621
|
}
|
|
607
622
|
|
|
623
|
+
/** Answer ONE self-contained input.
|
|
624
|
+
*
|
|
625
|
+
* A MULTI-TURN context is not that, and this is the wrong entry point for
|
|
626
|
+
* it. `respond` folds the bytes it is handed with no boundary set, because
|
|
627
|
+
* nothing in a flat byte string says where one turn ended — only the caller
|
|
628
|
+
* who assembled it knows, which is the whole reason `boundaries` is a
|
|
629
|
+
* parameter of {@link perceiveImpl} and never inferred from content. A
|
|
630
|
+
* conversation deposited through {@link ingest} folds its contexts over
|
|
631
|
+
* those turn boundaries, so a hand-concatenated transcript passed here
|
|
632
|
+
* folds differently from the way it was learnt and reaches the trained
|
|
633
|
+
* context node only by luck (measured on a 7-turn conversation: 5/7 here
|
|
634
|
+
* against 7/7 through {@link respondTurn}, same bytes). Use
|
|
635
|
+
* {@link beginConversation} + {@link respondTurn}, or {@link addTurn} to
|
|
636
|
+
* replay turns the Mind should hear but not answer. */
|
|
608
637
|
async respond(
|
|
609
638
|
input: Input,
|
|
610
639
|
inspectRationale?: InspectRationale,
|
|
@@ -613,8 +642,43 @@ export class Mind implements MindContext {
|
|
|
613
642
|
// through the generic entry point. Raw bytes / grids carry only the
|
|
614
643
|
// Mind-level canon option, if any.
|
|
615
644
|
const canon = this._canonFor(typeof input === "string" ? textCanon : null);
|
|
645
|
+
// EDGE WHITESPACE IS NOT PART OF THE QUESTION — trim it once, here, so
|
|
646
|
+
// every mechanism downstream sees the same question regardless of how the
|
|
647
|
+
// caller spaced it. See canon.ts's textEdgeTrim for why the outer edges of a
|
|
648
|
+
// whole input are exactly where canon.ts's no-trimming hazard cannot arise.
|
|
649
|
+
// Gated on the SAME modality test as the canonicalizer above: for bytes and
|
|
650
|
+
// grids 0x20 is content, and nothing is trimmed.
|
|
651
|
+
//
|
|
652
|
+
// Measured on the 15.7M-node store: without this, one leading space took
|
|
653
|
+
// `Who wrote Romeo and Juliet?` and `What is the chemical symbol for
|
|
654
|
+
// water?` from answered to silent, because a shift re-seats every fold
|
|
655
|
+
// boundary — the whole of analyze_training.ts's K2 phase-robustness gap.
|
|
656
|
+
// The caller's EXACT bytes are tried first and the trim is a RETRY, not a
|
|
657
|
+
// pre-filter. Trimming up front is asymmetric — it normalises the query but
|
|
658
|
+
// not the stored forms — so it breaks byte-exact identity for a form trained
|
|
659
|
+
// WITH edge whitespace: test/04 deposits [" ice ", "cold"] and asks
|
|
660
|
+
// " ice ", which must keep answering. Retrying preserves that (the raw
|
|
661
|
+
// query resolves on the first pass) while still reaching the padded case
|
|
662
|
+
// (the raw query grounds nothing, the trimmed one does).
|
|
663
|
+
//
|
|
664
|
+
// COST: nothing on any answering path. The retry needs BOTH silence AND
|
|
665
|
+
// edge whitespace on the query, the same "only on the already-failed path"
|
|
666
|
+
// discipline test/44 and the bridge's own trim retry use. The conversation
|
|
667
|
+
// entry point (respondTurn) is deliberately NOT trimmed — it tracks
|
|
668
|
+
// turn-boundary offsets into its accumulated context, and shifting the bytes
|
|
669
|
+
// under those offsets would desync them.
|
|
670
|
+
const bytes = inputBytes(this, input);
|
|
671
|
+
const first = await this._respondImpl(
|
|
672
|
+
bytes,
|
|
673
|
+
inspectRationale,
|
|
674
|
+
"respond",
|
|
675
|
+
canon,
|
|
676
|
+
);
|
|
677
|
+
if (first.bytes.length > 0 || typeof input !== "string") return first;
|
|
678
|
+
const trimmed = textEdgeTrim(bytes);
|
|
679
|
+
if (trimmed.length === bytes.length || trimmed.length === 0) return first;
|
|
616
680
|
return this._respondImpl(
|
|
617
|
-
|
|
681
|
+
trimmed,
|
|
618
682
|
inspectRationale,
|
|
619
683
|
"respond",
|
|
620
684
|
canon,
|
|
@@ -651,7 +715,22 @@ export class Mind implements MindContext {
|
|
|
651
715
|
beginConversation(state?: ConversationState): Conversation {
|
|
652
716
|
const id = this._nextConvId++;
|
|
653
717
|
const initBytes = state?.context ?? new Uint8Array(0);
|
|
654
|
-
|
|
718
|
+
// NORMALISE CALLER-SUPPLIED BOUNDARIES. `boundaries` is documented
|
|
719
|
+
// strictly increasing and every boundary this class produces is (they are
|
|
720
|
+
// appended as the context grows), but a restored {@link ConversationState}
|
|
721
|
+
// comes from OUTSIDE — hand-built, migrated, or round-tripped through a
|
|
722
|
+
// store that did not preserve order. The folds consume boundaries with a
|
|
723
|
+
// sequential `b > prev` filter, so an out-of-order entry is silently
|
|
724
|
+
// DROPPED rather than rejected, and the conversation would then fold over
|
|
725
|
+
// a different cut set than the one the caller believes it restored.
|
|
726
|
+
// `bytesToTree` used to sort on the way in and absorbed this; the
|
|
727
|
+
// incremental fold this now calls does not, so the normalisation belongs
|
|
728
|
+
// here, at the one public door untrusted boundaries come through.
|
|
729
|
+
const initBoundaries = state?.boundaries
|
|
730
|
+
? [...new Set(state.boundaries)]
|
|
731
|
+
.filter((b) => b > 0 && b < initBytes.length)
|
|
732
|
+
.sort((a, b) => a - b)
|
|
733
|
+
: [];
|
|
655
734
|
const initAnswered = state?.answeredSpans
|
|
656
735
|
? state.answeredSpans.map(([start, end]) =>
|
|
657
736
|
[start, end] as [number, number]
|
|
@@ -661,16 +740,18 @@ export class Mind implements MindContext {
|
|
|
661
740
|
? [[start, cuts[i + 1]] as [number, number]]
|
|
662
741
|
: []
|
|
663
742
|
);
|
|
664
|
-
|
|
743
|
+
// The same incremental fold `_growContext` uses, so a RESTORED
|
|
744
|
+
// conversation starts with segment state its next turn can reuse — a
|
|
745
|
+
// resumed conversation is otherwise identical to a live one and must not
|
|
746
|
+
// pay a full re-fold on every turn for the rest of its life.
|
|
747
|
+
const restored = contentFoldIncremental(
|
|
665
748
|
this.space,
|
|
666
749
|
this.alphabet,
|
|
667
750
|
initBytes,
|
|
668
|
-
undefined,
|
|
669
|
-
undefined,
|
|
670
|
-
initBoundaries.length > 0 ? initBoundaries : undefined,
|
|
671
751
|
);
|
|
672
752
|
this._conversations.set(id, {
|
|
673
|
-
tree,
|
|
753
|
+
tree: restored.tree,
|
|
754
|
+
content: restored.fold,
|
|
674
755
|
bytes: initBytes,
|
|
675
756
|
boundaries: initBoundaries,
|
|
676
757
|
answeredSpans: initAnswered,
|
|
@@ -707,7 +788,41 @@ export class Mind implements MindContext {
|
|
|
707
788
|
* This is the primitive for turns the Mind should hear but not answer:
|
|
708
789
|
* replaying a transcript, feeding the OTHER speaker's line in a
|
|
709
790
|
* prediction harness, or restoring context piecewise. {@link
|
|
710
|
-
* respondTurn} = addTurn + think + its own reply appended the same way.
|
|
791
|
+
* respondTurn} = addTurn + think + its own reply appended the same way.
|
|
792
|
+
*
|
|
793
|
+
* ── ON SEPARATORS: THERE IS NO SEPARATOR QUESTION ────────────────────
|
|
794
|
+
*
|
|
795
|
+
* "Never a separator" above says what this method DOES — it appends the
|
|
796
|
+
* bytes you give it and records an OFFSET — not that separator bytes are
|
|
797
|
+
* forbidden, unsupported, or something the engine must be taught about.
|
|
798
|
+
* Sema is agnostic to them, and reviewers keep mistaking that agnosticism
|
|
799
|
+
* for a constraint. To be explicit, because the mistake is easy:
|
|
800
|
+
*
|
|
801
|
+
* 1. A turn boundary is an OFFSET, held here, in `boundaries`. It is
|
|
802
|
+
* never a character the geometry scans for. Nothing downstream asks
|
|
803
|
+
* "what byte separates two turns?" because nothing downstream finds
|
|
804
|
+
* boundaries by looking at content at all.
|
|
805
|
+
* 2. A separator in a CORPUS is ordinary content. If a trainer joins
|
|
806
|
+
* turns with "\n" (example/train_base.ts does), those newlines are
|
|
807
|
+
* simply bytes inside the stream, folded like every other byte. They
|
|
808
|
+
* are a property of that corpus, not of this API and not of the fold.
|
|
809
|
+
* 3. This API can therefore reproduce ANY corpus exactly, with no
|
|
810
|
+
* convention to agree on: replaying a "\n"-joined corpus means passing
|
|
811
|
+
* `"\n" + turnText` as the turn. The separator rides along IN the
|
|
812
|
+
* turn bytes, where it belongs. There is nothing to configure and no
|
|
813
|
+
* mode to select.
|
|
814
|
+
* 4. Inference is not exact-match anyway. Recognition works over
|
|
815
|
+
* sub-spans, canonical equivalence and resonance, so a query that
|
|
816
|
+
* differs from the trained bytes by punctuation or whitespace still
|
|
817
|
+
* reaches the trained forms; it degrades, it does not fail closed.
|
|
818
|
+
*
|
|
819
|
+
* What follows from 1–4: differing separator bytes between a corpus and a
|
|
820
|
+
* query is an ordinary CONTENT difference — the same kind as any other
|
|
821
|
+
* wording difference — and it is measured the same way. It is NOT an
|
|
822
|
+
* incompatibility between the trainer and this API, and it does NOT
|
|
823
|
+
* require choosing a project-wide separator convention. A review that
|
|
824
|
+
* concludes otherwise (this one did, before being corrected) has mistaken
|
|
825
|
+
* its own harness feeding untrained bytes for an architectural defect. */
|
|
711
826
|
addTurn(conv: Conversation, turn: Input): ConversationState {
|
|
712
827
|
const data = this._conversations.get(conv.id);
|
|
713
828
|
if (!data) throw new Error(`Conversation ${conv.id} not found`);
|
|
@@ -730,17 +845,42 @@ export class Mind implements MindContext {
|
|
|
730
845
|
if (!grow) return data.tree;
|
|
731
846
|
const grown = prevLen > 0 ? concat2(data.bytes, turnBytes) : turnBytes;
|
|
732
847
|
if (prevLen > 0) data.boundaries.push(prevLen);
|
|
733
|
-
|
|
848
|
+
// THE PLAIN FOLD, INCREMENTALLY. No boundary set is imposed here: the
|
|
849
|
+
// tree is exactly the tree `perceive(grown)` builds for these bytes, which
|
|
850
|
+
// is exactly the tree the DEPOSIT path folded when it learnt them. That
|
|
851
|
+
// agreement is the whole point — it is what lets a cumulative context
|
|
852
|
+
// resolve to its trained node, and when it was absent the alignment family
|
|
853
|
+
// went quadratic (measured: 5.2M cells on a 476-byte context, against 0
|
|
854
|
+
// when the two sides agree).
|
|
855
|
+
//
|
|
856
|
+
// The optimisation is unaffected by dropping the boundaries, because it
|
|
857
|
+
// never came from them: content cuts are stable under append, so the
|
|
858
|
+
// incremental fold reuses every segment left of the new turn as the SAME
|
|
859
|
+
// object (see contentFoldIncremental). That object identity is what
|
|
860
|
+
// `resolvedSubtrees` — a WeakMap keyed by node identity — needs in order
|
|
861
|
+
// to hit at all. Measured against the stable-prefix fold it replaces:
|
|
862
|
+
// ~40 rebuilt nodes per turn either way, flat as the context grows
|
|
863
|
+
// sevenfold, and ~92% of nodes reused by identity in both.
|
|
864
|
+
//
|
|
865
|
+
// `data.boundaries` is still tracked, and is still exact — it is API
|
|
866
|
+
// metadata (ConversationState, answeredSpans, currentTurnStart), not a
|
|
867
|
+
// fold instruction.
|
|
868
|
+
const folded = contentFoldIncremental(
|
|
734
869
|
this.space,
|
|
735
870
|
this.alphabet,
|
|
736
871
|
grown,
|
|
737
|
-
|
|
738
|
-
undefined,
|
|
739
|
-
data.boundaries.length > 0 ? data.boundaries : undefined,
|
|
872
|
+
data.content,
|
|
740
873
|
);
|
|
874
|
+
const tree = folded.tree;
|
|
875
|
+
data.content = folded.fold;
|
|
741
876
|
data.tree = tree;
|
|
742
877
|
data.bytes = grown;
|
|
743
|
-
|
|
878
|
+
// Seeded under the PLAIN content key, and that is now the only key there
|
|
879
|
+
// is: with no boundary set imposed, this tree IS what `perceive(grown)`
|
|
880
|
+
// computes, so the memo entry is an ordinary cache hit rather than the
|
|
881
|
+
// deliberate alias it had to be while the two folds differed. The entry
|
|
882
|
+
// saves the pipeline re-folding the context it was just handed.
|
|
883
|
+
data.perceiveMemo.set(perceiveKey(grown), tree);
|
|
744
884
|
return tree;
|
|
745
885
|
}
|
|
746
886
|
|
|
@@ -587,6 +587,13 @@ export interface MechanismResult {
|
|
|
587
587
|
unexplained: string;
|
|
588
588
|
/** Explicit weight override. When absent, weight = moves + PASS·unaccounted. */
|
|
589
589
|
weight?: number;
|
|
590
|
+
/** Bytes of `bytes` that came from spans nothing recognised — the asker's
|
|
591
|
+
* own words carried through verbatim rather than derived (see
|
|
592
|
+
* {@link liftedScaffolding}). Reported, not priced: the ladder prices what
|
|
593
|
+
* a candidate leaves UNACCOUNTED, and this orders candidates that tie on
|
|
594
|
+
* exactly that. Omit when a mechanism composes its answer entirely from
|
|
595
|
+
* recognised material, which is the usual case. */
|
|
596
|
+
scaffolding?: number;
|
|
590
597
|
/** Override the mechanism's default provenance for this result.
|
|
591
598
|
* When absent, the pipeline uses `mech.provenance`. */
|
|
592
599
|
provenance?: string;
|
package/src/mind/pipeline.ts
CHANGED
|
@@ -189,6 +189,10 @@ export async function think(
|
|
|
189
189
|
accounted: ReadonlyArray<[number, number]>;
|
|
190
190
|
unexplained: string;
|
|
191
191
|
complete?: boolean;
|
|
192
|
+
/** Bytes of this candidate's ANSWER that came from spans nothing
|
|
193
|
+
* recognised — query words carried through verbatim (see
|
|
194
|
+
* {@link liftedScaffolding}). Absent means none/unreported. */
|
|
195
|
+
scaffolding?: number;
|
|
192
196
|
}
|
|
193
197
|
const grade = (w: number) => Math.floor(w / STEP);
|
|
194
198
|
const unaccounted = (spans: ReadonlyArray<[number, number]>): number =>
|
|
@@ -205,7 +209,34 @@ export async function think(
|
|
|
205
209
|
if (c.bytes.length === 0) return;
|
|
206
210
|
if (ctx.meter) ctx.meter.candidates++;
|
|
207
211
|
candidates.push(c);
|
|
208
|
-
if (best === null
|
|
212
|
+
if (best === null) {
|
|
213
|
+
best = c;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
const g = grade(c.weight), gb = grade(best.weight);
|
|
217
|
+
if (g < gb) {
|
|
218
|
+
best = c;
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
// TIE-BREAK: AT EQUAL GRADE, PREFER THE ANSWER THAT INVENTS LESS.
|
|
222
|
+
//
|
|
223
|
+
// The ladder prices what a candidate leaves UNACCOUNTED, which is the
|
|
224
|
+
// right primary question but cannot separate two candidates that leave
|
|
225
|
+
// the same bytes unaccounted — and then the winner is whichever mechanism
|
|
226
|
+
// happened to be considered first, which is not a reason.
|
|
227
|
+
//
|
|
228
|
+
// What still separates them is what they DID with those bytes. A
|
|
229
|
+
// candidate that carries an unexplained span into its answer is passing
|
|
230
|
+
// the asker's own words back as if they were derived; one that leaves
|
|
231
|
+
// them out has made a smaller, honest claim. Measured on test/22's
|
|
232
|
+
// two-fact chain: cover and recall both graded 11001 over 11 unexplained
|
|
233
|
+
// bytes, cover answering "The capital of France is Paris famous for" (11
|
|
234
|
+
// bytes of scaffolding) against recall's crossing of the hop (0). Order
|
|
235
|
+
// alone decided it, and the shallower reading won.
|
|
236
|
+
//
|
|
237
|
+
// This never overrides the ladder — it only orders within one grade, so
|
|
238
|
+
// coverage and moves still dominate exactly as before.
|
|
239
|
+
if (g === gb && (c.scaffolding ?? 0) < (best.scaffolding ?? 0)) best = c;
|
|
209
240
|
};
|
|
210
241
|
const worthRunning = (floor: number) =>
|
|
211
242
|
best === null || grade(floor) < grade(best.weight);
|
|
@@ -260,6 +291,7 @@ export async function think(
|
|
|
260
291
|
accounted: r.accounted,
|
|
261
292
|
unexplained: r.unexplained,
|
|
262
293
|
complete: r.complete,
|
|
294
|
+
scaffolding: r.scaffolding,
|
|
263
295
|
});
|
|
264
296
|
}
|
|
265
297
|
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
// prefix-completion.ts — Grounding a query that IS the opening of a trained
|
|
2
|
+
// form.
|
|
3
|
+
//
|
|
4
|
+
// THE SHAPE. `The capital of France is` grounds nothing, while
|
|
5
|
+
// `The capital of France is Paris.` is trained and reads back byte-exact. The
|
|
6
|
+
// query is not SIMILAR to that form, it is a PROPER PREFIX of it: every query
|
|
7
|
+
// byte is a literal match, in order, from offset zero. That is the strongest
|
|
8
|
+
// grounding relation in the store — stronger than the bridge's corroborated
|
|
9
|
+
// substitution, which pays a CONCEPT per substituted span, and stronger than
|
|
10
|
+
// resonance, which only claims an angle. Nothing is invented: the answer IS a
|
|
11
|
+
// trained form, voiced whole.
|
|
12
|
+
//
|
|
13
|
+
// NO NOTION OF TEXT. This mechanism reads bytes and geometry only. It has no
|
|
14
|
+
// separator, no character class, no "word": the only structural quantity it
|
|
15
|
+
// uses is W, the river's grouping window, which is the same capacity the
|
|
16
|
+
// perception tree groups by and the same bar the argument-binding tier holds
|
|
17
|
+
// its constituents to. A completion shorter than one grouping window carries
|
|
18
|
+
// no structure the geometry can perceive, whatever the modality — that is a
|
|
19
|
+
// statement about the fold, not about punctuation. Presentation (what is
|
|
20
|
+
// "spacing", what is "case") belongs to the injected canon and to the modality
|
|
21
|
+
// entry point, never here; see src/canon.ts.
|
|
22
|
+
//
|
|
23
|
+
// WHY THE EARLIER TIERS CANNOT DO IT. Two independent reasons, both measured:
|
|
24
|
+
//
|
|
25
|
+
// 1. `resolve(prefix)` is null. A proper prefix of a deposited stream has no
|
|
26
|
+
// branch of its own unless it was itself deposited, so the exact tiers
|
|
27
|
+
// have nothing to find.
|
|
28
|
+
// 2. The form is not among the resonance candidates AT ALL. Measured on the
|
|
29
|
+
// trained store: cos(query, that form) = 0.5752, yet the form is absent
|
|
30
|
+
// from `resonate(k)` at k = 24, 256 AND 2048 — while forms scoring LOWER
|
|
31
|
+
// (Germany 0.5670, Yemen 0.5591) are returned. `k` only reorders WITHIN
|
|
32
|
+
// the IVF clusters already probed, exactly as Store.resonate's doc warns,
|
|
33
|
+
// so no k recovers it. With `exhaustive` it ranks 8.
|
|
34
|
+
//
|
|
35
|
+
// So this is a RETRIEVABILITY gap, not a semantic one, and it is repaired by
|
|
36
|
+
// reading the candidate list recall's refusal path has ALREADY fetched
|
|
37
|
+
// exhaustively for the substitution bridge — never by resonating on its own.
|
|
38
|
+
// Measured cost of the scan over those 570 candidates: 2.9 ms warm, 20.4 ms
|
|
39
|
+
// cold, against a ~700 ms refusal path. Issuing a FRESH exhaustive call would
|
|
40
|
+
// cost 490 ms median against 13 ms non-exhaustive (36×), which is why this tier
|
|
41
|
+
// takes the candidate list as an argument and adds nothing to it.
|
|
42
|
+
//
|
|
43
|
+
// THREE GUARDS, each falsified into existence by measurement — do not drop any:
|
|
44
|
+
//
|
|
45
|
+
// 1. AN UNREADABLE CONTINUATION VETOES. Reads are bounded (a stored span can
|
|
46
|
+
// run to hundreds of kilobytes), so a candidate that opens with the query
|
|
47
|
+
// but SATURATES the read continues in a way nobody can see. It is a
|
|
48
|
+
// standing disagreement: if any such candidate exists, nothing is grounded.
|
|
49
|
+
// It must NOT be quietly skipped, and that is not a stylistic point — the
|
|
50
|
+
// skip is what MANUFACTURES a fragment. Measured on a one-deposit fixture
|
|
51
|
+
// whose form exceeds the cap: the query matched BOTH the whole 138-byte
|
|
52
|
+
// form (saturating) AND an interior fold node of 34 bytes (unsaturated,
|
|
53
|
+
// continuing `" Paris, an"`). Skipping the saturated candidate removed the
|
|
54
|
+
// only evidence that disagreed, uniqueness then passed on the interior
|
|
55
|
+
// node, and a mid-form slice was voiced as an answer. Suppressing the
|
|
56
|
+
// disagreement is what created the fabrication.
|
|
57
|
+
// (Testing instead whether a candidate is a "complete form" via the fold
|
|
58
|
+
// does NOT work and was measured: content addressing makes an interior
|
|
59
|
+
// node resolve to ITSELF, so self-resolution says nothing about
|
|
60
|
+
// completeness.)
|
|
61
|
+
// 2. THE CONTINUATION MUST REACH ONE GROUPING WINDOW. A trained
|
|
62
|
+
// `What is the capital of France??` opens with `What is the capital of
|
|
63
|
+
// France?` and continues by a single byte. Below W the continuation is
|
|
64
|
+
// sub-quantum — the fold groups nothing from it — and voicing it produces
|
|
65
|
+
// the degenerate reply that is a known failure smell.
|
|
66
|
+
// 3. UNIQUENESS. Several trained forms may open with the query and continue
|
|
67
|
+
// differently, and then the corpus does not say which continuation the
|
|
68
|
+
// asker means. Distinct continuations ⇒ refuse. This is the documented
|
|
69
|
+
// PREFIX TRAP, and it is real — just not for every prefix. Measured: of
|
|
70
|
+
// 15 battery probes exactly ONE yields a unique continuation, and all
|
|
71
|
+
// three honest-silence probes yield none (including `What is the capital
|
|
72
|
+
// of Zamunda?`, whose top hit scores 0.83).
|
|
73
|
+
//
|
|
74
|
+
// Uniqueness is judged on the continuation BYTES, not on the candidate id: the
|
|
75
|
+
// same continuation reached through two trained forms is one answer, not an
|
|
76
|
+
// ambiguity.
|
|
77
|
+
|
|
78
|
+
import type { MindContext } from "./types.js";
|
|
79
|
+
import { bytesEqual } from "../bytes.js";
|
|
80
|
+
import { rItem } from "./trace.js";
|
|
81
|
+
import { canonicalWindows, leafIdPrefix } from "./canonical.js";
|
|
82
|
+
import { hubBound } from "./traverse.js";
|
|
83
|
+
|
|
84
|
+
/** Trained forms the query may OPEN, proposed from the write side's own
|
|
85
|
+
* leaf-id window index — the supply of last resort for {@link
|
|
86
|
+
* prefixCompletion}.
|
|
87
|
+
*
|
|
88
|
+
* WHY A SECOND SUPPLY EXISTS. The ranked list this mechanism normally reads
|
|
89
|
+
* is a resonance list, and resonance cannot rank a proper prefix: measured on
|
|
90
|
+
* the trained store, cos(prefix, form) falls from 0.9629 at a one-byte
|
|
91
|
+
* truncation to 0.6206 at three bytes, against a reachThreshold of 0.8750.
|
|
92
|
+
* Three bytes of truncation put the answer out of reach on GEOMETRY, not on a
|
|
93
|
+
* bug, so no k and no re-ranking recovers it.
|
|
94
|
+
*
|
|
95
|
+
* WHY THIS ROUTE WORKS WHERE THE FOLD DOES NOT. A query's own fold is
|
|
96
|
+
* useless here: content addressing is not phrase-position-invariant, so a
|
|
97
|
+
* standalone prefix folds to a DIFFERENT node than the same bytes sitting
|
|
98
|
+
* inside a longer deposit, and neither the prefix's own node nor its
|
|
99
|
+
* ancestors lead to the deposit (measured: the 22-byte prefix of the
|
|
100
|
+
* photosynthesis form resolves, is shared by 6 contexts, and does not have
|
|
101
|
+
* the form among its ancestors). Leaf ids ARE position-invariant — they are
|
|
102
|
+
* content-addressed on single bytes — and `indexSubSpans` already interns a
|
|
103
|
+
* flat branch over every canonical WINDOW of a deposit's leaf-id stream, with
|
|
104
|
+
* containment edges to the chunks that window spans. A query that is a
|
|
105
|
+
* prefix therefore shares those window nodes exactly, and reaches the deposit
|
|
106
|
+
* by climbing containment then parents. Nothing is added to the write side;
|
|
107
|
+
* this reads an index training already built.
|
|
108
|
+
*
|
|
109
|
+
* BOUNDED (§2.8), AND WITH NO NEW THRESHOLD. The window whose containment is
|
|
110
|
+
* SMALLEST carries the most evidence, and one saturated at `hubBound` carries
|
|
111
|
+
* none — that is the same √N reading of "hub" the rest of the mind uses, not
|
|
112
|
+
* a tuned knob. The upward walk spends a budget of `hubBound` nodes and
|
|
113
|
+
* fans out by W, so a hub query enumerates nothing and the caller stays
|
|
114
|
+
* silent rather than guessing (§2.13). Measured on the trained store: the
|
|
115
|
+
* photosynthesis form at a one-byte truncation picks a window with 52
|
|
116
|
+
* containers, visits 446 nodes, and yields exactly ONE candidate that
|
|
117
|
+
* survives the caller's byte compare — the form itself.
|
|
118
|
+
*
|
|
119
|
+
* These are PROPOSALS only. Every candidate still faces the byte-exact
|
|
120
|
+
* prefix compare and all three guards below, so a wrong proposal costs one
|
|
121
|
+
* bounded read and can never be voiced (§2.3). */
|
|
122
|
+
export function prefixCandidates(
|
|
123
|
+
ctx: MindContext,
|
|
124
|
+
query: Uint8Array,
|
|
125
|
+
): number[] {
|
|
126
|
+
const store = ctx.store;
|
|
127
|
+
const W = ctx.space.maxGroup;
|
|
128
|
+
const run = leafIdPrefix(ctx, query);
|
|
129
|
+
// The widest canonical window is the most discriminative one the write side
|
|
130
|
+
// ever interned; a query too short to spell one carries no window evidence.
|
|
131
|
+
const len = canonicalWindows(W)[1];
|
|
132
|
+
if (run.length < len) return [];
|
|
133
|
+
const bound = hubBound(ctx);
|
|
134
|
+
|
|
135
|
+
let best: number | null = null;
|
|
136
|
+
let bestN = 0;
|
|
137
|
+
for (let off = 0; off + len <= run.length; off++) {
|
|
138
|
+
const wid = store.findBranch(run.slice(off, off + len));
|
|
139
|
+
if (wid === null) continue;
|
|
140
|
+
const n = store.containersSlice(wid, 0, bound).length;
|
|
141
|
+
// Empty says the window spans no chunk; saturated says it is a hub, whose
|
|
142
|
+
// containment discriminates nothing. Neither is evidence.
|
|
143
|
+
if (n === 0 || n >= bound) continue;
|
|
144
|
+
if (best === null || n < bestN) {
|
|
145
|
+
best = wid;
|
|
146
|
+
bestN = n;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (best === null) return [];
|
|
150
|
+
|
|
151
|
+
let frontier = store.containersSlice(best, 0, bound);
|
|
152
|
+
const seen = new Set<number>(frontier);
|
|
153
|
+
let budget = bound;
|
|
154
|
+
while (frontier.length > 0 && budget > 0) {
|
|
155
|
+
const next: number[] = [];
|
|
156
|
+
for (const f of frontier) {
|
|
157
|
+
if (budget-- <= 0) break;
|
|
158
|
+
for (const p of store.parentsFirst(f, W)) {
|
|
159
|
+
if (seen.has(p)) continue;
|
|
160
|
+
seen.add(p);
|
|
161
|
+
next.push(p);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
frontier = next;
|
|
165
|
+
}
|
|
166
|
+
return [...seen];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** A trained form the query opens, and the bytes by which it continues. */
|
|
170
|
+
export interface PrefixCompletion {
|
|
171
|
+
/** The trained form whose opening the query is — the answer, voiced whole. */
|
|
172
|
+
id: number;
|
|
173
|
+
/** The form's own bytes. The mechanism grounds a FORM, never a slice of
|
|
174
|
+
* one: slicing at the query's end would cut at an offset the geometry has
|
|
175
|
+
* no reason to treat as a boundary. */
|
|
176
|
+
form: Uint8Array;
|
|
177
|
+
/** The bytes past the query — carried for the rationale and for the
|
|
178
|
+
* uniqueness comparison, not voiced on its own. */
|
|
179
|
+
continuation: Uint8Array;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** The sole trained form the query opens — or null when no candidate opens with
|
|
183
|
+
* it, when the continuation is sub-quantum, when a candidate's continuation
|
|
184
|
+
* cannot be read through, or when the candidates disagree.
|
|
185
|
+
*
|
|
186
|
+
* `ranked` must be a list the caller has ALREADY fetched; this mechanism never
|
|
187
|
+
* resonates on its own (see the header's cost note). */
|
|
188
|
+
export function prefixCompletion(
|
|
189
|
+
ctx: MindContext,
|
|
190
|
+
query: Uint8Array,
|
|
191
|
+
ranked: ReadonlyArray<number>,
|
|
192
|
+
): PrefixCompletion | null {
|
|
193
|
+
const W = ctx.space.maxGroup;
|
|
194
|
+
const t = ctx.trace?.enter("prefixCompletion", [rItem(query, "query")]);
|
|
195
|
+
const done = (
|
|
196
|
+
hit: PrefixCompletion | null,
|
|
197
|
+
note: string,
|
|
198
|
+
data?: unknown,
|
|
199
|
+
): PrefixCompletion | null => {
|
|
200
|
+
t?.done(
|
|
201
|
+
hit === null ? [] : [rItem(hit.continuation, "continuation", hit.id)],
|
|
202
|
+
note,
|
|
203
|
+
data,
|
|
204
|
+
);
|
|
205
|
+
return hit;
|
|
206
|
+
};
|
|
207
|
+
// Reads are bounded to phrase scale, the same bound the frame filler uses.
|
|
208
|
+
// A query with no room for a whole grouping window past its own length
|
|
209
|
+
// cannot clear guard 2, so it is not worth a single read.
|
|
210
|
+
const cap = query.length * W;
|
|
211
|
+
if (query.length === 0 || cap < query.length + W) {
|
|
212
|
+
return done(null, "no room for a perceivable continuation within the cap");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Distinct continuations, each with the first form that offered it. Held as
|
|
216
|
+
// a list, not a byte-keyed map: candidates that open with the query are few
|
|
217
|
+
// (measured: 1 on the trained store's winning query), and a linear byte
|
|
218
|
+
// compare needs no string encoding of content. Uniqueness (guard 3) is
|
|
219
|
+
// decided over this list, so the scan cannot stop early — a second
|
|
220
|
+
// continuation IS the refusal, and finding it is the point.
|
|
221
|
+
const found: PrefixCompletion[] = [];
|
|
222
|
+
let opened = 0;
|
|
223
|
+
let unreadable = 0;
|
|
224
|
+
let subQuantum = 0;
|
|
225
|
+
for (const id of ranked) {
|
|
226
|
+
const form = ctx.store.bytesPrefix(id, cap);
|
|
227
|
+
if (form.length <= query.length) continue;
|
|
228
|
+
let opens = true;
|
|
229
|
+
for (let i = 0; i < query.length; i++) {
|
|
230
|
+
if (form[i] !== query[i]) {
|
|
231
|
+
opens = false;
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (!opens) continue;
|
|
236
|
+
opened++;
|
|
237
|
+
// Guard 1: a saturated read continues out of sight — a disagreement that
|
|
238
|
+
// cannot be resolved, so it ends the search rather than being skipped.
|
|
239
|
+
if (form.length >= cap) {
|
|
240
|
+
unreadable++;
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const rest = form.subarray(query.length);
|
|
244
|
+
// Guard 2: below one grouping window there is no structure to voice.
|
|
245
|
+
if (rest.length < W) {
|
|
246
|
+
subQuantum++;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
if (!found.some((f) => bytesEqual(f.continuation, rest))) {
|
|
250
|
+
found.push({ id, form, continuation: rest });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const data = {
|
|
255
|
+
candidates: ranked.length,
|
|
256
|
+
opened,
|
|
257
|
+
unreadable,
|
|
258
|
+
subQuantum,
|
|
259
|
+
distinctContinuations: found.length,
|
|
260
|
+
};
|
|
261
|
+
if (unreadable > 0 && found.length > 0) {
|
|
262
|
+
return done(
|
|
263
|
+
null,
|
|
264
|
+
"a form opens with this query but continues past the read bound — " +
|
|
265
|
+
"its continuation cannot be read, so none is licensed",
|
|
266
|
+
data,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
// Guard 2b: A SUB-QUANTUM CONTINUATION IS STILL A DISAGREEMENT. Guard 2
|
|
270
|
+
// refuses to VOICE a below-window continuation, and rightly — there is no
|
|
271
|
+
// structure there to speak. But dropping such a candidate from the
|
|
272
|
+
// uniqueness tally silently converts "the corpus offers many continuations,
|
|
273
|
+
// most of them unvoiceable" into "the corpus offers exactly one", and
|
|
274
|
+
// guard 3 then passes VACUOUSLY on the sole survivor. That is precisely
|
|
275
|
+
// the failure guard 1 documents for unreadable continuations — suppressing
|
|
276
|
+
// the disagreement is what manufactures the answer — so it is answered the
|
|
277
|
+
// same way, and for the same reason.
|
|
278
|
+
//
|
|
279
|
+
// Measured on a 4,300-fact fixture of "what is the value of <i>?": the
|
|
280
|
+
// query "what is the value of" drew candidates continuing " 0?", " 4?",
|
|
281
|
+
// " 8?" (3 bytes, sub-quantum at W=4) and " 10?" (4 bytes). The first
|
|
282
|
+
// three were dropped, leaving one survivor, and the mechanism reported
|
|
283
|
+
// "exactly one trained form" and voiced "the value of 10 is 20" — an
|
|
284
|
+
// arbitrary pick from thousands of equally-good readings, with the
|
|
285
|
+
// evidence of ambiguity discarded on the way.
|
|
286
|
+
//
|
|
287
|
+
// Note this can only ever cause SILENCE, never a different answer: it
|
|
288
|
+
// withholds a completion the corpus does not uniquely license.
|
|
289
|
+
if (subQuantum > 0 && found.length > 0) {
|
|
290
|
+
return done(
|
|
291
|
+
null,
|
|
292
|
+
"other trained forms open with this query but continue below one " +
|
|
293
|
+
"grouping window — the corpus offers competing readings, so no " +
|
|
294
|
+
"single completion is licensed",
|
|
295
|
+
data,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
// Guard 3: the corpus must agree on ONE continuation.
|
|
299
|
+
if (found.length !== 1) {
|
|
300
|
+
return done(
|
|
301
|
+
null,
|
|
302
|
+
found.length === 0
|
|
303
|
+
? "no trained form opens with this query and continues perceivably"
|
|
304
|
+
: "trained forms open with this query but continue differently — " +
|
|
305
|
+
"the corpus does not say which continuation is meant",
|
|
306
|
+
data,
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
return done(
|
|
310
|
+
found[0],
|
|
311
|
+
"one trained form opens with this query, and continues perceivably",
|
|
312
|
+
data,
|
|
313
|
+
);
|
|
314
|
+
}
|