@hviana/sema 0.5.7 → 0.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/AGENTS.md +23 -0
  2. package/DATASETS.md +159 -0
  3. package/HOW_IT_WORKS.md +74 -0
  4. package/README.md +12 -0
  5. package/dist/example/train_base.d.ts +73 -3
  6. package/dist/example/train_base.js +1000 -49
  7. package/dist/src/geometry.d.ts +20 -0
  8. package/dist/src/geometry.js +22 -0
  9. package/dist/src/mind/articulation.js +15 -2
  10. package/dist/src/mind/attention.d.ts +6 -0
  11. package/dist/src/mind/attention.js +44 -4
  12. package/dist/src/mind/learning.js +250 -3
  13. package/dist/src/mind/mechanisms/cast.js +45 -1
  14. package/dist/src/mind/mind.d.ts +6 -1
  15. package/dist/src/mind/mind.js +14 -2
  16. package/dist/src/mind/reasoning.js +59 -5
  17. package/dist/src/mind/recognition.js +29 -3
  18. package/dist/src/mind/traverse.d.ts +34 -0
  19. package/dist/src/mind/traverse.js +42 -0
  20. package/dist/src/store-sqlite.d.ts +4 -0
  21. package/dist/src/store-sqlite.js +47 -0
  22. package/dist/src/store.d.ts +7 -0
  23. package/example/train_base.ts +1193 -46
  24. package/jsr.json +1 -1
  25. package/package.json +1 -1
  26. package/src/geometry.ts +23 -0
  27. package/src/mind/articulation.ts +16 -2
  28. package/src/mind/attention.ts +54 -1
  29. package/src/mind/learning.ts +253 -4
  30. package/src/mind/mechanisms/cast.ts +48 -1
  31. package/src/mind/mind.ts +12 -1
  32. package/src/mind/reasoning.ts +64 -5
  33. package/src/mind/recognition.ts +29 -3
  34. package/src/mind/traverse.ts +48 -0
  35. package/src/store-sqlite.ts +53 -0
  36. package/src/store.ts +28 -0
  37. package/test/29-counterfactual.test.mjs +43 -6
  38. package/test/76-type-level-company.test.mjs +342 -0
  39. package/test/77-company-saturation.test.mjs +302 -0
  40. package/test/78-atom-hub-recognition-cliff.test.mjs +135 -0
  41. package/test/84-composed-answer-honesty.test.mjs +136 -0
  42. package/test/85-answered-directly.test.mjs +126 -0
  43. package/test/86-cast-voices-committed.test.mjs +164 -0
  44. package/test/87-codominant-commitment.test.mjs +250 -0
@@ -86,6 +86,26 @@ export declare function dominates(partLen: number, wholeLen: number): boolean;
86
86
  * recallByResonance trusting a climb anchor, and commitVotes admitting a
87
87
  * further point of attention. Defined once here so the two can never
88
88
  * drift apart. Derived from N, never tuned. */
89
+ /** SUPERPOSITION CAPACITY — how many quasi-orthogonal terms one vector can
90
+ * carry before an individual term stops being readable. `√D`.
91
+ *
92
+ * A superposition of m unit signatures has ‖acc‖² ≈ m, so one term's
93
+ * contribution to any cosine taken against that vector is ≈ 1/m. Setting
94
+ * that against the representation's own floor {@link estimatorNoise} = 1/√D:
95
+ *
96
+ * 1/m < 1/√D ⟺ m > √D
97
+ *
98
+ * Past √D terms a single shared constituent can no longer move a halo cosine
99
+ * above quantisation noise — and because the result is normalized, each extra
100
+ * term also shrinks every ALREADY-accepted term toward that floor. So this is
101
+ * not a budget that trades accuracy for time: beyond capacity, more evidence
102
+ * makes the representation strictly worse. It composes with
103
+ * {@link significanceBar} (3/√D) as it should — three shared units of √D is
104
+ * exactly the significance bar.
105
+ *
106
+ * Consumer: `companyProfile` (mind/learning.ts), which sizes its constituent
107
+ * sketch at this capacity instead of a visit budget. */
108
+ export declare function profileCapacity(D: number): number;
89
109
  export declare function consensusFloor(N: number): number;
90
110
  /** The coverage bar for the reach (interior) index, when vector-similarity
91
111
  * gating is used. Returns the concept threshold — the structural midpoint
@@ -121,6 +121,28 @@ export function dominates(partLen, wholeLen) {
121
121
  * recallByResonance trusting a climb anchor, and commitVotes admitting a
122
122
  * further point of attention. Defined once here so the two can never
123
123
  * drift apart. Derived from N, never tuned. */
124
+ /** SUPERPOSITION CAPACITY — how many quasi-orthogonal terms one vector can
125
+ * carry before an individual term stops being readable. `√D`.
126
+ *
127
+ * A superposition of m unit signatures has ‖acc‖² ≈ m, so one term's
128
+ * contribution to any cosine taken against that vector is ≈ 1/m. Setting
129
+ * that against the representation's own floor {@link estimatorNoise} = 1/√D:
130
+ *
131
+ * 1/m < 1/√D ⟺ m > √D
132
+ *
133
+ * Past √D terms a single shared constituent can no longer move a halo cosine
134
+ * above quantisation noise — and because the result is normalized, each extra
135
+ * term also shrinks every ALREADY-accepted term toward that floor. So this is
136
+ * not a budget that trades accuracy for time: beyond capacity, more evidence
137
+ * makes the representation strictly worse. It composes with
138
+ * {@link significanceBar} (3/√D) as it should — three shared units of √D is
139
+ * exactly the significance bar.
140
+ *
141
+ * Consumer: `companyProfile` (mind/learning.ts), which sizes its constituent
142
+ * sketch at this capacity instead of a visit budget. */
143
+ export function profileCapacity(D) {
144
+ return Math.max(1, Math.floor(Math.sqrt(D)));
145
+ }
124
146
  export function consensusFloor(N) {
125
147
  return Math.log(N) + 1 / 2;
126
148
  }
@@ -4,7 +4,7 @@
4
4
  // using concept (halo) resonance to match the voices.
5
5
  import { spliceAll } from "./types.js";
6
6
  import { recognise } from "./recognition.js";
7
- import { contains } from "./traverse.js";
7
+ import { answers, contains } from "./traverse.js";
8
8
  import { bestHaloMate } from "./match.js";
9
9
  import { coverSequence } from "../derive/src/index.js";
10
10
  import { rItem, rNode, traceDerivation } from "./trace.js";
@@ -67,9 +67,22 @@ export async function articulate(ctx, answer, query) {
67
67
  if (!found)
68
68
  continue;
69
69
  const voice = found.item;
70
- if (voice.node === s.payload || contains(ctx, voice.node, s.payload)) {
70
+ if (voice.node === s.payload || contains(ctx, voice.node, s.payload) ||
71
+ answers(ctx, voice.node, s.payload)) {
71
72
  continue;
72
73
  }
74
+ // A form spanning the WHOLE answer is not a concept inside the answer to
75
+ // revoice — substituting it discards the answer and emits the asker's own
76
+ // words back, which is what a conversational store makes tempting: an
77
+ // answer and the question it answers keep maximal company, so the whole
78
+ // answer resonates with the whole query above any concept threshold
79
+ // (measured on the CONV fixture at 0.809 against 0.516). Articulation
80
+ // splices the asker's wording INTO an answer where the same concept
81
+ // appears; when the "concept" is the entire answer there is nothing left
82
+ // of it, and "where is it kept now" comes back in place of "it hangs in
83
+ // madrid". §5's contract is re-voicing, never replacement.
84
+ if (s.start === 0 && s.end === answer.length)
85
+ continue;
73
86
  substitutions.set(s.payload, voice.bytes);
74
87
  }
75
88
  if (substitutions.size === 0) {
@@ -87,6 +87,12 @@ export interface ConsensusAnchorTrace {
87
87
  passesNaturalBreak?: boolean;
88
88
  passesConsensusFloor?: boolean;
89
89
  pastLeadingSaturation?: boolean;
90
+ /** Committed because its margin from the dominant is inside the estimator's
91
+ * own resolution — the co-dominant band in {@link commitVotes}. Recorded
92
+ * because commit decisions are kept in the exact shape the gates applied
93
+ * them: a root admitted this way must never read, in the trace, as one
94
+ * that cleared the two vote gates. */
95
+ tiedWithDominant?: boolean;
90
96
  rejectionReasons: AnchorRejectionReason[];
91
97
  };
92
98
  }
@@ -1169,7 +1169,7 @@ export function commitVotes(ctx, pooled, sat, regions, regionVoter, N, td, cfg)
1169
1169
  const floor = consensusFloor(N);
1170
1170
  const placed = [];
1171
1171
  const roots = [];
1172
- const recordAnchor = (point, rank, status, dominant, passesNaturalBreak, passesConsensusFloor, pastLeadingSaturation, rejectionReasons) => {
1172
+ const recordAnchor = (point, rank, status, dominant, passesNaturalBreak, passesConsensusFloor, pastLeadingSaturation, tiedWithDominant, rejectionReasons) => {
1173
1173
  if (!td)
1174
1174
  return;
1175
1175
  td.anchors.push({
@@ -1189,6 +1189,7 @@ export function commitVotes(ctx, pooled, sat, regions, regionVoter, N, td, cfg)
1189
1189
  passesNaturalBreak,
1190
1190
  passesConsensusFloor,
1191
1191
  pastLeadingSaturation,
1192
+ tiedWithDominant,
1192
1193
  rejectionReasons,
1193
1194
  },
1194
1195
  });
@@ -1204,6 +1205,7 @@ export function commitVotes(ctx, pooled, sat, regions, regionVoter, N, td, cfg)
1204
1205
  let passesNaturalBreak;
1205
1206
  let passesConsensusFloor;
1206
1207
  let pastLeadingSaturation;
1208
+ let tiedWithDominant;
1207
1209
  const rejectionReasons = [];
1208
1210
  if (absorbed) {
1209
1211
  status = "overlap";
@@ -1229,7 +1231,45 @@ export function commitVotes(ctx, pooled, sat, regions, regionVoter, N, td, cfg)
1229
1231
  else {
1230
1232
  passesNaturalBreak = vote >= rootCut;
1231
1233
  passesConsensusFloor = vote >= floor;
1232
- if (passesNaturalBreak && passesConsensusFloor && pastLeading) {
1234
+ // CO-DOMINANT an anchor the estimator cannot separate from the
1235
+ // dominant inherits the dominant's exemption, because that exemption's
1236
+ // only warrant is being TOP, and "top" is not a fact about the corpus
1237
+ // when the ordering moves with the seed.
1238
+ //
1239
+ // The dominant bypasses both vote gates ("it always grounds"); the
1240
+ // runner-up is held to an absolute ln(N)+1/2 floor the dominant never
1241
+ // had to clear. Which of them gets the exemption is then decided by a
1242
+ // sort over ESTIMATED quantities. Measured on test/29 D1's corpus,
1243
+ // 60 seeds per D — true separation 0.54s / 0.75s / 1.04s:
1244
+ //
1245
+ // D s=1/sqrt(D) vote SD (estimated anchor) SD/s flips
1246
+ // 256 0.0625 0.0561 0.90 19/60
1247
+ // 1024 0.0313 0.0268 0.86 12/60
1248
+ // 4096 0.0156 0.0074 0.48 2/60
1249
+ //
1250
+ // The SD tracks 1/sqrt(D) and the flip rate collapses with it, so the
1251
+ // reordering is the ESTIMATOR's, not the corpus's. The loser was then
1252
+ // refused by a floor at 1.599 that neither anchor could ever reach
1253
+ // (corpusN 3) — a coin flip decided which single structure the query
1254
+ // was allowed to have settled on.
1255
+ //
1256
+ // THE BAND IS sqrt(k)*s, NOT s. A vote is a SUM over the anchor's own
1257
+ // contributing regions, so its noise grows as sqrt(k); pricing a summed
1258
+ // margin against one s would be the category error chooseNext's comment
1259
+ // warns about. k is `regionAxioms`, already in hand; s is
1260
+ // `estimatorNoise(D)`, already derived. No constant is introduced.
1261
+ // Verified conservative: measured SD/(sqrt(k)*s) never exceeded 0.72.
1262
+ //
1263
+ // BOUNDED BY CONSTRUCTION: admission requires indistinguishability from
1264
+ // an anchor ALREADY admitted, so it can only admit what the ordinary
1265
+ // rule would have admitted had the noise fallen the other way. It is
1266
+ // N-independent for the same reason — a statement about the estimator,
1267
+ // not about corpus size.
1268
+ const tieBand = Math.sqrt(Math.max(1, regionAxioms.get(point.anchor) ?? 1)) * estimatorNoise(ctx.store.D);
1269
+ const dominantVote = votesIdf.get(roots[0].anchor) ?? 0;
1270
+ tiedWithDominant = dominantVote - vote < tieBand;
1271
+ if (((passesNaturalBreak && passesConsensusFloor) || tiedWithDominant) &&
1272
+ pastLeading) {
1233
1273
  status = "root";
1234
1274
  }
1235
1275
  else {
@@ -1247,11 +1287,11 @@ export function commitVotes(ctx, pooled, sat, regions, regionVoter, N, td, cfg)
1247
1287
  roots.push(point);
1248
1288
  }
1249
1289
  else {
1250
- recordAnchor(point, rank, status, dominant, passesNaturalBreak, passesConsensusFloor, pastLeadingSaturation, rejectionReasons);
1290
+ recordAnchor(point, rank, status, dominant, passesNaturalBreak, passesConsensusFloor, pastLeadingSaturation, tiedWithDominant, rejectionReasons);
1251
1291
  continue;
1252
1292
  }
1253
1293
  }
1254
- recordAnchor(point, rank, status, dominant, passesNaturalBreak, passesConsensusFloor, pastLeadingSaturation, rejectionReasons);
1294
+ recordAnchor(point, rank, status, dominant, passesNaturalBreak, passesConsensusFloor, pastLeadingSaturation, tiedWithDominant, rejectionReasons);
1255
1295
  placed.push(point);
1256
1296
  }
1257
1297
  traceAttention(ctx, regions, regionVoter, roots, steps, td, cfg ? { ...cfg, naturalBreak: rootCut, consensusFloor: floor } : undefined, ranked);
@@ -2,10 +2,14 @@
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 { rItem, rNode } from "./trace.js";
11
+ import { hubBound } from "./traverse.js";
12
+ import { dominates, estimatorNoise, profileCapacity } from "../geometry.js";
9
13
  import { fold as foldVecs } from "../sema.js";
10
14
  /** Intern a perceived tree into node ids, bottom-up, sharing equal subtrees.
11
15
  * Returns the root node id and a map from tree nodes to their ids.
@@ -188,6 +192,246 @@ async function propagateSuffixes(ctx, src, dst) {
188
192
  await ctx.store.link(id, dst);
189
193
  }
190
194
  }
195
+ /** Deterministic priority of a node for bottom-k selection — a fixed integer
196
+ * mix of the node id, NOT a function of the config seed.
197
+ *
198
+ * Seed-independence is the point: the sketch is a property of the STORE, so
199
+ * two Minds over one store must agree on it, and a rebuilt sketch must match
200
+ * a stored one. (Contrast {@link companySignature}, which is seeded — that is
201
+ * the VECTOR, this is only the CHOICE of which vectors to superpose.)
202
+ *
203
+ * Selecting the k smallest priorities makes the sketch a bottom-k sample keyed
204
+ * on each constituent's own identity, so a unit shared by two partners is kept
205
+ * by BOTH or neither, whatever its depth or position in either fold. That is
206
+ * what removes the traversal-order dependence a visit budget necessarily had. */
207
+ function unitPriority(id) {
208
+ let h = (id ^ 0x9e3779b9) >>> 0;
209
+ h = Math.imul(h ^ (h >>> 16), 0x85ebca6b) >>> 0;
210
+ h = Math.imul(h ^ (h >>> 13), 0xc2b2ae35) >>> 0;
211
+ return (h ^ (h >>> 16)) >>> 0;
212
+ }
213
+ /** Whether `n` is a MINIMAL UNIT: a stored branch, at least one fold window
214
+ * wide, no constituent of its own at or above W. Every clause is INTRINSIC —
215
+ * a property of the node's own subtree — which is what lets a sketch be stored
216
+ * once and stay valid as the corpus grows. The two corpus-relative readings
217
+ * (half-dominance of the PARTNER, and the hub test) are deliberately excluded
218
+ * and applied by {@link companyProfile} at pour time. */
219
+ function isMinimalUnit(ctx, n, W) {
220
+ if (n < 0)
221
+ return false; // byte atom — fan-in is the alphabet's
222
+ const kids = ctx.store.get(n)?.kids;
223
+ if (kids == null)
224
+ return false; // stored kid-less node: also an atom
225
+ if (ctx.store.contentLen(n, W) < W)
226
+ return false;
227
+ for (const kid of kids) {
228
+ if (kid >= 0 && ctx.store.contentLen(kid, W) >= W)
229
+ return false; // composite
230
+ }
231
+ return true;
232
+ }
233
+ /** The BOTTOM-K CONSTITUENT SKETCH of a node: the `k = profileCapacity(D)`
234
+ * minimal units of its subtree with the smallest {@link unitPriority}.
235
+ *
236
+ * COMPOSABLE, WHICH IS WHY IT COSTS NOTHING TWICE. Bottom-k of a union is
237
+ * the bottom-k of the children's bottom-k sets, so a node's sketch is built
238
+ * from its kids' sketches and each recursive result is stored on the way out.
239
+ * A partner met again reads O(k); an accumulated conversation, where turn k's
240
+ * context is a prefix of turn k+1's, reuses every unchanged child and pays
241
+ * O(changed) instead of O(context) — the quadratic that made a visit budget
242
+ * look necessary in the first place.
243
+ *
244
+ * It is DURABLE DERIVED STATE, not a cache (see Store.sketchGet): a miss must
245
+ * cost time only, and this decides which terms enter a halo. A backend
246
+ * without the capability recomputes per pour and loses only the amortisation.
247
+ *
248
+ * Recursion depth is the fold's, O(log_W len), and each level does O(k·arity)
249
+ * work, so construction is one pass over the subtree — the same pass the
250
+ * deposit that interned it already performed. */
251
+ function constituentSketch(ctx, id, k) {
252
+ const stored = ctx.store.sketchGet?.(id);
253
+ if (stored != null)
254
+ return stored; // [] is a real answer; null is "unknown"
255
+ const W = ctx.space.maxGroup;
256
+ const kids = id < 0 ? null : ctx.store.get(id)?.kids;
257
+ let out;
258
+ if (kids == null) {
259
+ out = [];
260
+ }
261
+ else {
262
+ const pool = [];
263
+ for (const kid of kids) {
264
+ if (isMinimalUnit(ctx, kid, W))
265
+ pool.push(kid);
266
+ else if (kid >= 0) {
267
+ for (const g of constituentSketch(ctx, kid, k))
268
+ pool.push(g);
269
+ }
270
+ }
271
+ // Bottom-k by identity, then by id so ties are corpus-determined (§2.1).
272
+ pool.sort((a, b) => (unitPriority(a) - unitPriority(b)) || (a - b));
273
+ const seen = new Set();
274
+ out = [];
275
+ for (const n of pool) {
276
+ if (seen.has(n))
277
+ continue;
278
+ seen.add(n);
279
+ out.push(n);
280
+ if (out.length >= k)
281
+ break;
282
+ }
283
+ }
284
+ ctx.store.sketchPut?.(id, out);
285
+ return out;
286
+ }
287
+ /** The COMPANY PROFILE of a partner: its own identity signature superposed
288
+ * with the signatures of its RECURRING content-defined constituents.
289
+ *
290
+ * WHY THE WHOLE-PARTNER SIGNATURE ALONE IS NOT ENOUGH. The distributional
291
+ * hypothesis is a claim about TYPES ("occurs near a city name"), but a
292
+ * signature keyed on the whole partner's node id records a TOKEN ("occurred
293
+ * near node #4711992"). Two nodes are then distributional siblings only when
294
+ * their partners are the very same node — and a content-addressed store of
295
+ * natural language almost never repeats a whole deposit (measured on the
296
+ * trained store: whole-span dedup 0.98×, i.e. effectively none). So the
297
+ * halos of genuine synonyms came out quasi-orthogonal BY CONSTRUCTION: the
298
+ * best distributional sibling of "Eiffel Tower" scored 0.146 against a
299
+ * concept threshold of 0.516, with its own attested translations absent
300
+ * entirely, and the whole concept-hop / articulation / analogy layer was
301
+ * inert at corpus scale. (Re-verified under the store's OWN training seed:
302
+ * company signatures key on NODE ID, not the alphabet, so this reading is
303
+ * seed-independent and the figures are identical either way. Worth stating
304
+ * because a Mind built with a seed other than the store's makes every GIST
305
+ * comparison meaningless while leaving halo comparisons untouched.)
306
+ *
307
+ * WHY THE DESCENT MUST NOT STOP AT DEPTH 1. Reading only `rec.kids` does
308
+ * NOT deliver this. Cuts are content-defined over a rolling window, so a
309
+ * chunk boundary depends on the bytes AROUND a unit: "The Eiffel Tower is in
310
+ * Paris" folds to "The Eiffel " + "Tower is in Paris", and "Tour Eiffel dia
311
+ * any Paris" to "Tour Eiffel " + "dia any Paris". The shared unit "Paris"
312
+ * is a node in NEITHER — depth-1 profiles of that pair intersect in the
313
+ * EMPTY SET, and their halos measured 0.0319 against 0.0416 for an unrelated
314
+ * control: no signal at all. A depth-1 read merely moves the token problem
315
+ * from whole-partner identity down to top-level-chunk identity, which for
316
+ * full sentences is nearly as rare. Descending, the same pair shares
317
+ * " Paris" and "ffel " while the control still shares nothing — the units
318
+ * the distributional hypothesis is actually about.
319
+ *
320
+ * EVERY DEPTH CONTRIBUTES, AND THE RULE MUST NOT DEPEND ON ARRIVAL ORDER.
321
+ * The tempting stop rule — descend only while a constituent is corpus-unique,
322
+ * stop at the first unit attested in ≥ 2 forms — is wrong, and measurably so.
323
+ * Recurrence is a property of the corpus SO FAR: when the first of a pair is
324
+ * deposited its shared unit has fan-in 1, so the descent runs past it, and
325
+ * only the second partner ever profiles it. The pair then never meets
326
+ * (measured on the fixture above: 0.0165 against a 0.0375 control — still
327
+ * nothing). Whether two synonyms become distributional siblings cannot be
328
+ * allowed to depend on which was trained first. So the walk descends through
329
+ * EVERY constituent within its budget and superposes each one that is not a
330
+ * hub, at whatever depth it sits. A partner's own unique chunks contribute
331
+ * terms unique to that partner, which dilute but never mislead; the shared
332
+ * units contribute the signal.
333
+ *
334
+ * HUBS ARE THE ONE EXCLUSION, read LIMITed as `parentsFirst(n, bound+1)` —
335
+ * the store's own exact hub-or-not probe (a result longer than the bound
336
+ * means MORE than the bound), never a fan-in-sized read. A constituent with
337
+ * more than √N structural parents is scaffolding by §8.8's bound: " is ",
338
+ * "the ". Superposing it would put a term shared by every deposit into every
339
+ * profile, ALL halos would correlate, and the concept threshold's null model
340
+ * (unrelated halos at 0 ± 1/√D) that §4.1's hygiene note protects would
341
+ * collapse. It is still DESCENDED into — a hub chunk can contain a rare
342
+ * unit — but contributes nothing itself.
343
+ *
344
+ * Byte atoms are skipped in BOTH representations (a negative id and a stored
345
+ * kid-less node): an atom's fan-in is the alphabet's, so it can only ever
346
+ * read as a hub, and a short partner folding FLAT would otherwise put a
347
+ * handful of alphabet signatures into every profile — which is what silenced
348
+ * CAST's analogy gate in the first version of this function (measured:
349
+ * analogy strength 0.3636 -> 0.2004, "no halo-tier company evidence",
350
+ * test/29 C1).
351
+ *
352
+ * A FUNCTION OF THE NODE AND THE CORPUS STATE — stated precisely, because
353
+ * the weaker claim is the true one. The constituents are read from the
354
+ * STORE, never from the depositing tree's id map: that map holds only the
355
+ * nodes THIS deposit newly interned, so a partner met a second time yielded a
356
+ * profile missing exactly those constituents, the exact-partner case fell
357
+ * from cosine 1 to 1/√(1+k), and the geometry stopped meaning anything.
358
+ * Reading the store fixes that. It does NOT make the profile permanent: the
359
+ * hub test reads fan-in against √N and both grow with training, so a partner
360
+ * poured early and again late can profile differently. That residue is
361
+ * confined to the hub EXCLUSION — which terms are dropped as scaffolding —
362
+ * and never to which units are found, because the descent itself is now
363
+ * order-independent. The drift is one-directional and benign: a term can
364
+ * only ever go from contributing to being excluded as scaffolding. Replay of
365
+ * a fixed training order is bit-identical, so §2.1 holds. What must not be
366
+ * claimed is that a node's profile is fixed for all time; it is fixed given
367
+ * the corpus that has been seen.
368
+ *
369
+ * THE NULL MODEL IS OTHERWISE UNTOUCHED (§4.1). Every term is still a seeded
370
+ * function of a NODE IDENTITY, never a gist, so no byte-similarity between
371
+ * partners can leak content similarity into distributional similarity. The
372
+ * result is normalized, so ONE episode still pours ONE unit of mass:
373
+ * {@link Store.haloMass} keeps counting episodes and every mass-based
374
+ * reading is unchanged. Two partners sharing j of k discriminating
375
+ * constituents meet at j/(1+k) — graded evidence, above the 1/√D noise floor
376
+ * and below conceptThreshold until the overlap is most of the content, which
377
+ * is the semantics "same company" should have.
378
+ *
379
+ * Bounded: at most {@link PROFILE_VISITS} constituents are classified, each
380
+ * by ONE LIMITed structural-parent read, so a pour costs O(1) reads in the
381
+ * partner's size and performs no scan. */
382
+ function companyProfile(ctx, id) {
383
+ const acc = zeros(ctx.space.D);
384
+ addInto(acc, companySignature(ctx.space, id));
385
+ const bound = hubBound(ctx);
386
+ const k = profileCapacity(ctx.space.D);
387
+ const whole = Math.max(1, ctx.store.contentLen(id));
388
+ const sketch = constituentSketch(ctx, id, k);
389
+ // The two CORPUS-RELATIVE readings, applied here and never stored: which
390
+ // terms count as scaffolding moves as N grows, which is the drift documented
391
+ // above, while the sketch itself must stay intrinsic to remain valid.
392
+ let accepted = 0, hubDropped = 0, dominating = 0;
393
+ for (const n of sketch) {
394
+ const len = ctx.store.contentLen(n, whole);
395
+ if (dominates(len, whole)) {
396
+ dominating++;
397
+ continue;
398
+ }
399
+ if (ctx.store.parentsFirst(n, bound + 1).length > bound) {
400
+ hubDropped++;
401
+ continue;
402
+ }
403
+ addInto(acc, companySignature(ctx.space, n));
404
+ accepted++;
405
+ }
406
+ // FALSIFIABILITY. The claim this function makes is that it stops because the
407
+ // representation is FULL, never because a budget ran out — so the diagnostics
408
+ // report the capacity, the mass actually reached, and what the frontier still
409
+ // held. `residual` is the evidence NOT superposed; `marginal` is what one
410
+ // more term would have contributed to a downstream cosine (1/mass), and
411
+ // `saturated` says whether that had fallen to or below `noiseFloor`. A run
412
+ // that reports `saturated: false` with `residual > 0` is this design being
413
+ // WRONG, not tuning: it would mean readable evidence was dropped.
414
+ const mass = accepted + 1; // the node's own signature counts
415
+ const marginal = 1 / mass;
416
+ const noiseFloor = estimatorNoise(ctx.space.D);
417
+ ctx.trace?.step("companyProfile", [rNode(ctx, id, "partner")], [rItem(new Uint8Array(0), "profile", id)], `superposed ${accepted} of ${sketch.length} sketched constituents ` +
418
+ `(capacity ${k}); marginal ${marginal.toFixed(4)} vs noise floor ` +
419
+ `${noiseFloor.toFixed(4)}`, undefined, {
420
+ capacity: k,
421
+ sketched: sketch.length,
422
+ accepted,
423
+ hubDropped,
424
+ dominating,
425
+ residual: sketch.length - accepted,
426
+ mass,
427
+ marginal,
428
+ noiseFloor,
429
+ saturated: sketch.length >= k,
430
+ stopReason: sketch.length >= k ? "capacity" : "constituents-exhausted",
431
+ wholeLen: whole,
432
+ });
433
+ return normalize(acc);
434
+ }
191
435
  /** Ingest a pair (context, continuation) — learn an edge and pour halos.
192
436
  * Returns the deposited root ids (context, continuation) — a pure
193
437
  * read-out; callers that ignore it behave exactly as before. */
@@ -203,12 +447,15 @@ export async function ingestPair(ctx, ctxInput, cont) {
203
447
  await ctx.store.link(ctxId, contId);
204
448
  await propagateSuffixes(ctx, ctxId, contId);
205
449
  // 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);
450
+ // companySignature in sema.ts — as a TYPE-level profile: the partner's own
451
+ // signature superposed with its discriminating constituents' (see
452
+ // companyProfile), so company is shared by what partners are MADE OF and
453
+ // not only by partner identity.
454
+ const contSeat = bindSeat(ctx.space, companyProfile(ctx, contId), 1);
208
455
  for (const part of c.changed) {
209
456
  const partId = c.ids.get(part);
210
457
  await ctx.store.pourHalo(partId, contSeat);
211
- await ctx.store.pourHalo(contId, bindSeat(ctx.space, companySignature(ctx.space, partId), 0));
458
+ await ctx.store.pourHalo(contId, bindSeat(ctx.space, companyProfile(ctx, partId), 0));
212
459
  }
213
460
  return { ctxId, contId };
214
461
  }
@@ -301,6 +301,36 @@ export async function counterfactualTransfer(ctx, query, pre) {
301
301
  }
302
302
  }
303
303
  const isRoot = (id) => roots.some((r) => r.anchor === id);
304
+ // VOICEABLE — a structure whose own learnt content a schema may SPEAK.
305
+ //
306
+ // The gate below asks only that the weave TOUCH a committed point. That is
307
+ // the right question for MEMBERSHIP — a weave needs uncommitted structure to
308
+ // compare against; that is what an analogy IS — and the wrong one for
309
+ // VOICING: satisfied by any committed bystander, it lets every OTHER aligned
310
+ // point put its own learnt content into the answer while a root that
311
+ // contributed nothing holds the door open. The refusal note below already
312
+ // states the principle — "CAST refuses to transfer through content the climb
313
+ // itself never settled on" — it was simply never asked of the structure a
314
+ // schema actually transfers THROUGH.
315
+ //
316
+ // Measured on a two-hop question over dialogue filler (N ~ 103,
317
+ // consensusFloor 5.13): the climb committed ONE root at vote 8.13, and
318
+ // substitution then voiced a filler deposit at vote 0.15 together with a
319
+ // second structure at 0.57 — neither committed, both an order of magnitude
320
+ // below the floor, while the licensing root supplied no bytes at all.
321
+ //
322
+ // OR THE QUERY NAMED IT. Commitment is not the only warrant: a structure the
323
+ // asker QUOTED is content the query did ask about, whoever the climb settled
324
+ // on. The naming test is the one redirection's own `named` list uses — an
325
+ // aligned run starting at the structure's OPENING bytes (`cs === 0`) — and
326
+ // NOT merely "has an aligned run", which every weave point has by
327
+ // construction. Without this disjunct the gate refuses test/29 B3 ("what if
328
+ // the capital of France were Lyon?" must answer about Lyon), where the
329
+ // substitute is named outright and the climb never commits it. This mirrors
330
+ // the pairing the comparison gate already makes with
331
+ // `!rootTrusted && !namedByQuery`.
332
+ const namedFromOpening = (p) => p.runs.some((r) => r.cs === 0 && usable(r.qs, r.qe));
333
+ const voiceable = (p) => isRoot(p.anchor) || namedFromOpening(p);
304
334
  // The weave must touch a COMMITTED point of attention: the dominant
305
335
  // structure itself, or another aligned point the climb committed to.
306
336
  if (!points.some((p) => isRoot(p.anchor))) {
@@ -450,6 +480,15 @@ export async function counterfactualTransfer(ctx, query, pre) {
450
480
  if (r.cs < quantum || !usable(r.qs, r.qe)) {
451
481
  return null;
452
482
  }
483
+ // The DISPLACED STRUCTURE is what this schema speaks — the answer is its
484
+ // tail past the seat plus its own continuation — so it must be
485
+ // voiceable. Filtered HERE rather than after the argmax so an eligible
486
+ // structure with less depth still fires the schema, instead of an
487
+ // ineligible deepest candidate suppressing it outright. The SUBJECT is
488
+ // deliberately not gated: `fillerOf` reads the QUERY's own bytes for it,
489
+ // so it contributes what the asker already said, not learnt content.
490
+ if (!voiceable(p))
491
+ return null;
453
492
  const before = beforeOf(p, r);
454
493
  if (before === undefined)
455
494
  return null;
@@ -534,7 +573,12 @@ export async function counterfactualTransfer(ctx, query, pre) {
534
573
  const domNext = ctx.store.nextFirst(dominant.anchor, hubBound(ctx));
535
574
  const displaced = domNext
536
575
  .every((n) => indexOf(query, read(ctx, n), 0) < 0);
537
- if (last !== undefined && last.point !== dominant && displaced) {
576
+ // The SUBSTITUTE is what redirection speaks the answer IS `project(last)`,
577
+ // its own fact — so the same bar applies. The displaced structure is only
578
+ // recognised as the slot being overridden and is never voiced, so it is
579
+ // deliberately not gated here.
580
+ if (last !== undefined && last.point !== dominant && displaced &&
581
+ voiceable(last.point)) {
538
582
  const g = await project(ctx, last.point.anchor, qv);
539
583
  if (g !== null) {
540
584
  ctx.trace?.step("projectCounterfactual", [
@@ -345,7 +345,12 @@ export declare class Mind implements MindContext {
345
345
  /** See {@link import("./learning.js").ingest} — `onDeposit`, when given,
346
346
  * reports each ingested item's deposited root node ids
347
347
  * ({@link DepositReport}); purely observational. */
348
- ingest(input: Input | (Input | [Input, Input])[], second?: Input, onDeposit?: (report: import("./learning.js").DepositReport) => void): Promise<(Sema & {
348
+ ingest(input: Input | (Input | [Input, Input])[], second?: Input, onDeposit?: (report: import("./learning.js").DepositReport) => void,
349
+ /** Witness the DEPOSIT path the way {@link respond}'s callback witnesses
350
+ * inference — `companyProfile` reports its saturation diagnostics here.
351
+ * Without it the tracer is never constructed and the emit sites cost
352
+ * nothing (§ rationale.ts), exactly as on the inference path. */
353
+ inspectRationale?: InspectRationale): Promise<(Sema & {
349
354
  id: number;
350
355
  }) | undefined>;
351
356
  private extensionHost;
@@ -644,10 +644,22 @@ export class Mind {
644
644
  /** See {@link import("./learning.js").ingest} — `onDeposit`, when given,
645
645
  * reports each ingested item's deposited root node ids
646
646
  * ({@link DepositReport}); purely observational. */
647
- async ingest(input, second, onDeposit) {
647
+ async ingest(input, second, onDeposit,
648
+ /** Witness the DEPOSIT path the way {@link respond}'s callback witnesses
649
+ * inference — `companyProfile` reports its saturation diagnostics here.
650
+ * Without it the tracer is never constructed and the emit sites cost
651
+ * nothing (§ rationale.ts), exactly as on the inference path. */
652
+ inspectRationale) {
648
653
  invalidateStructuralCaches(this);
649
654
  invalidateJunctionCache(this);
650
- return ingest(this, input, second, onDeposit);
655
+ const prevTrace = this.trace;
656
+ this.trace = inspectRationale ? new Rationale(inspectRationale) : null;
657
+ try {
658
+ return await ingest(this, input, second, onDeposit);
659
+ }
660
+ finally {
661
+ this.trace = prevTrace;
662
+ }
651
663
  }
652
664
  // ── Extension Surface ────────────────────────────────────────────────────
653
665
  extensionHost() {