@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.
Files changed (63) hide show
  1. package/AGENTS.md +290 -77
  2. package/HOW_IT_WORKS.md +2170 -735
  3. package/dist/example/train_base.d.ts +9 -3
  4. package/dist/example/train_base.js +21 -4
  5. package/dist/src/canon.d.ts +19 -0
  6. package/dist/src/canon.js +28 -0
  7. package/dist/src/geometry.d.ts +52 -0
  8. package/dist/src/geometry.js +87 -1
  9. package/dist/src/mind/bridge.js +27 -1
  10. package/dist/src/mind/frame-filler.d.ts +15 -0
  11. package/dist/src/mind/frame-filler.js +535 -0
  12. package/dist/src/mind/learning.js +6 -11
  13. package/dist/src/mind/mechanisms/cast.js +72 -2
  14. package/dist/src/mind/mechanisms/cover.js +6 -1
  15. package/dist/src/mind/mechanisms/extraction.js +27 -0
  16. package/dist/src/mind/mechanisms/recall.js +214 -34
  17. package/dist/src/mind/mind.d.ts +49 -1
  18. package/dist/src/mind/mind.js +137 -10
  19. package/dist/src/mind/pipeline-mechanism.d.ts +7 -0
  20. package/dist/src/mind/pipeline.js +29 -1
  21. package/dist/src/mind/prefix-completion.d.ts +59 -0
  22. package/dist/src/mind/prefix-completion.js +270 -0
  23. package/dist/src/mind/primitives.d.ts +29 -10
  24. package/dist/src/mind/primitives.js +52 -61
  25. package/dist/src/mind/recognition.js +119 -9
  26. package/dist/src/mind/traverse.d.ts +32 -0
  27. package/dist/src/mind/traverse.js +52 -0
  28. package/dist/src/mind/types.d.ts +55 -16
  29. package/dist/src/mind/types.js +68 -19
  30. package/dist/src/store.d.ts +21 -0
  31. package/dist/src/store.js +21 -0
  32. package/example/train_base.ts +21 -4
  33. package/package.json +1 -1
  34. package/src/canon.ts +28 -0
  35. package/src/geometry.ts +100 -1
  36. package/src/mind/bridge.ts +34 -0
  37. package/src/mind/frame-filler.ts +604 -0
  38. package/src/mind/learning.ts +5 -9
  39. package/src/mind/mechanisms/cast.ts +70 -2
  40. package/src/mind/mechanisms/cover.ts +6 -1
  41. package/src/mind/mechanisms/extraction.ts +27 -0
  42. package/src/mind/mechanisms/recall.ts +236 -37
  43. package/src/mind/mind.ts +154 -14
  44. package/src/mind/pipeline-mechanism.ts +7 -0
  45. package/src/mind/pipeline.ts +33 -1
  46. package/src/mind/prefix-completion.ts +314 -0
  47. package/src/mind/primitives.ts +59 -70
  48. package/src/mind/recognition.ts +117 -6
  49. package/src/mind/traverse.ts +52 -0
  50. package/src/mind/types.ts +98 -42
  51. package/src/store.ts +25 -0
  52. package/test/13-conversation.test.mjs +13 -0
  53. package/test/57-fusion-order.test.mjs +65 -0
  54. package/test/66-query-edge-whitespace.test.mjs +99 -0
  55. package/test/67-climb-anchor-breadth.test.mjs +113 -0
  56. package/test/68-extraction-unanchored.test.mjs +79 -0
  57. package/test/69-frame-filler.test.mjs +115 -0
  58. package/test/70-prefix-completion.test.mjs +170 -0
  59. package/test/71-embedded-canon-equivalence.test.mjs +121 -0
  60. package/test/72-prefix-candidate-supply.test.mjs +114 -0
  61. package/test/73-scaffolding-only-bridge-abstains.test.mjs +178 -0
  62. package/test/74-prefix-trap-not-sprung-early.test.mjs +114 -0
  63. package/test/75-multiturn-context-optimisation.test.mjs +1082 -0
@@ -62,9 +62,15 @@ export declare function bestOasstPath(root: OasstNode): OasstTurn[];
62
62
  * turn experiences and local adjacent-pair facts are NOT emitted (they are
63
63
  * subsumed by it and would merely replicate the content).
64
64
  *
65
- * The walk is byte-for-byte the pattern proven in test/13-conversation.test.mjs
66
- * ("teachConversation"): each turn is the continuation of all prior turns joined
67
- * by "\n", with BARE turn text — NO "User:/Assistant:" labels. Roles already
65
+ * The walk is the pattern proven in test/13-conversation.test.mjs
66
+ * ("teachConversation"): each turn is the continuation of all prior turns,
67
+ * with BARE turn text — NO "User:/Assistant:" labels. The SHAPE is identical
68
+ * (cumulative context → next turn); the join string is not, and does not need
69
+ * to be — that file joins with nothing and this corpus joins with "\n" (see
70
+ * `accumulate`). Saying "byte-for-byte", as this comment used to, invites the
71
+ * reading that the two must agree on a separator. They must not agree,
72
+ * because there is nothing to agree about: turn boundaries are offsets, and
73
+ * the join string is just corpus text. Roles already
68
74
  * alternate by position in an oasst2 best-path (the root is a prompter), so a
69
75
  * label adds nothing the position does not, while a clean continuation matches
70
76
  * the test's recall (predictNext queries bare prior turns) and lets a turn share
@@ -594,7 +594,18 @@ const isEpisode = (it) => typeof it !== "string";
594
594
  /** Build the accumulated-context episodes of a turn sequence: each successive
595
595
  * turn is the continuation of ALL the turns before it joined together. This is
596
596
  * the same cumulative-context shape a multi-turn conversation deposits, so the
597
- * store learns to continue a growing context. */
597
+ * store learns to continue a growing context.
598
+ *
599
+ * The "\n" below is a CORPUS choice, not a protocol. oasst2 turns are
600
+ * paragraphs, and reading them back with the newlines kept is how this corpus
601
+ * reads naturally; a different corpus may join with nothing, and
602
+ * test/13-conversation.test.mjs does exactly that. Neither has to match the
603
+ * other, because Sema never scans content for turn boundaries — those are
604
+ * offsets the Conversation API carries beside the bytes (see Mind.addTurn's
605
+ * "ON SEPARATORS" note). The newline here is simply part of the text this
606
+ * store learnt, so anything replaying this corpus feeds it back as part of
607
+ * the turn: `addTurn(conv, "\n" + turnText)`. It is not a convention the
608
+ * engine, the API, or the tests have to agree on. */
598
609
  function accumulate(turns) {
599
610
  const out = [];
600
611
  for (let i = 1; i < turns.length; i++) {
@@ -709,9 +720,15 @@ export function bestOasstPath(root) {
709
720
  * turn experiences and local adjacent-pair facts are NOT emitted (they are
710
721
  * subsumed by it and would merely replicate the content).
711
722
  *
712
- * The walk is byte-for-byte the pattern proven in test/13-conversation.test.mjs
713
- * ("teachConversation"): each turn is the continuation of all prior turns joined
714
- * by "\n", with BARE turn text — NO "User:/Assistant:" labels. Roles already
723
+ * The walk is the pattern proven in test/13-conversation.test.mjs
724
+ * ("teachConversation"): each turn is the continuation of all prior turns,
725
+ * with BARE turn text — NO "User:/Assistant:" labels. The SHAPE is identical
726
+ * (cumulative context → next turn); the join string is not, and does not need
727
+ * to be — that file joins with nothing and this corpus joins with "\n" (see
728
+ * `accumulate`). Saying "byte-for-byte", as this comment used to, invites the
729
+ * reading that the two must agree on a separator. They must not agree,
730
+ * because there is nothing to agree about: turn boundaries are offsets, and
731
+ * the join string is just corpus text. Roles already
715
732
  * alternate by position in an oasst2 best-path (the root is a prompter), so a
716
733
  * label adds nothing the position does not, while a clean continuation matches
717
734
  * the test's recall (predictNext queries bare prior turns) and lets a turn share
@@ -24,3 +24,22 @@ export declare function textCanon(bytes: Uint8Array): Uint8Array;
24
24
  * is keyed on. Same construction as the node table's content hash; a
25
25
  * collision is resolved by verifying canon(stored) === key, never trusted. */
26
26
  export declare function canonHash(key: Uint8Array): number;
27
+ /** The span of `bytes` between its first and last non-whitespace byte — the
28
+ * QUESTION, with the caller's edge spacing dropped. Returns a subarray (no
29
+ * copy), and the original when there is nothing to trim.
30
+ *
31
+ * THIS LIVES HERE, not in the core byte utilities, for the reason stated at
32
+ * the top of this file: "nothing in the store or the mind's core knows what
33
+ * 'case' or 'whitespace' is". Edge spacing is a TEXT fact — for a binary or
34
+ * grid modality 0x20 is content, not presentation — so it belongs beside the
35
+ * text canonicalizer, is injected on the same modality test, and never leaks
36
+ * into a mechanism. A modality that supplies its own canon supplies its own
37
+ * reading of "edge" too, or none.
38
+ *
39
+ * Why trimming is sound HERE when {@link textCanon} deliberately refuses it:
40
+ * canon preserves edge whitespace because the hazard is a recognised SUB-span
41
+ * swallowing the boundary byte that separates it from its neighbour (observed:
42
+ * "ice " matching the stored "ice"). At the outer edges of a WHOLE input
43
+ * there is no neighbour — nothing precedes byte 0, nothing follows the last
44
+ * byte — so that hazard cannot arise, and only there. */
45
+ export declare function textEdgeTrim(bytes: Uint8Array): Uint8Array;
package/dist/src/canon.js CHANGED
@@ -55,3 +55,31 @@ export function canonHash(key) {
55
55
  }
56
56
  return h >>> 0;
57
57
  }
58
+ /** The span of `bytes` between its first and last non-whitespace byte — the
59
+ * QUESTION, with the caller's edge spacing dropped. Returns a subarray (no
60
+ * copy), and the original when there is nothing to trim.
61
+ *
62
+ * THIS LIVES HERE, not in the core byte utilities, for the reason stated at
63
+ * the top of this file: "nothing in the store or the mind's core knows what
64
+ * 'case' or 'whitespace' is". Edge spacing is a TEXT fact — for a binary or
65
+ * grid modality 0x20 is content, not presentation — so it belongs beside the
66
+ * text canonicalizer, is injected on the same modality test, and never leaks
67
+ * into a mechanism. A modality that supplies its own canon supplies its own
68
+ * reading of "edge" too, or none.
69
+ *
70
+ * Why trimming is sound HERE when {@link textCanon} deliberately refuses it:
71
+ * canon preserves edge whitespace because the hazard is a recognised SUB-span
72
+ * swallowing the boundary byte that separates it from its neighbour (observed:
73
+ * "ice " matching the stored "ice"). At the outer edges of a WHOLE input
74
+ * there is no neighbour — nothing precedes byte 0, nothing follows the last
75
+ * byte — so that hazard cannot arise, and only there. */
76
+ export function textEdgeTrim(bytes) {
77
+ const space = (b) => b === 0x20 || b === 0x09 || b === 0x0a || b === 0x0d;
78
+ let from = 0;
79
+ let to = bytes.length;
80
+ while (from < to && space(bytes[from]))
81
+ from++;
82
+ while (to > from && space(bytes[to - 1]))
83
+ to--;
84
+ return from === 0 && to === bytes.length ? bytes : bytes.subarray(from, to);
85
+ }
@@ -131,6 +131,58 @@ export declare function knownPrefixLength(bytes: Uint8Array, leafAt: (i: number)
131
131
  * turn extend perception instead of refolding it: identical prefixes
132
132
  * produce identical subtrees regardless of what follows them. */
133
133
  export declare function bytesToTree(space: Space, alphabet: Alphabet, bytes: Uint8Array, leafAt?: (i: number) => number | null, lookup?: (leafIds: number[]) => number | null, boundaries?: readonly number[]): Sema;
134
+ /** A plain content fold's reusable state: the level-0 cut edges over the whole
135
+ * stream and each segment's independently-folded root. See
136
+ * {@link contentFoldIncremental}. */
137
+ export interface ContentFold {
138
+ edges: number[];
139
+ segs: Folded[];
140
+ }
141
+ /** {@link contentFoldSpan} over a WHOLE stream, reusing the segments a previous
142
+ * fold of a byte-identical prefix already produced.
143
+ *
144
+ * WHY THIS IS SOUND, AND WHY IT NEEDS NO BOUNDARIES. A level-0 segment is a
145
+ * pure function of its own bytes ({@link flatFold} reads nothing else), so
146
+ * reusing one whose [start,end) is unchanged is bit-identical to refolding it
147
+ * — the cache can never change the tree, only skip work. And the cuts
148
+ * themselves are stable under APPEND: {@link contentLevels} decides each cut
149
+ * from a rolling hash over a local window, so bytes added at the right edge
150
+ * cannot move a cut to their left (measured over a growing 12-turn context:
151
+ * 100% of prior cuts survive every append, zero tail churn). Together those
152
+ * two facts are the whole optimisation — a grown stream refolds only the
153
+ * segments at its right edge.
154
+ *
155
+ * This is the reuse the conversation path wants, and it costs NOTHING in
156
+ * structure: the tree is exactly the tree {@link bytesToTree} builds for the
157
+ * same bytes with no boundary set at all. Turn boundaries buy prefix-ROOT
158
+ * identity, which is a different property from incremental reuse; conflating
159
+ * the two is what put an imposed boundary set on the inference path and left
160
+ * it folding differently from the deposits it was querying.
161
+ *
162
+ * `groupByLevel` above the segments is re-run whole. It operates on segment
163
+ * ROOTS (a few dozen items for a several-hundred-byte context), not on bytes,
164
+ * and only its right edge actually changes shape — measured at ~40 rebuilt
165
+ * nodes per turn, flat as the context grows sevenfold.
166
+ *
167
+ * PRECONDITION — `prev` MUST have been folded over a BYTE-IDENTICAL PREFIX of
168
+ * `bytes`. Reuse is keyed on a segment's [start,end) OFFSETS, which is what
169
+ * makes it O(1) per segment; offsets alone cannot witness that the underlying
170
+ * bytes agree. Hand it a fold of DIFFERENT bytes whose cuts happen to land
171
+ * in the same places and it will splice those foreign segments in — measured,
172
+ * a deliberately mismatched `prev` produced a wrong tree on 336 of 400 random
173
+ * streams. Verifying the bytes here would cost O(prefix) and defeat the
174
+ * whole point, so the obligation sits with the caller, and every caller
175
+ * discharges it structurally rather than by care: `perceiveDeposit` looks the
176
+ * entry up under `latin1Key(bytes.subarray(0, L))` — the prefix's own bytes
177
+ * ARE the cache key — and a conversation's fold state advances only by
178
+ * append. A new caller that cannot make the same structural argument must
179
+ * pass no `prev` at all; the cold path is always correct.
180
+ * ({@link stablePrefixFoldIncremental} carries the identical precondition for
181
+ * the identical reason.) */
182
+ export declare function contentFoldIncremental(space: Space, alphabet: Alphabet, bytes: Uint8Array, prev?: ContentFold): {
183
+ tree: Sema;
184
+ fold: ContentFold;
185
+ };
134
186
  /** A stable-prefix fold's reusable state: the segment edge offsets and each
135
187
  * segment's independently-folded root ({@link riverFoldRaw} output). A
136
188
  * grown stream whose boundary set EXTENDS a previous fold's reuses every
@@ -620,6 +620,84 @@ function contentFoldSpan(space, alphabet, bytes, from, to) {
620
620
  return groupByLevel(space, segs, levels, 1);
621
621
  return segs[0];
622
622
  }
623
+ /** {@link contentFoldSpan} over a WHOLE stream, reusing the segments a previous
624
+ * fold of a byte-identical prefix already produced.
625
+ *
626
+ * WHY THIS IS SOUND, AND WHY IT NEEDS NO BOUNDARIES. A level-0 segment is a
627
+ * pure function of its own bytes ({@link flatFold} reads nothing else), so
628
+ * reusing one whose [start,end) is unchanged is bit-identical to refolding it
629
+ * — the cache can never change the tree, only skip work. And the cuts
630
+ * themselves are stable under APPEND: {@link contentLevels} decides each cut
631
+ * from a rolling hash over a local window, so bytes added at the right edge
632
+ * cannot move a cut to their left (measured over a growing 12-turn context:
633
+ * 100% of prior cuts survive every append, zero tail churn). Together those
634
+ * two facts are the whole optimisation — a grown stream refolds only the
635
+ * segments at its right edge.
636
+ *
637
+ * This is the reuse the conversation path wants, and it costs NOTHING in
638
+ * structure: the tree is exactly the tree {@link bytesToTree} builds for the
639
+ * same bytes with no boundary set at all. Turn boundaries buy prefix-ROOT
640
+ * identity, which is a different property from incremental reuse; conflating
641
+ * the two is what put an imposed boundary set on the inference path and left
642
+ * it folding differently from the deposits it was querying.
643
+ *
644
+ * `groupByLevel` above the segments is re-run whole. It operates on segment
645
+ * ROOTS (a few dozen items for a several-hundred-byte context), not on bytes,
646
+ * and only its right edge actually changes shape — measured at ~40 rebuilt
647
+ * nodes per turn, flat as the context grows sevenfold.
648
+ *
649
+ * PRECONDITION — `prev` MUST have been folded over a BYTE-IDENTICAL PREFIX of
650
+ * `bytes`. Reuse is keyed on a segment's [start,end) OFFSETS, which is what
651
+ * makes it O(1) per segment; offsets alone cannot witness that the underlying
652
+ * bytes agree. Hand it a fold of DIFFERENT bytes whose cuts happen to land
653
+ * in the same places and it will splice those foreign segments in — measured,
654
+ * a deliberately mismatched `prev` produced a wrong tree on 336 of 400 random
655
+ * streams. Verifying the bytes here would cost O(prefix) and defeat the
656
+ * whole point, so the obligation sits with the caller, and every caller
657
+ * discharges it structurally rather than by care: `perceiveDeposit` looks the
658
+ * entry up under `latin1Key(bytes.subarray(0, L))` — the prefix's own bytes
659
+ * ARE the cache key — and a conversation's fold state advances only by
660
+ * append. A new caller that cannot make the same structural argument must
661
+ * pass no `prev` at all; the cold path is always correct.
662
+ * ({@link stablePrefixFoldIncremental} carries the identical precondition for
663
+ * the identical reason.) */
664
+ export function contentFoldIncremental(space, alphabet, bytes, prev) {
665
+ if (bytes.length === 0) {
666
+ return {
667
+ tree: sema(alphabet.vecs[0], new Uint8Array(0), null),
668
+ fold: { edges: [0], segs: [] },
669
+ };
670
+ }
671
+ const { cuts, levels } = contentLevels(space, bytes);
672
+ const edges = [0, ...cuts, bytes.length];
673
+ const segs = [];
674
+ for (let i = 0; i + 1 < edges.length; i++) {
675
+ const hit = prev !== undefined && prev.edges[i] === edges[i] &&
676
+ prev.edges[i + 1] === edges[i + 1]
677
+ ? prev.segs[i]
678
+ : undefined;
679
+ segs.push(hit ?? flatFold(space, alphabet, bytes, edges[i], edges[i + 1]));
680
+ }
681
+ const folded = segs.length > 1
682
+ ? groupByLevel(space, segs, levels, 1)
683
+ : segs[0];
684
+ // THE ROOT IS NORMALIZED IN PLACE, A CACHED SEGMENT NEVER IS. With one
685
+ // segment — or with a grouping that passes a lone item through — `folded`
686
+ // IS a cached seg, and a later turn will reuse it as an interior node whose
687
+ // magnitude must stay byte-proportional. Copy before normalizing, exactly
688
+ // as the stable-prefix twin does. A single LEAF is copied too: its vector
689
+ // is the shared alphabet entry and must never be written.
690
+ const aliased = segs.some((s) => s.tree === folded.tree);
691
+ let tree = folded.tree;
692
+ if (aliased) {
693
+ tree = tree.kids === null
694
+ ? sema(tree.v, tree.leaf, null)
695
+ : sema(Float32Array.from(tree.v), null, tree.kids);
696
+ }
697
+ if (tree.kids !== null)
698
+ normalize(tree.v);
699
+ return { tree, fold: { edges, segs } };
700
+ }
623
701
  /** Group a row of items by the level of the cut BETWEEN them: items separated
624
702
  * by a cut of level < L belong to the same parent, and a cut of level ≥ L ends
625
703
  * it. Recurses upward until one root remains, so the shape at every level is
@@ -820,9 +898,17 @@ function stablePrefixFold(space, alphabet, bytes, boundaries) {
820
898
  * folded over a byte-identical prefix (the caller keys the cache by
821
899
  * content). Requires a non-empty effective boundary set. */
822
900
  export function stablePrefixFoldIncremental(space, alphabet, bytes, boundaries, prev) {
901
+ // SORTED, like {@link bytesToTree} does before calling the non-incremental
902
+ // twin. The filter below is sequential (`b > prevB`), so an out-of-order
903
+ // entry is silently DROPPED rather than rejected — and these two functions
904
+ // are documented as producing the same cuts, so a caller that hands the
905
+ // same set to each and gets different trees has hit a trap, not a contract.
906
+ // Sorting here makes the twins genuinely interchangeable; the set is one
907
+ // entry per conversation turn, so the cost is nil.
908
+ const sorted = [...boundaries].sort((a, b) => a - b);
823
909
  const cuts = [];
824
910
  let prevB = 0;
825
- for (const b of boundaries) {
911
+ for (const b of sorted) {
826
912
  if (b > prevB && b < bytes.length) {
827
913
  cuts.push(b);
828
914
  prevB = b;
@@ -94,7 +94,7 @@ import { conceptThreshold, dominates, significanceBar } from "../geometry.js";
94
94
  import { bytesEqual, indexOf } from "../bytes.js";
95
95
  import { foldTree, perceive, read } from "./primitives.js";
96
96
  import { chainReach, leafIdRun } from "./canonical.js";
97
- import { corpusN, edgeAncestors, hubBound, sharedReachMemo, } from "./traverse.js";
97
+ import { allWindowsAreScaffolding, corpusN, edgeAncestors, hubBound, sharedReachMemo, } from "./traverse.js";
98
98
  import { rItem, rNode } from "./trace.js";
99
99
  import { junctionContainersFrom } from "./junction.js";
100
100
  import { spanHalo } from "./match.js";
@@ -329,6 +329,32 @@ async function bridgeImpl(ctx, query, proposed) {
329
329
  ctx.trace?.step("substitutionBridge", [rItem(query, "query")], [], "no stored query window can anchor a corroborated substitution", undefined, diagnostics);
330
330
  return null;
331
331
  }
332
+ // NO DISCRIMINATING LITERAL EVIDENCE — abstain (§2.13). A bridge grounds
333
+ // through the literal spans it did NOT substitute; those anchors are the
334
+ // whole of its evidence. When every one of them is SATURATED — containment
335
+ // clamped at the √N hub bound, i.e. the window is corpus-global scaffolding
336
+ // — the query's unsubstituted part discriminates nothing, and the single
337
+ // substituted span is carrying the entire semantic load. That is not a
338
+ // corroborated bridge; it is a template match, and it FABRICATES.
339
+ //
340
+ // Measured on the trained store (hubBound 571). "What is the capital of"
341
+ // has 19 anchors, ALL saturated ("What":572, "hat ":572, "at i":572 …), and
342
+ // bridged to an unrelated trained context about an integral, voiced
343
+ // confidently. Every query the bridge answers CORRECTLY has at least one
344
+ // unsaturated anchor, by a wide margin and with no near miss:
345
+ // "Who is the author of Hamlet?" → "let?":12, "How do you say 'thank you'
346
+ // in French?" → "y 't":3, "…largest planet…" → "tem?":31, "What is the
347
+ // capital of France?" → "f Fr":114. The honest-silence probes sit on the
348
+ // same side as the correct ones ("Zamu":3), so this gate is not what makes
349
+ // them silent and cannot be credited for them.
350
+ //
351
+ // This introduces NO new threshold: `bound` is the same √N reading of "hub"
352
+ // the anchor scan already clamps its own containment read to (§2.2, §2.7).
353
+ if (allWindowsAreScaffolding(ctx, query)) {
354
+ ctx.trace?.step("substitutionBridge", [rItem(query, "query")], [], "every query window that could anchor is corpus-global scaffolding — " +
355
+ "no literal evidence to corroborate a substitution", undefined, diagnostics);
356
+ return null;
357
+ }
332
358
  // CORROBORATION (see the module-level doc) over the precomputed window
333
359
  // facts: the query span [qs,qe) attests when every full W-window inside
334
360
  // it is a stored flat form and at least one is reused across ≥ 2
@@ -0,0 +1,15 @@
1
+ import type { MindContext } from "./types.js";
2
+ /** A grounded frame-filler substitution: the stored form the constructed key
3
+ * resolved to, and the spans that explain how it was reached. */
4
+ export interface FrameFillerHit {
5
+ /** The trained form the key resolved to — grounded through its own edge. */
6
+ id: number;
7
+ /** `[start, end)` of the query span the filler stood in for. */
8
+ described: [number, number];
9
+ /** The filler's bytes, for the rationale trace. */
10
+ filler: Uint8Array;
11
+ }
12
+ /** Find the query's own most discriminative unit and the trained contexts that
13
+ * hold it, then try the store for the query with a candidate filler in the
14
+ * described span's place. Returns the sole surviving stored form, or null. */
15
+ export declare function frameFillerSubstitution(ctx: MindContext, query: Uint8Array, ranked: ReadonlyArray<number>): FrameFillerHit | null;