@mindrian_os/install 1.13.0-beta.24 → 1.13.0-beta.26
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/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +16 -0
- package/agents/brain-query.md +12 -15
- package/agents/grading.md +14 -26
- package/agents/investor.md +6 -7
- package/agents/research.md +1 -2
- package/commands/act.md +8 -8
- package/commands/rs-experts.md +3 -1
- package/commands/rs-explain.md +2 -2
- package/commands/rs-thesis.md +3 -1
- package/lib/agents/mva/brain-classic-traps.cjs +29 -51
- package/lib/brain/chain-recommender.cjs +14 -8
- package/lib/brain/framework-chain-slice.cjs +89 -70
- package/lib/core/brain-client.cjs +54 -0
- package/lib/core/brain-derivation-prompts.cjs +15 -10
- package/lib/core/brain-derivation.cjs +16 -2
- package/lib/core/rs-chain-feeder.cjs +62 -30
- package/lib/core/rs-nl-to-query.cjs +16 -6
- package/lib/hmi/cross-room-memory.cjs +72 -29
- package/lib/mcp/brain-router.cjs +69 -55
- package/lib/memory/brain-cypher-chain-slice.test.cjs +143 -143
- package/lib/memory/brain-derivation.test.cjs +10 -5
- package/package.json +1 -1
- package/references/brain/query-patterns.md +29 -17
|
@@ -2,25 +2,19 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* Phase 125-02 -- Brain
|
|
5
|
+
* Phase 125-02 -- Brain framework-chain slice (framework_chain_hint) tests.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* BUG 2 fix update (2026-05-22): the implementation was repointed from the
|
|
8
|
+
* admin-gated brain_query / raw-Cypher path onto brain.askOp('framework_chain_slice',
|
|
9
|
+
* { seeds, max_hops }) per .planning/brain-curated-ops-contract.md.
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* 4. max_hops bound to 1..3 enum (0 and 4 degrade; 1, 2, 3 valid).
|
|
15
|
-
* 5. Empty active_frameworks short-circuits (no Brain call).
|
|
16
|
-
* 6. Brain unreachable (isAvailable=false) degrades.
|
|
17
|
-
* 7. Brain throws degrades (slice_rationale carries brain_query_failed).
|
|
18
|
-
* 8. Result mapping: 5 fields propagate verbatim.
|
|
19
|
-
* 9. Null tolerance (G-05): null confidence + null transform_description preserved.
|
|
20
|
-
* 10. brain_snapshot_id is sha256 hash of the JSON-stringified raw rows.
|
|
11
|
+
* Tests updated to mock askOp() instead of query(). Cypher-constant-content
|
|
12
|
+
* assertions (LIMIT 50 verbatim, FEEDS_INTO*1..$max_hops) are no longer
|
|
13
|
+
* applicable since the Cypher is now frozen server-side. The constant is
|
|
14
|
+
* deprecated (empty string); Test 2 now asserts it is exported but empty.
|
|
21
15
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
16
|
+
* Run: node --test lib/memory/brain-cypher-chain-slice.test.cjs
|
|
17
|
+
* Exit 0 on pass. Uses node:test + node:assert/strict.
|
|
24
18
|
*/
|
|
25
19
|
|
|
26
20
|
const { test } = require('node:test');
|
|
@@ -34,82 +28,69 @@ const {
|
|
|
34
28
|
_test,
|
|
35
29
|
} = require('/home/jsagi/MindrianOS-Plugin/lib/brain/framework-chain-slice.cjs');
|
|
36
30
|
|
|
37
|
-
// Build a mock brainClient with stubbed isAvailable +
|
|
31
|
+
// Build a mock brainClient with stubbed isAvailable + askOp + _test seam.
|
|
38
32
|
// sanitizationCalls records every arg passed to sanitizeCypherInput so the
|
|
39
33
|
// test can assert call counts + bound-value equivalence.
|
|
34
|
+
// askOpInvocations records every call to askOp for inspection.
|
|
40
35
|
function makeMockClient(opts) {
|
|
41
36
|
const o = opts || {};
|
|
42
37
|
const sanitizationCalls = [];
|
|
43
|
-
const
|
|
38
|
+
const askOpInvocations = [];
|
|
44
39
|
const sanitizer = function (v) {
|
|
45
40
|
sanitizationCalls.push(v);
|
|
46
41
|
// Match the shipped whitelist exactly: [a-zA-Z0-9 ._-]
|
|
47
42
|
return String(v == null ? '' : v).replace(/[^a-zA-Z0-9 ._-]/g, '');
|
|
48
43
|
};
|
|
49
44
|
const isAvailable = (typeof o.available === 'boolean') ? o.available : true;
|
|
50
|
-
const
|
|
51
|
-
? o.
|
|
52
|
-
: (async function () { return []; });
|
|
45
|
+
const askOpFn = (typeof o.askOpFn === 'function')
|
|
46
|
+
? o.askOpFn
|
|
47
|
+
: (async function () { return { op: 'framework_chain_slice', source: 'neo4j_curated', count: 0, rows: [] }; });
|
|
53
48
|
return {
|
|
54
49
|
isAvailable: function () { return isAvailable; },
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return
|
|
50
|
+
askOp: async function (operation, params) {
|
|
51
|
+
askOpInvocations.push({ operation: operation, params: params });
|
|
52
|
+
return askOpFn(operation, params);
|
|
58
53
|
},
|
|
59
54
|
_test: {
|
|
60
55
|
sanitizeCypherInput: sanitizer,
|
|
61
56
|
},
|
|
62
|
-
// Exposed on the mock for the test to inspect
|
|
57
|
+
// Exposed on the mock for the test to inspect.
|
|
63
58
|
_sanitizationCalls: sanitizationCalls,
|
|
64
|
-
|
|
59
|
+
_askOpInvocations: askOpInvocations,
|
|
65
60
|
};
|
|
66
61
|
}
|
|
67
62
|
|
|
68
|
-
// ---------------------------------------------------------------- Test 2
|
|
69
|
-
//
|
|
63
|
+
// ---------------------------------------------------------------- Test 2
|
|
64
|
+
// FRAMEWORK_CHAIN_SLICE_CYPHER is now deprecated (empty string). Verify it
|
|
65
|
+
// is still exported (for backward compat) but acknowledges the change.
|
|
70
66
|
|
|
71
|
-
test('Test 2:
|
|
67
|
+
test('Test 2: FRAMEWORK_CHAIN_SLICE_CYPHER is exported (deprecated empty string)', function () {
|
|
72
68
|
assert.ok(
|
|
73
|
-
typeof FRAMEWORK_CHAIN_SLICE_CYPHER === 'string'
|
|
74
|
-
|
|
75
|
-
'FRAMEWORK_CHAIN_SLICE_CYPHER must be a non-empty string'
|
|
69
|
+
typeof FRAMEWORK_CHAIN_SLICE_CYPHER === 'string',
|
|
70
|
+
'FRAMEWORK_CHAIN_SLICE_CYPHER must still be exported as a string'
|
|
76
71
|
);
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
FRAMEWORK_CHAIN_SLICE_CYPHER.includes('FEEDS_INTO*1..$max_hops'),
|
|
84
|
-
'Cypher must include parameterized hop range "FEEDS_INTO*1..$max_hops"'
|
|
72
|
+
// Post-repoint: the Cypher is frozen server-side; the exported constant
|
|
73
|
+
// is intentionally empty to signal deprecation.
|
|
74
|
+
assert.equal(
|
|
75
|
+
FRAMEWORK_CHAIN_SLICE_CYPHER,
|
|
76
|
+
'',
|
|
77
|
+
'FRAMEWORK_CHAIN_SLICE_CYPHER must be empty after curated-op repoint'
|
|
85
78
|
);
|
|
86
|
-
// Canon Part 8 grep: no user-content tokens in the template.
|
|
87
|
-
const forbidden = ['roomDir', 'artifact', 'body', 'transcript', 'roomSlug'];
|
|
88
|
-
for (const tok of forbidden) {
|
|
89
|
-
assert.ok(
|
|
90
|
-
!FRAMEWORK_CHAIN_SLICE_CYPHER.includes(tok),
|
|
91
|
-
'Cypher must not contain user-content token "' + tok + '"'
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
// Read-only: no write keywords in the Cypher template.
|
|
95
|
-
const writeKeywords = ['MERGE', 'CREATE', 'DELETE', 'SET ', 'REMOVE'];
|
|
96
|
-
for (const kw of writeKeywords) {
|
|
97
|
-
assert.ok(
|
|
98
|
-
!FRAMEWORK_CHAIN_SLICE_CYPHER.includes(kw),
|
|
99
|
-
'Cypher must be read-only (no "' + kw + '")'
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
79
|
});
|
|
103
80
|
|
|
104
81
|
// ---------------------------------------------------------------- Test 1
|
|
105
82
|
|
|
106
83
|
test('Test 1: happy-path returns 3 mapped edges + slice_scope=2 + non-null snapshot_id', async function () {
|
|
107
84
|
const rows = [
|
|
108
|
-
{ from: 'A', to: 'B',
|
|
109
|
-
{ from: 'A', to: 'C',
|
|
110
|
-
{ from: 'B', to: 'D',
|
|
85
|
+
{ from: 'A', to: 'B', hop_distance: 1 },
|
|
86
|
+
{ from: 'A', to: 'C', hop_distance: 2 },
|
|
87
|
+
{ from: 'B', to: 'D', hop_distance: 2 },
|
|
111
88
|
];
|
|
112
|
-
const mock = makeMockClient({
|
|
89
|
+
const mock = makeMockClient({
|
|
90
|
+
askOpFn: async function () {
|
|
91
|
+
return { op: 'framework_chain_slice', source: 'neo4j_curated', count: 3, rows: rows };
|
|
92
|
+
},
|
|
93
|
+
});
|
|
113
94
|
const result = await fetchFrameworkChainSlice({
|
|
114
95
|
active_frameworks: ['Beautiful Question Framework'],
|
|
115
96
|
max_hops: 2,
|
|
@@ -122,14 +103,20 @@ test('Test 1: happy-path returns 3 mapped edges + slice_scope=2 + non-null snaps
|
|
|
122
103
|
assert.equal(result.brain_snapshot_id.length, 64); // sha256 hex
|
|
123
104
|
assert.ok(result.slice_rationale.includes('brain_reachable'));
|
|
124
105
|
assert.equal(typeof result.fetched_at, 'string');
|
|
125
|
-
// ISO 8601 sanity check.
|
|
126
106
|
assert.ok(/^\d{4}-\d{2}-\d{2}T/.test(result.fetched_at));
|
|
107
|
+
// confidence_omitted flag must be set
|
|
108
|
+
assert.equal(result.confidence_omitted, true);
|
|
127
109
|
});
|
|
128
110
|
|
|
129
111
|
// ---------------------------------------------------------------- Test 3
|
|
112
|
+
// Sanitization is still called on seed names (client-side defence-in-depth).
|
|
130
113
|
|
|
131
114
|
test('Test 3: sanitization called for every framework name; bound value is sanitized', async function () {
|
|
132
|
-
const mock = makeMockClient({
|
|
115
|
+
const mock = makeMockClient({
|
|
116
|
+
askOpFn: async function () {
|
|
117
|
+
return { op: 'framework_chain_slice', source: 'neo4j_curated', count: 0, rows: [] };
|
|
118
|
+
},
|
|
119
|
+
});
|
|
133
120
|
const dangerous = "SWOT; DROP TABLE x";
|
|
134
121
|
await fetchFrameworkChainSlice({
|
|
135
122
|
active_frameworks: [dangerous, 'Beautiful Question Framework'],
|
|
@@ -139,24 +126,24 @@ test('Test 3: sanitization called for every framework name; bound value is sanit
|
|
|
139
126
|
// Sanitizer was called on each name.
|
|
140
127
|
assert.equal(mock._sanitizationCalls.length, 2);
|
|
141
128
|
assert.equal(mock._sanitizationCalls[0], dangerous);
|
|
142
|
-
//
|
|
143
|
-
assert.equal(mock.
|
|
144
|
-
const bound = mock.
|
|
129
|
+
// askOp was called with the sanitized seeds array.
|
|
130
|
+
assert.equal(mock._askOpInvocations.length, 1);
|
|
131
|
+
const bound = mock._askOpInvocations[0].params.seeds;
|
|
145
132
|
assert.ok(Array.isArray(bound));
|
|
146
133
|
assert.ok(!bound[0].includes(';'), 'sanitized value must not contain ";"');
|
|
147
|
-
|
|
148
|
-
// `;` is stripped. The injection vector dies because the bound value is the
|
|
149
|
-
// sanitizer output (a plain identifier string), not raw user input.
|
|
150
|
-
assert.equal(bound[0], 'SWOT DROP TABLE x'); // ';' stripped, kept word chars
|
|
151
|
-
// The second framework name (clean) passes through unchanged.
|
|
134
|
+
assert.equal(bound[0], 'SWOT DROP TABLE x'); // ';' stripped
|
|
152
135
|
assert.equal(bound[1], 'Beautiful Question Framework');
|
|
153
136
|
});
|
|
154
137
|
|
|
155
138
|
// ---------------------------------------------------------------- Test 4
|
|
156
139
|
|
|
157
140
|
test('Test 4: max_hops bound to 1..3 enum; 0 and 4 degrade; 1, 2, 3 valid', async function () {
|
|
158
|
-
const mock = makeMockClient({
|
|
159
|
-
|
|
141
|
+
const mock = makeMockClient({
|
|
142
|
+
askOpFn: async function () {
|
|
143
|
+
return { op: 'framework_chain_slice', source: 'neo4j_curated', count: 0, rows: [] };
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
// max_hops = 0 -> degraded, no askOp call.
|
|
160
147
|
let r = await fetchFrameworkChainSlice({
|
|
161
148
|
active_frameworks: ['X'], max_hops: 0, brainClient: mock,
|
|
162
149
|
});
|
|
@@ -167,24 +154,28 @@ test('Test 4: max_hops bound to 1..3 enum; 0 and 4 degrade; 1, 2, 3 valid', asyn
|
|
|
167
154
|
active_frameworks: ['X'], max_hops: 4, brainClient: mock,
|
|
168
155
|
});
|
|
169
156
|
assert.ok(r.slice_rationale.includes('invalid_max_hops'));
|
|
170
|
-
// No
|
|
171
|
-
assert.equal(mock.
|
|
172
|
-
// max_hops = 1, 2, 3 -> valid (
|
|
157
|
+
// No askOp call for the two invalid hop counts.
|
|
158
|
+
assert.equal(mock._askOpInvocations.length, 0);
|
|
159
|
+
// max_hops = 1, 2, 3 -> valid (askOp called).
|
|
173
160
|
for (const hops of [1, 2, 3]) {
|
|
174
|
-
const m2 = makeMockClient({
|
|
161
|
+
const m2 = makeMockClient({
|
|
162
|
+
askOpFn: async function () {
|
|
163
|
+
return { op: 'framework_chain_slice', source: 'neo4j_curated', count: 0, rows: [] };
|
|
164
|
+
},
|
|
165
|
+
});
|
|
175
166
|
const ok = await fetchFrameworkChainSlice({
|
|
176
167
|
active_frameworks: ['X'], max_hops: hops, brainClient: m2,
|
|
177
168
|
});
|
|
178
169
|
assert.equal(ok.slice_scope, hops);
|
|
179
|
-
assert.equal(m2.
|
|
180
|
-
assert.equal(m2.
|
|
170
|
+
assert.equal(m2._askOpInvocations.length, 1);
|
|
171
|
+
assert.equal(m2._askOpInvocations[0].params.max_hops, hops);
|
|
181
172
|
}
|
|
182
173
|
});
|
|
183
174
|
|
|
184
175
|
// ---------------------------------------------------------------- Test 5
|
|
185
176
|
|
|
186
|
-
test('Test 5: empty active_frameworks short-circuits without
|
|
187
|
-
const mock = makeMockClient(
|
|
177
|
+
test('Test 5: empty active_frameworks short-circuits without askOp call', async function () {
|
|
178
|
+
const mock = makeMockClient();
|
|
188
179
|
const r = await fetchFrameworkChainSlice({
|
|
189
180
|
active_frameworks: [], max_hops: 2, brainClient: mock,
|
|
190
181
|
});
|
|
@@ -192,14 +183,14 @@ test('Test 5: empty active_frameworks short-circuits without Brain call', async
|
|
|
192
183
|
assert.equal(r.slice_scope, 2);
|
|
193
184
|
assert.ok(r.slice_rationale.includes('empty active_frameworks'));
|
|
194
185
|
assert.equal(r.brain_snapshot_id, null);
|
|
195
|
-
//
|
|
196
|
-
assert.equal(mock.
|
|
186
|
+
// askOp NOT called.
|
|
187
|
+
assert.equal(mock._askOpInvocations.length, 0);
|
|
197
188
|
assert.equal(mock._sanitizationCalls.length, 0);
|
|
198
189
|
});
|
|
199
190
|
|
|
200
191
|
// ---------------------------------------------------------------- Test 6
|
|
201
192
|
|
|
202
|
-
test('Test 6: Brain unreachable (isAvailable=false) degrades; no
|
|
193
|
+
test('Test 6: Brain unreachable (isAvailable=false) degrades; no askOp call', async function () {
|
|
203
194
|
const mock = makeMockClient({ available: false });
|
|
204
195
|
const r = await fetchFrameworkChainSlice({
|
|
205
196
|
active_frameworks: ['Beautiful Question Framework'],
|
|
@@ -211,14 +202,14 @@ test('Test 6: Brain unreachable (isAvailable=false) degrades; no query call', as
|
|
|
211
202
|
assert.equal(r.slice_rationale, 'brain_unreachable');
|
|
212
203
|
assert.equal(r.brain_snapshot_id, null);
|
|
213
204
|
assert.equal(typeof r.fetched_at, 'string');
|
|
214
|
-
assert.equal(mock.
|
|
205
|
+
assert.equal(mock._askOpInvocations.length, 0);
|
|
215
206
|
});
|
|
216
207
|
|
|
217
208
|
// ---------------------------------------------------------------- Test 7
|
|
218
209
|
|
|
219
|
-
test('Test 7:
|
|
210
|
+
test('Test 7: askOp throws -> degraded; no exception propagates', async function () {
|
|
220
211
|
const mock = makeMockClient({
|
|
221
|
-
|
|
212
|
+
askOpFn: async function () { throw new Error('connection refused'); },
|
|
222
213
|
});
|
|
223
214
|
const r = await fetchFrameworkChainSlice({
|
|
224
215
|
active_frameworks: ['X'],
|
|
@@ -236,16 +227,20 @@ test('Test 7: Brain query throws -> degraded; no exception propagates', async fu
|
|
|
236
227
|
});
|
|
237
228
|
|
|
238
229
|
// ---------------------------------------------------------------- Test 8
|
|
230
|
+
// After the repoint, edges have from/to/hop_distance; confidence and
|
|
231
|
+
// transform_description are always null (not in the curated op).
|
|
239
232
|
|
|
240
|
-
test('Test 8: result mapping
|
|
233
|
+
test('Test 8: result mapping: from/to/hop_distance propagate; confidence/transform null', async function () {
|
|
241
234
|
const row = {
|
|
242
235
|
from: 'SWOT',
|
|
243
236
|
to: 'Porter Five Forces',
|
|
244
|
-
confidence: 0.85,
|
|
245
|
-
transform_description: 'analysis -> strategy',
|
|
246
237
|
hop_distance: 1,
|
|
247
238
|
};
|
|
248
|
-
const mock = makeMockClient({
|
|
239
|
+
const mock = makeMockClient({
|
|
240
|
+
askOpFn: async function () {
|
|
241
|
+
return { op: 'framework_chain_slice', source: 'neo4j_curated', count: 1, rows: [row] };
|
|
242
|
+
},
|
|
243
|
+
});
|
|
249
244
|
const r = await fetchFrameworkChainSlice({
|
|
250
245
|
active_frameworks: ['SWOT'],
|
|
251
246
|
max_hops: 1,
|
|
@@ -255,22 +250,26 @@ test('Test 8: result mapping preserves all 5 fields verbatim', async function ()
|
|
|
255
250
|
const e = r.edges[0];
|
|
256
251
|
assert.equal(e.from, 'SWOT');
|
|
257
252
|
assert.equal(e.to, 'Porter Five Forces');
|
|
258
|
-
assert.equal(e.confidence, 0.85);
|
|
259
|
-
assert.equal(e.transform_description, 'analysis -> strategy');
|
|
260
253
|
assert.equal(e.hop_distance, 1);
|
|
254
|
+
// Curated op does not return confidence or transform_description.
|
|
255
|
+
assert.equal(e.confidence, null);
|
|
256
|
+
assert.equal(e.transform_description, null);
|
|
257
|
+
assert.equal(r.confidence_omitted, true);
|
|
261
258
|
});
|
|
262
259
|
|
|
263
260
|
// ---------------------------------------------------------------- Test 9 (G-05)
|
|
264
261
|
|
|
265
|
-
test('Test 9: null
|
|
262
|
+
test('Test 9: null hop_distance preserved; from/to strings preserved', async function () {
|
|
266
263
|
const row = {
|
|
267
264
|
from: 'A',
|
|
268
265
|
to: 'B',
|
|
269
|
-
|
|
270
|
-
transform_description: null,
|
|
271
|
-
hop_distance: 2,
|
|
266
|
+
hop_distance: null,
|
|
272
267
|
};
|
|
273
|
-
const mock = makeMockClient({
|
|
268
|
+
const mock = makeMockClient({
|
|
269
|
+
askOpFn: async function () {
|
|
270
|
+
return { op: 'framework_chain_slice', source: 'neo4j_curated', count: 1, rows: [row] };
|
|
271
|
+
},
|
|
272
|
+
});
|
|
274
273
|
const r = await fetchFrameworkChainSlice({
|
|
275
274
|
active_frameworks: ['A'],
|
|
276
275
|
max_hops: 2,
|
|
@@ -280,89 +279,90 @@ test('Test 9: null confidence + null transform_description preserved (not defaul
|
|
|
280
279
|
const e = r.edges[0];
|
|
281
280
|
assert.equal(e.from, 'A');
|
|
282
281
|
assert.equal(e.to, 'B');
|
|
283
|
-
|
|
284
|
-
assert.equal(e.confidence, null);
|
|
285
|
-
assert.equal(e.transform_description, null);
|
|
286
|
-
// hop_distance still propagates.
|
|
287
|
-
assert.equal(e.hop_distance, 2);
|
|
282
|
+
assert.equal(e.hop_distance, null);
|
|
288
283
|
});
|
|
289
284
|
|
|
290
285
|
// ---------------------------------------------------------------- Test 10
|
|
291
286
|
|
|
292
287
|
test('Test 10: brain_snapshot_id is sha256 of JSON.stringify(raw rows)', async function () {
|
|
293
288
|
const rows = [
|
|
294
|
-
{ from: 'A', to: 'B',
|
|
289
|
+
{ from: 'A', to: 'B', hop_distance: 1 },
|
|
295
290
|
];
|
|
296
|
-
const mock = makeMockClient({
|
|
291
|
+
const mock = makeMockClient({
|
|
292
|
+
askOpFn: async function () {
|
|
293
|
+
return { op: 'framework_chain_slice', source: 'neo4j_curated', count: 1, rows: rows };
|
|
294
|
+
},
|
|
295
|
+
});
|
|
297
296
|
const r = await fetchFrameworkChainSlice({
|
|
298
297
|
active_frameworks: ['A'],
|
|
299
298
|
max_hops: 1,
|
|
300
299
|
brainClient: mock,
|
|
301
300
|
});
|
|
302
|
-
// Expected hash: sha256 of JSON.stringify(rows) exactly as the implementation
|
|
303
|
-
// computes it (it is the raw rows after Cypher, before edge-shape mapping).
|
|
304
301
|
const expected = crypto.createHash('sha256')
|
|
305
302
|
.update(JSON.stringify(rows), 'utf8')
|
|
306
303
|
.digest('hex');
|
|
307
304
|
assert.equal(r.brain_snapshot_id, expected);
|
|
308
305
|
});
|
|
309
306
|
|
|
310
|
-
// ----------------------------------------------------------------
|
|
311
|
-
// (the live brain-client returns { records: [] }; this verifies both shapes work).
|
|
307
|
+
// ---------------------------------------------------------------- Test: degraded envelope from server
|
|
312
308
|
|
|
313
|
-
test('
|
|
314
|
-
const rows = [
|
|
315
|
-
{ from: 'X', to: 'Y', confidence: 0.6, transform_description: 'x->y', hop_distance: 1 },
|
|
316
|
-
];
|
|
309
|
+
test('Degraded envelope from server: edges=[], snapshot null', async function () {
|
|
317
310
|
const mock = makeMockClient({
|
|
318
|
-
|
|
311
|
+
askOpFn: async function () {
|
|
312
|
+
return { op: 'framework_chain_slice', source: 'neo4j_curated', count: 0, rows: [], degraded: true };
|
|
313
|
+
},
|
|
319
314
|
});
|
|
320
315
|
const r = await fetchFrameworkChainSlice({
|
|
321
316
|
active_frameworks: ['X'],
|
|
322
|
-
max_hops:
|
|
317
|
+
max_hops: 2,
|
|
323
318
|
brainClient: mock,
|
|
324
319
|
});
|
|
325
|
-
assert.equal(r.edges.length,
|
|
326
|
-
assert.
|
|
320
|
+
assert.equal(r.edges.length, 0);
|
|
321
|
+
assert.ok(r.slice_rationale.includes('brain_returned_degraded'));
|
|
322
|
+
assert.equal(r.brain_snapshot_id, null);
|
|
327
323
|
});
|
|
328
324
|
|
|
329
|
-
// ----------------------------------------------------------------
|
|
330
|
-
// Brain return (e.g. an error payload like { error: 'foo' }) degrades cleanly.
|
|
325
|
+
// ---------------------------------------------------------------- Test: askOp absent on client degrades
|
|
331
326
|
|
|
332
|
-
test('
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
327
|
+
test('askOp absent on brainClient: degrades without throwing', async function () {
|
|
328
|
+
// A minimal client without askOp (simulates old brain-client before update).
|
|
329
|
+
const minimalClient = {
|
|
330
|
+
isAvailable: function () { return true; },
|
|
331
|
+
_test: { sanitizeCypherInput: function (s) { return s; } },
|
|
332
|
+
};
|
|
336
333
|
const r = await fetchFrameworkChainSlice({
|
|
337
334
|
active_frameworks: ['X'],
|
|
338
|
-
max_hops:
|
|
339
|
-
brainClient:
|
|
335
|
+
max_hops: 2,
|
|
336
|
+
brainClient: minimalClient,
|
|
340
337
|
});
|
|
341
338
|
assert.equal(r.edges.length, 0);
|
|
342
|
-
assert.ok(r.slice_rationale.includes('
|
|
339
|
+
assert.ok(r.slice_rationale.includes('askOp_not_available'));
|
|
343
340
|
});
|
|
344
341
|
|
|
345
342
|
// ---------------------------------------------------------------- Canon Part 8:
|
|
346
|
-
//
|
|
343
|
+
// Verify no raw Cypher with user-content tokens is emitted.
|
|
347
344
|
|
|
348
|
-
test('Canon Part 8:
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
345
|
+
test('Canon Part 8: askOp receives only seeds (generic framework names) + max_hops', async function () {
|
|
346
|
+
const mock = makeMockClient({
|
|
347
|
+
askOpFn: async function () {
|
|
348
|
+
return { op: 'framework_chain_slice', source: 'neo4j_curated', count: 0, rows: [] };
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
await fetchFrameworkChainSlice({
|
|
352
|
+
active_frameworks: ['SWOT', 'Porter Five Forces'],
|
|
353
|
+
max_hops: 2,
|
|
354
|
+
brainClient: mock,
|
|
355
|
+
});
|
|
356
|
+
assert.equal(mock._askOpInvocations.length, 1);
|
|
357
|
+
const inv = mock._askOpInvocations[0];
|
|
358
|
+
assert.equal(inv.operation, 'framework_chain_slice');
|
|
359
|
+
// Only seeds + max_hops should be in params.
|
|
360
|
+
assert.ok(Array.isArray(inv.params.seeds));
|
|
361
|
+
assert.equal(inv.params.max_hops, 2);
|
|
362
|
+
// No user-content tokens in params.
|
|
363
|
+
const paramStr = JSON.stringify(inv.params);
|
|
361
364
|
const forbidden = ['roomDir', 'roomSlug', 'transcript', 'artifact_body'];
|
|
362
365
|
for (const tok of forbidden) {
|
|
363
|
-
assert.ok(
|
|
364
|
-
!cypherBlock.includes(tok),
|
|
365
|
-
'Cypher block must not embed user-content token "' + tok + '"'
|
|
366
|
-
);
|
|
366
|
+
assert.ok(!paramStr.includes(tok), 'params must not contain user-content token "' + tok + '"');
|
|
367
367
|
}
|
|
368
368
|
});
|
|
@@ -384,6 +384,10 @@ async function main() {
|
|
|
384
384
|
});
|
|
385
385
|
|
|
386
386
|
// Test 10: options.only_sections subset.
|
|
387
|
+
// BUG 2 fix update: 'Pattern Matches' was repointed from mode:'query' to
|
|
388
|
+
// mode:'search' (ungated brain_search). When only_sections includes
|
|
389
|
+
// 'Pattern Matches', search IS called (1 call). 'ProblemType Classification'
|
|
390
|
+
// remains mode:'query' (1 query call). Totals: 1 query + 1 search.
|
|
387
391
|
await runAsync('Test 10: only_sections subset -> only specified sections queried', async function () {
|
|
388
392
|
const audit = installMockBrainClient({});
|
|
389
393
|
const { sectionPath } = buildFixtureSection({});
|
|
@@ -392,11 +396,12 @@ async function main() {
|
|
|
392
396
|
only_sections: ['Pattern Matches', 'ProblemType Classification'],
|
|
393
397
|
});
|
|
394
398
|
assert.equal(r.success, true);
|
|
395
|
-
//
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
399
|
+
// 'Pattern Matches' -> mode:'search' -> 1 search call.
|
|
400
|
+
// 'ProblemType Classification' -> mode:'query' -> 1 query call.
|
|
401
|
+
assert.ok(audit.query_calls.length <= 1,
|
|
402
|
+
'expected at most 1 query call (ProblemType Classification), got ' + audit.query_calls.length);
|
|
403
|
+
assert.ok(audit.search_calls.length <= 1,
|
|
404
|
+
'expected at most 1 search call (Pattern Matches), got ' + audit.search_calls.length);
|
|
400
405
|
});
|
|
401
406
|
|
|
402
407
|
// Test 11: dry_run.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mindrian_os/install",
|
|
3
|
-
"version": "1.13.0-beta.
|
|
3
|
+
"version": "1.13.0-beta.26",
|
|
4
4
|
"description": "Install MindrianOS into Claude Code with one command -- `npx @mindrian_os/install`. Ships the MindrianOS plugin (Larry + PWS methodology + Data Room) plus a setup/diagnostics CLI (install/doctor/update).",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"mcp": "node bin/mindrian-mcp-server.cjs",
|
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
Agents and skills read this file on demand. To execute a pattern:
|
|
8
8
|
1. Find the named pattern below
|
|
9
|
-
2.
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
2. For patterns that use `brain_ask`: call `mcp__mindrian-brain__brain_ask` with the
|
|
10
|
+
natural-language question template shown. Read `next_gate.options[].framework` for the
|
|
11
|
+
ranked chain and `directive.guided.framework` for the matched anchor. No Cypher needed.
|
|
12
|
+
3. For `brain_search_semantic`, call `mcp__mindrian-brain__brain_search`
|
|
13
|
+
(or `mcp__pinecone-brain__search-records` as fallback).
|
|
12
14
|
|
|
13
|
-
Never
|
|
15
|
+
Never expose raw results to users -- synthesize into insights.
|
|
14
16
|
|
|
15
17
|
---
|
|
16
18
|
|
|
@@ -18,21 +20,31 @@ Never run Cypher without a LIMIT clause. Never expose raw results to users -- sy
|
|
|
18
20
|
|
|
19
21
|
**Purpose:** Given current frameworks + problem type, recommend next framework.
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
type(r) AS relation,
|
|
28
|
-
r.confidence AS confidence,
|
|
29
|
-
r.transform_description AS transform,
|
|
30
|
-
pt IS NOT NULL AS matches_problem_type
|
|
31
|
-
ORDER BY r.confidence DESC, matches_problem_type DESC
|
|
32
|
-
LIMIT 5
|
|
23
|
+
**Tool:** `mcp__mindrian-brain__brain_ask` (ungated -- works for all valid API keys)
|
|
24
|
+
|
|
25
|
+
**Question template:**
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
recommend a framework for a {problem_type} venture that has already applied {current_frameworks}
|
|
33
29
|
```
|
|
34
30
|
|
|
35
|
-
|
|
31
|
+
Example:
|
|
32
|
+
```
|
|
33
|
+
recommend a framework for a wicked problem venture that has already applied
|
|
34
|
+
"Beautiful Question Framework, Domain Selection"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**How to read the response:**
|
|
38
|
+
- `next_gate.options[].framework` -- ranked list of recommended next frameworks
|
|
39
|
+
- `next_gate.options[].confidence` -- confidence score per framework (0..1)
|
|
40
|
+
- `next_gate.options[].verb` -- canonical Canon Part 3 verb associated with this option
|
|
41
|
+
- `directive.guided.framework` -- the matched anchor framework Brain selected
|
|
42
|
+
|
|
43
|
+
**Graceful degradation:** if `brain_ask` is unavailable, use the local routing table at
|
|
44
|
+
`references/methodology/problem-types.md` -- match the problem type cell, exclude already-applied
|
|
45
|
+
frameworks, prioritize the one targeting the emptiest room section.
|
|
46
|
+
|
|
47
|
+
**Output:** Ranked next frameworks with confidence scores and problem-type alignment.
|
|
36
48
|
|
|
37
49
|
---
|
|
38
50
|
|