@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
@@ -12,7 +12,7 @@ import { PASS, STEP } from "./graph-search.js";
12
12
  import { gistOf, read, resolve } from "./primitives.js";
13
13
  import { recognise } from "./recognition.js";
14
14
  import { fuseAttention, reason } from "./reasoning.js";
15
- import { unexplainedSpans } from "./rationale.js";
15
+ import { unaccountedBytes, unexplainedSpans } from "./rationale.js";
16
16
  import { rItem } from "./trace.js";
17
17
  import { hubBound } from "./traverse.js";
18
18
  import { Precomputed } from "./pipeline-mechanism.js";
@@ -124,8 +124,7 @@ export async function think(ctx, query, mechs) {
124
124
  // (meter.md); the trace already represents structure.
125
125
  const pre = new Precomputed(ctx, query, rec, computed, ctx._edgeGuide);
126
126
  const grade = (w) => Math.floor(w / STEP);
127
- const unaccounted = (spans) => unexplainedSpans(query.length, spans)
128
- .reduce((sum, [s, e]) => sum + (e - s), 0);
127
+ const unaccounted = (spans) => unaccountedBytes(unexplainedSpans(query.length, spans));
129
128
  const weigh = (accounted, moves) => moves + PASS * unaccounted(accounted);
130
129
  const candidates = [];
131
130
  let best = null;
@@ -317,13 +316,10 @@ export async function think(ctx, query, mechs) {
317
316
  }
318
317
  const answer = decided.bytes;
319
318
  const provenance = decided.provenance;
320
- const castUsed = decided.used ?? new Set();
319
+ const declaredUsed = decided.used;
321
320
  // ── Post-grounding, gated by provenance ──────────────────────────────
322
- const preConsumed = provenance === "cast" || provenance === "join"
323
- ? castUsed
324
- : provenance === "recall" || provenance === "recall-echo"
325
- ? new Set()
326
- : new Set(recognise(ctx, answer).sites.map((s) => s.payload));
321
+ const preConsumed = declaredUsed ??
322
+ new Set(recognise(ctx, answer).sites.map((s) => s.payload));
327
323
  // A grounding that DECLARED itself complete is not extended: the answer is
328
324
  // already a trained form's own continuation, reached through an identity
329
325
  // claim about the query, so a multi-hop pivot could only chain past the
@@ -352,12 +348,59 @@ export async function think(ctx, query, mechs) {
352
348
  // `preConsumed` is derived by re-recognising the answer — "everything in
353
349
  // it", not "what it voiced" — and a containment rule over that would
354
350
  // suppress every pivot the answer legitimately contains.
355
- const voiced = (provenance === "cast" || provenance === "join")
356
- ? [...castUsed].flatMap((id) => ctx.store.nextFirst(id, hubBound(ctx)).map((n) => read(ctx, n)))
357
- : [];
358
- const reasoned = decided.complete ? answer : meter
359
- ? await meter.time("reason", () => reason(ctx, query, answer, preConsumed, pre, voiced))
360
- : await reason(ctx, query, answer, preConsumed, pre, voiced);
351
+ const voiced = declaredUsed === undefined ? [] : [...declaredUsed].flatMap((id) => ctx.store.nextFirst(id, hubBound(ctx)).map((n) => read(ctx, n)));
352
+ // WHAT THIS BRANCH READ, published where it was read. Post-grounding decides
353
+ // by `decided.used` and by the provenance NAME; the operands were invisible in
354
+ // the trace, so a change to the branching could not be shown equivalent or
355
+ // otherwise from outside — three separate investigations failed on exactly
356
+ // that gap. A gap in instrumentation is a defect IN the instrumentation
357
+ // (AGENTS.md §6): closed here, once, as counts only — never content.
358
+ ctx.trace?.step("postGrounding", [rItem(answer, provenance)], [], `used=${decided.used !== undefined ? "declared" : "absent"} · ` +
359
+ `preConsumed=${preConsumed.size} · voiced=${voiced.length}`, undefined, {
360
+ version: 1,
361
+ provenance,
362
+ usedDeclared: decided.used !== undefined,
363
+ preConsumed: preConsumed.size,
364
+ voiced: voiced.length,
365
+ });
366
+ // REPORTABLE, NOT SILENT. A declared-complete grounding ends the derivation
367
+ // here, and that decision is part of the derivation's shape: the reader of a
368
+ // rationale must be able to see that the chain stopped because the mechanism
369
+ // claimed the query WAS the context, not because nothing followed. The step
370
+ // carries the claim, not a re-description of the answer — the extension is
371
+ // skipped, so there is no output item to show.
372
+ if (decided.complete) {
373
+ ctx.trace?.step("completeGrounding", [rItem(answer, provenance)], [], "grounding declared complete — the query IS the context, so " +
374
+ "post-grounding extension is skipped");
375
+ }
376
+ // THE REASONER JUDGES ITS OWN EXTENSIONS BY THE PIPELINE'S REMAINDER, not by
377
+ // the ladder's `accounted` — and by the SAME reading the fuse gate below uses,
378
+ // with the same W floor. `accounted` is a COST quantity (measured: a query
379
+ // fully explained by one computed span plus bridged connectors reports
380
+ // `accounted: []` while nothing is unexplained), and a remainder under one
381
+ // river-fold quantum is bridging punctuation, never a second topic — so it
382
+ // licenses no extension and blocks none.
383
+ const explained = [
384
+ ...decided.accounted,
385
+ ...pre.computed.map((u) => [u.i, u.j]),
386
+ ];
387
+ const uncovered = unexplainedSpans(query.length, explained)
388
+ .filter(([a, b]) => b - a >= ctx.space.maxGroup);
389
+ // PUBLISHED, NOT RECOMPUTED: the same `uncovered` the gates below read. A
390
+ // write-only accounting (meter contract 1), so the number that licenses an
391
+ // extension or a fusion stops being invisible.
392
+ if (meter) {
393
+ meter.postGroundingRemainderSpans += uncovered.length;
394
+ meter.postGroundingRemainderBytes += unaccountedBytes(uncovered);
395
+ }
396
+ // The extension is kept as a WHOLE (bytes + what it carried + how many steps),
397
+ // not just its bytes: pricing it — `steps · STEP` against `PASS · unaccounted`
398
+ // — is the caller's job, one comparison away. `reasoned` stays the bytes so
399
+ // everything downstream is untouched.
400
+ const extension = decided.complete ? undefined : meter
401
+ ? await meter.time("reason", () => reason(ctx, query, answer, preConsumed, pre, voiced, uncovered))
402
+ : await reason(ctx, query, answer, preConsumed, pre, voiced, uncovered);
403
+ const reasoned = extension?.bytes ?? answer;
361
404
  // Fuse only when the query has a genuine REMAINDER no mechanism's
362
405
  // structural evidence touched at all. `decided.accounted` alone
363
406
  // undercounts this: it is a COST-LADDER quantity (cover.ts prices its
@@ -374,10 +417,6 @@ export async function think(ctx, query, mechs) {
374
417
  // observed: a single space between two fully-computed arithmetic spans
375
418
  // ("2+2 3+3") registered as "unaccounted" and pulled in an unrelated
376
419
  // corpus fact, corrupting "4 6" into "4 63".
377
- const explained = [
378
- ...decided.accounted,
379
- ...pre.computed.map((u) => [u.i, u.j]),
380
- ];
381
420
  const remainder = unaccounted(explained);
382
421
  // Whether the winning candidate's entire recognised substance is
383
422
  // COMPUTED — every accounted span exactly a pre.computed span, nothing
@@ -405,6 +444,13 @@ export async function think(ctx, query, mechs) {
405
444
  : meter
406
445
  ? await meter.time("fuse", () => fuseAttention(ctx, query, reasoned, pre, unclimbed, primarySpans))
407
446
  : await fuseAttention(ctx, query, reasoned, pre, unclimbed, primarySpans);
408
- done(fused, "grounded, reasoned forward, fused across points of attention");
447
+ done(fused,
448
+ // NO CLAIM ABOUT FUSION HERE. `fuseAttention` is entered whenever a
449
+ // remainder ≥ W exists and returns early when there is nothing to bridge, so
450
+ // this note used to assert a fusion that frequently did not happen (measured:
451
+ // "What is the capital of France famous for" fuses 0 times). The fusion is
452
+ // reported by `fuseAttention`'s own `done` when it happens — the layer that
453
+ // did the work is the layer that says so.
454
+ "grounded, reasoned forward");
409
455
  return { bytes: fused, provenance };
410
456
  }
@@ -297,7 +297,15 @@ export function canonResolve(ctx, bytes) {
297
297
  // on exactly the node the canonical-case query would have found.
298
298
  const folded = foldTree(ctx, perceive(ctx, bytesOf), 0).node;
299
299
  const use = folded ?? id;
300
- const leads = store.hasNext(use) || store.haloMass(use) > 0;
300
+ // THE ADMISSION PREDICATE, by its own pair of probes: `traverse.ts`'s
301
+ // `leadsSomewhere` is edge-or-halo, and `hasHalo` is the one that carries
302
+ // the mass bar (`mass >= minHaloMass`). Asking `haloMass(use) > 0` instead
303
+ // is the same answer only while `minHaloMass <= 1` (its default): raise the
304
+ // bar and this site would rank a node as leading on evidence the law
305
+ // refuses. Calling `leadsSomewhere` here is not possible — `traverse.ts`
306
+ // imports THIS file, so it would be a cycle — which is why the pair is
307
+ // spelled out rather than named.
308
+ const leads = store.hasNext(use) || store.hasHalo(use);
301
309
  if (best === null || (leads && !bestLeads) ||
302
310
  (leads === bestLeads && use < best)) {
303
311
  best = use;
@@ -26,6 +26,17 @@ export interface RationaleItem {
26
26
  * caller asked to carry it (off by default — a D-float array per item would
27
27
  * bury the reasoning it is meant to explain). */
28
28
  v?: Vec;
29
+ /** The element's OWN bytes, attached BY REFERENCE when the step was built from
30
+ * bytes (a `rationale.ts` item made from a node carries none: read it back
31
+ * through `node`). `text` is a RENDERING and cannot stand in for them — it
32
+ * decodes UTF-8 and DROPS NUL bytes, so a key containing one is unrecoverable
33
+ * from it, which is exactly how a join refusal (`deriveThroughMiss`) became
34
+ * impossible to test exactly without re-encoding. Treat as READ-ONLY: the
35
+ * array belongs to the caller (and may be a view into the query).
36
+ *
37
+ * Costs nothing when nothing inspects: items exist only while a rationale
38
+ * sink is attached, and this holds a reference rather than a copy. */
39
+ bytes?: Uint8Array;
29
40
  }
30
41
  /** A single completed act of inference — one mechanism, run once.
31
42
  *
@@ -72,6 +83,13 @@ export type InspectRationale = (step: RationaleStep) => void;
72
83
  /** Decode bytes to text for display, dropping the NUL padding the encoder uses
73
84
  * (the same cleanup {@link Mind.respondText} does for its result). */
74
85
  export declare function decodeText(bytes: Uint8Array): string;
86
+ /** The BYTE COUNT of the complement — what the currency calls `unaccounted`
87
+ * in `weight = moves + PASS·unaccounted`. It lives here, beside the function
88
+ * that produces the gaps, because the price's second term has ONE definition:
89
+ * this was four copies of the same `reduce` (two in reasoning.ts, two in
90
+ * pipeline.ts) before the architecture audit of `../auditoria-arquitectura-sema.md`
91
+ * collapsed them. Same value at every site — the control diff is identical. */
92
+ export declare function unaccountedBytes(spans: ReadonlyArray<readonly [number, number]>): number;
75
93
  /** The `[start, end)` gaps of `[0, queryLen)` NOT covered by `accounted` —
76
94
  * the same union-of-spans reading think's grounding decider prices at PASS
77
95
  * per byte, exposed here so a mechanism can turn it into a human label. */
@@ -133,7 +151,16 @@ export declare class Rationale {
133
151
  * now; the matching {@link Scope.done} supplies the outputs when it finishes.
134
152
  * `deps` overrides the default data-flow edge (previous sibling / parent). */
135
153
  enter(name: string, inputs: RationaleItem[], deps?: number[]): Scope;
136
- /** Record a mechanism that has no sub-steps — its inputs and outputs are both
154
+ /** WHY THIS NAME IS A FREE STRING, when the derivation's moves are a closed
155
+ * union: a mechanism name is WRITTEN and DISPLAYED, and it COMPOSES with
156
+ * the nesting — `mechanism` is the whole path (`["respond", "think",
157
+ * "recognise"]`), which no fixed union can express. Nothing branches on it:
158
+ * `nothing here drives the inference; it only WITNESSES it`. A vocabulary
159
+ * that is only witnessed needs no union; one that is read does
160
+ * (`DerivationMove`, in graph-search.ts). The asymmetry is the design, not
161
+ * a drift.
162
+ *
163
+ * Record a mechanism that has no sub-steps — its inputs and outputs are both
137
164
  * known at the call site. Returns its index, for a later step to depend on. */
138
165
  step(name: string, inputs: RationaleItem[], outputs: RationaleItem[], note?: string, deps?: number[], data?: unknown): number;
139
166
  }
@@ -24,6 +24,18 @@
24
24
  export function decodeText(bytes) {
25
25
  return new TextDecoder().decode(bytes.filter((b) => b !== 0x00));
26
26
  }
27
+ /** The BYTE COUNT of the complement — what the currency calls `unaccounted`
28
+ * in `weight = moves + PASS·unaccounted`. It lives here, beside the function
29
+ * that produces the gaps, because the price's second term has ONE definition:
30
+ * this was four copies of the same `reduce` (two in reasoning.ts, two in
31
+ * pipeline.ts) before the architecture audit of `../auditoria-arquitectura-sema.md`
32
+ * collapsed them. Same value at every site — the control diff is identical. */
33
+ export function unaccountedBytes(spans) {
34
+ let total = 0;
35
+ for (const [a, b] of spans)
36
+ total += b - a;
37
+ return total;
38
+ }
27
39
  /** The `[start, end)` gaps of `[0, queryLen)` NOT covered by `accounted` —
28
40
  * the same union-of-spans reading think's grounding decider prices at PASS
29
41
  * per byte, exposed here so a mechanism can turn it into a human label. */
@@ -151,7 +163,16 @@ export class Rationale {
151
163
  },
152
164
  };
153
165
  }
154
- /** Record a mechanism that has no sub-steps — its inputs and outputs are both
166
+ /** WHY THIS NAME IS A FREE STRING, when the derivation's moves are a closed
167
+ * union: a mechanism name is WRITTEN and DISPLAYED, and it COMPOSES with
168
+ * the nesting — `mechanism` is the whole path (`["respond", "think",
169
+ * "recognise"]`), which no fixed union can express. Nothing branches on it:
170
+ * `nothing here drives the inference; it only WITNESSES it`. A vocabulary
171
+ * that is only witnessed needs no union; one that is read does
172
+ * (`DerivationMove`, in graph-search.ts). The asymmetry is the design, not
173
+ * a drift.
174
+ *
175
+ * Record a mechanism that has no sub-steps — its inputs and outputs are both
155
176
  * known at the call site. Returns its index, for a later step to depend on. */
156
177
  step(name, inputs, outputs, note, deps, data) {
157
178
  const mechanism = this.path(name);
@@ -13,14 +13,36 @@ import type { Precomputed } from "./pipeline-mechanism.js";
13
13
  export declare function restatesQuery(query: Uint8Array, bytes: Uint8Array): boolean;
14
14
  /** Extend a grounded answer forward across facts (multi-hop reasoning).
15
15
  * Pivots on the longest unconsumed learnt context each answer contains,
16
- * then follows the pivot's continuation to the next fact. Repeats up
17
- * to `cfg.recallQueryK` hops. `preConsumed` carries node ids already
16
+ * then follows the pivot's continuation to the next fact. **The chain ends
17
+ * when it STOPS, never when a count runs out**: every exit is a refusal (no
18
+ * pivot, no forward step, no question material carried) and the walk is bounded
19
+ * by the material and the graph — `consumed` refuses to revisit a node. There
20
+ * is no hop allowance, so this doc deliberately names no `cfg` capacity: the
21
+ * cover prices every hop at `STEP` and lets the search decide the depth, and a
22
+ * second count here would be a second decision about the same thing.
23
+ * `preConsumed` carries node ids already
18
24
  * spoken for by the grounding stage (cover/extract/CAST). `voiced` carries
19
25
  * the BYTES of the anchors a mechanism declared it voiced (its `used` set),
20
26
  * when it declared one — see the pivot's own containment rule. `pre` is the
21
27
  * response's shared pre-computation — the post-grounding stages read the
22
28
  * same container the mechanisms did. */
23
- export declare function reason(ctx: MindContext, query: Uint8Array, answer: Uint8Array, preConsumed: ReadonlySet<number>, pre: Precomputed, voiced?: readonly Uint8Array[]): Promise<Uint8Array>;
29
+ /** What the multi-hop extension produced, and what it cost: the bytes (the
30
+ * answer), the spans of the grounding's UNCOVERED material that each step was
31
+ * justified by, and how many steps were taken. The last two exist so the
32
+ * caller can price the extension in the ladder's own currency — `steps · STEP`
33
+ * against `PASS · unaccounted` — instead of taking it unconditionally. Both
34
+ * are FACTS, not verdicts: nothing here says whether the extension was worth
35
+ * it; that is the comparison's job, one layer up. */
36
+ export interface ReasonedAnswer {
37
+ bytes: Uint8Array;
38
+ carried: Array<[number, number]>;
39
+ steps: number;
40
+ }
41
+ export declare function reason(ctx: MindContext, query: Uint8Array, answer: Uint8Array, preConsumed: ReadonlySet<number>, pre: Precomputed, voiced?: readonly Uint8Array[],
42
+ /** The query material the GROUNDING left uncovered — the cost ladder's own
43
+ * `unaccounted` spans. Only the reasoner's OWN extensions are judged
44
+ * against it; a mechanism carrying its own `used` set owns its shape. */
45
+ uncovered?: readonly (readonly [number, number])[]): Promise<ReasonedAnswer>;
24
46
  /** Fuse independent points of attention into one answer (multi-topic).
25
47
  * When the consensus climb finds more than one dominant point, each
26
48
  * independent point grounds its own answer; they are bridged together
@@ -6,8 +6,9 @@ import { rItem, rNode } from "./trace.js";
6
6
  import { bytesEqual, indexOf } from "../bytes.js";
7
7
  import { resolve } from "./primitives.js";
8
8
  import { hubBound } from "./traverse.js";
9
- import { follow, haloSiblings, project } from "./match.js";
9
+ import { containsSpan, follow, haloSiblings, project } from "./match.js";
10
10
  import { joinWithBridge, pivotInto } from "./resonance.js";
11
+ import { unaccountedBytes } from "./rationale.js";
11
12
  /** Whether `bytes` is a proper byte-subspan of `query` — already present in
12
13
  * the question, so voicing it back only restates part of what was asked,
13
14
  * never answers it. The exact guard recallByResonance already applies to
@@ -21,24 +22,20 @@ import { joinWithBridge, pivotInto } from "./resonance.js";
21
22
  export function restatesQuery(query, bytes) {
22
23
  return bytes.length < query.length && indexOf(query, bytes, 0) >= 0;
23
24
  }
24
- /** Extend a grounded answer forward across facts (multi-hop reasoning).
25
- * Pivots on the longest unconsumed learnt context each answer contains,
26
- * then follows the pivot's continuation to the next fact. Repeats up
27
- * to `cfg.recallQueryK` hops. `preConsumed` carries node ids already
28
- * spoken for by the grounding stage (cover/extract/CAST). `voiced` carries
29
- * the BYTES of the anchors a mechanism declared it voiced (its `used` set),
30
- * when it declared one — see the pivot's own containment rule. `pre` is the
31
- * response's shared pre-computation — the post-grounding stages read the
32
- * same container the mechanisms did. */
33
- export async function reason(ctx, query, answer, preConsumed, pre, voiced = []) {
25
+ export async function reason(ctx, query, answer, preConsumed, pre, voiced = [],
26
+ /** The query material the GROUNDING left uncovered — the cost ladder's own
27
+ * `unaccounted` spans. Only the reasoner's OWN extensions are judged
28
+ * against it; a mechanism carrying its own `used` set owns its shape. */
29
+ uncovered = []) {
34
30
  // Echo guard: a query that is ITSELF a learnt continuation (some context's
35
31
  // answer) is being asked back at the system — hopping forward from it would
36
32
  // chain through the very fact that produced it and echo the conversation
37
33
  // back. The grounded answer alone is the honest read-out. Deliberately a
38
34
  // broad structural gate; pinned by test/31-audit.
39
35
  const qId = pre.queryResolved;
40
- if (qId !== null && ctx.store.prevCount(qId) > 0)
41
- return answer;
36
+ if (qId !== null && ctx.store.prevCount(qId) > 0) {
37
+ return { bytes: answer, carried: [], steps: 0 };
38
+ }
42
39
  // Consume a node and its neighbours for pivot-cycle prevention — CAPPED at
43
40
  // the hub bound, via the store's LIMITed edge reads: a common continuation's
44
41
  // reverse fan-in (and a hub context's forward fan-out) is corpus-sized, and
@@ -95,7 +92,7 @@ export async function reason(ctx, query, answer, preConsumed, pre, voiced = [])
95
92
  ? null
96
93
  : ctx.store.prevFirst(groundedId, bound);
97
94
  if (qId !== null && groundedPrev !== null && groundedPrev.includes(qId)) {
98
- return answer;
95
+ return { bytes: answer, carried: [], steps: 0 };
99
96
  }
100
97
  const consumed = new Set();
101
98
  /** `prev` lets a caller hand in an already-read reverse-edge list — hop 0
@@ -143,7 +140,27 @@ export async function reason(ctx, query, answer, preConsumed, pre, voiced = [])
143
140
  const qv = pre.guide; // the response-wide guide IS the query's gist
144
141
  let t;
145
142
  const startedFrom = answer;
146
- for (let hop = 0; hop < ctx.cfg.recallQueryK; hop++) {
143
+ // INSTRUMENTATION ONLY — the two facts the extension's own decision already
144
+ // used and threw away: the spans of uncovered material each step was
145
+ // JUSTIFIED by (the gate below computes which span carries it and kept only a
146
+ // boolean), and how many steps were taken. Nothing here decides anything:
147
+ // both are read after the loop, to bump counters and to let the caller compare
148
+ // the extension's cost against what it explains, in the ladder's own currency.
149
+ const carried = [];
150
+ let steps = 0;
151
+ // NO ALLOWANCE: THE CHAIN ENDS WHEN IT STOPS. Every exit below is the law —
152
+ // no pivot, no forward step, no question material carried — and the walk is
153
+ // bounded by the material and the graph rather than by a count: each taken
154
+ // step must carry a W-window of the uncovered material (finite), and
155
+ // `consumed` refuses to revisit a node. `recallQueryK` no longer bounds the
156
+ // reasoner here; it keeps its other roles (the bridge's candidate reads, the
157
+ // pivot's probe budget, the resonance limits).
158
+ //
159
+ // Measured before removing it: test/89 — the corpus-cost guard, the heaviest
160
+ // case in the suite — is green and no slower without the allowance (27 s
161
+ // against 30 s); and raising it from 12 to 200 changed neither the answer nor
162
+ // `pivotSteps` on the chain fixtures.
163
+ for (let hop = 0;; hop++) {
147
164
  // Hop 0's `cur` IS `answer`, so the guard above already resolved it and
148
165
  // read its reverse edges — reuse both rather than repeat them.
149
166
  const curId = hop === 0 ? groundedId : resolve(ctx, cur);
@@ -166,6 +183,7 @@ export async function reason(ctx, query, answer, preConsumed, pre, voiced = [])
166
183
  ]);
167
184
  ctx.trace?.step("absorbForward", [rItem(cur, "answer", curId)], [rItem(fwd, "answer", resolve(ctx, fwd) ?? undefined)], "the answer is itself a learnt fact — follow its continuation to the fixpoint");
168
185
  cur = fwd;
186
+ steps++;
169
187
  continue;
170
188
  }
171
189
  }
@@ -178,12 +196,98 @@ export async function reason(ctx, query, answer, preConsumed, pre, voiced = [])
178
196
  consumeAll(pivot);
179
197
  if (fc === null || bytesEqual(fc, cur) || restatesQuery(query, fc))
180
198
  break;
199
+ // WHOSE EXTENSION IS THIS?
200
+ //
201
+ // `voiced` is what the mechanism WITHHELD (the pipeline sends the used
202
+ // anchors' CONTINUATIONS, not their bytes — see pipeline's own note), so a
203
+ // non-empty `voiced` means exactly what that note says: the grounding came
204
+ // from a mechanism that carries its own short `used` set (cast/join) and
205
+ // therefore owns the shape of its answer. The further terms inside such a
206
+ // seat are legitimately followable — test/29 C3's `Mona Lisa` lives inside
207
+ // the voiced seat and leads on to a fact about neither analog.
208
+ //
209
+ // Every other grounding is ordinary, and an extension of it is the
210
+ // reasoner's own inference: it is taken only while question material the
211
+ // grounding left uncovered remains AND the step carries some of it, judged
212
+ // by the mind's own line between chance and evidence — one W-byte window,
213
+ // no word notion, no character class, no threshold. Measured: the drift's
214
+ // second step (`the Eiffel Tower is in Paris` after `Paris is famous for
215
+ // the Eiffel Tower`) carries no window of `" famous for"` and is refused,
216
+ // while the first carries it. Terminates by a real argument: the uncovered
217
+ // material is finite and each taken extension must carry some of it.
218
+ const producerOwnsShape = voiced.length > 0;
219
+ if (!producerOwnsShape && uncovered.length > 0) {
220
+ const W = ctx.space.maxGroup;
221
+ let progress = false;
222
+ let justified;
223
+ for (const [a, b] of uncovered) {
224
+ for (let i = a; i + W <= b && !progress; i++) {
225
+ if (indexOf(fc, query.subarray(i, i + W), 0) >= 0) {
226
+ progress = true;
227
+ justified = [a, b];
228
+ }
229
+ }
230
+ if (progress)
231
+ break;
232
+ }
233
+ if (progress && justified !== undefined)
234
+ carried.push(justified);
235
+ if (!progress) {
236
+ // THE BRAKE, MADE VISIBLE. The reasoner declines a step that carries
237
+ // none of the material the grounding left uncovered — the drift the
238
+ // extension tests pin. A refusal that leaves no trace is the kind of
239
+ // silent cut AGENTS §6 forbids: the rationale is where a reader learns
240
+ // that an extension was declined for want of question material, and
241
+ // where the next person sees why the chain stopped here. Measured with
242
+ // the check disabled, test/110 and test/116 fail — so this brake is the
243
+ // only thing keeping the extension honest until the pivot reports its
244
+ // own accounted spans and the ladder can judge it instead.
245
+ //
246
+ // THE PROMISE IS NOW KEPT, AND THE BRAKE TURNS OUT TO BE THE LADDER'S
247
+ // OWN CONSEQUENCE. The extension reports what it carried (`carried`,
248
+ // the span each step was justified by) and what it cost (`steps`), both
249
+ // counted in the meter (`reasonCarriedBytes`, `reasonSteps`), so the
250
+ // ladder CAN judge it: it accepts while
251
+ //
252
+ // steps · STEP < PASS · carried
253
+ //
254
+ // and this brake accepts whenever the step carries a `W`-window, i.e.
255
+ // whenever `carried ≥ W ≥ 1`. With `PASS/STEP = 1000` the two therefore
256
+ // agree on every extension with `steps ≤ 1000 · carried` — and every
257
+ // extension this repository produces takes 0 or 1 steps (measured on
258
+ // chains of 3, 8, 20 and 40 links). Above that bound the ladder would
259
+ // refuse what this brake accepts, which is the corner named in the
260
+ // closure report's limits: a chain of thousands of links explaining a
261
+ // handful of bytes. No guard is added for it — a limit without a
262
+ // derivation is exactly what the brake must not become.
263
+ const left = unaccountedBytes(uncovered);
264
+ ctx.trace?.step("pivotRefused", [rItem(cur, "answer"), rItem(query, "query")], uncovered.map(([a, b]) => rItem(query.subarray(a, b), "uncovered")), `the step carries none of the question material the grounding left ` +
265
+ `uncovered (${left} byte(s) in ${uncovered.length} span(s)) — refused`);
266
+ break;
267
+ }
268
+ }
269
+ if (ctx.meter)
270
+ ctx.meter.pivotSteps++;
181
271
  t ??= ctx.trace?.enter("reason", [rItem(startedFrom, "grounded")]);
182
272
  ctx.trace?.step("pivotStep", [rItem(cur, "answer"), rNode(ctx, pivot, "pivot")], [rItem(fc, "answer", resolve(ctx, fc) ?? undefined)], "pivot on the shared span this answer contains, then step forward across that fact");
183
273
  cur = fc;
274
+ steps++;
275
+ }
276
+ // INSTRUMENTATION ONLY — the extension's two facts, untraced (meter.ts
277
+ // contract 1: a counter never reaches a decision). They are what a caller
278
+ // needs to PRICE the extension instead of taking it unconditionally: the work
279
+ // it did (`steps · STEP`) and the uncovered material it carried.
280
+ if (ctx.meter) {
281
+ ctx.meter.reasonSteps += steps;
282
+ ctx.meter.reasonCarriedBytes += unaccountedBytes(carried);
184
283
  }
185
- t?.done([rItem(cur, "answer", resolve(ctx, cur) ?? undefined)], "the multi-hop chain's fixpoint");
186
- return cur;
284
+ t?.done([rItem(cur, "answer", resolve(ctx, cur) ?? undefined)],
285
+ // A FIXPOINT: no further step was possible. This note used to also cover an
286
+ // exhausted hop allowance — a different fact, and the reason F1 added a
287
+ // counter for it. The allowance is gone, so the only way out of the loop is
288
+ // a refusal, and the note is true again by construction.
289
+ "the multi-hop chain's fixpoint");
290
+ return { bytes: cur, carried, steps };
187
291
  }
188
292
  /** Fuse independent points of attention into one answer (multi-topic).
189
293
  * When the consensus climb finds more than one dominant point, each
@@ -352,8 +456,9 @@ primarySpans = []) {
352
456
  out = await joinWithBridge(ctx, out, pieces[i].bytes);
353
457
  }
354
458
  t?.done([rItem(out, "answer", resolve(ctx, out) ?? undefined)], `fused ${pieces.length} independent points of attention into one answer`);
459
+ // THE FACT IS THE FUSED ANSWER, not the call: every early return above hands
460
+ // back `primary` untouched. Untraced on purpose (meter.ts contract 1).
461
+ if (ctx.meter)
462
+ ctx.meter.fuseRuns++;
355
463
  return out;
356
464
  }
357
- // (resonance.js is already a static dependency above — `bridge` — so the old
358
- // dynamic import of pivotInto guarded against a cycle that does not exist.)
359
- import { containsSpan } from "./match.js";
@@ -519,8 +519,11 @@ function recogniseImpl(ctx, bytes) {
519
519
  if (end - start < W)
520
520
  return false;
521
521
  if (flatProbe(start, end) === null) {
522
- if (!canonBudget)
522
+ if (!canonBudget) {
523
+ if (ctx.meter)
524
+ ctx.meter.canonProbesDenied++;
523
525
  return false;
526
+ }
524
527
  if (!canonAdmits(start, end))
525
528
  return false;
526
529
  }
@@ -543,13 +546,6 @@ function recogniseImpl(ctx, bytes) {
543
546
  // endpoints in order and running out partway along the query. A form
544
547
  // longer than that is out of this tier's reach — but so is a form the
545
548
  // chain cannot span, and that is exactly the trade the budget prices.
546
- // The factor is chainReach(W), the same W² scale the chain already
547
- // trusts; no new constant.
548
- // The factor is chainReach(W) — the same W² scale the chain itself
549
- // trusts — so the cap is derived from the fold's geometry, never tuned.
550
- // (It was briefly an environment variable while the cost was being
551
- // measured; an env-read here would make inference non-reproducible,
552
- // which the determinism contract forbids outright.)
553
549
  // The factor is chainReach(W) — the same W² scale the chain itself
554
550
  // trusts — so the cap is derived from the fold's geometry, never tuned.
555
551
  // (It was briefly an environment variable while the cost was being
@@ -249,7 +249,14 @@ export async function joinWithBridge(ctx, left, right) {
249
249
  * overlap, so C3's genuine further hop — "Mona Lisa", a term inside the seat
250
250
  * sentence but part of NEITHER analog — still fires. */
251
251
  export async function pivotInto(ctx, answer, consumed, voiced = []) {
252
- const k = ctx.cfg.recallQueryK;
252
+ // The pivot's OWN shortlist capacity — not `recallQueryK`: they are different
253
+ // quantities (a mechanical sweep's probe budget vs the bridge's candidate-read
254
+ // allowance), and one number serving both means tightening either silently
255
+ // starves the other. The reason is the duplication, NOT a measured strangle:
256
+ // an earlier version of this comment claimed "at recallQueryK 1 the pivot finds
257
+ // no pivot", and that was probed and is FALSE on test/23's fixture — the
258
+ // strangle is fixture-specific, so it is not the evidence for this split.
259
+ const k = ctx.cfg.pivotProbeK;
253
260
  // ONE perception of the answer, shared by the probe budget and the walk —
254
261
  // this used to fold the same bytes twice, back to back, on every hop.
255
262
  const tree = perceive(ctx, answer);
@@ -288,6 +295,18 @@ export async function pivotInto(ctx, answer, consumed, voiced = []) {
288
295
  for (const c of n.kids)
289
296
  queue.push(c); // breadth-first: larger regions first
290
297
  }
298
+ // The sweep's two FACTS, untraced (meter.ts contract 1: a counter never
299
+ // reaches a decision). `probes` is the work done; the shortfall is the
300
+ // capacity the cap withheld — the branches the breadth-first order never got
301
+ // to. Whether that withheld anything that mattered is NOT said here: the
302
+ // order spends the largest regions first, and recognition below still
303
+ // contributes every exact containment candidate regardless of the budget.
304
+ if (ctx.meter) {
305
+ ctx.meter.pivotProbes += probes;
306
+ const unprobed = branchCount - probeCap;
307
+ if (unprobed > 0)
308
+ ctx.meter.pivotBranchesUnprobed += unprobed;
309
+ }
291
310
  // THE FULL recognition, memo-shared with every other reader of these bytes.
292
311
  // A "skip the edge trims here" variant was refuted (see recognise's own
293
312
  // note): those trims are what find a WHOLE trained form embedded at an
@@ -8,6 +8,7 @@ import { decodeText } from "./rationale.js";
8
8
  export function rItem(bytes, role, node, span) {
9
9
  return {
10
10
  text: decodeText(bytes),
11
+ bytes,
11
12
  role,
12
13
  node: node ?? undefined,
13
14
  span,
@@ -8,6 +8,12 @@
8
8
  import { cosine } from "../vec.js";
9
9
  import { gistOf, read } from "./primitives.js";
10
10
  import { canonicalWindows, leafIdPrefix, leafIdRun } from "./canonical.js";
11
+ // Imported at the TOP, where every other import is. They used to sit 800 lines
12
+ // down under a note claiming the position mattered ("before trace module is
13
+ // loaded") — it does not: an ES module's static imports are HOISTED, so the
14
+ // file's line order never decides load order. The note described an intention
15
+ // the runtime does not honour; the imports move and the claim goes.
16
+ import { decodeText } from "./rationale.js";
11
17
  //
12
18
  // Budgeted on the same terms as the reach memo below (caches.md): these three
13
19
  // maps are cleared on every write, but a long read-only session over a large
@@ -664,7 +670,15 @@ export function chooseNext(ctx, id, guide) {
664
670
  // for the prevCount calls in the loop above, never for extra rItemShort
665
671
  // byte-reads.
666
672
  if (ctx.trace) {
667
- const others = capped.filter((c) => c !== best);
673
+ // A BOUNDED SAMPLE, AND THE COUNT. The step used to carry EVERY candidate
674
+ // it weighed — measured on the trained store, 1559 out-items in one step
675
+ // (hubBound's own size) and 1082 in another (the hub's degree). The
676
+ // rationale's job is to explain the CHOICE, and the count is what says how
677
+ // wide the field was; the declared candidate budget (`recallQueryK`) is what
678
+ // bounds the sample, so no number is invented here.
679
+ const others = capped
680
+ .filter((c) => c !== best)
681
+ .slice(0, ctx.cfg.rationaleSampleK);
668
682
  ctx.trace.step("disambiguate", [rItemShort(ctx, best, "halo-evidence", bestSupport)], others.map((c) => rItemShort(ctx, c, "candidate", ctx.store.prevCount(c))), `${capped.length} continuations — distributional evidence selects ` +
669
683
  `the most corroborated (distinct contexts ${bestSupport}, ` +
670
684
  `poured mass ${bestMass})`);
@@ -696,8 +710,6 @@ export function chooseAmong(ctx, candidates, guide) {
696
710
  ? { id: found.item, score: found.score }
697
711
  : { id: candidates[0], score: -Infinity };
698
712
  }
699
- // ── Trace shim (used by chooseNext before trace module is loaded) ────────
700
- import { decodeText } from "./rationale.js";
701
713
  function rItemShort(ctx, id, role, score) {
702
714
  return {
703
715
  text: decodeText(read(ctx, id)),