@hviana/sema 0.7.6 → 0.7.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/mind/graph-search.d.ts +37 -8
- package/dist/src/mind/graph-search.js +92 -10
- package/dist/src/mind/mechanisms/alu.js +10 -8
- package/dist/src/mind/mind.js +3 -2
- package/dist/src/mind/pipeline-mechanism.d.ts +7 -1
- package/dist/src/mind/recognition.js +37 -1
- package/dist/src/mind/trace.js +1 -0
- package/docs/mechanisms/alu.md +8 -2
- package/docs/mechanisms/cast.md +5 -0
- package/docs/mechanisms/confluence.md +7 -0
- package/docs/mechanisms/cover.md +5 -2
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/mind/graph-search.ts +107 -9
- package/src/mind/mechanisms/alu.ts +10 -8
- package/src/mind/mind.ts +3 -2
- package/src/mind/pipeline-mechanism.ts +7 -1
- package/src/mind/recognition.ts +35 -1
- package/src/mind/trace.ts +2 -0
- package/test/46-recognise-multibyte-edge.test.mjs +30 -0
- package/test/97-store-seed.test.mjs +3 -2
- package/test/99-fact-join.test.mjs +99 -0
|
@@ -73,6 +73,14 @@ export type GItem = {
|
|
|
73
73
|
* derivation actually CHOSE. Part of {@link key}, because it decides
|
|
74
74
|
* whether the span's final bytes may still change. */
|
|
75
75
|
fix?: boolean;
|
|
76
|
+
/** Set on the out DERIVE-THROUGH produced: a produced fact's own contained
|
|
77
|
+
* entity (the subject the query never named) combined with the query's tail
|
|
78
|
+
* named a learned key, and that key's continuation is this out. Part of
|
|
79
|
+
* {@link key} so the derived reading is a distinct chart item from the plain
|
|
80
|
+
* concatenation of the same bytes. Named for what it does — derive THROUGH
|
|
81
|
+
* a produced fact — because `join` is already the confluence mechanism's
|
|
82
|
+
* `Provenance`, a different act at a different layer. */
|
|
83
|
+
throughFact?: boolean;
|
|
76
84
|
};
|
|
77
85
|
export declare const STEP = 1;
|
|
78
86
|
export declare const CONCEPT = 10;
|
|
@@ -136,7 +144,7 @@ export interface DerivationItem {
|
|
|
136
144
|
* {@link GraphSearch}'s rules fired, recovered from the rule's premise/
|
|
137
145
|
* conclusion shape (the rules carry no label, so this classifies by structure,
|
|
138
146
|
* the single place that maps rule geometry to a name). */
|
|
139
|
-
export type DerivationMove = "axiom" | "follow-edge" | "concept-hop" | "voice" | "ground" | "splice-connector" | "split" | "fuse" | "recompose" | "bridge" | "pool-vote" | "step";
|
|
147
|
+
export type DerivationMove = "axiom" | "follow-edge" | "concept-hop" | "voice" | "ground" | "splice-connector" | "split" | "fuse" | "recompose" | "derive-through" | "bridge" | "pool-vote" | "step";
|
|
140
148
|
/** The lightest-derivation search over the Sema graph. One instance binds the
|
|
141
149
|
* store, `maxGroup` (the fusible span ceiling), and the canonical
|
|
142
150
|
* {@link resolve} callback; {@link cover} then solves one query. */
|
|
@@ -180,11 +188,12 @@ export declare class GraphSearch {
|
|
|
180
188
|
* connector rule (see {@link outRules}), so the returned spans already carry
|
|
181
189
|
* it — there is no post-pass. */
|
|
182
190
|
cover(queryLen: number, sites: ReadonlyArray<Site>, conceptTarget: ReadonlyMap<number, number>, leaves: ReadonlyArray<Leaf>, splits: ReadonlySet<number>, starts: ReadonlySet<number>, substitutions?: ReadonlyMap<number, Uint8Array>, connectors?: ReadonlyMap<string, Uint8Array>, computedResults?: ReadonlyArray<ComputedResult>,
|
|
183
|
-
/** When given, receives
|
|
184
|
-
*
|
|
185
|
-
* cover
|
|
186
|
-
*
|
|
187
|
-
*
|
|
191
|
+
/** When given, receives each solved span's lightest derivation — the full
|
|
192
|
+
* adapted A*LD proof tree as classified {@link DerivationStep}s — for the
|
|
193
|
+
* TOP cover AND every nested completion the sink is threaded into (see
|
|
194
|
+
* {@link recompleteNode}), so a produced form's own recompositions reach
|
|
195
|
+
* the rationale instead of stopping at the first layer. Off by default, so
|
|
196
|
+
* the search pays nothing when no one inspects. */
|
|
188
197
|
onDerivation?: (steps: DerivationStep[]) => void): {
|
|
189
198
|
segs: Seg[];
|
|
190
199
|
cost: number;
|
|
@@ -312,10 +321,30 @@ export declare class GraphSearch {
|
|
|
312
321
|
* most once per cover. A Set, not a flag, because it states WHICH node is
|
|
313
322
|
* open — the invariant a reader needs to check the guard. */
|
|
314
323
|
private recompleteOpen;
|
|
324
|
+
/** DERIVE-THROUGH — a RULE this module's DeductionSystem was missing. The
|
|
325
|
+
* A*LD library is untouched: this is one more `premises → conclusion + cost`
|
|
326
|
+
* rule in the system {@link buildSearch} hands to {@link lightestDerivation},
|
|
327
|
+
* the same kind of rule as `fuse`/`recompose` — not an extension of
|
|
328
|
+
* `src/derive`, and not a grounding mechanism. It derives the answer THROUGH
|
|
329
|
+
* a produced fact, without the intermediate key being named in the query.
|
|
330
|
+
*
|
|
331
|
+
* A produced fact (`fact.node`) carries the subject the query reached but
|
|
332
|
+
* never wrote; the query's remaining tail names the relation to follow from
|
|
333
|
+
* it. The pair IS a learned key — `"<entity><tail>"` — so the rule asks the
|
|
334
|
+
* store for that key's continuation and, when it exists, concludes with the
|
|
335
|
+
* fact reached through it. On the ladder it is one STEP: a direct edge,
|
|
336
|
+
* exactly as following a literal continuation is. Deterministic and
|
|
337
|
+
* point-probed (`resolve` + `nextFirst`, no scan), so it adds no read that
|
|
338
|
+
* grows with the corpus. The move is visible in the rationale as its own act
|
|
339
|
+
* (`classifyMove` reports `derive-through`), distinct from the
|
|
340
|
+
* byte-concatenating `fuse`/`splice` — and named `derive-through` rather than
|
|
341
|
+
* `join` so it cannot be read as the confluence mechanism's `Provenance`. */
|
|
342
|
+
private deriveThrough;
|
|
315
343
|
/** out(i,j,bytes,…): index it for the binary rules, then offer splicing a
|
|
316
344
|
* learnt connector (the in-search bridge), splitting (at a sub-leaf form
|
|
317
|
-
* boundary), bridging (cover(i) ∧ this → cover(j)),
|
|
318
|
-
*
|
|
345
|
+
* boundary), bridging (cover(i) ∧ this → cover(j)), fusing with an adjacent
|
|
346
|
+
* finalised out, and — for a produced fact — JOINING the entity it contains
|
|
347
|
+
* with the query's tail ({@link deriveThrough}). */
|
|
319
348
|
private outRules;
|
|
320
349
|
/** Whether the query span [from, to) is wholly covered by RECOGNISED outs —
|
|
321
350
|
* the test that lets a connector jump across INTERIOR answers (an N-ary whole)
|
|
@@ -101,8 +101,11 @@ function classifyMove(premises, conclusion, articulating) {
|
|
|
101
101
|
return "step";
|
|
102
102
|
return articulating ? "voice" : "ground";
|
|
103
103
|
}
|
|
104
|
-
if (p.kind === "out" && conclusion.kind === "out")
|
|
105
|
-
|
|
104
|
+
if (p.kind === "out" && conclusion.kind === "out") {
|
|
105
|
+
// A JOIN derives through a produced fact's own contained subject; a plain
|
|
106
|
+
// single-premise out→out is the byte-level split.
|
|
107
|
+
return conclusion.throughFact ? "derive-through" : "split";
|
|
108
|
+
}
|
|
106
109
|
return "step";
|
|
107
110
|
}
|
|
108
111
|
if (premises.length === 2) {
|
|
@@ -219,11 +222,12 @@ export class GraphSearch {
|
|
|
219
222
|
* connector rule (see {@link outRules}), so the returned spans already carry
|
|
220
223
|
* it — there is no post-pass. */
|
|
221
224
|
cover(queryLen, sites, conceptTarget, leaves, splits, starts, substitutions, connectors, computedResults,
|
|
222
|
-
/** When given, receives
|
|
223
|
-
*
|
|
224
|
-
* cover
|
|
225
|
-
*
|
|
226
|
-
*
|
|
225
|
+
/** When given, receives each solved span's lightest derivation — the full
|
|
226
|
+
* adapted A*LD proof tree as classified {@link DerivationStep}s — for the
|
|
227
|
+
* TOP cover AND every nested completion the sink is threaded into (see
|
|
228
|
+
* {@link recompleteNode}), so a produced form's own recompositions reach
|
|
229
|
+
* the rationale instead of stopping at the first layer. Off by default, so
|
|
230
|
+
* the search pays nothing when no one inspects. */
|
|
227
231
|
onDerivation) {
|
|
228
232
|
// Top-level entry: reset the per-call recursion state, then run the one
|
|
229
233
|
// {@link solve} routine that both the query and any produced composite go
|
|
@@ -325,6 +329,13 @@ export class GraphSearch {
|
|
|
325
329
|
// states the formula itself instead of importing it.
|
|
326
330
|
const atomsAreHubs = Math.max(1, Math.ceil((this.store.edgeSourceCount() * W) / 256)) > this.hubBound();
|
|
327
331
|
const nodeBytes = (n) => this.store.bytesPrefix(n, ALL);
|
|
332
|
+
// The query's own bytes, tiled from its perceived leaves. A JOIN reads the
|
|
333
|
+
// tail a produced fact's contained entity has to combine with, and `buildSearch`
|
|
334
|
+
// otherwise only ever sees positions, never the bytes behind them.
|
|
335
|
+
const queryBytes = new Uint8Array(queryLen);
|
|
336
|
+
for (const lf of leaves) {
|
|
337
|
+
queryBytes.set(lf.bytes.subarray(0, lf.end - lf.start), lf.start);
|
|
338
|
+
}
|
|
328
339
|
// Content-addressed probes over the store's hash-cons maps — the same keys
|
|
329
340
|
// training filled. No byte-by-byte trie walk.
|
|
330
341
|
const findLeafU = (b) => this.store.findLeaf(b) ?? undefined;
|
|
@@ -357,7 +368,7 @@ export class GraphSearch {
|
|
|
357
368
|
if (it.kind === "form") {
|
|
358
369
|
return `f${it.i}.${it.j}.${it.node}.${it.via ? 1 : 0}.${it.rcmp ? 1 : 0}`;
|
|
359
370
|
}
|
|
360
|
-
return `o${it.i}.${it.j}.${it.cover ? 1 : 0}.${it.rec ? 1 : 0}.${it.fix ? 1 : 0}.${it.node ?? -1}.${latin1(it.bytes)}`;
|
|
371
|
+
return `o${it.i}.${it.j}.${it.cover ? 1 : 0}.${it.rec ? 1 : 0}.${it.fix ? 1 : 0}.${it.throughFact ? 1 : 0}.${it.node ?? -1}.${latin1(it.bytes)}`;
|
|
361
372
|
},
|
|
362
373
|
*axioms() {
|
|
363
374
|
yield { item: { kind: "cover", p: 0 }, cost: 0 };
|
|
@@ -465,6 +476,8 @@ export class GraphSearch {
|
|
|
465
476
|
findBranchU,
|
|
466
477
|
linksByLeft,
|
|
467
478
|
linksByRight,
|
|
479
|
+
queryBytes,
|
|
480
|
+
queryLen,
|
|
468
481
|
});
|
|
469
482
|
},
|
|
470
483
|
};
|
|
@@ -854,10 +867,67 @@ export class GraphSearch {
|
|
|
854
867
|
* most once per cover. A Set, not a flag, because it states WHICH node is
|
|
855
868
|
* open — the invariant a reader needs to check the guard. */
|
|
856
869
|
recompleteOpen = new Set();
|
|
870
|
+
/** DERIVE-THROUGH — a RULE this module's DeductionSystem was missing. The
|
|
871
|
+
* A*LD library is untouched: this is one more `premises → conclusion + cost`
|
|
872
|
+
* rule in the system {@link buildSearch} hands to {@link lightestDerivation},
|
|
873
|
+
* the same kind of rule as `fuse`/`recompose` — not an extension of
|
|
874
|
+
* `src/derive`, and not a grounding mechanism. It derives the answer THROUGH
|
|
875
|
+
* a produced fact, without the intermediate key being named in the query.
|
|
876
|
+
*
|
|
877
|
+
* A produced fact (`fact.node`) carries the subject the query reached but
|
|
878
|
+
* never wrote; the query's remaining tail names the relation to follow from
|
|
879
|
+
* it. The pair IS a learned key — `"<entity><tail>"` — so the rule asks the
|
|
880
|
+
* store for that key's continuation and, when it exists, concludes with the
|
|
881
|
+
* fact reached through it. On the ladder it is one STEP: a direct edge,
|
|
882
|
+
* exactly as following a literal continuation is. Deterministic and
|
|
883
|
+
* point-probed (`resolve` + `nextFirst`, no scan), so it adds no read that
|
|
884
|
+
* grows with the corpus. The move is visible in the rationale as its own act
|
|
885
|
+
* (`classifyMove` reports `derive-through`), distinct from the
|
|
886
|
+
* byte-concatenating `fuse`/`splice` — and named `derive-through` rather than
|
|
887
|
+
* `join` so it cannot be read as the confluence mechanism's `Provenance`. */
|
|
888
|
+
*deriveThrough(fact, queryBytes, queryLen) {
|
|
889
|
+
if (!this.host.recogniseSpan)
|
|
890
|
+
return;
|
|
891
|
+
const tail = queryBytes.subarray(fact.j, queryLen);
|
|
892
|
+
if (tail.length === 0)
|
|
893
|
+
return;
|
|
894
|
+
// The entity candidates are the forms the fact's own bytes CONTAIN — the
|
|
895
|
+
// same recogniser the query went through, so the evidence standard is the
|
|
896
|
+
// query's. A byte atom is never a subject; the fact's own node is the span
|
|
897
|
+
// itself, not an entity inside it.
|
|
898
|
+
for (const site of this.host.recogniseSpan(fact.bytes).sites) {
|
|
899
|
+
if (site.payload < 0 || site.payload === fact.node)
|
|
900
|
+
continue;
|
|
901
|
+
if (!this.store.hasNext(site.payload) &&
|
|
902
|
+
!this.store.hasHalo(site.payload))
|
|
903
|
+
continue;
|
|
904
|
+
const key = this.host.resolve(concat2(this.store.bytesPrefix(site.payload, ALL), tail));
|
|
905
|
+
if (key === null)
|
|
906
|
+
continue;
|
|
907
|
+
const nx = this.store.nextFirst(key, 1);
|
|
908
|
+
if (nx.length === 0)
|
|
909
|
+
continue;
|
|
910
|
+
yield {
|
|
911
|
+
premises: [fact],
|
|
912
|
+
conclusion: {
|
|
913
|
+
kind: "out",
|
|
914
|
+
i: fact.i,
|
|
915
|
+
j: queryLen,
|
|
916
|
+
bytes: this.store.bytesPrefix(nx[0], ALL),
|
|
917
|
+
cover: true,
|
|
918
|
+
rec: true,
|
|
919
|
+
node: nx[0],
|
|
920
|
+
throughFact: true,
|
|
921
|
+
},
|
|
922
|
+
cost: STEP,
|
|
923
|
+
};
|
|
924
|
+
}
|
|
925
|
+
}
|
|
857
926
|
/** out(i,j,bytes,…): index it for the binary rules, then offer splicing a
|
|
858
927
|
* learnt connector (the in-search bridge), splitting (at a sub-leaf form
|
|
859
|
-
* boundary), bridging (cover(i) ∧ this → cover(j)),
|
|
860
|
-
*
|
|
928
|
+
* boundary), bridging (cover(i) ∧ this → cover(j)), fusing with an adjacent
|
|
929
|
+
* finalised out, and — for a produced fact — JOINING the entity it contains
|
|
930
|
+
* with the query's tail ({@link deriveThrough}). */
|
|
861
931
|
*outRules(it, ctx) {
|
|
862
932
|
const { splits, coversDone, outsByStart, outsByEnd, coverableByStart } = ctx;
|
|
863
933
|
const outsByNode = ctx.outsByNode;
|
|
@@ -950,6 +1020,18 @@ export class GraphSearch {
|
|
|
950
1020
|
yield* this.fuse(it, r, ctx);
|
|
951
1021
|
for (const l of outsByEnd.get(it.i) ?? [])
|
|
952
1022
|
yield* this.fuse(l, it, ctx);
|
|
1023
|
+
// ── DERIVE-THROUGH (one more rule of the DeductionSystem this module builds)
|
|
1024
|
+
// A produced fact may CONTAIN the subject the query never named; the query's
|
|
1025
|
+
// remaining tail then names the relation to follow FROM that subject. The
|
|
1026
|
+
// pair (contained entity, tail) is itself a learned key, and its
|
|
1027
|
+
// continuation is the derived answer — a genuine relational join, distinct
|
|
1028
|
+
// from the confluence mechanism's `join` PROVENANCE — and not the
|
|
1029
|
+
// juxtaposition the cover produces when the intermediate key IS named.
|
|
1030
|
+
// Fired per finalized out with a node, so it is the search's own rule, on
|
|
1031
|
+
// the ladder, memoised by {@link key}, and bounded by the fact's own length.
|
|
1032
|
+
if (it.node !== undefined) {
|
|
1033
|
+
yield* this.deriveThrough(it, ctx.queryBytes, ctx.queryLen);
|
|
1034
|
+
}
|
|
953
1035
|
}
|
|
954
1036
|
/** Whether the query span [from, to) is wholly covered by RECOGNISED outs —
|
|
955
1037
|
* the test that lets a connector jump across INTERIOR answers (an N-ary whole)
|
|
@@ -12,14 +12,16 @@ import { unexplainedLabel } from "../rationale.js";
|
|
|
12
12
|
export function aluToMechanism(alu) {
|
|
13
13
|
return {
|
|
14
14
|
name: "alu",
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
15
|
+
// The computation is GROUNDED BY COVER: this adapter's `parse` puts the
|
|
16
|
+
// authoritative span into `pre.computed`, cover masks it, and cover's
|
|
17
|
+
// derivation is what carries the answer out — measured, every computed
|
|
18
|
+
// probe reports provenance `cover`. So the adapter declares the core
|
|
19
|
+
// provenance the answer actually has. The ALU's own act is named where it
|
|
20
|
+
// belongs, in the TRACE (`evalComputation`, emitted by its `parse`), not in
|
|
21
|
+
// the provenance: a mechanism may not invent a label outside the pipeline's
|
|
22
|
+
// `Provenance` vocabulary, because post-grounding gates on that vocabulary
|
|
23
|
+
// (see the `provenance` contract in pipeline-mechanism.ts).
|
|
24
|
+
provenance: "cover",
|
|
23
25
|
parse: (query) => alu.parse(query),
|
|
24
26
|
async floor(_ctx, _query, pre, _worthRunning) {
|
|
25
27
|
return pre.computed.length > 0 ? 0 : null;
|
package/dist/src/mind/mind.js
CHANGED
|
@@ -155,8 +155,9 @@ export class Mind {
|
|
|
155
155
|
// `makeKeyring`, `Space.rand` and the `Alphabet` below, so folding a
|
|
156
156
|
// query under config.ts's default (42) against a store trained with
|
|
157
157
|
// another seed (e.g. 7) lands in a DIFFERENT vector space than the one
|
|
158
|
-
// the artifact's nodes were folded into: recognition and resonance
|
|
159
|
-
//
|
|
158
|
+
// the artifact's nodes were folded into: recognition and resonance read
|
|
159
|
+
// the wrong space, and answers silently diverge — pinned by test/97,
|
|
160
|
+
// where only adoption reproduces the artifact's own answer. An explicit
|
|
160
161
|
// caller seed still wins — this only replaces the unconfigured default.
|
|
161
162
|
if (explicitSeed === undefined && this.store.trainSeed !== null) {
|
|
162
163
|
this.cfg.seed = this.store.trainSeed;
|
|
@@ -185,7 +185,13 @@ export interface MechanismResult {
|
|
|
185
185
|
export interface PipelineMechanism {
|
|
186
186
|
/** Stable identifier for trace/debug. */
|
|
187
187
|
readonly name: string;
|
|
188
|
-
/** Which provenance tag the pipeline attaches to this mechanism's answers.
|
|
188
|
+
/** Which provenance tag the pipeline attaches to this mechanism's answers.
|
|
189
|
+
* DECLARED, not free-form: the pipeline narrows it to its own `Provenance`
|
|
190
|
+
* vocabulary (pipeline.ts), because post-grounding gates on that vocabulary
|
|
191
|
+
* — so an adapter declares one of those values, never a label of its own.
|
|
192
|
+
* The type is `string` only because a mechanism imports nothing from
|
|
193
|
+
* `pipeline.ts` (constraint 1, decoupling); the contract is semantic, and
|
|
194
|
+
* every shipped adapter honours it (see mechanisms/alu.ts). */
|
|
189
195
|
readonly provenance: string;
|
|
190
196
|
/** Parse authoritative spans BEFORE the grounding loop.
|
|
191
197
|
* Only needed by computational mechanisms (e.g. ALU). Results from ALL
|
|
@@ -501,7 +501,21 @@ function recogniseImpl(ctx, bytes) {
|
|
|
501
501
|
// embedded differently-cased form needed 64x the budget to be found,
|
|
502
502
|
// while the exact route it was competing with needed none of it.
|
|
503
503
|
const probe = (start, end, canonBudget) => {
|
|
504
|
-
|
|
504
|
+
// Any span at least one river window wide is worth a probe. This used
|
|
505
|
+
// to stop at `chainReach(W)` — "the chain already covers anything that
|
|
506
|
+
// short" — and that premise does not hold for every embedded form: the
|
|
507
|
+
// chain grows single-byte leaf ids and SKIPS a prefix whose
|
|
508
|
+
// `findBranch(ids)` is null, so a form the write side stored as a nested
|
|
509
|
+
// tree rather than as a flat leaf-id branch is never `resolveSpan`ned
|
|
510
|
+
// (measured: "Gustaf Molander" embedded in "The director of Eva is
|
|
511
|
+
// Gustaf Molander." gains no branch at any prefix), and its INTERIOR
|
|
512
|
+
// reach is one chunk plus W, which can be shorter than the form. The
|
|
513
|
+
// result was a dead zone: a form shorter than `chainReach` that neither
|
|
514
|
+
// starts on a fold cut nor ends on a node edge was unreachable by either
|
|
515
|
+
// tier — the exact site whose loss `tryChain`'s own note records as "the
|
|
516
|
+
// pivot dies with the site and multi-hop goes silent". The interior
|
|
517
|
+
// pass below spends the same budget on those pairs.
|
|
518
|
+
if (end - start < W)
|
|
505
519
|
return;
|
|
506
520
|
if (flatProbe(start, end) === null) {
|
|
507
521
|
if (!canonBudget)
|
|
@@ -565,6 +579,28 @@ function recogniseImpl(ctx, bytes) {
|
|
|
565
579
|
if (i < suffixes.length && !spend(suffixes[i], bytes.length))
|
|
566
580
|
break;
|
|
567
581
|
}
|
|
582
|
+
// INTERIOR pairs within the same `chainReach(W)` span bound the chain
|
|
583
|
+
// trusts — the dead zone the gate above used to leave: a form that neither
|
|
584
|
+
// starts on a fold cut nor ends on a node edge is exactly the one neither
|
|
585
|
+
// the chain (nested, `findBranch` misses) nor the two edge scans reach.
|
|
586
|
+
// Only pairs whose span lies in [W, chainReach(W)] are PROBED, which is
|
|
587
|
+
// the O(n · W²) work that matters; the enumeration itself is the all-pairs
|
|
588
|
+
// scan, each pair a constant-time span check — and the span bound is what
|
|
589
|
+
// keeps this off the quadratic path the budget note above describes (that
|
|
590
|
+
// one had no span bound at all).
|
|
591
|
+
{
|
|
592
|
+
const reach = chainReach(W);
|
|
593
|
+
for (const end of ordered) {
|
|
594
|
+
for (const start of ordered) {
|
|
595
|
+
if (start >= end)
|
|
596
|
+
continue;
|
|
597
|
+
const span = end - start;
|
|
598
|
+
if (span < W || span > reach)
|
|
599
|
+
continue;
|
|
600
|
+
spend(start, end);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
568
604
|
}
|
|
569
605
|
}
|
|
570
606
|
const chunkEnd = new Uint32Array(bytes.length);
|
package/dist/src/mind/trace.js
CHANGED
|
@@ -50,6 +50,7 @@ export const MOVE_NOTE = {
|
|
|
50
50
|
"split": "cut a span at a sub-leaf form boundary so a form can be reached",
|
|
51
51
|
"fuse": "fuse adjacent fragments toward a deeper learned form",
|
|
52
52
|
"recompose": "recompose fused parts into a learned whole that leads on",
|
|
53
|
+
"derive-through": "derive the answer through a produced fact's own subject and the query's tail, not alongside it",
|
|
53
54
|
"bridge": "advance the cover frontier across this span",
|
|
54
55
|
"pool-vote": "pool independent regions' evidence for a shared anchor (sum, not shortest path)",
|
|
55
56
|
"axiom": "a seed: a perceived leaf, recognised form, or computed result",
|
package/docs/mechanisms/alu.md
CHANGED
|
@@ -66,8 +66,14 @@ forms need no host; meaning-based paths do.
|
|
|
66
66
|
|
|
67
67
|
## Provenance
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
`cover`. A computation is grounded by cover: this adapter's `parse` puts the
|
|
70
|
+
authoritative span into `pre.computed`, cover masks it, and cover's derivation
|
|
71
|
+
carries the answer out (measured: 9 of 9 computed probes report `cover` —
|
|
72
|
+
`137*24`, `1000 - 421`, `15 * 7`, `3+3`, `5*5`). The adapter therefore declares
|
|
73
|
+
`cover` — a mechanism may not invent a label outside the pipeline's `Provenance`
|
|
74
|
+
vocabulary, because post-grounding gates on it (see `pipeline-mechanism.ts`).
|
|
75
|
+
The ALU's own act is named in the TRACE (`evalComputation`/`computeExtensions`),
|
|
76
|
+
not in the provenance.
|
|
71
77
|
|
|
72
78
|
## Pins
|
|
73
79
|
|
package/docs/mechanisms/cast.md
CHANGED
|
@@ -58,6 +58,11 @@ Floor is `2·STEP`. Before touching the shared expensive analyses
|
|
|
58
58
|
return the uninvested bound when it already loses. Never compute a shared
|
|
59
59
|
analysis just to discard it.
|
|
60
60
|
|
|
61
|
+
## Provenance
|
|
62
|
+
|
|
63
|
+
`cast` — the answer came from counterfactual transfer (substitution,
|
|
64
|
+
redirection, or analogical comparison), not from a literal continuation.
|
|
65
|
+
|
|
61
66
|
## Pins
|
|
62
67
|
|
|
63
68
|
- **test/17 intelligence** — reordered single-fact must not trigger
|
|
@@ -30,6 +30,13 @@ One currency (`mind/graph-search.ts`): `STEP=1`, `CONCEPT=10`, `PASS=1000`/byte.
|
|
|
30
30
|
`moves = STEP·slots + CONCEPT` (floor `3·STEP`: two constraints + meet). Weight
|
|
31
31
|
`moves + PASS·unaccounted` compared at `STEP` grade (`pipeline.ts:think`).
|
|
32
32
|
|
|
33
|
+
## Provenance
|
|
34
|
+
|
|
35
|
+
`join` — confluence is the mechanism that OWNS this provenance
|
|
36
|
+
(`src/mind/mechanisms/confluence.ts` sets it when the independent evidence
|
|
37
|
+
streams meet at one anchor). It is not cover's: cover reports `cover` for every
|
|
38
|
+
derivation it wins (see `docs/mechanisms/cover.md`).
|
|
39
|
+
|
|
33
40
|
## Pins
|
|
34
41
|
|
|
35
42
|
`test/32-confluence.test.mjs` — two-constraint intersection, order invariance,
|
package/docs/mechanisms/cover.md
CHANGED
|
@@ -45,8 +45,11 @@ by node pair. Bridges (`bridge`) splice connectors between rewrites.
|
|
|
45
45
|
|
|
46
46
|
## Provenance
|
|
47
47
|
|
|
48
|
-
`cover` for
|
|
49
|
-
learned form.
|
|
48
|
+
`cover` for every cover derivation — including the fusion/recomposition steps
|
|
49
|
+
(`fuse`/`recompose`) that name a deeper learned form. (`join` is NOT cover's:
|
|
50
|
+
that provenance belongs to the CONFLUENCE mechanism, which reports it when
|
|
51
|
+
independent evidence streams meet at one anchor — see
|
|
52
|
+
`docs/mechanisms/confluence.md`.)
|
|
50
53
|
|
|
51
54
|
## Pins
|
|
52
55
|
|
package/jsr.json
CHANGED
package/package.json
CHANGED
package/src/mind/graph-search.ts
CHANGED
|
@@ -115,6 +115,14 @@ export type GItem =
|
|
|
115
115
|
* derivation actually CHOSE. Part of {@link key}, because it decides
|
|
116
116
|
* whether the span's final bytes may still change. */
|
|
117
117
|
fix?: boolean;
|
|
118
|
+
/** Set on the out DERIVE-THROUGH produced: a produced fact's own contained
|
|
119
|
+
* entity (the subject the query never named) combined with the query's tail
|
|
120
|
+
* named a learned key, and that key's continuation is this out. Part of
|
|
121
|
+
* {@link key} so the derived reading is a distinct chart item from the plain
|
|
122
|
+
* concatenation of the same bytes. Named for what it does — derive THROUGH
|
|
123
|
+
* a produced fact — because `join` is already the confluence mechanism's
|
|
124
|
+
* `Provenance`, a different act at a different layer. */
|
|
125
|
+
throughFact?: boolean;
|
|
118
126
|
};
|
|
119
127
|
type OutItem = Extract<GItem, { kind: "out" }>;
|
|
120
128
|
|
|
@@ -240,6 +248,7 @@ export type DerivationMove =
|
|
|
240
248
|
| "split" // out→out cut at a sub-leaf form boundary
|
|
241
249
|
| "fuse" // out+out→out: adjacent fragments recomposed toward a learned form
|
|
242
250
|
| "recompose" // out+out→form: a fused pair that names an edge-bearing node
|
|
251
|
+
| "derive-through" // out→out: a produced fact's own contained subject + the query's tail names a learned key (derive the answer through the fact)
|
|
243
252
|
| "bridge" // cover+out→cover: the cover frontier advanced across a span
|
|
244
253
|
| "pool-vote" // N premises→conclusion, evidence pooled (combine:"sum" — see derive)
|
|
245
254
|
| "step"; // any other single-premise move (fallback)
|
|
@@ -276,7 +285,11 @@ function classifyMove(
|
|
|
276
285
|
if (!conclusion.rec) return "step";
|
|
277
286
|
return articulating ? "voice" : "ground";
|
|
278
287
|
}
|
|
279
|
-
if (p.kind === "out" && conclusion.kind === "out")
|
|
288
|
+
if (p.kind === "out" && conclusion.kind === "out") {
|
|
289
|
+
// A JOIN derives through a produced fact's own contained subject; a plain
|
|
290
|
+
// single-premise out→out is the byte-level split.
|
|
291
|
+
return conclusion.throughFact ? "derive-through" : "split";
|
|
292
|
+
}
|
|
280
293
|
return "step";
|
|
281
294
|
}
|
|
282
295
|
if (premises.length === 2) {
|
|
@@ -400,11 +413,12 @@ export class GraphSearch {
|
|
|
400
413
|
substitutions?: ReadonlyMap<number, Uint8Array>,
|
|
401
414
|
connectors?: ReadonlyMap<string, Uint8Array>,
|
|
402
415
|
computedResults?: ReadonlyArray<ComputedResult>,
|
|
403
|
-
/** When given, receives
|
|
404
|
-
*
|
|
405
|
-
* cover
|
|
406
|
-
*
|
|
407
|
-
*
|
|
416
|
+
/** When given, receives each solved span's lightest derivation — the full
|
|
417
|
+
* adapted A*LD proof tree as classified {@link DerivationStep}s — for the
|
|
418
|
+
* TOP cover AND every nested completion the sink is threaded into (see
|
|
419
|
+
* {@link recompleteNode}), so a produced form's own recompositions reach
|
|
420
|
+
* the rationale instead of stopping at the first layer. Off by default, so
|
|
421
|
+
* the search pays nothing when no one inspects. */
|
|
408
422
|
onDerivation?: (steps: DerivationStep[]) => void,
|
|
409
423
|
): { segs: Seg[]; cost: number } | null {
|
|
410
424
|
// Top-level entry: reset the per-call recursion state, then run the one
|
|
@@ -551,6 +565,13 @@ export class GraphSearch {
|
|
|
551
565
|
Math.ceil((this.store.edgeSourceCount() * W) / 256),
|
|
552
566
|
) > this.hubBound();
|
|
553
567
|
const nodeBytes = (n: number) => this.store.bytesPrefix(n, ALL);
|
|
568
|
+
// The query's own bytes, tiled from its perceived leaves. A JOIN reads the
|
|
569
|
+
// tail a produced fact's contained entity has to combine with, and `buildSearch`
|
|
570
|
+
// otherwise only ever sees positions, never the bytes behind them.
|
|
571
|
+
const queryBytes = new Uint8Array(queryLen);
|
|
572
|
+
for (const lf of leaves) {
|
|
573
|
+
queryBytes.set(lf.bytes.subarray(0, lf.end - lf.start), lf.start);
|
|
574
|
+
}
|
|
554
575
|
// Content-addressed probes over the store's hash-cons maps — the same keys
|
|
555
576
|
// training filled. No byte-by-byte trie walk.
|
|
556
577
|
const findLeafU = (b: Uint8Array) => this.store.findLeaf(b) ?? undefined;
|
|
@@ -589,7 +610,7 @@ export class GraphSearch {
|
|
|
589
610
|
}
|
|
590
611
|
return `o${it.i}.${it.j}.${it.cover ? 1 : 0}.${it.rec ? 1 : 0}.${
|
|
591
612
|
it.fix ? 1 : 0
|
|
592
|
-
}.${it.node ?? -1}.${latin1(it.bytes)}`;
|
|
613
|
+
}.${it.throughFact ? 1 : 0}.${it.node ?? -1}.${latin1(it.bytes)}`;
|
|
593
614
|
},
|
|
594
615
|
*axioms() {
|
|
595
616
|
yield { item: { kind: "cover", p: 0 }, cost: 0 };
|
|
@@ -697,6 +718,8 @@ export class GraphSearch {
|
|
|
697
718
|
findBranchU,
|
|
698
719
|
linksByLeft,
|
|
699
720
|
linksByRight,
|
|
721
|
+
queryBytes,
|
|
722
|
+
queryLen,
|
|
700
723
|
});
|
|
701
724
|
},
|
|
702
725
|
};
|
|
@@ -1113,10 +1136,70 @@ export class GraphSearch {
|
|
|
1113
1136
|
* open — the invariant a reader needs to check the guard. */
|
|
1114
1137
|
private recompleteOpen = new Set<number>();
|
|
1115
1138
|
|
|
1139
|
+
/** DERIVE-THROUGH — a RULE this module's DeductionSystem was missing. The
|
|
1140
|
+
* A*LD library is untouched: this is one more `premises → conclusion + cost`
|
|
1141
|
+
* rule in the system {@link buildSearch} hands to {@link lightestDerivation},
|
|
1142
|
+
* the same kind of rule as `fuse`/`recompose` — not an extension of
|
|
1143
|
+
* `src/derive`, and not a grounding mechanism. It derives the answer THROUGH
|
|
1144
|
+
* a produced fact, without the intermediate key being named in the query.
|
|
1145
|
+
*
|
|
1146
|
+
* A produced fact (`fact.node`) carries the subject the query reached but
|
|
1147
|
+
* never wrote; the query's remaining tail names the relation to follow from
|
|
1148
|
+
* it. The pair IS a learned key — `"<entity><tail>"` — so the rule asks the
|
|
1149
|
+
* store for that key's continuation and, when it exists, concludes with the
|
|
1150
|
+
* fact reached through it. On the ladder it is one STEP: a direct edge,
|
|
1151
|
+
* exactly as following a literal continuation is. Deterministic and
|
|
1152
|
+
* point-probed (`resolve` + `nextFirst`, no scan), so it adds no read that
|
|
1153
|
+
* grows with the corpus. The move is visible in the rationale as its own act
|
|
1154
|
+
* (`classifyMove` reports `derive-through`), distinct from the
|
|
1155
|
+
* byte-concatenating `fuse`/`splice` — and named `derive-through` rather than
|
|
1156
|
+
* `join` so it cannot be read as the confluence mechanism's `Provenance`. */
|
|
1157
|
+
private *deriveThrough(
|
|
1158
|
+
fact: OutItem,
|
|
1159
|
+
queryBytes: Uint8Array,
|
|
1160
|
+
queryLen: number,
|
|
1161
|
+
): Iterable<Rule<GItem>> {
|
|
1162
|
+
if (!this.host.recogniseSpan) return;
|
|
1163
|
+
const tail = queryBytes.subarray(fact.j, queryLen);
|
|
1164
|
+
if (tail.length === 0) return;
|
|
1165
|
+
// The entity candidates are the forms the fact's own bytes CONTAIN — the
|
|
1166
|
+
// same recogniser the query went through, so the evidence standard is the
|
|
1167
|
+
// query's. A byte atom is never a subject; the fact's own node is the span
|
|
1168
|
+
// itself, not an entity inside it.
|
|
1169
|
+
for (const site of this.host.recogniseSpan(fact.bytes).sites) {
|
|
1170
|
+
if (site.payload < 0 || site.payload === fact.node) continue;
|
|
1171
|
+
if (
|
|
1172
|
+
!this.store.hasNext(site.payload) &&
|
|
1173
|
+
!this.store.hasHalo(site.payload)
|
|
1174
|
+
) continue;
|
|
1175
|
+
const key = this.host.resolve(
|
|
1176
|
+
concat2(this.store.bytesPrefix(site.payload, ALL), tail),
|
|
1177
|
+
);
|
|
1178
|
+
if (key === null) continue;
|
|
1179
|
+
const nx = this.store.nextFirst(key, 1);
|
|
1180
|
+
if (nx.length === 0) continue;
|
|
1181
|
+
yield {
|
|
1182
|
+
premises: [fact],
|
|
1183
|
+
conclusion: {
|
|
1184
|
+
kind: "out",
|
|
1185
|
+
i: fact.i,
|
|
1186
|
+
j: queryLen,
|
|
1187
|
+
bytes: this.store.bytesPrefix(nx[0], ALL),
|
|
1188
|
+
cover: true,
|
|
1189
|
+
rec: true,
|
|
1190
|
+
node: nx[0],
|
|
1191
|
+
throughFact: true,
|
|
1192
|
+
},
|
|
1193
|
+
cost: STEP,
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1116
1198
|
/** out(i,j,bytes,…): index it for the binary rules, then offer splicing a
|
|
1117
1199
|
* learnt connector (the in-search bridge), splitting (at a sub-leaf form
|
|
1118
|
-
* boundary), bridging (cover(i) ∧ this → cover(j)),
|
|
1119
|
-
*
|
|
1200
|
+
* boundary), bridging (cover(i) ∧ this → cover(j)), fusing with an adjacent
|
|
1201
|
+
* finalised out, and — for a produced fact — JOINING the entity it contains
|
|
1202
|
+
* with the query's tail ({@link deriveThrough}). */
|
|
1120
1203
|
private *outRules(
|
|
1121
1204
|
it: OutItem,
|
|
1122
1205
|
ctx: {
|
|
@@ -1133,6 +1216,8 @@ export class GraphSearch {
|
|
|
1133
1216
|
findBranchU: (k: number[]) => number | undefined;
|
|
1134
1217
|
linksByLeft?: ReadonlyMap<number, Array<[number, Uint8Array]>>;
|
|
1135
1218
|
linksByRight?: ReadonlyMap<number, Array<[number, Uint8Array]>>;
|
|
1219
|
+
queryBytes: Uint8Array;
|
|
1220
|
+
queryLen: number;
|
|
1136
1221
|
},
|
|
1137
1222
|
): Iterable<Rule<GItem>> {
|
|
1138
1223
|
const { splits, coversDone, outsByStart, outsByEnd, coverableByStart } =
|
|
@@ -1225,6 +1310,19 @@ export class GraphSearch {
|
|
|
1225
1310
|
|
|
1226
1311
|
for (const r of outsByStart.get(it.j) ?? []) yield* this.fuse(it, r, ctx);
|
|
1227
1312
|
for (const l of outsByEnd.get(it.i) ?? []) yield* this.fuse(l, it, ctx);
|
|
1313
|
+
|
|
1314
|
+
// ── DERIVE-THROUGH (one more rule of the DeductionSystem this module builds)
|
|
1315
|
+
// A produced fact may CONTAIN the subject the query never named; the query's
|
|
1316
|
+
// remaining tail then names the relation to follow FROM that subject. The
|
|
1317
|
+
// pair (contained entity, tail) is itself a learned key, and its
|
|
1318
|
+
// continuation is the derived answer — a genuine relational join, distinct
|
|
1319
|
+
// from the confluence mechanism's `join` PROVENANCE — and not the
|
|
1320
|
+
// juxtaposition the cover produces when the intermediate key IS named.
|
|
1321
|
+
// Fired per finalized out with a node, so it is the search's own rule, on
|
|
1322
|
+
// the ladder, memoised by {@link key}, and bounded by the fact's own length.
|
|
1323
|
+
if (it.node !== undefined) {
|
|
1324
|
+
yield* this.deriveThrough(it, ctx.queryBytes, ctx.queryLen);
|
|
1325
|
+
}
|
|
1228
1326
|
}
|
|
1229
1327
|
|
|
1230
1328
|
/** Whether the query span [from, to) is wholly covered by RECOGNISED outs —
|
|
@@ -16,14 +16,16 @@ import type { PipelineMechanism } from "../pipeline-mechanism.js";
|
|
|
16
16
|
export function aluToMechanism(alu: Alu): PipelineMechanism {
|
|
17
17
|
return {
|
|
18
18
|
name: "alu",
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
19
|
+
// The computation is GROUNDED BY COVER: this adapter's `parse` puts the
|
|
20
|
+
// authoritative span into `pre.computed`, cover masks it, and cover's
|
|
21
|
+
// derivation is what carries the answer out — measured, every computed
|
|
22
|
+
// probe reports provenance `cover`. So the adapter declares the core
|
|
23
|
+
// provenance the answer actually has. The ALU's own act is named where it
|
|
24
|
+
// belongs, in the TRACE (`evalComputation`, emitted by its `parse`), not in
|
|
25
|
+
// the provenance: a mechanism may not invent a label outside the pipeline's
|
|
26
|
+
// `Provenance` vocabulary, because post-grounding gates on that vocabulary
|
|
27
|
+
// (see the `provenance` contract in pipeline-mechanism.ts).
|
|
28
|
+
provenance: "cover",
|
|
27
29
|
parse: (query) => alu.parse(query),
|
|
28
30
|
async floor(_ctx, _query, pre, _worthRunning) {
|
|
29
31
|
return pre.computed.length > 0 ? 0 : null;
|
package/src/mind/mind.ts
CHANGED
|
@@ -410,8 +410,9 @@ export class Mind implements MindContext {
|
|
|
410
410
|
// `makeKeyring`, `Space.rand` and the `Alphabet` below, so folding a
|
|
411
411
|
// query under config.ts's default (42) against a store trained with
|
|
412
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
|
|
414
|
-
//
|
|
413
|
+
// the artifact's nodes were folded into: recognition and resonance read
|
|
414
|
+
// the wrong space, and answers silently diverge — pinned by test/97,
|
|
415
|
+
// where only adoption reproduces the artifact's own answer. An explicit
|
|
415
416
|
// caller seed still wins — this only replaces the unconfigured default.
|
|
416
417
|
if (explicitSeed === undefined && this.store.trainSeed !== null) {
|
|
417
418
|
this.cfg.seed = this.store.trainSeed;
|
|
@@ -712,7 +712,13 @@ export interface PipelineMechanism {
|
|
|
712
712
|
/** Stable identifier for trace/debug. */
|
|
713
713
|
readonly name: string;
|
|
714
714
|
|
|
715
|
-
/** Which provenance tag the pipeline attaches to this mechanism's answers.
|
|
715
|
+
/** Which provenance tag the pipeline attaches to this mechanism's answers.
|
|
716
|
+
* DECLARED, not free-form: the pipeline narrows it to its own `Provenance`
|
|
717
|
+
* vocabulary (pipeline.ts), because post-grounding gates on that vocabulary
|
|
718
|
+
* — so an adapter declares one of those values, never a label of its own.
|
|
719
|
+
* The type is `string` only because a mechanism imports nothing from
|
|
720
|
+
* `pipeline.ts` (constraint 1, decoupling); the contract is semantic, and
|
|
721
|
+
* every shipped adapter honours it (see mechanisms/alu.ts). */
|
|
716
722
|
readonly provenance: string;
|
|
717
723
|
|
|
718
724
|
/** Parse authoritative spans BEFORE the grounding loop.
|
package/src/mind/recognition.ts
CHANGED
|
@@ -518,7 +518,21 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
|
|
|
518
518
|
end: number,
|
|
519
519
|
canonBudget: boolean,
|
|
520
520
|
): void => {
|
|
521
|
-
|
|
521
|
+
// Any span at least one river window wide is worth a probe. This used
|
|
522
|
+
// to stop at `chainReach(W)` — "the chain already covers anything that
|
|
523
|
+
// short" — and that premise does not hold for every embedded form: the
|
|
524
|
+
// chain grows single-byte leaf ids and SKIPS a prefix whose
|
|
525
|
+
// `findBranch(ids)` is null, so a form the write side stored as a nested
|
|
526
|
+
// tree rather than as a flat leaf-id branch is never `resolveSpan`ned
|
|
527
|
+
// (measured: "Gustaf Molander" embedded in "The director of Eva is
|
|
528
|
+
// Gustaf Molander." gains no branch at any prefix), and its INTERIOR
|
|
529
|
+
// reach is one chunk plus W, which can be shorter than the form. The
|
|
530
|
+
// result was a dead zone: a form shorter than `chainReach` that neither
|
|
531
|
+
// starts on a fold cut nor ends on a node edge was unreachable by either
|
|
532
|
+
// tier — the exact site whose loss `tryChain`'s own note records as "the
|
|
533
|
+
// pivot dies with the site and multi-hop goes silent". The interior
|
|
534
|
+
// pass below spends the same budget on those pairs.
|
|
535
|
+
if (end - start < W) return;
|
|
522
536
|
if (flatProbe(start, end) === null) {
|
|
523
537
|
if (!canonBudget) return;
|
|
524
538
|
if (!canonAdmits(start, end)) return;
|
|
@@ -575,6 +589,26 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
|
|
|
575
589
|
if (i < prefixes.length && !spend(0, prefixes[i])) break;
|
|
576
590
|
if (i < suffixes.length && !spend(suffixes[i], bytes.length)) break;
|
|
577
591
|
}
|
|
592
|
+
// INTERIOR pairs within the same `chainReach(W)` span bound the chain
|
|
593
|
+
// trusts — the dead zone the gate above used to leave: a form that neither
|
|
594
|
+
// starts on a fold cut nor ends on a node edge is exactly the one neither
|
|
595
|
+
// the chain (nested, `findBranch` misses) nor the two edge scans reach.
|
|
596
|
+
// Only pairs whose span lies in [W, chainReach(W)] are PROBED, which is
|
|
597
|
+
// the O(n · W²) work that matters; the enumeration itself is the all-pairs
|
|
598
|
+
// scan, each pair a constant-time span check — and the span bound is what
|
|
599
|
+
// keeps this off the quadratic path the budget note above describes (that
|
|
600
|
+
// one had no span bound at all).
|
|
601
|
+
{
|
|
602
|
+
const reach = chainReach(W);
|
|
603
|
+
for (const end of ordered) {
|
|
604
|
+
for (const start of ordered) {
|
|
605
|
+
if (start >= end) continue;
|
|
606
|
+
const span = end - start;
|
|
607
|
+
if (span < W || span > reach) continue;
|
|
608
|
+
spend(start, end);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
578
612
|
}
|
|
579
613
|
}
|
|
580
614
|
|
package/src/mind/trace.ts
CHANGED
|
@@ -75,6 +75,8 @@ export const MOVE_NOTE: Record<string, string> = {
|
|
|
75
75
|
"split": "cut a span at a sub-leaf form boundary so a form can be reached",
|
|
76
76
|
"fuse": "fuse adjacent fragments toward a deeper learned form",
|
|
77
77
|
"recompose": "recompose fused parts into a learned whole that leads on",
|
|
78
|
+
"derive-through":
|
|
79
|
+
"derive the answer through a produced fact's own subject and the query's tail, not alongside it",
|
|
78
80
|
"bridge": "advance the cover frontier across this span",
|
|
79
81
|
"pool-vote":
|
|
80
82
|
"pool independent regions' evidence for a shared anchor (sum, not shortest path)",
|
|
@@ -83,3 +83,33 @@ test("recognise(): a wide edge trim does not corrupt an unrelated short-form ans
|
|
|
83
83
|
const r = await m.respond("2+2 は何ですか");
|
|
84
84
|
assert.equal(dec(r.bytes), "4");
|
|
85
85
|
});
|
|
86
|
+
|
|
87
|
+
test("recognise(): an INTERIOR form at a non-cut offset is recovered, not only an edge one", async () => {
|
|
88
|
+
// The edge tier used to probe only prefixes of 0 and suffixes to bytes.length,
|
|
89
|
+
// and the flat-leaf chain cannot rebuild a write-side-chunked form (its
|
|
90
|
+
// `findBranch(ids)` pre-check misses at every prefix, so `resolveSpan` is
|
|
91
|
+
// never reached) while its interior reach is one chunk plus W. A form that
|
|
92
|
+
// neither starts on a fold cut nor ends on a node edge therefore fell in a
|
|
93
|
+
// dead zone — exactly the object inside a produced fact ("…is Gustaf
|
|
94
|
+
// Molander."), which is the site the pivot would need to chain on.
|
|
95
|
+
// MEASURED on the pre-change tree: no site for the entity. With the bounded
|
|
96
|
+
// interior pass (spans W..chainReach(W), linear in the query), it is found at
|
|
97
|
+
// its true span.
|
|
98
|
+
const m = new Mind({ seed: 7, store: new SQliteStore({ path: ":memory:" }) });
|
|
99
|
+
await m.ingest([
|
|
100
|
+
["x", "The director of Eva is Gustaf Molander."],
|
|
101
|
+
["Gustaf Molander", "The father of Gustaf Molander is Harald Molander."],
|
|
102
|
+
]);
|
|
103
|
+
const expected = resolve(m, enc("Gustaf Molander"));
|
|
104
|
+
assert.ok(expected !== null, "sanity: the entity must resolve standalone");
|
|
105
|
+
|
|
106
|
+
const rec = recognise(m, enc("The director of Eva is Gustaf Molander."));
|
|
107
|
+
const hit = rec.sites.find((s) => s.payload === expected);
|
|
108
|
+
assert.ok(
|
|
109
|
+
hit,
|
|
110
|
+
`expected an interior site for the entity, got: ` +
|
|
111
|
+
JSON.stringify(rec.sites.map((s) => [s.start, s.end, s.payload])),
|
|
112
|
+
);
|
|
113
|
+
assert.deepEqual([hit.start, hit.end], [23, 38]);
|
|
114
|
+
await m.store.close();
|
|
115
|
+
});
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
// authoritative for the artifact. The seed feeds `makeKeyring`, `Space.rand`
|
|
7
7
|
// and the `Alphabet` in the Mind constructor: folding a query under any other
|
|
8
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
|
|
10
|
-
//
|
|
9
|
+
// folded into, so recognition and resonance read the wrong space and answers
|
|
10
|
+
// silently diverge — this file pins that only adoption reproduces the
|
|
11
|
+
// artifact's own answer.
|
|
11
12
|
//
|
|
12
13
|
// The store recovers `train.D` and `geometry.maxGroup` from its own metadata at
|
|
13
14
|
// open; `train.seed` must be recovered the same way, and a Mind that did not
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// 99-fact-join.test.mjs — DERIVE-THROUGH: derive the answer THROUGH a produced
|
|
2
|
+
// fact whose subject the query never names.
|
|
3
|
+
//
|
|
4
|
+
// THE MOVE THIS PINS. A query like "Eiffel Tower country capital" names hop 1's
|
|
5
|
+
// key ("Eiffel Tower country") and hop 2's relation (" capital"), but never the
|
|
6
|
+
// intermediate subject ("France"). The cover alone can only juxtapose the two
|
|
7
|
+
// facts; the pivot needs the subject to surface as an unconsumed site inside the
|
|
8
|
+
// produced fact. The `deriveThrough` rule in graph-search.ts closes the gap: it
|
|
9
|
+
// takes the entity the produced fact CONTAINS, combines it with the query's
|
|
10
|
+
// remaining tail, and asks the store for that key's continuation — a direct STEP
|
|
11
|
+
// on the ladder, reported as `derive-through` in the rationale. (The operation
|
|
12
|
+
// is a relational join; the MOVE is named `derive-through` so it cannot be read
|
|
13
|
+
// as the confluence mechanism's `join` PROVENANCE.)
|
|
14
|
+
//
|
|
15
|
+
// WHY THE FIXTURE CROSSES N=4096. Below the atomIsHub flip the interior subject
|
|
16
|
+
// surfaces anyway (small-store recognition) and the PIVOT alone reaches the
|
|
17
|
+
// chain, so a small fixture passes under the pre-change tree too — measured on a
|
|
18
|
+
// 3-deposit store, which answered "The father of Gustaf Molander is Harald
|
|
19
|
+
// Molander." with no derive-through move. That rule is what carries the chain at corpus scale,
|
|
20
|
+
// so the fixture must sit past the flip, exactly as test/78 does. The filler is
|
|
21
|
+
// lexically varied for the same reason test/78's is: a templated corpus folds to
|
|
22
|
+
// shared chunks and leaves the query uncontested.
|
|
23
|
+
|
|
24
|
+
import { test } from "node:test";
|
|
25
|
+
import assert from "node:assert/strict";
|
|
26
|
+
import { Mind } from "../dist/src/index.js";
|
|
27
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
28
|
+
|
|
29
|
+
const CHAIN = [
|
|
30
|
+
["Eiffel Tower country", "The country of Eiffel Tower is France."],
|
|
31
|
+
["France capital", "The capital of France is Paris."],
|
|
32
|
+
// The pivot fact: the intermediate subject as a context of its own.
|
|
33
|
+
["France", "The capital of France is Paris."],
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const WORDS =
|
|
37
|
+
("alpha bravo charlie delta echo foxtrot golf hotel india juliet " +
|
|
38
|
+
"kilo lima mike november oscar papa quebec romeo sierra tango uniform " +
|
|
39
|
+
"victor whiskey xray yankee zulu amber bronze copper dahlia ember fjord " +
|
|
40
|
+
"gossamer harbour indigo jasmine kestrel lantern marigold nectar opal " +
|
|
41
|
+
"pewter quartz ripple saffron thistle umber violet willow xenon yarrow")
|
|
42
|
+
.split(" ");
|
|
43
|
+
const filler = (i) => {
|
|
44
|
+
const w = (n) => WORDS[(i * 7 + n * 13) % WORDS.length];
|
|
45
|
+
return [
|
|
46
|
+
`${w(1)} ${w(2)} ${w(3)} ${i}`,
|
|
47
|
+
`${w(4)} ${w(5)} ${w(6)} ${w(7)} ${i}`,
|
|
48
|
+
];
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/** One store, ingested past the atomIsHub flip. */
|
|
52
|
+
async function pastTheFlip() {
|
|
53
|
+
const store = new SQliteStore({ path: ":memory:", D: 1024 });
|
|
54
|
+
const mind = new Mind({ seed: 7, store });
|
|
55
|
+
await mind.ingest(CHAIN);
|
|
56
|
+
await mind.ingest(Array.from({ length: 4300 }, (_, i) => filler(i)));
|
|
57
|
+
return { store, mind };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
test("derive-through: a produced fact whose subject the query never names", async () => {
|
|
61
|
+
const { store, mind } = await pastTheFlip();
|
|
62
|
+
const query = "Eiffel Tower country capital";
|
|
63
|
+
assert.ok(
|
|
64
|
+
!query.includes("France") && !query.includes("capital of France"),
|
|
65
|
+
"sanity: the intermediate subject must NOT be named in the query",
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const moves = [];
|
|
69
|
+
const out = await mind.respondText(
|
|
70
|
+
query,
|
|
71
|
+
(s) => moves.push(s.mechanism[s.mechanism.length - 1]),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
assert.equal(out.trim(), "The capital of France is Paris.");
|
|
75
|
+
assert.ok(
|
|
76
|
+
moves.includes("derive-through"),
|
|
77
|
+
`expected derive-through to be the move that reached the answer, got: ${
|
|
78
|
+
[...new Set(moves)].join(", ")
|
|
79
|
+
}`,
|
|
80
|
+
);
|
|
81
|
+
await store.close();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("naming the intermediate does not need derive-through — the cover reads it directly", async () => {
|
|
85
|
+
// The contrast that keeps the move honest: when the subject IS written, the
|
|
86
|
+
// cover already reaches the chain, so no derive-through is claimed.
|
|
87
|
+
const { store, mind } = await pastTheFlip();
|
|
88
|
+
const moves = [];
|
|
89
|
+
const out = await mind.respondText(
|
|
90
|
+
"Eiffel Tower country France capital",
|
|
91
|
+
(s) => moves.push(s.mechanism[s.mechanism.length - 1]),
|
|
92
|
+
);
|
|
93
|
+
assert.ok(out.includes("The capital of France is Paris."));
|
|
94
|
+
assert.ok(
|
|
95
|
+
!moves.includes("derive-through"),
|
|
96
|
+
"a named intermediate must not be reported as derive-through",
|
|
97
|
+
);
|
|
98
|
+
await store.close();
|
|
99
|
+
});
|