@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
|
@@ -1,33 +1,46 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
// Phase 125-02 -- Brain
|
|
3
|
-
// CONTEXT.md Scope IN section B item 4
|
|
4
|
-
// brain-client.query that produces the framework_chain_hint shape Plan 03 stitches
|
|
5
|
-
// into the packet's local_graph_summary.
|
|
2
|
+
// Phase 125-02 -- Brain framework-chain slice (FEEDS_INTO 1-3 hops).
|
|
3
|
+
// CONTEXT.md Scope IN section B item 4.
|
|
6
4
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
5
|
+
// Repointed (BUG 2 fix) from the admin-gated brain_query / raw-Cypher path
|
|
6
|
+
// onto the curated brain.askOp('framework_chain_slice', { seeds, max_hops })
|
|
7
|
+
// surface so any valid API key can call it.
|
|
10
8
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
9
|
+
// Contract (from .planning/brain-curated-ops-contract.md):
|
|
10
|
+
// brain.askOp('framework_chain_slice', { seeds: string[], max_hops?: 1|2|3 })
|
|
11
|
+
// -> { op, source, count, rows: Array<{from, to, hop_distance}>, degraded? }
|
|
12
|
+
//
|
|
13
|
+
// INTEGRATOR NOTE -- partial substitute:
|
|
14
|
+
// The old Cypher returned `confidence` and `transform_description` per edge.
|
|
15
|
+
// The curated op returns ONLY `from`, `to`, `hop_distance` (no confidence,
|
|
16
|
+
// no transform_description). Downstream consumers that used those fields will
|
|
17
|
+
// receive `null` after this change. Flag logged in fetchFrameworkChainSlice
|
|
18
|
+
// return envelope as `confidence_omitted: true` so callers can detect this.
|
|
19
|
+
// A future curated-op extension could re-expose the fields if needed.
|
|
20
|
+
//
|
|
21
|
+
// Canon Part 8: seeds are generic framework-name strings (allow-listed by
|
|
22
|
+
// the server); max_hops is a clamped integer 1-3. No user content crosses the
|
|
23
|
+
// wire. The curated op enforces this server-side; no caller Cypher ever.
|
|
24
|
+
//
|
|
25
|
+
// Graceful degradation: any failure path (Brain unreachable, askOp throws,
|
|
26
|
+
// invalid max_hops, empty seeds, non-array result, degraded envelope) returns
|
|
27
|
+
// a degraded result envelope rather than throwing. Tier 0 stays functional.
|
|
28
|
+
//
|
|
29
|
+
// BACKWARD-COMPAT SURFACE: the module still exports FRAMEWORK_CHAIN_SLICE_CYPHER
|
|
30
|
+
// as an empty string so existing test-seam checks that assert on the exported
|
|
31
|
+
// constant do not break (tests/brain-cypher-chain-slice.test.cjs Test 2 checks
|
|
32
|
+
// that the CYPHER includes 'LIMIT 50' and 'FEEDS_INTO*1..$max_hops'; those
|
|
33
|
+
// tests will need updating now that the Cypher is server-side). The constant is
|
|
34
|
+
// deprecated; callers should NOT use it.
|
|
14
35
|
|
|
15
36
|
const crypto = require('node:crypto');
|
|
16
37
|
const defaultBrainClient = require('../core/brain-client.cjs');
|
|
17
38
|
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
const FRAMEWORK_CHAIN_SLICE_CYPHER =
|
|
24
|
-
'MATCH (f:Framework)-[r:FEEDS_INTO*1..$max_hops]->(g:Framework) ' +
|
|
25
|
-
'WHERE f.name IN $active_frameworks ' +
|
|
26
|
-
'RETURN f.name AS from, g.name AS to, ' +
|
|
27
|
-
' r.confidence AS confidence, ' +
|
|
28
|
-
' r.transform_description AS transform_description, ' +
|
|
29
|
-
' length(r) AS hop_distance ' +
|
|
30
|
-
'LIMIT 50';
|
|
39
|
+
// DEPRECATED: the Cypher is now frozen server-side inside the curated op.
|
|
40
|
+
// Retained as an empty string to avoid hard breaking any import that
|
|
41
|
+
// destructures this export. Tests that assert on its content should be
|
|
42
|
+
// updated to reflect the curated-op architecture.
|
|
43
|
+
const FRAMEWORK_CHAIN_SLICE_CYPHER = '';
|
|
31
44
|
|
|
32
45
|
function _nowIso() {
|
|
33
46
|
return new Date().toISOString();
|
|
@@ -62,22 +75,29 @@ function _unwrapRows(result) {
|
|
|
62
75
|
}
|
|
63
76
|
|
|
64
77
|
/**
|
|
65
|
-
* Fetch a
|
|
78
|
+
* Fetch a 1-3 hop FEEDS_INTO slice from the live Brain via the curated
|
|
79
|
+
* askOp surface (D-MOAT-1-safe; no admin gate required).
|
|
66
80
|
*
|
|
67
81
|
* @param {object} opts
|
|
68
|
-
* @param {string[]} opts.active_frameworks - Array of framework name strings
|
|
69
|
-
* Empty array short-circuits to a degraded envelope.
|
|
82
|
+
* @param {string[]} opts.active_frameworks - Array of framework name strings
|
|
83
|
+
* used as seeds. Empty array short-circuits to a degraded envelope.
|
|
70
84
|
* @param {1|2|3} opts.max_hops - Hop depth (must be exactly 1, 2, or 3).
|
|
71
85
|
* Any other value short-circuits to a degraded envelope.
|
|
72
86
|
* @param {object} [opts.brainClient] - Test seam; defaults to live brain-client.
|
|
73
87
|
* @returns {Promise<{
|
|
74
|
-
* edges: Array<{from:string|null, to:string|null, confidence:
|
|
75
|
-
* transform_description:
|
|
88
|
+
* edges: Array<{from:string|null, to:string|null, confidence:null,
|
|
89
|
+
* transform_description:null, hop_distance:number|null}>,
|
|
76
90
|
* slice_scope: 1|2|3,
|
|
77
91
|
* slice_rationale: string,
|
|
78
92
|
* brain_snapshot_id: string|null,
|
|
79
|
-
* fetched_at: string
|
|
93
|
+
* fetched_at: string,
|
|
94
|
+
* confidence_omitted: true
|
|
80
95
|
* }>}
|
|
96
|
+
*
|
|
97
|
+
* INTEGRATOR NOTE: confidence + transform_description are always null after
|
|
98
|
+
* this repoint. The curated op returns only { from, to, hop_distance }.
|
|
99
|
+
* `confidence_omitted: true` is set in every returned envelope so callers
|
|
100
|
+
* can detect the reduction and adapt accordingly.
|
|
81
101
|
*/
|
|
82
102
|
async function fetchFrameworkChainSlice(opts) {
|
|
83
103
|
const o = opts || {};
|
|
@@ -85,29 +105,31 @@ async function fetchFrameworkChainSlice(opts) {
|
|
|
85
105
|
const max_hops = (o.max_hops === 1 || o.max_hops === 2 || o.max_hops === 3) ? o.max_hops : null;
|
|
86
106
|
const brainClient = o.brainClient || defaultBrainClient;
|
|
87
107
|
|
|
88
|
-
// Gate 1: max_hops must be 1, 2, or 3.
|
|
89
|
-
// string, null) returns a degraded envelope with slice_scope defaulted to 3.
|
|
108
|
+
// Gate 1: max_hops must be 1, 2, or 3.
|
|
90
109
|
if (max_hops === null) {
|
|
91
|
-
return _degraded({ max_hops: 3, rationale: 'invalid_max_hops; expected 1|2|3' });
|
|
110
|
+
return Object.assign(_degraded({ max_hops: 3, rationale: 'invalid_max_hops; expected 1|2|3' }), { confidence_omitted: true });
|
|
92
111
|
}
|
|
93
112
|
|
|
94
113
|
// Gate 2: empty active_frameworks short-circuits before any Brain contact.
|
|
95
|
-
// The Cypher would match nothing anyway; the degraded envelope is honest.
|
|
96
114
|
if (active_frameworks.length === 0) {
|
|
97
|
-
return _degraded({ max_hops: max_hops, rationale: 'empty active_frameworks; no slice fetched' });
|
|
115
|
+
return Object.assign(_degraded({ max_hops: max_hops, rationale: 'empty active_frameworks; no slice fetched' }), { confidence_omitted: true });
|
|
98
116
|
}
|
|
99
117
|
|
|
100
|
-
// Gate 3: Brain reachability check
|
|
101
|
-
// live brain-client.isAvailable() is sync + cheap (an API-key resolve).
|
|
118
|
+
// Gate 3: Brain reachability check.
|
|
102
119
|
if (typeof brainClient.isAvailable === 'function' && !brainClient.isAvailable()) {
|
|
103
|
-
return _degraded({ max_hops: max_hops, rationale: 'brain_unreachable' });
|
|
120
|
+
return Object.assign(_degraded({ max_hops: max_hops, rationale: 'brain_unreachable' }), { confidence_omitted: true });
|
|
104
121
|
}
|
|
105
122
|
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
123
|
+
// Gate 4: askOp must be available on the client. The live brain-client
|
|
124
|
+
// gains askOp in the parallel BUG 2 build; tests may stub it. If the
|
|
125
|
+
// method is absent we fall back to the degraded path cleanly.
|
|
126
|
+
if (typeof brainClient.askOp !== 'function') {
|
|
127
|
+
return Object.assign(_degraded({ max_hops: max_hops, rationale: 'askOp_not_available_on_client' }), { confidence_omitted: true });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Sanitize seed names before passing to askOp. The server also enforces
|
|
131
|
+
// the allow-list but we sanitize client-side as defence-in-depth.
|
|
132
|
+
// Canon Part 8 invariant preserved.
|
|
111
133
|
const sanitizer = (brainClient._test && typeof brainClient._test.sanitizeCypherInput === 'function')
|
|
112
134
|
? brainClient._test.sanitizeCypherInput
|
|
113
135
|
: function (s) { return s; };
|
|
@@ -117,66 +139,63 @@ async function fetchFrameworkChainSlice(opts) {
|
|
|
117
139
|
.filter(function (n) { return typeof n === 'string' && n.length > 0; });
|
|
118
140
|
|
|
119
141
|
if (sanitized.length === 0) {
|
|
120
|
-
return _degraded({
|
|
142
|
+
return Object.assign(_degraded({
|
|
121
143
|
max_hops: max_hops,
|
|
122
144
|
rationale: 'all_active_frameworks_rejected_by_sanitizer',
|
|
123
|
-
});
|
|
145
|
+
}), { confidence_omitted: true });
|
|
124
146
|
}
|
|
125
147
|
|
|
126
|
-
// Issue the
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
// with caller-derived data (Canon Part 8 enforced by inspection).
|
|
130
|
-
let result;
|
|
148
|
+
// Issue the curated op. `seeds` is the param name per the contract;
|
|
149
|
+
// `max_hops` is passed through as-is (server clamps to [1,3]).
|
|
150
|
+
let opResult;
|
|
131
151
|
try {
|
|
132
|
-
|
|
133
|
-
|
|
152
|
+
opResult = await brainClient.askOp('framework_chain_slice', {
|
|
153
|
+
seeds: sanitized,
|
|
134
154
|
max_hops: max_hops,
|
|
135
155
|
});
|
|
136
156
|
} catch (e) {
|
|
137
157
|
const msg = (e && e.message) ? String(e.message) : 'unknown';
|
|
138
|
-
return _degraded({
|
|
158
|
+
return Object.assign(_degraded({
|
|
139
159
|
max_hops: max_hops,
|
|
140
160
|
rationale: 'brain_query_failed: ' + msg.slice(0, 60),
|
|
141
|
-
});
|
|
161
|
+
}), { confidence_omitted: true });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Degraded envelope from server.
|
|
165
|
+
if (!opResult || opResult.degraded) {
|
|
166
|
+
return Object.assign(_degraded({ max_hops: max_hops, rationale: 'brain_returned_degraded' }), { confidence_omitted: true });
|
|
142
167
|
}
|
|
143
168
|
|
|
144
|
-
const rows =
|
|
169
|
+
const rows = Array.isArray(opResult.rows) ? opResult.rows : null;
|
|
145
170
|
if (rows === null) {
|
|
146
|
-
return _degraded({ max_hops: max_hops, rationale: 'brain_returned_non_array' });
|
|
171
|
+
return Object.assign(_degraded({ max_hops: max_hops, rationale: 'brain_returned_non_array' }), { confidence_omitted: true });
|
|
147
172
|
}
|
|
148
173
|
|
|
149
|
-
// Map
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
// length(r) which is always a number when the row exists.
|
|
153
|
-
// Defensive: hard-cap at 50 even though the Cypher already enforces LIMIT 50.
|
|
174
|
+
// Map curated op rows { from, to, hop_distance } to the edge shape.
|
|
175
|
+
// confidence and transform_description are NOT in the curated op response;
|
|
176
|
+
// they are set to null explicitly and flagged via confidence_omitted.
|
|
154
177
|
const edges = rows.slice(0, 50).map(function (r) {
|
|
155
178
|
return {
|
|
156
179
|
from: (r && typeof r.from === 'string') ? r.from : null,
|
|
157
180
|
to: (r && typeof r.to === 'string') ? r.to : null,
|
|
158
|
-
confidence:
|
|
159
|
-
transform_description:
|
|
160
|
-
? r.transform_description
|
|
161
|
-
: null,
|
|
181
|
+
confidence: null, // not provided by curated op
|
|
182
|
+
transform_description: null, // not provided by curated op
|
|
162
183
|
hop_distance: (r && typeof r.hop_distance === 'number') ? r.hop_distance : null,
|
|
163
184
|
};
|
|
164
185
|
});
|
|
165
186
|
|
|
166
|
-
//
|
|
167
|
-
//
|
|
168
|
-
// expose a commit_id; we derive a deterministic content hash of the raw
|
|
169
|
-
// rows so callers can detect slice changes without holding the rows
|
|
170
|
-
// themselves (Plan 03 + downstream packet validator never sees them).
|
|
187
|
+
// Deterministic content hash for cache invalidation (content-addressed
|
|
188
|
+
// snapshot -- same rationale as the original implementation).
|
|
171
189
|
const snapshotId = _sha256Hex(JSON.stringify(rows));
|
|
172
190
|
|
|
173
191
|
return {
|
|
174
192
|
edges: edges,
|
|
175
193
|
slice_scope: max_hops,
|
|
176
194
|
slice_rationale: 'brain_reachable; ' + edges.length + ' edges fetched; '
|
|
177
|
-
+ sanitized.length + '
|
|
195
|
+
+ sanitized.length + ' seeds; max_hops=' + max_hops,
|
|
178
196
|
brain_snapshot_id: snapshotId,
|
|
179
197
|
fetched_at: _nowIso(),
|
|
198
|
+
confidence_omitted: true,
|
|
180
199
|
};
|
|
181
200
|
}
|
|
182
201
|
|
|
@@ -464,6 +464,59 @@ async function ask(question) {
|
|
|
464
464
|
return callTool('brain_ask', { question: question });
|
|
465
465
|
}
|
|
466
466
|
|
|
467
|
+
/**
|
|
468
|
+
* Curated-op call against the Brain (the `op` MODE of brain_ask).
|
|
469
|
+
*
|
|
470
|
+
* brain_ask gained an optional curated-op surface (BUG 2 fix). Where ask()
|
|
471
|
+
* runs the natural-language directive path, askOp() runs one of a closed set
|
|
472
|
+
* of named, parameterized operations the Brain resolves to a FROZEN
|
|
473
|
+
* server-side Cypher string. The three ops:
|
|
474
|
+
*
|
|
475
|
+
* - 'list_frameworks' params { limit? } -> rows { name, description, category }
|
|
476
|
+
* - 'framework_edges' params { edge_type, limit? } -> rows { from, to, confidence, transform }
|
|
477
|
+
* or { framework, problem_type }
|
|
478
|
+
* - 'framework_chain_slice' params { seeds, max_hops?, limit? } -> rows { from, to, hop_distance }
|
|
479
|
+
*
|
|
480
|
+
* Canon Part 8: every param is a generic methodology handle (framework name,
|
|
481
|
+
* closed enum, integer) -- never user content. No caller Cypher is ever sent;
|
|
482
|
+
* the Brain owns the query text. This path is ungated -- any valid key may
|
|
483
|
+
* call it (only query()/write() touch the admin-gated tools).
|
|
484
|
+
*
|
|
485
|
+
* Consumers that only need a framework chain for ONE anchor keep using
|
|
486
|
+
* ask(question) and read next_gate.options[].framework (the directive path).
|
|
487
|
+
*
|
|
488
|
+
* Returns the parsed curated-op payload { op, source, count, rows, degraded? }
|
|
489
|
+
* on success. On any transport / parse failure (Brain unreachable, no API key,
|
|
490
|
+
* bad payload) returns a graceful { op, count: 0, rows: [], degraded: true } so
|
|
491
|
+
* the caller never crashes -- mirrors query()'s graceful-degradation contract.
|
|
492
|
+
*
|
|
493
|
+
* @param {string} operation - one of the three curated op names
|
|
494
|
+
* @param {object} [params] - generic-handles-only params object
|
|
495
|
+
* @returns {Promise<{op: string, source?: string, count: number, rows: Array, degraded?: boolean}>}
|
|
496
|
+
*/
|
|
497
|
+
async function askOp(operation, params = {}) {
|
|
498
|
+
try {
|
|
499
|
+
const result = await callTool('brain_ask', { op: operation, params: params || {} });
|
|
500
|
+
// callTool already parses the JSON text payload of the MCP content item,
|
|
501
|
+
// so a well-formed curated-op response arrives as the payload object.
|
|
502
|
+
if (result && typeof result === 'object'
|
|
503
|
+
&& typeof result.count === 'number' && Array.isArray(result.rows)) {
|
|
504
|
+
return {
|
|
505
|
+
op: result.op || operation,
|
|
506
|
+
source: result.source,
|
|
507
|
+
count: result.count,
|
|
508
|
+
rows: result.rows,
|
|
509
|
+
...(result.degraded ? { degraded: true } : {}),
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
// Unreachable Brain (null), an error/text passthrough, or any unexpected
|
|
513
|
+
// shape -> graceful degraded sentinel.
|
|
514
|
+
return { op: operation, count: 0, rows: [], degraded: true };
|
|
515
|
+
} catch (_err) {
|
|
516
|
+
return { op: operation, count: 0, rows: [], degraded: true };
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
467
520
|
/**
|
|
468
521
|
* Get Pinecone stats.
|
|
469
522
|
*/
|
|
@@ -1242,6 +1295,7 @@ module.exports = {
|
|
|
1242
1295
|
search,
|
|
1243
1296
|
smartSearch,
|
|
1244
1297
|
ask,
|
|
1298
|
+
askOp,
|
|
1245
1299
|
schema,
|
|
1246
1300
|
stats,
|
|
1247
1301
|
enrichCausalEdges,
|
|
@@ -182,18 +182,20 @@ function safeInt(value, fallback) {
|
|
|
182
182
|
// interface (which takes raw cypher); interpolation is unavoidable.
|
|
183
183
|
// Safety is achieved by the two-layer validation above.
|
|
184
184
|
|
|
185
|
+
// BUG 2 fix: was mode:'query' (admin-gated brain_query with ADDRESSES_PROBLEM_TYPE
|
|
186
|
+
// Cypher). Repointed to mode:'search' (ungated brain_search). Returns a semantic
|
|
187
|
+
// search string instead of Cypher. The Brain's Pinecone index surfaces frameworks
|
|
188
|
+
// that address the problem type and phase via embedding similarity.
|
|
189
|
+
// Canon Part 8: only frozen enum strings are interpolated -- no user content.
|
|
185
190
|
function buildPatternMatchesQuery(ctx) {
|
|
186
191
|
validateCtx(ctx);
|
|
187
192
|
const section = safeEnum(ctx.section_slug, 'unknown');
|
|
188
193
|
const problemType = safeEnum(ctx.problem_type, 'UDP');
|
|
189
194
|
const phase = safeEnum(ctx.phase_indicator, 'unknown');
|
|
190
195
|
return (
|
|
191
|
-
'
|
|
192
|
-
'
|
|
193
|
-
'
|
|
194
|
-
'RETURN f.name AS framework, f.description AS description, ' +
|
|
195
|
-
'"' + section + '" AS section_slug ' +
|
|
196
|
-
'LIMIT 10'
|
|
196
|
+
'frameworks that address problem_type:' + problemType +
|
|
197
|
+
' at phase:' + phase +
|
|
198
|
+
' for section:' + section
|
|
197
199
|
);
|
|
198
200
|
}
|
|
199
201
|
|
|
@@ -241,15 +243,18 @@ function buildUnfilledOpportunityMatchesQuery(ctx) {
|
|
|
241
243
|
);
|
|
242
244
|
}
|
|
243
245
|
|
|
246
|
+
// BUG 2 fix: was mode:'query' (admin-gated brain_query with FEEDS_INTO Cypher).
|
|
247
|
+
// Repointed to mode:'search' (ungated brain_search). Returns a semantic
|
|
248
|
+
// search string instead of Cypher. The Brain's Pinecone index surfaces framework
|
|
249
|
+
// chain predictions via embedding similarity on FEEDS_INTO relationships.
|
|
250
|
+
// Canon Part 8: only frozen enum strings are interpolated -- no user content.
|
|
244
251
|
function buildFrameworkChainPredictionsQuery(ctx) {
|
|
245
252
|
validateCtx(ctx);
|
|
246
253
|
const phase = safeEnum(ctx.phase_indicator, 'unknown');
|
|
247
254
|
const problemType = safeEnum(ctx.problem_type, 'UDP');
|
|
248
255
|
return (
|
|
249
|
-
'
|
|
250
|
-
'
|
|
251
|
-
'OPTIONAL MATCH (f1)-[:ADDRESSES_PROBLEM_TYPE]->(pt:ProblemType {name: "' + problemType + '"}) ' +
|
|
252
|
-
'RETURN f1.name AS from_framework, f2.name AS to_framework LIMIT 10'
|
|
256
|
+
'framework chain predictions FEEDS_INTO phase:' + phase +
|
|
257
|
+
' problem_type:' + problemType
|
|
253
258
|
);
|
|
254
259
|
}
|
|
255
260
|
|
|
@@ -58,12 +58,26 @@ const prompts = require('./brain-derivation-prompts.cjs');
|
|
|
58
58
|
|
|
59
59
|
// Section heading -> prompt builder mapping. Order matters: this is the
|
|
60
60
|
// canonical emission order inside BRAIN.md body.
|
|
61
|
+
//
|
|
62
|
+
// BUG 2 fix (2026-05-22): sections whose Cypher traverses FEEDS_INTO or
|
|
63
|
+
// ADDRESSES_PROBLEM_TYPE edges are repointed from mode:'query' (admin-gated
|
|
64
|
+
// brain_query) to mode:'search' (ungated brain_search). Their prompt builders
|
|
65
|
+
// are also updated (see brain-derivation-prompts.cjs) to emit a semantic
|
|
66
|
+
// search string instead of raw Cypher. The SECTION_BUILDERS mode field is
|
|
67
|
+
// the dispatch key in the derivation loop below.
|
|
68
|
+
//
|
|
69
|
+
// Sections that still use mode:'query' target node types with no curated op
|
|
70
|
+
// and no practical semantic-search equivalent (WickedIndicator, Opportunity,
|
|
71
|
+
// RigorLevel, ProblemType, HsiRecommendation). Those calls will fail (or
|
|
72
|
+
// return empty) for non-admin users -- that is the correct graceful-degradation
|
|
73
|
+
// behaviour (existing firstError path). A future phase can add curated ops
|
|
74
|
+
// for those node types.
|
|
61
75
|
const SECTION_BUILDERS = Object.freeze([
|
|
62
|
-
{ heading: 'Pattern Matches', builder: 'buildPatternMatchesQuery', mode: '
|
|
76
|
+
{ heading: 'Pattern Matches', builder: 'buildPatternMatchesQuery', mode: 'search' },
|
|
63
77
|
{ heading: 'Cross-Domain Analogies', builder: 'buildCrossDomainAnalogiesQuery', mode: 'search' },
|
|
64
78
|
{ heading: 'Wicked Indicators', builder: 'buildWickedIndicatorsQuery', mode: 'query' },
|
|
65
79
|
{ heading: 'Unfilled Opportunity Matches', builder: 'buildUnfilledOpportunityMatchesQuery', mode: 'query' },
|
|
66
|
-
{ heading: 'Framework Chain Predictions', builder: 'buildFrameworkChainPredictionsQuery', mode: '
|
|
80
|
+
{ heading: 'Framework Chain Predictions', builder: 'buildFrameworkChainPredictionsQuery', mode: 'search' },
|
|
67
81
|
{ heading: 'Assessment Thinking Chain Position', builder: 'buildAssessmentThinkingChainPositionQuery', mode: 'query' },
|
|
68
82
|
{ heading: 'ProblemType Classification', builder: 'buildProblemTypeClassificationQuery', mode: 'query' },
|
|
69
83
|
{ heading: 'Flagged Contradictions (cross-room)', builder: 'buildFlaggedContradictionsXroomQuery', mode: 'query' },
|
|
@@ -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,
|