@hviana/sema 0.1.7 → 0.1.8

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 (42) hide show
  1. package/dist/example/train_base.js +24 -4
  2. package/dist/src/alu/src/parser.d.ts +9 -0
  3. package/dist/src/alu/src/parser.js +110 -2
  4. package/dist/src/canon.d.ts +26 -0
  5. package/dist/src/canon.js +57 -0
  6. package/dist/src/geometry.js +12 -0
  7. package/dist/src/index.d.ts +1 -0
  8. package/dist/src/index.js +1 -0
  9. package/dist/src/mind/learning.js +33 -1
  10. package/dist/src/mind/match.d.ts +4 -2
  11. package/dist/src/mind/match.js +30 -6
  12. package/dist/src/mind/mechanisms/cast.js +18 -4
  13. package/dist/src/mind/mechanisms/confluence.js +13 -1
  14. package/dist/src/mind/mechanisms/recall.js +97 -28
  15. package/dist/src/mind/mind.d.ts +64 -2
  16. package/dist/src/mind/mind.js +152 -23
  17. package/dist/src/mind/primitives.d.ts +9 -0
  18. package/dist/src/mind/primitives.js +68 -1
  19. package/dist/src/mind/recognition.js +11 -1
  20. package/dist/src/mind/types.d.ts +10 -0
  21. package/dist/src/store-sqlite.d.ts +8 -0
  22. package/dist/src/store-sqlite.js +52 -0
  23. package/dist/src/store.d.ts +12 -0
  24. package/example/train_base.ts +29 -4
  25. package/package.json +1 -1
  26. package/src/alu/src/parser.ts +105 -4
  27. package/src/canon.ts +65 -0
  28. package/src/geometry.ts +12 -0
  29. package/src/index.ts +1 -0
  30. package/src/mind/learning.ts +39 -1
  31. package/src/mind/match.ts +29 -6
  32. package/src/mind/mechanisms/cast.ts +21 -6
  33. package/src/mind/mechanisms/confluence.ts +15 -1
  34. package/src/mind/mechanisms/recall.ts +116 -41
  35. package/src/mind/mind.ts +172 -29
  36. package/src/mind/primitives.ts +66 -1
  37. package/src/mind/recognition.ts +10 -0
  38. package/src/mind/types.ts +10 -0
  39. package/src/store-sqlite.ts +68 -0
  40. package/src/store.ts +27 -0
  41. package/test/13-conversation.test.mjs +77 -27
  42. package/test/35-prefix-edge.test.mjs +86 -0
@@ -152,8 +152,11 @@ const LOCAL_PATH = env("LOCAL_PATH", ""); // train from a local dir of *.zip
152
152
  const CACHE_DIR = env("CACHE_DIR", join(process.cwd(), "cache"));
153
153
  const MAX_CACHE_BYTES = Number(env("MAX_CACHE_GB", "100")) * 1e9;
154
154
  const PROGRESS_MS = Number(env("PROGRESS_MS", "250")); // panel refresh cadence
155
- // Index maintenance at checkpoints: compact (remove garbage) then repair (fill
156
- // gaps). Both are idempotent batch operations; INDEX_MAINTENANCE=0 disables.
155
+ // Index maintenance at checkpoints: compact (remove garbage), repair (fill
156
+ // gaps), then refresh the canonical-form index (equivalence-class resolution —
157
+ // src/canon.ts). All three are idempotent batch operations (the canon build is
158
+ // additionally incremental via the store's `canon.upto` cursor);
159
+ // INDEX_MAINTENANCE=0 disables.
157
160
  const INDEX_MAINTENANCE = env("INDEX_MAINTENANCE", "1") !== "0";
158
161
  const DOWNLOAD_TRIES = 5;
159
162
  // In-progress downloads are written to a sibling "<dest>.part" and atomically
@@ -1485,8 +1488,9 @@ async function main() {
1485
1488
  // final exit for tidiness. Same lesson as waitMs above (deliberately un-unref'd).
1486
1489
  const keepAlive = setInterval(() => { }, 1 << 30);
1487
1490
  const checkpoint = () => mind.save();
1488
- /** Run index maintenance: compact (remove garbage) then repair (fill gaps).
1489
- * Both are idempotent running twice produces the same result as once.
1491
+ /** Run index maintenance: compact (remove garbage), repair (fill gaps),
1492
+ * then refresh the canonical-form index (see below). All three are
1493
+ * idempotent — running twice produces the same result as once.
1490
1494
  * Compaction frees index space first; repair then adds back every
1491
1495
  * edge/halo-bearing node whose gist was evicted from the pending cache
1492
1496
  * before it reached the content index, completing the coverage that
@@ -1528,6 +1532,22 @@ async function main() {
1528
1532
  catch (err) {
1529
1533
  progress.log(` ${YEL}⚠ index repair failed${R}: ${err instanceof Error ? err.message : String(err)}`);
1530
1534
  }
1535
+ // Canonical-form index (src/canon.ts): lets resolution find stored forms
1536
+ // across surface variation (case, width, whitespace). Incremental and
1537
+ // idempotent by construction — the `canon.upto` meta cursor scans only
1538
+ // nodes newer than the last pass, and the (h, id) primary key ignores
1539
+ // re-inserted rows — so it composes with the resume model exactly like
1540
+ // compact/repair: every checkpoint (and finish) leaves the index
1541
+ // covering all content trained so far.
1542
+ try {
1543
+ const added = await mind.buildCanonIndex();
1544
+ if (added > 0) {
1545
+ progress.log(` ${GRN}canon index: added ${int(added)} canonical-form entries${R}`);
1546
+ }
1547
+ }
1548
+ catch (err) {
1549
+ progress.log(` ${YEL}⚠ canon index build failed${R}: ${err instanceof Error ? err.message : String(err)}`);
1550
+ }
1531
1551
  };
1532
1552
  // The checkpoint recall is a best-effort diagnostic. It is time-bounded so a
1533
1553
  // slow/large store can never freeze the deposit loop, and guarded so a still
@@ -144,6 +144,15 @@ export declare class QueryParser {
144
144
  * same judgement the perception tree makes about spacing — so no character
145
145
  * is privileged as "the" separator. */
146
146
  private arithmeticRuns;
147
+ /** The maximal BRACKETED arithmetic runs: like {@link arithmeticRuns} but
148
+ * grouping brackets participate — an all-'(' term opens depth where an
149
+ * operand is expected, a term beginning with ')' closes it where an
150
+ * operator could follow (only its bracket prefix joins the run; anything
151
+ * after — a trailing "?" — ends it). A run is recorded only when it is
152
+ * BALANCED, actually crossed a bracket, and contains an operator — the
153
+ * bracket-free case is {@link arithmeticRuns}' own. Evaluation is the
154
+ * same registry-derived grammar, whose lexer already reads the brackets. */
155
+ private bracketedRuns;
147
156
  /** Evaluate an infix-arithmetic run to its canonical result bytes, through
148
157
  * the kernel's recursive expression evaluator, or null if it does not
149
158
  * evaluate. A whole result stays an exact int; otherwise the canonical
@@ -113,8 +113,26 @@ export class QueryParser {
113
113
  j,
114
114
  bytes: this.evalRun(query.subarray(i, j)),
115
115
  }));
116
- const spans = runs.filter((r) => r.bytes !== null);
117
- let stream = compose(tokens, runs);
116
+ // BRACKETED runs — "(3 + 4) * (10 - 2)" extend across grouping
117
+ // notation the flat alternation cannot cross. The expression grammar
118
+ // (expr.ts) already nests and parenthesizes; this only hands it the
119
+ // WHOLE bracketed span instead of the fragments between brackets. A
120
+ // bracketed run that evaluates ABSORBS the flat runs inside it (the
121
+ // authority law: contained spans are its material); one that does not
122
+ // evaluate leaves the flat readings untouched.
123
+ const bracketed = this.bracketedRuns(query, tokens)
124
+ .map(([i, j]) => ({ i, j, bytes: this.evalRun(query.subarray(i, j)) }))
125
+ .filter((r) => r.bytes !== null);
126
+ const absorbed = (r) => bracketed.some((b) => b.i <= r.i && r.j <= b.j);
127
+ const composedRuns = [
128
+ ...runs.filter((r) => !absorbed(r)),
129
+ ...bracketed,
130
+ ].sort((a, b) => a.i - b.i);
131
+ const spans = [
132
+ ...runs.filter((r) => r.bytes !== null),
133
+ ...bracketed,
134
+ ];
135
+ let stream = compose(tokens, composedRuns);
118
136
  const readings = new Map();
119
137
  for (;;) {
120
138
  const fired = await this.operations(query, stream, readings);
@@ -231,6 +249,96 @@ export class QueryParser {
231
249
  }
232
250
  return runs;
233
251
  }
252
+ /** The maximal BRACKETED arithmetic runs: like {@link arithmeticRuns} but
253
+ * grouping brackets participate — an all-'(' term opens depth where an
254
+ * operand is expected, a term beginning with ')' closes it where an
255
+ * operator could follow (only its bracket prefix joins the run; anything
256
+ * after — a trailing "?" — ends it). A run is recorded only when it is
257
+ * BALANCED, actually crossed a bracket, and contains an operator — the
258
+ * bracket-free case is {@link arithmeticRuns}' own. Evaluation is the
259
+ * same registry-derived grammar, whose lexer already reads the brackets. */
260
+ bracketedRuns(query, tokens) {
261
+ const OPEN = 0x28, CLOSE = 0x29;
262
+ const bracketPrefix = (t, b) => {
263
+ if (t.kind !== "term")
264
+ return 0;
265
+ let n = 0;
266
+ while (t.i + n < t.j && query[t.i + n] === b)
267
+ n++;
268
+ return n;
269
+ };
270
+ const allOf = (t, b) => bracketPrefix(t, b) === t.j - t.i;
271
+ const bridged = (from, to) => from >= to || this.host.segment(query.subarray(from, to)).length <= 1;
272
+ const runs = [];
273
+ let k = 0;
274
+ while (k < tokens.length) {
275
+ const first = tokens[k];
276
+ if (first.kind !== "operand" && !allOf(first, OPEN)) {
277
+ k++;
278
+ continue;
279
+ }
280
+ let depth = 0;
281
+ let ops = 0;
282
+ let brackets = 0;
283
+ let expectOperand = true;
284
+ let end = -1; // byte end of the last VALID run state (balanced, after operand/close)
285
+ let endTok = k; // token index just past the accepted prefix
286
+ let m = k;
287
+ let prevJ = -1;
288
+ while (m < tokens.length) {
289
+ const t = tokens[m];
290
+ if (prevJ >= 0 && !bridged(prevJ, t.i))
291
+ break;
292
+ if (expectOperand) {
293
+ if (allOf(t, OPEN)) {
294
+ depth += t.j - t.i;
295
+ brackets++;
296
+ }
297
+ else if (t.kind === "operand") {
298
+ expectOperand = false;
299
+ if (depth === 0) {
300
+ end = t.j;
301
+ endTok = m + 1;
302
+ }
303
+ }
304
+ else
305
+ break;
306
+ }
307
+ else {
308
+ const c = bracketPrefix(t, CLOSE);
309
+ if (c > 0 && depth > 0) {
310
+ const take = Math.min(c, depth);
311
+ depth -= take;
312
+ brackets++;
313
+ if (depth === 0) {
314
+ end = t.i + take;
315
+ endTok = m + 1;
316
+ }
317
+ // A term with content beyond its usable bracket prefix ("?")
318
+ // ends the run inside the term.
319
+ if (take < t.j - t.i)
320
+ break;
321
+ }
322
+ else if (t.kind === "operator") {
323
+ expectOperand = true;
324
+ ops++;
325
+ }
326
+ else
327
+ break;
328
+ }
329
+ prevJ = t.j;
330
+ m++;
331
+ }
332
+ if (end >= 0 && ops > 0 && brackets > 0) {
333
+ runs.push([first.i, end]);
334
+ k = endTok;
335
+ }
336
+ else {
337
+ k++;
338
+ }
339
+ }
340
+ return runs;
341
+ }
234
342
  /** Evaluate an infix-arithmetic run to its canonical result bytes, through
235
343
  * the kernel's recursive expression evaluator, or null if it does not
236
344
  * evaluate. A whole result stays an exact int; otherwise the canonical
@@ -0,0 +1,26 @@
1
+ /** A content canonicalizer: maps a byte span to the canonical representative
2
+ * of its equivalence class. Must be pure and deterministic. Returning the
3
+ * input unchanged is always sound (the class is then {input}). */
4
+ export type Canon = (bytes: Uint8Array) => Uint8Array;
5
+ /** The TEXT canonicalizer: Unicode-aware equivalence over every character
6
+ * variation that does not change what the text SAYS —
7
+ *
8
+ * • compatibility normalization (NFKC): full-width forms, ligatures,
9
+ * composed vs decomposed accents collapse to one representation;
10
+ * • case folding (locale-independent lowercase after NFKC — the standard
11
+ * simple fold);
12
+ * • whitespace: every INTERIOR run of Unicode whitespace becomes one plain
13
+ * space. EDGE whitespace is preserved verbatim: a span's leading or
14
+ * trailing separator belongs BETWEEN forms, not to the form — trimming
15
+ * it would let a recognised span swallow the boundary byte that
16
+ * separates it from its neighbour (observed: "ice fire" composing to
17
+ * "coldhot" because the span "ice " matched the stored "ice").
18
+ *
19
+ * "WHAT IS", "What is" and "what is" share one canonical form. This is
20
+ * deliberately conservative: punctuation, digits and word order are content
21
+ * and pass through untouched. */
22
+ export declare function textCanon(bytes: Uint8Array): Uint8Array;
23
+ /** 32-bit FNV-1a over a canonical key — the integer the store's canon index
24
+ * is keyed on. Same construction as the node table's content hash; a
25
+ * collision is resolved by verifying canon(stored) === key, never trusted. */
26
+ export declare function canonHash(key: Uint8Array): number;
@@ -0,0 +1,57 @@
1
+ // canon.ts — content canonicalization for equivalence-class resolution.
2
+ //
3
+ // The store is content-addressed on RAW bytes: "What", "WHAT" and "what" are
4
+ // three different hashes, so a query whose surface form varies from the
5
+ // trained form resolves to nothing even though the CONTENT is the same. A
6
+ // CANONICALIZER maps every surface variant of the same content onto one
7
+ // canonical byte string; the store keeps a small hash index from canonical
8
+ // keys to node ids (see Store.canonAdd/canonFind), and resolution falls back
9
+ // to that index when the exact content-addressed lookup misses.
10
+ //
11
+ // The canonicalizer is MODALITY-SPECIFIC and always INJECTED — nothing in the
12
+ // store or the mind's core knows what "case" or "whitespace" is. The text
13
+ // canonicalizer below is the one `respondText`/`respondTurnText` pass down;
14
+ // a grid or audio modality would supply its own (or none).
15
+ //
16
+ // Canonical keys are equivalence-class LABELS, never content: they are hashed
17
+ // and verified (canon(stored bytes) must equal canon(query bytes) before an
18
+ // id is accepted), so a hash collision costs a read, never a wrong id — the
19
+ // same discipline as the node table's own `h` index.
20
+ const dec = new TextDecoder("utf-8", { fatal: false });
21
+ const enc = new TextEncoder();
22
+ /** The TEXT canonicalizer: Unicode-aware equivalence over every character
23
+ * variation that does not change what the text SAYS —
24
+ *
25
+ * • compatibility normalization (NFKC): full-width forms, ligatures,
26
+ * composed vs decomposed accents collapse to one representation;
27
+ * • case folding (locale-independent lowercase after NFKC — the standard
28
+ * simple fold);
29
+ * • whitespace: every INTERIOR run of Unicode whitespace becomes one plain
30
+ * space. EDGE whitespace is preserved verbatim: a span's leading or
31
+ * trailing separator belongs BETWEEN forms, not to the form — trimming
32
+ * it would let a recognised span swallow the boundary byte that
33
+ * separates it from its neighbour (observed: "ice fire" composing to
34
+ * "coldhot" because the span "ice " matched the stored "ice").
35
+ *
36
+ * "WHAT IS", "What is" and "what is" share one canonical form. This is
37
+ * deliberately conservative: punctuation, digits and word order are content
38
+ * and pass through untouched. */
39
+ export function textCanon(bytes) {
40
+ const s = dec
41
+ .decode(bytes)
42
+ .normalize("NFKC")
43
+ .toLowerCase()
44
+ .replace(/(\S)\s+(?=\S)/g, "$1 ");
45
+ return enc.encode(s);
46
+ }
47
+ /** 32-bit FNV-1a over a canonical key — the integer the store's canon index
48
+ * is keyed on. Same construction as the node table's content hash; a
49
+ * collision is resolved by verifying canon(stored) === key, never trusted. */
50
+ export function canonHash(key) {
51
+ let h = 0x811c9dc5 >>> 0;
52
+ for (let i = 0; i < key.length; i++) {
53
+ h ^= key[i];
54
+ h = Math.imul(h, 0x01000193) >>> 0;
55
+ }
56
+ return h >>> 0;
57
+ }
@@ -376,6 +376,18 @@ export function bytesToTreePyramid(space, alphabet, bytes, prev) {
376
376
  pyramid: { levels: [], bytes: 0 },
377
377
  };
378
378
  }
379
+ // ZERO GROWTH: the previous pyramid already covers every byte — its top
380
+ // level holds the finished, normalized root. Refolding here would not
381
+ // only waste the work: when the whole span is one full block, the refold's
382
+ // top item is a REUSED raw interior of `prev`, and the final normalize
383
+ // below would mutate that shared raw block in place, corrupting every
384
+ // later incremental fold built on `prev`.
385
+ if (prev && prev.bytes === bytes.length && prev.levels.length > 0) {
386
+ return {
387
+ tree: prev.levels[prev.levels.length - 1][0].tree,
388
+ pyramid: prev,
389
+ };
390
+ }
379
391
  const mg = space.maxGroup;
380
392
  const reusable = (L) => {
381
393
  // prev's TOPMOST level holds its normalized ROOT — reusable blocks must
@@ -9,6 +9,7 @@ export * from "./mind/index.js";
9
9
  export * from "./store-sqlite.js";
10
10
  export * from "./config.js";
11
11
  export * from "./extension.js";
12
+ export * from "./canon.js";
12
13
  export * from "./ingest-cache.js";
13
14
  export { IvfIndex, Prng, RaBitQuantizer, VectorDatabase, } from "./rabitq-ivf/src/index.js";
14
15
  export type { DatabaseOptions, ExternalId, IvfConfig, IvfHit, QueryContext, QueryResult, RaBitQOptions, StorageStats, } from "./rabitq-ivf/src/index.js";
package/dist/src/index.js CHANGED
@@ -11,6 +11,7 @@ export * from "./mind/index.js";
11
11
  export * from "./store-sqlite.js";
12
12
  export * from "./config.js";
13
13
  export * from "./extension.js";
14
+ export * from "./canon.js";
14
15
  export * from "./ingest-cache.js";
15
16
  // rabitq-ivf: the partitioned (IVF) vector index over 1-bit RaBitQ codes.
16
17
  export { IvfIndex, Prng, RaBitQuantizer, VectorDatabase, } from "./rabitq-ivf/src/index.js";
@@ -4,7 +4,7 @@
4
4
  // node. A fact is an EDGE between node ids; recall traverses edges.
5
5
  import { bindSeat, companySignature, isChunk } from "../sema.js";
6
6
  import { changedNodes } from "./types.js";
7
- import { inputBytes, perceiveDeposit } from "./primitives.js";
7
+ import { inputBytes, perceiveDeposit, resolve, } from "./primitives.js";
8
8
  import { canonicalWindows, leafIdPrefix } from "./canonical.js";
9
9
  import { fold as foldVecs } from "../sema.js";
10
10
  /** Intern a perceived tree into node ids, bottom-up, sharing equal subtrees.
@@ -152,12 +152,44 @@ export async function ingestOne(ctx, input) {
152
152
  }
153
153
  return Object.assign(tree, { id: rootId });
154
154
  }
155
+ /** For each right-edge suffix of the context bytes, resolve it against the
156
+ * store. A suffix whose resolved node is already a known form inherits the
157
+ * continuation edge. Gate: ≥ 2 structural parents (reused across deposits),
158
+ * or (halo > 0 ∧ already an edge source). Pure answers do not qualify. */
159
+ async function propagateSuffixes(ctx, src, dst) {
160
+ const W = ctx.space.maxGroup;
161
+ const bytes = ctx.store.bytes(src);
162
+ const n = bytes.length;
163
+ if (n < 2 * W)
164
+ return;
165
+ // Existence prefilter — the write side of the canonical contract: every
166
+ // deposit interns its WHOLE byte stream as a flat branch of per-byte leaf
167
+ // ids (deposit(), canonical.ts). A suffix is a stored form exactly when
168
+ // that flat twin exists, so one content-hash probe per offset decides;
169
+ // only a hit pays for resolve()'s deposit-shaped perception. This keeps
170
+ // the scan free of river folds — O(1) probes over cheap byte hashes
171
+ // instead of O(suffix) vector folds per offset.
172
+ const leafIds = leafIdPrefix(ctx, bytes);
173
+ for (let i = 1; i <= n - W; i++) {
174
+ if (ctx.store.findBranch(leafIds.slice(i)) === null)
175
+ continue;
176
+ const id = resolve(ctx, bytes.subarray(i));
177
+ if (id === null || id === src)
178
+ continue;
179
+ const known = ctx.store.parentsFirst(id, 2).length >= 2 ||
180
+ (ctx.store.haloMass(id) > 0 && ctx.store.hasNext(id));
181
+ if (!known)
182
+ continue;
183
+ await ctx.store.link(id, dst);
184
+ }
185
+ }
155
186
  /** Ingest a pair (context, continuation) — learn an edge and pour halos. */
156
187
  export async function ingestPair(ctx, ctxInput, cont) {
157
188
  const c = await deposit(ctx, ctxInput, true);
158
189
  const cont_ = await deposit(ctx, cont, false);
159
190
  const ctxId = c.rootId, contId = cont_.rootId;
160
191
  await ctx.store.link(ctxId, contId);
192
+ await propagateSuffixes(ctx, ctxId, contId);
161
193
  // Halos pour company SIGNATURES (identity), not gists (content) — see
162
194
  // companySignature in sema.ts.
163
195
  const contSeat = bindSeat(ctx.space, companySignature(ctx.space, contId), 1);
@@ -94,8 +94,10 @@ export declare function follow(ctx: MindContext, id: number, guide?: Vec | null)
94
94
  * `guide` the context whose gist resonates with the query wins (seat
95
95
  * symmetry) — without one, the most-corroborated context wins (poured halo
96
96
  * MASS, the direct measure of how many episodes established it), falling
97
- * back to first-learnt on equal mass. Callers that HAVE a query gist must
98
- * pass it, or they silently change disambiguation regime.
97
+ * back to first-learnt on equal mass. Among many predecessors RECIPROCAL
98
+ * ones (mutual edges) are preferred when any exist (RC5). Callers that
99
+ * HAVE a query gist must pass it, or they silently change disambiguation
100
+ * regime.
99
101
  *
100
102
  * `rev`, when the caller has already materialised prevOf (one read per
101
103
  * relation — a hub's reverse fan-in is corpus-sized), is reused instead of
@@ -370,8 +370,10 @@ export async function follow(ctx, id, guide) {
370
370
  * `guide` the context whose gist resonates with the query wins (seat
371
371
  * symmetry) — without one, the most-corroborated context wins (poured halo
372
372
  * MASS, the direct measure of how many episodes established it), falling
373
- * back to first-learnt on equal mass. Callers that HAVE a query gist must
374
- * pass it, or they silently change disambiguation regime.
373
+ * back to first-learnt on equal mass. Among many predecessors RECIPROCAL
374
+ * ones (mutual edges) are preferred when any exist (RC5). Callers that
375
+ * HAVE a query gist must pass it, or they silently change disambiguation
376
+ * regime.
375
377
  *
376
378
  * `rev`, when the caller has already materialised prevOf (one read per
377
379
  * relation — a hub's reverse fan-in is corpus-sized), is reused instead of
@@ -386,11 +388,33 @@ export function reverseContext(ctx, id, guide, rev) {
386
388
  const candidates = rev ?? ctx.store.prevFirst(id, hubBound(ctx));
387
389
  if (candidates.length === 0)
388
390
  return null;
389
- const pick = candidates.length === 1
390
- ? candidates[0]
391
+ // RECIPROCAL PREFERENCE: among many predecessors, one that `id` also
392
+ // continues TO (cand → id AND id → cand both learnt) is a mutually
393
+ // established pairing — the strongest structural evidence a predecessor
394
+ // can carry (bidirectional training deposits both directions of a genuine
395
+ // pair). A bare predecessor is one episode's adjacency; guide-resonance
396
+ // over bare predecessors favours whichever stored document merely
397
+ // CONTAINS the query's bytes (the linear fold's cosine is byte overlap —
398
+ // the observed "merci → unrelated French document" failure). One capped
399
+ // forward read decides; when no reciprocal exists, behaviour is unchanged
400
+ // — bare predecessors ARE the honest answer for a shared deposited
401
+ // continuation (two questions → one answer; audited by 31-audit C1), and
402
+ // this arm serves every mechanism's reverse projection, so abstaining
403
+ // here starves far more than the one containment failure it would fix.
404
+ let pool = candidates;
405
+ if (candidates.length > 1) {
406
+ const fwd = new Set(ctx.store.nextFirst(id, hubBound(ctx)));
407
+ if (fwd.size > 0) {
408
+ const mutual = candidates.filter((c) => fwd.has(c));
409
+ if (mutual.length > 0)
410
+ pool = mutual;
411
+ }
412
+ }
413
+ const pick = pool.length === 1
414
+ ? pool[0]
391
415
  : guide
392
- ? chooseAmong(ctx, candidates, guide).id
393
- : pickByMass(ctx, candidates);
416
+ ? chooseAmong(ctx, pool, guide).id
417
+ : pickByMass(ctx, pool);
394
418
  const g = read(ctx, pick);
395
419
  return g.length > 0 ? g : null;
396
420
  }
@@ -262,7 +262,21 @@ export async function counterfactualTransfer(ctx, query, pre) {
262
262
  // corrupted-store read now falls back here instead of voicing a hollow
263
263
  // seat into the comparison — the same "empty bytes are no grounding"
264
264
  // invariant project() has always enforced.
265
- const seatOfNode = (id, fallback) => reverseContext(ctx, id, qv) ?? fallback;
265
+ // A node that only ever CONTINUES an edge SOURCE with no predecessor —
266
+ // is a learnt CONTEXT (a stored question-shaped frame), not an entity.
267
+ // Voicing it verbatim answers a question with a question (the observed
268
+ // cast echo: two stored questions concatenated as an "answer"). The
269
+ // context's role is established by what it LEADS TO, so its seat is its
270
+ // own continuation — the fact — with the reverse projection and the raw
271
+ // bytes as the ordinary fallbacks for genuine entities.
272
+ const seatOfNode = async (id, fallback) => {
273
+ if (ctx.store.prevCount(id) === 0 && ctx.store.hasNext(id)) {
274
+ const fwd = await follow(ctx, id, qv);
275
+ if (fwd !== null)
276
+ return fwd;
277
+ }
278
+ return reverseContext(ctx, id, qv) ?? fallback;
279
+ };
266
280
  const seatOf = (p) => seatOfNode(p.anchor, p.ctx);
267
281
  const analogs = [];
268
282
  for (const p of points) {
@@ -381,10 +395,10 @@ export async function counterfactualTransfer(ctx, query, pre) {
381
395
  rNode(ctx, dominant.anchor, "analog", bestSim),
382
396
  rNode(ctx, bestAnalog.anchor, "analog", bestSim),
383
397
  ], [], "the two structures keep distributional company beyond chance — genuine analogs");
384
- const a = seatOf(dominant);
398
+ const a = await seatOf(dominant);
385
399
  const b = bestAnalog.point !== null
386
- ? seatOf(bestAnalog.point)
387
- : seatOfNode(bestAnalog.anchor, read(ctx, bestAnalog.anchor));
400
+ ? await seatOf(bestAnalog.point)
401
+ : await seatOfNode(bestAnalog.anchor, read(ctx, bestAnalog.anchor));
388
402
  const answer = await joinWithBridge(ctx, a, b);
389
403
  record(answer, "analogical comparison — each analog voiced by the context that establishes its role", new Set([dominant.anchor, bestAnalog.anchor]),
390
404
  // A halo-mediated act (the analogy gate) plus two seat projections.
@@ -70,6 +70,18 @@ export async function confluenceJoin(ctx, query, pre) {
70
70
  // below.
71
71
  const queryWin = pre.queryWindows;
72
72
  const queryIds = new Set(queryWin.values());
73
+ // A constraint must bind a CONSTITUENT, not a shard. A genuinely shared
74
+ // form weaves a contiguous RUN of shared discriminative windows — its
75
+ // merged cover span is the form's own length, beyond one perception
76
+ // quantum (2W: the same "two quanta of structure" bar the conjunctive
77
+ // precondition `query.length < 2W` and CAST's weave live under). A long
78
+ // stored document holds thousands of W-windows and will hold a FEW of any
79
+ // query's by accident, but accidental sharing is one window (a merged
80
+ // span of W, at most ~W+overlap bytes: "ow ma", "ys a", "ías " —
81
+ // observed), never a run ("​ translucent", " featherlight" — the genuine
82
+ // constraints). Shard-bound streams are no constraints, and their meets
83
+ // are connective debris (". Sure,", "ngul" — observed).
84
+ const bindsAConstituent = (cover) => cover.some(([cs, ce]) => ce - cs >= 2 * W);
73
85
  const streams = [];
74
86
  const rankedCapped = ranked.length > pre.k ? ranked.slice(0, pre.k) : ranked;
75
87
  for (const cand of rankedCapped) {
@@ -96,7 +108,7 @@ export async function confluenceJoin(ctx, query, pre) {
96
108
  else
97
109
  cover.push(curC = [off, off + W]);
98
110
  }
99
- if (cover.length > 0) {
111
+ if (cover.length > 0 && bindsAConstituent(cover)) {
100
112
  streams.push({ anchor: cand.anchor, vote: cand.vote, ids, cover, held });
101
113
  }
102
114
  }