@mindrian_os/install 1.13.0-beta.24 → 1.13.0-beta.28

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.
@@ -2,25 +2,19 @@
2
2
  'use strict';
3
3
 
4
4
  /**
5
- * Phase 125-02 -- Brain Cypher chain slice (framework_chain_hint) tests.
5
+ * Phase 125-02 -- Brain framework-chain slice (framework_chain_hint) tests.
6
6
  *
7
- * Run: node --test lib/memory/brain-cypher-chain-slice.test.cjs
8
- * Exit 0 on pass. Uses node:test + node:assert/strict.
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
- * Coverage maps 1:1 to 125-02-PLAN.md Task 1 <behavior> Tests 1-10:
11
- * 1. Happy-path: 3 rows -> 3 edges, slice_scope=2, snapshot_id non-null.
12
- * 2. LIMIT 50 verbatim in the Cypher template constant.
13
- * 3. Sanitization called on each framework name; bound value is sanitized.
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
- * Plus Canon Part 8 grep audit: the Cypher template contains zero user-content
23
- * tokens (no roomDir, no artifact, no body, no transcript token).
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 + query + _test seam.
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 queryInvocations = [];
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 queryFn = (typeof o.queryFn === 'function')
51
- ? o.queryFn
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
- query: async function (cypher, params) {
56
- queryInvocations.push({ cypher: cypher, params: params });
57
- return queryFn(cypher, params);
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 (not on real brain-client).
57
+ // Exposed on the mock for the test to inspect.
63
58
  _sanitizationCalls: sanitizationCalls,
64
- _queryInvocations: queryInvocations,
59
+ _askOpInvocations: askOpInvocations,
65
60
  };
66
61
  }
67
62
 
68
- // ---------------------------------------------------------------- Test 2 (FIRST,
69
- // pure-constant check; runs without any async).
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: Cypher template constant contains LIMIT 50 verbatim', function () {
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
- && FRAMEWORK_CHAIN_SLICE_CYPHER.length > 0,
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
- assert.ok(
78
- FRAMEWORK_CHAIN_SLICE_CYPHER.includes('LIMIT 50'),
79
- 'FRAMEWORK_CHAIN_SLICE_CYPHER must contain literal "LIMIT 50"'
80
- );
81
- // Bonus: the parameterized hop range marker.
82
- assert.ok(
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', confidence: 0.9, transform_description: 'a->b', hop_distance: 1 },
109
- { from: 'A', to: 'C', confidence: 0.7, transform_description: 'a->c', hop_distance: 2 },
110
- { from: 'B', to: 'D', confidence: 0.8, transform_description: 'b->d', hop_distance: 2 },
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({ queryFn: async function () { return rows; } });
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({ queryFn: async function () { return []; } });
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
- // The bound active_frameworks param contains the SANITIZED string (no `;`).
143
- assert.equal(mock._queryInvocations.length, 1);
144
- const bound = mock._queryInvocations[0].params.active_frameworks;
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
- // The whitelist is [a-zA-Z0-9 ._-] -- letters survive, only the metacharacter
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({ queryFn: async function () { return []; } });
159
- // max_hops = 0 -> degraded, no Brain call.
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 Brain call should have been made for the two invalid hop counts.
171
- assert.equal(mock._queryInvocations.length, 0);
172
- // max_hops = 1, 2, 3 -> valid (Brain query issued).
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({ queryFn: async function () { return []; } });
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._queryInvocations.length, 1);
180
- assert.equal(m2._queryInvocations[0].params.max_hops, hops);
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 Brain call', async function () {
187
- const mock = makeMockClient({ queryFn: async function () { return []; } });
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
- // Brain query NOT called.
196
- assert.equal(mock._queryInvocations.length, 0);
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 query call', async function () {
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._queryInvocations.length, 0);
205
+ assert.equal(mock._askOpInvocations.length, 0);
215
206
  });
216
207
 
217
208
  // ---------------------------------------------------------------- Test 7
218
209
 
219
- test('Test 7: Brain query throws -> degraded; no exception propagates', async function () {
210
+ test('Test 7: askOp throws -> degraded; no exception propagates', async function () {
220
211
  const mock = makeMockClient({
221
- queryFn: async function () { throw new Error('connection refused'); },
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 preserves all 5 fields verbatim', async function () {
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({ queryFn: async function () { return [row]; } });
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 confidence + null transform_description preserved (not defaulted)', async function () {
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
- confidence: null,
270
- transform_description: null,
271
- hop_distance: 2,
266
+ hop_distance: null,
272
267
  };
273
- const mock = makeMockClient({ queryFn: async function () { return [row]; } });
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
- // Explicit null preservation per CONTEXT.md G-05 + Plan 04 schema.
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', confidence: 0.5, transform_description: 't', hop_distance: 1 },
289
+ { from: 'A', to: 'B', hop_distance: 1 },
295
290
  ];
296
- const mock = makeMockClient({ queryFn: async function () { return rows; } });
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
- // ---------------------------------------------------------------- Bonus: shape-tolerant
311
- // (the live brain-client returns { records: [] }; this verifies both shapes work).
307
+ // ---------------------------------------------------------------- Test: degraded envelope from server
312
308
 
313
- test('Bonus: { records: [...] } wrapper from brain-client.query is unwrapped', async function () {
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
- queryFn: async function () { return { records: rows }; },
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: 1,
317
+ max_hops: 2,
323
318
  brainClient: mock,
324
319
  });
325
- assert.equal(r.edges.length, 1);
326
- assert.equal(r.edges[0].from, 'X');
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
- // ---------------------------------------------------------------- Bonus: non-array
330
- // Brain return (e.g. an error payload like { error: 'foo' }) degrades cleanly.
325
+ // ---------------------------------------------------------------- Test: askOp absent on client degrades
331
326
 
332
- test('Bonus: non-array Brain return degrades cleanly', async function () {
333
- const mock = makeMockClient({
334
- queryFn: async function () { return { error: 'whatever' }; },
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: 1,
339
- brainClient: mock,
335
+ max_hops: 2,
336
+ brainClient: minimalClient,
340
337
  });
341
338
  assert.equal(r.edges.length, 0);
342
- assert.ok(r.slice_rationale.includes('brain_returned_non_array'));
339
+ assert.ok(r.slice_rationale.includes('askOp_not_available'));
343
340
  });
344
341
 
345
342
  // ---------------------------------------------------------------- Canon Part 8:
346
- // re-read the module source and grep it for forbidden tokens (defence in depth).
343
+ // Verify no raw Cypher with user-content tokens is emitted.
347
344
 
348
- test('Canon Part 8: module source contains zero user-content tokens', function () {
349
- const src = fs.readFileSync(
350
- '/home/jsagi/MindrianOS-Plugin/lib/brain/framework-chain-slice.cjs',
351
- 'utf8'
352
- );
353
- // The Cypher constant has been validated above. Here we audit that the
354
- // module never builds the query with user-content fields. The only tokens
355
- // forbidden in the Cypher *template* + *param-binding* are listed below.
356
- // We scan only the Cypher constant + the param object site.
357
- const cypherIdx = src.indexOf('FRAMEWORK_CHAIN_SLICE_CYPHER =');
358
- assert.ok(cypherIdx > 0);
359
- const cypherBlock = src.slice(cypherIdx, src.indexOf('}', cypherIdx + 200));
360
- // Cypher must not embed any user-content token literally.
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
- // Exactly 2 cypher queries expected (cross-domain is pinecone search).
396
- assert.ok(audit.query_calls.length <= 2,
397
- 'expected at most 2 query calls, got ' + audit.query_calls.length);
398
- assert.ok(audit.search_calls.length === 0,
399
- 'search should not be called when only pattern-matches + problemtype requested');
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.24",
3
+ "version": "1.13.0-beta.28",
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. Replace `$parameters` with specific values from current context
10
- 3. Call `mcp__neo4j-brain__read_neo4j_cypher` with the adapted Cypher
11
- 4. For `brain_search_semantic`, call `mcp__pinecone-brain__search-records` instead
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 run Cypher without a LIMIT clause. Never expose raw results to users -- synthesize into insights.
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
- ```cypher
22
- MATCH (current:Framework)-[r:FEEDS_INTO|TRANSFORMS_OUTPUT_TO]->(next:Framework)
23
- WHERE current.name IN $current_frameworks
24
- AND NOT next.name IN $current_frameworks
25
- OPTIONAL MATCH (next)-[:ADDRESSES_PROBLEM_TYPE]->(pt:ProblemType {name: $problem_type})
26
- RETURN next.name AS framework,
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
- **Output:** List of recommended next frameworks with confidence scores and problem-type alignment.
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