@hviana/sema 0.8.2 → 0.8.5
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 +38 -37
- package/README.md +17 -38
- package/TRADEMARKS.md +0 -1
- package/dist/example/demo.js +85 -34
- package/dist/src/config.d.ts +11 -0
- package/dist/src/config.js +2 -0
- package/dist/src/geometry.d.ts +21 -10
- package/dist/src/geometry.js +21 -12
- package/dist/src/meter.d.ts +62 -0
- package/dist/src/meter.js +62 -0
- package/dist/src/mind/articulation.js +1 -1
- package/dist/src/mind/attention.d.ts +4 -0
- package/dist/src/mind/attention.js +167 -17
- package/dist/src/mind/canonical.d.ts +16 -0
- package/dist/src/mind/canonical.js +41 -0
- package/dist/src/mind/derivation.d.ts +201 -0
- package/dist/src/mind/derivation.js +327 -0
- package/dist/src/mind/graph-search.d.ts +2 -1
- package/dist/src/mind/graph-search.js +70 -29
- package/dist/src/mind/match.d.ts +3 -1
- package/dist/src/mind/match.js +7 -3
- package/dist/src/mind/mechanisms/alu.js +0 -2
- package/dist/src/mind/mechanisms/cast.d.ts +1 -5
- package/dist/src/mind/mechanisms/cast.js +16 -19
- package/dist/src/mind/mechanisms/confluence.d.ts +0 -3
- package/dist/src/mind/mechanisms/confluence.js +27 -9
- package/dist/src/mind/mechanisms/cover.js +17 -20
- package/dist/src/mind/mechanisms/extraction.d.ts +0 -1
- package/dist/src/mind/mechanisms/extraction.js +13 -8
- package/dist/src/mind/mechanisms/prefix-completion.js +0 -1
- package/dist/src/mind/mechanisms/recall.d.ts +0 -1
- package/dist/src/mind/mechanisms/recall.js +40 -13
- package/dist/src/mind/mechanisms/reference.js +3 -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 -3
- package/dist/src/mind/pipeline.js +136 -44
- package/dist/src/mind/primitives.js +9 -1
- package/dist/src/mind/rationale.d.ts +21 -5
- package/dist/src/mind/rationale.js +16 -21
- package/dist/src/mind/reasoning.d.ts +12 -20
- package/dist/src/mind/reasoning.js +190 -106
- 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/dist/src/mind/types.js +6 -3
- package/docs/INDEX.md +23 -24
- package/docs/INVARIANTS.md +16 -17
- package/docs/architecture/bounded-reads.md +5 -5
- package/docs/architecture/closure.md +65 -0
- package/docs/architecture/commonality.md +29 -20
- package/docs/architecture/cost-model.md +7 -7
- package/docs/architecture/determinism.md +7 -7
- package/docs/architecture/exact-vs-approximate.md +4 -4
- package/docs/architecture/factored-machinery.md +14 -14
- package/docs/architecture/match-project.md +2 -3
- package/docs/architecture/mechanism-market.md +16 -16
- package/docs/architecture/meter.md +10 -11
- package/docs/architecture/store.md +4 -4
- package/docs/architecture/thresholds.md +1 -1
- package/docs/failures/tempting-but-wrong.md +14 -5
- package/docs/harness/gates.md +7 -7
- package/docs/mechanisms/cast.md +2 -2
- package/docs/mechanisms/cover.md +4 -5
- package/docs/mechanisms/extraction.md +7 -7
- package/docs/mechanisms/recall.md +8 -9
- package/example/demo.ts +90 -37
- 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 -13
- package/src/meter.ts +62 -0
- package/src/mind/articulation.ts +0 -1
- package/src/mind/attention.ts +169 -17
- package/src/mind/canonical.ts +43 -0
- package/src/mind/derivation.ts +473 -0
- package/src/mind/graph-search.ts +76 -34
- package/src/mind/match.ts +7 -3
- package/src/mind/mechanisms/alu.ts +0 -2
- package/src/mind/mechanisms/cast.ts +20 -22
- package/src/mind/mechanisms/confluence.ts +27 -13
- package/src/mind/mechanisms/cover.ts +17 -20
- package/src/mind/mechanisms/extraction.ts +13 -9
- package/src/mind/mechanisms/prefix-completion.ts +0 -1
- package/src/mind/mechanisms/recall.ts +39 -13
- package/src/mind/mechanisms/reference.ts +2 -3
- package/src/mind/mind.ts +6 -4
- package/src/mind/pipeline-mechanism.ts +7 -3
- package/src/mind/pipeline.ts +160 -52
- package/src/mind/primitives.ts +9 -1
- package/src/mind/rationale.ts +27 -23
- package/src/mind/reasoning.ts +227 -120
- 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 +41 -15
- 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/133-the-decision-point-renders-the-state.test.mjs +204 -0
- package/test/134-the-law-explains-the-engines-own-refusal.test.mjs +237 -0
- package/test/135-one-law-any-producer.test.mjs +289 -0
- package/test/136-the-two-named-limits.test.mjs +205 -0
- package/test/137-the-law-lives-once-and-below.test.mjs +400 -0
- package/test/138-the-remainder-drains-only-what-a-move-declares.test.mjs +62 -0
- package/test/139-the-witness-is-engagement-not-explanation.test.mjs +51 -0
- package/test/140-irrelevant-supply-does-not-change-an-answer.test.mjs +48 -0
- package/test/141-the-question-is-paid-at-construction.test.mjs +98 -0
- package/test/32-confluence.test.mjs +68 -0
- package/test/36-already-answered-fusion.test.mjs +20 -2
- package/test/37-cluster-dispersion-fusion.test.mjs +30 -3
- package/test/38-reason-restate-guard.test.mjs +28 -2
- package/test/43-cast-analog-seat.test.mjs +10 -0
- package/test/55-cost-meter.test.mjs +862 -0
package/src/mind/types.ts
CHANGED
|
@@ -34,6 +34,7 @@ export interface DepositCacheEntry {
|
|
|
34
34
|
content: ContentFold;
|
|
35
35
|
}
|
|
36
36
|
import { bytesEqual, concatBytes, indexOf } from "../bytes.js";
|
|
37
|
+
import { restates } from "./derivation.js";
|
|
37
38
|
import { dominates } from "../geometry.js";
|
|
38
39
|
|
|
39
40
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -65,15 +66,21 @@ export interface GraphSearchHost {
|
|
|
65
66
|
starts: ReadonlySet<number>;
|
|
66
67
|
};
|
|
67
68
|
chooseNext?(node: number): number | undefined;
|
|
68
|
-
/** The
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
|
|
69
|
+
/** The lengths `p` for which `prefix ‖ tail[0..p]` IS A STORED NODE, ascending
|
|
70
|
+
* — the join's candidate set, in the tail's own coordinates. Optional: a host
|
|
71
|
+
* that cannot answer makes the join fall back to every prefix, which is exact
|
|
72
|
+
* and complete but pays a `resolve` per offset.
|
|
73
|
+
*
|
|
74
|
+
* WHY NOT THE FOLD'S CUTS. A key names a relation exactly when the
|
|
75
|
+
* concatenation is a node, and a node's end is the end of ITS OWN stream —
|
|
76
|
+
* where the fold never emits a cut (geometry's `emit` guards `at >= n`). So a
|
|
77
|
+
* key can end strictly inside the tail with no boundary anywhere near it:
|
|
78
|
+
* measured, "stockholm mayor" exists, leads on to the mayor fact, and its
|
|
79
|
+
* boundary 6 is in neither the tail's cuts ([4,7]) nor the concatenation's.
|
|
80
|
+
* The fold's boundaries are a SUBSET of the real ends, not a proxy for them,
|
|
81
|
+
* and using them skipped the shortest names first — which is a semantic law,
|
|
82
|
+
* not an optimisation (test/106, test/108 pin it). */
|
|
83
|
+
contentKeyEnds?(prefix: Uint8Array, tail: Uint8Array): readonly number[];
|
|
77
84
|
/** The admission predicate — `traverse.ts`'s `leadsSomewhere`, its ONE
|
|
78
85
|
* definition: does this node bear an edge or a halo? Optional, so a bare
|
|
79
86
|
* host (a raw Store and nothing else) still works; when present, the search
|
|
@@ -145,11 +152,28 @@ export interface Attention {
|
|
|
145
152
|
* strength and its place.
|
|
146
153
|
* `vote` is a sum over every region that agreed, so it grows with how many
|
|
147
154
|
* places corroborated; `peak` is what the strongest one of them said on its
|
|
148
|
-
* own.
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
155
|
+
* own. THIS USED TO PRESCRIBE THE WRONG OPERAND. It read: "a consumer
|
|
156
|
+
* holding this point to consensusFloor(N) — a bar that prices ONE region's
|
|
157
|
+
* maximally-discriminative evidence — must read `peak`, not `vote`." The
|
|
158
|
+
* engine reads the POOLED vote, and thresholds.md §2 derives the floor for
|
|
159
|
+
* exactly that ("Pooled-vote significance floor": one maximally-specific
|
|
160
|
+
* region contributes at most ln N, and ln(N)+1/2 demands corroboration
|
|
161
|
+
* BEYOND one region). MEASURED across 27 anchors on 6 queries: all 11
|
|
162
|
+
* admissions cleared the floor by the sum and NONE by `peak` alone — a gate
|
|
163
|
+
* reading `peak` would refuse every root the engine elects. `peak` remains
|
|
164
|
+
* what it is: the strongest SINGLE region's contribution. */
|
|
152
165
|
peak: number;
|
|
166
|
+
/** The IDF-WEIGHTED sum behind this point — the quantity `consensusFloor` is
|
|
167
|
+
* derived for, and therefore the one the floor gates must read. It is
|
|
168
|
+
* MODE-INDEPENDENT by construction (its per-region weight is
|
|
169
|
+
* `mutual · idf / roots`, never the mode-dependent `wf`), so gating on it
|
|
170
|
+
* makes an anchor's admission the same in `inverse`, `direct` and `combined`.
|
|
171
|
+
* In `inverse` — the only mode the engine runs — it equals `vote` exactly
|
|
172
|
+
* (measured, test/55 test 17), so nothing about today's verdicts changes.
|
|
173
|
+
* MEASURED before this field existed: gating on `vote` DID flip a verdict,
|
|
174
|
+
* anchor 87 of test/55's query (inverse 2.682 admitted, direct 1.468
|
|
175
|
+
* refused, floor 2.292). */
|
|
176
|
+
idfVote: number;
|
|
153
177
|
/** SCALE-INVARIANT confidence: the fraction of the query's OWN regions
|
|
154
178
|
* whose evidence this point accounts for (Σ RegionVote.absorbed among
|
|
155
179
|
* its contributors, over the query's total region count) — read PER-
|
|
@@ -463,11 +487,13 @@ export function segRestatesQuery(
|
|
|
463
487
|
W: number,
|
|
464
488
|
): boolean {
|
|
465
489
|
if (!s.rec) return false;
|
|
490
|
+
// THE LITERAL EXEMPTION IS THE CALLER'S. A span that IS the site's own bytes
|
|
491
|
+
// at its own position is naming what is already there, not substituting for
|
|
492
|
+
// it — and only this caller knows that, so it says so by not asking.
|
|
466
493
|
const literal = s.j - s.i === s.bytes.length &&
|
|
467
494
|
bytesEqual(s.bytes, query.subarray(s.i, s.j));
|
|
468
495
|
if (literal) return false;
|
|
469
|
-
return s.bytes
|
|
470
|
-
indexOf(query, s.bytes, 0) >= 0;
|
|
496
|
+
return restates(query, s.bytes, W, { proper: true });
|
|
471
497
|
}
|
|
472
498
|
|
|
473
499
|
/** Lift the answer out of the cover for think: the recognised region, free of
|
|
@@ -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
|
+
});
|