mstro-app 0.5.0 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +129 -190
- package/PRIVACY.md +11 -11
- package/README.md +75 -28
- package/bin/commands/config.js +1 -2
- package/bin/mstro.js +55 -5
- package/bin/postinstall.js +0 -1
- package/dist/server/cli/eta-estimator.d.ts +55 -0
- package/dist/server/cli/eta-estimator.d.ts.map +1 -0
- package/dist/server/cli/eta-estimator.js +222 -0
- package/dist/server/cli/eta-estimator.js.map +1 -0
- package/dist/server/cli/headless/claude-invoker-process.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker-process.js +0 -1
- package/dist/server/cli/headless/claude-invoker-process.js.map +1 -1
- package/dist/server/cli/headless/claude-invoker-stall.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker-stall.js +0 -1
- package/dist/server/cli/headless/claude-invoker-stall.js.map +1 -1
- package/dist/server/cli/headless/claude-invoker-stream.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker-stream.js +0 -1
- package/dist/server/cli/headless/claude-invoker-stream.js.map +1 -1
- package/dist/server/cli/headless/claude-invoker-tools.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker-tools.js +0 -1
- package/dist/server/cli/headless/claude-invoker-tools.js.map +1 -1
- package/dist/server/cli/headless/claude-invoker.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker.js +0 -1
- package/dist/server/cli/headless/claude-invoker.js.map +1 -1
- package/dist/server/cli/headless/haiku-assessments.d.ts.map +1 -1
- package/dist/server/cli/headless/haiku-assessments.js +0 -1
- package/dist/server/cli/headless/haiku-assessments.js.map +1 -1
- package/dist/server/cli/headless/headless-logger.d.ts.map +1 -1
- package/dist/server/cli/headless/headless-logger.js +0 -1
- package/dist/server/cli/headless/headless-logger.js.map +1 -1
- package/dist/server/cli/headless/index.d.ts.map +1 -1
- package/dist/server/cli/headless/index.js +0 -1
- package/dist/server/cli/headless/index.js.map +1 -1
- package/dist/server/cli/headless/native-timeout-detector.d.ts.map +1 -1
- package/dist/server/cli/headless/native-timeout-detector.js +0 -1
- package/dist/server/cli/headless/native-timeout-detector.js.map +1 -1
- package/dist/server/cli/headless/output-utils.d.ts.map +1 -1
- package/dist/server/cli/headless/output-utils.js +0 -1
- package/dist/server/cli/headless/output-utils.js.map +1 -1
- package/dist/server/cli/headless/prompt-utils.d.ts.map +1 -1
- package/dist/server/cli/headless/prompt-utils.js +0 -1
- package/dist/server/cli/headless/prompt-utils.js.map +1 -1
- package/dist/server/cli/headless/resilient-runner.d.ts.map +1 -1
- package/dist/server/cli/headless/resilient-runner.js +0 -1
- package/dist/server/cli/headless/resilient-runner.js.map +1 -1
- package/dist/server/cli/headless/retry-strategies.d.ts.map +1 -1
- package/dist/server/cli/headless/retry-strategies.js +0 -1
- package/dist/server/cli/headless/retry-strategies.js.map +1 -1
- package/dist/server/cli/headless/runner.d.ts.map +1 -1
- package/dist/server/cli/headless/runner.js +0 -1
- package/dist/server/cli/headless/runner.js.map +1 -1
- package/dist/server/cli/headless/stall-assessor.d.ts +50 -0
- package/dist/server/cli/headless/stall-assessor.d.ts.map +1 -1
- package/dist/server/cli/headless/stall-assessor.js +64 -10
- package/dist/server/cli/headless/stall-assessor.js.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.d.ts +21 -0
- package/dist/server/cli/headless/tool-watchdog.d.ts.map +1 -1
- package/dist/server/cli/headless/tool-watchdog.js +19 -13
- package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
- package/dist/server/cli/headless/types.d.ts.map +1 -1
- package/dist/server/cli/headless/types.js +0 -1
- package/dist/server/cli/headless/types.js.map +1 -1
- package/dist/server/cli/improvisation-attachments.d.ts.map +1 -1
- package/dist/server/cli/improvisation-attachments.js +0 -1
- package/dist/server/cli/improvisation-attachments.js.map +1 -1
- package/dist/server/cli/improvisation-history-store.d.ts.map +1 -1
- package/dist/server/cli/improvisation-history-store.js +5 -2
- package/dist/server/cli/improvisation-history-store.js.map +1 -1
- package/dist/server/cli/improvisation-movements.d.ts.map +1 -1
- package/dist/server/cli/improvisation-movements.js +0 -1
- package/dist/server/cli/improvisation-movements.js.map +1 -1
- package/dist/server/cli/improvisation-output-queue.d.ts +5 -1
- package/dist/server/cli/improvisation-output-queue.d.ts.map +1 -1
- package/dist/server/cli/improvisation-output-queue.js +30 -8
- package/dist/server/cli/improvisation-output-queue.js.map +1 -1
- package/dist/server/cli/improvisation-retry.d.ts.map +1 -1
- package/dist/server/cli/improvisation-retry.js +0 -1
- package/dist/server/cli/improvisation-retry.js.map +1 -1
- package/dist/server/cli/improvisation-session-manager.d.ts +29 -0
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +50 -2
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/cli/improvisation-types.d.ts +2 -0
- package/dist/server/cli/improvisation-types.d.ts.map +1 -1
- package/dist/server/cli/improvisation-types.js +0 -1
- package/dist/server/cli/improvisation-types.js.map +1 -1
- package/dist/server/cli/retry/retry-best-result.d.ts.map +1 -1
- package/dist/server/cli/retry/retry-best-result.js +0 -1
- package/dist/server/cli/retry/retry-best-result.js.map +1 -1
- package/dist/server/cli/retry/retry-context-loss.d.ts.map +1 -1
- package/dist/server/cli/retry/retry-context-loss.js +0 -1
- package/dist/server/cli/retry/retry-context-loss.js.map +1 -1
- package/dist/server/cli/retry/retry-premature-completion.d.ts.map +1 -1
- package/dist/server/cli/retry/retry-premature-completion.js +1 -2
- package/dist/server/cli/retry/retry-premature-completion.js.map +1 -1
- package/dist/server/cli/retry/retry-recovery-strategies.d.ts.map +1 -1
- package/dist/server/cli/retry/retry-recovery-strategies.js +0 -1
- package/dist/server/cli/retry/retry-recovery-strategies.js.map +1 -1
- package/dist/server/cli/retry/retry-resume-strategy.d.ts.map +1 -1
- package/dist/server/cli/retry/retry-resume-strategy.js +0 -1
- package/dist/server/cli/retry/retry-resume-strategy.js.map +1 -1
- package/dist/server/cli/retry/retry-runner-factory.d.ts.map +1 -1
- package/dist/server/cli/retry/retry-runner-factory.js +0 -1
- package/dist/server/cli/retry/retry-runner-factory.js.map +1 -1
- package/dist/server/cli/retry/retry-tool-results.d.ts.map +1 -1
- package/dist/server/cli/retry/retry-tool-results.js +0 -1
- package/dist/server/cli/retry/retry-tool-results.js.map +1 -1
- package/dist/server/cli/retry/retry-types.d.ts.map +1 -1
- package/dist/server/cli/retry/retry-types.js +0 -1
- package/dist/server/cli/retry/retry-types.js.map +1 -1
- package/dist/server/engines/EngineEvent.d.ts +126 -0
- package/dist/server/engines/EngineEvent.d.ts.map +1 -0
- package/dist/server/engines/EngineEvent.js +11 -0
- package/dist/server/engines/EngineEvent.js.map +1 -0
- package/dist/server/engines/claude/ClaudeCodeEngine.d.ts +47 -0
- package/dist/server/engines/claude/ClaudeCodeEngine.d.ts.map +1 -0
- package/dist/server/engines/claude/ClaudeCodeEngine.js +338 -0
- package/dist/server/engines/claude/ClaudeCodeEngine.js.map +1 -0
- package/dist/server/engines/factory.d.ts +21 -0
- package/dist/server/engines/factory.d.ts.map +1 -0
- package/dist/server/engines/factory.js +152 -0
- package/dist/server/engines/factory.js.map +1 -0
- package/dist/server/engines/opencode/OpenCodeEngine.d.ts +148 -0
- package/dist/server/engines/opencode/OpenCodeEngine.d.ts.map +1 -0
- package/dist/server/engines/opencode/OpenCodeEngine.js +630 -0
- package/dist/server/engines/opencode/OpenCodeEngine.js.map +1 -0
- package/dist/server/engines/opencode/OpenCodeServerManager.d.ts +172 -0
- package/dist/server/engines/opencode/OpenCodeServerManager.d.ts.map +1 -0
- package/dist/server/engines/opencode/OpenCodeServerManager.js +390 -0
- package/dist/server/engines/opencode/OpenCodeServerManager.js.map +1 -0
- package/dist/server/engines/opencode/model-catalog.d.ts +94 -0
- package/dist/server/engines/opencode/model-catalog.d.ts.map +1 -0
- package/dist/server/engines/opencode/model-catalog.js +141 -0
- package/dist/server/engines/opencode/model-catalog.js.map +1 -0
- package/dist/server/engines/types.d.ts +146 -0
- package/dist/server/engines/types.d.ts.map +1 -0
- package/dist/server/engines/types.js +4 -0
- package/dist/server/engines/types.js.map +1 -0
- package/dist/server/index.js +1 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/bouncer-cli.js +0 -1
- package/dist/server/mcp/bouncer-cli.js.map +1 -1
- package/dist/server/mcp/bouncer-haiku.d.ts +17 -4
- package/dist/server/mcp/bouncer-haiku.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-haiku.js +8 -125
- package/dist/server/mcp/bouncer-haiku.js.map +1 -1
- package/dist/server/mcp/bouncer-integration.d.ts +45 -0
- package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-integration.js +69 -6
- package/dist/server/mcp/bouncer-integration.js.map +1 -1
- package/dist/server/mcp/classifier/BouncerClassifier.d.ts +34 -0
- package/dist/server/mcp/classifier/BouncerClassifier.d.ts.map +1 -0
- package/dist/server/mcp/classifier/BouncerClassifier.js +4 -0
- package/dist/server/mcp/classifier/BouncerClassifier.js.map +1 -0
- package/dist/server/mcp/classifier/ClaudeBouncerClassifier.d.ts +17 -0
- package/dist/server/mcp/classifier/ClaudeBouncerClassifier.d.ts.map +1 -0
- package/dist/server/mcp/classifier/ClaudeBouncerClassifier.js +142 -0
- package/dist/server/mcp/classifier/ClaudeBouncerClassifier.js.map +1 -0
- package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.d.ts +68 -0
- package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.d.ts.map +1 -0
- package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.js +182 -0
- package/dist/server/mcp/classifier/OpenCodeBouncerClassifier.js.map +1 -0
- package/dist/server/mcp/classifier/factory.d.ts +70 -0
- package/dist/server/mcp/classifier/factory.d.ts.map +1 -0
- package/dist/server/mcp/classifier/factory.js +155 -0
- package/dist/server/mcp/classifier/factory.js.map +1 -0
- package/dist/server/mcp/security-analysis.d.ts.map +1 -1
- package/dist/server/mcp/security-analysis.js +0 -1
- package/dist/server/mcp/security-analysis.js.map +1 -1
- package/dist/server/mcp/security-audit.d.ts.map +1 -1
- package/dist/server/mcp/security-audit.js +0 -1
- package/dist/server/mcp/security-audit.js.map +1 -1
- package/dist/server/mcp/security-patterns.d.ts.map +1 -1
- package/dist/server/mcp/security-patterns.js +0 -1
- package/dist/server/mcp/security-patterns.js.map +1 -1
- package/dist/server/mcp/server.js +0 -1
- package/dist/server/mcp/server.js.map +1 -1
- package/dist/server/routes/files.d.ts.map +1 -1
- package/dist/server/routes/files.js +0 -1
- package/dist/server/routes/files.js.map +1 -1
- package/dist/server/routes/improvise.d.ts.map +1 -1
- package/dist/server/routes/improvise.js +0 -1
- package/dist/server/routes/improvise.js.map +1 -1
- package/dist/server/routes/index.d.ts.map +1 -1
- package/dist/server/routes/index.js +0 -1
- package/dist/server/routes/index.js.map +1 -1
- package/dist/server/routes/instances.d.ts.map +1 -1
- package/dist/server/routes/instances.js +0 -1
- package/dist/server/routes/instances.js.map +1 -1
- package/dist/server/routes/notifications.d.ts.map +1 -1
- package/dist/server/routes/notifications.js +0 -1
- package/dist/server/routes/notifications.js.map +1 -1
- package/dist/server/server-setup.d.ts.map +1 -1
- package/dist/server/server-setup.js +0 -1
- package/dist/server/server-setup.js.map +1 -1
- package/dist/server/services/analytics.d.ts.map +1 -1
- package/dist/server/services/analytics.js +0 -1
- package/dist/server/services/analytics.js.map +1 -1
- package/dist/server/services/auth.d.ts.map +1 -1
- package/dist/server/services/auth.js +0 -1
- package/dist/server/services/auth.js.map +1 -1
- package/dist/server/services/client-id.d.ts.map +1 -1
- package/dist/server/services/client-id.js +0 -1
- package/dist/server/services/client-id.js.map +1 -1
- package/dist/server/services/file-explorer-ops.d.ts.map +1 -1
- package/dist/server/services/file-explorer-ops.js +0 -1
- package/dist/server/services/file-explorer-ops.js.map +1 -1
- package/dist/server/services/files.d.ts.map +1 -1
- package/dist/server/services/files.js +0 -1
- package/dist/server/services/files.js.map +1 -1
- package/dist/server/services/instances.d.ts.map +1 -1
- package/dist/server/services/instances.js +0 -1
- package/dist/server/services/instances.js.map +1 -1
- package/dist/server/services/pathUtils.d.ts.map +1 -1
- package/dist/server/services/pathUtils.js +0 -1
- package/dist/server/services/pathUtils.js.map +1 -1
- package/dist/server/services/plan/agent-loader.d.ts.map +1 -1
- package/dist/server/services/plan/agent-loader.js +0 -1
- package/dist/server/services/plan/agent-loader.js.map +1 -1
- package/dist/server/services/plan/agent-resolver.d.ts +26 -0
- package/dist/server/services/plan/agent-resolver.d.ts.map +1 -0
- package/dist/server/services/plan/agent-resolver.js +102 -0
- package/dist/server/services/plan/agent-resolver.js.map +1 -0
- package/dist/server/services/plan/board-config.d.ts.map +1 -1
- package/dist/server/services/plan/board-config.js +0 -1
- package/dist/server/services/plan/board-config.js.map +1 -1
- package/dist/server/services/plan/composer.d.ts.map +1 -1
- package/dist/server/services/plan/composer.js +59 -12
- package/dist/server/services/plan/composer.js.map +1 -1
- package/dist/server/services/plan/config-installer.d.ts.map +1 -1
- package/dist/server/services/plan/config-installer.js +0 -1
- package/dist/server/services/plan/config-installer.js.map +1 -1
- package/dist/server/services/plan/dependency-resolver.d.ts.map +1 -1
- package/dist/server/services/plan/dependency-resolver.js +0 -1
- package/dist/server/services/plan/dependency-resolver.js.map +1 -1
- package/dist/server/services/plan/executor.d.ts.map +1 -1
- package/dist/server/services/plan/executor.js +48 -4
- package/dist/server/services/plan/executor.js.map +1 -1
- package/dist/server/services/plan/front-matter.d.ts.map +1 -1
- package/dist/server/services/plan/front-matter.js +0 -1
- package/dist/server/services/plan/front-matter.js.map +1 -1
- package/dist/server/services/plan/issue-classification.d.ts.map +1 -1
- package/dist/server/services/plan/issue-classification.js +0 -1
- package/dist/server/services/plan/issue-classification.js.map +1 -1
- package/dist/server/services/plan/issue-loader.d.ts.map +1 -1
- package/dist/server/services/plan/issue-loader.js +0 -1
- package/dist/server/services/plan/issue-loader.js.map +1 -1
- package/dist/server/services/plan/issue-prompt-builder.d.ts.map +1 -1
- package/dist/server/services/plan/issue-prompt-builder.js +33 -2
- package/dist/server/services/plan/issue-prompt-builder.js.map +1 -1
- package/dist/server/services/plan/issue-retry.d.ts +3 -1
- package/dist/server/services/plan/issue-retry.d.ts.map +1 -1
- package/dist/server/services/plan/issue-retry.js +2 -1
- package/dist/server/services/plan/issue-retry.js.map +1 -1
- package/dist/server/services/plan/issue-writer.d.ts.map +1 -1
- package/dist/server/services/plan/issue-writer.js +0 -1
- package/dist/server/services/plan/issue-writer.js.map +1 -1
- package/dist/server/services/plan/output-manager.d.ts.map +1 -1
- package/dist/server/services/plan/output-manager.js +0 -1
- package/dist/server/services/plan/output-manager.js.map +1 -1
- package/dist/server/services/plan/parser-core.d.ts.map +1 -1
- package/dist/server/services/plan/parser-core.js +1 -1
- package/dist/server/services/plan/parser-core.js.map +1 -1
- package/dist/server/services/plan/parser-migration.d.ts.map +1 -1
- package/dist/server/services/plan/parser-migration.js +0 -1
- package/dist/server/services/plan/parser-migration.js.map +1 -1
- package/dist/server/services/plan/parser.d.ts.map +1 -1
- package/dist/server/services/plan/parser.js +0 -1
- package/dist/server/services/plan/parser.js.map +1 -1
- package/dist/server/services/plan/progress-log.d.ts.map +1 -1
- package/dist/server/services/plan/progress-log.js +0 -1
- package/dist/server/services/plan/progress-log.js.map +1 -1
- package/dist/server/services/plan/prompt-builder.d.ts.map +1 -1
- package/dist/server/services/plan/prompt-builder.js +0 -1
- package/dist/server/services/plan/prompt-builder.js.map +1 -1
- package/dist/server/services/plan/readiness-planner.d.ts.map +1 -1
- package/dist/server/services/plan/readiness-planner.js +0 -1
- package/dist/server/services/plan/readiness-planner.js.map +1 -1
- package/dist/server/services/plan/review-gate.d.ts.map +1 -1
- package/dist/server/services/plan/review-gate.js +0 -1
- package/dist/server/services/plan/review-gate.js.map +1 -1
- package/dist/server/services/plan/state-reconciler.d.ts.map +1 -1
- package/dist/server/services/plan/state-reconciler.js +0 -1
- package/dist/server/services/plan/state-reconciler.js.map +1 -1
- package/dist/server/services/plan/types.d.ts +1 -0
- package/dist/server/services/plan/types.d.ts.map +1 -1
- package/dist/server/services/plan/types.js +0 -1
- package/dist/server/services/plan/types.js.map +1 -1
- package/dist/server/services/plan/watcher.d.ts.map +1 -1
- package/dist/server/services/plan/watcher.js +0 -1
- package/dist/server/services/plan/watcher.js.map +1 -1
- package/dist/server/services/platform-credentials.d.ts.map +1 -1
- package/dist/server/services/platform-credentials.js +0 -1
- package/dist/server/services/platform-credentials.js.map +1 -1
- package/dist/server/services/platform-token-lifecycle.d.ts +70 -0
- package/dist/server/services/platform-token-lifecycle.d.ts.map +1 -0
- package/dist/server/services/platform-token-lifecycle.js +156 -0
- package/dist/server/services/platform-token-lifecycle.js.map +1 -0
- package/dist/server/services/platform.d.ts +21 -56
- package/dist/server/services/platform.d.ts.map +1 -1
- package/dist/server/services/platform.js +98 -142
- package/dist/server/services/platform.js.map +1 -1
- package/dist/server/services/sentry.d.ts.map +1 -1
- package/dist/server/services/sentry.js +0 -1
- package/dist/server/services/sentry.js.map +1 -1
- package/dist/server/services/settings.d.ts +76 -2
- package/dist/server/services/settings.d.ts.map +1 -1
- package/dist/server/services/settings.js +127 -5
- package/dist/server/services/settings.js.map +1 -1
- package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
- package/dist/server/services/terminal/pty-manager.js +0 -1
- package/dist/server/services/terminal/pty-manager.js.map +1 -1
- package/dist/server/services/terminal/pty-utils.d.ts.map +1 -1
- package/dist/server/services/terminal/pty-utils.js +0 -1
- package/dist/server/services/terminal/pty-utils.js.map +1 -1
- package/dist/server/services/websocket/autocomplete.d.ts.map +1 -1
- package/dist/server/services/websocket/autocomplete.js +0 -1
- package/dist/server/services/websocket/autocomplete.js.map +1 -1
- package/dist/server/services/websocket/file-definition-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/file-definition-handlers.js +0 -1
- package/dist/server/services/websocket/file-definition-handlers.js.map +1 -1
- package/dist/server/services/websocket/file-download-handler.d.ts.map +1 -1
- package/dist/server/services/websocket/file-download-handler.js +0 -1
- package/dist/server/services/websocket/file-download-handler.js.map +1 -1
- package/dist/server/services/websocket/file-explorer-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/file-explorer-handlers.js +0 -1
- package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -1
- package/dist/server/services/websocket/file-search-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/file-search-handlers.js +0 -1
- package/dist/server/services/websocket/file-search-handlers.js.map +1 -1
- package/dist/server/services/websocket/file-upload-handler.d.ts +2 -3
- package/dist/server/services/websocket/file-upload-handler.d.ts.map +1 -1
- package/dist/server/services/websocket/file-upload-handler.js +4 -7
- package/dist/server/services/websocket/file-upload-handler.js.map +1 -1
- package/dist/server/services/websocket/file-utils.d.ts.map +1 -1
- package/dist/server/services/websocket/file-utils.js +0 -1
- package/dist/server/services/websocket/file-utils.js.map +1 -1
- package/dist/server/services/websocket/git-branch-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-branch-handlers.js +19 -7
- package/dist/server/services/websocket/git-branch-handlers.js.map +1 -1
- package/dist/server/services/websocket/git-diff-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-diff-handlers.js +0 -1
- package/dist/server/services/websocket/git-diff-handlers.js.map +1 -1
- package/dist/server/services/websocket/git-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-handlers.js +58 -6
- package/dist/server/services/websocket/git-handlers.js.map +1 -1
- package/dist/server/services/websocket/git-head-watcher.d.ts.map +1 -1
- package/dist/server/services/websocket/git-head-watcher.js +0 -1
- package/dist/server/services/websocket/git-head-watcher.js.map +1 -1
- package/dist/server/services/websocket/git-log-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-log-handlers.js +0 -1
- package/dist/server/services/websocket/git-log-handlers.js.map +1 -1
- package/dist/server/services/websocket/git-pr-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-pr-handlers.js +0 -1
- package/dist/server/services/websocket/git-pr-handlers.js.map +1 -1
- package/dist/server/services/websocket/git-tag-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-tag-handlers.js +0 -1
- package/dist/server/services/websocket/git-tag-handlers.js.map +1 -1
- package/dist/server/services/websocket/git-utils.d.ts +18 -3
- package/dist/server/services/websocket/git-utils.d.ts.map +1 -1
- package/dist/server/services/websocket/git-utils.js +58 -8
- package/dist/server/services/websocket/git-utils.js.map +1 -1
- package/dist/server/services/websocket/git-worktree-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-worktree-handlers.js +230 -14
- package/dist/server/services/websocket/git-worktree-handlers.js.map +1 -1
- package/dist/server/services/websocket/handler-context.d.ts.map +1 -1
- package/dist/server/services/websocket/handler-context.js +0 -1
- package/dist/server/services/websocket/handler-context.js.map +1 -1
- package/dist/server/services/websocket/handler.d.ts +17 -1
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +57 -6
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/index.d.ts.map +1 -1
- package/dist/server/services/websocket/index.js +0 -1
- package/dist/server/services/websocket/index.js.map +1 -1
- package/dist/server/services/websocket/msg-id-tracker.d.ts.map +1 -1
- package/dist/server/services/websocket/msg-id-tracker.js +0 -1
- package/dist/server/services/websocket/msg-id-tracker.js.map +1 -1
- package/dist/server/services/websocket/plan-board-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-board-handlers.js +0 -1
- package/dist/server/services/websocket/plan-board-handlers.js.map +1 -1
- package/dist/server/services/websocket/plan-execution-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-execution-handlers.js +6 -2
- package/dist/server/services/websocket/plan-execution-handlers.js.map +1 -1
- package/dist/server/services/websocket/plan-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-handlers.js +0 -1
- package/dist/server/services/websocket/plan-handlers.js.map +1 -1
- package/dist/server/services/websocket/plan-helpers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-helpers.js +0 -1
- package/dist/server/services/websocket/plan-helpers.js.map +1 -1
- package/dist/server/services/websocket/plan-issue-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-issue-handlers.js +0 -1
- package/dist/server/services/websocket/plan-issue-handlers.js.map +1 -1
- package/dist/server/services/websocket/plan-sprint-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-sprint-handlers.js +0 -1
- package/dist/server/services/websocket/plan-sprint-handlers.js.map +1 -1
- package/dist/server/services/websocket/quality-complexity.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-complexity.js +78 -27
- package/dist/server/services/websocket/quality-complexity.js.map +1 -1
- package/dist/server/services/websocket/quality-eta.d.ts +47 -0
- package/dist/server/services/websocket/quality-eta.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-eta.js +110 -0
- package/dist/server/services/websocket/quality-eta.js.map +1 -0
- package/dist/server/services/websocket/quality-grading.d.ts +69 -0
- package/dist/server/services/websocket/quality-grading.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-grading.js +650 -0
- package/dist/server/services/websocket/quality-grading.js.map +1 -0
- package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-handlers.js +145 -8
- package/dist/server/services/websocket/quality-handlers.js.map +1 -1
- package/dist/server/services/websocket/quality-linting.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-linting.js +0 -1
- package/dist/server/services/websocket/quality-linting.js.map +1 -1
- package/dist/server/services/websocket/quality-operations.d.ts +34 -0
- package/dist/server/services/websocket/quality-operations.d.ts.map +1 -0
- package/dist/server/services/websocket/quality-operations.js +47 -0
- package/dist/server/services/websocket/quality-operations.js.map +1 -0
- package/dist/server/services/websocket/quality-persistence.d.ts +23 -0
- package/dist/server/services/websocket/quality-persistence.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-persistence.js +38 -12
- package/dist/server/services/websocket/quality-persistence.js.map +1 -1
- package/dist/server/services/websocket/quality-review-agent.d.ts +1 -1
- package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-review-agent.js +105 -57
- package/dist/server/services/websocket/quality-review-agent.js.map +1 -1
- package/dist/server/services/websocket/quality-service.d.ts +12 -2
- package/dist/server/services/websocket/quality-service.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-service.js +387 -72
- package/dist/server/services/websocket/quality-service.js.map +1 -1
- package/dist/server/services/websocket/quality-tools.d.ts +22 -1
- package/dist/server/services/websocket/quality-tools.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-tools.js +55 -3
- package/dist/server/services/websocket/quality-tools.js.map +1 -1
- package/dist/server/services/websocket/quality-types.d.ts +52 -3
- package/dist/server/services/websocket/quality-types.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-types.js +1 -2
- package/dist/server/services/websocket/quality-types.js.map +1 -1
- package/dist/server/services/websocket/session-handlers.d.ts +3 -1
- package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/session-handlers.js +57 -10
- package/dist/server/services/websocket/session-handlers.js.map +1 -1
- package/dist/server/services/websocket/session-history.d.ts.map +1 -1
- package/dist/server/services/websocket/session-history.js +3 -1
- package/dist/server/services/websocket/session-history.js.map +1 -1
- package/dist/server/services/websocket/session-initialization.d.ts.map +1 -1
- package/dist/server/services/websocket/session-initialization.js +158 -43
- package/dist/server/services/websocket/session-initialization.js.map +1 -1
- package/dist/server/services/websocket/session-registry.d.ts +25 -0
- package/dist/server/services/websocket/session-registry.d.ts.map +1 -1
- package/dist/server/services/websocket/session-registry.js +19 -1
- package/dist/server/services/websocket/session-registry.js.map +1 -1
- package/dist/server/services/websocket/settings-handlers.d.ts +1 -1
- package/dist/server/services/websocket/settings-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/settings-handlers.js +35 -5
- package/dist/server/services/websocket/settings-handlers.js.map +1 -1
- package/dist/server/services/websocket/skill-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/skill-handlers.js +0 -1
- package/dist/server/services/websocket/skill-handlers.js.map +1 -1
- package/dist/server/services/websocket/skill-watcher.d.ts.map +1 -1
- package/dist/server/services/websocket/skill-watcher.js +0 -1
- package/dist/server/services/websocket/skill-watcher.js.map +1 -1
- package/dist/server/services/websocket/tab-broadcast.d.ts +7 -2
- package/dist/server/services/websocket/tab-broadcast.d.ts.map +1 -1
- package/dist/server/services/websocket/tab-broadcast.js +10 -3
- package/dist/server/services/websocket/tab-broadcast.js.map +1 -1
- package/dist/server/services/websocket/tab-event-buffer.d.ts +97 -8
- package/dist/server/services/websocket/tab-event-buffer.d.ts.map +1 -1
- package/dist/server/services/websocket/tab-event-buffer.js +138 -13
- package/dist/server/services/websocket/tab-event-buffer.js.map +1 -1
- package/dist/server/services/websocket/tab-event-replay.d.ts +29 -13
- package/dist/server/services/websocket/tab-event-replay.d.ts.map +1 -1
- package/dist/server/services/websocket/tab-event-replay.js +55 -3
- package/dist/server/services/websocket/tab-event-replay.js.map +1 -1
- package/dist/server/services/websocket/tab-handlers.d.ts +9 -1
- package/dist/server/services/websocket/tab-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/tab-handlers.js +47 -3
- package/dist/server/services/websocket/tab-handlers.js.map +1 -1
- package/dist/server/services/websocket/terminal-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/terminal-handlers.js +39 -4
- package/dist/server/services/websocket/terminal-handlers.js.map +1 -1
- package/dist/server/services/websocket/types.d.ts +30 -7
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/dist/server/services/websocket/types.js +12 -7
- package/dist/server/services/websocket/types.js.map +1 -1
- package/dist/server/utils/agent-manager.d.ts.map +1 -1
- package/dist/server/utils/agent-manager.js +0 -1
- package/dist/server/utils/agent-manager.js.map +1 -1
- package/dist/server/utils/paths.d.ts.map +1 -1
- package/dist/server/utils/paths.js +0 -1
- package/dist/server/utils/paths.js.map +1 -1
- package/dist/server/utils/port-manager.d.ts.map +1 -1
- package/dist/server/utils/port-manager.js +0 -1
- package/dist/server/utils/port-manager.js.map +1 -1
- package/dist/server/utils/port.d.ts.map +1 -1
- package/dist/server/utils/port.js +0 -1
- package/dist/server/utils/port.js.map +1 -1
- package/package.json +6 -4
- package/server/cli/eta-estimator.ts +249 -0
- package/server/cli/headless/claude-invoker-process.ts +0 -1
- package/server/cli/headless/claude-invoker-stall.ts +0 -1
- package/server/cli/headless/claude-invoker-stream.ts +0 -1
- package/server/cli/headless/claude-invoker-tools.ts +0 -1
- package/server/cli/headless/claude-invoker.ts +0 -1
- package/server/cli/headless/haiku-assessments.ts +0 -1
- package/server/cli/headless/headless-logger.ts +0 -1
- package/server/cli/headless/index.ts +0 -1
- package/server/cli/headless/native-timeout-detector.ts +0 -1
- package/server/cli/headless/output-utils.ts +0 -1
- package/server/cli/headless/prompt-utils.ts +0 -1
- package/server/cli/headless/resilient-runner.ts +0 -1
- package/server/cli/headless/retry-strategies.ts +0 -1
- package/server/cli/headless/runner.ts +0 -1
- package/server/cli/headless/stall-assessor.ts +93 -1
- package/server/cli/headless/tool-watchdog.ts +21 -1
- package/server/cli/headless/types.ts +0 -1
- package/server/cli/improvisation-attachments.ts +0 -1
- package/server/cli/improvisation-history-store.ts +4 -2
- package/server/cli/improvisation-movements.ts +0 -1
- package/server/cli/improvisation-output-queue.ts +29 -8
- package/server/cli/improvisation-retry.ts +0 -1
- package/server/cli/improvisation-session-manager.ts +54 -2
- package/server/cli/improvisation-types.ts +2 -1
- package/server/cli/retry/retry-best-result.ts +0 -1
- package/server/cli/retry/retry-context-loss.ts +0 -1
- package/server/cli/retry/retry-premature-completion.ts +1 -2
- package/server/cli/retry/retry-recovery-strategies.ts +0 -1
- package/server/cli/retry/retry-resume-strategy.ts +0 -1
- package/server/cli/retry/retry-runner-factory.ts +0 -1
- package/server/cli/retry/retry-tool-results.ts +0 -1
- package/server/cli/retry/retry-types.ts +0 -1
- package/server/engines/EngineEvent.ts +156 -0
- package/server/engines/claude/ClaudeCodeEngine.ts +404 -0
- package/server/engines/factory.ts +176 -0
- package/server/engines/opencode/OpenCodeEngine.ts +786 -0
- package/server/engines/opencode/OpenCodeServerManager.ts +577 -0
- package/server/engines/opencode/model-catalog.ts +217 -0
- package/server/engines/types.ts +173 -0
- package/server/index.ts +1 -2
- package/server/mcp/bouncer-cli.ts +0 -1
- package/server/mcp/bouncer-haiku.ts +21 -146
- package/server/mcp/bouncer-integration.ts +107 -6
- package/server/mcp/classifier/BouncerClassifier.ts +40 -0
- package/server/mcp/classifier/ClaudeBouncerClassifier.ts +189 -0
- package/server/mcp/classifier/OpenCodeBouncerClassifier.ts +305 -0
- package/server/mcp/classifier/factory.ts +195 -0
- package/server/mcp/security-analysis.ts +0 -1
- package/server/mcp/security-audit.ts +0 -1
- package/server/mcp/security-patterns.ts +0 -1
- package/server/mcp/server.ts +0 -1
- package/server/routes/files.ts +0 -1
- package/server/routes/improvise.ts +0 -1
- package/server/routes/index.ts +0 -1
- package/server/routes/instances.ts +0 -1
- package/server/routes/notifications.ts +0 -1
- package/server/server-setup.ts +0 -1
- package/server/services/analytics.ts +0 -1
- package/server/services/auth.ts +0 -1
- package/server/services/client-id.ts +0 -1
- package/server/services/file-explorer-ops.ts +0 -1
- package/server/services/files.ts +0 -1
- package/server/services/instances.ts +0 -1
- package/server/services/pathUtils.ts +0 -1
- package/server/services/plan/agent-loader.ts +0 -1
- package/server/services/plan/agent-resolver.ts +115 -0
- package/server/services/plan/agents/code-review.md +43 -11
- package/server/services/plan/board-config.ts +0 -1
- package/server/services/plan/composer.ts +63 -12
- package/server/services/plan/config-installer.ts +0 -1
- package/server/services/plan/dependency-resolver.ts +0 -1
- package/server/services/plan/executor.ts +48 -4
- package/server/services/plan/front-matter.ts +0 -1
- package/server/services/plan/issue-classification.ts +0 -1
- package/server/services/plan/issue-loader.ts +0 -1
- package/server/services/plan/issue-prompt-builder.ts +39 -2
- package/server/services/plan/issue-retry.ts +5 -2
- package/server/services/plan/issue-writer.ts +0 -1
- package/server/services/plan/output-manager.ts +0 -1
- package/server/services/plan/parser-core.ts +1 -1
- package/server/services/plan/parser-migration.ts +0 -1
- package/server/services/plan/parser.ts +0 -1
- package/server/services/plan/progress-log.ts +0 -1
- package/server/services/plan/prompt-builder.ts +0 -1
- package/server/services/plan/readiness-planner.ts +0 -1
- package/server/services/plan/review-gate.ts +0 -1
- package/server/services/plan/state-reconciler.ts +0 -1
- package/server/services/plan/types.ts +4 -1
- package/server/services/plan/watcher.ts +0 -1
- package/server/services/platform-credentials.ts +0 -1
- package/server/services/platform-token-lifecycle.ts +171 -0
- package/server/services/platform.ts +106 -148
- package/server/services/sentry.ts +0 -1
- package/server/services/settings.ts +161 -5
- package/server/services/terminal/pty-manager.ts +0 -1
- package/server/services/terminal/pty-utils.ts +0 -1
- package/server/services/websocket/autocomplete.ts +0 -1
- package/server/services/websocket/file-definition-handlers.ts +0 -1
- package/server/services/websocket/file-download-handler.ts +0 -1
- package/server/services/websocket/file-explorer-handlers.ts +0 -1
- package/server/services/websocket/file-search-handlers.ts +0 -1
- package/server/services/websocket/file-upload-handler.ts +6 -5
- package/server/services/websocket/file-utils.ts +0 -1
- package/server/services/websocket/git-branch-handlers.ts +20 -7
- package/server/services/websocket/git-diff-handlers.ts +0 -1
- package/server/services/websocket/git-handlers.ts +66 -10
- package/server/services/websocket/git-head-watcher.ts +0 -1
- package/server/services/websocket/git-log-handlers.ts +0 -1
- package/server/services/websocket/git-pr-handlers.ts +0 -1
- package/server/services/websocket/git-tag-handlers.ts +0 -1
- package/server/services/websocket/git-utils.ts +69 -9
- package/server/services/websocket/git-worktree-handlers.ts +260 -17
- package/server/services/websocket/handler-context.ts +0 -1
- package/server/services/websocket/handler.ts +62 -6
- package/server/services/websocket/index.ts +0 -1
- package/server/services/websocket/msg-id-tracker.ts +0 -1
- package/server/services/websocket/plan-board-handlers.ts +0 -1
- package/server/services/websocket/plan-execution-handlers.ts +6 -2
- package/server/services/websocket/plan-handlers.ts +0 -1
- package/server/services/websocket/plan-helpers.ts +0 -1
- package/server/services/websocket/plan-issue-handlers.ts +0 -1
- package/server/services/websocket/plan-sprint-handlers.ts +0 -1
- package/server/services/websocket/quality-complexity.ts +80 -27
- package/server/services/websocket/quality-eta.ts +155 -0
- package/server/services/websocket/quality-grading.ts +834 -0
- package/server/services/websocket/quality-handlers.ts +153 -8
- package/server/services/websocket/quality-linting.ts +0 -1
- package/server/services/websocket/quality-operations.ts +72 -0
- package/server/services/websocket/quality-persistence.ts +47 -8
- package/server/services/websocket/quality-review-agent.ts +154 -65
- package/server/services/websocket/quality-service.ts +415 -68
- package/server/services/websocket/quality-tools.ts +62 -3
- package/server/services/websocket/quality-types.ts +61 -4
- package/server/services/websocket/session-handlers.ts +64 -11
- package/server/services/websocket/session-history.ts +3 -1
- package/server/services/websocket/session-initialization.ts +189 -47
- package/server/services/websocket/session-registry.ts +37 -1
- package/server/services/websocket/settings-handlers.ts +41 -5
- package/server/services/websocket/skill-handlers.ts +0 -1
- package/server/services/websocket/skill-watcher.ts +0 -1
- package/server/services/websocket/tab-broadcast.ts +10 -3
- package/server/services/websocket/tab-event-buffer.ts +143 -12
- package/server/services/websocket/tab-event-replay.ts +70 -4
- package/server/services/websocket/tab-handlers.ts +53 -6
- package/server/services/websocket/terminal-handlers.ts +39 -3
- package/server/services/websocket/types.ts +39 -8
- package/server/utils/agent-manager.ts +0 -1
- package/server/utils/paths.ts +0 -1
- package/server/utils/port-manager.ts +0 -1
- package/server/utils/port.ts +0 -1
|
@@ -0,0 +1,834 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Multi-Dimensional Quality Grading
|
|
5
|
+
// ============================================================================
|
|
6
|
+
//
|
|
7
|
+
// Three independent dimensions, severity-driven where it matters most:
|
|
8
|
+
//
|
|
9
|
+
// - Security — strictest; any medium-or-worse issue capped below B
|
|
10
|
+
// - Reliability — slightly lenient (not every complexity warning is a bug)
|
|
11
|
+
// - Maintainability — density-based (issues / KLOC) with a severity cap
|
|
12
|
+
//
|
|
13
|
+
// Letter grade scale (no `D` band — F covers 56-69, F- below 55):
|
|
14
|
+
//
|
|
15
|
+
// A+ 97-100 A 93-96 A- 90-92 "ship it"
|
|
16
|
+
// B+ 87-89 B 83-86 B- 80-82 "minor cleanup"
|
|
17
|
+
// C+ 77-79 C 73-76 C- 70-72 "needs work"
|
|
18
|
+
// F+ 65-69 F 56-64 F- 0-55 "broken"
|
|
19
|
+
//
|
|
20
|
+
// Three "auto-fail" rules layer on top of the dimension grades:
|
|
21
|
+
//
|
|
22
|
+
// 1. Build/compile errors → caps Reliability at F-
|
|
23
|
+
// 2. Critical security issue → caps Security at F-
|
|
24
|
+
// 3. Architectural findings → drop the affected dim's grade by 1-2 letters
|
|
25
|
+
//
|
|
26
|
+
// Industry alignment: SonarQube uses severity-driven A-E grades; Code Climate
|
|
27
|
+
// uses density-driven A-F. This module borrows the strictness of the former
|
|
28
|
+
// for Security/Reliability and the density model of the latter for
|
|
29
|
+
// Maintainability — matching the two metrics where each works best.
|
|
30
|
+
//
|
|
31
|
+
// All functions in this module are pure: same inputs -> same outputs, no I/O.
|
|
32
|
+
// ============================================================================
|
|
33
|
+
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Types
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
export type DimensionName = 'security' | 'reliability' | 'maintainability';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Letter grades. New code emits all variants except `'D'` (kept only for
|
|
42
|
+
* legacy reports persisted before the +/- rollout). See quality-types.ts for
|
|
43
|
+
* the score-band reference.
|
|
44
|
+
*/
|
|
45
|
+
export type Grade =
|
|
46
|
+
| 'A+' | 'A' | 'A-'
|
|
47
|
+
| 'B+' | 'B' | 'B-'
|
|
48
|
+
| 'C+' | 'C' | 'C-'
|
|
49
|
+
| 'D'
|
|
50
|
+
| 'F+' | 'F' | 'F-'
|
|
51
|
+
| 'N/A';
|
|
52
|
+
|
|
53
|
+
/** Letter grades excluding modifiers — used internally for band logic. */
|
|
54
|
+
type BaseGrade = 'A' | 'B' | 'C' | 'F';
|
|
55
|
+
|
|
56
|
+
type Severity = 'critical' | 'high' | 'medium' | 'low';
|
|
57
|
+
|
|
58
|
+
export interface DimensionScore {
|
|
59
|
+
name: DimensionName;
|
|
60
|
+
score: number;
|
|
61
|
+
grade: Grade;
|
|
62
|
+
rationale: string;
|
|
63
|
+
available: boolean;
|
|
64
|
+
findingCount: number;
|
|
65
|
+
worstSeverity: Severity | null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface QualityGate {
|
|
69
|
+
passed: boolean;
|
|
70
|
+
failingConditions: string[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface QualityRating {
|
|
74
|
+
overall: { score: number; grade: Grade };
|
|
75
|
+
dimensions: DimensionScore[];
|
|
76
|
+
qualityGate: QualityGate;
|
|
77
|
+
gradeRationale: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ============================================================================
|
|
81
|
+
// Category -> Dimension Mapping
|
|
82
|
+
// ============================================================================
|
|
83
|
+
|
|
84
|
+
const SECURITY_CATEGORIES = new Set<string>(['security']);
|
|
85
|
+
const RELIABILITY_CATEGORIES = new Set<string>(['bugs', 'logic', 'performance', 'complexity', 'build']);
|
|
86
|
+
const MAINTAINABILITY_CATEGORIES = new Set<string>([
|
|
87
|
+
'lint',
|
|
88
|
+
'linting',
|
|
89
|
+
'format',
|
|
90
|
+
'file-length',
|
|
91
|
+
'function-length',
|
|
92
|
+
'architecture',
|
|
93
|
+
'oop',
|
|
94
|
+
'maintainability',
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Map a finding category to one of the three quality dimensions.
|
|
99
|
+
* Unknown categories default to maintainability (the catch-all bucket) so
|
|
100
|
+
* that surprise categories never silently disappear from the grade.
|
|
101
|
+
*/
|
|
102
|
+
export function categoryToDimension(category: string): DimensionName {
|
|
103
|
+
if (SECURITY_CATEGORIES.has(category)) return 'security';
|
|
104
|
+
if (RELIABILITY_CATEGORIES.has(category)) return 'reliability';
|
|
105
|
+
if (MAINTAINABILITY_CATEGORIES.has(category)) return 'maintainability';
|
|
106
|
+
return 'maintainability';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Categories that represent architectural problems — used by the arch penalty. */
|
|
110
|
+
const ARCHITECTURE_CATEGORIES = new Set<string>(['architecture', 'oop']);
|
|
111
|
+
|
|
112
|
+
// ============================================================================
|
|
113
|
+
// Score Bands & Modifier Math
|
|
114
|
+
// ============================================================================
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Score boundaries for each base grade. Note the gap between C (70+) and F+
|
|
118
|
+
* (≤69): the band 60-69 maps to F+ instead of D, per product spec ("60s and
|
|
119
|
+
* below is F").
|
|
120
|
+
*/
|
|
121
|
+
const BASE_BAND_TOP: Record<BaseGrade, number> = {
|
|
122
|
+
A: 100,
|
|
123
|
+
B: 89,
|
|
124
|
+
C: 79,
|
|
125
|
+
F: 69, // F covers 56-69 (F+ for 65-69, F for 56-64) — F- splits off below
|
|
126
|
+
};
|
|
127
|
+
const BASE_BAND_BOTTOM: Record<BaseGrade, number> = {
|
|
128
|
+
A: 90,
|
|
129
|
+
B: 80,
|
|
130
|
+
C: 70,
|
|
131
|
+
F: 56, // F- covers 0-55 — handled specially in scoreToGrade()
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Convert a 0-100 score to the full letter grade including +/- modifier.
|
|
136
|
+
*
|
|
137
|
+
* Within an A/B/C band, the band is split into thirds:
|
|
138
|
+
* X- bottom third (e.g., A-: 90-92)
|
|
139
|
+
* X middle third (e.g., A : 93-96)
|
|
140
|
+
* X+ top third (e.g., A+: 97-100)
|
|
141
|
+
*
|
|
142
|
+
* The F band uses two slices instead of three because there is no academic
|
|
143
|
+
* "F0" anchor and the user wanted F+/F/F-:
|
|
144
|
+
* F- 0-55 "critically broken"
|
|
145
|
+
* F 56-64 "broken"
|
|
146
|
+
* F+ 65-69 "barely failing"
|
|
147
|
+
*
|
|
148
|
+
* Compile/critical-severity hard caps are applied separately, not by score.
|
|
149
|
+
*/
|
|
150
|
+
export function scoreToGrade(score: number): Grade {
|
|
151
|
+
if (score >= 97) return 'A+';
|
|
152
|
+
if (score >= 93) return 'A';
|
|
153
|
+
if (score >= 90) return 'A-';
|
|
154
|
+
if (score >= 87) return 'B+';
|
|
155
|
+
if (score >= 83) return 'B';
|
|
156
|
+
if (score >= 80) return 'B-';
|
|
157
|
+
if (score >= 77) return 'C+';
|
|
158
|
+
if (score >= 73) return 'C';
|
|
159
|
+
if (score >= 70) return 'C-';
|
|
160
|
+
if (score >= 65) return 'F+';
|
|
161
|
+
if (score >= 56) return 'F';
|
|
162
|
+
return 'F-';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Legacy single-letter conversion. Returns the *base* grade only (no
|
|
167
|
+
* modifier) for compatibility with callers that pre-date the +/- rollout
|
|
168
|
+
* (`scoreBreakdown.categoryPenalties[].grade`, etc.). New surfaces should
|
|
169
|
+
* call `scoreToGrade()` instead.
|
|
170
|
+
*/
|
|
171
|
+
export function gradeFromScore(score: number): Grade {
|
|
172
|
+
const full = scoreToGrade(score);
|
|
173
|
+
// Strip the modifier so legacy callers still see exactly one of A/B/C/F.
|
|
174
|
+
return baseGradeOf(full);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Strip the +/- modifier from a letter grade. */
|
|
178
|
+
function baseGradeOf(g: Grade): Grade {
|
|
179
|
+
if (g === 'N/A' || g === 'D') return g;
|
|
180
|
+
if (g.startsWith('A')) return 'A';
|
|
181
|
+
if (g.startsWith('B')) return 'B';
|
|
182
|
+
if (g.startsWith('C')) return 'C';
|
|
183
|
+
return 'F';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Linearly interpolate a score within a base band.
|
|
188
|
+
*
|
|
189
|
+
* `position` is in [0, 1]: 0 = "as bad as this grade gets" (band bottom),
|
|
190
|
+
* 1 = "as good as this grade gets" (band top, just below the next grade).
|
|
191
|
+
*/
|
|
192
|
+
function scoreInBand(grade: BaseGrade, position: number): number {
|
|
193
|
+
const clamped = Math.max(0, Math.min(1, position));
|
|
194
|
+
const bottom = BASE_BAND_BOTTOM[grade];
|
|
195
|
+
const top = BASE_BAND_TOP[grade];
|
|
196
|
+
return Math.round(bottom + (top - bottom) * clamped);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ============================================================================
|
|
200
|
+
// Severity Helpers
|
|
201
|
+
// ============================================================================
|
|
202
|
+
|
|
203
|
+
function isSeverity(s: string): s is Severity {
|
|
204
|
+
return s === 'critical' || s === 'high' || s === 'medium' || s === 'low';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
interface SeverityCounts {
|
|
208
|
+
critical: number;
|
|
209
|
+
high: number;
|
|
210
|
+
medium: number;
|
|
211
|
+
low: number;
|
|
212
|
+
total: number;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function countSeverities(findings: Array<{ severity: string }>): SeverityCounts {
|
|
216
|
+
const counts: SeverityCounts = { critical: 0, high: 0, medium: 0, low: 0, total: 0 };
|
|
217
|
+
for (const f of findings) {
|
|
218
|
+
if (!isSeverity(f.severity)) continue;
|
|
219
|
+
counts[f.severity]++;
|
|
220
|
+
counts.total++;
|
|
221
|
+
}
|
|
222
|
+
return counts;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function worstSeverity(counts: SeverityCounts): Severity | null {
|
|
226
|
+
if (counts.critical > 0) return 'critical';
|
|
227
|
+
if (counts.high > 0) return 'high';
|
|
228
|
+
if (counts.medium > 0) return 'medium';
|
|
229
|
+
if (counts.low > 0) return 'low';
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Result of consulting a dimension's severity escape hatch — used by both
|
|
235
|
+
* Reliability and Maintainability to short-circuit a forgiving density grade
|
|
236
|
+
* when the underlying findings include a critical or high. The grade is the
|
|
237
|
+
* worst the dimension may receive after applying the escape; the consumer
|
|
238
|
+
* still picks `min(severityEscape.grade, band.grade)` so an even worse
|
|
239
|
+
* density-derived grade isn't paved over.
|
|
240
|
+
*/
|
|
241
|
+
interface SeverityEscape {
|
|
242
|
+
grade: BaseGrade;
|
|
243
|
+
note: string;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ============================================================================
|
|
247
|
+
// Security Dimension
|
|
248
|
+
// ============================================================================
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Security grading — strictest of the three dimensions. Any medium-or-worse
|
|
252
|
+
* security finding immediately drops the grade below B because security
|
|
253
|
+
* issues can't be amortized over codebase size.
|
|
254
|
+
*
|
|
255
|
+
* A critical security issue caps at F- (the worst grade). One low-severity
|
|
256
|
+
* finding still earns a B- because every team has a few.
|
|
257
|
+
*/
|
|
258
|
+
function gradeSecurity(findings: Array<{ severity: string }>): DimensionScore {
|
|
259
|
+
const counts = countSeverities(findings);
|
|
260
|
+
const worst = worstSeverity(counts);
|
|
261
|
+
|
|
262
|
+
if (counts.total === 0) {
|
|
263
|
+
return makeDimension('security', 100, '0 security findings', 0, null);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (counts.critical > 0) {
|
|
267
|
+
// Critical security issue → F-, not just F. There's no recovering by
|
|
268
|
+
// averaging this away across a clean codebase.
|
|
269
|
+
return makeDimension(
|
|
270
|
+
'security',
|
|
271
|
+
Math.max(0, 55 - counts.critical * 5),
|
|
272
|
+
`${counts.critical} critical-severity security ${pluralize('issue', counts.critical)}`,
|
|
273
|
+
counts.total,
|
|
274
|
+
worst,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
let baseGrade: BaseGrade;
|
|
279
|
+
let position: number;
|
|
280
|
+
let rationale: string;
|
|
281
|
+
|
|
282
|
+
if (counts.high > 0) {
|
|
283
|
+
baseGrade = 'F';
|
|
284
|
+
position = 1 / (1 + counts.high);
|
|
285
|
+
rationale = `${counts.high} high-severity security ${pluralize('issue', counts.high)}`;
|
|
286
|
+
} else if (counts.medium > 0) {
|
|
287
|
+
baseGrade = 'C';
|
|
288
|
+
position = 1 / (1 + counts.medium);
|
|
289
|
+
rationale = `${counts.medium} medium-severity security ${pluralize('issue', counts.medium)}`;
|
|
290
|
+
} else {
|
|
291
|
+
// Only low-severity findings.
|
|
292
|
+
baseGrade = 'B';
|
|
293
|
+
position = 1 / Math.max(1, counts.low);
|
|
294
|
+
rationale = `${counts.low} low-severity security ${pluralize('issue', counts.low)}`;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const score = scoreInBand(baseGrade, position);
|
|
298
|
+
return makeDimension('security', score, rationale, counts.total, worst);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// ============================================================================
|
|
302
|
+
// Reliability Dimension
|
|
303
|
+
// ============================================================================
|
|
304
|
+
//
|
|
305
|
+
// Reliability uses the same density-based model as Maintainability, with a
|
|
306
|
+
// severity escape hatch — softer than the previous "2+ high → F" rule, which
|
|
307
|
+
// over-penalised codebases that had a handful of edge-case bugs flagged on
|
|
308
|
+
// rarely-executed paths. The new model:
|
|
309
|
+
//
|
|
310
|
+
// - Density ladder (issues per KLOC) at ≥5 KLOC
|
|
311
|
+
// - Absolute-count ladder at <5 KLOC (small projects shouldn't be density-
|
|
312
|
+
// rated; one extra finding moves the needle by 1.0+/KLOC)
|
|
313
|
+
// - Severity escape: 1 critical caps at F, any high caps at C
|
|
314
|
+
//
|
|
315
|
+
// Rationale: a real-world 10 KLOC service with 4 plausibly-improbable HIGH
|
|
316
|
+
// bugs (race conditions on degraded paths, edge-case fly-replay leaks)
|
|
317
|
+
// previously landed at F, dragging the entire app to F. Under this model
|
|
318
|
+
// it lands at C — "needs work" — which matches how a senior engineer would
|
|
319
|
+
// triage it on a code review. Critical bugs and compile errors still hit
|
|
320
|
+
// the F-tier through the escape hatch.
|
|
321
|
+
//
|
|
322
|
+
// Build/compile errors enter via the `build` category with severity `critical`,
|
|
323
|
+
// so they trip the escape hatch and hit F regardless of density.
|
|
324
|
+
|
|
325
|
+
interface ReliabilityBand {
|
|
326
|
+
grade: BaseGrade;
|
|
327
|
+
position: number;
|
|
328
|
+
label: string;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function reliabilityByCount(n: number): ReliabilityBand {
|
|
332
|
+
// Stricter than Maintainability's count ladder: a couple of real bugs hurt
|
|
333
|
+
// more than a couple of lint warnings, but a single isolated medium bug on
|
|
334
|
+
// a small project shouldn't pin the codebase at C.
|
|
335
|
+
const label = `${n} reliability ${pluralize('issue', n)}`;
|
|
336
|
+
if (n <= 2) return { grade: 'A', position: 1 - n / 2, label };
|
|
337
|
+
if (n <= 6) return { grade: 'B', position: 1 - (n - 2) / 4, label };
|
|
338
|
+
if (n <= 15) return { grade: 'C', position: 1 - (n - 6) / 9, label };
|
|
339
|
+
return { grade: 'F', position: 1 / (1 + (n - 15) / 15), label };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function reliabilityByDensity(n: number, kloc: number): ReliabilityBand {
|
|
343
|
+
// Density thresholds are tighter than Maintainability (5/10/25). A 50 KLOC
|
|
344
|
+
// codebase with 100 reliability bugs (density 2) is "minor cleanup", not
|
|
345
|
+
// pristine — but 1.4/KLOC is still A-band because real-world projects
|
|
346
|
+
// never get to zero. The escape hatch handles severity outliers above this.
|
|
347
|
+
const density = n / kloc;
|
|
348
|
+
const label = `${roundOne(density)} reliability ${pluralize('issue', n)} / KLOC`;
|
|
349
|
+
if (density < 1.5) return { grade: 'A', position: 1 - density / 1.5, label };
|
|
350
|
+
if (density < 4) return { grade: 'B', position: 1 - (density - 1.5) / 2.5, label };
|
|
351
|
+
if (density < 8) return { grade: 'C', position: 1 - (density - 4) / 4, label };
|
|
352
|
+
return { grade: 'F', position: 1 / (1 + (density - 8) / 8), label };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function reliabilityEscape(counts: SeverityCounts): SeverityEscape | null {
|
|
356
|
+
if (counts.critical > 0) {
|
|
357
|
+
return { grade: 'F', note: `${counts.critical} critical-severity ${pluralize('bug', counts.critical)}` };
|
|
358
|
+
}
|
|
359
|
+
if (counts.high > 0) {
|
|
360
|
+
return { grade: 'C', note: `${counts.high} high-severity ${pluralize('bug', counts.high)}` };
|
|
361
|
+
}
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Reliability grading — density-based with a severity escape hatch.
|
|
367
|
+
*
|
|
368
|
+
* - Empty / ≤1 low: A-band (clean by convention).
|
|
369
|
+
* - Density-based grade (≥5 KLOC) or count-based grade (<5 KLOC) drives
|
|
370
|
+
* the baseline. Both ladders mirror Maintainability's so reliability and
|
|
371
|
+
* maintainability remain comparable at a glance.
|
|
372
|
+
* - Severity escape: critical → F, high → C. This matches Maintainability and
|
|
373
|
+
* prevents a handful of medium-density bugs from being silently rated A
|
|
374
|
+
* when at least one is severe.
|
|
375
|
+
*
|
|
376
|
+
* Build/compile errors flow in via `build` category with severity `critical`
|
|
377
|
+
* and therefore land at F via the escape hatch — no special-case branching.
|
|
378
|
+
*/
|
|
379
|
+
function gradeReliability(findings: Array<{ severity: string }>, totalLines: number): DimensionScore {
|
|
380
|
+
const counts = countSeverities(findings);
|
|
381
|
+
const worst = worstSeverity(counts);
|
|
382
|
+
const kloc = Math.max(totalLines / 1000, 1.0);
|
|
383
|
+
|
|
384
|
+
if (counts.total === 0) {
|
|
385
|
+
return makeDimension('reliability', 100, '0 reliability findings', 0, null);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ≤1 low and nothing else is treated as clean — every team has one.
|
|
389
|
+
if (counts.low <= 1 && counts.medium === 0 && counts.high === 0 && counts.critical === 0) {
|
|
390
|
+
return makeDimension('reliability', scoreInBand('A', 0.5), '1 low-severity reliability issue', counts.total, worst);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const band = kloc < 5 ? reliabilityByCount(counts.total) : reliabilityByDensity(counts.total, kloc);
|
|
394
|
+
const severityCap = reliabilityEscape(counts);
|
|
395
|
+
const useCap = severityCap && baseIsWorse(severityCap.grade, band.grade);
|
|
396
|
+
const finalGrade = useCap ? severityCap.grade : band.grade;
|
|
397
|
+
const finalPosition = useCap ? 0.5 : band.position;
|
|
398
|
+
const rationale = useCap ? `${band.label}, ${severityCap.note}` : band.label;
|
|
399
|
+
|
|
400
|
+
return makeDimension('reliability', scoreInBand(finalGrade, finalPosition), rationale, counts.total, worst);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ============================================================================
|
|
404
|
+
// Maintainability Dimension
|
|
405
|
+
// ============================================================================
|
|
406
|
+
|
|
407
|
+
interface MaintainabilityBand {
|
|
408
|
+
grade: BaseGrade;
|
|
409
|
+
position: number;
|
|
410
|
+
label: string;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function maintainabilityByCount(n: number): MaintainabilityBand {
|
|
414
|
+
const label = `${n} maintainability ${pluralize('issue', n)}`;
|
|
415
|
+
if (n <= 5) return { grade: 'A', position: 1 - n / 5, label };
|
|
416
|
+
if (n <= 15) return { grade: 'B', position: 1 - (n - 5) / 10, label };
|
|
417
|
+
if (n <= 30) return { grade: 'C', position: 1 - (n - 15) / 15, label };
|
|
418
|
+
return { grade: 'F', position: 1 / (1 + (n - 30) / 30), label };
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function maintainabilityByDensity(n: number, kloc: number): MaintainabilityBand {
|
|
422
|
+
const density = n / kloc;
|
|
423
|
+
const label = `${roundOne(density)} ${pluralize('issue', n)} / KLOC`;
|
|
424
|
+
if (density < 5) return { grade: 'A', position: 1 - density / 5, label };
|
|
425
|
+
if (density < 10) return { grade: 'B', position: 1 - (density - 5) / 5, label };
|
|
426
|
+
if (density < 25) return { grade: 'C', position: 1 - (density - 10) / 15, label };
|
|
427
|
+
return { grade: 'F', position: 1 / (1 + (density - 25) / 25), label };
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function maintainabilityEscape(counts: SeverityCounts): SeverityEscape | null {
|
|
431
|
+
if (counts.critical > 0) {
|
|
432
|
+
return { grade: 'F', note: `${counts.critical} critical-severity ${pluralize('issue', counts.critical)}` };
|
|
433
|
+
}
|
|
434
|
+
if (counts.high > 0) {
|
|
435
|
+
return { grade: 'C', note: `${counts.high} high-severity ${pluralize('issue', counts.high)}` };
|
|
436
|
+
}
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Maintainability uses a density-based grade (issues per KLOC) once the
|
|
442
|
+
* codebase is at least 5 KLOC. For smaller codebases, density is too noisy
|
|
443
|
+
* (one extra lint issue moves density by 1.0+), so we fall back to absolute
|
|
444
|
+
* counts — preventing tiny projects from being unfairly penalized.
|
|
445
|
+
*
|
|
446
|
+
* Severity escape hatch: a critical maintainability finding (e.g., a 3000-
|
|
447
|
+
* line file with high cohesion-violation severity) caps at F; a high-severity
|
|
448
|
+
* one caps at C. "Worst wins" — we take min of density-grade and severity-cap.
|
|
449
|
+
*/
|
|
450
|
+
function gradeMaintainability(findings: Array<{ severity: string }>, totalLines: number): DimensionScore {
|
|
451
|
+
const counts = countSeverities(findings);
|
|
452
|
+
const kloc = Math.max(totalLines / 1000, 1.0);
|
|
453
|
+
|
|
454
|
+
if (counts.total === 0) {
|
|
455
|
+
return makeDimension('maintainability', 100, '0 maintainability findings', 0, null);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const band = kloc < 5 ? maintainabilityByCount(counts.total) : maintainabilityByDensity(counts.total, kloc);
|
|
459
|
+
const severityCap = maintainabilityEscape(counts);
|
|
460
|
+
const useCap = severityCap && baseIsWorse(severityCap.grade, band.grade);
|
|
461
|
+
const finalGrade = useCap ? severityCap.grade : band.grade;
|
|
462
|
+
const finalPosition = useCap ? 0.5 : band.position;
|
|
463
|
+
const rationale = useCap ? `${band.label}, ${severityCap.note}` : band.label;
|
|
464
|
+
|
|
465
|
+
return makeDimension('maintainability', scoreInBand(finalGrade, finalPosition), rationale, counts.total, worstSeverity(counts));
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ============================================================================
|
|
469
|
+
// Architectural Penalty
|
|
470
|
+
// ============================================================================
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Drop a dimension's grade by N letters because of architectural findings.
|
|
474
|
+
*
|
|
475
|
+
* Rationale: a high-severity architectural problem (god class, leaky
|
|
476
|
+
* abstraction, broken layering) is qualitatively different from a long-file
|
|
477
|
+
* lint warning — it pollutes every change that touches the affected code.
|
|
478
|
+
* The user spec calls for explicit letter-grade drops:
|
|
479
|
+
*
|
|
480
|
+
* - 1 high-severity arch issue → drop 1 letter
|
|
481
|
+
* - 2+ high-severity arch issues → drop 2 letters
|
|
482
|
+
* - any critical-severity arch issue → drop 2 letters
|
|
483
|
+
*
|
|
484
|
+
* Letters drop A → B → C → F → F-. We never go lower than F-. The drop is
|
|
485
|
+
* applied AFTER the dimension's normal grading so the displayed score still
|
|
486
|
+
* reflects the underlying finding count, but the letter grade carries the
|
|
487
|
+
* architectural weight that a density-based score would otherwise miss.
|
|
488
|
+
*/
|
|
489
|
+
function archDropCount(archFindings: Array<{ severity: string }>): number {
|
|
490
|
+
let highCount = 0;
|
|
491
|
+
let criticalCount = 0;
|
|
492
|
+
for (const f of archFindings) {
|
|
493
|
+
if (f.severity === 'critical') criticalCount++;
|
|
494
|
+
else if (f.severity === 'high') highCount++;
|
|
495
|
+
}
|
|
496
|
+
if (criticalCount >= 1) return 2;
|
|
497
|
+
if (highCount >= 2) return 2;
|
|
498
|
+
if (highCount >= 1) return 1;
|
|
499
|
+
return 0;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const BASE_LETTERS: BaseGrade[] = ['A', 'B', 'C', 'F'];
|
|
503
|
+
|
|
504
|
+
function gradeModifier(grade: Grade): '' | '+' | '-' {
|
|
505
|
+
if (grade.endsWith('+')) return '+';
|
|
506
|
+
if (grade.endsWith('-')) return '-';
|
|
507
|
+
return '';
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function applyModifierToTargetBase(targetBase: BaseGrade, modifier: '' | '+' | '-'): Grade {
|
|
511
|
+
// F's modifier semantics differ from A/B/C: F+ is "barely failing" while
|
|
512
|
+
// A+/B+/C+ are "top of band." For simplicity we map any modifier on F to
|
|
513
|
+
// its matching variant, and use F- (the worst) for any post-F overshoot.
|
|
514
|
+
if (targetBase === 'F') {
|
|
515
|
+
if (modifier === '+') return 'F+';
|
|
516
|
+
if (modifier === '-') return 'F-';
|
|
517
|
+
return 'F';
|
|
518
|
+
}
|
|
519
|
+
if (modifier === '+') return `${targetBase}+` as Grade;
|
|
520
|
+
if (modifier === '-') return `${targetBase}-` as Grade;
|
|
521
|
+
return targetBase as Grade;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Drop a grade by N "letters." A "letter" here means a full base-grade step
|
|
526
|
+
* (A → B → C → F → F-), preserving the modifier when possible. So A+ dropped
|
|
527
|
+
* by 1 becomes B+, not A. Stops at F-.
|
|
528
|
+
*/
|
|
529
|
+
function dropGradeByLetters(grade: Grade, letters: number): Grade {
|
|
530
|
+
if (letters <= 0 || grade === 'N/A' || grade === 'D') return grade;
|
|
531
|
+
const baseLetter = baseGradeOf(grade);
|
|
532
|
+
const baseIdx = BASE_LETTERS.indexOf(baseLetter as BaseGrade);
|
|
533
|
+
if (baseIdx === -1) return grade;
|
|
534
|
+
const targetBaseIdx = baseIdx + letters;
|
|
535
|
+
// Past the F base — bottom out at F- (the absolute worst grade).
|
|
536
|
+
if (targetBaseIdx > 3) return 'F-';
|
|
537
|
+
const targetBase = BASE_LETTERS[targetBaseIdx];
|
|
538
|
+
return applyModifierToTargetBase(targetBase, gradeModifier(grade));
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function applyArchPenalty(dim: DimensionScore, archFindings: Array<{ severity: string }>): DimensionScore {
|
|
542
|
+
const drop = archDropCount(archFindings);
|
|
543
|
+
if (drop === 0) return dim;
|
|
544
|
+
const dropped = dropGradeByLetters(dim.grade, drop);
|
|
545
|
+
if (dropped === dim.grade) return dim;
|
|
546
|
+
const archCount = archFindings.length;
|
|
547
|
+
const noun = pluralize('architectural finding', archCount);
|
|
548
|
+
const note = `dropped ${drop} ${pluralize('letter', drop)} by ${archCount} ${noun}`;
|
|
549
|
+
return {
|
|
550
|
+
...dim,
|
|
551
|
+
grade: dropped,
|
|
552
|
+
// Re-anchor score to the new band's midpoint so score and letter agree.
|
|
553
|
+
score: anchorScoreToGrade(dropped, dim.score),
|
|
554
|
+
rationale: dim.rationale === '0 maintainability findings' || dim.findingCount === 0
|
|
555
|
+
? note
|
|
556
|
+
: `${dim.rationale}; ${note}`,
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Re-snap a score to fall within the band of the given grade. Used after
|
|
562
|
+
* applying the architectural penalty so the displayed score never disagrees
|
|
563
|
+
* with the displayed letter (e.g., grade C with score 89 would be jarring).
|
|
564
|
+
*
|
|
565
|
+
* If the original score is already in-band, keep it; otherwise pick the
|
|
566
|
+
* band's midpoint as a sensible default.
|
|
567
|
+
*/
|
|
568
|
+
function anchorScoreToGrade(grade: Grade, originalScore: number): number {
|
|
569
|
+
if (grade === 'N/A' || grade === 'D') return originalScore;
|
|
570
|
+
const ranges: Record<Exclude<Grade, 'N/A' | 'D'>, [number, number]> = {
|
|
571
|
+
'A+': [97, 100], A: [93, 96], 'A-': [90, 92],
|
|
572
|
+
'B+': [87, 89], B: [83, 86], 'B-': [80, 82],
|
|
573
|
+
'C+': [77, 79], C: [73, 76], 'C-': [70, 72],
|
|
574
|
+
'F+': [65, 69], F: [56, 64], 'F-': [0, 55],
|
|
575
|
+
};
|
|
576
|
+
const [lo, hi] = ranges[grade as Exclude<Grade, 'N/A' | 'D'>];
|
|
577
|
+
if (originalScore >= lo && originalScore <= hi) return originalScore;
|
|
578
|
+
return Math.round((lo + hi) / 2);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// ============================================================================
|
|
582
|
+
// Grade Comparison Helpers
|
|
583
|
+
// ============================================================================
|
|
584
|
+
|
|
585
|
+
const BASE_RANK: Record<BaseGrade, number> = { F: 1, C: 2, B: 3, A: 4 };
|
|
586
|
+
|
|
587
|
+
function baseIsWorse(a: BaseGrade, b: BaseGrade): boolean {
|
|
588
|
+
return BASE_RANK[a] < BASE_RANK[b];
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const FULL_RANK: Record<Exclude<Grade, 'N/A' | 'D'>, number> = {
|
|
592
|
+
'F-': 0, F: 1, 'F+': 2,
|
|
593
|
+
'C-': 3, C: 4, 'C+': 5,
|
|
594
|
+
'B-': 6, B: 7, 'B+': 8,
|
|
595
|
+
'A-': 9, A: 10, 'A+': 11,
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
function gradeRank(g: Grade): number {
|
|
599
|
+
if (g === 'N/A') return -1;
|
|
600
|
+
if (g === 'D') return 1.5; // legacy: between F+ and C-
|
|
601
|
+
return FULL_RANK[g as Exclude<Grade, 'N/A' | 'D'>];
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function worstOf(grades: Grade[]): Grade {
|
|
605
|
+
let worst: Grade = 'A+';
|
|
606
|
+
for (const g of grades) {
|
|
607
|
+
if (g === 'N/A') continue;
|
|
608
|
+
if (gradeRank(g) < gradeRank(worst)) worst = g;
|
|
609
|
+
}
|
|
610
|
+
return worst;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// ============================================================================
|
|
614
|
+
// Misc Helpers
|
|
615
|
+
// ============================================================================
|
|
616
|
+
|
|
617
|
+
function pluralize(word: string, n: number): string {
|
|
618
|
+
return n === 1 ? word : `${word}s`;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function roundOne(n: number): number {
|
|
622
|
+
return Math.round(n * 10) / 10;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function dimensionDisplayName(name: DimensionName): string {
|
|
626
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function makeDimension(
|
|
630
|
+
name: DimensionName,
|
|
631
|
+
score: number,
|
|
632
|
+
rationale: string,
|
|
633
|
+
findingCount: number,
|
|
634
|
+
worst: Severity | null,
|
|
635
|
+
): DimensionScore {
|
|
636
|
+
return {
|
|
637
|
+
name,
|
|
638
|
+
score,
|
|
639
|
+
grade: scoreToGrade(score),
|
|
640
|
+
rationale,
|
|
641
|
+
available: true,
|
|
642
|
+
findingCount,
|
|
643
|
+
worstSeverity: worst,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function naDimension(name: DimensionName): DimensionScore {
|
|
648
|
+
return {
|
|
649
|
+
name,
|
|
650
|
+
score: 0,
|
|
651
|
+
grade: 'N/A',
|
|
652
|
+
rationale: 'No tools available to evaluate',
|
|
653
|
+
available: false,
|
|
654
|
+
findingCount: 0,
|
|
655
|
+
worstSeverity: null,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// ============================================================================
|
|
660
|
+
// Top-Level Entry Point
|
|
661
|
+
// ============================================================================
|
|
662
|
+
|
|
663
|
+
function bucketByDimension(
|
|
664
|
+
findings: Array<{ severity: string; category: string }>,
|
|
665
|
+
): {
|
|
666
|
+
security: Array<{ severity: string; category: string }>;
|
|
667
|
+
reliability: Array<{ severity: string; category: string }>;
|
|
668
|
+
maintainability: Array<{ severity: string; category: string }>;
|
|
669
|
+
architecture: Array<{ severity: string; category: string }>;
|
|
670
|
+
} {
|
|
671
|
+
const security: Array<{ severity: string; category: string }> = [];
|
|
672
|
+
const reliability: Array<{ severity: string; category: string }> = [];
|
|
673
|
+
const maintainability: Array<{ severity: string; category: string }> = [];
|
|
674
|
+
const architecture: Array<{ severity: string; category: string }> = [];
|
|
675
|
+
for (const f of findings) {
|
|
676
|
+
if (ARCHITECTURE_CATEGORIES.has(f.category)) architecture.push(f);
|
|
677
|
+
const dim = categoryToDimension(f.category);
|
|
678
|
+
if (dim === 'security') security.push(f);
|
|
679
|
+
else if (dim === 'reliability') reliability.push(f);
|
|
680
|
+
else maintainability.push(f);
|
|
681
|
+
}
|
|
682
|
+
return { security, reliability, maintainability, architecture };
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function isDimensionAvailable(
|
|
686
|
+
dim: DimensionName,
|
|
687
|
+
hasFindings: boolean,
|
|
688
|
+
options?: { availableDimensions?: Set<DimensionName>; forceNA?: Set<DimensionName> },
|
|
689
|
+
): boolean {
|
|
690
|
+
if (options?.forceNA?.has(dim)) return false;
|
|
691
|
+
const explicit = options?.availableDimensions;
|
|
692
|
+
if (explicit) return explicit.has(dim);
|
|
693
|
+
// Auto-detect: maintainability always on, security/reliability iff findings exist.
|
|
694
|
+
return dim === 'maintainability' ? true : hasFindings;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Combine the available dimensions into a single overall grade + score.
|
|
699
|
+
*
|
|
700
|
+
* "Worst dimension wins" for the letter grade — a single failing dimension
|
|
701
|
+
* caps the overall score, matching how SonarQube's quality gate behaves.
|
|
702
|
+
* The numeric score is `min(avg, worst)` so a great Maintainability score
|
|
703
|
+
* can't paper over a Security failure.
|
|
704
|
+
*/
|
|
705
|
+
function computeOverall(availableDims: DimensionScore[]): { grade: Grade; score: number } {
|
|
706
|
+
if (availableDims.length === 0) {
|
|
707
|
+
return { grade: 'N/A', score: 0 };
|
|
708
|
+
}
|
|
709
|
+
const grades = availableDims.map((d) => d.grade);
|
|
710
|
+
const scores = availableDims.map((d) => d.score);
|
|
711
|
+
const avg = scores.reduce((s, n) => s + n, 0) / scores.length;
|
|
712
|
+
const worst = worstOf(grades);
|
|
713
|
+
// Re-snap the displayed score so it lives in the worst dimension's band —
|
|
714
|
+
// otherwise we'd display a B-letter with a C-numeric score (or vice versa).
|
|
715
|
+
const blendedScore = Math.round(Math.min(avg, Math.min(...scores)));
|
|
716
|
+
return { grade: worst, score: anchorScoreToGrade(worst, blendedScore) };
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
export function computeQualityRating(
|
|
720
|
+
allFindings: Array<{ severity: string; category: string }>,
|
|
721
|
+
totalLines: number,
|
|
722
|
+
options?: { availableDimensions?: Set<DimensionName>; forceNA?: Set<DimensionName> },
|
|
723
|
+
): QualityRating {
|
|
724
|
+
const buckets = bucketByDimension(allFindings);
|
|
725
|
+
|
|
726
|
+
// Initial dimension grades, before architectural penalty.
|
|
727
|
+
const security = isDimensionAvailable('security', buckets.security.length > 0, options)
|
|
728
|
+
? gradeSecurity(buckets.security)
|
|
729
|
+
: naDimension('security');
|
|
730
|
+
const reliabilityRaw = isDimensionAvailable('reliability', buckets.reliability.length > 0, options)
|
|
731
|
+
? gradeReliability(buckets.reliability, totalLines)
|
|
732
|
+
: naDimension('reliability');
|
|
733
|
+
const maintainabilityRaw = isDimensionAvailable('maintainability', true, options)
|
|
734
|
+
? gradeMaintainability(buckets.maintainability, totalLines)
|
|
735
|
+
: naDimension('maintainability');
|
|
736
|
+
|
|
737
|
+
// Architectural penalty: hits whichever dimension(s) have arch findings
|
|
738
|
+
// bucketed into them (currently maintainability via the category map).
|
|
739
|
+
const archFindings = buckets.architecture;
|
|
740
|
+
const maintainability = maintainabilityRaw.available
|
|
741
|
+
? applyArchPenalty(maintainabilityRaw, archFindings)
|
|
742
|
+
: maintainabilityRaw;
|
|
743
|
+
|
|
744
|
+
const dimensions: DimensionScore[] = [security, reliabilityRaw, maintainability];
|
|
745
|
+
const availableDims = dimensions.filter((d) => d.available);
|
|
746
|
+
const overall = computeOverall(availableDims);
|
|
747
|
+
const qualityGate = computeQualityGate(security, reliabilityRaw, archFindings.length);
|
|
748
|
+
const gradeRationale = computeGradeRationale(availableDims, overall.grade, allFindings.length);
|
|
749
|
+
|
|
750
|
+
return {
|
|
751
|
+
overall,
|
|
752
|
+
dimensions,
|
|
753
|
+
qualityGate,
|
|
754
|
+
gradeRationale,
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// ============================================================================
|
|
759
|
+
// Quality Gate
|
|
760
|
+
// ============================================================================
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* The Quality Gate is a coarse PASS/FAIL signal layered on top of the grades.
|
|
764
|
+
* It only fires for the most user-actionable thresholds — any C-or-worse
|
|
765
|
+
* security grade, any F-tier reliability grade, or 2+ high-severity
|
|
766
|
+
* architectural findings. N/A dimensions never trigger a fail (we don't fail
|
|
767
|
+
* on missing data).
|
|
768
|
+
*/
|
|
769
|
+
function isFTier(g: Grade): boolean {
|
|
770
|
+
return g === 'F+' || g === 'F' || g === 'F-' || g === 'D';
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function isCorWorse(g: Grade): boolean {
|
|
774
|
+
return baseGradeOf(g) === 'C' || isFTier(g);
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function computeQualityGate(
|
|
778
|
+
security: DimensionScore,
|
|
779
|
+
reliability: DimensionScore,
|
|
780
|
+
archFindingCount: number,
|
|
781
|
+
): QualityGate {
|
|
782
|
+
const failingConditions: string[] = [];
|
|
783
|
+
|
|
784
|
+
if (security.available && isCorWorse(security.grade)) {
|
|
785
|
+
failingConditions.push(`Security grade ${security.grade} — ${security.rationale}`);
|
|
786
|
+
}
|
|
787
|
+
if (reliability.available && isFTier(reliability.grade)) {
|
|
788
|
+
failingConditions.push(`Reliability grade ${reliability.grade} — ${reliability.rationale}`);
|
|
789
|
+
}
|
|
790
|
+
if (archFindingCount >= 2) {
|
|
791
|
+
failingConditions.push(`${archFindingCount} architectural findings`);
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
return {
|
|
795
|
+
passed: failingConditions.length === 0,
|
|
796
|
+
failingConditions,
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// ============================================================================
|
|
801
|
+
// Grade Rationale
|
|
802
|
+
// ============================================================================
|
|
803
|
+
|
|
804
|
+
function computeGradeRationale(
|
|
805
|
+
availableDims: DimensionScore[],
|
|
806
|
+
overallGrade: Grade,
|
|
807
|
+
totalFindingCount: number,
|
|
808
|
+
): string {
|
|
809
|
+
if (totalFindingCount === 0) {
|
|
810
|
+
return 'Clean — no findings detected';
|
|
811
|
+
}
|
|
812
|
+
if (availableDims.length === 0 || overallGrade === 'N/A') {
|
|
813
|
+
return 'No dimensions available to grade';
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// All available dimensions share the same base letter -> "consistent
|
|
817
|
+
// quality". With +/- modifiers it's normal for sibling dimensions to land
|
|
818
|
+
// at A vs A+ depending on within-band position; calling that "inconsistent"
|
|
819
|
+
// would be misleading. We compare base letters so the user-facing message
|
|
820
|
+
// captures the high-level shape rather than every minor band difference.
|
|
821
|
+
const firstBase = baseGradeOf(availableDims[0].grade);
|
|
822
|
+
const allSameBase = availableDims.every((d) => baseGradeOf(d.grade) === firstBase);
|
|
823
|
+
if (allSameBase) {
|
|
824
|
+
return `All dimensions ${firstBase}-tier — consistent quality`;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
// Find the dimension that pinned the overall grade (worst available).
|
|
828
|
+
const worstDim =
|
|
829
|
+
availableDims.find((d) => d.grade === overallGrade) ??
|
|
830
|
+
// Fallback shouldn't fire since overallGrade was derived from availableDims.
|
|
831
|
+
availableDims[0];
|
|
832
|
+
|
|
833
|
+
return `Capped at ${overallGrade} by ${dimensionDisplayName(worstDim.name)} (${worstDim.rationale})`;
|
|
834
|
+
}
|