@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,109 @@
1
+ // 100-complete-grounding-trace.test.mjs — a declared-complete grounding SAYS
2
+ // SO in the rationale.
3
+ //
4
+ // THE GAP THIS CLOSES. `pipeline.ts` ends the derivation when the winning
5
+ // grounding carries `MechanismResult.complete` — the mechanism's own claim that
6
+ // the query IS a stored context, so its continuation is the whole read-out and
7
+ // a further pivot could only chain PAST the fact that produced the answer. The
8
+ // decision was correct and SILENT: no step, no note. A reader of the rationale
9
+ // therefore could not tell
10
+ //
11
+ // "the chain stopped because the query WAS the context" (complete)
12
+ //
13
+ // apart from
14
+ //
15
+ // "nothing followed" (no continuation)
16
+ //
17
+ // which are different claims about the same answer. AGENTS §6 makes that an
18
+ // instrumentation defect rather than a documentation gap: a bound that
19
+ // truncates must be reportable AT THE POINT it truncates, through the one
20
+ // surface that already exists.
21
+ //
22
+ // WHAT IS PINNED HERE.
23
+ // 1. the stop is REPORTED (step `completeGrounding`) when it happens;
24
+ // 2. the stop is REAL — the post-grounding extension did not run (no pivot or
25
+ // forward-absorb step), so the report cannot rot into a lie;
26
+ // 3. the report does NOT appear for an ordinary grounding, so it names a
27
+ // decision rather than decorating every response.
28
+ //
29
+ // The fixture is test/76's CARRIED frame: the continuation quotes its filler
30
+ // (`Run gcc <X>`), which is the shape the reference mechanism binds and
31
+ // declares complete.
32
+
33
+ import { test } from "node:test";
34
+ import assert from "node:assert/strict";
35
+ import { Mind } from "../dist/src/index.js";
36
+ import { SQliteStore } from "../dist/src/store-sqlite.js";
37
+
38
+ const CARRIED = [
39
+ ["How do I compile hello.c?", "Run gcc hello.c"],
40
+ ["How do I compile server.c?", "Run gcc server.c"],
41
+ ["How do I compile parser.c?", "Run gcc parser.c"],
42
+ ];
43
+
44
+ /** The frame fixture — the same one test/76 pins the binding on. */
45
+ async function frame() {
46
+ const m = new Mind({ seed: 7, store: new SQliteStore({ path: ":memory:" }) });
47
+ await m.ingest(CARRIED);
48
+ return m;
49
+ }
50
+
51
+ /** A grounding that is NOT declared complete: one plain learnt edge. */
52
+ async function plainChain() {
53
+ const m = new Mind({ seed: 7, store: new SQliteStore({ path: ":memory:" }) });
54
+ await m.ingest([["Eva director", "The director of Eva is Gustaf Molander."]]);
55
+ return m;
56
+ }
57
+
58
+ const moves = (steps) => steps.map((s) => s.mechanism.at(-1));
59
+
60
+ test("a binding that declares itself complete reports the stop", async () => {
61
+ const m = await frame();
62
+ const steps = [];
63
+ await m.respondText("How do I compile main.c?", (s) => steps.push(s));
64
+
65
+ const stop = steps.find((s) => s.mechanism.at(-1) === "completeGrounding");
66
+ assert.ok(stop, "the rationale must report the declared-complete stop");
67
+ assert.match(
68
+ stop.note,
69
+ /declared complete/,
70
+ "the note must carry the CLAIM, not a description of the answer",
71
+ );
72
+ });
73
+
74
+ test("the reported stop is real: the extension did not run", async () => {
75
+ const m = await frame();
76
+ const steps = [];
77
+ const answer = await m.respondText(
78
+ "How do I compile main.c?",
79
+ (s) => steps.push(s),
80
+ );
81
+
82
+ // The binding spliced the asker's referent (behaviour under test, asserted
83
+ // WITHOUT a trace below — a trace can change an answer, so the step and the
84
+ // bytes are pinned separately).
85
+ assert.ok(answer.length > 0, "the binding must answer");
86
+ const seen = moves(steps);
87
+ assert.equal(
88
+ seen.includes("pivotStep") || seen.includes("absorbForward"),
89
+ false,
90
+ "a complete grounding must not be extended",
91
+ );
92
+ });
93
+
94
+ test("the non-traced answer is the spliced one", async () => {
95
+ const m = await frame();
96
+ const answer = await m.respondText("How do I compile main.c?");
97
+ assert.equal(answer.replace(/\0+/g, "").trim(), "Run gcc main.c");
98
+ });
99
+
100
+ test("an ordinary grounding reports no complete-grounding stop", async () => {
101
+ const m = await plainChain();
102
+ const steps = [];
103
+ await m.respondText("Eva director", (s) => steps.push(s));
104
+ assert.equal(
105
+ moves(steps).includes("completeGrounding"),
106
+ false,
107
+ "the step names a decision, not every response",
108
+ );
109
+ });
@@ -0,0 +1,106 @@
1
+ // 101-alignment-gap-bound.test.mjs — the alignment's gap bound SAYS SO when it
2
+ // bites.
3
+ //
4
+ // THE GAP THIS CLOSES. `alignAround` matches a query against a stored context
5
+ // by sweeping for the next common run of ≥ W bytes, with each side's gap
6
+ // bounded by `chainReach(W)` = W². When the sweep ends with material still
7
+ // unmatched on BOTH sides, the alignment stopped because of that BOUND — and
8
+ // nothing reported it: `match.ts` emitted no trace at all. A reader of the
9
+ // rationale therefore saw a substituted answer (the frame filled with ANOTHER
10
+ // instance's filler) with no way to tell that the answer's own span had been
11
+ // refused by a cap.
12
+ //
13
+ // MEASURED BOUNDARY (W = 4, cap = 16). Against a learned
14
+ // `Book a table at <filler> tonight.` → `Your table at <filler> is booked.`
15
+ // frame, a NOVEL filler binds while it fits the cap and is replaced by a stored
16
+ // instance's filler once it does not:
17
+ //
18
+ // filler 7 B → answer quotes the asker's filler (bound)
19
+ // filler 12 B → answer quotes the asker's filler (bound)
20
+ // filler 18 B → answer carries ANOTHER filler (substituted)
21
+ // filler 30 B → answer carries ANOTHER filler (substituted)
22
+ //
23
+ // THIS FILE PINS THE REPORT, not the substitution: the step must exist and must
24
+ // carry the derived cap, so the bound is auditable at the point it truncates
25
+ // (AGENTS §6). The substitution itself is the scope defect a later lot fixes;
26
+ // when it is fixed, this file's behavioural control (a filler inside the cap is
27
+ // quoted back) stays true and the report stays reachable.
28
+
29
+ import { test } from "node:test";
30
+ import assert from "node:assert/strict";
31
+ import { Mind } from "../dist/src/index.js";
32
+ import { alignAround } from "../dist/src/mind/match.js";
33
+ import { SQliteStore } from "../dist/src/store-sqlite.js";
34
+
35
+ const WORDS =
36
+ ("alpha bravo charlie delta echo foxtrot golf hotel india juliet kilo lima " +
37
+ "mike november oscar papa quebec romeo sierra tango uniform victor whiskey " +
38
+ "xray yankee zulu amber bronze copper dahlia ember fjord gossamer harbour " +
39
+ "indigo jasmine kestrel lantern marigold nectar opal pewter quartz ripple " +
40
+ "saffron thistle umber violet willow xenon yarrow").split(" ");
41
+
42
+ const short = (i) => `${WORDS[i % 50]}${i}`;
43
+ const long = (i) =>
44
+ `${WORDS[i % 50]} ${WORDS[(i * 3) % 50]} ${WORDS[(i * 7) % 50]} street ${i}`;
45
+
46
+ /** The frame whose continuation quotes its filler, with mixed filler widths. */
47
+ async function frame(n = 60, opts = {}) {
48
+ const store = new SQliteStore({ path: ":memory:", D: 1024 });
49
+ const mind = new Mind({ seed: 7, store, ...opts });
50
+ const pairs = [];
51
+ for (let i = 0; i < n; i++) {
52
+ const f = i % 3 === 0 ? long(i) : short(i);
53
+ pairs.push([
54
+ `Book a table at ${f} tonight.`,
55
+ `Your table at ${f} is booked.`,
56
+ ]);
57
+ }
58
+ await mind.ingest(pairs);
59
+ return { store, mind };
60
+ }
61
+
62
+ /** A novel filler of exactly `len` bytes, never ingested. */
63
+ const novel = (len) => "zephyr quartz lantern ".repeat(3).slice(0, len).trim();
64
+
65
+ // THE LAW'S ASSERTION, straight on the aligner. There is no bound to report
66
+ // any more: the sweep's work is proportional to the bytes a run spans, so a
67
+ // divergence far past the old arity bound (chainReach(W) = 16) is simply
68
+ // bridged, on both sides, by finding the next common run.
69
+ test("a divergence far past the old arity bound is bridged, not truncated", () => {
70
+ const enc = new TextEncoder();
71
+ const head = "the common head of the frame ";
72
+ const tail = " and the common tail of the frame";
73
+ const q = enc.encode(head + "a".repeat(60) + tail);
74
+ const c = enc.encode(head + "b".repeat(60) + tail);
75
+ const at = head.length - 1; // the seed: the shared head's own boundary
76
+ const { matched, gaps } = alignAround(
77
+ { space: { maxGroup: 4 } },
78
+ q,
79
+ c,
80
+ at,
81
+ at,
82
+ );
83
+ assert.equal(
84
+ matched.length,
85
+ 2,
86
+ "both common runs must be found: " + JSON.stringify(matched),
87
+ );
88
+ assert.equal(gaps.length, 1, "with exactly one substitution between them");
89
+ const g = gaps[0];
90
+ assert.ok(
91
+ g.qe - g.qs >= 60 && g.ce - g.cs >= 60,
92
+ "the substitution's extent is the pair's own: " + JSON.stringify(g),
93
+ );
94
+ });
95
+
96
+ test("control: a filler inside the cap is quoted back, and the fixture is live", async () => {
97
+ const filler = novel(12);
98
+ const { mind } = await frame();
99
+ const answer = (await mind.respondText(
100
+ `Book a table at ${filler} tonight.`,
101
+ )).replace(/\0+/g, "").trim();
102
+ assert.ok(
103
+ answer.includes(filler),
104
+ "inside the cap the frame binds the asker's own filler",
105
+ );
106
+ });
@@ -0,0 +1,110 @@
1
+ // 102-production-composes-at-scale.test.mjs — a produced composite is
2
+ // decomposed by ITS OWN TREE, at every corpus size.
3
+ //
4
+ // THE GAP THIS CLOSES. `recompleteNode` re-covers a node an edge produced, and
5
+ // it seeds that re-cover from `recogniseSpan(bytes)` filtered to the node's own
6
+ // kids. Above the `atomIsHub` flip the recognition of a produced span returns
7
+ // the WHOLE and deliberately suppresses its atoms (the off-boundary
8
+ // suppression), so the kid filter admitted nothing and the chain ended at the
9
+ // intermediate composite. Measured on
10
+ //
11
+ // seed → "p q" → (p→r, q→s) → "r s" → "m n"
12
+ //
13
+ // below the flip it reached "m n" with `fuse`+`recompose`; above it stopped at
14
+ // "p q" with neither, and the trace showed `recognise("p q") ⇒ form "p q"`
15
+ // alone. The node's own kids ARE its decomposition — the machine that built
16
+ // the composite stated its parts — so they seed the re-cover directly, and a
17
+ // kid recognition already offers is skipped (below the flip the seed set is
18
+ // byte-identical to what it was).
19
+ //
20
+ // WHY THE FIXTURE CROSSES THE FLIP. The property was never false below it: the
21
+ // whole point is that composition must not depend on corpus size. The flip sits
22
+ // between N = 3 658 (composes) and N = 4 174 (did not), so the fixture ingests
23
+ // 4 400 filler pairs, exactly as test/99 does for the join.
24
+ //
25
+ // WHAT IS PINNED.
26
+ // 1. the answer above the flip EQUALS the answer below it (scale invariance,
27
+ // asserted as one comparison rather than two magic strings);
28
+ // 2. above the flip the composition is reported — `fuse` and `recompose`
29
+ // appear in the rationale, so the fix is visible in the rationale and not
30
+ // just in the bytes;
31
+ // 3. the chain still ends where the graph ends ("m n"), so the fix does not
32
+ // invent depth.
33
+
34
+ import { test } from "node:test";
35
+ import assert from "node:assert/strict";
36
+ import { Mind } from "../dist/src/index.js";
37
+ import { SQliteStore } from "../dist/src/store-sqlite.js";
38
+
39
+ const CHAIN = [
40
+ ["seed", "p q"],
41
+ ["p", "r"],
42
+ ["q", "s"],
43
+ ["r s", "m n"],
44
+ ];
45
+
46
+ const WORDS =
47
+ ("alpha bravo charlie delta echo foxtrot golf hotel india juliet kilo lima " +
48
+ "mike november oscar papa quebec romeo sierra tango uniform victor whiskey " +
49
+ "xray yankee zulu amber bronze copper dahlia ember fjord gossamer harbour " +
50
+ "indigo jasmine kestrel lantern marigold nectar opal pewter quartz ripple " +
51
+ "saffron thistle umber violet willow xenon yarrow").split(" ");
52
+
53
+ /** One store, ingested past the atomIsHub flip (N > 4 174). */
54
+ async function atScale(fillers = 4400) {
55
+ const store = new SQliteStore({ path: ":memory:", D: 1024 });
56
+ const mind = new Mind({ seed: 7, store });
57
+ const w = (i, n) => WORDS[(i * 7 + n * 13) % WORDS.length];
58
+ await mind.ingest([
59
+ ...CHAIN,
60
+ ...Array.from({ length: fillers }, (_, i) => [
61
+ `${w(i, 1)} ${w(i, 2)} ${w(i, 3)} ${i}`,
62
+ `${w(i, 4)} ${w(i, 5)} ${w(i, 6)} ${w(i, 7)} ${i}`,
63
+ ]),
64
+ ]);
65
+ return { store, mind };
66
+ }
67
+
68
+ /** The same chain BELOW the flip — the behaviour the scaled store must match. */
69
+ async function belowFlip() {
70
+ const store = new SQliteStore({ path: ":memory:", D: 1024 });
71
+ const mind = new Mind({ seed: 7, store });
72
+ await mind.ingest(CHAIN);
73
+ return { store, mind };
74
+ }
75
+
76
+ const text = (b) => b.replace(/\0+/g, "").trim();
77
+
78
+ test("a produced composite composes identically below and above the flip", async () => {
79
+ const small = await belowFlip();
80
+ const big = await atScale();
81
+ const deep = text(await big.mind.respondText("seed"));
82
+ const shallow = text(await small.mind.respondText("seed"));
83
+
84
+ // The property: depth is a fact about the graph, not about the corpus size.
85
+ assert.equal(deep, shallow, "scale must not change how deep the chain goes");
86
+ assert.equal(
87
+ deep,
88
+ "m n",
89
+ "and the graph ends at the composite's continuation",
90
+ );
91
+ });
92
+
93
+ test("above the flip the composition is reported, not just performed", async () => {
94
+ const { mind } = await atScale();
95
+ const steps = [];
96
+ await mind.respondText("seed", (s) => steps.push(s));
97
+ const moves = new Set(steps.map((s) => s.mechanism.at(-1)));
98
+ assert.ok(moves.has("fuse"), "the fused pair must appear in the rationale");
99
+ assert.ok(
100
+ moves.has("recompose"),
101
+ "and the recomposition that names the deeper form",
102
+ );
103
+ });
104
+
105
+ test("the fix does not invent depth: the chain still ends at the graph's end", async () => {
106
+ const { mind } = await atScale();
107
+ const answer = text(await mind.respondText("seed"));
108
+ assert.notEqual(answer, "r s", "it must not stop at the parts it followed");
109
+ assert.notEqual(answer, "z", "nor reach a form the graph never licensed");
110
+ });
@@ -0,0 +1,89 @@
1
+ // 103-alignment-gap-budget.test.mjs — a learned frame's slot is bounded by the
2
+ // PAIR, not by the write side's arity; the WORK is bounded by a declared budget.
3
+ //
4
+ // THE GAP THIS CLOSES. `alignAround` bounded each side's gap by
5
+ // `chainReach(W)` = W². A learnt frame whose slot is longer than that could not
6
+ // be aligned, so the frame binding produced no candidate and a cheaper
7
+ // mechanism answered instead — measured on a
8
+ // `Book a table at <filler> tonight.` → `Your table at <filler> is booked.`
9
+ // frame: at 18, 24, 30 and 36 bytes `bindReference` reported the cap and the
10
+ // answer came back carrying ANOTHER instance's filler ("bravo1").
11
+ //
12
+ // Bounding the work instead is the fix, and the two questions are genuinely
13
+ // different: the gap's LENGTH is how far the two byte strings actually diverge
14
+ // (a fact about the pair), while the WORK is how many (queryGap, contextGap)
15
+ // pairs the sweep examines, which is what a cost bound must limit — reaching a
16
+ // gap of size G costs about G²/2 pairs, so a length bound is a work bound only
17
+ // by accident, and at the wrong scale.
18
+ //
19
+ // WHAT IS PINNED: the asker's OWN filler comes back. The old defect used to be
20
+ // reproduced by injecting a tiny budget; that knob is gone because there is no
21
+ // budget any more — the sweep indexes the context's windows and walks the
22
+ // query's, so its work is proportional to the bytes a run spans and its reach is
23
+ // the bytes' own. There is nothing to make artificially small, and nothing that
24
+ // silently drops a far continuation:
25
+ //
26
+ // filler 18/20/24 B → the asker's own filler (the pair's own extent spans it)
27
+ //
28
+ // Beyond what the frame's own instances can span (measured: from ~30 bytes) a
29
+ // DIFFERENT cause takes over — the frame inventory drops to a single instance,
30
+ // and the licence correctly refuses to voice from one exemplar. That is the
31
+ // reference mechanism's own gate, not this bound, and it is not asserted here.
32
+
33
+ import { test } from "node:test";
34
+ import assert from "node:assert/strict";
35
+ import { Mind } from "../dist/src/index.js";
36
+ import { SQliteStore } from "../dist/src/store-sqlite.js";
37
+
38
+ const WORDS =
39
+ ("alpha bravo charlie delta echo foxtrot golf hotel india juliet kilo lima " +
40
+ "mike november oscar papa quebec romeo sierra tango uniform victor whiskey " +
41
+ "xray yankee zulu amber bronze copper dahlia ember fjord gossamer harbour " +
42
+ "indigo jasmine kestrel lantern marigold nectar opal pewter quartz ripple " +
43
+ "saffron thistle umber violet willow xenon yarrow").split(" ");
44
+
45
+ const short = (i) => `${WORDS[i % 50]}${i}`;
46
+ const long = (i) =>
47
+ `${WORDS[i % 50]} ${WORDS[(i * 3) % 50]} ${WORDS[(i * 7) % 50]} street ${i}`;
48
+
49
+ /** A frame whose continuation quotes its filler, plus the Mind options. */
50
+ async function frame(opts = {}, n = 60) {
51
+ const store = new SQliteStore({ path: ":memory:", D: 1024 });
52
+ const mind = new Mind({ seed: 7, store, ...opts });
53
+ const pairs = [];
54
+ for (let i = 0; i < n; i++) {
55
+ const f = i % 3 === 0 ? long(i) : short(i);
56
+ pairs.push([
57
+ `Book a table at ${f} tonight.`,
58
+ `Your table at ${f} is booked.`,
59
+ ]);
60
+ }
61
+ await mind.ingest(pairs);
62
+ return { store, mind };
63
+ }
64
+
65
+ /** A novel filler of exactly `len` bytes, never ingested. */
66
+ const novel = (len) => "zephyr quartz lantern ".repeat(4).slice(0, len).trim();
67
+
68
+ const answer = async (mind, filler) =>
69
+ (await mind.respondText(`Book a table at ${filler} tonight.`))
70
+ .replace(/\0+/g, "").trim();
71
+
72
+ test("the default budget spans it, and the answer quotes the ASKER's filler", async () => {
73
+ const filler = novel(24);
74
+ const { mind } = await frame();
75
+ const got = await answer(mind, filler);
76
+ assert.ok(
77
+ got.includes(filler),
78
+ "the pair's own extent must be reachable: the asker's bytes, not the corpus's",
79
+ );
80
+ });
81
+
82
+ test("an 18-byte slot is spanned too — the boundary moved off W²", async () => {
83
+ const { mind } = await frame();
84
+ for (const len of [18, 20, 24]) {
85
+ const filler = novel(len);
86
+ const got = await answer(mind, filler);
87
+ assert.ok(got.includes(filler), `${len} B must be spanned`);
88
+ }
89
+ });
@@ -0,0 +1,90 @@
1
+ // 104-composition-is-reported.test.mjs — every composition of an answer is
2
+ // VISIBLE in the rationale.
3
+ //
4
+ // THE GAP THIS CLOSES. `joinWithBridge` (resonance.ts) is the composition step
5
+ // every out-of-search assembly shares: it asks the corpus for a learnt
6
+ // connector between two pieces, and on a miss it joins them BARE and emits a
7
+ // `bridgeMiss` step — never silent. CAST's projection-to-continuation join
8
+ // bypassed it with a bare `concat2`, and that single bypass is the whole of the
9
+ // gluing the study measured: `"Steel is hard"` + `"wet"` came back as
10
+ // `"hardwet"`, `"eva director father"` + `"The father of Gustaf Molander…"` as
11
+ // `"fatherThe"`, the real store's `"Dutch.na"`. The house rule is NOT "never
12
+ // join bare" — it is "joined bare, and REPORTED", so that a reader of the
13
+ // rationale can see the seam and its pieces. The bypass made the seam
14
+ // invisible, which is why the gluing survived an audit that read rationales.
15
+ //
16
+ // WHAT IS PINNED. On the fixture below the join IS a miss (this tiny corpus
17
+ // attests no connector between the two pieces), so the step must exist and must
18
+ // name BOTH pieces. The answer must still USE the followed content — test/39
19
+ // pins that the follow is unaffected, and a fix that refuses the join instead
20
+ // of reporting it would silently drop the evidence.
21
+
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
+ /** test/39's corpus: a projection that must follow onto genuinely new content. */
28
+ async function fixture() {
29
+ const store = new SQliteStore({ path: ":memory:" });
30
+ const mind = new Mind({ seed: 7, store });
31
+ await mind.ingest([
32
+ ["Ice is cold", "cold"],
33
+ ["Fire is hot", "hot"],
34
+ ["Steel is hard", "hard"],
35
+ ["Water is wet", "wet"],
36
+ ["Ice is cold", "Water is wet"],
37
+ ["Something else entirely", "Water is wet"],
38
+ ]);
39
+ return mind;
40
+ }
41
+
42
+ const text = (resp) => new TextDecoder().decode(resp.bytes).replace(/\0+/g, "");
43
+
44
+ test("a composed seam is reported: the miss names its two pieces", async () => {
45
+ const mind = await fixture();
46
+ const steps = [];
47
+ const answer = text(
48
+ await mind.respond("What if steel were cold?", (s) => steps.push(s)),
49
+ );
50
+
51
+ const misses = steps.filter((s) => s.mechanism.at(-1) === "bridgeMiss");
52
+ assert.ok(
53
+ misses.length > 0,
54
+ "the composition must be visible: the shared joiner reports a miss",
55
+ );
56
+ // The seam this lot exists for: the followed continuation is joined on.
57
+ // The INTERNAL seam — the join the mechanism itself made — names the two
58
+ // pieces as they were BEFORE the join: the projection and the followed
59
+ // continuation, side by side. The answer-level assembly can only report the
60
+ // bytes it is handed, so this step is what makes the gluing auditable.
61
+ const internal = misses.some((s) => {
62
+ const parts = (s.inputs ?? []).map((i) => String(i.text));
63
+ return parts.some((t) => t.trim() === "wet") &&
64
+ parts.some((t) => t.includes("cold"));
65
+ });
66
+ assert.ok(internal, "the internal seam must be named, pre-join");
67
+ // Reporting, not refusing: the follow's content is still used (test/39).
68
+ assert.ok(
69
+ answer.includes("wet"),
70
+ `the followed content must still be used, got "${answer}"`,
71
+ );
72
+ await mind.store.close();
73
+ });
74
+
75
+ test("an in-search composition keeps its own trace (no report invented)", async () => {
76
+ // A query the corpus answers directly composes nothing that needs a bridge:
77
+ // cover's own steps are the report, so no `bridgeMiss` is fabricated.
78
+ const store = new SQliteStore({ path: ":memory:" });
79
+ const mind = new Mind({ seed: 7, store });
80
+ await mind.ingest([["Ice is cold", "Water is wet"]]);
81
+ const steps = [];
82
+ await mind.respond("Ice is cold", (s) => steps.push(s));
83
+ const misses = steps.filter((s) => s.mechanism.at(-1) === "bridgeMiss");
84
+ assert.equal(misses.length, 0, "a stored answer is not a composition");
85
+ assert.ok(
86
+ steps.some((s) => s.mechanism.at(-1) === "ground"),
87
+ "and the derivation that did answer is still traced",
88
+ );
89
+ await mind.store.close();
90
+ });
@@ -0,0 +1,137 @@
1
+ // 105-derive-through-reports-its-refusal.test.mjs — the join rule says WHY it
2
+ // did not join.
3
+ //
4
+ // THE GAP THIS CLOSES. DIRECTION (the study's gap 2): a produced fact carries
5
+ // the subject the query never wrote, and the query's remaining tail names the
6
+ // relation to follow FROM it — but on the measured chain the rule never fired
7
+ // and nothing said why. `deriveThrough` yields no rule when it refuses, and a
8
+ // rule that yields nothing leaves no step, so the gate was invisible from
9
+ // outside: four fixtures changed the SYMPTOM (`"eva director country"` answered
10
+ // with the intermediate KEY's bytes glued to the fact) without ever reaching the
11
+ // rule. AGENTS §6: a gap in instrumentation is a defect IN the instrumentation
12
+ // — close it there, once, through the rationale, never a channel of its own.
13
+ //
14
+ // WHAT IS PINNED.
15
+ // 1. the refusal is REPORTED, and it names the two pieces it tried (the fact
16
+ // and the tail), so the gate is readable in the rationale;
17
+ // 2. it is reported ONLY for a form: the search asks this rule for every
18
+ // finalized out with a node, one-byte outs included — measured, 68
19
+ // refusals for a single 3-relation query, all letters. W is the line the
20
+ // mind already draws between a chance overlap and a form, and a report per
21
+ // letter is noise, not instrumentation;
22
+ // 3. a fully covered query (no tail) is not a refusal at all — the rule does
23
+ // not apply, and it must not be reported as a miss.
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 F1 = "The director of Eva is Gustaf Molander.";
31
+ const F2 = "The country of Gustaf Molander is Sweden.";
32
+ const F3 = "The capital of Sweden is Stockholm.";
33
+
34
+ /** The measured chain: every relation filed under BOTH the entity and its key,
35
+ * which is how the real store files a fact (the study's round-3 dump). */
36
+ async function chain() {
37
+ const store = new SQliteStore({ path: ":memory:" });
38
+ const mind = new Mind({ seed: 7, store });
39
+ await mind.ingest([
40
+ ["eva", F1],
41
+ ["eva director", F1],
42
+ ["gustaf molander", F2],
43
+ ["gustaf molander country", F2],
44
+ ["sweden", F3],
45
+ ["sweden capital", F3],
46
+ ]);
47
+ return mind;
48
+ }
49
+
50
+ const misses = (steps) =>
51
+ steps.filter((s) => s.mechanism.at(-1) === "deriveThroughMiss");
52
+
53
+ test("the join's refusal is reported, naming the candidate and tail it tried", async () => {
54
+ const mind = await chain();
55
+ // The THREE-relation query: the second join is still refused (the rule
56
+ // concludes terminal — the study's other half), so this is where the refusal
57
+ // is observable. The two-relation one now JOINS (measured, and pinned by
58
+ // test/99's spec), which is why this test moved here.
59
+ const steps = [];
60
+ await mind.respond("eva director country capital", (s) => steps.push(s));
61
+ const got = misses(steps);
62
+ assert.ok(got.length > 0, "the join must say why it did not join");
63
+ const named = got.some((s) => {
64
+ const parts = (s.inputs ?? []).map((i) => String(i.text));
65
+ return parts.some((t) => t.includes("gustaf molander")) &&
66
+ parts.some((t) => t.includes("country"));
67
+ });
68
+ assert.ok(named, "the report must name the candidate and the tail it tried");
69
+ // …and WHERE the candidate came from: a refusal that names only bytes leaves
70
+ // the next reader guessing which proposal path produced them (three chained-
71
+ // join attempts were spent fixing paths that never proposed the offender).
72
+ assert.ok(
73
+ got.some((s) => /#\d+, from the .* source/.test(String(s.note))),
74
+ "the report must name the candidate's node and its source",
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
+ );
100
+ await mind.store.close();
101
+ });
102
+
103
+ test("a report per LETTER is noise: only forms are reported", async () => {
104
+ const mind = await chain();
105
+ const steps = [];
106
+ await mind.respond("eva director country capital", (s) => steps.push(s));
107
+ const got = misses(steps);
108
+ assert.ok(got.length > 0, "the real fact's refusal is still reported");
109
+ // STRUCTURAL, not a count: a report is only made for a FORM, so every report
110
+ // carries a piece at least one window long — no one-byte out reports a miss.
111
+ // `W` is the fixture's own geometry (the Mind's default maxGroup), the same
112
+ // line the rule uses.
113
+ const W = 4;
114
+ for (const m of got) {
115
+ const longest = Math.max(
116
+ 0,
117
+ ...(m.inputs ?? []).map((i) => String(i.text ?? "").length),
118
+ );
119
+ assert.ok(
120
+ longest >= W,
121
+ `a report must be about a form, got pieces of at most ${longest} bytes`,
122
+ );
123
+ }
124
+ await mind.store.close();
125
+ });
126
+
127
+ test("a fully covered query is not a refusal", async () => {
128
+ const mind = await chain();
129
+ const steps = [];
130
+ await mind.respond("eva director", (s) => steps.push(s));
131
+ assert.equal(
132
+ misses(steps).length,
133
+ 0,
134
+ "with no tail there is nothing to join through — not a miss",
135
+ );
136
+ await mind.store.close();
137
+ });