@hviana/sema 0.8.5 → 0.8.6

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.
@@ -199,15 +199,30 @@ export declare class Meter {
199
199
  * not computable at all. With it, the price of extending the answer is
200
200
  * `reasonSteps · STEP`, the ladder's own value for following an edge. */
201
201
  reasonSteps: number;
202
- /** Bytes of the grounding's UNCOVERED material the extension was justified by
203
- * — the union of the spans each step carried a `W`-window of. The gate
204
- * already computed WHICH span carried it per step and kept only a boolean;
205
- * this is that fact, accumulated. Read with {@link reasonSteps}: one is the
206
- * price, the other the explanation. */
202
+ /** Bytes the admitted steps ACCOUNTED for — the witnesses' spans, summed by the
203
+ * law's own function over the tail of the state's accounting. It is NOT what
204
+ * they carried: a step admitted by `reaches` may declare spans it holds no
205
+ * window of, and then it accounts without carrying and consumes nothing (see
206
+ * {@link closureDrainedBytes}). Read with {@link reasonSteps}: one is the
207
+ * price, the other the accounting. */
208
+ reasonAccountedBytes: number;
209
+ /** Bytes of the QUESTION's own material an admitted step CARRIED — the windows
210
+ * of the remainder its product holds, read by the law's one reading. The
211
+ * accounting may be larger (a `reaches` step declares spans it holds no window
212
+ * of) and the consumption is the window of a step that also reaches, so the
213
+ * three form a lattice: consumed ⊆ carried ⊆ accounted. */
207
214
  reasonCarriedBytes: number;
215
+ /** Times the layer OFFERED a continuation to the law. Read with
216
+ * {@link lawRejects}: the offers the law refused are the Model X contract being
217
+ * exercised, and `offerRuns - lawRejects` is the accepted transitions. */
218
+ offerRuns: number;
219
+ /** Times the law REFUSED the continuation the layer offered. A refusal is
220
+ * terminal by the layer's contract, so this counts the moments the contract
221
+ * mattered — zero means the walk never needed it. */
222
+ lawRejects: number;
208
223
  /** Bytes of the question's REMAINDER a step CONSUMED — the drop the law's own
209
224
  * `advance` makes when a declared move carries the material it accounts for.
210
- * Read with {@link reasonSteps} and {@link reasonCarriedBytes}: carrying is
225
+ * Read with {@link reasonSteps} and {@link reasonAccountedBytes}: carrying is
211
226
  * the engagement, this is the consumption, and before it the second was
212
227
  * invisible. */
213
228
  closureDrainedBytes: number;
package/dist/src/meter.js CHANGED
@@ -209,15 +209,30 @@ export class Meter {
209
209
  * not computable at all. With it, the price of extending the answer is
210
210
  * `reasonSteps · STEP`, the ladder's own value for following an edge. */
211
211
  reasonSteps = 0;
212
- /** Bytes of the grounding's UNCOVERED material the extension was justified by
213
- * — the union of the spans each step carried a `W`-window of. The gate
214
- * already computed WHICH span carried it per step and kept only a boolean;
215
- * this is that fact, accumulated. Read with {@link reasonSteps}: one is the
216
- * price, the other the explanation. */
212
+ /** Bytes the admitted steps ACCOUNTED for — the witnesses' spans, summed by the
213
+ * law's own function over the tail of the state's accounting. It is NOT what
214
+ * they carried: a step admitted by `reaches` may declare spans it holds no
215
+ * window of, and then it accounts without carrying and consumes nothing (see
216
+ * {@link closureDrainedBytes}). Read with {@link reasonSteps}: one is the
217
+ * price, the other the accounting. */
218
+ reasonAccountedBytes = 0;
219
+ /** Bytes of the QUESTION's own material an admitted step CARRIED — the windows
220
+ * of the remainder its product holds, read by the law's one reading. The
221
+ * accounting may be larger (a `reaches` step declares spans it holds no window
222
+ * of) and the consumption is the window of a step that also reaches, so the
223
+ * three form a lattice: consumed ⊆ carried ⊆ accounted. */
217
224
  reasonCarriedBytes = 0;
225
+ /** Times the layer OFFERED a continuation to the law. Read with
226
+ * {@link lawRejects}: the offers the law refused are the Model X contract being
227
+ * exercised, and `offerRuns - lawRejects` is the accepted transitions. */
228
+ offerRuns = 0;
229
+ /** Times the law REFUSED the continuation the layer offered. A refusal is
230
+ * terminal by the layer's contract, so this counts the moments the contract
231
+ * mattered — zero means the walk never needed it. */
232
+ lawRejects = 0;
218
233
  /** Bytes of the question's REMAINDER a step CONSUMED — the drop the law's own
219
234
  * `advance` makes when a declared move carries the material it accounts for.
220
- * Read with {@link reasonSteps} and {@link reasonCarriedBytes}: carrying is
235
+ * Read with {@link reasonSteps} and {@link reasonAccountedBytes}: carrying is
221
236
  * the engagement, this is the consumption, and before it the second was
222
237
  * invisible. */
223
238
  closureDrainedBytes = 0;
@@ -196,6 +196,6 @@ export type Offer = (d: DerivationState) => Promise<Continuation | null>;
196
196
  * Nothing here counts steps, and nothing here decides admissibility. */
197
197
  export declare function closure(d: DerivationState, query: Uint8Array, W: number, offer: Offer,
198
198
  /** Called for each step the law admits, with the state before and after. */
199
- onTaken?: (before: DerivationState, after: DerivationState) => void,
199
+ onTaken?: (before: DerivationState, after: DerivationState, witnesses: ReadonlyArray<Witness>) => void,
200
200
  /** Called when the law refused the continuation the layer offered. */
201
201
  onRefused?: (at: DerivationState) => void): Promise<DerivationState>;
@@ -321,7 +321,7 @@ onRefused) {
321
321
  return d;
322
322
  }
323
323
  const next = advance(d, t, explains);
324
- onTaken?.(d, next);
324
+ onTaken?.(d, next, explains);
325
325
  d = next;
326
326
  }
327
327
  }
@@ -183,6 +183,8 @@ export async function reason(ctx, query, d0, preConsumed, pre, voiced = []) {
183
183
  // admitted.
184
184
  let pending = null;
185
185
  const offer = async (d) => {
186
+ if (ctx.meter)
187
+ ctx.meter.offerRuns += 1;
186
188
  const cur = d.product;
187
189
  // The first step's `cur` IS the grounding's product, so the guard above
188
190
  // already resolved it and read its reverse edges — reuse both.
@@ -232,8 +234,9 @@ export async function reason(ctx, query, d0, preConsumed, pre, voiced = []) {
232
234
  cost: STEP,
233
235
  };
234
236
  };
235
- const closed_ = await closure(d0, query, W, offer, (before, after) => {
237
+ const closed_ = await closure(d0, query, W, offer, (before, after, witnesses) => {
236
238
  if (ctx.meter) {
239
+ ctx.meter.reasonCarriedBytes += witnesses.reduce((n, w) => n + (w.window ? w.window[1] - w.window[0] : 0), 0);
237
240
  ctx.meter.closureDrainedBytes += unaccountedBytes(before.remainder) -
238
241
  unaccountedBytes(after.remainder);
239
242
  }
@@ -251,6 +254,10 @@ export async function reason(ctx, query, d0, preConsumed, pre, voiced = []) {
251
254
  ctx.trace?.step("pivotStep", [rItem(p.cur, "answer"), rNode(ctx, p.pivot, "pivot")], [rItem(p.fc, "answer", resolve(ctx, p.fc) ?? undefined)], "pivot on the shared span this answer contains, then step forward across that fact");
252
255
  }
253
256
  }, (at) => {
257
+ // THE LAW REFUSED — counted where it happened, before the bookkeeping
258
+ // below decides whether there is a step to report.
259
+ if (ctx.meter)
260
+ ctx.meter.lawRejects += 1;
254
261
  const p = pending;
255
262
  pending = null;
256
263
  if (p === null || p.kind !== "pivot")
@@ -279,7 +286,7 @@ export async function reason(ctx, query, d0, preConsumed, pre, voiced = []) {
279
286
  // over the tail of the state's list — and computed ONLY when a meter is
280
287
  // attached: this used to slice and MAP a fresh array on every response, for a
281
288
  // counter that usually does not exist. One allocation, under the meter.
282
- ctx.meter.reasonCarriedBytes += unaccountedBytes(closed_.accounted.slice(d0.accounted.length));
289
+ ctx.meter.reasonAccountedBytes += unaccountedBytes(closed_.accounted.slice(d0.accounted.length));
283
290
  }
284
291
  t?.done([rItem(closed_.product, "answer", resolve(ctx, closed_.product) ?? undefined)],
285
292
  // A FIXPOINT: no further step was offered, or the law refused the one that
@@ -489,6 +496,7 @@ primarySpans = []) {
489
496
  return { ...state, product: out };
490
497
  const fused = advance(state, fusedT, fusedWitness);
491
498
  if (ctx.meter) {
499
+ ctx.meter.reasonCarriedBytes += fusedWitness.reduce((n, w) => n + (w.window ? w.window[1] - w.window[0] : 0), 0);
492
500
  ctx.meter.closureDrainedBytes += unaccountedBytes(state.remainder) -
493
501
  unaccountedBytes(fused.remainder);
494
502
  }
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`–`140` |
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` |
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`–`140` | `closure.md` |
18
+ | 14 | Closure | `src/mind/derivation.ts:closed,admissible,advance` | `test/133`–`143` | `closure.md` |
@@ -23,6 +23,14 @@ field, no producer field, and no count of any kind. The witnesses `contains` and
23
23
  `moves` are the LAYER's: it holds the structure and hands them in, so the law
24
24
  never probes the store.
25
25
 
26
+ ## The boundary
27
+
28
+ `product` (what it stands on, its identity being `resolve(product)`),
29
+ `accounted` (the price its steps summed) and `remainder` (the question's debt,
30
+ at one quantum) are the derivation's own. What it REACHED and what it SPENT are
31
+ the layer's (`reaches`, `consumed`), never a field here: the law decides
32
+ admission and consumption, the layer decides frontier and termination.
33
+
26
34
  ## The transition
27
35
 
28
36
  `admissible(state, continuation, query, W)` returns the witnesses the step pays
@@ -33,6 +41,10 @@ consumes what its window proves. Whole-span draining was refuted by `test/110`,
33
41
  the window alone by `test/138`. The state is BORN owing what its product does
34
42
  not carry.
35
43
 
44
+ The walk ends when the layer stops offering or the law refuses; owning no
45
+ search, the law cannot prevent a cycle — that is the layer's, over the structure
46
+ it holds.
47
+
36
48
  ## One cost home
37
49
 
38
50
  `moves + PASS · unaccountedBytes` is computed in ONE place (`pipeline.ts`,
@@ -60,6 +72,7 @@ vocabulary, not the tracer's.
60
72
  ## Pins
61
73
 
62
74
  - `test/133`–`137` — the law's home, readings, refusal, limits.
63
- - `test/138` — a cycle cannot close it; a carried move can.
75
+ - `test/138` — a cycle cannot close it, a carried move can.
64
76
  - `test/139` — carrying is ENGAGEMENT, not explanation.
65
- - `test/140` — irrelevant supply changes no answer.
77
+ - `test/140` — irrelevant supply changes nothing.
78
+ - `test/142`, `test/143` — the offer's contract, and the cycles.
@@ -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`–`140`). §14–25 (pipeline), §64 (derived thresholds), AGENTS.md §2
16
+ (`test/133`–`143`). §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.5",
4
+ "version": "0.8.6",
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.5",
3
+ "version": "0.8.6",
4
4
  "description": "Sema: a non-parametric, instance-based reasoning system.",
5
5
  "repository": {
6
6
  "type": "git",
package/src/meter.ts CHANGED
@@ -267,15 +267,30 @@ export class Meter {
267
267
  * not computable at all. With it, the price of extending the answer is
268
268
  * `reasonSteps · STEP`, the ladder's own value for following an edge. */
269
269
  reasonSteps = 0;
270
- /** Bytes of the grounding's UNCOVERED material the extension was justified by
271
- * — the union of the spans each step carried a `W`-window of. The gate
272
- * already computed WHICH span carried it per step and kept only a boolean;
273
- * this is that fact, accumulated. Read with {@link reasonSteps}: one is the
274
- * price, the other the explanation. */
270
+ /** Bytes the admitted steps ACCOUNTED for — the witnesses' spans, summed by the
271
+ * law's own function over the tail of the state's accounting. It is NOT what
272
+ * they carried: a step admitted by `reaches` may declare spans it holds no
273
+ * window of, and then it accounts without carrying and consumes nothing (see
274
+ * {@link closureDrainedBytes}). Read with {@link reasonSteps}: one is the
275
+ * price, the other the accounting. */
276
+ reasonAccountedBytes = 0;
277
+ /** Bytes of the QUESTION's own material an admitted step CARRIED — the windows
278
+ * of the remainder its product holds, read by the law's one reading. The
279
+ * accounting may be larger (a `reaches` step declares spans it holds no window
280
+ * of) and the consumption is the window of a step that also reaches, so the
281
+ * three form a lattice: consumed ⊆ carried ⊆ accounted. */
275
282
  reasonCarriedBytes = 0;
283
+ /** Times the layer OFFERED a continuation to the law. Read with
284
+ * {@link lawRejects}: the offers the law refused are the Model X contract being
285
+ * exercised, and `offerRuns - lawRejects` is the accepted transitions. */
286
+ offerRuns = 0;
287
+ /** Times the law REFUSED the continuation the layer offered. A refusal is
288
+ * terminal by the layer's contract, so this counts the moments the contract
289
+ * mattered — zero means the walk never needed it. */
290
+ lawRejects = 0;
276
291
  /** Bytes of the question's REMAINDER a step CONSUMED — the drop the law's own
277
292
  * `advance` makes when a declared move carries the material it accounts for.
278
- * Read with {@link reasonSteps} and {@link reasonCarriedBytes}: carrying is
293
+ * Read with {@link reasonSteps} and {@link reasonAccountedBytes}: carrying is
279
294
  * the engagement, this is the consumption, and before it the second was
280
295
  * invisible. */
281
296
  closureDrainedBytes = 0;
@@ -454,7 +454,11 @@ export async function closure(
454
454
  W: number,
455
455
  offer: Offer,
456
456
  /** Called for each step the law admits, with the state before and after. */
457
- onTaken?: (before: DerivationState, after: DerivationState) => void,
457
+ onTaken?: (
458
+ before: DerivationState,
459
+ after: DerivationState,
460
+ witnesses: ReadonlyArray<Witness>,
461
+ ) => void,
458
462
  /** Called when the law refused the continuation the layer offered. */
459
463
  onRefused?: (at: DerivationState) => void,
460
464
  ): Promise<DerivationState> {
@@ -467,7 +471,7 @@ export async function closure(
467
471
  return d;
468
472
  }
469
473
  const next = advance(d, t, explains);
470
- onTaken?.(d, next);
474
+ onTaken?.(d, next, explains);
471
475
  d = next;
472
476
  }
473
477
  }
@@ -206,6 +206,7 @@ export async function reason(
206
206
  | null = null;
207
207
 
208
208
  const offer: Offer = async (d) => {
209
+ if (ctx.meter) ctx.meter.offerRuns += 1;
209
210
  const cur = d.product;
210
211
  // The first step's `cur` IS the grounding's product, so the guard above
211
212
  // already resolved it and read its reverse edges — reuse both.
@@ -268,8 +269,12 @@ export async function reason(
268
269
  query,
269
270
  W,
270
271
  offer,
271
- (before, after) => {
272
+ (before, after, witnesses) => {
272
273
  if (ctx.meter) {
274
+ ctx.meter.reasonCarriedBytes += witnesses.reduce(
275
+ (n, w) => n + (w.window ? w.window[1] - w.window[0] : 0),
276
+ 0,
277
+ );
273
278
  ctx.meter.closureDrainedBytes += unaccountedBytes(before.remainder) -
274
279
  unaccountedBytes(after.remainder);
275
280
  }
@@ -295,6 +300,9 @@ export async function reason(
295
300
  }
296
301
  },
297
302
  (at) => {
303
+ // THE LAW REFUSED — counted where it happened, before the bookkeeping
304
+ // below decides whether there is a step to report.
305
+ if (ctx.meter) ctx.meter.lawRejects += 1;
298
306
  const p = pending;
299
307
  pending = null;
300
308
  if (p === null || p.kind !== "pivot") return;
@@ -329,7 +337,7 @@ export async function reason(
329
337
  // over the tail of the state's list — and computed ONLY when a meter is
330
338
  // attached: this used to slice and MAP a fresh array on every response, for a
331
339
  // counter that usually does not exist. One allocation, under the meter.
332
- ctx.meter.reasonCarriedBytes += unaccountedBytes(
340
+ ctx.meter.reasonAccountedBytes += unaccountedBytes(
333
341
  closed_.accounted.slice(d0.accounted.length),
334
342
  );
335
343
  }
@@ -570,6 +578,10 @@ export async function fuseAttention(
570
578
  if (fusedWitness === null) return { ...state, product: out };
571
579
  const fused = advance(state, fusedT, fusedWitness);
572
580
  if (ctx.meter) {
581
+ ctx.meter.reasonCarriedBytes += fusedWitness.reduce(
582
+ (n, w) => n + (w.window ? w.window[1] - w.window[0] : 0),
583
+ 0,
584
+ );
573
585
  ctx.meter.closureDrainedBytes += unaccountedBytes(state.remainder) -
574
586
  unaccountedBytes(fused.remainder);
575
587
  }
@@ -9,7 +9,7 @@
9
9
  //
10
10
  // MEASURED BEFORE THIS TEST EXISTED, one call per size:
11
11
  //
12
- // N= 250 / 500 / 1000 / 2000 ⇒ reasonSteps 1, reasonCarriedBytes 11,
12
+ // N= 250 / 500 / 1000 / 2000 ⇒ reasonSteps 1, reasonAccountedBytes 11,
13
13
  // pops 338 (k = 0.000), same answer
14
14
  //
15
15
  // THE BARS ARE THE REPO'S OWN (trap 4 — no new numbers): test/89 asserts k < 1 for
@@ -72,7 +72,7 @@ async function measure(n) {
72
72
  return {
73
73
  answer,
74
74
  steps: c.reasonSteps ?? 0,
75
- carried: c.reasonCarriedBytes ?? 0,
75
+ carried: c.reasonAccountedBytes ?? 0,
76
76
  pops: c.searchPops ?? 0,
77
77
  };
78
78
  }
@@ -91,7 +91,7 @@ test("a bigger corpus does not buy more extension work for the same answer", asy
91
91
  steps.push(r.steps);
92
92
  answers.push(r.answer);
93
93
  console.log(
94
- ` N=${n} → reasonSteps ${r.steps}, reasonCarriedBytes ${r.carried}, ` +
94
+ ` N=${n} → reasonSteps ${r.steps}, reasonAccountedBytes ${r.carried}, ` +
95
95
  `pops ${r.pops}`,
96
96
  );
97
97
  }
@@ -124,7 +124,7 @@ test("136.2 the extension is not priced — and it pays for itself", async () =>
124
124
  // would refuse the IDENTITY steps the law admits, which is the documented
125
125
  // failure of the (c) remedy (material lost: "hot" for "cold hot").
126
126
  assert.ok(
127
- (counters.reasonCarriedBytes ?? 0) * PASS >
127
+ (counters.reasonAccountedBytes ?? 0) * PASS >
128
128
  (counters.reasonSteps ?? 0) * STEP,
129
129
  "the extension is carrying less than its steps are worth — measured 11 bytes " +
130
130
  "against 1 step here. If this ever fails, the absent price comparison HAS " +
@@ -0,0 +1,93 @@
1
+ // 142 — the layer offers only what the law admits, which is what makes a refusal final.
2
+ //
3
+ // THE CONTRACT THIS PINS. `closure` stops when the law refuses the continuation the layer
4
+ // offered, and reads that refusal as "no continuation exists". That is valid only under
5
+ // ONE of two architectures: the layer searches for an admissible continuation and offers
6
+ // only a valid one (X), rather than offering candidates for the law to filter (Y). Sema is
7
+ // X — the producer tries the forward absorb first and reaches for the pivot only when the
8
+ // absorb's guards fail, returning null when neither exists — so the one offer the law can
9
+ // still refuse is the pivot without ownership, and it is the layer's last.
10
+ //
11
+ // 142.1 pins the premise: the law CAN refuse (a continuation that neither carries question
12
+ // material nor declares a move), and admits the two species. Without a refusal the contract
13
+ // would be vacuous. 142.2 pins the consequence in the engine: the walk ends at that refusal
14
+ // and the satisfying answer is untouched.
15
+
16
+ import { test } from "node:test";
17
+ import assert from "node:assert/strict";
18
+ import { Mind } from "../dist/src/index.js";
19
+ import { SQliteStore } from "../dist/src/store-sqlite.js";
20
+ import { admissible } from "../dist/src/mind/derivation.js";
21
+
22
+ const QUERY = new TextEncoder().encode("ABCDEFGHIJ");
23
+ const W = 4;
24
+ const state = () => ({
25
+ product: new Uint8Array(0),
26
+ accounted: [],
27
+ remainder: [[0, 10]],
28
+ cost: 0,
29
+ });
30
+
31
+ test("142.1 the law refuses what neither carries nor reaches, and admits both species", () => {
32
+ // neither: no window held, no move declared
33
+ assert.equal(
34
+ admissible(
35
+ state(),
36
+ { product: new TextEncoder().encode("Paris"), contains: true, cost: 1 },
37
+ QUERY,
38
+ W,
39
+ ),
40
+ null,
41
+ "an offer that neither carries nor reaches is refused — so offering one is not free",
42
+ );
43
+ // carries: the product holds a window of the remainder
44
+ assert.notEqual(
45
+ admissible(
46
+ state(),
47
+ {
48
+ product: new TextEncoder().encode("ZZZZABCDZZ"),
49
+ contains: true,
50
+ cost: 1,
51
+ },
52
+ QUERY,
53
+ W,
54
+ ),
55
+ null,
56
+ "carrying admits",
57
+ );
58
+ // reaches: a declared move, which needs no question material
59
+ assert.notEqual(
60
+ admissible(
61
+ state(),
62
+ {
63
+ product: new TextEncoder().encode("Paris"),
64
+ contains: true,
65
+ reaches: true,
66
+ cost: 1,
67
+ },
68
+ QUERY,
69
+ W,
70
+ ),
71
+ null,
72
+ "a declared move admits on its own ground",
73
+ );
74
+ });
75
+
76
+ test("142.2 the walk ends at the refusal, with the satisfying answer untouched", async () => {
77
+ const LINKS = [
78
+ ["What is the capital of France", "The capital of France is Paris"],
79
+ ["Paris", "Paris is famous for the Eiffel Tower"],
80
+ ["the Eiffel Tower", "the Eiffel Tower is in Paris"],
81
+ ];
82
+ const store = new SQliteStore({ path: ":memory:" });
83
+ const mind = new Mind({ seed: 7, store, profile: true });
84
+ await mind.ingest(LINKS);
85
+ const out = await mind.respond("What is the capital of France famous for");
86
+ const text = new TextDecoder().decode(out.bytes).replace(/\0+/g, "").trim();
87
+ await store.close();
88
+ assert.equal(
89
+ text,
90
+ LINKS[1][1],
91
+ "the layer's last offer was refused, and the answer stands",
92
+ );
93
+ });
@@ -0,0 +1,62 @@
1
+ // 143 — cycles, in the engine: three topologies, and what must hold for each.
2
+ //
3
+ // The law holds no `visited`, no `depth`, no `hop`, no `once` and no `cap` — cycle
4
+ // prevention belongs to the layer that controls states and consumption, and a cycle is not
5
+ // closure. So the engine must terminate on each topology anyway, and the answer must be a
6
+ // fact of the corpus rather than a composition typed around the loop.
7
+ //
8
+ // A→B→A · A→B→C→A · A→B, B→C, C→B (a loop with a tail, where the refusal is not trivial).
9
+ // Each fact is filed under both of its ends, the way the store files one, so the walk has
10
+ // every edge the loop needs.
11
+
12
+ import { test } from "node:test";
13
+ import assert from "node:assert/strict";
14
+ import { Mind } from "../dist/src/index.js";
15
+ import { SQliteStore } from "../dist/src/store-sqlite.js";
16
+
17
+ const TOPOLOGIES = {
18
+ "A→B→A": [
19
+ ["Alice knows Bob", "Alice knows Bob"],
20
+ ["Bob knows Alice", "Bob knows Alice"],
21
+ ],
22
+ "A→B→C→A": [
23
+ ["Alice knows Bob", "Alice knows Bob"],
24
+ ["Bob knows Carol", "Bob knows Carol"],
25
+ ["Carol knows Alice", "Carol knows Alice"],
26
+ ],
27
+ "A→B, B→C, C→B": [
28
+ ["Alice knows Bob", "Alice knows Bob"],
29
+ ["Bob knows Carol", "Bob knows Carol"],
30
+ ["Carol knows Bob", "Carol knows Bob"],
31
+ ],
32
+ };
33
+
34
+ async function ask(corpus, query) {
35
+ const store = new SQliteStore({ path: ":memory:" });
36
+ const mind = new Mind({ seed: 7, store, profile: true });
37
+ await mind.ingest(corpus);
38
+ const out = await mind.respond(query);
39
+ const text = new TextDecoder().decode(out.bytes).replace(/\0+/g, "").trim();
40
+ await store.close();
41
+ return text;
42
+ }
43
+
44
+ for (const [name, corpus] of Object.entries(TOPOLOGIES)) {
45
+ test(`143 ${name} terminates and answers from the corpus`, async () => {
46
+ const first = await ask(corpus, "Alice knows");
47
+ const second = await ask(corpus, "Alice knows");
48
+ assert.equal(
49
+ second,
50
+ first,
51
+ "two runs agree: no order-dependent wander around the loop",
52
+ );
53
+ if (first.length > 0) {
54
+ assert.ok(
55
+ corpus.some(([, fact]) => first.includes(fact) || fact.includes(first)),
56
+ `the answer is one of the corpus's own facts, not a composition typed around the loop: ${
57
+ JSON.stringify(first)
58
+ }`,
59
+ );
60
+ }
61
+ });
62
+ }
@@ -419,7 +419,7 @@ test("12. the extension's cost obeys the ladder's own inequality", async () => {
419
419
  await store.close();
420
420
 
421
421
  const steps = c.reasonSteps ?? 0;
422
- const carried = c.reasonCarriedBytes ?? 0;
422
+ const carried = c.reasonAccountedBytes ?? 0;
423
423
  assert.ok(
424
424
  steps >= 1,
425
425
  `the extension must run for this to mean anything (got ${steps})`,
@@ -765,7 +765,7 @@ test("18. the remainder the pipeline decides on is visible", async () => {
765
765
  };
766
766
 
767
767
  // The pivoting query: the reasoner runs, so the remainder is non-empty
768
- // (measured: reasonSteps 1, reasonCarriedBytes 11).
768
+ // (measured: reasonSteps 1, reasonAccountedBytes 11).
769
769
  const pivot = await run("What is the capital of France famous for");
770
770
  assert.ok(
771
771
  (pivot.steps ?? 0) >= 1,