@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
@@ -351,14 +351,27 @@ export async function counterfactualTransfer(
351
351
  ? s.ctx.subarray(0, r.cs + (r.qe - r.qs))
352
352
  : query.subarray(r.qs, r.qe);
353
353
  };
354
+ // The subject is the closest structure whose FILLER RUN precedes the seat.
355
+ // The gate is on `runs[0]` — the run `fillerOf` actually reads — not on the
356
+ // point's LAST run: requiring the subject's whole alignment to end before
357
+ // the seat disqualifies any structure the query mentions on BOTH sides of
358
+ // it, which is the shape CAST exists for. Measured on
359
+ // `steel is frigid so steel is ???` against `steel is hard so steel is
360
+ // strong` (runs "steel is " at 0-9 and "d so steel is " at 14-28) and
361
+ // `water is frigid so water is freezing` (seat "frigi" at 9-14): the
362
+ // subject's filler run sits squarely before the seat, but its second run —
363
+ // the recurrence AFTER it, the very thing that makes the sentence an
364
+ // analogy — pushed lastRun past the seat and no substitution fired at all.
365
+ // The ordering key follows the gate to the same run, so "closest preceding"
366
+ // still means closest by the evidence actually used.
354
367
  const beforeOf = (p: Point, r: GradedRun): Point | undefined =>
355
368
  argmaxBy(
356
369
  points.filter((s) =>
357
- s !== p && lastRun(s).qe <= r.qs &&
370
+ s !== p && s.runs[0].qe <= r.qs &&
358
371
  s.runs[0].cs < quantum &&
359
372
  usable(s.runs[0].qs, s.runs[0].qe)
360
373
  ),
361
- (s) => lastRun(s).qs,
374
+ (s) => s.runs[0].qs,
362
375
  -Infinity,
363
376
  true,
364
377
  )?.item;
@@ -371,6 +384,18 @@ export async function counterfactualTransfer(
371
384
  const before = beforeOf(p, r);
372
385
  if (before === undefined) return null;
373
386
  if (r.cs > fillerOf(before).length + quantum) return null;
387
+ // SUBSTITUTION MUST ACTUALLY DISPLACE. The schema's premise is that the
388
+ // displaced structure's seat is held by something ELSE, which the
389
+ // subject then replaces. When the subject's filler already occurs in
390
+ // that structure, there is nothing to displace — the "transfer" restates
391
+ // the structure with its own occupant put back, and the answer is a
392
+ // tautology. Measured on `Michelangelo is to sculpture as who is to
393
+ // literature?`: the weave aligned the concept `Michelangelo` and the
394
+ // exemplar `The David was sculpted by Michelangelo.`, and substitution
395
+ // produced `Michelangelo sculpted by Michelangelo.` — then outbid every
396
+ // honest candidate with it (test/29 A2). Byte containment, the same
397
+ // primitive the self-evidence and contradiction guards use.
398
+ if (indexOf(p.ctx, fillerOf(before), 0) >= 0) return null;
374
399
  return { p, before, depth: p.ctx.length - r.cs };
375
400
  })
376
401
  .filter((c): c is NonNullable<typeof c> => c !== null);
@@ -479,13 +504,27 @@ export async function counterfactualTransfer(
479
504
  * schema accounts them. */
480
505
  src: Point;
481
506
  }
507
+ // QUERY-SCALE — "this learnt context is the same size as the question", the
508
+ // bound comparison holds its dominant and its analogs to. A byte-exact
509
+ // `<= query.length` made that judgement turn on a difference the
510
+ // architecture cannot perceive: on `The Weeping Woman was painted by Pablo
511
+ // Picasso.` (47 bytes) the weave's dominant was `The Night Watch was painted
512
+ // by Rembrandt van Rijn.` (50) — three bytes over, so comparison refused
513
+ // outright, while the interchangeable `The Mona Lisa was painted by Leonardo
514
+ // da Vinci.` (47) would have passed. WHICH exemplar becomes dominant is
515
+ // settled by run-claiming order among equals, so a 3-byte difference was
516
+ // deciding whether the schema fires at all (test/33 1b). W is the smallest
517
+ // distinction perception can make — the same quantum countClusters separates
518
+ // neighbourhoods by — so a context within one quantum of the query's length
519
+ // carries no independently perceivable unit beyond it and is the same scale.
520
+ const queryScale = (n: number): boolean => n - query.length < quantum;
482
521
  const analogs: AnalogCandidate[] = [];
483
522
  for (const p of points) {
484
523
  if (p === dominant) continue;
485
524
  // Push the point's own anchor only when its context fits within
486
525
  // the query (the seat sentence must not dominate the comparison).
487
526
  if (
488
- p.ctx.length <= query.length &&
527
+ queryScale(p.ctx.length) &&
489
528
  indexOf(dominant.ctx, p.ctx, 0) < 0 &&
490
529
  indexOf(p.ctx, dominant.ctx, 0) < 0 &&
491
530
  indexOf(query, p.ctx, 0) < 0
@@ -504,7 +543,7 @@ export async function counterfactualTransfer(
504
543
  for (const nid of ctx.store.nextFirst(p.anchor, hubBound(ctx))) {
505
544
  const nctx = read(ctx, nid);
506
545
  if (
507
- nctx.length > query.length ||
546
+ !queryScale(nctx.length) ||
508
547
  indexOf(dominant.ctx, nctx, 0) >= 0 ||
509
548
  indexOf(nctx, dominant.ctx, 0) >= 0 ||
510
549
  indexOf(query, nctx, 0) >= 0
@@ -512,6 +551,23 @@ export async function counterfactualTransfer(
512
551
  analogs.push({ anchor: nid, point: null, src: p });
513
552
  }
514
553
  }
554
+ // MEASURED AND REFUTED — proposing analogs from the dominant's halo when the
555
+ // query-local generator finds none. Both loops above are query-local (an
556
+ // aligned point, or one forward hop off one), while the gate that judges
557
+ // candidates — analogyStrength's halo tier — is cross-domain by construction
558
+ // and is licence enough on its own (`bestHalo` exempts it from the naming and
559
+ // trusted-root bars). So the gate reads as strictly more capable than the
560
+ // generator, and closing that asymmetry looks like the fix for test/29 A2
561
+ // (`Michelangelo is to sculpture as who is to literature?`, whose only stored
562
+ // content is `Michelangelo`: the weave aligns the concept and its own
563
+ // exemplar, each contains the other, so every candidate is excluded and
564
+ // comparison checks zero).
565
+ //
566
+ // It proposes nothing. Measured on A2's own 13-pair corpus: `Michelangelo`
567
+ // HAS a halo, and `haloSiblings` returns not one sibling above
568
+ // significanceBar — the distributional company that would make Shakespeare a
569
+ // cross-domain analog was never trained. The asymmetry is real but it is not
570
+ // what stops A2; the corpus is.
515
571
  let bestAnalog: AnalogCandidate | null = null;
516
572
  let bestSim = 0;
517
573
  let bestHalo = false;
@@ -756,7 +812,7 @@ export async function counterfactualTransfer(
756
812
  bestAnalog !== null &&
757
813
  (bestHalo || analogNamed || rootTrusted) &&
758
814
  !cmpDismisses &&
759
- dominant.ctx.length <= query.length &&
815
+ queryScale(dominant.ctx.length) &&
760
816
  roots.length <= 1 &&
761
817
  !dominates(cmpMaxGap, query.length) &&
762
818
  cmpMaxGap < dominant.ctx.length
@@ -802,7 +858,7 @@ export async function counterfactualTransfer(
802
858
  );
803
859
  } else if (
804
860
  bestAnalog !== null &&
805
- dominant.ctx.length <= query.length &&
861
+ queryScale(dominant.ctx.length) &&
806
862
  roots.length <= 1
807
863
  ) {
808
864
  ctx.trace?.step(
@@ -7,12 +7,9 @@
7
7
  // (or one of its pieces is), and the context is the smallest spanning frame
8
8
  // that contains it.
9
9
 
10
- import type { Vec } from "../../vec.js";
11
10
  import type { MindContext } from "../types.js";
12
- import type { Site } from "../graph-search.js";
13
- import { foldTree, gistOf, perceive, read, resolve } from "../primitives.js";
14
- import { follow, locate } from "../match.js";
15
- import { chooseAmong, hubBound } from "../traverse.js";
11
+ import { read } from "../primitives.js";
12
+ import { follow, isSpanShaped, locate, skillExemplar } from "../match.js";
16
13
  import { concatBytes, indexOf } from "../../bytes.js";
17
14
  import { decodeText, unexplainedLabel } from "../rationale.js";
18
15
  import type {
@@ -358,104 +355,6 @@ export function answerRunsInContext(
358
355
  return merged.length > 0 ? merged : null;
359
356
  }
360
357
 
361
- /** Check whether an anchor is a span-shaped skill exemplar: it represents a
362
- * fact whose context and answer together form a span-in-context pattern.
363
- * If the anchor has a nextOf continuation, that is the answer and the anchor
364
- * itself is the context. Otherwise the anchor's prevOf parents provide
365
- * candidate contexts, and the longest one whose span is span-shaped wins. */
366
- export async function skillExemplar(
367
- ctx: MindContext,
368
- anchor: number,
369
- guide?: Vec | null,
370
- ): Promise<{ contextBytes: Uint8Array; answerBytes: Uint8Array } | null> {
371
- if (ctx.store.hasNext(anchor)) {
372
- const contextBytes = read(ctx, anchor);
373
- const answerBytes = await follow(ctx, anchor, guide);
374
- if (
375
- answerBytes !== null && isSpanShaped(ctx, contextBytes, answerBytes)
376
- ) {
377
- return { contextBytes, answerBytes };
378
- }
379
- return null;
380
- }
381
- const answerBytes = read(ctx, anchor);
382
- // Candidate contexts, capped at the hub bound (a common answer's reverse
383
- // fan-in is corpus-sized).
384
- const capped = ctx.store.prevFirst(anchor, hubBound(ctx));
385
- const spanShaped: Array<{ id: number; bytes: Uint8Array }> = [];
386
- for (const p of capped) {
387
- const ctxB = read(ctx, p);
388
- if (ctxB.length > 0 && isSpanShaped(ctx, ctxB, answerBytes)) {
389
- spanShaped.push({ id: p, bytes: ctxB });
390
- }
391
- }
392
- if (spanShaped.length === 0) return null;
393
- // Among span-shaped contexts, the longest wins (the smallest spanning frame
394
- // heuristic's dual: more frame to locate in the query); the query gist,
395
- // when given, breaks LENGTH TIES via chooseAmong — the same reverse-regime
396
- // disambiguator every context pick uses, whose gist cache spares the
397
- // re-fold this block once paid per tied candidate. Same strict first-seen
398
- // tie-break as the hand loop it replaces.
399
- const maxLen = Math.max(...spanShaped.map((s) => s.bytes.length));
400
- const longest = spanShaped.filter((s) => s.bytes.length === maxLen);
401
- let contextBytes = longest[0].bytes;
402
- if (guide && longest.length > 1) {
403
- const pick = chooseAmong(ctx, longest.map((s) => s.id), guide).id;
404
- contextBytes = longest.find((s) => s.id === pick)!.bytes;
405
- }
406
- return { contextBytes, answerBytes };
407
- }
408
-
409
- /** Whether the answer is a SPARSE subsequence of the context (bytes in
410
- * order, arbitrary gaps) — the OPEN span-shape reading (see the section
411
- * note above). This is what lets extraction validate a MULTI-PIECE
412
- * exemplar whose answer is stitched from several context runs — but it is
413
- * deliberately permissive, so it must never be used as evidence that one
414
- * span was "drawn from" another (see {@link containsSpan} for that).
415
- *
416
- * There is deliberately NO containsSpan pre-check here: strict containment
417
- * IMPLIES the subsequence embedding (a contiguous run, or a resolved node —
418
- * whose content-addressed identity means its bytes occur contiguously — is
419
- * an in-order embedding with zero gaps), so the scan below decides alone,
420
- * with the same truth value. The old pre-check re-perceived the context
421
- * (a full river fold) per CANDIDATE in skillExemplar's √N-capped loop —
422
- * pure cost, no discrimination. */
423
- export function isSpanShaped(
424
- _ctx: MindContext,
425
- context: Uint8Array,
426
- answer: Uint8Array,
427
- ): boolean {
428
- let ai = 0;
429
- for (let ci = 0; ci < context.length && ai < answer.length; ci++) {
430
- if (context[ci] === answer[ai]) ai++;
431
- }
432
- return ai === answer.length;
433
- }
434
-
435
- /** STRICT containment: the answer's resolved node appears in the context's
436
- * folded tree, or the answer occurs as one CONTIGUOUS byte run of the
437
- * context. This is real evidence the answer was drawn from the context.
438
- * Fusion gates on this — the sparse-subsequence reading of
439
- * {@link isSpanShaped} is trivially satisfied by short answers over long
440
- * queries ("cold" is a gap-tolerant subsequence of most sentences holding
441
- * c…o…l…d in order), and gating fusion on it silently starved multi-topic
442
- * queries of their further points of attention. */
443
- export function containsSpan(
444
- ctx: MindContext,
445
- context: Uint8Array,
446
- answer: Uint8Array,
447
- ): boolean {
448
- const ansId = resolve(ctx, answer);
449
- if (ansId !== null) {
450
- let found = false;
451
- foldTree(ctx, perceive(ctx, context), 0, (_n, _s, _e, node) => {
452
- if (node === ansId) found = true;
453
- });
454
- if (found) return true;
455
- }
456
- return indexOf(context, answer, 0) >= 0;
457
- }
458
-
459
358
  // ── Pipeline mechanism ──────────────────────────────────────────────────────
460
359
 
461
360
  export const extractionMechanism: PipelineMechanism = {
@@ -29,6 +29,9 @@ export interface RecallResult {
29
29
  accounted: Array<[number, number]>;
30
30
  moves: number;
31
31
  unexplained: string;
32
+ /** See {@link import("../pipeline-mechanism.js").MechanismResult.complete}
33
+ * — set by the IDENTITY-bridge tier alone. */
34
+ complete?: boolean;
32
35
  }
33
36
 
34
37
  /** Recall the answer by resonating the whole query against the content index. */
@@ -48,6 +51,7 @@ export async function recallByResonance(
48
51
  accounted: Array<[number, number]>,
49
52
  moves: number,
50
53
  echoed = false,
54
+ complete = false,
51
55
  ): RecallResult | null => {
52
56
  t?.done(
53
57
  bytes === null
@@ -61,6 +65,7 @@ export async function recallByResonance(
61
65
  accounted,
62
66
  moves,
63
67
  unexplained: unexplainedLabel(query, accounted),
68
+ ...(complete ? { complete } : {}),
64
69
  };
65
70
  };
66
71
  const k = pre.k;
@@ -329,14 +334,35 @@ export async function recallByResonance(
329
334
  // natural, tuning-free ceiling (probe every cluster) for a call that
330
335
  // is ALREADY refusal-path-only and must not miss a candidate hiding
331
336
  // behind an unlucky structural distance.
332
- const wide = k < hubBound(ctx)
333
- ? await ctx.store.resonate(queryGist, hubBound(ctx), true)
334
- : whole;
335
- const bridged = await substitutionBridge(
336
- ctx,
337
- query,
338
- wide.map((h) => h.id),
339
- );
337
+ //
338
+ // MEASURED COST, AND WHY IT STAYS (17.9M vectors / 325K contexts):
339
+ // this one call is ~570 ms and ~45% of all inference time on a refusing
340
+ // query. The cost is entirely `exhaustive` (nprobe = every cluster),
341
+ // NOT the widened k — timed on that store: k=571 exhaustive 632 ms,
342
+ // k=24 exhaustive 536 ms, k=571 NON-exhaustive 12 ms. So narrowing k
343
+ // buys nothing and the 50x is the whole-index scan itself.
344
+ // It is load-bearing: over an 18-query battery the bridge produced a
345
+ // winner 4 times, and ALL FOUR winners came from this proposal channel
346
+ // — the anchor-climb channel won nothing on its own. Reordering the
347
+ // channels (climb first, resonate only on failure) would therefore pay
348
+ // the climb, fail, and pay this anyway. Do not weaken it without
349
+ // re-running that measurement.
350
+ // Handed to the bridge as a THUNK: this exhaustive whole-index probe is
351
+ // the most expensive single act on the refusal path, and the bridge's
352
+ // own cheap gates (query length, the O(|query|) stored-window anchor
353
+ // scan) can refuse without any proposal at all. See substitutionBridge.
354
+ const wideIds = async (): Promise<ReadonlyArray<number>> => {
355
+ const wide = k >= hubBound(ctx)
356
+ ? whole
357
+ : ctx.meter
358
+ ? await ctx.meter.time(
359
+ "recall.exhaustiveResonate",
360
+ () => ctx.store.resonate(queryGist, hubBound(ctx), true),
361
+ )
362
+ : await ctx.store.resonate(queryGist, hubBound(ctx), true);
363
+ return wide.map((h) => h.id);
364
+ };
365
+ const bridged = await substitutionBridge(ctx, query, wideIds);
340
366
  if (bridged !== null) {
341
367
  const g = await project(ctx, bridged.id, queryGist);
342
368
  // A projection contained in a substituted candidate-side span is the
@@ -357,16 +383,56 @@ export async function recallByResonance(
357
383
  ) {
358
384
  return ground(
359
385
  g,
360
- `substitution bridge — a trained context accounts for the query ` +
361
- `up to ${bridged.subs.length} corroborated substitution(s)`,
362
- // Accounted NOTHING — the same epistemic humility as the echo
363
- // tier below: a substitution-bridged grounding is a last resort
364
- // that must lose to ANY mechanism that actually explained the
365
- // query (observed: pricing the aligned spans here outweighed
366
- // extraction's correct answer in the grounding decider), while
367
- // still beating silence when everything else refused.
368
- [],
386
+ bridged.subs.length === 0
387
+ ? `identity bridge — a trained context IS this query, up to ` +
388
+ `scaffolding the corpus itself treats as filler`
389
+ : `substitution bridge a trained context accounts for the ` +
390
+ `query up to ${bridged.subs.length} corroborated ` +
391
+ `substitution(s)`,
392
+ // WHAT THIS GROUNDING EXPLAINS the spans its alignment covers,
393
+ // matched AND substituted, for BOTH tiers.
394
+ //
395
+ // A corroborated substitution is not a gap in the explanation; it is
396
+ // an explanation the mechanism PAID for, one CONCEPT each in `moves`
397
+ // just below. Leaving its span unaccounted charges the same act
398
+ // twice — once as a move, once as PASS-per-unexplained-byte — and the
399
+ // second charge is far the larger, so a bridge that matched 28 of 29
400
+ // bytes declared the whole query unexplained and lost to any
401
+ // mechanism with a smaller honest claim. Measured on test/49's
402
+ // paraphrase: it FOUND the trained fact through two corroborated case
403
+ // substitutions and was outbid 29011 to 1012 by a CAST comparison
404
+ // that voiced the wrong country.
405
+ //
406
+ // This tier DID once report `[]` for the substituted case, against
407
+ // the observation that "pricing the aligned spans outweighed
408
+ // extraction's correct answer in the grounding decider". That is no
409
+ // longer so and the suite is the witness: with the fold's regions
410
+ // content-defined and the junction tiers no longer consuming each
411
+ // other's candidates, full accounting here passes every test that
412
+ // refutation was recorded for. Reporting only the LITERALLY matched
413
+ // spans (accounted minus the substituted ones) was also implemented
414
+ // and is a strictly worse reading of the same ladder — it still
415
+ // double-charges, just less.
416
+ //
417
+ // An IDENTITY bridge (zero substitutions) substituted nothing, so
418
+ // there is nothing to be humble about: every accounted byte is a
419
+ // LITERAL match against a trained form, and the query is that form
420
+ // up to scaffolding. Reporting `[]` for it was actively wrong in
421
+ // two ways — it priced a full explanation at PASS-per-byte so junk
422
+ // outweighed it, and, because the honest-remainder test in think()
423
+ // reads the same spans, it left the whole query "unaccounted" and
424
+ // forced the multi-topic fusion gate open. Observed live: the
425
+ // correct "What is the process of photosynthesis?" grounding was
426
+ // fused away into an unrelated "Hello! How can I assist you
427
+ // today?" point of attention.
428
+ [...bridged.accounted],
369
429
  CONCEPT * bridged.subs.length + STEP,
430
+ false,
431
+ // COMPLETE only for the identity tier: the query IS this trained
432
+ // context, so `g` is that context's own continuation — the whole
433
+ // read-out. A SUBSTITUTED bridge makes no such claim (it stood a
434
+ // different word in the query's place), so it stays extendable.
435
+ bridged.subs.length === 0,
370
436
  );
371
437
  }
372
438
  }
@@ -434,6 +500,7 @@ export const recallMechanism: PipelineMechanism = {
434
500
  moves: r.moves,
435
501
  unexplained: r.unexplained,
436
502
  provenance: r.echoed ? "recall-echo" : "recall",
503
+ ...(r.complete ? { complete: true } : {}),
437
504
  }];
438
505
  },
439
506
  };