@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
@@ -29,6 +29,9 @@ import { aluToMechanism, defaultMechanisms, think } from "./pipeline.js";
29
29
  import { articulate } from "./articulation.js";
30
30
  import { ingest } from "./learning.js";
31
31
  import { rItem } from "./trace.js";
32
+ // The work meter is exported from src/index.ts (via src/meter.ts) — the one
33
+ // definition; the Mind only consumes it.
34
+ import { Meter } from "../meter.js";
32
35
  // ═══════════════════════════════════════════════════════════════════════════
33
36
  // THE MIND
34
37
  // ═══════════════════════════════════════════════════════════════════════════
@@ -53,6 +56,19 @@ export class Mind {
53
56
  * `false` to disable canonical resolution, or null to let each entry
54
57
  * point decide (text entry points inject {@link textCanon}). */
55
58
  _canonOpt = null;
59
+ /** The work accumulator for the inference call in flight — see
60
+ * {@link MindContext.meter}. Non-null only between beginResponse and
61
+ * endResponse, and only when the Mind was constructed with
62
+ * `{ profile: true }`. */
63
+ meter = null;
64
+ /** Whether {@link MindOptions.profile} was set. */
65
+ _profile = false;
66
+ /** The computational-usage report of the LAST completed inference call, or
67
+ * null when profiling is off (or nothing has been asked yet). Overwritten
68
+ * by every `respond`/`respondTurn`; copy it if you are aggregating. See
69
+ * {@link import("../meter.js").CostReport} and `sumReports`/`formatReport`
70
+ * for battery-level aggregation. */
71
+ lastCost = null;
56
72
  /** Memo of the consensus climb — content-keyed. See {@link MindContext.climbMemo}. */
57
73
  climbMemo = null;
58
74
  /** Memo of recognise() — content-keyed. See {@link MindContext.recogniseMemo}. */
@@ -120,8 +136,9 @@ export class Mind {
120
136
  this.store = storeArg;
121
137
  }
122
138
  else {
123
- const { store: optsStore, mechanisms: userMechs, mechanismFactories: userFacts, canon: optsCanon, ...rest } = (optsOrCfg ?? {});
139
+ const { store: optsStore, mechanisms: userMechs, mechanismFactories: userFacts, canon: optsCanon, profile: optsProfile, ...rest } = (optsOrCfg ?? {});
124
140
  this._canonOpt = optsCanon ?? null;
141
+ this._profile = optsProfile === true;
125
142
  this.cfg = resolveConfig(rest);
126
143
  this.store = optsStore ?? new SQliteStore({
127
144
  maxGroup: this.cfg.geometry.maxGroup,
@@ -180,16 +197,44 @@ export class Mind {
180
197
  perceive(input, leafAt, lookup) {
181
198
  return perceiveImpl(this, input, leafAt, lookup);
182
199
  }
183
- /** Open one response's transient state — the tracer and the per-response
184
- * memos. Paired with {@link endResponse}; the ONE place this state is
185
- * created, so adding a memo cannot forget its reset. */
186
- beginResponse(inspectRationale, canon) {
200
+ /** Open one response's transient state — the tracer, the per-response
201
+ * memos, the work meter. The ONE place this state is created, and it
202
+ * serves BOTH entry points: `respond` takes fresh per-response memos,
203
+ * `respondTurn` passes its conversation, whose memos persist across turns
204
+ * (content-keyed, so the previous turn's results are found by this turn's
205
+ * sub-span calls) and whose `resolvedSubtrees` makes foldTree O(suffix)
206
+ * instead of O(context). respondTurn used to inline its own copy of this
207
+ * and of {@link endResponse}; the two drifted (a memo added to one was
208
+ * silently absent from the other), so there is exactly one pair now. */
209
+ beginResponse(inspectRationale, canon, conv) {
187
210
  this.trace = inspectRationale ? new Rationale(inspectRationale) : null;
188
- this.climbMemo = new Map();
189
- this.recogniseMemo = new Map();
190
- this.perceiveMemo = new Map();
211
+ this.climbMemo = conv ? conv.climbMemo : new Map();
212
+ this.recogniseMemo = conv ? conv.recogniseMemo : new Map();
213
+ this.perceiveMemo = conv ? conv.perceiveMemo : new Map();
214
+ this._resolvedSubtrees = conv ? conv.resolvedSubtrees : null;
191
215
  this.canon = canon ?? null;
192
216
  this.canonMemo = canon ? new Map() : null;
217
+ this._beginMeter();
218
+ }
219
+ /** Open (or leave closed) the response's work accumulator. Separate from
220
+ * {@link beginResponse} because {@link respondTurn} keeps its own
221
+ * conversation-scoped lifecycle and must not create fresh per-response
222
+ * memos — but it DOES meter, through this same pair. */
223
+ _beginMeter() {
224
+ if (!this._profile)
225
+ return;
226
+ this.meter = new Meter();
227
+ this.store.meter = this.meter;
228
+ }
229
+ /** Close the accumulator and publish its report. Detaching from the store
230
+ * matters: a Mind that shares a store with another Mind must not keep
231
+ * charging that store's reads to a finished response. */
232
+ _endMeter(queryBytes) {
233
+ if (this.meter === null)
234
+ return;
235
+ this.lastCost = this.meter.report(queryBytes);
236
+ this.store.meter = null;
237
+ this.meter = null;
193
238
  }
194
239
  /** The canonicalizer a response should carry: the Mind-level option when
195
240
  * set (or none when explicitly disabled), else the entry point's own
@@ -200,12 +245,20 @@ export class Mind {
200
245
  return this._canonOpt ?? entryDefault;
201
246
  }
202
247
  /** Close one response's transient state — every per-response field, incl.
203
- * the edge guide/choices `think` sets mid-flight. */
204
- endResponse() {
248
+ * the edge guide/choices `think` sets mid-flight, and the meter's report.
249
+ *
250
+ * A conversation's memo MAPS were mutated in place, so `data.*` still
251
+ * points at them and there is nothing to save back. Clearing the Mind's
252
+ * references is what matters: a concurrently-started `respond()` swaps its
253
+ * own fresh maps into these pointers, and copying back from them here
254
+ * would inject a foreign response's memos into the conversation. */
255
+ endResponse(queryBytes) {
256
+ this._endMeter(queryBytes);
205
257
  this.trace = null;
206
258
  this.climbMemo = null;
207
259
  this.recogniseMemo = null;
208
260
  this.perceiveMemo = null;
261
+ this._resolvedSubtrees = null;
209
262
  this.canon = null;
210
263
  this.canonMemo = null;
211
264
  this._edgeGuide = null;
@@ -217,26 +270,38 @@ export class Mind {
217
270
  async _respondImpl(queryBytes, inspectRationale, traceLabel = "respond", canon = null) {
218
271
  this.beginResponse(inspectRationale, canon);
219
272
  try {
220
- const top = this.trace?.enter(traceLabel, [
221
- rItem(queryBytes, "query"),
222
- ]);
223
- const thought = await think(this, queryBytes, this.mechanisms);
224
- if (thought === null) {
225
- top?.done([], "nothing to perceive or an empty store — no answer");
226
- return { v: null, bytes: new Uint8Array(0) };
227
- }
228
- const voiced = await articulate(this, thought.bytes, queryBytes);
229
- top?.done([rItem(voiced, "answer", resolveImpl(this, voiced) ?? undefined)], "the answer, re-voiced in the asker's words");
230
- return {
231
- v: gistOf(this, voiced),
232
- bytes: voiced,
233
- provenance: thought.provenance,
234
- };
273
+ return await this._groundAndVoice(queryBytes, traceLabel);
235
274
  }
236
275
  finally {
237
- this.endResponse();
276
+ this.endResponse(queryBytes.length);
238
277
  }
239
278
  }
279
+ /** The ONE path from query bytes to a voiced answer: ground (think), then
280
+ * re-voice in the asker's words (articulate). Both entry points run
281
+ * exactly this — they differ only in the LIFECYCLE around it (fresh
282
+ * per-response memos vs. a conversation's persistent ones) and in what
283
+ * they do with the answer afterwards. It must be called between
284
+ * {@link beginResponse} and {@link endResponse}. */
285
+ async _groundAndVoice(queryBytes, traceLabel) {
286
+ const top = this.trace?.enter(traceLabel, [rItem(queryBytes, "query")]);
287
+ const meter = this.meter;
288
+ const thought = meter
289
+ ? await meter.time("think", () => think(this, queryBytes, this.mechanisms))
290
+ : await think(this, queryBytes, this.mechanisms);
291
+ if (thought === null) {
292
+ top?.done([], "nothing to perceive or an empty store — no answer");
293
+ return { v: null, bytes: new Uint8Array(0) };
294
+ }
295
+ const voiced = meter
296
+ ? await meter.time("articulate", () => articulate(this, thought.bytes, queryBytes))
297
+ : await articulate(this, thought.bytes, queryBytes);
298
+ top?.done([rItem(voiced, "answer", resolveImpl(this, voiced) ?? undefined)], "the answer, re-voiced in the asker's words");
299
+ return {
300
+ v: gistOf(this, voiced),
301
+ bytes: voiced,
302
+ provenance: thought.provenance,
303
+ };
304
+ }
240
305
  async respond(input, inspectRationale) {
241
306
  // A STRING input is text by nature: it carries the text equivalence even
242
307
  // through the generic entry point. Raw bytes / grids carry only the
@@ -358,82 +423,36 @@ export class Mind {
358
423
  throw new Error(`Conversation ${conv.id} not found`);
359
424
  const turnBytes = inputBytes(this, turn);
360
425
  // Incremental perception — O(turn) instead of O(context).
361
- const tree = this._growContext(data, turnBytes);
426
+ this._growContext(data, turnBytes);
362
427
  const newContext = data.bytes;
363
- // Swap in the conversation's persistent state so the inference
364
- // pipeline does not re-process the prefix from scratch.
365
- // perceiveMemo / recogniseMemo / climbMemo content-keyed;
366
- // the prefix's results from the previous turn are found by
367
- // the current turn's sub-span calls.
368
- // _resolvedSubtrees — Sema-node-keyed; when the pyramid reuses
369
- // prefix subtrees (identical objects), foldTree returns their
370
- // ids immediately — O(suffix) instead of O(context).
371
- this.perceiveMemo = data.perceiveMemo;
372
- this.recogniseMemo = data.recogniseMemo;
373
- this.climbMemo = data.climbMemo;
374
- this._resolvedSubtrees = data.resolvedSubtrees;
375
- this.trace = inspectRationale ? new Rationale(inspectRationale) : null;
376
- // A string turn is text by nature — carry the text equivalence, same as
428
+ // The conversation's persistent memos and subtree cache are swapped in
429
+ // by beginResponse (see there) the SAME lifecycle respond() uses, so a
430
+ // memo added in one place can never be missing from the other. A string
431
+ // turn is text by nature and carries the text equivalence, same as
377
432
  // respond() (see _canonFor).
378
- this.canon = this._canonFor(typeof turn === "string" ? textCanon : null);
379
- this.canonMemo = this.canon ? new Map() : null;
433
+ //
434
+ // No recognise-memo pre-seeding here: that used to be necessary because
435
+ // the flat/positional fold lost visibility into an earlier turn's own
436
+ // structure once later bytes shifted its position (foldTree no longer
437
+ // visited the turn's root node). The STABLE-PREFIX fold (see {@link
438
+ // ConversationData}) makes every turn's subtree independent of what
439
+ // follows it by construction, so recognise() finds it correctly on its
440
+ // own, first-touch, exactly once per turn.
441
+ this.beginResponse(inspectRationale, this._canonFor(typeof turn === "string" ? textCanon : null), data);
380
442
  try {
381
- // No recognise-memo pre-seeding here: that used to be necessary
382
- // because the flat/positional fold lost visibility into an earlier
383
- // turn's own structure once later bytes shifted its position
384
- // (foldTree no longer visited the turn's root node). The
385
- // STABLE-PREFIX fold (see {@link ConversationData}) makes every
386
- // turn's subtree independent of what follows it by construction, so
387
- // recognise() finds it correctly on its own, first-touch, exactly
388
- // once per turn — no workaround needed, and none pre-empting
389
- // recognise()'s own memo with a partial result.
390
- const top = this.trace?.enter("respondTurn", [
391
- rItem(newContext, "query"),
392
- ]);
393
- const thought = await think(this, newContext, this.mechanisms);
394
- if (thought === null) {
395
- top?.done([], "nothing to perceive or an empty store — no answer");
396
- return {
397
- response: { v: null, bytes: new Uint8Array(0) },
398
- state: this.conversationState(conv),
399
- };
400
- }
401
- const voiced = await articulate(this, thought.bytes, newContext);
402
- top?.done([rItem(voiced, "answer", resolveImpl(this, voiced) ?? undefined)], "the answer, re-voiced in the asker's words");
443
+ const response = await this._groundAndVoice(newContext, "respondTurn");
403
444
  // The REPLY joins the accumulated context the same way a turn does
404
445
  // ({@link addTurn}): raw byte append plus a boundary offset — never a
405
446
  // separator. A conversation's context is the full exchange, exactly
406
447
  // the cumulative continuous shape multi-turn training deposits, so a
407
448
  // later turn can refer to what was ANSWERED ("which of those two…"),
408
449
  // not only to what was asked.
409
- if (voiced.length > 0)
410
- this.addTurn(conv, voiced);
411
- return {
412
- response: {
413
- v: gistOf(this, voiced),
414
- bytes: voiced,
415
- provenance: thought.provenance,
416
- },
417
- state: this.conversationState(conv),
418
- };
450
+ if (response.bytes.length > 0)
451
+ this.addTurn(conv, response.bytes);
452
+ return { response, state: this.conversationState(conv) };
419
453
  }
420
454
  finally {
421
- // No save-back: the conversation's memo MAPS were mutated in place —
422
- // `data.*` still points at them. Re-assigning from the Mind-level
423
- // pointers here was a no-op in the single-flight case and, under a
424
- // concurrently-started respond() (which swaps its own fresh maps into
425
- // those pointers), would inject a FOREIGN response's memos into this
426
- // conversation. Clear Mind references — non-conversation respond()
427
- // calls get fresh per-response memos.
428
- this.trace = null;
429
- this.perceiveMemo = null;
430
- this.recogniseMemo = null;
431
- this.climbMemo = null;
432
- this.canon = null;
433
- this.canonMemo = null;
434
- this._resolvedSubtrees = null;
435
- this._edgeGuide = null;
436
- this._edgeChoice.clear();
455
+ this.endResponse(newContext.length);
437
456
  }
438
457
  }
439
458
  /** Text view of {@link respondTurn}. See {@link respondText} for the
@@ -37,9 +37,21 @@ export declare class Precomputed {
37
37
  * any future identity-based mechanism reads the same cache. */
38
38
  windowsOf(anchor: number): Map<number, number>;
39
39
  /** Shared memo for {@link reachOf} (structural-IDF reads): a window's
40
- * ancestor reach is a pure function of the read-only store, so one
41
- * response-scoped memo serves every mechanism that prices commonality. */
42
- readonly reachMemo: Map<number, AncestorReach>;
40
+ * ancestor reach is a pure function of the read-only store, so one memo
41
+ * serves every mechanism that prices commonality — AND the consensus
42
+ * climb, which is the largest consumer and used to build its own. The
43
+ * ONE definition of its lifetime lives in traverse.ts
44
+ * ({@link sharedReachMemo}): response-scoped for respond(),
45
+ * conversation-scoped across turns, always cold under a trace. */
46
+ private _reach?;
47
+ get reachMemo(): Map<number, AncestorReach>;
48
+ /** Charge a lazily-shared analysis to its OWN phase rather than to the
49
+ * mechanism that happened to first-touch it. Without this the profile
50
+ * reads as "cast.floor costs 2 s" when what actually cost 2 s is the
51
+ * consensus climb — which cast merely paid for on everyone's behalf, and
52
+ * which every later consumer then got free. Attribution must follow the
53
+ * work, not the caller. */
54
+ private shared;
43
55
  private _attention?;
44
56
  /** The full consensus climb (roots + ranked anchors) — the query-level
45
57
  * evidence CAST, confluence, extraction, recall's scaffolding tier, and
@@ -109,6 +121,24 @@ export interface MechanismResult {
109
121
  /** Override the mechanism's default provenance for this result.
110
122
  * When absent, the pipeline uses `mech.provenance`. */
111
123
  provenance?: string;
124
+ /** This grounding is a COMPLETE trained answer — post-grounding must not
125
+ * extend it. Declared by the mechanism about its own result, exactly like
126
+ * `accounted`/`used`/`unexplained`; the decider honours the property and
127
+ * never asks which mechanism set it, so the market stays uniform.
128
+ *
129
+ * Set it only when the answer is a stored form's OWN continuation reached
130
+ * through an identity claim about the query — i.e. the query IS some
131
+ * trained context, so its continuation is the whole read-out and a further
132
+ * multi-hop pivot would chain PAST the fact that produced the answer.
133
+ * That is the same reasoning `reason`'s echo guard already applies to a
134
+ * query that resolves exactly (see reasoning.ts); this carries the claim
135
+ * for the mechanisms that establish the identity by another route.
136
+ *
137
+ * Observed without it: the correct "What is the process of
138
+ * photosynthesis?" grounding was pivoted forward four times, out of the
139
+ * fact that answered it and into an unrelated "Hello! How can I assist you
140
+ * today?" conversational turn. */
141
+ complete?: boolean;
112
142
  }
113
143
  export interface PipelineMechanism {
114
144
  /** Stable identifier for trace/debug. */
@@ -12,11 +12,13 @@
12
12
  // 3. VISIBLE BUDGET — every mechanism carries its own caps internally (√N, k).
13
13
  // 4. TRAVELING EVIDENCE — run() returns MechanismResult with accounted, moves,
14
14
  // and unexplained. The pipeline computes the weight.
15
+ import { indexOf } from "../bytes.js";
16
+ import { dominates } from "../geometry.js";
15
17
  import { windowIds } from "./canonical.js";
16
18
  import { read, resolve } from "./primitives.js";
17
- import { alignGraded } from "./match.js";
19
+ import { alignGraded, skillExemplar } from "./match.js";
18
20
  import { climbAttentionAll } from "./attention.js";
19
- import { skillExemplar } from "./mechanisms/extraction.js";
21
+ import { sharedReachMemo } from "./traverse.js";
20
22
  // ── Precomputed ──────────────────────────────────────────────────────────────
21
23
  //
22
24
  // Precomputed is a LAZY container for structural analyses of the query — the
@@ -88,29 +90,46 @@ export class Precomputed {
88
90
  return w;
89
91
  }
90
92
  /** Shared memo for {@link reachOf} (structural-IDF reads): a window's
91
- * ancestor reach is a pure function of the read-only store, so one
92
- * response-scoped memo serves every mechanism that prices commonality. */
93
- reachMemo = new Map();
93
+ * ancestor reach is a pure function of the read-only store, so one memo
94
+ * serves every mechanism that prices commonality — AND the consensus
95
+ * climb, which is the largest consumer and used to build its own. The
96
+ * ONE definition of its lifetime lives in traverse.ts
97
+ * ({@link sharedReachMemo}): response-scoped for respond(),
98
+ * conversation-scoped across turns, always cold under a trace. */
99
+ _reach;
100
+ get reachMemo() {
101
+ return this._reach ??= sharedReachMemo(this.ctx);
102
+ }
94
103
  // ── Expensive lazy analyses ───────────────────────────────────────────
95
104
  //
96
105
  // Async, cached-by-promise: the first caller starts the computation, every
97
106
  // later caller (any mechanism, any phase) awaits the same promise. A
98
107
  // mechanism MUST check its cheap floor gates and the pipeline's
99
108
  // `worthRunning` predicate before first-touching one of these.
109
+ /** Charge a lazily-shared analysis to its OWN phase rather than to the
110
+ * mechanism that happened to first-touch it. Without this the profile
111
+ * reads as "cast.floor costs 2 s" when what actually cost 2 s is the
112
+ * consensus climb — which cast merely paid for on everyone's behalf, and
113
+ * which every later consumer then got free. Attribution must follow the
114
+ * work, not the caller. */
115
+ shared(phase, fn) {
116
+ const meter = this.ctx.meter;
117
+ return meter ? meter.time(phase, fn) : fn();
118
+ }
100
119
  _attention;
101
120
  /** The full consensus climb (roots + ranked anchors) — the query-level
102
121
  * evidence CAST, confluence, extraction, recall's scaffolding tier, and
103
122
  * fusion all share. Computed on first access; a query no mechanism
104
123
  * climbs for (e.g. one an extension decided outright) never pays for it. */
105
124
  attention() {
106
- return this._attention ??= climbAttentionAll(this.ctx, this.query, this.k);
125
+ return this._attention ??= this.shared("attention", () => climbAttentionAll(this.ctx, this.query, this.k));
107
126
  }
108
127
  _weave;
109
128
  /** Result of {@link alignGraded} for the first k ranked anchors —
110
129
  * O(k · |query| · |ctx|). Consumed by CAST; reusable by any future
111
130
  * mechanism doing analogical transfer. */
112
131
  weave() {
113
- return this._weave ??= this.attention().then((climb) => computeWeave(this.ctx, this.query, this, climb));
132
+ return this._weave ??= this.attention().then((climb) => this.shared("weave", async () => computeWeave(this.ctx, this.query, this, climb)));
114
133
  }
115
134
  /** Span-shaped classification of one ranked anchor, memoised per anchor id
116
135
  * so repeated calls (extraction's own early-exit scan, any future
@@ -124,7 +143,7 @@ export class Precomputed {
124
143
  spanShapedOf(anchor) {
125
144
  let p = this._spanShaped.get(anchor);
126
145
  if (p === undefined) {
127
- p = skillExemplar(this.ctx, anchor, this.guide);
146
+ p = this.shared("spanShaped", () => skillExemplar(this.ctx, anchor, this.guide));
128
147
  this._spanShaped.set(anchor, p);
129
148
  }
130
149
  return p;
@@ -152,6 +171,7 @@ function computeWeave(ctx, query, pre, climb) {
152
171
  const rankedCapped = ranked.length > pre.k ? ranked.slice(0, pre.k) : ranked;
153
172
  const depth = new Float64Array(query.length);
154
173
  const points = [];
174
+ const byAnchor = new Map();
155
175
  // WEAVE-SCALE anchors only: CAST transfers structure between things the
156
176
  // QUERY weaves together — query-scale structures. A context an order of
157
177
  // magnitude beyond the query is not woven BY the query (the query can at
@@ -166,6 +186,34 @@ function computeWeave(ctx, query, pre, climb) {
166
186
  // uncapped weaves spent 5–8s per query recognising conversation-length
167
187
  // anchors that could never form a weave point.
168
188
  const capBytes = query.length * quantum;
189
+ // EXCLUSIVITY IS ARBITRATED BY THE CLIMB'S VOTE ORDER, DELIBERATELY. A query
190
+ // byte can only be independent evidence for ONE point, so points are built in
191
+ // ranked order and each new point's runs are trimmed against every point
192
+ // already accepted; a point left with no run of a full quantum drops out of
193
+ // the weave.
194
+ //
195
+ // That reads like first-come-wins — a point that merely ranked higher taking
196
+ // a span from the point that actually explains it — and arbitrating by LOCAL
197
+ // evidence instead (ownership of each byte to the longest covering run, then
198
+ // the heavier weight, then rank) was implemented and MEASURED: test/29 went
199
+ // 9/2 to 7/4, and the new failures name the reason. CAST requires the weave
200
+ // to touch a COMMITTED point of attention ("2 aligned structure(s), but none
201
+ // is one of the climb's 1 committed root(s)"), and it was precisely the vote
202
+ // order that kept the committed root's own point alive in the weave. Local
203
+ // run length knows nothing about what the climb settled on, so it evicted the
204
+ // root's evidence and left CAST refusing on its own consistency check.
205
+ //
206
+ // So the vote order here is not an accident of construction — it is what
207
+ // holds the weave and the climb to the same conclusion. Weave-local
208
+ // measures decide what is FRAME inside the weave (see the frame gates in
209
+ // cast.ts); which structures are in the weave at all stays the climb's call.
210
+ //
211
+ // TWO PASSES. `depth` — how much of the weave agrees on each query byte, and
212
+ // therefore what counts as FRAME — must be the whole weave's, not "whatever
213
+ // has been processed so far": read in one pass it made a candidate's own
214
+ // frame reading depend on its rank, and the proposed-run gate below needs the
215
+ // real thing.
216
+ const cands = [];
169
217
  for (const cand of rankedCapped) {
170
218
  const ctxBytes = read(ctx, cand.anchor, capBytes + 1);
171
219
  if (ctxBytes.length === 0 || ctxBytes.length > capBytes)
@@ -177,6 +225,9 @@ function computeWeave(ctx, query, pre, climb) {
177
225
  for (let i = r.qs; i < r.qe; i++)
178
226
  depth[i] += r.weight;
179
227
  }
228
+ cands.push({ cand, ctxBytes, raw });
229
+ }
230
+ for (const { cand, ctxBytes, raw } of cands) {
180
231
  const free = [];
181
232
  for (const r of raw) {
182
233
  let { qs, qe, cs, weight } = r;
@@ -201,12 +252,130 @@ function computeWeave(ctx, query, pre, climb) {
201
252
  }
202
253
  }
203
254
  if (free.length > 0) {
204
- points.push({
255
+ const pt = {
205
256
  anchor: cand.anchor,
206
257
  vote: cand.vote,
207
258
  ctx: ctxBytes,
208
259
  runs: free,
209
- });
260
+ };
261
+ byAnchor.set(cand.anchor, pt);
262
+ points.push(pt);
263
+ }
264
+ }
265
+ // A byte is FRAME when more than half the weave shares it, and a SPAN is
266
+ // frame when more than half its bytes are — the same two-level
267
+ // half-dominance reading cast.ts's own frame gate uses, over the same
268
+ // `depth`. Read against the accepted POINTS (as cast.ts does), so it is
269
+ // only meaningful once phase 1 has run.
270
+ const framed = (from, to) => {
271
+ let n = 0;
272
+ for (let i = from; i < to; i++)
273
+ if (dominates(depth[i], points.length))
274
+ n++;
275
+ return dominates(n, to - from);
276
+ };
277
+ // PHASE 2 — THE CLIMB'S OWN CONCLUSION IS AN ALIGNMENT THE LITERAL MATCHER
278
+ // CANNOT SEE. `alignRuns` seeds on W-grams, so two forms differing by a
279
+ // single byte share no run at all: on `How is ice like steel?` against a
280
+ // store holding `Ice is cold`, the query's `ice` and the stored `Ice` agree
281
+ // on only `ce ` — three bytes, never seeded — so that structure entered the
282
+ // weave carrying nothing but the ` is ` scaffolding every exemplar shares,
283
+ // lost it to the first point that claimed it, and vanished. The climb had
284
+ // ALREADY identified it: its resonance elected `Ice is cold` from the query
285
+ // span `ce l` and `Steel is hard` from `stee`, two disjoint spans each naming
286
+ // its own structure, weighed through the region's contrastive margin and its
287
+ // IDF — gates the aligner has no equivalent of.
288
+ //
289
+ // So the climb PROPOSES the pairing (which structure, which query span) and
290
+ // bytes DECIDE its terms (§2.3). Three gates, each one measured:
291
+ //
292
+ // • it may only take query bytes NO literal run claimed. Run inline with
293
+ // phase 1 this did the opposite of "exact decides" — a higher-ranked
294
+ // candidate's proposal trimmed a lower-ranked candidate's byte-for-byte
295
+ // match out of existence (`he W`, proposed for `a nickname meaning the
296
+ // divine one`, cut the literal `The ` out of `The Starry Night was
297
+ // painted by Vincent van Gogh.` and CAST's redirection lost its
298
+ // dominant — test/29 C4). Hence a second pass, after every literal run
299
+ // is placed.
300
+ // • the literal agreement must DOMINATE the span. A climb vote is not by
301
+ // itself an alignment: on `The Persistence of Memory was painted by
302
+ // Salvador Dali.` the climb elects `The Starry Night…` from the span
303
+ // ` Dali.`, which shares barely a byte with it — the resonance was
304
+ // carried by the frame those exemplars share. Admitting it let CAST
305
+ // weave points out of pure scaffolding and out-account the correct
306
+ // extraction (test/00, test/24). Where the proposal is real the
307
+ // agreement is overwhelming: both C1 spans agree on three of four bytes.
308
+ // • and the span must not be FRAME. Literal dominance alone is too weak
309
+ // at this scale — a 4-byte span agrees three-of-four with half the
310
+ // corpus by accident (`he W` against `a nickname meaning the divine
311
+ // one`). Frame is the weave-local measure of exactly that.
312
+ const claimed = new Uint8Array(query.length);
313
+ for (const p of points) {
314
+ for (const r of p.runs)
315
+ claimed.fill(1, r.qs, r.qe);
316
+ }
317
+ for (const { cand, ctxBytes } of cands) {
318
+ if (cand.end > cand.start) {
319
+ let qs = cand.start;
320
+ let qe = cand.end;
321
+ while (qs < qe && claimed[qs])
322
+ qs++;
323
+ while (qe > qs && claimed[qe - 1])
324
+ qe--;
325
+ let clear = true;
326
+ for (let i = qs; i < qe; i++)
327
+ if (claimed[i])
328
+ clear = false;
329
+ if (clear && qe - qs >= Math.min(quantum, ctxBytes.length)) {
330
+ // The gate only asks whether the agreement DOMINATES the span, so
331
+ // search DOWNWARD from the whole span and stop at the first hit: the
332
+ // first length found is both the longest agreement and, by
333
+ // construction, already past the dominance bar. At most O(W²) bounded
334
+ // substring probes — a span is one segment (≤ 2W) — where a full
335
+ // longest-common-substring scan would be O(|span|² · |ctx|) against a
336
+ // context that may be W× the query.
337
+ const span = query.subarray(qs, qe);
338
+ const bar = Math.floor(span.length / 2) + 1; // dominates(bar, length)
339
+ let bestLen = 0;
340
+ let bestCs = 0;
341
+ for (let len = span.length; len >= bar && bestLen === 0; len--) {
342
+ for (let off = 0; off + len <= span.length; off++) {
343
+ const at = indexOf(ctxBytes, span.subarray(off, off + len), 0);
344
+ if (at < 0)
345
+ continue;
346
+ bestLen = len;
347
+ // Where the span's FIRST byte lands, so `cs` means the same thing
348
+ // it does for a literal run: the context offset the run starts at.
349
+ bestCs = Math.max(0, at - off);
350
+ break;
351
+ }
352
+ }
353
+ if (bestLen > 0 && !framed(qs, qe)) {
354
+ const run = {
355
+ qs,
356
+ qe,
357
+ cs: bestCs,
358
+ weight: bestLen / (qe - qs),
359
+ proposed: true,
360
+ };
361
+ claimed.fill(1, qs, qe);
362
+ const pt = byAnchor.get(cand.anchor);
363
+ if (!pt) {
364
+ const made = {
365
+ anchor: cand.anchor,
366
+ vote: cand.vote,
367
+ ctx: ctxBytes,
368
+ runs: [run],
369
+ };
370
+ byAnchor.set(cand.anchor, made);
371
+ points.push(made);
372
+ }
373
+ else {
374
+ pt.runs.push(run);
375
+ pt.runs.sort((x, y) => x.qs - y.qs);
376
+ }
377
+ }
378
+ }
210
379
  }
211
380
  }
212
381
  return { points, depth };