@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.
Files changed (114) hide show
  1. package/AGENTS.md +29 -29
  2. package/TRADEMARKS.md +0 -1
  3. package/dist/src/config.d.ts +28 -0
  4. package/dist/src/config.js +20 -0
  5. package/dist/src/geometry.d.ts +21 -0
  6. package/dist/src/geometry.js +21 -0
  7. package/dist/src/meter.d.ts +76 -0
  8. package/dist/src/meter.js +95 -0
  9. package/dist/src/mind/attention.d.ts +4 -0
  10. package/dist/src/mind/attention.js +165 -16
  11. package/dist/src/mind/canonical.d.ts +16 -0
  12. package/dist/src/mind/canonical.js +41 -0
  13. package/dist/src/mind/corpus.d.ts +40 -0
  14. package/dist/src/mind/corpus.js +149 -0
  15. package/dist/src/mind/graph-search.d.ts +7 -0
  16. package/dist/src/mind/graph-search.js +254 -24
  17. package/dist/src/mind/index.d.ts +3 -1
  18. package/dist/src/mind/index.js +1 -0
  19. package/dist/src/mind/match.d.ts +9 -4
  20. package/dist/src/mind/match.js +147 -61
  21. package/dist/src/mind/mechanisms/cast.js +19 -3
  22. package/dist/src/mind/mechanisms/confluence.js +24 -0
  23. package/dist/src/mind/mechanisms/cover.js +6 -0
  24. package/dist/src/mind/mechanisms/recall.js +32 -4
  25. package/dist/src/mind/mind.d.ts +57 -0
  26. package/dist/src/mind/mind.js +72 -1
  27. package/dist/src/mind/pipeline-mechanism.d.ts +7 -0
  28. package/dist/src/mind/pipeline.js +66 -20
  29. package/dist/src/mind/primitives.js +9 -1
  30. package/dist/src/mind/rationale.d.ts +28 -1
  31. package/dist/src/mind/rationale.js +22 -1
  32. package/dist/src/mind/reasoning.d.ts +25 -3
  33. package/dist/src/mind/reasoning.js +125 -20
  34. package/dist/src/mind/recognition.js +4 -8
  35. package/dist/src/mind/resonance.js +20 -1
  36. package/dist/src/mind/trace.js +1 -0
  37. package/dist/src/mind/traverse.js +15 -3
  38. package/dist/src/mind/types.d.ts +49 -4
  39. package/docs/INVARIANTS.md +2 -2
  40. package/docs/architecture/bounded-reads.md +1 -1
  41. package/docs/architecture/commonality.md +2 -2
  42. package/docs/architecture/cost-model.md +2 -2
  43. package/docs/architecture/determinism.md +7 -7
  44. package/docs/architecture/match-project.md +2 -3
  45. package/docs/architecture/mechanism-market.md +10 -10
  46. package/docs/architecture/meter.md +5 -5
  47. package/docs/architecture/store.md +3 -3
  48. package/docs/failures/tempting-but-wrong.md +34 -6
  49. package/docs/harness/gates.md +2 -2
  50. package/docs/mechanisms/cast.md +2 -2
  51. package/docs/mechanisms/cover.md +2 -3
  52. package/docs/mechanisms/extraction.md +7 -7
  53. package/docs/mechanisms/recall.md +8 -9
  54. package/jsr.json +1 -1
  55. package/package.json +1 -1
  56. package/src/alu/README.md +11 -12
  57. package/src/config.ts +48 -0
  58. package/src/geometry.ts +21 -0
  59. package/src/meter.ts +98 -0
  60. package/src/mind/attention.ts +167 -16
  61. package/src/mind/canonical.ts +43 -0
  62. package/src/mind/corpus.ts +202 -0
  63. package/src/mind/graph-search.ts +277 -23
  64. package/src/mind/index.ts +8 -1
  65. package/src/mind/match.ts +148 -57
  66. package/src/mind/mechanisms/cast.ts +20 -2
  67. package/src/mind/mechanisms/confluence.ts +24 -0
  68. package/src/mind/mechanisms/cover.ts +5 -0
  69. package/src/mind/mechanisms/recall.ts +32 -4
  70. package/src/mind/mind.ts +125 -0
  71. package/src/mind/pipeline-mechanism.ts +7 -0
  72. package/src/mind/pipeline.ts +79 -22
  73. package/src/mind/primitives.ts +9 -1
  74. package/src/mind/rationale.ts +35 -1
  75. package/src/mind/reasoning.ts +145 -13
  76. package/src/mind/recognition.ts +4 -8
  77. package/src/mind/resonance.ts +19 -1
  78. package/src/mind/trace.ts +1 -0
  79. package/src/mind/traverse.ts +16 -6
  80. package/src/mind/types.ts +53 -4
  81. package/test/100-complete-grounding-trace.test.mjs +109 -0
  82. package/test/101-alignment-gap-bound.test.mjs +106 -0
  83. package/test/102-production-composes-at-scale.test.mjs +110 -0
  84. package/test/103-alignment-gap-budget.test.mjs +89 -0
  85. package/test/104-composition-is-reported.test.mjs +90 -0
  86. package/test/105-derive-through-reports-its-refusal.test.mjs +137 -0
  87. package/test/106-the-join-fires.test.mjs +94 -0
  88. package/test/107-the-join-is-counted.test.mjs +81 -0
  89. package/test/108-the-join-chains.test.mjs +78 -0
  90. package/test/109-the-pivot-is-counted.test.mjs +60 -0
  91. package/test/110-the-reasoner-stops-when-the-question-is-answered.test.mjs +91 -0
  92. package/test/111-the-cover-assembly-is-counted.test.mjs +74 -0
  93. package/test/112-the-exploration-does-not-grow-with-the-hub.test.mjs +89 -0
  94. package/test/113-the-rationale-payload-is-bounded.test.mjs +84 -0
  95. package/test/114-alignment-budget-is-per-sweep.test.mjs +93 -0
  96. package/test/116-the-extension-is-gated-by-the-pipelines-own-remainder.test.mjs +100 -0
  97. package/test/117-corpus-search.test.mjs +171 -0
  98. package/test/118-the-join-reaches-a-key-off-the-cut.test.mjs +74 -0
  99. package/test/119-the-work-does-not-grow-with-the-corpus.test.mjs +122 -0
  100. package/test/120-composition-is-consequence.test.mjs +132 -0
  101. package/test/121-the-extension-does-not-grow-with-the-corpus.test.mjs +128 -0
  102. package/test/122-the-climb-search-does-not-grow-with-the-corpus.test.mjs +117 -0
  103. package/test/123-the-paired-formulas-agree.test.mjs +90 -0
  104. package/test/125-the-post-grounding-branch-publishes-its-operand.test.mjs +51 -0
  105. package/test/126-the-pipeline-does-not-name-mechanisms.test.mjs +42 -0
  106. package/test/128-the-leads-somewhere-pair-agrees.test.mjs +83 -0
  107. package/test/129-the-trace-payload-shape.test.mjs +164 -0
  108. package/test/14-scaling.test.mjs +10 -7
  109. package/test/32-confluence.test.mjs +68 -0
  110. package/test/38-reason-restate-guard.test.mjs +8 -2
  111. package/test/43-cast-analog-seat.test.mjs +10 -0
  112. package/test/55-cost-meter.test.mjs +859 -0
  113. package/test/76-reference-binding.test.mjs +6 -1
  114. 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
+ });