@kb-labs/devkit 1.0.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/.cursorrules +32 -0
- package/.github/CODEOWNERS +2 -0
- package/.github/actions/setup-node-pnpm/action.yml +47 -0
- package/.github/workflow-templates/ci.yml +13 -0
- package/.github/workflow-templates/drift-check.yml +10 -0
- package/.github/workflow-templates/profiles-validate.yml +16 -0
- package/.github/workflow-templates/release.yml +8 -0
- package/.github/workflows/ci-reusable.yml +131 -0
- package/.github/workflows/drift-check-reusable.yml +23 -0
- package/.github/workflows/fixtures.yml +74 -0
- package/.github/workflows/profiles-validate-reusable.yml +67 -0
- package/.github/workflows/release-reusable.yml +50 -0
- package/.vscode/settings.json +23 -0
- package/AGENTS.md +130 -0
- package/LICENSE +21 -0
- package/README.md +1542 -0
- package/agents/devkit-maintainer/context.globs +15 -0
- package/agents/devkit-maintainer/permissions.yml +17 -0
- package/agents/devkit-maintainer/prompt.md +28 -0
- package/agents/devkit-maintainer/runbook.md +31 -0
- package/agents/docs-crafter/prompt.md +24 -0
- package/agents/docs-crafter/runbook.md +18 -0
- package/agents/release-manager/context.globs +7 -0
- package/agents/release-manager/prompt.md +27 -0
- package/agents/release-manager/runbook.md +17 -0
- package/agents/test-generator/context.globs +7 -0
- package/agents/test-generator/prompt.md +27 -0
- package/agents/test-generator/runbook.md +18 -0
- package/bin/devkit-architecture.mjs +1225 -0
- package/bin/devkit-build-order.mjs +500 -0
- package/bin/devkit-check-build-readiness.mjs +222 -0
- package/bin/devkit-check-commands.mjs +394 -0
- package/bin/devkit-check-configs.mjs +461 -0
- package/bin/devkit-check-deprecated.mjs +532 -0
- package/bin/devkit-check-duplicates.mjs +431 -0
- package/bin/devkit-check-exports.mjs +561 -0
- package/bin/devkit-check-imports.mjs +712 -0
- package/bin/devkit-check-paths.mjs +670 -0
- package/bin/devkit-check-scripts.mjs +335 -0
- package/bin/devkit-check-structure.mjs +495 -0
- package/bin/devkit-check-types.mjs +450 -0
- package/bin/devkit-ci.mjs +261 -0
- package/bin/devkit-core-gate.mjs +225 -0
- package/bin/devkit-fix-deps.mjs +1169 -0
- package/bin/devkit-freshness.mjs +199 -0
- package/bin/devkit-health.mjs +489 -0
- package/bin/devkit-migrate-configs.mjs +370 -0
- package/bin/devkit-paths.mjs +192 -0
- package/bin/devkit-stats.mjs +453 -0
- package/bin/devkit-sync.mjs +12 -0
- package/bin/devkit-tsup-external.mjs +148 -0
- package/bin/devkit-types-audit.mjs +615 -0
- package/bin/devkit-types-order.mjs +637 -0
- package/bin/devkit-validate-naming.mjs +203 -0
- package/bin/devkit-visualize.mjs +452 -0
- package/bin/kb-devkit-qa-history.mjs +453 -0
- package/bin/kb-devkit-qa.mjs +915 -0
- package/eslint/node.js +134 -0
- package/eslint/react.js +260 -0
- package/package.json +186 -0
- package/prettier/index.json +10 -0
- package/sync/index.mjs +693 -0
- package/templates/configs/README.md +306 -0
- package/templates/configs/eslint.config.js +27 -0
- package/templates/configs/package.json.bin +25 -0
- package/templates/configs/package.json.lib +30 -0
- package/templates/configs/tsconfig.build.json +15 -0
- package/templates/configs/tsconfig.json +9 -0
- package/templates/configs/tsup.config.bin.ts +34 -0
- package/templates/configs/tsup.config.cli.ts +41 -0
- package/templates/configs/tsup.config.dual.ts +46 -0
- package/templates/configs/tsup.config.ts +36 -0
- package/templates/package-json/README.md +290 -0
- package/templates/package-json/package.json.bin.template +40 -0
- package/templates/package-json/package.json.template +45 -0
- package/tsconfig/base.json +21 -0
- package/tsconfig/cli.json +12 -0
- package/tsconfig/dist/__tests__/cache.spec.d.ts +6 -0
- package/tsconfig/dist/__tests__/cache.spec.d.ts.map +1 -0
- package/tsconfig/dist/__tests__/cache.spec.js +85 -0
- package/tsconfig/dist/__tests__/cache.spec.js.map +1 -0
- package/tsconfig/dist/__tests__/fs-atomic.spec.d.ts +6 -0
- package/tsconfig/dist/__tests__/fs-atomic.spec.d.ts.map +1 -0
- package/tsconfig/dist/__tests__/fs-atomic.spec.js +153 -0
- package/tsconfig/dist/__tests__/fs-atomic.spec.js.map +1 -0
- package/tsconfig/dist/__tests__/init-workspace.spec.d.ts +6 -0
- package/tsconfig/dist/__tests__/init-workspace.spec.d.ts.map +1 -0
- package/tsconfig/dist/__tests__/init-workspace.spec.js +99 -0
- package/tsconfig/dist/__tests__/init-workspace.spec.js.map +1 -0
- package/tsconfig/dist/__tests__/kb-error.spec.d.ts +6 -0
- package/tsconfig/dist/__tests__/kb-error.spec.d.ts.map +1 -0
- package/tsconfig/dist/__tests__/kb-error.spec.js +190 -0
- package/tsconfig/dist/__tests__/kb-error.spec.js.map +1 -0
- package/tsconfig/dist/__tests__/preset-lockfile.spec.d.ts +6 -0
- package/tsconfig/dist/__tests__/preset-lockfile.spec.d.ts.map +1 -0
- package/tsconfig/dist/__tests__/preset-lockfile.spec.js +142 -0
- package/tsconfig/dist/__tests__/preset-lockfile.spec.js.map +1 -0
- package/tsconfig/dist/__tests__/product-config-profiles.spec.d.ts +6 -0
- package/tsconfig/dist/__tests__/product-config-profiles.spec.d.ts.map +1 -0
- package/tsconfig/dist/__tests__/product-config-profiles.spec.js +100 -0
- package/tsconfig/dist/__tests__/product-config-profiles.spec.js.map +1 -0
- package/tsconfig/dist/__tests__/product-config.spec.d.ts +6 -0
- package/tsconfig/dist/__tests__/product-config.spec.d.ts.map +1 -0
- package/tsconfig/dist/__tests__/product-config.spec.js +298 -0
- package/tsconfig/dist/__tests__/product-config.spec.js.map +1 -0
- package/tsconfig/dist/__tests__/runtime.spec.d.ts +2 -0
- package/tsconfig/dist/__tests__/runtime.spec.d.ts.map +1 -0
- package/tsconfig/dist/__tests__/runtime.spec.js +127 -0
- package/tsconfig/dist/__tests__/runtime.spec.js.map +1 -0
- package/tsconfig/dist/__tests__/upsert-lockfile.spec.d.ts +6 -0
- package/tsconfig/dist/__tests__/upsert-lockfile.spec.d.ts.map +1 -0
- package/tsconfig/dist/__tests__/upsert-lockfile.spec.js +251 -0
- package/tsconfig/dist/__tests__/upsert-lockfile.spec.js.map +1 -0
- package/tsconfig/dist/__tests__/validate-config.spec.d.ts +2 -0
- package/tsconfig/dist/__tests__/validate-config.spec.d.ts.map +1 -0
- package/tsconfig/dist/__tests__/validate-config.spec.js +14 -0
- package/tsconfig/dist/__tests__/validate-config.spec.js.map +1 -0
- package/tsconfig/dist/api/init-workspace.d.ts +10 -0
- package/tsconfig/dist/api/init-workspace.d.ts.map +1 -0
- package/tsconfig/dist/api/init-workspace.js +191 -0
- package/tsconfig/dist/api/init-workspace.js.map +1 -0
- package/tsconfig/dist/api/product-config.d.ts +21 -0
- package/tsconfig/dist/api/product-config.d.ts.map +1 -0
- package/tsconfig/dist/api/product-config.js +192 -0
- package/tsconfig/dist/api/product-config.js.map +1 -0
- package/tsconfig/dist/api/read-config.d.ts +22 -0
- package/tsconfig/dist/api/read-config.d.ts.map +1 -0
- package/tsconfig/dist/api/read-config.js +105 -0
- package/tsconfig/dist/api/read-config.js.map +1 -0
- package/tsconfig/dist/api/upsert-lockfile.d.ts +10 -0
- package/tsconfig/dist/api/upsert-lockfile.d.ts.map +1 -0
- package/tsconfig/dist/api/upsert-lockfile.js +63 -0
- package/tsconfig/dist/api/upsert-lockfile.js.map +1 -0
- package/tsconfig/dist/cache/fs-cache.d.ts +38 -0
- package/tsconfig/dist/cache/fs-cache.d.ts.map +1 -0
- package/tsconfig/dist/cache/fs-cache.js +142 -0
- package/tsconfig/dist/cache/fs-cache.js.map +1 -0
- package/tsconfig/dist/errors/kb-error.d.ts +32 -0
- package/tsconfig/dist/errors/kb-error.d.ts.map +1 -0
- package/tsconfig/dist/errors/kb-error.js +54 -0
- package/tsconfig/dist/errors/kb-error.js.map +1 -0
- package/tsconfig/dist/fs/__tests__/fs.spec.d.ts +2 -0
- package/tsconfig/dist/fs/__tests__/fs.spec.d.ts.map +1 -0
- package/tsconfig/dist/fs/__tests__/fs.spec.js +22 -0
- package/tsconfig/dist/fs/__tests__/fs.spec.js.map +1 -0
- package/tsconfig/dist/fs/fs.d.ts +6 -0
- package/tsconfig/dist/fs/fs.d.ts.map +1 -0
- package/tsconfig/dist/fs/fs.js +12 -0
- package/tsconfig/dist/fs/fs.js.map +1 -0
- package/tsconfig/dist/fs/index.d.ts +2 -0
- package/tsconfig/dist/fs/index.d.ts.map +1 -0
- package/tsconfig/dist/fs/index.js +2 -0
- package/tsconfig/dist/fs/index.js.map +1 -0
- package/tsconfig/dist/hash/config-hash.d.ts +17 -0
- package/tsconfig/dist/hash/config-hash.d.ts.map +1 -0
- package/tsconfig/dist/hash/config-hash.js +55 -0
- package/tsconfig/dist/hash/config-hash.js.map +1 -0
- package/tsconfig/dist/index.d.ts +5 -0
- package/tsconfig/dist/index.d.ts.map +1 -0
- package/tsconfig/dist/index.js +5 -0
- package/tsconfig/dist/index.js.map +1 -0
- package/tsconfig/dist/lockfile/lockfile.d.ts +54 -0
- package/tsconfig/dist/lockfile/lockfile.d.ts.map +1 -0
- package/tsconfig/dist/lockfile/lockfile.js +141 -0
- package/tsconfig/dist/lockfile/lockfile.js.map +1 -0
- package/tsconfig/dist/logging/__tests__/logger.spec.d.ts +2 -0
- package/tsconfig/dist/logging/__tests__/logger.spec.d.ts.map +1 -0
- package/tsconfig/dist/logging/__tests__/logger.spec.js +65 -0
- package/tsconfig/dist/logging/__tests__/logger.spec.js.map +1 -0
- package/tsconfig/dist/logging/__tests__/redaction.spec.d.ts +2 -0
- package/tsconfig/dist/logging/__tests__/redaction.spec.d.ts.map +1 -0
- package/tsconfig/dist/logging/__tests__/redaction.spec.js +34 -0
- package/tsconfig/dist/logging/__tests__/redaction.spec.js.map +1 -0
- package/tsconfig/dist/logging/__tests__/sinks-and-env.spec.d.ts +2 -0
- package/tsconfig/dist/logging/__tests__/sinks-and-env.spec.d.ts.map +1 -0
- package/tsconfig/dist/logging/__tests__/sinks-and-env.spec.js +90 -0
- package/tsconfig/dist/logging/__tests__/sinks-and-env.spec.js.map +1 -0
- package/tsconfig/dist/logging/index.d.ts +6 -0
- package/tsconfig/dist/logging/index.d.ts.map +1 -0
- package/tsconfig/dist/logging/index.js +6 -0
- package/tsconfig/dist/logging/index.js.map +1 -0
- package/tsconfig/dist/logging/logger.d.ts +9 -0
- package/tsconfig/dist/logging/logger.d.ts.map +1 -0
- package/tsconfig/dist/logging/logger.js +101 -0
- package/tsconfig/dist/logging/logger.js.map +1 -0
- package/tsconfig/dist/logging/redaction.d.ts +7 -0
- package/tsconfig/dist/logging/redaction.d.ts.map +1 -0
- package/tsconfig/dist/logging/redaction.js +23 -0
- package/tsconfig/dist/logging/redaction.js.map +1 -0
- package/tsconfig/dist/logging/sinks/json.d.ts +4 -0
- package/tsconfig/dist/logging/sinks/json.d.ts.map +1 -0
- package/tsconfig/dist/logging/sinks/json.js +23 -0
- package/tsconfig/dist/logging/sinks/json.js.map +1 -0
- package/tsconfig/dist/logging/sinks/stdout.d.ts +3 -0
- package/tsconfig/dist/logging/sinks/stdout.d.ts.map +1 -0
- package/tsconfig/dist/logging/sinks/stdout.js +24 -0
- package/tsconfig/dist/logging/sinks/stdout.js.map +1 -0
- package/tsconfig/dist/logging/types/index.d.ts +2 -0
- package/tsconfig/dist/logging/types/index.d.ts.map +1 -0
- package/tsconfig/dist/logging/types/index.js +2 -0
- package/tsconfig/dist/logging/types/index.js.map +1 -0
- package/tsconfig/dist/logging/types/types.d.ts +37 -0
- package/tsconfig/dist/logging/types/types.d.ts.map +1 -0
- package/tsconfig/dist/logging/types/types.js +2 -0
- package/tsconfig/dist/logging/types/types.js.map +1 -0
- package/tsconfig/dist/merge/layered-merge.d.ts +16 -0
- package/tsconfig/dist/merge/layered-merge.d.ts.map +1 -0
- package/tsconfig/dist/merge/layered-merge.js +97 -0
- package/tsconfig/dist/merge/layered-merge.js.map +1 -0
- package/tsconfig/dist/preset/resolve-preset.d.ts +29 -0
- package/tsconfig/dist/preset/resolve-preset.d.ts.map +1 -0
- package/tsconfig/dist/preset/resolve-preset.js +104 -0
- package/tsconfig/dist/preset/resolve-preset.js.map +1 -0
- package/tsconfig/dist/repo/__tests__/repo.spec.d.ts +2 -0
- package/tsconfig/dist/repo/__tests__/repo.spec.d.ts.map +1 -0
- package/tsconfig/dist/repo/__tests__/repo.spec.js +25 -0
- package/tsconfig/dist/repo/__tests__/repo.spec.js.map +1 -0
- package/tsconfig/dist/repo/index.d.ts +2 -0
- package/tsconfig/dist/repo/index.d.ts.map +1 -0
- package/tsconfig/dist/repo/index.js +2 -0
- package/tsconfig/dist/repo/index.js.map +1 -0
- package/tsconfig/dist/repo/repo.d.ts +6 -0
- package/tsconfig/dist/repo/repo.d.ts.map +1 -0
- package/tsconfig/dist/repo/repo.js +25 -0
- package/tsconfig/dist/repo/repo.js.map +1 -0
- package/tsconfig/dist/runtime/index.d.ts +2 -0
- package/tsconfig/dist/runtime/index.d.ts.map +1 -0
- package/tsconfig/dist/runtime/index.js +2 -0
- package/tsconfig/dist/runtime/index.js.map +1 -0
- package/tsconfig/dist/runtime/runtime.d.ts +46 -0
- package/tsconfig/dist/runtime/runtime.d.ts.map +1 -0
- package/tsconfig/dist/runtime/runtime.js +126 -0
- package/tsconfig/dist/runtime/runtime.js.map +1 -0
- package/tsconfig/dist/tsconfig.tools.tsbuildinfo +1 -0
- package/tsconfig/dist/tsconfig.tsbuildinfo +1 -0
- package/tsconfig/dist/types/index.d.ts +2 -0
- package/tsconfig/dist/types/index.d.ts.map +1 -0
- package/tsconfig/dist/types/index.js +2 -0
- package/tsconfig/dist/types/index.js.map +1 -0
- package/tsconfig/dist/types/init.d.ts +34 -0
- package/tsconfig/dist/types/init.d.ts.map +1 -0
- package/tsconfig/dist/types/init.js +6 -0
- package/tsconfig/dist/types/init.js.map +1 -0
- package/tsconfig/dist/types/preset.d.ts +27 -0
- package/tsconfig/dist/types/preset.d.ts.map +1 -0
- package/tsconfig/dist/types/preset.js +6 -0
- package/tsconfig/dist/types/preset.js.map +1 -0
- package/tsconfig/dist/types/types.d.ts +6 -0
- package/tsconfig/dist/types/types.d.ts.map +1 -0
- package/tsconfig/dist/types/types.js +2 -0
- package/tsconfig/dist/types/types.js.map +1 -0
- package/tsconfig/dist/utils/__tests__/env.spec.d.ts +2 -0
- package/tsconfig/dist/utils/__tests__/env.spec.d.ts.map +1 -0
- package/tsconfig/dist/utils/__tests__/env.spec.js +33 -0
- package/tsconfig/dist/utils/__tests__/env.spec.js.map +1 -0
- package/tsconfig/dist/utils/env.d.ts +7 -0
- package/tsconfig/dist/utils/env.d.ts.map +1 -0
- package/tsconfig/dist/utils/env.js +25 -0
- package/tsconfig/dist/utils/env.js.map +1 -0
- package/tsconfig/dist/utils/fs-atomic.d.ts +15 -0
- package/tsconfig/dist/utils/fs-atomic.d.ts.map +1 -0
- package/tsconfig/dist/utils/fs-atomic.js +45 -0
- package/tsconfig/dist/utils/fs-atomic.js.map +1 -0
- package/tsconfig/dist/utils/index.d.ts +3 -0
- package/tsconfig/dist/utils/index.d.ts.map +1 -0
- package/tsconfig/dist/utils/index.js +3 -0
- package/tsconfig/dist/utils/index.js.map +1 -0
- package/tsconfig/dist/utils/paths.d.ts +21 -0
- package/tsconfig/dist/utils/paths.d.ts.map +1 -0
- package/tsconfig/dist/utils/paths.js +32 -0
- package/tsconfig/dist/utils/paths.js.map +1 -0
- package/tsconfig/dist/utils/product-normalize.d.ts +27 -0
- package/tsconfig/dist/utils/product-normalize.d.ts.map +1 -0
- package/tsconfig/dist/utils/product-normalize.js +45 -0
- package/tsconfig/dist/utils/product-normalize.js.map +1 -0
- package/tsconfig/dist/validation/validate-config.d.ts +7 -0
- package/tsconfig/dist/validation/validate-config.d.ts.map +1 -0
- package/tsconfig/dist/validation/validate-config.js +22 -0
- package/tsconfig/dist/validation/validate-config.js.map +1 -0
- package/tsconfig/lib.json +13 -0
- package/tsconfig/node.json +12 -0
- package/tsconfig/react-app.json +8 -0
- package/tsconfig/react-lib.json +8 -0
- package/tsconfig/test.json +15 -0
- package/tsup/bin.js +172 -0
- package/tsup/dual.js +155 -0
- package/tsup/external-sync.mjs +41 -0
- package/tsup/external.mjs +109 -0
- package/tsup/node.js +65 -0
- package/tsup/react-lib.js +18 -0
- package/tsup/sdk.js +118 -0
- package/vite/react-app.js +50 -0
- package/vitest/node.js +37 -0
- package/vitest/react.js +39 -0
- package/vitest/vitest-setup.ts +9 -0
|
@@ -0,0 +1,1225 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @kb-labs/devkit - Architecture Audit Tool (Tier 1+)
|
|
5
|
+
*
|
|
6
|
+
* Analyzes monorepo architecture and generates:
|
|
7
|
+
* 1. AI-first JSON output (complete structured data for LLM consumption)
|
|
8
|
+
* 2. Human-first visual graph (interactive HTML with Cytoscape.js)
|
|
9
|
+
* 3. Human-first markdown report (executive summary with recommendations)
|
|
10
|
+
*
|
|
11
|
+
* Features:
|
|
12
|
+
* - Automated anomaly detection (10 types):
|
|
13
|
+
* ⢠Circular dependencies (score: 100)
|
|
14
|
+
* ⢠Layer violations (score: 90) - infrastructure depends on plugin/feature
|
|
15
|
+
* ⢠God packages (score: 80) - >15 dependents
|
|
16
|
+
* ⢠Unstable core (score: 75) - core/infra with instability >0.7
|
|
17
|
+
* ⢠Bidirectional dependencies (score: 70) - AāB && BāA (not circular)
|
|
18
|
+
* ⢠Code smell: Large packages (score: 60) - >10K LOC
|
|
19
|
+
* ⢠Orphan packages (score: 60) - 0 dependents
|
|
20
|
+
* ⢠Code smell: Many dependencies (score: 50) - >10 deps
|
|
21
|
+
* ⢠Deep chains (score: 50) - depth >7
|
|
22
|
+
* ⢠Code smell: No docs (score: 40) - missing README
|
|
23
|
+
* - Heuristic scoring (prioritize top 10 issues by impact)
|
|
24
|
+
* - Metrics calculation (coupling, instability, centrality, depth)
|
|
25
|
+
* - Layer inference (Infrastructure ā Core ā Plugin ā Feature ā UI)
|
|
26
|
+
* - Trend analysis (compare with previous runs, track improvements)
|
|
27
|
+
* - Dual output (AI-readable JSON + Human-readable graph/report)
|
|
28
|
+
*
|
|
29
|
+
* Usage:
|
|
30
|
+
* kb-devkit-architecture # Generate all outputs + trends
|
|
31
|
+
* kb-devkit-architecture --ai # Generate only JSON (stdout)
|
|
32
|
+
* kb-devkit-architecture --human # Generate only graph + report
|
|
33
|
+
* kb-devkit-architecture --format=json # JSON to stdout
|
|
34
|
+
* kb-devkit-architecture --format=md # Markdown report
|
|
35
|
+
* kb-devkit-architecture --format=html # HTML graph (TODO)
|
|
36
|
+
* kb-devkit-architecture --open # Open graph in browser (TODO)
|
|
37
|
+
* kb-devkit-architecture --layer=core # Filter by layer
|
|
38
|
+
* kb-devkit-architecture --threshold=70 # Show anomalies >= 70 score
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import fs from 'node:fs';
|
|
42
|
+
import path from 'node:path';
|
|
43
|
+
import { fileURLToPath } from 'node:url';
|
|
44
|
+
|
|
45
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
46
|
+
|
|
47
|
+
// ANSI colors
|
|
48
|
+
const colors = {
|
|
49
|
+
reset: '\x1b[0m',
|
|
50
|
+
red: '\x1b[31m',
|
|
51
|
+
green: '\x1b[32m',
|
|
52
|
+
yellow: '\x1b[33m',
|
|
53
|
+
blue: '\x1b[34m',
|
|
54
|
+
magenta: '\x1b[35m',
|
|
55
|
+
cyan: '\x1b[36m',
|
|
56
|
+
gray: '\x1b[90m',
|
|
57
|
+
bold: '\x1b[1m',
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
function log(message, color = 'reset') {
|
|
61
|
+
if (!options.json && options.format !== 'json') {
|
|
62
|
+
console.log(`${colors[color]}${message}${colors.reset}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Parse CLI arguments
|
|
67
|
+
const args = process.argv.slice(2);
|
|
68
|
+
const options = {
|
|
69
|
+
ai: args.includes('--ai'),
|
|
70
|
+
human: args.includes('--human'),
|
|
71
|
+
open: args.includes('--open'),
|
|
72
|
+
layer: args.find((arg) => arg.startsWith('--layer='))?.split('=')[1],
|
|
73
|
+
threshold: parseInt(args.find((arg) => arg.startsWith('--threshold='))?.split('=')[1] || '0'),
|
|
74
|
+
format: args.find((arg) => arg.startsWith('--format='))?.split('=')[1] || 'all',
|
|
75
|
+
json: args.includes('--json'),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// ============================
|
|
79
|
+
// Phase 1: Data Collection
|
|
80
|
+
// ============================
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Find all packages in monorepo
|
|
84
|
+
*/
|
|
85
|
+
function findPackages(rootDir) {
|
|
86
|
+
const packages = [];
|
|
87
|
+
const entries = fs.readdirSync(rootDir, { withFileTypes: true });
|
|
88
|
+
|
|
89
|
+
for (const entry of entries) {
|
|
90
|
+
if (!entry.isDirectory() || !entry.name.startsWith('kb-labs-')) {continue;}
|
|
91
|
+
|
|
92
|
+
const repoPath = path.join(rootDir, entry.name);
|
|
93
|
+
const packagesDir = path.join(repoPath, 'packages');
|
|
94
|
+
|
|
95
|
+
if (!fs.existsSync(packagesDir)) {continue;}
|
|
96
|
+
|
|
97
|
+
const packageDirs = fs.readdirSync(packagesDir, { withFileTypes: true });
|
|
98
|
+
|
|
99
|
+
for (const pkgDir of packageDirs) {
|
|
100
|
+
if (!pkgDir.isDirectory()) {continue;}
|
|
101
|
+
|
|
102
|
+
const packageJsonPath = path.join(packagesDir, pkgDir.name, 'package.json');
|
|
103
|
+
|
|
104
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
105
|
+
packages.push({
|
|
106
|
+
path: packageJsonPath,
|
|
107
|
+
dir: path.join(packagesDir, pkgDir.name),
|
|
108
|
+
repository: entry.name,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return packages;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Calculate package size (LOC, file count)
|
|
119
|
+
*/
|
|
120
|
+
function calculatePackageSize(packageDir) {
|
|
121
|
+
const srcDir = path.join(packageDir, 'src');
|
|
122
|
+
|
|
123
|
+
if (!fs.existsSync(srcDir)) {
|
|
124
|
+
return { fileCount: 0, linesOfCode: 0 };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
let fileCount = 0;
|
|
128
|
+
let linesOfCode = 0;
|
|
129
|
+
|
|
130
|
+
function walk(dir) {
|
|
131
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
132
|
+
|
|
133
|
+
for (const entry of entries) {
|
|
134
|
+
const fullPath = path.join(dir, entry.name);
|
|
135
|
+
|
|
136
|
+
if (entry.isDirectory()) {
|
|
137
|
+
walk(fullPath);
|
|
138
|
+
} else if (entry.isFile() && /\.(ts|tsx|js|jsx)$/.test(entry.name)) {
|
|
139
|
+
fileCount++;
|
|
140
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
141
|
+
linesOfCode += content.split('\n').length;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
walk(srcDir);
|
|
147
|
+
|
|
148
|
+
return { fileCount, linesOfCode };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Infer layer from package name
|
|
153
|
+
*/
|
|
154
|
+
function inferLayer(packageName) {
|
|
155
|
+
if (!packageName) {return 'unknown';}
|
|
156
|
+
|
|
157
|
+
// Extract meaningful part after @kb-labs/
|
|
158
|
+
const name = packageName.replace('@kb-labs/', '');
|
|
159
|
+
|
|
160
|
+
// Infrastructure layer
|
|
161
|
+
if (name.startsWith('core-') || name.startsWith('shared-')) {
|
|
162
|
+
return 'infrastructure';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Core/Platform layer
|
|
166
|
+
if (name.startsWith('plugin-') || name.startsWith('cli-') || name.startsWith('workflow-')) {
|
|
167
|
+
return 'core';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// AI/Intelligence layer (plugins)
|
|
171
|
+
if (name.startsWith('mind-') || name.startsWith('knowledge-') || name.startsWith('analytics-')) {
|
|
172
|
+
return 'plugin';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Feature layer
|
|
176
|
+
if (name.startsWith('ai-') || name.startsWith('audit-') || name.startsWith('devlink-')) {
|
|
177
|
+
return 'feature';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// UI layer
|
|
181
|
+
if (name.startsWith('studio-') || name.startsWith('rest-api-')) {
|
|
182
|
+
return 'ui';
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return 'unknown';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Build dependency graph
|
|
190
|
+
*/
|
|
191
|
+
function buildDependencyGraph(packages) {
|
|
192
|
+
const graph = new Map(); // package -> {dependencies, dependents, metadata}
|
|
193
|
+
const packageData = new Map(); // package -> full data
|
|
194
|
+
|
|
195
|
+
// First pass: collect all packages
|
|
196
|
+
for (const pkg of packages) {
|
|
197
|
+
const packageJson = JSON.parse(fs.readFileSync(pkg.path, 'utf-8'));
|
|
198
|
+
const packageName = packageJson.name;
|
|
199
|
+
|
|
200
|
+
if (!packageName || !packageName.startsWith('@kb-labs/')) {continue;}
|
|
201
|
+
|
|
202
|
+
const size = calculatePackageSize(pkg.dir);
|
|
203
|
+
const layer = inferLayer(packageName);
|
|
204
|
+
|
|
205
|
+
const metadata = {
|
|
206
|
+
name: packageName,
|
|
207
|
+
version: packageJson.version || '0.0.0',
|
|
208
|
+
description: packageJson.description || '',
|
|
209
|
+
path: pkg.dir,
|
|
210
|
+
repository: pkg.repository,
|
|
211
|
+
layer,
|
|
212
|
+
size,
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
graph.set(packageName, {
|
|
216
|
+
dependencies: [],
|
|
217
|
+
dependents: [],
|
|
218
|
+
metadata,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
packageData.set(packageName, { packageJson, metadata });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Second pass: build dependency relationships
|
|
225
|
+
for (const [packageName, data] of packageData.entries()) {
|
|
226
|
+
const allDeps = {
|
|
227
|
+
...data.packageJson.dependencies,
|
|
228
|
+
...data.packageJson.devDependencies,
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
for (const dep of Object.keys(allDeps)) {
|
|
232
|
+
if (dep.startsWith('@kb-labs/') && graph.has(dep)) {
|
|
233
|
+
graph.get(packageName).dependencies.push(dep);
|
|
234
|
+
graph.get(dep).dependents.push(packageName);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return { graph, packageData };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ============================
|
|
243
|
+
// Phase 2: Metrics Calculation
|
|
244
|
+
// ============================
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Calculate coupling metrics for each package
|
|
248
|
+
*/
|
|
249
|
+
function calculateMetrics(graph) {
|
|
250
|
+
const metrics = new Map();
|
|
251
|
+
|
|
252
|
+
for (const [packageName, data] of graph.entries()) {
|
|
253
|
+
const afferentCoupling = data.dependents.length; // incoming dependencies (Ca)
|
|
254
|
+
const efferentCoupling = data.dependencies.length; // outgoing dependencies (Ce)
|
|
255
|
+
|
|
256
|
+
// Instability: I = Ce / (Ce + Ca), where 0 = stable, 1 = unstable
|
|
257
|
+
const totalCoupling = efferentCoupling + afferentCoupling;
|
|
258
|
+
const instability = totalCoupling === 0 ? 0 : efferentCoupling / totalCoupling;
|
|
259
|
+
|
|
260
|
+
// Centrality: how important is this package (simple version: number of dependents)
|
|
261
|
+
const centrality = afferentCoupling / Math.max(1, graph.size);
|
|
262
|
+
|
|
263
|
+
// Depth: distance from root (packages with no dependencies)
|
|
264
|
+
const depth = calculateDepth(packageName, graph, new Set());
|
|
265
|
+
|
|
266
|
+
metrics.set(packageName, {
|
|
267
|
+
afferentCoupling,
|
|
268
|
+
efferentCoupling,
|
|
269
|
+
instability,
|
|
270
|
+
centrality,
|
|
271
|
+
depth,
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return metrics;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Calculate depth of package in dependency tree
|
|
280
|
+
*/
|
|
281
|
+
function calculateDepth(packageName, graph, visited) {
|
|
282
|
+
if (visited.has(packageName)) {return 0;} // circular dependency
|
|
283
|
+
visited.add(packageName);
|
|
284
|
+
|
|
285
|
+
const deps = graph.get(packageName).dependencies;
|
|
286
|
+
if (deps.length === 0) {return 0;}
|
|
287
|
+
|
|
288
|
+
let maxDepth = 0;
|
|
289
|
+
for (const dep of deps) {
|
|
290
|
+
const depthOfDep = calculateDepth(dep, graph, new Set(visited));
|
|
291
|
+
maxDepth = Math.max(maxDepth, depthOfDep + 1);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return maxDepth;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ============================
|
|
298
|
+
// Phase 3: Anomaly Detection
|
|
299
|
+
// ============================
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Detect circular dependencies using DFS
|
|
303
|
+
*/
|
|
304
|
+
function detectCircularDependencies(graph) {
|
|
305
|
+
const cycles = [];
|
|
306
|
+
const visited = new Set();
|
|
307
|
+
const recStack = new Set();
|
|
308
|
+
const path = [];
|
|
309
|
+
|
|
310
|
+
function dfs(node) {
|
|
311
|
+
visited.add(node);
|
|
312
|
+
recStack.add(node);
|
|
313
|
+
path.push(node);
|
|
314
|
+
|
|
315
|
+
const deps = graph.get(node).dependencies;
|
|
316
|
+
|
|
317
|
+
for (const dep of deps) {
|
|
318
|
+
if (!visited.has(dep)) {
|
|
319
|
+
if (dfs(dep)) {return true;}
|
|
320
|
+
} else if (recStack.has(dep)) {
|
|
321
|
+
// Found a cycle
|
|
322
|
+
const cycleStart = path.indexOf(dep);
|
|
323
|
+
const cycle = path.slice(cycleStart).concat([dep]);
|
|
324
|
+
cycles.push(cycle);
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
recStack.delete(node);
|
|
330
|
+
path.pop();
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
for (const node of graph.keys()) {
|
|
335
|
+
if (!visited.has(node)) {
|
|
336
|
+
dfs(node);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Deduplicate cycles
|
|
341
|
+
const uniqueCycles = [];
|
|
342
|
+
const seen = new Set();
|
|
343
|
+
|
|
344
|
+
for (const cycle of cycles) {
|
|
345
|
+
const normalized = cycle.slice(0, -1).sort().join('ā');
|
|
346
|
+
if (!seen.has(normalized)) {
|
|
347
|
+
seen.add(normalized);
|
|
348
|
+
uniqueCycles.push(cycle);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return uniqueCycles;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Detect architectural anomalies
|
|
357
|
+
*/
|
|
358
|
+
function detectAnomalies(graph, metrics) {
|
|
359
|
+
const anomalies = [];
|
|
360
|
+
let anomalyId = 0;
|
|
361
|
+
|
|
362
|
+
// 1. Circular dependencies (score: 100)
|
|
363
|
+
const cycles = detectCircularDependencies(graph);
|
|
364
|
+
for (const cycle of cycles) {
|
|
365
|
+
anomalies.push({
|
|
366
|
+
id: `cycle-${++anomalyId}`,
|
|
367
|
+
type: 'circular-dependency',
|
|
368
|
+
severity: 'critical',
|
|
369
|
+
score: 100,
|
|
370
|
+
packages: cycle.slice(0, -1), // remove duplicate last element
|
|
371
|
+
cycle,
|
|
372
|
+
impact: 'Blocks build order calculation',
|
|
373
|
+
affectedPackages: cycle.length - 1,
|
|
374
|
+
recommendation: `Extract shared types/interfaces to a new contracts package`,
|
|
375
|
+
estimatedEffort: '2-4 hours',
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// 2. God packages (score: 80, threshold: >15 dependents)
|
|
380
|
+
for (const [packageName, data] of graph.entries()) {
|
|
381
|
+
const Ca = metrics.get(packageName).afferentCoupling;
|
|
382
|
+
if (Ca > 15) {
|
|
383
|
+
anomalies.push({
|
|
384
|
+
id: `god-${++anomalyId}`,
|
|
385
|
+
type: 'god-package',
|
|
386
|
+
severity: 'high',
|
|
387
|
+
score: 80,
|
|
388
|
+
package: packageName,
|
|
389
|
+
dependents: Ca,
|
|
390
|
+
impact: `Changes ripple through ${Ca} packages (${((Ca / graph.size) * 100).toFixed(1)}% of codebase)`,
|
|
391
|
+
recommendation: `Split into smaller packages (e.g., ${packageName.split('/')[1]}-types, ${packageName.split('/')[1]}-impl)`,
|
|
392
|
+
estimatedEffort: '4-8 hours',
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// 3. Orphan packages (score: 60, threshold: 0 dependents, not CLI/plugin)
|
|
398
|
+
for (const [packageName, data] of graph.entries()) {
|
|
399
|
+
const Ca = metrics.get(packageName).afferentCoupling;
|
|
400
|
+
const name = packageName.replace('@kb-labs/', '');
|
|
401
|
+
|
|
402
|
+
// Skip expected orphans (CLI entry points, plugins)
|
|
403
|
+
const isExpectedOrphan =
|
|
404
|
+
name.endsWith('-cli') ||
|
|
405
|
+
name.endsWith('-plugin') ||
|
|
406
|
+
name.endsWith('-bin') ||
|
|
407
|
+
name.startsWith('rest-api-') ||
|
|
408
|
+
name.startsWith('studio-') ||
|
|
409
|
+
name.startsWith('playbooks-');
|
|
410
|
+
|
|
411
|
+
if (Ca === 0 && !isExpectedOrphan) {
|
|
412
|
+
anomalies.push({
|
|
413
|
+
id: `orphan-${++anomalyId}`,
|
|
414
|
+
type: 'orphan-package',
|
|
415
|
+
severity: 'medium',
|
|
416
|
+
score: 60,
|
|
417
|
+
package: packageName,
|
|
418
|
+
impact: 'No other package depends on this (potential dead code)',
|
|
419
|
+
recommendation: 'Review if package is still needed or should be removed',
|
|
420
|
+
estimatedEffort: '1-2 hours',
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// 4. Unstable core packages (score: 75, threshold: layer=infrastructure && I>0.7)
|
|
426
|
+
for (const [packageName, data] of graph.entries()) {
|
|
427
|
+
const layer = data.metadata.layer;
|
|
428
|
+
const I = metrics.get(packageName).instability;
|
|
429
|
+
|
|
430
|
+
if ((layer === 'infrastructure' || layer === 'core') && I > 0.7) {
|
|
431
|
+
anomalies.push({
|
|
432
|
+
id: `unstable-${++anomalyId}`,
|
|
433
|
+
type: 'unstable-core',
|
|
434
|
+
severity: 'high',
|
|
435
|
+
score: 75,
|
|
436
|
+
package: packageName,
|
|
437
|
+
instability: I.toFixed(2),
|
|
438
|
+
impact: 'Core package is unstable (high efferent coupling)',
|
|
439
|
+
recommendation: 'Reduce dependencies or extract to separate package',
|
|
440
|
+
estimatedEffort: '2-4 hours',
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// 5. Deep dependency chains (score: 50, threshold: depth > 7)
|
|
446
|
+
for (const [packageName, data] of graph.entries()) {
|
|
447
|
+
const depth = metrics.get(packageName).depth;
|
|
448
|
+
|
|
449
|
+
if (depth > 7) {
|
|
450
|
+
anomalies.push({
|
|
451
|
+
id: `deep-${++anomalyId}`,
|
|
452
|
+
type: 'deep-chain',
|
|
453
|
+
severity: 'medium',
|
|
454
|
+
score: 50,
|
|
455
|
+
package: packageName,
|
|
456
|
+
depth,
|
|
457
|
+
impact: 'Deep dependency chain increases fragility',
|
|
458
|
+
recommendation: 'Flatten dependency tree or introduce intermediate layers',
|
|
459
|
+
estimatedEffort: '4-6 hours',
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// 6. Layer violations (score: 90, threshold: lower layer depends on higher layer)
|
|
465
|
+
const layerHierarchy = { infrastructure: 0, core: 1, plugin: 2, feature: 3, ui: 4, unknown: 5 };
|
|
466
|
+
for (const [packageName, data] of graph.entries()) {
|
|
467
|
+
const packageLayer = data.metadata.layer;
|
|
468
|
+
const packageLayerLevel = layerHierarchy[packageLayer];
|
|
469
|
+
|
|
470
|
+
for (const dep of data.dependencies) {
|
|
471
|
+
const depLayer = graph.get(dep)?.metadata.layer;
|
|
472
|
+
if (!depLayer) {continue;}
|
|
473
|
+
|
|
474
|
+
const depLayerLevel = layerHierarchy[depLayer];
|
|
475
|
+
|
|
476
|
+
// Violation: lower layer depends on higher layer (e.g., infrastructure ā plugin)
|
|
477
|
+
if (packageLayerLevel < depLayerLevel) {
|
|
478
|
+
anomalies.push({
|
|
479
|
+
id: `layer-violation-${++anomalyId}`,
|
|
480
|
+
type: 'layer-violation',
|
|
481
|
+
severity: 'critical',
|
|
482
|
+
score: 90,
|
|
483
|
+
package: packageName,
|
|
484
|
+
dependency: dep,
|
|
485
|
+
fromLayer: packageLayer,
|
|
486
|
+
toLayer: depLayer,
|
|
487
|
+
impact: `${packageLayer} layer depends on ${depLayer} layer (inverted hierarchy)`,
|
|
488
|
+
recommendation: `Move ${dep} to ${packageLayer} layer or refactor dependency`,
|
|
489
|
+
estimatedEffort: '3-6 hours',
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// 7. Bidirectional dependencies (score: 70, threshold: AāB && BāA but not circular)
|
|
496
|
+
const bidirectional = new Set();
|
|
497
|
+
for (const [packageA, dataA] of graph.entries()) {
|
|
498
|
+
for (const packageB of dataA.dependencies) {
|
|
499
|
+
const dataB = graph.get(packageB);
|
|
500
|
+
if (!dataB) {continue;}
|
|
501
|
+
|
|
502
|
+
// Check if B also depends on A
|
|
503
|
+
if (dataB.dependencies.includes(packageA)) {
|
|
504
|
+
const pair = [packageA, packageB].sort().join('ā');
|
|
505
|
+
if (!bidirectional.has(pair)) {
|
|
506
|
+
bidirectional.add(pair);
|
|
507
|
+
|
|
508
|
+
// Verify it's not already detected as circular
|
|
509
|
+
const isCycle = cycles.some((cycle) => cycle.includes(packageA) && cycle.includes(packageB));
|
|
510
|
+
|
|
511
|
+
if (!isCycle) {
|
|
512
|
+
anomalies.push({
|
|
513
|
+
id: `bidirectional-${++anomalyId}`,
|
|
514
|
+
type: 'bidirectional-dependency',
|
|
515
|
+
severity: 'high',
|
|
516
|
+
score: 70,
|
|
517
|
+
packages: [packageA, packageB],
|
|
518
|
+
impact: 'Bidirectional coupling increases complexity and fragility',
|
|
519
|
+
recommendation: 'Extract shared types to a contracts package or invert one dependency',
|
|
520
|
+
estimatedEffort: '2-3 hours',
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// 8. Code smells: Large packages (score: 60, threshold: >10K LOC)
|
|
529
|
+
for (const [packageName, data] of graph.entries()) {
|
|
530
|
+
const loc = data.metadata.size.linesOfCode;
|
|
531
|
+
|
|
532
|
+
if (loc > 10000) {
|
|
533
|
+
anomalies.push({
|
|
534
|
+
id: `large-package-${++anomalyId}`,
|
|
535
|
+
type: 'code-smell-large-package',
|
|
536
|
+
severity: 'medium',
|
|
537
|
+
score: 60,
|
|
538
|
+
package: packageName,
|
|
539
|
+
linesOfCode: loc,
|
|
540
|
+
impact: `Package is very large (${loc.toLocaleString()} LOC), hard to maintain`,
|
|
541
|
+
recommendation: 'Split into smaller, focused packages by feature or domain',
|
|
542
|
+
estimatedEffort: '8-12 hours',
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// 9. Code smells: Too many dependencies (score: 50, threshold: >10 deps)
|
|
548
|
+
for (const [packageName, data] of graph.entries()) {
|
|
549
|
+
const depsCount = data.dependencies.length;
|
|
550
|
+
|
|
551
|
+
if (depsCount > 10) {
|
|
552
|
+
anomalies.push({
|
|
553
|
+
id: `many-deps-${++anomalyId}`,
|
|
554
|
+
type: 'code-smell-many-dependencies',
|
|
555
|
+
severity: 'medium',
|
|
556
|
+
score: 50,
|
|
557
|
+
package: packageName,
|
|
558
|
+
dependenciesCount: depsCount,
|
|
559
|
+
impact: `Package has ${depsCount} dependencies, high coupling`,
|
|
560
|
+
recommendation: 'Review and reduce dependencies, consider dependency injection',
|
|
561
|
+
estimatedEffort: '4-6 hours',
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// 10. Code smells: No documentation (score: 40, threshold: missing README)
|
|
567
|
+
for (const [packageName, data] of graph.entries()) {
|
|
568
|
+
const packageDir = path.dirname(data.metadata.path);
|
|
569
|
+
const readmePath = path.join(packageDir, 'README.md');
|
|
570
|
+
|
|
571
|
+
if (!fs.existsSync(readmePath)) {
|
|
572
|
+
anomalies.push({
|
|
573
|
+
id: `no-docs-${++anomalyId}`,
|
|
574
|
+
type: 'code-smell-no-docs',
|
|
575
|
+
severity: 'low',
|
|
576
|
+
score: 40,
|
|
577
|
+
package: packageName,
|
|
578
|
+
impact: 'Package has no README, difficult for new developers',
|
|
579
|
+
recommendation: 'Create README.md with overview, usage, and examples',
|
|
580
|
+
estimatedEffort: '1-2 hours',
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// Sort by score (descending)
|
|
586
|
+
anomalies.sort((a, b) => b.score - a.score);
|
|
587
|
+
|
|
588
|
+
return anomalies;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Load historical architecture data for trend analysis
|
|
593
|
+
*/
|
|
594
|
+
function loadHistoricalData(archDir) {
|
|
595
|
+
if (!fs.existsSync(archDir)) {
|
|
596
|
+
return [];
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
const files = fs.readdirSync(archDir).filter((f) => f.startsWith('architecture-') && f.endsWith('.json'));
|
|
600
|
+
|
|
601
|
+
const history = [];
|
|
602
|
+
|
|
603
|
+
for (const file of files.slice(-5)) {
|
|
604
|
+
// Load last 5 runs
|
|
605
|
+
try {
|
|
606
|
+
const data = JSON.parse(fs.readFileSync(path.join(archDir, file), 'utf-8'));
|
|
607
|
+
history.push({
|
|
608
|
+
date: data.metadata.generatedAt,
|
|
609
|
+
healthScore: data.metadata.healthScore,
|
|
610
|
+
totalPackages: data.metadata.totalPackages,
|
|
611
|
+
anomaliesCount: data.anomalies.length,
|
|
612
|
+
anomaliesBySeverity: {
|
|
613
|
+
critical: data.anomalies.filter((a) => a.severity === 'critical').length,
|
|
614
|
+
high: data.anomalies.filter((a) => a.severity === 'high').length,
|
|
615
|
+
medium: data.anomalies.filter((a) => a.severity === 'medium').length,
|
|
616
|
+
low: data.anomalies.filter((a) => a.severity === 'low').length,
|
|
617
|
+
},
|
|
618
|
+
anomaliesByType: data.anomalies.reduce((acc, a) => {
|
|
619
|
+
acc[a.type] = (acc[a.type] || 0) + 1;
|
|
620
|
+
return acc;
|
|
621
|
+
}, {}),
|
|
622
|
+
});
|
|
623
|
+
} catch (err) {
|
|
624
|
+
// Skip corrupted files
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return history.sort((a, b) => new Date(a.date) - new Date(b.date));
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Generate trend analysis comparing current run with historical data
|
|
633
|
+
*/
|
|
634
|
+
function generateTrendAnalysis(currentData, history) {
|
|
635
|
+
if (history.length === 0) {
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const previous = history[history.length - 1];
|
|
640
|
+
const trends = {};
|
|
641
|
+
|
|
642
|
+
// Health score trend
|
|
643
|
+
const healthDelta = currentData.metadata.healthScore - previous.healthScore;
|
|
644
|
+
trends.healthScore = {
|
|
645
|
+
current: currentData.metadata.healthScore,
|
|
646
|
+
previous: previous.healthScore,
|
|
647
|
+
delta: healthDelta,
|
|
648
|
+
direction: healthDelta > 0 ? 'š' : healthDelta < 0 ? 'š' : 'ā”ļø',
|
|
649
|
+
status: healthDelta > 5 ? 'ā
' : healthDelta < -5 ? 'ā' : 'ā ļø',
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
// Anomalies count trend
|
|
653
|
+
const anomaliesDelta = currentData.anomalies.length - previous.anomaliesCount;
|
|
654
|
+
trends.anomaliesCount = {
|
|
655
|
+
current: currentData.anomalies.length,
|
|
656
|
+
previous: previous.anomaliesCount,
|
|
657
|
+
delta: anomaliesDelta,
|
|
658
|
+
direction: anomaliesDelta < 0 ? 'š' : anomaliesDelta > 0 ? 'š' : 'ā”ļø',
|
|
659
|
+
status: anomaliesDelta < 0 ? 'ā
' : anomaliesDelta > 0 ? 'ā' : 'ā ļø',
|
|
660
|
+
};
|
|
661
|
+
|
|
662
|
+
// Anomalies by severity trends
|
|
663
|
+
const currentBySeverity = {
|
|
664
|
+
critical: currentData.anomalies.filter((a) => a.severity === 'critical').length,
|
|
665
|
+
high: currentData.anomalies.filter((a) => a.severity === 'high').length,
|
|
666
|
+
medium: currentData.anomalies.filter((a) => a.severity === 'medium').length,
|
|
667
|
+
low: currentData.anomalies.filter((a) => a.severity === 'low').length,
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
trends.bySeverity = {};
|
|
671
|
+
for (const [severity, count] of Object.entries(currentBySeverity)) {
|
|
672
|
+
const prevCount = previous.anomaliesBySeverity[severity] || 0;
|
|
673
|
+
const delta = count - prevCount;
|
|
674
|
+
trends.bySeverity[severity] = {
|
|
675
|
+
current: count,
|
|
676
|
+
previous: prevCount,
|
|
677
|
+
delta,
|
|
678
|
+
direction: delta < 0 ? 'š' : delta > 0 ? 'š' : 'ā”ļø',
|
|
679
|
+
status: delta <= 0 ? 'ā
' : 'ā',
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// Anomalies by type trends
|
|
684
|
+
const currentByType = currentData.anomalies.reduce((acc, a) => {
|
|
685
|
+
acc[a.type] = (acc[a.type] || 0) + 1;
|
|
686
|
+
return acc;
|
|
687
|
+
}, {});
|
|
688
|
+
|
|
689
|
+
trends.byType = {};
|
|
690
|
+
const allTypes = new Set([...Object.keys(currentByType), ...Object.keys(previous.anomaliesByType || {})]);
|
|
691
|
+
|
|
692
|
+
for (const type of allTypes) {
|
|
693
|
+
const current = currentByType[type] || 0;
|
|
694
|
+
const prev = (previous.anomaliesByType || {})[type] || 0;
|
|
695
|
+
const delta = current - prev;
|
|
696
|
+
|
|
697
|
+
if (delta !== 0) {
|
|
698
|
+
// Only track changed types
|
|
699
|
+
trends.byType[type] = {
|
|
700
|
+
current,
|
|
701
|
+
previous: prev,
|
|
702
|
+
delta,
|
|
703
|
+
direction: delta < 0 ? 'š' : 'š',
|
|
704
|
+
status: delta < 0 ? 'ā
' : 'ā',
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Historical comparison (last 30 days if available)
|
|
710
|
+
if (history.length > 1) {
|
|
711
|
+
const oldestDate = new Date(history[0].date);
|
|
712
|
+
const newestDate = new Date(currentData.metadata.generatedAt);
|
|
713
|
+
const daysDiff = Math.round((newestDate - oldestDate) / (1000 * 60 * 60 * 24));
|
|
714
|
+
|
|
715
|
+
trends.historical = {
|
|
716
|
+
period: `${daysDiff} days`,
|
|
717
|
+
healthScoreTrend: currentData.metadata.healthScore - history[0].healthScore,
|
|
718
|
+
anomaliesTrend: currentData.anomalies.length - history[0].anomaliesCount,
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
return trends;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// ============================
|
|
726
|
+
// Phase 4: Output Generation
|
|
727
|
+
// ============================
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Generate AI-first JSON output
|
|
731
|
+
*/
|
|
732
|
+
function generateJSON(graph, metrics, anomalies, options) {
|
|
733
|
+
const packages = [];
|
|
734
|
+
const layers = {};
|
|
735
|
+
const nodes = [];
|
|
736
|
+
const edges = [];
|
|
737
|
+
|
|
738
|
+
// Build packages array
|
|
739
|
+
for (const [packageName, data] of graph.entries()) {
|
|
740
|
+
const pkgMetrics = metrics.get(packageName);
|
|
741
|
+
const pkgAnomalies = anomalies.filter(
|
|
742
|
+
(a) => a.package === packageName || a.packages?.includes(packageName)
|
|
743
|
+
);
|
|
744
|
+
|
|
745
|
+
packages.push({
|
|
746
|
+
name: packageName,
|
|
747
|
+
path: data.metadata.path,
|
|
748
|
+
repository: data.metadata.repository,
|
|
749
|
+
layer: data.metadata.layer,
|
|
750
|
+
description: data.metadata.description,
|
|
751
|
+
version: data.metadata.version,
|
|
752
|
+
metrics: {
|
|
753
|
+
linesOfCode: data.metadata.size.linesOfCode,
|
|
754
|
+
fileCount: data.metadata.size.fileCount,
|
|
755
|
+
afferentCoupling: pkgMetrics.afferentCoupling,
|
|
756
|
+
efferentCoupling: pkgMetrics.efferentCoupling,
|
|
757
|
+
instability: parseFloat(pkgMetrics.instability.toFixed(3)),
|
|
758
|
+
centrality: parseFloat(pkgMetrics.centrality.toFixed(3)),
|
|
759
|
+
depth: pkgMetrics.depth,
|
|
760
|
+
},
|
|
761
|
+
dependencies: data.dependencies,
|
|
762
|
+
dependents: data.dependents,
|
|
763
|
+
anomalies: pkgAnomalies.map((a) => ({
|
|
764
|
+
type: a.type,
|
|
765
|
+
severity: a.severity,
|
|
766
|
+
score: a.score,
|
|
767
|
+
message: a.impact,
|
|
768
|
+
recommendation: a.recommendation,
|
|
769
|
+
})),
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
// Build graph nodes
|
|
773
|
+
nodes.push({
|
|
774
|
+
id: packageName,
|
|
775
|
+
layer: data.metadata.layer,
|
|
776
|
+
metrics: {
|
|
777
|
+
centrality: parseFloat(pkgMetrics.centrality.toFixed(3)),
|
|
778
|
+
instability: parseFloat(pkgMetrics.instability.toFixed(3)),
|
|
779
|
+
},
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
// Build graph edges
|
|
783
|
+
for (const dep of data.dependencies) {
|
|
784
|
+
edges.push({
|
|
785
|
+
from: packageName,
|
|
786
|
+
to: dep,
|
|
787
|
+
type: 'dependency',
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Group by layer
|
|
792
|
+
const layer = data.metadata.layer;
|
|
793
|
+
if (!layers[layer]) {
|
|
794
|
+
layers[layer] = {
|
|
795
|
+
packages: [],
|
|
796
|
+
count: 0,
|
|
797
|
+
metrics: {
|
|
798
|
+
averageInstability: 0,
|
|
799
|
+
averageCoupling: 0,
|
|
800
|
+
totalLOC: 0,
|
|
801
|
+
},
|
|
802
|
+
issues: {
|
|
803
|
+
critical: 0,
|
|
804
|
+
high: 0,
|
|
805
|
+
medium: 0,
|
|
806
|
+
low: 0,
|
|
807
|
+
},
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
layers[layer].packages.push(packageName);
|
|
812
|
+
layers[layer].count++;
|
|
813
|
+
layers[layer].metrics.totalLOC += data.metadata.size.linesOfCode;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Calculate layer metrics
|
|
817
|
+
for (const [layerName, layerData] of Object.entries(layers)) {
|
|
818
|
+
const layerPackages = layerData.packages;
|
|
819
|
+
const instabilities = layerPackages.map((p) => metrics.get(p).instability);
|
|
820
|
+
const couplings = layerPackages.map(
|
|
821
|
+
(p) => metrics.get(p).afferentCoupling + metrics.get(p).efferentCoupling
|
|
822
|
+
);
|
|
823
|
+
|
|
824
|
+
layerData.metrics.averageInstability = parseFloat(
|
|
825
|
+
(instabilities.reduce((a, b) => a + b, 0) / layerPackages.length).toFixed(2)
|
|
826
|
+
);
|
|
827
|
+
layerData.metrics.averageCoupling = parseFloat(
|
|
828
|
+
(couplings.reduce((a, b) => a + b, 0) / layerPackages.length).toFixed(1)
|
|
829
|
+
);
|
|
830
|
+
|
|
831
|
+
// Count issues by severity
|
|
832
|
+
for (const pkg of layerPackages) {
|
|
833
|
+
const pkgAnomalies = anomalies.filter(
|
|
834
|
+
(a) => a.package === pkg || a.packages?.includes(pkg)
|
|
835
|
+
);
|
|
836
|
+
for (const anomaly of pkgAnomalies) {
|
|
837
|
+
layerData.issues[anomaly.severity]++;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Find longest dependency chain
|
|
843
|
+
let longestChain = { depth: 0, path: [] };
|
|
844
|
+
for (const [packageName, pkgMetrics] of metrics.entries()) {
|
|
845
|
+
if (pkgMetrics.depth > longestChain.depth) {
|
|
846
|
+
// Reconstruct path (simplified - just show depth)
|
|
847
|
+
longestChain = {
|
|
848
|
+
depth: pkgMetrics.depth,
|
|
849
|
+
path: [packageName], // TODO: reconstruct full path
|
|
850
|
+
};
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Calculate health score
|
|
855
|
+
const totalPackages = graph.size;
|
|
856
|
+
let healthScore = 100;
|
|
857
|
+
|
|
858
|
+
// Deductions
|
|
859
|
+
const criticalCount = anomalies.filter((a) => a.severity === 'critical').length;
|
|
860
|
+
const highCount = anomalies.filter((a) => a.severity === 'high').length;
|
|
861
|
+
const mediumCount = anomalies.filter((a) => a.severity === 'medium').length;
|
|
862
|
+
|
|
863
|
+
healthScore -= criticalCount * 20;
|
|
864
|
+
healthScore -= highCount * 10;
|
|
865
|
+
healthScore -= mediumCount * 5;
|
|
866
|
+
|
|
867
|
+
healthScore = Math.max(0, Math.min(100, healthScore));
|
|
868
|
+
|
|
869
|
+
const healthGrade =
|
|
870
|
+
healthScore >= 90 ? 'A' : healthScore >= 80 ? 'B' : healthScore >= 70 ? 'C' : healthScore >= 60 ? 'D' : 'F';
|
|
871
|
+
|
|
872
|
+
// Build final JSON
|
|
873
|
+
return {
|
|
874
|
+
metadata: {
|
|
875
|
+
generatedAt: new Date().toISOString(),
|
|
876
|
+
version: '1.0.0',
|
|
877
|
+
totalPackages,
|
|
878
|
+
totalRepositories: new Set(packages.map((p) => p.repository)).size,
|
|
879
|
+
healthScore,
|
|
880
|
+
healthGrade,
|
|
881
|
+
},
|
|
882
|
+
packages,
|
|
883
|
+
layers,
|
|
884
|
+
anomalies,
|
|
885
|
+
graph: {
|
|
886
|
+
nodes,
|
|
887
|
+
edges,
|
|
888
|
+
},
|
|
889
|
+
chains: {
|
|
890
|
+
longest: longestChain,
|
|
891
|
+
},
|
|
892
|
+
recommendations: generateRecommendations(anomalies),
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Generate prioritized recommendations
|
|
898
|
+
*/
|
|
899
|
+
function generateRecommendations(anomalies) {
|
|
900
|
+
const recommendations = [];
|
|
901
|
+
|
|
902
|
+
const critical = anomalies.filter((a) => a.severity === 'critical');
|
|
903
|
+
const high = anomalies.filter((a) => a.severity === 'high');
|
|
904
|
+
const medium = anomalies.filter((a) => a.severity === 'medium');
|
|
905
|
+
|
|
906
|
+
if (critical.length > 0) {
|
|
907
|
+
recommendations.push({
|
|
908
|
+
priority: 'immediate',
|
|
909
|
+
timeframe: 'Week 1',
|
|
910
|
+
tasks: critical.slice(0, 3).map((a, i) => ({
|
|
911
|
+
id: `rec-${i + 1}`,
|
|
912
|
+
title: a.type.replace(/-/g, ' '),
|
|
913
|
+
description: a.recommendation,
|
|
914
|
+
effort: a.estimatedEffort,
|
|
915
|
+
impact: a.impact,
|
|
916
|
+
relatedAnomalies: [a.id],
|
|
917
|
+
})),
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
if (high.length > 0) {
|
|
922
|
+
recommendations.push({
|
|
923
|
+
priority: 'high',
|
|
924
|
+
timeframe: 'Month 1',
|
|
925
|
+
tasks: high.slice(0, 5).map((a, i) => ({
|
|
926
|
+
id: `rec-${critical.length + i + 1}`,
|
|
927
|
+
title: a.type.replace(/-/g, ' '),
|
|
928
|
+
description: a.recommendation,
|
|
929
|
+
effort: a.estimatedEffort,
|
|
930
|
+
impact: a.impact,
|
|
931
|
+
relatedAnomalies: [a.id],
|
|
932
|
+
})),
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
if (medium.length > 0) {
|
|
937
|
+
recommendations.push({
|
|
938
|
+
priority: 'medium',
|
|
939
|
+
timeframe: 'Quarter',
|
|
940
|
+
tasks: medium.slice(0, 5).map((a, i) => ({
|
|
941
|
+
id: `rec-${critical.length + high.length + i + 1}`,
|
|
942
|
+
title: a.type.replace(/-/g, ' '),
|
|
943
|
+
description: a.recommendation,
|
|
944
|
+
effort: a.estimatedEffort,
|
|
945
|
+
impact: a.impact,
|
|
946
|
+
relatedAnomalies: [a.id],
|
|
947
|
+
})),
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
return recommendations;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Generate Markdown report
|
|
956
|
+
*/
|
|
957
|
+
function generateMarkdownReport(jsonData) {
|
|
958
|
+
const { metadata, anomalies, layers, chains, recommendations } = jsonData;
|
|
959
|
+
|
|
960
|
+
let report = `# KB Labs Architecture Audit Report\n\n`;
|
|
961
|
+
report += `**Date:** ${new Date(metadata.generatedAt).toLocaleDateString()}\n\n`;
|
|
962
|
+
|
|
963
|
+
// Executive Summary
|
|
964
|
+
report += `## šÆ Executive Summary\n\n`;
|
|
965
|
+
report += `- **Total packages:** ${metadata.totalPackages}\n`;
|
|
966
|
+
report += `- **Total repositories:** ${metadata.totalRepositories}\n`;
|
|
967
|
+
report += `- **Health score:** ${metadata.healthScore}/100 (Grade ${metadata.healthGrade})\n`;
|
|
968
|
+
report += `- **Critical issues:** ${anomalies.filter((a) => a.severity === 'critical').length}\n`;
|
|
969
|
+
report += `- **High priority issues:** ${anomalies.filter((a) => a.severity === 'high').length}\n`;
|
|
970
|
+
report += `- **Medium priority issues:** ${anomalies.filter((a) => a.severity === 'medium').length}\n\n`;
|
|
971
|
+
|
|
972
|
+
// Top 10 Anomalies
|
|
973
|
+
const top10 = anomalies.slice(0, 10);
|
|
974
|
+
if (top10.length > 0) {
|
|
975
|
+
report += `## šØ Top 10 Anomalies (Require Architect Attention)\n\n`;
|
|
976
|
+
|
|
977
|
+
for (let i = 0; i < top10.length; i++) {
|
|
978
|
+
const anomaly = top10[i];
|
|
979
|
+
const emoji = anomaly.severity === 'critical' ? 'š“' : anomaly.severity === 'high' ? 'š ' : 'š”';
|
|
980
|
+
|
|
981
|
+
report += `### ${i + 1}. ${emoji} ${anomaly.type.replace(/-/g, ' ')}\n`;
|
|
982
|
+
report += `**Severity:** ${anomaly.severity} (Score: ${anomaly.score})\n`;
|
|
983
|
+
report += `**Impact:** ${anomaly.impact}\n`;
|
|
984
|
+
report += `**Recommendation:** ${anomaly.recommendation}\n`;
|
|
985
|
+
report += `**Estimated Effort:** ${anomaly.estimatedEffort}\n\n`;
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Trend Analysis
|
|
990
|
+
if (jsonData.trends) {
|
|
991
|
+
const { trends } = jsonData;
|
|
992
|
+
report += `## š Trend Analysis\n\n`;
|
|
993
|
+
|
|
994
|
+
report += `### Health Score\n`;
|
|
995
|
+
report += `- **Current:** ${trends.healthScore.current}/100\n`;
|
|
996
|
+
report += `- **Previous:** ${trends.healthScore.previous}/100\n`;
|
|
997
|
+
report += `- **Change:** ${trends.healthScore.delta >= 0 ? '+' : ''}${trends.healthScore.delta} ${trends.healthScore.direction} ${trends.healthScore.status}\n\n`;
|
|
998
|
+
|
|
999
|
+
report += `### Anomalies Count\n`;
|
|
1000
|
+
report += `- **Current:** ${trends.anomaliesCount.current}\n`;
|
|
1001
|
+
report += `- **Previous:** ${trends.anomaliesCount.previous}\n`;
|
|
1002
|
+
report += `- **Change:** ${trends.anomaliesCount.delta >= 0 ? '+' : ''}${trends.anomaliesCount.delta} ${trends.anomaliesCount.direction} ${trends.anomaliesCount.status}\n\n`;
|
|
1003
|
+
|
|
1004
|
+
if (Object.keys(trends.bySeverity).length > 0) {
|
|
1005
|
+
report += `### Changes by Severity\n\n`;
|
|
1006
|
+
for (const [severity, data] of Object.entries(trends.bySeverity)) {
|
|
1007
|
+
report += `- **${severity}:** ${data.previous} ā ${data.current} (${data.delta >= 0 ? '+' : ''}${data.delta}) ${data.direction} ${data.status}\n`;
|
|
1008
|
+
}
|
|
1009
|
+
report += `\n`;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
if (Object.keys(trends.byType).length > 0) {
|
|
1013
|
+
report += `### Notable Changes by Type\n\n`;
|
|
1014
|
+
const sortedTypes = Object.entries(trends.byType).sort((a, b) => Math.abs(b[1].delta) - Math.abs(a[1].delta));
|
|
1015
|
+
for (const [type, data] of sortedTypes.slice(0, 5)) {
|
|
1016
|
+
report += `- **${type.replace(/-/g, ' ')}:** ${data.previous} ā ${data.current} (${data.delta >= 0 ? '+' : ''}${data.delta}) ${data.direction} ${data.status}\n`;
|
|
1017
|
+
}
|
|
1018
|
+
report += `\n`;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
if (trends.historical) {
|
|
1022
|
+
report += `### Historical Comparison (${trends.historical.period})\n`;
|
|
1023
|
+
report += `- **Health Score Change:** ${trends.historical.healthScoreTrend >= 0 ? '+' : ''}${trends.historical.healthScoreTrend}\n`;
|
|
1024
|
+
report += `- **Anomalies Change:** ${trends.historical.anomaliesTrend >= 0 ? '+' : ''}${trends.historical.anomaliesTrend}\n\n`;
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// Metrics by Layer
|
|
1029
|
+
report += `## š Metrics by Layer\n\n`;
|
|
1030
|
+
for (const [layerName, layerData] of Object.entries(layers)) {
|
|
1031
|
+
report += `### ${layerName.charAt(0).toUpperCase() + layerName.slice(1)} Layer (${layerData.count} packages)\n`;
|
|
1032
|
+
report += `- **Average Instability:** ${layerData.metrics.averageInstability} ${layerData.metrics.averageInstability < 0.3 ? 'ā
' : layerData.metrics.averageInstability < 0.7 ? 'ā ļø' : 'ā'}\n`;
|
|
1033
|
+
report += `- **Average Coupling:** ${layerData.metrics.averageCoupling} deps/pkg ${layerData.metrics.averageCoupling < 5 ? 'ā
' : 'ā ļø'}\n`;
|
|
1034
|
+
report += `- **Total LOC:** ${layerData.metrics.totalLOC.toLocaleString()}\n`;
|
|
1035
|
+
report += `- **Issues:** ${layerData.issues.critical} critical, ${layerData.issues.high} high, ${layerData.issues.medium} medium\n\n`;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// Recommendations
|
|
1039
|
+
if (recommendations.length > 0) {
|
|
1040
|
+
report += `## ā
Recommendations (Prioritized)\n\n`;
|
|
1041
|
+
|
|
1042
|
+
for (const rec of recommendations) {
|
|
1043
|
+
report += `### ${rec.priority.charAt(0).toUpperCase() + rec.priority.slice(1)} Priority (${rec.timeframe})\n\n`;
|
|
1044
|
+
|
|
1045
|
+
for (const task of rec.tasks) {
|
|
1046
|
+
report += `${rec.tasks.indexOf(task) + 1}. **${task.title}**: ${task.description} (${task.effort})\n`;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
report += `\n`;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
report += `---\n\n`;
|
|
1054
|
+
report += `š¤ Generated with [KB Labs DevKit Architecture Tool](https://github.com/kb-labs/devkit)\n`;
|
|
1055
|
+
|
|
1056
|
+
return report;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// ============================
|
|
1060
|
+
// Main Function
|
|
1061
|
+
// ============================
|
|
1062
|
+
|
|
1063
|
+
/**
|
|
1064
|
+
* Main entry point
|
|
1065
|
+
*/
|
|
1066
|
+
async function main() {
|
|
1067
|
+
const rootDir = process.cwd();
|
|
1068
|
+
|
|
1069
|
+
const startTime = Date.now();
|
|
1070
|
+
|
|
1071
|
+
if (options.format !== 'json') {
|
|
1072
|
+
log('\nšļø KB Labs Architecture Audit\n', 'blue');
|
|
1073
|
+
log(`š Scanning packages across monorepo...\n`, 'gray');
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// Phase 1: Data Collection
|
|
1077
|
+
const packages = findPackages(rootDir);
|
|
1078
|
+
|
|
1079
|
+
if (packages.length === 0) {
|
|
1080
|
+
log('ā ļø No KB Labs packages found', 'yellow');
|
|
1081
|
+
log(' Run this command from the monorepo root\n', 'gray');
|
|
1082
|
+
process.exit(0);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
const { graph, packageData } = buildDependencyGraph(packages);
|
|
1086
|
+
|
|
1087
|
+
if (options.format !== 'json') {
|
|
1088
|
+
log(`ā
Found ${graph.size} package(s) (${((Date.now() - startTime) / 1000).toFixed(1)}s)\n`, 'green');
|
|
1089
|
+
log(`š Running analysis...\n`, 'gray');
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// Phase 2: Metrics Calculation
|
|
1093
|
+
const metrics = calculateMetrics(graph);
|
|
1094
|
+
|
|
1095
|
+
if (options.format !== 'json') {
|
|
1096
|
+
log(`ā
Calculated metrics for ${metrics.size} packages (${((Date.now() - startTime) / 1000).toFixed(1)}s)\n`, 'green');
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// Phase 3: Anomaly Detection
|
|
1100
|
+
const anomalies = detectAnomalies(graph, metrics);
|
|
1101
|
+
|
|
1102
|
+
if (options.format !== 'json') {
|
|
1103
|
+
log(`ā
Detected ${anomalies.length} anomalies (${((Date.now() - startTime) / 1000).toFixed(1)}s)\n`, 'green');
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// Phase 4: Output Generation
|
|
1107
|
+
const jsonData = generateJSON(graph, metrics, anomalies, options);
|
|
1108
|
+
|
|
1109
|
+
// Phase 5: Trend Analysis (load historical data and compare)
|
|
1110
|
+
const archDir = path.join(rootDir, '.kb', 'architecture');
|
|
1111
|
+
const history = loadHistoricalData(archDir);
|
|
1112
|
+
const trends = generateTrendAnalysis(jsonData, history);
|
|
1113
|
+
|
|
1114
|
+
if (trends) {
|
|
1115
|
+
jsonData.trends = trends;
|
|
1116
|
+
|
|
1117
|
+
if (options.format !== 'json') {
|
|
1118
|
+
log(`š Trend Analysis:`, 'blue');
|
|
1119
|
+
log(
|
|
1120
|
+
` Health Score: ${trends.healthScore.previous} ā ${trends.healthScore.current} (${trends.healthScore.delta >= 0 ? '+' : ''}${trends.healthScore.delta}) ${trends.healthScore.direction}`,
|
|
1121
|
+
trends.healthScore.status === 'ā
' ? 'green' : trends.healthScore.status === 'ā' ? 'red' : 'yellow'
|
|
1122
|
+
);
|
|
1123
|
+
log(
|
|
1124
|
+
` Anomalies: ${trends.anomaliesCount.previous} ā ${trends.anomaliesCount.current} (${trends.anomaliesCount.delta >= 0 ? '+' : ''}${trends.anomaliesCount.delta}) ${trends.anomaliesCount.direction}`,
|
|
1125
|
+
trends.anomaliesCount.status === 'ā
' ? 'green' : trends.anomaliesCount.status === 'ā' ? 'red' : 'yellow'
|
|
1126
|
+
);
|
|
1127
|
+
|
|
1128
|
+
if (trends.historical) {
|
|
1129
|
+
log(
|
|
1130
|
+
` Over ${trends.historical.period}: Health ${trends.historical.healthScoreTrend >= 0 ? '+' : ''}${trends.historical.healthScoreTrend}, Anomalies ${trends.historical.anomaliesTrend >= 0 ? '+' : ''}${trends.historical.anomaliesTrend}`,
|
|
1131
|
+
'gray'
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
log('');
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// Filter by layer if specified
|
|
1140
|
+
if (options.layer) {
|
|
1141
|
+
jsonData.packages = jsonData.packages.filter((p) => p.layer === options.layer);
|
|
1142
|
+
jsonData.anomalies = jsonData.anomalies.filter((a) => {
|
|
1143
|
+
if (a.package) {return jsonData.packages.some((p) => p.name === a.package);}
|
|
1144
|
+
if (a.packages) {return a.packages.some((pkg) => jsonData.packages.some((p) => p.name === pkg));}
|
|
1145
|
+
return false;
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
// Filter by threshold if specified
|
|
1150
|
+
if (options.threshold > 0) {
|
|
1151
|
+
jsonData.anomalies = jsonData.anomalies.filter((a) => a.score >= options.threshold);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// Output based on format
|
|
1155
|
+
if (options.format === 'json' || options.ai) {
|
|
1156
|
+
console.log(JSON.stringify(jsonData, null, 2));
|
|
1157
|
+
process.exit(0);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
if (options.format === 'md' || options.format === 'all') {
|
|
1161
|
+
const markdownReport = generateMarkdownReport(jsonData);
|
|
1162
|
+
|
|
1163
|
+
if (options.format === 'md') {
|
|
1164
|
+
console.log(markdownReport);
|
|
1165
|
+
process.exit(0);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// Save to file
|
|
1169
|
+
const outputDir = path.join(rootDir, '.kb', 'architecture');
|
|
1170
|
+
if (!fs.existsSync(outputDir)) {
|
|
1171
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
const timestamp = new Date().toISOString().split('T')[0];
|
|
1175
|
+
const reportPath = path.join(outputDir, `report-${timestamp}.md`);
|
|
1176
|
+
const jsonPath = path.join(outputDir, `architecture-${timestamp}.json`);
|
|
1177
|
+
|
|
1178
|
+
fs.writeFileSync(reportPath, markdownReport, 'utf-8');
|
|
1179
|
+
fs.writeFileSync(jsonPath, JSON.stringify(jsonData, null, 2), 'utf-8');
|
|
1180
|
+
|
|
1181
|
+
log(`\nš Generated files:`, 'blue');
|
|
1182
|
+
log(` - ${reportPath}`, 'cyan');
|
|
1183
|
+
log(` - ${jsonPath}`, 'cyan');
|
|
1184
|
+
log('');
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// Terminal output
|
|
1188
|
+
log(`\nš Health Score: ${jsonData.metadata.healthScore}/100 (Grade ${jsonData.metadata.healthGrade})\n`, 'blue');
|
|
1189
|
+
|
|
1190
|
+
// Top 10 Issues
|
|
1191
|
+
const top10 = jsonData.anomalies.slice(0, 10);
|
|
1192
|
+
if (top10.length > 0) {
|
|
1193
|
+
log(`šØ Top ${top10.length} Issues Requiring Attention:\n`, 'red');
|
|
1194
|
+
|
|
1195
|
+
for (let i = 0; i < top10.length; i++) {
|
|
1196
|
+
const anomaly = top10[i];
|
|
1197
|
+
const emoji = anomaly.severity === 'critical' ? 'š“' : anomaly.severity === 'high' ? 'š ' : 'š”';
|
|
1198
|
+
|
|
1199
|
+
log(` ${i + 1}. ${emoji} ${anomaly.type.replace(/-/g, ' ')}: ${anomaly.package || anomaly.packages?.join(' ā ')} (Score: ${anomaly.score})`, 'yellow');
|
|
1200
|
+
log(` Impact: ${anomaly.impact}`, 'gray');
|
|
1201
|
+
log(` Fix: ${anomaly.recommendation}`, 'gray');
|
|
1202
|
+
log('');
|
|
1203
|
+
}
|
|
1204
|
+
} else {
|
|
1205
|
+
log(`ā
No critical anomalies detected!\n`, 'green');
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
log(`š” Tips:`, 'blue');
|
|
1209
|
+
log(` ⢠Use --format=json to get machine-readable output`, 'gray');
|
|
1210
|
+
log(` ⢠Use --format=md to get markdown report`, 'gray');
|
|
1211
|
+
log(` ⢠Use --ai to pipe output to AI agent`, 'gray');
|
|
1212
|
+
log(` ⢠Use --layer=core to filter by specific layer`, 'gray');
|
|
1213
|
+
log(` ⢠Use --threshold=70 to show only high-impact issues`, 'gray');
|
|
1214
|
+
log('');
|
|
1215
|
+
|
|
1216
|
+
process.exit(0);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
main().catch((error) => {
|
|
1220
|
+
console.error('ā Error:', error.message);
|
|
1221
|
+
if (process.env.DEBUG) {
|
|
1222
|
+
console.error(error.stack);
|
|
1223
|
+
}
|
|
1224
|
+
process.exit(1);
|
|
1225
|
+
});
|