@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
|
@@ -20,13 +20,13 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
|
|
|
20
20
|
const period = params.period || '7d';
|
|
21
21
|
const companyId = params.Company_ID;
|
|
22
22
|
|
|
23
|
-
// Validate access - must be manager/admin
|
|
23
|
+
// Validate access - must be manager/admin
|
|
24
24
|
const accessCheck = await executeQuery(`
|
|
25
|
-
SELECT ue.
|
|
26
|
-
FROM
|
|
27
|
-
WHERE ue.
|
|
28
|
-
AND (ue.
|
|
29
|
-
${companyId ? 'AND ue.
|
|
25
|
+
SELECT ue.company_id
|
|
26
|
+
FROM rapport.user_entitlements ue
|
|
27
|
+
WHERE ue.email_address = $1
|
|
28
|
+
AND (ue.admin = true OR ue.manager = true)
|
|
29
|
+
${companyId ? 'AND ue.company_id = $2' : ''}
|
|
30
30
|
LIMIT 1
|
|
31
31
|
`, companyId ? [email, companyId] : [email]);
|
|
32
32
|
|
|
@@ -34,7 +34,7 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
|
|
|
34
34
|
return createErrorResponse(403, 'Manager or Admin access required');
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
const userCompanyId = companyId || accessCheck.rows[0].
|
|
37
|
+
const userCompanyId = companyId || accessCheck.rows[0].company_id;
|
|
38
38
|
const periodDays = parsePeriod(period);
|
|
39
39
|
const periodStart = new Date();
|
|
40
40
|
periodStart.setDate(periodStart.getDate() - periodDays);
|
|
@@ -43,16 +43,16 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
|
|
|
43
43
|
const investment = await executeQuery(`
|
|
44
44
|
WITH commit_modules AS (
|
|
45
45
|
SELECT
|
|
46
|
-
|
|
46
|
+
c.repo_name as module,
|
|
47
47
|
COUNT(*) as commit_count,
|
|
48
48
|
SUM(c.lines_added) as lines_added,
|
|
49
49
|
SUM(c.lines_removed) as lines_removed,
|
|
50
50
|
COUNT(DISTINCT c.author_email) as contributors
|
|
51
51
|
FROM rapport.commits c
|
|
52
52
|
JOIN rapport.git_repositories r ON c.repo_id = r.repo_id
|
|
53
|
-
WHERE r.
|
|
54
|
-
AND c.
|
|
55
|
-
GROUP BY
|
|
53
|
+
WHERE r.company_id = $1
|
|
54
|
+
AND c.commit_timestamp >= $2
|
|
55
|
+
GROUP BY c.repo_name
|
|
56
56
|
)
|
|
57
57
|
SELECT
|
|
58
58
|
module,
|
|
@@ -60,8 +60,8 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
|
|
|
60
60
|
lines_added,
|
|
61
61
|
lines_removed,
|
|
62
62
|
contributors,
|
|
63
|
-
ROUND(commit_count * 100.0 / SUM(commit_count) OVER (), 1) as commit_percentage,
|
|
64
|
-
ROUND((lines_added + lines_removed) * 100.0 / SUM(lines_added + lines_removed) OVER (), 1) as churn_percentage
|
|
63
|
+
ROUND(commit_count * 100.0 / NULLIF(SUM(commit_count) OVER (), 0), 1) as commit_percentage,
|
|
64
|
+
ROUND((lines_added + lines_removed) * 100.0 / NULLIF(SUM(lines_added + lines_removed) OVER (), 0), 1) as churn_percentage
|
|
65
65
|
FROM commit_modules
|
|
66
66
|
ORDER BY commit_count DESC
|
|
67
67
|
LIMIT 20
|
|
@@ -78,7 +78,7 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
|
|
|
78
78
|
AVG(dm.compliance_score) as avg_compliance
|
|
79
79
|
FROM rapport.developer_metrics dm
|
|
80
80
|
JOIN rapport.git_repositories r ON dm.repo_id = r.repo_id
|
|
81
|
-
WHERE r.
|
|
81
|
+
WHERE r.company_id = $1
|
|
82
82
|
AND dm.period_start >= $2
|
|
83
83
|
GROUP BY dm.developer_name, dm.developer_email
|
|
84
84
|
ORDER BY total_commits DESC
|
|
@@ -93,8 +93,8 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
|
|
|
93
93
|
COUNT(DISTINCT c.author_email) as contributors,
|
|
94
94
|
SUM(c.lines_added) as lines_added
|
|
95
95
|
FROM rapport.git_repositories r
|
|
96
|
-
LEFT JOIN rapport.commits c ON r.repo_id = c.repo_id AND c.
|
|
97
|
-
WHERE r.
|
|
96
|
+
LEFT JOIN rapport.commits c ON r.repo_id = c.repo_id AND c.commit_timestamp >= $2
|
|
97
|
+
WHERE r.company_id = $1
|
|
98
98
|
GROUP BY r.repo_id, r.repo_name
|
|
99
99
|
ORDER BY commit_count DESC
|
|
100
100
|
`, [userCompanyId, periodStart]);
|
|
@@ -19,13 +19,13 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
|
|
|
19
19
|
const params = queryStringParameters || {};
|
|
20
20
|
const companyId = params.Company_ID;
|
|
21
21
|
|
|
22
|
-
// Validate access - must be manager/admin
|
|
22
|
+
// Validate access - must be manager/admin
|
|
23
23
|
const accessCheck = await executeQuery(`
|
|
24
|
-
SELECT ue.
|
|
25
|
-
FROM
|
|
26
|
-
WHERE ue.
|
|
27
|
-
AND (ue.
|
|
28
|
-
${companyId ? 'AND ue.
|
|
24
|
+
SELECT ue.company_id
|
|
25
|
+
FROM rapport.user_entitlements ue
|
|
26
|
+
WHERE ue.email_address = $1
|
|
27
|
+
AND (ue.admin = true OR ue.manager = true)
|
|
28
|
+
${companyId ? 'AND ue.company_id = $2' : ''}
|
|
29
29
|
LIMIT 1
|
|
30
30
|
`, companyId ? [email, companyId] : [email]);
|
|
31
31
|
|
|
@@ -33,7 +33,7 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
|
|
|
33
33
|
return createErrorResponse(403, 'Manager or Admin access required');
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
const userCompanyId = companyId || accessCheck.rows[0].
|
|
36
|
+
const userCompanyId = companyId || accessCheck.rows[0].company_id;
|
|
37
37
|
|
|
38
38
|
// Knowledge silos (bus factor analysis)
|
|
39
39
|
let silos = { rows: [] };
|
|
@@ -47,7 +47,7 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
|
|
|
47
47
|
ks.risk_level
|
|
48
48
|
FROM rapport.knowledge_silos ks
|
|
49
49
|
JOIN rapport.git_repositories r ON ks.repo_id = r.repo_id
|
|
50
|
-
WHERE r.
|
|
50
|
+
WHERE r.company_id = $1
|
|
51
51
|
ORDER BY
|
|
52
52
|
CASE ks.risk_level
|
|
53
53
|
WHEN 'critical' THEN 1
|
|
@@ -74,7 +74,7 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
|
|
|
74
74
|
pr.lines_added + pr.lines_removed as total_changes
|
|
75
75
|
FROM rapport.pull_requests pr
|
|
76
76
|
JOIN rapport.git_repositories r ON pr.repo_id = r.repo_id
|
|
77
|
-
WHERE r.
|
|
77
|
+
WHERE r.company_id = $1
|
|
78
78
|
AND pr.status = 'open'
|
|
79
79
|
AND pr.created_at < NOW() - INTERVAL '7 days'
|
|
80
80
|
ORDER BY pr.created_at ASC
|
|
@@ -95,7 +95,7 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
|
|
|
95
95
|
wp.peak_hour
|
|
96
96
|
FROM rapport.working_patterns wp
|
|
97
97
|
JOIN rapport.git_repositories r ON wp.repo_id = r.repo_id
|
|
98
|
-
WHERE r.
|
|
98
|
+
WHERE r.company_id = $1
|
|
99
99
|
AND wp.period_start >= NOW() - INTERVAL '30 days'
|
|
100
100
|
GROUP BY wp.developer_email, wp.peak_hour
|
|
101
101
|
HAVING SUM(wp.after_hours_commits) > 10 OR SUM(wp.weekend_commits) > 5
|
|
@@ -114,21 +114,41 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
|
|
|
114
114
|
|
|
115
115
|
const riskScore = Math.min(100, criticalSilos * 20 + highSilos * 10 + staleCount * 5 + burnoutCount * 5);
|
|
116
116
|
|
|
117
|
+
const riskLevel = riskScore >= 70 ? 'critical' : riskScore >= 40 ? 'elevated' : 'normal';
|
|
118
|
+
|
|
117
119
|
return createSuccessResponse({
|
|
118
120
|
report_type: 'risk_forecast',
|
|
119
121
|
generated_at: new Date().toISOString(),
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
122
|
+
overall_risk_score: riskScore,
|
|
123
|
+
risk_level: riskLevel,
|
|
124
|
+
summary: {
|
|
125
|
+
inactive_developers: 0,
|
|
126
|
+
high_velocity_devs: burnoutCount,
|
|
127
|
+
critical_silos: criticalSilos,
|
|
128
|
+
stale_prs: staleCount,
|
|
129
|
+
active_alerts: 0,
|
|
130
|
+
high_churn_files: 0
|
|
131
|
+
},
|
|
132
|
+
risks: {
|
|
133
|
+
knowledge_silos: silos.rows,
|
|
134
|
+
inactive_developers: [],
|
|
135
|
+
burnout_risk: burnoutRisk.rows.map(b => ({
|
|
136
|
+
email: b.developer_email,
|
|
137
|
+
name: b.developer_email,
|
|
138
|
+
commits_7d: 0,
|
|
139
|
+
weekend_commits: parseInt(b.weekend_commits) || 0,
|
|
140
|
+
after_hours: parseInt(b.after_hours_commits) || 0
|
|
141
|
+
})),
|
|
142
|
+
review_bottleneck: stalePRs.rows.map(pr => ({
|
|
143
|
+
repo: '',
|
|
144
|
+
pr_number: 0,
|
|
145
|
+
title: pr.title,
|
|
146
|
+
author: pr.author_email,
|
|
147
|
+
days_open: parseInt(pr.days_open) || 0
|
|
148
|
+
})),
|
|
149
|
+
high_churn_files: []
|
|
128
150
|
},
|
|
129
|
-
|
|
130
|
-
stale_pull_requests: stalePRs.rows,
|
|
131
|
-
burnout_indicators: burnoutRisk.rows,
|
|
151
|
+
active_alerts: [],
|
|
132
152
|
recommendations: generateRecommendations(silos.rows, stalePRs.rows, burnoutRisk.rows)
|
|
133
153
|
});
|
|
134
154
|
});
|
|
@@ -20,13 +20,13 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
|
|
|
20
20
|
const period = params.period || '30d';
|
|
21
21
|
const companyId = params.Company_ID;
|
|
22
22
|
|
|
23
|
-
// Validate access - must be manager/admin
|
|
23
|
+
// Validate access - must be manager/admin
|
|
24
24
|
const accessCheck = await executeQuery(`
|
|
25
|
-
SELECT ue.
|
|
26
|
-
FROM
|
|
27
|
-
WHERE ue.
|
|
28
|
-
AND (ue.
|
|
29
|
-
${companyId ? 'AND ue.
|
|
25
|
+
SELECT ue.company_id
|
|
26
|
+
FROM rapport.user_entitlements ue
|
|
27
|
+
WHERE ue.email_address = $1
|
|
28
|
+
AND (ue.admin = true OR ue.manager = true)
|
|
29
|
+
${companyId ? 'AND ue.company_id = $2' : ''}
|
|
30
30
|
LIMIT 1
|
|
31
31
|
`, companyId ? [email, companyId] : [email]);
|
|
32
32
|
|
|
@@ -34,7 +34,7 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
|
|
|
34
34
|
return createErrorResponse(403, 'Manager or Admin access required');
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
const userCompanyId = companyId || accessCheck.rows[0].
|
|
37
|
+
const userCompanyId = companyId || accessCheck.rows[0].company_id;
|
|
38
38
|
const periodDays = parsePeriod(period);
|
|
39
39
|
const periodStart = new Date();
|
|
40
40
|
periodStart.setDate(periodStart.getDate() - periodDays);
|
|
@@ -52,7 +52,7 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
|
|
|
52
52
|
SUM(dm.anti_pattern_commits) as anti_pattern_commits
|
|
53
53
|
FROM rapport.developer_metrics dm
|
|
54
54
|
JOIN rapport.git_repositories r ON dm.repo_id = r.repo_id
|
|
55
|
-
WHERE r.
|
|
55
|
+
WHERE r.company_id = $1
|
|
56
56
|
AND dm.period_start >= $2
|
|
57
57
|
GROUP BY dm.developer_email, dm.developer_name
|
|
58
58
|
HAVING SUM(dm.commit_count) > 0
|
|
@@ -91,6 +91,95 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
|
|
|
91
91
|
const compliantCommits = complianceData.reduce((sum, r) => sum + parseInt(r.compliant_commits || 0), 0);
|
|
92
92
|
const antiPatternCommits = complianceData.reduce((sum, r) => sum + parseInt(r.anti_pattern_commits || 0), 0);
|
|
93
93
|
|
|
94
|
+
// Get standards shown count from session_standards
|
|
95
|
+
let standardsShown = 0;
|
|
96
|
+
try {
|
|
97
|
+
const shownResult = await executeQuery(`
|
|
98
|
+
SELECT COUNT(*) as total
|
|
99
|
+
FROM rapport.session_standards ss
|
|
100
|
+
JOIN rapport.sessions s ON ss.session_id = s.session_id
|
|
101
|
+
JOIN rapport.projects p ON s.project_id = p.project_id
|
|
102
|
+
WHERE p.company_id = $1
|
|
103
|
+
AND s.started_at >= $2
|
|
104
|
+
`, [userCompanyId, periodStart]);
|
|
105
|
+
standardsShown = parseInt(shownResult.rows[0]?.total) || 0;
|
|
106
|
+
} catch (e) {
|
|
107
|
+
// Table might not have data
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Adoption trend — standards injected per day
|
|
111
|
+
let adoptionTrend = [];
|
|
112
|
+
try {
|
|
113
|
+
const trendResult = await executeQuery(`
|
|
114
|
+
SELECT
|
|
115
|
+
DATE(ss.created_at) as date,
|
|
116
|
+
COUNT(*) as standards_shown,
|
|
117
|
+
COUNT(DISTINCT ss.session_id) as sessions,
|
|
118
|
+
COUNT(CASE WHEN ss.followed = true THEN 1 END) as followed,
|
|
119
|
+
COUNT(CASE WHEN ss.violated = true THEN 1 END) as violated
|
|
120
|
+
FROM rapport.session_standards ss
|
|
121
|
+
JOIN rapport.sessions s ON ss.session_id = s.session_id
|
|
122
|
+
JOIN rapport.projects p ON s.project_id = p.project_id
|
|
123
|
+
WHERE p.company_id = $1
|
|
124
|
+
AND ss.created_at >= $2
|
|
125
|
+
GROUP BY DATE(ss.created_at)
|
|
126
|
+
ORDER BY date
|
|
127
|
+
`, [userCompanyId, periodStart]);
|
|
128
|
+
adoptionTrend = trendResult.rows;
|
|
129
|
+
} catch (e) {
|
|
130
|
+
// Table might not have data
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Top standards — most frequently injected
|
|
134
|
+
let topStandards = [];
|
|
135
|
+
try {
|
|
136
|
+
const topResult = await executeQuery(`
|
|
137
|
+
SELECT
|
|
138
|
+
ss.standard_id,
|
|
139
|
+
ss.standard_name,
|
|
140
|
+
COUNT(*) as times_shown,
|
|
141
|
+
COUNT(DISTINCT ss.session_id) as unique_sessions,
|
|
142
|
+
ROUND(AVG(ss.relevance_score), 2) as avg_relevance,
|
|
143
|
+
COUNT(CASE WHEN ss.followed = true THEN 1 END) as times_followed,
|
|
144
|
+
COUNT(CASE WHEN ss.violated = true THEN 1 END) as times_violated
|
|
145
|
+
FROM rapport.session_standards ss
|
|
146
|
+
JOIN rapport.sessions s ON ss.session_id = s.session_id
|
|
147
|
+
JOIN rapport.projects p ON s.project_id = p.project_id
|
|
148
|
+
WHERE p.company_id = $1
|
|
149
|
+
AND ss.created_at >= $2
|
|
150
|
+
GROUP BY ss.standard_id, ss.standard_name
|
|
151
|
+
ORDER BY times_shown DESC
|
|
152
|
+
LIMIT 10
|
|
153
|
+
`, [userCompanyId, periodStart]);
|
|
154
|
+
topStandards = topResult.rows;
|
|
155
|
+
} catch (e) {
|
|
156
|
+
// Table might not have data
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Anti-patterns detected — standards that were violated
|
|
160
|
+
let antiPatternsDetected = [];
|
|
161
|
+
try {
|
|
162
|
+
const violationResult = await executeQuery(`
|
|
163
|
+
SELECT
|
|
164
|
+
ss.standard_id,
|
|
165
|
+
ss.standard_name,
|
|
166
|
+
COUNT(*) as violation_count,
|
|
167
|
+
MAX(ss.created_at) as last_violated
|
|
168
|
+
FROM rapport.session_standards ss
|
|
169
|
+
JOIN rapport.sessions s ON ss.session_id = s.session_id
|
|
170
|
+
JOIN rapport.projects p ON s.project_id = p.project_id
|
|
171
|
+
WHERE p.company_id = $1
|
|
172
|
+
AND ss.created_at >= $2
|
|
173
|
+
AND ss.violated = true
|
|
174
|
+
GROUP BY ss.standard_id, ss.standard_name
|
|
175
|
+
ORDER BY violation_count DESC
|
|
176
|
+
LIMIT 10
|
|
177
|
+
`, [userCompanyId, periodStart]);
|
|
178
|
+
antiPatternsDetected = violationResult.rows;
|
|
179
|
+
} catch (e) {
|
|
180
|
+
// Table might not have data
|
|
181
|
+
}
|
|
182
|
+
|
|
94
183
|
return createSuccessResponse({
|
|
95
184
|
report_type: 'standards_roi',
|
|
96
185
|
period: period,
|
|
@@ -103,9 +192,12 @@ exports.handler = wrapHandler(async ({ requestContext, queryStringParameters })
|
|
|
103
192
|
compliance_rate: totalCommits > 0 ? ((compliantCommits / totalCommits) * 100).toFixed(1) : '0',
|
|
104
193
|
anti_pattern_commits: antiPatternCommits,
|
|
105
194
|
anti_pattern_rate: totalCommits > 0 ? ((antiPatternCommits / totalCommits) * 100).toFixed(1) : '0',
|
|
106
|
-
|
|
195
|
+
standards_shown: standardsShown
|
|
107
196
|
},
|
|
197
|
+
adoption_trend: adoptionTrend,
|
|
108
198
|
compliance_by_developer: complianceData,
|
|
199
|
+
top_standards: topStandards,
|
|
200
|
+
anti_patterns_detected: antiPatternsDetected,
|
|
109
201
|
configured_patterns: patterns.rows.filter(p => p.pattern_type === 'standard'),
|
|
110
202
|
anti_patterns: patterns.rows.filter(p => p.pattern_type === 'anti_pattern'),
|
|
111
203
|
insights: generateInsights(complianceData)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maturity Update Scheduled Job
|
|
3
|
+
* Auto-progresses/demotes standards maturity based on 30-day compliance evidence
|
|
4
|
+
*
|
|
5
|
+
* Schedule: Daily
|
|
6
|
+
* Auth: None (Lambda scheduled event)
|
|
7
|
+
*
|
|
8
|
+
* Maturity progression rules (based on session_standards compliance data):
|
|
9
|
+
* - >90% followed + >50 sessions → enforced
|
|
10
|
+
* - >70% followed + >20 sessions → validated
|
|
11
|
+
* - <30% followed + >20 sessions → demote to provisional
|
|
12
|
+
*
|
|
13
|
+
* Records all transitions in standards_audit_trail for traceability
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { wrapHandler, executeQuery, createSuccessResponse } = require('./helpers');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Main handler
|
|
20
|
+
*/
|
|
21
|
+
exports.handler = wrapHandler(async (event, context) => {
|
|
22
|
+
console.log('[MaturityUpdateJob] Starting maturity auto-progression...');
|
|
23
|
+
|
|
24
|
+
const lookbackDays = (event && event.lookbackDays) || 30;
|
|
25
|
+
const dryRun = (event && event.dryRun) || false;
|
|
26
|
+
|
|
27
|
+
const summary = {
|
|
28
|
+
startedAt: new Date().toISOString(),
|
|
29
|
+
standardsEvaluated: 0,
|
|
30
|
+
promotions: 0,
|
|
31
|
+
demotions: 0,
|
|
32
|
+
unchanged: 0,
|
|
33
|
+
transitions: [],
|
|
34
|
+
errors: []
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// Query 30-day compliance rates per standard from session_standards
|
|
39
|
+
const complianceQuery = `
|
|
40
|
+
SELECT
|
|
41
|
+
ss.standard_id,
|
|
42
|
+
ss.standard_name,
|
|
43
|
+
COUNT(*) as total_sessions,
|
|
44
|
+
COUNT(*) FILTER (WHERE ss.followed = true) as followed_count,
|
|
45
|
+
COUNT(*) FILTER (WHERE ss.violated = true) as violated_count,
|
|
46
|
+
ROUND(
|
|
47
|
+
COUNT(*) FILTER (WHERE ss.followed = true)::decimal /
|
|
48
|
+
NULLIF(COUNT(*), 0),
|
|
49
|
+
3
|
|
50
|
+
) as follow_rate,
|
|
51
|
+
sp.maturity as current_maturity
|
|
52
|
+
FROM rapport.session_standards ss
|
|
53
|
+
LEFT JOIN rapport.standards_patterns sp ON sp.pattern_id = ss.standard_id
|
|
54
|
+
WHERE ss.shown_at > NOW() - INTERVAL '${lookbackDays} days'
|
|
55
|
+
GROUP BY ss.standard_id, ss.standard_name, sp.maturity
|
|
56
|
+
HAVING COUNT(*) >= 10
|
|
57
|
+
ORDER BY total_sessions DESC
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
const complianceResult = await executeQuery(complianceQuery);
|
|
61
|
+
const standards = complianceResult.rows;
|
|
62
|
+
|
|
63
|
+
summary.standardsEvaluated = standards.length;
|
|
64
|
+
console.log(`[MaturityUpdateJob] Evaluating ${standards.length} standards with sufficient data`);
|
|
65
|
+
|
|
66
|
+
for (const standard of standards) {
|
|
67
|
+
const totalSessions = parseInt(standard.total_sessions);
|
|
68
|
+
const followRate = parseFloat(standard.follow_rate) || 0;
|
|
69
|
+
const currentMaturity = standard.current_maturity || 'provisional';
|
|
70
|
+
|
|
71
|
+
// Determine target maturity
|
|
72
|
+
let targetMaturity = currentMaturity;
|
|
73
|
+
|
|
74
|
+
if (followRate > 0.90 && totalSessions >= 50) {
|
|
75
|
+
targetMaturity = 'enforced';
|
|
76
|
+
} else if (followRate > 0.70 && totalSessions >= 20) {
|
|
77
|
+
targetMaturity = 'validated';
|
|
78
|
+
} else if (followRate < 0.30 && totalSessions >= 20) {
|
|
79
|
+
targetMaturity = 'provisional';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (targetMaturity === currentMaturity) {
|
|
83
|
+
summary.unchanged++;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Determine if this is a promotion or demotion
|
|
88
|
+
const maturityOrder = { provisional: 0, validated: 1, reinforced: 2, enforced: 3 };
|
|
89
|
+
const isPromotion = (maturityOrder[targetMaturity] || 0) > (maturityOrder[currentMaturity] || 0);
|
|
90
|
+
|
|
91
|
+
const transition = {
|
|
92
|
+
standard_id: standard.standard_id,
|
|
93
|
+
standard_name: standard.standard_name,
|
|
94
|
+
from: currentMaturity,
|
|
95
|
+
to: targetMaturity,
|
|
96
|
+
direction: isPromotion ? 'promotion' : 'demotion',
|
|
97
|
+
follow_rate: followRate,
|
|
98
|
+
total_sessions: totalSessions
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
summary.transitions.push(transition);
|
|
102
|
+
|
|
103
|
+
if (dryRun) {
|
|
104
|
+
console.log(`[MaturityUpdateJob] DRY RUN: Would ${transition.direction} ${standard.standard_id}: ${currentMaturity} → ${targetMaturity} (${(followRate * 100).toFixed(1)}% followed, ${totalSessions} sessions)`);
|
|
105
|
+
if (isPromotion) summary.promotions++;
|
|
106
|
+
else summary.demotions++;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
// Update standards_patterns maturity
|
|
112
|
+
await executeQuery(`
|
|
113
|
+
UPDATE rapport.standards_patterns
|
|
114
|
+
SET maturity = $1, last_updated = NOW()
|
|
115
|
+
WHERE pattern_id = $2
|
|
116
|
+
`, [targetMaturity, standard.standard_id]);
|
|
117
|
+
|
|
118
|
+
// Record in audit trail
|
|
119
|
+
await executeQuery(`
|
|
120
|
+
INSERT INTO rapport.standards_audit_trail (
|
|
121
|
+
standard_id, action, old_state, new_state,
|
|
122
|
+
user_email, reason, metadata, created_at
|
|
123
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
|
|
124
|
+
`, [
|
|
125
|
+
standard.standard_id,
|
|
126
|
+
isPromotion ? 'auto_promote' : 'auto_demote',
|
|
127
|
+
currentMaturity,
|
|
128
|
+
targetMaturity,
|
|
129
|
+
'system@mindmeld.dev',
|
|
130
|
+
`Auto-${transition.direction}: ${(followRate * 100).toFixed(1)}% follow rate across ${totalSessions} sessions (30-day window)`,
|
|
131
|
+
JSON.stringify({
|
|
132
|
+
follow_rate: followRate,
|
|
133
|
+
total_sessions: totalSessions,
|
|
134
|
+
followed_count: parseInt(standard.followed_count),
|
|
135
|
+
violated_count: parseInt(standard.violated_count),
|
|
136
|
+
lookback_days: lookbackDays
|
|
137
|
+
})
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
if (isPromotion) summary.promotions++;
|
|
141
|
+
else summary.demotions++;
|
|
142
|
+
|
|
143
|
+
console.log(`[MaturityUpdateJob] ${transition.direction}: ${standard.standard_id} ${currentMaturity} → ${targetMaturity}`);
|
|
144
|
+
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.error(`[MaturityUpdateJob] Error updating ${standard.standard_id}:`, error.message);
|
|
147
|
+
summary.errors.push({
|
|
148
|
+
standard_id: standard.standard_id,
|
|
149
|
+
error: error.message
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error('[MaturityUpdateJob] Job failed:', error);
|
|
156
|
+
summary.errors.push({ step: 'main', error: error.message });
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
summary.completedAt = new Date().toISOString();
|
|
160
|
+
console.log(`[MaturityUpdateJob] Complete: ${summary.promotions} promotions, ${summary.demotions} demotions, ${summary.unchanged} unchanged`);
|
|
161
|
+
|
|
162
|
+
return createSuccessResponse(
|
|
163
|
+
summary,
|
|
164
|
+
`Maturity update complete: ${summary.promotions} promotions, ${summary.demotions} demotions`
|
|
165
|
+
);
|
|
166
|
+
});
|
|
@@ -41,7 +41,7 @@ async function recordSessionStandards({ body: requestBody = {}, requestContext }
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
// Try to ensure session exists (upsert with minimal data)
|
|
44
|
-
//
|
|
44
|
+
// If project doesn't exist, auto-create it to prevent silent session loss
|
|
45
45
|
if (project_id && user_id) {
|
|
46
46
|
const sessionUpsertQuery = `
|
|
47
47
|
INSERT INTO rapport.sessions (
|
|
@@ -59,9 +59,42 @@ async function recordSessionStandards({ body: requestBody = {}, requestContext }
|
|
|
59
59
|
try {
|
|
60
60
|
await executeQuery(sessionUpsertQuery, [session_id, project_id, user_id]);
|
|
61
61
|
} catch (sessionError) {
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
// FK constraint on project_id — auto-create the project and retry
|
|
63
|
+
if (sessionError.code === '23503' && sessionError.constraint && sessionError.constraint.includes('project_id')) {
|
|
64
|
+
console.log(`[sessionStandardsPost] Project ${project_id} not found, auto-creating`);
|
|
65
|
+
try {
|
|
66
|
+
// Look up user's company_id
|
|
67
|
+
const entResult = await executeQuery(
|
|
68
|
+
`SELECT company_id FROM rapport.user_entitlements WHERE email_address = $1 LIMIT 1`,
|
|
69
|
+
[user_id]
|
|
70
|
+
);
|
|
71
|
+
if (entResult.rowCount > 0) {
|
|
72
|
+
const company_id = entResult.rows[0].company_id;
|
|
73
|
+
// Derive a readable project name from the project_id
|
|
74
|
+
// e.g. prj_pareidolia_main_1770727596802 → pareidolia main
|
|
75
|
+
const projectName = project_id
|
|
76
|
+
.replace(/^prj_/, '')
|
|
77
|
+
.replace(/_\d+$/, '')
|
|
78
|
+
.replace(/_/g, ' ');
|
|
79
|
+
|
|
80
|
+
await executeQuery(`
|
|
81
|
+
INSERT INTO rapport.projects (project_id, company_id, project_name, private)
|
|
82
|
+
VALUES ($1, $2, $3, false)
|
|
83
|
+
ON CONFLICT (project_id) DO NOTHING
|
|
84
|
+
`, [project_id, company_id, projectName]);
|
|
85
|
+
|
|
86
|
+
// Retry session upsert
|
|
87
|
+
await executeQuery(sessionUpsertQuery, [session_id, project_id, user_id]);
|
|
88
|
+
console.log(`[sessionStandardsPost] Auto-created project ${project_id} under ${company_id}`);
|
|
89
|
+
} else {
|
|
90
|
+
console.error(`[sessionStandardsPost] No entitlement found for ${user_id}, cannot auto-create project`);
|
|
91
|
+
}
|
|
92
|
+
} catch (autoCreateError) {
|
|
93
|
+
console.error('[sessionStandardsPost] Auto-create project failed:', autoCreateError.message);
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
console.error('[sessionStandardsPost] Session upsert failed:', sessionError.message);
|
|
97
|
+
}
|
|
65
98
|
}
|
|
66
99
|
}
|
|
67
100
|
|
|
@@ -71,6 +104,7 @@ async function recordSessionStandards({ body: requestBody = {}, requestContext }
|
|
|
71
104
|
|
|
72
105
|
for (const standard of standards) {
|
|
73
106
|
const standardId = standard.pattern_id || standard.element;
|
|
107
|
+
const standardName = standard.title || standard.element || standardId;
|
|
74
108
|
const relevanceScore = standard.relevance_score || standard.score || 0;
|
|
75
109
|
|
|
76
110
|
if (!standardId) {
|
|
@@ -82,14 +116,15 @@ async function recordSessionStandards({ body: requestBody = {}, requestContext }
|
|
|
82
116
|
INSERT INTO rapport.session_standards (
|
|
83
117
|
session_id,
|
|
84
118
|
standard_id,
|
|
119
|
+
standard_name,
|
|
85
120
|
relevance_score,
|
|
86
|
-
|
|
121
|
+
created_at
|
|
87
122
|
) VALUES (
|
|
88
|
-
$1, $2, $3, NOW()
|
|
123
|
+
$1, $2, $3, $4, NOW()
|
|
89
124
|
)
|
|
90
125
|
ON CONFLICT (session_id, standard_id) DO UPDATE SET
|
|
91
126
|
relevance_score = EXCLUDED.relevance_score,
|
|
92
|
-
|
|
127
|
+
created_at = NOW()
|
|
93
128
|
RETURNING id
|
|
94
129
|
`;
|
|
95
130
|
|
|
@@ -97,6 +132,7 @@ async function recordSessionStandards({ body: requestBody = {}, requestContext }
|
|
|
97
132
|
const result = await executeQuery(insertQuery, [
|
|
98
133
|
session_id,
|
|
99
134
|
standardId,
|
|
135
|
+
standardName,
|
|
100
136
|
relevanceScore
|
|
101
137
|
]);
|
|
102
138
|
|