@girardmedia/bootspring 1.2.0 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +107 -14
- package/bin/bootspring.js +166 -27
- package/cli/agent.js +189 -17
- package/cli/analyze.js +499 -0
- package/cli/audit.js +557 -0
- package/cli/auth.js +495 -38
- package/cli/billing.js +302 -0
- package/cli/build.js +695 -0
- package/cli/business.js +109 -26
- package/cli/checkpoint-utils.js +168 -0
- package/cli/checkpoint.js +639 -0
- package/cli/cloud-sync.js +447 -0
- package/cli/content.js +198 -0
- package/cli/context.js +1 -1
- package/cli/deploy.js +543 -0
- package/cli/fundraise.js +112 -50
- package/cli/github-cmd.js +435 -0
- package/cli/health.js +477 -0
- package/cli/init.js +84 -13
- package/cli/legal.js +107 -95
- package/cli/log.js +2 -2
- package/cli/loop.js +976 -73
- package/cli/manager.js +711 -0
- package/cli/metrics.js +480 -0
- package/cli/monitor.js +812 -0
- package/cli/onboard.js +521 -0
- package/cli/orchestrator.js +12 -24
- package/cli/prd.js +594 -0
- package/cli/preseed-start.js +1483 -0
- package/cli/preseed.js +2302 -0
- package/cli/project.js +436 -0
- package/cli/quality.js +233 -0
- package/cli/security.js +913 -0
- package/cli/seed.js +1441 -5
- package/cli/skill.js +273 -211
- package/cli/suggest.js +989 -0
- package/cli/switch.js +453 -0
- package/cli/visualize.js +527 -0
- package/cli/watch.js +769 -0
- package/cli/workspace.js +607 -0
- package/core/analyze-workflow.js +1134 -0
- package/core/api-client.js +535 -22
- package/core/audit-workflow.js +1350 -0
- package/core/build-orchestrator.js +480 -0
- package/core/build-state.js +577 -0
- package/core/checkpoint-engine.js +408 -0
- package/core/config.js +1109 -26
- package/core/context-loader.js +21 -1
- package/core/deploy-workflow.js +836 -0
- package/core/entitlements.js +93 -22
- package/core/github-sync.js +610 -0
- package/core/index.js +8 -1
- package/core/ingest.js +1111 -0
- package/core/metrics-engine.js +768 -0
- package/core/onboard-workflow.js +1007 -0
- package/core/preseed-workflow.js +934 -0
- package/core/preseed.js +1617 -0
- package/core/project-context.js +325 -0
- package/core/project-state.js +694 -0
- package/core/r2-sync.js +583 -0
- package/core/scaffold.js +525 -7
- package/core/session.js +258 -0
- package/core/task-extractor.js +758 -0
- package/core/telemetry.js +28 -6
- package/core/tier-enforcement.js +737 -0
- package/core/utils.js +38 -14
- package/generators/questionnaire.js +15 -12
- package/generators/sections/ai.js +7 -7
- package/generators/sections/content.js +300 -0
- package/generators/sections/index.js +3 -0
- package/generators/sections/plugins.js +7 -6
- package/generators/templates/build-planning.template.js +596 -0
- package/generators/templates/content.template.js +819 -0
- package/generators/templates/index.js +2 -1
- package/hooks/git-autopilot.js +1250 -0
- package/hooks/index.js +9 -0
- package/intelligence/agent-collab.js +2057 -0
- package/intelligence/auto-suggest.js +634 -0
- package/intelligence/content-gen.js +1589 -0
- package/intelligence/cross-project.js +1647 -0
- package/intelligence/index.js +184 -0
- package/intelligence/learning/insights.json +517 -7
- package/intelligence/learning/pattern-learner.js +1008 -14
- package/intelligence/memory/decision-tracker.js +1431 -31
- package/intelligence/memory/decisions.jsonl +0 -0
- package/intelligence/orchestrator.js +2896 -1
- package/intelligence/prd.js +92 -1
- package/intelligence/recommendation-weights.json +14 -2
- package/intelligence/recommendations.js +463 -9
- package/intelligence/workflow-composer.js +1451 -0
- package/marketplace/index.d.ts +324 -0
- package/marketplace/index.js +1921 -0
- package/mcp/contracts/mcp-contract.v1.json +342 -4
- package/mcp/registry.js +680 -3
- package/mcp/response-formatter.js +23 -0
- package/mcp/tools/assist-tool.js +78 -4
- package/mcp/tools/autopilot-tool.js +408 -0
- package/mcp/tools/content-tool.js +571 -0
- package/mcp/tools/dashboard-tool.js +251 -5
- package/mcp/tools/mvp-tool.js +344 -0
- package/mcp/tools/plugin-tool.js +23 -1
- package/mcp/tools/prd-tool.js +579 -0
- package/mcp/tools/seed-tool.js +447 -0
- package/mcp/tools/skill-tool.js +43 -14
- package/mcp/tools/suggest-tool.js +147 -0
- package/package.json +15 -6
- package/agents/README.md +0 -93
- package/agents/ai-integration-expert/context.md +0 -386
- package/agents/api-expert/context.md +0 -416
- package/agents/architecture-expert/context.md +0 -454
- package/agents/auth-expert/context.md +0 -399
- package/agents/backend-expert/context.md +0 -483
- package/agents/business-strategy-expert/context.md +0 -180
- package/agents/code-review-expert/context.md +0 -365
- package/agents/competitive-analysis-expert/context.md +0 -239
- package/agents/data-modeling-expert/context.md +0 -352
- package/agents/database-expert/context.md +0 -250
- package/agents/devops-expert/context.md +0 -446
- package/agents/email-expert/context.md +0 -379
- package/agents/financial-expert/context.md +0 -213
- package/agents/frontend-expert/context.md +0 -364
- package/agents/fundraising-expert/context.md +0 -257
- package/agents/growth-expert/context.md +0 -249
- package/agents/index.js +0 -140
- package/agents/investor-relations-expert/context.md +0 -266
- package/agents/legal-expert/context.md +0 -284
- package/agents/marketing-expert/context.md +0 -236
- package/agents/monitoring-expert/context.md +0 -362
- package/agents/operations-expert/context.md +0 -279
- package/agents/partnerships-expert/context.md +0 -286
- package/agents/payment-expert/context.md +0 -340
- package/agents/performance-expert/context.md +0 -377
- package/agents/private-equity-expert/context.md +0 -246
- package/agents/railway-expert/context.md +0 -284
- package/agents/research-expert/context.md +0 -245
- package/agents/sales-expert/context.md +0 -241
- package/agents/security-expert/context.md +0 -343
- package/agents/testing-expert/context.md +0 -414
- package/agents/ui-ux-expert/context.md +0 -448
- package/agents/vercel-expert/context.md +0 -426
- package/skills/index.js +0 -787
- package/skills/patterns/README.md +0 -163
- package/skills/patterns/ai/agents.md +0 -281
- package/skills/patterns/ai/claude.md +0 -138
- package/skills/patterns/ai/embeddings.md +0 -150
- package/skills/patterns/ai/rag.md +0 -266
- package/skills/patterns/ai/streaming.md +0 -170
- package/skills/patterns/ai/structured-output.md +0 -162
- package/skills/patterns/ai/tools.md +0 -154
- package/skills/patterns/analytics/tracking.md +0 -220
- package/skills/patterns/api/errors.md +0 -296
- package/skills/patterns/api/graphql.md +0 -440
- package/skills/patterns/api/middleware.md +0 -279
- package/skills/patterns/api/openapi.md +0 -285
- package/skills/patterns/api/rate-limiting.md +0 -231
- package/skills/patterns/api/route-handler.md +0 -217
- package/skills/patterns/api/server-action.md +0 -249
- package/skills/patterns/api/versioning.md +0 -443
- package/skills/patterns/api/webhooks.md +0 -247
- package/skills/patterns/auth/clerk.md +0 -132
- package/skills/patterns/auth/mfa.md +0 -313
- package/skills/patterns/auth/nextauth.md +0 -140
- package/skills/patterns/auth/oauth.md +0 -237
- package/skills/patterns/auth/rbac.md +0 -152
- package/skills/patterns/auth/session-management.md +0 -367
- package/skills/patterns/auth/session.md +0 -120
- package/skills/patterns/database/audit.md +0 -177
- package/skills/patterns/database/migrations.md +0 -177
- package/skills/patterns/database/pagination.md +0 -230
- package/skills/patterns/database/pooling.md +0 -357
- package/skills/patterns/database/prisma.md +0 -180
- package/skills/patterns/database/relations.md +0 -187
- package/skills/patterns/database/seeding.md +0 -246
- package/skills/patterns/database/soft-delete.md +0 -153
- package/skills/patterns/database/transactions.md +0 -162
- package/skills/patterns/deployment/ci-cd.md +0 -231
- package/skills/patterns/deployment/docker.md +0 -188
- package/skills/patterns/deployment/monitoring.md +0 -387
- package/skills/patterns/deployment/vercel.md +0 -160
- package/skills/patterns/email/resend.md +0 -143
- package/skills/patterns/email/templates.md +0 -245
- package/skills/patterns/email/transactional.md +0 -503
- package/skills/patterns/email/verification.md +0 -176
- package/skills/patterns/files/download.md +0 -243
- package/skills/patterns/files/upload.md +0 -239
- package/skills/patterns/i18n/nextintl.md +0 -188
- package/skills/patterns/logging/structured.md +0 -292
- package/skills/patterns/notifications/email-queue.md +0 -248
- package/skills/patterns/notifications/push.md +0 -279
- package/skills/patterns/payments/checkout.md +0 -303
- package/skills/patterns/payments/invoices.md +0 -287
- package/skills/patterns/payments/portal.md +0 -245
- package/skills/patterns/payments/stripe.md +0 -272
- package/skills/patterns/payments/subscriptions.md +0 -300
- package/skills/patterns/payments/usage.md +0 -279
- package/skills/patterns/performance/caching.md +0 -276
- package/skills/patterns/performance/code-splitting.md +0 -233
- package/skills/patterns/performance/edge.md +0 -254
- package/skills/patterns/performance/isr.md +0 -266
- package/skills/patterns/performance/lazy-loading.md +0 -281
- package/skills/patterns/realtime/sse.md +0 -327
- package/skills/patterns/realtime/websockets.md +0 -336
- package/skills/patterns/search/filtering.md +0 -329
- package/skills/patterns/search/fulltext.md +0 -260
- package/skills/patterns/security/audit-logging.md +0 -444
- package/skills/patterns/security/csrf.md +0 -234
- package/skills/patterns/security/headers.md +0 -252
- package/skills/patterns/security/sanitization.md +0 -258
- package/skills/patterns/security/secrets.md +0 -261
- package/skills/patterns/security/validation.md +0 -268
- package/skills/patterns/security/xss.md +0 -229
- package/skills/patterns/seo/metadata.md +0 -252
- package/skills/patterns/state/context.md +0 -349
- package/skills/patterns/state/react-query.md +0 -313
- package/skills/patterns/state/url-state.md +0 -482
- package/skills/patterns/state/zustand.md +0 -262
- package/skills/patterns/testing/api.md +0 -259
- package/skills/patterns/testing/component.md +0 -233
- package/skills/patterns/testing/coverage.md +0 -207
- package/skills/patterns/testing/fixtures.md +0 -225
- package/skills/patterns/testing/integration.md +0 -436
- package/skills/patterns/testing/mocking.md +0 -177
- package/skills/patterns/testing/playwright.md +0 -162
- package/skills/patterns/testing/snapshot.md +0 -175
- package/skills/patterns/testing/vitest.md +0 -307
- package/skills/patterns/ui/accordions.md +0 -395
- package/skills/patterns/ui/cards.md +0 -299
- package/skills/patterns/ui/dropdowns.md +0 -476
- package/skills/patterns/ui/empty-states.md +0 -320
- package/skills/patterns/ui/forms.md +0 -405
- package/skills/patterns/ui/inputs.md +0 -319
- package/skills/patterns/ui/layouts.md +0 -282
- package/skills/patterns/ui/loading.md +0 -291
- package/skills/patterns/ui/modals.md +0 -338
- package/skills/patterns/ui/navigation.md +0 -374
- package/skills/patterns/ui/tables.md +0 -407
- package/skills/patterns/ui/toasts.md +0 -300
- package/skills/patterns/ui/tooltips.md +0 -396
- package/skills/patterns/utils/dates.md +0 -435
- package/skills/patterns/utils/errors.md +0 -451
- package/skills/patterns/utils/formatting.md +0 -345
- package/skills/patterns/utils/validation.md +0 -434
- package/templates/bootspring.config.js +0 -83
- package/templates/business/business-model-canvas.md +0 -246
- package/templates/business/business-plan.md +0 -266
- package/templates/business/competitive-analysis.md +0 -312
- package/templates/fundraising/data-room-checklist.md +0 -300
- package/templates/fundraising/investor-research.md +0 -243
- package/templates/fundraising/pitch-deck-outline.md +0 -253
- package/templates/legal/gdpr-checklist.md +0 -339
- package/templates/legal/privacy-policy.md +0 -285
- package/templates/legal/terms-of-service.md +0 -222
- package/templates/mcp.json +0 -9
|
@@ -0,0 +1,768 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bootspring Metrics Engine
|
|
3
|
+
* Comprehensive project metrics tracking and scoring
|
|
4
|
+
*
|
|
5
|
+
* @package bootspring
|
|
6
|
+
* @module core/metrics-engine
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { execSync } = require('child_process');
|
|
12
|
+
const projectState = require('./project-state');
|
|
13
|
+
|
|
14
|
+
// ============================================================================
|
|
15
|
+
// Metric Definitions
|
|
16
|
+
// ============================================================================
|
|
17
|
+
|
|
18
|
+
const METRICS = {
|
|
19
|
+
// Health & Progress
|
|
20
|
+
health: {
|
|
21
|
+
label: 'Health Score',
|
|
22
|
+
category: 'progress',
|
|
23
|
+
weight: 1.0,
|
|
24
|
+
icon: '💚'
|
|
25
|
+
},
|
|
26
|
+
checkpoints: {
|
|
27
|
+
label: 'Checkpoint Progress',
|
|
28
|
+
category: 'progress',
|
|
29
|
+
weight: 0.2,
|
|
30
|
+
icon: '✅'
|
|
31
|
+
},
|
|
32
|
+
security: {
|
|
33
|
+
label: 'Security Score',
|
|
34
|
+
category: 'quality',
|
|
35
|
+
weight: 0.25,
|
|
36
|
+
icon: '🔒'
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
// Code Quality
|
|
40
|
+
testCoverage: {
|
|
41
|
+
label: 'Test Coverage',
|
|
42
|
+
category: 'quality',
|
|
43
|
+
weight: 0.2,
|
|
44
|
+
icon: '🧪'
|
|
45
|
+
},
|
|
46
|
+
codeComplexity: {
|
|
47
|
+
label: 'Code Complexity',
|
|
48
|
+
category: 'quality',
|
|
49
|
+
weight: 0.1,
|
|
50
|
+
invert: true, // Lower is better
|
|
51
|
+
icon: '🔄'
|
|
52
|
+
},
|
|
53
|
+
lintScore: {
|
|
54
|
+
label: 'Lint Score',
|
|
55
|
+
category: 'quality',
|
|
56
|
+
weight: 0.1,
|
|
57
|
+
icon: '✨'
|
|
58
|
+
},
|
|
59
|
+
typeScore: {
|
|
60
|
+
label: 'TypeScript Coverage',
|
|
61
|
+
category: 'quality',
|
|
62
|
+
weight: 0.1,
|
|
63
|
+
icon: '📘'
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
// Documentation
|
|
67
|
+
docsCoverage: {
|
|
68
|
+
label: 'Documentation',
|
|
69
|
+
category: 'docs',
|
|
70
|
+
weight: 0.15,
|
|
71
|
+
icon: '📚'
|
|
72
|
+
},
|
|
73
|
+
readmeScore: {
|
|
74
|
+
label: 'README Quality',
|
|
75
|
+
category: 'docs',
|
|
76
|
+
weight: 0.05,
|
|
77
|
+
icon: '📖'
|
|
78
|
+
},
|
|
79
|
+
apiDocs: {
|
|
80
|
+
label: 'API Documentation',
|
|
81
|
+
category: 'docs',
|
|
82
|
+
weight: 0.1,
|
|
83
|
+
icon: '🔌'
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
// Performance
|
|
87
|
+
bundleSize: {
|
|
88
|
+
label: 'Bundle Size',
|
|
89
|
+
category: 'performance',
|
|
90
|
+
weight: 0.1,
|
|
91
|
+
invert: true,
|
|
92
|
+
icon: '📦'
|
|
93
|
+
},
|
|
94
|
+
buildTime: {
|
|
95
|
+
label: 'Build Time',
|
|
96
|
+
category: 'performance',
|
|
97
|
+
weight: 0.05,
|
|
98
|
+
invert: true,
|
|
99
|
+
icon: '⏱️'
|
|
100
|
+
},
|
|
101
|
+
lighthouseScore: {
|
|
102
|
+
label: 'Lighthouse Score',
|
|
103
|
+
category: 'performance',
|
|
104
|
+
weight: 0.15,
|
|
105
|
+
icon: '🚀'
|
|
106
|
+
},
|
|
107
|
+
|
|
108
|
+
// Activity
|
|
109
|
+
commitFrequency: {
|
|
110
|
+
label: 'Commit Frequency',
|
|
111
|
+
category: 'activity',
|
|
112
|
+
weight: 0.1,
|
|
113
|
+
icon: '📊'
|
|
114
|
+
},
|
|
115
|
+
prVelocity: {
|
|
116
|
+
label: 'PR Velocity',
|
|
117
|
+
category: 'activity',
|
|
118
|
+
weight: 0.05,
|
|
119
|
+
icon: '🔀'
|
|
120
|
+
},
|
|
121
|
+
issueResolution: {
|
|
122
|
+
label: 'Issue Resolution',
|
|
123
|
+
category: 'activity',
|
|
124
|
+
weight: 0.05,
|
|
125
|
+
icon: '🎯'
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
// Todos & Tasks
|
|
129
|
+
todoCompletion: {
|
|
130
|
+
label: 'Todo Completion',
|
|
131
|
+
category: 'progress',
|
|
132
|
+
weight: 0.1,
|
|
133
|
+
icon: '☑️'
|
|
134
|
+
},
|
|
135
|
+
technicalDebt: {
|
|
136
|
+
label: 'Technical Debt',
|
|
137
|
+
category: 'quality',
|
|
138
|
+
weight: 0.1,
|
|
139
|
+
invert: true,
|
|
140
|
+
icon: '💳'
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
// ============================================================================
|
|
145
|
+
// Metric Collectors
|
|
146
|
+
// ============================================================================
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Collect test coverage metrics
|
|
150
|
+
*/
|
|
151
|
+
async function collectTestCoverage(projectRoot) {
|
|
152
|
+
const result = { score: 0, data: null, source: null };
|
|
153
|
+
|
|
154
|
+
// Check for coverage reports
|
|
155
|
+
const coveragePaths = [
|
|
156
|
+
'coverage/coverage-summary.json',
|
|
157
|
+
'coverage/lcov-report/index.html',
|
|
158
|
+
'.nyc_output/coverage-summary.json'
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
for (const coveragePath of coveragePaths) {
|
|
162
|
+
const fullPath = path.join(projectRoot, coveragePath);
|
|
163
|
+
if (fs.existsSync(fullPath)) {
|
|
164
|
+
result.source = coveragePath;
|
|
165
|
+
|
|
166
|
+
if (coveragePath.endsWith('.json')) {
|
|
167
|
+
try {
|
|
168
|
+
const coverage = JSON.parse(fs.readFileSync(fullPath, 'utf-8'));
|
|
169
|
+
if (coverage.total) {
|
|
170
|
+
const { lines, statements, functions, branches } = coverage.total;
|
|
171
|
+
result.data = {
|
|
172
|
+
lines: lines?.pct || 0,
|
|
173
|
+
statements: statements?.pct || 0,
|
|
174
|
+
functions: functions?.pct || 0,
|
|
175
|
+
branches: branches?.pct || 0
|
|
176
|
+
};
|
|
177
|
+
result.score = Math.round(
|
|
178
|
+
(result.data.lines + result.data.statements +
|
|
179
|
+
result.data.functions + result.data.branches) / 4
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
// Invalid JSON
|
|
184
|
+
}
|
|
185
|
+
} else if (coveragePath.includes('lcov')) {
|
|
186
|
+
// Parse coverage from HTML (basic extraction)
|
|
187
|
+
try {
|
|
188
|
+
const html = fs.readFileSync(fullPath, 'utf-8');
|
|
189
|
+
const match = html.match(/(\d+\.?\d*)%\s*(?:statements|lines)/i);
|
|
190
|
+
if (match) {
|
|
191
|
+
result.score = Math.round(parseFloat(match[1]));
|
|
192
|
+
result.data = { lines: result.score };
|
|
193
|
+
}
|
|
194
|
+
} catch {
|
|
195
|
+
// Parse error
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return result;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Collect bundle size metrics
|
|
207
|
+
*/
|
|
208
|
+
async function collectBundleSize(projectRoot) {
|
|
209
|
+
const result = { score: 100, data: null, source: null };
|
|
210
|
+
|
|
211
|
+
// Check for Next.js build output
|
|
212
|
+
const nextBuildManifest = path.join(projectRoot, '.next/build-manifest.json');
|
|
213
|
+
const bundleAnalyzer = path.join(projectRoot, '.next/analyze/client.html');
|
|
214
|
+
|
|
215
|
+
if (fs.existsSync(nextBuildManifest)) {
|
|
216
|
+
result.source = 'next-build';
|
|
217
|
+
|
|
218
|
+
try {
|
|
219
|
+
// Get total size of pages
|
|
220
|
+
const pagesDir = path.join(projectRoot, '.next/static/chunks/pages');
|
|
221
|
+
if (fs.existsSync(pagesDir)) {
|
|
222
|
+
let totalSize = 0;
|
|
223
|
+
const files = fs.readdirSync(pagesDir);
|
|
224
|
+
for (const file of files) {
|
|
225
|
+
const stat = fs.statSync(path.join(pagesDir, file));
|
|
226
|
+
totalSize += stat.size;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
result.data = {
|
|
230
|
+
totalKB: Math.round(totalSize / 1024),
|
|
231
|
+
pageCount: files.length
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
// Score based on bundle size (smaller is better)
|
|
235
|
+
// Target: < 200KB = 100, > 1MB = 0
|
|
236
|
+
const sizeKB = result.data.totalKB;
|
|
237
|
+
if (sizeKB < 200) result.score = 100;
|
|
238
|
+
else if (sizeKB < 500) result.score = 80;
|
|
239
|
+
else if (sizeKB < 1000) result.score = 60;
|
|
240
|
+
else if (sizeKB < 2000) result.score = 40;
|
|
241
|
+
else result.score = 20;
|
|
242
|
+
}
|
|
243
|
+
} catch {
|
|
244
|
+
// Build not available
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Collect documentation coverage
|
|
253
|
+
*/
|
|
254
|
+
async function collectDocsCoverage(projectRoot) {
|
|
255
|
+
const result = { score: 0, data: { files: 0, documented: 0, coverage: 0 }, source: 'file-scan' };
|
|
256
|
+
|
|
257
|
+
// Check for documentation files
|
|
258
|
+
const docIndicators = {
|
|
259
|
+
readme: ['README.md', 'README.txt', 'readme.md'],
|
|
260
|
+
contributing: ['CONTRIBUTING.md', 'contributing.md'],
|
|
261
|
+
changelog: ['CHANGELOG.md', 'HISTORY.md'],
|
|
262
|
+
license: ['LICENSE', 'LICENSE.md'],
|
|
263
|
+
codeOfConduct: ['CODE_OF_CONDUCT.md'],
|
|
264
|
+
security: ['SECURITY.md'],
|
|
265
|
+
api: ['API.md', 'docs/api.md', 'docs/API.md']
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
let found = 0;
|
|
269
|
+
const total = Object.keys(docIndicators).length;
|
|
270
|
+
const details = {};
|
|
271
|
+
|
|
272
|
+
for (const [type, files] of Object.entries(docIndicators)) {
|
|
273
|
+
for (const file of files) {
|
|
274
|
+
if (fs.existsSync(path.join(projectRoot, file))) {
|
|
275
|
+
found++;
|
|
276
|
+
details[type] = file;
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Check for docs folder
|
|
283
|
+
const docsDir = path.join(projectRoot, 'docs');
|
|
284
|
+
if (fs.existsSync(docsDir) && fs.statSync(docsDir).isDirectory()) {
|
|
285
|
+
const docFiles = fs.readdirSync(docsDir).filter(f => f.endsWith('.md'));
|
|
286
|
+
result.data.docFiles = docFiles.length;
|
|
287
|
+
if (docFiles.length > 5) found += 0.5;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Check for JSDoc/TSDoc comments in code
|
|
291
|
+
const planningDir = path.join(projectRoot, 'planning');
|
|
292
|
+
if (fs.existsSync(planningDir)) {
|
|
293
|
+
const planningFiles = fs.readdirSync(planningDir).filter(f => f.endsWith('.md'));
|
|
294
|
+
result.data.planningFiles = planningFiles.length;
|
|
295
|
+
if (planningFiles.length > 0) found += 0.5;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
result.data.files = total;
|
|
299
|
+
result.data.documented = found;
|
|
300
|
+
result.data.coverage = Math.round((found / total) * 100);
|
|
301
|
+
result.data.details = details;
|
|
302
|
+
result.score = result.data.coverage;
|
|
303
|
+
|
|
304
|
+
return result;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Collect code complexity metrics
|
|
309
|
+
*/
|
|
310
|
+
async function collectCodeComplexity(projectRoot) {
|
|
311
|
+
const result = { score: 100, data: null, source: 'file-analysis' };
|
|
312
|
+
|
|
313
|
+
// Simple complexity analysis based on file structure
|
|
314
|
+
const srcDirs = ['src', 'app', 'lib', 'components', 'pages'];
|
|
315
|
+
let totalFiles = 0;
|
|
316
|
+
let totalLines = 0;
|
|
317
|
+
let avgLinesPerFile = 0;
|
|
318
|
+
|
|
319
|
+
for (const dir of srcDirs) {
|
|
320
|
+
const dirPath = path.join(projectRoot, dir);
|
|
321
|
+
if (fs.existsSync(dirPath)) {
|
|
322
|
+
const files = getFilesRecursive(dirPath, ['.js', '.ts', '.jsx', '.tsx']);
|
|
323
|
+
totalFiles += files.length;
|
|
324
|
+
|
|
325
|
+
for (const file of files) {
|
|
326
|
+
try {
|
|
327
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
328
|
+
totalLines += content.split('\n').length;
|
|
329
|
+
} catch {
|
|
330
|
+
// Skip unreadable files
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (totalFiles > 0) {
|
|
337
|
+
avgLinesPerFile = Math.round(totalLines / totalFiles);
|
|
338
|
+
|
|
339
|
+
result.data = {
|
|
340
|
+
totalFiles,
|
|
341
|
+
totalLines,
|
|
342
|
+
avgLinesPerFile,
|
|
343
|
+
// Estimate complexity based on average file size
|
|
344
|
+
complexity: avgLinesPerFile > 300 ? 'high' :
|
|
345
|
+
avgLinesPerFile > 150 ? 'medium' : 'low'
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
// Score: smaller files = better
|
|
349
|
+
if (avgLinesPerFile < 100) result.score = 100;
|
|
350
|
+
else if (avgLinesPerFile < 200) result.score = 80;
|
|
351
|
+
else if (avgLinesPerFile < 300) result.score = 60;
|
|
352
|
+
else if (avgLinesPerFile < 500) result.score = 40;
|
|
353
|
+
else result.score = 20;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Collect todo completion metrics
|
|
361
|
+
*/
|
|
362
|
+
async function collectTodoCompletion(projectRoot) {
|
|
363
|
+
const result = { score: 0, data: null, source: null };
|
|
364
|
+
|
|
365
|
+
// Check for bootspring todos
|
|
366
|
+
const todosFile = path.join(projectRoot, '.bootspring', 'todos.json');
|
|
367
|
+
if (fs.existsSync(todosFile)) {
|
|
368
|
+
try {
|
|
369
|
+
const todos = JSON.parse(fs.readFileSync(todosFile, 'utf-8'));
|
|
370
|
+
const total = todos.length;
|
|
371
|
+
const completed = todos.filter(t => t.done).length;
|
|
372
|
+
|
|
373
|
+
result.data = { total, completed, pending: total - completed };
|
|
374
|
+
result.score = total > 0 ? Math.round((completed / total) * 100) : 100;
|
|
375
|
+
result.source = 'bootspring-todos';
|
|
376
|
+
} catch {
|
|
377
|
+
// Invalid JSON
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Also check for TODO comments in code
|
|
382
|
+
const todoCount = countTodoComments(projectRoot);
|
|
383
|
+
if (todoCount > 0) {
|
|
384
|
+
result.data = result.data || {};
|
|
385
|
+
result.data.todoComments = todoCount;
|
|
386
|
+
// Penalize for many TODO comments
|
|
387
|
+
result.score = Math.max(0, result.score - Math.min(todoCount * 2, 30));
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
return result;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Collect technical debt metrics
|
|
395
|
+
*/
|
|
396
|
+
async function collectTechnicalDebt(projectRoot) {
|
|
397
|
+
const result = { score: 100, data: { indicators: [] }, source: 'code-analysis' };
|
|
398
|
+
|
|
399
|
+
const debtIndicators = [];
|
|
400
|
+
|
|
401
|
+
// Check for TODO/FIXME/HACK comments
|
|
402
|
+
const todoCount = countTodoComments(projectRoot);
|
|
403
|
+
if (todoCount > 10) {
|
|
404
|
+
debtIndicators.push({ type: 'todo-comments', count: todoCount, severity: 'low' });
|
|
405
|
+
result.score -= Math.min(todoCount, 20);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// Check for @ts-ignore or @ts-expect-error
|
|
409
|
+
const tsIgnoreCount = countPattern(projectRoot, /@ts-(ignore|expect-error)/g);
|
|
410
|
+
if (tsIgnoreCount > 0) {
|
|
411
|
+
debtIndicators.push({ type: 'ts-ignores', count: tsIgnoreCount, severity: 'medium' });
|
|
412
|
+
result.score -= tsIgnoreCount * 3;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Check for eslint-disable comments
|
|
416
|
+
const eslintDisableCount = countPattern(projectRoot, /eslint-disable/g);
|
|
417
|
+
if (eslintDisableCount > 5) {
|
|
418
|
+
debtIndicators.push({ type: 'eslint-disables', count: eslintDisableCount, severity: 'medium' });
|
|
419
|
+
result.score -= Math.min(eslintDisableCount * 2, 20);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// Check for console.log statements (should be removed in production)
|
|
423
|
+
const consoleCount = countPattern(projectRoot, /console\.(log|debug|info)\(/g);
|
|
424
|
+
if (consoleCount > 10) {
|
|
425
|
+
debtIndicators.push({ type: 'console-logs', count: consoleCount, severity: 'low' });
|
|
426
|
+
result.score -= Math.min(consoleCount, 15);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Check for 'any' type usage in TypeScript
|
|
430
|
+
const anyCount = countPattern(projectRoot, /:\s*any\b/g, ['.ts', '.tsx']);
|
|
431
|
+
if (anyCount > 5) {
|
|
432
|
+
debtIndicators.push({ type: 'any-types', count: anyCount, severity: 'medium' });
|
|
433
|
+
result.score -= Math.min(anyCount * 2, 25);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
result.data.indicators = debtIndicators;
|
|
437
|
+
result.data.totalIssues = debtIndicators.reduce((sum, i) => sum + i.count, 0);
|
|
438
|
+
result.score = Math.max(0, result.score);
|
|
439
|
+
|
|
440
|
+
return result;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Collect commit frequency metrics
|
|
445
|
+
*/
|
|
446
|
+
async function collectCommitFrequency(projectRoot) {
|
|
447
|
+
const result = { score: 0, data: null, source: 'git' };
|
|
448
|
+
|
|
449
|
+
try {
|
|
450
|
+
// Get commits from last 30 days
|
|
451
|
+
const output = execSync(
|
|
452
|
+
'git log --since="30 days ago" --oneline 2>/dev/null | wc -l',
|
|
453
|
+
{ cwd: projectRoot, encoding: 'utf-8' }
|
|
454
|
+
).trim();
|
|
455
|
+
|
|
456
|
+
const commits = parseInt(output, 10) || 0;
|
|
457
|
+
result.data = {
|
|
458
|
+
last30Days: commits,
|
|
459
|
+
avgPerDay: Math.round(commits / 30 * 10) / 10
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
// Score: active development = good
|
|
463
|
+
if (commits >= 60) result.score = 100; // 2+ per day
|
|
464
|
+
else if (commits >= 30) result.score = 80; // 1 per day
|
|
465
|
+
else if (commits >= 15) result.score = 60; // every 2 days
|
|
466
|
+
else if (commits >= 5) result.score = 40;
|
|
467
|
+
else result.score = 20;
|
|
468
|
+
} catch {
|
|
469
|
+
// Not a git repo
|
|
470
|
+
result.data = { error: 'Not a git repository' };
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return result;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Collect README quality score
|
|
478
|
+
*/
|
|
479
|
+
async function collectReadmeScore(projectRoot) {
|
|
480
|
+
const result = { score: 0, data: null, source: null };
|
|
481
|
+
|
|
482
|
+
const readmePaths = ['README.md', 'readme.md', 'Readme.md'];
|
|
483
|
+
let readmePath = null;
|
|
484
|
+
|
|
485
|
+
for (const p of readmePaths) {
|
|
486
|
+
if (fs.existsSync(path.join(projectRoot, p))) {
|
|
487
|
+
readmePath = path.join(projectRoot, p);
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (!readmePath) {
|
|
493
|
+
return { score: 0, data: { exists: false }, source: null };
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
const content = fs.readFileSync(readmePath, 'utf-8');
|
|
497
|
+
const lines = content.split('\n').length;
|
|
498
|
+
const words = content.split(/\s+/).length;
|
|
499
|
+
|
|
500
|
+
// Check for common README sections
|
|
501
|
+
const sections = {
|
|
502
|
+
installation: /#+\s*(install|getting started|setup)/i,
|
|
503
|
+
usage: /#+\s*(usage|how to use|examples)/i,
|
|
504
|
+
api: /#+\s*(api|reference|documentation)/i,
|
|
505
|
+
contributing: /#+\s*(contribut)/i,
|
|
506
|
+
license: /#+\s*(license)/i,
|
|
507
|
+
badges: /\[!\[/g, // Badge markdown
|
|
508
|
+
codeBlocks: /```/g
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
const found = {};
|
|
512
|
+
let sectionScore = 0;
|
|
513
|
+
|
|
514
|
+
for (const [section, pattern] of Object.entries(sections)) {
|
|
515
|
+
if (pattern.test(content)) {
|
|
516
|
+
found[section] = true;
|
|
517
|
+
sectionScore += 15;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Base score on length
|
|
522
|
+
let lengthScore = 0;
|
|
523
|
+
if (words > 500) lengthScore = 20;
|
|
524
|
+
else if (words > 200) lengthScore = 15;
|
|
525
|
+
else if (words > 100) lengthScore = 10;
|
|
526
|
+
else lengthScore = 5;
|
|
527
|
+
|
|
528
|
+
result.score = Math.min(100, sectionScore + lengthScore);
|
|
529
|
+
result.data = {
|
|
530
|
+
exists: true,
|
|
531
|
+
lines,
|
|
532
|
+
words,
|
|
533
|
+
sections: found,
|
|
534
|
+
path: readmePath
|
|
535
|
+
};
|
|
536
|
+
result.source = readmePath;
|
|
537
|
+
|
|
538
|
+
return result;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// ============================================================================
|
|
542
|
+
// Utility Functions
|
|
543
|
+
// ============================================================================
|
|
544
|
+
|
|
545
|
+
function getFilesRecursive(dir, extensions, depth = 0) {
|
|
546
|
+
if (depth > 5) return [];
|
|
547
|
+
const files = [];
|
|
548
|
+
|
|
549
|
+
try {
|
|
550
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
551
|
+
|
|
552
|
+
for (const entry of entries) {
|
|
553
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
554
|
+
|
|
555
|
+
const fullPath = path.join(dir, entry.name);
|
|
556
|
+
|
|
557
|
+
if (entry.isDirectory()) {
|
|
558
|
+
files.push(...getFilesRecursive(fullPath, extensions, depth + 1));
|
|
559
|
+
} else if (entry.isFile()) {
|
|
560
|
+
const ext = path.extname(entry.name);
|
|
561
|
+
if (!extensions || extensions.includes(ext)) {
|
|
562
|
+
files.push(fullPath);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
} catch {
|
|
567
|
+
// Permission denied
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return files;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function countTodoComments(projectRoot) {
|
|
574
|
+
const pattern = /\b(TODO|FIXME|HACK|XXX|BUG)\b/gi;
|
|
575
|
+
return countPattern(projectRoot, pattern);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function countPattern(projectRoot, pattern, extensions = ['.js', '.ts', '.jsx', '.tsx']) {
|
|
579
|
+
let count = 0;
|
|
580
|
+
const srcDirs = ['src', 'app', 'lib', 'components', 'pages', 'core', 'cli'];
|
|
581
|
+
|
|
582
|
+
for (const dir of srcDirs) {
|
|
583
|
+
const dirPath = path.join(projectRoot, dir);
|
|
584
|
+
if (!fs.existsSync(dirPath)) continue;
|
|
585
|
+
|
|
586
|
+
const files = getFilesRecursive(dirPath, extensions);
|
|
587
|
+
for (const file of files) {
|
|
588
|
+
try {
|
|
589
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
590
|
+
const matches = content.match(pattern);
|
|
591
|
+
if (matches) count += matches.length;
|
|
592
|
+
} catch {
|
|
593
|
+
// Skip
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
return count;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// ============================================================================
|
|
602
|
+
// Main Functions
|
|
603
|
+
// ============================================================================
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Collect all metrics for a project
|
|
607
|
+
*/
|
|
608
|
+
async function collectAllMetrics(projectRoot, options = {}) {
|
|
609
|
+
const results = {
|
|
610
|
+
timestamp: new Date().toISOString(),
|
|
611
|
+
metrics: {},
|
|
612
|
+
categories: {},
|
|
613
|
+
overallScore: 0
|
|
614
|
+
};
|
|
615
|
+
|
|
616
|
+
const collectors = {
|
|
617
|
+
testCoverage: collectTestCoverage,
|
|
618
|
+
bundleSize: collectBundleSize,
|
|
619
|
+
docsCoverage: collectDocsCoverage,
|
|
620
|
+
codeComplexity: collectCodeComplexity,
|
|
621
|
+
todoCompletion: collectTodoCompletion,
|
|
622
|
+
technicalDebt: collectTechnicalDebt,
|
|
623
|
+
commitFrequency: collectCommitFrequency,
|
|
624
|
+
readmeScore: collectReadmeScore
|
|
625
|
+
};
|
|
626
|
+
|
|
627
|
+
// Run collectors
|
|
628
|
+
for (const [metricId, collector] of Object.entries(collectors)) {
|
|
629
|
+
if (options.skip && options.skip.includes(metricId)) continue;
|
|
630
|
+
|
|
631
|
+
try {
|
|
632
|
+
results.metrics[metricId] = await collector(projectRoot);
|
|
633
|
+
} catch (error) {
|
|
634
|
+
results.metrics[metricId] = { score: 0, error: error.message };
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Add checkpoint progress from project state
|
|
639
|
+
const state = projectState.loadState(projectRoot);
|
|
640
|
+
if (state) {
|
|
641
|
+
const progress = projectState.getCheckpointProgress(projectRoot);
|
|
642
|
+
results.metrics.checkpoints = {
|
|
643
|
+
score: progress.percentage,
|
|
644
|
+
data: progress
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
// Add security if available
|
|
648
|
+
if (state.security?.score !== undefined) {
|
|
649
|
+
results.metrics.security = {
|
|
650
|
+
score: state.security.score,
|
|
651
|
+
data: state.security.summary
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Calculate category scores
|
|
657
|
+
const categories = {};
|
|
658
|
+
for (const [metricId, metric] of Object.entries(results.metrics)) {
|
|
659
|
+
const definition = METRICS[metricId];
|
|
660
|
+
if (!definition) continue;
|
|
661
|
+
|
|
662
|
+
const category = definition.category;
|
|
663
|
+
if (!categories[category]) {
|
|
664
|
+
categories[category] = { total: 0, count: 0, metrics: [] };
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
categories[category].total += metric.score;
|
|
668
|
+
categories[category].count++;
|
|
669
|
+
categories[category].metrics.push({
|
|
670
|
+
id: metricId,
|
|
671
|
+
label: definition.label,
|
|
672
|
+
score: metric.score,
|
|
673
|
+
icon: definition.icon
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Calculate category averages
|
|
678
|
+
for (const [category, data] of Object.entries(categories)) {
|
|
679
|
+
data.average = data.count > 0 ? Math.round(data.total / data.count) : 0;
|
|
680
|
+
}
|
|
681
|
+
results.categories = categories;
|
|
682
|
+
|
|
683
|
+
// Calculate overall score (weighted average)
|
|
684
|
+
let totalWeight = 0;
|
|
685
|
+
let weightedScore = 0;
|
|
686
|
+
|
|
687
|
+
for (const [metricId, metric] of Object.entries(results.metrics)) {
|
|
688
|
+
const definition = METRICS[metricId];
|
|
689
|
+
if (!definition) continue;
|
|
690
|
+
|
|
691
|
+
totalWeight += definition.weight;
|
|
692
|
+
weightedScore += metric.score * definition.weight;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
results.overallScore = totalWeight > 0
|
|
696
|
+
? Math.round(weightedScore / totalWeight)
|
|
697
|
+
: 0;
|
|
698
|
+
|
|
699
|
+
return results;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Store metrics in project state
|
|
704
|
+
*/
|
|
705
|
+
function storeMetrics(projectRoot, metrics) {
|
|
706
|
+
const state = projectState.getOrCreateState(projectRoot);
|
|
707
|
+
|
|
708
|
+
state.metrics = {
|
|
709
|
+
lastCollected: metrics.timestamp,
|
|
710
|
+
overallScore: metrics.overallScore,
|
|
711
|
+
categories: Object.fromEntries(
|
|
712
|
+
Object.entries(metrics.categories).map(([cat, data]) => [
|
|
713
|
+
cat,
|
|
714
|
+
{ score: data.average, count: data.count }
|
|
715
|
+
])
|
|
716
|
+
),
|
|
717
|
+
scores: Object.fromEntries(
|
|
718
|
+
Object.entries(metrics.metrics).map(([id, m]) => [id, m.score])
|
|
719
|
+
),
|
|
720
|
+
history: [
|
|
721
|
+
{ score: metrics.overallScore, date: metrics.timestamp },
|
|
722
|
+
...(state.metrics?.history || []).slice(0, 29) // Keep last 30
|
|
723
|
+
]
|
|
724
|
+
};
|
|
725
|
+
|
|
726
|
+
projectState.saveState(projectRoot, state);
|
|
727
|
+
|
|
728
|
+
return state.metrics;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Get metric trend
|
|
733
|
+
*/
|
|
734
|
+
function getMetricTrend(projectRoot, metricId) {
|
|
735
|
+
const state = projectState.loadState(projectRoot);
|
|
736
|
+
if (!state?.metrics?.history) return null;
|
|
737
|
+
|
|
738
|
+
const history = state.metrics.history;
|
|
739
|
+
if (history.length < 2) return 'stable';
|
|
740
|
+
|
|
741
|
+
const current = history[0].score;
|
|
742
|
+
const previous = history[1].score;
|
|
743
|
+
const diff = current - previous;
|
|
744
|
+
|
|
745
|
+
if (diff > 5) return 'up';
|
|
746
|
+
if (diff < -5) return 'down';
|
|
747
|
+
return 'stable';
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// ============================================================================
|
|
751
|
+
// Exports
|
|
752
|
+
// ============================================================================
|
|
753
|
+
|
|
754
|
+
module.exports = {
|
|
755
|
+
METRICS,
|
|
756
|
+
collectAllMetrics,
|
|
757
|
+
storeMetrics,
|
|
758
|
+
getMetricTrend,
|
|
759
|
+
// Individual collectors
|
|
760
|
+
collectTestCoverage,
|
|
761
|
+
collectBundleSize,
|
|
762
|
+
collectDocsCoverage,
|
|
763
|
+
collectCodeComplexity,
|
|
764
|
+
collectTodoCompletion,
|
|
765
|
+
collectTechnicalDebt,
|
|
766
|
+
collectCommitFrequency,
|
|
767
|
+
collectReadmeScore
|
|
768
|
+
};
|