mstro-app 0.5.1 → 0.5.6
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/PRIVACY.md +9 -9
- package/README.md +71 -28
- package/bin/commands/config.js +1 -1
- package/bin/mstro.js +55 -4
- 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 +9 -1
- package/dist/server/cli/headless/claude-invoker-process.js.map +1 -1
- package/dist/server/cli/headless/mcp-config.d.ts +22 -5
- package/dist/server/cli/headless/mcp-config.d.ts.map +1 -1
- package/dist/server/cli/headless/mcp-config.js +7 -5
- package/dist/server/cli/headless/mcp-config.js.map +1 -1
- package/dist/server/cli/headless/runner.d.ts.map +1 -1
- package/dist/server/cli/headless/runner.js +19 -0
- 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 -9
- 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 -12
- package/dist/server/cli/headless/tool-watchdog.js.map +1 -1
- package/dist/server/cli/headless/types.d.ts +16 -1
- package/dist/server/cli/headless/types.d.ts.map +1 -1
- package/dist/server/cli/improvisation-history-store.d.ts.map +1 -1
- package/dist/server/cli/improvisation-history-store.js +5 -1
- package/dist/server/cli/improvisation-history-store.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 -7
- package/dist/server/cli/improvisation-output-queue.js.map +1 -1
- package/dist/server/cli/improvisation-session-manager.d.ts +35 -0
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +58 -1
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/cli/improvisation-types.d.ts +9 -0
- package/dist/server/cli/improvisation-types.d.ts.map +1 -1
- package/dist/server/cli/improvisation-types.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 +1 -0
- package/dist/server/cli/retry/retry-runner-factory.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 +9 -2
- package/dist/server/index.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 -124
- 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 -5
- 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/server.js +52 -0
- package/dist/server/mcp/server.js.map +1 -1
- package/dist/server/routes/index.d.ts +1 -0
- package/dist/server/routes/index.d.ts.map +1 -1
- package/dist/server/routes/index.js +1 -0
- package/dist/server/routes/index.js.map +1 -1
- package/dist/server/routes/internal.d.ts +16 -0
- package/dist/server/routes/internal.d.ts.map +1 -0
- package/dist/server/routes/internal.js +94 -0
- package/dist/server/routes/internal.js.map +1 -0
- 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/composer.d.ts.map +1 -1
- package/dist/server/services/plan/composer.js +59 -11
- package/dist/server/services/plan/composer.js.map +1 -1
- package/dist/server/services/plan/executor.d.ts.map +1 -1
- package/dist/server/services/plan/executor.js +3 -1
- package/dist/server/services/plan/executor.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 -1
- package/dist/server/services/plan/issue-prompt-builder.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 -0
- package/dist/server/services/plan/parser-core.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/runtime-info.d.ts +3 -0
- package/dist/server/services/runtime-info.d.ts.map +1 -0
- package/dist/server/services/runtime-info.js +21 -0
- package/dist/server/services/runtime-info.js.map +1 -0
- 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 -4
- package/dist/server/services/settings.js.map +1 -1
- package/dist/server/services/websocket/ask-user-question-bridge.d.ts +32 -0
- package/dist/server/services/websocket/ask-user-question-bridge.d.ts.map +1 -0
- package/dist/server/services/websocket/ask-user-question-bridge.js +115 -0
- package/dist/server/services/websocket/ask-user-question-bridge.js.map +1 -0
- package/dist/server/services/websocket/git-branch-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/git-branch-handlers.js +19 -6
- package/dist/server/services/websocket/git-branch-handlers.js.map +1 -1
- package/dist/server/services/websocket/handler.d.ts +25 -1
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +84 -2
- package/dist/server/services/websocket/handler.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 -26
- 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 +27 -4
- package/dist/server/services/websocket/quality-grading.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-grading.js +369 -201
- package/dist/server/services/websocket/quality-grading.js.map +1 -1
- package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-handlers.js +145 -7
- package/dist/server/services/websocket/quality-handlers.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 +9 -0
- package/dist/server/services/websocket/quality-persistence.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-persistence.js +10 -0
- 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 -56
- package/dist/server/services/websocket/quality-review-agent.js.map +1 -1
- package/dist/server/services/websocket/quality-service.d.ts +9 -1
- package/dist/server/services/websocket/quality-service.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-service.js +334 -14
- package/dist/server/services/websocket/quality-service.js.map +1 -1
- package/dist/server/services/websocket/quality-tools.d.ts +21 -0
- package/dist/server/services/websocket/quality-tools.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-tools.js +49 -0
- package/dist/server/services/websocket/quality-tools.js.map +1 -1
- package/dist/server/services/websocket/quality-types.d.ts +35 -2
- package/dist/server/services/websocket/quality-types.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-types.js +1 -1
- 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 +60 -9
- package/dist/server/services/websocket/session-handlers.js.map +1 -1
- package/dist/server/services/websocket/session-history.js +3 -0
- 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 -42
- 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 -0
- 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 -4
- package/dist/server/services/websocket/settings-handlers.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 -2
- 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 -12
- 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 -2
- 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 -2
- package/dist/server/services/websocket/tab-handlers.js.map +1 -1
- package/dist/server/services/websocket/types.d.ts +67 -7
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/dist/server/services/websocket/types.js +12 -6
- package/dist/server/services/websocket/types.js.map +1 -1
- package/package.json +5 -3
- package/server/cli/eta-estimator.ts +249 -0
- package/server/cli/headless/claude-invoker-process.ts +9 -1
- package/server/cli/headless/mcp-config.ts +30 -5
- package/server/cli/headless/runner.ts +21 -0
- package/server/cli/headless/stall-assessor.ts +93 -0
- package/server/cli/headless/tool-watchdog.ts +21 -0
- package/server/cli/headless/types.ts +16 -1
- package/server/cli/improvisation-history-store.ts +4 -1
- package/server/cli/improvisation-output-queue.ts +29 -7
- package/server/cli/improvisation-session-manager.ts +63 -1
- package/server/cli/improvisation-types.ts +9 -0
- package/server/cli/retry/retry-runner-factory.ts +1 -0
- 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 +9 -1
- package/server/mcp/bouncer-haiku.ts +21 -145
- package/server/mcp/bouncer-integration.ts +107 -5
- 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/server.ts +57 -0
- package/server/routes/index.ts +1 -0
- package/server/routes/internal.ts +112 -0
- package/server/services/plan/agent-resolver.ts +115 -0
- package/server/services/plan/agents/code-review.md +38 -8
- package/server/services/plan/composer.ts +63 -11
- package/server/services/plan/executor.ts +3 -1
- package/server/services/plan/issue-prompt-builder.ts +39 -1
- package/server/services/plan/parser-core.ts +1 -0
- package/server/services/plan/types.ts +4 -0
- package/server/services/runtime-info.ts +24 -0
- package/server/services/settings.ts +161 -4
- package/server/services/websocket/ask-user-question-bridge.ts +148 -0
- package/server/services/websocket/git-branch-handlers.ts +20 -6
- package/server/services/websocket/handler.ts +89 -2
- package/server/services/websocket/quality-complexity.ts +80 -26
- package/server/services/websocket/quality-eta.ts +155 -0
- package/server/services/websocket/quality-grading.ts +445 -222
- package/server/services/websocket/quality-handlers.ts +153 -7
- package/server/services/websocket/quality-operations.ts +72 -0
- package/server/services/websocket/quality-persistence.ts +17 -0
- package/server/services/websocket/quality-review-agent.ts +154 -64
- package/server/services/websocket/quality-service.ts +361 -13
- package/server/services/websocket/quality-tools.ts +51 -0
- package/server/services/websocket/quality-types.ts +41 -2
- package/server/services/websocket/session-handlers.ts +67 -10
- package/server/services/websocket/session-history.ts +3 -0
- package/server/services/websocket/session-initialization.ts +189 -46
- package/server/services/websocket/session-registry.ts +37 -0
- package/server/services/websocket/settings-handlers.ts +41 -4
- package/server/services/websocket/tab-broadcast.ts +10 -2
- package/server/services/websocket/tab-event-buffer.ts +143 -11
- package/server/services/websocket/tab-event-replay.ts +70 -3
- package/server/services/websocket/tab-handlers.ts +53 -5
- package/server/services/websocket/types.ts +85 -7
|
@@ -4,7 +4,7 @@ import { extname } from 'node:path';
|
|
|
4
4
|
import { analyzeComplexity, analyzeFunctionLength } from './quality-complexity.js';
|
|
5
5
|
import { computeQualityRating, gradeFromScore } from './quality-grading.js';
|
|
6
6
|
import { analyzeLinting } from './quality-linting.js';
|
|
7
|
-
import { chunkFileList, collectSourceFiles, detectEcosystem, filesByExt, runCommand, type SourceFile } from './quality-tools.js';
|
|
7
|
+
import { chunkFileList, collectSourceFiles, detectEcosystem, filesByExt, isTestFile, runCommand, type SourceFile } from './quality-tools.js';
|
|
8
8
|
import { type CategoryPenalty, type CategoryScore, type DimensionName, type Ecosystem, FILE_LENGTH_THRESHOLD, hasInstalledToolInCategory, type QualityFinding, type QualityResults, type ScanProgress, type ScoreBreakdown, TOTAL_STEPS } from './quality-types.js';
|
|
9
9
|
|
|
10
10
|
const NODE_FMT_EXTS = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
|
|
@@ -112,6 +112,295 @@ async function analyzeFormatting(
|
|
|
112
112
|
return { score, available: true, issueCount: acc.totalFiles - acc.passingFiles, findings: acc.findings.slice(0, 50) };
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
// ============================================================================
|
|
116
|
+
// Build / Compile Error Detection
|
|
117
|
+
// ============================================================================
|
|
118
|
+
//
|
|
119
|
+
// A codebase that does not compile is, by the user's spec, an automatic F.
|
|
120
|
+
// We capture compile failures as `category: 'build'` findings with severity
|
|
121
|
+
// `critical` so they map to the Reliability dimension and trigger the
|
|
122
|
+
// "critical → F-" path through the standard severity logic — no special-
|
|
123
|
+
// case branching elsewhere in the grading module.
|
|
124
|
+
//
|
|
125
|
+
// Per ecosystem:
|
|
126
|
+
// - Node: tsc --noEmit (only if a tsconfig.json is present)
|
|
127
|
+
// - Rust: cargo check (idiomatic compile-test for crates)
|
|
128
|
+
// - Other: skipped — Python has no canonical "is it valid" check, Go
|
|
129
|
+
// projects vary too much in module structure for `go build ./...`
|
|
130
|
+
// to be reliable, and Swift/Kotlin compile via larger build
|
|
131
|
+
// systems we don't want to spawn from a quality scan.
|
|
132
|
+
//
|
|
133
|
+
// Findings are capped at the first 5 errors per check so a totally broken
|
|
134
|
+
// codebase doesn't produce 200 individual findings — one critical finding is
|
|
135
|
+
// enough to pin the grade.
|
|
136
|
+
|
|
137
|
+
const BUILD_FINDING_CAP = 5;
|
|
138
|
+
|
|
139
|
+
function tscOutputToFindings(output: string, dirPath: string): QualityFinding[] {
|
|
140
|
+
const findings: QualityFinding[] = [];
|
|
141
|
+
// tsc error format: `path/to/file.ts(line,col): error TS####: message`
|
|
142
|
+
const errorPattern = /^(.+?)\((\d+),\d+\):\s+error\s+TS\d+:\s+(.+)$/gm;
|
|
143
|
+
for (const match of output.matchAll(errorPattern)) {
|
|
144
|
+
if (findings.length >= BUILD_FINDING_CAP) break;
|
|
145
|
+
const filePath = match[1].replace(`${dirPath}/`, '').replace(/^\.\//, '');
|
|
146
|
+
findings.push({
|
|
147
|
+
severity: 'critical',
|
|
148
|
+
category: 'build',
|
|
149
|
+
file: filePath,
|
|
150
|
+
line: Number.parseInt(match[2], 10) || null,
|
|
151
|
+
title: `TypeScript build error`,
|
|
152
|
+
description: match[3].trim(),
|
|
153
|
+
suggestion: 'Resolve compile errors before merging — broken builds block all other quality work.',
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return findings;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function cargoCheckOutputToFindings(output: string, dirPath: string): QualityFinding[] {
|
|
160
|
+
const findings: QualityFinding[] = [];
|
|
161
|
+
// cargo emits one JSON object per line in --message-format=json mode; in
|
|
162
|
+
// plain mode it emits "error[E####]: message\n --> path:line:col"
|
|
163
|
+
const errorPattern = /^error(?:\[E\d+\])?:\s+(.+?)$\s+-->\s+([^:\s]+):(\d+):\d+/gm;
|
|
164
|
+
for (const match of output.matchAll(errorPattern)) {
|
|
165
|
+
if (findings.length >= BUILD_FINDING_CAP) break;
|
|
166
|
+
findings.push({
|
|
167
|
+
severity: 'critical',
|
|
168
|
+
category: 'build',
|
|
169
|
+
file: match[2].replace(`${dirPath}/`, ''),
|
|
170
|
+
line: Number.parseInt(match[3], 10) || null,
|
|
171
|
+
title: `Rust build error`,
|
|
172
|
+
description: match[1].trim(),
|
|
173
|
+
suggestion: 'Resolve compile errors before merging — broken builds block all other quality work.',
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
return findings;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function checkNodeBuild(dirPath: string, installed: Set<string> | null): Promise<QualityFinding[]> {
|
|
180
|
+
// Only run if TypeScript is installed. Avoids npm-installing tsc on the fly
|
|
181
|
+
// (slow + side-effecting) and cleanly skips JS-only projects.
|
|
182
|
+
if (installed && !installed.has('typescript')) return [];
|
|
183
|
+
|
|
184
|
+
// Only run if a tsconfig.json exists at the project root — otherwise tsc
|
|
185
|
+
// will pick up arbitrary nearby configs in monorepos and produce confusing
|
|
186
|
+
// results.
|
|
187
|
+
let hasTsconfig = false;
|
|
188
|
+
try {
|
|
189
|
+
const { readFileSync } = await import('node:fs');
|
|
190
|
+
readFileSync(`${dirPath}/tsconfig.json`, 'utf-8');
|
|
191
|
+
hasTsconfig = true;
|
|
192
|
+
} catch {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
if (!hasTsconfig) return [];
|
|
196
|
+
|
|
197
|
+
const result = await runCommand('npx', ['tsc', '--noEmit', '--pretty', 'false'], dirPath);
|
|
198
|
+
if (result.exitCode === 0) return [];
|
|
199
|
+
// Combine stdout + stderr — tsc writes errors to stdout in --pretty=false.
|
|
200
|
+
return tscOutputToFindings(`${result.stdout}\n${result.stderr}`, dirPath);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function checkRustBuild(dirPath: string): Promise<QualityFinding[]> {
|
|
204
|
+
const result = await runCommand('cargo', ['check', '--message-format=human'], dirPath);
|
|
205
|
+
if (result.exitCode === 0) return [];
|
|
206
|
+
return cargoCheckOutputToFindings(`${result.stdout}\n${result.stderr}`, dirPath);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function analyzeBuildErrors(
|
|
210
|
+
dirPath: string,
|
|
211
|
+
ecosystems: Ecosystem[],
|
|
212
|
+
installed: Set<string> | null,
|
|
213
|
+
): Promise<{ findings: QualityFinding[]; available: boolean }> {
|
|
214
|
+
const findings: QualityFinding[] = [];
|
|
215
|
+
let ran = false;
|
|
216
|
+
|
|
217
|
+
if (ecosystems.includes('node')) {
|
|
218
|
+
const nodeFindings = await checkNodeBuild(dirPath, installed);
|
|
219
|
+
if (nodeFindings.length > 0) ran = true;
|
|
220
|
+
findings.push(...nodeFindings);
|
|
221
|
+
}
|
|
222
|
+
if (ecosystems.includes('rust')) {
|
|
223
|
+
ran = true;
|
|
224
|
+
findings.push(...(await checkRustBuild(dirPath)));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// `available` only matters for the dimension-availability heuristic; the
|
|
228
|
+
// findings drive the actual grade. For build, "available" tracks whether
|
|
229
|
+
// we ran a build check at all (so a clean tsc output still counts).
|
|
230
|
+
return { findings, available: ran || ecosystems.includes('node') || ecosystems.includes('rust') };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ============================================================================
|
|
234
|
+
// File Cohesion Analysis (LCOM-inspired)
|
|
235
|
+
// ============================================================================
|
|
236
|
+
//
|
|
237
|
+
// Long files are not all equal. A 1500-line file with one focused public
|
|
238
|
+
// surface (one class, one large function, several private helpers) is fine —
|
|
239
|
+
// it's cohesive. A 1500-line file mixing config + parsing + rendering + IO is
|
|
240
|
+
// a real maintenance hazard.
|
|
241
|
+
//
|
|
242
|
+
// We compute a 0-1 "mixed-concerns score" per file using cheap textual
|
|
243
|
+
// signals (no AST parsing — we already have the file content in memory):
|
|
244
|
+
//
|
|
245
|
+
// - Top-level export count — many independent exports = many concerns.
|
|
246
|
+
// - Distinct top-level identifier prefixes — cohesive files share a domain
|
|
247
|
+
// vocabulary (e.g., everything starts with `User…`); mixed files do not.
|
|
248
|
+
// - Distinct import roots — files that import from many unrelated modules
|
|
249
|
+
// are usually doing many unrelated things.
|
|
250
|
+
// - Section-divider density — `// ===` style dividers signal that the
|
|
251
|
+
// author is mentally separating concerns; many sections + low export
|
|
252
|
+
// overlap = mixed.
|
|
253
|
+
//
|
|
254
|
+
// The score then modulates the severity of any file-length violation:
|
|
255
|
+
//
|
|
256
|
+
// cohesion ≤ 0.30 → SUPPRESS the finding (the file is long but focused)
|
|
257
|
+
// cohesion ≤ 0.55 → low severity
|
|
258
|
+
// cohesion ≤ 0.75 → medium severity
|
|
259
|
+
// cohesion > 0.75 → high severity
|
|
260
|
+
//
|
|
261
|
+
// This implements the user's requirement that a 1000-line file might be
|
|
262
|
+
// "just fine" while another 1000-line file is a "severe mix of concerns."
|
|
263
|
+
// ============================================================================
|
|
264
|
+
|
|
265
|
+
const TOP_LEVEL_EXPORT_PATTERN = /^export\s+(?:default\s+)?(?:async\s+)?(?:function|class|const|let|var|interface|type|enum)\s+(\w+)/gm;
|
|
266
|
+
const TOP_LEVEL_DECL_PATTERN = /^(?:export\s+)?(?:async\s+)?(?:function|class)\s+(\w+)/gm;
|
|
267
|
+
const PY_TOP_LEVEL_PATTERN = /^(?:def|class)\s+(\w+)/gm;
|
|
268
|
+
const IMPORT_PATTERN = /^(?:import\s+.+from\s+['"]([^'"]+)['"]|from\s+([^\s]+)\s+import|import\s+([^\s]+))/gm;
|
|
269
|
+
const SECTION_DIVIDER_PATTERN = /^\s*(?:\/\/|#)\s*={3,}|^\s*(?:\/\/|#)\s*-{3,}/gm;
|
|
270
|
+
|
|
271
|
+
/** Group identifiers by their leading word-prefix and return how many distinct groups exist. */
|
|
272
|
+
function distinctIdentifierPrefixes(names: string[]): number {
|
|
273
|
+
if (names.length === 0) return 0;
|
|
274
|
+
const prefixes = new Set<string>();
|
|
275
|
+
for (const name of names) {
|
|
276
|
+
// Split on camelCase / snake_case boundaries; take the first segment.
|
|
277
|
+
const first = name.replace(/[A-Z][a-z]+|_+/g, (m, _o, _s) => `${m.replace(/_/g, '')}`).split('').filter(Boolean)[0] ?? name;
|
|
278
|
+
const lowered = first.toLowerCase();
|
|
279
|
+
if (lowered.length >= 2) prefixes.add(lowered);
|
|
280
|
+
}
|
|
281
|
+
return prefixes.size;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Extract the path "root" from an import specifier (the first non-dot segment). */
|
|
285
|
+
function importRoot(spec: string): string {
|
|
286
|
+
const trimmed = spec.replace(/^['"]|['"]$/g, '').trim();
|
|
287
|
+
if (!trimmed) return '';
|
|
288
|
+
if (trimmed.startsWith('.')) return 'relative';
|
|
289
|
+
return trimmed.split('/')[0].replace(/^@/, '');
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
interface CohesionSignals {
|
|
293
|
+
exports: number;
|
|
294
|
+
decls: number;
|
|
295
|
+
prefixes: number;
|
|
296
|
+
importRoots: number;
|
|
297
|
+
dividers: number;
|
|
298
|
+
isJs: boolean;
|
|
299
|
+
isPy: boolean;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function jsDeclNames(content: string): { exports: string[]; decls: string[] } {
|
|
303
|
+
const exports: string[] = [];
|
|
304
|
+
const decls: string[] = [];
|
|
305
|
+
for (const match of content.matchAll(TOP_LEVEL_EXPORT_PATTERN)) exports.push(match[1]);
|
|
306
|
+
for (const match of content.matchAll(TOP_LEVEL_DECL_PATTERN)) decls.push(match[1]);
|
|
307
|
+
return { exports, decls };
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function pyDeclNames(content: string): { exports: string[]; decls: string[] } {
|
|
311
|
+
const exports: string[] = [];
|
|
312
|
+
const decls: string[] = [];
|
|
313
|
+
for (const match of content.matchAll(PY_TOP_LEVEL_PATTERN)) {
|
|
314
|
+
decls.push(match[1]);
|
|
315
|
+
// Python doesn't have explicit "export" — public iff it doesn't start with "_".
|
|
316
|
+
if (!match[1].startsWith('_')) exports.push(match[1]);
|
|
317
|
+
}
|
|
318
|
+
return { exports, decls };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function importRootCount(content: string): number {
|
|
322
|
+
const roots = new Set<string>();
|
|
323
|
+
for (const match of content.matchAll(IMPORT_PATTERN)) {
|
|
324
|
+
const spec = match[1] ?? match[2] ?? match[3] ?? '';
|
|
325
|
+
const root = importRoot(spec);
|
|
326
|
+
if (root) roots.add(root);
|
|
327
|
+
}
|
|
328
|
+
return roots.size;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function collectCohesionSignals(file: SourceFile): CohesionSignals {
|
|
332
|
+
const ext = extname(file.path).toLowerCase();
|
|
333
|
+
const isJs = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext);
|
|
334
|
+
const isPy = ['.py', '.pyi'].includes(ext);
|
|
335
|
+
|
|
336
|
+
const { exports, decls } = isJs
|
|
337
|
+
? jsDeclNames(file.content)
|
|
338
|
+
: isPy
|
|
339
|
+
? pyDeclNames(file.content)
|
|
340
|
+
: { exports: [], decls: [] };
|
|
341
|
+
|
|
342
|
+
const allNames = exports.length > 0 ? exports : decls;
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
exports: exports.length,
|
|
346
|
+
decls: decls.length,
|
|
347
|
+
prefixes: distinctIdentifierPrefixes(allNames),
|
|
348
|
+
importRoots: importRootCount(file.content),
|
|
349
|
+
dividers: (file.content.match(SECTION_DIVIDER_PATTERN) || []).length,
|
|
350
|
+
isJs,
|
|
351
|
+
isPy,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Compute a 0-1 "mixed-concerns" score for a file. 0 = highly cohesive,
|
|
357
|
+
* 1 = many unrelated concerns. The formula combines four signals with
|
|
358
|
+
* empirically chosen weights — tuned so that:
|
|
359
|
+
*
|
|
360
|
+
* - A 2000-line file with 1 class + helpers scores ~0.15
|
|
361
|
+
* - A 2000-line file with 8 unrelated exports scores ~0.85
|
|
362
|
+
* - The CLI quality-tools.ts (one domain, many helpers) scores < 0.4
|
|
363
|
+
* - A miscellaneous "utils.ts" (string + date + DOM helpers) scores > 0.7
|
|
364
|
+
*
|
|
365
|
+
* Returns 0 for files we can't analyze (non-JS/Py), since we don't want to
|
|
366
|
+
* fabricate a violation for languages we can't introspect.
|
|
367
|
+
*/
|
|
368
|
+
function computeMixedConcernsScore(file: SourceFile): number {
|
|
369
|
+
const sig = collectCohesionSignals(file);
|
|
370
|
+
if (!sig.isJs && !sig.isPy) return 0;
|
|
371
|
+
|
|
372
|
+
// Each component is independently normalized to [0, 1]; the final score
|
|
373
|
+
// averages them with slight weighting toward identifier-prefix variance
|
|
374
|
+
// (the strongest cohesion signal in practice).
|
|
375
|
+
const exportComponent = sig.exports <= 2 ? 0 : Math.min(1, (sig.exports - 2) / 12);
|
|
376
|
+
const prefixComponent = sig.prefixes <= 1 ? 0 : Math.min(1, (sig.prefixes - 1) / 6);
|
|
377
|
+
const importComponent = sig.importRoots <= 4 ? 0 : Math.min(1, (sig.importRoots - 4) / 12);
|
|
378
|
+
const dividerComponent = sig.dividers <= 2 ? 0 : Math.min(1, (sig.dividers - 2) / 6);
|
|
379
|
+
|
|
380
|
+
return Math.min(
|
|
381
|
+
1,
|
|
382
|
+
0.30 * prefixComponent +
|
|
383
|
+
0.30 * exportComponent +
|
|
384
|
+
0.25 * importComponent +
|
|
385
|
+
0.15 * dividerComponent,
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Map a file's mixed-concerns score to a severity for the file-length
|
|
391
|
+
* finding. Returns `null` to suppress the finding entirely when the file is
|
|
392
|
+
* cohesive enough that its length isn't a real concern.
|
|
393
|
+
*/
|
|
394
|
+
function severityFromCohesion(mixed: number, lines: number): QualityFinding['severity'] | null {
|
|
395
|
+
// Files that are absurdly long (>5x threshold) emit a finding regardless
|
|
396
|
+
// of cohesion — a 5000-line file is always worth flagging even if focused.
|
|
397
|
+
const isAbsurd = lines > FILE_LENGTH_THRESHOLD * 5;
|
|
398
|
+
if (mixed <= 0.30 && !isAbsurd) return null;
|
|
399
|
+
if (mixed <= 0.55) return 'low';
|
|
400
|
+
if (mixed <= 0.75) return 'medium';
|
|
401
|
+
return 'high';
|
|
402
|
+
}
|
|
403
|
+
|
|
115
404
|
// ============================================================================
|
|
116
405
|
// File Length Analysis
|
|
117
406
|
// ============================================================================
|
|
@@ -121,28 +410,53 @@ function analyzeFileLength(files: SourceFile[]): { score: number; findings: Qual
|
|
|
121
410
|
|
|
122
411
|
const findings: QualityFinding[] = [];
|
|
123
412
|
let totalScore = 0;
|
|
413
|
+
let scoredFiles = 0;
|
|
124
414
|
|
|
125
415
|
for (const file of files) {
|
|
416
|
+
// Test files are exempt from structural-length checks: a long test file
|
|
417
|
+
// is normally just many independent small tests, which is a feature.
|
|
418
|
+
// Excluding them from both scoring and finding emission keeps the
|
|
419
|
+
// dimension's score honest (otherwise a clean prod codebase with a
|
|
420
|
+
// huge test file would be unfairly penalised on file-length).
|
|
421
|
+
if (isTestFile(file.relativePath)) continue;
|
|
422
|
+
|
|
126
423
|
const ratio = Math.max(1, file.lines / FILE_LENGTH_THRESHOLD);
|
|
127
424
|
const fileScore = 100 / ratio ** 1.5;
|
|
128
425
|
totalScore += fileScore;
|
|
426
|
+
scoredFiles++;
|
|
129
427
|
|
|
130
428
|
if (file.lines > FILE_LENGTH_THRESHOLD) {
|
|
429
|
+
const mixedScore = computeMixedConcernsScore(file);
|
|
430
|
+
const severity = severityFromCohesion(mixedScore, file.lines);
|
|
431
|
+
if (!severity) continue; // Cohesive long file — not actually a violation.
|
|
432
|
+
|
|
433
|
+
const cohesionPct = Math.round((1 - mixedScore) * 100);
|
|
131
434
|
findings.push({
|
|
132
|
-
severity
|
|
435
|
+
severity,
|
|
133
436
|
category: 'file-length',
|
|
134
437
|
file: file.relativePath,
|
|
135
438
|
line: null,
|
|
136
|
-
title: `File has ${file.lines} lines (threshold: ${FILE_LENGTH_THRESHOLD})`,
|
|
137
|
-
description:
|
|
439
|
+
title: `File has ${file.lines} lines (threshold: ${FILE_LENGTH_THRESHOLD}, cohesion: ${cohesionPct}%)`,
|
|
440
|
+
description:
|
|
441
|
+
`Exceeds the ${FILE_LENGTH_THRESHOLD}-line threshold by ${file.lines - FILE_LENGTH_THRESHOLD} lines. ` +
|
|
442
|
+
`Mixed-concerns score is ${roundOne(mixedScore)} (0 = focused, 1 = many concerns); ` +
|
|
443
|
+
`severity reflects how mixed the file's responsibilities appear. ` +
|
|
444
|
+
(mixedScore > 0.55
|
|
445
|
+
? 'Consider splitting unrelated exports into separate modules.'
|
|
446
|
+
: 'The file is long but reasonably focused — split only if a clear seam exists.'),
|
|
138
447
|
});
|
|
139
448
|
}
|
|
140
449
|
}
|
|
141
450
|
|
|
142
|
-
|
|
451
|
+
if (scoredFiles === 0) return { score: 100, findings: [], issueCount: 0 };
|
|
452
|
+
const score = Math.round(totalScore / scoredFiles);
|
|
143
453
|
return { score: Math.min(100, score), findings: findings.slice(0, 50), issueCount: findings.length };
|
|
144
454
|
}
|
|
145
455
|
|
|
456
|
+
function roundOne(n: number): number {
|
|
457
|
+
return Math.round(n * 10) / 10;
|
|
458
|
+
}
|
|
459
|
+
|
|
146
460
|
// ============================================================================
|
|
147
461
|
// Legacy Scoring Breakdown — produces the per-category penalty data still
|
|
148
462
|
// consumed by older UI surfaces and persisted reports. The canonical grade
|
|
@@ -254,10 +568,27 @@ export function computeFormulaScore(
|
|
|
254
568
|
|
|
255
569
|
export type ProgressCallback = (progress: ScanProgress) => void;
|
|
256
570
|
|
|
571
|
+
/**
|
|
572
|
+
* Sentinel thrown when a scan is cancelled mid-flight via the `signal`
|
|
573
|
+
* argument. Callers should treat it as a clean cancellation, not a scan
|
|
574
|
+
* failure (no `qualityError` payload, no persisted partial result).
|
|
575
|
+
*/
|
|
576
|
+
export class QualityScanAbortedError extends Error {
|
|
577
|
+
constructor() {
|
|
578
|
+
super('Quality scan aborted');
|
|
579
|
+
this.name = 'QualityScanAbortedError';
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function checkAborted(signal: AbortSignal | undefined): void {
|
|
584
|
+
if (signal?.aborted) throw new QualityScanAbortedError();
|
|
585
|
+
}
|
|
586
|
+
|
|
257
587
|
export async function runQualityScan(
|
|
258
588
|
dirPath: string,
|
|
259
589
|
onProgress?: ProgressCallback,
|
|
260
590
|
installedToolNames?: string[],
|
|
591
|
+
signal?: AbortSignal,
|
|
261
592
|
): Promise<QualityResults> {
|
|
262
593
|
const ecosystems = detectEcosystem(dirPath);
|
|
263
594
|
|
|
@@ -269,10 +600,12 @@ export async function runQualityScan(
|
|
|
269
600
|
};
|
|
270
601
|
|
|
271
602
|
// Step 1: Collect source files
|
|
603
|
+
checkAborted(signal);
|
|
272
604
|
progress('Collecting source files', 1);
|
|
273
605
|
const files = await collectSourceFiles(dirPath, dirPath);
|
|
274
606
|
|
|
275
607
|
// Step 2: Run linting (only if a linter is installed)
|
|
608
|
+
checkAborted(signal);
|
|
276
609
|
progress('Running linters', 2);
|
|
277
610
|
const hasLinter = !installedSet || hasInstalledToolInCategory(installedSet, ecosystems, 'linter');
|
|
278
611
|
const lintResult = hasLinter
|
|
@@ -280,28 +613,39 @@ export async function runQualityScan(
|
|
|
280
613
|
: { score: 0, findings: [], available: false, issueCount: 0 };
|
|
281
614
|
|
|
282
615
|
// Step 3: Check formatting (only if a formatter is installed)
|
|
616
|
+
checkAborted(signal);
|
|
283
617
|
progress('Checking formatting', 3);
|
|
284
618
|
const hasFormatter = !installedSet || hasInstalledToolInCategory(installedSet, ecosystems, 'formatter');
|
|
285
619
|
const fmtResult = hasFormatter
|
|
286
620
|
? await analyzeFormatting(dirPath, ecosystems, files)
|
|
287
621
|
: { score: 0, available: false, issueCount: 0, findings: [] as QualityFinding[] };
|
|
288
622
|
|
|
289
|
-
// Step 4:
|
|
290
|
-
|
|
623
|
+
// Step 4: Check for build/compile errors (auto-F if any are found)
|
|
624
|
+
checkAborted(signal);
|
|
625
|
+
progress('Checking build', 4);
|
|
626
|
+
const buildResult = await analyzeBuildErrors(dirPath, ecosystems, installedSet);
|
|
627
|
+
|
|
628
|
+
// Step 5: Analyze complexity (using real tools: Biome, ESLint, radon)
|
|
629
|
+
checkAborted(signal);
|
|
630
|
+
progress('Analyzing complexity', 5);
|
|
291
631
|
const complexityResult = await analyzeComplexity(dirPath, ecosystems, files, installedToolNames);
|
|
292
632
|
|
|
293
|
-
// Step
|
|
294
|
-
|
|
633
|
+
// Step 6: Check file lengths
|
|
634
|
+
checkAborted(signal);
|
|
635
|
+
progress('Checking file lengths', 6);
|
|
295
636
|
const fileLengthResult = analyzeFileLength(files);
|
|
296
637
|
|
|
297
|
-
// Step
|
|
298
|
-
|
|
638
|
+
// Step 7: Check function lengths
|
|
639
|
+
checkAborted(signal);
|
|
640
|
+
progress('Checking function lengths', 7);
|
|
299
641
|
const funcLengthResult = analyzeFunctionLength(files);
|
|
300
642
|
|
|
301
|
-
// Step
|
|
302
|
-
|
|
643
|
+
// Step 8: Compute scores
|
|
644
|
+
checkAborted(signal);
|
|
645
|
+
progress('Computing scores', 8);
|
|
303
646
|
|
|
304
647
|
const allFindings = [
|
|
648
|
+
...buildResult.findings,
|
|
305
649
|
...lintResult.findings,
|
|
306
650
|
...fmtResult.findings,
|
|
307
651
|
...complexityResult.findings,
|
|
@@ -323,7 +667,11 @@ export async function runQualityScan(
|
|
|
323
667
|
}
|
|
324
668
|
const rating = computeQualityRating(allFindings, totalLines, { forceNA });
|
|
325
669
|
|
|
670
|
+
// Build score: 100 if no compile errors, 0 if any (one error breaks everything).
|
|
671
|
+
const buildScore = buildResult.findings.length === 0 ? 100 : 0;
|
|
672
|
+
|
|
326
673
|
const categories: CategoryScore[] = [
|
|
674
|
+
{ name: 'Build', score: buildScore, available: buildResult.available, issueCount: buildResult.findings.length },
|
|
327
675
|
{ name: 'Linting', score: lintResult.score, available: lintResult.available, issueCount: lintResult.issueCount },
|
|
328
676
|
{ name: 'Formatting', score: fmtResult.score, available: fmtResult.available, issueCount: fmtResult.issueCount },
|
|
329
677
|
{ name: 'Complexity', score: complexityResult.score, available: complexityResult.available, issueCount: complexityResult.issueCount },
|
|
@@ -260,6 +260,57 @@ export function filesByExt(files: SourceFile[], exts: string[]): string[] {
|
|
|
260
260
|
return out;
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
+
/** Folders that signal "this whole tree is test code" by convention. */
|
|
264
|
+
const TEST_FOLDER_SEGMENTS = ['__tests__', '__mocks__', 'tests', 'test', 'e2e', 'spec'];
|
|
265
|
+
|
|
266
|
+
/** Filename regexes that mark a single file as a test, regardless of folder. */
|
|
267
|
+
const TEST_FILE_PATTERNS: RegExp[] = [
|
|
268
|
+
/\.(test|spec)\.(ts|tsx|js|jsx|mjs|cjs|mts|cts)$/, // JS/TS .test/.spec
|
|
269
|
+
/_test\.(go|py)$/, // Go / Python *_test
|
|
270
|
+
/^test_.+\.py$/, // Python test_*.py
|
|
271
|
+
];
|
|
272
|
+
|
|
273
|
+
function pathHasTestFolder(path: string): boolean {
|
|
274
|
+
for (const segment of TEST_FOLDER_SEGMENTS) {
|
|
275
|
+
if (path.includes(`/${segment}/`) || path.startsWith(`${segment}/`)) return true;
|
|
276
|
+
}
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function fileNameLooksLikeTest(name: string): boolean {
|
|
281
|
+
for (const pattern of TEST_FILE_PATTERNS) {
|
|
282
|
+
if (pattern.test(name)) return true;
|
|
283
|
+
}
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Identify a path as a test/spec file. Test files are exempt from
|
|
289
|
+
* structural-length checks (long-file, long-function) because:
|
|
290
|
+
*
|
|
291
|
+
* - A 600-line test file with 50 small `it()` blocks is easy to read,
|
|
292
|
+
* each block is independent, and "split it" yields zero maintenance
|
|
293
|
+
* benefit while harming discoverability.
|
|
294
|
+
* - A 200-line test function (long Arrange-Act-Assert with helpers
|
|
295
|
+
* inlined) is normal for feature coverage and not a complexity smell.
|
|
296
|
+
*
|
|
297
|
+
* Linters and security/bug findings still apply to test files — only the
|
|
298
|
+
* structural-length heuristics defer. Pattern-matches the conventions used
|
|
299
|
+
* by Code Climate's default-exclude set:
|
|
300
|
+
*
|
|
301
|
+
* - JS/TS: *.test.ts, *.test.tsx, *.spec.js, *.spec.jsx, *.test.mts, ...
|
|
302
|
+
* - Folder: __tests__/, /tests/, /test/, e2e/, spec/, __mocks__/
|
|
303
|
+
* - Python: test_*.py, *_test.py
|
|
304
|
+
* - Go: *_test.go
|
|
305
|
+
* - Rust: files inside `tests/` are integration tests by convention
|
|
306
|
+
*/
|
|
307
|
+
export function isTestFile(relativePath: string): boolean {
|
|
308
|
+
const path = relativePath.replace(/\\/g, '/').toLowerCase();
|
|
309
|
+
if (pathHasTestFolder(path)) return true;
|
|
310
|
+
const name = path.split('/').pop() ?? path;
|
|
311
|
+
return fileNameLooksLikeTest(name);
|
|
312
|
+
}
|
|
313
|
+
|
|
263
314
|
/**
|
|
264
315
|
* Split a file list into chunks so a single command invocation doesn't
|
|
265
316
|
* blow past ARG_MAX. macOS ARG_MAX is ~256KB; 400 paths at ~200 chars each
|
|
@@ -4,7 +4,27 @@
|
|
|
4
4
|
// Types
|
|
5
5
|
// ============================================================================
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Letter grades produced by the multi-dimensional quality rating.
|
|
9
|
+
*
|
|
10
|
+
* Score → grade mapping (per product spec — note: no `D`; F covers 56-69, F-
|
|
11
|
+
* covers 0-55. `D` is retained ONLY for backward compatibility with reports
|
|
12
|
+
* persisted by older versions of this module — new code never emits it):
|
|
13
|
+
*
|
|
14
|
+
* A+ 97-100 A 93-96 A- 90-92
|
|
15
|
+
* B+ 87-89 B 83-86 B- 80-82
|
|
16
|
+
* C+ 77-79 C 73-76 C- 70-72
|
|
17
|
+
* F+ 65-69 F 56-64 F- 0-55
|
|
18
|
+
*
|
|
19
|
+
* `N/A` means the dimension had no tooling available to evaluate it.
|
|
20
|
+
*/
|
|
21
|
+
export type Grade =
|
|
22
|
+
| 'A+' | 'A' | 'A-'
|
|
23
|
+
| 'B+' | 'B' | 'B-'
|
|
24
|
+
| 'C+' | 'C' | 'C-'
|
|
25
|
+
| 'D' // legacy — only appears on reports persisted before the +/- rollout
|
|
26
|
+
| 'F+' | 'F' | 'F-'
|
|
27
|
+
| 'N/A';
|
|
8
28
|
export type DimensionName = 'security' | 'reliability' | 'maintainability';
|
|
9
29
|
|
|
10
30
|
export interface DimensionScore {
|
|
@@ -80,12 +100,31 @@ export interface QualityResults {
|
|
|
80
100
|
dimensions?: DimensionScore[];
|
|
81
101
|
qualityGate?: QualityGate;
|
|
82
102
|
gradeRationale?: string;
|
|
103
|
+
/** Wall-clock duration of the CLI scan that produced this report. Used to estimate ETA on subsequent scans of the same directory. */
|
|
104
|
+
scanDurationMs?: number;
|
|
105
|
+
/** Wall-clock duration of the AI code-review pass, when one ran. */
|
|
106
|
+
reviewDurationMs?: number;
|
|
83
107
|
}
|
|
84
108
|
|
|
85
109
|
export interface ScanProgress {
|
|
86
110
|
step: string;
|
|
87
111
|
current: number;
|
|
88
112
|
total: number;
|
|
113
|
+
/**
|
|
114
|
+
* Wall-clock estimate of the total scan duration, in milliseconds. Sent on
|
|
115
|
+
* the first progress event of a scan and again on subsequent events so
|
|
116
|
+
* reconnecting clients still get an ETA. The web subtracts elapsed time to
|
|
117
|
+
* render "≈ X remaining".
|
|
118
|
+
*/
|
|
119
|
+
etaMs?: number;
|
|
120
|
+
/**
|
|
121
|
+
* Server-side timestamp (ms since epoch) of when the scan started — paired
|
|
122
|
+
* with `etaMs` so the web can compute elapsed time without trusting its
|
|
123
|
+
* own clock alignment.
|
|
124
|
+
*/
|
|
125
|
+
startedAt?: number;
|
|
126
|
+
/** Optional sub-step detail used by long-running steps (e.g. "tsc --noEmit, 18s elapsed") to keep the UI from looking stuck. */
|
|
127
|
+
detail?: string;
|
|
89
128
|
}
|
|
90
129
|
|
|
91
130
|
export type Ecosystem = 'node' | 'python' | 'rust' | 'go' | 'swift' | 'kotlin' | 'unknown';
|
|
@@ -161,7 +200,7 @@ export const ADDITIONAL_EXCLUDES = new Set([
|
|
|
161
200
|
|
|
162
201
|
export const FILE_LENGTH_THRESHOLD = 300;
|
|
163
202
|
export const FUNCTION_LENGTH_THRESHOLD = 50;
|
|
164
|
-
export const TOTAL_STEPS =
|
|
203
|
+
export const TOTAL_STEPS = 8;
|
|
165
204
|
|
|
166
205
|
export function hasInstalledToolInCategory(
|
|
167
206
|
installedSet: Set<string>,
|