@hviana/sema 0.8.6 → 0.8.7

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.
@@ -203,6 +203,11 @@ export interface ClimbConsensusData {
203
203
  total: number;
204
204
  };
205
205
  regions?: ConsensusRegionTrace[];
206
+ /** A NAME SHARED WITH THE LAW, AND NOT THE SAME THING: these are the reaches
207
+ * the consensus climb took, for the trace. The law's `Continuation.reaches`
208
+ * (derivation.ts) is a producer's claim that a step moves — different type,
209
+ * different owner, no relation. Kept distinct rather than renamed because the
210
+ * trace's own vocabulary is about the climb, not about derivation. */
206
211
  reaches?: ConsensusReachTrace[];
207
212
  crossRegion?: {
208
213
  eligibleRegions: number;
@@ -176,14 +176,19 @@ export declare function advance(d: DerivationState, t: Continuation, explains: R
176
176
  /** What a layer offers the law: the next continuation of a state, or null when
177
177
  * it has none. A layer OFFERS; the law disposes.
178
178
  *
179
- * ONE OFFER, AND IT IS THE LAYER'S LAST: {@link closure} stops when the law
180
- * refuses what was offered, so a refusal is read as "no continuation exists".
181
- * A layer must not offer candidates one at a time and expect the walk to
182
- * continue after a refusal — its own fallbacks belong inside this function.
183
- * The producers do exactly that: they choose between the forward absorb and the
184
- * pivot before offering, and return null only when neither exists, which is why
185
- * the one offer the law can still refuse (a pivot without ownership, whose
186
- * material the answer does not carry) really is the last one. */
179
+ * ONE OFFER AT A TIME: {@link closure} asks again only while the law admits what
180
+ * was offered, so a refusal ends the walk. A layer must not offer candidates
181
+ * one at a time and expect the walk to continue after a refusal — its own
182
+ * fallbacks belong inside this function, and the producers do exactly that:
183
+ * they choose between the forward absorb and the pivot before offering.
184
+ *
185
+ * WHAT `null` DOES NOT SAY: it is not a claim of exhaustion. A layer returns
186
+ * null for several distinct reasons — it exhausted its enumeration, it refused a
187
+ * candidate it had reached, or a read bound cut one off — and the law cannot tell
188
+ * them apart: it reads every one as "no continuation exists". The layer's reasons
189
+ * are therefore made OBSERVABLE where they happen, on the rationale's own channel
190
+ * (`enumerationExhausted`, `pivotCandidateRefused`, `absorbCandidateRefused`), and
191
+ * it is there — not here — that a reader learns which of them stopped the walk. */
187
192
  export type Offer = (d: DerivationState) => Promise<Continuation | null>;
188
193
  /** THE CLOSURE — the walk of {@link advance} over the continuations `offer`
189
194
  * proposes, run until the layer has nothing further to offer or the law refuses
@@ -234,6 +234,15 @@ export function closed(d) {
234
234
  export function admissible(d, t, query, W) {
235
235
  if (d.fixed)
236
236
  return null;
237
+ // THE GATE THAT NEVER FIRES, AND WHY IT STAYS. `contains` is the step's claim
238
+ // to BE a continuation of this product at all, and the formula is stated with
239
+ // it (see the doc above). Every producer in the engine declares it true — the
240
+ // three literals in reasoning.ts are constants, measured — so this line cannot
241
+ // reject anything today. It stays because the claim is what makes an `Offer`
242
+ // answerable: a layer that handed over something it does not call a
243
+ // continuation would be refused here rather than trusted. What it must NOT be
244
+ // is a place where a producer says "false" to end a walk: the walk ends by
245
+ // offering nothing (`null`), which is the contract.
237
246
  if (!t.contains)
238
247
  return null;
239
248
  if (closed(d))
@@ -451,6 +451,15 @@ export async function think(ctx, query, mechs) {
451
451
  meter.postGroundingRemainderSpans += uncovered.length;
452
452
  meter.postGroundingRemainderBytes += unaccountedBytes(uncovered);
453
453
  }
454
+ // THE WALK THAT DOES NOT RUN, NAMED — the audit's point 1 could not attribute
455
+ // real questions that stopped with no note anywhere. They never reached the
456
+ // offer: `decided.complete` says the grounding supplied a fixed point, so the
457
+ // walk is skipped BY DESIGN and the state is the grounding's own. That state
458
+ // carries `fixed: true` (the trace already reports it), and the law's first
459
+ // clause refuses any continuation against it — so there is no `null` here to
460
+ // read as exhaustion, and the `Offer` contract is not in play at all. The
461
+ // silent stop is therefore a NAMED state, not a gap.
462
+ //
454
463
  // THE WALK CONSUMES AND RETURNS A STATE. It is handed the derivation's own —
455
464
  // the grounding's product, accounting, remainder and cost — and hands back the
456
465
  // state it advanced to, so what follows reads a state rather than bytes plus a
@@ -39,6 +39,13 @@ export async function reason(ctx, query, d0, preConsumed, pre, voiced = []) {
39
39
  // broad structural gate; pinned by test/31-audit.
40
40
  const qId = pre.queryResolved;
41
41
  if (qId !== null && ctx.store.prevCount(qId) > 0) {
42
+ // THE ECHO GUARD, NAMED — the audit's point 1 could not attribute two real
43
+ // walks that stopped with no note at all, and this is where one of them went:
44
+ // the query IS a learnt continuation, so the reasoner returns the grounded
45
+ // read-out and never offers anything. Without this note the walk looks like
46
+ // an unexplained stop; with it, "no note" can only mean the offer was never
47
+ // asked. No decision changes: the guard returns exactly where it did.
48
+ ctx.trace?.step("echoGuard", [rItem(query, "query")], [], "the query is itself a learnt continuation — answering the grounded read-out without walking");
42
49
  return d0;
43
50
  }
44
51
  // Consume a node and its neighbours for pivot-cycle prevention — CAPPED at
@@ -198,8 +205,30 @@ export async function reason(ctx, query, d0, preConsumed, pre, voiced = []) {
198
205
  // fixpoint falls through to the pivot step instead. Offered with `moves`:
199
206
  // completing the answer's OWN learnt form is an identity step, not a claim
200
207
  // about the asker's material.
201
- if (curId !== null &&
202
- ctx.store.nextFirst(curId, bound).some((n) => !consumed.has(n))) {
208
+ // THE READ, KEPT — its LENGTH is free and it is the only instrument the
209
+ // bound has: a read that came back full may have been cut off, and a read
210
+ // that came back short did not. No second read is paid for this.
211
+ const outs = curId !== null ? ctx.store.nextFirst(curId, bound) : [];
212
+ const saturated = outs.length >= bound;
213
+ // THE RE-REACH, NAMED — the audit's structural-identity case (its test G), and
214
+ // the silent path it hid in. MEASURED, and this is the point's closure: across
215
+ // thirteen real questions on the trained store — the roteiro's ten plus three —
216
+ // this note fires ZERO times. The case, if it exists in this engine, is not on
217
+ // that material. It is not unimplementable, it is unobserved, and the audit's
218
+ // own rule is that an unobserved claim stays declared as unobserved: a
219
+ // distinction between legitimate convergence and a cycle cannot be implemented
220
+ // responsibly before it is seen, and the brief's section 13 puts measurement
221
+ // before code. What exists here is the instrument that will show it the moment
222
+ // it happens. When every successor of the current product has already
223
+ // been spoken for, the absorb above is skipped WITHOUT a word and the walk
224
+ // falls to the pivot: the same structure reached again is indistinguishable
225
+ // from having none. This note does not change the decision — it makes the
226
+ // state observable first, because the distinction between a legitimate
227
+ // convergence and a cycle cannot be implemented responsibly until it is seen.
228
+ if (curId !== null && outs.length > 0 && !outs.some((n) => !consumed.has(n))) {
229
+ ctx.trace?.step("reachAlreadySpokenFor", [rItem(cur, "answer")], [], `every continuation of this product (${outs.length}) has already been spoken for — the same structure, reached again`);
230
+ }
231
+ if (curId !== null && outs.some((n) => !consumed.has(n))) {
203
232
  const fwd = await follow(ctx, curId, qv);
204
233
  const fwdId = fwd !== null ? resolve(ctx, fwd) : null;
205
234
  if (fwd !== null && !bytesEqual(fwd, cur) &&
@@ -209,16 +238,55 @@ export async function reason(ctx, query, d0, preConsumed, pre, voiced = []) {
209
238
  pending = { kind: "absorb", cur, curId, fwd };
210
239
  return { product: fwd, contains: true, reaches: true, cost: STEP };
211
240
  }
241
+ // THE ABSORB'S OWN REFUSALS, TOLD APART — a learnt continuation existed
242
+ // and was declined, so the walk falls through to the pivot rather than
243
+ // ending. Which of the four reasons it was is exactly what the rationale
244
+ // could not say before: `null` at the end of the walk is one state, and
245
+ // this is another (AGENTS §6 — the note belongs where the mechanism emits
246
+ // it, not in a counter beside it).
247
+ const whyAbsorb = fwd === null
248
+ ? "the unconsumed edge led nowhere"
249
+ : bytesEqual(fwd, cur)
250
+ ? "the continuation reached was the product already in hand"
251
+ : fwdId !== null && consumed.has(fwdId)
252
+ ? "the continuation reached had already been spoken for"
253
+ : "this continuation only restates the question";
254
+ ctx.trace?.step("absorbCandidateRefused", [rItem(cur, "answer")], [], `${whyAbsorb} — falling through to the pivot`);
255
+ }
256
+ if (saturated) {
257
+ // THE BUDGET, NAMED — the audit's point 2. `nextFirst` is read with a cap
258
+ // (`hubBound`), so a saturated read may have hidden a continuation, and the
259
+ // walk would end as if none existed. The note says MAY, because the length
260
+ // cannot tell a full read from a truncated one; it can only tell that this
261
+ // is where a cut would have happened.
262
+ ctx.trace?.step("readBoundSaturated", [rItem(cur, "answer")], [], "the read came back full — a continuation may have been cut off by the bound");
212
263
  }
213
264
  // Pivot: the longest unconsumed learnt context the answer contains.
214
265
  consumeAll(curId);
215
266
  const pivot = await pivotInto(ctx, cur, consumed, voiced);
216
- if (pivot === null)
267
+ if (pivot === null) {
268
+ // THE ENUMERATION, DECLARED — the one duty the law cannot perform for the
269
+ // layer: only the layer knows that it has nothing left to offer. Before
270
+ // this note the walk ended in a bare `null`, indistinguishable from the
271
+ // causes below it; a reader of the rationale could not tell "exhausted"
272
+ // from "the candidate was refused" (AGENTS §6: a gap in instrumentation
273
+ // is a defect IN the instrumentation).
274
+ ctx.trace?.step("enumerationExhausted", [rItem(cur, "answer")], [], "the layer has no further candidate — its enumeration is exhausted");
217
275
  return null;
276
+ }
218
277
  const fc = await follow(ctx, pivot, qv);
219
278
  consumeAll(pivot);
220
279
  if (fc === null || bytesEqual(fc, cur) ||
221
280
  restates(query, fc, 0, { proper: true })) {
281
+ // THE THREE REFUSALS, TOLD APART — a candidate was reached and declined,
282
+ // which is NOT the same state as having none: the law reads both as "no
283
+ // continuation", but the rationale should not.
284
+ const why = fc === null
285
+ ? "the pivot's context carried no continuation"
286
+ : bytesEqual(fc, cur)
287
+ ? "the continuation reached was the product already in hand"
288
+ : "this continuation only restates the question";
289
+ ctx.trace?.step("pivotCandidateRefused", [rItem(cur, "answer")], [], `${why} — refused`);
222
290
  return null;
223
291
  }
224
292
  pending = { kind: "pivot", cur, pivot, fc };
@@ -273,6 +341,20 @@ export async function reason(ctx, query, d0, preConsumed, pre, voiced = []) {
273
341
  ctx.trace?.step("pivotRefused", [rItem(p.cur, "answer"), rItem(query, "query")], at.remainder.map(([a, b]) => rItem(query.subarray(a, b), "uncovered")), `the step carries none of the question material the grounding left ` +
274
342
  `unaccounted (${left} byte(s) in ${at.remainder.length} span(s)) — refused`);
275
343
  });
344
+ // THE WALK'S OWN ENDING, NAMED — the audit's point 1. Every stop INSIDE the
345
+ // offer now has a note, but a walk can also end without the offer ever being
346
+ // asked, or after a step the law took and then found nothing to follow: those
347
+ // ended as "no note at all", indistinguishable from a mechanism that never ran.
348
+ // Two endings cover every remaining case, and together they make a silent stop
349
+ // impossible — which is what the `Offer` contract needs, because `null` is read
350
+ // as exhaustion and exhaustion must be a statement, not an absence.
351
+ t ??= ctx.trace?.enter("reason", [rItem(startedFrom, "grounded")]);
352
+ if (closed_.remainder.length === 0) {
353
+ ctx.trace?.step("walkClosed", [rItem(closed_.product, "answer")], [], "the derivation is closed — nothing is left unaccounted for");
354
+ }
355
+ else {
356
+ ctx.trace?.step("walkEndedWithoutOffer", [rItem(closed_.product, "answer")], closed_.remainder.map(([a, b]) => rItem(query.subarray(a, b), "uncovered")), `the walk ended with ${unaccountedBytes(closed_.remainder)} byte(s) unaccounted and no further offer — no structural continuation`);
357
+ }
276
358
  // INSTRUMENTATION ONLY — the extension's two facts, untraced (meter.ts
277
359
  // contract 1: a counter never reaches a decision). They are what a caller
278
360
  // needs to PRICE the extension instead of taking it unconditionally, and both
@@ -95,7 +95,13 @@ export declare function hubCap<T>(ctx: MindContext, ids: readonly T[]): readonly
95
95
  /** Whether `descendant` lies within `ancestor`'s subtree — a structural DAG
96
96
  * relation read off the hash-consed `kids` lists, by a bounded explicit-stack
97
97
  * descent. Used by articulation to keep a voice from revoicing a fragment
98
- * OF that voice. */
98
+ * OF that voice.
99
+ *
100
+ * A HOMONYM, NOT A RELATIVE: the law's `Continuation.contains` (derivation.ts) is
101
+ * a producer's claim that its step IS a continuation, a boolean on one step. This
102
+ * is a structural relation between two nodes in the DAG. The shared word is
103
+ * deliberate vocabulary — "containment" is what both name — and the two must not be
104
+ * read as one. */
99
105
  export declare function contains(ctx: MindContext, ancestor: number, descendant: number): boolean;
100
106
  /** Whether a continuation edge joins the two forms, in either direction —
101
107
  * the EXACT half's veto on calling them synonyms.
@@ -495,7 +495,13 @@ export function hubCap(ctx, ids) {
495
495
  /** Whether `descendant` lies within `ancestor`'s subtree — a structural DAG
496
496
  * relation read off the hash-consed `kids` lists, by a bounded explicit-stack
497
497
  * descent. Used by articulation to keep a voice from revoicing a fragment
498
- * OF that voice. */
498
+ * OF that voice.
499
+ *
500
+ * A HOMONYM, NOT A RELATIVE: the law's `Continuation.contains` (derivation.ts) is
501
+ * a producer's claim that its step IS a continuation, a boolean on one step. This
502
+ * is a structural relation between two nodes in the DAG. The shared word is
503
+ * deliberate vocabulary — "containment" is what both name — and the two must not be
504
+ * read as one. */
499
505
  export function contains(ctx, ancestor, descendant) {
500
506
  if (ancestor === descendant)
501
507
  return true;
package/docs/INDEX.md CHANGED
@@ -36,7 +36,7 @@ proof in `test/` (pins that fail when the law is broken).
36
36
  | 11 | `docs/architecture/memoization.md` | `Precomputed` is per-response lazy cache (promise-cached async); `beginResponse`/`endResponse` lifecycle | `test/42` |
37
37
  | 12 | `docs/architecture/saturation.md` | Every walk names a deciding saturation beside its cap; cap is safety net, not decision | `test/27`, `test/16` |
38
38
  | 13 | `docs/architecture/meter.md` | `meter.ts` is write-only work accounting; counts are exact, phases nest | `test/55` |
39
- | 14 | `docs/architecture/closure.md` | A derivation is closed when its structure accounts for the question's remainder; every transition asks that law | `test/133`–`143` |
39
+ | 14 | `docs/architecture/closure.md` | A derivation is closed when its structure accounts for the question's remainder; every transition asks that law | `test/133`–`147` |
40
40
 
41
41
  ## Mechanisms (8)
42
42
 
@@ -15,4 +15,4 @@
15
15
  | 11 | Honest degradation | `src/mind/pipeline.ts:weight=moves+PASS*unaccounted` `src/store.ts:BoundedMap:miss→re-derive` | `test/28` `test/84` | `store.md`+`caches.md` |
16
16
  | 12 | Meter contracts | `src/meter.ts:Meter,PhaseCost,time` `src/mind/pipeline-mechanism.ts:Precomputed.shared` | `test/55` | `meter.md` |
17
17
  | 13 | Saturation | `traverse.ts:edgeAncestors,types.ts:SaturationReason` `src/mind/junction.ts:junctionContainersFrom` `src/mind/resonance.ts:pivotInto` | `test/27` `test/16` | `saturation.md` |
18
- | 14 | Closure | `src/mind/derivation.ts:closed,admissible,advance` | `test/133`–`143` | `closure.md` |
18
+ | 14 | Closure | `src/mind/derivation.ts:closed,admissible,advance` | `test/133`–`147` | `closure.md` |
@@ -76,3 +76,5 @@ vocabulary, not the tracer's.
76
76
  - `test/139` — carrying is ENGAGEMENT, not explanation.
77
77
  - `test/140` — irrelevant supply changes nothing.
78
78
  - `test/142`, `test/143` — the offer's contract, and the cycles.
79
+ - `test/144`, `test/146`, `test/147` — identity is content, the `contains` gate,
80
+ the witness ladder.
@@ -13,7 +13,7 @@ Guards honest silence, determinism, and every pinned contract. Silence:
13
13
  unrelated queries ground to nothing (`test/28`, `50`, `56`, `67`, `76`, `84`).
14
14
  Determinism: same seed + deposit order + query gives byte-identical answer
15
15
  (`test/20`). Every invariant is pinned, the closure law included
16
- (`test/133`–`143`). §14–25 (pipeline), §64 (derived thresholds), AGENTS.md §2
16
+ (`test/133`–`147`). §14–25 (pipeline), §64 (derived thresholds), AGENTS.md §2
17
17
  invariants 1–5.
18
18
 
19
19
  ## 2 — Work accounting (profiler)
package/jsr.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "$schema": "https://jsr.io/schema/config-file.v1.json",
3
3
  "name": "@hviana/sema",
4
- "version": "0.8.6",
4
+ "version": "0.8.7",
5
5
  "exports": "./src/index.ts"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hviana/sema",
3
- "version": "0.8.6",
3
+ "version": "0.8.7",
4
4
  "description": "Sema: a non-parametric, instance-based reasoning system.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -347,6 +347,11 @@ export interface ClimbConsensusData {
347
347
  };
348
348
 
349
349
  regions?: ConsensusRegionTrace[];
350
+ /** A NAME SHARED WITH THE LAW, AND NOT THE SAME THING: these are the reaches
351
+ * the consensus climb took, for the trace. The law's `Continuation.reaches`
352
+ * (derivation.ts) is a producer's claim that a step moves — different type,
353
+ * different owner, no relation. Kept distinct rather than renamed because the
354
+ * trace's own vocabulary is about the climb, not about derivation. */
350
355
  reaches?: ConsensusReachTrace[];
351
356
 
352
357
  crossRegion?: {
@@ -359,6 +359,15 @@ export function admissible(
359
359
  W: number,
360
360
  ): ReadonlyArray<Witness> | null {
361
361
  if (d.fixed) return null;
362
+ // THE GATE THAT NEVER FIRES, AND WHY IT STAYS. `contains` is the step's claim
363
+ // to BE a continuation of this product at all, and the formula is stated with
364
+ // it (see the doc above). Every producer in the engine declares it true — the
365
+ // three literals in reasoning.ts are constants, measured — so this line cannot
366
+ // reject anything today. It stays because the claim is what makes an `Offer`
367
+ // answerable: a layer that handed over something it does not call a
368
+ // continuation would be refused here rather than trusted. What it must NOT be
369
+ // is a place where a producer says "false" to end a walk: the walk ends by
370
+ // offering nothing (`null`), which is the contract.
362
371
  if (!t.contains) return null;
363
372
  if (closed(d)) return (t.explains ?? []).map((span) => ({ span }));
364
373
  // MOVES BEFORE CARRIES, and that order is not arbitrary: a transition that
@@ -429,14 +438,19 @@ export function advance(
429
438
  /** What a layer offers the law: the next continuation of a state, or null when
430
439
  * it has none. A layer OFFERS; the law disposes.
431
440
  *
432
- * ONE OFFER, AND IT IS THE LAYER'S LAST: {@link closure} stops when the law
433
- * refuses what was offered, so a refusal is read as "no continuation exists".
434
- * A layer must not offer candidates one at a time and expect the walk to
435
- * continue after a refusal — its own fallbacks belong inside this function.
436
- * The producers do exactly that: they choose between the forward absorb and the
437
- * pivot before offering, and return null only when neither exists, which is why
438
- * the one offer the law can still refuse (a pivot without ownership, whose
439
- * material the answer does not carry) really is the last one. */
441
+ * ONE OFFER AT A TIME: {@link closure} asks again only while the law admits what
442
+ * was offered, so a refusal ends the walk. A layer must not offer candidates
443
+ * one at a time and expect the walk to continue after a refusal — its own
444
+ * fallbacks belong inside this function, and the producers do exactly that:
445
+ * they choose between the forward absorb and the pivot before offering.
446
+ *
447
+ * WHAT `null` DOES NOT SAY: it is not a claim of exhaustion. A layer returns
448
+ * null for several distinct reasons — it exhausted its enumeration, it refused a
449
+ * candidate it had reached, or a read bound cut one off — and the law cannot tell
450
+ * them apart: it reads every one as "no continuation exists". The layer's reasons
451
+ * are therefore made OBSERVABLE where they happen, on the rationale's own channel
452
+ * (`enumerationExhausted`, `pivotCandidateRefused`, `absorbCandidateRefused`), and
453
+ * it is there — not here — that a reader learns which of them stopped the walk. */
440
454
  export type Offer = (d: DerivationState) => Promise<Continuation | null>;
441
455
 
442
456
  /** THE CLOSURE — the walk of {@link advance} over the continuations `offer`
@@ -664,6 +664,15 @@ export async function think(
664
664
  meter.postGroundingRemainderSpans += uncovered.length;
665
665
  meter.postGroundingRemainderBytes += unaccountedBytes(uncovered);
666
666
  }
667
+ // THE WALK THAT DOES NOT RUN, NAMED — the audit's point 1 could not attribute
668
+ // real questions that stopped with no note anywhere. They never reached the
669
+ // offer: `decided.complete` says the grounding supplied a fixed point, so the
670
+ // walk is skipped BY DESIGN and the state is the grounding's own. That state
671
+ // carries `fixed: true` (the trace already reports it), and the law's first
672
+ // clause refuses any continuation against it — so there is no `null` here to
673
+ // read as exhaustion, and the `Offer` contract is not in play at all. The
674
+ // silent stop is therefore a NAMED state, not a gap.
675
+ //
667
676
  // THE WALK CONSUMES AND RETURNS A STATE. It is handed the derivation's own —
668
677
  // the grounding's product, accounting, remainder and cost — and hands back the
669
678
  // state it advanced to, so what follows reads a state rather than bytes plus a
@@ -58,6 +58,18 @@ export async function reason(
58
58
  // broad structural gate; pinned by test/31-audit.
59
59
  const qId = pre.queryResolved;
60
60
  if (qId !== null && ctx.store.prevCount(qId) > 0) {
61
+ // THE ECHO GUARD, NAMED — the audit's point 1 could not attribute two real
62
+ // walks that stopped with no note at all, and this is where one of them went:
63
+ // the query IS a learnt continuation, so the reasoner returns the grounded
64
+ // read-out and never offers anything. Without this note the walk looks like
65
+ // an unexplained stop; with it, "no note" can only mean the offer was never
66
+ // asked. No decision changes: the guard returns exactly where it did.
67
+ ctx.trace?.step(
68
+ "echoGuard",
69
+ [rItem(query, "query")],
70
+ [],
71
+ "the query is itself a learnt continuation — answering the grounded read-out without walking",
72
+ );
61
73
  return d0;
62
74
  }
63
75
 
@@ -221,10 +233,37 @@ export async function reason(
221
233
  // fixpoint falls through to the pivot step instead. Offered with `moves`:
222
234
  // completing the answer's OWN learnt form is an identity step, not a claim
223
235
  // about the asker's material.
236
+ // THE READ, KEPT — its LENGTH is free and it is the only instrument the
237
+ // bound has: a read that came back full may have been cut off, and a read
238
+ // that came back short did not. No second read is paid for this.
239
+ const outs = curId !== null ? ctx.store.nextFirst(curId, bound) : [];
240
+ const saturated = outs.length >= bound;
241
+ // THE RE-REACH, NAMED — the audit's structural-identity case (its test G), and
242
+ // the silent path it hid in. MEASURED, and this is the point's closure: across
243
+ // thirteen real questions on the trained store — the roteiro's ten plus three —
244
+ // this note fires ZERO times. The case, if it exists in this engine, is not on
245
+ // that material. It is not unimplementable, it is unobserved, and the audit's
246
+ // own rule is that an unobserved claim stays declared as unobserved: a
247
+ // distinction between legitimate convergence and a cycle cannot be implemented
248
+ // responsibly before it is seen, and the brief's section 13 puts measurement
249
+ // before code. What exists here is the instrument that will show it the moment
250
+ // it happens. When every successor of the current product has already
251
+ // been spoken for, the absorb above is skipped WITHOUT a word and the walk
252
+ // falls to the pivot: the same structure reached again is indistinguishable
253
+ // from having none. This note does not change the decision — it makes the
254
+ // state observable first, because the distinction between a legitimate
255
+ // convergence and a cycle cannot be implemented responsibly until it is seen.
224
256
  if (
225
- curId !== null &&
226
- ctx.store.nextFirst(curId, bound).some((n) => !consumed.has(n))
257
+ curId !== null && outs.length > 0 && !outs.some((n) => !consumed.has(n))
227
258
  ) {
259
+ ctx.trace?.step(
260
+ "reachAlreadySpokenFor",
261
+ [rItem(cur, "answer")],
262
+ [],
263
+ `every continuation of this product (${outs.length}) has already been spoken for — the same structure, reached again`,
264
+ );
265
+ }
266
+ if (curId !== null && outs.some((n) => !consumed.has(n))) {
228
267
  const fwd = await follow(ctx, curId, qv);
229
268
  const fwdId = fwd !== null ? resolve(ctx, fwd) : null;
230
269
  if (
@@ -236,18 +275,79 @@ export async function reason(
236
275
  pending = { kind: "absorb", cur, curId, fwd };
237
276
  return { product: fwd, contains: true, reaches: true, cost: STEP };
238
277
  }
278
+ // THE ABSORB'S OWN REFUSALS, TOLD APART — a learnt continuation existed
279
+ // and was declined, so the walk falls through to the pivot rather than
280
+ // ending. Which of the four reasons it was is exactly what the rationale
281
+ // could not say before: `null` at the end of the walk is one state, and
282
+ // this is another (AGENTS §6 — the note belongs where the mechanism emits
283
+ // it, not in a counter beside it).
284
+ const whyAbsorb = fwd === null
285
+ ? "the unconsumed edge led nowhere"
286
+ : bytesEqual(fwd, cur)
287
+ ? "the continuation reached was the product already in hand"
288
+ : fwdId !== null && consumed.has(fwdId)
289
+ ? "the continuation reached had already been spoken for"
290
+ : "this continuation only restates the question";
291
+ ctx.trace?.step(
292
+ "absorbCandidateRefused",
293
+ [rItem(cur, "answer")],
294
+ [],
295
+ `${whyAbsorb} — falling through to the pivot`,
296
+ );
297
+ }
298
+
299
+ if (saturated) {
300
+ // THE BUDGET, NAMED — the audit's point 2. `nextFirst` is read with a cap
301
+ // (`hubBound`), so a saturated read may have hidden a continuation, and the
302
+ // walk would end as if none existed. The note says MAY, because the length
303
+ // cannot tell a full read from a truncated one; it can only tell that this
304
+ // is where a cut would have happened.
305
+ ctx.trace?.step(
306
+ "readBoundSaturated",
307
+ [rItem(cur, "answer")],
308
+ [],
309
+ "the read came back full — a continuation may have been cut off by the bound",
310
+ );
239
311
  }
240
312
 
241
313
  // Pivot: the longest unconsumed learnt context the answer contains.
242
314
  consumeAll(curId);
243
315
  const pivot = await pivotInto(ctx, cur, consumed, voiced);
244
- if (pivot === null) return null;
316
+ if (pivot === null) {
317
+ // THE ENUMERATION, DECLARED — the one duty the law cannot perform for the
318
+ // layer: only the layer knows that it has nothing left to offer. Before
319
+ // this note the walk ended in a bare `null`, indistinguishable from the
320
+ // causes below it; a reader of the rationale could not tell "exhausted"
321
+ // from "the candidate was refused" (AGENTS §6: a gap in instrumentation
322
+ // is a defect IN the instrumentation).
323
+ ctx.trace?.step(
324
+ "enumerationExhausted",
325
+ [rItem(cur, "answer")],
326
+ [],
327
+ "the layer has no further candidate — its enumeration is exhausted",
328
+ );
329
+ return null;
330
+ }
245
331
  const fc = await follow(ctx, pivot, qv);
246
332
  consumeAll(pivot);
247
333
  if (
248
334
  fc === null || bytesEqual(fc, cur) ||
249
335
  restates(query, fc, 0, { proper: true })
250
336
  ) {
337
+ // THE THREE REFUSALS, TOLD APART — a candidate was reached and declined,
338
+ // which is NOT the same state as having none: the law reads both as "no
339
+ // continuation", but the rationale should not.
340
+ const why = fc === null
341
+ ? "the pivot's context carried no continuation"
342
+ : bytesEqual(fc, cur)
343
+ ? "the continuation reached was the product already in hand"
344
+ : "this continuation only restates the question";
345
+ ctx.trace?.step(
346
+ "pivotCandidateRefused",
347
+ [rItem(cur, "answer")],
348
+ [],
349
+ `${why} — refused`,
350
+ );
251
351
  return null;
252
352
  }
253
353
  pending = { kind: "pivot", cur, pivot, fc };
@@ -324,6 +424,34 @@ export async function reason(
324
424
  },
325
425
  );
326
426
 
427
+ // THE WALK'S OWN ENDING, NAMED — the audit's point 1. Every stop INSIDE the
428
+ // offer now has a note, but a walk can also end without the offer ever being
429
+ // asked, or after a step the law took and then found nothing to follow: those
430
+ // ended as "no note at all", indistinguishable from a mechanism that never ran.
431
+ // Two endings cover every remaining case, and together they make a silent stop
432
+ // impossible — which is what the `Offer` contract needs, because `null` is read
433
+ // as exhaustion and exhaustion must be a statement, not an absence.
434
+ t ??= ctx.trace?.enter("reason", [rItem(startedFrom, "grounded")]);
435
+ if (closed_.remainder.length === 0) {
436
+ ctx.trace?.step(
437
+ "walkClosed",
438
+ [rItem(closed_.product, "answer")],
439
+ [],
440
+ "the derivation is closed — nothing is left unaccounted for",
441
+ );
442
+ } else {
443
+ ctx.trace?.step(
444
+ "walkEndedWithoutOffer",
445
+ [rItem(closed_.product, "answer")],
446
+ closed_.remainder.map(([a, b]) =>
447
+ rItem(query.subarray(a, b), "uncovered")
448
+ ),
449
+ `the walk ended with ${
450
+ unaccountedBytes(closed_.remainder)
451
+ } byte(s) unaccounted and no further offer — no structural continuation`,
452
+ );
453
+ }
454
+
327
455
  // INSTRUMENTATION ONLY — the extension's two facts, untraced (meter.ts
328
456
  // contract 1: a counter never reaches a decision). They are what a caller
329
457
  // needs to PRICE the extension instead of taking it unconditionally, and both
@@ -563,7 +563,13 @@ export function hubCap<T>(
563
563
  /** Whether `descendant` lies within `ancestor`'s subtree — a structural DAG
564
564
  * relation read off the hash-consed `kids` lists, by a bounded explicit-stack
565
565
  * descent. Used by articulation to keep a voice from revoicing a fragment
566
- * OF that voice. */
566
+ * OF that voice.
567
+ *
568
+ * A HOMONYM, NOT A RELATIVE: the law's `Continuation.contains` (derivation.ts) is
569
+ * a producer's claim that its step IS a continuation, a boolean on one step. This
570
+ * is a structural relation between two nodes in the DAG. The shared word is
571
+ * deliberate vocabulary — "containment" is what both name — and the two must not be
572
+ * read as one. */
567
573
  export function contains(
568
574
  ctx: MindContext,
569
575
  ancestor: number,
@@ -17,7 +17,7 @@ import { test } from "node:test";
17
17
  import assert from "node:assert/strict";
18
18
  import { Mind } from "../dist/src/index.js";
19
19
  import { SQliteStore } from "../dist/src/store-sqlite.js";
20
- import { admissible } from "../dist/src/mind/derivation.js";
20
+ import { admissible, closure } from "../dist/src/mind/derivation.js";
21
21
 
22
22
  const QUERY = new TextEncoder().encode("ABCDEFGHIJ");
23
23
  const W = 4;
@@ -91,3 +91,65 @@ test("142.2 the walk ends at the refusal, with the satisfying answer untouched",
91
91
  "the layer's last offer was refused, and the answer stands",
92
92
  );
93
93
  });
94
+
95
+ // B OF THE AUDIT'S TEN: what a refusal MEANS at the walk's boundary.
96
+ //
97
+ // The walk asks ONCE per round and stops when the law refuses it — it does not ask
98
+ // again. That is the load-bearing half of the `Offer` contract: a refusal is read
99
+ // as "no continuation exists", and it is why the layer must offer only what the law
100
+ // will admit (model X, pinned by 142.1 and 142.2). A layer that offered candidates
101
+ // for the law to filter would be wrong here, and its own fallbacks must therefore be
102
+ // INTERNAL to one offer call — which is what the engine's layer does.
103
+ //
104
+ // WHAT THIS DOES NOT SETTLE, and must not be read as settling: the walk reads every
105
+ // reason the layer stopped as exhaustion, and the audit found six of them (exhausted,
106
+ // candidate refused, cycle, read bound, no structure, layer done). `enumerationExhausted`
107
+ // in reasoning.ts now makes that claim observable; proving the claim TRUE is the open
108
+ // decision the audit left (its section 18.6), not something this test asserts.
109
+ test("142.3 a refusal ends the walk, and is not retried", async () => {
110
+ const enc = (t) => new TextEncoder().encode(t);
111
+ // Refused on the law's own ground; admitted for carrying a window (both from 142.1).
112
+ const REFUSED = { product: enc("Paris"), contains: true, cost: 1 };
113
+ const ADMITTED = { product: enc("ZZZZABCDZZ"), contains: true, cost: 1 };
114
+
115
+ // SANITY FIRST: the literals must be what this test says they are, or the walk
116
+ // below would end for another reason and pin nothing.
117
+ assert.equal(
118
+ admissible(state(), REFUSED, QUERY, W),
119
+ null,
120
+ "REFUSED is refused by the law (as in 142.1)",
121
+ );
122
+ assert.notEqual(
123
+ admissible(state(), ADMITTED, QUERY, W),
124
+ null,
125
+ "ADMITTED carries a window, so the law admits it (as in 142.1)",
126
+ );
127
+
128
+ const queued = [REFUSED, ADMITTED];
129
+ const offered = [];
130
+ const offer = async () => {
131
+ const next = queued.length > 0 ? queued.shift() : null;
132
+ offered.push(next);
133
+ return next;
134
+ };
135
+ const taken = [];
136
+ await closure(
137
+ state(),
138
+ QUERY,
139
+ W,
140
+ offer,
141
+ (_before, after) => taken.push(after),
142
+ );
143
+
144
+ assert.equal(
145
+ offered.length,
146
+ 1,
147
+ "the law is asked once: a refusal is not retried",
148
+ );
149
+ assert.equal(
150
+ taken.length,
151
+ 0,
152
+ "nothing was taken — the walk ended at the refusal",
153
+ );
154
+ assert.equal(queued.length, 1, "the second candidate was never offered");
155
+ });
@@ -0,0 +1,55 @@
1
+ // 144-identity-is-content.test.mjs — two nodes with the same payload do not exist.
2
+ //
3
+ // WHAT IS PINNED. Two of the audit's ten discriminating tests look like identity
4
+ // gaps and are not: F ("same payload in two nodes") and G ("the same structure by
5
+ // different paths"). The store is content-addressed, so the same text deposited
6
+ // twice adds NO structure at all, and the second deposit resolves to the very node
7
+ // the first one interned. That is measured here, on the public surface, rather
8
+ // than argued.
9
+ //
10
+ // WHY IT MATTERS FOR THE LAW. The layer's cycle control is a set of NODE IDS, so
11
+ // if identical content could live at two ids, the set would miss the second — which
12
+ // is what the audit suspected. It cannot: identity IS the content. The consequence
13
+ // is the audit's answer to G — "the same structure by another path" is the same
14
+ // node, and calling its re-reach a cycle is correct, because that node's content was
15
+ // already accounted for.
16
+ //
17
+ // The measurement also carries its own sanity: the first deposit MUST move the
18
+ // count, or the probe is measuring the wrong quantity and proves nothing.
19
+
20
+ import { test } from "node:test";
21
+ import assert from "node:assert/strict";
22
+ import { mkdtempSync, rmSync } from "node:fs";
23
+ import { tmpdir } from "node:os";
24
+ import { join } from "node:path";
25
+ import { Mind, SQliteStore } from "../dist/src/index.js";
26
+
27
+ test("144.1 depositing the same text twice adds no structure", async () => {
28
+ const dir = mkdtempSync(join(tmpdir(), "sema-144-"));
29
+ const stem = join(dir, "store");
30
+ const store = new SQliteStore({ path: stem });
31
+ try {
32
+ const mind = new Mind({ seed: 7, store });
33
+ const before = store.edgeSourceCount();
34
+ await mind.ingest("a b c");
35
+ const once = store.edgeSourceCount();
36
+ await mind.ingest("a b c");
37
+ const twice = store.edgeSourceCount();
38
+
39
+ // SANITY FIRST: the quantity has to be one the deposit moves, otherwise the
40
+ // assertion below is true for the wrong reason and pins nothing.
41
+ assert.ok(
42
+ once > before,
43
+ `the first deposit must add structure (was ${before}, now ${once}) — ` +
44
+ `if it does not, this test is measuring the wrong quantity`,
45
+ );
46
+ assert.equal(
47
+ twice,
48
+ once,
49
+ "the same text deposited twice must not add structure: identity IS content",
50
+ );
51
+ } finally {
52
+ await store.close();
53
+ rmSync(dir, { recursive: true, force: true });
54
+ }
55
+ });
@@ -0,0 +1,58 @@
1
+ // 145-the-instrument-answers.test.mjs — a measurement must move what it claims to measure.
2
+ //
3
+ // WHY THIS FILE EXISTS. During the derivation audit four probes were run and four times the
4
+ // instrument, not the engine, was wrong: a local import map that could not resolve the engine's
5
+ // types; a call I believed deposited text and deposited nothing; a trace note that never fired
6
+ // because the corpus did not reach it; and a two-hop question that grounded on one hop. Every
7
+ // time the output looked like a result. The guards that caught them lived inside the probes,
8
+ // and a probe is thrown away — so the two quantities this suite measures with are pinned here,
9
+ // where the next person will find them (AGENTS 6: close the gap in the instrumentation, once).
10
+ //
11
+ // WHAT IS PINNED. `edgeSourceCount()` must MOVE when a deposit adds structure, and must NOT
12
+ // move when the same text is deposited again. Positive and negative together are what make it
13
+ // an instrument: the first says it is alive, the second says it discriminates.
14
+
15
+ import { test } from "node:test";
16
+ import assert from "node:assert/strict";
17
+ import { mkdtempSync, rmSync } from "node:fs";
18
+ import { tmpdir } from "node:os";
19
+ import { join } from "node:path";
20
+ import { Mind, SQliteStore } from "../dist/src/index.js";
21
+
22
+ async function withStore(fn) {
23
+ const dir = mkdtempSync(join(tmpdir(), "sema-145-"));
24
+ const store = new SQliteStore({ path: join(dir, "store") });
25
+ try {
26
+ return await fn(new Mind({ seed: 7, store }), store);
27
+ } finally {
28
+ await store.close();
29
+ rmSync(dir, { recursive: true, force: true });
30
+ }
31
+ }
32
+
33
+ test("145.1 the instrument is alive: a deposit moves the edge-source count", async () => {
34
+ await withStore(async (mind, store) => {
35
+ const before = store.edgeSourceCount();
36
+ await mind.ingest("a b c");
37
+ const after = store.edgeSourceCount();
38
+ assert.ok(
39
+ after > before,
40
+ `a deposit must move the count (was ${before}, now ${after}) — if it does not, ` +
41
+ `every probe that reads this number measures the wrong quantity`,
42
+ );
43
+ });
44
+ });
45
+
46
+ test("145.2 the instrument discriminates: the same text moves nothing", async () => {
47
+ await withStore(async (mind, store) => {
48
+ await mind.ingest("a b c");
49
+ const once = store.edgeSourceCount();
50
+ await mind.ingest("a b c");
51
+ assert.equal(
52
+ store.edgeSourceCount(),
53
+ once,
54
+ "the same text again must not move the count — otherwise the number cannot tell " +
55
+ "new structure from repeated text, and F of the audit would be unmeasurable",
56
+ );
57
+ });
58
+ });
@@ -0,0 +1,67 @@
1
+ // 146-the-contains-gate.test.mjs — `contains`, the one gate no producer ever closes.
2
+ //
3
+ // WHAT IS PINNED. The formula above `admissible` is written with CONTAINS as a term, and the
4
+ // engine's three producers all declare it true (measured: the only `contains:` literals in src/
5
+ // are reasoning.ts's absorb, pivot and fusion — all constants). So the gate cannot reject
6
+ // anything today, and this file makes that state explicit rather than accidental:
7
+ //
8
+ // • the clause WORKS when it is told false — the law is not ignoring the field;
9
+ // • and a layer that wants to END a walk does it by offering nothing (`null`), not by declaring
10
+ // a step that is not a continuation.
11
+ //
12
+ // The second half is the contract; the first is the proof that the field is real. A gate that
13
+ // neither fires nor is exercised is indistinguishable from a field that was forgotten.
14
+
15
+ import { test } from "node:test";
16
+ import assert from "node:assert/strict";
17
+ import { admissible } from "../dist/src/mind/derivation.js";
18
+
19
+ const QUERY = new TextEncoder().encode("ABCDEFGHIJ");
20
+ const W = 4;
21
+ const state = () => ({
22
+ product: new Uint8Array(0),
23
+ accounted: [],
24
+ remainder: [[0, 10]],
25
+ cost: 0,
26
+ });
27
+
28
+ test("146.1 the contains clause refuses when it is told false", () => {
29
+ const admits = {
30
+ product: new TextEncoder().encode("ZZZZABCDZZ"),
31
+ contains: true,
32
+ cost: 1,
33
+ };
34
+ assert.notEqual(
35
+ admissible(state(), admits, QUERY, W),
36
+ null,
37
+ "with contains true and a window held, the law admits",
38
+ );
39
+ assert.equal(
40
+ admissible(state(), { ...admits, contains: false }, QUERY, W),
41
+ null,
42
+ "with contains false the law refuses, whatever else the step carries — " +
43
+ "so the field is read, not decorative",
44
+ );
45
+ });
46
+
47
+ test("146.2 contains false is not how a walk ends", async () => {
48
+ // The contract's own words, in the law's doc: a layer ends a walk by offering
49
+ // nothing. A step that declares itself not-a-continuation is refused, which is
50
+ // what makes the declaration meaningful rather than a way to stop silently.
51
+ const step = {
52
+ product: new TextEncoder().encode("ZZZZABCDZZ"),
53
+ contains: true,
54
+ reaches: true,
55
+ cost: 1,
56
+ };
57
+ assert.notEqual(
58
+ admissible(state(), step, QUERY, W),
59
+ null,
60
+ "a declared move is admitted",
61
+ );
62
+ assert.equal(
63
+ admissible(state(), { ...step, contains: false }, QUERY, W),
64
+ null,
65
+ "and the same move with contains false is refused — the claim gates everything",
66
+ );
67
+ });
@@ -0,0 +1,76 @@
1
+ // 147-the-witness-ladder.test.mjs — consumed ⊆ carried ⊆ accounted, non-vacuously.
2
+ //
3
+ // WHY THIS FILE EXISTS. The audit's point 5 (E, partial) asks whether the accounting ever exceeds
4
+ // the window: the consumed window is where the remainder drains, the accounting registers the
5
+ // span, and `span != window` is the established reading. Measured on the real corpus, the three
6
+ // counter families are ZERO on every roteiro question — the answers there come from grounding and
7
+ // derive-through, not from the reasoner's own extensions — and a ladder of zeroes proves nothing.
8
+ // So the ladder is pinned where the values cannot be zero: on a state built for the purpose.
9
+ //
10
+ // WHAT IS PINNED. Given a step whose product carries a window of what is still owed:
11
+ // • the law admits it and the witness carries a SPAN and a WINDOW (the two readings);
12
+ // • the remainder shrinks by AT MOST the window — the consumed set is a subset of the carried one;
13
+ // • and the accounting grows by AT LEAST the window — nothing consumed goes unaccounted.
14
+ // The sanity assertion is part of the pin: the window must be non-zero, or the two inequalities
15
+ // above hold for the wrong reason.
16
+
17
+ import { test } from "node:test";
18
+ import assert from "node:assert/strict";
19
+ import { admissible, advance } from "../dist/src/mind/derivation.js";
20
+
21
+ const QUERY = new TextEncoder().encode("ABCDEFGHIJ"); // W = 4 ⇒ one window is "ABCD"
22
+ const W = 4;
23
+ const bytesOf = (spans) => spans.reduce((n, [a, b]) => n + (b - a), 0);
24
+ const state = () => ({
25
+ product: new Uint8Array(0),
26
+ accounted: [],
27
+ remainder: [[0, 10]],
28
+ cost: 0,
29
+ });
30
+
31
+ test("147.1 consumed ⊆ carried ⊆ accounted, with a window that is not zero", () => {
32
+ const step = {
33
+ product: new TextEncoder().encode("ZZZZABCDZZ"),
34
+ contains: true,
35
+ cost: 1,
36
+ };
37
+ const before = state();
38
+ const witnesses = admissible(before, step, QUERY, W);
39
+ assert.notEqual(
40
+ witnesses,
41
+ null,
42
+ "the law admits a step that carries a window",
43
+ );
44
+
45
+ const windows = witnesses.filter((x) => x.window !== undefined);
46
+ assert.ok(
47
+ windows.length > 0,
48
+ "SANITY: at least one witness must carry a window, or the inequalities below are vacuous",
49
+ );
50
+ const windowBytes = windows.reduce(
51
+ (n, x) => n + (x.window[1] - x.window[0]),
52
+ 0,
53
+ );
54
+ assert.ok(
55
+ windowBytes > 0,
56
+ `SANITY: the window must be non-zero (got ${windowBytes})`,
57
+ );
58
+
59
+ const after = advance(before, step, witnesses);
60
+ const spanBytes = bytesOf(witnesses.map((x) => x.span));
61
+ const drained = bytesOf(before.remainder) - bytesOf(after.remainder);
62
+ const accounted = bytesOf(after.accounted) - bytesOf(before.accounted);
63
+
64
+ assert.ok(
65
+ drained <= windowBytes,
66
+ `a step drains AT MOST what it carries: drained ${drained} > window ${windowBytes}`,
67
+ );
68
+ assert.ok(
69
+ accounted >= windowBytes,
70
+ `nothing consumed goes unaccounted: accounted ${accounted} < window ${windowBytes}`,
71
+ );
72
+ assert.ok(
73
+ spanBytes > 0,
74
+ "and the accounting is a span, which is why it may exceed the window",
75
+ );
76
+ });