@mindrian_os/install 1.13.0-beta.22 → 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 +39 -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/bin/mindrian-brain-mcp-client.cjs +16 -3
- package/bin/mindrian-mcp-server.cjs +18 -3
- 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/hooks/hooks.json +8 -8
- 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/mcp-dep-heal.cjs +246 -0
- package/lib/core/mcp-dep-heal.test.cjs +253 -0
- package/lib/core/npm-cli-resolve.cjs +151 -0
- package/lib/core/npm-cli-resolve.test.cjs +153 -0
- package/lib/core/npm-install-lock.cjs +302 -0
- package/lib/core/npm-install-lock.test.cjs +325 -0
- 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 +2 -4
- package/references/brain/query-patterns.md +29 -17
|
@@ -128,35 +128,32 @@ const SKILL_SPAWN_RULES = Object.freeze([
|
|
|
128
128
|
|
|
129
129
|
// ---------- Internal helpers ----------
|
|
130
130
|
|
|
131
|
-
// Sanitize a string
|
|
132
|
-
//
|
|
133
|
-
// ([a-zA-Z0-9 ._-])
|
|
134
|
-
//
|
|
135
|
-
function
|
|
131
|
+
// Sanitize a string to a safe generic handle suitable for embedding in a
|
|
132
|
+
// NL question sent to brain_ask. Mirrors the whitelist from brain-client.cjs
|
|
133
|
+
// ([a-zA-Z0-9 ._-]) so only generic methodology handles pass through.
|
|
134
|
+
// Canon Part 8: no user content ever flows into the NL question.
|
|
135
|
+
function _sanitizeHandle(value) {
|
|
136
136
|
if (value === null || value === undefined) return '';
|
|
137
137
|
const s = typeof value === 'string' ? value : String(value);
|
|
138
138
|
return s.replace(/[^a-zA-Z0-9 ._-]/g, '');
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
// Build the
|
|
142
|
-
// for the reverse-salient engine
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
'WHERE rs.id IS NOT NULL ' +
|
|
158
|
-
'RETURN upstream.name AS name LIMIT 10'
|
|
159
|
-
);
|
|
141
|
+
// Build the NL question for brain_ask asking for upstream FEEDS_INTO
|
|
142
|
+
// frameworks for the reverse-salient engine.
|
|
143
|
+
// BUG 2 fix: replaced the former admin-gated raw-Cypher brain_query path
|
|
144
|
+
// with a brain_ask question (ungated -- valid for all API keys).
|
|
145
|
+
// Canon Part 8: the question carries only sanitized generic enum handles,
|
|
146
|
+
// never user content, never artifact text.
|
|
147
|
+
function _buildUpstreamQuestion(problem_type, stage) {
|
|
148
|
+
const safeProblem = _sanitizeHandle(problem_type);
|
|
149
|
+
const safeStage = _sanitizeHandle(stage);
|
|
150
|
+
// Generic NL question: asks for frameworks that feed into the
|
|
151
|
+
// reverse-salient framework for the given problem type and stage.
|
|
152
|
+
let q = 'what frameworks feed into the reverse salient framework';
|
|
153
|
+
if (safeProblem) q += ' for a ' + safeProblem + ' problem';
|
|
154
|
+
if (safeStage) q += ' at the ' + safeStage + ' stage';
|
|
155
|
+
q += '?';
|
|
156
|
+
return q;
|
|
160
157
|
}
|
|
161
158
|
|
|
162
159
|
// Defensively pull the framework name from a Brain record. Brain
|
|
@@ -187,18 +184,53 @@ async function lookupUpstream(problem_type, stage, opts) {
|
|
|
187
184
|
return { state: 'ready' };
|
|
188
185
|
}
|
|
189
186
|
|
|
190
|
-
|
|
187
|
+
// BUG 2 fix: use brain_ask (ungated) instead of brain_query (admin-gated).
|
|
188
|
+
// brain.ask() returns a DirectiveEnvelope; we read next_gate.options[].framework
|
|
189
|
+
// for the upstream framework names, mirroring the old record.name extraction.
|
|
190
|
+
if (typeof brainClient.ask !== 'function') {
|
|
191
|
+
process.stderr.write('chain-feeder: brainClient.ask not available; assuming upstream ready\n');
|
|
192
|
+
return { state: 'ready' };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let askResult;
|
|
191
196
|
try {
|
|
192
|
-
const
|
|
193
|
-
|
|
197
|
+
const nlQuestion = _buildUpstreamQuestion(problem_type, stage);
|
|
198
|
+
askResult = await brainClient.ask(nlQuestion);
|
|
194
199
|
} catch (_err) {
|
|
195
|
-
process.stderr.write('chain-feeder: Brain
|
|
200
|
+
process.stderr.write('chain-feeder: Brain ask threw; assuming upstream ready\n');
|
|
201
|
+
return { state: 'ready' };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (!askResult) {
|
|
205
|
+
process.stderr.write(
|
|
206
|
+
'chain-feeder: Brain ask returned null; assuming upstream ready\n'
|
|
207
|
+
);
|
|
196
208
|
return { state: 'ready' };
|
|
197
209
|
}
|
|
198
210
|
|
|
199
|
-
|
|
211
|
+
// Translate the brain_ask response into the framework-name list shape
|
|
212
|
+
// the upstream-freshness check expects. next_gate.options[].framework
|
|
213
|
+
// carries the framework names; fall back to directive.guided.framework.
|
|
214
|
+
// Build a synthetic result.records array for the existing freshness loop.
|
|
215
|
+
const frameworkNames = [];
|
|
216
|
+
if (askResult.next_gate && Array.isArray(askResult.next_gate.options)) {
|
|
217
|
+
for (const opt of askResult.next_gate.options) {
|
|
218
|
+
if (opt && typeof opt.framework === 'string' && opt.framework.length > 0) {
|
|
219
|
+
frameworkNames.push(opt.framework);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (askResult.directive && askResult.directive.guided && askResult.directive.guided.framework) {
|
|
224
|
+
const anchor = askResult.directive.guided.framework;
|
|
225
|
+
if (!frameworkNames.includes(anchor)) frameworkNames.unshift(anchor);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Wrap in the shape the downstream freshness loop already handles.
|
|
229
|
+
const result = { records: frameworkNames.map(function (n) { return { name: n }; }) };
|
|
230
|
+
|
|
231
|
+
if (!Array.isArray(result.records)) {
|
|
200
232
|
process.stderr.write(
|
|
201
|
-
'chain-feeder: Brain
|
|
233
|
+
'chain-feeder: Brain ask returned unexpected shape; assuming upstream ready\n'
|
|
202
234
|
);
|
|
203
235
|
return { state: 'ready' };
|
|
204
236
|
}
|
|
@@ -182,9 +182,14 @@ const ALLOW_LIST_INTENTS = Object.freeze([
|
|
|
182
182
|
/downstream\s+of/i,
|
|
183
183
|
/what\s+chains?\s+into/i,
|
|
184
184
|
],
|
|
185
|
-
brain_template
|
|
186
|
-
|
|
187
|
-
|
|
185
|
+
// BUG 2 fix: brain_template is now null so buildBrainQueryFromNL returns
|
|
186
|
+
// null (the safe default) and does NOT attempt raw admin-gated Cypher.
|
|
187
|
+
// The brain_ask NL question is synthesized in executeBundle (rs-explain-
|
|
188
|
+
// command.cjs) from intent_id + params.target_framework (enum handle only;
|
|
189
|
+
// Canon Part 8 preserved). brain_ask is ungated; brain_query was admin-gated.
|
|
190
|
+
brain_template: null,
|
|
191
|
+
// brain_ask_template documents the NL question shape used at runtime.
|
|
192
|
+
brain_ask_template: 'what frameworks chain from {target_framework} via FEEDS_INTO?',
|
|
188
193
|
sql_template: null,
|
|
189
194
|
cypher_template: null,
|
|
190
195
|
params_extractor: extractFrameworkParam,
|
|
@@ -221,9 +226,14 @@ const ALLOW_LIST_INTENTS = Object.freeze([
|
|
|
221
226
|
/what\s+comes\s+after/i,
|
|
222
227
|
/chains?\s+from\s+(?:jtbd|persona|swot|porter)/i,
|
|
223
228
|
],
|
|
224
|
-
brain_template
|
|
225
|
-
|
|
226
|
-
|
|
229
|
+
// BUG 2 fix: brain_template is now null so buildBrainQueryFromNL returns
|
|
230
|
+
// null (the safe default) and does NOT attempt raw admin-gated Cypher.
|
|
231
|
+
// The brain_ask NL question is synthesized in executeBundle (rs-explain-
|
|
232
|
+
// command.cjs) from intent_id + params.source_id (enum handle only;
|
|
233
|
+
// Canon Part 8 preserved). brain_ask is ungated; brain_query was admin-gated.
|
|
234
|
+
brain_template: null,
|
|
235
|
+
// brain_ask_template documents the NL question shape used at runtime.
|
|
236
|
+
brain_ask_template: 'what methodology chain follows {source_id}?',
|
|
227
237
|
sql_template: null,
|
|
228
238
|
cypher_template: null,
|
|
229
239
|
params_extractor: extractMethodologyParam,
|
|
@@ -261,18 +261,35 @@ function buildBrainQueryContext(localContext) {
|
|
|
261
261
|
}
|
|
262
262
|
|
|
263
263
|
// ============================================================
|
|
264
|
-
// Brain
|
|
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
|
|
287
|
+
async function tryBrainSearch(queryText) {
|
|
271
288
|
const client = getBrainClient();
|
|
272
|
-
if (!client || typeof client.
|
|
289
|
+
if (!client || typeof client.search !== 'function') return null;
|
|
273
290
|
// The single Brain call site. (Tripwire #2: exactly one match for
|
|
274
|
-
// /(?:
|
|
275
|
-
return await client.
|
|
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
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
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;
|
package/lib/mcp/brain-router.cjs
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
//
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|