pmp-gywd 3.3.0
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/LICENSE +27 -0
- package/README.md +567 -0
- package/bin/install.js +348 -0
- package/commands/gywd/add-phase.md +207 -0
- package/commands/gywd/anticipate.md +271 -0
- package/commands/gywd/bootstrap.md +336 -0
- package/commands/gywd/challenge.md +344 -0
- package/commands/gywd/check-drift.md +144 -0
- package/commands/gywd/complete-milestone.md +106 -0
- package/commands/gywd/consider-issues.md +202 -0
- package/commands/gywd/context.md +93 -0
- package/commands/gywd/create-roadmap.md +115 -0
- package/commands/gywd/deps.md +169 -0
- package/commands/gywd/digest.md +138 -0
- package/commands/gywd/discuss-milestone.md +47 -0
- package/commands/gywd/discuss-phase.md +60 -0
- package/commands/gywd/execute-plan.md +161 -0
- package/commands/gywd/extract-decisions.md +325 -0
- package/commands/gywd/health.md +150 -0
- package/commands/gywd/help.md +556 -0
- package/commands/gywd/history.md +278 -0
- package/commands/gywd/impact.md +317 -0
- package/commands/gywd/init.md +95 -0
- package/commands/gywd/insert-phase.md +227 -0
- package/commands/gywd/list-phase-assumptions.md +50 -0
- package/commands/gywd/map-codebase.md +84 -0
- package/commands/gywd/memory.md +159 -0
- package/commands/gywd/new-milestone.md +59 -0
- package/commands/gywd/new-project.md +315 -0
- package/commands/gywd/pause-work.md +123 -0
- package/commands/gywd/plan-fix.md +205 -0
- package/commands/gywd/plan-phase.md +93 -0
- package/commands/gywd/preview-plan.md +139 -0
- package/commands/gywd/profile.md +363 -0
- package/commands/gywd/progress.md +317 -0
- package/commands/gywd/remove-phase.md +338 -0
- package/commands/gywd/research-phase.md +91 -0
- package/commands/gywd/resume-work.md +40 -0
- package/commands/gywd/rollback.md +179 -0
- package/commands/gywd/status.md +42 -0
- package/commands/gywd/sync-github.md +234 -0
- package/commands/gywd/verify-work.md +71 -0
- package/commands/gywd/why.md +251 -0
- package/docs/COMMANDS.md +722 -0
- package/docs/CONTRIBUTING.md +342 -0
- package/docs/EXAMPLES.md +535 -0
- package/docs/GETTING-STARTED.md +262 -0
- package/docs/README.md +55 -0
- package/docs/RELEASING.md +159 -0
- package/get-your-work-done/core/agent-patterns.md +331 -0
- package/get-your-work-done/core/architecture.md +334 -0
- package/get-your-work-done/core/context-model-schema.json +154 -0
- package/get-your-work-done/core/decisions-schema.json +193 -0
- package/get-your-work-done/core/learning-state-schema.json +133 -0
- package/get-your-work-done/core/profile-schema.json +257 -0
- package/get-your-work-done/references/adaptive-decomposition.md +175 -0
- package/get-your-work-done/references/checkpoints.md +287 -0
- package/get-your-work-done/references/confidence-scoring.md +169 -0
- package/get-your-work-done/references/continuation-format.md +255 -0
- package/get-your-work-done/references/git-integration.md +254 -0
- package/get-your-work-done/references/plan-format.md +428 -0
- package/get-your-work-done/references/principles.md +157 -0
- package/get-your-work-done/references/questioning.md +162 -0
- package/get-your-work-done/references/research-pitfalls.md +215 -0
- package/get-your-work-done/references/scope-estimation.md +172 -0
- package/get-your-work-done/references/tdd.md +263 -0
- package/get-your-work-done/templates/codebase/architecture.md +255 -0
- package/get-your-work-done/templates/codebase/concerns.md +310 -0
- package/get-your-work-done/templates/codebase/conventions.md +307 -0
- package/get-your-work-done/templates/codebase/integrations.md +280 -0
- package/get-your-work-done/templates/codebase/stack.md +186 -0
- package/get-your-work-done/templates/codebase/structure.md +285 -0
- package/get-your-work-done/templates/codebase/testing.md +480 -0
- package/get-your-work-done/templates/config.json +18 -0
- package/get-your-work-done/templates/context.md +161 -0
- package/get-your-work-done/templates/continue-here.md +78 -0
- package/get-your-work-done/templates/discovery.md +146 -0
- package/get-your-work-done/templates/issues.md +32 -0
- package/get-your-work-done/templates/milestone-archive.md +123 -0
- package/get-your-work-done/templates/milestone-context.md +93 -0
- package/get-your-work-done/templates/milestone.md +115 -0
- package/get-your-work-done/templates/phase-prompt.md +303 -0
- package/get-your-work-done/templates/project.md +184 -0
- package/get-your-work-done/templates/research.md +529 -0
- package/get-your-work-done/templates/roadmap.md +196 -0
- package/get-your-work-done/templates/state.md +210 -0
- package/get-your-work-done/templates/summary.md +273 -0
- package/get-your-work-done/templates/uat-issues.md +143 -0
- package/get-your-work-done/workflows/complete-milestone.md +643 -0
- package/get-your-work-done/workflows/create-milestone.md +416 -0
- package/get-your-work-done/workflows/create-roadmap.md +481 -0
- package/get-your-work-done/workflows/discovery-phase.md +293 -0
- package/get-your-work-done/workflows/discuss-milestone.md +236 -0
- package/get-your-work-done/workflows/discuss-phase.md +247 -0
- package/get-your-work-done/workflows/execute-phase.md +1625 -0
- package/get-your-work-done/workflows/list-phase-assumptions.md +178 -0
- package/get-your-work-done/workflows/map-codebase.md +434 -0
- package/get-your-work-done/workflows/plan-phase.md +488 -0
- package/get-your-work-done/workflows/research-phase.md +436 -0
- package/get-your-work-done/workflows/resume-project.md +287 -0
- package/get-your-work-done/workflows/transition.md +580 -0
- package/get-your-work-done/workflows/verify-work.md +202 -0
- package/lib/automation/dependency-analyzer.js +635 -0
- package/lib/automation/doc-generator.js +643 -0
- package/lib/automation/index.js +42 -0
- package/lib/automation/test-generator.js +628 -0
- package/lib/context/context-analyzer.js +554 -0
- package/lib/context/context-cache.js +426 -0
- package/lib/context/context-predictor.js +622 -0
- package/lib/context/index.js +44 -0
- package/lib/memory/confidence-calibrator.js +484 -0
- package/lib/memory/feedback-collector.js +551 -0
- package/lib/memory/global-memory.js +465 -0
- package/lib/memory/index.js +75 -0
- package/lib/memory/pattern-aggregator.js +487 -0
- package/lib/memory/team-sync.js +501 -0
- package/lib/profile/index.js +24 -0
- package/lib/profile/pattern-learner.js +303 -0
- package/lib/profile/profile-manager.js +445 -0
- package/lib/questioning/index.js +49 -0
- package/lib/questioning/question-engine.js +311 -0
- package/lib/questioning/question-templates.js +315 -0
- package/lib/validators/command-validator.js +188 -0
- package/lib/validators/index.js +29 -0
- package/lib/validators/schema-validator.js +183 -0
- package/package.json +61 -0
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency Analyzer
|
|
3
|
+
*
|
|
4
|
+
* Analyzes project dependencies to identify:
|
|
5
|
+
* - Internal module dependencies
|
|
6
|
+
* - Circular dependencies
|
|
7
|
+
* - Dependency graphs
|
|
8
|
+
* - Independent modules (can be tested/built in parallel)
|
|
9
|
+
* - Coupling metrics
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Import pattern matchers
|
|
17
|
+
*/
|
|
18
|
+
const IMPORT_PATTERNS = {
|
|
19
|
+
esm: /import\s+(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+)?['"]([^'"]+)['"]/g,
|
|
20
|
+
cjs: /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
21
|
+
dynamic: /import\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Dependency types
|
|
26
|
+
*/
|
|
27
|
+
const DEP_TYPES = {
|
|
28
|
+
INTERNAL: 'internal',
|
|
29
|
+
EXTERNAL: 'external',
|
|
30
|
+
BUILTIN: 'builtin',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Node.js built-in modules
|
|
35
|
+
*/
|
|
36
|
+
const BUILTIN_MODULES = new Set([
|
|
37
|
+
'assert', 'buffer', 'child_process', 'cluster', 'console', 'constants',
|
|
38
|
+
'crypto', 'dgram', 'dns', 'domain', 'events', 'fs', 'http', 'https',
|
|
39
|
+
'module', 'net', 'os', 'path', 'perf_hooks', 'process', 'punycode',
|
|
40
|
+
'querystring', 'readline', 'repl', 'stream', 'string_decoder', 'sys',
|
|
41
|
+
'timers', 'tls', 'trace_events', 'tty', 'url', 'util', 'v8', 'vm', 'zlib',
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Dependency Analyzer class
|
|
46
|
+
*/
|
|
47
|
+
class DependencyAnalyzer {
|
|
48
|
+
constructor(options = {}) {
|
|
49
|
+
this.rootDir = options.rootDir || process.cwd();
|
|
50
|
+
this.extensions = options.extensions || ['.js', '.ts', '.jsx', '.tsx', '.mjs', '.cjs'];
|
|
51
|
+
this.excludeDirs = options.excludeDirs || ['node_modules', '.git', 'dist', 'build', 'coverage'];
|
|
52
|
+
this.dependencies = new Map();
|
|
53
|
+
this.reverseDeps = new Map();
|
|
54
|
+
this.fileContents = new Map();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Analyze a directory recursively
|
|
59
|
+
* @param {string} dir - Directory to analyze
|
|
60
|
+
* @returns {object} Analysis results
|
|
61
|
+
*/
|
|
62
|
+
analyze(dir = this.rootDir) {
|
|
63
|
+
this.clear();
|
|
64
|
+
this.scanDirectory(dir);
|
|
65
|
+
this.buildReverseDependencies();
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
files: this.dependencies.size,
|
|
69
|
+
dependencies: this.getDependencyStats(),
|
|
70
|
+
circular: this.findCircularDependencies(),
|
|
71
|
+
independent: this.findIndependentModules(),
|
|
72
|
+
coupling: this.calculateCoupling(),
|
|
73
|
+
layers: this.detectLayers(),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Scan directory for source files
|
|
79
|
+
* @param {string} dir - Directory to scan
|
|
80
|
+
*/
|
|
81
|
+
scanDirectory(dir) {
|
|
82
|
+
let entries;
|
|
83
|
+
try {
|
|
84
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
85
|
+
} catch {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const entry of entries) {
|
|
90
|
+
const fullPath = path.join(dir, entry.name);
|
|
91
|
+
|
|
92
|
+
if (entry.isDirectory()) {
|
|
93
|
+
if (!this.excludeDirs.includes(entry.name)) {
|
|
94
|
+
this.scanDirectory(fullPath);
|
|
95
|
+
}
|
|
96
|
+
} else if (entry.isFile()) {
|
|
97
|
+
const ext = path.extname(entry.name);
|
|
98
|
+
if (this.extensions.includes(ext)) {
|
|
99
|
+
this.analyzeFile(fullPath);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Analyze a single file for dependencies
|
|
107
|
+
* @param {string} filePath - File to analyze
|
|
108
|
+
*/
|
|
109
|
+
analyzeFile(filePath) {
|
|
110
|
+
let content;
|
|
111
|
+
try {
|
|
112
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
113
|
+
} catch {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
this.fileContents.set(filePath, content);
|
|
118
|
+
const deps = this.extractDependencies(content, path.dirname(filePath));
|
|
119
|
+
this.dependencies.set(filePath, deps);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Extract dependencies from file content
|
|
124
|
+
* @param {string} content - File content
|
|
125
|
+
* @param {string} baseDir - Base directory for resolving paths
|
|
126
|
+
* @returns {Array<{path: string, type: string, raw: string}>}
|
|
127
|
+
*/
|
|
128
|
+
extractDependencies(content, baseDir) {
|
|
129
|
+
const deps = [];
|
|
130
|
+
const seen = new Set();
|
|
131
|
+
|
|
132
|
+
for (const [_name, regex] of Object.entries(IMPORT_PATTERNS)) {
|
|
133
|
+
regex.lastIndex = 0;
|
|
134
|
+
let match;
|
|
135
|
+
|
|
136
|
+
while ((match = regex.exec(content)) !== null) {
|
|
137
|
+
const raw = match[1];
|
|
138
|
+
if (seen.has(raw)) continue;
|
|
139
|
+
seen.add(raw);
|
|
140
|
+
|
|
141
|
+
const dep = this.classifyDependency(raw, baseDir);
|
|
142
|
+
deps.push(dep);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return deps;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Classify a dependency as internal, external, or builtin
|
|
151
|
+
* @param {string} importPath - Import path
|
|
152
|
+
* @param {string} baseDir - Base directory
|
|
153
|
+
* @returns {{path: string, type: string, raw: string}}
|
|
154
|
+
*/
|
|
155
|
+
classifyDependency(importPath, baseDir) {
|
|
156
|
+
const raw = importPath;
|
|
157
|
+
|
|
158
|
+
// Check for built-in modules
|
|
159
|
+
if (BUILTIN_MODULES.has(importPath) || importPath.startsWith('node:')) {
|
|
160
|
+
return { path: importPath, type: DEP_TYPES.BUILTIN, raw };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Check for relative imports (internal)
|
|
164
|
+
if (importPath.startsWith('.') || importPath.startsWith('/')) {
|
|
165
|
+
const resolved = this.resolveImport(importPath, baseDir);
|
|
166
|
+
return { path: resolved || importPath, type: DEP_TYPES.INTERNAL, raw };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Everything else is external (npm packages)
|
|
170
|
+
return { path: importPath.split('/')[0], type: DEP_TYPES.EXTERNAL, raw };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Resolve a relative import to absolute path
|
|
175
|
+
* @param {string} importPath - Import path
|
|
176
|
+
* @param {string} baseDir - Base directory
|
|
177
|
+
* @returns {string|null}
|
|
178
|
+
*/
|
|
179
|
+
resolveImport(importPath, baseDir) {
|
|
180
|
+
const resolved = path.resolve(baseDir, importPath);
|
|
181
|
+
|
|
182
|
+
// Try with extensions
|
|
183
|
+
for (const ext of this.extensions) {
|
|
184
|
+
const withExt = resolved + ext;
|
|
185
|
+
if (fs.existsSync(withExt)) {
|
|
186
|
+
return withExt;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Try index files
|
|
191
|
+
for (const ext of this.extensions) {
|
|
192
|
+
const indexPath = path.join(resolved, `index${ext}`);
|
|
193
|
+
if (fs.existsSync(indexPath)) {
|
|
194
|
+
return indexPath;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Try exact path
|
|
199
|
+
if (fs.existsSync(resolved)) {
|
|
200
|
+
return resolved;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Build reverse dependency map
|
|
208
|
+
*/
|
|
209
|
+
buildReverseDependencies() {
|
|
210
|
+
this.reverseDeps.clear();
|
|
211
|
+
|
|
212
|
+
for (const [file, deps] of this.dependencies) {
|
|
213
|
+
for (const dep of deps) {
|
|
214
|
+
if (dep.type === DEP_TYPES.INTERNAL && dep.path) {
|
|
215
|
+
if (!this.reverseDeps.has(dep.path)) {
|
|
216
|
+
this.reverseDeps.set(dep.path, new Set());
|
|
217
|
+
}
|
|
218
|
+
this.reverseDeps.get(dep.path).add(file);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Find circular dependencies
|
|
226
|
+
* @returns {Array<string[]>} Array of circular dependency chains
|
|
227
|
+
*/
|
|
228
|
+
findCircularDependencies() {
|
|
229
|
+
const cycles = [];
|
|
230
|
+
const visited = new Set();
|
|
231
|
+
const recursionStack = new Set();
|
|
232
|
+
|
|
233
|
+
const dfs = (file, chain = []) => {
|
|
234
|
+
if (recursionStack.has(file)) {
|
|
235
|
+
const cycleStart = chain.indexOf(file);
|
|
236
|
+
if (cycleStart !== -1) {
|
|
237
|
+
const cycle = chain.slice(cycleStart);
|
|
238
|
+
cycle.push(file);
|
|
239
|
+
cycles.push(cycle.map(f => this.relativePath(f)));
|
|
240
|
+
}
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (visited.has(file)) return;
|
|
245
|
+
|
|
246
|
+
visited.add(file);
|
|
247
|
+
recursionStack.add(file);
|
|
248
|
+
chain.push(file);
|
|
249
|
+
|
|
250
|
+
const deps = this.dependencies.get(file) || [];
|
|
251
|
+
for (const dep of deps) {
|
|
252
|
+
if (dep.type === DEP_TYPES.INTERNAL && dep.path) {
|
|
253
|
+
dfs(dep.path, [...chain]);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
recursionStack.delete(file);
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
for (const file of this.dependencies.keys()) {
|
|
261
|
+
dfs(file);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Deduplicate cycles
|
|
265
|
+
const uniqueCycles = [];
|
|
266
|
+
const seen = new Set();
|
|
267
|
+
|
|
268
|
+
for (const cycle of cycles) {
|
|
269
|
+
const normalized = this.normalizeCycle(cycle);
|
|
270
|
+
const key = normalized.join('->');
|
|
271
|
+
if (!seen.has(key)) {
|
|
272
|
+
seen.add(key);
|
|
273
|
+
uniqueCycles.push(cycle);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return uniqueCycles;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Normalize a cycle for deduplication
|
|
282
|
+
* @param {string[]} cycle - Cycle to normalize
|
|
283
|
+
* @returns {string[]}
|
|
284
|
+
*/
|
|
285
|
+
normalizeCycle(cycle) {
|
|
286
|
+
if (cycle.length <= 1) return cycle;
|
|
287
|
+
|
|
288
|
+
const withoutLast = cycle.slice(0, -1);
|
|
289
|
+
const minIndex = withoutLast.indexOf(
|
|
290
|
+
withoutLast.reduce((min, curr) => curr < min ? curr : min),
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
return [...withoutLast.slice(minIndex), ...withoutLast.slice(0, minIndex)];
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Find independent modules (no internal dependencies)
|
|
298
|
+
* @returns {string[]}
|
|
299
|
+
*/
|
|
300
|
+
findIndependentModules() {
|
|
301
|
+
const independent = [];
|
|
302
|
+
|
|
303
|
+
for (const [file, deps] of this.dependencies) {
|
|
304
|
+
const internalDeps = deps.filter(d => d.type === DEP_TYPES.INTERNAL);
|
|
305
|
+
if (internalDeps.length === 0) {
|
|
306
|
+
independent.push(this.relativePath(file));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return independent;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Calculate coupling metrics
|
|
315
|
+
* @returns {object}
|
|
316
|
+
*/
|
|
317
|
+
calculateCoupling() {
|
|
318
|
+
const metrics = {
|
|
319
|
+
afferentCoupling: new Map(),
|
|
320
|
+
efferentCoupling: new Map(),
|
|
321
|
+
instability: new Map(),
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
for (const [file, deps] of this.dependencies) {
|
|
325
|
+
const relPath = this.relativePath(file);
|
|
326
|
+
const internalDeps = deps.filter(d => d.type === DEP_TYPES.INTERNAL);
|
|
327
|
+
|
|
328
|
+
// Efferent coupling (outgoing dependencies)
|
|
329
|
+
metrics.efferentCoupling.set(relPath, internalDeps.length);
|
|
330
|
+
|
|
331
|
+
// Afferent coupling (incoming dependencies)
|
|
332
|
+
const incoming = this.reverseDeps.get(file)?.size || 0;
|
|
333
|
+
metrics.afferentCoupling.set(relPath, incoming);
|
|
334
|
+
|
|
335
|
+
// Instability = Ce / (Ca + Ce)
|
|
336
|
+
const ce = internalDeps.length;
|
|
337
|
+
const ca = incoming;
|
|
338
|
+
const instability = (ca + ce) > 0 ? ce / (ca + ce) : 0;
|
|
339
|
+
metrics.instability.set(relPath, Math.round(instability * 100) / 100);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
afferent: Object.fromEntries(metrics.afferentCoupling),
|
|
344
|
+
efferent: Object.fromEntries(metrics.efferentCoupling),
|
|
345
|
+
instability: Object.fromEntries(metrics.instability),
|
|
346
|
+
averageInstability: this.average([...metrics.instability.values()]),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Detect architectural layers based on directory structure
|
|
352
|
+
* @returns {object}
|
|
353
|
+
*/
|
|
354
|
+
detectLayers() {
|
|
355
|
+
const layers = new Map();
|
|
356
|
+
|
|
357
|
+
for (const file of this.dependencies.keys()) {
|
|
358
|
+
const relPath = this.relativePath(file);
|
|
359
|
+
const parts = relPath.split(path.sep);
|
|
360
|
+
|
|
361
|
+
// Use first directory as layer
|
|
362
|
+
const layer = parts.length > 1 ? parts[0] : 'root';
|
|
363
|
+
|
|
364
|
+
if (!layers.has(layer)) {
|
|
365
|
+
layers.set(layer, { files: [], dependencies: new Set() });
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
layers.get(layer).files.push(relPath);
|
|
369
|
+
|
|
370
|
+
// Track cross-layer dependencies
|
|
371
|
+
const deps = this.dependencies.get(file) || [];
|
|
372
|
+
for (const dep of deps) {
|
|
373
|
+
if (dep.type === DEP_TYPES.INTERNAL && dep.path) {
|
|
374
|
+
const depRel = this.relativePath(dep.path);
|
|
375
|
+
const depParts = depRel.split(path.sep);
|
|
376
|
+
const depLayer = depParts.length > 1 ? depParts[0] : 'root';
|
|
377
|
+
|
|
378
|
+
if (depLayer !== layer) {
|
|
379
|
+
layers.get(layer).dependencies.add(depLayer);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const result = {};
|
|
386
|
+
for (const [layer, data] of layers) {
|
|
387
|
+
result[layer] = {
|
|
388
|
+
fileCount: data.files.length,
|
|
389
|
+
files: data.files,
|
|
390
|
+
dependsOn: Array.from(data.dependencies),
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return result;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Get dependency statistics
|
|
399
|
+
* @returns {object}
|
|
400
|
+
*/
|
|
401
|
+
getDependencyStats() {
|
|
402
|
+
let internal = 0;
|
|
403
|
+
let external = 0;
|
|
404
|
+
let builtin = 0;
|
|
405
|
+
const externalPackages = new Set();
|
|
406
|
+
|
|
407
|
+
for (const deps of this.dependencies.values()) {
|
|
408
|
+
for (const dep of deps) {
|
|
409
|
+
switch (dep.type) {
|
|
410
|
+
case DEP_TYPES.INTERNAL:
|
|
411
|
+
internal++;
|
|
412
|
+
break;
|
|
413
|
+
case DEP_TYPES.EXTERNAL:
|
|
414
|
+
external++;
|
|
415
|
+
externalPackages.add(dep.path);
|
|
416
|
+
break;
|
|
417
|
+
case DEP_TYPES.BUILTIN:
|
|
418
|
+
builtin++;
|
|
419
|
+
break;
|
|
420
|
+
default:
|
|
421
|
+
// Unknown type, skip
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return {
|
|
428
|
+
internal,
|
|
429
|
+
external,
|
|
430
|
+
builtin,
|
|
431
|
+
total: internal + external + builtin,
|
|
432
|
+
externalPackages: Array.from(externalPackages).sort(),
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Get files that depend on a given file
|
|
438
|
+
* @param {string} filePath - File to check
|
|
439
|
+
* @returns {string[]}
|
|
440
|
+
*/
|
|
441
|
+
getDependents(filePath) {
|
|
442
|
+
const absPath = path.isAbsolute(filePath)
|
|
443
|
+
? filePath
|
|
444
|
+
: path.resolve(this.rootDir, filePath);
|
|
445
|
+
|
|
446
|
+
const dependents = this.reverseDeps.get(absPath);
|
|
447
|
+
return dependents
|
|
448
|
+
? Array.from(dependents).map(f => this.relativePath(f))
|
|
449
|
+
: [];
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Get files that a given file depends on
|
|
454
|
+
* @param {string} filePath - File to check
|
|
455
|
+
* @returns {string[]}
|
|
456
|
+
*/
|
|
457
|
+
getDependencies(filePath) {
|
|
458
|
+
const absPath = path.isAbsolute(filePath)
|
|
459
|
+
? filePath
|
|
460
|
+
: path.resolve(this.rootDir, filePath);
|
|
461
|
+
|
|
462
|
+
const deps = this.dependencies.get(absPath) || [];
|
|
463
|
+
return deps
|
|
464
|
+
.filter(d => d.type === DEP_TYPES.INTERNAL)
|
|
465
|
+
.map(d => this.relativePath(d.path))
|
|
466
|
+
.filter(Boolean);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Get topological sort order for building/testing
|
|
471
|
+
* @returns {string[]}
|
|
472
|
+
*/
|
|
473
|
+
getTopologicalOrder() {
|
|
474
|
+
const order = [];
|
|
475
|
+
const visited = new Set();
|
|
476
|
+
const temp = new Set();
|
|
477
|
+
|
|
478
|
+
const visit = (file) => {
|
|
479
|
+
if (temp.has(file)) return; // Circular dependency
|
|
480
|
+
if (visited.has(file)) return;
|
|
481
|
+
|
|
482
|
+
temp.add(file);
|
|
483
|
+
|
|
484
|
+
const deps = this.dependencies.get(file) || [];
|
|
485
|
+
for (const dep of deps) {
|
|
486
|
+
if (dep.type === DEP_TYPES.INTERNAL && dep.path && this.dependencies.has(dep.path)) {
|
|
487
|
+
visit(dep.path);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
temp.delete(file);
|
|
492
|
+
visited.add(file);
|
|
493
|
+
order.push(this.relativePath(file));
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
for (const file of this.dependencies.keys()) {
|
|
497
|
+
visit(file);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return order;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Generate dependency graph in DOT format
|
|
505
|
+
* @returns {string}
|
|
506
|
+
*/
|
|
507
|
+
toDot() {
|
|
508
|
+
const lines = ['digraph Dependencies {', ' rankdir=LR;', ' node [shape=box];'];
|
|
509
|
+
|
|
510
|
+
for (const [file, deps] of this.dependencies) {
|
|
511
|
+
const from = this.relativePath(file).replace(/[/\\]/g, '_').replace(/\./g, '_');
|
|
512
|
+
|
|
513
|
+
for (const dep of deps) {
|
|
514
|
+
if (dep.type === DEP_TYPES.INTERNAL && dep.path) {
|
|
515
|
+
const to = this.relativePath(dep.path).replace(/[/\\]/g, '_').replace(/\./g, '_');
|
|
516
|
+
lines.push(` ${from} -> ${to};`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
lines.push('}');
|
|
522
|
+
return lines.join('\n');
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Generate dependency report as markdown
|
|
527
|
+
* @returns {string}
|
|
528
|
+
*/
|
|
529
|
+
toMarkdown() {
|
|
530
|
+
const analysis = this.analyze();
|
|
531
|
+
const lines = [
|
|
532
|
+
'# Dependency Analysis Report',
|
|
533
|
+
'',
|
|
534
|
+
`Generated: ${new Date().toISOString()}`,
|
|
535
|
+
'',
|
|
536
|
+
'## Summary',
|
|
537
|
+
'',
|
|
538
|
+
`- **Total Files**: ${analysis.files}`,
|
|
539
|
+
`- **Internal Dependencies**: ${analysis.dependencies.internal}`,
|
|
540
|
+
`- **External Dependencies**: ${analysis.dependencies.external}`,
|
|
541
|
+
`- **Built-in Dependencies**: ${analysis.dependencies.builtin}`,
|
|
542
|
+
`- **Average Instability**: ${analysis.coupling.averageInstability.toFixed(2)}`,
|
|
543
|
+
'',
|
|
544
|
+
];
|
|
545
|
+
|
|
546
|
+
if (analysis.circular.length > 0) {
|
|
547
|
+
lines.push('## Circular Dependencies ⚠️', '');
|
|
548
|
+
for (const cycle of analysis.circular) {
|
|
549
|
+
lines.push(`- ${cycle.join(' → ')}`);
|
|
550
|
+
}
|
|
551
|
+
lines.push('');
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (analysis.dependencies.externalPackages.length > 0) {
|
|
555
|
+
lines.push('## External Packages', '');
|
|
556
|
+
for (const pkg of analysis.dependencies.externalPackages) {
|
|
557
|
+
lines.push(`- ${pkg}`);
|
|
558
|
+
}
|
|
559
|
+
lines.push('');
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
lines.push('## Layers', '');
|
|
563
|
+
for (const [layer, data] of Object.entries(analysis.layers)) {
|
|
564
|
+
lines.push(`### ${layer}`, '');
|
|
565
|
+
lines.push(`- Files: ${data.fileCount}`);
|
|
566
|
+
if (data.dependsOn.length > 0) {
|
|
567
|
+
lines.push(`- Depends on: ${data.dependsOn.join(', ')}`);
|
|
568
|
+
}
|
|
569
|
+
lines.push('');
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (analysis.independent.length > 0) {
|
|
573
|
+
lines.push('## Independent Modules', '');
|
|
574
|
+
lines.push('These modules have no internal dependencies and can be tested in isolation:', '');
|
|
575
|
+
for (const mod of analysis.independent) {
|
|
576
|
+
lines.push(`- ${mod}`);
|
|
577
|
+
}
|
|
578
|
+
lines.push('');
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return lines.join('\n');
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Get relative path from root
|
|
586
|
+
* @param {string} absPath - Absolute path
|
|
587
|
+
* @returns {string}
|
|
588
|
+
*/
|
|
589
|
+
relativePath(absPath) {
|
|
590
|
+
if (!absPath) return '';
|
|
591
|
+
return path.relative(this.rootDir, absPath);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* Calculate average of array
|
|
596
|
+
* @param {number[]} arr - Array of numbers
|
|
597
|
+
* @returns {number}
|
|
598
|
+
*/
|
|
599
|
+
average(arr) {
|
|
600
|
+
if (arr.length === 0) return 0;
|
|
601
|
+
return arr.reduce((a, b) => a + b, 0) / arr.length;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Clear all data
|
|
606
|
+
*/
|
|
607
|
+
clear() {
|
|
608
|
+
this.dependencies.clear();
|
|
609
|
+
this.reverseDeps.clear();
|
|
610
|
+
this.fileContents.clear();
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Export analysis data
|
|
615
|
+
* @returns {object}
|
|
616
|
+
*/
|
|
617
|
+
export() {
|
|
618
|
+
const deps = {};
|
|
619
|
+
for (const [file, fileDeps] of this.dependencies) {
|
|
620
|
+
deps[this.relativePath(file)] = fileDeps;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
return {
|
|
624
|
+
rootDir: this.rootDir,
|
|
625
|
+
dependencies: deps,
|
|
626
|
+
analysis: this.analyze(),
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
module.exports = {
|
|
632
|
+
DependencyAnalyzer,
|
|
633
|
+
DEP_TYPES,
|
|
634
|
+
BUILTIN_MODULES,
|
|
635
|
+
};
|