@hviana/sema 0.8.2 → 0.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +29 -29
- package/TRADEMARKS.md +0 -1
- package/dist/src/config.d.ts +11 -0
- package/dist/src/config.js +2 -0
- package/dist/src/geometry.d.ts +21 -0
- package/dist/src/geometry.js +21 -0
- package/dist/src/meter.d.ts +51 -0
- package/dist/src/meter.js +51 -0
- package/dist/src/mind/attention.d.ts +4 -0
- package/dist/src/mind/attention.js +165 -16
- package/dist/src/mind/canonical.d.ts +16 -0
- package/dist/src/mind/canonical.js +41 -0
- package/dist/src/mind/graph-search.js +33 -14
- package/dist/src/mind/match.d.ts +1 -1
- package/dist/src/mind/match.js +5 -3
- package/dist/src/mind/mechanisms/cast.js +1 -1
- package/dist/src/mind/mechanisms/confluence.js +24 -0
- package/dist/src/mind/mechanisms/recall.js +32 -4
- package/dist/src/mind/mind.d.ts +4 -2
- package/dist/src/mind/mind.js +5 -4
- package/dist/src/mind/pipeline-mechanism.d.ts +7 -0
- package/dist/src/mind/pipeline.js +41 -14
- package/dist/src/mind/primitives.js +9 -1
- package/dist/src/mind/rationale.d.ts +28 -1
- package/dist/src/mind/rationale.js +22 -1
- package/dist/src/mind/reasoning.d.ts +21 -3
- package/dist/src/mind/reasoning.js +73 -21
- package/dist/src/mind/recognition.js +4 -8
- package/dist/src/mind/resonance.js +20 -1
- package/dist/src/mind/trace.js +1 -0
- package/dist/src/mind/traverse.js +6 -2
- package/dist/src/mind/types.d.ts +36 -13
- package/docs/INVARIANTS.md +2 -2
- package/docs/architecture/bounded-reads.md +1 -1
- package/docs/architecture/commonality.md +2 -2
- package/docs/architecture/cost-model.md +2 -2
- package/docs/architecture/determinism.md +7 -7
- package/docs/architecture/match-project.md +2 -3
- package/docs/architecture/mechanism-market.md +10 -10
- package/docs/architecture/meter.md +5 -5
- package/docs/architecture/store.md +3 -3
- package/docs/failures/tempting-but-wrong.md +3 -4
- package/docs/harness/gates.md +2 -2
- package/docs/mechanisms/cast.md +2 -2
- package/docs/mechanisms/cover.md +2 -3
- package/docs/mechanisms/extraction.md +7 -7
- package/docs/mechanisms/recall.md +8 -9
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/alu/README.md +11 -12
- package/src/config.ts +13 -0
- package/src/geometry.ts +21 -0
- package/src/meter.ts +51 -0
- package/src/mind/attention.ts +167 -16
- package/src/mind/canonical.ts +43 -0
- package/src/mind/graph-search.ts +39 -14
- package/src/mind/match.ts +5 -3
- package/src/mind/mechanisms/cast.ts +3 -1
- package/src/mind/mechanisms/confluence.ts +24 -0
- package/src/mind/mechanisms/recall.ts +32 -4
- package/src/mind/mind.ts +6 -4
- package/src/mind/pipeline-mechanism.ts +7 -0
- package/src/mind/pipeline.ts +49 -16
- package/src/mind/primitives.ts +9 -1
- package/src/mind/rationale.ts +35 -1
- package/src/mind/reasoning.ts +92 -15
- package/src/mind/recognition.ts +4 -8
- package/src/mind/resonance.ts +19 -1
- package/src/mind/trace.ts +1 -0
- package/src/mind/traverse.ts +7 -5
- package/src/mind/types.ts +36 -13
- package/test/105-derive-through-reports-its-refusal.test.mjs +24 -0
- package/test/118-the-join-reaches-a-key-off-the-cut.test.mjs +74 -0
- package/test/119-the-work-does-not-grow-with-the-corpus.test.mjs +122 -0
- package/test/120-composition-is-consequence.test.mjs +132 -0
- package/test/121-the-extension-does-not-grow-with-the-corpus.test.mjs +128 -0
- package/test/122-the-climb-search-does-not-grow-with-the-corpus.test.mjs +117 -0
- package/test/123-the-paired-formulas-agree.test.mjs +90 -0
- package/test/125-the-post-grounding-branch-publishes-its-operand.test.mjs +51 -0
- package/test/126-the-pipeline-does-not-name-mechanisms.test.mjs +42 -0
- package/test/128-the-leads-somewhere-pair-agrees.test.mjs +83 -0
- package/test/129-the-trace-payload-shape.test.mjs +164 -0
- package/test/32-confluence.test.mjs +68 -0
- package/test/38-reason-restate-guard.test.mjs +8 -2
- package/test/43-cast-analog-seat.test.mjs +10 -0
- package/test/55-cost-meter.test.mjs +859 -0
package/src/mind/recognition.ts
CHANGED
|
@@ -535,7 +535,10 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
|
|
|
535
535
|
// emitted, so a caller can retry a trimmed edge on the miss path only.
|
|
536
536
|
if (end - start < W) return false;
|
|
537
537
|
if (flatProbe(start, end) === null) {
|
|
538
|
-
if (!canonBudget)
|
|
538
|
+
if (!canonBudget) {
|
|
539
|
+
if (ctx.meter) ctx.meter.canonProbesDenied++;
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
539
542
|
if (!canonAdmits(start, end)) return false;
|
|
540
543
|
}
|
|
541
544
|
const id = resolveSpan(start, end);
|
|
@@ -556,13 +559,6 @@ function recogniseImpl(ctx: MindContext, bytes: Uint8Array): Recognition {
|
|
|
556
559
|
// endpoints in order and running out partway along the query. A form
|
|
557
560
|
// longer than that is out of this tier's reach — but so is a form the
|
|
558
561
|
// chain cannot span, and that is exactly the trade the budget prices.
|
|
559
|
-
// The factor is chainReach(W), the same W² scale the chain already
|
|
560
|
-
// trusts; no new constant.
|
|
561
|
-
// The factor is chainReach(W) — the same W² scale the chain itself
|
|
562
|
-
// trusts — so the cap is derived from the fold's geometry, never tuned.
|
|
563
|
-
// (It was briefly an environment variable while the cost was being
|
|
564
|
-
// measured; an env-read here would make inference non-reproducible,
|
|
565
|
-
// which the determinism contract forbids outright.)
|
|
566
562
|
// The factor is chainReach(W) — the same W² scale the chain itself
|
|
567
563
|
// trusts — so the cap is derived from the fold's geometry, never tuned.
|
|
568
564
|
// (It was briefly an environment variable while the cost was being
|
package/src/mind/resonance.ts
CHANGED
|
@@ -329,7 +329,14 @@ export async function pivotInto(
|
|
|
329
329
|
consumed: ReadonlySet<number>,
|
|
330
330
|
voiced: readonly Uint8Array[] = [],
|
|
331
331
|
): Promise<number | null> {
|
|
332
|
-
|
|
332
|
+
// The pivot's OWN shortlist capacity — not `recallQueryK`: they are different
|
|
333
|
+
// quantities (a mechanical sweep's probe budget vs the bridge's candidate-read
|
|
334
|
+
// allowance), and one number serving both means tightening either silently
|
|
335
|
+
// starves the other. The reason is the duplication, NOT a measured strangle:
|
|
336
|
+
// an earlier version of this comment claimed "at recallQueryK 1 the pivot finds
|
|
337
|
+
// no pivot", and that was probed and is FALSE on test/23's fixture — the
|
|
338
|
+
// strangle is fixture-specific, so it is not the evidence for this split.
|
|
339
|
+
const k = ctx.cfg.pivotProbeK;
|
|
333
340
|
// ONE perception of the answer, shared by the probe budget and the walk —
|
|
334
341
|
// this used to fold the same bytes twice, back to back, on every hop.
|
|
335
342
|
const tree = perceive(ctx, answer);
|
|
@@ -365,6 +372,17 @@ export async function pivotInto(
|
|
|
365
372
|
}
|
|
366
373
|
for (const c of n.kids) queue.push(c); // breadth-first: larger regions first
|
|
367
374
|
}
|
|
375
|
+
// The sweep's two FACTS, untraced (meter.ts contract 1: a counter never
|
|
376
|
+
// reaches a decision). `probes` is the work done; the shortfall is the
|
|
377
|
+
// capacity the cap withheld — the branches the breadth-first order never got
|
|
378
|
+
// to. Whether that withheld anything that mattered is NOT said here: the
|
|
379
|
+
// order spends the largest regions first, and recognition below still
|
|
380
|
+
// contributes every exact containment candidate regardless of the budget.
|
|
381
|
+
if (ctx.meter) {
|
|
382
|
+
ctx.meter.pivotProbes += probes;
|
|
383
|
+
const unprobed = branchCount - probeCap;
|
|
384
|
+
if (unprobed > 0) ctx.meter.pivotBranchesUnprobed += unprobed;
|
|
385
|
+
}
|
|
368
386
|
// THE FULL recognition, memo-shared with every other reader of these bytes.
|
|
369
387
|
// A "skip the edge trims here" variant was refuted (see recognise's own
|
|
370
388
|
// note): those trims are what find a WHOLE trained form embedded at an
|
package/src/mind/trace.ts
CHANGED
package/src/mind/traverse.ts
CHANGED
|
@@ -10,6 +10,13 @@ import { cosine, Vec } from "../vec.js";
|
|
|
10
10
|
import type { AncestorReach, MindContext, SaturationStop } from "./types.js";
|
|
11
11
|
import { gistOf, read } from "./primitives.js";
|
|
12
12
|
import { canonicalWindows, leafIdPrefix, leafIdRun } from "./canonical.js";
|
|
13
|
+
// Imported at the TOP, where every other import is. They used to sit 800 lines
|
|
14
|
+
// down under a note claiming the position mattered ("before trace module is
|
|
15
|
+
// loaded") — it does not: an ES module's static imports are HOISTED, so the
|
|
16
|
+
// file's line order never decides load order. The note described an intention
|
|
17
|
+
// the runtime does not honour; the imports move and the claim goes.
|
|
18
|
+
import { decodeText } from "./rationale.js";
|
|
19
|
+
import type { RationaleItem } from "./rationale.js";
|
|
13
20
|
|
|
14
21
|
// ── Session structural memo ─────────────────────────────────────────────
|
|
15
22
|
//
|
|
@@ -824,11 +831,6 @@ export function chooseAmong(
|
|
|
824
831
|
: { id: candidates[0], score: -Infinity };
|
|
825
832
|
}
|
|
826
833
|
|
|
827
|
-
// ── Trace shim (used by chooseNext before trace module is loaded) ────────
|
|
828
|
-
|
|
829
|
-
import { decodeText } from "./rationale.js";
|
|
830
|
-
import type { RationaleItem } from "./rationale.js";
|
|
831
|
-
|
|
832
834
|
function rItemShort(
|
|
833
835
|
ctx: MindContext,
|
|
834
836
|
id: number,
|
package/src/mind/types.ts
CHANGED
|
@@ -65,15 +65,21 @@ export interface GraphSearchHost {
|
|
|
65
65
|
starts: ReadonlySet<number>;
|
|
66
66
|
};
|
|
67
67
|
chooseNext?(node: number): number | undefined;
|
|
68
|
-
/** The
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
|
|
68
|
+
/** The lengths `p` for which `prefix ‖ tail[0..p]` IS A STORED NODE, ascending
|
|
69
|
+
* — the join's candidate set, in the tail's own coordinates. Optional: a host
|
|
70
|
+
* that cannot answer makes the join fall back to every prefix, which is exact
|
|
71
|
+
* and complete but pays a `resolve` per offset.
|
|
72
|
+
*
|
|
73
|
+
* WHY NOT THE FOLD'S CUTS. A key names a relation exactly when the
|
|
74
|
+
* concatenation is a node, and a node's end is the end of ITS OWN stream —
|
|
75
|
+
* where the fold never emits a cut (geometry's `emit` guards `at >= n`). So a
|
|
76
|
+
* key can end strictly inside the tail with no boundary anywhere near it:
|
|
77
|
+
* measured, "stockholm mayor" exists, leads on to the mayor fact, and its
|
|
78
|
+
* boundary 6 is in neither the tail's cuts ([4,7]) nor the concatenation's.
|
|
79
|
+
* The fold's boundaries are a SUBSET of the real ends, not a proxy for them,
|
|
80
|
+
* and using them skipped the shortest names first — which is a semantic law,
|
|
81
|
+
* not an optimisation (test/106, test/108 pin it). */
|
|
82
|
+
contentKeyEnds?(prefix: Uint8Array, tail: Uint8Array): readonly number[];
|
|
77
83
|
/** The admission predicate — `traverse.ts`'s `leadsSomewhere`, its ONE
|
|
78
84
|
* definition: does this node bear an edge or a halo? Optional, so a bare
|
|
79
85
|
* host (a raw Store and nothing else) still works; when present, the search
|
|
@@ -145,11 +151,28 @@ export interface Attention {
|
|
|
145
151
|
* strength and its place.
|
|
146
152
|
* `vote` is a sum over every region that agreed, so it grows with how many
|
|
147
153
|
* places corroborated; `peak` is what the strongest one of them said on its
|
|
148
|
-
* own.
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
154
|
+
* own. THIS USED TO PRESCRIBE THE WRONG OPERAND. It read: "a consumer
|
|
155
|
+
* holding this point to consensusFloor(N) — a bar that prices ONE region's
|
|
156
|
+
* maximally-discriminative evidence — must read `peak`, not `vote`." The
|
|
157
|
+
* engine reads the POOLED vote, and thresholds.md §2 derives the floor for
|
|
158
|
+
* exactly that ("Pooled-vote significance floor": one maximally-specific
|
|
159
|
+
* region contributes at most ln N, and ln(N)+1/2 demands corroboration
|
|
160
|
+
* BEYOND one region). MEASURED across 27 anchors on 6 queries: all 11
|
|
161
|
+
* admissions cleared the floor by the sum and NONE by `peak` alone — a gate
|
|
162
|
+
* reading `peak` would refuse every root the engine elects. `peak` remains
|
|
163
|
+
* what it is: the strongest SINGLE region's contribution. */
|
|
152
164
|
peak: number;
|
|
165
|
+
/** The IDF-WEIGHTED sum behind this point — the quantity `consensusFloor` is
|
|
166
|
+
* derived for, and therefore the one the floor gates must read. It is
|
|
167
|
+
* MODE-INDEPENDENT by construction (its per-region weight is
|
|
168
|
+
* `mutual · idf / roots`, never the mode-dependent `wf`), so gating on it
|
|
169
|
+
* makes an anchor's admission the same in `inverse`, `direct` and `combined`.
|
|
170
|
+
* In `inverse` — the only mode the engine runs — it equals `vote` exactly
|
|
171
|
+
* (measured, test/55 test 17), so nothing about today's verdicts changes.
|
|
172
|
+
* MEASURED before this field existed: gating on `vote` DID flip a verdict,
|
|
173
|
+
* anchor 87 of test/55's query (inverse 2.682 admitted, direct 1.468
|
|
174
|
+
* refused, floor 2.292). */
|
|
175
|
+
idfVote: number;
|
|
153
176
|
/** SCALE-INVARIANT confidence: the fraction of the query's OWN regions
|
|
154
177
|
* whose evidence this point accounts for (Σ RegionVote.absorbed among
|
|
155
178
|
* its contributors, over the query's total region count) — read PER-
|
|
@@ -73,6 +73,30 @@ test("the join's refusal is reported, naming the candidate and tail it tried", a
|
|
|
73
73
|
got.some((s) => /#\d+, from the .* source/.test(String(s.note))),
|
|
74
74
|
"the report must name the candidate's node and its source",
|
|
75
75
|
);
|
|
76
|
+
// …and the bytes it actually tried, EXACTLY. The `text` rendering cannot
|
|
77
|
+
// stand in for them: it decodes UTF-8 and DROPS NUL bytes, so a key carrying
|
|
78
|
+
// one is unrecoverable from it — which is why a refusal could not be tested
|
|
79
|
+
// exactly without re-encoding, and why this assertion reads BYTES.
|
|
80
|
+
//
|
|
81
|
+
// The relation is the rule's own: key = fact ‖ prefix(tail).
|
|
82
|
+
const carried = got.filter((s) => {
|
|
83
|
+
const raw = (s.inputs ?? []).map((i) => i.bytes);
|
|
84
|
+
if (raw.length < 3) return false;
|
|
85
|
+
const [fact, tail, key] = raw;
|
|
86
|
+
if (![fact, tail, key].every((b) => b instanceof Uint8Array)) return false;
|
|
87
|
+
if (key.length < fact.length) return false;
|
|
88
|
+
if (!fact.every((b, k) => key[k] === b)) return false;
|
|
89
|
+
const rest = key.subarray(fact.length);
|
|
90
|
+
return rest.every((b, k) => tail[k] === b);
|
|
91
|
+
});
|
|
92
|
+
assert.equal(
|
|
93
|
+
carried.length,
|
|
94
|
+
got.length,
|
|
95
|
+
`every refusal must carry its OWN bytes (key = fact ‖ prefix of tail), not ` +
|
|
96
|
+
`only a rendering — ${
|
|
97
|
+
got.length - carried.length
|
|
98
|
+
} of ${got.length} did not`,
|
|
99
|
+
);
|
|
76
100
|
await mind.store.close();
|
|
77
101
|
});
|
|
78
102
|
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// 118-the-join-reaches-a-key-off-the-cut.test.mjs — a deposited, CONTINUING key
|
|
2
|
+
// must be reachable even when its boundary is not a fold cut of the tail.
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS EXISTS. The join builds `key = fact.bytes ‖ tail[0..len]` and used to
|
|
5
|
+
// try only the tail's own fold boundaries (plus its end). A boundary is not a
|
|
6
|
+
// proxy for a stored key's end: a deposited member's end is the end of ITS OWN
|
|
7
|
+
// stream, and the fold never emits a cut at a stream's end (geometry's `emit`
|
|
8
|
+
// guards `at >= n`) — so the boundary that names a real stored key can sit
|
|
9
|
+
// between cuts, or beyond every one of them. Measured on the five-fact chain
|
|
10
|
+
// below: the fourth hop's key "stockholm mayor" exists (resolve() finds it), its
|
|
11
|
+
// continuation is the mayor fact, and its boundary (p = 6) is NOT among the
|
|
12
|
+
// tail's cuts ([4,7]) — so the join could not try it and the answer stopped at
|
|
13
|
+
// the third fact.
|
|
14
|
+
//
|
|
15
|
+
// The candidate ends are now the prefixes that ARE stored nodes, ascending, from
|
|
16
|
+
// the host's `contentKeyEnds` — so the shortest name still wins, and every
|
|
17
|
+
// deposited key is reachable.
|
|
18
|
+
//
|
|
19
|
+
// WHAT IS PINNED. The chain reaches the FIFTH fact, and the join fires at least
|
|
20
|
+
// four times. The counters are the untraced view (AGENTS §6: profile without a
|
|
21
|
+
// trace attached).
|
|
22
|
+
import { test } from "node:test";
|
|
23
|
+
import assert from "node:assert/strict";
|
|
24
|
+
import { Mind } from "../dist/src/index.js";
|
|
25
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
26
|
+
|
|
27
|
+
const F1 = "The director of Eva is Gustaf Molander.";
|
|
28
|
+
const F2 = "The country of Gustaf Molander is Sweden.";
|
|
29
|
+
const F3 = "The capital of Sweden is Stockholm.";
|
|
30
|
+
const F4 = "The mayor of Stockholm is Karin Wanngard.";
|
|
31
|
+
const F5 = "The party of Karin Wanngard is the Social Democrats.";
|
|
32
|
+
|
|
33
|
+
async function chain() {
|
|
34
|
+
const store = new SQliteStore({ path: ":memory:" });
|
|
35
|
+
const mind = new Mind({ seed: 7, store, profile: true });
|
|
36
|
+
await mind.ingest([
|
|
37
|
+
["eva", F1],
|
|
38
|
+
["eva director", F1],
|
|
39
|
+
["gustaf molander", F2],
|
|
40
|
+
["gustaf molander country", F2],
|
|
41
|
+
["sweden", F3],
|
|
42
|
+
["sweden capital", F3],
|
|
43
|
+
["stockholm", F4],
|
|
44
|
+
["stockholm mayor", F4],
|
|
45
|
+
["karin wanngard", F5],
|
|
46
|
+
["karin wanngard party", F5],
|
|
47
|
+
]);
|
|
48
|
+
return { mind, store };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const text = (resp) => new TextDecoder().decode(resp.bytes).replace(/\0+/g, "");
|
|
52
|
+
|
|
53
|
+
test("a five-fact chain reaches the fifth fact, through four joins", async () => {
|
|
54
|
+
const { mind, store } = await chain();
|
|
55
|
+
const out = text(
|
|
56
|
+
await mind.respond("eva director country capital mayor party"),
|
|
57
|
+
).trim();
|
|
58
|
+
const fired = mind.lastCost?.counters.joinFired ?? 0;
|
|
59
|
+
await store.close();
|
|
60
|
+
|
|
61
|
+
assert.equal(
|
|
62
|
+
out,
|
|
63
|
+
F5,
|
|
64
|
+
`the chain must reach the fifth fact. A key that EXISTS and LEADS ON ` +
|
|
65
|
+
`("stockholm mayor" → the mayor fact) was not tried because its boundary ` +
|
|
66
|
+
`is not a cut of the tail alone — the join was asked to extend from the ` +
|
|
67
|
+
`third fact and stopped (joinFired=${fired})`,
|
|
68
|
+
);
|
|
69
|
+
assert.ok(
|
|
70
|
+
fired >= 4,
|
|
71
|
+
`the join must fire once per licensed hop (measured ${fired}); four hops ` +
|
|
72
|
+
`were available: eva→gustaf molander→sweden→stockholm→karin wanngard`,
|
|
73
|
+
);
|
|
74
|
+
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// 119-the-work-does-not-grow-with-the-corpus.test.mjs — the derivation's work
|
|
2
|
+
// must be FLAT in the corpus for a byte-identical answer.
|
|
3
|
+
//
|
|
4
|
+
// THE LAW. bounded-reads.md forbids per-query work that grows with N: a bigger
|
|
5
|
+
// store may answer differently (a better derivation can exist), but it must not
|
|
6
|
+
// buy arbitrary COMPUTATION for the same answer. test/89 measures this for the
|
|
7
|
+
// completion recursion against real prose, test/14 for recall reads; this closes
|
|
8
|
+
// the loop for the derivation as a whole after the F3/F4 migrations, where the
|
|
9
|
+
// hop allowance stopped deciding depth and the pivot's probe sweep got its own
|
|
10
|
+
// capacity.
|
|
11
|
+
//
|
|
12
|
+
// THE BARS ARE THE REPO'S OWN, not new numbers (trap 4). test/89 asserts
|
|
13
|
+
// `k < 0.6` for its `searches` (nested solve() calls — the recursion, which its
|
|
14
|
+
// fix governs end to end; measured 1.28 unfixed → 0.38 fixed) and `k < 1` for
|
|
15
|
+
// agenda pops — "only outright linear growth is the forbidden case … so this
|
|
16
|
+
// asserts the law itself", measured 1.40 unfixed → 0.54 fixed. Same helper as
|
|
17
|
+
// test/14 (`logLogSlope`), copied because suites here are standalone.
|
|
18
|
+
//
|
|
19
|
+
// MEASURED BEFORE THIS TEST EXISTED, on the fixture below, one call per size:
|
|
20
|
+
// pops 338 / 338 / 338 / 338 (k = 0.000), searches 2 everywhere, one pivot, and
|
|
21
|
+
// a byte-identical answer — flat, and flat by a wide margin.
|
|
22
|
+
|
|
23
|
+
import { test } from "node:test";
|
|
24
|
+
import assert from "node:assert/strict";
|
|
25
|
+
import { Mind } from "../dist/src/index.js";
|
|
26
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
27
|
+
|
|
28
|
+
/** Power-law exponent k in t ≈ c·n^k, by log–log least squares.
|
|
29
|
+
* k≈0 flat · k≈1 linear · k≈2 quadratic. (test/14's helper.) */
|
|
30
|
+
function logLogSlope(sizes, times) {
|
|
31
|
+
const n = sizes.length;
|
|
32
|
+
const xs = sizes.map(Math.log), ys = times.map(Math.log);
|
|
33
|
+
const mx = xs.reduce((a, b) => a + b, 0) / n;
|
|
34
|
+
const my = ys.reduce((a, b) => a + b, 0) / n;
|
|
35
|
+
let num = 0, den = 0;
|
|
36
|
+
for (let i = 0; i < n; i++) {
|
|
37
|
+
num += (xs[i] - mx) * (ys[i] - my);
|
|
38
|
+
den += (xs[i] - mx) ** 2;
|
|
39
|
+
}
|
|
40
|
+
return num / den;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const WORDS =
|
|
44
|
+
("alpha bravo charlie delta echo foxtrot golf hotel india juliet kilo lima " +
|
|
45
|
+
"mike november oscar papa").split(" ");
|
|
46
|
+
|
|
47
|
+
/** A three-fact chain the query closes through (test/110's LINKS), plus `n`
|
|
48
|
+
* unrelated pairs so the corpus grows without touching the derivation. */
|
|
49
|
+
const QUERY = "What is the capital of France famous for";
|
|
50
|
+
|
|
51
|
+
async function measure(n) {
|
|
52
|
+
const store = new SQliteStore({ path: ":memory:" });
|
|
53
|
+
const mind = new Mind({ seed: 7, store, profile: true });
|
|
54
|
+
const pairs = [
|
|
55
|
+
["What is the capital of France", "The capital of France is Paris"],
|
|
56
|
+
["Paris", "Paris is famous for the Eiffel Tower"],
|
|
57
|
+
["the Eiffel Tower", "the Eiffel Tower is in Paris"],
|
|
58
|
+
];
|
|
59
|
+
for (let i = 0; i < n; i++) {
|
|
60
|
+
pairs.push([
|
|
61
|
+
`note ${i} about ${WORDS[i % 16]} ${WORDS[(i * 3) % 16]}`,
|
|
62
|
+
`the ${WORDS[i % 16]} of ${WORDS[(i * 5) % 16]} is ${
|
|
63
|
+
WORDS[(i * 7) % 16]
|
|
64
|
+
} ${i}`,
|
|
65
|
+
]);
|
|
66
|
+
}
|
|
67
|
+
await mind.ingest(pairs);
|
|
68
|
+
const answer = String(await mind.respondText(QUERY)).trim();
|
|
69
|
+
const c = mind.lastCost.counters;
|
|
70
|
+
await store.close();
|
|
71
|
+
return { answer, pops: c.searchPops ?? 0, searches: c.searches ?? 0 };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
test("a bigger corpus does not buy more work for the same answer", async () => {
|
|
75
|
+
const base = 250;
|
|
76
|
+
const sizes = [];
|
|
77
|
+
const pops = [];
|
|
78
|
+
const searches = [];
|
|
79
|
+
const answers = [];
|
|
80
|
+
for (const mult of [1, 2, 4, 8]) {
|
|
81
|
+
const n = base * mult;
|
|
82
|
+
const r = await measure(n);
|
|
83
|
+
sizes.push(n);
|
|
84
|
+
pops.push(r.pops);
|
|
85
|
+
searches.push(r.searches);
|
|
86
|
+
answers.push(r.answer);
|
|
87
|
+
console.log(
|
|
88
|
+
` N=${n} → pops ${r.pops}, searches ${r.searches}, ` +
|
|
89
|
+
`answer "${r.answer.slice(0, 32)}"`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// The answer is the CONTROL: same answer, eight times the corpus. Without
|
|
94
|
+
// this the exponents below could be flat because the query stopped being
|
|
95
|
+
// answered at all.
|
|
96
|
+
assert.equal(
|
|
97
|
+
new Set(answers).size,
|
|
98
|
+
1,
|
|
99
|
+
`the answer must not move with the corpus (got ${JSON.stringify(answers)})`,
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
const kSearches = logLogSlope(sizes, searches);
|
|
103
|
+
const kPops = logLogSlope(sizes, pops);
|
|
104
|
+
console.log(
|
|
105
|
+
` growth exponents over N→8N: searches k=${kSearches.toFixed(2)}, ` +
|
|
106
|
+
`pops k=${kPops.toFixed(2)}`,
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
assert.ok(
|
|
110
|
+
kSearches < 0.6,
|
|
111
|
+
`nested searches grew with exponent k=${kSearches.toFixed(2)} in corpus ` +
|
|
112
|
+
`size (${searches.join(" → ")}) for a byte-identical answer`,
|
|
113
|
+
);
|
|
114
|
+
assert.ok(
|
|
115
|
+
kPops < 1,
|
|
116
|
+
`agenda pops grew with exponent k=${kPops.toFixed(2)} in corpus size ` +
|
|
117
|
+
`(${
|
|
118
|
+
pops.join(" → ")
|
|
119
|
+
}) for a byte-identical answer — k≈1 is work LINEAR ` +
|
|
120
|
+
`in the corpus, which bounded-reads.md forbids outright`,
|
|
121
|
+
);
|
|
122
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// 120-composition-is-consequence.test.mjs — the composition matrix.
|
|
2
|
+
//
|
|
3
|
+
// THE LAW. A derivation is closed when the structure it built accounts for the
|
|
4
|
+
// query's remainder; every transition is then an item that produced BYTES and a
|
|
5
|
+
// NODE, priced on the one ladder, and the next mechanism consumes it like any
|
|
6
|
+
// other premise. This file records, for the roadmap's fourteen pairs, which
|
|
7
|
+
// law permits the composition and which one forbids it — from the CODE, never
|
|
8
|
+
// from "this mechanism does not call that one" (which the roadmap refuses to
|
|
9
|
+
// accept as a reason).
|
|
10
|
+
//
|
|
11
|
+
// THE CLASSIFICATION, each with the code fact that decides it:
|
|
12
|
+
//
|
|
13
|
+
// VALID — the second transition consumes the first's conclusion:
|
|
14
|
+
// fact → join the join resolves its own key (resolve ?? canonResolve)
|
|
15
|
+
// fact → completion a produced form's continuation is charged MICRO (rcmp)
|
|
16
|
+
// completion → join VALID for a completion that IS a stored node
|
|
17
|
+
// join → completion the join concludes a `form` carrying its own node
|
|
18
|
+
// completion → recompletion the recursion itself (recompleteOpen, memo)
|
|
19
|
+
// recomposition → completion rcmp: following the fused whole completes it
|
|
20
|
+
// recompose → join `fuse` names the FUSED whole's node (concat of both)
|
|
21
|
+
// entity → join deriveThrough's premise IS the entity's node
|
|
22
|
+
//
|
|
23
|
+
// INVALID_BY_IDENTITY — the identity of the premise cannot carry the step:
|
|
24
|
+
// join → recompose the composite's `node` is a PART's, so decomposing it
|
|
25
|
+
// would return another object's structure
|
|
26
|
+
// completion → join for a node-less `out`: a computed value is not a
|
|
27
|
+
// corpus subject, so no stored relation exists through it
|
|
28
|
+
//
|
|
29
|
+
// NOT ON THE FRONTIER — semantically fine, structurally out of the chart
|
|
30
|
+
// (no taxonomy label exists for this; stated rather than forced):
|
|
31
|
+
// pivot → join reason() runs after grounding (pipeline.ts:579) and
|
|
32
|
+
// its result is the ANSWER, not a GItem on a frontier
|
|
33
|
+
// alignment → join a connector leaves the cover as a mechanism PAYLOAD
|
|
34
|
+
// (cover.ts:163 `return links`), not as an item
|
|
35
|
+
//
|
|
36
|
+
// UNKNOWN — the code does not decide, so no claim is made:
|
|
37
|
+
// join → alignment the two live in different layers (the cover's chart
|
|
38
|
+
// vs recall's candidate list); whether a legitimate
|
|
39
|
+
// composition exists there is not settled by the code
|
|
40
|
+
//
|
|
41
|
+
// THE INVARIANT THIS FILE PINS, and the reason it is not a duplicate of
|
|
42
|
+
// test/118: composition means the NEXT step's premise is the PREVIOUS step's
|
|
43
|
+
// conclusion. The join ATTEMPTS name their candidate entity, and on a five-fact
|
|
44
|
+
// chain those candidates ADVANCE through the chain's subjects — measured:
|
|
45
|
+
// "gustaf molander" → "gustaf molander" → "sweden" → "stockholm", never the
|
|
46
|
+
// query's own subject. test/118 pins that the chain completes and counts the
|
|
47
|
+
// joins that SUCCEED; this pins that the attempts ADVANCE.
|
|
48
|
+
//
|
|
49
|
+
// SCOPE, measured rather than assumed: this assertion SATURATES. A vacuity
|
|
50
|
+
// experiment on truncated chains gave 2 facts → ["gustaf molander"] (fails),
|
|
51
|
+
// 3 → ["gustaf molander", "sweden"] (fails), and 4 and 5 → the same three
|
|
52
|
+
// subjects (passes). So it proves the premise advances through at least three
|
|
53
|
+
// subjects and never stands on the query's own; it does NOT pin the last hop,
|
|
54
|
+
// because a SUCCESSFUL join is not reported under a `deriveThrough` step name —
|
|
55
|
+
// the count of successes is test/118's assertion, cited here rather than
|
|
56
|
+
// pretended.
|
|
57
|
+
|
|
58
|
+
import { test } from "node:test";
|
|
59
|
+
import assert from "node:assert/strict";
|
|
60
|
+
import { Mind } from "../dist/src/index.js";
|
|
61
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
62
|
+
|
|
63
|
+
const FACTS = [
|
|
64
|
+
"The director of Eva is Gustaf Molander.",
|
|
65
|
+
"The country of Gustaf Molander is Sweden.",
|
|
66
|
+
"The capital of Sweden is Stockholm.",
|
|
67
|
+
"The mayor of Stockholm is Karin Wanngard.",
|
|
68
|
+
"The party of Karin Wanngard is the Social Democrats.",
|
|
69
|
+
];
|
|
70
|
+
const KEYS = [
|
|
71
|
+
["eva director", 0],
|
|
72
|
+
["gustaf molander country", 1],
|
|
73
|
+
["sweden capital", 2],
|
|
74
|
+
["stockholm mayor", 3],
|
|
75
|
+
["karin wanngard party", 4],
|
|
76
|
+
];
|
|
77
|
+
const QUERY = "eva director country capital mayor party";
|
|
78
|
+
|
|
79
|
+
test("a join chain's attempts advance through the chain's subjects", async () => {
|
|
80
|
+
const store = new SQliteStore({ path: ":memory:" });
|
|
81
|
+
const mind = new Mind({ seed: 7, store, profile: true });
|
|
82
|
+
const pairs = [];
|
|
83
|
+
for (const [key, i] of KEYS) {
|
|
84
|
+
pairs.push([key.split(" ").slice(0, -1).join(" "), FACTS[i]], [
|
|
85
|
+
key,
|
|
86
|
+
FACTS[i],
|
|
87
|
+
]);
|
|
88
|
+
}
|
|
89
|
+
await mind.ingest(pairs);
|
|
90
|
+
|
|
91
|
+
const steps = [];
|
|
92
|
+
const answer = String(await mind.respondText(QUERY, (s) => steps.push(s)))
|
|
93
|
+
.trim();
|
|
94
|
+
|
|
95
|
+
// The candidate each join attempt stood on. The FIRST input of a
|
|
96
|
+
// derive-through step is the entity; taking it from the item's own bytes
|
|
97
|
+
// keeps this structural — no parsing of note text, which would be a magic
|
|
98
|
+
// string (the adversarial checklist names it).
|
|
99
|
+
const candidates = steps
|
|
100
|
+
.filter((s) => (s.mechanism ?? []).join("/").includes("deriveThrough"))
|
|
101
|
+
.map((s) => String((s.inputs ?? [])[0]?.text ?? ""));
|
|
102
|
+
|
|
103
|
+
assert.ok(
|
|
104
|
+
candidates.length >= 3,
|
|
105
|
+
`the chain must attempt several hops, got ${candidates.length}`,
|
|
106
|
+
);
|
|
107
|
+
const distinct = new Set(candidates);
|
|
108
|
+
assert.ok(
|
|
109
|
+
distinct.size >= 3,
|
|
110
|
+
`the premise must ADVANCE, not repeat the query's subject — candidates ` +
|
|
111
|
+
`${JSON.stringify([...distinct])}`,
|
|
112
|
+
);
|
|
113
|
+
// The chain's own subjects, and NOT the query's (composition means leaving
|
|
114
|
+
// the question's own subject behind and standing on what the last step found).
|
|
115
|
+
assert.ok(
|
|
116
|
+
!distinct.has("eva"),
|
|
117
|
+
`no hop may stand on the query's own subject; got ${
|
|
118
|
+
JSON.stringify([...distinct])
|
|
119
|
+
}`,
|
|
120
|
+
);
|
|
121
|
+
assert.ok(
|
|
122
|
+
distinct.has("sweden") && distinct.has("stockholm"),
|
|
123
|
+
`later hops must stand on the chain's subjects; got ${
|
|
124
|
+
JSON.stringify([...distinct])
|
|
125
|
+
}`,
|
|
126
|
+
);
|
|
127
|
+
assert.ok(
|
|
128
|
+
answer.includes("Social Democrats"),
|
|
129
|
+
`and the chain must reach the last fact, got ${JSON.stringify(answer)}`,
|
|
130
|
+
);
|
|
131
|
+
await store.close();
|
|
132
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// 121-the-extension-does-not-grow-with-the-corpus.test.mjs — the POST-GROUNDING
|
|
2
|
+
// extension must be flat in the corpus too.
|
|
3
|
+
//
|
|
4
|
+
// THE LAW, applied one layer out. test/119 pins that the DERIVATION's work does
|
|
5
|
+
// not grow with the corpus. But the answer also passes through the post-grounding
|
|
6
|
+
// extension (`reason()`), and bounded-reads.md does not care which layer does the
|
|
7
|
+
// work: a bigger store may answer differently, never buy arbitrary computation for
|
|
8
|
+
// the same answer.
|
|
9
|
+
//
|
|
10
|
+
// MEASURED BEFORE THIS TEST EXISTED, one call per size:
|
|
11
|
+
//
|
|
12
|
+
// N= 250 / 500 / 1000 / 2000 ⇒ reasonSteps 1, reasonCarriedBytes 11,
|
|
13
|
+
// pops 338 (k = 0.000), same answer
|
|
14
|
+
//
|
|
15
|
+
// THE BARS ARE THE REPO'S OWN (trap 4 — no new numbers): test/89 asserts k < 1 for
|
|
16
|
+
// agenda pops ("only outright linear growth is the forbidden case … so this asserts
|
|
17
|
+
// the law itself", measured 1.40 unfixed → 0.54 fixed). The exponent helper is
|
|
18
|
+
// test/14's `logLogSlope`, copied because suites here are standalone.
|
|
19
|
+
//
|
|
20
|
+
// THE ANTI-VACUITY GUARD IS THE POINT: `reasonSteps ≥ 1` at every size. Most
|
|
21
|
+
// fixtures in this repository answer before the extension can run — measured five
|
|
22
|
+
// times over in the closure work — and a flat curve for an extension that never
|
|
23
|
+
// runs would prove nothing at all.
|
|
24
|
+
|
|
25
|
+
import { test } from "node:test";
|
|
26
|
+
import assert from "node:assert/strict";
|
|
27
|
+
import { Mind } from "../dist/src/index.js";
|
|
28
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
29
|
+
|
|
30
|
+
/** Power-law exponent k in t ≈ c·n^k, by log–log least squares.
|
|
31
|
+
* k≈0 flat · k≈1 linear · k≈2 quadratic. (test/14's helper.) */
|
|
32
|
+
function logLogSlope(sizes, times) {
|
|
33
|
+
const n = sizes.length;
|
|
34
|
+
const xs = sizes.map(Math.log), ys = times.map(Math.log);
|
|
35
|
+
const mx = xs.reduce((a, b) => a + b, 0) / n;
|
|
36
|
+
const my = ys.reduce((a, b) => a + b, 0) / n;
|
|
37
|
+
let num = 0, den = 0;
|
|
38
|
+
for (let i = 0; i < n; i++) {
|
|
39
|
+
num += (xs[i] - mx) * (ys[i] - my);
|
|
40
|
+
den += (xs[i] - mx) ** 2;
|
|
41
|
+
}
|
|
42
|
+
return num / den;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const WORDS =
|
|
46
|
+
("alpha bravo charlie delta echo foxtrot golf hotel india juliet kilo lima " +
|
|
47
|
+
"mike november oscar papa").split(" ");
|
|
48
|
+
|
|
49
|
+
/** test/109's two-fact chain — the pivot is what crosses the hop — plus `n`
|
|
50
|
+
* unrelated pairs so the corpus grows without touching the extension. */
|
|
51
|
+
const QUERY = "What is the capital of France famous for";
|
|
52
|
+
|
|
53
|
+
async function measure(n) {
|
|
54
|
+
const store = new SQliteStore({ path: ":memory:" });
|
|
55
|
+
const mind = new Mind({ seed: 7, store, profile: true });
|
|
56
|
+
const pairs = [
|
|
57
|
+
["What is the capital of France", "The capital of France is Paris"],
|
|
58
|
+
["Paris", "Paris is famous for the Eiffel Tower"],
|
|
59
|
+
];
|
|
60
|
+
for (let i = 0; i < n; i++) {
|
|
61
|
+
pairs.push([
|
|
62
|
+
`note ${i} about ${WORDS[i % 16]} ${WORDS[(i * 3) % 16]}`,
|
|
63
|
+
`the ${WORDS[i % 16]} of ${WORDS[(i * 5) % 16]} is ${
|
|
64
|
+
WORDS[(i * 7) % 16]
|
|
65
|
+
} ${i}`,
|
|
66
|
+
]);
|
|
67
|
+
}
|
|
68
|
+
await mind.ingest(pairs);
|
|
69
|
+
const answer = String(await mind.respondText(QUERY)).trim();
|
|
70
|
+
const c = mind.lastCost.counters;
|
|
71
|
+
await store.close();
|
|
72
|
+
return {
|
|
73
|
+
answer,
|
|
74
|
+
steps: c.reasonSteps ?? 0,
|
|
75
|
+
carried: c.reasonCarriedBytes ?? 0,
|
|
76
|
+
pops: c.searchPops ?? 0,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
test("a bigger corpus does not buy more extension work for the same answer", async () => {
|
|
81
|
+
const base = 250;
|
|
82
|
+
const sizes = [];
|
|
83
|
+
const pops = [];
|
|
84
|
+
const steps = [];
|
|
85
|
+
const answers = [];
|
|
86
|
+
for (const mult of [1, 2, 4, 8]) {
|
|
87
|
+
const n = base * mult;
|
|
88
|
+
const r = await measure(n);
|
|
89
|
+
sizes.push(n);
|
|
90
|
+
pops.push(r.pops);
|
|
91
|
+
steps.push(r.steps);
|
|
92
|
+
answers.push(r.answer);
|
|
93
|
+
console.log(
|
|
94
|
+
` N=${n} → reasonSteps ${r.steps}, reasonCarriedBytes ${r.carried}, ` +
|
|
95
|
+
`pops ${r.pops}`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ANTI-VACUITY: the extension must be what is being measured. Without this,
|
|
100
|
+
// a flat curve would just mean the reasoner never ran.
|
|
101
|
+
assert.ok(
|
|
102
|
+
steps.every((s) => s >= 1),
|
|
103
|
+
`the extension must run at every size (got ${JSON.stringify(steps)})`,
|
|
104
|
+
);
|
|
105
|
+
assert.equal(
|
|
106
|
+
new Set(answers).size,
|
|
107
|
+
1,
|
|
108
|
+
`the answer must not move with the corpus (got ${JSON.stringify(answers)})`,
|
|
109
|
+
);
|
|
110
|
+
assert.equal(
|
|
111
|
+
new Set(steps).size,
|
|
112
|
+
1,
|
|
113
|
+
`and the extension must take the same number of steps (got ${
|
|
114
|
+
JSON.stringify(steps)
|
|
115
|
+
})`,
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const kPops = logLogSlope(sizes, pops);
|
|
119
|
+
console.log(` growth exponent over N→8N: pops k=${kPops.toFixed(2)}`);
|
|
120
|
+
assert.ok(
|
|
121
|
+
kPops < 1,
|
|
122
|
+
`agenda pops grew with exponent k=${kPops.toFixed(2)} in corpus size ` +
|
|
123
|
+
`(${
|
|
124
|
+
pops.join(" → ")
|
|
125
|
+
}) for a byte-identical answer — k≈1 is work LINEAR ` +
|
|
126
|
+
`in the corpus, which bounded-reads.md forbids outright`,
|
|
127
|
+
);
|
|
128
|
+
});
|