@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
@@ -23,6 +23,22 @@ export declare function leafIdRun(ctx: MindContext, bytes: Uint8Array, from: num
23
23
  * what a partial prefix means (deposit only interns the whole-stream flat
24
24
  * branch when the prefix covers everything). */
25
25
  export declare function leafIdPrefix(ctx: MindContext, bytes: Uint8Array): number[];
26
+ /** Which prefixes of `prefix ‖ tail` are STORED NODES — as lengths in the
27
+ * tail's own coordinates, ascending, excluding the empty one. This is the
28
+ * candidate set a rule needs to join an already-stored prefix to a suffix it
29
+ * has not stored: a key names a relation exactly when `prefix ‖ tail[0..p]` IS
30
+ * a node, and a key can end strictly inside the tail without sitting on any
31
+ * fold boundary (a stored member's end is the end of ITS OWN stream, and the
32
+ * fold never emits a cut at a stream's end). Measured: "stockholm mayor"
33
+ * exists, leads on, and its boundary 6 is in neither the tail's cuts nor the
34
+ * concatenation's.
35
+ *
36
+ * ONE cheap content-addressed probe per offset — `leafIdPrefix` walks the bytes
37
+ * once (a point probe each), `findBranch` hashes the growing kid run — and NO
38
+ * `resolve`, which is what keeps this off the O(suffix) vector folds the
39
+ * recognition path pays. It stops at the first byte that was never interned,
40
+ * which costs nothing real: a stored key's bytes are interned by construction. */
41
+ export declare function keyEnds(ctx: MindContext, prefix: Uint8Array, tail: Uint8Array): number[];
26
42
  /** The canonical W-window node ids of a byte stream, offset → id — the
27
43
  * CONTENT-ADDRESSED IDENTITY of every W-sized slice, under which any content
28
44
  * two deposits share IS the same node (hash-consing paid the comparison at
@@ -70,6 +70,47 @@ export function leafIdPrefix(ctx, bytes) {
70
70
  }
71
71
  return ids;
72
72
  }
73
+ /** Which prefixes of `prefix ‖ tail` are STORED NODES — as lengths in the
74
+ * tail's own coordinates, ascending, excluding the empty one. This is the
75
+ * candidate set a rule needs to join an already-stored prefix to a suffix it
76
+ * has not stored: a key names a relation exactly when `prefix ‖ tail[0..p]` IS
77
+ * a node, and a key can end strictly inside the tail without sitting on any
78
+ * fold boundary (a stored member's end is the end of ITS OWN stream, and the
79
+ * fold never emits a cut at a stream's end). Measured: "stockholm mayor"
80
+ * exists, leads on, and its boundary 6 is in neither the tail's cuts nor the
81
+ * concatenation's.
82
+ *
83
+ * ONE cheap content-addressed probe per offset — `leafIdPrefix` walks the bytes
84
+ * once (a point probe each), `findBranch` hashes the growing kid run — and NO
85
+ * `resolve`, which is what keeps this off the O(suffix) vector folds the
86
+ * recognition path pays. It stops at the first byte that was never interned,
87
+ * which costs nothing real: a stored key's bytes are interned by construction. */
88
+ export function keyEnds(ctx, prefix, tail) {
89
+ if (prefix.length === 0 || tail.length === 0)
90
+ return [];
91
+ const joined = new Uint8Array(prefix.length + tail.length);
92
+ joined.set(prefix, 0);
93
+ joined.set(tail, prefix.length);
94
+ const ids = leafIdPrefix(ctx, joined);
95
+ if (ids.length < prefix.length)
96
+ return [];
97
+ const ends = [];
98
+ // The kid run GROWS by one id per offset; `findBranch` wants an array, so the
99
+ // run is built once and pushed into, never re-sliced. Re-slicing
100
+ // `ids.slice(0, prefix.length + p)` per offset made this O(|tail| ·
101
+ // (|prefix| + |tail|)) — quadratic in the tail, where the learning path this
102
+ // follows slices a run that SHRINKS. Same ends, linear copying.
103
+ const run = ids.slice(0, prefix.length);
104
+ // The loop ENDS at the first byte that was never interned (`ids.length`):
105
+ // every later prefix contains it, so none of them can be a node either — this
106
+ // is where the scan stops, not a silent truncation of the answer.
107
+ for (let p = 1; prefix.length + p <= ids.length; p++) {
108
+ run.push(ids[prefix.length + p - 1]);
109
+ if (ctx.store.findBranch(run) !== null)
110
+ ends.push(p);
111
+ }
112
+ return ends;
113
+ }
73
114
  /** The canonical W-window node ids of a byte stream, offset → id — the
74
115
  * CONTENT-ADDRESSED IDENTITY of every W-sized slice, under which any content
75
116
  * two deposits share IS the same node (hash-consing paid the comparison at
@@ -868,6 +868,8 @@ export class GraphSearch {
868
868
  // concepts/connectors either (those need the caller's async
869
869
  // pre-resolution) — the recursion follows edges and fusion, which is what
870
870
  // a deeper rewrite chain is made of.
871
+ if (this.host.meter)
872
+ this.host.meter.recompletes++;
871
873
  const rec = this.host.recogniseSpan(bytes);
872
874
  const kids = new Set(nrec.kids);
873
875
  // THE NODE'S OWN KIDS ARE SITES BY STRUCTURE — recognition cannot be the
@@ -1100,20 +1102,37 @@ export class GraphSearch {
1100
1102
  // duplicate read and a branch that could never be taken.
1101
1103
  let next = null;
1102
1104
  let keyBytes = c.bytes;
1103
- // THE PREFIX ENDS ARE THE TAIL'S OWN FOLD BOUNDARIES, not every byte
1104
- // length. The key is `entity + prefix`, and the prefix that names a
1105
- // stored relation ends where the fold cuts: measured over four join-firing
1106
- // queries, 5 of 5 accepted keys ended on a boundary (or the tail's end)
1107
- // while the byte-by-byte scan spent 153 probes where 14 boundaries would
1108
- // do. Same criterion — resolves AND leads — same shortest-first order, so
1109
- // the answer is the same one the enumeration found; only the candidates
1110
- // come from the structure instead of from the byte count. A host with no
1111
- // boundary rule falls back to the enumeration.
1112
- const cuts = this.host.contentCuts?.(tail);
1113
- const ends = cuts && cuts.length > 0
1114
- ? [...cuts.filter((c) => c > 0 && c < tail.length), tail.length]
1115
- : Array.from({ length: tail.length }, (_, i) => i + 1);
1116
- for (const len of ends) {
1105
+ // THE CANDIDATE ENDS ARE THE PREFIXES THAT ARE STORED NODES, ASCENDING.
1106
+ // The key is `entity + prefix`, and it names a relation exactly when that
1107
+ // concatenation IS a node — so the ends come from a content-addressed
1108
+ // probe per offset (the host's `contentKeyEnds`, the learning path's own
1109
+ // mechanism: one leaf walk plus one `findBranch` per offset, no `resolve`),
1110
+ // never from the fold's boundaries. A boundary is not a proxy: a stored
1111
+ // member's end is the end of ITS OWN stream, and the fold never cuts at a
1112
+ // stream's end — measured, "stockholm mayor" exists, leads on to the mayor
1113
+ // fact, and its boundary 6 sits in neither the tail's cuts ([4,7]) nor the
1114
+ // concatenation's. Filtering the scan by "is this a node?" cannot change
1115
+ // the winner: a position that is not a node cannot resolve, so skipping it
1116
+ // is invisible; and the order stays SHORTEST FIRST, which is a semantic
1117
+ // law, not an optimisation (test/106, test/108 pin it).
1118
+ //
1119
+ // A host that cannot answer falls back to every prefix: exact and
1120
+ // complete, at a `resolve` per offset. A host that CAN answer is
1121
+ // authoritative even when it answers "none" — if no prefix is a node then
1122
+ // no key exists to resolve, so enumerating would only pay nulls. (A key
1123
+ // reachable through the CANONICAL equivalence alone and ending off every
1124
+ // node end is therefore not tried here; that dimension is unreachable on
1125
+ // this path by construction and is not part of the exact-key law.)
1126
+ const ends = this.host.contentKeyEnds?.(c.bytes, tail);
1127
+ const candidateEnds = function* () {
1128
+ if (ends !== undefined) {
1129
+ yield* ends;
1130
+ return;
1131
+ }
1132
+ for (let p = 1; p <= tail.length; p++)
1133
+ yield p;
1134
+ };
1135
+ for (const len of candidateEnds()) {
1117
1136
  keyBytes = concat2(c.bytes, tail.subarray(0, len));
1118
1137
  const k = this.host.resolve(keyBytes) ??
1119
1138
  this.host.canonResolve?.(keyBytes) ??
@@ -200,7 +200,7 @@ export declare function frameSlots(ctx: MindContext, query: Uint8Array, cand: Ui
200
200
  * ANCHOR that the query displaced. Neither implies the other, and the
201
201
  * observed failures pass the restatement guard cleanly.
202
202
  *
203
- * Three conditions, all byte-exact and all necessary:
203
+ * Four conditions, all byte-exact and all necessary:
204
204
  *
205
205
  * 1. the query and the anchor must be ONE STRUCTURE — what they share has to
206
206
  * dominate the query, or the query is not a variant of the anchor at all
@@ -546,7 +546,7 @@ export function frameSlots(ctx, query, cand, id) {
546
546
  * ANCHOR that the query displaced. Neither implies the other, and the
547
547
  * observed failures pass the restatement guard cleanly.
548
548
  *
549
- * Three conditions, all byte-exact and all necessary:
549
+ * Four conditions, all byte-exact and all necessary:
550
550
  *
551
551
  * 1. the query and the anchor must be ONE STRUCTURE — what they share has to
552
552
  * dominate the query, or the query is not a variant of the anchor at all
@@ -631,8 +631,10 @@ export function substituteAll(hay, pairs) {
631
631
  if (usable.length === 0)
632
632
  return hay;
633
633
  // Longest needle first, so a needle that is a prefix of another can never
634
- // pre-empt it. Ties cannot arise: an instance whose fillers are not
635
- // pairwise distinct is refused by frameSlots.
634
+ // pre-empt it. Ties cannot arise: a consumer that VOICES checks the
635
+ // fillers pairwise with `distinct` and refuses such an instance itself —
636
+ // `frameSlots` reports and does not judge (see its own doc), so the refusal
637
+ // lives with the mechanism that needs it, not here.
636
638
  const order = [...usable].sort((a, b) => b.needle.length - a.needle.length);
637
639
  const out = [];
638
640
  let i = 0;
@@ -731,7 +731,7 @@ export async function counterfactualTransfer(ctx, query, pre) {
731
731
  // grounds") — fine for ORIENTING mechanisms, not for voicing learnt
732
732
  // content the query never asked about. Computed once here; both the
733
733
  // hub fallback below and the comparison gate consume it.
734
- const rootTrusted = roots.some((r) => r.vote >= consensusFloor(corpusN(ctx)));
734
+ const rootTrusted = roots.some((r) => r.idfVote >= consensusFloor(corpusN(ctx))); // the IDF sum: the bar's own quantity
735
735
  // The context that ESTABLISHES a filler — the same reverse context, under
736
736
  // the same naming test, `seatOfNode` uses to VOICE an analog (a predecessor
737
737
  // whose bytes CONTAIN the node's: it names or describes it, rather than
@@ -98,6 +98,30 @@ export async function confluenceJoin(ctx, query, pre) {
98
98
  // constraints). Shard-bound streams are no constraints, and their meets
99
99
  // are connective debris (". Sure,", "ngul" — observed).
100
100
  const bindsAConstituent = (cover) => cover.some(([cs, ce]) => ce - cs >= 2 * W);
101
+ // THE VOTE ENTERS AS ORDER, NEVER AS A BAR. This is the only one of the
102
+ // climb's four consumers (recall, fuseAttention, cast, here) that uses the
103
+ // evidence's MAGNITUDE without a floor, and it is legitimate by construction:
104
+ // `ranked` answers "which anchor is stronger" — a question about votes, so the
105
+ // comparison stays within one dimension — and the vote is otherwise only
106
+ // REPORTED (Stream.vote travels to the rationale's constraint nodes). What
107
+ // actually SELECTS a constraint is byte-structural and never the magnitude: a
108
+ // run of at least 2W (`bindsAConstituent`, with its accidental-sharing
109
+ // counter-examples above), disjoint covers (`disjoint`), and scaffolding never
110
+ // binds at all (`dominates(reachOf(…), N)`). The MEET such a stream may
111
+ // produce is selected the same way: a span shorter than 2W is rejected, and
112
+ // the winner is the one with the smallest `reach` (ties broken by the longer
113
+ // span) — a corpus quantity and bytes, never the vote, which appears only in
114
+ // the trace item.
115
+ // binds at all (`dominates(reachOf(…), N)`). The only cut in this loop is a
116
+ // BUDGET, and it is measured: stopping the scan at 2W anchors saves 50-70% of
117
+ // confluence's cost on non-conjunctive queries while preserving every genuinely
118
+ // conjunctive case, whose top anchors ARE its constraints.
119
+ // MEASURED (this goal, on THIS file's own conjunctive fixture): the two
120
+ // streams appear at ranks 1 and 4 against a budget of 2W = 8, on a query whose
121
+ // `ranked` is 9 — so the cut IS live (it would have returned null at the 8th
122
+ // anchor) and it does NOT prune the case it exists to protect. The other
123
+ // conjunctive fixture (the Leonardo one) finds them at ranks 0 and 1. Scope:
124
+ // these are the repo's conjunctive fixtures, and no more.
101
125
  const streams = [];
102
126
  const rankedCapped = ranked.length > pre.k ? ranked.slice(0, pre.k) : ranked;
103
127
  // CONJUNCTIVITY EARLY-EXIT: a conjunctive query's top-ranked anchors
@@ -194,9 +194,36 @@ export async function recallByResonance(ctx, query, pre) {
194
194
  // consensus", while breadth is the SCALE-INVARIANT reading — "a point whose
195
195
  // breadth clears `dominates` (> half the query's regions corroborate it) is
196
196
  // real consensus; one that does not is a coincidental single-region echo".
197
- // Attention.peak's contract makes the same point from the other side:
198
- // comparing a POOLED SUM against a floor that prices ONE region's evidence
199
- // is a dimensional error.
197
+ // THIS USED TO CLAIM A DIMENSIONAL ERROR, AND THAT CLAIM WAS FALSE.
198
+ // It read: "comparing a POOLED SUM against a floor that prices ONE region's
199
+ // evidence is a dimensional error." `consensusFloor` is not priced for one
200
+ // region: thresholds.md §2 derives it as the POOLED-vote significance floor —
201
+ // "each region contributes at most ln(N/c) <= ln(N); ln(N)+1/2 demands ..." —
202
+ // and attention.ts says the same where it builds the vote ("the scale
203
+ // consensusFloor is derived for"). The comparison is in ONE dimension, and
204
+ // it is so because the climb WEIGHTS BY IDF: `wf` in voteRegions is
205
+ // `direct ? df : combined ? idf + df : idf`, and the engine only ever runs the
206
+ // last one (DFMode's default "inverse", the mode every non-test caller uses —
207
+ // `direct` and `combined` are exercised by test/24 and test/27 only, and
208
+ // test/24 pins that their votes DO differ). In those two the sum would leave
209
+ // the floor's dimension and the floor would need re-deriving.
210
+ //
211
+ // What the OR below is really for is SCALE, not dimension (the paragraph
212
+ // above says it): a vote that clears ln(N)+1/2 means "strong" on a small store
213
+ // and "weak" on a large one for the same genuine consensus, so the
214
+ // scale-invariant breadth reading is added beside it.
215
+ //
216
+ // AND THE PREMISE IS IDF. The deviation in the other two weighting modes is
217
+ // TWO-SIDED and DERIVED: `direct` DEFLATES a region (ln(1+c) < ln(N/c) for
218
+ // small c) and `combined` INFLATES it (ln N + ln(1+1/c)), both by at most
219
+ // `ln 2` — see `geometry.ts`'s `consensusFloor`, where the bound lives.
220
+ // MEASURED on 8 anchors across 5 queries, running the same climb in all
221
+ // three modes: ZERO gate inversions — every anchor's `vote >= floor` verdict
222
+ // is the same in `inverse`, `direct` and `combined`, even where the readings
223
+ // straddle the floor on opposite sides (#148: inverse 3.39, combined 4.71
224
+ // above it, direct 1.31 below). Pinned by test/55's test 20. The bar is not
225
+ // re-derived for those modes because nothing reachable needs it; the premise
226
+ // is IDF, and that is now written where the gate reads it.
200
227
  //
201
228
  // Measured on the 15.7M-node store (N=325,615, so the old floor was 13.19).
202
229
  // The absolute vote cannot separate right from wrong at this scale, and the
@@ -265,7 +292,7 @@ export async function recallByResonance(ctx, query, pre) {
265
292
  const minVote = consensusFloor(corpusN(ctx));
266
293
  if (forest.length > 0 &&
267
294
  !allWindowsAreScaffolding(ctx, query) &&
268
- (forest[0].vote >= minVote ||
295
+ (forest[0].idfVote >= minVote || // the IDF sum: the bar's own quantity
269
296
  (dominates(forest[0].breadth, 1) && forest[0].peak > Math.LN2))) {
270
297
  const g = await project(ctx, forest[0].anchor, queryGist);
271
298
  // THE ANCHOR'S OCCUPANT IS NOT THE ASKER'S. This tier grounds an anchor
@@ -468,6 +495,7 @@ export const recallMechanism = {
468
495
  moves: r.moves,
469
496
  unexplained: r.unexplained,
470
497
  provenance: r.echoed ? "recall-echo" : "recall",
498
+ used: new Set(),
471
499
  ...(r.complete ? { complete: true } : {}),
472
500
  }];
473
501
  },
@@ -75,6 +75,8 @@ export interface MindOptions {
75
75
  seed?: number;
76
76
  recallQueryK?: number;
77
77
  haloQueryK?: number;
78
+ /** Branch nodes the pivot sweep may probe — see {@link MindConfig}. */
79
+ pivotProbeK?: number;
78
80
  /** Items one rationale step may itemise — see {@link MindConfig}. */
79
81
  rationaleSampleK?: number;
80
82
  /** Corpus-reading capacities and budgets — see {@link MindConfig}. */
@@ -202,8 +204,8 @@ export declare class Mind implements MindContext {
202
204
  * with the most distributional evidence (highest `prevOf` count — the
203
205
  * structural manifestation of its halo). When evidence is equal the
204
206
  * first-inserted edge wins. */
205
- /** See {@link GraphSearchHost.contentCuts}. */
206
- contentCuts(bytes: Uint8Array): readonly number[];
207
+ /** See {@link GraphSearchHost.contentKeyEnds}. */
208
+ contentKeyEnds(prefix: Uint8Array, tail: Uint8Array): readonly number[];
207
209
  chooseNext(node: number): number | undefined;
208
210
  constructor(opts?: MindOptions);
209
211
  constructor(cfg: MindConfig, store: Store, _fromStore: true);
@@ -11,7 +11,8 @@
11
11
  import { makeKeyring, rng, setVecConfig } from "../vec.js";
12
12
  import { sampleCorpus, searchCorpus } from "./corpus.js";
13
13
  import { Alphabet } from "../alphabet.js";
14
- import { contentBoundaries, contentFoldIncremental, reachThreshold, } from "../geometry.js";
14
+ import { contentFoldIncremental, reachThreshold, } from "../geometry.js";
15
+ import { keyEnds } from "./canonical.js";
15
16
  import { BoundedMap } from "../store.js";
16
17
  import { SQliteStore } from "../store-sqlite.js";
17
18
  import { resolveConfig } from "../config.js";
@@ -160,9 +161,9 @@ export class Mind {
160
161
  * with the most distributional evidence (highest `prevOf` count — the
161
162
  * structural manifestation of its halo). When evidence is equal the
162
163
  * first-inserted edge wins. */
163
- /** See {@link GraphSearchHost.contentCuts}. */
164
- contentCuts(bytes) {
165
- return contentBoundaries(this.space, bytes);
164
+ /** See {@link GraphSearchHost.contentKeyEnds}. */
165
+ contentKeyEnds(prefix, tail) {
166
+ return keyEnds(this, prefix, tail);
166
167
  }
167
168
  chooseNext(node) {
168
169
  return chooseNext(this, node, this._edgeGuide);
@@ -151,6 +151,13 @@ export interface MechanismResult {
151
151
  bytes: Uint8Array;
152
152
  accounted: Array<[number, number]>;
153
153
  moves: number;
154
+ /** WHAT THIS ANSWER SPEAKS FOR — the anchors it voices, and therefore the
155
+ * content the reasoner must not pivot back through. Declared by the
156
+ * mechanism about its OWN result, exactly like `accounted`/`unexplained`/
157
+ * `complete`: post-grounding honours the property and NEVER ASKS WHICH
158
+ * MECHANISM SET IT, so the market stays uniform. An EMPTY set is a real
159
+ * declaration — "this answer voices nothing" (recall) — and withholds
160
+ * nothing; omit the field and the pipeline re-recognises the answer. */
154
161
  used?: ReadonlySet<number>;
155
162
  unexplained: string;
156
163
  /** Explicit weight override. When absent, weight = moves + PASS·unaccounted. */
@@ -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,9 +348,21 @@ 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
- : [];
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
+ });
358
366
  // REPORTABLE, NOT SILENT. A declared-complete grounding ends the derivation
359
367
  // here, and that decision is part of the derivation's shape: the reader of a
360
368
  // rationale must be able to see that the chain stopped because the mechanism
@@ -378,9 +386,21 @@ export async function think(ctx, query, mechs) {
378
386
  ];
379
387
  const uncovered = unexplainedSpans(query.length, explained)
380
388
  .filter(([a, b]) => b - a >= ctx.space.maxGroup);
381
- const reasoned = decided.complete ? answer : meter
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
382
401
  ? await meter.time("reason", () => reason(ctx, query, answer, preConsumed, pre, voiced, uncovered))
383
402
  : await reason(ctx, query, answer, preConsumed, pre, voiced, uncovered);
403
+ const reasoned = extension?.bytes ?? answer;
384
404
  // Fuse only when the query has a genuine REMAINDER no mechanism's
385
405
  // structural evidence touched at all. `decided.accounted` alone
386
406
  // undercounts this: it is a COST-LADDER quantity (cover.ts prices its
@@ -424,6 +444,13 @@ export async function think(ctx, query, mechs) {
424
444
  : meter
425
445
  ? await meter.time("fuse", () => fuseAttention(ctx, query, reasoned, pre, unclimbed, primarySpans))
426
446
  : await fuseAttention(ctx, query, reasoned, pre, unclimbed, primarySpans);
427
- 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");
428
455
  return { bytes: fused, provenance };
429
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,18 +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. */
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
+ }
23
41
  export declare function reason(ctx: MindContext, query: Uint8Array, answer: Uint8Array, preConsumed: ReadonlySet<number>, pre: Precomputed, voiced?: readonly Uint8Array[],
24
42
  /** The query material the GROUNDING left uncovered — the cost ladder's own
25
43
  * `unaccounted` spans. Only the reasoner's OWN extensions are judged
26
44
  * against it; a mechanism carrying its own `used` set owns its shape. */
27
- uncovered?: readonly (readonly [number, number])[]): Promise<Uint8Array>;
45
+ uncovered?: readonly (readonly [number, number])[]): Promise<ReasonedAnswer>;
28
46
  /** Fuse independent points of attention into one answer (multi-topic).
29
47
  * When the consensus climb finds more than one dominant point, each
30
48
  * independent point grounds its own answer; they are bridged together