@hviana/sema 0.8.1 → 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 +28 -0
- package/dist/src/config.js +20 -0
- package/dist/src/geometry.d.ts +21 -0
- package/dist/src/geometry.js +21 -0
- package/dist/src/meter.d.ts +76 -0
- package/dist/src/meter.js +95 -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/corpus.d.ts +40 -0
- package/dist/src/mind/corpus.js +149 -0
- package/dist/src/mind/graph-search.d.ts +7 -0
- package/dist/src/mind/graph-search.js +254 -24
- package/dist/src/mind/index.d.ts +3 -1
- package/dist/src/mind/index.js +1 -0
- package/dist/src/mind/match.d.ts +9 -4
- package/dist/src/mind/match.js +147 -61
- package/dist/src/mind/mechanisms/cast.js +19 -3
- package/dist/src/mind/mechanisms/confluence.js +24 -0
- package/dist/src/mind/mechanisms/cover.js +6 -0
- package/dist/src/mind/mechanisms/recall.js +32 -4
- package/dist/src/mind/mind.d.ts +57 -0
- package/dist/src/mind/mind.js +72 -1
- package/dist/src/mind/pipeline-mechanism.d.ts +7 -0
- package/dist/src/mind/pipeline.js +66 -20
- 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 +25 -3
- package/dist/src/mind/reasoning.js +125 -20
- 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 +15 -3
- package/dist/src/mind/types.d.ts +49 -4
- 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 +34 -6
- 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 +48 -0
- package/src/geometry.ts +21 -0
- package/src/meter.ts +98 -0
- package/src/mind/attention.ts +167 -16
- package/src/mind/canonical.ts +43 -0
- package/src/mind/corpus.ts +202 -0
- package/src/mind/graph-search.ts +277 -23
- package/src/mind/index.ts +8 -1
- package/src/mind/match.ts +148 -57
- package/src/mind/mechanisms/cast.ts +20 -2
- package/src/mind/mechanisms/confluence.ts +24 -0
- package/src/mind/mechanisms/cover.ts +5 -0
- package/src/mind/mechanisms/recall.ts +32 -4
- package/src/mind/mind.ts +125 -0
- package/src/mind/pipeline-mechanism.ts +7 -0
- package/src/mind/pipeline.ts +79 -22
- package/src/mind/primitives.ts +9 -1
- package/src/mind/rationale.ts +35 -1
- package/src/mind/reasoning.ts +145 -13
- 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 +16 -6
- package/src/mind/types.ts +53 -4
- package/test/100-complete-grounding-trace.test.mjs +109 -0
- package/test/101-alignment-gap-bound.test.mjs +106 -0
- package/test/102-production-composes-at-scale.test.mjs +110 -0
- package/test/103-alignment-gap-budget.test.mjs +89 -0
- package/test/104-composition-is-reported.test.mjs +90 -0
- package/test/105-derive-through-reports-its-refusal.test.mjs +137 -0
- package/test/106-the-join-fires.test.mjs +94 -0
- package/test/107-the-join-is-counted.test.mjs +81 -0
- package/test/108-the-join-chains.test.mjs +78 -0
- package/test/109-the-pivot-is-counted.test.mjs +60 -0
- package/test/110-the-reasoner-stops-when-the-question-is-answered.test.mjs +91 -0
- package/test/111-the-cover-assembly-is-counted.test.mjs +74 -0
- package/test/112-the-exploration-does-not-grow-with-the-hub.test.mjs +89 -0
- package/test/113-the-rationale-payload-is-bounded.test.mjs +84 -0
- package/test/114-alignment-budget-is-per-sweep.test.mjs +93 -0
- package/test/116-the-extension-is-gated-by-the-pipelines-own-remainder.test.mjs +100 -0
- package/test/117-corpus-search.test.mjs +171 -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/14-scaling.test.mjs +10 -7
- 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/test/76-reference-binding.test.mjs +6 -1
- package/test/89-completion-recursion.test.mjs +30 -5
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// 122-the-climb-search-does-not-grow-with-the-corpus.test.mjs — the climb's own
|
|
2
|
+
// search must be flat in the corpus too.
|
|
3
|
+
//
|
|
4
|
+
// THE LAW, applied to the last path that was not measured. test/119 pins the
|
|
5
|
+
// DERIVATION's work, test/121 the post-grounding extension. The consensus climb
|
|
6
|
+
// runs a search of its own (`lightestDerivation` over a pooling DeductionSystem,
|
|
7
|
+
// attention.ts) and until item 3 of the open list that search was not even
|
|
8
|
+
// timed — its cost was invisible. It is now a phase (`climb.derivation`), and
|
|
9
|
+
// the phase carries work-counter deltas, which is the honest growth measure: a
|
|
10
|
+
// millisecond reading is noise, a read count is work.
|
|
11
|
+
//
|
|
12
|
+
// MEASURED BEFORE THIS TEST EXISTED, one call per size, byte-identical answer:
|
|
13
|
+
//
|
|
14
|
+
// N= 250 → climb.derivation: calls=1, counters {edgeProbes: 3, prevReads: 23}
|
|
15
|
+
// N= 500 → climb.derivation: calls=1, counters {edgeProbes: 3, prevReads: 7}
|
|
16
|
+
// N=1000 → climb.derivation: calls=1, counters {edgeProbes: 3, prevReads: 7}
|
|
17
|
+
// N=2000 → climb.derivation: calls=1, counters {edgeProbes: 3, prevReads: 7}
|
|
18
|
+
// searchPops = 338 at every size
|
|
19
|
+
//
|
|
20
|
+
// THE ASSERTION IS NON-GROWTH, WHICH IS WHAT THE LAW SAYS (`bounded-reads.md`: no
|
|
21
|
+
// per-query read grows with N) — not equality of counters, because `prevReads` is
|
|
22
|
+
// genuinely smaller at the larger sizes (23 → 7). `searchPops` IS asserted equal,
|
|
23
|
+
// because that is what the measurement shows for this fixture.
|
|
24
|
+
//
|
|
25
|
+
// THE ANTI-VACUITY GUARD: `calls >= 1` at every size. A flat curve for a phase
|
|
26
|
+
// that never ran would prove nothing, and a walk with no pivot does not enter it.
|
|
27
|
+
|
|
28
|
+
import { test } from "node:test";
|
|
29
|
+
import assert from "node:assert/strict";
|
|
30
|
+
import { Mind } from "../dist/src/index.js";
|
|
31
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
32
|
+
|
|
33
|
+
const WORDS =
|
|
34
|
+
("alpha bravo charlie delta echo foxtrot golf hotel india juliet kilo lima " +
|
|
35
|
+
"mike november oscar papa").split(" ");
|
|
36
|
+
|
|
37
|
+
/** test/109's two-fact chain — the pivot is what crosses the hop — plus `n`
|
|
38
|
+
* unrelated pairs, exactly the fixture whose numbers are recorded above. */
|
|
39
|
+
const QUERY = "What is the capital of France famous for";
|
|
40
|
+
|
|
41
|
+
async function measure(n) {
|
|
42
|
+
const store = new SQliteStore({ path: ":memory:" });
|
|
43
|
+
const mind = new Mind({ seed: 7, store, profile: true });
|
|
44
|
+
const pairs = [
|
|
45
|
+
["What is the capital of France", "The capital of France is Paris"],
|
|
46
|
+
["Paris", "Paris is famous for the Eiffel Tower"],
|
|
47
|
+
];
|
|
48
|
+
for (let i = 0; i < n; i++) {
|
|
49
|
+
pairs.push([
|
|
50
|
+
`note ${i} about ${WORDS[i % 16]} ${WORDS[(i * 3) % 16]}`,
|
|
51
|
+
`the ${WORDS[i % 16]} of ${WORDS[(i * 5) % 16]} is ${
|
|
52
|
+
WORDS[(i * 7) % 16]
|
|
53
|
+
} ${i}`,
|
|
54
|
+
]);
|
|
55
|
+
}
|
|
56
|
+
await mind.ingest(pairs);
|
|
57
|
+
const answer = String(await mind.respondText(QUERY)).trim();
|
|
58
|
+
const c = mind.lastCost;
|
|
59
|
+
await store.close();
|
|
60
|
+
const phase = (c.phases ?? {})["climb.derivation"];
|
|
61
|
+
return {
|
|
62
|
+
answer,
|
|
63
|
+
calls: phase?.calls ?? 0,
|
|
64
|
+
counters: phase?.counters ?? {},
|
|
65
|
+
pops: c.counters?.searchPops ?? 0,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
test("a bigger corpus does not buy the climb's search more work", async () => {
|
|
70
|
+
const base = 250;
|
|
71
|
+
const runs = [];
|
|
72
|
+
for (const mult of [1, 2, 4, 8]) {
|
|
73
|
+
const r = await measure(base * mult);
|
|
74
|
+
runs.push(r);
|
|
75
|
+
console.log(
|
|
76
|
+
` N=${base * mult} → climb.derivation calls=${r.calls}, ` +
|
|
77
|
+
`counters ${JSON.stringify(r.counters)}, pops ${r.pops}`,
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ANTI-VACUITY: the phase must actually run at every size.
|
|
82
|
+
for (const [i, r] of runs.entries()) {
|
|
83
|
+
assert.ok(
|
|
84
|
+
r.calls >= 1,
|
|
85
|
+
`the climb's search must run at N=${base * 2 ** i} (calls=${r.calls})`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
assert.equal(
|
|
89
|
+
new Set(runs.map((r) => r.answer)).size,
|
|
90
|
+
1,
|
|
91
|
+
`the answer must not move with the corpus (got ${
|
|
92
|
+
JSON.stringify(runs.map((r) => r.answer))
|
|
93
|
+
})`,
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// NON-GROWTH, counter by counter: what the search reads at 8N must not exceed
|
|
97
|
+
// what it reads at N. (A counter absent at the small size counts as 0.)
|
|
98
|
+
const first = runs[0].counters;
|
|
99
|
+
const last = runs[runs.length - 1].counters;
|
|
100
|
+
for (const name of new Set([...Object.keys(first), ...Object.keys(last)])) {
|
|
101
|
+
const a = first[name] ?? 0;
|
|
102
|
+
const b = last[name] ?? 0;
|
|
103
|
+
assert.ok(
|
|
104
|
+
b <= a,
|
|
105
|
+
`${name} grew with the corpus: ${a} at N=${base} → ${b} at N=${base * 8}`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// And the agenda pops of the whole response are identical across sizes.
|
|
110
|
+
assert.equal(
|
|
111
|
+
new Set(runs.map((r) => r.pops)).size,
|
|
112
|
+
1,
|
|
113
|
+
`agenda pops must not move (got ${
|
|
114
|
+
JSON.stringify(runs.map((r) => r.pops))
|
|
115
|
+
})`,
|
|
116
|
+
);
|
|
117
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// 123 — the derived formulas the house writes PER SIDE have to agree.
|
|
2
|
+
//
|
|
3
|
+
// The structural review (finding 9) found four derived quantities duplicated
|
|
4
|
+
// by design: `hubBound`, `atomIsHub`/`atomReach`, `leadsSomewhere` and the
|
|
5
|
+
// interior's phrase scale. `graph-search.ts` is *host-based* on purpose (it
|
|
6
|
+
// does not know `MindContext`), so the formula repeats — and the house
|
|
7
|
+
// recorded that it HAS DRIFTED once ("they had already drifted on the
|
|
8
|
+
// `Math.max(2, …)` floor", attention.ts).
|
|
9
|
+
//
|
|
10
|
+
// A pair test cannot call both homes (one is private), but it CAN read the
|
|
11
|
+
// code — as `test/88-dependency-footprint` reads imports. Each pair is
|
|
12
|
+
// extracted by its own pattern, and if the extraction finds no formula the
|
|
13
|
+
// test FAILS: finding nothing would leave the pair comparing against nothing,
|
|
14
|
+
// the vacuous-pass class `prefix-completion.ts` names.
|
|
15
|
+
import { test } from "node:test";
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
import { readFileSync } from "node:fs";
|
|
18
|
+
|
|
19
|
+
const SRC = (p) =>
|
|
20
|
+
readFileSync(new URL(`../dist/src/${p}`, import.meta.url), "utf8");
|
|
21
|
+
|
|
22
|
+
/** Extract `pattern` and return normalised group 1 — or fail, naming the file. */
|
|
23
|
+
function extract(file, pattern, what) {
|
|
24
|
+
const text = SRC(file);
|
|
25
|
+
const m = text.match(pattern);
|
|
26
|
+
assert.ok(
|
|
27
|
+
m !== null,
|
|
28
|
+
`could not find ${what} in ${file} — the pair test would compare nothing ` +
|
|
29
|
+
`(pattern: ${pattern}). Update the pattern WITH the formula.`,
|
|
30
|
+
);
|
|
31
|
+
return m[1].replace(/\s+/g, "").replace(/ctx\.store|this\.store/g, "S");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
test("123. the duplicated derived formulas agree, home by home", () => {
|
|
35
|
+
// ── (1) hubBound = ⌈√max(2, N)⌉, in traverse.ts and in graph-search.ts ──
|
|
36
|
+
// The PAIR is the floor and the root — NOT how each home obtains N: one takes
|
|
37
|
+
// it as a parameter (`boundFor(contextCount)`), the other reads it from the
|
|
38
|
+
// store. Comparing the argument would compare the two call shapes instead of
|
|
39
|
+
// the formula, which is what the drift the house recorded was about.
|
|
40
|
+
const SHAPE = /Math\.ceil\(Math\.sqrt\(Math\.max\(2,\s*[^)]*\)\)\)/;
|
|
41
|
+
const shapeOf = (file, what) => {
|
|
42
|
+
const m = SRC(file).match(SHAPE);
|
|
43
|
+
assert.ok(
|
|
44
|
+
m !== null,
|
|
45
|
+
`could not find ${what} in ${file} (pattern: ${SHAPE})`,
|
|
46
|
+
);
|
|
47
|
+
return m[0].replace(/\s+/g, "").replace(
|
|
48
|
+
/Math\.max\(2,\s*[^)]*\)/,
|
|
49
|
+
"max(2,N)",
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
assert.equal(
|
|
53
|
+
shapeOf("mind/traverse.js", "boundFor's hub-bound shape"),
|
|
54
|
+
shapeOf("mind/graph-search.js", "GraphSearch.hubBound's shape"),
|
|
55
|
+
"the hub-bound formula has drifted between traverse.ts and graph-search.ts",
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// ── (2) atomReach = max(1, ⌈N·W/256⌉), in traverse.ts and in graph-search.ts ──
|
|
59
|
+
// Each home names N and W differently (a parameter and `ctx.space.maxGroup`
|
|
60
|
+
// against the store's edge-source count and the local `W`), so the PAIR is the
|
|
61
|
+
// arithmetic shape — the `256` and the floor.
|
|
62
|
+
const reachShape = (file, what) =>
|
|
63
|
+
extract(
|
|
64
|
+
file,
|
|
65
|
+
/Math\.max\(1,\s*Math\.ceil\(\((?:[^()]|\([^()]*\))*\*\s*[A-Za-z_.]+\)\s*\/\s*(256)\)\)/,
|
|
66
|
+
what,
|
|
67
|
+
);
|
|
68
|
+
assert.equal(
|
|
69
|
+
reachShape("mind/traverse.js", "atomReach's shape"),
|
|
70
|
+
reachShape("mind/graph-search.js", "the atomsAreHubs shape"),
|
|
71
|
+
"the atom-reach formula has drifted between traverse.ts and graph-search.ts",
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
// ── (4) the interior phrase-scale allowance, in resonance.ts and attention.ts ──
|
|
75
|
+
const resonance = extract(
|
|
76
|
+
"mind/resonance.js",
|
|
77
|
+
/maxInterior\s*=\s*[^;]*?\(\s*([A-Za-z_.]+\.length\s*\+\s*[A-Za-z_.]+\.length)\s*\)\s*\*\s*([A-Za-z_.]+\.maxGroup)/,
|
|
78
|
+
"bridgeUncached's maxInterior",
|
|
79
|
+
);
|
|
80
|
+
const attention = extract(
|
|
81
|
+
"mind/attention.js",
|
|
82
|
+
/maxSiblingBytes\s*=\s*\(([A-Za-z_.]+Len\s*\+\s*[A-Za-z_.]+Len)\)\s*\*\s*([A-Za-z_.]+\.maxGroup)/,
|
|
83
|
+
"maxSiblingBytes",
|
|
84
|
+
);
|
|
85
|
+
assert.equal(
|
|
86
|
+
resonance.replace(/[A-Za-z_.]*\.length/g, "LEN"),
|
|
87
|
+
attention.replace(/[A-Za-z_.]*Len/g, "LEN"),
|
|
88
|
+
"the interior phrase-scale allowance has drifted between resonance.ts and attention.ts",
|
|
89
|
+
);
|
|
90
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// 125 — the post-grounding branch publishes WHAT IT READ.
|
|
2
|
+
//
|
|
3
|
+
// Post-grounding decides on `decided.used` and on the provenance NAME. Those
|
|
4
|
+
// operands were not in the trace, so a change to the branch could not be shown
|
|
5
|
+
// equivalent (or not) from outside: three separate investigations failed in
|
|
6
|
+
// exactly that interval. A gap in instrumentation is a defect IN the
|
|
7
|
+
// instrumentation (AGENTS.md §6) — closed on the spot, once, as COUNTS, never
|
|
8
|
+
// content.
|
|
9
|
+
import { test } from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
|
|
12
|
+
test("125. the post-grounding branch publishes its operand", async () => {
|
|
13
|
+
const { Mind, SQliteStore } = await import("../dist/src/index.js");
|
|
14
|
+
const store = new SQliteStore({ path: ":memory:" });
|
|
15
|
+
const mind = new Mind({ seed: 7, store, profile: true });
|
|
16
|
+
await mind.ingest([
|
|
17
|
+
["What is the capital of France", "The capital of France is Paris"],
|
|
18
|
+
["Paris", "Paris is famous for the Eiffel Tower"],
|
|
19
|
+
["2+2", "2+2 equals 4"],
|
|
20
|
+
["Gustaf Molander", "Gustaf Molander was a Swedish film director"],
|
|
21
|
+
]);
|
|
22
|
+
const steps = [];
|
|
23
|
+
await mind.respondText("who was Gustaf Molander", (s) => steps.push(s));
|
|
24
|
+
await store.close();
|
|
25
|
+
const pg = steps.filter((p) => p.data && p.data.usedDeclared !== undefined);
|
|
26
|
+
assert.ok(
|
|
27
|
+
pg.length >= 1,
|
|
28
|
+
`the response must emit a postGrounding step carrying the operand ` +
|
|
29
|
+
`(steps with data: ${
|
|
30
|
+
steps.filter((p) => p.data).map((p) => String(p.note).slice(0, 24))
|
|
31
|
+
.join(" | ")
|
|
32
|
+
})`,
|
|
33
|
+
);
|
|
34
|
+
const d = pg[pg.length - 1].data;
|
|
35
|
+
assert.equal(
|
|
36
|
+
typeof d.usedDeclared,
|
|
37
|
+
"boolean",
|
|
38
|
+
"usedDeclared states whether `used` arrived",
|
|
39
|
+
);
|
|
40
|
+
assert.equal(
|
|
41
|
+
typeof d.preConsumed,
|
|
42
|
+
"number",
|
|
43
|
+
"preConsumed is a COUNT, never content",
|
|
44
|
+
);
|
|
45
|
+
assert.equal(typeof d.voiced, "number", "voiced is a COUNT, never content");
|
|
46
|
+
assert.equal(
|
|
47
|
+
typeof d.provenance,
|
|
48
|
+
"string",
|
|
49
|
+
"the name the OLD branch read is published too",
|
|
50
|
+
);
|
|
51
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// 126 — the pipeline does NOT know mechanisms by name.
|
|
2
|
+
//
|
|
3
|
+
// `pipeline-mechanism.ts` promises, in its header: "it never imports a
|
|
4
|
+
// mechanism-specific type and never has a special-case branch for any
|
|
5
|
+
// mechanism". Post-grounding broke that: it decided on a provenance NAME
|
|
6
|
+
// (cast/join/recall/recall-echo) rather than on what the mechanism
|
|
7
|
+
// DECLARES in its result. The contract already has the right mould:
|
|
8
|
+
// `MechanismResult.complete`, whose doc says "the decider honours the
|
|
9
|
+
// property and never asks which mechanism set it". This test pins the
|
|
10
|
+
// property, not the implementation — no mechanism name in pipeline.js.
|
|
11
|
+
import { test } from "node:test";
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
|
|
15
|
+
const NOMES =
|
|
16
|
+
/[=!]==?\s*"(cast|join|recall|recall-echo|cover|extract|prefix|reference|alu)"/;
|
|
17
|
+
|
|
18
|
+
test("126. the pipeline branches on what a mechanism DECLARES, not on its name", () => {
|
|
19
|
+
const src = readFileSync(
|
|
20
|
+
new URL("../dist/src/mind/pipeline.js", import.meta.url),
|
|
21
|
+
"utf8",
|
|
22
|
+
);
|
|
23
|
+
// ANTI-VACUITY: the file has to be the right one and has to talk about
|
|
24
|
+
// provenance — otherwise the test would pass on an empty file.
|
|
25
|
+
assert.ok(src.length > 2000, `pipeline.js looks wrong (${src.length} bytes)`);
|
|
26
|
+
assert.ok(
|
|
27
|
+
src.includes("provenance"),
|
|
28
|
+
"pipeline.js must mention provenance at all",
|
|
29
|
+
);
|
|
30
|
+
const nomes = src.split("\n")
|
|
31
|
+
.map((l, i) => [i + 1, l])
|
|
32
|
+
.filter(([, l]) => NOMES.test(l))
|
|
33
|
+
.map(([i, l]) => `${i}: ${l.trim().slice(0, 90)}`);
|
|
34
|
+
assert.deepEqual(
|
|
35
|
+
nomes,
|
|
36
|
+
[],
|
|
37
|
+
`the pipeline compares provenance against a mechanism NAME, which the market ` +
|
|
38
|
+
`contract forbids (it must read a declaration instead):\n ${
|
|
39
|
+
nomes.join("\n ")
|
|
40
|
+
}`,
|
|
41
|
+
);
|
|
42
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// 128-the-leads-somewhere-pair-agrees.test.mjs — THE PAIR, PINNED
|
|
2
|
+
//
|
|
3
|
+
// "Does this node lead somewhere?" is answered in TWO homes, and they cannot
|
|
4
|
+
// share a name: `traverse.ts`'s `leadsSomewhere` is `cachedHasNext || hasHalo`,
|
|
5
|
+
// and `primitives.ts` spells out `hasNext || hasHalo` because `traverse.ts`
|
|
6
|
+
// imports that file (a cycle) — the comment there says so. Two spellings of
|
|
7
|
+
// one predicate is a drift risk, so this pins the AGREEMENT, node by node,
|
|
8
|
+
// through the observable surface only: `mind.leadsSomewhere(id)` against
|
|
9
|
+
// `store.hasNext(id) || store.hasHalo(id)`.
|
|
10
|
+
//
|
|
11
|
+
// It is NOT a claim that the two are the same function: one reads a memo, the
|
|
12
|
+
// other reads the store. The claim is that the memoised reading and the store
|
|
13
|
+
// reading agree — what makes the spelled-out copy correct, and what would break
|
|
14
|
+
// first if `cachedHasNext` ever outlived a store write.
|
|
15
|
+
//
|
|
16
|
+
// THE REACH OF THIS PIN, said as it is: halos are poured on the WRITE side, so
|
|
17
|
+
// a synthetic corpus of this shape carries NONE (measured: 0 nodes with a halo
|
|
18
|
+
// on a 6-fact and on a 10-fact corpus). The halo clause of the pair therefore
|
|
19
|
+
// cannot be exercised here at all — the pin's last guard asserts that measured
|
|
20
|
+
// zero, so that a future corpus which DOES carry halos turns this into a
|
|
21
|
+
// signal to extend the pin. What IS exercised: the agreement itself, node by
|
|
22
|
+
// node, over every node the corpus mints, with guards so an all-false walk
|
|
23
|
+
// cannot pass.
|
|
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
|
+
const CORPUS = [
|
|
31
|
+
"birds fly in the sky",
|
|
32
|
+
"planes fly in the sky",
|
|
33
|
+
"kites fly in the sky",
|
|
34
|
+
"arrows fly in the sky",
|
|
35
|
+
"the sky is blue",
|
|
36
|
+
"the sea is blue",
|
|
37
|
+
"ice is cold",
|
|
38
|
+
"steel is cold",
|
|
39
|
+
"water boils at one hundred degrees",
|
|
40
|
+
"iron melts at one thousand degrees",
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
test("the two homes of 'leads somewhere' agree, node by node", async () => {
|
|
44
|
+
const m = new Mind({ seed: 1, store: new SQliteStore({ path: ":memory:" }) });
|
|
45
|
+
await m.ingest(CORPUS);
|
|
46
|
+
|
|
47
|
+
const total = m.store.nodeCount();
|
|
48
|
+
let withEdge = 0;
|
|
49
|
+
let withHalo = 0;
|
|
50
|
+
for (let id = 0; id < total; id++) {
|
|
51
|
+
const named = m.leadsSomewhere(id);
|
|
52
|
+
const spelled = m.store.hasNext(id) || m.store.hasHalo(id);
|
|
53
|
+
const hasEdge = m.store.hasNext(id);
|
|
54
|
+
const hasHalo = m.store.hasHalo(id);
|
|
55
|
+
if (hasEdge) withEdge++;
|
|
56
|
+
if (hasHalo) withHalo++;
|
|
57
|
+
assert.equal(
|
|
58
|
+
named,
|
|
59
|
+
spelled,
|
|
60
|
+
`node ${id}: traverse.leadsSomewhere says ${named}, ` +
|
|
61
|
+
`primitives' spelling says ${spelled} (hasNext=${hasEdge}, hasHalo=${hasHalo})`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
assert.ok(total > 10, `corpus should mint more than 10 nodes, got ${total}`);
|
|
66
|
+
assert.ok(
|
|
67
|
+
withEdge > 0,
|
|
68
|
+
"no node in this corpus has a continuation: the edge side of the pair is untested",
|
|
69
|
+
);
|
|
70
|
+
// The halo side, measured: `withHalo` is 0 on these corpora (a 6-fact and a
|
|
71
|
+
// 10-fact one), so the halo clause of the pair cannot be exercised by a
|
|
72
|
+
// synthetic corpus of this shape — halos are poured on the WRITE side. This
|
|
73
|
+
// is the pin's declared edge, not a hidden one: the agreement is pinned over
|
|
74
|
+
// every node the corpus mints, and the halo clause needs a store that
|
|
75
|
+
// carries halos to be pinned at all.
|
|
76
|
+
assert.equal(
|
|
77
|
+
withHalo,
|
|
78
|
+
0,
|
|
79
|
+
"these corpora now carry halos: extend this pin to exercise the halo clause",
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
await m.store.close();
|
|
83
|
+
});
|