@hviana/sema 0.8.5 → 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.
- package/dist/src/meter.d.ts +21 -6
- package/dist/src/meter.js +21 -6
- package/dist/src/mind/attention.d.ts +5 -0
- package/dist/src/mind/derivation.d.ts +14 -9
- package/dist/src/mind/derivation.js +10 -1
- package/dist/src/mind/pipeline.js +9 -0
- package/dist/src/mind/reasoning.js +95 -5
- package/dist/src/mind/traverse.d.ts +7 -1
- package/dist/src/mind/traverse.js +7 -1
- package/docs/INDEX.md +1 -1
- package/docs/INVARIANTS.md +1 -1
- package/docs/architecture/closure.md +17 -2
- package/docs/harness/gates.md +1 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/meter.ts +21 -6
- package/src/mind/attention.ts +5 -0
- package/src/mind/derivation.ts +28 -10
- package/src/mind/pipeline.ts +9 -0
- package/src/mind/reasoning.ts +145 -5
- package/src/mind/traverse.ts +7 -1
- package/test/121-the-extension-does-not-grow-with-the-corpus.test.mjs +3 -3
- package/test/136-the-two-named-limits.test.mjs +1 -1
- package/test/142-the-layer-offers-only-what-the-law-admits.test.mjs +155 -0
- package/test/143-cycles-terminate-and-are-not-closure.test.mjs +62 -0
- package/test/144-identity-is-content.test.mjs +55 -0
- package/test/145-the-instrument-answers.test.mjs +58 -0
- package/test/146-the-contains-gate.test.mjs +67 -0
- package/test/147-the-witness-ladder.test.mjs +76 -0
- package/test/55-cost-meter.test.mjs +2 -2
package/src/mind/reasoning.ts
CHANGED
|
@@ -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
|
|
|
@@ -206,6 +218,7 @@ export async function reason(
|
|
|
206
218
|
| null = null;
|
|
207
219
|
|
|
208
220
|
const offer: Offer = async (d) => {
|
|
221
|
+
if (ctx.meter) ctx.meter.offerRuns += 1;
|
|
209
222
|
const cur = d.product;
|
|
210
223
|
// The first step's `cur` IS the grounding's product, so the guard above
|
|
211
224
|
// already resolved it and read its reverse edges — reuse both.
|
|
@@ -220,10 +233,37 @@ export async function reason(
|
|
|
220
233
|
// fixpoint falls through to the pivot step instead. Offered with `moves`:
|
|
221
234
|
// completing the answer's OWN learnt form is an identity step, not a claim
|
|
222
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.
|
|
223
256
|
if (
|
|
224
|
-
curId !== null &&
|
|
225
|
-
ctx.store.nextFirst(curId, bound).some((n) => !consumed.has(n))
|
|
257
|
+
curId !== null && outs.length > 0 && !outs.some((n) => !consumed.has(n))
|
|
226
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))) {
|
|
227
267
|
const fwd = await follow(ctx, curId, qv);
|
|
228
268
|
const fwdId = fwd !== null ? resolve(ctx, fwd) : null;
|
|
229
269
|
if (
|
|
@@ -235,18 +275,79 @@ export async function reason(
|
|
|
235
275
|
pending = { kind: "absorb", cur, curId, fwd };
|
|
236
276
|
return { product: fwd, contains: true, reaches: true, cost: STEP };
|
|
237
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
|
+
);
|
|
238
311
|
}
|
|
239
312
|
|
|
240
313
|
// Pivot: the longest unconsumed learnt context the answer contains.
|
|
241
314
|
consumeAll(curId);
|
|
242
315
|
const pivot = await pivotInto(ctx, cur, consumed, voiced);
|
|
243
|
-
if (pivot === 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
|
+
}
|
|
244
331
|
const fc = await follow(ctx, pivot, qv);
|
|
245
332
|
consumeAll(pivot);
|
|
246
333
|
if (
|
|
247
334
|
fc === null || bytesEqual(fc, cur) ||
|
|
248
335
|
restates(query, fc, 0, { proper: true })
|
|
249
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
|
+
);
|
|
250
351
|
return null;
|
|
251
352
|
}
|
|
252
353
|
pending = { kind: "pivot", cur, pivot, fc };
|
|
@@ -268,8 +369,12 @@ export async function reason(
|
|
|
268
369
|
query,
|
|
269
370
|
W,
|
|
270
371
|
offer,
|
|
271
|
-
(before, after) => {
|
|
372
|
+
(before, after, witnesses) => {
|
|
272
373
|
if (ctx.meter) {
|
|
374
|
+
ctx.meter.reasonCarriedBytes += witnesses.reduce(
|
|
375
|
+
(n, w) => n + (w.window ? w.window[1] - w.window[0] : 0),
|
|
376
|
+
0,
|
|
377
|
+
);
|
|
273
378
|
ctx.meter.closureDrainedBytes += unaccountedBytes(before.remainder) -
|
|
274
379
|
unaccountedBytes(after.remainder);
|
|
275
380
|
}
|
|
@@ -295,6 +400,9 @@ export async function reason(
|
|
|
295
400
|
}
|
|
296
401
|
},
|
|
297
402
|
(at) => {
|
|
403
|
+
// THE LAW REFUSED — counted where it happened, before the bookkeeping
|
|
404
|
+
// below decides whether there is a step to report.
|
|
405
|
+
if (ctx.meter) ctx.meter.lawRejects += 1;
|
|
298
406
|
const p = pending;
|
|
299
407
|
pending = null;
|
|
300
408
|
if (p === null || p.kind !== "pivot") return;
|
|
@@ -316,6 +424,34 @@ export async function reason(
|
|
|
316
424
|
},
|
|
317
425
|
);
|
|
318
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
|
+
|
|
319
455
|
// INSTRUMENTATION ONLY — the extension's two facts, untraced (meter.ts
|
|
320
456
|
// contract 1: a counter never reaches a decision). They are what a caller
|
|
321
457
|
// needs to PRICE the extension instead of taking it unconditionally, and both
|
|
@@ -329,7 +465,7 @@ export async function reason(
|
|
|
329
465
|
// over the tail of the state's list — and computed ONLY when a meter is
|
|
330
466
|
// attached: this used to slice and MAP a fresh array on every response, for a
|
|
331
467
|
// counter that usually does not exist. One allocation, under the meter.
|
|
332
|
-
ctx.meter.
|
|
468
|
+
ctx.meter.reasonAccountedBytes += unaccountedBytes(
|
|
333
469
|
closed_.accounted.slice(d0.accounted.length),
|
|
334
470
|
);
|
|
335
471
|
}
|
|
@@ -570,6 +706,10 @@ export async function fuseAttention(
|
|
|
570
706
|
if (fusedWitness === null) return { ...state, product: out };
|
|
571
707
|
const fused = advance(state, fusedT, fusedWitness);
|
|
572
708
|
if (ctx.meter) {
|
|
709
|
+
ctx.meter.reasonCarriedBytes += fusedWitness.reduce(
|
|
710
|
+
(n, w) => n + (w.window ? w.window[1] - w.window[0] : 0),
|
|
711
|
+
0,
|
|
712
|
+
);
|
|
573
713
|
ctx.meter.closureDrainedBytes += unaccountedBytes(state.remainder) -
|
|
574
714
|
unaccountedBytes(fused.remainder);
|
|
575
715
|
}
|
package/src/mind/traverse.ts
CHANGED
|
@@ -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,
|
|
@@ -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,
|
|
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.
|
|
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},
|
|
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.
|
|
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,155 @@
|
|
|
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, closure } 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
|
+
});
|
|
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,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
|
+
}
|
|
@@ -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
|
+
});
|