@hviana/sema 0.7.7 → 0.7.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.
@@ -73,12 +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 a JOIN produced: a produced fact's own contained entity
77
- * (the subject the query never named) combined with the query's adjacent
78
- * relation span named a learned key, and that key's continuation is this
79
- * out. Part of {@link key} so the joined reading is a distinct chart item
80
- * from the plain concatenation of the same bytes. */
81
- join?: 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;
82
84
  };
83
85
  export declare const STEP = 1;
84
86
  export declare const CONCEPT = 10;
@@ -142,7 +144,7 @@ export interface DerivationItem {
142
144
  * {@link GraphSearch}'s rules fired, recovered from the rule's premise/
143
145
  * conclusion shape (the rules carry no label, so this classifies by structure,
144
146
  * the single place that maps rule geometry to a name). */
145
- export type DerivationMove = "axiom" | "follow-edge" | "concept-hop" | "voice" | "ground" | "splice-connector" | "split" | "fuse" | "recompose" | "join-fact" | "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";
146
148
  /** The lightest-derivation search over the Sema graph. One instance binds the
147
149
  * store, `maxGroup` (the fusible span ceiling), and the canonical
148
150
  * {@link resolve} callback; {@link cover} then solves one query. */
@@ -186,11 +188,12 @@ export declare class GraphSearch {
186
188
  * connector rule (see {@link outRules}), so the returned spans already carry
187
189
  * it — there is no post-pass. */
188
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>,
189
- /** When given, receives the lightest derivation's rule applications — the
190
- * full adapted A*LD proof tree as classified {@link DerivationStep}s — for the TOP
191
- * cover only (a recursive recompletion solves its own sub-cover and is not
192
- * reported here, to keep the trace one layer per think). Off by default,
193
- * so the search pays nothing when no one inspects. */
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. */
194
197
  onDerivation?: (steps: DerivationStep[]) => void): {
195
198
  segs: Seg[];
196
199
  cost: number;
@@ -318,25 +321,30 @@ export declare class GraphSearch {
318
321
  * most once per cover. A Set, not a flag, because it states WHICH node is
319
322
  * open — the invariant a reader needs to check the guard. */
320
323
  private recompleteOpen;
321
- /** JOIN — the move the substrate was missing: derive the answer THROUGH a
322
- * produced fact, without the intermediate key being named in the query.
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.
323
330
  *
324
331
  * A produced fact (`fact.node`) carries the subject the query reached but
325
332
  * never wrote; the query's remaining tail names the relation to follow from
326
333
  * it. The pair IS a learned key — `"<entity><tail>"` — so the rule asks the
327
334
  * store for that key's continuation and, when it exists, concludes with the
328
- * joined fact. On the ladder it is one STEP: a direct edge, exactly as
329
- * following a literal continuation is. Deterministic and point-probed
330
- * (`resolve` + `nextFirst`, no scan), so it adds no read that grows with the
331
- * corpus. The move is visible in the rationale as its own act
332
- * (`classifyMove` reports `join-fact`), distinct from the byte-concatenating
333
- * `fuse`/`splice`. */
334
- private join;
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;
335
343
  /** out(i,j,bytes,…): index it for the binary rules, then offer splicing a
336
344
  * learnt connector (the in-search bridge), splitting (at a sub-leaf form
337
345
  * boundary), bridging (cover(i) ∧ this → cover(j)), fusing with an adjacent
338
346
  * finalised out, and — for a produced fact — JOINING the entity it contains
339
- * with the query's tail ({@link join}). */
347
+ * with the query's tail ({@link deriveThrough}). */
340
348
  private outRules;
341
349
  /** Whether the query span [from, to) is wholly covered by RECOGNISED outs —
342
350
  * the test that lets a connector jump across INTERIOR answers (an N-ary whole)
@@ -104,7 +104,7 @@ function classifyMove(premises, conclusion, articulating) {
104
104
  if (p.kind === "out" && conclusion.kind === "out") {
105
105
  // A JOIN derives through a produced fact's own contained subject; a plain
106
106
  // single-premise out→out is the byte-level split.
107
- return conclusion.join ? "join-fact" : "split";
107
+ return conclusion.throughFact ? "derive-through" : "split";
108
108
  }
109
109
  return "step";
110
110
  }
@@ -222,11 +222,12 @@ export class GraphSearch {
222
222
  * connector rule (see {@link outRules}), so the returned spans already carry
223
223
  * it — there is no post-pass. */
224
224
  cover(queryLen, sites, conceptTarget, leaves, splits, starts, substitutions, connectors, computedResults,
225
- /** When given, receives the lightest derivation's rule applications — the
226
- * full adapted A*LD proof tree as classified {@link DerivationStep}s — for the TOP
227
- * cover only (a recursive recompletion solves its own sub-cover and is not
228
- * reported here, to keep the trace one layer per think). Off by default,
229
- * so the search pays nothing when no one inspects. */
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. */
230
231
  onDerivation) {
231
232
  // Top-level entry: reset the per-call recursion state, then run the one
232
233
  // {@link solve} routine that both the query and any produced composite go
@@ -367,7 +368,7 @@ export class GraphSearch {
367
368
  if (it.kind === "form") {
368
369
  return `f${it.i}.${it.j}.${it.node}.${it.via ? 1 : 0}.${it.rcmp ? 1 : 0}`;
369
370
  }
370
- return `o${it.i}.${it.j}.${it.cover ? 1 : 0}.${it.rec ? 1 : 0}.${it.fix ? 1 : 0}.${it.join ? 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)}`;
371
372
  },
372
373
  *axioms() {
373
374
  yield { item: { kind: "cover", p: 0 }, cost: 0 };
@@ -866,20 +867,25 @@ export class GraphSearch {
866
867
  * most once per cover. A Set, not a flag, because it states WHICH node is
867
868
  * open — the invariant a reader needs to check the guard. */
868
869
  recompleteOpen = new Set();
869
- /** JOIN — the move the substrate was missing: derive the answer THROUGH a
870
- * produced fact, without the intermediate key being named in the query.
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.
871
876
  *
872
877
  * A produced fact (`fact.node`) carries the subject the query reached but
873
878
  * never wrote; the query's remaining tail names the relation to follow from
874
879
  * it. The pair IS a learned key — `"<entity><tail>"` — so the rule asks the
875
880
  * store for that key's continuation and, when it exists, concludes with the
876
- * joined fact. On the ladder it is one STEP: a direct edge, exactly as
877
- * following a literal continuation is. Deterministic and point-probed
878
- * (`resolve` + `nextFirst`, no scan), so it adds no read that grows with the
879
- * corpus. The move is visible in the rationale as its own act
880
- * (`classifyMove` reports `join-fact`), distinct from the byte-concatenating
881
- * `fuse`/`splice`. */
882
- *join(fact, queryBytes, queryLen) {
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) {
883
889
  if (!this.host.recogniseSpan)
884
890
  return;
885
891
  const tail = queryBytes.subarray(fact.j, queryLen);
@@ -892,8 +898,16 @@ export class GraphSearch {
892
898
  for (const site of this.host.recogniseSpan(fact.bytes).sites) {
893
899
  if (site.payload < 0 || site.payload === fact.node)
894
900
  continue;
895
- if (!this.store.hasNext(site.payload) &&
896
- !this.store.hasHalo(site.payload))
901
+ // The admission predicate has ONE definition — `traverse.ts`'s
902
+ // `leadsSomewhere` (edge or halo). The host LENDS it when it can (Mind
903
+ // does, with the response-scoped struct cache); a bare host falls back to
904
+ // the raw-store probe, so the search stays host-based. Same semantics
905
+ // either way; the hook spares the repeated probe and keeps the predicate
906
+ // from being redefined here.
907
+ const leads = this.host.leadsSomewhere !== undefined
908
+ ? this.host.leadsSomewhere(site.payload)
909
+ : this.store.hasNext(site.payload) || this.store.hasHalo(site.payload);
910
+ if (!leads)
897
911
  continue;
898
912
  const key = this.host.resolve(concat2(this.store.bytesPrefix(site.payload, ALL), tail));
899
913
  if (key === null)
@@ -911,7 +925,7 @@ export class GraphSearch {
911
925
  cover: true,
912
926
  rec: true,
913
927
  node: nx[0],
914
- join: true,
928
+ throughFact: true,
915
929
  },
916
930
  cost: STEP,
917
931
  };
@@ -921,7 +935,7 @@ export class GraphSearch {
921
935
  * learnt connector (the in-search bridge), splitting (at a sub-leaf form
922
936
  * boundary), bridging (cover(i) ∧ this → cover(j)), fusing with an adjacent
923
937
  * finalised out, and — for a produced fact — JOINING the entity it contains
924
- * with the query's tail ({@link join}). */
938
+ * with the query's tail ({@link deriveThrough}). */
925
939
  *outRules(it, ctx) {
926
940
  const { splits, coversDone, outsByStart, outsByEnd, coverableByStart } = ctx;
927
941
  const outsByNode = ctx.outsByNode;
@@ -1014,16 +1028,17 @@ export class GraphSearch {
1014
1028
  yield* this.fuse(it, r, ctx);
1015
1029
  for (const l of outsByEnd.get(it.i) ?? [])
1016
1030
  yield* this.fuse(l, it, ctx);
1017
- // ── JOIN (the A*LD extension) ───────────────────────────────────────
1031
+ // ── DERIVE-THROUGH (one more rule of the DeductionSystem this module builds)
1018
1032
  // A produced fact may CONTAIN the subject the query never named; the query's
1019
1033
  // remaining tail then names the relation to follow FROM that subject. The
1020
1034
  // pair (contained entity, tail) is itself a learned key, and its
1021
- // continuation is the derived answer — a genuine two-fact join, not the
1035
+ // continuation is the derived answer — a genuine relational join, distinct
1036
+ // from the confluence mechanism's `join` PROVENANCE — and not the
1022
1037
  // juxtaposition the cover produces when the intermediate key IS named.
1023
1038
  // Fired per finalized out with a node, so it is the search's own rule, on
1024
1039
  // the ladder, memoised by {@link key}, and bounded by the fact's own length.
1025
1040
  if (it.node !== undefined) {
1026
- yield* this.join(it, ctx.queryBytes, ctx.queryLen);
1041
+ yield* this.deriveThrough(it, ctx.queryBytes, ctx.queryLen);
1027
1042
  }
1028
1043
  }
1029
1044
  /** Whether the query span [from, to) is wholly covered by RECOGNISED outs —
@@ -12,14 +12,16 @@ import { unexplainedLabel } from "../rationale.js";
12
12
  export function aluToMechanism(alu) {
13
13
  return {
14
14
  name: "alu",
15
- // Not a cover derivation: cover.ts composes an answer by walking
16
- // recognised query STRUCTURE; the ALU evaluates a recognised expression
17
- // to its authoritative result and hands the bytes back untouched. It
18
- // shares cover's near-zero floor (computation always wins, masked into
19
- // cover's own search — see mechanisms/cover.ts), but the candidate this
20
- // produces is not one of cover's derivations, so it carries its own
21
- // honest label, the same way extract/cast/recall each carry theirs.
22
- provenance: "alu",
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;
@@ -150,6 +150,11 @@ export declare class Mind implements MindContext {
150
150
  private _conversations;
151
151
  /** Canonical node id of a byte span. Required by GraphSearchHost & MindContext. */
152
152
  resolve(bytes: Uint8Array): number | null;
153
+ /** Whether a node leads somewhere — the admission predicate, delegating to
154
+ * `traverse.ts`'s ONE definition (edge or halo, with its response-scoped
155
+ * cache). The search holds a bare Store and cannot reach that cache itself,
156
+ * so it asks through this hook; a bare host keeps its raw-store fallback. */
157
+ leadsSomewhere(id: number): boolean;
153
158
  recogniseSpan(bytes: Uint8Array): {
154
159
  sites: ReadonlyArray<Site>;
155
160
  leaves: ReadonlyArray<Leaf>;
@@ -20,7 +20,7 @@ import { GraphSearch, } from "./graph-search.js";
20
20
  import { Alu } from "../alu/src/index.js";
21
21
  import { decodeText, Rationale, } from "./rationale.js";
22
22
  import { gistOf, inputBytes, perceive as perceiveImpl, perceiveKey, resolve as resolveImpl, } from "./primitives.js";
23
- import { chooseNext, edgeAncestors as edgeAncestorsFn, invalidateStructuralCaches, } from "./traverse.js";
23
+ import { chooseNext, edgeAncestors as edgeAncestorsFn, invalidateStructuralCaches, leadsSomewhere, } from "./traverse.js";
24
24
  import { invalidateJunctionCache } from "./junction.js";
25
25
  import { follow } from "./match.js";
26
26
  import { recognise, segment } from "./recognition.js";
@@ -112,6 +112,13 @@ export class Mind {
112
112
  resolve(bytes) {
113
113
  return resolveImpl(this, bytes);
114
114
  }
115
+ /** Whether a node leads somewhere — the admission predicate, delegating to
116
+ * `traverse.ts`'s ONE definition (edge or halo, with its response-scoped
117
+ * cache). The search holds a bare Store and cannot reach that cache itself,
118
+ * so it asks through this hook; a bare host keeps its raw-store fallback. */
119
+ leadsSomewhere(id) {
120
+ return leadsSomewhere(this, id);
121
+ }
115
122
  // recogniseSpan wraps recognise
116
123
  recogniseSpan(bytes) {
117
124
  const r = recognise(this, bytes);
@@ -155,8 +162,9 @@ export class Mind {
155
162
  // `makeKeyring`, `Space.rand` and the `Alphabet` below, so folding a
156
163
  // query under config.ts's default (42) against a store trained with
157
164
  // 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 then
159
- // read the wrong space and every answer degrades silently. An explicit
165
+ // the artifact's nodes were folded into: recognition and resonance read
166
+ // the wrong space, and answers silently diverge — pinned by test/97,
167
+ // where only adoption reproduces the artifact's own answer. An explicit
160
168
  // caller seed still wins — this only replaces the unconfigured default.
161
169
  if (explicitSeed === undefined && this.store.trainSeed !== null) {
162
170
  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
@@ -503,18 +503,18 @@ function recogniseImpl(ctx, bytes) {
503
503
  const probe = (start, end, canonBudget) => {
504
504
  // Any span at least one river window wide is worth a probe. This used
505
505
  // to stop at `chainReach(W)` — "the chain already covers anything that
506
- // short" — and that premise is false for a NESTED form: the chain grows
507
- // single-byte leaf ids and gates each step on `findBranch(ids)`, which
508
- // is null for a form the write side chunked (measured: "Gustaf
509
- // Molander" embedded in "The director of Eva is Gustaf Molander." gains
510
- // no branch at any prefix, so `resolveSpan` is never reached), and its
511
- // INTERIOR reach is one chunk plus W, which can be shorter than the
512
- // form. The result was a dead zone: a form shorter than `chainReach`
513
- // that neither starts on a fold cut nor ends on a node edge was
514
- // unreachable by either tier — the exact site whose loss `tryChain`'s
515
- // own note records as "the pivot dies with the site and multi-hop goes
516
- // silent". The interior pass below spends the same budget on those
517
- // pairs.
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
518
  if (end - start < W)
519
519
  return;
520
520
  if (flatProbe(start, end) === null) {
@@ -579,13 +579,15 @@ function recogniseImpl(ctx, bytes) {
579
579
  if (i < suffixes.length && !spend(suffixes[i], bytes.length))
580
580
  break;
581
581
  }
582
- // INTERIOR pairs, bounded by the same `chainReach(W)` the chain trusts —
583
- // the dead zone the gate above used to leave: a form that neither starts
584
- // on a fold cut nor ends on a node edge is exactly the one neither the
585
- // chain (nested, `findBranch` misses) nor the two edge scans reach. The
586
- // pair count is `|endpoints| · chainReach(W)`, i.e. LINEAR in the query —
587
- // the W² span bound is what keeps this from being the quadratic scan the
588
- // budget note above describes (that one had no span bound at all).
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).
589
591
  {
590
592
  const reach = chainReach(W);
591
593
  for (const end of ordered) {
@@ -50,7 +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
- "join-fact": "join a produced fact's own subject with the query's tail — derive through the fact, not alongside it",
53
+ "derive-through": "derive the answer through a produced fact's own subject and the query's tail, not alongside it",
54
54
  "bridge": "advance the cover frontier across this span",
55
55
  "pool-vote": "pool independent regions' evidence for a shared anchor (sum, not shortest path)",
56
56
  "axiom": "a seed: a perceived leaf, recognised form, or computed result",
@@ -35,6 +35,12 @@ export interface GraphSearchHost {
35
35
  starts: ReadonlySet<number>;
36
36
  };
37
37
  chooseNext?(node: number): number | undefined;
38
+ /** The admission predicate — `traverse.ts`'s `leadsSomewhere`, its ONE
39
+ * definition: does this node bear an edge or a halo? Optional, so a bare
40
+ * host (a raw Store and nothing else) still works; when present, the search
41
+ * uses it rather than re-probing the store, which keeps the predicate
42
+ * single-defined AND memoised on the response-scoped struct cache. */
43
+ leadsSomewhere?(id: number): boolean;
38
44
  }
39
45
  export interface Recognition {
40
46
  /** Forms that can lead somewhere — they have an edge or a halo. */
@@ -66,8 +66,14 @@ forms need no host; meaning-based paths do.
66
66
 
67
67
  ## Provenance
68
68
 
69
- Grounded ALU answers carry `alu`; `computeExtensions`/`evalComputation` trace
70
- the expression and result.
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
 
@@ -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,
@@ -45,8 +45,11 @@ by node pair. Bridges (`bridge`) splice connectors between rewrites.
45
45
 
46
46
  ## Provenance
47
47
 
48
- `cover` for the query's own cover; `join` when fusing fragments into a deeper
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://jsr.io/schema/config-file.v1.json",
3
3
  "name": "@hviana/sema",
4
- "version": "0.7.7",
4
+ "version": "0.7.9",
5
5
  "exports": "./src/index.ts"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hviana/sema",
3
- "version": "0.7.7",
3
+ "version": "0.7.9",
4
4
  "description": "Sema: a non-parametric, instance-based reasoning system.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -115,12 +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 a JOIN produced: a produced fact's own contained entity
119
- * (the subject the query never named) combined with the query's adjacent
120
- * relation span named a learned key, and that key's continuation is this
121
- * out. Part of {@link key} so the joined reading is a distinct chart item
122
- * from the plain concatenation of the same bytes. */
123
- join?: 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;
124
126
  };
125
127
  type OutItem = Extract<GItem, { kind: "out" }>;
126
128
 
@@ -246,7 +248,7 @@ export type DerivationMove =
246
248
  | "split" // out→out cut at a sub-leaf form boundary
247
249
  | "fuse" // out+out→out: adjacent fragments recomposed toward a learned form
248
250
  | "recompose" // out+out→form: a fused pair that names an edge-bearing node
249
- | "join-fact" // out→out: a produced fact's own contained subject + the query's tail names a learned key (the join)
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)
250
252
  | "bridge" // cover+out→cover: the cover frontier advanced across a span
251
253
  | "pool-vote" // N premises→conclusion, evidence pooled (combine:"sum" — see derive)
252
254
  | "step"; // any other single-premise move (fallback)
@@ -286,7 +288,7 @@ function classifyMove(
286
288
  if (p.kind === "out" && conclusion.kind === "out") {
287
289
  // A JOIN derives through a produced fact's own contained subject; a plain
288
290
  // single-premise out→out is the byte-level split.
289
- return conclusion.join ? "join-fact" : "split";
291
+ return conclusion.throughFact ? "derive-through" : "split";
290
292
  }
291
293
  return "step";
292
294
  }
@@ -411,11 +413,12 @@ export class GraphSearch {
411
413
  substitutions?: ReadonlyMap<number, Uint8Array>,
412
414
  connectors?: ReadonlyMap<string, Uint8Array>,
413
415
  computedResults?: ReadonlyArray<ComputedResult>,
414
- /** When given, receives the lightest derivation's rule applications — the
415
- * full adapted A*LD proof tree as classified {@link DerivationStep}s — for the TOP
416
- * cover only (a recursive recompletion solves its own sub-cover and is not
417
- * reported here, to keep the trace one layer per think). Off by default,
418
- * so the search pays nothing when no one inspects. */
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. */
419
422
  onDerivation?: (steps: DerivationStep[]) => void,
420
423
  ): { segs: Seg[]; cost: number } | null {
421
424
  // Top-level entry: reset the per-call recursion state, then run the one
@@ -607,7 +610,7 @@ export class GraphSearch {
607
610
  }
608
611
  return `o${it.i}.${it.j}.${it.cover ? 1 : 0}.${it.rec ? 1 : 0}.${
609
612
  it.fix ? 1 : 0
610
- }.${it.join ? 1 : 0}.${it.node ?? -1}.${latin1(it.bytes)}`;
613
+ }.${it.throughFact ? 1 : 0}.${it.node ?? -1}.${latin1(it.bytes)}`;
611
614
  },
612
615
  *axioms() {
613
616
  yield { item: { kind: "cover", p: 0 }, cost: 0 };
@@ -1133,20 +1136,25 @@ export class GraphSearch {
1133
1136
  * open — the invariant a reader needs to check the guard. */
1134
1137
  private recompleteOpen = new Set<number>();
1135
1138
 
1136
- /** JOIN — the move the substrate was missing: derive the answer THROUGH a
1137
- * produced fact, without the intermediate key being named in the query.
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.
1138
1145
  *
1139
1146
  * A produced fact (`fact.node`) carries the subject the query reached but
1140
1147
  * never wrote; the query's remaining tail names the relation to follow from
1141
1148
  * it. The pair IS a learned key — `"<entity><tail>"` — so the rule asks the
1142
1149
  * store for that key's continuation and, when it exists, concludes with the
1143
- * joined fact. On the ladder it is one STEP: a direct edge, exactly as
1144
- * following a literal continuation is. Deterministic and point-probed
1145
- * (`resolve` + `nextFirst`, no scan), so it adds no read that grows with the
1146
- * corpus. The move is visible in the rationale as its own act
1147
- * (`classifyMove` reports `join-fact`), distinct from the byte-concatenating
1148
- * `fuse`/`splice`. */
1149
- private *join(
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(
1150
1158
  fact: OutItem,
1151
1159
  queryBytes: Uint8Array,
1152
1160
  queryLen: number,
@@ -1160,10 +1168,16 @@ export class GraphSearch {
1160
1168
  // itself, not an entity inside it.
1161
1169
  for (const site of this.host.recogniseSpan(fact.bytes).sites) {
1162
1170
  if (site.payload < 0 || site.payload === fact.node) continue;
1163
- if (
1164
- !this.store.hasNext(site.payload) &&
1165
- !this.store.hasHalo(site.payload)
1166
- ) continue;
1171
+ // The admission predicate has ONE definition — `traverse.ts`'s
1172
+ // `leadsSomewhere` (edge or halo). The host LENDS it when it can (Mind
1173
+ // does, with the response-scoped struct cache); a bare host falls back to
1174
+ // the raw-store probe, so the search stays host-based. Same semantics
1175
+ // either way; the hook spares the repeated probe and keeps the predicate
1176
+ // from being redefined here.
1177
+ const leads = this.host.leadsSomewhere !== undefined
1178
+ ? this.host.leadsSomewhere(site.payload)
1179
+ : this.store.hasNext(site.payload) || this.store.hasHalo(site.payload);
1180
+ if (!leads) continue;
1167
1181
  const key = this.host.resolve(
1168
1182
  concat2(this.store.bytesPrefix(site.payload, ALL), tail),
1169
1183
  );
@@ -1180,7 +1194,7 @@ export class GraphSearch {
1180
1194
  cover: true,
1181
1195
  rec: true,
1182
1196
  node: nx[0],
1183
- join: true,
1197
+ throughFact: true,
1184
1198
  },
1185
1199
  cost: STEP,
1186
1200
  };
@@ -1191,7 +1205,7 @@ export class GraphSearch {
1191
1205
  * learnt connector (the in-search bridge), splitting (at a sub-leaf form
1192
1206
  * boundary), bridging (cover(i) ∧ this → cover(j)), fusing with an adjacent
1193
1207
  * finalised out, and — for a produced fact — JOINING the entity it contains
1194
- * with the query's tail ({@link join}). */
1208
+ * with the query's tail ({@link deriveThrough}). */
1195
1209
  private *outRules(
1196
1210
  it: OutItem,
1197
1211
  ctx: {
@@ -1303,16 +1317,17 @@ export class GraphSearch {
1303
1317
  for (const r of outsByStart.get(it.j) ?? []) yield* this.fuse(it, r, ctx);
1304
1318
  for (const l of outsByEnd.get(it.i) ?? []) yield* this.fuse(l, it, ctx);
1305
1319
 
1306
- // ── JOIN (the A*LD extension) ───────────────────────────────────────
1320
+ // ── DERIVE-THROUGH (one more rule of the DeductionSystem this module builds)
1307
1321
  // A produced fact may CONTAIN the subject the query never named; the query's
1308
1322
  // remaining tail then names the relation to follow FROM that subject. The
1309
1323
  // pair (contained entity, tail) is itself a learned key, and its
1310
- // continuation is the derived answer — a genuine two-fact join, not the
1324
+ // continuation is the derived answer — a genuine relational join, distinct
1325
+ // from the confluence mechanism's `join` PROVENANCE — and not the
1311
1326
  // juxtaposition the cover produces when the intermediate key IS named.
1312
1327
  // Fired per finalized out with a node, so it is the search's own rule, on
1313
1328
  // the ladder, memoised by {@link key}, and bounded by the fact's own length.
1314
1329
  if (it.node !== undefined) {
1315
- yield* this.join(it, ctx.queryBytes, ctx.queryLen);
1330
+ yield* this.deriveThrough(it, ctx.queryBytes, ctx.queryLen);
1316
1331
  }
1317
1332
  }
1318
1333
 
@@ -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
- // Not a cover derivation: cover.ts composes an answer by walking
20
- // recognised query STRUCTURE; the ALU evaluates a recognised expression
21
- // to its authoritative result and hands the bytes back untouched. It
22
- // shares cover's near-zero floor (computation always wins, masked into
23
- // cover's own search — see mechanisms/cover.ts), but the candidate this
24
- // produces is not one of cover's derivations, so it carries its own
25
- // honest label, the same way extract/cast/recall each carry theirs.
26
- provenance: "alu",
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
@@ -159,6 +159,7 @@ import {
159
159
  chooseNext,
160
160
  edgeAncestors as edgeAncestorsFn,
161
161
  invalidateStructuralCaches,
162
+ leadsSomewhere,
162
163
  } from "./traverse.js";
163
164
  import { invalidateJunctionCache } from "./junction.js";
164
165
  import { follow } from "./match.js";
@@ -343,6 +344,14 @@ export class Mind implements MindContext {
343
344
  return resolveImpl(this, bytes);
344
345
  }
345
346
 
347
+ /** Whether a node leads somewhere — the admission predicate, delegating to
348
+ * `traverse.ts`'s ONE definition (edge or halo, with its response-scoped
349
+ * cache). The search holds a bare Store and cannot reach that cache itself,
350
+ * so it asks through this hook; a bare host keeps its raw-store fallback. */
351
+ leadsSomewhere(id: number): boolean {
352
+ return leadsSomewhere(this, id);
353
+ }
354
+
346
355
  // recogniseSpan wraps recognise
347
356
  recogniseSpan(bytes: Uint8Array): {
348
357
  sites: ReadonlyArray<Site>;
@@ -410,8 +419,9 @@ export class Mind implements MindContext {
410
419
  // `makeKeyring`, `Space.rand` and the `Alphabet` below, so folding a
411
420
  // query under config.ts's default (42) against a store trained with
412
421
  // 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
422
+ // the artifact's nodes were folded into: recognition and resonance read
423
+ // the wrong space, and answers silently diverge — pinned by test/97,
424
+ // where only adoption reproduces the artifact's own answer. An explicit
415
425
  // caller seed still wins — this only replaces the unconfigured default.
416
426
  if (explicitSeed === undefined && this.store.trainSeed !== null) {
417
427
  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.
@@ -520,18 +520,18 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
520
520
  ): void => {
521
521
  // Any span at least one river window wide is worth a probe. This used
522
522
  // to stop at `chainReach(W)` — "the chain already covers anything that
523
- // short" — and that premise is false for a NESTED form: the chain grows
524
- // single-byte leaf ids and gates each step on `findBranch(ids)`, which
525
- // is null for a form the write side chunked (measured: "Gustaf
526
- // Molander" embedded in "The director of Eva is Gustaf Molander." gains
527
- // no branch at any prefix, so `resolveSpan` is never reached), and its
528
- // INTERIOR reach is one chunk plus W, which can be shorter than the
529
- // form. The result was a dead zone: a form shorter than `chainReach`
530
- // that neither starts on a fold cut nor ends on a node edge was
531
- // unreachable by either tier — the exact site whose loss `tryChain`'s
532
- // own note records as "the pivot dies with the site and multi-hop goes
533
- // silent". The interior pass below spends the same budget on those
534
- // pairs.
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
535
  if (end - start < W) return;
536
536
  if (flatProbe(start, end) === null) {
537
537
  if (!canonBudget) return;
@@ -589,13 +589,15 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
589
589
  if (i < prefixes.length && !spend(0, prefixes[i])) break;
590
590
  if (i < suffixes.length && !spend(suffixes[i], bytes.length)) break;
591
591
  }
592
- // INTERIOR pairs, bounded by the same `chainReach(W)` the chain trusts —
593
- // the dead zone the gate above used to leave: a form that neither starts
594
- // on a fold cut nor ends on a node edge is exactly the one neither the
595
- // chain (nested, `findBranch` misses) nor the two edge scans reach. The
596
- // pair count is `|endpoints| · chainReach(W)`, i.e. LINEAR in the query —
597
- // the W² span bound is what keeps this from being the quadratic scan the
598
- // budget note above describes (that one had no span bound at all).
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).
599
601
  {
600
602
  const reach = chainReach(W);
601
603
  for (const end of ordered) {
package/src/mind/trace.ts CHANGED
@@ -75,8 +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
- "join-fact":
79
- "join a produced fact's own subject with the query's tail — derive through the fact, not alongside it",
78
+ "derive-through":
79
+ "derive the answer through a produced fact's own subject and the query's tail, not alongside it",
80
80
  "bridge": "advance the cover frontier across this span",
81
81
  "pool-vote":
82
82
  "pool independent regions' evidence for a shared anchor (sum, not shortest path)",
package/src/mind/types.ts CHANGED
@@ -65,6 +65,12 @@ export interface GraphSearchHost {
65
65
  starts: ReadonlySet<number>;
66
66
  };
67
67
  chooseNext?(node: number): number | undefined;
68
+ /** The admission predicate — `traverse.ts`'s `leadsSomewhere`, its ONE
69
+ * definition: does this node bear an edge or a halo? Optional, so a bare
70
+ * host (a raw Store and nothing else) still works; when present, the search
71
+ * uses it rather than re-probing the store, which keeps the predicate
72
+ * single-defined AND memoised on the response-scoped struct cache. */
73
+ leadsSomewhere?(id: number): boolean;
68
74
  }
69
75
 
70
76
  // ═══════════════════════════════════════════════════════════════════════════
@@ -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 every
10
- // answer degrades silently.
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
@@ -1,20 +1,22 @@
1
- // 99-fact-join.test.mjs — the JOIN: derive the answer THROUGH a produced fact
2
- // whose subject the query never names.
1
+ // 99-fact-join.test.mjs — DERIVE-THROUGH: derive the answer THROUGH a produced
2
+ // fact whose subject the query never names.
3
3
  //
4
4
  // THE MOVE THIS PINS. A query like "Eiffel Tower country capital" names hop 1's
5
5
  // key ("Eiffel Tower country") and hop 2's relation (" capital"), but never the
6
6
  // intermediate subject ("France"). The cover alone can only juxtapose the two
7
7
  // facts; the pivot needs the subject to surface as an unconsumed site inside the
8
- // produced fact. The `join` rule in graph-search.ts closes the gap: it takes the
9
- // entity the produced fact CONTAINS, combines it with the query's remaining tail,
10
- // and asks the store for that key's continuation — a direct STEP on the ladder,
11
- // reported as `join-fact` in the rationale.
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.)
12
14
  //
13
15
  // WHY THE FIXTURE CROSSES N=4096. Below the atomIsHub flip the interior subject
14
16
  // surfaces anyway (small-store recognition) and the PIVOT alone reaches the
15
17
  // chain, so a small fixture passes under the pre-change tree too — measured on a
16
18
  // 3-deposit store, which answered "The father of Gustaf Molander is Harald
17
- // Molander." with no join. The join is what carries the chain at corpus scale,
19
+ // Molander." with no derive-through move. That rule is what carries the chain at corpus scale,
18
20
  // so the fixture must sit past the flip, exactly as test/78 does. The filler is
19
21
  // lexically varied for the same reason test/78's is: a templated corpus folds to
20
22
  // shared chunks and leaves the query uncontested.
@@ -55,7 +57,7 @@ async function pastTheFlip() {
55
57
  return { store, mind };
56
58
  }
57
59
 
58
- test("a join derives through a produced fact whose subject the query never names", async () => {
60
+ test("derive-through: a produced fact whose subject the query never names", async () => {
59
61
  const { store, mind } = await pastTheFlip();
60
62
  const query = "Eiffel Tower country capital";
61
63
  assert.ok(
@@ -71,17 +73,17 @@ test("a join derives through a produced fact whose subject the query never names
71
73
 
72
74
  assert.equal(out.trim(), "The capital of France is Paris.");
73
75
  assert.ok(
74
- moves.includes("join-fact"),
75
- `expected the join to be the move that reached the answer, got: ${
76
+ moves.includes("derive-through"),
77
+ `expected derive-through to be the move that reached the answer, got: ${
76
78
  [...new Set(moves)].join(", ")
77
79
  }`,
78
80
  );
79
81
  await store.close();
80
82
  });
81
83
 
82
- test("naming the intermediate does not need the join — the cover reads it directly", async () => {
84
+ test("naming the intermediate does not need derive-through — the cover reads it directly", async () => {
83
85
  // The contrast that keeps the move honest: when the subject IS written, the
84
- // cover already reaches the chain, so no join is claimed.
86
+ // cover already reaches the chain, so no derive-through is claimed.
85
87
  const { store, mind } = await pastTheFlip();
86
88
  const moves = [];
87
89
  const out = await mind.respondText(
@@ -90,8 +92,8 @@ test("naming the intermediate does not need the join — the cover reads it dire
90
92
  );
91
93
  assert.ok(out.includes("The capital of France is Paris."));
92
94
  assert.ok(
93
- !moves.includes("join-fact"),
94
- "a named intermediate must not be reported as a join",
95
+ !moves.includes("derive-through"),
96
+ "a named intermediate must not be reported as derive-through",
95
97
  );
96
98
  await store.close();
97
99
  });