@hviana/sema 0.2.9 → 0.4.0

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 (84) hide show
  1. package/AGENTS.md +71 -5
  2. package/dist/src/derive/src/deduction.d.ts +12 -1
  3. package/dist/src/derive/src/deduction.js +5 -1
  4. package/dist/src/derive/src/index.d.ts +1 -0
  5. package/dist/src/geometry.d.ts +3 -30
  6. package/dist/src/geometry.js +330 -82
  7. package/dist/src/index.d.ts +1 -0
  8. package/dist/src/index.js +1 -0
  9. package/dist/src/meter.d.ts +171 -0
  10. package/dist/src/meter.js +269 -0
  11. package/dist/src/mind/attention.d.ts +5 -4
  12. package/dist/src/mind/attention.js +254 -23
  13. package/dist/src/mind/bridge.d.ts +10 -1
  14. package/dist/src/mind/bridge.js +179 -10
  15. package/dist/src/mind/canonical.d.ts +6 -1
  16. package/dist/src/mind/canonical.js +6 -1
  17. package/dist/src/mind/graph-search.d.ts +9 -0
  18. package/dist/src/mind/graph-search.js +59 -19
  19. package/dist/src/mind/index.d.ts +2 -0
  20. package/dist/src/mind/junction.d.ts +10 -0
  21. package/dist/src/mind/junction.js +14 -0
  22. package/dist/src/mind/learning.d.ts +32 -4
  23. package/dist/src/mind/learning.js +26 -4
  24. package/dist/src/mind/match.d.ts +40 -0
  25. package/dist/src/mind/match.js +125 -1
  26. package/dist/src/mind/mechanisms/cast.js +63 -6
  27. package/dist/src/mind/mechanisms/extraction.d.ts +0 -34
  28. package/dist/src/mind/mechanisms/extraction.js +1 -88
  29. package/dist/src/mind/mechanisms/recall.d.ts +3 -0
  30. package/dist/src/mind/mechanisms/recall.js +77 -14
  31. package/dist/src/mind/mind.d.ts +59 -5
  32. package/dist/src/mind/mind.js +115 -93
  33. package/dist/src/mind/pipeline-mechanism.d.ts +33 -3
  34. package/dist/src/mind/pipeline-mechanism.js +179 -10
  35. package/dist/src/mind/pipeline.d.ts +29 -0
  36. package/dist/src/mind/pipeline.js +79 -21
  37. package/dist/src/mind/primitives.d.ts +11 -15
  38. package/dist/src/mind/primitives.js +47 -28
  39. package/dist/src/mind/reasoning.d.ts +7 -1
  40. package/dist/src/mind/reasoning.js +40 -8
  41. package/dist/src/mind/recognition.js +93 -20
  42. package/dist/src/mind/traverse.d.ts +11 -0
  43. package/dist/src/mind/traverse.js +88 -7
  44. package/dist/src/mind/types.d.ts +39 -5
  45. package/dist/src/store.d.ts +15 -0
  46. package/dist/src/store.js +91 -6
  47. package/package.json +1 -1
  48. package/src/derive/src/deduction.ts +15 -0
  49. package/src/derive/src/index.ts +1 -0
  50. package/src/geometry.ts +350 -122
  51. package/src/index.ts +1 -0
  52. package/src/meter.ts +333 -0
  53. package/src/mind/attention.ts +276 -31
  54. package/src/mind/bridge.ts +187 -10
  55. package/src/mind/canonical.ts +6 -1
  56. package/src/mind/graph-search.ts +60 -21
  57. package/src/mind/index.ts +6 -0
  58. package/src/mind/junction.ts +12 -0
  59. package/src/mind/learning.ts +46 -5
  60. package/src/mind/match.ts +146 -1
  61. package/src/mind/mechanisms/cast.ts +62 -6
  62. package/src/mind/mechanisms/extraction.ts +2 -103
  63. package/src/mind/mechanisms/recall.ts +84 -17
  64. package/src/mind/mind.ts +144 -99
  65. package/src/mind/pipeline-mechanism.ts +203 -13
  66. package/src/mind/pipeline.ts +144 -36
  67. package/src/mind/primitives.ts +49 -33
  68. package/src/mind/reasoning.ts +39 -7
  69. package/src/mind/recognition.ts +89 -19
  70. package/src/mind/traverse.ts +89 -8
  71. package/src/mind/types.ts +39 -5
  72. package/src/store.ts +75 -6
  73. package/test/14-scaling.test.mjs +17 -7
  74. package/test/31-audit.test.mjs +4 -1
  75. package/test/33-multi-candidate.test.mjs +13 -1
  76. package/test/36-already-answered-fusion.test.mjs +10 -3
  77. package/test/46-recognise-multibyte-edge.test.mjs +3 -3
  78. package/test/53-cross-region-probe-instrumentation.test.mjs +36 -6
  79. package/test/54-evidence-k-instrumentation.test.mjs +175 -0
  80. package/test/55-cost-meter.test.mjs +284 -0
  81. package/test/56-bridge-identity-admission.test.mjs +209 -0
  82. package/test/57-fusion-order.test.mjs +104 -0
  83. package/test/58-subquantum-sites.test.mjs +112 -0
  84. package/test/59-fold-invariance.test.mjs +226 -0
@@ -44,6 +44,7 @@ export interface Conversation {
44
44
  import type { AttentionRead, MindContext, Recognition } from "./types.js";
45
45
  export type { AnchorRejectionReason, ClimbConsensusData, ConsensusAnchorTrace, ConsensusReachTrace, ConsensusRegionTrace, CrossRegionTier, JunctionVoteTrace, RegionOutcome, } from "./attention.js";
46
46
  export type { AncestorReach, SaturationReason, SaturationStop, } from "./types.js";
47
+ import { type CostReport, Meter } from "../meter.js";
47
48
  export interface MindOptions {
48
49
  seed?: number;
49
50
  recallQueryK?: number;
@@ -58,6 +59,15 @@ export interface MindOptions {
58
59
  mechanisms?: import("./pipeline-mechanism.js").PipelineMechanism[];
59
60
  /** Factories that receive the {@link ExtensionHost} and return mechanisms. */
60
61
  mechanismFactories?: ((host: import("../extension.js").ExtensionHost) => import("./pipeline-mechanism.js").PipelineMechanism)[];
62
+ /** Measure the computational usage of every inference call — see
63
+ * src/meter.ts. Off by default and free when off (one null check per
64
+ * store read); on, each `respond`/`respondTurn` leaves a {@link
65
+ * Mind.lastCost} report behind. Counters are deterministic, so two runs
66
+ * of the same query on the same store are diffable; the millisecond
67
+ * fields are not. Profiling NEVER changes an answer — but note that
68
+ * attaching a RATIONALE does (traced responses bypass the ctx memos,
69
+ * AGENTS §2.11), so profile without a trace. */
70
+ profile?: boolean;
61
71
  /** Content canonicalizer applied to EVERY response (any modality) for
62
72
  * equivalence-class resolution — see src/canon.ts. Text entry points
63
73
  * ({@link Mind.respondText}, {@link Mind.respondTurnText}) inject the
@@ -86,6 +96,19 @@ export declare class Mind implements MindContext {
86
96
  * `false` to disable canonical resolution, or null to let each entry
87
97
  * point decide (text entry points inject {@link textCanon}). */
88
98
  private _canonOpt;
99
+ /** The work accumulator for the inference call in flight — see
100
+ * {@link MindContext.meter}. Non-null only between beginResponse and
101
+ * endResponse, and only when the Mind was constructed with
102
+ * `{ profile: true }`. */
103
+ meter: Meter | null;
104
+ /** Whether {@link MindOptions.profile} was set. */
105
+ private _profile;
106
+ /** The computational-usage report of the LAST completed inference call, or
107
+ * null when profiling is off (or nothing has been asked yet). Overwritten
108
+ * by every `respond`/`respondTurn`; copy it if you are aggregating. See
109
+ * {@link import("../meter.js").CostReport} and `sumReports`/`formatReport`
110
+ * for battery-level aggregation. */
111
+ lastCost: CostReport | null;
89
112
  /** Memo of the consensus climb — content-keyed. See {@link MindContext.climbMemo}. */
90
113
  climbMemo: Map<string, Map<string, AttentionRead>> | null;
91
114
  /** Memo of recognise() — content-keyed. See {@link MindContext.recogniseMemo}. */
@@ -146,21 +169,49 @@ export declare class Mind implements MindContext {
146
169
  /** Perceive input into a content-defined tree. Deterministic — identical
147
170
  * bytes always produce an identical tree. Public for ingest-cache. */
148
171
  perceive(input: Input, leafAt?: (i: number) => number | null, lookup?: (ids: number[]) => number | null): Sema;
149
- /** Open one response's transient state — the tracer and the per-response
150
- * memos. Paired with {@link endResponse}; the ONE place this state is
151
- * created, so adding a memo cannot forget its reset. */
172
+ /** Open one response's transient state — the tracer, the per-response
173
+ * memos, the work meter. The ONE place this state is created, and it
174
+ * serves BOTH entry points: `respond` takes fresh per-response memos,
175
+ * `respondTurn` passes its conversation, whose memos persist across turns
176
+ * (content-keyed, so the previous turn's results are found by this turn's
177
+ * sub-span calls) and whose `resolvedSubtrees` makes foldTree O(suffix)
178
+ * instead of O(context). respondTurn used to inline its own copy of this
179
+ * and of {@link endResponse}; the two drifted (a memo added to one was
180
+ * silently absent from the other), so there is exactly one pair now. */
152
181
  private beginResponse;
182
+ /** Open (or leave closed) the response's work accumulator. Separate from
183
+ * {@link beginResponse} because {@link respondTurn} keeps its own
184
+ * conversation-scoped lifecycle and must not create fresh per-response
185
+ * memos — but it DOES meter, through this same pair. */
186
+ private _beginMeter;
187
+ /** Close the accumulator and publish its report. Detaching from the store
188
+ * matters: a Mind that shares a store with another Mind must not keep
189
+ * charging that store's reads to a finished response. */
190
+ private _endMeter;
153
191
  /** The canonicalizer a response should carry: the Mind-level option when
154
192
  * set (or none when explicitly disabled), else the entry point's own
155
193
  * default — text entry points pass {@link textCanon}, binary ones null. */
156
194
  private _canonFor;
157
195
  /** Close one response's transient state — every per-response field, incl.
158
- * the edge guide/choices `think` sets mid-flight. */
196
+ * the edge guide/choices `think` sets mid-flight, and the meter's report.
197
+ *
198
+ * A conversation's memo MAPS were mutated in place, so `data.*` still
199
+ * points at them and there is nothing to save back. Clearing the Mind's
200
+ * references is what matters: a concurrently-started `respond()` swaps its
201
+ * own fresh maps into these pointers, and copying back from them here
202
+ * would inject a foreign response's memos into the conversation. */
159
203
  private endResponse;
160
204
  /** Shared response core — the one path from bytes to voiced answer.
161
205
  * `respond` calls this directly; `respondTurn` has its own path
162
206
  * with conversation-persistent memos and incremental perception. */
163
207
  private _respondImpl;
208
+ /** The ONE path from query bytes to a voiced answer: ground (think), then
209
+ * re-voice in the asker's words (articulate). Both entry points run
210
+ * exactly this — they differ only in the LIFECYCLE around it (fresh
211
+ * per-response memos vs. a conversation's persistent ones) and in what
212
+ * they do with the answer afterwards. It must be called between
213
+ * {@link beginResponse} and {@link endResponse}. */
214
+ private _groundAndVoice;
164
215
  respond(input: Input, inspectRationale?: InspectRationale): Promise<Response>;
165
216
  /** Text view of {@link respond}. NUL bytes (0x00) are stripped before
166
217
  * decoding — they are structural padding in text answers. LOSSY for a
@@ -235,7 +286,10 @@ export declare class Mind implements MindContext {
235
286
  * read-out direction of the same operation, without recall's grounding
236
287
  * ladder. If either side's acceptance rule changes, revisit the other. */
237
288
  express(idOrV: number | Vec): Promise<Uint8Array>;
238
- ingest(input: Input | (Input | [Input, Input])[], second?: Input): Promise<(Sema & {
289
+ /** See {@link import("./learning.js").ingest} `onDeposit`, when given,
290
+ * reports each ingested item's deposited root node ids
291
+ * ({@link DepositReport}); purely observational. */
292
+ ingest(input: Input | (Input | [Input, Input])[], second?: Input, onDeposit?: (report: import("./learning.js").DepositReport) => void): Promise<(Sema & {
239
293
  id: number;
240
294
  }) | undefined>;
241
295
  private extensionHost;
@@ -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
@@ -464,8 +483,11 @@ export class Mind {
464
483
  return new Uint8Array(0);
465
484
  }
466
485
  // ── Learning ─────────────────────────────────────────────────────────────
467
- async ingest(input, second) {
468
- return ingest(this, input, second);
486
+ /** See {@link import("./learning.js").ingest} — `onDeposit`, when given,
487
+ * reports each ingested item's deposited root node ids
488
+ * ({@link DepositReport}); purely observational. */
489
+ async ingest(input, second, onDeposit) {
490
+ return ingest(this, input, second, onDeposit);
469
491
  }
470
492
  // ── Extension Surface ────────────────────────────────────────────────────
471
493
  extensionHost() {
@@ -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. */