@equilateral_ai/mindmeld 3.3.1 → 3.5.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-end.js +112 -3
- 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/mindmeldMcpCore.js +594 -0
- package/src/handlers/helpers/subscriptionTiers.js +27 -27
- package/src/handlers/mcp/mcpHandler.js +569 -0
- package/src/handlers/mcp/mindmeldMcpHandler.js +124 -0
- package/src/handlers/mcp/mindmeldMcpStreamHandler.js +243 -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 +15 -11
- package/src/handlers/webhooks/githubWebhook.js +117 -125
- package/src/index.js +8 -5
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Control Tower Handler
|
|
3
|
+
* Unified org health dashboard for enterprise tier
|
|
4
|
+
*
|
|
5
|
+
* GET /api/enterprise/control-tower
|
|
6
|
+
* Auth: Cognito JWT required, Enterprise tier
|
|
7
|
+
*
|
|
8
|
+
* Aggregates cross-project health metrics:
|
|
9
|
+
* - Organization health score
|
|
10
|
+
* - Standards compliance across projects
|
|
11
|
+
* - Risk indicators (stale devs, violations, coverage gaps)
|
|
12
|
+
* - Governance activity (audit events, knowledge items)
|
|
13
|
+
* - Project-level breakdown
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { wrapHandler, executeQuery, createSuccessResponse, createErrorResponse } = require('./helpers');
|
|
17
|
+
|
|
18
|
+
async function getControlTower({ requestContext, queryStringParameters }) {
|
|
19
|
+
const email = requestContext.authorizer?.claims?.email || requestContext.authorizer?.jwt?.claims?.email;
|
|
20
|
+
|
|
21
|
+
if (!email) {
|
|
22
|
+
return createErrorResponse(401, 'Authentication required');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Verify enterprise admin/manager access
|
|
26
|
+
const accessCheck = await executeQuery(`
|
|
27
|
+
SELECT ue.company_id, ue.admin, ue.manager, c.subscription_tier, co.company_name
|
|
28
|
+
FROM rapport.user_entitlements ue
|
|
29
|
+
JOIN rapport.clients c ON ue.client_id = c.client_id
|
|
30
|
+
JOIN rapport.companies co ON ue.company_id = co.company_id
|
|
31
|
+
WHERE ue.email_address = $1
|
|
32
|
+
AND (ue.admin = true OR ue.manager = true)
|
|
33
|
+
LIMIT 1
|
|
34
|
+
`, [email]);
|
|
35
|
+
|
|
36
|
+
if (accessCheck.rows.length === 0) {
|
|
37
|
+
return createErrorResponse(403, 'Admin or Manager access required for Control Tower');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { company_id: companyId, company_name: companyName } = accessCheck.rows[0];
|
|
41
|
+
const periodDays = 30;
|
|
42
|
+
const periodStart = new Date();
|
|
43
|
+
periodStart.setDate(periodStart.getDate() - periodDays);
|
|
44
|
+
|
|
45
|
+
// 1. Team health metrics
|
|
46
|
+
let teamHealth = { total: 0, active: 0, stale: 0, very_stale: 0, no_sessions: 0 };
|
|
47
|
+
try {
|
|
48
|
+
const teamResult = await executeQuery(`
|
|
49
|
+
SELECT
|
|
50
|
+
COUNT(DISTINCT ue.email_address) as total,
|
|
51
|
+
COUNT(DISTINCT CASE WHEN mda.sessions_last_30d > 0 AND mda.commits_last_30d > 0 THEN ue.email_address END) as active,
|
|
52
|
+
COUNT(DISTINCT CASE WHEN mda.days_since_last_commit BETWEEN 8 AND 14 THEN ue.email_address END) as stale,
|
|
53
|
+
COUNT(DISTINCT CASE WHEN mda.days_since_last_commit > 14 THEN ue.email_address END) as very_stale,
|
|
54
|
+
COUNT(DISTINCT CASE WHEN mda.sessions_last_30d = 0 OR mda.sessions_last_30d IS NULL THEN ue.email_address END) as no_sessions
|
|
55
|
+
FROM rapport.user_entitlements ue
|
|
56
|
+
LEFT JOIN rapport.mv_developer_activity mda ON ue.email_address = mda.email_address
|
|
57
|
+
WHERE ue.company_id = $1
|
|
58
|
+
`, [companyId]);
|
|
59
|
+
if (teamResult.rows[0]) {
|
|
60
|
+
teamHealth = {
|
|
61
|
+
total: parseInt(teamResult.rows[0].total) || 0,
|
|
62
|
+
active: parseInt(teamResult.rows[0].active) || 0,
|
|
63
|
+
stale: parseInt(teamResult.rows[0].stale) || 0,
|
|
64
|
+
very_stale: parseInt(teamResult.rows[0].very_stale) || 0,
|
|
65
|
+
no_sessions: parseInt(teamResult.rows[0].no_sessions) || 0
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
} catch (e) {
|
|
69
|
+
console.log('[ControlTower] Team health query failed:', e.message);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 2. Project coverage
|
|
73
|
+
let projects = [];
|
|
74
|
+
try {
|
|
75
|
+
const projectResult = await executeQuery(`
|
|
76
|
+
SELECT
|
|
77
|
+
p.project_id,
|
|
78
|
+
p.project_name,
|
|
79
|
+
p.repo_url,
|
|
80
|
+
COUNT(DISTINCT s.session_id) as sessions_30d,
|
|
81
|
+
COUNT(DISTINCT ss.standard_id) as standards_used,
|
|
82
|
+
MAX(s.started_at) as last_session
|
|
83
|
+
FROM rapport.projects p
|
|
84
|
+
LEFT JOIN rapport.sessions s ON p.project_id = s.project_id AND s.started_at >= $2
|
|
85
|
+
LEFT JOIN rapport.session_standards ss ON s.session_id = ss.session_id AND ss.created_at >= $2
|
|
86
|
+
WHERE p.company_id = $1
|
|
87
|
+
GROUP BY p.project_id, p.project_name, p.repo_url
|
|
88
|
+
ORDER BY sessions_30d DESC
|
|
89
|
+
`, [companyId, periodStart]);
|
|
90
|
+
projects = projectResult.rows.map(row => ({
|
|
91
|
+
project_id: row.project_id,
|
|
92
|
+
project_name: row.project_name,
|
|
93
|
+
repo_connected: !!row.repo_url,
|
|
94
|
+
sessions_30d: parseInt(row.sessions_30d) || 0,
|
|
95
|
+
standards_used: parseInt(row.standards_used) || 0,
|
|
96
|
+
last_session: row.last_session,
|
|
97
|
+
status: parseInt(row.sessions_30d) > 0 ? 'active' : 'inactive'
|
|
98
|
+
}));
|
|
99
|
+
} catch (e) {
|
|
100
|
+
console.log('[ControlTower] Projects query failed:', e.message);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 3. Standards coverage (how many unique standards used across org)
|
|
104
|
+
let standardsCoverage = { total_injections: 0, unique_standards: 0, sessions_with_standards: 0 };
|
|
105
|
+
try {
|
|
106
|
+
const stdResult = await executeQuery(`
|
|
107
|
+
SELECT
|
|
108
|
+
COUNT(*) as total_injections,
|
|
109
|
+
COUNT(DISTINCT ss.standard_id) as unique_standards,
|
|
110
|
+
COUNT(DISTINCT ss.session_id) as sessions_with_standards
|
|
111
|
+
FROM rapport.session_standards ss
|
|
112
|
+
JOIN rapport.sessions s ON ss.session_id = s.session_id
|
|
113
|
+
JOIN rapport.projects p ON s.project_id = p.project_id
|
|
114
|
+
WHERE p.company_id = $1
|
|
115
|
+
AND ss.created_at >= $2
|
|
116
|
+
`, [companyId, periodStart]);
|
|
117
|
+
if (stdResult.rows[0]) {
|
|
118
|
+
standardsCoverage = {
|
|
119
|
+
total_injections: parseInt(stdResult.rows[0].total_injections) || 0,
|
|
120
|
+
unique_standards: parseInt(stdResult.rows[0].unique_standards) || 0,
|
|
121
|
+
sessions_with_standards: parseInt(stdResult.rows[0].sessions_with_standards) || 0
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
} catch (e) {
|
|
125
|
+
console.log('[ControlTower] Standards coverage query failed:', e.message);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 4. Governance activity (audit events, knowledge items)
|
|
129
|
+
let governance = { audit_events_30d: 0, knowledge_items: 0, published_knowledge: 0 };
|
|
130
|
+
try {
|
|
131
|
+
const auditResult = await executeQuery(`
|
|
132
|
+
SELECT COUNT(*) as count
|
|
133
|
+
FROM rapport.unified_audit_log
|
|
134
|
+
WHERE client_id = (
|
|
135
|
+
SELECT client_id FROM rapport.user_entitlements WHERE company_id = $1 LIMIT 1
|
|
136
|
+
)
|
|
137
|
+
AND created_at >= $2
|
|
138
|
+
`, [companyId, periodStart]);
|
|
139
|
+
governance.audit_events_30d = parseInt(auditResult.rows[0]?.count) || 0;
|
|
140
|
+
} catch (e) {
|
|
141
|
+
console.log('[ControlTower] Audit events query failed:', e.message);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
const knowledgeResult = await executeQuery(`
|
|
146
|
+
SELECT
|
|
147
|
+
COUNT(*) as total,
|
|
148
|
+
COUNT(CASE WHEN status = 'published' THEN 1 END) as published
|
|
149
|
+
FROM rapport.curated_knowledge
|
|
150
|
+
WHERE client_id = (
|
|
151
|
+
SELECT client_id FROM rapport.user_entitlements WHERE company_id = $1 LIMIT 1
|
|
152
|
+
)
|
|
153
|
+
`, [companyId]);
|
|
154
|
+
governance.knowledge_items = parseInt(knowledgeResult.rows[0]?.total) || 0;
|
|
155
|
+
governance.published_knowledge = parseInt(knowledgeResult.rows[0]?.published) || 0;
|
|
156
|
+
} catch (e) {
|
|
157
|
+
console.log('[ControlTower] Knowledge query failed:', e.message);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 5. Risk indicators
|
|
161
|
+
const risks = [];
|
|
162
|
+
if (teamHealth.very_stale > 0) {
|
|
163
|
+
risks.push({
|
|
164
|
+
severity: 'high',
|
|
165
|
+
category: 'team',
|
|
166
|
+
message: `${teamHealth.very_stale} developer${teamHealth.very_stale > 1 ? 's' : ''} inactive for 14+ days`
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
if (teamHealth.no_sessions > teamHealth.total * 0.3 && teamHealth.total > 1) {
|
|
170
|
+
risks.push({
|
|
171
|
+
severity: 'medium',
|
|
172
|
+
category: 'adoption',
|
|
173
|
+
message: `${teamHealth.no_sessions} of ${teamHealth.total} developers have no AI sessions in 30 days`
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
const projectsWithoutStandards = projects.filter(p => p.sessions_30d > 0 && p.standards_used === 0);
|
|
177
|
+
if (projectsWithoutStandards.length > 0) {
|
|
178
|
+
risks.push({
|
|
179
|
+
severity: 'medium',
|
|
180
|
+
category: 'coverage',
|
|
181
|
+
message: `${projectsWithoutStandards.length} active project${projectsWithoutStandards.length > 1 ? 's' : ''} with no standards injection`
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
const inactiveProjects = projects.filter(p => p.status === 'inactive');
|
|
185
|
+
if (inactiveProjects.length > projects.length * 0.5 && projects.length > 1) {
|
|
186
|
+
risks.push({
|
|
187
|
+
severity: 'low',
|
|
188
|
+
category: 'projects',
|
|
189
|
+
message: `${inactiveProjects.length} of ${projects.length} projects had no sessions in 30 days`
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 6. Calculate org health score (0-100)
|
|
194
|
+
let healthScore = 50; // baseline
|
|
195
|
+
if (teamHealth.total > 0) {
|
|
196
|
+
const activeRate = teamHealth.active / teamHealth.total;
|
|
197
|
+
healthScore = Math.round(activeRate * 40); // up to 40 points for team activity
|
|
198
|
+
}
|
|
199
|
+
if (projects.length > 0) {
|
|
200
|
+
const coveredProjects = projects.filter(p => p.sessions_30d > 0).length;
|
|
201
|
+
healthScore += Math.round((coveredProjects / projects.length) * 30); // up to 30 for project coverage
|
|
202
|
+
}
|
|
203
|
+
if (standardsCoverage.unique_standards > 0) {
|
|
204
|
+
healthScore += Math.min(standardsCoverage.unique_standards * 2, 20); // up to 20 for standards breadth
|
|
205
|
+
}
|
|
206
|
+
if (governance.published_knowledge > 0) {
|
|
207
|
+
healthScore += Math.min(governance.published_knowledge, 10); // up to 10 for knowledge curation
|
|
208
|
+
}
|
|
209
|
+
healthScore = Math.min(healthScore, 100);
|
|
210
|
+
|
|
211
|
+
return createSuccessResponse({
|
|
212
|
+
company_name: companyName,
|
|
213
|
+
health_score: healthScore,
|
|
214
|
+
team: teamHealth,
|
|
215
|
+
projects,
|
|
216
|
+
standards_coverage: standardsCoverage,
|
|
217
|
+
governance,
|
|
218
|
+
risks,
|
|
219
|
+
period_days: periodDays,
|
|
220
|
+
period_start: periodStart.toISOString()
|
|
221
|
+
}, 'Control Tower data retrieved');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
exports.handler = wrapHandler(getControlTower);
|
|
@@ -31,8 +31,8 @@ async function enterpriseOnboardingSetup({ requestContext, body }) {
|
|
|
31
31
|
return createErrorResponse(403, 'User not found or no company assigned');
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
const { client_id, company_id } = userResult.rows[0];
|
|
35
|
-
if (!
|
|
34
|
+
const { client_id, company_id: currentCompanyId } = userResult.rows[0];
|
|
35
|
+
if (!currentCompanyId) {
|
|
36
36
|
return createErrorResponse(400, 'No company associated with this account');
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -55,13 +55,52 @@ async function enterpriseOnboardingSetup({ requestContext, body }) {
|
|
|
55
55
|
const industry = body?.industry || null;
|
|
56
56
|
const companySize = body?.company_size || null;
|
|
57
57
|
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
58
|
+
// Generate a proper enterprise company_id from company name
|
|
59
|
+
const enterpriseCompanyId = `${companyName.replace(/[^a-zA-Z0-9]/g, '_').toUpperCase()}_MAIN`;
|
|
60
|
+
let company_id = currentCompanyId;
|
|
61
|
+
|
|
62
|
+
if (enterpriseCompanyId !== currentCompanyId) {
|
|
63
|
+
// Create the enterprise company (or update if it already exists)
|
|
64
|
+
await executeQuery(
|
|
65
|
+
`INSERT INTO rapport.companies (company_id, client_id, company_name, company_status, domain, industry, company_size)
|
|
66
|
+
VALUES ($1, $2, $3, 'Active', $4, $5, $6)
|
|
67
|
+
ON CONFLICT (company_id) DO UPDATE SET
|
|
68
|
+
company_name = $3, domain = $4, industry = $5, company_size = $6, last_updated = NOW()`,
|
|
69
|
+
[enterpriseCompanyId, client_id, companyName, domain, industry, companySize]
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// Migrate user entitlement from personal to enterprise company
|
|
73
|
+
await executeQuery(
|
|
74
|
+
`UPDATE rapport.user_entitlements
|
|
75
|
+
SET company_id = $1
|
|
76
|
+
WHERE email_address = $2 AND company_id = $3`,
|
|
77
|
+
[enterpriseCompanyId, email, currentCompanyId]
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// Clean up orphaned personal company if nothing else references it
|
|
81
|
+
await executeQuery(
|
|
82
|
+
`DELETE FROM rapport.companies
|
|
83
|
+
WHERE company_id = $1
|
|
84
|
+
AND NOT EXISTS (
|
|
85
|
+
SELECT 1 FROM rapport.user_entitlements WHERE company_id = $1
|
|
86
|
+
)
|
|
87
|
+
AND NOT EXISTS (
|
|
88
|
+
SELECT 1 FROM rapport.projects WHERE company_id = $1
|
|
89
|
+
)`,
|
|
90
|
+
[currentCompanyId]
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
company_id = enterpriseCompanyId;
|
|
94
|
+
console.log(`Migrated company: ${currentCompanyId} -> ${enterpriseCompanyId}`);
|
|
95
|
+
} else {
|
|
96
|
+
// Same company_id, just update profile
|
|
97
|
+
await executeQuery(
|
|
98
|
+
`UPDATE rapport.companies
|
|
99
|
+
SET company_name = $1, domain = $2, industry = $3, company_size = $4, last_updated = NOW()
|
|
100
|
+
WHERE company_id = $5`,
|
|
101
|
+
[companyName, domain, industry, companySize, company_id]
|
|
102
|
+
);
|
|
103
|
+
}
|
|
65
104
|
|
|
66
105
|
// Upsert onboarding status
|
|
67
106
|
await executeQuery(
|
|
@@ -59,9 +59,7 @@ async function getEnterpriseOnboardingStatus({ requestContext }) {
|
|
|
59
59
|
|
|
60
60
|
// Get onboarding status
|
|
61
61
|
const statusResult = await executeQuery(
|
|
62
|
-
`SELECT
|
|
63
|
-
standards_configured, repo_connected, completed,
|
|
64
|
-
started_at, completed_at, completed_by
|
|
62
|
+
`SELECT *
|
|
65
63
|
FROM rapport.enterprise_onboarding_status
|
|
66
64
|
WHERE company_id = $1`,
|
|
67
65
|
[company_id]
|
|
@@ -30,7 +30,7 @@ async function githubConnectionStatus({ requestContext }) {
|
|
|
30
30
|
SELECT COUNT(*) as count FROM rapport.projects p
|
|
31
31
|
JOIN rapport.project_collaborators pc ON p.project_id = pc.project_id
|
|
32
32
|
WHERE pc.email_address = $1
|
|
33
|
-
AND p.
|
|
33
|
+
AND p.repo_url IS NOT NULL
|
|
34
34
|
AND p.archived = FALSE
|
|
35
35
|
`, [email]);
|
|
36
36
|
|
|
@@ -585,18 +585,20 @@ async function githubDiscoverPatterns({ body, requestContext }) {
|
|
|
585
585
|
WHERE project_id = $3
|
|
586
586
|
`, [owner, repo, project_id]);
|
|
587
587
|
|
|
588
|
-
// Save discoveries to DB
|
|
588
|
+
// Save discoveries to DB and capture generated IDs
|
|
589
589
|
for (const discovery of validDiscoveries) {
|
|
590
|
-
await executeQuery(`
|
|
590
|
+
const insertResult = await executeQuery(`
|
|
591
591
|
INSERT INTO rapport.onboarding_discoveries (
|
|
592
592
|
project_id, email_address, discovery_type, pattern_name,
|
|
593
593
|
pattern_description, confidence, evidence
|
|
594
594
|
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
595
|
+
RETURNING discovery_id
|
|
595
596
|
`, [
|
|
596
597
|
project_id, email, discovery.discovery_type, discovery.pattern_name,
|
|
597
598
|
discovery.pattern_description, discovery.confidence,
|
|
598
599
|
JSON.stringify(discovery.evidence)
|
|
599
600
|
]);
|
|
601
|
+
discovery.discovery_id = insertResult.rows[0]?.discovery_id;
|
|
600
602
|
}
|
|
601
603
|
|
|
602
604
|
return createSuccessResponse({
|
|
@@ -36,46 +36,17 @@ async function githubPatternsReview({ body, requestContext }) {
|
|
|
36
36
|
|
|
37
37
|
for (const { discovery_id, approved } of approvals) {
|
|
38
38
|
if (!discovery_id) continue;
|
|
39
|
+
// Skip frontend-generated placeholder IDs (disc_0, disc_1, etc.)
|
|
40
|
+
// These occur when the discover endpoint didn't return DB-generated UUIDs
|
|
41
|
+
if (discovery_id.startsWith('disc_')) continue;
|
|
39
42
|
|
|
40
43
|
if (approved) {
|
|
41
|
-
//
|
|
42
|
-
const discoveryResult = await executeQuery(`
|
|
43
|
-
SELECT discovery_type, pattern_name, pattern_description, evidence
|
|
44
|
-
FROM rapport.onboarding_discoveries
|
|
45
|
-
WHERE discovery_id = $1 AND project_id = $2
|
|
46
|
-
`, [discovery_id, project_id]);
|
|
47
|
-
|
|
48
|
-
if (discoveryResult.rowCount === 0) continue;
|
|
49
|
-
|
|
50
|
-
const discovery = discoveryResult.rows[0];
|
|
51
|
-
|
|
52
|
-
// Create pattern entry
|
|
53
|
-
const patternId = `pat_${project_id}_${Date.now()}_${patterns_created}`;
|
|
54
|
-
await executeQuery(`
|
|
55
|
-
INSERT INTO rapport.patterns (
|
|
56
|
-
pattern_id, project_id, intent, constraints, outcome_criteria,
|
|
57
|
-
maturity, discovered_by, pattern_data
|
|
58
|
-
) VALUES ($1, $2, $3, $4, $5, 'provisional', $6, $7)
|
|
59
|
-
`, [
|
|
60
|
-
patternId,
|
|
61
|
-
project_id,
|
|
62
|
-
discovery.pattern_name,
|
|
63
|
-
JSON.stringify([discovery.pattern_description || '']),
|
|
64
|
-
JSON.stringify([`Discovered via GitHub onboarding: ${discovery.discovery_type}`]),
|
|
65
|
-
email,
|
|
66
|
-
JSON.stringify({
|
|
67
|
-
source: 'github_onboarding',
|
|
68
|
-
discovery_type: discovery.discovery_type,
|
|
69
|
-
evidence: discovery.evidence
|
|
70
|
-
})
|
|
71
|
-
]);
|
|
72
|
-
|
|
73
|
-
// Update discovery status
|
|
44
|
+
// Update discovery status to approved
|
|
74
45
|
await executeQuery(`
|
|
75
46
|
UPDATE rapport.onboarding_discoveries
|
|
76
|
-
SET status = 'approved', reviewed_at = NOW()
|
|
77
|
-
WHERE discovery_id = $2
|
|
78
|
-
`, [
|
|
47
|
+
SET status = 'approved', reviewed_at = NOW()
|
|
48
|
+
WHERE discovery_id = $1 AND project_id = $2
|
|
49
|
+
`, [discovery_id, project_id]);
|
|
79
50
|
|
|
80
51
|
patterns_created++;
|
|
81
52
|
} else {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Health Check Handler
|
|
3
|
+
* Returns system health status for monitoring
|
|
4
|
+
*
|
|
5
|
+
* GET /api/health
|
|
6
|
+
* Auth: None (public endpoint for external monitors)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { executeQuery } = require('./helpers');
|
|
10
|
+
|
|
11
|
+
exports.handler = async (event) => {
|
|
12
|
+
const headers = {
|
|
13
|
+
'Content-Type': 'application/json',
|
|
14
|
+
'Access-Control-Allow-Origin': '*',
|
|
15
|
+
'Access-Control-Allow-Methods': 'GET,OPTIONS',
|
|
16
|
+
'Access-Control-Allow-Headers': 'Content-Type'
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const result = {
|
|
20
|
+
status: 'ok',
|
|
21
|
+
timestamp: new Date().toISOString(),
|
|
22
|
+
version: process.env.STACK_VERSION || '1.0.0',
|
|
23
|
+
checks: {}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Database connectivity
|
|
27
|
+
try {
|
|
28
|
+
const start = Date.now();
|
|
29
|
+
await executeQuery('SELECT 1 AS ok');
|
|
30
|
+
result.checks.database = {
|
|
31
|
+
status: 'ok',
|
|
32
|
+
latency_ms: Date.now() - start
|
|
33
|
+
};
|
|
34
|
+
} catch (err) {
|
|
35
|
+
result.status = 'degraded';
|
|
36
|
+
result.checks.database = {
|
|
37
|
+
status: 'error',
|
|
38
|
+
message: 'Database connection failed'
|
|
39
|
+
};
|
|
40
|
+
console.error('[Health] Database check failed:', err.message);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Stripe configuration
|
|
44
|
+
result.checks.stripe = {
|
|
45
|
+
status: process.env.STRIPE_SECRET_KEY ? 'configured' : 'not_configured'
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const statusCode = result.status === 'ok' ? 200 : 503;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
statusCode,
|
|
52
|
+
headers,
|
|
53
|
+
body: JSON.stringify(result)
|
|
54
|
+
};
|
|
55
|
+
};
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Super Admin Check Helper
|
|
3
|
-
* Follows Tim-Combo pattern: simple boolean flag in Users table
|
|
4
3
|
*/
|
|
5
4
|
|
|
6
5
|
const { executeQuery } = require('./dbOperations');
|
|
@@ -12,12 +11,12 @@ const { executeQuery } = require('./dbOperations');
|
|
|
12
11
|
*/
|
|
13
12
|
async function isSuperAdmin(email) {
|
|
14
13
|
const query = `
|
|
15
|
-
SELECT
|
|
16
|
-
FROM
|
|
17
|
-
WHERE
|
|
14
|
+
SELECT super_admin
|
|
15
|
+
FROM rapport.users
|
|
16
|
+
WHERE email_address = $1
|
|
18
17
|
`;
|
|
19
18
|
const result = await executeQuery(query, [email]);
|
|
20
|
-
return result.rows.length > 0 && result.rows[0].
|
|
19
|
+
return result.rows.length > 0 && result.rows[0].super_admin === true;
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
/**
|
|
@@ -42,15 +41,15 @@ async function requireSuperAdmin(email) {
|
|
|
42
41
|
async function getUserWithSuperAdminStatus(email) {
|
|
43
42
|
const query = `
|
|
44
43
|
SELECT
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
FROM
|
|
53
|
-
WHERE
|
|
44
|
+
email_address,
|
|
45
|
+
client_id,
|
|
46
|
+
CONCAT(first_name, ' ', last_name) as user_display_name,
|
|
47
|
+
first_name,
|
|
48
|
+
last_name,
|
|
49
|
+
super_admin,
|
|
50
|
+
user_status
|
|
51
|
+
FROM rapport.users
|
|
52
|
+
WHERE email_address = $1
|
|
54
53
|
`;
|
|
55
54
|
const result = await executeQuery(query, [email]);
|
|
56
55
|
return result.rows.length > 0 ? result.rows[0] : null;
|