@mrpatronz/nexusflow 0.2.1 → 0.2.3

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.
Files changed (63) hide show
  1. package/dist/analyzers/index.d.ts +2 -0
  2. package/dist/analyzers/index.d.ts.map +1 -1
  3. package/dist/analyzers/index.js +9 -1
  4. package/dist/analyzers/index.js.map +1 -1
  5. package/dist/analyzers/messaging-analyzer.d.ts +17 -0
  6. package/dist/analyzers/messaging-analyzer.d.ts.map +1 -0
  7. package/dist/analyzers/messaging-analyzer.js +229 -0
  8. package/dist/analyzers/messaging-analyzer.js.map +1 -0
  9. package/dist/analyzers/readme-summarizer.d.ts.map +1 -1
  10. package/dist/analyzers/readme-summarizer.js +8 -4
  11. package/dist/analyzers/readme-summarizer.js.map +1 -1
  12. package/dist/analyzers/run-analyzer.d.ts +15 -0
  13. package/dist/analyzers/run-analyzer.d.ts.map +1 -0
  14. package/dist/analyzers/run-analyzer.js +246 -0
  15. package/dist/analyzers/run-analyzer.js.map +1 -0
  16. package/dist/core/config.js +1 -1
  17. package/dist/core/config.js.map +1 -1
  18. package/dist/generators/base.d.ts.map +1 -1
  19. package/dist/generators/base.js +18 -57
  20. package/dist/generators/base.js.map +1 -1
  21. package/dist/generators/index.d.ts.map +1 -1
  22. package/dist/generators/index.js +30 -14
  23. package/dist/generators/index.js.map +1 -1
  24. package/dist/generators/map-generator.d.ts +1 -1
  25. package/dist/generators/map-generator.d.ts.map +1 -1
  26. package/dist/generators/map-generator.js +157 -54
  27. package/dist/generators/map-generator.js.map +1 -1
  28. package/dist/generators/map-generator.test.js +71 -0
  29. package/dist/generators/map-generator.test.js.map +1 -1
  30. package/dist/generators/plan-generator.d.ts +2 -2
  31. package/dist/generators/plan-generator.d.ts.map +1 -1
  32. package/dist/generators/plan-generator.js +47 -67
  33. package/dist/generators/plan-generator.js.map +1 -1
  34. package/dist/generators/skills-generator.d.ts +15 -0
  35. package/dist/generators/skills-generator.d.ts.map +1 -0
  36. package/dist/generators/skills-generator.js +225 -0
  37. package/dist/generators/skills-generator.js.map +1 -0
  38. package/dist/gui/assets/index-B3PIuWJS.js +22 -0
  39. package/dist/gui/assets/index-D-VigurY.css +2 -0
  40. package/dist/gui/index.html +2 -2
  41. package/dist/mcp/server.d.ts.map +1 -1
  42. package/dist/mcp/server.js +16 -85
  43. package/dist/mcp/server.js.map +1 -1
  44. package/dist/types.d.ts +46 -0
  45. package/dist/types.d.ts.map +1 -1
  46. package/gui/src/App.tsx +85 -3
  47. package/gui/src/features/workspace/WorkspaceList.tsx +85 -3
  48. package/package.json +1 -1
  49. package/src/analyzers/index.ts +9 -1
  50. package/src/analyzers/messaging-analyzer.ts +254 -0
  51. package/src/analyzers/readme-summarizer.ts +9 -4
  52. package/src/analyzers/run-analyzer.ts +269 -0
  53. package/src/core/config.ts +1 -1
  54. package/src/generators/base.ts +19 -56
  55. package/src/generators/index.ts +32 -14
  56. package/src/generators/map-generator.test.ts +78 -0
  57. package/src/generators/map-generator.ts +164 -53
  58. package/src/generators/plan-generator.ts +53 -75
  59. package/src/generators/skills-generator.ts +255 -0
  60. package/src/mcp/server.ts +16 -89
  61. package/src/types.ts +54 -0
  62. package/dist/gui/assets/index-C8W6FoPa.js +0 -21
  63. package/dist/gui/assets/index-CJn2LW8K.css +0 -2
@@ -8,7 +8,7 @@ import { generateCopilotConfig } from './copilot.js';
8
8
  import { generateCursorConfig } from './cursor.js';
9
9
  import { buildContextContent } from './base.js';
10
10
  import { generateImplementationPlan } from './plan-generator.js';
11
- import { generateWorkspaceGraphFiles } from '../core/graph.js';
11
+ import { generateSkills } from './skills-generator.js';
12
12
 
13
13
  /** Maps each assistant to its generator function and the file it produces. */
14
14
  const GENERATORS: Record<
@@ -65,6 +65,18 @@ ${feature.description}
65
65
 
66
66
  ${repoList}
67
67
 
68
+ ## Project Assumptions (verify with user)
69
+
70
+ <!-- AI assistants: Fill this in during your first session. List each project and describe what you assume its main purpose, tech stack, and responsibilities are. -->
71
+
72
+ _(No assumptions recorded yet. AI assistant to populate.)_
73
+
74
+ ## Clarifying Questions for the User
75
+
76
+ <!-- AI assistants: List any clarifying questions or ambiguities about requirements/architecture here. -->
77
+
78
+ _(No open questions recorded yet. AI assistant to populate.)_
79
+
68
80
  ## Architecture Decisions
69
81
 
70
82
  <!-- AI assistants: append decisions here as they are made during development.
@@ -86,12 +98,6 @@ ${progressItems}
86
98
  the same debugging. -->
87
99
 
88
100
  _(No gotchas recorded yet.)_
89
-
90
- ## Open Questions
91
-
92
- <!-- Add questions that need human input before the AI can proceed. -->
93
-
94
- _(No open questions.)_
95
101
  `;
96
102
  }
97
103
 
@@ -141,12 +147,23 @@ export async function generateContextFiles(
141
147
 
142
148
  // Generate per-repo architecture maps
143
149
  if (ctx.analysis) {
150
+ const allProduced = new Set<string>();
151
+ for (const [, a] of ctx.analysis) {
152
+ if (a.produces) {
153
+ for (const p of a.produces) {
154
+ allProduced.add(p.name.toLowerCase());
155
+ }
156
+ }
157
+ // Also treat the repository name as a produced package concept
158
+ allProduced.add(a.name.toLowerCase());
159
+ }
160
+
144
161
  for (const repo of ctx.repos) {
145
162
  const a = ctx.analysis.get(repo.path);
146
163
  if (a) {
147
164
  try {
148
165
  const { generateRepoMap } = await import('./map-generator.js');
149
- await generateRepoMap(repo, a, workspacePath);
166
+ await generateRepoMap(repo, a, workspacePath, allProduced);
150
167
  console.log(chalk.green(' ✔'), `Generated Architecture Map for ${chalk.bold(repo.name)}`);
151
168
  } catch (error) {
152
169
  const message = error instanceof Error ? error.message : String(error);
@@ -174,25 +191,26 @@ export async function generateContextFiles(
174
191
  }
175
192
  }
176
193
 
177
- // Generate Workspace Architecture Graph
194
+
195
+ // Generate implementation plan from dependency analysis (if analysis data available)
178
196
  try {
179
- await generateWorkspaceGraphFiles(ctx, workspacePath);
197
+ await generateImplementationPlan(ctx, workspacePath);
180
198
  } catch (error) {
181
199
  const message = error instanceof Error ? error.message : String(error);
182
200
  console.error(
183
201
  chalk.red(' ✖'),
184
- `Failed to generate workspace architecture graph: ${message}`,
202
+ `Failed to generate implementation plan: ${message}`,
185
203
  );
186
204
  }
187
205
 
188
- // Generate implementation plan from dependency analysis (if analysis data available)
206
+ // Generate skills files for selected assistants
189
207
  try {
190
- await generateImplementationPlan(ctx, workspacePath);
208
+ await generateSkills(ctx, assistants, workspacePath);
191
209
  } catch (error) {
192
210
  const message = error instanceof Error ? error.message : String(error);
193
211
  console.error(
194
212
  chalk.red(' ✖'),
195
- `Failed to generate implementation plan: ${message}`,
213
+ `Failed to generate skills: ${message}`,
196
214
  );
197
215
  }
198
216
  }
@@ -78,4 +78,82 @@ describe('generateRepoMap', () => {
78
78
  expect(content).toContain('GET');
79
79
  expect(content).toContain('/api/v1/users');
80
80
  });
81
+
82
+ it('should render messaging topology, run config, and group endpoints by module', async () => {
83
+ const mockRepo = {
84
+ name: 'test-repo',
85
+ path: '/original/path/test-repo',
86
+ defaultBranch: 'main',
87
+ };
88
+
89
+ const mockAnalysis: ProjectAnalysis = {
90
+ name: 'test-repo',
91
+ path: '/original/path/test-repo',
92
+ techStack: {
93
+ languages: ['typescript' as Language],
94
+ frameworks: ['react' as Framework],
95
+ buildTools: ['vite'],
96
+ projectType: 'frontend' as const,
97
+ },
98
+ endpoints: [
99
+ { method: 'GET', path: '/api/v1/users', source: 'src/routes/users.ts' },
100
+ { method: 'POST', path: '/api/v1/users', source: 'src/routes/users.ts' }
101
+ ],
102
+ dependencies: [
103
+ { name: 'my-internal-package', type: 'npm' as const },
104
+ { name: 'eslint', type: 'npm' as const }
105
+ ],
106
+ ports: [],
107
+ readmeSummary: 'A test repository.',
108
+ existingAIConfigs: [],
109
+ messaging: {
110
+ publishers: [
111
+ { contractType: 'OrderCreated', topicOrQueue: 'order-events', publisherFile: 'src/services/order.ts' }
112
+ ],
113
+ subscribers: [
114
+ { contractType: 'OrderCreated', handlerFile: 'src/handlers/order.ts', registrationFile: 'src/index.ts' }
115
+ ]
116
+ },
117
+ runConfig: {
118
+ entryPoints: [
119
+ { projectPath: 'package.json', type: 'node', command: 'npm run dev' }
120
+ ],
121
+ databases: [
122
+ { provider: 'PostgreSQL', host: 'localhost', configFile: '.env' }
123
+ ],
124
+ sharedInfraWarnings: [
125
+ { resource: 'Database', host: 'staging-db.org', configFile: '.env', warning: '⚠️ SHARED INFRA warning' }
126
+ ],
127
+ committedSecrets: [
128
+ { file: '.env', lineHint: 'DATABASE_PASSWORD' }
129
+ ],
130
+ externalDependencies: []
131
+ }
132
+ };
133
+
134
+ vi.spyOn(globby, 'globby').mockImplementation(async () => []);
135
+
136
+ const writtenFiles: Record<string, string> = {};
137
+ vi.spyOn(fs, 'writeFile').mockImplementation(async (filePath: any, content: any) => {
138
+ writtenFiles[filePath as string] = content as string;
139
+ return Promise.resolve();
140
+ });
141
+ vi.spyOn(fs, 'readFile').mockImplementation(async () => '## custom rule here');
142
+
143
+ await generateRepoMap(mockRepo, mockAnalysis, workspacePath, new Set(['my-internal-package']));
144
+
145
+ const expectedOutPath = path.join(workspacePath, 'nexusflow-map-test-repo.md');
146
+ const content = writtenFiles[expectedOutPath]!;
147
+
148
+ expect(content).toContain('## 📨 Messaging Topology');
149
+ expect(content).toContain('OrderCreated');
150
+ expect(content).toContain('order-events');
151
+ expect(content).toContain('## ▶️ Running Locally');
152
+ expect(content).toContain('### Entry Points');
153
+ expect(content).toContain('### ⚠️ Shared Infrastructure Warnings');
154
+ expect(content).toContain('my-internal-package');
155
+ expect(content).not.toContain('eslint');
156
+ expect(content).toContain('Endpoint Group (Router/Module/File)');
157
+ expect(content).toContain('users');
158
+ });
81
159
  });
@@ -8,7 +8,6 @@ import * as fs from 'node:fs/promises';
8
8
  import * as path from 'node:path';
9
9
  import { globby } from 'globby';
10
10
  import type { ProjectAnalysis, RepoInfo } from '../types.js';
11
- import { loadConfig } from '../core/config.js';
12
11
 
13
12
  interface PatternRule {
14
13
  name: string;
@@ -119,6 +118,7 @@ export async function generateRepoMap(
119
118
  repo: RepoInfo,
120
119
  analysis: ProjectAnalysis,
121
120
  workspacePath: string,
121
+ allProducedPackages: Set<string> = new Set(),
122
122
  ): Promise<void> {
123
123
  const repoName = repo.name;
124
124
  const worktreePath = path.join(workspacePath, repoName);
@@ -133,13 +133,6 @@ export async function generateRepoMap(
133
133
  md.push(`> **Note**: Maps are advisory snapshots of the codebase. Always verify route parameters, patterns, and filenames before relying on them.`);
134
134
  md.push('');
135
135
 
136
- const config = await loadConfig();
137
- if (config.packContextXml) {
138
- const contextXmlPath = path.join(workspacePath, `nexusflow-context-${repoName}.xml`).replace(/\\/g, '/');
139
- md.push(`> **AI-friendly Packed Context**: [nexusflow-context-${repoName}.xml](file:///${contextXmlPath})`);
140
- md.push(`> — **Instruction**: If you need a complete, AI-friendly XML snapshot of this repository's codebase, read this file.`);
141
- md.push('');
142
- }
143
136
 
144
137
  // 1. Solution/Project Layout
145
138
  md.push('## 🏗️ Project Layout');
@@ -191,18 +184,55 @@ export async function generateRepoMap(
191
184
  md.push('');
192
185
  }
193
186
 
187
+ // 1.5. Messaging Topology
188
+ md.push('## 📨 Messaging Topology');
189
+ md.push('');
190
+ if (analysis.messaging && (analysis.messaging.publishers.length > 0 || analysis.messaging.subscribers.length > 0)) {
191
+ if (analysis.messaging.publishers.length > 0) {
192
+ md.push('### Publishes');
193
+ md.push('| Message Contract | Topic/Queue/Channel | Publisher (file) |');
194
+ md.push('|---|---|---|');
195
+ for (const p of analysis.messaging.publishers) {
196
+ const fileLink = p.publisherFile
197
+ ? `[${path.basename(p.publisherFile)}](file:///${path.join(worktreePath, p.publisherFile)})`
198
+ : '—';
199
+ md.push(`| ${p.contractType} | ${p.topicOrQueue} | ${fileLink} |`);
200
+ }
201
+ md.push('');
202
+ }
203
+
204
+ if (analysis.messaging.subscribers.length > 0) {
205
+ md.push('### Subscribes');
206
+ md.push('| Message Contract | Handler (file) | Registered in |');
207
+ md.push('|---|---|---|');
208
+ for (const s of analysis.messaging.subscribers) {
209
+ const handlerLink = s.handlerFile
210
+ ? `[${path.basename(s.handlerFile)}](file:///${path.join(worktreePath, s.handlerFile)})`
211
+ : '—';
212
+ const regLink = s.registrationFile
213
+ ? `[${path.basename(s.registrationFile)}](file:///${path.join(worktreePath, s.registrationFile)})`
214
+ : '—';
215
+ md.push(`| ${s.contractType} | ${handlerLink} | ${regLink} |`);
216
+ }
217
+ md.push('');
218
+ }
219
+ } else {
220
+ md.push('_No pub/sub messaging patterns detected in this repository._');
221
+ md.push('');
222
+ }
223
+
194
224
  // 2. Extensible Usage Pattern Scanning
195
225
  md.push('## 💡 Detected Architectural Patterns & Usages');
196
226
  md.push('');
197
227
 
198
228
  const detectedLanguages = analysis.techStack.languages;
199
- const patternCounts = new Map<string, { label: string; count: number; description: string }>();
229
+ const patternExamples = new Map<string, { label: string; firstFile?: string; description: string }>();
200
230
 
201
- // Initialize counts
231
+ // Initialize examples
202
232
  for (const lang of detectedLanguages) {
203
233
  const rules = LANG_PATTERNS[lang] || [];
204
234
  for (const rule of rules) {
205
- patternCounts.set(rule.name, { label: rule.label, count: 0, description: rule.description });
235
+ patternExamples.set(rule.name, { label: rule.label, description: rule.description });
206
236
  }
207
237
  }
208
238
 
@@ -231,11 +261,12 @@ export async function generateRepoMap(
231
261
  for (const lang of detectedLanguages) {
232
262
  const rules = LANG_PATTERNS[lang] || [];
233
263
  for (const rule of rules) {
264
+ const val = patternExamples.get(rule.name)!;
265
+ if (val.firstFile) continue; // Already found an example
266
+
234
267
  rule.regex.lastIndex = 0;
235
- const matches = content.match(rule.regex);
236
- if (matches) {
237
- const val = patternCounts.get(rule.name)!;
238
- val.count += matches.length;
268
+ if (rule.regex.test(content)) {
269
+ val.firstFile = file.replace(/\\/g, '/');
239
270
  }
240
271
  }
241
272
  }
@@ -246,22 +277,41 @@ export async function generateRepoMap(
246
277
  }
247
278
 
248
279
  md.push('### Static Analysis Findings');
249
- if (patternCounts.size > 0) {
250
- for (const [, v] of patternCounts) {
251
- md.push(`- **${v.label}**: Found ${v.count} occurrence(s). _(${v.description})_`);
280
+ let hasFindings = false;
281
+ if (patternExamples.size > 0) {
282
+ for (const [, v] of patternExamples) {
283
+ if (v.firstFile) {
284
+ hasFindings = true;
285
+ const fileLink = `[${path.basename(v.firstFile)}](file:///${path.join(worktreePath, v.firstFile).replace(/\\/g, '/')})`;
286
+ md.push(`- **${v.label}** example: ${fileLink} _(${v.description})_`);
287
+ }
252
288
  }
253
- } else {
289
+ }
290
+ if (!hasFindings) {
254
291
  md.push('_No architectural usage patterns detected via static analysis._');
255
292
  }
256
293
  md.push('');
257
294
 
258
295
  md.push('### Packages Present (Dependencies)');
259
- if (analysis.dependencies.length > 0) {
260
- for (const dep of analysis.dependencies) {
296
+ // Filter dependencies: only show internal packages produced by repos in this workspace,
297
+ // or those matching a common organization namespace prefix (e.g. if one package is MyOrg.Common, then MyOrg.*).
298
+ const internalPrefixes = Array.from(allProducedPackages).map(p => {
299
+ const parts = p.split('.');
300
+ return parts.length > 1 ? parts[0] + '.' : null;
301
+ }).filter((p): p is string => p !== null);
302
+
303
+ const filteredDeps = analysis.dependencies.filter(dep => {
304
+ const depNameLower = dep.name.toLowerCase();
305
+ if (allProducedPackages.has(depNameLower)) return true;
306
+ return internalPrefixes.some(prefix => depNameLower.startsWith(prefix));
307
+ });
308
+
309
+ if (filteredDeps.length > 0) {
310
+ for (const dep of filteredDeps) {
261
311
  md.push(`- \`${dep.name}\` (${dep.version || 'unknown version'})`);
262
312
  }
263
313
  } else {
264
- md.push('_No package dependencies detected._');
314
+ md.push('_No cross-repo or organization-internal package dependencies detected._');
265
315
  }
266
316
  md.push('');
267
317
 
@@ -269,13 +319,46 @@ export async function generateRepoMap(
269
319
  md.push('## 🔌 API Endpoints');
270
320
  md.push('');
271
321
  if (analysis.endpoints.length > 0) {
272
- md.push('| Method | Route | Controller/Source File |');
273
- md.push('|:---|:---|:---|');
322
+ md.push('| Endpoint Group (Router/Module/File) | Route Prefix / Pattern | Verbs | Source File |');
323
+ md.push('|:---|:---|:---|:---|');
324
+
325
+ // Group endpoints by source file
326
+ const grouped = new Map<string, typeof analysis.endpoints>();
274
327
  for (const ep of analysis.endpoints) {
275
- const sourceLink = ep.source
276
- ? `[${path.basename(ep.source)}](file:///${path.join(worktreePath, ep.source)})`
328
+ const src = ep.source || 'Unknown';
329
+ if (!grouped.has(src)) {
330
+ grouped.set(src, []);
331
+ }
332
+ grouped.get(src)!.push(ep);
333
+ }
334
+
335
+ const findCommonPrefix = (paths: string[]): string => {
336
+ if (paths.length === 0) return '';
337
+ if (paths.length === 1) return paths[0]!;
338
+ const sorted = [...paths].sort();
339
+ const first = sorted[0]!.split('/');
340
+ const last = sorted[sorted.length - 1]!.split('/');
341
+ const common: string[] = [];
342
+ for (let i = 0; i < first.length; i++) {
343
+ if (first[i] === last[i]) {
344
+ common.push(first[i]!);
345
+ } else {
346
+ break;
347
+ }
348
+ }
349
+ const prefix = common.join('/');
350
+ return prefix || '/';
351
+ };
352
+
353
+ for (const [src, eps] of grouped) {
354
+ const groupName = src !== 'Unknown' ? path.basename(src, path.extname(src)) : 'Inferred';
355
+ const paths = eps.map(e => e.path);
356
+ const commonPrefix = findCommonPrefix(paths);
357
+ const verbs = Array.from(new Set(eps.map(e => e.method.toUpperCase()))).join(', ');
358
+ const sourceLink = src !== 'Unknown'
359
+ ? `[${path.basename(src)}](file:///${path.join(worktreePath, src).replace(/\\/g, '/')})`
277
360
  : '—';
278
- md.push(`| \`${ep.method}\` | \`${ep.path}\` | ${sourceLink} |`);
361
+ md.push(`| ${groupName} | \`${commonPrefix}\` | \`${verbs}\` | ${sourceLink} |`);
279
362
  }
280
363
  } else {
281
364
  md.push('_No endpoints detected._');
@@ -324,6 +407,50 @@ export async function generateRepoMap(
324
407
  }
325
408
  md.push('');
326
409
 
410
+ // 4.5. Running Locally
411
+ md.push('## ▶️ Running Locally');
412
+ md.push('');
413
+ if (analysis.runConfig) {
414
+ const { entryPoints, databases, sharedInfraWarnings, committedSecrets } = analysis.runConfig;
415
+
416
+ if (entryPoints.length > 0) {
417
+ md.push('### Entry Points');
418
+ for (const ep of entryPoints) {
419
+ md.push(`- **${ep.type.toUpperCase()} App**: \`${ep.command || 'dotnet run'}\` (configured in \`${ep.projectPath}\`)`);
420
+ }
421
+ md.push('');
422
+ }
423
+
424
+ if (databases.length > 0) {
425
+ md.push('### Databases & Data Stores');
426
+ md.push('| Provider | Target Host | Config File |');
427
+ md.push('|---|---|---|');
428
+ for (const db of databases) {
429
+ md.push(`| ${db.provider} | ${db.host} | \`${db.configFile}\` |`);
430
+ }
431
+ md.push('');
432
+ }
433
+
434
+ if (sharedInfraWarnings.length > 0) {
435
+ md.push('### ⚠️ Shared Infrastructure Warnings');
436
+ for (const w of sharedInfraWarnings) {
437
+ md.push(`- ${w.warning}`);
438
+ }
439
+ md.push('');
440
+ }
441
+
442
+ if (committedSecrets.length > 0) {
443
+ md.push('### 🔒 Potential Committed Secrets');
444
+ for (const s of committedSecrets) {
445
+ md.push(`- **Warning**: Possible plaintext secret/key/password found in \`${s.file}\` (key hint: \`${s.lineHint}\`)`);
446
+ }
447
+ md.push('');
448
+ }
449
+ } else {
450
+ md.push('_No run configurations detected._');
451
+ md.push('');
452
+ }
453
+
327
454
  // 5. Custom Skills
328
455
  md.push('## 🛠️ Custom Agent Skills');
329
456
  md.push('');
@@ -363,41 +490,25 @@ export async function generateRepoMap(
363
490
  md.push('');
364
491
 
365
492
  // 7. Project-Specific Conventions (Agent-Defined)
493
+ md.push('## 📝 Discovered Conventions');
494
+ md.push('');
495
+
366
496
  const conventionsFile = path.join(workspacePath, `nexusflow-conventions-${repoName}.md`);
367
- let hasConventions = false;
368
- try {
369
- await fs.access(conventionsFile);
370
- hasConventions = true;
371
- } catch {}
372
-
373
- if (!hasConventions) {
374
- const starterContent = [
375
- `# Project Conventions — ${repoName}`,
376
- '',
377
- `<!--`,
378
- `This file is dedicated for the AI assistant and developers to document project-specific conventions.`,
379
- `Any corrections or guidelines discovered during implementation should be appended here.`,
380
- `The NexusFlow generator will automatically merge these into the architecture map during sync.`,
381
- `-->`,
382
- '',
383
- `## 📌 Custom Rules & Discovered Conventions`,
384
- '- ',
385
- ].join('\n');
386
- try {
387
- await fs.writeFile(conventionsFile, starterContent, 'utf-8');
388
- } catch {}
389
- }
390
-
391
497
  let customConventions = '';
392
498
  try {
393
499
  customConventions = await fs.readFile(conventionsFile, 'utf-8');
500
+ // Strip the title from the old conventions file if present
501
+ customConventions = customConventions.replace(/^#\s+.+\n?/, '').trim();
394
502
  } catch {}
395
503
 
396
504
  if (customConventions) {
397
- md.push('## 📝 Project-Specific Conventions (Agent-Defined)');
398
- md.push('');
399
505
  md.push(customConventions);
400
506
  md.push('');
507
+ } else {
508
+ md.push('<!-- AI assistants: Document any project-specific conventions, gotchas, or coding rules discovered here. -->');
509
+ md.push('');
510
+ md.push('- None recorded yet.');
511
+ md.push('');
401
512
  }
402
513
 
403
514
  const outPath = path.join(workspacePath, `nexusflow-map-${repoName}.md`);
@@ -15,22 +15,11 @@ import type {
15
15
  DependencyGraph,
16
16
  } from '../types.js';
17
17
 
18
- // ─── Constants ────────────────────────────────────────────────────────────
19
-
20
- /** Repo-name substrings that signal a shared/foundation package. */
21
- const SHARED_PACKAGE_KEYWORDS = ['shared', 'common', 'contracts', 'types'];
22
-
23
- /** Project types that act as backend producers. */
24
- const BACKEND_TYPES = ['api', 'backend', 'service'];
25
-
26
- /** Project types that act as frontend consumers. */
27
- const FRONTEND_TYPES = ['frontend', 'webapp', 'app'];
28
-
29
18
  // ─── Dependency Graph Builder ─────────────────────────────────────────────
30
19
 
31
20
  /**
32
- * Build a dependency graph by analysing package dependencies, API
33
- * relationships and shared-package conventions across the workspace repos.
21
+ * Build a dependency graph by analysing package dependencies
22
+ * across the workspace repos.
34
23
  *
35
24
  * @param analysis Per-repo analysis results, keyed by repo path.
36
25
  * @param repos Metadata for every repo in the workspace.
@@ -88,67 +77,16 @@ export function buildDependencyGraph(
88
77
  for (const dep of a.dependencies) {
89
78
  const depNameLower = dep.name.toLowerCase();
90
79
 
91
- // 1. Direct match with a produced package
80
+ // Direct match with a produced package
92
81
  if (packageToRepo.has(depNameLower)) {
93
82
  const targetRepo = packageToRepo.get(depNameLower)!;
94
83
  if (targetRepo !== repo.name) {
95
84
  addEdge(graph, repo.name, targetRepo);
96
85
  }
97
- } else {
98
- // 2. Check if the dependency contains or is contained by a produced package name
99
- for (const [prodPkg, targetRepo] of packageToRepo) {
100
- if (targetRepo === repo.name) continue;
101
- if (depNameLower.includes(prodPkg) || prodPkg.includes(depNameLower)) {
102
- addEdge(graph, repo.name, targetRepo);
103
- }
104
- }
105
- }
106
- }
107
- }
108
-
109
- // ── 2. API relationships (frontend → backend heuristic) ─────────────
110
- for (const repoA of repos) {
111
- const analysisA = analysisByName.get(repoA.name);
112
- if (!analysisA) continue;
113
-
114
- const typeA = analysisA.techStack.projectType;
115
-
116
- if (!BACKEND_TYPES.includes(typeA)) continue;
117
-
118
- for (const repoB of repos) {
119
- if (repoB.name === repoA.name) continue;
120
-
121
- const analysisB = analysisByName.get(repoB.name);
122
- if (!analysisB) continue;
123
-
124
- const typeB = analysisB.techStack.projectType;
125
- if (FRONTEND_TYPES.includes(typeB)) {
126
- // B (frontend) depends on A (backend)
127
- addEdge(graph, repoB.name, repoA.name);
128
86
  }
129
87
  }
130
88
  }
131
89
 
132
- // ── 3. Shared-type packages are always foundation ───────────────────
133
- for (const repo of repos) {
134
- const isShared = SHARED_PACKAGE_KEYWORDS.some((kw) =>
135
- repo.name.toLowerCase().includes(kw),
136
- );
137
- if (!isShared) continue;
138
-
139
- const node = graph.get(repo.name);
140
- if (!node) continue;
141
-
142
- // Ensure no outgoing deps (it's a leaf producer)
143
- node.dependsOn = [];
144
-
145
- // Every other repo that doesn't already depend on it — add edge
146
- for (const other of repos) {
147
- if (other.name === repo.name) continue;
148
- addEdge(graph, other.name, repo.name);
149
- }
150
- }
151
-
152
90
  return graph;
153
91
  }
154
92
 
@@ -297,25 +235,19 @@ export async function generateImplementationPlan(
297
235
 
298
236
  md.push('```');
299
237
  md.push('');
238
+ md.push('> ⚠️ This diagram is derived from detected package dependencies (`package.json`, `.csproj`, etc.) only.');
239
+ md.push('> If you changed a package, the producing repo must release/build before consumer repos can merge.');
240
+ md.push('');
300
241
 
301
242
  // ── Phase descriptions ──────────────────────────────────────────────
302
243
  md.push('## Suggested Implementation Order');
303
244
  md.push('');
304
245
 
305
- const phaseLabels = [
306
- 'Foundation',
307
- 'Core Services',
308
- 'Integration Layer',
309
- 'Consumers',
310
- 'Final',
311
- ];
312
-
313
246
  for (let i = 0; i < phases.length; i++) {
314
247
  const phase = phases[i];
315
- const label = phaseLabels[Math.min(i, phaseLabels.length - 1)];
316
248
  const ordinal = ordinalWord(i + 1);
317
249
 
318
- md.push(`### Phase ${i + 1}: ${label}`);
250
+ md.push(`### Phase ${i + 1}`);
319
251
  md.push('');
320
252
  md.push(`**Repos:** ${phase.join(', ')}`);
321
253
  md.push('');
@@ -421,6 +353,52 @@ export async function generateImplementationPlan(
421
353
  }
422
354
  md.push('');
423
355
 
356
+ // ── Cross-Repo Messaging Roll-up ────────────────────────────────────
357
+ md.push('## 📨 Cross-Repo Messaging');
358
+ md.push('');
359
+ md.push('| Publisher Repo | Message | → Subscriber Repo | Handler |');
360
+ md.push('|---|---|---|---|');
361
+
362
+ interface CrossRepoMessage {
363
+ pubRepo: string;
364
+ message: string;
365
+ subRepo: string;
366
+ handler: string;
367
+ }
368
+ const crossRepoMessages: CrossRepoMessage[] = [];
369
+
370
+ for (const [pubPath, pubA] of analysis) {
371
+ if (!pubA.messaging || !pubA.messaging.publishers) continue;
372
+ for (const pub of pubA.messaging.publishers) {
373
+ // Find subscribers in other repos matching this contract type
374
+ for (const [subPath, subA] of analysis) {
375
+ if (subPath === pubPath) continue;
376
+ if (!subA.messaging || !subA.messaging.subscribers) continue;
377
+ for (const sub of subA.messaging.subscribers) {
378
+ const pubContract = pub.contractType.toLowerCase().trim();
379
+ const subContract = sub.contractType.toLowerCase().trim();
380
+ if (pubContract === subContract && pubContract !== 'goservicebusmessage' && pubContract !== 'servicebusmessage') {
381
+ crossRepoMessages.push({
382
+ pubRepo: pubA.name,
383
+ message: pub.contractType,
384
+ subRepo: subA.name,
385
+ handler: sub.handlerFile,
386
+ });
387
+ }
388
+ }
389
+ }
390
+ }
391
+ }
392
+
393
+ if (crossRepoMessages.length > 0) {
394
+ for (const m of crossRepoMessages) {
395
+ md.push(`| \`${m.pubRepo}\` | \`${m.message}\` | \`${m.subRepo}\` | \`${m.handler}\` |`);
396
+ }
397
+ } else {
398
+ md.push('| _No cross-repo messaging detected_ | | | |');
399
+ }
400
+ md.push('');
401
+
424
402
  // ── Local Package Development Loop Tip ──────────────────────────────
425
403
  md.push('## 💡 Local Package Development Loop');
426
404
  md.push('');