@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/mind.ts CHANGED
@@ -155,6 +155,9 @@ import { aluToMechanism, defaultMechanisms, think } from "./pipeline.js";
155
155
  import { articulate } from "./articulation.js";
156
156
  import { ingest } from "./learning.js";
157
157
  import { rItem } from "./trace.js";
158
+ // The work meter is exported from src/index.ts (via src/meter.ts) — the one
159
+ // definition; the Mind only consumes it.
160
+ import { type CostReport, Meter } from "../meter.js";
158
161
 
159
162
  // ── MindOptions ───────────────────────────────────────────────────────────
160
163
 
@@ -174,6 +177,15 @@ export interface MindOptions {
174
177
  mechanismFactories?: ((
175
178
  host: import("../extension.js").ExtensionHost,
176
179
  ) => import("./pipeline-mechanism.js").PipelineMechanism)[];
180
+ /** Measure the computational usage of every inference call — see
181
+ * src/meter.ts. Off by default and free when off (one null check per
182
+ * store read); on, each `respond`/`respondTurn` leaves a {@link
183
+ * Mind.lastCost} report behind. Counters are deterministic, so two runs
184
+ * of the same query on the same store are diffable; the millisecond
185
+ * fields are not. Profiling NEVER changes an answer — but note that
186
+ * attaching a RATIONALE does (traced responses bypass the ctx memos,
187
+ * AGENTS §2.11), so profile without a trace. */
188
+ profile?: boolean;
177
189
  /** Content canonicalizer applied to EVERY response (any modality) for
178
190
  * equivalence-class resolution — see src/canon.ts. Text entry points
179
191
  * ({@link Mind.respondText}, {@link Mind.respondTurnText}) inject the
@@ -215,6 +227,22 @@ export class Mind implements MindContext {
215
227
  * point decide (text entry points inject {@link textCanon}). */
216
228
  private _canonOpt: Canon | false | null = null;
217
229
 
230
+ /** The work accumulator for the inference call in flight — see
231
+ * {@link MindContext.meter}. Non-null only between beginResponse and
232
+ * endResponse, and only when the Mind was constructed with
233
+ * `{ profile: true }`. */
234
+ meter: Meter | null = null;
235
+
236
+ /** Whether {@link MindOptions.profile} was set. */
237
+ private _profile = false;
238
+
239
+ /** The computational-usage report of the LAST completed inference call, or
240
+ * null when profiling is off (or nothing has been asked yet). Overwritten
241
+ * by every `respond`/`respondTurn`; copy it if you are aggregating. See
242
+ * {@link import("../meter.js").CostReport} and `sumReports`/`formatReport`
243
+ * for battery-level aggregation. */
244
+ lastCost: CostReport | null = null;
245
+
218
246
  /** Memo of the consensus climb — content-keyed. See {@link MindContext.climbMemo}. */
219
247
  climbMemo: Map<string, Map<string, AttentionRead>> | null = null;
220
248
 
@@ -326,9 +354,11 @@ export class Mind implements MindContext {
326
354
  mechanisms: userMechs,
327
355
  mechanismFactories: userFacts,
328
356
  canon: optsCanon,
357
+ profile: optsProfile,
329
358
  ...rest
330
359
  } = (optsOrCfg ?? {}) as MindOptions;
331
360
  this._canonOpt = optsCanon ?? null;
361
+ this._profile = optsProfile === true;
332
362
  this.cfg = resolveConfig(rest as Partial<MindConfig>);
333
363
  this.store = optsStore ?? new SQliteStore({
334
364
  maxGroup: this.cfg.geometry.maxGroup,
@@ -417,19 +447,48 @@ export class Mind implements MindContext {
417
447
  return perceiveImpl(this, input, leafAt, lookup);
418
448
  }
419
449
 
420
- /** Open one response's transient state — the tracer and the per-response
421
- * memos. Paired with {@link endResponse}; the ONE place this state is
422
- * created, so adding a memo cannot forget its reset. */
450
+ /** Open one response's transient state — the tracer, the per-response
451
+ * memos, the work meter. The ONE place this state is created, and it
452
+ * serves BOTH entry points: `respond` takes fresh per-response memos,
453
+ * `respondTurn` passes its conversation, whose memos persist across turns
454
+ * (content-keyed, so the previous turn's results are found by this turn's
455
+ * sub-span calls) and whose `resolvedSubtrees` makes foldTree O(suffix)
456
+ * instead of O(context). respondTurn used to inline its own copy of this
457
+ * and of {@link endResponse}; the two drifted (a memo added to one was
458
+ * silently absent from the other), so there is exactly one pair now. */
423
459
  private beginResponse(
424
460
  inspectRationale?: InspectRationale,
425
461
  canon?: Canon | null,
462
+ conv?: ConversationData,
426
463
  ): void {
427
464
  this.trace = inspectRationale ? new Rationale(inspectRationale) : null;
428
- this.climbMemo = new Map();
429
- this.recogniseMemo = new Map();
430
- this.perceiveMemo = new Map();
465
+ this.climbMemo = conv ? conv.climbMemo : new Map();
466
+ this.recogniseMemo = conv ? conv.recogniseMemo : new Map();
467
+ this.perceiveMemo = conv ? conv.perceiveMemo : new Map();
468
+ this._resolvedSubtrees = conv ? conv.resolvedSubtrees : null;
431
469
  this.canon = canon ?? null;
432
470
  this.canonMemo = canon ? new Map() : null;
471
+ this._beginMeter();
472
+ }
473
+
474
+ /** Open (or leave closed) the response's work accumulator. Separate from
475
+ * {@link beginResponse} because {@link respondTurn} keeps its own
476
+ * conversation-scoped lifecycle and must not create fresh per-response
477
+ * memos — but it DOES meter, through this same pair. */
478
+ private _beginMeter(): void {
479
+ if (!this._profile) return;
480
+ this.meter = new Meter();
481
+ this.store.meter = this.meter;
482
+ }
483
+
484
+ /** Close the accumulator and publish its report. Detaching from the store
485
+ * matters: a Mind that shares a store with another Mind must not keep
486
+ * charging that store's reads to a finished response. */
487
+ private _endMeter(queryBytes: number): void {
488
+ if (this.meter === null) return;
489
+ this.lastCost = this.meter.report(queryBytes);
490
+ this.store.meter = null;
491
+ this.meter = null;
433
492
  }
434
493
 
435
494
  /** The canonicalizer a response should carry: the Mind-level option when
@@ -441,12 +500,20 @@ export class Mind implements MindContext {
441
500
  }
442
501
 
443
502
  /** Close one response's transient state — every per-response field, incl.
444
- * the edge guide/choices `think` sets mid-flight. */
445
- private endResponse(): void {
503
+ * the edge guide/choices `think` sets mid-flight, and the meter's report.
504
+ *
505
+ * A conversation's memo MAPS were mutated in place, so `data.*` still
506
+ * points at them and there is nothing to save back. Clearing the Mind's
507
+ * references is what matters: a concurrently-started `respond()` swaps its
508
+ * own fresh maps into these pointers, and copying back from them here
509
+ * would inject a foreign response's memos into the conversation. */
510
+ private endResponse(queryBytes: number): void {
511
+ this._endMeter(queryBytes);
446
512
  this.trace = null;
447
513
  this.climbMemo = null;
448
514
  this.recogniseMemo = null;
449
515
  this.perceiveMemo = null;
516
+ this._resolvedSubtrees = null;
450
517
  this.canon = null;
451
518
  this.canonMemo = null;
452
519
  this._edgeGuide = null;
@@ -464,30 +531,51 @@ export class Mind implements MindContext {
464
531
  ): Promise<Response> {
465
532
  this.beginResponse(inspectRationale, canon);
466
533
  try {
467
- const top = this.trace?.enter(traceLabel, [
468
- rItem(queryBytes, "query"),
469
- ]);
470
- const thought = await think(this, queryBytes, this.mechanisms);
471
- if (thought === null) {
472
- top?.done([], "nothing to perceive or an empty store — no answer");
473
- return { v: null, bytes: new Uint8Array(0) };
474
- }
475
-
476
- const voiced = await articulate(this, thought.bytes, queryBytes);
477
- top?.done(
478
- [rItem(voiced, "answer", resolveImpl(this, voiced) ?? undefined)],
479
- "the answer, re-voiced in the asker's words",
480
- );
481
- return {
482
- v: gistOf(this, voiced),
483
- bytes: voiced,
484
- provenance: thought.provenance,
485
- };
534
+ return await this._groundAndVoice(queryBytes, traceLabel);
486
535
  } finally {
487
- this.endResponse();
536
+ this.endResponse(queryBytes.length);
488
537
  }
489
538
  }
490
539
 
540
+ /** The ONE path from query bytes to a voiced answer: ground (think), then
541
+ * re-voice in the asker's words (articulate). Both entry points run
542
+ * exactly this — they differ only in the LIFECYCLE around it (fresh
543
+ * per-response memos vs. a conversation's persistent ones) and in what
544
+ * they do with the answer afterwards. It must be called between
545
+ * {@link beginResponse} and {@link endResponse}. */
546
+ private async _groundAndVoice(
547
+ queryBytes: Uint8Array,
548
+ traceLabel: string,
549
+ ): Promise<Response> {
550
+ const top = this.trace?.enter(traceLabel, [rItem(queryBytes, "query")]);
551
+ const meter = this.meter;
552
+ const thought = meter
553
+ ? await meter.time(
554
+ "think",
555
+ () => think(this, queryBytes, this.mechanisms),
556
+ )
557
+ : await think(this, queryBytes, this.mechanisms);
558
+ if (thought === null) {
559
+ top?.done([], "nothing to perceive or an empty store — no answer");
560
+ return { v: null, bytes: new Uint8Array(0) };
561
+ }
562
+ const voiced = meter
563
+ ? await meter.time(
564
+ "articulate",
565
+ () => articulate(this, thought.bytes, queryBytes),
566
+ )
567
+ : await articulate(this, thought.bytes, queryBytes);
568
+ top?.done(
569
+ [rItem(voiced, "answer", resolveImpl(this, voiced) ?? undefined)],
570
+ "the answer, re-voiced in the asker's words",
571
+ );
572
+ return {
573
+ v: gistOf(this, voiced),
574
+ bytes: voiced,
575
+ provenance: thought.provenance,
576
+ };
577
+ }
578
+
491
579
  async respond(
492
580
  input: Input,
493
581
  inspectRationale?: InspectRationale,
@@ -642,55 +730,30 @@ export class Mind implements MindContext {
642
730
 
643
731
  const turnBytes = inputBytes(this, turn);
644
732
  // Incremental perception — O(turn) instead of O(context).
645
- const tree = this._growContext(data, turnBytes);
733
+ this._growContext(data, turnBytes);
646
734
  const newContext = data.bytes;
647
735
 
648
- // Swap in the conversation's persistent state so the inference
649
- // pipeline does not re-process the prefix from scratch.
650
- // perceiveMemo / recogniseMemo / climbMemo content-keyed;
651
- // the prefix's results from the previous turn are found by
652
- // the current turn's sub-span calls.
653
- // _resolvedSubtrees — Sema-node-keyed; when the pyramid reuses
654
- // prefix subtrees (identical objects), foldTree returns their
655
- // ids immediately — O(suffix) instead of O(context).
656
- this.perceiveMemo = data.perceiveMemo;
657
- this.recogniseMemo = data.recogniseMemo;
658
- this.climbMemo = data.climbMemo;
659
- this._resolvedSubtrees = data.resolvedSubtrees;
660
- this.trace = inspectRationale ? new Rationale(inspectRationale) : null;
661
- // A string turn is text by nature — carry the text equivalence, same as
736
+ // The conversation's persistent memos and subtree cache are swapped in
737
+ // by beginResponse (see there) the SAME lifecycle respond() uses, so a
738
+ // memo added in one place can never be missing from the other. A string
739
+ // turn is text by nature and carries the text equivalence, same as
662
740
  // respond() (see _canonFor).
663
- this.canon = this._canonFor(typeof turn === "string" ? textCanon : null);
664
- this.canonMemo = this.canon ? new Map() : null;
741
+ //
742
+ // No recognise-memo pre-seeding here: that used to be necessary because
743
+ // the flat/positional fold lost visibility into an earlier turn's own
744
+ // structure once later bytes shifted its position (foldTree no longer
745
+ // visited the turn's root node). The STABLE-PREFIX fold (see {@link
746
+ // ConversationData}) makes every turn's subtree independent of what
747
+ // follows it by construction, so recognise() finds it correctly on its
748
+ // own, first-touch, exactly once per turn.
749
+ this.beginResponse(
750
+ inspectRationale,
751
+ this._canonFor(typeof turn === "string" ? textCanon : null),
752
+ data,
753
+ );
665
754
 
666
755
  try {
667
- // No recognise-memo pre-seeding here: that used to be necessary
668
- // because the flat/positional fold lost visibility into an earlier
669
- // turn's own structure once later bytes shifted its position
670
- // (foldTree no longer visited the turn's root node). The
671
- // STABLE-PREFIX fold (see {@link ConversationData}) makes every
672
- // turn's subtree independent of what follows it by construction, so
673
- // recognise() finds it correctly on its own, first-touch, exactly
674
- // once per turn — no workaround needed, and none pre-empting
675
- // recognise()'s own memo with a partial result.
676
- const top = this.trace?.enter("respondTurn", [
677
- rItem(newContext, "query"),
678
- ]);
679
-
680
- const thought = await think(this, newContext, this.mechanisms);
681
- if (thought === null) {
682
- top?.done([], "nothing to perceive or an empty store — no answer");
683
- return {
684
- response: { v: null, bytes: new Uint8Array(0) },
685
- state: this.conversationState(conv)!,
686
- };
687
- }
688
-
689
- const voiced = await articulate(this, thought.bytes, newContext);
690
- top?.done(
691
- [rItem(voiced, "answer", resolveImpl(this, voiced) ?? undefined)],
692
- "the answer, re-voiced in the asker's words",
693
- );
756
+ const response = await this._groundAndVoice(newContext, "respondTurn");
694
757
 
695
758
  // The REPLY joins the accumulated context the same way a turn does
696
759
  // ({@link addTurn}): raw byte append plus a boundary offset — never a
@@ -698,33 +761,11 @@ export class Mind implements MindContext {
698
761
  // the cumulative continuous shape multi-turn training deposits, so a
699
762
  // later turn can refer to what was ANSWERED ("which of those two…"),
700
763
  // not only to what was asked.
701
- if (voiced.length > 0) this.addTurn(conv, voiced);
702
-
703
- return {
704
- response: {
705
- v: gistOf(this, voiced),
706
- bytes: voiced,
707
- provenance: thought.provenance,
708
- },
709
- state: this.conversationState(conv)!,
710
- };
764
+ if (response.bytes.length > 0) this.addTurn(conv, response.bytes);
765
+
766
+ return { response, state: this.conversationState(conv)! };
711
767
  } finally {
712
- // No save-back: the conversation's memo MAPS were mutated in place —
713
- // `data.*` still points at them. Re-assigning from the Mind-level
714
- // pointers here was a no-op in the single-flight case and, under a
715
- // concurrently-started respond() (which swaps its own fresh maps into
716
- // those pointers), would inject a FOREIGN response's memos into this
717
- // conversation. Clear Mind references — non-conversation respond()
718
- // calls get fresh per-response memos.
719
- this.trace = null;
720
- this.perceiveMemo = null;
721
- this.recogniseMemo = null;
722
- this.climbMemo = null;
723
- this.canon = null;
724
- this.canonMemo = null;
725
- this._resolvedSubtrees = null;
726
- this._edgeGuide = null;
727
- this._edgeChoice.clear();
768
+ this.endResponse(newContext.length);
728
769
  }
729
770
  }
730
771
 
@@ -17,11 +17,13 @@ import type { AncestorReach, MindContext, Recognition } from "./types.js";
17
17
  import type { AttentionRead } from "./types.js";
18
18
  import type { ComputedSpan } from "../extension.js";
19
19
  import type { Vec } from "../vec.js";
20
+ import { indexOf } from "../bytes.js";
21
+ import { dominates } from "../geometry.js";
20
22
  import { windowIds } from "./canonical.js";
21
23
  import { read, resolve } from "./primitives.js";
22
- import { alignGraded, type GradedRun } from "./match.js";
24
+ import { alignGraded, type GradedRun, skillExemplar } from "./match.js";
23
25
  import { climbAttentionAll } from "./attention.js";
24
- import { skillExemplar } from "./mechanisms/extraction.js";
26
+ import { sharedReachMemo } from "./traverse.js";
25
27
 
26
28
  // ── Precomputed ──────────────────────────────────────────────────────────────
27
29
  //
@@ -94,9 +96,16 @@ export class Precomputed {
94
96
  }
95
97
 
96
98
  /** Shared memo for {@link reachOf} (structural-IDF reads): a window's
97
- * ancestor reach is a pure function of the read-only store, so one
98
- * response-scoped memo serves every mechanism that prices commonality. */
99
- readonly reachMemo = new Map<number, AncestorReach>();
99
+ * ancestor reach is a pure function of the read-only store, so one memo
100
+ * serves every mechanism that prices commonality — AND the consensus
101
+ * climb, which is the largest consumer and used to build its own. The
102
+ * ONE definition of its lifetime lives in traverse.ts
103
+ * ({@link sharedReachMemo}): response-scoped for respond(),
104
+ * conversation-scoped across turns, always cold under a trace. */
105
+ private _reach?: Map<number, AncestorReach>;
106
+ get reachMemo(): Map<number, AncestorReach> {
107
+ return this._reach ??= sharedReachMemo(this.ctx);
108
+ }
100
109
 
101
110
  // ── Expensive lazy analyses ───────────────────────────────────────────
102
111
  //
@@ -105,16 +114,26 @@ export class Precomputed {
105
114
  // mechanism MUST check its cheap floor gates and the pipeline's
106
115
  // `worthRunning` predicate before first-touching one of these.
107
116
 
117
+ /** Charge a lazily-shared analysis to its OWN phase rather than to the
118
+ * mechanism that happened to first-touch it. Without this the profile
119
+ * reads as "cast.floor costs 2 s" when what actually cost 2 s is the
120
+ * consensus climb — which cast merely paid for on everyone's behalf, and
121
+ * which every later consumer then got free. Attribution must follow the
122
+ * work, not the caller. */
123
+ private shared<T>(phase: string, fn: () => Promise<T>): Promise<T> {
124
+ const meter = this.ctx.meter;
125
+ return meter ? meter.time(phase, fn) : fn();
126
+ }
127
+
108
128
  private _attention?: Promise<AttentionRead>;
109
129
  /** The full consensus climb (roots + ranked anchors) — the query-level
110
130
  * evidence CAST, confluence, extraction, recall's scaffolding tier, and
111
131
  * fusion all share. Computed on first access; a query no mechanism
112
132
  * climbs for (e.g. one an extension decided outright) never pays for it. */
113
133
  attention(): Promise<AttentionRead> {
114
- return this._attention ??= climbAttentionAll(
115
- this.ctx,
116
- this.query,
117
- this.k,
134
+ return this._attention ??= this.shared(
135
+ "attention",
136
+ () => climbAttentionAll(this.ctx, this.query, this.k),
118
137
  );
119
138
  }
120
139
 
@@ -124,7 +143,10 @@ export class Precomputed {
124
143
  * mechanism doing analogical transfer. */
125
144
  weave(): Promise<WeaveInfo> {
126
145
  return this._weave ??= this.attention().then((climb) =>
127
- computeWeave(this.ctx, this.query, this, climb)
146
+ this.shared(
147
+ "weave",
148
+ async () => computeWeave(this.ctx, this.query, this, climb),
149
+ )
128
150
  );
129
151
  }
130
152
 
@@ -140,7 +162,10 @@ export class Precomputed {
140
162
  spanShapedOf(anchor: number): Promise<SkillInfo | null> {
141
163
  let p = this._spanShaped.get(anchor);
142
164
  if (p === undefined) {
143
- p = skillExemplar(this.ctx, anchor, this.guide);
165
+ p = this.shared(
166
+ "spanShaped",
167
+ () => skillExemplar(this.ctx, anchor, this.guide),
168
+ );
144
169
  this._spanShaped.set(anchor, p);
145
170
  }
146
171
  return p;
@@ -192,6 +217,7 @@ function computeWeave(
192
217
  const rankedCapped = ranked.length > pre.k ? ranked.slice(0, pre.k) : ranked;
193
218
  const depth = new Float64Array(query.length);
194
219
  const points: WeaveInfo["points"] = [];
220
+ const byAnchor = new Map<number, WeaveInfo["points"][number]>();
195
221
 
196
222
  // WEAVE-SCALE anchors only: CAST transfers structure between things the
197
223
  // QUERY weaves together — query-scale structures. A context an order of
@@ -207,6 +233,38 @@ function computeWeave(
207
233
  // uncapped weaves spent 5–8s per query recognising conversation-length
208
234
  // anchors that could never form a weave point.
209
235
  const capBytes = query.length * quantum;
236
+ // EXCLUSIVITY IS ARBITRATED BY THE CLIMB'S VOTE ORDER, DELIBERATELY. A query
237
+ // byte can only be independent evidence for ONE point, so points are built in
238
+ // ranked order and each new point's runs are trimmed against every point
239
+ // already accepted; a point left with no run of a full quantum drops out of
240
+ // the weave.
241
+ //
242
+ // That reads like first-come-wins — a point that merely ranked higher taking
243
+ // a span from the point that actually explains it — and arbitrating by LOCAL
244
+ // evidence instead (ownership of each byte to the longest covering run, then
245
+ // the heavier weight, then rank) was implemented and MEASURED: test/29 went
246
+ // 9/2 to 7/4, and the new failures name the reason. CAST requires the weave
247
+ // to touch a COMMITTED point of attention ("2 aligned structure(s), but none
248
+ // is one of the climb's 1 committed root(s)"), and it was precisely the vote
249
+ // order that kept the committed root's own point alive in the weave. Local
250
+ // run length knows nothing about what the climb settled on, so it evicted the
251
+ // root's evidence and left CAST refusing on its own consistency check.
252
+ //
253
+ // So the vote order here is not an accident of construction — it is what
254
+ // holds the weave and the climb to the same conclusion. Weave-local
255
+ // measures decide what is FRAME inside the weave (see the frame gates in
256
+ // cast.ts); which structures are in the weave at all stays the climb's call.
257
+ //
258
+ // TWO PASSES. `depth` — how much of the weave agrees on each query byte, and
259
+ // therefore what counts as FRAME — must be the whole weave's, not "whatever
260
+ // has been processed so far": read in one pass it made a candidate's own
261
+ // frame reading depend on its rank, and the proposed-run gate below needs the
262
+ // real thing.
263
+ const cands: Array<{
264
+ cand: (typeof rankedCapped)[number];
265
+ ctxBytes: Uint8Array;
266
+ raw: GradedRun[];
267
+ }> = [];
210
268
  for (const cand of rankedCapped) {
211
269
  const ctxBytes = read(ctx, cand.anchor, capBytes + 1);
212
270
  if (ctxBytes.length === 0 || ctxBytes.length > capBytes) continue;
@@ -215,6 +273,9 @@ function computeWeave(
215
273
  for (const r of raw) {
216
274
  for (let i = r.qs; i < r.qe; i++) depth[i] += r.weight;
217
275
  }
276
+ cands.push({ cand, ctxBytes, raw });
277
+ }
278
+ for (const { cand, ctxBytes, raw } of cands) {
218
279
  const free: GradedRun[] = [];
219
280
  for (const r of raw) {
220
281
  let { qs, qe, cs, weight } = r;
@@ -236,12 +297,122 @@ function computeWeave(
236
297
  }
237
298
  }
238
299
  if (free.length > 0) {
239
- points.push({
300
+ const pt = {
240
301
  anchor: cand.anchor,
241
302
  vote: cand.vote,
242
303
  ctx: ctxBytes,
243
304
  runs: free,
244
- });
305
+ };
306
+ byAnchor.set(cand.anchor, pt);
307
+ points.push(pt);
308
+ }
309
+ }
310
+
311
+ // A byte is FRAME when more than half the weave shares it, and a SPAN is
312
+ // frame when more than half its bytes are — the same two-level
313
+ // half-dominance reading cast.ts's own frame gate uses, over the same
314
+ // `depth`. Read against the accepted POINTS (as cast.ts does), so it is
315
+ // only meaningful once phase 1 has run.
316
+ const framed = (from: number, to: number): boolean => {
317
+ let n = 0;
318
+ for (let i = from; i < to; i++) if (dominates(depth[i], points.length)) n++;
319
+ return dominates(n, to - from);
320
+ };
321
+ // PHASE 2 — THE CLIMB'S OWN CONCLUSION IS AN ALIGNMENT THE LITERAL MATCHER
322
+ // CANNOT SEE. `alignRuns` seeds on W-grams, so two forms differing by a
323
+ // single byte share no run at all: on `How is ice like steel?` against a
324
+ // store holding `Ice is cold`, the query's `ice` and the stored `Ice` agree
325
+ // on only `ce ` — three bytes, never seeded — so that structure entered the
326
+ // weave carrying nothing but the ` is ` scaffolding every exemplar shares,
327
+ // lost it to the first point that claimed it, and vanished. The climb had
328
+ // ALREADY identified it: its resonance elected `Ice is cold` from the query
329
+ // span `ce l` and `Steel is hard` from `stee`, two disjoint spans each naming
330
+ // its own structure, weighed through the region's contrastive margin and its
331
+ // IDF — gates the aligner has no equivalent of.
332
+ //
333
+ // So the climb PROPOSES the pairing (which structure, which query span) and
334
+ // bytes DECIDE its terms (§2.3). Three gates, each one measured:
335
+ //
336
+ // • it may only take query bytes NO literal run claimed. Run inline with
337
+ // phase 1 this did the opposite of "exact decides" — a higher-ranked
338
+ // candidate's proposal trimmed a lower-ranked candidate's byte-for-byte
339
+ // match out of existence (`he W`, proposed for `a nickname meaning the
340
+ // divine one`, cut the literal `The ` out of `The Starry Night was
341
+ // painted by Vincent van Gogh.` and CAST's redirection lost its
342
+ // dominant — test/29 C4). Hence a second pass, after every literal run
343
+ // is placed.
344
+ // • the literal agreement must DOMINATE the span. A climb vote is not by
345
+ // itself an alignment: on `The Persistence of Memory was painted by
346
+ // Salvador Dali.` the climb elects `The Starry Night…` from the span
347
+ // ` Dali.`, which shares barely a byte with it — the resonance was
348
+ // carried by the frame those exemplars share. Admitting it let CAST
349
+ // weave points out of pure scaffolding and out-account the correct
350
+ // extraction (test/00, test/24). Where the proposal is real the
351
+ // agreement is overwhelming: both C1 spans agree on three of four bytes.
352
+ // • and the span must not be FRAME. Literal dominance alone is too weak
353
+ // at this scale — a 4-byte span agrees three-of-four with half the
354
+ // corpus by accident (`he W` against `a nickname meaning the divine
355
+ // one`). Frame is the weave-local measure of exactly that.
356
+ const claimed = new Uint8Array(query.length);
357
+ for (const p of points) {
358
+ for (const r of p.runs) claimed.fill(1, r.qs, r.qe);
359
+ }
360
+ for (const { cand, ctxBytes } of cands) {
361
+ if (cand.end > cand.start) {
362
+ let qs = cand.start;
363
+ let qe = cand.end;
364
+ while (qs < qe && claimed[qs]) qs++;
365
+ while (qe > qs && claimed[qe - 1]) qe--;
366
+ let clear = true;
367
+ for (let i = qs; i < qe; i++) if (claimed[i]) clear = false;
368
+ if (clear && qe - qs >= Math.min(quantum, ctxBytes.length)) {
369
+ // The gate only asks whether the agreement DOMINATES the span, so
370
+ // search DOWNWARD from the whole span and stop at the first hit: the
371
+ // first length found is both the longest agreement and, by
372
+ // construction, already past the dominance bar. At most O(W²) bounded
373
+ // substring probes — a span is one segment (≤ 2W) — where a full
374
+ // longest-common-substring scan would be O(|span|² · |ctx|) against a
375
+ // context that may be W× the query.
376
+ const span = query.subarray(qs, qe);
377
+ const bar = Math.floor(span.length / 2) + 1; // dominates(bar, length)
378
+ let bestLen = 0;
379
+ let bestCs = 0;
380
+ for (let len = span.length; len >= bar && bestLen === 0; len--) {
381
+ for (let off = 0; off + len <= span.length; off++) {
382
+ const at = indexOf(ctxBytes, span.subarray(off, off + len), 0);
383
+ if (at < 0) continue;
384
+ bestLen = len;
385
+ // Where the span's FIRST byte lands, so `cs` means the same thing
386
+ // it does for a literal run: the context offset the run starts at.
387
+ bestCs = Math.max(0, at - off);
388
+ break;
389
+ }
390
+ }
391
+ if (bestLen > 0 && !framed(qs, qe)) {
392
+ const run = {
393
+ qs,
394
+ qe,
395
+ cs: bestCs,
396
+ weight: bestLen / (qe - qs),
397
+ proposed: true,
398
+ };
399
+ claimed.fill(1, qs, qe);
400
+ const pt = byAnchor.get(cand.anchor);
401
+ if (!pt) {
402
+ const made = {
403
+ anchor: cand.anchor,
404
+ vote: cand.vote,
405
+ ctx: ctxBytes,
406
+ runs: [run],
407
+ };
408
+ byAnchor.set(cand.anchor, made);
409
+ points.push(made);
410
+ } else {
411
+ pt.runs.push(run);
412
+ pt.runs.sort((x, y) => x.qs - y.qs);
413
+ }
414
+ }
415
+ }
245
416
  }
246
417
  }
247
418
  return { points, depth };
@@ -277,6 +448,25 @@ export interface MechanismResult {
277
448
  /** Override the mechanism's default provenance for this result.
278
449
  * When absent, the pipeline uses `mech.provenance`. */
279
450
  provenance?: string;
451
+
452
+ /** This grounding is a COMPLETE trained answer — post-grounding must not
453
+ * extend it. Declared by the mechanism about its own result, exactly like
454
+ * `accounted`/`used`/`unexplained`; the decider honours the property and
455
+ * never asks which mechanism set it, so the market stays uniform.
456
+ *
457
+ * Set it only when the answer is a stored form's OWN continuation reached
458
+ * through an identity claim about the query — i.e. the query IS some
459
+ * trained context, so its continuation is the whole read-out and a further
460
+ * multi-hop pivot would chain PAST the fact that produced the answer.
461
+ * That is the same reasoning `reason`'s echo guard already applies to a
462
+ * query that resolves exactly (see reasoning.ts); this carries the claim
463
+ * for the mechanisms that establish the identity by another route.
464
+ *
465
+ * Observed without it: the correct "What is the process of
466
+ * photosynthesis?" grounding was pivoted forward four times, out of the
467
+ * fact that answered it and into an unrelated "Hello! How can I assist you
468
+ * today?" conversational turn. */
469
+ complete?: boolean;
280
470
  }
281
471
 
282
472
  // ── PipelineMechanism ────────────────────────────────────────────────────────