@hviana/sema 0.5.7 → 0.5.9
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 +23 -0
- package/DATASETS.md +159 -0
- package/HOW_IT_WORKS.md +74 -0
- package/README.md +12 -0
- package/dist/example/train_base.d.ts +73 -3
- package/dist/example/train_base.js +1000 -49
- package/dist/src/geometry.d.ts +20 -0
- package/dist/src/geometry.js +22 -0
- package/dist/src/mind/articulation.js +15 -2
- package/dist/src/mind/attention.d.ts +6 -0
- package/dist/src/mind/attention.js +44 -4
- package/dist/src/mind/learning.js +250 -3
- package/dist/src/mind/mechanisms/cast.js +45 -1
- package/dist/src/mind/mind.d.ts +6 -1
- package/dist/src/mind/mind.js +14 -2
- package/dist/src/mind/reasoning.js +59 -5
- package/dist/src/mind/recognition.js +29 -3
- package/dist/src/mind/traverse.d.ts +34 -0
- package/dist/src/mind/traverse.js +42 -0
- package/dist/src/store-sqlite.d.ts +4 -0
- package/dist/src/store-sqlite.js +47 -0
- package/dist/src/store.d.ts +7 -0
- package/example/train_base.ts +1193 -46
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/geometry.ts +23 -0
- package/src/mind/articulation.ts +16 -2
- package/src/mind/attention.ts +54 -1
- package/src/mind/learning.ts +253 -4
- package/src/mind/mechanisms/cast.ts +48 -1
- package/src/mind/mind.ts +12 -1
- package/src/mind/reasoning.ts +64 -5
- package/src/mind/recognition.ts +29 -3
- package/src/mind/traverse.ts +48 -0
- package/src/store-sqlite.ts +53 -0
- package/src/store.ts +28 -0
- package/test/29-counterfactual.test.mjs +43 -6
- package/test/76-type-level-company.test.mjs +342 -0
- package/test/77-company-saturation.test.mjs +302 -0
- package/test/78-atom-hub-recognition-cliff.test.mjs +135 -0
- package/test/84-composed-answer-honesty.test.mjs +136 -0
- package/test/85-answered-directly.test.mjs +126 -0
- package/test/86-cast-voices-committed.test.mjs +164 -0
- package/test/87-codominant-commitment.test.mjs +250 -0
package/src/mind/recognition.ts
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
perceive,
|
|
16
16
|
resolve,
|
|
17
17
|
} from "./primitives.js";
|
|
18
|
-
import { atomIsHub, corpusN, leadsSomewhere } from "./traverse.js";
|
|
18
|
+
import { atomIsHub, bearsEdge, corpusN, leadsSomewhere } from "./traverse.js";
|
|
19
19
|
import { chainReach, leafIdAt, leafIdRun } from "./canonical.js";
|
|
20
20
|
import { canonHash } from "../canon.js";
|
|
21
21
|
import { isChunk, type Sema } from "../sema.js";
|
|
@@ -594,6 +594,31 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
|
|
|
594
594
|
// "Eiffel Tower" site vanished with it). The premise is wrong but the
|
|
595
595
|
// trust it stood in for is real; a replacement signal is still open work.
|
|
596
596
|
// See bench/README.md.
|
|
597
|
+
//
|
|
598
|
+
// THE REPLACEMENT SIGNAL (2026-08-13): `leadsSomewhere` on the BYTE-EXACT
|
|
599
|
+
// branch the chain already found. The blanket off-boundary suppression is
|
|
600
|
+
// a decision that CHANGES WITH CORPUS SIZE — `atomsAreHubs` flips at
|
|
601
|
+
// N = 4096 (atomReach = ⌈N·W/256⌉ exceeds √N there) — so a store crossing
|
|
602
|
+
// that point silently loses interior sites it used to have. Measured: with
|
|
603
|
+
// the two-hop chain deposited, `recognise("The country of Eiffel Tower is
|
|
604
|
+
// France.")` yields 4 sites including `France` at N = 3920 and 2 sites
|
|
605
|
+
// without it at N = 4227; the pivot dies with the site and multi-hop goes
|
|
606
|
+
// silent from there up (the trained store is N = 325,615).
|
|
607
|
+
//
|
|
608
|
+
// The honest gate is the one `emit` already applies, moved EARLIER and paid
|
|
609
|
+
// for with existence probes instead of a fold: `findBranch` has already
|
|
610
|
+
// proved these bytes are a stored branch, so the only remaining question is
|
|
611
|
+
// whether that branch is a deposited whole (bears an edge or a halo) or an
|
|
612
|
+
// interned fragment. "hi" out of "W[hi]ch" leads nowhere and is still
|
|
613
|
+
// suppressed; `France` bears both and is admitted. Structural, not scalar
|
|
614
|
+
// — no constant enters and nothing reads N, so the verdict no longer moves
|
|
615
|
+
// when the corpus grows.
|
|
616
|
+
//
|
|
617
|
+
// COST: `bearsEdge` is the response-MEMOISED edge probe, not the full
|
|
618
|
+
// `leadsSomewhere` — its uncached `hasHalo` tier took haloProbes from 922 to
|
|
619
|
+
// 9,144 on a nine-query battery over the trained store, which is not a price
|
|
620
|
+
// this pass may charge. `emit` still applies the full predicate, so this is
|
|
621
|
+
// a pre-filter that never widens what is admitted.
|
|
597
622
|
const tryChain = (
|
|
598
623
|
p: number,
|
|
599
624
|
maxIds: number,
|
|
@@ -610,8 +635,9 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
|
|
|
610
635
|
if (!nx) break;
|
|
611
636
|
ids.push(nx.id);
|
|
612
637
|
pos = nx.end;
|
|
613
|
-
|
|
614
|
-
if (
|
|
638
|
+
const branch = store.findBranch(ids);
|
|
639
|
+
if (branch === null) continue;
|
|
640
|
+
if (!boundary && atomsAreHubs && !bearsEdge(ctx, branch)) continue;
|
|
615
641
|
const id = resolveSpan(p, pos);
|
|
616
642
|
if (id === null || id === prevId) continue;
|
|
617
643
|
prevId = id;
|
package/src/mind/traverse.ts
CHANGED
|
@@ -461,6 +461,25 @@ export function atomIsHub(ctx: MindContext, contextCount: number): boolean {
|
|
|
461
461
|
return atomReach(ctx, contextCount) > boundFor(contextCount);
|
|
462
462
|
}
|
|
463
463
|
|
|
464
|
+
/** Cached "does this node bear a continuation edge?" — the CHEAP half of
|
|
465
|
+
* {@link leadsSomewhere}, exported for hot paths that must PRE-FILTER a
|
|
466
|
+
* candidate before paying for a fold and cannot afford the halo tier.
|
|
467
|
+
*
|
|
468
|
+
* `leadsSomewhere`'s second tier (`hasHalo`) is deliberately uncached — one
|
|
469
|
+
* indexed point probe per candidate, which is right where candidates are
|
|
470
|
+
* already few. On recognition's off-boundary chain pass they are not few:
|
|
471
|
+
* using the full predicate there took haloProbes from 922 to 9,144 on a
|
|
472
|
+
* nine-query battery over the trained store. The edge tier alone is memoised
|
|
473
|
+
* for the response, so it is ~free, and a node bearing an edge is exactly the
|
|
474
|
+
* "deposited whole, not an interned fragment" claim that pass needs.
|
|
475
|
+
*
|
|
476
|
+
* Strictly NARROWER than `leadsSomewhere` — a halo-only node reads false — so
|
|
477
|
+
* it is sound as a pre-filter before a consumer that applies the full
|
|
478
|
+
* predicate, and never as a replacement for it. */
|
|
479
|
+
export function bearsEdge(ctx: MindContext, id: number): boolean {
|
|
480
|
+
return cachedHasNext(ctx, id, getStructCache(ctx));
|
|
481
|
+
}
|
|
482
|
+
|
|
464
483
|
/** Whether a node LEADS SOMEWHERE — it bears a continuation edge or a halo.
|
|
465
484
|
* The admission predicate recognition filters sites with (HOW_IT_WORKS
|
|
466
485
|
* §15.3): a form that leads nowhere contributes nothing to any derivation.
|
|
@@ -558,6 +577,35 @@ export function contains(
|
|
|
558
577
|
return false;
|
|
559
578
|
}
|
|
560
579
|
|
|
580
|
+
/** Whether a continuation edge joins the two forms, in either direction —
|
|
581
|
+
* the EXACT half's veto on calling them synonyms.
|
|
582
|
+
*
|
|
583
|
+
* Halos measure company, and the strongest company any two forms can keep is
|
|
584
|
+
* standing next to each other: a question and its answer co-occur in every
|
|
585
|
+
* episode that taught the pair, so their halos SHOULD be similar, and on a
|
|
586
|
+
* conversational store they are (measured on the CONV fixture: consecutive
|
|
587
|
+
* turns at 0.809 against a 0.516 concept threshold). A gate reading halo
|
|
588
|
+
* cosine alone therefore reads adjacency as synonymy and revoices an answer
|
|
589
|
+
* in the words of the question it answers — "it hangs in madrid" spliced back
|
|
590
|
+
* into "where is it kept now". The distributional layer cannot tell the two
|
|
591
|
+
* relations apart, because to it they are the same observation; the exact
|
|
592
|
+
* half can, for free, because it stored the edge. §4.1's division of labour
|
|
593
|
+
* exactly: approximate proposes, exact decides.
|
|
594
|
+
*
|
|
595
|
+
* Read LIMITed in both directions at the hub bound — a common continuation's
|
|
596
|
+
* fan-in is corpus-sized, and no single decision may scale with it. */
|
|
597
|
+
export function answers(
|
|
598
|
+
ctx: MindContext,
|
|
599
|
+
a: number,
|
|
600
|
+
b: number,
|
|
601
|
+
): boolean {
|
|
602
|
+
const bound = hubBound(ctx);
|
|
603
|
+
if (ctx.store.hasNext(a) && ctx.store.nextFirst(a, bound).includes(b)) {
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
return ctx.store.hasNext(b) && ctx.store.nextFirst(b, bound).includes(a);
|
|
607
|
+
}
|
|
608
|
+
|
|
561
609
|
// ── Edge disambiguation (Section 6) ──────────────────────────────────────
|
|
562
610
|
|
|
563
611
|
/** The best-scoring item by cosine against `query`, among items scoring at
|
package/src/store-sqlite.ts
CHANGED
|
@@ -146,6 +146,20 @@ CREATE TABLE IF NOT EXISTS canon (
|
|
|
146
146
|
id INTEGER NOT NULL,
|
|
147
147
|
PRIMARY KEY (h, id)
|
|
148
148
|
) WITHOUT ROWID;
|
|
149
|
+
-- CONSTITUENT SKETCH (Store.sketchGet/sketchPut): the bottom-k minimal
|
|
150
|
+
-- constituents of a node's subtree, k = √D, chosen by identity hash. The blob is
|
|
151
|
+
-- a packed int32 little-endian run, already in hash order; an EMPTY blob is a
|
|
152
|
+
-- real answer (a minimal unit has no constituents) and a MISSING ROW means
|
|
153
|
+
-- "not yet computed" — the two must stay distinguishable, which is why absence
|
|
154
|
+
-- is a missing row rather than an empty blob sentinel. (node:sqlite binds a
|
|
155
|
+
-- zero-length Uint8Array as NULL, so the column is nullable and a NULL blob
|
|
156
|
+
-- reads back as the empty sketch — the ROW is what records "computed".)
|
|
157
|
+
-- Measured on the trained
|
|
158
|
+
-- store: 80.8% of nodes sketch EMPTY, mean 1.14 ids, ~72 MB over 15.7M nodes.
|
|
159
|
+
CREATE TABLE IF NOT EXISTS sketch (
|
|
160
|
+
id INTEGER PRIMARY KEY,
|
|
161
|
+
ids BLOB
|
|
162
|
+
);
|
|
149
163
|
CREATE TABLE IF NOT EXISTS snapshot (
|
|
150
164
|
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
151
165
|
data BLOB NOT NULL
|
|
@@ -256,6 +270,8 @@ export class SQliteStore extends AbstractStore implements Store {
|
|
|
256
270
|
private _insCanon: any = null;
|
|
257
271
|
private _selCanon: any = null;
|
|
258
272
|
private _cntCanon: any = null;
|
|
273
|
+
private _insSketch: any = null;
|
|
274
|
+
private _selSketch: any = null;
|
|
259
275
|
private _selContentFrom: any = null;
|
|
260
276
|
private _delMeta: any = null;
|
|
261
277
|
private _insSnapshot: any = null;
|
|
@@ -1029,6 +1045,43 @@ export class SQliteStore extends AbstractStore implements Store {
|
|
|
1029
1045
|
return (this._selCanon.all(h) as Array<{ id: number }>).map((r) => r.id);
|
|
1030
1046
|
}
|
|
1031
1047
|
|
|
1048
|
+
// -- Constituent sketch (Store optional capability) --
|
|
1049
|
+
|
|
1050
|
+
sketchGet(id: number): number[] | null {
|
|
1051
|
+
if (!this._selSketch) {
|
|
1052
|
+
this._selSketch = this.sqlite!.prepare(
|
|
1053
|
+
"SELECT ids FROM sketch WHERE id = ?",
|
|
1054
|
+
);
|
|
1055
|
+
}
|
|
1056
|
+
const row = this._selSketch.get(id) as
|
|
1057
|
+
| { ids: Uint8Array | null }
|
|
1058
|
+
| undefined;
|
|
1059
|
+
if (row === undefined) return null; // never computed — NOT the same as []
|
|
1060
|
+
const b = row.ids;
|
|
1061
|
+
if (b === null || b.byteLength === 0) return []; // computed, no constituents
|
|
1062
|
+
const out: number[] = [];
|
|
1063
|
+
const dv = new DataView(b.buffer, b.byteOffset, b.byteLength);
|
|
1064
|
+
for (let i = 0; i + 4 <= b.byteLength; i += 4) {
|
|
1065
|
+
out.push(dv.getInt32(i, true));
|
|
1066
|
+
}
|
|
1067
|
+
return out;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
sketchPut(id: number, ids: readonly number[]): void {
|
|
1071
|
+
if (!this._insSketch) {
|
|
1072
|
+
this._insSketch = this.sqlite!.prepare(
|
|
1073
|
+
"INSERT OR REPLACE INTO sketch (id, ids) VALUES (?, ?)",
|
|
1074
|
+
);
|
|
1075
|
+
}
|
|
1076
|
+
const b = new Uint8Array(ids.length * 4);
|
|
1077
|
+
const dv = new DataView(b.buffer);
|
|
1078
|
+
for (let i = 0; i < ids.length; i++) dv.setInt32(i * 4, ids[i], true);
|
|
1079
|
+
// Join the deferred write transaction (committed by flush/commit), like
|
|
1080
|
+
// canonAdd — a training run writes these in bulk.
|
|
1081
|
+
this._dbBeginTx();
|
|
1082
|
+
this._insSketch.run(id, b);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1032
1085
|
canonCount(): number {
|
|
1033
1086
|
if (!this._cntCanon) {
|
|
1034
1087
|
this._cntCanon = this.sqlite!.prepare(
|
package/src/store.ts
CHANGED
|
@@ -524,6 +524,34 @@ export interface Store {
|
|
|
524
524
|
fromId?: NodeId,
|
|
525
525
|
): void;
|
|
526
526
|
|
|
527
|
+
// ── constituent sketch (optional capability) ───────────────────────────
|
|
528
|
+
// The bottom-k MINIMAL CONSTITUENTS of a node's subtree, k derived from the
|
|
529
|
+
// representation's own capacity (√D — see companyProfile in mind/learning.ts),
|
|
530
|
+
// selected by identity hash so the choice is a property of each constituent
|
|
531
|
+
// and never of where it sits in the fold.
|
|
532
|
+
//
|
|
533
|
+
// DURABLE DERIVED STATE, NOT A CACHE. §2.12 permits a cache to cost only
|
|
534
|
+
// speed; this decides which terms enter a halo — a learned relation — so an
|
|
535
|
+
// eviction would change the geometry rather than slow it down. It is
|
|
536
|
+
// therefore written like the canon index: computed once, kept, never
|
|
537
|
+
// budgeted. Soundness rests on the set being INTRINSIC — minimality,
|
|
538
|
+
// `len ≥ W` and non-domination are properties of the node's own subtree and
|
|
539
|
+
// do not move as the corpus grows. The one corpus-dependent reading, the
|
|
540
|
+
// hub exclusion, is deliberately NOT stored: it is applied by the caller at
|
|
541
|
+
// pour time over the ≤ k candidates, which is the drift companyProfile
|
|
542
|
+
// already documents as benign and one-directional.
|
|
543
|
+
//
|
|
544
|
+
// Backends that do not implement the pair leave both absent; companyProfile
|
|
545
|
+
// then recomputes the sketch per pour and simply loses the amortisation.
|
|
546
|
+
|
|
547
|
+
/** The stored sketch of `id`, or null when it has never been computed.
|
|
548
|
+
* An empty array is a REAL answer (a minimal unit has no constituents) and
|
|
549
|
+
* must be distinguished from null. */
|
|
550
|
+
sketchGet?(id: NodeId): NodeId[] | null;
|
|
551
|
+
/** Record `ids` as the sketch of `id`. Idempotent; ids are already sorted
|
|
552
|
+
* by the caller's identity hash. */
|
|
553
|
+
sketchPut?(id: NodeId, ids: readonly NodeId[]): void;
|
|
554
|
+
|
|
527
555
|
// ── lifecycle ──────────────────────────────────────────────────────────
|
|
528
556
|
size(): Promise<number>;
|
|
529
557
|
saveSnapshot(bytes: Uint8Array): Promise<void>;
|
|
@@ -521,8 +521,27 @@ test("D1 — site-aware climb finds diverse anchors for CAST weave", async () =>
|
|
|
521
521
|
await m.store.close();
|
|
522
522
|
});
|
|
523
523
|
|
|
524
|
+
// D2 asserted `provenance === "cast"` — a PROXY, and it hid the very thing this
|
|
525
|
+
// test is named for. The climb here elects between two anchors whose votes sit
|
|
526
|
+
// 0.54σ–1.04σ apart, i.e. inside the estimator's own resolution: `steel is hard
|
|
527
|
+
// so steel is strong` scores 0.897–1.013 depending on the seed while `water is
|
|
528
|
+
// frigid so water is freezing` holds ~0.983, so the TOP FLIPS on 6 of 24 seeds
|
|
529
|
+
// (measured; the SD tracks 1/√D and the flip rate collapses 19/60 → 12/60 →
|
|
530
|
+
// 2/60 as D goes 256 → 1024 → 4096). CAST voiced the runner-up regardless of
|
|
531
|
+
// which anchor the climb had committed, so the proxy stayed green while the
|
|
532
|
+
// climb was demonstrably seed-dependent. See `test/87-codominant-commitment`.
|
|
533
|
+
//
|
|
534
|
+
// The strong form asserts the property in the title: every seed must produce
|
|
535
|
+
// the SAME OUTCOME — same provenance and same bytes. Uniformity alone is not
|
|
536
|
+
// enough (all seeds could agree on a wrong answer), so correctness is asserted
|
|
537
|
+
// too: the property transfer must still land on "freezing" via CAST.
|
|
538
|
+
//
|
|
539
|
+
// The seed set deliberately includes 1, 8, 18, 20, 22 and 23 — the seeds whose
|
|
540
|
+
// noise puts the OTHER anchor on top. A seed set that never flips would not
|
|
541
|
+
// exercise the defect at all.
|
|
524
542
|
test("D2 — site-aware climb is seed-independent", async () => {
|
|
525
|
-
|
|
543
|
+
const outcomes = new Map();
|
|
544
|
+
for (const seed of [1, 7, 8, 18, 20, 22, 23, 42, 99]) {
|
|
526
545
|
const m = mk(seed);
|
|
527
546
|
await m.ingest([
|
|
528
547
|
["ice is cold so ice is brittle", "brittle"],
|
|
@@ -530,11 +549,29 @@ test("D2 — site-aware climb is seed-independent", async () => {
|
|
|
530
549
|
["water is frigid so water is freezing", "freezing"],
|
|
531
550
|
]);
|
|
532
551
|
const r = await m.respond("steel is frigid");
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
"cast",
|
|
536
|
-
`seed ${seed}: CAST must fire — got ${r.provenance}`,
|
|
537
|
-
);
|
|
552
|
+
const text = new TextDecoder().decode(r.bytes ?? new Uint8Array());
|
|
553
|
+
outcomes.set(seed, `${r.provenance}|${text}`);
|
|
538
554
|
await m.store.close();
|
|
539
555
|
}
|
|
556
|
+
const distinct = new Set(outcomes.values());
|
|
557
|
+
assert.equal(
|
|
558
|
+
distinct.size,
|
|
559
|
+
1,
|
|
560
|
+
`the outcome depends on the seed — ${
|
|
561
|
+
[...outcomes].map(([s, o]) => `seed ${s}: ${JSON.stringify(o)}`).join(
|
|
562
|
+
"; ",
|
|
563
|
+
)
|
|
564
|
+
}`,
|
|
565
|
+
);
|
|
566
|
+
const [only] = distinct;
|
|
567
|
+
assert.ok(
|
|
568
|
+
only.startsWith("cast|"),
|
|
569
|
+
`seed-independent, but not through CAST: ${JSON.stringify(only)}`,
|
|
570
|
+
);
|
|
571
|
+
assert.ok(
|
|
572
|
+
/freezing/i.test(only),
|
|
573
|
+
`seed-independent, but the property transfer was lost: ${
|
|
574
|
+
JSON.stringify(only)
|
|
575
|
+
}`,
|
|
576
|
+
);
|
|
540
577
|
});
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
// 76 — TYPE-LEVEL COMPANY (the halo pour's constituent profile).
|
|
2
|
+
//
|
|
3
|
+
// What is under test is ONE claim: two forms become distributional siblings
|
|
4
|
+
// when their partners are MADE OF a shared content unit, even though the
|
|
5
|
+
// partners share no node at the top of the fold. That is the difference
|
|
6
|
+
// between a halo keyed on a TOKEN ("occurred next to node #4711992") and one
|
|
7
|
+
// keyed on a TYPE ("occurred next to something containing 'Paris'").
|
|
8
|
+
//
|
|
9
|
+
// WHY THESE TESTS CANNOT PASS BY ACCIDENT. Every positive assertion is paired
|
|
10
|
+
// with a NEGATIVE CONTROL drawn from the same fixture, trained in the same
|
|
11
|
+
// store, of the same shape and comparable length — so a change that merely
|
|
12
|
+
// made all halos correlate (the null model collapsing) fails the control
|
|
13
|
+
// instead of passing the positive. Every bar is DERIVED from D, never tuned:
|
|
14
|
+
// `significanceBar` (3/√D) for "this is not chance" and `conceptThreshold` for
|
|
15
|
+
// "these are the same concept". And T1 computes, in-test, the fact that makes
|
|
16
|
+
// the whole file a test of the constituent descent rather than of halos in
|
|
17
|
+
// general: the two partners' depth-1 constituent sets are DISJOINT, so a
|
|
18
|
+
// profile reading only `rec.kids` has literally nothing in common to find.
|
|
19
|
+
//
|
|
20
|
+
// WHAT EACH TEST IS, STATED HONESTLY. T1 and T2 are the CAPABILITY tests:
|
|
21
|
+
// run against the previous depth-1 profile they fail, on the capability
|
|
22
|
+
// assertion itself and not on a precondition — measured -0.0086 against the
|
|
23
|
+
// 0.0938 bar, while the fixture's own preconditions still passed, so the
|
|
24
|
+
// failure is the missing capability and nothing else. T3, T4 and T5 pass
|
|
25
|
+
// under BOTH implementations by construction: they are not evidence for the
|
|
26
|
+
// capability, they are the invariants it must not buy itself with, and each
|
|
27
|
+
// one pins a regression this work actually hit — the null model collapsing
|
|
28
|
+
// when the descent superposed scaffolding, mass tracking constituents instead
|
|
29
|
+
// of episodes, and the profile being read from the deposit's id map instead
|
|
30
|
+
// of the store. Claiming all five as proof of the capability would be false;
|
|
31
|
+
// dropping the three would leave the two unfalsifiable.
|
|
32
|
+
//
|
|
33
|
+
// WHAT IS DELIBERATELY NOT TESTED HERE, AND WHY. A sixth test asserting that
|
|
34
|
+
// company GRADES with shared content (more shared units => more company) was
|
|
35
|
+
// written and removed: it is false as stated. Cosine normalizes by profile
|
|
36
|
+
// size, so a pair sharing 3 constituents out of a larger profile scores BELOW
|
|
37
|
+
// a pair sharing 1 out of a smaller one — measured at 0.085 against 0.111,
|
|
38
|
+
// consistently across all four seeds. The design's own `shared / (1 + k)`
|
|
39
|
+
// reading is size-RELATIVE, and asserting the absolute form encodes a law the
|
|
40
|
+
// system does not obey. Testing the relative form from outside would require
|
|
41
|
+
// re-deriving the profile's term-selection rules inside the test, which makes
|
|
42
|
+
// the test a mirror of the implementation and worthless as a check on it.
|
|
43
|
+
//
|
|
44
|
+
// Also untested, and a real limitation rather than an oversight: on a store
|
|
45
|
+
// this small the hub bound √N is large enough that frame scaffolding is not
|
|
46
|
+
// excluded, so short partners sharing only a frame do keep some company. That
|
|
47
|
+
// is the documented honest floor — a corpus that cannot yet say what
|
|
48
|
+
// discriminates — but it means these fixtures must share genuine CONTENT
|
|
49
|
+
// units, which T1 now asserts rather than assumes.
|
|
50
|
+
//
|
|
51
|
+
// Each test pins a DIFFERENT rule. Deleting any one of them lets a specific,
|
|
52
|
+
// named regression back in; none of them subsumes another.
|
|
53
|
+
|
|
54
|
+
import { test } from "node:test";
|
|
55
|
+
import assert from "node:assert/strict";
|
|
56
|
+
import {
|
|
57
|
+
conceptThreshold,
|
|
58
|
+
cosine,
|
|
59
|
+
Mind,
|
|
60
|
+
significanceBar,
|
|
61
|
+
} from "../dist/src/index.js";
|
|
62
|
+
|
|
63
|
+
const D = 1024;
|
|
64
|
+
const BAR = significanceBar(D); // 3/√D — above chance
|
|
65
|
+
const enc = new TextEncoder();
|
|
66
|
+
|
|
67
|
+
// Company signatures key on NODE ID, not on the alphabet, so a seeded
|
|
68
|
+
// keyring cannot be what makes these comparisons come out — but that is an
|
|
69
|
+
// argument, and the capability tests below check it instead, across seeds.
|
|
70
|
+
const SEEDS = [7, 1, 42, 99];
|
|
71
|
+
const newMind = (seed = 7) => new Mind({ seed, D });
|
|
72
|
+
const idOf = (m, s) => m.resolve(enc.encode(s));
|
|
73
|
+
const haloOf = (m, s) => {
|
|
74
|
+
const id = idOf(m, s);
|
|
75
|
+
return id === null || id === undefined ? null : m.store.halo(id);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/** The halo read must work at all before any comparison means anything. A
|
|
79
|
+
* missing halo silently makes every cosine below unreachable, and a broken
|
|
80
|
+
* one makes them meaningless — this is the control whose absence has voided
|
|
81
|
+
* whole investigations on this codebase. */
|
|
82
|
+
const assertHaloControl = (m, cues) => {
|
|
83
|
+
for (const c of cues) {
|
|
84
|
+
const h = haloOf(m, c);
|
|
85
|
+
assert.ok(h, `CONTROL: no halo poured for ${JSON.stringify(c)}`);
|
|
86
|
+
assert.ok(
|
|
87
|
+
Math.abs(cosine(h, h) - 1) < 1e-9,
|
|
88
|
+
`CONTROL: halo of ${JSON.stringify(c)} is not self-identical`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/** The continuation a cue was trained with, as a node id. */
|
|
94
|
+
const partnerOf = (m, cue) => m.store.next(idOf(m, cue))[0];
|
|
95
|
+
|
|
96
|
+
/** Depth-1 constituents — what a profile reading only `rec.kids` would see. */
|
|
97
|
+
const depth1 = (m, node) => new Set(m.store.get(node)?.kids ?? []);
|
|
98
|
+
|
|
99
|
+
/** Every constituent reachable below `node`, to a depth the fold cannot
|
|
100
|
+
* exceed for these fixtures — what the descent can see. */
|
|
101
|
+
const deepConstituents = (m, node, depth = 6, out = new Set()) => {
|
|
102
|
+
if (depth === 0) return out;
|
|
103
|
+
for (const k of m.store.get(node)?.kids ?? []) {
|
|
104
|
+
if (k < 0) continue;
|
|
105
|
+
out.add(k);
|
|
106
|
+
deepConstituents(m, k, depth - 1, out);
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const intersect = (a, b) => [...a].filter((x) => b.has(x));
|
|
112
|
+
|
|
113
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
114
|
+
// T1 — THE CAPABILITY, with its own impossibility proof for the old rule.
|
|
115
|
+
//
|
|
116
|
+
// Two sentences in different languages that both mention Paris. Content-
|
|
117
|
+
// defined cuts put the shared unit in DIFFERENT top-level chunks:
|
|
118
|
+
// "The Eiffel Tower is in Paris" -> "The Eiffel " · "Tower is in Paris"
|
|
119
|
+
// "Tour Eiffel dia any Paris" -> "Tour Eiffel " · "dia any Paris"
|
|
120
|
+
// so their depth-1 constituents are disjoint — asserted below, not assumed.
|
|
121
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
122
|
+
test("T1: partners sharing a unit BELOW the top of the fold keep company", async () => {
|
|
123
|
+
for (const seed of SEEDS) {
|
|
124
|
+
const m = newMind(seed);
|
|
125
|
+
await m.ingest([
|
|
126
|
+
["cue_en", "The Eiffel Tower is in Paris"],
|
|
127
|
+
["cue_mg", "Tour Eiffel dia any Paris"],
|
|
128
|
+
["cue_zz", "Bananas are grown in humid climates"],
|
|
129
|
+
]);
|
|
130
|
+
assertHaloControl(m, ["cue_en", "cue_mg", "cue_zz"]);
|
|
131
|
+
|
|
132
|
+
const pEn = partnerOf(m, "cue_en");
|
|
133
|
+
const pMg = partnerOf(m, "cue_mg");
|
|
134
|
+
|
|
135
|
+
// THE IMPOSSIBILITY PROOF. A profile built from `rec.kids` alone sees these
|
|
136
|
+
// sets and nothing else; they do not intersect, so no depth-1 rule — however
|
|
137
|
+
// weighted, however filtered — can make these two partners share a term.
|
|
138
|
+
// This test therefore measures the DESCENT, not halos in general.
|
|
139
|
+
assert.equal(
|
|
140
|
+
intersect(depth1(m, pEn), depth1(m, pMg)).length,
|
|
141
|
+
0,
|
|
142
|
+
"fixture no longer exercises the descent: the two partners now share a " +
|
|
143
|
+
"depth-1 constituent, so a depth-1 profile could pass T1 as well",
|
|
144
|
+
);
|
|
145
|
+
// And the units the descent is supposed to find must actually be there —
|
|
146
|
+
// AND be eligible to become profile terms. A fixture whose only shared
|
|
147
|
+
// constituents are sub-window shards or frame scaffolding measures frame
|
|
148
|
+
// similarity while reading like a content test; one was written during this
|
|
149
|
+
// work and passed for exactly that wrong reason. The shared unit must be
|
|
150
|
+
// at least the fold's own window wide.
|
|
151
|
+
const W = m.space.maxGroup;
|
|
152
|
+
const shared = intersect(
|
|
153
|
+
deepConstituents(m, pEn),
|
|
154
|
+
deepConstituents(m, pMg),
|
|
155
|
+
);
|
|
156
|
+
assert.ok(
|
|
157
|
+
shared.some((n) => m.store.contentLen(n, W) >= W),
|
|
158
|
+
`fixture is broken: the partners share no constituent of at least W=${W} ` +
|
|
159
|
+
`bytes, so nothing they share can enter a profile`,
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const related = cosine(haloOf(m, "cue_en"), haloOf(m, "cue_mg"));
|
|
163
|
+
const control = cosine(haloOf(m, "cue_en"), haloOf(m, "cue_zz"));
|
|
164
|
+
|
|
165
|
+
assert.ok(
|
|
166
|
+
related >= BAR,
|
|
167
|
+
`seed ${seed}: partners sharing a content unit must keep measurable ` +
|
|
168
|
+
`company: got ${related.toFixed(4)}, need >= ${
|
|
169
|
+
BAR.toFixed(4)
|
|
170
|
+
} (3/sqrt(D))`,
|
|
171
|
+
);
|
|
172
|
+
// The control is what makes the line above falsifiable: without it, a
|
|
173
|
+
// regression that made EVERY halo correlate would pass.
|
|
174
|
+
assert.ok(
|
|
175
|
+
control < BAR,
|
|
176
|
+
`seed ${seed}: partners sharing nothing must stay at chance: got ` +
|
|
177
|
+
`${control.toFixed(4)}, need < ${
|
|
178
|
+
BAR.toFixed(4)
|
|
179
|
+
} — null model collapsed`,
|
|
180
|
+
);
|
|
181
|
+
await m.store.close();
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
186
|
+
// T2 — ORDER INDEPENDENCE.
|
|
187
|
+
//
|
|
188
|
+
// The tempting stop rule ("descend while a constituent is corpus-unique, stop
|
|
189
|
+
// at the first one attested twice") passes T1 in exactly one training order
|
|
190
|
+
// and fails in the other: when the FIRST partner is poured its shared unit has
|
|
191
|
+
// fan-in 1, so the descent runs past it and only the second partner ever
|
|
192
|
+
// profiles it. Whether two forms become siblings must not depend on which was
|
|
193
|
+
// taught first.
|
|
194
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
195
|
+
test("T2: company does not depend on which partner was taught first", async () => {
|
|
196
|
+
const measure = async (first, second, seed) => {
|
|
197
|
+
const m = newMind(seed);
|
|
198
|
+
await m.ingest([
|
|
199
|
+
[first[0], first[1]],
|
|
200
|
+
[second[0], second[1]],
|
|
201
|
+
["cue_zz", "Bananas are grown in humid climates"],
|
|
202
|
+
]);
|
|
203
|
+
assertHaloControl(m, ["cue_en", "cue_mg", "cue_zz"]);
|
|
204
|
+
return {
|
|
205
|
+
related: cosine(haloOf(m, "cue_en"), haloOf(m, "cue_mg")),
|
|
206
|
+
control: cosine(haloOf(m, "cue_en"), haloOf(m, "cue_zz")),
|
|
207
|
+
};
|
|
208
|
+
};
|
|
209
|
+
const EN = ["cue_en", "The Eiffel Tower is in Paris"];
|
|
210
|
+
const MG = ["cue_mg", "Tour Eiffel dia any Paris"];
|
|
211
|
+
|
|
212
|
+
for (const seed of SEEDS) {
|
|
213
|
+
const forward = await measure(EN, MG, seed);
|
|
214
|
+
const reverse = await measure(MG, EN, seed);
|
|
215
|
+
|
|
216
|
+
for (const [name, r] of [["forward", forward], ["reverse", reverse]]) {
|
|
217
|
+
assert.ok(
|
|
218
|
+
r.related >= BAR,
|
|
219
|
+
`seed ${seed} ${name} order: shared-unit company must survive ` +
|
|
220
|
+
`training order — got ${r.related.toFixed(4)}, need >= ` +
|
|
221
|
+
`${BAR.toFixed(4)}`,
|
|
222
|
+
);
|
|
223
|
+
assert.ok(
|
|
224
|
+
r.control < BAR,
|
|
225
|
+
`seed ${seed} ${name} order: control must stay at chance, got ` +
|
|
226
|
+
`${r.control.toFixed(4)}`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
233
|
+
// T3 — THE NULL MODEL SURVIVES.
|
|
234
|
+
//
|
|
235
|
+
// The failure mode opposite to T1's: a descent that superposed everything it
|
|
236
|
+
// walked past would put terms shared by every deposit into every profile, and
|
|
237
|
+
// ALL halos would correlate. That regression passes T1 handsomely. Here a
|
|
238
|
+
// population of mutually unrelated partners must stay mutually at chance —
|
|
239
|
+
// and, because it is the same store, T1's positive is re-checked against this
|
|
240
|
+
// population's own noise level rather than against a bar alone.
|
|
241
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
242
|
+
test("T3: unrelated partners stay mutually at chance", async () => {
|
|
243
|
+
const m = newMind();
|
|
244
|
+
const FACTS = [
|
|
245
|
+
["c1", "Volcanoes erupt when magma reaches the surface"],
|
|
246
|
+
["c2", "The violin has four strings tuned in fifths"],
|
|
247
|
+
["c3", "Penguins are flightless birds of the southern seas"],
|
|
248
|
+
["c4", "Concrete gains strength for weeks after it is poured"],
|
|
249
|
+
["c5", "The abacus was used for arithmetic in many cultures"],
|
|
250
|
+
["c6", "Lightning heats the air it passes through"],
|
|
251
|
+
];
|
|
252
|
+
await m.ingest(FACTS);
|
|
253
|
+
assertHaloControl(m, FACTS.map((f) => f[0]));
|
|
254
|
+
|
|
255
|
+
let worst = -1, worstPair = "";
|
|
256
|
+
for (let i = 0; i < FACTS.length; i++) {
|
|
257
|
+
for (let j = i + 1; j < FACTS.length; j++) {
|
|
258
|
+
const c = cosine(haloOf(m, FACTS[i][0]), haloOf(m, FACTS[j][0]));
|
|
259
|
+
if (c > worst) {
|
|
260
|
+
worst = c;
|
|
261
|
+
worstPair = `${FACTS[i][0]}~${FACTS[j][0]}`;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
assert.ok(
|
|
266
|
+
worst < BAR,
|
|
267
|
+
`unrelated partners must not keep company: worst pair ${worstPair} at ` +
|
|
268
|
+
`${worst.toFixed(4)}, need < ${BAR.toFixed(4)}. A profile that ` +
|
|
269
|
+
`superposes scaffolding makes every halo correlate.`,
|
|
270
|
+
);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
274
|
+
// T4 — ONE EPISODE POURS ONE UNIT OF MASS.
|
|
275
|
+
//
|
|
276
|
+
// The profile is normalized precisely so that enriching it cannot inflate the
|
|
277
|
+
// evidence it represents: `haloMass` counts EPISODES, and every mass-based
|
|
278
|
+
// reading in the system (recall's corroboration counts, the disambiguation
|
|
279
|
+
// tiers) depends on that staying true. A profile that forgot to normalize
|
|
280
|
+
// would pass T1 and T3 and silently re-weight the whole distributional layer.
|
|
281
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
282
|
+
test("T4: enriching the profile does not inflate halo mass", async () => {
|
|
283
|
+
const m = newMind();
|
|
284
|
+
await m.ingest([
|
|
285
|
+
// A partner with MANY constituents, and one with very few — if mass
|
|
286
|
+
// tracked constituent count instead of episodes, these would differ.
|
|
287
|
+
["rich", "The quick brown fox jumps over the lazy dog beside the river"],
|
|
288
|
+
["lean", "Ice melts"],
|
|
289
|
+
]);
|
|
290
|
+
assertHaloControl(m, ["rich", "lean"]);
|
|
291
|
+
|
|
292
|
+
const massRich = m.store.haloMass(idOf(m, "rich"));
|
|
293
|
+
const massLean = m.store.haloMass(idOf(m, "lean"));
|
|
294
|
+
assert.equal(
|
|
295
|
+
massRich,
|
|
296
|
+
massLean,
|
|
297
|
+
`halo mass must count episodes, not constituents: a 59-byte partner ` +
|
|
298
|
+
`poured ${massRich} against a 9-byte partner's ${massLean}`,
|
|
299
|
+
);
|
|
300
|
+
assert.equal(massRich, 1, `one episode must pour exactly one unit of mass`);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
304
|
+
// T5 — THE PROFILE IS A FUNCTION OF THE STORE, NOT OF THE DEPOSIT.
|
|
305
|
+
//
|
|
306
|
+
// The constituents must be read from the STORE. Read instead from the
|
|
307
|
+
// depositing tree's id map — which holds only the nodes THIS deposit newly
|
|
308
|
+
// interned — and a partner met a SECOND time profiles differently from the
|
|
309
|
+
// first, because its subtrees are already stored and therefore absent from the
|
|
310
|
+
// map. The exact-partner case then falls from cosine 1 to 1/sqrt(1+k) and the
|
|
311
|
+
// geometry stops meaning anything. Two cues sharing the SAME partner are the
|
|
312
|
+
// direct probe: their halos must be identical, whatever else changed between
|
|
313
|
+
// the two deposits.
|
|
314
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
315
|
+
test("T5: the same partner profiles identically on every episode", async () => {
|
|
316
|
+
const m = newMind();
|
|
317
|
+
const PARTNER = "Paris is the capital city of France";
|
|
318
|
+
await m.ingest([
|
|
319
|
+
["first", PARTNER],
|
|
320
|
+
// An unrelated deposit in between, so the second pour happens against a
|
|
321
|
+
// store that has grown and a tree whose subtrees are all already interned.
|
|
322
|
+
["filler", "Sandstone forms from compressed grains"],
|
|
323
|
+
["second", PARTNER],
|
|
324
|
+
]);
|
|
325
|
+
assertHaloControl(m, ["first", "second"]);
|
|
326
|
+
|
|
327
|
+
const same = cosine(haloOf(m, "first"), haloOf(m, "second"));
|
|
328
|
+
assert.ok(
|
|
329
|
+
same > 1 - 1e-6,
|
|
330
|
+
`two cues sharing one partner must have identical halos: got ` +
|
|
331
|
+
`${same.toFixed(6)}. The profile is being read from the deposit's id ` +
|
|
332
|
+
`map rather than from the store.`,
|
|
333
|
+
);
|
|
334
|
+
// Falsifiability: identical halos must not be an artefact of ALL halos in
|
|
335
|
+
// this store being identical.
|
|
336
|
+
const different = cosine(haloOf(m, "first"), haloOf(m, "filler"));
|
|
337
|
+
assert.ok(
|
|
338
|
+
different < conceptThreshold(D),
|
|
339
|
+
`control: a different partner must not yield the same halo, got ` +
|
|
340
|
+
`${different.toFixed(4)}`,
|
|
341
|
+
);
|
|
342
|
+
});
|