kachow 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.
Files changed (48) hide show
  1. package/README.md +77 -0
  2. package/_server/dist/app.js +130 -0
  3. package/_server/dist/db/index.js +50 -0
  4. package/_server/dist/db/schema.js +247 -0
  5. package/_server/dist/queues/ingestQueue.js +49 -0
  6. package/_server/dist/queues/redis.js +58 -0
  7. package/_server/dist/routes/agents.js +162 -0
  8. package/_server/dist/routes/architecture.js +88 -0
  9. package/_server/dist/routes/config.js +24 -0
  10. package/_server/dist/routes/github.js +158 -0
  11. package/_server/dist/routes/graph.js +112 -0
  12. package/_server/dist/routes/healing.js +137 -0
  13. package/_server/dist/routes/impact.js +100 -0
  14. package/_server/dist/routes/ingest.js +182 -0
  15. package/_server/dist/routes/manager.js +179 -0
  16. package/_server/dist/routes/notifications.js +85 -0
  17. package/_server/dist/routes/qa.js +68 -0
  18. package/_server/dist/routes/scanner.js +221 -0
  19. package/_server/dist/routes/stream.js +179 -0
  20. package/_server/dist/routes/webhooks.js +168 -0
  21. package/_server/dist/server.js +46 -0
  22. package/_server/dist/services/agentService.js +715 -0
  23. package/_server/dist/services/architectureService.js +172 -0
  24. package/_server/dist/services/demoSeed.js +181 -0
  25. package/_server/dist/services/graphLayout.js +102 -0
  26. package/_server/dist/services/graphService.js +532 -0
  27. package/_server/dist/services/healingService.js +253 -0
  28. package/_server/dist/services/impactService.js +304 -0
  29. package/_server/dist/services/ingestService.js +129 -0
  30. package/_server/dist/services/managerService.js +260 -0
  31. package/_server/dist/services/notificationService.js +283 -0
  32. package/_server/dist/services/qaService.js +413 -0
  33. package/_server/dist/services/scannerService.js +748 -0
  34. package/_server/dist/services/seedService.js +215 -0
  35. package/_server/dist/sse/sseManager.js +101 -0
  36. package/_server/dist/types/index.js +38 -0
  37. package/_server/dist/workers/ingestWorker.js +274 -0
  38. package/_server/public/assets/index-BTkbB_YF.js +4546 -0
  39. package/_server/public/assets/index-Bmh3jWBm.css +1 -0
  40. package/_server/public/favicon.ico +0 -0
  41. package/_server/public/images/glass-waves-bg.png +0 -0
  42. package/_server/public/index.html +29 -0
  43. package/_server/public/placeholder.svg +1 -0
  44. package/_server/public/robots.txt +14 -0
  45. package/dist/config.js +133 -0
  46. package/dist/index.js +510 -0
  47. package/dist/setup.js +223 -0
  48. package/package.json +62 -0
@@ -0,0 +1,748 @@
1
+ "use strict";
2
+ /**
3
+ * KA-CHOW Repository Scanner.
4
+ *
5
+ * Capabilities:
6
+ * \u2022 Monorepo awareness \u2014 discovers every service under services/, packages/, apps/, etc.
7
+ * \u2022 Multi-language \u2014 TypeScript, JavaScript, Python, Go, Java, Rust, Ruby, PHP
8
+ * \u2022 Infrastructure \u2014 Terraform resource detection, Docker Compose, Kubernetes
9
+ * \u2022 Health scoring \u2014 OpenAPI, README, tests, CI/CD, Docker, linting, secrets
10
+ * \u2022 Endpoint detection \u2014 Express, Fastify, Hapi, NestJS, Flask, FastAPI, Gin, Echo,
11
+ * Spring MVC, Actix
12
+ * \u2022 Dependency mapping \u2014 HTTP calls, env-var service refs, RabbitMQ, Kafka, gRPC, DB
13
+ * \u2022 Git metadata \u2014 repo URL, subpath, last-commit timestamp per service
14
+ */
15
+ var __importDefault = (this && this.__importDefault) || function (mod) {
16
+ return (mod && mod.__esModule) ? mod : { "default": mod };
17
+ };
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.serviceIdFromName = serviceIdFromName;
20
+ exports.calculateHealthScore = calculateHealthScore;
21
+ exports.detectEndpoints = detectEndpoints;
22
+ exports.detectDependencies = detectDependencies;
23
+ exports.walkSourceFiles = walkSourceFiles;
24
+ exports.bestFileMatch = bestFileMatch;
25
+ exports.scanMonorepo = scanMonorepo;
26
+ exports.scanRepo = scanRepo;
27
+ const fs_1 = __importDefault(require("fs"));
28
+ const path_1 = __importDefault(require("path"));
29
+ const crypto_1 = __importDefault(require("crypto"));
30
+ const child_process_1 = require("child_process");
31
+ // \u2500\u2500 Constants \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
32
+ const SKIP_DIRS = new Set([
33
+ 'node_modules', '.git', 'dist', 'build', '.next', '__pycache__',
34
+ 'venv', '.venv', 'env', '.env', 'coverage', '.nyc_output', 'vendor',
35
+ 'target', 'out', '.gradle', '.idea', '.vscode', 'bin', 'obj',
36
+ 'terraform.tfstate.d', '.terraform',
37
+ ]);
38
+ const SOURCE_EXTS = new Set([
39
+ '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
40
+ '.py', '.go', '.java', '.kt', '.rs', '.rb', '.php',
41
+ ]);
42
+ const SERVICE_MARKERS = [
43
+ 'package.json', 'go.mod', 'Cargo.toml', 'pom.xml',
44
+ 'build.gradle', 'build.gradle.kts', 'setup.py',
45
+ 'pyproject.toml', 'requirements.txt', 'Gemfile', 'composer.json',
46
+ ];
47
+ const WORKSPACE_CONTAINER_NAMES = new Set([
48
+ 'services', 'packages', 'apps', 'microservices',
49
+ 'modules', 'libs', 'src', 'cmd', 'internal',
50
+ 'frontends', 'infrastructure', 'monitoring', 'orchestration',
51
+ 'data', 'scripts', 'deploy', 'deployments',
52
+ ]);
53
+ const MONOREPO_ROOT_MARKERS = [
54
+ 'pnpm-workspace.yaml', 'lerna.json', 'nx.json', 'turbo.json', 'rush.json',
55
+ ];
56
+ // \u2500\u2500 Regex patterns \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
57
+ const HTTP_CALL_RE = /(?:axios|got|request)\s*\.\s*(?:get|post|put|patch|delete)\s*\(\s*[`'""]([^`'"]+)[`'""]/g;
58
+ const FETCH_RE = /fetch\s*\(\s*[`'""]([^`'"\s]+)[`'""]/g;
59
+ const REQUESTS_RE = /requests\s*\.\s*(?:get|post|put|patch|delete)\s*\(\s*["']([^"\']+)["']/g;
60
+ const HTTPX_RE = /httpx\s*\.\s*(?:get|post|put|patch|delete)\s*\(\s*["']([^"\']+)["']/g;
61
+ const GO_HTTP_RE = /http\s*\.\s*(?:Get|Post|Put|Patch|Delete)\s*\(\s*"([^"]+)"/g;
62
+ const ENV_VAR_HTTP_RE = /(?:axios|got|request|fetch)\s*(?:\.\s*(?:get|post|put|patch|delete)\s*)?\(\s*`\s*\$\{process\.env\.([A-Za-z_]+)\}([^`]*)`/g;
63
+ const AMQP_LITERAL_RE = /channel\s*\.\s*(?:sendToQueue|publish|assertQueue|consume|bindQueue)\s*\(\s*[`'""]([^`'"]+)[`'""]/g;
64
+ const AMQP_VAR_RE = /channel\s*\.\s*(?:sendToQueue|publish|assertQueue|consume|bindQueue)\s*\(\s*(\w+)/g;
65
+ const AMQP_WRAPPER_RE = /(?:publish|send|enqueue)To(?:Queue|Topic|Exchange)\s*\(\s*[`'""]([^`'"]+)[`'""]/gi;
66
+ const KAFKA_PRODUCER_RE = /producer\.send\s*\(\s*\{\s*topic:\s*[`'""]([^`'"]+)[`'""]/g;
67
+ const KAFKA_CONSUMER_RE = /consumer\.subscribe\s*\(\s*\{\s*topics?:\s*[`'"\[[]([^`'"\]]+)[`'"]/g;
68
+ const NATS_RE = /nc\.publish\s*\(\s*[`'""]([^`'"]+)[`'""]/g;
69
+ const GRPC_RE = /loadPackageDefinition|\.proto[`'""]/g;
70
+ const DB_URL_RE = /DATABASE_URL|Prisma|Sequelize|createConnection|mongoose\.connect|psycopg2|asyncpg|sqlalchemy|GORM|diesel/g;
71
+ const EXPRESS_RE = /(?:router|app|fastify|server)\s*\.\s*(get|post|put|patch|delete|all|route)\s*\(\s*[`'""]([^`'"]+)[`'""]/gi;
72
+ const NESTJS_RE = /@(Get|Post|Put|Patch|Delete|Options|Head|All)\s*\(\s*(?:[`'""]([^`'"]*)[`'"])?\s*\)/gi;
73
+ const FLASK_RE = /@(?:app|blueprint|bp|\w+_bp)\s*\.route\s*\(\s*[`'""]([^`'"]+)[`'""]/gi;
74
+ const FASTAPI_RE = /@(?:app|router|api_router)\s*\.\s*(get|post|put|patch|delete|options|head)\s*\(\s*[`'""]([^`'"]+)[`'""]/gi;
75
+ const GIN_RE = /(?:r|router|e|mux|v\d|api)\s*\.\s*(GET|POST|PUT|PATCH|DELETE|OPTIONS|HEAD)\s*\(\s*"([^"]+)"/g;
76
+ const SPRING_RE = /@(GetMapping|PostMapping|PutMapping|PatchMapping|DeleteMapping|RequestMapping)\s*\(\s*(?:value\s*=\s*)?[`'""]([^`'"]+)[`'""]/gi;
77
+ const ACTIX_RE = /#\[\s*(get|post|put|patch|delete|options|head)\s*\(\s*"([^"]+)"\s*\)\s*\]/gi;
78
+ const HAPI_METHOD_RE = /method\s*:\s*[`'""]([A-Z]+)[`'""]/g;
79
+ const HAPI_PATH_RE = /path\s*:\s*[`'""]([^`'"]+)[`'""]/g;
80
+ // \u2500\u2500 Utility helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
81
+ function walkDir(dir, maxDepth = 12, _depth = 0) {
82
+ if (_depth > maxDepth)
83
+ return [];
84
+ const results = [];
85
+ let entries;
86
+ try {
87
+ entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
88
+ }
89
+ catch {
90
+ return results;
91
+ }
92
+ for (const entry of entries) {
93
+ if (SKIP_DIRS.has(entry.name))
94
+ continue;
95
+ const full = path_1.default.join(dir, entry.name);
96
+ if (entry.isDirectory()) {
97
+ results.push(...walkDir(full, maxDepth, _depth + 1));
98
+ }
99
+ else {
100
+ results.push(full);
101
+ }
102
+ }
103
+ return results;
104
+ }
105
+ function readSafe(filePath) {
106
+ try {
107
+ return fs_1.default.readFileSync(filePath, 'utf8');
108
+ }
109
+ catch {
110
+ return '';
111
+ }
112
+ }
113
+ function serviceIdFromName(name) {
114
+ return crypto_1.default.createHash('sha256').update(name).digest('hex').slice(0, 12);
115
+ }
116
+ function extractHostname(rawUrl) {
117
+ const cleaned = rawUrl.replace(/\$\{[^}]+\}/g, 'placeholder');
118
+ try {
119
+ return new URL(cleaned).hostname;
120
+ }
121
+ catch {
122
+ return rawUrl.split('/')[0] ?? rawUrl;
123
+ }
124
+ }
125
+ function detectGitInfo(dir) {
126
+ try {
127
+ const gitRoot = fs_1.default.realpathSync((0, child_process_1.execSync)('git rev-parse --show-toplevel', { cwd: dir, stdio: 'pipe' }).toString().trim());
128
+ let repoUrl = null;
129
+ try {
130
+ repoUrl = (0, child_process_1.execSync)('git remote get-url origin', { cwd: dir, stdio: 'pipe' })
131
+ .toString().trim()
132
+ .replace(/\.git$/, '')
133
+ .replace(/^git@([^:]+):(.+)$/, 'https://$1/$2');
134
+ }
135
+ catch { /* no remote */ }
136
+ return { gitRoot, repoUrl };
137
+ }
138
+ catch {
139
+ return { gitRoot: null, repoUrl: null };
140
+ }
141
+ }
142
+ function detectLastCommit(dir) {
143
+ try {
144
+ return (0, child_process_1.execSync)('git log -1 --format="%ci" -- .', { cwd: dir, stdio: 'pipe' }).toString().trim() || null;
145
+ }
146
+ catch {
147
+ return null;
148
+ }
149
+ }
150
+ // \u2500\u2500 Language detection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
151
+ function detectLanguage(allFiles) {
152
+ const exts = new Set(allFiles.map((f) => path_1.default.extname(f)));
153
+ const bases = new Set(allFiles.map((f) => path_1.default.basename(f)));
154
+ if (bases.has('tsconfig.json') || exts.has('.ts') || exts.has('.tsx'))
155
+ return 'typescript';
156
+ if (bases.has('package.json') || exts.has('.js') || exts.has('.jsx'))
157
+ return 'javascript';
158
+ if (bases.has('go.mod') || exts.has('.go'))
159
+ return 'go';
160
+ if (bases.has('Cargo.toml') || exts.has('.rs'))
161
+ return 'rust';
162
+ if (bases.has('pom.xml') || bases.has('build.gradle') || exts.has('.java') || exts.has('.kt'))
163
+ return 'java';
164
+ if (exts.has('.py') || bases.has('setup.py') || bases.has('pyproject.toml') || bases.has('requirements.txt'))
165
+ return 'python';
166
+ if (bases.has('Gemfile') || exts.has('.rb'))
167
+ return 'ruby';
168
+ if (bases.has('composer.json') || exts.has('.php'))
169
+ return 'php';
170
+ return 'unknown';
171
+ }
172
+ // \u2500\u2500 Health score \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
173
+ function calculateHealthScore(serviceDir, allFiles, repoRoot) {
174
+ // For CI/CD and Docker checks, also consider repo-root files
175
+ const rootFiles = repoRoot && repoRoot !== serviceDir
176
+ ? walkDir(repoRoot, 3).filter((f) => {
177
+ const rel = path_1.default.relative(repoRoot, f);
178
+ return rel.split(path_1.default.sep).length <= 3; // max 3 levels from root
179
+ })
180
+ : [];
181
+ const issues = [];
182
+ let score = 0;
183
+ // 20 pts \u2014 OpenAPI/Swagger spec
184
+ const hasOpenApi = allFiles.some((f) => {
185
+ const b = path_1.default.basename(f).toLowerCase();
186
+ return b.includes('openapi') || b.includes('swagger') || b === 'api.yaml' || b === 'api.json';
187
+ });
188
+ if (hasOpenApi) {
189
+ score += 20;
190
+ }
191
+ else {
192
+ issues.push('Missing OpenAPI/Swagger spec');
193
+ }
194
+ // 15 pts \u2014 README >= 300 chars
195
+ const readmeContent = readSafe(path_1.default.join(serviceDir, 'README.md'));
196
+ const hasReadme = readmeContent.length >= 300;
197
+ if (hasReadme) {
198
+ score += 15;
199
+ }
200
+ else {
201
+ issues.push(readmeContent.length === 0 ? 'Missing README.md' : 'README.md too short (<300 chars)');
202
+ }
203
+ const docCoverage = hasOpenApi && hasReadme ? 100 : (hasOpenApi || hasReadme) ? 50 : 0;
204
+ // 15 pts \u2014 test coverage
205
+ let testCoverage = 0;
206
+ let coverageFound = false;
207
+ const coveragePaths = [
208
+ path_1.default.join(serviceDir, 'coverage', 'coverage-summary.json'),
209
+ path_1.default.join(serviceDir, 'coverage', 'coverage-final.json'),
210
+ ];
211
+ for (const cp of coveragePaths) {
212
+ const raw = readSafe(cp);
213
+ if (!raw)
214
+ continue;
215
+ try {
216
+ const p = JSON.parse(raw);
217
+ const pct = p['total']?.lines?.pct;
218
+ if (pct !== undefined) {
219
+ testCoverage = pct;
220
+ coverageFound = true;
221
+ break;
222
+ }
223
+ }
224
+ catch { /* skip */ }
225
+ }
226
+ const hasTestDir = allFiles.some((f) => {
227
+ const rel = path_1.default.relative(serviceDir, f);
228
+ return /^(test|tests|__tests__|spec)[\\/]/.test(rel) || /\.(test|spec)\.[jt]sx?$/.test(f);
229
+ });
230
+ if (coverageFound) {
231
+ if (testCoverage >= 70) {
232
+ score += 15;
233
+ }
234
+ else {
235
+ issues.push(`Test coverage ${testCoverage.toFixed(1)}% (need >=70%)`);
236
+ }
237
+ }
238
+ else if (hasTestDir) {
239
+ score += 8;
240
+ testCoverage = 50;
241
+ }
242
+ else {
243
+ issues.push('No tests detected');
244
+ }
245
+ // 10 pts \u2014 no @deprecated markers
246
+ const srcFiles = allFiles.filter((f) => SOURCE_EXTS.has(path_1.default.extname(f)));
247
+ const hasDeprecated = srcFiles.some((f) => /@deprecated/.test(readSafe(f)));
248
+ if (!hasDeprecated) {
249
+ score += 10;
250
+ }
251
+ else {
252
+ issues.push('Deprecated endpoint markers found');
253
+ }
254
+ // 10 pts \u2014 CI/CD
255
+ const ciCdCheck = (f, base) => {
256
+ const rel = path_1.default.relative(base, f);
257
+ return /^\.github[\\/]workflows[\\/].+\.ya?ml$/.test(rel) ||
258
+ path_1.default.basename(f) === '.gitlab-ci.yml' ||
259
+ path_1.default.basename(f) === 'Jenkinsfile' ||
260
+ path_1.default.basename(f) === '.travis.yml' ||
261
+ /^\.circleci[\\/]config\.ya?ml$/.test(rel) ||
262
+ path_1.default.basename(f) === 'azure-pipelines.yml';
263
+ };
264
+ const hasCiCd = allFiles.some((f) => ciCdCheck(f, serviceDir)) ||
265
+ rootFiles.some((f) => ciCdCheck(f, repoRoot));
266
+ if (hasCiCd) {
267
+ score += 10;
268
+ }
269
+ else {
270
+ issues.push('No CI/CD configuration detected');
271
+ }
272
+ // 10 pts \u2014 Dockerfile / docker-compose
273
+ const dockerCheck = (f) => {
274
+ const b = path_1.default.basename(f);
275
+ return b === 'Dockerfile' || b.startsWith('Dockerfile.') ||
276
+ b === 'docker-compose.yml' || b === 'docker-compose.yaml';
277
+ };
278
+ const hasDocker = allFiles.some(dockerCheck) || rootFiles.some(dockerCheck);
279
+ if (hasDocker) {
280
+ score += 10;
281
+ }
282
+ else {
283
+ issues.push('No Dockerfile or docker-compose found');
284
+ }
285
+ // 10 pts \u2014 error handling on external calls
286
+ const bareCallFiles = srcFiles.filter((f) => {
287
+ const c = readSafe(f);
288
+ return /^\s*(?:await\s+)?(?:fetch|axios)\s*\(/m.test(c) && !/try\s*\{/.test(c);
289
+ });
290
+ if (bareCallFiles.length === 0) {
291
+ score += 10;
292
+ }
293
+ else {
294
+ issues.push(`${bareCallFiles.length} file(s) with unprotected external calls`);
295
+ }
296
+ // 5 pts \u2014 linting config
297
+ const hasLint = allFiles.some((f) => {
298
+ const b = path_1.default.basename(f);
299
+ return [
300
+ '.eslintrc.js', '.eslintrc.json', '.eslintrc.yml',
301
+ 'eslint.config.js', 'eslint.config.mjs',
302
+ '.pylintrc', 'golangci.yml', '.golangci.yml',
303
+ '.rubocop.yml', 'phpcs.xml',
304
+ ].includes(b);
305
+ });
306
+ if (hasLint) {
307
+ score += 5;
308
+ }
309
+ else {
310
+ issues.push('No linter config found');
311
+ }
312
+ // 5 pts \u2014 no hardcoded secrets
313
+ const secretPattern = /(?:password|secret|api_key|apikey|token)\s*=\s*[""][^""]{6,}/i;
314
+ const hasSecrets = srcFiles.some((f) => secretPattern.test(readSafe(f)));
315
+ if (!hasSecrets) {
316
+ score += 5;
317
+ }
318
+ else {
319
+ issues.push('Possible hardcoded secrets detected');
320
+ }
321
+ return { score: Math.min(100, Math.max(0, score)), docCoverage, testCoverage: Math.round(testCoverage), issues };
322
+ }
323
+ // \u2500\u2500 Endpoint detection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
324
+ function detectEndpoints(allFiles) {
325
+ const endpoints = [];
326
+ const srcFiles = allFiles.filter((f) => SOURCE_EXTS.has(path_1.default.extname(f)));
327
+ for (const filePath of srcFiles) {
328
+ const content = readSafe(filePath);
329
+ if (!content)
330
+ continue;
331
+ let m;
332
+ EXPRESS_RE.lastIndex = 0;
333
+ while ((m = EXPRESS_RE.exec(content)) !== null) {
334
+ endpoints.push({ method: m[1].toUpperCase(), path: m[2], deprecated: /@deprecated/.test(content), hasOpenApiSpec: false });
335
+ }
336
+ NESTJS_RE.lastIndex = 0;
337
+ while ((m = NESTJS_RE.exec(content)) !== null) {
338
+ endpoints.push({ method: m[1].toUpperCase(), path: m[2] || '/', deprecated: false, hasOpenApiSpec: false });
339
+ }
340
+ FLASK_RE.lastIndex = 0;
341
+ while ((m = FLASK_RE.exec(content)) !== null) {
342
+ endpoints.push({ method: 'GET', path: m[1], deprecated: false, hasOpenApiSpec: false });
343
+ }
344
+ FASTAPI_RE.lastIndex = 0;
345
+ while ((m = FASTAPI_RE.exec(content)) !== null) {
346
+ endpoints.push({ method: m[1].toUpperCase(), path: m[2], deprecated: false, hasOpenApiSpec: false });
347
+ }
348
+ GIN_RE.lastIndex = 0;
349
+ while ((m = GIN_RE.exec(content)) !== null) {
350
+ endpoints.push({ method: m[1].toUpperCase(), path: m[2], deprecated: false, hasOpenApiSpec: false });
351
+ }
352
+ SPRING_RE.lastIndex = 0;
353
+ while ((m = SPRING_RE.exec(content)) !== null) {
354
+ const verb = m[1].replace('Mapping', '').replace('Request', 'GET');
355
+ endpoints.push({ method: verb.toUpperCase(), path: m[2], deprecated: false, hasOpenApiSpec: false });
356
+ }
357
+ ACTIX_RE.lastIndex = 0;
358
+ while ((m = ACTIX_RE.exec(content)) !== null) {
359
+ endpoints.push({ method: m[1].toUpperCase(), path: m[2], deprecated: false, hasOpenApiSpec: false });
360
+ }
361
+ // Hapi route blocks
362
+ const routeBlocks = content.matchAll(/server\s*\.\s*route\s*\(\s*\{([^}]+)\}/gs);
363
+ for (const block of routeBlocks) {
364
+ const body = block[1];
365
+ HAPI_METHOD_RE.lastIndex = 0;
366
+ HAPI_PATH_RE.lastIndex = 0;
367
+ const methodM = HAPI_METHOD_RE.exec(body);
368
+ const pathM = HAPI_PATH_RE.exec(body);
369
+ if (methodM && pathM) {
370
+ endpoints.push({ method: methodM[1].toUpperCase(), path: pathM[1], deprecated: false, hasOpenApiSpec: false });
371
+ }
372
+ }
373
+ }
374
+ const seen = new Set();
375
+ return endpoints.filter((ep) => {
376
+ const key = `${ep.method}:${ep.path}`;
377
+ if (seen.has(key))
378
+ return false;
379
+ seen.add(key);
380
+ return true;
381
+ });
382
+ }
383
+ // \u2500\u2500 Dependency detection \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
384
+ function detectDependencies(_serviceDir, serviceId, allFiles) {
385
+ const deps = [];
386
+ const srcFiles = allFiles.filter((f) => SOURCE_EXTS.has(path_1.default.extname(f)));
387
+ for (const filePath of srcFiles) {
388
+ const content = readSafe(filePath);
389
+ if (!content)
390
+ continue;
391
+ let m;
392
+ // HTTP calls JS/TS
393
+ for (const re of [HTTP_CALL_RE, FETCH_RE]) {
394
+ re.lastIndex = 0;
395
+ while ((m = re.exec(content)) !== null) {
396
+ const rawUrl = m[1];
397
+ if (!rawUrl || rawUrl.startsWith('/'))
398
+ continue;
399
+ deps.push({ sourceId: serviceId, targetName: extractHostname(rawUrl), targetUrl: rawUrl, type: 'REST', endpoints: [rawUrl] });
400
+ }
401
+ }
402
+ // HTTP calls Python
403
+ for (const re of [REQUESTS_RE, HTTPX_RE]) {
404
+ re.lastIndex = 0;
405
+ while ((m = re.exec(content)) !== null) {
406
+ const rawUrl = m[1];
407
+ if (!rawUrl || rawUrl.startsWith('/'))
408
+ continue;
409
+ deps.push({ sourceId: serviceId, targetName: extractHostname(rawUrl), targetUrl: rawUrl, type: 'REST', endpoints: [rawUrl] });
410
+ }
411
+ }
412
+ // HTTP calls Go
413
+ GO_HTTP_RE.lastIndex = 0;
414
+ while ((m = GO_HTTP_RE.exec(content)) !== null) {
415
+ const rawUrl = m[1];
416
+ if (!rawUrl || rawUrl.startsWith('/'))
417
+ continue;
418
+ deps.push({ sourceId: serviceId, targetName: extractHostname(rawUrl), targetUrl: rawUrl, type: 'REST', endpoints: [rawUrl] });
419
+ }
420
+ // Env-var HTTP
421
+ ENV_VAR_HTTP_RE.lastIndex = 0;
422
+ while ((m = ENV_VAR_HTTP_RE.exec(content)) !== null) {
423
+ const envVarName = m[1];
424
+ const pathPart = (m[2] ?? '').split(/\$\{/)[0].split('?')[0];
425
+ deps.push({ sourceId: serviceId, targetName: `env:${envVarName}`, targetUrl: `env://${envVarName}${pathPart}`, type: 'REST', endpoints: [pathPart || '/'] });
426
+ }
427
+ // Build var map for queue resolution
428
+ const varAssigns = new Map();
429
+ const VAR_RE = /(?:const|let|var)\s+(\w+)\s*=\s*["'`]([^"'`\n]{2,80})["'`]/g;
430
+ VAR_RE.lastIndex = 0;
431
+ while ((m = VAR_RE.exec(content)) !== null)
432
+ varAssigns.set(m[1], m[2]);
433
+ // RabbitMQ
434
+ AMQP_LITERAL_RE.lastIndex = 0;
435
+ while ((m = AMQP_LITERAL_RE.exec(content)) !== null) {
436
+ if (m[1] && m[1].length > 1)
437
+ deps.push({ sourceId: serviceId, targetName: `amqp:${m[1]}`, targetUrl: `amqp://${m[1]}`, type: 'Event', endpoints: [m[1]] });
438
+ }
439
+ AMQP_VAR_RE.lastIndex = 0;
440
+ while ((m = AMQP_VAR_RE.exec(content)) !== null) {
441
+ const q = varAssigns.get(m[1]);
442
+ if (q && q.length > 1)
443
+ deps.push({ sourceId: serviceId, targetName: `amqp:${q}`, targetUrl: `amqp://${q}`, type: 'Event', endpoints: [q] });
444
+ }
445
+ AMQP_WRAPPER_RE.lastIndex = 0;
446
+ while ((m = AMQP_WRAPPER_RE.exec(content)) !== null) {
447
+ if (m[1] && m[1].length > 1)
448
+ deps.push({ sourceId: serviceId, targetName: `amqp:${m[1]}`, targetUrl: `amqp://${m[1]}`, type: 'Event', endpoints: [m[1]] });
449
+ }
450
+ // Kafka
451
+ KAFKA_PRODUCER_RE.lastIndex = 0;
452
+ while ((m = KAFKA_PRODUCER_RE.exec(content)) !== null) {
453
+ deps.push({ sourceId: serviceId, targetName: `kafka:${m[1]}`, targetUrl: `kafka://${m[1]}`, type: 'Event', endpoints: [m[1]] });
454
+ }
455
+ KAFKA_CONSUMER_RE.lastIndex = 0;
456
+ while ((m = KAFKA_CONSUMER_RE.exec(content)) !== null) {
457
+ deps.push({ sourceId: serviceId, targetName: `kafka:${m[1]}`, targetUrl: `kafka://${m[1]}`, type: 'Event', endpoints: [m[1]] });
458
+ }
459
+ // NATS
460
+ NATS_RE.lastIndex = 0;
461
+ while ((m = NATS_RE.exec(content)) !== null) {
462
+ deps.push({ sourceId: serviceId, targetName: `nats:${m[1]}`, targetUrl: `nats://${m[1]}`, type: 'Event', endpoints: [m[1]] });
463
+ }
464
+ // gRPC
465
+ GRPC_RE.lastIndex = 0;
466
+ if (GRPC_RE.test(content)) {
467
+ deps.push({ sourceId: serviceId, targetName: 'grpc-target', targetUrl: 'grpc://unknown', type: 'gRPC', endpoints: [] });
468
+ }
469
+ // DB
470
+ DB_URL_RE.lastIndex = 0;
471
+ if (DB_URL_RE.test(content)) {
472
+ deps.push({ sourceId: serviceId, targetName: 'database', targetUrl: 'db://internal', type: 'DB', endpoints: [] });
473
+ }
474
+ }
475
+ const seen = new Set();
476
+ return deps.filter((d) => {
477
+ const key = `${d.targetName}:${d.type}`;
478
+ if (seen.has(key))
479
+ return false;
480
+ seen.add(key);
481
+ return true;
482
+ });
483
+ }
484
+ function findServiceRoots(repoPath) {
485
+ const roots = [];
486
+ _findRec(repoPath, roots, 0, 5);
487
+ return roots;
488
+ }
489
+ function _findRec(dir, results, depth, maxDepth) {
490
+ if (depth > maxDepth)
491
+ return;
492
+ let entries;
493
+ try {
494
+ entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
495
+ }
496
+ catch {
497
+ return;
498
+ }
499
+ const fileNames = new Set(entries.filter((e) => e.isFile()).map((e) => e.name));
500
+ const isMonorepoRoot = MONOREPO_ROOT_MARKERS.some((m) => fileNames.has(m)) ||
501
+ (fileNames.has('package.json') && _isMonorepoPackageJson(path_1.default.join(dir, 'package.json')));
502
+ const markerFile = SERVICE_MARKERS.find((m) => fileNames.has(m));
503
+ if (markerFile && !isMonorepoRoot) {
504
+ results.push({ dir, name: _extractServiceName(dir, markerFile), markerFile });
505
+ return; // don't recurse further into a service
506
+ }
507
+ // Implicit service: directory under a workspace container that has source files
508
+ // but no explicit marker (e.g. services/evaluator with only runner.py)
509
+ const parentIsContainer = WORKSPACE_CONTAINER_NAMES.has(path_1.default.basename(path_1.default.dirname(dir)));
510
+ if (!markerFile && parentIsContainer && depth >= 2) {
511
+ const dirEntryFiles = entries.filter((e) => e.isFile()).map((e) => e.name);
512
+ const hasSourceFiles = dirEntryFiles.some((f) => SOURCE_EXTS.has(path_1.default.extname(f)) ||
513
+ f === 'Makefile' || f === 'Dockerfile');
514
+ if (hasSourceFiles) {
515
+ results.push({ dir, name: path_1.default.basename(dir), markerFile: '' });
516
+ return;
517
+ }
518
+ }
519
+ const isWorkspaceContainer = WORKSPACE_CONTAINER_NAMES.has(path_1.default.basename(dir));
520
+ for (const entry of entries) {
521
+ if (!entry.isDirectory() || SKIP_DIRS.has(entry.name))
522
+ continue;
523
+ if (depth === 0 || isMonorepoRoot || isWorkspaceContainer ||
524
+ WORKSPACE_CONTAINER_NAMES.has(entry.name)) {
525
+ _findRec(path_1.default.join(dir, entry.name), results, depth + 1, maxDepth);
526
+ }
527
+ }
528
+ }
529
+ function _isMonorepoPackageJson(pkgPath) {
530
+ try {
531
+ const pkg = JSON.parse(readSafe(pkgPath));
532
+ // Must have explicit workspaces field — `private: true` alone is NOT enough
533
+ // (many child packages in a monorepo are also marked private)
534
+ return !!(pkg.workspaces);
535
+ }
536
+ catch {
537
+ return false;
538
+ }
539
+ }
540
+ function _extractServiceName(dir, markerFile) {
541
+ const full = path_1.default.join(dir, markerFile);
542
+ if (markerFile === 'package.json') {
543
+ try {
544
+ const pkg = JSON.parse(readSafe(full));
545
+ if (pkg.name && pkg.name !== 'root' && !pkg.name.includes('workspace')) {
546
+ return pkg.name.replace(/^@[^\/]+\//, '');
547
+ }
548
+ }
549
+ catch { /* fall through */ }
550
+ }
551
+ if (markerFile === 'go.mod') {
552
+ const firstLine = readSafe(full).split('\n')[0] ?? '';
553
+ const mod = firstLine.replace(/^module\s+/, '').trim();
554
+ if (mod)
555
+ return path_1.default.basename(mod);
556
+ }
557
+ if (markerFile === 'Cargo.toml') {
558
+ const m = readSafe(full).match(/^name\s*=\s*"([^"]+)"/m);
559
+ if (m)
560
+ return m[1];
561
+ }
562
+ if (markerFile === 'pom.xml') {
563
+ const m = readSafe(full).match(/<artifactId>([^<]+)<\/artifactId>/);
564
+ if (m)
565
+ return m[1];
566
+ }
567
+ if (markerFile === 'pyproject.toml') {
568
+ const m = readSafe(full).match(/^name\s*=\s*["'\']([^"'\']+)["'\']/m);
569
+ if (m)
570
+ return m[1];
571
+ }
572
+ return path_1.default.basename(dir);
573
+ }
574
+ function detectInfrastructure(repoPath, repoUrl, gitRoot) {
575
+ const services = [];
576
+ const dependencies = [];
577
+ const allFiles = walkDir(repoPath, 8);
578
+ // Terraform
579
+ const tfFiles = allFiles.filter((f) => path_1.default.extname(f) === '.tf');
580
+ if (tfFiles.length > 0) {
581
+ const providers = new Set();
582
+ const resources = new Set();
583
+ for (const tf of tfFiles) {
584
+ const content = readSafe(tf);
585
+ for (const m of content.matchAll(/^provider\s+"([^"]+)"/gm))
586
+ providers.add(m[1]);
587
+ for (const m of content.matchAll(/^resource\s+"([^"]+)"\s+"([^"]+)"/gm))
588
+ resources.add(m[1]);
589
+ }
590
+ const providerLabel = providers.size ? [...providers].join('+') : 'terraform';
591
+ const infraId = serviceIdFromName(`infra:terraform:${repoPath}`);
592
+ const subpath = gitRoot ? path_1.default.relative(gitRoot, repoPath) : null;
593
+ services.push({
594
+ id: infraId, name: `${providerLabel}-infra`,
595
+ repoPath, repoUrl, repoSubpath: subpath,
596
+ team: null, language: 'infra',
597
+ healthScore: resources.size > 0 ? 85 : 40,
598
+ docCoverage: 50, testCoverage: 0, lastCommit: null,
599
+ endpoints: [],
600
+ rawIssues: resources.size === 0 ? ['No Terraform resources found'] : [],
601
+ });
602
+ }
603
+ // CI/CD workflows (GitHub Actions, GitLab CI, etc.)
604
+ const ciFiles = allFiles.filter((f) => {
605
+ const rel = path_1.default.relative(repoPath, f);
606
+ return /^\.github[\\/]workflows[\\/].+\.ya?ml$/.test(rel) ||
607
+ path_1.default.basename(f) === '.gitlab-ci.yml' ||
608
+ path_1.default.basename(f) === 'Jenkinsfile' ||
609
+ path_1.default.basename(f) === '.travis.yml' ||
610
+ /^\.circleci[\\/]config\.ya?ml$/.test(rel) ||
611
+ path_1.default.basename(f) === 'azure-pipelines.yml';
612
+ });
613
+ if (ciFiles.length > 0) {
614
+ const ciId = serviceIdFromName(`infra:ci-cd:${repoPath}`);
615
+ const subpath = gitRoot ? path_1.default.relative(gitRoot, repoPath) : null;
616
+ services.push({
617
+ id: ciId, name: 'ci-cd-pipelines',
618
+ repoPath, repoUrl, repoSubpath: subpath,
619
+ team: null, language: 'infra',
620
+ healthScore: 80,
621
+ docCoverage: 30, testCoverage: 0, lastCommit: null,
622
+ endpoints: ciFiles.map((f) => ({
623
+ method: 'WORKFLOW',
624
+ path: path_1.default.relative(repoPath, f),
625
+ deprecated: false,
626
+ hasOpenApiSpec: false,
627
+ })),
628
+ rawIssues: [],
629
+ });
630
+ }
631
+ // Docker Compose (repo-level)
632
+ const composeFiles = allFiles.filter((f) => {
633
+ const b = path_1.default.basename(f);
634
+ return (b === 'docker-compose.yml' || b === 'docker-compose.yaml') &&
635
+ path_1.default.dirname(f) === repoPath;
636
+ });
637
+ if (composeFiles.length > 0) {
638
+ const content = readSafe(composeFiles[0]);
639
+ const dockerServices = [];
640
+ for (const m of content.matchAll(/^\s{2}([a-z][a-z0-9_-]+):\s*$/gm)) {
641
+ dockerServices.push(m[1]);
642
+ }
643
+ if (dockerServices.length > 0) {
644
+ // Add dependency links: each docker-compose service → potential matching service
645
+ const composeId = serviceIdFromName('infra:docker-compose');
646
+ for (const ds of dockerServices) {
647
+ dependencies.push({
648
+ sourceId: composeId,
649
+ targetName: ds,
650
+ targetUrl: `docker-compose://${ds}`,
651
+ type: 'infra',
652
+ endpoints: [],
653
+ });
654
+ }
655
+ }
656
+ }
657
+ return { services, dependencies };
658
+ }
659
+ // \u2500\u2500 Source file walker for QA citations \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
660
+ function walkSourceFiles(dir, max = 200) {
661
+ if (!fs_1.default.existsSync(dir))
662
+ return [];
663
+ return walkDir(dir, 8)
664
+ .filter((f) => SOURCE_EXTS.has(path_1.default.extname(f)))
665
+ .slice(0, max)
666
+ .map((f) => path_1.default.relative(dir, f));
667
+ }
668
+ function bestFileMatch(hallucinated, realFiles) {
669
+ if (realFiles.length === 0)
670
+ return null;
671
+ const norm = hallucinated.replace(/\\/g, '/').replace(/^\.\//, '');
672
+ if (realFiles.includes(norm))
673
+ return norm;
674
+ const base = path_1.default.basename(norm);
675
+ const nameNoExt = base.replace(/\.[^.]+$/, '');
676
+ const ext = path_1.default.extname(norm);
677
+ const byBase = realFiles.find((f) => path_1.default.basename(f) === base);
678
+ if (byBase)
679
+ return byBase;
680
+ const byName = realFiles.find((f) => path_1.default.basename(f).replace(/\.[^.]+$/, '') === nameNoExt);
681
+ if (byName)
682
+ return byName;
683
+ const byContains = realFiles.find((f) => f.includes(nameNoExt) && nameNoExt.length > 3);
684
+ if (byContains)
685
+ return byContains;
686
+ const segments = norm.split('/').filter((s) => s.length > 3);
687
+ for (const seg of segments) {
688
+ const bySeg = realFiles.find((f) => f.includes(seg) && path_1.default.extname(f) === ext);
689
+ if (bySeg)
690
+ return bySeg;
691
+ }
692
+ return null;
693
+ }
694
+ // \u2500\u2500 scanMonorepo \u2014 main entry point \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
695
+ function scanMonorepo(repoPath, repoUrl, team) {
696
+ if (!fs_1.default.existsSync(repoPath))
697
+ throw new Error(`Path does not exist: ${repoPath}`);
698
+ if (!fs_1.default.statSync(repoPath).isDirectory())
699
+ throw new Error(`Not a directory: ${repoPath}`);
700
+ // Resolve symlinks (macOS /tmp → /private/tmp) so path.relative works with gitRoot
701
+ repoPath = fs_1.default.realpathSync(repoPath);
702
+ const gitInfo = detectGitInfo(repoPath);
703
+ const resolvedUrl = repoUrl ?? gitInfo.repoUrl;
704
+ const gitRoot = gitInfo.gitRoot;
705
+ let serviceRoots = findServiceRoots(repoPath);
706
+ if (serviceRoots.length === 0) {
707
+ serviceRoots = [{ dir: repoPath, name: path_1.default.basename(repoPath), markerFile: '' }];
708
+ }
709
+ const services = [];
710
+ const allDeps = [];
711
+ const allIssues = [];
712
+ for (const root of serviceRoots) {
713
+ const allFiles = walkDir(root.dir, 10);
714
+ const language = detectLanguage(allFiles);
715
+ const { score, docCoverage, testCoverage, issues } = calculateHealthScore(root.dir, allFiles, repoPath);
716
+ const endpoints = detectEndpoints(allFiles);
717
+ const serviceId = serviceIdFromName(root.name);
718
+ const deps = detectDependencies(root.dir, serviceId, allFiles);
719
+ const lastCommit = detectLastCommit(root.dir);
720
+ const subpath = gitRoot ? path_1.default.relative(gitRoot, root.dir) || null : null;
721
+ services.push({
722
+ id: serviceId, name: root.name,
723
+ repoPath: root.dir, repoUrl: resolvedUrl, repoSubpath: subpath,
724
+ team: team ?? null, language,
725
+ healthScore: score, docCoverage, testCoverage, lastCommit,
726
+ endpoints, rawIssues: issues,
727
+ });
728
+ allDeps.push(...deps);
729
+ allIssues.push(...issues.map((i) => `[${root.name}] ${i}`));
730
+ }
731
+ const infra = detectInfrastructure(repoPath, resolvedUrl ?? null, gitRoot);
732
+ services.push(...infra.services);
733
+ allDeps.push(...infra.dependencies);
734
+ const seen = new Set();
735
+ const uniqueDeps = allDeps.filter((d) => {
736
+ const key = `${d.sourceId}:${d.targetName}:${d.type}`;
737
+ if (seen.has(key))
738
+ return false;
739
+ seen.add(key);
740
+ return true;
741
+ });
742
+ return { services, dependencies: uniqueDeps, issues: allIssues };
743
+ }
744
+ // \u2500\u2500 scanRepo \u2014 legacy alias for ingestWorker \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
745
+ function scanRepo(repoPath, repoUrl, team) {
746
+ return scanMonorepo(repoPath, repoUrl, team);
747
+ }
748
+ //# sourceMappingURL=scannerService.js.map