@hviana/sema 0.8.2 → 0.8.3

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 (86) hide show
  1. package/AGENTS.md +29 -29
  2. package/TRADEMARKS.md +0 -1
  3. package/dist/src/config.d.ts +11 -0
  4. package/dist/src/config.js +2 -0
  5. package/dist/src/geometry.d.ts +21 -0
  6. package/dist/src/geometry.js +21 -0
  7. package/dist/src/meter.d.ts +51 -0
  8. package/dist/src/meter.js +51 -0
  9. package/dist/src/mind/attention.d.ts +4 -0
  10. package/dist/src/mind/attention.js +165 -16
  11. package/dist/src/mind/canonical.d.ts +16 -0
  12. package/dist/src/mind/canonical.js +41 -0
  13. package/dist/src/mind/graph-search.js +33 -14
  14. package/dist/src/mind/match.d.ts +1 -1
  15. package/dist/src/mind/match.js +5 -3
  16. package/dist/src/mind/mechanisms/cast.js +1 -1
  17. package/dist/src/mind/mechanisms/confluence.js +24 -0
  18. package/dist/src/mind/mechanisms/recall.js +32 -4
  19. package/dist/src/mind/mind.d.ts +4 -2
  20. package/dist/src/mind/mind.js +5 -4
  21. package/dist/src/mind/pipeline-mechanism.d.ts +7 -0
  22. package/dist/src/mind/pipeline.js +41 -14
  23. package/dist/src/mind/primitives.js +9 -1
  24. package/dist/src/mind/rationale.d.ts +28 -1
  25. package/dist/src/mind/rationale.js +22 -1
  26. package/dist/src/mind/reasoning.d.ts +21 -3
  27. package/dist/src/mind/reasoning.js +73 -21
  28. package/dist/src/mind/recognition.js +4 -8
  29. package/dist/src/mind/resonance.js +20 -1
  30. package/dist/src/mind/trace.js +1 -0
  31. package/dist/src/mind/traverse.js +6 -2
  32. package/dist/src/mind/types.d.ts +36 -13
  33. package/docs/INVARIANTS.md +2 -2
  34. package/docs/architecture/bounded-reads.md +1 -1
  35. package/docs/architecture/commonality.md +2 -2
  36. package/docs/architecture/cost-model.md +2 -2
  37. package/docs/architecture/determinism.md +7 -7
  38. package/docs/architecture/match-project.md +2 -3
  39. package/docs/architecture/mechanism-market.md +10 -10
  40. package/docs/architecture/meter.md +5 -5
  41. package/docs/architecture/store.md +3 -3
  42. package/docs/failures/tempting-but-wrong.md +3 -4
  43. package/docs/harness/gates.md +2 -2
  44. package/docs/mechanisms/cast.md +2 -2
  45. package/docs/mechanisms/cover.md +2 -3
  46. package/docs/mechanisms/extraction.md +7 -7
  47. package/docs/mechanisms/recall.md +8 -9
  48. package/jsr.json +1 -1
  49. package/package.json +1 -1
  50. package/src/alu/README.md +11 -12
  51. package/src/config.ts +13 -0
  52. package/src/geometry.ts +21 -0
  53. package/src/meter.ts +51 -0
  54. package/src/mind/attention.ts +167 -16
  55. package/src/mind/canonical.ts +43 -0
  56. package/src/mind/graph-search.ts +39 -14
  57. package/src/mind/match.ts +5 -3
  58. package/src/mind/mechanisms/cast.ts +3 -1
  59. package/src/mind/mechanisms/confluence.ts +24 -0
  60. package/src/mind/mechanisms/recall.ts +32 -4
  61. package/src/mind/mind.ts +6 -4
  62. package/src/mind/pipeline-mechanism.ts +7 -0
  63. package/src/mind/pipeline.ts +49 -16
  64. package/src/mind/primitives.ts +9 -1
  65. package/src/mind/rationale.ts +35 -1
  66. package/src/mind/reasoning.ts +92 -15
  67. package/src/mind/recognition.ts +4 -8
  68. package/src/mind/resonance.ts +19 -1
  69. package/src/mind/trace.ts +1 -0
  70. package/src/mind/traverse.ts +7 -5
  71. package/src/mind/types.ts +36 -13
  72. package/test/105-derive-through-reports-its-refusal.test.mjs +24 -0
  73. package/test/118-the-join-reaches-a-key-off-the-cut.test.mjs +74 -0
  74. package/test/119-the-work-does-not-grow-with-the-corpus.test.mjs +122 -0
  75. package/test/120-composition-is-consequence.test.mjs +132 -0
  76. package/test/121-the-extension-does-not-grow-with-the-corpus.test.mjs +128 -0
  77. package/test/122-the-climb-search-does-not-grow-with-the-corpus.test.mjs +117 -0
  78. package/test/123-the-paired-formulas-agree.test.mjs +90 -0
  79. package/test/125-the-post-grounding-branch-publishes-its-operand.test.mjs +51 -0
  80. package/test/126-the-pipeline-does-not-name-mechanisms.test.mjs +42 -0
  81. package/test/128-the-leads-somewhere-pair-agrees.test.mjs +83 -0
  82. package/test/129-the-trace-payload-shape.test.mjs +164 -0
  83. package/test/32-confluence.test.mjs +68 -0
  84. package/test/38-reason-restate-guard.test.mjs +8 -2
  85. package/test/43-cast-analog-seat.test.mjs +10 -0
  86. package/test/55-cost-meter.test.mjs +859 -0
@@ -238,6 +238,14 @@ export interface DerivationItem {
238
238
  * {@link GraphSearch}'s rules fired, recovered from the rule's premise/
239
239
  * conclusion shape (the rules carry no label, so this classifies by structure,
240
240
  * the single place that maps rule geometry to a name). */
241
+ // CLOSED ON PURPOSE — AND ONLY THIS ONE IS. A derivation move is BRANCHED ON
242
+ // (`classifyMove`, the rationale's readers, MOVE_NOTE's fallback), so it is a
243
+ // closed union: adding one without teaching every reader is a compile error,
244
+ // which is what a closed vocabulary buys. The MECHANISM names in
245
+ // `rationale.ts` are the opposite case — written and displayed, never
246
+ // branched on — and they stay free strings that COMPOSE with the nesting
247
+ // (`["respond", "think", "recognise"]`). That asymmetry is deliberate; do not
248
+ // "fix" it by uniting the two (see test/126 for the pipeline half of it).
241
249
  export type DerivationMove =
242
250
  | "axiom" // a seed: a perceived leaf, a recognised form, or a computed result
243
251
  | "follow-edge" // form→form via a continuation edge (STEP) — the core "what follows what"
@@ -1135,6 +1143,7 @@ export class GraphSearch {
1135
1143
  // concepts/connectors either (those need the caller's async
1136
1144
  // pre-resolution) — the recursion follows edges and fusion, which is what
1137
1145
  // a deeper rewrite chain is made of.
1146
+ if (this.host.meter) this.host.meter.recompletes++;
1138
1147
  const rec = this.host.recogniseSpan(bytes);
1139
1148
  const kids = new Set(nrec.kids);
1140
1149
  // THE NODE'S OWN KIDS ARE SITES BY STRUCTURE — recognition cannot be the
@@ -1391,20 +1400,36 @@ export class GraphSearch {
1391
1400
  // duplicate read and a branch that could never be taken.
1392
1401
  let next: number | null = null;
1393
1402
  let keyBytes = c.bytes;
1394
- // THE PREFIX ENDS ARE THE TAIL'S OWN FOLD BOUNDARIES, not every byte
1395
- // length. The key is `entity + prefix`, and the prefix that names a
1396
- // stored relation ends where the fold cuts: measured over four join-firing
1397
- // queries, 5 of 5 accepted keys ended on a boundary (or the tail's end)
1398
- // while the byte-by-byte scan spent 153 probes where 14 boundaries would
1399
- // do. Same criterion — resolves AND leads — same shortest-first order, so
1400
- // the answer is the same one the enumeration found; only the candidates
1401
- // come from the structure instead of from the byte count. A host with no
1402
- // boundary rule falls back to the enumeration.
1403
- const cuts = this.host.contentCuts?.(tail);
1404
- const ends = cuts && cuts.length > 0
1405
- ? [...cuts.filter((c) => c > 0 && c < tail.length), tail.length]
1406
- : Array.from({ length: tail.length }, (_, i) => i + 1);
1407
- for (const len of ends) {
1403
+ // THE CANDIDATE ENDS ARE THE PREFIXES THAT ARE STORED NODES, ASCENDING.
1404
+ // The key is `entity + prefix`, and it names a relation exactly when that
1405
+ // concatenation IS a node — so the ends come from a content-addressed
1406
+ // probe per offset (the host's `contentKeyEnds`, the learning path's own
1407
+ // mechanism: one leaf walk plus one `findBranch` per offset, no `resolve`),
1408
+ // never from the fold's boundaries. A boundary is not a proxy: a stored
1409
+ // member's end is the end of ITS OWN stream, and the fold never cuts at a
1410
+ // stream's end — measured, "stockholm mayor" exists, leads on to the mayor
1411
+ // fact, and its boundary 6 sits in neither the tail's cuts ([4,7]) nor the
1412
+ // concatenation's. Filtering the scan by "is this a node?" cannot change
1413
+ // the winner: a position that is not a node cannot resolve, so skipping it
1414
+ // is invisible; and the order stays SHORTEST FIRST, which is a semantic
1415
+ // law, not an optimisation (test/106, test/108 pin it).
1416
+ //
1417
+ // A host that cannot answer falls back to every prefix: exact and
1418
+ // complete, at a `resolve` per offset. A host that CAN answer is
1419
+ // authoritative even when it answers "none" — if no prefix is a node then
1420
+ // no key exists to resolve, so enumerating would only pay nulls. (A key
1421
+ // reachable through the CANONICAL equivalence alone and ending off every
1422
+ // node end is therefore not tried here; that dimension is unreachable on
1423
+ // this path by construction and is not part of the exact-key law.)
1424
+ const ends = this.host.contentKeyEnds?.(c.bytes, tail);
1425
+ const candidateEnds = function* (): Generator<number> {
1426
+ if (ends !== undefined) {
1427
+ yield* ends;
1428
+ return;
1429
+ }
1430
+ for (let p = 1; p <= tail.length; p++) yield p;
1431
+ };
1432
+ for (const len of candidateEnds()) {
1408
1433
  keyBytes = concat2(c.bytes, tail.subarray(0, len));
1409
1434
  const k = this.host.resolve(keyBytes) ??
1410
1435
  this.host.canonResolve?.(keyBytes) ??
package/src/mind/match.ts CHANGED
@@ -723,7 +723,7 @@ export function frameSlots(
723
723
  * ANCHOR that the query displaced. Neither implies the other, and the
724
724
  * observed failures pass the restatement guard cleanly.
725
725
  *
726
- * Three conditions, all byte-exact and all necessary:
726
+ * Four conditions, all byte-exact and all necessary:
727
727
  *
728
728
  * 1. the query and the anchor must be ONE STRUCTURE — what they share has to
729
729
  * dominate the query, or the query is not a variant of the anchor at all
@@ -811,8 +811,10 @@ export function substituteAll(
811
811
  const usable = pairs.filter((p) => p.needle.length > 0);
812
812
  if (usable.length === 0) return hay;
813
813
  // Longest needle first, so a needle that is a prefix of another can never
814
- // pre-empt it. Ties cannot arise: an instance whose fillers are not
815
- // pairwise distinct is refused by frameSlots.
814
+ // pre-empt it. Ties cannot arise: a consumer that VOICES checks the
815
+ // fillers pairwise with `distinct` and refuses such an instance itself —
816
+ // `frameSlots` reports and does not judge (see its own doc), so the refusal
817
+ // lives with the mechanism that needs it, not here.
816
818
  const order = [...usable].sort((a, b) => b.needle.length - a.needle.length);
817
819
  const out: number[] = [];
818
820
  let i = 0;
@@ -867,7 +867,9 @@ export async function counterfactualTransfer(
867
867
  // grounds") — fine for ORIENTING mechanisms, not for voicing learnt
868
868
  // content the query never asked about. Computed once here; both the
869
869
  // hub fallback below and the comparison gate consume it.
870
- const rootTrusted = roots.some((r) => r.vote >= consensusFloor(corpusN(ctx)));
870
+ const rootTrusted = roots.some((r) =>
871
+ r.idfVote >= consensusFloor(corpusN(ctx))
872
+ ); // the IDF sum: the bar's own quantity
871
873
  // The context that ESTABLISHES a filler — the same reverse context, under
872
874
  // the same naming test, `seatOfNode` uses to VOICE an analog (a predecessor
873
875
  // whose bytes CONTAIN the node's: it names or describes it, rather than
@@ -146,6 +146,30 @@ export async function confluenceJoin(
146
146
  const bindsAConstituent = (cover: Array<[number, number]>): boolean =>
147
147
  cover.some(([cs, ce]) => ce - cs >= 2 * W);
148
148
 
149
+ // THE VOTE ENTERS AS ORDER, NEVER AS A BAR. This is the only one of the
150
+ // climb's four consumers (recall, fuseAttention, cast, here) that uses the
151
+ // evidence's MAGNITUDE without a floor, and it is legitimate by construction:
152
+ // `ranked` answers "which anchor is stronger" — a question about votes, so the
153
+ // comparison stays within one dimension — and the vote is otherwise only
154
+ // REPORTED (Stream.vote travels to the rationale's constraint nodes). What
155
+ // actually SELECTS a constraint is byte-structural and never the magnitude: a
156
+ // run of at least 2W (`bindsAConstituent`, with its accidental-sharing
157
+ // counter-examples above), disjoint covers (`disjoint`), and scaffolding never
158
+ // binds at all (`dominates(reachOf(…), N)`). The MEET such a stream may
159
+ // produce is selected the same way: a span shorter than 2W is rejected, and
160
+ // the winner is the one with the smallest `reach` (ties broken by the longer
161
+ // span) — a corpus quantity and bytes, never the vote, which appears only in
162
+ // the trace item.
163
+ // binds at all (`dominates(reachOf(…), N)`). The only cut in this loop is a
164
+ // BUDGET, and it is measured: stopping the scan at 2W anchors saves 50-70% of
165
+ // confluence's cost on non-conjunctive queries while preserving every genuinely
166
+ // conjunctive case, whose top anchors ARE its constraints.
167
+ // MEASURED (this goal, on THIS file's own conjunctive fixture): the two
168
+ // streams appear at ranks 1 and 4 against a budget of 2W = 8, on a query whose
169
+ // `ranked` is 9 — so the cut IS live (it would have returned null at the 8th
170
+ // anchor) and it does NOT prune the case it exists to protect. The other
171
+ // conjunctive fixture (the Leonardo one) finds them at ranks 0 and 1. Scope:
172
+ // these are the repo's conjunctive fixtures, and no more.
149
173
  const streams: Stream[] = [];
150
174
  const rankedCapped = ranked.length > pre.k ? ranked.slice(0, pre.k) : ranked;
151
175
  // CONJUNCTIVITY EARLY-EXIT: a conjunctive query's top-ranked anchors
@@ -276,9 +276,36 @@ export async function recallByResonance(
276
276
  // consensus", while breadth is the SCALE-INVARIANT reading — "a point whose
277
277
  // breadth clears `dominates` (> half the query's regions corroborate it) is
278
278
  // real consensus; one that does not is a coincidental single-region echo".
279
- // Attention.peak's contract makes the same point from the other side:
280
- // comparing a POOLED SUM against a floor that prices ONE region's evidence
281
- // is a dimensional error.
279
+ // THIS USED TO CLAIM A DIMENSIONAL ERROR, AND THAT CLAIM WAS FALSE.
280
+ // It read: "comparing a POOLED SUM against a floor that prices ONE region's
281
+ // evidence is a dimensional error." `consensusFloor` is not priced for one
282
+ // region: thresholds.md §2 derives it as the POOLED-vote significance floor —
283
+ // "each region contributes at most ln(N/c) <= ln(N); ln(N)+1/2 demands ..." —
284
+ // and attention.ts says the same where it builds the vote ("the scale
285
+ // consensusFloor is derived for"). The comparison is in ONE dimension, and
286
+ // it is so because the climb WEIGHTS BY IDF: `wf` in voteRegions is
287
+ // `direct ? df : combined ? idf + df : idf`, and the engine only ever runs the
288
+ // last one (DFMode's default "inverse", the mode every non-test caller uses —
289
+ // `direct` and `combined` are exercised by test/24 and test/27 only, and
290
+ // test/24 pins that their votes DO differ). In those two the sum would leave
291
+ // the floor's dimension and the floor would need re-deriving.
292
+ //
293
+ // What the OR below is really for is SCALE, not dimension (the paragraph
294
+ // above says it): a vote that clears ln(N)+1/2 means "strong" on a small store
295
+ // and "weak" on a large one for the same genuine consensus, so the
296
+ // scale-invariant breadth reading is added beside it.
297
+ //
298
+ // AND THE PREMISE IS IDF. The deviation in the other two weighting modes is
299
+ // TWO-SIDED and DERIVED: `direct` DEFLATES a region (ln(1+c) < ln(N/c) for
300
+ // small c) and `combined` INFLATES it (ln N + ln(1+1/c)), both by at most
301
+ // `ln 2` — see `geometry.ts`'s `consensusFloor`, where the bound lives.
302
+ // MEASURED on 8 anchors across 5 queries, running the same climb in all
303
+ // three modes: ZERO gate inversions — every anchor's `vote >= floor` verdict
304
+ // is the same in `inverse`, `direct` and `combined`, even where the readings
305
+ // straddle the floor on opposite sides (#148: inverse 3.39, combined 4.71
306
+ // above it, direct 1.31 below). Pinned by test/55's test 20. The bar is not
307
+ // re-derived for those modes because nothing reachable needs it; the premise
308
+ // is IDF, and that is now written where the gate reads it.
282
309
  //
283
310
  // Measured on the 15.7M-node store (N=325,615, so the old floor was 13.19).
284
311
  // The absolute vote cannot separate right from wrong at this scale, and the
@@ -348,7 +375,7 @@ export async function recallByResonance(
348
375
  if (
349
376
  forest.length > 0 &&
350
377
  !allWindowsAreScaffolding(ctx, query) &&
351
- (forest[0].vote >= minVote ||
378
+ (forest[0].idfVote >= minVote || // the IDF sum: the bar's own quantity
352
379
  (dominates(forest[0].breadth, 1) && forest[0].peak > Math.LN2))
353
380
  ) {
354
381
  const g = await project(ctx, forest[0].anchor, queryGist);
@@ -602,6 +629,7 @@ export const recallMechanism: PipelineMechanism = {
602
629
  moves: r.moves,
603
630
  unexplained: r.unexplained,
604
631
  provenance: r.echoed ? "recall-echo" : "recall",
632
+ used: new Set<number>(),
605
633
  ...(r.complete ? { complete: true } : {}),
606
634
  }];
607
635
  },
package/src/mind/mind.ts CHANGED
@@ -16,7 +16,6 @@ import type { CorpusPair, CorpusResult } from "./corpus.js";
16
16
  import { Alphabet } from "../alphabet.js";
17
17
  import {
18
18
  bytesToTree,
19
- contentBoundaries,
20
19
  contentFoldIncremental,
21
20
  Grid,
22
21
  gridToTree,
@@ -24,6 +23,7 @@ import {
24
23
  reachThreshold,
25
24
  stackGrids,
26
25
  } from "../geometry.js";
26
+ import { keyEnds } from "./canonical.js";
27
27
  import type { ContentFold } from "../geometry.js";
28
28
  import { BoundedMap, type Store } from "../store.js";
29
29
  import { SQliteStore } from "../store-sqlite.js";
@@ -244,6 +244,8 @@ export interface MindOptions {
244
244
  seed?: number;
245
245
  recallQueryK?: number;
246
246
  haloQueryK?: number;
247
+ /** Branch nodes the pivot sweep may probe — see {@link MindConfig}. */
248
+ pivotProbeK?: number;
247
249
  /** Items one rationale step may itemise — see {@link MindConfig}. */
248
250
  rationaleSampleK?: number;
249
251
  /** Corpus-reading capacities and budgets — see {@link MindConfig}. */
@@ -443,9 +445,9 @@ export class Mind implements MindContext {
443
445
  * with the most distributional evidence (highest `prevOf` count — the
444
446
  * structural manifestation of its halo). When evidence is equal the
445
447
  * first-inserted edge wins. */
446
- /** See {@link GraphSearchHost.contentCuts}. */
447
- contentCuts(bytes: Uint8Array): readonly number[] {
448
- return contentBoundaries(this.space, bytes);
448
+ /** See {@link GraphSearchHost.contentKeyEnds}. */
449
+ contentKeyEnds(prefix: Uint8Array, tail: Uint8Array): readonly number[] {
450
+ return keyEnds(this, prefix, tail);
449
451
  }
450
452
 
451
453
  chooseNext(node: number): number | undefined {
@@ -675,6 +675,13 @@ export interface MechanismResult {
675
675
  bytes: Uint8Array;
676
676
  accounted: Array<[number, number]>;
677
677
  moves: number;
678
+ /** WHAT THIS ANSWER SPEAKS FOR — the anchors it voices, and therefore the
679
+ * content the reasoner must not pivot back through. Declared by the
680
+ * mechanism about its OWN result, exactly like `accounted`/`unexplained`/
681
+ * `complete`: post-grounding honours the property and NEVER ASKS WHICH
682
+ * MECHANISM SET IT, so the market stays uniform. An EMPTY set is a real
683
+ * declaration — "this answer voices nothing" (recall) — and withholds
684
+ * nothing; omit the field and the pipeline re-recognises the answer. */
678
685
  used?: ReadonlySet<number>;
679
686
  unexplained: string;
680
687
  /** Explicit weight override. When absent, weight = moves + PASS·unaccounted. */
@@ -15,7 +15,7 @@ import type { ComputedSpan } from "../extension.js";
15
15
  import { gistOf, read, resolve } from "./primitives.js";
16
16
  import { recognise } from "./recognition.js";
17
17
  import { fuseAttention, reason } from "./reasoning.js";
18
- import { unexplainedSpans } from "./rationale.js";
18
+ import { unaccountedBytes, unexplainedSpans } from "./rationale.js";
19
19
  import { rItem } from "./trace.js";
20
20
  import { hubBound } from "./traverse.js";
21
21
  import { type PipelineMechanism, Precomputed } from "./pipeline-mechanism.js";
@@ -253,8 +253,7 @@ export async function think(
253
253
  }
254
254
  const grade = (w: number) => Math.floor(w / STEP);
255
255
  const unaccounted = (spans: ReadonlyArray<[number, number]>): number =>
256
- unexplainedSpans(query.length, spans)
257
- .reduce((sum, [s, e]) => sum + (e - s), 0);
256
+ unaccountedBytes(unexplainedSpans(query.length, spans));
258
257
  const weigh = (
259
258
  accounted: ReadonlyArray<[number, number]>,
260
259
  moves: number,
@@ -504,14 +503,11 @@ export async function think(
504
503
  }
505
504
  const answer: Uint8Array = decided.bytes;
506
505
  const provenance = decided.provenance as Provenance;
507
- const castUsed: ReadonlySet<number> = decided.used ?? new Set();
506
+ const declaredUsed = decided.used;
508
507
 
509
508
  // ── Post-grounding, gated by provenance ──────────────────────────────
510
- const preConsumed = provenance === "cast" || provenance === "join"
511
- ? castUsed
512
- : provenance === "recall" || provenance === "recall-echo"
513
- ? new Set<number>()
514
- : new Set(recognise(ctx, answer).sites.map((s) => s.payload));
509
+ const preConsumed = declaredUsed ??
510
+ new Set(recognise(ctx, answer).sites.map((s) => s.payload));
515
511
  // A grounding that DECLARED itself complete is not extended: the answer is
516
512
  // already a trained form's own continuation, reached through an identity
517
513
  // claim about the query, so a multi-hop pivot could only chain past the
@@ -540,11 +536,30 @@ export async function think(
540
536
  // `preConsumed` is derived by re-recognising the answer — "everything in
541
537
  // it", not "what it voiced" — and a containment rule over that would
542
538
  // suppress every pivot the answer legitimately contains.
543
- const voiced = (provenance === "cast" || provenance === "join")
544
- ? [...castUsed].flatMap((id) =>
545
- ctx.store.nextFirst(id, hubBound(ctx)).map((n) => read(ctx, n))
546
- )
547
- : [];
539
+ const voiced = declaredUsed === undefined ? [] : [...declaredUsed].flatMap(
540
+ (id) => ctx.store.nextFirst(id, hubBound(ctx)).map((n) => read(ctx, n)),
541
+ );
542
+ // WHAT THIS BRANCH READ, published where it was read. Post-grounding decides
543
+ // by `decided.used` and by the provenance NAME; the operands were invisible in
544
+ // the trace, so a change to the branching could not be shown equivalent or
545
+ // otherwise from outside — three separate investigations failed on exactly
546
+ // that gap. A gap in instrumentation is a defect IN the instrumentation
547
+ // (AGENTS.md §6): closed here, once, as counts only — never content.
548
+ ctx.trace?.step(
549
+ "postGrounding",
550
+ [rItem(answer, provenance)],
551
+ [],
552
+ `used=${decided.used !== undefined ? "declared" : "absent"} · ` +
553
+ `preConsumed=${preConsumed.size} · voiced=${voiced.length}`,
554
+ undefined,
555
+ {
556
+ version: 1,
557
+ provenance,
558
+ usedDeclared: decided.used !== undefined,
559
+ preConsumed: preConsumed.size,
560
+ voiced: voiced.length,
561
+ },
562
+ );
548
563
  // REPORTABLE, NOT SILENT. A declared-complete grounding ends the derivation
549
564
  // here, and that decision is part of the derivation's shape: the reader of a
550
565
  // rationale must be able to see that the chain stopped because the mechanism
@@ -573,12 +588,24 @@ export async function think(
573
588
  ];
574
589
  const uncovered = unexplainedSpans(query.length, explained)
575
590
  .filter(([a, b]) => b - a >= ctx.space.maxGroup);
576
- const reasoned = decided.complete ? answer : meter
591
+ // PUBLISHED, NOT RECOMPUTED: the same `uncovered` the gates below read. A
592
+ // write-only accounting (meter contract 1), so the number that licenses an
593
+ // extension or a fusion stops being invisible.
594
+ if (meter) {
595
+ meter.postGroundingRemainderSpans += uncovered.length;
596
+ meter.postGroundingRemainderBytes += unaccountedBytes(uncovered);
597
+ }
598
+ // The extension is kept as a WHOLE (bytes + what it carried + how many steps),
599
+ // not just its bytes: pricing it — `steps · STEP` against `PASS · unaccounted`
600
+ // — is the caller's job, one comparison away. `reasoned` stays the bytes so
601
+ // everything downstream is untouched.
602
+ const extension = decided.complete ? undefined : meter
577
603
  ? await meter.time(
578
604
  "reason",
579
605
  () => reason(ctx, query, answer, preConsumed, pre, voiced, uncovered),
580
606
  )
581
607
  : await reason(ctx, query, answer, preConsumed, pre, voiced, uncovered);
608
+ const reasoned = extension?.bytes ?? answer;
582
609
 
583
610
  // Fuse only when the query has a genuine REMAINDER no mechanism's
584
611
  // structural evidence touched at all. `decided.accounted` alone
@@ -639,7 +666,13 @@ export async function think(
639
666
 
640
667
  done(
641
668
  fused,
642
- "grounded, reasoned forward, fused across points of attention",
669
+ // NO CLAIM ABOUT FUSION HERE. `fuseAttention` is entered whenever a
670
+ // remainder ≥ W exists and returns early when there is nothing to bridge, so
671
+ // this note used to assert a fusion that frequently did not happen (measured:
672
+ // "What is the capital of France famous for" fuses 0 times). The fusion is
673
+ // reported by `fuseAttention`'s own `done` when it happens — the layer that
674
+ // did the work is the layer that says so.
675
+ "grounded, reasoned forward",
643
676
  );
644
677
  return { bytes: fused, provenance };
645
678
  }
@@ -338,7 +338,15 @@ export function canonResolve(
338
338
  // on exactly the node the canonical-case query would have found.
339
339
  const folded = foldTree(ctx, perceive(ctx, bytesOf), 0).node;
340
340
  const use = folded ?? id;
341
- const leads = store.hasNext(use) || store.haloMass(use) > 0;
341
+ // THE ADMISSION PREDICATE, by its own pair of probes: `traverse.ts`'s
342
+ // `leadsSomewhere` is edge-or-halo, and `hasHalo` is the one that carries
343
+ // the mass bar (`mass >= minHaloMass`). Asking `haloMass(use) > 0` instead
344
+ // is the same answer only while `minHaloMass <= 1` (its default): raise the
345
+ // bar and this site would rank a node as leading on evidence the law
346
+ // refuses. Calling `leadsSomewhere` here is not possible — `traverse.ts`
347
+ // imports THIS file, so it would be a cycle — which is why the pair is
348
+ // spelled out rather than named.
349
+ const leads = store.hasNext(use) || store.hasHalo(use);
342
350
  if (
343
351
  best === null || (leads && !bestLeads) ||
344
352
  (leads === bestLeads && use < best)
@@ -49,6 +49,17 @@ export interface RationaleItem {
49
49
  * caller asked to carry it (off by default — a D-float array per item would
50
50
  * bury the reasoning it is meant to explain). */
51
51
  v?: Vec;
52
+ /** The element's OWN bytes, attached BY REFERENCE when the step was built from
53
+ * bytes (a `rationale.ts` item made from a node carries none: read it back
54
+ * through `node`). `text` is a RENDERING and cannot stand in for them — it
55
+ * decodes UTF-8 and DROPS NUL bytes, so a key containing one is unrecoverable
56
+ * from it, which is exactly how a join refusal (`deriveThroughMiss`) became
57
+ * impossible to test exactly without re-encoding. Treat as READ-ONLY: the
58
+ * array belongs to the caller (and may be a view into the query).
59
+ *
60
+ * Costs nothing when nothing inspects: items exist only while a rationale
61
+ * sink is attached, and this holds a reference rather than a copy. */
62
+ bytes?: Uint8Array;
52
63
  }
53
64
 
54
65
  /** A single completed act of inference — one mechanism, run once.
@@ -101,6 +112,20 @@ export function decodeText(bytes: Uint8Array): string {
101
112
  return new TextDecoder().decode(bytes.filter((b) => b !== 0x00));
102
113
  }
103
114
 
115
+ /** The BYTE COUNT of the complement — what the currency calls `unaccounted`
116
+ * in `weight = moves + PASS·unaccounted`. It lives here, beside the function
117
+ * that produces the gaps, because the price's second term has ONE definition:
118
+ * this was four copies of the same `reduce` (two in reasoning.ts, two in
119
+ * pipeline.ts) before the architecture audit of `../auditoria-arquitectura-sema.md`
120
+ * collapsed them. Same value at every site — the control diff is identical. */
121
+ export function unaccountedBytes(
122
+ spans: ReadonlyArray<readonly [number, number]>,
123
+ ): number {
124
+ let total = 0;
125
+ for (const [a, b] of spans) total += b - a;
126
+ return total;
127
+ }
128
+
104
129
  /** The `[start, end)` gaps of `[0, queryLen)` NOT covered by `accounted` —
105
130
  * the same union-of-spans reading think's grounding decider prices at PASS
106
131
  * per byte, exposed here so a mechanism can turn it into a human label. */
@@ -264,7 +289,16 @@ export class Rationale {
264
289
  };
265
290
  }
266
291
 
267
- /** Record a mechanism that has no sub-steps — its inputs and outputs are both
292
+ /** WHY THIS NAME IS A FREE STRING, when the derivation's moves are a closed
293
+ * union: a mechanism name is WRITTEN and DISPLAYED, and it COMPOSES with
294
+ * the nesting — `mechanism` is the whole path (`["respond", "think",
295
+ * "recognise"]`), which no fixed union can express. Nothing branches on it:
296
+ * `nothing here drives the inference; it only WITNESSES it`. A vocabulary
297
+ * that is only witnessed needs no union; one that is read does
298
+ * (`DerivationMove`, in graph-search.ts). The asymmetry is the design, not
299
+ * a drift.
300
+ *
301
+ * Record a mechanism that has no sub-steps — its inputs and outputs are both
268
302
  * known at the call site. Returns its index, for a later step to depend on. */
269
303
  step(
270
304
  name: string,
@@ -8,10 +8,10 @@ import { bytesEqual, indexOf } from "../bytes.js";
8
8
  import type { Attention, MindContext } from "./types.js";
9
9
  import { resolve } from "./primitives.js";
10
10
  import { corpusN, hubBound } from "./traverse.js";
11
- import { follow, haloSiblings, project } from "./match.js";
11
+ import { containsSpan, follow, haloSiblings, project } from "./match.js";
12
12
  import { joinWithBridge, pivotInto } from "./resonance.js";
13
13
  import type { Precomputed } from "./pipeline-mechanism.js";
14
- import type { Rationale } from "./rationale.js";
14
+ import { type Rationale, unaccountedBytes } from "./rationale.js";
15
15
 
16
16
  /** Whether `bytes` is a proper byte-subspan of `query` — already present in
17
17
  * the question, so voicing it back only restates part of what was asked,
@@ -29,13 +29,32 @@ export function restatesQuery(query: Uint8Array, bytes: Uint8Array): boolean {
29
29
 
30
30
  /** Extend a grounded answer forward across facts (multi-hop reasoning).
31
31
  * Pivots on the longest unconsumed learnt context each answer contains,
32
- * then follows the pivot's continuation to the next fact. Repeats up
33
- * to `cfg.recallQueryK` hops. `preConsumed` carries node ids already
32
+ * then follows the pivot's continuation to the next fact. **The chain ends
33
+ * when it STOPS, never when a count runs out**: every exit is a refusal (no
34
+ * pivot, no forward step, no question material carried) and the walk is bounded
35
+ * by the material and the graph — `consumed` refuses to revisit a node. There
36
+ * is no hop allowance, so this doc deliberately names no `cfg` capacity: the
37
+ * cover prices every hop at `STEP` and lets the search decide the depth, and a
38
+ * second count here would be a second decision about the same thing.
39
+ * `preConsumed` carries node ids already
34
40
  * spoken for by the grounding stage (cover/extract/CAST). `voiced` carries
35
41
  * the BYTES of the anchors a mechanism declared it voiced (its `used` set),
36
42
  * when it declared one — see the pivot's own containment rule. `pre` is the
37
43
  * response's shared pre-computation — the post-grounding stages read the
38
44
  * same container the mechanisms did. */
45
+ /** What the multi-hop extension produced, and what it cost: the bytes (the
46
+ * answer), the spans of the grounding's UNCOVERED material that each step was
47
+ * justified by, and how many steps were taken. The last two exist so the
48
+ * caller can price the extension in the ladder's own currency — `steps · STEP`
49
+ * against `PASS · unaccounted` — instead of taking it unconditionally. Both
50
+ * are FACTS, not verdicts: nothing here says whether the extension was worth
51
+ * it; that is the comparison's job, one layer up. */
52
+ export interface ReasonedAnswer {
53
+ bytes: Uint8Array;
54
+ carried: Array<[number, number]>;
55
+ steps: number;
56
+ }
57
+
39
58
  export async function reason(
40
59
  ctx: MindContext,
41
60
  query: Uint8Array,
@@ -47,14 +66,16 @@ export async function reason(
47
66
  * `unaccounted` spans. Only the reasoner's OWN extensions are judged
48
67
  * against it; a mechanism carrying its own `used` set owns its shape. */
49
68
  uncovered: readonly (readonly [number, number])[] = [],
50
- ): Promise<Uint8Array> {
69
+ ): Promise<ReasonedAnswer> {
51
70
  // Echo guard: a query that is ITSELF a learnt continuation (some context's
52
71
  // answer) is being asked back at the system — hopping forward from it would
53
72
  // chain through the very fact that produced it and echo the conversation
54
73
  // back. The grounded answer alone is the honest read-out. Deliberately a
55
74
  // broad structural gate; pinned by test/31-audit.
56
75
  const qId = pre.queryResolved;
57
- if (qId !== null && ctx.store.prevCount(qId) > 0) return answer;
76
+ if (qId !== null && ctx.store.prevCount(qId) > 0) {
77
+ return { bytes: answer, carried: [], steps: 0 };
78
+ }
58
79
 
59
80
  // Consume a node and its neighbours for pivot-cycle prevention — CAPPED at
60
81
  // the hub bound, via the store's LIMITed edge reads: a common continuation's
@@ -113,7 +134,7 @@ export async function reason(
113
134
  ? null
114
135
  : ctx.store.prevFirst(groundedId, bound);
115
136
  if (qId !== null && groundedPrev !== null && groundedPrev.includes(qId)) {
116
- return answer;
137
+ return { bytes: answer, carried: [], steps: 0 };
117
138
  }
118
139
 
119
140
  const consumed = new Set<number>();
@@ -159,7 +180,27 @@ export async function reason(
159
180
  const qv = pre.guide; // the response-wide guide IS the query's gist
160
181
  let t: ReturnType<Rationale["enter"]> | undefined;
161
182
  const startedFrom = answer;
162
- for (let hop = 0; hop < ctx.cfg.recallQueryK; hop++) {
183
+ // INSTRUMENTATION ONLY — the two facts the extension's own decision already
184
+ // used and threw away: the spans of uncovered material each step was
185
+ // JUSTIFIED by (the gate below computes which span carries it and kept only a
186
+ // boolean), and how many steps were taken. Nothing here decides anything:
187
+ // both are read after the loop, to bump counters and to let the caller compare
188
+ // the extension's cost against what it explains, in the ladder's own currency.
189
+ const carried: Array<[number, number]> = [];
190
+ let steps = 0;
191
+ // NO ALLOWANCE: THE CHAIN ENDS WHEN IT STOPS. Every exit below is the law —
192
+ // no pivot, no forward step, no question material carried — and the walk is
193
+ // bounded by the material and the graph rather than by a count: each taken
194
+ // step must carry a W-window of the uncovered material (finite), and
195
+ // `consumed` refuses to revisit a node. `recallQueryK` no longer bounds the
196
+ // reasoner here; it keeps its other roles (the bridge's candidate reads, the
197
+ // pivot's probe budget, the resonance limits).
198
+ //
199
+ // Measured before removing it: test/89 — the corpus-cost guard, the heaviest
200
+ // case in the suite — is green and no slower without the allowance (27 s
201
+ // against 30 s); and raising it from 12 to 200 changed neither the answer nor
202
+ // `pivotSteps` on the chain fixtures.
203
+ for (let hop = 0;; hop++) {
163
204
  // Hop 0's `cur` IS `answer`, so the guard above already resolved it and
164
205
  // read its reverse edges — reuse both rather than repeat them.
165
206
  const curId = hop === 0 ? groundedId : resolve(ctx, cur);
@@ -192,6 +233,7 @@ export async function reason(
192
233
  "the answer is itself a learnt fact — follow its continuation to the fixpoint",
193
234
  );
194
235
  cur = fwd;
236
+ steps++;
195
237
  continue;
196
238
  }
197
239
  }
@@ -227,12 +269,17 @@ export async function reason(
227
269
  if (!producerOwnsShape && uncovered.length > 0) {
228
270
  const W = ctx.space.maxGroup;
229
271
  let progress = false;
272
+ let justified: [number, number] | undefined;
230
273
  for (const [a, b] of uncovered) {
231
274
  for (let i = a; i + W <= b && !progress; i++) {
232
- if (indexOf(fc, query.subarray(i, i + W), 0) >= 0) progress = true;
275
+ if (indexOf(fc, query.subarray(i, i + W), 0) >= 0) {
276
+ progress = true;
277
+ justified = [a, b];
278
+ }
233
279
  }
234
280
  if (progress) break;
235
281
  }
282
+ if (progress && justified !== undefined) carried.push(justified);
236
283
  if (!progress) {
237
284
  // THE BRAKE, MADE VISIBLE. The reasoner declines a step that carries
238
285
  // none of the material the grounding left uncovered — the drift the
@@ -243,7 +290,25 @@ export async function reason(
243
290
  // the check disabled, test/110 and test/116 fail — so this brake is the
244
291
  // only thing keeping the extension honest until the pivot reports its
245
292
  // own accounted spans and the ladder can judge it instead.
246
- const left = uncovered.reduce((n, [a, b]) => n + (b - a), 0);
293
+ //
294
+ // THE PROMISE IS NOW KEPT, AND THE BRAKE TURNS OUT TO BE THE LADDER'S
295
+ // OWN CONSEQUENCE. The extension reports what it carried (`carried`,
296
+ // the span each step was justified by) and what it cost (`steps`), both
297
+ // counted in the meter (`reasonCarriedBytes`, `reasonSteps`), so the
298
+ // ladder CAN judge it: it accepts while
299
+ //
300
+ // steps · STEP < PASS · carried
301
+ //
302
+ // and this brake accepts whenever the step carries a `W`-window, i.e.
303
+ // whenever `carried ≥ W ≥ 1`. With `PASS/STEP = 1000` the two therefore
304
+ // agree on every extension with `steps ≤ 1000 · carried` — and every
305
+ // extension this repository produces takes 0 or 1 steps (measured on
306
+ // chains of 3, 8, 20 and 40 links). Above that bound the ladder would
307
+ // refuse what this brake accepts, which is the corner named in the
308
+ // closure report's limits: a chain of thousands of links explaining a
309
+ // handful of bytes. No guard is added for it — a limit without a
310
+ // derivation is exactly what the brake must not become.
311
+ const left = unaccountedBytes(uncovered);
247
312
  ctx.trace?.step(
248
313
  "pivotRefused",
249
314
  [rItem(cur, "answer"), rItem(query, "query")],
@@ -263,12 +328,25 @@ export async function reason(
263
328
  "pivot on the shared span this answer contains, then step forward across that fact",
264
329
  );
265
330
  cur = fc;
331
+ steps++;
332
+ }
333
+ // INSTRUMENTATION ONLY — the extension's two facts, untraced (meter.ts
334
+ // contract 1: a counter never reaches a decision). They are what a caller
335
+ // needs to PRICE the extension instead of taking it unconditionally: the work
336
+ // it did (`steps · STEP`) and the uncovered material it carried.
337
+ if (ctx.meter) {
338
+ ctx.meter.reasonSteps += steps;
339
+ ctx.meter.reasonCarriedBytes += unaccountedBytes(carried);
266
340
  }
267
341
  t?.done(
268
342
  [rItem(cur, "answer", resolve(ctx, cur) ?? undefined)],
343
+ // A FIXPOINT: no further step was possible. This note used to also cover an
344
+ // exhausted hop allowance — a different fact, and the reason F1 added a
345
+ // counter for it. The allowance is gone, so the only way out of the loop is
346
+ // a refusal, and the note is true again by construction.
269
347
  "the multi-hop chain's fixpoint",
270
348
  );
271
- return cur;
349
+ return { bytes: cur, carried, steps };
272
350
  }
273
351
 
274
352
  /** Fuse independent points of attention into one answer (multi-topic).
@@ -462,9 +540,8 @@ export async function fuseAttention(
462
540
  [rItem(out, "answer", resolve(ctx, out) ?? undefined)],
463
541
  `fused ${pieces.length} independent points of attention into one answer`,
464
542
  );
543
+ // THE FACT IS THE FUSED ANSWER, not the call: every early return above hands
544
+ // back `primary` untouched. Untraced on purpose (meter.ts contract 1).
545
+ if (ctx.meter) ctx.meter.fuseRuns++;
465
546
  return out;
466
547
  }
467
-
468
- // (resonance.js is already a static dependency above — `bridge` — so the old
469
- // dynamic import of pivotInto guarded against a cycle that does not exist.)
470
- import { containsSpan } from "./match.js";