@girardmedia/bootspring 2.0.21 → 2.0.23
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/bin/bootspring.js +5 -0
- package/cli/org.js +474 -0
- package/cli/preseed/index.js +16 -0
- package/cli/preseed/interactive.js +143 -0
- package/cli/preseed/templates.js +227 -0
- package/cli/preseed.js +9 -301
- package/cli/seed/builders/ai-context-builder.js +85 -0
- package/cli/seed/builders/index.js +13 -0
- package/cli/seed/builders/seed-builder.js +272 -0
- package/cli/seed/extractors/content-extractors.js +383 -0
- package/cli/seed/extractors/index.js +47 -0
- package/cli/seed/extractors/metadata-extractors.js +167 -0
- package/cli/seed/extractors/section-extractor.js +54 -0
- package/cli/seed/extractors/stack-extractors.js +228 -0
- package/cli/seed/index.js +18 -0
- package/cli/seed/utils/folder-structure.js +84 -0
- package/cli/seed/utils/index.js +11 -0
- package/cli/seed.js +23 -1074
- package/core/api-client.js +77 -0
- package/core/entitlements.js +36 -0
- package/core/organizations.js +223 -0
- package/core/policies.js +51 -6
- package/core/policy-matrix.js +303 -0
- package/core/project-context.js +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +3220 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/context-McpJQa_2.d.ts +5710 -0
- package/dist/core/index.d.ts +635 -0
- package/dist/core/index.js +2593 -0
- package/dist/core/index.js.map +1 -0
- package/dist/index-QqbeEiDm.d.ts +857 -0
- package/dist/index-UiYCgwiH.d.ts +174 -0
- package/dist/index.d.ts +453 -0
- package/dist/index.js +44228 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +41173 -0
- package/dist/mcp/index.js.map +1 -0
- package/generators/index.ts +82 -0
- package/intelligence/orchestrator/config/failure-signatures.js +48 -0
- package/intelligence/orchestrator/config/index.js +23 -0
- package/intelligence/orchestrator/config/pack-lifecycle.js +262 -0
- package/intelligence/orchestrator/config/phases.js +111 -0
- package/intelligence/orchestrator/config/remediation.js +150 -0
- package/intelligence/orchestrator/config/workflows.js +168 -0
- package/intelligence/orchestrator/core/index.js +16 -0
- package/intelligence/orchestrator/core/state-manager.js +88 -0
- package/intelligence/orchestrator/core/telemetry.js +24 -0
- package/intelligence/orchestrator/index.js +17 -0
- package/intelligence/orchestrator.js +17 -512
- package/mcp/contracts/mcp-contract.v1.json +1 -1
- package/package.json +16 -3
- package/src/cli/agent.ts +703 -0
- package/src/cli/analyze.ts +640 -0
- package/src/cli/audit.ts +707 -0
- package/src/cli/auth.ts +930 -0
- package/src/cli/billing.ts +364 -0
- package/src/cli/build.ts +1089 -0
- package/src/cli/business.ts +508 -0
- package/src/cli/checkpoint-utils.ts +236 -0
- package/src/cli/checkpoint.ts +757 -0
- package/src/cli/cloud-sync.ts +534 -0
- package/src/cli/content.ts +273 -0
- package/src/cli/context.ts +667 -0
- package/src/cli/dashboard.ts +133 -0
- package/src/cli/deploy.ts +704 -0
- package/src/cli/doctor.ts +480 -0
- package/src/cli/fundraise.ts +494 -0
- package/src/cli/generate.ts +346 -0
- package/src/cli/github-cmd.ts +566 -0
- package/src/cli/health.ts +599 -0
- package/src/cli/index.ts +113 -0
- package/src/cli/init.ts +838 -0
- package/src/cli/legal.ts +495 -0
- package/src/cli/log.ts +316 -0
- package/src/cli/loop.ts +1660 -0
- package/src/cli/manager.ts +878 -0
- package/src/cli/mcp.ts +275 -0
- package/src/cli/memory.ts +346 -0
- package/src/cli/metrics.ts +590 -0
- package/src/cli/monitor.ts +960 -0
- package/src/cli/mvp.ts +662 -0
- package/src/cli/onboard.ts +663 -0
- package/src/cli/orchestrator.ts +622 -0
- package/src/cli/plugin.ts +483 -0
- package/src/cli/prd.ts +671 -0
- package/src/cli/preseed-start.ts +1633 -0
- package/src/cli/preseed.ts +2434 -0
- package/src/cli/project.ts +526 -0
- package/src/cli/quality.ts +885 -0
- package/src/cli/security.ts +1079 -0
- package/src/cli/seed.ts +1224 -0
- package/src/cli/skill.ts +537 -0
- package/src/cli/suggest.ts +1225 -0
- package/src/cli/switch.ts +518 -0
- package/src/cli/task.ts +780 -0
- package/src/cli/telemetry.ts +172 -0
- package/src/cli/todo.ts +627 -0
- package/src/cli/types.ts +15 -0
- package/src/cli/update.ts +334 -0
- package/src/cli/visualize.ts +609 -0
- package/src/cli/watch.ts +895 -0
- package/src/cli/workspace.ts +709 -0
- package/src/core/action-recorder.ts +673 -0
- package/src/core/analyze-workflow.ts +1453 -0
- package/src/core/api-client.ts +1120 -0
- package/src/core/audit-workflow.ts +1681 -0
- package/src/core/auth.ts +471 -0
- package/src/core/build-orchestrator.ts +509 -0
- package/src/core/build-state.ts +621 -0
- package/src/core/checkpoint-engine.ts +482 -0
- package/src/core/config.ts +1285 -0
- package/src/core/context-loader.ts +694 -0
- package/src/core/context.ts +410 -0
- package/src/core/deploy-workflow.ts +1085 -0
- package/src/core/entitlements.ts +322 -0
- package/src/core/github-sync.ts +720 -0
- package/src/core/index.ts +981 -0
- package/src/core/ingest.ts +1186 -0
- package/src/core/metrics-engine.ts +886 -0
- package/src/core/mvp.ts +847 -0
- package/src/core/onboard-workflow.ts +1293 -0
- package/src/core/policies.ts +81 -0
- package/src/core/preseed-workflow.ts +1163 -0
- package/src/core/preseed.ts +1826 -0
- package/src/core/project-context.ts +380 -0
- package/src/core/project-state.ts +699 -0
- package/src/core/r2-sync.ts +691 -0
- package/src/core/scaffold.ts +1715 -0
- package/src/core/session.ts +286 -0
- package/src/core/task-extractor.ts +799 -0
- package/src/core/telemetry.ts +371 -0
- package/src/core/tier-enforcement.ts +737 -0
- package/src/core/utils.ts +437 -0
- package/src/index.ts +29 -0
- package/src/intelligence/agent-collab.ts +2376 -0
- package/src/intelligence/auto-suggest.ts +713 -0
- package/src/intelligence/content-gen.ts +1351 -0
- package/src/intelligence/cross-project.ts +1692 -0
- package/src/intelligence/git-memory.ts +529 -0
- package/src/intelligence/index.ts +318 -0
- package/src/intelligence/orchestrator.ts +534 -0
- package/src/intelligence/prd.ts +466 -0
- package/src/intelligence/recommendations.ts +982 -0
- package/src/intelligence/workflow-composer.ts +1472 -0
- package/src/mcp/capabilities.ts +233 -0
- package/src/mcp/index.ts +37 -0
- package/src/mcp/registry.ts +1268 -0
- package/src/mcp/response-formatter.ts +797 -0
- package/src/mcp/server.ts +240 -0
- package/src/types/agent.ts +69 -0
- package/src/types/config.ts +86 -0
- package/src/types/context.ts +77 -0
- package/src/types/index.ts +53 -0
- package/src/types/mcp.ts +91 -0
- package/src/types/skills.ts +47 -0
- package/src/types/workflow.ts +155 -0
- package/generators/index.js +0 -18
|
@@ -0,0 +1,1453 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bootspring Analyze Workflow Engine
|
|
3
|
+
*
|
|
4
|
+
* Deep codebase analysis for understanding architecture,
|
|
5
|
+
* dependencies, patterns, and code flows.
|
|
6
|
+
*
|
|
7
|
+
* @package bootspring
|
|
8
|
+
* @module core/analyze-workflow
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
|
|
14
|
+
// Import analyzers from JS modules
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
16
|
+
const { StructureAnalyzer } = require('../../analyzers/structure-analyzer') as {
|
|
17
|
+
StructureAnalyzer: new (projectRoot: string, options?: Record<string, unknown>) => {
|
|
18
|
+
analyze: () => Record<string, unknown>;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
23
|
+
const { ArchitectureAnalyzer } = require('../../analyzers/architecture-analyzer') as {
|
|
24
|
+
ArchitectureAnalyzer: new (projectRoot: string) => {
|
|
25
|
+
analyze: () => Record<string, unknown>;
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
30
|
+
const { DependencyAnalyzer } = require('../../analyzers/dependency-analyzer') as {
|
|
31
|
+
DependencyAnalyzer: new (projectRoot: string) => {
|
|
32
|
+
analyze: () => Record<string, unknown>;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Types
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
export type PhaseStatus = 'pending' | 'in_progress' | 'completed' | 'skipped' | 'failed';
|
|
41
|
+
export type AnalysisDepthLevel = 'shallow' | 'standard' | 'deep';
|
|
42
|
+
|
|
43
|
+
export interface PhaseDefinition {
|
|
44
|
+
name: string;
|
|
45
|
+
description: string;
|
|
46
|
+
order: number;
|
|
47
|
+
required: boolean;
|
|
48
|
+
dependencies?: string[] | undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface PhaseState {
|
|
52
|
+
status: PhaseStatus;
|
|
53
|
+
startedAt: string | null;
|
|
54
|
+
completedAt: string | null;
|
|
55
|
+
result: unknown | null;
|
|
56
|
+
error: string | null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface DepthConfig {
|
|
60
|
+
phases: string[];
|
|
61
|
+
description: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface AnalyzeWorkflowState {
|
|
65
|
+
version: string;
|
|
66
|
+
startedAt: string | null;
|
|
67
|
+
lastUpdated: string | null;
|
|
68
|
+
currentPhase: string | null;
|
|
69
|
+
depth: AnalysisDepthLevel;
|
|
70
|
+
phases: Record<string, PhaseState>;
|
|
71
|
+
results: Record<string, unknown>;
|
|
72
|
+
summary: WorkflowSummary | null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface WorkflowSummary {
|
|
76
|
+
reportPath: string;
|
|
77
|
+
generatedAt: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface StructureResult {
|
|
81
|
+
totalFiles: number;
|
|
82
|
+
totalDirs: number;
|
|
83
|
+
structureType: unknown;
|
|
84
|
+
entryPoints: number;
|
|
85
|
+
keyDirectories: string[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface ArchitectureResult {
|
|
89
|
+
patterns: unknown[];
|
|
90
|
+
layers: string[];
|
|
91
|
+
modules: number;
|
|
92
|
+
hasComponentSystem: boolean;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface DependencyResult {
|
|
96
|
+
fileCount: number;
|
|
97
|
+
circularCount: number;
|
|
98
|
+
unusedCount: number;
|
|
99
|
+
externalPackages: number;
|
|
100
|
+
mostImported: unknown[];
|
|
101
|
+
skipped?: boolean | undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface PatternResult {
|
|
105
|
+
designPatterns: number;
|
|
106
|
+
antiPatterns: number;
|
|
107
|
+
codeSmells: number;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface FlowResult {
|
|
111
|
+
entryPoints: number;
|
|
112
|
+
apiEndpoints: number;
|
|
113
|
+
pageRoutes: number;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface ComponentResult {
|
|
117
|
+
total: number;
|
|
118
|
+
categories: number;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface ReportResult {
|
|
122
|
+
reportPath: string;
|
|
123
|
+
reportLength: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface ApiEndpoint {
|
|
127
|
+
path: string;
|
|
128
|
+
file: string;
|
|
129
|
+
methods: string[];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export interface HealthScore {
|
|
133
|
+
overall: number;
|
|
134
|
+
structure: number;
|
|
135
|
+
architecture: number;
|
|
136
|
+
dependencies: number;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface Recommendation {
|
|
140
|
+
title: string;
|
|
141
|
+
description: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface PhaseProgress {
|
|
145
|
+
id: string;
|
|
146
|
+
name: string;
|
|
147
|
+
description: string;
|
|
148
|
+
order: number;
|
|
149
|
+
required: boolean;
|
|
150
|
+
status: PhaseStatus;
|
|
151
|
+
dependenciesMet: boolean;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export interface WorkflowProgress {
|
|
155
|
+
currentPhase: string | null;
|
|
156
|
+
depth: AnalysisDepthLevel;
|
|
157
|
+
startedAt: string | null;
|
|
158
|
+
lastUpdated: string | null;
|
|
159
|
+
phases: PhaseProgress[];
|
|
160
|
+
overall: {
|
|
161
|
+
completed: number;
|
|
162
|
+
total: number;
|
|
163
|
+
percentage: number;
|
|
164
|
+
};
|
|
165
|
+
isComplete: boolean;
|
|
166
|
+
summary: WorkflowSummary | null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export interface ResumePoint {
|
|
170
|
+
phase: string;
|
|
171
|
+
phaseName: string | undefined;
|
|
172
|
+
phaseStatus: PhaseStatus | undefined;
|
|
173
|
+
lastUpdated: string | null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface AnalyzeWorkflowOptions {
|
|
177
|
+
depth?: AnalysisDepthLevel | undefined;
|
|
178
|
+
include?: string | null | undefined;
|
|
179
|
+
exclude?: string | null | undefined;
|
|
180
|
+
[key: string]: unknown;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ============================================================================
|
|
184
|
+
// Constants
|
|
185
|
+
// ============================================================================
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Workflow phase status
|
|
189
|
+
*/
|
|
190
|
+
export const PHASE_STATUS: Record<string, PhaseStatus> = {
|
|
191
|
+
PENDING: 'pending',
|
|
192
|
+
IN_PROGRESS: 'in_progress',
|
|
193
|
+
COMPLETED: 'completed',
|
|
194
|
+
SKIPPED: 'skipped',
|
|
195
|
+
FAILED: 'failed'
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Analysis phases
|
|
200
|
+
*/
|
|
201
|
+
export const ANALYZE_PHASES: Record<string, PhaseDefinition> = {
|
|
202
|
+
structure: {
|
|
203
|
+
name: 'Structure Mapping',
|
|
204
|
+
description: 'Parse files, build tree, detect monorepo',
|
|
205
|
+
order: 1,
|
|
206
|
+
required: true
|
|
207
|
+
},
|
|
208
|
+
architecture: {
|
|
209
|
+
name: 'Architecture Analysis',
|
|
210
|
+
description: 'Identify layers, modules, patterns (MVC, etc.)',
|
|
211
|
+
order: 2,
|
|
212
|
+
required: true,
|
|
213
|
+
dependencies: ['structure']
|
|
214
|
+
},
|
|
215
|
+
dependencies: {
|
|
216
|
+
name: 'Dependency Analysis',
|
|
217
|
+
description: 'Import graph, circular deps, unused deps',
|
|
218
|
+
order: 3,
|
|
219
|
+
required: true,
|
|
220
|
+
dependencies: ['structure']
|
|
221
|
+
},
|
|
222
|
+
patterns: {
|
|
223
|
+
name: 'Pattern Detection',
|
|
224
|
+
description: 'Design patterns, anti-patterns, code smells',
|
|
225
|
+
order: 4,
|
|
226
|
+
required: true,
|
|
227
|
+
dependencies: ['structure', 'architecture']
|
|
228
|
+
},
|
|
229
|
+
flows: {
|
|
230
|
+
name: 'Flow Tracing',
|
|
231
|
+
description: 'Entry points, request flows, data flows',
|
|
232
|
+
order: 5,
|
|
233
|
+
required: false,
|
|
234
|
+
dependencies: ['structure', 'dependencies']
|
|
235
|
+
},
|
|
236
|
+
components: {
|
|
237
|
+
name: 'Component Analysis',
|
|
238
|
+
description: 'Component signatures, prop drilling, coupling',
|
|
239
|
+
order: 6,
|
|
240
|
+
required: false,
|
|
241
|
+
dependencies: ['structure', 'architecture']
|
|
242
|
+
},
|
|
243
|
+
report: {
|
|
244
|
+
name: 'Report Generation',
|
|
245
|
+
description: 'Executive summary, Mermaid diagrams',
|
|
246
|
+
order: 7,
|
|
247
|
+
required: true,
|
|
248
|
+
dependencies: ['structure', 'architecture', 'dependencies', 'patterns']
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Analysis depth levels
|
|
254
|
+
*/
|
|
255
|
+
export const ANALYSIS_DEPTH: Record<AnalysisDepthLevel, DepthConfig> = {
|
|
256
|
+
shallow: {
|
|
257
|
+
phases: ['structure', 'architecture', 'report'],
|
|
258
|
+
description: 'Quick scan for basic understanding'
|
|
259
|
+
},
|
|
260
|
+
standard: {
|
|
261
|
+
phases: ['structure', 'architecture', 'dependencies', 'patterns', 'report'],
|
|
262
|
+
description: 'Standard analysis with patterns'
|
|
263
|
+
},
|
|
264
|
+
deep: {
|
|
265
|
+
phases: ['structure', 'architecture', 'dependencies', 'patterns', 'flows', 'components', 'report'],
|
|
266
|
+
description: 'Comprehensive analysis with flows'
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Default workflow state
|
|
272
|
+
*/
|
|
273
|
+
export const DEFAULT_STATE: AnalyzeWorkflowState = {
|
|
274
|
+
version: '1.0.0',
|
|
275
|
+
startedAt: null,
|
|
276
|
+
lastUpdated: null,
|
|
277
|
+
currentPhase: null,
|
|
278
|
+
depth: 'standard',
|
|
279
|
+
phases: {},
|
|
280
|
+
results: {},
|
|
281
|
+
summary: null
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// ============================================================================
|
|
285
|
+
// AnalyzeWorkflowEngine Class
|
|
286
|
+
// ============================================================================
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* AnalyzeWorkflowEngine - Manages analysis workflow
|
|
290
|
+
*/
|
|
291
|
+
export class AnalyzeWorkflowEngine {
|
|
292
|
+
readonly projectRoot: string;
|
|
293
|
+
readonly workflowDir: string;
|
|
294
|
+
readonly stateFile: string;
|
|
295
|
+
readonly reportsDir: string;
|
|
296
|
+
readonly artifactsDir: string;
|
|
297
|
+
readonly options: AnalyzeWorkflowOptions;
|
|
298
|
+
state: AnalyzeWorkflowState | null;
|
|
299
|
+
|
|
300
|
+
constructor(projectRoot: string, options: AnalyzeWorkflowOptions = {}) {
|
|
301
|
+
this.projectRoot = projectRoot;
|
|
302
|
+
this.workflowDir = path.join(projectRoot, '.bootspring', 'analyze');
|
|
303
|
+
this.stateFile = path.join(this.workflowDir, 'workflow-state.json');
|
|
304
|
+
this.reportsDir = path.join(this.workflowDir, 'reports');
|
|
305
|
+
this.artifactsDir = path.join(this.workflowDir, 'artifacts');
|
|
306
|
+
this.options = {
|
|
307
|
+
depth: options.depth ?? 'standard',
|
|
308
|
+
include: options.include ?? null,
|
|
309
|
+
exclude: options.exclude ?? null,
|
|
310
|
+
...options
|
|
311
|
+
};
|
|
312
|
+
this.state = null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Setup directories
|
|
317
|
+
*/
|
|
318
|
+
setupDirectories(): void {
|
|
319
|
+
const dirs = [this.workflowDir, this.reportsDir, this.artifactsDir];
|
|
320
|
+
|
|
321
|
+
for (const dir of dirs) {
|
|
322
|
+
if (!fs.existsSync(dir)) {
|
|
323
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Load workflow state
|
|
330
|
+
*/
|
|
331
|
+
loadState(): boolean {
|
|
332
|
+
if (fs.existsSync(this.stateFile)) {
|
|
333
|
+
try {
|
|
334
|
+
this.state = JSON.parse(fs.readFileSync(this.stateFile, 'utf-8')) as AnalyzeWorkflowState;
|
|
335
|
+
return true;
|
|
336
|
+
} catch {
|
|
337
|
+
this.state = { ...DEFAULT_STATE };
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
this.state = { ...DEFAULT_STATE };
|
|
342
|
+
return false;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Save workflow state
|
|
347
|
+
*/
|
|
348
|
+
saveState(): void {
|
|
349
|
+
if (!this.state) return;
|
|
350
|
+
this.setupDirectories();
|
|
351
|
+
this.state.lastUpdated = new Date().toISOString();
|
|
352
|
+
fs.writeFileSync(this.stateFile, JSON.stringify(this.state, null, 2));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Initialize workflow
|
|
357
|
+
*/
|
|
358
|
+
initializeWorkflow(depth: AnalysisDepthLevel = 'standard'): AnalyzeWorkflowState {
|
|
359
|
+
this.setupDirectories();
|
|
360
|
+
this.state = {
|
|
361
|
+
...DEFAULT_STATE,
|
|
362
|
+
startedAt: new Date().toISOString(),
|
|
363
|
+
lastUpdated: new Date().toISOString(),
|
|
364
|
+
depth,
|
|
365
|
+
phases: {},
|
|
366
|
+
results: {}
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
// Get phases for this depth
|
|
370
|
+
const depthConfig = ANALYSIS_DEPTH[depth];
|
|
371
|
+
const activePhases = depthConfig?.phases ?? ANALYSIS_DEPTH.standard.phases;
|
|
372
|
+
|
|
373
|
+
// Initialize phase states
|
|
374
|
+
for (const phaseId of Object.keys(ANALYZE_PHASES)) {
|
|
375
|
+
const phaseStatus: PhaseStatus = activePhases.includes(phaseId) ? 'pending' : 'skipped';
|
|
376
|
+
this.state.phases[phaseId] = {
|
|
377
|
+
status: phaseStatus,
|
|
378
|
+
startedAt: null,
|
|
379
|
+
completedAt: null,
|
|
380
|
+
result: null,
|
|
381
|
+
error: null
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
this.state.currentPhase = 'structure';
|
|
386
|
+
this.saveState();
|
|
387
|
+
return this.state;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Check if workflow exists
|
|
392
|
+
*/
|
|
393
|
+
hasWorkflow(): boolean {
|
|
394
|
+
return fs.existsSync(this.stateFile);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Reset workflow
|
|
399
|
+
*/
|
|
400
|
+
resetWorkflow(): boolean {
|
|
401
|
+
if (fs.existsSync(this.stateFile)) {
|
|
402
|
+
fs.unlinkSync(this.stateFile);
|
|
403
|
+
}
|
|
404
|
+
this.state = null;
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Check if phase dependencies are met
|
|
410
|
+
*/
|
|
411
|
+
arePhaseDependenciesMet(phaseId: string): boolean {
|
|
412
|
+
const phase = ANALYZE_PHASES[phaseId];
|
|
413
|
+
if (!phase || !phase.dependencies || phase.dependencies.length === 0) {
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
for (const depPhaseId of phase.dependencies) {
|
|
418
|
+
const depPhase = this.state?.phases[depPhaseId];
|
|
419
|
+
if (!depPhase || (depPhase.status !== 'completed' && depPhase.status !== 'skipped')) {
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return true;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Get next phase
|
|
429
|
+
*/
|
|
430
|
+
getNextPhase(): string | null {
|
|
431
|
+
if (!this.state) return null;
|
|
432
|
+
|
|
433
|
+
const phaseOrder = Object.keys(ANALYZE_PHASES).sort((a, b) => {
|
|
434
|
+
const phaseA = ANALYZE_PHASES[a];
|
|
435
|
+
const phaseB = ANALYZE_PHASES[b];
|
|
436
|
+
return (phaseA?.order ?? 0) - (phaseB?.order ?? 0);
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
for (const phaseId of phaseOrder) {
|
|
440
|
+
const phase = this.state.phases[phaseId];
|
|
441
|
+
if (phase?.status === 'pending' && this.arePhaseDependenciesMet(phaseId)) {
|
|
442
|
+
return phaseId;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Start a phase
|
|
451
|
+
*/
|
|
452
|
+
startPhase(phaseId: string): void {
|
|
453
|
+
if (!this.state) {
|
|
454
|
+
throw new Error('Workflow not initialized');
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const phase = this.state.phases[phaseId];
|
|
458
|
+
if (!phase) {
|
|
459
|
+
throw new Error(`Unknown phase: ${phaseId}`);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
phase.status = 'in_progress';
|
|
463
|
+
phase.startedAt = new Date().toISOString();
|
|
464
|
+
this.state.currentPhase = phaseId;
|
|
465
|
+
this.saveState();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Complete a phase
|
|
470
|
+
*/
|
|
471
|
+
completePhase(phaseId: string, result: unknown = null): void {
|
|
472
|
+
if (!this.state) {
|
|
473
|
+
throw new Error('Workflow not initialized');
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const phase = this.state.phases[phaseId];
|
|
477
|
+
if (!phase) {
|
|
478
|
+
throw new Error(`Unknown phase: ${phaseId}`);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
phase.status = 'completed';
|
|
482
|
+
phase.completedAt = new Date().toISOString();
|
|
483
|
+
phase.result = result;
|
|
484
|
+
this.state.results[phaseId] = result;
|
|
485
|
+
this.saveState();
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Fail a phase
|
|
490
|
+
*/
|
|
491
|
+
failPhase(phaseId: string, error: string): void {
|
|
492
|
+
if (!this.state) {
|
|
493
|
+
throw new Error('Workflow not initialized');
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const phase = this.state.phases[phaseId];
|
|
497
|
+
if (!phase) {
|
|
498
|
+
throw new Error(`Unknown phase: ${phaseId}`);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
phase.status = 'failed';
|
|
502
|
+
phase.completedAt = new Date().toISOString();
|
|
503
|
+
phase.error = error;
|
|
504
|
+
this.saveState();
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Run structure mapping phase
|
|
509
|
+
*/
|
|
510
|
+
async runStructureMapping(): Promise<StructureResult> {
|
|
511
|
+
const analyzer = new StructureAnalyzer(this.projectRoot, {
|
|
512
|
+
includeHidden: false
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
const result = analyzer.analyze();
|
|
516
|
+
|
|
517
|
+
// Save artifact
|
|
518
|
+
fs.writeFileSync(
|
|
519
|
+
path.join(this.artifactsDir, 'structure.json'),
|
|
520
|
+
JSON.stringify(result, null, 2)
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
const keyDirs = result.keyDirectories as Record<string, unknown> | undefined;
|
|
524
|
+
const keyDirectories = keyDirs
|
|
525
|
+
? Object.keys(keyDirs).filter(k => keyDirs[k])
|
|
526
|
+
: [];
|
|
527
|
+
|
|
528
|
+
const stats = result.stats as Record<string, number> | undefined;
|
|
529
|
+
const entryPoints = result.entryPoints as unknown[] | undefined;
|
|
530
|
+
|
|
531
|
+
return {
|
|
532
|
+
totalFiles: stats?.totalFiles ?? 0,
|
|
533
|
+
totalDirs: stats?.totalDirs ?? 0,
|
|
534
|
+
structureType: result.structureType,
|
|
535
|
+
entryPoints: entryPoints?.length ?? 0,
|
|
536
|
+
keyDirectories
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Run architecture analysis phase
|
|
542
|
+
*/
|
|
543
|
+
async runArchitectureAnalysis(): Promise<ArchitectureResult> {
|
|
544
|
+
const analyzer = new ArchitectureAnalyzer(this.projectRoot);
|
|
545
|
+
const result = analyzer.analyze();
|
|
546
|
+
|
|
547
|
+
// Save artifact
|
|
548
|
+
fs.writeFileSync(
|
|
549
|
+
path.join(this.artifactsDir, 'architecture.json'),
|
|
550
|
+
JSON.stringify(result, null, 2)
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
// Save architecture report
|
|
554
|
+
const report = this.generateArchitectureReport(result);
|
|
555
|
+
fs.writeFileSync(
|
|
556
|
+
path.join(this.reportsDir, 'architecture.md'),
|
|
557
|
+
report
|
|
558
|
+
);
|
|
559
|
+
|
|
560
|
+
const patterns = result.patterns as unknown[] | undefined;
|
|
561
|
+
const layers = result.layers as Record<string, unknown> | undefined;
|
|
562
|
+
const modules = result.modules as unknown[] | undefined;
|
|
563
|
+
const componentStructure = result.componentStructure as Record<string, boolean> | undefined;
|
|
564
|
+
|
|
565
|
+
return {
|
|
566
|
+
patterns: (patterns ?? []).slice(0, 3),
|
|
567
|
+
layers: Object.keys(layers ?? {}),
|
|
568
|
+
modules: modules?.length ?? 0,
|
|
569
|
+
hasComponentSystem: componentStructure?.hasComponentDir ?? false
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Generate architecture report
|
|
575
|
+
*/
|
|
576
|
+
generateArchitectureReport(analysis: Record<string, unknown>): string {
|
|
577
|
+
const lines: string[] = [
|
|
578
|
+
'# Architecture Analysis',
|
|
579
|
+
'',
|
|
580
|
+
`Generated: ${new Date().toISOString()}`,
|
|
581
|
+
'',
|
|
582
|
+
'## Detected Patterns',
|
|
583
|
+
''
|
|
584
|
+
];
|
|
585
|
+
|
|
586
|
+
const patterns = analysis.patterns as Array<{
|
|
587
|
+
name: string;
|
|
588
|
+
confidence: number;
|
|
589
|
+
description?: string;
|
|
590
|
+
matchedIndicators: string[];
|
|
591
|
+
}> | undefined;
|
|
592
|
+
|
|
593
|
+
if (patterns) {
|
|
594
|
+
for (const pattern of patterns) {
|
|
595
|
+
lines.push(`### ${pattern.name} (${pattern.confidence}% confidence)`);
|
|
596
|
+
lines.push(`${pattern.description ?? ''}`);
|
|
597
|
+
lines.push(`Indicators: ${pattern.matchedIndicators.join(', ')}`);
|
|
598
|
+
lines.push('');
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
lines.push('## Layer Structure');
|
|
603
|
+
lines.push('');
|
|
604
|
+
|
|
605
|
+
const layers = analysis.layers as Record<string, Array<{ path: string; purpose: string }>> | undefined;
|
|
606
|
+
if (layers) {
|
|
607
|
+
for (const [layer, dirs] of Object.entries(layers)) {
|
|
608
|
+
if (dirs.length > 0) {
|
|
609
|
+
lines.push(`### ${layer.charAt(0).toUpperCase() + layer.slice(1)}`);
|
|
610
|
+
for (const dir of dirs) {
|
|
611
|
+
lines.push(`- \`${dir.path}\`: ${dir.purpose}`);
|
|
612
|
+
}
|
|
613
|
+
lines.push('');
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
const modules = analysis.modules as Array<{ name: string; path: string; type: string }> | undefined;
|
|
619
|
+
if (modules && modules.length > 0) {
|
|
620
|
+
lines.push('## Modules/Features');
|
|
621
|
+
lines.push('');
|
|
622
|
+
for (const mod of modules) {
|
|
623
|
+
lines.push(`- **${mod.name}** (\`${mod.path}\`): ${mod.type}`);
|
|
624
|
+
}
|
|
625
|
+
lines.push('');
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
lines.push('## Architecture Diagram');
|
|
629
|
+
lines.push('');
|
|
630
|
+
lines.push(String(analysis.diagram ?? ''));
|
|
631
|
+
|
|
632
|
+
return lines.join('\n');
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Run dependency analysis phase
|
|
637
|
+
*/
|
|
638
|
+
async runDependencyAnalysis(): Promise<DependencyResult> {
|
|
639
|
+
// Auto-detect large codebase and skip heavy analysis
|
|
640
|
+
const LARGE_CODEBASE_THRESHOLD = 500;
|
|
641
|
+
const structurePath = path.join(this.artifactsDir, 'structure.json');
|
|
642
|
+
let fileCount = 0;
|
|
643
|
+
|
|
644
|
+
if (fs.existsSync(structurePath)) {
|
|
645
|
+
try {
|
|
646
|
+
const structure = JSON.parse(fs.readFileSync(structurePath, 'utf-8')) as Record<string, unknown>;
|
|
647
|
+
const stats = structure.stats as Record<string, number> | undefined;
|
|
648
|
+
fileCount = stats?.totalFiles ?? 0;
|
|
649
|
+
} catch {
|
|
650
|
+
// Ignore
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// For large codebases, return lightweight results
|
|
655
|
+
if (fileCount >= LARGE_CODEBASE_THRESHOLD) {
|
|
656
|
+
const lightResult = {
|
|
657
|
+
fileCount: fileCount,
|
|
658
|
+
circularDependencies: [],
|
|
659
|
+
unusedDependencies: [],
|
|
660
|
+
packageUsage: [],
|
|
661
|
+
mostImported: [],
|
|
662
|
+
skipped: true,
|
|
663
|
+
reason: `Large codebase (${fileCount} files) - dependency analysis skipped for performance`
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
// Save artifact with note
|
|
667
|
+
fs.writeFileSync(
|
|
668
|
+
path.join(this.artifactsDir, 'dependencies.json'),
|
|
669
|
+
JSON.stringify(lightResult, null, 2)
|
|
670
|
+
);
|
|
671
|
+
|
|
672
|
+
// Save lightweight report
|
|
673
|
+
const report = [
|
|
674
|
+
'# Dependency Analysis',
|
|
675
|
+
'',
|
|
676
|
+
`> Skipped: Large codebase (${fileCount} files)`,
|
|
677
|
+
'',
|
|
678
|
+
'Dependency analysis was skipped for performance.',
|
|
679
|
+
'Run `bootspring analyze --depth=deep` to force full analysis.',
|
|
680
|
+
''
|
|
681
|
+
].join('\n');
|
|
682
|
+
|
|
683
|
+
fs.writeFileSync(
|
|
684
|
+
path.join(this.reportsDir, 'dependencies.md'),
|
|
685
|
+
report
|
|
686
|
+
);
|
|
687
|
+
|
|
688
|
+
return {
|
|
689
|
+
fileCount: fileCount,
|
|
690
|
+
circularCount: 0,
|
|
691
|
+
unusedCount: 0,
|
|
692
|
+
externalPackages: 0,
|
|
693
|
+
mostImported: [],
|
|
694
|
+
skipped: true
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const analyzer = new DependencyAnalyzer(this.projectRoot);
|
|
699
|
+
const result = analyzer.analyze();
|
|
700
|
+
|
|
701
|
+
// Save artifact
|
|
702
|
+
fs.writeFileSync(
|
|
703
|
+
path.join(this.artifactsDir, 'dependencies.json'),
|
|
704
|
+
JSON.stringify(result, null, 2)
|
|
705
|
+
);
|
|
706
|
+
|
|
707
|
+
// Save dependency report
|
|
708
|
+
const report = this.generateDependencyReport(result);
|
|
709
|
+
fs.writeFileSync(
|
|
710
|
+
path.join(this.reportsDir, 'dependencies.md'),
|
|
711
|
+
report
|
|
712
|
+
);
|
|
713
|
+
|
|
714
|
+
const circularDependencies = result.circularDependencies as unknown[] | undefined;
|
|
715
|
+
const unusedDependencies = result.unusedDependencies as unknown[] | undefined;
|
|
716
|
+
const packageUsage = result.packageUsage as unknown[] | undefined;
|
|
717
|
+
const mostImported = result.mostImported as unknown[] | undefined;
|
|
718
|
+
|
|
719
|
+
return {
|
|
720
|
+
fileCount: (result.fileCount as number | undefined) ?? 0,
|
|
721
|
+
circularCount: circularDependencies?.length ?? 0,
|
|
722
|
+
unusedCount: unusedDependencies?.length ?? 0,
|
|
723
|
+
externalPackages: packageUsage?.length ?? 0,
|
|
724
|
+
mostImported: (mostImported ?? []).slice(0, 5)
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Generate dependency report
|
|
730
|
+
*/
|
|
731
|
+
generateDependencyReport(analysis: Record<string, unknown>): string {
|
|
732
|
+
const lines: string[] = [
|
|
733
|
+
'# Dependency Analysis',
|
|
734
|
+
'',
|
|
735
|
+
`Generated: ${new Date().toISOString()}`,
|
|
736
|
+
'',
|
|
737
|
+
'## Summary',
|
|
738
|
+
'',
|
|
739
|
+
`- **Total Files Analyzed**: ${analysis.fileCount ?? 0}`,
|
|
740
|
+
`- **Circular Dependencies**: ${(analysis.circularDependencies as unknown[])?.length ?? 0}`,
|
|
741
|
+
`- **Unused Dependencies**: ${(analysis.unusedDependencies as unknown[])?.length ?? 0}`,
|
|
742
|
+
`- **External Packages**: ${(analysis.packageUsage as unknown[])?.length ?? 0}`,
|
|
743
|
+
''
|
|
744
|
+
];
|
|
745
|
+
|
|
746
|
+
const circularDependencies = analysis.circularDependencies as string[][] | undefined;
|
|
747
|
+
if (circularDependencies && circularDependencies.length > 0) {
|
|
748
|
+
lines.push('## Circular Dependencies');
|
|
749
|
+
lines.push('');
|
|
750
|
+
lines.push('> These should be resolved to improve maintainability.');
|
|
751
|
+
lines.push('');
|
|
752
|
+
for (let i = 0; i < Math.min(circularDependencies.length, 5); i++) {
|
|
753
|
+
const cycle = circularDependencies[i];
|
|
754
|
+
if (cycle) {
|
|
755
|
+
lines.push(`${i + 1}. ${cycle.join(' → ')}`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
lines.push('');
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const unusedDependencies = analysis.unusedDependencies as Array<{ name: string; type: string }> | undefined;
|
|
762
|
+
if (unusedDependencies && unusedDependencies.length > 0) {
|
|
763
|
+
lines.push('## Potentially Unused Dependencies');
|
|
764
|
+
lines.push('');
|
|
765
|
+
lines.push('> Consider removing these to reduce bundle size.');
|
|
766
|
+
lines.push('');
|
|
767
|
+
for (const dep of unusedDependencies) {
|
|
768
|
+
lines.push(`- \`${dep.name}\` (${dep.type})`);
|
|
769
|
+
}
|
|
770
|
+
lines.push('');
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
lines.push('## Most Imported Files');
|
|
774
|
+
lines.push('');
|
|
775
|
+
lines.push('| File | Import Count |');
|
|
776
|
+
lines.push('|------|--------------|');
|
|
777
|
+
const mostImported = analysis.mostImported as Array<{ file: string; importCount: number }> | undefined;
|
|
778
|
+
if (mostImported) {
|
|
779
|
+
for (const file of mostImported.slice(0, 10)) {
|
|
780
|
+
lines.push(`| \`${file.file}\` | ${file.importCount} |`);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
lines.push('');
|
|
784
|
+
|
|
785
|
+
lines.push('## Package Usage');
|
|
786
|
+
lines.push('');
|
|
787
|
+
lines.push('| Package | Usage Count |');
|
|
788
|
+
lines.push('|---------|-------------|');
|
|
789
|
+
const packageUsage = analysis.packageUsage as Array<{ package: string; count: number }> | undefined;
|
|
790
|
+
if (packageUsage) {
|
|
791
|
+
for (const pkg of packageUsage.slice(0, 15)) {
|
|
792
|
+
lines.push(`| \`${pkg.package}\` | ${pkg.count} |`);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
lines.push('');
|
|
796
|
+
|
|
797
|
+
lines.push('## Dependency Graph');
|
|
798
|
+
lines.push('');
|
|
799
|
+
lines.push(String(analysis.diagram ?? ''));
|
|
800
|
+
|
|
801
|
+
return lines.join('\n');
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Run pattern detection phase
|
|
806
|
+
*/
|
|
807
|
+
async runPatternDetection(): Promise<PatternResult> {
|
|
808
|
+
// Load previous results
|
|
809
|
+
const architectureArtifact = path.join(this.artifactsDir, 'architecture.json');
|
|
810
|
+
const dependencyArtifact = path.join(this.artifactsDir, 'dependencies.json');
|
|
811
|
+
|
|
812
|
+
const patterns: {
|
|
813
|
+
designPatterns: unknown[];
|
|
814
|
+
antiPatterns: Array<{
|
|
815
|
+
id: string;
|
|
816
|
+
name: string;
|
|
817
|
+
severity: string;
|
|
818
|
+
count: number;
|
|
819
|
+
description: string;
|
|
820
|
+
}>;
|
|
821
|
+
codeSmells: unknown[];
|
|
822
|
+
} = {
|
|
823
|
+
designPatterns: [],
|
|
824
|
+
antiPatterns: [],
|
|
825
|
+
codeSmells: []
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
if (fs.existsSync(architectureArtifact)) {
|
|
829
|
+
const architecture = JSON.parse(fs.readFileSync(architectureArtifact, 'utf-8')) as Record<string, unknown>;
|
|
830
|
+
patterns.designPatterns = (architecture.patterns as unknown[]) ?? [];
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
if (fs.existsSync(dependencyArtifact)) {
|
|
834
|
+
const dependencies = JSON.parse(fs.readFileSync(dependencyArtifact, 'utf-8')) as Record<string, unknown>;
|
|
835
|
+
|
|
836
|
+
// Detect anti-patterns from dependencies
|
|
837
|
+
const circularDeps = dependencies.circularDependencies as unknown[] | undefined;
|
|
838
|
+
if (circularDeps && circularDeps.length > 0) {
|
|
839
|
+
patterns.antiPatterns.push({
|
|
840
|
+
id: 'circular-deps',
|
|
841
|
+
name: 'Circular Dependencies',
|
|
842
|
+
severity: 'medium',
|
|
843
|
+
count: circularDeps.length,
|
|
844
|
+
description: 'Files that depend on each other in a cycle'
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// High coupling detection
|
|
849
|
+
const mostImported = dependencies.mostImported as Array<{ importCount: number }> | undefined;
|
|
850
|
+
const highCoupling = mostImported?.filter(f => f.importCount > 20);
|
|
851
|
+
if (highCoupling && highCoupling.length > 0) {
|
|
852
|
+
patterns.antiPatterns.push({
|
|
853
|
+
id: 'high-coupling',
|
|
854
|
+
name: 'High Coupling',
|
|
855
|
+
severity: 'low',
|
|
856
|
+
count: highCoupling.length,
|
|
857
|
+
description: 'Files with many dependents (potential god objects)'
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// Save artifact
|
|
863
|
+
fs.writeFileSync(
|
|
864
|
+
path.join(this.artifactsDir, 'patterns.json'),
|
|
865
|
+
JSON.stringify(patterns, null, 2)
|
|
866
|
+
);
|
|
867
|
+
|
|
868
|
+
// Save patterns report
|
|
869
|
+
const report = this.generatePatternsReport(patterns);
|
|
870
|
+
fs.writeFileSync(
|
|
871
|
+
path.join(this.reportsDir, 'patterns.md'),
|
|
872
|
+
report
|
|
873
|
+
);
|
|
874
|
+
|
|
875
|
+
return {
|
|
876
|
+
designPatterns: patterns.designPatterns.length,
|
|
877
|
+
antiPatterns: patterns.antiPatterns.length,
|
|
878
|
+
codeSmells: patterns.codeSmells.length
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Generate patterns report
|
|
884
|
+
*/
|
|
885
|
+
generatePatternsReport(patterns: {
|
|
886
|
+
designPatterns: unknown[];
|
|
887
|
+
antiPatterns: Array<{ name: string; severity: string; count: number; description?: string }>;
|
|
888
|
+
codeSmells: unknown[];
|
|
889
|
+
}): string {
|
|
890
|
+
const lines: string[] = [
|
|
891
|
+
'# Pattern Analysis',
|
|
892
|
+
'',
|
|
893
|
+
`Generated: ${new Date().toISOString()}`,
|
|
894
|
+
''
|
|
895
|
+
];
|
|
896
|
+
|
|
897
|
+
if (patterns.designPatterns.length > 0) {
|
|
898
|
+
lines.push('## Design Patterns');
|
|
899
|
+
lines.push('');
|
|
900
|
+
for (const patternRaw of patterns.designPatterns) {
|
|
901
|
+
const pattern = patternRaw as { name: string; confidence: number; description?: string };
|
|
902
|
+
lines.push(`### ${pattern.name}`);
|
|
903
|
+
lines.push(`**Confidence**: ${pattern.confidence}%`);
|
|
904
|
+
lines.push(`${pattern.description ?? ''}`);
|
|
905
|
+
lines.push('');
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
if (patterns.antiPatterns.length > 0) {
|
|
910
|
+
lines.push('## Anti-Patterns Detected');
|
|
911
|
+
lines.push('');
|
|
912
|
+
for (const pattern of patterns.antiPatterns) {
|
|
913
|
+
lines.push(`### ${pattern.name}`);
|
|
914
|
+
lines.push(`**Severity**: ${pattern.severity}`);
|
|
915
|
+
lines.push(`**Count**: ${pattern.count}`);
|
|
916
|
+
lines.push(`${pattern.description ?? ''}`);
|
|
917
|
+
lines.push('');
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
if (patterns.designPatterns.length === 0 && patterns.antiPatterns.length === 0) {
|
|
922
|
+
lines.push('No significant patterns detected.');
|
|
923
|
+
lines.push('');
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
return lines.join('\n');
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Run flow tracing phase
|
|
931
|
+
*/
|
|
932
|
+
async runFlowTracing(): Promise<FlowResult> {
|
|
933
|
+
// Load structure artifact
|
|
934
|
+
const structureArtifact = path.join(this.artifactsDir, 'structure.json');
|
|
935
|
+
|
|
936
|
+
const flows: {
|
|
937
|
+
entryPoints: unknown[];
|
|
938
|
+
apiEndpoints: ApiEndpoint[];
|
|
939
|
+
pageRoutes: unknown[];
|
|
940
|
+
} = {
|
|
941
|
+
entryPoints: [],
|
|
942
|
+
apiEndpoints: [],
|
|
943
|
+
pageRoutes: []
|
|
944
|
+
};
|
|
945
|
+
|
|
946
|
+
if (fs.existsSync(structureArtifact)) {
|
|
947
|
+
const structure = JSON.parse(fs.readFileSync(structureArtifact, 'utf-8')) as Record<string, unknown>;
|
|
948
|
+
flows.entryPoints = (structure.entryPoints as unknown[]) ?? [];
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Detect API routes
|
|
952
|
+
const apiDirs = ['api', 'app/api', 'pages/api', 'src/api'];
|
|
953
|
+
for (const apiDir of apiDirs) {
|
|
954
|
+
const apiPath = path.join(this.projectRoot, apiDir);
|
|
955
|
+
if (fs.existsSync(apiPath)) {
|
|
956
|
+
flows.apiEndpoints = this.findApiEndpoints(apiPath);
|
|
957
|
+
break;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Save artifact
|
|
962
|
+
fs.writeFileSync(
|
|
963
|
+
path.join(this.artifactsDir, 'flows.json'),
|
|
964
|
+
JSON.stringify(flows, null, 2)
|
|
965
|
+
);
|
|
966
|
+
|
|
967
|
+
return {
|
|
968
|
+
entryPoints: flows.entryPoints.length,
|
|
969
|
+
apiEndpoints: flows.apiEndpoints.length,
|
|
970
|
+
pageRoutes: flows.pageRoutes.length
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
/**
|
|
975
|
+
* Find API endpoints in a directory
|
|
976
|
+
*/
|
|
977
|
+
findApiEndpoints(apiDir: string, basePath: string = '/api'): ApiEndpoint[] {
|
|
978
|
+
const endpoints: ApiEndpoint[] = [];
|
|
979
|
+
|
|
980
|
+
const walk = (dir: string, currentPath: string): void => {
|
|
981
|
+
try {
|
|
982
|
+
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
983
|
+
|
|
984
|
+
for (const item of items) {
|
|
985
|
+
const fullPath = path.join(dir, item.name);
|
|
986
|
+
|
|
987
|
+
if (item.isDirectory()) {
|
|
988
|
+
const routePath = item.name.startsWith('[')
|
|
989
|
+
? `${currentPath}/:${item.name.replace(/[[\]]/g, '')}`
|
|
990
|
+
: `${currentPath}/${item.name}`;
|
|
991
|
+
walk(fullPath, routePath);
|
|
992
|
+
} else if (item.isFile() && /\.(js|ts)x?$/.test(item.name)) {
|
|
993
|
+
// Skip non-route files
|
|
994
|
+
if (item.name.startsWith('_')) continue;
|
|
995
|
+
|
|
996
|
+
// Determine route path
|
|
997
|
+
let routePath = currentPath;
|
|
998
|
+
if (item.name !== 'route.js' && item.name !== 'route.ts') {
|
|
999
|
+
const baseName = item.name.replace(/\.(js|ts)x?$/, '');
|
|
1000
|
+
if (baseName !== 'index') {
|
|
1001
|
+
routePath = `${currentPath}/${baseName}`;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// Read file to detect methods
|
|
1006
|
+
try {
|
|
1007
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
1008
|
+
const methods: string[] = [];
|
|
1009
|
+
if (/export\s+(async\s+)?function\s+GET/i.test(content)) methods.push('GET');
|
|
1010
|
+
if (/export\s+(async\s+)?function\s+POST/i.test(content)) methods.push('POST');
|
|
1011
|
+
if (/export\s+(async\s+)?function\s+PUT/i.test(content)) methods.push('PUT');
|
|
1012
|
+
if (/export\s+(async\s+)?function\s+PATCH/i.test(content)) methods.push('PATCH');
|
|
1013
|
+
if (/export\s+(async\s+)?function\s+DELETE/i.test(content)) methods.push('DELETE');
|
|
1014
|
+
|
|
1015
|
+
endpoints.push({
|
|
1016
|
+
path: routePath,
|
|
1017
|
+
file: path.relative(this.projectRoot, fullPath),
|
|
1018
|
+
methods: methods.length > 0 ? methods : ['ALL']
|
|
1019
|
+
});
|
|
1020
|
+
} catch {
|
|
1021
|
+
// Skip unreadable files
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
} catch {
|
|
1026
|
+
// Skip inaccessible directories
|
|
1027
|
+
}
|
|
1028
|
+
};
|
|
1029
|
+
|
|
1030
|
+
walk(apiDir, basePath);
|
|
1031
|
+
return endpoints;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* Run component analysis phase
|
|
1036
|
+
*/
|
|
1037
|
+
async runComponentAnalysis(): Promise<ComponentResult> {
|
|
1038
|
+
// Load architecture artifact
|
|
1039
|
+
const architectureArtifact = path.join(this.artifactsDir, 'architecture.json');
|
|
1040
|
+
|
|
1041
|
+
const components: {
|
|
1042
|
+
total: number;
|
|
1043
|
+
categories: unknown[];
|
|
1044
|
+
propDrilling: unknown[];
|
|
1045
|
+
coupling: unknown[];
|
|
1046
|
+
} = {
|
|
1047
|
+
total: 0,
|
|
1048
|
+
categories: [],
|
|
1049
|
+
propDrilling: [],
|
|
1050
|
+
coupling: []
|
|
1051
|
+
};
|
|
1052
|
+
|
|
1053
|
+
if (fs.existsSync(architectureArtifact)) {
|
|
1054
|
+
const architecture = JSON.parse(fs.readFileSync(architectureArtifact, 'utf-8')) as Record<string, unknown>;
|
|
1055
|
+
const componentStructure = architecture.componentStructure as Record<string, unknown> | undefined;
|
|
1056
|
+
components.total = (componentStructure?.totalComponents as number) ?? 0;
|
|
1057
|
+
components.categories = (componentStructure?.categories as unknown[]) ?? [];
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// Save artifact
|
|
1061
|
+
fs.writeFileSync(
|
|
1062
|
+
path.join(this.artifactsDir, 'components.json'),
|
|
1063
|
+
JSON.stringify(components, null, 2)
|
|
1064
|
+
);
|
|
1065
|
+
|
|
1066
|
+
return {
|
|
1067
|
+
total: components.total,
|
|
1068
|
+
categories: components.categories.length
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
/**
|
|
1073
|
+
* Run report generation phase
|
|
1074
|
+
*/
|
|
1075
|
+
async runReportGeneration(): Promise<ReportResult> {
|
|
1076
|
+
const report = this.generateFullReport();
|
|
1077
|
+
|
|
1078
|
+
// Save main report
|
|
1079
|
+
const reportPath = path.join(this.projectRoot, 'planning', 'CODEBASE_ANALYSIS.md');
|
|
1080
|
+
const planningDir = path.join(this.projectRoot, 'planning');
|
|
1081
|
+
|
|
1082
|
+
if (!fs.existsSync(planningDir)) {
|
|
1083
|
+
fs.mkdirSync(planningDir, { recursive: true });
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
fs.writeFileSync(reportPath, report);
|
|
1087
|
+
|
|
1088
|
+
// Also save in workflow directory
|
|
1089
|
+
fs.writeFileSync(
|
|
1090
|
+
path.join(this.reportsDir, 'CODEBASE_ANALYSIS.md'),
|
|
1091
|
+
report
|
|
1092
|
+
);
|
|
1093
|
+
|
|
1094
|
+
if (this.state) {
|
|
1095
|
+
this.state.summary = {
|
|
1096
|
+
reportPath,
|
|
1097
|
+
generatedAt: new Date().toISOString()
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
return {
|
|
1102
|
+
reportPath,
|
|
1103
|
+
reportLength: report.length
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
/**
|
|
1108
|
+
* Generate full analysis report
|
|
1109
|
+
*/
|
|
1110
|
+
generateFullReport(): string {
|
|
1111
|
+
const lines: string[] = [
|
|
1112
|
+
'# Codebase Analysis Report',
|
|
1113
|
+
'',
|
|
1114
|
+
'**Generated by**: Bootspring Analyze',
|
|
1115
|
+
`**Date**: ${new Date().toISOString().split('T')[0]}`,
|
|
1116
|
+
`**Analysis Depth**: ${this.state?.depth ?? 'standard'}`,
|
|
1117
|
+
''
|
|
1118
|
+
];
|
|
1119
|
+
|
|
1120
|
+
// Executive Summary
|
|
1121
|
+
lines.push('## Executive Summary');
|
|
1122
|
+
lines.push('');
|
|
1123
|
+
|
|
1124
|
+
const structure = this.loadArtifact('structure.json') as Record<string, unknown> | null;
|
|
1125
|
+
const architecture = this.loadArtifact('architecture.json') as Record<string, unknown> | null;
|
|
1126
|
+
const dependencies = this.loadArtifact('dependencies.json') as Record<string, unknown> | null;
|
|
1127
|
+
|
|
1128
|
+
if (structure) {
|
|
1129
|
+
const stats = structure.stats as Record<string, unknown> | undefined;
|
|
1130
|
+
const byCategory = stats?.byCategory as Record<string, number> | undefined;
|
|
1131
|
+
const structureType = structure.structureType as Record<string, unknown> | undefined;
|
|
1132
|
+
lines.push(`- **Total Files**: ${stats?.totalFiles ?? 0}`);
|
|
1133
|
+
lines.push(`- **Source Files**: ${byCategory?.source ?? 0}`);
|
|
1134
|
+
lines.push(`- **Test Files**: ${byCategory?.test ?? 0}`);
|
|
1135
|
+
lines.push(`- **Structure Type**: ${structureType?.pattern ?? 'unknown'}`);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
if (architecture) {
|
|
1139
|
+
const patterns = architecture.patterns as Array<{ name: string }> | undefined;
|
|
1140
|
+
const primaryPattern = patterns?.[0];
|
|
1141
|
+
const modules = architecture.modules as unknown[] | undefined;
|
|
1142
|
+
lines.push(`- **Primary Architecture**: ${primaryPattern?.name ?? 'Not determined'}`);
|
|
1143
|
+
lines.push(`- **Modules**: ${modules?.length ?? 0}`);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
if (dependencies) {
|
|
1147
|
+
const circularDeps = dependencies.circularDependencies as unknown[] | undefined;
|
|
1148
|
+
const packageUsage = dependencies.packageUsage as unknown[] | undefined;
|
|
1149
|
+
lines.push(`- **Circular Dependencies**: ${circularDeps?.length ?? 0}`);
|
|
1150
|
+
lines.push(`- **External Packages**: ${packageUsage?.length ?? 0}`);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
lines.push('');
|
|
1154
|
+
|
|
1155
|
+
// Health Score
|
|
1156
|
+
lines.push('## Health Score');
|
|
1157
|
+
lines.push('');
|
|
1158
|
+
const score = this.calculateHealthScore(structure, architecture, dependencies);
|
|
1159
|
+
lines.push(`**Overall**: ${score.overall}/100`);
|
|
1160
|
+
lines.push('');
|
|
1161
|
+
lines.push('| Category | Score |');
|
|
1162
|
+
lines.push('|----------|-------|');
|
|
1163
|
+
lines.push(`| Structure | ${score.structure}/100 |`);
|
|
1164
|
+
lines.push(`| Architecture | ${score.architecture}/100 |`);
|
|
1165
|
+
lines.push(`| Dependencies | ${score.dependencies}/100 |`);
|
|
1166
|
+
lines.push('');
|
|
1167
|
+
|
|
1168
|
+
// Architecture Overview
|
|
1169
|
+
if (architecture) {
|
|
1170
|
+
lines.push('## Architecture Overview');
|
|
1171
|
+
lines.push('');
|
|
1172
|
+
lines.push(String(architecture.diagram ?? ''));
|
|
1173
|
+
lines.push('');
|
|
1174
|
+
|
|
1175
|
+
const patterns = architecture.patterns as Array<{ name: string; confidence: number; description: string }> | undefined;
|
|
1176
|
+
if (patterns && patterns.length > 0) {
|
|
1177
|
+
lines.push('### Detected Patterns');
|
|
1178
|
+
lines.push('');
|
|
1179
|
+
for (const pattern of patterns.slice(0, 3)) {
|
|
1180
|
+
lines.push(`- **${pattern.name}** (${pattern.confidence}%): ${pattern.description}`);
|
|
1181
|
+
}
|
|
1182
|
+
lines.push('');
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// Entry Points
|
|
1187
|
+
const entryPoints = structure?.entryPoints as Array<{ path: string; type: string }> | undefined;
|
|
1188
|
+
if (entryPoints && entryPoints.length > 0) {
|
|
1189
|
+
lines.push('## Entry Points');
|
|
1190
|
+
lines.push('');
|
|
1191
|
+
lines.push('| Path | Type |');
|
|
1192
|
+
lines.push('|------|------|');
|
|
1193
|
+
for (const entry of entryPoints.slice(0, 10)) {
|
|
1194
|
+
lines.push(`| \`${entry.path}\` | ${entry.type} |`);
|
|
1195
|
+
}
|
|
1196
|
+
lines.push('');
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// Dependencies
|
|
1200
|
+
if (dependencies) {
|
|
1201
|
+
lines.push('## Dependency Overview');
|
|
1202
|
+
lines.push('');
|
|
1203
|
+
|
|
1204
|
+
const circularDeps = dependencies.circularDependencies as string[][] | undefined;
|
|
1205
|
+
if (circularDeps && circularDeps.length > 0) {
|
|
1206
|
+
lines.push('### Circular Dependencies');
|
|
1207
|
+
lines.push('');
|
|
1208
|
+
lines.push('> These create tight coupling and should be resolved.');
|
|
1209
|
+
lines.push('');
|
|
1210
|
+
for (const cycle of circularDeps.slice(0, 5)) {
|
|
1211
|
+
lines.push(`- ${cycle.join(' → ')}`);
|
|
1212
|
+
}
|
|
1213
|
+
lines.push('');
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
const mostImported = dependencies.mostImported as Array<{ file: string; importCount: number }> | undefined;
|
|
1217
|
+
if (mostImported && mostImported.length > 0) {
|
|
1218
|
+
lines.push('### Most Imported Files');
|
|
1219
|
+
lines.push('');
|
|
1220
|
+
lines.push('| File | Imported By |');
|
|
1221
|
+
lines.push('|------|-------------|');
|
|
1222
|
+
for (const file of mostImported.slice(0, 10)) {
|
|
1223
|
+
lines.push(`| \`${file.file}\` | ${file.importCount} files |`);
|
|
1224
|
+
}
|
|
1225
|
+
lines.push('');
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// Recommendations
|
|
1230
|
+
lines.push('## Recommendations');
|
|
1231
|
+
lines.push('');
|
|
1232
|
+
const recommendations = this.generateRecommendations(structure, architecture, dependencies);
|
|
1233
|
+
for (let i = 0; i < recommendations.length; i++) {
|
|
1234
|
+
const rec = recommendations[i];
|
|
1235
|
+
if (rec) {
|
|
1236
|
+
lines.push(`${i + 1}. **${rec.title}**: ${rec.description}`);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
lines.push('');
|
|
1240
|
+
|
|
1241
|
+
lines.push('---');
|
|
1242
|
+
lines.push('');
|
|
1243
|
+
lines.push('*Generated by [Bootspring](https://bootspring.com)*');
|
|
1244
|
+
|
|
1245
|
+
return lines.join('\n');
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
/**
|
|
1249
|
+
* Load artifact file
|
|
1250
|
+
*/
|
|
1251
|
+
loadArtifact(filename: string): unknown | null {
|
|
1252
|
+
const filePath = path.join(this.artifactsDir, filename);
|
|
1253
|
+
if (fs.existsSync(filePath)) {
|
|
1254
|
+
try {
|
|
1255
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
1256
|
+
} catch {
|
|
1257
|
+
return null;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
return null;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* Calculate health score
|
|
1265
|
+
*/
|
|
1266
|
+
calculateHealthScore(
|
|
1267
|
+
structure: Record<string, unknown> | null,
|
|
1268
|
+
architecture: Record<string, unknown> | null,
|
|
1269
|
+
dependencies: Record<string, unknown> | null
|
|
1270
|
+
): HealthScore {
|
|
1271
|
+
let structureScore = 100;
|
|
1272
|
+
let architectureScore = 100;
|
|
1273
|
+
let dependenciesScore = 100;
|
|
1274
|
+
|
|
1275
|
+
// Structure scoring
|
|
1276
|
+
if (structure) {
|
|
1277
|
+
const stats = structure.stats as Record<string, unknown> | undefined;
|
|
1278
|
+
const byCategory = stats?.byCategory as Record<string, number> | undefined;
|
|
1279
|
+
const sourceFiles = byCategory?.source ?? 0;
|
|
1280
|
+
const testFiles = byCategory?.test ?? 0;
|
|
1281
|
+
const testRatio = sourceFiles > 0 ? testFiles / sourceFiles : 0;
|
|
1282
|
+
|
|
1283
|
+
if (testRatio < 0.1) structureScore -= 20;
|
|
1284
|
+
else if (testRatio < 0.3) structureScore -= 10;
|
|
1285
|
+
|
|
1286
|
+
const structureType = structure.structureType as Record<string, boolean> | undefined;
|
|
1287
|
+
if (!structureType?.hasTypescript) structureScore -= 10;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// Architecture scoring
|
|
1291
|
+
if (architecture) {
|
|
1292
|
+
const patterns = architecture.patterns as unknown[] | undefined;
|
|
1293
|
+
const modules = architecture.modules as unknown[] | undefined;
|
|
1294
|
+
if (!patterns || patterns.length === 0) architectureScore -= 20;
|
|
1295
|
+
if (!modules || modules.length === 0) architectureScore -= 10;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
// Dependencies scoring
|
|
1299
|
+
if (dependencies) {
|
|
1300
|
+
const circularDeps = dependencies.circularDependencies as unknown[] | undefined;
|
|
1301
|
+
const circularCount = circularDeps?.length ?? 0;
|
|
1302
|
+
if (circularCount > 10) dependenciesScore -= 30;
|
|
1303
|
+
else if (circularCount > 5) dependenciesScore -= 20;
|
|
1304
|
+
else if (circularCount > 0) dependenciesScore -= 10;
|
|
1305
|
+
|
|
1306
|
+
const unusedDeps = dependencies.unusedDependencies as unknown[] | undefined;
|
|
1307
|
+
const unusedCount = unusedDeps?.length ?? 0;
|
|
1308
|
+
if (unusedCount > 10) dependenciesScore -= 10;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// Ensure scores are within bounds
|
|
1312
|
+
structureScore = Math.max(0, Math.min(100, structureScore));
|
|
1313
|
+
architectureScore = Math.max(0, Math.min(100, architectureScore));
|
|
1314
|
+
dependenciesScore = Math.max(0, Math.min(100, dependenciesScore));
|
|
1315
|
+
|
|
1316
|
+
return {
|
|
1317
|
+
overall: Math.round(
|
|
1318
|
+
structureScore * 0.3 +
|
|
1319
|
+
architectureScore * 0.3 +
|
|
1320
|
+
dependenciesScore * 0.4
|
|
1321
|
+
),
|
|
1322
|
+
structure: structureScore,
|
|
1323
|
+
architecture: architectureScore,
|
|
1324
|
+
dependencies: dependenciesScore
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
/**
|
|
1329
|
+
* Generate recommendations
|
|
1330
|
+
*/
|
|
1331
|
+
generateRecommendations(
|
|
1332
|
+
structure: Record<string, unknown> | null,
|
|
1333
|
+
_architecture: Record<string, unknown> | null,
|
|
1334
|
+
dependencies: Record<string, unknown> | null
|
|
1335
|
+
): Recommendation[] {
|
|
1336
|
+
const recommendations: Recommendation[] = [];
|
|
1337
|
+
|
|
1338
|
+
const circularDeps = dependencies?.circularDependencies as unknown[] | undefined;
|
|
1339
|
+
if (circularDeps && circularDeps.length > 0) {
|
|
1340
|
+
recommendations.push({
|
|
1341
|
+
title: 'Resolve Circular Dependencies',
|
|
1342
|
+
description: `Found ${circularDeps.length} circular dependency chains that increase coupling.`
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
const unusedDeps = dependencies?.unusedDependencies as unknown[] | undefined;
|
|
1347
|
+
if (unusedDeps && unusedDeps.length > 5) {
|
|
1348
|
+
recommendations.push({
|
|
1349
|
+
title: 'Clean Up Unused Dependencies',
|
|
1350
|
+
description: `Found ${unusedDeps.length} potentially unused packages. Consider removing them.`
|
|
1351
|
+
});
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
const structureType = structure?.structureType as Record<string, boolean> | undefined;
|
|
1355
|
+
if (structure && !structureType?.hasTypescript) {
|
|
1356
|
+
recommendations.push({
|
|
1357
|
+
title: 'Consider TypeScript',
|
|
1358
|
+
description: 'TypeScript provides better type safety and developer experience.'
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
const stats = structure?.stats as Record<string, unknown> | undefined;
|
|
1363
|
+
const byCategory = stats?.byCategory as Record<string, number> | undefined;
|
|
1364
|
+
const testFiles = byCategory?.test ?? 0;
|
|
1365
|
+
const sourceFiles = byCategory?.source ?? 1;
|
|
1366
|
+
const testRatio = structure ? testFiles / sourceFiles : 0;
|
|
1367
|
+
if (testRatio < 0.2) {
|
|
1368
|
+
recommendations.push({
|
|
1369
|
+
title: 'Increase Test Coverage',
|
|
1370
|
+
description: 'Current test file ratio is low. Add more tests to improve reliability.'
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
if (recommendations.length === 0) {
|
|
1375
|
+
recommendations.push({
|
|
1376
|
+
title: 'Codebase Looks Good',
|
|
1377
|
+
description: 'No major issues detected. Continue following best practices.'
|
|
1378
|
+
});
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
return recommendations;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
/**
|
|
1385
|
+
* Get workflow progress
|
|
1386
|
+
*/
|
|
1387
|
+
getProgress(): WorkflowProgress | null {
|
|
1388
|
+
if (!this.state) return null;
|
|
1389
|
+
|
|
1390
|
+
const phases: PhaseProgress[] = Object.entries(ANALYZE_PHASES).map(([phaseId, phase]) => {
|
|
1391
|
+
const phaseState = this.state?.phases[phaseId];
|
|
1392
|
+
const status: PhaseStatus = phaseState?.status ?? 'pending';
|
|
1393
|
+
|
|
1394
|
+
return {
|
|
1395
|
+
id: phaseId,
|
|
1396
|
+
name: phase.name,
|
|
1397
|
+
description: phase.description,
|
|
1398
|
+
order: phase.order,
|
|
1399
|
+
required: phase.required,
|
|
1400
|
+
status,
|
|
1401
|
+
dependenciesMet: this.arePhaseDependenciesMet(phaseId)
|
|
1402
|
+
};
|
|
1403
|
+
});
|
|
1404
|
+
|
|
1405
|
+
const completedCount = phases.filter(p => p.status === 'completed').length;
|
|
1406
|
+
const activeCount = phases.filter(p => p.status !== 'skipped').length;
|
|
1407
|
+
|
|
1408
|
+
return {
|
|
1409
|
+
currentPhase: this.state.currentPhase,
|
|
1410
|
+
depth: this.state.depth,
|
|
1411
|
+
startedAt: this.state.startedAt,
|
|
1412
|
+
lastUpdated: this.state.lastUpdated,
|
|
1413
|
+
phases,
|
|
1414
|
+
overall: {
|
|
1415
|
+
completed: completedCount,
|
|
1416
|
+
total: activeCount,
|
|
1417
|
+
percentage: activeCount > 0 ? Math.round((completedCount / activeCount) * 100) : 0
|
|
1418
|
+
},
|
|
1419
|
+
isComplete: completedCount === activeCount,
|
|
1420
|
+
summary: this.state.summary
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
/**
|
|
1425
|
+
* Get resume point
|
|
1426
|
+
*/
|
|
1427
|
+
getResumePoint(): ResumePoint | null {
|
|
1428
|
+
if (!this.state || !this.state.currentPhase) {
|
|
1429
|
+
return null;
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
const phase = ANALYZE_PHASES[this.state.currentPhase];
|
|
1433
|
+
const phaseState = this.state.phases[this.state.currentPhase];
|
|
1434
|
+
|
|
1435
|
+
return {
|
|
1436
|
+
phase: this.state.currentPhase,
|
|
1437
|
+
phaseName: phase?.name,
|
|
1438
|
+
phaseStatus: phaseState?.status,
|
|
1439
|
+
lastUpdated: this.state.lastUpdated
|
|
1440
|
+
};
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// ============================================================================
|
|
1445
|
+
// Factory Function
|
|
1446
|
+
// ============================================================================
|
|
1447
|
+
|
|
1448
|
+
export function createAnalyzeWorkflowEngine(
|
|
1449
|
+
projectRoot: string,
|
|
1450
|
+
options: AnalyzeWorkflowOptions = {}
|
|
1451
|
+
): AnalyzeWorkflowEngine {
|
|
1452
|
+
return new AnalyzeWorkflowEngine(projectRoot, options);
|
|
1453
|
+
}
|