@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.
@@ -261,18 +261,35 @@ function buildBrainQueryContext(localContext) {
261
261
  }
262
262
 
263
263
  // ============================================================
264
- // Brain query -- SINGLE chokepoint (Tripwire #2).
264
+ // Brain search -- SINGLE chokepoint (Tripwire #2).
265
265
  // ============================================================
266
266
  //
267
267
  // The ONE AND ONLY function in this module that reaches Brain. Every
268
268
  // other code path that wants Brain enrichment routes through here.
269
+ //
270
+ // BUG FIX 2026-05-22: the former implementation called client.query(payload)
271
+ // where payload was a plain OBJECT (assembled by tryBrainHints). brain_query
272
+ // requires a Cypher STRING; passing an object causes MCP input validation to
273
+ // fail silently, making Mode A cross-room enrichment dead for all users.
274
+ //
275
+ // Fix: route the JTBD co-occurrence hint lookup through the UNGATED
276
+ // brain_search (semantic) instead. We build a generic methodology query
277
+ // string from the allow-listed context keys and call client.search().
278
+ // brain_search accepts a free-text query string and returns semantic
279
+ // matches from the Pinecone index -- no admin gate, no Cypher string
280
+ // required, no user content (the query string is derived ONLY from
281
+ // allow-listed enums and slugs per Canon Part 8).
282
+ //
283
+ // Result shape from brain_search: { matches: Array<{title,score,...}> }
284
+ // or { records: Array<...> } depending on the server version. We adapt
285
+ // both shapes in tryBrainHints below.
269
286
 
270
- async function tryBrainQuery(payload) {
287
+ async function tryBrainSearch(queryText) {
271
288
  const client = getBrainClient();
272
- if (!client || typeof client.query !== 'function') return null;
289
+ if (!client || typeof client.search !== 'function') return null;
273
290
  // The single Brain call site. (Tripwire #2: exactly one match for
274
- // /(?:brainClient|brain|client)\.query\(/ in the entire module.)
275
- return await client.query(payload);
291
+ // /(?:tryBrainSearch|client\.search)\(/ in the entire module.)
292
+ return await client.search(queryText, { topK: 3 });
276
293
  }
277
294
 
278
295
  // ============================================================
@@ -411,32 +428,58 @@ async function tryBrainHints(byJtbd) {
411
428
  const jtbds = Object.keys(byJtbd);
412
429
  if (jtbds.length === 0) return null;
413
430
 
414
- const hints = {};
415
- for (const jtbd of jtbds) {
416
- const ctx = buildBrainQueryContext({
417
- jtbd_id: jtbd,
418
- jtbd_state_enum: 'in_flight',
419
- });
420
- const payload = Object.assign({ query_type: 'jtbd_cooccurrence' }, ctx);
431
+ // Build a SINGLE generic methodology query string from the JTBD slugs.
432
+ // The query string is derived from allow-listed slug tokens only (no user
433
+ // prose, no artifact bodies, no room names). Canon Part 8 safe.
434
+ //
435
+ // We send ONE search call for all JTBDs together (rather than one per JTBD)
436
+ // to minimize Brain round-trips; the semantic results are generic methodology
437
+ // patterns applicable to any venture doing these jobs.
438
+ const safeJtbdList = jtbds
439
+ .filter(function (j) { return typeof j === 'string' && /^[a-z0-9-]{1,64}$/.test(j); })
440
+ .slice(0, 8);
441
+
442
+ if (safeJtbdList.length === 0) return null;
443
+
444
+ // Query string: fully generic methodology language (no user content).
445
+ // e.g. "methodology patterns for jtbd: prepare-pitch find-bottleneck"
446
+ const queryText = 'methodology patterns for jtbd: ' + safeJtbdList.join(' ');
447
+
448
+ // Tripwire #4: scan the outgoing query string before send.
449
+ const queryPayload = { q: queryText };
450
+ if (!payloadIsClean(queryPayload)) {
451
+ // Query audit failed; abort Mode A.
452
+ return null;
453
+ }
421
454
 
422
- // Tripwire #4: scan before send
423
- if (!payloadIsClean(payload)) {
424
- // payload audit failed; abort Mode A (pretend Brain unreachable)
425
- return null;
426
- }
455
+ let r;
456
+ try {
457
+ r = await tryBrainSearch(queryText);
458
+ } catch (err) {
459
+ // Re-throw so outer aggregateAcrossRooms can record the warning
460
+ // row. This is the "read-only Brain failure mid-render" case --
461
+ // we degrade to Mode B but stamp a single warning row.
462
+ throw err;
463
+ }
427
464
 
428
- let r;
429
- try {
430
- r = await tryBrainQuery(payload);
431
- } catch (err) {
432
- // Re-throw so outer aggregateAcrossRooms can record the warning
433
- // row. This is the "read-only Brain failure mid-render" case --
434
- // we degrade to Mode B but stamp a single warning row.
435
- throw err;
436
- }
437
- if (r && r.patterns) {
438
- hints[jtbd] = r.patterns;
439
- }
465
+ // brain_search returns { matches: [...] } or { records: [...] }.
466
+ // Extract the pattern list and assign uniformly across all JTBDs.
467
+ // (The response is generic -- there are no per-JTBD buckets from semantic
468
+ // search; we attribute the same hint set to every JTBD in the batch.)
469
+ const matchItems = (r && Array.isArray(r.matches)) ? r.matches
470
+ : (r && Array.isArray(r.records)) ? r.records
471
+ : [];
472
+
473
+ if (matchItems.length === 0) return null;
474
+
475
+ // Map semantic matches to the patterns shape the caller expects.
476
+ const patterns = matchItems.slice(0, 5).map(function (m) {
477
+ return { title: m.title || m.name || 'methodology pattern', score: m.score || 0 };
478
+ });
479
+
480
+ const hints = {};
481
+ for (const jtbd of safeJtbdList) {
482
+ hints[jtbd] = patterns;
440
483
  }
441
484
 
442
485
  return Object.keys(hints).length > 0 ? hints : null;
@@ -254,7 +254,13 @@ function localRoute(roomDir, stateContent, intent) {
254
254
  // ---------------------------------------------------------------------------
255
255
 
256
256
  /**
257
- * Call Brain API for framework recommendation.
257
+ * Call Brain API for framework recommendation via brain_ask (ungated).
258
+ * Replaced the former raw-Cypher brain_query path (admin-gated, BUG 2)
259
+ * with brain.ask(question) -- valid for all API keys.
260
+ * Reads next_gate.options[].framework for the ranked chain.
261
+ * Canon Part 8: the NL question carries only the generic problem-type enum,
262
+ * never user content or artifact text.
263
+ *
258
264
  * @param {string} roomDir
259
265
  * @param {string} stateContent
260
266
  * @param {string} [intent]
@@ -270,65 +276,73 @@ async function brainRoute(roomDir, stateContent, intent) {
270
276
 
271
277
  if (!brainClient.isAvailable()) return null;
272
278
 
273
- // Query Brain for framework recommendations based on room state
279
+ // Build a generic NL question carrying only the problem-type enum.
280
+ // Canon Part 8: no user content, no artifact text, no proprietary numbers.
274
281
  const { definition, complexity } = extractProblemType(stateContent);
282
+ const safeDefinition = definition.replace(/[^a-zA-Z-]/g, '') || 'undefined';
283
+ const safeComplexity = complexity.replace(/[^a-zA-Z-]/g, '') || 'complex';
284
+ const question = 'recommend a framework for a ' + safeDefinition + ' definition '
285
+ + safeComplexity + ' problem';
286
+
287
+ let brainResult;
288
+ try {
289
+ if (typeof brainClient.ask !== 'function') return null;
290
+ brainResult = await brainClient.ask(question);
291
+ } catch (_e) {
292
+ return null;
293
+ }
294
+
295
+ if (!brainResult) return null;
296
+
297
+ // Read next_gate.options[] for the ranked framework chain.
298
+ // Gracefully handle both presence and absence of next_gate.
299
+ const options = (brainResult.next_gate && Array.isArray(brainResult.next_gate.options))
300
+ ? brainResult.next_gate.options
301
+ : [];
275
302
 
276
- // PIPE-03: Include both CO_OCCURS and FEEDS_INTO relationships
277
- // for chain ordering. FEEDS_INTO encodes directional sequences
278
- // (e.g., scenario-plan FEEDS_INTO root-cause), while CO_OCCURS
279
- // captures frameworks that commonly run together.
280
- // I-3 fix: sanitize complexity to prevent Cypher injection from malformed STATE.md
281
- const safeComplexity = complexity.replace(/[^a-zA-Z]/g, '');
282
- const cypher = `
283
- MATCH (pt:ProblemType)<-[:ADDRESSES_PROBLEM_TYPE]-(f:Framework)
284
- WHERE pt.name CONTAINS "${safeComplexity}"
285
- WITH f, pt
286
- OPTIONAL MATCH (f)-[:FEEDS_INTO]->(f_next:Framework)
287
- WITH f, pt, collect(DISTINCT f_next.name) AS feeds_chain
288
- OPTIONAL MATCH (f)-[:CO_OCCURS]->(f_co:Framework)
289
- WITH f, pt, feeds_chain, collect(DISTINCT f_co.name) AS co_chain
290
- RETURN f.name AS primary,
291
- CASE WHEN size(feeds_chain) > 0 THEN feeds_chain[..3]
292
- ELSE co_chain[..3] END AS chain,
293
- pt.name AS problem_type,
294
- CASE WHEN size(feeds_chain) > 0 THEN 'feeds_into'
295
- ELSE 'co_occurs' END AS chain_type
296
- LIMIT 3
297
- `;
298
-
299
- const result = await brainClient.query(cypher);
300
-
301
- if (result && result.records && result.records.length > 0) {
302
- const rec = result.records[0];
303
- const primary = rec.primary || rec[0];
304
- const chainNames = rec.chain || rec[1] || [];
305
- const fullChain = [primary, ...chainNames].filter(Boolean);
306
-
307
- // Map Brain framework names to CLI command names
308
- const mappedChain = fullChain
309
- .map(name => {
310
- const normalized = name.toLowerCase().replace(/[^a-z-]/g, '');
311
- return KNOWN_METHODOLOGIES.find(m => m === normalized || normalized.includes(m))
312
- || normalized;
313
- })
314
- .filter(Boolean)
315
- .slice(0, 4);
316
-
317
- if (mappedChain.length > 0) {
318
- const chainType = rec.chain_type || rec[3] || 'co_occurs';
319
- return {
320
- chain: mappedChain,
321
- confidence: 0.85,
322
- source: 'brain',
323
- chain_type: chainType,
324
- reasoning: `Brain recommends ${mappedChain.join(' -> ')} for ${definition} ${complexity} problem. ` +
325
- `Chain derived from ${chainType === 'feeds_into' ? 'FEEDS_INTO (sequential)' : 'CO_OCCURS (complementary)'} relationships.`,
326
- target_sections: []
327
- };
303
+ const anchorFramework = (brainResult.directive && brainResult.directive.guided)
304
+ ? (brainResult.directive.guided.framework || null)
305
+ : null;
306
+
307
+ // Build the chain: anchor first (if present + not already in options), then options[].framework.
308
+ const rawChain = [];
309
+ if (anchorFramework && typeof anchorFramework === 'string') rawChain.push(anchorFramework);
310
+ for (const opt of options) {
311
+ if (opt && typeof opt.framework === 'string' && opt.framework.length > 0) {
312
+ if (!rawChain.includes(opt.framework)) rawChain.push(opt.framework);
328
313
  }
329
314
  }
330
315
 
331
- return null;
316
+ if (rawChain.length === 0) return null;
317
+
318
+ // Map Brain framework names to CLI methodology names (slug normalization).
319
+ // This mirrors the previous mapping in the old Cypher path.
320
+ const mappedChain = rawChain
321
+ .map(function (name) {
322
+ const normalized = name.toLowerCase().replace(/[^a-z-]/g, '');
323
+ return KNOWN_METHODOLOGIES.find(function (m) { return m === normalized || normalized.includes(m); })
324
+ || normalized;
325
+ })
326
+ .filter(Boolean)
327
+ .slice(0, 4);
328
+
329
+ if (mappedChain.length === 0) return null;
330
+
331
+ // Derive top confidence from the options array (or default 0.8).
332
+ const topConf = (options.length > 0 && typeof options[0].confidence === 'number')
333
+ ? options[0].confidence
334
+ : 0.8;
335
+
336
+ return {
337
+ chain: mappedChain,
338
+ confidence: topConf,
339
+ source: 'brain',
340
+ chain_type: 'feeds_into',
341
+ reasoning: 'Brain recommends ' + mappedChain.join(' -> ') + ' for '
342
+ + safeDefinition + ' ' + safeComplexity + ' problem. '
343
+ + 'Chain derived from brain_ask FEEDS_INTO recommendations.',
344
+ target_sections: [],
345
+ };
332
346
  }
333
347
 
334
348
  // ---------------------------------------------------------------------------