@mindrian_os/cli 2.0.0-beta.31 → 2.0.0-beta.35

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.
@@ -1,438 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * sync-rooms-brain -- Sync room hierarchy to Neo4j Brain (remote graph)
4
- *
5
- * Creates/updates Room and RoomGroup nodes in Neo4j Brain via the Brain HTTP API.
6
- * Creates semantic edges: AT_STAGE, USES_FRAMEWORK, SHARES_THEME, HAS_SECTION.
7
- * Wires 13 orphaned DataRoomSection nodes to parent Room nodes via HAS_SECTION.
8
- *
9
- * Idempotent: uses MERGE semantics throughout.
10
- * Fire-and-forget: failure never blocks any operation (D-16).
11
- * Additive only: NEVER writes to filesystem or registry.json (D-15).
12
- * Only runs when Brain API key is available.
13
- *
14
- * Usage: node sync-rooms-brain [roomsHome]
15
- * roomsHome - Override ~/MindrianRooms (default: $MINDRIAN_ROOMS_HOME or ~/MindrianRooms)
16
- *
17
- * Exit codes:
18
- * 0 - Always (fire-and-forget, D-16)
19
- */
20
-
21
- 'use strict';
22
-
23
- const fs = require('fs');
24
- const path = require('path');
25
- const os = require('os');
26
-
27
- const ROOMS_HOME = process.argv[2] || process.env.MINDRIAN_ROOMS_HOME || path.join(os.homedir(), 'MindrianRooms');
28
- const REGISTRY_PATH = path.join(ROOMS_HOME, '.rooms', 'registry.json');
29
- const SCRIPT_DIR = __dirname;
30
- const PLUGIN_ROOT = path.resolve(SCRIPT_DIR, '..');
31
-
32
- let brain;
33
- try {
34
- brain = require(path.join(PLUGIN_ROOT, 'lib', 'core', 'brain-client.cjs'));
35
- } catch (_) {
36
- process.stderr.write('sync-rooms-brain: brain-client not found\n');
37
- process.exit(0);
38
- }
39
-
40
- /**
41
- * Execute a Cypher write query via Brain API.
42
- * Returns true on success, false on failure.
43
- * @param {string} cypher
44
- * @returns {Promise<boolean>}
45
- */
46
- async function brainWrite(cypher) {
47
- try {
48
- const result = await brain.write(cypher);
49
- return result !== null;
50
- } catch (_) {
51
- return false;
52
- }
53
- }
54
-
55
- /**
56
- * Execute a Cypher read query via Brain API.
57
- * @param {string} cypher
58
- * @returns {Promise<object|null>}
59
- */
60
- async function brainRead(cypher) {
61
- try {
62
- return await brain.query(cypher);
63
- } catch (_) {
64
- return null;
65
- }
66
- }
67
-
68
- /**
69
- * Create a RoomRoot node (the ~/MindrianRooms/ root).
70
- */
71
- async function syncRoomRoot() {
72
- return brainWrite(`
73
- MERGE (root:RoomRoot {name: 'MindrianRooms'})
74
- ON CREATE SET root.created = datetime(), root.path = '~/MindrianRooms/'
75
- `);
76
- }
77
-
78
- /**
79
- * Sync Room nodes to Brain.
80
- * @param {object} rooms - rooms object from registry.json
81
- */
82
- async function syncRoomNodes(rooms) {
83
- for (const [name, room] of Object.entries(rooms)) {
84
- if (room.status === 'archived') continue;
85
-
86
- const stage = (room.venture_stage || '').replace(/'/g, "\\'");
87
- const vname = (room.venture_name || '').replace(/'/g, "\\'");
88
- const domain = (room.domain || '').replace(/'/g, "\\'");
89
- const status = (room.status || 'unknown').replace(/'/g, "\\'");
90
- const created = room.created || '';
91
- const roomPath = (room.path || name).replace(/'/g, "\\'");
92
-
93
- await brainWrite(`
94
- MERGE (r:Room {name: '${name}'})
95
- ON CREATE SET r.venture_name = '${vname}', r.venture_stage = '${stage}',
96
- r.domain = '${domain}', r.status = '${status}',
97
- r.created = '${created}', r.path = '${roomPath}'
98
- ON MATCH SET r.venture_name = '${vname}', r.venture_stage = '${stage}',
99
- r.domain = '${domain}', r.status = '${status}', r.path = '${roomPath}'
100
- `);
101
- }
102
- }
103
-
104
- /**
105
- * Detect group directories and sync RoomGroup nodes.
106
- * @param {object} rooms - rooms object from registry.json
107
- * @returns {Map<string, string[]>} groupName -> [roomNames]
108
- */
109
- async function syncRoomGroups(rooms) {
110
- const groups = new Map();
111
-
112
- for (const [name, room] of Object.entries(rooms)) {
113
- if (room.status === 'archived') continue;
114
- const roomPath = room.path || name;
115
- const parts = roomPath.split('/').filter(Boolean);
116
-
117
- if (parts.length >= 2) {
118
- const groupName = parts[0];
119
- if (!groups.has(groupName)) {
120
- groups.set(groupName, []);
121
- }
122
- groups.get(groupName).push(name);
123
- }
124
- }
125
-
126
- for (const [groupName, roomNames] of groups) {
127
- // Create RoomGroup node
128
- await brainWrite(`
129
- MERGE (g:RoomGroup {name: '${groupName}'})
130
- ON CREATE SET g.icm_layer = 'group', g.created = datetime()
131
- `);
132
-
133
- // CONTAINS: RoomRoot -> RoomGroup
134
- await brainWrite(`
135
- MATCH (root:RoomRoot {name: 'MindrianRooms'}), (g:RoomGroup {name: '${groupName}'})
136
- MERGE (root)-[:CONTAINS]->(g)
137
- `);
138
-
139
- // CONTAINS: RoomGroup -> Room
140
- for (const roomName of roomNames) {
141
- await brainWrite(`
142
- MATCH (g:RoomGroup {name: '${groupName}'}), (r:Room {name: '${roomName}'})
143
- MERGE (g)-[:CONTAINS]->(r)
144
- `);
145
- }
146
- }
147
-
148
- // Rooms not in any group: RoomRoot -> Room directly
149
- for (const [name, room] of Object.entries(rooms)) {
150
- if (room.status === 'archived') continue;
151
- const roomPath = room.path || name;
152
- const parts = roomPath.split('/').filter(Boolean);
153
-
154
- if (parts.length < 2) {
155
- await brainWrite(`
156
- MATCH (root:RoomRoot {name: 'MindrianRooms'}), (r:Room {name: '${name}'})
157
- MERGE (root)-[:CONTAINS]->(r)
158
- `);
159
- }
160
- }
161
-
162
- return groups;
163
- }
164
-
165
- /**
166
- * Create AT_STAGE edges connecting Room nodes to existing VentureStage taxonomy.
167
- * @param {object} rooms - rooms object from registry.json
168
- */
169
- async function syncAtStageEdges(rooms) {
170
- for (const [name, room] of Object.entries(rooms)) {
171
- if (room.status === 'archived' || !room.venture_stage) continue;
172
-
173
- const stage = room.venture_stage.replace(/'/g, "\\'");
174
-
175
- // Remove existing AT_STAGE edges (room can change stage)
176
- await brainWrite(`
177
- MATCH (r:Room {name: '${name}'})-[e:AT_STAGE]->(:VentureStage)
178
- DELETE e
179
- `);
180
-
181
- // Create new AT_STAGE edge
182
- await brainWrite(`
183
- MATCH (r:Room {name: '${name}'}), (s:VentureStage {name: '${stage}'})
184
- MERGE (r)-[:AT_STAGE]->(s)
185
- `);
186
- }
187
- }
188
-
189
- /**
190
- * Create USES_FRAMEWORK edges from room analytics data.
191
- * Reads each room's .analytics.json for command usage that maps to frameworks.
192
- * @param {object} rooms - rooms object from registry.json
193
- */
194
- async function syncUsesFrameworkEdges(rooms) {
195
- // Command-to-framework mapping (from methodology index)
196
- const cmdToFramework = {
197
- 'jtbd': 'Jobs-to-Be-Done',
198
- 'analyze-needs': 'Jobs-to-Be-Done',
199
- 'dominant-designs': 'Blue Ocean Strategy',
200
- 'think-hats': 'Six Thinking Hats',
201
- 'persona': 'Design Thinking',
202
- 'swot': 'SWOT Analysis',
203
- 'bmc': 'Business Model Canvas',
204
- 'lean-canvas': 'Lean Canvas',
205
- 'pestel': 'PESTEL Analysis',
206
- 'porters': "Porter's Five Forces",
207
- 'value-chain': 'Value Chain Analysis',
208
- 'design-thinking': 'Design Thinking',
209
- 'systems-thinking': 'Systems Thinking',
210
- 'cynefin': 'Cynefin Framework',
211
- 'root-cause': 'Root Cause Analysis',
212
- 'find-analogies': 'Design-by-Analogy',
213
- 'stakeholder-map': 'Stakeholder Mapping',
214
- 'risk-matrix': 'Risk Matrix',
215
- 'scenario-planning': 'Scenario Planning',
216
- 'triple-validation': 'PWS Triple Validation Compass',
217
- };
218
-
219
- for (const [name, room] of Object.entries(rooms)) {
220
- if (room.status === 'archived') continue;
221
-
222
- const roomPath = path.join(ROOMS_HOME, room.path || name);
223
- const analyticsPath = path.join(roomPath, '.analytics.json');
224
-
225
- if (!fs.existsSync(analyticsPath)) continue;
226
-
227
- let analytics;
228
- try {
229
- analytics = JSON.parse(fs.readFileSync(analyticsPath, 'utf8'));
230
- } catch (_) {
231
- continue;
232
- }
233
-
234
- const commands = analytics.commands || {};
235
- const frameworksUsed = new Set();
236
-
237
- for (const [cmd, count] of Object.entries(commands)) {
238
- // Strip /mos: prefix if present
239
- const cleanCmd = cmd.replace(/^\/mos:/, '').replace(/^mos:/, '');
240
- const framework = cmdToFramework[cleanCmd];
241
- if (framework && !frameworksUsed.has(framework)) {
242
- frameworksUsed.add(framework);
243
- const escapedFw = framework.replace(/'/g, "\\'");
244
- await brainWrite(`
245
- MATCH (r:Room {name: '${name}'}), (f:Framework {name: '${escapedFw}'})
246
- MERGE (r)-[e:USES_FRAMEWORK]->(f)
247
- ON CREATE SET e.first_used = datetime(), e.usage_count = ${count || 1}
248
- ON MATCH SET e.usage_count = ${count || 1}
249
- `);
250
- }
251
- }
252
- }
253
- }
254
-
255
- /**
256
- * Wire orphaned DataRoomSection nodes to parent Room nodes via HAS_SECTION.
257
- * DataRoomSection nodes already exist in Brain (13 orphans detected during context gathering).
258
- * @param {object} rooms - rooms object from registry.json
259
- */
260
- async function wireOrphanedSections(rooms) {
261
- // Standard room sections (matching DataRoomSection names in Brain)
262
- const sectionNames = [
263
- 'Problem Definition', 'Market Analysis', 'Solution Design',
264
- 'Business Model', 'Competitive Analysis', 'Team & Execution',
265
- 'Legal & IP', 'Financial Model', 'Opportunity Bank',
266
- 'Funding', 'Product', 'Decisions', 'Beta Testing'
267
- ];
268
-
269
- for (const [name, room] of Object.entries(rooms)) {
270
- if (room.status === 'archived') continue;
271
-
272
- const roomPath = path.join(ROOMS_HOME, room.path || name);
273
-
274
- // Check which sections actually exist in this room's filesystem
275
- for (const sectionName of sectionNames) {
276
- const slug = sectionName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-+$/, '');
277
- const sectionDir = path.join(roomPath, slug);
278
-
279
- if (fs.existsSync(sectionDir)) {
280
- const escapedSection = sectionName.replace(/'/g, "\\'");
281
- await brainWrite(`
282
- MATCH (r:Room {name: '${name}'}), (s:DataRoomSection {name: '${escapedSection}'})
283
- MERGE (r)-[:HAS_SECTION]->(s)
284
- `);
285
- }
286
- }
287
- }
288
- }
289
-
290
- /**
291
- * Detect SHARES_THEME edges between rooms based on CO_OCCURS pattern.
292
- * Reads problem-definition content from each room and finds shared domain keywords.
293
- * @param {object} rooms - rooms object from registry.json
294
- */
295
- async function detectSharedThemes(rooms) {
296
- const roomKeywords = new Map();
297
-
298
- // Extract keywords from each room's problem-definition
299
- for (const [name, room] of Object.entries(rooms)) {
300
- if (room.status === 'archived') continue;
301
-
302
- const roomPath = path.join(ROOMS_HOME, room.path || name);
303
- const probDefDir = path.join(roomPath, 'problem-definition');
304
-
305
- if (!fs.existsSync(probDefDir)) continue;
306
-
307
- const keywords = new Set();
308
-
309
- try {
310
- const files = fs.readdirSync(probDefDir).filter(f => f.endsWith('.md'));
311
- for (const file of files.slice(0, 5)) { // Limit to 5 files per room
312
- const content = fs.readFileSync(path.join(probDefDir, file), 'utf8');
313
- // Extract meaningful words (5+ chars, not common noise)
314
- const noise = new Set([
315
- 'about', 'their', 'these', 'those', 'which', 'where', 'through',
316
- 'between', 'using', 'based', 'should', 'would', 'could', 'other',
317
- 'being', 'there', 'every', 'after', 'before', 'while', 'since'
318
- ]);
319
- const words = content.toLowerCase().match(/\b[a-z]{5,}\b/g) || [];
320
- for (const word of words) {
321
- if (!noise.has(word)) keywords.add(word);
322
- }
323
- }
324
- } catch (_) {
325
- continue;
326
- }
327
-
328
- if (keywords.size > 0) {
329
- roomKeywords.set(name, keywords);
330
- }
331
- }
332
-
333
- // Find room pairs with significant keyword overlap
334
- const roomNames = [...roomKeywords.keys()];
335
- for (let i = 0; i < roomNames.length; i++) {
336
- for (let j = i + 1; j < roomNames.length; j++) {
337
- const nameA = roomNames[i];
338
- const nameB = roomNames[j];
339
- const kwA = roomKeywords.get(nameA);
340
- const kwB = roomKeywords.get(nameB);
341
-
342
- // Count intersection
343
- let shared = 0;
344
- const sharedTerms = [];
345
- for (const kw of kwA) {
346
- if (kwB.has(kw)) {
347
- shared++;
348
- if (sharedTerms.length < 10) sharedTerms.push(kw);
349
- }
350
- }
351
-
352
- // Require significant overlap (10+ shared domain words)
353
- if (shared >= 10) {
354
- const themes = sharedTerms.slice(0, 5).join(', ').replace(/'/g, "\\'");
355
- await brainWrite(`
356
- MATCH (r1:Room {name: '${nameA}'}), (r2:Room {name: '${nameB}'})
357
- MERGE (r1)-[e:SHARES_THEME]->(r2)
358
- ON CREATE SET e.shared_terms = '${themes}', e.term_count = ${shared}, e.detected = datetime()
359
- ON MATCH SET e.shared_terms = '${themes}', e.term_count = ${shared}
360
- `);
361
- }
362
- }
363
- }
364
- }
365
-
366
- async function main() {
367
- // Check Brain availability
368
- if (!brain.isAvailable()) {
369
- process.stderr.write('sync-rooms-brain: Brain API not available (no key), skipping\n');
370
- process.exit(0);
371
- }
372
-
373
- // Check registry exists
374
- if (!fs.existsSync(REGISTRY_PATH)) {
375
- process.exit(0);
376
- }
377
-
378
- // Load registry
379
- let registry;
380
- try {
381
- registry = JSON.parse(fs.readFileSync(REGISTRY_PATH, 'utf8'));
382
- } catch (_) {
383
- process.stderr.write('sync-rooms-brain: invalid registry.json\n');
384
- process.exit(0);
385
- }
386
-
387
- const rooms = registry.rooms || {};
388
- if (Object.keys(rooms).length === 0) {
389
- process.exit(0);
390
- }
391
-
392
- const results = {
393
- root: false,
394
- rooms: 0,
395
- groups: 0,
396
- atStage: 0,
397
- usesFramework: 0,
398
- hasSections: 0,
399
- sharesTheme: 0,
400
- errors: []
401
- };
402
-
403
- try {
404
- // 1. Create RoomRoot
405
- results.root = await syncRoomRoot();
406
-
407
- // 2. Sync Room nodes
408
- await syncRoomNodes(rooms);
409
- results.rooms = Object.keys(rooms).filter(n => rooms[n].status !== 'archived').length;
410
-
411
- // 3. Sync RoomGroup nodes and CONTAINS edges
412
- const groups = await syncRoomGroups(rooms);
413
- results.groups = groups.size;
414
-
415
- // 4. AT_STAGE edges
416
- await syncAtStageEdges(rooms);
417
- results.atStage = Object.values(rooms).filter(r => r.venture_stage && r.status !== 'archived').length;
418
-
419
- // 5. USES_FRAMEWORK edges
420
- await syncUsesFrameworkEdges(rooms);
421
-
422
- // 6. Wire orphaned DataRoomSection nodes
423
- await wireOrphanedSections(rooms);
424
-
425
- // 7. SHARES_THEME detection
426
- await detectSharedThemes(rooms);
427
-
428
- } catch (err) {
429
- results.errors.push(err.message);
430
- }
431
-
432
- console.log(JSON.stringify(results));
433
- }
434
-
435
- main().catch(err => {
436
- process.stderr.write(`sync-rooms-brain: ${err.message}\n`);
437
- process.exit(0);
438
- });