@hviana/sema 0.5.6 → 0.5.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.
@@ -2,10 +2,13 @@
2
2
  //
3
3
  // Learning is DEPOSITION: perceive a stream into a tree and intern every
4
4
  // node. A fact is an EDGE between node ids; recall traverses edges.
5
+ import { addInto, normalize, zeros } from "../vec.js";
5
6
  import { bindSeat, companySignature, isChunk } from "../sema.js";
6
7
  import { changedNodes } from "./types.js";
7
8
  import { inputBytes, perceiveDeposit, resolve, } from "./primitives.js";
8
9
  import { canonicalWindows, leafIdPrefix } from "./canonical.js";
10
+ import { hubBound } from "./traverse.js";
11
+ import { dominates } from "../geometry.js";
9
12
  import { fold as foldVecs } from "../sema.js";
10
13
  /** Intern a perceived tree into node ids, bottom-up, sharing equal subtrees.
11
14
  * Returns the root node id and a map from tree nodes to their ids.
@@ -188,6 +191,163 @@ async function propagateSuffixes(ctx, src, dst) {
188
191
  await ctx.store.link(id, dst);
189
192
  }
190
193
  }
194
+ /** How many constituents one profile may VISIT. A partner's constituent tree
195
+ * is O(len/W) nodes, so an uncapped descent would make a pour cost grow with
196
+ * the partner's LENGTH — and a partner is a whole deposit, which may be a
197
+ * paragraph. The budget is what keeps a pour O(1) in the input, the property
198
+ * that lets {@link companyProfile} claim no new cost class. It binds only on
199
+ * long partners whose constituents are all corpus-unique; the descent's own
200
+ * stop rule (below) reaches recurring units far sooner on a trained store. */
201
+ const PROFILE_VISITS = 64;
202
+ /** The COMPANY PROFILE of a partner: its own identity signature superposed
203
+ * with the signatures of its RECURRING content-defined constituents.
204
+ *
205
+ * WHY THE WHOLE-PARTNER SIGNATURE ALONE IS NOT ENOUGH. The distributional
206
+ * hypothesis is a claim about TYPES ("occurs near a city name"), but a
207
+ * signature keyed on the whole partner's node id records a TOKEN ("occurred
208
+ * near node #4711992"). Two nodes are then distributional siblings only when
209
+ * their partners are the very same node — and a content-addressed store of
210
+ * natural language almost never repeats a whole deposit (measured on the
211
+ * trained store: whole-span dedup 0.98×, i.e. effectively none). So the
212
+ * halos of genuine synonyms came out quasi-orthogonal BY CONSTRUCTION: the
213
+ * best distributional sibling of "Eiffel Tower" scored 0.146 against a
214
+ * concept threshold of 0.516, with its own attested translations absent
215
+ * entirely, and the whole concept-hop / articulation / analogy layer was
216
+ * inert at corpus scale. (Re-verified under the store's OWN training seed:
217
+ * company signatures key on NODE ID, not the alphabet, so this reading is
218
+ * seed-independent and the figures are identical either way. Worth stating
219
+ * because a Mind built with a seed other than the store's makes every GIST
220
+ * comparison meaningless while leaving halo comparisons untouched.)
221
+ *
222
+ * WHY THE DESCENT MUST NOT STOP AT DEPTH 1. Reading only `rec.kids` does
223
+ * NOT deliver this. Cuts are content-defined over a rolling window, so a
224
+ * chunk boundary depends on the bytes AROUND a unit: "The Eiffel Tower is in
225
+ * Paris" folds to "The Eiffel " + "Tower is in Paris", and "Tour Eiffel dia
226
+ * any Paris" to "Tour Eiffel " + "dia any Paris". The shared unit "Paris"
227
+ * is a node in NEITHER — depth-1 profiles of that pair intersect in the
228
+ * EMPTY SET, and their halos measured 0.0319 against 0.0416 for an unrelated
229
+ * control: no signal at all. A depth-1 read merely moves the token problem
230
+ * from whole-partner identity down to top-level-chunk identity, which for
231
+ * full sentences is nearly as rare. Descending, the same pair shares
232
+ * " Paris" and "ffel " while the control still shares nothing — the units
233
+ * the distributional hypothesis is actually about.
234
+ *
235
+ * EVERY DEPTH CONTRIBUTES, AND THE RULE MUST NOT DEPEND ON ARRIVAL ORDER.
236
+ * The tempting stop rule — descend only while a constituent is corpus-unique,
237
+ * stop at the first unit attested in ≥ 2 forms — is wrong, and measurably so.
238
+ * Recurrence is a property of the corpus SO FAR: when the first of a pair is
239
+ * deposited its shared unit has fan-in 1, so the descent runs past it, and
240
+ * only the second partner ever profiles it. The pair then never meets
241
+ * (measured on the fixture above: 0.0165 against a 0.0375 control — still
242
+ * nothing). Whether two synonyms become distributional siblings cannot be
243
+ * allowed to depend on which was trained first. So the walk descends through
244
+ * EVERY constituent within its budget and superposes each one that is not a
245
+ * hub, at whatever depth it sits. A partner's own unique chunks contribute
246
+ * terms unique to that partner, which dilute but never mislead; the shared
247
+ * units contribute the signal.
248
+ *
249
+ * HUBS ARE THE ONE EXCLUSION, read LIMITed as `parentsFirst(n, bound+1)` —
250
+ * the store's own exact hub-or-not probe (a result longer than the bound
251
+ * means MORE than the bound), never a fan-in-sized read. A constituent with
252
+ * more than √N structural parents is scaffolding by §8.8's bound: " is ",
253
+ * "the ". Superposing it would put a term shared by every deposit into every
254
+ * profile, ALL halos would correlate, and the concept threshold's null model
255
+ * (unrelated halos at 0 ± 1/√D) that §4.1's hygiene note protects would
256
+ * collapse. It is still DESCENDED into — a hub chunk can contain a rare
257
+ * unit — but contributes nothing itself.
258
+ *
259
+ * Byte atoms are skipped in BOTH representations (a negative id and a stored
260
+ * kid-less node): an atom's fan-in is the alphabet's, so it can only ever
261
+ * read as a hub, and a short partner folding FLAT would otherwise put a
262
+ * handful of alphabet signatures into every profile — which is what silenced
263
+ * CAST's analogy gate in the first version of this function (measured:
264
+ * analogy strength 0.3636 -> 0.2004, "no halo-tier company evidence",
265
+ * test/29 C1).
266
+ *
267
+ * A FUNCTION OF THE NODE AND THE CORPUS STATE — stated precisely, because
268
+ * the weaker claim is the true one. The constituents are read from the
269
+ * STORE, never from the depositing tree's id map: that map holds only the
270
+ * nodes THIS deposit newly interned, so a partner met a second time yielded a
271
+ * profile missing exactly those constituents, the exact-partner case fell
272
+ * from cosine 1 to 1/√(1+k), and the geometry stopped meaning anything.
273
+ * Reading the store fixes that. It does NOT make the profile permanent: the
274
+ * hub test reads fan-in against √N and both grow with training, so a partner
275
+ * poured early and again late can profile differently. That residue is
276
+ * confined to the hub EXCLUSION — which terms are dropped as scaffolding —
277
+ * and never to which units are found, because the descent itself is now
278
+ * order-independent. The drift is one-directional and benign: a term can
279
+ * only ever go from contributing to being excluded as scaffolding. Replay of
280
+ * a fixed training order is bit-identical, so §2.1 holds. What must not be
281
+ * claimed is that a node's profile is fixed for all time; it is fixed given
282
+ * the corpus that has been seen.
283
+ *
284
+ * THE NULL MODEL IS OTHERWISE UNTOUCHED (§4.1). Every term is still a seeded
285
+ * function of a NODE IDENTITY, never a gist, so no byte-similarity between
286
+ * partners can leak content similarity into distributional similarity. The
287
+ * result is normalized, so ONE episode still pours ONE unit of mass:
288
+ * {@link Store.haloMass} keeps counting episodes and every mass-based
289
+ * reading is unchanged. Two partners sharing j of k discriminating
290
+ * constituents meet at j/(1+k) — graded evidence, above the 1/√D noise floor
291
+ * and below conceptThreshold until the overlap is most of the content, which
292
+ * is the semantics "same company" should have.
293
+ *
294
+ * Bounded: at most {@link PROFILE_VISITS} constituents are classified, each
295
+ * by ONE LIMITed structural-parent read, so a pour costs O(1) reads in the
296
+ * partner's size and performs no scan. */
297
+ function companyProfile(ctx, id) {
298
+ const acc = zeros(ctx.space.D);
299
+ addInto(acc, companySignature(ctx.space, id));
300
+ const bound = hubBound(ctx);
301
+ const W = ctx.space.maxGroup;
302
+ const whole = Math.max(1, ctx.store.contentLen(id));
303
+ const frontier = [];
304
+ const seen = new Set([id]);
305
+ const descend = (n) => {
306
+ const kids = ctx.store.get(n)?.kids;
307
+ if (!kids)
308
+ return;
309
+ for (const kid of kids)
310
+ if (!seen.has(kid))
311
+ frontier.push(kid);
312
+ };
313
+ descend(id);
314
+ for (let visits = 0; visits < PROFILE_VISITS && frontier.length > 0;) {
315
+ const n = frontier.shift();
316
+ if (seen.has(n))
317
+ continue;
318
+ seen.add(n);
319
+ visits++;
320
+ // Atoms in both representations — negative id, or a stored kid-less node.
321
+ if (n < 0 || ctx.store.get(n)?.kids == null)
322
+ continue;
323
+ descend(n);
324
+ const len = ctx.store.contentLen(n, whole);
325
+ if (len < W || dominates(len, whole))
326
+ continue;
327
+ // MINIMAL units only: a constituent that still has a constituent of its
328
+ // own at or above W is a composite, and superposing it as well as its
329
+ // parts would count the same content twice. Nested partners — an
330
+ // accumulated conversation, where turn k's context is a prefix of turn
331
+ // k+1's — share their large chunks structurally rather than
332
+ // distributionally, so those composites are exactly the terms that make
333
+ // adjacent turns read as synonyms (measured: consecutive turns at 0.809
334
+ // and 0.740 against a 0.516 concept threshold). The smallest units at or
335
+ // above the fold's own window are the word-sized types company should be
336
+ // keyed at.
337
+ const kids = ctx.store.get(n).kids;
338
+ let composite = false;
339
+ for (const kid of kids) {
340
+ if (kid >= 0 && ctx.store.contentLen(kid, W) >= W)
341
+ composite = true;
342
+ }
343
+ if (composite)
344
+ continue;
345
+ if (ctx.store.parentsFirst(n, bound + 1).length > bound)
346
+ continue;
347
+ addInto(acc, companySignature(ctx.space, n));
348
+ }
349
+ return normalize(acc);
350
+ }
191
351
  /** Ingest a pair (context, continuation) — learn an edge and pour halos.
192
352
  * Returns the deposited root ids (context, continuation) — a pure
193
353
  * read-out; callers that ignore it behave exactly as before. */
@@ -203,12 +363,15 @@ export async function ingestPair(ctx, ctxInput, cont) {
203
363
  await ctx.store.link(ctxId, contId);
204
364
  await propagateSuffixes(ctx, ctxId, contId);
205
365
  // Halos pour company SIGNATURES (identity), not gists (content) — see
206
- // companySignature in sema.ts.
207
- const contSeat = bindSeat(ctx.space, companySignature(ctx.space, contId), 1);
366
+ // companySignature in sema.ts — as a TYPE-level profile: the partner's own
367
+ // signature superposed with its discriminating constituents' (see
368
+ // companyProfile), so company is shared by what partners are MADE OF and
369
+ // not only by partner identity.
370
+ const contSeat = bindSeat(ctx.space, companyProfile(ctx, contId), 1);
208
371
  for (const part of c.changed) {
209
372
  const partId = c.ids.get(part);
210
373
  await ctx.store.pourHalo(partId, contSeat);
211
- await ctx.store.pourHalo(contId, bindSeat(ctx.space, companySignature(ctx.space, partId), 0));
374
+ await ctx.store.pourHalo(contId, bindSeat(ctx.space, companyProfile(ctx, partId), 0));
212
375
  }
213
376
  return { ctxId, contId };
214
377
  }
@@ -80,6 +80,24 @@ export declare function hubCap<T>(ctx: MindContext, ids: readonly T[]): readonly
80
80
  * descent. Used by articulation to keep a voice from revoicing a fragment
81
81
  * OF that voice. */
82
82
  export declare function contains(ctx: MindContext, ancestor: number, descendant: number): boolean;
83
+ /** Whether a continuation edge joins the two forms, in either direction —
84
+ * the EXACT half's veto on calling them synonyms.
85
+ *
86
+ * Halos measure company, and the strongest company any two forms can keep is
87
+ * standing next to each other: a question and its answer co-occur in every
88
+ * episode that taught the pair, so their halos SHOULD be similar, and on a
89
+ * conversational store they are (measured on the CONV fixture: consecutive
90
+ * turns at 0.809 against a 0.516 concept threshold). A gate reading halo
91
+ * cosine alone therefore reads adjacency as synonymy and revoices an answer
92
+ * in the words of the question it answers — "it hangs in madrid" spliced back
93
+ * into "where is it kept now". The distributional layer cannot tell the two
94
+ * relations apart, because to it they are the same observation; the exact
95
+ * half can, for free, because it stored the edge. §4.1's division of labour
96
+ * exactly: approximate proposes, exact decides.
97
+ *
98
+ * Read LIMITed in both directions at the hub bound — a common continuation's
99
+ * fan-in is corpus-sized, and no single decision may scale with it. */
100
+ export declare function answers(ctx: MindContext, a: number, b: number): boolean;
83
101
  /** The best-scoring item by cosine against `query`, among items scoring at
84
102
  * or above `threshold` — the shared arg-max every Pattern-A "which of these
85
103
  * resonates best" decision reduces to. `strict` picks the tie-break a
@@ -490,6 +490,30 @@ export function contains(ctx, ancestor, descendant) {
490
490
  }
491
491
  return false;
492
492
  }
493
+ /** Whether a continuation edge joins the two forms, in either direction —
494
+ * the EXACT half's veto on calling them synonyms.
495
+ *
496
+ * Halos measure company, and the strongest company any two forms can keep is
497
+ * standing next to each other: a question and its answer co-occur in every
498
+ * episode that taught the pair, so their halos SHOULD be similar, and on a
499
+ * conversational store they are (measured on the CONV fixture: consecutive
500
+ * turns at 0.809 against a 0.516 concept threshold). A gate reading halo
501
+ * cosine alone therefore reads adjacency as synonymy and revoices an answer
502
+ * in the words of the question it answers — "it hangs in madrid" spliced back
503
+ * into "where is it kept now". The distributional layer cannot tell the two
504
+ * relations apart, because to it they are the same observation; the exact
505
+ * half can, for free, because it stored the edge. §4.1's division of labour
506
+ * exactly: approximate proposes, exact decides.
507
+ *
508
+ * Read LIMITed in both directions at the hub bound — a common continuation's
509
+ * fan-in is corpus-sized, and no single decision may scale with it. */
510
+ export function answers(ctx, a, b) {
511
+ const bound = hubBound(ctx);
512
+ if (ctx.store.hasNext(a) && ctx.store.nextFirst(a, bound).includes(b)) {
513
+ return true;
514
+ }
515
+ return ctx.store.hasNext(b) && ctx.store.nextFirst(b, bound).includes(a);
516
+ }
493
517
  // ── Edge disambiguation (Section 6) ──────────────────────────────────────
494
518
  /** The best-scoring item by cosine against `query`, among items scoring at
495
519
  * or above `threshold` — the shared arg-max every Pattern-A "which of these
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.5.6",
4
+ "version": "0.5.8",
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.5.6",
3
+ "version": "0.5.8",
4
4
  "description": "Sema: a non-parametric, instance-based reasoning system.",
5
5
  "type": "module",
6
6
  "main": "dist/src/index.js",
@@ -7,7 +7,7 @@ import { Vec } from "../vec.js";
7
7
  import type { MindContext } from "./types.js";
8
8
  import { spliceAll } from "./types.js";
9
9
  import { recognise } from "./recognition.js";
10
- import { contains } from "./traverse.js";
10
+ import { answers, contains } from "./traverse.js";
11
11
  import { bestHaloMate } from "./match.js";
12
12
  import type { Site } from "./graph-search.js";
13
13
  import type { CandidateSpan } from "../derive/src/index.js";
@@ -75,9 +75,23 @@ export async function articulate(
75
75
  const found = bestHaloMate(ctx, h, voices, (v) => v.halo);
76
76
  if (!found) continue;
77
77
  const voice = found.item;
78
- if (voice.node === s.payload || contains(ctx, voice.node, s.payload)) {
78
+ if (
79
+ voice.node === s.payload || contains(ctx, voice.node, s.payload) ||
80
+ answers(ctx, voice.node, s.payload)
81
+ ) {
79
82
  continue;
80
83
  }
84
+ // A form spanning the WHOLE answer is not a concept inside the answer to
85
+ // revoice — substituting it discards the answer and emits the asker's own
86
+ // words back, which is what a conversational store makes tempting: an
87
+ // answer and the question it answers keep maximal company, so the whole
88
+ // answer resonates with the whole query above any concept threshold
89
+ // (measured on the CONV fixture at 0.809 against 0.516). Articulation
90
+ // splices the asker's wording INTO an answer where the same concept
91
+ // appears; when the "concept" is the entire answer there is nothing left
92
+ // of it, and "where is it kept now" comes back in place of "it hangs in
93
+ // madrid". §5's contract is re-voicing, never replacement.
94
+ if (s.start === 0 && s.end === answer.length) continue;
81
95
  substitutions.set(s.payload, voice.bytes);
82
96
  }
83
97
  if (substitutions.size === 0) {
@@ -3,7 +3,7 @@
3
3
  // Learning is DEPOSITION: perceive a stream into a tree and intern every
4
4
  // node. A fact is an EDGE between node ids; recall traverses edges.
5
5
 
6
- import { Vec } from "../vec.js";
6
+ import { addInto, normalize, Vec, zeros } from "../vec.js";
7
7
  import { bindSeat, companySignature, isChunk, Sema } from "../sema.js";
8
8
  import type { Input, MindContext } from "./types.js";
9
9
  import { changedNodes } from "./types.js";
@@ -15,6 +15,8 @@ import {
15
15
  resolve,
16
16
  } from "./primitives.js";
17
17
  import { canonicalWindows, leafIdPrefix } from "./canonical.js";
18
+ import { hubBound } from "./traverse.js";
19
+ import { dominates } from "../geometry.js";
18
20
  import { fold as foldVecs } from "../sema.js";
19
21
 
20
22
  /** Intern a perceived tree into node ids, bottom-up, sharing equal subtrees.
@@ -233,6 +235,156 @@ export interface DepositReport {
233
235
  continuationId?: number;
234
236
  }
235
237
 
238
+ /** How many constituents one profile may VISIT. A partner's constituent tree
239
+ * is O(len/W) nodes, so an uncapped descent would make a pour cost grow with
240
+ * the partner's LENGTH — and a partner is a whole deposit, which may be a
241
+ * paragraph. The budget is what keeps a pour O(1) in the input, the property
242
+ * that lets {@link companyProfile} claim no new cost class. It binds only on
243
+ * long partners whose constituents are all corpus-unique; the descent's own
244
+ * stop rule (below) reaches recurring units far sooner on a trained store. */
245
+ const PROFILE_VISITS = 64;
246
+
247
+ /** The COMPANY PROFILE of a partner: its own identity signature superposed
248
+ * with the signatures of its RECURRING content-defined constituents.
249
+ *
250
+ * WHY THE WHOLE-PARTNER SIGNATURE ALONE IS NOT ENOUGH. The distributional
251
+ * hypothesis is a claim about TYPES ("occurs near a city name"), but a
252
+ * signature keyed on the whole partner's node id records a TOKEN ("occurred
253
+ * near node #4711992"). Two nodes are then distributional siblings only when
254
+ * their partners are the very same node — and a content-addressed store of
255
+ * natural language almost never repeats a whole deposit (measured on the
256
+ * trained store: whole-span dedup 0.98×, i.e. effectively none). So the
257
+ * halos of genuine synonyms came out quasi-orthogonal BY CONSTRUCTION: the
258
+ * best distributional sibling of "Eiffel Tower" scored 0.146 against a
259
+ * concept threshold of 0.516, with its own attested translations absent
260
+ * entirely, and the whole concept-hop / articulation / analogy layer was
261
+ * inert at corpus scale. (Re-verified under the store's OWN training seed:
262
+ * company signatures key on NODE ID, not the alphabet, so this reading is
263
+ * seed-independent and the figures are identical either way. Worth stating
264
+ * because a Mind built with a seed other than the store's makes every GIST
265
+ * comparison meaningless while leaving halo comparisons untouched.)
266
+ *
267
+ * WHY THE DESCENT MUST NOT STOP AT DEPTH 1. Reading only `rec.kids` does
268
+ * NOT deliver this. Cuts are content-defined over a rolling window, so a
269
+ * chunk boundary depends on the bytes AROUND a unit: "The Eiffel Tower is in
270
+ * Paris" folds to "The Eiffel " + "Tower is in Paris", and "Tour Eiffel dia
271
+ * any Paris" to "Tour Eiffel " + "dia any Paris". The shared unit "Paris"
272
+ * is a node in NEITHER — depth-1 profiles of that pair intersect in the
273
+ * EMPTY SET, and their halos measured 0.0319 against 0.0416 for an unrelated
274
+ * control: no signal at all. A depth-1 read merely moves the token problem
275
+ * from whole-partner identity down to top-level-chunk identity, which for
276
+ * full sentences is nearly as rare. Descending, the same pair shares
277
+ * " Paris" and "ffel " while the control still shares nothing — the units
278
+ * the distributional hypothesis is actually about.
279
+ *
280
+ * EVERY DEPTH CONTRIBUTES, AND THE RULE MUST NOT DEPEND ON ARRIVAL ORDER.
281
+ * The tempting stop rule — descend only while a constituent is corpus-unique,
282
+ * stop at the first unit attested in ≥ 2 forms — is wrong, and measurably so.
283
+ * Recurrence is a property of the corpus SO FAR: when the first of a pair is
284
+ * deposited its shared unit has fan-in 1, so the descent runs past it, and
285
+ * only the second partner ever profiles it. The pair then never meets
286
+ * (measured on the fixture above: 0.0165 against a 0.0375 control — still
287
+ * nothing). Whether two synonyms become distributional siblings cannot be
288
+ * allowed to depend on which was trained first. So the walk descends through
289
+ * EVERY constituent within its budget and superposes each one that is not a
290
+ * hub, at whatever depth it sits. A partner's own unique chunks contribute
291
+ * terms unique to that partner, which dilute but never mislead; the shared
292
+ * units contribute the signal.
293
+ *
294
+ * HUBS ARE THE ONE EXCLUSION, read LIMITed as `parentsFirst(n, bound+1)` —
295
+ * the store's own exact hub-or-not probe (a result longer than the bound
296
+ * means MORE than the bound), never a fan-in-sized read. A constituent with
297
+ * more than √N structural parents is scaffolding by §8.8's bound: " is ",
298
+ * "the ". Superposing it would put a term shared by every deposit into every
299
+ * profile, ALL halos would correlate, and the concept threshold's null model
300
+ * (unrelated halos at 0 ± 1/√D) that §4.1's hygiene note protects would
301
+ * collapse. It is still DESCENDED into — a hub chunk can contain a rare
302
+ * unit — but contributes nothing itself.
303
+ *
304
+ * Byte atoms are skipped in BOTH representations (a negative id and a stored
305
+ * kid-less node): an atom's fan-in is the alphabet's, so it can only ever
306
+ * read as a hub, and a short partner folding FLAT would otherwise put a
307
+ * handful of alphabet signatures into every profile — which is what silenced
308
+ * CAST's analogy gate in the first version of this function (measured:
309
+ * analogy strength 0.3636 -> 0.2004, "no halo-tier company evidence",
310
+ * test/29 C1).
311
+ *
312
+ * A FUNCTION OF THE NODE AND THE CORPUS STATE — stated precisely, because
313
+ * the weaker claim is the true one. The constituents are read from the
314
+ * STORE, never from the depositing tree's id map: that map holds only the
315
+ * nodes THIS deposit newly interned, so a partner met a second time yielded a
316
+ * profile missing exactly those constituents, the exact-partner case fell
317
+ * from cosine 1 to 1/√(1+k), and the geometry stopped meaning anything.
318
+ * Reading the store fixes that. It does NOT make the profile permanent: the
319
+ * hub test reads fan-in against √N and both grow with training, so a partner
320
+ * poured early and again late can profile differently. That residue is
321
+ * confined to the hub EXCLUSION — which terms are dropped as scaffolding —
322
+ * and never to which units are found, because the descent itself is now
323
+ * order-independent. The drift is one-directional and benign: a term can
324
+ * only ever go from contributing to being excluded as scaffolding. Replay of
325
+ * a fixed training order is bit-identical, so §2.1 holds. What must not be
326
+ * claimed is that a node's profile is fixed for all time; it is fixed given
327
+ * the corpus that has been seen.
328
+ *
329
+ * THE NULL MODEL IS OTHERWISE UNTOUCHED (§4.1). Every term is still a seeded
330
+ * function of a NODE IDENTITY, never a gist, so no byte-similarity between
331
+ * partners can leak content similarity into distributional similarity. The
332
+ * result is normalized, so ONE episode still pours ONE unit of mass:
333
+ * {@link Store.haloMass} keeps counting episodes and every mass-based
334
+ * reading is unchanged. Two partners sharing j of k discriminating
335
+ * constituents meet at j/(1+k) — graded evidence, above the 1/√D noise floor
336
+ * and below conceptThreshold until the overlap is most of the content, which
337
+ * is the semantics "same company" should have.
338
+ *
339
+ * Bounded: at most {@link PROFILE_VISITS} constituents are classified, each
340
+ * by ONE LIMITed structural-parent read, so a pour costs O(1) reads in the
341
+ * partner's size and performs no scan. */
342
+ function companyProfile(ctx: MindContext, id: number): Vec {
343
+ const acc = zeros(ctx.space.D);
344
+ addInto(acc, companySignature(ctx.space, id));
345
+ const bound = hubBound(ctx);
346
+ const W = ctx.space.maxGroup;
347
+ const whole = Math.max(1, ctx.store.contentLen(id));
348
+ const frontier: number[] = [];
349
+ const seen = new Set<number>([id]);
350
+ const descend = (n: number) => {
351
+ const kids = ctx.store.get(n)?.kids;
352
+ if (!kids) return;
353
+ for (const kid of kids) if (!seen.has(kid)) frontier.push(kid);
354
+ };
355
+ descend(id);
356
+ for (let visits = 0; visits < PROFILE_VISITS && frontier.length > 0;) {
357
+ const n = frontier.shift()!;
358
+ if (seen.has(n)) continue;
359
+ seen.add(n);
360
+ visits++;
361
+ // Atoms in both representations — negative id, or a stored kid-less node.
362
+ if (n < 0 || ctx.store.get(n)?.kids == null) continue;
363
+ descend(n);
364
+ const len = ctx.store.contentLen(n, whole);
365
+ if (len < W || dominates(len, whole)) continue;
366
+ // MINIMAL units only: a constituent that still has a constituent of its
367
+ // own at or above W is a composite, and superposing it as well as its
368
+ // parts would count the same content twice. Nested partners — an
369
+ // accumulated conversation, where turn k's context is a prefix of turn
370
+ // k+1's — share their large chunks structurally rather than
371
+ // distributionally, so those composites are exactly the terms that make
372
+ // adjacent turns read as synonyms (measured: consecutive turns at 0.809
373
+ // and 0.740 against a 0.516 concept threshold). The smallest units at or
374
+ // above the fold's own window are the word-sized types company should be
375
+ // keyed at.
376
+ const kids = ctx.store.get(n)!.kids!;
377
+ let composite = false;
378
+ for (const kid of kids) {
379
+ if (kid >= 0 && ctx.store.contentLen(kid, W) >= W) composite = true;
380
+ }
381
+ if (composite) continue;
382
+ if (ctx.store.parentsFirst(n, bound + 1).length > bound) continue;
383
+ addInto(acc, companySignature(ctx.space, n));
384
+ }
385
+ return normalize(acc);
386
+ }
387
+
236
388
  /** Ingest a pair (context, continuation) — learn an edge and pour halos.
237
389
  * Returns the deposited root ids (context, continuation) — a pure
238
390
  * read-out; callers that ignore it behave exactly as before. */
@@ -255,14 +407,17 @@ export async function ingestPair(
255
407
  await propagateSuffixes(ctx, ctxId, contId);
256
408
 
257
409
  // Halos pour company SIGNATURES (identity), not gists (content) — see
258
- // companySignature in sema.ts.
259
- const contSeat = bindSeat(ctx.space, companySignature(ctx.space, contId), 1);
410
+ // companySignature in sema.ts — as a TYPE-level profile: the partner's own
411
+ // signature superposed with its discriminating constituents' (see
412
+ // companyProfile), so company is shared by what partners are MADE OF and
413
+ // not only by partner identity.
414
+ const contSeat = bindSeat(ctx.space, companyProfile(ctx, contId), 1);
260
415
  for (const part of c.changed) {
261
416
  const partId = c.ids.get(part)!;
262
417
  await ctx.store.pourHalo(partId, contSeat);
263
418
  await ctx.store.pourHalo(
264
419
  contId,
265
- bindSeat(ctx.space, companySignature(ctx.space, partId), 0),
420
+ bindSeat(ctx.space, companyProfile(ctx, partId), 0),
266
421
  );
267
422
  }
268
423
  return { ctxId, contId };
@@ -558,6 +558,35 @@ export function contains(
558
558
  return false;
559
559
  }
560
560
 
561
+ /** Whether a continuation edge joins the two forms, in either direction —
562
+ * the EXACT half's veto on calling them synonyms.
563
+ *
564
+ * Halos measure company, and the strongest company any two forms can keep is
565
+ * standing next to each other: a question and its answer co-occur in every
566
+ * episode that taught the pair, so their halos SHOULD be similar, and on a
567
+ * conversational store they are (measured on the CONV fixture: consecutive
568
+ * turns at 0.809 against a 0.516 concept threshold). A gate reading halo
569
+ * cosine alone therefore reads adjacency as synonymy and revoices an answer
570
+ * in the words of the question it answers — "it hangs in madrid" spliced back
571
+ * into "where is it kept now". The distributional layer cannot tell the two
572
+ * relations apart, because to it they are the same observation; the exact
573
+ * half can, for free, because it stored the edge. §4.1's division of labour
574
+ * exactly: approximate proposes, exact decides.
575
+ *
576
+ * Read LIMITed in both directions at the hub bound — a common continuation's
577
+ * fan-in is corpus-sized, and no single decision may scale with it. */
578
+ export function answers(
579
+ ctx: MindContext,
580
+ a: number,
581
+ b: number,
582
+ ): boolean {
583
+ const bound = hubBound(ctx);
584
+ if (ctx.store.hasNext(a) && ctx.store.nextFirst(a, bound).includes(b)) {
585
+ return true;
586
+ }
587
+ return ctx.store.hasNext(b) && ctx.store.nextFirst(b, bound).includes(a);
588
+ }
589
+
561
590
  // ── Edge disambiguation (Section 6) ──────────────────────────────────────
562
591
 
563
592
  /** The best-scoring item by cosine against `query`, among items scoring at