@equilateral_ai/mindmeld 3.3.1 → 3.4.0
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 +1 -10
- package/hooks/pre-compact.js +213 -25
- package/hooks/session-start.js +635 -41
- package/hooks/subagent-start.js +150 -0
- package/hooks/subagent-stop.js +184 -0
- package/package.json +8 -7
- package/scripts/init-project.js +74 -33
- package/scripts/mcp-bridge.js +220 -0
- package/src/core/CorrelationAnalyzer.js +157 -0
- package/src/core/LLMPatternDetector.js +198 -0
- package/src/core/RelevanceDetector.js +123 -36
- package/src/core/StandardsIngestion.js +119 -18
- package/src/handlers/activity/activityGetMe.js +1 -1
- package/src/handlers/activity/activityGetTeam.js +100 -55
- package/src/handlers/admin/adminSetup.js +216 -0
- package/src/handlers/alerts/alertsAcknowledge.js +6 -6
- package/src/handlers/alerts/alertsGet.js +11 -11
- package/src/handlers/analytics/activitySummaryGet.js +34 -35
- package/src/handlers/analytics/coachingGet.js +11 -11
- package/src/handlers/analytics/convergenceGet.js +236 -0
- package/src/handlers/analytics/developerScoreGet.js +41 -111
- package/src/handlers/collaborators/collaboratorInvite.js +1 -1
- package/src/handlers/company/companyUsersDelete.js +141 -0
- package/src/handlers/company/companyUsersGet.js +90 -0
- package/src/handlers/company/companyUsersPost.js +267 -0
- package/src/handlers/company/companyUsersPut.js +76 -0
- package/src/handlers/correlations/correlationsDeveloperGet.js +12 -12
- package/src/handlers/correlations/correlationsGet.js +8 -8
- package/src/handlers/correlations/correlationsProjectGet.js +5 -5
- package/src/handlers/enterprise/controlTowerGet.js +224 -0
- package/src/handlers/enterprise/enterpriseOnboardingSetup.js +48 -9
- package/src/handlers/enterprise/enterpriseOnboardingStatus.js +1 -3
- package/src/handlers/github/githubConnectionStatus.js +1 -1
- package/src/handlers/github/githubDiscoverPatterns.js +4 -2
- package/src/handlers/github/githubPatternsReview.js +7 -36
- package/src/handlers/health/healthGet.js +55 -0
- package/src/handlers/helpers/checkSuperAdmin.js +13 -14
- package/src/handlers/helpers/subscriptionTiers.js +27 -27
- package/src/handlers/mcp/mcpHandler.js +569 -0
- package/src/handlers/mcp/mindmeldMcpHandler.js +689 -0
- package/src/handlers/notifications/sendNotification.js +18 -18
- package/src/handlers/patterns/patternEvaluatePromotionPost.js +173 -0
- package/src/handlers/projects/projectCreate.js +124 -10
- package/src/handlers/projects/projectDelete.js +4 -4
- package/src/handlers/projects/projectGet.js +8 -8
- package/src/handlers/projects/projectUpdate.js +4 -4
- package/src/handlers/reports/aiLeverage.js +34 -30
- package/src/handlers/reports/engineeringInvestment.js +16 -16
- package/src/handlers/reports/riskForecast.js +41 -21
- package/src/handlers/reports/standardsRoi.js +101 -9
- package/src/handlers/scheduled/maturityUpdateJob.js +166 -0
- package/src/handlers/sessions/sessionStandardsPost.js +43 -7
- package/src/handlers/standards/discoveriesGet.js +93 -0
- package/src/handlers/standards/projectStandardsGet.js +2 -2
- package/src/handlers/standards/projectStandardsPut.js +2 -2
- package/src/handlers/standards/standardsRelevantPost.js +107 -12
- package/src/handlers/standards/standardsTransition.js +112 -15
- package/src/handlers/stripe/billingPortalPost.js +1 -1
- package/src/handlers/stripe/enterpriseCheckoutPost.js +2 -2
- package/src/handlers/stripe/subscriptionCreatePost.js +2 -2
- package/src/handlers/stripe/webhookPost.js +42 -14
- package/src/handlers/user/apiTokenCreate.js +71 -0
- package/src/handlers/user/apiTokenList.js +64 -0
- package/src/handlers/user/userSplashGet.js +90 -73
- package/src/handlers/users/cognitoPostConfirmation.js +37 -1
- package/src/handlers/users/cognitoPreSignUp.js +114 -0
- package/src/handlers/users/userGet.js +12 -8
- package/src/handlers/webhooks/githubWebhook.js +117 -125
- package/src/index.js +8 -5
|
@@ -215,20 +215,27 @@ class RelevanceDetector {
|
|
|
215
215
|
* Detect React usage
|
|
216
216
|
*/
|
|
217
217
|
async detectReact(keyFiles) {
|
|
218
|
-
|
|
219
|
-
return false;
|
|
220
|
-
}
|
|
221
|
-
|
|
218
|
+
// Check root package.json first
|
|
222
219
|
try {
|
|
223
220
|
const pkgPath = path.join(this.workingDirectory, 'package.json');
|
|
224
221
|
const content = await fs.readFile(pkgPath, 'utf-8');
|
|
225
222
|
const pkg = JSON.parse(content);
|
|
223
|
+
if (pkg.dependencies?.react || pkg.devDependencies?.react) return true;
|
|
224
|
+
} catch (error) { /* continue */ }
|
|
226
225
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
226
|
+
// Check subdirectory package.json files (monorepo support)
|
|
227
|
+
const subDirs = ['frontend', 'frontend/admin', 'client', 'web', 'app'];
|
|
228
|
+
for (const dir of subDirs) {
|
|
229
|
+
try {
|
|
230
|
+
const pkgPath = path.join(this.workingDirectory, dir, 'package.json');
|
|
231
|
+
const content = await fs.readFile(pkgPath, 'utf-8');
|
|
232
|
+
const pkg = JSON.parse(content);
|
|
233
|
+
if (pkg.dependencies?.react || pkg.devDependencies?.react) return true;
|
|
234
|
+
} catch (error) { /* continue */ }
|
|
231
235
|
}
|
|
236
|
+
|
|
237
|
+
// Fallback: check for .jsx or .tsx files in keyFiles
|
|
238
|
+
return keyFiles.some(f => f.endsWith('.jsx') || f.endsWith('.tsx'));
|
|
232
239
|
}
|
|
233
240
|
|
|
234
241
|
/**
|
|
@@ -462,15 +469,18 @@ class RelevanceDetector {
|
|
|
462
469
|
} catch (error) {
|
|
463
470
|
// Database unavailable — fall back to reading YAML files directly
|
|
464
471
|
console.warn('Standards patterns table not found. Falling back to file-based standards.');
|
|
465
|
-
return this.loadStandardsFromFiles(categories);
|
|
472
|
+
return this.loadStandardsFromFiles(categories, characteristics);
|
|
466
473
|
}
|
|
467
474
|
}
|
|
468
475
|
|
|
469
476
|
/**
|
|
470
477
|
* File-system fallback: load and filter standards from YAML files
|
|
471
478
|
* Used when database is unavailable (local dev, first run, DB down)
|
|
479
|
+
* @param {Array<string>} categories - Categories to filter by
|
|
480
|
+
* @param {Object} characteristics - Project characteristics for relevance scoring
|
|
481
|
+
* @returns {Promise<Array>} Ranked standards
|
|
472
482
|
*/
|
|
473
|
-
async loadStandardsFromFiles(categories) {
|
|
483
|
+
async loadStandardsFromFiles(categories, characteristics = null) {
|
|
474
484
|
try {
|
|
475
485
|
const standardsPath = path.isAbsolute(this.standardsPath)
|
|
476
486
|
? this.standardsPath
|
|
@@ -483,12 +493,20 @@ class RelevanceDetector {
|
|
|
483
493
|
const validMaturity = ['enforced', 'validated', 'recommended'];
|
|
484
494
|
const categorySet = new Set(categories);
|
|
485
495
|
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
496
|
+
const filtered = allPatterns.filter(p =>
|
|
497
|
+
categorySet.has(p.category) && validMaturity.includes(p.maturity)
|
|
498
|
+
);
|
|
499
|
+
|
|
500
|
+
// Apply full relevance scoring if characteristics provided
|
|
501
|
+
if (characteristics) {
|
|
502
|
+
return this.rankStandards(filtered, characteristics, categories);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Fallback: sort by maturity only (legacy behavior)
|
|
506
|
+
return filtered.sort((a, b) => {
|
|
507
|
+
const maturityOrder = { enforced: 1, validated: 2, recommended: 3 };
|
|
508
|
+
return (maturityOrder[a.maturity] || 3) - (maturityOrder[b.maturity] || 3);
|
|
509
|
+
});
|
|
492
510
|
} catch (fallbackError) {
|
|
493
511
|
console.error('File-based standards fallback failed:', fallbackError.message);
|
|
494
512
|
return [];
|
|
@@ -533,6 +551,11 @@ class RelevanceDetector {
|
|
|
533
551
|
score += 5;
|
|
534
552
|
}
|
|
535
553
|
|
|
554
|
+
// Workflow bonus (actionable procedures are high-value)
|
|
555
|
+
if (standard.type === 'workflow' || (standard.tags && standard.tags.includes('workflow'))) {
|
|
556
|
+
score += 10;
|
|
557
|
+
}
|
|
558
|
+
|
|
536
559
|
return {
|
|
537
560
|
...standard,
|
|
538
561
|
relevance_score: Math.round(score * 10) / 10 // Round to 1 decimal
|
|
@@ -542,39 +565,103 @@ class RelevanceDetector {
|
|
|
542
565
|
|
|
543
566
|
/**
|
|
544
567
|
* Format standards for injection into Claude Code context
|
|
568
|
+
* Handles both standard rules and workflow procedures
|
|
545
569
|
*/
|
|
546
570
|
formatForInjection(relevantStandards) {
|
|
547
571
|
if (!relevantStandards || relevantStandards.length === 0) {
|
|
548
572
|
return '';
|
|
549
573
|
}
|
|
550
574
|
|
|
551
|
-
|
|
575
|
+
const standards = [];
|
|
576
|
+
const workflows = [];
|
|
577
|
+
|
|
578
|
+
for (const item of relevantStandards) {
|
|
579
|
+
if (item.type === 'workflow' || (item.tags && item.tags.includes('workflow') && item.examples && item.examples[0]?.type === 'workflow')) {
|
|
580
|
+
workflows.push(item);
|
|
581
|
+
} else {
|
|
582
|
+
standards.push(item);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
let output = '';
|
|
552
587
|
|
|
553
|
-
|
|
554
|
-
output +=
|
|
555
|
-
output += `**Category**: ${standard.category}\n`;
|
|
556
|
-
output += `**Maturity**: ${standard.maturity}\n`;
|
|
557
|
-
output += `**Relevance**: ${standard.relevance_score}/100\n\n`;
|
|
558
|
-
output += `${standard.rule}\n\n`;
|
|
588
|
+
if (standards.length > 0) {
|
|
589
|
+
output += '## Relevant Standards (Auto-Detected)\n\n';
|
|
559
590
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
output +=
|
|
563
|
-
|
|
564
|
-
|
|
591
|
+
for (const standard of standards) {
|
|
592
|
+
output += `### ${standard.element}\n`;
|
|
593
|
+
output += `**Category**: ${standard.category}\n`;
|
|
594
|
+
output += `**Maturity**: ${standard.maturity}\n`;
|
|
595
|
+
output += `**Relevance**: ${standard.relevance_score}/100\n\n`;
|
|
596
|
+
output += `${standard.rule}\n\n`;
|
|
597
|
+
|
|
598
|
+
if (standard.examples && standard.examples.length > 0) {
|
|
599
|
+
output += '**Examples**:\n';
|
|
600
|
+
for (const example of standard.examples.slice(0, 2)) {
|
|
601
|
+
output += `\`\`\`javascript\n${example.code || example}\n\`\`\`\n\n`;
|
|
602
|
+
}
|
|
565
603
|
}
|
|
566
|
-
}
|
|
567
604
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
605
|
+
if (standard.anti_patterns && Object.keys(standard.anti_patterns).length > 0) {
|
|
606
|
+
output += '**Anti-Patterns** (Avoid):\n';
|
|
607
|
+
for (const [key, value] of Object.entries(standard.anti_patterns)) {
|
|
608
|
+
output += `- ❌ ${key}: ${value}\n`;
|
|
609
|
+
}
|
|
610
|
+
output += '\n';
|
|
573
611
|
}
|
|
574
|
-
|
|
612
|
+
|
|
613
|
+
output += '---\n\n';
|
|
575
614
|
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
if (workflows.length > 0) {
|
|
618
|
+
output += '## Workflows (Auto-Detected)\n\n';
|
|
619
|
+
|
|
620
|
+
for (const workflow of workflows) {
|
|
621
|
+
const workflowData = workflow.examples && workflow.examples[0]?.type === 'workflow'
|
|
622
|
+
? workflow.examples[0]
|
|
623
|
+
: null;
|
|
624
|
+
|
|
625
|
+
output += `### ${workflow.element}\n`;
|
|
626
|
+
output += `**Category**: ${workflow.category}\n`;
|
|
576
627
|
|
|
577
|
-
|
|
628
|
+
if (workflowData) {
|
|
629
|
+
output += `**When**: ${workflowData.trigger}\n\n`;
|
|
630
|
+
|
|
631
|
+
if (workflowData.preconditions && workflowData.preconditions.length > 0) {
|
|
632
|
+
output += '**Preconditions**:\n';
|
|
633
|
+
for (const pre of workflowData.preconditions) {
|
|
634
|
+
output += `- ${pre}\n`;
|
|
635
|
+
}
|
|
636
|
+
output += '\n';
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (workflowData.steps && workflowData.steps.length > 0) {
|
|
640
|
+
output += '**Steps**:\n';
|
|
641
|
+
for (const step of workflowData.steps) {
|
|
642
|
+
const gateMarker = step.gate ? ' (GATE)' : '';
|
|
643
|
+
output += `${step.index}. **${step.name}**${gateMarker} — ${step.description}\n`;
|
|
644
|
+
if (step.command) {
|
|
645
|
+
output += ` \`${step.command.trim().split('\n')[0]}\`\n`;
|
|
646
|
+
}
|
|
647
|
+
if (step.validation) {
|
|
648
|
+
output += ` Verify: ${step.validation}\n`;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
output += '\n';
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
if (workflow.anti_patterns && Object.keys(workflow.anti_patterns).length > 0) {
|
|
656
|
+
output += '**Anti-Patterns** (Avoid):\n';
|
|
657
|
+
for (const [key, value] of Object.entries(workflow.anti_patterns)) {
|
|
658
|
+
output += `- ❌ ${key}: ${value}\n`;
|
|
659
|
+
}
|
|
660
|
+
output += '\n';
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
output += '---\n\n';
|
|
664
|
+
}
|
|
578
665
|
}
|
|
579
666
|
|
|
580
667
|
return output;
|
|
@@ -227,16 +227,13 @@ class StandardsIngestion {
|
|
|
227
227
|
|
|
228
228
|
/**
|
|
229
229
|
* Parse a YAML standards file into Rapport pattern format
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
* applies_to: [glob patterns]
|
|
238
|
-
* anti_patterns:
|
|
239
|
-
* - string
|
|
230
|
+
* Supports both type: standard (v2.0) and type: workflow (v3.0)
|
|
231
|
+
*
|
|
232
|
+
* YAML format (standard):
|
|
233
|
+
* id, category, priority, rules[], anti_patterns[]
|
|
234
|
+
*
|
|
235
|
+
* YAML format (workflow):
|
|
236
|
+
* id, category, priority, type: workflow, trigger, steps[], preconditions[], anti_patterns[]
|
|
240
237
|
*/
|
|
241
238
|
async parseYamlStandard(filepath, category) {
|
|
242
239
|
const content = await fs.readFile(filepath, 'utf-8');
|
|
@@ -254,6 +251,11 @@ class StandardsIngestion {
|
|
|
254
251
|
throw new Error('Invalid YAML structure: expected an object');
|
|
255
252
|
}
|
|
256
253
|
|
|
254
|
+
// Route to workflow parser if type: workflow
|
|
255
|
+
if (doc.type === 'workflow') {
|
|
256
|
+
return this.parseYamlWorkflow(doc, filepath, filename, category);
|
|
257
|
+
}
|
|
258
|
+
|
|
257
259
|
const patterns = [];
|
|
258
260
|
const standardId = doc.id || filename;
|
|
259
261
|
const standardCategory = doc.category || category;
|
|
@@ -268,15 +270,36 @@ class StandardsIngestion {
|
|
|
268
270
|
}))
|
|
269
271
|
: [];
|
|
270
272
|
|
|
271
|
-
// Extract examples -
|
|
273
|
+
// Extract examples - handles both array of strings and named object formats
|
|
272
274
|
const examples = [];
|
|
273
|
-
if (doc.examples
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
275
|
+
if (doc.examples) {
|
|
276
|
+
if (Array.isArray(doc.examples)) {
|
|
277
|
+
// Array format: ["code example 1", "code example 2"]
|
|
278
|
+
for (const example of doc.examples) {
|
|
279
|
+
if (typeof example === 'string') {
|
|
280
|
+
examples.push({
|
|
281
|
+
description: 'Example',
|
|
282
|
+
code: example.trim(),
|
|
283
|
+
language: this.inferLanguageFromCode(example)
|
|
284
|
+
});
|
|
285
|
+
} else if (typeof example === 'object' && example !== null) {
|
|
286
|
+
// Object in array: { description: "...", code: "..." }
|
|
287
|
+
examples.push({
|
|
288
|
+
description: example.description || 'Example',
|
|
289
|
+
code: (example.code || '').trim(),
|
|
290
|
+
language: example.language || this.inferLanguageFromCode(example.code || '')
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
} else if (typeof doc.examples === 'object') {
|
|
295
|
+
// Named object format: { example_name: "code here" }
|
|
296
|
+
for (const [name, code] of Object.entries(doc.examples)) {
|
|
297
|
+
examples.push({
|
|
298
|
+
description: name.replace(/_/g, ' '),
|
|
299
|
+
code: typeof code === 'string' ? code.trim() : String(code),
|
|
300
|
+
language: this.inferLanguageFromCode(code)
|
|
301
|
+
});
|
|
302
|
+
}
|
|
280
303
|
}
|
|
281
304
|
}
|
|
282
305
|
|
|
@@ -366,6 +389,84 @@ class StandardsIngestion {
|
|
|
366
389
|
return { patterns };
|
|
367
390
|
}
|
|
368
391
|
|
|
392
|
+
/**
|
|
393
|
+
* Parse a workflow YAML file into a single Rapport pattern with workflow metadata
|
|
394
|
+
* Workflows are stored as one pattern (not exploded into per-rule patterns)
|
|
395
|
+
* so the injection system can render them as ordered procedures.
|
|
396
|
+
*/
|
|
397
|
+
parseYamlWorkflow(doc, filepath, filename, category) {
|
|
398
|
+
const standardId = doc.id || filename;
|
|
399
|
+
const standardCategory = doc.category || category;
|
|
400
|
+
const standardPriority = doc.priority || 10; // Workflows default to high priority
|
|
401
|
+
|
|
402
|
+
const antiPatterns = Array.isArray(doc.anti_patterns)
|
|
403
|
+
? doc.anti_patterns.map(ap => ({
|
|
404
|
+
description: typeof ap === 'string' ? ap : ap.description || 'Anti-pattern to avoid',
|
|
405
|
+
code: typeof ap === 'object' ? ap.code : null,
|
|
406
|
+
language: typeof ap === 'object' ? ap.language : null
|
|
407
|
+
}))
|
|
408
|
+
: [];
|
|
409
|
+
|
|
410
|
+
const tags = Array.isArray(doc.tags) ? doc.tags : [];
|
|
411
|
+
const costImpact = doc.cost_impact && typeof doc.cost_impact === 'object'
|
|
412
|
+
? doc.cost_impact
|
|
413
|
+
: (typeof doc.cost_impact === 'string' ? { description: doc.cost_impact } : null);
|
|
414
|
+
|
|
415
|
+
const steps = Array.isArray(doc.steps) ? doc.steps : [];
|
|
416
|
+
const preconditions = Array.isArray(doc.preconditions) ? doc.preconditions : [];
|
|
417
|
+
const trigger = doc.trigger || '';
|
|
418
|
+
const related = Array.isArray(doc.related) ? doc.related : [];
|
|
419
|
+
|
|
420
|
+
const patternId = this.generatePatternId(standardCategory, standardId, 'workflow');
|
|
421
|
+
|
|
422
|
+
// Build a readable element name from the id
|
|
423
|
+
const element = standardId.replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
424
|
+
|
|
425
|
+
// The rule field contains the trigger for search/matching
|
|
426
|
+
const rule = `WORKFLOW: ${trigger}`;
|
|
427
|
+
|
|
428
|
+
// Store workflow structure in examples field (JSON-serializable)
|
|
429
|
+
// This preserves the full workflow for the injection formatter
|
|
430
|
+
const workflowData = {
|
|
431
|
+
type: 'workflow',
|
|
432
|
+
trigger: trigger,
|
|
433
|
+
preconditions: preconditions,
|
|
434
|
+
steps: steps.map((step, i) => ({
|
|
435
|
+
index: i + 1,
|
|
436
|
+
name: step.name || `Step ${i + 1}`,
|
|
437
|
+
description: step.description || '',
|
|
438
|
+
command: step.command || null,
|
|
439
|
+
validation: step.validation || null,
|
|
440
|
+
standards: Array.isArray(step.standards) ? step.standards : [],
|
|
441
|
+
gate: step.gate === true
|
|
442
|
+
})),
|
|
443
|
+
related: related
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
patterns: [{
|
|
448
|
+
pattern_id: patternId,
|
|
449
|
+
file_name: filename,
|
|
450
|
+
element: element,
|
|
451
|
+
rule: rule,
|
|
452
|
+
priority: standardPriority,
|
|
453
|
+
correlation: 1.0,
|
|
454
|
+
source: 'equilateral-standards',
|
|
455
|
+
maturity: 'enforced',
|
|
456
|
+
scope: 'organization',
|
|
457
|
+
category: standardCategory,
|
|
458
|
+
applicable_files: this.extractApplicableFiles('', standardCategory),
|
|
459
|
+
anti_patterns: antiPatterns,
|
|
460
|
+
examples: [workflowData], // Workflow data stored as first example
|
|
461
|
+
cost_impact: costImpact,
|
|
462
|
+
tags: [...tags, 'workflow'],
|
|
463
|
+
source_file: filepath,
|
|
464
|
+
title: standardId,
|
|
465
|
+
type: 'workflow' // Flag for injection formatter
|
|
466
|
+
}]
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
369
470
|
/**
|
|
370
471
|
* Parse a markdown standards file into Rapport pattern format
|
|
371
472
|
*/
|
|
@@ -77,7 +77,7 @@ exports.handler = wrapHandler(async ({ requestContext }) => {
|
|
|
77
77
|
last_30_days: parseInt(activity.commits_30d) || 0,
|
|
78
78
|
last_7_days: parseInt(activity.commits_7d) || 0,
|
|
79
79
|
last_commit: activity.last_commit,
|
|
80
|
-
days_since_commit: parseInt(activity.days_since_commit)
|
|
80
|
+
days_since_commit: activity.days_since_commit !== null ? parseInt(activity.days_since_commit) : null
|
|
81
81
|
},
|
|
82
82
|
pull_requests: {
|
|
83
83
|
opened_30d: parseInt(activity.prs_opened_30d) || 0,
|
|
@@ -18,10 +18,10 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
|
|
|
18
18
|
|
|
19
19
|
// Check if user is a manager or admin
|
|
20
20
|
const managerCheck = await executeQuery(`
|
|
21
|
-
SELECT ue.
|
|
22
|
-
FROM
|
|
23
|
-
JOIN
|
|
24
|
-
WHERE ue.
|
|
21
|
+
SELECT ue.company_id, ue.manager, ue.admin, u.super_admin
|
|
22
|
+
FROM rapport.user_entitlements ue
|
|
23
|
+
JOIN rapport.users u ON ue.email_address = u.email_address
|
|
24
|
+
WHERE ue.email_address = $1
|
|
25
25
|
`, [email]);
|
|
26
26
|
|
|
27
27
|
if (managerCheck.rowCount === 0) {
|
|
@@ -29,72 +29,117 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
const userRole = managerCheck.rows[0];
|
|
32
|
-
const isAuthorized = userRole.
|
|
32
|
+
const isAuthorized = userRole.manager || userRole.admin || userRole.super_admin;
|
|
33
33
|
|
|
34
34
|
if (!isAuthorized) {
|
|
35
35
|
return createErrorResponse(403, 'Manager or Admin access required');
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
const companyId = queryStringParameters?.company_id || userRole.
|
|
38
|
+
const companyId = queryStringParameters?.company_id || userRole.company_id;
|
|
39
39
|
|
|
40
|
-
// Get team summary
|
|
40
|
+
// Get team summary directly from base tables
|
|
41
41
|
const summaryResult = await executeQuery(`
|
|
42
|
-
SELECT
|
|
43
|
-
|
|
42
|
+
SELECT
|
|
43
|
+
COUNT(DISTINCT ue.email_address) as total_developers,
|
|
44
|
+
COUNT(DISTINCT ue.email_address) FILTER (
|
|
45
|
+
WHERE EXISTS (
|
|
46
|
+
SELECT 1 FROM rapport.sessions s
|
|
47
|
+
WHERE s.email_address = ue.email_address
|
|
48
|
+
AND s.started_at > NOW() - INTERVAL '30 days'
|
|
49
|
+
)
|
|
50
|
+
) as active_developers
|
|
51
|
+
FROM rapport.user_entitlements ue
|
|
52
|
+
JOIN rapport.users u ON ue.email_address = u.email_address
|
|
53
|
+
WHERE ue.company_id = $1
|
|
54
|
+
AND u.active = true
|
|
44
55
|
`, [companyId]);
|
|
45
56
|
|
|
46
57
|
// Get individual developer activity
|
|
47
58
|
const developersResult = await executeQuery(`
|
|
48
59
|
SELECT
|
|
49
|
-
email_address,
|
|
50
|
-
display_name,
|
|
51
|
-
sessions_30d,
|
|
52
|
-
sessions_7d,
|
|
53
|
-
last_session,
|
|
54
|
-
commits_30d,
|
|
55
|
-
commits_7d,
|
|
56
|
-
last_commit,
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
ue.email_address,
|
|
61
|
+
u.first_name || ' ' || u.last_name as display_name,
|
|
62
|
+
COALESCE(sess.sessions_30d, 0) as sessions_30d,
|
|
63
|
+
COALESCE(sess.sessions_7d, 0) as sessions_7d,
|
|
64
|
+
sess.last_session,
|
|
65
|
+
COALESCE(comm.commits_30d, 0) as commits_30d,
|
|
66
|
+
COALESCE(comm.commits_7d, 0) as commits_7d,
|
|
67
|
+
comm.last_commit,
|
|
68
|
+
CASE WHEN comm.last_commit IS NOT NULL
|
|
69
|
+
THEN EXTRACT(DAY FROM NOW() - comm.last_commit)::INTEGER
|
|
70
|
+
ELSE NULL
|
|
71
|
+
END as days_since_commit,
|
|
72
|
+
COALESCE(pr_data.prs_merged_30d, 0) as prs_merged_30d
|
|
73
|
+
FROM rapport.user_entitlements ue
|
|
74
|
+
JOIN rapport.users u ON ue.email_address = u.email_address
|
|
75
|
+
LEFT JOIN (
|
|
76
|
+
SELECT email_address,
|
|
77
|
+
COUNT(*) FILTER (WHERE started_at > NOW() - INTERVAL '30 days') as sessions_30d,
|
|
78
|
+
COUNT(*) FILTER (WHERE started_at > NOW() - INTERVAL '7 days') as sessions_7d,
|
|
79
|
+
MAX(started_at) as last_session
|
|
80
|
+
FROM rapport.sessions
|
|
81
|
+
GROUP BY email_address
|
|
82
|
+
) sess ON sess.email_address = ue.email_address
|
|
83
|
+
LEFT JOIN (
|
|
84
|
+
SELECT author_email,
|
|
85
|
+
COUNT(*) FILTER (WHERE commit_timestamp > NOW() - INTERVAL '30 days') as commits_30d,
|
|
86
|
+
COUNT(*) FILTER (WHERE commit_timestamp > NOW() - INTERVAL '7 days') as commits_7d,
|
|
87
|
+
MAX(commit_timestamp) as last_commit
|
|
88
|
+
FROM rapport.commits
|
|
89
|
+
GROUP BY author_email
|
|
90
|
+
) comm ON comm.author_email = ue.email_address
|
|
91
|
+
LEFT JOIN (
|
|
92
|
+
SELECT pr.author_email,
|
|
93
|
+
COUNT(*) as prs_merged_30d
|
|
94
|
+
FROM rapport.pull_requests pr
|
|
95
|
+
WHERE pr.merged_at > NOW() - INTERVAL '30 days'
|
|
96
|
+
GROUP BY pr.author_email
|
|
97
|
+
) pr_data ON pr_data.author_email = ue.email_address
|
|
98
|
+
WHERE ue.company_id = $1
|
|
99
|
+
AND u.active = true
|
|
62
100
|
ORDER BY
|
|
63
|
-
CASE
|
|
64
|
-
|
|
65
|
-
ELSE days_since_commit
|
|
101
|
+
CASE WHEN comm.last_commit IS NULL THEN 999
|
|
102
|
+
ELSE EXTRACT(DAY FROM NOW() - comm.last_commit)
|
|
66
103
|
END DESC,
|
|
67
|
-
sessions_30d DESC
|
|
104
|
+
COALESCE(sess.sessions_30d, 0) DESC
|
|
68
105
|
`, [companyId]);
|
|
69
106
|
|
|
70
107
|
const summary = summaryResult.rows[0] || {
|
|
71
108
|
total_developers: 0,
|
|
72
|
-
active_developers: 0
|
|
73
|
-
stale_developers: 0,
|
|
74
|
-
very_stale_developers: 0,
|
|
75
|
-
avg_sessions_30d: 0,
|
|
76
|
-
avg_commits_30d: 0,
|
|
77
|
-
avg_conversion_pct: 0
|
|
109
|
+
active_developers: 0
|
|
78
110
|
};
|
|
79
111
|
|
|
80
|
-
const developers = developersResult.rows.map(dev =>
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
112
|
+
const developers = developersResult.rows.map(dev => {
|
|
113
|
+
const sessions30d = parseInt(dev.sessions_30d) || 0;
|
|
114
|
+
const commits30d = parseInt(dev.commits_30d) || 0;
|
|
115
|
+
const conversionPct = sessions30d > 0 ? Math.round((commits30d / sessions30d) * 100) : 0;
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
email_address: dev.email_address,
|
|
119
|
+
display_name: dev.display_name,
|
|
120
|
+
sessions: {
|
|
121
|
+
last_30_days: sessions30d,
|
|
122
|
+
last_7_days: parseInt(dev.sessions_7d) || 0,
|
|
123
|
+
last_session: dev.last_session
|
|
124
|
+
},
|
|
125
|
+
commits: {
|
|
126
|
+
last_30_days: commits30d,
|
|
127
|
+
last_7_days: parseInt(dev.commits_7d) || 0,
|
|
128
|
+
last_commit: dev.last_commit,
|
|
129
|
+
days_since_commit: parseInt(dev.days_since_commit) || null
|
|
130
|
+
},
|
|
131
|
+
prs_merged_30d: parseInt(dev.prs_merged_30d) || 0,
|
|
132
|
+
session_to_commit_conversion_pct: conversionPct,
|
|
133
|
+
status: getDevStatus(dev)
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
const totalSessions = developers.reduce((sum, d) => sum + d.sessions.last_30_days, 0);
|
|
138
|
+
const totalCommits = developers.reduce((sum, d) => sum + d.commits.last_30_days, 0);
|
|
139
|
+
const devCount = developers.length || 1;
|
|
140
|
+
const avgConversion = developers.length > 0
|
|
141
|
+
? developers.reduce((sum, d) => sum + d.session_to_commit_conversion_pct, 0) / developers.length
|
|
142
|
+
: 0;
|
|
98
143
|
|
|
99
144
|
return createSuccessResponse(
|
|
100
145
|
{
|
|
@@ -103,11 +148,11 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
|
|
|
103
148
|
summary: {
|
|
104
149
|
total_developers: parseInt(summary.total_developers) || 0,
|
|
105
150
|
active_developers: parseInt(summary.active_developers) || 0,
|
|
106
|
-
stale_developers:
|
|
107
|
-
very_stale_developers:
|
|
108
|
-
avg_sessions_30d: parseFloat(
|
|
109
|
-
avg_commits_30d: parseFloat(
|
|
110
|
-
avg_conversion_pct: parseFloat(
|
|
151
|
+
stale_developers: developers.filter(d => d.status === 'stale').length,
|
|
152
|
+
very_stale_developers: developers.filter(d => d.status === 'very_stale').length,
|
|
153
|
+
avg_sessions_30d: parseFloat((totalSessions / devCount).toFixed(1)),
|
|
154
|
+
avg_commits_30d: parseFloat((totalCommits / devCount).toFixed(1)),
|
|
155
|
+
avg_conversion_pct: parseFloat(avgConversion.toFixed(1))
|
|
111
156
|
},
|
|
112
157
|
developers
|
|
113
158
|
}]
|