@hviana/sema 0.3.0 → 0.4.1

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 (79) hide show
  1. package/AGENTS.md +71 -5
  2. package/CONTRIBUTING.md +92 -10
  3. package/LICENSE.md +2 -2
  4. package/dist/src/derive/src/deduction.d.ts +12 -1
  5. package/dist/src/derive/src/deduction.js +5 -1
  6. package/dist/src/derive/src/index.d.ts +1 -0
  7. package/dist/src/geometry.d.ts +3 -30
  8. package/dist/src/geometry.js +330 -82
  9. package/dist/src/index.d.ts +1 -0
  10. package/dist/src/index.js +1 -0
  11. package/dist/src/meter.d.ts +171 -0
  12. package/dist/src/meter.js +269 -0
  13. package/dist/src/mind/attention.d.ts +0 -4
  14. package/dist/src/mind/attention.js +251 -23
  15. package/dist/src/mind/bridge.d.ts +10 -1
  16. package/dist/src/mind/bridge.js +179 -10
  17. package/dist/src/mind/canonical.d.ts +6 -1
  18. package/dist/src/mind/canonical.js +6 -1
  19. package/dist/src/mind/graph-search.d.ts +9 -0
  20. package/dist/src/mind/graph-search.js +59 -19
  21. package/dist/src/mind/junction.d.ts +10 -0
  22. package/dist/src/mind/junction.js +14 -0
  23. package/dist/src/mind/match.d.ts +40 -0
  24. package/dist/src/mind/match.js +125 -1
  25. package/dist/src/mind/mechanisms/cast.js +63 -6
  26. package/dist/src/mind/mechanisms/extraction.d.ts +0 -34
  27. package/dist/src/mind/mechanisms/extraction.js +1 -88
  28. package/dist/src/mind/mechanisms/recall.d.ts +3 -0
  29. package/dist/src/mind/mechanisms/recall.js +77 -14
  30. package/dist/src/mind/mind.d.ts +55 -4
  31. package/dist/src/mind/mind.js +110 -91
  32. package/dist/src/mind/pipeline-mechanism.d.ts +33 -3
  33. package/dist/src/mind/pipeline-mechanism.js +179 -10
  34. package/dist/src/mind/pipeline.js +52 -10
  35. package/dist/src/mind/primitives.d.ts +11 -15
  36. package/dist/src/mind/primitives.js +47 -28
  37. package/dist/src/mind/reasoning.d.ts +7 -1
  38. package/dist/src/mind/reasoning.js +40 -8
  39. package/dist/src/mind/recognition.js +93 -20
  40. package/dist/src/mind/traverse.d.ts +11 -0
  41. package/dist/src/mind/traverse.js +58 -5
  42. package/dist/src/mind/types.d.ts +28 -5
  43. package/dist/src/store.d.ts +15 -0
  44. package/dist/src/store.js +91 -6
  45. package/package.json +1 -1
  46. package/src/derive/src/deduction.ts +15 -0
  47. package/src/derive/src/index.ts +1 -0
  48. package/src/geometry.ts +350 -122
  49. package/src/index.ts +1 -0
  50. package/src/meter.ts +333 -0
  51. package/src/mind/attention.ts +268 -31
  52. package/src/mind/bridge.ts +187 -10
  53. package/src/mind/canonical.ts +6 -1
  54. package/src/mind/graph-search.ts +60 -21
  55. package/src/mind/junction.ts +12 -0
  56. package/src/mind/match.ts +146 -1
  57. package/src/mind/mechanisms/cast.ts +62 -6
  58. package/src/mind/mechanisms/extraction.ts +2 -103
  59. package/src/mind/mechanisms/recall.ts +84 -17
  60. package/src/mind/mind.ts +139 -98
  61. package/src/mind/pipeline-mechanism.ts +203 -13
  62. package/src/mind/pipeline.ts +65 -8
  63. package/src/mind/primitives.ts +49 -33
  64. package/src/mind/reasoning.ts +39 -7
  65. package/src/mind/recognition.ts +89 -19
  66. package/src/mind/traverse.ts +59 -5
  67. package/src/mind/types.ts +28 -5
  68. package/src/store.ts +75 -6
  69. package/test/14-scaling.test.mjs +17 -7
  70. package/test/31-audit.test.mjs +4 -1
  71. package/test/33-multi-candidate.test.mjs +13 -1
  72. package/test/36-already-answered-fusion.test.mjs +10 -3
  73. package/test/46-recognise-multibyte-edge.test.mjs +3 -3
  74. package/test/53-cross-region-probe-instrumentation.test.mjs +36 -6
  75. package/test/55-cost-meter.test.mjs +284 -0
  76. package/test/56-bridge-identity-admission.test.mjs +209 -0
  77. package/test/57-fusion-order.test.mjs +104 -0
  78. package/test/58-subquantum-sites.test.mjs +112 -0
  79. package/test/59-fold-invariance.test.mjs +226 -0
package/src/mind/types.ts CHANGED
@@ -9,6 +9,7 @@ import type { BoundedMap, Store } from "../store.js";
9
9
  import type { Space } from "../sema.js";
10
10
  import type { Alphabet } from "../alphabet.js";
11
11
  import type { MindConfig } from "../config.js";
12
+ import type { Meter } from "../meter.js";
12
13
  import type {
13
14
  ComputedResult,
14
15
  DerivationItem,
@@ -19,7 +20,7 @@ import type {
19
20
  Site,
20
21
  } from "./graph-search.js";
21
22
  import type { Rationale } from "./rationale.js";
22
- import type { FoldPyramid, Grid, StableFold } from "../geometry.js";
23
+ import type { Grid, StableFold } from "../geometry.js";
23
24
 
24
25
  /** One {@link MindContext._depositTrees} entry — see that field's doc. */
25
26
  export interface DepositCacheEntry {
@@ -27,8 +28,6 @@ export interface DepositCacheEntry {
27
28
  * strictly increasing proper offsets, each a previously-deposited
28
29
  * whole-context length. Empty for a first-seen (single-turn) input. */
29
30
  boundaries: number[];
30
- /** Plain-fold pyramid (first-seen inputs only). */
31
- pyramid?: FoldPyramid;
32
31
  /** Stable-prefix segment folds (grown-context inputs only). */
33
32
  stable?: StableFold;
34
33
  /** The continuation bytes this ctxInput was paired with in ingestPair, if
@@ -59,6 +58,10 @@ export type Input = string | Uint8Array | Grid | Grid[];
59
58
  /** The host capabilities GraphSearch consults during a cover. MindContext
60
59
  * extends this so the Mind can pass itself as the host. */
61
60
  export interface GraphSearchHost {
61
+ /** Work accumulator, or null/absent when nothing is profiling — see
62
+ * src/meter.ts. Declared here (not only on MindContext) so the graph
63
+ * search can report its chart effort without importing mind code. */
64
+ readonly meter?: Meter | null;
62
65
  resolve(bytes: Uint8Array): number | null;
63
66
  recogniseSpan?(bytes: Uint8Array): {
64
67
  sites: ReadonlyArray<Site>;
@@ -173,6 +176,19 @@ export interface RegionVote {
173
176
  * to "one region" just because it collapsed to one pooled axiom.
174
177
  * Defaults to 1 when absent. */
175
178
  absorbed?: number;
179
+ /** The SEPARATE query places this vote's evidence occupies, when that is
180
+ * more than the one contiguous run [start, end]. A cross-region junction
181
+ * vote is pooled as a single synthetic region spanning its endpoints and
182
+ * the gap between them, so `[start, end]` reads as ONE place — yet the
183
+ * vote exists precisely because two non-adjacent regions each voted and
184
+ * only their conjunction resolved. Cluster counting (Attention.clusters)
185
+ * asks "how many separate places in the query corroborate this?", and
186
+ * answering it from the merged span makes every joint binding look like a
187
+ * single local neighbourhood; fusion's dispersion gate then drops it
188
+ * unless it also explains most of the whole query, which a binding inside
189
+ * a MULTI-topic query structurally cannot. Absent for an ordinary
190
+ * per-region vote, where the merged span already is the truth. */
191
+ parts?: readonly (readonly [number, number])[];
176
192
  }
177
193
 
178
194
  /** The structural gate that first decided an {@link edgeAncestors} climb was
@@ -237,6 +253,12 @@ export type AItem =
237
253
 
238
254
  export interface MindContext extends GraphSearchHost {
239
255
  store: Store;
256
+ /** The work accumulator for the inference call in flight, or null when
257
+ * nothing is profiling — see src/meter.ts. WRITE-ONLY from the engine's
258
+ * point of view: no inference decision may read a counter, or the
259
+ * determinism contract (AGENTS §2.1) is gone. Every call site is
260
+ * `ctx.meter?.x++`, so an unprofiled response allocates nothing. */
261
+ meter: Meter | null;
240
262
  space: Space;
241
263
  alphabet: Alphabet;
242
264
  cfg: MindConfig;
@@ -294,8 +316,9 @@ export interface MindContext extends GraphSearchHost {
294
316
  * folds with the SAME stable-prefix fold query-time perception uses
295
317
  * (structural train/inference agreement, load-bearing for recall),
296
318
  * reusing every already-folded segment via `stable` (see StableFold) —
297
- * O(turn) per deposit instead of O(context). A first-seen input keeps
298
- * the plain fold and caches its `pyramid` (see FoldPyramid). Purely a
319
+ * O(turn) per deposit instead of O(context). A first-seen input takes the
320
+ * same fold with no boundaries at all, and caches the segments it produced
321
+ * so a later turn of the same conversation reuses them. Purely a
299
322
  * performance cache for the FOLD STATE; the boundaries are semantic but
300
323
  * derived only from the deposit sequence itself (an evicted chain falls
301
324
  * back to plain-fold behavior, exactly the pre-boundary shape). */
package/src/store.ts CHANGED
@@ -31,6 +31,7 @@
31
31
  import { addInto, copy, dot, normalize, Vec } from "./vec.js";
32
32
  import { DEFAULT_CONFIG, type StoreConfig } from "./config.js";
33
33
  import { identityBar } from "./geometry.js";
34
+ import type { Meter } from "./meter.js";
34
35
 
35
36
  /** A node id: a dense, non-negative integer assigned in creation order. */
36
37
  export type NodeId = number;
@@ -226,6 +227,14 @@ export class BoundedMap<K, V> {
226
227
  export interface Store {
227
228
  readonly D: number;
228
229
 
230
+ /** The work accumulator for the inference call in flight, or null. The
231
+ * Mind attaches one per profiled response and detaches it after (see
232
+ * src/meter.ts). A store MUST only ever write to it — no read may reach
233
+ * a decision, or determinism is gone. An adapter that adds reads of its
234
+ * own should bump the matching counter; one that does not simply reports
235
+ * less, never wrongly. */
236
+ meter: Meter | null;
237
+
229
238
  // ── the DAG (hash-consed) ──────────────────────────────────────────────
230
239
  /** Insert a leaf, returning its content id. Idempotent. */
231
240
  putLeaf(bytes: Uint8Array, gist: Vec): Promise<NodeId>;
@@ -1010,6 +1019,7 @@ export abstract class AbstractStore implements Store {
1010
1019
  // ── DAG traversal ──────────────────────────────────────────────────────
1011
1020
 
1012
1021
  get(id: NodeId): NodeRec | null {
1022
+ if (this.meter) this.meter.nodeRecords++;
1013
1023
  // Byte leaves are implicit — fabricate from the id.
1014
1024
  if (id < 0) {
1015
1025
  return { id, leaf: new Uint8Array([-(id + 1)]), kids: null };
@@ -1030,7 +1040,20 @@ export abstract class AbstractStore implements Store {
1030
1040
  * counter is what keeps that degradation observable. */
1031
1041
  danglingReads = 0;
1032
1042
 
1043
+ /** {@link Store.meter} — the per-response work accumulator, or null when
1044
+ * nothing is profiling. Every read below bumps it through `?.`, so an
1045
+ * unprofiled store pays one null check per read and allocates nothing. */
1046
+ meter: Meter | null = null;
1047
+
1033
1048
  bytes(id: NodeId): Uint8Array {
1049
+ if (this.meter) {
1050
+ this.meter.byteReads++;
1051
+ // Charged BEFORE the fast path returns, and from the cache entry when
1052
+ // there is one: the volume a caller pulled through is the cost signal
1053
+ // (an unbounded read is unbounded whether or not it was cached).
1054
+ const c = this._bytesCache.get(id);
1055
+ if (c) this.meter.bytesRead += c.length;
1056
+ }
1034
1057
  // Fast path.
1035
1058
  const hit = this._bytesCache.get(id);
1036
1059
  if (hit) return hit;
@@ -1082,7 +1105,9 @@ export abstract class AbstractStore implements Store {
1082
1105
  cache.set(nid, out);
1083
1106
  }
1084
1107
 
1085
- return cache.get(id) ?? _ZERO;
1108
+ const out = cache.get(id) ?? _ZERO;
1109
+ if (this.meter) this.meter.bytesRead += out.length;
1110
+ return out;
1086
1111
  }
1087
1112
 
1088
1113
  /** First `maxLen` bytes of a node. Walks only the leftmost branch,
@@ -1093,14 +1118,27 @@ export abstract class AbstractStore implements Store {
1093
1118
  * may be shared with the byte cache and with other callers — treat them
1094
1119
  * as read-only. Mutating one would corrupt every subsequent read. */
1095
1120
  bytesPrefix(id: NodeId, maxLen: number): Uint8Array {
1096
- if (maxLen <= 0) return _ZERO;
1097
-
1098
1121
  // A FULL read (the ALL sentinel) routes through bytes(), whose
1099
1122
  // reconstruction enters the byte-budget cache. Without this, the mind's
1100
1123
  // read() — which always passes ALL — re-walked the DAG and re-concatenated
1101
1124
  // on EVERY repeated read of an uncached branch, bypassing the cache that
1102
1125
  // exists precisely for those reconstructions.
1103
1126
  if (maxLen >= 0x7fffffff) return this.bytes(id);
1127
+ // METERING BOUNDARY: this public entry point is charged ONCE per logical
1128
+ // read; the walk below recurses through `_prefix`, which is not charged.
1129
+ // Counting the recursion instead made a single read of an N-byte branch
1130
+ // report as N reads (one per node descended), so `byteReads` measured
1131
+ // tree size rather than read requests — it read as ~1 byte per read.
1132
+ if (this.meter) this.meter.byteReads++;
1133
+ const out = this._prefix(id, maxLen);
1134
+ if (this.meter) this.meter.bytesRead += out.length;
1135
+ return out;
1136
+ }
1137
+
1138
+ /** {@link bytesPrefix}'s recursive body — uncharged; see the metering
1139
+ * boundary note there. */
1140
+ private _prefix(id: NodeId, maxLen: number): Uint8Array {
1141
+ if (maxLen <= 0) return _ZERO;
1104
1142
 
1105
1143
  // Full-cache hit: bytes() already reconstructed the whole node.
1106
1144
  const full = this._bytesCache.get(id);
@@ -1125,7 +1163,7 @@ export abstract class AbstractStore implements Store {
1125
1163
  let got = 0;
1126
1164
  for (const k of kids) {
1127
1165
  if (got >= maxLen) break;
1128
- const child = this.bytesPrefix(k, maxLen - got);
1166
+ const child = this._prefix(k, maxLen - got);
1129
1167
  parts.push(child);
1130
1168
  got += child.length;
1131
1169
  }
@@ -1133,6 +1171,7 @@ export abstract class AbstractStore implements Store {
1133
1171
  }
1134
1172
 
1135
1173
  contentLen(id: NodeId, cap = Infinity): number {
1174
+ if (this.meter) this.meter.lenReads++;
1136
1175
  if (id < 0) return 1; // implicit single-byte leaf
1137
1176
  const hit = this._lenCache.get(id);
1138
1177
  if (hit !== undefined) return hit; // exact — valid under any cap
@@ -1159,6 +1198,7 @@ export abstract class AbstractStore implements Store {
1159
1198
  // ── Content-addressed lookup ───────────────────────────────────────────
1160
1199
 
1161
1200
  findLeaf(bytes: Uint8Array): NodeId | null {
1201
+ if (this.meter) this.meter.leafLookups++;
1162
1202
  if (bytes.length === 1) return -(bytes[0] + 1);
1163
1203
  const key = keyOf(bytes);
1164
1204
  const cached = this._leafKey.get(key);
@@ -1169,6 +1209,7 @@ export abstract class AbstractStore implements Store {
1169
1209
  }
1170
1210
 
1171
1211
  findBranch(kids: NodeId[]): NodeId | null {
1212
+ if (this.meter) this.meter.branchLookups++;
1172
1213
  const key = kids.join(",");
1173
1214
  const cached = this._branchKey.get(key);
1174
1215
  if (cached !== undefined) return cached;
@@ -1189,18 +1230,22 @@ export abstract class AbstractStore implements Store {
1189
1230
  // ── Structural parents ─────────────────────────────────────────────────
1190
1231
 
1191
1232
  parents(id: NodeId): NodeId[] {
1233
+ if (this.meter) this.meter.parentReads++;
1192
1234
  return this._dbGetParents(id);
1193
1235
  }
1194
1236
 
1195
1237
  parentsFirst(id: NodeId, limit: number): NodeId[] {
1238
+ if (this.meter) this.meter.parentReads++;
1196
1239
  return this._dbGetParentsFirst(id, limit);
1197
1240
  }
1198
1241
 
1199
1242
  hasParents(id: NodeId): boolean {
1243
+ if (this.meter) this.meter.parentProbes++;
1200
1244
  return this._dbGetParentsFirst(id, 1).length > 0;
1201
1245
  }
1202
1246
 
1203
1247
  chainRun(id: NodeId): readonly NodeId[] {
1248
+ if (this.meter) this.meter.chainRuns++;
1204
1249
  const hit = this._chainMemo.get(id);
1205
1250
  if (hit !== undefined) return hit;
1206
1251
  const run = this._chainWalk(id, CHAIN_DEPTH_CAP);
@@ -1236,12 +1281,14 @@ export abstract class AbstractStore implements Store {
1236
1281
  }
1237
1282
 
1238
1283
  hasContainers(child: NodeId): boolean {
1284
+ if (this.meter) this.meter.containerProbes++;
1239
1285
  if (this._dbContainExists(child)) return true;
1240
1286
  const buf = this._containBuf.get(child);
1241
1287
  return buf !== undefined && buf.size > 0;
1242
1288
  }
1243
1289
 
1244
1290
  containersSlice(child: NodeId, offset: number, limit: number): NodeId[] {
1291
+ if (this.meter) this.meter.containerReads++;
1245
1292
  const out = this._dbGetContainParentsSlice(child, offset, limit);
1246
1293
  if (out.length >= limit) return out;
1247
1294
  // Buffered adds page in AFTER the stored ones. A buffered parent that is
@@ -1262,6 +1309,7 @@ export abstract class AbstractStore implements Store {
1262
1309
  }
1263
1310
 
1264
1311
  containers(child: NodeId): NodeId[] {
1312
+ if (this.meter) this.meter.containerReads++;
1265
1313
  const stored = this._dbGetContainParents(child);
1266
1314
  const buf = this._containBuf.get(child);
1267
1315
  if (stored.length === 0) return buf ? [...buf] : [];
@@ -1662,9 +1710,13 @@ export abstract class AbstractStore implements Store {
1662
1710
  const cache = this._resonateCache;
1663
1711
  if (cache) {
1664
1712
  const hit = cache.get(rk);
1665
- if (hit !== undefined) return hit;
1713
+ if (hit !== undefined) {
1714
+ if (this.meter) this.meter.annCacheHits++;
1715
+ return hit;
1716
+ }
1666
1717
  }
1667
1718
 
1719
+ if (this.meter) this.meter.annQueries++;
1668
1720
  const clusters = this._vecContentClusterCount();
1669
1721
  const results = this._vecContentQuery(
1670
1722
  normalize(copy(v)),
@@ -1682,6 +1734,10 @@ export abstract class AbstractStore implements Store {
1682
1734
  if (out.length >= k) break;
1683
1735
  }
1684
1736
 
1737
+ // Vectors the index actually scored for THIS descent — the counter that
1738
+ // exposes a query whose ANN cost is growing with the corpus.
1739
+ if (this.meter) this.meter.annVectorReads += this._vecContentLastReads();
1740
+
1685
1741
  const rc = this._resonateCache ??= new Map();
1686
1742
  if (rc.size >= RESONATE_CACHE_MAX) rc.clear();
1687
1743
  rc.set(rk, out);
@@ -1924,29 +1980,35 @@ export abstract class AbstractStore implements Store {
1924
1980
  }
1925
1981
 
1926
1982
  next(id: NodeId): NodeId[] {
1983
+ if (this.meter) this.meter.edgeReads++;
1927
1984
  return this._dbGetNextEdges(id);
1928
1985
  }
1929
1986
 
1930
1987
  /** {@link Store.hasNext} — one indexed point probe, never a range read. */
1931
1988
  hasNext(id: NodeId): boolean {
1989
+ if (this.meter) this.meter.edgeProbes++;
1932
1990
  return this._dbEdgeSrcExists(id);
1933
1991
  }
1934
1992
 
1935
1993
  prev(id: NodeId): NodeId[] {
1994
+ if (this.meter) this.meter.prevReads++;
1936
1995
  return this._dbGetPrevEdges(id);
1937
1996
  }
1938
1997
 
1939
1998
  nextFirst(id: NodeId, limit: number): NodeId[] {
1999
+ if (this.meter) this.meter.edgeReads++;
1940
2000
  return this._dbGetNextEdgesFirst(id, limit);
1941
2001
  }
1942
2002
 
1943
2003
  prevFirst(id: NodeId, limit: number): NodeId[] {
2004
+ if (this.meter) this.meter.prevReads++;
1944
2005
  return this._dbGetPrevEdgesFirst(id, limit);
1945
2006
  }
1946
2007
 
1947
2008
  /** {@link Store.prevCount}. Subclasses with an indexed reverse-edge count
1948
2009
  * should override; this default materialises (correct, not optimal). */
1949
2010
  prevCount(id: NodeId): number {
2011
+ if (this.meter) this.meter.prevProbes++;
1950
2012
  return this._dbGetPrevEdges(id).length;
1951
2013
  }
1952
2014
 
@@ -1964,11 +2026,13 @@ export abstract class AbstractStore implements Store {
1964
2026
  // ── Halos ──────────────────────────────────────────────────────────────
1965
2027
 
1966
2028
  haloMass(id: NodeId): number {
2029
+ if (this.meter) this.meter.haloProbes++;
1967
2030
  const r = this._dbGetHalo(id);
1968
2031
  return r ? r.mass : 0;
1969
2032
  }
1970
2033
 
1971
2034
  halo(id: NodeId): Vec | null {
2035
+ if (this.meter) this.meter.haloReads++;
1972
2036
  const cached = this._haloNorm.get(id);
1973
2037
  if (cached !== undefined) return copy(cached);
1974
2038
  const r = this._dbGetHalo(id);
@@ -1982,6 +2046,7 @@ export abstract class AbstractStore implements Store {
1982
2046
  /** {@link Store.hasHalo} — MUST mirror {@link halo}'s null condition
1983
2047
  * exactly (row present AND mass ≥ minHaloMass), minus the decode. */
1984
2048
  hasHalo(id: NodeId): boolean {
2049
+ if (this.meter) this.meter.haloProbes++;
1985
2050
  const r = this._dbGetHalo(id);
1986
2051
  return r !== null && r.mass >= this.minHaloMass;
1987
2052
  }
@@ -2018,9 +2083,13 @@ export abstract class AbstractStore implements Store {
2018
2083
  const cache = this._resonateHaloCache;
2019
2084
  if (cache) {
2020
2085
  const hit = cache.get(rk);
2021
- if (hit !== undefined) return hit;
2086
+ if (hit !== undefined) {
2087
+ if (this.meter) this.meter.annCacheHits++;
2088
+ return hit;
2089
+ }
2022
2090
  }
2023
2091
 
2092
+ if (this.meter) this.meter.haloQueries++;
2024
2093
  const results = this._vecHaloQuery(
2025
2094
  normalize(copy(v)),
2026
2095
  k * this.overfetch,
@@ -376,16 +376,26 @@ test("inference: input is processed at a roughly constant KB/s (linear, not quad
376
376
 
377
377
  await mind.respond(queries[queries.length - 1]); // warm up
378
378
 
379
- const times = [];
380
- for (const q of queries) {
381
- const samples = [];
382
- for (let r = 0; r < 5; r++) {
379
+ // INTERLEAVED sampling — round-robin across sizes, not size-at-a-time.
380
+ // The inputs and the store are deterministic, so the WORK per query is
381
+ // fixed; only wall clock varies. Sampling one size to completion before
382
+ // moving on let a single GC/JIT episode land on one size's ENTIRE sample
383
+ // block, so even `fastest` (min of 5) came back ~2.7x high for that size
384
+ // alone — a bimodal reading that made the throughput-band ratio below trip
385
+ // intermittently (observed on both the 1KB point and, more weakly, before
386
+ // any of this file's callers changed). Round-robin spreads every episode
387
+ // across all sizes, so `fastest` sees at least one clean sample each.
388
+ // This does not weaken the assertion — it removes the sampling artefact
389
+ // the assertion was reacting to.
390
+ const samples = queries.map(() => []);
391
+ for (let r = 0; r < 5; r++) {
392
+ for (let i = 0; i < queries.length; i++) {
383
393
  const t0 = performance.now();
384
- await mind.respond(q);
385
- samples.push(performance.now() - t0);
394
+ await mind.respond(queries[i]);
395
+ samples[i].push(performance.now() - t0);
386
396
  }
387
- times.push(fastest(samples));
388
397
  }
398
+ const times = samples.map(fastest);
389
399
  await mind.store.close();
390
400
 
391
401
  const rates = bytes.map((b, i) => (b / 1024) / (times[i] / 1000)); // KB/s
@@ -235,8 +235,11 @@ test("H2: the ALU meaning-memo does not change answers across repeat responds",
235
235
  // ═══════════════════════════════════════════════════════════════════════
236
236
 
237
237
  test("I1: containsSpan rejects a sparse subsequence that isSpanShaped accepts", async () => {
238
+ // The span-shape family lives in the shared match/project family
239
+ // (match.ts), not inside a mechanism: pipeline-mechanism.ts and
240
+ // reasoning.ts both consume it, so a mechanism may not own it.
238
241
  const { containsSpan, isSpanShaped } = await import(
239
- "../dist/src/mind/mechanisms/extraction.js"
242
+ "../dist/src/mind/match.js"
240
243
  );
241
244
  const m = newMind();
242
245
  await m.ingest([["what is ice?", "ice is frozen water"]]);
@@ -142,7 +142,19 @@ test("3 — a near-tie between the winner and runner-up emits narrowDecision", a
142
142
  ["steel is hard so steel is strong", "strong"],
143
143
  ["water is frigid so water is freezing", "freezing"],
144
144
  ]);
145
- const { steps } = await trace(m, "steel is frigid so steel is ???");
145
+ // The near-tie has to be REAL: two candidates whose accounted spans are the
146
+ // same, separated by a single move. `steel is hard so steel is` is that —
147
+ // extraction and recall both explain the whole prefix (grades 21011 and
148
+ // 21010, margin 1).
149
+ //
150
+ // The old fixture, `steel is frigid so steel is ???`, was a near-tie only
151
+ // while CAST could not fire on it: the counterfactual subject and the seat
152
+ // it fills sit on opposite sides of one recurring structure, and CAST's
153
+ // subject gate used to reject any point whose alignment continued PAST the
154
+ // seat — which that very recurrence guarantees. With CAST firing, it leaves
155
+ // only "???" unexplained and wins by 22008; a decisive win is the opposite
156
+ // of what this test pins, and test 3b already covers it.
157
+ const { steps } = await trace(m, "steel is hard so steel is");
146
158
  const narrow = stepsNamed(steps, "narrowDecision");
147
159
  assert.equal(narrow.length, 1, "expected exactly one narrowDecision step");
148
160
  assert.ok(/margin \d+ grade-unit/.test(narrow[0].note), narrow[0].note);
@@ -69,9 +69,16 @@ test("fuseAttention: a root whose continuation is already answered in the query
69
69
  // topic with no such embedded answer.
70
70
  const q = enc("greet reply-greet then red then circle");
71
71
  const roots = await m.climbAttention(q, 24);
72
- const greet = roots.find((r) => r.start === 0);
73
- const redCircle = roots.find((r) => r.start !== 0);
74
- assert.ok(greet, "expected the 'greet' root at query offset 0");
72
+ // Roots are identified by the ANCHOR they climbed to, never by the query
73
+ // offset of the region that got there: which region wins the climb is a
74
+ // property of the evidence, and the fold's cuts are content-defined, so an
75
+ // offset test would be asserting where a cut happened to fall. (It did:
76
+ // this once read `r.start === 0`, and passed only while the region reaching
77
+ // `greet` began at byte 0 — it now begins at 6, on the same anchor.)
78
+ const anchorOf = (r) => dec(m.store.bytesPrefix(r.anchor, 1e9));
79
+ const greet = roots.find((r) => anchorOf(r) === "greet");
80
+ const redCircle = roots.find((r) => anchorOf(r) === "red circle");
81
+ assert.ok(greet, "expected a root anchored on 'greet'");
75
82
  assert.ok(redCircle, "expected the 'red circle' binding root");
76
83
 
77
84
  const guide = gistOf(m, q);
@@ -2,9 +2,9 @@
2
2
  // must recover a trained form whose edge misalignment is MORE than one byte
3
3
  // (the shipped ±1 fix only covers a single stray edge byte).
4
4
  //
5
- // bytesToTree's plain fold is RADIX-ALIGNED: a chunk's own boundary is a
6
- // multiple of the geometry's group width W from the LOCAL start of whatever
7
- // it was folded from (see geometry.ts's FoldPyramid comment). A query whose
5
+ // bytesToTree's fold is CONTENT-DEFINED: a chunk's own boundary is a cut the
6
+ // bytes chose (see geometry.ts's contentBoundaries), so it moves with the
7
+ // content rather than with an absolute offset. A query whose
8
8
  // recognised span sits at a different local offset than the trained deposit
9
9
  // did — e.g. extra leading whitespace, which canon deliberately preserves
10
10
  // verbatim at the edges, only collapsing INTERIOR whitespace — shifts every
@@ -47,6 +47,27 @@ const SYN_ATTR_CORPUS = [
47
47
  ["blue square", "answer delta"],
48
48
  ];
49
49
 
50
+ // Structural-resonance is the LAST rung: it runs only when every DAG tier —
51
+ // exact junction, single- and double-synonym — found no container at all.
52
+ // SYN_ATTR_CORPUS cannot reach it, and no longer pretends to: every attribute
53
+ // there shares a context ("is a color" / "is a shape") with its neighbours, so
54
+ // the single-synonym tier always finds a bridge and accepts before the ladder
55
+ // gets this far. (It once did reach resonance, when the junction search was
56
+ // phase-dependent and missed containers it should have found; asserting the
57
+ // weaker rung still fires would now be asserting a defect.)
58
+ //
59
+ // This corpus withholds exactly what the synonym tiers need: each attribute's
60
+ // context is unique, so no two attributes are halo siblings, and `red` and
61
+ // `square` never co-occur. `red then square` therefore exhausts the DAG tiers
62
+ // and reaches resonance for real — one probe margin-rejected, one ineligible.
63
+ const NO_BRIDGE_CORPUS = [
64
+ ["red", "warm hue"],
65
+ ["square", "four sides"],
66
+ ["circle", "round form"],
67
+ ["red circle", "answer alpha"],
68
+ ["blue square", "answer delta"],
69
+ ];
70
+
50
71
  // ═══════════════════════════════════════════════════════════════════════════
51
72
  // 1. ordinary-region contrastive rival.
52
73
  // ═══════════════════════════════════════════════════════════════════════════
@@ -204,8 +225,8 @@ test("5. double-synonym tier: probe fields report single returned zero and doubl
204
225
 
205
226
  test("6. structural-resonance: eligible probes report variants, merged proposals and an examined sequence with effectiveScore = annScore * semanticConfidence", async () => {
206
227
  const m = mk(1);
207
- await m.ingest(SYN_ATTR_CORPUS);
208
- const { steps } = await trace(m, "crimson then square");
228
+ await m.ingest(NO_BRIDGE_CORPUS);
229
+ const { steps } = await trace(m, "red then square");
209
230
  await m.store.close();
210
231
 
211
232
  const step = climbStep(steps);
@@ -262,10 +283,10 @@ test("6. structural-resonance: eligible probes report variants, merged proposals
262
283
 
263
284
  test("7. resonance outcomes: ineligible reasons are named and a margin-rejected probe reports a sub-noise-floor margin", async () => {
264
285
  const mIneligible = mk(1);
265
- await mIneligible.ingest(SYN_ATTR_CORPUS);
286
+ await mIneligible.ingest(NO_BRIDGE_CORPUS);
266
287
  const { steps: ineligibleSteps } = await trace(
267
288
  mIneligible,
268
- "crimson beside square",
289
+ "red then square",
269
290
  );
270
291
  await mIneligible.store.close();
271
292
 
@@ -286,9 +307,18 @@ test("7. resonance outcomes: ineligible reasons are named and a margin-rejected
286
307
  assert.equal(p.outcome, "resonance-ineligible");
287
308
  }
288
309
 
310
+ // A margin rejection needs resonance to actually PRODUCE a proposal and
311
+ // then find it indistinguishable from its runner-up. On the tight
312
+ // `red then square` the single surviving proposal clears the noise floor and
313
+ // is accepted; spreading the two attributes apart puts many more pairs in
314
+ // play, and the pair that reaches resonance proposes an ANN neighbour its
315
+ // rival matches — which is exactly the case this field exists to report.
289
316
  const mMargin = mk(1);
290
- await mMargin.ingest(SYN_ATTR_CORPUS);
291
- const { steps: marginSteps } = await trace(mMargin, "crimson then square");
317
+ await mMargin.ingest(NO_BRIDGE_CORPUS);
318
+ const { steps: marginSteps } = await trace(
319
+ mMargin,
320
+ "red and a very long interior gap before square",
321
+ );
292
322
  await mMargin.store.close();
293
323
  const marginStep = climbStep(marginSteps);
294
324
  const marginRejected = (marginStep.data.crossRegion?.probes ?? []).filter(