@hviana/sema 0.7.3 → 0.7.6
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 +95 -843
- package/README.md +11 -11
- package/dist/src/mind/graph-search.d.ts +32 -22
- package/dist/src/mind/graph-search.js +97 -54
- package/dist/src/mind/mind.js +14 -0
- package/dist/src/store-sqlite.js +17 -0
- package/dist/src/store.d.ts +18 -0
- package/dist/src/store.js +10 -0
- package/docs/INDEX.md +71 -0
- package/docs/INVARIANTS.md +19 -0
- package/docs/architecture/bounded-reads.md +85 -0
- package/docs/architecture/caches.md +89 -0
- package/docs/architecture/commonality.md +45 -0
- package/docs/architecture/cost-model.md +71 -0
- package/docs/architecture/determinism.md +73 -0
- package/docs/architecture/exact-vs-approximate.md +47 -0
- package/docs/architecture/factored-machinery.md +28 -0
- package/docs/architecture/fold-contract.md +87 -0
- package/docs/architecture/halo-sketch.md +99 -0
- package/docs/architecture/match-project.md +62 -0
- package/docs/architecture/mechanism-market.md +95 -0
- package/docs/architecture/memoization.md +96 -0
- package/docs/architecture/meter.md +55 -0
- package/docs/architecture/saturation.md +92 -0
- package/docs/architecture/store.md +79 -0
- package/docs/architecture/thresholds.md +79 -0
- package/docs/failures/tempting-but-wrong.md +144 -0
- package/docs/harness/gates.md +56 -0
- package/docs/mechanisms/alu.md +75 -0
- package/docs/mechanisms/cast.md +75 -0
- package/docs/mechanisms/confluence.md +36 -0
- package/docs/mechanisms/cover.md +54 -0
- package/docs/mechanisms/extraction.md +53 -0
- package/docs/mechanisms/prefix-completion.md +54 -0
- package/docs/mechanisms/recall.md +69 -0
- package/docs/mechanisms/reference.md +58 -0
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/mind/graph-search.ts +101 -55
- package/src/mind/mind.ts +14 -0
- package/src/store-sqlite.ts +19 -0
- package/src/store.ts +22 -0
- package/test/89-completion-recursion.test.mjs +30 -10
- package/test/97-store-seed.test.mjs +105 -0
- package/test/98-completion-chaining.test.mjs +140 -0
- package/HOW_IT_WORKS.md +0 -5836
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Reference — Voicing a Slot with the Asker's Bytes
|
|
2
|
+
|
|
3
|
+
Reference voices a slot of a learned frame with the bytes the asker supplied in
|
|
4
|
+
that position — asserting _position_, not equivalence. The bytes are the
|
|
5
|
+
asker's, so voicing them cannot fabricate corpus knowledge; the fabricable claim
|
|
6
|
+
is the _relation_ about them, which the licence withholds.
|
|
7
|
+
|
|
8
|
+
## Matcher — `frameSlots` inventory
|
|
9
|
+
|
|
10
|
+
The shared matcher is `frameSlots` (`src/mind/match.ts`) via
|
|
11
|
+
`Precomputed.frames()`.
|
|
12
|
+
|
|
13
|
+
- Seeded at origin `(0,0)`, `alignAround` finds common runs (seed `W`) then
|
|
14
|
+
sweeps both directions; each gap is contracted by `contractGap` to its varying
|
|
15
|
+
core (shared prefix/suffix stripped) and tagged
|
|
16
|
+
`substitution | insertion | deletion`.
|
|
17
|
+
- `frameSlots` **reports, never judges**: every gap (any kind, any size), sorted
|
|
18
|
+
by `qs`, plus `covered` (shared bytes) and `matched` spans. No gate is applied
|
|
19
|
+
there.
|
|
20
|
+
|
|
21
|
+
## Gate — reference elects and licences; matcher does not
|
|
22
|
+
|
|
23
|
+
All voicing gates belong to the **consumer**
|
|
24
|
+
(`src/mind/mechanisms/reference.ts`), not the matcher. A shared layer that
|
|
25
|
+
refused on their behalf would be reference-shaped and hide most real pairings.
|
|
26
|
+
|
|
27
|
+
- **Election:** `electFrame` groups inventory by full slot signature
|
|
28
|
+
(`qs:qe,...`) and keeps the modal group — one frame, not one slot.
|
|
29
|
+
- **Carriage licence:** `carriesFillers` —
|
|
30
|
+
`substituteAll(contA, fillersA→fillersB) == contB` byte-exact, all slots
|
|
31
|
+
simultaneously (longest needle first). Constant continuations pass vacuously;
|
|
32
|
+
filler-dependent content is refused.
|
|
33
|
+
- **Four voicing gates** (in `voiceable` + caller):
|
|
34
|
+
1. frame `dominates` query (`covered > |query|/2`);
|
|
35
|
+
2. every slot reaches one window `W` on _both_ sides;
|
|
36
|
+
3. no insertion/deletion (substitutions only);
|
|
37
|
+
4. fillers pairwise distinct. Additional: referents pairwise distinct and no
|
|
38
|
+
slot inside `answeredSpans`.
|
|
39
|
+
|
|
40
|
+
Matched frame + every slot is `accounted`; `complete: true`.
|
|
41
|
+
|
|
42
|
+
## Cost
|
|
43
|
+
|
|
44
|
+
`moves = STEP·slots + STEP` (one binding per slot + one edge follow). Not
|
|
45
|
+
`CONCEPT` — byte identity, not halo. `scaffolding` is never reported; a referent
|
|
46
|
+
is explained, not carried for lack of explanation.
|
|
47
|
+
|
|
48
|
+
`floor` is `STEP+STEP`, investment-disciplined before touching `frames()`.
|
|
49
|
+
|
|
50
|
+
## Provenance
|
|
51
|
+
|
|
52
|
+
`provenance: "reference"` with trace steps `bindReferent` / `referenceLicence`.
|
|
53
|
+
|
|
54
|
+
## Pins
|
|
55
|
+
|
|
56
|
+
- `test/76-reference-binding` — inventory vs gate split, carried/absorbed/
|
|
57
|
+
refused, multi-slot licence, `complete` and answered-span guard.
|
|
58
|
+
- `test/76-type-level-company` — type-level halo company underpinning.
|
package/jsr.json
CHANGED
package/package.json
CHANGED
package/src/mind/graph-search.ts
CHANGED
|
@@ -412,7 +412,11 @@ export class GraphSearch {
|
|
|
412
412
|
// through (completion is cover, recursively — see {@link recompleteNode}).
|
|
413
413
|
this.recompleteOpen.clear();
|
|
414
414
|
this.recompleteMemo = new Map<number, Uint8Array | null>();
|
|
415
|
-
|
|
415
|
+
// The top cover's derivation sink is threaded into every nested completion
|
|
416
|
+
// so the recompositions a produced form needs are reported in the same
|
|
417
|
+
// trace instead of vanishing after the first layer.
|
|
418
|
+
this.derivationSink = onDerivation;
|
|
419
|
+
const solved = this.solve(
|
|
416
420
|
queryLen,
|
|
417
421
|
{
|
|
418
422
|
sites,
|
|
@@ -426,8 +430,17 @@ export class GraphSearch {
|
|
|
426
430
|
computedResults,
|
|
427
431
|
onDerivation,
|
|
428
432
|
);
|
|
433
|
+
// Deepening runs HERE, once, on the derivation the top cover CHOSE — never
|
|
434
|
+
// inside the nested solve a completion runs. Nesting the deepening is what
|
|
435
|
+
// made per-query cost track how densely the corpus interconnects the forms
|
|
436
|
+
// passed through: every re-cover fanned out into the whole corpus's
|
|
437
|
+
// continuations instead of following the chain the answer itself licensed.
|
|
438
|
+
// With deepening only at the top, `recompleteNode` walks the accepted chain
|
|
439
|
+
// one link at a time (its own memo and stack), so the work is the answer's.
|
|
440
|
+
return solved === null
|
|
441
|
+
? null
|
|
442
|
+
: { segs: this.deepen(solved.segs), cost: solved.cost };
|
|
429
443
|
}
|
|
430
|
-
|
|
431
444
|
/** Build the deduction system for one span and return its lightest cover's
|
|
432
445
|
* chosen spans — the SINGLE routine the query and every produced composite
|
|
433
446
|
* run through. `recognition` carries the span's recognised forms; the query
|
|
@@ -484,7 +497,7 @@ export class GraphSearch {
|
|
|
484
497
|
onDerivation(readDerivation(derivation, substitutions !== undefined));
|
|
485
498
|
}
|
|
486
499
|
return derivation
|
|
487
|
-
? { segs:
|
|
500
|
+
? { segs: readCover(derivation), cost: derivation.cost }
|
|
488
501
|
: null;
|
|
489
502
|
}
|
|
490
503
|
|
|
@@ -974,53 +987,49 @@ export class GraphSearch {
|
|
|
974
987
|
* and recompose into a deeper learnt form (→ FINAL). This is why a single
|
|
975
988
|
* edge-target needs no bespoke logic — it routes back through {@link solve}.
|
|
976
989
|
*
|
|
977
|
-
*
|
|
978
|
-
*
|
|
979
|
-
*
|
|
990
|
+
* The produced bytes are decomposed by the machinery that owns their shape:
|
|
991
|
+
* the span's LEAVES and SPLITS drive the split rule, which resolves each half
|
|
992
|
+
* through findLeaf — so a part that straddles a content-defined cut is still
|
|
993
|
+
* recovered even though the node's tree children need not align with the
|
|
994
|
+
* learnt parts (the fold cuts "p1 p2" as "p1 p"|"2"). Recognised SITES are
|
|
995
|
+
* filtered to the node's own kids: a produced form is completed out of what
|
|
996
|
+
* it was built from, never by re-recognising arbitrary forms inside it. That
|
|
997
|
+
* filter is load-bearing — a 37-byte dialogue sentence carries seven hub
|
|
998
|
+
* openers, and re-covering them chained through the corpus's whole
|
|
999
|
+
* continuation population: 2.5 GB and OOM for `respond("hi.")`.
|
|
980
1000
|
*
|
|
981
1001
|
* The recovered answer is accepted only when it MOVED and names a LEARNT node
|
|
982
1002
|
* ({@link resolve}) — the graph itself gates against re-expanding a contained
|
|
983
|
-
* form ("ice is cold" ⊅→ "ice is cold is cold").
|
|
984
|
-
*
|
|
985
|
-
*
|
|
986
|
-
* another re-cover (see the guard below), so one cover pays at most one
|
|
987
|
-
* nested {@link solve} per distinct produced node it actually reaches, and
|
|
988
|
-
* {@link recompleteMemo} collapses a repeat to nothing.
|
|
1003
|
+
* form ("ice is cold" ⊅→ "ice is cold is cold"). An ACCEPTED completion is
|
|
1004
|
+
* then re-covered in turn, so a chain runs as deep as the graph licenses; a
|
|
1005
|
+
* rejected one ends its branch, so no work is spent past it.
|
|
989
1006
|
*
|
|
990
|
-
*
|
|
991
|
-
*
|
|
992
|
-
*
|
|
993
|
-
*
|
|
994
|
-
*
|
|
995
|
-
*
|
|
1007
|
+
* Termination and cost are STRUCTURAL: {@link recompleteOpen} is the chain's
|
|
1008
|
+
* stack (membership is the cycle guard), {@link recompleteMemo} re-covers each
|
|
1009
|
+
* node at most once, only accepted completions recurse, and every level
|
|
1010
|
+
* deepens only the top derivation — so a cover pays for the chain it finds,
|
|
1011
|
+
* not for how densely the corpus interconnects the forms it passes through.
|
|
1012
|
+
* Guard: test/89-completion-recursion.test.mjs. */
|
|
996
1013
|
private recompleteNode(node: number): Uint8Array | null {
|
|
997
1014
|
if (!this.host.recogniseSpan) return null;
|
|
998
1015
|
const memo = this.recompleteMemo;
|
|
999
1016
|
if (memo.has(node)) return memo.get(node) ?? null;
|
|
1000
|
-
// ONE re-cover per produced node — never a re-cover inside a re-cover.
|
|
1001
|
-
//
|
|
1002
1017
|
// Re-covering is how a PRODUCED node's bytes enter the search at all: the
|
|
1003
|
-
// cover machinery otherwise only ever sees the QUERY's spans.
|
|
1004
|
-
//
|
|
1005
|
-
//
|
|
1006
|
-
//
|
|
1007
|
-
//
|
|
1008
|
-
//
|
|
1009
|
-
// bytes nothing else brings in) needs it.
|
|
1018
|
+
// cover machinery otherwise only ever sees the QUERY's spans. The recursion
|
|
1019
|
+
// is allowed to nest — a chain IS nested completions — but it is bounded so
|
|
1020
|
+
// the work stays the ANSWER's (AGENTS §2.8): the stack below is the cycle
|
|
1021
|
+
// guard, only ACCEPTED completions recurse, and the nested solve decomposes
|
|
1022
|
+
// the form by its own shape instead of re-recognising the corpus's hub forms
|
|
1023
|
+
// inside it.
|
|
1010
1024
|
//
|
|
1011
|
-
//
|
|
1012
|
-
//
|
|
1013
|
-
//
|
|
1014
|
-
//
|
|
1015
|
-
//
|
|
1016
|
-
//
|
|
1017
|
-
//
|
|
1018
|
-
|
|
1019
|
-
//
|
|
1020
|
-
// `recompleteOpen` is that stack, so a non-empty stack means we are already
|
|
1021
|
-
// inside one. This subsumes the old cycle guard: a node cannot recurse
|
|
1022
|
-
// back into itself when nothing recurses at all.
|
|
1023
|
-
if (this.recompleteOpen.size > 0) return null;
|
|
1025
|
+
// `recompleteOpen` IS the stack of the chain being built, so MEMBERSHIP is
|
|
1026
|
+
// the cycle guard: a node already open on this chain cannot re-enter it.
|
|
1027
|
+
// Testing the NODE — not the stack's size — is what lets a completion
|
|
1028
|
+
// recurse as deep as the graph licenses, exactly the intrinsic convergence
|
|
1029
|
+
// {@link solve}'s contract states. Work stays the answer's because the
|
|
1030
|
+
// recursion only ever advances through an ACCEPTED completion (below) and
|
|
1031
|
+
// {@link cover} deepens only the top derivation.
|
|
1032
|
+
if (this.recompleteOpen.has(node)) return null;
|
|
1024
1033
|
|
|
1025
1034
|
// A leaf or single-child node has no parts to recompose; skip before the
|
|
1026
1035
|
// costly recognition so a plain terminal answer pays nothing.
|
|
@@ -1033,20 +1042,51 @@ export class GraphSearch {
|
|
|
1033
1042
|
const bytes = this.store.bytesPrefix(node, ALL);
|
|
1034
1043
|
this.recompleteOpen.add(node);
|
|
1035
1044
|
try {
|
|
1036
|
-
// Completion is cover
|
|
1037
|
-
//
|
|
1038
|
-
//
|
|
1039
|
-
//
|
|
1045
|
+
// Completion is cover, but the produced form is decomposed by its own
|
|
1046
|
+
// shape. The LEAVES and SPLITS are kept whole, so the split rule still
|
|
1047
|
+
// recovers a part that straddles a content-defined cut (the fold cuts
|
|
1048
|
+
// "p1 p2" as "p1 p"|"2", and findLeaf still resolves p1 and p2). The
|
|
1049
|
+
// recognised SITES are filtered to the node's own kids, because
|
|
1050
|
+
// re-recognising arbitrary forms inside the bytes is what let a hub-heavy
|
|
1051
|
+
// utterance explode: a 37-byte dialogue sentence carries seven hub
|
|
1052
|
+
// openers, and re-covering them chained through the corpus's whole
|
|
1053
|
+
// continuation population (2.5 GB and OOM for `"hi."`). No
|
|
1054
|
+
// concepts/connectors either (those need the caller's async
|
|
1055
|
+
// pre-resolution) — the recursion follows edges and fusion, which is what
|
|
1056
|
+
// a deeper rewrite chain is made of.
|
|
1057
|
+
const rec = this.host.recogniseSpan(bytes);
|
|
1058
|
+
const kids = new Set(nrec.kids);
|
|
1040
1059
|
const solved = this.solve(
|
|
1041
1060
|
bytes.length,
|
|
1042
|
-
|
|
1061
|
+
{
|
|
1062
|
+
sites: rec.sites.filter((s) => kids.has(s.payload)),
|
|
1063
|
+
leaves: rec.leaves,
|
|
1064
|
+
splits: rec.splits,
|
|
1065
|
+
starts: rec.starts,
|
|
1066
|
+
},
|
|
1043
1067
|
new Map(),
|
|
1068
|
+
undefined,
|
|
1069
|
+
undefined,
|
|
1070
|
+
undefined,
|
|
1071
|
+
this.derivationSink,
|
|
1044
1072
|
);
|
|
1045
1073
|
const answer = solved && concatBytes(solved.segs.map((s) => s.bytes));
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1074
|
+
// ACCEPT, then CONTINUE THE CHAIN — but only along an accepted
|
|
1075
|
+
// completion. A re-cover whose result is not itself a learnt node (the
|
|
1076
|
+
// 70→374-byte concatenations that name nothing) ends its branch here
|
|
1077
|
+
// instead of recursing into work the answer never asked for, and an
|
|
1078
|
+
// accepted one names a node that is re-covered in turn. That is what
|
|
1079
|
+
// keeps a deep chain possible while the work stays proportional to the
|
|
1080
|
+
// chain rather than to the corpus's interconnections.
|
|
1081
|
+
const composed = answer !== null && !bytesEqual(answer, bytes)
|
|
1082
|
+
? this.host.resolve(answer)
|
|
1049
1083
|
: null;
|
|
1084
|
+
if (composed === null) {
|
|
1085
|
+
memo.set(node, null);
|
|
1086
|
+
return null;
|
|
1087
|
+
}
|
|
1088
|
+
const deeper = this.recompleteNode(composed);
|
|
1089
|
+
const out = deeper ?? answer!;
|
|
1050
1090
|
memo.set(node, out);
|
|
1051
1091
|
return out;
|
|
1052
1092
|
} finally {
|
|
@@ -1058,13 +1098,19 @@ export class GraphSearch {
|
|
|
1058
1098
|
* outs of a long query re-cover each distinct node at most once); reset at the
|
|
1059
1099
|
* top of {@link cover}. */
|
|
1060
1100
|
private recompleteMemo = new Map<number, Uint8Array | null>();
|
|
1061
|
-
/** The
|
|
1062
|
-
*
|
|
1063
|
-
*
|
|
1064
|
-
* is
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
*
|
|
1101
|
+
/** The derivation sink of the TOP cover, threaded into every nested
|
|
1102
|
+
* completion so a produced form's own recompositions are reported in the
|
|
1103
|
+
* same trace instead of vanishing after the first layer. Undefined when
|
|
1104
|
+
* nothing is inspecting, so an uninspected response pays nothing. */
|
|
1105
|
+
private derivationSink?: (steps: DerivationStep[]) => void;
|
|
1106
|
+
/** The chain of nodes currently being re-completed — the recursion STACK.
|
|
1107
|
+
* MEMBERSHIP is the cycle guard ({@link recompleteNode} refuses a node
|
|
1108
|
+
* already open on this chain), which is what lets a completion recurse as
|
|
1109
|
+
* deep as the graph licenses while the work stays the answer's: the
|
|
1110
|
+
* recursion only advances through an ACCEPTED completion, every produced
|
|
1111
|
+
* form is decomposed into its own kids, and the memo re-covers each node at
|
|
1112
|
+
* most once per cover. A Set, not a flag, because it states WHICH node is
|
|
1113
|
+
* open — the invariant a reader needs to check the guard. */
|
|
1068
1114
|
private recompleteOpen = new Set<number>();
|
|
1069
1115
|
|
|
1070
1116
|
/** out(i,j,bytes,…): index it for the binary rules, then offer splicing a
|
package/src/mind/mind.ts
CHANGED
|
@@ -398,10 +398,24 @@ export class Mind implements MindContext {
|
|
|
398
398
|
} = (optsOrCfg ?? {}) as MindOptions;
|
|
399
399
|
this._canonOpt = optsCanon ?? null;
|
|
400
400
|
this._profile = optsProfile === true;
|
|
401
|
+
// `explicitSeed` is read BEFORE resolveConfig folds the default in, so
|
|
402
|
+
// the store can be consulted only when the caller did not choose.
|
|
403
|
+
const explicitSeed = (rest as Partial<MindConfig>).seed;
|
|
401
404
|
this.cfg = resolveConfig(rest as Partial<MindConfig>);
|
|
402
405
|
this.store = optsStore ?? new SQliteStore({
|
|
403
406
|
maxGroup: this.cfg.geometry.maxGroup,
|
|
404
407
|
});
|
|
408
|
+
// THE ARTIFACT'S SEED GOVERNS. `train.seed` is recovered by the store at
|
|
409
|
+
// open, exactly like `train.D` and `geometry.maxGroup`. The seed feeds
|
|
410
|
+
// `makeKeyring`, `Space.rand` and the `Alphabet` below, so folding a
|
|
411
|
+
// query under config.ts's default (42) against a store trained with
|
|
412
|
+
// another seed (e.g. 7) lands in a DIFFERENT vector space than the one
|
|
413
|
+
// the artifact's nodes were folded into: recognition and resonance then
|
|
414
|
+
// read the wrong space and every answer degrades silently. An explicit
|
|
415
|
+
// caller seed still wins — this only replaces the unconfigured default.
|
|
416
|
+
if (explicitSeed === undefined && this.store.trainSeed !== null) {
|
|
417
|
+
this.cfg.seed = this.store.trainSeed;
|
|
418
|
+
}
|
|
405
419
|
userMechanisms = userMechs ?? [];
|
|
406
420
|
userFactories = userFacts ?? [];
|
|
407
421
|
}
|
package/src/store-sqlite.ts
CHANGED
|
@@ -401,6 +401,25 @@ export class SQliteStore extends AbstractStore implements Store {
|
|
|
401
401
|
}
|
|
402
402
|
}
|
|
403
403
|
|
|
404
|
+
// Recover the TRAINING seed exactly as D and maxGroup are recovered. The
|
|
405
|
+
// seed seeds the alphabet and the seat keyring (mind.ts), so a Mind that
|
|
406
|
+
// folds a query under any other seed lands in a different vector space
|
|
407
|
+
// than the one this artifact's nodes were folded into — recognition,
|
|
408
|
+
// resonance and every mechanism downstream then read the wrong space. The
|
|
409
|
+
// trainer persists `train.seed` and refuses to resume against a store
|
|
410
|
+
// trained with a different one (example/train_base/main.ts), so the value
|
|
411
|
+
// is authoritative for this artifact. Absent on a store that was never
|
|
412
|
+
// trained, where the caller's configured seed stands.
|
|
413
|
+
{
|
|
414
|
+
const row = this.sqlite.prepare(
|
|
415
|
+
"SELECT val FROM meta WHERE key = 'train.seed'",
|
|
416
|
+
).get() as { val: string } | undefined;
|
|
417
|
+
if (row) {
|
|
418
|
+
const s = Number(row.val);
|
|
419
|
+
if (Number.isInteger(s) && s >= 0) this._trainSeed = s;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
404
423
|
// Persist maxGroup to meta when opening a FRESH store (no rows yet) so
|
|
405
424
|
// indexSubtree always sees the training-time value even when the store is
|
|
406
425
|
// accessed without a Mind / full snapshot.
|
package/src/store.ts
CHANGED
|
@@ -285,6 +285,17 @@ export class BoundedMap<K, V> {
|
|
|
285
285
|
export interface Store {
|
|
286
286
|
readonly D: number;
|
|
287
287
|
|
|
288
|
+
/** The seed the artifact was TRAINED with, recovered from the store's own
|
|
289
|
+
* `train.seed` metadata, or null for a store that was never trained.
|
|
290
|
+
*
|
|
291
|
+
* This is not decoration: the seed feeds the alphabet and the seat keyring
|
|
292
|
+
* (see the Mind constructor), so folding a query under any other seed lands
|
|
293
|
+
* in a different vector space than the one the artifact's nodes were folded
|
|
294
|
+
* into. A Mind opening a trained store MUST adopt this seed unless the
|
|
295
|
+
* caller explicitly overrides it — the same discipline that recovers
|
|
296
|
+
* `train.D` and `geometry.maxGroup` from the metadata. */
|
|
297
|
+
readonly trainSeed: number | null;
|
|
298
|
+
|
|
288
299
|
/** The work accumulator for the inference call in flight, or null. The
|
|
289
300
|
* Mind attaches one per profiled response and detaches it after (see
|
|
290
301
|
* src/meter.ts). A store MUST only ever write to it — no read may reach
|
|
@@ -917,6 +928,10 @@ export abstract class AbstractStore implements Store {
|
|
|
917
928
|
|
|
918
929
|
protected _D: number;
|
|
919
930
|
protected _maxGroup: number;
|
|
931
|
+
/** `train.seed` recovered by the backend at open, or null when the store was
|
|
932
|
+
* never trained. A backend that omits it simply reports null, which leaves
|
|
933
|
+
* the caller's configured seed in force. */
|
|
934
|
+
protected _trainSeed: number | null = null;
|
|
920
935
|
protected readonly minHaloMass: number;
|
|
921
936
|
protected readonly efSearch: number;
|
|
922
937
|
protected readonly overfetch: number;
|
|
@@ -1091,6 +1106,13 @@ export abstract class AbstractStore implements Store {
|
|
|
1091
1106
|
return this._D;
|
|
1092
1107
|
}
|
|
1093
1108
|
|
|
1109
|
+
/** The seed the artifact was trained with, recovered from `train.seed` at
|
|
1110
|
+
* open. Null for a store that was never trained. See
|
|
1111
|
+
* {@link Store.trainSeed} for why this must govern inference. */
|
|
1112
|
+
get trainSeed(): number | null {
|
|
1113
|
+
return this._trainSeed;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1094
1116
|
/** Await the async initialisation performed by the concrete constructor. */
|
|
1095
1117
|
protected async _ensureReady(): Promise<void> {
|
|
1096
1118
|
if (!this._ready) throw new Error("Store: not open");
|
|
@@ -77,15 +77,35 @@ const mix = (x) => {
|
|
|
77
77
|
return (x ^ (x >>> 15)) >>> 0;
|
|
78
78
|
};
|
|
79
79
|
|
|
80
|
-
/** Four-word windows of the repo's own
|
|
81
|
-
*
|
|
82
|
-
*
|
|
80
|
+
/** Four-word windows of the repo's own prose, taken from the CODE's comments
|
|
81
|
+
* under src (TypeScript) and test (the suites) — never from documentation. A
|
|
82
|
+
* test that reads docs is coupled to every documentation edit: deleting one
|
|
83
|
+
* root file once took its corpus below the non-vacuity guard and broke every
|
|
84
|
+
* release. Code comments are prose too, and the code is always here.
|
|
85
|
+
*
|
|
86
|
+
* Comment markers, code fences, inline code and link targets are stripped so
|
|
87
|
+
* what is left is language, which is where the fragment overlap lives. */
|
|
83
88
|
function fragments() {
|
|
84
89
|
const out = [];
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
90
|
+
const files = [];
|
|
91
|
+
const walk = (dir, exts) => {
|
|
92
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
93
|
+
if (
|
|
94
|
+
e.name.startsWith(".") || e.name === "node_modules" || e.name === "dist"
|
|
95
|
+
) continue;
|
|
96
|
+
const p = join(dir, e.name);
|
|
97
|
+
if (e.isDirectory()) walk(p, exts);
|
|
98
|
+
else if (exts.some((x) => e.name.endsWith(x))) files.push(p);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
walk(join(REPO, "src"), [".ts"]);
|
|
102
|
+
walk(join(REPO, "test"), [".mjs"]);
|
|
103
|
+
for (const f of files.sort()) {
|
|
104
|
+
const text = readFileSync(f, "utf8");
|
|
105
|
+
let t = "";
|
|
106
|
+
for (const m of text.matchAll(/\/\*[\s\S]*?\*\/|\/\/[^\n]*/g)) {
|
|
107
|
+
t += " " + m[0];
|
|
108
|
+
}
|
|
89
109
|
t = t.toLowerCase().replace(/[^a-z ]+/g, " ").replace(/\s+/g, " ");
|
|
90
110
|
const w = t.split(" ").filter(Boolean);
|
|
91
111
|
for (let i = 0; i + 4 < w.length; i += 2) {
|
|
@@ -140,9 +160,9 @@ const SIZES = [750, 1000, 1500];
|
|
|
140
160
|
test("completion recursion: per-query work does not grow with the corpus", async () => {
|
|
141
161
|
assert.ok(
|
|
142
162
|
FRAG.length > 4000,
|
|
143
|
-
`only ${FRAG.length} prose fragments found in
|
|
144
|
-
`draws its corpus from
|
|
145
|
-
`it can no longer exercise the completion recursion at all`,
|
|
163
|
+
`only ${FRAG.length} prose fragments found in the source comments — this ` +
|
|
164
|
+
`test draws its corpus from src/**/*.ts and test/**/*.mjs; with the ` +
|
|
165
|
+
`comments gone it can no longer exercise the completion recursion at all`,
|
|
146
166
|
);
|
|
147
167
|
|
|
148
168
|
const searches = [], pops = [], answers = [], secs = [];
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// 97-store-seed.test.mjs — a trained store's OWN seed governs the Mind that
|
|
2
|
+
// opens it.
|
|
3
|
+
//
|
|
4
|
+
// `train.seed` is persisted by the trainer (example/train_base/main.ts) and the
|
|
5
|
+
// trainer refuses to resume a store under a different seed, so the value is
|
|
6
|
+
// authoritative for the artifact. The seed feeds `makeKeyring`, `Space.rand`
|
|
7
|
+
// and the `Alphabet` in the Mind constructor: folding a query under any other
|
|
8
|
+
// seed lands in a DIFFERENT vector space than the one the artifact's nodes were
|
|
9
|
+
// folded into, so recognition and resonance read the wrong space and every
|
|
10
|
+
// answer degrades silently.
|
|
11
|
+
//
|
|
12
|
+
// The store recovers `train.D` and `geometry.maxGroup` from its own metadata at
|
|
13
|
+
// open; `train.seed` must be recovered the same way, and a Mind that did not
|
|
14
|
+
// receive an explicit seed must adopt it. An explicit caller seed still wins.
|
|
15
|
+
|
|
16
|
+
import { test } from "node:test";
|
|
17
|
+
import assert from "node:assert/strict";
|
|
18
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
19
|
+
import { tmpdir } from "node:os";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { DEFAULT_CONFIG, Mind } from "../dist/src/index.js";
|
|
22
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
23
|
+
|
|
24
|
+
/** A store that was never trained carries no seed and leaves the caller's
|
|
25
|
+
* configured default in force. */
|
|
26
|
+
test("an untrained store reports no trainSeed and keeps the default seed", async () => {
|
|
27
|
+
const store = new SQliteStore({ path: ":memory:", D: 256 });
|
|
28
|
+
const mind = new Mind({ store });
|
|
29
|
+
assert.equal(store.trainSeed, null);
|
|
30
|
+
assert.equal(mind.cfg.seed, DEFAULT_CONFIG.seed);
|
|
31
|
+
await store.close();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/** The artifact's seed is recovered at open and adopted by a Mind that was not
|
|
35
|
+
* given one; an explicit seed still overrides it. */
|
|
36
|
+
test("a trained store's seed is recovered and adopted unless overridden", async () => {
|
|
37
|
+
const dir = mkdtempSync(join(tmpdir(), "sema-seed-"));
|
|
38
|
+
const stem = join(dir, "trained");
|
|
39
|
+
const TRAIN_SEED = 7;
|
|
40
|
+
|
|
41
|
+
// Build the artifact: ingest under an explicit seed, then persist the seed
|
|
42
|
+
// exactly as the trainer does.
|
|
43
|
+
{
|
|
44
|
+
const store = new SQliteStore({ path: stem, D: 256 });
|
|
45
|
+
const mind = new Mind({ seed: TRAIN_SEED, store });
|
|
46
|
+
await mind.ingest([["the sky is blue", "blue"]]);
|
|
47
|
+
await store.setMeta("train.seed", String(TRAIN_SEED));
|
|
48
|
+
store.commit();
|
|
49
|
+
await store.close();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Reopen WITHOUT a seed: the store's own seed must stand.
|
|
53
|
+
{
|
|
54
|
+
const store = new SQliteStore({ path: stem, D: 256 });
|
|
55
|
+
assert.equal(store.trainSeed, TRAIN_SEED);
|
|
56
|
+
const adopted = new Mind({ store });
|
|
57
|
+
assert.equal(adopted.cfg.seed, TRAIN_SEED);
|
|
58
|
+
await store.close();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Reopen WITH an explicit seed: the caller wins over the artifact.
|
|
62
|
+
{
|
|
63
|
+
const store = new SQliteStore({ path: stem, D: 256 });
|
|
64
|
+
const explicit = new Mind({ seed: 3, store });
|
|
65
|
+
assert.equal(explicit.cfg.seed, 3);
|
|
66
|
+
await store.close();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
rmSync(dir, { recursive: true, force: true });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
/** The adopted seed is the one the answer is computed under: a store ingested
|
|
73
|
+
* under seed 7 answers a query identically when reopened without a seed and
|
|
74
|
+
* when reopened with seed 7 passed explicitly. */
|
|
75
|
+
test("adopting the artifact seed reproduces the artifact's answer", async () => {
|
|
76
|
+
const dir = mkdtempSync(join(tmpdir(), "sema-seed-"));
|
|
77
|
+
const stem = join(dir, "trained");
|
|
78
|
+
const TRAIN_SEED = 7;
|
|
79
|
+
const QUESTION = "the sky is blue";
|
|
80
|
+
|
|
81
|
+
let artifactAnswer;
|
|
82
|
+
{
|
|
83
|
+
const store = new SQliteStore({ path: stem, D: 256 });
|
|
84
|
+
const mind = new Mind({ seed: TRAIN_SEED, store });
|
|
85
|
+
await mind.ingest([
|
|
86
|
+
["the sky is blue", "blue"],
|
|
87
|
+
["the grass is green", "green"],
|
|
88
|
+
]);
|
|
89
|
+
artifactAnswer = (await mind.respondText(QUESTION)).trim();
|
|
90
|
+
await store.setMeta("train.seed", String(TRAIN_SEED));
|
|
91
|
+
store.commit();
|
|
92
|
+
await store.close();
|
|
93
|
+
}
|
|
94
|
+
assert.equal(artifactAnswer, "blue");
|
|
95
|
+
|
|
96
|
+
{
|
|
97
|
+
const store = new SQliteStore({ path: stem, D: 256 });
|
|
98
|
+
const adopted = new Mind({ store });
|
|
99
|
+
assert.equal(adopted.cfg.seed, TRAIN_SEED);
|
|
100
|
+
assert.equal((await adopted.respondText(QUESTION)).trim(), artifactAnswer);
|
|
101
|
+
await store.close();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
rmSync(dir, { recursive: true, force: true });
|
|
105
|
+
});
|