@ai-sdlc/orchestrator 0.1.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/dist/adapters.d.ts +60 -0
- package/dist/adapters.js +160 -0
- package/dist/admission.d.ts +49 -0
- package/dist/admission.js +90 -0
- package/dist/analysis/analyzer.d.ts +10 -0
- package/dist/analysis/analyzer.js +67 -0
- package/dist/analysis/complexity-scorer.d.ts +24 -0
- package/dist/analysis/complexity-scorer.js +39 -0
- package/dist/analysis/context-builder.d.ts +14 -0
- package/dist/analysis/context-builder.js +79 -0
- package/dist/analysis/convention-detector.d.ts +9 -0
- package/dist/analysis/convention-detector.js +177 -0
- package/dist/analysis/dependency-parser.d.ts +17 -0
- package/dist/analysis/dependency-parser.js +167 -0
- package/dist/analysis/file-walker.d.ts +18 -0
- package/dist/analysis/file-walker.js +158 -0
- package/dist/analysis/hotspot-analyzer.d.ts +28 -0
- package/dist/analysis/hotspot-analyzer.js +85 -0
- package/dist/analysis/index.d.ts +13 -0
- package/dist/analysis/index.js +12 -0
- package/dist/analysis/pattern-detector.d.ts +10 -0
- package/dist/analysis/pattern-detector.js +86 -0
- package/dist/analysis/types.d.ts +94 -0
- package/dist/analysis/types.js +5 -0
- package/dist/audit-archival.d.ts +48 -0
- package/dist/audit-archival.js +72 -0
- package/dist/audit-export.d.ts +45 -0
- package/dist/audit-export.js +100 -0
- package/dist/audit-extended.d.ts +28 -0
- package/dist/audit-extended.js +38 -0
- package/dist/audit-scheduler.d.ts +32 -0
- package/dist/audit-scheduler.js +52 -0
- package/dist/audit-sqlite-sink.d.ts +11 -0
- package/dist/audit-sqlite-sink.js +51 -0
- package/dist/autonomy-tracker.d.ts +76 -0
- package/dist/autonomy-tracker.js +277 -0
- package/dist/check-runs.d.ts +37 -0
- package/dist/check-runs.js +72 -0
- package/dist/cli/commands/agents.d.ts +6 -0
- package/dist/cli/commands/agents.js +33 -0
- package/dist/cli/commands/complexity.d.ts +6 -0
- package/dist/cli/commands/complexity.js +35 -0
- package/dist/cli/commands/cost.d.ts +6 -0
- package/dist/cli/commands/cost.js +53 -0
- package/dist/cli/commands/dashboard.d.ts +9 -0
- package/dist/cli/commands/dashboard.js +50 -0
- package/dist/cli/commands/health.d.ts +6 -0
- package/dist/cli/commands/health.js +33 -0
- package/dist/cli/commands/init.d.ts +6 -0
- package/dist/cli/commands/init.js +127 -0
- package/dist/cli/commands/routing.d.ts +6 -0
- package/dist/cli/commands/routing.js +46 -0
- package/dist/cli/commands/run.d.ts +6 -0
- package/dist/cli/commands/run.js +36 -0
- package/dist/cli/commands/start.d.ts +6 -0
- package/dist/cli/commands/start.js +26 -0
- package/dist/cli/commands/status.d.ts +6 -0
- package/dist/cli/commands/status.js +34 -0
- package/dist/cli/dashboard-renderer.d.ts +24 -0
- package/dist/cli/dashboard-renderer.js +129 -0
- package/dist/cli/formatters/index.d.ts +6 -0
- package/dist/cli/formatters/index.js +18 -0
- package/dist/cli/formatters/json.d.ts +5 -0
- package/dist/cli/formatters/json.js +7 -0
- package/dist/cli/formatters/minimal.d.ts +5 -0
- package/dist/cli/formatters/minimal.js +36 -0
- package/dist/cli/formatters/table.d.ts +5 -0
- package/dist/cli/formatters/table.js +184 -0
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.js +35 -0
- package/dist/compliance-extended.d.ts +41 -0
- package/dist/compliance-extended.js +58 -0
- package/dist/config.d.ts +26 -0
- package/dist/config.js +66 -0
- package/dist/context-enrichment.d.ts +33 -0
- package/dist/context-enrichment.js +125 -0
- package/dist/cost-governance.d.ts +22 -0
- package/dist/cost-governance.js +106 -0
- package/dist/cost-tracker.d.ts +75 -0
- package/dist/cost-tracker.js +177 -0
- package/dist/defaults.d.ts +140 -0
- package/dist/defaults.js +218 -0
- package/dist/deploy/flyio-target.d.ts +19 -0
- package/dist/deploy/flyio-target.js +114 -0
- package/dist/deploy/index.d.ts +15 -0
- package/dist/deploy/index.js +11 -0
- package/dist/deploy/kubernetes-target.d.ts +25 -0
- package/dist/deploy/kubernetes-target.js +124 -0
- package/dist/deploy/metrics-collector.d.ts +31 -0
- package/dist/deploy/metrics-collector.js +75 -0
- package/dist/deploy/rollout-controller.d.ts +42 -0
- package/dist/deploy/rollout-controller.js +192 -0
- package/dist/deploy/rollout-types.d.ts +86 -0
- package/dist/deploy/rollout-types.js +5 -0
- package/dist/deploy/types.d.ts +47 -0
- package/dist/deploy/types.js +5 -0
- package/dist/deploy/vercel-target.d.ts +16 -0
- package/dist/deploy/vercel-target.js +115 -0
- package/dist/discovery.d.ts +26 -0
- package/dist/discovery.js +46 -0
- package/dist/episodic-enhanced.d.ts +88 -0
- package/dist/episodic-enhanced.js +212 -0
- package/dist/execute.d.ts +81 -0
- package/dist/execute.js +703 -0
- package/dist/fix-ci.d.ts +61 -0
- package/dist/fix-ci.js +380 -0
- package/dist/handoff-executor.d.ts +61 -0
- package/dist/handoff-executor.js +119 -0
- package/dist/index.d.ts +70 -0
- package/dist/index.js +87 -0
- package/dist/instrumented.d.ts +27 -0
- package/dist/instrumented.js +34 -0
- package/dist/logger.d.ts +17 -0
- package/dist/logger.js +37 -0
- package/dist/multi-repo/impact-analyzer.d.ts +22 -0
- package/dist/multi-repo/impact-analyzer.js +95 -0
- package/dist/multi-repo/index.d.ts +5 -0
- package/dist/multi-repo/index.js +4 -0
- package/dist/multi-repo/monorepo-detector.d.ts +16 -0
- package/dist/multi-repo/monorepo-detector.js +246 -0
- package/dist/multi-repo/service-map-builder.d.ts +24 -0
- package/dist/multi-repo/service-map-builder.js +235 -0
- package/dist/multi-repo/types.d.ts +54 -0
- package/dist/multi-repo/types.js +5 -0
- package/dist/notifications/index.d.ts +4 -0
- package/dist/notifications/index.js +4 -0
- package/dist/notifications/notification-router.d.ts +62 -0
- package/dist/notifications/notification-router.js +206 -0
- package/dist/notifications/slack-messenger.d.ts +26 -0
- package/dist/notifications/slack-messenger.js +69 -0
- package/dist/notifications/teams-messenger.d.ts +21 -0
- package/dist/notifications/teams-messenger.js +85 -0
- package/dist/notifications.d.ts +13 -0
- package/dist/notifications.js +15 -0
- package/dist/orchestration.d.ts +20 -0
- package/dist/orchestration.js +53 -0
- package/dist/orchestrator.d.ts +160 -0
- package/dist/orchestrator.js +372 -0
- package/dist/otel-exporter.d.ts +37 -0
- package/dist/otel-exporter.js +128 -0
- package/dist/plugin.d.ts +45 -0
- package/dist/plugin.js +8 -0
- package/dist/policy-evaluators.d.ts +40 -0
- package/dist/policy-evaluators.js +57 -0
- package/dist/process-escalation.d.ts +38 -0
- package/dist/process-escalation.js +129 -0
- package/dist/progressive-gates.d.ts +54 -0
- package/dist/progressive-gates.js +135 -0
- package/dist/provenance.d.ts +29 -0
- package/dist/provenance.js +61 -0
- package/dist/reconcilers.d.ts +28 -0
- package/dist/reconcilers.js +37 -0
- package/dist/runners/claude-code.d.ts +17 -0
- package/dist/runners/claude-code.js +200 -0
- package/dist/runners/codex.d.ts +19 -0
- package/dist/runners/codex.js +153 -0
- package/dist/runners/copilot-stub.d.ts +9 -0
- package/dist/runners/copilot-stub.js +15 -0
- package/dist/runners/copilot.d.ts +15 -0
- package/dist/runners/copilot.js +119 -0
- package/dist/runners/cursor-stub.d.ts +9 -0
- package/dist/runners/cursor-stub.js +15 -0
- package/dist/runners/cursor.d.ts +20 -0
- package/dist/runners/cursor.js +140 -0
- package/dist/runners/devin-stub.d.ts +9 -0
- package/dist/runners/devin-stub.js +15 -0
- package/dist/runners/generic-llm.d.ts +51 -0
- package/dist/runners/generic-llm.js +125 -0
- package/dist/runners/index.d.ts +8 -0
- package/dist/runners/index.js +7 -0
- package/dist/runners/runner-registry.d.ts +50 -0
- package/dist/runners/runner-registry.js +156 -0
- package/dist/runners/types.d.ts +59 -0
- package/dist/runners/types.js +7 -0
- package/dist/security.d.ts +51 -0
- package/dist/security.js +70 -0
- package/dist/shared.d.ts +134 -0
- package/dist/shared.js +284 -0
- package/dist/state/index.d.ts +4 -0
- package/dist/state/index.js +3 -0
- package/dist/state/schema.d.ts +16 -0
- package/dist/state/schema.js +264 -0
- package/dist/state/store.d.ts +109 -0
- package/dist/state/store.js +644 -0
- package/dist/state/types.d.ts +190 -0
- package/dist/state/types.js +5 -0
- package/dist/structured-logger.d.ts +20 -0
- package/dist/structured-logger.js +64 -0
- package/dist/task-decomposer.d.ts +61 -0
- package/dist/task-decomposer.js +265 -0
- package/dist/telemetry-extended.d.ts +24 -0
- package/dist/telemetry-extended.js +31 -0
- package/dist/types.d.ts +9 -0
- package/dist/types.js +7 -0
- package/dist/validate-agent-output.d.ts +30 -0
- package/dist/validate-agent-output.js +81 -0
- package/dist/validate-issue.d.ts +25 -0
- package/dist/validate-issue.js +102 -0
- package/dist/watch.d.ts +43 -0
- package/dist/watch.js +99 -0
- package/dist/webhook-manager.d.ts +38 -0
- package/dist/webhook-manager.js +66 -0
- package/package.json +59 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detect coding conventions: naming style, test organization, import style.
|
|
3
|
+
*/
|
|
4
|
+
import { basename, dirname, extname } from 'node:path';
|
|
5
|
+
// ── Naming style helpers ──────────────────────────────────────────
|
|
6
|
+
function isCamelCase(name) {
|
|
7
|
+
return /^[a-z][a-zA-Z0-9]*$/.test(name);
|
|
8
|
+
}
|
|
9
|
+
function isPascalCase(name) {
|
|
10
|
+
return /^[A-Z][a-zA-Z0-9]*$/.test(name);
|
|
11
|
+
}
|
|
12
|
+
function isKebabCase(name) {
|
|
13
|
+
return /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/.test(name);
|
|
14
|
+
}
|
|
15
|
+
function isSnakeCase(name) {
|
|
16
|
+
return /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/.test(name);
|
|
17
|
+
}
|
|
18
|
+
function detectFileNamingStyle(files) {
|
|
19
|
+
const counts = {
|
|
20
|
+
camelCase: 0,
|
|
21
|
+
PascalCase: 0,
|
|
22
|
+
'kebab-case': 0,
|
|
23
|
+
snake_case: 0,
|
|
24
|
+
mixed: 0,
|
|
25
|
+
};
|
|
26
|
+
const examples = {
|
|
27
|
+
camelCase: [],
|
|
28
|
+
PascalCase: [],
|
|
29
|
+
'kebab-case': [],
|
|
30
|
+
snake_case: [],
|
|
31
|
+
mixed: [],
|
|
32
|
+
};
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
const name = basename(file.relativePath, extname(file.relativePath)).replace(/\.(test|spec|stories|d)$/, ''); // Strip test/spec suffixes
|
|
35
|
+
if (!name || name === 'index')
|
|
36
|
+
continue;
|
|
37
|
+
if (isKebabCase(name)) {
|
|
38
|
+
counts['kebab-case']++;
|
|
39
|
+
if (examples['kebab-case'].length < 3)
|
|
40
|
+
examples['kebab-case'].push(file.relativePath);
|
|
41
|
+
}
|
|
42
|
+
else if (isSnakeCase(name)) {
|
|
43
|
+
counts.snake_case++;
|
|
44
|
+
if (examples.snake_case.length < 3)
|
|
45
|
+
examples.snake_case.push(file.relativePath);
|
|
46
|
+
}
|
|
47
|
+
else if (isCamelCase(name)) {
|
|
48
|
+
counts.camelCase++;
|
|
49
|
+
if (examples.camelCase.length < 3)
|
|
50
|
+
examples.camelCase.push(file.relativePath);
|
|
51
|
+
}
|
|
52
|
+
else if (isPascalCase(name)) {
|
|
53
|
+
counts.PascalCase++;
|
|
54
|
+
if (examples.PascalCase.length < 3)
|
|
55
|
+
examples.PascalCase.push(file.relativePath);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const total = Object.values(counts).reduce((a, b) => a + b, 0);
|
|
59
|
+
if (total === 0)
|
|
60
|
+
return { style: 'mixed', confidence: 0, examples: [] };
|
|
61
|
+
const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]);
|
|
62
|
+
const [topStyle, topCount] = sorted[0];
|
|
63
|
+
const confidence = topCount / total;
|
|
64
|
+
return {
|
|
65
|
+
style: confidence >= 0.5 ? topStyle : 'mixed',
|
|
66
|
+
confidence: Math.round(confidence * 100) / 100,
|
|
67
|
+
examples: examples[topStyle],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function detectTestingStyle(files) {
|
|
71
|
+
let coLocated = 0;
|
|
72
|
+
let testsDir = 0;
|
|
73
|
+
let testDir = 0;
|
|
74
|
+
const examples = {
|
|
75
|
+
'co-located': [],
|
|
76
|
+
__tests__: [],
|
|
77
|
+
'test-directory': [],
|
|
78
|
+
mixed: [],
|
|
79
|
+
};
|
|
80
|
+
const testFiles = files.filter((f) => {
|
|
81
|
+
const name = basename(f.relativePath);
|
|
82
|
+
return (name.includes('.test.') || name.includes('.spec.') || f.relativePath.includes('__tests__'));
|
|
83
|
+
});
|
|
84
|
+
for (const file of testFiles) {
|
|
85
|
+
const dir = dirname(file.relativePath);
|
|
86
|
+
if (dir.includes('__tests__')) {
|
|
87
|
+
testsDir++;
|
|
88
|
+
if (examples.__tests__.length < 3)
|
|
89
|
+
examples.__tests__.push(file.relativePath);
|
|
90
|
+
}
|
|
91
|
+
else if (dir === 'test' ||
|
|
92
|
+
dir === 'tests' ||
|
|
93
|
+
dir.startsWith('test/') ||
|
|
94
|
+
dir.startsWith('tests/')) {
|
|
95
|
+
testDir++;
|
|
96
|
+
if (examples['test-directory'].length < 3)
|
|
97
|
+
examples['test-directory'].push(file.relativePath);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
coLocated++;
|
|
101
|
+
if (examples['co-located'].length < 3)
|
|
102
|
+
examples['co-located'].push(file.relativePath);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const total = coLocated + testsDir + testDir;
|
|
106
|
+
if (total === 0)
|
|
107
|
+
return { style: 'mixed', confidence: 0, examples: [] };
|
|
108
|
+
const counts = { 'co-located': coLocated, __tests__: testsDir, 'test-directory': testDir };
|
|
109
|
+
const sorted = Object.entries(counts).sort((a, b) => b[1] - a[1]);
|
|
110
|
+
const [topStyle, topCount] = sorted[0];
|
|
111
|
+
const confidence = topCount / total;
|
|
112
|
+
return {
|
|
113
|
+
style: confidence >= 0.5 ? topStyle : 'mixed',
|
|
114
|
+
confidence: Math.round(confidence * 100) / 100,
|
|
115
|
+
examples: examples[topStyle],
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function detectImportStyle(files) {
|
|
119
|
+
// Check for barrel index files
|
|
120
|
+
const indexFiles = files.filter((f) => basename(f.relativePath).startsWith('index.'));
|
|
121
|
+
const usesBarrels = indexFiles.length > 3;
|
|
122
|
+
// For a proper import analysis we'd need to read file contents,
|
|
123
|
+
// but for the convention detector we just note barrel presence
|
|
124
|
+
return {
|
|
125
|
+
style: 'relative',
|
|
126
|
+
confidence: 0.8,
|
|
127
|
+
usesBarrels,
|
|
128
|
+
examples: indexFiles.slice(0, 3).map((f) => f.relativePath),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
// ── Public API ──────────────────────────────────────────────────
|
|
132
|
+
/**
|
|
133
|
+
* Detect coding conventions from the file list.
|
|
134
|
+
*/
|
|
135
|
+
export function detectConventions(files) {
|
|
136
|
+
const conventions = [];
|
|
137
|
+
// File naming convention
|
|
138
|
+
const naming = detectFileNamingStyle(files);
|
|
139
|
+
if (naming.confidence > 0) {
|
|
140
|
+
conventions.push({
|
|
141
|
+
category: 'naming',
|
|
142
|
+
pattern: `${naming.style} for file names`,
|
|
143
|
+
confidence: naming.confidence,
|
|
144
|
+
examples: naming.examples,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
// Testing convention
|
|
148
|
+
const testing = detectTestingStyle(files);
|
|
149
|
+
if (testing.confidence > 0) {
|
|
150
|
+
const desc = testing.style === 'co-located'
|
|
151
|
+
? 'Co-located test files (*.test.ts)'
|
|
152
|
+
: testing.style === '__tests__'
|
|
153
|
+
? 'Test files in __tests__/ directories'
|
|
154
|
+
: testing.style === 'test-directory'
|
|
155
|
+
? 'Test files in test/ directory'
|
|
156
|
+
: 'Mixed test file placement';
|
|
157
|
+
conventions.push({
|
|
158
|
+
category: 'testing',
|
|
159
|
+
pattern: desc,
|
|
160
|
+
confidence: testing.confidence,
|
|
161
|
+
examples: testing.examples,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
// Import style convention
|
|
165
|
+
const imports = detectImportStyle(files);
|
|
166
|
+
const importDesc = imports.usesBarrels
|
|
167
|
+
? 'Relative imports, barrel re-exports via index.ts'
|
|
168
|
+
: 'Relative imports';
|
|
169
|
+
conventions.push({
|
|
170
|
+
category: 'imports',
|
|
171
|
+
pattern: importDesc,
|
|
172
|
+
confidence: imports.confidence,
|
|
173
|
+
examples: imports.examples,
|
|
174
|
+
});
|
|
175
|
+
return conventions;
|
|
176
|
+
}
|
|
177
|
+
//# sourceMappingURL=convention-detector.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse TS/JS imports via regex, build module dependency graph, detect cycles.
|
|
3
|
+
* Uses regex rather than AST for speed and zero external dependencies.
|
|
4
|
+
*/
|
|
5
|
+
import type { ImportStatement, ModuleInfo, ModuleGraph } from './types.js';
|
|
6
|
+
/**
|
|
7
|
+
* Parse all import/export/require statements from a single file.
|
|
8
|
+
*/
|
|
9
|
+
export declare function parseImports(filePath: string): Promise<ImportStatement[]>;
|
|
10
|
+
/**
|
|
11
|
+
* Build a module dependency graph from file-level imports.
|
|
12
|
+
*/
|
|
13
|
+
export declare function buildModuleGraph(files: {
|
|
14
|
+
path: string;
|
|
15
|
+
relativePath: string;
|
|
16
|
+
}[], modules: ModuleInfo[], rootDir: string): Promise<ModuleGraph>;
|
|
17
|
+
//# sourceMappingURL=dependency-parser.d.ts.map
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse TS/JS imports via regex, build module dependency graph, detect cycles.
|
|
3
|
+
* Uses regex rather than AST for speed and zero external dependencies.
|
|
4
|
+
*/
|
|
5
|
+
import { readFile } from 'node:fs/promises';
|
|
6
|
+
import { dirname, relative, resolve } from 'node:path';
|
|
7
|
+
// Matches: import ... from 'specifier' | import 'specifier'
|
|
8
|
+
const IMPORT_FROM_RE = /import\s+(?:[\s\S]*?\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
9
|
+
// Matches: export ... from 'specifier'
|
|
10
|
+
const EXPORT_FROM_RE = /export\s+(?:[\s\S]*?\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
11
|
+
// Matches: require('specifier')
|
|
12
|
+
const REQUIRE_RE = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
13
|
+
function isExternal(specifier) {
|
|
14
|
+
return !specifier.startsWith('.') && !specifier.startsWith('/');
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Parse all import/export/require statements from a single file.
|
|
18
|
+
*/
|
|
19
|
+
export async function parseImports(filePath) {
|
|
20
|
+
let content;
|
|
21
|
+
try {
|
|
22
|
+
content = await readFile(filePath, 'utf-8');
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
const imports = [];
|
|
28
|
+
// Track which specifiers we've already seen to avoid duplicates from re-exports
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
for (const regex of [IMPORT_FROM_RE, EXPORT_FROM_RE, REQUIRE_RE]) {
|
|
31
|
+
regex.lastIndex = 0;
|
|
32
|
+
let match;
|
|
33
|
+
while ((match = regex.exec(content)) !== null) {
|
|
34
|
+
const specifier = match[1];
|
|
35
|
+
if (seen.has(specifier))
|
|
36
|
+
continue;
|
|
37
|
+
seen.add(specifier);
|
|
38
|
+
// Find approximate line number
|
|
39
|
+
const upToMatch = content.slice(0, match.index);
|
|
40
|
+
const line = upToMatch.split('\n').length;
|
|
41
|
+
imports.push({
|
|
42
|
+
source: filePath,
|
|
43
|
+
specifier,
|
|
44
|
+
isExternal: isExternal(specifier),
|
|
45
|
+
line,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return imports;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Resolve a relative import specifier to a module path.
|
|
53
|
+
* Returns the module directory relative to rootDir, or null if external.
|
|
54
|
+
*/
|
|
55
|
+
function resolveToModule(sourceFile, specifier, rootDir, modulePaths) {
|
|
56
|
+
if (isExternal(specifier))
|
|
57
|
+
return null;
|
|
58
|
+
const sourceDir = dirname(sourceFile);
|
|
59
|
+
const resolved = resolve(sourceDir, specifier);
|
|
60
|
+
const relResolved = relative(rootDir, resolved).split('\\').join('/');
|
|
61
|
+
// Walk up from the resolved path to find the containing module
|
|
62
|
+
const parts = relResolved.split('/');
|
|
63
|
+
for (let i = parts.length; i > 0; i--) {
|
|
64
|
+
const candidate = parts.slice(0, i).join('/');
|
|
65
|
+
if (modulePaths.has(candidate))
|
|
66
|
+
return candidate;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Build a module dependency graph from file-level imports.
|
|
72
|
+
*/
|
|
73
|
+
export async function buildModuleGraph(files, modules, rootDir) {
|
|
74
|
+
const modulePaths = new Set(modules.map((m) => m.path));
|
|
75
|
+
const edgeMap = new Map();
|
|
76
|
+
const externalDeps = new Set();
|
|
77
|
+
for (const file of files) {
|
|
78
|
+
const imports = await parseImports(file.path);
|
|
79
|
+
// Determine which module this file belongs to
|
|
80
|
+
const relParts = file.relativePath.split('/');
|
|
81
|
+
let sourceModule = null;
|
|
82
|
+
for (let i = relParts.length; i > 0; i--) {
|
|
83
|
+
const candidate = relParts.slice(0, i).join('/');
|
|
84
|
+
if (modulePaths.has(candidate)) {
|
|
85
|
+
sourceModule = candidate;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
for (const imp of imports) {
|
|
90
|
+
if (imp.isExternal) {
|
|
91
|
+
// Extract package name (handle scoped packages)
|
|
92
|
+
const pkgName = imp.specifier.startsWith('@')
|
|
93
|
+
? imp.specifier.split('/').slice(0, 2).join('/')
|
|
94
|
+
: imp.specifier.split('/')[0];
|
|
95
|
+
externalDeps.add(pkgName);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (!sourceModule)
|
|
99
|
+
continue;
|
|
100
|
+
const targetModule = resolveToModule(file.path, imp.specifier, rootDir, modulePaths);
|
|
101
|
+
if (targetModule && targetModule !== sourceModule) {
|
|
102
|
+
if (!edgeMap.has(sourceModule))
|
|
103
|
+
edgeMap.set(sourceModule, new Map());
|
|
104
|
+
const targets = edgeMap.get(sourceModule);
|
|
105
|
+
targets.set(targetModule, (targets.get(targetModule) ?? 0) + 1);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Build edges and update module dependencies/dependents
|
|
110
|
+
const edges = [];
|
|
111
|
+
const moduleMap = new Map(modules.map((m) => [m.path, m]));
|
|
112
|
+
for (const [from, targets] of edgeMap) {
|
|
113
|
+
for (const [to, count] of targets) {
|
|
114
|
+
edges.push({ from, to, importCount: count });
|
|
115
|
+
moduleMap.get(from)?.dependencies.push(to);
|
|
116
|
+
moduleMap.get(to)?.dependents.push(from);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const cycles = detectCycles(modules, edges);
|
|
120
|
+
return {
|
|
121
|
+
modules,
|
|
122
|
+
edges,
|
|
123
|
+
externalDependencies: [...externalDeps].sort(),
|
|
124
|
+
cycles,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Detect cycles in the module graph using DFS.
|
|
129
|
+
*/
|
|
130
|
+
function detectCycles(modules, edges) {
|
|
131
|
+
const adjacency = new Map();
|
|
132
|
+
for (const edge of edges) {
|
|
133
|
+
if (!adjacency.has(edge.from))
|
|
134
|
+
adjacency.set(edge.from, []);
|
|
135
|
+
adjacency.get(edge.from).push(edge.to);
|
|
136
|
+
}
|
|
137
|
+
const cycles = [];
|
|
138
|
+
const visited = new Set();
|
|
139
|
+
const inStack = new Set();
|
|
140
|
+
const stack = [];
|
|
141
|
+
function dfs(node) {
|
|
142
|
+
visited.add(node);
|
|
143
|
+
inStack.add(node);
|
|
144
|
+
stack.push(node);
|
|
145
|
+
for (const neighbor of adjacency.get(node) ?? []) {
|
|
146
|
+
if (!visited.has(neighbor)) {
|
|
147
|
+
dfs(neighbor);
|
|
148
|
+
}
|
|
149
|
+
else if (inStack.has(neighbor)) {
|
|
150
|
+
// Found a cycle — extract it from the stack
|
|
151
|
+
const cycleStart = stack.indexOf(neighbor);
|
|
152
|
+
if (cycleStart >= 0) {
|
|
153
|
+
cycles.push([...stack.slice(cycleStart), neighbor]);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
stack.pop();
|
|
158
|
+
inStack.delete(node);
|
|
159
|
+
}
|
|
160
|
+
for (const mod of modules) {
|
|
161
|
+
if (!visited.has(mod.path)) {
|
|
162
|
+
dfs(mod.path);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return cycles;
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=dependency-parser.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Walk filesystem, count files, detect module boundaries.
|
|
3
|
+
* A module boundary is a directory containing index.ts, index.js, or package.json.
|
|
4
|
+
*/
|
|
5
|
+
import type { FileInfo, ModuleInfo } from './types.js';
|
|
6
|
+
export interface WalkOptions {
|
|
7
|
+
include?: string[];
|
|
8
|
+
exclude?: string[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Walk the filesystem starting from rootDir, collecting file info.
|
|
12
|
+
*/
|
|
13
|
+
export declare function walkFiles(rootDir: string, options?: WalkOptions): Promise<FileInfo[]>;
|
|
14
|
+
/**
|
|
15
|
+
* Detect module boundaries — directories that contain a module marker file.
|
|
16
|
+
*/
|
|
17
|
+
export declare function detectModules(rootDir: string, options?: WalkOptions): Promise<ModuleInfo[]>;
|
|
18
|
+
//# sourceMappingURL=file-walker.d.ts.map
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Walk filesystem, count files, detect module boundaries.
|
|
3
|
+
* A module boundary is a directory containing index.ts, index.js, or package.json.
|
|
4
|
+
*/
|
|
5
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
6
|
+
import { join, relative, extname, basename } from 'node:path';
|
|
7
|
+
const CODE_EXTENSIONS = new Set([
|
|
8
|
+
'.ts',
|
|
9
|
+
'.tsx',
|
|
10
|
+
'.js',
|
|
11
|
+
'.jsx',
|
|
12
|
+
'.mjs',
|
|
13
|
+
'.cjs',
|
|
14
|
+
'.py',
|
|
15
|
+
'.go',
|
|
16
|
+
'.rs',
|
|
17
|
+
'.java',
|
|
18
|
+
'.rb',
|
|
19
|
+
'.php',
|
|
20
|
+
'.c',
|
|
21
|
+
'.cpp',
|
|
22
|
+
'.h',
|
|
23
|
+
'.hpp',
|
|
24
|
+
'.cs',
|
|
25
|
+
'.swift',
|
|
26
|
+
]);
|
|
27
|
+
const MODULE_MARKERS = ['index.ts', 'index.js', 'index.mjs', 'package.json'];
|
|
28
|
+
function matchesGlob(filePath, patterns) {
|
|
29
|
+
for (const pattern of patterns) {
|
|
30
|
+
// Simple glob matching: supports ** and *
|
|
31
|
+
const regex = pattern
|
|
32
|
+
.replace(/\./g, '\\.')
|
|
33
|
+
.replace(/\*\*/g, '{{GLOBSTAR}}')
|
|
34
|
+
.replace(/\*/g, '[^/]*')
|
|
35
|
+
.replace(/\{\{GLOBSTAR\}\}/g, '.*');
|
|
36
|
+
if (new RegExp(`^${regex}$`).test(filePath) ||
|
|
37
|
+
new RegExp(`^${regex}$`).test(basename(filePath))) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
function shouldExclude(relativePath, exclude) {
|
|
44
|
+
if (exclude.length === 0)
|
|
45
|
+
return false;
|
|
46
|
+
return matchesGlob(relativePath, exclude);
|
|
47
|
+
}
|
|
48
|
+
async function countLines(filePath) {
|
|
49
|
+
try {
|
|
50
|
+
const content = await readFile(filePath, 'utf-8');
|
|
51
|
+
return content.split('\n').length;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Walk the filesystem starting from rootDir, collecting file info.
|
|
59
|
+
*/
|
|
60
|
+
export async function walkFiles(rootDir, options) {
|
|
61
|
+
const exclude = options?.exclude ?? [
|
|
62
|
+
'node_modules/**',
|
|
63
|
+
'.git/**',
|
|
64
|
+
'dist/**',
|
|
65
|
+
'build/**',
|
|
66
|
+
'coverage/**',
|
|
67
|
+
];
|
|
68
|
+
const files = [];
|
|
69
|
+
async function walk(dir) {
|
|
70
|
+
let entries;
|
|
71
|
+
try {
|
|
72
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
const fullPath = join(dir, entry.name);
|
|
79
|
+
const relPath = relative(rootDir, fullPath);
|
|
80
|
+
if (shouldExclude(relPath, exclude))
|
|
81
|
+
continue;
|
|
82
|
+
if (entry.isDirectory()) {
|
|
83
|
+
// Also check directory-level exclusions
|
|
84
|
+
if (shouldExclude(relPath + '/', exclude))
|
|
85
|
+
continue;
|
|
86
|
+
await walk(fullPath);
|
|
87
|
+
}
|
|
88
|
+
else if (entry.isFile()) {
|
|
89
|
+
const ext = extname(entry.name);
|
|
90
|
+
if (CODE_EXTENSIONS.has(ext)) {
|
|
91
|
+
const lineCount = await countLines(fullPath);
|
|
92
|
+
files.push({
|
|
93
|
+
path: fullPath,
|
|
94
|
+
relativePath: relPath,
|
|
95
|
+
lineCount,
|
|
96
|
+
extension: ext,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
await walk(rootDir);
|
|
103
|
+
return files;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Detect module boundaries — directories that contain a module marker file.
|
|
107
|
+
*/
|
|
108
|
+
export async function detectModules(rootDir, options) {
|
|
109
|
+
const exclude = options?.exclude ?? [
|
|
110
|
+
'node_modules/**',
|
|
111
|
+
'.git/**',
|
|
112
|
+
'dist/**',
|
|
113
|
+
'build/**',
|
|
114
|
+
'coverage/**',
|
|
115
|
+
];
|
|
116
|
+
const modules = [];
|
|
117
|
+
async function walk(dir) {
|
|
118
|
+
let entries;
|
|
119
|
+
try {
|
|
120
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const relDir = relative(rootDir, dir);
|
|
126
|
+
if (relDir && shouldExclude(relDir + '/', exclude))
|
|
127
|
+
return;
|
|
128
|
+
const names = entries.map((e) => e.name);
|
|
129
|
+
const hasMarker = MODULE_MARKERS.some((m) => names.includes(m));
|
|
130
|
+
if (hasMarker && relDir) {
|
|
131
|
+
// Count files in this module
|
|
132
|
+
let fileCount = 0;
|
|
133
|
+
for (const entry of entries) {
|
|
134
|
+
if (entry.isFile() && CODE_EXTENSIONS.has(extname(entry.name))) {
|
|
135
|
+
fileCount++;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
modules.push({
|
|
139
|
+
name: relDir.split('/').pop() || relDir,
|
|
140
|
+
path: relDir,
|
|
141
|
+
fileCount,
|
|
142
|
+
dependencies: [],
|
|
143
|
+
dependents: [],
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
for (const entry of entries) {
|
|
147
|
+
if (entry.isDirectory()) {
|
|
148
|
+
const childRel = relative(rootDir, join(dir, entry.name));
|
|
149
|
+
if (!shouldExclude(childRel + '/', exclude)) {
|
|
150
|
+
await walk(join(dir, entry.name));
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
await walk(rootDir);
|
|
156
|
+
return modules;
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=file-walker.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git log churn analysis + file complexity heuristic → composite hotspot score.
|
|
3
|
+
* Complexity heuristic: lineCount * 0.6 + importCount * 0.4, normalized to 1-10.
|
|
4
|
+
*/
|
|
5
|
+
import type { FileInfo, Hotspot, ImportStatement } from './types.js';
|
|
6
|
+
interface ChurnEntry {
|
|
7
|
+
filePath: string;
|
|
8
|
+
commitCount: number;
|
|
9
|
+
lastModified?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Run `git log` to get file churn data.
|
|
13
|
+
*/
|
|
14
|
+
export declare function getFileChurn(repoPath: string, historyDays?: number): Promise<ChurnEntry[]>;
|
|
15
|
+
/**
|
|
16
|
+
* Compute file complexity as a heuristic: lineCount * 0.6 + importCount * 0.4.
|
|
17
|
+
* Normalized to 1-10 scale.
|
|
18
|
+
*/
|
|
19
|
+
export declare function computeFileComplexity(lineCount: number, importCount: number): number;
|
|
20
|
+
/**
|
|
21
|
+
* Analyze hotspots: files with high churn and/or high complexity.
|
|
22
|
+
*/
|
|
23
|
+
export declare function analyzeHotspots(repoPath: string, files: FileInfo[], importsByFile: Map<string, ImportStatement[]>, options?: {
|
|
24
|
+
historyDays?: number;
|
|
25
|
+
threshold?: number;
|
|
26
|
+
}): Promise<Hotspot[]>;
|
|
27
|
+
export {};
|
|
28
|
+
//# sourceMappingURL=hotspot-analyzer.d.ts.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git log churn analysis + file complexity heuristic → composite hotspot score.
|
|
3
|
+
* Complexity heuristic: lineCount * 0.6 + importCount * 0.4, normalized to 1-10.
|
|
4
|
+
*/
|
|
5
|
+
import { execFile } from 'node:child_process';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
import { DEFAULT_GIT_HISTORY_DAYS, DEFAULT_HOTSPOT_THRESHOLD } from '../defaults.js';
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
/**
|
|
10
|
+
* Run `git log` to get file churn data.
|
|
11
|
+
*/
|
|
12
|
+
export async function getFileChurn(repoPath, historyDays = DEFAULT_GIT_HISTORY_DAYS) {
|
|
13
|
+
try {
|
|
14
|
+
const since = `${historyDays} days ago`;
|
|
15
|
+
const { stdout } = await execFileAsync('git', ['log', '--format=', '--name-only', `--since=${since}`], { cwd: repoPath, maxBuffer: 10 * 1024 * 1024 });
|
|
16
|
+
const counts = new Map();
|
|
17
|
+
for (const line of stdout.split('\n')) {
|
|
18
|
+
const trimmed = line.trim();
|
|
19
|
+
if (!trimmed)
|
|
20
|
+
continue;
|
|
21
|
+
counts.set(trimmed, (counts.get(trimmed) ?? 0) + 1);
|
|
22
|
+
}
|
|
23
|
+
// Get last modified dates
|
|
24
|
+
const entries = [];
|
|
25
|
+
for (const [filePath, commitCount] of counts) {
|
|
26
|
+
entries.push({ filePath, commitCount });
|
|
27
|
+
}
|
|
28
|
+
return entries;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// Not a git repo or git not available
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Compute file complexity as a heuristic: lineCount * 0.6 + importCount * 0.4.
|
|
37
|
+
* Normalized to 1-10 scale.
|
|
38
|
+
*/
|
|
39
|
+
export function computeFileComplexity(lineCount, importCount) {
|
|
40
|
+
// Raw score: weighted sum
|
|
41
|
+
const raw = lineCount * 0.6 + importCount * 0.4;
|
|
42
|
+
// Normalize to 1-10 using a logarithmic scale
|
|
43
|
+
// ~50 lines + 5 imports ≈ 3, ~200 lines + 20 imports ≈ 5, ~500+ lines + 30+ imports ≈ 8+
|
|
44
|
+
const normalized = (Math.log2(raw + 1) / Math.log2(500)) * 10;
|
|
45
|
+
return Math.max(1, Math.min(10, Math.round(normalized)));
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Analyze hotspots: files with high churn and/or high complexity.
|
|
49
|
+
*/
|
|
50
|
+
export async function analyzeHotspots(repoPath, files, importsByFile, options) {
|
|
51
|
+
const historyDays = options?.historyDays ?? DEFAULT_GIT_HISTORY_DAYS;
|
|
52
|
+
const threshold = options?.threshold ?? DEFAULT_HOTSPOT_THRESHOLD;
|
|
53
|
+
const churnEntries = await getFileChurn(repoPath, historyDays);
|
|
54
|
+
const churnMap = new Map(churnEntries.map((e) => [e.filePath, e]));
|
|
55
|
+
// Total commits to compute churn rate
|
|
56
|
+
const totalCommits = churnEntries.reduce((sum, e) => sum + e.commitCount, 0);
|
|
57
|
+
const hotspots = [];
|
|
58
|
+
for (const file of files) {
|
|
59
|
+
const churn = churnMap.get(file.relativePath);
|
|
60
|
+
const commitCount = churn?.commitCount ?? 0;
|
|
61
|
+
const churnRate = totalCommits > 0 ? commitCount / totalCommits : 0;
|
|
62
|
+
const imports = importsByFile.get(file.relativePath) ?? [];
|
|
63
|
+
const complexity = computeFileComplexity(file.lineCount, imports.length);
|
|
64
|
+
// Composite score: churnRate * 0.5 + normalizedComplexity * 0.5
|
|
65
|
+
const normalizedComplexity = complexity / 10;
|
|
66
|
+
const compositeScore = churnRate * 0.5 + normalizedComplexity * 0.5;
|
|
67
|
+
if (compositeScore >= threshold || complexity >= 7) {
|
|
68
|
+
hotspots.push({
|
|
69
|
+
filePath: file.relativePath,
|
|
70
|
+
churnRate: Math.round(churnRate * 1000) / 1000,
|
|
71
|
+
complexity,
|
|
72
|
+
commitCount,
|
|
73
|
+
lastModified: churn?.lastModified,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Sort by composite score descending
|
|
78
|
+
hotspots.sort((a, b) => {
|
|
79
|
+
const scoreA = a.churnRate * 0.5 + (a.complexity / 10) * 0.5;
|
|
80
|
+
const scoreB = b.churnRate * 0.5 + (b.complexity / 10) * 0.5;
|
|
81
|
+
return scoreB - scoreA;
|
|
82
|
+
});
|
|
83
|
+
return hotspots;
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=hotspot-analyzer.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Barrel exports for the analysis module.
|
|
3
|
+
*/
|
|
4
|
+
export type { CodebaseProfile, CodebaseContext, Hotspot, ArchitecturalPattern, DetectedConvention, ModuleInfo, ModuleGraph, DependencyEdge, AnalyzerOptions, FileInfo, ImportStatement, } from './types.js';
|
|
5
|
+
export { walkFiles, detectModules } from './file-walker.js';
|
|
6
|
+
export { parseImports, buildModuleGraph } from './dependency-parser.js';
|
|
7
|
+
export { detectConventions } from './convention-detector.js';
|
|
8
|
+
export { detectPatterns } from './pattern-detector.js';
|
|
9
|
+
export { analyzeHotspots } from './hotspot-analyzer.js';
|
|
10
|
+
export { computeComplexityScore } from './complexity-scorer.js';
|
|
11
|
+
export { analyzeCodebase } from './analyzer.js';
|
|
12
|
+
export { buildCodebaseContext, formatContextForPrompt } from './context-builder.js';
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Barrel exports for the analysis module.
|
|
3
|
+
*/
|
|
4
|
+
export { walkFiles, detectModules } from './file-walker.js';
|
|
5
|
+
export { parseImports, buildModuleGraph } from './dependency-parser.js';
|
|
6
|
+
export { detectConventions } from './convention-detector.js';
|
|
7
|
+
export { detectPatterns } from './pattern-detector.js';
|
|
8
|
+
export { analyzeHotspots } from './hotspot-analyzer.js';
|
|
9
|
+
export { computeComplexityScore } from './complexity-scorer.js';
|
|
10
|
+
export { analyzeCodebase } from './analyzer.js';
|
|
11
|
+
export { buildCodebaseContext, formatContextForPrompt } from './context-builder.js';
|
|
12
|
+
//# sourceMappingURL=index.js.map
|