@hviana/sema 0.5.2 → 0.5.3

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 (41) hide show
  1. package/AGENTS.md +114 -52
  2. package/HOW_IT_WORKS.md +275 -184
  3. package/dist/src/mind/bridge.d.ts +5 -7
  4. package/dist/src/mind/bridge.js +6 -97
  5. package/dist/src/mind/match.d.ts +159 -0
  6. package/dist/src/mind/match.js +300 -7
  7. package/dist/src/mind/mechanisms/prefix-completion.d.ts +22 -0
  8. package/dist/src/mind/{prefix-completion.js → mechanisms/prefix-completion.js} +64 -91
  9. package/dist/src/mind/mechanisms/recall.js +10 -108
  10. package/dist/src/mind/mechanisms/reference.d.ts +6 -0
  11. package/dist/src/mind/mechanisms/reference.js +296 -0
  12. package/dist/src/mind/mind.d.ts +1 -1
  13. package/dist/src/mind/pipeline-mechanism.d.ts +56 -1
  14. package/dist/src/mind/pipeline-mechanism.js +104 -3
  15. package/dist/src/mind/pipeline.d.ts +1 -1
  16. package/dist/src/mind/pipeline.js +13 -1
  17. package/dist/src/mind/traverse.d.ts +38 -0
  18. package/dist/src/mind/traverse.js +91 -1
  19. package/dist/src/store.d.ts +4 -4
  20. package/jsr.json +6 -0
  21. package/package.json +1 -1
  22. package/src/mind/bridge.ts +10 -104
  23. package/src/mind/match.ts +416 -7
  24. package/src/mind/{prefix-completion.ts → mechanisms/prefix-completion.ts} +66 -92
  25. package/src/mind/mechanisms/recall.ts +9 -126
  26. package/src/mind/mechanisms/reference.ts +343 -0
  27. package/src/mind/mind.ts +12 -8
  28. package/src/mind/pipeline-mechanism.ts +120 -3
  29. package/src/mind/pipeline.ts +16 -2
  30. package/src/mind/traverse.ts +92 -1
  31. package/src/store.ts +13 -4
  32. package/test/33-multi-candidate.test.mjs +21 -11
  33. package/test/70-prefix-completion.test.mjs +1 -1
  34. package/test/72-prefix-candidate-supply.test.mjs +7 -9
  35. package/test/74-prefix-trap-not-sprung-early.test.mjs +1 -1
  36. package/test/76-reference-binding.test.mjs +471 -0
  37. package/dist/src/mind/frame-filler.d.ts +0 -15
  38. package/dist/src/mind/frame-filler.js +0 -535
  39. package/dist/src/mind/prefix-completion.d.ts +0 -59
  40. package/src/mind/frame-filler.ts +0 -604
  41. package/test/69-frame-filler.test.mjs +0 -115
@@ -1,8 +1,9 @@
1
1
  import type { AncestorReach, MindContext, Recognition } from "./types.js";
2
2
  import type { AttentionRead } from "./types.js";
3
3
  import type { ComputedSpan } from "../extension.js";
4
+ import type { Hit } from "../store.js";
4
5
  import type { Vec } from "../vec.js";
5
- import { type GradedRun } from "./match.js";
6
+ import { type FrameInstance, type GradedRun } from "./match.js";
6
7
  export declare class Precomputed {
7
8
  readonly ctx: MindContext;
8
9
  readonly query: Uint8Array;
@@ -52,6 +53,60 @@ export declare class Precomputed {
52
53
  * which every later consumer then got free. Attribution must follow the
53
54
  * work, not the caller. */
54
55
  private shared;
56
+ private _resonance?;
57
+ /** The response's ONE top-k content-index read: the k learnt forms nearest
58
+ * the whole-query gist, ranked. Recall's every gist tier is built on it,
59
+ * and {@link frames} assembles the frame inventory from it.
60
+ *
61
+ * An ANN query is the single most expensive read in the engine, and two
62
+ * mechanisms asking the same question of the same gist is the one
63
+ * duplication a profile shows as doubled `annVectorReads` with nothing to
64
+ * account for it. Cached BY PROMISE, so a second caller awaits the first. */
65
+ resonance(): Promise<ReadonlyArray<Hit>>;
66
+ private _wide?;
67
+ /** The response's WIDE candidate list — the top-k when the query's gist has
68
+ * no concept-level match anywhere, and an exhaustive √N read when it does.
69
+ *
70
+ * Every mechanism that has to look PAST the top-k reads this one list: the
71
+ * substitution bridge, prefix completion and the frame filler all did, and
72
+ * it was memoised inside recall for exactly that reason (measured: 490 ms
73
+ * median re-issued against 13 ms non-exhaustive, 36x). A memo inside one
74
+ * mechanism only serves that mechanism's own tiers, so it lives here now —
75
+ * the same move `resonance` made for the top-k.
76
+ *
77
+ * THE CONDITION IS THE TOP HIT'S SCORE, NOT THE CORPUS SIZE. When nothing
78
+ * ranks at concept level, an exhaustive ANN only scores more vectors below
79
+ * the bar (profiled at 38K–40K annVectorReads per refusing query on a 325K-
80
+ * context store); the structural channels — junction walks, anchor climbs,
81
+ * the write side's window index — are the correct proposal source there,
82
+ * because the ANN cannot propose what the gist cannot rank. This was once
83
+ * spelled `corpusN(ctx) <= (k · W)³`, which asks a different question and
84
+ * answers it wrongly at exactly the scale it was written from: at N =
85
+ * 325,608 with k = 24 and W = 4 the cube is 884,736, so that store took the
86
+ * exhaustive branch — the very branch measured above. Measured cost of the
87
+ * mismatch: substitutionBridge 8,544 ms of a 19,548 ms think (44%), against
88
+ * 1,248 ms and 14,218 ms without it, every answer byte-identical. */
89
+ wideResonance(): Promise<ReadonlyArray<number>>;
90
+ private _frames?;
91
+ /** THE FRAME INVENTORY — every ranked candidate that reads as an instance of
92
+ * the same frame as the query, each with the query spans it leaves VARIABLE
93
+ * ({@link FrameInstance}). The one place the engine represents "a position
94
+ * whose occupant comes from the context rather than the corpus".
95
+ *
96
+ * AN INVENTORY, NOT AN ELECTION. It reports every pairing and elects no
97
+ * frame, deliberately: a slot is a property of a PAIRING, not of the query,
98
+ * and different candidates put slots in different places. Committing to one
99
+ * reading here would push whichever consumer asked first onto everyone else
100
+ * — the market's decoupling (§2.6) broken from inside the shared container,
101
+ * and the population error §2.7 names. Each consumer groups and commits
102
+ * for its own question; reference elects the modal slot signature, and a
103
+ * consumer wanting a different reading is not fighting this one.
104
+ *
105
+ * NO LICENCE EITHER. Knowing a span is variable is safe for every consumer
106
+ * — it can only improve an alignment. Knowing one may be VOICED through is
107
+ * a different and much stronger claim, gated separately by
108
+ * {@link carriesFillers}, which needs projections this must not perform. */
109
+ frames(): Promise<ReadonlyArray<FrameInstance>>;
55
110
  private _attention?;
56
111
  /** The full consensus climb (roots + ranked anchors) — the query-level
57
112
  * evidence CAST, confluence, extraction, recall's scaffolding tier, and
@@ -13,12 +13,12 @@
13
13
  // 4. TRAVELING EVIDENCE — run() returns MechanismResult with accounted, moves,
14
14
  // and unexplained. The pipeline computes the weight.
15
15
  import { indexOf } from "../bytes.js";
16
- import { dominates } from "../geometry.js";
16
+ import { conceptThreshold, dominates } from "../geometry.js";
17
17
  import { windowIds } from "./canonical.js";
18
18
  import { read, resolve } from "./primitives.js";
19
- import { alignGraded, skillExemplar } from "./match.js";
19
+ import { alignGraded, frameSlots, skillExemplar, } from "./match.js";
20
20
  import { climbAttentionAll } from "./attention.js";
21
- import { sharedReachMemo } from "./traverse.js";
21
+ import { hubBound, sharedReachMemo } from "./traverse.js";
22
22
  // ── Precomputed ──────────────────────────────────────────────────────────────
23
23
  //
24
24
  // Precomputed is a LAZY container for structural analyses of the query — the
@@ -116,6 +116,107 @@ export class Precomputed {
116
116
  const meter = this.ctx.meter;
117
117
  return meter ? meter.time(phase, fn) : fn();
118
118
  }
119
+ _resonance;
120
+ /** The response's ONE top-k content-index read: the k learnt forms nearest
121
+ * the whole-query gist, ranked. Recall's every gist tier is built on it,
122
+ * and {@link frames} assembles the frame inventory from it.
123
+ *
124
+ * An ANN query is the single most expensive read in the engine, and two
125
+ * mechanisms asking the same question of the same gist is the one
126
+ * duplication a profile shows as doubled `annVectorReads` with nothing to
127
+ * account for it. Cached BY PROMISE, so a second caller awaits the first. */
128
+ resonance() {
129
+ return this._resonance ??= this.shared("resonance", () => this.ctx.store.resonate(this.guide, this.k));
130
+ }
131
+ _wide;
132
+ /** The response's WIDE candidate list — the top-k when the query's gist has
133
+ * no concept-level match anywhere, and an exhaustive √N read when it does.
134
+ *
135
+ * Every mechanism that has to look PAST the top-k reads this one list: the
136
+ * substitution bridge, prefix completion and the frame filler all did, and
137
+ * it was memoised inside recall for exactly that reason (measured: 490 ms
138
+ * median re-issued against 13 ms non-exhaustive, 36x). A memo inside one
139
+ * mechanism only serves that mechanism's own tiers, so it lives here now —
140
+ * the same move `resonance` made for the top-k.
141
+ *
142
+ * THE CONDITION IS THE TOP HIT'S SCORE, NOT THE CORPUS SIZE. When nothing
143
+ * ranks at concept level, an exhaustive ANN only scores more vectors below
144
+ * the bar (profiled at 38K–40K annVectorReads per refusing query on a 325K-
145
+ * context store); the structural channels — junction walks, anchor climbs,
146
+ * the write side's window index — are the correct proposal source there,
147
+ * because the ANN cannot propose what the gist cannot rank. This was once
148
+ * spelled `corpusN(ctx) <= (k · W)³`, which asks a different question and
149
+ * answers it wrongly at exactly the scale it was written from: at N =
150
+ * 325,608 with k = 24 and W = 4 the cube is 884,736, so that store took the
151
+ * exhaustive branch — the very branch measured above. Measured cost of the
152
+ * mismatch: substitutionBridge 8,544 ms of a 19,548 ms think (44%), against
153
+ * 1,248 ms and 14,218 ms without it, every answer byte-identical. */
154
+ wideResonance() {
155
+ return this._wide ??= this.shared("wideResonance", async () => {
156
+ const hits = await this.resonance();
157
+ if (hits.length > 0 &&
158
+ hits[0].score >= conceptThreshold(this.ctx.store.D)) {
159
+ const exhaustive = await this.ctx.store.resonate(this.guide, hubBound(this.ctx), true);
160
+ return exhaustive.map((h) => h.id);
161
+ }
162
+ return hits.map((h) => h.id);
163
+ });
164
+ }
165
+ _frames;
166
+ /** THE FRAME INVENTORY — every ranked candidate that reads as an instance of
167
+ * the same frame as the query, each with the query spans it leaves VARIABLE
168
+ * ({@link FrameInstance}). The one place the engine represents "a position
169
+ * whose occupant comes from the context rather than the corpus".
170
+ *
171
+ * AN INVENTORY, NOT AN ELECTION. It reports every pairing and elects no
172
+ * frame, deliberately: a slot is a property of a PAIRING, not of the query,
173
+ * and different candidates put slots in different places. Committing to one
174
+ * reading here would push whichever consumer asked first onto everyone else
175
+ * — the market's decoupling (§2.6) broken from inside the shared container,
176
+ * and the population error §2.7 names. Each consumer groups and commits
177
+ * for its own question; reference elects the modal slot signature, and a
178
+ * consumer wanting a different reading is not fighting this one.
179
+ *
180
+ * NO LICENCE EITHER. Knowing a span is variable is safe for every consumer
181
+ * — it can only improve an alignment. Knowing one may be VOICED through is
182
+ * a different and much stronger claim, gated separately by
183
+ * {@link carriesFillers}, which needs projections this must not perform. */
184
+ frames() {
185
+ return this._frames ??= this.shared("frames", async () => {
186
+ const ctx = this.ctx;
187
+ const W = ctx.space.maxGroup;
188
+ // PHRASE SCALE, the same bound the bridge and the frame filler put on a
189
+ // candidate's bytes: a form an order of magnitude longer than the query
190
+ // is not a candidate for BEING it with a span replaced.
191
+ const capBytes = this.query.length * W;
192
+ const out = [];
193
+ for (const h of await this.resonance()) {
194
+ // REJECT BY LENGTH BEFORE RECONSTRUCTING (§2.8): `contentLen` is an
195
+ // indexed read, `bytesPrefix` rebuilds a subtree. ONLY the phrase-scale
196
+ // cap is applied — it is a bounded-read discipline, not a judgement.
197
+ //
198
+ // A LOWER bound was here too (`dominates(len, query.length)`, on the
199
+ // reasoning that a candidate shorter than half the query cannot supply
200
+ // a frame that dominates it). That is reference's gate wearing a cost
201
+ // argument's clothes, and it hid the very pairings another consumer
202
+ // needs: `What is the capital of France?` (30 B) against `What is the
203
+ // capital of the country where the Eiffel Tower is?` (61 B) was
204
+ // rejected before it was ever read — a definite description standing
205
+ // where a noun stands, which is exactly the shape the frame filler
206
+ // exists for.
207
+ const len = ctx.store.contentLen(h.id, capBytes + 1);
208
+ if (len === 0 || len > capBytes)
209
+ continue;
210
+ const cand = ctx.store.bytesPrefix(h.id, capBytes + 1);
211
+ if (cand.length === 0 || cand.length > capBytes)
212
+ continue;
213
+ const inst = frameSlots(ctx, this.query, cand, h.id);
214
+ if (inst !== null)
215
+ out.push(inst);
216
+ }
217
+ return out;
218
+ });
219
+ }
119
220
  _attention;
120
221
  /** The full consensus climb (roots + ranked anchors) — the query-level
121
222
  * evidence CAST, confluence, extraction, recall's scaffolding tier, and
@@ -3,7 +3,7 @@ import { type PipelineMechanism } from "./pipeline-mechanism.js";
3
3
  export { resolveConcepts, resolveConnectors } from "./mechanisms/cover.js";
4
4
  export { aluToMechanism } from "./mechanisms/alu.js";
5
5
  export declare const defaultMechanisms: PipelineMechanism[];
6
- export type Provenance = "cast" | "join" | "cover" | "extract" | "recall" | "recall-echo";
6
+ export type Provenance = "cast" | "join" | "cover" | "extract" | "reference" | "recall" | "recall-echo" | "prefix";
7
7
  export interface Thought {
8
8
  bytes: Uint8Array;
9
9
  provenance: Provenance;
@@ -20,6 +20,8 @@ import { coverMechanism } from "./mechanisms/cover.js";
20
20
  import { castMechanism } from "./mechanisms/cast.js";
21
21
  import { confluenceMechanism } from "./mechanisms/confluence.js";
22
22
  import { extractionMechanism } from "./mechanisms/extraction.js";
23
+ import { referenceMechanism } from "./mechanisms/reference.js";
24
+ import { prefixMechanism } from "./mechanisms/prefix-completion.js";
23
25
  import { recallMechanism } from "./mechanisms/recall.js";
24
26
  // Re-exports: cover's pre-resolution helpers and the ALU adapter kept
25
27
  // importable from the pipeline module (their historical home).
@@ -49,13 +51,23 @@ async function collectComputed(ctx, mechanisms, query) {
49
51
  // floor pruning every mechanism is already subject to — not by asking
50
52
  // "is this an extension?". Grade TIES keep the earlier candidate, so this
51
53
  // order is also the tie-break priority: cover, cast, confluence, extraction,
52
- // recall.
54
+ // reference, recall.
55
+ //
56
+ // REFERENCE sits after extraction and before recall because that is what its
57
+ // claim is worth: extraction READS a span out of the query (no synthesis),
58
+ // reference voices one through a learnt slot, and recall's tiers degrade
59
+ // toward echo and silence. It does not PRUNE recall — its floor is two
60
+ // projections, so recall's one-STEP floor still clears `worthRunning` — and it
61
+ // is not meant to: both run, share one resonance read
62
+ // (Precomputed.resonance), and the ladder decides.
53
63
  export const defaultMechanisms = [
54
64
  coverMechanism,
55
65
  castMechanism,
56
66
  confluenceMechanism,
57
67
  extractionMechanism,
68
+ referenceMechanism,
58
69
  recallMechanism,
70
+ prefixMechanism,
59
71
  ];
60
72
  /** Think: a single lightest-derivation exploration of the Sema graph.
61
73
  *
@@ -156,3 +156,41 @@ export declare function chooseAmong(ctx: MindContext, candidates: readonly numbe
156
156
  * scaffolding-only — it has no evidence either way, and its callers already
157
157
  * refuse it on their own terms. */
158
158
  export declare function allWindowsAreScaffolding(ctx: MindContext, query: Uint8Array): boolean;
159
+ /** Trained forms the query may OPEN, proposed from the write side's own
160
+ * leaf-id window index — the supply of last resort for prefix completion.
161
+ *
162
+ * WHY A SECOND SUPPLY EXISTS. The ranked list prefix completion normally reads
163
+ * is a resonance list, and resonance cannot rank a proper prefix: measured on
164
+ * the trained store, cos(prefix, form) falls from 0.9629 at a one-byte
165
+ * truncation to 0.6206 at three bytes, against a reachThreshold of 0.8750.
166
+ * Three bytes of truncation put the answer out of reach on GEOMETRY, not on a
167
+ * bug, so no k and no re-ranking recovers it.
168
+ *
169
+ * WHY THIS ROUTE WORKS WHERE THE FOLD DOES NOT. A query's own fold is
170
+ * useless here: content addressing is not phrase-position-invariant, so a
171
+ * standalone prefix folds to a DIFFERENT node than the same bytes sitting
172
+ * inside a longer deposit, and neither the prefix's own node nor its
173
+ * ancestors lead to the deposit (measured: the 22-byte prefix of the
174
+ * photosynthesis form resolves, is shared by 6 contexts, and does not have
175
+ * the form among its ancestors). Leaf ids ARE position-invariant — they are
176
+ * content-addressed on single bytes — and `indexSubSpans` already interns a
177
+ * flat branch over every canonical WINDOW of a deposit's leaf-id stream, with
178
+ * containment edges to the chunks that window spans. A query that is a
179
+ * prefix therefore shares those window nodes exactly, and reaches the deposit
180
+ * by climbing containment then parents. Nothing is added to the write side;
181
+ * this reads an index training already built.
182
+ *
183
+ * BOUNDED (§2.8), AND WITH NO NEW THRESHOLD. The window whose containment is
184
+ * SMALLEST carries the most evidence, and one saturated at `hubBound` carries
185
+ * none — that is the same √N reading of "hub" the rest of the mind uses, not
186
+ * a tuned knob. The upward walk spends a budget of `hubBound` nodes and
187
+ * fans out by W, so a hub query enumerates nothing and the caller stays
188
+ * silent rather than guessing (§2.13). Measured on the trained store: the
189
+ * photosynthesis form at a one-byte truncation picks a window with 52
190
+ * containers, visits 446 nodes, and yields exactly ONE candidate that
191
+ * survives the caller's byte compare — the form itself.
192
+ *
193
+ * These are PROPOSALS only. Every candidate still faces the byte-exact
194
+ * prefix compare and all three guards below, so a wrong proposal costs one
195
+ * bounded read and can never be voiced (§2.3). */
196
+ export declare function formsOpenedBy(ctx: MindContext, query: Uint8Array): number[];
@@ -7,7 +7,7 @@
7
7
  // project) live in match.ts — the elementary match-and-project operation.
8
8
  import { cosine } from "../vec.js";
9
9
  import { gistOf, read } from "./primitives.js";
10
- import { leafIdRun } from "./canonical.js";
10
+ import { canonicalWindows, leafIdPrefix, leafIdRun } from "./canonical.js";
11
11
  //
12
12
  // Budgeted on the same terms as the reach memo below (AGENTS §2.12): these
13
13
  // three maps are cleared on every write, but a long read-only session over a
@@ -714,3 +714,93 @@ export function allWindowsAreScaffolding(ctx, query) {
714
714
  }
715
715
  return sawOne;
716
716
  }
717
+ // ── THE PREFIX SUPPLY ───────────────────────────────────────────────────────
718
+ //
719
+ // A RETRIEVAL capability, not a grounding one: "which trained forms does this
720
+ // byte run OPEN?" It lived inside a recall tier, which is the wrong altitude
721
+ // — it reads the write side's own leaf-id window index and answers a question
722
+ // about the STORE, so any mechanism may ask it.
723
+ /** Trained forms the query may OPEN, proposed from the write side's own
724
+ * leaf-id window index — the supply of last resort for prefix completion.
725
+ *
726
+ * WHY A SECOND SUPPLY EXISTS. The ranked list prefix completion normally reads
727
+ * is a resonance list, and resonance cannot rank a proper prefix: measured on
728
+ * the trained store, cos(prefix, form) falls from 0.9629 at a one-byte
729
+ * truncation to 0.6206 at three bytes, against a reachThreshold of 0.8750.
730
+ * Three bytes of truncation put the answer out of reach on GEOMETRY, not on a
731
+ * bug, so no k and no re-ranking recovers it.
732
+ *
733
+ * WHY THIS ROUTE WORKS WHERE THE FOLD DOES NOT. A query's own fold is
734
+ * useless here: content addressing is not phrase-position-invariant, so a
735
+ * standalone prefix folds to a DIFFERENT node than the same bytes sitting
736
+ * inside a longer deposit, and neither the prefix's own node nor its
737
+ * ancestors lead to the deposit (measured: the 22-byte prefix of the
738
+ * photosynthesis form resolves, is shared by 6 contexts, and does not have
739
+ * the form among its ancestors). Leaf ids ARE position-invariant — they are
740
+ * content-addressed on single bytes — and `indexSubSpans` already interns a
741
+ * flat branch over every canonical WINDOW of a deposit's leaf-id stream, with
742
+ * containment edges to the chunks that window spans. A query that is a
743
+ * prefix therefore shares those window nodes exactly, and reaches the deposit
744
+ * by climbing containment then parents. Nothing is added to the write side;
745
+ * this reads an index training already built.
746
+ *
747
+ * BOUNDED (§2.8), AND WITH NO NEW THRESHOLD. The window whose containment is
748
+ * SMALLEST carries the most evidence, and one saturated at `hubBound` carries
749
+ * none — that is the same √N reading of "hub" the rest of the mind uses, not
750
+ * a tuned knob. The upward walk spends a budget of `hubBound` nodes and
751
+ * fans out by W, so a hub query enumerates nothing and the caller stays
752
+ * silent rather than guessing (§2.13). Measured on the trained store: the
753
+ * photosynthesis form at a one-byte truncation picks a window with 52
754
+ * containers, visits 446 nodes, and yields exactly ONE candidate that
755
+ * survives the caller's byte compare — the form itself.
756
+ *
757
+ * These are PROPOSALS only. Every candidate still faces the byte-exact
758
+ * prefix compare and all three guards below, so a wrong proposal costs one
759
+ * bounded read and can never be voiced (§2.3). */
760
+ export function formsOpenedBy(ctx, query) {
761
+ const store = ctx.store;
762
+ const W = ctx.space.maxGroup;
763
+ const run = leafIdPrefix(ctx, query);
764
+ // The widest canonical window is the most discriminative one the write side
765
+ // ever interned; a query too short to spell one carries no window evidence.
766
+ const len = canonicalWindows(W)[1];
767
+ if (run.length < len)
768
+ return [];
769
+ const bound = hubBound(ctx);
770
+ let best = null;
771
+ let bestN = 0;
772
+ for (let off = 0; off + len <= run.length; off++) {
773
+ const wid = store.findBranch(run.slice(off, off + len));
774
+ if (wid === null)
775
+ continue;
776
+ const n = store.containersSlice(wid, 0, bound).length;
777
+ // Empty says the window spans no chunk; saturated says it is a hub, whose
778
+ // containment discriminates nothing. Neither is evidence.
779
+ if (n === 0 || n >= bound)
780
+ continue;
781
+ if (best === null || n < bestN) {
782
+ best = wid;
783
+ bestN = n;
784
+ }
785
+ }
786
+ if (best === null)
787
+ return [];
788
+ let frontier = store.containersSlice(best, 0, bound);
789
+ const seen = new Set(frontier);
790
+ let budget = bound;
791
+ while (frontier.length > 0 && budget > 0) {
792
+ const next = [];
793
+ for (const f of frontier) {
794
+ if (budget-- <= 0)
795
+ break;
796
+ for (const p of store.parentsFirst(f, W)) {
797
+ if (seen.has(p))
798
+ continue;
799
+ seen.add(p);
800
+ next.push(p);
801
+ }
802
+ }
803
+ frontier = next;
804
+ }
805
+ return [...seen];
806
+ }
@@ -533,15 +533,15 @@ export declare abstract class AbstractStore implements Store {
533
533
  vector: Float32Array;
534
534
  }>;
535
535
  /** Halo index write buffer — keyed by id so repeats within a batch coalesce. */
536
- protected _haloBuffer: Map<number, Float32Array<ArrayBufferLike>>;
536
+ protected _haloBuffer: Map<NodeId, Float32Array>;
537
537
  /** Containment write buffer: child → new parents, merged on flush cadence. */
538
- protected _containBuf: Map<number, Set<number>>;
538
+ protected _containBuf: Map<NodeId, Set<NodeId>>;
539
539
  /** Dedup-target candidates still in the write buffer (keyed by id). Only
540
540
  * roots that have gained an edge/halo are targets; a fresh intermediate
541
541
  * branch is never folded onto. */
542
- protected _nearDedupBuf: Map<number, Float32Array<ArrayBufferLike>>;
542
+ protected _nearDedupBuf: Map<NodeId, Float32Array>;
543
543
  /** Ids currently in `_contentBuffer` (not yet flushed) — O(1) membership. */
544
- protected _bufferedIds: Set<number>;
544
+ protected _bufferedIds: Set<NodeId>;
545
545
  /** {@link Store.chainRun} results, valid for the store's lifetime BETWEEN
546
546
  * writes: a chain is a pure function of the kid and edge tables, so any
547
547
  * write that could break a node's transparency (a fresh mint inserting kid
package/jsr.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://jsr.io/schema/config-file.v1.json",
3
+ "name": "@hviana/sema",
4
+ "version": "0.5.3",
5
+ "exports": "./src/index.ts"
6
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hviana/sema",
3
- "version": "0.5.2",
3
+ "version": "0.5.3",
4
4
  "description": "Sema: a non-parametric, instance-based reasoning system.",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",
@@ -105,16 +105,13 @@ import {
105
105
  } from "./traverse.js";
106
106
  import { rItem, rNode } from "./trace.js";
107
107
  import { junctionContainersFrom } from "./junction.js";
108
- import { spanHalo } from "./match.js";
108
+ import { alignAround, type AlignGap, spanHalo } from "./match.js";
109
109
 
110
110
  /** One accepted substitution: query span [qs,qe) stands in for the
111
- * candidate context's span — recorded for the rationale trace. */
112
- interface Substitution {
113
- qs: number;
114
- qe: number;
115
- cs: number;
116
- ce: number;
117
- }
111
+ * candidate context's span — recorded for the rationale trace. The same
112
+ * shape the shared aligner reports a disagreement as ({@link AlignGap}); an
113
+ * accepted substitution is a gap that cleared this file's gates. */
114
+ type Substitution = AlignGap;
118
115
 
119
116
  /** A bridged grounding proposal: the trained context to ground, the query
120
117
  * spans its alignment accounts for, and the substitutions that closed it. */
@@ -151,102 +148,11 @@ export function dismissedKnownContent(
151
148
  return false;
152
149
  }
153
150
 
154
- /** Extend a seed match (query offset qo candidate offset co) to its
155
- * maximal common run, then walk outward in both directions collecting
156
- * further common runs of at least W bytes across bounded mismatch gaps
157
- * (each side chainReach). Returns the matched query spans and the
158
- * mismatch pairs between consecutive runs. */
159
- function align(
160
- ctx: MindContext,
161
- q: Uint8Array,
162
- c: Uint8Array,
163
- qo: number,
164
- co: number,
165
- ): { matched: Array<[number, number]>; gaps: Substitution[] } {
166
- const W = ctx.space.maxGroup;
167
- const reachCap = chainReach(W);
168
- // Maximal run around the seed.
169
- let qs = qo, ss = co;
170
- while (qs > 0 && ss > 0 && q[qs - 1] === c[ss - 1]) {
171
- qs--;
172
- ss--;
173
- }
174
- let qe = qo, se = co;
175
- while (qe < q.length && se < c.length && q[qe] === c[se]) {
176
- qe++;
177
- se++;
178
- }
179
- const matched: Array<[number, number]> = [[qs, qe]];
180
- const gaps: Substitution[] = [];
181
- // The next common run of ≥ W bytes past (qi, si), with each side's gap
182
- // bounded by chainReach; smallest total gap wins (nearest continuation).
183
- const runLenAt = (qi: number, si: number): number => {
184
- let n = 0;
185
- while (qi + n < q.length && si + n < c.length && q[qi + n] === c[si + n]) {
186
- n++;
187
- }
188
- return n;
189
- };
190
- // RIGHT sweep.
191
- let qi = qe, si = se;
192
- for (;;) {
193
- let found = false;
194
- for (let total = 1; total <= 2 * reachCap && !found; total++) {
195
- for (let gq = 0; gq <= Math.min(total, reachCap); gq++) {
196
- const gs = total - gq;
197
- if (gs > reachCap) continue;
198
- if (qi + gq >= q.length || si + gs >= c.length) continue;
199
- const n = runLenAt(qi + gq, si + gs);
200
- if (n >= W || qi + gq + n === q.length) {
201
- if (n === 0) continue;
202
- if (gq > 0 || gs > 0) {
203
- gaps.push({ qs: qi, qe: qi + gq, cs: si, ce: si + gs });
204
- }
205
- matched.push([qi + gq, qi + gq + n]);
206
- qi = qi + gq + n;
207
- si = si + gs + n;
208
- found = true;
209
- break;
210
- }
211
- }
212
- }
213
- if (!found) break;
214
- }
215
- // LEFT sweep (mirror).
216
- qi = qs;
217
- si = ss;
218
- for (;;) {
219
- let found = false;
220
- for (let total = 1; total <= 2 * reachCap && !found; total++) {
221
- for (let gq = 0; gq <= Math.min(total, reachCap); gq++) {
222
- const gs = total - gq;
223
- if (gs > reachCap) continue;
224
- if (qi - gq <= 0 || si - gs <= 0) continue;
225
- // Run ENDING at (qi - gq, si - gs).
226
- let n = 0;
227
- while (
228
- n < qi - gq && n < si - gs &&
229
- q[qi - gq - 1 - n] === c[si - gs - 1 - n]
230
- ) {
231
- n++;
232
- }
233
- if (n >= W || n === qi - gq) {
234
- if (n === 0) continue;
235
- if (gq > 0 || gs > 0) {
236
- gaps.push({ qs: qi - gq, qe: qi, cs: si - gs, ce: si });
237
- }
238
- matched.push([qi - gq - n, qi - gq]);
239
- qi = qi - gq - n;
240
- si = si - gs - n;
241
- found = true;
242
- break;
243
- }
244
- }
245
- }
246
- if (!found) break;
247
- }
248
- return { matched, gaps };
249
- }
151
+ // The seeded aligner this file used to own now lives in the shared match
152
+ // family as {@link alignAround} the frame reading (match.ts) reads the same
153
+ // gaps and asks the OPPOSITE question of them (see AlignGap's own doc). Two
154
+ // consumers, one definition (AGENTS §2.5); the bridge's reading is unchanged.
155
+ const align = alignAround;
250
156
 
251
157
  /** Recall's corroborated-substitution bridge — see the module comment.
252
158
  * Returns the best bridged grounding proposal, or null. */