@hviana/sema 0.8.1 → 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 (114) hide show
  1. package/AGENTS.md +29 -29
  2. package/TRADEMARKS.md +0 -1
  3. package/dist/src/config.d.ts +28 -0
  4. package/dist/src/config.js +20 -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 +76 -0
  8. package/dist/src/meter.js +95 -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/corpus.d.ts +40 -0
  14. package/dist/src/mind/corpus.js +149 -0
  15. package/dist/src/mind/graph-search.d.ts +7 -0
  16. package/dist/src/mind/graph-search.js +254 -24
  17. package/dist/src/mind/index.d.ts +3 -1
  18. package/dist/src/mind/index.js +1 -0
  19. package/dist/src/mind/match.d.ts +9 -4
  20. package/dist/src/mind/match.js +147 -61
  21. package/dist/src/mind/mechanisms/cast.js +19 -3
  22. package/dist/src/mind/mechanisms/confluence.js +24 -0
  23. package/dist/src/mind/mechanisms/cover.js +6 -0
  24. package/dist/src/mind/mechanisms/recall.js +32 -4
  25. package/dist/src/mind/mind.d.ts +57 -0
  26. package/dist/src/mind/mind.js +72 -1
  27. package/dist/src/mind/pipeline-mechanism.d.ts +7 -0
  28. package/dist/src/mind/pipeline.js +66 -20
  29. package/dist/src/mind/primitives.js +9 -1
  30. package/dist/src/mind/rationale.d.ts +28 -1
  31. package/dist/src/mind/rationale.js +22 -1
  32. package/dist/src/mind/reasoning.d.ts +25 -3
  33. package/dist/src/mind/reasoning.js +125 -20
  34. package/dist/src/mind/recognition.js +4 -8
  35. package/dist/src/mind/resonance.js +20 -1
  36. package/dist/src/mind/trace.js +1 -0
  37. package/dist/src/mind/traverse.js +15 -3
  38. package/dist/src/mind/types.d.ts +49 -4
  39. package/docs/INVARIANTS.md +2 -2
  40. package/docs/architecture/bounded-reads.md +1 -1
  41. package/docs/architecture/commonality.md +2 -2
  42. package/docs/architecture/cost-model.md +2 -2
  43. package/docs/architecture/determinism.md +7 -7
  44. package/docs/architecture/match-project.md +2 -3
  45. package/docs/architecture/mechanism-market.md +10 -10
  46. package/docs/architecture/meter.md +5 -5
  47. package/docs/architecture/store.md +3 -3
  48. package/docs/failures/tempting-but-wrong.md +34 -6
  49. package/docs/harness/gates.md +2 -2
  50. package/docs/mechanisms/cast.md +2 -2
  51. package/docs/mechanisms/cover.md +2 -3
  52. package/docs/mechanisms/extraction.md +7 -7
  53. package/docs/mechanisms/recall.md +8 -9
  54. package/jsr.json +1 -1
  55. package/package.json +1 -1
  56. package/src/alu/README.md +11 -12
  57. package/src/config.ts +48 -0
  58. package/src/geometry.ts +21 -0
  59. package/src/meter.ts +98 -0
  60. package/src/mind/attention.ts +167 -16
  61. package/src/mind/canonical.ts +43 -0
  62. package/src/mind/corpus.ts +202 -0
  63. package/src/mind/graph-search.ts +277 -23
  64. package/src/mind/index.ts +8 -1
  65. package/src/mind/match.ts +148 -57
  66. package/src/mind/mechanisms/cast.ts +20 -2
  67. package/src/mind/mechanisms/confluence.ts +24 -0
  68. package/src/mind/mechanisms/cover.ts +5 -0
  69. package/src/mind/mechanisms/recall.ts +32 -4
  70. package/src/mind/mind.ts +125 -0
  71. package/src/mind/pipeline-mechanism.ts +7 -0
  72. package/src/mind/pipeline.ts +79 -22
  73. package/src/mind/primitives.ts +9 -1
  74. package/src/mind/rationale.ts +35 -1
  75. package/src/mind/reasoning.ts +145 -13
  76. package/src/mind/recognition.ts +4 -8
  77. package/src/mind/resonance.ts +19 -1
  78. package/src/mind/trace.ts +1 -0
  79. package/src/mind/traverse.ts +16 -6
  80. package/src/mind/types.ts +53 -4
  81. package/test/100-complete-grounding-trace.test.mjs +109 -0
  82. package/test/101-alignment-gap-bound.test.mjs +106 -0
  83. package/test/102-production-composes-at-scale.test.mjs +110 -0
  84. package/test/103-alignment-gap-budget.test.mjs +89 -0
  85. package/test/104-composition-is-reported.test.mjs +90 -0
  86. package/test/105-derive-through-reports-its-refusal.test.mjs +137 -0
  87. package/test/106-the-join-fires.test.mjs +94 -0
  88. package/test/107-the-join-is-counted.test.mjs +81 -0
  89. package/test/108-the-join-chains.test.mjs +78 -0
  90. package/test/109-the-pivot-is-counted.test.mjs +60 -0
  91. package/test/110-the-reasoner-stops-when-the-question-is-answered.test.mjs +91 -0
  92. package/test/111-the-cover-assembly-is-counted.test.mjs +74 -0
  93. package/test/112-the-exploration-does-not-grow-with-the-hub.test.mjs +89 -0
  94. package/test/113-the-rationale-payload-is-bounded.test.mjs +84 -0
  95. package/test/114-alignment-budget-is-per-sweep.test.mjs +93 -0
  96. package/test/116-the-extension-is-gated-by-the-pipelines-own-remainder.test.mjs +100 -0
  97. package/test/117-corpus-search.test.mjs +171 -0
  98. package/test/118-the-join-reaches-a-key-off-the-cut.test.mjs +74 -0
  99. package/test/119-the-work-does-not-grow-with-the-corpus.test.mjs +122 -0
  100. package/test/120-composition-is-consequence.test.mjs +132 -0
  101. package/test/121-the-extension-does-not-grow-with-the-corpus.test.mjs +128 -0
  102. package/test/122-the-climb-search-does-not-grow-with-the-corpus.test.mjs +117 -0
  103. package/test/123-the-paired-formulas-agree.test.mjs +90 -0
  104. package/test/125-the-post-grounding-branch-publishes-its-operand.test.mjs +51 -0
  105. package/test/126-the-pipeline-does-not-name-mechanisms.test.mjs +42 -0
  106. package/test/128-the-leads-somewhere-pair-agrees.test.mjs +83 -0
  107. package/test/129-the-trace-payload-shape.test.mjs +164 -0
  108. package/test/14-scaling.test.mjs +10 -7
  109. package/test/32-confluence.test.mjs +68 -0
  110. package/test/38-reason-restate-guard.test.mjs +8 -2
  111. package/test/43-cast-analog-seat.test.mjs +10 -0
  112. package/test/55-cost-meter.test.mjs +859 -0
  113. package/test/76-reference-binding.test.mjs +6 -1
  114. package/test/89-completion-recursion.test.mjs +30 -5
@@ -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,17 +536,76 @@ 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
- : [];
548
- const reasoned = decided.complete ? answer : meter
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
+ );
563
+ // REPORTABLE, NOT SILENT. A declared-complete grounding ends the derivation
564
+ // here, and that decision is part of the derivation's shape: the reader of a
565
+ // rationale must be able to see that the chain stopped because the mechanism
566
+ // claimed the query WAS the context, not because nothing followed. The step
567
+ // carries the claim, not a re-description of the answer — the extension is
568
+ // skipped, so there is no output item to show.
569
+ if (decided.complete) {
570
+ ctx.trace?.step(
571
+ "completeGrounding",
572
+ [rItem(answer, provenance)],
573
+ [],
574
+ "grounding declared complete — the query IS the context, so " +
575
+ "post-grounding extension is skipped",
576
+ );
577
+ }
578
+ // THE REASONER JUDGES ITS OWN EXTENSIONS BY THE PIPELINE'S REMAINDER, not by
579
+ // the ladder's `accounted` — and by the SAME reading the fuse gate below uses,
580
+ // with the same W floor. `accounted` is a COST quantity (measured: a query
581
+ // fully explained by one computed span plus bridged connectors reports
582
+ // `accounted: []` while nothing is unexplained), and a remainder under one
583
+ // river-fold quantum is bridging punctuation, never a second topic — so it
584
+ // licenses no extension and blocks none.
585
+ const explained: Array<[number, number]> = [
586
+ ...decided.accounted,
587
+ ...pre.computed.map((u): [number, number] => [u.i, u.j]),
588
+ ];
589
+ const uncovered = unexplainedSpans(query.length, explained)
590
+ .filter(([a, b]) => b - a >= ctx.space.maxGroup);
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
549
603
  ? await meter.time(
550
604
  "reason",
551
- () => reason(ctx, query, answer, preConsumed, pre, voiced),
605
+ () => reason(ctx, query, answer, preConsumed, pre, voiced, uncovered),
552
606
  )
553
- : await reason(ctx, query, answer, preConsumed, pre, voiced);
607
+ : await reason(ctx, query, answer, preConsumed, pre, voiced, uncovered);
608
+ const reasoned = extension?.bytes ?? answer;
554
609
 
555
610
  // Fuse only when the query has a genuine REMAINDER no mechanism's
556
611
  // structural evidence touched at all. `decided.accounted` alone
@@ -568,10 +623,6 @@ export async function think(
568
623
  // observed: a single space between two fully-computed arithmetic spans
569
624
  // ("2+2 3+3") registered as "unaccounted" and pulled in an unrelated
570
625
  // corpus fact, corrupting "4 6" into "4 63".
571
- const explained: Array<[number, number]> = [
572
- ...decided.accounted,
573
- ...pre.computed.map((u): [number, number] => [u.i, u.j]),
574
- ];
575
626
  const remainder = unaccounted(explained);
576
627
  // Whether the winning candidate's entire recognised substance is
577
628
  // COMPUTED — every accounted span exactly a pre.computed span, nothing
@@ -615,7 +666,13 @@ export async function think(
615
666
 
616
667
  done(
617
668
  fused,
618
- "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",
619
676
  );
620
677
  return { bytes: fused, provenance };
621
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,
@@ -43,14 +62,20 @@ export async function reason(
43
62
  preConsumed: ReadonlySet<number>,
44
63
  pre: Precomputed,
45
64
  voiced: readonly Uint8Array[] = [],
46
- ): Promise<Uint8Array> {
65
+ /** The query material the GROUNDING left uncovered — the cost ladder's own
66
+ * `unaccounted` spans. Only the reasoner's OWN extensions are judged
67
+ * against it; a mechanism carrying its own `used` set owns its shape. */
68
+ uncovered: readonly (readonly [number, number])[] = [],
69
+ ): Promise<ReasonedAnswer> {
47
70
  // Echo guard: a query that is ITSELF a learnt continuation (some context's
48
71
  // answer) is being asked back at the system — hopping forward from it would
49
72
  // chain through the very fact that produced it and echo the conversation
50
73
  // back. The grounded answer alone is the honest read-out. Deliberately a
51
74
  // broad structural gate; pinned by test/31-audit.
52
75
  const qId = pre.queryResolved;
53
- 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
+ }
54
79
 
55
80
  // Consume a node and its neighbours for pivot-cycle prevention — CAPPED at
56
81
  // the hub bound, via the store's LIMITed edge reads: a common continuation's
@@ -109,7 +134,7 @@ export async function reason(
109
134
  ? null
110
135
  : ctx.store.prevFirst(groundedId, bound);
111
136
  if (qId !== null && groundedPrev !== null && groundedPrev.includes(qId)) {
112
- return answer;
137
+ return { bytes: answer, carried: [], steps: 0 };
113
138
  }
114
139
 
115
140
  const consumed = new Set<number>();
@@ -155,7 +180,27 @@ export async function reason(
155
180
  const qv = pre.guide; // the response-wide guide IS the query's gist
156
181
  let t: ReturnType<Rationale["enter"]> | undefined;
157
182
  const startedFrom = answer;
158
- 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++) {
159
204
  // Hop 0's `cur` IS `answer`, so the guard above already resolved it and
160
205
  // read its reverse edges — reuse both rather than repeat them.
161
206
  const curId = hop === 0 ? groundedId : resolve(ctx, cur);
@@ -188,6 +233,7 @@ export async function reason(
188
233
  "the answer is itself a learnt fact — follow its continuation to the fixpoint",
189
234
  );
190
235
  cur = fwd;
236
+ steps++;
191
237
  continue;
192
238
  }
193
239
  }
@@ -200,6 +246,80 @@ export async function reason(
200
246
  const fc = await follow(ctx, pivot, qv);
201
247
  consumeAll(pivot);
202
248
  if (fc === null || bytesEqual(fc, cur) || restatesQuery(query, fc)) break;
249
+ // WHOSE EXTENSION IS THIS?
250
+ //
251
+ // `voiced` is what the mechanism WITHHELD (the pipeline sends the used
252
+ // anchors' CONTINUATIONS, not their bytes — see pipeline's own note), so a
253
+ // non-empty `voiced` means exactly what that note says: the grounding came
254
+ // from a mechanism that carries its own short `used` set (cast/join) and
255
+ // therefore owns the shape of its answer. The further terms inside such a
256
+ // seat are legitimately followable — test/29 C3's `Mona Lisa` lives inside
257
+ // the voiced seat and leads on to a fact about neither analog.
258
+ //
259
+ // Every other grounding is ordinary, and an extension of it is the
260
+ // reasoner's own inference: it is taken only while question material the
261
+ // grounding left uncovered remains AND the step carries some of it, judged
262
+ // by the mind's own line between chance and evidence — one W-byte window,
263
+ // no word notion, no character class, no threshold. Measured: the drift's
264
+ // second step (`the Eiffel Tower is in Paris` after `Paris is famous for
265
+ // the Eiffel Tower`) carries no window of `" famous for"` and is refused,
266
+ // while the first carries it. Terminates by a real argument: the uncovered
267
+ // material is finite and each taken extension must carry some of it.
268
+ const producerOwnsShape = voiced.length > 0;
269
+ if (!producerOwnsShape && uncovered.length > 0) {
270
+ const W = ctx.space.maxGroup;
271
+ let progress = false;
272
+ let justified: [number, number] | undefined;
273
+ for (const [a, b] of uncovered) {
274
+ for (let i = a; i + W <= b && !progress; i++) {
275
+ if (indexOf(fc, query.subarray(i, i + W), 0) >= 0) {
276
+ progress = true;
277
+ justified = [a, b];
278
+ }
279
+ }
280
+ if (progress) break;
281
+ }
282
+ if (progress && justified !== undefined) carried.push(justified);
283
+ if (!progress) {
284
+ // THE BRAKE, MADE VISIBLE. The reasoner declines a step that carries
285
+ // none of the material the grounding left uncovered — the drift the
286
+ // extension tests pin. A refusal that leaves no trace is the kind of
287
+ // silent cut AGENTS §6 forbids: the rationale is where a reader learns
288
+ // that an extension was declined for want of question material, and
289
+ // where the next person sees why the chain stopped here. Measured with
290
+ // the check disabled, test/110 and test/116 fail — so this brake is the
291
+ // only thing keeping the extension honest until the pivot reports its
292
+ // own accounted spans and the ladder can judge it instead.
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);
312
+ ctx.trace?.step(
313
+ "pivotRefused",
314
+ [rItem(cur, "answer"), rItem(query, "query")],
315
+ uncovered.map(([a, b]) => rItem(query.subarray(a, b), "uncovered")),
316
+ `the step carries none of the question material the grounding left ` +
317
+ `uncovered (${left} byte(s) in ${uncovered.length} span(s)) — refused`,
318
+ );
319
+ break;
320
+ }
321
+ }
322
+ if (ctx.meter) ctx.meter.pivotSteps++;
203
323
  t ??= ctx.trace?.enter("reason", [rItem(startedFrom, "grounded")]);
204
324
  ctx.trace?.step(
205
325
  "pivotStep",
@@ -208,12 +328,25 @@ export async function reason(
208
328
  "pivot on the shared span this answer contains, then step forward across that fact",
209
329
  );
210
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);
211
340
  }
212
341
  t?.done(
213
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.
214
347
  "the multi-hop chain's fixpoint",
215
348
  );
216
- return cur;
349
+ return { bytes: cur, carried, steps };
217
350
  }
218
351
 
219
352
  /** Fuse independent points of attention into one answer (multi-topic).
@@ -407,9 +540,8 @@ export async function fuseAttention(
407
540
  [rItem(out, "answer", resolve(ctx, out) ?? undefined)],
408
541
  `fused ${pieces.length} independent points of attention into one answer`,
409
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++;
410
546
  return out;
411
547
  }
412
-
413
- // (resonance.js is already a static dependency above — `bridge` — so the old
414
- // dynamic import of pivotInto guarded against a cycle that does not exist.)
415
- import { containsSpan } from "./match.js";
@@ -535,7 +535,10 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
535
535
  // emitted, so a caller can retry a trimmed edge on the miss path only.
536
536
  if (end - start < W) return false;
537
537
  if (flatProbe(start, end) === null) {
538
- if (!canonBudget) return false;
538
+ if (!canonBudget) {
539
+ if (ctx.meter) ctx.meter.canonProbesDenied++;
540
+ return false;
541
+ }
539
542
  if (!canonAdmits(start, end)) return false;
540
543
  }
541
544
  const id = resolveSpan(start, end);
@@ -556,13 +559,6 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
556
559
  // endpoints in order and running out partway along the query. A form
557
560
  // longer than that is out of this tier's reach — but so is a form the
558
561
  // chain cannot span, and that is exactly the trade the budget prices.
559
- // The factor is chainReach(W), the same W² scale the chain already
560
- // trusts; no new constant.
561
- // The factor is chainReach(W) — the same W² scale the chain itself
562
- // trusts — so the cap is derived from the fold's geometry, never tuned.
563
- // (It was briefly an environment variable while the cost was being
564
- // measured; an env-read here would make inference non-reproducible,
565
- // which the determinism contract forbids outright.)
566
562
  // The factor is chainReach(W) — the same W² scale the chain itself
567
563
  // trusts — so the cap is derived from the fold's geometry, never tuned.
568
564
  // (It was briefly an environment variable while the cost was being
@@ -329,7 +329,14 @@ export async function pivotInto(
329
329
  consumed: ReadonlySet<number>,
330
330
  voiced: readonly Uint8Array[] = [],
331
331
  ): Promise<number | null> {
332
- const k = ctx.cfg.recallQueryK;
332
+ // The pivot's OWN shortlist capacity — not `recallQueryK`: they are different
333
+ // quantities (a mechanical sweep's probe budget vs the bridge's candidate-read
334
+ // allowance), and one number serving both means tightening either silently
335
+ // starves the other. The reason is the duplication, NOT a measured strangle:
336
+ // an earlier version of this comment claimed "at recallQueryK 1 the pivot finds
337
+ // no pivot", and that was probed and is FALSE on test/23's fixture — the
338
+ // strangle is fixture-specific, so it is not the evidence for this split.
339
+ const k = ctx.cfg.pivotProbeK;
333
340
  // ONE perception of the answer, shared by the probe budget and the walk —
334
341
  // this used to fold the same bytes twice, back to back, on every hop.
335
342
  const tree = perceive(ctx, answer);
@@ -365,6 +372,17 @@ export async function pivotInto(
365
372
  }
366
373
  for (const c of n.kids) queue.push(c); // breadth-first: larger regions first
367
374
  }
375
+ // The sweep's two FACTS, untraced (meter.ts contract 1: a counter never
376
+ // reaches a decision). `probes` is the work done; the shortfall is the
377
+ // capacity the cap withheld — the branches the breadth-first order never got
378
+ // to. Whether that withheld anything that mattered is NOT said here: the
379
+ // order spends the largest regions first, and recognition below still
380
+ // contributes every exact containment candidate regardless of the budget.
381
+ if (ctx.meter) {
382
+ ctx.meter.pivotProbes += probes;
383
+ const unprobed = branchCount - probeCap;
384
+ if (unprobed > 0) ctx.meter.pivotBranchesUnprobed += unprobed;
385
+ }
368
386
  // THE FULL recognition, memo-shared with every other reader of these bytes.
369
387
  // A "skip the edge trims here" variant was refuted (see recognise's own
370
388
  // note): those trims are what find a WHOLE trained form embedded at an
package/src/mind/trace.ts CHANGED
@@ -18,6 +18,7 @@ export function rItem(
18
18
  ): RationaleItem {
19
19
  return {
20
20
  text: decodeText(bytes),
21
+ bytes,
21
22
  role,
22
23
  node: node ?? undefined,
23
24
  span,
@@ -10,6 +10,13 @@ import { cosine, Vec } from "../vec.js";
10
10
  import type { AncestorReach, MindContext, SaturationStop } from "./types.js";
11
11
  import { gistOf, read } from "./primitives.js";
12
12
  import { canonicalWindows, leafIdPrefix, leafIdRun } from "./canonical.js";
13
+ // Imported at the TOP, where every other import is. They used to sit 800 lines
14
+ // down under a note claiming the position mattered ("before trace module is
15
+ // loaded") — it does not: an ES module's static imports are HOISTED, so the
16
+ // file's line order never decides load order. The note described an intention
17
+ // the runtime does not honour; the imports move and the claim goes.
18
+ import { decodeText } from "./rationale.js";
19
+ import type { RationaleItem } from "./rationale.js";
13
20
 
14
21
  // ── Session structural memo ─────────────────────────────────────────────
15
22
  //
@@ -765,7 +772,15 @@ export function chooseNext(
765
772
  // for the prevCount calls in the loop above, never for extra rItemShort
766
773
  // byte-reads.
767
774
  if (ctx.trace) {
768
- const others = capped.filter((c) => c !== best);
775
+ // A BOUNDED SAMPLE, AND THE COUNT. The step used to carry EVERY candidate
776
+ // it weighed — measured on the trained store, 1559 out-items in one step
777
+ // (hubBound's own size) and 1082 in another (the hub's degree). The
778
+ // rationale's job is to explain the CHOICE, and the count is what says how
779
+ // wide the field was; the declared candidate budget (`recallQueryK`) is what
780
+ // bounds the sample, so no number is invented here.
781
+ const others = capped
782
+ .filter((c) => c !== best)
783
+ .slice(0, ctx.cfg.rationaleSampleK);
769
784
  ctx.trace.step(
770
785
  "disambiguate",
771
786
  [rItemShort(ctx, best, "halo-evidence", bestSupport)],
@@ -816,11 +831,6 @@ export function chooseAmong(
816
831
  : { id: candidates[0], score: -Infinity };
817
832
  }
818
833
 
819
- // ── Trace shim (used by chooseNext before trace module is loaded) ────────
820
-
821
- import { decodeText } from "./rationale.js";
822
- import type { RationaleItem } from "./rationale.js";
823
-
824
834
  function rItemShort(
825
835
  ctx: MindContext,
826
836
  id: number,
package/src/mind/types.ts CHANGED
@@ -65,12 +65,44 @@ export interface GraphSearchHost {
65
65
  starts: ReadonlySet<number>;
66
66
  };
67
67
  chooseNext?(node: number): number | undefined;
68
+ /** The lengths `p` for which `prefix ‖ tail[0..p]` IS A STORED NODE, ascending
69
+ * — the join's candidate set, in the tail's own coordinates. Optional: a host
70
+ * that cannot answer makes the join fall back to every prefix, which is exact
71
+ * and complete but pays a `resolve` per offset.
72
+ *
73
+ * WHY NOT THE FOLD'S CUTS. A key names a relation exactly when the
74
+ * concatenation is a node, and a node's end is the end of ITS OWN stream —
75
+ * where the fold never emits a cut (geometry's `emit` guards `at >= n`). So a
76
+ * key can end strictly inside the tail with no boundary anywhere near it:
77
+ * measured, "stockholm mayor" exists, leads on to the mayor fact, and its
78
+ * boundary 6 is in neither the tail's cuts ([4,7]) nor the concatenation's.
79
+ * The fold's boundaries are a SUBSET of the real ends, not a proxy for them,
80
+ * and using them skipped the shortest names first — which is a semantic law,
81
+ * not an optimisation (test/106, test/108 pin it). */
82
+ contentKeyEnds?(prefix: Uint8Array, tail: Uint8Array): readonly number[];
68
83
  /** The admission predicate — `traverse.ts`'s `leadsSomewhere`, its ONE
69
84
  * definition: does this node bear an edge or a halo? Optional, so a bare
70
85
  * host (a raw Store and nothing else) still works; when present, the search
71
86
  * uses it rather than re-probing the store, which keeps the predicate
72
87
  * single-defined AND memoised on the response-scoped struct cache. */
73
88
  leadsSomewhere?(id: number): boolean;
89
+ /** Report a SEARCH REFUSAL into the rationale — the channel AGENTS §6
90
+ * requires: a callback threaded through a call chain must FEED the
91
+ * rationale, the way `GraphSearch`'s `onDerivation` feeds `traceDerivation`,
92
+ * never a channel of its own. Optional, so a bare host stays silent rather
93
+ * than crashing. */
94
+ reportSearch?(
95
+ name: string,
96
+ parts: ReadonlyArray<Uint8Array>,
97
+ note: string,
98
+ ): void;
99
+ /** The CANONICAL resolver ({@link canonResolve}), optional like
100
+ * {@link leadsSomewhere}. The store's keys were written through the
101
+ * canonical fold, so a fact's `Gustaf Molander` and the deposited
102
+ * `gustaf molander` are the SAME node (measured inside a response: the
103
+ * canonical resolver maps the surface form to the deposited node while a raw
104
+ * resolve returns null). A bare host falls back to the plain probe. */
105
+ canonResolve?(bytes: Uint8Array): number | null;
74
106
  }
75
107
 
76
108
  // ═══════════════════════════════════════════════════════════════════════════
@@ -119,11 +151,28 @@ export interface Attention {
119
151
  * strength and its place.
120
152
  * `vote` is a sum over every region that agreed, so it grows with how many
121
153
  * places corroborated; `peak` is what the strongest one of them said on its
122
- * own. A consumer holding this point to consensusFloor(N) — a bar that
123
- * prices ONE region's maximally-discriminative evidence — must read `peak`,
124
- * not `vote`: six scaffolding regions summing past the floor is not the
125
- * same claim as one region clearing it. */
154
+ * own. THIS USED TO PRESCRIBE THE WRONG OPERAND. It read: "a consumer
155
+ * holding this point to consensusFloor(N) — a bar that prices ONE region's
156
+ * maximally-discriminative evidence — must read `peak`, not `vote`." The
157
+ * engine reads the POOLED vote, and thresholds.md §2 derives the floor for
158
+ * exactly that ("Pooled-vote significance floor": one maximally-specific
159
+ * region contributes at most ln N, and ln(N)+1/2 demands corroboration
160
+ * BEYOND one region). MEASURED across 27 anchors on 6 queries: all 11
161
+ * admissions cleared the floor by the sum and NONE by `peak` alone — a gate
162
+ * reading `peak` would refuse every root the engine elects. `peak` remains
163
+ * what it is: the strongest SINGLE region's contribution. */
126
164
  peak: number;
165
+ /** The IDF-WEIGHTED sum behind this point — the quantity `consensusFloor` is
166
+ * derived for, and therefore the one the floor gates must read. It is
167
+ * MODE-INDEPENDENT by construction (its per-region weight is
168
+ * `mutual · idf / roots`, never the mode-dependent `wf`), so gating on it
169
+ * makes an anchor's admission the same in `inverse`, `direct` and `combined`.
170
+ * In `inverse` — the only mode the engine runs — it equals `vote` exactly
171
+ * (measured, test/55 test 17), so nothing about today's verdicts changes.
172
+ * MEASURED before this field existed: gating on `vote` DID flip a verdict,
173
+ * anchor 87 of test/55's query (inverse 2.682 admitted, direct 1.468
174
+ * refused, floor 2.292). */
175
+ idfVote: number;
127
176
  /** SCALE-INVARIANT confidence: the fraction of the query's OWN regions
128
177
  * whose evidence this point accounts for (Σ RegionVote.absorbed among
129
178
  * its contributors, over the query's total region count) — read PER-