@hviana/sema 0.6.0 → 0.7.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 (40) hide show
  1. package/.github/workflows/release.yml +80 -0
  2. package/AGENTS.md +53 -9
  3. package/dist/src/meter.d.ts +1 -4
  4. package/dist/src/meter.js +0 -3
  5. package/dist/src/mind/attention.js +22 -20
  6. package/dist/src/mind/graph-search.d.ts +43 -9
  7. package/dist/src/mind/graph-search.js +82 -15
  8. package/dist/src/mind/junction.d.ts +13 -0
  9. package/dist/src/mind/junction.js +13 -0
  10. package/dist/src/mind/mechanisms/cover.js +23 -2
  11. package/dist/src/mind/mechanisms/prefix-completion.js +13 -11
  12. package/dist/src/mind/mechanisms/recall.js +8 -4
  13. package/dist/src/mind/pipeline-mechanism.d.ts +0 -24
  14. package/dist/src/mind/pipeline-mechanism.js +13 -36
  15. package/dist/src/mind/pipeline.d.ts +23 -0
  16. package/dist/src/mind/pipeline.js +51 -3
  17. package/dist/src/mind/recognition.d.ts +6 -1
  18. package/dist/src/mind/recognition.js +11 -6
  19. package/dist/src/mind/resonance.js +48 -13
  20. package/dist/src/store.js +22 -1
  21. package/jsr.json +1 -1
  22. package/package.json +7 -2
  23. package/src/meter.ts +1 -4
  24. package/src/mind/attention.ts +22 -19
  25. package/src/mind/graph-search.ts +93 -16
  26. package/src/mind/junction.ts +13 -0
  27. package/src/mind/mechanisms/cover.ts +23 -4
  28. package/src/mind/mechanisms/prefix-completion.ts +13 -11
  29. package/src/mind/mechanisms/recall.ts +8 -4
  30. package/src/mind/pipeline-mechanism.ts +13 -42
  31. package/src/mind/pipeline.ts +87 -3
  32. package/src/mind/recognition.ts +19 -6
  33. package/src/mind/resonance.ts +79 -50
  34. package/src/store.ts +21 -1
  35. package/test/89-completion-recursion.test.mjs +230 -0
  36. package/test/90-connector-read-cap.test.mjs +130 -0
  37. package/test/91-branch-bytes-cache.test.mjs +152 -0
  38. package/test/93-regime-prediction.test.mjs +148 -0
  39. package/test/94-cross-region-budget.test.mjs +67 -0
  40. package/test/95-wide-resonance-removed.test.mjs +109 -0
@@ -108,6 +108,13 @@ export type GItem =
108
108
  * because subtraction is more "the point" of its query). See
109
109
  * {@link liftAnswer}. */
110
110
  computed?: boolean;
111
+ /** Set on the out emitted at a chain's GENUINE FIXPOINT — the one span kind a
112
+ * recursive re-cover ({@link GraphSearch.recompleteNode}) may deepen. The
113
+ * re-cover does NOT run here: this marks the span as eligible, and
114
+ * {@link GraphSearch.deepen} runs it afterwards on the spans the lightest
115
+ * derivation actually CHOSE. Part of {@link key}, because it decides
116
+ * whether the span's final bytes may still change. */
117
+ fix?: boolean;
111
118
  };
112
119
  type OutItem = Extract<GItem, { kind: "out" }>;
113
120
 
@@ -153,6 +160,10 @@ export interface Seg {
153
160
  /** See the `computed` field of the "out" {@link GItem} — set only for an
154
161
  * extension's derived value, never a genuinely recognised learned form. */
155
162
  computed?: boolean;
163
+ /** See the `fix` field of the "out" {@link GItem} — this span ended a chain at
164
+ * a genuine fixpoint, so it is the one span kind a recursive re-cover may
165
+ * still deepen. Consumed by {@link GraphSearch.deepen}. */
166
+ fix?: boolean;
156
167
  }
157
168
 
158
169
  /** Read the chosen spans back off a derivation: the goal is a chain of bridge
@@ -171,6 +182,7 @@ function readCover(derivation: Derivation<GItem>): Seg[] {
171
182
  rec: out.rec,
172
183
  node: out.node,
173
184
  computed: out.computed,
185
+ fix: out.fix,
174
186
  });
175
187
  }
176
188
  node = node.premises[0];
@@ -472,10 +484,32 @@ export class GraphSearch {
472
484
  onDerivation(readDerivation(derivation, substitutions !== undefined));
473
485
  }
474
486
  return derivation
475
- ? { segs: readCover(derivation), cost: derivation.cost }
487
+ ? { segs: this.deepen(readCover(derivation)), cost: derivation.cost }
476
488
  : null;
477
489
  }
478
490
 
491
+ /** Re-cover the CHOSEN fixpoint spans, in place.
492
+ *
493
+ * Completion is still "cover, recursively" — it just runs on the answer
494
+ * instead of on the exploration. A cover chooses O(segs) spans, so a level
495
+ * pays O(answer) re-covers however densely the corpus interconnects the forms
496
+ * the search passed through on the way. That is the bound
497
+ * {@link recompleteNode}'s contract always claimed and, running per fixpoint
498
+ * REACHED, never had.
499
+ *
500
+ * Deepening cannot change which cover won: the derivation is already final
501
+ * and every span keeps its i..j and its cost. It only replaces a chosen
502
+ * span's bytes with the deeper learnt form they rewrite to — what the
503
+ * recursion was always for. */
504
+ private deepen(segs: Seg[]): Seg[] {
505
+ for (const s of segs) {
506
+ if (!s.fix || s.node === undefined) continue;
507
+ const deeper = this.recompleteNode(s.node);
508
+ if (deeper !== null) s.bytes = deeper;
509
+ }
510
+ return segs;
511
+ }
512
+
479
513
  /** The weighted deduction system the graph exploration solves (the four
480
514
  * reductions of adapted A*LD live in {@link lightestDerivation}; this only states the
481
515
  * items, axioms, goal, and rules — see {@link GItem} for the item kinds).
@@ -541,8 +575,8 @@ export class GraphSearch {
541
575
  }`;
542
576
  }
543
577
  return `o${it.i}.${it.j}.${it.cover ? 1 : 0}.${it.rec ? 1 : 0}.${
544
- it.node ?? -1
545
- }.${latin1(it.bytes)}`;
578
+ it.fix ? 1 : 0
579
+ }.${it.node ?? -1}.${latin1(it.bytes)}`;
546
580
  },
547
581
  *axioms() {
548
582
  yield { item: { kind: "cover", p: 0 }, cost: 0 };
@@ -855,17 +889,29 @@ export class GraphSearch {
855
889
  // actual end, never per intermediate stop — so its cost tracks the
856
890
  // ANSWER's own structure, not how densely the corpus interconnects
857
891
  // the nodes passed through on the way there.
858
- const deeper = this.recompleteNode(it.node);
892
+ // MARK the fixpoint; do not re-cover it here. Re-covering at this point
893
+ // pays a full {@link recompleteNode} — a recognition of the node's whole
894
+ // bytes — for every fixpoint the exploration REACHES, and how many it
895
+ // reaches is set by how densely the corpus interconnects the forms passed
896
+ // through. Measured on an 18.9M-node store, a 2-byte query: 12,000
897
+ // re-covers inside ONE cover, folding 95,258 distinct spans, ~2.4 GB and
898
+ // climbing to a V8 fatal. The answer needs a handful.
899
+ //
900
+ // {@link deepen} runs it instead on the spans the lightest derivation
901
+ // CHOSE. The ladder is untouched — this out still costs 0, so a genuine
902
+ // fixpoint still beats any premature stop at the same depth, exactly as
903
+ // the ordering above states.
859
904
  yield {
860
905
  premises: [it],
861
906
  conclusion: {
862
907
  kind: "out",
863
908
  i: it.i,
864
909
  j: it.j,
865
- bytes: deeper ?? nodeBytes(it.node),
910
+ bytes: nodeBytes(it.node),
866
911
  cover: true,
867
912
  rec: true,
868
913
  node: it.node,
914
+ fix: true,
869
915
  },
870
916
  cost: 0,
871
917
  };
@@ -936,18 +982,45 @@ export class GraphSearch {
936
982
  * ({@link resolve}) — the graph itself gates against re-expanding a contained
937
983
  * form ("ice is cold" ⊅→ "ice is cold is cold").
938
984
  *
939
- * Termination is INTRINSIC, not a depth limit: a node already on the
940
- * completion stack ({@link recompleteOpen}) is not re-entered a self-
941
- * referential recomposition is a cycle that can yield nothing new, so it
942
- * stops there, exactly as {@link completeForward} stops on a revisited edge.
943
- * Distinct node ids are finite and each finished completion is memoised, so a
944
- * legitimate chain runs as deep as the graph licenses and no further. */
985
+ * Termination is STRUCTURAL: a produced node is re-covered once, never inside
986
+ * another re-cover (see the guard below), so one cover pays at most one
987
+ * nested {@link solve} per distinct produced node it actually reaches, and
988
+ * {@link recompleteMemo} collapses a repeat to nothing.
989
+ *
990
+ * This comment used to argue termination from "distinct node ids are finite
991
+ * and each finished completion is memoised". That is a bound of N — the one
992
+ * AGENTS §2.8 forbids — and it was load-bearing, not pedantic: nested, the
993
+ * recursion reached depth 331 and 9.1 GB on an 18.9M-node store for a 2-byte
994
+ * query and did not terminate, which is what killed a 5 h training run at its
995
+ * checkpoint recall. Guard: test/89-completion-recursion.test.mjs. */
945
996
  private recompleteNode(node: number): Uint8Array | null {
946
997
  if (!this.host.recogniseSpan) return null;
947
998
  const memo = this.recompleteMemo;
948
999
  if (memo.has(node)) return memo.get(node) ?? null;
949
- // Cycle guard: a node being completed must not recurse back into itself.
950
- if (this.recompleteOpen.has(node)) return null;
1000
+ // ONE re-cover per produced node never a re-cover inside a re-cover.
1001
+ //
1002
+ // Re-covering is how a PRODUCED node's bytes enter the search at all: the
1003
+ // cover machinery otherwise only ever sees the QUERY's spans. That is
1004
+ // needed once. The alternation of decomposition and recomposition that
1005
+ // follows — parts rewriting several times, siblings fusing, a recomposition
1006
+ // feeding another — is the main search's own fuse/`rcmp` work, not this
1007
+ // recursion's: 15-decomposition-gap §9–§12 all pass with this method
1008
+ // disabled outright, and only §6 (the produced composite "p1 p2", whose
1009
+ // bytes nothing else brings in) needs it.
1010
+ //
1011
+ // Nesting it was the defect. Each level is a full {@link solve} with its
1012
+ // own agenda and chart, exploring from a node the answer never asked about,
1013
+ // so per-query cost tracked how densely the corpus interconnects the forms
1014
+ // passed through — the growth AGENTS §2.8 forbids. Measured on an
1015
+ // 18.9M-node store: depth 331 and 9.1 GB for a 2-byte query, not
1016
+ // terminating; and on the guard corpus every one of 125 nested re-covers
1017
+ // was REJECTED by the resolve() gate below, expanding a 70-byte node into a
1018
+ // 374-byte concatenation that names nothing. All of it was waste.
1019
+ //
1020
+ // `recompleteOpen` is that stack, so a non-empty stack means we are already
1021
+ // inside one. This subsumes the old cycle guard: a node cannot recurse
1022
+ // back into itself when nothing recurses at all.
1023
+ if (this.recompleteOpen.size > 0) return null;
951
1024
 
952
1025
  // A leaf or single-child node has no parts to recompose; skip before the
953
1026
  // costly recognition so a plain terminal answer pays nothing.
@@ -985,9 +1058,13 @@ export class GraphSearch {
985
1058
  * outs of a long query re-cover each distinct node at most once); reset at the
986
1059
  * top of {@link cover}. */
987
1060
  private recompleteMemo = new Map<number, Uint8Array | null>();
988
- /** The nodes currently being re-completed — the recursion stack. A node in
989
- * this set is not re-entered, so a cyclic recomposition terminates naturally
990
- * (the same cycle guard {@link completeForward} uses), with no depth cap. */
1061
+ /** The node currently being re-completed — the recursion stack, and so also
1062
+ * the nesting depth. {@link recompleteNode} refuses to start while it is
1063
+ * non-empty (one re-cover per produced node, never one inside another), which
1064
+ * is what keeps a query's cost proportional to its answer rather than to the
1065
+ * corpus; it therefore holds at most one id. A Set, not a flag, because it
1066
+ * states WHICH node is open — the invariant a reader needs to check the
1067
+ * guard, and what makes the old cycle-guard reading still hold. */
991
1068
  private recompleteOpen = new Set<number>();
992
1069
 
993
1070
  /** out(i,j,bytes,…): index it for the binary rules, then offer splicing a
@@ -207,6 +207,19 @@ function cachedContainers(
207
207
  * edgeAncestors' question and wrong for this one: a junction container
208
208
  * is legitimately reached across many containing structures. Half the
209
209
  * successful junctions would be lost.
210
+ *
211
+ * REFUTED EARLY-STOP (side-cone exhaustion, §2.17's "real saturation"):
212
+ * stopping the walk the moment ONE side's upward cone is emptied is wrong,
213
+ * in both a hub-guarded form and a hub-flagged form. The junction test is
214
+ * a BYTE containment over the UNION of the two cones, and a junction can be
215
+ * structurally reachable from only ONE side — the side whose seed is a
216
+ * FOLD sub-node of the container (test/16: "cold or hot" is reached from
217
+ * the window "cold", but the 3-byte answer "hot" is not a 4-byte window of
218
+ * it, so "hot"'s cone is empty while the junction still lies ahead in
219
+ * "cold"'s cone; test/34's n-ary binding fails the hub-guarded form the
220
+ * same way). "One cone exhausted" therefore never proves "no junction
221
+ * left", and the walk must keep the √N·W budget as its NET after the
222
+ * per-node saturations below.
210
223
  * • per-node hub guards — parent fan-outs beyond √N are hubs (not
211
224
  * expanded); each node contributes at most one √N page of containers;
212
225
  * √N collected candidates decide. */
@@ -59,7 +59,9 @@ export async function resolveConnectors(
59
59
  // transcript evidence: cover still needs the site for structural context,
60
60
  // but liftAnswer will trim that continuation as already answered. Building
61
61
  // pairwise/n-ary bridges for it can only create connectors that are later
62
- // discarded, and on cumulative dialogue that dominated the whole search.
62
+ // discarded a semantically neutral gate (it removes work whose product
63
+ // liftAnswer throws away), and a cumulative (multi-turn) query is exactly
64
+ // where such already-answered continuations recur.
63
65
  let answered = 0;
64
66
  const ordered = [...sites]
65
67
  .sort((a, b) => a.start - b.start)
@@ -72,9 +74,26 @@ export async function resolveConnectors(
72
74
  if (span && span[0] <= s.start && s.end <= span[1]) return false;
73
75
  if (query === undefined || ctx.answeredSpans.length === 0) return true;
74
76
  const continuations = ctx.store.nextFirst(s.payload, hubBound(ctx));
75
- return !continuations.some((answer) =>
76
- indexOf(query, read(ctx, answer), 0) >= 0
77
- );
77
+ return !continuations.some((answer) => {
78
+ // PREFIX-CAPPED (AGENTS §2.8): a candidate longer than the query cannot
79
+ // occur INSIDE it, so read one byte past the query's length — enough to
80
+ // detect the overflow — and reject without reconstructing the rest.
81
+ // The `+ 1` is what makes the test exact rather than a truncation: a
82
+ // result of exactly `query.length + 1` bytes is known to be too long,
83
+ // and anything shorter is the candidate's COMPLETE content, so the
84
+ // substring test below is the same test as before. (The same overflow
85
+ // probe bridge.ts:256 already uses.)
86
+ //
87
+ // This loop runs up to hubBound(ctx) = √N reads PER SITE, and only on a
88
+ // multi-turn response — `answeredSpans` is empty for a plain respond(),
89
+ // so the probe does not execute there. The cap cannot reduce the read
90
+ // COUNT — only a semantic change to the "already answered" test could —
91
+ // but it bounds each read by the query instead of by the corpus, which
92
+ // is what §2.8 asks for and what rescues a SHORT query: at 3 bytes this
93
+ // reads 4 bytes per candidate instead of the ~231 it averaged before.
94
+ const bytes = read(ctx, answer, query.length + 1);
95
+ return bytes.length <= query.length && indexOf(query, bytes, 0) >= 0;
96
+ });
78
97
  });
79
98
  const bridgePair = async (l: number, r: number) => {
80
99
  if (l === r || links.has(l + "," + r)) return;
@@ -251,9 +251,9 @@ export const prefixMechanism: PipelineMechanism = {
251
251
  provenance: "prefix",
252
252
  async floor(ctx, query, _pre, worthRunning) {
253
253
  // One projection: the form is voiced whole, nothing is substituted.
254
- // INVESTMENT DISCIPLINE — the supplies below are the response's wide
255
- // candidate list and a bounded √N walk, so neither is touched until the
256
- // bound can still beat the incumbent.
254
+ // INVESTMENT DISCIPLINE — the supplies below are a bounded √N window walk
255
+ // and the response's memoised top-k resonance read, so neither is touched
256
+ // until the bound can still beat the incumbent.
257
257
  if (!worthRunning(STEP)) return STEP;
258
258
  // A query with no room for a perceivable continuation inside the phrase
259
259
  // cap cannot clear guard 2, so it is not worth a single read.
@@ -264,14 +264,16 @@ export const prefixMechanism: PipelineMechanism = {
264
264
  return STEP;
265
265
  },
266
266
  async run(ctx, query, pre) {
267
- // The response's shared wide list first; only when it supplies nothing does
268
- // the write side's window index propose. That ordering is the whole cost
269
- // story: a query the ranked list can already explain pays not one extra
270
- // read, and the bounded walk is spent only where the alternative is an
271
- // empty answer. A second SUPPLY, not a second mechanism the same three
272
- // guards decide either way.
273
- const completed = prefixCompletion(ctx, query, await pre.wideResonance()) ??
274
- prefixCompletion(ctx, query, formsOpenedBy(ctx, query));
267
+ // The write side's window index proposes FIRST: a proper prefix's gist
268
+ // cannot rank its own continuation (cos falls below reachThreshold at a
269
+ // few bytes of truncation), so the content-addressed window walk is the
270
+ // correct measure for this question (§2.3), and it is a bounded √N walk —
271
+ // cheaper than an exhaustive ANN. The top-k resonance list is the SECOND
272
+ // supply, for prefixes long enough that the gist still ranks the form. A
273
+ // second SUPPLY, not a second mechanism — the same three guards decide
274
+ // either way.
275
+ const completed = prefixCompletion(ctx, query, formsOpenedBy(ctx, query)) ??
276
+ prefixCompletion(ctx, query, (await pre.resonance()).map((h) => h.id));
275
277
  if (completed === null) return [];
276
278
  return [{
277
279
  bytes: completed.form,
@@ -412,10 +412,14 @@ export async function recallByResonance(
412
412
  }
413
413
  }
414
414
  // 3b. Corroborated-substitution bridge — refusal-path only (bridge.ts).
415
- // The WIDE candidate list every past-the-top-k mechanism reads lives on
416
- // Precomputed (see wideResonance): shared across the whole response, so the
417
- // exhaustive branch runs at most once whoever first-touches it.
418
- const wideIds = () => pre.wideResonance();
415
+ // The bridge's proposal source is the response's ONE top-k read the same
416
+ // list recall already ranked above never an exhaustive √N scan. The
417
+ // bridge's own candidate cap is 2·recallQueryK, so top-k proposals are
418
+ // exactly the budget it can consume, and every proposal is byte-verified
419
+ // downstream (§2.3). Reuse the memoised `resonance()`; scanning every IVF
420
+ // cluster here once made every honest refusal cost hundreds of ms regardless
421
+ // of k.
422
+ const wideIds = async () => (await pre.resonance()).map((h) => h.id);
419
423
 
420
424
  // Every gist-based tier has failed; before refusing, align the query
421
425
  // byte-for-byte against the trained contexts its own stored windows
@@ -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,30 @@ 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 right after the first mechanism ran, or null when
148
+ * it grounded nothing (best === null — composition, with no incumbent). */
149
+ incumbentGrade: number | null;
150
+ /** The cheapest composition floor in grade units (`grade(2 * STEP)` = 2,
151
+ * CAST's floor) — the bar the incumbent must sit at or below for the
152
+ * consensus climb to be skipped. */
153
+ climbFloorGrade: number;
154
+ }
155
+
132
156
  /** Think: a single lightest-derivation exploration of the Sema graph.
133
157
  *
134
158
  * Every answer travels the same path:
@@ -161,10 +185,22 @@ export async function think(
161
185
 
162
186
  // ── Pre-computation ──────────────────────────────────────────────────
163
187
  const mechanisms = mechs ?? defaultMechanisms;
164
- const rec = recognise(ctx, query);
188
+ const meter = ctx.meter;
189
+ // recognition is a shared analysis (§2.14 contract 5): it does the query's
190
+ // own store work (perceive → foldTree → resolve), which used to land in
191
+ // `think` and in nothing narrower — the meter's one accounting surface must
192
+ // charge it to itself, exactly as attention/weave/resonance are charged.
193
+ const rec = meter
194
+ ? await meter.time("recognise", async () => recognise(ctx, query))
195
+ : recognise(ctx, query);
165
196
 
166
197
  // Phase 1: collect computed spans from mechanisms that implement parse()
167
- const computed = await collectComputed(ctx, mechanisms, query);
198
+ const computed = meter
199
+ ? await meter.time(
200
+ "collectComputed",
201
+ () => collectComputed(ctx, mechanisms, query),
202
+ )
203
+ : await collectComputed(ctx, mechanisms, query);
168
204
 
169
205
  if (computed.length > 0) {
170
206
  ctx.trace?.step(
@@ -191,6 +227,9 @@ export async function think(
191
227
  // method on Precomputed, first-touched by whichever mechanism's floor
192
228
  // survives its cheap gates and the worthRunning check. A query no
193
229
  // mechanism climbs for (e.g. one an extension decided) never climbs.
230
+ // NOT phased: the constructor itself is trivial (it only derives `k`), so a
231
+ // phase here would add a zero-work entry to every profiled report — the meter
232
+ // attributes WORK (§2.14); the trace already represents structure.
194
233
  const pre = new Precomputed(ctx, query, rec, computed, ctx._edgeGuide);
195
234
 
196
235
  // ── Grounding: ONE lightest-derivation choice among the mechanisms ────
@@ -259,7 +298,7 @@ export async function think(
259
298
  // Per-mechanism accounting (src/meter.ts). The market's whole premise is
260
299
  // that mechanisms compete on one cost scale — so the profiling read-out is
261
300
  // also per-mechanism, uniformly: the loop never asks which one it holds.
262
- const meter = ctx.meter;
301
+ let regimeReported = false;
263
302
  for (const mech of mechanisms) {
264
303
  const floor = meter
265
304
  ? await meter.time(
@@ -308,6 +347,51 @@ export async function think(
308
347
  scaffolding: r.scaffolding,
309
348
  });
310
349
  }
350
+ // REGIME PREDICTION (R8) — observational only. After the FIRST mechanism
351
+ // runs (cover, which §2.6 places first and floors at 0), the market's
352
+ // outcome is already determined: the consensus climb runs exactly when
353
+ // `worthRunning(2 * STEP)` is true — CAST (floor 2·STEP) is the cheapest
354
+ // mechanism that first-touches it, so an incumbent at or below grade 2
355
+ // prunes CAST and, with it, confluence (3·STEP) and extraction
356
+ // (CONCEPT+STEP) (retrieval); anything above — or no incumbent — runs the
357
+ // full market and the climb (composition). The predicate is
358
+ // `worthRunning`, the same function the loop just used — nothing is
359
+ // computed here that the engine had not already computed, and nothing is
360
+ // read back by inference.
361
+ if (!regimeReported) {
362
+ regimeReported = true;
363
+ const climbFloorGrade = grade(2 * STEP);
364
+ // TS narrows `best` to null in the outer flow (it cannot see the closure
365
+ // assignments in `consider`) — cast back, the same read-back as `decided`
366
+ // below.
367
+ const incumbent = best as Candidate | null;
368
+ const incumbentGrade = incumbent === null
369
+ ? null
370
+ : grade(incumbent.weight);
371
+ const regime: "retrieval" | "composition" = worthRunning(2 * STEP)
372
+ ? "composition"
373
+ : "retrieval";
374
+ ctx.trace?.step(
375
+ "regimePrediction",
376
+ [rItem(query, "query")],
377
+ [],
378
+ regime === "retrieval"
379
+ ? `retrieval regime — incumbent grade ${incumbentGrade} ≤ climb floor ${climbFloorGrade}, so no composition mechanism runs; ` +
380
+ `the consensus climb will not run`
381
+ : `composition regime — ${
382
+ incumbentGrade === null
383
+ ? "no incumbent (nothing grounded)"
384
+ : `incumbent grade ${incumbentGrade}`
385
+ } above climb floor ${climbFloorGrade}, so the full market and climb run`,
386
+ undefined,
387
+ {
388
+ version: 1,
389
+ regime,
390
+ incumbentGrade,
391
+ climbFloorGrade,
392
+ } satisfies RegimePredictionData,
393
+ );
394
+ }
311
395
  }
312
396
 
313
397
  // (TS cannot see the closure assignments into `best` and narrows it to its
@@ -33,7 +33,16 @@ import type { Leaf, Site } from "./graph-search.js";
33
33
  * query's own cut cannot, and records sub-leaf boundaries as `splits`.
34
34
  *
35
35
  * Both O(n · maxGroup) bounded O(1) probes — never a scan of the corpus. */
36
- export function recognise(ctx: MindContext, bytes: Uint8Array): Recognition {
36
+ /** Decompose `bytes` into the learnt forms it contains. `trimmed` skips the
37
+ * edge-trim fallbacks (which recover misaligned FRAGMENTS) — for callers whose
38
+ * own gate rejects fragments anyway (the pivot), so the O(n·W²) trim search is
39
+ * paid only where its output can be used. Byte-identical for every caller
40
+ * that keeps only top-level forms. */
41
+ export function recognise(
42
+ ctx: MindContext,
43
+ bytes: Uint8Array,
44
+ trimmed = false,
45
+ ): Recognition {
37
46
  // Content-keyed memo — works for both single-turn respond() and multi-turn
38
47
  // respondTurn() (where the map persists across calls). ALWAYS consulted,
39
48
  // regardless of tracing — matching perceive()'s own memo, which carries no
@@ -73,7 +82,7 @@ export function recognise(ctx: MindContext, bytes: Uint8Array): Recognition {
73
82
  // not silent), so it is emitted here directly rather than only inside
74
83
  // recogniseImpl.
75
84
  if (ctx.recogniseMemo) {
76
- const key = latin1Key(bytes);
85
+ const key = (trimmed ? "t" : "f") + latin1Key(bytes);
77
86
  const hit = ctx.recogniseMemo.get(key);
78
87
  if (hit !== undefined) {
79
88
  if (ctx.meter) ctx.meter.recogniseHits++;
@@ -91,14 +100,18 @@ export function recognise(ctx: MindContext, bytes: Uint8Array): Recognition {
91
100
  );
92
101
  return hit;
93
102
  }
94
- const fresh = recogniseImpl(ctx, bytes);
103
+ const fresh = recogniseImpl(ctx, bytes, trimmed);
95
104
  ctx.recogniseMemo.set(key, fresh);
96
105
  return fresh;
97
106
  }
98
- return recogniseImpl(ctx, bytes);
107
+ return recogniseImpl(ctx, bytes, trimmed);
99
108
  }
100
109
 
101
- function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
110
+ function recogniseImpl(
111
+ ctx: MindContext,
112
+ bytes: Uint8Array,
113
+ trimmed = false,
114
+ ): Recognition {
102
115
  if (ctx.meter) {
103
116
  ctx.meter.recognitions++;
104
117
  ctx.meter.recognisedBytes += bytes.length;
@@ -211,7 +224,7 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
211
224
  // n.kids !== null enforces above) rather than degenerate into
212
225
  // single-byte-atom territory, which atomIsHub already governs
213
226
  // separately.
214
- else if (end - start - 1 >= 2) {
227
+ else if (!trimmed && end - start - 1 >= 2) {
215
228
  // The chunk's own boundary is drawn by content geometry, not by
216
229
  // any notion of "form" — it can include one edge byte the query's
217
230
  // fold happened to attach here that the trained span never had