@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.
- package/AGENTS.md +114 -52
- package/HOW_IT_WORKS.md +275 -184
- package/dist/src/mind/bridge.d.ts +5 -7
- package/dist/src/mind/bridge.js +6 -97
- package/dist/src/mind/match.d.ts +159 -0
- package/dist/src/mind/match.js +300 -7
- package/dist/src/mind/mechanisms/prefix-completion.d.ts +22 -0
- package/dist/src/mind/{prefix-completion.js → mechanisms/prefix-completion.js} +64 -91
- package/dist/src/mind/mechanisms/recall.js +10 -108
- package/dist/src/mind/mechanisms/reference.d.ts +6 -0
- package/dist/src/mind/mechanisms/reference.js +296 -0
- package/dist/src/mind/mind.d.ts +1 -1
- package/dist/src/mind/pipeline-mechanism.d.ts +56 -1
- package/dist/src/mind/pipeline-mechanism.js +104 -3
- package/dist/src/mind/pipeline.d.ts +1 -1
- package/dist/src/mind/pipeline.js +13 -1
- package/dist/src/mind/traverse.d.ts +38 -0
- package/dist/src/mind/traverse.js +91 -1
- package/dist/src/store.d.ts +4 -4
- package/jsr.json +6 -0
- package/package.json +1 -1
- package/src/mind/bridge.ts +10 -104
- package/src/mind/match.ts +416 -7
- package/src/mind/{prefix-completion.ts → mechanisms/prefix-completion.ts} +66 -92
- package/src/mind/mechanisms/recall.ts +9 -126
- package/src/mind/mechanisms/reference.ts +343 -0
- package/src/mind/mind.ts +12 -8
- package/src/mind/pipeline-mechanism.ts +120 -3
- package/src/mind/pipeline.ts +16 -2
- package/src/mind/traverse.ts +92 -1
- package/src/store.ts +13 -4
- package/test/33-multi-candidate.test.mjs +21 -11
- package/test/70-prefix-completion.test.mjs +1 -1
- package/test/72-prefix-candidate-supply.test.mjs +7 -9
- package/test/74-prefix-trap-not-sprung-early.test.mjs +1 -1
- package/test/76-reference-binding.test.mjs +471 -0
- package/dist/src/mind/frame-filler.d.ts +0 -15
- package/dist/src/mind/frame-filler.js +0 -535
- package/dist/src/mind/prefix-completion.d.ts +0 -59
- package/src/mind/frame-filler.ts +0 -604
- package/test/69-frame-filler.test.mjs +0 -115
package/src/mind/match.ts
CHANGED
|
@@ -18,19 +18,29 @@
|
|
|
18
18
|
// direct or mutual-sibling)
|
|
19
19
|
// multi-hop pivot byte containment forward —
|
|
20
20
|
// articulation halo sibling substitute conceptThreshold
|
|
21
|
+
// reference frameSlots() (the shared carry into carriesFillers
|
|
22
|
+
// aligner, gaps contracted) the answer
|
|
21
23
|
//
|
|
22
24
|
// This module holds the shared vocabulary those configurations are built
|
|
23
|
-
// from — the MATCHERS (locate, alignRuns, alignGraded,
|
|
24
|
-
// the PROJECTIONS (follow, conceptHop, reverseContext,
|
|
25
|
-
// mechanism file states only its configuration, never its
|
|
26
|
-
// machinery.
|
|
25
|
+
// from — the MATCHERS (locate, alignRuns, alignGraded, alignAround/frameSlots,
|
|
26
|
+
// analogyStrength) and the PROJECTIONS (follow, conceptHop, reverseContext,
|
|
27
|
+
// project) — so each mechanism file states only its configuration, never its
|
|
28
|
+
// own copy of the machinery. Most gates live in geometry.ts (derived, never
|
|
29
|
+
// tuned); the two STRUCTURAL gates that are byte predicates rather than
|
|
30
|
+
// thresholds — isSpanShaped and carriesFillers — live here beside the matchers
|
|
31
|
+
// they gate.
|
|
27
32
|
|
|
28
33
|
import { addInto, cosine, dot, normalize, Vec, zeros } from "../vec.js";
|
|
29
34
|
import type { Hit } from "../store.js";
|
|
30
|
-
import {
|
|
31
|
-
|
|
35
|
+
import {
|
|
36
|
+
conceptThreshold,
|
|
37
|
+
dominates,
|
|
38
|
+
identityBar,
|
|
39
|
+
significanceBar,
|
|
40
|
+
} from "../geometry.js";
|
|
41
|
+
import { bytesEqual, indexOf } from "../bytes.js";
|
|
32
42
|
import type { MindContext } from "./types.js";
|
|
33
|
-
import { leafIdRun } from "./canonical.js";
|
|
43
|
+
import { chainReach, leafIdRun } from "./canonical.js";
|
|
34
44
|
import { foldTree, gistOf, perceive, read, resolve } from "./primitives.js";
|
|
35
45
|
import {
|
|
36
46
|
argmaxCosine,
|
|
@@ -294,6 +304,405 @@ export function alignGraded(
|
|
|
294
304
|
return out;
|
|
295
305
|
}
|
|
296
306
|
|
|
307
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
308
|
+
// THE FRAME READING — variable positions, and the licence to voice through one
|
|
309
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
310
|
+
//
|
|
311
|
+
// Sema is otherwise a fully GROUND system: every item of the deduction system,
|
|
312
|
+
// every matcher and every gate compares ground bytes. Nothing anywhere
|
|
313
|
+
// represents a POSITION whose occupant comes from the context rather than the
|
|
314
|
+
// corpus, and so no mechanism can tell
|
|
315
|
+
//
|
|
316
|
+
// "the corpus does not explain these bytes" (PASS — refuse)
|
|
317
|
+
//
|
|
318
|
+
// apart from
|
|
319
|
+
//
|
|
320
|
+
// "these bytes occupy a place the corpus keeps open" (bind).
|
|
321
|
+
//
|
|
322
|
+
// Both arrive as unaligned residue. That single missing distinction is why
|
|
323
|
+
// the substitution bridge refuses on `attestedQ`, why the cover charges PASS
|
|
324
|
+
// over a slot, and why CAST reads a filler as noise rather than as the
|
|
325
|
+
// variable it is. The family below supplies it, and it lives HERE — not in
|
|
326
|
+
// any mechanism — because it is the ordinary (matcher, projection, gate)
|
|
327
|
+
// triple of §2.5 with its three parts in their proper places:
|
|
328
|
+
//
|
|
329
|
+
// matcher alignAround + contractGap + frameSlots — bytes only, no
|
|
330
|
+
// projection, no licence. SAFE FOR EVERY CONSUMER: knowing a
|
|
331
|
+
// span is variable can only improve an alignment.
|
|
332
|
+
// projection follow / project — unchanged, already here.
|
|
333
|
+
// gate carriesFillers — the licence to VOICE through a slot. Needed
|
|
334
|
+
// only by a mechanism that voices, so it is deliberately NOT
|
|
335
|
+
// folded into the matcher.
|
|
336
|
+
//
|
|
337
|
+
// The split matters. Slot detection is universal; the licence is not, and a
|
|
338
|
+
// consumer that took the matcher's answer as permission to voice would be
|
|
339
|
+
// making exactly the claim the licence exists to withhold.
|
|
340
|
+
|
|
341
|
+
/** One place two byte streams DISAGREE, between runs where they agree: the
|
|
342
|
+
* query span `[qs,qe)` standing where the candidate's `[cs,ce)` stands.
|
|
343
|
+
*
|
|
344
|
+
* Two mechanisms read the same gap and ask OPPOSITE questions of it, which is
|
|
345
|
+
* why the shape lives here rather than in either of them:
|
|
346
|
+
*
|
|
347
|
+
* • the substitution bridge asks whether the two sides MEAN THE SAME, and
|
|
348
|
+
* so EXPANDS the gap (absorbing flanking matched bytes) until the query
|
|
349
|
+
* side is corpus-attested and the pair clears the concept bar;
|
|
350
|
+
* • the frame reading asks WHERE THE SLOT IS, and so CONTRACTS it
|
|
351
|
+
* ({@link contractGap}) until the two sides share nothing at all.
|
|
352
|
+
*
|
|
353
|
+
* Neither reading is derivable from the other, and both need the same gap. */
|
|
354
|
+
export interface AlignGap {
|
|
355
|
+
qs: number;
|
|
356
|
+
qe: number;
|
|
357
|
+
cs: number;
|
|
358
|
+
ce: number;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** Extend a seed match (query offset qo ↔ candidate offset co) to its maximal
|
|
362
|
+
* common run, then walk outward in both directions collecting further common
|
|
363
|
+
* runs of at least W bytes across bounded mismatch gaps (each side ≤
|
|
364
|
+
* chainReach). Returns the matched query spans and the mismatch pairs
|
|
365
|
+
* between consecutive runs.
|
|
366
|
+
*
|
|
367
|
+
* This is the SEEDED aligner, distinct from {@link alignRuns}: that one finds
|
|
368
|
+
* every run two structures share anywhere (a weave), this one reads two
|
|
369
|
+
* streams as ONE structure that diverges in bounded places (a frame with
|
|
370
|
+
* slots).
|
|
371
|
+
*
|
|
372
|
+
* Gaps come back in SWEEP order (right sweep, then left), not query order,
|
|
373
|
+
* and only the INTERIOR ones are reported — a consumer that needs the query's
|
|
374
|
+
* unmatched head or tail derives it from `matched`. Both are the bridge's
|
|
375
|
+
* contract, which prices its edges separately (see its matchStart/matchEnd
|
|
376
|
+
* window test); {@link frameSlots} takes the other reading. */
|
|
377
|
+
export function alignAround(
|
|
378
|
+
ctx: MindContext,
|
|
379
|
+
q: Uint8Array,
|
|
380
|
+
c: Uint8Array,
|
|
381
|
+
qo: number,
|
|
382
|
+
co: number,
|
|
383
|
+
): { matched: Array<[number, number]>; gaps: AlignGap[] } {
|
|
384
|
+
const W = ctx.space.maxGroup;
|
|
385
|
+
const reachCap = chainReach(W);
|
|
386
|
+
// Maximal run around the seed.
|
|
387
|
+
let qs = qo, ss = co;
|
|
388
|
+
while (qs > 0 && ss > 0 && q[qs - 1] === c[ss - 1]) {
|
|
389
|
+
qs--;
|
|
390
|
+
ss--;
|
|
391
|
+
}
|
|
392
|
+
let qe = qo, se = co;
|
|
393
|
+
while (qe < q.length && se < c.length && q[qe] === c[se]) {
|
|
394
|
+
qe++;
|
|
395
|
+
se++;
|
|
396
|
+
}
|
|
397
|
+
const matched: Array<[number, number]> = [[qs, qe]];
|
|
398
|
+
const gaps: AlignGap[] = [];
|
|
399
|
+
// The next common run of ≥ W bytes past (qi, si), with each side's gap
|
|
400
|
+
// bounded by chainReach; smallest total gap wins (nearest continuation).
|
|
401
|
+
const runLenAt = (qi: number, si: number): number => {
|
|
402
|
+
let n = 0;
|
|
403
|
+
while (qi + n < q.length && si + n < c.length && q[qi + n] === c[si + n]) {
|
|
404
|
+
n++;
|
|
405
|
+
}
|
|
406
|
+
return n;
|
|
407
|
+
};
|
|
408
|
+
// RIGHT sweep.
|
|
409
|
+
let qi = qe, si = se;
|
|
410
|
+
for (;;) {
|
|
411
|
+
let found = false;
|
|
412
|
+
for (let total = 1; total <= 2 * reachCap && !found; total++) {
|
|
413
|
+
for (let gq = 0; gq <= Math.min(total, reachCap); gq++) {
|
|
414
|
+
const gs = total - gq;
|
|
415
|
+
if (gs > reachCap) continue;
|
|
416
|
+
if (qi + gq >= q.length || si + gs >= c.length) continue;
|
|
417
|
+
const n = runLenAt(qi + gq, si + gs);
|
|
418
|
+
if (n >= W || qi + gq + n === q.length) {
|
|
419
|
+
if (n === 0) continue;
|
|
420
|
+
if (gq > 0 || gs > 0) {
|
|
421
|
+
gaps.push({ qs: qi, qe: qi + gq, cs: si, ce: si + gs });
|
|
422
|
+
}
|
|
423
|
+
matched.push([qi + gq, qi + gq + n]);
|
|
424
|
+
qi = qi + gq + n;
|
|
425
|
+
si = si + gs + n;
|
|
426
|
+
found = true;
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (!found) break;
|
|
432
|
+
}
|
|
433
|
+
// LEFT sweep (mirror).
|
|
434
|
+
qi = qs;
|
|
435
|
+
si = ss;
|
|
436
|
+
for (;;) {
|
|
437
|
+
let found = false;
|
|
438
|
+
for (let total = 1; total <= 2 * reachCap && !found; total++) {
|
|
439
|
+
for (let gq = 0; gq <= Math.min(total, reachCap); gq++) {
|
|
440
|
+
const gs = total - gq;
|
|
441
|
+
if (gs > reachCap) continue;
|
|
442
|
+
if (qi - gq <= 0 || si - gs <= 0) continue;
|
|
443
|
+
// Run ENDING at (qi - gq, si - gs).
|
|
444
|
+
let n = 0;
|
|
445
|
+
while (
|
|
446
|
+
n < qi - gq && n < si - gs &&
|
|
447
|
+
q[qi - gq - 1 - n] === c[si - gs - 1 - n]
|
|
448
|
+
) {
|
|
449
|
+
n++;
|
|
450
|
+
}
|
|
451
|
+
if (n >= W || n === qi - gq) {
|
|
452
|
+
if (n === 0) continue;
|
|
453
|
+
if (gq > 0 || gs > 0) {
|
|
454
|
+
gaps.push({ qs: qi - gq, qe: qi, cs: si - gs, ce: si });
|
|
455
|
+
}
|
|
456
|
+
matched.push([qi - gq - n, qi - gq]);
|
|
457
|
+
qi = qi - gq - n;
|
|
458
|
+
si = si - gs - n;
|
|
459
|
+
found = true;
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (!found) break;
|
|
465
|
+
}
|
|
466
|
+
return { matched, gaps };
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** Contract a gap to its VARYING CORE: strip the prefix and suffix the two
|
|
470
|
+
* sides share. {@link alignAround} cannot match a shared affix shorter than
|
|
471
|
+
* W, so that affix lands INSIDE the gap — measured, the slot of
|
|
472
|
+
* `How do I compile main.c?` against `…hello.c?` comes back as
|
|
473
|
+
* `main.c?`/`hello.c?`, three bytes of which (`.c?`) both sides hold.
|
|
474
|
+
*
|
|
475
|
+
* Splicing the uncontracted gap carries the query's own punctuation into the
|
|
476
|
+
* answer; worse, it hides what actually VARIES, which is the only thing a
|
|
477
|
+
* cohort can agree about. Returns null when nothing is left on either side —
|
|
478
|
+
* a pure insertion or deletion, which names no slot. */
|
|
479
|
+
export function contractGap(
|
|
480
|
+
q: Uint8Array,
|
|
481
|
+
c: Uint8Array,
|
|
482
|
+
g: AlignGap,
|
|
483
|
+
): AlignGap | null {
|
|
484
|
+
let { qs, qe, cs, ce } = g;
|
|
485
|
+
while (qs < qe && cs < ce && q[qs] === c[cs]) {
|
|
486
|
+
qs++;
|
|
487
|
+
cs++;
|
|
488
|
+
}
|
|
489
|
+
while (qe > qs && ce > cs && q[qe - 1] === c[ce - 1]) {
|
|
490
|
+
qe--;
|
|
491
|
+
ce--;
|
|
492
|
+
}
|
|
493
|
+
return qe > qs && ce > cs ? { qs, qe, cs, ce } : null;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** What one place two streams disagree IS, once contracted to its varying
|
|
497
|
+
* core. A consumer decides which kinds it can use; the matcher only reports.
|
|
498
|
+
*
|
|
499
|
+
* substitution both sides carry bytes — one thing stands where another does
|
|
500
|
+
* insertion the query carries bytes the candidate does not
|
|
501
|
+
* deletion the candidate carries bytes the query does not */
|
|
502
|
+
export type SlotKind = "substitution" | "insertion" | "deletion";
|
|
503
|
+
|
|
504
|
+
/** One VARIABLE POSITION of a pairing: where the query and a candidate differ,
|
|
505
|
+
* contracted to the bytes that actually vary. */
|
|
506
|
+
export interface FrameSlot {
|
|
507
|
+
/** Query span (empty for a deletion). */
|
|
508
|
+
qs: number;
|
|
509
|
+
qe: number;
|
|
510
|
+
/** Candidate span (empty for an insertion). */
|
|
511
|
+
cs: number;
|
|
512
|
+
ce: number;
|
|
513
|
+
kind: SlotKind;
|
|
514
|
+
/** The candidate's own bytes here — empty for an insertion. */
|
|
515
|
+
filler: Uint8Array;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/** One trained context read against the query as ONE structure with variable
|
|
519
|
+
* positions.
|
|
520
|
+
*
|
|
521
|
+
* EVERYTHING THE ALIGNER SAW, NOTHING JUDGED. `slots` holds every place the
|
|
522
|
+
* pairing varies, in query order, whatever its kind or size, and `covered`
|
|
523
|
+
* says how much of the query the two hold in common. No gate is applied
|
|
524
|
+
* here — see {@link frameSlots}. */
|
|
525
|
+
export interface FrameInstance {
|
|
526
|
+
/** The trained context this reading is against. */
|
|
527
|
+
id: number;
|
|
528
|
+
/** Every variable position, in query order. */
|
|
529
|
+
slots: FrameSlot[];
|
|
530
|
+
/** Query spans the pairing literally matched — the frame itself. */
|
|
531
|
+
matched: Array<[number, number]>;
|
|
532
|
+
/** Query bytes the frame accounts for: the size of what is shared. */
|
|
533
|
+
covered: number;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** THE SLOT MATCHER: read one query ↔ context pairing as one structure with
|
|
537
|
+
* variable positions.
|
|
538
|
+
*
|
|
539
|
+
* IT REPORTS; IT DOES NOT JUDGE. This returns every gap the aligner found,
|
|
540
|
+
* contracted to its varying core and tagged with its kind, plus the shared
|
|
541
|
+
* coverage — and rejects nothing. That is the whole point of the split, and
|
|
542
|
+
* it was got WRONG first: four VOICING gates (the frame must dominate the
|
|
543
|
+
* query, each slot must reach one window on both sides, an insertion or
|
|
544
|
+
* deletion disqualifies the pairing, fillers must be pairwise distinct) were
|
|
545
|
+
* applied here, and every one of them is a requirement for SUBSTITUTING AND
|
|
546
|
+
* SPEAKING, not for knowing where a pairing varies. With them in place the
|
|
547
|
+
* shared reading was reference-shaped: measured over four real pairings, three
|
|
548
|
+
* were hidden from every consumer —
|
|
549
|
+
*
|
|
550
|
+
* `What is the capital of the country where the Eiffel Tower is?`
|
|
551
|
+
* against `What is the capital of France?` (covered 23/61) HIDDEN
|
|
552
|
+
* `What is the capital of France, really?` (an insertion) HIDDEN
|
|
553
|
+
* `What is the capital of Fran?` (sub-window) HIDDEN
|
|
554
|
+
*
|
|
555
|
+
* — including the case of the one consumer that most obviously needed it. A
|
|
556
|
+
* shared layer with one usable consumer is private code at a public address.
|
|
557
|
+
* Each gate now lives with the mechanism that needs it (see reference.ts).
|
|
558
|
+
*
|
|
559
|
+
* Seeded at the origin, because a frame is shared structure the query and its
|
|
560
|
+
* instances both OPEN with: the maximal run around (0,0) is the frame's head
|
|
561
|
+
* and the sweeps find the rest.
|
|
562
|
+
*
|
|
563
|
+
* Null only for a degenerate pairing (either side empty). */
|
|
564
|
+
export function frameSlots(
|
|
565
|
+
ctx: MindContext,
|
|
566
|
+
query: Uint8Array,
|
|
567
|
+
cand: Uint8Array,
|
|
568
|
+
id: number,
|
|
569
|
+
): FrameInstance | null {
|
|
570
|
+
if (query.length === 0 || cand.length === 0) return null;
|
|
571
|
+
const { matched, gaps } = alignAround(ctx, query, cand, 0, 0);
|
|
572
|
+
const spans = [...matched].sort((a, b) => a[0] - b[0]);
|
|
573
|
+
// Where the alignment RAN OUT on each side. Seeded at the origin there is
|
|
574
|
+
// no leading gap, so both cursors are everything consumed so far: the
|
|
575
|
+
// matched runs (equal length on both sides by construction) plus what each
|
|
576
|
+
// interior gap ate of its own side. Counting only the runs reads the
|
|
577
|
+
// candidate cursor short by exactly the fillers already seen, and invents a
|
|
578
|
+
// trailing gap on every well-aligned instance.
|
|
579
|
+
const all: AlignGap[] = [...gaps];
|
|
580
|
+
let qEnd = 0, cEnd = 0;
|
|
581
|
+
for (const [s, e] of spans) {
|
|
582
|
+
cEnd += e - s;
|
|
583
|
+
qEnd = Math.max(qEnd, e);
|
|
584
|
+
}
|
|
585
|
+
for (const g of gaps) cEnd += g.ce - g.cs;
|
|
586
|
+
if (qEnd < query.length || cEnd < cand.length) {
|
|
587
|
+
all.push({ qs: qEnd, qe: query.length, cs: cEnd, ce: cand.length });
|
|
588
|
+
}
|
|
589
|
+
const slots: FrameSlot[] = [];
|
|
590
|
+
for (const gap of all.sort((a, b) => a.qs - b.qs)) {
|
|
591
|
+
if (gap.qe <= gap.qs && gap.ce <= gap.cs) continue;
|
|
592
|
+
// Contract to the varying core. contractGap returns null when one side is
|
|
593
|
+
// wholly shared with the other — a pure insertion or deletion, which is a
|
|
594
|
+
// real variation and is reported AS ONE, not discarded.
|
|
595
|
+
const core = contractGap(query, cand, gap);
|
|
596
|
+
const g = core ?? gap;
|
|
597
|
+
const kind: SlotKind = g.qe > g.qs && g.ce > g.cs
|
|
598
|
+
? "substitution"
|
|
599
|
+
: g.qe > g.qs
|
|
600
|
+
? "insertion"
|
|
601
|
+
: "deletion";
|
|
602
|
+
slots.push({
|
|
603
|
+
qs: g.qs,
|
|
604
|
+
qe: g.qe,
|
|
605
|
+
cs: g.cs,
|
|
606
|
+
ce: g.ce,
|
|
607
|
+
kind,
|
|
608
|
+
filler: cand.slice(g.cs, g.ce),
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
const covered = spans.reduce((n, [s, e]) => n + e - s, 0);
|
|
612
|
+
return { id, slots, matched: spans, covered };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/** Whether every member is byte-distinct from the others. */
|
|
616
|
+
export function distinct(items: readonly Uint8Array[]): boolean {
|
|
617
|
+
for (let i = 0; i < items.length; i++) {
|
|
618
|
+
for (let j = i + 1; j < items.length; j++) {
|
|
619
|
+
if (bytesEqual(items[i], items[j])) return false;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return true;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
/** Substitute every `needle -> repl` pair SIMULTANEOUSLY: one left-to-right
|
|
626
|
+
* pass, longest needle first at each position, and a replacement is never
|
|
627
|
+
* re-examined.
|
|
628
|
+
*
|
|
629
|
+
* SIMULTANEOUS IS NOT A DETAIL. Applying the pairs in sequence lets one
|
|
630
|
+
* substitution's OUTPUT be another's input: with slots `gcc -> zig` and
|
|
631
|
+
* `hello.c -> zig.c` a sequential pass rewrites bytes it had just written,
|
|
632
|
+
* and the result depends on the order the slots happened to be found in.
|
|
633
|
+
* Longest-first at each position makes the pass independent of pair order,
|
|
634
|
+
* which is what keeps {@link carriesFillers} and the binding it licenses the
|
|
635
|
+
* SAME operation — if they could disagree, the licence would not be testing
|
|
636
|
+
* what is voiced. */
|
|
637
|
+
export function substituteAll(
|
|
638
|
+
hay: Uint8Array,
|
|
639
|
+
pairs: ReadonlyArray<{ needle: Uint8Array; repl: Uint8Array }>,
|
|
640
|
+
): Uint8Array {
|
|
641
|
+
const usable = pairs.filter((p) => p.needle.length > 0);
|
|
642
|
+
if (usable.length === 0) return hay;
|
|
643
|
+
// Longest needle first, so a needle that is a prefix of another can never
|
|
644
|
+
// pre-empt it. Ties cannot arise: an instance whose fillers are not
|
|
645
|
+
// pairwise distinct is refused by frameSlots.
|
|
646
|
+
const order = [...usable].sort((a, b) => b.needle.length - a.needle.length);
|
|
647
|
+
const out: number[] = [];
|
|
648
|
+
let i = 0;
|
|
649
|
+
let hit = false;
|
|
650
|
+
outer:
|
|
651
|
+
while (i < hay.length) {
|
|
652
|
+
for (const p of order) {
|
|
653
|
+
if (i + p.needle.length > hay.length) continue;
|
|
654
|
+
let k = 0;
|
|
655
|
+
while (k < p.needle.length && hay[i + k] === p.needle[k]) k++;
|
|
656
|
+
if (k < p.needle.length) continue;
|
|
657
|
+
for (const b of p.repl) out.push(b);
|
|
658
|
+
i += p.needle.length;
|
|
659
|
+
hit = true;
|
|
660
|
+
continue outer;
|
|
661
|
+
}
|
|
662
|
+
out.push(hay[i]);
|
|
663
|
+
i++;
|
|
664
|
+
}
|
|
665
|
+
return hit ? Uint8Array.from(out) : hay;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/** THE CARRIAGE LICENCE — the gate that decides whether a slot may be VOICED
|
|
669
|
+
* through. Given two instances of one frame and what each one continues to,
|
|
670
|
+
* it asks one byte question:
|
|
671
|
+
*
|
|
672
|
+
* substituteAll(contA, fillersA -> fillersB) == contB
|
|
673
|
+
*
|
|
674
|
+
* When it holds, the corpus attests byte-exactly that the continuation is a
|
|
675
|
+
* function of the fillers and nothing else, so putting a NEW occupant through
|
|
676
|
+
* the same carriage is derivation rather than invention. No threshold, no
|
|
677
|
+
* similarity, no new constant: the store's own instances decide, exactly as
|
|
678
|
+
* the bridge's `unanimous` decides whether a frame is a value slot.
|
|
679
|
+
*
|
|
680
|
+
* Its FAILURE is what this is really for. A frame whose continuation carries
|
|
681
|
+
* filler-DEPENDENT content — `What is the capital of X?` answering a different
|
|
682
|
+
* city per X — fails it, and that failure is the only thing between a slot
|
|
683
|
+
* and an invented fact. Measured on the trained 15.7M-node store (325,615
|
|
684
|
+
* contexts): `What is the capital of Zamunda?` resonates to a PURE cohort,
|
|
685
|
+
* every one of the top 14 hits an instance of that frame, with an unambiguous
|
|
686
|
+
* slot; every structural gate passes and only this one refuses, on
|
|
687
|
+
* `replace("Tokyo", "Japan" -> "France") != "Paris"`.
|
|
688
|
+
*
|
|
689
|
+
* With SEVERAL slots the test is unchanged, which is the point of testing the
|
|
690
|
+
* whole substitution at once: a frame whose answer tracks one slot but
|
|
691
|
+
* invents around another fails exactly as a single-slot value slot does. */
|
|
692
|
+
export function carriesFillers(
|
|
693
|
+
contA: Uint8Array,
|
|
694
|
+
fillersA: readonly Uint8Array[],
|
|
695
|
+
contB: Uint8Array,
|
|
696
|
+
fillersB: readonly Uint8Array[],
|
|
697
|
+
): boolean {
|
|
698
|
+
if (fillersA.length !== fillersB.length) return false;
|
|
699
|
+
const projected = substituteAll(
|
|
700
|
+
contA,
|
|
701
|
+
fillersA.map((needle, s) => ({ needle, repl: fillersB[s] })),
|
|
702
|
+
);
|
|
703
|
+
return bytesEqual(projected, contB);
|
|
704
|
+
}
|
|
705
|
+
|
|
297
706
|
/** The IN-LIST halo matcher: the best halo-mate for `halo` among EXPLICIT
|
|
298
707
|
* candidates, above the concept threshold — the list counterpart of
|
|
299
708
|
* {@link haloSiblings}, which asks the halo INDEX for candidates instead.
|
|
@@ -1,5 +1,20 @@
|
|
|
1
|
-
// prefix-completion.ts — Grounding a query that IS the opening of a
|
|
2
|
-
// form.
|
|
1
|
+
// mechanisms/prefix-completion.ts — Grounding a query that IS the opening of a
|
|
2
|
+
// trained form (Grounding V).
|
|
3
|
+
//
|
|
4
|
+
// A MECHANISM, NOT A TIER. This used to run inside recall's refusal path, in
|
|
5
|
+
// a fixed if-chain that first-match-wins — the shape CAST was refactored away
|
|
6
|
+
// from, where placement rather than the cost ladder decided. Its claim is
|
|
7
|
+
// maximal (every query byte literally matched, from offset zero, against a
|
|
8
|
+
// trained form) at one STEP, so as a market candidate it competes honestly and
|
|
9
|
+
// the decider weighs it like everything else. It is registered LAST: recall's
|
|
10
|
+
// exact self-match makes an IDENTITY claim about the query while this makes a
|
|
11
|
+
// CONTAINMENT one, and on an exact grade tie the identity claim is the
|
|
12
|
+
// stronger evidence — the same ordering §2.3's ladders use.
|
|
13
|
+
//
|
|
14
|
+
// Its SUPPLY moved too, and further: `formsOpenedBy` (traverse.ts) answers a
|
|
15
|
+
// question about the STORE — "which trained forms does this byte run open?" —
|
|
16
|
+
// so it is retrieval machinery any mechanism may ask, not this one's private
|
|
17
|
+
// helper.
|
|
3
18
|
//
|
|
4
19
|
// THE SHAPE. `The capital of France is` grounds nothing, while
|
|
5
20
|
// `The capital of France is Paris.` is trained and reads back byte-exact. The
|
|
@@ -75,96 +90,12 @@
|
|
|
75
90
|
// same continuation reached through two trained forms is one answer, not an
|
|
76
91
|
// ambiguity.
|
|
77
92
|
|
|
78
|
-
import type { MindContext } from "
|
|
79
|
-
import { bytesEqual } from "
|
|
80
|
-
import { rItem } from "
|
|
81
|
-
import {
|
|
82
|
-
import {
|
|
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
|
-
}
|
|
93
|
+
import type { MindContext } from "../types.js";
|
|
94
|
+
import { bytesEqual } from "../../bytes.js";
|
|
95
|
+
import { rItem } from "../trace.js";
|
|
96
|
+
import { formsOpenedBy } from "../traverse.js";
|
|
97
|
+
import { STEP } from "../graph-search.js";
|
|
98
|
+
import type { PipelineMechanism, Precomputed } from "../pipeline-mechanism.js";
|
|
168
99
|
|
|
169
100
|
/** A trained form the query opens, and the bytes by which it continues. */
|
|
170
101
|
export interface PrefixCompletion {
|
|
@@ -312,3 +243,46 @@ export function prefixCompletion(
|
|
|
312
243
|
data,
|
|
313
244
|
);
|
|
314
245
|
}
|
|
246
|
+
|
|
247
|
+
// ── Pipeline mechanism ──────────────────────────────────────────────────────
|
|
248
|
+
|
|
249
|
+
export const prefixMechanism: PipelineMechanism = {
|
|
250
|
+
name: "prefix",
|
|
251
|
+
provenance: "prefix",
|
|
252
|
+
async floor(ctx, query, _pre, worthRunning) {
|
|
253
|
+
// One projection: the form is voiced whole, nothing is substituted.
|
|
254
|
+
// INVESTMENT DISCIPLINE — the supplies below are the response's wide
|
|
255
|
+
// candidate list and a bounded √N walk, so neither is touched until the
|
|
256
|
+
// bound can still beat the incumbent.
|
|
257
|
+
if (!worthRunning(STEP)) return STEP;
|
|
258
|
+
// A query with no room for a perceivable continuation inside the phrase
|
|
259
|
+
// cap cannot clear guard 2, so it is not worth a single read.
|
|
260
|
+
const cap = query.length * ctx.space.maxGroup;
|
|
261
|
+
if (query.length === 0 || cap < query.length + ctx.space.maxGroup) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
return STEP;
|
|
265
|
+
},
|
|
266
|
+
async run(ctx, query, pre) {
|
|
267
|
+
// The response's shared wide list first; only when it supplies nothing does
|
|
268
|
+
// the write side's window index propose. That ordering is the whole cost
|
|
269
|
+
// story: a query the ranked list can already explain pays not one extra
|
|
270
|
+
// read, and the bounded walk is spent only where the alternative is an
|
|
271
|
+
// empty answer. A second SUPPLY, not a second mechanism — the same three
|
|
272
|
+
// guards decide either way.
|
|
273
|
+
const completed = prefixCompletion(ctx, query, await pre.wideResonance()) ??
|
|
274
|
+
prefixCompletion(ctx, query, formsOpenedBy(ctx, query));
|
|
275
|
+
if (completed === null) return [];
|
|
276
|
+
return [{
|
|
277
|
+
bytes: completed.form,
|
|
278
|
+
// Every query byte is literally matched against the form, so there is
|
|
279
|
+
// nothing to be humble about in the accounting — the same reading the
|
|
280
|
+
// IDENTITY bridge takes.
|
|
281
|
+
accounted: [[0, query.length]],
|
|
282
|
+
moves: STEP,
|
|
283
|
+
unexplained: "",
|
|
284
|
+
// NOT complete: the query is a proper PREFIX, so the form may carry more
|
|
285
|
+
// past the remainder this voiced.
|
|
286
|
+
}];
|
|
287
|
+
},
|
|
288
|
+
};
|