@hviana/sema 0.6.0 → 0.7.2

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 (42) hide show
  1. package/.github/workflows/release.yml +80 -0
  2. package/AGENTS.md +53 -9
  3. package/HOW_IT_WORKS.md +17 -16
  4. package/dist/src/meter.d.ts +14 -4
  5. package/dist/src/meter.js +27 -3
  6. package/dist/src/mind/attention.js +22 -20
  7. package/dist/src/mind/graph-search.d.ts +43 -9
  8. package/dist/src/mind/graph-search.js +82 -15
  9. package/dist/src/mind/junction.d.ts +13 -0
  10. package/dist/src/mind/junction.js +26 -1
  11. package/dist/src/mind/mechanisms/cover.js +23 -2
  12. package/dist/src/mind/mechanisms/prefix-completion.d.ts +2 -1
  13. package/dist/src/mind/mechanisms/prefix-completion.js +40 -20
  14. package/dist/src/mind/mechanisms/recall.js +8 -4
  15. package/dist/src/mind/pipeline-mechanism.d.ts +0 -24
  16. package/dist/src/mind/pipeline-mechanism.js +13 -36
  17. package/dist/src/mind/pipeline.d.ts +24 -0
  18. package/dist/src/mind/pipeline.js +71 -5
  19. package/dist/src/mind/recognition.d.ts +15 -1
  20. package/dist/src/mind/recognition.js +15 -1
  21. package/dist/src/mind/resonance.js +54 -12
  22. package/dist/src/store.js +22 -1
  23. package/jsr.json +1 -1
  24. package/package.json +7 -2
  25. package/src/meter.ts +27 -4
  26. package/src/mind/attention.ts +22 -19
  27. package/src/mind/graph-search.ts +93 -16
  28. package/src/mind/junction.ts +25 -1
  29. package/src/mind/mechanisms/cover.ts +23 -4
  30. package/src/mind/mechanisms/prefix-completion.ts +40 -20
  31. package/src/mind/mechanisms/recall.ts +8 -4
  32. package/src/mind/pipeline-mechanism.ts +13 -42
  33. package/src/mind/pipeline.ts +106 -5
  34. package/src/mind/recognition.ts +19 -2
  35. package/src/mind/resonance.ts +84 -49
  36. package/src/store.ts +21 -1
  37. package/test/89-completion-recursion.test.mjs +230 -0
  38. package/test/90-connector-read-cap.test.mjs +130 -0
  39. package/test/91-branch-bytes-cache.test.mjs +152 -0
  40. package/test/93-regime-prediction.test.mjs +148 -0
  41. package/test/94-cross-region-budget.test.mjs +67 -0
  42. package/test/95-wide-resonance-removed.test.mjs +109 -0
@@ -19,7 +19,7 @@ import type { ComputedSpan } from "../extension.js";
19
19
  import type { Hit } from "../store.js";
20
20
  import type { Vec } from "../vec.js";
21
21
  import { indexOf } from "../bytes.js";
22
- import { conceptThreshold, dominates } from "../geometry.js";
22
+ import { dominates } from "../geometry.js";
23
23
  import { windowIds } from "./canonical.js";
24
24
  import { read, resolve } from "./primitives.js";
25
25
  import {
@@ -30,7 +30,7 @@ import {
30
30
  skillExemplar,
31
31
  } from "./match.js";
32
32
  import { climbAttentionAll } from "./attention.js";
33
- import { hubBound, sharedReachMemo } from "./traverse.js";
33
+ import { sharedReachMemo } from "./traverse.js";
34
34
 
35
35
  // ── Precomputed ──────────────────────────────────────────────────────────────
36
36
  //
@@ -148,46 +148,17 @@ export class Precomputed {
148
148
  );
149
149
  }
150
150
 
151
- private _wide?: Promise<ReadonlyArray<number>>;
152
- /** The response's WIDE candidate list — the top-k when the query's gist has
153
- * no concept-level match anywhere, and an exhaustive √N read when it does.
154
- *
155
- * Every mechanism that has to look PAST the top-k reads this one list: the
156
- * substitution bridge, prefix completion and the frame filler all did, and
157
- * it was memoised inside recall for exactly that reason (measured: 490 ms
158
- * median re-issued against 13 ms non-exhaustive, 36x). A memo inside one
159
- * mechanism only serves that mechanism's own tiers, so it lives here now —
160
- * the same move `resonance` made for the top-k.
161
- *
162
- * THE CONDITION IS THE TOP HIT'S SCORE, NOT THE CORPUS SIZE. When nothing
163
- * ranks at concept level, an exhaustive ANN only scores more vectors below
164
- * the bar (profiled at 38K–40K annVectorReads per refusing query on a 325K-
165
- * context store); the structural channels — junction walks, anchor climbs,
166
- * the write side's window index — are the correct proposal source there,
167
- * because the ANN cannot propose what the gist cannot rank. This was once
168
- * spelled `corpusN(ctx) <= (k · W)³`, which asks a different question and
169
- * answers it wrongly at exactly the scale it was written from: at N =
170
- * 325,608 with k = 24 and W = 4 the cube is 884,736, so that store took the
171
- * exhaustive branch — the very branch measured above. Measured cost of the
172
- * mismatch: substitutionBridge 8,544 ms of a 19,548 ms think (44%), against
173
- * 1,248 ms and 14,218 ms without it, every answer byte-identical. */
174
- wideResonance(): Promise<ReadonlyArray<number>> {
175
- return this._wide ??= this.shared("wideResonance", async () => {
176
- const hits = await this.resonance();
177
- if (
178
- hits.length > 0 &&
179
- hits[0].score >= conceptThreshold(this.ctx.store.D)
180
- ) {
181
- const exhaustive = await this.ctx.store.resonate(
182
- this.guide,
183
- hubBound(this.ctx),
184
- true,
185
- );
186
- return exhaustive.map((h) => h.id);
187
- }
188
- return hits.map((h) => h.id);
189
- });
190
- }
151
+ // REMOVED — the WIDE exhaustive-√N resonance list (`wideResonance`). It ran
152
+ // `resonate(guide, √N, exhaustive=true)` whenever the top hit cleared
153
+ // conceptThreshold, so consumers could look "past the top-k". Every consumer
154
+ // only ever needed ≤ 2·recallQueryK proposals (the substitution bridge's own
155
+ // candidate cap) or a content-addressed answer (prefix completion's
156
+ // formsOpenedBy), and every proposal is byte-verified downstream (§2.3), so
157
+ // the exhaustive scan bought recall at O(index) cost for an O(k) need
158
+ // measured: 244K annVectorReads per refusing query, ~1.5 s, every answer
159
+ // byte-identical to a top-k read. The two consumers now read `resonance()`
160
+ // (the one top-k read) and the write side's window index respectively — see
161
+ // recall.ts and prefix-completion.ts.
191
162
 
192
163
  private _frames?: Promise<ReadonlyArray<FrameInstance>>;
193
164
  /** THE FRAME INVENTORY — every ranked candidate that reads as an instance of
@@ -129,6 +129,31 @@ export interface NarrowDecisionData {
129
129
  margin: number;
130
130
  }
131
131
 
132
+ /** Structured payload of the "regimePrediction" rationale step — the R8
133
+ * observation exposed as data. After the first mechanism (cover, which §2.6
134
+ * runs first) grounds or abstains, the market's whole outcome is already
135
+ * determined by the one cost ladder: the consensus climb runs exactly when
136
+ * `worthRunning(2 * STEP)` is true — CAST (floor 2·STEP) is the cheapest
137
+ * mechanism that first-touches it, and confluence (3·STEP) / extraction
138
+ * (CONCEPT+STEP) are only reached after CAST is. An incumbent at or below
139
+ * that floor prunes CAST and, with it, the climb (retrieval); anything above
140
+ * — or no incumbent — runs the full market and the climb (composition).
141
+ * Purely observational; never read by inference. */
142
+ export interface RegimePredictionData {
143
+ version: 1;
144
+ /** retrieval | composition — the two regimes R1 measured as a ~100× cost
145
+ * step. */
146
+ regime: "retrieval" | "composition";
147
+ /** The incumbent's grade once the first mechanism's turn is over (it ran, or
148
+ * it was skipped), or null when nothing has grounded — `best === null`,
149
+ * which is composition with no incumbent. */
150
+ incumbentGrade: number | null;
151
+ /** The cheapest composition floor in grade units (`grade(2 * STEP)` = 2,
152
+ * CAST's floor) — the bar the incumbent must sit at or below for the
153
+ * consensus climb to be skipped. */
154
+ climbFloorGrade: number;
155
+ }
156
+
132
157
  /** Think: a single lightest-derivation exploration of the Sema graph.
133
158
  *
134
159
  * Every answer travels the same path:
@@ -161,10 +186,25 @@ export async function think(
161
186
 
162
187
  // ── Pre-computation ──────────────────────────────────────────────────
163
188
  const mechanisms = mechs ?? defaultMechanisms;
164
- const rec = recognise(ctx, query);
189
+ const meter = ctx.meter;
190
+ // recognition is a shared analysis (§2.14 contract 5): it does the query's
191
+ // own store work (perceive → foldTree → resolve), which used to land in
192
+ // `think` and in nothing narrower — the meter's one accounting surface must
193
+ // charge it to itself, exactly as attention/weave/resonance are charged.
194
+ // SYNCHRONOUS phase: recognition is on the sync side of §2.10's seam, so it
195
+ // is timed with `timeSync` — wrapping it in a promise would make a profiled
196
+ // response await where an unprofiled one does not.
197
+ const rec = meter
198
+ ? meter.timeSync("recognise", () => recognise(ctx, query))
199
+ : recognise(ctx, query);
165
200
 
166
201
  // Phase 1: collect computed spans from mechanisms that implement parse()
167
- const computed = await collectComputed(ctx, mechanisms, query);
202
+ const computed = meter
203
+ ? await meter.time(
204
+ "collectComputed",
205
+ () => collectComputed(ctx, mechanisms, query),
206
+ )
207
+ : await collectComputed(ctx, mechanisms, query);
168
208
 
169
209
  if (computed.length > 0) {
170
210
  ctx.trace?.step(
@@ -191,6 +231,9 @@ export async function think(
191
231
  // method on Precomputed, first-touched by whichever mechanism's floor
192
232
  // survives its cheap gates and the worthRunning check. A query no
193
233
  // mechanism climbs for (e.g. one an extension decided) never climbs.
234
+ // NOT phased: the constructor itself is trivial (it only derives `k`), so a
235
+ // phase here would add a zero-work entry to every profiled report — the meter
236
+ // attributes WORK (§2.14); the trace already represents structure.
194
237
  const pre = new Precomputed(ctx, query, rec, computed, ctx._edgeGuide);
195
238
 
196
239
  // ── Grounding: ONE lightest-derivation choice among the mechanisms ────
@@ -255,12 +298,66 @@ export async function think(
255
298
  const worthRunning = (floor: number) =>
256
299
  best === null || grade(floor) < grade(best.weight);
257
300
 
301
+ // REGIME PREDICTION (R8) — observational only. Once the FIRST mechanism has
302
+ // had its turn (cover, which §2.6 places first and floors at 0), the market's
303
+ // outcome is already determined by the one cost ladder: the consensus climb
304
+ // runs exactly when `worthRunning(2 * STEP)` is true — CAST (floor 2·STEP) is
305
+ // the cheapest mechanism that first-touches it, so an incumbent at or below
306
+ // grade 2 prunes CAST and, with it, confluence (3·STEP) and extraction
307
+ // (CONCEPT+STEP) (retrieval); anything above — or no incumbent — runs the
308
+ // full market and the climb (composition). The predicate is `worthRunning`,
309
+ // the same function the loop itself uses — nothing is computed here that the
310
+ // engine had not already computed, and nothing is read back by inference.
311
+ //
312
+ // EMITTED BEFORE THE SECOND MECHANISM'S FLOOR, never after some mechanism's
313
+ // run: a "prediction" published after the fact could assert "the climb will
314
+ // not run" about a climb that already ran — which is what happens whenever
315
+ // the first mechanism is SKIPPED (null floor or pruned) and the block sits at
316
+ // the end of the first mechanism that actually ran. Emitting on entry to
317
+ // iteration 1 makes the claim true by construction, whatever the first
318
+ // mechanism did, and keeps the payload identical on the ordinary path (the
319
+ // incumbent cannot change between the two positions).
320
+ let regimeReported = false;
321
+ const reportRegime = () => {
322
+ if (regimeReported) return;
323
+ regimeReported = true;
324
+ const climbFloorGrade = grade(2 * STEP);
325
+ // TS narrows `best` to null in the outer flow (it cannot see the closure
326
+ // assignments in `consider`) — cast back, the same read-back as `decided`
327
+ // below.
328
+ const incumbent = best as Candidate | null;
329
+ const incumbentGrade = incumbent === null ? null : grade(incumbent.weight);
330
+ const regime: "retrieval" | "composition" = worthRunning(2 * STEP)
331
+ ? "composition"
332
+ : "retrieval";
333
+ ctx.trace?.step(
334
+ "regimePrediction",
335
+ [rItem(query, "query")],
336
+ [],
337
+ regime === "retrieval"
338
+ ? `retrieval regime — incumbent grade ${incumbentGrade} ≤ climb floor ${climbFloorGrade}, ` +
339
+ `so no mechanism floored above that grade runs; the consensus climb will not run`
340
+ : `composition regime — ${
341
+ incumbentGrade === null
342
+ ? "no incumbent (nothing grounded)"
343
+ : `incumbent grade ${incumbentGrade}`
344
+ } above climb floor ${climbFloorGrade}, so the full market and climb run`,
345
+ undefined,
346
+ {
347
+ version: 1,
348
+ regime,
349
+ incumbentGrade,
350
+ climbFloorGrade,
351
+ } satisfies RegimePredictionData,
352
+ );
353
+ };
258
354
  // Phase 3: grounding loop
259
355
  // Per-mechanism accounting (src/meter.ts). The market's whole premise is
260
356
  // that mechanisms compete on one cost scale — so the profiling read-out is
261
357
  // also per-mechanism, uniformly: the loop never asks which one it holds.
262
- const meter = ctx.meter;
263
- for (const mech of mechanisms) {
358
+ for (let mi = 0; mi < mechanisms.length; mi++) {
359
+ const mech = mechanisms[mi];
360
+ if (mi > 0) reportRegime();
264
361
  const floor = meter
265
362
  ? await meter.time(
266
363
  `${mech.name}.floor`,
@@ -309,6 +406,10 @@ export async function think(
309
406
  });
310
407
  }
311
408
  }
409
+ // A market of ONE mechanism never reaches iteration 1; the step is still
410
+ // emitted exactly once per think(), so a consumer never has to ask whether
411
+ // the list was long enough for the prediction to exist.
412
+ reportRegime();
312
413
 
313
414
  // (TS cannot see the closure assignments into `best` and narrows it to its
314
415
  // initial null, so the read-back needs the assertion.)
@@ -508,7 +609,7 @@ export async function think(
508
609
  reasoned,
509
610
  pre,
510
611
  unclimbed,
511
- decided.accounted,
612
+ primarySpans,
512
613
  );
513
614
 
514
615
  done(
@@ -32,8 +32,25 @@ import type { Leaf, Site } from "./graph-search.js";
32
32
  * the longest known leaf, chained into flat branches. Names forms the
33
33
  * query's own cut cannot, and records sub-leaf boundaries as `splits`.
34
34
  *
35
- * Both O(n · maxGroup) bounded O(1) probes — never a scan of the corpus. */
36
- export function recognise(ctx: MindContext, bytes: Uint8Array): Recognition {
35
+ * Both O(n · maxGroup) bounded O(1) probes — never a scan of the corpus.
36
+ *
37
+ * ONE READING PER BYTE STREAM, deliberately: there is no "cheap mode" that
38
+ * skips the edge-trim fallbacks. A `trimmed` variant was tried and REFUTED
39
+ * twice over. Its premise — "the trims only recover misaligned FRAGMENTS, so
40
+ * a consumer whose gate rejects fragments loses nothing" — is false: the
41
+ * left/right trim loops below exist precisely to find WHOLE trained forms
42
+ * embedded at an offset the query's own fold did not cut, and such a form has
43
+ * no structural parents or containers, so it passes the pivot's fragment gate
44
+ * and is exactly the candidate a multi-hop chain steps through. Skipping them
45
+ * narrows the pivot's evidence silently. And a per-caller variant has to key
46
+ * the memo by the variant, which breaks the "computed at most once" property
47
+ * (§2.11): the pipeline recognises a grounded answer untrimmed for
48
+ * `preConsumed`, and the pivot then recognises the same bytes again — the
49
+ * saving inverts into a doubling on the path it was measured for. */
50
+ export function recognise(
51
+ ctx: MindContext,
52
+ bytes: Uint8Array,
53
+ ): Recognition {
37
54
  // Content-keyed memo — works for both single-turn respond() and multi-turn
38
55
  // respondTurn() (where the map persists across calls). ALWAYS consulted,
39
56
  // regardless of tracing — matching perceive()'s own memo, which carries no
@@ -12,7 +12,7 @@ import { concat2, concatBytes, indexOf } from "../bytes.js";
12
12
  import type { MindContext } from "./types.js";
13
13
  import { gistOf, read, resolve, walkTree } from "./primitives.js";
14
14
  import { perceive } from "./primitives.js";
15
- import { argmaxBy, argmaxCosine, candidateGist, hubBound } from "./traverse.js";
15
+ import { argmaxCosine, candidateGist, hubBound } from "./traverse.js";
16
16
  import {
17
17
  cachedRead,
18
18
  type Junction,
@@ -365,6 +365,12 @@ export async function pivotInto(
365
365
  }
366
366
  for (const c of n.kids) queue.push(c); // breadth-first: larger regions first
367
367
  }
368
+ // THE FULL recognition, memo-shared with every other reader of these bytes.
369
+ // A "skip the edge trims here" variant was refuted (see recognise's own
370
+ // note): those trims are what find a WHOLE trained form embedded at an
371
+ // offset the answer's fold did not cut, and such a form is parentless,
372
+ // container-free and edge-bearing — i.e. exactly what the filter below
373
+ // ADMITS as a pivot, not what it rejects.
368
374
  const rec = recognise(ctx, answer);
369
375
  for (const s of rec.sites) {
370
376
  if (!consumed.has(s.payload) && ctx.store.hasNext(s.payload)) {
@@ -373,54 +379,83 @@ export async function pivotInto(
373
379
  }
374
380
  // Byte containment, longest wins — the answer literally contains the
375
381
  // pivot's bytes, and the biggest well-evidenced span is the real pivot.
376
- const found = argmaxBy(
377
- scored.keys(),
378
- (id) => {
379
- // A PIVOT MUST BE A THING THE CORPUS DEPOSITED, NOT A PIECE OF ONE.
380
- // "Longest wins" ranks candidates but never asks whether the winner is
381
- // an entity at all, and by the time a chain reaches here `consumeAll`
382
- // has taken the answer's real contexts so on a corpus of
383
- // near-identical records the field is left to whatever interned
384
- // fragments remain. Measured on a 200-line templated log corpus, query
385
- // "what happened to request_id=1042 and request_id=1077?": CAST
386
- // produced the correct comparison and one `pivotStep` replaced it
387
- // wholesale, pivoting through `s=70` — a four-byte tail of
388
- // `latency_ms=70` — onto an unrelated record (`handled 1130`).
389
- //
390
- // The separator is NOT length. Measured against the multi-hop tests'
391
- // own pivots: `Paris` (5 bytes), `Jupiter` (7), `lithium` (7), `Mona
392
- // Lisa` (9) against junk `s=70` (4) a two-quantum floor, which
393
- // confluence.ts applies to a meet for the same "one window is not an
394
- // entity" reason, discards three of the four legitimate pivots.
395
- // Entities are simply short.
396
- //
397
- // What separates them is STRUCTURAL, and the store already holds it:
398
- //
399
- // s=70 parents 2 containers 1 prevCount 0 halo no
400
- // Paris parents 0 containers 0 prevCount 1 halo yes
401
- // Jupiter parents 0 containers 0 prevCount 1 halo yes
402
- // lithium parents 0 containers 0 prevCount 1 halo yes
403
- // Mona Lisa parents 0 containers 0 prevCount 1 halo yes
404
- //
405
- // A deposited whole a context or an answer is interned in its own
406
- // right and has neither structural parents nor containment links. A
407
- // fragment is addressable ONLY because window interning made its span
408
- // addressable inside something bigger, and that containment is exactly
409
- // what `parents`/`containers` record. Reasoning steps THROUGH a fact;
410
- // a span that was never a fact on its own is not one to step through.
411
- // No constant enters it is a structural predicate, not a threshold.
412
- if (ctx.store.hasParents(id) || ctx.store.hasContainers(id)) {
413
- return -Infinity;
414
- }
415
- const bytes = read(ctx, id);
416
- if (indexOf(answer, bytes, 0) < 0) return -Infinity;
417
- for (const v of voiced) if (indexOf(v, bytes, 0) >= 0) return -Infinity;
418
- return bytes.length;
419
- },
420
- 0,
421
- true,
422
- );
423
- return found?.item ?? null;
382
+ //
383
+ // REAL SATURATION, not a hard cap: the score IS the candidate's byte
384
+ // length, so the scan is DECIDED the moment the first candidate that passes
385
+ // every filter is found in DESCENDING length order a shorter candidate can
386
+ // never outscore it. `contentLen` (the prefix-capped length read, §2.8) is
387
+ // the cheap ordering key, and the first-inserted tie-break is made explicit
388
+ // (`a.index - b.index`) so equal lengths keep `scored`'s insertion order
389
+ // exactly the tie argmaxBy(strict) used to keep. The bytes of at most ONE
390
+ // winning candidate are read; every shorter candidate the probes proposed is
391
+ // skipped without reconstruction, where the old argmax read them all.
392
+ const ranked = [...scored.keys()]
393
+ .map((id, index) => ({
394
+ id,
395
+ index,
396
+ len: ctx.store.contentLen(id, answer.length + 1),
397
+ }))
398
+ .sort((a, b) => b.len - a.len || a.index - b.index);
399
+ let pivotId: number | null = null;
400
+ for (const c of ranked) {
401
+ const id = c.id;
402
+ // A ZERO-LENGTH candidate is not a pivot. `argmaxBy(…, 0, strict)` used to
403
+ // carry this floor in its threshold argument, and dropping it here would
404
+ // admit an empty node: `indexOf(answer, <empty>)` returns 0, so every
405
+ // filter below passes and the chain would hop through nothing (§2.13 —
406
+ // empty bytes are truthy).
407
+ if (c.len === 0) continue;
408
+ // A PIVOT MUST BE A THING THE CORPUS DEPOSITED, NOT A PIECE OF ONE.
409
+ // "Longest wins" ranks candidates but never asks whether the winner is
410
+ // an entity at all, and by the time a chain reaches here `consumeAll`
411
+ // has taken the answer's real contextsso on a corpus of
412
+ // near-identical records the field is left to whatever interned
413
+ // fragments remain. Measured on a 200-line templated log corpus, query
414
+ // "what happened to request_id=1042 and request_id=1077?": CAST
415
+ // produced the correct comparison and one `pivotStep` replaced it
416
+ // wholesale, pivoting through `s=70` a four-byte tail of
417
+ // `latency_ms=70`onto an unrelated record (`handled 1130`).
418
+ //
419
+ // The separator is NOT length. Measured against the multi-hop tests'
420
+ // own pivots: `Paris` (5 bytes), `Jupiter` (7), `lithium` (7), `Mona
421
+ // Lisa` (9) against junk `s=70` (4) — a two-quantum floor, which
422
+ // confluence.ts applies to a meet for the same "one window is not an
423
+ // entity" reason, discards three of the four legitimate pivots.
424
+ // Entities are simply short.
425
+ //
426
+ // What separates them is STRUCTURAL, and the store already holds it:
427
+ //
428
+ // s=70 parents 2 containers 1 prevCount 0 halo no
429
+ // Paris parents 0 containers 0 prevCount 1 halo yes
430
+ // Jupiter parents 0 containers 0 prevCount 1 halo yes
431
+ // lithium parents 0 containers 0 prevCount 1 halo yes
432
+ // Mona Lisa parents 0 containers 0 prevCount 1 halo yes
433
+ //
434
+ // A deposited whole — a context or an answer — is interned in its own
435
+ // right and has neither structural parents nor containment links. A
436
+ // fragment is addressable ONLY because window interning made its span
437
+ // addressable inside something bigger, and that containment is exactly
438
+ // what `parents`/`containers` record. Reasoning steps THROUGH a fact;
439
+ // a span that was never a fact on its own is not one to step through.
440
+ // No constant enters — it is a structural predicate, not a threshold.
441
+ if (ctx.store.hasParents(id) || ctx.store.hasContainers(id)) continue;
442
+ // A candidate whose bytes are LONGER than the answer cannot be a
443
+ // substring of it — `indexOf` would return −1 regardless. Prune by
444
+ // length BEFORE reconstructing the bytes: `read` is an UNCAPPED read
445
+ // (AGENTS §2.8), and a resonated context far longer than the answer is
446
+ // exactly the candidate that makes it cost a whole deposit's worth of
447
+ // reconstruction for a containment test that must fail. `contentLen`
448
+ // with the `answer.length + 1` cap is the prefix-capped length read the
449
+ // same contract prescribes; the prune is byte-identical to the old
450
+ // `indexOf` miss (it returns −1 for a needle longer than the haystack).
451
+ if (c.len > answer.length) continue;
452
+ const bytes = read(ctx, id);
453
+ if (indexOf(answer, bytes, 0) < 0) continue;
454
+ if (voiced.some((v) => indexOf(v, bytes, 0) >= 0)) continue;
455
+ pivotId = id;
456
+ break;
457
+ }
458
+ return pivotId;
424
459
  }
425
460
 
426
461
  /** Which of the given labelled forms a span MEANS — generic resonance over
package/src/store.ts CHANGED
@@ -1269,7 +1269,27 @@ export abstract class AbstractStore implements Store {
1269
1269
  parts.push(child);
1270
1270
  got += child.length;
1271
1271
  }
1272
- return concat(parts);
1272
+ const out = concat(parts);
1273
+ // Cache the BRANCH too, not just the leaf above. Reconstruction is a pure
1274
+ // function of the store, so this is a transparent cache in the strict sense
1275
+ // — an eviction costs a re-walk and nothing else — which is exactly what
1276
+ // `_bytesCache`'s "smallest"/"clock" configuration is for.
1277
+ //
1278
+ // Caching only leaves made every branch re-walk its whole subtree on every
1279
+ // request, and the DAG is hash-consed, so the same children recur under many
1280
+ // parents. Measured on the 18.9M-node store, ONE 1,314-byte query:
1281
+ // 20,021,474 `_prefix` calls over 469,083 distinct ids (42.7x reuse) to
1282
+ // produce 87,789 results — 97.7% of the work re-derived bytes it had already
1283
+ // built. One single-byte leaf was reconstructed 2,599,984 times. Measuring
1284
+ // reuse at the TOP level only shows 1.1x and hides all of it.
1285
+ //
1286
+ // Only a COMPLETE reconstruction may be cached: `_prefix` is also called
1287
+ // with a cap, and a truncated prefix stored under `id` would be served as
1288
+ // if it were the node's whole content by the `_bytesCache` hit above.
1289
+ // `got < maxLen` is that proof — the walk ran out of children before it ran
1290
+ // out of budget, so nothing below was truncated either.
1291
+ if (got < maxLen) this._bytesCache.set(id, out);
1292
+ return out;
1273
1293
  }
1274
1294
 
1275
1295
  contentLen(id: NodeId, cap = Infinity): number {