@unerr-ai/unerr 0.2.1 → 0.2.2
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/README.md +6 -0
- package/dist/cli.js +37236 -35793
- package/package.json +1 -1
- package/dist/behaviors/agent-llm-bridge.js +0 -166
- package/dist/behaviors/architecture-guard.js +0 -256
- package/dist/behaviors/auto-doc.js +0 -247
- package/dist/behaviors/cascade-guard.js +0 -289
- package/dist/behaviors/change-narrative.js +0 -270
- package/dist/behaviors/convention-drift.js +0 -290
- package/dist/behaviors/framework.js +0 -235
- package/dist/behaviors/guard-formatter.js +0 -44
- package/dist/behaviors/incomplete-work.js +0 -270
- package/dist/behaviors/loop-breaker.js +0 -300
- package/dist/behaviors/session-continuity.js +0 -208
- package/dist/commands/branches.js +0 -97
- package/dist/commands/check-commit.js +0 -225
- package/dist/commands/compress-output.js +0 -64
- package/dist/commands/config-verify.js +0 -243
- package/dist/commands/daemon.js +0 -905
- package/dist/commands/dashboard.js +0 -52
- package/dist/commands/debug.js +0 -200
- package/dist/commands/enrich.js +0 -184
- package/dist/commands/exec.js +0 -233
- package/dist/commands/gain.js +0 -156
- package/dist/commands/hook.js +0 -88
- package/dist/commands/index.js +0 -88
- package/dist/commands/init.js +0 -74
- package/dist/commands/install.js +0 -505
- package/dist/commands/learn.js +0 -116
- package/dist/commands/manifest.js +0 -193
- package/dist/commands/rewind.js +0 -103
- package/dist/commands/serve.js +0 -19
- package/dist/commands/setup-wizard.js +0 -414
- package/dist/commands/skills.js +0 -64
- package/dist/commands/stats.js +0 -20
- package/dist/commands/status.js +0 -654
- package/dist/commands/timeline.js +0 -139
- package/dist/commands/uninstall.js +0 -230
- package/dist/components/App.js +0 -109
- package/dist/components/Banner.js +0 -12
- package/dist/components/ConfirmPrompt.js +0 -25
- package/dist/components/DriftSummary.js +0 -23
- package/dist/components/GradeBadge.js +0 -15
- package/dist/components/HealthCard.js +0 -18
- package/dist/components/InkSpinner.js +0 -22
- package/dist/components/InputBox.js +0 -17
- package/dist/components/KeyValue.js +0 -13
- package/dist/components/MessageList.js +0 -14
- package/dist/components/ProgressBar.js +0 -26
- package/dist/components/Section.js +0 -16
- package/dist/components/SessionSummaryCard.js +0 -73
- package/dist/components/StartupDisplay.js +0 -24
- package/dist/components/StatusDashboard.js +0 -57
- package/dist/components/StatusLine.js +0 -8
- package/dist/components/StepLine.js +0 -22
- package/dist/components/Theme.js +0 -20
- package/dist/components/ToolProgress.js +0 -8
- package/dist/components/ViolationList.js +0 -21
- package/dist/components/render.js +0 -13
- package/dist/config/agent-registry.js +0 -237
- package/dist/config/claude-settings-hooks.js +0 -304
- package/dist/config/hook-installer.js +0 -65
- package/dist/config/instruction-writer.js +0 -388
- package/dist/config/mcp-config-writer.js +0 -266
- package/dist/config/settings.js +0 -174
- package/dist/config/tool-detector.js +0 -42
- package/dist/config/value-surfacing.js +0 -119
- package/dist/core/context-assembly.js +0 -108
- package/dist/core/conversation.js +0 -33
- package/dist/core/local-chat-provider.js +0 -475
- package/dist/core/provider-factory.js +0 -55
- package/dist/core/providers.js +0 -90
- package/dist/core/query-engine.js +0 -174
- package/dist/daemon/api.js +0 -312
- package/dist/daemon/autostart.js +0 -119
- package/dist/daemon/bootstrap.js +0 -39
- package/dist/daemon/client.js +0 -164
- package/dist/daemon/detect-ci.js +0 -81
- package/dist/daemon/platform-linux.js +0 -146
- package/dist/daemon/platform-macos.js +0 -134
- package/dist/daemon/platform-windows.js +0 -116
- package/dist/daemon/process-manager.js +0 -299
- package/dist/daemon/protocol.js +0 -23
- package/dist/daemon/registry.js +0 -270
- package/dist/daemon/settings-schema.js +0 -72
- package/dist/daemon/system-health.js +0 -134
- package/dist/daemon/version-checker.js +0 -262
- package/dist/daemon/warm-start.js +0 -223
- package/dist/entrypoints/cli.js +0 -1043
- package/dist/entrypoints/daemon.js +0 -380
- package/dist/entrypoints/repl.js +0 -147
- package/dist/hooks/adapters/claude-code.js +0 -90
- package/dist/hooks/adapters/cline.js +0 -100
- package/dist/hooks/adapters/cursor.js +0 -98
- package/dist/hooks/hook-dedup.js +0 -79
- package/dist/hooks/hook-runner.js +0 -113
- package/dist/hooks/navigation-hooks.js +0 -175
- package/dist/hooks/prompt-hooks.js +0 -63
- package/dist/hooks/shell-hooks.js +0 -47
- package/dist/ignore.js +0 -111
- package/dist/intelligence/approach-suggester.js +0 -61
- package/dist/intelligence/ast-extractor.js +0 -2615
- package/dist/intelligence/ast-worker.js +0 -34
- package/dist/intelligence/background-indexer.js +0 -121
- package/dist/intelligence/blast-radius.js +0 -200
- package/dist/intelligence/community-detection.js +0 -691
- package/dist/intelligence/community-detector.js +0 -184
- package/dist/intelligence/computation-scheduler.js +0 -75
- package/dist/intelligence/confidence-propagation.js +0 -47
- package/dist/intelligence/convention-detector.js +0 -242
- package/dist/intelligence/convention-learner.js +0 -205
- package/dist/intelligence/convention-matcher.js +0 -205
- package/dist/intelligence/cozo-schema.js +0 -376
- package/dist/intelligence/decision-point-detector.js +0 -90
- package/dist/intelligence/deep-dive-tools.js +0 -586
- package/dist/intelligence/durability-scorer.js +0 -84
- package/dist/intelligence/exploration-cost.js +0 -204
- package/dist/intelligence/exploration-pattern-tracker.js +0 -61
- package/dist/intelligence/fact-generator.js +0 -322
- package/dist/intelligence/facts-schema.js +0 -90
- package/dist/intelligence/file-intelligence.js +0 -59
- package/dist/intelligence/graph-holder.js +0 -220
- package/dist/intelligence/graph-temporal-joiner.js +0 -238
- package/dist/intelligence/health-grade.js +0 -423
- package/dist/intelligence/health-grader.js +0 -200
- package/dist/intelligence/health-map-data.js +0 -259
- package/dist/intelligence/import-symbols.js +0 -136
- package/dist/intelligence/incremental-indexer.js +0 -658
- package/dist/intelligence/indexer/centrality.js +0 -62
- package/dist/intelligence/indexer/cfg-context.js +0 -95
- package/dist/intelligence/indexer/confidence.js +0 -34
- package/dist/intelligence/indexer/cross-file-resolver.js +0 -104
- package/dist/intelligence/indexer/edge-repair.js +0 -89
- package/dist/intelligence/indexer/entity-key.js +0 -17
- package/dist/intelligence/indexer/export-map.js +0 -132
- package/dist/intelligence/indexer/git-cochange.js +0 -128
- package/dist/intelligence/indexer/graph-patch.js +0 -147
- package/dist/intelligence/indexer/incremental.js +0 -78
- package/dist/intelligence/indexer/ingest.js +0 -160
- package/dist/intelligence/indexer/language-detect.js +0 -226
- package/dist/intelligence/indexer/metadata.js +0 -63
- package/dist/intelligence/indexer/mutation-tracker.js +0 -79
- package/dist/intelligence/indexer/orchestrator.js +0 -155
- package/dist/intelligence/indexer/plugin-interface.js +0 -31
- package/dist/intelligence/indexer/plugins/csharp.js +0 -440
- package/dist/intelligence/indexer/plugins/go.js +0 -335
- package/dist/intelligence/indexer/plugins/java.js +0 -370
- package/dist/intelligence/indexer/plugins/python.js +0 -358
- package/dist/intelligence/indexer/plugins/regex-fallback.js +0 -82
- package/dist/intelligence/indexer/plugins/ruby.js +0 -290
- package/dist/intelligence/indexer/plugins/rust.js +0 -484
- package/dist/intelligence/indexer/plugins/tier2-generic.js +0 -310
- package/dist/intelligence/indexer/plugins/typescript.js +0 -456
- package/dist/intelligence/indexer/resource-monitor.js +0 -93
- package/dist/intelligence/indexer/scip/decoder.js +0 -253
- package/dist/intelligence/indexer/scip/detector.js +0 -232
- package/dist/intelligence/indexer/scip/downloader.js +0 -427
- package/dist/intelligence/indexer/scip/fallback.js +0 -34
- package/dist/intelligence/indexer/scip/merger.js +0 -109
- package/dist/intelligence/indexer/scip/orchestrator.js +0 -433
- package/dist/intelligence/indexer/scip/runner.js +0 -98
- package/dist/intelligence/indexer/snapshot.js +0 -66
- package/dist/intelligence/indexer/test-detector.js +0 -196
- package/dist/intelligence/indexer/watch-integration.js +0 -61
- package/dist/intelligence/indexer/worker.js +0 -85
- package/dist/intelligence/local-convention-detector.js +0 -437
- package/dist/intelligence/local-embeddings.js +0 -190
- package/dist/intelligence/local-graph.js +0 -1946
- package/dist/intelligence/local-indexer.js +0 -1575
- package/dist/intelligence/local-llm.js +0 -163
- package/dist/intelligence/local-rule-generator.js +0 -154
- package/dist/intelligence/local-snapshot.js +0 -213
- package/dist/intelligence/negative-knowledge.js +0 -103
- package/dist/intelligence/persistent-db.js +0 -85
- package/dist/intelligence/query-router.js +0 -2556
- package/dist/intelligence/risk-classifier.js +0 -116
- package/dist/intelligence/rule-evaluator.js +0 -380
- package/dist/intelligence/rule-generator.js +0 -49
- package/dist/intelligence/search-index.js +0 -173
- package/dist/intelligence/semantic/docstring-extractor.js +0 -67
- package/dist/intelligence/semantic/embedding-store.js +0 -52
- package/dist/intelligence/semantic/enrichment-orchestrator.js +0 -48
- package/dist/intelligence/semantic/git-message-miner.js +0 -114
- package/dist/intelligence/semantic/identifier-tokenizer.js +0 -51
- package/dist/intelligence/semantic/node2vec-embeddings.js +0 -71
- package/dist/intelligence/semantic/node2vec-walks.js +0 -103
- package/dist/intelligence/semantic/path-domain-inference.js +0 -112
- package/dist/intelligence/semantic/similarity-engine.js +0 -60
- package/dist/intelligence/semantic/tfidf-vectors.js +0 -88
- package/dist/intelligence/session-brief-builder.js +0 -159
- package/dist/intelligence/session-context.js +0 -221
- package/dist/intelligence/session-health-monitor.js +0 -211
- package/dist/intelligence/session-narrative.js +0 -197
- package/dist/intelligence/session-pattern-analyzer.js +0 -218
- package/dist/intelligence/signal-scorer.js +0 -390
- package/dist/intelligence/signal-show-store.js +0 -182
- package/dist/intelligence/smart-truncate.js +0 -158
- package/dist/intelligence/subgraph-cache.js +0 -88
- package/dist/intelligence/temporal-facts.js +0 -494
- package/dist/intelligence/token-estimator.js +0 -100
- package/dist/intelligence/tool-injector.js +0 -87
- package/dist/intelligence/tree-sitter-loader.js +0 -71
- package/dist/intelligence/worker-pool.js +0 -116
- package/dist/proxy/arg-validator.js +0 -79
- package/dist/proxy/auto-bootstrap.js +0 -167
- package/dist/proxy/bridge.js +0 -147
- package/dist/proxy/budget-enforcer.js +0 -70
- package/dist/proxy/compression-quality-monitor.js +0 -160
- package/dist/proxy/compression-stats.js +0 -51
- package/dist/proxy/context-rot-detector.js +0 -137
- package/dist/proxy/drift-detector.js +0 -139
- package/dist/proxy/efficiency-tracker.js +0 -79
- package/dist/proxy/fact-ranking.js +0 -154
- package/dist/proxy/format-encoder.js +0 -266
- package/dist/proxy/http-transport.js +0 -90
- package/dist/proxy/lifecycle-actor.js +0 -55
- package/dist/proxy/lifecycle-machine.js +0 -187
- package/dist/proxy/log-tailer.js +0 -265
- package/dist/proxy/model-pricing.js +0 -98
- package/dist/proxy/network-firewall.js +0 -141
- package/dist/proxy/nudge-state.js +0 -93
- package/dist/proxy/output-compressor.js +0 -185
- package/dist/proxy/pid-lock.js +0 -291
- package/dist/proxy/proxy-context.js +0 -11
- package/dist/proxy/proxy.js +0 -2633
- package/dist/proxy/response-enrichment.js +0 -32
- package/dist/proxy/response-envelope.js +0 -313
- package/dist/proxy/session-dedup.js +0 -82
- package/dist/proxy/session-legend.js +0 -30
- package/dist/proxy/session-persistence.js +0 -210
- package/dist/proxy/session-resume.js +0 -94
- package/dist/proxy/session-stats.js +0 -513
- package/dist/proxy/shell-classifier.js +0 -1346
- package/dist/proxy/shell-compression-log.js +0 -93
- package/dist/proxy/shell-compressor.js +0 -390
- package/dist/proxy/shell-graph-boost.js +0 -202
- package/dist/proxy/shell-monitor-map.js +0 -18
- package/dist/proxy/shell-stats.js +0 -54
- package/dist/proxy/shell-strategies/cloud.js +0 -215
- package/dist/proxy/shell-strategies/diff.js +0 -159
- package/dist/proxy/shell-strategies/error-diagnostic.js +0 -796
- package/dist/proxy/shell-strategies/filter-dsl.js +0 -358
- package/dist/proxy/shell-strategies/git-status.js +0 -177
- package/dist/proxy/shell-strategies/key-value.js +0 -193
- package/dist/proxy/shell-strategies/log-text.js +0 -154
- package/dist/proxy/shell-strategies/omni.js +0 -188
- package/dist/proxy/shell-strategies/progress.js +0 -55
- package/dist/proxy/shell-strategies/redact.js +0 -76
- package/dist/proxy/shell-strategies/structured.js +0 -241
- package/dist/proxy/shell-strategies/tabular.js +0 -243
- package/dist/proxy/shell-strategies/test-results-types.js +0 -13
- package/dist/proxy/shell-strategies/test-results.js +0 -784
- package/dist/proxy/shell-strategies/tree-paths.js +0 -144
- package/dist/proxy/shell-strategies/yaml.js +0 -182
- package/dist/proxy/shell-tee.js +0 -111
- package/dist/proxy/signal-dedup.js +0 -171
- package/dist/proxy/startup-renderer.js +0 -158
- package/dist/proxy/task-token-display.js +0 -38
- package/dist/proxy/token-counter.js +0 -61
- package/dist/proxy/tool-clusters.js +0 -273
- package/dist/proxy/tool-definitions.js +0 -525
- package/dist/proxy/transport-mux.js +0 -229
- package/dist/proxy/wire-cap.js +0 -268
- package/dist/rules/developer.mozilla.org.json +0 -9
- package/dist/rules/github.com.json +0 -21
- package/dist/schemas/api/skills.js +0 -19
- package/dist/schemas/common/errors.js +0 -7
- package/dist/schemas/common/headers.js +0 -5
- package/dist/schemas/entities/edge.js +0 -25
- package/dist/schemas/entities/entity.js +0 -22
- package/dist/schemas/entities/rule.js +0 -18
- package/dist/schemas/index.js +0 -14
- package/dist/server/event-bus.js +0 -59
- package/dist/server/http.js +0 -156
- package/dist/server/middleware.js +0 -70
- package/dist/server/routes/drift.js +0 -97
- package/dist/server/routes/intelligence.js +0 -1217
- package/dist/server/routes/reasoning-quality.js +0 -444
- package/dist/server/routes/session.js +0 -86
- package/dist/server/routes/stream.js +0 -120
- package/dist/server/routes/system.js +0 -73
- package/dist/server/routes/temporal.js +0 -170
- package/dist/server/routes/timeline.js +0 -232
- package/dist/server/routes/token-flow.js +0 -403
- package/dist/skills/effectiveness-tracker.js +0 -93
- package/dist/skills/local-pack.js +0 -380
- package/dist/skills/resolver.js +0 -495
- package/dist/state-detector.js +0 -83
- package/dist/timeline/intent-detector.js +0 -263
- package/dist/timeline/loop-miner.js +0 -140
- package/dist/timeline/open-threads.js +0 -49
- package/dist/timeline/signal-reinforcer.js +0 -62
- package/dist/timeline/timeline-bootstrap.js +0 -151
- package/dist/timeline/timeline-store.js +0 -618
- package/dist/tools/coding/bash.js +0 -49
- package/dist/tools/coding/file-edit.js +0 -72
- package/dist/tools/coding/file-outline.js +0 -227
- package/dist/tools/coding/file-read-protocol.js +0 -425
- package/dist/tools/coding/file-read.js +0 -35
- package/dist/tools/coding/file-write.js +0 -43
- package/dist/tools/coding/glob-tool.js +0 -109
- package/dist/tools/coding/grep.js +0 -162
- package/dist/tools/coding/index.js +0 -27
- package/dist/tools/intelligence/index.js +0 -269
- package/dist/tools/intelligence/record-fact.js +0 -48
- package/dist/tools/intelligence/timeline-markers.js +0 -130
- package/dist/tools/registry.js +0 -47
- package/dist/tools/types.js +0 -8
- package/dist/tracking/auto-snapshot-triggers.js +0 -246
- package/dist/tracking/branch-context.js +0 -115
- package/dist/tracking/branch-snapshot.js +0 -217
- package/dist/tracking/causal-bridge.js +0 -317
- package/dist/tracking/circuit-breaker.js +0 -147
- package/dist/tracking/commit-watcher.js +0 -114
- package/dist/tracking/context-ledger.js +0 -119
- package/dist/tracking/correction-detector.js +0 -324
- package/dist/tracking/drift-tracker.js +0 -874
- package/dist/tracking/durability-tracker.js +0 -94
- package/dist/tracking/entity-rewind.js +0 -200
- package/dist/tracking/file-hash-state.js +0 -114
- package/dist/tracking/git-attribution.js +0 -132
- package/dist/tracking/git-trailers.js +0 -171
- package/dist/tracking/intelligence-counter.js +0 -46
- package/dist/tracking/intent-correlator.js +0 -202
- package/dist/tracking/intent-encoder.js +0 -52
- package/dist/tracking/intent-token-tracker.js +0 -159
- package/dist/tracking/ledger-archiver.js +0 -94
- package/dist/tracking/ledger-chains.js +0 -245
- package/dist/tracking/metrics-store.js +0 -361
- package/dist/tracking/native-watcher.js +0 -131
- package/dist/tracking/offline-rewind.js +0 -295
- package/dist/tracking/pending-violations.js +0 -74
- package/dist/tracking/persistence-effectiveness.js +0 -167
- package/dist/tracking/prompt-durability.js +0 -202
- package/dist/tracking/quality-signals.js +0 -213
- package/dist/tracking/redactor.js +0 -73
- package/dist/tracking/rewind-engine.js +0 -161
- package/dist/tracking/session-history.js +0 -128
- package/dist/tracking/session-receipt.js +0 -88
- package/dist/tracking/session-summary-writer.js +0 -157
- package/dist/tracking/shadow-ledger.js +0 -321
- package/dist/tracking/stash-manager.js +0 -258
- package/dist/tracking/timeline-fork.js +0 -213
- package/dist/tracking/timeline.js +0 -69
- package/dist/tracking/token-flow.js +0 -276
- package/dist/tracking/turn-segmenter.js +0 -122
- package/dist/tracking/weekly-accumulator.js +0 -179
- package/dist/tracking/working-snapshots.js +0 -188
- package/dist/tracking/workspace-manifest.js +0 -176
- package/dist/transport/http.js +0 -102
- package/dist/utils/counterfactual.js +0 -65
- package/dist/utils/deep-link.js +0 -34
- package/dist/utils/detect.js +0 -193
- package/dist/utils/exec.js +0 -73
- package/dist/utils/file-logger.js +0 -87
- package/dist/utils/format-error.js +0 -29
- package/dist/utils/git.js +0 -181
- package/dist/utils/log.js +0 -57
- package/dist/utils/logger.js +0 -35
- package/dist/utils/mcp-content-json.js +0 -8
- package/dist/utils/session-logger.js +0 -154
- package/dist/utils/startup-log.js +0 -512
- package/dist/utils/ui.js +0 -56
|
@@ -1,423 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* First-Contact Health Grade — surfaces architectural anti-patterns in 60 seconds.
|
|
3
|
-
*
|
|
4
|
-
* After setup wizard triggers indexing, this module polls for graph snapshot
|
|
5
|
-
* availability, loads into CozoDB, and queries for:
|
|
6
|
-
* 1. Dead functions (fan_in=0, excluding entry points)
|
|
7
|
-
* 2. Highest-risk entities (top 3 by fan_in + fan_out)
|
|
8
|
-
* 3. Total entities / edges / rules for context
|
|
9
|
-
*
|
|
10
|
-
* Prints a health grade card to stderr. The PLG acquisition hook.
|
|
11
|
-
*/
|
|
12
|
-
import { isTestFile } from "./indexer/test-detector.js";
|
|
13
|
-
/** Entry point names excluded from "dead function" detection. */
|
|
14
|
-
const ENTRY_POINT_PATTERNS = [
|
|
15
|
-
"main",
|
|
16
|
-
"index",
|
|
17
|
-
"app",
|
|
18
|
-
"server",
|
|
19
|
-
"bootstrap",
|
|
20
|
-
"configure",
|
|
21
|
-
"setup",
|
|
22
|
-
"init",
|
|
23
|
-
"run",
|
|
24
|
-
"start",
|
|
25
|
-
"default",
|
|
26
|
-
"register",
|
|
27
|
-
"create",
|
|
28
|
-
"make",
|
|
29
|
-
"build",
|
|
30
|
-
"get",
|
|
31
|
-
"use",
|
|
32
|
-
];
|
|
33
|
-
const HANDLER_SUFFIXES = [
|
|
34
|
-
"Handler",
|
|
35
|
-
"handler",
|
|
36
|
-
"Controller",
|
|
37
|
-
"controller",
|
|
38
|
-
"Middleware",
|
|
39
|
-
"middleware",
|
|
40
|
-
"Route",
|
|
41
|
-
"route",
|
|
42
|
-
"Action",
|
|
43
|
-
"action",
|
|
44
|
-
"Callback",
|
|
45
|
-
"callback",
|
|
46
|
-
"Listener",
|
|
47
|
-
"listener",
|
|
48
|
-
"Resolver",
|
|
49
|
-
"resolver",
|
|
50
|
-
"Factory",
|
|
51
|
-
"factory",
|
|
52
|
-
"Provider",
|
|
53
|
-
"provider",
|
|
54
|
-
"Hook",
|
|
55
|
-
"hook",
|
|
56
|
-
"Plugin",
|
|
57
|
-
"plugin",
|
|
58
|
-
"Command",
|
|
59
|
-
"command",
|
|
60
|
-
"Adapter",
|
|
61
|
-
"adapter",
|
|
62
|
-
];
|
|
63
|
-
/** Patterns that indicate a function is part of a framework lifecycle (not dead). */
|
|
64
|
-
const FRAMEWORK_PATTERNS = [
|
|
65
|
-
// React
|
|
66
|
-
/^use[A-Z]/, // hooks: useEffect, useState, useCustomHook
|
|
67
|
-
/^render/, // render methods
|
|
68
|
-
/^on[A-Z]/, // event handlers: onClick, onChange
|
|
69
|
-
/^handle[A-Z]/, // event handlers: handleClick, handleSubmit
|
|
70
|
-
// Express/Hono/Fastify
|
|
71
|
-
/^(get|post|put|patch|delete|head|options)$/i,
|
|
72
|
-
// Lifecycle
|
|
73
|
-
/^(before|after|on)(Each|All|Mount|Unmount|Create|Destroy|Init|Close|Load|Ready)/,
|
|
74
|
-
// Exports (common patterns)
|
|
75
|
-
/^(export|define|provide|inject|connect|subscribe|dispatch|emit)/,
|
|
76
|
-
// CLI
|
|
77
|
-
/^(action|command|program)/,
|
|
78
|
-
// Testing
|
|
79
|
-
/^(describe|it|expect|assert|should|before|after|vi|jest)/,
|
|
80
|
-
];
|
|
81
|
-
/**
|
|
82
|
-
* Compute health grade from a loaded CozoDB instance.
|
|
83
|
-
*/
|
|
84
|
-
export async function computeHealthGrade(db) {
|
|
85
|
-
// Total entity count
|
|
86
|
-
let totalEntities = 0;
|
|
87
|
-
let totalEdges = 0;
|
|
88
|
-
let totalRules = 0;
|
|
89
|
-
let deadFunctionCount = 0;
|
|
90
|
-
let highRiskEntities = [];
|
|
91
|
-
try {
|
|
92
|
-
const entityResult = await db.run("?[count(key)] := *entities{key}");
|
|
93
|
-
totalEntities = entityResult?.rows?.[0]?.[0] ?? 0;
|
|
94
|
-
}
|
|
95
|
-
catch {
|
|
96
|
-
// Empty or uninitialized graph
|
|
97
|
-
}
|
|
98
|
-
try {
|
|
99
|
-
const edgeResult = await db.run("?[count(from_key)] := *edges{from_key}");
|
|
100
|
-
totalEdges = edgeResult?.rows?.[0]?.[0] ?? 0;
|
|
101
|
-
}
|
|
102
|
-
catch {
|
|
103
|
-
// Empty or uninitialized graph
|
|
104
|
-
}
|
|
105
|
-
try {
|
|
106
|
-
const ruleResult = await db.run("?[count(key)] := *rules{key}");
|
|
107
|
-
totalRules = ruleResult?.rows?.[0]?.[0] ?? 0;
|
|
108
|
-
}
|
|
109
|
-
catch {
|
|
110
|
-
// Empty or uninitialized graph
|
|
111
|
-
}
|
|
112
|
-
// Dead functions: fan_in=0, fan_out=0, kind=function, not entry points/framework patterns
|
|
113
|
-
// A truly dead function has NO callers AND calls nothing (isolated).
|
|
114
|
-
// fan_in=0 alone is too aggressive — exported functions, hooks, handlers all have fan_in=0
|
|
115
|
-
// because static analysis can't capture dynamic/framework-mediated calls.
|
|
116
|
-
try {
|
|
117
|
-
const allFunctions = await db.run(`?[key, name, fp, fan_in, fan_out] := *entities{key, kind, name, file_path: fp, fan_in, fan_out}, kind = "function", fan_in == 0`);
|
|
118
|
-
const deadFunctions = (allFunctions?.rows ?? []).filter((row) => {
|
|
119
|
-
const name = row[1];
|
|
120
|
-
const filePath = row[2];
|
|
121
|
-
const fanOut = row[4];
|
|
122
|
-
const nameLower = name.toLowerCase();
|
|
123
|
-
// Functions with fan_out > 0 are likely real (they call other things, just aren't
|
|
124
|
-
// called via edges we can track — e.g. exported API, CLI commands, handlers)
|
|
125
|
-
if (fanOut > 2)
|
|
126
|
-
return false;
|
|
127
|
-
// Exclude test files entirely �� test functions are called by test runners
|
|
128
|
-
if (isTestFile(filePath))
|
|
129
|
-
return false;
|
|
130
|
-
// Exclude entry points
|
|
131
|
-
if (ENTRY_POINT_PATTERNS.some((p) => nameLower === p || nameLower.startsWith(p)))
|
|
132
|
-
return false;
|
|
133
|
-
// Exclude handlers/suffixes
|
|
134
|
-
if (HANDLER_SUFFIXES.some((s) => name.endsWith(s)))
|
|
135
|
-
return false;
|
|
136
|
-
// Exclude framework patterns
|
|
137
|
-
if (FRAMEWORK_PATTERNS.some((p) => p.test(name)))
|
|
138
|
-
return false;
|
|
139
|
-
// Exclude test functions
|
|
140
|
-
if (nameLower.startsWith("test") ||
|
|
141
|
-
nameLower.includes("spec") ||
|
|
142
|
-
nameLower.includes("mock") ||
|
|
143
|
-
nameLower.includes("stub") ||
|
|
144
|
-
nameLower.includes("fixture"))
|
|
145
|
-
return false;
|
|
146
|
-
// Exclude single-letter or very short names (likely params/lambdas misclassified)
|
|
147
|
-
if (name.length <= 2)
|
|
148
|
-
return false;
|
|
149
|
-
return true;
|
|
150
|
-
});
|
|
151
|
-
deadFunctionCount = deadFunctions.length;
|
|
152
|
-
}
|
|
153
|
-
catch {
|
|
154
|
-
// Query failed — treat as 0 dead functions
|
|
155
|
-
}
|
|
156
|
-
// High-risk entities: top 3 by total blast radius (fan_in + fan_out)
|
|
157
|
-
try {
|
|
158
|
-
const highRiskResult = await db.run(`?[key, kind, name, fp, fan_in, fan_out, risk_level] :=
|
|
159
|
-
*entities{key, kind, name, file_path: fp, fan_in, fan_out, risk_level},
|
|
160
|
-
risk_level == "high"
|
|
161
|
-
:order -(fan_in + fan_out)
|
|
162
|
-
:limit 3`);
|
|
163
|
-
highRiskEntities = (highRiskResult?.rows ?? []).map((row) => ({
|
|
164
|
-
name: row[2],
|
|
165
|
-
kind: row[1],
|
|
166
|
-
file_path: row[3],
|
|
167
|
-
fan_in: row[4],
|
|
168
|
-
fan_out: row[5],
|
|
169
|
-
}));
|
|
170
|
-
}
|
|
171
|
-
catch {
|
|
172
|
-
// Query failed — no high risk entities
|
|
173
|
-
}
|
|
174
|
-
// Sprint 3.3: Circular dependency detection via CozoDB cycle query
|
|
175
|
-
const circularDeps = await detectCircularDeps(db);
|
|
176
|
-
// Sprint 3.3: Longest import chain depth
|
|
177
|
-
const maxImportDepth = await computeMaxImportDepth(db);
|
|
178
|
-
// Sprint 3.3: Convention adherence rate
|
|
179
|
-
const conventionAdherence = await computeConventionAdherence(db, totalEntities);
|
|
180
|
-
// Sprint 3.3: Drift impact score
|
|
181
|
-
const driftImpactScore = await computeDriftImpact(db);
|
|
182
|
-
// Orphan test files: test files with no outbound "tests" edges
|
|
183
|
-
const orphanTestFiles = await detectOrphanTestFiles(db);
|
|
184
|
-
// Compute score (0-100) with new signals
|
|
185
|
-
const score = computeScore(totalEntities, deadFunctionCount, highRiskEntities.length, totalRules, circularDeps.length, maxImportDepth, conventionAdherence, driftImpactScore);
|
|
186
|
-
const grade = scoreToGrade(score);
|
|
187
|
-
return {
|
|
188
|
-
grade,
|
|
189
|
-
totalEntities,
|
|
190
|
-
totalEdges,
|
|
191
|
-
totalRules,
|
|
192
|
-
deadFunctionCount,
|
|
193
|
-
highRiskEntities,
|
|
194
|
-
score,
|
|
195
|
-
circularDeps: circularDeps.length > 0 ? circularDeps : undefined,
|
|
196
|
-
maxImportDepth,
|
|
197
|
-
conventionAdherence,
|
|
198
|
-
driftImpactScore: driftImpactScore > 0 ? driftImpactScore : undefined,
|
|
199
|
-
orphanTestFiles: orphanTestFiles.length > 0 ? orphanTestFiles : undefined,
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
function computeScore(totalEntities, deadFunctions, highRiskCount, ruleCount, circularDepCount = 0, maxImportDepth = 0, conventionAdherence = 1, driftImpact = 0) {
|
|
203
|
-
if (totalEntities === 0)
|
|
204
|
-
return 50; // Empty repo, neutral
|
|
205
|
-
let score = 80; // Start at B
|
|
206
|
-
// Dead function ratio penalty (max -30 points)
|
|
207
|
-
const deadRatio = deadFunctions / totalEntities;
|
|
208
|
-
if (deadRatio > 0.3)
|
|
209
|
-
score -= 30;
|
|
210
|
-
else if (deadRatio > 0.2)
|
|
211
|
-
score -= 20;
|
|
212
|
-
else if (deadRatio > 0.1)
|
|
213
|
-
score -= 10;
|
|
214
|
-
else if (deadRatio > 0.05)
|
|
215
|
-
score -= 5;
|
|
216
|
-
// High-risk entities penalty (max -20 points)
|
|
217
|
-
score -= Math.min(highRiskCount * 7, 20);
|
|
218
|
-
// Rules bonus (+10 if any rules exist)
|
|
219
|
-
if (ruleCount > 0)
|
|
220
|
-
score += 10;
|
|
221
|
-
// Small codebase bonus (+5 for <100 entities)
|
|
222
|
-
if (totalEntities < 100)
|
|
223
|
-
score += 5;
|
|
224
|
-
// Sprint 3.3: Circular dependency penalty (max -15 points)
|
|
225
|
-
if (circularDepCount > 5)
|
|
226
|
-
score -= 15;
|
|
227
|
-
else if (circularDepCount > 2)
|
|
228
|
-
score -= 10;
|
|
229
|
-
else if (circularDepCount > 0)
|
|
230
|
-
score -= 5;
|
|
231
|
-
// Sprint 3.3: Import chain depth penalty (max -10 points)
|
|
232
|
-
if (maxImportDepth > 15)
|
|
233
|
-
score -= 10;
|
|
234
|
-
else if (maxImportDepth > 10)
|
|
235
|
-
score -= 7;
|
|
236
|
-
else if (maxImportDepth > 7)
|
|
237
|
-
score -= 3;
|
|
238
|
-
// Sprint 3.3: Convention adherence bonus (max +10 points)
|
|
239
|
-
if (conventionAdherence >= 0.9)
|
|
240
|
-
score += 10;
|
|
241
|
-
else if (conventionAdherence >= 0.7)
|
|
242
|
-
score += 5;
|
|
243
|
-
else if (conventionAdherence < 0.5)
|
|
244
|
-
score -= 5;
|
|
245
|
-
// Sprint 3.3: Drift impact penalty (max -10 points)
|
|
246
|
-
if (driftImpact > 10)
|
|
247
|
-
score -= 10;
|
|
248
|
-
else if (driftImpact > 5)
|
|
249
|
-
score -= 7;
|
|
250
|
-
else if (driftImpact > 0)
|
|
251
|
-
score -= 3;
|
|
252
|
-
return Math.max(0, Math.min(100, score));
|
|
253
|
-
}
|
|
254
|
-
/**
|
|
255
|
-
* Sprint 3.3: Detect circular dependencies using CozoDB cycle detection.
|
|
256
|
-
* Uses recursive Datalog to find paths from A back to A via "calls" edges.
|
|
257
|
-
* Returns up to 10 cycles (capped for performance).
|
|
258
|
-
*/
|
|
259
|
-
async function detectCircularDeps(db) {
|
|
260
|
-
try {
|
|
261
|
-
// Find entities that can reach themselves through "calls" edges (cycle detection)
|
|
262
|
-
// Use named rule for recursion, then filter for self-reachable entities
|
|
263
|
-
const result = await db.run(`reach[start, next] := *edges{from_key: start, to_key: next, type: "calls"}
|
|
264
|
-
reach[start, next] := reach[start, mid], *edges{from_key: mid, to_key: next, type: "calls"}
|
|
265
|
-
?[key] := reach[key, key]
|
|
266
|
-
:limit 10`);
|
|
267
|
-
return result.rows.map((row) => ({ cycle: [row[0]] }));
|
|
268
|
-
}
|
|
269
|
-
catch {
|
|
270
|
-
// CozoDB may not support this query form — graceful fallback
|
|
271
|
-
// Use simpler 2-hop cycle detection
|
|
272
|
-
try {
|
|
273
|
-
const result = await db.run(`?[a, b] := *edges{from_key: a, to_key: b, type: "calls"}, *edges{from_key: b, to_key: a, type: "calls"}
|
|
274
|
-
:limit 10`);
|
|
275
|
-
return result.rows.map((row) => ({
|
|
276
|
-
cycle: [row[0], row[1]],
|
|
277
|
-
}));
|
|
278
|
-
}
|
|
279
|
-
catch {
|
|
280
|
-
return [];
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
/**
|
|
285
|
-
* Sprint 3.3: Compute longest import chain depth using recursive Datalog.
|
|
286
|
-
* Finds the maximum depth of the call graph from any root.
|
|
287
|
-
*/
|
|
288
|
-
async function computeMaxImportDepth(db) {
|
|
289
|
-
try {
|
|
290
|
-
const result = await db.run(`reach[target, depth] := *edges{from_key, to_key: target, type: "calls"}, depth = 1
|
|
291
|
-
reach[target, depth] := reach[mid, d], *edges{from_key: mid, to_key: target, type: "calls"}, depth = d + 1, d < 20
|
|
292
|
-
?[max(depth)] := reach[_, depth]`);
|
|
293
|
-
return result.rows[0]?.[0] ?? 0;
|
|
294
|
-
}
|
|
295
|
-
catch {
|
|
296
|
-
return 0;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* Sprint 3.3: Compute convention adherence rate from patterns.
|
|
301
|
-
* Returns ratio of entities following detected naming patterns (0-1).
|
|
302
|
-
*/
|
|
303
|
-
async function computeConventionAdherence(db, totalEntities) {
|
|
304
|
-
if (totalEntities === 0)
|
|
305
|
-
return 1;
|
|
306
|
-
try {
|
|
307
|
-
const patterns = await db.run("?[freq] := *patterns[_, _, kind, freq, conf, _, _], kind = 'naming', conf > 0.7");
|
|
308
|
-
if (patterns.rows.length === 0)
|
|
309
|
-
return 1; // No patterns = 100% adherence by default
|
|
310
|
-
const totalFollowing = patterns.rows.reduce((sum, row) => sum + row[0], 0);
|
|
311
|
-
return Math.min(1, totalFollowing / totalEntities);
|
|
312
|
-
}
|
|
313
|
-
catch {
|
|
314
|
-
return 1;
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
/**
|
|
318
|
-
* Sprint 3.3: Compute drift impact score.
|
|
319
|
-
* Counts drifted entities that are in high-fan-in zones (fan_in > 5).
|
|
320
|
-
*/
|
|
321
|
-
async function computeDriftImpact(db) {
|
|
322
|
-
try {
|
|
323
|
-
const result = await db.run(`?[count(dk)] := *drift_overlay{key: dk},
|
|
324
|
-
*entities{key: dk, fan_in: fi}, fi > 5`);
|
|
325
|
-
return result.rows[0]?.[0] ?? 0;
|
|
326
|
-
}
|
|
327
|
-
catch {
|
|
328
|
-
return 0;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
/**
|
|
332
|
-
* Detect orphan test files — test files whose entities have no outbound "tests" edges.
|
|
333
|
-
* These are test files not associated with any source entity in the graph.
|
|
334
|
-
*/
|
|
335
|
-
async function detectOrphanTestFiles(db) {
|
|
336
|
-
try {
|
|
337
|
-
// Get all test files (files containing is_test=true entities)
|
|
338
|
-
const testFilesResult = await db.run("?[fp] := *entities{file_path: fp, is_test: t}, t = true");
|
|
339
|
-
const allTestFiles = new Set(testFilesResult.rows.map((row) => row[0]));
|
|
340
|
-
if (allTestFiles.size === 0)
|
|
341
|
-
return [];
|
|
342
|
-
// Get test files that have at least one "tests" edge from any of their entities
|
|
343
|
-
const coveredResult = await db.run(`?[fp] := *entities{key: ek, file_path: fp, is_test: t}, t = true,
|
|
344
|
-
*edges{from_key: ek, type: "tests"}`);
|
|
345
|
-
const coveredFiles = new Set(coveredResult.rows.map((row) => row[0]));
|
|
346
|
-
// Orphans = test files NOT in the covered set
|
|
347
|
-
const orphans = [];
|
|
348
|
-
for (const file of allTestFiles) {
|
|
349
|
-
if (!coveredFiles.has(file)) {
|
|
350
|
-
orphans.push({
|
|
351
|
-
file,
|
|
352
|
-
reason: "no source imports detected",
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
return orphans.sort((a, b) => a.file.localeCompare(b.file));
|
|
357
|
-
}
|
|
358
|
-
catch {
|
|
359
|
-
return [];
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
function scoreToGrade(score) {
|
|
363
|
-
if (score >= 90)
|
|
364
|
-
return "A";
|
|
365
|
-
if (score >= 80)
|
|
366
|
-
return "B+";
|
|
367
|
-
if (score >= 70)
|
|
368
|
-
return "B";
|
|
369
|
-
if (score >= 60)
|
|
370
|
-
return "C+";
|
|
371
|
-
if (score >= 50)
|
|
372
|
-
return "C";
|
|
373
|
-
if (score >= 40)
|
|
374
|
-
return "D";
|
|
375
|
-
return "F";
|
|
376
|
-
}
|
|
377
|
-
/**
|
|
378
|
-
* Format health grade card for terminal output.
|
|
379
|
-
*/
|
|
380
|
-
export function formatHealthGrade(result) {
|
|
381
|
-
const lines = [
|
|
382
|
-
"",
|
|
383
|
-
"── Health Grade ────────────────────────────────",
|
|
384
|
-
` Grade: ${result.grade} (${result.score}/100)`,
|
|
385
|
-
` Entities: ${result.totalEntities} (${result.totalEdges} edges)`,
|
|
386
|
-
` Rules: ${result.totalRules}`,
|
|
387
|
-
` Dead functions: ${result.deadFunctionCount}`,
|
|
388
|
-
];
|
|
389
|
-
if (result.highRiskEntities.length > 0) {
|
|
390
|
-
lines.push(" High-risk:");
|
|
391
|
-
for (const entity of result.highRiskEntities) {
|
|
392
|
-
lines.push(` - ${entity.name} (${entity.kind}) — ${entity.fan_in} callers, ${entity.fan_out} callees`);
|
|
393
|
-
lines.push(` ${entity.file_path}`);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
else {
|
|
397
|
-
lines.push(" High-risk: None detected");
|
|
398
|
-
}
|
|
399
|
-
if (result.circularDeps && result.circularDeps.length > 0) {
|
|
400
|
-
lines.push(` Circular deps: ${result.circularDeps.length} cycle(s) detected`);
|
|
401
|
-
}
|
|
402
|
-
if (result.maxImportDepth !== undefined && result.maxImportDepth > 0) {
|
|
403
|
-
lines.push(` Import depth: ${result.maxImportDepth} (longest chain)`);
|
|
404
|
-
}
|
|
405
|
-
if (result.conventionAdherence !== undefined) {
|
|
406
|
-
lines.push(` Conventions: ${Math.round(result.conventionAdherence * 100)}% adherence`);
|
|
407
|
-
}
|
|
408
|
-
if (result.driftImpactScore !== undefined && result.driftImpactScore > 0) {
|
|
409
|
-
lines.push(` Drift impact: ${result.driftImpactScore} high-fan-in entities drifted`);
|
|
410
|
-
}
|
|
411
|
-
if (result.orphanTestFiles && result.orphanTestFiles.length > 0) {
|
|
412
|
-
lines.push(` Orphan tests: ${result.orphanTestFiles.length} test file(s) not linked to source`);
|
|
413
|
-
for (const orphan of result.orphanTestFiles.slice(0, 5)) {
|
|
414
|
-
lines.push(` - ${orphan.file}`);
|
|
415
|
-
}
|
|
416
|
-
if (result.orphanTestFiles.length > 5) {
|
|
417
|
-
lines.push(` ... and ${result.orphanTestFiles.length - 5} more`);
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
lines.push("───────────────────────────────────────────────");
|
|
421
|
-
lines.push("");
|
|
422
|
-
return lines.join("\n");
|
|
423
|
-
}
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Enhanced Health Grading — 5-factor composite score → A-F grade.
|
|
3
|
-
*
|
|
4
|
-
* O.1: Circular dependency detection (Tarjan's SCC on call graph)
|
|
5
|
-
* O.2: Fan-in distribution analysis (skew detection)
|
|
6
|
-
* O.3: Import depth measurement (longest chain)
|
|
7
|
-
* O.4: Test coverage proxy (test file ratio)
|
|
8
|
-
* O.5: Composite health grade (weighted score)
|
|
9
|
-
*
|
|
10
|
-
* Factors (100 points each, weighted):
|
|
11
|
-
* 1. Circular dependencies (30%): 0 cycles = 100, each cycle -15
|
|
12
|
-
* 2. Fan-in distribution (20%): low skew = 100, high skew (few hubs) -
|
|
13
|
-
* 3. Import depth (15%): shallow chains = 100, deep (>8) penalized
|
|
14
|
-
* 4. Test coverage proxy (20%): test_files/source_files ratio
|
|
15
|
-
* 5. Community cohesion (15%): avg cohesion across communities
|
|
16
|
-
*/
|
|
17
|
-
import { isTestFile } from "./indexer/test-detector.js";
|
|
18
|
-
/**
|
|
19
|
-
* Detect circular dependencies using DFS-based cycle detection.
|
|
20
|
-
*/
|
|
21
|
-
export function detectCircularDeps(edges) {
|
|
22
|
-
const adj = new Map();
|
|
23
|
-
for (const edge of edges) {
|
|
24
|
-
if (edge.type !== "calls" && edge.type !== "imports")
|
|
25
|
-
continue;
|
|
26
|
-
if (!adj.has(edge.from_key))
|
|
27
|
-
adj.set(edge.from_key, []);
|
|
28
|
-
adj.get(edge.from_key)?.push(edge.to_key);
|
|
29
|
-
}
|
|
30
|
-
const cycles = [];
|
|
31
|
-
const visited = new Set();
|
|
32
|
-
const inStack = new Set();
|
|
33
|
-
const stack = [];
|
|
34
|
-
function dfs(node) {
|
|
35
|
-
if (cycles.length >= 10)
|
|
36
|
-
return;
|
|
37
|
-
visited.add(node);
|
|
38
|
-
inStack.add(node);
|
|
39
|
-
stack.push(node);
|
|
40
|
-
for (const neighbor of adj.get(node) ?? []) {
|
|
41
|
-
if (!visited.has(neighbor)) {
|
|
42
|
-
dfs(neighbor);
|
|
43
|
-
}
|
|
44
|
-
else if (inStack.has(neighbor)) {
|
|
45
|
-
const cycleStart = stack.indexOf(neighbor);
|
|
46
|
-
if (cycleStart >= 0) {
|
|
47
|
-
cycles.push(stack.slice(cycleStart));
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
stack.pop();
|
|
52
|
-
inStack.delete(node);
|
|
53
|
-
}
|
|
54
|
-
for (const node of adj.keys()) {
|
|
55
|
-
if (!visited.has(node)) {
|
|
56
|
-
dfs(node);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return cycles;
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Analyze fan-in distribution (Gini coefficient of caller counts).
|
|
63
|
-
*/
|
|
64
|
-
export function analyzeFanInDistribution(edges) {
|
|
65
|
-
const fanIn = new Map();
|
|
66
|
-
for (const edge of edges) {
|
|
67
|
-
if (edge.type === "calls") {
|
|
68
|
-
fanIn.set(edge.to_key, (fanIn.get(edge.to_key) ?? 0) + 1);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
const values = [...fanIn.values()].sort((a, b) => a - b);
|
|
72
|
-
if (values.length === 0)
|
|
73
|
-
return { giniCoefficient: 0, maxFanIn: 0, avgFanIn: 0 };
|
|
74
|
-
const n = values.length;
|
|
75
|
-
const sum = values.reduce((a, b) => a + b, 0);
|
|
76
|
-
const avg = sum / n;
|
|
77
|
-
let numerator = 0;
|
|
78
|
-
for (let i = 0; i < n; i++) {
|
|
79
|
-
for (let j = 0; j < n; j++) {
|
|
80
|
-
numerator += Math.abs(values[i] - values[j]);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
const gini = n > 1 ? numerator / (2 * n * sum || 1) : 0;
|
|
84
|
-
return {
|
|
85
|
-
giniCoefficient: Math.min(1, gini),
|
|
86
|
-
maxFanIn: values[n - 1] ?? 0,
|
|
87
|
-
avgFanIn: avg,
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Measure maximum import depth (longest dependency chain).
|
|
92
|
-
*/
|
|
93
|
-
export function measureImportDepth(edges) {
|
|
94
|
-
const importEdges = edges.filter((e) => e.type === "imports" || e.type === "calls");
|
|
95
|
-
const adj = new Map();
|
|
96
|
-
for (const edge of importEdges) {
|
|
97
|
-
if (!adj.has(edge.from_key))
|
|
98
|
-
adj.set(edge.from_key, []);
|
|
99
|
-
adj.get(edge.from_key)?.push(edge.to_key);
|
|
100
|
-
}
|
|
101
|
-
const memo = new Map();
|
|
102
|
-
const visiting = new Set();
|
|
103
|
-
function depth(node) {
|
|
104
|
-
if (memo.has(node))
|
|
105
|
-
return memo.get(node);
|
|
106
|
-
if (visiting.has(node))
|
|
107
|
-
return 0;
|
|
108
|
-
visiting.add(node);
|
|
109
|
-
let maxChild = 0;
|
|
110
|
-
for (const child of adj.get(node) ?? []) {
|
|
111
|
-
maxChild = Math.max(maxChild, depth(child) + 1);
|
|
112
|
-
}
|
|
113
|
-
visiting.delete(node);
|
|
114
|
-
memo.set(node, maxChild);
|
|
115
|
-
return maxChild;
|
|
116
|
-
}
|
|
117
|
-
let maxDepth = 0;
|
|
118
|
-
for (const node of adj.keys()) {
|
|
119
|
-
maxDepth = Math.max(maxDepth, depth(node));
|
|
120
|
-
}
|
|
121
|
-
return maxDepth;
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* Compute test coverage proxy from file paths.
|
|
125
|
-
*/
|
|
126
|
-
export function computeTestCoverageProxy(entities) {
|
|
127
|
-
const files = new Set(entities.map((e) => e.file_path));
|
|
128
|
-
const testFileSet = [...files].filter((f) => isTestFile(f));
|
|
129
|
-
const sourceFiles = [...files].filter((f) => !isTestFile(f));
|
|
130
|
-
if (sourceFiles.length === 0)
|
|
131
|
-
return 1.0;
|
|
132
|
-
return Math.min(1.0, testFileSet.length / sourceFiles.length);
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* Compute the composite health grade from all factors.
|
|
136
|
-
*/
|
|
137
|
-
export function computeHealthGrade(entities, edges, communityCohesion = 0.5) {
|
|
138
|
-
const cycles = detectCircularDeps(edges);
|
|
139
|
-
const cycleScore = Math.max(0, 100 - cycles.length * 15);
|
|
140
|
-
const fanInAnalysis = analyzeFanInDistribution(edges);
|
|
141
|
-
const fanInScore = Math.max(0, 100 - fanInAnalysis.giniCoefficient * 80);
|
|
142
|
-
const importDepth = measureImportDepth(edges);
|
|
143
|
-
const depthScore = importDepth <= 5 ? 100 : Math.max(0, 100 - (importDepth - 5) * 10);
|
|
144
|
-
const testRatio = computeTestCoverageProxy(entities);
|
|
145
|
-
const testScore = Math.min(100, testRatio * 100);
|
|
146
|
-
const cohesionScore = communityCohesion * 100;
|
|
147
|
-
const factors = [
|
|
148
|
-
{
|
|
149
|
-
name: "Circular Dependencies",
|
|
150
|
-
score: cycleScore,
|
|
151
|
-
weight: 0.3,
|
|
152
|
-
details: `${cycles.length} cycle(s) detected`,
|
|
153
|
-
},
|
|
154
|
-
{
|
|
155
|
-
name: "Fan-in Distribution",
|
|
156
|
-
score: fanInScore,
|
|
157
|
-
weight: 0.2,
|
|
158
|
-
details: `Gini: ${fanInAnalysis.giniCoefficient.toFixed(2)}, max: ${fanInAnalysis.maxFanIn}`,
|
|
159
|
-
},
|
|
160
|
-
{
|
|
161
|
-
name: "Import Depth",
|
|
162
|
-
score: depthScore,
|
|
163
|
-
weight: 0.15,
|
|
164
|
-
details: `Max depth: ${importDepth}`,
|
|
165
|
-
},
|
|
166
|
-
{
|
|
167
|
-
name: "Test Coverage Proxy",
|
|
168
|
-
score: testScore,
|
|
169
|
-
weight: 0.2,
|
|
170
|
-
details: `Ratio: ${(testRatio * 100).toFixed(0)}%`,
|
|
171
|
-
},
|
|
172
|
-
{
|
|
173
|
-
name: "Community Cohesion",
|
|
174
|
-
score: cohesionScore,
|
|
175
|
-
weight: 0.15,
|
|
176
|
-
details: `Avg cohesion: ${communityCohesion.toFixed(2)}`,
|
|
177
|
-
},
|
|
178
|
-
];
|
|
179
|
-
const compositeScore = factors.reduce((sum, f) => sum + f.score * f.weight, 0);
|
|
180
|
-
const grade = compositeScore >= 85
|
|
181
|
-
? "A"
|
|
182
|
-
: compositeScore >= 70
|
|
183
|
-
? "B"
|
|
184
|
-
: compositeScore >= 55
|
|
185
|
-
? "C"
|
|
186
|
-
: compositeScore >= 40
|
|
187
|
-
? "D"
|
|
188
|
-
: "F";
|
|
189
|
-
return {
|
|
190
|
-
grade,
|
|
191
|
-
score: Math.round(compositeScore),
|
|
192
|
-
factors,
|
|
193
|
-
circularDeps: cycles.map((c) => ({
|
|
194
|
-
cycle: c,
|
|
195
|
-
severity: c.length > 3 ? "high" : "medium",
|
|
196
|
-
})),
|
|
197
|
-
entityCount: entities.length,
|
|
198
|
-
edgeCount: edges.length,
|
|
199
|
-
};
|
|
200
|
-
}
|