@girardmedia/bootspring 2.0.21 → 2.0.23
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/bin/bootspring.js +5 -0
- package/cli/org.js +474 -0
- package/cli/preseed/index.js +16 -0
- package/cli/preseed/interactive.js +143 -0
- package/cli/preseed/templates.js +227 -0
- package/cli/preseed.js +9 -301
- package/cli/seed/builders/ai-context-builder.js +85 -0
- package/cli/seed/builders/index.js +13 -0
- package/cli/seed/builders/seed-builder.js +272 -0
- package/cli/seed/extractors/content-extractors.js +383 -0
- package/cli/seed/extractors/index.js +47 -0
- package/cli/seed/extractors/metadata-extractors.js +167 -0
- package/cli/seed/extractors/section-extractor.js +54 -0
- package/cli/seed/extractors/stack-extractors.js +228 -0
- package/cli/seed/index.js +18 -0
- package/cli/seed/utils/folder-structure.js +84 -0
- package/cli/seed/utils/index.js +11 -0
- package/cli/seed.js +23 -1074
- package/core/api-client.js +77 -0
- package/core/entitlements.js +36 -0
- package/core/organizations.js +223 -0
- package/core/policies.js +51 -6
- package/core/policy-matrix.js +303 -0
- package/core/project-context.js +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +3220 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/context-McpJQa_2.d.ts +5710 -0
- package/dist/core/index.d.ts +635 -0
- package/dist/core/index.js +2593 -0
- package/dist/core/index.js.map +1 -0
- package/dist/index-QqbeEiDm.d.ts +857 -0
- package/dist/index-UiYCgwiH.d.ts +174 -0
- package/dist/index.d.ts +453 -0
- package/dist/index.js +44228 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +41173 -0
- package/dist/mcp/index.js.map +1 -0
- package/generators/index.ts +82 -0
- package/intelligence/orchestrator/config/failure-signatures.js +48 -0
- package/intelligence/orchestrator/config/index.js +23 -0
- package/intelligence/orchestrator/config/pack-lifecycle.js +262 -0
- package/intelligence/orchestrator/config/phases.js +111 -0
- package/intelligence/orchestrator/config/remediation.js +150 -0
- package/intelligence/orchestrator/config/workflows.js +168 -0
- package/intelligence/orchestrator/core/index.js +16 -0
- package/intelligence/orchestrator/core/state-manager.js +88 -0
- package/intelligence/orchestrator/core/telemetry.js +24 -0
- package/intelligence/orchestrator/index.js +17 -0
- package/intelligence/orchestrator.js +17 -512
- package/mcp/contracts/mcp-contract.v1.json +1 -1
- package/package.json +16 -3
- package/src/cli/agent.ts +703 -0
- package/src/cli/analyze.ts +640 -0
- package/src/cli/audit.ts +707 -0
- package/src/cli/auth.ts +930 -0
- package/src/cli/billing.ts +364 -0
- package/src/cli/build.ts +1089 -0
- package/src/cli/business.ts +508 -0
- package/src/cli/checkpoint-utils.ts +236 -0
- package/src/cli/checkpoint.ts +757 -0
- package/src/cli/cloud-sync.ts +534 -0
- package/src/cli/content.ts +273 -0
- package/src/cli/context.ts +667 -0
- package/src/cli/dashboard.ts +133 -0
- package/src/cli/deploy.ts +704 -0
- package/src/cli/doctor.ts +480 -0
- package/src/cli/fundraise.ts +494 -0
- package/src/cli/generate.ts +346 -0
- package/src/cli/github-cmd.ts +566 -0
- package/src/cli/health.ts +599 -0
- package/src/cli/index.ts +113 -0
- package/src/cli/init.ts +838 -0
- package/src/cli/legal.ts +495 -0
- package/src/cli/log.ts +316 -0
- package/src/cli/loop.ts +1660 -0
- package/src/cli/manager.ts +878 -0
- package/src/cli/mcp.ts +275 -0
- package/src/cli/memory.ts +346 -0
- package/src/cli/metrics.ts +590 -0
- package/src/cli/monitor.ts +960 -0
- package/src/cli/mvp.ts +662 -0
- package/src/cli/onboard.ts +663 -0
- package/src/cli/orchestrator.ts +622 -0
- package/src/cli/plugin.ts +483 -0
- package/src/cli/prd.ts +671 -0
- package/src/cli/preseed-start.ts +1633 -0
- package/src/cli/preseed.ts +2434 -0
- package/src/cli/project.ts +526 -0
- package/src/cli/quality.ts +885 -0
- package/src/cli/security.ts +1079 -0
- package/src/cli/seed.ts +1224 -0
- package/src/cli/skill.ts +537 -0
- package/src/cli/suggest.ts +1225 -0
- package/src/cli/switch.ts +518 -0
- package/src/cli/task.ts +780 -0
- package/src/cli/telemetry.ts +172 -0
- package/src/cli/todo.ts +627 -0
- package/src/cli/types.ts +15 -0
- package/src/cli/update.ts +334 -0
- package/src/cli/visualize.ts +609 -0
- package/src/cli/watch.ts +895 -0
- package/src/cli/workspace.ts +709 -0
- package/src/core/action-recorder.ts +673 -0
- package/src/core/analyze-workflow.ts +1453 -0
- package/src/core/api-client.ts +1120 -0
- package/src/core/audit-workflow.ts +1681 -0
- package/src/core/auth.ts +471 -0
- package/src/core/build-orchestrator.ts +509 -0
- package/src/core/build-state.ts +621 -0
- package/src/core/checkpoint-engine.ts +482 -0
- package/src/core/config.ts +1285 -0
- package/src/core/context-loader.ts +694 -0
- package/src/core/context.ts +410 -0
- package/src/core/deploy-workflow.ts +1085 -0
- package/src/core/entitlements.ts +322 -0
- package/src/core/github-sync.ts +720 -0
- package/src/core/index.ts +981 -0
- package/src/core/ingest.ts +1186 -0
- package/src/core/metrics-engine.ts +886 -0
- package/src/core/mvp.ts +847 -0
- package/src/core/onboard-workflow.ts +1293 -0
- package/src/core/policies.ts +81 -0
- package/src/core/preseed-workflow.ts +1163 -0
- package/src/core/preseed.ts +1826 -0
- package/src/core/project-context.ts +380 -0
- package/src/core/project-state.ts +699 -0
- package/src/core/r2-sync.ts +691 -0
- package/src/core/scaffold.ts +1715 -0
- package/src/core/session.ts +286 -0
- package/src/core/task-extractor.ts +799 -0
- package/src/core/telemetry.ts +371 -0
- package/src/core/tier-enforcement.ts +737 -0
- package/src/core/utils.ts +437 -0
- package/src/index.ts +29 -0
- package/src/intelligence/agent-collab.ts +2376 -0
- package/src/intelligence/auto-suggest.ts +713 -0
- package/src/intelligence/content-gen.ts +1351 -0
- package/src/intelligence/cross-project.ts +1692 -0
- package/src/intelligence/git-memory.ts +529 -0
- package/src/intelligence/index.ts +318 -0
- package/src/intelligence/orchestrator.ts +534 -0
- package/src/intelligence/prd.ts +466 -0
- package/src/intelligence/recommendations.ts +982 -0
- package/src/intelligence/workflow-composer.ts +1472 -0
- package/src/mcp/capabilities.ts +233 -0
- package/src/mcp/index.ts +37 -0
- package/src/mcp/registry.ts +1268 -0
- package/src/mcp/response-formatter.ts +797 -0
- package/src/mcp/server.ts +240 -0
- package/src/types/agent.ts +69 -0
- package/src/types/config.ts +86 -0
- package/src/types/context.ts +77 -0
- package/src/types/index.ts +53 -0
- package/src/types/mcp.ts +91 -0
- package/src/types/skills.ts +47 -0
- package/src/types/workflow.ts +155 -0
- package/generators/index.js +0 -18
|
@@ -0,0 +1,886 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bootspring Metrics Engine
|
|
3
|
+
* Comprehensive project metrics tracking and scoring
|
|
4
|
+
*
|
|
5
|
+
* @package bootspring
|
|
6
|
+
* @module core/metrics-engine
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
import { execSync } from 'child_process';
|
|
12
|
+
import * as projectState from './project-state';
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Types
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
export type MetricCategory = 'progress' | 'quality' | 'docs' | 'performance' | 'activity';
|
|
19
|
+
export type ComplexityLevel = 'low' | 'medium' | 'high';
|
|
20
|
+
export type TrendDirection = 'up' | 'down' | 'stable';
|
|
21
|
+
|
|
22
|
+
export interface MetricDefinition {
|
|
23
|
+
label: string;
|
|
24
|
+
category: MetricCategory;
|
|
25
|
+
weight: number;
|
|
26
|
+
icon: string;
|
|
27
|
+
invert?: boolean | undefined; // Lower is better
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface MetricResult {
|
|
31
|
+
score: number;
|
|
32
|
+
data?: unknown | undefined;
|
|
33
|
+
source?: string | null | undefined;
|
|
34
|
+
error?: string | undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CoverageData {
|
|
38
|
+
lines?: number | undefined;
|
|
39
|
+
statements?: number | undefined;
|
|
40
|
+
functions?: number | undefined;
|
|
41
|
+
branches?: number | undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface BundleSizeData {
|
|
45
|
+
totalKB: number;
|
|
46
|
+
pageCount: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface DocsCoverageData {
|
|
50
|
+
files: number;
|
|
51
|
+
documented: number;
|
|
52
|
+
coverage: number;
|
|
53
|
+
details?: Record<string, string> | undefined;
|
|
54
|
+
docFiles?: number | undefined;
|
|
55
|
+
planningFiles?: number | undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface CodeComplexityData {
|
|
59
|
+
totalFiles: number;
|
|
60
|
+
totalLines: number;
|
|
61
|
+
avgLinesPerFile: number;
|
|
62
|
+
complexity: ComplexityLevel;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface TodoCompletionData {
|
|
66
|
+
total: number;
|
|
67
|
+
completed: number;
|
|
68
|
+
pending: number;
|
|
69
|
+
todoComments?: number | undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface DebtIndicator {
|
|
73
|
+
type: string;
|
|
74
|
+
count: number;
|
|
75
|
+
severity: 'low' | 'medium' | 'high';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface TechnicalDebtData {
|
|
79
|
+
indicators: DebtIndicator[];
|
|
80
|
+
totalIssues: number;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface CommitFrequencyData {
|
|
84
|
+
last30Days: number;
|
|
85
|
+
avgPerDay: number;
|
|
86
|
+
error?: string | undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface ReadmeScoreData {
|
|
90
|
+
exists: boolean;
|
|
91
|
+
lines?: number | undefined;
|
|
92
|
+
words?: number | undefined;
|
|
93
|
+
sections?: Record<string, boolean> | undefined;
|
|
94
|
+
path?: string | undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface CategoryData {
|
|
98
|
+
total: number;
|
|
99
|
+
count: number;
|
|
100
|
+
average?: number | undefined;
|
|
101
|
+
metrics: CategoryMetricSummary[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface CategoryMetricSummary {
|
|
105
|
+
id: string;
|
|
106
|
+
label: string;
|
|
107
|
+
score: number;
|
|
108
|
+
icon: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface MetricsResults {
|
|
112
|
+
timestamp: string;
|
|
113
|
+
metrics: Record<string, MetricResult>;
|
|
114
|
+
categories: Record<string, CategoryData>;
|
|
115
|
+
overallScore: number;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface StoredMetrics {
|
|
119
|
+
lastCollected: string;
|
|
120
|
+
overallScore: number;
|
|
121
|
+
categories: Record<string, { score: number; count: number }>;
|
|
122
|
+
scores: Record<string, number>;
|
|
123
|
+
history: Array<{ score: number; date: string }>;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface CollectOptions {
|
|
127
|
+
skip?: string[] | undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ============================================================================
|
|
131
|
+
// Metric Definitions
|
|
132
|
+
// ============================================================================
|
|
133
|
+
|
|
134
|
+
export const METRICS: Record<string, MetricDefinition> = {
|
|
135
|
+
// Health & Progress
|
|
136
|
+
health: {
|
|
137
|
+
label: 'Health Score',
|
|
138
|
+
category: 'progress',
|
|
139
|
+
weight: 1.0,
|
|
140
|
+
icon: '💚'
|
|
141
|
+
},
|
|
142
|
+
checkpoints: {
|
|
143
|
+
label: 'Checkpoint Progress',
|
|
144
|
+
category: 'progress',
|
|
145
|
+
weight: 0.2,
|
|
146
|
+
icon: '✅'
|
|
147
|
+
},
|
|
148
|
+
security: {
|
|
149
|
+
label: 'Security Score',
|
|
150
|
+
category: 'quality',
|
|
151
|
+
weight: 0.25,
|
|
152
|
+
icon: '🔒'
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
// Code Quality
|
|
156
|
+
testCoverage: {
|
|
157
|
+
label: 'Test Coverage',
|
|
158
|
+
category: 'quality',
|
|
159
|
+
weight: 0.2,
|
|
160
|
+
icon: '🧪'
|
|
161
|
+
},
|
|
162
|
+
codeComplexity: {
|
|
163
|
+
label: 'Code Complexity',
|
|
164
|
+
category: 'quality',
|
|
165
|
+
weight: 0.1,
|
|
166
|
+
invert: true, // Lower is better
|
|
167
|
+
icon: '🔄'
|
|
168
|
+
},
|
|
169
|
+
lintScore: {
|
|
170
|
+
label: 'Lint Score',
|
|
171
|
+
category: 'quality',
|
|
172
|
+
weight: 0.1,
|
|
173
|
+
icon: '✨'
|
|
174
|
+
},
|
|
175
|
+
typeScore: {
|
|
176
|
+
label: 'TypeScript Coverage',
|
|
177
|
+
category: 'quality',
|
|
178
|
+
weight: 0.1,
|
|
179
|
+
icon: '📘'
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
// Documentation
|
|
183
|
+
docsCoverage: {
|
|
184
|
+
label: 'Documentation',
|
|
185
|
+
category: 'docs',
|
|
186
|
+
weight: 0.15,
|
|
187
|
+
icon: '📚'
|
|
188
|
+
},
|
|
189
|
+
readmeScore: {
|
|
190
|
+
label: 'README Quality',
|
|
191
|
+
category: 'docs',
|
|
192
|
+
weight: 0.05,
|
|
193
|
+
icon: '📖'
|
|
194
|
+
},
|
|
195
|
+
apiDocs: {
|
|
196
|
+
label: 'API Documentation',
|
|
197
|
+
category: 'docs',
|
|
198
|
+
weight: 0.1,
|
|
199
|
+
icon: '🔌'
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
// Performance
|
|
203
|
+
bundleSize: {
|
|
204
|
+
label: 'Bundle Size',
|
|
205
|
+
category: 'performance',
|
|
206
|
+
weight: 0.1,
|
|
207
|
+
invert: true,
|
|
208
|
+
icon: '📦'
|
|
209
|
+
},
|
|
210
|
+
buildTime: {
|
|
211
|
+
label: 'Build Time',
|
|
212
|
+
category: 'performance',
|
|
213
|
+
weight: 0.05,
|
|
214
|
+
invert: true,
|
|
215
|
+
icon: '⏱️'
|
|
216
|
+
},
|
|
217
|
+
lighthouseScore: {
|
|
218
|
+
label: 'Lighthouse Score',
|
|
219
|
+
category: 'performance',
|
|
220
|
+
weight: 0.15,
|
|
221
|
+
icon: '🚀'
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
// Activity
|
|
225
|
+
commitFrequency: {
|
|
226
|
+
label: 'Commit Frequency',
|
|
227
|
+
category: 'activity',
|
|
228
|
+
weight: 0.1,
|
|
229
|
+
icon: '📊'
|
|
230
|
+
},
|
|
231
|
+
prVelocity: {
|
|
232
|
+
label: 'PR Velocity',
|
|
233
|
+
category: 'activity',
|
|
234
|
+
weight: 0.05,
|
|
235
|
+
icon: '🔀'
|
|
236
|
+
},
|
|
237
|
+
issueResolution: {
|
|
238
|
+
label: 'Issue Resolution',
|
|
239
|
+
category: 'activity',
|
|
240
|
+
weight: 0.05,
|
|
241
|
+
icon: '🎯'
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
// Todos & Tasks
|
|
245
|
+
todoCompletion: {
|
|
246
|
+
label: 'Todo Completion',
|
|
247
|
+
category: 'progress',
|
|
248
|
+
weight: 0.1,
|
|
249
|
+
icon: '☑️'
|
|
250
|
+
},
|
|
251
|
+
technicalDebt: {
|
|
252
|
+
label: 'Technical Debt',
|
|
253
|
+
category: 'quality',
|
|
254
|
+
weight: 0.1,
|
|
255
|
+
invert: true,
|
|
256
|
+
icon: '💳'
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// ============================================================================
|
|
261
|
+
// Utility Functions
|
|
262
|
+
// ============================================================================
|
|
263
|
+
|
|
264
|
+
function getFilesRecursive(dir: string, extensions?: string[], depth: number = 0): string[] {
|
|
265
|
+
if (depth > 5) return [];
|
|
266
|
+
const files: string[] = [];
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
270
|
+
|
|
271
|
+
for (const entry of entries) {
|
|
272
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
273
|
+
|
|
274
|
+
const fullPath = path.join(dir, entry.name);
|
|
275
|
+
|
|
276
|
+
if (entry.isDirectory()) {
|
|
277
|
+
files.push(...getFilesRecursive(fullPath, extensions, depth + 1));
|
|
278
|
+
} else if (entry.isFile()) {
|
|
279
|
+
const ext = path.extname(entry.name);
|
|
280
|
+
if (!extensions || extensions.includes(ext)) {
|
|
281
|
+
files.push(fullPath);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
} catch {
|
|
286
|
+
// Permission denied
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return files;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function countTodoComments(projectRoot: string): number {
|
|
293
|
+
const pattern = /\b(TODO|FIXME|HACK|XXX|BUG)\b/gi;
|
|
294
|
+
return countPattern(projectRoot, pattern);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function countPattern(projectRoot: string, pattern: RegExp, extensions: string[] = ['.js', '.ts', '.jsx', '.tsx']): number {
|
|
298
|
+
let count = 0;
|
|
299
|
+
const srcDirs = ['src', 'app', 'lib', 'components', 'pages', 'core', 'cli'];
|
|
300
|
+
|
|
301
|
+
for (const dir of srcDirs) {
|
|
302
|
+
const dirPath = path.join(projectRoot, dir);
|
|
303
|
+
if (!fs.existsSync(dirPath)) continue;
|
|
304
|
+
|
|
305
|
+
const files = getFilesRecursive(dirPath, extensions);
|
|
306
|
+
for (const file of files) {
|
|
307
|
+
try {
|
|
308
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
309
|
+
const matches = content.match(pattern);
|
|
310
|
+
if (matches) count += matches.length;
|
|
311
|
+
} catch {
|
|
312
|
+
// Skip
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return count;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ============================================================================
|
|
321
|
+
// Metric Collectors
|
|
322
|
+
// ============================================================================
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Collect test coverage metrics
|
|
326
|
+
*/
|
|
327
|
+
export async function collectTestCoverage(projectRoot: string): Promise<MetricResult> {
|
|
328
|
+
const result: MetricResult = { score: 0, data: null, source: null };
|
|
329
|
+
|
|
330
|
+
// Check for coverage reports
|
|
331
|
+
const coveragePaths = [
|
|
332
|
+
'coverage/coverage-summary.json',
|
|
333
|
+
'coverage/lcov-report/index.html',
|
|
334
|
+
'.nyc_output/coverage-summary.json'
|
|
335
|
+
];
|
|
336
|
+
|
|
337
|
+
for (const coveragePath of coveragePaths) {
|
|
338
|
+
const fullPath = path.join(projectRoot, coveragePath);
|
|
339
|
+
if (fs.existsSync(fullPath)) {
|
|
340
|
+
result.source = coveragePath;
|
|
341
|
+
|
|
342
|
+
if (coveragePath.endsWith('.json')) {
|
|
343
|
+
try {
|
|
344
|
+
const coverage = JSON.parse(fs.readFileSync(fullPath, 'utf-8')) as {
|
|
345
|
+
total?: {
|
|
346
|
+
lines?: { pct?: number };
|
|
347
|
+
statements?: { pct?: number };
|
|
348
|
+
functions?: { pct?: number };
|
|
349
|
+
branches?: { pct?: number };
|
|
350
|
+
}
|
|
351
|
+
};
|
|
352
|
+
if (coverage.total) {
|
|
353
|
+
const { lines, statements, functions, branches } = coverage.total;
|
|
354
|
+
const data: CoverageData = {
|
|
355
|
+
lines: lines?.pct || 0,
|
|
356
|
+
statements: statements?.pct || 0,
|
|
357
|
+
functions: functions?.pct || 0,
|
|
358
|
+
branches: branches?.pct || 0
|
|
359
|
+
};
|
|
360
|
+
result.data = data;
|
|
361
|
+
result.score = Math.round(
|
|
362
|
+
((data.lines ?? 0) + (data.statements ?? 0) +
|
|
363
|
+
(data.functions ?? 0) + (data.branches ?? 0)) / 4
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
} catch {
|
|
367
|
+
// Invalid JSON
|
|
368
|
+
}
|
|
369
|
+
} else if (coveragePath.includes('lcov')) {
|
|
370
|
+
// Parse coverage from HTML (basic extraction)
|
|
371
|
+
try {
|
|
372
|
+
const html = fs.readFileSync(fullPath, 'utf-8');
|
|
373
|
+
const match = html.match(/(\d+\.?\d*)%\s*(?:statements|lines)/i);
|
|
374
|
+
if (match && match[1]) {
|
|
375
|
+
result.score = Math.round(parseFloat(match[1]));
|
|
376
|
+
result.data = { lines: result.score };
|
|
377
|
+
}
|
|
378
|
+
} catch {
|
|
379
|
+
// Parse error
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return result;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Collect bundle size metrics
|
|
391
|
+
*/
|
|
392
|
+
export async function collectBundleSize(projectRoot: string): Promise<MetricResult> {
|
|
393
|
+
const result: MetricResult = { score: 100, data: null, source: null };
|
|
394
|
+
|
|
395
|
+
// Check for Next.js build output
|
|
396
|
+
const nextBuildManifest = path.join(projectRoot, '.next/build-manifest.json');
|
|
397
|
+
|
|
398
|
+
if (fs.existsSync(nextBuildManifest)) {
|
|
399
|
+
result.source = 'next-build';
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
// Get total size of pages
|
|
403
|
+
const pagesDir = path.join(projectRoot, '.next/static/chunks/pages');
|
|
404
|
+
if (fs.existsSync(pagesDir)) {
|
|
405
|
+
let totalSize = 0;
|
|
406
|
+
const files = fs.readdirSync(pagesDir);
|
|
407
|
+
for (const file of files) {
|
|
408
|
+
const stat = fs.statSync(path.join(pagesDir, file));
|
|
409
|
+
totalSize += stat.size;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const data: BundleSizeData = {
|
|
413
|
+
totalKB: Math.round(totalSize / 1024),
|
|
414
|
+
pageCount: files.length
|
|
415
|
+
};
|
|
416
|
+
result.data = data;
|
|
417
|
+
|
|
418
|
+
// Score based on bundle size (smaller is better)
|
|
419
|
+
// Target: < 200KB = 100, > 1MB = 0
|
|
420
|
+
const sizeKB = data.totalKB;
|
|
421
|
+
if (sizeKB < 200) result.score = 100;
|
|
422
|
+
else if (sizeKB < 500) result.score = 80;
|
|
423
|
+
else if (sizeKB < 1000) result.score = 60;
|
|
424
|
+
else if (sizeKB < 2000) result.score = 40;
|
|
425
|
+
else result.score = 20;
|
|
426
|
+
}
|
|
427
|
+
} catch {
|
|
428
|
+
// Build not available
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return result;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Collect documentation coverage
|
|
437
|
+
*/
|
|
438
|
+
export async function collectDocsCoverage(projectRoot: string): Promise<MetricResult> {
|
|
439
|
+
const data: DocsCoverageData = { files: 0, documented: 0, coverage: 0 };
|
|
440
|
+
const result: MetricResult = { score: 0, data, source: 'file-scan' };
|
|
441
|
+
|
|
442
|
+
// Check for documentation files
|
|
443
|
+
const docIndicators: Record<string, string[]> = {
|
|
444
|
+
readme: ['README.md', 'README.txt', 'readme.md'],
|
|
445
|
+
contributing: ['CONTRIBUTING.md', 'contributing.md'],
|
|
446
|
+
changelog: ['CHANGELOG.md', 'HISTORY.md'],
|
|
447
|
+
license: ['LICENSE', 'LICENSE.md'],
|
|
448
|
+
codeOfConduct: ['CODE_OF_CONDUCT.md'],
|
|
449
|
+
security: ['SECURITY.md'],
|
|
450
|
+
api: ['API.md', 'docs/api.md', 'docs/API.md']
|
|
451
|
+
};
|
|
452
|
+
|
|
453
|
+
let found = 0;
|
|
454
|
+
const total = Object.keys(docIndicators).length;
|
|
455
|
+
const details: Record<string, string> = {};
|
|
456
|
+
|
|
457
|
+
for (const [type, files] of Object.entries(docIndicators)) {
|
|
458
|
+
for (const file of files) {
|
|
459
|
+
if (fs.existsSync(path.join(projectRoot, file))) {
|
|
460
|
+
found++;
|
|
461
|
+
details[type] = file;
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Check for docs folder
|
|
468
|
+
const docsDir = path.join(projectRoot, 'docs');
|
|
469
|
+
if (fs.existsSync(docsDir) && fs.statSync(docsDir).isDirectory()) {
|
|
470
|
+
const docFiles = fs.readdirSync(docsDir).filter(f => f.endsWith('.md'));
|
|
471
|
+
data.docFiles = docFiles.length;
|
|
472
|
+
if (docFiles.length > 5) found += 0.5;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Check for planning folder
|
|
476
|
+
const planningDir = path.join(projectRoot, 'planning');
|
|
477
|
+
if (fs.existsSync(planningDir)) {
|
|
478
|
+
const planningFiles = fs.readdirSync(planningDir).filter(f => f.endsWith('.md'));
|
|
479
|
+
data.planningFiles = planningFiles.length;
|
|
480
|
+
if (planningFiles.length > 0) found += 0.5;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
data.files = total;
|
|
484
|
+
data.documented = found;
|
|
485
|
+
data.coverage = Math.round((found / total) * 100);
|
|
486
|
+
data.details = details;
|
|
487
|
+
result.score = data.coverage;
|
|
488
|
+
|
|
489
|
+
return result;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Collect code complexity metrics
|
|
494
|
+
*/
|
|
495
|
+
export async function collectCodeComplexity(projectRoot: string): Promise<MetricResult> {
|
|
496
|
+
const result: MetricResult = { score: 100, data: null, source: 'file-analysis' };
|
|
497
|
+
|
|
498
|
+
// Simple complexity analysis based on file structure
|
|
499
|
+
const srcDirs = ['src', 'app', 'lib', 'components', 'pages'];
|
|
500
|
+
let totalFiles = 0;
|
|
501
|
+
let totalLines = 0;
|
|
502
|
+
|
|
503
|
+
for (const dir of srcDirs) {
|
|
504
|
+
const dirPath = path.join(projectRoot, dir);
|
|
505
|
+
if (fs.existsSync(dirPath)) {
|
|
506
|
+
const files = getFilesRecursive(dirPath, ['.js', '.ts', '.jsx', '.tsx']);
|
|
507
|
+
totalFiles += files.length;
|
|
508
|
+
|
|
509
|
+
for (const file of files) {
|
|
510
|
+
try {
|
|
511
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
512
|
+
totalLines += content.split('\n').length;
|
|
513
|
+
} catch {
|
|
514
|
+
// Skip unreadable files
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (totalFiles > 0) {
|
|
521
|
+
const avgLinesPerFile = Math.round(totalLines / totalFiles);
|
|
522
|
+
|
|
523
|
+
const data: CodeComplexityData = {
|
|
524
|
+
totalFiles,
|
|
525
|
+
totalLines,
|
|
526
|
+
avgLinesPerFile,
|
|
527
|
+
// Estimate complexity based on average file size
|
|
528
|
+
complexity: avgLinesPerFile > 300 ? 'high' :
|
|
529
|
+
avgLinesPerFile > 150 ? 'medium' : 'low'
|
|
530
|
+
};
|
|
531
|
+
result.data = data;
|
|
532
|
+
|
|
533
|
+
// Score: smaller files = better
|
|
534
|
+
if (avgLinesPerFile < 100) result.score = 100;
|
|
535
|
+
else if (avgLinesPerFile < 200) result.score = 80;
|
|
536
|
+
else if (avgLinesPerFile < 300) result.score = 60;
|
|
537
|
+
else if (avgLinesPerFile < 500) result.score = 40;
|
|
538
|
+
else result.score = 20;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return result;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Collect todo completion metrics
|
|
546
|
+
*/
|
|
547
|
+
export async function collectTodoCompletion(projectRoot: string): Promise<MetricResult> {
|
|
548
|
+
const result: MetricResult = { score: 0, data: null, source: null };
|
|
549
|
+
|
|
550
|
+
// Check for bootspring todos
|
|
551
|
+
const todosFile = path.join(projectRoot, '.bootspring', 'todos.json');
|
|
552
|
+
if (fs.existsSync(todosFile)) {
|
|
553
|
+
try {
|
|
554
|
+
const todos = JSON.parse(fs.readFileSync(todosFile, 'utf-8')) as Array<{ done: boolean }>;
|
|
555
|
+
const total = todos.length;
|
|
556
|
+
const completed = todos.filter(t => t.done).length;
|
|
557
|
+
|
|
558
|
+
const data: TodoCompletionData = { total, completed, pending: total - completed };
|
|
559
|
+
result.data = data;
|
|
560
|
+
result.score = total > 0 ? Math.round((completed / total) * 100) : 100;
|
|
561
|
+
result.source = 'bootspring-todos';
|
|
562
|
+
} catch {
|
|
563
|
+
// Invalid JSON
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Also check for TODO comments in code
|
|
568
|
+
const todoCount = countTodoComments(projectRoot);
|
|
569
|
+
if (todoCount > 0) {
|
|
570
|
+
const data = (result.data || {}) as TodoCompletionData;
|
|
571
|
+
data.todoComments = todoCount;
|
|
572
|
+
result.data = data;
|
|
573
|
+
// Penalize for many TODO comments
|
|
574
|
+
result.score = Math.max(0, result.score - Math.min(todoCount * 2, 30));
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
return result;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Collect technical debt metrics
|
|
582
|
+
*/
|
|
583
|
+
export async function collectTechnicalDebt(projectRoot: string): Promise<MetricResult> {
|
|
584
|
+
const data: TechnicalDebtData = { indicators: [], totalIssues: 0 };
|
|
585
|
+
const result: MetricResult = { score: 100, data, source: 'code-analysis' };
|
|
586
|
+
|
|
587
|
+
const debtIndicators: DebtIndicator[] = [];
|
|
588
|
+
|
|
589
|
+
// Check for TODO/FIXME/HACK comments
|
|
590
|
+
const todoCount = countTodoComments(projectRoot);
|
|
591
|
+
if (todoCount > 10) {
|
|
592
|
+
debtIndicators.push({ type: 'todo-comments', count: todoCount, severity: 'low' });
|
|
593
|
+
result.score -= Math.min(todoCount, 20);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Check for @ts-ignore or @ts-expect-error
|
|
597
|
+
const tsIgnoreCount = countPattern(projectRoot, /@ts-(ignore|expect-error)/g);
|
|
598
|
+
if (tsIgnoreCount > 0) {
|
|
599
|
+
debtIndicators.push({ type: 'ts-ignores', count: tsIgnoreCount, severity: 'medium' });
|
|
600
|
+
result.score -= tsIgnoreCount * 3;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// Check for eslint-disable comments
|
|
604
|
+
const eslintDisableCount = countPattern(projectRoot, /eslint-disable/g);
|
|
605
|
+
if (eslintDisableCount > 5) {
|
|
606
|
+
debtIndicators.push({ type: 'eslint-disables', count: eslintDisableCount, severity: 'medium' });
|
|
607
|
+
result.score -= Math.min(eslintDisableCount * 2, 20);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Check for console.log statements (should be removed in production)
|
|
611
|
+
const consoleCount = countPattern(projectRoot, /console\.(log|debug|info)\(/g);
|
|
612
|
+
if (consoleCount > 10) {
|
|
613
|
+
debtIndicators.push({ type: 'console-logs', count: consoleCount, severity: 'low' });
|
|
614
|
+
result.score -= Math.min(consoleCount, 15);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Check for 'any' type usage in TypeScript
|
|
618
|
+
const anyCount = countPattern(projectRoot, /:\s*any\b/g, ['.ts', '.tsx']);
|
|
619
|
+
if (anyCount > 5) {
|
|
620
|
+
debtIndicators.push({ type: 'any-types', count: anyCount, severity: 'medium' });
|
|
621
|
+
result.score -= Math.min(anyCount * 2, 25);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
data.indicators = debtIndicators;
|
|
625
|
+
data.totalIssues = debtIndicators.reduce((sum, i) => sum + i.count, 0);
|
|
626
|
+
result.score = Math.max(0, result.score);
|
|
627
|
+
|
|
628
|
+
return result;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Collect commit frequency metrics
|
|
633
|
+
*/
|
|
634
|
+
export async function collectCommitFrequency(projectRoot: string): Promise<MetricResult> {
|
|
635
|
+
const result: MetricResult = { score: 0, data: null, source: 'git' };
|
|
636
|
+
|
|
637
|
+
try {
|
|
638
|
+
// Get commits from last 30 days
|
|
639
|
+
const output = execSync(
|
|
640
|
+
'git log --since="30 days ago" --oneline 2>/dev/null | wc -l',
|
|
641
|
+
{ cwd: projectRoot, encoding: 'utf-8' }
|
|
642
|
+
).trim();
|
|
643
|
+
|
|
644
|
+
const commits = parseInt(output, 10) || 0;
|
|
645
|
+
const data: CommitFrequencyData = {
|
|
646
|
+
last30Days: commits,
|
|
647
|
+
avgPerDay: Math.round(commits / 30 * 10) / 10
|
|
648
|
+
};
|
|
649
|
+
result.data = data;
|
|
650
|
+
|
|
651
|
+
// Score: active development = good
|
|
652
|
+
if (commits >= 60) result.score = 100; // 2+ per day
|
|
653
|
+
else if (commits >= 30) result.score = 80; // 1 per day
|
|
654
|
+
else if (commits >= 15) result.score = 60; // every 2 days
|
|
655
|
+
else if (commits >= 5) result.score = 40;
|
|
656
|
+
else result.score = 20;
|
|
657
|
+
} catch {
|
|
658
|
+
// Not a git repo
|
|
659
|
+
result.data = { error: 'Not a git repository' };
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return result;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Collect README quality score
|
|
667
|
+
*/
|
|
668
|
+
export async function collectReadmeScore(projectRoot: string): Promise<MetricResult> {
|
|
669
|
+
const result: MetricResult = { score: 0, data: null, source: null };
|
|
670
|
+
|
|
671
|
+
const readmePaths = ['README.md', 'readme.md', 'Readme.md'];
|
|
672
|
+
let readmePath: string | null = null;
|
|
673
|
+
|
|
674
|
+
for (const p of readmePaths) {
|
|
675
|
+
if (fs.existsSync(path.join(projectRoot, p))) {
|
|
676
|
+
readmePath = path.join(projectRoot, p);
|
|
677
|
+
break;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
if (!readmePath) {
|
|
682
|
+
return { score: 0, data: { exists: false }, source: null };
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
const content = fs.readFileSync(readmePath, 'utf-8');
|
|
686
|
+
const lines = content.split('\n').length;
|
|
687
|
+
const words = content.split(/\s+/).length;
|
|
688
|
+
|
|
689
|
+
// Check for common README sections
|
|
690
|
+
const sections: Record<string, RegExp> = {
|
|
691
|
+
installation: /#+\s*(install|getting started|setup)/i,
|
|
692
|
+
usage: /#+\s*(usage|how to use|examples)/i,
|
|
693
|
+
api: /#+\s*(api|reference|documentation)/i,
|
|
694
|
+
contributing: /#+\s*(contribut)/i,
|
|
695
|
+
license: /#+\s*(license)/i,
|
|
696
|
+
badges: /\[!\[/g, // Badge markdown
|
|
697
|
+
codeBlocks: /```/g
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
const found: Record<string, boolean> = {};
|
|
701
|
+
let sectionScore = 0;
|
|
702
|
+
|
|
703
|
+
for (const [section, pattern] of Object.entries(sections)) {
|
|
704
|
+
if (pattern.test(content)) {
|
|
705
|
+
found[section] = true;
|
|
706
|
+
sectionScore += 15;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Base score on length
|
|
711
|
+
let lengthScore = 0;
|
|
712
|
+
if (words > 500) lengthScore = 20;
|
|
713
|
+
else if (words > 200) lengthScore = 15;
|
|
714
|
+
else if (words > 100) lengthScore = 10;
|
|
715
|
+
else lengthScore = 5;
|
|
716
|
+
|
|
717
|
+
result.score = Math.min(100, sectionScore + lengthScore);
|
|
718
|
+
const data: ReadmeScoreData = {
|
|
719
|
+
exists: true,
|
|
720
|
+
lines,
|
|
721
|
+
words,
|
|
722
|
+
sections: found,
|
|
723
|
+
path: readmePath
|
|
724
|
+
};
|
|
725
|
+
result.data = data;
|
|
726
|
+
result.source = readmePath;
|
|
727
|
+
|
|
728
|
+
return result;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// ============================================================================
|
|
732
|
+
// Main Functions
|
|
733
|
+
// ============================================================================
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Collect all metrics for a project
|
|
737
|
+
*/
|
|
738
|
+
export async function collectAllMetrics(projectRoot: string, options: CollectOptions = {}): Promise<MetricsResults> {
|
|
739
|
+
const results: MetricsResults = {
|
|
740
|
+
timestamp: new Date().toISOString(),
|
|
741
|
+
metrics: {},
|
|
742
|
+
categories: {},
|
|
743
|
+
overallScore: 0
|
|
744
|
+
};
|
|
745
|
+
|
|
746
|
+
const collectors: Record<string, (root: string) => Promise<MetricResult>> = {
|
|
747
|
+
testCoverage: collectTestCoverage,
|
|
748
|
+
bundleSize: collectBundleSize,
|
|
749
|
+
docsCoverage: collectDocsCoverage,
|
|
750
|
+
codeComplexity: collectCodeComplexity,
|
|
751
|
+
todoCompletion: collectTodoCompletion,
|
|
752
|
+
technicalDebt: collectTechnicalDebt,
|
|
753
|
+
commitFrequency: collectCommitFrequency,
|
|
754
|
+
readmeScore: collectReadmeScore
|
|
755
|
+
};
|
|
756
|
+
|
|
757
|
+
// Run collectors
|
|
758
|
+
for (const [metricId, collector] of Object.entries(collectors)) {
|
|
759
|
+
if (options.skip && options.skip.includes(metricId)) continue;
|
|
760
|
+
|
|
761
|
+
try {
|
|
762
|
+
results.metrics[metricId] = await collector(projectRoot);
|
|
763
|
+
} catch (error) {
|
|
764
|
+
const err = error as Error;
|
|
765
|
+
results.metrics[metricId] = { score: 0, error: err.message };
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Add checkpoint progress from project state
|
|
770
|
+
const state = projectState.loadState(projectRoot);
|
|
771
|
+
if (state) {
|
|
772
|
+
const progress = projectState.getCheckpointProgress(projectRoot);
|
|
773
|
+
results.metrics['checkpoints'] = {
|
|
774
|
+
score: progress.percentage,
|
|
775
|
+
data: progress
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
// Add security if available
|
|
779
|
+
const securityState = state as { security?: { score?: number; summary?: unknown } };
|
|
780
|
+
if (securityState.security?.score !== undefined) {
|
|
781
|
+
results.metrics['security'] = {
|
|
782
|
+
score: securityState.security.score,
|
|
783
|
+
data: securityState.security.summary
|
|
784
|
+
};
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Calculate category scores
|
|
789
|
+
const categories: Record<string, CategoryData> = {};
|
|
790
|
+
for (const [metricId, metric] of Object.entries(results.metrics)) {
|
|
791
|
+
const definition = METRICS[metricId];
|
|
792
|
+
if (!definition) continue;
|
|
793
|
+
|
|
794
|
+
const category = definition.category;
|
|
795
|
+
if (!categories[category]) {
|
|
796
|
+
categories[category] = { total: 0, count: 0, metrics: [] };
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const categoryData = categories[category];
|
|
800
|
+
if (categoryData) {
|
|
801
|
+
categoryData.total += metric.score;
|
|
802
|
+
categoryData.count++;
|
|
803
|
+
categoryData.metrics.push({
|
|
804
|
+
id: metricId,
|
|
805
|
+
label: definition.label,
|
|
806
|
+
score: metric.score,
|
|
807
|
+
icon: definition.icon
|
|
808
|
+
});
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Calculate category averages
|
|
813
|
+
for (const [, data] of Object.entries(categories)) {
|
|
814
|
+
data.average = data.count > 0 ? Math.round(data.total / data.count) : 0;
|
|
815
|
+
}
|
|
816
|
+
results.categories = categories;
|
|
817
|
+
|
|
818
|
+
// Calculate overall score (weighted average)
|
|
819
|
+
let totalWeight = 0;
|
|
820
|
+
let weightedScore = 0;
|
|
821
|
+
|
|
822
|
+
for (const [metricId, metric] of Object.entries(results.metrics)) {
|
|
823
|
+
const definition = METRICS[metricId];
|
|
824
|
+
if (!definition) continue;
|
|
825
|
+
|
|
826
|
+
totalWeight += definition.weight;
|
|
827
|
+
weightedScore += metric.score * definition.weight;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
results.overallScore = totalWeight > 0
|
|
831
|
+
? Math.round(weightedScore / totalWeight)
|
|
832
|
+
: 0;
|
|
833
|
+
|
|
834
|
+
return results;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Store metrics in project state
|
|
839
|
+
*/
|
|
840
|
+
export function storeMetrics(projectRoot: string, metrics: MetricsResults): StoredMetrics {
|
|
841
|
+
const state = projectState.getOrCreateState(projectRoot);
|
|
842
|
+
|
|
843
|
+
const storedMetrics: StoredMetrics = {
|
|
844
|
+
lastCollected: metrics.timestamp,
|
|
845
|
+
overallScore: metrics.overallScore,
|
|
846
|
+
categories: Object.fromEntries(
|
|
847
|
+
Object.entries(metrics.categories).map(([cat, data]) => [
|
|
848
|
+
cat,
|
|
849
|
+
{ score: data.average ?? 0, count: data.count }
|
|
850
|
+
])
|
|
851
|
+
),
|
|
852
|
+
scores: Object.fromEntries(
|
|
853
|
+
Object.entries(metrics.metrics).map(([id, m]) => [id, m.score])
|
|
854
|
+
),
|
|
855
|
+
history: [
|
|
856
|
+
{ score: metrics.overallScore, date: metrics.timestamp },
|
|
857
|
+
...((state as { metrics?: { history?: Array<{ score: number; date: string }> } }).metrics?.history || []).slice(0, 29) // Keep last 30
|
|
858
|
+
]
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
// Update state with metrics
|
|
862
|
+
(state as { metrics?: StoredMetrics }).metrics = storedMetrics;
|
|
863
|
+
projectState.saveState(projectRoot, state);
|
|
864
|
+
|
|
865
|
+
return storedMetrics;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* Get metric trend
|
|
870
|
+
*/
|
|
871
|
+
export function getMetricTrend(projectRoot: string, _metricId: string): TrendDirection | null {
|
|
872
|
+
const state = projectState.loadState(projectRoot);
|
|
873
|
+
const stateWithMetrics = state as { metrics?: { history?: Array<{ score: number; date: string }> } } | null;
|
|
874
|
+
if (!stateWithMetrics?.metrics?.history) return null;
|
|
875
|
+
|
|
876
|
+
const history = stateWithMetrics.metrics.history;
|
|
877
|
+
if (history.length < 2) return 'stable';
|
|
878
|
+
|
|
879
|
+
const current = history[0]?.score ?? 0;
|
|
880
|
+
const previous = history[1]?.score ?? 0;
|
|
881
|
+
const diff = current - previous;
|
|
882
|
+
|
|
883
|
+
if (diff > 5) return 'up';
|
|
884
|
+
if (diff < -5) return 'down';
|
|
885
|
+
return 'stable';
|
|
886
|
+
}
|