@mrpatronz/nexusflow 0.2.0 → 0.2.2
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/dist/analyzers/index.d.ts +2 -0
- package/dist/analyzers/index.d.ts.map +1 -1
- package/dist/analyzers/index.js +9 -1
- package/dist/analyzers/index.js.map +1 -1
- package/dist/analyzers/messaging-analyzer.d.ts +17 -0
- package/dist/analyzers/messaging-analyzer.d.ts.map +1 -0
- package/dist/analyzers/messaging-analyzer.js +229 -0
- package/dist/analyzers/messaging-analyzer.js.map +1 -0
- package/dist/analyzers/readme-summarizer.d.ts.map +1 -1
- package/dist/analyzers/readme-summarizer.js +8 -4
- package/dist/analyzers/readme-summarizer.js.map +1 -1
- package/dist/analyzers/run-analyzer.d.ts +15 -0
- package/dist/analyzers/run-analyzer.d.ts.map +1 -0
- package/dist/analyzers/run-analyzer.js +246 -0
- package/dist/analyzers/run-analyzer.js.map +1 -0
- package/dist/core/config.js +1 -1
- package/dist/core/config.js.map +1 -1
- package/dist/generators/base.d.ts.map +1 -1
- package/dist/generators/base.js +18 -57
- package/dist/generators/base.js.map +1 -1
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +30 -14
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/map-generator.d.ts +1 -1
- package/dist/generators/map-generator.d.ts.map +1 -1
- package/dist/generators/map-generator.js +157 -54
- package/dist/generators/map-generator.js.map +1 -1
- package/dist/generators/map-generator.test.js +71 -0
- package/dist/generators/map-generator.test.js.map +1 -1
- package/dist/generators/plan-generator.d.ts +2 -2
- package/dist/generators/plan-generator.d.ts.map +1 -1
- package/dist/generators/plan-generator.js +47 -67
- package/dist/generators/plan-generator.js.map +1 -1
- package/dist/generators/skills-generator.d.ts +15 -0
- package/dist/generators/skills-generator.d.ts.map +1 -0
- package/dist/generators/skills-generator.js +225 -0
- package/dist/generators/skills-generator.js.map +1 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +16 -85
- package/dist/mcp/server.js.map +1 -1
- package/dist/server.d.ts +0 -5
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +9 -6
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +46 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/analyzers/index.ts +9 -1
- package/src/analyzers/messaging-analyzer.ts +254 -0
- package/src/analyzers/readme-summarizer.ts +9 -4
- package/src/analyzers/run-analyzer.ts +269 -0
- package/src/core/config.ts +1 -1
- package/src/generators/base.ts +19 -56
- package/src/generators/index.ts +32 -14
- package/src/generators/map-generator.test.ts +78 -0
- package/src/generators/map-generator.ts +164 -53
- package/src/generators/plan-generator.ts +53 -75
- package/src/generators/skills-generator.ts +255 -0
- package/src/mcp/server.ts +16 -89
- package/src/server.ts +9 -6
- package/src/types.ts +54 -0
package/src/generators/base.ts
CHANGED
|
@@ -17,7 +17,7 @@ function formatProjectSection(analysis: ProjectAnalysis, workspacePath: string):
|
|
|
17
17
|
lines.push(`### ${analysis.name}`);
|
|
18
18
|
|
|
19
19
|
const mapPath = path.join(workspacePath, `nexusflow-map-${analysis.name}.md`).replace(/\\/g, '/');
|
|
20
|
-
lines.push(`- **Architecture Map**: [nexusflow-map-${analysis.name}.md](file:///${mapPath}) — **Instruction**:
|
|
20
|
+
lines.push(`- **Architecture Map**: [nexusflow-map-${analysis.name}.md](file:///${mapPath}) — **Instruction**: Before modifying this repository, read its architecture map. For exploration, consult the map's section index on demand.`);
|
|
21
21
|
|
|
22
22
|
// Tech stack
|
|
23
23
|
const { techStack } = analysis;
|
|
@@ -47,15 +47,7 @@ function formatProjectSection(analysis: ProjectAnalysis, workspacePath: string):
|
|
|
47
47
|
|
|
48
48
|
// API endpoints
|
|
49
49
|
if (analysis.endpoints.length > 0) {
|
|
50
|
-
lines.push(`- **API
|
|
51
|
-
// Show up to 10 endpoints
|
|
52
|
-
const shown = analysis.endpoints.slice(0, 10);
|
|
53
|
-
for (const ep of shown) {
|
|
54
|
-
lines.push(` - \`${ep.method} ${ep.path}\``);
|
|
55
|
-
}
|
|
56
|
-
if (analysis.endpoints.length > 10) {
|
|
57
|
-
lines.push(` - _...and ${analysis.endpoints.length - 10} more_`);
|
|
58
|
-
}
|
|
50
|
+
lines.push(`- **API surface**: ${analysis.endpoints.length} endpoints — see architecture map for details`);
|
|
59
51
|
}
|
|
60
52
|
|
|
61
53
|
// Ports
|
|
@@ -128,24 +120,18 @@ ${allConfigs.join('\n')}
|
|
|
128
120
|
if (mockCommand) parts.push(`- **Setup/Mock Command**: \`${mockCommand}\``);
|
|
129
121
|
if (startCommand) parts.push(`- **Start/Run Command**: \`${startCommand}\``);
|
|
130
122
|
|
|
123
|
+
const standardCommands = [
|
|
124
|
+
'npm run test', 'npm test', 'npm t', 'yarn test', 'yarn t', 'pnpm test', 'pnpm t', 'bun test',
|
|
125
|
+
'dotnet test',
|
|
126
|
+
'pytest', 'python -m unittest', 'python -m pytest',
|
|
127
|
+
'go test', 'go test ./...',
|
|
128
|
+
'cargo test',
|
|
129
|
+
];
|
|
130
|
+
|
|
131
131
|
if (testCommand) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
return a?.techStack.languages.includes('typescript') || a?.techStack.languages.includes('javascript');
|
|
136
|
-
});
|
|
137
|
-
const hasCsharp = repos.some(r => {
|
|
138
|
-
const a = analysis?.get(r.path);
|
|
139
|
-
return a?.techStack.languages.includes('csharp');
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
if (hasCsharp && !hasJs) {
|
|
143
|
-
testCommand = 'dotnet test';
|
|
144
|
-
} else if (!hasJs && !hasCsharp) {
|
|
145
|
-
testCommand = undefined;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
if (testCommand) {
|
|
132
|
+
const normalizedCmd = testCommand.trim().toLowerCase();
|
|
133
|
+
const isStandard = standardCommands.some(cmd => normalizedCmd === cmd || normalizedCmd.startsWith(cmd + ' '));
|
|
134
|
+
if (!isStandard) {
|
|
149
135
|
parts.push(`- **Verification/Test Command**: \`${testCommand}\``);
|
|
150
136
|
}
|
|
151
137
|
}
|
|
@@ -163,39 +149,16 @@ ${parts.join('\n')}
|
|
|
163
149
|
}
|
|
164
150
|
}
|
|
165
151
|
|
|
166
|
-
|
|
167
|
-
const overviewFile = path.join(workspacePath, 'nexusflow-overview.md');
|
|
168
|
-
const hasOverview = fs.existsSync(overviewFile);
|
|
169
|
-
|
|
170
|
-
let taskSection = '';
|
|
171
|
-
if (hasOverview) {
|
|
172
|
-
taskSection = `## Task & Step-by-Step Maintenance
|
|
173
|
-
|
|
174
|
-
The universal reference file **\`nexusflow-overview.md\`** has already been created. Your task is to:
|
|
175
|
-
|
|
176
|
-
1. **Keep it Updated**: Maintain and update \`nexusflow-overview.md\` with any new architectural findings, layout changes, or assumptions.
|
|
177
|
-
2. **Review Assumptions**: Ensure that inter-repo relationships and package dependencies documented there reflect the current codebase.
|
|
178
|
-
3. **Address Open Questions**: If there are outstanding items in the "Clarifying Questions for the User" section, discuss them with the user.
|
|
179
|
-
`;
|
|
180
|
-
} else {
|
|
181
|
-
taskSection = `## Task & Step-by-Step Initialization
|
|
152
|
+
const knowledgePath = path.join(workspacePath, 'nexusflow-knowledge.md').replace(/\\/g, '/');
|
|
182
153
|
|
|
183
|
-
|
|
154
|
+
const taskSection = `## First Steps
|
|
184
155
|
|
|
185
|
-
|
|
186
|
-
2. **Project Assumptions**: For each project, write down a clear assumption of what it does, its primary tech stack, and its core responsibilities.
|
|
187
|
-
3. **Inter-Repo Relationships**: Document how the repos relate:
|
|
188
|
-
- Shared libraries/packages (producers and consumers).
|
|
189
|
-
- API boundaries (which repos expose APIs, which ones consume them).
|
|
190
|
-
- Data flows and dependencies.
|
|
191
|
-
4. **Clarifying Questions**: If any feature requirements, architectural patterns, or API contracts are unclear, list them explicitly under a section called **"Clarifying Questions for the User"**.
|
|
192
|
-
5. **Universal Reference**: Keep this file updated. This acts as a universal reference so that any LLM assistant (Claude, Antigravity, Codex, Cursor, Copilot) joining this workspace instantly understands the project landscape.
|
|
156
|
+
Your very first task upon entering this workspace is to explore the codebase and align with the user:
|
|
193
157
|
|
|
194
|
-
|
|
158
|
+
1. **Verify Assumptions**: Open [nexusflow-knowledge.md](file:///${knowledgePath}) and fill in the **Project Assumptions** section with a brief description of what each project does, its tech stack, and responsibilities.
|
|
159
|
+
2. **Raise Questions**: Document any outstanding uncertainties or architectural questions in the **Clarifying Questions for the User** section.
|
|
160
|
+
3. **Obtain Approval**: Ask the user to confirm your assumptions and answer your questions before writing any code.
|
|
195
161
|
`;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
const knowledgePath = path.join(workspacePath, 'nexusflow-knowledge.md').replace(/\\/g, '/');
|
|
199
162
|
|
|
200
163
|
return `# Multi-Repo Workspace Context
|
|
201
164
|
|
package/src/generators/index.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
194
|
+
|
|
195
|
+
// Generate implementation plan from dependency analysis (if analysis data available)
|
|
178
196
|
try {
|
|
179
|
-
await
|
|
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
|
|
202
|
+
`Failed to generate implementation plan: ${message}`,
|
|
185
203
|
);
|
|
186
204
|
}
|
|
187
205
|
|
|
188
|
-
// Generate
|
|
206
|
+
// Generate skills files for selected assistants
|
|
189
207
|
try {
|
|
190
|
-
await
|
|
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
|
|
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
|
|
229
|
+
const patternExamples = new Map<string, { label: string; firstFile?: string; description: string }>();
|
|
200
230
|
|
|
201
|
-
// Initialize
|
|
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
|
-
|
|
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
|
-
|
|
236
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
260
|
-
|
|
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('|
|
|
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
|
|
276
|
-
|
|
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(`| \`${
|
|
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`);
|