@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.
Files changed (126) hide show
  1. package/AGENTS.md +38 -37
  2. package/README.md +17 -38
  3. package/TRADEMARKS.md +0 -1
  4. package/dist/example/demo.js +85 -34
  5. package/dist/src/config.d.ts +11 -0
  6. package/dist/src/config.js +2 -0
  7. package/dist/src/geometry.d.ts +21 -10
  8. package/dist/src/geometry.js +21 -12
  9. package/dist/src/meter.d.ts +62 -0
  10. package/dist/src/meter.js +62 -0
  11. package/dist/src/mind/articulation.js +1 -1
  12. package/dist/src/mind/attention.d.ts +4 -0
  13. package/dist/src/mind/attention.js +167 -17
  14. package/dist/src/mind/canonical.d.ts +16 -0
  15. package/dist/src/mind/canonical.js +41 -0
  16. package/dist/src/mind/derivation.d.ts +201 -0
  17. package/dist/src/mind/derivation.js +327 -0
  18. package/dist/src/mind/graph-search.d.ts +2 -1
  19. package/dist/src/mind/graph-search.js +70 -29
  20. package/dist/src/mind/match.d.ts +3 -1
  21. package/dist/src/mind/match.js +7 -3
  22. package/dist/src/mind/mechanisms/alu.js +0 -2
  23. package/dist/src/mind/mechanisms/cast.d.ts +1 -5
  24. package/dist/src/mind/mechanisms/cast.js +16 -19
  25. package/dist/src/mind/mechanisms/confluence.d.ts +0 -3
  26. package/dist/src/mind/mechanisms/confluence.js +27 -9
  27. package/dist/src/mind/mechanisms/cover.js +17 -20
  28. package/dist/src/mind/mechanisms/extraction.d.ts +0 -1
  29. package/dist/src/mind/mechanisms/extraction.js +13 -8
  30. package/dist/src/mind/mechanisms/prefix-completion.js +0 -1
  31. package/dist/src/mind/mechanisms/recall.d.ts +0 -1
  32. package/dist/src/mind/mechanisms/recall.js +40 -13
  33. package/dist/src/mind/mechanisms/reference.js +3 -4
  34. package/dist/src/mind/mind.d.ts +4 -2
  35. package/dist/src/mind/mind.js +5 -4
  36. package/dist/src/mind/pipeline-mechanism.d.ts +7 -3
  37. package/dist/src/mind/pipeline.js +136 -44
  38. package/dist/src/mind/primitives.js +9 -1
  39. package/dist/src/mind/rationale.d.ts +21 -5
  40. package/dist/src/mind/rationale.js +16 -21
  41. package/dist/src/mind/reasoning.d.ts +12 -20
  42. package/dist/src/mind/reasoning.js +190 -106
  43. package/dist/src/mind/recognition.js +4 -8
  44. package/dist/src/mind/resonance.js +20 -1
  45. package/dist/src/mind/trace.js +1 -0
  46. package/dist/src/mind/traverse.js +6 -2
  47. package/dist/src/mind/types.d.ts +36 -13
  48. package/dist/src/mind/types.js +6 -3
  49. package/docs/INDEX.md +23 -24
  50. package/docs/INVARIANTS.md +16 -17
  51. package/docs/architecture/bounded-reads.md +5 -5
  52. package/docs/architecture/closure.md +65 -0
  53. package/docs/architecture/commonality.md +29 -20
  54. package/docs/architecture/cost-model.md +7 -7
  55. package/docs/architecture/determinism.md +7 -7
  56. package/docs/architecture/exact-vs-approximate.md +4 -4
  57. package/docs/architecture/factored-machinery.md +14 -14
  58. package/docs/architecture/match-project.md +2 -3
  59. package/docs/architecture/mechanism-market.md +16 -16
  60. package/docs/architecture/meter.md +10 -11
  61. package/docs/architecture/store.md +4 -4
  62. package/docs/architecture/thresholds.md +1 -1
  63. package/docs/failures/tempting-but-wrong.md +14 -5
  64. package/docs/harness/gates.md +7 -7
  65. package/docs/mechanisms/cast.md +2 -2
  66. package/docs/mechanisms/cover.md +4 -5
  67. package/docs/mechanisms/extraction.md +7 -7
  68. package/docs/mechanisms/recall.md +8 -9
  69. package/example/demo.ts +90 -37
  70. package/jsr.json +1 -1
  71. package/package.json +1 -1
  72. package/src/alu/README.md +11 -12
  73. package/src/config.ts +13 -0
  74. package/src/geometry.ts +21 -13
  75. package/src/meter.ts +62 -0
  76. package/src/mind/articulation.ts +0 -1
  77. package/src/mind/attention.ts +169 -17
  78. package/src/mind/canonical.ts +43 -0
  79. package/src/mind/derivation.ts +473 -0
  80. package/src/mind/graph-search.ts +76 -34
  81. package/src/mind/match.ts +7 -3
  82. package/src/mind/mechanisms/alu.ts +0 -2
  83. package/src/mind/mechanisms/cast.ts +20 -22
  84. package/src/mind/mechanisms/confluence.ts +27 -13
  85. package/src/mind/mechanisms/cover.ts +17 -20
  86. package/src/mind/mechanisms/extraction.ts +13 -9
  87. package/src/mind/mechanisms/prefix-completion.ts +0 -1
  88. package/src/mind/mechanisms/recall.ts +39 -13
  89. package/src/mind/mechanisms/reference.ts +2 -3
  90. package/src/mind/mind.ts +6 -4
  91. package/src/mind/pipeline-mechanism.ts +7 -3
  92. package/src/mind/pipeline.ts +160 -52
  93. package/src/mind/primitives.ts +9 -1
  94. package/src/mind/rationale.ts +27 -23
  95. package/src/mind/reasoning.ts +227 -120
  96. package/src/mind/recognition.ts +4 -8
  97. package/src/mind/resonance.ts +19 -1
  98. package/src/mind/trace.ts +1 -0
  99. package/src/mind/traverse.ts +7 -5
  100. package/src/mind/types.ts +41 -15
  101. package/test/105-derive-through-reports-its-refusal.test.mjs +24 -0
  102. package/test/118-the-join-reaches-a-key-off-the-cut.test.mjs +74 -0
  103. package/test/119-the-work-does-not-grow-with-the-corpus.test.mjs +122 -0
  104. package/test/120-composition-is-consequence.test.mjs +132 -0
  105. package/test/121-the-extension-does-not-grow-with-the-corpus.test.mjs +128 -0
  106. package/test/122-the-climb-search-does-not-grow-with-the-corpus.test.mjs +117 -0
  107. package/test/123-the-paired-formulas-agree.test.mjs +90 -0
  108. package/test/125-the-post-grounding-branch-publishes-its-operand.test.mjs +51 -0
  109. package/test/126-the-pipeline-does-not-name-mechanisms.test.mjs +42 -0
  110. package/test/128-the-leads-somewhere-pair-agrees.test.mjs +83 -0
  111. package/test/129-the-trace-payload-shape.test.mjs +164 -0
  112. package/test/133-the-decision-point-renders-the-state.test.mjs +204 -0
  113. package/test/134-the-law-explains-the-engines-own-refusal.test.mjs +237 -0
  114. package/test/135-one-law-any-producer.test.mjs +289 -0
  115. package/test/136-the-two-named-limits.test.mjs +205 -0
  116. package/test/137-the-law-lives-once-and-below.test.mjs +400 -0
  117. package/test/138-the-remainder-drains-only-what-a-move-declares.test.mjs +62 -0
  118. package/test/139-the-witness-is-engagement-not-explanation.test.mjs +51 -0
  119. package/test/140-irrelevant-supply-does-not-change-an-answer.test.mjs +48 -0
  120. package/test/141-the-question-is-paid-at-construction.test.mjs +98 -0
  121. package/test/32-confluence.test.mjs +68 -0
  122. package/test/36-already-answered-fusion.test.mjs +20 -2
  123. package/test/37-cluster-dispersion-fusion.test.mjs +30 -3
  124. package/test/38-reason-restate-guard.test.mjs +28 -2
  125. package/test/43-cast-analog-seat.test.mjs +10 -0
  126. 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 boundary positions of `bytes` under the engine's ONE boundary rule
69
- * (geometry.ts's `contentBoundaries`), or undefined when the host has no
70
- * space to ask. The join's key is an entity plus a prefix of the tail, and
71
- * the prefix that names a stored relation ENDS on one of these boundaries —
72
- * measured, 5 of 5 accepted keys over four join-firing queries, where the
73
- * byte-by-byte scan spent 153 probes for 14 boundaries. Boundaries are
74
- * content-defined and STABLE under prefix extension, which is why a corpus
75
- * key's end is a boundary of the query's own fold of the same bytes. */
76
- contentCuts?(bytes: Uint8Array): readonly number[];
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. A consumer holding this point to consensusFloor(N) — a bar that
149
- * prices ONE region's maximally-discriminative evidence — must read `peak`,
150
- * not `vote`: six scaffolding regions summing past the floor is not the
151
- * same claim as one region clearing it. */
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.length >= W && s.bytes.length < queryLen &&
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
+ });