@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
|
@@ -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;
|
|
@@ -117,9 +117,9 @@ const SUBSCRIPTION_TIERS = {
|
|
|
117
117
|
'community_standards',
|
|
118
118
|
'community_feedback' // Patterns feed back to community repo
|
|
119
119
|
],
|
|
120
|
-
stripeProductId: process.env.STRIPE_PRODUCT_TEAM || '
|
|
121
|
-
stripePriceIdMonthly: process.env.STRIPE_PRICE_TEAM_MONTHLY || '
|
|
122
|
-
stripePriceIdAnnual: process.env.STRIPE_PRICE_TEAM_ANNUAL || '
|
|
120
|
+
stripeProductId: process.env.STRIPE_PRODUCT_TEAM || 'prod_TqxmkB1XGIm9qM',
|
|
121
|
+
stripePriceIdMonthly: process.env.STRIPE_PRICE_TEAM_MONTHLY || 'price_1StFrbQoD4pT2xXuoclFMmzn',
|
|
122
|
+
stripePriceIdAnnual: process.env.STRIPE_PRICE_TEAM_ANNUAL || 'price_1StFrxQoD4pT2xXuBWIZK1gX'
|
|
123
123
|
},
|
|
124
124
|
team_private: {
|
|
125
125
|
name: 'Team Private',
|
|
@@ -141,9 +141,9 @@ const SUBSCRIPTION_TIERS = {
|
|
|
141
141
|
'community_standards',
|
|
142
142
|
'private_patterns' // Patterns do NOT feed back to community
|
|
143
143
|
],
|
|
144
|
-
stripeProductId: process.env.STRIPE_PRODUCT_TEAM_PRIVATE || '
|
|
145
|
-
stripePriceIdMonthly: process.env.STRIPE_PRICE_TEAM_PRIVATE_MONTHLY || '
|
|
146
|
-
stripePriceIdAnnual: process.env.STRIPE_PRICE_TEAM_PRIVATE_ANNUAL || '
|
|
144
|
+
stripeProductId: process.env.STRIPE_PRODUCT_TEAM_PRIVATE || 'prod_TqxmYMX4mxPs6A',
|
|
145
|
+
stripePriceIdMonthly: process.env.STRIPE_PRICE_TEAM_PRIVATE_MONTHLY || 'price_1StFrdQoD4pT2xXupsZM0B3Q',
|
|
146
|
+
stripePriceIdAnnual: process.env.STRIPE_PRICE_TEAM_PRIVATE_ANNUAL || 'price_1StFryQoD4pT2xXuFqVKflKV'
|
|
147
147
|
},
|
|
148
148
|
professional: {
|
|
149
149
|
name: 'Professional',
|
|
@@ -170,9 +170,9 @@ const SUBSCRIPTION_TIERS = {
|
|
|
170
170
|
'aws_standards',
|
|
171
171
|
'community_feedback'
|
|
172
172
|
],
|
|
173
|
-
stripeProductId: process.env.STRIPE_PRODUCT_PROFESSIONAL || '
|
|
174
|
-
stripePriceIdMonthly: process.env.STRIPE_PRICE_PROFESSIONAL_MONTHLY || '
|
|
175
|
-
stripePriceIdAnnual: process.env.STRIPE_PRICE_PROFESSIONAL_ANNUAL || '
|
|
173
|
+
stripeProductId: process.env.STRIPE_PRODUCT_PROFESSIONAL || 'prod_Tqxm0PwlEAFviD',
|
|
174
|
+
stripePriceIdMonthly: process.env.STRIPE_PRICE_PROFESSIONAL_MONTHLY || 'price_1StFriQoD4pT2xXuJVlSx1pr',
|
|
175
|
+
stripePriceIdAnnual: process.env.STRIPE_PRICE_PROFESSIONAL_ANNUAL || 'price_1StFryQoD4pT2xXuPG3jzk0a'
|
|
176
176
|
},
|
|
177
177
|
professional_private: {
|
|
178
178
|
name: 'Professional Private',
|
|
@@ -199,9 +199,9 @@ const SUBSCRIPTION_TIERS = {
|
|
|
199
199
|
'aws_standards',
|
|
200
200
|
'private_patterns'
|
|
201
201
|
],
|
|
202
|
-
stripeProductId: process.env.STRIPE_PRODUCT_PROFESSIONAL_PRIVATE || '
|
|
203
|
-
stripePriceIdMonthly: process.env.STRIPE_PRICE_PROFESSIONAL_PRIVATE_MONTHLY || '
|
|
204
|
-
stripePriceIdAnnual: process.env.STRIPE_PRICE_PROFESSIONAL_PRIVATE_ANNUAL || '
|
|
202
|
+
stripeProductId: process.env.STRIPE_PRODUCT_PROFESSIONAL_PRIVATE || 'prod_TqxmNJrJRVXSKC',
|
|
203
|
+
stripePriceIdMonthly: process.env.STRIPE_PRICE_PROFESSIONAL_PRIVATE_MONTHLY || 'price_1StFrjQoD4pT2xXunEPrL5e0',
|
|
204
|
+
stripePriceIdAnnual: process.env.STRIPE_PRICE_PROFESSIONAL_PRIVATE_ANNUAL || 'price_1StFrzQoD4pT2xXupWDgIquo'
|
|
205
205
|
},
|
|
206
206
|
enterprise: {
|
|
207
207
|
name: 'Enterprise',
|
|
@@ -234,7 +234,7 @@ const SUBSCRIPTION_TIERS = {
|
|
|
234
234
|
'dedicated_support',
|
|
235
235
|
'custom_integrations'
|
|
236
236
|
],
|
|
237
|
-
stripeProductId: process.env.STRIPE_PRODUCT_ENTERPRISE || '
|
|
237
|
+
stripeProductId: process.env.STRIPE_PRODUCT_ENTERPRISE || 'prod_Tqxmy47rFwQqHf',
|
|
238
238
|
stripePriceIdMonthly: null, // Contact sales
|
|
239
239
|
stripePriceIdAnnual: null // Contact sales
|
|
240
240
|
}
|
|
@@ -276,7 +276,7 @@ function checkSubscriptionLimits(client, action, counts) {
|
|
|
276
276
|
switch (action) {
|
|
277
277
|
case 'add_collaborator':
|
|
278
278
|
const collabLimit = tier.maxCollaborators;
|
|
279
|
-
if (counts.collaborators >= collabLimit) {
|
|
279
|
+
if (collabLimit !== null && counts.collaborators >= collabLimit) {
|
|
280
280
|
return {
|
|
281
281
|
allowed: false,
|
|
282
282
|
message: `${tier.displayName} tier allows up to ${collabLimit} collaborators. Upgrade to add more.`,
|
|
@@ -287,7 +287,7 @@ function checkSubscriptionLimits(client, action, counts) {
|
|
|
287
287
|
|
|
288
288
|
case 'create_project':
|
|
289
289
|
const projectLimit = tier.maxProjects;
|
|
290
|
-
if (counts.projects >= projectLimit) {
|
|
290
|
+
if (projectLimit !== null && counts.projects >= projectLimit) {
|
|
291
291
|
return {
|
|
292
292
|
allowed: false,
|
|
293
293
|
message: `${tier.displayName} tier allows up to ${projectLimit} projects. Upgrade to create more.`,
|
|
@@ -743,9 +743,9 @@ const ENTERPRISE_PACKAGES = {
|
|
|
743
743
|
'admin_dashboard',
|
|
744
744
|
'all_professional_features'
|
|
745
745
|
],
|
|
746
|
-
stripeProductId: process.env.STRIPE_PRODUCT_ENTERPRISE || '
|
|
747
|
-
stripePriceIdMonthly25: process.env.STRIPE_PRICE_ENT_BASE_MONTHLY || '
|
|
748
|
-
stripePriceIdAnnual25: process.env.STRIPE_PRICE_ENT_BASE_ANNUAL || '
|
|
746
|
+
stripeProductId: process.env.STRIPE_PRODUCT_ENTERPRISE || 'prod_Tqxmy47rFwQqHf',
|
|
747
|
+
stripePriceIdMonthly25: process.env.STRIPE_PRICE_ENT_BASE_MONTHLY || 'price_1StFyhQoD4pT2xXusUSHq6au',
|
|
748
|
+
stripePriceIdAnnual25: process.env.STRIPE_PRICE_ENT_BASE_ANNUAL || 'price_1StFyiQoD4pT2xXuevv2PYYB'
|
|
749
749
|
},
|
|
750
750
|
full: {
|
|
751
751
|
name: 'Full Platform',
|
|
@@ -772,9 +772,9 @@ const ENTERPRISE_PACKAGES = {
|
|
|
772
772
|
'onboarding_acceleration',
|
|
773
773
|
'knowledge_transfer_docs'
|
|
774
774
|
],
|
|
775
|
-
stripeProductId: process.env.STRIPE_PRODUCT_ENTERPRISE || '
|
|
776
|
-
stripePriceIdMonthly25: process.env.STRIPE_PRICE_ENT_FULL_MONTHLY || '
|
|
777
|
-
stripePriceIdAnnual25: process.env.STRIPE_PRICE_ENT_FULL_ANNUAL || '
|
|
775
|
+
stripeProductId: process.env.STRIPE_PRODUCT_ENTERPRISE || 'prod_Tqxmy47rFwQqHf',
|
|
776
|
+
stripePriceIdMonthly25: process.env.STRIPE_PRICE_ENT_FULL_MONTHLY || 'price_1StFykQoD4pT2xXuuOnck3Gc',
|
|
777
|
+
stripePriceIdAnnual25: process.env.STRIPE_PRICE_ENT_FULL_ANNUAL || 'price_1StFylQoD4pT2xXuv7JJXYZN'
|
|
778
778
|
}
|
|
779
779
|
};
|
|
780
780
|
|
|
@@ -797,9 +797,9 @@ const ENTERPRISE_ADDONS = {
|
|
|
797
797
|
'session_commit_correlation',
|
|
798
798
|
'roi_reporting'
|
|
799
799
|
],
|
|
800
|
-
stripeProductId: process.env.STRIPE_PRODUCT_ADDON_ENG_INTEL || '
|
|
801
|
-
stripePriceIdMonthly25: process.env.STRIPE_PRICE_ADDON_ENG_INTEL_MONTHLY || '
|
|
802
|
-
stripePriceIdAnnual25: process.env.STRIPE_PRICE_ADDON_ENG_INTEL_ANNUAL || '
|
|
800
|
+
stripeProductId: process.env.STRIPE_PRODUCT_ADDON_ENG_INTEL || 'prod_TqxmsazHo5nJZi',
|
|
801
|
+
stripePriceIdMonthly25: process.env.STRIPE_PRICE_ADDON_ENG_INTEL_MONTHLY || 'price_1StFymQoD4pT2xXuJzHg42Jq',
|
|
802
|
+
stripePriceIdAnnual25: process.env.STRIPE_PRICE_ADDON_ENG_INTEL_ANNUAL || 'price_1StFynQoD4pT2xXuUIpe1PyM'
|
|
803
803
|
},
|
|
804
804
|
knowledge_continuity: {
|
|
805
805
|
id: 'knowledge_continuity',
|
|
@@ -815,9 +815,9 @@ const ENTERPRISE_ADDONS = {
|
|
|
815
815
|
'onboarding_acceleration',
|
|
816
816
|
'knowledge_transfer_docs'
|
|
817
817
|
],
|
|
818
|
-
stripeProductId: process.env.STRIPE_PRODUCT_ADDON_KNOWLEDGE || '
|
|
819
|
-
stripePriceIdMonthly25: process.env.STRIPE_PRICE_ADDON_KNOWLEDGE_MONTHLY || '
|
|
820
|
-
stripePriceIdAnnual25: process.env.STRIPE_PRICE_ADDON_KNOWLEDGE_ANNUAL || '
|
|
818
|
+
stripeProductId: process.env.STRIPE_PRODUCT_ADDON_KNOWLEDGE || 'prod_TqxmqIjRtKiIbY',
|
|
819
|
+
stripePriceIdMonthly25: process.env.STRIPE_PRICE_ADDON_KNOWLEDGE_MONTHLY || 'price_1StFypQoD4pT2xXu2tNNMma9',
|
|
820
|
+
stripePriceIdAnnual25: process.env.STRIPE_PRICE_ADDON_KNOWLEDGE_ANNUAL || 'price_1StFyqQoD4pT2xXufWC2jgxz'
|
|
821
821
|
}
|
|
822
822
|
};
|
|
823
823
|
|