@hviana/sema 0.8.2 → 0.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/AGENTS.md +29 -29
  2. package/TRADEMARKS.md +0 -1
  3. package/dist/src/config.d.ts +11 -0
  4. package/dist/src/config.js +2 -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 +51 -0
  8. package/dist/src/meter.js +51 -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/graph-search.js +33 -14
  14. package/dist/src/mind/match.d.ts +1 -1
  15. package/dist/src/mind/match.js +5 -3
  16. package/dist/src/mind/mechanisms/cast.js +1 -1
  17. package/dist/src/mind/mechanisms/confluence.js +24 -0
  18. package/dist/src/mind/mechanisms/recall.js +32 -4
  19. package/dist/src/mind/mind.d.ts +4 -2
  20. package/dist/src/mind/mind.js +5 -4
  21. package/dist/src/mind/pipeline-mechanism.d.ts +7 -0
  22. package/dist/src/mind/pipeline.js +41 -14
  23. package/dist/src/mind/primitives.js +9 -1
  24. package/dist/src/mind/rationale.d.ts +28 -1
  25. package/dist/src/mind/rationale.js +22 -1
  26. package/dist/src/mind/reasoning.d.ts +21 -3
  27. package/dist/src/mind/reasoning.js +73 -21
  28. package/dist/src/mind/recognition.js +4 -8
  29. package/dist/src/mind/resonance.js +20 -1
  30. package/dist/src/mind/trace.js +1 -0
  31. package/dist/src/mind/traverse.js +6 -2
  32. package/dist/src/mind/types.d.ts +36 -13
  33. package/docs/INVARIANTS.md +2 -2
  34. package/docs/architecture/bounded-reads.md +1 -1
  35. package/docs/architecture/commonality.md +2 -2
  36. package/docs/architecture/cost-model.md +2 -2
  37. package/docs/architecture/determinism.md +7 -7
  38. package/docs/architecture/match-project.md +2 -3
  39. package/docs/architecture/mechanism-market.md +10 -10
  40. package/docs/architecture/meter.md +5 -5
  41. package/docs/architecture/store.md +3 -3
  42. package/docs/failures/tempting-but-wrong.md +3 -4
  43. package/docs/harness/gates.md +2 -2
  44. package/docs/mechanisms/cast.md +2 -2
  45. package/docs/mechanisms/cover.md +2 -3
  46. package/docs/mechanisms/extraction.md +7 -7
  47. package/docs/mechanisms/recall.md +8 -9
  48. package/jsr.json +1 -1
  49. package/package.json +1 -1
  50. package/src/alu/README.md +11 -12
  51. package/src/config.ts +13 -0
  52. package/src/geometry.ts +21 -0
  53. package/src/meter.ts +51 -0
  54. package/src/mind/attention.ts +167 -16
  55. package/src/mind/canonical.ts +43 -0
  56. package/src/mind/graph-search.ts +39 -14
  57. package/src/mind/match.ts +5 -3
  58. package/src/mind/mechanisms/cast.ts +3 -1
  59. package/src/mind/mechanisms/confluence.ts +24 -0
  60. package/src/mind/mechanisms/recall.ts +32 -4
  61. package/src/mind/mind.ts +6 -4
  62. package/src/mind/pipeline-mechanism.ts +7 -0
  63. package/src/mind/pipeline.ts +49 -16
  64. package/src/mind/primitives.ts +9 -1
  65. package/src/mind/rationale.ts +35 -1
  66. package/src/mind/reasoning.ts +92 -15
  67. package/src/mind/recognition.ts +4 -8
  68. package/src/mind/resonance.ts +19 -1
  69. package/src/mind/trace.ts +1 -0
  70. package/src/mind/traverse.ts +7 -5
  71. package/src/mind/types.ts +36 -13
  72. package/test/105-derive-through-reports-its-refusal.test.mjs +24 -0
  73. package/test/118-the-join-reaches-a-key-off-the-cut.test.mjs +74 -0
  74. package/test/119-the-work-does-not-grow-with-the-corpus.test.mjs +122 -0
  75. package/test/120-composition-is-consequence.test.mjs +132 -0
  76. package/test/121-the-extension-does-not-grow-with-the-corpus.test.mjs +128 -0
  77. package/test/122-the-climb-search-does-not-grow-with-the-corpus.test.mjs +117 -0
  78. package/test/123-the-paired-formulas-agree.test.mjs +90 -0
  79. package/test/125-the-post-grounding-branch-publishes-its-operand.test.mjs +51 -0
  80. package/test/126-the-pipeline-does-not-name-mechanisms.test.mjs +42 -0
  81. package/test/128-the-leads-somewhere-pair-agrees.test.mjs +83 -0
  82. package/test/129-the-trace-payload-shape.test.mjs +164 -0
  83. package/test/32-confluence.test.mjs +68 -0
  84. package/test/38-reason-restate-guard.test.mjs +8 -2
  85. package/test/43-cast-analog-seat.test.mjs +10 -0
  86. package/test/55-cost-meter.test.mjs +859 -0
@@ -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
+ });
@@ -0,0 +1,164 @@
1
+ // 129-the-trace-payload-shape.test.mjs — THE PAYLOAD'S SHAPE, PINNED
2
+ //
3
+ // The climb's trace payload (`ClimbConsensusData`, `version: 1`) is built by
4
+ // hand next to the internal `TraceDraft` that feeds it, and the pair has
5
+ // already drifted once (`visited?` — "Absent on payloads recorded before this
6
+ // field existed"). The draft is NOT exported, so a test cannot compare the two
7
+ // side by side (and exporting it just for a test would be new plumbing the
8
+ // architecture forbids). What CAN be pinned from outside is the payload's
9
+ // SHAPE: a closed key set per section, so that a field added, removed or
10
+ // renamed on one side of the pair cannot pass unnoticed.
11
+ //
12
+ // The sets below are MEASURED from a real payload (printed before being
13
+ // written), not chosen. If this test fails, the payload changed: update the
14
+ // section it names AND the draft that feeds it AND the note in attention.ts
15
+ // that says which fields older payloads lack.
16
+ //
17
+ // The sections are optional by design (that is what keeps recorded payloads
18
+ // valid); this query produces ALL of them, and the test asserts that too, so a
19
+ // section silently disappearing is caught rather than tolerated.
20
+
21
+ import { test } from "node:test";
22
+ import assert from "node:assert/strict";
23
+ import { Mind } from "../dist/src/index.js";
24
+ import { SQliteStore } from "../dist/src/store-sqlite.js";
25
+
26
+ const CORPUS = [
27
+ ["red", "is a color"],
28
+ ["blue", "is a color"],
29
+ ["circle", "is a shape"],
30
+ ["square", "is a shape"],
31
+ ["red circle", "answer alpha"],
32
+ ["red square", "answer beta"],
33
+ ["blue circle", "answer gamma"],
34
+ ["blue square", "answer delta"],
35
+ ];
36
+
37
+ const TOP = [
38
+ "anchors",
39
+ "cache",
40
+ "candidates",
41
+ "config",
42
+ "crossRegion",
43
+ "pooling",
44
+ "reaches",
45
+ "regions",
46
+ "result",
47
+ "saturation",
48
+ "version",
49
+ ];
50
+ const REQUIRED = ["cache", "candidates", "config", "result", "version"];
51
+ const SECTIONS = {
52
+ cache: ["detailAvailable", "hit"],
53
+ config: [
54
+ "annK",
55
+ "consensusFloor",
56
+ "corpusN",
57
+ "crossRegionProbeLimit",
58
+ "dimension",
59
+ "estimatorNoise",
60
+ "hubBound",
61
+ "mode",
62
+ "naturalBreak",
63
+ ],
64
+ candidates: ["perceived", "recognised", "total"],
65
+ crossRegion: [
66
+ "eligibleRegions",
67
+ "junctionVotes",
68
+ "maximalRegions",
69
+ "probeLimit",
70
+ "probes",
71
+ "probesAttempted",
72
+ "stopReason",
73
+ "supersededOrdinaryVotes",
74
+ ],
75
+ saturation: ["hasLeading", "leadingEnd", "regionIntervals"],
76
+ pooling: ["eligibleVotes", "inputVotes", "saturationMaskedVotes"],
77
+ };
78
+ const ANCHOR = [
79
+ "anchor",
80
+ "breadth",
81
+ "candidateBreadth",
82
+ "clusters",
83
+ "commit",
84
+ "contributingEvidence",
85
+ "contributingSpans",
86
+ "contributingVotes",
87
+ "idfVote",
88
+ "peak",
89
+ "pooledVote",
90
+ "rank",
91
+ ];
92
+ const REGION = [
93
+ "annHitsExamined",
94
+ "annHitsReturned",
95
+ "annQueried",
96
+ "canonicalFailed",
97
+ "canonicalId",
98
+ "canonicalUsable",
99
+ "chunk",
100
+ "dfWeight",
101
+ "focusWeightPerRoot",
102
+ "idf",
103
+ "index",
104
+ "known",
105
+ "mutualWeight",
106
+ "ordinaryVoteProduced",
107
+ "outcome",
108
+ "reachNode",
109
+ "selected",
110
+ "source",
111
+ "span",
112
+ "superseded",
113
+ "voteWeightPerRoot",
114
+ ];
115
+
116
+ /** The payload, found the way a reader finds it: deep in the recorded steps. */
117
+ function findPayload(steps) {
118
+ const walk = (o, depth) => {
119
+ if (o === null || typeof o !== "object" || depth > 6) return null;
120
+ if (o.version === 1 && o.config && o.result) return o;
121
+ for (const v of Object.values(o)) {
122
+ const hit = walk(v, depth + 1);
123
+ if (hit) return hit;
124
+ }
125
+ return null;
126
+ };
127
+ for (const step of steps) {
128
+ const hit = walk(step.data, 0);
129
+ if (hit) return hit;
130
+ }
131
+ return null;
132
+ }
133
+
134
+ test("the climb trace payload keeps its published shape", async () => {
135
+ const m = new Mind({ seed: 1, store: new SQliteStore({ path: ":memory:" }) });
136
+ await m.ingest(CORPUS);
137
+ const steps = [];
138
+ await m.respondText("red then circle", (step) => steps.push(step));
139
+ const payload = findPayload(steps);
140
+ assert.ok(payload, "no version:1 payload with config+result was recorded");
141
+
142
+ assert.deepEqual(Object.keys(payload).sort(), TOP);
143
+ for (const key of REQUIRED) {
144
+ assert.ok(key in payload, `required section ${key} is missing`);
145
+ }
146
+ for (const [name, keys] of Object.entries(SECTIONS)) {
147
+ assert.ok(payload[name] !== undefined, `section ${name} is absent`);
148
+ assert.deepEqual(
149
+ Object.keys(payload[name]).sort(),
150
+ keys,
151
+ `section ${name} changed shape`,
152
+ );
153
+ }
154
+ assert.ok(payload.anchors.length > 0, "no anchors recorded");
155
+ assert.deepEqual(Object.keys(payload.anchors[0]).sort(), ANCHOR);
156
+ assert.ok(payload.regions.length > 0, "no regions recorded");
157
+ assert.deepEqual(Object.keys(payload.regions[0]).sort(), REGION);
158
+ assert.ok(
159
+ Array.isArray(payload.anchors[0].commit.rejectionReasons),
160
+ "an anchor's commit no longer carries its rejection reasons",
161
+ );
162
+
163
+ await m.store.close();
164
+ });
@@ -171,3 +171,71 @@ test("E1 — the intersection is seed-independent (it is exact, not resonant)",
171
171
  await m.store.close();
172
172
  }
173
173
  });
174
+
175
+ // ═══════════════════════════════════════════════════════════════════════════
176
+ // Section F — the early-exit budget: live, but never pruning
177
+ // ═══════════════════════════════════════════════════════════════════════════
178
+
179
+ test("F1 — the two streams are found well inside the early-exit's budget", async () => {
180
+ // The mechanism scans the climb's ranked anchors with two cuts: `ranked.slice(0,
181
+ // pre.k)` and an early exit after `2W` anchors that returns null when fewer than
182
+ // two constraint streams exist. Both are spec-legal (a capacity, and a formula
183
+ // over W — config.ts's default `maxGroup` is 4) but neither was DERIVED; what
184
+ // makes them legitimate is that they do not prune the case they exist to protect.
185
+ //
186
+ // MEASURED on this corpus: the two streams appear at ranks 1 and 4 against the
187
+ // budget of 8, while `ranked` is 9 — the cut IS live (it would have returned
188
+ // null at the 8th anchor) and it does NOT prune. The last assertion is the
189
+ // anti-vacuity guard: if `ranked` ever fell to 8 or below, this test would be
190
+ // pinning nothing at all.
191
+ const W = 4; // the engine's default quantum: config.ts, geometry.maxGroup
192
+ const m = new Mind({
193
+ seed: 7,
194
+ store: new SQliteStore({ path: ":memory:" }),
195
+ profile: true,
196
+ });
197
+ await m.ingest(materials);
198
+ const steps = [];
199
+ await m.respondText(
200
+ "Which material is translucent and featherlight?",
201
+ (s) => steps.push(s),
202
+ );
203
+ await m.store.close();
204
+
205
+ const findAnchors = (o, depth = 0) => {
206
+ if (o === null || typeof o !== "object" || depth > 5) return null;
207
+ if (Array.isArray(o.anchors) && o.anchors.length) return o;
208
+ for (const v of Object.values(o)) {
209
+ const r = findAnchors(v, depth + 1);
210
+ if (r) return r;
211
+ }
212
+ return null;
213
+ };
214
+ const step = steps.find((s) => findAnchors(s.data ?? s) !== null);
215
+ const td = step ? findAnchors(step.data ?? step) : null;
216
+ assert.ok(td, "the climb must report its anchors");
217
+
218
+ const meet = steps.find((s) =>
219
+ (s.mechanism ?? []).join("/").includes("intersectEvidence")
220
+ );
221
+ assert.ok(meet, "confluence must ENTER on a conjunctive query");
222
+
223
+ const ranks = (meet.inputs ?? [])
224
+ .filter((i) => typeof i.node === "number")
225
+ .map((i) => (td.anchors.find((a) => a.anchor === i.node) ?? {}).rank);
226
+ assert.ok(
227
+ ranks.length >= 2,
228
+ `the meet needs two streams (got ${ranks.length})`,
229
+ );
230
+ assert.ok(
231
+ Math.max(...ranks) < 2 * W,
232
+ `the streams must appear inside the budget (ranks ${
233
+ ranks.join(",")
234
+ } vs 2W=${2 * W})`,
235
+ );
236
+ assert.ok(
237
+ td.anchors.length > 2 * W,
238
+ `and the cut must be live on this fixture, else this test pins nothing ` +
239
+ `(ranked=${td.anchors.length}, 2W=${2 * W})`,
240
+ );
241
+ });
@@ -57,7 +57,10 @@ test("reason(): a forward hop landing on bytes already present in the query is r
57
57
  queryResolved: resolve(m, query),
58
58
  };
59
59
 
60
- const out = await reason(m, query, answer, new Set(), pre);
60
+ // `reason()` reports the WHOLE extension (bytes + what it carried + how many
61
+ // steps) so a caller can price it; these assertions measure the bytes, and
62
+ // that is all this change touches — the law they pin is untouched.
63
+ const out = (await reason(m, query, answer, new Set(), pre)).bytes;
61
64
  assert.equal(
62
65
  dec(out),
63
66
  "bridge context",
@@ -82,7 +85,10 @@ test("reason(): an ordinary forward hop onto genuinely new content is unaffected
82
85
  queryResolved: resolve(m, query),
83
86
  };
84
87
 
85
- const out = await reason(m, query, answer, new Set(), pre);
88
+ // `reason()` reports the WHOLE extension (bytes + what it carried + how many
89
+ // steps) so a caller can price it; these assertions measure the bytes, and
90
+ // that is all this change touches — the law they pin is untouched.
91
+ const out = (await reason(m, query, answer, new Set(), pre)).bytes;
86
92
  assert.equal(
87
93
  dec(out),
88
94
  "a wholly new fact never mentioned in any query",