@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.
Files changed (159) hide show
  1. package/bin/bootspring.js +5 -0
  2. package/cli/org.js +474 -0
  3. package/cli/preseed/index.js +16 -0
  4. package/cli/preseed/interactive.js +143 -0
  5. package/cli/preseed/templates.js +227 -0
  6. package/cli/preseed.js +9 -301
  7. package/cli/seed/builders/ai-context-builder.js +85 -0
  8. package/cli/seed/builders/index.js +13 -0
  9. package/cli/seed/builders/seed-builder.js +272 -0
  10. package/cli/seed/extractors/content-extractors.js +383 -0
  11. package/cli/seed/extractors/index.js +47 -0
  12. package/cli/seed/extractors/metadata-extractors.js +167 -0
  13. package/cli/seed/extractors/section-extractor.js +54 -0
  14. package/cli/seed/extractors/stack-extractors.js +228 -0
  15. package/cli/seed/index.js +18 -0
  16. package/cli/seed/utils/folder-structure.js +84 -0
  17. package/cli/seed/utils/index.js +11 -0
  18. package/cli/seed.js +23 -1074
  19. package/core/api-client.js +77 -0
  20. package/core/entitlements.js +36 -0
  21. package/core/organizations.js +223 -0
  22. package/core/policies.js +51 -6
  23. package/core/policy-matrix.js +303 -0
  24. package/core/project-context.js +1 -0
  25. package/dist/cli/index.d.ts +3 -0
  26. package/dist/cli/index.js +3220 -0
  27. package/dist/cli/index.js.map +1 -0
  28. package/dist/context-McpJQa_2.d.ts +5710 -0
  29. package/dist/core/index.d.ts +635 -0
  30. package/dist/core/index.js +2593 -0
  31. package/dist/core/index.js.map +1 -0
  32. package/dist/index-QqbeEiDm.d.ts +857 -0
  33. package/dist/index-UiYCgwiH.d.ts +174 -0
  34. package/dist/index.d.ts +453 -0
  35. package/dist/index.js +44228 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/mcp/index.d.ts +1 -0
  38. package/dist/mcp/index.js +41173 -0
  39. package/dist/mcp/index.js.map +1 -0
  40. package/generators/index.ts +82 -0
  41. package/intelligence/orchestrator/config/failure-signatures.js +48 -0
  42. package/intelligence/orchestrator/config/index.js +23 -0
  43. package/intelligence/orchestrator/config/pack-lifecycle.js +262 -0
  44. package/intelligence/orchestrator/config/phases.js +111 -0
  45. package/intelligence/orchestrator/config/remediation.js +150 -0
  46. package/intelligence/orchestrator/config/workflows.js +168 -0
  47. package/intelligence/orchestrator/core/index.js +16 -0
  48. package/intelligence/orchestrator/core/state-manager.js +88 -0
  49. package/intelligence/orchestrator/core/telemetry.js +24 -0
  50. package/intelligence/orchestrator/index.js +17 -0
  51. package/intelligence/orchestrator.js +17 -512
  52. package/mcp/contracts/mcp-contract.v1.json +1 -1
  53. package/package.json +16 -3
  54. package/src/cli/agent.ts +703 -0
  55. package/src/cli/analyze.ts +640 -0
  56. package/src/cli/audit.ts +707 -0
  57. package/src/cli/auth.ts +930 -0
  58. package/src/cli/billing.ts +364 -0
  59. package/src/cli/build.ts +1089 -0
  60. package/src/cli/business.ts +508 -0
  61. package/src/cli/checkpoint-utils.ts +236 -0
  62. package/src/cli/checkpoint.ts +757 -0
  63. package/src/cli/cloud-sync.ts +534 -0
  64. package/src/cli/content.ts +273 -0
  65. package/src/cli/context.ts +667 -0
  66. package/src/cli/dashboard.ts +133 -0
  67. package/src/cli/deploy.ts +704 -0
  68. package/src/cli/doctor.ts +480 -0
  69. package/src/cli/fundraise.ts +494 -0
  70. package/src/cli/generate.ts +346 -0
  71. package/src/cli/github-cmd.ts +566 -0
  72. package/src/cli/health.ts +599 -0
  73. package/src/cli/index.ts +113 -0
  74. package/src/cli/init.ts +838 -0
  75. package/src/cli/legal.ts +495 -0
  76. package/src/cli/log.ts +316 -0
  77. package/src/cli/loop.ts +1660 -0
  78. package/src/cli/manager.ts +878 -0
  79. package/src/cli/mcp.ts +275 -0
  80. package/src/cli/memory.ts +346 -0
  81. package/src/cli/metrics.ts +590 -0
  82. package/src/cli/monitor.ts +960 -0
  83. package/src/cli/mvp.ts +662 -0
  84. package/src/cli/onboard.ts +663 -0
  85. package/src/cli/orchestrator.ts +622 -0
  86. package/src/cli/plugin.ts +483 -0
  87. package/src/cli/prd.ts +671 -0
  88. package/src/cli/preseed-start.ts +1633 -0
  89. package/src/cli/preseed.ts +2434 -0
  90. package/src/cli/project.ts +526 -0
  91. package/src/cli/quality.ts +885 -0
  92. package/src/cli/security.ts +1079 -0
  93. package/src/cli/seed.ts +1224 -0
  94. package/src/cli/skill.ts +537 -0
  95. package/src/cli/suggest.ts +1225 -0
  96. package/src/cli/switch.ts +518 -0
  97. package/src/cli/task.ts +780 -0
  98. package/src/cli/telemetry.ts +172 -0
  99. package/src/cli/todo.ts +627 -0
  100. package/src/cli/types.ts +15 -0
  101. package/src/cli/update.ts +334 -0
  102. package/src/cli/visualize.ts +609 -0
  103. package/src/cli/watch.ts +895 -0
  104. package/src/cli/workspace.ts +709 -0
  105. package/src/core/action-recorder.ts +673 -0
  106. package/src/core/analyze-workflow.ts +1453 -0
  107. package/src/core/api-client.ts +1120 -0
  108. package/src/core/audit-workflow.ts +1681 -0
  109. package/src/core/auth.ts +471 -0
  110. package/src/core/build-orchestrator.ts +509 -0
  111. package/src/core/build-state.ts +621 -0
  112. package/src/core/checkpoint-engine.ts +482 -0
  113. package/src/core/config.ts +1285 -0
  114. package/src/core/context-loader.ts +694 -0
  115. package/src/core/context.ts +410 -0
  116. package/src/core/deploy-workflow.ts +1085 -0
  117. package/src/core/entitlements.ts +322 -0
  118. package/src/core/github-sync.ts +720 -0
  119. package/src/core/index.ts +981 -0
  120. package/src/core/ingest.ts +1186 -0
  121. package/src/core/metrics-engine.ts +886 -0
  122. package/src/core/mvp.ts +847 -0
  123. package/src/core/onboard-workflow.ts +1293 -0
  124. package/src/core/policies.ts +81 -0
  125. package/src/core/preseed-workflow.ts +1163 -0
  126. package/src/core/preseed.ts +1826 -0
  127. package/src/core/project-context.ts +380 -0
  128. package/src/core/project-state.ts +699 -0
  129. package/src/core/r2-sync.ts +691 -0
  130. package/src/core/scaffold.ts +1715 -0
  131. package/src/core/session.ts +286 -0
  132. package/src/core/task-extractor.ts +799 -0
  133. package/src/core/telemetry.ts +371 -0
  134. package/src/core/tier-enforcement.ts +737 -0
  135. package/src/core/utils.ts +437 -0
  136. package/src/index.ts +29 -0
  137. package/src/intelligence/agent-collab.ts +2376 -0
  138. package/src/intelligence/auto-suggest.ts +713 -0
  139. package/src/intelligence/content-gen.ts +1351 -0
  140. package/src/intelligence/cross-project.ts +1692 -0
  141. package/src/intelligence/git-memory.ts +529 -0
  142. package/src/intelligence/index.ts +318 -0
  143. package/src/intelligence/orchestrator.ts +534 -0
  144. package/src/intelligence/prd.ts +466 -0
  145. package/src/intelligence/recommendations.ts +982 -0
  146. package/src/intelligence/workflow-composer.ts +1472 -0
  147. package/src/mcp/capabilities.ts +233 -0
  148. package/src/mcp/index.ts +37 -0
  149. package/src/mcp/registry.ts +1268 -0
  150. package/src/mcp/response-formatter.ts +797 -0
  151. package/src/mcp/server.ts +240 -0
  152. package/src/types/agent.ts +69 -0
  153. package/src/types/config.ts +86 -0
  154. package/src/types/context.ts +77 -0
  155. package/src/types/index.ts +53 -0
  156. package/src/types/mcp.ts +91 -0
  157. package/src/types/skills.ts +47 -0
  158. package/src/types/workflow.ts +155 -0
  159. 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
+ }