@hviana/sema 0.2.9 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +71 -5
- package/dist/src/derive/src/deduction.d.ts +12 -1
- package/dist/src/derive/src/deduction.js +5 -1
- package/dist/src/derive/src/index.d.ts +1 -0
- package/dist/src/geometry.d.ts +3 -30
- package/dist/src/geometry.js +330 -82
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/meter.d.ts +171 -0
- package/dist/src/meter.js +269 -0
- package/dist/src/mind/attention.d.ts +5 -4
- package/dist/src/mind/attention.js +254 -23
- package/dist/src/mind/bridge.d.ts +10 -1
- package/dist/src/mind/bridge.js +179 -10
- package/dist/src/mind/canonical.d.ts +6 -1
- package/dist/src/mind/canonical.js +6 -1
- package/dist/src/mind/graph-search.d.ts +9 -0
- package/dist/src/mind/graph-search.js +59 -19
- package/dist/src/mind/index.d.ts +2 -0
- package/dist/src/mind/junction.d.ts +10 -0
- package/dist/src/mind/junction.js +14 -0
- package/dist/src/mind/learning.d.ts +32 -4
- package/dist/src/mind/learning.js +26 -4
- package/dist/src/mind/match.d.ts +40 -0
- package/dist/src/mind/match.js +125 -1
- package/dist/src/mind/mechanisms/cast.js +63 -6
- package/dist/src/mind/mechanisms/extraction.d.ts +0 -34
- package/dist/src/mind/mechanisms/extraction.js +1 -88
- package/dist/src/mind/mechanisms/recall.d.ts +3 -0
- package/dist/src/mind/mechanisms/recall.js +77 -14
- package/dist/src/mind/mind.d.ts +59 -5
- package/dist/src/mind/mind.js +115 -93
- package/dist/src/mind/pipeline-mechanism.d.ts +33 -3
- package/dist/src/mind/pipeline-mechanism.js +179 -10
- package/dist/src/mind/pipeline.d.ts +29 -0
- package/dist/src/mind/pipeline.js +79 -21
- package/dist/src/mind/primitives.d.ts +11 -15
- package/dist/src/mind/primitives.js +47 -28
- package/dist/src/mind/reasoning.d.ts +7 -1
- package/dist/src/mind/reasoning.js +40 -8
- package/dist/src/mind/recognition.js +93 -20
- package/dist/src/mind/traverse.d.ts +11 -0
- package/dist/src/mind/traverse.js +88 -7
- package/dist/src/mind/types.d.ts +39 -5
- package/dist/src/store.d.ts +15 -0
- package/dist/src/store.js +91 -6
- package/package.json +1 -1
- package/src/derive/src/deduction.ts +15 -0
- package/src/derive/src/index.ts +1 -0
- package/src/geometry.ts +350 -122
- package/src/index.ts +1 -0
- package/src/meter.ts +333 -0
- package/src/mind/attention.ts +276 -31
- package/src/mind/bridge.ts +187 -10
- package/src/mind/canonical.ts +6 -1
- package/src/mind/graph-search.ts +60 -21
- package/src/mind/index.ts +6 -0
- package/src/mind/junction.ts +12 -0
- package/src/mind/learning.ts +46 -5
- package/src/mind/match.ts +146 -1
- package/src/mind/mechanisms/cast.ts +62 -6
- package/src/mind/mechanisms/extraction.ts +2 -103
- package/src/mind/mechanisms/recall.ts +84 -17
- package/src/mind/mind.ts +144 -99
- package/src/mind/pipeline-mechanism.ts +203 -13
- package/src/mind/pipeline.ts +144 -36
- package/src/mind/primitives.ts +49 -33
- package/src/mind/reasoning.ts +39 -7
- package/src/mind/recognition.ts +89 -19
- package/src/mind/traverse.ts +89 -8
- package/src/mind/types.ts +39 -5
- package/src/store.ts +75 -6
- package/test/14-scaling.test.mjs +17 -7
- package/test/31-audit.test.mjs +4 -1
- package/test/33-multi-candidate.test.mjs +13 -1
- package/test/36-already-answered-fusion.test.mjs +10 -3
- package/test/46-recognise-multibyte-edge.test.mjs +3 -3
- package/test/53-cross-region-probe-instrumentation.test.mjs +36 -6
- package/test/54-evidence-k-instrumentation.test.mjs +175 -0
- package/test/55-cost-meter.test.mjs +284 -0
- package/test/56-bridge-identity-admission.test.mjs +209 -0
- package/test/57-fusion-order.test.mjs +104 -0
- package/test/58-subquantum-sites.test.mjs +112 -0
- package/test/59-fold-invariance.test.mjs +226 -0
|
@@ -47,6 +47,27 @@ const SYN_ATTR_CORPUS = [
|
|
|
47
47
|
["blue square", "answer delta"],
|
|
48
48
|
];
|
|
49
49
|
|
|
50
|
+
// Structural-resonance is the LAST rung: it runs only when every DAG tier —
|
|
51
|
+
// exact junction, single- and double-synonym — found no container at all.
|
|
52
|
+
// SYN_ATTR_CORPUS cannot reach it, and no longer pretends to: every attribute
|
|
53
|
+
// there shares a context ("is a color" / "is a shape") with its neighbours, so
|
|
54
|
+
// the single-synonym tier always finds a bridge and accepts before the ladder
|
|
55
|
+
// gets this far. (It once did reach resonance, when the junction search was
|
|
56
|
+
// phase-dependent and missed containers it should have found; asserting the
|
|
57
|
+
// weaker rung still fires would now be asserting a defect.)
|
|
58
|
+
//
|
|
59
|
+
// This corpus withholds exactly what the synonym tiers need: each attribute's
|
|
60
|
+
// context is unique, so no two attributes are halo siblings, and `red` and
|
|
61
|
+
// `square` never co-occur. `red then square` therefore exhausts the DAG tiers
|
|
62
|
+
// and reaches resonance for real — one probe margin-rejected, one ineligible.
|
|
63
|
+
const NO_BRIDGE_CORPUS = [
|
|
64
|
+
["red", "warm hue"],
|
|
65
|
+
["square", "four sides"],
|
|
66
|
+
["circle", "round form"],
|
|
67
|
+
["red circle", "answer alpha"],
|
|
68
|
+
["blue square", "answer delta"],
|
|
69
|
+
];
|
|
70
|
+
|
|
50
71
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
51
72
|
// 1. ordinary-region contrastive rival.
|
|
52
73
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -204,8 +225,8 @@ test("5. double-synonym tier: probe fields report single returned zero and doubl
|
|
|
204
225
|
|
|
205
226
|
test("6. structural-resonance: eligible probes report variants, merged proposals and an examined sequence with effectiveScore = annScore * semanticConfidence", async () => {
|
|
206
227
|
const m = mk(1);
|
|
207
|
-
await m.ingest(
|
|
208
|
-
const { steps } = await trace(m, "
|
|
228
|
+
await m.ingest(NO_BRIDGE_CORPUS);
|
|
229
|
+
const { steps } = await trace(m, "red then square");
|
|
209
230
|
await m.store.close();
|
|
210
231
|
|
|
211
232
|
const step = climbStep(steps);
|
|
@@ -262,10 +283,10 @@ test("6. structural-resonance: eligible probes report variants, merged proposals
|
|
|
262
283
|
|
|
263
284
|
test("7. resonance outcomes: ineligible reasons are named and a margin-rejected probe reports a sub-noise-floor margin", async () => {
|
|
264
285
|
const mIneligible = mk(1);
|
|
265
|
-
await mIneligible.ingest(
|
|
286
|
+
await mIneligible.ingest(NO_BRIDGE_CORPUS);
|
|
266
287
|
const { steps: ineligibleSteps } = await trace(
|
|
267
288
|
mIneligible,
|
|
268
|
-
"
|
|
289
|
+
"red then square",
|
|
269
290
|
);
|
|
270
291
|
await mIneligible.store.close();
|
|
271
292
|
|
|
@@ -286,9 +307,18 @@ test("7. resonance outcomes: ineligible reasons are named and a margin-rejected
|
|
|
286
307
|
assert.equal(p.outcome, "resonance-ineligible");
|
|
287
308
|
}
|
|
288
309
|
|
|
310
|
+
// A margin rejection needs resonance to actually PRODUCE a proposal and
|
|
311
|
+
// then find it indistinguishable from its runner-up. On the tight
|
|
312
|
+
// `red then square` the single surviving proposal clears the noise floor and
|
|
313
|
+
// is accepted; spreading the two attributes apart puts many more pairs in
|
|
314
|
+
// play, and the pair that reaches resonance proposes an ANN neighbour its
|
|
315
|
+
// rival matches — which is exactly the case this field exists to report.
|
|
289
316
|
const mMargin = mk(1);
|
|
290
|
-
await mMargin.ingest(
|
|
291
|
-
const { steps: marginSteps } = await trace(
|
|
317
|
+
await mMargin.ingest(NO_BRIDGE_CORPUS);
|
|
318
|
+
const { steps: marginSteps } = await trace(
|
|
319
|
+
mMargin,
|
|
320
|
+
"red and a very long interior gap before square",
|
|
321
|
+
);
|
|
292
322
|
await mMargin.store.close();
|
|
293
323
|
const marginStep = climbStep(marginSteps);
|
|
294
324
|
const marginRejected = (marginStep.data.crossRegion?.probes ?? []).filter(
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// 54-evidence-k-instrumentation.test.mjs — the three purely-additive
|
|
2
|
+
// read-outs added for evidence-breadth measurement tooling:
|
|
3
|
+
// 1. AncestorReach.visited / maxDepth (trace-gated, serialised into
|
|
4
|
+
// ClimbConsensusData.reaches),
|
|
5
|
+
// 2. structured data payloads on "decideGrounding" / "narrowDecision",
|
|
6
|
+
// 3. ingest()'s optional onDeposit provenance callback.
|
|
7
|
+
//
|
|
8
|
+
// Every assertion here checks a READ-OUT; the final test pins that none of
|
|
9
|
+
// them changes the answer (the same invariant tests 52 §6 / 53 §9 pin for
|
|
10
|
+
// the climb instrumentation).
|
|
11
|
+
|
|
12
|
+
import { test } from "node:test";
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import { decodeText, Mind } from "../dist/src/index.js";
|
|
15
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
16
|
+
|
|
17
|
+
const mk = (seed = 1) =>
|
|
18
|
+
new Mind({ seed, store: new SQliteStore({ path: ":memory:" }) });
|
|
19
|
+
|
|
20
|
+
const CORPUS = [
|
|
21
|
+
["red", "is a color"],
|
|
22
|
+
["blue", "is a color"],
|
|
23
|
+
["circle", "is a shape"],
|
|
24
|
+
["square", "is a shape"],
|
|
25
|
+
["red circle", "answer alpha"],
|
|
26
|
+
["red square", "answer beta"],
|
|
27
|
+
["blue circle", "answer gamma"],
|
|
28
|
+
["blue square", "answer delta"],
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
async function trace(mind, q) {
|
|
32
|
+
const steps = [];
|
|
33
|
+
const ans = await mind.respondText(q, (s) => steps.push(s));
|
|
34
|
+
return { steps, ans };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
38
|
+
// 1. climb read-out: visited / maxDepth
|
|
39
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
40
|
+
|
|
41
|
+
test("1. traced climb reaches carry visited/maxDepth; untraced ones do not", async () => {
|
|
42
|
+
const m = mk(1);
|
|
43
|
+
await m.ingest(CORPUS);
|
|
44
|
+
|
|
45
|
+
const { steps } = await trace(m, "red then circle");
|
|
46
|
+
const climb = steps.find((s) => s.mechanism.at(-1) === "climbConsensus");
|
|
47
|
+
assert.ok(climb?.data?.reaches?.length > 0, "expected reach traces");
|
|
48
|
+
for (const r of climb.data.reaches) {
|
|
49
|
+
assert.equal(typeof r.visited, "number");
|
|
50
|
+
assert.equal(typeof r.maxDepth, "number");
|
|
51
|
+
assert.ok(r.visited >= 0);
|
|
52
|
+
assert.ok(r.maxDepth >= 0);
|
|
53
|
+
// A climb that processed no node cannot have ascended. (One processed
|
|
54
|
+
// node can already sit at depth 1: containment seeds start one hop up.)
|
|
55
|
+
if (r.visited === 0) assert.equal(r.maxDepth, 0);
|
|
56
|
+
// Depth is bounded by the number of processed nodes plus any
|
|
57
|
+
// transparent-chain interiors — sanity only: never negative, and zero
|
|
58
|
+
// whenever nothing was climbed.
|
|
59
|
+
assert.ok(r.maxDepth >= 0);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Untraced (no respond in flight): the same contract as `saturation` —
|
|
63
|
+
// instrumentation fields absent when no trace was requested.
|
|
64
|
+
const someNode = climb.data.reaches[0].node;
|
|
65
|
+
const reach = m.edgeAncestors(someNode, 8);
|
|
66
|
+
assert.equal(reach.visited, undefined);
|
|
67
|
+
assert.equal(reach.maxDepth, undefined);
|
|
68
|
+
await m.store.close();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
72
|
+
// 2. decideGrounding / narrowDecision data payloads
|
|
73
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
74
|
+
|
|
75
|
+
test("2. decideGrounding carries every candidate's weight/grade as data, exactly one decided", async () => {
|
|
76
|
+
const m = mk(1);
|
|
77
|
+
await m.ingest(CORPUS);
|
|
78
|
+
const { steps, ans } = await trace(m, "red then circle");
|
|
79
|
+
await m.store.close();
|
|
80
|
+
|
|
81
|
+
const dg = steps.find((s) => s.mechanism.at(-1) === "decideGrounding");
|
|
82
|
+
assert.ok(dg, "expected a decideGrounding step (≥2 candidates)");
|
|
83
|
+
assert.ok(dg.data, "decideGrounding must carry a data payload");
|
|
84
|
+
assert.equal(dg.data.version, 1);
|
|
85
|
+
assert.equal(dg.data.candidates.length, dg.inputs.length);
|
|
86
|
+
for (const c of dg.data.candidates) {
|
|
87
|
+
assert.equal(typeof c.provenance, "string");
|
|
88
|
+
assert.equal(typeof c.weight, "number");
|
|
89
|
+
assert.equal(typeof c.grade, "number");
|
|
90
|
+
assert.equal(typeof c.unexplainedBytes, "number");
|
|
91
|
+
assert.ok(c.unexplainedBytes >= 0);
|
|
92
|
+
}
|
|
93
|
+
const decided = dg.data.candidates.filter((c) => c.decided);
|
|
94
|
+
assert.equal(decided.length, 1, "exactly one candidate wins");
|
|
95
|
+
// The winner's grade is minimal — the data restates the decision rule.
|
|
96
|
+
const minGrade = Math.min(...dg.data.candidates.map((c) => c.grade));
|
|
97
|
+
assert.equal(decided[0].grade, minGrade);
|
|
98
|
+
assert.ok(ans.length > 0);
|
|
99
|
+
|
|
100
|
+
if (dg.data.runnerUpMargin !== undefined) {
|
|
101
|
+
assert.ok(dg.data.runnerUpMargin >= 0);
|
|
102
|
+
const nd = steps.find((s) => s.mechanism.at(-1) === "narrowDecision");
|
|
103
|
+
if (dg.data.runnerUpMargin <= 1) {
|
|
104
|
+
assert.ok(nd, "margin ≤ 1 must emit narrowDecision");
|
|
105
|
+
assert.equal(nd.data.version, 1);
|
|
106
|
+
assert.equal(nd.data.margin, dg.data.runnerUpMargin);
|
|
107
|
+
} else {
|
|
108
|
+
assert.equal(nd, undefined);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
114
|
+
// 3. onDeposit provenance callback
|
|
115
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
116
|
+
|
|
117
|
+
test("3. onDeposit reports one item-indexed record per ingested item, ids content-addressed", async () => {
|
|
118
|
+
const m = mk(1);
|
|
119
|
+
const reports = [];
|
|
120
|
+
await m.ingest(CORPUS, undefined, (r) => reports.push(r));
|
|
121
|
+
|
|
122
|
+
assert.equal(reports.length, CORPUS.length);
|
|
123
|
+
reports.forEach((r, i) => {
|
|
124
|
+
assert.equal(r.index, i);
|
|
125
|
+
assert.equal(r.kind, "pair");
|
|
126
|
+
assert.equal(typeof r.contextId, "number");
|
|
127
|
+
assert.equal(typeof r.continuationId, "number");
|
|
128
|
+
// The reported ids ARE the items' content-addressed nodes.
|
|
129
|
+
assert.equal(decodeText(m.store.bytes(r.contextId)), CORPUS[i][0]);
|
|
130
|
+
assert.equal(decodeText(m.store.bytes(r.continuationId)), CORPUS[i][1]);
|
|
131
|
+
});
|
|
132
|
+
// Content addressing: identical continuations share one node id.
|
|
133
|
+
const colorIds = [reports[0], reports[1]].map((r) => r.continuationId);
|
|
134
|
+
assert.equal(colorIds[0], colorIds[1]);
|
|
135
|
+
await m.store.close();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("3b. onDeposit reports bare items as kind 'one'; omitting it changes nothing", async () => {
|
|
139
|
+
const m = mk(1);
|
|
140
|
+
const reports = [];
|
|
141
|
+
await m.ingest(
|
|
142
|
+
["standalone note", ["ctx", "cont"]],
|
|
143
|
+
undefined,
|
|
144
|
+
(r) => reports.push(r),
|
|
145
|
+
);
|
|
146
|
+
assert.deepEqual(reports.map((r) => [r.index, r.kind]), [[0, "one"], [
|
|
147
|
+
1,
|
|
148
|
+
"pair",
|
|
149
|
+
]]);
|
|
150
|
+
assert.equal(reports[1].continuationId !== undefined, true);
|
|
151
|
+
assert.equal(reports[0].continuationId, undefined);
|
|
152
|
+
await m.store.close();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
156
|
+
// 4. none of the read-outs changes the answer
|
|
157
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
158
|
+
|
|
159
|
+
test("4. answers are identical with and without the new read-outs attached", async () => {
|
|
160
|
+
const ask = async (withHooks) => {
|
|
161
|
+
const m = mk(7);
|
|
162
|
+
await m.ingest(CORPUS, undefined, withHooks ? () => {} : undefined);
|
|
163
|
+
const out = [];
|
|
164
|
+
for (
|
|
165
|
+
const q of ["red then circle", "red circle blue square", "blue square"]
|
|
166
|
+
) {
|
|
167
|
+
out.push(
|
|
168
|
+
withHooks ? await m.respondText(q, () => {}) : await m.respondText(q),
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
await m.store.close();
|
|
172
|
+
return out;
|
|
173
|
+
};
|
|
174
|
+
assert.deepEqual(await ask(true), await ask(false));
|
|
175
|
+
});
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// 55-cost-meter.test.mjs — the cross-stack computational-usage meter
|
|
2
|
+
// (src/meter.ts).
|
|
3
|
+
//
|
|
4
|
+
// The meter's four contracts, one test each:
|
|
5
|
+
// 1. OFF BY DEFAULT — an unprofiled Mind never attaches one, and the store
|
|
6
|
+
// is left with no meter after a response either way.
|
|
7
|
+
// 2. NEVER READ BY INFERENCE — profiled and unprofiled answers are
|
|
8
|
+
// byte-identical, and so is the provenance.
|
|
9
|
+
// 3. COUNTS ARE DETERMINISTIC — the same query on the same store meters
|
|
10
|
+
// identically (only the millisecond fields may differ).
|
|
11
|
+
// 4. THE WHOLE STACK REPORTS — store reads, perception, recognition,
|
|
12
|
+
// the mechanism market and the graph search all appear.
|
|
13
|
+
//
|
|
14
|
+
// Plus the two aggregation helpers (sumReports, formatReport) and the
|
|
15
|
+
// multi-turn lifecycle (respondTurn meters through the SAME beginResponse /
|
|
16
|
+
// endResponse pair respond() uses).
|
|
17
|
+
|
|
18
|
+
import { test } from "node:test";
|
|
19
|
+
import assert from "node:assert/strict";
|
|
20
|
+
import { formatReport, Mind, sumReports } from "../dist/src/index.js";
|
|
21
|
+
import { SQliteStore } from "../dist/src/store-sqlite.js";
|
|
22
|
+
|
|
23
|
+
const mk = (opts = {}) =>
|
|
24
|
+
new Mind({ seed: 1, store: new SQliteStore({ path: ":memory:" }), ...opts });
|
|
25
|
+
|
|
26
|
+
/** A small store with enough structure that every layer does some work. */
|
|
27
|
+
async function trained(opts) {
|
|
28
|
+
const mind = mk(opts);
|
|
29
|
+
await mind.ingest([
|
|
30
|
+
["the capital of France is", " Paris"],
|
|
31
|
+
["the capital of Japan is", " Tokyo"],
|
|
32
|
+
["the capital of Italy is", " Rome"],
|
|
33
|
+
["Paris is in", " Europe"],
|
|
34
|
+
["Tokyo is in", " Asia"],
|
|
35
|
+
]);
|
|
36
|
+
return mind;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const QUERY = "the capital of France is";
|
|
40
|
+
|
|
41
|
+
test("1. profiling is off by default and leaves no meter on the store", async () => {
|
|
42
|
+
const mind = await trained();
|
|
43
|
+
assert.equal(mind.lastCost, null);
|
|
44
|
+
await mind.respondText(QUERY);
|
|
45
|
+
assert.equal(mind.lastCost, null, "no report without { profile: true }");
|
|
46
|
+
assert.equal(mind.store.meter, null, "store meter never attached");
|
|
47
|
+
assert.equal(mind.meter, null);
|
|
48
|
+
await mind.store.close();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("2. profiling never changes the answer", async () => {
|
|
52
|
+
const plain = await trained();
|
|
53
|
+
const profiled = await trained({ profile: true });
|
|
54
|
+
|
|
55
|
+
for (const q of [QUERY, "Paris is in", "the capital of Japan is", "zzz"]) {
|
|
56
|
+
const a = await plain.respond(q);
|
|
57
|
+
const b = await profiled.respond(q);
|
|
58
|
+
assert.deepEqual(
|
|
59
|
+
Array.from(b.bytes),
|
|
60
|
+
Array.from(a.bytes),
|
|
61
|
+
`answer changed under profiling for "${q}"`,
|
|
62
|
+
);
|
|
63
|
+
assert.equal(b.provenance, a.provenance);
|
|
64
|
+
}
|
|
65
|
+
await plain.store.close();
|
|
66
|
+
await profiled.store.close();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("3. the meter is detached and the report published after each response", async () => {
|
|
70
|
+
const mind = await trained({ profile: true });
|
|
71
|
+
await mind.respondText(QUERY);
|
|
72
|
+
|
|
73
|
+
assert.equal(mind.meter, null, "meter torn down with the response");
|
|
74
|
+
assert.equal(mind.store.meter, null, "store detached — no cross-charging");
|
|
75
|
+
|
|
76
|
+
const r = mind.lastCost;
|
|
77
|
+
assert.ok(r, "a report was published");
|
|
78
|
+
assert.equal(r.version, 1);
|
|
79
|
+
assert.equal(r.queryBytes, QUERY.length);
|
|
80
|
+
assert.ok(r.elapsedMs >= 0);
|
|
81
|
+
await mind.store.close();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("4. counters are deterministic across identical calls on identical stores", async () => {
|
|
85
|
+
const a = await trained({ profile: true });
|
|
86
|
+
const b = await trained({ profile: true });
|
|
87
|
+
await a.respondText(QUERY);
|
|
88
|
+
await b.respondText(QUERY);
|
|
89
|
+
assert.deepEqual(
|
|
90
|
+
a.lastCost.counters,
|
|
91
|
+
b.lastCost.counters,
|
|
92
|
+
"same query + same store ⇒ same work; a diff here is a real regression",
|
|
93
|
+
);
|
|
94
|
+
// Phase CALL counts are deterministic too (their millisecond totals are not).
|
|
95
|
+
const calls = (r) =>
|
|
96
|
+
Object.fromEntries(
|
|
97
|
+
Object.entries(r.phases).map(([k, v]) => [k, v.calls]),
|
|
98
|
+
);
|
|
99
|
+
assert.deepEqual(calls(a.lastCost), calls(b.lastCost));
|
|
100
|
+
await a.store.close();
|
|
101
|
+
await b.store.close();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("5. every layer of the stack reports", async () => {
|
|
105
|
+
const mind = await trained({ profile: true });
|
|
106
|
+
await mind.respondText(QUERY);
|
|
107
|
+
const c = mind.lastCost.counters;
|
|
108
|
+
|
|
109
|
+
// Store layer — identity, content, structure.
|
|
110
|
+
assert.ok(c.leafLookups > 0, "content-addressed leaf lookups counted");
|
|
111
|
+
assert.ok(c.branchLookups > 0, "content-addressed branch lookups counted");
|
|
112
|
+
assert.ok(c.byteReads > 0, "node byte reads counted");
|
|
113
|
+
assert.ok(c.bytesRead > 0, "read VOLUME counted, not just call count");
|
|
114
|
+
|
|
115
|
+
// Mind layer — perception and recognition.
|
|
116
|
+
assert.ok(c.perceptions > 0, "perceptions counted");
|
|
117
|
+
assert.ok(c.perceivedBytes >= c.perceptions, "perceived volume counted");
|
|
118
|
+
assert.ok(c.recognitions > 0, "recognitions counted");
|
|
119
|
+
assert.ok(c.resolves > 0, "identity resolutions counted");
|
|
120
|
+
|
|
121
|
+
// The mechanism market — every mechanism was offered the query.
|
|
122
|
+
assert.ok(
|
|
123
|
+
c.mechanismFloors + c.mechanismSkips >= 5,
|
|
124
|
+
"each built-in mechanism's floor() was accounted",
|
|
125
|
+
);
|
|
126
|
+
assert.ok(c.mechanismRuns > 0, "at least one mechanism ran");
|
|
127
|
+
assert.ok(c.candidates > 0, "candidates weighed");
|
|
128
|
+
|
|
129
|
+
// Phases are per-mechanism and named after the mechanism itself.
|
|
130
|
+
const phases = Object.keys(mind.lastCost.phases);
|
|
131
|
+
assert.ok(phases.includes("think"));
|
|
132
|
+
assert.ok(phases.includes("articulate"));
|
|
133
|
+
assert.ok(
|
|
134
|
+
phases.some((p) => p.endsWith(".floor")),
|
|
135
|
+
"per-mechanism floor phases present",
|
|
136
|
+
);
|
|
137
|
+
await mind.store.close();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("6. zero-valued counters are omitted, and no bookkeeping leaks in", async () => {
|
|
141
|
+
const mind = await trained({ profile: true });
|
|
142
|
+
await mind.respondText(QUERY);
|
|
143
|
+
const c = mind.lastCost.counters;
|
|
144
|
+
for (const [k, v] of Object.entries(c)) {
|
|
145
|
+
assert.notEqual(v, 0, `${k} is zero and should have been dropped`);
|
|
146
|
+
assert.ok(!k.startsWith("_"), `${k} is meter bookkeeping, not work`);
|
|
147
|
+
}
|
|
148
|
+
await mind.store.close();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("7. a bigger query costs more than a smaller one", async () => {
|
|
152
|
+
const mind = await trained({ profile: true });
|
|
153
|
+
await mind.respondText("Paris");
|
|
154
|
+
const small = mind.lastCost.counters;
|
|
155
|
+
await mind.respondText(
|
|
156
|
+
"the capital of France is and the capital of Japan is and Paris is in",
|
|
157
|
+
);
|
|
158
|
+
const big = mind.lastCost.counters;
|
|
159
|
+
assert.ok(
|
|
160
|
+
big.perceivedBytes > small.perceivedBytes,
|
|
161
|
+
"perception cost tracks query length",
|
|
162
|
+
);
|
|
163
|
+
await mind.store.close();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("8. respondTurn meters through the same lifecycle", async () => {
|
|
167
|
+
const mind = await trained({ profile: true });
|
|
168
|
+
const conv = mind.beginConversation();
|
|
169
|
+
await mind.respondTurnText(conv, "the capital of France is");
|
|
170
|
+
const first = mind.lastCost;
|
|
171
|
+
assert.ok(first, "a turn publishes a report");
|
|
172
|
+
assert.ok(first.counters.perceptions > 0);
|
|
173
|
+
assert.equal(mind.store.meter, null, "detached after the turn too");
|
|
174
|
+
|
|
175
|
+
await mind.respondTurnText(conv, "Paris is in");
|
|
176
|
+
const second = mind.lastCost;
|
|
177
|
+
assert.notEqual(second, null);
|
|
178
|
+
assert.ok(
|
|
179
|
+
second.queryBytes > first.queryBytes,
|
|
180
|
+
"a turn is metered against the ACCUMULATED context, which grows",
|
|
181
|
+
);
|
|
182
|
+
mind.endConversation(conv);
|
|
183
|
+
await mind.store.close();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("9. conversation memos survive the shared lifecycle (turn 2 re-uses turn 1)", async () => {
|
|
187
|
+
const mind = await trained({ profile: true });
|
|
188
|
+
const conv = mind.beginConversation();
|
|
189
|
+
await mind.respondTurnText(conv, "the capital of France is");
|
|
190
|
+
await mind.respondTurnText(conv, "the capital of Japan is");
|
|
191
|
+
const c = mind.lastCost.counters;
|
|
192
|
+
// The conversation's perceive/recognise memos are swapped in by
|
|
193
|
+
// beginResponse; the second turn must therefore HIT them for the prefix it
|
|
194
|
+
// shares with the first. A zero here means respondTurn stopped carrying
|
|
195
|
+
// the conversation's persistent state — the exact drift the shared
|
|
196
|
+
// lifecycle exists to prevent.
|
|
197
|
+
assert.ok(
|
|
198
|
+
(c.perceiveHits ?? 0) > 0 || (c.recogniseHits ?? 0) > 0,
|
|
199
|
+
"the prefix's earlier results were re-used across turns",
|
|
200
|
+
);
|
|
201
|
+
mind.endConversation(conv);
|
|
202
|
+
await mind.store.close();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("9b. every phase reports the work done inside it, and phases nest", async () => {
|
|
206
|
+
const mind = await trained({ profile: true });
|
|
207
|
+
await mind.respondText(QUERY);
|
|
208
|
+
const { phases, counters } = mind.lastCost;
|
|
209
|
+
|
|
210
|
+
// Each phase carries counter deltas, not just a duration.
|
|
211
|
+
const withWork = Object.entries(phases).filter(([, p]) =>
|
|
212
|
+
Object.keys(p.counters).length > 0
|
|
213
|
+
);
|
|
214
|
+
assert.ok(withWork.length > 0, "phases attribute work, not only time");
|
|
215
|
+
|
|
216
|
+
// `think` is the outermost inference phase, so its counters must DOMINATE
|
|
217
|
+
// every phase nested inside it — that is what "inclusive" means, and it is
|
|
218
|
+
// the property that makes the deltas readable as attribution.
|
|
219
|
+
const think = phases["think"];
|
|
220
|
+
assert.ok(think, "think is a phase");
|
|
221
|
+
for (const [name, p] of Object.entries(phases)) {
|
|
222
|
+
if (name === "think" || name === "articulate") continue;
|
|
223
|
+
for (const [k, v] of Object.entries(p.counters)) {
|
|
224
|
+
assert.ok(
|
|
225
|
+
(think.counters[k] ?? 0) >= v,
|
|
226
|
+
`${name}.${k}=${v} exceeds think.${k}=${think.counters[k] ?? 0} — ` +
|
|
227
|
+
`phases must nest`,
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// And no phase may claim more of a counter than the whole response spent.
|
|
232
|
+
for (const [name, p] of Object.entries(phases)) {
|
|
233
|
+
for (const [k, v] of Object.entries(p.counters)) {
|
|
234
|
+
assert.ok(
|
|
235
|
+
(counters[k] ?? 0) >= v,
|
|
236
|
+
`${name}.${k}=${v} exceeds the response total ${counters[k] ?? 0}`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
await mind.store.close();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("9c. a recursive read is one read, not one per node descended", async () => {
|
|
244
|
+
const mind = await trained({ profile: true });
|
|
245
|
+
await mind.respondText(QUERY);
|
|
246
|
+
const c = mind.lastCost.counters;
|
|
247
|
+
// bytesRead is the byte VOLUME, byteReads the number of read REQUESTS.
|
|
248
|
+
// Reconstructing a branch used to charge one read per node descended, which
|
|
249
|
+
// made byteReads track tree size — it read as ~1 byte per read. Real reads
|
|
250
|
+
// return whole forms, so the volume must comfortably exceed the count.
|
|
251
|
+
assert.ok(c.byteReads > 0 && c.bytesRead > 0);
|
|
252
|
+
assert.ok(
|
|
253
|
+
c.bytesRead > c.byteReads,
|
|
254
|
+
`bytesRead ${c.bytesRead} should exceed byteReads ${c.byteReads} — ` +
|
|
255
|
+
`a per-node charge would invert this`,
|
|
256
|
+
);
|
|
257
|
+
await mind.store.close();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("10. sumReports and formatReport aggregate a battery", async () => {
|
|
261
|
+
const mind = await trained({ profile: true });
|
|
262
|
+
const reports = [];
|
|
263
|
+
for (const q of [QUERY, "Paris is in", "the capital of Italy is"]) {
|
|
264
|
+
await mind.respondText(q);
|
|
265
|
+
reports.push(mind.lastCost);
|
|
266
|
+
}
|
|
267
|
+
const total = sumReports(reports);
|
|
268
|
+
assert.equal(total.version, 1);
|
|
269
|
+
assert.equal(
|
|
270
|
+
total.queryBytes,
|
|
271
|
+
reports.reduce((s, r) => s + r.queryBytes, 0),
|
|
272
|
+
);
|
|
273
|
+
for (const key of Object.keys(reports[0].counters)) {
|
|
274
|
+
assert.equal(
|
|
275
|
+
total.counters[key],
|
|
276
|
+
reports.reduce((s, r) => s + (r.counters[key] ?? 0), 0),
|
|
277
|
+
`${key} did not sum`,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
const text = formatReport(total);
|
|
281
|
+
assert.match(text, /^cost: /);
|
|
282
|
+
assert.match(text, /perceptions/);
|
|
283
|
+
await mind.store.close();
|
|
284
|
+
});
|